├── .forgejo └── workflows │ ├── def.yml │ ├── label.yml │ └── stale.yml ├── .gitea ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md ├── SUPPORT.md └── labeler.yml ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── repo-lockdown.yml ├── .gitignore ├── .woodpecker └── lint.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.md ├── Makefile ├── README.md ├── core ├── auto-clean.png ├── auto-queue.png ├── context-help.png ├── help-context.png ├── main-help.png ├── queue-and-search.png └── stats.png ├── dashboard ├── .env.example ├── .eslintrc.json ├── .gitignore ├── .prettierrc.json ├── README.md ├── kbdsrct.md ├── next-env.d.ts ├── next.config.js ├── package-lock.json ├── package.json ├── src │ ├── assets │ │ ├── icons │ │ │ ├── AudiotrackRounded.svg │ │ │ ├── DnsRounded.svg │ │ │ ├── PersonRounded.svg │ │ │ ├── RocketLaunchRounded.svg │ │ │ ├── caret-outline-left.svg │ │ │ ├── caret-outline-right.svg │ │ │ ├── config-button.png │ │ │ ├── drag-handle.svg │ │ │ ├── modal-close.svg │ │ │ ├── navbar-icon.svg │ │ │ ├── next.svg │ │ │ ├── pause-2.svg │ │ │ ├── pause.svg │ │ │ ├── play-button.png │ │ │ ├── play.svg │ │ │ ├── trash.svg │ │ │ └── x-solid.svg │ │ └── images │ │ │ └── sample-thumbnail.png │ ├── components │ │ ├── Modal.tsx │ │ ├── ModalShortcut.tsx │ │ ├── PlaylistBar.tsx │ │ ├── ProcessData.tsx │ │ ├── StatCard.tsx │ │ ├── containers.tsx │ │ ├── content.tsx │ │ ├── navbar.tsx │ │ ├── server.tsx │ │ └── shortcuts │ │ │ ├── Key.tsx │ │ │ └── ShortcutEntry.tsx │ ├── configs │ │ └── constants.ts │ ├── hooks │ │ ├── useAbortDelay.ts │ │ ├── useAuthGuard.ts │ │ ├── useProcessData.ts │ │ ├── useSharedStateGetter.ts │ │ └── useSharedStateSetter.ts │ ├── interfaces │ │ ├── api.ts │ │ ├── components │ │ │ ├── Modal.ts │ │ │ ├── Navbar.ts │ │ │ ├── PlaylistBar.ts │ │ │ └── Shortcuts.ts │ │ ├── globalState.ts │ │ ├── guild.ts │ │ ├── image.ts │ │ ├── kbdsrct.ts │ │ ├── layouts.ts │ │ ├── playerState.ts │ │ ├── sharedState.ts │ │ ├── user.ts │ │ ├── ws.ts │ │ └── wsShared.ts │ ├── layouts │ │ ├── AppLayout.tsx │ │ └── DashboardLayout.tsx │ ├── libs │ │ ├── kbdsrct.ts │ │ ├── queryClient.ts │ │ ├── sharedState.ts │ │ └── sockets │ │ │ ├── index.ts │ │ │ └── player │ │ │ ├── emit.ts │ │ │ ├── index.ts │ │ │ └── states.ts │ ├── pages │ │ ├── _app.tsx │ │ ├── _document.tsx │ │ ├── dashboard │ │ │ └── index.tsx │ │ ├── global.scss │ │ ├── index.tsx │ │ ├── invite │ │ │ └── index.tsx │ │ ├── login │ │ │ └── index.tsx │ │ ├── logout │ │ │ └── index.tsx │ │ └── servers │ │ │ ├── [id] │ │ │ ├── index.tsx │ │ │ └── player.tsx │ │ │ └── index.tsx │ ├── services │ │ └── api.ts │ ├── sharedStates │ │ ├── globalState.ts │ │ └── playerState.ts │ └── utils │ │ ├── common.ts │ │ ├── dashboard.ts │ │ ├── data.ts │ │ ├── formatting.ts │ │ ├── image.ts │ │ ├── localStorage.ts │ │ ├── query.ts │ │ └── wsShared.ts └── tsconfig.json ├── dc.sh ├── delightful-contributors.md ├── djs-bot ├── .env.example ├── .prettierrc.json ├── .vimspector.json ├── README.md ├── api │ ├── v0 │ │ ├── index.js │ │ └── routes │ │ │ ├── commands.js │ │ │ ├── dashboard.js │ │ │ ├── servers.js │ │ │ └── test.js │ └── v1 │ │ ├── .gitignore │ │ ├── .prettierrc.json │ │ ├── src │ │ ├── index.ts │ │ ├── interfaces │ │ │ ├── common.ts │ │ │ ├── discord.ts │ │ │ ├── log.ts │ │ │ ├── user.ts │ │ │ ├── ws.ts │ │ │ └── wsShared.ts │ │ ├── lib │ │ │ ├── APICache.ts │ │ │ ├── APIError.ts │ │ │ ├── constants.ts │ │ │ ├── db.ts │ │ │ └── jwt.ts │ │ ├── routes │ │ │ └── v1 │ │ │ │ ├── errorHandler │ │ │ │ └── index.ts │ │ │ │ ├── index.ts │ │ │ │ └── routesHandler │ │ │ │ ├── commands.ts │ │ │ │ ├── dashboard.ts │ │ │ │ ├── invite.ts │ │ │ │ ├── login.ts │ │ │ │ └── servers.ts │ │ ├── services │ │ │ └── discord.ts │ │ ├── standalone.ts │ │ ├── utils │ │ │ ├── common.ts │ │ │ ├── log.ts │ │ │ ├── player.ts │ │ │ ├── reply.ts │ │ │ ├── ws.ts │ │ │ └── wsShared.ts │ │ └── ws │ │ │ ├── eventsHandler │ │ │ ├── index.ts │ │ │ ├── pause.ts │ │ │ ├── progressUpdate.ts │ │ │ ├── queueUpdate.ts │ │ │ ├── stop.ts │ │ │ └── trackStart.ts │ │ │ ├── index.ts │ │ │ ├── messageHandler │ │ │ ├── handlers.ts │ │ │ └── index.ts │ │ │ └── openHandler │ │ │ └── index.ts │ │ └── tsconfig.json ├── assets │ ├── .gitkeep │ ├── Embed.txt │ ├── no_bg.png │ └── playlists.js.not ├── bot.js ├── commands │ ├── misc │ │ ├── guildleave.js │ │ ├── help.js │ │ └── invite.js │ ├── music │ │ ├── 247.js │ │ ├── autoleave.js │ │ ├── autopause.js │ │ ├── autoqueue.js │ │ ├── clean.js │ │ ├── clear.js │ │ ├── filters.js │ │ ├── history.js │ │ ├── loop.js │ │ ├── loopq.js │ │ ├── lyrics.js │ │ ├── move.js │ │ ├── nowplaying.js │ │ ├── pause.js │ │ ├── play.js │ │ ├── playlists │ │ │ ├── add.js │ │ │ ├── create.js │ │ │ ├── delete.js │ │ │ ├── index.js │ │ │ ├── play.js │ │ │ ├── remove.js │ │ │ └── view.js │ │ ├── previous.js │ │ ├── queue.js │ │ ├── remove.js │ │ ├── replay.js │ │ ├── resume.js │ │ ├── save.js │ │ ├── search.js │ │ ├── seek.js │ │ ├── shuffle.js │ │ ├── skip.js │ │ ├── skipto.js │ │ ├── stop.js │ │ ├── summon.js │ │ └── volume.js │ └── utility │ │ ├── config │ │ ├── control-channel.js │ │ ├── dj-role.js │ │ └── index.js │ │ ├── nodes.js │ │ ├── ping.js │ │ ├── reload.js │ │ ├── stats.js │ │ └── userinfo.js ├── config.js ├── index.js ├── interactions │ ├── autoqueue.js │ ├── next.js │ ├── playpause.js │ ├── prev.js │ ├── shuffle.js │ ├── stop.js │ ├── vlouder.js │ └── vlower.js ├── lib │ ├── Bot.d.ts │ ├── Bot.js │ ├── DBMS.js │ ├── Logger.js │ ├── MusicEvents.d.ts │ ├── MusicEvents.js │ ├── MusicManager.js │ ├── SlashCommand.d.ts │ ├── SlashCommand.js │ └── clients │ │ ├── Cosmicord.js │ │ ├── Erela.js │ │ ├── MusicClient.d.ts │ │ └── Shoukaku.js.not ├── loaders │ ├── events │ │ ├── interactionCreate.js │ │ ├── messageCreate.js │ │ ├── messageDelete.js │ │ ├── messageDeleteBulk.js │ │ ├── raw.js │ │ ├── ready.js │ │ └── voiceStateUpdate.js │ └── schedules │ │ └── rmLogs.js ├── package-lock.json ├── package.json ├── prisma │ ├── mongodb.prisma │ └── postgresql.prisma ├── scripts │ ├── DBScript.js │ ├── destroy.js │ ├── global.js │ ├── guild.js │ └── update.js └── util │ ├── commands.d.ts │ ├── commands.js │ ├── common.js │ ├── controlChannel.js │ ├── controlChannelEvents.js │ ├── dates.js │ ├── debug.js │ ├── embeds.js │ ├── getChannel.js │ ├── getConfig.js │ ├── getDirs.js │ ├── getLavalink.js │ ├── interactions.js │ ├── message.d.ts │ ├── message.js │ ├── musicManager.js │ ├── player.d.ts │ ├── player.js │ └── string.js └── docker ├── .gitignore ├── djs-bot └── Dockerfile ├── docker-compose.yml ├── docker-compose_traefik.yml ├── lavalink.env.example └── lavalink ├── application.yml └── standalone.sh /.forgejo/workflows/def.yml: -------------------------------------------------------------------------------- 1 | on: [push] 2 | jobs: 3 | test: 4 | runs-on: ubuntu-latest 5 | steps: 6 | - run: echo All Good -------------------------------------------------------------------------------- /.forgejo/workflows/label.yml: -------------------------------------------------------------------------------- 1 | # This workflow will triage pull requests and apply a label based on the 2 | # paths that are modified in the pull request. 3 | # 4 | # To use this workflow, you will need to set up a .github/labeler.yml 5 | # file with configuration. For more information, see: 6 | # https://github.com/actions/labeler 7 | 8 | name: Labeler 9 | on: [pull_request_target] 10 | 11 | jobs: 12 | label: 13 | 14 | runs-on: ubuntu-latest 15 | permissions: 16 | contents: read 17 | pull-requests: write 18 | 19 | steps: 20 | - uses: actions/labeler@v4 21 | with: 22 | repo-token: "${{ secrets.DPASTE_TOKEN }}" 23 | -------------------------------------------------------------------------------- /.forgejo/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | # This workflow warns and then closes issues and PRs that have had no activity for a specified amount of time. 2 | # 3 | # You can adjust the behavior by modifying this file. 4 | # For more information, see: 5 | # https://github.com/actions/stale 6 | name: Stale 7 | 8 | on: 9 | schedule: 10 | - cron: '35 15 * * *' 11 | 12 | jobs: 13 | stale: 14 | 15 | runs-on: ubuntu-latest 16 | permissions: 17 | issues: write 18 | pull-requests: write 19 | 20 | steps: 21 | - uses: actions/stale@v5 22 | with: 23 | repo-token: ${{ secrets.DPASTE_TOKEN }} 24 | stale-issue-message: 'This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.' 25 | stale-pr-message: 'This PR has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.' 26 | stale-issue-label: 'no-issue-activity' 27 | stale-pr-label: 'no-pr-activity' 28 | -------------------------------------------------------------------------------- /.gitea/CODEOWNERS: -------------------------------------------------------------------------------- 1 | @SudhanPlayz @DarrenOfficial 2 | -------------------------------------------------------------------------------- /.gitea/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Report incorrect or unexpected behavior of the Music Bot 4 | title: "" 5 | labels: "s: unverified, type: bug" 6 | assignees: "" 7 | --- 8 | 9 | 10 | 11 | ## Please describe the problem you are having in as much detail as possible: 12 | 13 | ## Include a reproducible code sample here, if possible, the snippet of code which is throwing the error or which you presume is giving issues: 14 | 15 | ```js 16 | // Place your code here 17 | ``` 18 | ## If you have your own setup, without the original docker configuration, please provide: 19 | 20 | ## Further details: 21 | 22 | - discord.js version: 23 | - Node.js version: 24 | - Operating system: 25 | - Priority this issue should have – please be realistic and elaborate if possible: 26 | 27 | ## Relevant client options: 28 | 29 | - partials: none 30 | - gateway intents: none 31 | - other: none 32 | 33 | 38 | -------------------------------------------------------------------------------- /.gitea/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Discord server 4 | url: https://discord.gg/sbySMS7m3v 5 | about: Please visit our Discord server for questions and support requests. 6 | -------------------------------------------------------------------------------- /.gitea/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Request a feature for the Music Bot 4 | title: "" 5 | labels: "type: enhancement" 6 | assignees: "" 7 | --- 8 | 9 | 10 | 11 | ## Is your feature request related to a problem? Please describe. 12 | 13 | A clear and concise description of what the problem is. Eg. I'm always frustrated when [...] 14 | 15 | ## Describe the ideal solution 16 | 17 | A clear and concise description of what you want to happen. 18 | 19 | ## Describe alternatives you've considered 20 | 21 | A clear and concise description of any alternative solutions or features you've considered. 22 | 23 | ## Additional context 24 | 25 | Add any other context or screenshots about the feature request here. 26 | -------------------------------------------------------------------------------- /.gitea/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Please describe the changes this PR makes and why it should be merged: 2 | 3 | ## Status and versioning classification: 4 | 5 | 13 | 14 | # Important. 15 | 16 | - Write in camelCase, not snake_case. 17 | - Do not push to master/main without testing your changes first, make a branch 18 | if you have to. -------------------------------------------------------------------------------- /.gitea/SUPPORT.md: -------------------------------------------------------------------------------- 1 | # Seeking support? 2 | 3 | We only use this issue tracker for bug reports and feature request. We are not able to provide general support or answer 4 | questions in the form of GitHub issues. 5 | 6 | For general questions about the Music Bot and use please use the dedicated support channels in our Discord 7 | server: https://discord.gg/sbySMS7m3v 8 | 9 | Any issues that don't directly involve a bug or a feature request will likely be closed and redirected. 10 | -------------------------------------------------------------------------------- /.gitea/labeler.yml: -------------------------------------------------------------------------------- 1 | djs-bot: 2 | - 'djs-bot/**' 3 | 4 | dashboard: 5 | - 'dashboard/**' 6 | 7 | context: 8 | - 'docker/**' 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Report incorrect or unexpected behavior of the Music Bot 4 | title: "" 5 | labels: "s: unverified, type: bug" 6 | assignees: "" 7 | --- 8 | 9 | 10 | 11 | 12 | ## Please describe the problem you are having in as much detail as possible: 13 | 14 | ## Include a reproducible code sample here, if possible, the snippet of code which is throwing the error or which you presume is giving issues: 15 | 16 | ```js 17 | // Place your code here 18 | ``` 19 | ## If you have your own setup, without the original docker configuration, please provide: 20 | 21 | ## Further details: 22 | 23 | - discord.js version: 24 | - Node.js version: 25 | - Docker version: 26 | - Operating system: 27 | - Priority this issue should have – please be realistic and elaborate if possible: 28 | 29 | ## Relevant client options: 30 | 31 | - partials: none 32 | - gateway intents: none 33 | - other: none 34 | 35 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Request a feature for the Music Bot 4 | title: "" 5 | labels: "type: enhancement" 6 | assignees: "" 7 | --- 8 | 9 | 10 | 11 | ## Is your feature request related to a problem? Please describe. 12 | 13 | A clear and concise description of what the problem is. Eg. I'm always frustrated when [...] 14 | 15 | ## Describe the ideal solution 16 | 17 | A clear and concise description of what you want to happen. 18 | 19 | ## Describe alternatives you've considered 20 | 21 | A clear and concise description of any alternative solutions or features you've considered. 22 | 23 | ## Additional context 24 | 25 | Add any other context or screenshots about the feature request here. -------------------------------------------------------------------------------- /.github/workflows/repo-lockdown.yml: -------------------------------------------------------------------------------- 1 | name: 'Repo Lockdown' 2 | 3 | on: 4 | pull_request_target: 5 | types: opened 6 | 7 | permissions: 8 | pull-requests: write 9 | 10 | jobs: 11 | action: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: dessant/repo-lockdown@v4 15 | with: 16 | pr-comment: > 17 | This repository does not accept pull requests, 18 | see the [README](https://git.dpaste.org/dmb/Discord-MusicBot/src/branch/repo-lockdown/#contributors) for details. 19 | skip-closed-pr-comment: true -------------------------------------------------------------------------------- /.woodpecker/lint.yml: -------------------------------------------------------------------------------- 1 | when: 2 | - event: [pull_request, tag, cron] 3 | - event: push 4 | branch: 5 | - develop 6 | - master 7 | - renovate/* 8 | 9 | steps: 10 | lint-dockerfile: 11 | image: hadolint/hadolint:v2.12.1-beta-alpine 12 | commands: 13 | - hadolint Dockerfile 14 | when: 15 | path: "Dockerfile" 16 | 17 | lint-shell: 18 | image: koalaman/shellcheck-alpine:latest # not maintained semver version as of 2023-10 19 | group: lint-build 20 | commands: 21 | - shellcheck *.sh 22 | when: 23 | path: "*.sh" 24 | 25 | lint-markdown: 26 | image: davidanson/markdownlint-cli2:v0.12.1 27 | commands: 28 | - "markdownlint-cli2 *.{md,markdown}" 29 | when: 30 | path: "*.{md,markdown}" 31 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # License for Discord-MusicBot 2 | 3 | - The **credits** should not be changed. 4 | - The **bot/code** should be used for **private hosting** and **personal usage** only. 5 | - Using the code for **public usage** is **not allowed**. 6 | - The code should not be used for **commercial purposes**. 7 | - The code should not be used for gains of **monetary benefits**. 8 | - Selling the code as **your** own is **not allowed**. 9 | 10 | > **Note:** if you are found to be violating any of the above stated rule you might be asked to take down your bot, happy listening!! Incase of any doubts in the license contact owner. 11 | 12 | # Definitions 13 | 14 | - **You** - The user of the bot. 15 | - **Bot** - The Discord-MusicBot. 16 | - **Code** - The code of the bot. 17 | - **Host** - The person/company/service hosting and providing for the resources of the bot. 18 | - **Credits** - The credits to the author(s) of the bot. 19 | - **Private Hosting** - In terms that the bot's code is not available to the public. 20 | - The bot's running code should reside in a private repository, VPS, or any other form of private storage. 21 | - **Personal Usage** - i.e. only for personal servers, not discoverable on the discord server list (or online promotion services like disboard). 22 | - **Commercial Purposes** - means the carriage of persons or property for any fare, fee, rate, charge or other consideration, or directly or indirectly in connection with any business, or other undertaking intended for profit. 23 | - **Monetary Benefits** - financial incentives, including but not limited to, donations, payments, etc. 24 | 25 | # Liability 26 | 27 | - The bot is provided as is, and the host is not liable for any damages that may occur from the use of the bot. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # This file is basically a wrapper for the dc.sh script. 2 | # It is used to make the docker-compose commands easier to use. 3 | # Basically acting as an alias for the dc.sh script. 4 | 5 | all: rebuild 6 | 7 | 8 | down: 9 | @./dc.sh down 10 | 11 | log: 12 | @./dc.sh log 13 | 14 | purge: 15 | @./dc.sh purge 16 | 17 | rebuild: 18 | @./dc.sh rebuild $(filter-out $@,$(MAKECMDGOALS)) 19 | 20 | enter: 21 | @./dc.sh enter $(filter-out $@,$(MAKECMDGOALS)) 22 | 23 | up: 24 | @./dc.sh up $(filter-out $@,$(MAKECMDGOALS)) 25 | 26 | lite: 27 | @./dc.sh lite $(filter-out $@,$(MAKECMDGOALS)) 28 | 29 | del: 30 | @./dc.sh del $(filter-out $@,$(MAKECMDGOALS)) 31 | 32 | %: 33 | @./dc.sh $@ 34 | -------------------------------------------------------------------------------- /core/auto-clean.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wtfnotavailable/Discord-MusicBot/b44d6baf2e005a682efc238e5dbb4864d2e82857/core/auto-clean.png -------------------------------------------------------------------------------- /core/auto-queue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wtfnotavailable/Discord-MusicBot/b44d6baf2e005a682efc238e5dbb4864d2e82857/core/auto-queue.png -------------------------------------------------------------------------------- /core/context-help.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wtfnotavailable/Discord-MusicBot/b44d6baf2e005a682efc238e5dbb4864d2e82857/core/context-help.png -------------------------------------------------------------------------------- /core/help-context.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wtfnotavailable/Discord-MusicBot/b44d6baf2e005a682efc238e5dbb4864d2e82857/core/help-context.png -------------------------------------------------------------------------------- /core/main-help.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wtfnotavailable/Discord-MusicBot/b44d6baf2e005a682efc238e5dbb4864d2e82857/core/main-help.png -------------------------------------------------------------------------------- /core/queue-and-search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wtfnotavailable/Discord-MusicBot/b44d6baf2e005a682efc238e5dbb4864d2e82857/core/queue-and-search.png -------------------------------------------------------------------------------- /core/stats.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wtfnotavailable/Discord-MusicBot/b44d6baf2e005a682efc238e5dbb4864d2e82857/core/stats.png -------------------------------------------------------------------------------- /dashboard/.env.example: -------------------------------------------------------------------------------- 1 | # if you ever want to host it elsewhere separated from your bot 2 | 3 | # api options 4 | NEXT_PUBLIC_API_URL=www.somedomain.com 5 | NEXT_PUBLIC_API_VERSION=v1 6 | NEXT_PUBLIC_API_SECURE_PROTOCOL=true 7 | 8 | # ws options 9 | NEXT_PUBLIC_WS_URL=sub.somedomain.com 10 | NEXT_PUBLIC_WS_VERSION=v1 11 | NEXT_PUBLIC_WS_SECURE_PROTOCOL=true 12 | -------------------------------------------------------------------------------- /dashboard/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /dashboard/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | 14 | # production 15 | /build 16 | 17 | # misc 18 | .DS_Store 19 | *.pem 20 | 21 | # debug 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | .pnpm-debug.log* 26 | 27 | # local env files 28 | .env*.local 29 | 30 | # vercel 31 | .vercel 32 | -------------------------------------------------------------------------------- /dashboard/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "singleQuote": true, 4 | "printWidth": 80, 5 | "tabWidth": 4, 6 | "endOfLine": "auto" 7 | } 8 | -------------------------------------------------------------------------------- /dashboard/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped 2 | with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 3 | 4 | ## Getting Started 5 | 6 | First, run the development server: 7 | 8 | ```bash 9 | npm run dev 10 | # or 11 | yarn dev 12 | ``` 13 | 14 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 15 | 16 | You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file. 17 | 18 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed 19 | on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited 20 | in `pages/api/hello.js`. 21 | 22 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated 23 | as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 24 | 25 | ## Learn More 26 | 27 | To learn more about Next.js, take a look at the following resources: 28 | 29 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 30 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 31 | 32 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions 33 | are welcome! 34 | 35 | ## Deploy on Vercel 36 | 37 | The easiest way to deploy your Next.js app is to use 38 | the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) 39 | from the creators of Next.js. 40 | 41 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 42 | -------------------------------------------------------------------------------- /dashboard/kbdsrct.md: -------------------------------------------------------------------------------- 1 | # Spotify Shortcuts 2 | 3 | For reference, only a few are gonna be implemented: 4 | 5 | 6 | #### Basic 7 | 8 | - Create new playlist Alt+Shift+P 9 | - Create new folder Ctrl+Alt+Shift+P 10 | - Open context menu Alt+J 11 | - Open Quick Search Ctrl+K 12 | - Search in Your Library Shift+Ctrl+Alt+F 13 | - Log out Alt+Shift+F6 14 | 15 | 16 | #### Playback 17 | 18 | - Play / Pause Space 19 | - Like Alt+Shift+B 20 | - Shuffle Alt+S 21 | - Repeat Alt+R 22 | - Skip to previous Alt+← 23 | - Skip to next Alt+→ 24 | - Seek backward Shift+← 25 | - Seek forward Shift+→ 26 | - Raise volume Alt+↑ 27 | - Lower volume Alt+↓ 28 | 29 | 30 | #### Navigation 31 | 32 | - Home Alt+Shift+H 33 | - Back in history Ctrl+← 34 | - Forward in history Ctrl+→ 35 | - Currently playing Alt+Shift+J 36 | - Search Ctrl+Shift+L 37 | - Liked songs Alt+Shift+S 38 | - Queue Alt+Shift+Q 39 | - Your Library Alt+Shift+0 40 | - Your playlists Alt+Shift+1 41 | - Your podcasts Alt+Shift+2 42 | - Your artists Alt+Shift+3 43 | - Your albums Alt+Shift+4 44 | - Made for you Alt+Shift+M 45 | - New Releases Alt+Shift+N 46 | - Charts Alt+Shift+C 47 | 48 | 49 | #### Layout 50 | 51 | - Toggle left sidebar Alt+Shift+L 52 | - Decrease navigation bar width Alt+Shift+← 53 | - Increase navigation bar width Alt+Shift+→ 54 | - Toggle right sidebar Alt+Shift+R 55 | - Decrease activity tab width Alt+Shift+↓ 56 | - Increase activity tab width Alt+Shift+↑ 57 | -------------------------------------------------------------------------------- /dashboard/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /dashboard/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | webpack(config) { 5 | config.module.rules.push({ 6 | test: /\.svg$/i, 7 | issuer: /\.[jt]sx?$/, 8 | use: ['@svgr/webpack'], 9 | }); 10 | 11 | return config; 12 | }, 13 | images: { 14 | domains: ['img.youtube.com'], 15 | }, 16 | }; 17 | 18 | module.exports = nextConfig; 19 | -------------------------------------------------------------------------------- /dashboard/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dashboard", 3 | "version": "0.3.0-alpha", 4 | "private": true, 5 | "scripts": { 6 | "dev": "npm ci && next dev", 7 | "lint": "npm ci && next lint", 8 | "build": "npm ci && next build", 9 | "start": "npm ci && next start", 10 | "export": "npm ci && next export", 11 | "build-and-start": "npm ci && next build && next start" 12 | }, 13 | "dependencies": { 14 | "@emotion/react": "^11.11.1", 15 | "@emotion/styled": "^11.11.0", 16 | "@mui/icons-material": "^5.11.16", 17 | "@mui/material": "^5.13.6", 18 | "@nextui-org/react": "1.0.0-beta.13", 19 | "@tanstack/react-query": "^4.32.0", 20 | "axios": "^1.4.0", 21 | "classnames": "^2.3.2", 22 | "next": "13.5.6", 23 | "react": "18.2.0", 24 | "react-dom": "18.2.0", 25 | "sass": "^1.68.0", 26 | "sharp": "^0.32.6" 27 | }, 28 | "devDependencies": { 29 | "@svgr/webpack": "^8.1.0", 30 | "@types/node": "20.8.9", 31 | "@types/react": "18.2.33", 32 | "eslint": "8.52.0", 33 | "eslint-config-next": "13.5.6", 34 | "typescript": "5.2.2" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /dashboard/src/assets/icons/AudiotrackRounded.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dashboard/src/assets/icons/DnsRounded.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dashboard/src/assets/icons/PersonRounded.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dashboard/src/assets/icons/RocketLaunchRounded.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dashboard/src/assets/icons/caret-outline-left.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dashboard/src/assets/icons/caret-outline-right.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dashboard/src/assets/icons/config-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wtfnotavailable/Discord-MusicBot/b44d6baf2e005a682efc238e5dbb4864d2e82857/dashboard/src/assets/icons/config-button.png -------------------------------------------------------------------------------- /dashboard/src/assets/icons/drag-handle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /dashboard/src/assets/icons/modal-close.svg: -------------------------------------------------------------------------------- 1 | Close 2 | -------------------------------------------------------------------------------- /dashboard/src/assets/icons/navbar-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /dashboard/src/assets/icons/next.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /dashboard/src/assets/icons/pause-2.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dashboard/src/assets/icons/pause.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /dashboard/src/assets/icons/play-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wtfnotavailable/Discord-MusicBot/b44d6baf2e005a682efc238e5dbb4864d2e82857/dashboard/src/assets/icons/play-button.png -------------------------------------------------------------------------------- /dashboard/src/assets/icons/play.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dashboard/src/assets/icons/trash.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dashboard/src/assets/icons/x-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dashboard/src/assets/images/sample-thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wtfnotavailable/Discord-MusicBot/b44d6baf2e005a682efc238e5dbb4864d2e82857/dashboard/src/assets/images/sample-thumbnail.png -------------------------------------------------------------------------------- /dashboard/src/components/Modal.tsx: -------------------------------------------------------------------------------- 1 | import { IModalProps } from '@/interfaces/components/Modal'; 2 | 3 | const containerStyle: React.HTMLAttributes['style'] = { 4 | position: 'fixed', 5 | width: '100vw', 6 | height: '100vh', 7 | top: 0, 8 | left: 0, 9 | display: 'flex', 10 | justifyContent: 'center', 11 | zIndex: 500, 12 | }; 13 | 14 | export default function Modal({ children, fullHeight, open }: IModalProps) { 15 | if (!open) return null; 16 | 17 | const cpyStyles = { ...containerStyle }; 18 | 19 | if (!fullHeight) cpyStyles.alignItems = 'center'; 20 | 21 | return
{children}
; 22 | } 23 | -------------------------------------------------------------------------------- /dashboard/src/components/ProcessData.tsx: -------------------------------------------------------------------------------- 1 | import { useProcessData } from '@/hooks/useProcessData'; 2 | import { IBaseApiResponse, IUseProcessDataOptions } from '@/interfaces/api'; 3 | 4 | interface IProcessDataProps extends IUseProcessDataOptions { 5 | data: IBaseApiResponse | undefined; 6 | isLoading: boolean; 7 | children: React.ReactNode; 8 | } 9 | 10 | export default function ProcessData({ 11 | data, 12 | isLoading, 13 | children, 14 | ...props 15 | }: IProcessDataProps) { 16 | const processData = useProcessData(data, isLoading, { 17 | ...props, 18 | }); 19 | 20 | return processData(children); 21 | } 22 | -------------------------------------------------------------------------------- /dashboard/src/components/StatCard.tsx: -------------------------------------------------------------------------------- 1 | import {Card, Text} from "@nextui-org/react"; 2 | import {ReactNode} from "react"; 3 | 4 | export default function StatCard(props: { 5 | title: string; 6 | amount: number | string; 7 | icon: ReactNode; 8 | }) { 9 | return ( 10 | 11 | 12 |
13 | { props.title } 14 | { props.amount } 15 |
16 | { props.icon } 17 |
18 |
19 | ) 20 | } -------------------------------------------------------------------------------- /dashboard/src/components/containers.tsx: -------------------------------------------------------------------------------- 1 | import { Card, Container, CSS } from "@nextui-org/react"; 2 | 3 | interface IHalfContainerProps { 4 | children: React.ReactNode; 5 | containerProps?: CSS; 6 | } 7 | 8 | export function HalfContainer({ children, containerProps = {} }: IHalfContainerProps) { 9 | return ( 10 | 21 | {children} 22 | 23 | ); 24 | } 25 | 26 | export function HalfContainerCard({ children }: IHalfContainerProps) { 27 | return ( 28 |
36 | 43 | {children} 44 | 45 |
46 | ); 47 | } -------------------------------------------------------------------------------- /dashboard/src/components/content.tsx: -------------------------------------------------------------------------------- 1 | import {PropsWithChildren} from "react"; 2 | import Navbar from "./navbar"; 3 | 4 | export default function Content(props: PropsWithChildren) { 5 | return
10 | 11 |
14 | { props.children } 15 |
16 |
17 | } -------------------------------------------------------------------------------- /dashboard/src/components/shortcuts/Key.tsx: -------------------------------------------------------------------------------- 1 | import { IKeyProps } from '@/interfaces/components/Shortcuts'; 2 | 3 | export default function Key({ children }: IKeyProps) { 4 | return ( 5 | 13 | {children} 14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /dashboard/src/components/shortcuts/ShortcutEntry.tsx: -------------------------------------------------------------------------------- 1 | import { IShortcutEntryProps } from '@/interfaces/components/Shortcuts'; 2 | import Key from '@/components/shortcuts/Key'; 3 | 4 | export default function ShortcutEntry({ 5 | description, 6 | comb = [], 7 | }: IShortcutEntryProps) { 8 | return ( 9 |
16 |
{description}
17 |
22 | {comb?.map((v, i) => {v})} 23 |
24 |
25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /dashboard/src/configs/constants.ts: -------------------------------------------------------------------------------- 1 | export const API_VERSION = process.env.NEXT_PUBLIC_API_VERSION || 'v1'; 2 | export const WS_VERSION = process.env.NEXT_PUBLIC_WS_VERSION || 'v1'; 3 | 4 | export const BASE_API_URL = `${ 5 | process.env.NEXT_PUBLIC_API_URL || 'localhost:3000' 6 | }/api/${API_VERSION}`; 7 | 8 | export const BASE_WS_URL = `${ 9 | process.env.NEXT_PUBLIC_WS_URL || 'localhost:8080' 10 | }/ws/${WS_VERSION}`; 11 | 12 | export const API_SECURE_PROTOCOL = 13 | process.env.NEXT_PUBLIC_API_SECURE_PROTOCOL === 'true'; 14 | 15 | export const WS_SECURE_PROTOCOL = 16 | process.env.NEXT_PUBLIC_WS_SECURE_PROTOCOL === 'true'; 17 | 18 | const apiSecureStr = API_SECURE_PROTOCOL ? 's' : ''; 19 | const wsSecureStr = WS_SECURE_PROTOCOL ? 's' : ''; 20 | 21 | export const API_URL = `http${apiSecureStr}://${BASE_API_URL}`; 22 | export const WS_URL = `ws${wsSecureStr}://${BASE_WS_URL}`; 23 | -------------------------------------------------------------------------------- /dashboard/src/hooks/useAbortDelay.ts: -------------------------------------------------------------------------------- 1 | import { useRef } from 'react'; 2 | 3 | export default function useAbortDelay() { 4 | const ref = useRef(); 5 | 6 | const start = () => { 7 | ref.current = true; 8 | }; 9 | 10 | const end = () => { 11 | ref.current = undefined; 12 | }; 13 | 14 | const run = (cb: () => void, delay: number) => { 15 | start(); 16 | 17 | setTimeout(() => { 18 | if (!ref.current) return; 19 | 20 | cb(); 21 | 22 | end(); 23 | }, delay); 24 | }; 25 | 26 | const reset = () => { 27 | if (ref.current) ref.current = undefined; 28 | }; 29 | 30 | return { reset, ref, run }; 31 | } 32 | -------------------------------------------------------------------------------- /dashboard/src/hooks/useAuthGuard.ts: -------------------------------------------------------------------------------- 1 | import { getSavedUser } from '@/utils/localStorage'; 2 | import { useRouter } from 'next/router'; 3 | import { useEffect } from 'react'; 4 | 5 | export default function useAuthGuard() { 6 | const router = useRouter(); 7 | 8 | useEffect(() => { 9 | const user = getSavedUser(); 10 | 11 | if (!user?.access_token?.length) router.push('/login'); 12 | }, []); 13 | } 14 | -------------------------------------------------------------------------------- /dashboard/src/hooks/useProcessData.ts: -------------------------------------------------------------------------------- 1 | import { IBaseApiResponse, IUseProcessDataOptions } from '@/interfaces/api'; 2 | 3 | export function useProcessData( 4 | data: IBaseApiResponse | undefined, 5 | isLoading: boolean, 6 | options: IUseProcessDataOptions = {}, 7 | ) { 8 | const { 9 | loadingComponent = 'Loading', 10 | failedComponent = 'Failed to fetch data', 11 | enabled = true, 12 | } = options; 13 | 14 | return function (successValue: T) { 15 | return isLoading && enabled 16 | ? loadingComponent 17 | : data?.success || !enabled 18 | ? typeof successValue === 'function' 19 | ? successValue() 20 | : successValue 21 | : failedComponent; 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /dashboard/src/hooks/useSharedStateGetter.ts: -------------------------------------------------------------------------------- 1 | import SharedState from '@/libs/sharedState'; 2 | import { useCallback, useEffect, useState } from 'react'; 3 | 4 | export default function useSharedStateGetter(stateManager: SharedState) { 5 | const [state, setState] = useState(stateManager.get()); 6 | 7 | const handler = useCallback((newState: K) => { 8 | setState(stateManager.copy(newState)); 9 | }, []); 10 | 11 | useEffect(() => { 12 | stateManager.registerListener(handler); 13 | 14 | return () => { 15 | stateManager.removeListener(handler); 16 | }; 17 | }, []); 18 | 19 | return state; 20 | } 21 | -------------------------------------------------------------------------------- /dashboard/src/hooks/useSharedStateSetter.ts: -------------------------------------------------------------------------------- 1 | import SharedState from '@/libs/sharedState'; 2 | import { useCallback, useEffect } from 'react'; 3 | 4 | interface IUseSharedStateSetterParam { 5 | 0: keyof T; 6 | 1: T[keyof T]; 7 | } 8 | 9 | export default function useSharedStateSetter( 10 | stateManager: SharedState, 11 | ...args: IUseSharedStateSetterParam[] 12 | ) { 13 | const deps = args.map((v) => v[1]); 14 | 15 | const doUpdate = useCallback((unmount?: boolean) => { 16 | for (const v of args) { 17 | stateManager.set(v[0], unmount ? (undefined as K[keyof K]) : v[1]); 18 | } 19 | 20 | if (args.length) stateManager.updateListener(); 21 | }, deps); 22 | 23 | useEffect(() => { 24 | doUpdate(); 25 | return () => doUpdate(true); 26 | }, deps); 27 | } 28 | -------------------------------------------------------------------------------- /dashboard/src/interfaces/components/Modal.ts: -------------------------------------------------------------------------------- 1 | export interface IModalProps { 2 | children?: React.ReactNode; 3 | fullHeight?: boolean; 4 | open?: boolean; 5 | } 6 | 7 | export interface IModalShortcutProps { 8 | open?: IModalProps['open']; 9 | onClose?: React.ComponentProps<'svg'>['onClick']; 10 | modalContainerProps?: IModalProps; 11 | } 12 | -------------------------------------------------------------------------------- /dashboard/src/interfaces/components/Navbar.ts: -------------------------------------------------------------------------------- 1 | export interface INavbarProps {} 2 | -------------------------------------------------------------------------------- /dashboard/src/interfaces/components/PlaylistBar.ts: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { ITrack } from '../wsShared'; 3 | 4 | export interface IPlaylistBarProps { 5 | queue: ITrack[]; 6 | setQueue: ReturnType>[1]; 7 | hide?: boolean; 8 | } 9 | -------------------------------------------------------------------------------- /dashboard/src/interfaces/components/Shortcuts.ts: -------------------------------------------------------------------------------- 1 | export interface IKeyProps { 2 | children?: React.ReactNode; 3 | } 4 | 5 | export interface IShortcutEntryProps { 6 | description?: React.ReactNode; 7 | comb?: IKeyProps['children'][]; 8 | } 9 | -------------------------------------------------------------------------------- /dashboard/src/interfaces/globalState.ts: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | type IBooleanStateRT = ReturnType>; 4 | 5 | export interface IGlobalState { 6 | navbarShow?: IBooleanStateRT[0]; 7 | setNavbarShow?: IBooleanStateRT[1]; 8 | navbarAbsolute?: IBooleanStateRT[0]; 9 | setNavbarAbsolute?: IBooleanStateRT[1]; 10 | } 11 | -------------------------------------------------------------------------------- /dashboard/src/interfaces/guild.ts: -------------------------------------------------------------------------------- 1 | export interface IGuildRole { 2 | id: string; 3 | name: string; 4 | color: string; 5 | } 6 | 7 | export interface IGuildChannel { 8 | id: string; 9 | name: string; 10 | type: number; 11 | parent: string; 12 | } 13 | 14 | export interface IGuildMember { 15 | id: string; 16 | username: string; 17 | discriminator: string; 18 | avatar?: string; 19 | roles: string[]; 20 | } 21 | 22 | export interface ITrack { 23 | title: string; 24 | author: string; 25 | duration: number; 26 | } 27 | 28 | export interface IGuildPlayer { 29 | queue: ITrack[]; 30 | playing: ITrack; 31 | } 32 | 33 | export interface IGuildDisplay { 34 | id: string; 35 | name: string; 36 | icon?: string; 37 | mutual: boolean; 38 | } 39 | 40 | export interface IGuild { 41 | id: string; 42 | name: string; 43 | icon?: string; 44 | owner: string; 45 | roles?: IGuildRole[]; 46 | channels: IGuildChannel[]; 47 | members: IGuildMember[]; 48 | player: IGuildPlayer; 49 | } 50 | -------------------------------------------------------------------------------- /dashboard/src/interfaces/image.ts: -------------------------------------------------------------------------------- 1 | export interface IGetImageOnErrorHandlerOptions { 2 | img: string; 3 | setImgFallback: (fb: boolean) => void; 4 | setNewImg: (newImg: string) => void; 5 | } 6 | -------------------------------------------------------------------------------- /dashboard/src/interfaces/kbdsrct.ts: -------------------------------------------------------------------------------- 1 | interface IBaseKbdsrct { 2 | comb: string[]; 3 | cb: () => void; 4 | description?: string; 5 | combDisplay?: string[]; 6 | } 7 | 8 | export interface IDefaultKbdsrct extends IBaseKbdsrct { 9 | category: string; 10 | hide?: false; 11 | } 12 | 13 | export interface IHiddenKbdsrct extends IBaseKbdsrct { 14 | category?: string; 15 | hide: true; 16 | } 17 | 18 | export type IKbdsrct = IDefaultKbdsrct | IHiddenKbdsrct; 19 | 20 | export interface IShorcutGroup { 21 | category: string; 22 | shorcuts: IKbdsrct[]; 23 | } 24 | -------------------------------------------------------------------------------- /dashboard/src/interfaces/layouts.ts: -------------------------------------------------------------------------------- 1 | import { AppProps } from 'next/app'; 2 | 3 | export interface NextPageWithLayout extends React.FC { 4 | getLayout?: (page: React.ReactElement) => React.ReactNode; 5 | } 6 | 7 | export type AppPropsWithLayout = AppProps & { 8 | Component: NextPageWithLayout; 9 | }; 10 | 11 | export interface IPageLayoutProps { 12 | children: React.ReactNode; 13 | contentContainerStyle?: React.HTMLAttributes['style']; 14 | } 15 | 16 | export type PageLayout = React.FC; 17 | -------------------------------------------------------------------------------- /dashboard/src/interfaces/playerState.ts: -------------------------------------------------------------------------------- 1 | export type IPlayerState = { 2 | // !TODO 3 | }; 4 | -------------------------------------------------------------------------------- /dashboard/src/interfaces/sharedState.ts: -------------------------------------------------------------------------------- 1 | export type IUpdateListener = (states: T) => void; 2 | -------------------------------------------------------------------------------- /dashboard/src/interfaces/user.ts: -------------------------------------------------------------------------------- 1 | export interface IUserAuth { 2 | access_token: string; 3 | token_type: 'Bearer'; 4 | expires_in: number; 5 | refresh_token: string; 6 | scope: string; 7 | } 8 | 9 | export interface IUser { 10 | id: string; 11 | username: string; 12 | avatar?: string; 13 | discriminator: string; 14 | public_flags: number; 15 | flags: number; 16 | banner?: string; 17 | accent_color: number; 18 | global_name: string; 19 | avatar_decoration?: string; 20 | banner_color: string; 21 | access_token: string; 22 | } 23 | -------------------------------------------------------------------------------- /dashboard/src/interfaces/ws.ts: -------------------------------------------------------------------------------- 1 | import { ESocketEventType, ISocketEvent } from './wsShared'; 2 | 3 | export interface IPlayerSocketOptions { 4 | connURL?: string; 5 | logUnhandledEvent?: boolean; 6 | } 7 | 8 | export interface IPlayerSocketHandlers { 9 | close?: (e: CloseEvent) => void; 10 | open?: (e: Event) => void; 11 | message?: (e: MessageEvent) => void; 12 | error?: (e: Event) => void; 13 | } 14 | 15 | export type IPlayerEventHandlers = { 16 | [K in ESocketEventType]?: (e: ISocketEvent) => void; 17 | }; 18 | 19 | export interface IPlayerWSMountOptions { 20 | mountHandler?: IPlayerSocketHandlers; 21 | eventHandler?: IPlayerEventHandlers; 22 | logUnhandledEvent?: boolean; 23 | } 24 | -------------------------------------------------------------------------------- /dashboard/src/layouts/DashboardLayout.tsx: -------------------------------------------------------------------------------- 1 | import { PageLayout } from '@/interfaces/layouts'; 2 | import Navbar from '@/components/navbar'; 3 | import useAuthGuard from '@/hooks/useAuthGuard'; 4 | import { INavbarProps } from '@/interfaces/components/Navbar'; 5 | 6 | interface IDashboardLayoutProps { 7 | navbarProps?: INavbarProps; 8 | layoutContainerProps?: React.HTMLAttributes['style']; 9 | } 10 | 11 | const DashboardLayout: PageLayout = ({ 12 | children, 13 | contentContainerStyle = { 14 | paddingLeft: '50px', 15 | paddingRight: '50px', 16 | paddingTop: '30px', 17 | paddingBottom: '50px', 18 | }, 19 | navbarProps = {}, 20 | layoutContainerProps = { 21 | display: 'flex', 22 | height: '100%', 23 | overflow: 'auto', 24 | }, 25 | }) => { 26 | useAuthGuard(); 27 | 28 | return ( 29 |
30 | 31 |
{children}
32 |
33 | ); 34 | }; 35 | 36 | export default DashboardLayout; 37 | -------------------------------------------------------------------------------- /dashboard/src/libs/queryClient.ts: -------------------------------------------------------------------------------- 1 | import { QueryClient } from '@tanstack/react-query'; 2 | 3 | const queryClient = new QueryClient(); 4 | 5 | export default queryClient; 6 | -------------------------------------------------------------------------------- /dashboard/src/libs/sharedState.ts: -------------------------------------------------------------------------------- 1 | import { IUpdateListener } from '@/interfaces/sharedState'; 2 | 3 | export default abstract class SharedState { 4 | states: T; 5 | updateHandlers: IUpdateListener[]; 6 | 7 | constructor() { 8 | this.states = {} as T; 9 | this.updateHandlers = []; 10 | } 11 | 12 | get() { 13 | return this.states; 14 | } 15 | 16 | updateListener() { 17 | for (const handler of this.updateHandlers) { 18 | handler(this.states); 19 | } 20 | } 21 | 22 | set(key: KT, value: T[KT]) { 23 | this.states[key] = value; 24 | } 25 | 26 | registerListener(handler: this['updateHandlers'][number]) { 27 | if (this.updateHandlers.includes(handler)) return -1; 28 | 29 | this.updateHandlers.push(handler); 30 | } 31 | 32 | removeListener(handler: this['updateHandlers'][number]) { 33 | const idx = this.updateHandlers.findIndex((h) => h === handler); 34 | 35 | if (idx !== -1) { 36 | this.updateHandlers.splice(idx, 1); 37 | } 38 | 39 | return idx; 40 | } 41 | 42 | abstract copy(state: T): T; 43 | } 44 | -------------------------------------------------------------------------------- /dashboard/src/libs/sockets/index.ts: -------------------------------------------------------------------------------- 1 | export * as playerSocket from './player'; 2 | -------------------------------------------------------------------------------- /dashboard/src/libs/sockets/player/emit.ts: -------------------------------------------------------------------------------- 1 | import { createEventPayload } from '@/utils/wsShared'; 2 | import { sendJson } from './states'; 3 | import { ESocketEventType } from '@/interfaces/wsShared'; 4 | 5 | export function emitSeek(position: number) { 6 | sendJson(createEventPayload(ESocketEventType.SEEK, position)); 7 | } 8 | 9 | export function emitPause(state: boolean) { 10 | sendJson(createEventPayload(ESocketEventType.PAUSE, state)); 11 | } 12 | 13 | export function emitPrevious() { 14 | sendJson(createEventPayload(ESocketEventType.PREVIOUS)); 15 | } 16 | 17 | export function emitNext() { 18 | sendJson(createEventPayload(ESocketEventType.NEXT)); 19 | } 20 | 21 | export function emitQueueUpdate(queue: number[]) { 22 | sendJson(createEventPayload(ESocketEventType.UPDATE_QUEUE, queue)); 23 | } 24 | 25 | export function emitTrackRemove(id: number) { 26 | sendJson(createEventPayload(ESocketEventType.REMOVE_TRACK, id)); 27 | } 28 | -------------------------------------------------------------------------------- /dashboard/src/libs/sockets/player/states.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IPlayerEventHandlers, 3 | IPlayerSocketHandlers, 4 | IPlayerSocketOptions, 5 | } from '@/interfaces/ws'; 6 | 7 | let socket: WebSocket | undefined; 8 | 9 | const opts: IPlayerSocketOptions = {}; 10 | const handlers: IPlayerSocketHandlers = {}; 11 | const eventHandlers: IPlayerEventHandlers = {}; 12 | 13 | function mountState(state: T, newState: T) { 14 | for (const k in newState) { 15 | state[k] = newState[k]; 16 | } 17 | } 18 | 19 | function unmountState(s: T) { 20 | for (const k in s) { 21 | // @ts-ignore 22 | s[k] = undefined; 23 | } 24 | } 25 | 26 | export function getSocket() { 27 | return socket; 28 | } 29 | 30 | export function setSocket(v: typeof socket) { 31 | socket = v; 32 | } 33 | 34 | export function getOptions() { 35 | return opts; 36 | } 37 | 38 | export function getHandlers() { 39 | return handlers; 40 | } 41 | 42 | export function getEventHandlers() { 43 | return eventHandlers; 44 | } 45 | 46 | export function optionsMount(v: typeof opts) { 47 | mountState(opts, v); 48 | } 49 | 50 | export function handlersMount(v: typeof handlers) { 51 | mountState(handlers, v); 52 | } 53 | 54 | export function eventHandlersMount(v: typeof eventHandlers) { 55 | mountState(eventHandlers, v); 56 | } 57 | 58 | export function optionsUnmount() { 59 | unmountState(opts); 60 | } 61 | 62 | export function handlersUnmount() { 63 | unmountState(handlers); 64 | } 65 | 66 | export function eventHandlersUnmount() { 67 | unmountState(eventHandlers); 68 | } 69 | 70 | export function statesUnmount() { 71 | handlersUnmount(); 72 | eventHandlersUnmount(); 73 | optionsUnmount(); 74 | } 75 | 76 | export function sendJson(json: object) { 77 | // silently fail if socket unitialized 78 | socket?.send(JSON.stringify(json)); 79 | } 80 | -------------------------------------------------------------------------------- /dashboard/src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import { AppPropsWithLayout } from '@/interfaces/layouts'; 2 | import AppLayout from '@/layouts/AppLayout'; 3 | import queryClient from '@/libs/queryClient'; 4 | import { QueryClientProvider } from '@tanstack/react-query'; 5 | import './global.scss'; 6 | 7 | export default function MyApp({ Component, pageProps }: AppPropsWithLayout) { 8 | // Use the layout defined at the page level, if available 9 | const getLayout = Component.getLayout ?? ((page) => page); 10 | 11 | return ( 12 | 13 | {getLayout()} 14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /dashboard/src/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Document, { 3 | DocumentContext, 4 | Head, 5 | Html, 6 | Main, 7 | NextScript, 8 | } from 'next/document'; 9 | import { CssBaseline } from '@nextui-org/react'; 10 | 11 | class MyDocument extends Document { 12 | static async getInitialProps(ctx: DocumentContext) { 13 | const initialProps = await Document.getInitialProps(ctx); 14 | return { 15 | ...initialProps, 16 | styles: React.Children.toArray([initialProps.styles]), 17 | }; 18 | } 19 | 20 | render() { 21 | return ( 22 | 23 | 24 | {CssBaseline.flush()} 25 | 30 | 31 | 32 |
33 | 34 | 35 | 36 | ); 37 | } 38 | } 39 | 40 | export default MyDocument; 41 | -------------------------------------------------------------------------------- /dashboard/src/pages/invite/index.tsx: -------------------------------------------------------------------------------- 1 | import { getInvite } from '@/services/api'; 2 | import Head from 'next/head'; 3 | import { useRouter } from 'next/router'; 4 | import { useEffect } from 'react'; 5 | 6 | export const getServerSideProps = () => { 7 | return { props: {} }; 8 | }; 9 | 10 | export default function Logout(_props: any) { 11 | const router = useRouter(); 12 | 13 | useEffect(() => { 14 | getInvite().then((inv) => { 15 | if (!inv?.data?.length) throw new Error("Can't get invite"); 16 | 17 | let appendUri = router.query.redirect_uri?.length 18 | ? '&redirect_uri=' + 19 | encodeURIComponent(router.query.redirect_uri as string) 20 | : ''; 21 | 22 | router.replace(inv.data + appendUri); 23 | }); 24 | }, []); 25 | 26 | return ( 27 | <> 28 | 29 | Invite | Discord Music Bot 30 | 31 |
39 |

Inviting...

40 |
41 | 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /dashboard/src/pages/login/index.tsx: -------------------------------------------------------------------------------- 1 | import ProcessData from '@/components/ProcessData'; 2 | import { useGetLoginURL, usePostLogin } from '@/services/api'; 3 | import { saveUser } from '@/utils/localStorage'; 4 | import { getQueryData } from '@/utils/query'; 5 | import Head from 'next/head'; 6 | import { useRouter } from 'next/router'; 7 | import { useEffect } from 'react'; 8 | 9 | function LoggingIn() { 10 | return

Logging in...

; 11 | } 12 | 13 | // it doesn't seems like it does anything 14 | // but i can guarantee you this page will 15 | // break if you remove this, so don't 16 | export const getServerSideProps = () => { 17 | return { props: {} }; 18 | }; 19 | 20 | const Login = () => { 21 | const router = useRouter(); 22 | const query = router.query; 23 | 24 | const enableGetLoginURL = !query.code; 25 | 26 | const { data: loginURL, isLoading } = useGetLoginURL({ 27 | enabled: enableGetLoginURL, 28 | }); 29 | 30 | const { data } = usePostLogin(query, { 31 | onError: () => router.replace('/login'), 32 | }); 33 | 34 | const url = getQueryData(loginURL); 35 | 36 | useEffect(() => { 37 | if (url) 38 | window.location.href = 39 | url + 40 | '&redirect_uri=' + 41 | encodeURIComponent(window.location.href); 42 | }, [url]); 43 | 44 | if (data?.data) { 45 | saveUser(data.data); 46 | 47 | router.replace('/dashboard'); 48 | } 49 | 50 | const opts = { 51 | loadingComponent: , 52 | failedComponent:

Failed logging in

, 53 | enabled: enableGetLoginURL, 54 | }; 55 | 56 | return ( 57 | <> 58 | 59 | Logging In | Discord Music Bot 60 | 61 |
69 | 70 | {opts.loadingComponent} 71 | 72 |
73 | 74 | ); 75 | }; 76 | 77 | export default Login; 78 | -------------------------------------------------------------------------------- /dashboard/src/pages/logout/index.tsx: -------------------------------------------------------------------------------- 1 | import { logout } from '@/utils/common'; 2 | import Head from 'next/head'; 3 | import { useRouter } from 'next/router'; 4 | import { useEffect } from 'react'; 5 | 6 | export default function Logout(_props: any) { 7 | const router = useRouter(); 8 | 9 | useEffect(() => { 10 | logout(); 11 | router.replace('/'); 12 | }, []); 13 | 14 | return ( 15 | <> 16 | 17 | Logging Out | Discord Music Bot 18 | 19 |
27 |

Logging out...

28 |
29 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /dashboard/src/pages/servers/index.tsx: -------------------------------------------------------------------------------- 1 | import Head from 'next/head'; 2 | import Server from '@/components/server'; 3 | import { NextPageWithLayout } from '@/interfaces/layouts'; 4 | import DashboardLayout from '@/layouts/DashboardLayout'; 5 | import { useGetServerList, usePostLogin } from '@/services/api'; 6 | import { useProcessData } from '@/hooks/useProcessData'; 7 | import { getQueryData } from '@/utils/query'; 8 | import { useRouter } from 'next/router'; 9 | import { saveUser } from '@/utils/localStorage'; 10 | 11 | export const getServerSideProps = () => { 12 | return { props: {} }; 13 | }; 14 | 15 | const Servers: NextPageWithLayout = () => { 16 | const router = useRouter(); 17 | const { data, isLoading } = useGetServerList(); 18 | 19 | const processData = useProcessData(data, isLoading); 20 | 21 | const { data: userData } = usePostLogin(router.query, { 22 | onError: () => router.replace('/login'), 23 | }); 24 | 25 | if (userData?.data) { 26 | saveUser(userData.data); 27 | 28 | router.replace('/servers'); 29 | } 30 | 31 | console.log('serverList:', data); 32 | 33 | const { servers } = getQueryData(data) || {}; 34 | 35 | return ( 36 |
43 | 44 | Servers | Discord Music Bot 45 | 46 |

Select a server

47 |
54 | {processData(() => 55 | servers?.map((server) => ( 56 | 57 | )), 58 | )} 59 |
60 |
61 | ); 62 | }; 63 | 64 | Servers.getLayout = (page) => {page}; 65 | 66 | export default Servers; 67 | -------------------------------------------------------------------------------- /dashboard/src/sharedStates/globalState.ts: -------------------------------------------------------------------------------- 1 | import { IGlobalState } from '@/interfaces/globalState'; 2 | import SharedState from '@/libs/sharedState'; 3 | 4 | class GlobalSharedState extends SharedState { 5 | constructor() { 6 | super(); 7 | } 8 | 9 | copy(state: IGlobalState) { 10 | return { ...state }; 11 | } 12 | } 13 | 14 | const globalState = new GlobalSharedState(); 15 | 16 | export default globalState; 17 | -------------------------------------------------------------------------------- /dashboard/src/sharedStates/playerState.ts: -------------------------------------------------------------------------------- 1 | import { IPlayerState } from '@/interfaces/playerState'; 2 | import SharedState from '@/libs/sharedState'; 3 | 4 | class PlayerSharedState extends SharedState { 5 | constructor() { 6 | super(); 7 | } 8 | 9 | copy(state: IPlayerState) { 10 | return { ...state }; 11 | } 12 | } 13 | 14 | const playerState = new PlayerSharedState(); 15 | 16 | export default playerState; 17 | -------------------------------------------------------------------------------- /dashboard/src/utils/common.ts: -------------------------------------------------------------------------------- 1 | import { clearAuth, clearUser } from './localStorage'; 2 | 3 | export const logout = () => { 4 | clearAuth(); 5 | clearUser(); 6 | // !TODO: notify server to invalidate cache 7 | }; 8 | 9 | /** 10 | * Should be called client side 11 | */ 12 | export const getDocumentDragHandler = () => { 13 | return document.body; 14 | }; 15 | 16 | export const setElementActive = (el: Element) => { 17 | el.classList.add('active'); 18 | }; 19 | 20 | export const setElementInactive = (el: Element) => { 21 | el.classList.remove('active'); 22 | }; 23 | -------------------------------------------------------------------------------- /dashboard/src/utils/dashboard.ts: -------------------------------------------------------------------------------- 1 | export interface IDashboard { 2 | commandsRan: number; 3 | users: number; 4 | servers: number; 5 | songsPlayed: number; 6 | } 7 | 8 | // export const getDashboard: () => Promise = () => { 9 | // return new Promise(async (resolve, _reject) => { 10 | // let json = await apiCall("GET", "/dashboard", { 11 | // credentials: "same-origin", 12 | // }).then(async ({data}) => { 13 | // return await data; 14 | // }); 15 | // resolve(json); 16 | // }) 17 | // } 18 | -------------------------------------------------------------------------------- /dashboard/src/utils/data.ts: -------------------------------------------------------------------------------- 1 | // !TODO: move these to interfaces when needs to be used 2 | export interface ICommand { 3 | name: string; 4 | description: string; 5 | } 6 | 7 | export interface IData { 8 | name: string; 9 | version: string; 10 | commands: ICommand[]; 11 | inviteURL: string; 12 | loggedIn: boolean | null; 13 | redirect: string | null; 14 | } 15 | 16 | // export const getData: () => Promise = () => { 17 | // return new Promise(async (resolve, _reject) => { 18 | // let commands = await (apiCall("GET", "/commands", { 19 | // method: "GET" 20 | // })).then(async ({data} = {}) => { 21 | // return await data; 22 | // }); 23 | // resolve(await commands) 24 | // }); 25 | // } 26 | -------------------------------------------------------------------------------- /dashboard/src/utils/formatting.ts: -------------------------------------------------------------------------------- 1 | export function isNumber(v: any): v is number { 2 | return typeof v === 'number'; 3 | } 4 | 5 | export function pad2digit(d: number) { 6 | if (d < 10) { 7 | return `0${d}`; 8 | } 9 | 10 | return `${d}`; 11 | } 12 | 13 | export function formatDuration(dur: number) { 14 | const minuteMs = 60 * 1000; 15 | const hourMs = 60 * minuteMs; 16 | 17 | const hour = Math.floor(dur / hourMs); 18 | const minute = Math.floor(dur / minuteMs); 19 | const second = Math.round((dur % minuteMs) / 1000); 20 | 21 | return ( 22 | (hour > 0 ? `${pad2digit(hour)}:` : '') + 23 | `${pad2digit(minute)}:${pad2digit(second)}` 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /dashboard/src/utils/image.ts: -------------------------------------------------------------------------------- 1 | import { IGetImageOnErrorHandlerOptions } from '../interfaces/image'; 2 | 3 | export const getImageOnErrorHandler = ({ 4 | img, 5 | setImgFallback, 6 | setNewImg, 7 | }: IGetImageOnErrorHandlerOptions) => { 8 | return () => { 9 | const newImg = img.replace('maxresdefault.', 'hqdefault.'); 10 | 11 | if (img === newImg) return; 12 | 13 | console.warn('Image fallback:', img); 14 | 15 | setImgFallback(true); 16 | setNewImg(newImg); 17 | }; 18 | }; 19 | -------------------------------------------------------------------------------- /dashboard/src/utils/localStorage.ts: -------------------------------------------------------------------------------- 1 | import { IUser, IUserAuth } from '@/interfaces/user'; 2 | 3 | export function setLocalStorage(key: string, data: T) { 4 | if (typeof data !== 'object') return; 5 | 6 | localStorage.setItem(key, JSON.stringify(data)); 7 | 8 | return data; 9 | } 10 | 11 | export function getLocalStorage(key: string) { 12 | const str = localStorage.getItem(key); 13 | 14 | if (!str?.length) return; 15 | 16 | return JSON.parse(str) as T; 17 | } 18 | 19 | export function removeLocalStorage(key: string) { 20 | return localStorage.removeItem(key); 21 | } 22 | 23 | export function saveAuth(data: IUserAuth) { 24 | return setLocalStorage('userAuth', data); 25 | } 26 | 27 | export function clearAuth() { 28 | return removeLocalStorage('userAuth'); 29 | } 30 | 31 | export function getSavedAuth() { 32 | return getLocalStorage('userAuth') as IUserAuth | undefined; 33 | } 34 | 35 | export function saveUser(data: IUser) { 36 | return setLocalStorage('user', data); 37 | } 38 | 39 | export function clearUser() { 40 | return removeLocalStorage('user'); 41 | } 42 | 43 | export function getSavedUser() { 44 | return getLocalStorage('user') as IUser | undefined; 45 | } 46 | -------------------------------------------------------------------------------- /dashboard/src/utils/query.ts: -------------------------------------------------------------------------------- 1 | import { IBaseApiResponse } from '@/interfaces/api'; 2 | import { getSavedUser } from './localStorage'; 3 | 4 | export function getQueryData(data: IBaseApiResponse | undefined) { 5 | return data?.success ? data.data || undefined : undefined; 6 | } 7 | 8 | export function getAuthHeaders() { 9 | return { 10 | access_token: getSavedUser()?.access_token, 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /dashboard/src/utils/wsShared.ts: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////////////////////////////// 2 | // "Shared Utils" 3 | // Must be in sync with api utils 4 | //////////////////////////////////////////////////////////////////////////////////////////////////// 5 | 6 | import { 7 | ESocketErrorCode, 8 | ESocketEventType, 9 | IConstructITrackOptions, 10 | ISocketData, 11 | ISocketEvent, 12 | ITrack, 13 | } from '../interfaces/wsShared'; 14 | 15 | export function createErrPayload( 16 | code: K, 17 | message?: string, 18 | ): ISocketEvent { 19 | return { 20 | e: ESocketEventType.ERROR, 21 | d: { code, message }, 22 | }; 23 | } 24 | 25 | export function createEventPayload( 26 | e: K, 27 | d: ISocketData[K] | null = null, 28 | ) { 29 | return { 30 | e, 31 | d, 32 | }; 33 | } 34 | 35 | export function processTrackThumbnail(track: ITrack, hq?: boolean) { 36 | return track.thumbnail?.replace( 37 | 'default.', 38 | hq ? 'hqdefault.' : 'maxresdefault.', 39 | ); 40 | } 41 | 42 | export function constructITrack({ 43 | track, 44 | id, 45 | hqThumbnail, 46 | }: IConstructITrackOptions) { 47 | return { 48 | ...track, 49 | thumbnail: processTrackThumbnail(track, hqThumbnail), 50 | id, 51 | }; 52 | } 53 | 54 | //////////////////////////////////////////////////////////////////////////////////////////////////// 55 | -------------------------------------------------------------------------------- /dashboard/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "@/*": ["src/*"] 6 | }, 7 | "target": "es5", 8 | "lib": [ 9 | "dom", 10 | "dom.iterable", 11 | "esnext" 12 | ], 13 | "allowJs": true, 14 | "skipLibCheck": true, 15 | "strict": true, 16 | "forceConsistentCasingInFileNames": true, 17 | "noEmit": true, 18 | "incremental": true, 19 | "esModuleInterop": true, 20 | "module": "esnext", 21 | "moduleResolution": "Node", 22 | "resolveJsonModule": true, 23 | "isolatedModules": true, 24 | "jsx": "preserve" 25 | }, 26 | "include": [ 27 | "next-env.d.ts", 28 | "**/*.ts", 29 | "**/*.tsx" 30 | ], 31 | "exclude": [ 32 | "node_modules" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /delightful-contributors.md: -------------------------------------------------------------------------------- 1 | # delightful contributors 2 | 3 | These fine people brought us delight by adding their gems of freedom to this delightful project. 4 | 5 | > **Note**: This page is maintained by you, contributors. Please create a pull request if you contributed list entries and want to be included. [More info here](https://codeberg.org/teaserbot-labs/delightful/src/branch/main/delight-us.md#attribution-of-contributors). 6 | 7 | ## We thank you for your gems of freedom :gem: 8 | 9 | - BioCla 10 | - LewdHuTao 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /djs-bot/.env.example: -------------------------------------------------------------------------------- 1 | ########################## DIST TEMPLATE ######################### 2 | ## Replace the values with your own and rename the file to .env ## 3 | ################################################################## 4 | 5 | # Discord configuration 6 | 7 | # https://discord.com/developers/applications/YOUR_CLIENTID/bot 8 | TOKEN=your_token 9 | # https://discord.com/developers/applications/YOUR_CLIENTID/oauth2 10 | CLIENTID=0123456789012345678 11 | CLIENTSECRET=abcdefghijklmnopqrstuvwxyz123456 12 | DEVUID=012345678901234567 13 | JWT_SECRET_KEY=some_secretkey 14 | 15 | # This was inserted by `prisma init`: 16 | # Environment variables declared in this file are automatically made available to Prisma. 17 | # See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema 18 | 19 | # Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB. 20 | # See the documentation for all the connection string options: https://pris.ly/d/connection-strings 21 | # Options: postgresql, mongodb, mysql, sqlite 22 | DATABASE="postgresql" 23 | 24 | # I don't recommend changing this unless you know what you're doing. 25 | # To get rid of the DB entirely, remove this line completely. 26 | POSTGRES_USER=your_username 27 | POSTGRES_PASSWORD=your_pg_pswd 28 | POSTGRES_DB=your_db_name 29 | PGPORT=5432 30 | DATABASE_URL="postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres-db:${PGPORT}/${POSTGRES_DB}?schema=public" 31 | 32 | # Backend configuration 33 | API_PORT=8080 34 | WS_PORT=3001 35 | -------------------------------------------------------------------------------- /djs-bot/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 8, 4 | "useTabs": true, 5 | "singleQuote": false, 6 | "printWidth": 100, 7 | "endOfLine": "auto" 8 | } 9 | -------------------------------------------------------------------------------- /djs-bot/.vimspector.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": { 3 | "Launch": { 4 | "adapter": "js-debug", 5 | "configuration": { 6 | "request": "launch", 7 | "program": "${workspaceRoot}/index.js", 8 | "cwd": "${workspaceRoot}", 9 | "externalConsole": true, 10 | "type": "pwa-node" 11 | } 12 | }, 13 | "Attach": { 14 | "adapter": "js-debug", 15 | "configuration": { 16 | "request": "attach", 17 | "program": "${workspaceRoot}/index.js", 18 | "cwd": "${workspaceRoot}", 19 | "externalConsole": true, 20 | "type": "pwa-node" 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /djs-bot/api/v0/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const cors = require('cors'); 3 | const express = require('express'); 4 | const Bot = require('../../lib/Bot'); 5 | 6 | // https://expressjs.com/en/starter/installing.html 7 | const app = express(); 8 | app.use(cors()); 9 | app.get('/', (req, res) => { 10 | res.status(200).json({ 11 | message: "Systems Operational!", 12 | version: require("../../package.json").version, 13 | }); 14 | }); 15 | 16 | // It will load all the routes from the `./routes` folder 17 | // and will mount them on the `/api/v0` path 18 | // So, for example, if you have a route in `./routes/test.js` 19 | // it will be mounted on `/api/v0/test` 20 | // read https://expressjs.com/en/guide/routing.html for more info 21 | const router = express.Router() 22 | 23 | /** 24 | * Constructs and mounts the API on to the Bot instance provided 25 | * @param {Bot} bot 26 | */ 27 | module.exports = (bot) => { 28 | const routes = fs.readdirSync(__dirname + '/routes').filter(file => file.endsWith('.js')) 29 | for (const file of routes) { 30 | const route = require('./routes/' + file); 31 | router.use('/' + file.split('.')[0], (req, res) => route(req, res, bot)) 32 | } 33 | app.use('/api/v0', router) 34 | 35 | return app; 36 | } 37 | -------------------------------------------------------------------------------- /djs-bot/api/v0/routes/commands.js: -------------------------------------------------------------------------------- 1 | /** 2 | * /commands route 3 | * returns the commands the bot has 4 | * 5 | * @param {import("express").Request} req 6 | * @param {import("express").Response} res 7 | * @param {import("../../../lib/Bot")} bot 8 | */ 9 | module.exports = (req, res, bot) => { 10 | res.status(200).json({ 11 | commands: bot.slash.map(command => ({ 12 | name: command.name, 13 | description: command.description, 14 | })), 15 | }); 16 | } -------------------------------------------------------------------------------- /djs-bot/api/v0/routes/dashboard.js: -------------------------------------------------------------------------------- 1 | /** 2 | * /dashboard route 3 | * returns how many commands have been ran, The Users it has, and the Servers it is in and how many songs have been played 4 | * 5 | * @param {import("express").Request} req 6 | * @param {import("express").Response} res 7 | * @param {import("../../../lib/Bot")} bot 8 | */ 9 | module.exports = (req, res, bot) => { 10 | res.status(200).json({ 11 | commandsRan: bot.commandsRan || 0, 12 | users: bot.guilds.cache.reduce((acc, guild) => acc + guild.memberCount, 0), 13 | servers: bot.guilds.cache.size, 14 | songsPlayed: bot.songsPlayed || 0, 15 | }); 16 | } -------------------------------------------------------------------------------- /djs-bot/api/v0/routes/servers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * /servers route 3 | * returns the servers the bot is in 4 | * 5 | * @param {import("express").Request} req 6 | * @param {import("express").Response} res 7 | * @param {import("../../../lib/Bot")} bot 8 | */ 9 | module.exports = (req, res, bot) => { 10 | const id = req.query.id; 11 | if (id) { 12 | const guild = bot.guilds.cache.get(id); 13 | if (!guild) return res.status(404).json({ error: "Server not found" }); 14 | 15 | return res.status(200).json({ 16 | id: guild.id, 17 | name: guild.name, 18 | icon: guild.iconURL(), 19 | owner: guild.ownerId, 20 | roles: guild.roles.cache.map(role => ({ 21 | id: role.id, 22 | name: role.name, 23 | color: role.hexColor, 24 | })), 25 | channels: guild.channels.cache.map(channel => ({ 26 | id: channel.id, 27 | name: channel.name, 28 | type: channel.type, 29 | parent: channel.parentId, 30 | })), 31 | members: guild.members.cache.map(member => ({ 32 | id: member.id, 33 | username: member.user.username, 34 | discriminator: member.user.discriminator, 35 | avatar: member.user.avatarURL(), 36 | roles: member.roles.cache.map(role => role.id), 37 | })), 38 | player: { 39 | queue: bot.manager.Engine.players.get(guild.id)?.queue.map(track => ({ 40 | title: track.title, 41 | author: track.author, 42 | duration: track.duration, 43 | })), 44 | playing: { 45 | title: bot.manager.Engine.players.get(guild.id)?.queue.current?.title, 46 | author: bot.manager.Engine.players.get(guild.id)?.queue.current?.author, 47 | duration: bot.manager.Engine.players.get(guild.id)?.queue.current?.duration, 48 | } 49 | } 50 | }); 51 | } 52 | 53 | res.status(200).json({ 54 | servers: bot.guilds.cache.map(guild => ({ 55 | id: guild.id, 56 | name: guild.name, 57 | icon: guild.iconURL(), 58 | })), 59 | }); 60 | } -------------------------------------------------------------------------------- /djs-bot/api/v0/routes/test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test route 3 | * @param {import("express").Request} req 4 | * @param {import("express").Response} res 5 | * @param {import("../../../lib/Bot")} bot 6 | */ 7 | module.exports = (req, res, bot) => { 8 | res.status(200).json({ 9 | message: "You are in /api/v0/test", 10 | bot: bot.denom, 11 | }); 12 | } -------------------------------------------------------------------------------- /djs-bot/api/v1/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | # 0x 40 | profile-* 41 | 42 | # mac files 43 | .DS_Store 44 | 45 | # vim swap files 46 | *.swp 47 | 48 | # webstorm 49 | .idea 50 | 51 | # vscode 52 | .vscode 53 | *code-workspace 54 | 55 | # clinic 56 | profile* 57 | *clinic* 58 | *flamegraph* 59 | 60 | # generated code 61 | examples/typescript-server.js 62 | test/types/index.js 63 | 64 | # compiled app 65 | dist 66 | -------------------------------------------------------------------------------- /djs-bot/api/v1/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "singleQuote": true, 4 | "printWidth": 80, 5 | "tabWidth": 2, 6 | "endOfLine": "auto" 7 | } 8 | -------------------------------------------------------------------------------- /djs-bot/api/v1/src/index.ts: -------------------------------------------------------------------------------- 1 | import fastify from 'fastify'; 2 | import * as uws from 'uWebSockets.js'; 3 | import type { Bot, WSApp } from './interfaces/common'; 4 | import routes from './routes/v1'; 5 | import routesErrorHandler from './routes/v1/errorHandler'; 6 | import APIError from './lib/APIError'; 7 | import cors from '@fastify/cors'; 8 | import { API_ROUTES_PREFIX } from './lib/constants'; 9 | import { setupWsServer } from './ws'; 10 | 11 | const pkg = require('../../../package.json'); 12 | 13 | const server = fastify({ 14 | logger: false, // true, 15 | }); 16 | 17 | const wsServer: WSApp = uws.App(); 18 | 19 | let bot: Bot | undefined; 20 | 21 | /** 22 | * Careful with noThrow = true, the return value might be 23 | * undefined. Type was forced for convenience 24 | */ 25 | const getBot = (noThrow: boolean = false) => { 26 | if (!noThrow && !bot) 27 | throw new APIError( 28 | 'Bot not running', 29 | APIError.STATUS_CODES.NO_BOT, 30 | APIError.ERROR_CODES.NO_BOT, 31 | ); 32 | 33 | return bot as Bot; 34 | }; 35 | 36 | const getPkg = () => pkg; 37 | 38 | const corsOpts = { 39 | origin: true, 40 | methods: 'GET', 41 | credentials: true, 42 | }; 43 | 44 | const setupServer = async () => { 45 | server.setErrorHandler(routesErrorHandler); 46 | 47 | await server.register(cors); 48 | 49 | server.get('/', async (request, reply) => { 50 | return { 51 | message: 'Systems Operational!', 52 | version: pkg.version, 53 | }; 54 | }); 55 | 56 | await server.register(routes, { 57 | prefix: API_ROUTES_PREFIX, 58 | }); 59 | }; 60 | 61 | const app = (djsBot?: Bot) => { 62 | bot = djsBot; 63 | 64 | setupServer(); 65 | 66 | return server; 67 | }; 68 | 69 | const wsApp = () => { 70 | setupWsServer(wsServer); 71 | 72 | return wsServer; 73 | }; 74 | 75 | export { app, getBot, getPkg, wsApp }; 76 | -------------------------------------------------------------------------------- /djs-bot/api/v1/src/interfaces/common.ts: -------------------------------------------------------------------------------- 1 | import { FastifyInstance } from 'fastify'; 2 | import * as uws from 'uWebSockets.js'; 3 | import { ERROR_CODES, STATUS_CODES } from '../lib/constants'; 4 | import type DJSBot from '../../../../lib/Bot'; 5 | 6 | export type Bot = DJSBot; 7 | 8 | export type RegisterRouteHandler = Parameters[0]; 9 | 10 | export type FastifyRouteHandler = Parameters< 11 | FastifyInstance['route'] 12 | >[0]['handler']; 13 | 14 | export interface IRouteHandlerOptions { 15 | requiresAuth?: boolean; 16 | } 17 | 18 | export interface APIRouteHandler { 19 | default: FastifyRouteHandler; 20 | method?: IServerMethod; 21 | options?: IRouteHandlerOptions; 22 | } 23 | 24 | export type IServerMethod = 25 | | 'delete' 26 | | 'get' 27 | | 'head' 28 | | 'patch' 29 | | 'post' 30 | | 'put' 31 | | 'options'; 32 | 33 | export interface RouteHandlerEntry { 34 | handler: FastifyRouteHandler; 35 | method: IServerMethod; 36 | options?: IRouteHandlerOptions; 37 | } 38 | 39 | export type RouteHandler = FastifyRouteHandler | RouteHandlerEntry; 40 | 41 | export type RouteErrorHandler = Parameters< 42 | FastifyInstance['setErrorHandler'] 43 | >[0]; 44 | 45 | export type IErrorCode = (typeof ERROR_CODES)[keyof typeof ERROR_CODES]; 46 | export type IStatusCode = (typeof STATUS_CODES)[keyof typeof STATUS_CODES]; 47 | 48 | export type WSApp = ReturnType; 49 | -------------------------------------------------------------------------------- /djs-bot/api/v1/src/interfaces/discord.ts: -------------------------------------------------------------------------------- 1 | import { IUser, IUserGuild } from './user'; 2 | 3 | export interface IPostLoginData { 4 | client_id: string; 5 | client_secret: string; 6 | grant_type: 'authorization_code'; 7 | code: string; 8 | redirect_uri: string; 9 | } 10 | 11 | export interface IPostLoginResponse { 12 | access_token: string; 13 | token_type: 'Bearer'; 14 | expires_in: number; 15 | refresh_token: string; 16 | scope: string; 17 | guild?: any; 18 | } 19 | 20 | export interface IGetUserOauthInfoParams { 21 | authType: string; 22 | authToken: string; 23 | } 24 | 25 | export interface IGetUserOauthInfoResponse { 26 | // unused property left out, fill when needed 27 | user?: IUser; 28 | } 29 | 30 | export type IGetUserGuildsResponse = IUserGuild[]; 31 | -------------------------------------------------------------------------------- /djs-bot/api/v1/src/interfaces/log.ts: -------------------------------------------------------------------------------- 1 | export type LogSeverety = 'log' | 'info' | 'debug' | 'warn' | 'error'; 2 | -------------------------------------------------------------------------------- /djs-bot/api/v1/src/interfaces/user.ts: -------------------------------------------------------------------------------- 1 | import { IPostLoginResponse } from './discord'; 2 | 3 | export interface IUser { 4 | id: string; 5 | username: string; 6 | avatar?: string; 7 | discriminator: string; 8 | public_flags: number; 9 | flags: number; 10 | banner?: string; 11 | accent_color: number; 12 | global_name: string; 13 | avatar_decoration?: string; 14 | banner_color: string; 15 | access_token: string; 16 | } 17 | 18 | export interface IUserAuth extends IPostLoginResponse { 19 | userId: string; 20 | } 21 | 22 | export interface IUserGuild { 23 | id: string; 24 | name: string; 25 | icon: string; 26 | owner: boolean; 27 | permissions: string; 28 | features: string[]; 29 | } 30 | -------------------------------------------------------------------------------- /djs-bot/api/v1/src/interfaces/ws.ts: -------------------------------------------------------------------------------- 1 | export interface IPlayerSocket { 2 | serverId: string; 3 | } 4 | -------------------------------------------------------------------------------- /djs-bot/api/v1/src/lib/APICache.ts: -------------------------------------------------------------------------------- 1 | export interface APICacheOptions { 2 | invalidateTimeout?: number; 3 | } 4 | 5 | export default class APICache { 6 | cache: Map; 7 | timers: Map; 8 | invalidateTimeout: number; 9 | 10 | constructor({ invalidateTimeout = 0 }: APICacheOptions = {}) { 11 | this.cache = new Map(); 12 | this.timers = new Map(); 13 | this.invalidateTimeout = invalidateTimeout; 14 | } 15 | 16 | setTimer(key: K, cb: () => boolean, timeout: number) { 17 | this.timers.set( 18 | key, 19 | setTimeout(() => { 20 | const deleteTimer = cb(); 21 | 22 | if (deleteTimer) this.cleanUpTimer(key); 23 | }, timeout), 24 | ); 25 | } 26 | 27 | cleanUpTimer(key: K) { 28 | const exist = this.timers.get(key); 29 | if (exist) clearTimeout(exist); 30 | 31 | return this.timers.delete(key); 32 | } 33 | 34 | set(key: K, value: T) { 35 | const ret = this.cache.set(key, value); 36 | 37 | this.cleanUpTimer(key); 38 | 39 | if (this.invalidateTimeout > 0) 40 | this.setTimer( 41 | key, 42 | () => { 43 | return this.cache.delete(key); 44 | }, 45 | this.invalidateTimeout, 46 | ); 47 | 48 | return ret; 49 | } 50 | 51 | get(key: K) { 52 | return this.cache.get(key); 53 | } 54 | 55 | delete(key: K) { 56 | const deleted = this.cache.delete(key); 57 | 58 | if (deleted) this.cleanUpTimer(key); 59 | 60 | return deleted; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /djs-bot/api/v1/src/lib/APIError.ts: -------------------------------------------------------------------------------- 1 | import type { IStatusCode, IErrorCode } from '../interfaces/common'; 2 | import { ERROR_CODES, STATUS_CODES } from './constants'; 3 | 4 | export default class APIError extends Error { 5 | status: IStatusCode; 6 | code: IErrorCode; 7 | 8 | constructor( 9 | message?: Error['message'], 10 | status: IStatusCode = STATUS_CODES['INTERNAL_ERROR'], 11 | code: IErrorCode = ERROR_CODES['INTERNAL_ERROR'], 12 | ) { 13 | super(message); 14 | 15 | this.name = 'APIError'; 16 | this.code = code; 17 | this.status = status; 18 | } 19 | 20 | static STATUS_CODES = STATUS_CODES; 21 | 22 | static ERROR_CODES = ERROR_CODES; 23 | } 24 | -------------------------------------------------------------------------------- /djs-bot/api/v1/src/lib/constants.ts: -------------------------------------------------------------------------------- 1 | export const STATUS_CODES = Object.freeze({ 2 | NO_BOT: 503, 3 | BAD_REQUEST: 400, 4 | UNAUTHORIZED: 401, 5 | FORBIDDEN: 403, 6 | NOT_FOUND: 404, 7 | INTERNAL_ERROR: 500, 8 | }); 9 | 10 | export const ERROR_CODES = Object.freeze({ 11 | NO_BOT: 1, 12 | BAD_REQUEST: 400, 13 | UNAUTHORIZED: 401, 14 | FORBIDDEN: 403, 15 | NOT_FOUND: 404, 16 | INTERNAL_ERROR: 500, 17 | INVALID_CONFIGURATION: 69420, 18 | }); 19 | 20 | export const DISCORD_API_URL = 'https://discord.com/api/v10'; 21 | 22 | export const API_ROUTES_PREFIX = '/api/v1'; 23 | 24 | export const WS_ROUTES_PREFIX = '/ws/v1'; 25 | -------------------------------------------------------------------------------- /djs-bot/api/v1/src/lib/db.ts: -------------------------------------------------------------------------------- 1 | import { getBot } from '..'; 2 | import { IPostLoginResponse } from '../interfaces/discord'; 3 | import { IUserAuth } from '../interfaces/user'; 4 | import { invalidateGetUserGuildsResponseCache } from '../services/discord'; 5 | import APICache from './APICache'; 6 | 7 | const getUserAuthDbCache = new APICache({ 8 | invalidateTimeout: 60 * 1000, 9 | }); 10 | 11 | export const getUserAuth = async (userId: string) => { 12 | const bot = getBot(); 13 | 14 | const cache = getUserAuthDbCache.get(userId); 15 | if (cache) return cache; 16 | 17 | const data: IUserAuth = await bot.db.userAuth.findUnique({ 18 | where: { userId }, 19 | }); 20 | 21 | invalidateGetUserGuildsResponseCache(userId); 22 | getUserAuthDbCache.set(userId, data); 23 | 24 | return data; 25 | }; 26 | 27 | export const updateUserAuth = async ( 28 | userId: string, 29 | authData: IPostLoginResponse, 30 | ) => { 31 | const bot = getBot(); 32 | 33 | const res = await bot.db.userAuth.upsert({ 34 | where: { 35 | userId, 36 | }, 37 | create: { ...authData, userId }, 38 | update: authData, 39 | }); 40 | 41 | invalidateGetUserGuildsResponseCache(userId); 42 | getUserAuthDbCache.set(userId, res); 43 | 44 | return res; 45 | }; 46 | -------------------------------------------------------------------------------- /djs-bot/api/v1/src/lib/jwt.ts: -------------------------------------------------------------------------------- 1 | import jwt from 'jsonwebtoken'; 2 | import APIError from './APIError'; 3 | const { JWT_SECRET_KEY } = process.env; 4 | 5 | const checkSecretKey = () => { 6 | if (!JWT_SECRET_KEY?.length) 7 | throw new APIError( 8 | "No authorization set up, I can't allow you to do anything. Have a nice day and go sleep", 9 | APIError.STATUS_CODES.INTERNAL_ERROR, 10 | APIError.ERROR_CODES.INVALID_CONFIGURATION, 11 | ); 12 | 13 | return true; 14 | }; 15 | 16 | interface IJWTPayload { 17 | user_id: string; 18 | } 19 | 20 | export const signToken = (payload: IJWTPayload) => { 21 | checkSecretKey(); 22 | 23 | return jwt.sign(payload, JWT_SECRET_KEY as string); 24 | }; 25 | 26 | export const verifyToken = (token: string) => { 27 | checkSecretKey(); 28 | 29 | return jwt.verify(token, JWT_SECRET_KEY as string) as unknown as IJWTPayload; 30 | }; 31 | -------------------------------------------------------------------------------- /djs-bot/api/v1/src/routes/v1/errorHandler/index.ts: -------------------------------------------------------------------------------- 1 | import { getBot } from '../../..'; 2 | import type { RouteErrorHandler } from '../../../interfaces/common'; 3 | import APIError from '../../../lib/APIError'; 4 | import axios from 'axios'; 5 | 6 | const routesErrorHandler: RouteErrorHandler = (err, request, reply) => { 7 | const bot = getBot(true); 8 | 9 | if (err instanceof APIError) { 10 | reply.status(err.status).send({ 11 | success: false, 12 | code: err.code, 13 | message: err.message, 14 | }); 15 | 16 | return; 17 | } 18 | 19 | if (err?.name === 'JsonWebTokenError') { 20 | reply.status(APIError.STATUS_CODES.UNAUTHORIZED).send({ 21 | success: false, 22 | code: APIError.ERROR_CODES.UNAUTHORIZED, 23 | message: 'Go away you pesky hacker', 24 | }); 25 | 26 | return; 27 | } 28 | 29 | if (err instanceof axios.AxiosError && err.response) { 30 | const { status, data } = err.response; 31 | 32 | reply.status(status).send({ 33 | success: false, 34 | code: status, 35 | data, 36 | message: data?.error, 37 | description: data?.error_description, 38 | }); 39 | return; 40 | } 41 | 42 | if (bot) { 43 | bot.error('Unhandled server error:'); 44 | bot.error(err); 45 | } 46 | 47 | console.error(err); 48 | 49 | reply.status(500).send(); 50 | }; 51 | 52 | export default routesErrorHandler; 53 | -------------------------------------------------------------------------------- /djs-bot/api/v1/src/routes/v1/routesHandler/commands.ts: -------------------------------------------------------------------------------- 1 | import { getBot } from '../../..'; 2 | import { RouteHandler } from '../../../interfaces/common'; 3 | import { createReply } from '../../../utils/reply'; 4 | 5 | // !TODO: what this endpoint made for? 6 | const handler: RouteHandler = async (request, reply) => { 7 | const bot = getBot(); 8 | 9 | return createReply({ 10 | commands: bot.slash.map((command) => ({ 11 | name: command.name, 12 | description: command.description, 13 | })), 14 | }); 15 | }; 16 | 17 | export const options = { requiresAuth: true }; 18 | 19 | export default handler; 20 | -------------------------------------------------------------------------------- /djs-bot/api/v1/src/routes/v1/routesHandler/dashboard.ts: -------------------------------------------------------------------------------- 1 | import { getBot } from '../../..'; 2 | import { RouteHandler } from '../../../interfaces/common'; 3 | import { createReply } from '../../../utils/reply'; 4 | import APIError from '../../../lib/APIError'; 5 | import moment from 'moment'; 6 | 7 | 8 | const handler: RouteHandler = async (request, reply) => { 9 | const bot = getBot(); 10 | 11 | if (!bot) 12 | throw new APIError( 13 | 'Bot not found', 14 | APIError.STATUS_CODES.NOT_FOUND, 15 | APIError.ERROR_CODES.NOT_FOUND, 16 | ); 17 | 18 | const lavaNodes = bot.manager?.Engine.nodes.entries(); 19 | 20 | if (!lavaNodes) 21 | throw new APIError( 22 | 'Lava nodes not found', 23 | APIError.STATUS_CODES.NOT_FOUND, 24 | APIError.ERROR_CODES.NOT_FOUND, 25 | ); 26 | 27 | let lavaNodesAggregates = {}; 28 | for (const [nodeId, node] of lavaNodes) { 29 | lavaNodesAggregates = { 30 | ...lavaNodesAggregates, 31 | [nodeId]: { 32 | id: nodeId, 33 | connected: node.connected, 34 | nodeStats: node.stats, 35 | stats: { 36 | cores: node.stats.cpu.cores, 37 | // @ts-ignore 38 | uptime: moment.duration(node.stats.uptime).format("d[ Days]・h[ Hrs]・m[ Mins]・s[ Secs]"), 39 | ramUsage: (node.stats.memory.used / 1024 / 1024).toFixed(2), 40 | ramTotal: (node.stats.memory.allocated / 1024 / 1024).toFixed(2), 41 | } 42 | }, 43 | }; 44 | } 45 | 46 | return createReply({ 47 | commandsRan: bot.commandsRan || 0, 48 | users: bot.guilds.cache.reduce((acc, guild) => acc + guild.memberCount, 0), 49 | servers: bot.guilds.cache.size, 50 | songsPlayed: bot.songsPlayed || 0, 51 | nodes: lavaNodesAggregates, 52 | }); 53 | }; 54 | 55 | export const options = { requiresAuth: true }; 56 | 57 | export default handler; 58 | -------------------------------------------------------------------------------- /djs-bot/api/v1/src/routes/v1/routesHandler/invite.ts: -------------------------------------------------------------------------------- 1 | import { getBot } from '../../..'; 2 | import { RouteHandler } from '../../../interfaces/common'; 3 | import { 4 | getBaseOauthURL, 5 | getInvitePermissionsParameter, 6 | getInviteScopesParameter, 7 | } from '../../../utils/common'; 8 | import { createReply } from '../../../utils/reply'; 9 | 10 | const handler: RouteHandler = async (request, reply) => { 11 | const bot = getBot(); 12 | 13 | return createReply( 14 | getBaseOauthURL() + 15 | getInvitePermissionsParameter() + 16 | getInviteScopesParameter() + 17 | encodeURIComponent(' ' + bot.getOauthScopes()), 18 | ); 19 | }; 20 | 21 | export default handler; 22 | -------------------------------------------------------------------------------- /djs-bot/api/v1/src/services/discord.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | import { DISCORD_API_URL } from '../lib/constants'; 4 | 5 | import type { 6 | IGetUserGuildsResponse, 7 | IGetUserOauthInfoParams, 8 | IGetUserOauthInfoResponse, 9 | IPostLoginData, 10 | IPostLoginResponse, 11 | } from '../interfaces/discord'; 12 | import APICache from '../lib/APICache'; 13 | import * as db from '../lib/db'; 14 | 15 | const discordService = axios.create({ 16 | baseURL: DISCORD_API_URL, 17 | }); 18 | 19 | export const postLogin = async (data: IPostLoginData) => { 20 | const res = await discordService.post( 21 | '/oauth2/token', 22 | data, 23 | { 24 | headers: { 25 | 'Content-Type': 'application/x-www-form-urlencoded', 26 | }, 27 | }, 28 | ); 29 | 30 | return res.data; 31 | }; 32 | 33 | export const getUserOauthInfo = async ({ 34 | authType, 35 | authToken, 36 | }: IGetUserOauthInfoParams) => { 37 | const res = await discordService.get( 38 | '/oauth2/@me', 39 | { 40 | headers: { 41 | Authorization: `${authType} ${authToken}`, 42 | }, 43 | }, 44 | ); 45 | 46 | return res.data; 47 | }; 48 | 49 | const getUserGuildsResponseCache = new APICache( 50 | { 51 | invalidateTimeout: 10 * 60 * 1000, 52 | }, 53 | ); 54 | 55 | export const invalidateGetUserGuildsResponseCache = async (userId: string) => { 56 | const { token_type, access_token } = await db.getUserAuth(userId as string); 57 | const auth = `${token_type} ${access_token}`; 58 | getUserGuildsResponseCache.delete(auth); 59 | }; 60 | 61 | export const getUserGuilds = async (userId: string) => { 62 | const { token_type, access_token } = await db.getUserAuth(userId as string); 63 | 64 | const auth = `${token_type} ${access_token}`; 65 | 66 | const cache = getUserGuildsResponseCache.get(auth); 67 | 68 | if (cache) { 69 | return cache; 70 | } 71 | 72 | const res = await discordService.get( 73 | '/users/@me/guilds', 74 | { 75 | headers: { 76 | Authorization: auth, 77 | }, 78 | }, 79 | ); 80 | 81 | getUserGuildsResponseCache.set(auth, res.data); 82 | 83 | return res.data; 84 | }; 85 | -------------------------------------------------------------------------------- /djs-bot/api/v1/src/standalone.ts: -------------------------------------------------------------------------------- 1 | import { app, wsApp } from './index'; 2 | 3 | const API_PORT = 3000; 4 | const WS_PORT = 8080; 5 | 6 | const server = app(); 7 | 8 | server.listen({ host: '0.0.0.0', port: API_PORT }, (err, address) => { 9 | if (err) { 10 | console.error(err); 11 | process.exit(1); 12 | } 13 | console.log(`API Server listening at ${address}`); 14 | }); 15 | 16 | const wsServer = wsApp(); 17 | 18 | wsServer.listen(WS_PORT, (listenSocket) => { 19 | if (listenSocket) { 20 | console.log(`WS Server listening to port ${WS_PORT}`); 21 | return; 22 | } 23 | 24 | throw new Error('Unable to start WS Server'); 25 | }); 26 | -------------------------------------------------------------------------------- /djs-bot/api/v1/src/utils/common.ts: -------------------------------------------------------------------------------- 1 | import { getBot } from '..'; 2 | 3 | export const getBaseOauthURL = () => { 4 | const bot = getBot(); 5 | 6 | return `https://discord.com/api/oauth2/authorize?client_id=${bot.config.clientId}&response_type=code&prompt=none`; 7 | }; 8 | 9 | export const getInvitePermissionsParameter = () => { 10 | const bot = getBot(); 11 | 12 | return '&permissions=' + bot.config.permissions; 13 | }; 14 | 15 | export const getInviteScopesParameter = () => { 16 | const bot = getBot(); 17 | 18 | return '&scope=' + encodeURIComponent(bot.config.scopes.join(' ')); 19 | }; 20 | 21 | export const getOauthScopesParameter = () => { 22 | const bot = getBot(); 23 | 24 | return '&scope=' + encodeURIComponent(bot.getOauthScopes()); 25 | }; 26 | -------------------------------------------------------------------------------- /djs-bot/api/v1/src/utils/log.ts: -------------------------------------------------------------------------------- 1 | import { WebSocket } from 'uWebSockets.js'; 2 | import { LogSeverety } from '../interfaces/log'; 3 | import { IPlayerSocket } from '../interfaces/ws'; 4 | 5 | export function wsLog(sev: LogSeverety, ...args: any[]) { 6 | console[sev]('wsServer:', ...args); 7 | } 8 | 9 | export function playerLog( 10 | sev: LogSeverety, 11 | ws: WebSocket, 12 | ...args: any[] 13 | ) { 14 | const wsData = ws.getUserData(); 15 | 16 | wsLog(sev, 'player/' + wsData.serverId + ':', ...args); 17 | } 18 | -------------------------------------------------------------------------------- /djs-bot/api/v1/src/utils/player.ts: -------------------------------------------------------------------------------- 1 | /* 2 | API should roll its own player utils, importing from main utils is BAD and causes circular import 3 | as api should only be imported by Bot, not the other way around 4 | */ 5 | import { CosmicordPlayerExtended } from '../../../../lib/clients/MusicClient'; 6 | import { handleQueueUpdate, handleStop } from '../ws/eventsHandler'; 7 | 8 | export function spliceQueue( 9 | player: CosmicordPlayerExtended, 10 | ...restArgs: Parameters 11 | ) { 12 | const ret = player.queue.splice(...restArgs); 13 | 14 | handleQueueUpdate({ guildId: player.guild, player }); 15 | 16 | return ret; 17 | } 18 | 19 | export async function playPrevious(player: CosmicordPlayerExtended) { 20 | const previousSong = player.queue.previous; 21 | const currentSong = player.queue.current; 22 | const nextSong = player.queue[0]; 23 | 24 | if ( 25 | !previousSong || 26 | previousSong === currentSong || 27 | previousSong === nextSong 28 | ) { 29 | return 1; 30 | } 31 | 32 | if ( 33 | currentSong && 34 | previousSong !== currentSong && 35 | previousSong !== nextSong 36 | ) { 37 | spliceQueue(player, 0, 0, currentSong); 38 | 39 | // whatever the hell is this 40 | // @ts-ignore 41 | await player.play(previousSong); 42 | } 43 | } 44 | 45 | export function skip(player: CosmicordPlayerExtended) { 46 | const autoQueue = player.get('autoQueue'); 47 | if (player.queue[0] == undefined && (!autoQueue || autoQueue === false)) { 48 | return 1; 49 | } 50 | 51 | player.queue.previous = player.queue.current; 52 | player.stop(); 53 | 54 | handleStop({ guildId: (player as CosmicordPlayerExtended).guild }); 55 | } 56 | -------------------------------------------------------------------------------- /djs-bot/api/v1/src/utils/reply.ts: -------------------------------------------------------------------------------- 1 | export function createReply(data: T) { 2 | return { 3 | success: true, 4 | data: data, 5 | }; 6 | } 7 | -------------------------------------------------------------------------------- /djs-bot/api/v1/src/utils/ws.ts: -------------------------------------------------------------------------------- 1 | import { HttpResponse, WebSocket } from 'uWebSockets.js'; 2 | import { WS_ROUTES_PREFIX } from '../lib/constants'; 3 | import { IPlayerSocket } from '../interfaces/ws'; 4 | import { ESocketEventType, ISocketEvent, ITrack } from '../interfaces/wsShared'; 5 | import { getBot } from '..'; 6 | import { CosmiPlayer } from 'cosmicord.js'; 7 | import { constructITrack } from './wsShared'; 8 | 9 | export function createWsRoute(route: string) { 10 | return WS_ROUTES_PREFIX + route; 11 | } 12 | 13 | export function bufferToString(buf: ArrayBuffer) { 14 | return Buffer.from(buf).toString(); 15 | } 16 | 17 | export function resEndJson(res: HttpResponse, json: object) { 18 | res.writeHeader('Content-Type', 'application/json').end(JSON.stringify(json)); 19 | } 20 | 21 | export function wsSendJson(ws: WebSocket, json: object) { 22 | ws.send(JSON.stringify(json)); 23 | } 24 | 25 | export function wsPlayerSubscribe(ws: WebSocket) { 26 | const wsData = ws.getUserData(); 27 | 28 | ws.subscribe('player/' + wsData.serverId); 29 | } 30 | 31 | export function wsPublish( 32 | topic: string, 33 | e: ISocketEvent, 34 | ) { 35 | const bot = getBot(true); 36 | 37 | // silently fail if server isn't initialized 38 | if (!bot) return; 39 | 40 | bot.wsServer?.publish(topic, JSON.stringify(e)); 41 | } 42 | 43 | export function getPlayerQueue(player?: CosmiPlayer, hqThumbnail?: boolean) { 44 | if (!player) return []; 45 | 46 | return player.queue.map((t, idx) => 47 | constructITrack({ track: t as any, id: idx, hqThumbnail }), 48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /djs-bot/api/v1/src/utils/wsShared.ts: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////////////////////////////// 2 | // "Shared Utils" 3 | // Must be in sync with dashboard utils 4 | //////////////////////////////////////////////////////////////////////////////////////////////////// 5 | 6 | import { 7 | ESocketErrorCode, 8 | ESocketEventType, 9 | IConstructITrackOptions, 10 | ISocketData, 11 | ISocketEvent, 12 | ITrack, 13 | } from '../interfaces/wsShared'; 14 | 15 | export function createErrPayload( 16 | code: K, 17 | message?: string, 18 | ): ISocketEvent { 19 | return { 20 | e: ESocketEventType.ERROR, 21 | d: { code, message }, 22 | }; 23 | } 24 | 25 | export function createEventPayload( 26 | e: K, 27 | d: ISocketData[K] | null = null, 28 | ) { 29 | return { 30 | e, 31 | d, 32 | }; 33 | } 34 | 35 | export function processTrackThumbnail(track: ITrack, hq?: boolean) { 36 | return track.thumbnail?.replace( 37 | 'default.', 38 | hq ? 'hqdefault.' : 'maxresdefault.', 39 | ); 40 | } 41 | 42 | export function constructITrack({ 43 | track, 44 | id, 45 | hqThumbnail, 46 | }: IConstructITrackOptions) { 47 | return { 48 | ...track, 49 | thumbnail: processTrackThumbnail(track, hqThumbnail), 50 | id, 51 | }; 52 | } 53 | 54 | //////////////////////////////////////////////////////////////////////////////////////////////////// 55 | -------------------------------------------------------------------------------- /djs-bot/api/v1/src/ws/eventsHandler/index.ts: -------------------------------------------------------------------------------- 1 | export { default as handleTrackStart } from './trackStart'; 2 | export { default as handleQueueUpdate } from './queueUpdate'; 3 | export { default as handleStop } from './stop'; 4 | export { default as handleProgressUpdate } from './progressUpdate'; 5 | export { default as handlePause } from './pause'; 6 | -------------------------------------------------------------------------------- /djs-bot/api/v1/src/ws/eventsHandler/pause.ts: -------------------------------------------------------------------------------- 1 | import { ESocketEventType } from '../../interfaces/wsShared'; 2 | import { wsPublish } from '../../utils/ws'; 3 | import { createEventPayload } from '../../utils/wsShared'; 4 | 5 | export default function handlePause({ 6 | guildId, 7 | state, 8 | }: { 9 | guildId: string; 10 | state: boolean; 11 | }) { 12 | if (!guildId?.length) throw new TypeError('Missing guildId'); 13 | 14 | const to = 'player/' + guildId; 15 | const d = createEventPayload(ESocketEventType.PAUSE, state); 16 | 17 | // !TODO: debug log, remove when done 18 | // console.log({ publish: to, d }); 19 | 20 | wsPublish(to, d); 21 | } 22 | -------------------------------------------------------------------------------- /djs-bot/api/v1/src/ws/eventsHandler/progressUpdate.ts: -------------------------------------------------------------------------------- 1 | import { ESocketEventType } from '../../interfaces/wsShared'; 2 | import { wsPublish } from '../../utils/ws'; 3 | import { createEventPayload } from '../../utils/wsShared'; 4 | 5 | export default function handleProgressUpdate({ 6 | guildId, 7 | position, 8 | }: { 9 | guildId: string; 10 | position: number; 11 | }) { 12 | if (!guildId?.length) throw new TypeError('Missing guildId'); 13 | 14 | const to = 'player/' + guildId; 15 | const d = createEventPayload(ESocketEventType.PROGRESS, position); 16 | 17 | // !TODO: debug log, remove when done 18 | // console.log({ publish: to, d }); 19 | 20 | wsPublish(to, d); 21 | } 22 | -------------------------------------------------------------------------------- /djs-bot/api/v1/src/ws/eventsHandler/queueUpdate.ts: -------------------------------------------------------------------------------- 1 | import { ESocketEventType } from '../../interfaces/wsShared'; 2 | import { getPlayerQueue, wsPublish } from '../../utils/ws'; 3 | 4 | // this is hilarious 5 | import { IHandleQueueUpdateParams } from '../../../../../lib/MusicEvents.d'; 6 | import { createEventPayload } from '../../utils/wsShared'; 7 | 8 | export default function handleQueueUpdate({ 9 | guildId, 10 | player, 11 | }: IHandleQueueUpdateParams) { 12 | if (!guildId?.length) throw new TypeError('Missing guildId'); 13 | if (!player?.queue) throw new TypeError('Missing player queue'); 14 | 15 | const to = 'player/' + guildId; 16 | const d = createEventPayload( 17 | ESocketEventType.GET_QUEUE, 18 | getPlayerQueue(player, true), 19 | ); 20 | 21 | // !TODO: debug log, remove when done 22 | // console.log({ publish: to, d }); 23 | 24 | wsPublish(to, d); 25 | } 26 | -------------------------------------------------------------------------------- /djs-bot/api/v1/src/ws/eventsHandler/stop.ts: -------------------------------------------------------------------------------- 1 | import { ESocketEventType } from '../../interfaces/wsShared'; 2 | import { wsPublish } from '../../utils/ws'; 3 | import { createEventPayload } from '../../utils/wsShared'; 4 | 5 | export default function handleStop({ guildId }: { guildId: string }) { 6 | if (!guildId?.length) throw new TypeError('Missing guildId'); 7 | 8 | const to = 'player/' + guildId; 9 | const d = createEventPayload(ESocketEventType.PLAYING, null); 10 | 11 | // !TODO: debug log, remove when done 12 | // console.log({ publish: to, d }); 13 | 14 | wsPublish(to, d); 15 | } 16 | -------------------------------------------------------------------------------- /djs-bot/api/v1/src/ws/eventsHandler/trackStart.ts: -------------------------------------------------------------------------------- 1 | // this is hilarious 2 | import { IHandleTrackStartParams } from '../../../../../lib/MusicEvents.d'; 3 | import { ESocketEventType } from '../../interfaces/wsShared'; 4 | import { wsPublish } from '../../utils/ws'; 5 | import { constructITrack, createEventPayload } from '../../utils/wsShared'; 6 | 7 | export default function handleTrackStart({ 8 | player, 9 | track, 10 | }: IHandleTrackStartParams) { 11 | if (!player?.guild?.length) throw new TypeError('Missing guildId'); 12 | 13 | const to = 'player/' + player.guild; 14 | const d = createEventPayload( 15 | ESocketEventType.PLAYING, 16 | constructITrack({ track: track as any, id: -1 }), 17 | ); 18 | 19 | // !TODO: debug log, remove when done 20 | // console.log({ publish: to, d }); 21 | 22 | wsPublish(to, d); 23 | } 24 | -------------------------------------------------------------------------------- /djs-bot/api/v1/src/ws/openHandler/index.ts: -------------------------------------------------------------------------------- 1 | import { WebSocket } from 'uWebSockets.js'; 2 | import { ESocketErrorCode, ESocketEventType } from '../../interfaces/wsShared'; 3 | import { getBot } from '../..'; 4 | import { getPlayerQueue, wsPlayerSubscribe, wsSendJson } from '../../utils/ws'; 5 | import { IPlayerSocket } from '../../interfaces/ws'; 6 | import { 7 | constructITrack, 8 | createErrPayload, 9 | createEventPayload, 10 | } from '../../utils/wsShared'; 11 | 12 | export default function handleOpen(ws: WebSocket) { 13 | const bot = getBot(); 14 | const wsData = ws.getUserData(); 15 | // !TODO: WTF IS A `bot.manager` 16 | const player = bot.manager?.Engine.players.get(wsData.serverId); 17 | 18 | const sendDEmpty = () => { 19 | const dEmpty = createEventPayload(ESocketEventType.PLAYING); 20 | const dEmpty2 = createEventPayload(ESocketEventType.PROGRESS, 0); 21 | 22 | wsSendJson(ws, dEmpty); 23 | wsSendJson(ws, dEmpty2); 24 | }; 25 | 26 | wsPlayerSubscribe(ws); 27 | 28 | const d = createEventPayload( 29 | ESocketEventType.GET_QUEUE, 30 | getPlayerQueue(player, true), 31 | ); 32 | wsSendJson(ws, d); 33 | 34 | if (!player) { 35 | sendDEmpty(); 36 | 37 | const d2 = createErrPayload(ESocketErrorCode.NOTHING, 'No active player'); 38 | wsSendJson(ws, d2); 39 | 40 | return; 41 | } 42 | 43 | const playing = player.queue.current; 44 | 45 | if (playing) { 46 | // track payload 47 | const d = createEventPayload( 48 | ESocketEventType.PLAYING, 49 | constructITrack({ track: playing as any, id: -1 }), 50 | ); 51 | // progress payload 52 | const d2 = createEventPayload(ESocketEventType.PROGRESS, player.position); 53 | // paused state 54 | const dp = createEventPayload(ESocketEventType.PAUSE, player.paused); 55 | 56 | wsSendJson(ws, d); 57 | wsSendJson(ws, d2); 58 | wsSendJson(ws, dp); 59 | } else sendDEmpty(); 60 | } 61 | -------------------------------------------------------------------------------- /djs-bot/api/v1/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@tsconfig/node16/tsconfig.json"], 3 | "compilerOptions": { 4 | "sourceMap": true, 5 | "allowJs": false, 6 | "alwaysStrict": true, 7 | "strict": true, 8 | "outDir": "./dist" 9 | }, 10 | "include": ["src/**/*.ts"], 11 | "exclude": ["./dist/**/*"] 12 | } 13 | -------------------------------------------------------------------------------- /djs-bot/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wtfnotavailable/Discord-MusicBot/b44d6baf2e005a682efc238e5dbb4864d2e82857/djs-bot/assets/.gitkeep -------------------------------------------------------------------------------- /djs-bot/assets/Embed.txt: -------------------------------------------------------------------------------- 1 | // For reference 2 | 3 | const { EmbedBuilder, ActionRowBuilder, StringSelectMenuBuilder, ButtonBuilder } = require('discord.js'); 4 | 5 | /** 6 | * retro compatibility for v13 7 | */ 8 | class MessageActionRow extends ActionRowBuilder { 9 | constructor() { 10 | super(); 11 | } 12 | } 13 | 14 | /** 15 | * retro compatibility for v13 16 | */ 17 | class MessageSelectMenu extends StringSelectMenuBuilder { 18 | constructor() { 19 | super(); 20 | } 21 | } 22 | 23 | /** 24 | * retro compatibility for v13 25 | */ 26 | class MessageButton extends ButtonBuilder { 27 | constructor() { 28 | super(); 29 | } 30 | } 31 | 32 | /** 33 | * retro compatibility for v13 34 | */ 35 | class MessageEmbed extends EmbedBuilder { 36 | /** 37 | * @param {string} name 38 | * @param {string} value 39 | * @returns {MessageEmbed} 40 | */ 41 | addField(name, value) { 42 | this.addFields({ name, value }); 43 | return this; 44 | } 45 | 46 | /** 47 | * @param {Number} pageNo 48 | * @param {Number} maxPages 49 | * @returns {ActionRowBuilder} 50 | */ 51 | getButtons = (pageNo, maxPages) => { 52 | return new ActionRowBuilder().addComponents( 53 | new MessageButton() 54 | .setCustomId("previous_page") 55 | .setEmoji("◀️") 56 | .setStyle("Primary") 57 | .setDisabled(pageNo == 0), 58 | new MessageButton() 59 | .setCustomId("next_page") 60 | .setEmoji("▶️") 61 | .setStyle("Primary") 62 | .setDisabled(pageNo == (maxPages - 1)), 63 | ); 64 | }; 65 | } 66 | 67 | module.exports = { MessageEmbed, MessageActionRow, MessageSelectMenu, MessageButton }; 68 | -------------------------------------------------------------------------------- /djs-bot/assets/no_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wtfnotavailable/Discord-MusicBot/b44d6baf2e005a682efc238e5dbb4864d2e82857/djs-bot/assets/no_bg.png -------------------------------------------------------------------------------- /djs-bot/bot.js: -------------------------------------------------------------------------------- 1 | // Constructing the client as a new class (refer to `./lib/Bot`) 2 | const Bot = require('./lib/Bot'); 3 | const client = new Bot(); 4 | module.exports.getClient = () => client; 5 | -------------------------------------------------------------------------------- /djs-bot/commands/misc/guildleave.js: -------------------------------------------------------------------------------- 1 | const { EmbedBuilder } = require("discord.js"); 2 | 3 | module.exports = { 4 | name: "guildleave", 5 | category: "misc", 6 | usage: "/guildleave ", 7 | description: "Leaves a guild specified by the ID.", 8 | options: [ 9 | { 10 | name: "id", 11 | type: 3, // "STRING" 12 | description: "Enter the guild ID to leave (type `list` for guild IDs)", 13 | required: true, 14 | }, 15 | ], 16 | ownerOnly: true, 17 | run: async (client, interaction, options) => { 18 | try { 19 | const id = interaction.options.getString('id'); 20 | 21 | if (id.toLowerCase() === 'list') { 22 | try { 23 | const guildList = client.guilds.cache.map(guild => `${guild.name} | ${guild.id}`).join('\n'); 24 | return interaction.reply({ content: `Guilds:\n\`${guildList}\``, ephemeral: true }); 25 | } catch (error) { 26 | console.error('Error listing guilds:', error); 27 | return interaction.reply({ content: `Check console for list of guilds`, ephemeral: true }); 28 | } 29 | } 30 | 31 | const guild = client.guilds.cache.get(id); 32 | if (!guild) { 33 | return interaction.reply({ content: `\`${id}\` is not a valid guild ID`, ephemeral: true }); 34 | } 35 | 36 | await guild.leave(); 37 | return interaction.reply({ content: `Left guild \`${id}\``, ephemeral: true }); 38 | } catch (error) { 39 | console.error(`There was an error trying to leave guild ${id}:`, error); 40 | return interaction.reply({ content: `Error leaving guild.`, ephemeral: true }); 41 | } 42 | }, 43 | }; 44 | -------------------------------------------------------------------------------- /djs-bot/commands/misc/invite.js: -------------------------------------------------------------------------------- 1 | const { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } = require("discord.js"); 2 | const SlashCommand = require("../../lib/SlashCommand"); 3 | 4 | const command = new SlashCommand() 5 | .setName("invite") 6 | .setDescription("Invite me to your server") 7 | .setRun(async (client, interaction, options) => { 8 | return interaction.reply({ 9 | embeds: [ 10 | new EmbedBuilder() 11 | .setColor(client.config.embedColor) 12 | .setTitle(`Invite me to your server!`), 13 | ], 14 | components: [ 15 | new ActionRowBuilder().addComponents( 16 | new ButtonBuilder() 17 | .setLabel("Invite me") 18 | .setStyle(ButtonStyle.Link) 19 | .setURL( 20 | client.getInviteLink(), 21 | ) 22 | ), 23 | ], 24 | }); 25 | }); 26 | module.exports = command; 27 | -------------------------------------------------------------------------------- /djs-bot/commands/music/247.js: -------------------------------------------------------------------------------- 1 | const colors = require("colors"); 2 | const { EmbedBuilder } = require("discord.js"); 3 | const SlashCommand = require("../../lib/SlashCommand"); 4 | 5 | const command = new SlashCommand() 6 | .setName("247") 7 | .setDescription("Prevents the bot from ever disconnecting from a VC (toggle)") 8 | .setRun(async (client, interaction, options) => { 9 | let channel = await client.getChannel(client, interaction); 10 | if (!channel) { 11 | return; 12 | } 13 | 14 | let player; 15 | if (client.manager.Engine) { 16 | player = client.manager.Engine.players.get(interaction.guild.id); 17 | } else { 18 | return interaction.reply({ 19 | embeds: [ 20 | new EmbedBuilder() 21 | .setColor("Red") 22 | .setDescription("Lavalink node is not connected"), 23 | ], 24 | }); 25 | } 26 | 27 | if (!player) { 28 | return interaction.reply({ 29 | embeds: [ 30 | new EmbedBuilder() 31 | .setColor("Red") 32 | .setDescription("There's nothing to play 24/7."), 33 | ], 34 | ephemeral: true, 35 | }); 36 | } 37 | 38 | let twentyFourSevenEmbed = new EmbedBuilder().setColor( 39 | client.config.embedColor, 40 | ); 41 | const twentyFourSeven = player.get("twentyFourSeven"); 42 | 43 | if (!twentyFourSeven) { 44 | player.set("twentyFourSeven", true); 45 | } else { 46 | player.set("twentyFourSeven", false); 47 | } 48 | twentyFourSevenEmbed 49 | .setDescription(`**24/7 mode is** \`${!twentyFourSeven ? "ON" : "OFF"}\``) 50 | .setFooter({ 51 | text: `The bot will ${!twentyFourSeven ? "now" : "no longer"} stay connected to the voice channel 24/7.` 52 | }); 53 | client.warn( 54 | `Player: ${ player.options.guild } | [${ colors.blue( 55 | "24/7", 56 | ) }] has been [${ colors.blue( 57 | !twentyFourSeven? "ENABLED" : "DISABLED", 58 | ) }] in ${ 59 | client.guilds.cache.get(player.options.guild) 60 | ? client.guilds.cache.get(player.options.guild).name 61 | : "a guild" 62 | }`, 63 | ); 64 | 65 | if (!player.playing && player.queue.totalSize === 0 && twentyFourSeven) { 66 | player.destroy(); 67 | } 68 | 69 | const ret = await interaction.reply({ embeds: [twentyFourSevenEmbed], fetchReply: true }); 70 | if (ret) setTimeout(() => ret.delete().catch(client.warn), 20000); 71 | return ret; 72 | }); 73 | 74 | module.exports = command; 75 | -------------------------------------------------------------------------------- /djs-bot/commands/music/autoleave.js: -------------------------------------------------------------------------------- 1 | const colors = require("colors"); 2 | const { EmbedBuilder } = require("discord.js"); 3 | const SlashCommand = require("../../lib/SlashCommand"); 4 | 5 | const command = new SlashCommand() 6 | .setName("autoleave") 7 | .setDescription("Automatically leaves when everyone leaves the voice channel (toggle)") 8 | .setRun(async (client, interaction) => { 9 | let channel = await client.getChannel(client, interaction); 10 | if (!channel) return; 11 | 12 | let player; 13 | if (client.manager.Engine) 14 | player = client.manager.Engine.players.get(interaction.guild.id); 15 | else 16 | return interaction.reply({ 17 | embeds: [ 18 | new EmbedBuilder() 19 | .setColor("Red") 20 | .setDescription("Lavalink node is not connected"), 21 | ], 22 | }); 23 | 24 | if (!player) { 25 | return interaction.reply({ 26 | embeds: [ 27 | new EmbedBuilder() 28 | .setColor("Red") 29 | .setDescription("There's nothing playing in the queue"), 30 | ], 31 | ephemeral: true, 32 | }); 33 | } 34 | 35 | let autoLeaveEmbed = new EmbedBuilder().setColor(client.config.embedColor); 36 | const autoLeave = player.get("autoLeave"); 37 | player.set("requester", interaction.guild.members.me); 38 | 39 | if (!autoLeave) { 40 | player.set("autoLeave", true); 41 | } else { 42 | player.set("autoLeave", false); 43 | } 44 | autoLeaveEmbed 45 | .setDescription(`**Auto Leave is** \`${!autoLeave ? "ON" : "OFF"}\``) 46 | .setFooter({ 47 | text: `The player will ${!autoLeave ? "now automatically" : "not automatically"} leave when the voice channel is empty.` 48 | }); 49 | client.warn( 50 | `Player: ${player.options.guild} | [${colors.blue( 51 | "autoLeave" 52 | )}] has been [${colors.blue(!autoLeave ? "ENABLED" : "DISABLED")}] in ${ 53 | client.guilds.cache.get(player.options.guild) 54 | ? client.guilds.cache.get(player.options.guild).name 55 | : "a guild" 56 | }` 57 | ); 58 | 59 | const ret = await interaction.reply({ embeds: [autoLeaveEmbed], fetchReply: true }); 60 | if (ret) setTimeout(() => ret.delete().catch(client.warn), 20000); 61 | return ret; 62 | }); 63 | 64 | module.exports = command; 65 | -------------------------------------------------------------------------------- /djs-bot/commands/music/autopause.js: -------------------------------------------------------------------------------- 1 | const colors = require("colors"); 2 | const { EmbedBuilder } = require("discord.js"); 3 | const SlashCommand = require("../../lib/SlashCommand"); 4 | 5 | const command = new SlashCommand() 6 | .setName("autopause") 7 | .setDescription("Automatically pause when everyone leaves the voice channel (toggle)") 8 | .setRun(async (client, interaction) => { 9 | let channel = await client.getChannel(client, interaction); 10 | if (!channel) return; 11 | 12 | let player; 13 | if (client.manager.Engine) 14 | player = client.manager.Engine.players.get(interaction.guild.id); 15 | else 16 | return interaction.reply({ 17 | embeds: [ 18 | new EmbedBuilder() 19 | .setColor("Red") 20 | .setDescription("Lavalink node is not connected"), 21 | ], 22 | }); 23 | 24 | if (!player) { 25 | return interaction.reply({ 26 | embeds: [ 27 | new EmbedBuilder() 28 | .setColor("Red") 29 | .setDescription("There's nothing playing in the queue"), 30 | ], 31 | ephemeral: true, 32 | }); 33 | } 34 | 35 | let autoPauseEmbed = new EmbedBuilder().setColor(client.config.embedColor); 36 | const autoPause = player.get("autoPause"); 37 | player.set("requester", interaction.guild.members.me); 38 | 39 | if (!autoPause) { 40 | player.set("autoPause", true); 41 | } else { 42 | player.set("autoPause", false); 43 | } 44 | autoPauseEmbed 45 | .setDescription(`**Auto Pause is** \`${!autoPause ? "ON" : "OFF"}\``) 46 | .setFooter({ 47 | text: `The player will ${!autoPause ? "now be automatically" : "no longer be"} paused when everyone leaves the voice channel.` 48 | }); 49 | client.warn( 50 | `Player: ${player.options.guild} | [${colors.blue( 51 | "AUTOPAUSE" 52 | )}] has been [${colors.blue(!autoPause ? "ENABLED" : "DISABLED")}] in ${ 53 | client.guilds.cache.get(player.options.guild) 54 | ? client.guilds.cache.get(player.options.guild).name 55 | : "a guild" 56 | }` 57 | ); 58 | 59 | const ret = await interaction.reply({ embeds: [autoPauseEmbed], fetchReply: true }); 60 | if (ret) setTimeout(() => ret.delete().catch(client.warn), 20000); 61 | return ret; 62 | }); 63 | 64 | module.exports = command; 65 | -------------------------------------------------------------------------------- /djs-bot/commands/music/autoqueue.js: -------------------------------------------------------------------------------- 1 | const colors = require("colors"); 2 | const { EmbedBuilder } = require("discord.js"); 3 | const SlashCommand = require("../../lib/SlashCommand"); 4 | const { autoQueueEmbed } = require("../../util/embeds"); 5 | 6 | const command = new SlashCommand() 7 | .setName("autoqueue") 8 | .setDescription("Automatically add songs to the queue (toggle)") 9 | .setRun(async (client, interaction) => { 10 | let channel = await client.getChannel(client, interaction); 11 | if (!channel) { 12 | return; 13 | } 14 | 15 | let player; 16 | if (client.manager.Engine) { 17 | player = client.manager.Engine.players.get(interaction.guild.id); 18 | } else { 19 | return interaction.reply({ 20 | embeds: [ 21 | new EmbedBuilder() 22 | .setColor("Red") 23 | .setDescription("Lavalink node is not connected"), 24 | ], 25 | }); 26 | } 27 | 28 | if (!player) { 29 | return interaction.reply({ 30 | embeds: [ 31 | new EmbedBuilder() 32 | .setColor("Red") 33 | .setDescription("There's nothing playing in the queue"), 34 | ], 35 | ephemeral: true, 36 | }); 37 | } 38 | 39 | const autoQueue = player.get("autoQueue"); 40 | player.set("requester", interaction.guild.members.me); 41 | 42 | if (!autoQueue) { 43 | player.set("autoQueue", true); 44 | } else { 45 | player.set("autoQueue", false); 46 | } 47 | 48 | client.warn( 49 | `Player: ${ player.options.guild } | [${ colors.blue( 50 | "AUTOQUEUE", 51 | ) }] has been [${ colors.blue(!autoQueue? "ENABLED" : "DISABLED") }] in ${ 52 | client.guilds.cache.get(player.options.guild) 53 | ? client.guilds.cache.get(player.options.guild).name 54 | : "a guild" 55 | }`, 56 | ); 57 | 58 | const ret = await interaction.reply({ embeds: [autoQueueEmbed({autoQueue})], fetchReply: true }); 59 | if (ret) setTimeout(() => ret.delete().catch(client.warn), 20000); 60 | return ret; 61 | }); 62 | 63 | module.exports = command; 64 | -------------------------------------------------------------------------------- /djs-bot/commands/music/clean.js: -------------------------------------------------------------------------------- 1 | const { EmbedBuilder } = require("discord.js"); 2 | const SlashCommand = require("../../lib/SlashCommand"); 3 | 4 | const command = new SlashCommand() 5 | .setName("clean") 6 | .setDescription("Cleans the last 100 bot messages from channel.") 7 | .addIntegerOption((option) => 8 | option 9 | .setName("number") 10 | .setDescription("Number of messages to delete.") 11 | .setMinValue(2).setMaxValue(100) 12 | .setRequired(false), 13 | ) 14 | .setRun(async (client, interaction, options) => { 15 | 16 | await interaction.deferReply(); 17 | let number = interaction.options.getInteger("number"); 18 | number = number && number < 100? ++number : 100; 19 | 20 | 21 | interaction.channel.messages.fetch({ 22 | limit: number, 23 | }).then((messages) => { 24 | const botMessages = []; 25 | messages.filter(m => m.author.id === client.user.id).forEach(msg => botMessages.push(msg)) 26 | 27 | botMessages.shift(); 28 | interaction.channel.bulkDelete(botMessages, true) 29 | .then(async deletedMessages => { 30 | //Filtering out messages that did not get deleted. 31 | messages = messages.filter(msg => { 32 | !deletedMessages.some(deletedMsg => deletedMsg == msg); 33 | }); 34 | if (messages.size > 0) { 35 | client.log(`Deleting [${ messages.size }] messages older than 14 days.`) 36 | for (const msg of messages) { 37 | await msg.delete(); 38 | } 39 | } 40 | 41 | await interaction.editReply({ embeds: [new EmbedBuilder().setDescription(`:white_check_mark: | Deleted ${ botMessages.length } bot messages`)] }); 42 | setTimeout(() => { 43 | interaction.deleteReply(); 44 | }, 5000); 45 | }) 46 | 47 | }); 48 | }) 49 | 50 | module.exports = command; 51 | -------------------------------------------------------------------------------- /djs-bot/commands/music/clear.js: -------------------------------------------------------------------------------- 1 | const SlashCommand = require("../../lib/SlashCommand"); 2 | const { EmbedBuilder } = require("discord.js"); 3 | const { clearQueue } = require("../../util/player"); 4 | 5 | const command = new SlashCommand() 6 | .setName("clear") 7 | .setDescription("Clear all tracks from queue") 8 | .setRun(async (client, interaction, options) => { 9 | let channel = await client.getChannel(client, interaction); 10 | if (!channel) { 11 | return; 12 | } 13 | 14 | let player; 15 | if (client.manager.Engine) { 16 | player = client.manager.Engine.players.get(interaction.guild.id); 17 | } else { 18 | return interaction.reply({ 19 | embeds: [ 20 | new EmbedBuilder() 21 | .setColor("Red") 22 | .setDescription("Lavalink node is not connected"), 23 | ], 24 | }); 25 | } 26 | 27 | if (!player) { 28 | return interaction.reply({ 29 | embeds: [ 30 | new EmbedBuilder() 31 | .setColor("Red") 32 | .setDescription("Nothing is playing right now."), 33 | ], 34 | ephemeral: true, 35 | }); 36 | } 37 | 38 | if (!player.queue || !player.queue.length || player.queue.length === 0) { 39 | let cembed = new EmbedBuilder() 40 | .setColor(client.config.embedColor) 41 | .setDescription( 42 | "❌ | **Invalid, Not enough track to be cleared.**" 43 | ); 44 | 45 | return interaction.reply({ embeds: [cembed], ephemeral: true }); 46 | } 47 | 48 | clearQueue(player); 49 | 50 | let clearEmbed = new EmbedBuilder() 51 | .setColor(client.config.embedColor) 52 | .setDescription(`✅ | **Cleared the queue!**`); 53 | 54 | return interaction.reply({ embeds: [clearEmbed] }); 55 | }); 56 | 57 | module.exports = command; 58 | -------------------------------------------------------------------------------- /djs-bot/commands/music/history.js: -------------------------------------------------------------------------------- 1 | const colors = require("colors"); 2 | const { EmbedBuilder } = require("discord.js"); 3 | const SlashCommand = require("../../lib/SlashCommand"); 4 | const { historyEmbed } = require("../../util/embeds"); 5 | const { deleteMessageDelay } = require("../../util/message"); 6 | 7 | const command = new SlashCommand() 8 | .setName("history") 9 | .setDescription("Keep song history in chat (toggle)") 10 | .setRun(async (client, interaction) => { 11 | let channel = await client.getChannel(client, interaction); 12 | if (!channel) { 13 | return; 14 | } 15 | 16 | let player; 17 | if (client.manager.Engine) { 18 | player = client.manager.Engine.players.get(interaction.guild.id); 19 | } else { 20 | return interaction.reply({ 21 | embeds: [ 22 | new EmbedBuilder() 23 | .setColor("Red") 24 | .setDescription("Lavalink node is not connected"), 25 | ], 26 | }); 27 | } 28 | 29 | if (!player) { 30 | return interaction.reply({ 31 | embeds: [ 32 | new EmbedBuilder() 33 | .setColor("Red") 34 | .setDescription("There's nothing playing in the queue"), 35 | ], 36 | ephemeral: true, 37 | }); 38 | } 39 | 40 | const history = player.get("history"); 41 | player.set("requester", interaction.guild.members.me); 42 | 43 | if (!history) { 44 | player.set("history", true); 45 | } else { 46 | player.set("history", false); 47 | } 48 | 49 | client.warn( 50 | `Player: ${ player.options.guild } | [${ colors.blue( 51 | "history", 52 | ) }] has been [${ colors.blue(!history? "ENABLED" : "DISABLED") }] in ${ 53 | client.guilds.cache.get(player.options.guild) 54 | ? client.guilds.cache.get(player.options.guild).name 55 | : "a guild" 56 | }`, 57 | ); 58 | 59 | const ret = await interaction.reply({ embeds: [historyEmbed({history})], fetchReply: true }); 60 | deleteMessageDelay(ret); 61 | return ret; 62 | }); 63 | 64 | module.exports = command; 65 | -------------------------------------------------------------------------------- /djs-bot/commands/music/loop.js: -------------------------------------------------------------------------------- 1 | const SlashCommand = require("../../lib/SlashCommand"); 2 | const { EmbedBuilder } = require("discord.js"); 3 | 4 | const command = new SlashCommand() 5 | .setName("loop") 6 | .setDescription("Loops the current song") 7 | .setRun(async (client, interaction, options) => { 8 | let channel = await client.getChannel(client, interaction); 9 | if (!channel) { 10 | return; 11 | } 12 | 13 | let player; 14 | if (client.manager.Engine) { 15 | player = client.manager.Engine.players.get(interaction.guild.id); 16 | } else { 17 | return interaction.reply({ 18 | embeds: [ 19 | new EmbedBuilder() 20 | .setColor("Red") 21 | .setDescription("Lavalink node is not connected"), 22 | ], 23 | }); 24 | } 25 | 26 | if (!player) { 27 | return interaction.reply({ 28 | embeds: [ 29 | new EmbedBuilder() 30 | .setColor("Red") 31 | .setDescription("Nothing is playing right now."), 32 | ], 33 | ephemeral: true, 34 | }); 35 | } 36 | 37 | if (player.setTrackRepeat(!player.trackRepeat)) { 38 | ; 39 | } 40 | const trackRepeat = player.trackRepeat? "enabled" : "disabled"; 41 | 42 | interaction.reply({ 43 | embeds: [ 44 | new EmbedBuilder() 45 | .setColor(client.config.embedColor) 46 | .setDescription(`👍 | **Loop has been \`${ trackRepeat }\`**`), 47 | ], 48 | }); 49 | }); 50 | 51 | module.exports = command; 52 | -------------------------------------------------------------------------------- /djs-bot/commands/music/loopq.js: -------------------------------------------------------------------------------- 1 | const SlashCommand = require("../../lib/SlashCommand"); 2 | const { EmbedBuilder } = require("discord.js"); 3 | 4 | const command = new SlashCommand() 5 | .setName("loopq") 6 | .setDescription("Loop the current song queue") 7 | .setRun(async (client, interaction, options) => { 8 | let channel = await client.getChannel(client, interaction); 9 | if (!channel) { 10 | return; 11 | } 12 | 13 | let player; 14 | if (client.manager.Engine) { 15 | player = client.manager.Engine.players.get(interaction.guild.id); 16 | } else { 17 | return interaction.reply({ 18 | embeds: [ 19 | new EmbedBuilder() 20 | .setColor("Red") 21 | .setDescription("Lavalink node is not connected"), 22 | ], 23 | }); 24 | } 25 | 26 | if (!player) { 27 | return interaction.reply({ 28 | embeds: [ 29 | new EmbedBuilder() 30 | .setColor("Red") 31 | .setDescription("There is no music playing."), 32 | ], 33 | ephemeral: true, 34 | }); 35 | } 36 | 37 | if (player.setQueueRepeat(!player.queueRepeat)) { 38 | ; 39 | } 40 | const queueRepeat = player.queueRepeat? "enabled" : "disabled"; 41 | 42 | interaction.reply({ 43 | embeds: [ 44 | new EmbedBuilder() 45 | .setColor(client.config.embedColor) 46 | .setDescription( 47 | `:thumbsup: | **Loop queue is now \`${ queueRepeat }\`**`, 48 | ), 49 | ], 50 | }); 51 | }); 52 | 53 | module.exports = command; 54 | -------------------------------------------------------------------------------- /djs-bot/commands/music/move.js: -------------------------------------------------------------------------------- 1 | const SlashCommand = require("../../lib/SlashCommand"); 2 | const { EmbedBuilder } = require("discord.js"); 3 | const { spliceQueue } = require("../../util/player"); 4 | 5 | const command = new SlashCommand() 6 | .setName("move") 7 | .setDescription("Moves track to a different position") 8 | .addIntegerOption((option) => 9 | option.setName("track").setDescription("The track number to move").setRequired(true) 10 | ) 11 | .addIntegerOption((option) => 12 | option 13 | .setName("position") 14 | .setDescription("The position to move the track to") 15 | .setRequired(true) 16 | ) 17 | 18 | .setRun(async (client, interaction) => { 19 | const track = interaction.options.getInteger("track"); 20 | const position = interaction.options.getInteger("position"); 21 | 22 | let channel = await client.getChannel(client, interaction); 23 | if (!channel) { 24 | return; 25 | } 26 | 27 | let player; 28 | if (client.manager.Engine) { 29 | player = client.manager.Engine.players.get(interaction.guild.id); 30 | } else { 31 | return interaction.reply({ 32 | embeds: [ 33 | new EmbedBuilder() 34 | .setColor("Red") 35 | .setDescription("Lavalink node is not connected"), 36 | ], 37 | }); 38 | } 39 | 40 | if (!player) { 41 | return interaction.reply({ 42 | embeds: [ 43 | new EmbedBuilder() 44 | .setColor("Red") 45 | .setDescription("There's nothing playing."), 46 | ], 47 | ephemeral: true, 48 | }); 49 | } 50 | 51 | let trackNum = Number(track) - 1; 52 | if (trackNum < 0 || trackNum > player.queue.length - 1) { 53 | return interaction.reply(":x: | **Invalid track number**"); 54 | } 55 | 56 | let dest = Number(position) - 1; 57 | if (dest < 0 || dest > player.queue.length - 1) { 58 | return interaction.reply(":x: | **Invalid position number**"); 59 | } 60 | 61 | const thing = player.queue[trackNum]; 62 | 63 | spliceQueue(player, trackNum, 1); 64 | spliceQueue(player, dest, 0, thing); 65 | 66 | return interaction.reply({ 67 | embeds: [ 68 | new EmbedBuilder() 69 | .setColor(client.config.embedColor) 70 | .setDescription(":white_check_mark: | **Moved track**"), 71 | ], 72 | }); 73 | }); 74 | 75 | module.exports = command; 76 | -------------------------------------------------------------------------------- /djs-bot/commands/music/pause.js: -------------------------------------------------------------------------------- 1 | const SlashCommand = require("../../lib/SlashCommand"); 2 | const { EmbedBuilder } = require("discord.js"); 3 | const { pause } = require("../../util/player"); 4 | 5 | const command = new SlashCommand() 6 | .setName("pause") 7 | .setDescription("Pauses the current playing track") 8 | .setRun(async (client, interaction, options) => { 9 | let channel = await client.getChannel(client, interaction); 10 | if (!channel) { 11 | return; 12 | } 13 | 14 | let player; 15 | if (client.manager.Engine) { 16 | player = client.manager.Engine.players.get(interaction.guild.id); 17 | } else { 18 | return interaction.reply({ 19 | embeds: [ 20 | new EmbedBuilder() 21 | .setColor("Red") 22 | .setDescription("Lavalink node is not connected"), 23 | ], 24 | }); 25 | } 26 | 27 | if (!player) { 28 | return interaction.reply({ 29 | embeds: [ 30 | new EmbedBuilder() 31 | .setColor("Red") 32 | .setDescription("Nothing is playing."), 33 | ], 34 | ephemeral: true, 35 | }); 36 | } 37 | 38 | if (player.paused) { 39 | return interaction.reply({ 40 | embeds: [ 41 | new EmbedBuilder() 42 | .setColor("Red") 43 | .setDescription("Current playing track is already paused!"), 44 | ], 45 | ephemeral: true, 46 | }); 47 | } 48 | 49 | pause(player, true); 50 | 51 | return interaction.reply({ 52 | embeds: [ 53 | new EmbedBuilder() 54 | .setColor(client.config.embedColor) 55 | .setDescription(`⏸ | **Paused!**`), 56 | ], 57 | }); 58 | }); 59 | 60 | module.exports = command; 61 | -------------------------------------------------------------------------------- /djs-bot/commands/music/playlists/index.js: -------------------------------------------------------------------------------- 1 | const SlashCommand = require("../../../lib/SlashCommand"); 2 | 3 | module.exports = new SlashCommand() 4 | .setName("playlists") 5 | .setCategory("music") 6 | .setDBMS() 7 | .setDescription("Playlist management") 8 | .setUsage("/playlists [view | create | delete | add | remove | play]") 9 | .setRun(async function(...args) { 10 | return this.handleSubCommandInteraction(...args); 11 | }) -------------------------------------------------------------------------------- /djs-bot/commands/music/previous.js: -------------------------------------------------------------------------------- 1 | const SlashCommand = require("../../lib/SlashCommand"); 2 | const { EmbedBuilder } = require("discord.js"); 3 | const playerUtil = require("../../util/player"); 4 | const { redEmbed } = require("../../util/embeds"); 5 | 6 | const command = new SlashCommand() 7 | .setName("previous") 8 | .setDescription("Go back to the previous song.") 9 | .setRun(async (client, interaction) => { 10 | let channel = await client.getChannel(client, interaction); 11 | if (!channel) { 12 | return; 13 | } 14 | 15 | let player; 16 | if (client.manager.Engine) { 17 | player = client.manager.Engine.players.get(interaction.guild.id); 18 | } else { 19 | return interaction.reply({ 20 | embeds: [ 21 | new EmbedBuilder() 22 | .setColor("Red") 23 | .setDescription("Lavalink node is not connected"), 24 | ], 25 | }); 26 | } 27 | 28 | if (!player) { 29 | return interaction.reply({ 30 | embeds: [ 31 | new EmbedBuilder() 32 | .setColor("Red") 33 | .setDescription("There are no previous songs for this session."), 34 | ], 35 | ephemeral: true, 36 | }); 37 | } 38 | 39 | const previousSong = player.queue.previous; 40 | const status = await playerUtil.playPrevious(player); 41 | 42 | if (status === 1) return interaction.reply({ 43 | embeds: [ 44 | redEmbed({desc: "There is no previous song in the queue."}), 45 | ], 46 | }) 47 | 48 | interaction.reply({ 49 | embeds: [ 50 | new EmbedBuilder() 51 | .setColor(client.config.embedColor) 52 | .setDescription( 53 | `⏮ | Previous song: **${ previousSong.title }**`, 54 | ), 55 | ], 56 | }); 57 | }); 58 | 59 | module.exports = command; 60 | -------------------------------------------------------------------------------- /djs-bot/commands/music/remove.js: -------------------------------------------------------------------------------- 1 | const SlashCommand = require("../../lib/SlashCommand"); 2 | const { EmbedBuilder } = require("discord.js"); 3 | const { removeTrack } = require("../../util/player"); 4 | 5 | const command = new SlashCommand() 6 | .setName("remove") 7 | .setDescription("Remove track you don't want from queue") 8 | .addNumberOption((option) => 9 | option.setName("number").setDescription("Enter track number.").setRequired(true) 10 | ) 11 | 12 | .setRun(async (client, interaction) => { 13 | const args = interaction.options.getNumber("number"); 14 | 15 | let channel = await client.getChannel(client, interaction); 16 | if (!channel) { 17 | return; 18 | } 19 | 20 | let player; 21 | if (client.manager.Engine) { 22 | player = client.manager.Engine.players.get(interaction.guild.id); 23 | } else { 24 | return interaction.reply({ 25 | embeds: [ 26 | new EmbedBuilder() 27 | .setColor("Red") 28 | .setDescription("Lavalink node is not connected"), 29 | ], 30 | }); 31 | } 32 | 33 | if (!player) { 34 | return interaction.reply({ 35 | embeds: [ 36 | new EmbedBuilder() 37 | .setColor("Red") 38 | .setDescription("There are no songs to remove."), 39 | ], 40 | ephemeral: true, 41 | }); 42 | } 43 | 44 | await interaction.deferReply(); 45 | 46 | const position = Number(args) - 1; 47 | if (position > player.queue.size) { 48 | let thing = new EmbedBuilder() 49 | .setColor(client.config.embedColor) 50 | .setDescription( 51 | `Current queue has only **${player.queue.size}** track` 52 | ); 53 | return interaction.editReply({ embeds: [thing] }); 54 | } 55 | 56 | removeTrack(player, position); 57 | 58 | const number = position + 1; 59 | let removeEmbed = new EmbedBuilder() 60 | .setColor(client.config.embedColor) 61 | .setDescription(`Removed track number **${number}** from queue`); 62 | 63 | return interaction.editReply({ embeds: [removeEmbed] }); 64 | }); 65 | 66 | module.exports = command; 67 | -------------------------------------------------------------------------------- /djs-bot/commands/music/replay.js: -------------------------------------------------------------------------------- 1 | const SlashCommand = require("../../lib/SlashCommand"); 2 | const { EmbedBuilder } = require("discord.js"); 3 | 4 | const command = new SlashCommand() 5 | .setName("replay") 6 | .setDescription("Replay current playing track") 7 | .setRun(async (client, interaction, options) => { 8 | let channel = await client.getChannel(client, interaction); 9 | if (!channel) { 10 | return; 11 | } 12 | 13 | let player; 14 | if (client.manager.Engine) { 15 | player = client.manager.Engine.players.get(interaction.guild.id); 16 | } else { 17 | return interaction.reply({ 18 | embeds: [ 19 | new EmbedBuilder() 20 | .setColor("Red") 21 | .setDescription("Lavalink node is not connected"), 22 | ], 23 | }); 24 | } 25 | 26 | if (!player) { 27 | return interaction.reply({ 28 | embeds: [ 29 | new EmbedBuilder() 30 | .setColor("Red") 31 | .setDescription("I'm not playing anything."), 32 | ], 33 | ephemeral: true, 34 | }); 35 | } 36 | 37 | await interaction.deferReply(); 38 | 39 | player.seek(0); 40 | 41 | let song = player.queue.current; 42 | return interaction.editReply({ 43 | embeds: [ 44 | new EmbedBuilder() 45 | .setColor(client.config.embedColor) 46 | .setDescription(`Replay [${ song.title }](${ song.uri })`), 47 | ], 48 | }); 49 | }); 50 | 51 | module.exports = command; 52 | -------------------------------------------------------------------------------- /djs-bot/commands/music/resume.js: -------------------------------------------------------------------------------- 1 | const SlashCommand = require("../../lib/SlashCommand"); 2 | const { EmbedBuilder } = require("discord.js"); 3 | const { pause } = require("../../util/player"); 4 | 5 | const command = new SlashCommand() 6 | .setName("resume") 7 | .setDescription("Resume current track") 8 | .setRun(async (client, interaction, options) => { 9 | let channel = await client.getChannel(client, interaction); 10 | if (!channel) { 11 | return; 12 | } 13 | 14 | let player; 15 | if (client.manager.Engine) { 16 | player = client.manager.Engine.players.get(interaction.guild.id); 17 | } else { 18 | return interaction.reply({ 19 | embeds: [ 20 | new EmbedBuilder() 21 | .setColor("Red") 22 | .setDescription("Lavalink node is not connected"), 23 | ], 24 | }); 25 | } 26 | 27 | if (!player) { 28 | return interaction.reply({ 29 | embeds: [ 30 | new EmbedBuilder() 31 | .setColor("Red") 32 | .setDescription("There is no song playing right now."), 33 | ], 34 | ephemeral: true, 35 | }); 36 | } 37 | 38 | if (!player.paused) { 39 | return interaction.reply({ 40 | embeds: [ 41 | new EmbedBuilder() 42 | .setColor("Red") 43 | .setDescription("Current track is already resumed"), 44 | ], 45 | ephemeral: true, 46 | }); 47 | } 48 | 49 | pause(player, false); 50 | 51 | return interaction.reply({ 52 | embeds: [ 53 | new EmbedBuilder() 54 | .setColor(client.config.embedColor) 55 | .setDescription(`⏯ **Resumed!**`), 56 | ], 57 | }); 58 | }); 59 | 60 | module.exports = command; 61 | -------------------------------------------------------------------------------- /djs-bot/commands/music/save.js: -------------------------------------------------------------------------------- 1 | const SlashCommand = require("../../lib/SlashCommand"); 2 | const { EmbedBuilder } = require("discord.js"); 3 | const prettyMilliseconds = require("pretty-ms"); 4 | 5 | const command = new SlashCommand() 6 | .setName("save") 7 | .setDescription("Saves current song to your DM's") 8 | .setRun(async (client, interaction) => { 9 | let channel = await client.getChannel(client, interaction); 10 | if (!channel) { 11 | return; 12 | } 13 | 14 | let player; 15 | if (client.manager.Engine) { 16 | player = client.manager.Engine.players.get(interaction.guild.id); 17 | } else { 18 | return interaction.reply({ 19 | embeds: [ 20 | new EmbedBuilder() 21 | .setColor("Red") 22 | .setDescription("Lavalink node is not connected"), 23 | ], 24 | }); 25 | } 26 | 27 | if (!player) { 28 | return interaction.reply({ 29 | embeds: [ 30 | new EmbedBuilder() 31 | .setColor("Red") 32 | .setDescription("There is no music playing right now."), 33 | ], 34 | ephemeral: true, 35 | }); 36 | } 37 | 38 | const sendtoDmEmbed = new EmbedBuilder() 39 | .setColor(client.config.embedColor) 40 | .setAuthor({ 41 | name: "Saved track", 42 | iconURL: `${ interaction.user.displayAvatarURL({ dynamic: true }) }`, 43 | }) 44 | .setDescription( 45 | `**Saved [${ player.queue.current.title }](${ player.queue.current.uri }) to your DM**`, 46 | ) 47 | .addFields( 48 | { 49 | name: "Track Duration", 50 | value: `\`${ prettyMilliseconds(player.queue.current.duration, { 51 | colonNotation: true, 52 | }) }\``, 53 | inline: true, 54 | }, 55 | { 56 | name: "Track Author", 57 | value: `\`${ player.queue.current.author }\``, 58 | inline: true, 59 | }, 60 | { 61 | name: "Requested Guild", 62 | value: `\`${ interaction.guild }\``, 63 | inline: true, 64 | }, 65 | ); 66 | 67 | interaction.user.send({ embeds: [sendtoDmEmbed] }); 68 | 69 | return interaction.reply({ 70 | embeds: [ 71 | new EmbedBuilder() 72 | .setColor(client.config.embedColor) 73 | .setDescription( 74 | "Please check your **DMs**. If you didn't receive any message from me please make sure your **DMs** are open", 75 | ), 76 | ], 77 | ephemeral: true, 78 | }); 79 | }); 80 | 81 | module.exports = command; 82 | -------------------------------------------------------------------------------- /djs-bot/commands/music/shuffle.js: -------------------------------------------------------------------------------- 1 | const SlashCommand = require("../../lib/SlashCommand"); 2 | const { EmbedBuilder } = require("discord.js"); 3 | const { shuffleQueue } = require("../../util/player"); 4 | 5 | const command = new SlashCommand() 6 | .setName("shuffle") 7 | .setDescription("Randomizes the queue") 8 | .setRun(async (client, interaction, options) => { 9 | let channel = await client.getChannel(client, interaction); 10 | if (!channel) { 11 | return; 12 | } 13 | 14 | let player; 15 | if (client.manager.Engine) { 16 | player = client.manager.Engine.players.get(interaction.guild.id); 17 | } else { 18 | return interaction.reply({ 19 | embeds: [ 20 | new EmbedBuilder() 21 | .setColor("Red") 22 | .setDescription("Lavalink node is not connected"), 23 | ], 24 | }); 25 | } 26 | 27 | if (!player) { 28 | return interaction.reply({ 29 | embeds: [ 30 | new EmbedBuilder() 31 | .setColor("Red") 32 | .setDescription("There is no music playing."), 33 | ], 34 | ephemeral: true, 35 | }); 36 | } 37 | 38 | if (!player.queue || !player.queue.length || player.queue.length === 0) { 39 | return interaction.reply({ 40 | embeds: [ 41 | new EmbedBuilder() 42 | .setColor("Red") 43 | .setDescription( 44 | "There are not enough songs in the queue." 45 | ), 46 | ], 47 | ephemeral: true, 48 | }); 49 | } 50 | 51 | // if the queue is not empty, shuffle the entire queue 52 | shuffleQueue(player); 53 | return interaction.reply({ 54 | embeds: [ 55 | new EmbedBuilder() 56 | .setColor(client.config.embedColor) 57 | .setDescription( 58 | "🔀 | **Successfully shuffled the queue.**" 59 | ), 60 | ], 61 | }); 62 | }); 63 | 64 | module.exports = command; 65 | -------------------------------------------------------------------------------- /djs-bot/commands/music/skip.js: -------------------------------------------------------------------------------- 1 | const SlashCommand = require("../../lib/SlashCommand"); 2 | const { EmbedBuilder } = require("discord.js"); 3 | const playerUtil = require("../../util/player"); 4 | const { redEmbed } = require("../../util/embeds"); 5 | 6 | const command = new SlashCommand() 7 | .setName("skip") 8 | .setDescription("Skip the current song") 9 | .setRun(async (client, interaction, options) => { 10 | let channel = await client.getChannel(client, interaction); 11 | if (!channel) { 12 | return; 13 | } 14 | 15 | let player; 16 | if (client.manager.Engine) { 17 | player = client.manager.Engine.players.get(interaction.guild.id); 18 | } else { 19 | return interaction.reply({ 20 | embeds: [ 21 | new EmbedBuilder() 22 | .setColor("Red") 23 | .setDescription("Lavalink node is not connected"), 24 | ], 25 | }); 26 | } 27 | 28 | if (!player) { 29 | return interaction.reply({ 30 | embeds: [ 31 | new EmbedBuilder() 32 | .setColor("Red") 33 | .setDescription("There is nothing to skip."), 34 | ], 35 | ephemeral: true, 36 | }); 37 | } 38 | 39 | const song = player.queue.current; 40 | 41 | const status = playerUtil.skip(player); 42 | 43 | if (status === 1) { 44 | return interaction.reply({ 45 | embeds: [ 46 | redEmbed({ 47 | desc: `There is nothing after [${song.title}](${song.uri}) in the queue.` 48 | }), 49 | ], 50 | }); 51 | } 52 | 53 | const ret = await interaction.reply({ 54 | embeds: [ 55 | new EmbedBuilder() 56 | .setColor(client.config.embedColor) 57 | .setDescription("✅ | **Skipped!**"), 58 | ], 59 | fetchReply: true 60 | }); 61 | if (ret) setTimeout(() => ret.delete().catch(client.warn), 20000); 62 | return ret; 63 | }); 64 | 65 | module.exports = command; 66 | -------------------------------------------------------------------------------- /djs-bot/commands/music/skipto.js: -------------------------------------------------------------------------------- 1 | const SlashCommand = require("../../lib/SlashCommand"); 2 | const { EmbedBuilder } = require("discord.js"); 3 | const { removeTrack } = require("../../util/player"); 4 | 5 | const command = new SlashCommand() 6 | .setName("skipto") 7 | .setDescription("skip to a specific song in the queue") 8 | .addNumberOption((option) => 9 | option 10 | .setName("number") 11 | .setDescription("The number of tracks to skipto") 12 | .setRequired(true) 13 | ) 14 | 15 | .setRun(async (client, interaction, options) => { 16 | const args = interaction.options.getNumber("number"); 17 | //const duration = player.queue.current.duration 18 | 19 | let channel = await client.getChannel(client, interaction); 20 | if (!channel) { 21 | return; 22 | } 23 | 24 | let player; 25 | if (client.manager.Engine) { 26 | player = client.manager.Engine.players.get(interaction.guild.id); 27 | } else { 28 | return interaction.reply({ 29 | embeds: [ 30 | new EmbedBuilder() 31 | .setColor("Red") 32 | .setDescription("Lavalink node is not connected"), 33 | ], 34 | }); 35 | } 36 | 37 | if (!player) { 38 | return interaction.reply({ 39 | embeds: [ 40 | new EmbedBuilder() 41 | .setColor("Red") 42 | .setDescription("I'm not in a channel."), 43 | ], 44 | ephemeral: true, 45 | }); 46 | } 47 | 48 | await interaction.deferReply(); 49 | 50 | const position = Number(args); 51 | 52 | try { 53 | if (!position || position < 0 || position > player.queue.size) { 54 | let thing = new EmbedBuilder() 55 | .setColor(client.config.embedColor) 56 | .setDescription("❌ | Invalid position!"); 57 | return interaction.editReply({ embeds: [thing] }); 58 | } 59 | 60 | removeTrack(player, 0, position - 1); 61 | player.stop(); 62 | 63 | let thing = new EmbedBuilder() 64 | .setColor(client.config.embedColor) 65 | .setDescription("✅ | Skipped to position " + position); 66 | 67 | return interaction.editReply({ embeds: [thing] }); 68 | } catch { 69 | if (position === 1) { 70 | player.stop(); 71 | } 72 | return interaction.editReply({ 73 | embeds: [ 74 | new EmbedBuilder() 75 | .setColor(client.config.embedColor) 76 | .setDescription( 77 | "✅ | Skipped to position " + position 78 | ), 79 | ], 80 | }); 81 | } 82 | }); 83 | 84 | module.exports = command; 85 | -------------------------------------------------------------------------------- /djs-bot/commands/music/stop.js: -------------------------------------------------------------------------------- 1 | const SlashCommand = require("../../lib/SlashCommand"); 2 | const { EmbedBuilder } = require("discord.js"); 3 | const playerUtil = require("../../util/player"); 4 | 5 | const command = new SlashCommand() 6 | .setName("stop") 7 | .setDescription("Stops whatever the bot is playing and leaves the voice channel\n(This command will clear the queue)") 8 | 9 | .setRun(async (client, interaction, options) => { 10 | let channel = await client.getChannel(client, interaction); 11 | if (!channel) { 12 | return; 13 | } 14 | 15 | let player; 16 | if (client.manager.Engine) { 17 | player = client.manager.Engine.players.get(interaction.guild.id); 18 | } else { 19 | return interaction.reply({ 20 | embeds: [ 21 | new EmbedBuilder() 22 | .setColor("Red") 23 | .setDescription("Lavalink node is not connected"), 24 | ], 25 | }); 26 | } 27 | 28 | if (!player) { 29 | return interaction.reply({ 30 | embeds: [ 31 | new EmbedBuilder() 32 | .setColor("Red") 33 | .setDescription("I'm not in a channel."), 34 | ], 35 | ephemeral: true, 36 | }); 37 | } 38 | 39 | const status = playerUtil.stop(player); 40 | 41 | const ret = await interaction.reply({ 42 | embeds: [ 43 | new EmbedBuilder() 44 | .setColor(client.config.embedColor) 45 | .setDescription(`:wave: | **Bye Bye!**`), 46 | ], 47 | fetchReply: true 48 | }); 49 | 50 | if (ret) setTimeout(() => ret.delete().catch(client.warn), 20000); 51 | return ret; 52 | }); 53 | 54 | module.exports = command; 55 | -------------------------------------------------------------------------------- /djs-bot/commands/music/summon.js: -------------------------------------------------------------------------------- 1 | const SlashCommand = require("../../lib/SlashCommand"); 2 | const { EmbedBuilder } = require("discord.js"); 3 | 4 | const command = new SlashCommand() 5 | .setName("summon") 6 | .setDescription("Summons the bot to the channel.") 7 | .setRun(async (client, interaction, options) => { 8 | let channel = await client.getChannel(client, interaction); 9 | if (!interaction.member.voice.channel) { 10 | const joinEmbed = new EmbedBuilder() 11 | .setColor(client.config.embedColor) 12 | .setDescription( 13 | "❌ | **You must be in a voice channel to use this command.**", 14 | ); 15 | return interaction.reply({ embeds: [joinEmbed], ephemeral: true }); 16 | } 17 | 18 | let player = client.manager.Engine.players.get(interaction.guild.id); 19 | if (!player) { 20 | player = client.manager.Engine.createPlayer({ 21 | guildId: interaction.guild.id, 22 | voiceChannel: channel.id, 23 | textChannel: interaction.channel.id, 24 | }); 25 | player.connect(true); 26 | } 27 | 28 | if (channel.id !== player.voiceChannel) { 29 | player.setVoiceChannel(channel.id); 30 | player.connect(); 31 | } 32 | 33 | interaction.reply({ 34 | embeds: [ 35 | new EmbedBuilder().setDescription(`:thumbsup: | **Successfully joined <#${ channel.id }>!**`), 36 | ], 37 | }); 38 | }); 39 | 40 | module.exports = command; 41 | -------------------------------------------------------------------------------- /djs-bot/commands/music/volume.js: -------------------------------------------------------------------------------- 1 | const SlashCommand = require("../../lib/SlashCommand"); 2 | const { EmbedBuilder } = require("discord.js"); 3 | 4 | const command = new SlashCommand() 5 | .setName("volume") 6 | .setDescription("Change the volume of the current song.") 7 | .addNumberOption((option) => 8 | option 9 | .setName("amount") 10 | .setDescription("Amount of volume you want to change. Ex: 10") 11 | .setRequired(false), 12 | ) 13 | .setRun(async (client, interaction) => { 14 | let channel = await client.getChannel(client, interaction); 15 | if (!channel) { 16 | return; 17 | } 18 | 19 | let player; 20 | if (client.manager.Engine) { 21 | player = client.manager.Engine.players.get(interaction.guild.id); 22 | } else { 23 | return interaction.reply({ 24 | embeds: [ 25 | new EmbedBuilder() 26 | .setColor("Red") 27 | .setDescription("Lavalink node is not connected"), 28 | ], 29 | }); 30 | } 31 | 32 | if (!player) { 33 | return interaction.reply({ 34 | embeds: [ 35 | new EmbedBuilder() 36 | .setColor("Red") 37 | .setDescription("There is no music playing."), 38 | ], 39 | ephemeral: true, 40 | }); 41 | } 42 | 43 | let vol = interaction.options.getNumber("amount"); 44 | if (!vol || vol < 1 || vol > 125) { 45 | return interaction.reply({ 46 | embeds: [ 47 | new EmbedBuilder() 48 | .setColor(client.config.embedColor) 49 | .setDescription( 50 | `:loud_sound: | Current volume **${ player.volume }**`, 51 | ), 52 | ], 53 | }); 54 | } 55 | 56 | player.setVolume(vol); 57 | return interaction.reply({ 58 | embeds: [ 59 | new EmbedBuilder() 60 | .setColor(client.config.embedColor) 61 | .setDescription( 62 | `:loud_sound: | Successfully set volume to **${ player.volume }**`, 63 | ), 64 | ], 65 | }); 66 | }); 67 | 68 | module.exports = command; 69 | -------------------------------------------------------------------------------- /djs-bot/commands/utility/config/dj-role.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {import("../../../lib/SlashCommand")} baseCommand 3 | */ 4 | module.exports = function djRole(baseCommand) { 5 | baseCommand.addSubcommand((command) => 6 | command 7 | .setName("dj-role") 8 | .setDescription("Set server DJ role") 9 | .addRoleOption((opt) => 10 | opt 11 | .setName("role") 12 | .setDescription( 13 | "Set this role as server DJ role, leave empty to reset" 14 | ) 15 | ) 16 | ); 17 | 18 | return baseCommand.setSubCommandHandler( 19 | "dj-role", 20 | async function (client, interaction, options) { 21 | const role = options.getRole("role", false); 22 | 23 | const guildId = interaction.guild.id; 24 | const roleId = role?.id || null; 25 | 26 | try { 27 | await client.db.guild.upsert({ 28 | where: { 29 | guildId, 30 | }, 31 | create: { DJRole: roleId, guildId }, 32 | update: { DJRole: roleId }, 33 | }); 34 | } catch (e) { 35 | client.error(e); 36 | 37 | return interaction.reply("Error updating config"); 38 | } 39 | 40 | const reply = !roleId ? "DJ Role reset!" : "DJ Role set!"; 41 | 42 | return interaction.reply(reply); 43 | } 44 | ); 45 | }; 46 | -------------------------------------------------------------------------------- /djs-bot/commands/utility/config/index.js: -------------------------------------------------------------------------------- 1 | const SlashCommand = require("../../../lib/SlashCommand"); 2 | 3 | module.exports = new SlashCommand() 4 | .setName("config") 5 | .setCategory("utility") 6 | .setDBMS() 7 | .setDescription("Configure various bot settings") 8 | .setUsage("/config [dj-role | control-channel]") 9 | .setRun(async function(...args) { 10 | return this.handleSubCommandInteraction(...args); 11 | }) 12 | .setPermissions(["Administrator"]); 13 | -------------------------------------------------------------------------------- /djs-bot/commands/utility/nodes.js: -------------------------------------------------------------------------------- 1 | const moment = require("moment"); 2 | const { EmbedBuilder } = require("discord.js") 3 | const Bot = require("../../lib/Bot"); 4 | 5 | module.exports = { 6 | name: "nodes", 7 | category: "utility", 8 | usage: "/nodes", 9 | description: "Check the bot's lavalink node statistics!", 10 | ownerOnly: false, 11 | /** 12 | * 13 | * @param {Bot} client 14 | * @param {import("discord.js").Interaction} interaction 15 | * @returns 16 | */ 17 | run: async (client, interaction) => { 18 | let lavauptime, lavaram, lavaclientstats; 19 | 20 | const statsEmbed = new EmbedBuilder() 21 | .setTitle(`${client.user.username} Nodes Information`) 22 | .setColor(client.config.embedColor) 23 | 24 | if (client.manager) { 25 | for (const [index, lavalinkClient] of client.manager.Engine.nodes.entries()){ 26 | 27 | lavaclientstats = lavalinkClient.stats; 28 | lavacores = lavaclientstats.cpu.cores; 29 | lavauptime = moment.duration(lavaclientstats.uptime).format("d[ Days]・h[ Hrs]・m[ Mins]・s[ Secs]"); 30 | lavaram = (lavaclientstats.memory.used / 1024 / 1024).toFixed(2); 31 | lavalloc = (lavaclientstats.memory.allocated / 1024 / 1024).toFixed(2); 32 | statsEmbed.addFields([{ 33 | name: `${index}`, 34 | value: `\`\`\`yml\nUptime: ${lavauptime}\nRAM: ${lavaram} / ${lavalloc}MB\nCPU: ${(lavacores === 1) ? "1 Core" : `${lavacores} Cores`}\nPlaying: ${lavaclientstats.playingPlayers} out of ${lavaclientstats.players}\n\`\`\``, 35 | }]) 36 | } 37 | } else { 38 | statsEmbed.setDescription("**Lavalink manager was not initialized on startup, there are no nodes connected.**") 39 | } 40 | return interaction.reply({ embeds: [statsEmbed], ephemeral: true }); 41 | }, 42 | }; -------------------------------------------------------------------------------- /djs-bot/commands/utility/ping.js: -------------------------------------------------------------------------------- 1 | const { EmbedBuilder } = require("discord.js"); 2 | 3 | module.exports = { 4 | name: "ping", 5 | category: "utility", 6 | usage: "/ping", 7 | description: 8 | "Is the bot running slow? Check the bot's ping and see if it's lagging or if you are!", 9 | ownerOnly: false, 10 | run: async (client, interaction) => { 11 | const msg = await interaction.channel.send(`🏓 Pinging...`); 12 | await interaction.reply({ 13 | embeds: [ 14 | new EmbedBuilder() 15 | .setTitle(":signal_strength: PONG!") 16 | .addFields([ 17 | { 18 | name: "BOT", 19 | value: `\`\`\`yml\n${Math.floor( 20 | msg.createdAt - 21 | interaction.createdAt 22 | )}ms\`\`\``, 23 | inline: true, 24 | }, 25 | { 26 | name: "API", 27 | value: `\`\`\`yml\n${client.ws.ping}ms\`\`\``, 28 | inline: true, 29 | }, 30 | ]) 31 | .setColor(client.config.embedColor) 32 | .setTimestamp(), 33 | ], 34 | }); 35 | msg.delete(); 36 | }, 37 | }; 38 | -------------------------------------------------------------------------------- /djs-bot/index.js: -------------------------------------------------------------------------------- 1 | // Main file that initializes everything, sets up the shard manager and initializes everything from the `bot.js` 2 | // https://discordjs.guide/sharding/ 3 | // https://discord.com/developers/docs/topics/gateway#sharding 4 | // This is just the shard manager, the actual client is in `bot.js` 5 | // All this isn't really necessary if the bot resides in less than 2000 servers, it was just a 6 | // proof of concept to help the user to understand sharding as well as how to set it up 7 | // 8 | // Bots, even without shard manager, reside on the 0 indexed shard either way 9 | // If you want to remove sharding just delete this file (or move it to the assets folder) 10 | // and rename `bot.js` to `index.js` 11 | 12 | const colors = require("colors"); 13 | const getConfig = require("./util/getConfig"); 14 | const { ShardingManager } = require('discord.js'); 15 | const { getCurrentTimeString } = require("./util/dates"); 16 | 17 | try { 18 | // Gets the config file and passes it (as if returned) to the function in `.then( () => {} )` 19 | getConfig().then((conf) => { 20 | const manager = new ShardingManager('./bot.js', { token: conf.token, respawn: true, totalShards: "auto", timeout: -1 }); 21 | manager.on('shardCreate', shard => { 22 | console.log(colors.gray(getCurrentTimeString()) + colors.cyan(`${' '.repeat(9)}| Spawned shard ${shard.id}`)); 23 | }); 24 | manager.spawn({ amount: "auto", delay: 5500, timeout: -1 }).catch((err) => { 25 | console.log(colors.red("\tError spawning shards: " + err)); 26 | }); 27 | }) 28 | } catch (err) { 29 | console.log(colors.red("Error: " + err)); 30 | } 31 | -------------------------------------------------------------------------------- /djs-bot/interactions/autoqueue.js: -------------------------------------------------------------------------------- 1 | const SlashCommand = require("../lib/SlashCommand"); 2 | const { ccInteractionHook } = require("../util/interactions"); 3 | const { updateControlMessage } = require("../util/controlChannel"); 4 | 5 | const command = new SlashCommand() 6 | .setName("autoqueue") 7 | .setCategory("cc") 8 | .setDescription("Autoqueue interaction") 9 | .setRun(async (client, interaction, options) => { 10 | const { error, data } = await ccInteractionHook(client, interaction); 11 | 12 | if (error || !data || data instanceof Promise) return data; 13 | 14 | const { player, channel, sendError } = data; 15 | 16 | const autoQueue = player.get("autoQueue"); 17 | player.set("requester", interaction.guild.members.me); 18 | 19 | if (!autoQueue || autoQueue === false) { 20 | player.set("autoQueue", true); 21 | } else { 22 | player.set("autoQueue", false); 23 | } 24 | 25 | const currentTrack = player.queue.current; 26 | if (currentTrack) updateControlMessage(interaction.guildId, currentTrack); 27 | 28 | return interaction.deferUpdate(); 29 | }); 30 | 31 | module.exports = command; 32 | -------------------------------------------------------------------------------- /djs-bot/interactions/next.js: -------------------------------------------------------------------------------- 1 | const SlashCommand = require("../lib/SlashCommand"); 2 | const { ccInteractionHook } = require("../util/interactions"); 3 | const playerUtil = require("../util/player"); 4 | const { redEmbed } = require("../util/embeds"); 5 | 6 | const command = new SlashCommand() 7 | .setName("next") 8 | .setCategory("cc") 9 | .setDescription("Next interaction") 10 | .setRun(async (client, interaction, options) => { 11 | const { error, data } = await ccInteractionHook(client, interaction); 12 | 13 | if (error || !data || data instanceof Promise) return data; 14 | 15 | const { player, channel, sendError } = data; 16 | 17 | const song = player.queue.current; 18 | const status = playerUtil.skip(player); 19 | 20 | if (status === 1) { 21 | return interaction.reply({ 22 | embeds: [ 23 | redEmbed({ 24 | desc: `There is nothing after [${song.title}](${song.uri}) in the queue.`, 25 | }), 26 | ], 27 | ephemeral: true, 28 | }); 29 | } 30 | 31 | return interaction.deferUpdate(); 32 | }); 33 | 34 | module.exports = command; 35 | -------------------------------------------------------------------------------- /djs-bot/interactions/playpause.js: -------------------------------------------------------------------------------- 1 | const SlashCommand = require("../lib/SlashCommand"); 2 | const { ccInteractionHook } = require("../util/interactions"); 3 | const { pause } = require("../util/player"); 4 | 5 | const command = new SlashCommand() 6 | .setName("playpause") 7 | .setCategory("cc") 8 | .setDescription("Play and Pause interaction") 9 | .setRun(async (client, interaction, options) => { 10 | const { error, data } = await ccInteractionHook(client, interaction); 11 | 12 | if (error || !data || data instanceof Promise) return data; 13 | 14 | const { player, channel, sendError } = data; 15 | 16 | if (player.paused) { 17 | pause(player, false); 18 | } else pause(player,true); 19 | 20 | return interaction.deferUpdate(); 21 | }); 22 | 23 | module.exports = command; 24 | -------------------------------------------------------------------------------- /djs-bot/interactions/prev.js: -------------------------------------------------------------------------------- 1 | const SlashCommand = require("../lib/SlashCommand"); 2 | const { redEmbed } = require("../util/embeds"); 3 | const { ccInteractionHook } = require("../util/interactions"); 4 | const playerUtil = require("../util/player"); 5 | 6 | const command = new SlashCommand() 7 | .setName("prev") 8 | .setCategory("cc") 9 | .setDescription("Prev interaction") 10 | .setRun(async (client, interaction, options) => { 11 | const { error, data } = await ccInteractionHook(client, interaction); 12 | 13 | if (error || !data || data instanceof Promise) return data; 14 | 15 | const { player, channel, sendError } = data; 16 | 17 | const status = await playerUtil.playPrevious(player); 18 | 19 | if (status === 1) 20 | return interaction.reply({ 21 | embeds: [ 22 | redEmbed({ 23 | desc: "There is no previous song in the queue.", 24 | }), 25 | ], 26 | ephemeral: true, 27 | }); 28 | 29 | return interaction.deferUpdate(); 30 | }); 31 | 32 | module.exports = command; 33 | -------------------------------------------------------------------------------- /djs-bot/interactions/shuffle.js: -------------------------------------------------------------------------------- 1 | const SlashCommand = require("../lib/SlashCommand"); 2 | const { colorEmbed } = require("../util/embeds"); 3 | const { ccInteractionHook } = require("../util/interactions"); 4 | const playerUtil = require("../util/player"); 5 | 6 | const command = new SlashCommand() 7 | .setName("shuffle") 8 | .setCategory("cc") 9 | .setDescription("Shuffle interaction") 10 | .setRun(async (client, interaction, options) => { 11 | const { error, data } = await ccInteractionHook(client, interaction, { 12 | minimumQueueLength: 1, 13 | }); 14 | 15 | if (error || !data || data instanceof Promise) return data; 16 | 17 | const { player, channel, sendError } = data; 18 | 19 | playerUtil.shuffleQueue(player); 20 | 21 | return interaction.reply({ 22 | embeds: [colorEmbed({ desc: "🔀 | **Successfully shuffled the queue.**" })], 23 | ephemeral: true, 24 | }); 25 | }); 26 | 27 | module.exports = command; 28 | -------------------------------------------------------------------------------- /djs-bot/interactions/stop.js: -------------------------------------------------------------------------------- 1 | const SlashCommand = require("../lib/SlashCommand"); 2 | const { ccInteractionHook } = require("../util/interactions"); 3 | const playerUtil = require("../util/player"); 4 | 5 | const command = new SlashCommand() 6 | .setName("stop") 7 | .setCategory("cc") 8 | .setDescription("Stop interaction") 9 | .setRun(async (client, interaction, options) => { 10 | const { error, data } = await ccInteractionHook(client, interaction); 11 | 12 | if (error || !data || data instanceof Promise) return data; 13 | 14 | const { player, channel, sendError } = data; 15 | 16 | const status = playerUtil.stop(player); 17 | 18 | return interaction.deferUpdate(); 19 | }); 20 | 21 | module.exports = command; 22 | -------------------------------------------------------------------------------- /djs-bot/interactions/vlouder.js: -------------------------------------------------------------------------------- 1 | const SlashCommand = require("../lib/SlashCommand"); 2 | const { ccInteractionHook, checkPlayerVolume } = require("../util/interactions"); 3 | 4 | const command = new SlashCommand() 5 | .setName("vlouder") 6 | .setCategory("cc") 7 | .setDescription("Louder Volume interaction") 8 | .setRun(async (client, interaction, options) => { 9 | const { error, data } = await ccInteractionHook(client, interaction); 10 | 11 | if (error || !data || data instanceof Promise) return data; 12 | 13 | const { player, channel, sendError } = data; 14 | 15 | // this probably will never run but just in case 16 | const replied = await checkPlayerVolume(player, interaction); 17 | if (replied) return replied; 18 | 19 | let vol = player.volume + 20; 20 | if (vol > 125) vol = 125; 21 | 22 | player.setVolume(vol); 23 | 24 | return interaction.deferUpdate(); 25 | }); 26 | 27 | module.exports = command; 28 | -------------------------------------------------------------------------------- /djs-bot/interactions/vlower.js: -------------------------------------------------------------------------------- 1 | const SlashCommand = require("../lib/SlashCommand"); 2 | const { ccInteractionHook, checkPlayerVolume } = require("../util/interactions"); 3 | 4 | const command = new SlashCommand() 5 | .setName("vlower") 6 | .setCategory("cc") 7 | .setDescription("Lower Volume interaction") 8 | .setRun(async (client, interaction, options) => { 9 | const { error, data } = await ccInteractionHook(client, interaction); 10 | 11 | if (error || !data || data instanceof Promise) return data; 12 | 13 | const { player, channel, sendError } = data; 14 | 15 | // this probably will never run but just in case 16 | const replied = await checkPlayerVolume(player, interaction); 17 | if (replied) return replied; 18 | 19 | let vol = player.volume - 20; 20 | if (vol < 0) vol = 0; 21 | 22 | player.setVolume(vol); 23 | 24 | return interaction.deferUpdate(); 25 | }); 26 | 27 | module.exports = command; 28 | -------------------------------------------------------------------------------- /djs-bot/lib/DBMS.js: -------------------------------------------------------------------------------- 1 | const Bot = require('./Bot'); 2 | 3 | const { PrismaClient } = require('@prisma/client'); 4 | 5 | class PrismaManager extends PrismaClient { 6 | /** 7 | * DataBase Management System (DBMS) wrapper for the Prisma ORM 8 | * @param {Bot} client 9 | */ 10 | constructor(client) { 11 | let logLevels = ["warn", "error"]; 12 | switch (client.OPLevel) { 13 | case 2: 14 | logLevels.push("query"); 15 | case 1: 16 | logLevels.push("info"); 17 | } 18 | super({ 19 | log: logLevels, errorFormat: "pretty", 20 | datasources: { db: { url: client.config.db_url } } //- not really needed but it's here for reference 21 | }); 22 | client.log(`Prisma ORM has been loaded`); 23 | } 24 | } 25 | 26 | module.exports = PrismaManager; 27 | -------------------------------------------------------------------------------- /djs-bot/lib/MusicEvents.d.ts: -------------------------------------------------------------------------------- 1 | import { CosmiTrack } from "cosmicord.js"; 2 | import { Track } from "erela.js"; 3 | import { CosmicordPlayerExtended } from "../lib/clients/MusicClient"; 4 | import { VoiceState } from "discord.js"; 5 | 6 | export type IUsingPlayer = CosmicordPlayerExtended; 7 | 8 | export interface IHandleStopParams { 9 | player: IUsingPlayer; 10 | } 11 | 12 | export interface IHandleTrackStartParams { 13 | player: IUsingPlayer; 14 | track: CosmiTrack | Track; 15 | } 16 | 17 | export interface IHandleQueueUpdateParams { 18 | guildId: string; 19 | player: IUsingPlayer; 20 | } 21 | 22 | export interface IHandlePauseParams { 23 | player: IUsingPlayer; 24 | state: boolean; 25 | } 26 | 27 | export function handleStop(params: IHandleStopParams): void; 28 | 29 | export function handleTrackStart(params: IHandleTrackStartParams): void; 30 | 31 | export function handleQueueUpdate(params: IHandleQueueUpdateParams): void; 32 | 33 | export function updateProgress(params: IHandleTrackStartParams): void; 34 | 35 | export function stopProgressUpdater(guildId: string): void; 36 | 37 | export function handleVoiceStateUpdate(oldState: VoiceState, newState: VoiceState): void; 38 | 39 | export function handlePause(params: IHandlePauseParams): void; 40 | -------------------------------------------------------------------------------- /djs-bot/lib/MusicManager.js: -------------------------------------------------------------------------------- 1 | const Bot = require("./Bot"); 2 | const fs = require("fs"); 3 | 4 | /** 5 | * @property { import("./clients/MusicClient.d.ts").MusicClient } Engine 6 | */ 7 | class MusicManager { 8 | 9 | /** 10 | * Music Manager 11 | * ============= 12 | * The class is used as an interface with all the music clients in the bot's client folder 13 | * acts as an API for the music clients and allows for easy switching between clients 14 | * @param {Bot} client 15 | * @returns {MusicManager} 16 | */ 17 | constructor(client) { 18 | const clients = fs.readdirSync("./lib/clients").filter((file) => file.endsWith(".js")).map((file) => file.split(".")[0]); 19 | const specifiedEngine = client.config.musicEngine; 20 | 21 | // check if the music engine is valid 22 | if (!specifiedEngine) throw new Error("Music engine is not specified in the config file"); 23 | if (!clients.includes(specifiedEngine)) throw new Error(`Music engine "${specifiedEngine}" does not exist`); 24 | 25 | /** @type { import("./clients/MusicClient").MusicClient } */ 26 | this.Engine = require(`./clients/${specifiedEngine}`)(client); 27 | 28 | // validate the music engine 29 | if (!this.Engine || !(this.Engine instanceof Object)) { 30 | throw new Error(`Music engine "${specifiedEngine}" wasn't loaded correctly`); 31 | } 32 | 33 | client.log(`Music engine "${specifiedEngine}" has been loaded`); 34 | } 35 | } 36 | 37 | module.exports = MusicManager; 38 | -------------------------------------------------------------------------------- /djs-bot/lib/clients/MusicClient.d.ts: -------------------------------------------------------------------------------- 1 | import Bot from "../Bot"; 2 | import { Message } from "discord.js"; 3 | import { 4 | CosmiNode, 5 | CosmiPlayerOptions, 6 | CosmiSearchQuery, 7 | Cosmicord, 8 | CosmiLoadedTracks, 9 | CosmiPlayer, 10 | } from "cosmicord.js"; 11 | 12 | export interface CosmicordPlayerExtended extends CosmiPlayer { 13 | search(query: CosmiSearchQuery, requesterId?: string): Promise; 14 | setResumeMessage(client: Bot, message: Message): Message; 15 | setPausedMessage(client: Bot, message: Message): Message; 16 | setNowplayingMessage(client: Bot, message: Message): Message; 17 | 18 | /** The guild id of the player */ 19 | get guild(): string; 20 | } 21 | 22 | // this interface is confusing looking at its usage as `bot.manager` 23 | export interface MusicClient extends Cosmicord { 24 | // `this` is wrong and only here for quick type workaround, also why does it extends Cosmicord 25 | // !TODO: declare proper type for these extended class 26 | Engine: this; // CosmicordExtended | ErelaExtended; 27 | 28 | createPlayer(options: CosmiPlayerOptions, node?: CosmiNode): CosmicordPlayerExtended; 29 | 30 | get leastUsedNode(): CosmiNode; 31 | } 32 | 33 | export enum Engine { 34 | "Cosmicord" = "Cosmicord", 35 | "Erela" = "Erela", 36 | } 37 | -------------------------------------------------------------------------------- /djs-bot/loaders/events/interactionCreate.js: -------------------------------------------------------------------------------- 1 | const { Interaction } = require("discord.js"); 2 | const Bot = require("../../lib/Bot"); 3 | const SlashCommand = require("../../lib/SlashCommand"); 4 | const controlChannel = require("../../util/controlChannel"); 5 | 6 | // Defines whenever a "interactionCreate" event is fired, basically whenever a user writes a slash command in 7 | // a server in which the bot is present 8 | 9 | // node_modules\discord.js\typings\index.d.ts:3971 10 | // @interactionCreate: [interaction: Interaction]; 11 | // This module checks some properties of the command and determines if it should be ran for that user or not 12 | /** 13 | * 14 | * @param {Bot} client 15 | * @param {Interaction} interaction 16 | * @returns {Promise>} 17 | */ 18 | module.exports = async (client, interaction) => { 19 | const isComponentInteraction = await SlashCommand.handleComponentInteraction(interaction); 20 | if (isComponentInteraction) return isComponentInteraction; 21 | 22 | const isAutocomplete = await SlashCommand.checkAutocomplete(interaction); 23 | if (isAutocomplete) return isAutocomplete; 24 | 25 | // Gets general info from a command during execution, if sent then check the guards 26 | // run only if everything is valid 27 | if (interaction.isCommand()) { 28 | const isPrevented = await controlChannel.preventInteraction(interaction); 29 | if (isPrevented) return isPrevented; 30 | 31 | /** @type {SlashCommand} */ 32 | const command = client.slash.get(interaction.commandName); 33 | if (!command || !command.run) { 34 | return interaction.reply( 35 | "Sorry the command you used doesn't have any run function" 36 | ); 37 | } 38 | 39 | const replied = await SlashCommand.checkConfigs(command, interaction); 40 | if (replied) return replied; 41 | 42 | try { 43 | command.run(client, interaction, interaction.options); 44 | } catch (err) { 45 | interaction[interaction.replied ? "editReply" : "reply"]({ 46 | content: err.message, 47 | }); 48 | } 49 | } 50 | }; 51 | -------------------------------------------------------------------------------- /djs-bot/loaders/events/messageCreate.js: -------------------------------------------------------------------------------- 1 | const { EmbedBuilder, Message } = require("discord.js"); 2 | const Bot = require("../../lib/Bot"); 3 | const { handleMessageCreate } = require("../../util/controlChannelEvents"); 4 | 5 | // node_modules\discord.js\typings\index.d.ts:3940 6 | // @messageCreate: [message: Message]; 7 | /** 8 | * 9 | * @param {Bot} client 10 | * @param {Message} message 11 | * @returns {Promise>} 12 | */ 13 | module.exports = async (client, message) => { 14 | const mention = new RegExp(`^<@!?${client.user.id}>( |)$`); 15 | // Checks if, on every message sent in a server in which the bot is in, the bot is being mentioned and 16 | // determines if it should behave in a manner or another according to if the user is a bot dev or not 17 | if (message.content.match(mention)) { 18 | let timeout; 19 | let embed = new EmbedBuilder().setColor(client.config.embedColor); 20 | if (client.config.ownerId.includes(message.author.id)) { 21 | timeout = 10000; 22 | embed.setTitle("Reinvite").setURL( 23 | `https://discord.com/oauth2/authorize?client_id=${ 24 | client.config.clientId 25 | }&permissions=${ 26 | client.config.permissions 27 | }&scope=${client.config.scopes.toString().replace(/,/g, "%20")}` 28 | ); 29 | } else { 30 | timeout = 15000; 31 | embed.setDescription( 32 | `To use my commands use the \`/\` (Slash Command).\nTo see a list of the available commands type \`/help\`.\nIf you can't see the list, make sure you're using me in the appropriate channels. If you have trouble please contact a server Mod.` 33 | ).setThumbnail(`${client.config.iconURL}`); 34 | } 35 | embed.setFooter({ text: `Message will be deleted in ${timeout / 1000} seconds` }); 36 | return message.channel 37 | .send({ embeds: [embed], ephemeral: true }) 38 | .then((msg) => setTimeout(() => msg.delete(), timeout)); 39 | } 40 | 41 | handleMessageCreate(message); 42 | }; 43 | -------------------------------------------------------------------------------- /djs-bot/loaders/events/messageDelete.js: -------------------------------------------------------------------------------- 1 | const { Message } = require("discord.js"); 2 | const Bot = require("../../lib/Bot"); 3 | const controlChannel = require("../../util/controlChannel"); 4 | 5 | // node_modules\discord.js\typings\index.d.ts:3940 6 | // @messageCreate: [message: Message]; 7 | /** 8 | * 9 | * @param {Bot} client 10 | * @param {Message} message 11 | * @returns {Promise>} 12 | */ 13 | module.exports = async (client, message) => { 14 | controlChannel.handleMessageDelete(message); 15 | }; 16 | -------------------------------------------------------------------------------- /djs-bot/loaders/events/messageDeleteBulk.js: -------------------------------------------------------------------------------- 1 | const { Message } = require("discord.js"); 2 | const Bot = require("../../lib/Bot"); 3 | const controlChannel = require("../../util/controlChannel"); 4 | 5 | // node_modules\discord.js\typings\index.d.ts:3940 6 | // @messageCreate: [message: Message]; 7 | /** 8 | * 9 | * @param {Bot} client 10 | * @param {Message} message 11 | * @returns {Promise>} 12 | */ 13 | module.exports = async (client, messages) => { 14 | for (const [k, v] of messages) 15 | await controlChannel.handleMessageDelete(v); 16 | }; 17 | -------------------------------------------------------------------------------- /djs-bot/loaders/events/raw.js: -------------------------------------------------------------------------------- 1 | // 4153 constant events 2 | // (node_modules\discord.js\typings\rawDataTypes.d.ts) 3 | 4 | const { RawActivityData } = require("discord.js"); 5 | const Bot = require("../../lib/Bot"); 6 | 7 | // raw data, meaning it can be any of the emitted events from the API 8 | // functions as a general handler for any event 9 | 10 | /* It processes data of this structure 11 | data structure: { 12 | t: the event name for this payload (type) 13 | s: sequence number, used for resuming sessions and heartbeats (number of the event) 14 | op: opcode for the payload () 15 | d: event data 16 | } 17 | 18 | OPCODES: 19 | 0 Dispatch Receive An event was dispatched. 20 | 1 Heartbeat Send/Receive Fired periodically by the client to keep the connection alive. 21 | 2 Identify Send Starts a new session during the initial handshake. 22 | 3 Presence Update Send Update the client's presence. 23 | 4 Voice State Update Send Used to join/leave or move between voice channels. 24 | 6 Resume Send Resume a previous session that was disconnected. 25 | 7 Reconnect Receive You should attempt to reconnect and resume immediately. 26 | 8 Request Guild Members Send Request information about offline guild members in a large guild. 27 | 9 Invalid Session Receive The session has been invalidated. You should reconnect and identify/resume accordingly. 28 | 10 Hello Receive Sent immediately after connecting, contains the heartbeat_interval to use. 29 | 11 Heartbeat ACK Receive Sent in response to receiving a heartbeat to acknowledge that it has been received. 30 | 31 | Heartbeat regex: ^\{[^}]*\}","[^"]*":"\[[^\]]*\]"\}$ 32 | */ 33 | const exceptions = [11, 1]; 34 | 35 | /** 36 | * 37 | * @param {Bot} client 38 | * @param {RawActivityData} data 39 | */ 40 | module.exports = (client, data) => { 41 | if ((client.OPLevel >= 2) && !exceptions.includes(parseInt(data.op))) 42 | client.debug("> rawData", JSON.stringify(data, null, 4)) 43 | }; 44 | -------------------------------------------------------------------------------- /djs-bot/loaders/events/ready.js: -------------------------------------------------------------------------------- 1 | const { ActivityType } = require("discord.js"); 2 | const { capitalize, format } = require("../../util/string"); 3 | const Bot = require("../../lib/Bot"); 4 | // this fires once on the bot being launched, sets the presence for the bot 5 | 6 | /** 7 | * 8 | * @param {Bot} client 9 | */ 10 | module.exports = (client) => { 11 | clearTimeout(client.loginTimer); 12 | client.loginTimer = null; 13 | 14 | const activities = client.config.presence.activities; 15 | setInterval(() => { 16 | const index = Math.floor(Math.random() * activities.length); 17 | 18 | let data = {}; 19 | try { 20 | data = activities[index].data(client); 21 | } catch (error) {} 22 | 23 | client.user.setActivity({ 24 | name: format(activities[index].name, data), 25 | type: ActivityType[capitalize(activities[index].type)], 26 | }); 27 | }, 10000); 28 | 29 | // Express API 30 | client.api.listen({ host: "0.0.0.0", port: client.config.api.port }, (err, address) => { 31 | if (err) { 32 | client.error("Can't start API:"); 33 | client.error(err); 34 | return; 35 | } 36 | 37 | client.info(`API is now listening on port ${client.config.api.port}`); 38 | }); 39 | 40 | client.wsServer.listen(client.config.ws.port, (sock) => { 41 | if (sock) { 42 | client.info(`WS is now listening on port ${client.config.ws.port}`); 43 | return; 44 | } 45 | 46 | client.error(new Error("Can't start WS")); 47 | }); 48 | 49 | client.info("Successfully logged in as " + client.user.tag); 50 | }; 51 | -------------------------------------------------------------------------------- /djs-bot/loaders/events/voiceStateUpdate.js: -------------------------------------------------------------------------------- 1 | const musicEvents = require("../../lib/MusicEvents"); 2 | 3 | module.exports = (client, oldState, newState) => { 4 | musicEvents.handleVoiceStateUpdate(oldState, newState); 5 | }; 6 | -------------------------------------------------------------------------------- /djs-bot/loaders/schedules/rmLogs.js: -------------------------------------------------------------------------------- 1 | const cron = require('cron'); 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | const Bot = require('../../lib/Bot'); 5 | 6 | // https://crontab.guru/ 7 | // https://cronitor.io/cron-reference?utm_source=crontabguru&utm_campaign=cron_reference 8 | // https://cron-job.org/en/ 9 | // https://it.wikipedia.org/wiki/Crontab 10 | 11 | // module is just a cron job which gets loaded on startup and executes on cron tab syntax `0 0 * * 0` 12 | // clears the `logs.log` file every day at midnight (00:00) 13 | 14 | // TODO: Archive logs.log instead of deleting it completely 15 | /** 16 | * @param {Bot} client 17 | */ 18 | module.exports = (client) => { 19 | const rmLogsSchedule = new cron.CronJob('0 0 * * *', async () => { 20 | // Relative Path: "../logs.log" 21 | const logsPath = path.join(__dirname, "..", "..", "logs.log"); 22 | fs.writeFile(logsPath, '', function () { client.info("'logs.log' has been purged.") }) 23 | }); 24 | rmLogsSchedule.start() 25 | }; -------------------------------------------------------------------------------- /djs-bot/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "", 3 | "main": "index.js", 4 | "version": "5.5.1", 5 | "name": "discord-musicbot", 6 | "scripts": { 7 | "guild": "npm run api-build && node scripts/guild", 8 | "deploy": "npm run api-build && node scripts/global", 9 | "destroy": "node scripts/destroy", 10 | "update": "node scripts/update", 11 | "bot": "npm run api-build && node index.js", 12 | "start": "npm ci && npm run bot", 13 | "db-start": "npm ci && node scripts/DBScript.js && npm run bot", 14 | "api-build": "tsc -p api/v1/tsconfig.json", 15 | "api-start": "node api/v1/dist/standalone.js" 16 | }, 17 | "keywords": [ 18 | "music", 19 | "discord", 20 | "discord-bot", 21 | "discord-music", 22 | "discord-musicbot" 23 | ], 24 | "author": "SudhanPlayz", 25 | "license": "CUSTOM", 26 | "dependencies": { 27 | "@fastify/cors": "^8.5.0", 28 | "@prisma/client": "^5.9.1", 29 | "axios": "^1.6.7", 30 | "better-erela.js-apple": "^1.0.5", 31 | "better-erela.js-spotify": "^1.3.11", 32 | "colors": "^1.4.0", 33 | "cors": "^2.8.5", 34 | "cosmicord.js": "^1.1.0", 35 | "cron": "^2.4.4", 36 | "discord.js": "^14.15.2", 37 | "dotenv": "^16.4.2", 38 | "erela.js": "^2.4.0", 39 | "erela.js-deezer": "^1.0.7", 40 | "express": "^4.18.2", 41 | "fastify": "^4.26.0", 42 | "fuzzysort": "^2.0.4", 43 | "jsonwebtoken": "^9.0.2", 44 | "llyrics": "^1.0.8", 45 | "moment": "^2.30.1", 46 | "moment-duration-format": "^2.3.2", 47 | "pretty-ms": "^7.0.1", 48 | "prisma": "^5.9.1", 49 | "shoukaku": "^3.4.2", 50 | "songcard": "^1.2.0", 51 | "uWebSockets.js": "github:uNetworking/uWebSockets.js#v20.33.0", 52 | "winston": "^3.11.0", 53 | "youtube-sr": "^4.3.10" 54 | }, 55 | "repository": { 56 | "type": "git", 57 | "url": "git+https://github.com/SudhanPlayz/Discord-MusicBot.git" 58 | }, 59 | "devDependencies": { 60 | "@tsconfig/node16": "^16.1.1", 61 | "@types/jsonwebtoken": "^9.0.5", 62 | "@types/node": "^20.11.17", 63 | "typescript": "^5.3.3" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /djs-bot/prisma/mongodb.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | generator client { 5 | provider = "prisma-client-js" 6 | 7 | // Allows the Prisma ORM to be built for your native platform and for docker platform 8 | binaryTargets = ["native", "debian-openssl-1.1.x", "debian-openssl-3.0.x"] 9 | } 10 | 11 | datasource db { 12 | provider = "mongodb" 13 | url = env("DATABASE_URL") 14 | } 15 | 16 | model Song { 17 | id String @id @default(auto()) @map("_id") @db.ObjectId 18 | createdAt DateTime @default(now()) 19 | updatedAt DateTime @updatedAt 20 | name String 21 | link String 22 | artist String? 23 | Playlist Playlist? @relation(fields: [playlistId], references: [id]) 24 | playlistId String @db.ObjectId 25 | } 26 | 27 | model Playlist { 28 | id String @id @default(auto()) @map("_id") @db.ObjectId 29 | createdAt DateTime @default(now()) 30 | updatedAt DateTime @updatedAt 31 | name String 32 | user User @relation(fields: [userId], references: [id]) 33 | userId String @db.ObjectId 34 | songs Song[] 35 | } 36 | 37 | model User { 38 | id String @id @default(auto()) @map("_id") @db.ObjectId 39 | createdAt DateTime @default(now()) 40 | updatedAt DateTime @updatedAt 41 | username String @unique 42 | playlists Playlist[] 43 | } 44 | 45 | model Guild { 46 | id String @id @default(auto()) @map("_id") @db.ObjectId 47 | createdAt DateTime @default(now()) 48 | updatedAt DateTime @updatedAt 49 | guildId String @unique 50 | DJRole String? 51 | controlChannelId String? 52 | controlChannelMessageId String? 53 | } 54 | 55 | model UserAuth { 56 | id String @id @default(auto()) @map("_id") @db.ObjectId 57 | createdAt DateTime @default(now()) 58 | updatedAt DateTime @updatedAt 59 | userId String @unique 60 | access_token String 61 | token_type String 62 | expires_in Int 63 | refresh_token String 64 | scope String 65 | } 66 | 67 | -------------------------------------------------------------------------------- /djs-bot/prisma/postgresql.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | generator client { 5 | provider = "prisma-client-js" 6 | 7 | // Allows the Prisma ORM to be built for your native platform and for docker platform 8 | binaryTargets = ["native", "debian-openssl-1.1.x", "debian-openssl-3.0.x"] 9 | } 10 | 11 | datasource db { 12 | provider = "postgresql" 13 | url = env("DATABASE_URL") 14 | } 15 | 16 | model Song { 17 | id Int @id @default(autoincrement()) 18 | createdAt DateTime @default(now()) 19 | updatedAt DateTime @updatedAt 20 | name String 21 | link String 22 | artist String? 23 | Playlist Playlist? @relation(fields: [playlistId], references: [id]) 24 | playlistId Int? 25 | } 26 | 27 | model Playlist { 28 | id Int @id @default(autoincrement()) 29 | createdAt DateTime @default(now()) 30 | updatedAt DateTime @updatedAt 31 | name String 32 | user User @relation(fields: [userId], references: [id]) 33 | userId String 34 | songs Song[] 35 | } 36 | 37 | model User { 38 | id String @id 39 | createdAt DateTime @default(now()) 40 | updatedAt DateTime @updatedAt 41 | username String @unique 42 | playlists Playlist[] 43 | } 44 | 45 | model Guild { 46 | id Int @id @default(autoincrement()) 47 | createdAt DateTime @default(now()) 48 | updatedAt DateTime @updatedAt 49 | guildId String @unique 50 | DJRole String? 51 | controlChannelId String? 52 | controlChannelMessageId String? 53 | } 54 | 55 | model UserAuth { 56 | id Int @id @default(autoincrement()) 57 | createdAt DateTime @default(now()) 58 | updatedAt DateTime @updatedAt 59 | userId String @unique 60 | access_token String 61 | token_type String 62 | expires_in Int 63 | refresh_token String 64 | scope String 65 | } 66 | -------------------------------------------------------------------------------- /djs-bot/scripts/DBScript.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const path = require("path"); 3 | const { execSync } = require("child_process"); 4 | 5 | const configPath = path.join(__dirname, "..", "config.js"); 6 | const config = require(configPath); 7 | 8 | const schemasPath = path.join(__dirname, "..", "prisma"); 9 | 10 | const schemas = fs.readdirSync(schemasPath).filter((file) => file.endsWith(".prisma")).map((file) => file.split(".")[0]); 11 | const schema = config.database; 12 | 13 | // check if the schema is valid 14 | if (!schema || schema == "") throw new Error("Schema was not specified in the config file"); 15 | if (!schemas.includes(schema)) throw new Error(`Schema "${schema}" does not exist`); 16 | console.log(`Using "${schema}" as the database schema`); 17 | 18 | function managePrisma(command) { 19 | const cmd = "npx prisma", 20 | args = [command, `--schema=${path.join(__dirname, "..", "prisma", `${schema}.prisma`)}`]; 21 | console.log(`Running command: ${cmd} ${args.join(" ")}`); 22 | execSync(`${cmd} ${args.join(" ")}`); 23 | } 24 | 25 | // execute the generate command 26 | try { 27 | console.log("Generating Prisma client..."); 28 | managePrisma("generate"); 29 | } catch (error) { 30 | console.error(error); 31 | throw new Error("Error generating Prisma client"); 32 | } 33 | 34 | // push the schema to the database 35 | try { 36 | console.log("Pushing schema to database..."); 37 | managePrisma("db push"); 38 | } catch (error) { 39 | console.error(error); 40 | throw new Error("Error pushing schema to database"); 41 | } 42 | console.log("Database schema pushed successfully"); 43 | -------------------------------------------------------------------------------- /djs-bot/scripts/destroy.js: -------------------------------------------------------------------------------- 1 | const { rl } = require("../util/common"); 2 | const { REST } = require("@discordjs/rest"); 3 | const getConfig = require("../util/getConfig"); 4 | const { Routes } = require("discord-api-types/v10"); 5 | 6 | // Removes all slash commands from a given guild (shell command `node "./deploy/destroy"`) 7 | // Basically a reverse `guild.js`, instead of 'put' it's 'delete' iykyk 8 | // https://github.com/discordjs/discord.js/tree/main/packages/rest 9 | // https://github.com/discordjs/discord-api-types/ 10 | (async () => { 11 | const config = await getConfig(); 12 | const rest = new REST({ version: "10" }).setToken(config.token); 13 | 14 | rl.question("Enter the guild id you want to delete commands in: ", async (guild) => { 15 | console.log("Bot has started to delete commands..."); 16 | await rest.put( Routes.applicationGuildCommands(config.clientId, guild), { body: [] }); 17 | console.log("Done"); 18 | return rl.close(); 19 | }); 20 | })(); 21 | -------------------------------------------------------------------------------- /djs-bot/scripts/global.js: -------------------------------------------------------------------------------- 1 | const { REST } = require("@discordjs/rest"); 2 | const getConfig = require("../util/getConfig"); 3 | const { Routes } = require("discord-api-types/v10"); 4 | const { writeFile } = require("fs"); 5 | const { join } = require("path"); 6 | const Bot = require("../lib/Bot"); 7 | 8 | Bot.setNoBoot(true); 9 | 10 | const { getClient } = require("../bot"); 11 | 12 | // Posts slash commands to all guilds containing the bot 13 | // Docs: https://discordjs.guide/interactions/slash-commands.html#global-commands 14 | // https://github.com/discordjs/discord.js/tree/main/packages/rest 15 | // https://github.com/discordjs/discord-api-types/ 16 | (async () => { 17 | const config = await getConfig(); 18 | const rest = new REST({ version: "10" }).setToken(config.token); 19 | const commands = getClient().slash.map(slash => slash); 20 | 21 | try { 22 | console.log("Started refreshing application (/) commands."); 23 | await rest.put(Routes.applicationCommands(config.clientId), { 24 | body: commands, 25 | }); 26 | console.log("Successfully reloaded application (/) commands."); 27 | 28 | writeFile(join(__dirname, "..", "registered-global"), "", (err) => { 29 | if (err) 30 | console.error(new Error("Failed creating file registered-global")); 31 | 32 | process.exit(); 33 | }); 34 | } catch (error) { 35 | console.error(error); 36 | } 37 | })(); 38 | -------------------------------------------------------------------------------- /djs-bot/scripts/guild.js: -------------------------------------------------------------------------------- 1 | const { rl } = require("../util/common"); 2 | const { REST } = require("@discordjs/rest"); 3 | const getConfig = require("../util/getConfig"); 4 | const { Routes } = require("discord-api-types/v10"); 5 | const Bot = require("../lib/Bot"); 6 | 7 | Bot.setNoBoot(true); 8 | 9 | const { getClient } = require("../bot"); 10 | 11 | // Posts slash commands to a given guild containing the bot 12 | // Docs: https://discordjs.guide/interactions/slash-commands.html#guild-commands 13 | // https://github.com/discordjs/discord.js/tree/main/packages/rest 14 | // https://github.com/discordjs/discord-api-types/ 15 | (async () => { 16 | const config = await getConfig(); 17 | const rest = new REST({ version: "10" }).setToken(config.token); 18 | const commands = getClient().slash.map(slash => slash); 19 | 20 | rl.question("Enter the guild id you wanted to deploy commands: ", async (guild) => { 21 | console.log("Deploying commands to guild..."); 22 | await rest.put(Routes.applicationGuildCommands(config.clientId, guild), { 23 | body: commands, 24 | }).catch(console.log); 25 | console.log("Successfully deployed commands!"); 26 | rl.close(); 27 | }); 28 | })(); 29 | -------------------------------------------------------------------------------- /djs-bot/scripts/update.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const { exit } = require('process'); 3 | const { run, dirExists } = require("../util/common"); 4 | 5 | // synchronously runs the script directly on the console 6 | (() => { 7 | fs.rmSync("./package-lock.json", { force: true }); 8 | 9 | if (dirExists("./node_modules")) { 10 | console.log("Cleaning node_modules..."); 11 | run("npm", ["cache", "clean", "--force"]); 12 | fs.rmSync("./node_modules", { recursive: true, force: true }); 13 | } 14 | 15 | console.log("Updating and saving dependencies to package.json..."); 16 | run("npm", ["update", "--save"]); 17 | 18 | console.log("Installing dependencies..."); 19 | run("npm", ["install"]); 20 | 21 | exit(); 22 | })(); -------------------------------------------------------------------------------- /djs-bot/util/commands.d.ts: -------------------------------------------------------------------------------- 1 | import { ClientEvents } from "discord.js"; 2 | 3 | export function reply( 4 | interaction: ClientEvents["interactionCreate"][0], 5 | desc: string 6 | ): Promise; 7 | -------------------------------------------------------------------------------- /djs-bot/util/commands.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { colorEmbed } = require("./embeds"); 4 | 5 | const reply = async (interaction, desc) => 6 | interaction[interaction.deferred || interaction.replied ? "editReply" : "reply"]({ 7 | embeds: [ 8 | colorEmbed({ 9 | desc, 10 | }), 11 | ], 12 | ephemeral: true, 13 | }); 14 | 15 | module.exports = { 16 | reply, 17 | }; 18 | -------------------------------------------------------------------------------- /djs-bot/util/common.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const readline = require("readline"); 3 | const { spawnSync } = require("child_process"); 4 | 5 | /** 6 | * checks if a directory exists 7 | * @param {string} directory 8 | */ 9 | function dirExists(dir) { 10 | try { 11 | fs.readdirSync(dir); 12 | } catch (err) { 13 | if (err) return false; 14 | } 15 | return true; 16 | } 17 | 18 | /** 19 | * command line interface for the script 20 | */ 21 | const rl = readline.createInterface({ 22 | input: process.stdin, 23 | output: process.stdout, 24 | }); 25 | 26 | /** 27 | * Runs a command through spawn and displays the output directly to the console. 28 | * Options are normalized to allow for EoA 29 | * 30 | * @param {string} command command to execute through the shell using `child_process.spawnSync` 31 | * @param {string[]} argv arguments to pass to the command 32 | */ 33 | function run(command, argv) { 34 | spawnSync(command, argv, { 35 | stdio: "inherit", // display output directly to the console (Live) 36 | shell: true, // runs the command through the shell (This option is required for the command to run through on any shell) 37 | cwd: process.cwd(), // sets the current working directory to the project root (Arbitrary) 38 | env: process.env // sets the environment variables to the system PATH (Arbitrary) 39 | }); 40 | console.log("Done!"); 41 | }; 42 | 43 | const permissionsConfigMessageMapper = (perm) => 44 | (typeof perm === "object") ? `${perm.permission}${perm.message?.length ? ` (${perm.message})` : ""}` : perm; 45 | 46 | module.exports = { 47 | dirExists, 48 | run, 49 | rl, 50 | permissionsConfigMessageMapper, 51 | }; 52 | -------------------------------------------------------------------------------- /djs-bot/util/getChannel.js: -------------------------------------------------------------------------------- 1 | const Bot = require("../lib/Bot"); 2 | const { redEmbed } = require("../util/embeds"); 3 | 4 | // Module checks if you meet the channel requirements to use music commands 5 | /** 6 | * 7 | * @param {Bot} client 8 | * @param {import("discord.js").Interaction} interaction 9 | * @param {import("discord.js").InteractionReplyOptions} options 10 | * @returns {Promise} 11 | */ 12 | module.exports = async (client, interaction, options = {}) => { 13 | return new Promise(async (resolve) => { 14 | let errorStr; 15 | 16 | 17 | if (!interaction.member.voice.channel) { 18 | errorStr = "You must be in a voice channel to use this command!"; 19 | } 20 | else if ( 21 | interaction.guild.members.cache.get(client.user.id).voice.channel && 22 | !interaction.guild.members.cache 23 | .get(client.user.id) 24 | .voice.channel.equals(interaction.member.voice.channel) 25 | ) { 26 | errorStr = 27 | "You must be in the same voice channel as me to use this command!"; 28 | } 29 | else if (!interaction.member.voice.channel.joinable) { 30 | errorStr = "I don't have enough permission to join your voice channel!"; 31 | } 32 | 33 | 34 | if (errorStr) { 35 | await interaction.reply({ 36 | embeds: [redEmbed({ desc: errorStr })], 37 | ...options, 38 | }); 39 | 40 | return resolve(false); 41 | } 42 | 43 | resolve(interaction.member.voice.channel); 44 | }); 45 | }; 46 | -------------------------------------------------------------------------------- /djs-bot/util/getConfig.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Promise based module to get and return the contents of `config.js` 3 | * @returns {Promise} 4 | */ 5 | module.exports = async () => { 6 | return new Promise((resolve, reject) => { 7 | try { 8 | const config = require("../config"); 9 | resolve(config); 10 | } catch { 11 | reject("No config file found.\nMake sure it is filled in completely!"); 12 | } 13 | }).catch(err => { 14 | console.log(err); 15 | }); 16 | }; 17 | -------------------------------------------------------------------------------- /djs-bot/util/getDirs.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | 3 | /** 4 | * Reads all files of a dir and sub dirs 5 | * @param {string} dir Directory to read 6 | * @param {Array} files_ 7 | * @returns {Array} Array of files 8 | * @note Dunno where I found this function but I'm pretty sure it's not mine 9 | */ 10 | const getFiles = (dir, files_) => { 11 | files_ = files_ || []; 12 | let files = fs.readdirSync(dir); 13 | for (let i in files) { 14 | let name = dir + '/' + files[i]; 15 | if (fs.statSync(name).isDirectory()) { 16 | getFiles(name, files_); 17 | } else { 18 | files_.push(name); 19 | } 20 | } 21 | return files_; 22 | } 23 | 24 | module.exports = { 25 | getFiles, 26 | }; 27 | -------------------------------------------------------------------------------- /djs-bot/util/getLavalink.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Get the first available Lavalink node 3 | * @param {import("../lib/Bot")} client 4 | */ 5 | module.exports = async (client) => { 6 | return (client.manager.Engine.leastUsedNode); 7 | }; -------------------------------------------------------------------------------- /djs-bot/util/interactions.js: -------------------------------------------------------------------------------- 1 | const { embedNoLLNode, embedNoTrackPlaying, embedNotEnoughSong } = require("./embeds"); 2 | 3 | /** 4 | * @param {import("../lib/Bot")} client 5 | * @param {import("discord.js").Interaction} interaction 6 | * @param {{minimumQueueLength?: number}} 7 | */ 8 | const ccInteractionHook = async (client, interaction, { minimumQueueLength } = {}) => { 9 | if (!interaction.isButton()) { 10 | throw new Error("Invalid interaction type for this command"); 11 | } 12 | 13 | const channel = await client.getChannel(client, interaction, { 14 | ephemeral: true, 15 | }); 16 | 17 | /** 18 | * @template T 19 | * @param {T} data 20 | */ 21 | const returnError = (data) => { 22 | return { 23 | error: true, 24 | data, 25 | }; 26 | }; 27 | 28 | if (!channel) { 29 | return returnError(undefined); 30 | } 31 | 32 | /** 33 | * @param {import("discord.js").EmbedBuilder} embed 34 | */ 35 | const sendError = (embed) => { 36 | return interaction.reply({ 37 | embeds: [embed], 38 | ephemeral: true, 39 | }); 40 | }; 41 | 42 | if (!client.manager.Engine) { 43 | return returnError(sendError(embedNoLLNode())); 44 | } 45 | 46 | const player = client.manager.Engine.players.get(interaction.guild.id); 47 | 48 | if (!player) { 49 | return returnError(sendError(embedNoTrackPlaying())); 50 | } 51 | 52 | if ( 53 | typeof minimumQueueLength === "number" && 54 | (player.queue?.length ?? 0) < minimumQueueLength 55 | ) { 56 | return returnError(sendError(embedNotEnoughSong())); 57 | } 58 | 59 | return { error: false, data: { channel, sendError, player } }; 60 | }; 61 | 62 | const checkPlayerVolume = async (player, interaction) => { 63 | if (typeof player.volume !== "number") 64 | return interaction.reply({ 65 | content: "Something's wrong: volume is not a number", 66 | ephemeral: true, 67 | }); 68 | }; 69 | 70 | module.exports = { 71 | ccInteractionHook, 72 | checkPlayerVolume, 73 | }; 74 | -------------------------------------------------------------------------------- /djs-bot/util/message.d.ts: -------------------------------------------------------------------------------- 1 | import { Message } from "discord.js"; 2 | 3 | export function deleteMessageDelay(message: Message, delay?: number): void; 4 | -------------------------------------------------------------------------------- /djs-bot/util/message.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { getClient } = require("../bot"); 4 | 5 | const deleteMessageDelay = (message, delay = 20000) => { 6 | if (!message) return; 7 | 8 | setTimeout(() => message.delete().catch(getClient().warn), delay); 9 | }; 10 | 11 | module.exports = { 12 | deleteMessageDelay, 13 | }; 14 | -------------------------------------------------------------------------------- /djs-bot/util/musicManager.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { getClient } = require("../bot"); 4 | 5 | const setDefaultPlayerConfig = (instance) => { 6 | const config = getClient().config; 7 | const defaultValues = config.defaultPlayerValues; 8 | 9 | if (typeof defaultValues !== "object") return; 10 | 11 | const defaultKeys = Object.keys(config.defaultPlayerValues); 12 | 13 | defaultKeys.forEach((key) => { 14 | instance.set(key, defaultValues[key]); 15 | }); 16 | }; 17 | 18 | module.exports = { 19 | setDefaultPlayerConfig, 20 | }; 21 | -------------------------------------------------------------------------------- /djs-bot/util/player.d.ts: -------------------------------------------------------------------------------- 1 | import { CosmiPlayer } from "cosmicord.js"; 2 | import { GuildMember } from "discord.js"; 3 | import { IUsingPlayer } from "../lib/MusicEvents"; 4 | 5 | export function triggerSocketQueueUpdate(player: IUsingPlayer): void; 6 | 7 | export function spliceQueue( 8 | player: IUsingPlayer, 9 | ...restArgs: Parameters 10 | ): ReturnType; 11 | 12 | export function clearQueue(player: IUsingPlayer): ReturnType; 13 | 14 | export function removeTrack( 15 | player: IUsingPlayer, 16 | ...restArgs: Parameters 17 | ): ReturnType; 18 | 19 | export function shuffleQueue(player: IUsingPlayer): ReturnType; 20 | 21 | export function playPrevious(player: CosmiPlayer): Promise; 22 | 23 | export function stop(player: CosmiPlayer): number; 24 | 25 | export function skip(player: CosmiPlayer): number; 26 | 27 | export function joinStageChannelRoutine(me: GuildMember): void; 28 | 29 | export function addTrack( 30 | player: IUsingPlayer, 31 | tracks: Parameters[0] 32 | ): ReturnType; 33 | 34 | export function triggerSocketPause(player: IUsingPlayer, state: boolean): void; 35 | 36 | export function pause(player: IUsingPlayer, state: boolean): ReturnType; 37 | -------------------------------------------------------------------------------- /docker/.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | lavalink.env 3 | -------------------------------------------------------------------------------- /docker/djs-bot/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20 as builder 2 | 3 | RUN apt-get update -y && \ 4 | apt-get install -y \ 5 | build-essential \ 6 | libpango1.0-dev \ 7 | libcairo2-dev \ 8 | librsvg2-dev \ 9 | libjpeg-dev \ 10 | libgif-dev \ 11 | fonts-noto-cjk fonts-noto 12 | RUN npm config set audit false 13 | RUN npm config set fund false 14 | -------------------------------------------------------------------------------- /docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | djs-bot: 3 | container_name: djs-bot 4 | build: 5 | context: .. 6 | dockerfile: ./docker/djs-bot/Dockerfile 7 | restart: "unless-stopped" 8 | init: true 9 | environment: 10 | - TOKEN=${TOKEN} 11 | - CLIENTID=${CLIENTID} 12 | - CLIENTSECRET=${CLIENTSECRET} 13 | - DEVUID=${DEVUID} 14 | - JWT_SECRET_KEY=${JWT_SECRET_KEY} 15 | stdin_open: true 16 | tty: true 17 | volumes: 18 | - ../djs-bot:/djs-bot 19 | depends_on: 20 | - postgres-db 21 | - lavalink 22 | ports: 23 | - "8080:8080" 24 | - "3001:3001" 25 | working_dir: /djs-bot 26 | command: ["npm", "run", "${ENABLE:-start}", "--loglevel=error"] 27 | env_file: 28 | - .env 29 | - lavalink.env 30 | 31 | dashboard: 32 | container_name: dashboard 33 | image: node:latest 34 | restart: "unless-stopped" 35 | init: true 36 | stdin_open: true 37 | tty: true 38 | volumes: 39 | - ../dashboard:/dashboard 40 | depends_on: 41 | - postgres-db 42 | - lavalink 43 | ports: 44 | - "3000:3000" 45 | working_dir: /dashboard 46 | command: sh -c " 47 | npm config set fund false && 48 | npm config set audit false && 49 | npm run build-and-start 50 | " 51 | 52 | postgres-db: 53 | image: postgres:16.4-alpine 54 | container_name: postgres 55 | restart: unless-stopped 56 | volumes: 57 | - postgres_data:/var/lib/postgresql/data 58 | env_file: 59 | - .env 60 | 61 | lavalink: 62 | image: fredboat/lavalink:3.7.12 63 | container_name: lavalink 64 | restart: unless-stopped 65 | hostname: docker.lavalink 66 | user: root 67 | volumes: 68 | - ./lavalink/application.yml:/opt/Lavalink/application.yml 69 | - ./lavalink/plugins/:/opt/Lavalink/plugins 70 | ports: 71 | - 2333:2333 72 | env_file: 73 | - lavalink.env 74 | 75 | volumes: 76 | postgres_data: 77 | -------------------------------------------------------------------------------- /docker/lavalink/standalone.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | docker run --rm -p 2333:2333 -v "./$(dirname "$0")/application.yml:/opt/Lavalink/application.yml" --name lavalink-standalone fredboat/lavalink:3.7.12 4 | --------------------------------------------------------------------------------