├── .env.example ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── dependabot.yml └── workflows │ └── eslint.yml ├── .gitignore ├── .prettierrc ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── ad_generate.ts ├── eslint.config.js ├── package.json ├── prisma └── schema.prisma ├── seyfert.config.js ├── src ├── client │ ├── commands │ │ ├── admin │ │ │ └── lang.ts │ │ ├── info │ │ │ ├── botinfo.ts │ │ │ ├── cluster.ts │ │ │ ├── nodes.ts │ │ │ └── ping.ts │ │ └── music │ │ │ ├── Filters.ts │ │ │ ├── autoplay.ts │ │ │ ├── loop.ts │ │ │ ├── move-node.ts │ │ │ ├── pause.ts │ │ │ ├── play.ts │ │ │ ├── resume.ts │ │ │ ├── skip.ts │ │ │ ├── stop.ts │ │ │ └── volume.ts │ ├── environment.ts │ ├── events │ │ └── client │ │ │ ├── GuildCreate.ts │ │ │ ├── GuildDelete.ts │ │ │ ├── botReady.ts │ │ │ ├── interaction.ts │ │ │ └── raw.ts │ ├── index.ts │ ├── interfaces │ │ └── IDatabase.ts │ ├── languages │ │ ├── en.ts │ │ ├── th.ts │ │ └── types.ts │ ├── service │ │ ├── commands │ │ │ ├── BotinfoCommand.ts │ │ │ ├── ClusterCommand.ts │ │ │ ├── FiltersCommmand.ts │ │ │ ├── LangCommand.ts │ │ │ ├── LoopCommand.ts │ │ │ ├── MoveNode.ts │ │ │ ├── MusicPlay.ts │ │ │ ├── NodeCommand.ts │ │ │ ├── PauseCommand.ts │ │ │ ├── ResumeCommand.ts │ │ │ ├── SkipCommand.ts │ │ │ ├── StopCommand.ts │ │ │ └── VolumeCommand.ts │ │ └── player │ │ │ ├── NodeConnect.ts │ │ │ ├── NodeCreate.ts │ │ │ ├── NodeDestroy.ts │ │ │ ├── NodeDisconnect.ts │ │ │ ├── NodeError.ts │ │ │ ├── PlayerCreate.ts │ │ │ ├── PlayerDestroy.ts │ │ │ ├── PlayerDisconnect.ts │ │ │ ├── PlayerMove.ts │ │ │ ├── QueueEnd.ts │ │ │ ├── TrackEnd.ts │ │ │ ├── TrackError.ts │ │ │ ├── TrackStart.ts │ │ │ └── TrackStuck.ts │ └── structures │ │ ├── ServiceExecute.ts │ │ ├── Starlight.ts │ │ ├── Utils.ts │ │ └── utils │ │ ├── Client.ts │ │ ├── Logger.ts │ │ └── cluster │ │ ├── ClusterClient.ts │ │ ├── GetInfo.ts │ │ ├── IWorkerManager.ts │ │ └── Worker.ts ├── cluster.ts ├── config.ts └── lib │ ├── DiscordAnalytics.ts │ ├── modules │ └── Logger.ts │ └── utils │ └── types.ts └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | PRODUCTION_TOKEN=your_production_token_here 2 | PRODUCTION_REDIS=your_production_redis_here 3 | 4 | DEVELOPMENT_TOKEN=your_development_token_here 5 | DEVELOPMENT_REDIS=your_development_redis_here 6 | // MongoDB 7 | DATABASE_URL= -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [EvarinDev] 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/eslint.yml: -------------------------------------------------------------------------------- 1 | name: ESLint 2 | 3 | on: 4 | push: 5 | branches: [ "full-system", "dev" ] 6 | pull_request: 7 | branches: [ "full-system", "dev" ] 8 | 9 | jobs: 10 | eslint: 11 | name: Run eslint scanning 12 | runs-on: ubuntu-latest 13 | permissions: 14 | contents: read 15 | security-events: write 16 | steps: 17 | - name: Checkout code 18 | uses: actions/checkout@v3 19 | 20 | - name: Install ESLint and Dependencies 21 | run: | 22 | npm install eslint@8.51.0 @typescript-eslint/eslint-plugin@6.7.4 @typescript-eslint/parser@6.7.4 @microsoft/eslint-formatter-sarif --force 23 | - name: Run ESLint 24 | run: | 25 | mkdir -p reports 26 | npx eslint src/**/*.ts \ 27 | --config ./eslint.config.js \ 28 | -f @microsoft/eslint-formatter-sarif \ 29 | -o ./reports/eslint-results.sarif 30 | 31 | - name: Upload SARIF file 32 | uses: github/codeql-action/upload-sarif@v2 33 | with: 34 | sarif_file: ./reports/eslint-results.sarif 35 | wait-for-processing: true -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore 2 | 3 | # Logs 4 | 5 | logs 6 | _.log 7 | npm-debug.log_ 8 | yarn-debug.log* 9 | yarn-error.log* 10 | lerna-debug.log* 11 | .pnpm-debug.log* 12 | 13 | # Caches 14 | 15 | .cache 16 | 17 | # Diagnostic reports (https://nodejs.org/api/report.html) 18 | 19 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 20 | 21 | # Runtime data 22 | 23 | pids 24 | _.pid 25 | _.seed 26 | *.pid.lock 27 | 28 | # Directory for instrumented libs generated by jscoverage/JSCover 29 | 30 | lib-cov 31 | 32 | # Coverage directory used by tools like istanbul 33 | 34 | coverage 35 | *.lcov 36 | 37 | # nyc test coverage 38 | 39 | .nyc_output 40 | 41 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 42 | 43 | .grunt 44 | 45 | # Bower dependency directory (https://bower.io/) 46 | 47 | bower_components 48 | 49 | # node-waf configuration 50 | 51 | .lock-wscript 52 | 53 | # Compiled binary addons (https://nodejs.org/api/addons.html) 54 | 55 | build/Release 56 | 57 | # Dependency directories 58 | 59 | node_modules/ 60 | jspm_packages/ 61 | 62 | # Snowpack dependency directory (https://snowpack.dev/) 63 | 64 | web_modules/ 65 | 66 | # TypeScript cache 67 | 68 | *.tsbuildinfo 69 | 70 | # Optional npm cache directory 71 | 72 | .npm 73 | 74 | # Optional eslint cache 75 | 76 | .eslintcache 77 | 78 | # Optional stylelint cache 79 | 80 | .stylelintcache 81 | 82 | # Microbundle cache 83 | 84 | .rpt2_cache/ 85 | .rts2_cache_cjs/ 86 | .rts2_cache_es/ 87 | .rts2_cache_umd/ 88 | 89 | # Optional REPL history 90 | 91 | .node_repl_history 92 | 93 | # Output of 'npm pack' 94 | 95 | *.tgz 96 | 97 | # Yarn Integrity file 98 | 99 | .yarn-integrity 100 | 101 | # dotenv environment variable files 102 | 103 | .env 104 | .env.development.local 105 | .env.test.local 106 | .env.production.local 107 | .env.local 108 | 109 | # parcel-bundler cache (https://parceljs.org/) 110 | 111 | .parcel-cache 112 | 113 | # Next.js build output 114 | 115 | .next 116 | out 117 | 118 | # Nuxt.js build / generate output 119 | 120 | .nuxt 121 | dist 122 | 123 | # Gatsby files 124 | 125 | # Comment in the public line in if your project uses Gatsby and not Next.js 126 | 127 | # https://nextjs.org/blog/next-9-1#public-directory-support 128 | 129 | # public 130 | 131 | # vuepress build output 132 | 133 | .vuepress/dist 134 | 135 | # vuepress v2.x temp and cache directory 136 | 137 | .temp 138 | 139 | # Docusaurus cache and generated files 140 | 141 | .docusaurus 142 | 143 | # Serverless directories 144 | 145 | .serverless/ 146 | 147 | # FuseBox cache 148 | 149 | .fusebox/ 150 | 151 | # DynamoDB Local files 152 | 153 | .dynamodb/ 154 | 155 | # TernJS port file 156 | 157 | .tern-port 158 | 159 | # Stores VSCode versions used for testing VSCode extensions 160 | 161 | .vscode-test 162 | 163 | # yarn v2 164 | 165 | .yarn/cache 166 | .yarn/unplugged 167 | .yarn/build-state.yml 168 | .yarn/install-state.gz 169 | .pnp.* 170 | 171 | # IntelliJ based IDEs 172 | .idea 173 | 174 | # Finder (MacOS) folder config 175 | .DS_Store 176 | bun.lockb 177 | .env 178 | dump.rdb 179 | bun.lock -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "printWidth": 200 4 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.wordBasedSuggestions": "off" 3 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2024 EvarinDev 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | 15 | ## Additional Terms / ข้อกำหนดเพิ่มเติม 16 | 17 | EN: It is prohibited to sell this source code without written consent from the copyright owner. 18 | TH: ห้ามนำ source code นี้ไปขายโดยไม่ได้รับการยินยอมเป็นลายลักษณ์อักษรจากเจ้าของลิขสิทธิ์ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Starlight

