diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..4988fcde3 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,9 @@ +.DS_Store +node_modules/ +.git/ +.vscode/ +docs/ +*.log +*.tar.gz +*.tar +.env \ No newline at end of file diff --git a/.env.example b/.env.example index 4b417395c..7fbf4f575 100644 --- a/.env.example +++ b/.env.example @@ -1,20 +1,24 @@ -# Bot Token [Required] -BOT_TOKEN= - -# Mongo Database Connection String [Required] -MONGO_CONNECTION= - -# Webhooks [Optional] -ERROR_LOGS= -JOIN_LEAVE_LOGS= - -# Dashboard [Required for dashboard] -BOT_SECRET= -SESSION_PASSWORD= - -# API Keys [Required for Weather Command] -WEATHERSTACK_KEY= - -# SPOTFIY [Required for Spotify Support] -SPOTIFY_CLIENT_ID= -SPOTIFY_CLIENT_SECRET= +@@ -1,23 +0,0 @@ +# Bot Token [Required] +BOT_TOKEN= + +# Mongo Database Connection String [Required] +MONGO_CONNECTION= + +# Webhooks [Optional] +ERROR_LOGS= +JOIN_LEAVE_LOGS= + +# Dashboard [Required for dashboard] +BOT_SECRET= +SESSION_PASSWORD= + +# Required for Weather Command (https://weatherstack.com) +WEATHERSTACK_KEY= + +# Required for image commands (https://strangeapi.fun/docs) +STRANGE_API_KEY= + +# SPOTFIY [Required for Spotify Support] +SPOTIFY_CLIENT_ID= +SPOTIFY_CLIENT_SECRET= \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json index d25f7aee2..6e037488c 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -13,8 +13,5 @@ "no-unused-vars": ["error", { "args": "none" }], "jsdoc/no-undefined-types": 1, "no-cond-assign": 0 - }, - "globals": { - "__appRoot": "readonly" } } diff --git a/.gitbook.yaml b/.gitbook.yaml index 57e123cae..e66b6e80f 100644 --- a/.gitbook.yaml +++ b/.gitbook.yaml @@ -1,4 +1,4 @@ root: ./docs/ structure: readme: ../README.md - summary: SUMMARY.md + summary: SUMMARY.md diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml new file mode 100644 index 000000000..9a79ead56 --- /dev/null +++ b/.github/workflows/docker-publish.yml @@ -0,0 +1,48 @@ +name: Publish Docker image + +on: + push: + branches: [main] + +env: + REGISTRY: docker.io + IMAGE_NAME: saitejamadha/discord-js-bot + +jobs: + push_to_registry: + name: Push Docker image to Docker Hub + runs-on: ubuntu-latest + permissions: + packages: write + contents: read + attestations: write + id-token: write + steps: + - name: Check out the repo + uses: actions/checkout@v4 + + - name: Log in to Docker Hub + uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Extract version from package.json + id: version + run: echo "VERSION=$(jq -r .version package.json)" >> $GITHUB_ENV + + - name: Build and push Docker image + id: push + uses: docker/build-push-action@v4 + with: + context: . + file: ./Dockerfile + push: true + tags: ${{ env.IMAGE_NAME }}:${{ env.VERSION }} + + - name: Generate artifact attestation + uses: actions/attest-build-provenance@v1 + with: + subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}} + subject-digest: ${{ steps.push.outputs.digest }} + push-to-registry: false diff --git a/.gitignore b/.gitignore index de6c79883..7d0b3e132 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ +.DS_Store .vscode/ node_modules/ -logs/* +*.log +*.tar.gz .env \ No newline at end of file diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 000000000..c37466e2b --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +npx lint-staged \ No newline at end of file diff --git a/.prettierrc.json b/.prettierrc.json index 45a0c08e5..e1f258c61 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -6,5 +6,7 @@ "singleQuote": false, "printWidth": 120, "bracketSpacing": true, - "arrowParens": "always" + "arrowParens": "always", + "endOfLine": "lf" } + diff --git a/.replit b/.replit index 2efe7969f..7c8aef1c6 100644 --- a/.replit +++ b/.replit @@ -1,2 +1,89 @@ +entrypoint = "bot.js" + +hidden = [".config", "package-lock.json"] + +[interpreter] +command = [ + "prybar-nodejs", + "-q", + "--ps1", + "\u0001\u001b[33m\u0002\u0001\u001b[00m\u0002 ", + "-i" +] + +[[hints]] +regex = "Error \\[ERR_REQUIRE_ESM\\]" +message = "We see that you are using require(...) inside your code. We currently do not support this syntax. Please use 'import' instead when using external modules. (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import)" + +[nix] +channel = "stable-22_11" + +[env] +XDG_CONFIG_HOME = "/home/runner/$REPL_SLUG/.config" +PATH = "/home/runner/$REPL_SLUG/.config/npm/node_global/bin:/home/runner/$REPL_SLUG/node_modules/.bin" +npm_config_prefix = "/home/runner/$REPL_SLUG/.config/npm/node_global" + +[gitHubImport] +requiredFiles = [".replit", "replit.nix", ".config", "package.json", "package-lock.json"] + +[packager] language = "nodejs" -run = "node_modules/.bin/node bot.js" \ No newline at end of file + + [packager.features] + packageSearch = true + guessImports = true + enabledForHosting = false + +[unitTest] +language = "nodejs" + +[debugger] +support = true + + [debugger.interactive] + transport = "localhost:0" + startCommand = [ "dap-node" ] + + [debugger.interactive.initializeMessage] + command = "initialize" + type = "request" + + [debugger.interactive.initializeMessage.arguments] + clientID = "replit" + clientName = "replit.com" + columnsStartAt1 = true + linesStartAt1 = true + locale = "en-us" + pathFormat = "path" + supportsInvalidatedEvent = true + supportsProgressReporting = true + supportsRunInTerminalRequest = true + supportsVariablePaging = true + supportsVariableType = true + + [debugger.interactive.launchMessage] + command = "launch" + type = "request" + + [debugger.interactive.launchMessage.arguments] + args = [] + console = "externalTerminal" + cwd = "." + environment = [] + pauseForSourceMap = false + program = "./index.js" + request = "launch" + sourceMaps = true + stopOnEntry = false + type = "pwa-node" + +[languages] + +[languages.javascript] +pattern = "**/{*.js,*.jsx,*.ts,*.tsx}" + +[languages.javascript.languageServer] +start = "typescript-language-server --stdio" + +[deployment] +run = ["sh", "-c", "node index.js"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..261eeb9e9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/Procfile b/Procfile new file mode 100644 index 000000000..e8f79ea7b --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: npm start \ No newline at end of file diff --git a/README.md b/README.md index 1b66a5cfb..705639beb 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,45 @@ -# 🤖 About Me +

