├── .dockerignore ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── pull_request_template.md └── workflows │ ├── codeql-analysis.yml │ └── fly-deploy.yml ├── .gitignore ├── .prettierrc ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.MD ├── Dockerfile ├── LICENSE ├── README.md ├── Trivia_Bot.png ├── babel.config.json ├── fly.toml ├── package-lock.json ├── package.json ├── renovate.json └── src ├── Commands ├── help.js ├── info.js ├── mcchill.js ├── mccompetitive.js ├── ping.js ├── tfchill.js └── tfcompetitive.js ├── Events ├── messageCreate │ └── messageCreate.js └── ready.js ├── Structures ├── Command.js ├── Event.js ├── MDClient.js └── Util.js └── bot.js /.dockerignore: -------------------------------------------------------------------------------- 1 | # flyctl launch added from .gitignore 2 | **/.DS_Store 3 | **/.vscode 4 | **/node_modules 5 | **/.env 6 | **/*icloud 7 | **/.github 8 | fly.toml 9 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: elenirotsides 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # Description 2 | 3 | 4 | 5 | closes # 6 | 7 | ## Type of change 8 | 9 | 10 | 11 | - [ ] Bug fix (non-breaking change which fixes an issue) 12 | - [ ] New feature (non-breaking change which adds functionality) 13 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 14 | - [ ] This change requires a documentation update 15 | 16 | # Checklist: 17 | 18 | - [ ] My code follows the style guidelines of this project 19 | - [ ] I have performed a self-review of my own code 20 | - [ ] I have commented my code, particularly in hard-to-understand areas 21 | - [ ] I have made corresponding changes to the documentation (if applicable) 22 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | schedule: 21 | - cron: '24 22 * * 2' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'javascript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 37 | # Learn more: 38 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 39 | 40 | steps: 41 | - name: Checkout repository 42 | uses: actions/checkout@v3 43 | 44 | # Initializes the CodeQL tools for scanning. 45 | - name: Initialize CodeQL 46 | uses: github/codeql-action/init@v2 47 | with: 48 | languages: ${{ matrix.language }} 49 | # If you wish to specify custom queries, you can do so here or in a config file. 50 | # By default, queries listed here will override any specified in a config file. 51 | # Prefix the list here with "+" to use these queries and those in the config file. 52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 53 | 54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 55 | # If this step fails, then you should remove it and run the build manually (see below) 56 | - name: Autobuild 57 | uses: github/codeql-action/autobuild@v2 58 | 59 | # ℹ️ Command-line programs to run using the OS shell. 60 | # 📚 https://git.io/JvXDl 61 | 62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 63 | # and modify them (or add more) to build your code if your project 64 | # uses a compiled language 65 | 66 | #- run: | 67 | # make bootstrap 68 | # make release 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v2 72 | -------------------------------------------------------------------------------- /.github/workflows/fly-deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to Fly.io 2 | on: 3 | push: 4 | branches: [main] 5 | jobs: 6 | deploy: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - name: Checkout code 11 | uses: actions/checkout@v2 12 | 13 | - name: Set up Fly CLI 14 | uses: superfly/flyctl-actions/setup-flyctl@master 15 | 16 | - name: Deploy app to Fly.io 17 | run: flyctl deploy --app trivia-bot --remote-only 18 | env: 19 | FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .vscode 3 | node_modules 4 | .env 5 | *icloud -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 4, 4 | "semi": true, 5 | "singleQuote": true, 6 | "printWidth": 130 7 | } -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | elenitriviabot@gmail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.MD: -------------------------------------------------------------------------------- 1 | # Contributions are welcomed! 2 | 3 | ## Installation 4 | 5 | The (recommended) instructions on how to get Trivia Bot up and running in your Discord server are outlined below: 6 | 7 | _Prerequisites: You must have Node and NPM installed since this is a Node.js project. Please see [this website](https://www.npmjs.com/get-npm) for installation instructions before proceeding._ 8 | 9 | 1. Fork this repository 10 | 2. Create a clone of your fork so that you can make changes 11 | 12 | ``` 13 | git clone .git 14 | ``` 15 | 16 | 3. Once you've done that, navigate to the root of the `Trivia-Bot` directory on your local machine (or wherever you cloned your fork) and create a `.env` file. In here, you need to create an enviornment variable like so: 17 | 18 | ``` 19 | BOT_TOKEN= 20 | ``` 21 | 22 | _If you don't have a bot token, I recommend following the directions [here](https://www.writebots.com/discord-bot-token/). The author of this article did a fantastic job in explaining the process of getting a token and navigating the Discord Developer Portal (which is very simple and painless! Shout out to the Discord team for making this experience seamless!)_ 23 | 24 | Replace `` with your bot token and get rid of the < >'s. **Please gitignore this file when pushing your work in a PR. Your bot token should be known by no one other than yourself!** 25 | 26 | Make sure to run 27 | 28 | ``` 29 | npm install 30 | ``` 31 | 32 | to install all the project's required dependancies! 33 | 34 | You can run your bot with `npm start` but if you want it to automatically refresh every time you save, you can use `npm run dev` 35 | 36 | ### Note 37 | Please install the prettier extension and make sure to use our config. 38 | 39 | ## Where to start 40 | 41 | Please comment on any issue you'd like to take or open an issue to add something I haven't detailed out already. I'll decide later if its worth doing and will let you know. Or, open a PR with whatever you've done even if its not already fleshed out in an issue. **TLDR**; when in doubt, open a PR! 42 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Start image with a node base image 2 | FROM node:18-alpine 3 | 4 | # Create an application directory 5 | RUN mkdir -p /app 6 | 7 | # Set the /app directory as the working directory for any command that follows 8 | WORKDIR /app 9 | 10 | # Copy the local app package and package-lock.json file to the container 11 | COPY package*.json ./ 12 | 13 | # Copy local directories to the working directory of the docker image (/app) 14 | COPY ./src ./src 15 | 16 | # Install node packages 17 | RUN npm install 18 | 19 | # TODO: recursively delete node_modules once babel-node is no longer necessary 20 | # \ 21 | # && rm -fr node_modules 22 | 23 | # Start the app 24 | CMD ["npm", "run", "start"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Eleni Rotsides 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | Trivia Bot Logo 3 | 4 |
5 |
6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | # UPDATE 16 | 17 | I've been super busy with my full time job and have been unable to complete the full rewrite that I've started working on a little while back. So, **I've taken Trivia Bot offline for now**. Does that mean it'll never come back online? No! I'd like to take a different development approach with Trivia Bot in the future. Here's what I mean by that: 18 | 19 | When I originally designed and wrote the code for the bot, I was a college senior and this was originally a class project. The code was written very poorly as it was rushed. I was also an inexperienced developer at the time and it has been very difficult to scale the project given how crappy it was originally designed. So, I'd like to rewrite it in the future, but using an entirely different programming language. This means I need to completely rip it apart so I can put it back together, and this takes a lot of time. 20 | 21 | Please forgive me as life has been super busy these past few years. I love building discord bots and really want to revive Trivia Bot once and for all. For now, all I ask is that you forgive my silence. Thanks for the support these past few years, it meant the world to me as an aspiring software engineer! 22 | 23 | ## What is Trivia Bot? 24 | 25 | Trivia Bot is a fun, fully functional, and verified Discord bot that serves the purpose of satisfying your Trivia craving! There are many different modes of Trivia that you can play, either by yourself or with others in your Discord Server. The bot uses the Open Trivia Database API for the trivia questions that are used in the game, so shout out to them for the really cool API! You can find out more about that [here](https://opentdb.com/). 26 | 27 | **Please Note: Slash Commands are actively in the works and on the way!! For now, the bot is indeed broken for use in servers, but you can use normal commands in DMs.** 28 | 29 | ## Add to your server 30 | 31 | Click [here](https://discord.com/api/oauth2/authorize?client_id=831974682709721099&permissions=157760&scope=bot) to add Trivia Bot to your Discord server! Tell all your friends, too! 32 | 33 | ## Commands 34 | 35 | There are many different commands you can use to interact with Trivia Bot. _This section will be evolving as more features are continually added!_ Here is a list of the current commands: 36 | 37 | - `!tfchill` Starts a round of chill T/F Trivia. 38 | - `!tfcompetitive` Starts a round of competitive T/F Trivia. 39 | - `!mcchill` Starts a round of chill Multiple Choice Trivia. 40 | - `!mccompetitive` Starts a round of competitive Multiple Choice Trivia. 41 | - `!help` Lists out all the commands that Trivia Bot responds to, and what they do. 42 | - `!info` Responds with a Discord embed that contains links to `GitHub`, `Top.gg`, and the `Discord Support Server` 43 | - 🛑 Stops the current Trivia game 44 | 45 | The difference between `chill` and `competitive`: 46 | 47 | `chill` allows all users to select an answer within the time limit 48 | `competitive` only accepts the first correct answer; everyone else loses by default 49 | 50 | You can also append any of the commands to `help` to learn more about the different game modes, like so: 51 | 52 | - `!help tfchill` Will give more detail about this specific game mode, for example. 53 | 54 | ## Sub Commands 55 | 56 | **Trivia Bot now supports optional sub commands!** 57 | 58 | So...what does this mean for you? Trivia Bot can now take `time [seconds]` as an optional sub command so that you can extend the time limit per question in a Trivia round! This was done to make the game more accessible for those that can't read as quickly as others, or simply, for those that just want to take their time playing! 59 | 60 | _Please note: This option is only available for chill modes of gameplay. This is due to the nature of chill modes; competitive modes are meant to be quick, rapid-fire rounds which is why this option was not applied to competitive modes._ 61 | 62 | _Please note part 2: There will be more optional sub commands added in the future such as selecting more than 10 questions, specific category selection, and difficulty selection, to name a few. **Please stay tuned for these!**_ 63 | 64 | ### How to use the Sub Commands 65 | 66 | #### time [seconds] 67 | 68 | Let's say you want to play a round of `tfchill` but want to make each question have a time limit of 20 seconds: 69 | 70 | All you need to do is type the following: `!tfchill time 20` 71 | 72 | By default, Trivia Bot will give you 10 seconds per question. So, if you don't provide `time [seconds]`, then the bot will default to 10. 73 | 74 | **Limits:** 75 | 76 | - Minimum: 10 seconds 77 | - Maximum: 180 seconds 78 | - Can only be applied to `tfchill` or `mcchill` game modes 79 | 80 | ## Command Aliases 81 | 82 | Sometimes you'll mispell something and sometimes you'll want a quicker way to interact with the bot. Below are a list of aliases that exist so that your original intention will be recognized: 83 | 84 | - `!halp`, `!hwlp`, `!hrlp` works for `!help` 85 | - `!mchill` works for `!mcchill` 86 | - `!mcompetitive`, `!mccomp`, `!mcomp` works for `!mccompetitive` 87 | - `!pong` works for `!ping` 88 | - `!tfcomp` works for `!tfcompetitive` 89 | 90 | ## Contributing 91 | 92 | Please see [CONTRIBUTING.md](https://github.com/elenirotsides/Trivia-Bot/blob/main/CONTRIBUTING.MD) for instructions on how you can contribute to the development of this bot. Trivia Bot always welcomes PRs! 93 | 94 | ## Help 95 | 96 | Discussions have been enabled on this repository, so please feel free to ask any questions, make suggestions, etc. over [here](https://github.com/elenirotsides/Trivia-Bot/discussions) if you'd like! Please report bugs by opening up an issue for it in the [Issues](https://github.com/elenirotsides/Trivia-Bot/issues) tab. 97 | 98 | You can also join the Trivia Bot Support server and ask questions there, too: 99 | 100 | 101 | 102 | ## Known Limitations/Bugs 103 | 104 | Trivia Bot is still an active work in progress and therefore, it has some _quirks_ that still need to be ironed out. (Contributions are welcomed and encouraged, and this is a great place to start if you're wanting to dip your toes in the codebase.) This is a list of the issues that will be fixed eventually, but until then, please know that they're there: 105 | 106 | - This happens in both chill and competitive modes. 107 | 108 | > User 1 selects choice B, unselects choice B, then selects choice C. The correct answer was C and the bot accepts User 1's choice even though only the first selection should be considered. 109 | 110 | The Discord API limits what can be done with reactions (which is how Trivia Bot collects answers). The solution would be to disregard any other clicks after the first attempt has been executed. The issue relevant to fixing this can be found by clicking [here](https://github.com/elenirotsides/Trivia-Bot/issues/52). 111 | 112 | - The bot will throw an error, can be seen [here](https://github.com/elenirotsides/Trivia-Bot/issues/110), about Permissions, but I have no idea what causes it or how to fix it. I am actively investigating this and I hope to find a solution soon! 113 | -------------------------------------------------------------------------------- /Trivia_Bot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elenirotsides/Trivia-Bot/5ceb4d6a2bacfa90c50000d475972b1e2f14fe69/Trivia_Bot.png -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env"] 3 | } 4 | -------------------------------------------------------------------------------- /fly.toml: -------------------------------------------------------------------------------- 1 | # fly.toml file generated for trivia-bot on 2023-04-12T22:14:30-04:00 2 | 3 | app = "trivia-bot" 4 | kill_signal = "SIGINT" 5 | kill_timeout = 5 6 | mounts = [] 7 | primary_region = "ewr" 8 | processes = [] 9 | 10 | [[services]] 11 | internal_port = 8080 12 | processes = ["app"] 13 | protocol = "tcp" 14 | [services.concurrency] 15 | hard_limit = 25 16 | soft_limit = 20 17 | type = "connections" 18 | 19 | [[services.ports]] 20 | force_https = true 21 | handlers = ["http"] 22 | port = 80 23 | 24 | [[services.ports]] 25 | handlers = ["tls", "http"] 26 | port = 443 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "trivia-bot", 3 | "version": "1.0.1", 4 | "description": "## Team Members", 5 | "main": "bot.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "babel-node": "babel-node --presets='@babel/preset-env'", 9 | "start": "babel-node src/bot.js", 10 | "dev": "nodemon --exec npm run babel-node -- src/bot.js" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/elenirotsides/Trivia-Bot.git" 15 | }, 16 | "author": "", 17 | "license": "MIT", 18 | "bugs": { 19 | "url": "https://github.com/elenirotsides/Trivia-Bot/issues" 20 | }, 21 | "homepage": "https://github.com/elenirotsides/Trivia-Bot#readme", 22 | "dependencies": { 23 | "axios": "^0.27.0", 24 | "discord.js": "^13.8.0", 25 | "dotenv": "^16.0.0", 26 | "glob": "^8.0.0", 27 | "parse-entities": "^4.0.0", 28 | "path": "^0.12.7", 29 | "topgg-autoposter": "^2.0.1", 30 | "util": "^0.12.4" 31 | }, 32 | "devDependencies": { 33 | "@babel/core": "7.18.13", 34 | "@babel/node": "7.18.10", 35 | "@babel/preset-env": "7.18.10", 36 | "nodemon": "2.0.19" 37 | }, 38 | "type": "module" 39 | } 40 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:base", ":disableDependencyDashboard", "group:all", "schedule:weekends"], 3 | "schedule": ["every weekend"], 4 | "timezone": "America/New_York", 5 | "labels": ["dependencies"], 6 | "reviewers": ["elenirotsides"], 7 | "ignoreDeps": ["discord.js", "node", "npm"] 8 | } 9 | -------------------------------------------------------------------------------- /src/Commands/help.js: -------------------------------------------------------------------------------- 1 | import { MessageEmbed } from 'discord.js'; 2 | import Command from '../Structures/Command.js'; 3 | 4 | export default class extends Command { 5 | constructor(...args) { 6 | super(...args, { 7 | aliases: ['halp', 'hwlp', 'hrlp'], 8 | description: "Displays all the bot's commands", 9 | category: 'Utilities', 10 | usage: '[command]', 11 | }); 12 | } 13 | 14 | async run(message, commands) { 15 | const embed = new MessageEmbed() 16 | .setColor('#fb94d3') 17 | .setAuthor({ name: `Help Menu`, iconURL: message.guild === null ? null : message.guild.iconURL({ dynamic: true }) }) 18 | .setThumbnail(this.client.user.displayAvatarURL()) 19 | .setFooter({ text: `Requested by ${message.author.username}`, iconURL: message.author.displayAvatarURL({ dynamic: true }) }) 20 | .setTimestamp(); 21 | 22 | if (commands.length > 0) { 23 | // only consider the first command that a user asks for help on to avoid spam 24 | const cmd = this.client.commands.get(commands[0]) || this.client.commands.get(this.client.aliases.get(commands[0])); 25 | 26 | if (!cmd) { 27 | try { 28 | message.channel.send({ content: `No command named: \`${commands[0]}\` exists` }); 29 | return; 30 | } catch (e) { 31 | console.log(e); 32 | return; 33 | } 34 | } 35 | 36 | embed.setAuthor({ 37 | name: `Command Help: ${commands[0]}`, 38 | iconURL: message.guild === null ? null : message.guild.iconURL({ dynamic: true }), 39 | }); 40 | embed.setDescription( 41 | `**❯ Aliases:** ${cmd.aliases.length ? cmd.aliases.map((alias) => `\`${alias}\``).join(' ') : 'No Aliases'}\n**❯ Description:** ${ 42 | cmd.description 43 | }\n**❯ Category:** ${cmd.category}\n**❯ Usage:** ${cmd.usage}` 44 | ); 45 | 46 | try { 47 | message.channel.send({ embeds: [embed] }); 48 | return; 49 | } catch (e) { 50 | console.log(e); 51 | return; 52 | } 53 | } else { 54 | // This will be necessary later when I implement multi word commands 55 | // `Command Parameters: \`<>\` is strict & \`[]\` is optional`, 56 | embed.setDescription( 57 | `❯ The bot's prefix is: ${this.client.prefix}\n❯ For more detailed information on a specific command, type \`!help\` followed by any of the commands listed below\n\n**These are the available commands for Trivia Bot:**\n` 58 | ); 59 | let categories; 60 | if (!this.client.owners.includes(message.author.id)) { 61 | categories = this.client.utils.removeDuplicates( 62 | this.client.commands.filter((cmd) => cmd.category !== 'Owner').map((cmd) => cmd.category) 63 | ); 64 | } else { 65 | categories = this.client.utils.removeDuplicates(this.client.commands.map((cmd) => cmd.category)); 66 | } 67 | 68 | for (const category of categories) { 69 | embed.addField( 70 | `**${category}**`, 71 | this.client.commands 72 | .filter((cmd) => cmd.category === category) 73 | .map((cmd) => `\`${cmd.name}\``) 74 | .join(' ') 75 | ); 76 | } 77 | try { 78 | message.channel.send({ embeds: [embed] }); 79 | return; 80 | } catch (e) { 81 | console.log(e); 82 | return; 83 | } 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Commands/info.js: -------------------------------------------------------------------------------- 1 | import { MessageEmbed } from 'discord.js'; 2 | import Command from '../Structures/Command.js'; 3 | 4 | export default class extends Command { 5 | constructor(...args) { 6 | super(...args, { 7 | aliases: ['inf', 'support', 'request', 'bug', 'feature'], 8 | description: 'Displays extra info related to Trivia Bot that might be useful', 9 | category: 'Utilities', 10 | }); 11 | } 12 | 13 | async run(message, commands) { 14 | if (!this.validateCommands(message, commands)) { 15 | return; 16 | } 17 | const embed = new MessageEmbed() 18 | .setColor('#fb94d3') 19 | .setTitle("Hi, I'm Trivia Bot! 🧠") 20 | .setDescription( 21 | 'My prefix is `!` \n A list of commands can be found by sending `!help` \n \n [**Support Server**](https://discord.gg/wsyUhnDrmd) Questions? Join if you need help!' + 22 | "\n [**GitHub**](https://github.com/elenirotsides/Trivia-Bot) Are you a developer looking to support another fellow developer? Give me a star and I'll be eternally grateful!" + 23 | '\n [**Bug Reports/Feature Requests**](https://github.com/elenirotsides/Trivia-Bot/issues) You can submit bug reports or request features here.' + 24 | '\n [**Top.gg**](https://top.gg/bot/831974682709721099) Do you like Trivia Bot? Please consider voting and leaving a rating!' 25 | ) 26 | .setThumbnail(this.client.user.displayAvatarURL()) 27 | .setFooter({ text: 'Yours truly, Trivia Bot ❤️' }) 28 | .setTimestamp(); 29 | 30 | try { 31 | message.channel.send({ embeds: [embed] }); 32 | } catch (e) { 33 | console.log(e); 34 | return; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Commands/mcchill.js: -------------------------------------------------------------------------------- 1 | import Command from '../Structures/Command.js'; 2 | import axios from 'axios'; 3 | import { parseEntities } from 'parse-entities'; 4 | import { MessageEmbed } from 'discord.js'; 5 | 6 | export default class extends Command { 7 | constructor(...args) { 8 | super(...args, { 9 | aliases: ['mcchill', 'mchill'], 10 | description: 11 | 'Initiates a round of 10 question Multiple Choice trivia with random difficulties and random categories. Its `chill` because this mode allows all users to attempt to answer within the 10 second time limit.', 12 | category: 'Game Modes', 13 | usage: 'time [seconds (10 to 180)]', 14 | optSubCommands: ['time'], 15 | }); 16 | } 17 | 18 | async run(message, commands) { 19 | const parsedCommands = this.validateCommands(message, commands); 20 | if (!parsedCommands) { 21 | return; 22 | } 23 | let time = parsedCommands.time; 24 | if (isNaN(time)) { 25 | time = 10000; // 10 seconds default 26 | } else { 27 | time = time * 1000; 28 | } 29 | 30 | // sends a cute lil message to the channel letting the users know that a game will begin 31 | try { 32 | message.channel.send({ content: `Lemme grab some questions for ya....\nYou have ${time / 1000} seconds to answer each question` }); 33 | } catch (e) { 34 | console.log(e); 35 | return; 36 | } 37 | 38 | let triviaData; 39 | // will hold the response that the api gave after a successful request 40 | try { 41 | // the api call is wrapped in a try/catch because it can fail, and we don't want our program to crash 42 | triviaData = await (await axios(`https://opentdb.com/api.php?amount=10&type=multiple`)).data.results; 43 | } catch (e) { 44 | // if the api call does fail, we log the result and then send a cute lil error to the channel 45 | console.log(e); 46 | try { 47 | message.channel.send({ content: 'Uh oh, something has gone wrong while trying to get some questions. Please try again' }); 48 | } catch (e) { 49 | console.log(e); 50 | return; 51 | } 52 | } 53 | 54 | // looping over the length of the api response, and adding entries to the triviaData object with all the data we need in a structure that works for us 55 | 56 | const embed = new MessageEmbed(); // creates new embed instance 57 | let counter = 10; // a counter that will help us execute the other channel messages later (helps us keep track of loop iterations) 58 | let stopped = false; 59 | /* instantiate empty leaderboard object where we'll store leaderboard stats 60 | Takes the form: 61 | { 62 | elmo: 4, 63 | bobthebuilder: 7, 64 | ....and so on and so forth.... 65 | } 66 | */ 67 | let leaderboard = {}; 68 | 69 | function shuffle(array) { 70 | var currentIndex = array.length, 71 | randomIndex, 72 | temporaryValue; 73 | 74 | // While there remain elements to shuffle... 75 | while (0 !== currentIndex) { 76 | // Pick a remaining element... 77 | randomIndex = Math.floor(Math.random() * currentIndex); 78 | currentIndex -= 1; 79 | 80 | // And swap it with the current element. 81 | temporaryValue = array[currentIndex]; 82 | array[currentIndex] = array[randomIndex]; 83 | array[randomIndex] = temporaryValue; 84 | } 85 | 86 | return array; 87 | } 88 | 89 | /* and now the fun begins..... 90 | Loops over the contents of triviaData, and sends the question in an embed after the completion of the embed construction 91 | */ 92 | for (let i = 0; i < triviaData.length; i++) { 93 | let choices = [`${triviaData[i].correct_answer}`]; 94 | for (let j = 0; j < 3; j++) { 95 | choices.push(`${triviaData[i].incorrect_answers[j]}`); 96 | } 97 | shuffle(choices); 98 | 99 | embed 100 | .setTitle(`Question ${i + 1}`) // Title dynamically updates depending on which iteration we're on 101 | .setColor('#5fdbe3') // color of the embed for multiple choice 102 | .setDescription( 103 | // the meat and potatoes of the embed 104 | parseEntities(triviaData[i].question) + // the question 105 | '\n' + 106 | '\n**Choices:**' + 107 | '\n' + 108 | '\n 🇦 ' + 109 | parseEntities(choices[0]) + 110 | '\n 🇧 ' + 111 | parseEntities(choices[1]) + 112 | '\n 🇨 ' + 113 | parseEntities(choices[2]) + 114 | '\n 🇩 ' + 115 | parseEntities(choices[3]) + 116 | '\n' + 117 | '\n**Difficulty:** ' + // putting double ** bolds the text, and single * italicizes it (in the Discord application) 118 | parseEntities(triviaData[i].difficulty) + // difficulty 119 | '\n**Category:** ' + 120 | parseEntities(triviaData[i].category) // category 121 | ); 122 | 123 | let msgEmbed; 124 | try { 125 | msgEmbed = await message.channel.send({ embeds: [embed] }); // sends the embed 126 | } catch (e) { 127 | console.log(e); 128 | return; 129 | } 130 | msgEmbed.react('🇦'); // adds a universal A emoji 131 | msgEmbed.react('🇧'); // adds a universal B emoji 132 | msgEmbed.react('🇨'); // adds a universal C emoji 133 | msgEmbed.react('🇩'); // adds a universal D emoji 134 | msgEmbed.react('🛑'); // add a stop reaction 135 | 136 | let answer = ''; // instantiate empty answer string, where correctAns will be housed 137 | if (triviaData[i].correct_answer === choices[0]) { 138 | // if the correct answer is in index 0, answer is equal to the A emoji 139 | answer = '🇦'; 140 | } 141 | if (triviaData[i].correct_answer === choices[1]) { 142 | // if the correct answer is in index 1, answer is equal to the B emoji 143 | answer = '🇧'; 144 | } 145 | if (triviaData[i].correct_answer === choices[2]) { 146 | // if the correct answer is in index 2, answer is equal to the C emoji 147 | answer = '🇨'; 148 | } 149 | if (triviaData[i].correct_answer === choices[3]) { 150 | // otherwise its equal to the D emoji 151 | answer = '🇩'; 152 | } 153 | 154 | // the createReactionCollector takes in a filter function, so we need to create the basis for what that filter is here 155 | const filter = (reaction, user) => { 156 | // filters only the reactions that are equal to the answer 157 | return (reaction.emoji.name === answer || reaction.emoji.name === '🛑') && user.username !== this.client.user.username; 158 | }; 159 | 160 | // adds createReactionCollector to the embed we sent, so we can 'collect' all the correct answers 161 | const collector = msgEmbed.createReactionCollector({ filter, time: time }); // will only collect for n seconds 162 | 163 | // an array that will hold all the users that answered correctly 164 | let usersWithCorrectAnswer = []; 165 | 166 | // starts collecting 167 | // r is reaction and user is user 168 | collector.on('collect', (r, user) => { 169 | // if the user is not the bot, and the reaction given is equal to the answer 170 | // add the users that answered correctly to the usersWithCorrect Answer array 171 | if (r.emoji.name === '🛑') { 172 | counter = 0; 173 | stopped = true; 174 | collector.stop(); 175 | } else { 176 | usersWithCorrectAnswer.push(user.username); 177 | if (leaderboard[user.username] === undefined) { 178 | // if the user isn't already in the leaderboard object, add them and give them a score of 1 179 | leaderboard[user.username] = 1; 180 | } else { 181 | // otherwise, increment the user's score 182 | leaderboard[user.username] += 1; 183 | } 184 | } 185 | }); 186 | 187 | let newEmbed = new MessageEmbed(); // new embed instance 188 | let result; 189 | 190 | // what will be executed when the collector completes 191 | collector.on('end', async () => { 192 | // if no one got any answers right 193 | if (usersWithCorrectAnswer.length === 0) { 194 | // create an embed 195 | result = newEmbed 196 | .setTitle("Time's Up! No one got it....") 197 | .setColor('#f40404') 198 | .setDescription('\n The correct answer was ' + parseEntities(triviaData[i].correct_answer)); 199 | 200 | // send the embed to the channel if the game wasn't terminated 201 | if (!stopped) { 202 | try { 203 | message.channel.send({ embeds: [result] }); 204 | } catch (e) { 205 | console.log(e); 206 | return; 207 | } 208 | } 209 | } else { 210 | // otherwise, create an embed with the results of the question 211 | /* 212 | since the array is an array of strings, I used the javascript join() method to concat them, and then the replace() to replace the 213 | comma with a comma and a space, so its human readable and pleasant to the eye 214 | */ 215 | result = newEmbed 216 | .setTitle("Time's Up! Here's who got it right:") 217 | .setDescription(usersWithCorrectAnswer.join().replace(',', ', ')) 218 | .setFooter({ text: '\n The correct answer was ' + parseEntities(triviaData[i].correct_answer) }) 219 | .setColor('#f40404'); 220 | // send the embed to the channel if the game wasn't terminated 221 | if (!stopped) { 222 | try { 223 | message.channel.send({ embeds: [result] }); 224 | } catch (e) { 225 | console.log(e); 226 | return; 227 | } 228 | } 229 | } 230 | if (stopped) { 231 | // if the game was stopped, then we need to send the the scores to the guild 232 | 233 | // iterate over the leaderboard if winners exist (if the length of the object's keys isn't 0, then we have winners) 234 | if (Object.keys(leaderboard).length !== 0) { 235 | // send the embed to the channel after the edit is complete 236 | message.channel.send({ embeds: [result] }).then((msg) => { 237 | // loop over the contents of the leaderboard, and add fields to the embed on every iteration 238 | for (const key in leaderboard) { 239 | result.addField(`${key}:`, `${leaderboard[key]}`.toString()); 240 | } 241 | 242 | // to avoid exceeding the rate limit, we will be editing the result embed instead of sending a new one 243 | // msg.edit(result.setTitle('**Game Over!**\nFinal Scores:').setDescription('').setColor('#fb94d3')); 244 | msg.edit({ embeds: [result.setTitle('**Game Over!**\nFinal Scores:').setDescription('').setColor('#fb94d3')] }); 245 | }); 246 | } else { 247 | // if the leaderboard is empty, construct a different embed 248 | 249 | // send the embed to the channel after the edit is complete 250 | message.channel.send({ embeds: [result] }).then((msg) => { 251 | // to avoid exceeding the rate limit, we will be editing the result embed instead of sending a new one 252 | // msg.edit(result.setTitle('Game Over! No one got anything right....').setColor('#fb94d3')); 253 | msg.edit({ embeds: [result.setTitle('Game Over! No one got anything right....').setColor('#fb94d3')] }); 254 | }); 255 | } 256 | // so the for loop can stop executing 257 | triviaData.length = 0; 258 | } 259 | }); 260 | if (counter === 0 || stopped) { 261 | break; 262 | } 263 | /* if I don't include a pause of some sort, then the for loop will RAPID FIRE send all the questions to the channel 264 | adding a pause here that is equal to the collection time allows for time in between questions, and an 265 | overall pleasant user experience 266 | */ 267 | await this.client.utils.wait(time); 268 | 269 | // decrement the counter, tbh I don't know if having a counter is necessary now that I'm looking at this....we can fix this later 270 | counter--; 271 | } 272 | if (counter === 0 && !stopped) { 273 | // if the game wasn't triggered to stop before the questions ran out, then here is where the results will be sent to the guild 274 | 275 | let winnerEmbed = new MessageEmbed(); // create new embed instance 276 | 277 | // iterate over the leaderboard if winners exist (if the length of the object's keys isn't 0, then we have winners) 278 | if (Object.keys(leaderboard).length !== 0) { 279 | // specify the contents of the embed 280 | let winner = winnerEmbed.setTitle('**Game Over!**\nFinal Scores:').setColor('#fb94d3'); 281 | 282 | // loop over the contents of the leaderboard, and add fields to the embed on every iteration 283 | for (const key in leaderboard) { 284 | winner.addField(`${key}:`, `${leaderboard[key]}`.toString()); 285 | } 286 | try { 287 | message.channel.send({ embeds: [winner] }); 288 | } catch (e) { 289 | console.log(e); 290 | return; 291 | } 292 | } else { 293 | // if the leaderboard is empty, construct a different embed 294 | winnerEmbed.setTitle('Game Over! No one got anything right...').setColor('#fb94d3'); 295 | // send the embed to the channel 296 | try { 297 | message.channel.send({ embeds: [winnerEmbed] }); 298 | } catch (e) { 299 | console.log(e); 300 | return; 301 | } 302 | } 303 | } 304 | } 305 | } 306 | -------------------------------------------------------------------------------- /src/Commands/mccompetitive.js: -------------------------------------------------------------------------------- 1 | import Command from '../Structures/Command.js'; 2 | import axios from 'axios'; 3 | import { parseEntities } from 'parse-entities'; 4 | import { MessageEmbed } from 'discord.js'; 5 | 6 | export default class extends Command { 7 | constructor(...args) { 8 | super(...args, { 9 | aliases: ['mccompetitive', 'mcompetitive', 'mccomp', 'mcomp'], 10 | description: 11 | 'Initiates a round of 10 question Multiple Choice trivia with random difficulties and random categories. Its `competitive` because this will only accept the first person that guesses correctly; everyone else loses by default. **TLDR; you have to be the first to answer correctly!**', 12 | category: 'Game Modes', 13 | //usage: '[time]', 14 | }); 15 | } 16 | 17 | async run(message, commands) { 18 | if (!this.validateCommands(message, commands)) { 19 | return; 20 | } 21 | 22 | // sends a cute lil message to the channel letting the users know that a game will begin 23 | try { 24 | message.channel.send({ content: 'Lemme grab some questions for ya....' }); 25 | } catch (e) { 26 | console.log(e); 27 | return; 28 | } 29 | 30 | /* creating empty trivia data variable for this round of trivia 31 | It will be filled with data that was queried from the api, like so: 32 | (the api sends it as an array of objects) 33 | 34 | [ 35 | { 36 | category: "Entertainment: Television", 37 | type: "multiple", 38 | difficulty: "medium", 39 | question: "In the original Doctor Who series (1963), fourth doctor Tom Baker's scarf was how long?", 40 | correct_answer: "7 Meters", 41 | incorrect_answers: [ 42 | "10 Meters", 43 | "2 Meters", 44 | "5 Meters" 45 | ] 46 | }, 47 | { 48 | ....so on and so forth.... 49 | } 50 | ] 51 | 52 | Notice the data that the api sends back has more data than what we need; that's okay, we just won't use it 53 | */ 54 | let triviaData; // will hold the response that the api gave after a successful request 55 | 56 | try { 57 | // the api call is wrapped in a try/catch because it can fail, and we don't want our program to crash 58 | triviaData = await (await axios(`https://opentdb.com/api.php?amount=10&type=multiple`)).data.results; 59 | } catch (e) { 60 | // if the api call does fail, we log the result and then send a cute lil error to the channel 61 | console.log(e); 62 | try { 63 | message.channel.send({ content: 'Uh oh, something has gone wrong while trying to get some questions. Please try again' }); 64 | } catch (e) { 65 | console.log(e); 66 | return; 67 | } 68 | } 69 | 70 | const embed = new MessageEmbed(); // creates new embed instance 71 | let counter = 10; // a counter that will help us execute the other channel messages later (helps us keep track of loop iterations) 72 | let stopped = false; 73 | 74 | /* instantiate empty leaderboard object where we'll store leaderboard stats 75 | Takes the form: 76 | { 77 | elmo: 4, 78 | bobthebuilder: 7, 79 | ....and so on and so forth.... 80 | } 81 | */ 82 | let leaderboard = {}; 83 | 84 | function shuffle(array) { 85 | var currentIndex = array.length, 86 | randomIndex, 87 | temporaryValue; 88 | 89 | // While there remain elements to shuffle... 90 | while (0 !== currentIndex) { 91 | // Pick a remaining element... 92 | randomIndex = Math.floor(Math.random() * currentIndex); 93 | currentIndex -= 1; 94 | 95 | // And swap it with the current element. 96 | temporaryValue = array[currentIndex]; 97 | array[currentIndex] = array[randomIndex]; 98 | array[randomIndex] = temporaryValue; 99 | } 100 | 101 | return array; 102 | } 103 | 104 | /* and now the fun begins..... 105 | Loops over the contents of triviaData, and sends the question in an embed after the completion of the embed construction 106 | */ 107 | for (let i = 0; i < triviaData.length; i++) { 108 | let choices = [`${triviaData[i].correct_answer}`]; // for testing, inputs the correct answer as the first choice for each question 109 | for (let j = 0; j < 3; j++) { 110 | // adds the incorrect answers into the choices array created before 111 | choices.push(`${triviaData[i].incorrect_answers[j]}`); 112 | } 113 | shuffle(choices); 114 | 115 | embed 116 | .setTitle(`Question ${i + 1}`) // Title dynamically updates depending on which iteration we're on 117 | .setColor('#5fdbe3') // color of the embed 118 | .setDescription( 119 | // the meat and potatoes of the embed 120 | parseEntities(triviaData[i].question) + // the question 121 | '\n' + // added a space 122 | '\n**Choices:**' + // added a space 123 | '\n' + 124 | '\n🇦 ' + 125 | parseEntities(choices[0]) + // outputs the choices from the array 'choices' 126 | '\n🇧 ' + 127 | parseEntities(choices[1]) + 128 | '\n🇨 ' + 129 | parseEntities(choices[2]) + 130 | '\n🇩 ' + 131 | parseEntities(choices[3]) + 132 | '\n' + 133 | '\n**Difficulty:** ' + // putting double ** bolds the text, and single * italicizes it (in the Discord application) 134 | parseEntities(triviaData[i].difficulty) + // difficulty 135 | '\n**Category:** ' + 136 | parseEntities(triviaData[i].category) // category 137 | ); 138 | 139 | let msgEmbed; 140 | try { 141 | msgEmbed = await message.channel.send({ embeds: [embed] }); // sends the embed 142 | } catch (e) { 143 | console.log(e); 144 | return; 145 | } 146 | msgEmbed.react('🇦'); // adds a universal A emoji 147 | msgEmbed.react('🇧'); // adds a universal B emoji 148 | msgEmbed.react('🇨'); // and so on... 149 | msgEmbed.react('🇩'); 150 | msgEmbed.react('🛑'); 151 | 152 | let answer = ''; // instantiate empty answer string, where correctAns will be housed 153 | 154 | if (triviaData[i].correct_answer === choices[0]) { 155 | // if the correct answer is the instance in the array, answer is equal to the corresponding letter emoji 156 | answer = '🇦'; 157 | } else if (triviaData[i].correct_answer === choices[1]) { 158 | // otherwise its incorrect emoji 159 | answer = '🇧'; 160 | } else if (triviaData[i].correct_answer === choices[2]) { 161 | // otherwise its incorrect emoji 162 | answer = '🇨'; 163 | } else { 164 | answer = '🇩'; 165 | } 166 | 167 | // the createReactionCollector takes in a filter function, so we need to create the basis for what that filter is here 168 | const filter = (reaction, user) => { 169 | // filters only the reactions that are equal to the answer 170 | return (reaction.emoji.name === answer || reaction.emoji.name === '🛑') && user.username !== this.client.user.username; 171 | }; 172 | 173 | // adds createReactionCollector to the embed we sent, so we can 'collect' all the correct answers 174 | const collector = msgEmbed.createReactionCollector({ filter, max: 1, time: 10000 }); // will only collect for 10 seconds, and take one correct answer 175 | 176 | // an array that will hold all the users that answered correctly 177 | let usersWithCorrectAnswer = []; 178 | 179 | // starts collecting 180 | // r is reaction and user is user 181 | collector.on('collect', (r, user) => { 182 | // if the user is not the bot, and the reaction given is equal to the answer 183 | // add the users that answered correctly to the usersWithCorrect Answer array 184 | if (r.emoji.name === '🛑') { 185 | counter = 0; 186 | stopped = true; 187 | collector.stop(); 188 | } else { 189 | usersWithCorrectAnswer.push(user.username); 190 | if (leaderboard[user.username] === undefined) { 191 | // if the user isn't already in the leaderboard object, add them and give them a score of 1 192 | leaderboard[user.username] = 1; 193 | } else { 194 | // otherwise, increment the user's score 195 | leaderboard[user.username] += 1; 196 | } 197 | } 198 | }); 199 | let newEmbed = new MessageEmbed(); // new embed instance 200 | let result; 201 | 202 | // what will be executed when the collector completes 203 | collector.on('end', async () => { 204 | // if no one got any answers right 205 | if (usersWithCorrectAnswer.length === 0) { 206 | // create an embed 207 | result = newEmbed 208 | .setTitle("Time's Up! No one got it....") 209 | .setDescription('\n The correct answer was ' + parseEntities(triviaData[i].correct_answer)) 210 | .setColor('#f40404'); 211 | // send the embed to the channel if the game wasn't terminated 212 | if (!stopped) { 213 | try { 214 | message.channel.send({ embeds: [result] }); 215 | } catch (e) { 216 | console.log(e); 217 | return; 218 | } 219 | } 220 | } else { 221 | // otherwise, create an embed with the results of the question 222 | /* since the array is an array of strings, I used the javascript join() method to concat them, and then the replace() to replace the 223 | comma with a comma and a space, so its human readable and pleasant to the eye 224 | */ 225 | result = newEmbed 226 | .setTitle("That's IT! Here's who got it first:") 227 | .setDescription(usersWithCorrectAnswer.join().replace(',', ', ')) 228 | .setFooter({ text: '\n The correct answer was ' + parseEntities(triviaData[i].correct_answer) }) 229 | .setColor('#f40404'); 230 | // send the embed to the channel if the game wasn't terminated 231 | if (!stopped) { 232 | try { 233 | message.channel.send({ embeds: [result] }); 234 | } catch (e) { 235 | console.log(e); 236 | return; 237 | } 238 | } 239 | } 240 | if (stopped) { 241 | // if the game was stopped, then we need to send the the scores to the guild 242 | 243 | // iterate over the leaderboard if winners exist (if the length of the object's keys isn't 0, then we have winners) 244 | if (Object.keys(leaderboard).length !== 0) { 245 | // send the embed to the channel after the edit is complete 246 | message.channel.send({ embeds: [result] }).then((msg) => { 247 | // loop over the contents of the leaderboard, and add fields to the embed on every iteration 248 | for (const key in leaderboard) { 249 | result.addField(`${key}:`, `${leaderboard[key]}`.toString()); 250 | } 251 | 252 | // to avoid exceeding the rate limit, we will be editing the result embed instead of sending a new one 253 | msg.edit({ embeds: [result.setTitle('**Game Over!**\nFinal Scores:').setDescription('').setColor('#fb94d3')] }); 254 | }); 255 | } else { 256 | // if the leaderboard is empty, construct a different embed 257 | 258 | // send the embed to the channel after the edit is complete 259 | message.channel.send({ embeds: [result] }).then((msg) => { 260 | // to avoid exceeding the rate limit, we will be editing the result embed instead of sending a new one 261 | msg.edit({ embeds: [result.setTitle('Game Over! No one got anything right....').setColor('#fb94d3')] }); 262 | }); 263 | } 264 | // so the for loop can stop executing 265 | triviaData.length = 0; 266 | } 267 | }); 268 | if (counter === 0 || stopped) { 269 | break; 270 | } 271 | /* if I don't include a pause of some sort, then the for loop will RAPID FIRE send all the questions to the channel 272 | adding a pause here that is equal to the collection time (10 seconds) allows for time in between questions, and an 273 | overall pleasant user experience 274 | */ 275 | await this.client.utils.wait(10000); 276 | // decrement the counter, tbh I don't know if having a counter is necessary now that I'm looking at this....we can fix this later 277 | 278 | counter--; 279 | } 280 | if (counter === 0 && !stopped) { 281 | let winnerEmbed = new MessageEmbed(); // create new embed instance 282 | 283 | // iterate over the leaderboard if winners exist (if the length of the object's keys isn't 0, then we have winners) 284 | if (Object.keys(leaderboard).length !== 0) { 285 | // specify the contents of the embed 286 | let winner = winnerEmbed.setTitle('**Game Over!**\nFinal Scores:').setColor('#fb94d3'); 287 | 288 | // loop over the contents of the leaderboard, and add fields to the embed on every iteration 289 | for (const key in leaderboard) { 290 | winner.addField(`${key}:`, `${leaderboard[key]}`.toString()); 291 | } 292 | try { 293 | message.channel.send({ embeds: [winner] }); 294 | } catch (e) { 295 | console.log(e); 296 | return; 297 | } 298 | } else { 299 | // if the leaderboard is empty, construct a different embed 300 | winnerEmbed.setTitle('Game Over! No one got anything right...').setColor('#fb94d3'); 301 | // send the embed to the channel 302 | try { 303 | message.channel.send({ embeds: [winnerEmbed] }); 304 | } catch (e) { 305 | console.log(e); 306 | return; 307 | } 308 | } 309 | } 310 | } 311 | } 312 | -------------------------------------------------------------------------------- /src/Commands/ping.js: -------------------------------------------------------------------------------- 1 | import Command from '../Structures/Command.js'; 2 | 3 | export default class extends Command { 4 | constructor(...args) { 5 | super(...args, { 6 | aliases: ['pong'], 7 | description: 'This provides the ping of the bot', 8 | category: 'Utilities', 9 | }); 10 | } 11 | 12 | async run(message, commands) { 13 | if (!this.validateCommands(message, commands)) { 14 | return; 15 | } 16 | 17 | let msg; 18 | try { 19 | msg = await message.channel.send({ content: 'Pinging...' }); 20 | } catch (e) { 21 | console.log(e); 22 | return; 23 | } 24 | const latency = msg.createdTimestamp - message.createdTimestamp; 25 | 26 | msg.edit(`Bot Latency: \`${latency}ms\`, API Latency: \`${Math.round(this.client.ws.ping)}ms\``); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Commands/tfchill.js: -------------------------------------------------------------------------------- 1 | import Command from '../Structures/Command.js'; 2 | import axios from 'axios'; 3 | import { parseEntities } from 'parse-entities'; 4 | import { MessageEmbed } from 'discord.js'; 5 | 6 | export default class extends Command { 7 | constructor(...args) { 8 | super(...args, { 9 | description: 10 | 'Initiates a round of 10 question T/F trivia with random difficulties and random categories. Its `chill` because this mode allows all users to attempt to answer within the 10 second time limit.', 11 | category: 'Game Modes', 12 | usage: 'time [seconds (10 to 180)]', 13 | optSubCommands: ['time'], 14 | }); 15 | } 16 | 17 | async run(message, commands) { 18 | const parsedCommands = this.validateCommands(message, commands); 19 | if (!parsedCommands) { 20 | return; 21 | } 22 | let time = parsedCommands.time; 23 | if (isNaN(time)) { 24 | time = 10000; // 10 seconds default 25 | } else { 26 | time = time * 1000; 27 | } 28 | 29 | // sends a cute lil message to the channel letting the users know that a game will begin 30 | try { 31 | message.channel.send({ content: `Lemme grab some questions for ya....\nYou have ${time / 1000} seconds to answer each question` }); 32 | } catch (e) { 33 | console.log(e); 34 | return; 35 | } 36 | 37 | /* creating empty trivia data variable for this round of trivia 38 | It will be filled with data that was queried from the api, like so: 39 | (the api sends it as an array of objects) 40 | 41 | [ 42 | { 43 | "category": "Entertainment: Film", 44 | "type": "boolean", 45 | "difficulty": "easy", 46 | "question": "Han Solo's co-pilot and best friend, "Chewbacca", is an Ewok.", 47 | "correct_answer": "False", 48 | "incorrect_answers": ["True"] 49 | }, 50 | { 51 | ....so on and so forth.... 52 | } 53 | ] 54 | 55 | Notice the data that the api sends back has more data than what we need; that's okay, we just won't use it 56 | */ 57 | let triviaData; // will hold the response that the api gave after a successful request 58 | 59 | try { 60 | // the api call is wrapped in a try/catch because it can fail, and we don't want our program to crash 61 | triviaData = await (await axios(`https://opentdb.com/api.php?amount=10&type=boolean`)).data.results; 62 | } catch (e) { 63 | // if the api call does fail, we log the result and then send a cute lil error to the channel 64 | console.log(e); 65 | try { 66 | message.channel.send({ content: 'Uh oh, something has gone wrong while trying to get some questions. Please try again' }); 67 | } catch (e) { 68 | console.log(e); 69 | return; 70 | } 71 | } 72 | 73 | const embed = new MessageEmbed(); // creates new embed instance 74 | let counter = 10; // a counter that will help us execute the other channel messages later (helps us keep track of loop iterations) 75 | let stopped = false; // will help control the behaivor of the embeds and game termination since behavior of the message collector is limited 76 | 77 | /* instantiate empty leaderboard object where we'll store leaderboard stats 78 | Takes the form: 79 | { 80 | elmo: 4, 81 | bobthebuilder: 7, 82 | ....and so on and so forth.... 83 | } 84 | */ 85 | let leaderboard = {}; 86 | 87 | /* and now the fun begins..... 88 | Loops over the contents of triviaData, and sends the question in an embed after the completion of the embed construction 89 | */ 90 | for (let i = 0; i < triviaData.length; i++) { 91 | embed 92 | .setTitle(`Question ${i + 1}`) // Title dynamically updates depending on which iteration we're on 93 | .setColor('#5fdbe3') // color of the embed 94 | .setDescription( 95 | // the meat and potatoes of the embed 96 | parseEntities(triviaData[i].question) + // the question 97 | '\n' + // added a space 98 | '\n**Difficulty:** ' + // putting double ** bolds the text, and single * italicizes it (in the Discord application) 99 | parseEntities(triviaData[i].difficulty) + // difficulty 100 | '\n**Category:** ' + 101 | parseEntities(triviaData[i].category) // category 102 | ); 103 | 104 | let msgEmbed; 105 | try { 106 | msgEmbed = await message.channel.send({ embeds: [embed] }); // sends the embed 107 | } catch (e) { 108 | console.log(e); 109 | return; 110 | } 111 | msgEmbed.react('🇹'); // adds a universal T emoji 112 | msgEmbed.react('🇫'); // adds a universal F emoji 113 | msgEmbed.react('🛑'); // adds a universal stop sign 114 | 115 | let answer = ''; // instantiate empty answer string, where correctAns will be housed 116 | if (triviaData[i].correct_answer === 'True') { 117 | // if the correct answer is True, answer is equal to the T emoji 118 | answer = '🇹'; 119 | } else { 120 | // otherwise its equal to the F emoji 121 | answer = '🇫'; 122 | } 123 | 124 | // the createReactionCollector takes in a filter function, so we need to create the basis for what that filter is here 125 | const filter = (reaction, user) => { 126 | // filters only the reactions that are equal to the answer 127 | return (reaction.emoji.name === answer || reaction.emoji.name === '🛑') && user.username !== this.client.user.username; 128 | }; 129 | 130 | // adds createReactionCollector to the embed we sent, so we can 'collect' all the correct answers 131 | const collector = msgEmbed.createReactionCollector({ filter, time: time }); // will only collect for n seconds 132 | 133 | // an array that will hold all the users that answered correctly 134 | let usersWithCorrectAnswer = []; 135 | 136 | // starts collecting 137 | // r is reaction and user is user 138 | collector.on('collect', (r, user) => { 139 | // if the user is not the bot, and the reaction given is equal to the answer 140 | // add the users that answered correctly to the usersWithCorrect Answer array 141 | if (r.emoji.name === '🛑') { 142 | counter = 0; 143 | stopped = true; 144 | collector.stop(); 145 | } else { 146 | usersWithCorrectAnswer.push(user.username); 147 | if (leaderboard[user.username] === undefined) { 148 | // if the user isn't already in the leaderboard object, add them and give them a score of 1 149 | leaderboard[user.username] = 1; 150 | } else { 151 | // otherwise, increment the user's score 152 | leaderboard[user.username] += 1; 153 | } 154 | } 155 | }); 156 | let newEmbed = new MessageEmbed(); // new embed instance 157 | let result; 158 | 159 | // what will be executed when the collector completes 160 | collector.on('end', () => { 161 | // if no one got any answers right 162 | if (usersWithCorrectAnswer.length === 0) { 163 | // create an embed 164 | result = newEmbed 165 | .setTitle("Time's Up! No one got it....") 166 | .setDescription('\n The correct answer was ' + parseEntities(triviaData[i].correct_answer)) 167 | .setColor('#f40404'); 168 | // send the embed to the channel if the game wasn't terminated 169 | if (!stopped) { 170 | try { 171 | message.channel.send({ embeds: [result] }); 172 | } catch (e) { 173 | console.log(e); 174 | return; 175 | } 176 | } 177 | } else { 178 | // otherwise, create an embed with the results of the question 179 | 180 | /* 181 | since the array is an array of strings, I used the javascript join() method to concat them, and then the replace() to replace the 182 | comma with a comma and a space, so its human readable and pleasant to the eye 183 | */ 184 | result = newEmbed 185 | .setTitle("Time's Up! Here's who got it right:") 186 | .setDescription(usersWithCorrectAnswer.join().replace(',', ', ')) 187 | .setFooter({ text: '\n The correct answer was ' + parseEntities(triviaData[i].correct_answer) }) 188 | .setColor('#f40404'); 189 | // send the embed to the channel if the game wasn't terminated 190 | if (!stopped) { 191 | try { 192 | message.channel.send({ embeds: [result] }); 193 | } catch (e) { 194 | console.log(e); 195 | return; 196 | } 197 | } 198 | } 199 | if (stopped) { 200 | // if the game was stopped, then we need to send the the scores to the guild 201 | 202 | // iterate over the leaderboard if winners exist (if the length of the object's keys isn't 0, then we have winners) 203 | if (Object.keys(leaderboard).length !== 0) { 204 | // send the embed to the channel after the edit is complete 205 | message.channel.send({ embeds: [result] }).then((msg) => { 206 | // loop over the contents of the leaderboard, and add fields to the embed on every iteration 207 | for (const key in leaderboard) { 208 | result.addField(`${key}:`, `${leaderboard[key]}`.toString()); 209 | } 210 | 211 | // to avoid exceeding the rate limit, we will be editing the result embed instead of sending a new one 212 | msg.edit({ embeds: [result.setTitle('**Game Over!**\nFinal Scores:').setDescription('').setColor('#fb94d3')] }); 213 | }); 214 | } else { 215 | // if the leaderboard is empty, construct a different embed 216 | 217 | // send the embed to the channel after the edit is complete 218 | message.channel.send({ embeds: [result] }).then((msg) => { 219 | // to avoid exceeding the rate limit, we will be editing the result embed instead of sending a new one 220 | msg.edit({ embeds: [result.setTitle('Game Over! No one got anything right....').setColor('#fb94d3')] }); 221 | }); 222 | } 223 | // so the for loop can stop executing 224 | triviaData.length = 0; 225 | } 226 | }); 227 | if (counter === 0 || stopped) { 228 | break; 229 | } 230 | /* if I don't include a pause of some sort, then the for loop will RAPID FIRE send all the questions to the channel 231 | adding a pause here that is equal to the collection time allows for time in between questions, and an 232 | overall pleasant user experience 233 | */ 234 | await this.client.utils.wait(time); 235 | 236 | // decrement the counter, tbh I don't know if having a counter is necessary now that I'm looking at this....we can fix this later 237 | counter--; 238 | } 239 | if (counter === 0 && !stopped) { 240 | // if the game wasn't triggered to stop before the questions ran out, then here is where the results will be sent to the guild 241 | 242 | let winnerEmbed = new MessageEmbed(); // create new embed instance 243 | 244 | // iterate over the leaderboard if winners exist (if the length of the object's keys isn't 0, then we have winners) 245 | if (Object.keys(leaderboard).length !== 0) { 246 | // specify the contents of the embed 247 | let winner = winnerEmbed.setTitle('**Game Over!**\nFinal Scores:').setColor('#fb94d3'); 248 | 249 | // loop over the contents of the leaderboard, and add fields to the embed on every iteration 250 | for (const key in leaderboard) { 251 | winner.addField(`${key}:`, `${leaderboard[key]}`.toString()); 252 | } 253 | try { 254 | message.channel.send({ embeds: [winner] }); 255 | } catch (e) { 256 | console.log(e); 257 | return; 258 | } 259 | } else { 260 | // if the leaderboard is empty, construct a different embed 261 | winnerEmbed.setTitle('Game Over! No one got anything right...').setColor('#fb94d3'); 262 | // send the embed to the channel 263 | try { 264 | message.channel.send({ embeds: [winnerEmbed] }); 265 | } catch (e) { 266 | console.log(e); 267 | return; 268 | } 269 | } 270 | } 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /src/Commands/tfcompetitive.js: -------------------------------------------------------------------------------- 1 | import Command from '../Structures/Command.js'; 2 | import axios from 'axios'; 3 | import { parseEntities } from 'parse-entities'; 4 | import { MessageEmbed } from 'discord.js'; 5 | 6 | export default class extends Command { 7 | constructor(...args) { 8 | super(...args, { 9 | aliases: ['tfcompetitive', 'tfcomp'], 10 | description: 11 | 'Initiates a round of 10 question T/F trivia with random difficulties and random categories. Its `competitive` because this will only accept the first person that guesses correctly; everyone else loses by default. **TLDR; you have to be the first to answer correctly!**', 12 | category: 'Game Modes', 13 | //usage: '[time]', 14 | }); 15 | } 16 | 17 | async run(message, commands) { 18 | if (!this.validateCommands(message, commands)) { 19 | return; 20 | } 21 | 22 | let triviaData; 23 | 24 | try { 25 | triviaData = await (await axios(`https://opentdb.com/api.php?amount=10&type=boolean`)).data.results; 26 | } catch (e) { 27 | console.log(e); 28 | try { 29 | message.channel.send({ content: 'Uh oh, something has gone wrong while trying to get some questions. Please try again' }); 30 | } catch (e) { 31 | console.log(e); 32 | return; 33 | } 34 | } 35 | 36 | const embed = new MessageEmbed(); 37 | let counter = 10; 38 | let stopped = false; 39 | 40 | let leaderboard = {}; 41 | 42 | for (let i = 0; i < triviaData.length; i++) { 43 | embed 44 | .setTitle(`Question ${i + 1}`) 45 | .setColor('#5fdbe3') 46 | .setDescription( 47 | parseEntities(triviaData[i].question) + 48 | '\n' + 49 | '\n**Difficulty:** ' + 50 | parseEntities(triviaData[i].difficulty) + 51 | '\n**Category:** ' + 52 | parseEntities(triviaData[i].category) 53 | ); 54 | 55 | let msgEmbed; 56 | try { 57 | msgEmbed = await message.channel.send({ embeds: [embed] }); 58 | } catch (e) { 59 | console.log(e); 60 | return; 61 | } 62 | msgEmbed.react('🇹'); 63 | msgEmbed.react('🇫'); 64 | msgEmbed.react('🛑'); // adds a universal stop sign 65 | 66 | let answer = ''; 67 | if (triviaData[i].correct_answer === 'True') { 68 | answer = '🇹'; 69 | } else { 70 | answer = '🇫'; 71 | } 72 | 73 | const filter = (reaction, user) => { 74 | return (reaction.emoji.name === answer || reaction.emoji.name === '🛑') && user.username !== this.client.user.username; 75 | }; 76 | 77 | const collector = msgEmbed.createReactionCollector({ filter, max: 1, time: 10000 }); 78 | 79 | let userWithCorrectAnswer = []; 80 | 81 | collector.on('collect', (r, user) => { 82 | // add the users that answered correctly to the usersWithCorrect Answer array 83 | if (r.emoji.name === '🛑') { 84 | counter = 0; 85 | stopped = true; 86 | collector.stop(); 87 | } else { 88 | userWithCorrectAnswer.push(user.username); 89 | if (leaderboard[user.username] === undefined) { 90 | // if the user isn't already in the leaderboard object, add them and give them a score of 1 91 | leaderboard[user.username] = 1; 92 | } else { 93 | // otherwise, increment the user's score 94 | leaderboard[user.username] += 1; 95 | } 96 | } 97 | }); 98 | let newEmbed = new MessageEmbed(); 99 | let result; 100 | 101 | collector.on('end', async () => { 102 | // if no one got any answers right 103 | if (userWithCorrectAnswer.length === 0) { 104 | // create an embed 105 | result = newEmbed 106 | .setTitle("Time's Up! No one got it....") 107 | .setDescription('\n The correct answer was ' + parseEntities(triviaData[i].correct_answer)) 108 | .setColor('#f40404'); 109 | // send the embed to the channel if the game wasn't terminated 110 | if (!stopped) { 111 | try { 112 | message.channel.send({ embeds: [result] }); 113 | } catch (e) { 114 | console.log(e); 115 | return; 116 | } 117 | } 118 | } else { 119 | // otherwise, create an embed with the results of the question 120 | 121 | /* 122 | since the array is an array of strings, I used the javascript join() method to concat them, and then the replace() to replace the 123 | comma with a comma and a space, so its human readable and pleasant to the eye 124 | */ 125 | result = newEmbed 126 | .setTitle("That's IT!! Here's who got it first:") 127 | .setDescription(userWithCorrectAnswer.join().replace(',', ', ')) 128 | .setFooter({ text: '\n The correct answer was ' + parseEntities(triviaData[i].correct_answer) }) 129 | .setColor('#f40404'); 130 | // send the embed to the channel if the game wasn't terminated 131 | if (!stopped) { 132 | try { 133 | message.channel.send({ embeds: [result] }); 134 | } catch (e) { 135 | console.log(e); 136 | return; 137 | } 138 | } 139 | } 140 | if (stopped) { 141 | // if the game was stopped, then we need to send the the scores to the guild 142 | 143 | // iterate over the leaderboard if winners exist (if the length of the object's keys isn't 0, then we have winners) 144 | if (Object.keys(leaderboard).length !== 0) { 145 | // send the embed to the channel after the edit is complete 146 | message.channel.send({ embeds: [result] }).then((msg) => { 147 | // loop over the contents of the leaderboard, and add fields to the embed on every iteration 148 | for (const key in leaderboard) { 149 | result.addField(`${key}:`, `${leaderboard[key]}`.toString()); 150 | } 151 | 152 | // to avoid exceeding the rate limit, we will be editing the result embed instead of sending a new one 153 | msg.edit({ embeds: [result.setTitle('**Game Over!**\nFinal Scores:').setDescription('').setColor('#fb94d3')] }); 154 | }); 155 | } else { 156 | // if the leaderboard is empty, construct a different embed 157 | 158 | // send the embed to the channel after the edit is complete 159 | message.channel.send({ embeds: [result] }).then((msg) => { 160 | // to avoid exceeding the rate limit, we will be editing the result embed instead of sending a new one 161 | msg.edit({ embeds: [result.setTitle('Game Over! No one got anything right....').setColor('#fb94d3')] }); 162 | }); 163 | } 164 | // so the for loop can stop executing 165 | triviaData.length = 0; 166 | } 167 | }); 168 | if (counter === 0 || stopped) { 169 | break; 170 | } 171 | 172 | await this.client.utils.wait(10000); 173 | 174 | counter--; 175 | } 176 | if (counter === 0 && !stopped) { 177 | let winnerEmbed = new MessageEmbed(); // create new embed instance 178 | 179 | // iterate over the leaderboard if winners exist (if the length of the object's keys isn't 0, then we have winners) 180 | if (Object.keys(leaderboard).length !== 0) { 181 | // specify the contents of the embed 182 | let winner = winnerEmbed.setTitle('**Game Over!**\nFinal Scores:').setColor('#fb94d3'); 183 | 184 | // loop over the contents of the leaderboard, and add fields to the embed on every iteration 185 | for (const key in leaderboard) { 186 | winner.addField(`${key}:`, `${leaderboard[key]}`.toString()); 187 | } 188 | try { 189 | message.channel.send({ embeds: [winner] }); 190 | } catch (e) { 191 | console.log(e); 192 | return; 193 | } 194 | } else { 195 | // if the leaderboard is empty, construct a different embed 196 | winnerEmbed.setTitle('Game Over! No one got anything right...').setColor('#fb94d3'); 197 | // send the embed to the channel 198 | try { 199 | message.channel.send({ embeds: [winnerEmbed] }); 200 | } catch (e) { 201 | console.log(e); 202 | return; 203 | } 204 | } 205 | } 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /src/Events/messageCreate/messageCreate.js: -------------------------------------------------------------------------------- 1 | import Event from '../../Structures/Event.js'; 2 | 3 | export default class extends Event { 4 | async run(message) { 5 | const mentionRegex = RegExp(`^<@!${this.client.user.id}>$`); 6 | const mentionRegexPrefix = RegExp(`^<@!${this.client.user.id}> `); 7 | 8 | if (message.author.bot) return; 9 | 10 | if (message.content.toLocaleLowerCase().includes('trivia')) { 11 | let responseArray = [ 12 | 'Did someone say my name?', 13 | 'You called?', 14 | 'Looking for me?', 15 | 'You know you wanna play...', 16 | 'What are you waiting for, play some trivia!', 17 | 'If you ever forget how to use me, just type `!help`', 18 | ]; 19 | let randomIndex = Math.floor(Math.random() * responseArray.length); 20 | 21 | try { 22 | message.channel.send({ content: responseArray[randomIndex] }); 23 | } catch (e) { 24 | console.log(e); 25 | } 26 | } 27 | 28 | if (message.content.match(mentionRegex)) { 29 | try { 30 | message.channel.send({ content: `My prefix for ${message.guild.name} is \`${this.client.prefix}\`.` }); 31 | } catch (e) { 32 | console.log(e); 33 | } 34 | } 35 | 36 | const prefix = message.content.match(mentionRegexPrefix) ? message.content.match(mentionRegexPrefix)[0] : this.client.prefix; 37 | 38 | if (!message.content.startsWith(prefix)) return; 39 | 40 | const [cmd, ...args] = message.content.slice(prefix.length).trim().split(/ +/g); 41 | 42 | const command = this.client.commands.get(cmd.toLowerCase()) || this.client.commands.get(this.client.aliases.get(cmd.toLowerCase())); 43 | 44 | if (command) { 45 | command.run(message, args); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Events/ready.js: -------------------------------------------------------------------------------- 1 | import Event from '../Structures/Event.js'; 2 | 3 | export default class extends Event { 4 | constructor(...args) { 5 | super(...args, { 6 | once: true, 7 | }); 8 | } 9 | 10 | run() { 11 | console.log( 12 | [ 13 | `Logged in as ${this.client.user.tag}`, 14 | `Loaded ${this.client.commands.size} commands!`, 15 | `Loaded ${this.client.events.size} events!`, 16 | `Trivia bot is currently hanging out in ${this.client.guilds.cache.size} servers!`, 17 | ].join('\n') 18 | ); 19 | this.client.user.setActivity('!help', { type: 'WATCHING' }); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Structures/Command.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This is used as a dictionary in which the key is the command 3 | * and the value is the configuration of the arguments accepted 4 | * by the command. 5 | * 6 | * The key and the `cmd` must match 7 | */ 8 | const optSubCommandsDefinitions = { 9 | time: { 10 | cmd: 'time', 11 | type: Number, 12 | default: 10, 13 | min: 10, 14 | max: 180, 15 | }, 16 | }; 17 | 18 | export default class Command { 19 | constructor(client, name, options = {}) { 20 | this.client = client; 21 | this.name = options.name || name; 22 | this.aliases = options.aliases || []; 23 | this.description = options.description || 'No description provided.'; 24 | this.category = options.category || 'Miscellaneous'; 25 | this.usage = `${this.client.prefix}${this.name} ${options.usage || ''}`.trim(); 26 | this.strictSubCommands = options.strictSubCommands || []; 27 | this.optSubCommands = options.optSubCommands || []; 28 | } 29 | 30 | async run(message, args) { 31 | throw new Error(`Command ${this.name} doesn't provide a run method!`); 32 | } 33 | 34 | /** 35 | * Casts the value based on the `validations.type` property 36 | * and checks all the validations against the value. 37 | * 38 | * Returns an object with the validity and casted value of the arguments 39 | */ 40 | validateArgument(arg, validations) { 41 | try { 42 | let allowed = true; 43 | // Create the value with the `type` property, a Class/constructor 44 | const { type, min, max } = validations; 45 | const value = type(arg); 46 | // Check the different validations 47 | // The validations have assumptions based on the value type 48 | // e.g. `max` and `min` will only be present when the `type` is `Number` 49 | allowed = allowed && (max !== undefined ? value <= max : true); 50 | allowed = allowed && (min !== undefined ? value >= min : true); 51 | return { 52 | isValid: allowed, 53 | value, 54 | }; 55 | } catch { 56 | // If there's any error during the conversion, take it as invalid 57 | return { 58 | isValid: false, 59 | value: null, 60 | }; 61 | } 62 | } 63 | 64 | /** 65 | * Returns `false` if there are commands in the command param 66 | * that do not exist. Sends a message in the channel about 67 | * the first invalid command, if there is one. 68 | * Returns the casted values if applicable 69 | * `commands` is an array of the passed options followed by the desired value. 70 | * e.g. ['time', '15', 'questions', '5'] 71 | */ 72 | validateCommands(message, commands) { 73 | if (commands) { 74 | const parsedCmds = {}; 75 | const subCmds = []; 76 | // Parse the commands, consuming the arguments if necessary 77 | for (let i = 0; i < commands.length; i++) { 78 | let consumeInput = false; 79 | let cmd = commands[i]; 80 | let cmdDef = optSubCommandsDefinitions[cmd]; 81 | // Unknown command, stop checking the input 82 | if (!cmdDef) { 83 | // Add the command so the next step shows the error to the user 84 | subCmds.push(cmd); 85 | break; 86 | } 87 | // Add the sub command to the list to check if it's valid 88 | subCmds.push(cmd); 89 | // If a command has a `type` property it consumes the next input 90 | if (cmdDef.type) { 91 | consumeInput = true; 92 | } 93 | // Check if an expected argument is missing 94 | if (consumeInput && i + 1 >= commands.length) { 95 | break; 96 | } 97 | // Update the counter if necessary to send the argument to the validator 98 | i = consumeInput ? i + 1 : i; 99 | // Run the validations for the arguments 100 | parsedCmds[cmd] = this.validateArgument(commands[i], cmdDef); 101 | } 102 | 103 | const validCmds = subCmds.map((cmd, i) => { 104 | return this.strictSubCommands.includes(cmd) || this.optSubCommands.includes(cmd); 105 | }); 106 | 107 | for (let i = 0; i < validCmds.length; i++) { 108 | // Check for subcommands 109 | if (!validCmds[i]) { 110 | message.channel.send({ content: `\`${this.name}\` does not have sub-command named: \`${subCmds[i]}\`` }); 111 | return false; 112 | } 113 | // Check for arguments 114 | if (parsedCmds[subCmds[i]] && !parsedCmds[subCmds[i]].isValid) { 115 | message.channel.send({ 116 | content: `\`${this.name}\` option: \`${subCmds[i]}\` has an unexpected value \`${parsedCmds[subCmds[i]].value}\``, 117 | }); 118 | return false; 119 | } 120 | } 121 | 122 | // At this point everything is validated. Flatten the commands 123 | return Object.keys(parsedCmds).reduce((acc, subCmds) => { 124 | acc[subCmds] = parsedCmds[subCmds].value; 125 | return acc; 126 | }, {}); 127 | } 128 | 129 | return true; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/Structures/Event.js: -------------------------------------------------------------------------------- 1 | export default class Event { 2 | constructor(client, name, options = {}) { 3 | this.name = name; 4 | this.client = client; 5 | this.type = options.once ? 'once' : 'on'; 6 | this.emitter = (typeof options.emitter === 'string' ? this.client[options.emitter] : options.emitter) || this.client; 7 | } 8 | 9 | async run(...args) { 10 | throw new Error(`The run method has not been implemented in ${this.name}`); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Structures/MDClient.js: -------------------------------------------------------------------------------- 1 | import { Client, Collection, Intents } from 'discord.js'; 2 | import Util from './Util.js'; 3 | 4 | export default class MDClient extends Client { 5 | constructor(options = {}) { 6 | super({ 7 | disableMentions: 'everyone', 8 | intents: [ 9 | Intents.FLAGS.DIRECT_MESSAGES, 10 | Intents.FLAGS.DIRECT_MESSAGE_REACTIONS, 11 | Intents.FLAGS.GUILDS, 12 | Intents.FLAGS.GUILD_MESSAGES, 13 | Intents.FLAGS.GUILD_MESSAGE_REACTIONS, 14 | ], 15 | partials: ['MESSAGE', 'CHANNEL', 'REACTION', 'USER'], 16 | }); 17 | this.validate(options); 18 | 19 | this.commands = new Collection(); 20 | 21 | this.aliases = new Collection(); 22 | 23 | this.events = new Collection(); 24 | 25 | this.utils = new Util(this); 26 | } 27 | 28 | validate(options) { 29 | if (typeof options !== 'object') throw new TypeError('Options should be a type of Object.'); 30 | 31 | if (!options.token) throw new Error('You must pass the token for the client.'); 32 | this.token = options.token; 33 | 34 | if (!options.prefix) throw new Error('You must pass a prefix for the client.'); 35 | if (typeof options.prefix !== 'string') throw new TypeError('Prefix should be a type of String.'); 36 | this.prefix = options.prefix; 37 | 38 | if (!options.owners) throw new Error('You must pass a list of owners for the client.'); 39 | if (!Array.isArray(options.owners)) throw new TypeError('Owners should be a type of Array.'); 40 | this.owners = options.owners; 41 | } 42 | 43 | async start(token = this.token) { 44 | this.utils.loadCommands(); 45 | this.utils.loadEvents(); 46 | super.login(token); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Structures/Util.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { promisify } from 'util'; 3 | import g from 'glob'; 4 | import Command from './Command.js'; 5 | import Event from './Event.js'; 6 | import { createRequire } from 'module'; 7 | const require = createRequire(import.meta.url); 8 | const glob = promisify(g); 9 | 10 | export default class Util { 11 | constructor(client) { 12 | this.client = client; 13 | } 14 | 15 | isClass(input) { 16 | return typeof input === 'function' && typeof input.prototype === 'object' && input.toString().includes('class'); 17 | } 18 | 19 | get directory() { 20 | return `${process.cwd()}/src`; 21 | } 22 | 23 | removeDuplicates(arr) { 24 | return [...new Set(arr)]; 25 | } 26 | 27 | async wait(time) { 28 | // sets timer in ms so the for loop in games pauses 29 | return new Promise((res) => setTimeout(res, time)); 30 | } 31 | 32 | async loadCommands() { 33 | return glob(`${this.directory}/Commands/**/*.js`).then((commands) => { 34 | for (const commandFile of commands) { 35 | delete require.cache[commandFile]; 36 | const { name } = path.parse(commandFile); 37 | import(commandFile).then((c) => { 38 | const File = c.default; 39 | if (!this.isClass(File)) throw new TypeError(`Command ${name} doesn't export a class.`); 40 | const command = new File(this.client, name.toLowerCase()); 41 | if (!(command instanceof Command)) throw new TypeError(`Command ${name} doesnt belong in Commands.`); 42 | this.client.commands.set(command.name, command); 43 | if (command.aliases.length) { 44 | for (const alias of command.aliases) { 45 | this.client.aliases.set(alias, command.name); 46 | } 47 | } 48 | }); 49 | } 50 | }); 51 | } 52 | 53 | async loadEvents() { 54 | return glob(`${this.directory}/Events/**/*.js`).then((events) => { 55 | for (const eventFile of events) { 56 | delete require.cache[eventFile]; 57 | const { name } = path.parse(eventFile); 58 | import(eventFile).then((e) => { 59 | const File = e.default; 60 | if (!this.isClass(File)) throw new TypeError(`Event ${name} doesn't export a class!`); 61 | const event = new File(this.client, name); 62 | if (!(event instanceof Event)) throw new TypeError(`Event ${name} doesn't belong in Events`); 63 | this.client.events.set(event.name, event); 64 | event.emitter[event.type](name, (...args) => event.run(...args)); 65 | }); 66 | } 67 | }); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/bot.js: -------------------------------------------------------------------------------- 1 | // OOP design highly inspired by https://github.com/MenuDocs/discord.js-template 2 | 3 | import 'dotenv/config.js'; 4 | import MDClient from './Structures/MDClient.js'; 5 | import { AutoPoster } from 'topgg-autoposter'; 6 | 7 | const client = new MDClient({ 8 | token: process.env.BOT_TOKEN, 9 | prefix: '!', 10 | owners: ['Eleni Rotsides', 'Joshua Hector', 'Sylvia Boamah', 'Julio Lora'], 11 | }); 12 | 13 | client.start(); 14 | 15 | const poster = AutoPoster(process.env.TOP_GG_TOKEN, client); 16 | 17 | poster.on('posted', (stats) => { 18 | console.log(`Posted stats to Top.gg | ${stats.serverCount} servers`); 19 | }); 20 | 21 | poster.on('error', (e) => { 22 | console.log('Something has gone wrong while trying to send stats to Top.gg'); 23 | }); 24 | --------------------------------------------------------------------------------