From 8bd000a57a2b0393a40658957e3d59e82513f6d0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Mar 2026 23:27:30 +0000 Subject: [PATCH 1/2] Initial plan From e8049008ebfab9d876dec185216b7f0ccee3433e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Mar 2026 23:32:21 +0000 Subject: [PATCH 2/2] Add Discord.js bot base with prefix commands and Winston logging Co-authored-by: yhqe <139268606+yhqe@users.noreply.github.com> --- .env.example | 8 + README.md | 122 ++++++- package-lock.json | 609 +++++++++++++++++++++++++++++++++ package.json | 30 ++ src/commands/help.js | 45 +++ src/commands/ping.js | 18 + src/events/messageCreate.js | 37 ++ src/events/ready.js | 14 + src/handlers/commandHandler.js | 51 +++ src/handlers/eventHandler.js | 48 +++ src/index.js | 49 +++ src/utils/logger.js | 47 +++ 12 files changed, 1076 insertions(+), 2 deletions(-) create mode 100644 .env.example create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/commands/help.js create mode 100644 src/commands/ping.js create mode 100644 src/events/messageCreate.js create mode 100644 src/events/ready.js create mode 100644 src/handlers/commandHandler.js create mode 100644 src/handlers/eventHandler.js create mode 100644 src/index.js create mode 100644 src/utils/logger.js diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..94d5f0e --- /dev/null +++ b/.env.example @@ -0,0 +1,8 @@ +# Discord Bot Token β€” get yours at https://discord.com/developers/applications +BOT_TOKEN=your_bot_token_here + +# Command prefix (default: !) +PREFIX=! + +# Guild ID for test server (optional β€” used for dev utilities) +GUILD_ID=your_guild_id_here diff --git a/README.md b/README.md index 7076e02..ea754ce 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,120 @@ -# twitter_v3 -discord moderation bort +# twitter_v3 β€” Discord Moderation Bot + +A Discord.js **v14** bot base using traditional **prefix commands** (no slash commands) and structured **Winston logging**. + +--- + +## Features + +- πŸ“¨ **Prefix command system** β€” commands triggered by a configurable prefix (default `!`) +- πŸ“ **Auto-loading** β€” commands and events are loaded automatically from their folders +- πŸ”– **Aliases** β€” each command can define one or more aliases +- πŸ“ **Winston logging** β€” coloured console output + rolling log files (`logs/combined.log`, `logs/error.log`) +- πŸ›‘οΈ **Global error handling** β€” unhandled rejections and uncaught exceptions are caught and logged + +--- + +## Project Structure + +``` +src/ +β”œβ”€β”€ index.js # Entry point β€” creates the client, loads handlers +β”œβ”€β”€ commands/ +β”‚ β”œβ”€β”€ help.js # !help β€” lists commands or shows command detail +β”‚ └── ping.js # !ping β€” latency check +β”œβ”€β”€ events/ +β”‚ β”œβ”€β”€ ready.js # Fires once when the bot is online +β”‚ └── messageCreate.js # Parses every message for prefix commands +β”œβ”€β”€ handlers/ +β”‚ β”œβ”€β”€ commandHandler.js # Reads src/commands/ and registers commands +β”‚ └── eventHandler.js # Reads src/events/ and registers Discord events +└── utils/ + └── logger.js # Winston logger instance +logs/ # Created automatically at runtime (git-ignored) +``` + +--- + +## Setup + +### 1. Clone & install dependencies + +```bash +git clone https://github.com/yhqe/twitter_v3.git +cd twitter_v3 +npm install +``` + +### 2. Configure environment variables + +```bash +cp .env.example .env +``` + +Open `.env` and fill in: + +| Variable | Required | Description | +|-------------|----------|------------------------------------------------------| +| `BOT_TOKEN` | βœ… Yes | Your bot token from the Discord Developer Portal | +| `PREFIX` | ❌ No | Command prefix (default: `!`) | +| `GUILD_ID` | ❌ No | Test server ID (handy for dev utilities) | + +### 3. Enable the Message Content intent + +Go to your application in the [Discord Developer Portal](https://discord.com/developers/applications), open **Bot β†’ Privileged Gateway Intents**, and enable **Message Content Intent**. This is required for prefix commands. + +### 4. Start the bot + +```bash +# Production +npm start + +# Development (auto-restarts on file changes, Node.js 18+) +npm run dev +``` + +--- + +## Adding Commands + +Create a file in `src/commands/`. The handler will load it automatically on next start. + +```js +'use strict'; + +module.exports = { + name: 'example', // Primary command name (also used as the key) + description: 'An example command.', + aliases: ['ex'], // Optional aliases + usage: 'example ', // Shown in !help example + + async execute(client, message, args) { + await message.reply(`You said: ${args.join(' ')}`); + }, +}; +``` + +## Adding Events + +Create a file in `src/events/`. Set `once: true` if it should fire only once (e.g. `ready`). + +```js +'use strict'; + +const { Events } = require('discord.js'); + +module.exports = { + name: Events.GuildMemberAdd, + + async execute(client, member) { + console.log(`${member.user.tag} joined ${member.guild.name}`); + }, +}; +``` + +--- + +## Logging + +Log files are written to the `logs/` directory (git-ignored). The log level defaults to `info` and can be overridden with the `LOG_LEVEL` environment variable (e.g. `LOG_LEVEL=debug npm start`). + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..eeae5a7 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,609 @@ +{ + "name": "twitter_v3", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "twitter_v3", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "discord.js": "^14.25.1", + "dotenv": "^17.3.1", + "winston": "^3.19.0" + } + }, + "node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.8.tgz", + "integrity": "sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==", + "license": "MIT", + "dependencies": { + "@so-ric/colorspace": "^1.1.6", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, + "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/@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/@so-ric/colorspace": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@so-ric/colorspace/-/colorspace-1.1.6.tgz", + "integrity": "sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==", + "license": "MIT", + "dependencies": { + "color": "^5.0.2", + "text-hex": "1.0.x" + } + }, + "node_modules/@types/node": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", + "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", + "license": "MIT" + }, + "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/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, + "node_modules/color": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/color/-/color-5.0.3.tgz", + "integrity": "sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA==", + "license": "MIT", + "dependencies": { + "color-convert": "^3.1.3", + "color-string": "^2.1.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/color-convert": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-3.1.3.tgz", + "integrity": "sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg==", + "license": "MIT", + "dependencies": { + "color-name": "^2.0.0" + }, + "engines": { + "node": ">=14.6" + } + }, + "node_modules/color-name": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.0.tgz", + "integrity": "sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==", + "license": "MIT", + "engines": { + "node": ">=12.20" + } + }, + "node_modules/color-string": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-2.1.4.tgz", + "integrity": "sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==", + "license": "MIT", + "dependencies": { + "color-name": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/discord-api-types": { + "version": "0.38.42", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.42.tgz", + "integrity": "sha512-qs1kya7S84r5RR8m9kgttywGrmmoHaRifU1askAoi+wkoSefLpZP6aGXusjNw5b0jD3zOg3LTwUa3Tf2iHIceQ==", + "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": "17.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz", + "integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", + "license": "MIT" + }, + "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/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", + "license": "MIT" + }, + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", + "license": "MIT" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", + "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/logform": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", + "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", + "license": "MIT", + "dependencies": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "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/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "license": "MIT", + "dependencies": { + "fn.name": "1.x.x" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", + "license": "MIT" + }, + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/ts-mixer": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.4.tgz", + "integrity": "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==", + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/undici": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.0.tgz", + "integrity": "sha512-jxytwMHhsbdpBXxLAcuu0fzlQeXCNnWdDyRHpvWsUl8vd98UwYdl9YTyn8/HcpcJPC3pwUveefsa3zTxyD/ERg==", + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "license": "MIT" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/winston": { + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.19.0.tgz", + "integrity": "sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA==", + "license": "MIT", + "dependencies": { + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.8", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.7.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.9.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-transport": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", + "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", + "license": "MIT", + "dependencies": { + "logform": "^2.7.0", + "readable-stream": "^3.6.2", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "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 + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..7855c06 --- /dev/null +++ b/package.json @@ -0,0 +1,30 @@ +{ + "name": "twitter_v3", + "version": "1.0.0", + "description": "discord moderation bort", + "main": "src/index.js", + "scripts": { + "start": "node src/index.js", + "dev": "node --watch src/index.js" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/yhqe/twitter_v3.git" + }, + "keywords": [], + "author": "", + "license": "ISC", + "type": "commonjs", + "bugs": { + "url": "https://github.com/yhqe/twitter_v3/issues" + }, + "homepage": "https://github.com/yhqe/twitter_v3#readme", + "dependencies": { + "discord.js": "^14.25.1", + "dotenv": "^17.3.1", + "winston": "^3.19.0" + }, + "overrides": { + "undici": ">=6.23.0" + } +} diff --git a/src/commands/help.js b/src/commands/help.js new file mode 100644 index 0000000..e617777 --- /dev/null +++ b/src/commands/help.js @@ -0,0 +1,45 @@ +'use strict'; + +module.exports = { + name: 'help', + description: 'Lists all available commands or shows info about a specific one.', + aliases: ['h', 'commands'], + usage: 'help [command]', + + execute(client, message, args) { + const prefix = process.env.PREFIX || '!'; + + // Show info for a specific command + if (args.length > 0) { + const name = args[0].toLowerCase(); + const command = client.commands.get(name); + + if (!command) { + return message.reply(`No command found for \`${name}\`.`); + } + + const lines = [`**${prefix}${command.name}**`]; + if (command.description) lines.push(`> ${command.description}`); + if (command.aliases?.length) + lines.push(`> **Aliases:** ${command.aliases.map((a) => `\`${prefix}${a}\``).join(', ')}`); + if (command.usage) + lines.push(`> **Usage:** \`${prefix}${command.usage}\``); + + return message.reply(lines.join('\n')); + } + + // Build a deduplicated command list (ignore aliases pointing to the same command) + const seen = new Set(); + const commandList = []; + + for (const command of client.commands.values()) { + if (seen.has(command.name)) continue; + seen.add(command.name); + commandList.push(`\`${prefix}${command.name}\` β€” ${command.description || 'No description.'}`); + } + + return message.reply( + `**Available commands:**\n${commandList.join('\n')}\n\nUse \`${prefix}help \` for more details.`, + ); + }, +}; diff --git a/src/commands/ping.js b/src/commands/ping.js new file mode 100644 index 0000000..64e0635 --- /dev/null +++ b/src/commands/ping.js @@ -0,0 +1,18 @@ +'use strict'; + +module.exports = { + name: 'ping', + description: 'Replies with Pong! and shows the bot latency.', + aliases: ['pong'], + usage: 'ping', + + async execute(client, message) { + const sent = await message.reply('Pinging…'); + const latency = sent.createdTimestamp - message.createdTimestamp; + const apiLatency = Math.round(client.ws.ping); + + await sent.edit( + `πŸ“ Pong!\n> Bot latency: **${latency}ms** | API latency: **${apiLatency}ms**`, + ); + }, +}; diff --git a/src/events/messageCreate.js b/src/events/messageCreate.js new file mode 100644 index 0000000..a5b1ff8 --- /dev/null +++ b/src/events/messageCreate.js @@ -0,0 +1,37 @@ +'use strict'; + +const { Events } = require('discord.js'); +const logger = require('../utils/logger'); + +module.exports = { + name: Events.MessageCreate, + + async execute(client, message) { + // Ignore bots and messages without the prefix + if (message.author.bot) return; + + const prefix = process.env.PREFIX || '!'; + if (!message.content.startsWith(prefix)) return; + + // Parse command name and arguments + const args = message.content.slice(prefix.length).trim().split(/\s+/); + const commandName = args.shift().toLowerCase(); + + const command = client.commands.get(commandName); + if (!command) return; + + logger.info( + `[Command] "${commandName}" used by ${message.author.tag} (${message.author.id})` + + ` in ${message.guild ? `#${message.channel.name} (${message.guild.name})` : 'DMs'}`, + ); + + try { + await command.execute(client, message, args); + } catch (err) { + logger.error(`[Command] Error executing "${commandName}":`, err); + await message + .reply('An error occurred while running that command.') + .catch(() => null); + } + }, +}; diff --git a/src/events/ready.js b/src/events/ready.js new file mode 100644 index 0000000..3279ccb --- /dev/null +++ b/src/events/ready.js @@ -0,0 +1,14 @@ +'use strict'; + +const { Events } = require('discord.js'); +const logger = require('../utils/logger'); + +module.exports = { + name: Events.ClientReady, + once: true, + + execute(client) { + logger.info(`[Ready] Logged in as ${client.user.tag} (${client.user.id})`); + logger.info(`[Ready] Serving ${client.guilds.cache.size} guild(s).`); + }, +}; diff --git a/src/handlers/commandHandler.js b/src/handlers/commandHandler.js new file mode 100644 index 0000000..0c54833 --- /dev/null +++ b/src/handlers/commandHandler.js @@ -0,0 +1,51 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const logger = require('../utils/logger'); + +/** + * Loads all command files from src/commands and registers them on the client. + * Each command file must export: { name, description, aliases?, usage?, execute } + * + * @param {import('discord.js').Client} client + */ +function loadCommands(client) { + const commandsPath = path.join(__dirname, '..', 'commands'); + const commandFiles = fs + .readdirSync(commandsPath) + .filter((file) => file.endsWith('.js')); + + let loaded = 0; + + for (const file of commandFiles) { + try { + const command = require(path.join(commandsPath, file)); + + if (!command.name || typeof command.execute !== 'function') { + logger.warn( + `[CommandHandler] Skipping ${file}: missing "name" or "execute".`, + ); + continue; + } + + client.commands.set(command.name.toLowerCase(), command); + + // Register aliases if provided + if (Array.isArray(command.aliases)) { + for (const alias of command.aliases) { + client.commands.set(alias.toLowerCase(), command); + } + } + + logger.debug(`[CommandHandler] Loaded command: ${command.name}`); + loaded++; + } catch (err) { + logger.error(`[CommandHandler] Failed to load ${file}:`, err); + } + } + + logger.info(`[CommandHandler] ${loaded} command(s) loaded.`); +} + +module.exports = { loadCommands }; diff --git a/src/handlers/eventHandler.js b/src/handlers/eventHandler.js new file mode 100644 index 0000000..958ceb0 --- /dev/null +++ b/src/handlers/eventHandler.js @@ -0,0 +1,48 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const logger = require('../utils/logger'); + +/** + * Loads all event files from src/events and registers them on the client. + * Each event file must export: { name, once?, execute } + * + * @param {import('discord.js').Client} client + */ +function loadEvents(client) { + const eventsPath = path.join(__dirname, '..', 'events'); + const eventFiles = fs + .readdirSync(eventsPath) + .filter((file) => file.endsWith('.js')); + + let loaded = 0; + + for (const file of eventFiles) { + try { + const event = require(path.join(eventsPath, file)); + + if (!event.name || typeof event.execute !== 'function') { + logger.warn( + `[EventHandler] Skipping ${file}: missing "name" or "execute".`, + ); + continue; + } + + if (event.once) { + client.once(event.name, (...args) => event.execute(client, ...args)); + } else { + client.on(event.name, (...args) => event.execute(client, ...args)); + } + + logger.debug(`[EventHandler] Registered event: ${event.name}`); + loaded++; + } catch (err) { + logger.error(`[EventHandler] Failed to load ${file}:`, err); + } + } + + logger.info(`[EventHandler] ${loaded} event(s) registered.`); +} + +module.exports = { loadEvents }; diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..c536763 --- /dev/null +++ b/src/index.js @@ -0,0 +1,49 @@ +'use strict'; + +require('dotenv').config(); + +const { Client, Collection, GatewayIntentBits } = require('discord.js'); +const logger = require('./utils/logger'); +const { loadCommands } = require('./handlers/commandHandler'); +const { loadEvents } = require('./handlers/eventHandler'); + +// Validate required environment variables +if (!process.env.BOT_TOKEN) { + logger.error( + 'BOT_TOKEN is not set. Copy .env.example to .env and fill in your token.', + ); + process.exit(1); +} + +// Create the Discord client with the intents needed for prefix commands +const client = new Client({ + intents: [ + GatewayIntentBits.Guilds, + GatewayIntentBits.GuildMessages, + GatewayIntentBits.MessageContent, // Required to read message content for prefix commands + GatewayIntentBits.DirectMessages, + ], +}); + +// Attach a Collection to store commands +client.commands = new Collection(); + +// Load commands and events +loadCommands(client); +loadEvents(client); + +// Global error handling +process.on('unhandledRejection', (err) => { + logger.error('[Process] Unhandled rejection:', err); +}); + +process.on('uncaughtException', (err) => { + logger.error('[Process] Uncaught exception:', err); + process.exit(1); +}); + +// Connect to Discord +client.login(process.env.BOT_TOKEN).catch((err) => { + logger.error('[Login] Failed to log in:', err); + process.exit(1); +}); diff --git a/src/utils/logger.js b/src/utils/logger.js new file mode 100644 index 0000000..669471d --- /dev/null +++ b/src/utils/logger.js @@ -0,0 +1,47 @@ +'use strict'; + +const { createLogger, format, transports } = require('winston'); +const { combine, timestamp, printf, colorize, errors } = format; +const path = require('path'); +const fs = require('fs'); + +// Ensure logs directory exists +const logsDir = path.join(__dirname, '..', '..', 'logs'); +if (!fs.existsSync(logsDir)) { + fs.mkdirSync(logsDir, { recursive: true }); +} + +const logFormat = printf(({ level, message, timestamp: ts, stack }) => { + return `[${ts}] [${level}]: ${stack || message}`; +}); + +const logger = createLogger({ + level: process.env.LOG_LEVEL || 'info', + format: combine( + timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), + errors({ stack: true }), + logFormat, + ), + transports: [ + // Human-readable coloured output in the console + new transports.Console({ + format: combine( + colorize({ all: true }), + timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), + errors({ stack: true }), + logFormat, + ), + }), + // All logs at info level and above written to combined.log + new transports.File({ + filename: path.join(logsDir, 'combined.log'), + }), + // Error logs written to a dedicated file + new transports.File({ + filename: path.join(logsDir, 'error.log'), + level: 'error', + }), + ], +}); + +module.exports = logger;