├── .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 | []()
6 | [](https://github.com/EvarinDev/Starlight/issues)
7 | [](https://github.com/EvarinDev/Starlight/pulls)
8 | [](/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 | [](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