+
+ Discord.js v14 Bot +
+ Discord.js v14 Bot +
+

-![Strange Bot](https://i.imgur.com/nFrS5wC.png) +

Admin, AutoMod, Anime, Economy, Fun, Giveaway, Image, Invite, Information, Moderation, Music, Owner, Social, Statistics, Suggestion, Ticket, Utility and More...

-> An awesome multipurpose discord bot built using [discord.js v13](https://discord.js.org) with support for slash commands and context menus +
-> Demo Bot: [Invite Here](https://discord.com/oauth2/authorize?client\_id=752922609733337190\&permissions=397602323830\&scope=bot%20applications.commands) -> -> Support Server: [Join Here](https://discord.gg/fE75UShbqB) -> -> Documentation URL: [Visit Here](https://docs.strangebot.xyz) +

+ Resource Links + • + Prerequisites + • + Getting Started + • + Features + • + Contributing +

-### Prerequisites +
-* [Node.js](https://nodejs.org/en/) v16.6.0 or higher -* [Git](https://git-scm.com/downloads) -* [MongoDB](https://www.mongodb.com) +## 🔗 Resource Links -### Getting Started +- 🤖 Demo Bot: [Invite Here](https://discord.com/oauth2/authorize?client_id=1013236808353599488&permissions=397602323830&scope=bot%20applications.commands) +- 🤝 Support Server: [Join Here](https://discord.gg/2gPy75zgbW) +- 📂 Documentation URL: [Visit Here](https://strangedocs.hostz.me) +- 🐳 Docker Image: [Hub](https://hub.docker.com/r/saitejamadha/discord-js-bot) -* Open the terminal and run the following commands +## 📦 Prerequisites + +- [Node.js](https://nodejs.org/en/) v16.11.0 or higher +- [Git](https://git-scm.com/downloads) +- [MongoDB](https://www.mongodb.com) + +## 🚀 Getting Started + +- Open the terminal and run the following commands ``` git clone https://github.com/saiteja-madha/discord-js-bot.git @@ -26,46 +47,111 @@ cd discord-js-bot npm install ``` -* Wait for all the dependencies to be installed -* Rename `.env.example` to `.env` and fill the values -* Optionally edit `config.js` -* Type `npm run start` to start the bot +- Wait for all the dependencies to be installed +- Rename `.env.example` to `.env` and fill the values +- Optionally edit `config.js` +- Type `npm run start` to start the bot If you need any additional help, make sure to read our guides [here](docs/additional/installation.md) -### Features +
+ +

✨ Features ✨

+ +### 📡 **Advanced Dashboard** + +- Manage your servers and make your server-specific settings! +- Make custom adjustments easy! + +### 🛑 **Powerful Moderation:** + +- **Moderation Commands.**
_Commands:_ `ban`, `unban`, `timeout`, `voice moderation`, `deafen`, `move`, `warn`, `setnick`, ... +- **Multi-Function Purge Commands.**
_Commands:_ `purge`, `purge attach`, `purge bots`, `purge links`, `purge token`, `purge user`, ... + +### 🤖 **Auto Moderation:** + +- **Anti system**
_Commands:_ `anti ghostping`, `anti spam`, `anti massmention`, ... +- **Auto Delete system**
_Commands:_ `autodelete attachments`, `autodelete invites`, `autodelete links`, `autodelete maxlines`, ... +- **AutoMod system**
_Commands:_ `automod status`, `automod strikes`, `automod action`, `automod debug`, `automod whitelist`, ... + +### ⚙️ **Admin Configuration:** + +- **Let a bot be the server's assistant!**
_Commands:_ `autorole`, `farewell`, `welcome`, `counters`, `flag translation`, `reaction roles`, ... +- **Make custom settings for your own server.**
_Commands:_ `setprefix`, `maxwarns`, `modlog`... + +### 💁 **Information Gathering:** + +- **User Context Interactions** +- **Advanced Information** Get deep information about a user, channel, role, etc. + +### 🎵 **Music:** + +- **LossLess Music!** Enjoy high quality lossless music +- **Multi-Platform** Play music from YouTube, SoundCloud, Spotify, and more +- **Filters** Apply filters to your music and spice it up + +### 🎉 **Giveaways:** + +- **Easy to use** Create giveaways with ease +- **Role specific** giveaways +- **Customizable** Customize the giveaway to your liking +- **Limitless** Create unlimited giveaways + +### 🫂 **Social Content:** + +- **You Have A CV In Each Server-Specific Bot!**
_Commands:_ `rep`, `rep view`... +- **Do You Love Someone?**
_Commands:_ `rep give`... + +### 🎟 **Ticket System:** + +- **Make Supporting Members A Breeze With Tickets!**
Highly customizable ticket system with staff roles +- **Multiple Categories**
Don't Want The Tickets To Be Everywhere? Categorize them using select menus + +### 📉 **Stats Tracking:** + +- **Levelling** Track your server's activity with a level system +- **Leaderboards** See who is the most active user in your server +- **Customizable System** Configure the levelup message, rank cards to your liking + +### 🙋‍♂️ **Suggestions:** + +- **Get Suggestions From Server Members To Help Your Server Become The Best!**
_Commands:_ `suggest`, `suggestion`... +- **Accept Or Decline The Suggestions And Customize Them To The Max!**
_Commands:_ `suggestion status`, `suggestion channel`, `suggestion appch`, `suggestion rejch`, `suggestion approve`, `suggestion staffadd`, `suggestion staffremove`... + +### ⚒️ **Utility Commands:** + +- **Need Some Help With Something? Use The Utility Commands To Find Out The Answer To It**
_Commands:_ `bigemoji`, `covid`, `pokedex`, `urban`, `weather`, ... +- **Need Help With Some More Stuff?**
_Commands:_ `help`, `proxies`, `translate`, `paste`, ... + +### ⭐ **Anime Content:** + +- **Love Anime? Express You Love To Someone Using The React Commands**
_Commands:_ `react`, `hug`, `kiss`, `cuddle`, `pat`, `poke`, `slap`, `smug`, ... + +### 🪙 **Economy System:** + +- **Want To Become Richest? Use The Economy Commands!**
_Commands:_ `bank`, `daily`, `beg`, `gamble`... +- **Give People Money, Check Your Balance, Or Just Flex!**
_Commands:_ `bank balance`, `bank deposit`, `bank withdraw`, `bank transfer`, ... + +### 😁 **Fun Commands:** -Strange is a feature-rich discord bot with new features constantly being updated! Current features include +- **Have Some Fun In Your Server!**
_Commands:_ `animal`, `facts`, `meme`, `flip`, ... +- **Play Games And Enjoy Yourself**
_Commands:_ `snake`, `together`, `flip coin`, `flip text`, ... -* **Auto-Moderation**: Power auto-moderation to keep your discord server clean -* **Powerful Moderation with Logging**: Moderate and log every action you take -* **Image Manipulation**: Have fun with various image `filters` and `generators` -* **Economy & XP System**: Engage user interaction with the economy and Levelling system -* **Invite Tracking**: Best invite tracking with configurable invite ranks -* **Ticketing**: Support for creating multiple `ticket` channels -* **Reaction Roles**: Support for creation of multiple custom reaction roles -* **Greeting**: Highly Customizable welcome and farewell embeds +### 📨 **Invite Tracking:** -### Categories +- **Track who has been inviting people to your server!** +- **Invite Ranks!** Inviter can get awesome rewards and be recognised +- **Configure these settings and customize them to your liking!**
_Commands:_ `resetinvites`, `addinvites`, `invitesimport`, `inviterank`... -Strange has an extensive list of all useful commands (**more than 100**) which are categorized as follows +### 📷 **Image Manipulation:** -* **Automod**: `antighostping`, `antiinvites`, `antilinks`, `antiscam`, `maxlines`, `maxmentions`, ... -* **Admin**: `welcome`, `farewell`, `reaction-roles`, ... -* **Economy**: `daily`, `gamble`, `deposit`, `withdraw`, `transfer`, ... -* **Fun**: `cat`, `doc`, `flipcoin`, `fliptext`, ... -* **Information**: `avatar`, `roleinfo`, `channelinfo`, `guildinfo`, `profile`, ... -* **Invites**: `inviter`, `invites`, `invitesimport`, `invitecodes`, ... -* **Moderation**: `warn`, `kick`, `softban`, `ban`, `mute`, `unmute`, ... -* **Music**: `play`, `pause`, `resume`, `stop`, `skip`, `queue`, `np`, ... -* **Social**: `reputation list,` `give reputation,` ... -* **Ticket**: setup, close, log, ... -* **Utility**: `proxies`, `translation`, `weather`, `covid`, ... +- **Customize other peoples avatars**
_Commands:_ `blur`, `greyscale`, `invert`, `pixelate`, `blur`, `sepia`, `sharpen`, `ad`, `affect`, `beautiful`, `color`... +- **Make some images by yourself or make some art**
_Commands:_ `bobross`, `confusedstonk`, `delete`, `facepalm`, ` hitler`, `jail`, `jokeoverhead`, `karaba`, `mms`, `notstonk`, `poutine`, `rainbow`, `rip`, ` shit`, `stonk`, `tatoo`, `thomas`, `trash`, `wanted`, `wasted`, ... -A complete list of commands can be found in the [documentation](docs/commands/) +
-### 🤝 Contributing +

🤝 Contributing 🤝

-* Special thanks to [@Androzz](https://github.com/Androz2091/AtlantaBot) for the [dashboard](https://github.com/Androz2091/AtlantaBot) and his other cool discord bot projects -* Feel free to [Fork](https://github.com/saiteja-madha/discord-js-bot/fork) this repository, create a feature branch and submit a pull request -* You can check all the planned features [here](https://github.com/saiteja-madha/discord-js-bot/projects) or make a request for one at our discord +- Special thanks to [@Androzz](https://github.com/Androz2091/AtlantaBot) for the [dashboard](https://github.com/Androz2091/AtlantaBot) and his other cool discord bot projects +- Feel free to [Fork](https://github.com/saiteja-madha/discord-js-bot/fork) this repository, create a feature branch and submit a pull request +- You can keep track of all the planned features [here](https://github.com/saiteja-madha/discord-js-bot/projects) or make a request for one at our discord diff --git a/bot.js b/bot.js index 39f1ed1a6..799a7855a 100644 --- a/bot.js +++ b/bot.js @@ -1,13 +1,17 @@ require("dotenv").config(); require("module-alias/register"); -require("@src/helpers/extenders"); -const path = require("path"); -const { validateConfig, checkForUpdates } = require("@utils/botUtils"); +// register extenders +require("@helpers/extenders/Message"); +require("@helpers/extenders/Guild"); +require("@helpers/extenders/GuildChannel"); + +const { checkForUpdates } = require("@helpers/BotUtils"); const { initializeMongoose } = require("@src/database/mongoose"); const { BotClient } = require("@src/structures"); +const { validateConfiguration } = require("@helpers/Validator"); -global.__appRoot = path.resolve(__dirname); +validateConfiguration(); // initialize client const client = new BotClient(); @@ -19,11 +23,6 @@ client.loadEvents("src/events"); process.on("unhandledRejection", (err) => client.logger.error(`Unhandled exception`, err)); (async () => { - validateConfig(); - - // initialize the database - await initializeMongoose(); - // check for updates await checkForUpdates(); @@ -32,10 +31,15 @@ process.on("unhandledRejection", (err) => client.logger.error(`Unhandled excepti client.logger.log("Launching dashboard"); try { const { launch } = require("@root/dashboard/app"); + + // let the dashboard initialize the database await launch(client); } catch (ex) { client.logger.error("Failed to launch dashboard", ex); } + } else { + // initialize the database + await initializeMongoose(); } // start the client diff --git a/config.js b/config.js index b851056d0..33db48fed 100644 --- a/config.js +++ b/config.js @@ -1,103 +1,134 @@ module.exports = { - OWNER_IDS: [], // Bot owner ID's - PREFIX: "!", // Default prefix for the bot + OWNER_IDS: [""], // Bot owner ID's SUPPORT_SERVER: "", // Your bot support server - PRESENCE: { - ENABLED: true, // Whether or not the bot should update its status - STATUS: "online", // The bot's status [online, idle, dnd, invisible] - TYPE: "WATCHING", // Status type for the bot [PLAYING | LISTENING | WATCHING | COMPETING] - MESSAGE: "{members} members in {servers} servers", // Your bot status message - }, - DASHBOARD: { - enabled: false, // enable or disable dashboard - baseURL: "http://localhost:8080", // base url - failureURL: "http://localhost:8080", // failure redirect url - port: "8080", // port to run the bot on + PREFIX_COMMANDS: { + ENABLED: true, // Enable/Disable prefix commands + DEFAULT_PREFIX: "!", // Default prefix for the bot }, INTERACTIONS: { SLASH: false, // Should the interactions be enabled CONTEXT: false, // Should contexts be enabled GLOBAL: false, // Should the interactions be registered globally - TEST_GUILD_ID: "xxxxxxxxxx", // Guild ID where the interactions should be registered. [** Test you commands here first **] + TEST_GUILD_ID: "xxxxxxxxxxx", // Guild ID where the interactions should be registered. [** Test you commands here first **] }, - XP_SYSTEM: { - COOLDOWN: 5, // Cooldown in seconds between messages - DEFAULT_LVL_UP_MSG: "{m}, You just advanced to **Level {l}**", + EMBED_COLORS: { + BOT_EMBED: "#068ADD", + TRANSPARENT: "#36393F", + SUCCESS: "#00A56A", + ERROR: "#D61A3C", + WARNING: "#F7E919", }, - MISCELLANEOUS: { - DAILY_COINS: 100, // coins to be received by daily command + CACHE_SIZE: { + GUILDS: 100, + USERS: 10000, + MEMBERS: 10000, + }, + MESSAGES: { + API_ERROR: "Unexpected Backend Error! Try again later or contact support server", + }, + + // PLUGINS + + AUTOMOD: { + ENABLED: false, + LOG_EMBED: "#36393F", + DM_EMBED: "#36393F", + }, + + DASHBOARD: { + enabled: false, // enable or disable dashboard + baseURL: "http://localhost:8080", // base url + failureURL: "http://localhost:8080", // failure redirect url + port: "8080", // port to run the bot on }, + ECONOMY: { + ENABLED: false, CURRENCY: "₪", DAILY_COINS: 100, // coins to be received by daily command MIN_BEG_AMOUNT: 100, // minimum coins to be received when beg command is used MAX_BEG_AMOUNT: 2500, // maximum coins to be received when beg command is used }, - SUGGESTIONS: { - ENABLED: true, // Should the suggestion system be enabled - EMOJI: { - UP_VOTE: "⬆️", - DOWN_VOTE: "⬇️", - }, - DEFAULT_EMBED: "#0099ff", - APPROVED_EMBED: "#00ff00", - DENIED_EMBED: "#ff0000", - }, - IMAGE: { - BASE_API: "https://image-api.strangebot.xyz", - }, + MUSIC: { - IDLE_TIME: 60, // Time in seconds before the bot disconnects from the voice channel + ENABLED: false, + IDLE_TIME: 60, // Time in seconds before the bot disconnects from an idle voice channel MAX_SEARCH_RESULTS: 5, - NODES: [ + DEFAULT_SOURCE: "SC", // YT = Youtube, YTM = Youtube Music, SC = SoundCloud + // Add any number of lavalink nodes here + // Refer to https://github.com/freyacodes/Lavalink to host your own lavalink server + LAVALINK_NODES: [ { - host: "ger.lavalink.mitask.tech", + host: "localhost", port: 2333, - password: "lvserver", - identifier: "German Link", - retryDelay: 5000, - secure: false, - }, - { - host: "usa.lavalink.mitask.tech", - port: 2333, - password: "lvserver", - identifier: "USA Link", - retryDelay: 5000, + password: "youshallnotpass", + id: "Local Node", secure: false, }, ], }, - /* Bot Embed Colors */ - EMBED_COLORS: { - BOT_EMBED: "#068ADD", - TRANSPARENT: "#36393F", - SUCCESS: "#00A56A", - ERROR: "#D61A3C", - WARNING: "#F7E919", - AUTOMOD: "#36393F", - TICKET_CREATE: "#068ADD", - TICKET_CLOSE: "#068ADD", - TIMEOUT_LOG: "#102027", - UNTIMEOUT_LOG: "#4B636E", - KICK_LOG: "#FF7961", - SOFTBAN_LOG: "#AF4448", - BAN_LOG: "#D32F2F", - VMUTE_LOG: "#102027", - VUNMUTE_LOG: "#4B636E", - DEAFEN_LOG: "#102027", - UNDEAFEN_LOG: "#4B636E", - DISCONNECT_LOG: "RANDOM", - MOVE_LOG: "RANDOM", - GIVEAWAYS: "#FF468A", - }, - /* Maximum number of keys that can be stored */ - CACHE_SIZE: { - GUILDS: 100, - USERS: 10000, - MEMBERS: 10000, + + GIVEAWAYS: { + ENABLED: false, + REACTION: "🎁", + START_EMBED: "#FF468A", + END_EMBED: "#FF468A", }, - MESSAGES: { - API_ERROR: "Unexpected Backend Error! Try again later or contact support server", + + IMAGE: { + ENABLED: false, + BASE_API: "https://strangeapi.hostz.me/api", + }, + + INVITE: { + ENABLED: false, + }, + + MODERATION: { + ENABLED: false, + EMBED_COLORS: { + TIMEOUT: "#102027", + UNTIMEOUT: "#4B636E", + KICK: "#FF7961", + SOFTBAN: "#AF4448", + BAN: "#D32F2F", + UNBAN: "#00C853", + VMUTE: "#102027", + VUNMUTE: "#4B636E", + DEAFEN: "#102027", + UNDEAFEN: "#4B636E", + DISCONNECT: "RANDOM", + MOVE: "RANDOM", + }, + }, + + PRESENCE: { + ENABLED: false, // Whether or not the bot should update its status + STATUS: "online", // The bot's status [online, idle, dnd, invisible] + TYPE: "WATCHING", // Status type for the bot [PLAYING | LISTENING | WATCHING | COMPETING] + MESSAGE: "{members} members in {servers} servers", // Your bot status message + }, + + STATS: { + ENABLED: false, + XP_COOLDOWN: 5, // Cooldown in seconds between messages + DEFAULT_LVL_UP_MSG: "{member:tag}, You just advanced to **Level {level}**", + }, + + SUGGESTIONS: { + ENABLED: false, // Should the suggestion system be enabled + EMOJI: { + UP_VOTE: "⬆️", + DOWN_VOTE: "⬇️", + }, + DEFAULT_EMBED: "#4F545C", + APPROVED_EMBED: "#43B581", + DENIED_EMBED: "#F04747", + }, + + TICKET: { + ENABLED: false, + CREATE_EMBED: "#068ADD", + CLOSE_EMBED: "#068ADD", }, }; diff --git a/dashboard/app.js b/dashboard/app.js index 96995a50c..c980e0f6c 100644 --- a/dashboard/app.js +++ b/dashboard/app.js @@ -7,6 +7,8 @@ module.exports.launch = async (client) => { const express = require("express"), session = require("express-session"), + MongoStore = require("connect-mongo"), + mongoose = require("@src/database/mongoose"), path = require("path"), app = express(); @@ -19,6 +21,8 @@ module.exports.launch = async (client) => { client.states = {}; client.config = config; + const db = await mongoose.initializeMongoose(); + /* App configuration */ app .use(express.json()) // For post methods @@ -28,7 +32,23 @@ module.exports.launch = async (client) => { .use(express.static(path.join(__dirname, "/public"))) // Set the css and js folder to ./public .set("views", path.join(__dirname, "/views")) // Set the ejs templates to ./views .set("port", config.DASHBOARD.port) // Set the dashboard port - .use(session({ secret: process.env.SESSION_PASSWORD, resave: false, saveUninitialized: false })) // Set the express session password and configuration + .use( + session({ + secret: process.env.SESSION_PASSWORD, + cookie: { maxAge: 336 * 60 * 60 * 1000 }, + name: "djs_connection_cookie", + resave: true, + saveUninitialized: false, + store: MongoStore.create({ + client: db.getClient(), + dbName: db.name, + collectionName: "sessions", + stringify: false, + autoRemove: "interval", + autoRemoveInterval: 1, + }), + }) + ) // Set the express session password and configuration .use(async function (req, res, next) { req.user = req.session.user; req.client = client; diff --git a/dashboard/routes/discord.js b/dashboard/routes/discord.js index ad0c0ae62..90568a362 100644 --- a/dashboard/routes/discord.js +++ b/dashboard/routes/discord.js @@ -7,6 +7,12 @@ const fetch = require("node-fetch"), // Gets login page router.get("/login", async function (req, res) { if (!req.user || !req.user.id || !req.user.guilds) { + // check if client user is ready + if (!req.client.user?.id) { + req.client.logger.debug("Client is not ready! Redirecting to /login"); + return res.redirect("/login"); + } + return res.redirect( `https://discordapp.com/api/oauth2/authorize?client_id=${ req.client.user.id @@ -19,7 +25,11 @@ router.get("/login", async function (req, res) { }); router.get("/callback", async (req, res) => { - if (!req.query.code) return res.redirect(req.client.config.DASHBOARD.failureURL); + if (!req.query.code) { + req.client.logger.debug({ query: req.query, body: req.body }); + req.client.logger.error("Failed to login to dashboard! Check /logs folder for more details"); + return res.redirect(req.client.config.DASHBOARD.failureURL); + } if (req.query.state && req.query.state.startsWith("invite")) { if (req.query.code) { const guildID = req.query.state.substr("invite".length, req.query.state.length); @@ -43,7 +53,11 @@ router.get("/callback", async (req, res) => { // Fetch tokens (used to fetch user information's) const tokens = await response.json(); // If the code isn't valid - if (tokens.error || !tokens.access_token) return res.redirect(`/api/login&state=${req.query.state}`); + if (tokens.error || !tokens.access_token) { + req.client.logger.debug(tokens); + req.client.logger.error("Failed to login to dashboard! Check /logs folder for more details"); + return res.redirect(`/api/login&state=${req.query.state}`); + } const userData = { infos: null, guilds: null, @@ -51,7 +65,7 @@ router.get("/callback", async (req, res) => { while (!userData.infos || !userData.guilds) { /* User infos */ if (!userData.infos) { - response = await fetch("http://discordapp.com/api/users/@me", { + response = await fetch("https://discordapp.com/api/users/@me", { method: "GET", headers: { Authorization: `Bearer ${tokens.access_token}` }, }); diff --git a/dashboard/routes/guild-manager.js b/dashboard/routes/guild-manager.js index 84c3b6ac6..f4c1b3635 100644 --- a/dashboard/routes/guild-manager.js +++ b/dashboard/routes/guild-manager.js @@ -76,27 +76,41 @@ router.post("/:serverID/basic", CheckAuth, async (req, res) => { const settings = await getSettings(guild); const data = req.body; + // BASIC CONFIGURATION if (Object.prototype.hasOwnProperty.call(data, "basicUpdate")) { if (data.prefix && data.prefix !== settings.prefix) { settings.prefix = data.prefix; } - data.ranking = data.ranking === "on" ? true : false; - if (data.ranking !== (settings.ranking.enabled || false)) { - settings.ranking.enabled = data.ranking; - } - data.flag_translation = data.flag_translation === "on" ? true : false; if (data.flag_translation !== (settings.flag_translation.enabled || false)) { settings.flag_translation.enabled = data.flag_translation; } - data.modlog_channel = guild.channels.cache.find((ch) => "#" + ch.name === data.modlog_channel)?.id || null; - if (data.modlog_channel !== settings.modlog_channel) { - settings.modlog_channel = data.modlog_channel; + data.invite_tracking = data.invite_tracking === "on" ? true : false; + if (data.invite_tracking !== (settings.invite.tracking || false)) { + settings.invite.tracking = data.invite_tracking; + } + } + + // STATISTICS CONFIGURATION + if (Object.prototype.hasOwnProperty.call(data, "statsUpdate")) { + data.ranking = data.ranking === "on" ? true : false; + if (data.ranking !== (settings.stats.enabled || false)) { + settings.stats.enabled = data.ranking; + } + + if (data.levelup_message && data.levelup_message !== settings.stats.xp.message) { + settings.stats.xp.message = data.levelup_message; + } + + data.levelup_channel = guild.channels.cache.find((ch) => "#" + ch.name === data.levelup_channel)?.id || null; + if (data.levelup_channel !== settings.stats.xp.channel) { + settings.stats.xp.channel = data.levelup_channel; } } + // TICKET CONFIGURATION if (Object.prototype.hasOwnProperty.call(data, "ticketUpdate")) { if (data.limit && data.limit != settings.ticket.limit) { settings.ticket.limit = data.limit; @@ -108,19 +122,29 @@ router.post("/:serverID/basic", CheckAuth, async (req, res) => { } } - if (Object.prototype.hasOwnProperty.call(data, "inviteUpdate")) { - data.tracking = data.tracking === "on" ? true : false; - if (data.tracking !== (settings.invite.tracking || false)) { - settings.invite.tracking = data.tracking; + // MODERATION CONFIGURATION + if (Object.prototype.hasOwnProperty.call(data, "modUpdate")) { + if (data.max_warnings && data.max_warnings != settings.max_warn.limit) { + settings.max_warn.limit = data.max_warnings; + } + + if (data.max_warn_action !== settings.max_warn.action) { + settings.max_warn.action = data.max_warn_action; + } + + data.modlog_channel = guild.channels.cache.find((ch) => "#" + ch.name === data.modlog_channel)?.id || null; + if (data.modlog_channel !== settings.modlog_channel) { + settings.modlog_channel = data.modlog_channel; } } + // AUTOMOD CONFIGURATION if (Object.prototype.hasOwnProperty.call(data, "automodUpdate")) { - if (data.max_strikes != settings.automod.strikes) { + if (data.max_strikes && data.max_strikes !== settings.automod.strikes) { settings.automod.strikes = data.max_strikes; } - if (data.automod_action !== settings.automod.action) { + if (data.automod_action && data.automod_action !== settings.automod.action) { settings.automod.action = data.automod_action; } @@ -128,11 +152,14 @@ router.post("/:serverID/basic", CheckAuth, async (req, res) => { settings.automod.max_lines = data.max_lines; } - if (data.max_mentions && data.max_mentions !== settings.automod.max_mentions) { - settings.automod.max_mentions = data.max_mentions; + data.anti_attachments = data.anti_attachments === "on" ? true : false; + if (data.anti_attachments !== (settings.automod.anti_attachments || false)) { + settings.automod.anti_attachments = data.anti_attachments; } - if (data.max_role_mentions && data.max_role_mentions !== settings.automod.max_role_mentions) { - settings.automod.max_role_mentions = data.max_role_mentions; + + data.anti_invites = data.anti_invites === "on" ? true : false; + if (data.anti_invites !== (settings.automod.anti_invites || false)) { + settings.automod.anti_invites = data.anti_invites; } data.anti_links = data.anti_links === "on" ? true : false; @@ -140,20 +167,27 @@ router.post("/:serverID/basic", CheckAuth, async (req, res) => { settings.automod.anti_links = data.anti_links; } - data.anti_scam = data.anti_scam === "on" ? true : false; - if (data.anti_scam !== (settings.automod.anti_scam || false)) { - settings.automod.anti_scam = data.anti_scam; - } - - data.anti_invites = data.anti_invites === "on" ? true : false; - if (data.anti_invites !== (settings.automod.anti_invites || false)) { - settings.automod.anti_invites = data.anti_invites; + data.anti_spam = data.anti_spam === "on" ? true : false; + if (data.anti_spam !== (settings.automod.anti_spam || false)) { + settings.automod.anti_spam = data.anti_spam; } data.anti_ghostping = data.anti_ghostping === "on" ? true : false; if (data.anti_ghostping !== (settings.automod.anti_ghostping || false)) { settings.automod.anti_ghostping = data.anti_ghostping; } + + data.anti_massmention = data.anti_massmention === "on" ? true : false; + if (data.anti_massmention !== (settings.automod.anti_massmention || false)) { + settings.automod.anti_massmention = data.anti_massmention; + } + + if (data.channels?.length) { + if (typeof data.channels === "string") data.channels = [data.channels]; + settings.automod.wh_channels = data.channels + .map((ch) => guild.channels.cache.find((c) => "#" + c.name === ch)?.id) + .filter((c) => c); + } } await settings.save(); @@ -185,32 +219,56 @@ router.post("/:serverID/greeting", CheckAuth, async (req, res) => { Object.prototype.hasOwnProperty.call(data, "welcomeEnable") || Object.prototype.hasOwnProperty.call(data, "welcomeUpdate") ) { + if (data.content !== settings.welcome.content) { + settings.welcome.content = data.content; + } + data.content = data.content.replace(/\r?\n/g, "\\n"); if (data.content && data.content !== settings.welcome.content) { settings.welcome.content = data.content; } + if (data.description !== settings.welcome.embed.description) { + settings.welcome.embed.description = data.description; + } + data.description = data.description?.replaceAll(/\r\n/g, "\\n"); if (data.description && data.description !== settings.welcome.embed?.description) { settings.welcome.embed.description = data.description; } + if (data.footer !== settings.welcome.embed.footer) { + settings.welcome.embed.footer = data.footer; + } + if (data.footer && data.footer !== settings.welcome.embed?.footer) { settings.welcome.embed.footer = data.footer; } + if (data.hexcolor !== settings.welcome.embed.hexcolor) { + settings.welcome.embed.hexcolor = data.hexcolor; + } + if (data.hexcolor && data.hexcolor !== settings.welcome.embed?.color) { settings.welcome.embed.color = data.hexcolor; } + if (data.image !== settings.welcome.embed.image) { + settings.welcome.embed.image = data.image; + } + + if (data.image && data.image !== settings.welcome.embed?.image) { + settings.welcome.embed.image = data.image; + } + data.thumbnail = data.thumbnail === "on" ? true : false; if (data.thumbnail !== (settings.welcome.embed?.thumbnail || false)) { settings.welcome.embed.thumbnail = data.thumbnail; } data.channel = guild.channels.cache.find((ch) => "#" + ch.name === data.channel)?.id; - if (data.channel !== settings.welcome.channel_id) { - settings.welcome.channel_id = data.channel; + if (data.channel !== settings.welcome.channel) { + settings.welcome.channel = data.channel; } if (!settings.welcome.enabled) settings.welcome.enabled = true; @@ -224,32 +282,56 @@ router.post("/:serverID/greeting", CheckAuth, async (req, res) => { Object.prototype.hasOwnProperty.call(data, "farewellEnable") || Object.prototype.hasOwnProperty.call(data, "farewellUpdate") ) { + if (data.content !== settings.farewell.content) { + settings.farewell.content = data.content; + } + data.content = data.content.replace(/\r?\n/g, "\\n"); if (data.content && data.content !== settings.farewell.content) { settings.farewell.content = data.content; } + if (data.description !== settings.farewell.description) { + settings.farewell.description = data.description; + } + data.description = data.description?.replaceAll(/\r\n/g, "\\n"); if (data.description && data.description !== settings.farewell.embed?.description) { settings.farewell.embed.description = data.description; } + if (data.footer !== settings.farewell.footer) { + settings.farewell.footer = data.footer; + } + if (data.footer && data.footer !== settings.farewell.embed?.footer) { settings.farewell.embed.footer = data.footer; } + if (data.hexcolor !== settings.farewell.hexcolor) { + settings.farewell.hexcolor = data.hexcolor; + } + if (data.hexcolor && data.hexcolor !== settings.farewell.embed?.color) { settings.farewell.embed.color = data.hexcolor; } + if (data.image !== settings.farewell.image) { + settings.farewell.image = data.image; + } + + if (data.image && data.image !== settings.farewell.embed?.image) { + settings.farewell.embed.image = data.image; + } + data.thumbnail = data.thumbnail === "on" ? true : false; if (data.thumbnail !== (settings.farewell.embed?.thumbnail || false)) { settings.farewell.embed.thumbnail = data.thumbnail; } data.channel = guild.channels.cache.find((ch) => "#" + ch.name === data.channel)?.id; - if (data.channel !== settings.farewell.channel_id) { - settings.farewell.channel_id = data.channel; + if (data.channel !== settings.farewell.channel) { + settings.farewell.channel = data.channel; } if (!settings.farewell.enabled) settings.farewell.enabled = true; diff --git a/dashboard/utils.js b/dashboard/utils.js index 926b72cae..36260accb 100644 --- a/dashboard/utils.js +++ b/dashboard/utils.js @@ -12,8 +12,8 @@ async function fetchUser(userData, client, query) { if (userData.guilds) { userData.guilds.forEach((guild) => { if (guild.permissions) { - const perms = new Discord.Permissions(BigInt(guild.permissions)); - if (perms.has("MANAGE_GUILD")) guild.admin = true; + const perms = new Discord.PermissionsBitField(BigInt(guild.permissions)); + if (perms.has("ManageGuild")) guild.admin = true; } guild.settingsUrl = client.guilds.cache.get(guild.id) ? `/manage/${guild.id}/` @@ -33,7 +33,7 @@ async function fetchUser(userData, client, query) { } const user = await client.users.fetch(userData.id); user.displayAvatar = user.displayAvatarURL(); - const userDb = await getUser(user.id); + const userDb = await getUser(user); const userInfos = { ...user, ...userDb, ...userData, ...user.presence }; return userInfos; } diff --git a/dashboard/views/includes/sidebar.ejs b/dashboard/views/includes/sidebar.ejs index f1519bf81..ad65cfb47 100644 --- a/dashboard/views/includes/sidebar.ejs +++ b/dashboard/views/includes/sidebar.ejs @@ -4,13 +4,12 @@