2 | 3 |
4 | 5 | [![Status](https://img.shields.io/badge/status-active-success.svg)]() 6 | [![GitHub Issues](https://img.shields.io/github/issues/EvarinDev/Starlight.svg)](https://github.com/EvarinDev/Starlight/issues) 7 | [![GitHub Pull Requests](https://img.shields.io/github/issues-pr/EvarinDev/Starlight.svg)](https://github.com/EvarinDev/Starlight/pulls) 8 | [![License](https://img.shields.io/badge/license-MIT-blue.svg)](/LICENSE) 9 | 10 |
11 | 12 | --- 13 | 14 |

Discord Bot Music 15 |
16 |

17 | 18 | ## 📝 Table of Contents 19 | 20 | - [📝 Table of Contents](#-table-of-contents) 21 | - [🧐 About ](#-about-) 22 | - [🏁 Getting Started ](#-getting-started-) 23 | - [Prerequisites](#prerequisites) 24 | - [Installing](#installing) 25 | - [⛏️ Built Using ](#️-built-using-) 26 | - [✍️ Authors ](#️-authors-) 27 | - [Star History](#star-history) 28 | 29 | ## 🧐 About 30 | This bot is the **full source code** of [Asta](https://discord.com/application-directory/1259476682730111097), a powerful and efficient Discord bot. The implementation below uses **Seyfert** and **TypeScript**, providing a solid structure to handle messages and commands effortlessly. 31 | 32 | Asta is designed to deliver a smooth user experience while being highly extensible. This code contains everything needed to get Asta up and running on your own Discord server, complete with essential features for managing messages, handling commands, and integrating with music functionality. 33 | 34 | ## 🏁 Getting Started 35 | 36 | These instructions will get you a copy of the project up and running on your local machine for development and testing purposes. See [deployment](#deployment) for notes on how to deploy the project on a live system. 37 | 38 | ### Prerequisites 39 | 40 | What things you need to install the software and how to install them. 41 | 42 | ``` 43 | Bun 1.1.29 44 | ``` 45 | 46 | ### Installing 47 | 48 | A step by step series of examples that tell you how to get a development env running. 49 | 50 | Say what the step will be 51 | 52 | ``` 53 | git clone https://github.com/EvarinDev/Starlight.git 54 | cd Starlight 55 | bun run full-install 56 | Rename .env.example to .env and Setup Config 57 | bun run dev 58 | ``` 59 | 60 | ## ⛏️ Built Using 61 | 62 | - [MongoDB](https://www.mongodb.com/) - Database 63 | - [Prisma](https://www.prisma.io/) - Database toolkit 64 | - [Seyfert](https://www.seyfert.dev/) - Discord framework 65 | - [Lithiumx](https://github.com/anantix-network/LithiumX) - Lavalink wrapper 66 | - [Bun](https://bun.sh/) - JavaScript runtime 67 | 68 | ## ✍️ Authors 69 | 70 | - [@EvarinDev](https://github.com/EvarinDev) - Idea & Initial work 71 | See also the list of [contributors](https://github.com/EvarinDev/Starlight/contributors) who participated in this project. 72 | 73 | ## Star History 74 | 75 | [![Star History Chart](https://api.star-history.com/svg?repos=EvarinDev/Starlight&type=Date)](https://star-history.com/#EvarinDev/Starlight&Date) 76 | -------------------------------------------------------------------------------- /ad_generate.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | 3 | // Create the PrismaClient with more robust error handling 4 | let prisma: PrismaClient; 5 | try { 6 | prisma = new PrismaClient({}); 7 | } catch (error) { 8 | console.error("Failed to initialize Prisma Client:", error); 9 | console.error("\nPlease run 'npx prisma generate' or 'bunx prisma generate' before using this script."); 10 | process.exit(1); 11 | } 12 | 13 | async function generateAds() { 14 | // Show help if requested 15 | if (process.argv.includes('--help') || process.argv.includes('-h')) { 16 | displayHelp(); 17 | return; 18 | } 19 | 20 | if (process.argv.length < 7) { 21 | console.error("Usage: bun ad_generate.ts "); 22 | console.error("Try 'bun ad_generate.ts --help' for more information."); 23 | return; 24 | } 25 | const input = process.argv[2]; 26 | if (!input) { 27 | console.error("Please provide a language code (en or th)."); 28 | return; 29 | } 30 | const lang = input.toLowerCase(); 31 | if (lang !== "en" && lang !== "th") { 32 | console.error("Invalid language code. Please use 'en' or 'th'."); 33 | return; 34 | } 35 | const name = process.argv[3]; 36 | const description = process.argv[4]; 37 | const image = process.argv[5]; 38 | const url = process.argv[6]; 39 | if (!name || !description || !image || !url) { 40 | console.error("Please provide all required fields: name, description, image, url."); 41 | return; 42 | } 43 | // Validate URL format 44 | if (!isValidUrl(url)) { 45 | console.error("Error: Invalid URL format. Please provide a valid URL (e.g., https://example.com)"); 46 | return; 47 | } 48 | 49 | // Validate image URL format (simple check) 50 | if (!isValidImageUrl(image)) { 51 | console.error("Warning: Image URL might not be valid. Please ensure it points to an image file."); 52 | // Just a warning, continue execution 53 | } 54 | 55 | // Add confirmation step 56 | console.log("\nAd details to be created:"); 57 | console.log("-------------------------"); 58 | console.log(`Language: ${lang}`); 59 | console.log(`Name: ${name}`); 60 | console.log(`Description: ${description}`); 61 | console.log(`Image URL: ${image}`); 62 | console.log(`URL: ${url}`); 63 | console.log("-------------------------"); 64 | 65 | // Check for --force flag to skip confirmation 66 | if (!process.argv.includes('--force') && !process.argv.includes('-f')) { 67 | const confirmation = await askForConfirmation("Do you want to create this ad? (y/n): "); 68 | if (!confirmation) { 69 | console.log("Operation cancelled by user."); 70 | await prisma.$disconnect(); 71 | return; 72 | } 73 | } 74 | 75 | try { 76 | // Generate a UUID for the ID field 77 | const id = generateUUID(); 78 | 79 | const ad = await prisma.ads.create({ 80 | data: { 81 | id: id, 82 | lang: lang, 83 | name: name, 84 | description: description, 85 | image: image, 86 | url: url, 87 | }, 88 | }); 89 | console.log("\n✅ Ad created successfully!"); 90 | console.log(`ID: ${ad.id}`); 91 | console.log(`Language: ${ad.lang}`); 92 | console.log(`Name: ${ad.name}`); 93 | } catch (error) { 94 | console.error("Error creating ad:", error); 95 | } finally { 96 | // Always disconnect from Prisma client to avoid hanging connections 97 | await prisma.$disconnect(); 98 | } 99 | } 100 | 101 | // Helper functions 102 | function isValidUrl(string: string): boolean { 103 | try { 104 | const url = new URL(string); 105 | return url.protocol === "http:" || url.protocol === "https:"; 106 | } catch (_) { 107 | return false; 108 | } 109 | } 110 | 111 | function isValidImageUrl(string: string): boolean { 112 | try { 113 | const url = new URL(string); 114 | const path = url.pathname.toLowerCase(); 115 | return ( 116 | (url.protocol === "http:" || url.protocol === "https:") && 117 | (path.endsWith('.jpg') || path.endsWith('.jpeg') || 118 | path.endsWith('.png') || path.endsWith('.gif') || 119 | path.endsWith('.webp') || path.endsWith('.svg')) 120 | ); 121 | } catch (_) { 122 | return false; 123 | } 124 | } 125 | 126 | async function askForConfirmation(question: string): Promise { 127 | const readline = require('readline').createInterface({ 128 | input: process.stdin, 129 | output: process.stdout 130 | }); 131 | 132 | return new Promise((resolve) => { 133 | readline.question(question, (answer: string) => { 134 | readline.close(); 135 | resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes'); 136 | }); 137 | }); 138 | } 139 | 140 | function displayHelp(): void { 141 | console.log("\nAd Generator Help"); 142 | console.log("================"); 143 | console.log("\nUsage:"); 144 | console.log(" bun ad_generate.ts [options]"); 145 | console.log("\nArguments:"); 146 | console.log(" lang Language code (en or th)"); 147 | console.log(" name Name of the advertisement"); 148 | console.log(" description Description text for the advertisement"); 149 | console.log(" image URL to the image for the advertisement"); 150 | console.log(" url Destination URL for the advertisement"); 151 | console.log("\nOptions:"); 152 | console.log(" --help, -h Display this help text"); 153 | console.log(" --force, -f Skip confirmation prompt"); 154 | console.log("\nExample:"); 155 | console.log(' bun ad_generate.ts en "Product Name" "Amazing product description" "https://example.com/image.jpg" "https://example.com/product"'); 156 | console.log(""); 157 | } 158 | 159 | // Helper function to generate a UUID 160 | function generateUUID(): string { 161 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { 162 | const r = Math.random() * 16 | 0; 163 | const v = c === 'x' ? r : (r & 0x3 | 0x8); 164 | return v.toString(16); 165 | }); 166 | } 167 | 168 | // Display a clear message to the user 169 | console.log("Ads Generator Script - Starting..."); 170 | console.log("Make sure Prisma is properly generated using 'bunx prisma generate' if you encounter errors"); 171 | 172 | // Execute the function 173 | generateAds() 174 | .catch(e => { 175 | console.error("Script execution failed:", e); 176 | prisma.$disconnect(); 177 | process.exit(1); 178 | }); -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | const pluginJs = require("@eslint/js"); 2 | const globals = require("globals"); 3 | const tseslint = require("typescript-eslint"); 4 | 5 | module.exports = [ 6 | pluginJs.configs.recommended, 7 | ...tseslint.configs.recommendedTypeChecked, 8 | { 9 | files: ["**/*.{js,mjs,cjs,ts,tsx}"], 10 | ignores: ["**/node_modules/**", "**/dist/**", "**/build/**"], 11 | languageOptions: { 12 | globals: { 13 | ...globals.browser, 14 | ...globals.node 15 | }, 16 | }, 17 | plugins: { 18 | "@typescript-eslint": tseslint.plugin 19 | }, 20 | rules: { 21 | "no-unused-vars": "off", 22 | "@typescript-eslint/no-unused-vars": "warn", 23 | "no-undef": "off", 24 | }, 25 | }, 26 | { 27 | files: ["**/*.ts", "**/*.tsx"], 28 | languageOptions: { 29 | parser: tseslint.parser, 30 | parserOptions: { 31 | project: "./tsconfig.json" 32 | }, 33 | }, 34 | } 35 | ]; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@starlight/client", 3 | "module": "src/cluster.ts", 4 | "main": "dist/cluster.js", 5 | "version": "2.0.0-fix", 6 | "author": "EvarinDev", 7 | "license": "Apache-2.0", 8 | "scripts": { 9 | "full-install": "bun i && bun x prisma generate", 10 | "start": "bun x tsc && NODE_ENV=production bun dist/cluster.js", 11 | "dev": "bun x tsc && NODE_ENV=development bun dist/cluster.js", 12 | "lint": "bun x eslint src/**/*.ts", 13 | "type:check": "bun x tsc", 14 | "build": "bun x tsc" 15 | }, 16 | "devDependencies": { 17 | "@dep-tree/cli": "^0.23.4", 18 | "@eslint/js": "^9.27.0", 19 | "@types/bun": "latest", 20 | "@types/common-tags": "^1.8.4", 21 | "@types/ws": "^8.18.1", 22 | "eslint": "^9.27.0", 23 | "globals": "^16.1.0", 24 | "typescript-eslint": "^8.32.1" 25 | }, 26 | "peerDependencies": { 27 | "typescript": "^5.8.3" 28 | }, 29 | "dependencies": { 30 | "@prisma/client": "^6.8.2", 31 | "@slipher/redis-adapter": "^0.0.4", 32 | "ansi-colors": "^4.1.3", 33 | "common-tags": "^1.8.2", 34 | "discord-api-types": "^0.38.8", 35 | "dotenv": "^16.5.0", 36 | "ioredis": "^5.6.1", 37 | "lithiumx": "^1.0.8", 38 | "ms": "^2.1.3", 39 | "redis": "^5.1.0", 40 | "seyfert": "3.1.2" 41 | } 42 | } -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | // Looking for ways to speed up your queries, or scale easily with your serverless or edge functions? 5 | // Try Prisma Accelerate: https://pris.ly/cli/accelerate-init 6 | 7 | generator client { 8 | provider = "prisma-client-js" 9 | binaryTargets = ["native", "windows", "debian-openssl-3.0.x"] 10 | } 11 | 12 | datasource db { 13 | provider = "mongodb" 14 | url = env("DATABASE_URL") 15 | } 16 | 17 | model guild { 18 | uuid String @id @default(auto()) @map("_id") @db.ObjectId 19 | id String 20 | name String 21 | room Room @relation(fields: [roomid], references: [uuid]) 22 | ai AI @relation(fields: [aiid], references: [uuid]) 23 | aiid String @db.ObjectId 24 | roomid String @db.ObjectId 25 | lang Lang 26 | } 27 | 28 | model Room { 29 | uuid String @id @default(auto()) @map("_id") @db.ObjectId 30 | id String @default("") 31 | message String @default("") 32 | guild guild[] 33 | } 34 | 35 | model AI { 36 | uuid String @id @default(auto()) @map("_id") @db.ObjectId 37 | name String 38 | guild guild[] 39 | channel String 40 | } 41 | 42 | model Client { 43 | uuid String @id @default(auto()) @map("_id") @db.ObjectId 44 | id String 45 | token String 46 | } 47 | 48 | model Ads { 49 | uuid String @id @default(auto()) @map("_id") @db.ObjectId 50 | id String 51 | name String 52 | description String 53 | lang Lang 54 | url String 55 | image String 56 | } 57 | 58 | enum Lang { 59 | en 60 | th 61 | en_us 62 | } -------------------------------------------------------------------------------- /seyfert.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check is better 2 | const { config } = require('seyfert'); 3 | require("dotenv/config"); 4 | const c = require("./dist/config.js"); 5 | 6 | module.exports = config.bot({ 7 | token: c.default.TOKEN ?? "", 8 | applicationId: c.default.APPLICATION_ID ?? "", 9 | intents: ["Guilds", "GuildVoiceStates"], 10 | locations: { 11 | base: "src/client", 12 | commands: "commands", 13 | events: "events", 14 | langs: "languages" 15 | } 16 | }); -------------------------------------------------------------------------------- /src/client/commands/admin/lang.ts: -------------------------------------------------------------------------------- 1 | import { ErrorRequest } from "@/client/structures/utils/Client"; 2 | import { Options, createStringOption, Declare, Command, type CommandContext } from "seyfert"; 3 | 4 | export const LangCommandOptions = { 5 | language: createStringOption({ 6 | description: "[EN]: Change the language | [TH]: เปลี่ยนภาษา", 7 | required: true, 8 | choices: [ 9 | { 10 | name: "English", 11 | value: "en", 12 | }, 13 | { 14 | name: "Thai", 15 | value: "th", 16 | }, 17 | ] as const, 18 | }), 19 | }; 20 | 21 | @Declare({ 22 | name: "lang", 23 | description: "[EN]: Change the language | [TH]: เปลี่ยนภาษา", 24 | }) 25 | @Options(LangCommandOptions) 26 | export default class LangCommand extends Command { 27 | async run(ctx: CommandContext) { 28 | try { 29 | return await ctx.client.services.execute("LangCommand", ctx); 30 | } catch (error) { 31 | return ErrorRequest(ctx, error as Error); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/client/commands/info/botinfo.ts: -------------------------------------------------------------------------------- 1 | import { ErrorRequest } from "@/client/structures/utils/Client"; 2 | import { Declare, Command, type CommandContext } from "seyfert"; 3 | 4 | @Declare({ 5 | name: "botinfo", 6 | description: "[EN]: Show the bot information | [TH]: แสดงข้อมูลของบอท", 7 | }) 8 | export default class BotinfoCommand extends Command { 9 | async run(ctx: CommandContext) { 10 | try { 11 | return await ctx.client.services.execute("BotinfoCommand", ctx); 12 | } catch (error) { 13 | return ErrorRequest(ctx, error as Error); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/client/commands/info/cluster.ts: -------------------------------------------------------------------------------- 1 | import { ErrorRequest } from "@/client/structures/utils/Client"; 2 | import { Declare, Command, type CommandContext } from "seyfert"; 3 | 4 | @Declare({ 5 | name: "cluster", 6 | description: "[EN]: Show the cluster information | [TH]: แสดงข้อมูลของคลัสเตอร์", 7 | }) 8 | export default class ClusterCommand extends Command { 9 | async run(ctx: CommandContext) { 10 | try { 11 | return await ctx.client.services.execute("ClusterCommand", ctx); 12 | } catch (error) { 13 | return ErrorRequest(ctx, error as Error); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/client/commands/info/nodes.ts: -------------------------------------------------------------------------------- 1 | import { ErrorRequest } from "@/client/structures/utils/Client"; 2 | import { Declare, Command, type CommandContext } from "seyfert"; 3 | 4 | @Declare({ 5 | name: "nodes", 6 | description: "[EN]: Show the nodes | [TH]: แสดงโหนด", 7 | }) 8 | export default class NodeCommand extends Command { 9 | async run(ctx: CommandContext) { 10 | try { 11 | return await ctx.client.services.execute("NodeCommand", ctx); 12 | } catch (error) { 13 | return ErrorRequest(ctx, error as Error); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/client/commands/info/ping.ts: -------------------------------------------------------------------------------- 1 | import { Declare, Command, type CommandContext, Embed } from "seyfert"; 2 | 3 | @Declare({ 4 | name: "ping", 5 | description: "Show the ping with discord", 6 | }) 7 | export default class PingCommand extends Command { 8 | async run(ctx: CommandContext) { 9 | const start = performance.now(); 10 | function PingStatus(ping: number) { 11 | if (ping < 50) { 12 | return "🟢" 13 | } else if (ping < 100) { 14 | return "🟡" 15 | } else if (ping < 260) { 16 | return "🔴" 17 | } else { 18 | return "⚫" 19 | } 20 | } 21 | const embed: Embed = new Embed() 22 | .setAuthor({ 23 | name: `${ctx.client.me?.username} Pong!`, 24 | iconUrl: ctx.client.me?.avatarURL(), 25 | }) 26 | .addFields( 27 | { 28 | name: `${PingStatus(ctx.client.latency)} Cluster [${ctx.client.workerId}]`, 29 | value: `┗ ${ctx.client.latency}ms\n` 30 | } 31 | ) 32 | .setFooter({ 33 | text: `Requested by ${ctx.author.username} | Execution Time: ${Math.round(performance.now() - start)}ms`, 34 | iconUrl: ctx.author.avatarURL() 35 | }) 36 | return ctx.editResponse({ 37 | embeds: [embed] 38 | }) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/client/commands/music/Filters.ts: -------------------------------------------------------------------------------- 1 | import { ErrorRequest } from "@/client/structures/utils/Client"; 2 | import { Declare, Command, type CommandContext, Options, createStringOption, createBooleanOption } from "seyfert"; 3 | 4 | export const FiltersCommandOptions = { 5 | filter: createStringOption({ 6 | description: "[EN]: The filter you want to apply | [TH]: ตัวกรองที่คุณต้องการใช้", 7 | choices: [ 8 | { name: 'Bass Boost', value: 'bassboost' }, 9 | { name: 'Distortion', value: 'distort' }, 10 | { name: '8D', value: 'eightD' }, 11 | { name: 'Karaoke', value: 'karaoke' }, 12 | { name: 'Nightcore', value: 'nightcore' }, 13 | { name: 'Slow Motion', value: 'slowmo' }, 14 | { name: 'Soft', value: 'soft' }, 15 | { name: 'Treble Bass', value: 'trebleBass' }, 16 | { name: 'TV', value: 'tv' }, 17 | { name: 'Vaporwave', value: 'vaporwave' }, 18 | { name: 'Clear', value: 'clear' }, 19 | ], 20 | required: true, 21 | }), 22 | mode: createBooleanOption({ 23 | description: "[EN]: Enable or disable the filter | [TH]: เปิดหรือปิดตัวกรอง", 24 | required: false, 25 | }), 26 | }; 27 | @Declare({ 28 | name: "filters", 29 | description: "[EN]: Apply a filter to the current song | [TH]: ใช้ตัวกรองกับเพลงปัจจุบัน", 30 | contexts: ["Guild"], 31 | }) 32 | @Options(FiltersCommandOptions) 33 | export default class PlayCommand extends Command { 34 | async run(ctx: CommandContext) { 35 | try { 36 | return await ctx.client.services.execute("FiltersCommmand", ctx); 37 | } catch (error) { 38 | return ErrorRequest(ctx, error as Error); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/client/commands/music/autoplay.ts: -------------------------------------------------------------------------------- 1 | import { ErrorRequest } from "@/client/structures/utils/Client"; 2 | import { Declare, Command, type CommandContext, Options, createStringOption } from "seyfert"; 3 | 4 | export const AutoPlayCommandOptions = { 5 | type: createStringOption({ 6 | description: "[EN]: Skip the music | [TH]: ข้ามเพลง", 7 | required: true, 8 | choices: [ 9 | { 10 | name: "Enable | เปิด", 11 | value: "true", 12 | }, 13 | { 14 | name: "Disable | ปิด", 15 | value: "false", 16 | }, 17 | ] as const, 18 | }), 19 | }; 20 | 21 | @Declare({ 22 | name: "autoplay", 23 | description: "[EN]: AutoPlay the music | [TH]: เล่นเพลงอัตโนมัติ", 24 | contexts: ["Guild"], 25 | }) 26 | @Options(AutoPlayCommandOptions) 27 | export default class AutoPlayCommand extends Command { 28 | async run(ctx: CommandContext) { 29 | try { 30 | const player = ctx.client.lithiumx.players.get(ctx.guildId); 31 | if (!player) { 32 | return ctx.editOrReply({ 33 | content: "There is no player in this guild.", 34 | }); 35 | } else { 36 | if (ctx.options.type) { 37 | player.setAutoplay(true, ctx.author); 38 | return ctx.editOrReply({ 39 | content: "AutoPlay has been enabled.", 40 | }); 41 | } else { 42 | player.setAutoplay(false, ctx.author); 43 | return ctx.editOrReply({ 44 | content: "AutoPlay has been disabled.", 45 | }); 46 | } 47 | } 48 | } catch (error) { 49 | return ErrorRequest(ctx, error as Error); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/client/commands/music/loop.ts: -------------------------------------------------------------------------------- 1 | import { ErrorRequest } from "@/client/structures/utils/Client"; 2 | import { Declare, Command, type CommandContext, Options, createStringOption } from "seyfert"; 3 | 4 | export const LoopCommandOptions = { 5 | type: createStringOption({ 6 | description: "[EN]: Loop the queue | [TH]: วนรายการ", 7 | required: true, 8 | choices: [ 9 | { 10 | name: "Queue", 11 | value: "queue", 12 | }, 13 | { 14 | name: "Track", 15 | value: "song", 16 | }, 17 | { 18 | name: "Off", 19 | value: "off", 20 | }, 21 | ] as const, 22 | }), 23 | }; 24 | 25 | @Declare({ 26 | name: "loop", 27 | description: "[EN]: Loop the queue | [TH]: วนรายการ", 28 | contexts: ["Guild"], 29 | }) 30 | @Options(LoopCommandOptions) 31 | export default class LoopCommand extends Command { 32 | async run(ctx: CommandContext) { 33 | try { 34 | return await ctx.client.services.execute("LoopCommand", ctx); 35 | } catch (error) { 36 | return ErrorRequest(ctx, error as Error); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/client/commands/music/move-node.ts: -------------------------------------------------------------------------------- 1 | import { ErrorRequest } from "@/client/structures/utils/Client"; 2 | import { Declare, Command, type CommandContext, createStringOption, AutocompleteInteraction, Options } from "seyfert"; 3 | 4 | 5 | export const MoveNodeCommandOptions = { 6 | node: createStringOption({ 7 | description: "[EN]: The node you want to play the song | [TH]: โหนดที่คุณต้องการเล่นเพลง", 8 | required: true, 9 | autocomplete: async (interaction: AutocompleteInteraction) => { 10 | const nodes: { 11 | name: string; 12 | value: string; 13 | }[] = interaction.client.lithiumx.nodes.map((node) => ({ 14 | name: `${node.options.identifier} - ${node.stats.players} Players`, 15 | value: node.options.identifier, 16 | })); 17 | return await interaction.respond(nodes).catch(() => { }); 18 | }, 19 | }), 20 | }; 21 | 22 | @Declare({ 23 | name: "move-node", 24 | description: "[EN]: Move Node | [TH]: ย้ายโหนด", 25 | contexts: ["Guild"], 26 | }) 27 | @Options(MoveNodeCommandOptions) 28 | export default class PauseCommand extends Command { 29 | async run(ctx: CommandContext) { 30 | try { 31 | return await ctx.client.services.execute("MoveNode", ctx); 32 | } catch (error) { 33 | return ErrorRequest(ctx, error as Error); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/client/commands/music/pause.ts: -------------------------------------------------------------------------------- 1 | import { ErrorRequest } from "@/client/structures/utils/Client"; 2 | import { Declare, Command, type CommandContext } from "seyfert"; 3 | @Declare({ 4 | name: "pause", 5 | description: "[EN]: Pause the music | [TH]: หยุดเพลง", 6 | contexts: ["Guild"], 7 | }) 8 | export default class PauseCommand extends Command { 9 | async run(ctx: CommandContext) { 10 | try { 11 | return await ctx.client.services.execute("PauseCommand", ctx); 12 | } catch (error) { 13 | return ErrorRequest(ctx, error as Error); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/client/commands/music/play.ts: -------------------------------------------------------------------------------- 1 | import { ErrorRequest } from "@/client/structures/utils/Client"; 2 | import { Declare, Command, type CommandContext, Options, createStringOption, AutocompleteInteraction } from "seyfert"; 3 | 4 | export const PlayCommandOptions = { 5 | search: createStringOption({ 6 | description: "[EN]: The song you want to play | [TH]: เพลงที่คุณต้องการเล่น", 7 | required: true, 8 | }), 9 | node: createStringOption({ 10 | description: "[EN]: The node you want to play the song | [TH]: โหนดที่คุณต้องการเล่นเพลง", 11 | required: false, 12 | autocomplete: async (interaction: AutocompleteInteraction) => { 13 | const nodes: { 14 | name: string; 15 | value: string; 16 | }[] = interaction.client.lithiumx.nodes.map((node) => ({ 17 | name: `${node.options.identifier} - ${node.stats.players} Players`, 18 | value: node.options.identifier, 19 | })); 20 | return await interaction.respond(nodes).catch(() => { }); 21 | }, 22 | }), 23 | }; 24 | @Declare({ 25 | name: "play", 26 | description: "[EN]: Play a song | [TH]: เล่นเพลง", 27 | contexts: ["Guild"], 28 | }) 29 | @Options(PlayCommandOptions) 30 | export default class PlayCommand extends Command { 31 | async run(ctx: CommandContext) { 32 | try { 33 | return await ctx.client.services.execute("MusicPlay", ctx); 34 | } catch (error) { 35 | return ErrorRequest(ctx, error as Error); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/client/commands/music/resume.ts: -------------------------------------------------------------------------------- 1 | import { ErrorRequest } from "@/client/structures/utils/Client"; 2 | import { Declare, Command, type CommandContext } from "seyfert"; 3 | 4 | @Declare({ 5 | name: "resume", 6 | description: "[EN]: Resume the music | [TH]: เล่นเพลงต่อ", 7 | contexts: ["Guild"], 8 | }) 9 | export default class ResumeCommand extends Command { 10 | async run(ctx: CommandContext) { 11 | try { 12 | return await ctx.client.services.execute("ResumeCommand", ctx); 13 | } catch (error) { 14 | return ErrorRequest(ctx, error as Error); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/client/commands/music/skip.ts: -------------------------------------------------------------------------------- 1 | import { ErrorRequest } from "@/client/structures/utils/Client"; 2 | import { Declare, Command, type CommandContext } from "seyfert"; 3 | 4 | @Declare({ 5 | name: "skip", 6 | description: "[EN]: Skip the music | [TH]: ข้ามเพลง", 7 | contexts: ["Guild"], 8 | }) 9 | export default class SkipCommand extends Command { 10 | async run(ctx: CommandContext) { 11 | try { 12 | return await ctx.client.services.execute("SkipCommand", ctx); 13 | } catch (error) { 14 | return ErrorRequest(ctx, error as Error); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/client/commands/music/stop.ts: -------------------------------------------------------------------------------- 1 | import { ErrorRequest } from "@/client/structures/utils/Client"; 2 | import { Declare, Command, type CommandContext } from "seyfert"; 3 | 4 | @Declare({ 5 | name: "stop", 6 | description: "[EN]: Stop the music | [TH]: หยุดเพลง", 7 | contexts: ["Guild"], 8 | }) 9 | export default class StopCommand extends Command { 10 | async run(ctx: CommandContext) { 11 | try { 12 | return await ctx.client.services.execute("StopCommand", ctx); 13 | } catch (error) { 14 | return ErrorRequest(ctx, error as Error); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/client/commands/music/volume.ts: -------------------------------------------------------------------------------- 1 | import { ErrorRequest } from "@/client/structures/utils/Client"; 2 | import { Declare, Command, type CommandContext, Options, createNumberOption } from "seyfert"; 3 | 4 | export const VolumeCommandOptions = { 5 | percent: createNumberOption({ 6 | description: "[EN]: The song you want to play | [TH]: เพลงที่คุณต้องการเล่น", 7 | required: true, 8 | }), 9 | }; 10 | @Declare({ 11 | name: "volume", 12 | description: "[EN]: Change the volume | [TH]: เปลี่ยนระดับเสียง", 13 | contexts: ["Guild"], 14 | }) 15 | @Options(VolumeCommandOptions) 16 | export default class VolumeCommand extends Command { 17 | async run(ctx: CommandContext) { 18 | try { 19 | return await ctx.client.services.execute("VolumeCommand", ctx); 20 | } catch (error) { 21 | return ErrorRequest(ctx, error as Error); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/client/environment.ts: -------------------------------------------------------------------------------- 1 | import { ParseClient, ParseLocales } from "seyfert"; 2 | import { Starlight } from "./structures/Starlight"; 3 | import type English from "./languages/en"; 4 | 5 | declare module "seyfert" { 6 | // eslint-disable-next-line @typescript-eslint/no-empty-object-type 7 | interface UsingClient extends ParseClient {} 8 | // eslint-disable-next-line @typescript-eslint/no-empty-object-type 9 | interface DefaultLocale extends ParseLocales {} 10 | } 11 | 12 | declare global { 13 | // eslint-disable-next-line @typescript-eslint/no-namespace 14 | namespace NodeJS { 15 | interface Process { 16 | noDeprecation: boolean; 17 | } 18 | interface ProcessEnv { 19 | NODE_ENV: "development" | "production"; 20 | PRODUCTION_TOKEN: string; 21 | PRODUCTION_REDIS: string; 22 | DEVELOPMENT_TOKEN: string; 23 | DEVELOPMENT_REDIS: string; 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /src/client/events/client/GuildCreate.ts: -------------------------------------------------------------------------------- 1 | import { createEvent } from "seyfert"; 2 | 3 | export default createEvent({ 4 | data: { once: true, name: "guildCreate" }, 5 | async run(guild, client) { 6 | client.logger.info(`Joined guild ${guild.name} (${guild.id})`); 7 | await client.analytics.trackGuilds(guild, "create"); 8 | const db = await client.prisma.guild.create({ 9 | data: { 10 | id: guild.id, 11 | lang: "en", 12 | name: guild.name, 13 | room: { create: { id: "" } }, 14 | ai: { create: { name: "", channel: "" } }, 15 | }, 16 | select: { 17 | uuid: true, 18 | roomid: true, 19 | id: true, 20 | lang: true, 21 | name: true, 22 | room: { select: { id: true, message: true } }, 23 | ai: { select: { name: true, channel: true } }, 24 | }, 25 | }); 26 | 27 | await client.redis.set(`guild:${client.me.id}:${guild.id}`, JSON.stringify(db)); 28 | return Promise.resolve(); 29 | }, 30 | }); 31 | -------------------------------------------------------------------------------- /src/client/events/client/GuildDelete.ts: -------------------------------------------------------------------------------- 1 | import { IDatabase } from "@/client/interfaces/IDatabase"; 2 | import { createEvent } from "seyfert"; 3 | 4 | export default createEvent({ 5 | data: { once: true, name: "guildCreate" }, 6 | async run(guild, client) { 7 | client.logger.info(`Attempting to delete guild ${guild.name}(${guild.id})`); 8 | await client.analytics.trackGuilds(guild, "delete"); 9 | const databaseString: string | null = await client.redis.get(`guild:${client.me.id}:${guild.id}`); 10 | const database: IDatabase = databaseString 11 | ? (JSON.parse(databaseString) as IDatabase) 12 | : await client.prisma.guild.findFirst({ 13 | where: { 14 | id: guild.id, 15 | }, 16 | select: { 17 | uuid: true, 18 | id: true, 19 | name: true, 20 | roomid: true, 21 | room: { 22 | select: { 23 | id: true, 24 | message: true, 25 | uuid: true, 26 | }, 27 | }, 28 | ai: { 29 | select: { 30 | name: true, 31 | channel: true, 32 | }, 33 | }, 34 | lang: true, 35 | }, 36 | }) as IDatabase; 37 | const existingGuild = await client.prisma.guild.findUnique({ 38 | where: { 39 | uuid: database.uuid, 40 | id: guild.id, 41 | }, 42 | }); 43 | if (existingGuild) { 44 | await client.prisma.guild.delete({ 45 | where: { 46 | uuid: existingGuild.uuid, 47 | id: existingGuild.id, 48 | }, 49 | }); 50 | await client.prisma.room.delete({ 51 | where: { 52 | uuid: database.room.uuid, 53 | id: database.room.id, 54 | }, 55 | }); 56 | return await client.redis 57 | .del(`guild:${client.me.id}:${guild.id}`) 58 | .then(() => { 59 | client.logger.info(`Deleted guild ${guild.name}(${guild.id}) from the database`); 60 | }) 61 | .catch((error: Error) => { 62 | client.logger.error(`Error deleting guild ${guild.name}(${guild.id}): ${error.message}`); 63 | }); 64 | } else { 65 | return client.logger.warn(`Guild ${guild.name}(${guild.id}) not found in the database, skipping deletion.`); 66 | } 67 | }, 68 | }); 69 | -------------------------------------------------------------------------------- /src/client/events/client/botReady.ts: -------------------------------------------------------------------------------- 1 | import { UpdateStatus } from "@/client/structures/utils/Client"; 2 | import { createEvent } from "seyfert"; 3 | 4 | export default createEvent({ 5 | data: { once: true, name: "botReady" }, 6 | async run(user, client) { 7 | const users = () => { 8 | let totalMembers = 0; 9 | for (const guild of client.cache.guilds.values().filter((g) => g.memberCount)) { 10 | totalMembers += guild.memberCount; 11 | } 12 | return totalMembers; 13 | } 14 | client.logger.info(`${user.username} is ready ${(process.memoryUsage().heapTotal / 1024 / 1024).toFixed(2)}MB | Guild: ${client.cache.guilds.count()} | User: ${users()}`); 15 | client.logger.info(`[System] Language Data: ${JSON.stringify(client.langs.values)}`); 16 | await client.analytics.init(); 17 | UpdateStatus(client); 18 | Array.from(client.cache.guilds.values()).forEach(async (guild) => { 19 | const guildData = await client.prisma.guild.findFirst({ 20 | where: { id: guild.id }, 21 | }) 22 | if (!guildData) { 23 | setTimeout(async () => { 24 | await Promise.all([ 25 | client.prisma.guild.create({ 26 | data: { 27 | id: guild.id, 28 | lang: "th", 29 | name: guild.name, 30 | room: { create: { id: "" } }, 31 | ai: { create: { name: "", channel: "" } }, 32 | }, 33 | select: { 34 | uuid: true, 35 | roomid: true, 36 | id: true, 37 | lang: true, 38 | name: true, 39 | room: { select: { id: true, message: true } }, 40 | ai: { select: { name: true, channel: true } }, 41 | }, 42 | }).then((db) => { 43 | client.redis.set(`guild:${client.me.id}:${guild.id}`, JSON.stringify(db)); 44 | client.logger.info(`[System] Created new guild ${guild.name} (${guild.id})`); 45 | }).catch((err) => { 46 | client.logger.error(`[System] Error creating guild ${guild.name} (${guild.id})`, err); 47 | }) 48 | ]) 49 | }, 1000 * 2); 50 | } else { 51 | client.redis.set(`guild:${client.me.id}:${guild.id}`, JSON.stringify(guildData)); 52 | client.logger.info(`[System] Loaded guild ${guild.name} (${guild.id})`); 53 | } 54 | }) 55 | }, 56 | }); 57 | -------------------------------------------------------------------------------- /src/client/events/client/interaction.ts: -------------------------------------------------------------------------------- 1 | import { createEvent } from "seyfert"; 2 | 3 | export default createEvent({ 4 | data: { once: false, name: "interactionCreate" }, 5 | async run(interaction, client) { 6 | // @ts-ignore 7 | await client.analytics.trackInteractions(interaction); 8 | if (interaction.isChatInput()) { 9 | client.logger.info(`[Commands] ${interaction.user.username} (${interaction.user.id}) Command: ${interaction.data.name}`); 10 | } 11 | if (interaction.isAutocomplete()) { 12 | client.logger.info(`[AutoComplete] ${interaction.user.username} (${interaction.user.id}) Data: ${JSON.stringify((interaction.data.options[0] as { value: string }).value)}`); 13 | } 14 | }, 15 | }); 16 | -------------------------------------------------------------------------------- /src/client/events/client/raw.ts: -------------------------------------------------------------------------------- 1 | import type { VoicePacket } from "lithiumx"; 2 | import { createEvent } from "seyfert"; 3 | 4 | export default createEvent({ 5 | data: { once: false, name: "raw" }, 6 | run(data, client) { 7 | return client.lithiumx.updateVoiceState(data as VoicePacket); 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /src/client/index.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from "seyfert"; 2 | import { Starlight } from "./structures/Starlight"; 3 | import { customLogger } from "./structures/utils/Logger"; 4 | 5 | export const client = new Starlight(); 6 | Logger.customize(customLogger); 7 | 8 | process.on("unhandledRejection", (reason, promise) => { 9 | (async () => { 10 | const result = await promise; 11 | client.logger.error(`Unhandled Rejection at: ${JSON.stringify(result)} reason: ${JSON.stringify(reason)}`); 12 | })().catch((err: Error) => { 13 | client.logger.error(`Error in unhandledRejection handler: ${err}`); 14 | }); 15 | }); 16 | 17 | process.on("uncaughtException", (err: Error) => { 18 | client.logger.error(`Uncaught Exception: ${err.message}`); 19 | }); 20 | 21 | process.on("uncaughtExceptionMonitor", (err: Error) => { 22 | client.logger.error(`Uncaught Exception Monitor: ${err.message}`); 23 | }) 24 | 25 | client.start().then(() => { 26 | client.services.watchServices() 27 | .then(() => client.logger.info("Watching services for changes")) 28 | .catch(error => client.logger.error("Failed to watch services:", error)); 29 | client.uploadCommands().then(() => { 30 | client.logger.info("Commands uploaded"); 31 | }).catch((err: Error) => { 32 | client.logger.error(err.message); 33 | }); 34 | }).catch((err) => { 35 | client.logger.error(err); 36 | }); -------------------------------------------------------------------------------- /src/client/interfaces/IDatabase.ts: -------------------------------------------------------------------------------- 1 | import { $Enums } from "@prisma/client"; 2 | 3 | export interface IDatabase { 4 | uuid: string; 5 | id: string; 6 | name: string; 7 | roomid: string; 8 | lang: $Enums.Lang; 9 | room: { 10 | uuid: string; 11 | id: string; 12 | message: string; 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /src/client/languages/en.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | lang: { 3 | already: "The bot is already in this language", 4 | success: "Success", 5 | song: "song", 6 | }, 7 | music: { 8 | stop: "The bot has stopped playing music and left the voice channel.", 9 | volume: (value: number) => `The volume has been changed to ${value}.`, 10 | resume: "Resumed", 11 | skip: "Skipped", 12 | }, 13 | play: { 14 | not_join_voice_channel: "Please enter the voice channel before using this command!", 15 | not_same_voice_channel: "You are not on the same voice channel as the bot!", 16 | search_404: "No results found!", 17 | playlist_author_name: "The playlist was successfully added to the queue.", 18 | track_author_name: "The track has been successfully added to the queue", 19 | added_song: "Added a song", 20 | added_playlist: "Added a playlist", 21 | request: "Request by", 22 | time: "Time", 23 | pause: "Paused", 24 | not_playing: "There is no song currently playing.", 25 | not_found: "The song was not found.", 26 | playlist: "Playlist", 27 | track: "Track", 28 | }, 29 | loop: { 30 | not_playing: "There is no song currently playing.", 31 | specify_type: "Please specify a loop type.", 32 | loop_song: "Song loop has been successfully turnned on.", 33 | loop_queue: "On Queue loop complete", 34 | loop_off: "Loop closed successfully.", 35 | }, 36 | filter: { 37 | specify_filter: "Please specify a filter.", 38 | filter_not_found: "The filter was not found.", 39 | filter_already: "The filter is already enabled.", 40 | filter_cleared: "The filter has been successfully cleared.", 41 | filter_success: (name: string) => `The filter ${name} has been successfully enabled.`, 42 | filter_removed: (name: string) => `The filter ${name} has been successfully removed.`, 43 | } 44 | }; 45 | -------------------------------------------------------------------------------- /src/client/languages/th.ts: -------------------------------------------------------------------------------- 1 | import type English from './en' 2 | 3 | export default { 4 | lang: { 5 | already: "หนูใช้ภาษานี้อยู่แล้วนะ", 6 | success: "สำเร็จ", 7 | song: "เพลง", 8 | }, 9 | music: { 10 | stop: "หนูหยุดเล่นเพลงแล้วนะ", 11 | volume: (value) => `เปลี่ยนระดับเสียงเป็น ${value} แล้วนะ`, 12 | resume: "เล่นต่อแล้วนะ", 13 | skip: "ข้ามเพลงแล้วนะ", 14 | }, 15 | play: { 16 | not_join_voice_channel: "กรุณาเข้าช่องเสียงก่อนใช้คำสั่งนี้นะ", 17 | not_same_voice_channel: "คุณไม่ได้อยู่ห้องเดียวกันกับหนูนะ", 18 | search_404: "ดูเหมือนว่าหนูจะไม่หาเพลงที่คุณต้องการได้นะ", 19 | playlist_author_name: "เพลย์ลิสต์ถูกเพิ่มลงในคิวแล้ว", 20 | track_author_name: "เพลงนี้ถูกเพิ่มลงในคิวแล้ว", 21 | added_song: "เพิ่มเพลงแล้วนะ", 22 | added_playlist: "เพิ่มเพลย์ลิสต์แล้วนะ", 23 | request: "ขอเพลงโดย", 24 | time: "ระยะเวลา", 25 | pause: "หยุดชั่วคราวแล้วนะ", 26 | not_playing: "ไม่มีเพลงที่กําลังเล่นอยู่นะ", 27 | not_found: "ไม่พบเพลงที่คุณต้องการนะ", 28 | playlist: "เพลย์ลิสต์", 29 | track: "เพลง", 30 | }, 31 | loop: { 32 | not_playing: "ไม่มีเพลงที่กําลังเล่นอยู่นะ", 33 | specify_type: "กรุณาระบุประเภทของวนนะ", 34 | loop_song: "วนเพลงแล้วนะ", 35 | loop_queue: "วนคิวแล้วนะ", 36 | loop_off: "ปิดวนรายการแล้วนะ", 37 | }, 38 | filter: { 39 | specify_filter: "กรุณาระบุตัวกรอง", 40 | filter_not_found: "ไม่พบตัวกรองนะ", 41 | filter_already: "ตัวกรองนี้เปิดอยู่แล้วนะ", 42 | filter_cleared: "ล้างตัวกรองแล้วนะ", 43 | filter_success: (name) => `เปิดตัวกรอง ${name} แล้วนะ`, 44 | filter_removed: (name) => `ปิดตัวกรอง ${name} แล้วนะ`, 45 | } 46 | } satisfies typeof English; // inherit types from default lang to ensure 1:1 locales 47 | -------------------------------------------------------------------------------- /src/client/languages/types.ts: -------------------------------------------------------------------------------- 1 | export interface TypePlayTranslations { 2 | not_join_voice_channel: string; 3 | not_same_voice_channel: string; 4 | search_404: string; 5 | playlist_author_name: string; 6 | track_author_name: string; 7 | added_song: string; 8 | added_playlist: string; 9 | request: string; 10 | time: string; 11 | } 12 | -------------------------------------------------------------------------------- /src/client/service/commands/BotinfoCommand.ts: -------------------------------------------------------------------------------- 1 | import { ActionRow, CommandContext, StringSelectMenu, SelectMenuInteraction, UsingClient, Message, Embed } from "seyfert"; 2 | import { IDatabase } from "@/client/interfaces/IDatabase"; 3 | import os from "os"; 4 | import { ServiceExecute } from "@/client/structures/ServiceExecute"; 5 | 6 | const Botinfo: ServiceExecute = { 7 | name: "BotinfoCommand", 8 | type: "commands", 9 | filePath: __filename, 10 | async execute(client: UsingClient, database: IDatabase, interaction: CommandContext) { 11 | try { 12 | const start = performance.now(); 13 | const results = await Promise.all([ 14 | interaction.client.worker.broadcastEval((c) => { 15 | try { 16 | return c.cache.guilds.count() || 0; 17 | } catch (err) { 18 | console.error('Error counting guilds:', err); 19 | return 0; 20 | } 21 | }), 22 | interaction.client.worker.broadcastEval(async (c) => { 23 | try { 24 | return Array.from(await c.cache.guilds.values()) 25 | .reduce((acc, guild) => acc + (guild.memberCount || 0), 0); 26 | } catch (err) { 27 | console.error('Error counting members:', err); 28 | return 0; 29 | } 30 | }), 31 | interaction.client.worker.broadcastEval(() => { 32 | try { 33 | return process.memoryUsage().heapUsed / 1024 / 1024; 34 | } catch (err) { 35 | console.error('Error getting memory usage:', err); 36 | return 0; 37 | } 38 | }), 39 | ]); 40 | const totalGuilds = results[0].reduce((a, b) => (Number(a) || 0) + (Number(b) || 0), 0); 41 | const totalUsers = results[1].reduce((a, b) => (Number(a) || 0) + (Number(b) || 0), 0); 42 | const totalMemory = results[2].reduce((a, b) => (Number(a) || 0) + (Number(b) || 0), 0); 43 | const clusterCount = interaction.client.workerData.shards.length + 1; 44 | const averageMemoryPerCluster = totalMemory / clusterCount; 45 | 46 | const embed = new Embed() 47 | .setTitle("Client Information") 48 | .setColor('Blurple') 49 | .setDescription(`┊ **ID:** \`${interaction.client.me?.id || 'Unknown'}\` 50 | ╰ **Username:** \`${interaction.client.me?.username || 'Unknown'}\` 51 | **Resources**: 52 | ┊ **CPU:** \`${os.cpus()[0].model}\` 53 | ┊ **Memory:** \`${(process.memoryUsage().rss / 1024 / 1024).toFixed(2)} MB / ${(os.totalmem() / 1024 / 1024 / 1024).toFixed(2)} GB\` 54 | ╰ **Avg Memory:** \`(${averageMemoryPerCluster.toFixed(2)} MB) | All Usage: ${totalMemory.toFixed(2)} MB\` 55 | **Size:** 56 | ┊ **Server(s):** \`${totalGuilds}\` 57 | ┊ **Member(s):** \`${totalUsers}\` 58 | ╰ **Ping:** \`${interaction.client.latency}ms\` 59 | **Data:** 60 | ┊ **API:** \`Starlight ${(await import("../../../../package.json")).version}\` 61 | ┊ **Node.js:** \`${process.version}\` 62 | ┊ **LithiumX:** \`v${(await import("../../../../package.json")).dependencies.lithiumx}\` 63 | ╰ **Seyfert:** \`v${(await import("../../../../package.json")).dependencies.seyfert}\` 64 | `) 65 | .setFooter({ 66 | text: `Execution Time: ${Math.round(performance.now() - start)}ms` 67 | }) 68 | 69 | interaction.editResponse({ 70 | embeds: [embed] 71 | }); 72 | return 73 | } catch (error) { 74 | console.error('Error in info command:', error); 75 | interaction.editResponse({ 76 | content: 'An error occurred while fetching bot information.', 77 | embeds: [] 78 | }); 79 | return 80 | } 81 | }, 82 | }; 83 | export default Botinfo; -------------------------------------------------------------------------------- /src/client/service/commands/ClusterCommand.ts: -------------------------------------------------------------------------------- 1 | import { ActionRow, CommandContext, Embed, StringSelectMenu } from 'seyfert'; 2 | import { IDatabase } from './../../interfaces/IDatabase'; 3 | import { UsingClient } from 'seyfert'; 4 | import { ServiceExecute } from "@/client/structures/ServiceExecute"; 5 | 6 | const ClusterCommand: ServiceExecute = { 7 | name: "ClusterCommand", 8 | type: "commands", 9 | filePath: __filename, 10 | async execute(client: UsingClient, database: IDatabase, interaction: CommandContext) { 11 | try { 12 | const start = performance.now(); 13 | const mainEmbed = new Embed() 14 | .setColor("#8e8aff") 15 | .setAuthor({ 16 | name: `${interaction.client.me?.username} Cluster Information ✨`, 17 | iconUrl: interaction.client.me?.avatarURL() || undefined, 18 | }) 19 | .addFields({ 20 | name: "Cluster Information", 21 | value: "Select a page to view cluster information.", 22 | }) 23 | .setFooter({ 24 | text: `Execution Time: ${Math.round(performance.now() - start)}ms`, 25 | }); 26 | 27 | const clusterData = await interaction.client.worker.broadcastEval(async (c) => { 28 | try { 29 | return { 30 | shards: c.workerData.shards, 31 | id: c.workerId, 32 | guilds: (await c.cache.guilds.count()), 33 | users: Array.from(await c.cache.guilds.values()) 34 | .reduce((acc, guild) => acc + (guild.memberCount || 0), 0), 35 | memory: process.memoryUsage().heapUsed, 36 | uptime: c.uptime, 37 | } 38 | } catch (err) { 39 | interaction.client.logger.error('Error Broadcasting:', err); 40 | return 0; 41 | } 42 | }).then((data) => data) as Array<{ 43 | shards: number[], 44 | id: number, 45 | guilds: number, 46 | users: number, 47 | memory: number, 48 | uptime: number 49 | }>; 50 | 51 | const maxClustersPerPage = 12; 52 | const totalPages = Math.ceil(clusterData.length / maxClustersPerPage) + 1; 53 | 54 | const createClusterEmbed = (page: number): Promise => { 55 | const t_start = performance.now(); 56 | if (page === 0) return Promise.resolve(mainEmbed); 57 | 58 | const start = (page - 1) * maxClustersPerPage; 59 | const end = Math.min(start + maxClustersPerPage, clusterData.length); 60 | const clusterInfo = clusterData.slice(start, end); 61 | 62 | const embed = new Embed() 63 | .setColor("#8e8aff") 64 | .setAuthor({ 65 | name: `${interaction.client.me?.username} Cluster Information ✨`, 66 | iconUrl: interaction.client.me?.avatarURL() || undefined, 67 | }); 68 | 69 | clusterInfo.forEach((cluster) => { 70 | embed.addFields({ 71 | name: `<:8891bluestar:1267053991473320069> Cluster: ${cluster.id}`, 72 | value: `\`\`\`autohotkey\nShards: ${cluster.shards.join(', ')} \nGuilds : ${cluster.guilds}\nUsers : ${cluster.users}\nMemory : ${interaction.client.FormatMemory(cluster.memory)}\nUptime: ${(interaction.client.FormatTime(cluster.uptime))} \`\`\``, 73 | inline: true 74 | }); 75 | }); 76 | embed.setFooter({ 77 | text: `Page ${page} / ${totalPages - 1} | Execution Time: ${Math.round(performance.now() - t_start)}ms`, 78 | }); 79 | return Promise.resolve(embed); 80 | }; 81 | const createSelectMenu = () => { 82 | const options = [{ 83 | label: "Main Page", 84 | description: "View the main status page", 85 | value: "0", 86 | }]; 87 | 88 | options.push(...Array.from({ length: totalPages - 1 }, (_, i) => ({ 89 | label: `Page ${i + 1}`, 90 | description: `View cluster information for page ${i + 1}`, 91 | value: `${i + 1}`, 92 | }))); 93 | 94 | return new ActionRow().addComponents( 95 | new StringSelectMenu({ 96 | custom_id: 'clusterPage', 97 | placeholder: 'Select a page', 98 | options: options 99 | }) 100 | ); 101 | }; 102 | const initialClusterEmbed = await createClusterEmbed(0); 103 | const selectMenu = createSelectMenu(); 104 | const message = await interaction.editResponse({ 105 | embeds: [initialClusterEmbed], 106 | components: [selectMenu], 107 | }); 108 | if (!message) return; 109 | const collector = message.createComponentCollector({ 110 | timeout: 60000, 111 | filter: i => i.user.id === interaction.author.id 112 | }); 113 | collector.run('clusterPage', async (interaction) => { 114 | if (!interaction.isStringSelectMenu()) return; 115 | try { 116 | const selectedPage = parseInt(interaction.values[0]); 117 | const updatedClusterEmbed = await createClusterEmbed(selectedPage); 118 | await interaction.update({ 119 | embeds: [updatedClusterEmbed], 120 | components: [selectMenu], 121 | }).catch(() => { }); 122 | } catch (error) { 123 | interaction.client.logger.error(error); 124 | } 125 | }); 126 | } catch (error) { 127 | interaction.client.logger.error(error); 128 | await interaction.editResponse({ 129 | content: 'An error occurred while executing the command.', 130 | components: [], 131 | }).catch(() => { }); 132 | } 133 | } 134 | } 135 | 136 | export default ClusterCommand; -------------------------------------------------------------------------------- /src/client/service/commands/FiltersCommmand.ts: -------------------------------------------------------------------------------- 1 | import { ServiceExecute } from "@/client/structures/ServiceExecute"; 2 | import { UsingClient, CommandContext, Embed } from 'seyfert'; 3 | import { IDatabase } from "@/client/interfaces/IDatabase"; 4 | import { FiltersCommandOptions } from "@/client/commands/music/Filters"; 5 | import { Filters, LithiumXPlayer, } from "lithiumx"; 6 | 7 | const FiltersCommmand: ServiceExecute = { 8 | name: "FiltersCommmand", 9 | type: "commands", 10 | filePath: __filename, 11 | async execute(client: UsingClient, database: IDatabase, interaction: CommandContext): Promise { 12 | const player: LithiumXPlayer = client.lithiumx.players.get(interaction.guildId); 13 | const filter = interaction.options.filter 14 | let mode = interaction.options.mode; 15 | if (typeof mode === "undefined") mode = true; 16 | const t = client.t(database.lang); 17 | const ad = await client.utils.GetAds(database.lang as "en" | "th"); 18 | if (!player) { 19 | interaction.editOrReply({ 20 | embeds: [new Embed().setColor("Red").setDescription(t.loop.not_playing.get())], 21 | }).then().catch(console.error); 22 | return 23 | } 24 | if (!filter) { 25 | interaction.editOrReply({ 26 | embeds: [ 27 | { 28 | color: 0xff0000, 29 | description: t.loop.specify_type.get(), 30 | }, 31 | ], 32 | }).then().catch(console.error); 33 | return 34 | } 35 | const filters = new Filters(player); 36 | if (filter === "clear") { 37 | await filters.clearFilters(); 38 | interaction.editOrReply({ 39 | embeds: [ 40 | new Embed() 41 | .setColor("#a861ff") 42 | .setDescription(t.filter.filter_cleared.get()) 43 | .setImage(ad.image) 44 | .addFields([ 45 | { 46 | name: "Sponsor", 47 | value: ad.title, 48 | inline: false, 49 | }, 50 | ]) 51 | .setTimestamp(), 52 | ], 53 | }).then().catch(console.error); 54 | return 55 | } 56 | switch (mode) { 57 | case true: { 58 | await filters.setFilter(filter, true); 59 | interaction.editOrReply({ 60 | embeds: [ 61 | new Embed() 62 | .setColor("#a861ff") 63 | .setDescription(t.filter.filter_success(filter).get()) 64 | .setImage(ad.image) 65 | .addFields([ 66 | { 67 | name: "Sponsor", 68 | value: ad.title, 69 | inline: false, 70 | }, 71 | ]) 72 | .setTimestamp(), 73 | ], 74 | }).then().catch(console.error); 75 | return 76 | } 77 | case false: { 78 | await filters.setFilter(filter, false); 79 | interaction.editOrReply({ 80 | embeds: [ 81 | new Embed() 82 | .setColor("#a861ff") 83 | .setDescription(t.filter.filter_removed(filter).get()) 84 | .setImage(ad.image) 85 | .addFields([ 86 | { 87 | name: "Sponsor", 88 | value: ad.title, 89 | inline: false, 90 | }, 91 | ]) 92 | .setTimestamp(), 93 | ], 94 | }).then().catch(console.error); 95 | return 96 | } 97 | default: { 98 | interaction.editOrReply({ 99 | embeds: [ 100 | { 101 | color: 0xff0000, 102 | description: t.filter.filter_not_found.get(), 103 | }, 104 | ], 105 | }).then().catch(console.error); 106 | return 107 | } 108 | } 109 | }, 110 | } 111 | 112 | export default FiltersCommmand; -------------------------------------------------------------------------------- /src/client/service/commands/LangCommand.ts: -------------------------------------------------------------------------------- 1 | import { CommandContext, UsingClient } from 'seyfert'; 2 | import { IDatabase } from "@/client/interfaces/IDatabase"; 3 | import { PermissionFlagsBits } from "seyfert/lib/types"; 4 | import { ServiceExecute } from "@/client/structures/ServiceExecute"; 5 | import { LangCommandOptions } from "@/client/commands/admin/lang"; 6 | 7 | const LangCommand: ServiceExecute = { 8 | name: "LangCommand", 9 | type: "commands", 10 | filePath: __filename, 11 | async execute(client: UsingClient, database: IDatabase, interaction: CommandContext) { 12 | const lang = interaction.options.language; 13 | const t = client.t(database.lang); 14 | if (!interaction.guild) return void 0; 15 | if (interaction.member.permissions.has([PermissionFlagsBits.Administrator])) { 16 | if (database.lang === lang) { 17 | return interaction.editOrReply({ 18 | content: `${t.lang.already.get()}`, 19 | }) as unknown as void; 20 | } 21 | const existingGuild = await client.prisma.guild.findFirst({ where: { id: interaction.guildId } }); 22 | if (existingGuild) { 23 | await client.prisma.guild 24 | .update({ 25 | where: { 26 | uuid: database.uuid, 27 | id: interaction.guildId, 28 | }, 29 | data: { 30 | id: interaction.guildId, 31 | lang: lang, 32 | }, 33 | select: { 34 | id: true, 35 | name: true, 36 | lang: true, 37 | room: true, 38 | uuid: true, 39 | roomid: true, 40 | }, 41 | }) 42 | .then(async (data) => { 43 | await client.redis.set(`guild:${client.me.id}:${data.id}`, JSON.stringify(data)); 44 | interaction.editOrReply({ 45 | content: `${t.lang.success.get()}: ${data.lang}`, 46 | }).then().catch(console.error); 47 | return void 0; 48 | }); 49 | } else { 50 | await client.prisma.guild 51 | .create({ 52 | data: { 53 | id: interaction.guildId, 54 | lang: lang, 55 | name: interaction.guild.name, 56 | room: { create: { id: "" } }, 57 | ai: { create: { name: "", channel: "" } }, 58 | }, 59 | select: { 60 | uuid: true, 61 | roomid: true, 62 | lang: true, 63 | id: true, 64 | name: true, 65 | }, 66 | }) 67 | .then(async (data) => { 68 | await client.redis.set(`guild:${client.me.id}:${data.id}`, JSON.stringify(data)); 69 | interaction.editOrReply({ 70 | content: `${t.lang.success.get()}: ${data.lang}`, 71 | }).then().catch(console.error); 72 | return void 0; 73 | }); 74 | } 75 | } 76 | }, 77 | }; 78 | 79 | export default LangCommand; 80 | -------------------------------------------------------------------------------- /src/client/service/commands/LoopCommand.ts: -------------------------------------------------------------------------------- 1 | import { ServiceExecute } from "@/client/structures/ServiceExecute"; 2 | import { IDatabase } from "@/client/interfaces/IDatabase"; 3 | import { CommandContext, Embed, UsingClient } from "seyfert"; 4 | import { LoopCommandOptions } from "@/client/commands/music/loop"; 5 | import config from "@/config"; 6 | 7 | const LoopCommand: ServiceExecute = { 8 | name: "LoopCommand", 9 | type: "commands", 10 | filePath: __filename, 11 | async execute(client: UsingClient, database: IDatabase, interaction: CommandContext): Promise { 12 | try { 13 | const t = client.t(database.lang); 14 | const player = client.lithiumx.players.get(interaction.guildId); 15 | const ad = await client.utils.GetAds(database.lang as "en" | "th"); 16 | if (!player) { 17 | interaction.editOrReply({ 18 | embeds: [new Embed().setColor("Red").setDescription(t.loop.not_playing.get())], 19 | }).then().catch(console.error); 20 | return 21 | } 22 | const type = interaction.options.type; 23 | 24 | if (!type) { 25 | interaction.editOrReply({ 26 | embeds: [ 27 | { 28 | color: 0xff0000, 29 | description: t.loop.specify_type.get(), 30 | }, 31 | ], 32 | }).then().catch(console.error); 33 | return 34 | } 35 | switch (type) { 36 | case "song": { 37 | player.setTrackRepeat(true); 38 | player.setQueueRepeat(false); 39 | interaction.editOrReply({ 40 | embeds: [ 41 | new Embed() 42 | .setColor("#a861ff") 43 | .setDescription(t.loop.loop_song.get()) 44 | .setImage(ad.image) 45 | .addFields([ 46 | { 47 | name: "Sponsor", 48 | value: ad.title, 49 | inline: false, 50 | }, 51 | ]) 52 | .setTimestamp(), 53 | ], 54 | }).then().catch(console.error); 55 | return 56 | } 57 | case "queue": { 58 | player.setTrackRepeat(false); 59 | player.setQueueRepeat(true); 60 | interaction.editOrReply({ 61 | embeds: [ 62 | new Embed() 63 | .setColor("#a861ff") 64 | .setDescription(t.loop.loop_queue.get()) 65 | .setImage(ad.image) 66 | .addFields([ 67 | { 68 | name: "Sponsor", 69 | value: ad.title, 70 | inline: false, 71 | }, 72 | ]) 73 | .toJSON(), 74 | ], 75 | }).then().catch(console.error); 76 | return 77 | } 78 | case "off": { 79 | player.setTrackRepeat(false); 80 | player.setQueueRepeat(false); 81 | interaction.editOrReply({ 82 | embeds: [ 83 | new Embed() 84 | .setColor("#a861ff") 85 | .setDescription(`Loop closed successfully.`) 86 | .setImage(ad.image) 87 | .addFields([ 88 | { 89 | name: "Sponsor", 90 | value: ad.title, 91 | inline: false, 92 | }, 93 | ]) 94 | .toJSON(), 95 | ], 96 | }).then().catch(console.error); 97 | return 98 | } 99 | } 100 | } catch (err) { 101 | console.error(err); 102 | await interaction.editOrReply({ content: (err as Error).message }).then().catch(console.error); 103 | return; 104 | } 105 | } 106 | }; 107 | export default LoopCommand; 108 | -------------------------------------------------------------------------------- /src/client/service/commands/MoveNode.ts: -------------------------------------------------------------------------------- 1 | import { IDatabase } from "@/client/interfaces/IDatabase"; 2 | import { CommandContext, UsingClient } from 'seyfert'; 3 | import { MoveNodeCommandOptions } from "@/client/commands/music/move-node"; 4 | import { ServiceExecute } from "@/client/structures/ServiceExecute"; 5 | 6 | const MoveNode: ServiceExecute = { 7 | name: "MoveNode", 8 | type: "commands", 9 | filePath: __filename, 10 | async execute(client: UsingClient, database: IDatabase, interaction: CommandContext): Promise { 11 | try { 12 | const member = interaction.member; 13 | const t = client.t(database.lang); 14 | const voice = await client.cache.voiceStates?.get(member.id, interaction.guildId)?.channel(); 15 | const bot = client.cache.voiceStates?.get(client.me.id, interaction.guildId); 16 | const player = client.lithiumx.players.get(interaction.guildId); 17 | if (!player) { 18 | await interaction.editOrReply({ 19 | content: `Error: Not Found`, 20 | }); 21 | return; 22 | } 23 | if (!voice?.is(["GuildVoice", "GuildStageVoice"])) { 24 | await interaction.editOrReply({ 25 | embeds: [ 26 | { 27 | color: 0xff0000, 28 | description: t.play.not_join_voice_channel.get(), 29 | }, 30 | ], 31 | }); 32 | return; 33 | } 34 | if (bot && bot.channelId !== voice.id) { 35 | interaction.editOrReply({ 36 | embeds: [ 37 | { 38 | color: 0xff0000, 39 | description: t.play.not_same_voice_channel.get(), 40 | }, 41 | ], 42 | }).then().catch(console.error); 43 | return; 44 | } 45 | await player.moveNode(interaction.options["node"]).then().catch(console.error); 46 | interaction.editOrReply({ 47 | content: t.lang.success.get(), 48 | }).then().catch(console.error); 49 | return; 50 | } catch (error) { 51 | interaction.editOrReply({ content: (error as Error).message }).then().catch(console.error); 52 | return 53 | } 54 | }, 55 | } 56 | export default MoveNode; -------------------------------------------------------------------------------- /src/client/service/commands/MusicPlay.ts: -------------------------------------------------------------------------------- 1 | import { ActionRow, Button, CommandContext, UsingClient } from 'seyfert'; 2 | import { PlayCommandOptions } from "@/client/commands/music/play"; 3 | import { IDatabase } from "@/client/interfaces/IDatabase"; 4 | import { ServiceExecute } from "@/client/structures/ServiceExecute"; 5 | import { ButtonStyle } from 'seyfert/lib/types'; 6 | 7 | const MusicPlay: ServiceExecute = { 8 | name: "MusicPlay", 9 | type: "commands", 10 | filePath: __filename, 11 | async execute(client: UsingClient, database: IDatabase, interaction: CommandContext): Promise { 12 | // Define buildActionRow as a local function 13 | function buildActionRow(label1: string, url1: string, emoji1: string, label2: string, url2: string, emoji2: string): ActionRow