diff --git a/.env.example b/.env.example index c68b7961e2..c2eae4ad28 100644 --- a/.env.example +++ b/.env.example @@ -3,20 +3,10 @@ # Discord Bot Configuration DISCORD_TOKEN=your_discord_bot_token_here CLIENT_ID=your_discord_client_id_here -<<<<<<< HEAD -GUILD_ID=your_discord_guild_id_here -OWNER_IDS=your_discord_id_here (optional) - -# Optional — enable slash commands in every server the bot is invited to. -# Leave unset or false for single-server setup (recommended for most users). -MULTI_GUILD=false - -======= # Your server ID (used in the setup tutorial; slash commands register globally via CLIENT_ID). GUILD_ID=your_discord_guild_id_here OWNER_IDS=your_discord_id_here (optional) ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) # Bot Runtime Configuration NODE_ENV=production LOG_LEVEL=warn @@ -61,18 +51,6 @@ BACKUP_DIR=./backups BACKUP_RETENTION_DAYS=14 POSTGRES_RESTORE_URL= -<<<<<<< HEAD -# Music (Lavalink + Riffy) — requires a Lavalink v4 server -# Local dev: run Lavalink via docker compose (see README), then use localhost below. -# Docker Compose bot service uses LAVALINK_HOST=lavalink instead. -LAVALINK_HOST=localhost -LAVALINK_PORT=2333 -LAVALINK_PASSWORD=youshallnotpass -LAVALINK_SECURE=false -LAVALINK_NAME=Main -# Optional: override nodes as JSON array (for multiple Lavalink nodes) -# LAVALINK_NODES=[{"host":"lavalink","port":2333,"password":"youshallnotpass","secure":false,"name":"Main"}] -======= # Music (Lavalink + Riffy) — requires Lavalink v4 nodes # Default: loads public v4 SSL nodes from lavalink/nodes.json # Source: https://lavalink.darrennathanael.com/SSL/Lavalink-SSL/ @@ -85,7 +63,6 @@ LAVALINK_NAME=Main # LAVALINK_PASSWORD=youshallnotpass # LAVALINK_SECURE=false # LAVALINK_NAME=Main ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) LAVALINK_SEARCH_PLATFORM=ytmsearch LAVALINK_REST_VERSION=v4 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml deleted file mode 100644 index ed3029ba48..0000000000 --- a/.github/workflows/tests.yml +++ /dev/null @@ -1,29 +0,0 @@ -name: Tests - -on: - pull_request: - push: - branches: - - main - - master - - mitchwork - -jobs: - test: - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - cache: npm - - - name: Install dependencies - run: npm ci - - - name: Run tests - run: npm test diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5e9ee49f36..0b7e3d974b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,10 +7,6 @@ Thank you for your interest in contributing to TitanBot! This guide covers local - Bug fixes and reliability improvements - New commands or enhancements to existing features - Documentation updates -<<<<<<< HEAD -- Test coverage for behavior that is easy to regress -======= ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) Before starting large features, open an issue or discuss in the [support server](https://discord.gg/8kJBYhTGW9) so we can align on scope and avoid duplicate work. @@ -18,11 +14,7 @@ Before starting large features, open an issue or discuss in the [support server] ### Prerequisites -<<<<<<< HEAD -- **Node.js 18+** (CI uses Node 20) -======= - **Node.js 20+** (Docker and CI use Node 20) ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) - **PostgreSQL** (recommended for development; the bot can fall back to in-memory storage if PostgreSQL is unavailable) - A **Discord bot application** with the intents listed in [README.md](README.md#required-bot-intents) @@ -53,12 +45,7 @@ For Docker-based setup, see [README.md](README.md#docker-deployment-recommended) 1. **Fork the repository** and create a branch from `main`. 2. **Make focused changes** — one logical change per pull request when possible. -<<<<<<< HEAD -3. **Run tests** before opening a PR (see below). -4. **Open a pull request** with a clear description of what changed and why. -======= 3. **Open a pull request** with a clear description of what changed and why. ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) Use descriptive branch names, for example: @@ -66,25 +53,6 @@ Use descriptive branch names, for example: - `feat/economy-shop-filter` - `docs/contributing-guide` -<<<<<<< HEAD -## Running Tests - -Tests use Node's built-in test runner: - -```bash -npm test -``` - -Test files live in `tests/` and follow the `*.test.js` naming pattern. When adding or changing behavior, add or update tests for: - -- Permission checks and command access rules -- Parsing, validation, and utility logic -- UI/panel builders and status helpers - -CI runs `npm test` on every pull request and on pushes to `main` and `master`. A separate workflow also validates database migrations against PostgreSQL when migration-related code changes. - -======= ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) ## Database & Migrations TitanBot uses PostgreSQL as its primary store. If PostgreSQL is unreachable at startup, the bot can operate in a **degraded in-memory mode** — but that mode is not suitable for production and should not be the only way you test persistence-related changes. @@ -105,11 +73,7 @@ Test features that read or write guild data with **both** PostgreSQL and the mem - **Match existing style** — ES modules (`import`/`export`), async/await, and the conventions used in neighboring files. - **Handle errors gracefully** — catch failures, log with context, and send user-friendly embed replies where appropriate. -<<<<<<< HEAD -- **Avoid breaking guild isolation** — guild-specific config and data must stay scoped per server, especially when `MULTI_GUILD=true`. -======= - **Avoid breaking guild isolation** — guild-specific config and data must stay scoped per server (`guild:{guildId}:...` keys, `interaction.guildId`). ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) - **Keep changes minimal** — prefer extending existing utilities and services over duplicating logic. - **Document user-facing behavior** — update README.md when setup steps or configuration change; mention new env vars in `.env.example`. diff --git a/Dockerfile b/Dockerfile index 17120758e4..03b7bd00f1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,25 +1,5 @@ FROM node:20-alpine -<<<<<<< HEAD -# Create app directory -WORKDIR /usr/src/app - -# Install app dependencies -# A wildcard is used to ensure both package.json AND package-lock.json are copied -COPY package*.json ./ - -# Install only production dependencies -RUN npm ci --omit=dev - -# Bundle app source -COPY . . - -# Expose the health check port from src/app.js -EXPOSE 3000 - -# Start the bot -CMD [ "npm", "start" ] -======= WORKDIR /usr/src/app ENV NODE_ENV=production @@ -32,4 +12,3 @@ COPY . . EXPOSE 3000 CMD ["npm", "start"] ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) diff --git a/README.md b/README.md index e2f8e063db..700ac20739 100644 --- a/README.md +++ b/README.md @@ -109,16 +109,6 @@ TitanBot is fully containerized for easy deployment. ``` 2. **Configure environment variables:** -<<<<<<< HEAD - Create a `.env` file from `.env.example` and fill in your bot details and PostgreSQL credentials. - -3. **Start the containers:** - ```bash - docker-compose up -d - ``` - -This will start the bot, PostgreSQL, and Lavalink (when music is enabled). -======= ```bash cp .env.example .env ``` @@ -136,31 +126,21 @@ This will start the bot, PostgreSQL, and Lavalink (when music is enabled). ``` This starts the bot and PostgreSQL. The compose file sets `POSTGRES_SSL=false` and `AUTO_MIGRATE=true` for the bundled database. Music uses public Lavalink v4 nodes from `lavalink/nodes.json` by default. ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) ### Music Music uses [Lavalink v4](https://github.com/lavalink-devs/Lavalink) via [Riffy](https://github.com/riffy-rb/riffy), similar to [Musicify](https://github.com/codebymitch/Musicify). -<<<<<<< HEAD -1. Set in `.env`: -======= 1. By default, the bot loads multiple public v4 SSL nodes from [`lavalink/nodes.json`](lavalink/nodes.json) (sourced from [lavalink.darrennathanael.com](https://lavalink.darrennathanael.com/SSL/Lavalink-SSL/)). Edit that file to add or remove nodes. 2. To self-host Lavalink instead, run `docker compose --profile local-lavalink up -d` and set single-node env vars in `.env`: ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) ```env LAVALINK_HOST=lavalink LAVALINK_PORT=2333 LAVALINK_PASSWORD=youshallnotpass LAVALINK_SECURE=false ``` -<<<<<<< HEAD -2. With Docker Compose, Lavalink is included automatically when you `docker compose up`. -3. On Railway, deploy Lavalink separately or as another service and point `LAVALINK_HOST` at the private hostname. -======= Remove or rename `lavalink/nodes.json` so the bot falls back to those env vars. 3. Override nodes inline with `LAVALINK_NODES` (JSON array) or point at another file with `LAVALINK_NODES_FILE`. ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) 4. Use `/play ` from a voice channel, or `/join` to connect without playing. Prefix shortcuts: `join`, `np`, `leave`, `pause`, `resume`, `skip`, `stop`, `volume <0-100>`, or `music `. Use `/nowplaying` and `/queue` for status; `/music` for loop, shuffle, seek, and other controls. ### Using GitHub Container Registry @@ -175,11 +155,7 @@ docker pull ghcr.io/codebymitch/titanbot:main ## Manual Installation Steps ### Prerequisites -<<<<<<< HEAD -- Node.js 18.0.0 or higher -======= - Node.js 20.10.0 or higher ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) - PostgreSQL server (recommended) or memory storage fallback - Discord bot application with proper intents @@ -236,20 +212,6 @@ docker pull ghcr.io/codebymitch/titanbot:main This gives clear startup/online status messages while keeping logs simple for non-technical operators. If port `3000` is busy, the bot tries the next available ports automatically (up to `PORT_RETRY_ATTEMPTS`). -<<<<<<< HEAD -### Running in multiple servers (optional) - -Most users run TitanBot on a **single server** with `GUILD_ID` set (default tutorial setup). If you want commands to work in **every server** the bot is invited to, opt in with: - -```env -MULTI_GUILD=true -``` - -Notes for multi-server mode: -- `GUILD_ID` is not used for command registration when `MULTI_GUILD=true` (you can leave it set or remove it) -- Global slash commands may take up to about an hour to propagate on first deploy -- Each server still has **isolated** config, economy, tickets, leveling, and other data -======= ### Multiple servers Slash commands are registered **globally** on startup (via `CLIENT_ID`), so the bot works in every server it is invited to. `GUILD_ID` stays in the tutorial `.env` for setup steps but is not used for command registration. @@ -257,7 +219,6 @@ Slash commands are registered **globally** on startup (via `CLIENT_ID`), so the Notes: - Global slash commands may take up to about an hour to propagate on first deploy - Each server has **isolated** data: config, economy, tickets, leveling, dashboards, warnings, etc. (all keys are scoped as `guild:{guildId}:...`) ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) - In the [Discord Developer Portal](https://discord.com/developers/applications), ensure your bot is not restricted to a single guild if you plan to invite it elsewhere - Generate an OAuth2 invite URL from the [Discord Developer Portal](https://discord.com/developers/applications) (OAuth2 → URL Generator, scopes: `bot` and `applications.commands`) @@ -279,15 +240,12 @@ Notes: ```bash npm start ``` -<<<<<<< HEAD -======= > **Note on database migrations:** Schema tables and legacy key migrations run > **automatically on startup**, so` managed hosts like **Railway** need no manual > migration step — just deploy/restart. To disable auto-migration set > `AUTO_MIGRATE=false`. You can still run a manual key migration locally with > `node scripts/migrate-keys.js --dry-run` (preview) or `node scripts/migrate-keys.js`. ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) ## Required Bot Intents diff --git a/docker-compose.yml b/docker-compose.yml index 0f16214f2d..9de8fec548 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,30 +3,13 @@ services: build: . container_name: titanbot restart: unless-stopped -<<<<<<< HEAD -======= env_file: - .env ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) environment: - NODE_ENV=production - DISCORD_TOKEN=${DISCORD_TOKEN} - CLIENT_ID=${CLIENT_ID} - GUILD_ID=${GUILD_ID} -<<<<<<< HEAD - - MULTI_GUILD=${MULTI_GUILD:-false} - - DATABASE_URL=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB} - - LAVALINK_HOST=lavalink - - LAVALINK_PORT=2333 - - LAVALINK_PASSWORD=${LAVALINK_PASSWORD:-youshallnotpass} - - LAVALINK_SECURE=false - - PORT=3000 - depends_on: - db: - condition: service_healthy - lavalink: - condition: service_started -======= - DATABASE_URL=postgres://${POSTGRES_USER:-titanbot}:${POSTGRES_PASSWORD:-password}@db:5432/${POSTGRES_DB:-titanbot} - POSTGRES_URL=postgres://${POSTGRES_USER:-titanbot}:${POSTGRES_PASSWORD:-password}@db:5432/${POSTGRES_DB:-titanbot} - POSTGRES_HOST=db @@ -51,16 +34,12 @@ services: timeout: 5s retries: 3 start_period: 40s ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) networks: - titan-network lavalink: -<<<<<<< HEAD -======= profiles: - local-lavalink ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) image: ghcr.io/lavalink-devs/lavalink:4 container_name: titanbot-lavalink restart: unless-stopped diff --git a/package-lock.json b/package-lock.json index 2eafbb1fdc..b11b7cdec6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,12 +7,6 @@ "": { "name": "titanbot", "version": "2.1.0", -<<<<<<< HEAD -======= - "engines": { - "node": ">=20.10.0" - }, ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) "dependencies": { "@discordjs/rest": "^2.6.1", "axios": "^1.15.2", @@ -26,14 +20,8 @@ "winston-daily-rotate-file": "^5.0.0", "zod": "^3.25.76" }, -<<<<<<< HEAD "engines": { - "node": ">=18.0.0" -======= - "overrides": { - "form-data": "^4.0.6", - "undici": "^6.27.0" ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) + "node": ">=20.10.0" } }, "node_modules/@asamuzakjp/css-color": { @@ -229,11 +217,7 @@ "discord-api-types": "^0.38.40", "magic-bytes.js": "^1.13.0", "tslib": "^2.6.3", -<<<<<<< HEAD - "undici": "6.24.1" -======= "undici": "6.27.0" ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) }, "engines": { "node": ">=18" @@ -621,11 +605,7 @@ "lodash.snakecase": "4.1.1", "magic-bytes.js": "^1.13.0", "tslib": "^2.6.3", -<<<<<<< HEAD - "undici": "6.24.1" -======= "undici": "6.27.0" ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) }, "engines": { "node": ">=18" @@ -863,15 +843,9 @@ } }, "node_modules/form-data": { -<<<<<<< HEAD - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", -======= "version": "4.0.6", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -1894,15 +1868,9 @@ } }, "node_modules/undici": { -<<<<<<< HEAD - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.24.1.tgz", - "integrity": "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==", -======= "version": "6.27.0", "resolved": "https://registry.npmjs.org/undici/-/undici-6.27.0.tgz", "integrity": "sha512-YmfV3YnEDzXRC5lZ2jWtWWHKGUm1zIt8AhesR1tens+HTNv+YZlN/dp6G727LOvMJ8xjP9Be7Y2Sdr96LDm+pg==", ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) "engines": { "node": ">=18.17" } @@ -2200,11 +2168,7 @@ "discord-api-types": "^0.38.40", "magic-bytes.js": "^1.13.0", "tslib": "^2.6.3", -<<<<<<< HEAD - "undici": "6.24.1" -======= - "undici": "6.27.0" ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) + "undici": "^6.27.0" } }, "@discordjs/util": { @@ -2318,7 +2282,7 @@ "integrity": "sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==", "requires": { "follow-redirects": "^1.16.0", - "form-data": "^4.0.5", + "form-data": "^4.0.6", "https-proxy-agent": "^5.0.1", "proxy-from-env": "^2.1.0" } @@ -2483,11 +2447,7 @@ "lodash.snakecase": "4.1.1", "magic-bytes.js": "^1.13.0", "tslib": "^2.6.3", -<<<<<<< HEAD - "undici": "6.24.1" -======= - "undici": "6.27.0" ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) + "undici": "^6.27.0" }, "dependencies": { "@discordjs/collection": { @@ -2653,15 +2613,9 @@ "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==" }, "form-data": { -<<<<<<< HEAD - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", -======= "version": "4.0.6", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) "requires": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -3158,7 +3112,7 @@ "integrity": "sha512-rFVIERWq0FeKINDOTKobUamrvX9m4xdP8NTJfA8thYbLrkOfK6vgZ6UlTjHeBippjFq6xlPv7OGVT2INl6Y0hQ==", "requires": { "jsdom": "^26.0.0", - "undici": ">=6.11.1", + "undici": "^6.27.0", "ws": "^8.16.0" } }, @@ -3380,15 +3334,9 @@ } }, "undici": { -<<<<<<< HEAD - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.24.1.tgz", - "integrity": "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==" -======= "version": "6.27.0", "resolved": "https://registry.npmjs.org/undici/-/undici-6.27.0.tgz", "integrity": "sha512-YmfV3YnEDzXRC5lZ2jWtWWHKGUm1zIt8AhesR1tens+HTNv+YZlN/dp6G727LOvMJ8xjP9Be7Y2Sdr96LDm+pg==" ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) }, "undici-types": { "version": "7.24.6", diff --git a/package.json b/package.json index f20da53b03..2430aec17b 100644 --- a/package.json +++ b/package.json @@ -6,18 +6,11 @@ "type": "module", "scripts": { "start": "node src/app.js", -<<<<<<< HEAD - "test": "node --test tests/**/*.test.js", - "migrate": "node scripts/migrate.js apply", - "migrate:check": "node scripts/migrate.js check", - "migrate:status": "node scripts/migrate.js status", -======= "migrate": "node scripts/migrate.js apply", "migrate:check": "node scripts/migrate.js check", "migrate:status": "node scripts/migrate.js status", "migrate:keys": "node scripts/migrate-keys.js", "migrate:keys:dry": "node scripts/migrate-keys.js --dry-run", ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) "backup:db": "node scripts/backup.js", "restore:db": "node scripts/restore.js", "backup:drill": "node scripts/restore-drill.js" @@ -36,14 +29,10 @@ "riffy": "1.0.12" }, "engines": { -<<<<<<< HEAD - "node": ">=18.0.0" -======= "node": ">=20.10.0" }, "overrides": { "form-data": "^4.0.6", "undici": "^6.27.0" ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } } diff --git a/scripts/migrate.js b/scripts/migrate.js index 5ef61169d5..27dd907d93 100644 --- a/scripts/migrate.js +++ b/scripts/migrate.js @@ -3,17 +3,11 @@ import dotenv from 'dotenv'; import path from 'path'; import { fileURLToPath } from 'url'; import { logger } from '../src/utils/logger.js'; -<<<<<<< HEAD -import { EXPECTED_SCHEMA_LABEL, EXPECTED_SCHEMA_VERSION } from '../src/config/schemaVersion.js'; -======= import { EXPECTED_SCHEMA_LABEL, EXPECTED_SCHEMA_VERSION } from '../src/config/database/schemaVersion.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) const __dirname = path.dirname(fileURLToPath(import.meta.url)); dotenv.config({ path: path.join(__dirname, '..', '.env') }); -<<<<<<< HEAD -======= // Imported after dotenv.config so resolveSslConfig sees the loaded env vars. const { resolveSslConfig } = await import('../src/config/database/postgres.js'); // The schema is the single source of truth shared with the runtime auto-create @@ -26,16 +20,11 @@ const { } = await import('../src/utils/database/schema.js'); const { assertAllowlistedIdentifier, quoteIdentifier } = await import('../src/utils/sqlIdentifiers.js'); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) const { Pool } = pg; const pool = new Pool({ connectionString: process.env.POSTGRES_URL, -<<<<<<< HEAD - ssl: false, -======= ssl: resolveSslConfig(), ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) }); const migrationTable = process.env.POSTGRES_MIGRATION_TABLE || 'schema_migrations'; @@ -82,142 +71,9 @@ const getCurrentSchemaVersion = async (client) => { const createTables = async (client) => { logger.info('📊 Creating database tables...'); -<<<<<<< HEAD - const tables = [ - - `CREATE TABLE IF NOT EXISTS guild_configs ( - guild_id VARCHAR(255) PRIMARY KEY, - prefix VARCHAR(10) DEFAULT '!', - welcome_channel VARCHAR(255), - welcome_message TEXT, - welcome_enabled BOOLEAN DEFAULT false, - autorole_ids TEXT[] DEFAULT '{}', - modlog_channel VARCHAR(255), - settings JSONB DEFAULT '{}', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - )`, - - `CREATE TABLE IF NOT EXISTS user_levels ( - id SERIAL PRIMARY KEY, - user_id VARCHAR(255) NOT NULL, - guild_id VARCHAR(255) NOT NULL, - xp BIGINT DEFAULT 0, - level INT DEFAULT 1, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - UNIQUE(user_id, guild_id) - )`, - - `CREATE TABLE IF NOT EXISTS user_economy ( - id SERIAL PRIMARY KEY, - user_id VARCHAR(255) NOT NULL, - guild_id VARCHAR(255) NOT NULL, - balance BIGINT DEFAULT 0, - bank BIGINT DEFAULT 0, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - UNIQUE(user_id, guild_id) - )`, - - `CREATE TABLE IF NOT EXISTS birthdays ( - id SERIAL PRIMARY KEY, - user_id VARCHAR(255) NOT NULL, - guild_id VARCHAR(255) NOT NULL, - month INT NOT NULL, - day INT NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - UNIQUE(user_id, guild_id) - )`, - - `CREATE TABLE IF NOT EXISTS tickets ( - id SERIAL PRIMARY KEY, - ticket_id VARCHAR(255) UNIQUE NOT NULL, - guild_id VARCHAR(255) NOT NULL, - user_id VARCHAR(255) NOT NULL, - channel_id VARCHAR(255) NOT NULL, - status VARCHAR(50) DEFAULT 'open', - subject TEXT, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - closed_at TIMESTAMP - )`, - - `CREATE TABLE IF NOT EXISTS giveaways ( - id SERIAL PRIMARY KEY, - giveaway_id VARCHAR(255) UNIQUE NOT NULL, - guild_id VARCHAR(255) NOT NULL, - channel_id VARCHAR(255) NOT NULL, - message_id VARCHAR(255) NOT NULL, - prize TEXT NOT NULL, - winners_count INT DEFAULT 1, - ended BOOLEAN DEFAULT false, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - ends_at TIMESTAMP NOT NULL - )`, - - `CREATE TABLE IF NOT EXISTS giveaway_entries ( - id SERIAL PRIMARY KEY, - giveaway_id INTEGER NOT NULL, - user_id VARCHAR(255) NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - UNIQUE(giveaway_id, user_id), - FOREIGN KEY(giveaway_id) REFERENCES giveaways(id) ON DELETE CASCADE - )`, - - `CREATE TABLE IF NOT EXISTS reaction_roles ( - id SERIAL PRIMARY KEY, - guild_id VARCHAR(255) NOT NULL, - channel_id VARCHAR(255) NOT NULL, - message_id VARCHAR(255) NOT NULL, - emoji VARCHAR(255) NOT NULL, - role_id VARCHAR(255) NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - UNIQUE(message_id, emoji) - )`, - - `CREATE TABLE IF NOT EXISTS welcome_system ( - id SERIAL PRIMARY KEY, - guild_id VARCHAR(255) UNIQUE NOT NULL, - channel_id VARCHAR(255), - message TEXT, - enabled BOOLEAN DEFAULT false, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - )`, - - `CREATE TABLE IF NOT EXISTS counters ( - id SERIAL PRIMARY KEY, - guild_id VARCHAR(255) UNIQUE NOT NULL, - user_count_channel VARCHAR(255), - bot_count_channel VARCHAR(255), - online_count_channel VARCHAR(255), - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - )`, - - `CREATE TABLE IF NOT EXISTS audit_logs ( - id SERIAL PRIMARY KEY, - guild_id VARCHAR(255) NOT NULL, - user_id VARCHAR(255), - action VARCHAR(255) NOT NULL, - target_id VARCHAR(255), - reason TEXT, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - )` - ]; - - for (const table of tables) { - try { - await client.query(table); -======= for (const statement of tableStatements) { try { await client.query(statement); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } catch (error) { logger.error(`❌ Error creating table: ${error.message}`); throw error; @@ -230,23 +86,9 @@ const createTables = async (client) => { const createIndexes = async (client) => { logger.info('📈 Creating indexes...'); -<<<<<<< HEAD - const indexes = [ - 'CREATE INDEX IF NOT EXISTS idx_user_levels_guild ON user_levels(guild_id)', - 'CREATE INDEX IF NOT EXISTS idx_user_economy_guild ON user_economy(guild_id)', - 'CREATE INDEX IF NOT EXISTS idx_tickets_guild ON tickets(guild_id)', - 'CREATE INDEX IF NOT EXISTS idx_giveaways_guild ON giveaways(guild_id)', - 'CREATE INDEX IF NOT EXISTS idx_audit_logs_guild ON audit_logs(guild_id)', - ]; - - for (const index of indexes) { - try { - await client.query(index); -======= for (const statement of indexStatements) { try { await client.query(statement); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } catch (error) { logger.error(`❌ Error creating index: ${error.message}`); throw error; @@ -259,67 +101,6 @@ const createIndexes = async (client) => { const createTriggers = async (client) => { logger.info('⏰ Setting up automatic timestamps...'); -<<<<<<< HEAD - const triggers = [ - { - table: 'guild_configs', - name: 'update_guild_configs_timestamp' - }, - { - table: 'user_levels', - name: 'update_user_levels_timestamp' - }, - { - table: 'user_economy', - name: 'update_user_economy_timestamp' - }, - { - table: 'birthdays', - name: 'update_birthdays_timestamp' - }, - { - table: 'tickets', - name: 'update_tickets_timestamp' - }, - { - table: 'giveaways', - name: 'update_giveaways_timestamp' - }, - { - table: 'reaction_roles', - name: 'update_reaction_roles_timestamp' - }, - { - table: 'welcome_system', - name: 'update_welcome_system_timestamp' - }, - { - table: 'counters', - name: 'update_counters_timestamp' - } - ]; - - for (const { table, name } of triggers) { - try { - - await client.query(` - CREATE OR REPLACE FUNCTION update_timestamp_${table}() - RETURNS TRIGGER AS $$ - BEGIN - NEW.updated_at = CURRENT_TIMESTAMP; - RETURN NEW; - END; - $$ LANGUAGE plpgsql; - `); - - await client.query(` - DROP TRIGGER IF EXISTS ${name} ON ${table}; - CREATE TRIGGER ${name} - BEFORE UPDATE ON ${table} - FOR EACH ROW - EXECUTE FUNCTION update_timestamp_${table}(); - `); -======= await client.query(UPDATE_TIMESTAMP_FUNCTION); const allowedTriggerIdentifiers = new Set(triggerDefinitions.map((trigger) => trigger.name)); @@ -340,7 +121,6 @@ const createTriggers = async (client) => { BEFORE UPDATE ON ${safeTable} FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();` ); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } catch (error) { logger.error(`❌ Error creating trigger for ${table}: ${error.message}`); throw error; @@ -438,8 +218,4 @@ if (command === 'apply') { } else { logger.error(`Unknown command: ${command}. Use one of: apply, check, status`); process.exit(1); -<<<<<<< HEAD -} -======= } ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) diff --git a/src/app.js b/src/app.js index 3017c5e55e..693cd228ea 100644 --- a/src/app.js +++ b/src/app.js @@ -6,29 +6,17 @@ import cron from 'node-cron'; import config from './config/application.js'; import { initializeDatabase } from './utils/database.js'; -<<<<<<< HEAD -import { getGuildConfig } from './services/guildConfig.js'; -======= import { getGuildConfig } from './services/config/guildConfig.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) import { getServerCounters, saveServerCounters, updateCounter } from './services/serverstatsService.js'; import { logger, startupLog, shutdownLog } from './utils/logger.js'; import { checkBirthdays } from './services/birthdayService.js'; import { checkGiveaways } from './services/giveawayService.js'; -<<<<<<< HEAD -import { loadCommands, registerCommands as registerSlashCommands } from './handlers/commandLoader.js'; -import { initializeMusic } from './services/music/riffySetup.js'; -import { shutdownMusic } from './services/music/playerHandler.js'; -import pkg from '../package.json' with { type: 'json' }; -import { EXPECTED_SCHEMA_VERSION, EXPECTED_SCHEMA_LABEL } from './config/schemaVersion.js'; -======= import { loadCommands, registerCommands as registerSlashCommands } from './handlers/loaders/commandLoader.js'; import { runSafeTask, handleTaskError, ErrorCodes } from './utils/errorHandler.js'; import { initializeMusic } from './services/music/riffySetup.js'; import { shutdownMusic } from './services/music/playerHandler.js'; import pkg from '../package.json' with { type: 'json' }; import { EXPECTED_SCHEMA_VERSION, EXPECTED_SCHEMA_LABEL } from './config/database/schemaVersion.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) class TitanBot extends Client { constructor() { @@ -102,18 +90,8 @@ class TitanBot extends Client { await this.login(this.config.bot.token); startupLog('Discord login successful'); -<<<<<<< HEAD - startupLog('Registering slash commands...'); - await this.registerCommands(); - if (this.config.bot.multiGuild) { - startupLog('Multi-guild mode enabled — slash commands registered globally'); - } else if (this.config.bot.guildId) { - startupLog(`Single-guild mode — slash commands registered for guild ${this.config.bot.guildId}`); - } -======= startupLog('Registering slash commands globally...'); await this.registerCommands(); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) startupLog('Slash commands registration complete'); const databaseMode = dbStatus.isDegraded @@ -155,11 +133,7 @@ class TitanBot extends Client { }); const requestCounts = new Map(); -<<<<<<< HEAD - const windowMs = 60000; -======= const windowMs = this.config.api?.rateLimit?.windowMs || 60000; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) const maxRequests = this.config.api?.rateLimit?.max || 100; app.use((req, res, next) => { @@ -274,15 +248,9 @@ class TitanBot extends Client { } setupCronJobs() { -<<<<<<< HEAD - cron.schedule('0 6 * * *', () => checkBirthdays(this)); - cron.schedule('* * * * *', () => checkGiveaways(this)); - cron.schedule('*/15 * * * *', () => this.updateAllCounters()); -======= cron.schedule('0 6 * * *', runSafeTask('birthday_check', () => checkBirthdays(this))); cron.schedule('* * * * *', runSafeTask('giveaway_check', () => checkGiveaways(this))); cron.schedule('*/15 * * * *', runSafeTask('counter_update', () => this.updateAllCounters())); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } async updateAllCounters() { @@ -332,11 +300,7 @@ class TitanBot extends Client { for (const handler of handlers) { try { startupLog(`Loading handler: ${handler.path}`); -<<<<<<< HEAD - const module = await import(`./handlers/${handler.path}.js`); -======= const module = await import(`./handlers/loaders/${handler.path}.js`); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) const loaderFn = handler.type.startsWith('named:') ? module[handler.type.split(':')[1]] : module.default; @@ -360,12 +324,7 @@ class TitanBot extends Client { async registerCommands() { try { -<<<<<<< HEAD - const { clientId, guildId, multiGuild } = this.config.bot; - await registerSlashCommands(this, { clientId, guildId, multiGuild }); -======= await registerSlashCommands(this, { clientId: this.config.bot.clientId }); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } catch (error) { logger.error('Error registering commands:', error); } @@ -387,15 +346,12 @@ class TitanBot extends Client { await shutdownMusic(this); logger.info('✅ Music players stopped'); -<<<<<<< HEAD -======= if (this.webServer) { logger.info('Closing web server...'); await new Promise((resolve) => this.webServer.close(resolve)); logger.info('✅ Web server closed'); } ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) // Close database connection // Close database connection if (this.db && this.db.db) { @@ -439,20 +395,12 @@ try { process.on('SIGINT', () => bot.shutdown('SIGINT')); process.on('uncaughtException', (error) => { -<<<<<<< HEAD - logger.error('Uncaught Exception:', error); - bot.shutdown('UNCAUGHT_EXCEPTION'); - }); - - process.on('unhandledRejection', (reason, promise) => { -======= // Process state may be corrupt after an uncaught throw; log and shut down cleanly. handleTaskError('uncaught_exception', error, { fatal: true }); bot.shutdown('UNCAUGHT_EXCEPTION'); }); process.on('unhandledRejection', (reason) => { ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) const code = reason?.code; if (code === 10062 || code === 40060 || code === 50027) { logger.warn('Recoverable Discord interaction rejection:', reason?.message || reason); @@ -462,16 +410,11 @@ try { return; } -<<<<<<< HEAD - logger.error('Unhandled Rejection at:', promise, 'reason:', reason); - bot.shutdown('UNHANDLED_REJECTION'); -======= // A stray rejection is a bug to fix, not a reason to take the bot down. // Log loudly with full context; the central task handler categorizes it. handleTaskError('unhandled_rejection', reason instanceof Error ? reason : new Error(String(reason)), { errorCode: ErrorCodes.UNHANDLED_REJECTION, }); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) }); }; diff --git a/src/commands/Birthday/birthday.js b/src/commands/Birthday/birthday.js index f14a269ad1..c1f8672d00 100644 --- a/src/commands/Birthday/birthday.js +++ b/src/commands/Birthday/birthday.js @@ -1,11 +1,6 @@ import { SlashCommandBuilder, MessageFlags, ChannelType } from 'discord.js'; import { createEmbed, successEmbed } from '../../utils/embeds.js'; -<<<<<<< HEAD -import { logger } from '../../utils/logger.js'; -import { handleInteractionError } from '../../utils/errorHandler.js'; -======= import { replyUserError, ErrorTypes } from '../../utils/errorHandler.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) import birthdaySet from './modules/birthday_set.js'; import birthdayInfo from './modules/birthday_info.js'; @@ -80,40 +75,6 @@ export default { ), async execute(interaction, config, client) { -<<<<<<< HEAD - try { - const subcommand = interaction.options.getSubcommand(); - - switch (subcommand) { - case 'set': - return await birthdaySet.execute(interaction, config, client); - case 'info': - return await birthdayInfo.execute(interaction, config, client); - case 'list': - return await birthdayList.execute(interaction, config, client); - case 'remove': - return await birthdayRemove.execute(interaction, config, client); - case 'next': - return await nextBirthdays.execute(interaction, config, client); - case 'setchannel': - return await birthdaySetchannel.execute(interaction, config, client); - default: - return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Unknown subcommand' }); - } - } catch (error) { - logger.error('Birthday command execution failed', { - error: error.message, - stack: error.stack, - userId: interaction.user.id, - guildId: interaction.guildId, - commandName: 'birthday', - subcommand: interaction.options.getSubcommand() - }); - await handleInteractionError(interaction, error, { - commandName: 'birthday', - source: 'birthday_command' - }); -======= const subcommand = interaction.options.getSubcommand(); switch (subcommand) { @@ -131,7 +92,6 @@ export default { return await birthdaySetchannel.execute(interaction, config, client); default: return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Unknown subcommand' }); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } } }; \ No newline at end of file diff --git a/src/commands/Birthday/modules/birthday_info.js b/src/commands/Birthday/modules/birthday_info.js index 84ed2d1b3e..07b8c20020 100644 --- a/src/commands/Birthday/modules/birthday_info.js +++ b/src/commands/Birthday/modules/birthday_info.js @@ -1,65 +1,10 @@ import { EmbedBuilder } from 'discord.js'; import { getUserBirthday } from '../../../services/birthdayService.js'; import { logger } from '../../../utils/logger.js'; -<<<<<<< HEAD -import { handleInteractionError } from '../../../utils/errorHandler.js'; -======= ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) import { InteractionHelper } from '../../../utils/interactionHelper.js'; export default { async execute(interaction, config, client) { -<<<<<<< HEAD - try { - await InteractionHelper.safeDefer(interaction); - - const targetUser = interaction.options.getUser("user") || interaction.user; - const userId = targetUser.id; - const guildId = interaction.guildId; - - const birthdayData = await getUserBirthday(client, guildId, userId); - - if (!birthdayData) { - const embed = new EmbedBuilder() - .setColor(0xFF0000) - .setTitle('No Birthday Found') - .setDescription(targetUser.id === interaction.user.id - ? "You haven't set your birthday yet. Use `/birthday set` to add it!" - : `${targetUser.username} hasn't set their birthday yet.`); - return await InteractionHelper.safeEditReply(interaction, { - embeds: [embed] - }); - } - - const embed = new EmbedBuilder() - .setColor(0x00FF00) - .setTitle('Birthday Information') - .setDescription(`**Date:** ${birthdayData.monthName} ${birthdayData.day}\n**User:** ${targetUser.toString()}`); - - await InteractionHelper.safeEditReply(interaction, { - embeds: [embed] - }); - - logger.info('Birthday info retrieved successfully', { - userId: interaction.user.id, - targetUserId: targetUser.id, - guildId, - commandName: 'birthday_info' - }); - } catch (error) { - logger.error("Birthday info command execution failed", { - error: error.message, - stack: error.stack, - userId: interaction.user.id, - guildId: interaction.guildId, - commandName: 'birthday_info' - }); - await handleInteractionError(interaction, error, { - commandName: 'birthday_info', - source: 'birthday_info_module' - }); - } -======= await InteractionHelper.safeDefer(interaction); const targetUser = interaction.options.getUser("user") || interaction.user; @@ -95,6 +40,5 @@ export default { guildId, commandName: 'birthday_info' }); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } }; \ No newline at end of file diff --git a/src/commands/Birthday/modules/birthday_list.js b/src/commands/Birthday/modules/birthday_list.js index 127972b97d..ce7ee064da 100644 --- a/src/commands/Birthday/modules/birthday_list.js +++ b/src/commands/Birthday/modules/birthday_list.js @@ -2,96 +2,10 @@ import { EmbedBuilder } from 'discord.js'; import { getAllBirthdays } from '../../../services/birthdayService.js'; import { deleteBirthday } from '../../../utils/database.js'; import { logger } from '../../../utils/logger.js'; -<<<<<<< HEAD -import { handleInteractionError } from '../../../utils/errorHandler.js'; -======= ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) import { InteractionHelper } from '../../../utils/interactionHelper.js'; export default { async execute(interaction, config, client) { -<<<<<<< HEAD - try { - await InteractionHelper.safeDefer(interaction); - - const guildId = interaction.guildId; - - const sortedBirthdays = await getAllBirthdays(client, guildId); - - if (sortedBirthdays.length === 0) { - const embed = new EmbedBuilder() - .setColor(0xFF0000) - .setTitle('No Birthdays') - .setDescription('No birthdays have been set in this server yet.'); - return await InteractionHelper.safeEditReply(interaction, { - embeds: [embed] - }); - } - - const userIds = sortedBirthdays.map(b => b.userId); - const fetchedMembers = await interaction.guild.members.fetch({ user: userIds }).catch(() => null); - - let birthdayList = ''; - let displayIndex = 0; - const staleUserIds = []; - - for (const birthday of sortedBirthdays) { - if (fetchedMembers && !fetchedMembers.has(birthday.userId)) { - staleUserIds.push(birthday.userId); - continue; - } - displayIndex++; - birthdayList += `${displayIndex}. <@${birthday.userId}> - ${birthday.monthName} ${birthday.day}\n`; - } - - if (fetchedMembers && staleUserIds.length > 0) { - for (const userId of staleUserIds) { - deleteBirthday(client, guildId, userId).catch(() => null); - } - } - - if (displayIndex === 0) { - const embed = new EmbedBuilder() - .setColor(0xFF0000) - .setTitle('No Birthdays') - .setDescription('No birthdays have been set by current server members.'); - return await InteractionHelper.safeEditReply(interaction, { - embeds: [embed] - }); - } - - birthdayList = `**${displayIndex} birthday${displayIndex !== 1 ? 's' : ''} in ${interaction.guild.name}**\n\n` + birthdayList; - - const embed = new EmbedBuilder() - .setColor(0x00FF00) - .setTitle('Server Birthdays') - .setDescription(`${birthdayList}\n\nTotal: ${displayIndex} birthday${displayIndex !== 1 ? 's' : ''}`); - - await InteractionHelper.safeEditReply(interaction, { - embeds: [embed] - }); - - logger.info('Birthday list retrieved successfully', { - userId: interaction.user.id, - guildId, - birthdayCount: displayIndex, - staleRemoved: staleUserIds.length, - commandName: 'birthday_list' - }); - } catch (error) { - logger.error("Birthday list command execution failed", { - error: error.message, - stack: error.stack, - userId: interaction.user.id, - guildId: interaction.guildId, - commandName: 'birthday_list' - }); - await handleInteractionError(interaction, error, { - commandName: 'birthday_list', - source: 'birthday_list_module' - }); - } -======= await InteractionHelper.safeDefer(interaction); const guildId = interaction.guildId; @@ -158,6 +72,5 @@ export default { staleRemoved: staleUserIds.length, commandName: 'birthday_list' }); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } }; \ No newline at end of file diff --git a/src/commands/Birthday/modules/birthday_remove.js b/src/commands/Birthday/modules/birthday_remove.js index a724b0dedf..2494c42768 100644 --- a/src/commands/Birthday/modules/birthday_remove.js +++ b/src/commands/Birthday/modules/birthday_remove.js @@ -1,54 +1,9 @@ import { EmbedBuilder } from 'discord.js'; import { deleteBirthday } from '../../../services/birthdayService.js'; -<<<<<<< HEAD -import { logger } from '../../../utils/logger.js'; -import { handleInteractionError } from '../../../utils/errorHandler.js'; -======= ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) import { InteractionHelper } from '../../../utils/interactionHelper.js'; export default { async execute(interaction, config, client) { -<<<<<<< HEAD - try { - await InteractionHelper.safeDefer(interaction); - - const userId = interaction.user.id; - const guildId = interaction.guildId; - - const result = await deleteBirthday(client, guildId, userId); - - if (result.success) { - const embed = new EmbedBuilder() - .setColor(0x00FF00) - .setTitle('Birthday Removed') - .setDescription('Your birthday has been successfully removed from the server.'); - await InteractionHelper.safeEditReply(interaction, { - embeds: [embed] - }); - } else if (result.notFound) { - const embed = new EmbedBuilder() - .setColor(0xFF0000) - .setTitle('No Birthday Found') - .setDescription('You don\'t have a birthday set to remove.'); - await InteractionHelper.safeEditReply(interaction, { - embeds: [embed] - }); - } - } catch (error) { - logger.error("Birthday remove command execution failed", { - error: error.message, - stack: error.stack, - userId: interaction.user.id, - guildId: interaction.guildId, - commandName: 'birthday_remove' - }); - await handleInteractionError(interaction, error, { - commandName: 'birthday_remove', - source: 'birthday_remove_module' - }); - } -======= await InteractionHelper.safeDefer(interaction); const userId = interaction.user.id; @@ -74,6 +29,5 @@ export default { await InteractionHelper.safeEditReply(interaction, { embeds: [embed] }); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } }; \ No newline at end of file diff --git a/src/commands/Birthday/modules/birthday_set.js b/src/commands/Birthday/modules/birthday_set.js index d0d91bbcb5..bf968b065d 100644 --- a/src/commands/Birthday/modules/birthday_set.js +++ b/src/commands/Birthday/modules/birthday_set.js @@ -1,47 +1,9 @@ import { EmbedBuilder } from 'discord.js'; import { setBirthday } from '../../../services/birthdayService.js'; -<<<<<<< HEAD -import { logger } from '../../../utils/logger.js'; -import { handleInteractionError } from '../../../utils/errorHandler.js'; -======= ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) import { InteractionHelper } from '../../../utils/interactionHelper.js'; export default { async execute(interaction, config, client) { -<<<<<<< HEAD - try { - await InteractionHelper.safeDefer(interaction); - - const month = interaction.options.getInteger("month"); - const day = interaction.options.getInteger("day"); - const userId = interaction.user.id; - const guildId = interaction.guildId; - - const result = await setBirthday(client, guildId, userId, month, day); - - const embed = new EmbedBuilder() - .setColor(0x00FF00) - .setTitle('Birthday Set!') - .setDescription(`Your birthday has been set to **${result.data.monthName} ${result.data.day}**!`); - - await InteractionHelper.safeEditReply(interaction, { - embeds: [embed] - }); - } catch (error) { - logger.error("Birthday set command execution failed", { - error: error.message, - stack: error.stack, - userId: interaction.user.id, - guildId: interaction.guildId, - commandName: 'birthday_set' - }); - await handleInteractionError(interaction, error, { - commandName: 'birthday_set', - source: 'birthday_set_module' - }); - } -======= await InteractionHelper.safeDefer(interaction); const month = interaction.options.getInteger("month"); @@ -59,6 +21,5 @@ export default { await InteractionHelper.safeEditReply(interaction, { embeds: [embed] }); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } }; \ No newline at end of file diff --git a/src/commands/Birthday/modules/birthday_setchannel.js b/src/commands/Birthday/modules/birthday_setchannel.js index 944d7aa466..09083bfa9f 100644 --- a/src/commands/Birthday/modules/birthday_setchannel.js +++ b/src/commands/Birthday/modules/birthday_setchannel.js @@ -1,10 +1,5 @@ -<<<<<<< HEAD -import { PermissionsBitField, EmbedBuilder } from 'discord.js'; -import { getGuildConfig, setGuildConfig } from '../../../services/guildConfig.js'; -======= import { PermissionsBitField, EmbedBuilder, MessageFlags } from 'discord.js'; import { getGuildConfig, setGuildConfig } from '../../../services/config/guildConfig.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) import { InteractionHelper } from '../../../utils/interactionHelper.js'; import { logger } from '../../../utils/logger.js'; diff --git a/src/commands/Birthday/modules/next_birthdays.js b/src/commands/Birthday/modules/next_birthdays.js index b7ab3c69f4..0695347c16 100644 --- a/src/commands/Birthday/modules/next_birthdays.js +++ b/src/commands/Birthday/modules/next_birthdays.js @@ -2,111 +2,10 @@ import { EmbedBuilder } from 'discord.js'; import { getUpcomingBirthdays } from '../../../services/birthdayService.js'; import { deleteBirthday } from '../../../utils/database.js'; import { logger } from '../../../utils/logger.js'; -<<<<<<< HEAD -import { handleInteractionError } from '../../../utils/errorHandler.js'; -======= ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) import { InteractionHelper } from '../../../utils/interactionHelper.js'; export default { async execute(interaction, config, client) { -<<<<<<< HEAD - try { - await InteractionHelper.safeDefer(interaction); - - const next5 = await getUpcomingBirthdays(client, interaction.guildId, 5); - - if (next5.length === 0) { - const embed = new EmbedBuilder() - .setColor(0xFF0000) - .setTitle('No Birthdays Found') - .setDescription('No birthdays have been set up in this server yet. Use `/birthday set` to add birthdays!'); - return await InteractionHelper.safeEditReply(interaction, { - embeds: [embed] - }); - } - - let displayIndex = 0; - for (const birthday of next5) { - const member = await interaction.guild.members.fetch(birthday.userId).catch(() => null); - if (!member) { - deleteBirthday(client, interaction.guildId, birthday.userId).catch(() => null); - continue; - } - displayIndex++; - - let timeUntil = ''; - if (birthday.daysUntil === 0) { - timeUntil = '🎉 **Today!**'; - } else if (birthday.daysUntil === 1) { - timeUntil = '📅 **Tomorrow!**'; - } else { - timeUntil = `In ${birthday.daysUntil} day${birthday.daysUntil > 1 ? 's' : ''}`; - } - } - - if (displayIndex === 0) { - const embed = new EmbedBuilder() - .setColor(0xFF0000) - .setTitle('No Upcoming Birthdays') - .setDescription('No upcoming birthdays found for current server members.'); - return await InteractionHelper.safeEditReply(interaction, { - embeds: [embed] - }); - } - - let birthdayList = `🎂 **Next 5 Upcoming Birthdays**\n\nHere are the next 5 birthdays in ${interaction.guild.name}:\n\n`; - displayIndex = 0; - for (const birthday of next5) { - const member = await interaction.guild.members.fetch(birthday.userId).catch(() => null); - if (!member) { - continue; - } - displayIndex++; - - let timeUntil = ''; - if (birthday.daysUntil === 0) { - timeUntil = '🎉 **Today!**'; - } else if (birthday.daysUntil === 1) { - timeUntil = '📅 **Tomorrow!**'; - } else { - timeUntil = `In ${birthday.daysUntil} day${birthday.daysUntil > 1 ? 's' : ''}`; - } - - birthdayList += `${displayIndex}. **${member.displayName}**\n<@${birthday.userId}>\n📅 **Date:** ${birthday.monthName} ${birthday.day}\n⏰ **Time:** ${timeUntil}\n\n`; - } - - birthdayList += `Use /birthday set to add your birthday!`; - - const embed = new EmbedBuilder() - .setColor(0x00FF00) - .setTitle('Next 5 Upcoming Birthdays') - .setDescription(birthdayList); - - await InteractionHelper.safeEditReply(interaction, { - embeds: [embed] - }); - - logger.info('Next birthdays retrieved successfully', { - userId: interaction.user.id, - guildId: interaction.guildId, - upcomingCount: displayIndex, - commandName: 'next_birthdays' - }); - } catch (error) { - logger.error('Next birthdays command execution failed', { - error: error.message, - stack: error.stack, - userId: interaction.user.id, - guildId: interaction.guildId, - commandName: 'next_birthdays' - }); - await handleInteractionError(interaction, error, { - commandName: 'next_birthdays', - source: 'next_birthdays_module' - }); - } -======= await InteractionHelper.safeDefer(interaction); const next5 = await getUpcomingBirthdays(client, interaction.guildId, 5); @@ -188,6 +87,5 @@ export default { upcomingCount: displayIndex, commandName: 'next_birthdays' }); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } }; \ No newline at end of file diff --git a/src/commands/Community/app-admin.js b/src/commands/Community/app-admin.js index 16e8782bcf..6ffc02b3f8 100644 --- a/src/commands/Community/app-admin.js +++ b/src/commands/Community/app-admin.js @@ -1,14 +1,8 @@ import { SlashCommandBuilder, PermissionFlagsBits, PermissionsBitField, ChannelType, ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder, ModalBuilder, TextInputBuilder, TextInputStyle, ComponentType, LabelBuilder, RoleSelectMenuBuilder } from 'discord.js'; import { createEmbed, successEmbed } from '../../utils/embeds.js'; -<<<<<<< HEAD -import { getColor } from '../../config/bot.js'; -import { logger } from '../../utils/logger.js'; -import { handleInteractionError, withErrorHandling, createError, ErrorTypes } from '../../utils/errorHandler.js'; -======= import { getColor, getApplicationStatusColor } from '../../config/bot.js'; import { logger } from '../../utils/logger.js'; import { withErrorHandling, createError, ErrorTypes, replyUserError } from '../../utils/errorHandler.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) import ApplicationService from '../../services/applicationService.js'; import { getApplicationSettings, @@ -244,11 +238,7 @@ async function handleSetup(interaction) { const existingRoles = await getApplicationRoles(interaction.client, interaction.guild.id); if (existingRoles.some(r => r.roleId === roleId)) { -<<<<<<< HEAD - await replyUserError(submitted, { type: ErrorTypes.CONFIGURATION, message: 'The role ${role} is already configured as an application.' }); -======= await replyUserError(submitted, { type: ErrorTypes.CONFIGURATION, message: `The role ${role} is already configured as an application.` }); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) return; } @@ -387,16 +377,6 @@ async function handleReview(interaction) { try { const user = await reasonSubmit.client.users.fetch(application.userId); -<<<<<<< HEAD - const statusColor = status === "approved" ? getColor('success') : getColor('error'); - const reviewStatus = getApplicationStatusPresentation(status); - const dmEmbed = createEmbed( - `${reviewStatus.statusEmoji} Application ${reviewStatus.statusLabel}`, - `Your application for **${application.roleName}** has been **${status}**\n` + - `**Note:** ${reason}\n\n` + - `Use \`/apply status id:${appId}\` to view details.` - ).setColor(statusColor); -======= const statusColor = getApplicationStatusColor(status); const reviewStatus = getApplicationStatusPresentation(status); const dmEmbed = createEmbed({ @@ -405,7 +385,6 @@ async function handleReview(interaction) { `**Note:** ${reason}\n\n` + `Use \`/apply status id:${appId}\` to view details.` }).setColor(statusColor); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) await user.send({ embeds: [dmEmbed] }); } catch (error) { @@ -418,11 +397,7 @@ async function handleReview(interaction) { if (application.logMessageId && application.logChannelId) { try { -<<<<<<< HEAD - const statusColor = status === "approved" ? getColor('success') : getColor('error'); -======= const statusColor = getApplicationStatusColor(status); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) const logChannel = interaction.guild.channels.cache.get( application.logChannelId, ); @@ -567,15 +542,11 @@ async function handleList(interaction) { return InteractionHelper.safeEditReply(interaction, { embeds: [embed], flags: ["Ephemeral"] }); } else { -<<<<<<< HEAD - return await replyUserError(interaction, { type: ErrorTypes.CONFIGURATION, message: '"No applications found and no application roles configured.\\n" +\n "Use `/app-admin roles add` to configure application roles first."' }); -======= return await replyUserError(interaction, { type: ErrorTypes.CONFIGURATION, message: 'No applications found and no application roles configured.\n' + 'Use `/app-admin roles add` to configure application roles first.' }); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } } @@ -610,98 +581,3 @@ async function handleList(interaction) { }); } -<<<<<<< HEAD -export async function handleApplicationReviewModal(interaction) { - if (!interaction.isModalSubmit()) return; - - const customId = interaction.customId; - if (!customId.startsWith('app_review_')) return; - - const [, appId, action] = customId.split('_'); - const reason = interaction.fields.getTextInputValue('reason') || 'No reason provided.'; - const isApprove = action === 'approve'; - - try { - const application = await getApplication(interaction.client, interaction.guild.id, appId); - if (!application) { - return await replyUserError(interaction, { type: ErrorTypes.USER_INPUT, message: 'Application not found.' }); - } - - const status = isApprove ? 'approved' : 'denied'; - await updateApplication(interaction.client, interaction.guild.id, appId, { - status, - reviewer: interaction.user.id, - reviewMessage: reason, - reviewedAt: new Date().toISOString() - }); - - try { - const user = await interaction.client.users.fetch(application.userId); - const reviewStatus = getApplicationStatusPresentation(status); - const dmEmbed = createEmbed( - `${reviewStatus.statusEmoji} Application ${reviewStatus.statusLabel}`, - `Your application for **${application.roleName}** has been **${status}**.\n` + - `**Note:** ${reason}\n\n` + - `Use \`/apply status id:${appId}\` to view details.`, - isApprove ? '#00FF00' : '#FF0000' - ); - - await user.send({ embeds: [dmEmbed] }); - } catch (error) { - logger.error('Error sending DM to user:', error); - } - - if (application.logMessageId && application.logChannelId) { - try { - const logChannel = interaction.guild.channels.cache.get(application.logChannelId); - if (logChannel) { - const logMessage = await logChannel.messages.fetch(application.logMessageId); - if (logMessage) { - const embed = logMessage.embeds[0]; - if (embed) { - const reviewStatus = getApplicationStatusPresentation(status); - const newEmbed = EmbedBuilder.from(embed) - .setColor(isApprove ? '#00FF00' : '#FF0000') - .spliceFields(0, 1, { - name: 'Status', - value: `${reviewStatus.statusEmoji} ${reviewStatus.statusLabel}` - }); - - await logMessage.edit({ - embeds: [newEmbed], - components: [] - }); - } - } - } - } catch (error) { - logger.error('Error updating log message:', error); - } - } - - if (isApprove) { - try { - const member = await interaction.guild.members.fetch(application.userId); - await member.roles.add(application.role); - } catch (error) { - logger.error('Error assigning role:', error); - } - } - - await InteractionHelper.safeEditReply(interaction, { - embeds: [ - successEmbed( - `${getApplicationStatusPresentation(status).statusEmoji} Application ${getApplicationStatusPresentation(status).statusLabel}`, - `The application has been marked as ${getApplicationStatusPresentation(status).statusLabel}.` - ) - ], - flags: ["Ephemeral"] - }); - - } catch (error) { - logger.error('Error processing application review:', error); - await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'An error occurred while processing the application.' }); - } -} -======= ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) diff --git a/src/commands/Community/apply.js b/src/commands/Community/apply.js index 213a7f5139..0e2c2c9249 100644 --- a/src/commands/Community/apply.js +++ b/src/commands/Community/apply.js @@ -1,15 +1,3 @@ -<<<<<<< HEAD -import { getColor } from '../../config/bot.js'; -import { SlashCommandBuilder, ActionRowBuilder, ModalBuilder, TextInputBuilder, TextInputStyle } from 'discord.js'; -import { createEmbed, successEmbed } from '../../utils/embeds.js'; -import { logger } from '../../utils/logger.js'; -import { handleInteractionError, withErrorHandling, createError, ErrorTypes } from '../../utils/errorHandler.js'; -import ApplicationService from '../../services/applicationService.js'; -import { InteractionHelper } from '../../utils/interactionHelper.js'; -import { logEvent, EVENT_TYPES, resolveApplicationLogChannel } from '../../services/loggingService.js'; -import { formatLogLine, resolveUserAuthor } from '../../utils/logEmbeds.js'; -import { getGuildConfig } from '../../services/guildConfig.js'; -======= import { getColor, getDefaultApplicationQuestions } from '../../config/bot.js'; import { SlashCommandBuilder, ActionRowBuilder, ModalBuilder, TextInputBuilder, TextInputStyle } from 'discord.js'; import { createEmbed, successEmbed } from '../../utils/embeds.js'; @@ -20,7 +8,6 @@ import { InteractionHelper } from '../../utils/interactionHelper.js'; import { logEvent, EVENT_TYPES, resolveApplicationLogChannel } from '../../services/loggingService.js'; import { formatLogLine, resolveUserAuthor } from '../../utils/logging/logEmbeds.js'; import { getGuildConfig } from '../../services/config/guildConfig.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) import { getApplicationSettings, getUserApplications, @@ -150,11 +137,7 @@ export async function handleApplicationModal(interaction) { const answers = []; const settings = await getApplicationSettings(interaction.client, interaction.guild.id); -<<<<<<< HEAD - let questions = settings.questions || ["Why do you want this role?", "What is your experience?"]; -======= let questions = settings.questions?.length ? settings.questions : getDefaultApplicationQuestions(); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) const roleSettings = await getApplicationRoleSettings(interaction.client, interaction.guild.id, roleId); if (roleSettings.questions && roleSettings.questions.length > 0) { questions = roleSettings.questions; @@ -317,11 +300,7 @@ async function handleSubmit(interaction, settings) { .setCustomId(`app_modal_${applicationRole.roleId}`) .setTitle(`Application for ${applicationRole.name}`); -<<<<<<< HEAD - let questions = settings.questions || ["Why do you want this role?", "What is your experience?"]; -======= let questions = settings.questions?.length ? settings.questions : getDefaultApplicationQuestions(); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) const roleSettings = await getApplicationRoleSettings(interaction.client, interaction.guild.id, applicationRole.roleId); if (roleSettings.questions && roleSettings.questions.length > 0) { questions = roleSettings.questions; diff --git a/src/commands/Community/modules/app_dashboard.js b/src/commands/Community/modules/app_dashboard.js index 9623da4c91..856a28b918 100644 --- a/src/commands/Community/modules/app_dashboard.js +++ b/src/commands/Community/modules/app_dashboard.js @@ -1,8 +1,4 @@ -<<<<<<< HEAD -import { getColor } from '../../../config/bot.js'; -======= import { getColor, getDefaultApplicationQuestions, botConfig } from '../../../config/bot.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) import { ActionRowBuilder, StringSelectMenuBuilder, @@ -38,11 +34,7 @@ import { getApplications, deleteApplication, } from '../../../utils/database.js'; -<<<<<<< HEAD -import { getGuildConfig } from '../../../services/guildConfig.js'; -======= import { getGuildConfig } from '../../../services/config/guildConfig.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) import { setLogChannel, resolveApplicationLogChannel, resolveLogChannel } from '../../../services/loggingService.js'; async function buildDashboardEmbed(settings, roles, guild, client) { @@ -950,22 +942,15 @@ async function handleRoleAdd(selectInteraction, rootInteraction, settings, roles const customName = modalSubmission.fields.getTextInputValue('role_name').trim() || role?.name || roleId; if (roles.some(r => r.roleId === roleId)) { -<<<<<<< HEAD - await replyUserError(modalSubmission, { type: ErrorTypes.UNKNOWN, message: '${role ?? roleId} is already an application role.' }); -======= await replyUserError(modalSubmission, { type: ErrorTypes.UNKNOWN, message: `${role ?? roleId} is already an application role.` }); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) return; } roles.push({ roleId, name: customName }); await saveApplicationRoles(client, guildId, roles); -<<<<<<< HEAD -======= await saveApplicationRoleSettings(client, guildId, roleId, { questions: getDefaultApplicationQuestions(), }); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) await modalSubmission.reply({ embeds: [successEmbed('Role Added', `${role ?? roleId} added as **${customName}**.`)], @@ -1022,11 +1007,7 @@ async function handleRoleRemove(selectInteraction, rootInteraction, settings, ro const index = roles.findIndex(r => r.roleId === roleId); if (index === -1) { -<<<<<<< HEAD - await replyUserError(modalSubmission, { type: ErrorTypes.USER_INPUT, message: '<@&${roleId}> is not in the application roles list.' }); -======= await replyUserError(modalSubmission, { type: ErrorTypes.USER_INPUT, message: `<@&${roleId}> is not in the application roles list.` }); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) return; } diff --git a/src/commands/Core/commands.js b/src/commands/Core/commands.js index e675428a14..579cbf2bd3 100644 --- a/src/commands/Core/commands.js +++ b/src/commands/Core/commands.js @@ -6,11 +6,7 @@ import { import { InteractionHelper } from '../../utils/interactionHelper.js'; import { successEmbed } from '../../utils/embeds.js'; import { logger } from '../../utils/logger.js'; -<<<<<<< HEAD -import { handleInteractionError, replyUserError, ErrorTypes } from '../../utils/errorHandler.js'; -======= import { replyUserError, ErrorTypes } from '../../utils/errorHandler.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) import { disableCategory, enableCategory, @@ -158,74 +154,6 @@ export default { }, async execute(interaction, config, client) { -<<<<<<< HEAD - try { - if (!(await ensureManageGuild(interaction))) { - return; - } - - const subcommand = interaction.options.getSubcommand(); - - if (subcommand === 'dashboard') { - const deferred = await InteractionHelper.safeDefer(interaction, { flags: MessageFlags.Ephemeral }); - if (!deferred) { - return; - } - - const view = await buildDashboardView(client, interaction.guildId, interaction.guild, 'overview'); - await InteractionHelper.safeEditReply(interaction, { - embeds: [view.embed], - components: view.components, - }); - - const replyMessage = await interaction.fetchReply().catch(() => null); - if (!replyMessage) { - return; - } - - const collector = replyMessage.createMessageComponentCollector({ - filter: createDashboardCollectorFilter(interaction.user.id, interaction.guildId), - time: DASHBOARD_TIMEOUT_MS, - }); - - collector.on('collect', async (componentInteraction) => { - try { - if (!isCommandAccessCustomId(componentInteraction.customId)) { - return; - } - await handleDashboardComponent(componentInteraction, client); - } catch (error) { - logger.error('Command access dashboard interaction failed', { - error: error.message, - customId: componentInteraction.customId, - guildId: interaction.guildId, - }); - await replyUserError(componentInteraction, { - type: ErrorTypes.UNKNOWN, - message: error.message || 'Failed to update command access.', - }).catch(() => {}); - } - }); - - collector.on('end', async () => { - const finalView = await buildDashboardView(client, interaction.guildId, interaction.guild, 'overview'); - const disabledComponents = finalView.components.map((row) => { - const newRow = row.toJSON(); - newRow.components = newRow.components.map((component) => ({ ...component, disabled: true })); - return newRow; - }); - - await replyMessage.edit({ components: disabledComponents }).catch(() => {}); - }); - - return; - } - - const scope = interaction.options.getString('scope'); - const target = interaction.options.getString('target'); - const isDisable = subcommand === 'disable'; - -======= if (!(await ensureManageGuild(interaction))) { return; } @@ -233,59 +161,11 @@ export default { const subcommand = interaction.options.getSubcommand(); if (subcommand === 'dashboard') { ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) const deferred = await InteractionHelper.safeDefer(interaction, { flags: MessageFlags.Ephemeral }); if (!deferred) { return; } -<<<<<<< HEAD - if (scope === 'category') { - const category = resolveCategoryChoice(client, target); - if (!category) { - return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'No category matched \\`${target}\\`. Use \\`/commands dashboard\\` to browse categories.' }); - } - - if (isDisable) { - await disableCategory(client, interaction.guildId, category.key); - return InteractionHelper.safeEditReply(interaction, { - embeds: [ - successEmbed( - 'Category Disabled', - `All **${category.displayName}** commands are now disabled.\nProtected commands remain available.`, - ), - ], - }); - } - - await enableCategory(client, interaction.guildId, category.key); - return InteractionHelper.safeEditReply(interaction, { - embeds: [successEmbed('Category Enabled', `**${category.displayName}** commands are now enabled (except individually disabled commands).`)], - }); - } - - const commandName = target.toLowerCase(); - if (isDisable) { - await disableCommand(client, interaction.guildId, commandName); - return InteractionHelper.safeEditReply(interaction, { - embeds: [successEmbed('Command Disabled', `\`/${commandName}\` is now disabled in this server.`)], - }); - } - - await enableCommand(client, interaction.guildId, commandName); - return InteractionHelper.safeEditReply(interaction, { - embeds: [successEmbed('Command Enabled', `\`/${commandName}\` is now enabled in this server.`)], - }); - } catch (error) { - logger.error('commands command failed', { - error: error.message, - stack: error.stack, - guildId: interaction.guildId, - userId: interaction.user.id, - }); - await handleInteractionError(interaction, error, { commandName: 'commands' }); - } -======= const view = await buildDashboardView(client, interaction.guildId, interaction.guild, 'overview'); await InteractionHelper.safeEditReply(interaction, { embeds: [view.embed], @@ -380,6 +260,5 @@ export default { return InteractionHelper.safeEditReply(interaction, { embeds: [successEmbed('Command Enabled', `\`/${commandName}\` is now enabled in this server.`)], }); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) }, }; diff --git a/src/commands/Core/configWizard.js b/src/commands/Core/configWizard.js index 9292ccfc63..22ac013ec0 100644 --- a/src/commands/Core/configWizard.js +++ b/src/commands/Core/configWizard.js @@ -18,17 +18,10 @@ import { import { InteractionHelper } from '../../utils/interactionHelper.js'; import { createEmbed, successEmbed, infoEmbed, warningEmbed, buildUserErrorEmbed } from '../../utils/embeds.js'; import { replyUserError, ErrorTypes } from '../../utils/errorHandler.js'; -<<<<<<< HEAD -import { getGuildConfig, setConfigValue } from '../../services/guildConfig.js'; -import ConfigService from '../../services/configService.js'; -import { logger } from '../../utils/logger.js'; -import { botConfig } from '../../config/bot.js'; -======= import { getGuildConfig, setConfigValue } from '../../services/config/guildConfig.js'; import ConfigService from '../../services/config/configService.js'; import { logger } from '../../utils/logger.js'; import { botConfig, getCommandPrefix } from '../../config/bot.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) const DASHBOARD_CUSTOM_ID = 'config_select'; const WIZARD_BUTTON_ID = 'config_wizard'; @@ -107,11 +100,7 @@ function buildDashboardEmbed(config, guild) { fields: [ { name: '⌨️ Server Prefix', -<<<<<<< HEAD - value: `\`${config.prefix || guild.client.config.bot.prefix || '!'}\``, -======= value: `\`${config.prefix || getCommandPrefix()}\``, ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) inline: true, }, { @@ -311,11 +300,7 @@ async function runSetupWizard(buttonInteraction, config, guild, client, rootInte { key: 'prefix', skipMessage: 'Keeping the current server prefix.', -<<<<<<< HEAD - question: 'What command prefix should this server use?\nCurrent: `' + (config.prefix || guild.client.config.bot.prefix || '!') + '`\nReply `skip` to keep it, or `cancel` to stop.', -======= question: 'What command prefix should this server use?\nCurrent: `' + (config.prefix || getCommandPrefix()) + '`\nReply `skip` to keep it, or `cancel` to stop.', ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) parse: async (answer) => { const normalized = answer.trim(); if (normalized.toLowerCase() === 'skip') return undefined; diff --git a/src/commands/Core/help.js b/src/commands/Core/help.js index 4b5f1dd74e..3d9d7eb783 100644 --- a/src/commands/Core/help.js +++ b/src/commands/Core/help.js @@ -174,11 +174,7 @@ export default { components: [], }); } catch (error) { -<<<<<<< HEAD - -======= logger.debug('Help menu close edit failed (interaction may have expired):', error?.message); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } }, HELP_MENU_TIMEOUT_MS); }, diff --git a/src/commands/Core/modules/commands_dashboard.js b/src/commands/Core/modules/commands_dashboard.js index 0d8a14a383..88a8865a50 100644 --- a/src/commands/Core/modules/commands_dashboard.js +++ b/src/commands/Core/modules/commands_dashboard.js @@ -14,11 +14,7 @@ import { enableCommand, resetCategoryCommands, } from '../../../services/commandAccessService.js'; -<<<<<<< HEAD -import { getGuildConfig } from '../../../services/guildConfig.js'; -======= import { getGuildConfig } from '../../../services/config/guildConfig.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) export const DASHBOARD_CATEGORY_SELECT = 'cmdaccess_category'; export const DASHBOARD_COMMAND_SELECT = 'cmdaccess_command'; diff --git a/src/commands/Economy/beg.js b/src/commands/Economy/beg.js index 278593e354..9b2fc717f9 100644 --- a/src/commands/Economy/beg.js +++ b/src/commands/Economy/beg.js @@ -6,13 +6,8 @@ import { withErrorHandling, createError, ErrorTypes } from '../../utils/errorHan import { InteractionHelper } from '../../utils/interactionHelper.js'; const COOLDOWN = 30 * 60 * 1000; -<<<<<<< HEAD -const MIN_WIN = 50; -const MAX_WIN = 200; -======= const MIN_WIN = Number(botConfig?.economy?.begMin) || 50; const MAX_WIN = Number(botConfig?.economy?.begMax) || 200; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) const SUCCESS_CHANCE = 0.7; export default { diff --git a/src/commands/Economy/buy.js b/src/commands/Economy/buy.js index de7a504918..1a90387315 100644 --- a/src/commands/Economy/buy.js +++ b/src/commands/Economy/buy.js @@ -2,11 +2,7 @@ import { SlashCommandBuilder, MessageFlags } from 'discord.js'; import { createEmbed, errorEmbed, successEmbed, infoEmbed, warningEmbed } from '../../utils/embeds.js'; import { shopItems } from '../../config/shop/items.js'; import { getEconomyData, setEconomyData } from '../../utils/economy.js'; -<<<<<<< HEAD -import { getGuildConfig } from '../../services/guildConfig.js'; -======= import { getGuildConfig } from '../../services/config/guildConfig.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) import { withErrorHandling, createError, ErrorTypes } from '../../utils/errorHandler.js'; import { InteractionHelper } from '../../utils/interactionHelper.js'; @@ -140,18 +136,12 @@ export default { } else if (item.type === "upgrade") { userData.upgrades[itemId] = true; successDescription += `\n\n**✨ Your upgrade is now active!**`; -<<<<<<< HEAD - } else if (item.type === "consumable") { - userData.inventory[itemId] = - (userData.inventory[itemId] || 0) + quantity; -======= } else if (item.type === "consumable" || item.type === "tool") { userData.inventory[itemId] = (userData.inventory[itemId] || 0) + quantity; if (item.type === "tool") { successDescription += `\n\n**🛠️ ${item.name} added to your inventory!**`; } ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } await setEconomyData(client, guildId, userId, userData); diff --git a/src/commands/Economy/crime.js b/src/commands/Economy/crime.js index 1ad962861f..101dc5a575 100644 --- a/src/commands/Economy/crime.js +++ b/src/commands/Economy/crime.js @@ -5,15 +5,8 @@ import { withErrorHandling, createError, ErrorTypes } from '../../utils/errorHan import { InteractionHelper } from '../../utils/interactionHelper.js'; const CRIME_COOLDOWN = 60 * 60 * 1000; -<<<<<<< HEAD -const MIN_CRIME_AMOUNT = 100; -const MAX_CRIME_AMOUNT = 2000; -const FAILURE_RATE = 0.4; -const JAIL_TIME = 2 * 60 * 60 * 1000; -======= const JAIL_TIME = 2 * 60 * 60 * 1000; const FINE_RATE = 0.2; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) const CRIME_TYPES = [ { name: "Pickpocketing", min: 100, max: 500, risk: 0.3 }, @@ -106,13 +99,9 @@ export default { await InteractionHelper.safeEditReply(interaction, { embeds: [embed] }); } else { -<<<<<<< HEAD - const fine = Math.floor(amountEarned * 0.2); -======= // Fine is based on the potential haul of the attempted crime const potentialHaul = Math.floor((crime.min + crime.max) / 2); const fine = Math.min(Math.floor(potentialHaul * FINE_RATE), userData.wallet || 0); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) userData.wallet = Math.max(0, (userData.wallet || 0) - fine); userData.jailedUntil = now + JAIL_TIME; @@ -120,13 +109,8 @@ export default { const embed = warningEmbed( "🚔 Crime Failed!", -<<<<<<< HEAD - `You were caught while attempting ${crime.name} and have been sent to jail!` + - `You were fined ${fine} coins and will be in jail for 2 hours.` -======= `You were caught while attempting ${crime.name} and have been sent to jail! ` + `You were fined ${fine.toLocaleString()} coins and will be in jail for 2 hours.` ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) ); await InteractionHelper.safeEditReply(interaction, { embeds: [embed] }); diff --git a/src/commands/Economy/daily.js b/src/commands/Economy/daily.js index f1c1e26577..4045d23ed0 100644 --- a/src/commands/Economy/daily.js +++ b/src/commands/Economy/daily.js @@ -1,25 +1,15 @@ import { SlashCommandBuilder } from 'discord.js'; import { createEmbed, errorEmbed, successEmbed, infoEmbed, warningEmbed } from '../../utils/embeds.js'; import { getEconomyData, setEconomyData } from '../../utils/economy.js'; -<<<<<<< HEAD -import { getGuildConfig } from '../../services/guildConfig.js'; -======= import { getGuildConfig } from '../../services/config/guildConfig.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) import { formatDuration } from '../../utils/embeds.js'; import { withErrorHandling, createError, ErrorTypes } from '../../utils/errorHandler.js'; import { logger } from '../../utils/logger.js'; import { InteractionHelper } from '../../utils/interactionHelper.js'; -<<<<<<< HEAD - -const DAILY_COOLDOWN = 24 * 60 * 60 * 1000; -const DAILY_AMOUNT = 1000; -======= import { botConfig } from '../../config/bot.js'; const DAILY_COOLDOWN = 24 * 60 * 60 * 1000; const DAILY_AMOUNT = botConfig.economy?.dailyAmount ?? 100; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) const PREMIUM_BONUS_PERCENTAGE = 0.1; export default { diff --git a/src/commands/Economy/eleaderboard.js b/src/commands/Economy/eleaderboard.js index 2fa4c6576d..e4603797a7 100644 --- a/src/commands/Economy/eleaderboard.js +++ b/src/commands/Economy/eleaderboard.js @@ -3,10 +3,7 @@ import { createEmbed } from '../../utils/embeds.js'; import { withErrorHandling, createError, ErrorTypes } from '../../utils/errorHandler.js'; import { logger } from '../../utils/logger.js'; import { InteractionHelper } from '../../utils/interactionHelper.js'; -<<<<<<< HEAD -======= import { getEconomyPrefix } from '../../utils/database.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) export default { data: new SlashCommandBuilder() @@ -22,11 +19,7 @@ export default { logger.debug(`[ECONOMY] Leaderboard requested`, { guildId }); -<<<<<<< HEAD - const prefix = `economy:${guildId}:`; -======= const prefix = getEconomyPrefix(guildId); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) let allKeys = await client.db.list(prefix); diff --git a/src/commands/Economy/gamble.js b/src/commands/Economy/gamble.js index 77290cfbcf..b4acf1745d 100644 --- a/src/commands/Economy/gamble.js +++ b/src/commands/Economy/gamble.js @@ -83,12 +83,8 @@ export default { if (win) { const amountWon = Math.floor(betAmount * PAYOUT_MULTIPLIER); -<<<<<<< HEAD -cashChange = amountWon; -======= // Net change: the bet is replaced by the payout (bet was at stake, not pre-deducted) cashChange = amountWon - betAmount; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) resultEmbed = successEmbed( "🎉 You Won!", diff --git a/src/commands/Economy/modules/economy_dashboard.js b/src/commands/Economy/modules/economy_dashboard.js index 4cbc48b4df..04f507ff97 100644 --- a/src/commands/Economy/modules/economy_dashboard.js +++ b/src/commands/Economy/modules/economy_dashboard.js @@ -18,10 +18,7 @@ import { InteractionHelper } from '../../../utils/interactionHelper.js'; import { successEmbed } from '../../../utils/embeds.js'; import { logger } from '../../../utils/logger.js'; import { TitanBotError, ErrorTypes, replyUserError } from '../../../utils/errorHandler.js'; -<<<<<<< HEAD -======= import { getEconomyPrefix } from '../../../utils/database.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) import { getEconomyData, addMoney, removeMoney, getMaxBankCapacity } from '../../../utils/economy.js'; import fs from 'fs/promises'; import path from 'path'; @@ -38,11 +35,7 @@ async function buildDashboardEmbed(guild, client) { let userCount = 0; try { -<<<<<<< HEAD - const economyKeys = await client.db.list(`economy:${guild.id}:`); -======= const economyKeys = await client.db.list(getEconomyPrefix(guild.id)); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) if (economyKeys && economyKeys.length > 0) { for (const key of economyKeys) { @@ -119,11 +112,7 @@ async function refreshDashboard(rootInteraction, guild, client) { async function updateConfigFile(currencySymbol, currencyName) { try { -<<<<<<< HEAD - const configPath = path.join(__dirname, '../../config/bot.js'); -======= const configPath = path.join(__dirname, '../../../config/bot.js'); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) let configContent = await fs.readFile(configPath, 'utf-8'); configContent = configContent.replace( @@ -312,25 +301,12 @@ async function handleAddCurrency(selectInteraction, rootInteraction, guild, clie return; } -<<<<<<< HEAD - const result = await addMoney(client, guild.id, userId, amount, type); - - if (!result.success) { - await replyUserError(submitted, { type: ErrorTypes.UNKNOWN, message: 'result.error || \'An error occurred.\'' }); - return; - } -======= const { newBalance } = await addMoney(client, guild.id, userId, amount, type); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) const currencySymbol = BotConfig.economy.currency.symbol; await submitted.reply({ -<<<<<<< HEAD - embeds: [successEmbed('Currency Added', `Successfully added ${currencySymbol}${amount.toLocaleString()} to ${member.user.tag}'s ${type}.\n**New Balance:** ${currencySymbol}${result.newBalance.toLocaleString()}`)], -======= embeds: [successEmbed('Currency Added', `Successfully added ${currencySymbol}${amount.toLocaleString()} to ${member.user.tag}'s ${type}.\n**New Balance:** ${currencySymbol}${newBalance.toLocaleString()}`)], ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) flags: MessageFlags.Ephemeral, }); @@ -339,11 +315,7 @@ async function handleAddCurrency(selectInteraction, rootInteraction, guild, clie targetUserId: userId, amount, type, -<<<<<<< HEAD - newBalance: result.newBalance -======= newBalance, ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) }); await refreshDashboard(rootInteraction, guild, client); @@ -426,25 +398,12 @@ async function handleRemoveCurrency(selectInteraction, rootInteraction, guild, c return; } -<<<<<<< HEAD - const result = await removeMoney(client, guild.id, userId, amount, type); - - if (!result.success) { - await replyUserError(submitted, { type: ErrorTypes.UNKNOWN, message: 'result.error || \'An error occurred.\'' }); - return; - } -======= const { newBalance } = await removeMoney(client, guild.id, userId, amount, type); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) const currencySymbol = BotConfig.economy.currency.symbol; await submitted.reply({ -<<<<<<< HEAD - embeds: [successEmbed('Currency Removed', `Successfully removed ${currencySymbol}${amount.toLocaleString()} from ${member.user.tag}'s ${type}.\n**New Balance:** ${currencySymbol}${result.newBalance.toLocaleString()}`)], -======= embeds: [successEmbed('Currency Removed', `Successfully removed ${currencySymbol}${amount.toLocaleString()} from ${member.user.tag}'s ${type}.\n**New Balance:** ${currencySymbol}${newBalance.toLocaleString()}`)], ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) flags: MessageFlags.Ephemeral, }); @@ -453,11 +412,7 @@ async function handleRemoveCurrency(selectInteraction, rootInteraction, guild, c targetUserId: userId, amount, type, -<<<<<<< HEAD - newBalance: result.newBalance -======= newBalance, ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) }); await refreshDashboard(rootInteraction, guild, client); diff --git a/src/commands/Economy/modules/shop_browse.js b/src/commands/Economy/modules/shop_browse.js index 83b1beac5a..b7adcf40dd 100644 --- a/src/commands/Economy/modules/shop_browse.js +++ b/src/commands/Economy/modules/shop_browse.js @@ -2,10 +2,7 @@ import { ActionRowBuilder, ButtonBuilder, ButtonStyle, ComponentType, EmbedBuild import { shopItems } from '../../../config/shop/items.js'; import { getColor } from '../../../config/bot.js'; import { logger } from '../../../utils/logger.js'; -<<<<<<< HEAD -======= import { handleInteractionError } from '../../../utils/errorHandler.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) export default { async execute(interaction, config, client) { @@ -84,13 +81,6 @@ export default { const disabledComponents = createShopComponents(currentPage); disabledComponents.forEach(row => row.components.forEach(btn => btn.setDisabled(true))); await message.edit({ components: disabledComponents }); -<<<<<<< HEAD - } catch (_) {} - }); - } catch (error) { - logger.error('shop_browse error:', error); - await interaction.reply({ content: '❌ An error occurred while loading the shop.', flags: MessageFlags.Ephemeral }); -======= } catch (error) { logger.debug('shop_browse: could not disable components on collector end', { error: error.message, @@ -99,7 +89,6 @@ export default { }); } catch (error) { await handleInteractionError(interaction, error, { command: 'shop_browse' }); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } }, }; \ No newline at end of file diff --git a/src/commands/Economy/modules/shop_config_setrole.js b/src/commands/Economy/modules/shop_config_setrole.js index 585850b9e5..e917e9e69a 100644 --- a/src/commands/Economy/modules/shop_config_setrole.js +++ b/src/commands/Economy/modules/shop_config_setrole.js @@ -1,17 +1,10 @@ import { PermissionsBitField } from 'discord.js'; import { successEmbed } from '../../../utils/embeds.js'; -<<<<<<< HEAD -import { getGuildConfig, setGuildConfig } from '../../../services/guildConfig.js'; -import { InteractionHelper } from '../../../utils/interactionHelper.js'; -import { logger } from '../../../utils/logger.js'; - -======= import { getGuildConfig, setGuildConfig } from '../../../services/config/guildConfig.js'; import { InteractionHelper } from '../../../utils/interactionHelper.js'; import { logger } from '../../../utils/logger.js'; import { replyUserError, ErrorTypes } from '../../../utils/errorHandler.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) export default { async execute(interaction, config, client) { if (!interaction.member.permissions.has(PermissionsBitField.Flags.ManageGuild)) { diff --git a/src/commands/Economy/rob.js b/src/commands/Economy/rob.js index 4417a5bbe7..68f74fbfee 100644 --- a/src/commands/Economy/rob.js +++ b/src/commands/Economy/rob.js @@ -3,16 +3,10 @@ import { successEmbed, warningEmbed, buildUserErrorEmbed } from '../../utils/emb import { getEconomyData, setEconomyData } from '../../utils/economy.js'; import { withErrorHandling, createError, ErrorTypes } from '../../utils/errorHandler.js'; import { InteractionHelper } from '../../utils/interactionHelper.js'; -<<<<<<< HEAD - -const ROB_COOLDOWN = 4 * 60 * 60 * 1000; -const BASE_ROB_SUCCESS_CHANCE = 0.25; -======= import { BotConfig } from '../../config/bot.js'; const ROB_COOLDOWN = BotConfig.economy?.cooldowns?.rob ?? 4 * 60 * 60 * 1000; const BASE_ROB_SUCCESS_CHANCE = BotConfig.economy?.robSuccessRate ?? 0.4; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) const ROB_PERCENTAGE = 0.15; const FINE_PERCENTAGE = 0.1; @@ -153,11 +147,7 @@ export default { inline: true, }, ) -<<<<<<< HEAD - .setFooter({ text: `Next robbery available in 4 hours.` }); -======= .setFooter({ text: `Next robbery available in ${Math.ceil(ROB_COOLDOWN / (60 * 60 * 1000))} hours.` }); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) await InteractionHelper.safeEditReply(interaction, { embeds: [resultEmbed] }); }, { command: 'rob' }) diff --git a/src/commands/Economy/work.js b/src/commands/Economy/work.js index 0e0e505df3..775be897ad 100644 --- a/src/commands/Economy/work.js +++ b/src/commands/Economy/work.js @@ -4,18 +4,11 @@ import { getEconomyData, setEconomyData } from '../../utils/economy.js'; import { withErrorHandling, createError, ErrorTypes } from '../../utils/errorHandler.js'; import { logger } from '../../utils/logger.js'; import { InteractionHelper } from '../../utils/interactionHelper.js'; -<<<<<<< HEAD - -const WORK_COOLDOWN = 30 * 60 * 1000; -const MIN_WORK_AMOUNT = 50; -const MAX_WORK_AMOUNT = 300; -======= import { botConfig } from '../../config/bot.js'; const WORK_COOLDOWN = botConfig.economy?.cooldowns?.work ?? 30 * 60 * 1000; const MIN_WORK_AMOUNT = botConfig.economy?.workMin ?? 10; const MAX_WORK_AMOUNT = botConfig.economy?.workMax ?? 100; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) const LAPTOP_MULTIPLIER = 1.5; const WORK_JOBS = [ "Software Developer", diff --git a/src/commands/Fun/count.js b/src/commands/Fun/count.js index 3a1b5538c0..1dbd6014b1 100644 --- a/src/commands/Fun/count.js +++ b/src/commands/Fun/count.js @@ -13,10 +13,7 @@ import { } from '../../services/countingGameService.js'; import { logger } from '../../utils/logger.js'; -<<<<<<< HEAD -======= import { replyUserError, ErrorTypes } from '../../utils/errorHandler.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) export default { data: new SlashCommandBuilder() .setName('count') @@ -88,11 +85,7 @@ export default { } if (config.enabled && config.channelId && config.channelId !== channel.id) { -<<<<<<< HEAD - return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'This server already has an active counting channel configured: <#${config.channelId}>. Disable the current counting game first, or use that existing channel.' }); -======= return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: `This server already has an active counting channel configured: <#${config.channelId}>. Disable the current counting game first, or use that existing channel.` }); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } await activateCountingGame(interaction.client, guildId, channel.id, system); diff --git a/src/commands/Fun/fight.js b/src/commands/Fun/fight.js index 8f36163574..3093af2419 100644 --- a/src/commands/Fun/fight.js +++ b/src/commands/Fun/fight.js @@ -1,10 +1,6 @@ import { SlashCommandBuilder } from 'discord.js'; import { successEmbed, warningEmbed } from '../../utils/embeds.js'; import { logger } from '../../utils/logger.js'; -<<<<<<< HEAD -import { handleInteractionError } from '../../utils/errorHandler.js'; -======= ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) import { InteractionHelper } from '../../utils/interactionHelper.js'; const rand = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min; @@ -23,76 +19,6 @@ export default { category: 'Fun', async execute(interaction, config, client) { -<<<<<<< HEAD - try { - await InteractionHelper.safeDefer(interaction); - - const challenger = interaction.user; - const opponent = interaction.options.getUser("opponent"); - - if (challenger.id === opponent.id) { - const embed = warningEmbed( - `**${challenger.username}**, you can't fight yourself! That's a draw before it even starts.`, - "⚔️ Invalid Challenge" - ); - return await InteractionHelper.safeEditReply(interaction, { embeds: [embed] }); - } - - if (opponent.bot) { - const embed = warningEmbed( - "You can't fight bots! Challenge a real person instead.", - "⚔️ Invalid Opponent" - ); - return await InteractionHelper.safeEditReply(interaction, { embeds: [embed] }); - } - - const winner = rand(0, 1) === 0 ? challenger : opponent; - const loser = winner.id === challenger.id ? opponent : challenger; - const rounds = rand(3, 7); - const damage = rand(10, 50); - - const log = []; - log.push( - `💥 **${challenger.username}** challenges **${opponent.username}** to a duel! (Best of ${rounds} rounds)`, - ); - - for (let i = 1; i <= rounds; i++) { - const attacker = rand(0, 1) === 0 ? challenger : opponent; - const target = attacker.id === challenger.id ? opponent : challenger; - const action = [ - "throws a wild punch", - "lands a critical hit", - "uses a weak spell", - "parries and counterattacks", - ][rand(0, 3)]; - log.push( - `\n**Round ${i}:** ${attacker.username} ${action} on ${target.username} for ${rand(1, damage)} damage!`, - ); - } - - const outcomeText = log.join("\n"); - const winnerText = `👑 **${winner.username}** has defeated ${loser.username} and claims the victory!`; - const fullDescription = `${outcomeText}\n\n${winnerText}`; - - const description = fullDescription.length <= EMBED_DESCRIPTION_LIMIT - ? fullDescription - : `${fullDescription.slice(0, EMBED_DESCRIPTION_LIMIT - 15)}\n\n...`; - - const embed = successEmbed( - description, - "🏆 Duel Complete!" - ); - - await InteractionHelper.safeEditReply(interaction, { embeds: [embed] }); - logger.debug(`Fight command executed between ${challenger.id} and ${opponent.id} in guild ${interaction.guildId}`); - } catch (error) { - logger.error('Fight command error:', error); - await handleInteractionError(interaction, error, { - commandName: 'fight', - source: 'fight_command' - }); - } -======= await InteractionHelper.safeDefer(interaction); const challenger = interaction.user; @@ -153,6 +79,5 @@ export default { await InteractionHelper.safeEditReply(interaction, { embeds: [embed] }); logger.debug(`Fight command executed between ${challenger.id} and ${opponent.id} in guild ${interaction.guildId}`); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) }, }; \ No newline at end of file diff --git a/src/commands/Fun/flip.js b/src/commands/Fun/flip.js index d6c31932be..2eeef5cfa9 100644 --- a/src/commands/Fun/flip.js +++ b/src/commands/Fun/flip.js @@ -1,11 +1,7 @@ import { SlashCommandBuilder } from 'discord.js'; import { createEmbed, errorEmbed, successEmbed, infoEmbed, warningEmbed } from '../../utils/embeds.js'; import { logger } from '../../utils/logger.js'; -<<<<<<< HEAD -import { handleInteractionError, TitanBotError, ErrorTypes } from '../../utils/errorHandler.js'; -======= import { TitanBotError, ErrorTypes } from '../../utils/errorHandler.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) import { InteractionHelper } from '../../utils/interactionHelper.js'; export default { @@ -15,26 +11,6 @@ export default { category: 'Fun', async execute(interaction, config, client) { -<<<<<<< HEAD - try { - const result = Math.random() < 0.5 ? "Heads" : "Tails"; - const emoji = result === "Heads" ? "🪙" : "🔮"; - - const embed = successEmbed( - "Heads or Tails?", - `The coin landed on... **${result}** ${emoji}!`, - ); - - await InteractionHelper.safeReply(interaction, { embeds: [embed] }); - logger.debug(`Flip command executed by user ${interaction.user.id} in guild ${interaction.guildId}`); - } catch (error) { - logger.error('Flip command error:', error); - await handleInteractionError(interaction, error, { - commandName: 'flip', - source: 'flip_command' - }); - } -======= const result = Math.random() < 0.5 ? "Heads" : "Tails"; const emoji = result === "Heads" ? "🪙" : "🔮"; @@ -45,6 +21,5 @@ export default { await InteractionHelper.safeReply(interaction, { embeds: [embed] }); logger.debug(`Flip command executed by user ${interaction.user.id} in guild ${interaction.guildId}`); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) }, }; \ No newline at end of file diff --git a/src/commands/Fun/roll.js b/src/commands/Fun/roll.js index 9baebdbde2..988fe1b663 100644 --- a/src/commands/Fun/roll.js +++ b/src/commands/Fun/roll.js @@ -1,11 +1,7 @@ import { SlashCommandBuilder } from 'discord.js'; import { successEmbed } from '../../utils/embeds.js'; import { logger } from '../../utils/logger.js'; -<<<<<<< HEAD -import { handleInteractionError, TitanBotError, ErrorTypes } from '../../utils/errorHandler.js'; -======= import { TitanBotError, ErrorTypes } from '../../utils/errorHandler.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) import { InteractionHelper } from '../../utils/interactionHelper.js'; export default { @@ -22,74 +18,6 @@ export default { category: 'Fun', async execute(interaction, config, client) { -<<<<<<< HEAD - try { - await InteractionHelper.safeDefer(interaction); - - const notation = interaction.options - .getString("notation") - .toLowerCase() - .replace(/\s/g, ""); - - const match = notation.match(/^(\d*)d(\d+)([\+\-]\d+)?$/); - - if (!match) { - throw new TitanBotError( - `Invalid dice notation: ${notation}`, - ErrorTypes.USER_INPUT, - 'Invalid notation. Use format like `1d20` or `3d6+5`.' - ); - } - - const numDice = parseInt(match[1] || "1", 10); - const numSides = parseInt(match[2], 10); - const modifier = parseInt(match[3] || "0", 10); - - if (numDice < 1 || numDice > 20) { - throw new TitanBotError( - `Too many dice requested: ${numDice}`, - ErrorTypes.VALIDATION, - 'Please keep the number of dice between 1 and 20.' - ); - } - - if (numSides < 1 || numSides > 1000) { - throw new TitanBotError( - `Invalid number of sides: ${numSides}`, - ErrorTypes.VALIDATION, - 'Please keep the number of sides between 1 and 1000.' - ); - } - - let rolls = []; - let totalRoll = 0; - - for (let i = 0; i < numDice; i++) { - const roll = Math.floor(Math.random() * numSides) + 1; - rolls.push(roll); - totalRoll += roll; - } - - const finalTotal = totalRoll + modifier; - - const resultsDetail = - numDice > 1 ? `**Rolls:** ${rolls.join(" + ")}\n` : ""; - const modifierText = modifier !== 0 ? `+ (${modifier})` : ""; - - const embed = successEmbed( - `🎲 Rolling ${numDice}d${numSides}${modifier !== 0 ? match[3] : ""}`, - `${resultsDetail}**Total Roll:** ${totalRoll}${modifierText} = **${finalTotal}**`, - ); - - await InteractionHelper.safeEditReply(interaction, { embeds: [embed] }); - logger.debug(`Roll command executed by user ${interaction.user.id} with notation ${notation} in guild ${interaction.guildId}`); - } catch (error) { - await handleInteractionError(interaction, error, { - commandName: 'roll', - source: 'roll_command' - }); - } -======= await InteractionHelper.safeDefer(interaction); const notation = interaction.options @@ -149,6 +77,5 @@ export default { await InteractionHelper.safeEditReply(interaction, { embeds: [embed] }); logger.debug(`Roll command executed by user ${interaction.user.id} with notation ${notation} in guild ${interaction.guildId}`); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) }, }; \ No newline at end of file diff --git a/src/commands/Giveaway/gcreate.js b/src/commands/Giveaway/gcreate.js index 310634809a..e5f19c048c 100644 --- a/src/commands/Giveaway/gcreate.js +++ b/src/commands/Giveaway/gcreate.js @@ -1,11 +1,7 @@ import { SlashCommandBuilder, PermissionFlagsBits, PermissionsBitField, ChannelType, MessageFlags } from 'discord.js'; import { errorEmbed, successEmbed } from '../../utils/embeds.js'; import { logger } from '../../utils/logger.js'; -<<<<<<< HEAD -import { TitanBotError, ErrorTypes, handleInteractionError } from '../../utils/errorHandler.js'; -======= import { TitanBotError, ErrorTypes } from '../../utils/errorHandler.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) import { saveGiveaway } from '../../utils/giveaways.js'; import { parseDuration, @@ -17,14 +13,11 @@ import { import { logEvent, EVENT_TYPES } from '../../services/loggingService.js'; import { InteractionHelper } from '../../utils/interactionHelper.js'; -<<<<<<< HEAD -======= import { botConfig } from '../../config/bot.js'; const GIVEAWAY_MIN_WINNERS = botConfig.giveaways?.minimumWinners ?? 1; const GIVEAWAY_MAX_WINNERS = botConfig.giveaways?.maximumWinners ?? 10; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) export default { data: new SlashCommandBuilder() .setName("gcreate") @@ -41,13 +34,8 @@ export default { option .setName("winners") .setDescription("The number of winners to pick.") -<<<<<<< HEAD - .setMinValue(1) - .setMaxValue(10) -======= .setMinValue(GIVEAWAY_MIN_WINNERS) .setMaxValue(GIVEAWAY_MAX_WINNERS) ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) .setRequired(true), ) .addStringOption((option) => @@ -66,141 +54,6 @@ export default { .setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild), async execute(interaction) { -<<<<<<< HEAD - try { - - if (!interaction.inGuild()) { - throw new TitanBotError( - 'Giveaway command used outside guild', - ErrorTypes.VALIDATION, - 'This command can only be used in a server.', - { userId: interaction.user.id } - ); - } - - if (!interaction.member.permissions.has(PermissionFlagsBits.ManageGuild)) { - throw new TitanBotError( - 'User lacks ManageGuild permission', - ErrorTypes.PERMISSION, - "You need the 'Manage Server' permission to start a giveaway.", - { userId: interaction.user.id, guildId: interaction.guildId } - ); - } - - logger.info(`Giveaway creation started by ${interaction.user.tag} in guild ${interaction.guildId}`); - - const durationString = interaction.options.getString("duration"); - const winnerCount = interaction.options.getInteger("winners"); - const prize = interaction.options.getString("prize"); - const targetChannel = interaction.options.getChannel("channel") || interaction.channel; - - const durationMs = parseDuration(durationString); - validateWinnerCount(winnerCount); - const prizeName = validatePrize(prize); - - if (!targetChannel.isTextBased()) { - throw new TitanBotError( - 'Target channel is not text-based', - ErrorTypes.VALIDATION, - 'The channel must be a text channel.', - { channelId: targetChannel.id, channelType: targetChannel.type } - ); - } - - const endTime = Date.now() + durationMs; - - const initialGiveawayData = { - messageId: "placeholder", - channelId: targetChannel.id, - guildId: interaction.guildId, - prize: prizeName, - hostId: interaction.user.id, - endTime: endTime, - endsAt: endTime, - winnerCount: winnerCount, - participants: [], - isEnded: false, - ended: false, - createdAt: new Date().toISOString() - }; - - const embed = createGiveawayEmbed(initialGiveawayData, "active"); - const row = createGiveawayButtons(false); - - const giveawayMessage = await targetChannel.send({ - content: "🎉 **NEW GIVEAWAY** 🎉", - embeds: [embed], - components: [row], - }); - - initialGiveawayData.messageId = giveawayMessage.id; - const saved = await saveGiveaway( - interaction.client, - interaction.guildId, - initialGiveawayData, - ); - - if (!saved) { - logger.warn(`Failed to save giveaway to database: ${giveawayMessage.id}`); - } - - try { - await logEvent({ - client: interaction.client, - guildId: interaction.guildId, - eventType: EVENT_TYPES.GIVEAWAY_CREATE, - data: { - description: `Giveaway created: ${prizeName}`, - channelId: targetChannel.id, - userId: interaction.user.id, - fields: [ - { - name: 'Prize', - value: prizeName, - inline: true - }, - { - name: 'Winners', - value: winnerCount.toString(), - inline: true - }, - { - name: 'Duration', - value: durationString, - inline: true - }, - { - name: 'Channel', - value: targetChannel.toString(), - inline: true - } - ] - } - }); - } catch (logError) { - logger.debug('Error logging giveaway creation event:', logError); - } - - logger.info(`Giveaway created successfully: ${giveawayMessage.id} in ${targetChannel.name}`); - - await InteractionHelper.safeReply(interaction, { - embeds: [ - successEmbed( - `Giveaway Started! 🎉`, - `A new giveaway for **${prizeName}** has been started in ${targetChannel} and will end in **${durationString}**.`, - ), - ], - flags: MessageFlags.Ephemeral, - }); - - } catch (error) { - await handleInteractionError(interaction, error, { - type: 'command', - commandName: 'gcreate', - context: 'giveaway_creation' - }); - } -======= // Defer up front: sending the giveaway message + DB write can exceed the 3s window await InteractionHelper.safeDefer(interaction, { flags: MessageFlags.Ephemeral }); @@ -327,6 +180,5 @@ export default { ], flags: MessageFlags.Ephemeral, }); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) }, }; \ No newline at end of file diff --git a/src/commands/Giveaway/gdelete.js b/src/commands/Giveaway/gdelete.js index 826dfa2cbc..838209c0c0 100644 --- a/src/commands/Giveaway/gdelete.js +++ b/src/commands/Giveaway/gdelete.js @@ -1,11 +1,7 @@ import { SlashCommandBuilder, PermissionFlagsBits, MessageFlags } from 'discord.js'; import { errorEmbed, successEmbed } from '../../utils/embeds.js'; import { logger } from '../../utils/logger.js'; -<<<<<<< HEAD -import { TitanBotError, ErrorTypes, handleInteractionError } from '../../utils/errorHandler.js'; -======= import { TitanBotError, ErrorTypes } from '../../utils/errorHandler.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) import { getGuildGiveaways, deleteGiveaway } from '../../utils/giveaways.js'; import { logEvent, EVENT_TYPES } from '../../services/loggingService.js'; @@ -25,183 +21,6 @@ export default { .setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild), async execute(interaction) { -<<<<<<< HEAD - try { - - if (!interaction.inGuild()) { - throw new TitanBotError( - 'Giveaway command used outside guild', - ErrorTypes.VALIDATION, - 'This command can only be used in a server.', - { userId: interaction.user.id } - ); - } - - if (!interaction.member.permissions.has(PermissionFlagsBits.ManageGuild)) { - throw new TitanBotError( - 'User lacks ManageGuild permission', - ErrorTypes.PERMISSION, - "You need the 'Manage Server' permission to delete a giveaway.", - { userId: interaction.user.id, guildId: interaction.guildId } - ); - } - - logger.info(`Giveaway deletion started by ${interaction.user.tag} in guild ${interaction.guildId}`); - - const messageId = interaction.options.getString("messageid"); - - if (!messageId || !/^\d+$/.test(messageId)) { - throw new TitanBotError( - 'Invalid message ID format', - ErrorTypes.VALIDATION, - 'Please provide a valid message ID.', - { providedId: messageId } - ); - } - - const giveaways = await getGuildGiveaways(interaction.client, interaction.guildId); - const giveaway = giveaways.find(g => g.messageId === messageId); - - if (!giveaway) { - throw new TitanBotError( - `Giveaway not found: ${messageId}`, - ErrorTypes.VALIDATION, - "No giveaway was found with that message ID.", - { messageId, guildId: interaction.guildId } - ); - } - - let deletedMessage = false; - let channelName = "Unknown Channel"; - - const tryDeleteFromChannel = async (channel) => { - if (!channel || !channel.isTextBased() || !channel.messages?.fetch) { - return false; - } - - const message = await channel.messages.fetch(messageId).catch(() => null); - if (!message) { - return false; - } - - await message.delete(); - channelName = channel.name || 'unknown-channel'; - deletedMessage = true; - return true; - }; - - try { - const channel = await interaction.client.channels.fetch(giveaway.channelId).catch(() => null); - if (await tryDeleteFromChannel(channel)) { - logger.debug(`Deleted giveaway message ${messageId} from channel ${channelName}`); - } - - if (!deletedMessage && interaction.guild) { - const textChannels = interaction.guild.channels.cache.filter( - ch => ch.id !== giveaway.channelId && ch.isTextBased() && ch.messages?.fetch - ); - - for (const [, guildChannel] of textChannels) { - const foundAndDeleted = await tryDeleteFromChannel(guildChannel).catch(() => false); - if (foundAndDeleted) { - logger.debug(`Deleted giveaway message ${messageId} via fallback lookup in #${channelName}`); - break; - } - } - } - } catch (error) { - logger.warn(`Could not delete giveaway message: ${error.message}`); - } - - const removedFromDatabase = await deleteGiveaway( - interaction.client, - interaction.guildId, - messageId, - ); - - if (!removedFromDatabase) { - throw new TitanBotError( - `Failed to delete giveaway from database: ${messageId}`, - ErrorTypes.UNKNOWN, - 'The giveaway could not be removed from the database. Please try again.', - { messageId, guildId: interaction.guildId } - ); - } - - const giveawaysAfterDelete = await getGuildGiveaways(interaction.client, interaction.guildId); - const stillExistsInDatabase = giveawaysAfterDelete.some(g => g.messageId === messageId); - - if (stillExistsInDatabase) { - throw new TitanBotError( - `Giveaway still exists after deletion: ${messageId}`, - ErrorTypes.UNKNOWN, - 'Deletion did not persist in the database. Please try again.', - { messageId, guildId: interaction.guildId } - ); - } - - const statusMsg = deletedMessage - ? `and the message was deleted from #${channelName}` - : `but the message was already deleted or the channel was inaccessible.`; - - const winnerIds = Array.isArray(giveaway.winnerIds) ? giveaway.winnerIds : []; - const hasWinners = winnerIds.length > 0; - const wasEnded = giveaway.ended === true || giveaway.isEnded === true || hasWinners; - - const winnerStatusMsg = hasWinners - ? `This giveaway already had ${winnerIds.length} winner(s) selected.` - : wasEnded - ? 'This giveaway was ended with no valid winners.' - : 'No winner was picked before deletion.'; - - logger.info(`Giveaway deleted: ${messageId} in ${channelName}`); - - try { - await logEvent({ - client: interaction.client, - guildId: interaction.guildId, - eventType: EVENT_TYPES.GIVEAWAY_DELETE, - data: { - description: `Giveaway deleted: ${giveaway.prize}`, - channelId: giveaway.channelId, - userId: interaction.user.id, - fields: [ - { - name: 'Prize', - value: giveaway.prize || 'Unknown', - inline: true - }, - { - name: 'Entries', - value: (giveaway.participants?.length || 0).toString(), - inline: true - } - ] - } - }); - } catch (logError) { - logger.debug('Error logging giveaway deletion:', logError); - } - - return InteractionHelper.safeReply(interaction, { - embeds: [ - successEmbed( - "Giveaway Deleted", - `Successfully deleted the giveaway for **${giveaway.prize}** ${statusMsg}. ${winnerStatusMsg}`, - ), - ], - flags: MessageFlags.Ephemeral, - }); - - } catch (error) { - logger.error('Error in gdelete command:', error); - await handleInteractionError(interaction, error, { - type: 'command', - commandName: 'gdelete', - context: 'giveaway_deletion' - }); - } -======= if (!interaction.inGuild()) { throw new TitanBotError( 'Giveaway command used outside guild', @@ -366,6 +185,5 @@ export default { ], flags: MessageFlags.Ephemeral, }); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) }, }; \ No newline at end of file diff --git a/src/commands/Giveaway/gend.js b/src/commands/Giveaway/gend.js index 6825ae1f14..700ca5eaeb 100644 --- a/src/commands/Giveaway/gend.js +++ b/src/commands/Giveaway/gend.js @@ -1,11 +1,7 @@ import { SlashCommandBuilder, PermissionFlagsBits, MessageFlags } from 'discord.js'; import { errorEmbed, successEmbed } from '../../utils/embeds.js'; import { logger } from '../../utils/logger.js'; -<<<<<<< HEAD -import { TitanBotError, ErrorTypes, handleInteractionError } from '../../utils/errorHandler.js'; -======= import { TitanBotError, ErrorTypes } from '../../utils/errorHandler.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) import { getGuildGiveaways, saveGiveaway } from '../../utils/giveaways.js'; import { endGiveaway as endGiveawayService, @@ -30,179 +26,6 @@ export default { .setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild), async execute(interaction) { -<<<<<<< HEAD - try { - - if (!interaction.inGuild()) { - throw new TitanBotError( - 'Giveaway command used outside guild', - ErrorTypes.VALIDATION, - 'This command can only be used in a server.', - { userId: interaction.user.id } - ); - } - - if (!interaction.member.permissions.has(PermissionFlagsBits.ManageGuild)) { - throw new TitanBotError( - 'User lacks ManageGuild permission', - ErrorTypes.PERMISSION, - "You need the 'Manage Server' permission to end a giveaway.", - { userId: interaction.user.id, guildId: interaction.guildId } - ); - } - - logger.info(`Giveaway end initiated by ${interaction.user.tag} in guild ${interaction.guildId}`); - - const messageId = interaction.options.getString("messageid"); - - if (!messageId || !/^\d+$/.test(messageId)) { - throw new TitanBotError( - 'Invalid message ID format', - ErrorTypes.VALIDATION, - 'Please provide a valid message ID.', - { providedId: messageId } - ); - } - - const giveaways = await getGuildGiveaways(interaction.client, interaction.guildId); - const giveaway = giveaways.find(g => g.messageId === messageId); - - if (!giveaway) { - throw new TitanBotError( - `Giveaway not found: ${messageId}`, - ErrorTypes.VALIDATION, - "No giveaway was found with that message ID in the database.", - { messageId, guildId: interaction.guildId } - ); - } - - const endResult = await endGiveawayService( - interaction.client, - giveaway, - interaction.guildId, - interaction.user.id - ); - - const updatedGiveaway = endResult.giveaway; - const winners = endResult.winners; - - const channel = await interaction.client.channels.fetch( - updatedGiveaway.channelId, - ).catch(err => { - logger.warn(`Could not fetch channel ${updatedGiveaway.channelId}:`, err.message); - return null; - }); - - if (!channel || !channel.isTextBased()) { - throw new TitanBotError( - `Channel not found: ${updatedGiveaway.channelId}`, - ErrorTypes.VALIDATION, - "Could not find the channel where the giveaway was hosted. The giveaway state has been updated.", - { channelId: updatedGiveaway.channelId, messageId } - ); - } - - const message = await channel.messages - .fetch(messageId) - .catch(err => { - logger.warn(`Could not fetch message ${messageId}:`, err.message); - return null; - }); - - if (!message) { - throw new TitanBotError( - `Message not found: ${messageId}`, - ErrorTypes.VALIDATION, - "Could not find the giveaway message. The giveaway state has been updated.", - { messageId, channelId: updatedGiveaway.channelId } - ); - } - - await saveGiveaway( - interaction.client, - interaction.guildId, - updatedGiveaway, - ); - - const newEmbed = createGiveawayEmbed(updatedGiveaway, "ended", winners); - const newRow = createGiveawayButtons(true); - - await message.edit({ - content: "🎉 **GIVEAWAY ENDED** 🎉", - embeds: [newEmbed], - components: [newRow], - }); - - if (winners.length > 0) { - const winnerMentions = winners - .map((id) => `<@${id}>`) - .join(","); - const winnerPingMsg = await channel.send({ - content: `🎉 CONGRATULATIONS ${winnerMentions}! You won the **${updatedGiveaway.prize}** giveaway! Please contact the host <@${updatedGiveaway.hostId}> to claim your prize.`, - }); - updatedGiveaway.winnerPingMessageId = winnerPingMsg.id; - await saveGiveaway(interaction.client, interaction.guildId, updatedGiveaway); - - logger.info(`Giveaway ended with ${winners.length} winner(s): ${messageId}`); - - try { - await logEvent({ - client: interaction.client, - guildId: interaction.guildId, - eventType: EVENT_TYPES.GIVEAWAY_WINNER, - data: { - description: `Giveaway ended with ${winners.length} winner(s)`, - channelId: channel.id, - userId: interaction.user.id, - fields: [ - { - name: 'Prize', - value: updatedGiveaway.prize || 'Mystery Prize!', - inline: true - }, - { - name: 'Winners', - value: winnerMentions, - inline: false - }, - { - name: 'Entries', - value: endResult.participantCount.toString(), - inline: true - } - ] - } - }); - } catch (logError) { - logger.debug('Error logging giveaway winner event:', logError); - } - } else { - await channel.send({ - content: `The giveaway for **${updatedGiveaway.prize}** has ended with no valid entries.`, - }); - logger.info(`Giveaway ended with no winners: ${messageId}`); - } - - logger.info(`Giveaway successfully ended by ${interaction.user.tag}: ${messageId}`); - - return InteractionHelper.safeReply(interaction, { - embeds: [ - successEmbed( - "Giveaway Ended ✅", - `Successfully ended the giveaway for **${updatedGiveaway.prize}** in ${channel}. Selected ${winners.length} winner(s) from ${endResult.participantCount} entries.`, - ), - ], - flags: MessageFlags.Ephemeral, - }); - - } catch (error) { - await handleInteractionError(interaction, error, { - type: 'command', - commandName: 'gend', - context: 'giveaway_end' - }); - } -======= if (!interaction.inGuild()) { throw new TitanBotError( 'Giveaway command used outside guild', @@ -364,6 +187,5 @@ export default { ], flags: MessageFlags.Ephemeral, }); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) }, }; \ No newline at end of file diff --git a/src/commands/Giveaway/greroll.js b/src/commands/Giveaway/greroll.js index d8b0c6e4e6..64be4b3e03 100644 --- a/src/commands/Giveaway/greroll.js +++ b/src/commands/Giveaway/greroll.js @@ -1,11 +1,7 @@ import { SlashCommandBuilder, PermissionFlagsBits, MessageFlags } from 'discord.js'; import { errorEmbed, successEmbed } from '../../utils/embeds.js'; import { logger } from '../../utils/logger.js'; -<<<<<<< HEAD -import { TitanBotError, ErrorTypes, handleInteractionError } from '../../utils/errorHandler.js'; -======= import { TitanBotError, ErrorTypes } from '../../utils/errorHandler.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) import { getGuildGiveaways, saveGiveaway } from '../../utils/giveaways.js'; import { selectWinners, @@ -28,194 +24,6 @@ export default { .setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild), async execute(interaction) { -<<<<<<< HEAD - try { - - if (!interaction.inGuild()) { - throw new TitanBotError( - 'Giveaway command used outside guild', - ErrorTypes.VALIDATION, - 'This command can only be used in a server.', - { userId: interaction.user.id } - ); - } - - if (!interaction.member.permissions.has(PermissionFlagsBits.ManageGuild)) { - throw new TitanBotError( - 'User lacks ManageGuild permission', - ErrorTypes.PERMISSION, - "You need the 'Manage Server' permission to reroll a giveaway.", - { userId: interaction.user.id, guildId: interaction.guildId } - ); - } - - logger.info(`Giveaway reroll initiated by ${interaction.user.tag} in guild ${interaction.guildId}`); - - const messageId = interaction.options.getString("messageid"); - - if (!messageId || !/^\d+$/.test(messageId)) { - throw new TitanBotError( - 'Invalid message ID format', - ErrorTypes.VALIDATION, - 'Please provide a valid message ID.', - { providedId: messageId } - ); - } - - const giveaways = await getGuildGiveaways( - interaction.client, - interaction.guildId, - ); - - const giveaway = giveaways.find(g => g.messageId === messageId); - - if (!giveaway) { - throw new TitanBotError( - `Giveaway not found: ${messageId}`, - ErrorTypes.VALIDATION, - "No giveaway was found with that message ID in the database.", - { messageId, guildId: interaction.guildId } - ); - } - - if (!giveaway.isEnded && !giveaway.ended) { - throw new TitanBotError( - `Giveaway still active: ${messageId}`, - ErrorTypes.VALIDATION, - "This giveaway is still active. Please use `/gend` to end it first.", - { messageId, status: 'active' } - ); - } - - const participants = giveaway.participants || []; - - if (participants.length < giveaway.winnerCount) { - throw new TitanBotError( - `Insufficient participants for reroll: ${participants.length} < ${giveaway.winnerCount}`, - ErrorTypes.VALIDATION, - "Not enough entries to pick the required number of winners.", - { participantsCount: participants.length, winnersNeeded: giveaway.winnerCount } - ); - } - - const newWinners = selectWinners( - participants, - giveaway.winnerCount, - ); - - const updatedGiveaway = { - ...giveaway, - winnerIds: newWinners, - rerolledAt: new Date().toISOString(), - rerolledBy: interaction.user.id - }; - - const channel = await interaction.client.channels.fetch( - giveaway.channelId, - ).catch(err => { - logger.warn(`Could not fetch channel ${giveaway.channelId}:`, err.message); - return null; - }); - - if (!channel || !channel.isTextBased()) { - - await saveGiveaway( - interaction.client, - interaction.guildId, - updatedGiveaway, - ); - - logger.warn(`Could not find channel for giveaway ${messageId}, but saved new winners to database`); - - return InteractionHelper.safeReply(interaction, { - embeds: [ - successEmbed( - "Reroll Complete", - "The new winners have been selected and saved to the database. Could not find channel to announce.", - ), - ], - flags: MessageFlags.Ephemeral, - }); - } - - const message = await channel.messages - .fetch(messageId) - .catch(err => { - logger.warn(`Could not fetch message ${messageId}:`, err.message); - return null; - }); - - if (!message) { - - await saveGiveaway( - interaction.client, - interaction.guildId, - updatedGiveaway, - ); - - const winnerMentions = newWinners - .map((id) => `<@${id}>`) - .join(","); - - const existingPingMsg = giveaway.winnerPingMessageId - ? await channel.messages.fetch(giveaway.winnerPingMessageId).catch(() => null) - : null; - if (existingPingMsg) { - await existingPingMsg.edit({ - content: `🔄 **GIVEAWAY REROLL** 🔄 New winners for **${giveaway.prize}**: ${winnerMentions}!`, - }); - } else { - const newPingMsg = await channel.send({ - content: `🔄 **GIVEAWAY REROLL** 🔄 New winners for **${giveaway.prize}**: ${winnerMentions}!`, - }); - updatedGiveaway.winnerPingMessageId = newPingMsg.id; - } - - logger.info(`Giveaway rerolled (message not found, but announced): ${messageId}`); - - try { - await logEvent({ - client: interaction.client, - guildId: interaction.guildId, - eventType: EVENT_TYPES.GIVEAWAY_REROLL, - data: { - description: `Giveaway rerolled: ${giveaway.prize}`, - channelId: giveaway.channelId, - userId: interaction.user.id, - fields: [ - { - name: 'Prize', - value: giveaway.prize || 'Mystery Prize!', - inline: true - }, - { - name: 'New Winners', - value: winnerMentions, - inline: false - }, - { - name: 'Total Entries', - value: participants.length.toString(), - inline: true - } - ] - } - }); - } catch (logError) { - logger.debug('Error logging giveaway reroll:', logError); - } - - return InteractionHelper.safeReply(interaction, { - embeds: [ - successEmbed( - "Reroll Complete", - `The new winners have been announced in ${channel}. (Original message not found).`, - ), - ], - flags: MessageFlags.Ephemeral, - }); - } -======= if (!interaction.inGuild()) { throw new TitanBotError( 'Giveaway command used outside guild', @@ -303,7 +111,6 @@ export default { }); if (!channel || !channel.isTextBased()) { ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) await saveGiveaway( interaction.client, @@ -311,16 +118,6 @@ export default { updatedGiveaway, ); -<<<<<<< HEAD - const newEmbed = createGiveawayEmbed(updatedGiveaway, "reroll", newWinners); - const newRow = createGiveawayButtons(true); - - await message.edit({ - content: "🔄 **GIVEAWAY REROLLED** 🔄", - embeds: [newEmbed], - components: [newRow], - }); -======= logger.warn(`Could not find channel for giveaway ${messageId}, but saved new winners to database`); return InteractionHelper.safeReply(interaction, { @@ -348,7 +145,6 @@ export default { interaction.guildId, updatedGiveaway, ); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) const winnerMentions = newWinners .map((id) => `<@${id}>`) @@ -359,28 +155,16 @@ export default { : null; if (existingPingMsg) { await existingPingMsg.edit({ -<<<<<<< HEAD - content: `🔄 **REROLL WINNERS** 🔄 CONGRATULATIONS ${winnerMentions}! You are the new winner(s) for the **${giveaway.prize}** giveaway! Please contact the host <@${giveaway.hostId}> to claim your prize.`, - }); - } else { - const newPingMsg = await channel.send({ - content: `🔄 **REROLL WINNERS** 🔄 CONGRATULATIONS ${winnerMentions}! You are the new winner(s) for the **${giveaway.prize}** giveaway! Please contact the host <@${giveaway.hostId}> to claim your prize.`, -======= content: `🔄 **GIVEAWAY REROLL** 🔄 New winners for **${giveaway.prize}**: ${winnerMentions}!`, }); } else { const newPingMsg = await channel.send({ content: `🔄 **GIVEAWAY REROLL** 🔄 New winners for **${giveaway.prize}**: ${winnerMentions}!`, ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) }); updatedGiveaway.winnerPingMessageId = newPingMsg.id; } -<<<<<<< HEAD - logger.info(`Giveaway successfully rerolled: ${messageId} with ${newWinners.length} new winners`); -======= logger.info(`Giveaway rerolled (message not found, but announced): ${messageId}`); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) try { await logEvent({ @@ -411,38 +195,18 @@ export default { } }); } catch (logError) { -<<<<<<< HEAD - logger.debug('Error logging giveaway reroll event:', logError); -======= logger.debug('Error logging giveaway reroll:', logError); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } return InteractionHelper.safeReply(interaction, { embeds: [ successEmbed( -<<<<<<< HEAD - "Reroll Successful ✅", - `Successfully rerolled the giveaway for **${giveaway.prize}** in ${channel}. Selected ${newWinners.length} new winner(s).`, -======= "Reroll Complete", `The new winners have been announced in ${channel}. (Original message not found).`, ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) ), ], flags: MessageFlags.Ephemeral, }); -<<<<<<< HEAD - - } catch (error) { - logger.error('Error in greroll command:', error); - await handleInteractionError(interaction, error, { - type: 'command', - commandName: 'greroll', - context: 'giveaway_reroll' - }); - } -======= } await saveGiveaway( @@ -521,6 +285,5 @@ export default { ], flags: MessageFlags.Ephemeral, }); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) }, }; \ No newline at end of file diff --git a/src/commands/JoinToCreate/jointocreate.js b/src/commands/JoinToCreate/jointocreate.js index 64581f2020..593212f856 100644 --- a/src/commands/JoinToCreate/jointocreate.js +++ b/src/commands/JoinToCreate/jointocreate.js @@ -452,11 +452,7 @@ async function handleNameTemplateModal(interaction, triggerChannel, currentConfi async function handleUserLimitModal(interaction, triggerChannel, currentConfig, client) { try { -<<<<<<< HEAD - const currentLimit = currentConfig.channelConfig.userLimit ?? currentConfig.userLimit ?? 0; -======= const currentLimit = currentConfig.channelConfig?.userLimit ?? currentConfig.userLimit ?? 0; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) const modal = new ModalBuilder() .setCustomId(`jtc_limit_modal_${triggerChannel.id}`) diff --git a/src/commands/JoinToCreate/modules/setup.js b/src/commands/JoinToCreate/modules/setup.js index ba3ea25bad..00ebd31053 100644 --- a/src/commands/JoinToCreate/modules/setup.js +++ b/src/commands/JoinToCreate/modules/setup.js @@ -36,22 +36,14 @@ export default { }); const embed = successEmbed( -<<<<<<< HEAD -======= '✅ Join to Create Setup Complete', ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) `Created trigger channel: ${triggerChannel}\n\n` + `**Settings:**\n` + `• Temporary Channel Name Template: \`${nameTemplate}\`\n` + `• User Limit: ${userLimit === 0 ? 'No limit' : userLimit + ' users'}\n` + `• Bitrate: ${bitrate} kbps\n` + `${category ?`• Category: ${category.name}`: '• Category: None (root level)'}\n\n` + -<<<<<<< HEAD - `When users join this channel, a temporary voice channel will be created for them.`, - '✅ Join to Create Setup Complete' -======= `When users join this channel, a temporary voice channel will be created for them.` ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) ); try { diff --git a/src/commands/Leveling/leaderboard.js b/src/commands/Leveling/leaderboard.js index ffedbf557c..52169ae6cb 100644 --- a/src/commands/Leveling/leaderboard.js +++ b/src/commands/Leveling/leaderboard.js @@ -1,12 +1,7 @@ import { SlashCommandBuilder, EmbedBuilder, MessageFlags } from 'discord.js'; import { logger } from '../../utils/logger.js'; -<<<<<<< HEAD -import { handleInteractionError, TitanBotError, ErrorTypes } from '../../utils/errorHandler.js'; -import { getLeaderboard, getLevelingConfig, getXpForLevel } from '../../services/leveling.js'; -======= import { TitanBotError, ErrorTypes } from '../../utils/errorHandler.js'; import { getLeaderboard, getLevelingConfig, getXpForLevel } from '../../services/leveling/leveling.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) import { InteractionHelper } from '../../utils/interactionHelper.js'; export default { @@ -17,75 +12,6 @@ export default { category: 'Leveling', async execute(interaction, config, client) { -<<<<<<< HEAD - try { - await InteractionHelper.safeDefer(interaction); - - const levelingConfig = await getLevelingConfig(client, interaction.guildId); - - if (!levelingConfig?.enabled) { - await InteractionHelper.safeEditReply(interaction, { - embeds: [ - new EmbedBuilder() - .setColor('#f1c40f') - .setDescription('The leveling system is currently disabled on this server.') - ], - flags: MessageFlags.Ephemeral - }); - return; - } - - const leaderboard = await getLeaderboard(client, interaction.guildId, 10); - - if (leaderboard.length === 0) { - throw new TitanBotError( - 'No leaderboard data found', - ErrorTypes.DATABASE, - 'No level data found yet. Start chatting to gain XP!' - ); - } - - const embed = new EmbedBuilder() - .setTitle('Level Leaderboard') - .setColor('#2ecc71') - .setDescription("Top 10 most active members in this server:") - .setTimestamp(); - - const leaderboardText = await Promise.all( - leaderboard.map(async (user, index) => { - try { - const member = await interaction.guild.members.fetch(user.userId).catch(() => null); - const userMention = member?.user.toString() || `<@${user.userId}>`; - const xpForNextLevel = getXpForLevel(user.level + 1); - - let rankPrefix = `${index + 1}.`; - if (index === 0) rankPrefix = '🥇'; - else if (index === 1) rankPrefix = '🥈'; - else if (index === 2) rankPrefix = '🥉'; - else rankPrefix = `**${index + 1}.**`; - - return `${rankPrefix} ${userMention} - Level ${user.level} (${user.xp}/${xpForNextLevel} XP)`; - } catch { - return `**${index + 1}.** Error loading user ${user.userId}`; - } - }) - ); - - embed.addFields({ - name: 'Rankings', - value: leaderboardText.join('\n') - }); - - await InteractionHelper.safeEditReply(interaction, { embeds: [embed] }); - logger.debug(`Leaderboard displayed for guild ${interaction.guildId}`); - } catch (error) { - logger.error('Leaderboard command error:', error); - await handleInteractionError(interaction, error, { - type: 'command', - commandName: 'leaderboard' - }); - } -======= await InteractionHelper.safeDefer(interaction); const levelingConfig = await getLevelingConfig(client, interaction.guildId); @@ -145,6 +71,5 @@ export default { await InteractionHelper.safeEditReply(interaction, { embeds: [embed] }); logger.debug(`Leaderboard displayed for guild ${interaction.guildId}`); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } }; \ No newline at end of file diff --git a/src/commands/Leveling/level.js b/src/commands/Leveling/level.js index 0c5506e2df..ecfb5620e3 100644 --- a/src/commands/Leveling/level.js +++ b/src/commands/Leveling/level.js @@ -1,15 +1,9 @@ import { getColor } from '../../config/bot.js'; import { SlashCommandBuilder, PermissionFlagsBits, ChannelType, MessageFlags } from 'discord.js'; import { createEmbed } from '../../utils/embeds.js'; -<<<<<<< HEAD -import { getLevelingConfig, saveLevelingConfig } from '../../services/leveling.js'; -import { botHasPermission } from '../../utils/permissionGuard.js'; -import { TitanBotError, ErrorTypes, handleInteractionError } from '../../utils/errorHandler.js'; -======= import { getLevelingConfig, saveLevelingConfig } from '../../services/leveling/leveling.js'; import { botHasPermission } from '../../utils/permissionGuard.js'; import { TitanBotError, ErrorTypes, replyUserError } from '../../utils/errorHandler.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) import { InteractionHelper } from '../../utils/interactionHelper.js'; import { logger } from '../../utils/logger.js'; import levelDashboard from './modules/level_dashboard.js'; @@ -73,93 +67,6 @@ export default { category: 'Leveling', async execute(interaction, config, client) { -<<<<<<< HEAD - try { - const deferred = await InteractionHelper.safeDefer(interaction, { - flags: MessageFlags.Ephemeral, - }); - if (!deferred) return; - - if (!interaction.memberPermissions?.has(PermissionFlagsBits.ManageGuild)) { - return await replyUserError(interaction, { type: ErrorTypes.PERMISSION, message: 'You need the **Manage Server** permission to use this command.' }); - } - - const subcommand = interaction.options.getSubcommand(); - - if (subcommand === 'dashboard') { - return levelDashboard.execute(interaction, config, client); - } - - if (subcommand === 'setup') { - const channel = interaction.options.getChannel('channel'); - const xpMin = interaction.options.getInteger('xp_min') ?? 15; - const xpMax = interaction.options.getInteger('xp_max') ?? 25; - const message = - interaction.options.getString('message') ?? - '{user} has leveled up to level {level}!'; - const xpCooldown = interaction.options.getInteger('xp_cooldown') ?? 60; - - if (xpMin > xpMax) { - return await replyUserError(interaction, { type: ErrorTypes.VALIDATION, message: 'Minimum XP (**${xpMin}**) cannot be greater than maximum XP (**${xpMax}**).' }); - } - - if (!botHasPermission(channel, ['SendMessages', 'EmbedLinks'])) { - throw new TitanBotError( - 'Bot missing permissions in the specified channel', - ErrorTypes.PERMISSION, - `I need **SendMessages** and **EmbedLinks** permissions in ${channel} to send level-up notifications.`, - ); - } - - const existingConfig = await getLevelingConfig(client, interaction.guildId); - - if (existingConfig.configured) { - return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'The leveling system is already set up on this server (level-up notifications go to <#${existingConfig.levelUpChannel}>).\n\nUse \\`/level dashboard\\` to adjust any settings.' }); - } - - const newConfig = { - ...existingConfig, - configured: true, - enabled: true, - levelUpChannel: channel.id, - xpRange: { min: xpMin, max: xpMax }, - xpCooldown: xpCooldown, - levelUpMessage: message, - announceLevelUp: true, - }; - - await saveLevelingConfig(client, interaction.guildId, newConfig); - - logger.info(`Leveling system set up in guild ${interaction.guildId}`, { - channelId: channel.id, - xpMin, - xpMax, - xpCooldown, - userId: interaction.user.id, - }); - - return await InteractionHelper.safeEditReply(interaction, { - embeds: [ - createEmbed({ - title: 'Leveling System Set Up', - description: - `The leveling system is now **enabled** and ready to go.\n\n` + - `**Level-up Channel:** ${channel}\n` + - `**XP per Message:** ${xpMin} – ${xpMax}\n` + - `**XP Cooldown:** ${xpCooldown}s\n` + - `**Level-up Message:** \`${message}\`\n\n` + - `Use \`/level dashboard\` to adjust any of these settings at any time.`, - color: 'success', - }), - ], - }); - } - } catch (error) { - logger.error('Level command error:', error); - await handleInteractionError(interaction, error, { - type: 'command', - commandName: 'level', -======= const deferred = await InteractionHelper.safeDefer(interaction, { flags: MessageFlags.Ephemeral, }); @@ -237,7 +144,6 @@ export default { color: 'success', }), ], ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) }); } }, diff --git a/src/commands/Leveling/leveladd.js b/src/commands/Leveling/leveladd.js index 812bd56bd6..9d439696b7 100644 --- a/src/commands/Leveling/leveladd.js +++ b/src/commands/Leveling/leveladd.js @@ -1,14 +1,8 @@ import { SlashCommandBuilder, PermissionFlagsBits, EmbedBuilder, MessageFlags } from 'discord.js'; import { logger } from '../../utils/logger.js'; -<<<<<<< HEAD -import { handleInteractionError, TitanBotError, ErrorTypes } from '../../utils/errorHandler.js'; -import { checkUserPermissions } from '../../utils/permissionGuard.js'; -import { addLevels, getLevelingConfig } from '../../services/leveling.js'; -======= import { TitanBotError, ErrorTypes } from '../../utils/errorHandler.js'; import { checkUserPermissions } from '../../utils/permissionGuard.js'; import { addLevels, getLevelingConfig } from '../../services/leveling/leveling.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) import { createEmbed } from '../../utils/embeds.js'; import { InteractionHelper } from '../../utils/interactionHelper.js'; @@ -34,65 +28,6 @@ export default { category: 'Leveling', async execute(interaction, config, client) { -<<<<<<< HEAD - try { - await InteractionHelper.safeDefer(interaction); - - const hasPermission = await checkUserPermissions( - interaction, - PermissionFlagsBits.ManageGuild, - 'You need ManageGuild permission to use this command.' - ); - if (!hasPermission) return; - - const levelingConfig = await getLevelingConfig(client, interaction.guildId); - if (!levelingConfig?.enabled) { - await InteractionHelper.safeEditReply(interaction, { - embeds: [ - new EmbedBuilder() - .setColor('#f1c40f') - .setDescription('The leveling system is currently disabled on this server.') - ], - flags: MessageFlags.Ephemeral - }); - return; - } - - const targetUser = interaction.options.getUser('user'); - const levelsToAdd = interaction.options.getInteger('levels'); - - const member = await interaction.guild.members.fetch(targetUser.id).catch(() => null); - if (!member) { - throw new TitanBotError( - `User ${targetUser.id} not found in this guild`, - ErrorTypes.USER_INPUT, - 'The specified user is not in this server.' - ); - } - - const userData = await addLevels(client, interaction.guildId, targetUser.id, levelsToAdd); - - await InteractionHelper.safeEditReply(interaction, { - embeds: [ - createEmbed({ - title: 'Levels Added', - description: `Successfully added ${levelsToAdd} levels to ${targetUser.tag}.\n**New Level:** ${userData.level}`, - color: 'success' - }) - ] - }); - - logger.info( - `[ADMIN] User ${interaction.user.tag} added ${levelsToAdd} levels to ${targetUser.tag} in guild ${interaction.guildId}` - ); - } catch (error) { - logger.error('LevelAdd command error:', error); - await handleInteractionError(interaction, error, { - type: 'command', - commandName: 'leveladd' - }); - } -======= await InteractionHelper.safeDefer(interaction); const hasPermission = await checkUserPermissions( @@ -142,6 +77,5 @@ export default { logger.info( `[ADMIN] User ${interaction.user.tag} added ${levelsToAdd} levels to ${targetUser.tag} in guild ${interaction.guildId}` ); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } }; \ No newline at end of file diff --git a/src/commands/Leveling/levelremove.js b/src/commands/Leveling/levelremove.js index a3c83e50a1..f781d33df8 100644 --- a/src/commands/Leveling/levelremove.js +++ b/src/commands/Leveling/levelremove.js @@ -1,14 +1,8 @@ import { SlashCommandBuilder, PermissionFlagsBits, EmbedBuilder, MessageFlags } from 'discord.js'; import { logger } from '../../utils/logger.js'; -<<<<<<< HEAD -import { handleInteractionError, TitanBotError, ErrorTypes } from '../../utils/errorHandler.js'; -import { checkUserPermissions } from '../../utils/permissionGuard.js'; -import { removeLevels, getUserLevelData, getLevelingConfig } from '../../services/leveling.js'; -======= import { TitanBotError, ErrorTypes } from '../../utils/errorHandler.js'; import { checkUserPermissions } from '../../utils/permissionGuard.js'; import { removeLevels, getUserLevelData, getLevelingConfig } from '../../services/leveling/leveling.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) import { createEmbed } from '../../utils/embeds.js'; import { InteractionHelper } from '../../utils/interactionHelper.js'; @@ -34,74 +28,6 @@ export default { category: 'Leveling', async execute(interaction, config, client) { -<<<<<<< HEAD - try { - await InteractionHelper.safeDefer(interaction); - - const hasPermission = await checkUserPermissions( - interaction, - PermissionFlagsBits.ManageGuild, - 'You need ManageGuild permission to use this command.' - ); - if (!hasPermission) return; - - const levelingConfig = await getLevelingConfig(client, interaction.guildId); - if (!levelingConfig?.enabled) { - await InteractionHelper.safeEditReply(interaction, { - embeds: [ - new EmbedBuilder() - .setColor('#f1c40f') - .setDescription('The leveling system is currently disabled on this server.') - ], - flags: MessageFlags.Ephemeral - }); - return; - } - - const targetUser = interaction.options.getUser('user'); - const levelsToRemove = interaction.options.getInteger('levels'); - - const member = await interaction.guild.members.fetch(targetUser.id).catch(() => null); - if (!member) { - throw new TitanBotError( - `User ${targetUser.id} not found in this guild`, - ErrorTypes.USER_INPUT, - 'The specified user is not in this server.' - ); - } - - const userData = await getUserLevelData(client, interaction.guildId, targetUser.id); - if (userData.level === 0) { - throw new TitanBotError( - `User ${targetUser.id} is already at minimum level`, - ErrorTypes.VALIDATION, - `${targetUser.tag} is already at level 0 and cannot have levels removed.` - ); - } - - const updatedData = await removeLevels(client, interaction.guildId, targetUser.id, levelsToRemove); - - await InteractionHelper.safeEditReply(interaction, { - embeds: [ - createEmbed({ - title: 'Levels Removed', - description: `Successfully removed ${levelsToRemove} levels from ${targetUser.tag}.\n**New Level:** ${updatedData.level}`, - color: 'success' - }) - ] - }); - - logger.info( - `[ADMIN] User ${interaction.user.tag} removed ${levelsToRemove} levels from ${targetUser.tag} in guild ${interaction.guildId}` - ); - } catch (error) { - logger.error('LevelRemove command error:', error); - await handleInteractionError(interaction, error, { - type: 'command', - commandName: 'levelremove' - }); - } -======= await InteractionHelper.safeDefer(interaction); const hasPermission = await checkUserPermissions( @@ -160,6 +86,5 @@ export default { logger.info( `[ADMIN] User ${interaction.user.tag} removed ${levelsToRemove} levels from ${targetUser.tag} in guild ${interaction.guildId}` ); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } }; \ No newline at end of file diff --git a/src/commands/Leveling/levelset.js b/src/commands/Leveling/levelset.js index 80845adc62..4ce8589ac0 100644 --- a/src/commands/Leveling/levelset.js +++ b/src/commands/Leveling/levelset.js @@ -1,14 +1,8 @@ import { SlashCommandBuilder, PermissionFlagsBits, EmbedBuilder, MessageFlags } from 'discord.js'; import { logger } from '../../utils/logger.js'; -<<<<<<< HEAD -import { handleInteractionError, TitanBotError, ErrorTypes } from '../../utils/errorHandler.js'; -import { checkUserPermissions } from '../../utils/permissionGuard.js'; -import { setUserLevel, getLevelingConfig } from '../../services/leveling.js'; -======= import { TitanBotError, ErrorTypes } from '../../utils/errorHandler.js'; import { checkUserPermissions } from '../../utils/permissionGuard.js'; import { setUserLevel, getLevelingConfig } from '../../services/leveling/leveling.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) import { createEmbed } from '../../utils/embeds.js'; import { InteractionHelper } from '../../utils/interactionHelper.js'; @@ -34,65 +28,6 @@ export default { category: 'Leveling', async execute(interaction, config, client) { -<<<<<<< HEAD - try { - await InteractionHelper.safeDefer(interaction); - - const hasPermission = await checkUserPermissions( - interaction, - PermissionFlagsBits.ManageGuild, - 'You need ManageGuild permission to use this command.' - ); - if (!hasPermission) return; - - const levelingConfig = await getLevelingConfig(client, interaction.guildId); - if (!levelingConfig?.enabled) { - await InteractionHelper.safeEditReply(interaction, { - embeds: [ - new EmbedBuilder() - .setColor('#f1c40f') - .setDescription('The leveling system is currently disabled on this server.') - ], - flags: MessageFlags.Ephemeral - }); - return; - } - - const targetUser = interaction.options.getUser('user'); - const newLevel = interaction.options.getInteger('level'); - - const member = await interaction.guild.members.fetch(targetUser.id).catch(() => null); - if (!member) { - throw new TitanBotError( - `User ${targetUser.id} not found in this guild`, - ErrorTypes.USER_INPUT, - 'The specified user is not in this server.' - ); - } - - const userData = await setUserLevel(client, interaction.guildId, targetUser.id, newLevel); - - await InteractionHelper.safeEditReply(interaction, { - embeds: [ - createEmbed({ - title: 'Level Set', - description: `Successfully set ${targetUser.tag}'s level to **${newLevel}**.\n**Total XP:** ${userData.totalXp}`, - color: 'success' - }) - ] - }); - - logger.info( - `[ADMIN] User ${interaction.user.tag} set ${targetUser.tag}'s level to ${newLevel} in guild ${interaction.guildId}` - ); - } catch (error) { - logger.error('LevelSet command error:', error); - await handleInteractionError(interaction, error, { - type: 'command', - commandName: 'levelset' - }); - } -======= await InteractionHelper.safeDefer(interaction); const hasPermission = await checkUserPermissions( @@ -142,6 +77,5 @@ export default { logger.info( `[ADMIN] User ${interaction.user.tag} set ${targetUser.tag}'s level to ${newLevel} in guild ${interaction.guildId}` ); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } }; \ No newline at end of file diff --git a/src/commands/Leveling/modules/level_dashboard.js b/src/commands/Leveling/modules/level_dashboard.js index e023337bc6..3d8ee34b61 100644 --- a/src/commands/Leveling/modules/level_dashboard.js +++ b/src/commands/Leveling/modules/level_dashboard.js @@ -20,11 +20,7 @@ import { InteractionHelper } from '../../../utils/interactionHelper.js'; import { successEmbed } from '../../../utils/embeds.js'; import { logger } from '../../../utils/logger.js'; import { TitanBotError, ErrorTypes, replyUserError } from '../../../utils/errorHandler.js'; -<<<<<<< HEAD -import { getLevelingConfig, saveLevelingConfig } from '../../../services/leveling.js'; -======= import { getLevelingConfig, saveLevelingConfig } from '../../../services/leveling/leveling.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) import { botHasPermission } from '../../../utils/permissionGuard.js'; import { startDashboardSession } from '../../../utils/dashboardSession.js'; @@ -361,11 +357,7 @@ async function handleRoleRewardRemove(selectInteraction, rootInteraction, cfg, g const level = parseInt(rawLevel, 10); if (isNaN(level) || !cfg.roleRewards?.[level]) { -<<<<<<< HEAD - await replyUserError(submitted, { type: ErrorTypes.USER_INPUT, message: 'No role reward is configured for level **${rawLevel}**.' }); -======= await replyUserError(submitted, { type: ErrorTypes.USER_INPUT, message: `No role reward is configured for level **${rawLevel}**.` }); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) return; } @@ -415,11 +407,7 @@ async function handleChannel(selectInteraction, rootInteraction, cfg, guildId, c const channel = selectInteraction.guild.channels.cache.get(channelId); if (channel && !botHasPermission(channel, ['SendMessages', 'EmbedLinks'])) { -<<<<<<< HEAD - await replyUserError(submitted, { type: ErrorTypes.PERMISSION, message: 'I need **SendMessages** and **EmbedLinks** permissions in ${channel} to send level-up notifications.' }); -======= await replyUserError(submitted, { type: ErrorTypes.PERMISSION, message: `I need **SendMessages** and **EmbedLinks** permissions in ${channel} to send level-up notifications.` }); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) return; } diff --git a/src/commands/Leveling/rank.js b/src/commands/Leveling/rank.js index 9b34462d13..fea894105c 100644 --- a/src/commands/Leveling/rank.js +++ b/src/commands/Leveling/rank.js @@ -1,12 +1,7 @@ import { SlashCommandBuilder, EmbedBuilder, MessageFlags } from 'discord.js'; import { logger } from '../../utils/logger.js'; -<<<<<<< HEAD -import { handleInteractionError, TitanBotError, ErrorTypes } from '../../utils/errorHandler.js'; -import { getUserLevelData, getLevelingConfig, getXpForLevel } from '../../services/leveling.js'; -======= import { TitanBotError, ErrorTypes } from '../../utils/errorHandler.js'; import { getUserLevelData, getLevelingConfig, getXpForLevel } from '../../services/leveling/leveling.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) import { InteractionHelper } from '../../utils/interactionHelper.js'; export default { @@ -23,85 +18,6 @@ export default { category: 'Leveling', async execute(interaction, config, client) { -<<<<<<< HEAD - try { - await InteractionHelper.safeDefer(interaction); - - const levelingConfig = await getLevelingConfig(client, interaction.guildId); - if (!levelingConfig?.enabled) { - await InteractionHelper.safeEditReply(interaction, { - embeds: [ - new EmbedBuilder() - .setColor('#f1c40f') - .setDescription('The leveling system is currently disabled on this server.') - ], - flags: MessageFlags.Ephemeral - }); - return; - } - - const targetUser = interaction.options.getUser('user') || interaction.user; - const member = await interaction.guild.members - .fetch(targetUser.id) - .catch(() => null); - - if (!member) { - throw new TitanBotError( - `User ${targetUser.id} not found in guild`, - ErrorTypes.USER_INPUT, - 'Could not find the specified user in this server.' - ); - } - - const userData = await getUserLevelData(client, interaction.guildId, targetUser.id); - - const safeUserData = { - level: userData?.level ?? 0, - xp: userData?.xp ?? 0, - totalXp: userData?.totalXp ?? 0 - }; - - const xpNeeded = getXpForLevel(safeUserData.level + 1); - const progress = xpNeeded > 0 ? Math.floor((safeUserData.xp / xpNeeded) * 100) : 0; - const progressBar = createProgressBar(progress, 20); - - const embed = new EmbedBuilder() - .setTitle(`${member.displayName}'s Rank`) - .setThumbnail(member.displayAvatarURL({ dynamic: true })) - .addFields( - { - name: 'Level', - value: safeUserData.level.toString(), - inline: true - }, - { - name: 'XP', - value: `${safeUserData.xp}/${xpNeeded}`, - inline: true - }, - { - name: 'Total XP', - value: safeUserData.totalXp.toString(), - inline: true - }, - { - name: `Progress to Level ${safeUserData.level + 1}`, - value: `${progressBar} ${progress}%` - } - ) - .setColor('#2ecc71') - .setTimestamp(); - - await InteractionHelper.safeEditReply(interaction, { embeds: [embed] }); - logger.debug(`Rank checked for user ${targetUser.id} in guild ${interaction.guildId}`); - } catch (error) { - logger.error('Rank command error:', error); - await handleInteractionError(interaction, error, { - type: 'command', - commandName: 'rank' - }); - } -======= await InteractionHelper.safeDefer(interaction); const levelingConfig = await getLevelingConfig(client, interaction.guildId); @@ -171,7 +87,6 @@ export default { await InteractionHelper.safeEditReply(interaction, { embeds: [embed] }); logger.debug(`Rank checked for user ${targetUser.id} in guild ${interaction.guildId}`); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } }; diff --git a/src/commands/Logging/logging.js b/src/commands/Logging/logging.js index 89ff285636..0d94386314 100644 --- a/src/commands/Logging/logging.js +++ b/src/commands/Logging/logging.js @@ -5,10 +5,7 @@ import { InteractionHelper } from '../../utils/interactionHelper.js'; import dashboard from './modules/logging_dashboard.js'; import channel from './modules/logging_channel.js'; -<<<<<<< HEAD -======= import { replyUserError, ErrorTypes } from '../../utils/errorHandler.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) export default { data: new SlashCommandBuilder() .setName('logging') diff --git a/src/commands/Logging/modules/logging_channel.js b/src/commands/Logging/modules/logging_channel.js index a9573b4c24..f92e6a6d3b 100644 --- a/src/commands/Logging/modules/logging_channel.js +++ b/src/commands/Logging/modules/logging_channel.js @@ -4,10 +4,7 @@ import { successEmbed } from '../../../utils/embeds.js'; import { InteractionHelper } from '../../../utils/interactionHelper.js'; import { logger } from '../../../utils/logger.js'; -<<<<<<< HEAD -======= import { replyUserError, ErrorTypes } from '../../../utils/errorHandler.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) const DESTINATION_LABELS = { audit: 'Audit Log', applications: 'Applications', @@ -44,11 +41,7 @@ export default { const botPerms = channel.permissionsFor(interaction.guild.members.me); if (!botPerms?.has(['ViewChannel', 'SendMessages', 'EmbedLinks'])) { -<<<<<<< HEAD - return await replyUserError(interaction, { type: ErrorTypes.PERMISSION, message: 'I need **View Channel**, **Send Messages**, and **Embed Links** in ${channel}.' }); -======= return await replyUserError(interaction, { type: ErrorTypes.PERMISSION, message: `I need **View Channel**, **Send Messages**, and **Embed Links** in ${channel}.` }); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } await setLogChannel(client, interaction.guildId, destination, channel.id); diff --git a/src/commands/Logging/modules/logging_dashboard.js b/src/commands/Logging/modules/logging_dashboard.js index 653354035b..f317698ccf 100644 --- a/src/commands/Logging/modules/logging_dashboard.js +++ b/src/commands/Logging/modules/logging_dashboard.js @@ -1,10 +1,6 @@ import { EmbedBuilder, MessageFlags, PermissionsBitField } from 'discord.js'; import { getColor } from '../../../config/bot.js'; -<<<<<<< HEAD -import { getGuildConfig } from '../../../services/guildConfig.js'; -======= import { getGuildConfig } from '../../../services/config/guildConfig.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) import { getLoggingStatus } from '../../../services/loggingService.js'; import { createLoggingDashboardComponents, @@ -13,18 +9,11 @@ import { DASHBOARD_CATEGORIES, DASHBOARD_CATEGORY_LABELS, EVENT_TYPES_BY_CATEGORY, -<<<<<<< HEAD -} from '../../../utils/loggingUi.js'; -import { InteractionHelper } from '../../../utils/interactionHelper.js'; -import { logger } from '../../../utils/logger.js'; - -======= } from '../../../utils/logging/loggingUi.js'; import { InteractionHelper } from '../../../utils/interactionHelper.js'; import { logger } from '../../../utils/logger.js'; import { replyUserError, ErrorTypes } from '../../../utils/errorHandler.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) export function getCategoryStatus(enabledEvents, category, auditEnabled) { if (!auditEnabled) return false; const events = enabledEvents || {}; diff --git a/src/commands/Moderation/ban.js b/src/commands/Moderation/ban.js index b2d901dea8..86dd778858 100644 --- a/src/commands/Moderation/ban.js +++ b/src/commands/Moderation/ban.js @@ -1,19 +1,9 @@ -<<<<<<< HEAD -import { SlashCommandBuilder, PermissionFlagsBits, PermissionsBitField, ChannelType } from 'discord.js'; -import { createEmbed, errorEmbed, successEmbed, infoEmbed, warningEmbed } from '../../utils/embeds.js'; -import { logModerationAction } from '../../utils/moderation.js'; -import { logger } from '../../utils/logger.js'; -import { InteractionHelper } from '../../utils/interactionHelper.js'; -import { ModerationService } from '../../services/moderationService.js'; -import { handleInteractionError, TitanBotError, ErrorTypes } from '../../utils/errorHandler.js'; -======= import { SlashCommandBuilder, PermissionFlagsBits } from 'discord.js'; import { successEmbed } from '../../utils/embeds.js'; import { InteractionHelper } from '../../utils/interactionHelper.js'; import { ModerationService } from '../../services/moderation/moderationService.js'; import { TitanBotError, ErrorTypes } from '../../utils/errorHandler.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) export default { data: new SlashCommandBuilder() .setName("ban") @@ -27,53 +17,6 @@ export default { .addStringOption((option) => option.setName("reason").setDescription("Reason for the ban"), ) -<<<<<<< HEAD -.setDefaultMemberPermissions(PermissionFlagsBits.BanMembers), - category: "moderation", - - async execute(interaction, config, client) { - try { - const user = interaction.options.getUser("target"); - const reason = interaction.options.getString("reason") || "No reason provided"; - - if (!user) { - throw new TitanBotError( - 'Missing target user', - ErrorTypes.USER_INPUT, - 'You must specify a user to ban.', - { subtype: 'invalid_user' }, - ); - } - - if (user.id === interaction.user.id) { - throw new Error("You cannot ban yourself."); - } - if (user.id === client.user.id) { - throw new Error("You cannot ban the bot."); - } - - const result = await ModerationService.banUser({ - guild: interaction.guild, - user, - moderator: interaction.member, - reason - }); - - await InteractionHelper.universalReply(interaction, { - embeds: [ - successEmbed( - `🚫 **Banned** ${user.tag}`, - `**Reason:** ${reason}\n**Case ID:** #${result.caseId}`, - ), - ], - }); - } catch (error) { - logger.error('Ban command error:', error); - await handleInteractionError(interaction, error, { subtype: 'ban_failed' }); - } - }, -}; -======= .setDefaultMemberPermissions(PermissionFlagsBits.BanMembers), category: "moderation", @@ -122,4 +65,3 @@ export default { }); }, }; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) diff --git a/src/commands/Moderation/cases.js b/src/commands/Moderation/cases.js index 47b81b0862..5999cd834d 100644 --- a/src/commands/Moderation/cases.js +++ b/src/commands/Moderation/cases.js @@ -3,10 +3,7 @@ import { createEmbed, successEmbed } from '../../utils/embeds.js'; import { getModerationCases } from '../../utils/moderation.js'; import { logger } from '../../utils/logger.js'; import { InteractionHelper } from '../../utils/interactionHelper.js'; -<<<<<<< HEAD -======= import { replyUserError, ErrorTypes } from '../../utils/errorHandler.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) export default { data: new SlashCommandBuilder() .setName('cases') @@ -154,11 +151,7 @@ time: 120000 currentPage++; } -<<<<<<< HEAD - await buttonInteraction.editReply({ -======= await interaction.editReply({ ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) embeds: [createCasesEmbed(currentPage)], components: [createNavigationRow(currentPage)] }); diff --git a/src/commands/Moderation/dm.js b/src/commands/Moderation/dm.js index a1e8fe0bdb..da9714f18a 100644 --- a/src/commands/Moderation/dm.js +++ b/src/commands/Moderation/dm.js @@ -5,10 +5,7 @@ import { logger } from '../../utils/logger.js'; import { sanitizeMarkdown } from '../../utils/validation.js'; import { InteractionHelper } from '../../utils/interactionHelper.js'; -<<<<<<< HEAD -======= import { replyUserError, ErrorTypes } from '../../utils/errorHandler.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) export default { data: new SlashCommandBuilder() .setName("dm") @@ -104,17 +101,10 @@ export default { logger.error('DM command error:', error); if (error.code === 50007) { -<<<<<<< HEAD - return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Could not send a DM to ${targetUser.tag}. They may have DMs disabled.' }); - } - - return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Failed to send DM: ${error.message}' }); -======= return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: `Could not send a DM to ${targetUser.tag}. They may have DMs disabled.` }); } return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: `Failed to send DM: ${error.message}` }); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } } }; \ No newline at end of file diff --git a/src/commands/Moderation/kick.js b/src/commands/Moderation/kick.js index 045ee9cf83..03fa9f677c 100644 --- a/src/commands/Moderation/kick.js +++ b/src/commands/Moderation/kick.js @@ -1,88 +1,5 @@ import { SlashCommandBuilder, PermissionFlagsBits } from 'discord.js'; import { successEmbed } from '../../utils/embeds.js'; -<<<<<<< HEAD -import { logger } from '../../utils/logger.js'; -import { InteractionHelper } from '../../utils/interactionHelper.js'; -import { ModerationService } from '../../services/moderationService.js'; -import { handleInteractionError, TitanBotError, ErrorTypes } from '../../utils/errorHandler.js'; - -export default { - data: new SlashCommandBuilder() - .setName("kick") - .setDescription("Kick a user from the server") - .addUserOption((option) => - option - .setName("target") - .setDescription("The user to kick") - .setRequired(true), - ) - .addStringOption((option) => - option.setName("reason").setDescription("Reason for the kick"), - ) -.setDefaultMemberPermissions(PermissionFlagsBits.KickMembers), - category: "moderation", - - async execute(interaction, config, client) { - try { - const targetUser = interaction.options.getUser("target"); - const member = interaction.options.getMember("target"); - const reason = interaction.options.getString("reason") || "No reason provided"; - - if (!targetUser) { - throw new TitanBotError( - 'Missing target user', - ErrorTypes.USER_INPUT, - 'You must specify a user to kick.', - { subtype: 'invalid_user' }, - ); - } - - if (targetUser.id === interaction.user.id) { - throw new TitanBotError( - "Cannot kick self", - ErrorTypes.VALIDATION, - "You cannot kick yourself." - ); - } - - if (targetUser.id === client.user.id) { - throw new TitanBotError( - "Cannot kick bot", - ErrorTypes.VALIDATION, - "You cannot kick the bot." - ); - } - - if (!member) { - throw new TitanBotError( - "Target not found", - ErrorTypes.USER_INPUT, - "The target user is not currently in this server.", - { subtype: 'user_not_found' } - ); - } - - const result = await ModerationService.kickUser({ - guild: interaction.guild, - member, - moderator: interaction.member, - reason, - }); - - await InteractionHelper.universalReply(interaction, { - embeds: [ - successEmbed( - `👢 **Kicked** ${targetUser.tag}`, - `**Reason:** ${reason}\n**Case ID:** #${result.caseId}`, - ), - ], - }); - } catch (error) { - logger.error('Kick command error:', error); - await handleInteractionError(interaction, error, { subtype: 'kick_failed' }); - } - } -======= import { InteractionHelper } from '../../utils/interactionHelper.js'; import { ModerationService } from '../../services/moderation/moderationService.js'; import { TitanBotError, ErrorTypes } from '../../utils/errorHandler.js'; @@ -158,5 +75,4 @@ export default { ], }); }, ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) }; diff --git a/src/commands/Moderation/lock.js b/src/commands/Moderation/lock.js index 109741f00b..75b251973f 100644 --- a/src/commands/Moderation/lock.js +++ b/src/commands/Moderation/lock.js @@ -5,10 +5,7 @@ import { logger } from '../../utils/logger.js'; import { getColor } from '../../config/bot.js'; import { InteractionHelper } from '../../utils/interactionHelper.js'; -<<<<<<< HEAD -======= import { replyUserError, ErrorTypes } from '../../utils/errorHandler.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) export default { data: new SlashCommandBuilder() .setName("lock") @@ -35,11 +32,7 @@ export default { try { const currentPermissions = channel.permissionsFor(everyoneRole); if (currentPermissions.has(PermissionFlagsBits.SendMessages) === false) { -<<<<<<< HEAD - return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: '${channel} is already locked.' }); -======= return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: `${channel} is already locked.` }); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } await channel.permissionOverwrites.edit( @@ -48,23 +41,6 @@ export default { { type: 0, reason: `Channel locked by ${interaction.user.tag}` }, ); -<<<<<<< HEAD - const lockEmbed = createEmbed( - "🔒 Channel Locked (Action Log)", - `${channel} has been locked down by ${interaction.user}.`, - ) -.setColor(getColor('moderation')) - .addFields( - { name: "Channel", value: channel.toString(), inline: true }, - { - name: "Moderator", - value: `${interaction.user.tag} (${interaction.user.id})`, - inline: true, - }, - ); - -======= ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) await logEvent({ client, guild: interaction.guild, diff --git a/src/commands/Moderation/massban.js b/src/commands/Moderation/massban.js index 803fa72a7e..9f6f8637a4 100644 --- a/src/commands/Moderation/massban.js +++ b/src/commands/Moderation/massban.js @@ -2,13 +2,8 @@ import { SlashCommandBuilder, PermissionFlagsBits, MessageFlags } from 'discord. import { createEmbed, successEmbed, warningEmbed } from '../../utils/embeds.js'; import { logModerationAction } from '../../utils/moderation.js'; import { logger } from '../../utils/logger.js'; -<<<<<<< HEAD -import { ModerationService } from '../../services/moderationService.js'; -import { TitanBotError } from '../../utils/errorHandler.js'; -======= import { ModerationService } from '../../services/moderation/moderationService.js'; import { TitanBotError, replyUserError, ErrorTypes } from '../../utils/errorHandler.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) import { InteractionHelper } from '../../utils/interactionHelper.js'; export default { @@ -113,11 +108,7 @@ export default { await interaction.guild.members.ban(userId, { reason: reason, -<<<<<<< HEAD - deleteMessageDays: deleteDays -======= deleteMessageSeconds: deleteDays * 24 * 60 * 60 ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) }); results.successful.push({ diff --git a/src/commands/Moderation/masskick.js b/src/commands/Moderation/masskick.js index 3d26df8f64..26e7e21a48 100644 --- a/src/commands/Moderation/masskick.js +++ b/src/commands/Moderation/masskick.js @@ -2,13 +2,8 @@ import { SlashCommandBuilder, PermissionFlagsBits, MessageFlags } from 'discord. import { createEmbed, successEmbed, warningEmbed } from '../../utils/embeds.js'; import { logModerationAction } from '../../utils/moderation.js'; import { logger } from '../../utils/logger.js'; -<<<<<<< HEAD -import { ModerationService } from '../../services/moderationService.js'; -import { TitanBotError } from '../../utils/errorHandler.js'; -======= import { ModerationService } from '../../services/moderation/moderationService.js'; import { TitanBotError, replyUserError, ErrorTypes } from '../../utils/errorHandler.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) import { InteractionHelper } from '../../utils/interactionHelper.js'; export default { diff --git a/src/commands/Moderation/purge.js b/src/commands/Moderation/purge.js index e411951b51..7b279f2d4f 100644 --- a/src/commands/Moderation/purge.js +++ b/src/commands/Moderation/purge.js @@ -5,10 +5,7 @@ import { logger } from '../../utils/logger.js'; import { getColor } from '../../config/bot.js'; import { InteractionHelper } from '../../utils/interactionHelper.js'; -<<<<<<< HEAD -======= import { replyUserError, ErrorTypes } from '../../utils/errorHandler.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) export default { data: new SlashCommandBuilder() .setName("purge") @@ -47,24 +44,6 @@ export default { const deleted = await channel.bulkDelete(fetched, true); const deletedCount = deleted.size; -<<<<<<< HEAD - const purgeEmbed = createEmbed( - "🗑️ Messages Purged (Action Log)", - `${deletedCount} messages were deleted by ${interaction.user}.`, - ) -.setColor(getColor('moderation')) - .addFields( - { name: "Channel", value: channel.toString(), inline: true }, - { - name: "Moderator", - value: `${interaction.user.tag} (${interaction.user.id})`, - inline: true, - }, - { name: "Count", value: `${deletedCount} messages`, inline: false }, - ); - -======= ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) await logEvent({ client, guild: interaction.guild, diff --git a/src/commands/Moderation/timeout.js b/src/commands/Moderation/timeout.js index 8266f180f3..936464ced6 100644 --- a/src/commands/Moderation/timeout.js +++ b/src/commands/Moderation/timeout.js @@ -1,15 +1,9 @@ import { SlashCommandBuilder, PermissionFlagsBits } from 'discord.js'; import { successEmbed } from '../../utils/embeds.js'; import { logger } from '../../utils/logger.js'; -<<<<<<< HEAD -import { TitanBotError, ErrorTypes, handleInteractionError } from '../../utils/errorHandler.js'; -import { InteractionHelper } from '../../utils/interactionHelper.js'; -import { ModerationService } from '../../services/moderationService.js'; -======= import { TitanBotError, ErrorTypes } from '../../utils/errorHandler.js'; import { InteractionHelper } from '../../utils/interactionHelper.js'; import { ModerationService } from '../../services/moderation/moderationService.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) const durationChoices = [ { name: "5 minutes", value: 5 }, @@ -37,20 +31,12 @@ export default { .setName("duration") .setDescription("Duration of the timeout") .setRequired(true) -<<<<<<< HEAD -.addChoices(...durationChoices), -======= .addChoices(...durationChoices), ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) ) .addStringOption((option) => option.setName("reason").setDescription("Reason for the timeout"), ) -<<<<<<< HEAD -.setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers), -======= .setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers), ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) category: "moderation", async execute(interaction, config, client) { @@ -59,80 +45,11 @@ export default { logger.warn(`Timeout interaction defer failed`, { userId: interaction.user.id, guildId: interaction.guildId, -<<<<<<< HEAD - commandName: 'timeout' -======= commandName: 'timeout', ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) }); return; } -<<<<<<< HEAD - try { - const targetUser = interaction.options.getUser("target"); - const member = interaction.options.getMember("target"); - const durationMinutes = interaction.options.getInteger("duration"); - const reason = interaction.options.getString("reason") || "No reason provided"; - - if (!targetUser) { - throw new TitanBotError( - 'Missing target user', - ErrorTypes.USER_INPUT, - 'You must specify a user to timeout.', - { subtype: 'invalid_user' }, - ); - } - - if (targetUser.id === interaction.user.id) { - throw new TitanBotError( - "Cannot timeout self", - ErrorTypes.VALIDATION, - "You cannot timeout yourself." - ); - } - if (targetUser.id === client.user.id) { - throw new TitanBotError( - "Cannot timeout bot", - ErrorTypes.VALIDATION, - "You cannot timeout the bot." - ); - } - if (!member) { - throw new TitanBotError( - "Target not found", - ErrorTypes.USER_INPUT, - "The target user is not currently in this server." - ); - } - - const durationMs = durationMinutes * 60 * 1000; - const result = await ModerationService.timeoutUser({ - guild: interaction.guild, - member, - moderator: interaction.member, - durationMs, - reason, - }); - - const durationDisplay = - durationChoices.find((c) => c.value === durationMinutes) - ?.name || `${durationMinutes} minutes`; - - await InteractionHelper.safeEditReply(interaction, { - embeds: [ - successEmbed( - `⏳ **Timed out** ${targetUser.tag} for ${durationDisplay}.`, - `**Reason:** ${reason}\n**Case ID:** #${result.caseId}`, - ), - ], - }); - } catch (error) { - logger.error('Timeout command error:', error); - await handleInteractionError(interaction, error, { subtype: 'timeout_failed' }); - } - } -======= const targetUser = interaction.options.getUser("target"); const member = interaction.options.getMember("target"); const durationMinutes = interaction.options.getInteger("duration"); @@ -191,5 +108,4 @@ export default { ], }); }, ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) }; diff --git a/src/commands/Moderation/unban.js b/src/commands/Moderation/unban.js index 26e0466040..484cdec02a 100644 --- a/src/commands/Moderation/unban.js +++ b/src/commands/Moderation/unban.js @@ -1,45 +1,24 @@ import { SlashCommandBuilder, PermissionFlagsBits } from 'discord.js'; -<<<<<<< HEAD -import { createEmbed, errorEmbed, successEmbed, infoEmbed, warningEmbed } from '../../utils/embeds.js'; -import { logModerationAction } from '../../utils/moderation.js'; -import { logger } from '../../utils/logger.js'; -import { ModerationService } from '../../services/moderationService.js'; -import { handleInteractionError } from '../../utils/errorHandler.js'; -import { InteractionHelper } from '../../utils/interactionHelper.js'; -======= import { successEmbed } from '../../utils/embeds.js'; import { logger } from '../../utils/logger.js'; import { ModerationService } from '../../services/moderation/moderationService.js'; import { replyUserError, ErrorTypes } from '../../utils/errorHandler.js'; import { InteractionHelper } from '../../utils/interactionHelper.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) export default { data: new SlashCommandBuilder() .setName("unban") .setDescription("Unban a user from the server") -<<<<<<< HEAD - .addUserOption(option => - option - .setName("target") - .setDescription("The user to unban (can be ID or mention)") - .setRequired(true) -======= .addStringOption(option => option .setName("target") .setDescription("The ID (or mention) of the user to unban") .setRequired(true), ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) ) .addStringOption(option => option.setName("reason") .setDescription("Reason for the unban") -<<<<<<< HEAD - .setRequired(false) -======= .setRequired(false), ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) ) .setDefaultMemberPermissions(PermissionFlagsBits.BanMembers), category: "moderation", @@ -50,42 +29,11 @@ export default { logger.warn(`Unban interaction defer failed`, { userId: interaction.user.id, guildId: interaction.guildId, -<<<<<<< HEAD - commandName: 'unban' -======= commandName: 'unban', ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) }); return; } -<<<<<<< HEAD - try { - const targetUser = interaction.options.getUser("target"); - const reason = interaction.options.getString("reason") || "No reason provided"; - - const result = await ModerationService.unbanUser({ - guild: interaction.guild, - user: targetUser, - moderator: interaction.member, - reason - }); - - await InteractionHelper.safeEditReply(interaction, { - embeds: [ - successEmbed( - "✅ User Unbanned", - `Successfully unbanned **${targetUser.tag}** from the server.\n\n**Reason:** ${reason}\n**Case ID:** #${result.caseId}` - ) - ] - }); - } catch (error) { - logger.error('Unban command error:', error); - await handleInteractionError(interaction, error, { subtype: 'unban_failed' }); - } - } -}; -======= const rawTarget = interaction.options.getString("target"); const targetId = rawTarget.replace(/[<@!>]/g, '').trim(); @@ -123,4 +71,3 @@ export default { }); }, }; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) diff --git a/src/commands/Moderation/unlock.js b/src/commands/Moderation/unlock.js index 568cfe90f6..408c2b3b07 100644 --- a/src/commands/Moderation/unlock.js +++ b/src/commands/Moderation/unlock.js @@ -5,10 +5,7 @@ import { logger } from '../../utils/logger.js'; import { getColor } from '../../config/bot.js'; import { InteractionHelper } from '../../utils/interactionHelper.js'; -<<<<<<< HEAD -======= import { replyUserError, ErrorTypes } from '../../utils/errorHandler.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) export default { data: new SlashCommandBuilder() .setName("unlock") @@ -40,11 +37,7 @@ export default { currentPermissions.has(PermissionFlagsBits.SendMessages) === null ) { -<<<<<<< HEAD - return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: '${channel} is not explicitly locked (everyone can already send messages).' }); -======= return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: `${channel} is not explicitly locked (everyone can already send messages).` }); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } await channel.permissionOverwrites.edit( @@ -56,27 +49,6 @@ export default { }, ); -<<<<<<< HEAD - const unlockEmbed = createEmbed( - "🔓 Channel Unlocked (Action Log)", - `${channel} has been unlocked by ${interaction.user}.`, - ) -.setColor(getColor('success')) - .addFields( - { - name: "Channel", - value: channel.toString(), - inline: true, - }, - { - name: "Moderator", - value: `${interaction.user.tag} (${interaction.user.id})`, - inline: true, - }, - ); - -======= ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) await logEvent({ client, guild: interaction.guild, diff --git a/src/commands/Moderation/untimeout.js b/src/commands/Moderation/untimeout.js index 0baca98d00..104a108f2e 100644 --- a/src/commands/Moderation/untimeout.js +++ b/src/commands/Moderation/untimeout.js @@ -1,13 +1,8 @@ import { SlashCommandBuilder, PermissionFlagsBits } from 'discord.js'; import { successEmbed } from '../../utils/embeds.js'; import { logger } from '../../utils/logger.js'; -<<<<<<< HEAD -import { ModerationService } from '../../services/moderationService.js'; -import { handleInteractionError, TitanBotError, ErrorTypes } from '../../utils/errorHandler.js'; -======= import { ModerationService } from '../../services/moderation/moderationService.js'; import { TitanBotError, ErrorTypes } from '../../utils/errorHandler.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) import { InteractionHelper } from '../../utils/interactionHelper.js'; export default { @@ -20,11 +15,7 @@ export default { .setDescription("User to untimeout") .setRequired(true), ) -<<<<<<< HEAD -.setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers), -======= .setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers), ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) category: "moderation", async execute(interaction, config, client) { @@ -33,56 +24,11 @@ export default { logger.warn(`Untimeout interaction defer failed`, { userId: interaction.user.id, guildId: interaction.guildId, -<<<<<<< HEAD - commandName: 'untimeout' -======= commandName: 'untimeout', ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) }); return; } -<<<<<<< HEAD - try { - const targetUser = interaction.options.getUser("target"); - const member = interaction.options.getMember("target"); - - if (!targetUser) { - throw new TitanBotError( - 'Missing target user', - ErrorTypes.USER_INPUT, - 'You must specify a user to untimeout.', - { subtype: 'invalid_user' }, - ); - } - - if (!member) { - throw new TitanBotError( - "Target not found", - ErrorTypes.USER_INPUT, - "The target user is not currently in this server." - ); - } - - await ModerationService.removeTimeoutUser({ - guild: interaction.guild, - member, - moderator: interaction.member - }); - - await InteractionHelper.safeEditReply(interaction, { - embeds: [ - successEmbed( - `🔓 **Removed timeout** from ${targetUser.tag}`, - ), - ], - }); - } catch (error) { - logger.error('Untimeout command error:', error); - await handleInteractionError(interaction, error, { subtype: 'untimeout_failed' }); - } - } -======= const targetUser = interaction.options.getUser("target"); const member = interaction.options.getMember("target"); @@ -117,5 +63,4 @@ export default { ], }); }, ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) }; diff --git a/src/commands/Moderation/usernotes.js b/src/commands/Moderation/usernotes.js index ea8dfccdd5..4fc581fec7 100644 --- a/src/commands/Moderation/usernotes.js +++ b/src/commands/Moderation/usernotes.js @@ -1,25 +1,11 @@ import { SlashCommandBuilder, PermissionFlagsBits, MessageFlags } from 'discord.js'; import { createEmbed, successEmbed, infoEmbed, warningEmbed } from '../../utils/embeds.js'; import { logger } from '../../utils/logger.js'; -<<<<<<< HEAD -import { getFromDb, setInDb, deleteFromDb } from '../../utils/database.js'; -import { sanitizeInput } from '../../utils/validation.js'; - -import { InteractionHelper } from '../../utils/interactionHelper.js'; -function getUserNotesKey(guildId, userId) { - return `moderation_user_notes_${guildId}_${userId}`; -} - -function getGuildNotesListKey(guildId) { - return `moderation_user_notes_list_${guildId}`; -} -======= import { getFromDb, setInDb, deleteFromDb, getUserNotesKey, getUserNotesListKey } from '../../utils/database.js'; import { sanitizeInput } from '../../utils/validation.js'; import { InteractionHelper } from '../../utils/interactionHelper.js'; import { replyUserError, ErrorTypes } from '../../utils/errorHandler.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) export default { data: new SlashCommandBuilder() @@ -214,16 +200,6 @@ async function handleViewNotes(interaction, targetUser, notes) { } async function handleRemoveNote(interaction, targetUser, notes, guildId) { -<<<<<<< HEAD -const index = interaction.options.getInteger("index") - 1; - - if (index < 0 || index >= notes.length) { - return await replyUserError(interaction, { type: ErrorTypes.VALIDATION, message: 'Please provide a valid note index (1-${notes.length}).' }); - } - - const removedNote = notes[index]; - notes.splice(index, 1); -======= const index = interaction.options.getInteger("index") - 1; if (index < 0 || index >= notes.length) { @@ -236,7 +212,6 @@ const index = interaction.options.getInteger("index") - 1; const removedNote = sortedNotes[index]; const originalIndex = notes.indexOf(removedNote); notes.splice(originalIndex, 1); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) const notesKey = getUserNotesKey(guildId, targetUser.id); await setInDb(notesKey, notes); diff --git a/src/commands/Moderation/warn.js b/src/commands/Moderation/warn.js index 5da638e40c..1d1b2ef1eb 100644 --- a/src/commands/Moderation/warn.js +++ b/src/commands/Moderation/warn.js @@ -2,15 +2,9 @@ import { SlashCommandBuilder, PermissionFlagsBits, PermissionsBitField, ChannelT import { createEmbed, errorEmbed, successEmbed, infoEmbed, warningEmbed } from '../../utils/embeds.js'; import { logModerationAction } from '../../utils/moderation.js'; import { logger } from '../../utils/logger.js'; -<<<<<<< HEAD -import { WarningService } from '../../services/warningService.js'; -import { ModerationService } from '../../services/moderationService.js'; -import { handleInteractionError, TitanBotError, ErrorTypes } from '../../utils/errorHandler.js'; -======= import { WarningService } from '../../services/moderation/warningService.js'; import { ModerationService } from '../../services/moderation/moderationService.js'; import { TitanBotError, ErrorTypes } from '../../utils/errorHandler.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) import { InteractionHelper } from '../../utils/interactionHelper.js'; export default { data: new SlashCommandBuilder() @@ -42,94 +36,6 @@ export default { return; } -<<<<<<< HEAD - try { - const target = interaction.options.getUser("target"); - const member = interaction.options.getMember("target"); - const reason = interaction.options.getString("reason"); - const moderator = interaction.user; - const guildId = interaction.guildId; - - if (!target) { - throw new TitanBotError( - 'Missing target user', - ErrorTypes.USER_INPUT, - 'You must specify a user to warn.', - { subtype: 'invalid_user' }, - ); - } - - if (!reason) { - throw new TitanBotError( - 'Missing warning reason', - ErrorTypes.VALIDATION, - 'You must provide a reason for the warning.', - { subtype: 'missing_required' }, - ); - } - - if (!member) { - throw new TitanBotError( - "Target not found", - ErrorTypes.USER_INPUT, - "The target user is not currently in this server." - ); - } - - const hierarchyCheck = ModerationService.validateHierarchy(interaction.member, member, 'warn'); - if (!hierarchyCheck.valid) { - throw new TitanBotError( - hierarchyCheck.error, - ErrorTypes.PERMISSION, - hierarchyCheck.error - ); - } - - const result = await WarningService.addWarning({ - guildId, - userId: target.id, - moderatorId: moderator.id, - reason, - timestamp: Date.now() - }); - - if (!result.success) { - throw new Error("Failed to store warning in database"); - } - - const totalWarns = result.totalCount; - - await logModerationAction({ - client, - guild: interaction.guild, - event: { - action: "User Warned", - target: `${target.tag} (${target.id})`, - executor: `${moderator.tag} (${moderator.id})`, - reason, - metadata: { - userId: target.id, - moderatorId: moderator.id, - totalWarns, - warningNumber: totalWarns, - warningId: result.id - } - } - }); - - await InteractionHelper.safeEditReply(interaction, { - embeds: [ - successEmbed( - `⚠️ **Warned** ${target.tag}`, - `**Reason:** ${reason}\n**Total Warns:** ${totalWarns}`, - ), - ], - }); - } catch (error) { - logger.error('Warn command error:', error); - await handleInteractionError(interaction, error, { subtype: 'warn_failed' }); - } -======= const target = interaction.options.getUser("target"); const member = interaction.options.getMember("target"); const reason = interaction.options.getString("reason"); @@ -198,6 +104,5 @@ export default { ), ], }); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } }; \ No newline at end of file diff --git a/src/commands/Moderation/warnings.js b/src/commands/Moderation/warnings.js index 782148d0b9..59fbce081c 100644 --- a/src/commands/Moderation/warnings.js +++ b/src/commands/Moderation/warnings.js @@ -1,20 +1,11 @@ import { getColor } from '../../config/bot.js'; import { SlashCommandBuilder, PermissionFlagsBits, ActionRowBuilder, ButtonBuilder, ButtonStyle } from 'discord.js'; -<<<<<<< HEAD -import { createEmbed, errorEmbed, successEmbed } from '../../utils/embeds.js'; -import { logEvent } from '../../utils/moderation.js'; -import { logger } from '../../utils/logger.js'; -import { WarningService } from '../../services/warningService.js'; -import { handleInteractionError } from '../../utils/errorHandler.js'; -import { InteractionHelper } from '../../utils/interactionHelper.js'; -======= import { createEmbed } from '../../utils/embeds.js'; import { logEvent } from '../../utils/moderation.js'; import { logger } from '../../utils/logger.js'; import { WarningService } from '../../services/moderation/warningService.js'; import { InteractionHelper } from '../../utils/interactionHelper.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) export default { data: new SlashCommandBuilder() .setName("warnings") @@ -34,88 +25,11 @@ export default { logger.warn(`Warnings interaction defer failed`, { userId: interaction.user.id, guildId: interaction.guildId, -<<<<<<< HEAD - commandName: 'warnings' -======= commandName: 'warnings', ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) }); return; } -<<<<<<< HEAD - try { - const target = interaction.options.getUser("target"); - const guildId = interaction.guildId; - - const validWarnings = await WarningService.getWarnings(guildId, target.id); - const totalWarns = validWarnings.length; - - if (totalWarns === 0) { - await InteractionHelper.safeEditReply(interaction, { - embeds: [ - createEmbed({ - title: `Warnings: ${target.tag}`, - description: "This user has no recorded warnings." - }).setColor(getColor('success')), - ], - }); - return; - } - - const embed = createEmbed({ - title: `Warnings: ${target.tag}`, - description: `Total Warnings: **${totalWarns}**` - }).setColor(getColor('warning')); - - const warningFields = validWarnings - .map((w, i) => { - const discordTimestamp = Math.floor(w.timestamp / 1000); - return { - name: `[#${i + 1}] Reason: ${w.reason.substring(0, 100)}`, - value: `**Moderator:** <@${w.moderatorId}>\n**Date:** ()`, - inline: false, - }; - }) - .slice(0, 25); - - embed.addFields(warningFields); - - const actionRow = new ActionRowBuilder().addComponents( - new ButtonBuilder() - .setCustomId(`warning_delete_specific:${target.id}:${interaction.user.id}`) - .setLabel('Delete Specific Warning') - .setStyle(ButtonStyle.Danger), - new ButtonBuilder() - .setCustomId(`warning_clear_all:${target.id}:${interaction.user.id}`) - .setLabel('Clear All Warnings') - .setStyle(ButtonStyle.Danger) - ); - - await logEvent({ - client, - guild: interaction.guild, - event: { - action: "Warnings Viewed", - target: `${target.tag} (${target.id})`, - executor: `${interaction.user.tag} (${interaction.user.id})`, - reason: `Viewed ${totalWarns} warnings`, - metadata: { - userId: target.id, - moderatorId: interaction.user.id, - totalWarnings: totalWarns - } - } - }); - - await InteractionHelper.safeEditReply(interaction, { embeds: [embed], components: [actionRow] }); - } catch (error) { - logger.error('Warnings command error:', error); - await handleInteractionError(interaction, error, { subtype: 'warnings_view_failed' }); - } - } -}; -======= const target = interaction.options.getUser("target"); const guildId = interaction.guildId; @@ -182,4 +96,3 @@ export default { await InteractionHelper.safeEditReply(interaction, { embeds: [embed], components: [actionRow] }); }, }; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) diff --git a/src/commands/Music/join.js b/src/commands/Music/join.js index 45e0cdf6c1..7606a2a492 100644 --- a/src/commands/Music/join.js +++ b/src/commands/Music/join.js @@ -1,9 +1,5 @@ import { SlashCommandBuilder } from 'discord.js'; import { InteractionHelper } from '../../utils/interactionHelper.js'; -<<<<<<< HEAD -import { handleInteractionError } from '../../utils/errorHandler.js'; -======= ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) import { joinVoiceChannel, replyMusicSuccess } from '../../services/music/musicActions.js'; import { deferMusicCommand } from '../../services/music/prefixSupport.js'; @@ -14,18 +10,8 @@ export default { .setDescription('Join your voice channel without starting playback'), async execute(interaction, config, client) { -<<<<<<< HEAD - try { - await deferMusicCommand(interaction); - const embed = await joinVoiceChannel(client, interaction); - await replyMusicSuccess(interaction, embed); - } catch (error) { - await handleInteractionError(interaction, error, { command: 'join' }); - } -======= await deferMusicCommand(interaction); const embed = await joinVoiceChannel(client, interaction); await replyMusicSuccess(interaction, embed); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) }, }; diff --git a/src/commands/Music/music.js b/src/commands/Music/music.js index 676243b36b..fa8d7cfef6 100644 --- a/src/commands/Music/music.js +++ b/src/commands/Music/music.js @@ -1,9 +1,5 @@ import { SlashCommandBuilder } from 'discord.js'; import { InteractionHelper } from '../../utils/interactionHelper.js'; -<<<<<<< HEAD -import { handleInteractionError } from '../../utils/errorHandler.js'; -======= ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) import { skipTrack, stopPlayback, @@ -109,90 +105,6 @@ export default { ), async execute(interaction, config, client) { -<<<<<<< HEAD - try { - await deferMusicCommand(interaction); - const subcommand = interaction.options.getSubcommand(); - - switch (subcommand) { - case 'pause': { - const embed = await pausePlayback(client, interaction); - await replyMusicSuccess(interaction, embed); - break; - } - case 'resume': { - const embed = await resumePlayback(client, interaction); - await replyMusicSuccess(interaction, embed); - break; - } - case 'skip': { - const embed = await skipTrack(client, interaction); - await replyMusicSuccess(interaction, embed); - break; - } - case 'stop': { - const embed = await stopPlayback(client, interaction); - await replyMusicSuccess(interaction, embed); - break; - } - case 'shuffle': { - const embed = await shuffleQueue(client, interaction); - await replyMusicSuccess(interaction, embed); - break; - } - case 'loop': { - const embed = await setLoopMode(client, interaction, interaction.options.getString('mode')); - await replyMusicSuccess(interaction, embed); - break; - } - case 'volume': { - const embed = await setVolume(client, interaction, interaction.options.getInteger('level')); - await replyMusicSuccess(interaction, embed); - break; - } - case 'seek': { - const embed = await seekTrack(client, interaction, interaction.options.getInteger('seconds')); - await replyMusicSuccess(interaction, embed); - break; - } - case 'remove': { - const embed = await removeFromQueue(client, interaction, interaction.options.getInteger('position')); - await replyMusicSuccess(interaction, embed); - break; - } - case 'move': { - const embed = await moveInQueue( - client, - interaction, - interaction.options.getInteger('from'), - interaction.options.getInteger('to'), - ); - await replyMusicSuccess(interaction, embed); - break; - } - case 'clear': { - const embed = await clearQueue(client, interaction); - await replyMusicSuccess(interaction, embed); - break; - } - case 'leave': { - const embed = await leaveVoiceChannel(client, interaction); - await replyMusicSuccess(interaction, embed); - break; - } - case '247': { - const embed = await setTwentyFourSeven(client, interaction, interaction.options.getBoolean('enabled')); - await replyMusicSuccess(interaction, embed); - break; - } - default: - await InteractionHelper.safeEditReply(interaction, { - content: 'Unknown music subcommand.', - }); - } - } catch (error) { - await handleInteractionError(interaction, error, { command: 'music' }); -======= await deferMusicCommand(interaction); const subcommand = interaction.options.getSubcommand(); @@ -271,7 +183,6 @@ export default { await InteractionHelper.safeEditReply(interaction, { content: 'Unknown music subcommand.', }); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } }, }; diff --git a/src/commands/Music/nowplaying.js b/src/commands/Music/nowplaying.js index 9bbec1a7a4..e50c685122 100644 --- a/src/commands/Music/nowplaying.js +++ b/src/commands/Music/nowplaying.js @@ -1,9 +1,5 @@ import { SlashCommandBuilder } from 'discord.js'; import { InteractionHelper } from '../../utils/interactionHelper.js'; -<<<<<<< HEAD -import { handleInteractionError } from '../../utils/errorHandler.js'; -======= ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) import { buildNowPlayingReply } from '../../services/music/musicActions.js'; import { deferMusicCommand } from '../../services/music/prefixSupport.js'; @@ -14,18 +10,8 @@ export default { .setDescription('Show the currently playing track'), async execute(interaction, config, client) { -<<<<<<< HEAD - try { - await deferMusicCommand(interaction); - const payload = buildNowPlayingReply(client, interaction.guild.id); - await InteractionHelper.safeEditReply(interaction, payload); - } catch (error) { - await handleInteractionError(interaction, error, { command: 'nowplaying' }); - } -======= await deferMusicCommand(interaction); const payload = buildNowPlayingReply(client, interaction.guild.id); await InteractionHelper.safeEditReply(interaction, payload); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) }, }; diff --git a/src/commands/Music/play.js b/src/commands/Music/play.js index 42599fe2f3..193b2d4d69 100644 --- a/src/commands/Music/play.js +++ b/src/commands/Music/play.js @@ -1,9 +1,5 @@ import { SlashCommandBuilder, MessageFlags } from 'discord.js'; import { InteractionHelper } from '../../utils/interactionHelper.js'; -<<<<<<< HEAD -import { handleInteractionError } from '../../utils/errorHandler.js'; -======= ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) import { playQuery, replyMusicSuccess } from '../../services/music/musicActions.js'; export default { @@ -17,18 +13,8 @@ export default { ), async execute(interaction, config, client) { -<<<<<<< HEAD - try { - await InteractionHelper.safeDefer(interaction, { flags: MessageFlags.Ephemeral }); - const result = await playQuery(client, interaction, interaction.options.getString('query')); - await replyMusicSuccess(interaction, result.embed); - } catch (error) { - await handleInteractionError(interaction, error, { command: 'play' }); - } -======= await InteractionHelper.safeDefer(interaction, { flags: MessageFlags.Ephemeral }); const result = await playQuery(client, interaction, interaction.options.getString('query')); await replyMusicSuccess(interaction, result.embed); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) }, }; diff --git a/src/commands/Music/queue.js b/src/commands/Music/queue.js index 3f98d2ee02..66faa4eb9d 100644 --- a/src/commands/Music/queue.js +++ b/src/commands/Music/queue.js @@ -1,9 +1,5 @@ import { SlashCommandBuilder, MessageFlags } from 'discord.js'; import { InteractionHelper } from '../../utils/interactionHelper.js'; -<<<<<<< HEAD -import { handleInteractionError } from '../../utils/errorHandler.js'; -======= ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) import { buildQueueReply } from '../../services/music/musicActions.js'; export default { @@ -17,19 +13,6 @@ export default { ), async execute(interaction, config, client) { -<<<<<<< HEAD - try { - await InteractionHelper.safeDefer(interaction, { flags: MessageFlags.Ephemeral }); - const page = (interaction.options.getInteger('page') || 1) - 1; - const payload = buildQueueReply(client, interaction.guild.id, page); - await InteractionHelper.safeEditReply(interaction, { - embeds: payload.embeds, - components: payload.components, - }); - } catch (error) { - await handleInteractionError(interaction, error, { command: 'queue' }); - } -======= await InteractionHelper.safeDefer(interaction, { flags: MessageFlags.Ephemeral }); const page = (interaction.options.getInteger('page') || 1) - 1; const payload = buildQueueReply(client, interaction.guild.id, page); @@ -37,6 +20,5 @@ export default { embeds: payload.embeds, components: payload.components, }); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) }, }; diff --git a/src/commands/Reaction_roles/reactroles.js b/src/commands/Reaction_roles/reactroles.js index 227387eccf..692443c5e9 100644 --- a/src/commands/Reaction_roles/reactroles.js +++ b/src/commands/Reaction_roles/reactroles.js @@ -2,11 +2,7 @@ import { getColor } from '../../config/bot.js'; import { SlashCommandBuilder, PermissionFlagsBits, ChannelType, ActionRowBuilder, StringSelectMenuBuilder, StringSelectMenuOptionBuilder, RoleSelectMenuBuilder, ModalBuilder, TextInputBuilder, TextInputStyle, ButtonBuilder, ButtonStyle, MessageFlags, ComponentType, EmbedBuilder, LabelBuilder, CheckboxBuilder, TextDisplayBuilder } from 'discord.js'; import { createEmbed, successEmbed, infoEmbed, warningEmbed } from '../../utils/embeds.js'; import { logger } from '../../utils/logger.js'; -<<<<<<< HEAD -import { handleInteractionError, createError, TitanBotError, ErrorTypes, replyUserError } from '../../utils/errorHandler.js'; -======= import { createError, TitanBotError, ErrorTypes, replyUserError } from '../../utils/errorHandler.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) import { InteractionHelper } from '../../utils/interactionHelper.js'; import { createReactionRoleMessage, hasDangerousPermissions, getAllReactionRoleMessages, deleteReactionRoleMessage } from '../../services/reactionRoleService.js'; import { logEvent, EVENT_TYPES } from '../../services/loggingService.js'; @@ -15,10 +11,6 @@ import { formatPanelStatusField, } from '../../utils/panelStatus.js'; import { startDashboardSession } from '../../utils/dashboardSession.js'; -<<<<<<< HEAD - -const DASHBOARD_EPHEMERAL = MessageFlags.Ephemeral; -======= import { getReactionRoleKey } from '../../utils/database/keys.js'; const DASHBOARD_EPHEMERAL = MessageFlags.Ephemeral; @@ -29,7 +21,6 @@ function truncateText(value, maxLength) { const text = String(value ?? ''); return text.length > maxLength ? text.substring(0, maxLength) : text; } ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) export default { data: new SlashCommandBuilder() @@ -43,10 +34,7 @@ export default { .addChannelOption(option => option.setName('channel') .setDescription('The channel to send the reaction role message to') -<<<<<<< HEAD -======= .addChannelTypes(ChannelType.GuildText, ChannelType.GuildAnnouncement) ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) .setRequired(true) ) .addStringOption(option => @@ -101,27 +89,11 @@ export default { async execute(interaction) { const subcommand = interaction.options.getSubcommand(); -<<<<<<< HEAD - try { - if (subcommand === 'setup') { - await handleSetup(interaction); - } else if (subcommand === 'dashboard') { - const selectedPanelId = interaction.options.getString('panel'); - await handleDashboard(interaction, selectedPanelId); - } - } catch (error) { - await handleInteractionError(interaction, error, { - type: 'command', - commandName: 'reactroles', - subcommand: subcommand - }); -======= if (subcommand === 'setup') { await handleSetup(interaction); } else if (subcommand === 'dashboard') { const selectedPanelId = interaction.options.getString('panel'); await handleDashboard(interaction, selectedPanelId); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } }, @@ -129,17 +101,6 @@ export default { if (interaction.commandName !== 'reactroles') return; if (interaction.options.getSubcommand() !== 'dashboard') return; -<<<<<<< HEAD - try { - const guildId = interaction.guild.id; - const client = interaction.client; - - let panels; - try { - panels = await getAllReactionRoleMessages(client, guildId); - } catch (dbError) { - -======= // Autocomplete must respond within 3s. Build choices from stored panel data and // cached channels/messages only — no network fetches — to avoid DiscordAPIError 10062. try { @@ -151,75 +112,15 @@ export default { try { panels = await getAllReactionRoleMessages(client, guildId); } catch { ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) await interaction.respond([]).catch(() => {}); return; } -<<<<<<< HEAD - if (!panels || panels.length === 0) { -======= if (!panels?.length) { ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) - await interaction.respond([]).catch(() => {}); - return; - } - -<<<<<<< HEAD - const guild = interaction.guild; - - const validPanels = []; - for (const panel of panels) { - - if (!panel.messageId || !panel.channelId) { - continue; - } - - const channel = guild.channels.cache.get(panel.channelId); - if (!channel) { - await deleteReactionRoleMessage(client, guildId, panel.messageId).catch(() => {}); - continue; - } - - const msg = await channel.messages.fetch(panel.messageId).catch(() => null); - if (!msg) { - await deleteReactionRoleMessage(client, guildId, panel.messageId).catch(() => {}); - continue; - } - validPanels.push(panel); - } - - if (validPanels.length === 0) { await interaction.respond([]).catch(() => {}); return; } - const choices = await Promise.all( - validPanels.slice(0, 25).map(async panel => { - try { - const channel = guild.channels.cache.get(panel.channelId); - if (!channel) return null; - - const msg = await channel.messages.fetch(panel.messageId).catch(() => null); - if (!msg) return null; - - const title = msg?.embeds?.[0]?.title ?? 'Untitled Panel'; - const channelName = channel?.name ?? 'unknown'; - - return { - name: `${title} (${channelName})`.substring(0, 100), - value: panel.messageId - }; - } catch (e) { - return null; - } - }) - ); - - const validChoices = choices.filter(c => c !== null); - await interaction.respond(validChoices).catch(() => {}); - } catch (error) { -======= const choices = []; for (const panel of panels) { if (!panel.messageId || !panel.channelId) continue; @@ -239,7 +140,6 @@ export default { await interaction.respond(choices).catch(() => {}); } catch { ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) await interaction.respond([]).catch(() => {}); } } @@ -294,22 +194,16 @@ async function handleSetup(interaction) { const roles = []; const roleValidationErrors = []; -<<<<<<< HEAD -======= const seenRoleIds = new Set(); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) for (let i = 1; i <= 5; i++) { const role = interaction.options.getRole(`role${i}`); if (role) { -<<<<<<< HEAD -======= if (seenRoleIds.has(role.id)) { roleValidationErrors.push(`**${role.name}** - This role was selected more than once`); continue; } ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) if (role.position >= interaction.guild.members.me.roles.highest.position) { roleValidationErrors.push(`**${role.name}** - My bot's role is positioned lower than this role in your server's role hierarchy and cannot assign it`); continue; @@ -330,10 +224,7 @@ async function handleSetup(interaction) { continue; } -<<<<<<< HEAD -======= seenRoleIds.add(role.id); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) roles.push(role); } } @@ -352,11 +243,7 @@ async function handleSetup(interaction) { await interaction.followUp({ embeds: [warningEmbed('Role Validation Warning', errorMsg)], -<<<<<<< HEAD - ephemeral: true -======= flags: MessageFlags.Ephemeral ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) }); } @@ -377,13 +264,8 @@ async function handleSetup(interaction) { .setMaxValues(roles.length) .addOptions( roles.map(role => ({ -<<<<<<< HEAD - label: role.name, - description: `Add/remove the ${role.name} role`, -======= label: truncateText(role.name, SELECT_OPTION_LABEL_LIMIT), description: truncateText(`Add/remove the ${role.name} role`, SELECT_OPTION_DESCRIPTION_LIMIT), ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) value: role.id, emoji: '🎭' })) @@ -406,16 +288,6 @@ async function handleSetup(interaction) { }); const roleIds = roles.map(role => role.id); -<<<<<<< HEAD - await createReactionRoleMessage( - interaction.client, - interaction.guildId, - channel.id, - message.id, - roleIds - ); - -======= try { await createReactionRoleMessage( interaction.client, @@ -431,7 +303,6 @@ async function handleSetup(interaction) { throw saveError; } ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) logger.info(`Reaction role message created: ${message.id} with ${roles.length} roles by ${interaction.user.tag}`); try { @@ -633,15 +504,9 @@ function buildReactionRoleDashboardPayload(panelData, discordMsg, guildId, guild async function migrateReactionRoleMessageId(client, guildId, panelData, newMessageId) { if (!newMessageId || panelData.messageId === newMessageId) return; -<<<<<<< HEAD - const oldKey = `reaction_roles:${guildId}:${panelData.messageId}`; - panelData.messageId = newMessageId; - await client.db.set(`reaction_roles:${guildId}:${newMessageId}`, panelData); -======= const oldKey = getReactionRoleKey(guildId, panelData.messageId); panelData.messageId = newMessageId; await client.db.set(getReactionRoleKey(guildId, newMessageId), panelData); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) await client.db.delete(oldKey).catch(() => {}); } @@ -960,11 +825,7 @@ async function handleAddRole(selectInteraction, rootInteraction, panelData, guil } panelData.roles.push(role.id); -<<<<<<< HEAD - const key = `reaction_roles:${guildId}:${panelData.messageId}`; -======= const key = getReactionRoleKey(guildId, panelData.messageId); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) await client.db.set(key, panelData); await rebuildLivePanelMessage(guild, panelData); @@ -1093,11 +954,7 @@ async function handleRemoveRole(selectInteraction, rootInteraction, panelData, p }); } } else { -<<<<<<< HEAD - const key = `reaction_roles:${guildId}:${panelData.messageId}`; -======= const key = getReactionRoleKey(guildId, panelData.messageId); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) await client.db.set(key, panelData); await rebuildLivePanelMessage(guild, panelData); diff --git a/src/commands/Search/google.js b/src/commands/Search/google.js deleted file mode 100644 index 655072c9f2..0000000000 --- a/src/commands/Search/google.js +++ /dev/null @@ -1,74 +0,0 @@ -import { SlashCommandBuilder, MessageFlags } from 'discord.js'; -import { createEmbed, errorEmbed, successEmbed, infoEmbed, warningEmbed } from '../../utils/embeds.js'; -import { logger } from '../../utils/logger.js'; -<<<<<<< HEAD -import { handleInteractionError } from '../../utils/errorHandler.js'; -======= ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) -import { getColor } from '../../config/bot.js'; - -import { InteractionHelper } from '../../utils/interactionHelper.js'; -export default { - data: new SlashCommandBuilder() - .setName('google') - .setDescription('Search Google') - .addStringOption(option => - option.setName('query') - .setDescription('What would you like to search for?') - .setRequired(true)), - async execute(interaction) { -<<<<<<< HEAD - try { - const query = interaction.options.getString('query'); - const searchUrl = `https://www.google.com/search?q=${encodeURIComponent(query)}`; - - const embed = createEmbed({ - title: 'Google Search', - description: `[Search for "${query}"](${searchUrl})`, - color: 'info' - }) - .setFooter({ text: 'Google Search Results' }); - - await InteractionHelper.safeReply(interaction, { embeds: [embed] }); - - logger.info('Google search link generated', { - userId: interaction.user.id, - query: query, - guildId: interaction.guildId, - commandName: 'google' - }); - } catch (error) { - logger.error('Error in google command', { - error: error.message, - stack: error.stack, - userId: interaction.user.id, - guildId: interaction.guildId, - commandName: 'google' - }); - await handleInteractionError(interaction, error, { - commandName: 'google', - source: 'google_search' - }); - } -======= - const query = interaction.options.getString('query'); - const searchUrl = `https://www.google.com/search?q=${encodeURIComponent(query)}`; - - const embed = createEmbed({ - title: 'Google Search', - description: `[Search for "${query}"](${searchUrl})`, - color: 'info' - }) - .setFooter({ text: 'Google Search Results' }); - - await InteractionHelper.safeReply(interaction, { embeds: [embed] }); - - logger.info('Google search link generated', { - userId: interaction.user.id, - query: query, - guildId: interaction.guildId, - commandName: 'google' - }); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) - }, -}; \ No newline at end of file diff --git a/src/commands/Search/define.js b/src/commands/Search/modules/search_define.js similarity index 69% rename from src/commands/Search/define.js rename to src/commands/Search/modules/search_define.js index b887419539..149ec0f3aa 100644 --- a/src/commands/Search/define.js +++ b/src/commands/Search/modules/search_define.js @@ -1,33 +1,19 @@ -import { SlashCommandBuilder, MessageFlags } from 'discord.js'; import axios from 'axios'; -import { createEmbed, successEmbed, infoEmbed, warningEmbed } from '../../utils/embeds.js'; -import { logger } from '../../utils/logger.js'; -<<<<<<< HEAD -import { handleInteractionError } from '../../utils/errorHandler.js'; -======= -import { handleInteractionError, replyUserError, ErrorTypes } from '../../utils/errorHandler.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) -import { InteractionHelper } from '../../utils/interactionHelper.js'; -import { getColor } from '../../config/bot.js'; +import { createEmbed } from '../../../utils/embeds.js'; +import { logger } from '../../../utils/logger.js'; +import { handleInteractionError, replyUserError, ErrorTypes } from '../../../utils/errorHandler.js'; +import { InteractionHelper } from '../../../utils/interactionHelper.js'; export default { - data: new SlashCommandBuilder() - .setName('define') - .setDescription('Look up a word definition') - .addStringOption(option => - option.setName('word') - .setDescription('The word to look up') - .setRequired(true)), async execute(interaction) { try { - const deferred = await InteractionHelper.safeDefer(interaction); if (!deferred) { return; } const word = interaction.options.getString('word'); - + if (word.length < 2) { logger.warn('Define command - word too short', { userId: interaction.user.id, @@ -36,27 +22,23 @@ export default { }); return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Please enter a word with at least 2 characters.' }); } - + const response = await axios.get( `https://api.dictionaryapi.dev/api/v2/entries/en/${encodeURIComponent(word)}`, { timeout: 5000 } ); - + if (!response.data || response.data.length === 0) { -<<<<<<< HEAD - return await replyUserError(interaction, { type: ErrorTypes.USER_INPUT, message: 'No definitions found for "${word}".' }); -======= return await replyUserError(interaction, { type: ErrorTypes.USER_INPUT, message: `No definitions found for "${word}".` }); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } - + const data = response.data[0]; const embed = createEmbed({ title: data.word, description: data.phonetic ? `*${data.phonetic}*` : '', color: 'success' }); - + data.meanings.slice(0, 5).forEach(meaning => { const definitions = meaning.definitions .slice(0, 3) @@ -68,7 +50,7 @@ export default { return text; }) .join('\n\n'); - + if (definitions) { embed.addFields({ name: `**${meaning.partOfSpeech || 'Definition'}**`, @@ -77,18 +59,18 @@ export default { }); } }); - + embed.setFooter({ text: 'Powered by Free Dictionary API' }); - + await InteractionHelper.safeEditReply(interaction, { embeds: [embed] }); - + logger.info('Dictionary definition retrieved', { userId: interaction.user.id, word: word, guildId: interaction.guildId, commandName: 'define' }); - + } catch (error) { logger.error('Dictionary lookup error', { error: error.message, @@ -100,11 +82,7 @@ export default { }); if (error.response?.status === 404) { -<<<<<<< HEAD - await replyUserError(interaction, { type: ErrorTypes.USER_INPUT, message: 'No definitions found for "${interaction.options.getString(\'word\')}".' }); -======= await replyUserError(interaction, { type: ErrorTypes.USER_INPUT, message: `No definitions found for "${interaction.options.getString('word')}".` }); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } else { await handleInteractionError(interaction, error, { commandName: 'define', @@ -113,4 +91,4 @@ export default { } } }, -}; \ No newline at end of file +}; diff --git a/src/commands/Search/modules/search_google.js b/src/commands/Search/modules/search_google.js new file mode 100644 index 0000000000..c277046d49 --- /dev/null +++ b/src/commands/Search/modules/search_google.js @@ -0,0 +1,26 @@ +import { createEmbed } from '../../../utils/embeds.js'; +import { logger } from '../../../utils/logger.js'; +import { InteractionHelper } from '../../../utils/interactionHelper.js'; + +export default { + async execute(interaction) { + const query = interaction.options.getString('query'); + const searchUrl = `https://www.google.com/search?q=${encodeURIComponent(query)}`; + + const embed = createEmbed({ + title: 'Google Search', + description: `[Search for "${query}"](${searchUrl})`, + color: 'info' + }) + .setFooter({ text: 'Google Search Results' }); + + await InteractionHelper.safeReply(interaction, { embeds: [embed] }); + + logger.info('Google search link generated', { + userId: interaction.user.id, + query: query, + guildId: interaction.guildId, + commandName: 'google' + }); + }, +}; diff --git a/src/commands/Search/urban.js b/src/commands/Search/modules/search_urban.js similarity index 68% rename from src/commands/Search/urban.js rename to src/commands/Search/modules/search_urban.js index c62c0eaa46..b382fe30c0 100644 --- a/src/commands/Search/urban.js +++ b/src/commands/Search/modules/search_urban.js @@ -1,28 +1,14 @@ -import { SlashCommandBuilder, MessageFlags } from 'discord.js'; import axios from 'axios'; -import { createEmbed, successEmbed, infoEmbed, warningEmbed } from '../../utils/embeds.js'; -import { logger } from '../../utils/logger.js'; -<<<<<<< HEAD -import { handleInteractionError } from '../../utils/errorHandler.js'; -======= -import { handleInteractionError, replyUserError, ErrorTypes } from '../../utils/errorHandler.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) -import { InteractionHelper } from '../../utils/interactionHelper.js'; -import { getColor } from '../../config/bot.js'; +import { createEmbed } from '../../../utils/embeds.js'; +import { logger } from '../../../utils/logger.js'; +import { handleInteractionError, replyUserError, ErrorTypes } from '../../../utils/errorHandler.js'; +import { InteractionHelper } from '../../../utils/interactionHelper.js'; export default { - data: new SlashCommandBuilder() - .setName('urban') - .setDescription('Search Urban Dictionary for definitions') - .addStringOption(option => - option.setName('term') - .setDescription('The term to look up on Urban Dictionary') - .setRequired(true)), - async execute(interaction) { try { const term = interaction.options.getString('term'); - + if (term.length < 2) { logger.warn('Urban command - term too short', { userId: interaction.user.id, @@ -49,33 +35,29 @@ export default { }); }); }, 1500); - + const response = await axios.get( `https://api.urbandictionary.com/v0/define?term=${encodeURIComponent(term)}`, { timeout: 5000 } ); clearDeferTimer(); - + if (!response.data?.list?.length) { -<<<<<<< HEAD - return await replyUserError(interaction, { type: ErrorTypes.USER_INPUT, message: 'No definitions found for "${term}" on Urban Dictionary.' }); -======= return await replyUserError(interaction, { type: ErrorTypes.USER_INPUT, message: `No definitions found for "${term}" on Urban Dictionary.` }); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } - + const definition = response.data.list[0]; const cleanDefinition = definition.definition.replace(/\[|\]/g, ''); const cleanExample = definition.example.replace(/\[|\]/g, ''); - + const formattedDefinition = cleanDefinition -.replace(/\n\s*\n/g, '\n\n') + .replace(/\n\s*\n/g, '\n\n') .slice(0, 2000); - + const formattedExample = cleanExample ? `*"${cleanExample.replace(/\n/g, ' ').slice(0, 500)}..."*` : '*No example provided*'; - + const embed = createEmbed({ title: definition.word, description: formattedDefinition, @@ -83,36 +65,36 @@ export default { }) .setURL(definition.permalink) .addFields( - { - name: 'Example', + { + name: 'Example', value: formattedExample, - inline: false + inline: false }, - { - name: 'Stats', + { + name: 'Stats', value: `${definition.thumbs_up.toLocaleString()} • ${definition.thumbs_down.toLocaleString()}`, - inline: true + inline: true }, - { - name: 'Author', + { + name: 'Author', value: definition.author || 'Anonymous', - inline: true + inline: true } ) - .setFooter({ + .setFooter({ text: 'Urban Dictionary', - iconURL: 'https://i.imgur.com/8aQrX3a.png' + iconURL: 'https://i.imgur.com/8aQrX3a.png' }); - + await InteractionHelper.safeReply(interaction, { embeds: [embed] }); - + logger.info('Urban Dictionary definition retrieved', { userId: interaction.user.id, term: term, guildId: interaction.guildId, commandName: 'urban' }); - + } catch (error) { logger.error('Urban Dictionary error', { error: error.message, @@ -125,11 +107,7 @@ export default { }); if (error.response?.status === 404 || !error.response) { -<<<<<<< HEAD - await replyUserError(interaction, { type: ErrorTypes.USER_INPUT, message: 'No definitions found for "${interaction.options.getString(\'term\')}" on Urban Dictionary.' }); -======= await replyUserError(interaction, { type: ErrorTypes.USER_INPUT, message: `No definitions found for "${interaction.options.getString('term')}" on Urban Dictionary.` }); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } else if (error.response?.status === 429) { await replyUserError(interaction, { type: ErrorTypes.RATE_LIMIT, message: 'Too many requests to Urban Dictionary. Please try again in a few minutes.' }); } else { @@ -140,4 +118,4 @@ export default { } } }, -}; \ No newline at end of file +}; diff --git a/src/commands/Search/search.js b/src/commands/Search/search.js new file mode 100644 index 0000000000..52c1871902 --- /dev/null +++ b/src/commands/Search/search.js @@ -0,0 +1,54 @@ +import { SlashCommandBuilder } from 'discord.js'; +import { replyUserError, ErrorTypes } from '../../utils/errorHandler.js'; + +import searchDefine from './modules/search_define.js'; +import searchGoogle from './modules/search_google.js'; +import searchUrban from './modules/search_urban.js'; + +export default { + data: new SlashCommandBuilder() + .setName('search') + .setDescription('Search the web and dictionaries') + .addSubcommand(subcommand => + subcommand + .setName('define') + .setDescription('Look up a word definition') + .addStringOption(option => + option.setName('word') + .setDescription('The word to look up') + .setRequired(true)) + ) + .addSubcommand(subcommand => + subcommand + .setName('google') + .setDescription('Search Google') + .addStringOption(option => + option.setName('query') + .setDescription('What would you like to search for?') + .setRequired(true)) + ) + .addSubcommand(subcommand => + subcommand + .setName('urban') + .setDescription('Search Urban Dictionary for definitions') + .addStringOption(option => + option.setName('term') + .setDescription('The term to look up on Urban Dictionary') + .setRequired(true)) + ), + + async execute(interaction, config, client) { + const subcommand = interaction.options.getSubcommand(); + + switch (subcommand) { + case 'define': + return await searchDefine.execute(interaction, config, client); + case 'google': + return await searchGoogle.execute(interaction, config, client); + case 'urban': + return await searchUrban.execute(interaction, config, client); + default: + return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Unknown subcommand' }); + } + } +}; diff --git a/src/commands/ServerStats/modules/serverstats_create.js b/src/commands/ServerStats/modules/serverstats_create.js index 1ee7dfc6ca..e5e750a933 100644 --- a/src/commands/ServerStats/modules/serverstats_create.js +++ b/src/commands/ServerStats/modules/serverstats_create.js @@ -4,10 +4,7 @@ import { getServerCounters, saveServerCounters, updateCounter, getCounterBaseNam import { logger } from '../../../utils/logger.js'; import { InteractionHelper } from '../../../utils/interactionHelper.js'; -<<<<<<< HEAD -======= import { replyUserError, ErrorTypes } from '../../../utils/errorHandler.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) export async function handleCreate(interaction, client) { const guild = interaction.guild; const type = interaction.options.getString("type"); @@ -41,11 +38,7 @@ export async function handleCreate(interaction, client) { if (duplicateType) { const duplicateChannel = guild.channels.cache.get(duplicateType.channelId); -<<<<<<< HEAD - await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: '`A **${getCounterTypeLabel(type)}** counter already exists for this server${duplicateChannel ? ` in ${duplicateChannel}` : \'\'}. Delete it first before creating another.`' }).catch(logger.error); -======= await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: `A **${getCounterTypeLabel(type)}** counter already exists for this server${duplicateChannel ? ` in ${duplicateChannel}` : ''}. Delete it first before creating another.` }).catch(logger.error); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) return; } @@ -58,11 +51,7 @@ export async function handleCreate(interaction, client) { const existingCounter = counters.find(c => c.channelId === targetChannel.id); if (existingCounter) { -<<<<<<< HEAD - await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'A counter already exists for channel **${targetChannel.name}**. Please delete it first or choose a different type.' }).catch(logger.error); -======= await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: `A counter already exists for channel **${targetChannel.name}**. Please delete it first or choose a different type.` }).catch(logger.error); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) return; } @@ -91,11 +80,7 @@ export async function handleCreate(interaction, client) { } await InteractionHelper.safeEditReply(interaction, { -<<<<<<< HEAD - embeds: [successEmbed(`**Counter Created Successfully!**\n\n**Type:** ${getCounterTypeLabel(type)}\n**Channel Type:** ${targetChannel.type === ChannelType.GuildVoice ? 'voice' : 'text'}\n**Category:** ${category}\n**Channel:** ${targetChannel}\n**Channel Name:** ${targetChannel.name}\n**Counter ID:** \`${newCounter.id}\`\n\nThe counter will automatically update every 15 minutes.\n\nUse \`/counter list\` to view all counters.`)] -======= embeds: [successEmbed(`**Counter Created Successfully!**\n\n**Type:** ${getCounterTypeLabel(type)}\n**Channel Type:** ${targetChannel.type === ChannelType.GuildVoice ? 'voice' : 'text'}\n**Category:** ${category}\n**Channel:** ${targetChannel}\n**Channel Name:** ${targetChannel.name}\n**Counter ID:** \`${newCounter.id}\`\n\nThe counter will automatically update every 15 minutes.\n\nUse \`/serverstats list\` to view all counters.`)] ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) }).catch(logger.error); } catch (error) { diff --git a/src/commands/ServerStats/modules/serverstats_delete.js b/src/commands/ServerStats/modules/serverstats_delete.js index 44d12ae0c2..54e3e9ccfc 100644 --- a/src/commands/ServerStats/modules/serverstats_delete.js +++ b/src/commands/ServerStats/modules/serverstats_delete.js @@ -5,10 +5,7 @@ import { getServerCounters, saveServerCounters, getCounterEmoji, getCounterTypeL import { logger } from '../../../utils/logger.js'; import { InteractionHelper } from '../../../utils/interactionHelper.js'; -<<<<<<< HEAD -======= import { replyUserError, ErrorTypes, createError, wrapServiceBoundary } from '../../../utils/errorHandler.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) export async function handleDelete(interaction, client) { const guild = interaction.guild; const counterId = interaction.options.getString("counter-id"); @@ -35,11 +32,7 @@ export async function handleDelete(interaction, client) { const counterToDelete = counters.find(c => c.id === counterId); if (!counterToDelete) { -<<<<<<< HEAD - await replyUserError(interaction, { type: ErrorTypes.USER_INPUT, message: 'Counter with ID \\`${counterId}\\` not found. Use \\`/counter list\\` to see all counters.' }).catch(logger.error); -======= await replyUserError(interaction, { type: ErrorTypes.USER_INPUT, message: `Counter with ID \`${counterId}\` not found. Use \`/serverstats list\` to see all counters.` }).catch(logger.error); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) return; } @@ -70,65 +63,6 @@ export async function handleDelete(interaction, client) { } } -<<<<<<< HEAD -export async function performDeletionByCounterId(client, guild, counterId) { - try { - const counters = await getServerCounters(client, guild.id); - - const counter = counters.find(c => c.id === counterId); - if (!counter) { - return { - success: false, - message: `Counter with ID \`${counterId}\` was not found.` - }; - } - - const updatedCounters = counters.filter(c => c.id !== counter.id); - - const saved = await saveServerCounters(client, guild.id, updatedCounters); - if (!saved) { - return { - success: false, - message: "Failed to delete counter. Please try again." - }; - } - - const channel = guild.channels.cache.get(counter.channelId); - let channelDeleted = false; - - if (channel) { - try { - await channel.delete(`Counter deleted - removing channel: ${counter.id}`); - channelDeleted = true; - } catch (error) { - logger.error("Error deleting channel:", error); - } - } - - let message = `✅ **Counter Deleted Successfully!**\n\n**ID:** \`${counter.id}\`\n**Type:** ${getCounterTypeDisplay(counter.type)}`; - - if (channelDeleted) { - message += `\n**Channel:** ${channel.name} (deleted)`; - } else if (channel) { - message += `\n**Channel:** ${channel.name} (failed to delete)`; - } else { - message += `\n**Channel:** Already deleted`; - } - - return { - success: true, - message - }; - - } catch (error) { - logger.error("Error deleting counter:", error); - return { - success: false, - message: "An error occurred while deleting the counter. Please try again." - }; - } -} -======= export const performDeletionByCounterId = wrapServiceBoundary(async function performDeletionByCounterId(client, guild, counterId) { const counters = await getServerCounters(client, guild.id); @@ -182,7 +116,6 @@ export const performDeletionByCounterId = wrapServiceBoundary(async function per operation: 'performDeletionByCounterId', userMessage: 'An error occurred while deleting the counter. Please try again.', }); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) function getCounterTypeDisplay(type) { return `${getCounterEmoji(type)} ${getCounterTypeLabel(type)}`; diff --git a/src/commands/ServerStats/modules/serverstats_list.js b/src/commands/ServerStats/modules/serverstats_list.js index d055010a61..ad6d789227 100644 --- a/src/commands/ServerStats/modules/serverstats_list.js +++ b/src/commands/ServerStats/modules/serverstats_list.js @@ -5,10 +5,7 @@ import { getServerCounters, saveServerCounters, getCounterEmoji as getCounterTyp import { logger } from '../../../utils/logger.js'; import { InteractionHelper } from '../../../utils/interactionHelper.js'; -<<<<<<< HEAD -======= import { replyUserError, ErrorTypes } from '../../../utils/errorHandler.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) export async function handleList(interaction, client) { const guild = interaction.guild; @@ -49,11 +46,7 @@ export async function handleList(interaction, client) { if (validCounters.length === 0) { const embed = createEmbed({ title: "Server Counters", -<<<<<<< HEAD - description: "No counters have been set up for this server yet.\n\nUse `/counter create` to set up your first counter!", -======= description: "No counters have been set up for this server yet.\n\nUse `/serverstats create` to set up your first counter!", ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) color: getColor('warning') }); @@ -65,11 +58,7 @@ export async function handleList(interaction, client) { embed.addFields({ name: "**Usage Examples**", -<<<<<<< HEAD - value: "`/counter create type:members channel_type:voice category:Stats`\n`/counter create type:bots channel_type:text category:Server Info`\n`/counter list`", -======= value: "`/serverstats create type:members channel_type:voice category:Stats`\n`/serverstats create type:bots channel_type:text category:Server Info`\n`/serverstats list`", ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) inline: false }); @@ -118,11 +107,7 @@ export async function handleList(interaction, client) { embed.addFields({ name: "**Management Commands**", -<<<<<<< HEAD - value: "`/counter create` - Create new counter\n`/counter update` - Update existing counter\n`/counter delete` - Delete counter", -======= value: "`/serverstats create` - Create new counter\n`/serverstats update` - Update existing counter\n`/serverstats delete` - Delete counter", ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) inline: false }); diff --git a/src/commands/ServerStats/modules/serverstats_update.js b/src/commands/ServerStats/modules/serverstats_update.js index 5ddb1260fb..b2ebb45434 100644 --- a/src/commands/ServerStats/modules/serverstats_update.js +++ b/src/commands/ServerStats/modules/serverstats_update.js @@ -4,10 +4,7 @@ import { getServerCounters, saveServerCounters, updateCounter, getCounterEmoji, import { logger } from '../../../utils/logger.js'; import { InteractionHelper } from '../../../utils/interactionHelper.js'; -<<<<<<< HEAD -======= import { replyUserError, ErrorTypes } from '../../../utils/errorHandler.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) export async function handleUpdate(interaction, client) { const guild = interaction.guild; const counterId = interaction.options.getString("counter-id"); @@ -35,11 +32,7 @@ export async function handleUpdate(interaction, client) { const counterIndex = counters.findIndex(c => c.id === counterId); if (counterIndex === -1) { -<<<<<<< HEAD - await replyUserError(interaction, { type: ErrorTypes.USER_INPUT, message: 'Counter with ID \\`${counterId}\\` not found. Use \\`/counter list\\` to see all counters.' }).catch(logger.error); -======= await replyUserError(interaction, { type: ErrorTypes.USER_INPUT, message: `Counter with ID \`${counterId}\` not found. Use \`/serverstats list\` to see all counters.` }).catch(logger.error); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) return; } @@ -55,11 +48,7 @@ export async function handleUpdate(interaction, client) { const existingTypeCounter = counters.find(c => c.type === newType && c.id !== counter.id); if (existingTypeCounter) { const existingChannel = guild.channels.cache.get(existingTypeCounter.channelId); -<<<<<<< HEAD - await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: '`A **${getCounterTypeLabel(newType)}** counter already exists for this server${existingChannel ? ` in ${existingChannel}` : \'\'}. Delete it first before reusing that type.`' }).catch(logger.error); -======= await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: `A **${getCounterTypeLabel(newType)}** counter already exists for this server${existingChannel ? ` in ${existingChannel}` : ''}. Delete it first before reusing that type.` }).catch(logger.error); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) return; } } diff --git a/src/commands/ServerStats/serverstats.js b/src/commands/ServerStats/serverstats.js index 845ec3f2ec..9fe3dda3b8 100644 --- a/src/commands/ServerStats/serverstats.js +++ b/src/commands/ServerStats/serverstats.js @@ -9,10 +9,7 @@ import { handleUpdate } from './modules/serverstats_update.js'; import { handleDelete } from './modules/serverstats_delete.js'; import { InteractionHelper } from '../../utils/interactionHelper.js'; -<<<<<<< HEAD -======= import { replyUserError, ErrorTypes } from '../../utils/errorHandler.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) export default { data: new SlashCommandBuilder() .setName("serverstats") @@ -93,39 +90,6 @@ export default { async execute(interaction, guildConfig, client) { const subcommand = interaction.options.getSubcommand(); -<<<<<<< HEAD - try { - switch (subcommand) { - case "create": - await handleCreate(interaction, client); - break; - case "list": - await handleList(interaction, client); - break; - case "update": - await handleUpdate(interaction, client); - break; - case "delete": - await handleDelete(interaction, client); - break; - default: - await replyUserError(interaction, { type: ErrorTypes.VALIDATION, message: 'Unknown subcommand.' }); - } - } catch (error) { - logger.error(`Error in serverstats ${subcommand}:`, error); - - const errorEmbedMsg = createEmbed({ - title: "❌ Error", - description: "An error occurred while processing your request.", - color: getColor('error') - }); - - if (!interaction.replied && !interaction.deferred) { - await InteractionHelper.safeReply(interaction, { embeds: [errorEmbedMsg], flags: MessageFlags.Ephemeral }).catch(logger.error); - } else { - await interaction.followUp({ embeds: [errorEmbedMsg], flags: MessageFlags.Ephemeral }).catch(logger.error); - } -======= switch (subcommand) { case "create": await handleCreate(interaction, client); @@ -141,7 +105,6 @@ export default { break; default: await replyUserError(interaction, { type: ErrorTypes.VALIDATION, message: 'Unknown subcommand.' }); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } } }; \ No newline at end of file diff --git a/src/commands/Ticket/claim.js b/src/commands/Ticket/claim.js index a9133f69c1..d1d1c1f938 100644 --- a/src/commands/Ticket/claim.js +++ b/src/commands/Ticket/claim.js @@ -2,15 +2,9 @@ import { getColor } from '../../config/bot.js'; import { SlashCommandBuilder, PermissionFlagsBits, MessageFlags } from 'discord.js'; import { successEmbed } from '../../utils/embeds.js'; import { logger } from '../../utils/logger.js'; -<<<<<<< HEAD -import { handleInteractionError } from '../../utils/errorHandler.js'; -import { InteractionHelper } from '../../utils/interactionHelper.js'; -import { getTicketPermissionContext } from '../../utils/ticketPermissions.js'; -======= import { replyUserError, ErrorTypes } from '../../utils/errorHandler.js'; import { InteractionHelper } from '../../utils/interactionHelper.js'; import { getTicketPermissionContext } from '../../utils/ticket/ticketPermissions.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) import { claimTicket } from '../../services/ticket.js'; export default { data: new SlashCommandBuilder() @@ -19,69 +13,6 @@ export default { .setDMPermission(false), async execute(interaction, guildConfig, client) { -<<<<<<< HEAD - try { - - const deferred = await InteractionHelper.safeDefer(interaction, { flags: MessageFlags.Ephemeral }); - if (!deferred) { - return; - } - - const permissionContext = await getTicketPermissionContext({ client, interaction }); - if (!permissionContext.ticketData) { - return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'This command can only be used in a valid ticket channel.' }); - } - - if (!permissionContext.canManageTicket) { - return await replyUserError(interaction, { type: ErrorTypes.PERMISSION, message: 'You need the `Manage Channels` permission or the configured `Ticket Staff Role` to claim tickets.' }); - } - - const channel = interaction.channel; - const result = await claimTicket(channel, interaction.user); - - if (!result.success) { - logger.warn('Ticket claim failed - not a valid ticket channel', { - userId: interaction.user.id, - channelId: channel.id, - guildId: interaction.guildId, - error: result.error - }); - return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'result.error || "This command can only be used in a valid ticket channel."' }); - } - - await InteractionHelper.safeEditReply(interaction, { - embeds: [ - successEmbed( - "Ticket Claimed!", - "You have successfully claimed this ticket.", - ), - ], - }); - - logger.info('Ticket claimed successfully', { - userId: interaction.user.id, - userTag: interaction.user.tag, - channelId: channel.id, - channelName: channel.name, - guildId: interaction.guildId, - commandName: 'claim' - }); - - } catch (error) { - logger.error('Error executing claim command', { - error: error.message, - stack: error.stack, - userId: interaction.user.id, - channelId: interaction.channel?.id, - guildId: interaction.guildId, - commandName: 'claim' - }); - await handleInteractionError(interaction, error, { - commandName: 'claim', - source: 'ticket_claim_command' - }); - } -======= const deferred = await InteractionHelper.safeDefer(interaction, { flags: MessageFlags.Ephemeral }); if (!deferred) { return; @@ -115,6 +46,5 @@ export default { guildId: interaction.guildId, commandName: 'claim' }); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) }, }; \ No newline at end of file diff --git a/src/commands/Ticket/close.js b/src/commands/Ticket/close.js index 6ec24ef1db..056a699b9b 100644 --- a/src/commands/Ticket/close.js +++ b/src/commands/Ticket/close.js @@ -2,15 +2,9 @@ import { getColor } from '../../config/bot.js'; import { SlashCommandBuilder, PermissionFlagsBits, ChannelType, MessageFlags } from 'discord.js'; import { successEmbed } from '../../utils/embeds.js'; import { logger } from '../../utils/logger.js'; -<<<<<<< HEAD -import { handleInteractionError, replyUserError, ErrorTypes } from '../../utils/errorHandler.js'; -import { InteractionHelper } from '../../utils/interactionHelper.js'; -import { getTicketPermissionContext } from '../../utils/ticketPermissions.js'; -======= import { replyUserError, ErrorTypes } from '../../utils/errorHandler.js'; import { InteractionHelper } from '../../utils/interactionHelper.js'; import { getTicketPermissionContext } from '../../utils/ticket/ticketPermissions.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) import { closeTicket } from '../../services/ticket.js'; export default { data: new SlashCommandBuilder() @@ -25,76 +19,6 @@ export default { ), async execute(interaction, guildConfig, client) { -<<<<<<< HEAD - try { - - const deferred = await InteractionHelper.safeDefer(interaction, { flags: MessageFlags.Ephemeral }); - if (!deferred) { - return; - } - - const permissionContext = await getTicketPermissionContext({ client, interaction }); - if (!permissionContext.ticketData) { - return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'This command can only be used in a valid ticket channel.' }); - } - - if (!permissionContext.canCloseTicket) { - return await replyUserError(interaction, { type: ErrorTypes.PERMISSION, message: 'You need the `Manage Channels` permission, the configured `Ticket Staff Role`, or be the ticket creator to close this ticket.' }); - } - - const channel = interaction.channel; - const reason = - interaction.options?.getString("reason") || - "Closed via command without a specific reason."; - - const result = await closeTicket(channel, interaction.user, reason); - - if (!result.success) { - logger.warn('Ticket close failed - not a valid ticket channel', { - userId: interaction.user.id, - channelId: channel.id, - guildId: interaction.guildId, - error: result.error - }); - return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: result.error || 'This command can only be used in a valid ticket channel.' }); - } - - await InteractionHelper.safeEditReply(interaction, { - embeds: [ - successEmbed( - "Ticket Closed!", - "This ticket has been closed successfully.", - ), - ], - }); - - logger.info('Ticket closed successfully', { - userId: interaction.user.id, - userTag: interaction.user.tag, - channelId: channel.id, - channelName: channel.name, - guildId: interaction.guildId, - reason: reason, - commandName: 'close' - }); - - } catch (error) { - logger.error('Error executing close command', { - error: error.message, - stack: error.stack, - userId: interaction.user.id, - channelId: interaction.channel?.id, - guildId: interaction.guildId, - commandName: 'close' - }); - await handleInteractionError(interaction, error, { - commandName: 'close', - source: 'ticket_close_command' - }); - } - }, -}; -======= const deferred = await InteractionHelper.safeDefer(interaction, { flags: MessageFlags.Ephemeral }); if (!deferred) { return; @@ -135,4 +59,3 @@ export default { }); }, }; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) diff --git a/src/commands/Ticket/modules/ticket_dashboard.js b/src/commands/Ticket/modules/ticket_dashboard.js index 9bc38de775..17f3f43f88 100644 --- a/src/commands/Ticket/modules/ticket_dashboard.js +++ b/src/commands/Ticket/modules/ticket_dashboard.js @@ -20,12 +20,7 @@ import { InteractionHelper } from '../../../utils/interactionHelper.js'; import { successEmbed, infoEmbed } from '../../../utils/embeds.js'; import { logger } from '../../../utils/logger.js'; import { TitanBotError, ErrorTypes, replyUserError } from '../../../utils/errorHandler.js'; -<<<<<<< HEAD -import { getGuildConfig } from '../../../services/guildConfig.js'; -import { getGuildConfigKey } from '../../../utils/database.js'; -======= import { getGuildConfig, setGuildConfig } from '../../../services/config/guildConfig.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) import { getGuildTicketStats } from '../../../utils/database/tickets.js'; import { getUserTicketCount } from '../../../services/ticket.js'; import { @@ -80,11 +75,7 @@ async function persistPanelMessageId(client, guildId, guildConfig, messageId) { if (!messageId || guildConfig.ticketPanelMessageId === messageId) return; guildConfig.ticketPanelMessageId = messageId; if (client.db) { -<<<<<<< HEAD - await client.db.set(getGuildConfigKey(guildId), guildConfig); -======= await setGuildConfig(client, guildId, guildConfig); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } } @@ -387,11 +378,7 @@ async function handlePanelMessage(selectInteraction, rootInteraction, guildConfi const newMessage = submitted.fields.getTextInputValue('panel_msg_input').trim(); guildConfig.ticketPanelMessage = newMessage; -<<<<<<< HEAD - await client.db.set(getGuildConfigKey(guildId), guildConfig); -======= await setGuildConfig(client, guildId, guildConfig); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) const panelUpdated = await updateLivePanel(client, rootInteraction.guild, guildConfig, guildId); @@ -444,11 +431,7 @@ async function handleButtonLabel(selectInteraction, rootInteraction, guildConfig const newLabel = submitted.fields.getTextInputValue('btn_label_input').trim(); guildConfig.ticketButtonLabel = newLabel; -<<<<<<< HEAD - await client.db.set(getGuildConfigKey(guildId), guildConfig); -======= await setGuildConfig(client, guildId, guildConfig); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) const panelUpdated = await updateLivePanel(client, rootInteraction.guild, guildConfig, guildId); @@ -505,11 +488,7 @@ async function handleStaffRole(selectInteraction, rootInteraction, guildConfig, const role = roleInteraction.roles.first(); guildConfig.ticketStaffRoleId = role.id; -<<<<<<< HEAD - await client.db.set(getGuildConfigKey(guildId), guildConfig); -======= await setGuildConfig(client, guildId, guildConfig); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) await roleInteraction.followUp({ embeds: [successEmbed('Staff Role Updated', `Staff role set to ${role}.`)], @@ -564,11 +543,7 @@ async function handleOpenCategory(selectInteraction, rootInteraction, guildConfi const category = catInteraction.channels.first(); guildConfig.ticketCategoryId = category.id; -<<<<<<< HEAD - await client.db.set(getGuildConfigKey(guildId), guildConfig); -======= await setGuildConfig(client, guildId, guildConfig); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) await catInteraction.followUp({ embeds: [ @@ -628,11 +603,7 @@ async function handleClosedCategory(selectInteraction, rootInteraction, guildCon const category = catInteraction.channels.first(); guildConfig.ticketClosedCategoryId = category.id; -<<<<<<< HEAD - await client.db.set(getGuildConfigKey(guildId), guildConfig); -======= await setGuildConfig(client, guildId, guildConfig); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) await catInteraction.followUp({ embeds: [ @@ -699,11 +670,7 @@ async function handleMaxTickets(selectInteraction, rootInteraction, guildConfig, } guildConfig.maxTicketsPerUser = newMax; -<<<<<<< HEAD - await client.db.set(getGuildConfigKey(guildId), guildConfig); -======= await setGuildConfig(client, guildId, guildConfig); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) await submitted.reply({ embeds: [ @@ -723,11 +690,7 @@ async function handleDmOnClose(btnInteraction, rootInteraction, guildConfig, gui const newState = guildConfig.dmOnClose === false; guildConfig.dmOnClose = newState; -<<<<<<< HEAD - await client.db.set(getGuildConfigKey(guildId), guildConfig); -======= await setGuildConfig(client, guildId, guildConfig); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) await btnInteraction.followUp({ embeds: [ @@ -774,11 +737,7 @@ async function handleLogsChannel(selectInteraction, rootInteraction, guildConfig const channel = channelInteraction.channels.first(); guildConfig.ticketLogsChannelId = channel.id; -<<<<<<< HEAD - await client.db.set(getGuildConfigKey(guildId), guildConfig); -======= await setGuildConfig(client, guildId, guildConfig); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) await channelInteraction.followUp({ embeds: [successEmbed('Logs Channel Updated', `Ticket logs will be sent to ${channel}`)], @@ -830,11 +789,7 @@ async function handleTranscriptChannel(selectInteraction, rootInteraction, guild const channel = channelInteraction.channels.first(); guildConfig.ticketTranscriptChannelId = channel.id; -<<<<<<< HEAD - await client.db.set(getGuildConfigKey(guildId), guildConfig); -======= await setGuildConfig(client, guildId, guildConfig); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) await channelInteraction.followUp({ embeds: [successEmbed('Transcript Channel Updated', `Transcripts will be sent to ${channel}`)], @@ -1026,11 +981,7 @@ async function handleDeleteSystem(btnInteraction, rootInteraction, guildConfig, } try { -<<<<<<< HEAD - const { pgConfig } = await import('../../../config/postgres.js'); -======= const { pgConfig } = await import('../../../config/database/postgres.js'); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) if (client.db?.db?.pool && typeof client.db.db.isAvailable === 'function' && client.db.db.isAvailable()) { await client.db.db.pool.query( `DELETE FROM ${pgConfig.tables.tickets} WHERE guild_id = $1`, @@ -1044,11 +995,7 @@ async function handleDeleteSystem(btnInteraction, rootInteraction, guildConfig, for (const key of keysToDelete) { delete guildConfig[key]; } -<<<<<<< HEAD - await client.db.set(getGuildConfigKey(guildId), guildConfig); -======= await setGuildConfig(client, guildId, guildConfig); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) await submitted.followUp({ embeds: [ diff --git a/src/commands/Ticket/priority.js b/src/commands/Ticket/priority.js index 29c8932b85..fde4195156 100644 --- a/src/commands/Ticket/priority.js +++ b/src/commands/Ticket/priority.js @@ -2,15 +2,9 @@ import { getColor } from '../../config/bot.js'; import { SlashCommandBuilder, PermissionFlagsBits, MessageFlags } from 'discord.js'; import { successEmbed } from '../../utils/embeds.js'; import { logger } from '../../utils/logger.js'; -<<<<<<< HEAD -import { handleInteractionError } from '../../utils/errorHandler.js'; -import { InteractionHelper } from '../../utils/interactionHelper.js'; -import { getTicketPermissionContext } from '../../utils/ticketPermissions.js'; -======= import { replyUserError, ErrorTypes } from '../../utils/errorHandler.js'; import { InteractionHelper } from '../../utils/interactionHelper.js'; import { getTicketPermissionContext } from '../../utils/ticket/ticketPermissions.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) import { updateTicketPriority } from '../../services/ticket.js'; export default { @@ -34,72 +28,6 @@ export default { category: "Ticket", async execute(interaction, guildConfig, client) { -<<<<<<< HEAD - try { - - const deferred = await InteractionHelper.safeDefer(interaction, { flags: MessageFlags.Ephemeral }); - if (!deferred) { - return; - } - - const permissionContext = await getTicketPermissionContext({ client, interaction }); - if (!permissionContext.ticketData) { - return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'This command can only be used in a valid ticket channel.' }); - } - - if (!permissionContext.canManageTicket) { - return await replyUserError(interaction, { type: ErrorTypes.PERMISSION, message: 'You need the `Manage Channels` permission or the configured `Ticket Staff Role` to change ticket priority.' }); - } - - const priorityLevel = interaction.options.getString("level"); - const result = await updateTicketPriority(interaction.channel, priorityLevel, interaction.user); - - if (!result.success) { - logger.warn('Priority update failed - not a valid ticket channel', { - userId: interaction.user.id, - channelId: interaction.channel.id, - guildId: interaction.guildId, - error: result.error - }); - return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'result.error || "This command can only be used in a valid ticket channel."' }); - } - - await InteractionHelper.safeEditReply(interaction, { - embeds: [ - successEmbed( - "Priority Updated", - `Ticket priority set to **${priorityLevel.toUpperCase()}**.`, - ), - ], - }); - - logger.info('Ticket priority updated successfully', { - userId: interaction.user.id, - userTag: interaction.user.tag, - channelId: interaction.channel.id, - channelName: interaction.channel.name, - guildId: interaction.guildId, - priority: priorityLevel, - commandName: 'priority' - }); - - } catch (error) { - logger.error('Error executing priority command', { - error: error.message, - stack: error.stack, - userId: interaction.user.id, - channelId: interaction.channel?.id, - guildId: interaction.guildId, - commandName: 'priority' - }); - await handleInteractionError(interaction, error, { - commandName: 'priority', - source: 'ticket_priority_command' - }); - } - }, -}; -======= const deferred = await InteractionHelper.safeDefer(interaction, { flags: MessageFlags.Ephemeral }); if (!deferred) { return; @@ -137,4 +65,3 @@ export default { }); }, }; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) diff --git a/src/commands/Ticket/ticket.js b/src/commands/Ticket/ticket.js index c91d5e2960..982747b118 100644 --- a/src/commands/Ticket/ticket.js +++ b/src/commands/Ticket/ticket.js @@ -1,11 +1,7 @@ import { getColor } from '../../config/bot.js'; import { SlashCommandBuilder, PermissionFlagsBits, PermissionsBitField, ChannelType, ActionRowBuilder, ButtonBuilder, ButtonStyle, MessageFlags } from 'discord.js'; import { createEmbed, successEmbed, infoEmbed, warningEmbed } from '../../utils/embeds.js'; -<<<<<<< HEAD -import { getGuildConfig } from '../../services/guildConfig.js'; -======= import { getGuildConfig, setGuildConfig } from '../../services/config/guildConfig.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) import { InteractionHelper } from '../../utils/interactionHelper.js'; import { logger } from '../../utils/logger.js'; import { handleInteractionError, replyUserError, ErrorTypes } from '../../utils/errorHandler.js'; @@ -98,29 +94,6 @@ export default { category: "ticket", async execute(interaction, config, client) { -<<<<<<< HEAD - try { - - const deferred = await InteractionHelper.safeDefer(interaction, { flags: MessageFlags.Ephemeral }); - if (!deferred) { - return; - } - - if ( - !interaction.member.permissions.has( - PermissionFlagsBits.ManageChannels, - ) - ) { - logger.warn('Ticket command permission denied', { - userId: interaction.user.id, - guildId: interaction.guildId, - commandName: 'ticket' - }); - return await replyUserError(interaction, { type: ErrorTypes.PERMISSION, message: 'You need the `Manage Channels` permission for this action.' }); - } - - const subcommand = interaction.options.getSubcommand(); -======= const deferred = await InteractionHelper.safeDefer(interaction, { flags: MessageFlags.Ephemeral }); if (!deferred) { return; @@ -140,7 +113,6 @@ export default { } const subcommand = interaction.options.getSubcommand(); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) if (subcommand === "dashboard") { return ticketConfig.execute(interaction, config, client); @@ -196,20 +168,6 @@ description: panelMessage, currentConfig.maxTicketsPerUser = maxTicketsPerUser; currentConfig.dmOnClose = dmOnClose; -<<<<<<< HEAD - const { getGuildConfigKey } = await import('../../utils/database.js'); - const configKey = getGuildConfigKey(interaction.guildId); - await client.db.set(configKey, currentConfig); - logger.info('Ticket configuration saved', { - guildId: interaction.guildId, - categoryId: categoryChannel?.id, - closedCategoryId: closedCategoryChannel?.id, - staffRoleId: staffRole?.id, - maxTickets: maxTicketsPerUser, - dmOnClose: dmOnClose - }); - } -======= await setGuildConfig(client, interaction.guildId, currentConfig); logger.info('Ticket configuration saved', { guildId: interaction.guildId, @@ -224,7 +182,6 @@ description: panelMessage, guildId: interaction.guildId, }); } ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) let successMessage = `The ticket creation panel has been sent to ${panelChannel}.`; @@ -338,21 +295,5 @@ description: panelMessage, } } } -<<<<<<< HEAD - } catch (error) { - logger.error('Error executing ticket command', { - error: error.message, - stack: error.stack, - userId: interaction.user.id, - guildId: interaction.guildId, - commandName: 'ticket' - }); - await handleInteractionError(interaction, error, { - commandName: 'ticket', - source: 'ticket_command_main' - }); - } -======= ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } }; \ No newline at end of file diff --git a/src/commands/Tools/baseconvert.js b/src/commands/Tools/baseconvert.js index 108775f080..1ef560dc7e 100644 --- a/src/commands/Tools/baseconvert.js +++ b/src/commands/Tools/baseconvert.js @@ -1,11 +1,7 @@ import { SlashCommandBuilder } from 'discord.js'; import { createEmbed, successEmbed, infoEmbed, warningEmbed } from '../../utils/embeds.js'; import { logger } from '../../utils/logger.js'; -<<<<<<< HEAD -import { handleInteractionError, replyUserError, ErrorTypes } from '../../utils/errorHandler.js'; -======= import { replyUserError, ErrorTypes } from '../../utils/errorHandler.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) import { InteractionHelper } from '../../utils/interactionHelper.js'; import { getColor } from '../../config/bot.js'; @@ -128,118 +124,6 @@ export default { return; } -<<<<<<< HEAD - try { - const numberStr = interaction.options.getString('number').trim(); - const fromBase = interaction.options.getString('from'); - const toBase = interaction.options.getString('to'); - - const { prefix: fromPrefix, name: fromName } = BASE_ALPHABETS[fromBase]; - - const cleanNumber = fromPrefix && numberStr.startsWith(fromPrefix) - ? numberStr.slice(fromPrefix.length) - : numberStr; - - if (!cleanNumber) { - return replyUserError(interaction, { - type: ErrorTypes.VALIDATION, - message: 'You must provide a number to convert.\n\n**Example:** `/baseconvert number:1010 from:BIN to:DEC`', - }); - } - - const alphabet = BASE_ALPHABETS[fromBase].alphabet; - const regex = new RegExp(`^[${alphabet}]+$`, 'i'); - - if (!regex.test(cleanNumber)) { - let examples = ''; - if (fromBase === 'BIN') { - examples = '\n\n**Valid:** 101, 1010, 11111 | **Invalid:** 5 (digit 5 not allowed)'; - } else if (fromBase === 'OCT') { - examples = '\n\n**Valid:** 77, 123, 755 | **Invalid:** 8 (only 0-7 allowed)'; - } else if (fromBase === 'DEC') { - examples = '\n\n**Valid:** 42, 123, 999 | **Invalid:** 12.34 (no decimals)'; - } else if (fromBase === 'HEX') { - examples = '\n\n**Valid:** FF, A1B2, DEADBEEF | **Invalid:** G (only 0-9, A-F)'; - } - logger.warn(`Invalid base conversion input: ${cleanNumber} for base ${fromBase}`); - return replyUserError(interaction, { - type: ErrorTypes.VALIDATION, - message: `You provided: \`${cleanNumber}\`\n\nValid characters: \`${alphabet}\`${examples}`, - }); - } - - let decimalValue; - try { - if (fromBase === 'B64') { - decimalValue = parseBigIntFromBase(cleanNumber, fromBase); - } else { - decimalValue = parseBigIntFromBase(cleanNumber, fromBase); - } - } catch (error) { - logger.error('Base conversion parse error:', error); - return replyUserError(interaction, { - type: ErrorTypes.VALIDATION, - message: 'The number is too large to process.\n\nTry with a smaller number.', - }); - } - - if (toBase) { - const { prefix: toPrefix, name: toName } = BASE_ALPHABETS[toBase]; - let result; - - try { - result = formatBigIntToBase(decimalValue, toBase); - - const embed = successEmbed( - '🔄 Base Conversion Result', - `**From ${fromName} (${fromBase}):** \`${fromPrefix}${cleanNumber}\`\n` + - `**To ${toName} (${toBase}):** \`${toPrefix}${result}\`\n` + - `**Decimal:** \`${decimalValue.toLocaleString()}\`` - ); - embed.setColor(getColor('success')); - - await InteractionHelper.safeEditReply(interaction, { embeds: [embed] }); - - } catch (error) { - logger.error(`Base conversion error to ${toName}:`, error); - await replyUserError(interaction, { - type: ErrorTypes.VALIDATION, - message: 'The result would be too large or incompatible.\n\nTry with a smaller number or different target base.', - }); - } - - } else { - let description = `**Input (${fromName}):** \`${fromPrefix}${cleanNumber}\`\n`; - description += `**Decimal:** \`${decimalValue.toLocaleString()}\`\n\n`; - - for (const [baseKey, { prefix, name }] of Object.entries(BASE_ALPHABETS)) { - if (baseKey === fromBase) continue; - - try { - let value = formatBigIntToBase(decimalValue, baseKey); - - description += `**${name} (${baseKey}):** \`${prefix}${value}\`\n`; - } catch (error) { - description += `**${name} (${baseKey}):** *Too large to convert*\n`; - } - } - - const embed = successEmbed( - '🔄 Base Conversion Results', - description - ); - embed.setColor(getColor('primary')); - - await InteractionHelper.safeEditReply(interaction, { embeds: [embed] }); - } - - } catch (error) { - await handleInteractionError(interaction, error, { - type: 'command', - commandName: 'baseconvert' - }); - } -======= const numberStr = interaction.options.getString('number').trim(); const fromBase = interaction.options.getString('from'); const toBase = interaction.options.getString('to'); @@ -342,6 +226,5 @@ export default { await InteractionHelper.safeEditReply(interaction, { embeds: [embed] }); } ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) }, }; \ No newline at end of file diff --git a/src/commands/Tools/calculate.js b/src/commands/Tools/calculate.js index 78cfc35394..233b88ab8f 100644 --- a/src/commands/Tools/calculate.js +++ b/src/commands/Tools/calculate.js @@ -1,11 +1,7 @@ import { SlashCommandBuilder, ButtonBuilder, ButtonStyle, ActionRowBuilder } from 'discord.js'; import { createEmbed, successEmbed, infoEmbed, warningEmbed } from '../../utils/embeds.js'; import { logger } from '../../utils/logger.js'; -<<<<<<< HEAD -import { handleInteractionError, replyUserError, ErrorTypes } from '../../utils/errorHandler.js'; -======= import { replyUserError, ErrorTypes } from '../../utils/errorHandler.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) import { InteractionHelper } from '../../utils/interactionHelper.js'; import { evaluateMathExpression } from '../../utils/safeMathParser.js'; @@ -44,154 +40,6 @@ export default { return; } -<<<<<<< HEAD -try { - - const expression = interaction.options.getString("expression"); - - if ( - !/^[0-9+\-*/.()^%! ,<>=&|~?:\[\]{}a-z√π∞°]+$/i.test(expression) - ) { - return await replyUserError(interaction, { type: ErrorTypes.VALIDATION, message: '"**Contains unsupported characters.**\\n\\n" +\n "✅ Supported: Numbers, decimals, + - * / ^ %, sin cos tan sqrt abs log exp, pi e, ()\\n" +\n "❌ Not supported: Brackets, curly braces, and other symbols"' }); - } - - const dangerousPatterns = [ - /\b(?:import|require|process|fs|child_process|exec|eval|Function|setTimeout|setInterval|new\s+Function)\s*\(/i, - /`/g, -/\$\{.*\}/, - /\b(?:localStorage|document|window|fetch|XMLHttpRequest)\b/, - /\b(?:while|for)\s*\([^)]*\)\s*\{/, - /\b(?:function\*|yield|await|async)\b/, - ]; - - for (const pattern of dangerousPatterns) { - if (pattern.test(expression)) { - return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: '"**Contains blocked code patterns.**\\n\\n" +\n "🚫 **Blocked:** import, require, eval, Function, setTimeout, setInterval, process, fs, document, window, fetch, loops, async/await\\n\\n" +\n "Code-like syntax is not allowed in calculations."' }); - } - } - - let result; - try { - result = evaluate(expression); - - let formattedResult; - if (typeof result === "number") { - formattedResult = result.toLocaleString("en-US", { - maximumFractionDigits: 10, - }); - - if ( - Math.abs(result) > 0 && - (Math.abs(result) >= 1e10 || Math.abs(result) < 1e-3) - ) { - formattedResult = result.toExponential(6); - } - } else if (typeof result === "boolean") { - formattedResult = result ? "true" : "false"; - } else if (result === null || result === undefined) { - formattedResult = "No result"; - } else if ( - Array.isArray(result) || - typeof result === "object" - ) { - formattedResult = - "```json\n" + JSON.stringify(result, null, 2) + "\n```"; - } else { - formattedResult = String(result); - } - - const userId = interaction.user.id; - if (!calculationHistory.has(userId)) { - calculationHistory.set(userId, []); - } - - const history = calculationHistory.get(userId); - history.unshift({ - expression, - result: formattedResult, - timestamp: Date.now(), - }); - - if (history.length > MAX_HISTORY) { - history.pop(); - } - - const row = new ActionRowBuilder().addComponents( - new ButtonBuilder() - .setCustomId(`calc_${interaction.id}_add`) - .setLabel("+") - .setStyle(ButtonStyle.Primary), - new ButtonBuilder() - .setCustomId(`calc_${interaction.id}_subtract`) - .setLabel("-") - .setStyle(ButtonStyle.Primary), - new ButtonBuilder() - .setCustomId(`calc_${interaction.id}_multiply`) - .setLabel("×") - .setStyle(ButtonStyle.Primary), - new ButtonBuilder() - .setCustomId(`calc_${interaction.id}_divide`) - .setLabel("÷") - .setStyle(ButtonStyle.Primary), - new ButtonBuilder() - .setCustomId(`calc_${interaction.id}_history`) - .setLabel("History") - .setStyle(ButtonStyle.Secondary), - ); - - const embed = successEmbed( - "🧮 Calculation Result", - `**Expression:** \`${expression.replace(/`/g, "\`")}\`\n` + - `**Result:** \`${formattedResult}\`\n\n` + - `*Use the buttons below to perform operations with the result.*`, - ); - - await InteractionHelper.safeEditReply(interaction, { - embeds: [embed], - components: [row], - }); - - const filter = (i) => - i.customId.startsWith(`calc_${interaction.id}`) && - i.user.id === interaction.user.id; -const BUTTON_TIMEOUT = 300000; - const collector = - interaction.channel.createMessageComponentCollector({ - filter, - time: BUTTON_TIMEOUT, - }); - - collector.on("collect", async (i) => { - try { - const operation = i.customId.split("_")[2]; - - if (operation === "history") { - if (!i.deferred && !i.replied) { - await i.deferUpdate().catch(console.error); - } - - const userHistory = - calculationHistory.get(userId) || []; - - if (userHistory.length === 0) { - await i.followUp({ - content: "No calculation history found.", - flags: ["Ephemeral"], - }); - return; - } - - const historyText = userHistory - .map( - (item, index) => - `${index + 1}. **${item.expression}** = \`${item.result}\`\n` + - ``, - ) - .join("\n\n"); - - await i.followUp({ - content: `📜 **Your Calculation History**\n\n${historyText}`, -======= const expression = interaction.options.getString("expression"); if ( @@ -331,150 +179,11 @@ const BUTTON_TIMEOUT = 300000; if (userHistory.length === 0) { await i.followUp({ content: "No calculation history found.", ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) flags: ["Ephemeral"], }); return; } -<<<<<<< HEAD - let operator = ""; - - switch (operation) { - case "add": - operator = "+"; - break; - case "subtract": - operator = "-"; - break; - case "multiply": - operator = "*"; - break; - case "divide": - operator = "/"; - break; - } - - try { - const contextKey = `${i.user.id}_${operation}`; - calculationContexts.set(contextKey, { - expression, - formattedResult, - operator, - messageId: interaction.message?.id, - channelId: interaction.channelId, - userId: i.user.id - }); - - await i.showModal({ - customId: `calc_modal:${operation}`, - title: `Enter a number to ${operation}`, - components: [ - { - type: 1, - components: [ - { - type: 4, - customId: `operand:${contextKey}`, - label: `Number to ${operator} with ${formattedResult}`, - placeholder: "Enter a number...", - style: 1, - required: true, - maxLength: 50, - }, - ], - }, - ], - }); - } catch (modalError) { - logger.error("Failed to show modal:", modalError); - if (!i.replied && !i.deferred) { - await i.reply({ - content: "Failed to open calculator. Please try again.", - flags: ["Ephemeral"], - }).catch(console.error); - } - return; - } - - } catch (error) { - logger.error("Button interaction error:", error); - if (!i.deferred && !i.replied) { - await i.followUp({ - content: "An error occurred while processing your request.", - flags: ["Ephemeral"], - }).catch(console.error); - } - } - }); - - collector.on("end", (collected, reason) => { - if (reason === "timeout") { - const disabledRow = - new ActionRowBuilder().addComponents( - new ButtonBuilder() - .setCustomId( - `calc_${interaction.id}_expired`, - ) - .setLabel("Calculator Expired") - .setStyle(ButtonStyle.Secondary) - .setDisabled(true), - ); - - interaction - .editReply({ - components: [disabledRow], - content: - "⏱️ This calculator has expired. Use the command again to perform more calculations.", - }) - .catch(console.error); - } else { - const disabledRow = ActionRowBuilder.from( - row, - ).setComponents( - row.components.map((component) => - ButtonBuilder.from(component).setDisabled(true), - ), - ); - - interaction - .editReply({ components: [disabledRow] }) - .catch(console.error); - } - }); - } catch (error) { - logger.error('Calculation error:', error); - - let errorMessage = 'Failed to evaluate the expression.'; - - if (error.message.includes('Unexpected type')) { - errorMessage += - 'The expression contains an unsupported operation or function.'; - } else if (error.message.includes('Undefined symbol')) { - errorMessage += - 'The expression contains an undefined variable or function.'; - } else if (error.message.includes('Brackets not balanced')) { - errorMessage += 'The expression has unbalanced brackets.'; - } else if ( - error.message.includes('Unexpected operator') || - error.message.includes('Unexpected character') - ) { - errorMessage += - 'The expression contains an invalid operator or character.'; - } else { - errorMessage += 'Please check the syntax and try again.'; - } - - await replyUserError(interaction, { - type: ErrorTypes.VALIDATION, - message: errorMessage, - }); - } - } catch (error) { - await handleInteractionError(interaction, error, { - type: 'command', - commandName: 'calculate' -======= const historyText = userHistory .map( (item, index) => @@ -620,7 +329,6 @@ const BUTTON_TIMEOUT = 300000; await replyUserError(interaction, { type: ErrorTypes.VALIDATION, message: errorMessage, ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) }); } }, diff --git a/src/commands/Tools/countdown.js b/src/commands/Tools/countdown.js index 2cad81c666..d7561ddbf3 100644 --- a/src/commands/Tools/countdown.js +++ b/src/commands/Tools/countdown.js @@ -1,10 +1,6 @@ import { SlashCommandBuilder, MessageFlags } from 'discord.js'; import { successEmbed } from '../../utils/embeds.js'; import { logger } from '../../utils/logger.js'; -<<<<<<< HEAD -import { handleInteractionError } from '../../utils/errorHandler.js'; -======= ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) import { InteractionHelper } from '../../utils/interactionHelper.js'; import { createControlButtons, formatTime, startCountdown } from '../../handlers/countdownButtons.js'; @@ -50,61 +46,6 @@ export default { return; } -<<<<<<< HEAD - try { - const minutes = interaction.options.getInteger("minutes") || 0; - const seconds = interaction.options.getInteger("seconds") || 0; - const title = interaction.options.getString("title") || "Countdown Timer"; - - const totalSeconds = minutes * 60 + seconds; - - if (totalSeconds <= 0) { - throw new Error("Please specify a duration of at least 1 second."); - } - - if (totalSeconds > 86400) { - throw new Error("Countdown cannot be longer than 24 hours."); - } - - const endTime = Date.now() + totalSeconds * 1000; - const countdownId = `${interaction.channelId}-${Date.now()}`; - - const row = createControlButtons(countdownId); - - const initialEmbed = successEmbed( - `⏱️ ${title}`, - `Time remaining: **${formatTime(totalSeconds)}**`, - ); - - const message = await interaction.channel.send({ - embeds: [initialEmbed], - components: [row], - }); - - const countdownData = { - message, - endTime, - remainingTime: totalSeconds * 1000, - isPaused: false, - title, - lastUpdate: Date.now(), - interval: null, - }; - - activeCountdowns.set(countdownId, countdownData); - startCountdown(countdownId, countdownData, activeCountdowns); - - await InteractionHelper.safeEditReply(interaction, { - content: "✅ Countdown started!", - flags: MessageFlags.Ephemeral, - }); - } catch (error) { - await handleInteractionError(interaction, error, { - type: 'command', - commandName: 'countdown' - }); - } -======= const minutes = interaction.options.getInteger("minutes") || 0; const seconds = interaction.options.getInteger("seconds") || 0; const title = interaction.options.getString("title") || "Countdown Timer"; @@ -151,6 +92,5 @@ export default { content: "✅ Countdown started!", flags: MessageFlags.Ephemeral, }); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) }, }; \ No newline at end of file diff --git a/src/commands/Tools/generatepassword.js b/src/commands/Tools/generatepassword.js index 6c94154090..51628bfed0 100644 --- a/src/commands/Tools/generatepassword.js +++ b/src/commands/Tools/generatepassword.js @@ -1,16 +1,9 @@ -<<<<<<< HEAD -======= import { webcrypto as crypto } from 'node:crypto'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) import { getColor } from '../../config/bot.js'; import { SlashCommandBuilder, MessageFlags } from 'discord.js'; import { createEmbed, successEmbed, infoEmbed, warningEmbed } from '../../utils/embeds.js'; import { logger } from '../../utils/logger.js'; -<<<<<<< HEAD -import { handleInteractionError } from '../../utils/errorHandler.js'; -======= import { replyUserError, ErrorTypes } from '../../utils/errorHandler.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) import { InteractionHelper } from '../../utils/interactionHelper.js'; export default { data: new SlashCommandBuilder() @@ -49,115 +42,6 @@ export default { return; } -<<<<<<< HEAD - try { - const length = interaction.options.getInteger('length') || 16; - const includeUppercase = interaction.options.getBoolean('uppercase') ?? true; - const includeNumbers = interaction.options.getBoolean('numbers') ?? true; - const includeSymbols = interaction.options.getBoolean('symbols') ?? true; - - if (length < 8 || length > 50) { - await replyUserError(interaction, { type: ErrorTypes.VALIDATION, message: '\'Password must be 8-50 characters. You provided:\' + length' }); - return; - } - - const lowercase = 'abcdefghijklmnopqrstuvwxyz'; - const uppercase = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; - const numbers = '0123456789'; - const symbols = '!@#$%^&*()_+-=[]{}|;:,.<>?'; - - let chars = lowercase; - if (includeUppercase) chars += uppercase; - if (includeNumbers) chars += numbers; - if (includeSymbols) chars += symbols; - - let password = ''; - const randomValues = new Uint32Array(length); - crypto.getRandomValues(randomValues); - - for (let i = 0; i < length; i++) { - const randomIndex = randomValues[i] % chars.length; - password += chars[randomIndex]; - } - - if (includeUppercase && !/[A-Z]/.test(password)) { - const randomIndex = Math.floor(Math.random() * length); - const randomUpper = uppercase[Math.floor(Math.random() * uppercase.length)]; - password = password.substring(0, randomIndex) + randomUpper + password.substring(randomIndex + 1); - } - - if (includeNumbers && !/[0-9]/.test(password)) { - const randomIndex = Math.floor(Math.random() * length); - const randomNumber = numbers[Math.floor(Math.random() * numbers.length)]; - password = password.substring(0, randomIndex) + randomNumber + password.substring(randomIndex + 1); - } - - if (includeSymbols && !/[!@#$%^&*()_+\-=\[\]{}|;:,.<>?]/.test(password)) { - const randomIndex = Math.floor(Math.random() * length); - const randomSymbol = symbols[Math.floor(Math.random() * symbols.length)]; - password = password.substring(0, randomIndex) + randomSymbol + password.substring(randomIndex + 1); - } - - let strength = 'Weak'; - let strengthEmoji = '🔴'; -let strengthColor = getColor('error'); - - const hasLower = /[a-z]/.test(password); - const hasUpper = /[A-Z]/.test(password); - const hasNumber = /[0-9]/.test(password); - const hasSymbol = /[^a-zA-Z0-9]/.test(password); - - const uniqueChars = new Set(password).size; - const uniqueRatio = uniqueChars / password.length; - - let score = 0; - score += password.length * 4; - score += (password.length - (password.match(/[a-z]/g) || []).length) * 2; - score += (password.length - (password.match(/[A-Z]/g) || []).length) * 2; - score += (password.match(/[0-9]/g) || []).length * 4; - score += (password.match(/[^a-zA-Z0-9]/g) || []).length * 6; - - if (uniqueRatio < 0.5) score *= 0.7; - if (hasLower && hasUpper) score *= 1.2; - if (hasNumber) score *= 1.2; - if (hasSymbol) score *= 1.3; - - if (score > 80) { - strength = 'Very Strong'; - strengthEmoji = '🟢'; -strengthColor = getColor('success'); - } else if (score > 60) { - strength = 'Strong'; - strengthEmoji = '🟢'; -strengthColor = getColor('success'); - } else if (score > 40) { - strength = 'Good'; - strengthEmoji = '🟡'; -strengthColor = getColor('warning'); - } else if (score > 20) { - strength = 'Weak'; - strengthEmoji = '🟠'; -strengthColor = getColor('warning'); - } - - const embed = successEmbed( - '🔑 Generated Password', - `**Password:** ||\`${password}\`||\n` + - `**Length:** ${password.length} characters\n` + - `**Strength:** ${strengthEmoji} ${strength}\n` + - `**Contains:** ${hasLower ? 'Lowercase' : ''}${hasUpper ? ', Uppercase' : ''}${hasNumber ? ', Numbers' : ''}${hasSymbol ? ', Symbols' : ''}` - ).setColor(strengthColor); - - await InteractionHelper.safeEditReply(interaction, { - embeds: [embed], - }); - } catch (error) { - await handleInteractionError(interaction, error, { - type: 'command', - commandName: 'generatepassword' - }); - } -======= const length = interaction.options.getInteger('length') || 16; const includeUppercase = interaction.options.getBoolean('uppercase') ?? true; const includeNumbers = interaction.options.getBoolean('numbers') ?? true; @@ -258,6 +142,5 @@ strengthColor = getColor('warning'); await InteractionHelper.safeEditReply(interaction, { embeds: [embed], }); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) }, }; \ No newline at end of file diff --git a/src/commands/Tools/hexcolor.js b/src/commands/Tools/hexcolor.js index c0eb7b48fd..36736e3a6e 100644 --- a/src/commands/Tools/hexcolor.js +++ b/src/commands/Tools/hexcolor.js @@ -4,10 +4,7 @@ import { logger } from '../../utils/logger.js'; import { getColor } from '../../config/bot.js'; import { InteractionHelper } from '../../utils/interactionHelper.js'; -<<<<<<< HEAD -======= import { replyUserError, ErrorTypes } from '../../utils/errorHandler.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) export default { data: new SlashCommandBuilder() .setName('hexcolor') diff --git a/src/commands/Tools/poll.js b/src/commands/Tools/poll.js index 6ac4eb64da..772f1500c5 100644 --- a/src/commands/Tools/poll.js +++ b/src/commands/Tools/poll.js @@ -1,10 +1,6 @@ import { SlashCommandBuilder, MessageFlags } from 'discord.js'; import { createEmbed, errorEmbed, successEmbed, infoEmbed, warningEmbed } from '../../utils/embeds.js'; import { logger } from '../../utils/logger.js'; -<<<<<<< HEAD -import { handleInteractionError } from '../../utils/errorHandler.js'; -======= ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) import { getColor } from '../../config/bot.js'; import { InteractionHelper } from '../../utils/interactionHelper.js'; const EMOJIS = ['1️⃣', '2️⃣', '3️⃣', '4️⃣', '5️⃣', '6️⃣', '7️⃣', '8️⃣', '9️⃣', '🔟']; @@ -73,54 +69,6 @@ export default { return; } -<<<<<<< HEAD - try { - const question = interaction.options.getString('question'); - const isAnonymous = interaction.options.getBoolean('anonymous') || false; - - const options = []; - for (let i = 1; i <= MAX_OPTIONS; i++) { - const option = interaction.options.getString(`option${i}`); - if (option) options.push(option); - } - - if (options.length < 2) { - throw new Error("You must provide at least 2 options for the poll."); - } - - let description = `**${question}**\n\n`; - options.forEach((option, index) => { - description += `${EMOJIS[index]} ${option}\n`; - }); - - if (isAnonymous) { - description += '\n*This is an anonymous poll. Votes are not tracked to users.*'; - } else { - description += '\n*React with the emoji to vote!*'; - } - - const embed = successEmbed( - `📊 ${isAnonymous ? 'Anonymous ' : ''}Poll`, - description - ); - - const message = await interaction.channel.send({ embeds: [embed] }); - - for (let i = 0; i < options.length; i++) { - await message.react(EMOJIS[i]); - await new Promise(resolve => setTimeout(resolve, 500)); - } - - await InteractionHelper.safeEditReply(interaction, { - content: '✅ Poll created successfully!', - }); - } catch (error) { - await handleInteractionError(interaction, error, { - type: 'command', - commandName: 'poll' - }); - } -======= const question = interaction.options.getString('question'); const isAnonymous = interaction.options.getBoolean('anonymous') || false; @@ -160,6 +108,5 @@ export default { await InteractionHelper.safeEditReply(interaction, { content: '✅ Poll created successfully!', }); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) }, }; \ No newline at end of file diff --git a/src/commands/Tools/randomuser.js b/src/commands/Tools/randomuser.js index c0771e7609..4dc15a6fd1 100644 --- a/src/commands/Tools/randomuser.js +++ b/src/commands/Tools/randomuser.js @@ -1,11 +1,7 @@ import { SlashCommandBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } from 'discord.js'; import { createEmbed, successEmbed, infoEmbed, warningEmbed } from '../../utils/embeds.js'; import { logger } from '../../utils/logger.js'; -<<<<<<< HEAD -import { handleInteractionError, replyUserError, ErrorTypes } from '../../utils/errorHandler.js'; -======= import { replyUserError, ErrorTypes } from '../../utils/errorHandler.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) import { getColor } from '../../config/bot.js'; import { InteractionHelper } from '../../utils/interactionHelper.js'; @@ -41,168 +37,6 @@ export default { return; } -<<<<<<< HEAD -try { - if (!interaction.guild) { - return replyUserError(interaction, { - type: ErrorTypes.VALIDATION, - message: 'This command can only be used in a server/guild.', - }); - } - - const role = interaction.options.getRole('role'); - const includeBots = interaction.options.getBoolean('bots') || false; - const onlineOnly = interaction.options.getBoolean('online') || false; - const shouldMention = interaction.options.getBoolean('mention') || false; - - let members = interaction.guild.members.cache.filter(member => { - if (member.user.bot && !includeBots) return false; - - if (onlineOnly && member.presence?.status === 'offline') return false; - - if (role && !member.roles.cache.has(role.id)) return false; - - return true; - }); - - let memberArray = Array.from(members.values()); - - if (!includeBots) { - memberArray = memberArray.filter(member => !member.user.bot); - } - - if (memberArray.length === 0) { - let errorMessage = 'Could not find any users matching your filters:'; - if (role) errorMessage = `No users have the **${role.name}** role.`; - if (onlineOnly) errorMessage = 'No users are currently online.'; - if (role && onlineOnly) errorMessage = `No **${role.name}** members are online.`; - - return replyUserError(interaction, { - type: ErrorTypes.USER_INPUT, - message: errorMessage + '\n\nTry adjusting your filters.', - }); - } - - const randomIndex = Math.floor(Math.random() * memberArray.length); - const selectedMember = memberArray[randomIndex]; - - const user = selectedMember.user; - const joinDate = selectedMember.joinedAt; - const roles = selectedMember.roles.cache -.filter(role => role.id !== interaction.guild.id) - .sort((a, b) => b.position - a.position) - .map(role => role.toString()) -.slice(0, 10); - - const embed = successEmbed( - '🎲 Random User Selected', - shouldMention ? `${selectedMember}` : `**${user.username}**` - ) - .setThumbnail(user.displayAvatarURL({ dynamic: true, size: 256 })) - .addFields( - { name: 'Username', value: user.username, inline: true }, - { name: 'Bot', value: user.bot ? 'Yes' : 'No', inline: true }, - { name: `Roles (${roles.length})`, value: roles.length > 0 ? roles.slice(0, 5).join('') + (roles.length > 5 ? `+${roles.length - 5} more` : '') : 'No roles', inline: false } - ) - .setColor('primary'); - - const row = new ActionRowBuilder() - .addComponents( - new ButtonBuilder() - .setCustomId(`randomuser_${interaction.user.id}_again`) - .setLabel('🎲 Pick Another User') - .setStyle(ButtonStyle.Primary) - ); - - const response = await interaction.editReply({ - content: shouldMention ? `${selectedMember}, you've been chosen!` : null, - embeds: [embed], - components: [row], - allowedMentions: { users: shouldMention ? [user.id] : [] } - }); - - const filter = (i) => i.customId === `randomuser_${interaction.user.id}_again` && i.user.id === interaction.user.id; -const collector = response.createMessageComponentCollector({ filter, time: 300000 }); - - collector.on('collect', async (i) => { - try { - let newMembers = interaction.guild.members.cache.filter(member => { - if (member.user.bot && !includeBots) return false; - - if (onlineOnly && member.presence?.status === 'offline') return false; - - if (role && !member.roles.cache.has(role.id)) return false; - - return true; - }); - - let newMemberArray = Array.from(newMembers.values()); - - if (!includeBots) { - newMemberArray = newMemberArray.filter(member => !member.user.bot); - } - - if (newMemberArray.length === 0) { - await replyUserError(i, { - type: ErrorTypes.USER_INPUT, - message: 'No users found matching the criteria.', - }); - return; - } - - const newRandomIndex = Math.floor(Math.random() * newMemberArray.length); - const newSelectedMember = newMemberArray[newRandomIndex]; - const newUser = newSelectedMember.user; - - const newRoles = newSelectedMember.roles.cache - .filter(r => r.id !== interaction.guild.id) - .sort((a, b) => b.position - a.position) - .map(r => r.toString()) - .slice(0, 10); - - const newEmbed = successEmbed( - '🎲 Random User Selected', - shouldMention ? `${newSelectedMember}` : `**${newUser.username}**` - ) - .setThumbnail(newUser.displayAvatarURL({ dynamic: true, size: 256 })) - .addFields( - { name: 'Username', value: newUser.username, inline: true }, - { name: 'Bot', value: newUser.bot ? 'Yes' : 'No', inline: true }, - { name: `Roles (${newRoles.length})`, value: newRoles.length > 0 ? newRoles.slice(0, 5).join('') + (newRoles.length > 5 ? `+${newRoles.length - 5} more` : '') : 'No roles', inline: false } - ) - .setColor(newSelectedMember.displayHexColor || '#3498db'); - - await i.update({ - content: shouldMention ? `${newSelectedMember}, you've been chosen!` : null, - embeds: [newEmbed], - components: [row], - allowedMentions: { users: shouldMention ? [newUser.id] : [] } - }); - - } catch (error) { - logger.error('Button interaction error:', error); - await i.reply({ - content: 'An error occurred while selecting another user.', - flags: ['Ephemeral'] - }); - } - }); - - collector.on('end', () => { - const disabledRow = ActionRowBuilder.from(row).setComponents( - ButtonBuilder.from(row.components[0]).setDisabled(true) - ); - - interaction.editReply({ components: [disabledRow] }).catch(console.error); - }); - - } catch (error) { - await handleInteractionError(interaction, error, { - type: 'command', - commandName: 'randomuser' - }); - } -======= if (!interaction.guild) { return replyUserError(interaction, { type: ErrorTypes.VALIDATION, @@ -355,6 +189,5 @@ const collector = response.createMessageComponentCollector({ filter, time: 30000 interaction.editReply({ components: [disabledRow] }).catch(console.error); }); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) }, }; \ No newline at end of file diff --git a/src/commands/Tools/shorten.js b/src/commands/Tools/shorten.js index 800b356dbe..4b5b9af5f5 100644 --- a/src/commands/Tools/shorten.js +++ b/src/commands/Tools/shorten.js @@ -1,11 +1,7 @@ import { SlashCommandBuilder, MessageFlags } from 'discord.js'; import { createEmbed, successEmbed, infoEmbed, warningEmbed } from '../../utils/embeds.js'; import { logger } from '../../utils/logger.js'; -<<<<<<< HEAD -import { handleInteractionError, replyUserError, ErrorTypes } from '../../utils/errorHandler.js'; -======= import { replyUserError, ErrorTypes } from '../../utils/errorHandler.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) import { getColor } from '../../config/bot.js'; import { InteractionHelper } from '../../utils/interactionHelper.js'; @@ -41,96 +37,6 @@ export default { return; } -<<<<<<< HEAD - try { - const url = interaction.options.getString("url"); - const custom = interaction.options.getString("custom"); - - try { - new URL(url); - } catch (e) { - return replyUserError(interaction, { - type: ErrorTypes.VALIDATION, - message: 'Invalid URL format. Include http:// or https://', - }); - } - - if (custom && !/^[a-zA-Z0-9_-]+$/.test(custom)) { - return replyUserError(interaction, { - type: ErrorTypes.VALIDATION, - message: 'Custom URL can only contain letters, numbers, underscores, and hyphens.', - }); - } - - let apiUrl = `https://is.gd/create.php?format=simple&url=${encodeURIComponent(url)}`; - if (custom) { - apiUrl += `&shorturl=${encodeURIComponent(custom)}`; - } - - const controller = new AbortController(); - const timeout = setTimeout(() => controller.abort(), 10000); - - let response; - try { - response = await fetch(apiUrl, { - signal: controller.signal, - headers: { - 'User-Agent': 'TitanBot URL Shortener/1.0' - } - }); - } catch (networkError) { - const message = networkError?.name === 'AbortError' - ? 'The URL shortener timed out. Please try again in a moment.' - : 'Unable to reach the URL shortener service right now. Please try again later.'; - return replyUserError(interaction, { - type: ErrorTypes.NETWORK, - message, - }); - } finally { - clearTimeout(timeout); - } - - if (!response.ok) { - return replyUserError(interaction, { - type: ErrorTypes.UNKNOWN, - message: `Shortener service returned HTTP ${response.status}. Please try again later.`, - }); - } - - const shortUrl = await response.text(); - - try { - new URL(shortUrl); - } catch (e) { - if (shortUrl.includes("already exists")) { - return replyUserError(interaction, { - type: ErrorTypes.VALIDATION, - message: 'That custom URL is already taken. Try a different one.', - }); - } else if (shortUrl.includes("invalid")) { - return replyUserError(interaction, { - type: ErrorTypes.VALIDATION, - message: 'Invalid URL. Include http:// or https://', - }); - } - return replyUserError(interaction, { - type: ErrorTypes.UNKNOWN, - message: `URL shortening failed: ${shortUrl}`, - }); - } - - const embed = successEmbed('URL Shortened', `Here's your shortened URL: ${shortUrl}`); - embed.setColor(getColor('success')); - await InteractionHelper.safeEditReply(interaction, { - embeds: [embed], - }); - } catch (error) { - await handleInteractionError(interaction, error, { - type: 'command', - commandName: 'shorten' - }); - } -======= const url = interaction.options.getString("url"); const custom = interaction.options.getString("custom"); @@ -212,6 +118,5 @@ export default { await InteractionHelper.safeEditReply(interaction, { embeds: [embed], }); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) }, }; \ No newline at end of file diff --git a/src/commands/Utility/avatar.js b/src/commands/Utility/avatar.js index bb1b2b9ce9..b9caf87daf 100644 --- a/src/commands/Utility/avatar.js +++ b/src/commands/Utility/avatar.js @@ -1,10 +1,6 @@ import { SlashCommandBuilder, MessageFlags } from 'discord.js'; import { createEmbed } from '../../utils/embeds.js'; import { logger } from '../../utils/logger.js'; -<<<<<<< HEAD -import { handleInteractionError } from '../../utils/errorHandler.js'; -======= ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) import { InteractionHelper } from '../../utils/interactionHelper.js'; export default { @@ -20,37 +16,6 @@ export default { ), async execute(interaction) { -<<<<<<< HEAD - try { - const user = interaction.options.getUser("target") || interaction.user; - const avatarUrl = user.displayAvatarURL({ size: 2048, dynamic: true }); - - const embed = createEmbed({ - title: `${user.username}'s Avatar`, - description: `[Download Link](${avatarUrl})` - }) - .setImage(avatarUrl); - - await InteractionHelper.safeReply(interaction, { embeds: [embed] }); - logger.info(`Avatar command executed`, { - userId: interaction.user.id, - targetUserId: user.id, - guildId: interaction.guildId - }); - } catch (error) { - logger.error(`Avatar command execution failed`, { - error: error.message, - stack: error.stack, - userId: interaction.user.id, - guildId: interaction.guildId, - commandName: 'avatar' - }); - await handleInteractionError(interaction, error, { - commandName: 'avatar', - source: 'avatar_command' - }); - } -======= const user = interaction.options.getUser("target") || interaction.user; const avatarUrl = user.displayAvatarURL({ size: 2048, dynamic: true }); @@ -66,6 +31,5 @@ export default { targetUserId: user.id, guildId: interaction.guildId }); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } }; \ No newline at end of file diff --git a/src/commands/Utility/firstmsg.js b/src/commands/Utility/firstmsg.js index cc2621237b..1624921387 100644 --- a/src/commands/Utility/firstmsg.js +++ b/src/commands/Utility/firstmsg.js @@ -2,10 +2,6 @@ import { SlashCommandBuilder, PermissionFlagsBits, PermissionsBitField, ChannelT import { createEmbed, errorEmbed, successEmbed } from '../../utils/embeds.js'; import { InteractionHelper } from '../../utils/interactionHelper.js'; import { logger } from '../../utils/logger.js'; -<<<<<<< HEAD -import { handleInteractionError } from '../../utils/errorHandler.js'; -======= ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) export default { data: new SlashCommandBuilder() .setName("firstmsg") @@ -15,69 +11,6 @@ export default { category: "Utility", async execute(interaction, config, client) { -<<<<<<< HEAD - try { - const deferSuccess = await InteractionHelper.safeDefer(interaction); - if (!deferSuccess) { - logger.warn(`FirstMsg interaction defer failed`, { - userId: interaction.user.id, - guildId: interaction.guildId, - commandName: 'firstmsg' - }); - return; - } - - const messages = await interaction.channel.messages.fetch({ - limit: 1, - after: '1', - cache: false - }); - - const firstMessage = messages.first(); - - if (!firstMessage) { - logger.info(`FirstMsg - no messages found in channel`, { - userId: interaction.user.id, - channelId: interaction.channelId, - guildId: interaction.guildId - }); - return await InteractionHelper.safeEditReply(interaction, { - embeds: [successEmbed('First Message', "No messages found in this channel!")], - }); - } - - const messageLink = `https://discord.com/channels/${interaction.guildId}/${interaction.channelId}/${firstMessage.id}`; - - await InteractionHelper.safeEditReply(interaction, { - embeds: [ - successEmbed( - "First Message in #" + interaction.channel.name, - `Message Link: ${messageLink}` - ), - ], - }); - - logger.info(`FirstMsg command executed`, { - userId: interaction.user.id, - channelId: interaction.channelId, - messageId: firstMessage.id, - guildId: interaction.guildId - }); - } catch (error) { - logger.error(`FirstMsg command execution failed`, { - error: error.message, - stack: error.stack, - userId: interaction.user.id, - channelId: interaction.channelId, - guildId: interaction.guildId, - commandName: 'firstmsg' - }); - await handleInteractionError(interaction, error, { - commandName: 'firstmsg', - source: 'firstmsg_command' - }); - } -======= const deferSuccess = await InteractionHelper.safeDefer(interaction); if (!deferSuccess) { logger.warn(`FirstMsg interaction defer failed`, { @@ -124,6 +57,5 @@ export default { messageId: firstMessage.id, guildId: interaction.guildId }); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) }, }; \ No newline at end of file diff --git a/src/commands/Utility/modules/report.js b/src/commands/Utility/modules/report.js index 3bf8e0ae28..86b34b7773 100644 --- a/src/commands/Utility/modules/report.js +++ b/src/commands/Utility/modules/report.js @@ -1,17 +1,9 @@ import { createEmbed } from '../../../utils/embeds.js'; -<<<<<<< HEAD -import { getGuildConfig } from '../../../services/guildConfig.js'; -import { logEvent, EVENT_TYPES, resolveLogChannel } from '../../../services/loggingService.js'; -import { formatLogLine, resolveUserAuthor } from '../../../utils/logEmbeds.js'; -import { InteractionHelper } from '../../../utils/interactionHelper.js'; -import { handleInteractionError } from '../../../utils/errorHandler.js'; -======= import { getGuildConfig } from '../../../services/config/guildConfig.js'; import { logEvent, EVENT_TYPES, resolveLogChannel } from '../../../services/loggingService.js'; import { formatLogLine, resolveUserAuthor } from '../../../utils/logging/logEmbeds.js'; import { InteractionHelper } from '../../../utils/interactionHelper.js'; import { replyUserError, ErrorTypes } from '../../../utils/errorHandler.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) import { logger } from '../../../utils/logger.js'; export default { @@ -33,48 +25,6 @@ export default { return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'The report channel has not been set up. Ask a moderator to use `/logging dashboard` or `/logging channel`.' }); } -<<<<<<< HEAD - try { - const ownerMention = interaction.guild.ownerId - ? `<@${interaction.guild.ownerId}> New report!` - : 'New report!'; - - await logEvent({ - client, - guildId, - eventType: EVENT_TYPES.REPORT_FILE, - content: ownerMention, - data: { - title: 'User Report', - lines: [ - formatLogLine('Reported User', `${targetUser.tag} (\`${targetUser.id}\`)`), - formatLogLine('Reported By', `${interaction.user.tag} (\`${interaction.user.id}\`)`), - formatLogLine('Channel', interaction.channel.toString()), - ], - blockFields: [{ name: 'Reason', value: reason }], - author: await resolveUserAuthor(client, targetUser.id), - thumbnail: targetUser.displayAvatarURL(), - }, - }); - - await InteractionHelper.safeEditReply(interaction, { - embeds: [createEmbed({ - title: 'Report Submitted', - description: `Your report against **${targetUser.tag}** has been successfully filed and sent to the moderation team. Thank you!`, - })], - }); - - logger.info('Report submitted', { - userId: interaction.user.id, - reportedUserId: targetUser.id, - guildId, - reasonLength: reason.length, - }); - } catch (error) { - logger.error('report error:', error); - await handleInteractionError(interaction, error, { commandName: 'report', source: 'report' }); - } -======= const ownerMention = interaction.guild.ownerId ? `<@${interaction.guild.ownerId}> New report!` : 'New report!'; @@ -110,6 +60,5 @@ export default { guildId, reasonLength: reason.length, }); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) }, }; diff --git a/src/commands/Utility/modules/report_setchannel.js b/src/commands/Utility/modules/report_setchannel.js index bead89265d..0da04de754 100644 --- a/src/commands/Utility/modules/report_setchannel.js +++ b/src/commands/Utility/modules/report_setchannel.js @@ -4,10 +4,7 @@ import { setLogChannel } from '../../../services/loggingService.js'; import { InteractionHelper } from '../../../utils/interactionHelper.js'; import { logger } from '../../../utils/logger.js'; -<<<<<<< HEAD -======= import { replyUserError, ErrorTypes } from '../../../utils/errorHandler.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) export default { async execute(interaction, config, client) { if (!interaction.member.permissions.has(PermissionsBitField.Flags.ManageGuild)) { diff --git a/src/commands/Utility/report.js b/src/commands/Utility/report.js index a2dc08389f..561aaf983c 100644 --- a/src/commands/Utility/report.js +++ b/src/commands/Utility/report.js @@ -1,10 +1,5 @@ import { SlashCommandBuilder, ChannelType } from 'discord.js'; -<<<<<<< HEAD -import { logger } from '../../utils/logger.js'; -import { handleInteractionError } from '../../utils/errorHandler.js'; -======= import { replyUserError, ErrorTypes } from '../../utils/errorHandler.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) import { InteractionHelper } from '../../utils/interactionHelper.js'; import report from './modules/report.js'; @@ -48,24 +43,6 @@ export default { category: 'Utility', async execute(interaction, config, client) { -<<<<<<< HEAD - try { - const subcommand = interaction.options.getSubcommand(); - - if (subcommand === 'file') { - return await report.execute(interaction, config, client); - } - - if (subcommand === 'setchannel') { - return await reportSetchannel.execute(interaction, config, client); - } - - return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Unknown subcommand.' }); - } catch (error) { - logger.error('report command error:', error); - await handleInteractionError(interaction, error, { commandName: 'report', source: 'report_command' }); - } -======= const subcommand = interaction.options.getSubcommand(); if (subcommand === 'file') { @@ -77,6 +54,5 @@ export default { } return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Unknown subcommand.' }); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) }, }; \ No newline at end of file diff --git a/src/commands/Utility/serverinfo.js b/src/commands/Utility/serverinfo.js index ea09f0955a..fd417d6845 100644 --- a/src/commands/Utility/serverinfo.js +++ b/src/commands/Utility/serverinfo.js @@ -1,10 +1,6 @@ import { SlashCommandBuilder } from 'discord.js'; import { createEmbed } from '../../utils/embeds.js'; import { logger } from '../../utils/logger.js'; -<<<<<<< HEAD -import { handleInteractionError } from '../../utils/errorHandler.js'; -======= ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) import { InteractionHelper } from '../../utils/interactionHelper.js'; export default { @@ -13,73 +9,13 @@ export default { .setDescription("Get detailed information about the server"), async execute(interaction) { -<<<<<<< HEAD - try { - const deferSuccess = await InteractionHelper.safeDefer(interaction); - if (!deferSuccess) { - logger.warn(`ServerInfo interaction defer failed`, { - userId: interaction.user.id, - guildId: interaction.guildId, - commandName: 'serverinfo' - }); - return; - } - - const guild = interaction.guild; - const owner = await guild.fetchOwner(); - - const createdTimestamp = Math.floor(guild.createdAt.getTime() / 1000); - - const embed = createEmbed({ title: `Server Info: ${guild.name}`, description: `Server ID: ${guild.id}` }) - .setThumbnail(guild.iconURL({ size: 256 })) - .addFields( - { name: "Owner", value: owner.user.tag, inline: true }, - { name: "Members", value: `${guild.memberCount}`, inline: true }, - { - name: "Channels", - value: `${guild.channels.cache.size}`, - inline: true, - }, - { name: "Roles", value: `${guild.roles.cache.size}`, inline: true }, - { - name: "Boosts", - value: `Level ${guild.premiumTier} (${guild.premiumSubscriptionCount})`, - inline: true, - }, - { - name: "Creation Date", - value: ``, - inline: true, - }, - ); - - await InteractionHelper.safeEditReply(interaction, { embeds: [embed] }); - logger.info(`ServerInfo command executed`, { - userId: interaction.user.id, - guildId: guild.id, - guildName: guild.name, - memberCount: guild.memberCount - }); - } catch (error) { - logger.error(`ServerInfo command execution failed`, { - error: error.message, - stack: error.stack, -======= const deferSuccess = await InteractionHelper.safeDefer(interaction); if (!deferSuccess) { logger.warn(`ServerInfo interaction defer failed`, { ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) userId: interaction.user.id, guildId: interaction.guildId, commandName: 'serverinfo' }); -<<<<<<< HEAD - await handleInteractionError(interaction, error, { - commandName: 'serverinfo', - source: 'serverinfo_command' - }); - } -======= return; } @@ -118,6 +54,5 @@ export default { guildName: guild.name, memberCount: guild.memberCount }); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) }, }; \ No newline at end of file diff --git a/src/commands/Utility/todo.js b/src/commands/Utility/todo.js index 9ec05d81df..af82306340 100644 --- a/src/commands/Utility/todo.js +++ b/src/commands/Utility/todo.js @@ -2,11 +2,7 @@ import { SlashCommandBuilder, PermissionFlagsBits, PermissionsBitField, ChannelT import { createEmbed, successEmbed, infoEmbed, warningEmbed } from '../../utils/embeds.js'; import { getFromDb, setInDb } from '../../utils/database.js'; import { logger } from '../../utils/logger.js'; -<<<<<<< HEAD -import { handleInteractionError } from '../../utils/errorHandler.js'; -======= import { replyUserError, ErrorTypes } from '../../utils/errorHandler.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) import { InteractionHelper } from '../../utils/interactionHelper.js'; import crypto from 'crypto'; @@ -173,355 +169,13 @@ export default { return listData; } -<<<<<<< HEAD - try { - const deferSuccess = await InteractionHelper.safeDefer(interaction); - if (!deferSuccess) { - logger.warn(`Todo interaction defer failed`, { - userId: interaction.user.id, - guildId: interaction.guildId, - commandName: 'todo' - }); - return; - } - - if (shareSubcommand) { - switch (shareSubcommand) { - case 'create': { - const listName = interaction.options.getString('name'); - const listId = generateShareId(); - - await getOrCreateSharedList(listId, userId, listName); - - const userSharedLists = await getFromDb(`user_shared_lists_${userId}`, []); - const sharedListsArray = Array.isArray(userSharedLists) ? userSharedLists : []; - if (!sharedListsArray.includes(listId)) { - sharedListsArray.push(listId); - await setInDb(`user_shared_lists_${userId}`, sharedListsArray); - } - - return await InteractionHelper.safeEditReply(interaction, { - embeds: [ - successEmbed( - "Shared List Created", - `Created shared list "${listName}" with ID: \`${listId}\`\n` + - `Use \`/todo share add list_id:${listId} user:@username\` to add members.` - ) - ] - }); - } - - case 'add': { - const listId = interaction.options.getString('list_id'); - const memberToAdd = interaction.options.getUser('user'); - - const listData = await getOrCreateSharedList(listId); - if (!listData) { - return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Shared list not found.' }); - } - - if (listData.creatorId !== userId) { - return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Only the list creator can add members.' }); - } - - if (!listData.members.includes(memberToAdd.id)) { - listData.members.push(memberToAdd.id); - await setInDb(`shared_todo_${listId}`, listData); - - const memberLists = await getFromDb(`user_shared_lists_${memberToAdd.id}`, []); - const memberListsArray = Array.isArray(memberLists) ? memberLists : []; - if (!memberListsArray.includes(listId)) { - memberListsArray.push(listId); - await setInDb(`user_shared_lists_${memberToAdd.id}`, memberListsArray); - } - - return await InteractionHelper.safeEditReply(interaction, { - embeds: [ - successEmbed('Member Added', - `Added ${memberToAdd.username} to the shared list "${listData.name}"` - ) - ] - }); - } else { - return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'User is already a member of this list.' }); - } - } - - case 'view': { - const listId = interaction.options.getString('list_id'); - const listData = await getOrCreateSharedList(listId); - - if (!listData) { - return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Shared list not found.' }); - } - - if (!listData.members.includes(userId)) { - return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'You don\'t have access to this list.' }); - } - - if (listData.tasks.length === 0) { - const memberList = listData.members.map(memberId => { - const member = interaction.guild.members.cache.get(memberId); - return member ? member.user.username : `<@${memberId}>`; - }).join(','); - - const owner = interaction.guild.members.cache.get(listData.creatorId); - const ownerName = owner ? owner.user.username : `<@${listData.creatorId}>`; - - return await InteractionHelper.safeEditReply(interaction, { - embeds: [ - successEmbed( - `📋 **${listData.name}**\n\n` + - `👑 **Owner:** ${ownerName}\n` + - `👥 **Members:** ${memberList}\n\n` + - `*This list is currently empty. Use the "Add Task" button to add tasks!*`, - `Shared List (ID: \`${listId}\`)` - ) - ], - components: [ - new ActionRowBuilder().addComponents( - new ButtonBuilder() - .setCustomId(`shared_todo_add_${listId}`) - .setLabel('Add Task') - .setStyle(ButtonStyle.Primary), - new ButtonBuilder() - .setCustomId(`shared_todo_complete_${listId}`) - .setLabel('Complete Task') - .setStyle(ButtonStyle.Success), - new ButtonBuilder() - .setCustomId(`shared_todo_remove_${listId}`) - .setLabel('Remove Task') - .setStyle(ButtonStyle.Danger) - ) - ] - }); - } - - const taskList = listData.tasks - .map(task => - `${task.completed ? '✅' : '📝'} #${task.id} ${task.text}` + - `\`[${new Date(task.createdAt).toLocaleDateString()}]` + - (task.completed ? `• Completed by ${task.completedBy}` : '') + '`' - ) - .join('\n'); - - const memberList = listData.members.map(memberId => { - const member = interaction.guild.members.cache.get(memberId); - return member ? member.user.username : `<@${memberId}>`; - }).join(','); - - const owner = interaction.guild.members.cache.get(listData.creatorId); - const ownerName = owner ? owner.user.username : `<@${listData.creatorId}>`; - - const fullListDisplay = `📋 **${listData.name}**\n\n` + - `👑 **Owner:** ${ownerName}\n` + - `👥 **Members:** ${memberList}\n\n` + - `**Tasks:**\n${taskList}`; - - return await InteractionHelper.safeEditReply(interaction, { - embeds: [ - successEmbed(`Shared List (ID: \`${listId}\`)`, fullListDisplay) - ], - components: [ - new ActionRowBuilder().addComponents( - new ButtonBuilder() - .setCustomId(`shared_todo_add_${listId}`) - .setLabel('Add Task') - .setStyle(ButtonStyle.Primary), - new ButtonBuilder() - .setCustomId(`shared_todo_complete_${listId}`) - .setLabel('Complete Task') - .setStyle(ButtonStyle.Success), - new ButtonBuilder() - .setCustomId(`shared_todo_remove_${listId}`) - .setLabel('Remove Task') - .setStyle(ButtonStyle.Danger) - ) - ] - }); - } - - case 'addtask': { - const listId = interaction.options.getString('list_id'); - const taskText = interaction.options.getString('task'); - - const listData = await getOrCreateSharedList(listId); - - if (!listData) { - return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Shared list not found.' }); - } - - if (!listData.members.includes(userId)) { - return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'You don\'t have access to this list.' }); - } - - const newTask = { - id: listData.nextId++, - text: taskText, - completed: false, - createdAt: new Date().toISOString(), - createdBy: userId - }; - - listData.tasks.push(newTask); - await setInDb(`shared_todo_${listId}`, listData); - - return await InteractionHelper.safeEditReply(interaction, { - embeds: [ - successEmbed('Task Added', `Added "${taskText}" to the shared list "${listData.name}"`) - ] - }); - } - - case 'remove': { - const listId = interaction.options.getString('list_id'); - const taskNumber = interaction.options.getInteger('number'); - - const listData = await getOrCreateSharedList(listId); - - if (!listData) { - return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Shared list not found.' }); - } - - if (!listData.members.includes(userId)) { - return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'You don\'t have access to this list.' }); - } - - const taskIndex = listData.tasks.findIndex(task => task.id === taskNumber); - if (taskIndex === -1) { - return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Task not found.' }); - } - - const [removedTask] = listData.tasks.splice(taskIndex, 1); - await setInDb(`shared_todo_${listId}`, listData); - - return await InteractionHelper.safeEditReply(interaction, { - embeds: [ - successEmbed('Task Removed', `Removed "${removedTask.text}" from the shared list "${listData.name}".`) - ] - }); - } - } - return; - } - - const dbKey = `todo_${userId}`; - - const userData = await getFromDb(dbKey, { - tasks: [], - nextId: 1 - }); - - if (!userData.tasks) userData.tasks = []; - if (!userData.nextId) userData.nextId = 1; - - switch (subcommand) { - case 'add': { - const taskText = interaction.options.getString('task'); - - const newTask = { - id: userData.nextId++, - text: taskText, - completed: false, - createdAt: new Date().toISOString() - }; - - userData.tasks.push(newTask); - await setInDb(dbKey, userData); - - return await InteractionHelper.safeEditReply(interaction, { - embeds: [ - successEmbed( - "Task Added", - `Added "${taskText}" to your to-do list.` - ), - ], - }); - } - - case 'list': { - if (userData.tasks.length === 0) { - return await InteractionHelper.safeEditReply(interaction, { - embeds: [successEmbed('Your to-do list is empty!', "Your To-Do List")], - }); - } - - const taskList = userData.tasks - .map(task => - `${task.completed ? '✅' : '📝'} #${task.id} ${task.text}` + - `\`[${new Date(task.createdAt).toLocaleDateString()}\`` - ) - .join('\n'); - - return await InteractionHelper.safeEditReply(interaction, { - embeds: [ - successEmbed('Your To-Do List', taskList) - ], - }); - } - - case 'complete': { - const taskNumber = interaction.options.getInteger('number'); - const task = userData.tasks.find(t => t.id === taskNumber); - - if (!task) { - return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Task not found.' }); - } - - if (task.completed) { - return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Task #${task.id} is already completed.' }); - } - - task.completed = true; - await setInDb(`todo_${userId}`, userData); - - return await InteractionHelper.safeEditReply(interaction, { - embeds: [ - successEmbed('Task Completed', `Marked "${task.text}" as complete!`) - ], - }); - } - - case 'remove': { - const taskNumber = interaction.options.getInteger('number'); - const taskIndex = userData.tasks.findIndex(t => t.id === taskNumber); - - if (taskIndex === -1) { - return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Task not found.' }); - } - - const [removedTask] = userData.tasks.splice(taskIndex, 1); - await setInDb(`todo_${userId}`, userData); - - return await InteractionHelper.safeEditReply(interaction, { - embeds: [ - successEmbed('Task Removed', `Removed "${removedTask.text}" from your to-do list.`) - ], - }); - } - - default: - return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Invalid subcommand.' }); - } - } catch (error) { - logger.error(`Todo command execution failed`, { - error: error.message, - stack: error.stack, -======= const deferSuccess = await InteractionHelper.safeDefer(interaction); if (!deferSuccess) { logger.warn(`Todo interaction defer failed`, { ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) userId: interaction.user.id, guildId: interaction.guildId, commandName: 'todo' }); -<<<<<<< HEAD - await handleInteractionError(interaction, error, { - commandName: 'todo', - source: 'todo_command' - }); -======= return; } @@ -842,7 +496,6 @@ export default { default: return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Invalid subcommand.' }); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } }, }; \ No newline at end of file diff --git a/src/commands/Utility/userinfo.js b/src/commands/Utility/userinfo.js index 299706ff09..2455b0f4a9 100644 --- a/src/commands/Utility/userinfo.js +++ b/src/commands/Utility/userinfo.js @@ -1,10 +1,6 @@ import { SlashCommandBuilder } from 'discord.js'; import { createEmbed } from '../../utils/embeds.js'; import { logger } from '../../utils/logger.js'; -<<<<<<< HEAD -import { handleInteractionError } from '../../utils/errorHandler.js'; -======= ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) import { InteractionHelper } from '../../utils/interactionHelper.js'; export default { data: new SlashCommandBuilder() @@ -17,83 +13,13 @@ export default { ), async execute(interaction) { -<<<<<<< HEAD - try { - const deferSuccess = await InteractionHelper.safeDefer(interaction); - if (!deferSuccess) { - logger.warn(`UserInfo interaction defer failed`, { - userId: interaction.user.id, - guildId: interaction.guildId, - commandName: 'userinfo' - }); - return; - } - - const user = interaction.options.getUser("target") || interaction.user; - const member = interaction.guild.members.cache.get(user.id); - - const createdTimestamp = Math.floor(user.createdAt.getTime() / 1000); - const joinedTimestamp = member?.joinedAt ? Math.floor(member.joinedAt.getTime() / 1000) : null; - - const embed = createEmbed({ title: `User Info: ${user.username}` }) - .setThumbnail(user.displayAvatarURL({ size: 256 })) - .addFields( - { name: "ID", value: user.id, inline: true }, - { name: "Bot", value: user.bot ? "Yes" : "No", inline: true }, - { - name: "Roles", - value: - member && member.roles.cache.size > 1 - ? member.roles.cache - .map((r) => r.name) - .slice(0, 5) - .join(",") - : "None", - inline: true, - }, - { - name: "Account Created", - value: ``, - inline: false, - }, - { - name: "Joined Server", - value: joinedTimestamp ? `` : "Not in server", - inline: false, - }, - { - name: "Highest Role", - value: member?.roles?.highest?.name || "None", - inline: true, - }, - ); - - await InteractionHelper.safeEditReply(interaction, { embeds: [embed] }); - logger.info(`UserInfo command executed`, { - userId: interaction.user.id, - targetUserId: user.id, - guildId: interaction.guildId - }); - } catch (error) { - logger.error(`UserInfo command execution failed`, { - error: error.message, - stack: error.stack, -======= const deferSuccess = await InteractionHelper.safeDefer(interaction); if (!deferSuccess) { logger.warn(`UserInfo interaction defer failed`, { ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) userId: interaction.user.id, guildId: interaction.guildId, commandName: 'userinfo' }); -<<<<<<< HEAD - await handleInteractionError(interaction, error, { - commandName: 'userinfo', - source: 'userinfo_command' - }); - } -======= return; } @@ -142,6 +68,5 @@ export default { targetUserId: user.id, guildId: interaction.guildId }); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) }, }; \ No newline at end of file diff --git a/src/commands/Utility/weather.js b/src/commands/Utility/weather.js index fb9c034549..a76c3eeb09 100644 --- a/src/commands/Utility/weather.js +++ b/src/commands/Utility/weather.js @@ -1,11 +1,7 @@ import { SlashCommandBuilder } from 'discord.js'; import { createEmbed, successEmbed, infoEmbed, warningEmbed } from '../../utils/embeds.js'; import { logger } from '../../utils/logger.js'; -<<<<<<< HEAD -import { handleInteractionError } from '../../utils/errorHandler.js'; -======= import { replyUserError, ErrorTypes } from '../../utils/errorHandler.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) import { InteractionHelper } from '../../utils/interactionHelper.js'; const GEOCODING_URL = "https://geocoding-api.open-meteo.com/v1/search"; const WEATHER_URL = "https://api.open-meteo.com/v1/forecast"; @@ -22,112 +18,13 @@ export default { ), async execute(interaction) { -<<<<<<< HEAD - try { - const deferSuccess = await InteractionHelper.safeDefer(interaction); - if (!deferSuccess) { - logger.warn(`Weather interaction defer failed`, { - userId: interaction.user.id, - guildId: interaction.guildId, - commandName: 'weather' - }); - return; - } - - const city = interaction.options.getString("city"); - - const geoResponse = await fetch( - `${GEOCODING_URL}?name=${encodeURIComponent(city)}`, - ); - const geoData = await geoResponse.json(); - - if (!geoData.results || geoData.results.length === 0) { - logger.info(`Weather command - city not found`, { - userId: interaction.user.id, - city: city, - guildId: interaction.guildId - }); - await replyUserError(interaction, { type: ErrorTypes.USER_INPUT, message: 'Could not find a location for **${city}**. Please check the spelling.' }); - return; - } - - const { latitude, longitude, name, country } = geoData.results[0]; - const cityDisplay = name; - - const weatherResponse = await fetch( - `${WEATHER_URL}?latitude=${latitude}&longitude=${longitude}¤t_weather=true`, - ); - const weatherData = await weatherResponse.json(); - - if (weatherData.error) { - logger.error(`Weather API error`, { - error: weatherData.reason, - city: city, - userId: interaction.user.id, - guildId: interaction.guildId - }); - await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'A weather service error occurred.' }); - return; - } - - const current = weatherData.current || weatherData.current_weather || {}; - const temperature = current.temperature != null ? Math.round(current.temperature) : "N/A"; - const humidity = current.relativehumidity ?? current.relative_humidity_2m ?? "N/A"; - const windSpeed = current.windspeed != null ? Math.round(current.windspeed) : "N/A"; - const weatherCode = current.weathercode ?? current.weather_code ?? null; - - const condition = getWeatherDescription(weatherCode); - - const embed = createEmbed({ title: `Weather in ${cityDisplay}, ${country}`, description: condition.description }) - .addFields( - { - name: "Temperature", - value: `${temperature}°C`, - inline: true, - }, - { - name: "Humidity", - value: `${humidity}%`, - inline: true, - }, - { - name: "Wind Speed", - value: `${windSpeed} km/h`, - inline: true, - }, - ) - .setFooter({ - text: `Latitude: ${latitude.toFixed(2)} | Longitude: ${longitude.toFixed(2)}`, - }); - - await InteractionHelper.safeEditReply(interaction, { embeds: [embed] }); - logger.info(`Weather command executed`, { - userId: interaction.user.id, - city: cityDisplay, - country: country, - temperature: temperature, - guildId: interaction.guildId - }); - } catch (error) { - logger.error(`Weather command execution failed`, { - error: error.message, - stack: error.stack, -======= const deferSuccess = await InteractionHelper.safeDefer(interaction); if (!deferSuccess) { logger.warn(`Weather interaction defer failed`, { ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) userId: interaction.user.id, guildId: interaction.guildId, commandName: 'weather' }); -<<<<<<< HEAD - await handleInteractionError(interaction, error, { - commandName: 'weather', - source: 'weather_command' - }); - } -======= return; } @@ -205,7 +102,6 @@ export default { temperature: temperature, guildId: interaction.guildId }); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) }, }; diff --git a/src/commands/Utility/wipedata.js b/src/commands/Utility/wipedata.js index c3bb004212..941265dca8 100644 --- a/src/commands/Utility/wipedata.js +++ b/src/commands/Utility/wipedata.js @@ -2,10 +2,6 @@ import { SlashCommandBuilder, MessageFlags } from 'discord.js'; import { createEmbed, errorEmbed, warningEmbed } from '../../utils/embeds.js'; import { getConfirmationButtons } from '../../utils/components.js'; import { logger } from '../../utils/logger.js'; -<<<<<<< HEAD -import { handleInteractionError } from '../../utils/errorHandler.js'; -======= ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) import { InteractionHelper } from '../../utils/interactionHelper.js'; export default { @@ -15,48 +11,6 @@ export default { .setDescription('Delete all your personal data from the bot (irreversible)'), async execute(interaction, guildConfig, client) { -<<<<<<< HEAD - try { - const warningMessage = - `⚠️ **THIS ACTION IS IRREVERSIBLE!** ⚠️\n\n` + - `This will permanently delete **ALL** your data from this server including:\n` + - `• 💰 Economy balance (wallet & bank)\n` + - `• 📊 Levels and XP\n` + - `• 🎒 Inventory items\n` + - `• 🛍️ Shop purchases\n` + - `• 🎂 Birthday information\n` + - `• 🔢 Counter data\n` + - `• 📋 All other personal data\n\n` + - `**This cannot be undone. Are you absolutely sure?**`; - - const embed = warningEmbed('Wipe All Data', warningMessage); - - const confirmButtons = getConfirmationButtons('wipedata'); - - await InteractionHelper.safeReply(interaction, { - embeds: [embed], - components: [confirmButtons], - flags: MessageFlags.Ephemeral - }); - - logger.info(`Wipedata command executed - confirmation prompt shown`, { - userId: interaction.user.id, - guildId: interaction.guildId - }); - } catch (error) { - logger.error(`Wipedata command execution failed`, { - error: error.message, - stack: error.stack, - userId: interaction.user.id, - guildId: interaction.guildId, - commandName: 'wipedata' - }); - await handleInteractionError(interaction, error, { - commandName: 'wipedata', - source: 'wipedata_command' - }); - } -======= const warningMessage = `⚠️ **THIS ACTION IS IRREVERSIBLE!** ⚠️\n\n` + `This will permanently delete **ALL** your data from this server including:\n` + @@ -83,6 +37,5 @@ export default { userId: interaction.user.id, guildId: interaction.guildId }); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } }; \ No newline at end of file diff --git a/src/commands/Verification/modules/autoVerify.js b/src/commands/Verification/modules/autoVerify.js index 0bb7b68a27..31ca0e881d 100644 --- a/src/commands/Verification/modules/autoVerify.js +++ b/src/commands/Verification/modules/autoVerify.js @@ -1,11 +1,7 @@ import { botConfig, getColor } from '../../../config/bot.js'; import { SlashCommandBuilder, PermissionFlagsBits, MessageFlags } from 'discord.js'; import { createEmbed, errorEmbed, successEmbed, infoEmbed } from '../../../utils/embeds.js'; -<<<<<<< HEAD -import { getGuildConfig, setGuildConfig } from '../../../services/guildConfig.js'; -======= import { getGuildConfig, setGuildConfig } from '../../../services/config/guildConfig.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) import { withErrorHandling, createError, ErrorTypes } from '../../../utils/errorHandler.js'; import { validateAutoVerifyCriteria } from '../../../services/verificationService.js'; import { logger } from '../../../utils/logger.js'; diff --git a/src/commands/Verification/modules/autoVerifyDashboard.js b/src/commands/Verification/modules/autoVerifyDashboard.js index cf845280f7..7209da79fd 100644 --- a/src/commands/Verification/modules/autoVerifyDashboard.js +++ b/src/commands/Verification/modules/autoVerifyDashboard.js @@ -17,11 +17,7 @@ import { InteractionHelper } from '../../../utils/interactionHelper.js'; import { successEmbed } from '../../../utils/embeds.js'; import { logger } from '../../../utils/logger.js'; import { TitanBotError, ErrorTypes, replyUserError } from '../../../utils/errorHandler.js'; -<<<<<<< HEAD -import { getGuildConfig, setGuildConfig } from '../../../services/guildConfig.js'; -======= import { getGuildConfig, setGuildConfig } from '../../../services/config/guildConfig.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) import { getWelcomeConfig } from '../../../utils/database.js'; import { validateAutoVerifyCriteria } from '../../../services/verificationService.js'; import { botHasPermission } from '../../../utils/permissionGuard.js'; @@ -492,11 +488,7 @@ async function handleAccountAge(selectInteraction, rootInteraction, guildConfig, const days = parseInt(inputValue, 10); if (isNaN(days) || days < minAccountAgeDays || days > maxAccountAgeDays) { -<<<<<<< HEAD - await replyUserError(submitted, { type: ErrorTypes.VALIDATION, message: 'Please enter a number between ${minAccountAgeDays} and ${maxAccountAgeDays}.' }); -======= await replyUserError(submitted, { type: ErrorTypes.VALIDATION, message: `Please enter a number between ${minAccountAgeDays} and ${maxAccountAgeDays}.` }); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) return; } diff --git a/src/commands/Verification/modules/verification_dashboard.js b/src/commands/Verification/modules/verification_dashboard.js index 11ebd3648a..ab32689527 100644 --- a/src/commands/Verification/modules/verification_dashboard.js +++ b/src/commands/Verification/modules/verification_dashboard.js @@ -19,11 +19,7 @@ import { InteractionHelper } from '../../../utils/interactionHelper.js'; import { successEmbed } from '../../../utils/embeds.js'; import { logger } from '../../../utils/logger.js'; import { TitanBotError, ErrorTypes, replyUserError } from '../../../utils/errorHandler.js'; -<<<<<<< HEAD -import { getGuildConfig, setGuildConfig } from '../../../services/guildConfig.js'; -======= import { getGuildConfig, setGuildConfig } from '../../../services/config/guildConfig.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) import { getWelcomeConfig } from '../../../utils/database.js'; import { botHasPermission } from '../../../utils/permissionGuard.js'; import { diff --git a/src/commands/Verification/verification.js b/src/commands/Verification/verification.js index 9f3933b1b7..8906809fe7 100644 --- a/src/commands/Verification/verification.js +++ b/src/commands/Verification/verification.js @@ -1,13 +1,8 @@ import { botConfig, getColor } from '../../config/bot.js'; import { SlashCommandBuilder, PermissionFlagsBits, ChannelType, ActionRowBuilder, ButtonBuilder, ButtonStyle, MessageFlags } from 'discord.js'; import { createEmbed, infoEmbed, successEmbed } from '../../utils/embeds.js'; -<<<<<<< HEAD -import { getGuildConfig, setGuildConfig } from '../../services/guildConfig.js'; -import { handleInteractionError, withErrorHandling, createError, ErrorTypes } from '../../utils/errorHandler.js'; -======= import { getGuildConfig, setGuildConfig } from '../../services/config/guildConfig.js'; import { withErrorHandling, createError, ErrorTypes, replyUserError } from '../../utils/errorHandler.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) import { removeVerification, verifyUser } from '../../services/verificationService.js'; import { logger } from '../../utils/logger.js'; import { InteractionHelper } from '../../utils/interactionHelper.js'; @@ -230,41 +225,6 @@ async function handleSetup(interaction, guild, client) { async function handleRemove(interaction, guild, client) { const targetUser = interaction.options.getUser("user"); -<<<<<<< HEAD - - try { - const result = await removeVerification(client, guild.id, targetUser.id, { - moderatorId: interaction.user.id, - reason: 'admin_removal' - }); - - if (!result.success) { - if (result.notVerified) { - return await InteractionHelper.safeReply(interaction, { - embeds: [infoEmbed('Not Verified', `${targetUser.tag} does not currently have the verified role.`)], - flags: MessageFlags.Ephemeral - }); - } - } - - logger.info('Verification removed via command', { - guildId: guild.id, - targetUserId: targetUser.id, - moderatorId: interaction.user.id - }); - - return await InteractionHelper.safeReply(interaction, { - embeds: [successEmbed('Verification Removed', `Verification removed from ${targetUser.tag}.`)] - }); - - } catch (error) { - await handleInteractionError( - interaction, - error, - { command: 'verification', subcommand: 'remove' } - ); - } -======= const result = await removeVerification(client, guild.id, targetUser.id, { moderatorId: interaction.user.id, @@ -287,5 +247,4 @@ async function handleRemove(interaction, guild, client) { return await InteractionHelper.safeReply(interaction, { embeds: [successEmbed('Verification Removed', `Verification removed from ${targetUser.tag}.`)] }); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } \ No newline at end of file diff --git a/src/commands/Verification/verify.js b/src/commands/Verification/verify.js index 80bc7a3cb5..7c203ac8d6 100644 --- a/src/commands/Verification/verify.js +++ b/src/commands/Verification/verify.js @@ -1,13 +1,7 @@ import { SlashCommandBuilder, MessageFlags } from 'discord.js'; import { infoEmbed, successEmbed } from '../../utils/embeds.js'; -<<<<<<< HEAD -import { withErrorHandling } from '../../utils/errorHandler.js'; -import { verifyUser } from '../../services/verificationService.js'; -import { logger } from '../../utils/logger.js'; -======= import { replyUserError, ErrorTypes } from '../../utils/errorHandler.js'; import { verifyUser } from '../../services/verificationService.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) import { InteractionHelper } from '../../utils/interactionHelper.js'; export default { @@ -16,39 +10,6 @@ export default { .setDescription('Verify yourself and gain access to the server'), async execute(interaction, config, client) { -<<<<<<< HEAD - const wrappedExecute = withErrorHandling(async () => { - const guild = interaction.guild; - - const result = await verifyUser(client, guild.id, interaction.user.id, { - source: 'command_self', - moderatorId: null - }); - - if (!result.success) { - if (result.alreadyVerified) { - return await InteractionHelper.safeReply(interaction, { - embeds: [infoEmbed('Already Verified', "You are already verified.")], - flags: MessageFlags.Ephemeral - }); - } - - return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'An error occurred during verification. Please try again or contact an administrator.' }); - } - - await InteractionHelper.safeReply(interaction, { - embeds: [successEmbed( - "Verification Complete", - `You have been verified and given the **${result.roleName}** role! Welcome to the server! 🎉` - )], - flags: MessageFlags.Ephemeral - }); - }, { command: 'verify' }); - - return await wrappedExecute(interaction, config, client); - } -}; -======= const guild = interaction.guild; const result = await verifyUser(client, guild.id, interaction.user.id, { @@ -72,4 +33,3 @@ export default { }); } }; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) diff --git a/src/commands/Welcome/autorole.js b/src/commands/Welcome/autorole.js index 9eecf3ce05..81764e4c46 100644 --- a/src/commands/Welcome/autorole.js +++ b/src/commands/Welcome/autorole.js @@ -3,11 +3,7 @@ import { SlashCommandBuilder, PermissionFlagsBits, ChannelType, EmbedBuilder, Me import { getWelcomeConfig, updateWelcomeConfig } from '../../utils/database.js'; import { logger } from '../../utils/logger.js'; import { InteractionHelper } from '../../utils/interactionHelper.js'; -<<<<<<< HEAD -import { getGuildConfig } from '../../services/guildConfig.js'; -======= import { getGuildConfig } from '../../services/config/guildConfig.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) import { ErrorTypes, replyUserError } from '../../utils/errorHandler.js'; function createAutoroleInfoEmbed(description) { diff --git a/src/commands/Welcome/goodbye.js b/src/commands/Welcome/goodbye.js index 77a57a45b2..3d45b93309 100644 --- a/src/commands/Welcome/goodbye.js +++ b/src/commands/Welcome/goodbye.js @@ -61,11 +61,7 @@ export default { const existingConfig = await getWelcomeConfig(client, guild.id); if (existingConfig?.goodbyeChannelId) { logger.info(`[Goodbye] Setup blocked because config already exists in channel ${existingConfig.goodbyeChannelId} for guild ${guild.id}`); -<<<<<<< HEAD - return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: `Goodbye is already configured for <#${existingConfig.goodbyeChannelId}>. Use **/goodbye config** to customize channel, message, ping, or image.` }); -======= return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: `Goodbye is already configured for <#${existingConfig.goodbyeChannelId}>. Use **/greet dashboard** to customize channel, message, ping, or image.` }); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } if (!message || message.trim().length === 0) { @@ -113,11 +109,7 @@ export default { { name: 'Ping User', value: ping ? 'Yes' : 'No' }, { name: 'Status', value: 'Enabled' } ) -<<<<<<< HEAD - .setFooter({ text: 'Tip: Use /goodbye config to customize goodbye settings' }); -======= .setFooter({ text: 'Tip: Use /greet dashboard to customize goodbye settings' }); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) if (image) { embed.setImage(image); diff --git a/src/commands/Welcome/welcome.js b/src/commands/Welcome/welcome.js index af9ac8aa16..840ba7bdd3 100644 --- a/src/commands/Welcome/welcome.js +++ b/src/commands/Welcome/welcome.js @@ -66,11 +66,7 @@ export default { const existingConfig = await getWelcomeConfig(client, guild.id); if (existingConfig?.channelId) { logger.info(`[Welcome] Setup blocked because config already exists in channel ${existingConfig.channelId} for guild ${guild.id}`); -<<<<<<< HEAD - return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: `Welcome is already configured for <#${existingConfig.channelId}>. Use **/welcome config** to customize channel, message, ping, or image.` }); -======= return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: `Welcome is already configured for <#${existingConfig.channelId}>. Use **/greet dashboard** to customize channel, message, ping, or image.` }); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } if (!message || message.trim().length === 0) { @@ -112,11 +108,7 @@ export default { { name: 'Ping User', value: ping ? 'Yes' : 'No' }, { name: 'Status', value: 'Enabled' } ) -<<<<<<< HEAD - .setFooter({ text: 'Tip: Use /welcome config to customize welcome settings' }); -======= .setFooter({ text: 'Tip: Use /greet dashboard to customize welcome settings' }); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) if (image) { embed.setImage(image); diff --git a/src/config/application.js b/src/config/application.js index 3a321b28df..159b9eb295 100644 --- a/src/config/application.js +++ b/src/config/application.js @@ -2,11 +2,7 @@ import { fileURLToPath } from "url"; import path from "path"; import botConfig, { validateConfig } from "./bot.js"; import { shopConfig as shop } from "./shop/index.js"; -<<<<<<< HEAD -import { pgConfig } from "./postgres.js"; -======= import { pgConfig } from "./database/postgres.js"; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -26,13 +22,8 @@ const appConfig = { ...botConfig, token: process.env.DISCORD_TOKEN || process.env.TOKEN, clientId: process.env.CLIENT_ID, -<<<<<<< HEAD - guildId: process.env.GUILD_ID, - multiGuild: process.env.MULTI_GUILD === 'true', -======= // Retained for tutorial/setup compatibility; not used for command registration. guildId: process.env.GUILD_ID, ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) shop: { ...botConfig.shop, @@ -82,35 +73,8 @@ const appConfig = { shop, features: { -<<<<<<< HEAD - - economy: true, - leveling: true, - moderation: true, - logging: true, - welcome: true, - - tickets: true, - giveaways: true, - birthday: true, - counter: true, - - verification: true, - reactionRoles: true, - joinToCreate: true, - - voice: true, - search: true, - tools: true, - utility: true, - community: true, - fun: true, - - music: true, -======= ...botConfig.features, music: botConfig.features?.music ?? true, ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) }, env: process.env.NODE_ENV || "development", diff --git a/src/config/bot.js b/src/config/bot.js index d5f2855ff0..86dc861cc3 100644 --- a/src/config/bot.js +++ b/src/config/bot.js @@ -23,10 +23,9 @@ export const botConfig = { // 5 = Competing activities: [ { - // Text users will see (example: "Playing /help | Titan Bot"). - name: "Made with ❤️", - // Activity type number (0 = Playing). - type: 0, + name: "Custom Status", // required by Discord API, not shown in the client + state: "stalking", // this is what people actually see + type: 4, // Custom }, ], }, @@ -37,11 +36,7 @@ export const botConfig = { commands: { // Bot owner user IDs (comma-separated in OWNER_IDS env var). // Owners can access owner/admin-level bot commands. -<<<<<<< HEAD - owners: process.env.OWNER_IDS?.split(",") || [], -======= owners: process.env.OWNER_IDS?.split(",").map((id) => id.trim()).filter(Boolean) || [], ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) // Default wait time between command uses (in seconds). defaultCooldown: 3, @@ -49,18 +44,12 @@ export const botConfig = { // If true, old commands are removed before re-registering. deleteCommands: false, -<<<<<<< HEAD - // Optional server ID used for testing slash commands quickly. - testGuildId: process.env.TEST_GUILD_ID, - -======= // Optional server ID retained for tutorial compatibility; not used for command registration. testGuildId: process.env.TEST_GUILD_ID, // When true (or MAINTENANCE_MODE=true), only bot owners can run commands. maintenanceMode: process.env.MAINTENANCE_MODE === "true", ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) // Command prefix for text-based commands (e.g., "!" for "!ping"). // Supports both slash commands and prefix commands. prefix: process.env.PREFIX || "!", @@ -196,8 +185,6 @@ export const botConfig = { begMin: 5, begMax: 50, -<<<<<<< HEAD -======= // Command cooldowns (milliseconds). cooldowns: { daily: 24 * 60 * 60 * 1000, @@ -206,7 +193,6 @@ export const botConfig = { rob: 4 * 60 * 60 * 1000, }, ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) // Chance to succeed when robbing (0.4 = 40%). robSuccessRate: 0.4, @@ -487,10 +473,7 @@ export const botConfig = { utility: true, community: true, fun: true, -<<<<<<< HEAD -======= music: true, ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) }, }; @@ -516,16 +499,6 @@ export function validateConfig(config) { } if (process.env.NODE_ENV === 'production') { -<<<<<<< HEAD - if (!process.env.POSTGRES_HOST) { - errors.push("PostgreSQL host is required in production (POSTGRES_HOST environment variable)"); - } - if (!process.env.POSTGRES_USER) { - errors.push("PostgreSQL user is required in production (POSTGRES_USER environment variable)"); - } - if (!process.env.POSTGRES_PASSWORD) { - errors.push("PostgreSQL password is required in production (POSTGRES_PASSWORD environment variable)"); -======= // A full connection URL (DATABASE_URL / POSTGRES_URL) satisfies all Postgres // requirements, matching how src/config/database/postgres.js resolves the pool config. const hasConnectionUrl = Boolean(process.env.POSTGRES_URL || process.env.DATABASE_URL); @@ -540,7 +513,6 @@ export function validateConfig(config) { if (!process.env.POSTGRES_PASSWORD) { errors.push("PostgreSQL password is required in production (set DATABASE_URL/POSTGRES_URL, or POSTGRES_PASSWORD)"); } ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } } @@ -557,8 +529,6 @@ if (configErrors.length > 0) { export const BotConfig = botConfig; -<<<<<<< HEAD -======= const COMMAND_CATEGORY_FEATURE_MAP = { birthday: "birthday", community: "community", @@ -651,7 +621,6 @@ export function getDefaultApplicationQuestions() { ).filter(Boolean); } ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) export function getColor(path, fallback = "#99AAB5") { if (typeof path === "number") return path; diff --git a/src/config/commandAliases.js b/src/config/commandAliases.js deleted file mode 100644 index b23023a143..0000000000 --- a/src/config/commandAliases.js +++ /dev/null @@ -1,152 +0,0 @@ -/** - * Command Aliases Configuration - * Maps shortened command names to their full command names - */ - -export const commandAliases = { - 'bal': 'balance', - 'money': 'balance', - 'cash': 'balance', - - 'dep': 'deposit', - 'with': 'withdraw', - 'work': 'work', - 'daily': 'daily', - 'gamble': 'gamble', - 'bet': 'gamble', - 'rob': 'rob', - 'crime': 'crime', - 'pay': 'pay', - 'give': 'pay', - 'send': 'pay', - - 'ping': 'ping', - 'help': 'help', - 'h': 'help', - 'info': 'help', - - 'ban': 'ban', - 'kick': 'kick', - 'mute': 'timeout', - 'warn': 'warn', - 'clear': 'purge', - 'purge': 'purge', - 'untimeout': 'untimeout', - 'unmute': 'untimeout', - - 'rank': 'rank', - 'lvl': 'rank', - 'xp': 'rank', - 'leaderboard': 'leaderboard', - 'lb': 'leaderboard', - 'top': 'leaderboard', - - 'shop': 'shop', - 'buy': 'buy', - 'inventory': 'inventory', - 'inv': 'inventory', - 'items': 'inventory', - - 'user': 'userinfo', - 'avatar': 'avatar', - 'pfp': 'avatar', - 'icon': 'avatar', - - 'bd': 'birthday', - 'bday': 'birthday', - 'b': 'birthday', - - 'flip': 'flip', - 'coin': 'flip', - 'roll': 'roll', - 'dice': 'roll', - 'fight': 'fight', - - 'gcreate': 'gcreate', - 'gstart': 'gcreate', - 'gend': 'gend', - 'gstop': 'gend', - 'gdelete': 'gdelete', - 'greroll': 'greroll', - 'groll': 'greroll', - - 'ticket': 'ticket', - 't': 'ticket', - 'new': 'ticket', - - 'ver': 'verify', - 'vadmin': 'verification', - 'av': 'autoverify', - - 'welcome': 'welcome', - 'greet': 'greet', - 'goodbye': 'goodbye', - 'autorole': 'autorole', - - 'calc': 'calculate', - 'math': 'calculate', - 'weather': 'weather', - 'todo': 'todo', - 'report': 'report', - 'userinfo': 'userinfo', - 'whois': 'userinfo', - 'ui': 'userinfo', - - 'serverstats': 'serverstats', - 'ss': 'serverstats', - 'sstats': 'serverstats', - - 'rr': 'reactroles', - 'reactionroles': 'reactroles', - - 'jtc': 'jointocreate', - 'jointocreate': 'jointocreate', - - 'np': 'nowplaying', - 'now': 'nowplaying', -}; - -export const subcommandAliases = { - 'l': 'list', - 'ls': 'list', - 's': 'set', - 'i': 'info', - 'r': 'remove', - 'rm': 'remove', - 'del': 'remove', - 'n': 'next', - 'sc': 'setchannel', - - 'a': 'add', - 'c': 'complete', - 'done': 'complete', - 'd': 'complete', - - 'start': 'create', - 'stop': 'end', - 'roll': 'reroll', - - 'add': 'add', - 'remove': 'remove', - 'list': 'list', -}; - -/** - * Resolve a command alias to its full command name - * @param {string} commandName - The command name (could be an alias) - * @returns {string} - The full command name, or the original if not an alias - */ -export function resolveCommandAlias(commandName) { - const normalized = commandName.toLowerCase(); - return commandAliases[normalized] || commandName; -} - -/** - * Resolve a subcommand alias to its full subcommand name - * @param {string} subcommandName - The subcommand name (could be an alias) - * @returns {string} - The full subcommand name, or the original if not an alias - */ -export function resolveSubcommandAlias(subcommandName) { - const normalized = subcommandName.toLowerCase(); - return subcommandAliases[normalized] || subcommandName; -} diff --git a/src/config/commandCategories.js b/src/config/commandCategories.js deleted file mode 100644 index 3d3ec1b5ee..0000000000 --- a/src/config/commandCategories.js +++ /dev/null @@ -1,46 +0,0 @@ -/** - * Command category metadata for the command access manager. - */ - -export const CATEGORY_ICONS = { - Birthday: '🎂', - Community: '👥', - Core: 'ℹ️', - Economy: '💰', - Fun: '🎮', - Giveaway: '🎉', - JoinToCreate: '🔌', - Leveling: '📊', - Logging: '📝', - Moderation: '🛡️', - Music: '🎵', - Reaction_roles: '🎭', - Search: '🔍', - ServerStats: '📈', - Ticket: '🎫', - Tools: '🛠️', - Utility: '🔧', - Verification: '✅', - Welcome: '👋', -}; - -/** Commands that always stay available so admins can recover access. */ -export const PROTECTED_COMMANDS = new Set(['commands', 'configwizard']); - -export function normalizeCategoryKey(category) { - return String(category || '') - .trim() - .toLowerCase() - .replace(/\s+/g, '_'); -} - -export function formatCategoryName(rawCategory) { - return String(rawCategory || '') - .replace(/_/g, ' ') - .replace(/([a-z])([A-Z])/g, '$1 $2') - .replace(/\b\w/g, (char) => char.toUpperCase()); -} - -export function getCategoryIcon(category) { - return CATEGORY_ICONS[category] || CATEGORY_ICONS[formatCategoryName(category)] || '📁'; -} diff --git a/src/config/lavalink.js b/src/config/lavalink.js deleted file mode 100644 index 8121828a8b..0000000000 --- a/src/config/lavalink.js +++ /dev/null @@ -1,48 +0,0 @@ -function parseBoolean(value, defaultValue = false) { - if (value === undefined || value === null || value === '') { - return defaultValue; - } - return ['true', '1', 'yes'].includes(String(value).toLowerCase()); -} - -function parseNodesFromEnv() { - const raw = process.env.LAVALINK_NODES?.trim(); - if (!raw) { - return null; - } - - try { - const parsed = JSON.parse(raw); - return Array.isArray(parsed) ? parsed : null; - } catch { - return null; - } -} - -export function getLavalinkNodes() { - const fromJson = parseNodesFromEnv(); - if (fromJson?.length) { - return fromJson; - } - - const host = process.env.LAVALINK_HOST || 'localhost'; - const port = Number(process.env.LAVALINK_PORT || 2333); - const password = process.env.LAVALINK_PASSWORD || 'youshallnotpass'; - const secure = parseBoolean(process.env.LAVALINK_SECURE, false); - - return [{ - host, - port, - password, - secure, - name: process.env.LAVALINK_NAME || 'Main', - }]; -} - -export const lavalinkConfig = { - nodes: getLavalinkNodes(), - defaultSearchPlatform: process.env.LAVALINK_SEARCH_PLATFORM || 'ytmsearch', - restVersion: process.env.LAVALINK_REST_VERSION || 'v4', -}; - -export default lavalinkConfig; diff --git a/src/config/postgres.js b/src/config/postgres.js deleted file mode 100644 index 06ceee0fb2..0000000000 --- a/src/config/postgres.js +++ /dev/null @@ -1,196 +0,0 @@ -import { assertAllowlistedIdentifier } from '../utils/sqlIdentifiers.js'; -import { EXPECTED_SCHEMA_LABEL, EXPECTED_SCHEMA_VERSION } from './schemaVersion.js'; - -const configuredTables = { - guilds: 'guilds', - users: 'users', - guild_users: 'guild_users', - birthdays: 'birthdays', - giveaways: 'giveaways', - tickets: 'ticket_data', - afk_status: 'afk_status', - welcome_configs: 'welcome_configs', - leveling_configs: 'leveling_configs', - user_levels: 'user_levels', - economy: 'economy', - invite_tracking: 'invite_tracking', - application_roles: 'application_roles', - verification_audit: 'verification_audit', - temp_data: 'temp_data', - cache_data: 'cache_data', -}; - -const allowedTableIdentifiers = new Set([ - 'guilds', - 'users', - 'guild_users', - 'birthdays', - 'giveaways', - 'ticket_data', - 'afk_status', - 'welcome_configs', - 'leveling_configs', - 'user_levels', - 'economy', - 'invite_tracking', - 'application_roles', - 'verification_audit', - 'temp_data', - 'cache_data', -]); - -const validatedTables = Object.fromEntries( - Object.entries(configuredTables).map(([key, value]) => [ - key, - assertAllowlistedIdentifier(value, allowedTableIdentifiers, `PostgreSQL table identifier (${key})`), - ]) -); - -const DEFAULT_POSTGRES_URL = 'postgresql://localhost:5432/titanbot'; - -export function resolveSslConfig() { - const sslEnv = process.env.POSTGRES_SSL?.toLowerCase(); - if (sslEnv === 'false' || sslEnv === '0') { - return false; - } - if (sslEnv === 'true' || sslEnv === '1') { - return { rejectUnauthorized: false }; - } - - const url = process.env.POSTGRES_URL || process.env.DATABASE_URL || ''; - if (/sslmode=(require|verify-ca|verify-full|prefer)/i.test(url)) { - return { rejectUnauthorized: false }; - } - - if (process.env.RAILWAY_ENVIRONMENT || process.env.RAILWAY_PROJECT_ID) { - return { rejectUnauthorized: false }; - } - - if (process.env.NODE_ENV === 'production') { - return { rejectUnauthorized: false }; - } - - return false; -} - -export function resolvePostgresPoolConfig() { - const ssl = resolveSslConfig(); - const url = (process.env.POSTGRES_URL || process.env.DATABASE_URL || '').trim(); - const sharedOptions = { - max: parseInt(process.env.POSTGRES_MAX_CONNECTIONS) || 20, - min: parseInt(process.env.POSTGRES_MIN_CONNECTIONS) || 2, - idleTimeoutMillis: parseInt(process.env.POSTGRES_IDLE_TIMEOUT) || 30000, - connectionTimeoutMillis: parseInt(process.env.POSTGRES_CONNECTION_TIMEOUT) || 10000, - application_name: 'titanbot', - statement_timeout: process.env.NODE_ENV === 'production' ? 30000 : 0, - keepalives: 1, - keepalives_idle: 30, - ssl, - }; - - if (url && url !== DEFAULT_POSTGRES_URL) { - return { connectionString: url, ...sharedOptions }; - } - - return { - host: process.env.POSTGRES_HOST || 'localhost', - port: parseInt(process.env.POSTGRES_PORT) || 5432, - database: process.env.POSTGRES_DB || 'titanbot', - user: process.env.POSTGRES_USER || 'postgres', - password: (process.env.POSTGRES_PASSWORD || '').toString(), - ...sharedOptions, - }; -} - -export const pgConfig = { - url: process.env.POSTGRES_URL || process.env.DATABASE_URL || DEFAULT_POSTGRES_URL, - - options: { - - host: process.env.POSTGRES_HOST || 'localhost', - port: parseInt(process.env.POSTGRES_PORT) || 5432, - database: process.env.POSTGRES_DB || 'titanbot', - user: process.env.POSTGRES_USER || 'postgres', - password: (process.env.POSTGRES_PASSWORD || '').toString(), - ssl: resolveSslConfig(), - - max: parseInt(process.env.POSTGRES_MAX_CONNECTIONS) || 20, - min: parseInt(process.env.POSTGRES_MIN_CONNECTIONS) || 2, - idleTimeoutMillis: parseInt(process.env.POSTGRES_IDLE_TIMEOUT) || 30000, - connectionTimeoutMillis: parseInt(process.env.POSTGRES_CONNECTION_TIMEOUT) || 10000, - - application_name: 'titanbot', - statement_timeout: process.env.NODE_ENV === 'production' ? 30000 : 0, - keepalives: 1, - keepalives_idle: 30, - - retries: parseInt(process.env.POSTGRES_RETRIES) || 3, - backoffBase: parseInt(process.env.POSTGRES_BACKOFF_BASE) || 100, - backoffMultiplier: parseInt(process.env.POSTGRES_BACKOFF_MULTIPLIER) || 2, - }, - - tables: validatedTables, - - defaultTTL: { - userSession: 86400, - - temp: 3600, - - cache: 1800, - - guildConfig: null, - - economy: null, - - leveling: null, - - giveaway: null, - - ticket: 604800, - - afk: 86400, - - welcome: null, - - birthday: null, - }, - - features: { - pooling: true, - ssl: process.env.NODE_ENV === 'production', - - metrics: true, - - debug: process.env.NODE_ENV === 'development', - - autoCreateTables: true, - - autoMigrate: process.env.AUTO_MIGRATE !== 'false', - }, - - healthCheck: { - enabled: true, - - interval: 30000, - - maxFailures: 3, - - query: 'SELECT 1', - }, - - migration: { - enabled: true, - - table: 'schema_migrations', - - directory: 'database/migrations', - - rollbackOnFailure: false, - - expectedVersion: EXPECTED_SCHEMA_VERSION, - - expectedLabel: EXPECTED_SCHEMA_LABEL, - } -}; - -export default pgConfig; \ No newline at end of file diff --git a/src/config/prefixRestrictions.js b/src/config/prefixRestrictions.js deleted file mode 100644 index 2b57f7bd60..0000000000 --- a/src/config/prefixRestrictions.js +++ /dev/null @@ -1,131 +0,0 @@ -/** - * Prefix command restrictions — dashboard and advanced setup flows stay slash-only. - */ - -/** Top-level commands that cannot be invoked via prefix at all. */ -export const SLASH_ONLY_COMMANDS = new Set([ - 'configwizard', - 'help', - 'embedbuilder', - 'wipedata', - 'apply', -]); - -/** Subcommands blocked for every command when invoked via prefix. */ -export const GLOBAL_BLOCKED_SUBCOMMANDS = new Set([ - 'dashboard', - 'setup', -]); - -/** Subcommand groups blocked for every command when invoked via prefix. */ -export const GLOBAL_BLOCKED_SUBCOMMAND_GROUPS = new Set([ - 'config', -]); - -/** Per-command subcommands that stay slash-only (beyond the global block list). */ -export const COMMAND_BLOCKED_SUBCOMMANDS = { - music: new Set([ - 'shuffle', - 'loop', - 'seek', - 'remove', - 'move', - 'clear', - '247', - ]), - birthday: new Set(['setchannel']), - report: new Set(['setchannel']), -}; - -function collectSubcommandNames(commandJson) { - const subcommandGroup = commandJson.options?.find((opt) => opt.type === 2); - - if (subcommandGroup) { - const names = []; - for (const group of subcommandGroup.options || []) { - names.push(...(group.options?.map((opt) => opt.name) || [])); - } - return names; - } - - return (commandJson.options?.filter((opt) => opt.type === 1) || []).map((sub) => sub.name); -} - -function isSubcommandBlocked(commandName, subcommandName) { - if (!subcommandName) { - return false; - } - - if (GLOBAL_BLOCKED_SUBCOMMANDS.has(subcommandName)) { - return true; - } - - const commandBlocked = COMMAND_BLOCKED_SUBCOMMANDS[commandName]; - return commandBlocked?.has(subcommandName) ?? false; -} - -/** - * Returns whether a prefix invocation should be rejected. - * @param {object} command - Loaded command module - * @param {string[]} args - Parsed prefix arguments (after command name) - * @param {(name: string) => string} resolveSubcommandAlias - * @returns {{ blocked: boolean, reason?: string }} - */ -export function getPrefixRestriction(command, args, resolveSubcommandAlias) { - if (!command?.data?.toJSON) { - return { blocked: false }; - } - - const commandJson = command.data.toJSON(); - const commandName = commandJson.name?.toLowerCase(); - - if (command.prefixOnly === false || command.slashOnly === true) { - return { blocked: true, reason: 'This command is only available as a slash command.' }; - } - - if (SLASH_ONLY_COMMANDS.has(commandName)) { - return { blocked: true, reason: 'This command is only available as a slash command.' }; - } - - const [firstArg, secondArg] = args.map((arg) => arg?.toLowerCase?.() || null); - const resolvedFirstArg = firstArg ? resolveSubcommandAlias(firstArg) : null; - const resolvedSecondArg = secondArg ? resolveSubcommandAlias(secondArg) : null; - - const subcommandGroup = commandJson.options?.find((opt) => opt.type === 2); - - const allSubcommandNames = collectSubcommandNames(commandJson); - const allSubcommandsBlocked = - allSubcommandNames.length > 0 && - allSubcommandNames.every((name) => isSubcommandBlocked(commandName, name)); - - if (allSubcommandsBlocked) { - return { blocked: true, reason: 'This command is only available as a slash command.' }; - } - - if (firstArg && GLOBAL_BLOCKED_SUBCOMMAND_GROUPS.has(firstArg)) { - return { - blocked: true, - reason: 'This configuration flow is only available as a slash command.', - }; - } - - if (resolvedFirstArg && isSubcommandBlocked(commandName, resolvedFirstArg)) { - return { - blocked: true, - reason: 'This subcommand is only available as a slash command.', - }; - } - - if (subcommandGroup && resolvedSecondArg && isSubcommandBlocked(commandName, resolvedSecondArg)) { - return { - blocked: true, - reason: 'This subcommand is only available as a slash command.', - }; - } - - return { blocked: false }; -} - -export function isPrefixRestrictedCommand(command, args, resolveSubcommandAlias) { - return getPrefixRestriction(command, args, resolveSubcommandAlias).blocked; -} diff --git a/src/config/schemaVersion.js b/src/config/schemaVersion.js deleted file mode 100644 index 36c35b6a4c..0000000000 --- a/src/config/schemaVersion.js +++ /dev/null @@ -1,7 +0,0 @@ -const rawSchemaVersion = Number.parseInt(process.env.SCHEMA_VERSION || '1', 10); - -export const EXPECTED_SCHEMA_VERSION = Number.isInteger(rawSchemaVersion) && rawSchemaVersion > 0 - ? rawSchemaVersion - : 1; - -export const EXPECTED_SCHEMA_LABEL = process.env.SCHEMA_VERSION_LABEL || `baseline-v${EXPECTED_SCHEMA_VERSION}`; \ No newline at end of file diff --git a/src/events/guildCreate.js b/src/events/guildCreate.js index 44bc135db1..7972714a59 100644 --- a/src/events/guildCreate.js +++ b/src/events/guildCreate.js @@ -1,10 +1,6 @@ import { Events } from 'discord.js'; import { logger } from '../utils/logger.js'; -<<<<<<< HEAD -import { getGuildConfig, setGuildConfig } from '../services/guildConfig.js'; -======= import { getGuildConfig, setGuildConfig } from '../services/config/guildConfig.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) export default { name: Events.GuildCreate, diff --git a/src/events/guildMemberAdd.js b/src/events/guildMemberAdd.js index c9f5aacbb1..cb77b477b4 100644 --- a/src/events/guildMemberAdd.js +++ b/src/events/guildMemberAdd.js @@ -1,11 +1,6 @@ import { Events, EmbedBuilder, PermissionFlagsBits } from 'discord.js'; -<<<<<<< HEAD -import { getColor } from '../config/bot.js'; -import { getGuildConfig } from '../services/guildConfig.js'; -======= import { getColor, botConfig } from '../config/bot.js'; import { getGuildConfig } from '../services/config/guildConfig.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) import { getWelcomeConfig } from '../utils/database.js'; import { formatWelcomeMessage } from '../utils/welcome.js'; import { logEvent, EVENT_TYPES } from '../services/loggingService.js'; @@ -29,18 +24,6 @@ export default { if (welcomeConfig?.enabled && welcomeChannelId) { const channel = guild.channels.cache.get(welcomeChannelId); -<<<<<<< HEAD - if (channel?.isTextBased?.()) { - const me = guild.members.me; - const permissions = me ? channel.permissionsFor(me) : null; - if (!permissions?.has([PermissionFlagsBits.ViewChannel, PermissionFlagsBits.SendMessages])) { - return; - } - - const formatData = { user, guild, member }; - const welcomeMessage = formatWelcomeMessage( - welcomeConfig.welcomeMessage || welcomeConfig.welcomeEmbed?.description || 'Welcome {user} to {server}!', -======= const me = guild.members.me; const permissions = channel?.isTextBased?.() && me ? channel.permissionsFor(me) : null; // Skip only the welcome message if permissions are missing; the rest of the @@ -49,7 +32,6 @@ export default { const formatData = { user, guild, member }; const welcomeMessage = formatWelcomeMessage( welcomeConfig.welcomeMessage || welcomeConfig.welcomeEmbed?.description || botConfig.welcome?.defaultWelcomeMessage || 'Welcome {user} to {server}!', ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) formatData ); diff --git a/src/events/guildMemberRemove.js b/src/events/guildMemberRemove.js index 72752b1e81..a550082635 100644 --- a/src/events/guildMemberRemove.js +++ b/src/events/guildMemberRemove.js @@ -1,19 +1,11 @@ import { Events, EmbedBuilder, PermissionFlagsBits } from 'discord.js'; -<<<<<<< HEAD -import { getColor } from '../config/bot.js'; -======= import { getColor, botConfig } from '../config/bot.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) import { getWelcomeConfig, getUserApplications, deleteApplication } from '../utils/database.js'; import { formatWelcomeMessage } from '../utils/welcome.js'; import { logEvent, EVENT_TYPES } from '../services/loggingService.js'; import { getServerCounters, updateCounter } from '../services/serverstatsService.js'; import { getGuildBirthdays, deleteBirthday } from '../utils/database.js'; -<<<<<<< HEAD -import { deleteUserLevelData } from '../services/leveling.js'; -======= import { deleteUserLevelData } from '../services/leveling/leveling.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) import { logger } from '../utils/logger.js'; export default { @@ -39,11 +31,7 @@ export default { const formatData = { user, guild, member }; const goodbyeMessage = formatWelcomeMessage( -<<<<<<< HEAD - welcomeConfig.leaveMessage || welcomeConfig.leaveEmbed?.description || '{user.tag} has left the server.', -======= welcomeConfig.leaveMessage || welcomeConfig.leaveEmbed?.description || botConfig.welcome?.defaultGoodbyeMessage || '{user} has left the server.', ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) formatData ); diff --git a/src/events/interactionCreate.js b/src/events/interactionCreate.js index 0d577f9cbb..50a468b73e 100644 --- a/src/events/interactionCreate.js +++ b/src/events/interactionCreate.js @@ -1,11 +1,5 @@ import { Events, MessageFlags } from 'discord.js'; import { logger } from '../utils/logger.js'; -<<<<<<< HEAD -import { getGuildConfig } from '../services/guildConfig.js'; -import { handleApplicationModal } from '../commands/Community/apply.js'; -import { handleApplicationReviewModal } from '../commands/Community/app-admin.js'; -import { handleInteractionError, createError, ErrorTypes } from '../utils/errorHandler.js'; -======= import { getGuildConfig } from '../services/config/guildConfig.js'; import { getBotMessage, @@ -16,7 +10,6 @@ import { import botConfig from '../config/bot.js'; import { handleApplicationModal } from '../commands/Community/apply.js'; import { handleInteractionError, createError, ErrorTypes, ErrorCodes } from '../utils/errorHandler.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) import { InteractionHelper } from '../utils/interactionHelper.js'; import { createInteractionTraceContext, runWithTraceContext } from '../utils/logger.js'; import { validateChatInputPayloadOrThrow } from '../utils/commandInputValidation.js'; @@ -27,8 +20,6 @@ import { isCollectorManagedComponent } from '../utils/collectorComponents.js'; import { ResponseCoordinator } from '../utils/responseCoordinator.js'; import { enforceDefaultCommandPermissions } from '../utils/permissionGuard.js'; -<<<<<<< HEAD -======= const COMMAND_ERROR_SUBTYPES = { warn: 'warn_failed', kick: 'kick_failed', @@ -45,7 +36,6 @@ const COMMAND_ERROR_SUBTYPES = { greroll: 'giveaway_failed', }; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) function withTraceContext(context = {}, traceContext = {}) { return { traceId: traceContext.traceId, @@ -94,8 +84,6 @@ export default { ); } -<<<<<<< HEAD -======= if (isMaintenanceMode() && !isBotOwner(interaction.user.id)) { throw createError( 'Bot is in maintenance mode', @@ -132,7 +120,6 @@ export default { client.cooldowns.set(cooldownKey, Date.now() + defaultCooldownSec * 1000); } ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) const abuseProtection = await enforceAbuseProtection(interaction, command, interaction.commandName); if (!abuseProtection.allowed) { const formattedCooldown = formatCooldownDuration(abuseProtection.remainingMs); @@ -177,12 +164,8 @@ export default { } catch (error) { await handleInteractionError(interaction, error, withTraceContext({ type: 'command', -<<<<<<< HEAD - commandName: interaction.commandName -======= commandName: interaction.commandName, subtype: COMMAND_ERROR_SUBTYPES[interaction.commandName] || error?.context?.subtype, ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) }, interactionTraceContext)); } } else if (interaction.isAutocomplete()) { @@ -415,27 +398,9 @@ export default { return; } -<<<<<<< HEAD - if (interaction.customId.startsWith('app_review_')) { - try { - await handleApplicationReviewModal(interaction); - } catch (error) { - await handleInteractionError(interaction, error, withTraceContext({ - type: 'modal', - customId: interaction.customId, - handler: 'application_review' - }, interactionTraceContext)); - } - return; - } - - if ( - interaction.customId.startsWith('jtc_') -======= if ( interaction.customId.startsWith('app_review_') || interaction.customId.startsWith('jtc_') ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) || interaction.customId.startsWith('config_wizard_modal:') || interaction.customId.startsWith('log_dash_channel_modal:') || interaction.customId.startsWith('log_dash_filter_modal:') @@ -477,11 +442,7 @@ export default { } catch (error) { logger.error('Unhandled error in interactionCreate:', { event: 'interaction.unhandled_error', -<<<<<<< HEAD - errorCode: 'INTERACTION_UNHANDLED_ERROR', -======= errorCode: ErrorCodes.INTERACTION_UNHANDLED, ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) error, traceId: interactionTraceContext.traceId, interactionId: interaction.id, @@ -499,11 +460,7 @@ export default { } catch (replyError) { logger.error('Failed to send fallback error response:', { event: 'interaction.error_response_failed', -<<<<<<< HEAD - errorCode: 'INTERACTION_ERROR_RESPONSE_FAILED', -======= errorCode: ErrorCodes.INTERACTION_RESPONSE_FAILED, ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) error: replyError, traceId: interactionTraceContext.traceId }); diff --git a/src/events/messageCreate.js b/src/events/messageCreate.js index 8d1e2dab0b..fb1a165f31 100644 --- a/src/events/messageCreate.js +++ b/src/events/messageCreate.js @@ -1,15 +1,5 @@ import { Events } from 'discord.js'; import { logger } from '../utils/logger.js'; -<<<<<<< HEAD -import { getLevelingConfig, getUserLevelData } from '../services/leveling.js'; -import { addXp } from '../services/xpSystem.js'; -import { checkRateLimit } from '../utils/rateLimiter.js'; -import { parsePrefixCommand } from '../utils/prefixParser.js'; -import { supportsPrefixExecution, executePrefixCommand, resolvePrefixAccessKey } from '../utils/messageAdapter.js'; -import { resolveCommandAlias, resolveSubcommandAlias } from '../config/commandAliases.js'; -import { getPrefixRestriction } from '../config/prefixRestrictions.js'; -import { getGuildConfig } from '../services/guildConfig.js'; -======= import { getLevelingConfig, getUserLevelData } from '../services/leveling/leveling.js'; import { addXp } from '../services/leveling/xpSystem.js'; import { checkRateLimit } from '../utils/rateLimiter.js'; @@ -19,7 +9,6 @@ import { resolveCommandAlias, resolveSubcommandAlias } from '../config/commands/ import { getPrefixRestriction } from '../config/commands/prefixRestrictions.js'; import { getGuildConfig } from '../services/config/guildConfig.js'; import { getCommandPrefix, getBotMessage, isBotOwner, isCommandCategoryEnabled, isMaintenanceMode } from '../config/bot.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) import { enforceAbuseProtection, formatCooldownDuration } from '../utils/abuseProtection.js'; import { createEmbed } from '../utils/embeds.js'; import { isCommandEnabled } from '../services/commandAccessService.js'; @@ -58,11 +47,7 @@ export default { async function handlePrefixCommand(message, client) { try { const guildConfig = await getGuildConfig(client, message.guild.id); -<<<<<<< HEAD - const prefix = guildConfig?.prefix || client.config.bot.prefix || '!'; -======= const prefix = guildConfig?.prefix || getCommandPrefix(); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) const parsed = parsePrefixCommand(message.content, prefix); if (!parsed) { @@ -88,8 +73,6 @@ async function handlePrefixCommand(message, client) { return; } -<<<<<<< HEAD -======= if (isMaintenanceMode() && !isBotOwner(message.author.id)) { await message.channel.send({ embeds: [createEmbed({ @@ -112,7 +95,6 @@ async function handlePrefixCommand(message, client) { return; } ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) const restriction = getPrefixRestriction(command, args, resolveSubcommandAlias); if (!supportsPrefixExecution(command) || restriction.blocked) { if (restriction.blocked && restriction.reason) { @@ -259,13 +241,8 @@ async function handleLeveling(message, client) { } const result = await addXp(client, message.guild, message.member, finalXP); -<<<<<<< HEAD - - if (result.success && result.leveledUp) { -======= if (result?.leveledUp) { ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) logger.info( `${message.author.tag} leveled up to level ${result.level} in ${message.guild.name}` ); diff --git a/src/events/messageDelete.js b/src/events/messageDelete.js index 1d6b1f4361..54d7a9c1db 100644 --- a/src/events/messageDelete.js +++ b/src/events/messageDelete.js @@ -2,11 +2,7 @@ import { Events } from 'discord.js'; import { logEvent, EVENT_TYPES } from '../services/loggingService.js'; import { logger } from '../utils/logger.js'; import { getReactionRoleMessage, deleteReactionRoleMessage } from '../services/reactionRoleService.js'; -<<<<<<< HEAD -import { formatLogLine } from '../utils/logEmbeds.js'; -======= import { formatLogLine } from '../utils/logging/logEmbeds.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) const MAX_LOGGED_MESSAGE_CONTENT_LENGTH = 1024; diff --git a/src/events/messageUpdate.js b/src/events/messageUpdate.js index 3c457e3ffa..18761fd553 100644 --- a/src/events/messageUpdate.js +++ b/src/events/messageUpdate.js @@ -1,11 +1,7 @@ import { Events } from 'discord.js'; import { logEvent, EVENT_TYPES } from '../services/loggingService.js'; import { logger } from '../utils/logger.js'; -<<<<<<< HEAD -import { formatLogLine } from '../utils/logEmbeds.js'; -======= import { formatLogLine } from '../utils/logging/logEmbeds.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) const MAX_LOGGED_EDIT_CONTENT_LENGTH = 512; diff --git a/src/events/ready.js b/src/events/ready.js index 2006298ee2..2be8e5e424 100644 --- a/src/events/ready.js +++ b/src/events/ready.js @@ -3,11 +3,7 @@ import { logger, startupLog } from "../utils/logger.js"; import config from "../config/application.js"; import { reconcileReactionRoleMessages } from "../services/reactionRoleService.js"; import { reconcileTicketPanels, reconcileVerificationPanels, reconcileReactionRolePanelHealth } from "../services/panelHealthService.js"; -<<<<<<< HEAD -import { reconcileLevelRoles } from "../services/levelRoleSyncService.js"; -======= import { reconcileLevelRoles } from "../services/leveling/levelRoleSyncService.js"; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) import { initRiffyAfterReady } from "../services/music/riffySetup.js"; export default { diff --git a/src/events/roleCreate.js b/src/events/roleCreate.js index 81af81e650..1625974db1 100644 --- a/src/events/roleCreate.js +++ b/src/events/roleCreate.js @@ -1,11 +1,7 @@ import { Events } from 'discord.js'; import { logEvent, EVENT_TYPES } from '../services/loggingService.js'; import { logger } from '../utils/logger.js'; -<<<<<<< HEAD -import { buildRoleAuditLines } from '../utils/logEmbeds.js'; -======= import { buildRoleAuditLines } from '../utils/logging/logEmbeds.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) export default { name: Events.GuildRoleCreate, diff --git a/src/events/roleDelete.js b/src/events/roleDelete.js index 3911fd7f4c..1e16b23dc8 100644 --- a/src/events/roleDelete.js +++ b/src/events/roleDelete.js @@ -1,11 +1,7 @@ import { Events } from 'discord.js'; import { logEvent, EVENT_TYPES } from '../services/loggingService.js'; import { logger } from '../utils/logger.js'; -<<<<<<< HEAD -import { buildRoleAuditLines } from '../utils/logEmbeds.js'; -======= import { buildRoleAuditLines } from '../utils/logging/logEmbeds.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) export default { name: Events.GuildRoleDelete, diff --git a/src/handlers/calculateModals.js b/src/handlers/calculateModals.js index f459ccc2f7..d83d013261 100644 --- a/src/handlers/calculateModals.js +++ b/src/handlers/calculateModals.js @@ -2,10 +2,7 @@ import { successEmbed } from '../utils/embeds.js'; import { logger } from '../utils/logger.js'; import { evaluateMathExpression } from '../utils/safeMathParser.js'; -<<<<<<< HEAD -======= import { replyUserError, ErrorTypes } from '../utils/errorHandler.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) function evaluate(expression) { return evaluateMathExpression(expression); } diff --git a/src/handlers/commandLoader.js b/src/handlers/commandLoader.js deleted file mode 100644 index 7a7d2baa34..0000000000 --- a/src/handlers/commandLoader.js +++ /dev/null @@ -1,360 +0,0 @@ -import fs from 'fs/promises'; -import path from 'path'; -import { fileURLToPath, pathToFileURL } from 'url'; -import { Collection } from 'discord.js'; -import { logger } from '../utils/logger.js'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); -const MAX_COMMANDS = 100; -const COMMAND_COUNT_WARN_THRESHOLD = 90; - -function getSubcommandInfo(commandData) { - const subcommands = []; - - if (commandData.options) { - for (const option of commandData.options) { -if (option.type === 1) { - subcommands.push(option.name); -} else if (option.type === 2) { - if (option.options) { - for (const subOption of option.options) { -if (subOption.type === 1) { - subcommands.push(`${option.name}/${subOption.name}`); - } - } - } - } - } - } - - return subcommands; -} - -async function getAllFiles(directory, fileList = []) { - const files = await fs.readdir(directory, { withFileTypes: true }); - - for (const file of files) { - const filePath = path.join(directory, file.name); - - if (file.isDirectory()) { - if (file.name === 'modules') { - continue; - } - await getAllFiles(filePath, fileList); - } else if (file.name.endsWith('.js')) { - fileList.push(filePath); - } - } - - return fileList; -} - -export async function loadCommands(client) { - client.commands = new Collection(); - const commandsPath = path.join(__dirname, '../commands'); - const commandFiles = await getAllFiles(commandsPath); - - logger.info(`Found ${commandFiles.length} command files to load`); - - const uniqueCommandNames = new Set(); - - for (const filePath of commandFiles) { - try { - const normalizedPath = filePath.replace(/\\/g, '/'); - - const commandName = path.basename(filePath, '.js'); - const commandDir = path.dirname(filePath); - const category = path.basename(commandDir); - - const commandModule = await import(`file://${filePath}`); - const command = commandModule.default || commandModule; - - if (!command.data || !command.execute) { - logger.warn(`Command at ${filePath} is missing required "data" or "execute" property.`); - continue; - } - - command.category = category; - command.filePath = normalizedPath; - - const primaryCommandName = command.data.name; - - if (!uniqueCommandNames.has(primaryCommandName)) { - uniqueCommandNames.add(primaryCommandName); - - client.commands.set(primaryCommandName, command); - } - - const subcommands = getSubcommandInfo(command.data.toJSON()); - - logger.info(`Loaded command: ${primaryCommandName} from ${normalizedPath} (category: ${category})`); - - if (subcommands.length > 0) { - logger.info(` - Subcommands: ${subcommands.join(', ')}`); - } - - } catch (error) { - logger.error(`Error loading command from ${filePath}:`, error); - } - } - - const commandsWithSubcommands = Array.from(client.commands.values()).filter(cmd => { - const subcommands = getSubcommandInfo(cmd.data.toJSON()); - return subcommands.length > 0; - }); - - const totalSubcommands = commandsWithSubcommands.reduce((total, cmd) => { - return total + getSubcommandInfo(cmd.data.toJSON()).length; - }, 0); - - const uniqueCommands = new Set(); - for (const [name, command] of client.commands.entries()) { - if (command.data && command.data.name) { - uniqueCommands.add(command.data.name); - } - } - - logger.info(`Loaded ${uniqueCommands.size} commands`); - return client.commands; -} - -function collectCommandPayloads(client) { - const commands = []; - let totalSubcommands = 0; - const registeredNames = new Set(); - - for (const command of client.commands.values()) { - if (!command.data || typeof command.data.toJSON !== 'function') { - logger.warn(`Command missing data or toJSON method: ${command}`); - continue; - } - - const commandName = command.data.name; - logger.debug(`Processing command for registration: ${commandName}`); - - if (registeredNames.has(commandName)) { - logger.debug(`Skipping duplicate command: ${commandName}`); - continue; - } - - registeredNames.add(commandName); - const commandJson = command.data.toJSON(); - commands.push(commandJson); - totalSubcommands += getSubcommandInfo(commandJson).length; - - if (process.env.NODE_ENV !== 'production') { - logger.debug(`Registering command: ${commandName}`); - } - } - - return { commands, totalSubcommands }; -} - -function validateCommands(commands) { - const validationErrors = []; - - for (const cmd of commands) { - if (cmd.name && cmd.name.length > 32) { - validationErrors.push(`Command ${cmd.name} has name longer than 32 chars: "${cmd.name}" (${cmd.name.length} chars)`); - } - if (cmd.description && cmd.description.length > 110) { - validationErrors.push(`Command ${cmd.name} has description longer than 110 chars: "${cmd.description}" (${cmd.description.length} chars)`); - } - - if (!cmd.options) { - continue; - } - - for (const option of cmd.options) { - if (option.name && option.name.length > 32) { - validationErrors.push(`Command ${cmd.name} option ${option.name} has name longer than 32 chars: "${option.name}" (${option.name.length} chars)`); - } - if (option.description && option.description.length > 110) { - validationErrors.push(`Command ${cmd.name} option ${option.name} has description longer than 110 chars: "${option.description}" (${option.description.length} chars)`); - } - - if (option.choices) { - for (const choice of option.choices) { - if (choice.name && choice.name.length > 110) { - validationErrors.push(`Command ${cmd.name} option ${option.name} choice ${choice.name} has name longer than 110 chars: "${choice.name}" (${choice.name.length} chars)`); - } - if (choice.value && choice.value.length > 100) { - validationErrors.push(`Command ${cmd.name} option ${option.name} choice ${choice.name} has value longer than 100 chars: "${choice.value}" (${choice.value.length} chars)`); - } - } - } - - if (!option.options) { - continue; - } - - for (const subOption of option.options) { - if (subOption.name && subOption.name.length > 32) { - validationErrors.push(`Command ${cmd.name} subcommand ${option.name} option ${subOption.name} has name longer than 32 chars: "${subOption.name}" (${subOption.name.length} chars)`); - } - if (subOption.description && subOption.description.length > 110) { - validationErrors.push(`Command ${cmd.name} subcommand ${option.name} option ${subOption.name} has description longer than 110 chars: "${subOption.description}" (${subOption.description.length} chars)`); - } - - if (!subOption.choices) { - continue; - } - - for (const choice of subOption.choices) { - if (choice.name && choice.name.length > 110) { - validationErrors.push(`Command ${cmd.name} subcommand ${option.name} option ${subOption.name} choice ${choice.name} has name longer than 110 chars: "${choice.name}" (${choice.name.length} chars)`); - } - if (choice.value && choice.value.length > 100) { - validationErrors.push(`Command ${cmd.name} subcommand ${option.name} option ${subOption.name} choice ${choice.name} has value longer than 100 chars: "${choice.value}" (${choice.value.length} chars)`); - } - } - } - } - } - - if (validationErrors.length > 0) { - logger.error('Command validation failed. Errors:'); - validationErrors.forEach((error) => logger.error(` - ${error}`)); - throw new Error(`Command validation failed with ${validationErrors.length} errors`); - } -} - -function prepareCommandsForRegistration(commands, { multiGuild = false } = {}) { - if (commands.length >= COMMAND_COUNT_WARN_THRESHOLD) { - logger.warn( - `Command count (${commands.length}) is near Discord's ${MAX_COMMANDS} limit` + - (multiGuild ? ' for global registration' : ' for guild registration') - ); - } - - if (commands.length <= MAX_COMMANDS) { - return commands; - } - - logger.warn(`Command count (${commands.length}) exceeds Discord limit (${MAX_COMMANDS}), truncating...`); - const truncated = commands.slice(0, MAX_COMMANDS); - logger.info(`Truncated to ${truncated.length} commands for registration`); - return truncated; -} - -async function registerGlobalCommands(client, clientId, commands, totalSubcommands) { - if (!clientId) { - throw new Error('CLIENT_ID is required for global command registration when MULTI_GUILD=true'); - } - - if (!client.rest) { - throw new Error('Discord REST client is not available for global command registration'); - } - - logger.info(`Preparing to register ${totalSubcommands + commands.length} commands globally (multi-guild mode)`); - logger.info('Validating commands before registration...'); - validateCommands(commands); - logger.info('Command validation passed'); - - const commandsToRegister = prepareCommandsForRegistration(commands, { multiGuild: true }); - - logger.info(`Registering ${commandsToRegister.length} global commands...`); - await client.rest.put(`/applications/${clientId}/commands`, { body: commandsToRegister }); - logger.info(`Successfully registered ${commandsToRegister.length} global commands`); - logger.info('Global commands may take up to an hour to appear in all servers on first deploy'); -} - -async function registerGuildCommands(client, guildId, commands, totalSubcommands) { - logger.info(`Preparing to register ${totalSubcommands + commands.length} commands for guild ${guildId}`); - logger.info('Validating commands before registration...'); - validateCommands(commands); - logger.info('Command validation passed'); - - const guild = await client.guilds.fetch(guildId); - const existingCommands = await guild.commands.fetch(); - logger.info(`Found ${existingCommands.size} existing guild commands`); - - const commandsToRegister = prepareCommandsForRegistration(commands, { multiGuild: false }); - - if (process.env.NODE_ENV !== 'production') { - logger.info(`Registering ${commandsToRegister.length} commands for guild ${guild.name} (${guild.id})`); - } - - try { - logger.info(`Registering ${commandsToRegister.length} guild commands...`); - await guild.commands.set(commandsToRegister); - logger.info(`Successfully registered ${commandsToRegister.length} guild commands`); - - const registeredCommands = await guild.commands.fetch(); - if (registeredCommands.size !== commandsToRegister.length) { - logger.warn(`Warning: Expected ${commandsToRegister.length} commands, but Discord reports ${registeredCommands.size} registered`); - } else { - logger.info(`Verification passed: ${registeredCommands.size} commands successfully registered`); - } - } catch (error) { - logger.error('Failed to register commands:', error); - - if (existingCommands.size > 0) { - logger.info('Attempting to restore previous commands due to registration failure...'); - try { - await guild.commands.set(existingCommands.map((cmd) => cmd)); - logger.info('Successfully restored previous commands'); - } catch (restoreError) { - logger.error('Failed to restore previous commands:', restoreError); - } - } - - throw error; - } -} - -export async function registerCommands(client, options = {}) { - const { - clientId = null, - guildId = null, - multiGuild = false, - } = options; - - try { - const { commands, totalSubcommands } = collectCommandPayloads(client); - - if (multiGuild) { - logger.info('Command registration mode: multi-guild (global commands)'); - await registerGlobalCommands(client, clientId, commands, totalSubcommands); - return; - } - - if (!guildId) { - logger.warn( - 'Command registration skipped: set GUILD_ID for single-server setup, or MULTI_GUILD=true for multi-server support' - ); - return; - } - - logger.info(`Command registration mode: single-guild (GUILD_ID=${guildId})`); - await registerGuildCommands(client, guildId, commands, totalSubcommands); - } catch (error) { - logger.error('Error registering commands:', error); - throw error; - } -} - -export async function reloadCommand(client, commandName) { - const command = client.commands.get(commandName); - - if (!command) { - return { success: false, message: `Command "${commandName}" not found` }; - } - - try { - const commandPath = path.resolve(command.filePath); - const moduleUrl = pathToFileURL(commandPath); - moduleUrl.searchParams.set('t', Date.now().toString()); - - const newCommand = (await import(moduleUrl.href)).default; - - client.commands.set(commandName, newCommand); - - logger.info(`Reloaded command: ${commandName}`); - return { success: true, message: `Successfully reloaded command "${commandName}"` }; - } catch (error) { - logger.error(`Error reloading command "${commandName}":`, error); - return { success: false, message: `Error reloading command: ${error.message}` }; - } -} \ No newline at end of file diff --git a/src/handlers/countdownButtons.js b/src/handlers/countdownButtons.js index 87d0d2b775..d8c2b2a893 100644 --- a/src/handlers/countdownButtons.js +++ b/src/handlers/countdownButtons.js @@ -1,15 +1,8 @@ -<<<<<<< HEAD -import { ActionRowBuilder, ButtonBuilder, ButtonStyle } from 'discord.js'; -import { successEmbed } from '../utils/embeds.js'; -import { logger } from '../utils/logger.js'; - -======= import { ActionRowBuilder, ButtonBuilder, ButtonStyle, PermissionFlagsBits } from 'discord.js'; import { successEmbed } from '../utils/embeds.js'; import { logger } from '../utils/logger.js'; import { replyUserError, ErrorTypes } from '../utils/errorHandler.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) function createControlButtons(countdownId, isPaused = false) { return new ActionRowBuilder().addComponents( new ButtonBuilder() @@ -120,11 +113,7 @@ async function countdownButtonHandler(interaction, client, args) { }); } -<<<<<<< HEAD - if (!interaction.member.permissions.has("MANAGE_MESSAGES")) { -======= if (!interaction.member.permissions.has(PermissionFlagsBits.ManageMessages)) { ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) return await interaction.reply({ content: 'You need the "Manage Messages" permission to control countdowns.', flags: ["Ephemeral"], diff --git a/src/handlers/counterButtons.js b/src/handlers/counterButtons.js index b6e1a5774b..fedd0b9370 100644 --- a/src/handlers/counterButtons.js +++ b/src/handlers/counterButtons.js @@ -2,11 +2,7 @@ import { MessageFlags } from 'discord.js'; import { createEmbed, successEmbed } from '../utils/embeds.js'; import { performDeletionByCounterId } from '../commands/ServerStats/modules/serverstats_delete.js'; import { logger } from '../utils/logger.js'; -<<<<<<< HEAD -import { ErrorTypes, replyUserError } from '../utils/errorHandler.js'; -======= import { ErrorTypes, replyUserError, handleInteractionError } from '../utils/errorHandler.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) export const counterDeleteActionHandler = { name: 'counter-delete', @@ -54,26 +50,6 @@ export const counterDeleteActionHandler = { return; } -<<<<<<< HEAD - const result = await performDeletionByCounterId(client, interaction.guild, counterId); - - if (!result.success) { - await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: result.message }).catch(logger.error); - return; - } - - await interaction.editReply({ - embeds: [successEmbed(result.message)], - components: [] - }).catch(logger.error); - } catch (error) { - logger.error('Error handling counter-delete button:', error); - if (!interaction.replied && !interaction.deferred) { - await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'An error occurred while processing this action.' }).catch(() => null); - } else { - await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'An error occurred while processing this action.' }).catch(() => null); - } -======= const { message } = await performDeletionByCounterId(client, interaction.guild, counterId); await interaction.editReply({ @@ -86,7 +62,6 @@ export const counterDeleteActionHandler = { handler: 'counter_delete', customId: interaction.customId, }); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } } }; diff --git a/src/handlers/events.js b/src/handlers/events.js deleted file mode 100644 index e941604f88..0000000000 --- a/src/handlers/events.js +++ /dev/null @@ -1,45 +0,0 @@ -import { readdir } from 'fs/promises'; -import { join } from 'path'; -import { fileURLToPath } from 'url'; -import { dirname } from 'path'; -import { logger } from '../utils/logger.js'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); - -export default async function loadEvents(client) { - const eventsPath = join(__dirname, '../events'); - const eventFiles = await readdir(eventsPath).then(files => files.filter(file => file.endsWith('.js'))); - - logger.info(`Found ${eventFiles.length} event files to load`); - - for (const file of eventFiles) { - const filePath = join(eventsPath, file); - try { - const { default: event } = await import(`file://${filePath}`); - - if (!event?.name || typeof event.execute !== 'function') { - logger.warn(`Event ${file} is missing required "name" or "execute" properties.`); - continue; - } - - const safeExecute = async (...args) => { - try { - await event.execute(...args, client); - } catch (error) { - logger.error(`Error executing event ${event.name}:`, error); - } - }; - - if (event.once) { - client.once(event.name, safeExecute); - logger.info(`✅ Registered once event: ${event.name}`); - } else { - client.on(event.name, safeExecute); - logger.info(`✅ Registered event: ${event.name}`); - } - } catch (error) { - logger.error(`Error loading event ${file}:`, error); - } - } -} \ No newline at end of file diff --git a/src/handlers/giveawayButtons.js b/src/handlers/giveawayButtons.js index 4290f7ed50..ab0f46603e 100644 --- a/src/handlers/giveawayButtons.js +++ b/src/handlers/giveawayButtons.js @@ -1,11 +1,7 @@ import { MessageFlags, PermissionFlagsBits } from 'discord.js'; import { successEmbed } from '../utils/embeds.js'; import { logger } from '../utils/logger.js'; -<<<<<<< HEAD -import { TitanBotError, ErrorTypes, handleInteractionError } from '../utils/errorHandler.js'; -======= import { TitanBotError, ErrorTypes, handleInteractionError, replyUserError } from '../utils/errorHandler.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) import { getGuildGiveaways, saveGiveaway, diff --git a/src/handlers/helpButtons.js b/src/handlers/helpButtons.js deleted file mode 100644 index 86f589e8c9..0000000000 --- a/src/handlers/helpButtons.js +++ /dev/null @@ -1,148 +0,0 @@ -import { createEmbed } from '../utils/embeds.js'; -import { createAllCommandsMenu } from './helpSelectMenus.js'; -import { createInitialHelpMenu } from '../commands/Core/help.js'; -import { ActionRowBuilder, ButtonBuilder, ButtonStyle, MessageFlags } from 'discord.js'; -import { logger } from '../utils/logger.js'; - -const COMMAND_LIST_ID = "help-command-list"; -const BACK_BUTTON_ID = "help-back-to-main"; -const PAGINATION_PREFIX = "help-page"; -const BUG_REPORT_BUTTON_ID = "help-bug-report"; - -export const helpBackButton = { - name: BACK_BUTTON_ID, - async execute(interaction, client) { - try { - if (!interaction.deferred && !interaction.replied) { - await interaction.deferUpdate(); - } - - const { embeds, components } = await createInitialHelpMenu(client); - await interaction.editReply({ - embeds, - components, - }); - } catch (error) { - if (error?.code === 40060 || error?.code === 10062) { - logger.warn('Help back button interaction already acknowledged or expired.', { - event: 'interaction.help.button.unavailable', - errorCode: String(error.code), - customId: interaction.customId, - interactionId: interaction.id, - }); - return; - } - - throw error; - } - }, -}; - -export const helpBugReportButton = { - name: BUG_REPORT_BUTTON_ID, - async execute(interaction, client) { - const githubButton = new ButtonBuilder() - .setLabel('🐛 Report Bug on GitHub') - .setStyle(ButtonStyle.Link) - .setURL('https://github.com/codebymitch/TitanBot/issues'); - - const bugRow = new ActionRowBuilder().addComponents(githubButton); - - const bugReportEmbed = createEmbed({ - title: '🐛 Bug Report', - description: 'Found a bug? Please report it on our GitHub Issues page!\n\n' + - '**When reporting a bug, please include:**\n' + - '• 📝 Detailed description of the issue\n' + - '• 📋 Steps to reproduce the problem\n' + - '• 📸 Screenshots if applicable\n' + - '• 💻 Your bot version and environment\n\n' + - 'This helps us fix issues faster and more effectively!', - color: 'error' - }); - bugReportEmbed.setFooter({ - text: 'TitanBot Bug Reporting System', - iconURL: client.user.displayAvatarURL() - }); - bugReportEmbed.setTimestamp(); - - await interaction.reply({ - embeds: [bugReportEmbed], - components: [bugRow], - flags: MessageFlags.Ephemeral - }); - }, -}; - -export const helpReportCommand = { - name: COMMAND_LIST_ID, - categoryName: null, - async execute(interaction, client) { - - } -}; - -function getPaginationInfo(components) { - for (const row of components || []) { - for (const component of row.components || []) { - if (component.customId === `${PAGINATION_PREFIX}_page`) { - const label = component.label || ''; - const match = label.match(/Page\s+(\d+)\s+of\s+(\d+)/i); - if (match) { - return { - currentPage: Number(match[1]), - totalPages: Number(match[2]), - }; - } - } - } - } - - return { currentPage: 1, totalPages: 1 }; -} - -export const helpPaginationButton = { - name: `${PAGINATION_PREFIX}_next`, - async execute(interaction, client) { - try { - if (!interaction.deferred && !interaction.replied) { - await interaction.deferUpdate(); - } - - const { currentPage, totalPages } = getPaginationInfo(interaction.message?.components); - - let nextPage = currentPage; - switch (interaction.customId) { - case `${PAGINATION_PREFIX}_first`: - nextPage = 1; - break; - case `${PAGINATION_PREFIX}_prev`: - nextPage = Math.max(1, currentPage - 1); - break; - case `${PAGINATION_PREFIX}_next`: - nextPage = Math.min(totalPages, currentPage + 1); - break; - case `${PAGINATION_PREFIX}_last`: - nextPage = totalPages; - break; - default: - nextPage = currentPage; - break; - } - - const { embeds, components } = await createAllCommandsMenu(nextPage, client); - await interaction.editReply({ embeds, components }); - } catch (error) { - if (error?.code === 40060 || error?.code === 10062) { - logger.warn('Help pagination interaction already acknowledged or expired.', { - event: 'interaction.help.pagination.unavailable', - errorCode: String(error.code), - customId: interaction.customId, - interactionId: interaction.id, - }); - return; - } - - throw error; - } - }, -}; \ No newline at end of file diff --git a/src/handlers/helpSelectMenus.js b/src/handlers/helpSelectMenus.js deleted file mode 100644 index 2ff8bc9240..0000000000 --- a/src/handlers/helpSelectMenus.js +++ /dev/null @@ -1,416 +0,0 @@ -import { createEmbed } from '../utils/embeds.js'; -import { createButton, getPaginationRow } from '../utils/components.js'; -import fs from 'fs/promises'; -import path from 'path'; -import { fileURLToPath } from 'url'; -import { Collection, ActionRowBuilder, MessageFlags } from 'discord.js'; -import { logger } from '../utils/logger.js'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); - -const BACK_BUTTON_ID = "help-back-to-main"; -const ALL_COMMANDS_ID = "help-all-commands"; -const PAGINATION_PREFIX = "help-page"; -const CATEGORY_SELECT_ID = "help-category-select"; -const FOOTER_TEXT = "Made with ❤️"; -const SUBCOMMAND_TYPE = 1; -const SUBCOMMAND_GROUP_TYPE = 2; - -const CATEGORY_ICONS = { - Core: "ℹ️", - Moderation: "🛡️", - Economy: "💰", - Music: "🎵", - Fun: "🎮", - Leveling: "📊", - Utility: "🔧", - Ticket: "🎫", - Welcome: "👋", - Giveaway: "🎉", - Counter: "🔢", - Tools: "🛠️", - Search: "🔍", - "Reaction Roles": "🎭", - Community: "👥", - Birthday: "🎂", - "Join To Create": "🔌", - Verification: "✅", - Config: "⚙️", -}; - -function formatCategoryName(rawCategory) { - return rawCategory - .replace(/_/g, ' ') - .replace(/([a-z])([A-Z])/g, '$1 $2') - .replace(/\b\w/g, (char) => char.toUpperCase()); -} - -function buildHelpEntries(command, category) { - const commandData = normalizeCommandData(command); - if (!commandData?.name) { - return []; - } - - const baseName = commandData.name; - const baseDescription = commandData.description || "No description"; - const options = commandData.options || []; - - const entries = []; - - for (const option of options) { - if (!option) continue; - - if (option.type === SUBCOMMAND_TYPE) { - entries.push({ - baseName, - displayName: `${baseName} ${option.name}`, - description: option.description || baseDescription, - category, - }); - continue; - } - - if (option.type === SUBCOMMAND_GROUP_TYPE) { - const nestedOptions = option.options || []; - for (const nested of nestedOptions) { - if (nested?.type !== SUBCOMMAND_TYPE) continue; - - entries.push({ - baseName, - displayName: `${baseName} ${option.name} ${nested.name}`, - description: nested.description || option.description || baseDescription, - category, - }); - } - } - } - - if (entries.length === 0) { - entries.push({ - baseName, - displayName: baseName, - description: baseDescription, - category, - }); - } - - return entries; -} - -function normalizeCommandData(command) { - const rawData = command?.data; - if (!rawData) { - return null; - } - - const jsonData = typeof rawData.toJSON === 'function' ? rawData.toJSON() : rawData; - if (!jsonData?.name) { - return null; - } - - return { - ...jsonData, - options: Array.isArray(jsonData.options) - ? jsonData.options.map((option) => - typeof option?.toJSON === 'function' ? option.toJSON() : option, - ) - : [], - }; -} - -async function createCategoryCommandsMenu(category, client) { - const categoryName = formatCategoryName(category); - const icon = CATEGORY_ICONS[categoryName] || "🔍"; - - const categoryCommands = []; - - try { - const categoryPath = path.join(__dirname, "../commands", category); - const commandFiles = (await fs.readdir(categoryPath)) - .filter((file) => file.endsWith(".js")) - .sort(); - - for (const file of commandFiles) { - const filePath = path.join(categoryPath, file); - const commandModule = await import(`file://${filePath}`); - const command = commandModule.default; - const commandData = normalizeCommandData(command); - - if (commandData) { - if ( - commandData.name === "help" || - commandData.name === "commandlist" - ) - continue; - - categoryCommands.push(...buildHelpEntries(command, categoryName)); - } - } - } catch (error) { - logger.error( - `Error reading commands from category ${category}:`, - error, - ); - } - - categoryCommands.sort((a, b) => a.displayName.localeCompare(b.displayName)); - - let registeredCommands = new Collection(); - try { - if (client?.application?.commands?.fetch) { - const commands = await client.application.commands.fetch(); - for (const cmd of commands.values()) { - registeredCommands.set(cmd.name, cmd); - } - } - } catch (error) { - logger.error('Error fetching registered commands:', error); - } - - const embed = createEmbed({ - title: `${icon} ${categoryName} Commands`, - description: categoryCommands.length > 0 - ? `Click any command mention below to use it.` - : `No commands found in the **${categoryName}** category.` - }); - - if (categoryCommands.length > 0) { - const commandMentions = categoryCommands - .map((cmd) => { - const registeredCmd = registeredCommands.get(cmd.baseName); - if (registeredCmd && registeredCmd.id) { - return ` · ${cmd.description}`; - } - return `\`/${cmd.displayName}\` · ${cmd.description}`; - }) - .join("\n"); - - const maxLength = 1000; - if (commandMentions.length <= maxLength) { - embed.addFields({ - name: "Commands", - value: commandMentions, - inline: false, - }); - } else { - const chunks = []; - let currentChunk = ""; - const lines = commandMentions.split("\n"); - - for (const line of lines) { - if ((currentChunk + "\n" + line).length > maxLength) { - if (currentChunk) chunks.push(currentChunk); - currentChunk = line; - } else { - currentChunk += (currentChunk ? "\n" : "") + line; - } - } - if (currentChunk) chunks.push(currentChunk); - - chunks.forEach((chunk, index) => { - embed.addFields({ - name: `Commands (Part ${index + 1})`, - value: chunk, - inline: false, - }); - }); - } - } - - embed.setFooter({ text: FOOTER_TEXT }); - embed.setTimestamp(); - - const backButton = createButton( - BACK_BUTTON_ID, - "Back", - "primary", - "⬅️", - false, - ); - - const buttonRow = new ActionRowBuilder().addComponents(backButton); - - return { - embeds: [embed], - components: [buttonRow], - }; -} - -export async function createAllCommandsMenu(page = 1, client) { - const commandsPerPage = 45; - const allCommands = []; - - const commandsPath = path.join(__dirname, "../commands"); - const categoryDirs = ( - await fs.readdir(commandsPath, { withFileTypes: true }) - ) - .filter((dirent) => dirent.isDirectory()) - .map((dirent) => dirent.name) - .sort(); - - for (const category of categoryDirs) { - try { - const categoryPath = path.join( - __dirname, - "../commands", - category, - ); - const commandFiles = (await fs.readdir(categoryPath)) - .filter((file) => file.endsWith(".js")) - .sort(); - - for (const file of commandFiles) { - const filePath = path.join(categoryPath, file); - const commandModule = await import(`file://${filePath}`); - const command = commandModule.default; - const commandData = normalizeCommandData(command); - - if (commandData) { - if ( - commandData.name === "help" || - commandData.name === "commandlist" - ) - continue; - - const categoryName = formatCategoryName(category); - - allCommands.push(...buildHelpEntries(command, categoryName)); - } - } - } catch (error) { - logger.error( - `Error reading commands from category ${category}:`, - error, - ); - } - } - - allCommands.sort((a, b) => a.displayName.localeCompare(b.displayName)); - - let registeredCommands = new Collection(); - try { - if (client?.application?.commands?.fetch) { - const commands = await client.application.commands.fetch(); - for (const cmd of commands.values()) { - registeredCommands.set(cmd.name, cmd); - } - } - } catch (error) { - logger.error('Error fetching registered commands:', error); - } - - const totalPages = Math.ceil(allCommands.length / commandsPerPage); - const startIndex = (page - 1) * commandsPerPage; - const endIndex = startIndex + commandsPerPage; - const pageCommands = allCommands.slice(startIndex, endIndex); - - const embed = createEmbed({ - title: "📋 All Commands", - description: `Browse every available command in one list. Use the page buttons below to move through the full set.` - }); - - embed.setFooter({ text: FOOTER_TEXT }); - embed.setTimestamp(); - - if (pageCommands.length > 0) { - const commandMentions = pageCommands.map((cmd) => { - const registeredCmd = registeredCommands.get(cmd.baseName); - if (registeredCmd && registeredCmd.id) { - return ` · ${cmd.category}`; - } - return `\`/${cmd.displayName}\` · ${cmd.category}`; - }); - - const columnCount = pageCommands.length > 20 ? 3 : (pageCommands.length > 10 ? 2 : 1); - const chunkSize = Math.ceil(commandMentions.length / columnCount); - - for (let i = 0; i < columnCount; i++) { - const chunk = commandMentions - .slice(i * chunkSize, (i + 1) * chunkSize) - .join("\n"); - - if (!chunk) continue; - - embed.addFields({ - name: i === 0 ? `Commands (Page ${page})` : "Commands (cont.)", - value: chunk, - inline: columnCount > 1, - }); - } - } - - const components = []; - - if (totalPages > 1) { - const paginationRow = getPaginationRow( - PAGINATION_PREFIX, - page, - totalPages, - ); - components.push(paginationRow); - } - - const backButton = createButton( - BACK_BUTTON_ID, - "Back", - "primary", - "⬅️", - false, - ); - - const buttonRow = new ActionRowBuilder().addComponents(backButton); - components.push(buttonRow); - - return { - embeds: [embed], - components, - currentPage: page, - totalPages, - }; -} - -export const helpCategorySelectMenu = { - name: CATEGORY_SELECT_ID, - async execute(interaction, client) { - try { - if (!interaction.deferred && !interaction.replied) { - await interaction.deferUpdate(); - } - - const selectedCategory = interaction.values[0]; - - if (selectedCategory === ALL_COMMANDS_ID) { - const { embeds, components } = await createAllCommandsMenu(1, client); - await interaction.editReply({ - embeds, - components, - }); - } else { - const { embeds, components } = await createCategoryCommandsMenu(selectedCategory, client); - await interaction.editReply({ - embeds, - components, - }); - } - } catch (error) { - if (error?.code === 40060 || error?.code === 10062) { - logger.warn('Help category select interaction already acknowledged or expired.', { - event: 'interaction.help.select.unavailable', - errorCode: String(error.code), - customId: interaction.customId, - interactionId: interaction.id, - }); - return; - } - - logger.error('Error in help category select menu handler:', error); - if (!interaction.replied && !interaction.deferred) { - await interaction.reply({ - content: 'An error occurred while loading help categories.', - flags: MessageFlags.Ephemeral, - }); - } - } - }, -}; \ No newline at end of file diff --git a/src/handlers/interactions.js b/src/handlers/interactions.js deleted file mode 100644 index e52db15701..0000000000 --- a/src/handlers/interactions.js +++ /dev/null @@ -1,56 +0,0 @@ -import { readdir } from 'fs/promises'; -import { join } from 'path'; -import { fileURLToPath } from 'url'; -import { dirname } from 'path'; -import { logger } from '../utils/logger.js'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); - -const interactionTypes = ['buttons', 'selectMenus', 'modals']; - -export default async (client) => { - try { - const interactionsPath = join(__dirname, '../interactions'); - - for (const type of interactionTypes) { - const typePath = join(interactionsPath, type); - - try { - const interactionFiles = (await readdir(typePath)).filter(file => file.endsWith('.js')); - let loadedCount = 0; - - for (const file of interactionFiles) { - try { - const module = await import(`../interactions/${type}/${file}`); - const moduleExport = module.default; - const interactions = Array.isArray(moduleExport) ? moduleExport : [moduleExport]; - - for (const interaction of interactions) { - if (!interaction?.name || !interaction?.execute) { - logger.warn(`Interaction ${file} in ${type} is missing required properties.`); - continue; - } - - client[type].set(interaction.name, interaction); - loadedCount += 1; - logger.info(`Loaded ${type.slice(0, -1)}: ${interaction.name}`); - } - } catch (error) { - logger.error(`Error loading interaction ${file} in ${type}:`, error); - } - } - - logger.info(`Loaded ${loadedCount} ${type}`); - } catch (error) { - if (error.code !== 'ENOENT') { - logger.error(`Error loading ${type}:`, error); - } else { - logger.debug(`No ${type} directory found, skipping...`); - } - } - } - } catch (error) { - logger.error('Error loading interactions:', error); - } -}; \ No newline at end of file diff --git a/src/handlers/loggingButtons.js b/src/handlers/loggingButtons.js index 675f3815d3..0f5144c205 100644 --- a/src/handlers/loggingButtons.js +++ b/src/handlers/loggingButtons.js @@ -18,15 +18,9 @@ import { updateIgnoreList, getIgnoreList, } from '../services/loggingService.js'; -<<<<<<< HEAD -import { getGuildConfig } from '../services/guildConfig.js'; -import { successEmbed } from '../utils/embeds.js'; -import { replyUserError, ErrorTypes } from '../utils/errorHandler.js'; -======= import { getGuildConfig } from '../services/config/guildConfig.js'; import { successEmbed } from '../utils/embeds.js'; import { replyUserError, ErrorTypes, handleInteractionError } from '../utils/errorHandler.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) import { logger } from '../utils/logger.js'; import { buildLoggingDashboardView, @@ -83,19 +77,11 @@ export default { return handleToggle(interaction); } } catch (error) { -<<<<<<< HEAD - logger.error('Error in logging button handler:', error); - await interaction.reply({ - content: '❌ An error occurred while processing your request.', - ephemeral: true, - }).catch(() => {}); -======= await handleInteractionError(interaction, error, { type: 'button', customId: interaction.customId, handler: 'logging', }); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } }, }; @@ -379,19 +365,11 @@ async function showChannelModal(interaction, destination) { if (error.code === 'INTERACTION_TIMEOUT') { return; } -<<<<<<< HEAD - logger.error('Error in logging channel modal:', error); - await interaction.followUp({ - content: '❌ Failed to update the log channel.', - flags: MessageFlags.Ephemeral, - }).catch(() => {}); -======= await handleInteractionError(interaction, error, { type: 'modal', customId: interaction.customId, handler: 'logging_channel', }); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } } diff --git a/src/handlers/musicButtons.js b/src/handlers/musicButtons.js index ba25e48427..13b8acd9c6 100644 --- a/src/handlers/musicButtons.js +++ b/src/handlers/musicButtons.js @@ -100,14 +100,11 @@ async function handleMusicButton(interaction, client) { await applyResume(client, interaction.guild.id); break; case MUSIC_BUTTON_IDS.SKIP: -<<<<<<< HEAD -======= // Under track-loop, stop() would replay the same track. Clear it so the // skip advances; trackStart re-applies the stored loop to the next track. if (player.loop === 'track') { player.setLoop('none'); } ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) player.stop(); break; case MUSIC_BUTTON_IDS.STOP: @@ -140,15 +137,11 @@ async function handleMusicButton(interaction, client) { break; } } catch (error) { -<<<<<<< HEAD - logger.error('Music button handler error:', error); -======= await handleInteractionError(interaction, error, { type: 'button', customId: interaction.customId, handler: 'music', }); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } } diff --git a/src/handlers/ticketButtons.js b/src/handlers/ticketButtons.js index bae0a202eb..76909a6d1c 100644 --- a/src/handlers/ticketButtons.js +++ b/src/handlers/ticketButtons.js @@ -1,15 +1,6 @@ import { ModalBuilder, TextInputBuilder, TextInputStyle, ActionRowBuilder, AttachmentBuilder, MessageFlags } from 'discord.js'; import { createEmbed, successEmbed } from '../utils/embeds.js'; import { createTicket, closeTicket, claimTicket, updateTicketPriority } from '../services/ticket.js'; -<<<<<<< HEAD -import { getGuildConfig } from '../services/guildConfig.js'; -import { logTicketEvent } from '../utils/ticketLogging.js'; -import { logger } from '../utils/logger.js'; -import { InteractionHelper } from '../utils/interactionHelper.js'; -import { checkRateLimit } from '../utils/rateLimiter.js'; -import { replyUserError, ErrorTypes } from '../utils/errorHandler.js'; -import { getTicketPermissionContext } from '../utils/ticketPermissions.js'; -======= import { getGuildConfig } from '../services/config/guildConfig.js'; import { logTicketEvent } from '../utils/ticket/ticketLogging.js'; import { logger } from '../utils/logger.js'; @@ -17,7 +8,6 @@ import { InteractionHelper } from '../utils/interactionHelper.js'; import { checkRateLimit } from '../utils/rateLimiter.js'; import { replyUserError, ErrorTypes, handleInteractionError, createError } from '../utils/errorHandler.js'; import { getTicketPermissionContext } from '../utils/ticket/ticketPermissions.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) function escapeHtml(text) { if (!text) return ''; @@ -41,56 +31,15 @@ async function ensureGuildContext(interaction) { return false; } -<<<<<<< HEAD -async function replyPermissionCheckFailure(interaction, permissionCheck) { - let type = ErrorTypes.UNKNOWN; - if (permissionCheck.error === 'Permission Denied') { - type = ErrorTypes.PERMISSION; - } else if (permissionCheck.error === 'Request Timeout') { - type = ErrorTypes.RATE_LIMIT; - } - - await replyUserError(interaction, { type, message: permissionCheck.details }); -} - -async function checkTicketPermissionWithTimeout(interaction, client, actionLabel, options = {}, timeoutMs = 2500) { - const { allowTicketCreator = false } = options; - -======= async function assertTicketPermission(interaction, client, actionLabel, options = {}, timeoutMs = 2500) { const { allowTicketCreator = false } = options; let context; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) try { const contextPromise = getTicketPermissionContext({ client, interaction }); const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), timeoutMs) ); -<<<<<<< HEAD - - const context = await Promise.race([contextPromise, timeoutPromise]); - - if (!context.ticketData) { - return { success: false, error: 'Not a Ticket Channel', details: 'This action can only be used in a valid ticket channel.' }; - } - - const allowed = allowTicketCreator ? context.canCloseTicket : context.canManageTicket; - if (!allowed) { - const permissionMessage = allowTicketCreator - ? 'You must have **Manage Channels**, the configured **Ticket Staff Role**, or be the **ticket creator**.' - : 'You must have **Manage Channels** or the configured **Ticket Staff Role**.'; - return { success: false, error: 'Permission Denied', details: `${permissionMessage}\n\nYou cannot ${actionLabel}.` }; - } - - return { success: true, context }; - } catch (error) { - if (error.message === 'Timeout') { - return { success: false, error: 'Request Timeout', details: 'The permission check took too long. Please try again.' }; - } - return { success: false, error: 'Error', details: `Failed to check permissions: ${error.message}` }; - } -======= context = await Promise.race([contextPromise, timeoutPromise]); } catch (error) { if (error.message === 'Timeout') { @@ -128,7 +77,6 @@ async function assertTicketPermission(interaction, client, actionLabel, options } return context; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } async function ensureTicketPermission(interaction, client, actionLabel, options = {}) { @@ -215,32 +163,12 @@ const createTicketModalHandler = { const config = await getGuildConfig(client, interaction.guildId); const categoryId = config.ticketCategoryId || null; -<<<<<<< HEAD - const result = await createTicket( -======= const { channel } = await createTicket( ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) interaction.guild, interaction.member, categoryId, reason ); -<<<<<<< HEAD - - if (result.success) { - await interaction.editReply({ - embeds: [successEmbed( - 'Ticket Created', - `Your ticket has been created in ${result.channel}!` - )] - }); - } else { - await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: result.error || 'Failed to create ticket.' }); - } - } catch (error) { - logger.error('Error creating ticket:', error); - await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'An error occurred while creating your ticket.' }); -======= await interaction.editReply({ embeds: [successEmbed( 'Ticket Created', @@ -249,7 +177,6 @@ const createTicketModalHandler = { }); } catch (error) { await handleInteractionError(interaction, error, { type: 'button', handler: 'ticket', customId: interaction.customId }); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } } }; @@ -260,22 +187,7 @@ const closeTicketHandler = { try { if (!(await ensureGuildContext(interaction))) return; -<<<<<<< HEAD - const permissionCheck = await checkTicketPermissionWithTimeout( - interaction, - client, - 'close this ticket', - { allowTicketCreator: true }, - 2000 - ); - - if (!permissionCheck.success) { - await replyPermissionCheckFailure(interaction, permissionCheck); - return; - } -======= await assertTicketPermission(interaction, client, 'close this ticket', { allowTicketCreator: true }, 2000); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) const modal = new ModalBuilder() .setCustomId('ticket_close_modal') @@ -309,22 +221,7 @@ const closeTicketModalHandler = { try { if (!(await ensureGuildContext(interaction))) return; -<<<<<<< HEAD - const permissionCheck = await checkTicketPermissionWithTimeout( - interaction, - client, - 'close this ticket', - { allowTicketCreator: true }, - 2000 - ); - - if (!permissionCheck.success) { - await replyPermissionCheckFailure(interaction, permissionCheck); - return; - } -======= await assertTicketPermission(interaction, client, 'close this ticket', { allowTicketCreator: true }, 2000); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) const deferSuccess = await InteractionHelper.safeDefer(interaction, { flags: MessageFlags.Ephemeral }); if (!deferSuccess) return; @@ -332,21 +229,8 @@ const closeTicketModalHandler = { const providedReason = interaction.fields.getTextInputValue('reason')?.trim(); const reason = providedReason || 'Closed via ticket button without a specific reason.'; -<<<<<<< HEAD - const result = await closeTicket(interaction.channel, interaction.user, reason); - - if (result.success) { - await interaction.editReply({ - embeds: [successEmbed('Ticket Closed', 'This ticket has been closed.')], - flags: MessageFlags.Ephemeral - }); - } else { - await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: result.error || 'Failed to close ticket.' }); - } -======= await closeTicket(interaction.channel, interaction.user, reason); await interaction.editReply({ embeds: [successEmbed('Ticket Closed', 'This ticket has been closed.')] }); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } catch (error) { logger.error('Error submitting close ticket modal:', error); if (!interaction.replied && !interaction.deferred) { @@ -364,41 +248,13 @@ const claimTicketHandler = { try { if (!(await ensureGuildContext(interaction))) return; -<<<<<<< HEAD - const permissionCheck = await checkTicketPermissionWithTimeout( - interaction, - client, - 'claim tickets', - {}, - 2000 - ); - - if (!permissionCheck.success) { - await replyPermissionCheckFailure(interaction, permissionCheck); - return; - } -======= await assertTicketPermission(interaction, client, 'claim tickets', {}, 2000); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) const deferSuccess = await InteractionHelper.safeDefer(interaction, { flags: MessageFlags.Ephemeral }); if (!deferSuccess) return; -<<<<<<< HEAD - const result = await claimTicket(interaction.channel, interaction.user); - - if (result.success) { - await interaction.editReply({ - embeds: [successEmbed('Ticket Claimed', 'You have successfully claimed this ticket!')], - flags: MessageFlags.Ephemeral - }); - } else { - await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: result.error || 'Failed to claim ticket.' }); - } -======= await claimTicket(interaction.channel, interaction.user); await interaction.editReply({ embeds: [successEmbed('Ticket Claimed', 'You have claimed this ticket.')] }); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } catch (error) { logger.error('Error claiming ticket:', error); if (!interaction.replied && !interaction.deferred) { @@ -416,22 +272,7 @@ const priorityTicketHandler = { try { if (!(await ensureGuildContext(interaction))) return; -<<<<<<< HEAD - const permissionCheck = await checkTicketPermissionWithTimeout( - interaction, - client, - 'change ticket priority', - {}, - 2000 - ); - - if (!permissionCheck.success) { - await replyPermissionCheckFailure(interaction, permissionCheck); - return; - } -======= await assertTicketPermission(interaction, client, 'change ticket priority', {}, 2000); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) const deferSuccess = await InteractionHelper.safeDefer(interaction, { flags: MessageFlags.Ephemeral }); if (!deferSuccess) return; @@ -442,21 +283,8 @@ const priorityTicketHandler = { return; } -<<<<<<< HEAD - const result = await updateTicketPriority(interaction.channel, priority, interaction.user); - - if (result.success) { - await interaction.editReply({ - embeds: [successEmbed('Priority Updated', `Ticket priority set to ${priority}.`)], - flags: MessageFlags.Ephemeral - }); - } else { - await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: result.error || 'Failed to update priority.' }); - } -======= await updateTicketPriority(interaction.channel, priority, interaction.user); await interaction.editReply({ embeds: [successEmbed('Priority Updated', `Ticket priority set to **${priority.toUpperCase()}**.`)] }); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } catch (error) { logger.error('Error updating ticket priority:', error); if (!interaction.replied && !interaction.deferred) { @@ -474,22 +302,7 @@ const pinTicketHandler = { try { if (!(await ensureGuildContext(interaction))) return; -<<<<<<< HEAD - const permissionCheck = await checkTicketPermissionWithTimeout( - interaction, - client, - 'pin tickets', - {}, - 2000 - ); - - if (!permissionCheck.success) { - await replyPermissionCheckFailure(interaction, permissionCheck); - return; - } -======= await assertTicketPermission(interaction, client, 'pin tickets', {}, 2000); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) const deferSuccess = await InteractionHelper.safeDefer(interaction, { flags: MessageFlags.Ephemeral }); if (!deferSuccess) return; @@ -585,42 +398,14 @@ const unclaimTicketHandler = { try { if (!(await ensureGuildContext(interaction))) return; -<<<<<<< HEAD - const permissionCheck = await checkTicketPermissionWithTimeout( - interaction, - client, - 'unclaim tickets', - {}, - 2000 - ); - - if (!permissionCheck.success) { - await replyPermissionCheckFailure(interaction, permissionCheck); - return; - } -======= await assertTicketPermission(interaction, client, 'unclaim tickets', {}, 2000); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) const deferSuccess = await InteractionHelper.safeDefer(interaction, { flags: MessageFlags.Ephemeral }); if (!deferSuccess) return; const { unclaimTicket } = await import('../services/ticket.js'); -<<<<<<< HEAD - const result = await unclaimTicket(interaction.channel, interaction.member); - - if (result.success) { - await interaction.editReply({ - embeds: [successEmbed('Ticket Unclaimed', 'You have successfully unclaimed this ticket!')], - flags: MessageFlags.Ephemeral - }); - } else { - await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: result.error || 'Failed to unclaim ticket.' }); - } -======= await unclaimTicket(interaction.channel, interaction.member); await interaction.editReply({ embeds: [successEmbed('Ticket Unclaimed', 'This ticket has been unclaimed.')] }); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } catch (error) { logger.error('Error unclaiming ticket:', error); if (!interaction.replied && !interaction.deferred) { @@ -638,51 +423,18 @@ const reopenTicketHandler = { try { if (!(await ensureGuildContext(interaction))) return; -<<<<<<< HEAD - const permissionCheck = await checkTicketPermissionWithTimeout( - interaction, - client, - 'reopen tickets', - {}, - 2000 - ); - - if (!permissionCheck.success) { - await replyPermissionCheckFailure(interaction, permissionCheck); - return; - } -======= await assertTicketPermission(interaction, client, 'reopen tickets', {}, 2000); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) const deferSuccess = await InteractionHelper.safeDefer(interaction, { flags: MessageFlags.Ephemeral }); if (!deferSuccess) return; const { reopenTicket } = await import('../services/ticket.js'); -<<<<<<< HEAD - const result = await reopenTicket(interaction.channel, interaction.member); - - if (result.success) { - let reopenMessage = 'You have successfully reopened this ticket!'; - if (result.openCategoryMoveFailed) { - reopenMessage += '\n\n⚠️ The ticket was reopened, but it could not be moved to the configured open ticket category.'; - } - - await interaction.editReply({ - embeds: [successEmbed('Ticket Reopened', reopenMessage)], - flags: MessageFlags.Ephemeral - }); - } else { - await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: result.error || 'Failed to reopen ticket.' }); - } -======= const { movedToOpenCategory, openCategoryMoveFailed } = await reopenTicket(interaction.channel, interaction.member); let reopenMessage = 'This ticket has been reopened.'; if (openCategoryMoveFailed) { reopenMessage += ' Note: Could not move the channel back to the open tickets category.'; } await interaction.editReply({ embeds: [successEmbed('Ticket Reopened', reopenMessage)] }); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } catch (error) { logger.error('Error reopening ticket:', error); if (!interaction.replied && !interaction.deferred) { @@ -700,42 +452,14 @@ const deleteTicketHandler = { try { if (!(await ensureGuildContext(interaction))) return; -<<<<<<< HEAD - const permissionCheck = await checkTicketPermissionWithTimeout( - interaction, - client, - 'delete tickets', - {}, - 2000 - ); - - if (!permissionCheck.success) { - await replyPermissionCheckFailure(interaction, permissionCheck); - return; - } -======= await assertTicketPermission(interaction, client, 'delete tickets', {}, 2000); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) const deferSuccess = await InteractionHelper.safeDefer(interaction, { flags: MessageFlags.Ephemeral }); if (!deferSuccess) return; const { deleteTicket } = await import('../services/ticket.js'); -<<<<<<< HEAD - const result = await deleteTicket(interaction.channel, interaction.member); - - if (result.success) { - await interaction.editReply({ - embeds: [successEmbed('Ticket Deleted', 'This ticket will be permanently deleted in 3 seconds.')], - flags: MessageFlags.Ephemeral - }); - } else { - await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: result.error || 'Failed to delete ticket.' }); - } -======= await deleteTicket(interaction.channel, interaction.member); await interaction.editReply({ embeds: [successEmbed('Ticket Deleted', 'This ticket will be deleted shortly.')] }); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } catch (error) { logger.error('Error deleting ticket:', error); if (!interaction.replied && !interaction.deferred) { diff --git a/src/handlers/todoButtons.js b/src/handlers/todoButtons.js index aee8e69ab0..3b86997a11 100644 --- a/src/handlers/todoButtons.js +++ b/src/handlers/todoButtons.js @@ -4,10 +4,7 @@ import { getFromDb, setInDb } from '../utils/database.js'; import { checkRateLimit } from '../utils/rateLimiter.js'; import { logger } from '../utils/logger.js'; -<<<<<<< HEAD -======= import { replyUserError, ErrorTypes } from '../utils/errorHandler.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) function buildSharedTodoViewPayload(listData, listId, guild) { const memberList = (listData.members || []).map(memberId => { const member = guild?.members?.cache?.get(memberId); @@ -306,11 +303,7 @@ const sharedTodoCompleteModalHandler = { } if (task.completed) { -<<<<<<< HEAD - return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'Task #${task.id} is already completed.' }); -======= return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: `Task #${task.id} is already completed.` }); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } task.completed = true; diff --git a/src/handlers/verificationButtons.js b/src/handlers/verificationButtons.js index 717e0ae21d..663621ec8f 100644 --- a/src/handlers/verificationButtons.js +++ b/src/handlers/verificationButtons.js @@ -1,11 +1,7 @@ import { MessageFlags } from 'discord.js'; import { successEmbed } from '../utils/embeds.js'; import { verifyUser } from '../services/verificationService.js'; -<<<<<<< HEAD -import { handleInteractionError } from '../utils/errorHandler.js'; -======= import { handleInteractionError, replyUserError, ErrorTypes } from '../utils/errorHandler.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) import { logger } from '../utils/logger.js'; import { InteractionHelper } from '../utils/interactionHelper.js'; @@ -31,17 +27,8 @@ export async function handleVerificationButton(interaction, client) { moderatorId: null }); -<<<<<<< HEAD - if (!result.success) { - if (result.alreadyVerified) { - return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'You are already verified and have access to all server channels.' }); - } - - return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'An error occurred during verification. Please try again or contact an administrator.' }); -======= if (result.status === 'already_verified') { return await replyUserError(interaction, { type: ErrorTypes.VALIDATION, message: 'You are already verified and have access to all server channels.' }); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } logger.info('User verified via button', { diff --git a/src/handlers/warningHandlers.js b/src/handlers/warningHandlers.js index 1acb0ad340..26b5970bb2 100644 --- a/src/handlers/warningHandlers.js +++ b/src/handlers/warningHandlers.js @@ -1,11 +1,3 @@ -<<<<<<< HEAD -import { ModalBuilder, TextInputBuilder, TextInputStyle, ActionRowBuilder } from 'discord.js'; -import { successEmbed } from '../utils/embeds.js'; -import { WarningService } from '../services/warningService.js'; -import { InteractionHelper } from '../utils/interactionHelper.js'; -import { logger } from '../utils/logger.js'; - -======= import { ModalBuilder, TextInputBuilder, TextInputStyle, ActionRowBuilder, MessageFlags } from 'discord.js'; import { successEmbed } from '../utils/embeds.js'; import { WarningService } from '../services/moderation/warningService.js'; @@ -13,7 +5,6 @@ import { InteractionHelper } from '../utils/interactionHelper.js'; import { logger } from '../utils/logger.js'; import { replyUserError, ErrorTypes } from '../utils/errorHandler.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) const warningDeleteSpecificHandler = { name: 'warning_delete_specific', async execute(interaction, client) { @@ -106,23 +97,11 @@ async function warningDeleteModalHandler(interaction, client) { const warnings = await WarningService.getWarnings(guildId, targetUserId); if (warningNumber > warnings.length) { -<<<<<<< HEAD - return await replyUserError(interaction, { type: ErrorTypes.USER_INPUT, message: 'Warning #${warningNumber} does not exist. This user only has ${warnings.length} warning(s).' }); - } - - const warningToDelete = warnings[warningNumber - 1]; - const result = await WarningService.removeWarning(guildId, targetUserId, warningToDelete.id); - - if (!result.success) { - throw new Error(result.error || 'Failed to delete warning'); - } -======= return await replyUserError(interaction, { type: ErrorTypes.USER_INPUT, message: `Warning #${warningNumber} does not exist. This user only has ${warnings.length} warning(s).` }); } const warningToDelete = warnings[warningNumber - 1]; await WarningService.removeWarning(guildId, targetUserId, warningToDelete.id); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) const targetUser = await client.users.fetch(targetUserId).catch(() => null); const targetName = targetUser ? targetUser.username : 'the user'; @@ -156,21 +135,10 @@ async function warningClearConfirmModalHandler(interaction, client) { return await replyUserError(interaction, { type: ErrorTypes.UNKNOWN, message: 'You must type "DELETE" exactly to confirm clearing all warnings.' }); } -<<<<<<< HEAD - await interaction.deferReply({ flags: ['Ephemeral'] }); - - const guildId = interaction.guildId; - const result = await WarningService.clearWarnings(guildId, targetUserId); - - if (!result.success) { - throw new Error(result.error || 'Failed to clear warnings'); - } -======= await interaction.deferReply({ flags: MessageFlags.Ephemeral }); const guildId = interaction.guildId; const { count } = await WarningService.clearWarnings(guildId, targetUserId); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) const targetUser = await client.users.fetch(targetUserId).catch(() => null); const targetName = targetUser ? targetUser.username : 'the user'; @@ -178,11 +146,7 @@ async function warningClearConfirmModalHandler(interaction, client) { logger.info(`[MODERATION] All warnings cleared for ${targetUserId} in ${guildId} by ${interaction.user.id}`); await interaction.editReply({ -<<<<<<< HEAD - embeds: [successEmbed('✅ Warnings Cleared', `All warnings for **${targetName}** have been cleared. **${result.count}** warning(s) removed.`)] -======= embeds: [successEmbed('✅ Warnings Cleared', `All warnings for **${targetName}** have been cleared. **${count}** warning(s) removed.`)] ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) }); } catch (error) { logger.error('Warning clear confirm modal handler error:', error); diff --git a/src/handlers/wipedataButtons.js b/src/handlers/wipedataButtons.js index ef3b3719e8..57c2d7ce17 100644 --- a/src/handlers/wipedataButtons.js +++ b/src/handlers/wipedataButtons.js @@ -3,8 +3,6 @@ import { InteractionHelper } from '../utils/interactionHelper.js'; import { MessageFlags } from 'discord.js'; import { logger } from '../utils/logger.js'; -<<<<<<< HEAD -======= import { replyUserError, ErrorTypes } from '../utils/errorHandler.js'; import { getEconomyKey, @@ -15,7 +13,6 @@ import { getEconomyPrefix, getUserLevelPrefix, } from '../utils/database.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) const wipedataConfirmHandler = { name: 'wipedata_yes', async execute(interaction, client) { @@ -27,15 +24,11 @@ const wipedataConfirmHandler = { const guildId = interaction.guildId; const dataKeyPatterns = [ -<<<<<<< HEAD - `economy:${guildId}:${userId}`, -======= getEconomyKey(guildId, userId), getUserLevelKey(guildId, userId), getAFKKey(guildId, userId), getWarningsKey(guildId, userId), getUserNotesKey(guildId, userId), ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) `level:${guildId}:${userId}`, `xp:${guildId}:${userId}`, `inventory:${guildId}:${userId}`, @@ -57,10 +50,7 @@ const wipedataConfirmHandler = { `lastWork:${guildId}:${userId}`, `lastCrime:${guildId}:${userId}`, `lastRob:${guildId}:${userId}`, -<<<<<<< HEAD -======= `${guildId}:leveling:users:${userId}`, ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) ]; let deletedCount = 0; @@ -84,12 +74,8 @@ const wipedataConfirmHandler = { const searchPrefixes = [ `${guildId}:${userId}`, `${guildId}:`, -<<<<<<< HEAD - `economy:${guildId}:`, -======= getEconomyPrefix(guildId), getUserLevelPrefix(guildId), ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) `level:${guildId}:`, `xp:${guildId}:`, `user:${guildId}:` diff --git a/src/interactions/buttons/countdown.js b/src/interactions/buttons/countdown.js deleted file mode 100644 index 6503f40ca5..0000000000 --- a/src/interactions/buttons/countdown.js +++ /dev/null @@ -1,12 +0,0 @@ -import countdownButtonHandler from '../../handlers/countdownButtons.js'; - -export default [ - { - name: 'countdown_pause', - execute: countdownButtonHandler, - }, - { - name: 'countdown_cancel', - execute: countdownButtonHandler, - }, -]; \ No newline at end of file diff --git a/src/interactions/buttons/counterDelete.js b/src/interactions/buttons/counterDelete.js deleted file mode 100644 index df171dcb10..0000000000 --- a/src/interactions/buttons/counterDelete.js +++ /dev/null @@ -1,3 +0,0 @@ -import counterDeleteActionHandler from '../../handlers/counterButtons.js'; - -export default counterDeleteActionHandler; \ No newline at end of file diff --git a/src/interactions/buttons/giveaway.js b/src/interactions/buttons/giveaway.js deleted file mode 100644 index 962b6f5da4..0000000000 --- a/src/interactions/buttons/giveaway.js +++ /dev/null @@ -1,20 +0,0 @@ -import { - giveawayJoinHandler, - giveawayEndHandler, - giveawayRerollHandler, - giveawayViewHandler, -} from '../../handlers/giveawayButtons.js'; - -function fromCustomId(handler) { - return { - name: handler.customId, - execute: handler.execute, - }; -} - -export default [ - fromCustomId(giveawayJoinHandler), - fromCustomId(giveawayEndHandler), - fromCustomId(giveawayRerollHandler), - fromCustomId(giveawayViewHandler), -]; \ No newline at end of file diff --git a/src/interactions/buttons/help.js b/src/interactions/buttons/help.js deleted file mode 100644 index 9e7a011262..0000000000 --- a/src/interactions/buttons/help.js +++ /dev/null @@ -1,19 +0,0 @@ -import { - helpBackButton, - helpBugReportButton, - helpPaginationButton, -} from '../../handlers/helpButtons.js'; - -const paginationIds = [ - 'help-page_first', - 'help-page_prev', - 'help-page_next', - 'help-page_last', -]; - -const paginationInteractions = paginationIds.map((name) => ({ - name, - execute: helpPaginationButton.execute, -})); - -export default [helpBackButton, helpBugReportButton, ...paginationInteractions]; \ No newline at end of file diff --git a/src/interactions/buttons/logging.js b/src/interactions/buttons/logging.js deleted file mode 100644 index bd389c81f1..0000000000 --- a/src/interactions/buttons/logging.js +++ /dev/null @@ -1,24 +0,0 @@ -import loggingButtonsHandler from '../../handlers/loggingButtons.js'; - -export default [ - { - name: 'log_dash_toggle', - execute: loggingButtonsHandler.execute, - }, - { - name: 'log_dash_refresh', - execute: loggingButtonsHandler.execute, - }, - { - name: 'log_dash_back', - execute: loggingButtonsHandler.execute, - }, - { - name: 'log_dash_add_filter', - execute: loggingButtonsHandler.execute, - }, - { - name: 'log_dash_remove_filter', - execute: loggingButtonsHandler.execute, - }, -]; diff --git a/src/interactions/buttons/music.js b/src/interactions/buttons/music.js deleted file mode 100644 index d6ce6e3bde..0000000000 --- a/src/interactions/buttons/music.js +++ /dev/null @@ -1,23 +0,0 @@ -import musicButtonHandler from '../../handlers/musicButtons.js'; -import { MUSIC_BUTTON_IDS } from '../../services/music/musicEmbeds.js'; - -const buttonIds = [ - MUSIC_BUTTON_IDS.PAUSE, - MUSIC_BUTTON_IDS.RESUME, - MUSIC_BUTTON_IDS.SKIP, - MUSIC_BUTTON_IDS.STOP, - MUSIC_BUTTON_IDS.SHUFFLE, - MUSIC_BUTTON_IDS.LOOP, - MUSIC_BUTTON_IDS.VOL_DOWN, - MUSIC_BUTTON_IDS.VOL_UP, - MUSIC_BUTTON_IDS.QUEUE, - MUSIC_BUTTON_IDS.QUEUE_FIRST, - MUSIC_BUTTON_IDS.QUEUE_PREV, - MUSIC_BUTTON_IDS.QUEUE_NEXT, - MUSIC_BUTTON_IDS.QUEUE_LAST, -]; - -export default buttonIds.map((name) => ({ - name, - execute: musicButtonHandler.execute, -})); diff --git a/src/interactions/buttons/ticket.js b/src/interactions/buttons/ticket.js deleted file mode 100644 index bcc2521f71..0000000000 --- a/src/interactions/buttons/ticket.js +++ /dev/null @@ -1,20 +0,0 @@ -import createTicketHandler, { - closeTicketHandler, - claimTicketHandler, - priorityTicketHandler, - pinTicketHandler, - unclaimTicketHandler, - reopenTicketHandler, - deleteTicketHandler, -} from '../../handlers/ticketButtons.js'; - -export default [ - createTicketHandler, - closeTicketHandler, - claimTicketHandler, - priorityTicketHandler, - pinTicketHandler, - unclaimTicketHandler, - reopenTicketHandler, - deleteTicketHandler, -]; \ No newline at end of file diff --git a/src/interactions/buttons/ticketFeedback.js b/src/interactions/buttons/ticketFeedback.js deleted file mode 100644 index 6759ac4b9d..0000000000 --- a/src/interactions/buttons/ticketFeedback.js +++ /dev/null @@ -1,189 +0,0 @@ -import { EmbedBuilder, ModalBuilder, ActionRowBuilder, TextInputBuilder, TextInputStyle } from 'discord.js'; -import { getTicketData, saveTicketData } from '../../utils/database.js'; -import { logger } from '../../utils/logger.js'; -import { getColor } from '../../config/bot.js'; -import { logTicketFeedback } from '../../utils/ticketLogging.js'; -import { InteractionHelper } from '../../utils/interactionHelper.js'; - -const STAR_LABELS = { - '1': '⭐ 1 — Poor', - '2': '⭐ 2 — Below Average', - '3': '⭐ 3 — Average', - '4': '⭐ 4 — Good', - '5': '⭐ 5 — Excellent', -}; - -const feedbackHandler = { - name: 'ticket_feedback', - - async execute(interaction, client, args) { - - const [guildId, channelId, ratingStr] = args; - - if (!guildId || !channelId || !ratingStr) { - await InteractionHelper.safeReply(interaction, { - embeds: [ - new EmbedBuilder() - .setTitle('⚠️ Invalid Feedback Link') - .setDescription('This feedback link appears to be malformed.') - .setColor(getColor('error')), - ], - components: [], - }); - return; - } - - try { - await interaction.deferUpdate(); - } catch (err) { - logger.warn('ticketFeedback: interaction expired before deferUpdate', { guildId, channelId, error: err.message }); - return; - } - - let ticketData; - try { - ticketData = await getTicketData(guildId, channelId); - } catch (err) { - logger.warn('ticketFeedback: failed to load ticket data', { guildId, channelId, error: err.message }); - } - - if (!ticketData) { - await InteractionHelper.safeEditReply(interaction, { - embeds: [ - new EmbedBuilder() - .setTitle('⚠️ Ticket Not Found') - .setDescription('Could not find the ticket associated with this survey.') - .setColor(getColor('error')), - ], - components: [], - }); - return; - } - - if (interaction.user.id !== ticketData.userId) { - await InteractionHelper.safeEditReply(interaction, { - embeds: [ - new EmbedBuilder() - .setTitle('❌ Not Allowed') - .setDescription('Only the ticket creator can submit feedback for this ticket.') - .setColor(getColor('error')), - ], - components: [], - }); - return; - } - - if (ticketData.feedback?.rating) { - await InteractionHelper.safeEditReply(interaction, { - embeds: [ - new EmbedBuilder() - .setTitle('✅ Already Submitted') - .setDescription(`You already rated this ticket **${STAR_LABELS[String(ticketData.feedback.rating)]}**.\nThank you for your feedback!`) - .setColor(getColor('success')), - ], - components: [], - }); - return; - } - - const rating = parseInt(ratingStr, 10); - const ratingLabel = STAR_LABELS[String(rating)] ?? `${rating} stars`; - - try { - ticketData.feedback = { - rating, - submittedAt: new Date().toISOString(), - }; - await saveTicketData(guildId, channelId, ticketData); - } catch (err) { - logger.error('ticketFeedback: failed to save feedback', { guildId, channelId, rating, error: err.message }); - } - - try { - await logTicketFeedback({ - client: interaction.client, - guildId, - ticketNumber: ticketData.id, - ticketChannelId: channelId, - userId: interaction.user.id, - rating, - }); - } catch (err) { - logger.warn('ticketFeedback: failed to send log', { guildId, channelId, error: err.message }); - } - - await InteractionHelper.safeEditReply(interaction, { - embeds: [ - new EmbedBuilder() - .setTitle('✅ Thanks for your feedback!') - .setDescription(`You rated your support experience **${ratingLabel}**.\n\nYour feedback has been recorded and helps us improve!`) - .setColor(getColor('success')) - .setFooter({ text: 'Thank you for using our support system.' }) - .setTimestamp(), - ], - components: [], - }); - - logger.info('Ticket feedback submitted', { - guildId, - channelId, - userId: interaction.user.id, - rating, - }); - }, -}; - -const commentHandler = { - name: 'ticket_feedback_comment', - - async execute(interaction, client, args) { - const [guildId, channelId] = args; - - if (!guildId || !channelId) { - await interaction.update({ - embeds: [ - new EmbedBuilder() - .setTitle('⚠️ Invalid Feedback Link') - .setDescription('This feedback action appears to be malformed.') - .setColor(getColor('error')), - ], - components: [], - }); - return; - } - - const modal = new ModalBuilder() - .setCustomId(`ticket_feedback_comment_modal:${guildId}:${channelId}`) - .setTitle('Add Ticket Feedback'); - - const commentInput = new TextInputBuilder() - .setCustomId('feedback_comment') - .setLabel('Your feedback') - .setStyle(TextInputStyle.Paragraph) - .setPlaceholder('Share what went well or how we can improve...') - .setRequired(true) - .setMaxLength(1000); - - modal.addComponents(new ActionRowBuilder().addComponents(commentInput)); - - await interaction.showModal(modal); - }, -}; - -const declineHandler = { - name: 'ticket_feedback_decline', - - async execute(interaction) { - await interaction.update({ - embeds: [ - new EmbedBuilder() - .setTitle('👋 No problem!') - .setDescription('You can always reach out again if you need further support.') - .setColor(getColor('default')), - ], - components: [], - }); - }, -}; - -export default [feedbackHandler, commentHandler, declineHandler]; \ No newline at end of file diff --git a/src/interactions/buttons/todoShared.js b/src/interactions/buttons/todoShared.js deleted file mode 100644 index 02b5fb2009..0000000000 --- a/src/interactions/buttons/todoShared.js +++ /dev/null @@ -1,10 +0,0 @@ -import todoAddHandler, { - sharedTodoCompleteHandler, - sharedTodoRemoveHandler, -} from '../../handlers/todoButtons.js'; - -export default [ - todoAddHandler, - sharedTodoCompleteHandler, - sharedTodoRemoveHandler, -]; \ No newline at end of file diff --git a/src/interactions/buttons/verification.js b/src/interactions/buttons/verification.js deleted file mode 100644 index f0bc0887e3..0000000000 --- a/src/interactions/buttons/verification.js +++ /dev/null @@ -1,6 +0,0 @@ -import verificationButtonHandler from '../../handlers/verificationButtons.js'; - -export default { - name: verificationButtonHandler.customId, - execute: verificationButtonHandler.execute, -}; \ No newline at end of file diff --git a/src/interactions/buttons/warnings.js b/src/interactions/buttons/warnings.js deleted file mode 100644 index f1a4acbb6c..0000000000 --- a/src/interactions/buttons/warnings.js +++ /dev/null @@ -1,9 +0,0 @@ -import { - warningDeleteSpecificHandler, - warningClearAllHandler -} from '../../handlers/warningHandlers.js'; - -export default [ - warningDeleteSpecificHandler, - warningClearAllHandler -]; \ No newline at end of file diff --git a/src/interactions/buttons/wipedata.js b/src/interactions/buttons/wipedata.js deleted file mode 100644 index 38d910b145..0000000000 --- a/src/interactions/buttons/wipedata.js +++ /dev/null @@ -1,6 +0,0 @@ -import { - wipedataConfirmHandler, - wipedataCancelHandler, -} from '../../handlers/wipedataButtons.js'; - -export default [wipedataConfirmHandler, wipedataCancelHandler]; \ No newline at end of file diff --git a/src/interactions/modals/calculate.js b/src/interactions/modals/calculate.js deleted file mode 100644 index be3ab8fb20..0000000000 --- a/src/interactions/modals/calculate.js +++ /dev/null @@ -1,10 +0,0 @@ -import calculateModalHandler from '../../handlers/calculateModals.js'; - -const execute = typeof calculateModalHandler === 'function' - ? calculateModalHandler - : calculateModalHandler.execute; - -export default { - name: 'calc_modal', - execute, -}; \ No newline at end of file diff --git a/src/interactions/modals/config_modal.js b/src/interactions/modals/config_modal.js deleted file mode 100644 index 5535fe1a23..0000000000 --- a/src/interactions/modals/config_modal.js +++ /dev/null @@ -1,110 +0,0 @@ -import { successEmbed } from '../../utils/embeds.js'; - -import ConfigService from '../../services/configService.js'; -import { MessageFlags } from 'discord.js'; -import { logger } from '../../utils/logger.js'; -import { ErrorTypes, replyUserError } from '../../utils/errorHandler.js'; - -function extractId(value) { - if (!value || typeof value !== 'string') return null; - - const channelMention = value.match(/^<#(\d+)>$/); - if (channelMention) return channelMention[1]; - - const roleMention = value.match(/^<@&(\d+)>$/); - if (roleMention) return roleMention[1]; - - const digits = value.match(/^(\d{17,19})$/); - if (digits) return digits[1]; - - return null; -} - -function parseValue(key, rawValue) { - const value = rawValue.trim(); - - if (['modRole', 'adminRole', 'autoRole', 'logChannelId'].includes(key)) { - if (value.toLowerCase() === 'none') { - return null; - } - const id = extractId(value); - if (!id) { - throw new Error('Please provide a valid mention or ID.'); - } - return id; - } - - if (key === 'dmOnClose') { - if (['yes', 'true', 'enabled', 'enable'].includes(value.toLowerCase())) { - return true; - } - if (['no', 'false', 'disabled', 'disable'].includes(value.toLowerCase())) { - return false; - } - throw new Error('Please enter either yes or no.'); - } - - if (key === 'prefix') { - if (value.length < 1 || value.length > 10 || /\s/.test(value)) { - throw new Error('Prefix must be 1-10 characters with no spaces.'); - } - return value; - } - - return value; -} - -function resolveModalValue(key, interaction) { - if (key === 'logChannelId') { - const channelId = interaction.fields.getField('log_channel')?.values?.[0]; - if (!channelId) { - throw new Error('Please select a log channel.'); - } - return channelId; - } - - if (key === 'modRole') { - const roleId = interaction.fields.getField('mod_role')?.values?.[0]; - if (!roleId) { - throw new Error('Please select a moderator role.'); - } - return roleId; - } - - const rawValue = interaction.fields.getTextInputValue('value'); - return parseValue(key, rawValue); -} - -function buildSuccessMessage(key, value, guild) { - if (key === 'logChannelId') { - const channel = guild?.channels?.cache?.get(value); - return `Log channel set to ${channel ?? `<#${value}>`}.`; - } - - if (key === 'modRole') { - const role = guild?.roles?.cache?.get(value); - return `Moderator role set to ${role ?? `<@&${value}>`}.`; - } - - return `The setting \`${key}\` has been updated successfully.`; -} - -export default { - name: 'config_modal', - async execute(interaction) { - const [key, guildId] = interaction.customId.split(':').slice(1); - - try { - const value = resolveModalValue(key, interaction); - await ConfigService.updateSetting(interaction.client, guildId, key, value, interaction.user.id); - - await interaction.reply({ - embeds: [successEmbed('Configuration Updated', buildSuccessMessage(key, value, interaction.guild))], - flags: MessageFlags.Ephemeral, - }); - } catch (error) { - logger.error('Config modal handler error:', error); - await replyUserError(interaction, { type: ErrorTypes.CONFIGURATION, message: error.message || 'Please try again.' }); - } - }, -}; diff --git a/src/interactions/modals/ticket.js b/src/interactions/modals/ticket.js deleted file mode 100644 index 83eb50a8e5..0000000000 --- a/src/interactions/modals/ticket.js +++ /dev/null @@ -1,6 +0,0 @@ -import { - createTicketModalHandler, - closeTicketModalHandler, -} from '../../handlers/ticketButtons.js'; - -export default [createTicketModalHandler, closeTicketModalHandler]; \ No newline at end of file diff --git a/src/interactions/modals/ticketFeedbackComment.js b/src/interactions/modals/ticketFeedbackComment.js deleted file mode 100644 index 40d27fd369..0000000000 --- a/src/interactions/modals/ticketFeedbackComment.js +++ /dev/null @@ -1,120 +0,0 @@ -import { EmbedBuilder, MessageFlags } from 'discord.js'; -import { getTicketData, saveTicketData } from '../../utils/database.js'; -import { logger } from '../../utils/logger.js'; -import { getColor } from '../../config/bot.js'; -import { logTicketFeedback } from '../../utils/ticketLogging.js'; -import { InteractionHelper } from '../../utils/interactionHelper.js'; - -function buildEmbed(title, description, color) { - return new EmbedBuilder() - .setTitle(title) - .setDescription(description) - .setColor(color); -} - -export default { - name: 'ticket_feedback_comment_modal', - - async execute(interaction, client, args) { - const [guildId, channelId] = args; - - if (!guildId || !channelId) { - await InteractionHelper.safeReply(interaction, { - embeds: [buildEmbed( - '⚠️ Invalid Feedback Submission', - 'This feedback form appears to be malformed.', - getColor('error'), - )], - flags: MessageFlags.Ephemeral, - }); - return; - } - - const comment = interaction.fields.getTextInputValue('feedback_comment')?.trim(); - if (!comment) { - await InteractionHelper.safeReply(interaction, { - embeds: [buildEmbed( - '⚠️ Empty Feedback', - 'Please enter a comment before submitting your feedback.', - getColor('warning'), - )], - flags: MessageFlags.Ephemeral, - }); - return; - } - - const deferred = await InteractionHelper.safeDefer(interaction, { flags: MessageFlags.Ephemeral }); - if (!deferred) { - return; - } - - let ticketData; - try { - ticketData = await getTicketData(guildId, channelId); - } catch (err) { - logger.warn('ticketFeedbackComment: failed to load ticket data', { guildId, channelId, error: err.message }); - } - - if (!ticketData) { - await InteractionHelper.safeEditReply(interaction, { - embeds: [buildEmbed( - '⚠️ Ticket Not Found', - 'Could not find the ticket associated with this feedback.', - getColor('error'), - )], - }); - return; - } - - if (interaction.user.id !== ticketData.userId) { - await InteractionHelper.safeEditReply(interaction, { - embeds: [buildEmbed( - '❌ Not Allowed', - 'Only the ticket creator can submit feedback for this ticket.', - getColor('error'), - )], - }); - return; - } - - ticketData.feedback = { - ...ticketData.feedback, - comment, - commentSubmittedAt: new Date().toISOString(), - }; - - try { - await saveTicketData(guildId, channelId, ticketData); - } catch (err) { - logger.error('ticketFeedbackComment: failed to save feedback', { guildId, channelId, error: err.message }); - } - - try { - await logTicketFeedback({ - client: interaction.client, - guildId, - ticketNumber: ticketData.id, - ticketChannelId: channelId, - userId: interaction.user.id, - rating: ticketData.feedback?.rating ?? null, - comment, - }); - } catch (err) { - logger.warn('ticketFeedbackComment: failed to send log', { guildId, channelId, error: err.message }); - } - - await InteractionHelper.safeEditReply(interaction, { - embeds: [buildEmbed( - '✅ Feedback Submitted', - 'Your written feedback has been recorded. Thank you for helping us improve!', - getColor('success'), - )], - }); - - logger.info('Ticket feedback comment submitted', { - guildId, - channelId, - userId: interaction.user.id, - }); - }, -}; diff --git a/src/interactions/modals/todoShared.js b/src/interactions/modals/todoShared.js deleted file mode 100644 index 8b15ac337c..0000000000 --- a/src/interactions/modals/todoShared.js +++ /dev/null @@ -1,11 +0,0 @@ -import { - sharedTodoAddModalHandler, - sharedTodoCompleteModalHandler, - sharedTodoRemoveModalHandler, -} from '../../handlers/todoButtons.js'; - -export default [ - sharedTodoAddModalHandler, - sharedTodoCompleteModalHandler, - sharedTodoRemoveModalHandler, -]; \ No newline at end of file diff --git a/src/interactions/modals/warningDelete.js b/src/interactions/modals/warningDelete.js deleted file mode 100644 index 55897200ff..0000000000 --- a/src/interactions/modals/warningDelete.js +++ /dev/null @@ -1,20 +0,0 @@ -import { warningDeleteModalHandler, warningClearConfirmModalHandler } from '../../handlers/warningHandlers.js'; - -const deleteExecute = typeof warningDeleteModalHandler === 'function' - ? warningDeleteModalHandler - : warningDeleteModalHandler.execute; - -const clearExecute = typeof warningClearConfirmModalHandler === 'function' - ? warningClearConfirmModalHandler - : warningClearConfirmModalHandler.execute; - -export default [ - { - name: 'warning_delete_modal', - execute: deleteExecute - }, - { - name: 'warning_clear_confirm_modal', - execute: clearExecute - } -]; \ No newline at end of file diff --git a/src/interactions/selectMenus/helpCategory.js b/src/interactions/selectMenus/helpCategory.js deleted file mode 100644 index ce224ea566..0000000000 --- a/src/interactions/selectMenus/helpCategory.js +++ /dev/null @@ -1,3 +0,0 @@ -import { helpCategorySelectMenu } from '../../handlers/helpSelectMenus.js'; - -export default helpCategorySelectMenu; \ No newline at end of file diff --git a/src/interactions/selectMenus/logging.js b/src/interactions/selectMenus/logging.js deleted file mode 100644 index e8396d0616..0000000000 --- a/src/interactions/selectMenus/logging.js +++ /dev/null @@ -1,6 +0,0 @@ -import { handleLoggingMenuSelect } from '../../handlers/loggingButtons.js'; - -export default { - name: 'log_dash_menu', - execute: handleLoggingMenuSelect, -}; diff --git a/src/interactions/selectMenus/reaction_roles.js b/src/interactions/selectMenus/reaction_roles.js deleted file mode 100644 index 8080759d84..0000000000 --- a/src/interactions/selectMenus/reaction_roles.js +++ /dev/null @@ -1,10 +0,0 @@ -import { handleReactionRolesSelectMenu } from '../../handlers/interactionHandlers/reactionRolesSelectMenu.js'; - -export async function execute(interaction, client) { - return handleReactionRolesSelectMenu(interaction, client); -} - -export default { - name: 'reaction_roles', - execute -}; \ No newline at end of file diff --git a/src/interactions/selectMenus/ticketFeedback.js b/src/interactions/selectMenus/ticketFeedback.js deleted file mode 100644 index 5252cae334..0000000000 --- a/src/interactions/selectMenus/ticketFeedback.js +++ /dev/null @@ -1,126 +0,0 @@ -import { EmbedBuilder } from 'discord.js'; -import { getTicketData, saveTicketData } from '../../utils/database.js'; -import { logger } from '../../utils/logger.js'; -import { getColor } from '../../config/bot.js'; -import { logTicketFeedback } from '../../utils/ticketLogging.js'; - -const STAR_LABELS = { - '1': '⭐ 1 — Poor', - '2': '⭐ 2 — Below Average', - '3': '⭐ 3 — Average', - '4': '⭐ 4 — Good', - '5': '⭐ 5 — Excellent', -}; - -export default { - name: 'ticket_feedback', - - async execute(interaction, client, args) { - - const [guildId, channelId] = args; - - if (!guildId || !channelId) { - await interaction.update({ - embeds: [ - new EmbedBuilder() - .setTitle('⚠️ Invalid Feedback Link') - .setDescription('This feedback link appears to be malformed.') - .setColor(getColor('error')), - ], - components: [], - }); - return; - } - - let ticketData; - try { - ticketData = await getTicketData(guildId, channelId); - } catch (err) { - logger.warn('ticketFeedback: failed to load ticket data', { guildId, channelId, error: err.message }); - } - - if (!ticketData) { - await interaction.update({ - embeds: [ - new EmbedBuilder() - .setTitle('⚠️ Ticket Not Found') - .setDescription('Could not find the ticket associated with this survey.') - .setColor(getColor('error')), - ], - components: [], - }); - return; - } - - if (interaction.user.id !== ticketData.userId) { - await interaction.reply({ - embeds: [ - new EmbedBuilder() - .setTitle('❌ Not Allowed') - .setDescription('Only the ticket creator can submit feedback for this ticket.') - .setColor(getColor('error')), - ], - ephemeral: true, - }); - return; - } - - if (ticketData.feedback?.rating) { - await interaction.update({ - embeds: [ - new EmbedBuilder() - .setTitle('✅ Already Submitted') - .setDescription(`You already rated this ticket **${STAR_LABELS[String(ticketData.feedback.rating)]}**.\nThank you for your feedback!`) - .setColor(getColor('success')), - ], - components: [], - }); - return; - } - - const rating = parseInt(interaction.values[0], 10); - const ratingLabel = STAR_LABELS[String(rating)] ?? `${rating} stars`; - - try { - ticketData.feedback = { - rating, - submittedAt: new Date().toISOString(), - }; - await saveTicketData(guildId, channelId, ticketData); - } catch (err) { - logger.error('ticketFeedback: failed to save feedback', { guildId, channelId, rating, error: err.message }); - } - - try { - await logTicketFeedback({ - client: interaction.client, - guildId, - ticketNumber: ticketData.id, - ticketChannelId: channelId, - userId: interaction.user.id, - rating, - }); - } catch (err) { - logger.warn('ticketFeedback: failed to send log', { guildId, channelId, error: err.message }); - } - - const thankYouEmbed = new EmbedBuilder() - .setTitle('✅ Thanks for your feedback!') - .setDescription(`You rated your support experience **${ratingLabel}**.\n\nYour feedback has been recorded and helps us improve!`) - .setColor(getColor('success')) - .setFooter({ text: 'Thank you for using our support system.' }) - .setTimestamp(); - - await interaction.update({ - embeds: [thankYouEmbed], - components: [], - }); - - logger.info('Ticket feedback submitted', { - guildId, - channelId, - userId: interaction.user.id, - rating, - }); - }, -}; \ No newline at end of file diff --git a/src/services/applicationService.js b/src/services/applicationService.js index a7de677548..e322ad4c1a 100644 --- a/src/services/applicationService.js +++ b/src/services/applicationService.js @@ -15,16 +15,10 @@ import { getApplicationRoles, saveApplicationRoles } from '../utils/database.js'; -<<<<<<< HEAD - -const applicationCooldowns = new Map(); -const APPLICATION_SUBMIT_COOLDOWN = 5 * 60 * 1000; -======= import botConfig from '../config/bot.js'; const applicationCooldowns = new Map(); const APPLICATION_SUBMIT_COOLDOWN = (botConfig.applications?.applicationCooldown ?? 24) * 60 * 60 * 1000; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) class ApplicationService { static sanitizeApplicationText(value, maxLength) { diff --git a/src/services/birthdayService.js b/src/services/birthdayService.js index 4d6ac3d314..002665d4eb 100644 --- a/src/services/birthdayService.js +++ b/src/services/birthdayService.js @@ -1,11 +1,7 @@ // birthdayService.js -import { getGuildConfig } from './guildConfig.js'; -<<<<<<< HEAD -import { getGuildBirthdays, setBirthday as dbSetBirthday, deleteBirthday as dbDeleteBirthday, getMonthName } from '../utils/database.js'; -======= +import { getGuildConfig } from './config/guildConfig.js'; import { getGuildBirthdays, setBirthday as dbSetBirthday, deleteBirthday as dbDeleteBirthday, getMonthName, getBirthdayTrackingKey } from '../utils/database.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) import { logger } from '../utils/logger.js'; import { TitanBotError, ErrorTypes } from '../utils/errorHandler.js'; @@ -86,10 +82,6 @@ export async function setBirthday(client, guildId, userId, month, day) { }); return { -<<<<<<< HEAD - success: true, -======= ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) data: { month, day, @@ -171,13 +163,7 @@ export async function deleteBirthday(client, guildId, userId) { if (!birthday) { return { -<<<<<<< HEAD - success: false, - notFound: true, - message: 'No birthday found to remove' -======= status: 'not_found', ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) }; } @@ -198,12 +184,7 @@ export async function deleteBirthday(client, guildId, userId) { }); return { -<<<<<<< HEAD - success: true, - message: 'Birthday removed successfully' -======= status: 'removed', ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) }; } catch (error) { logger.error('Error in deleteBirthday service', { @@ -304,16 +285,10 @@ export async function checkBirthdays(client) { const config = await getGuildConfig(client, guildId); const { birthdayChannelId, birthdayRoleId } = config; -<<<<<<< HEAD - if (!birthdayChannelId || !birthdayRoleId) { - if (process.env.NODE_ENV !== 'production') { - logger.debug(`Skipping birthday check for ${guild.name}: Missing channel or role config.`); -======= // A channel is required for announcements; the birthday role is optional. if (!birthdayChannelId) { if (process.env.NODE_ENV !== 'production') { logger.debug(`Skipping birthday check for ${guild.name}: Missing channel config.`); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } continue; } @@ -321,27 +296,17 @@ export async function checkBirthdays(client) { const channel = await guild.channels.fetch(birthdayChannelId).catch(() => null); if (!channel) continue; -<<<<<<< HEAD - const trackingKey = `bday-role-tracking-${guildId}`; -======= const trackingKey = getBirthdayTrackingKey(guildId); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) const trackingData = (await client.db.get(trackingKey)) || {}; const updatedTrackingData = { ...trackingData }; for (const userId of Object.keys(trackingData)) { try { -<<<<<<< HEAD - const member = await guild.members.fetch(userId).catch(() => null); - if (member && member.roles.cache.has(birthdayRoleId)) { - await member.roles.remove(birthdayRoleId, "Birthday role expired"); -======= if (birthdayRoleId) { const member = await guild.members.fetch(userId).catch(() => null); if (member && member.roles.cache.has(birthdayRoleId)) { await member.roles.remove(birthdayRoleId, "Birthday role expired"); } ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } delete updatedTrackingData[userId]; } catch (error) { @@ -353,26 +318,14 @@ export async function checkBirthdays(client) { await client.db.set(trackingKey, updatedTrackingData); } -<<<<<<< HEAD - const birthdaysKey = `birthdays:${guildId}`; - const birthdays = (await client.db.get(birthdaysKey)) || {}; -======= // Use the canonical birthday storage (guild::birthdays) that set/remove commands write to. const birthdays = (await getGuildBirthdays(client, guildId)) || {}; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) const birthdayMembers = []; for (const [userId, userData] of Object.entries(birthdays)) { if (userData.month === currentMonth && userData.day === currentDay) { const member = await guild.members.fetch(userId).catch(() => null); if (member) { birthdayMembers.push(member); -<<<<<<< HEAD - try { - await member.roles.add(birthdayRoleId, "Happy Birthday! 🎉"); - updatedTrackingData[userId] = true; - } catch (error) { - logger.error(`Error adding birthday role to ${member.user.tag}:`, error); -======= if (birthdayRoleId) { try { await member.roles.add(birthdayRoleId, "Happy Birthday! 🎉"); @@ -380,7 +333,6 @@ export async function checkBirthdays(client) { } catch (error) { logger.error(`Error adding birthday role to ${member.user.tag}:`, error); } ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } } } diff --git a/src/services/commandAccessService.js b/src/services/commandAccessService.js index 2ef351f3d2..122b08f416 100644 --- a/src/services/commandAccessService.js +++ b/src/services/commandAccessService.js @@ -1,16 +1,12 @@ // commandAccessService.js -import { getGuildConfig, updateGuildConfig } from './guildConfig.js'; +import { getGuildConfig, updateGuildConfig } from './config/guildConfig.js'; import { normalizeCategoryKey, formatCategoryName, getCategoryIcon, PROTECTED_COMMANDS, -<<<<<<< HEAD -} from '../config/commandCategories.js'; -======= } from '../config/commands/commandCategories.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) function normalizeToggleRecord(raw) { if (!raw) { diff --git a/src/services/configService.js b/src/services/configService.js deleted file mode 100644 index c7984e7678..0000000000 --- a/src/services/configService.js +++ /dev/null @@ -1,628 +0,0 @@ -// configService.js - -import { logger } from '../utils/logger.js'; -import { getGuildConfig, setGuildConfig } from './guildConfig.js'; -import { PermissionFlagsBits } from 'discord.js'; -import { createError, ErrorTypes } from '../utils/errorHandler.js'; -import { wrapServiceClassMethods } from '../utils/serviceErrorBoundary.js'; -import { z } from 'zod'; -import { LogIgnoreSchema, LoggingConfigSchema } from '../utils/schemas.js'; - -const configChangeHistory = new Map(); -const CONFIG_HISTORY_LIMIT = 100; - -const CONFIG_VALIDATION_RULES = { - logChannelId: { type: 'channel', required: false }, - reportChannelId: { type: 'channel', required: false }, - premiumRoleId: { type: 'role', required: false }, - autoRole: { type: 'role', required: false }, - modRole: { type: 'role', required: false }, - adminRole: { type: 'role', required: false }, - prefix: { type: 'string', required: false, maxLength: 10, minLength: 1 }, - dmOnClose: { type: 'boolean', required: false }, - maxTicketsPerUser: { type: 'number', required: false, min: 1, max: 50 }, - birthdayChannelId: { type: 'channel', required: false }, - logIgnore: { type: 'object', required: false }, - logging: { type: 'object', required: false } -}; - -const SETTING_CONFLICTS = { - 'birthdayChannelId': [], - 'logging': [], -}; - -const LEGACY_LOGGING_KEY_MAP = { - logChannelId: 'audit', - reportChannelId: 'reports', -}; - -const ConfigValueSchemas = Object.freeze({ - logChannelId: z.union([z.string().min(1), z.object({ id: z.string().min(1) }), z.null()]), - reportChannelId: z.union([z.string().min(1), z.object({ id: z.string().min(1) }), z.null()]), - premiumRoleId: z.union([z.string().min(1), z.object({ id: z.string().min(1) })]), - autoRole: z.union([z.string().min(1), z.object({ id: z.string().min(1) })]), - modRole: z.union([z.string().min(1), z.object({ id: z.string().min(1) })]), - adminRole: z.union([z.string().min(1), z.object({ id: z.string().min(1) })]), - prefix: z.string().min(1).max(10), - dmOnClose: z.boolean(), - maxTicketsPerUser: z.number().int().min(1).max(50), - birthdayChannelId: z.union([z.string().min(1), z.object({ id: z.string().min(1) })]), - logIgnore: LogIgnoreSchema, - logging: LoggingConfigSchema, -}); - -class ConfigService { - - static MAX_CHANNEL_IDS = 10; - static MAX_ROLE_IDS = 20; - static MAX_PREFIX_LENGTH = 10; - static PROTECTED_SETTINGS = ['_id', 'guildId', 'createdAt']; - static UNSAFE_KEYS = ['__proto__', 'prototype', 'constructor']; - - static applyLoggingLegacyKey(config, key, value, previousConfig = {}) { - if (key === 'logIgnore') { - const logging = { - ...(previousConfig.logging || config.logging || {}), - ignore: value, - }; - const next = { ...config, logging }; - delete next.logIgnore; - return next; - } - - const destination = LEGACY_LOGGING_KEY_MAP[key]; - if (!destination) { - return config; - } - - const channelId = value && typeof value === 'object' ? value.id : value; - const logging = { - ...(previousConfig.logging || config.logging || {}), - channels: { - ...((previousConfig.logging || config.logging || {}).channels || {}), - [destination]: channelId ?? null, - }, - enabled: channelId ? true : (previousConfig.logging?.enabled ?? config.logging?.enabled ?? false), - }; - - const next = { ...config, logging }; - delete next[key]; - if (key === 'logChannelId') { - delete next.enableLogging; - } - if (key === 'reportChannelId') { - delete next.reportChannelId; - } - return next; - } - - static validateConfigKeySafety(key) { - if (typeof key !== 'string' || key.trim().length === 0) { - throw createError( - 'Invalid setting key', - ErrorTypes.VALIDATION, - 'Setting key must be a non-empty string.', - { key } - ); - } - - if (this.UNSAFE_KEYS.includes(key)) { - throw createError( - 'Unsafe setting key', - ErrorTypes.VALIDATION, - 'This setting key is not allowed for security reasons.', - { key } - ); - } - } - - static async validateConfigValue(key, value, guild) { - logger.debug(`[CONFIG_SERVICE] Validating config value`, { key, type: typeof value }); - - const rule = CONFIG_VALIDATION_RULES[key]; - - if (!rule) { - logger.warn(`[CONFIG_SERVICE] No validation rule for key: ${key}`); - return true; - } - - if (rule.required === false && (value === null || value === undefined)) { - return true; - } - - const zodSchema = ConfigValueSchemas[key]; - if (zodSchema) { - const parsed = zodSchema.safeParse(value); - if (!parsed.success) { - throw createError( - 'Invalid configuration value', - ErrorTypes.VALIDATION, - 'Provided configuration value is invalid.', - { - key, - errorCode: 'VALIDATION_FAILED', - issues: parsed.error.issues.map((issue) => ({ - path: issue.path.join('.'), - message: issue.message, - code: issue.code - })) - } - ); - } - } - - if (rule.type === 'channel') { - if (typeof value !== 'string' && typeof value !== 'object') { - throw createError( - 'Invalid channel', - ErrorTypes.VALIDATION, - 'Channel ID must be a string.', - { key, provided: typeof value } - ); - } - - const channelId = typeof value === 'string' ? value : value.id; - const channel = guild.channels.cache.get(channelId); - - if (!channel) { - throw createError( - 'Channel not found', - ErrorTypes.VALIDATION, - 'The specified channel does not exist.', - { key, channelId } - ); - } - - if (!channel.isTextBased?.()) { - throw createError( - 'Invalid channel type', - ErrorTypes.VALIDATION, - 'Only text channels are allowed.', - { key, channelId, channelType: channel.type } - ); - } - - return true; - } - - if (rule.type === 'role') { - if (typeof value !== 'string' && typeof value !== 'object') { - throw createError( - 'Invalid role', - ErrorTypes.VALIDATION, - 'Role ID must be a string.', - { key, provided: typeof value } - ); - } - - const roleId = typeof value === 'string' ? value : value.id; - const role = guild.roles.cache.get(roleId); - - if (!role) { - throw createError( - 'Role not found', - ErrorTypes.VALIDATION, - 'The specified role does not exist.', - { key, roleId } - ); - } - - const botHighestRole = guild.members.me?.roles.highest; - if (role.position >= botHighestRole?.position) { - throw createError( - 'Role too high', - ErrorTypes.VALIDATION, - "Can't set roles higher than my highest role.", - { key, roleId, rolePosition: role.position } - ); - } - - return true; - } - - if (rule.type === 'string') { - if (typeof value !== 'string') { - throw createError( - 'Invalid value type', - ErrorTypes.VALIDATION, - 'Value must be a string.', - { key, provided: typeof value } - ); - } - - const length = value.length; - if (rule.maxLength && length > rule.maxLength) { - throw createError( - 'Value too long', - ErrorTypes.VALIDATION, - `Value cannot exceed **${rule.maxLength}** characters.`, - { key, current: length, max: rule.maxLength } - ); - } - - if (rule.minLength && length < rule.minLength) { - throw createError( - 'Value too short', - ErrorTypes.VALIDATION, - `Value must be at least **${rule.minLength}** character(s).`, - { key, current: length, min: rule.minLength } - ); - } - - return true; - } - - if (rule.type === 'number') { - if (typeof value !== 'number') { - throw createError( - 'Invalid value type', - ErrorTypes.VALIDATION, - 'Value must be a number.', - { key, provided: typeof value } - ); - } - - if (rule.min !== undefined && value < rule.min) { - throw createError( - 'Value too low', - ErrorTypes.VALIDATION, - `Value must be at least **${rule.min}**.`, - { key, value, min: rule.min } - ); - } - - if (rule.max !== undefined && value > rule.max) { - throw createError( - 'Value too high', - ErrorTypes.VALIDATION, - `Value cannot exceed **${rule.max}**.`, - { key, value, max: rule.max } - ); - } - - return true; - } - - if (rule.type === 'boolean') { - if (typeof value !== 'boolean') { - throw createError( - 'Invalid value type', - ErrorTypes.VALIDATION, - 'Value must be true or false.', - { key, provided: typeof value } - ); - } - - return true; - } - - if (rule.type === 'object') { - if (typeof value !== 'object' || value === null) { - throw createError( - 'Invalid value type', - ErrorTypes.VALIDATION, - 'Value must be an object.', - { key, provided: typeof value } - ); - } - - return true; - } - - return true; - } - - static detectConflicts(currentConfig, key, value) { - logger.debug(`[CONFIG_SERVICE] Checking for config conflicts`, { key }); - - const conflicts = []; - const relatedSettings = SETTING_CONFLICTS[key] || []; - - for (const related of relatedSettings) { - if (related === 'logging' && value === null) { - - if (currentConfig.logging?.enabled) { - conflicts.push( - `Disabling log channel but logging system is still enabled. Consider disabling logging first.` - ); - } - } - } - - return conflicts; - } - - static async updateSetting(client, guildId, key, value, adminId) { - logger.info(`[CONFIG_SERVICE] Updating setting`, { - guildId, - key, - adminId, - valueType: typeof value - }); - - this.validateConfigKeySafety(key); - - if (this.PROTECTED_SETTINGS.includes(key)) { - logger.warn(`[CONFIG_SERVICE] Attempted to modify protected setting`, { - key, - guildId, - adminId - }); - throw createError( - 'Protected setting', - ErrorTypes.VALIDATION, - `The setting **${key}** cannot be modified.`, - { key } - ); - } - - const guild = client.guilds.cache.get(guildId); - if (!guild) { - throw createError( - 'Guild not found', - ErrorTypes.VALIDATION, - 'Guild does not exist.', - { guildId } - ); - } - - await this.validateConfigValue(key, value, guild); - - const currentConfig = await getGuildConfig(client, guildId); - - const conflicts = this.detectConflicts(currentConfig, key, value); - if (conflicts.length > 0) { - logger.warn(`[CONFIG_SERVICE] Config conflicts detected`, { - guildId, - key, - conflicts - }); - - } - - const oldValue = currentConfig[key]; - - let updatedConfig = { ...currentConfig, [key]: value }; - updatedConfig = this.applyLoggingLegacyKey(updatedConfig, key, value, currentConfig); - - await setGuildConfig(client, guildId, updatedConfig); - - this.recordChange(guildId, { - key, - oldValue, - newValue: value, - changedBy: adminId, - timestamp: new Date().toISOString(), - conflicts - }); - - logger.info(`[CONFIG_SERVICE] Setting updated successfully`, { - guildId, - key, - adminId, - oldValue: typeof oldValue === 'string' ? oldValue.substring(0, 50) : oldValue, - newValue: typeof value === 'string' ? value.substring(0, 50) : value, - hasConflicts: conflicts.length > 0, - timestamp: new Date().toISOString() - }); - - return { - success: true, - key, - oldValue, - newValue: value, - conflicts - }; - } - - static async bulkUpdate(client, guildId, updates, adminId) { - logger.info(`[CONFIG_SERVICE] Bulk updating settings`, { - guildId, - updateCount: Object.keys(updates).length, - adminId - }); - - const guild = client.guilds.cache.get(guildId); - if (!guild) { - throw createError( - 'Guild not found', - ErrorTypes.VALIDATION, - 'Guild does not exist.', - { guildId } - ); - } - - const validatedUpdates = {}; - const validationErrors = []; - - for (const [key, value] of Object.entries(updates)) { - try { - this.validateConfigKeySafety(key); - - if (this.PROTECTED_SETTINGS.includes(key)) { - validationErrors.push(`${key}: Protected setting cannot be modified`); - continue; - } - - await this.validateConfigValue(key, value, guild); - validatedUpdates[key] = value; - } catch (error) { - validationErrors.push(`${key}: ${error.details?.message || error.message}`); - } - } - - if (validationErrors.length > 0) { - logger.warn(`[CONFIG_SERVICE] Bulk update validation failed`, { - guildId, - errors: validationErrors - }); - throw createError( - 'Validation failed', - ErrorTypes.VALIDATION, - `Some settings failed validation:\n• ${validationErrors.join('\n• ')}`, - { errors: validationErrors } - ); - } - - const currentConfig = await getGuildConfig(client, guildId); - - const updatedConfig = { ...currentConfig, ...validatedUpdates }; - await setGuildConfig(client, guildId, updatedConfig); - - for (const [key, value] of Object.entries(validatedUpdates)) { - this.recordChange(guildId, { - key, - oldValue: currentConfig[key], - newValue: value, - changedBy: adminId, - isBulkUpdate: true, - timestamp: new Date().toISOString() - }); - } - - logger.info(`[CONFIG_SERVICE] Bulk update completed`, { - guildId, - adminId, - appliedCount: Object.keys(validatedUpdates).length, - failedCount: validationErrors.length, - timestamp: new Date().toISOString() - }); - - return { - success: true, - applied: Object.keys(validatedUpdates), - failed: validationErrors, - appliedCount: Object.keys(validatedUpdates).length, - failedCount: validationErrors.length - }; - } - - static recordChange(guildId, changeData) { - if (!configChangeHistory.has(guildId)) { - configChangeHistory.set(guildId, []); - } - - const history = configChangeHistory.get(guildId); - history.push(changeData); - - if (history.length > CONFIG_HISTORY_LIMIT) { - history.shift(); - } - - logger.debug(`[CONFIG_SERVICE] Change recorded for audit trail`, { - guildId, - key: changeData.key, - historySize: history.length - }); - } - - static getChangeHistory(guildId, limit = 20) { - const history = configChangeHistory.get(guildId) || []; - return history.slice(-limit).reverse(); - } - - static async resetSetting(client, guildId, key, adminId) { - logger.info(`[CONFIG_SERVICE] Resetting setting`, { - guildId, - key, - adminId - }); - - const currentConfig = await getGuildConfig(client, guildId); - const oldValue = currentConfig[key]; - - const defaultValue = null; - - const updatedConfig = { ...currentConfig, [key]: defaultValue }; - await setGuildConfig(client, guildId, updatedConfig); - - this.recordChange(guildId, { - key, - oldValue, - newValue: defaultValue, - changedBy: adminId, - isReset: true, - timestamp: new Date().toISOString() - }); - - logger.info(`[CONFIG_SERVICE] Setting reset successfully`, { - guildId, - key, - adminId, - oldValue, - timestamp: new Date().toISOString() - }); - - return { - success: true, - key, - oldValue, - newValue: defaultValue - }; - } - - static async getConfigSummary(client, guildId) { - logger.debug(`[CONFIG_SERVICE] Fetching config summary`, { guildId }); - - const config = await getGuildConfig(client, guildId); - const guild = client.guilds.cache.get(guildId); - - if (!guild) { - throw createError( - 'Guild not found', - ErrorTypes.VALIDATION, - 'Guild does not exist.', - { guildId } - ); - } - - const summary = {}; - - for (const [key, value] of Object.entries(config)) { - if (this.PROTECTED_SETTINGS.includes(key)) continue; - - const rule = CONFIG_VALIDATION_RULES[key]; - if (!rule) continue; - - if (rule.type === 'channel' && value) { - const channel = guild.channels.cache.get(value); - summary[key] = { - id: value, - name: channel?.name || 'Unknown', - status: channel ? 'Valid' : 'Missing' - }; - } else if (rule.type === 'role' && value) { - const role = guild.roles.cache.get(value); - summary[key] = { - id: value, - name: role?.name || 'Unknown', - status: role ? 'Valid' : 'Missing' - }; - } else { - summary[key] = value; - } - } - - return { - guildId, - settings: summary, - recordedAt: new Date().toISOString() - }; - } - - static verifyPermission(member) { - return member.permissions.has([ - PermissionFlagsBits.Administrator, - PermissionFlagsBits.ManageGuild - ]); - } -} - -wrapServiceClassMethods(ConfigService, (methodName) => ({ - service: 'ConfigService', - operation: methodName, - message: `Configuration service operation failed: ${methodName}`, - userMessage: 'A configuration operation failed. Please try again in a moment.' -})); - -export default ConfigService; \ No newline at end of file diff --git a/src/services/economyService.js b/src/services/economyService.js index 95ca2474fe..9203754280 100644 --- a/src/services/economyService.js +++ b/src/services/economyService.js @@ -204,10 +204,6 @@ class EconomyService { }); return { -<<<<<<< HEAD - success: true, -======= ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) senderNewBalance: senderData.wallet, receiverNewBalance: receiverData.wallet }; diff --git a/src/services/giveawayService.js b/src/services/giveawayService.js index 626aa5cd54..7f07fff939 100644 --- a/src/services/giveawayService.js +++ b/src/services/giveawayService.js @@ -3,19 +3,12 @@ import { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, MessageFlags } from 'discord.js'; import { logger } from '../utils/logger.js'; import { TitanBotError, ErrorTypes } from '../utils/errorHandler.js'; -<<<<<<< HEAD -import { getColor } from '../config/bot.js'; -======= import { getColor, botConfig } from '../config/bot.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) import { getEndedGiveaways, markGiveawayEnded } from '../utils/database.js'; import { checkRateLimit, getRateLimitStatus } from '../utils/rateLimiter.js'; import { logEvent, EVENT_TYPES } from './loggingService.js'; -<<<<<<< HEAD -======= const GIVEAWAY_CONFIG = botConfig.giveaways || {}; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) const GIVEAWAY_INTERACTION_COOLDOWN = 1000; function getGiveawayInteractionKey(userId, giveawayId) { @@ -79,38 +72,22 @@ export function parseDuration(durationString) { ); } -<<<<<<< HEAD - const maxDuration = 30 * 24 * 60 * 60 * 1000; -======= const maxDuration = GIVEAWAY_CONFIG.maximumDuration ?? 30 * 24 * 60 * 60 * 1000; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) if (ms > maxDuration) { throw new TitanBotError( `Duration exceeds maximum: ${ms}ms > ${maxDuration}ms`, ErrorTypes.VALIDATION, -<<<<<<< HEAD - 'Maximum duration is 30 days.', -======= `Maximum duration is ${Math.floor(maxDuration / (24 * 60 * 60 * 1000))} days.`, ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) { requestedMs: ms, maxMs: maxDuration } ); } -<<<<<<< HEAD - const minDuration = 10 * 1000; -======= const minDuration = GIVEAWAY_CONFIG.minimumDuration ?? 10 * 1000; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) if (ms < minDuration) { throw new TitanBotError( `Duration below minimum: ${ms}ms < ${minDuration}ms`, ErrorTypes.VALIDATION, -<<<<<<< HEAD - 'Minimum duration is 10 seconds.', -======= `Minimum duration is ${Math.ceil(minDuration / 1000)} seconds.`, ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) { requestedMs: ms, minMs: minDuration } ); } @@ -142,14 +119,6 @@ export function validatePrize(prize) { } export function validateWinnerCount(winnerCount) { -<<<<<<< HEAD - if (!Number.isInteger(winnerCount) || winnerCount < 1 || winnerCount > 10) { - throw new TitanBotError( - `Invalid winner count: ${winnerCount}`, - ErrorTypes.VALIDATION, - 'Winner count must be between 1 and 10.', - { winnerCount } -======= const minimumWinners = GIVEAWAY_CONFIG.minimumWinners ?? 1; const maximumWinners = GIVEAWAY_CONFIG.maximumWinners ?? 10; @@ -159,7 +128,6 @@ export function validateWinnerCount(winnerCount) { ErrorTypes.VALIDATION, `Winner count must be between ${minimumWinners} and ${maximumWinners}.`, { winnerCount, minimumWinners, maximumWinners } ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) ); } } @@ -337,10 +305,6 @@ export async function endGiveaway(client, giveaway, guildId, endedBy) { logger.info(`Ending giveaway ${giveaway.messageId}: selected ${winners.length} winners from ${participants.length} entries`); return { -<<<<<<< HEAD - success: true, -======= ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) giveaway: updatedGiveaway, winners: winners, participantCount: participants.length diff --git a/src/services/guildConfig.js b/src/services/guildConfig.js deleted file mode 100644 index ef991df319..0000000000 --- a/src/services/guildConfig.js +++ /dev/null @@ -1,78 +0,0 @@ -// guildConfig.js - -import { getGuildConfig as getGuildConfigDb, setGuildConfig as setGuildConfigDb } from '../utils/database.js'; -import { BotConfig } from '../config/bot.js'; -import { normalizeGuildConfig, validateGuildConfigOrThrow } from '../utils/schemas.js'; -import { wrapServiceBoundary } from '../utils/serviceErrorBoundary.js'; - -const GUILD_CONFIG_DEFAULTS = { - prefix: BotConfig.prefix, - modRole: null, - adminRole: null, - welcomeChannel: null, - welcomeMessage: 'Welcome {user} to {server}!', - autoRole: null, - dmOnClose: true, - disabledCommands: {}, - disabledCategories: {}, - logging: { - enabled: false, - channels: { audit: null, applications: null, reports: null }, - ignore: { users: [], channels: [] }, - enabledEvents: {}, - }, -}; - -export const getGuildConfig = wrapServiceBoundary(async function getGuildConfig(client, guildId, context = {}) { - const config = await getGuildConfigDb(client, guildId, context); - - return normalizeGuildConfig(config, GUILD_CONFIG_DEFAULTS); -}, { - service: 'guildConfigService', - operation: 'getGuildConfig', - message: 'Failed to fetch guild configuration', - userMessage: 'Failed to load server configuration. Please try again.' -}); - -export const setGuildConfig = wrapServiceBoundary(async function setGuildConfig(client, guildId, config, context = {}) { - const normalized = normalizeGuildConfig(config, GUILD_CONFIG_DEFAULTS); - const validated = validateGuildConfigOrThrow(normalized, { guildId, ...context }); - return await setGuildConfigDb(client, guildId, validated, context); -}, { - service: 'guildConfigService', - operation: 'setGuildConfig', - message: 'Failed to save guild configuration', - userMessage: 'Failed to save server configuration. Please try again.' -}); - -export const updateGuildConfig = wrapServiceBoundary(async function updateGuildConfig(client, guildId, updates, context = {}) { - const currentConfig = await getGuildConfigDb(client, guildId, context); - const newConfig = { ...currentConfig, ...updates }; - const normalized = normalizeGuildConfig(newConfig, GUILD_CONFIG_DEFAULTS); - const validated = validateGuildConfigOrThrow(normalized, { guildId, ...context }); - return await setGuildConfigDb(client, guildId, validated, context); -}, { - service: 'guildConfigService', - operation: 'updateGuildConfig', - message: 'Failed to update guild configuration', - userMessage: 'Failed to update server configuration. Please try again.' -}); - -export const getConfigValue = wrapServiceBoundary(async function getConfigValue(client, guildId, key, defaultValue = null, context = {}) { - const config = await getGuildConfig(client, guildId, context); - return config[key] !== undefined ? config[key] : defaultValue; -}, { - service: 'guildConfigService', - operation: 'getConfigValue', - message: 'Failed to read guild configuration value', - userMessage: 'Failed to read a server setting. Please try again.' -}); - -export const setConfigValue = wrapServiceBoundary(async function setConfigValue(client, guildId, key, value, context = {}) { - return await updateGuildConfig(client, guildId, { [key]: value }, context); -}, { - service: 'guildConfigService', - operation: 'setConfigValue', - message: 'Failed to update guild configuration value', - userMessage: 'Failed to update a server setting. Please try again.' -}); \ No newline at end of file diff --git a/src/services/joinToCreateService.js b/src/services/joinToCreateService.js index 60a537599d..a39318716f 100644 --- a/src/services/joinToCreateService.js +++ b/src/services/joinToCreateService.js @@ -10,11 +10,7 @@ import { import { logger } from '../utils/logger.js'; import { TitanBotError, ErrorTypes } from '../utils/errorHandler.js'; import { logEvent, EVENT_TYPES } from './loggingService.js'; -<<<<<<< HEAD -import { formatLogLine } from '../utils/logEmbeds.js'; -======= import { formatLogLine } from '../utils/logging/logEmbeds.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) import { ChannelType, PermissionFlagsBits } from 'discord.js'; const CHANNEL_NAME_MAX_LENGTH = 100; diff --git a/src/services/levelRoleSyncService.js b/src/services/levelRoleSyncService.js deleted file mode 100644 index 18f6e76230..0000000000 --- a/src/services/levelRoleSyncService.js +++ /dev/null @@ -1,108 +0,0 @@ -import { logger } from '../utils/logger.js'; -import { getLevelingConfig, getUserLevelData, saveLevelingConfig } from './leveling.js'; - -async function listLevelUserIds(client, guildId) { - if (!client.db?.list) return []; - - const prefixes = [`${guildId}:leveling:users:`, `guild:${guildId}:leveling:users:`]; - const userIds = new Set(); - - for (const prefix of prefixes) { - let keys = await client.db.list(prefix).catch(() => []); - if (!Array.isArray(keys)) { - keys = typeof keys === 'object' && keys !== null ? Object.keys(keys) : []; - } - - for (const key of keys) { - if (!key.startsWith(prefix)) continue; - const userId = key.slice(prefix.length); - if (/^\d{17,19}$/.test(userId)) userIds.add(userId); - } - } - - return [...userIds]; -} - -async function tryAwardRole(member, roleId, level) { - const role = member.guild.roles.cache.get(roleId) || (await member.guild.roles.fetch(roleId).catch(() => null)); - if (!role || member.roles.cache.has(roleId)) return false; - - await member.roles.add(role, `Level ${level} reward (startup sync)`); - return true; -} - -export async function reconcileLevelRoles(client, guildId = null) { - const summary = { - scannedGuilds: 0, - prunedRewardEntries: 0, - rolesReAwarded: 0, - errors: 0, - }; - - const guilds = guildId - ? [client.guilds.cache.get(guildId)].filter(Boolean) - : [...client.guilds.cache.values()]; - - for (const guild of guilds) { - summary.scannedGuilds += 1; - - try { - const cfg = await getLevelingConfig(client, guild.id); - if (cfg.enabled === false) continue; - - const rewards = { ...(cfg.roleRewards || {}) }; - if (Object.keys(rewards).length === 0) continue; - - let configChanged = false; - - for (const [level, roleId] of Object.entries(rewards)) { - const role = - guild.roles.cache.get(roleId) || (await guild.roles.fetch(roleId).catch(() => null)); - if (!role) { - delete rewards[level]; - configChanged = true; - summary.prunedRewardEntries += 1; - logger.warn( - `Removed missing level ${level} reward role ${roleId} from config in guild ${guild.id}`, - ); - } - } - - if (configChanged) { - cfg.roleRewards = rewards; - await saveLevelingConfig(client, guild.id, cfg); - } - - if (Object.keys(rewards).length === 0) continue; - - const userIds = await listLevelUserIds(client, guild.id); - - for (const userId of userIds) { - const levelData = await getUserLevelData(client, guild.id, userId); - const member = await guild.members.fetch(userId).catch(() => null); - if (!member) continue; - - for (const [levelStr, roleId] of Object.entries(rewards)) { - const requiredLevel = Number(levelStr); - if (!Number.isFinite(requiredLevel) || levelData.level < requiredLevel) continue; - - try { - const awarded = await tryAwardRole(member, roleId, requiredLevel); - if (awarded) summary.rolesReAwarded += 1; - } catch (awardError) { - summary.errors += 1; - logger.warn( - `Could not re-award level ${requiredLevel} role to ${userId} in guild ${guild.id}:`, - awardError.message, - ); - } - } - } - } catch (error) { - summary.errors += 1; - logger.warn(`Level role sync failed for guild ${guild.id}:`, error.message); - } - } - - return summary; -} diff --git a/src/services/leveling.js b/src/services/leveling.js deleted file mode 100644 index 13bd26f73e..0000000000 --- a/src/services/leveling.js +++ /dev/null @@ -1,459 +0,0 @@ -// leveling.js - -import { EmbedBuilder } from 'discord.js'; -import { logger } from '../utils/logger.js'; -import { getGuildConfig, setGuildConfig } from '../services/guildConfig.js'; -import { TitanBotError, ErrorTypes } from '../utils/errorHandler.js'; -import { addXp } from './xpSystem.js'; - -const BASE_XP = 100; -const XP_MULTIPLIER = 1.5; -const MAX_LEVEL = 1000; -const MIN_LEVEL = 0; - -export function getXpForLevel(level) { - if (!Number.isInteger(level) || level < 0 || level > MAX_LEVEL) { - throw new TitanBotError( - `Invalid level: ${level}. Must be between ${MIN_LEVEL} and ${MAX_LEVEL}`, - ErrorTypes.VALIDATION, - 'The level must be a valid number.' - ); - } - return 5 * Math.pow(level, 2) + 50 * level + 50; -} - -export function getLevelFromXp(xp) { - if (!Number.isInteger(xp) || xp < 0) { - throw new TitanBotError( - `Invalid XP: ${xp}`, - ErrorTypes.VALIDATION, - 'XP must be a non-negative number.' - ); - } - - let level = 0; - let xpNeeded = 0; - - while (xp >= getXpForLevel(level) && level < MAX_LEVEL) { - xpNeeded = getXpForLevel(level); - xp -= xpNeeded; - level++; - } - - return { - level: Math.min(level, MAX_LEVEL), - currentXp: xp, - xpNeeded: getXpForLevel(Math.min(level, MAX_LEVEL)) - }; -} - -export function calculateTotalXp(level, currentXp = 0) { - let total = currentXp; - for (let i = 0; i < level; i++) { - total += getXpForLevel(i); - } - return total; -} - -export async function getLeaderboard(client, guildId, limit = 10) { - try { - - if (!guildId || typeof guildId !== 'string') { - throw new TitanBotError( - 'Invalid guild ID', - ErrorTypes.VALIDATION, - 'Guild ID is required.' - ); - } - - if (!Number.isInteger(limit) || limit < 1 || limit > 100) { - limit = Math.min(Math.max(limit, 1), 100); - } - - const guild = client.guilds.cache.get(guildId); - if (!guild) { - logger.warn(`Guild ${guildId} not found in cache`); - return []; - } - - const members = await guild.members.fetch().catch(error => { - logger.error(`Failed to fetch members for guild ${guildId}:`, error); - return new Map(); - }); - - const leaderboard = []; - - for (const [userId, member] of members) { - if (member.user.bot) continue; - - const data = await getUserLevelData(client, guildId, userId); - if (data && (data.totalXp > 0 || data.level > 0)) { - leaderboard.push({ - userId, - username: member.user.username, - discriminator: member.user.discriminator, - ...data - }); - } - } - - leaderboard.sort((a, b) => b.totalXp - a.totalXp); - - leaderboard.forEach((entry, index) => { - entry.rank = index + 1; - }); - - return leaderboard.slice(0, limit); - - } catch (error) { - logger.error('Error getting leaderboard:', error); - if (error instanceof TitanBotError) throw error; - throw new TitanBotError( - `Failed to fetch leaderboard: ${error.message}`, - ErrorTypes.DATABASE, - 'Could not fetch the leaderboard at this time.' - ); - } -} - -export function createLeaderboardEmbed(leaderboard, guild) { - const embed = new EmbedBuilder() - .setTitle(`🏆 ${guild.name} Leaderboard`) - .setColor('#2ecc71') - .setTimestamp(); - - if (!leaderboard || leaderboard.length === 0) { - embed.setDescription('No users on the leaderboard yet!'); - return embed; - } - - const top3 = leaderboard.slice(0, 3); - const rest = leaderboard.slice(3); - - const top3Text = top3.map((user, index) => { - const medal = ['🥇', '🥈', '🥉'][index]; - return `${medal} **#${user.rank}** ${user.username} - Level ${user.level} (${user.totalXp} XP)`; - }).join('\n'); - - const restText = rest.map(user => { - return `**#${user.rank}** ${user.username} - Level ${user.level} (${user.totalXp} XP)`; - }).join('\n'); - - embed.setDescription( - `**Top Members**\n${top3Text}${restText ? '\n\n' + restText : ''}` - ); - - return embed; -} - -export async function getLevelingConfig(client, guildId) { - try { - const guildConfig = await getGuildConfig(client, guildId); - return guildConfig.leveling || { - enabled: true, - xpPerMessage: { min: 15, max: 25 }, - xpCooldown: 20, - levelUpMessage: '{user} has leveled up to level {level}!', - levelUpChannel: null, - ignoredChannels: [], - ignoredRoles: [], - blacklistedUsers: [], - roleRewards: {}, - announceLevelUp: true, - xpMultiplier: 1 - }; - } catch (error) { - logger.error(`Error getting leveling config for guild ${guildId}:`, error); - return { - enabled: true, - xpPerMessage: { min: 15, max: 25 }, - xpCooldown: 20, - levelUpMessage: '{user} has leveled up to level {level}!', - levelUpChannel: null, - ignoredChannels: [], - ignoredRoles: [], - blacklistedUsers: [], - roleRewards: {}, - announceLevelUp: true, - xpMultiplier: 1 - }; - } -} - -export async function getUserLevelData(client, guildId, userId) { - try { - if (!guildId || !userId) { - throw new TitanBotError( - 'Guild ID and User ID are required', - ErrorTypes.VALIDATION - ); - } - - const key = `${guildId}:leveling:users:${userId}`; - const data = await client.db.get(key); - - if (!data) { - return { - xp: 0, - level: 0, - totalXp: 0, - lastMessage: 0, - rank: 0 - }; - } - - return { - xp: Math.max(0, data.xp || 0), - level: Math.max(0, Math.min(data.level || 0, MAX_LEVEL)), - totalXp: Math.max(0, data.totalXp || 0), - lastMessage: data.lastMessage || 0, - rank: data.rank || 0 - }; - } catch (error) { - logger.error(`Error getting user level data for ${userId}:`, error); - if (error instanceof TitanBotError) throw error; - throw new TitanBotError( - `Failed to fetch user data: ${error.message}`, - ErrorTypes.DATABASE, - 'Could not fetch level data at this time.' - ); - } -} - -export async function saveUserLevelData(client, guildId, userId, data) { - try { - if (!guildId || !userId) { - throw new TitanBotError( - 'Guild ID and User ID are required', - ErrorTypes.VALIDATION - ); - } - - if (!data || typeof data !== 'object') { - throw new TitanBotError( - 'Invalid user level data', - ErrorTypes.VALIDATION - ); - } - - const sanitizedData = { - xp: Math.max(0, Number(data.xp) || 0), - level: Math.max(0, Math.min(Number(data.level) || 0, MAX_LEVEL)), - totalXp: Math.max(0, Number(data.totalXp) || 0), - lastMessage: Number(data.lastMessage) || 0, - rank: Number(data.rank) || 0 - }; - - const key = `${guildId}:leveling:users:${userId}`; - await client.db.set(key, sanitizedData); - } catch (error) { - logger.error(`Error saving user level data for ${userId}:`, error); - if (error instanceof TitanBotError) throw error; - throw new TitanBotError( - `Failed to save user data: ${error.message}`, - ErrorTypes.DATABASE, - 'Could not save level data at this time.' - ); - } -} - -export async function saveLevelingConfig(client, guildId, config) { - try { - if (!guildId || !config) { - throw new TitanBotError( - 'Guild ID and config are required', - ErrorTypes.VALIDATION - ); - } - - const guildConfig = await getGuildConfig(client, guildId); - - if (config.xpCooldown && (config.xpCooldown < 0 || config.xpCooldown > 3600)) { - throw new TitanBotError( - 'XP cooldown must be between 0 and 3600 seconds', - ErrorTypes.VALIDATION, - 'Cooldown must be between 0 and 3600 seconds.' - ); - } - - if (config.xpRange && (config.xpRange.min < 1 || config.xpRange.max < 1 || config.xpRange.min > config.xpRange.max)) { - throw new TitanBotError( - 'Invalid XP range configuration', - ErrorTypes.VALIDATION, - 'Minimum XP must be less than maximum XP, and both must be positive.' - ); - } - - guildConfig.leveling = config; - await setGuildConfig(client, guildId, guildConfig); - - logger.info(`Leveling config updated for guild ${guildId}`); - } catch (error) { - logger.error(`Error saving leveling config for guild ${guildId}:`, error); - if (error instanceof TitanBotError) throw error; - throw new TitanBotError( - `Failed to save config: ${error.message}`, - ErrorTypes.DATABASE, - 'Could not save configuration at this time.' - ); - } -} - -export async function addLevels(client, guildId, userId, levels) { - try { - const levelingConfig = await getLevelingConfig(client, guildId); - if (!levelingConfig?.enabled) { - throw new TitanBotError( - 'Leveling system is disabled on this server', - ErrorTypes.CONFIGURATION, - 'The leveling system is currently disabled on this server.' - ); - } - - if (!Number.isInteger(levels) || levels <= 0) { - throw new TitanBotError( - `Invalid level amount: ${levels}`, - ErrorTypes.VALIDATION, - 'You must add a positive number of levels.' - ); - } - - const userData = await getUserLevelData(client, guildId, userId); - const newLevel = userData.level + levels; - - if (newLevel > MAX_LEVEL) { - throw new TitanBotError( - `Level ${newLevel} exceeds maximum level ${MAX_LEVEL}`, - ErrorTypes.VALIDATION, - `Maximum level is ${MAX_LEVEL}.` - ); - } - - const newXp = 0; - const newTotalXp = calculateTotalXp(newLevel, newXp); - - userData.level = newLevel; - userData.xp = newXp; - userData.totalXp = newTotalXp; - - await saveUserLevelData(client, guildId, userId, userData); - - logger.info(`Added ${levels} levels to user ${userId} in guild ${guildId}`); - return userData; - } catch (error) { - logger.error(`Error adding levels for user ${userId}:`, error); - if (error instanceof TitanBotError) throw error; - throw new TitanBotError( - `Failed to add levels: ${error.message}`, - ErrorTypes.DATABASE, - 'Could not add levels at this time.' - ); - } -} - -export async function removeLevels(client, guildId, userId, levels) { - try { - const levelingConfig = await getLevelingConfig(client, guildId); - if (!levelingConfig?.enabled) { - throw new TitanBotError( - 'Leveling system is disabled on this server', - ErrorTypes.CONFIGURATION, - 'The leveling system is currently disabled on this server.' - ); - } - - if (!Number.isInteger(levels) || levels <= 0) { - throw new TitanBotError( - `Invalid level amount: ${levels}`, - ErrorTypes.VALIDATION, - 'You must remove a positive number of levels.' - ); - } - - const userData = await getUserLevelData(client, guildId, userId); - const newLevel = Math.max(MIN_LEVEL, userData.level - levels); - - const newXp = 0; - const newTotalXp = calculateTotalXp(newLevel, newXp); - - userData.level = newLevel; - userData.xp = newXp; - userData.totalXp = newTotalXp; - - await saveUserLevelData(client, guildId, userId, userData); - - logger.info(`Removed ${levels} levels from user ${userId} in guild ${guildId}`); - return userData; - } catch (error) { - logger.error(`Error removing levels for user ${userId}:`, error); - if (error instanceof TitanBotError) throw error; - throw new TitanBotError( - `Failed to remove levels: ${error.message}`, - ErrorTypes.DATABASE, - 'Could not remove levels at this time.' - ); - } -} - -export async function setUserLevel(client, guildId, userId, level) { - try { - const levelingConfig = await getLevelingConfig(client, guildId); - if (!levelingConfig?.enabled) { - throw new TitanBotError( - 'Leveling system is disabled on this server', - ErrorTypes.CONFIGURATION, - 'The leveling system is currently disabled on this server.' - ); - } - - if (!Number.isInteger(level) || level < MIN_LEVEL || level > MAX_LEVEL) { - throw new TitanBotError( - `Invalid level: ${level}`, - ErrorTypes.VALIDATION, - `Level must be between ${MIN_LEVEL} and ${MAX_LEVEL}.` - ); - } - - const userData = await getUserLevelData(client, guildId, userId); - - const newXp = 0; - const newTotalXp = calculateTotalXp(level, newXp); - - userData.level = level; - userData.xp = newXp; - userData.totalXp = newTotalXp; - - await saveUserLevelData(client, guildId, userId, userData); - - logger.info(`Set level for user ${userId} to ${level} in guild ${guildId}`); - return userData; - } catch (error) { - logger.error(`Error setting level for user ${userId}:`, error); - if (error instanceof TitanBotError) throw error; - throw new TitanBotError( - `Failed to set level: ${error.message}`, - ErrorTypes.DATABASE, - 'Could not set level at this time.' - ); - } -} - -export async function deleteUserLevelData(client, guildId, userId) { - try { - if (!guildId || !userId) { - throw new TitanBotError( - 'Guild ID and User ID are required', - ErrorTypes.VALIDATION - ); - } - - const key = `${guildId}:leveling:users:${userId}`; - await client.db.delete(key); - - logger.debug(`Deleted level data for user ${userId} in guild ${guildId}`); - } catch (error) { - logger.error(`Error deleting level data for user ${userId}:`, error); - if (error instanceof TitanBotError) throw error; - logger.warn(`Could not delete level data for user ${userId} in guild ${guildId}`); - } -} \ No newline at end of file diff --git a/src/services/loggingService.js b/src/services/loggingService.js index 96c921ef86..3eddcd307d 100644 --- a/src/services/loggingService.js +++ b/src/services/loggingService.js @@ -1,7 +1,7 @@ // loggingService.js import { ChannelType } from 'discord.js'; -import { getGuildConfig, updateGuildConfig } from './guildConfig.js'; +import { getGuildConfig, updateGuildConfig } from './config/guildConfig.js'; import { logger } from '../utils/logger.js'; import { appendContentSection, @@ -9,11 +9,7 @@ import { buildStandardLogEmbed, fieldsToLines, splitComparisonFields, -<<<<<<< HEAD -} from '../utils/logEmbeds.js'; -======= } from '../utils/logging/logEmbeds.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) const LOG_DESTINATIONS = ['audit', 'applications', 'reports']; diff --git a/src/services/moderationService.js b/src/services/moderationService.js deleted file mode 100644 index 681342f5e7..0000000000 --- a/src/services/moderationService.js +++ /dev/null @@ -1,451 +0,0 @@ -// moderationService.js - -import { PermissionFlagsBits } from 'discord.js'; -import { logger } from '../utils/logger.js'; -import { TitanBotError, ErrorTypes } from '../utils/errorHandler.js'; -import { logModerationAction } from '../utils/moderation.js'; - -function getTargetLabel(target) { - return target.user?.tag ?? target.displayName ?? 'this user'; -} - -function getHighestRole(member) { - return member?.roles?.highest ?? null; -} - -export class ModerationService { - - static buildHierarchyMessage({ actor, actorRole, targetRole, targetLabel, action }) { - if (actor === 'moderator') { - return ( - `You cannot ${action} **${targetLabel}** — their role **${targetRole.name}** is equal to or above yours (**${actorRole.name}**). ` + - `In **Server Settings → Roles**, drag your moderator role above **${targetRole.name}**.` - ); - } - - return ( - `I cannot ${action} **${targetLabel}** — my role **${actorRole.name}** is equal to or below theirs (**${targetRole.name}**). ` + - `In **Server Settings → Roles**, drag my bot role above **${targetRole.name}**.` - ); - } - - static buildHierarchySkipReason(moderator, target, action, actor = 'moderator') { - const targetLabel = getTargetLabel(target); - const targetRole = getHighestRole(target); - - if (actor === 'bot') { - const botMember = target.guild?.members?.me; - const botRole = getHighestRole(botMember); - if (!botRole || !targetRole) { - return `Bot role hierarchy blocked ${action} for ${targetLabel}`; - } - return `Bot role **${botRole.name}** is too low for **${targetRole.name}** — move the bot role higher`; - } - - const modRole = getHighestRole(moderator); - if (!modRole || !targetRole) { - return `Role hierarchy blocked ${action} for ${targetLabel}`; - } - return `Your role **${modRole.name}** is too low for **${targetRole.name}** — move your role higher`; - } - - static validateHierarchy(moderator, target, action) { - if (!moderator || !target) { - return { valid: false, error: 'Invalid moderator or target' }; - } - - if (moderator.guild?.ownerId === moderator.id) { - return { valid: true }; - } - - const modRole = getHighestRole(moderator); - const targetRole = getHighestRole(target); - - if (!modRole || !targetRole) { - return { - valid: false, - error: 'Could not resolve role hierarchy. Try mentioning the user or use the slash command.', - }; - } - - if (modRole.position <= targetRole.position) { - return { - valid: false, - error: this.buildHierarchyMessage({ - actor: 'moderator', - actorRole: modRole, - targetRole, - targetLabel: getTargetLabel(target), - action, - }), - }; - } - - return { valid: true }; - } - - static validateBotHierarchy(target, action) { - if (!target) { - return { valid: false, error: 'Invalid target' }; - } - - const botMember = target.guild?.members?.me; - if (!botMember) { - return { valid: false, error: 'Bot is not in the guild' }; - } - - const botRole = getHighestRole(botMember); - const targetRole = getHighestRole(target); - - if (!botRole || !targetRole) { - return { - valid: false, - error: 'Could not resolve bot role hierarchy. Check that my role is configured in this server.', - }; - } - - if (botRole.position <= targetRole.position) { - return { - valid: false, - error: this.buildHierarchyMessage({ - actor: 'bot', - actorRole: botRole, - targetRole, - targetLabel: getTargetLabel(target), - action, - }), - }; - } - - return { valid: true }; - } - - static assertModerationHierarchy(moderator, target, action) { - const botCheck = this.validateBotHierarchy(target, action); - if (!botCheck.valid) { - throw new TitanBotError(botCheck.error, ErrorTypes.PERMISSION, botCheck.error); - } - - const modCheck = this.validateHierarchy(moderator, target, action); - if (!modCheck.valid) { - throw new TitanBotError(modCheck.error, ErrorTypes.PERMISSION, modCheck.error); - } - } - - static async banUser({ - guild, - user, - moderator, - reason = 'No reason provided', - deleteDays = 0 - }) { - try { - if (!guild || !user || !moderator) { - throw new TitanBotError( - 'Missing required parameters', - ErrorTypes.VALIDATION, - 'Guild, user, and moderator are required' - ); - } - - let targetMember = null; - try { - targetMember = await guild.members.fetch(user.id).catch(() => null); - } catch (err) { - logger.debug('Target not in guild, proceeding with ban'); - } - - if (targetMember) { - this.assertModerationHierarchy(moderator, targetMember, 'ban'); - } else { - - const isOwner = guild.ownerId === moderator.id; - const hasHighPerms = moderator.permissions.has([ - PermissionFlagsBits.ManageGuild, - PermissionFlagsBits.Administrator - ]); - - if (!isOwner && !hasHighPerms) { - throw new TitanBotError( - 'You do not have sufficient permissions to ban users who are not in the server.', - ErrorTypes.PERMISSION, - 'You need "Manage Server" or "Administrator" permissions to ban users not currently in the guild.' - ); - } - } - - await guild.members.ban(user.id, { reason }); - - const caseId = await logModerationAction({ - client: guild.client, - guild, - event: { - action: 'Member Banned', - target: `${user.tag} (${user.id})`, - executor: `${moderator.user.tag} (${moderator.id})`, - reason, - metadata: { - userId: user.id, - moderatorId: moderator.id, - permanent: true, - deleteDays - } - } - }); - - logger.info(`User banned: ${user.tag} by ${moderator.user.tag} in ${guild.name}`); - - return { - success: true, - caseId, - user: user.tag, - reason - }; - } catch (error) { - logger.error('Error banning user:', error); - throw error; - } - } - - static async kickUser({ - guild, - member, - moderator, - reason = 'No reason provided' - }) { - try { - if (!guild || !member || !moderator) { - throw new TitanBotError( - 'Missing required parameters', - ErrorTypes.VALIDATION, - 'Guild, member, and moderator are required' - ); - } - - this.assertModerationHierarchy(moderator, member, 'kick'); - - if (!member.kickable) { - const targetLabel = getTargetLabel(member); - throw new TitanBotError( - 'Cannot kick member', - ErrorTypes.PERMISSION, - `I cannot kick **${targetLabel}**. They may have **Administrator** permission or a managed/integration role. ` + - 'Ensure my bot role is above theirs in **Server Settings → Roles** and that they do not have Admin.' - ); - } - - await member.kick(reason); - - const caseId = await logModerationAction({ - client: guild.client, - guild, - event: { - action: 'Member Kicked', - target: `${member.user.tag} (${member.id})`, - executor: `${moderator.user.tag} (${moderator.id})`, - reason, - metadata: { - userId: member.id, - moderatorId: moderator.id - } - } - }); - - logger.info(`User kicked: ${member.user.tag} by ${moderator.user.tag} in ${guild.name}`); - - return { - success: true, - caseId, - user: member.user.tag, - reason - }; - } catch (error) { - logger.error('Error kicking user:', error); - throw error; - } - } - - static async timeoutUser({ - guild, - member, - moderator, - durationMs, - reason = 'No reason provided' - }) { - try { - if (!guild || !member || !moderator || !durationMs) { - throw new TitanBotError( - 'Missing required parameters', - ErrorTypes.VALIDATION, - 'Guild, member, moderator, and duration are required' - ); - } - - this.assertModerationHierarchy(moderator, member, 'timeout'); - - if (!member.moderatable) { - const targetLabel = getTargetLabel(member); - throw new TitanBotError( - 'Cannot timeout member', - ErrorTypes.PERMISSION, - `I cannot timeout **${targetLabel}**. They may have **Administrator** permission or a managed/integration role. ` + - 'Ensure my bot role is above theirs in **Server Settings → Roles** and that they do not have Admin.' - ); - } - - await member.timeout(durationMs, reason); - - const durationMinutes = Math.floor(durationMs / 60000); - const caseId = await logModerationAction({ - client: guild.client, - guild, - event: { - action: 'Member Timed Out', - target: `${member.user.tag} (${member.id})`, - executor: `${moderator.user.tag} (${moderator.id})`, - reason, - duration: `${durationMinutes} minutes`, - metadata: { - userId: member.id, - moderatorId: moderator.id, - durationMs - } - } - }); - - logger.info(`User timed out: ${member.user.tag} by ${moderator.user.tag} in ${guild.name}`); - - return { - success: true, - caseId, - user: member.user.tag, - duration: durationMinutes, - reason - }; - } catch (error) { - logger.error('Error timing out user:', error); - throw error; - } - } - - static async removeTimeoutUser({ - guild, - member, - moderator, - reason = 'Timeout removed by moderator' - }) { - try { - if (!guild || !member || !moderator) { - throw new TitanBotError( - 'Missing required parameters', - ErrorTypes.VALIDATION, - 'Guild, member, and moderator are required' - ); - } - - this.assertModerationHierarchy(moderator, member, 'remove the timeout from'); - - if (!member.moderatable) { - const targetLabel = getTargetLabel(member); - throw new TitanBotError( - 'Cannot modify member', - ErrorTypes.PERMISSION, - `I cannot modify **${targetLabel}**. They may have **Administrator** permission or a managed/integration role. ` + - 'Ensure my bot role is above theirs in **Server Settings → Roles**.' - ); - } - - if (!member.isCommunicationDisabled()) { - throw new TitanBotError( - 'User not timed out', - ErrorTypes.VALIDATION, - `${member.user.tag} is not currently timed out` - ); - } - - await member.timeout(null, reason); - - await logModerationAction({ - client: guild.client, - guild, - event: { - action: 'Member Untimeouted', - target: `${member.user.tag} (${member.id})`, - executor: `${moderator.user.tag} (${moderator.id})`, - reason, - metadata: { - userId: member.id, - moderatorId: moderator.id - } - } - }); - - logger.info(`Timeout removed: ${member.user.tag} by ${moderator.user.tag} in ${guild.name}`); - - return { - success: true, - user: member.user.tag - }; - } catch (error) { - logger.error('Error removing timeout:', error); - throw error; - } - } - - static async unbanUser({ - guild, - user, - moderator, - reason = 'No reason provided' - }) { - try { - if (!guild || !user || !moderator) { - throw new TitanBotError( - 'Missing required parameters', - ErrorTypes.VALIDATION, - 'Guild, user, and moderator are required' - ); - } - - const bans = await guild.bans.fetch(); - const banInfo = bans.get(user.id); - - if (!banInfo) { - throw new TitanBotError( - 'User not banned', - ErrorTypes.VALIDATION, - `${user.tag} is not currently banned from this server` - ); - } - - await guild.members.unban(user.id, reason); - - const caseId = await logModerationAction({ - client: guild.client, - guild, - event: { - action: 'Member Unbanned', - target: `${user.tag} (${user.id})`, - executor: `${moderator.user.tag} (${moderator.id})`, - reason, - metadata: { - userId: user.id, - moderatorId: moderator.id - } - } - }); - - logger.info(`User unbanned: ${user.tag} by ${moderator.user.tag} in ${guild.name}`); - - return { - success: true, - caseId, - user: user.tag, - reason - }; - } catch (error) { - logger.error('Error unbanning user:', error); - throw error; - } - } -} diff --git a/src/services/music/musicActions.js b/src/services/music/musicActions.js index 2392eb4415..206da64956 100644 --- a/src/services/music/musicActions.js +++ b/src/services/music/musicActions.js @@ -149,11 +149,7 @@ export async function playQuery(client, interaction, query) { added += 1; } -<<<<<<< HEAD - if (!player.playing && !player.paused && !player.current) { -======= if (!player.playing && !player.paused) { ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) player.play(); } @@ -185,32 +181,21 @@ export async function playQuery(client, interaction, query) { } track.info.requester = interaction.user; -<<<<<<< HEAD - player.queue.add(track); - - if (!player.playing && !player.paused && !player.current) { -======= const willPlayNow = !player.playing && !player.paused; player.queue.add(track); const queuePosition = player.queue.length; if (willPlayNow) { ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) player.play(); } return { embed: successEmbed( -<<<<<<< HEAD - 'Track Added', - `**${track.info.title}**\n${track.info.author}\nPosition: #${player.queue.length} in queue`, -======= willPlayNow ? 'Now Playing' : 'Track Added', willPlayNow ? `**${track.info.title}**\n${track.info.author}` : `**${track.info.title}**\n${track.info.author}\nPosition: #${queuePosition} in queue`, ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) ), }; } @@ -225,14 +210,11 @@ export async function skipTrack(client, interaction) { } assertCanControl(interaction.member, player); const title = player.current.info?.title || 'Unknown'; -<<<<<<< HEAD -======= // Under track-loop, stop() would replay the same track. Clear it so the skip // advances; trackStart re-applies the stored loop mode to the next track. if (player.loop === 'track') { player.setLoop('none'); } ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) player.stop(); return successEmbed('Skipped', `Skipped **${title}**.`); } @@ -272,10 +254,6 @@ export async function applyPause(client, guildId) { } player.pause(true); -<<<<<<< HEAD - getGuildMusicData(guildId).wasPaused = true; -======= ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) await refreshPlayerMessage(client, guildId); return true; } @@ -287,10 +265,6 @@ export async function applyResume(client, guildId) { } player.pause(false); -<<<<<<< HEAD - getGuildMusicData(guildId).wasPaused = false; -======= ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) await refreshPlayerMessage(client, guildId); return true; } @@ -385,9 +359,6 @@ export async function seekTrack(client, interaction, seconds) { } assertCanControl(interaction.member, player); -<<<<<<< HEAD - const position = Math.max(0, seconds * 1000); -======= const info = player.current.info || {}; if (info.isStream || info.isSeekable === false) { throw new TitanBotError( @@ -406,7 +377,6 @@ export async function seekTrack(client, interaction, seconds) { ); } ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) player.seek(position); await refreshPlayerMessage(client, interaction.guild.id); return successEmbed('Seeked', `Seeked to **${seconds}s**.`); @@ -426,10 +396,7 @@ export async function removeFromQueue(client, interaction, index) { const removed = player.queue[queueIndex]; player.queue.remove(queueIndex); -<<<<<<< HEAD -======= await refreshPlayerMessage(client, interaction.guild.id); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) return successEmbed('Removed', `Removed **${removed.info?.title || 'track'}** from the queue.`); } @@ -449,10 +416,7 @@ export async function moveInQueue(client, interaction, from, to) { const track = player.queue[fromIndex]; player.queue.remove(fromIndex); player.queue.splice(toIndex, 0, track); -<<<<<<< HEAD -======= await refreshPlayerMessage(client, interaction.guild.id); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) return successEmbed('Moved', `Moved **${track.info?.title || 'track'}** to position #${to}.`); } @@ -515,11 +479,8 @@ export async function destroyPlayerSession(client, guildId, player, guildData, { guildData.previousTracks = []; guildData.stopConfirmPending = null; -<<<<<<< HEAD -======= guildData.autoPaused = false; guildData.queuePages?.clear(); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) if (guildData.playerMessageId && guildData.playerChannelId) { try { diff --git a/src/services/music/musicVoiceState.js b/src/services/music/musicVoiceState.js index 9d07810b6b..f6ba98d518 100644 --- a/src/services/music/musicVoiceState.js +++ b/src/services/music/musicVoiceState.js @@ -27,11 +27,7 @@ export async function handleMusicVoiceState(client, oldState, newState) { const hasUsers = humansInChannel.size > 0; if (!hasUsers && !player.paused && player.playing) { -<<<<<<< HEAD - guildData.wasPaused = true; -======= guildData.autoPaused = true; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) await applyPause(client, guildId); if (guildData.playerChannelId) { const channel = client.channels.cache.get(guildData.playerChannelId); @@ -42,14 +38,9 @@ export async function handleMusicVoiceState(client, oldState, newState) { return; } -<<<<<<< HEAD - if (hasUsers && guildData.wasPaused && player.paused) { - await applyResume(client, guildId); -======= if (hasUsers && guildData.autoPaused && player.paused) { await applyResume(client, guildId); guildData.autoPaused = false; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) if (guildData.playerChannelId) { const channel = client.channels.cache.get(guildData.playerChannelId); if (channel) { diff --git a/src/services/music/playerHandler.js b/src/services/music/playerHandler.js index 43752cd5c0..b16290fbc3 100644 --- a/src/services/music/playerHandler.js +++ b/src/services/music/playerHandler.js @@ -72,35 +72,64 @@ export function setupPlayerHandler(client) { return; } + // Lavalink nodes often flap (reconnect -> error -> reconnect). Throttle all + // per-node messages to one line per interval, log the first connect only, + // and skip reconnect noise entirely since it is meaningless during flapping. + const nodeLogState = new Map(); + const NODE_LOG_INTERVAL_MS = 5 * 60 * 1000; + + const shouldLogNodeEvent = (nodeName) => { + const prev = nodeLogState.get(nodeName) ?? { lastLogAt: 0, hasConnected: false }; + const now = Date.now(); + if (now - prev.lastLogAt < NODE_LOG_INTERVAL_MS) { + return false; + } + nodeLogState.set(nodeName, { ...prev, lastLogAt: now }); + return true; + }; + + const markNodeConnected = (nodeName) => { + const prev = nodeLogState.get(nodeName) ?? { lastLogAt: 0, hasConnected: false }; + nodeLogState.set(nodeName, { ...prev, hasConnected: true }); + }; + client.riffy.on('nodeConnect', (node) => { + const prev = nodeLogState.get(node.name) ?? { lastLogAt: 0, hasConnected: false }; + if (prev.hasConnected) { + return; + } + markNodeConnected(node.name); logger.info(`Lavalink node "${node.name}" connected.`); }); + client.riffy.on('nodeReconnect', () => { + // Intentionally silent — reconnect spam is not actionable during flapping. + }); + client.riffy.on('nodeError', (node, error) => { - logger.error(`Lavalink node "${node.name}" error:`, error?.message || error); + if (!shouldLogNodeEvent(node.name)) { + return; + } + logger.warn(`Lavalink node "${node.name}" error: ${error?.message || error}`); }); client.riffy.on('nodeDisconnect', (node) => { + if (!shouldLogNodeEvent(node.name)) { + return; + } logger.warn(`Lavalink node "${node.name}" disconnected.`); }); - client.riffy.on('nodeReconnect', (node) => { - logger.info(`Lavalink node "${node.name}" reconnected.`); - }); - client.riffy.on('trackStart', async (player, track) => { try { const guildData = getGuildMusicData(player.guildId); -<<<<<<< HEAD -======= // Keep the Lavalink player's loop mode aligned with the stored preference. // Skip temporarily clears track-loop so it can advance; restore it here. if (guildData.loop && player.loop !== guildData.loop) { player.setLoop(guildData.loop); } ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) if (player.previous) { guildData.previousTracks.push(player.previous); if (guildData.previousTracks.length > 20) { @@ -187,10 +216,7 @@ export function setupPlayerHandler(client) { guildData.playerMessageId = null; guildData.playerChannelId = null; guildData.previousTracks = []; -<<<<<<< HEAD -======= guildData.autoPaused = false; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) if (guildData.idleTimeout) { clearTimeout(guildData.idleTimeout); guildData.idleTimeout = null; diff --git a/src/services/music/playerStore.js b/src/services/music/playerStore.js index 475051755b..009ad3e686 100644 --- a/src/services/music/playerStore.js +++ b/src/services/music/playerStore.js @@ -13,11 +13,7 @@ export class GuildMusicData { this.queuePages = new Map(); this.updateInterval = null; this.idleTimeout = null; -<<<<<<< HEAD - this.wasPaused = false; -======= this.autoPaused = false; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) this.stopConfirmPending = null; } } diff --git a/src/services/music/riffySetup.js b/src/services/music/riffySetup.js index 1052d100d7..4514512e7e 100644 --- a/src/services/music/riffySetup.js +++ b/src/services/music/riffySetup.js @@ -1,11 +1,7 @@ import { createRequire } from 'module'; import { GatewayDispatchEvents } from 'discord.js'; import { logger } from '../../utils/logger.js'; -<<<<<<< HEAD -import lavalinkConfig from '../../config/lavalink.js'; -======= import lavalinkConfig from '../../config/music/lavalink.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) import { setupPlayerHandler } from './playerHandler.js'; const require = createRequire(import.meta.url); @@ -13,11 +9,7 @@ const { Riffy } = require('riffy'); export function initializeMusic(client) { if (!lavalinkConfig.nodes?.length) { -<<<<<<< HEAD - logger.error('No Lavalink nodes configured. Set LAVALINK_HOST (or LAVALINK_NODES) in your environment.'); -======= logger.error('No Lavalink nodes configured. Add lavalink/nodes.json, set LAVALINK_NODES, or set LAVALINK_HOST in your environment.'); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) return; } diff --git a/src/services/panelHealthService.js b/src/services/panelHealthService.js index 507c645c29..1a5a32196e 100644 --- a/src/services/panelHealthService.js +++ b/src/services/panelHealthService.js @@ -1,10 +1,6 @@ import { logger } from '../utils/logger.js'; -<<<<<<< HEAD -import { getGuildConfigKey } from '../utils/database/keys.js'; -======= import { getReactionRoleKey } from '../utils/database/keys.js'; -import { getGuildConfig, setGuildConfig, patchGuildConfig } from './guildConfig.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) +import { getGuildConfig, setGuildConfig, patchGuildConfig } from './config/guildConfig.js'; import { getTicketPanelStatus, getVerificationPanelStatus, @@ -14,27 +10,16 @@ import { getAllReactionRoleMessages } from './reactionRoleService.js'; async function persistVerificationMessageId(client, guildId, config, messageId) { if (!messageId || config.verification?.messageId === messageId) return; -<<<<<<< HEAD - config.verification = { ...config.verification, messageId }; - await client.db.set(getGuildConfigKey(guildId), config); -======= await patchGuildConfig(client, guildId, { verification: { ...config.verification, messageId }, }); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } async function persistReactionRoleMessageId(client, guildId, panelData, messageId) { if (!messageId || panelData.messageId === messageId) return; -<<<<<<< HEAD - const oldKey = `reaction_roles:${guildId}:${panelData.messageId}`; - panelData.messageId = messageId; - const newKey = `reaction_roles:${guildId}:${messageId}`; -======= const oldKey = getReactionRoleKey(guildId, panelData.messageId); panelData.messageId = messageId; const newKey = getReactionRoleKey(guildId, messageId); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) await client.db.set(newKey, panelData); await client.db.delete(oldKey).catch(() => {}); } @@ -53,11 +38,7 @@ export async function reconcileTicketPanels(client) { summary.scannedGuilds += 1; try { -<<<<<<< HEAD - const config = await client.db.get(getGuildConfigKey(guild.id)); -======= const config = await getGuildConfig(client, guild.id); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) if (!config?.ticketPanelChannelId) continue; const panelStatus = await getTicketPanelStatus(client, guild, config); @@ -65,11 +46,7 @@ export async function reconcileTicketPanels(client) { if (panelStatus.recoveredId) { summary.recoveredIds += 1; config.ticketPanelMessageId = panelStatus.recoveredId; -<<<<<<< HEAD - await client.db.set(getGuildConfigKey(guild.id), config); -======= await setGuildConfig(client, guild.id, config); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } if (panelStatus.exists) { @@ -106,11 +83,7 @@ export async function reconcileVerificationPanels(client) { summary.scannedGuilds += 1; try { -<<<<<<< HEAD - const config = await client.db.get(getGuildConfigKey(guild.id)); -======= const config = await getGuildConfig(client, guild.id); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) const verification = config?.verification; if (!verification?.channelId || verification.enabled === false) continue; diff --git a/src/services/reactionRoleService.js b/src/services/reactionRoleService.js index 82d6138213..70c9be2134 100644 --- a/src/services/reactionRoleService.js +++ b/src/services/reactionRoleService.js @@ -2,10 +2,7 @@ import { logger } from '../utils/logger.js'; import { createError, ErrorTypes } from '../utils/errorHandler.js'; -<<<<<<< HEAD -======= import { getReactionRoleKey, getReactionRolesPrefix } from '../utils/database/keys.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) const MAX_ROLES_PER_MESSAGE = 25; @@ -109,11 +106,7 @@ export async function getReactionRoleMessage(client, guildId, messageId) { validateGuildId(guildId); validateMessageId(messageId); -<<<<<<< HEAD - const key = `reaction_roles:${guildId}:${messageId}`; -======= const key = getReactionRoleKey(guildId, messageId); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) const data = await client.db.get(key); return data || null; } catch (error) { @@ -175,11 +168,7 @@ export async function createReactionRoleMessage(client, guildId, channelId, mess createdAt: new Date().toISOString() }; -<<<<<<< HEAD - const key = `reaction_roles:${guildId}:${messageId}`; -======= const key = getReactionRoleKey(guildId, messageId); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) await client.db.set(key, reactionRoleData); logger.info(`Created reaction role message ${messageId} in guild ${guildId} with ${roleIds.length} roles`); @@ -205,11 +194,7 @@ export async function addReactionRole(client, guildId, messageId, emoji, roleId) validateRoleId(roleId); await validateRoleSafety(client, guildId, roleId); -<<<<<<< HEAD - const key = `reaction_roles:${guildId}:${messageId}`; -======= const key = getReactionRoleKey(guildId, messageId); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) const data = await getReactionRoleMessage(client, guildId, messageId) || { messageId, guildId, @@ -241,11 +226,7 @@ export async function deleteReactionRoleMessage(client, guildId, messageId) { validateGuildId(guildId); validateMessageId(messageId); -<<<<<<< HEAD - const key = `reaction_roles:${guildId}:${messageId}`; -======= const key = getReactionRoleKey(guildId, messageId); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) const data = await getReactionRoleMessage(client, guildId, messageId); if (!data) { @@ -276,11 +257,7 @@ export async function removeReactionRole(client, guildId, messageId, emoji) { validateGuildId(guildId); validateMessageId(messageId); -<<<<<<< HEAD - const key = `reaction_roles:${guildId}:${messageId}`; -======= const key = getReactionRoleKey(guildId, messageId); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) const data = await getReactionRoleMessage(client, guildId, messageId); if (!data || !data.roles[emoji]) { @@ -316,11 +293,7 @@ export async function getAllReactionRoleMessages(client, guildId) { try { validateGuildId(guildId); -<<<<<<< HEAD - const prefix = `reaction_roles:${guildId}:`; -======= const prefix = getReactionRolesPrefix(guildId); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) let keys; try { @@ -416,11 +389,7 @@ export async function setReactionRoleChannel(client, guildId, messageId, channel ); } -<<<<<<< HEAD - const key = `reaction_roles:${guildId}:${messageId}`; -======= const key = getReactionRoleKey(guildId, messageId); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) const data = await getReactionRoleMessage(client, guildId, messageId) || { messageId, guildId, @@ -479,11 +448,7 @@ export async function reconcileReactionRoleMessages(client, guildId = null) { if (!guild) { for (const reactionRoleMessage of reactionRoleMessages) { summary.scannedMessages += 1; -<<<<<<< HEAD - await client.db.delete(`reaction_roles:${targetGuildId}:${reactionRoleMessage.messageId}`); -======= await client.db.delete(getReactionRoleKey(targetGuildId, reactionRoleMessage.messageId)); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) summary.removedMessages += 1; } logger.info(`Removed ${reactionRoleMessages.length} stale reaction role message(s) for unavailable guild ${targetGuildId}`); @@ -498,22 +463,14 @@ export async function reconcileReactionRoleMessages(client, guildId = null) { || await guild.channels.fetch(reactionRoleMessage.channelId).catch(() => null); if (!channel || !channel.isTextBased?.()) { -<<<<<<< HEAD - await client.db.delete(`reaction_roles:${targetGuildId}:${reactionRoleMessage.messageId}`); -======= await client.db.delete(getReactionRoleKey(targetGuildId, reactionRoleMessage.messageId)); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) summary.removedMessages += 1; continue; } const message = await channel.messages.fetch(reactionRoleMessage.messageId).catch(() => null); if (!message) { -<<<<<<< HEAD - await client.db.delete(`reaction_roles:${targetGuildId}:${reactionRoleMessage.messageId}`); -======= await client.db.delete(getReactionRoleKey(targetGuildId, reactionRoleMessage.messageId)); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) summary.removedMessages += 1; } } catch (messageCheckError) { diff --git a/src/services/serverstatsService.js b/src/services/serverstatsService.js index f54d7be309..ef8f7e8a6d 100644 --- a/src/services/serverstatsService.js +++ b/src/services/serverstatsService.js @@ -2,13 +2,9 @@ import { logger } from '../utils/logger.js'; import { logEvent, EVENT_TYPES } from './loggingService.js'; -<<<<<<< HEAD -import { formatLogLine } from '../utils/logEmbeds.js'; -======= import { formatLogLine } from '../utils/logging/logEmbeds.js'; import { getServerCountersKey } from '../utils/database/keys.js'; import botConfig from '../config/bot.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) export const COUNTER_TYPE_CONFIG = { members: { @@ -48,8 +44,6 @@ export function getCounterEmoji(type) { return getCounterConfig(type).emoji; } -<<<<<<< HEAD -======= export function formatCounterChannelName(type, count) { const template = botConfig.counters?.defaults?.channelName || '{name}-{count}'; const baseName = getCounterBaseName(type); @@ -70,7 +64,6 @@ export function getCounterActionMessage(action, values = {}) { ); } ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) export async function getGuildCounterStats(guild) { let memberCollection = guild.members.cache; @@ -179,11 +172,7 @@ export async function updateCounter(client, guild, counter) { logger.debug(`Base name: "${baseName}", Current name: "${channel.name}"`); } -<<<<<<< HEAD - const newName = `${baseName}: ${count}`; -======= const newName = formatCounterChannelName(type, count); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) if (process.env.NODE_ENV !== 'production') { logger.debug(`New name would be: "${newName}"`); } @@ -237,11 +226,7 @@ export async function getServerCounters(client, guildId) { return []; } -<<<<<<< HEAD - const data = await client.db.get(`counters:${guildId}`); -======= const data = await client.db.get(getServerCountersKey(guildId)); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) let counters = []; @@ -285,11 +270,7 @@ export async function saveServerCounters(client, guildId, counters) { logger.debug(`Saving ${sanitizedCounters.length} counters for guild ${guildId}:`, sanitizedCounters); } -<<<<<<< HEAD - await client.db.set(`counters:${guildId}`, sanitizedCounters); -======= await client.db.set(getServerCountersKey(guildId), sanitizedCounters); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) if (process.env.NODE_ENV !== 'production') { logger.debug('Counters saved successfully'); } diff --git a/src/services/ticket.js b/src/services/ticket.js index b53b9075d9..158a364d1c 100644 --- a/src/services/ticket.js +++ b/src/services/ticket.js @@ -8,22 +8,11 @@ import { PermissionFlagsBits, AttachmentBuilder, } from 'discord.js'; -<<<<<<< HEAD -import { buildStandardLogEmbed, formatLogLine } from '../utils/logEmbeds.js'; -======= import { buildStandardLogEmbed, formatLogLine } from '../utils/logging/logEmbeds.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) -import { getGuildConfig } from './guildConfig.js'; +import { getGuildConfig } from './config/guildConfig.js'; import { getTicketData, saveTicketData, deleteTicketData, getOpenTicketCountForUser, incrementTicketCounter } from '../utils/database.js'; import { logger } from '../utils/logger.js'; import { createEmbed, errorEmbed } from '../utils/embeds.js'; -<<<<<<< HEAD -import { logTicketEvent } from '../utils/ticketLogging.js'; -import { ensureTypedServiceError } from '../utils/serviceErrorBoundary.js'; -import { PRIORITY_MAP } from '../utils/helpers.js'; -const TICKET_DELETE_DELAY_MS = 3000; -const TICKET_DELETE_DELAY_SECONDS = Math.floor(TICKET_DELETE_DELAY_MS / 1000); -======= import { logTicketEvent } from '../utils/ticket/ticketLogging.js'; import { createError, ErrorTypes } from '../utils/errorHandler.js'; import { ensureTypedServiceError, wrapServiceBoundary } from '../utils/serviceErrorBoundary.js'; @@ -59,7 +48,6 @@ function rethrowTicketError(error, operation, userMessage, context = {}) { } ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) function buildTicketControlRow({ claimedBy = null } = {}) { return new ActionRowBuilder().addComponents( @@ -82,28 +70,6 @@ function buildTicketControlRow({ claimedBy = null } = {}) { ); } -<<<<<<< HEAD -export async function getUserTicketCount(guildId, userId) { - try { - return await getOpenTicketCountForUser(guildId, userId); - } catch (error) { - const typedError = ensureTypedServiceError(error, { - service: 'ticketService', - operation: 'getUserTicketCount', - message: 'Ticket operation failed: getUserTicketCount', - userMessage: 'Failed to count open tickets.', - context: { guildId, userId } - }); - logger.error('Error counting user tickets:', { - guildId, - userId, - error: typedError.message, - errorCode: typedError.context?.errorCode - }); - return 0; - } -} -======= export const getUserTicketCount = wrapServiceBoundary(async function getUserTicketCount(guildId, userId) { return await getOpenTicketCountForUser(guildId, userId); }, { @@ -112,7 +78,6 @@ export const getUserTicketCount = wrapServiceBoundary(async function getUserTick userMessage: 'Failed to count open tickets.', context: {}, }); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) export async function createTicket(guild, member, categoryId, reason = 'No reason provided', priority = 'none') { try { @@ -123,19 +88,12 @@ export async function createTicket(guild, member, categoryId, reason = 'No reaso const currentTicketCount = await getUserTicketCount(guild.id, member.id); if (currentTicketCount >= maxTicketsPerUser) { -<<<<<<< HEAD - return { - success: false, - error: `You have reached the maximum number of open tickets (${maxTicketsPerUser}). Please close your existing tickets before creating a new one.` - }; -======= ticketUserError( `Max open tickets reached for ${member.id}`, `You have reached the maximum number of open tickets (${maxTicketsPerUser}). Please close your existing tickets before creating a new one.`, ErrorTypes.VALIDATION, { guildId: guild.id, userId: member.id, operation: 'createTicket' } ); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } let category = categoryId ? @@ -271,47 +229,16 @@ export async function createTicket(guild, member, categoryId, reason = 'No reaso } }); -<<<<<<< HEAD - return { success: true, channel, ticketData }; - - } catch (error) { - const typedError = ensureTypedServiceError(error, { - service: 'ticketService', - operation: 'createTicket', - message: 'Ticket operation failed: createTicket', - userMessage: 'Failed to create ticket. Please try again in a moment.', - context: { guildId: guild?.id, userId: member?.id } - }); - logger.error('Error creating ticket:', { - guildId: guild?.id, - userId: member?.id, - error: typedError.message, - errorCode: typedError.context?.errorCode - }); - return { - success: false, - error: typedError.userMessage || typedError.message, - errorCode: typedError.context?.errorCode - }; -======= return { channel, ticketData }; } catch (error) { rethrowTicketError(error, 'createTicket', 'Failed to create ticket. Please try again in a moment.', { guildId: guild?.id, userId: member?.id }); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } } export async function closeTicket(channel, closer, reason = 'No reason provided') { try { -<<<<<<< HEAD - const ticketData = await getTicketData(channel.guild.id, channel.id); - if (!ticketData) { - return { success: false, error: 'This is not a ticket channel' }; - } -======= const ticketData = requireTicket(await getTicketData(channel.guild.id, channel.id), channel); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) const config = await getGuildConfig(channel.client, channel.guild.id); const dmOnClose = config.dmOnClose !== false; @@ -484,52 +411,15 @@ components: [] } }); -<<<<<<< HEAD - return { success: true, ticketData }; - - } catch (error) { - const typedError = ensureTypedServiceError(error, { - service: 'ticketService', - operation: 'closeTicket', - message: 'Ticket operation failed: closeTicket', - userMessage: 'Failed to close ticket. Please try again in a moment.', - context: { guildId: channel?.guild?.id, channelId: channel?.id, closerId: closer?.id } - }); - logger.error('Error closing ticket:', { - guildId: channel?.guild?.id, - channelId: channel?.id, - userId: closer?.id, - error: typedError.message, - errorCode: typedError.context?.errorCode - }); - return { - success: false, - error: typedError.userMessage || typedError.message, - errorCode: typedError.context?.errorCode - }; -======= return ticketData; } catch (error) { rethrowTicketError(error, 'closeTicket', 'Failed to close ticket. Please try again in a moment.', { guildId: channel?.guild?.id, channelId: channel?.id, closerId: closer?.id }); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } } export async function claimTicket(channel, claimer) { try { -<<<<<<< HEAD - const ticketData = await getTicketData(channel.guild.id, channel.id); - if (!ticketData) { - return { success: false, error: 'This is not a ticket channel' }; - } - - if (ticketData.claimedBy) { - return { - success: false, - error: `This ticket is already claimed by <@${ticketData.claimedBy}>` - }; -======= const ticketData = requireTicket(await getTicketData(channel.guild.id, channel.id), channel); if (ticketData.claimedBy) { @@ -539,7 +429,6 @@ export async function claimTicket(channel, claimer) { ErrorTypes.VALIDATION, { channelId: channel.id, claimedBy: ticketData.claimedBy, operation: 'claimTicket' } ); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } ticketData.claimedBy = claimer.id; @@ -609,52 +498,15 @@ export async function claimTicket(channel, claimer) { } }); -<<<<<<< HEAD - return { success: true, ticketData }; - - } catch (error) { - const typedError = ensureTypedServiceError(error, { - service: 'ticketService', - operation: 'claimTicket', - message: 'Ticket operation failed: claimTicket', - userMessage: 'Failed to claim ticket. Please try again in a moment.', - context: { guildId: channel?.guild?.id, channelId: channel?.id, claimerId: claimer?.id } - }); - logger.error('Error claiming ticket:', { - guildId: channel?.guild?.id, - channelId: channel?.id, - userId: claimer?.id, - error: typedError.message, - errorCode: typedError.context?.errorCode - }); - return { - success: false, - error: typedError.userMessage || typedError.message, - errorCode: typedError.context?.errorCode - }; -======= return ticketData; } catch (error) { rethrowTicketError(error, 'claimTicket', 'Failed to claim ticket. Please try again in a moment.', { guildId: channel?.guild?.id, channelId: channel?.id, claimerId: claimer?.id }); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } } export async function reopenTicket(channel, reopener) { try { -<<<<<<< HEAD - const ticketData = await getTicketData(channel.guild.id, channel.id); - if (!ticketData) { - return { success: false, error: 'This is not a ticket channel' }; - } - - if (ticketData.status !== 'closed') { - return { - success: false, - error: 'This ticket is not currently closed' - }; -======= const ticketData = requireTicket(await getTicketData(channel.guild.id, channel.id), channel); if (ticketData.status !== 'closed') { @@ -664,7 +516,6 @@ export async function reopenTicket(channel, reopener) { ErrorTypes.VALIDATION, { channelId: channel.id, operation: 'reopenTicket' } ); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } const config = await getGuildConfig(channel.client, channel.guild.id); @@ -752,40 +603,10 @@ export async function reopenTicket(channel, reopener) { await channel.send({ embeds: [reopenEmbed] }); } -<<<<<<< HEAD - return { - success: true, - ticketData, - movedToOpenCategory, - openCategoryMoveFailed - }; - - } catch (error) { - const typedError = ensureTypedServiceError(error, { - service: 'ticketService', - operation: 'reopenTicket', - message: 'Ticket operation failed: reopenTicket', - userMessage: 'Failed to reopen ticket. Please try again in a moment.', - context: { guildId: channel?.guild?.id, channelId: channel?.id, reopenerId: reopener?.id } - }); - logger.error('Error reopening ticket:', { - guildId: channel?.guild?.id, - channelId: channel?.id, - userId: reopener?.id, - error: typedError.message, - errorCode: typedError.context?.errorCode - }); - return { - success: false, - error: typedError.userMessage || typedError.message, - errorCode: typedError.context?.errorCode - }; -======= return { ticketData, movedToOpenCategory, openCategoryMoveFailed }; } catch (error) { rethrowTicketError(error, 'reopenTicket', 'Failed to reopen ticket. Please try again in a moment.', { guildId: channel?.guild?.id, channelId: channel?.id, reopenerId: reopener?.id }); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } } @@ -886,14 +707,7 @@ ${rows} export async function deleteTicket(channel, deleter) { try { -<<<<<<< HEAD - const ticketData = await getTicketData(channel.guild.id, channel.id); - if (!ticketData) { - return { success: false, error: 'This is not a ticket channel' }; - } -======= const ticketData = requireTicket(await getTicketData(channel.guild.id, channel.id), channel); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) const deleteEmbed = createEmbed({ title: 'Ticket Deleted', @@ -1035,59 +849,15 @@ export async function deleteTicket(channel, deleter) { } }, TICKET_DELETE_DELAY_MS); -<<<<<<< HEAD - return { success: true, ticketData }; - - } catch (error) { - const typedError = ensureTypedServiceError(error, { - service: 'ticketService', - operation: 'deleteTicket', - message: 'Ticket operation failed: deleteTicket', - userMessage: 'Failed to delete ticket. Please try again in a moment.', - context: { guildId: channel?.guild?.id, channelId: channel?.id, deleterId: deleter?.id } - }); - logger.error('Error deleting ticket:', { - guildId: channel?.guild?.id, - channelId: channel?.id, - userId: deleter?.id, - error: typedError.message, - errorCode: typedError.context?.errorCode - }); - return { - success: false, - error: typedError.userMessage || typedError.message, - errorCode: typedError.context?.errorCode - }; -======= return ticketData; } catch (error) { rethrowTicketError(error, 'deleteTicket', 'Failed to delete ticket. Please try again in a moment.', { guildId: channel?.guild?.id, channelId: channel?.id, deleterId: deleter?.id }); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } } export async function unclaimTicket(channel, unclaimer) { try { -<<<<<<< HEAD - const ticketData = await getTicketData(channel.guild.id, channel.id); - if (!ticketData) { - return { success: false, error: 'This is not a ticket channel' }; - } - - if (!ticketData.claimedBy) { - return { - success: false, - error: 'This ticket is not currently claimed' - }; - } - - if (ticketData.claimedBy !== unclaimer.id && !unclaimer.permissions.has(PermissionFlagsBits.ManageChannels)) { - return { - success: false, - error: 'You can only unclaim your own tickets or need Manage Channels permission.' - }; -======= const ticketData = requireTicket(await getTicketData(channel.guild.id, channel.id), channel); if (!ticketData.claimedBy) { @@ -1106,7 +876,6 @@ export async function unclaimTicket(channel, unclaimer) { ErrorTypes.PERMISSION, { channelId: channel.id, operation: 'unclaimTicket' } ); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } const previousClaimer = ticketData.claimedBy; @@ -1178,35 +947,10 @@ export async function unclaimTicket(channel, unclaimer) { } }); -<<<<<<< HEAD - return { success: true, ticketData }; - - } catch (error) { - const typedError = ensureTypedServiceError(error, { - service: 'ticketService', - operation: 'unclaimTicket', - message: 'Ticket operation failed: unclaimTicket', - userMessage: 'Failed to unclaim ticket. Please try again in a moment.', - context: { guildId: channel?.guild?.id, channelId: channel?.id, unclaimerId: unclaimer?.id } - }); - logger.error('Error unclaiming ticket:', { - guildId: channel?.guild?.id, - channelId: channel?.id, - userId: unclaimer?.id, - error: typedError.message, - errorCode: typedError.context?.errorCode - }); - return { - success: false, - error: typedError.userMessage || typedError.message, - errorCode: typedError.context?.errorCode - }; -======= return ticketData; } catch (error) { rethrowTicketError(error, 'unclaimTicket', 'Failed to unclaim ticket. Please try again in a moment.', { guildId: channel?.guild?.id, channelId: channel?.id, unclaimerId: unclaimer?.id }); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } } @@ -1216,16 +960,6 @@ async function getNextTicketNumber(guildId) { export async function updateTicketPriority(channel, priority, updater) { try { -<<<<<<< HEAD - const ticketData = await getTicketData(channel.guild.id, channel.id); - if (!ticketData) { - return { success: false, error: 'This is not a ticket channel' }; - } - - const priorityInfo = PRIORITY_MAP[priority]; - if (!priorityInfo) { - return { success: false, error: 'Invalid priority level' }; -======= const ticketData = requireTicket(await getTicketData(channel.guild.id, channel.id), channel); const priorityInfo = PRIORITY_MAP[priority]; @@ -1236,7 +970,6 @@ export async function updateTicketPriority(channel, priority, updater) { ErrorTypes.VALIDATION, { channelId: channel.id, priority, operation: 'updateTicketPriority' } ); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } ticketData.priority = priority; @@ -1306,34 +1039,9 @@ export async function updateTicketPriority(channel, priority, updater) { } }); -<<<<<<< HEAD - return { success: true, ticketData }; - - } catch (error) { - const typedError = ensureTypedServiceError(error, { - service: 'ticketService', - operation: 'updateTicketPriority', - message: 'Ticket operation failed: updateTicketPriority', - userMessage: 'Failed to update ticket priority. Please try again in a moment.', - context: { guildId: channel?.guild?.id, channelId: channel?.id, updaterId: updater?.id, priority } - }); - logger.error('Error updating ticket priority:', { - guildId: channel?.guild?.id, - channelId: channel?.id, - userId: updater?.id, - error: typedError.message, - errorCode: typedError.context?.errorCode - }); - return { - success: false, - error: typedError.userMessage || typedError.message, - errorCode: typedError.context?.errorCode - }; -======= return ticketData; } catch (error) { rethrowTicketError(error, 'updateTicketPriority', 'Failed to update ticket priority. Please try again in a moment.', { guildId: channel?.guild?.id, channelId: channel?.id, updaterId: updater?.id, priority }); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } } \ No newline at end of file diff --git a/src/services/verificationService.js b/src/services/verificationService.js index 5b6459f917..49a548c07d 100644 --- a/src/services/verificationService.js +++ b/src/services/verificationService.js @@ -3,7 +3,7 @@ import { PermissionFlagsBits } from 'discord.js'; import { botConfig } from '../config/bot.js'; import { logger } from '../utils/logger.js'; -import { getGuildConfig, setGuildConfig } from './guildConfig.js'; +import { getGuildConfig, setGuildConfig } from './config/guildConfig.js'; import { createError, ErrorTypes } from '../utils/errorHandler.js'; import { insertVerificationAudit } from '../utils/database.js'; import { ensureTypedServiceError } from '../utils/serviceErrorBoundary.js'; @@ -81,18 +81,10 @@ export async function verifyUser(client, guildId, userId, options = {}) { if (member.roles.cache.has(verifiedRole.id)) { return { -<<<<<<< HEAD - success: false, - alreadyVerified: true, - message: "User already verified", - userId, - roleId: verifiedRole.id -======= status: 'already_verified', userId, roleId: verifiedRole.id, roleName: verifiedRole.name, ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) }; } @@ -117,18 +109,10 @@ export async function verifyUser(client, guildId, userId, options = {}) { }); return { -<<<<<<< HEAD - success: true, - userId, - roleId: verifiedRole.id, - roleName: verifiedRole.name, - message: "User verified successfully" -======= status: 'verified', userId, roleId: verifiedRole.id, roleName: verifiedRole.name, ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) }; } catch (error) { @@ -364,15 +348,8 @@ export async function removeVerification(client, guildId, userId, options = {}) if (!member.roles.cache.has(verifiedRole.id)) { return { -<<<<<<< HEAD - success: false, - notVerified: true, - message: "User doesn't have the verified role", - userId -======= status: 'not_verified', userId, ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) }; } @@ -396,16 +373,9 @@ export async function removeVerification(client, guildId, userId, options = {}) }); return { -<<<<<<< HEAD - success: true, - userId, - roleId: verifiedRole.id, - message: "Verification removed successfully" -======= status: 'removed', userId, roleId: verifiedRole.id, ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) }; } catch (error) { diff --git a/src/services/warningService.js b/src/services/warningService.js deleted file mode 100644 index 460ff29487..0000000000 --- a/src/services/warningService.js +++ /dev/null @@ -1,123 +0,0 @@ -// warningService.js - -import { getFromDb, setInDb } from '../utils/database.js'; -import { logger } from '../utils/logger.js'; - -export class WarningService { - - static async addWarning({ - guildId, - userId, - moderatorId, - reason, - timestamp = Date.now() - }) { - try { - const key = `moderation:warnings:${guildId}:${userId}`; - - const warnings = await getFromDb(key, []); - - if (!Array.isArray(warnings)) { - logger.warn(`Warnings for ${userId} in ${guildId} corrupted, resetting`); - await setInDb(key, []); - return { success: false, error: 'Corrupted data' }; - } - - const warning = { - id: Date.now(), - guildId, - userId, - moderatorId, - reason, - timestamp, - status: 'active' - }; - - warnings.push(warning); - - await setInDb(key, warnings); - - logger.info(`Warning added: ${userId} in ${guildId} by ${moderatorId}`); - - return { - success: true, - id: warning.id, - totalCount: warnings.length - }; - } catch (error) { - logger.error('Error adding warning:', error); - return { success: false, error: error.message }; - } - } - - static async getWarnings(guildId, userId) { - try { - const key = `moderation:warnings:${guildId}:${userId}`; - const warnings = await getFromDb(key, []); - - return Array.isArray(warnings) - ? warnings.filter(w => w && w.status !== 'deleted') - : []; - } catch (error) { - logger.error('Error fetching warnings:', error); - return []; - } - } - - static async getWarningCount(guildId, userId) { - const warnings = await this.getWarnings(guildId, userId); - return warnings.length; - } - - static async removeWarning(guildId, userId, warningId) { - try { - const key = `moderation:warnings:${guildId}:${userId}`; - const warnings = await getFromDb(key, []); - - const index = warnings.findIndex(w => w.id === warningId); - if (index === -1) { - return { success: false, error: 'Warning not found' }; - } - - warnings[index].status = 'deleted'; - await setInDb(key, warnings); - - logger.info(`Warning removed: ${warningId} for ${userId} in ${guildId}`); - return { success: true }; - } catch (error) { - logger.error('Error removing warning:', error); - return { success: false, error: error.message }; - } - } - - static async clearWarnings(guildId, userId) { - try { - const key = `moderation:warnings:${guildId}:${userId}`; - const warnings = await getFromDb(key, []); - const count = warnings.length; - - await setInDb(key, []); - - logger.info(`Warnings cleared for ${userId} in ${guildId} (${count} removed)`); - return { success: true, count }; - } catch (error) { - logger.error('Error clearing warnings:', error); - return { success: false, error: error.message }; - } - } - - static async getGuildWarnings(guildId, filters = {}) { - try { - const { moderatorId, limit = 100 } = filters; - const prefix = `moderation:warnings:${guildId}:`; - - const allWarnings = []; - - logger.debug(`Fetched guild warnings for ${guildId} with ${allWarnings.length} total`); - return allWarnings.slice(0, limit); - } catch (error) { - logger.error('Error fetching guild warnings:', error); - return []; - } - } -} \ No newline at end of file diff --git a/src/services/xpSystem.js b/src/services/xpSystem.js deleted file mode 100644 index afb3eec503..0000000000 --- a/src/services/xpSystem.js +++ /dev/null @@ -1,140 +0,0 @@ -// xpSystem.js - -import { logger } from '../utils/logger.js'; -import { getLevelingConfig, getXpForLevel, getUserLevelData, saveUserLevelData } from './leveling.js'; -import { logEvent, EVENT_TYPES } from './loggingService.js'; -import { formatLogLine } from '../utils/logEmbeds.js'; -import { Mutex } from '../utils/mutex.js'; - -export async function addXp(client, guild, member, xpToAdd) { - const lockKey = `leveling:${guild.id}:${member.user.id}`; - return await Mutex.runExclusive(lockKey, async () => { - try { - - if (!xpToAdd || xpToAdd <= 0) { - return { success: false, reason: 'Invalid XP amount' }; - } - - const config = await getLevelingConfig(client, guild.id); - - if (!config.enabled) { - return { success: false, reason: 'Leveling is disabled in this server' }; - } - - const levelData = await getUserLevelData(client, guild.id, member.user.id); - - levelData.xp += xpToAdd; - levelData.totalXp += xpToAdd; - levelData.lastMessage = Date.now(); - - let xpNeededForNextLevel = getXpForLevel(levelData.level); - let didLevelUp = false; - const initialLevel = levelData.level; - - while (levelData.xp >= xpNeededForNextLevel && levelData.level < 1000) { - levelData.xp -= xpNeededForNextLevel; - levelData.level += 1; - didLevelUp = true; - xpNeededForNextLevel = getXpForLevel(levelData.level); - - logger.info(`🎉 ${member.user.tag} leveled up to level ${levelData.level} in ${guild.name}`); - - if (config.roleRewards && config.roleRewards[levelData.level]) { - await awardRoleReward(guild, member, config.roleRewards[levelData.level], levelData.level); - } - } - - if (didLevelUp) { - - if (config.announceLevelUp) { - await sendLevelUpAnnouncement(guild, member, levelData, config); - } - - try { - await logEvent({ - client, - guildId: guild.id, - eventType: EVENT_TYPES.LEVELING_LEVELUP, - data: { - title: 'Level Up', - lines: [ - formatLogLine('Member', `${member.user.tag} (\`${member.user.id}\`)`), - formatLogLine('New Level', levelData.level.toString()), - formatLogLine('Levels Gained', (levelData.level - initialLevel).toString()), - formatLogLine('Total XP', levelData.totalXp.toString()), - ], - userId: member.user.id, - }, - }); - } catch (logError) { - logger.debug('Failed to log leveling event:', logError.message); - } - } - - await saveUserLevelData(client, guild.id, member.user.id, levelData); - - return { - success: true, - level: levelData.level, - xp: levelData.xp, - totalXp: levelData.totalXp, - xpNeeded: getXpForLevel(levelData.level + 1), - leveledUp: didLevelUp - }; - - } catch (error) { - logger.error('Error adding XP:', error); - return { success: false, error: error.message }; - } - }); -} - -async function awardRoleReward(guild, member, roleId, level) { - try { - const role = guild.roles.cache.get(roleId); - - if (!role) { - logger.warn(`Role ${roleId} not found for level ${level} reward in guild ${guild.id}`); - return; - } - - if (member.roles.cache.has(roleId)) { - return; - } - - await member.roles.add(role, `Level ${level} reward`); - logger.info(`✅ Awarded role ${role.name} to ${member.user.tag} for reaching level ${level}`); - } catch (error) { - logger.error(`Failed to award role reward to ${member.user.id}:`, error); - } -} - -async function sendLevelUpAnnouncement(guild, member, levelData, config) { - try { - const levelUpChannel = config.levelUpChannel - ? guild.channels.cache.get(config.levelUpChannel) - : guild.systemChannel; - - if (!levelUpChannel || !levelUpChannel.isTextBased()) { - return; - } - - const permissions = levelUpChannel.permissionsFor(guild.members.me); - if (!permissions || !permissions.has(['SendMessages', 'EmbedLinks'])) { - logger.warn(`Missing permissions to send levelup message in ${levelUpChannel.id}`); - return; - } - - const message = config.levelUpMessage - .replace(/{user}/g, member.toString()) - .replace(/{level}/g, levelData.level) - .replace(/{xp}/g, levelData.xp) - .replace(/{xpNeeded}/g, getXpForLevel(levelData.level + 1)); - - await levelUpChannel.send(message).catch(error => { - logger.error(`Failed to send level up message in channel ${levelUpChannel.id}:`, error); - }); - } catch (error) { - logger.error('Error sending level up announcement:', error); - } -} \ No newline at end of file diff --git a/src/utils/database.js b/src/utils/database.js index f3c9bb159e..65a6f2696f 100644 --- a/src/utils/database.js +++ b/src/utils/database.js @@ -2,13 +2,7 @@ import { pgDb } from './postgresDatabase.js'; import { logger } from './logger.js'; -<<<<<<< HEAD -import { BotConfig } from '../config/bot.js'; -import { normalizeGuildConfig, validateGuildConfigOrThrow } from './schemas.js'; -import { DEFAULT_GUILD_CONFIG } from './constants.js'; -======= import { BotConfig, getDefaultApplicationQuestions } from '../config/bot.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) export { db, @@ -21,11 +15,8 @@ export { export { getGuildConfigKey, getGuildBirthdaysKey, -<<<<<<< HEAD -======= getBirthdayLeftBackupKey, getBirthdayTrackingKey, ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) getTicketKey, getTicketCounterKey, getInviteTrackingKey, @@ -33,26 +24,16 @@ export { getInviteUsesKey, getFakeAccountKey, getEconomyKey, -<<<<<<< HEAD -======= getEconomyPrefix, ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) getAFKKey, getWelcomeConfigKey, getLevelingKey, getUserLevelKey, -<<<<<<< HEAD -======= getUserLevelPrefix, ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) getApplicationRolesKey, getApplicationSettingsKey, getUserApplicationsKey, getApplicationKey, -<<<<<<< HEAD - getJoinToCreateConfigKey, - getJoinToCreateChannelsKey, -======= getApplicationsPrefix, getJoinToCreateConfigKey, getJoinToCreateChannelsKey, @@ -67,7 +48,6 @@ export { getGiveawayLockKey, canonicalizeKey, getLegacyVariantsForCanonical, ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } from './database/keys.js'; export { @@ -95,10 +75,7 @@ import { getWelcomeConfigKey, getEconomyKey, getAFKKey, -<<<<<<< HEAD -======= getUserLevelPrefix, ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } from './database/keys.js'; export async function insertVerificationAudit(record) { @@ -145,56 +122,8 @@ export function unwrapReplitData(data) { return data; } -<<<<<<< HEAD -export async function getGuildConfig(client, guildId, context = {}) { - try { - if (!client.db || typeof client.db.get !== "function") { - return {}; - } - - const configKey = getGuildConfigKey(guildId); - const rawConfig = await client.db.get(configKey, {}); - const cleanedConfig = unwrapReplitData(rawConfig); - - return normalizeGuildConfig(cleanedConfig, DEFAULT_GUILD_CONFIG); - } catch (error) { - logger.error(`Error fetching config for guild ${guildId}`, { - error, - traceId: context.traceId, - guildId, - userId: context.userId, - command: context.command - }); - return {}; - } -} - -export async function setGuildConfig(client, guildId, config, context = {}) { - try { - if (!client.db || typeof client.db.set !== "function") { - logger.error("Database client is not available for setGuildConfig"); - return false; - } - - const key = getGuildConfigKey(guildId); - const validated = validateGuildConfigOrThrow(config, { guildId, ...context }); - await client.db.set(key, validated); - return true; - } catch (error) { - logger.error(`Error saving config for guild ${guildId}`, { - error, - traceId: context.traceId, - guildId, - userId: context.userId, - command: context.command - }); - return false; - } -} -======= // Guild config access: import from services/config/guildConfig.js only. // Low-level storage lives in ./database/guildConfigStorage.js ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) export { pgDb }; @@ -347,11 +276,7 @@ export async function getEndedGiveaways(client) { } if (isPostgresSqlReady(wrapper)) { -<<<<<<< HEAD - const { pgConfig } = await import('../config/postgres.js'); -======= const { pgConfig } = await import('../config/database/postgres.js'); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) const result = await wrapper.db.pool.query( `SELECT id, guild_id, message_id, data, ends_at @@ -387,11 +312,7 @@ export async function markGiveawayEnded(client, giveawayId, endedData) { } if (isPostgresSqlReady(wrapper)) { -<<<<<<< HEAD - const { pgConfig } = await import('../config/postgres.js'); -======= const { pgConfig } = await import('../config/database/postgres.js'); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) await wrapper.db.pool.query( `UPDATE ${pgConfig.tables.giveaways} @@ -484,14 +405,11 @@ export async function getWelcomeConfig(client, guildId) { export async function saveWelcomeConfig(client, guildId, config) { const key = getWelcomeConfigKey(guildId); try { -<<<<<<< HEAD -======= if (!client.db || typeof client.db.set !== 'function') { logger.error('Database client is not available for saveWelcomeConfig.'); return false; } ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) const existingConfig = await getWelcomeConfig(client, guildId); const mergedConfig = { ...existingConfig, ...config }; @@ -629,11 +547,7 @@ export async function getLeaderboard(client, guildId, limit = 10) { return []; } -<<<<<<< HEAD - const prefix = `guild:${guildId}:leveling:users:`; -======= const prefix = getUserLevelPrefix(guildId); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) let keys = await client.db.list(prefix); if (!Array.isArray(keys)) { @@ -717,21 +631,6 @@ export async function saveApplicationRoles(client, guildId, roles) { } } -<<<<<<< HEAD -export async function getApplicationSettings(client, guildId) { - if (!client.db) { - logger.warn('Database not available for getApplicationSettings'); - return { - enabled: false, - applicationChannelId: null, - logChannelId: null, - questions: [ - "Why do you want to join our staff team?", - "What experience do you have that would make you a good fit?", - "How much time can you dedicate to this role?" - ] - }; -======= function buildApplicationSettingsDefaults() { return { enabled: false, @@ -761,7 +660,6 @@ export async function getApplicationSettings(client, guildId) { if (!client.db) { logger.warn('Database not available for getApplicationSettings'); return buildApplicationSettingsDefaults(); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } const key = getApplicationSettingsKey(guildId); @@ -769,70 +667,12 @@ export async function getApplicationSettings(client, guildId) { const settings = await client.db.get(key, {}); const unwrapped = unwrapReplitData(settings); -<<<<<<< HEAD - const defaultSettings = { - enabled: false, - applicationChannelId: null, - logChannelId: null, - questions: [ - "Why do you want to join our staff team?", - "What experience do you have that would make you a good fit?", - "How much time can you dedicate to this role?" - ], - roles: { - admin: null, - reviewer: null, - accepted: null, - denied: null - }, - requiredRoles: [], - deniedRoles: [], -minAccountAge: 0, - maxApplications: 1, -cooldown: 7, - allowMultipleApplications: false, - requireVerification: false, - customWelcomeMessage: "", - pendingApplicationRetentionDays: 30, - reviewedApplicationRetentionDays: 14 - }; -======= const defaultSettings = buildApplicationSettingsDefaults(); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) return { ...defaultSettings, ...unwrapped }; } catch (error) { logger.error(`Error getting application settings for guild ${guildId}:`, error); -<<<<<<< HEAD - return { - enabled: false, - applicationChannelId: null, - logChannelId: null, - questions: [ - "Why do you want to join our staff team?", - "What experience do you have that would make you a good fit?", - "How much time can you dedicate to this role?" - ], - roles: { - admin: null, - reviewer: null, - accepted: null, - denied: null - }, - requiredRoles: [], - deniedRoles: [], - minAccountAge: 0, - maxApplications: 1, - cooldown: 7, - allowMultipleApplications: false, - requireVerification: false, - customWelcomeMessage: "", - pendingApplicationRetentionDays: 30, - reviewedApplicationRetentionDays: 14 - }; -======= return buildApplicationSettingsDefaults(); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } } diff --git a/src/utils/database/keys.js b/src/utils/database/keys.js index 4e1f256188..139922dee8 100644 --- a/src/utils/database/keys.js +++ b/src/utils/database/keys.js @@ -1,7 +1,3 @@ -<<<<<<< HEAD -export const getGuildConfigKey = (guildId) => `guild:${guildId}:config`; -export const getGuildBirthdaysKey = (guildId) => `guild:${guildId}:birthdays`; -======= /** * Canonical database key registry. * All storage keys should be built through these helpers. @@ -11,7 +7,6 @@ export const getGuildConfigKey = (guildId) => `guild:${guildId}:config`; export const getGuildBirthdaysKey = (guildId) => `guild:${guildId}:birthdays`; export const getBirthdayLeftBackupKey = (guildId) => `guild:${guildId}:birthdays:left`; export const getBirthdayTrackingKey = (guildId) => `guild:${guildId}:birthdays:tracking`; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) export function getTicketKey(guildId, channelId) { return `guild:${guildId}:ticket:${channelId}`; @@ -41,13 +36,10 @@ export function getEconomyKey(guildId, userId) { return `guild:${guildId}:economy:${userId}`; } -<<<<<<< HEAD -======= export function getEconomyPrefix(guildId) { return `guild:${guildId}:economy:`; } ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) export function getAFKKey(guildId, userId) { return `guild:${guildId}:afk:${userId}`; } @@ -64,13 +56,10 @@ export function getUserLevelKey(guildId, userId) { return `guild:${guildId}:leveling:users:${userId}`; } -<<<<<<< HEAD -======= export function getUserLevelPrefix(guildId) { return `guild:${guildId}:leveling:users:`; } ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) export function getApplicationRolesKey(guildId) { return `guild:${guildId}:applications:roles`; } @@ -87,13 +76,10 @@ export function getApplicationKey(guildId, applicationId) { return `guild:${guildId}:applications:${applicationId}`; } -<<<<<<< HEAD -======= export function getApplicationsPrefix(guildId) { return `guild:${guildId}:applications:`; } ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) export function getJoinToCreateConfigKey(guildId) { return `guild:${guildId}:jointocreate`; } @@ -101,8 +87,6 @@ export function getJoinToCreateConfigKey(guildId) { export function getJoinToCreateChannelsKey(guildId) { return `guild:${guildId}:jointocreate:channels`; } -<<<<<<< HEAD -======= export function getWarningsKey(guildId, userId) { return `guild:${guildId}:warnings:${userId}`; @@ -266,4 +250,3 @@ export function getLegacyVariantsForCanonical(canonicalKey) { return variants; } ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) diff --git a/src/utils/database/tickets.js b/src/utils/database/tickets.js index 6348630681..58384f57be 100644 --- a/src/utils/database/tickets.js +++ b/src/utils/database/tickets.js @@ -20,11 +20,7 @@ export async function getOpenTicketCountForUser(guildId, userId) { } if (db.db?.pool && typeof db.db.isAvailable === 'function' && db.db.isAvailable()) { -<<<<<<< HEAD - const { pgConfig } = await import('../../config/postgres.js'); -======= const { pgConfig } = await import('../../config/database/postgres.js'); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) const result = await db.db.pool.query( `SELECT COUNT(*)::int AS count FROM ${pgConfig.tables.tickets} WHERE guild_id = $1 @@ -106,11 +102,7 @@ async function listGuildTickets(guildId) { } if (db.db?.pool && typeof db.db.isAvailable === 'function' && db.db.isAvailable()) { -<<<<<<< HEAD - const { pgConfig } = await import('../../config/postgres.js'); -======= const { pgConfig } = await import('../../config/database/postgres.js'); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) const result = await db.db.pool.query( `SELECT data FROM ${pgConfig.tables.tickets} WHERE guild_id = $1`, [guildId], diff --git a/src/utils/economy.js b/src/utils/economy.js index 61f99deefe..c50e9ab1f3 100644 --- a/src/utils/economy.js +++ b/src/utils/economy.js @@ -1,19 +1,12 @@ // economy.js -<<<<<<< HEAD -import { getColor } from './database.js'; -======= import { getColor, getEconomyKey as getEconomyStorageKey } from './database.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) import { BotConfig } from '../config/bot.js'; import { normalizeEconomyData } from './schemas.js'; import { logger } from './logger.js'; import { validateDiscordId, validateNumber } from './validation.js'; import { DEFAULT_ECONOMY_DATA } from './constants.js'; -<<<<<<< HEAD -======= import { createError, ErrorTypes, wrapServiceBoundary } from './errorHandler.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) const ECONOMY_CONFIG = BotConfig.economy || {}; const BASE_BANK_CAPACITY = ECONOMY_CONFIG.baseBankCapacity || 10000; @@ -36,11 +29,7 @@ export function getEconomyKey(guildId, userId) { throw new Error('Invalid guild ID or user ID'); } -<<<<<<< HEAD - return `economy:${validGuildId}:${validUserId}`; -======= return getEconomyStorageKey(validGuildId, validUserId); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } export function getMaxBankCapacity(userData) { @@ -63,12 +52,8 @@ export function getMaxBankCapacity(userData) { } export function formatCurrency(amount) { -<<<<<<< HEAD - return `${amount.toLocaleString()} ${ECONOMY_CONFIG.currency || 'coins'}`; -======= const currencyName = ECONOMY_CONFIG.currency?.name || 'coins'; return `${amount.toLocaleString()} ${currencyName}`; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } export async function getEconomyData(client, guildId, userId) { @@ -79,17 +64,12 @@ export async function getEconomyData(client, guildId, userId) { const key = getEconomyKey(guildId, userId); const data = await client.db.get(key, {}); -<<<<<<< HEAD - - return normalizeEconomyData(data, DEFAULT_ECONOMY_DATA); -======= const defaults = { ...DEFAULT_ECONOMY_DATA, wallet: ECONOMY_CONFIG.startingBalance ?? DEFAULT_ECONOMY_DATA.wallet, }; return normalizeEconomyData(data, defaults); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } catch (error) { logger.error(`Error getting economy data for user ${userId}`, error); return normalizeEconomyData({}, DEFAULT_ECONOMY_DATA); @@ -265,99 +245,6 @@ export function formatShopItem(item, index) { return `**${index + 1}.** ${item.emoji} **${item.name}** - ${formatCurrency(item.price)}\n${item.description}\n`; } -<<<<<<< HEAD -export async function addMoney(client, guildId, userId, amount, type = 'wallet') { - try { - - const validAmount = validateNumber(amount, 'amount'); - if (validAmount === null || validAmount <= 0) { - return { success: false, error: 'Amount must be a positive number' }; - } - - if (type !== 'wallet' && type !== 'bank') { - logger.warn('[VALIDATION] Invalid money type:', { type }); - return { success: false, error: 'Type must be "wallet" or "bank"' }; - } - - const userData = await getEconomyData(client, guildId, userId); - - if (type === 'bank') { - const maxBank = getMaxBankCapacity(userData); - if ((userData.bank || 0) + validAmount > maxBank) { - return { - success: false, - error: 'Bank capacity exceeded', - current: userData.bank || 0, - max: maxBank - }; - } - userData.bank = (userData.bank || 0) + validAmount; - } else { - userData.wallet = (userData.wallet || 0) + validAmount; - } - - await setEconomyData(client, guildId, userId, userData); - - return { - success: true, - newBalance: type === 'bank' ? userData.bank : userData.wallet, - ...(type === 'bank' ? { maxBank: getMaxBankCapacity(userData) } : {}) - }; - } catch (error) { - logger.error(`Error adding money to ${type} for user ${userId} in guild ${guildId}`, error); - return { success: false, error: 'An error occurred while processing your request' }; - } -} - -export async function removeMoney(client, guildId, userId, amount, type = 'wallet') { - try { - - const validAmount = validateNumber(amount, 'amount'); - if (validAmount === null || validAmount <= 0) { - return { success: false, error: 'Amount must be a positive number' }; - } - - if (type !== 'wallet' && type !== 'bank') { - logger.warn('[VALIDATION] Invalid money type:', { type }); - return { success: false, error: 'Type must be "wallet" or "bank"' }; - } - - const userData = await getEconomyData(client, guildId, userId); - - if (type === 'bank') { - if ((userData.bank || 0) < validAmount) { - return { - success: false, - error: 'Insufficient funds in bank', - current: userData.bank || 0, - required: validAmount - }; - } - userData.bank = (userData.bank || 0) - validAmount; - } else { - if ((userData.wallet || 0) < validAmount) { - return { - success: false, - error: 'Insufficient funds in wallet', - current: userData.wallet || 0, - required: validAmount - }; - } - userData.wallet = (userData.wallet || 0) - validAmount; - } - - await setEconomyData(client, guildId, userId, userData); - - return { - success: true, - newBalance: type === 'bank' ? userData.bank : userData.wallet - }; - } catch (error) { - logger.error(`Error removing money from ${type} for user ${userId} in guild ${guildId}`, error); - return { success: false, error: 'An error occurred while processing your request' }; - } -} -======= export const addMoney = wrapServiceBoundary(async function addMoney(client, guildId, userId, amount, type = 'wallet') { const validAmount = validateNumber(amount, 'amount'); if (validAmount === null || validAmount <= 0) { @@ -461,7 +348,6 @@ export const removeMoney = wrapServiceBoundary(async function removeMoney(client operation: 'removeMoney', userMessage: 'Failed to remove money. Please try again.', }); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) export function getShopInventory() { return [ diff --git a/src/utils/embeds.js b/src/utils/embeds.js index 1190980ade..67b40ad2ab 100644 --- a/src/utils/embeds.js +++ b/src/utils/embeds.js @@ -1,11 +1,7 @@ // embeds.js import { EmbedBuilder } from 'discord.js'; -<<<<<<< HEAD -import { getColor } from '../config/bot.js'; -======= import { getColor, botConfig } from '../config/bot.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) const EMOJI_REGEX = /[\p{Extended_Pictographic}\uFE0F]/gu; const EMBED_FOOTER_SYMBOL = Symbol('titanbotFooterText'); @@ -161,15 +157,12 @@ export function createEmbed({ } catch (error) { } -<<<<<<< HEAD -======= } else if (botConfig.embeds?.author?.name) { embed.setAuthor({ name: botConfig.embeds.author.name, ...(botConfig.embeds.author.icon ? { iconURL: botConfig.embeds.author.icon } : {}), ...(botConfig.embeds.author.url ? { url: botConfig.embeds.author.url } : {}), }); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } if (footer) { @@ -182,15 +175,12 @@ export function createEmbed({ } catch (error) { } -<<<<<<< HEAD -======= } else if (botConfig.embeds?.footer?.text) { const defaultFooter = { text: botConfig.embeds.footer.text, ...(botConfig.embeds.footer.icon ? { iconURL: botConfig.embeds.footer.icon } : {}), }; embed.setFooter(defaultFooter); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } if (thumbnail) { @@ -203,11 +193,8 @@ export function createEmbed({ } catch (error) { } -<<<<<<< HEAD -======= } else if (botConfig.embeds?.thumbnail) { embed.setThumbnail(botConfig.embeds.thumbnail); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } if (image) { diff --git a/src/utils/errorHandler.js b/src/utils/errorHandler.js index 738c76c4b0..d7e7c5c1e8 100644 --- a/src/utils/errorHandler.js +++ b/src/utils/errorHandler.js @@ -1,13 +1,3 @@ -<<<<<<< HEAD -// errorHandler.js -// -// User-facing error rules: -// 1. Use replyUserError or handleInteractionError — not raw errorEmbed in commands/handlers -// 2. Set a specific userMessage when you know the cause -// 3. Use ErrorTypes — don't invent custom title strings -// 4. Put guidance in the description, not embed tip fields -// 5. Success/info/warning replies use successEmbed / infoEmbed / warningEmbed -======= // errorHandler.js — the single entry point for all error handling. // // Rules: @@ -25,7 +15,6 @@ // 3. Background tasks (cron, timers): wrap with handleTaskError / runSafeTask. // 4. Set a specific userMessage when you know the cause; use ErrorTypes, don't invent titles. // 5. Success/info/warning replies use successEmbed / infoEmbed / warningEmbed. ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) import { logger } from './logger.js'; import { buildUserErrorEmbed } from './embeds.js'; @@ -33,13 +22,10 @@ import { MessageFlags } from 'discord.js'; import { getErrorMetadata, getDefaultErrorCodeByType, resolveErrorCode, ErrorCodes } from './errorRegistry.js'; import { InteractionHelper } from './interactionHelper.js'; -<<<<<<< HEAD -======= // Re-export so consumers only ever need to import from errorHandler.js export { ErrorCodes, getErrorMetadata, resolveErrorCode, getDefaultErrorCodeByType } from './errorRegistry.js'; export { ensureTypedServiceError, wrapServiceBoundary, wrapServiceClassMethods } from './serviceErrorBoundary.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) export const ErrorTypes = { VALIDATION: 'validation', PERMISSION: 'permission', @@ -64,8 +50,6 @@ export class TitanBotError extends Error { } } -<<<<<<< HEAD -======= // Discord API error codes that indicate a permission problem rather than a bug. const DISCORD_PERMISSION_CODES = new Set([ 50001, // Missing Access @@ -83,39 +67,11 @@ const DATABASE_ERROR_CODES = new Set([ '40001', '40P01', // serialization failure / deadlock ]); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) export function categorizeError(error) { if (error instanceof TitanBotError) { return error.type; } -<<<<<<< HEAD - const message = error.message?.toLowerCase() || ''; - const code = error.code; - - if (code >= 10000 && code < 20000) { - return ErrorTypes.DISCORD_API; - } - - if (message.includes('rate limit') || code === 50001) { - return ErrorTypes.RATE_LIMIT; - } - - if (message.includes('permission') || message.includes('missing') || code === 50013) { - return ErrorTypes.PERMISSION; - } - - if (message.includes('database') || message.includes('connection') || message.includes('timeout')) { - return ErrorTypes.DATABASE; - } - - if (message.includes('network') || message.includes('fetch') || message.includes('enotconn')) { - return ErrorTypes.NETWORK; - } - - if (message.includes('config') || message.includes('not found') || message.includes('invalid')) { - return ErrorTypes.CONFIGURATION; -======= const message = error?.message?.toLowerCase() || ''; const code = error?.code; @@ -146,20 +102,16 @@ export function categorizeError(error) { if (message.includes('database') || message.includes('postgres') || message.includes('sql') || message.includes('connection') || message.includes('timeout')) { return ErrorTypes.DATABASE; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } if (message.includes('validation') || message.includes('invalid') || message.includes('required')) { return ErrorTypes.VALIDATION; } -<<<<<<< HEAD -======= if (message.includes('config') || message.includes('not found')) { return ErrorTypes.CONFIGURATION; } ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) return ErrorTypes.UNKNOWN; } @@ -270,16 +222,7 @@ function buildErrorLogData(interaction, error, errorType, context = {}) { } function logInteractionError(error, errorType, logData) { -<<<<<<< HEAD - const isUserError = [ - ErrorTypes.VALIDATION, - ErrorTypes.RATE_LIMIT, - ErrorTypes.USER_INPUT, - ErrorTypes.PERMISSION - ].includes(errorType); -======= const isUserError = USER_ERROR_TYPES.has(errorType); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) const isExpectedError = Boolean(error?.context?.expected === true || error?.context?.suppressErrorLog === true); if (isUserError || isExpectedError) { @@ -336,15 +279,6 @@ async function sendErrorResponse(interaction, embed, context = {}) { } const useEphemeral = context.ephemeral !== false; -<<<<<<< HEAD - if (useEphemeral && !interaction.deferred && !interaction.replied) { - errorMessage.flags = MessageFlags.Ephemeral; - } - - if (interaction.deferred || interaction.replied) { - await interaction.editReply(errorMessage); - } else { -======= if (interaction.replied) { // A visible reply already exists; don't overwrite it — follow up ephemerally. @@ -355,7 +289,6 @@ async function sendErrorResponse(interaction, embed, context = {}) { if (useEphemeral) { errorMessage.flags = MessageFlags.Ephemeral; } ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) await interaction.reply(errorMessage); } @@ -416,19 +349,6 @@ export async function replyUserError(interaction, { return sendErrorResponse(interaction, embed, { ...context, traceId, ephemeral, subtype }); } -<<<<<<< HEAD -export async function handleInteractionError(interaction, error, context = {}) { - const errorType = categorizeError(error); - const userMessage = getUserMessage(error, context); - const { logData, traceId } = buildErrorLogData(interaction, error, errorType, context); - - logInteractionError(error, errorType, logData); - - const embed = buildUserErrorEmbed(errorType, userMessage); - await sendErrorResponse(interaction, embed, { ...context, traceId }); -} - -======= const USER_ERROR_TYPES = new Set([ ErrorTypes.VALIDATION, ErrorTypes.RATE_LIMIT, @@ -496,7 +416,6 @@ export function runSafeTask(taskName, fn, context = {}) { }; } ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) export function withErrorHandling(fn, context = {}) { return async (...args) => { try { @@ -507,15 +426,12 @@ export function withErrorHandling(fn, context = {}) { (arg.isCommand || arg.isButton || arg.isModalSubmit || arg.isStringSelectMenu || arg.isChatInputCommand || arg._isPrefixCommand) ); -<<<<<<< HEAD -======= // Slash commands are handled by interactionCreate — re-throw so the // central handler can attach trace context and command subtypes. if (interaction?.isChatInputCommand?.()) { throw error; } ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) if (interaction) { await handleInteractionError(interaction, error, context); } else { @@ -543,11 +459,8 @@ export default { getUserMessage, replyUserError, handleInteractionError, -<<<<<<< HEAD -======= handleTaskError, runSafeTask, ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) withErrorHandling, createError }; diff --git a/src/utils/errorRegistry.js b/src/utils/errorRegistry.js index 05947857ac..18880bb41b 100644 --- a/src/utils/errorRegistry.js +++ b/src/utils/errorRegistry.js @@ -12,12 +12,9 @@ const ErrorCodes = Object.freeze({ INTERACTION_INVALID: 'INTERACTION_INVALID', INTERACTION_EXPIRED: 'INTERACTION_EXPIRED', INTERACTION_RESPONSE_FAILED: 'INTERACTION_RESPONSE_FAILED', -<<<<<<< HEAD -======= INTERACTION_UNHANDLED: 'INTERACTION_UNHANDLED', TASK_ERROR: 'TASK_ERROR', UNHANDLED_REJECTION: 'UNHANDLED_REJECTION', ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) UNKNOWN_ERROR: 'UNKNOWN_ERROR' }); @@ -77,8 +74,6 @@ const ErrorCodeRegistry = Object.freeze({ retryable: false, remediation: 'Check interaction acknowledgement state and Discord response error codes.' }, -<<<<<<< HEAD -======= [ErrorCodes.INTERACTION_UNHANDLED]: { severity: 'high', retryable: false, @@ -94,7 +89,6 @@ const ErrorCodeRegistry = Object.freeze({ retryable: false, remediation: 'Find the promise that rejected without a catch handler and route it through runSafeTask or an explicit catch.' }, ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) [ErrorCodes.UNKNOWN_ERROR]: { severity: 'high', retryable: false, diff --git a/src/utils/logEmbeds.js b/src/utils/logEmbeds.js deleted file mode 100644 index f334e8dec7..0000000000 --- a/src/utils/logEmbeds.js +++ /dev/null @@ -1,242 +0,0 @@ -// logEmbeds.js — shared helpers for clean, scannable log embeds - -import { EmbedBuilder } from 'discord.js'; - -const EMOJI_PREFIX = /^[\p{Extended_Pictographic}\uFE0F\s]+/u; - -export function stripFieldLabel(name = '') { - return name.replace(EMOJI_PREFIX, '').trim() || name; -} - -export function formatLogLine(label, value) { - return `**${label}:** ${value}`; -} - -export function formatMetaLine(entries) { - return entries - .filter(([, value]) => value !== undefined && value !== null && value !== '') - .map(([label, value]) => `**${label}:** ${value}`) - .join(' • '); -} - -export function buildQuotedBlock(lines) { - return lines.map((line) => `> ${line}`).join('\n'); -} - -export function buildLogDescription({ headline, lines = [], quoted = false, meta = [] } = {}) { - const parts = []; - - if (headline) { - parts.push(headline); - } - - if (lines.length > 0) { - parts.push(quoted ? buildQuotedBlock(lines) : lines.join('\n')); - } - - if (meta.length > 0) { - parts.push(formatMetaLine(meta)); - } - - return parts.join('\n\n').slice(0, 4096); -} - -export function fieldsToLines(fields = []) { - return fields.map((field) => formatLogLine(stripFieldLabel(field.name), field.value)); -} - -export function splitComparisonFields(fields = []) { - const comparison = { before: null, after: null, rest: [] }; - - for (const field of fields) { - const label = stripFieldLabel(field.name).toLowerCase(); - - if (/^old|^before/.test(label)) { - comparison.before = field.value; - } else if (/^new|^after/.test(label)) { - comparison.after = field.value; - } else { - comparison.rest.push(field); - } - } - - return comparison; -} - -export function applyLogFooter(embed, { guild, executorId, executorTag, executorAvatar, footerText } = {}) { - if (footerText) { - embed.setFooter({ text: footerText.slice(0, 2048), iconURL: executorAvatar || undefined }); - return embed; - } - - if (executorId && executorTag) { - embed.setFooter({ - text: `${executorTag} • `, - iconURL: executorAvatar || undefined, - }); - return embed; - } - - if (guild) { - embed.setFooter({ - text: guild.name, - iconURL: guild.iconURL({ dynamic: true }) || undefined, - }); - } - - return embed; -} - -export function appendContentSection(description = '', label, content) { - if (!content) { - return description; - } - - const base = description?.trim() || ''; - const section = `**${label}**\n${content}`; - return base ? `${base}\n\n${section}`.slice(0, 4096) : section.slice(0, 4096); -} - -export function formatRatingStars(rating) { - const numeric = Number(rating); - if (!Number.isFinite(numeric) || numeric < 1) { - return null; - } - - const clamped = Math.min(5, Math.max(1, Math.round(numeric))); - return `${'⭐'.repeat(clamped)} (${clamped}/5)`; -} - -export async function resolveUserAuthor(client, userId) { - if (!userId) { - return null; - } - - try { - const user = await client.users.fetch(userId); - return { - name: user.tag, - iconURL: user.displayAvatarURL({ dynamic: true }), - }; - } catch { - return { - name: `User ${userId}`, - }; - } -} - -export function buildStandardLogEmbed({ - color, - title, - description, - thumbnail, - image, - inlineFields = [], - fields = [], - author = null, - timestamp = true, - footer, -}) { - const embed = new EmbedBuilder().setColor(color); - - if (title) embed.setTitle(title.slice(0, 256)); - if (description) embed.setDescription(description); - if (thumbnail) embed.setThumbnail(thumbnail); - if (image) embed.setImage(image); - if (author?.name) { - embed.setAuthor({ - name: author.name.slice(0, 256), - iconURL: author.iconURL || undefined, - }); - } - - const combinedFields = [...inlineFields, ...fields]; - if (combinedFields.length > 0) { - embed.addFields( - combinedFields.map((field) => ({ - name: field.name.slice(0, 256), - value: String(field.value).slice(0, 1024), - inline: field.inline === true, - })) - ); - } - - if (timestamp) embed.setTimestamp(); - - if (footer?.text) { - embed.setFooter({ - text: footer.text.slice(0, 2048), - iconURL: footer.iconURL || undefined, - }); - } - - return embed; -} - -const MAX_DISPLAYED_ROLE_PERMISSIONS = 5; - -export function buildRoleAuditFields(role, { includeMemberCount = false } = {}) { - const fields = [ - { - name: 'Role Name', - value: role.name, - inline: true - }, - { - name: 'Color', - value: role.hexColor || '#000000', - inline: true - }, - { - name: 'Role ID', - value: role.id, - inline: true - } - ]; - - const permissions = role.permissions.toArray(); - if (permissions.length > 0) { - const displayPerms = permissions.slice(0, MAX_DISPLAYED_ROLE_PERMISSIONS).join(','); - fields.push({ - name: 'Permissions', - value: permissions.length > MAX_DISPLAYED_ROLE_PERMISSIONS - ? `${displayPerms}... (+${permissions.length - MAX_DISPLAYED_ROLE_PERMISSIONS} more)` - : displayPerms, - inline: false - }); - } - - fields.push( - { - name: 'Hoisted', - value: role.hoist ? 'Yes' : 'No', - inline: true - }, - { - name: 'Managed', - value: role.managed ? 'Yes (Bot role)' : 'No', - inline: true - }, - { - name: 'Position', - value: role.position.toString(), - inline: true - } - ); - - if (includeMemberCount) { - fields.push({ - name: 'Members with Role', - value: role.members.size.toString(), - inline: true - }); - } - - return fields; -} - -export function buildRoleAuditLines(role, options = {}) { - return buildRoleAuditFields(role, options).map((field) => - formatLogLine(field.name, field.value), - ); -} diff --git a/src/utils/loggingUi.js b/src/utils/loggingUi.js deleted file mode 100644 index 6c3bc6f4d6..0000000000 --- a/src/utils/loggingUi.js +++ /dev/null @@ -1,196 +0,0 @@ -// loggingUi.js - -import { - ActionRowBuilder, - ButtonBuilder, - ButtonStyle, - StringSelectMenuBuilder, - StringSelectMenuOptionBuilder, -} from 'discord.js'; -import { EVENT_TYPES } from '../services/loggingService.js'; - -const EVENT_TYPES_BY_CATEGORY = Object.values(EVENT_TYPES).reduce((accumulator, eventType) => { - const [category] = eventType.split('.'); - if (!accumulator[category]) { - accumulator[category] = []; - } - accumulator[category].push(eventType); - return accumulator; -}, {}); - -export const DASHBOARD_CATEGORIES = [ - 'moderation', - 'message', - 'role', - 'member', - 'leveling', - 'reactionrole', - 'giveaway', - 'counter', - 'application', - 'report', -]; - -const DASHBOARD_CATEGORY_EMOJIS = { - moderation: '🔨', - message: '✉️', - role: '🏷️', - member: '👥', - leveling: '📈', - reactionrole: '🎭', - giveaway: '🎁', - counter: '📊', - application: '📝', - report: '🚨', -}; - -export const DASHBOARD_CATEGORY_LABELS = { - moderation: 'Moderation', - message: 'Messages', - role: 'Roles', - member: 'Members', - leveling: 'Leveling', - reactionrole: 'Reaction Roles', - giveaway: 'Giveaways', - counter: 'Counters', - application: 'Applications', - report: 'Reports', -}; - -function createBackButton() { - return new ButtonBuilder() - .setCustomId('log_dash_back') - .setLabel('Back to Dashboard') - .setStyle(ButtonStyle.Secondary); -} - -function createCategoryToggleButtons(enabledEvents = {}, loggingEnabled = false) { - const buttons = DASHBOARD_CATEGORIES.map((category) => { - const wildcardDisabled = enabledEvents[`${category}.*`] === false; - const categoryEvents = EVENT_TYPES_BY_CATEGORY[category] || []; - const allEnabled = categoryEvents.length === 0 - ? true - : categoryEvents.every((t) => enabledEvents[t] !== false); - const isEnabled = loggingEnabled && !wildcardDisabled && allEnabled; - const emoji = DASHBOARD_CATEGORY_EMOJIS[category] || '📌'; - const label = DASHBOARD_CATEGORY_LABELS[category] || category; - - return new ButtonBuilder() - .setCustomId(`log_dash_toggle:${category}.*`) - .setLabel(`${emoji} ${label}`) - .setStyle(isEnabled ? ButtonStyle.Success : ButtonStyle.Danger); - }); - - const rows = []; - for (let i = 0; i < buttons.length; i += 5) { - rows.push(new ActionRowBuilder().addComponents(buttons.slice(i, i + 5))); - } - return rows; -} - -export function createLoggingMainMenuSelect() { - return new ActionRowBuilder().addComponents( - new StringSelectMenuBuilder() - .setCustomId('log_dash_menu') - .setPlaceholder('Choose a setting to configure…') - .addOptions( - new StringSelectMenuOptionBuilder() - .setLabel('Set Audit Log Channel') - .setDescription('Moderation, messages, members, roles, etc.') - .setValue('set:audit') - .setEmoji('🧾'), - new StringSelectMenuOptionBuilder() - .setLabel('Set Applications Channel') - .setDescription('New applications and review updates') - .setValue('set:applications') - .setEmoji('📝'), - new StringSelectMenuOptionBuilder() - .setLabel('Set Reports Channel') - .setDescription('User reports filed via /report') - .setValue('set:reports') - .setEmoji('🚨'), - new StringSelectMenuOptionBuilder() - .setLabel('Clear Audit Channel') - .setValue('clear:audit') - .setEmoji('🗑️'), - new StringSelectMenuOptionBuilder() - .setLabel('Clear Applications Channel') - .setValue('clear:applications') - .setEmoji('🗑️'), - new StringSelectMenuOptionBuilder() - .setLabel('Clear Reports Channel') - .setValue('clear:reports') - .setEmoji('🗑️'), - new StringSelectMenuOptionBuilder() - .setLabel('Event Categories') - .setDescription('Toggle which log types are sent') - .setValue('view:categories') - .setEmoji('📋'), - new StringSelectMenuOptionBuilder() - .setLabel('Manage Ignore Filters') - .setDescription('Skip logs from specific users or channels') - .setValue('view:filters') - .setEmoji('🔇'), - ), - ); -} - -export function createLoggingMainActionRow(loggingEnabled = false) { - return new ActionRowBuilder().addComponents( - new ButtonBuilder() - .setCustomId('log_dash_toggle:audit_enabled') - .setLabel('Audit Logging') - .setStyle(loggingEnabled ? ButtonStyle.Success : ButtonStyle.Danger), - new ButtonBuilder() - .setCustomId('log_dash_refresh') - .setLabel('Refresh') - .setStyle(ButtonStyle.Primary), - ); -} - -export function createLoggingDashboardComponents(_enabledEvents, loggingEnabled = false) { - return [ - createLoggingMainMenuSelect(), - createLoggingMainActionRow(loggingEnabled), - ]; -} - -export function createLoggingCategoryViewComponents(enabledEvents, loggingEnabled = false) { - const categoryRows = createCategoryToggleButtons(enabledEvents, loggingEnabled); - - const actionRow = new ActionRowBuilder().addComponents( - createBackButton(), - new ButtonBuilder() - .setCustomId('log_dash_toggle:all') - .setLabel('Toggle All Categories') - .setStyle(ButtonStyle.Secondary), - new ButtonBuilder() - .setCustomId('log_dash_refresh') - .setLabel('Refresh') - .setStyle(ButtonStyle.Primary), - ); - - return [...categoryRows, actionRow]; -} - -export function createLoggingFilterComponents() { - return [ - new ActionRowBuilder().addComponents( - createBackButton(), - new ButtonBuilder() - .setCustomId('log_dash_add_filter:user') - .setLabel('Add User Filter') - .setStyle(ButtonStyle.Primary), - new ButtonBuilder() - .setCustomId('log_dash_add_filter:channel') - .setLabel('Add Channel Filter') - .setStyle(ButtonStyle.Primary), - new ButtonBuilder() - .setCustomId('log_dash_remove_filter') - .setLabel('Remove Filter') - .setStyle(ButtonStyle.Danger), - ), - ]; -} - -export { EVENT_TYPES_BY_CATEGORY }; diff --git a/src/utils/memoryStorage.js b/src/utils/memoryStorage.js index 6234c4c996..26d3465b01 100644 --- a/src/utils/memoryStorage.js +++ b/src/utils/memoryStorage.js @@ -41,11 +41,7 @@ class MemoryStorage { async list(prefix) { const keys = []; -<<<<<<< HEAD - for (const [key] of this.data.keys()) { -======= for (const key of this.data.keys()) { ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) if (key.startsWith(prefix)) { if (this.expirationTimes.has(key)) { const expirationTime = this.expirationTimes.get(key); diff --git a/src/utils/messageAdapter.js b/src/utils/messageAdapter.js index 2de63fbef6..2d69d689b4 100644 --- a/src/utils/messageAdapter.js +++ b/src/utils/messageAdapter.js @@ -2,17 +2,11 @@ import { mapArgumentsToOptions } from './prefixParser.js'; import { createEmbed } from './embeds.js'; -<<<<<<< HEAD -import { logger } from './logger.js'; -import { InteractionHelper } from './interactionHelper.js'; -import { SLASH_ONLY_COMMANDS } from '../config/prefixRestrictions.js'; -======= import { handleInteractionError } from './errorHandler.js'; import { logger } from './logger.js'; import { InteractionHelper } from './interactionHelper.js'; import { SLASH_ONLY_COMMANDS } from '../config/commands/prefixRestrictions.js'; import { getCommandPrefix } from '../config/bot.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) import { ResponseCoordinator, buildPrefixUsage } from './responseCoordinator.js'; import { enforceDefaultCommandPermissions } from './permissionGuard.js'; @@ -197,11 +191,7 @@ export function supportsPrefixExecution(command) { export async function executePrefixCommand(command, message, args, client, prefixOverride = null, guildConfig = null) { const mockInteraction = createMockInteraction(message, command.data, args); const coordinator = mockInteraction._responseCoordinator; -<<<<<<< HEAD - const prefix = prefixOverride || message.client?.config?.bot?.prefix || '!'; -======= const prefix = prefixOverride || getCommandPrefix(); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) try { const permissionAllowed = await enforceDefaultCommandPermissions(mockInteraction, command, { @@ -224,30 +214,10 @@ export async function executePrefixCommand(command, message, args, client, prefi await command.execute(mockInteraction, guildConfig, client); } } catch (error) { -<<<<<<< HEAD - logger.error('Prefix command execution error:', { - command: command.data.name, - args, - error: error.message, - stack: error.stack, - }); - - if (!coordinator.hasResponded()) { - const embed = createEmbed({ - title: 'Command Execution Failed', - description: `An error occurred while executing this command.\n\n${error.message}`, - color: 'error', - }); - await coordinator.respond({ embeds: [embed] }); - } - - throw error; -======= await handleInteractionError(mockInteraction, error, { type: 'prefix_command', command: command.data?.name, source: 'messageAdapter.executePrefixCommand', }); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } } diff --git a/src/utils/moderation.js b/src/utils/moderation.js index a2a2568bf8..13ac5e4e53 100644 --- a/src/utils/moderation.js +++ b/src/utils/moderation.js @@ -1,7 +1,7 @@ // moderation.js import { logEvent as logAuditEvent, EVENT_TYPES } from '../services/loggingService.js'; -import { formatLogLine } from './logEmbeds.js'; +import { formatLogLine } from './logging/logEmbeds.js'; import { logger } from './logger.js'; import { getFromDb, setInDb } from './database.js'; @@ -17,10 +17,7 @@ const ACTION_TO_EVENT_TYPE = { 'Channel Locked': EVENT_TYPES.MODERATION_LOCK, 'Channel Unlocked': EVENT_TYPES.MODERATION_UNLOCK, 'DM Sent': EVENT_TYPES.MODERATION_DM, -<<<<<<< HEAD -======= 'Bot Message Sent': EVENT_TYPES.MODERATION_CONFIG, ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) 'Log Channel Activated': EVENT_TYPES.MODERATION_CONFIG, 'Log Filter Updated': EVENT_TYPES.MODERATION_CONFIG, 'Case Created': EVENT_TYPES.MODERATION_CONFIG, diff --git a/src/utils/permissionGuard.js b/src/utils/permissionGuard.js index eb4ff78f46..d6cb4a84d5 100644 --- a/src/utils/permissionGuard.js +++ b/src/utils/permissionGuard.js @@ -3,10 +3,7 @@ import { PermissionFlagsBits } from 'discord.js'; import { logger } from './logger.js'; import { replyUserError, ErrorTypes } from './errorHandler.js'; -<<<<<<< HEAD -======= import { isBotOwner, getBotMessage } from '../config/bot.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) /** * Read default_member_permissions from a SlashCommandBuilder (or its JSON). @@ -152,13 +149,10 @@ export async function checkModerationPermissions( * @returns {Promise} true when the member may proceed */ export async function enforceDefaultCommandPermissions(interaction, command, context = {}) { -<<<<<<< HEAD -======= if (isBotOwner(interaction.user?.id)) { return true; } ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) const requiredPermissions = getCommandDefaultPermissions(command?.data); if (requiredPermissions == null) { return true; @@ -175,11 +169,7 @@ export async function enforceDefaultCommandPermissions(interaction, command, con const commandName = command?.data?.name ?? interaction.commandName ?? 'command'; await replyUserError(interaction, { type: ErrorTypes.PERMISSION, -<<<<<<< HEAD - message: 'You do not have permission to use this command.', -======= message: getBotMessage('noPermission'), ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) context: { source: context.source ?? 'permissionGuard.enforceDefaultCommandPermissions', commandName, diff --git a/src/utils/postgresDatabase.js b/src/utils/postgresDatabase.js index f0752f7f80..84e9678f0c 100644 --- a/src/utils/postgresDatabase.js +++ b/src/utils/postgresDatabase.js @@ -1,11 +1,6 @@ // postgresDatabase.js import pg from 'pg'; -<<<<<<< HEAD -import { pgConfig, resolvePostgresPoolConfig } from '../config/postgres.js'; -import { logger } from './logger.js'; -import { assertAllowlistedIdentifier, quoteIdentifier } from './sqlIdentifiers.js'; -======= import { pgConfig, resolvePostgresPoolConfig } from '../config/database/postgres.js'; import { logger } from './logger.js'; import { assertAllowlistedIdentifier, quoteIdentifier } from './sqlIdentifiers.js'; @@ -25,7 +20,6 @@ import { UPDATE_TIMESTAMP_FUNCTION, triggerDefinitions, } from './database/schema.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) class PostgreSQLDatabase { constructor() { @@ -110,10 +104,7 @@ class PostgreSQLDatabase { logger.warn( `No schema version found. Bootstrapped schema ledger to version ${pgConfig.migration.expectedVersion} (${pgConfig.migration.expectedLabel}).` ); -<<<<<<< HEAD -======= await this.runStartupKeyMigration(); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) return true; } @@ -125,10 +116,7 @@ class PostgreSQLDatabase { } } -<<<<<<< HEAD -======= await this.runStartupKeyMigration(); ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) return true; } catch (error) { this.lastFailureReason = error.code || 'POSTGRES_CONNECTION_FAILED'; @@ -167,8 +155,6 @@ class PostgreSQLDatabase { return false; } -<<<<<<< HEAD -======= async runStartupKeyMigration() { if (pgConfig.features.autoMigrate === false) { return; @@ -187,7 +173,6 @@ class PostgreSQLDatabase { } } ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) isAvailable() { return this.isConnected && this.pool; } @@ -269,176 +254,7 @@ class PostgreSQLDatabase { } async createTables() { -<<<<<<< HEAD - const tables = [ - `CREATE TABLE IF NOT EXISTS ${pgConfig.tables.guilds} ( - id VARCHAR(20) PRIMARY KEY, - config JSONB DEFAULT '{}', - counters JSONB DEFAULT '[]', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - )`, - - `CREATE TABLE IF NOT EXISTS ${pgConfig.tables.users} ( - id VARCHAR(20) PRIMARY KEY, - username VARCHAR(100), - discriminator VARCHAR(10), - avatar VARCHAR(100), - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - )`, - - `CREATE TABLE IF NOT EXISTS ${pgConfig.tables.guild_users} ( - guild_id VARCHAR(20), - user_id VARCHAR(20), - joined_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (guild_id, user_id), - FOREIGN KEY (guild_id) REFERENCES ${pgConfig.tables.guilds}(id) ON DELETE CASCADE, - FOREIGN KEY (user_id) REFERENCES ${pgConfig.tables.users}(id) ON DELETE CASCADE - )`, - - `CREATE TABLE IF NOT EXISTS ${pgConfig.tables.birthdays} ( - guild_id VARCHAR(20), - user_id VARCHAR(20), - month INTEGER NOT NULL, - day INTEGER NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (guild_id, user_id), - FOREIGN KEY (guild_id) REFERENCES ${pgConfig.tables.guilds}(id) ON DELETE CASCADE, - FOREIGN KEY (user_id) REFERENCES ${pgConfig.tables.users}(id) ON DELETE CASCADE - )`, - - `CREATE TABLE IF NOT EXISTS ${pgConfig.tables.giveaways} ( - id SERIAL PRIMARY KEY, - guild_id VARCHAR(20), - message_id VARCHAR(20) NOT NULL, - data JSONB NOT NULL, - ends_at TIMESTAMP, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (guild_id) REFERENCES ${pgConfig.tables.guilds}(id) ON DELETE CASCADE, - UNIQUE(guild_id, message_id) - )`, - - `CREATE TABLE IF NOT EXISTS ${pgConfig.tables.tickets} ( - guild_id VARCHAR(20), - channel_id VARCHAR(20) PRIMARY KEY, - data JSONB NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - expires_at TIMESTAMP, - FOREIGN KEY (guild_id) REFERENCES ${pgConfig.tables.guilds}(id) ON DELETE CASCADE - )`, - - `CREATE TABLE IF NOT EXISTS ${pgConfig.tables.afk_status} ( - guild_id VARCHAR(20), - user_id VARCHAR(20), - reason TEXT, - status_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - expires_at TIMESTAMP, - PRIMARY KEY (guild_id, user_id), - FOREIGN KEY (guild_id) REFERENCES ${pgConfig.tables.guilds}(id) ON DELETE CASCADE, - FOREIGN KEY (user_id) REFERENCES ${pgConfig.tables.users}(id) ON DELETE CASCADE - )`, - - `CREATE TABLE IF NOT EXISTS ${pgConfig.tables.welcome_configs} ( - guild_id VARCHAR(20) PRIMARY KEY, - config JSONB NOT NULL DEFAULT '{}', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (guild_id) REFERENCES ${pgConfig.tables.guilds}(id) ON DELETE CASCADE - )`, - - `CREATE TABLE IF NOT EXISTS ${pgConfig.tables.leveling_configs} ( - guild_id VARCHAR(20) PRIMARY KEY, - config JSONB NOT NULL DEFAULT '{}', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (guild_id) REFERENCES ${pgConfig.tables.guilds}(id) ON DELETE CASCADE - )`, - - `CREATE TABLE IF NOT EXISTS ${pgConfig.tables.user_levels} ( - guild_id VARCHAR(20), - user_id VARCHAR(20), - xp BIGINT DEFAULT 0, - level INTEGER DEFAULT 0, - total_xp BIGINT DEFAULT 0, - last_message TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - rank INTEGER DEFAULT 0, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (guild_id, user_id), - FOREIGN KEY (guild_id) REFERENCES ${pgConfig.tables.guilds}(id) ON DELETE CASCADE, - FOREIGN KEY (user_id) REFERENCES ${pgConfig.tables.users}(id) ON DELETE CASCADE - )`, - - `CREATE TABLE IF NOT EXISTS ${pgConfig.tables.economy} ( - guild_id VARCHAR(20), - user_id VARCHAR(20), - balance BIGINT DEFAULT 0, - bank BIGINT DEFAULT 0, - data JSONB DEFAULT '{}', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (guild_id, user_id), - FOREIGN KEY (guild_id) REFERENCES ${pgConfig.tables.guilds}(id) ON DELETE CASCADE, - FOREIGN KEY (user_id) REFERENCES ${pgConfig.tables.users}(id) ON DELETE CASCADE - )`, - - `CREATE TABLE IF NOT EXISTS ${pgConfig.tables.verification_audit} ( - id SERIAL PRIMARY KEY, - guild_id VARCHAR(20) NOT NULL, - user_id VARCHAR(20) NOT NULL, - action VARCHAR(50) NOT NULL, - source VARCHAR(50), - moderator_id VARCHAR(20), - metadata JSONB DEFAULT '{}', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - )`, - - `CREATE TABLE IF NOT EXISTS ${pgConfig.tables.invite_tracking} ( - guild_id VARCHAR(20), - inviter_id VARCHAR(20), - invite_code VARCHAR(20), - uses INTEGER DEFAULT 0, - data JSONB DEFAULT '{}', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (guild_id, invite_code), - FOREIGN KEY (guild_id) REFERENCES ${pgConfig.tables.guilds}(id) ON DELETE CASCADE - )`, - - `CREATE TABLE IF NOT EXISTS ${pgConfig.tables.application_roles} ( - guild_id VARCHAR(20), - role_id VARCHAR(20), - data JSONB DEFAULT '{}', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (guild_id, role_id), - FOREIGN KEY (guild_id) REFERENCES ${pgConfig.tables.guilds}(id) ON DELETE CASCADE - )`, - - `CREATE TABLE IF NOT EXISTS ${pgConfig.tables.temp_data} ( - key VARCHAR(255) PRIMARY KEY, - value JSONB NOT NULL, - expires_at TIMESTAMP, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - )`, - - `CREATE TABLE IF NOT EXISTS ${pgConfig.tables.cache_data} ( - key VARCHAR(255) PRIMARY KEY, - value JSONB NOT NULL, - expires_at TIMESTAMP, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - )` - ]; - - for (const table of tables) { -======= for (const table of tableStatements) { ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) try { await this.pool.query(table); } catch (error) { @@ -453,32 +269,7 @@ class PostgreSQLDatabase { } async createIndexes() { -<<<<<<< HEAD - const indexes = [ - `CREATE INDEX IF NOT EXISTS idx_guild_users_guild_id ON ${pgConfig.tables.guild_users}(guild_id)`, - `CREATE INDEX IF NOT EXISTS idx_guild_users_user_id ON ${pgConfig.tables.guild_users}(user_id)`, - `CREATE INDEX IF NOT EXISTS idx_birthdays_guild_id ON ${pgConfig.tables.birthdays}(guild_id)`, - `CREATE INDEX IF NOT EXISTS idx_birthdays_month_day ON ${pgConfig.tables.birthdays}(month, day)`, - `CREATE INDEX IF NOT EXISTS idx_giveaways_guild_id ON ${pgConfig.tables.giveaways}(guild_id)`, - `CREATE INDEX IF NOT EXISTS idx_giveaways_ends_at ON ${pgConfig.tables.giveaways}(ends_at)`, - `CREATE INDEX IF NOT EXISTS idx_tickets_guild_id ON ${pgConfig.tables.tickets}(guild_id)`, - `CREATE INDEX IF NOT EXISTS idx_tickets_expires_at ON ${pgConfig.tables.tickets}(expires_at)`, - `CREATE INDEX IF NOT EXISTS idx_afk_status_guild_id ON ${pgConfig.tables.afk_status}(guild_id)`, - `CREATE INDEX IF NOT EXISTS idx_afk_status_expires_at ON ${pgConfig.tables.afk_status}(expires_at)`, - `CREATE INDEX IF NOT EXISTS idx_user_levels_guild_id ON ${pgConfig.tables.user_levels}(guild_id)`, - `CREATE INDEX IF NOT EXISTS idx_user_levels_xp ON ${pgConfig.tables.user_levels}(xp)`, - `CREATE INDEX IF NOT EXISTS idx_economy_guild_id ON ${pgConfig.tables.economy}(guild_id)`, - `CREATE INDEX IF NOT EXISTS idx_verification_audit_guild_id ON ${pgConfig.tables.verification_audit}(guild_id)`, - `CREATE INDEX IF NOT EXISTS idx_verification_audit_user_id ON ${pgConfig.tables.verification_audit}(user_id)`, - `CREATE INDEX IF NOT EXISTS idx_verification_audit_created_at ON ${pgConfig.tables.verification_audit}(created_at)`, - `CREATE INDEX IF NOT EXISTS idx_temp_data_expires_at ON ${pgConfig.tables.temp_data}(expires_at)`, - `CREATE INDEX IF NOT EXISTS idx_cache_data_expires_at ON ${pgConfig.tables.cache_data}(expires_at)` - ]; - - for (const index of indexes) { -======= for (const index of indexStatements) { ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) try { await this.pool.query(index); } catch (error) { @@ -491,39 +282,9 @@ class PostgreSQLDatabase { async createAuditTriggers() { try { -<<<<<<< HEAD - const functionQuery = ` - CREATE OR REPLACE FUNCTION update_updated_at_column() - RETURNS TRIGGER AS $$ - BEGIN - NEW.updated_at = CURRENT_TIMESTAMP; - RETURN NEW; - END; - $$ language 'plpgsql'; - `; - - await this.pool.query(functionQuery); - - const triggers = [ - { name: 'update_guilds_updated_at', table: pgConfig.tables.guilds }, - { name: 'update_users_updated_at', table: pgConfig.tables.users }, - { name: 'update_welcome_configs_updated_at', table: pgConfig.tables.welcome_configs }, - { name: 'update_leveling_configs_updated_at', table: pgConfig.tables.leveling_configs }, - { name: 'update_user_levels_updated_at', table: pgConfig.tables.user_levels }, - { name: 'update_economy_updated_at', table: pgConfig.tables.economy }, - { name: 'update_application_roles_updated_at', table: pgConfig.tables.application_roles }, - { name: 'update_invite_tracking_updated_at', table: pgConfig.tables.invite_tracking }, - { name: 'update_guild_users_updated_at', table: pgConfig.tables.guild_users }, - { name: 'update_birthdays_updated_at', table: pgConfig.tables.birthdays }, - { name: 'update_giveaways_updated_at', table: pgConfig.tables.giveaways }, - { name: 'update_tickets_updated_at', table: pgConfig.tables.tickets }, - { name: 'update_afk_status_updated_at', table: pgConfig.tables.afk_status }, - ]; -======= await this.pool.query(UPDATE_TIMESTAMP_FUNCTION); const triggers = triggerDefinitions; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) const allowedTriggerIdentifiers = new Set(triggers.map(trigger => trigger.name)); @@ -559,8 +320,6 @@ class PostgreSQLDatabase { } } -<<<<<<< HEAD -======= async _getTempValue(key, defaultValue = null) { const result = await this.pool.query( `SELECT value FROM ${pgConfig.tables.temp_data} WHERE key = $1 AND (expires_at IS NULL OR expires_at > NOW())`, @@ -590,7 +349,6 @@ class PostgreSQLDatabase { return defaultValue; } ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) async get(key, defaultValue = null) { try { if (!this.isAvailable()) { @@ -598,22 +356,6 @@ class PostgreSQLDatabase { return defaultValue; } -<<<<<<< HEAD - const parsedKey = this.parseKey(key); - - if (parsedKey.type === 'temp') { - const result = await this.pool.query( - `SELECT value FROM ${pgConfig.tables.temp_data} WHERE key = $1 AND (expires_at IS NULL OR expires_at > NOW())`, - [parsedKey.fullKey] - ); - return result.rows.length > 0 ? result.rows[0].value : defaultValue; - } - - if (parsedKey.type === 'cache') { - const result = await this.pool.query( - `SELECT value FROM ${pgConfig.tables.cache_data} WHERE key = $1 AND (expires_at IS NULL OR expires_at > NOW())`, - [parsedKey.fullKey] -======= const canonicalKey = canonicalizeKey(key); const parsedKey = parseKey(canonicalKey); @@ -625,14 +367,10 @@ class PostgreSQLDatabase { const result = await this.pool.query( `SELECT value FROM ${pgConfig.tables.cache_data} WHERE key = $1 AND (expires_at IS NULL OR expires_at > NOW())`, [parsedKey.fullKey], ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) ); return result.rows.length > 0 ? result.rows[0].value : defaultValue; } -<<<<<<< HEAD - return await this.getStructuredData(parsedKey, defaultValue); -======= const structuredValue = await this.getStructuredData(parsedKey, defaultValue); if (structuredValue !== defaultValue) { return structuredValue; @@ -646,7 +384,6 @@ class PostgreSQLDatabase { } return structuredValue; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } catch (error) { logger.error(`Error getting value for key ${key}:`, error); return defaultValue; @@ -660,28 +397,6 @@ class PostgreSQLDatabase { return false; } -<<<<<<< HEAD - const parsedKey = this.parseKey(key); - const expiresAt = ttl ? new Date(Date.now() + ttl * 1000) : null; - const jsonValue = JSON.stringify(value ?? null); - - if (parsedKey.type === 'temp') { - await this.pool.query( - `INSERT INTO ${pgConfig.tables.temp_data} (key, value, expires_at) - VALUES ($1, $2, $3) - ON CONFLICT (key) DO UPDATE SET value = $2, expires_at = $3`, - [parsedKey.fullKey, jsonValue, expiresAt] - ); - return true; - } - - if (parsedKey.type === 'cache') { - await this.pool.query( - `INSERT INTO ${pgConfig.tables.cache_data} (key, value, expires_at) - VALUES ($1, $2, $3) - ON CONFLICT (key) DO UPDATE SET value = $2, expires_at = $3`, - [parsedKey.fullKey, jsonValue, expiresAt] -======= const canonicalKey = canonicalizeKey(key); const parsedKey = parseKey(canonicalKey); const expiresAt = ttl ? new Date(Date.now() + ttl * 1000) : null; @@ -703,7 +418,6 @@ class PostgreSQLDatabase { VALUES ($1, $2, $3) ON CONFLICT (key) DO UPDATE SET value = $2, expires_at = $3`, [parsedKey.fullKey, jsonValue, expiresAt], ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) ); return true; } @@ -722,21 +436,6 @@ class PostgreSQLDatabase { return false; } -<<<<<<< HEAD - const parsedKey = this.parseKey(key); - - if (parsedKey.type === 'temp') { - await this.pool.query(`DELETE FROM ${pgConfig.tables.temp_data} WHERE key = $1`, [parsedKey.fullKey]); - return true; - } - - if (parsedKey.type === 'cache') { - await this.pool.query(`DELETE FROM ${pgConfig.tables.cache_data} WHERE key = $1`, [parsedKey.fullKey]); - return true; - } - - return await this.deleteStructuredData(parsedKey); -======= const canonicalKey = canonicalizeKey(key); const parsedKey = parseKey(canonicalKey); let deleted = false; @@ -760,7 +459,6 @@ class PostgreSQLDatabase { } return deleted; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } catch (error) { logger.error(`Error deleting key ${key}:`, error); return false; @@ -774,23 +472,6 @@ class PostgreSQLDatabase { return []; } -<<<<<<< HEAD - const keys = []; - - const tempResult = await this.pool.query( - `SELECT key FROM ${pgConfig.tables.temp_data} WHERE key LIKE $1 AND (expires_at IS NULL OR expires_at > NOW())`, - [`${prefix}%`] - ); - keys.push(...tempResult.rows.map(row => row.key)); - - const cacheResult = await this.pool.query( - `SELECT key FROM ${pgConfig.tables.cache_data} WHERE key LIKE $1 AND (expires_at IS NULL OR expires_at > NOW())`, - [`${prefix}%`] - ); - keys.push(...cacheResult.rows.map(row => row.key)); - - return keys; -======= const keys = new Set(); const plan = getStructuredListPlan(prefix, pgConfig.tables); const tempPrefixes = plan.tempPrefixes ?? [prefix]; @@ -828,7 +509,6 @@ class PostgreSQLDatabase { } return [...keys]; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) } catch (error) { logger.error(`Error listing keys with prefix ${prefix}:`, error); return []; @@ -912,61 +592,6 @@ class PostgreSQLDatabase { } } -<<<<<<< HEAD - parseKey(key) { - - if (key.startsWith('temp:')) { - return { type: 'temp', fullKey: key }; - } - if (key.startsWith('cache:')) { - return { type: 'cache', fullKey: key }; - } - - const parts = key.split(':'); - - if (parts[0] === 'guild') { - if (parts[2] === 'config') { - return { type: 'guild_config', guildId: parts[1], fullKey: key }; - } - if (parts[2] === 'birthdays') { - return { type: 'guild_birthdays', guildId: parts[1], fullKey: key }; - } - if (parts[2] === 'giveaways') { - return { type: 'guild_giveaways', guildId: parts[1], fullKey: key }; - } - if (parts[2] === 'welcome') { - return { type: 'welcome_config', guildId: parts[1], fullKey: key }; - } - if (parts[2] === 'leveling') { - - if (parts[3] === 'config') { - return { type: 'leveling_config', guildId: parts[1], fullKey: key }; - } - if (parts[3] === 'users') { - return { type: 'user_level', guildId: parts[1], userId: parts[4], fullKey: key }; - } - return { type: 'leveling_data', guildId: parts[1], fullKey: key }; - } - if (parts[2] === 'economy' && parts[3]) { - return { type: 'economy', guildId: parts[1], userId: parts[3], fullKey: key }; - } - if (parts[2] === 'afk' && parts[3]) { - return { type: 'afk_status', guildId: parts[1], userId: parts[3], fullKey: key }; - } - if (parts[2] === 'ticket' && parts[3]) { - return { type: 'ticket', guildId: parts[1], channelId: parts[3], fullKey: key }; - } - } - - if (parts[0] === 'counters' && parts[1]) { - return { type: 'counters', guildId: parts[1], fullKey: key }; - } - - return { type: 'temp', fullKey: key }; - } - -======= ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) async getStructuredData(parsedKey, defaultValue) { try { switch (parsedKey.type) { @@ -1009,18 +634,11 @@ class PostgreSQLDatabase { ); return levelingConfigResult.rows.length > 0 ? levelingConfigResult.rows[0].config : defaultValue; -<<<<<<< HEAD - case 'user_level': -======= case 'user_level': { ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) const userLevelResult = await this.pool.query( `SELECT xp, level, total_xp, last_message, rank FROM ${pgConfig.tables.user_levels} WHERE guild_id = $1 AND user_id = $2`, [parsedKey.guildId, parsedKey.userId] ); -<<<<<<< HEAD - return userLevelResult.rows.length > 0 ? userLevelResult.rows[0] : defaultValue; -======= if (userLevelResult.rows.length === 0) return defaultValue; // Map snake_case columns to the camelCase shape consumers expect const levelRow = userLevelResult.rows[0]; @@ -1032,7 +650,6 @@ class PostgreSQLDatabase { rank: Number(levelRow.rank) || 0, }; } ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) case 'economy': { const economyResult = await this.pool.query( @@ -1048,14 +665,6 @@ class PostgreSQLDatabase { return { wallet: row.balance ?? 0, bank: row.bank ?? 0 }; } -<<<<<<< HEAD - case 'afk_status': - const afkResult = await this.pool.query( - `SELECT reason, status_at, expires_at FROM ${pgConfig.tables.afk_status} WHERE guild_id = $1 AND user_id = $2`, - [parsedKey.guildId, parsedKey.userId] - ); - return afkResult.rows.length > 0 ? afkResult.rows[0] : defaultValue; -======= case 'afk_status': { const afkResult = await this.pool.query( `SELECT reason, status_at, expires_at FROM ${pgConfig.tables.afk_status} WHERE guild_id = $1 AND user_id = $2`, @@ -1069,7 +678,6 @@ class PostgreSQLDatabase { expiresAt: row.expires_at, }; } ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) case 'ticket': const ticketResult = await this.pool.query( @@ -1258,11 +866,7 @@ class PostgreSQLDatabase { VALUES ($1, $2, $3, $4) ON CONFLICT (guild_id, user_id) DO UPDATE SET reason = $3, expires_at = $4, status_at = CURRENT_TIMESTAMP`, -<<<<<<< HEAD - [parsedKey.guildId, parsedKey.userId, value.reason, value.expiresAt ? new Date(value.expiresAt) : null] -======= [parsedKey.guildId, parsedKey.userId, value.reason, (value.expiresAt ?? value.expires_at) ? new Date(value.expiresAt ?? value.expires_at) : null] ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) ); return true; @@ -1376,8 +980,6 @@ class PostgreSQLDatabase { case 'ticket': await this.pool.query(`DELETE FROM ${pgConfig.tables.tickets} WHERE guild_id = $1 AND channel_id = $2`, [parsedKey.guildId, parsedKey.channelId]); return true; -<<<<<<< HEAD -======= case 'counters': await this.pool.query( @@ -1385,7 +987,6 @@ class PostgreSQLDatabase { [parsedKey.guildId], ); return true; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) default: return false; diff --git a/src/utils/prefixParser.js b/src/utils/prefixParser.js index b43321125e..53a1e37d15 100644 --- a/src/utils/prefixParser.js +++ b/src/utils/prefixParser.js @@ -1,10 +1,6 @@ // prefixParser.js -<<<<<<< HEAD -import { resolveSubcommandAlias } from '../config/commandAliases.js'; -======= import { resolveSubcommandAlias } from '../config/commands/commandAliases.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) import { logger } from './logger.js'; export function parsePrefixCommand(content, prefix) { diff --git a/src/utils/safeMathParser.js b/src/utils/safeMathParser.js index 6cb7dd1b28..bb2f8d6c29 100644 --- a/src/utils/safeMathParser.js +++ b/src/utils/safeMathParser.js @@ -61,11 +61,7 @@ function tokenize(expression) { while (index < expression.length) { const character = expression[index]; -<<<<<<< HEAD - if (character === '' || character === '\t' || character === '\n') { -======= if (character === ' ' || character === '\t' || character === '\n' || character === '\r') { ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) index += 1; continue; } diff --git a/src/utils/serviceErrorBoundary.js b/src/utils/serviceErrorBoundary.js index cd725e7f58..75d845b105 100644 --- a/src/utils/serviceErrorBoundary.js +++ b/src/utils/serviceErrorBoundary.js @@ -1,10 +1,6 @@ // serviceErrorBoundary.js -<<<<<<< HEAD -import { createError, ErrorTypes, TitanBotError } from './errorHandler.js'; -======= import { createError, ErrorTypes, TitanBotError, categorizeError } from './errorHandler.js'; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) import { resolveErrorCode, getErrorMetadata } from './errorRegistry.js'; function normalizeBoundaryContext(context = {}) { @@ -15,42 +11,6 @@ function normalizeBoundaryContext(context = {}) { return context; } -<<<<<<< HEAD -function inferErrorType(error, fallbackType = ErrorTypes.UNKNOWN) { - const message = error?.message?.toLowerCase?.() || ''; - const code = error?.code; - - if (typeof code === 'string') { - if (code.includes('PERMISSION') || code.includes('FORBIDDEN')) { - return ErrorTypes.PERMISSION; - } - - if (code.includes('VALIDATION') || code.includes('INVALID')) { - return ErrorTypes.VALIDATION; - } - - if (code.includes('DB') || code.includes('SQL') || code.includes('POSTGRES')) { - return ErrorTypes.DATABASE; - } - } - - if (message.includes('permission') || message.includes('forbidden')) { - return ErrorTypes.PERMISSION; - } - - if (message.includes('database') || message.includes('sql') || message.includes('connection') || message.includes('timeout')) { - return ErrorTypes.DATABASE; - } - - if (message.includes('validation') || message.includes('invalid') || message.includes('required')) { - return ErrorTypes.VALIDATION; - } - - return fallbackType; -} - -======= ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) export function ensureTypedServiceError(error, options = {}) { if (error instanceof TitanBotError) { return error; @@ -58,12 +18,8 @@ export function ensureTypedServiceError(error, options = {}) { const context = normalizeBoundaryContext(options.context); const fallbackType = options.type || ErrorTypes.UNKNOWN; -<<<<<<< HEAD - const type = inferErrorType(error, fallbackType); -======= const categorized = categorizeError(error); const type = categorized === ErrorTypes.UNKNOWN ? fallbackType : categorized; ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) const service = options.service || 'unknown_service'; const operation = options.operation || 'unknown_operation'; const errorCode = resolveErrorCode({ @@ -132,8 +88,4 @@ export function wrapServiceClassMethods(ServiceClass, optionsFactory) { } return ServiceClass; -<<<<<<< HEAD -} -======= } ->>>>>>> 771ebe2 (Reorganize project structure, wire bot config, and fix dependency vulnerabilities) diff --git a/src/utils/ticketLogging.js b/src/utils/ticketLogging.js deleted file mode 100644 index db297ddaad..0000000000 --- a/src/utils/ticketLogging.js +++ /dev/null @@ -1,280 +0,0 @@ -// ticketLogging.js - -import { ChannelType } from 'discord.js'; -import { getGuildConfig } from '../services/guildConfig.js'; -import { logger } from './logger.js'; -import { - buildStandardLogEmbed, - formatRatingStars, - resolveUserAuthor, -} from './logEmbeds.js'; - -export async function logTicketEvent({ client, guildId, event }) { - try { - const guild = client.guilds.cache.get(guildId) || await client.guilds.fetch(guildId).catch(() => null); - if (!guild) { - logger.warn(`logTicketEvent invoked without valid guild: ${guildId}`); - return; - } - - const config = await getGuildConfig(client, guildId); - - const logChannelId = getLogChannelForEventType(config, event.type); - if (!logChannelId) { - return; - } - - const channel = guild.channels.cache.get(logChannelId) || await guild.channels.fetch(logChannelId).catch(() => null); - if (!channel) { - logger.warn(`Ticket log channel not found: ${logChannelId} for event type: ${event.type}`); - return; - } - - const permissions = channel.permissionsFor(guild.members.me); - if (!permissions.has(['SendMessages', 'EmbedLinks'])) { - logger.warn(`Missing permissions in ticket log channel: ${logChannelId}`); - return; - } - - const embed = await createTicketLogEmbed(guild, event); - - const messageOptions = { embeds: [embed] }; - - if (event.attachments && event.attachments.length > 0) { - messageOptions.files = event.attachments; - } - - await channel.send(messageOptions); - logger.info(`Ticket event logged: ${event.type} in guild ${guildId}`); - } catch (error) { - logger.error('Error logging ticket event:', error); - } -} - -export async function logTicketFeedback({ - client, - guildId, - ticketNumber, - ticketChannelId, - userId, - rating = null, - comment = null, -}) { - await logTicketEvent({ - client, - guildId, - event: { - type: 'feedback', - ticketId: ticketChannelId, - ticketNumber, - userId, - metadata: { - rating, - comment, - }, - }, - }); -} - -function getLogChannelForEventType(config, eventType) { - switch (eventType) { - case 'transcript': - return config.ticketTranscriptChannelId || null; - - case 'open': - case 'close': - case 'delete': - case 'claim': - case 'unclaim': - case 'priority': - case 'pin': - case 'unpin': - case 'feedback': - return config.ticketLogsChannelId || null; - - default: - return null; - } -} - -const TICKET_EVENT_STYLES = { - open: { color: 0x5865F2, title: 'Ticket Created' }, - close: { color: 0xED4245, title: 'Ticket Closed' }, - delete: { color: 0x8b0000, title: 'Ticket Deleted' }, - claim: { color: 0x5865F2, title: 'Ticket Claimed' }, - unclaim: { color: 0xFAA61A, title: 'Ticket Unclaimed' }, - priority: { color: 0x9b59b6, title: 'Priority Updated' }, - transcript: { color: 0x57F287, title: 'Transcript Generated' }, - feedback: { color: 0x57F287, title: 'Feedback Received' }, -}; - -async function createTicketLogEmbed(guild, event) { - const style = TICKET_EVENT_STYLES[event.type] || { color: 0x95a5a6, title: 'Ticket Event' }; - const ticketNumber = event.ticketNumber || event.ticketId; - const ticketRef = ticketNumber ? `#${ticketNumber}` : 'Unknown'; - const channelMention = event.ticketId ? `<#${event.ticketId}>` : null; - const executorMention = event.executorId ? `<@${event.executorId}>` : null; - const userMention = event.userId ? `<@${event.userId}>` : null; - - let inlineFields = []; - let fields = []; - let author = null; - let footer = { text: 'TitanBot Ticketing' }; - - switch (event.type) { - case 'open': - author = await resolveUserAuthor(guild.client, event.userId); - inlineFields = [ - { name: 'Ticket', value: ticketRef, inline: true }, - { name: 'Creator', value: userMention || 'Unknown', inline: true }, - ]; - if (channelMention) { - inlineFields.push({ name: 'Channel', value: channelMention, inline: true }); - } - if (event.reason) { - fields.push({ name: 'Reason', value: String(event.reason).slice(0, 1024), inline: false }); - } - break; - - case 'close': - author = await resolveUserAuthor(guild.client, event.executorId); - inlineFields = [ - { name: 'Ticket', value: ticketRef, inline: true }, - { name: 'Closed by', value: executorMention || 'Unknown', inline: true }, - ]; - if (channelMention) { - inlineFields.push({ name: 'Channel', value: channelMention, inline: true }); - } - if (event.reason) { - fields.push({ name: 'Reason', value: String(event.reason).slice(0, 1024), inline: false }); - } - break; - - case 'delete': - author = await resolveUserAuthor(guild.client, event.executorId); - inlineFields = [ - { name: 'Ticket', value: ticketRef, inline: true }, - { name: 'Deleted by', value: executorMention || 'Unknown', inline: true }, - ]; - break; - - case 'claim': - case 'unclaim': - author = await resolveUserAuthor(guild.client, event.executorId); - inlineFields = [ - { name: 'Ticket', value: ticketRef, inline: true }, - { - name: event.type === 'claim' ? 'Claimed by' : 'Unclaimed by', - value: executorMention || 'Unknown', - inline: true, - }, - ]; - break; - - case 'priority': { - const priorityEmojis = { none: '⚪', low: '🔵', medium: '🟢', high: '🟡', urgent: '🔴' }; - const priorityLabel = event.priority - ? `${priorityEmojis[event.priority] || '⚪'} ${event.priority.charAt(0).toUpperCase()}${event.priority.slice(1)}` - : 'Unknown'; - author = await resolveUserAuthor(guild.client, event.executorId); - inlineFields = [ - { name: 'Ticket', value: ticketRef, inline: true }, - { name: 'Priority', value: priorityLabel, inline: true }, - { name: 'Updated by', value: executorMention || 'Unknown', inline: true }, - ]; - break; - } - - case 'transcript': - inlineFields = [ - { name: 'Ticket', value: ticketRef, inline: true }, - { name: 'Creator', value: userMention || 'Unknown', inline: true }, - ]; - if (event.metadata?.messageCount) { - inlineFields.push({ name: 'Messages', value: String(event.metadata.messageCount), inline: true }); - } - if (event.metadata?.duration) { - fields.push({ name: 'Duration', value: String(event.metadata.duration), inline: false }); - } - if (event.metadata?.subject || event.reason) { - fields.push({ - name: 'Subject', - value: String(event.metadata?.subject || event.reason).slice(0, 1024), - inline: false, - }); - } - break; - - case 'feedback': { - const rating = event.metadata?.rating ?? event.rating; - const comment = event.metadata?.comment; - const ratingDisplay = formatRatingStars(rating) || 'No rating'; - - author = await resolveUserAuthor(guild.client, event.userId); - inlineFields = [ - { name: 'Ticket', value: ticketRef, inline: true }, - { name: 'Rating', value: ratingDisplay, inline: true }, - ]; - - if (comment) { - fields.push({ - name: 'Comment', - value: String(comment).slice(0, 1024), - inline: false, - }); - } - break; - } - - default: - inlineFields = [ - { name: 'Ticket', value: ticketRef, inline: true }, - ]; - if (event.reason) { - fields.push({ name: 'Details', value: String(event.reason).slice(0, 1024), inline: false }); - } - } - - const titlePrefix = event.type === 'feedback' ? '⭐ ' : ''; - return buildStandardLogEmbed({ - color: style.color, - title: `${titlePrefix}${style.title}`, - inlineFields, - fields, - author, - footer, - }); -} - -export async function getTicketLoggingConfig(client, guildId) { - const config = await getGuildConfig(client, guildId); - return { - enabled: !!(config.ticketLogsChannelId || config.ticketTranscriptChannelId), - lifecycleChannelId: config.ticketLogsChannelId || null, - transcriptChannelId: config.ticketTranscriptChannelId || null, - }; -} - -export function validateLogChannel(channel, botMember) { - if (!channel || channel.type !== ChannelType.GuildText) { - return { - valid: false, - error: 'Channel must be a text channel.', - }; - } - - const permissions = channel.permissionsFor(botMember); - const requiredPermissions = ['SendMessages', 'EmbedLinks']; - - const missing = requiredPermissions.filter((perm) => !permissions.has(perm)); - - if (missing.length > 0) { - return { - valid: false, - error: `Missing permissions: ${missing.join(', ')}`, - }; - } - - return { valid: true }; -} - diff --git a/src/utils/ticketPermissions.js b/src/utils/ticketPermissions.js deleted file mode 100644 index a056f4a1f8..0000000000 --- a/src/utils/ticketPermissions.js +++ /dev/null @@ -1,32 +0,0 @@ -// ticketPermissions.js - -import { PermissionFlagsBits } from 'discord.js'; -import { getGuildConfig } from '../services/guildConfig.js'; -import { getTicketData } from './database.js'; - -export async function getTicketPermissionContext({ client, interaction }) { - const guildId = interaction.guildId; - const channelId = interaction.channelId; - - const [config, ticketData] = await Promise.all([ - getGuildConfig(client, guildId), - getTicketData(guildId, channelId) - ]); - - const hasManageChannels = interaction.member.permissions.has(PermissionFlagsBits.ManageChannels); - const staffRoleId = config.ticketStaffRoleId || null; - const hasTicketStaffRole = Boolean(staffRoleId && interaction.member.roles?.cache?.has(staffRoleId)); - const isTicketCreator = Boolean( - ticketData?.userId && String(ticketData.userId) === String(interaction.user.id), - ); - - return { - config, - ticketData, - hasManageChannels, - hasTicketStaffRole, - isTicketCreator, - canManageTicket: hasManageChannels || hasTicketStaffRole, - canCloseTicket: hasManageChannels || hasTicketStaffRole || isTicketCreator, - }; -} \ No newline at end of file diff --git a/tests/commandAccessService.test.js b/tests/commandAccessService.test.js deleted file mode 100644 index 65508ded45..0000000000 --- a/tests/commandAccessService.test.js +++ /dev/null @@ -1,81 +0,0 @@ -import test from 'node:test'; -import assert from 'node:assert/strict'; - -import { - isCommandEnabledInConfig, - getCommandAccessSnapshot, - buildCommandRegistry, - resolveCategoryChoice, -} from '../src/services/commandAccessService.js'; - -const mockClient = { - commands: new Map([ - ['balance', { data: { name: 'balance', description: 'Check balance' }, category: 'Economy' }], - ['daily', { data: { name: 'daily', description: 'Daily reward' }, category: 'Economy' }], - ['ping', { data: { name: 'ping', description: 'Ping' }, category: 'Core' }], - ['commands', { data: { name: 'commands', description: 'Manage commands' }, category: 'Core' }], - ['configwizard', { data: { name: 'configwizard', description: 'Config wizard' }, category: 'Core' }], - ]), -}; - -test('isCommandEnabledInConfig respects category and command disables', () => { - const config = { - disabledCategories: { economy: true }, - disabledCommands: {}, - }; - - assert.equal(isCommandEnabledInConfig(config, 'balance', 'Economy'), false); - assert.equal(isCommandEnabledInConfig(config, 'ping', 'Core'), true); -}); - -test('isCommandEnabledInConfig keeps protected commands available', () => { - const config = { - disabledCategories: { core: true }, - disabledCommands: { commands: true, configwizard: true }, - }; - - assert.equal(isCommandEnabledInConfig(config, 'commands', 'Core'), true); - assert.equal(isCommandEnabledInConfig(config, 'configwizard', 'Core'), true); - assert.equal(isCommandEnabledInConfig(config, 'ping', 'Core'), false); -}); - -test('isCommandEnabledInConfig supports legacy array disabledCommands', () => { - const config = { - disabledCategories: {}, - disabledCommands: ['urban', 'flip'], - }; - - assert.equal(isCommandEnabledInConfig(config, 'urban', 'Search'), false); - assert.equal(isCommandEnabledInConfig(config, 'flip', 'Fun'), false); -}); - -test('getCommandAccessSnapshot summarizes category state', () => { - const config = { - disabledCategories: { economy: true }, - disabledCommands: { ping: true }, - }; - - const snapshot = getCommandAccessSnapshot(mockClient, config); - const economy = snapshot.categories.find((category) => category.key === 'economy'); - const core = snapshot.categories.find((category) => category.key === 'core'); - - assert.equal(economy.categoryDisabled, true); - assert.equal(core.disabledCount, 1); - assert.equal(snapshot.enabledTotal, 2); -}); - -test('buildCommandRegistry groups commands by category folder', () => { - const registry = buildCommandRegistry(mockClient); - assert.equal(registry.get('economy')?.commands.length, 2); - assert.equal(registry.get('core')?.commands.length, 3); -}); - -test('isCommandEnabledInConfig supports subcommand-level disables', () => { - const config = { - disabledCategories: {}, - disabledCommands: { 'birthday list': true }, - }; - - assert.equal(isCommandEnabledInConfig(config, 'birthday list', 'Birthday'), false); - assert.equal(isCommandEnabledInConfig(config, 'birthday info', 'Birthday'), true); -}); diff --git a/tests/embedUtils.test.js b/tests/embedUtils.test.js deleted file mode 100644 index 1fff605bab..0000000000 --- a/tests/embedUtils.test.js +++ /dev/null @@ -1,73 +0,0 @@ -import test from 'node:test'; -import assert from 'node:assert/strict'; -import { createEmbed } from '../src/utils/embeds.js'; - -test('createEmbed should ignore unimportant footer text and should not add a timestamp by default', () => { - const embed = createEmbed({ - title: 'Test', - description: 'Hello world', - footer: 'Footer text should move into description' - }); - - const data = embed.toJSON(); - assert.equal(data.footer, undefined); - assert.equal(data.timestamp, undefined); - assert.equal(data.description, 'Hello world'); -}); - -test('createEmbed should set important footer text on the embed footer', () => { - const embed = createEmbed({ - title: 'Test', - description: 'Hello world', - footer: 'Dashboard closes after 10 minutes of inactivity' - }); - - const data = embed.toJSON(); - assert.equal(data.footer?.text, 'Dashboard closes after 10 minutes of inactivity'); - assert.equal(data.timestamp, undefined); - assert.equal(data.description, 'Hello world'); -}); - -test('setFooter should place important footer text on the embed footer', () => { - const embed = createEmbed({ - title: 'Footer Test', - description: 'Base description.' - }); - - embed.setFooter({ text: 'Dashboard closes after 10 minutes of inactivity' }); - const data = embed.toJSON(); - - assert.equal(data.footer?.text, 'Dashboard closes after 10 minutes of inactivity'); - assert.equal(data.description, 'Base description.'); -}); - -test('setFooter should keep footer text at the bottom when fields are added after footer', () => { - const embed = createEmbed({ - title: 'Dashboard', - description: 'Manage settings for **Test Server**.', - }); - - embed.setFooter({ text: 'Dashboard closes after 10 minutes of inactivity' }); - embed.addFields( - { name: 'Status', value: '`Enabled`', inline: true }, - { name: 'Channel', value: '`Not set`', inline: true }, - ); - - const data = embed.toJSON(); - assert.equal(data.footer?.text, 'Dashboard closes after 10 minutes of inactivity'); - assert.equal(data.description, 'Manage settings for **Test Server**.'); - assert.equal(data.fields?.length, 2); -}); - -test('setFooter should ignore unimportant footer text', () => { - const embed = createEmbed({ - title: 'Footer Test', - description: 'Base description.' - }); - - embed.setFooter({ text: 'Requested by mrpinkify' }); - const data = embed.toJSON(); - - assert.equal(data.footer, undefined); - assert.equal(data.description, 'Base description.'); -}); diff --git a/tests/failure-paths/abuseProtection.test.js b/tests/failure-paths/abuseProtection.test.js deleted file mode 100644 index e09419a23a..0000000000 --- a/tests/failure-paths/abuseProtection.test.js +++ /dev/null @@ -1,100 +0,0 @@ -import test from 'node:test'; -import assert from 'node:assert/strict'; - -import { - enforceAbuseProtection, - formatCooldownDuration, - isRiskyCommand, - resetAbuseProtectionState -} from '../../src/utils/abuseProtection.js'; -import { logger } from '../../src/utils/logger.js'; - -function createInteraction(overrides = {}) { - return { - guildId: 'guild-1', - user: { id: 'user-1' }, - ...overrides - }; -} - -test('detects risky commands by category and explicit name', () => { - assert.equal(isRiskyCommand({ category: 'moderation' }, 'ban'), true); - assert.equal(isRiskyCommand({ category: 'fun' }, 'wipedata'), true); - assert.equal(isRiskyCommand({ category: 'fun' }, 'ping'), false); -}); - -test('blocks risky command after cooldown limit is exceeded', async () => { - resetAbuseProtectionState(); - const interaction = createInteraction(); - const riskyCommand = { category: 'moderation' }; - - const first = await enforceAbuseProtection(interaction, riskyCommand, 'ban'); - const second = await enforceAbuseProtection(interaction, riskyCommand, 'ban'); - const third = await enforceAbuseProtection(interaction, riskyCommand, 'ban'); - - assert.equal(first.allowed, true); - assert.equal(second.allowed, true); - assert.equal(third.allowed, false); - assert.ok(third.remainingMs > 0); -}); - -test('supports command-level abuse policy overrides', async () => { - resetAbuseProtectionState(); - const interaction = createInteraction(); - const command = { - category: 'fun', - abuseProtection: { - enabled: true, - maxAttempts: 1, - windowMs: 5_000 - } - }; - - const first = await enforceAbuseProtection(interaction, command, 'custom-risky'); - const second = await enforceAbuseProtection(interaction, command, 'custom-risky'); - - assert.equal(first.allowed, true); - assert.equal(second.allowed, false); - assert.equal(second.policy.maxAttempts, 1); - assert.equal(second.policy.windowMs, 5_000); -}); - -test('logs anomaly warning after repeated blocked attempts', async () => { - resetAbuseProtectionState(); - const interaction = createInteraction({ guildId: 'guild-2', user: { id: 'user-2' } }); - const command = { - category: 'moderation', - abuseProtection: { - maxAttempts: 1, - windowMs: 60_000, - anomaly: { - threshold: 3, - windowMs: 60_000 - } - } - }; - - const warnLogs = []; - const originalWarn = logger.warn; - logger.warn = (message, meta) => { - warnLogs.push({ message, meta }); - }; - - try { - await enforceAbuseProtection(interaction, command, 'ban'); - await enforceAbuseProtection(interaction, command, 'ban'); - await enforceAbuseProtection(interaction, command, 'ban'); - await enforceAbuseProtection(interaction, command, 'ban'); - - const anomalyLog = warnLogs.find((entry) => entry.meta?.event === 'interaction.command.abuse_anomaly'); - assert.ok(anomalyLog, 'expected anomaly log event after repeated blocked attempts'); - assert.equal(anomalyLog.meta.command, 'ban'); - } finally { - logger.warn = originalWarn; - } -}); - -test('formats cooldown duration for user-facing messaging', () => { - assert.equal(formatCooldownDuration(500), '1s'); - assert.equal(formatCooldownDuration(61_000), '1m 1s'); -}); \ No newline at end of file diff --git a/tests/failure-paths/database.failure.test.js b/tests/failure-paths/database.failure.test.js deleted file mode 100644 index 84e1ff67d3..0000000000 --- a/tests/failure-paths/database.failure.test.js +++ /dev/null @@ -1,45 +0,0 @@ -import test from 'node:test'; -import assert from 'node:assert/strict'; - -import { db, initializeDatabase } from '../../src/utils/database.js'; -import { pgDb } from '../../src/utils/postgresDatabase.js'; - -function resetDbSingleton() { - db.initialized = false; - db.db = null; - db.useFallback = false; - db.connectionType = 'none'; - db.degradedModeWarningShown = false; - db.degradedReason = null; -} - -test('DB down path falls back to memory with degraded status', async () => { - const originalConnect = pgDb.connect; - const originalGetLastFailure = pgDb.getLastFailure; - - try { - resetDbSingleton(); - - pgDb.connect = async () => false; - pgDb.getLastFailure = () => ({ - reason: 'POSTGRES_CONNECTION_FAILED', - message: 'connect ECONNREFUSED' - }); - - await initializeDatabase(); - - const status = db.getStatus(); - assert.equal(status.initialized, true); - assert.equal(status.isDegraded, true); - assert.equal(status.connectionType, 'memory'); - assert.equal(status.degradedReason, 'POSTGRES_UNAVAILABLE'); - - await db.set('health:key', { ok: true }); - const value = await db.get('health:key'); - assert.deepEqual(value, { ok: true }); - } finally { - pgDb.connect = originalConnect; - pgDb.getLastFailure = originalGetLastFailure; - resetDbSingleton(); - } -}); \ No newline at end of file diff --git a/tests/failure-paths/errorHandler.failure.test.js b/tests/failure-paths/errorHandler.failure.test.js deleted file mode 100644 index 080dc50232..0000000000 --- a/tests/failure-paths/errorHandler.failure.test.js +++ /dev/null @@ -1,169 +0,0 @@ -import test from 'node:test'; -import assert from 'node:assert/strict'; -import { MessageFlags } from 'discord.js'; - -import { - handleInteractionError, - replyUserError, - ErrorTypes, - createError -} from '../../src/utils/errorHandler.js'; -import { buildUserErrorEmbed } from '../../src/utils/embeds.js'; -import { logger } from '../../src/utils/logger.js'; - -function createLoggerCapture() { - const entries = []; - const originals = { - warn: logger.warn, - error: logger.error, - debug: logger.debug - }; - - logger.warn = (message, meta) => entries.push({ level: 'warn', message, meta }); - logger.error = (message, meta) => entries.push({ level: 'error', message, meta }); - logger.debug = (message, meta) => entries.push({ level: 'debug', message, meta }); - - return { - entries, - restore() { - logger.warn = originals.warn; - logger.error = originals.error; - logger.debug = originals.debug; - } - }; -} - -function createInteraction(overrides = {}) { - return { - id: 'interaction-1', - createdTimestamp: Date.now(), - deferred: false, - replied: false, - guildId: 'guild-1', - channelId: 'channel-1', - commandName: 'ping', - type: 2, - customId: null, - user: { id: 'user-1' }, - async reply() {}, - async editReply() {}, - ...overrides - }; -} - -test('expired interaction path logs INTERACTION_EXPIRED and skips reply', async () => { - const capture = createLoggerCapture(); - let replyCalled = false; - - try { - const interaction = createInteraction({ - createdTimestamp: Date.now() - (15 * 60 * 1000), - reply: async () => { - replyCalled = true; - } - }); - - const error = new Error('database timeout during operation'); - await handleInteractionError(interaction, error, {}); - - assert.equal(replyCalled, false, 'expired interactions should not attempt reply'); - - const expiredLog = capture.entries.find( - (entry) => entry.level === 'warn' && entry.meta?.event === 'interaction.error.expired' - ); - - assert.ok(expiredLog, 'should log interaction expiry event'); - assert.equal(expiredLog.meta?.errorCode, 'INTERACTION_EXPIRED'); - } finally { - capture.restore(); - } -}); - -test('Discord API response failure path logs response_unavailable when API says expired', async () => { - const capture = createLoggerCapture(); - - try { - const interaction = createInteraction({ - reply: async () => { - const err = new Error('Unknown interaction'); - err.code = 10062; - throw err; - } - }); - - const apiError = new Error('Discord API request failed'); - apiError.code = 10062; - - await handleInteractionError(interaction, apiError, {}); - - const unavailableLog = capture.entries.find( - (entry) => entry.level === 'warn' && entry.meta?.event === 'interaction.error.response_unavailable' - ); - - assert.ok(unavailableLog, 'should log unavailable response event'); - assert.equal(unavailableLog.meta?.errorCode, '10062'); - } finally { - capture.restore(); - } -}); - -test('buildUserErrorEmbed uses plain titles and typed colors', () => { - const validationEmbed = buildUserErrorEmbed('validation', 'Check your input.'); - assert.equal(validationEmbed.data.title, 'Invalid Input'); - assert.equal(validationEmbed.data.description, 'Check your input.'); - - const rateLimitEmbed = buildUserErrorEmbed('rate_limit', 'Wait a moment.'); - assert.equal(rateLimitEmbed.data.title, 'Too Fast'); -}); - -test('handleInteractionError does not add tip fields', async () => { - let replyPayload = null; - const interaction = createInteraction({ - reply: async (payload) => { - replyPayload = payload; - } - }); - - await handleInteractionError(interaction, createError('rate limited', ErrorTypes.RATE_LIMIT), {}); - - const embed = replyPayload?.embeds?.[0]; - assert.ok(embed); - assert.equal(embed.data.fields?.length ?? 0, 0, 'error embeds should not include tip fields'); - assert.equal(embed.data.title, 'Too Fast'); -}); - -test('replyUserError sends ephemeral replies for fresh interactions', async () => { - let replyPayload = null; - const interaction = createInteraction({ - reply: async (payload) => { - replyPayload = payload; - } - }); - - await replyUserError(interaction, { - type: ErrorTypes.PERMISSION, - message: 'You need Manage Server.' - }); - - assert.equal(replyPayload?.flags, MessageFlags.Ephemeral); - assert.equal(replyPayload?.embeds?.[0]?.data?.title, 'Permission Denied'); - assert.equal(replyPayload?.embeds?.[0]?.data?.description, 'You need Manage Server.'); -}); - -test('replyUserError uses editReply when interaction is deferred', async () => { - let editPayload = null; - const interaction = createInteraction({ - deferred: true, - editReply: async (payload) => { - editPayload = payload; - } - }); - - await replyUserError(interaction, { - type: ErrorTypes.VALIDATION, - message: 'Missing required option.' - }); - - assert.ok(editPayload); - assert.equal(editPayload.embeds?.[0]?.data?.title, 'Invalid Input'); -}); \ No newline at end of file diff --git a/tests/failure-paths/safeMathParser.test.js b/tests/failure-paths/safeMathParser.test.js deleted file mode 100644 index 7298dc09a6..0000000000 --- a/tests/failure-paths/safeMathParser.test.js +++ /dev/null @@ -1,33 +0,0 @@ -import test from 'node:test'; -import assert from 'node:assert/strict'; - -import { evaluateMathExpression } from '../../src/utils/safeMathParser.js'; - -test('safe parser evaluates arithmetic precedence correctly', () => { - const result = evaluateMathExpression('2 + 2 * 3'); - assert.equal(result, 8); -}); - -test('safe parser evaluates trig with degree conversion', () => { - const result = evaluateMathExpression('sin(45 deg)'); - assert.ok(Math.abs(result - 0.7071067811865476) < 1e-10); -}); - -test('safe parser supports constants and exponent operator', () => { - const result = evaluateMathExpression('pi ^ 2'); - assert.ok(Math.abs(result - (Math.PI ** 2)) < 1e-10); -}); - -test('safe parser rejects code-like tokens', () => { - assert.throws( - () => evaluateMathExpression('process.exit()'), - /Unsupported token|Unsupported character/ - ); -}); - -test('safe parser rejects malformed expressions', () => { - assert.throws( - () => evaluateMathExpression('2 + (3 * 4'), - /Mismatched parentheses/ - ); -}); \ No newline at end of file diff --git a/tests/failure-paths/zodValidationCoverage.test.js b/tests/failure-paths/zodValidationCoverage.test.js deleted file mode 100644 index ce2d9ab211..0000000000 --- a/tests/failure-paths/zodValidationCoverage.test.js +++ /dev/null @@ -1,72 +0,0 @@ -import test from 'node:test'; -import assert from 'node:assert/strict'; - -import { validateChatInputPayloadOrThrow } from '../../src/utils/commandInputValidation.js'; -import { validateGuildConfigOrThrow } from '../../src/utils/schemas.js'; - -test('zod command input validation accepts valid command payload', () => { - const interaction = { - commandName: 'ping', - options: { - data: [ - { name: 'target', type: 3, value: 'user123' }, - { name: 'count', type: 4, value: 3 } - ] - } - }; - - const result = validateChatInputPayloadOrThrow(interaction); - assert.equal(result.commandName, 'ping'); - assert.equal(result.options.length, 2); -}); - -test('zod command input validation rejects invalid option payload shape', () => { - const interaction = { - commandName: 'ping', - options: { - data: [ - { name: 'bad', type: 3, value: { nested: 'not-allowed' } } - ] - } - }; - - assert.throws( - () => validateChatInputPayloadOrThrow(interaction), - /Invalid command input payload/ - ); -}); - -test('zod guild config validation accepts valid config write payload', () => { - const config = { - prefix: '!', - logging: { - enabled: true, - enabledEvents: { - 'message.delete': true - } - }, - logIgnore: { - users: ['123'], - channels: ['456'] - } - }; - - const validated = validateGuildConfigOrThrow(config); - assert.equal(validated.prefix, '!'); - assert.equal(validated.logging.enabled, true); -}); - -test('zod guild config validation rejects invalid config write payload', () => { - const invalidConfig = { - prefix: '!', - logging: { - enabled: 'yes', - enabledEvents: {} - } - }; - - assert.throws( - () => validateGuildConfigOrThrow(invalidConfig), - /Invalid guild configuration payload/ - ); -}); \ No newline at end of file diff --git a/tests/panelStatus.reaction.test.js b/tests/panelStatus.reaction.test.js deleted file mode 100644 index c56d2d522a..0000000000 --- a/tests/panelStatus.reaction.test.js +++ /dev/null @@ -1,55 +0,0 @@ -import test from 'node:test'; -import assert from 'node:assert/strict'; - -import { - messageHasSelectMenuCustomId, - formatPanelStatusField, - getReactionRolePanelStatus, -} from '../src/utils/panelStatus.js'; - -test('messageHasSelectMenuCustomId finds select menu custom_id', () => { - const message = { - components: [ - { - type: 1, - components: [{ type: 3, custom_id: 'reaction_roles', options: [] }], - }, - ], - }; - - assert.equal(messageHasSelectMenuCustomId(message, 'reaction_roles'), true); - assert.equal(messageHasSelectMenuCustomId(message, 'other'), false); -}); - -test('formatPanelStatusField describes deleted panels with repost hint', () => { - const value = formatPanelStatusField({ exists: false, reason: 'panel_deleted' }); - assert.match(value, /Repost Panel/); -}); - -test('getReactionRolePanelStatus scans for reaction_roles select menu', async () => { - const panelMessage = { - id: 'msg-rr', - author: { id: 'bot' }, - components: [{ type: 1, components: [{ type: 3, custom_id: 'reaction_roles' }] }], - }; - - const client = { user: { id: 'bot' } }; - const guild = { - id: 'g1', - channels: { - fetch: async () => ({ - messages: { - fetch: async () => new Map([['msg-rr', panelMessage]]), - }, - }), - }, - }; - - const status = await getReactionRolePanelStatus(client, guild, { - channelId: 'ch1', - messageId: 'stale', - }); - - assert.equal(status.exists, true); - assert.equal(status.recoveredId, 'msg-rr'); -}); diff --git a/tests/panelStatus.test.js b/tests/panelStatus.test.js deleted file mode 100644 index 486bb9820c..0000000000 --- a/tests/panelStatus.test.js +++ /dev/null @@ -1,144 +0,0 @@ -import test from 'node:test'; -import assert from 'node:assert/strict'; - -import { - messageHasButtonCustomId, - getBotPanelStatus, - getTicketPanelStatus, -} from '../src/utils/panelStatus.js'; - -test('messageHasButtonCustomId finds nested button custom_id', () => { - const message = { - components: [ - { - type: 1, - components: [{ type: 2, custom_id: 'create_ticket', style: 1, label: 'Create' }], - }, - ], - }; - - assert.equal(messageHasButtonCustomId(message, 'create_ticket'), true); - assert.equal(messageHasButtonCustomId(message, 'other'), false); -}); - -test('messageHasButtonCustomId returns false for empty input', () => { - assert.equal(messageHasButtonCustomId(null, 'create_ticket'), false); - assert.equal(messageHasButtonCustomId({ components: [] }, ''), false); -}); - -test('getBotPanelStatus returns no_channel when channelId missing', async () => { - const client = { user: { id: 'bot' } }; - const guild = { id: 'g1' }; - - const status = await getBotPanelStatus(client, guild, { buttonCustomId: 'create_ticket' }); - assert.deepEqual(status, { exists: false, reason: 'no_channel' }); -}); - -test('getBotPanelStatus returns channel_missing when fetch fails', async () => { - const client = { user: { id: 'bot' } }; - const guild = { - id: 'g1', - channels: { - fetch: async () => null, - }, - }; - - const status = await getBotPanelStatus(client, guild, { - channelId: 'missing', - buttonCustomId: 'create_ticket', - }); - assert.equal(status.exists, false); - assert.equal(status.reason, 'channel_missing'); -}); - -test('getBotPanelStatus returns active when stored message has button', async () => { - const panelMessage = { - id: 'msg1', - url: 'https://discord.com/channels/1/2/3', - components: [{ type: 1, components: [{ type: 2, custom_id: 'create_ticket' }] }], - }; - - const client = { user: { id: 'bot' } }; - const guild = { - id: 'g1', - channels: { - fetch: async () => ({ - messages: { - fetch: async (idOrOpts) => { - if (idOrOpts === 'msg1') return panelMessage; - return null; - }, - }, - }), - }, - }; - - const status = await getBotPanelStatus(client, guild, { - channelId: 'ch1', - messageId: 'msg1', - buttonCustomId: 'create_ticket', - }); - - assert.equal(status.exists, true); - assert.equal(status.message.id, 'msg1'); -}); - -test('getBotPanelStatus scans channel when messageId missing or stale', async () => { - const recovered = { - id: 'recovered', - author: { id: 'bot' }, - components: [{ type: 1, components: [{ type: 2, custom_id: 'create_ticket' }] }], - }; - - const client = { user: { id: 'bot' } }; - const guild = { - id: 'g1', - channels: { - fetch: async () => ({ - messages: { - fetch: async (idOrOpts) => { - if (typeof idOrOpts === 'string') return null; - return new Map([['recovered', recovered]]); - }, - }, - }), - }, - }; - - const status = await getBotPanelStatus(client, guild, { - channelId: 'ch1', - messageId: 'stale', - buttonCustomId: 'create_ticket', - }); - - assert.equal(status.exists, true); - assert.equal(status.recoveredId, 'recovered'); -}); - -test('getTicketPanelStatus delegates to ticket config fields', async () => { - let captured = null; - const client = { - user: { id: 'bot' }, - }; - const guild = { - id: 'g1', - channels: { - fetch: async (channelId) => { - captured = channelId; - return { - messages: { - fetch: async () => new Map(), - }, - }; - }, - }, - }; - - const status = await getTicketPanelStatus(client, guild, { - ticketPanelChannelId: 'panel-ch', - ticketPanelMessageId: null, - }); - - assert.equal(captured, 'panel-ch'); - assert.equal(status.reason, 'panel_deleted'); -}); diff --git a/tests/prefixParser.test.js b/tests/prefixParser.test.js deleted file mode 100644 index f54939788f..0000000000 --- a/tests/prefixParser.test.js +++ /dev/null @@ -1,103 +0,0 @@ -import test from 'node:test'; -import assert from 'node:assert/strict'; - -import { parsePrefixCommand, mapArgumentsToOptions } from '../src/utils/prefixParser.js'; -import { resolveCommandAlias, resolveSubcommandAlias } from '../src/config/commandAliases.js'; -import { getPrefixRestriction } from '../src/config/prefixRestrictions.js'; -import balanceCommand from '../src/commands/Economy/balance.js'; -import countCommand from '../src/commands/Fun/count.js'; -import helpCommand from '../src/commands/Core/help.js'; -import ticketCommand from '../src/commands/Ticket/ticket.js'; -import birthdayCommand from '../src/commands/Birthday/birthday.js'; - -const balanceData = balanceCommand.default?.data || balanceCommand.data || balanceCommand; -const countData = countCommand.default?.data || countCommand.data || countCommand; -const helpData = helpCommand.default?.data || helpCommand.data || helpCommand; -const ticketData = ticketCommand.default?.data || ticketCommand.data || ticketCommand; -const birthdayData = birthdayCommand.default?.data || birthdayCommand.data || birthdayCommand; - -test('parsePrefixCommand should detect the balance command with default prefix', () => { - const parsed = parsePrefixCommand('!balance', '!'); - assert.deepEqual(parsed, { commandName: 'balance', args: [] }); -}); - -test('parsePrefixCommand should split space-separated arguments', () => { - const parsed = parsePrefixCommand('!birthday set 3 15', '!'); - assert.deepEqual(parsed, { - commandName: 'birthday', - args: ['set', '3', '15'], - }); -}); - -test('parsePrefixCommand should parse quoted arguments', () => { - const parsed = parsePrefixCommand('!warn @user "being rude in chat"', '!'); - assert.deepEqual(parsed, { - commandName: 'warn', - args: ['@user', 'being rude in chat'], - }); -}); - -test('resolveCommandAlias should not shadow level or stats commands', () => { - assert.equal(resolveCommandAlias('level'), 'level'); - assert.equal(resolveCommandAlias('stats'), 'stats'); - assert.equal(resolveCommandAlias('verify'), 'verify'); - assert.equal(resolveCommandAlias('lvl'), 'rank'); - assert.equal(resolveCommandAlias('ss'), 'serverstats'); - assert.equal(resolveCommandAlias('vadmin'), 'verification'); -}); - -test('resolveSubcommandAlias should keep count disable intact', () => { - assert.equal(resolveSubcommandAlias('disable'), 'disable'); -}); - -test('mapArgumentsToOptions should correctly map a single user argument', () => { - const args = ['<@1234567890>']; - const options = mapArgumentsToOptions(args, balanceData); - - assert.equal(options.getUser('user') instanceof Object, false); - assert.equal(options._positional[0], '<@1234567890>'); - assert.equal(typeof options.getString('user'), 'string'); - assert.equal(options.getString('user'), '<@1234567890>'); -}); - -test('mapArgumentsToOptions should validate optional user option as valid', () => { - const args = []; - const options = mapArgumentsToOptions(args, balanceData); - const validation = options.validateRequired(); - assert.equal(validation.valid, true); - assert.equal(validation.missing.length, 0); -}); - -test('mapArgumentsToOptions should reject unknown subcommands', () => { - const options = mapArgumentsToOptions(['notreal'], countData); - const validation = options.validateRequired(); - assert.equal(validation.valid, false); - assert.equal(validation.missing[0].name, 'subcommand'); -}); - -test('mapArgumentsToOptions should resolve subcommand aliases', () => { - const options = mapArgumentsToOptions(['s', '3', '15'], birthdayData); - assert.equal(options.getSubcommand(), 'set'); - assert.equal(options.getInteger('month'), 3); - assert.equal(options.getInteger('day'), 15); -}); - -test('getPrefixRestriction should block slash-only commands', () => { - const restriction = getPrefixRestriction(helpCommand, [], resolveSubcommandAlias); - assert.equal(restriction.blocked, true); -}); - -test('getPrefixRestriction should block ticket command entirely via prefix', () => { - const restriction = getPrefixRestriction(ticketCommand, ['setup'], resolveSubcommandAlias); - assert.equal(restriction.blocked, true); -}); - -test('getPrefixRestriction should allow simple subcommands on mixed commands', () => { - const restriction = getPrefixRestriction(countCommand, ['status'], resolveSubcommandAlias); - assert.equal(restriction.blocked, false); -}); - -test('getPrefixRestriction should allow count disable via prefix', () => { - const restriction = getPrefixRestriction(countCommand, ['disable'], resolveSubcommandAlias); - assert.equal(restriction.blocked, false); -}); diff --git a/tests/prefixPermissions.test.js b/tests/prefixPermissions.test.js deleted file mode 100644 index 3ac76111bc..0000000000 --- a/tests/prefixPermissions.test.js +++ /dev/null @@ -1,196 +0,0 @@ -import test from 'node:test'; -import assert from 'node:assert/strict'; -import { PermissionFlagsBits } from 'discord.js'; - -import banCommand from '../src/commands/Moderation/ban.js'; -import pingCommand from '../src/commands/Core/ping.js'; -import { - getCommandDefaultPermissions, - memberMeetsCommandPermissions, - memberHasConfiguredModeratorRole, -} from '../src/utils/permissionGuard.js'; -import { executePrefixCommand } from '../src/utils/messageAdapter.js'; - -const banData = banCommand.default?.data || banCommand.data || banCommand; -const pingData = pingCommand.default?.data || pingCommand.data || pingCommand; - -function createMockMessage({ hasPermission = true, content = '!ban @user reason', modRoleId = null } = {}) { - const sentMessages = []; - - const channel = { - send: async (payload) => { - sentMessages.push(payload); - return { - id: `msg-${sentMessages.length}`, - edit: async (editPayload) => { - sentMessages[sentMessages.length - 1] = editPayload; - return sentMessages[sentMessages.length - 1]; - }, - deletable: true, - delete: async () => {}, - }; - }, - }; - - const required = getCommandDefaultPermissions(banData); - const rolesCache = new Map(); - if (modRoleId) { - rolesCache.set(modRoleId, { id: modRoleId }); - } - - const message = { - id: 'trigger-msg', - content, - author: { id: 'user-1', tag: 'User#0001', bot: false, toString: () => '<@user-1>' }, - member: { - id: 'user-1', - permissions: { - has: (perm) => { - if (!hasPermission) return false; - if (required == null) return true; - return (BigInt(perm) & required) === required || perm === required; - }, - }, - roles: { - cache: { - has: (roleId) => rolesCache.has(roleId), - }, - }, - }, - guild: { - id: 'guild-1', - ownerId: 'owner-1', - members: { cache: new Map(), fetch: async () => null }, - }, - channel, - client: { config: { bot: { prefix: '!' } } }, - createdTimestamp: Date.now(), - createdAt: new Date(), - deletable: false, - }; - - return { message, sentMessages }; -} - -function getEmbedDescription(payload) { - const embed = payload.embeds?.[0]; - return embed?.data?.description ?? embed?.description ?? ''; -} - -test('getCommandDefaultPermissions reads BanMembers from ban command', () => { - const permissions = getCommandDefaultPermissions(banData); - assert.equal(permissions, PermissionFlagsBits.BanMembers); -}); - -test('getCommandDefaultPermissions returns null when command has no default permissions', () => { - const permissions = getCommandDefaultPermissions(pingData); - assert.equal(permissions, null); -}); - -test('memberMeetsCommandPermissions allows guild owner without explicit flags', () => { - const member = { - id: 'owner-1', - guild: { ownerId: 'owner-1' }, - permissions: { has: () => false }, - }; - - assert.equal( - memberMeetsCommandPermissions(member, PermissionFlagsBits.BanMembers), - true, - ); -}); - -test('executePrefixCommand blocks prefix ban when member lacks BanMembers', async () => { - const { message, sentMessages } = createMockMessage({ hasPermission: false }); - let executeCalled = false; - - const command = { - data: banData, - execute: async () => { - executeCalled = true; - }, - category: 'moderation', - }; - - await executePrefixCommand(command, message, ['@user', 'reason'], message.client, '!'); - - assert.equal(executeCalled, false); - assert.equal(sentMessages.length, 1); - assert.match(getEmbedDescription(sentMessages[0]), /do not have permission/i); -}); - -test('executePrefixCommand allows prefix ban when member has BanMembers', async () => { - const { message, sentMessages } = createMockMessage({ hasPermission: true }); - let executeCalled = false; - - const command = { - data: banData, - execute: async () => { - executeCalled = true; - }, - category: 'moderation', - }; - - await executePrefixCommand(command, message, ['@user', 'reason'], message.client, '!'); - - assert.equal(executeCalled, true); - assert.equal(sentMessages.length, 0); -}); - -test('executePrefixCommand allows prefix ban when member has configured modRole without BanMembers', async () => { - const modRoleId = 'mod-role-1'; - const { message, sentMessages } = createMockMessage({ - hasPermission: false, - modRoleId, - }); - let executeCalled = false; - - const command = { - data: banData, - category: 'moderation', - execute: async () => { - executeCalled = true; - }, - }; - - await executePrefixCommand(command, message, ['@user', 'reason'], message.client, '!', { - modRole: modRoleId, - }); - - assert.equal(executeCalled, true); - assert.equal(sentMessages.length, 0); -}); - -test('memberHasConfiguredModeratorRole detects modRole membership', () => { - const member = { - roles: { - cache: { - has: (id) => id === 'mod-role-1', - }, - }, - }; - - assert.equal(memberHasConfiguredModeratorRole(member, { modRole: 'mod-role-1' }), true); - assert.equal(memberHasConfiguredModeratorRole(member, { modRole: 'other-role' }), false); -}); - -test('memberMeetsCommandPermissions accepts modRole for moderation commands', () => { - const member = { - id: 'user-1', - guild: { ownerId: 'owner-1' }, - permissions: { has: () => false }, - roles: { - cache: { - has: (id) => id === 'mod-role-1', - }, - }, - }; - - assert.equal( - memberMeetsCommandPermissions(member, PermissionFlagsBits.BanMembers, { - guildConfig: { modRole: 'mod-role-1' }, - commandCategory: 'moderation', - }), - true, - ); -}); diff --git a/tests/prefixResponse.test.js b/tests/prefixResponse.test.js deleted file mode 100644 index d4e6741c33..0000000000 --- a/tests/prefixResponse.test.js +++ /dev/null @@ -1,133 +0,0 @@ -import test from 'node:test'; -import assert from 'node:assert/strict'; - -import banCommand from '../src/commands/Moderation/ban.js'; -import warnCommand from '../src/commands/Moderation/warn.js'; -import { ResponseCoordinator, buildPrefixUsage } from '../src/utils/responseCoordinator.js'; -import { executePrefixCommand, createMockInteraction } from '../src/utils/messageAdapter.js'; - -const banData = banCommand.default?.data || banCommand.data || banCommand; -const warnData = warnCommand.default?.data || warnCommand.data || warnCommand; - -function createMockMessage(content = '!ban') { - const sentMessages = []; - - const channel = { - send: async (payload) => { - sentMessages.push(payload); - return { - id: `msg-${sentMessages.length}`, - edit: async (editPayload) => { - sentMessages[sentMessages.length - 1] = editPayload; - return sentMessages[sentMessages.length - 1]; - }, - deletable: true, - delete: async () => {}, - }; - }, - }; - - const message = { - id: 'trigger-msg', - content, - author: { id: 'user-1', tag: 'User#0001', bot: false, toString: () => '<@user-1>' }, - member: { permissions: { has: () => true } }, - guild: { id: 'guild-1', members: { cache: new Map(), fetch: async () => null } }, - channel, - client: { config: { bot: { prefix: '!' } } }, - createdTimestamp: Date.now(), - createdAt: new Date(), - deletable: false, - }; - - return { message, sentMessages, channel }; -} - -test('buildPrefixUsage formats empty ban command args', () => { - const options = { subcommandName: null, subcommandGroupName: null, optionDefs: banData.options.map((opt) => ({ name: opt.name, required: opt.required })) }; - const usage = buildPrefixUsage('!', banData, options); - assert.equal(usage, '!ban [target] [reason]'); -}); - -function getEmbedTitle(payload) { - const embed = payload.embeds?.[0]; - return embed?.data?.title ?? embed?.title ?? ''; -} - -function getEmbedDescription(payload) { - const embed = payload.embeds?.[0]; - return embed?.data?.description ?? embed?.description ?? ''; -} - -test('ResponseCoordinator respondUsage finalizes and blocks second respond', async () => { - const { message, sentMessages } = createMockMessage(); - const interaction = { replied: false, deferred: false, _isPrefixCommand: true, _replyMessage: null }; - const coordinator = ResponseCoordinator.attach(interaction, { message }); - - await coordinator.respondUsage('!ban [target] [reason]'); - assert.equal(sentMessages.length, 1); - assert.match(getEmbedTitle(sentMessages[0]), /Wrong usage/); - assert.equal(coordinator.isUsageFinalized(), true); - - await coordinator.respond({ content: 'should not send' }); - assert.equal(sentMessages.length, 1); -}); - -test('ResponseCoordinator second respond edits existing message', async () => { - const { message, sentMessages } = createMockMessage(); - const interaction = { replied: false, deferred: false, _isPrefixCommand: true, _replyMessage: null }; - const coordinator = ResponseCoordinator.attach(interaction, { message }); - - await coordinator.respond({ content: 'first' }); - await coordinator.respond({ content: 'second' }); - assert.equal(sentMessages.length, 1); - assert.equal(sentMessages[0].content, 'second'); -}); - -test('executePrefixCommand sends exactly one usage embed for !ban with no args', async () => { - const { message, sentMessages } = createMockMessage('!ban'); - const command = { data: banData, execute: banCommand.default?.execute || banCommand.execute, category: 'moderation' }; - - await executePrefixCommand(command, message, [], message.client, '!'); - - assert.equal(sentMessages.length, 1); - assert.match(getEmbedTitle(sentMessages[0]), /Wrong usage/); - assert.match(getEmbedDescription(sentMessages[0]), /!ban \[target\] \[reason\]/); -}); - -test('executePrefixCommand sends exactly one usage embed for !warn with no args', async () => { - const { message, sentMessages } = createMockMessage('!warn'); - const command = { data: warnData, execute: warnCommand.default?.execute || warnCommand.execute, category: 'moderation' }; - - await executePrefixCommand(command, message, [], message.client, '!'); - - assert.equal(sentMessages.length, 1); - assert.match(getEmbedTitle(sentMessages[0]), /Wrong usage/); - assert.match(getEmbedDescription(sentMessages[0]), /!warn \[target\] \[reason\]/); -}); - -test('ResponseCoordinator edit after defer sends without recursion for prefix commands', async () => { - const { message, sentMessages } = createMockMessage(); - const mockInteraction = createMockInteraction(message, banData, []); - - await mockInteraction.deferReply(); - await mockInteraction.editReply({ content: 'balance result' }); - - assert.equal(sentMessages.length, 1); - assert.equal(sentMessages[0].content, 'balance result'); -}); - -test('createMockInteraction routes command reply through coordinator without duplicate sends', async () => { - const { message, sentMessages } = createMockMessage('!ban'); - const mockInteraction = createMockInteraction(message, banData, []); - - await mockInteraction._responseCoordinator.respondUsage('!ban [target] [reason]'); - await mockInteraction.reply({ content: 'duplicate attempt' }); - - assert.equal(sentMessages.length, 1); - assert.equal(coordinatorUsageFinalized(mockInteraction), true); -}); - -function coordinatorUsageFinalized(interaction) { - return interaction._responseCoordinator?.isUsageFinalized() === true; -}