├── .github ├── Issue_Template │ ├── ADD_issue.yml │ ├── Doc_report.yml │ ├── bug.yml │ ├── bug_report.yml │ ├── feature_request.yml │ ├── other.yml │ └── styles.yml ├── Pull_Request_Template.md ├── dependabot.yml └── workflows │ └── codeql.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── PRIVACY_POLICY.md ├── README.md ├── TERMS_OF_SERVICE.md ├── commands ├── contests.js ├── help.js ├── invite.js ├── setup-contest.js ├── setup-problem.js ├── stats.js └── stop.js ├── database ├── mongo.js └── schema │ ├── configuration.js │ ├── contest.js │ ├── contestChannel.js │ └── problemChannel.js ├── deploy.js ├── index.js ├── interactions ├── contests-in.js ├── contests.js └── notifications.js ├── loops ├── changing activity.js ├── contests message.js ├── contests scraping.js └── problem message.js ├── package-lock.json ├── package.json └── utility ├── contests-in.js ├── embed message.js └── joining message.js /.github/Issue_Template/ADD_issue.yml: -------------------------------------------------------------------------------- 1 | name: 🚀 New Issue 2 | description: Use this to create a new issue 3 | title: "[Add] " 4 | labels: ["new issue", "good first issue"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: "Thanks for submitting an issue! Before you proceed with the submission, please provide the following information." 9 | - type: textarea 10 | id: description-feature 11 | attributes: 12 | label: "Details" 13 | description: Please describe the details in a clear and detailed manner. 14 | placeholder: Please enter your detailed description of the new issue here. 15 | validations: 16 | required: true 17 | - type: textarea 18 | id: screenshots-feature 19 | attributes: 20 | label: Screenshots 21 | description: Please add screenshots if applicable 22 | validations: 23 | required: false 24 | - type: checkboxes 25 | id: contribution-terms 26 | attributes: 27 | label: "Type of Contribution" 28 | options: 29 | - label: "Updating the existing feature?" 30 | required: false 31 | - label: "Resolving a bug" 32 | required: false 33 | - label: "Proposal to the Repository" 34 | required: false 35 | - label: "Changes related to documentation or README.md" 36 | required: false 37 | - label: "Other Changes" 38 | required: false 39 | - type: checkboxes 40 | id: checklist-terms 41 | attributes: 42 | label: "Checklist" 43 | description: "By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/roshan1337d/coding-contests-companion/blob/master/CODE_OF_CONDUCT.md)" 44 | options: 45 | - label: "I have checked the existing [issues](https://github.com/roshan1337d/coding-contests-companion/issues/53#issuecomment-1581932720)" 46 | required: true 47 | - label: "I have read the [Contributing Guidelines](https://github.com/roshan1337d/coding-contests-companion/blob/master/CONTRIBUTING.md)" 48 | required: true 49 | - label: "I am willing to work on this issue" 50 | required: false 51 | - label: "I am a GSSoC'23 contributor" 52 | required: false 53 | -------------------------------------------------------------------------------- /.github/Issue_Template/Doc_report.yml: -------------------------------------------------------------------------------- 1 | name: Documentation request 2 | description: Change regarding improving the docs to be more accessible 3 | title: '[docs]' 4 | labels: ['documentation'] 5 | body: 6 | - type: textarea 7 | attributes: 8 | label: Category of documentation update 9 | description: | 10 | What category does this change fall under. For example Typo error, New category addition, Rephrasing the sentences, fixing broken links etc. 11 | validations: 12 | required: true 13 | - type: textarea 14 | attributes: 15 | label: Describe the change you think might work 16 | description: Please describe the change & need of change in atmost detail possible. 17 | validations: 18 | required: false 19 | -------------------------------------------------------------------------------- /.github/Issue_Template/bug.yml: -------------------------------------------------------------------------------- 1 | name: ​🐞 Bug 2 | description: Report an issue to help us improve the project. 3 | title: "[BUG] " 4 | labels: ["bug", "goal: fix"] 5 | body: 6 | - type: textarea 7 | attributes: 8 | label: Description 9 | id: description 10 | description: A brief description of the issue or bug you are facing, also include what you tried and what didn't work. 11 | validations: 12 | required: false 13 | - type: textarea 14 | attributes: 15 | label: Screenshots 16 | id: screenshots 17 | description: Please add screenshots if applicable 18 | validations: 19 | required: false 20 | - type: textarea 21 | attributes: 22 | label: Any additional information? 23 | id: extrainfo 24 | description: Any additional information or Is there anything we should know about this bug? 25 | validations: 26 | required: false 27 | - type: checkboxes 28 | id: no-duplicate-issues 29 | attributes: 30 | label: "Checklist" 31 | options: 32 | - label: "I have checked the existing issues" 33 | required: true 34 | 35 | - label: "I have read the [Contributing Guidelines]" 36 | required: true 37 | 38 | - label: "I am willing to work on this issue (optional)" 39 | required: false 40 | 41 | - label: "I am a GSSoC'23 contributor" 42 | required: false 43 | -------------------------------------------------------------------------------- /.github/Issue_Template/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: 🐛 Bug report 2 | description: Create a report to help us improve 3 | title: '[bug]' 4 | labels: ['bug'] 5 | body: 6 | - type: textarea 7 | attributes: 8 | label: Describe the bug 9 | description: A clear and concise description of what the bug is 10 | validations: 11 | required: false 12 | - type: textarea 13 | attributes: 14 | label: To Reproduce 15 | description: | 16 | Steps to reproduce the behavior. 17 | 1. Go to '...' 18 | 2. Click on '...' 19 | 3. Scroll down to '...' 20 | 4. See error 21 | validations: 22 | required: false 23 | - type: textarea 24 | attributes: 25 | label: Expected Behavior 26 | description: A clear and concise description of what you expected to happen. 27 | validations: 28 | required: false 29 | - type: textarea 30 | attributes: 31 | label: Screenshot/ Video 32 | description: If applicable, add screenshots to help explain your problem. 33 | validations: 34 | required: false 35 | - type: textarea 36 | attributes: 37 | label: Additional context 38 | description: Add any other context about the problem here. 39 | validations: 40 | required: false -------------------------------------------------------------------------------- /.github/Issue_Template/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature Request 💡 2 | description: Have any new idea or new feature? Please suggest! 3 | title: "[Feature] " 4 | labels: ["enhancement", "goal: addition"] 5 | body: 6 | - type: textarea 7 | id: description 8 | attributes: 9 | label: Description 10 | description: A clear and concise description of any alternative solution or features you've considered. 11 | validations: 12 | required: true 13 | - type: textarea 14 | id: screenshots 15 | attributes: 16 | label: Screenshots 17 | description: Please add screenshots if applicable 18 | validations: 19 | required: false 20 | - type: checkboxes 21 | id: no-duplicate-issues 22 | attributes: 23 | label: "Checklist" 24 | options: 25 | - label: "I have checked the existing issues" 26 | required: true 27 | 28 | - label: "I have read the [Contributing Guidelines]" 29 | required: true 30 | 31 | - label: "I am willing to work on this issue (optional)" 32 | required: false 33 | 34 | - label: "I am a GSSoC'23 contributor" 35 | required: false 36 | -------------------------------------------------------------------------------- /.github/Issue_Template/other.yml: -------------------------------------------------------------------------------- 1 | name: Other 2 | description: Use this for any other issues. PLEASE do not create blank issues 3 | title: "[other]" 4 | labels: ["awaiting triage"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: "# Other issue" 9 | - type: textarea 10 | id: issuedescription 11 | attributes: 12 | label: What would you like to share? 13 | description: Provide a clear and concise explanation of your issue. 14 | validations: 15 | required: true 16 | - type: textarea 17 | id: extrainfo 18 | attributes: 19 | label: Additional information 20 | description: Is there anything else we should know about this issue? 21 | validations: 22 | required: false -------------------------------------------------------------------------------- /.github/Issue_Template/styles.yml: -------------------------------------------------------------------------------- 1 | name: 👯‍♂️ Style Changing Request 2 | description: Suggest a style designs 3 | title: '[style]' 4 | labels: ["enhancement"] 5 | 6 | body: 7 | - type: markdown 8 | attributes: 9 | value: | 10 | Thanks for taking the time to fill out this template! 11 | 12 | - type: textarea 13 | id: style-idea 14 | attributes: 15 | label: What's the style idea? 16 | placeholder: Add descriptions 17 | value: 'We need to improve' 18 | validations: 19 | required: true 20 | 21 | - type: checkboxes 22 | id: terms 23 | attributes: 24 | label: Code of Conduct 25 | description: By submitting this issue, you agree to follow our Code of Conduct 26 | options: 27 | - label: I agree to follow this project's Code of Conduct -------------------------------------------------------------------------------- /.github/Pull_Request_Template.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Related Issue 4 | 5 | Closes #issue_number 6 | 7 | 8 | 9 | ## Description 10 | 11 | 13 | 14 | ## Screenshots 15 | 16 | 17 | 18 | ## Checklist 19 | 20 | 21 | 22 | 23 | - [ ] My code adheres to the established style guidelines of the project. 24 | - [ ] I have included comments in areas that may be difficult to understand. 25 | - [ ] My changes have not introduced any new warnings. 26 | - [ ] I have conducted a self-review of my code. -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # Update GitHub Actions 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "daily" -------------------------------------------------------------------------------- /.github/workflows/codeql.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: [ "master" ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ "master" ] 20 | schedule: 21 | - cron: '43 3 * * 6' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} 27 | timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} 28 | permissions: 29 | actions: read 30 | contents: read 31 | security-events: write 32 | 33 | strategy: 34 | fail-fast: false 35 | matrix: 36 | language: [ 'javascript' ] 37 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby', 'swift' ] 38 | # Use only 'java' to analyze code written in Java, Kotlin or both 39 | # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both 40 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 41 | 42 | steps: 43 | - name: Checkout repository 44 | uses: actions/checkout@v3 45 | 46 | # Initializes the CodeQL tools for scanning. 47 | - name: Initialize CodeQL 48 | uses: github/codeql-action/init@v2 49 | with: 50 | languages: ${{ matrix.language }} 51 | # If you wish to specify custom queries, you can do so here or in a config file. 52 | # By default, queries listed here will override any specified in a config file. 53 | # Prefix the list here with "+" to use these queries and those in the config file. 54 | 55 | # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 56 | # queries: security-extended,security-and-quality 57 | 58 | 59 | # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift). 60 | # If this step fails, then you should remove it and run the build manually (see below) 61 | - name: Autobuild 62 | uses: github/codeql-action/autobuild@v2 63 | 64 | # ℹ️ Command-line programs to run using the OS shell. 65 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 66 | 67 | # If the Autobuild fails above, remove it and uncomment the following three lines. 68 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 69 | 70 | # - run: | 71 | # echo "Run, Build Application using script" 72 | # ./location_of_script_within_repo/buildscript.sh 73 | 74 | - name: Perform CodeQL Analysis 75 | uses: github/codeql-action/analyze@v2 76 | with: 77 | category: "/language:${{matrix.language}}" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | config.json 2 | node_modules -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | We are committed to providing a friendly, inclusive, and safe environment for all participants in our project repository. This code of conduct outlines our expectations for behavior and ensures that everyone can collaborate in a positive and respectful manner. By participating in this project, you agree to abide by this code of conduct. 4 | 5 | ## 1. Be Respectful and Inclusive 6 | 7 | Treat all participants with respect and courtesy, regardless of their race, ethnicity, nationality, gender identity, sexual orientation, age, disability, religion, or any other personal characteristics. Create an inclusive and welcoming environment for everyone to contribute. 8 | 9 | ## 2. Foster a Collaborative Atmosphere 10 | 11 | Encourage open and constructive discussions. Be receptive to different ideas and perspectives. Avoid personal attacks, harassment, or any form of offensive or derogatory language or behavior. 12 | 13 | ## 3. Be Mindful of Language and Tone 14 | 15 | Use clear and inclusive language when communicating in discussions, comments, and documentation. Be mindful of how your words may be perceived by others. Refrain from using offensive, discriminatory, or inflammatory language. 16 | ## 4. Exercise Empathy and Understanding 17 | 18 | Take into account that participants may have different backgrounds and experiences. Be considerate and understanding when communicating with others. If a misunderstanding occurs, seek to resolve it in a peaceful and respectful manner. 19 | 20 | ## 5. Respect Privacy and Confidentiality 21 | 22 | Respect the privacy and confidentiality of others. Do not share personal information without consent. Be cautious when handling sensitive data and ensure compliance with relevant privacy laws and regulations. 23 | 24 | ## 6. Report Incidents 25 | 26 | If you witness or experience any behavior that violates this code of conduct, promptly report it to the project maintainers or administrators. Provide as much detail as possible to help in the investigation. All reports will be handled confidentially and with discretion. 27 | 28 | ## 7. Enforcement 29 | 30 | Violation of this code of conduct may result in temporary or permanent restrictions on participation in the project repository. Project maintainers and administrators reserve the right to enforce this code of conduct and take appropriate actions to address any misconduct or breaches of conduct. 31 | 32 | 33 | ## 8. Acknowledgment 34 | 35 | We value and appreciate everyone's contributions to our project repository. By following this code of conduct, we can create a supportive and inclusive environment where collaboration and growth thrive. 36 | 37 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | I am open to, and grateful for any contributions made by the community to help develop this discord bot. Please go through this document once before you contribute or open a pull request. 2 | 3 | # General Information 4 | 5 | ## Made With 6 | 7 | - JavaScript and Node.js 8 | - Puppeteer, Axios, and jsDom for scraping the contest details 9 | - Discord.js for interacting with Discord 10 | - MonogDB as the database 11 | - Moongoose for interacting with MongoDB 12 | - Microsoft Azure for hosting the bot 13 | 14 | ## Folder Structure 15 | 16 | | File or Folder | Use| 17 | | --- | --- | 18 | | index.js | Main entry file which initializes everything and starts the bot | 19 | | deploy.js | Push all the slash commands to discord | 20 | | commands | All the slash commands | 21 | | interactions | All the interaction hooks for things like button press, select change etc | 22 | | database | The mongo.js contains all the database operations, and the schema folder contains the document schemas | 23 | | loops | The contests scraping.js file fetches the latest contest details from various platforms, contest message.js file sends the contest notifications, problem message.js file send the daily problems, and changing activity.js file changes the bot activity every 15 seconds | 24 | | utility | The embed message.js file sends or returns embed messages, joining message.js file sends a message on joining a new server | 25 | 26 | # Setup Instructions 27 | 28 | ## 1. Local Setup 29 | 30 | Clone this repository and install all required dependencies using `npm install` 31 | 32 | ## 2. Create config.json File 33 | 34 | A config.json file present in the same location as the index.js file contains all the bot secret configurations and hence not available publicly on this repo. The format is given below. You need to create one and update the following: `guildId`, `clientIdTest`, `tokenTest`, `mongourlTest` 35 | 36 | ```json 37 | { 38 | "guildId": "testing_server_id", 39 | "clientIdTest": "testing_bot_id", 40 | "clientIdProd": "production_bot_id", 41 | "tokenTest": "testing_bot_token", 42 | "tokenProd": "production_bot_token", 43 | "mongourlTest": "testing_mongodb_url/db", 44 | "mongourlProd": "production_mongodb_url/db", 45 | "codeforcesKey": "codeforces_api_key", 46 | "codeforcesSecret": "codeforces_api_secret", 47 | "isProduction": false 48 | } 49 | ``` 50 | 51 | 52 | ## 3. Create Discord Bot Account 53 | 54 | 1. Open the [Discord Developers Portal.](https://discord.com/developers/applications) 55 | 56 | 2. Switch to `Applications` tab and then click the `New Application` button, enter your desired bot name and click `Create`. 57 | 58 | 3. Switch to `Bot` tab, click `Add Bot`, and confirm `Yes, do it!`. Click on `Reset Token` and copy the new token (this is the `tokenTest` for config.json file). 59 | 60 | 4. Switch to `General Information` tab, copy the `APPLICATION ID` (this is the `clientIdTest` for config.json file), and invite the bot to your test server using the below link by replacing the `{APPLICATION_ID_HERE}` 61 | ```sh 62 | https://discord.com/oauth2/authorize?client_id={APPLICATION_ID_HERE}&permissions=268435456&scope=bot%20applications.commands 63 | ``` 64 | 5. From the dropdown, you need to choose an existing server from the options. 65 | 66 | 6. But if there are none to be selected from the dropdown, click on [ Discord Website. ](https://discord.com/) to login or create a discord account. Then Click on the `+` tab on the left menu to add a New Server. 67 | 68 | 7. Switch back to the link below and refresh, from the dropdown select the newly created server. 69 | 70 | ```sh 71 | https://discord.com/oauth2/authorize?client_id={APPLICATION_ID_HERE}&permissions=268435456&scope=bot%20applications.commands 72 | ``` 73 | 74 | ## 4. Other config.json Parameters 75 | 76 | 1. When you open your test server on discord, the link will be in the below format. Get the `guildId` from here. 77 | 78 | ``` 79 | https://discord.com/channels/guildId/channelId 80 | ``` 81 | 82 | 2. Setup MongoDB server locally or create a MongoDB Atlas account, and get the connection url (look this up on YouTube as it is beyond the scope of this setup instruction). This will be the `mongourlTest`. 83 | 84 | ## 5. Finishing Up 85 | 86 | 1. Deploy the slash commands once using `node deploy.js`, you will see this `Successfully registered server application commands!` if successful. 87 | 88 | 2. Run the bot using `node index.js` this shows up if successful 89 | ``` 90 | Connected to MongoDB. 91 | Notifications loop started. 92 | Daily problem loop started. 93 | Scraping loop started. 94 | Bot is online. 95 | ``` 96 | 97 | 3. Go back to your discord account, on the chat box type the `/` and choose different files from the commands folder in VS code 98 | find a screenshot below. [Discord Bot.](https://imgur.com/a/Hfm06SE) 99 | 100 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 roshan1337d 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 | -------------------------------------------------------------------------------- /PRIVACY_POLICY.md: -------------------------------------------------------------------------------- 1 | # Coding Contests Companion Bot - Privacy Policy 2 | 3 | **Effective Date:** 8 October 2024 4 | 5 | At **Coding Contests Companion Bot** ("Bot"), we take your privacy seriously. This Privacy Policy outlines the types of information we collect, how we use it, and the measures we take to protect it when you use the Bot in your Discord server. By using the Bot, you agree to the terms outlined in this Privacy Policy. 6 | 7 | ## 1. **Information We Collect** 8 | 9 | We collect only the minimal information necessary to provide the services of the Bot. The types of information we may collect include: 10 | 11 | ### 1.1 **Server Information** 12 | - **Server ID**: Used to uniquely identify and interact with your Discord server. 13 | - **Channel IDs**: Used to determine where to send messages (e.g., contest notifications or daily coding problems). 14 | - **Roles**: The Bot accesses roles to allow users to self-assign a role for coding contest notifications. 15 | - **Messages**: The Bot sends messages to the server channels (e.g., contest reminders, daily challenges), but it does not collect or store any message content from your server. 16 | 17 | ### 1.2 **User Information** 18 | - **User ID**: The Bot may interact with individual user IDs for role management (i.e., when users opt into receiving notifications for coding contests). 19 | - **User interactions**: Information such as when a user interacts with the Bot, such as adding or removing roles, is logged temporarily for operational purposes. 20 | 21 | ## 2. **How We Use Your Information** 22 | 23 | We use the information collected to provide and enhance the Bot’s functionality, including but not limited to: 24 | - Displaying ongoing and upcoming coding contests. 25 | - Sending notifications to users who have opted into contest reminders. 26 | - Delivering daily coding challenges from LeetCode. 27 | 28 | ### 2.1 **Non-Personal Data** 29 | We may aggregate and anonymize certain non-personal data to analyze Bot usage trends and improve the service. 30 | 31 | ## 3. **Data Storage and Retention** 32 | 33 | ### 3.1 **Storage** 34 | The Bot stores minimal data for operational purposes only. This includes basic server configurations like channel IDs for posting updates and user role information. We do not store personal message content or sensitive information. 35 | 36 | ### 3.2 **Retention** 37 | We retain the information collected for as long as it is necessary to provide the Bot’s services. If the Bot is removed from your server, all associated data will be deleted. 38 | 39 | ## 4. **Data Sharing and Disclosure** 40 | 41 | We do not sell, trade, or otherwise share your information with third parties, except in the following limited circumstances: 42 | - **Service Providers**: We may share information with trusted third-party services that assist in hosting or improving the Bot. These service providers are bound by confidentiality agreements. 43 | - **Legal Requirements**: We may disclose information if required by law, regulation, or a valid legal request (e.g., a subpoena or court order). 44 | 45 | ## 5. **Security** 46 | 47 | We implement reasonable security measures to protect the data collected from unauthorized access, alteration, or disclosure. However, no method of transmission over the Internet or method of electronic storage is 100% secure. While we strive to use commercially acceptable means to protect your information, we cannot guarantee absolute security. 48 | 49 | ## 6. **Children's Privacy** 50 | 51 | The Bot is intended for use by Discord users who are 13 years of age or older, or the minimum age required by your local jurisdiction. We do not knowingly collect or solicit personal information from anyone under the age of 13. If we discover that we have collected personal information from a child under 13, we will promptly delete such information. 52 | 53 | ## 7. **Your Rights and Choices** 54 | 55 | ### 7.1 **Access and Deletion** 56 | As a Discord server owner or user, you have the right to: 57 | - **Access**: Request a copy of the information we collect about your server. 58 | - **Delete**: Remove the Bot from your server to erase all associated data. 59 | 60 | If you wish to exercise any of these rights, please contact us using the details provided below. 61 | 62 | ### 7.2 **Opting Out of Notifications** 63 | Users can self-assign or remove roles to opt in or out of contest notifications. Server administrators also have the ability to manage Bot permissions and roles to control how the Bot interacts with users. 64 | 65 | ## 8. **Changes to this Privacy Policy** 66 | 67 | We may update this Privacy Policy from time to time. When we do, we will revise the "Effective Date" at the top of this document. It is your responsibility to review this policy periodically to stay informed about any changes. 68 | 69 | ## 9. **Contact Us** 70 | 71 | If you have any questions or concerns about this Privacy Policy or the information we collect, please contact us at [Insert Contact Information]. 72 | 73 | By using the Bot, you acknowledge that you have read and understood this Privacy Policy and agree to the collection and use of your information as outlined. 74 | 75 | --- 76 | 77 | **Thank you for trusting Coding Contests Companion Bot with your data!** 78 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |

Coding Contests Companion

3 | 4 | 5 | ## About 6 | 7 | Coding Contests Companion is a discord bot that displays all ongoing and upcoming coding contests from CodeChef, LeetCode, HackerRank, CodeForces, AtCoder, Google KickStart, and HackerEarth in one place in your discord server. You can set up an embed for your members to take a role themselves which will be pinged an hour before any contest is about to start with the contest details. **Never miss a coding contest again!** 8 | 9 | The bot can also send a random problem selected by LeetCode every day covering a variety of topics to keep your server members consistent, sharp, and ready 💪 10 | 11 | Use the /help command after inviting the bot for the user guide. 12 | 13 | ## Contributing 14 | 15 |
16 | Project contributors 17 | 18 | ### 19 | 20 | | | **Contribution**| 21 | | --- | --- | 22 | | [B4CKF1SH](https://github.com/B4CKF1SH) | Added support for AtCoder | 23 | | [Taduri Saimahesh](https://github.com/saimaheshtaduri) | Added support for HackerEarth | 24 | | [Swoyam Siddharth Nayak](https://github.com/swoyam2609) | Corrected available platform details | 25 | | [Souvik Nayak](https://github.com/Souvik-Nayak) | Wrote the setup instructions | 26 | | [Sahil Anower](https://github.com/SahilAnower) | Added support for Google KickStart | 27 | | [DevilzzNikhil](https://github.com/DevilzzNikhil) | Added server joining message feature | 28 | 29 |
30 | 31 | Wish to contribute? **[Check this out](CONTRIBUTING.md)** -------------------------------------------------------------------------------- /TERMS_OF_SERVICE.md: -------------------------------------------------------------------------------- 1 | # Coding Contests Companion Bot - Terms of Service 2 | 3 | **Effective Date:** 8 October 2024 4 | 5 | Welcome to the **Coding Contests Companion Bot** ("Bot"), a Discord bot designed to provide information about coding contests and daily coding challenges from popular coding platforms. By using the Bot, you agree to the following Terms of Service ("Terms"). These Terms govern your use of the Bot and its features. Please read them carefully. 6 | 7 | ## 1. **Acceptance of Terms** 8 | By adding or using the Bot in your Discord server, you agree to be bound by these Terms. If you do not agree with these Terms, do not use the Bot. 9 | 10 | ## 2. **Description of Service** 11 | The Bot provides features including, but not limited to: 12 | - Displaying ongoing and upcoming coding contests from CodeChef, LeetCode, HackerRank, CodeForces, AtCoder, Google KickStart, and HackerEarth. 13 | - Ping alerts to remind users of contests 1 hour before they start. 14 | - Sending a random daily coding problem from LeetCode. 15 | 16 | ## 3. **User Responsibilities** 17 | - **Eligibility**: You must be at least 13 years old or meet the minimum age requirements of your local jurisdiction to use Discord and this Bot. 18 | - **Account Security**: As a server administrator, it is your responsibility to manage your server and the roles assigned to users. The Bot allows users to self-assign contest reminder roles, and it is your responsibility to ensure proper management of these roles. 19 | - **Compliance**: You are responsible for ensuring that your use of the Bot complies with Discord's Terms of Service, Community Guidelines, and applicable laws. 20 | 21 | ## 4. **Content** 22 | - The Bot relies on public APIs from third-party platforms (CodeChef, LeetCode, HackerRank, CodeForces, AtCoder, Google KickStart, and HackerEarth) to gather contest information. We do not guarantee the accuracy, completeness, or timeliness of the information provided. 23 | - The daily coding problems sent by the Bot are sourced from LeetCode’s API. We are not responsible for the availability or quality of the problems. 24 | 25 | ## 5. **Permissions** 26 | By using the Bot, you grant us permission to: 27 | - Access and interact with your Discord server. 28 | - Store and process your server information (e.g., server name, user roles) to provide the service. 29 | - Send messages to your server's text channels in accordance with the Bot's features (e.g., contest reminders, daily problems). 30 | 31 | ## 6. **Modifications to Service** 32 | We reserve the right to modify or discontinue the Bot or any feature of the Bot at any time, without notice. We are not liable to you or any third party for any modification, suspension, or termination of the Bot's service. 33 | 34 | ## 7. **Prohibited Uses** 35 | You agree not to: 36 | - Use the Bot for any illegal activities or in violation of any laws or regulations. 37 | - Use the Bot to harass, abuse, or harm others. 38 | - Attempt to exploit or harm the Bot, its features, or other users through hacking, data mining, or unauthorized access. 39 | - Reverse-engineer, decompile, or attempt to derive the source code of the Bot. 40 | 41 | ## 8. **Limitation of Liability** 42 | The Bot is provided on an "as-is" and "as-available" basis. To the fullest extent permitted by law, we disclaim any warranties, either express or implied, regarding the Bot, including but not limited to any warranties of merchantability, fitness for a particular purpose, or non-infringement. 43 | 44 | We are not liable for: 45 | - Any errors or omissions in the contest information provided by the Bot. 46 | - Any server downtime, data loss, or issues caused by the Bot’s interaction with your Discord server. 47 | - Any direct, indirect, incidental, or consequential damages arising out of your use of the Bot. 48 | 49 | ## 9. **Indemnity** 50 | You agree to indemnify and hold harmless the Bot’s developers, owners, and affiliates from any claims, losses, damages, liabilities, and expenses (including attorneys' fees) arising out of your use of the Bot or violation of these Terms. 51 | 52 | ## 10. **Termination** 53 | We reserve the right to terminate or restrict your access to the Bot at any time, without notice, if you violate these Terms or if we discontinue the service. 54 | 55 | ## 11. **Changes to the Terms** 56 | We may update these Terms at any time. When we do, we will revise the "Effective Date" at the top of this document. It is your responsibility to review these Terms periodically to stay informed about any changes. 57 | 58 | ## 12. **Contact Information** 59 | If you have any questions or concerns about these Terms, or the use of the Bot, please contact us using the issues tab of github or using rdash.me contact form. 60 | 61 | By using the Bot, you acknowledge that you have read and understood these Terms and agree to be bound by them. 62 | 63 | --- 64 | 65 | **Thank you for using Coding Contests Companion!** 66 | -------------------------------------------------------------------------------- /commands/contests.js: -------------------------------------------------------------------------------- 1 | const embedMessage = require('../utility/embed message'); 2 | const { SlashCommandBuilder, ActionRowBuilder, StringSelectMenuBuilder, SlashCommandNumberOption } = require('discord.js'); 3 | const contestsInPaginate = require("../utility/contests-in"); 4 | 5 | // contests command to view ongoing and upcoming coding contests 6 | module.exports = { 7 | data: new SlashCommandBuilder() 8 | .setName('contests') 9 | .addNumberOption( 10 | new SlashCommandNumberOption() 11 | .setName("start") 12 | .setDescription( 13 | "View contests starting in X time" 14 | ) 15 | .addChoices( 16 | { name: '1 day', value: 1 }, 17 | { name: '1 week', value: 7 }, 18 | ) 19 | ) 20 | .setDescription('View ongoing and upcoming coding contests'), 21 | async execute(interaction) { 22 | await interaction.deferReply(); 23 | 24 | let days = interaction.options.getNumber("start"); 25 | if (days != null) { 26 | await contestsInPaginate(interaction, true); 27 | return; 28 | } 29 | 30 | // Create the embed and selection box 31 | const embed = await embedMessage(interaction, 'CODING CONTESTS', 'Select a contest platform using the selection box below. CodeChef, LeetCode, HackerRank, CodeForces, AtCoder, HackerEarth, GeeksforGeeks and Coding Ninjas are the currently available platforms. Support for more platforms coming soon :sparkles:', false, 'https://github.com/roshan1337d/coding-contests-companion', true); 32 | const row = new ActionRowBuilder() 33 | .addComponents( 34 | new StringSelectMenuBuilder() 35 | .setCustomId('contestsSelect') 36 | .setPlaceholder('Select contest platform') 37 | .addOptions( 38 | { 39 | label: 'CodeChef', 40 | value: 'codechef', 41 | emoji: { id: '1024020300834279484' }, 42 | }, 43 | { 44 | label: 'LeetCode', 45 | value: 'leetcode', 46 | emoji: { id: '1024019529283674183' }, 47 | }, 48 | { 49 | label: 'HackerRank', 50 | value: 'hackerrank', 51 | emoji: { id: '1024019532190339193' }, 52 | }, 53 | { 54 | label: 'CodeForces', 55 | value: 'codeforces', 56 | emoji: { id: '1024341762166243348' }, 57 | }, 58 | { 59 | label: 'AtCoder', 60 | value: 'atcoder', 61 | emoji: { id: '1025657008688484363' }, 62 | }, 63 | { 64 | label: 'HackerEarth', 65 | value: 'hackerearth', 66 | emoji: { id: '1025657011360243782' }, 67 | }, 68 | { 69 | label: 'Geeksforgeeks', 70 | value: 'geeksforgeeks', 71 | emoji: { id: '1110941777260711986' } 72 | }, 73 | { 74 | label: 'Coding Ninjas', 75 | value: 'codingninjas', 76 | emoji: { id: '1118598468978618518' } 77 | } 78 | ), 79 | ); 80 | 81 | // Send the embed with the selection box 82 | return interaction.editReply({ embeds: [embed], components: [row] }); 83 | }, 84 | }; -------------------------------------------------------------------------------- /commands/help.js: -------------------------------------------------------------------------------- 1 | const embedMessage = require('../utility/embed message'); 2 | const { SlashCommandBuilder } = require('discord.js'); 3 | 4 | // help command for the user guide 5 | module.exports = { 6 | data: new SlashCommandBuilder() 7 | .setName('help') 8 | .setDescription('User guide on how to use the bot'), 9 | async execute(interaction) { 10 | await interaction.deferReply({ ephemeral: true }); 11 | 12 | respStr = `**FOR EVERYONE** 13 | **[View Contests](http://ignore-the-link.com)** 14 | Use the \`/contests\` command. Select any contest platform using the selection box present below the message sent by the bot, to view all its ongoing and upcoming contests. If you want to view contests starting in X time, you can use the optional start field of the command. 15 | 16 | **FOR ADMIN ONLY** 17 | **[Setup Contest Notifications](http://ignore-the-link.com)** 18 | Use the \`/setup-contest\` command. Select the role it should ping, and the channel where it should send the notifications. Make sure the bot has permission to send messages in the selected channel, and that the selected role is present below the bot role in your server roles settings. The bot would now create an embed with a button for your members to opt-in and out-out of the notifications, and start notifying about all upcoming contests. 19 | 20 | **[Setup Problem Of The Day](http://ignore-the-link.com)** 21 | Use the \`/setup-problem\` command and select the channel where it should send the daily problems. Make sure the bot has permission to send messages in the selected channel. The bot would now start sending a problem every day. 22 | 23 | **[Stop Running Services](http://ignore-the-link.com)** 24 | Use the \`/stop\` command and select the feature service that you want to stop for your server. Optional: for the contest notifications, you may also want to delete the role it used to ping, and the role managing embed. 25 | 26 | Need more help? **[Join the support server](https://discord.gg/9sDtq74DMn)**` 27 | 28 | await embedMessage(interaction, 'HOW TO USE THE BOT', respStr, false, 'https://github.com/roshan1337d/coding-contests-companion'); 29 | }, 30 | }; -------------------------------------------------------------------------------- /commands/invite.js: -------------------------------------------------------------------------------- 1 | const embedMessage = require('../utility/embed message'); 2 | const { SlashCommandBuilder} = require('discord.js'); 3 | 4 | // invite command to receive the bot invitation link 5 | module.exports = { 6 | data: new SlashCommandBuilder() 7 | .setName('invite') 8 | .setDescription('Invite the bot to another server'), 9 | async execute(interaction) { 10 | await interaction.deferReply({ ephemeral: true }); 11 | await embedMessage(interaction, 'INVITE THE BOT', 'Open the above link, select the server where you want to invite this bot, keep all permissions checked, and click authorize. Use the **/help** command for setup instructions once the bot joins your server.', false, 'https://discord.com/api/oauth2/authorize?client_id=1023627528860086332&permissions=268435456&scope=bot%20applications.commands'); 12 | }, 13 | }; -------------------------------------------------------------------------------- /commands/setup-contest.js: -------------------------------------------------------------------------------- 1 | const embedMessage = require("../utility/embed message"); 2 | const { 3 | SlashCommandBuilder, 4 | SlashCommandRoleOption, 5 | SlashCommandChannelOption, 6 | EmbedBuilder, 7 | ActionRowBuilder, 8 | ButtonBuilder, 9 | ButtonStyle, 10 | PermissionFlagsBits, 11 | } = require("discord.js"); 12 | 13 | // setup-contest command for creating a role opting embed and turning the 14 | // contest notifications service on for a server 15 | module.exports = { 16 | data: new SlashCommandBuilder() 17 | .setName("setup-contest") 18 | .addRoleOption( 19 | new SlashCommandRoleOption() 20 | .setName("role") 21 | .setDescription( 22 | "Select the role to ping for contest notifications" 23 | ) 24 | .setRequired(true) 25 | ) 26 | .addChannelOption( 27 | new SlashCommandChannelOption() 28 | .setName("channel") 29 | .setDescription( 30 | "Select the channel where notifications will be sent" 31 | ) 32 | .setRequired(true) 33 | ) 34 | .setDescription("Setup upcoming contest notifications"), 35 | async execute(interaction) { 36 | await interaction.deferReply({ ephemeral: true }); 37 | 38 | // Only the admin can use this command 39 | if ( 40 | !( 41 | interaction.member.permissions.has( 42 | PermissionFlagsBits.Administrator 43 | ) || interaction.member.id === "415490428721168384" 44 | ) 45 | ) { 46 | await embedMessage( 47 | interaction, 48 | "PERMISSIONS ERROR", 49 | "You need to be an **Administrator** to run this command." 50 | ); 51 | return; 52 | } 53 | 54 | // The bot should be able to send messages in the selected channels 55 | const channel = interaction.options.getChannel("channel"); 56 | if ( 57 | !channel 58 | .permissionsFor(interaction.client.user) 59 | .has(PermissionFlagsBits.SendMessages) 60 | ) { 61 | await embedMessage( 62 | interaction, 63 | "PERMISSIONS ERROR", 64 | "The bot needs to have **Send Messages** permission in the selected channel." 65 | ); 66 | return; 67 | } 68 | 69 | // The bot should be able to manage the selected notifications role 70 | const role = interaction.options.getRole("role"); 71 | if (!role.editable) { 72 | await embedMessage( 73 | interaction, 74 | "LOWER ROLE ERROR", 75 | `**${role.name}** role need to be present below the bot role, in order for the bot to manage it. Open server settings, go to roles, drag the bot role above the ${role.name} role, and run this command again.` 76 | ); 77 | return; 78 | } 79 | 80 | // Create the embed for members to opt-in or out of notifications role 81 | const row = new ActionRowBuilder().addComponents( 82 | new ButtonBuilder() 83 | .setCustomId(`notificationsOn${role.id}`) 84 | .setLabel("Get Notified") 85 | .setStyle(ButtonStyle.Secondary), 86 | new ButtonBuilder() 87 | .setCustomId(`notificationsOff${role.id}`) 88 | .setLabel("Stop Notifications") 89 | .setStyle(ButtonStyle.Secondary) 90 | ); 91 | const embed = await embedMessage( 92 | interaction, 93 | "Wish to get notified about coding contests?", 94 | "Click the **Get Notified** button below to get a role which will be pinged whenever any coding contest on CodeChef, LeetCode, HackerRank, CodeForces, AtCoder, HackerEarth, or GeeksForGeeks is about to start. Click on **Stop Notifications** again to opt-out anytime.", 95 | false, 96 | "https://github.com/roshan1337d/coding-contests-companion", 97 | true 98 | ); 99 | 100 | // Save server and channel data to db and send a confirmation message 101 | await interaction.client.database.saveContestChannel( 102 | interaction.guildId, 103 | channel.id, 104 | role.id 105 | ); 106 | await embedMessage( 107 | interaction, 108 | "SERVICE ACTIVATED", 109 | "Upcoming contest notifications service has been activated for your server.", 110 | false 111 | ); 112 | await interaction.followUp({ embeds: [embed], components: [row] }); 113 | }, 114 | }; 115 | -------------------------------------------------------------------------------- /commands/setup-problem.js: -------------------------------------------------------------------------------- 1 | const embedMessage = require('../utility/embed message'); 2 | const { SlashCommandBuilder, SlashCommandChannelOption, PermissionFlagsBits } = require('discord.js'); 3 | 4 | // setup-problem command for turning problem of the day service on for a server 5 | module.exports = { 6 | data: new SlashCommandBuilder() 7 | .setName('setup-problem') 8 | .addChannelOption( 9 | new SlashCommandChannelOption() 10 | .setName('channel') 11 | .setDescription('Select the channel where daily problems will be sent') 12 | .setRequired(true) 13 | ) 14 | .setDescription('Setup problem of the day'), 15 | async execute(interaction) { 16 | await interaction.deferReply({ ephemeral: true }); 17 | 18 | // Only the admin can use this command 19 | if (!(interaction.member.permissions.has(PermissionFlagsBits.Administrator) || interaction.member.id === '415490428721168384')) { 20 | await embedMessage(interaction, 'PERMISSIONS ERROR', 'You need to be an **Administrator** to run this command.') 21 | return; 22 | } 23 | 24 | // The bot should be able to send messages in the selected channels 25 | const channel = interaction.options.getChannel('channel'); 26 | if (!channel.permissionsFor(interaction.client.user).has(PermissionFlagsBits.SendMessages)) { 27 | await embedMessage(interaction, 'PERMISSIONS ERROR', 'The bot needs to have **Send Messages** permission in the selected channel.') 28 | return; 29 | } 30 | 31 | // Save server and channel data to db and send a confirmation message 32 | await interaction.client.database.saveProblemChannel(interaction.guildId, channel.id); 33 | await embedMessage(interaction, 'SERVICE ACTIVATED', 'Problem of the day service has been activated for you server!', false); 34 | await interaction.editReply({ embeds: [embed] }); 35 | }, 36 | }; -------------------------------------------------------------------------------- /commands/stats.js: -------------------------------------------------------------------------------- 1 | const embedMessage = require('../utility/embed message'); 2 | const { SlashCommandBuilder } = require('discord.js'); 3 | 4 | // Convert seconds to the most significant unit among seconds, minutes, hours, or days 5 | function formatTime(seconds) { 6 | const units = [[1, "s"], [60, "m"], [60 * 60, "h"], [60 * 60 * 24, "d"]]; 7 | let bestUnit = units[0]; 8 | for (const unit of units) { 9 | if (seconds >= unit[0]) { 10 | bestUnit = unit; 11 | } 12 | } 13 | const [divisor, label] = bestUnit; 14 | return Math.floor(seconds / divisor) + label; 15 | } 16 | 17 | // stats command to view total server, users, latency, ram usage, and uptime 18 | module.exports = { 19 | data: new SlashCommandBuilder() 20 | .setName('stats') 21 | .setDescription('View bot stats'), 22 | async execute(interaction) { 23 | await interaction.deferReply({ ephemeral: true }); 24 | 25 | // Stats data for the embed body 26 | let ram = ((process.memoryUsage().heapUsed / 1024 / 1024) + (process.memoryUsage().heapTotal / 1024 / 1024)).toFixed(2); 27 | let respStr = `**\`Servers \`** \`${interaction.client.guilds.cache.size.toString().padEnd(10)}\` 28 | **\`Users \`** \`${interaction.client.guilds.cache.reduce((a, b) => a + b.memberCount, 0).toString().padEnd(10)}\` 29 | **\`Latency \`** \`${(interaction.client.ws.ping.toString() + 'ms').padEnd(10)}\` 30 | **\`RAM Usage \`** \`${(ram.toString() + 'mb').padEnd(10)}\` 31 | **\`Uptime \`** \`${(formatTime(Math.floor(interaction.client.uptime / 1000))).padEnd(10)}\`` 32 | 33 | await embedMessage(interaction, 'BOT STATS', respStr, false); 34 | }, 35 | }; -------------------------------------------------------------------------------- /commands/stop.js: -------------------------------------------------------------------------------- 1 | const embedMessage = require('../utility/embed message'); 2 | const { SlashCommandBuilder, PermissionFlagsBits } = require('discord.js'); 3 | 4 | // stop command for a server to stop any running service 5 | module.exports = { 6 | data: new SlashCommandBuilder() 7 | .setName('stop') 8 | .addStringOption( 9 | option => option 10 | .setName('feature') 11 | .setDescription('Select the service to stop') 12 | .setRequired(true) 13 | .addChoices( 14 | { name: 'Upcoming contest notifications', value: 'contest' }, 15 | { name: 'Problem of the day', value: 'problem' } 16 | ) 17 | ) 18 | .setDescription('Stop any service running for your server'), 19 | async execute(interaction) { 20 | await interaction.deferReply({ ephemeral: true }); 21 | 22 | // Only the admin can use this command 23 | if (!(interaction.member.permissions.has(PermissionFlagsBits.Administrator) || interaction.member.id === '415490428721168384')) { 24 | await embedMessage(interaction, 'PERMISSIONS ERROR', 'You need to be an **Administrator** to run this command!') 25 | return; 26 | } 27 | 28 | // Get the feature service to be stopped 29 | const feature = interaction.options.getString('feature'); 30 | 31 | // Remove server from the bot db and send a confirmation message 32 | if (feature === 'contest') { 33 | if (!(await interaction.client.database.deleteContestServer(interaction.guildId))) 34 | await embedMessage(interaction, 'INACTIVE SERVICE', 'Upcoming contest notifications service was not active for you server.'); 35 | else 36 | await embedMessage(interaction, 'SERVICE STOPPED', 'Upcoming contest notifications service stopped. Your server members will no longer be notified about any coding contests.', false); 37 | } 38 | else if (feature === 'problem') { 39 | if (!(await interaction.client.database.deleteProblemServer(interaction.guildId))) 40 | await embedMessage(interaction, 'INACTIVE SERVICE', 'Problem of the day service was not active for you server.'); 41 | else 42 | await embedMessage(interaction, 'SERVICE STOPPED', 'Problem of the day service stopped. Your server will no longer receive daily problems.', false); 43 | } 44 | }, 45 | }; -------------------------------------------------------------------------------- /database/mongo.js: -------------------------------------------------------------------------------- 1 | const contestSchema = require('./schema/contest.js'); 2 | const configurationSchema = require('./schema/configuration.js'); 3 | const contestChannelSchema = require('./schema/contestChannel.js'); 4 | const problemChannelSchema = require('./schema/problemChannel.js'); 5 | 6 | // Save contests from the data array as individual documents for the given platform 7 | module.exports.saveContests = async function (platform, data) { 8 | for (d of data) { 9 | // If a contest with the url exists, ignore it. This makes the query 10 | // insert only the new contests not already in the db 11 | await contestSchema.findOneAndUpdate( 12 | { url: d.url }, 13 | { 14 | $setOnInsert: { 15 | name: d.name, 16 | url: d.url, 17 | start: d.start, 18 | end: (d.start + d.duration), 19 | duration: d.duration, 20 | platform: platform, 21 | notified: (d.start < Math.floor(Date.now() / 1000)), 22 | } 23 | }, 24 | { upsert: true } 25 | ); 26 | } 27 | } 28 | 29 | // Return an array of contests sorted by their starting date for the given platform 30 | module.exports.getPlatformContests = async function (platform) { 31 | let contests = await contestSchema.find({ platform: platform, end: { $gte: Math.floor(Date.now() / 1000) } }) 32 | contests.sort((a, b) => (a.start - b.start)); 33 | return contests; 34 | } 35 | 36 | // Return an array of contests which start within an hour whose notifications 37 | // haven't been sent yet, and then set their notified property to true 38 | module.exports.getContestsStartingSoon = async function () { 39 | let contests = await contestSchema.find({ notified: false, start: { $lte: (Math.floor(Date.now() / 1000) + 3600) } }); 40 | await contestSchema.updateMany( 41 | { notified: false, start: { $lte: (Math.floor(Date.now() / 1000) + 3600) } }, 42 | { $set: { notified: true } } 43 | ); 44 | return contests; 45 | } 46 | 47 | module.exports.getNextXContests = async function (x) { 48 | let contests = await contestSchema.find({ notified: false }).sort({ start: 1 }).limit(x); 49 | return contests; 50 | } 51 | 52 | // Return an array of contests which start in coming X days 53 | module.exports.getContestsStartingInXDays = async function (days) { 54 | let contests = await contestSchema 55 | .find({ start: { $lte: (Math.floor(Date.now() / 1000) + days * 86400), $gte: Math.floor(Date.now() / 1000) } }) 56 | .sort({ start: 1 }); 57 | return contests; 58 | } 59 | 60 | // Delete the finished contests from the db 61 | module.exports.deleteFinishedContests = async function () { 62 | await contestSchema.deleteMany({ end: { $lte: Math.floor(Date.now() / 1000) } }); 63 | } 64 | 65 | // Save channel where contest notifications would be sent by the bot 66 | module.exports.saveContestChannel = async function (server, channel, role) { 67 | await contestChannelSchema.findOneAndUpdate( 68 | { server: server }, 69 | { 70 | $set: { 71 | channel: channel, 72 | roleToPing: role 73 | } 74 | }, 75 | { upsert: true, new: true } 76 | ); 77 | } 78 | 79 | // Save channel where daily problems would be sent by the bot 80 | module.exports.saveProblemChannel = async function (server, channel) { 81 | await problemChannelSchema.findOneAndUpdate( 82 | { server: server }, 83 | { 84 | $set: { 85 | channel: channel 86 | } 87 | }, 88 | { upsert: true, new: true } 89 | ); 90 | } 91 | 92 | // Get all channels opted in for receiving the contest notifications 93 | module.exports.getContestChannels = async function () { 94 | let channels = await contestChannelSchema.find({}); 95 | return channels; 96 | } 97 | 98 | // Get all channels opted in for receiving the daily problems 99 | module.exports.getProblemChannels = async function () { 100 | let channels = await problemChannelSchema.find({}); 101 | return channels; 102 | } 103 | 104 | // Delete a saved channel by channel id for contest notifications 105 | // Used when the bot is unable to send a message in the channel 106 | module.exports.deleteContestChannel = async function (channelID) { 107 | await contestChannelSchema.deleteOne({ channel: channelID }); 108 | } 109 | 110 | // Delete a saved channel by channel id for daily problems 111 | // Used when the bot is unable to send a message in the channel 112 | module.exports.deleteProblemChannel = async function (channelID) { 113 | await problemChannelSchema.deleteOne({ channel: channelID }); 114 | } 115 | 116 | // Delete a saved channel by its server id 117 | // Used by the stop command for contest notifications 118 | module.exports.deleteContestServer = async function (serverID) { 119 | const resp = await contestChannelSchema.deleteOne({ server: serverID }); 120 | return resp.deletedCount; 121 | } 122 | 123 | // Delete a saved channel by its server id 124 | // Used by the stop command for daily problems 125 | module.exports.deleteProblemServer = async function (serverID) { 126 | const resp = await problemChannelSchema.deleteOne({ server: serverID }); 127 | return resp.deletedCount; 128 | } 129 | 130 | // Set last daily problem sent day to today 131 | module.exports.setLastDailyProblem = async function () { 132 | const day = (new Date()).getUTCDate(); 133 | await configurationSchema.findOneAndUpdate( 134 | { name: 'config' }, 135 | { 136 | $set: { 137 | lastDailyProblem: day 138 | } 139 | }, 140 | { upsert: true, new: true } 141 | ); 142 | } 143 | 144 | // Get last daily problem sent day 145 | module.exports.getLastDailyProblem = async function () { 146 | let config = await configurationSchema.findOne({ name: 'config' }); 147 | if (!config) return -1; 148 | return config.lastDailyProblem; 149 | } -------------------------------------------------------------------------------- /database/schema/configuration.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | module.exports = mongoose.model("configuration", new mongoose.Schema({ 4 | name: { type: String, default: 'config' }, 5 | lastDailyProblem: { type: Number }, 6 | })); -------------------------------------------------------------------------------- /database/schema/contest.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | module.exports = mongoose.model("contest", new mongoose.Schema({ 4 | name: { type: String }, 5 | url: { type: String }, 6 | platform: { type: String }, 7 | start: { type: Number }, 8 | end: { type: Number }, 9 | duration: { type: Number }, 10 | notified: { type: Boolean, default: false } 11 | })); -------------------------------------------------------------------------------- /database/schema/contestChannel.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | module.exports = mongoose.model("contestChannel", new mongoose.Schema({ 4 | channel: { type: String }, 5 | server: { type: String }, 6 | roleToPing: { type: String }, 7 | })); -------------------------------------------------------------------------------- /database/schema/problemChannel.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | module.exports = mongoose.model("problemChannel", new mongoose.Schema({ 4 | channel: { type: String }, 5 | server: { type: String } 6 | })); -------------------------------------------------------------------------------- /deploy.js: -------------------------------------------------------------------------------- 1 | const fs = require('node:fs'); 2 | const path = require('node:path'); 3 | const { REST } = require('@discordjs/rest'); 4 | const { Routes } = require('discord.js'); 5 | const { clientIdTest, clientIdProd, guildId, tokenTest, tokenProd, isProduction } = require('./config.json'); 6 | const rest = new REST({ version: '10' }).setToken(isProduction ? tokenProd : tokenTest); 7 | 8 | // Fetch and store all the slash commands from the commands folder 9 | const commands = []; 10 | const commandsPath = path.join(__dirname, 'commands'); 11 | const commandFiles = fs.readdirSync(commandsPath).filter(file => file.endsWith('.js')); 12 | for (const file of commandFiles) { 13 | const filePath = path.join(commandsPath, file); 14 | const command = require(filePath); 15 | commands.push(command.data.toJSON()); 16 | } 17 | 18 | // Register all the fetched slash commands on discord 19 | if (isProduction) 20 | rest.put(Routes.applicationCommands(isProduction ? clientIdProd : clientIdTest), { body: commands }) 21 | .then(() => console.log('Successfully registered global application commands!')) 22 | .catch(console.error); 23 | else 24 | rest.put(Routes.applicationGuildCommands(isProduction ? clientIdProd : clientIdTest, guildId), { body: commands }) 25 | .then(() => console.log('Successfully registered server application commands!')) 26 | .catch(console.error); -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const fs = require('node:fs'); 2 | const path = require('node:path'); 3 | const changingActivityLoop = require('./loops/changing activity'); 4 | const contestsScrapingLoop = require('./loops/contests scraping'); 5 | const contestsMessageLoop = require('./loops/contests message'); 6 | const problemMessageLoop = require('./loops/problem message'); 7 | const joiningMessage = require('./utility/joining message'); 8 | const { tokenTest, tokenProd, mongourlTest, mongourlProd, isProduction, tokenTelegramProd, tokenTelegramTest, channelTelegramProd, channelTelegramTest } = require('./config.json'); 9 | const { Client, Collection, GatewayIntentBits, EmbedBuilder, PermissionFlagsBits, ActivityType } = require('discord.js'); 10 | const client = new Client({ intents: [GatewayIntentBits.Guilds] }); 11 | const TelegramBot = require('node-telegram-bot-api'); 12 | 13 | const tokenTelegram = (isProduction) ? tokenTelegramProd : tokenTelegramTest; 14 | client.channelTelegram = (isProduction) ? channelTelegramProd : channelTelegramTest; 15 | client.telegramBot = new TelegramBot(tokenTelegram, { polling: true }); 16 | 17 | process.env.TZ = 'Asia/Kolkata' 18 | 19 | // Initilize mongoDB connection 20 | const mongoose = require('mongoose'); 21 | client.database = require('./database/mongo.js'); 22 | mongoose.connect((isProduction) ? mongourlProd : mongourlTest, { useNewUrlParser: true, useUnifiedTopology: true }) 23 | .then(() => console.log('Connected to MongoDB.')) 24 | .catch((err) => console.log('Unable to connect to MongoDB.\nError: ' + err)); 25 | 26 | // Fetch and initilize all the slash commands from the commands folder 27 | client.commands = new Collection(); 28 | const commandsPath = path.join(__dirname, 'commands'); 29 | const commandFiles = fs.readdirSync(commandsPath).filter(file => file.endsWith('.js')); 30 | for (const file of commandFiles) { 31 | const filePath = path.join(commandsPath, file); 32 | const command = require(filePath); 33 | client.commands.set(command.data.name, command); 34 | } 35 | 36 | // Fetch and initilize all the interaction hooks from the interactions folder 37 | let interactionCommands = []; 38 | const interactionsPath = path.join(__dirname, 'interactions'); 39 | const interactionsFiles = fs.readdirSync(interactionsPath).filter(file => file.endsWith('.js')); 40 | for (const file of interactionsFiles) { 41 | const filePath = path.join(interactionsPath, file); 42 | const command = require(filePath); 43 | interactionCommands.push(command); 44 | } 45 | 46 | // On interaction check and run slash commands 47 | client.on('interactionCreate', async interaction => { 48 | if (!interaction.isChatInputCommand()) return; 49 | const command = client.commands.get(interaction.commandName); 50 | if (!command) return; 51 | try { 52 | await command.execute(interaction); 53 | } catch (error) { 54 | console.error(error); 55 | } 56 | }); 57 | 58 | 59 | // On interaction check and run interaction hooks 60 | client.on('interactionCreate', async interaction => { 61 | for (const command of interactionCommands) { 62 | try { 63 | await command(interaction); 64 | } catch (error) { 65 | console.error(error); 66 | } 67 | } 68 | }); 69 | 70 | // Sends a joining message in the first channel of the server where it has send messages permission 71 | client.on('guildCreate', async guild => await joiningMessage(guild)); 72 | 73 | // Start the contests updating loop and notifications sending loop when bot is ready 74 | let loopsInitialized = false; 75 | client.once('ready', () => { 76 | if (!loopsInitialized) { 77 | changingActivityLoop(client); 78 | contestsMessageLoop(client); 79 | problemMessageLoop(client); 80 | contestsScrapingLoop(client.database); 81 | loopsInitialized = true; 82 | } 83 | console.log('Bot is online.'); 84 | }); 85 | 86 | // Login and start the discord bot 87 | client.login(isProduction ? tokenProd : tokenTest); -------------------------------------------------------------------------------- /interactions/contests-in.js: -------------------------------------------------------------------------------- 1 | const contestsInPaginate = require("../utility/contests-in"); 2 | 3 | async function contestsInPaginateWrap(interaction) { 4 | await contestsInPaginate(interaction, false); 5 | } 6 | 7 | module.exports = contestsInPaginateWrap; -------------------------------------------------------------------------------- /interactions/contests.js: -------------------------------------------------------------------------------- 1 | const { EmbedBuilder, ActionRowBuilder, StringSelectMenuBuilder, ButtonBuilder, ButtonStyle } = require('discord.js'); 2 | 3 | // Data about the different supported platforms 4 | const platforms = { 5 | 'codechef': { 'name': 'CodeChef', 'url': 'https://www.codechef.com', 'thumb': 'https://i.imgur.com/XdBzq6c.jpg' }, 6 | 'leetcode': { 'name': 'LeetCode', 'url': 'https://leetcode.com', 'thumb': 'https://i.imgur.com/slIjkP3.jpg' }, 7 | 'hackerrank': { 'name': 'HackerRank', 'url': 'https://www.hackerrank.com', 'thumb': 'https://i.imgur.com/sduFQNq.jpg' }, 8 | 'codeforces': { 'name': 'CodeForces', 'url': 'https://codeforces.com', 'thumb': 'https://i.imgur.com/EVmQOW5.jpg' }, 9 | 'atcoder': { 'name': 'AtCoder', 'url': 'https://atcoder.jp', 'thumb': 'https://i.imgur.com/mfB9fEI.jpg' }, 10 | 'hackerearth': { 'name': 'HackerEarth', 'url': 'https://www.hackerearth.com', 'thumb': 'https://i.imgur.com/CACYwoz.jpg' }, 11 | 'geeksforgeeks': { 'name': 'Geeksforgeeks', 'url': 'https://practice.geeksforgeeks.org', 'thumb': 'https://i.imgur.com/ejRKy7l.jpg' }, 12 | 'codingninjas': { 'name': 'Coding Ninjas', 'url': 'https://www.codingninjas.com', 'thumb': 'https://i.imgur.com/X9WJiRv.png'} 13 | } 14 | 15 | // Total contests to show per page 16 | // This is needed due to the 4k character limit of the embed description 17 | const contestsPerPage = 6; 18 | 19 | // Updates the embed from /contests command when the select menu is used 20 | async function contestsPaginate(interaction) { 21 | let pageNum; 22 | let platform; 23 | 24 | // To handle both the select menu, and the previous and next page buttons 25 | if (interaction.isButton()) { 26 | if (interaction.customId.substring(0, 14) != 'contestsButton') return; 27 | pageNum = parseInt(interaction.customId.substring(14, 15), 10); 28 | platform = interaction.customId.substring(15, interaction.customId.length); 29 | } 30 | else if (interaction.isSelectMenu()) { 31 | if (interaction.customId != 'contestsSelect') return; 32 | pageNum = 0; 33 | platform = interaction.values.toString(); 34 | } 35 | else return; 36 | 37 | // Incase it takes longer than 3 seconds to respond 38 | await interaction.deferUpdate(); 39 | 40 | // Get the platform from interaction and fetch its contests data from db 41 | let data = await interaction.client.database.getPlatformContests(platform); 42 | 43 | // Format the contests data for the embed body 44 | let respStr = ""; 45 | let ongoingTextSent = false; 46 | let upcomingTextSent = false; 47 | let contestsCount = data.length - contestsPerPage * pageNum; 48 | let maxContests = pageNum * contestsPerPage + ((contestsCount > contestsPerPage) ? contestsPerPage : contestsCount); 49 | for (let i = pageNum * contestsPerPage; i < maxContests; i++) { 50 | let contestData = data[i]; 51 | let ongoing = (contestData['start'] < Math.floor(Date.now() / 1000)); 52 | let hours = Math.floor(contestData['duration'] / 3600); 53 | let mins = Math.floor((contestData['duration'] / 60) % 60); 54 | let durationLeft = contestData['duration'] + contestData['start'] - Date.now() / 1000; 55 | let hoursLeft = Math.floor(durationLeft / 3600); 56 | let minsLeft = Math.floor((durationLeft / 60) % 60); 57 | if (ongoing) { 58 | if (!ongoingTextSent) { 59 | respStr += "**ONGOING CONTESTS**\n"; 60 | ongoingTextSent = true; 61 | } 62 | respStr += `**[${contestData['name']}](${contestData['url']})**\n:stopwatch: **Time left:** ${hoursLeft} ${hoursLeft === 1 ? 'hour' : 'hours'} and ${minsLeft} ${minsLeft === 1 ? 'minute' : 'minutes'}`; 63 | } 64 | else { 65 | if (!upcomingTextSent) { 66 | respStr += "**UPCOMING CONTESTS**\n"; 67 | upcomingTextSent = true; 68 | } 69 | respStr += `**[${contestData['name']}](${contestData['url']})**\n:calendar: **Start:** at \n:stopwatch: **Duration:** ${hours} ${hours === 1 ? 'hour' : 'hours'}${mins === 0 ? '' : (' and ' + mins + ' minutes')}`; 70 | } 71 | if (i !== maxContests - 1) respStr += "\n\n"; 72 | } 73 | if (!maxContests) respStr += "**No scheduled contests**\n\u200b"; 74 | 75 | // Previous and Next page buttons incase total contests exceeed contestsPerPage 76 | const row1 = new ActionRowBuilder() 77 | .addComponents( 78 | new ButtonBuilder() 79 | .setCustomId(`contestsButton${pageNum - 1}${platform}`) 80 | .setLabel('Previous Page') 81 | .setDisabled(!(pageNum > 0)) 82 | .setStyle(ButtonStyle.Primary), 83 | new ButtonBuilder() 84 | .setCustomId(`contestsButton${pageNum + 1}${platform}`) 85 | .setLabel('Next Page') 86 | .setDisabled(!(contestsCount > contestsPerPage)) 87 | .setStyle(ButtonStyle.Primary), 88 | ); 89 | 90 | // Select menu for switching to other platforms 91 | const row2 = new ActionRowBuilder() 92 | .addComponents( 93 | new StringSelectMenuBuilder() 94 | .setCustomId('contestsSelect') 95 | .setPlaceholder('Select contest platform') 96 | .addOptions( 97 | { 98 | label: 'CodeChef', 99 | value: 'codechef', 100 | emoji: { id: '1024020300834279484' }, 101 | }, 102 | { 103 | label: 'LeetCode', 104 | value: 'leetcode', 105 | emoji: { id: '1024019529283674183' }, 106 | }, 107 | { 108 | label: 'HackerRank', 109 | value: 'hackerrank', 110 | emoji: { id: '1024019532190339193' }, 111 | }, 112 | { 113 | label: 'CodeForces', 114 | value: 'codeforces', 115 | emoji: { id: '1024341762166243348' }, 116 | }, 117 | { 118 | label: 'AtCoder', 119 | value: 'atcoder', 120 | emoji: { id: '1025657008688484363' }, 121 | }, 122 | { 123 | label: 'HackerEarth', 124 | value: 'hackerearth', 125 | emoji: { id: '1025657011360243782' }, 126 | }, 127 | { 128 | label: 'Geeksforgeeks', 129 | value: 'geeksforgeeks', 130 | emoji: { id: '1110941777260711986' } 131 | }, 132 | { 133 | label: 'Coding Ninjas', 134 | value: 'codingninjas', 135 | emoji: { id: '1118598468978618518' } 136 | } 137 | ), 138 | ); 139 | 140 | // Don't add previous and next page buttons if total contests is less than contestsPerPage 141 | const rows = []; 142 | rows.push(row2); 143 | if (data.length > contestsPerPage) rows.push(row1); 144 | 145 | // Create the embed 146 | let embed = new EmbedBuilder() 147 | .setColor(0x1089DF) 148 | .setTitle(platforms[platform]['name'].toUpperCase()) 149 | .setURL(platforms[platform]['url']) 150 | .setDescription(respStr + "** **") 151 | .setImage(platforms[platform]['thumb']); 152 | 153 | // Set current page number in footer if paginated 154 | if (data.length > contestsPerPage) embed.setFooter({ text: `Current page ${pageNum + 1}/${Math.trunc(data.length / contestsPerPage) + 1}` }); 155 | 156 | // Send the embed with the components 157 | await interaction.editReply({ embeds: [embed], components: rows }); 158 | } 159 | 160 | module.exports = contestsPaginate; -------------------------------------------------------------------------------- /interactions/notifications.js: -------------------------------------------------------------------------------- 1 | // Handle the Notifications button click to add and remove ping role 2 | async function notificationsRole(interaction) { 3 | if (!interaction.isButton()) return; 4 | 5 | const isOn = interaction.customId.startsWith("notificationsOn"); 6 | const isOff = interaction.customId.startsWith("notificationsOff"); 7 | if (!isOn && !isOff) return; 8 | 9 | await interaction.deferUpdate(); 10 | 11 | // Role ID is stored directly as a part of the button custom ID as 'notifications(On/Off){ID}' 12 | const roleId = interaction.customId.substring(isOn ? 15 : 16); 13 | const role = interaction.guild.roles.cache.get(roleId); 14 | 15 | if (!role) { 16 | await interaction.followUp({ 17 | content: `Role not found. Please contact the server admin to rerun the notifications commands.`, 18 | ephemeral: true, 19 | }); 20 | return; 21 | } 22 | 23 | const hasRole = interaction.member.roles.cache.has(roleId); 24 | if (isOn && hasRole) { 25 | await interaction.followUp({ 26 | content: `You are already receiving the notification - **${role.name}**`, 27 | ephemeral: true, 28 | }); 29 | } else if (isOff && !hasRole) { 30 | await interaction.followUp({ 31 | content: `You don't have the role **${role.name}**, first subscribe to the notifications by clicking on **Get Notified** button.`, 32 | ephemeral: true, 33 | }); 34 | } else { 35 | if (isOn) { 36 | await interaction.member.roles.add(role); 37 | await interaction.followUp({ 38 | content: `**${role.name}** role was added for you!`, 39 | ephemeral: true, 40 | }); 41 | } else { 42 | await interaction.member.roles.remove(role); 43 | await interaction.followUp({ 44 | content: `**${role.name}** role was removed for you!`, 45 | ephemeral: true, 46 | }); 47 | } 48 | } 49 | } 50 | 51 | module.exports = notificationsRole; 52 | -------------------------------------------------------------------------------- /loops/changing activity.js: -------------------------------------------------------------------------------- 1 | const { ActivityType } = require('discord.js'); 2 | 3 | async function activity(client) { 4 | let i = 0; 5 | setInterval(() => { 6 | const activities = [`${client.guilds.cache.size} servers`, "/help for user guide"] 7 | client.user.setActivity(activities[i], { type: ActivityType.Watching }); 8 | i = 1 - i; 9 | }, 15000); 10 | } 11 | 12 | module.exports = activity; 13 | -------------------------------------------------------------------------------- /loops/contests message.js: -------------------------------------------------------------------------------- 1 | const { EmbedBuilder } = require('discord.js'); 2 | 3 | // Data about the different supported platforms 4 | const platforms = { 5 | 'codechef': 'CodeChef', 6 | 'leetcode': 'LeetCode', 7 | 'hackerrank': 'HackerRank', 8 | 'codeforces': 'CodeForces', 9 | 'atcoder': 'AtCoder', 10 | 'geeksforgeeks': 'Geeksforgeeks', 11 | 'codingninjas': 'CodingNinjas', 12 | 'hackerearth': 'HackerEarth', 13 | } 14 | 15 | const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; 16 | 17 | function formatAMPM(date) { 18 | let hours = date.getHours(); 19 | let minutes = date.getMinutes(); 20 | let ampm = hours >= 12 ? 'pm' : 'am'; 21 | hours = hours % 12; 22 | hours = hours ? hours : 12; 23 | minutes = minutes < 10 ? '0' + minutes : minutes; 24 | let strTime = hours.toString().padStart(2, "0") + ':' + minutes.toString().padStart(2, "0") + ampm; 25 | return strTime; 26 | } 27 | // Send notifications for the contests starting soon 28 | async function notify(client) { 29 | const contests = await client.database.getContestsStartingSoon(); 30 | if (contests.length === 0) return; // No contests starting soon 31 | 32 | const nextContests = await client.database.getNextXContests(5); 33 | 34 | // Format the information about the contests starting soon for the embed body 35 | let respStr = ""; 36 | let telegramStr = "❇️❇️ *Coding contest starting soon* ❇️❇️"; 37 | for (let i = 0; i < contests.length + nextContests.length; i++) { 38 | let contestData; 39 | if (i == contests.length) telegramStr += `\n\n❇️❇️ *Next five scheduled contests* ❇️❇️`; 40 | if (i >= contests.length) 41 | contestData = nextContests[i - contests.length]; 42 | else 43 | contestData = contests[i]; 44 | 45 | let hours = Math.floor(contestData['duration'] / 3600); 46 | let mins = Math.floor((contestData['duration'] / 60) % 60); 47 | let time = `${hours} ${hours === 1 ? 'hour' : 'hours'}${mins === 0 ? '' : (' and ' + mins + ' minutes')}`; 48 | let date = new Date(contestData['start'] * 1000); 49 | let formattedDate = `${date.getDate().toString().padStart(2, "0")} ${months[date.getMonth()]} at ${formatAMPM(date)} for ${hours}H:${mins.toString().padStart(2, "0")}M`; 50 | telegramStr += `\n\n[${contestData['name'].replace(/[\[\]]/g, '')}](${contestData['url']})\n🎟️ _${platforms[contestData['platform']]}\n⏰ ${formattedDate}_`; 51 | 52 | if (i < contests.length) { 53 | respStr += `**[${contestData['name'].replace(/[\[\]]/g, '')}](${contestData['url']})**\n:dart: **Platform:** ${platforms[contestData['platform']]}\n:calendar: **Start:** \n:stopwatch: **Duration:** ${time}`; 54 | if (i !== contests.length - 1) respStr += "\n\n"; 55 | } 56 | } 57 | 58 | // Send message on telegram channel 59 | client.telegramBot.sendPhoto(client.channelTelegram, "https://i.imgur.com/KCnOAHf.jpg", { caption: telegramStr, parse_mode: "Markdown", disable_web_page_preview: true }) 60 | 61 | // Create the embed to be sent to all channels 62 | const embed = new EmbedBuilder() 63 | .setColor(0x1089DF) 64 | .setTitle((contests.length === 1) ? 'CODING CONTEST STARTING SOON' : 'CODING CONTESTS STARTING SOON') 65 | .setImage('https://i.imgur.com/o76L2rG.jpg') 66 | .setDescription(respStr); 67 | 68 | // Send the embed with their respective role ping to all the channels in db 69 | const channels = await client.database.getContestChannels(); 70 | for (let channelData of channels) { 71 | try { 72 | let channel = await client.channels.fetch(channelData['channel']); 73 | await channel.send({ content: `<@&${channelData['roleToPing']}>`, embeds: [embed] }); 74 | } catch (error) { 75 | // Delete the channel from db if bot was unable to access it 76 | await client.database.deleteContestChannel(channelData['channel']); 77 | } 78 | } 79 | } 80 | 81 | // Run the notify function once on load, and then every 15 minutes 82 | async function notifyLoop(client) { 83 | console.log("Notifications loop started."); 84 | await notify(client); 85 | setInterval(async () => { 86 | await notify(client); 87 | }, 900000); 88 | } 89 | 90 | module.exports = notifyLoop; 91 | -------------------------------------------------------------------------------- /loops/contests scraping.js: -------------------------------------------------------------------------------- 1 | const axios = require("axios"); 2 | const jsdom = require("jsdom"); 3 | const crypto = require('crypto'); 4 | const { codeforcesKey, codeforcesSecret } = require('../config.json'); 5 | const { JSDOM } = jsdom; 6 | const puppeteer = require("puppeteer"); 7 | const headers = { 'headers': { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36' } }; 8 | 9 | // Get contests from CodeChef 10 | async function codeChef() { 11 | const res = await axios.get('https://www.codechef.com/api/list/contests/all?sort_by=START&sorting_order=asc&offset=0&mode=all', headers) 12 | const futureContests = [...res.data["future_contests"], ...res.data["present_contests"]]; 13 | let processedData = futureContests.map(c => { 14 | let name = c["contest_name"]; 15 | let url = "https://www.codechef.com/" + c["contest_code"] 16 | let start = Math.floor((new Date(c["contest_start_date_iso"])).getTime() / 1000); 17 | let duration = (parseInt(c["contest_duration"], 10) || 0) * 60; 18 | return { name, url, start, duration }; 19 | }); 20 | return processedData; 21 | } 22 | 23 | // Get contests from LeetCode 24 | async function leetCode() { 25 | const res = await axios.get('https://leetcode.com/graphql?query={%20allContests%20{%20title%20titleSlug%20startTime%20duration%20__typename%20}%20}', headers) 26 | const futureContests = res.data["data"]["allContests"].filter((e) => ((e["startTime"] + e["duration"]) > Math.floor(Date.now() / 1000))); 27 | let processedData = futureContests.map(c => { 28 | let name = c["title"]; 29 | let url = "https://leetcode.com/contest/" + c["titleSlug"] 30 | let start = c["startTime"]; 31 | let duration = c["duration"]; 32 | return { name, url, start, duration }; 33 | }); 34 | return processedData; 35 | } 36 | 37 | // Get contests from HackerRank 38 | async function hackerRank() { 39 | const res = await axios.get('https://www.hackerrank.com/rest/contests/upcoming?limit=10', headers) 40 | const futureContests = res.data["models"].filter((e) => (e["epoch_endtime"] > Math.floor(Date.now() / 1000))); 41 | let processedData = futureContests.map(c => { 42 | let name = c["name"]; 43 | let url = "https://www.hackerrank.com/contests/" + c["slug"] 44 | let start = c["epoch_starttime"]; 45 | let duration = c["epoch_endtime"] - c["epoch_starttime"]; 46 | return { name, url, start, duration }; 47 | }); 48 | return processedData; 49 | } 50 | 51 | // Get contests from CodeForces 52 | async function codeForces() { 53 | const random = Math.floor(Math.random() * 899999) + 100000; 54 | const currentTime = Math.floor(Date.now() / 1000); 55 | let hashRaw = `${random}/contest.list?apiKey=${codeforcesKey}&time=${currentTime}#${codeforcesSecret}`; 56 | let hash = crypto.createHash('sha512').update(hashRaw).digest('hex'); 57 | let url = `https://codeforces.com/api/contest.list?apiKey=${codeforcesKey}&time=${currentTime}&apiSig=${random + hash}`; 58 | 59 | const res = await axios.get(url, headers) 60 | const futureContests = res.data["result"].filter(e => (e["phase"] === "BEFORE" || e["phase"] === "CODING")); 61 | let processedData = futureContests.map(c => { 62 | let name = c["name"]; 63 | let url = "https://codeforces.com/contest/" + c["id"]; 64 | let start = c["startTimeSeconds"]; 65 | let duration = c["durationSeconds"]; 66 | return { name, url, start, duration }; 67 | }); 68 | return processedData; 69 | } 70 | 71 | // Get contests from AtCoder 72 | async function atCoder() { 73 | const res = await axios.get('https://atcoder.jp/contests', headers); 74 | const dom = new JSDOM(res.data); 75 | const active = (dom.window.document.getElementById('contest-table-action')); 76 | const upcoming = (dom.window.document.getElementById('contest-table-upcoming')); 77 | 78 | let array = []; 79 | function addToArray(row) { 80 | const epochtime = Date.parse(new Date(row.cells[0].lastChild.firstChild.innerHTML)); 81 | if (Number.isNaN(epochtime)) return; // Exit if start time isn't valid 82 | 83 | const link = row.cells[1].querySelector('a'); 84 | const url = 'https://atcoder.jp' + link.href; 85 | const durstring = row.cells[2].innerHTML.split(':'); 86 | const duration = durstring[0] * 3600 + durstring[1] * 60; 87 | 88 | array.push({ name: link.innerHTML, url: url, start: Math.floor(epochtime / 1000), duration: duration }); 89 | } 90 | 91 | if (active) active.querySelectorAll('table tr').forEach(row => addToArray(row)); 92 | if (upcoming) upcoming.querySelectorAll('table tr').forEach(row => addToArray(row)); 93 | 94 | return Array.from(array); 95 | } 96 | 97 | // Get contests from HackerEarth 98 | async function hackerEarth() { 99 | const res = await axios.get('https://www.hackerearth.com/chrome-extension/events', headers); 100 | const futureContests = res.data.response.filter(c => { 101 | const endtime = new Date(c["end_utc_tz"]); 102 | return Math.floor(endtime.getTime() / 1000) > Math.floor(Date.now() / 1000) 103 | }); 104 | let processedData = futureContests.map(c => { 105 | const endTimeSeconds = new Date(c["end_utc_tz"]).getTime() / 1000; 106 | const startTimeSeconds = new Date(c["start_utc_tz"]).getTime() / 1000; 107 | let name = c["title"]; 108 | let url = c["url"]; 109 | let start = startTimeSeconds; 110 | let duration = endTimeSeconds - startTimeSeconds; 111 | return { name, url, start, duration }; 112 | }); 113 | return processedData; 114 | } 115 | 116 | // Get contests from GeeksforGeeks 117 | async function geeksforgeeks() { 118 | const res = await axios.get("https://practiceapi.geeksforgeeks.org/api/vr/events/?page_number=1&sub_type=all&type=contest", headers) 119 | const futureContests = res.data["results"]["upcoming"]; 120 | let processedData = futureContests.map(c => { 121 | let name = c["name"] 122 | let url = "https://practice.geeksforgeeks.org/contest/" + c["slug"] 123 | let start = Math.floor(((new Date(c["start_time"])).getTime()) / 1000); 124 | const endTimeSeconds = (new Date(c["end_time"]).getTime()) / 1000; 125 | const startTimeSeconds = (new Date(c["start_time"]).getTime()) / 1000; 126 | let duration = endTimeSeconds - startTimeSeconds; 127 | 128 | return { name, url, start, duration }; 129 | }) 130 | return processedData; 131 | } 132 | 133 | // Get contests for coding ninjas 134 | async function codingninjas() { 135 | const res = await axios.get("https://api.codingninjas.com/api/v4/public_section/contest_list", headers) 136 | const contests = res.data["data"]["events"]; 137 | 138 | let processedData = contests.map(c => { 139 | let name = c["name"] 140 | let url = "https://www.codingninjas.com/codestudio/contests/" + c["slug"] 141 | let start = c["event_start_time"]; 142 | const endTimeSeconds = c["event_end_duration"] 143 | const startTimeSeconds = c["event_start_duration"] 144 | let duration = endTimeSeconds - startTimeSeconds; 145 | 146 | return { name, url, start, duration }; 147 | }) 148 | return processedData 149 | } 150 | 151 | // Save new contests to the db and delete the finished ones 152 | async function contests(db) { 153 | try { 154 | await db.saveContests('codechef', await codeChef()); 155 | } catch (error) { 156 | console.log("Codechef scraping failed - " + error) 157 | } 158 | try { 159 | await db.saveContests('leetcode', await leetCode()); 160 | } catch (error) { 161 | console.log("Leetcode scraping failed - " + error) 162 | } 163 | try { 164 | await db.saveContests('hackerrank', await hackerRank()); 165 | } catch (error) { 166 | console.log("Hackerrank scraping failed - " + error) 167 | } 168 | try { 169 | await db.saveContests('codeforces', await codeForces()); 170 | } catch (error) { 171 | console.log("Codeforcees scraping failed - " + error) 172 | } 173 | try { 174 | await db.saveContests('atcoder', await atCoder()); 175 | } catch (error) { 176 | console.log("Atcoder scraping failed - " + error) 177 | } 178 | try { 179 | await db.saveContests('hackerearth', await hackerEarth()); 180 | } catch (error) { 181 | console.log("Hackerearth scraping failed - " + error) 182 | } 183 | try { 184 | await db.saveContests('geeksforgeeks', await geeksforgeeks()); 185 | } catch (error) { 186 | console.log("Geeksforgeeks scraping failed - " + error) 187 | } 188 | try { 189 | await db.saveContests('codingninjas', await codingninjas()); 190 | } catch (error) { 191 | console.log("Codingninjas scraping failed - " + error) 192 | } 193 | await db.deleteFinishedContests(); 194 | console.log(`DB updated with contests at ${new Date()}`); 195 | } 196 | 197 | // Run the notify function once on load, and then every 6 hour 198 | async function updateContests(db) { 199 | console.log("Scraping loop started."); 200 | await contests(db); 201 | setInterval(async () => { 202 | await contests(db); 203 | }, 21600000); 204 | } 205 | 206 | module.exports = updateContests; -------------------------------------------------------------------------------- /loops/problem message.js: -------------------------------------------------------------------------------- 1 | const axios = require("axios"); 2 | const { EmbedBuilder } = require('discord.js'); 3 | const headers = { 'headers': { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36' } }; 4 | 5 | // Get problem of the day from LeetCode 6 | async function problemOfTheDay() { 7 | const query = await axios.get('https://leetcode.com/graphql?query={ activeDailyCodingChallengeQuestion {link question { difficulty acRate hasSolution title titleSlug topicTags { name } } }}', headers); 8 | const res = query.data["data"]["activeDailyCodingChallengeQuestion"]; 9 | let name = res["question"]["title"]; 10 | let url = "https://leetcode.com" + res["link"]; 11 | let difficulty = res["question"]["difficulty"]; 12 | let acRate = Math.round(res["question"]["acRate"]); 13 | let hasSolution = res["question"]["hasSolution"]; 14 | let topics = res["question"]["topicTags"].map(topic => { 15 | return topic["name"]; 16 | }); 17 | return { name, url, acRate, hasSolution, difficulty, topics }; 18 | } 19 | 20 | // Send problem of the day to all the channels opted in for it 21 | async function sendProblem(client) { 22 | 23 | // Return if problem for that day is sent already 24 | const lastDailyProblem = await client.database.getLastDailyProblem(); 25 | const today = new Date(); 26 | if (lastDailyProblem === today.getUTCDate() || today.getUTCHours < 2) return; 27 | 28 | // Get and format the problem for embed body 29 | const problem = await problemOfTheDay(); 30 | let respStr = ""; 31 | respStr += `**[${problem.name}](${problem.url})**\n\n`; 32 | respStr += `:muscle: **Difficulty:** ${problem.difficulty}\n`; 33 | respStr += `:dart: **Acceptance Rate:** ${problem.acRate}%\n`; 34 | respStr += (problem.hasSolution) ? ':white_check_mark: **Solution: ** Present\n\n' : ':negative_squared_cross_mark: **Solution: ** Absent\n\n'; 35 | respStr += ':pencil: **Concepts:** '; 36 | for (let [i, topic] of problem.topics.entries()) { 37 | respStr += topic.toLowerCase(); 38 | if (i === problem.topics.length - 2) respStr += ', and ' 39 | else if (i !== problem.topics.length - 1) respStr += ', '; 40 | } 41 | 42 | // Create the embed to be sent 43 | const embed = new EmbedBuilder() 44 | .setColor(0x1089DF) 45 | .setTitle('PROBLEM OF THE DAY') 46 | .setImage('https://i.imgur.com/o76L2rG.jpg') 47 | .setDescription(respStr); 48 | 49 | // Send the problem to all the channels opted in for it 50 | const channels = await client.database.getProblemChannels(); 51 | for (let channelData of channels) { 52 | try { 53 | let channel = await client.channels.fetch(channelData['channel']); 54 | await channel.send({ embeds: [embed] }); 55 | } catch (error) { 56 | await client.database.deleteProblemChannel(channelData['channel']); 57 | } 58 | } 59 | 60 | // Set last daily problem sent day to the current day 61 | await client.database.setLastDailyProblem(); 62 | } 63 | 64 | // Run the sendProblem function once on load, and then every 1 hour 65 | async function sendProblemLoop(client) { 66 | console.log("Daily problem loop started."); 67 | await sendProblem(client); 68 | setInterval(async () => { 69 | await sendProblem(client); 70 | }, 3600000); 71 | } 72 | 73 | module.exports = sendProblemLoop; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "discord", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "axios": "^0.27.2", 14 | "discord.js": "^14.8.0", 15 | "jsdom": "^20.0.0", 16 | "mongoose": "^6.6.4", 17 | "node-telegram-bot-api": "^0.63.0", 18 | "nodemon": "^2.0.22", 19 | "puppeteer": "^18.0.5" 20 | }, 21 | "devDependencies": { 22 | "nodemon": "^2.0.22" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /utility/contests-in.js: -------------------------------------------------------------------------------- 1 | const { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require('discord.js'); 2 | 3 | // Data about the different supported platforms 4 | const platforms = { 5 | 'codechef': { 'name': 'CodeChef', 'url': 'https://www.codechef.com', 'thumb': 'https://i.imgur.com/XdBzq6c.jpg' }, 6 | 'leetcode': { 'name': 'LeetCode', 'url': 'https://leetcode.com', 'thumb': 'https://i.imgur.com/slIjkP3.jpg' }, 7 | 'hackerrank': { 'name': 'HackerRank', 'url': 'https://www.hackerrank.com', 'thumb': 'https://i.imgur.com/sduFQNq.jpg' }, 8 | 'codeforces': { 'name': 'CodeForces', 'url': 'https://codeforces.com', 'thumb': 'https://i.imgur.com/EVmQOW5.jpg' }, 9 | 'atcoder': { 'name': 'AtCoder', 'url': 'https://atcoder.jp', 'thumb': 'https://i.imgur.com/mfB9fEI.jpg' }, 10 | 'hackerearth': { 'name': 'HackerEarth', 'url': 'https://www.hackerearth.com', 'thumb': 'https://i.imgur.com/CACYwoz.jpg' }, 11 | 'geeksforgeeks': { 'name': 'Geeksforgeeks', 'url': 'https://practice.geeksforgeeks.org', 'thumb': 'https://i.imgur.com/ejRKy7l.jpg' }, 12 | 'codingninjas': { 'name': 'Coding Ninjas', 'url': 'https://www.codingninjas.com', 'thumb': 'https://i.imgur.com/X9WJiRv.png' } 13 | } 14 | 15 | // Total contests to show per page 16 | // This is needed due to the 4k character limit of the embed description 17 | const contestsPerPage = 6; 18 | 19 | // Updates the embed from /contests command when the select menu is used 20 | async function contestsInPaginate(interaction, commandType) { 21 | let pageNum; 22 | let days; 23 | 24 | // To handle both the select menu, and the previous and next page buttons 25 | if (interaction.isButton()) { 26 | if (interaction.customId.substring(0, 16) != 'contestsInButton') return; 27 | pageNum = parseInt(interaction.customId.substring(16, 17), 10); 28 | days = parseInt(interaction.customId.substring(17, 18), 10); 29 | } 30 | else if (interaction.isCommand() && commandType) { 31 | days = interaction.options.getNumber("start"); 32 | pageNum = 0; 33 | if (days == null) return; 34 | } 35 | else return; 36 | 37 | // Incase it takes longer than 3 seconds to respond 38 | if (!commandType) 39 | await interaction.deferUpdate(); 40 | 41 | // Get the platform from interaction and fetch its contests data from db 42 | let data = await interaction.client.database.getContestsStartingInXDays(days); 43 | 44 | // Format the contests data for the embed body 45 | let respStr = ""; 46 | let contestsCount = data.length - contestsPerPage * pageNum; 47 | let maxContests = pageNum * contestsPerPage + ((contestsCount > contestsPerPage) ? contestsPerPage : contestsCount); 48 | for (let i = pageNum * contestsPerPage; i < maxContests; i++) { 49 | let contestData = data[i]; 50 | let hours = Math.floor(contestData['duration'] / 3600); 51 | let mins = Math.floor((contestData['duration'] / 60) % 60); 52 | respStr += `**[${contestData['name']}](${contestData['url']})**\n:calendar: **Start:** at \n:stopwatch: **Duration:** ${hours} ${hours === 1 ? 'hour' : 'hours'}${mins === 0 ? '' : (' and ' + mins + ' minutes')}\n:flags: **Platform:** ${platforms[contestData['platform']]['name']}`; 53 | if (i !== maxContests - 1) respStr += "\n\n"; 54 | } 55 | if (!maxContests) respStr += "**No scheduled contests**\n\u200b"; 56 | 57 | // Previous and Next page buttons incase total contests exceeed contestsPerPage 58 | const row1 = new ActionRowBuilder() 59 | .addComponents( 60 | new ButtonBuilder() 61 | .setCustomId(`contestsInButton${pageNum - 1}${days}`) 62 | .setLabel('Previous Page') 63 | .setDisabled(!(pageNum > 0)) 64 | .setStyle(ButtonStyle.Primary), 65 | new ButtonBuilder() 66 | .setCustomId(`contestsInButton${pageNum + 1}${days}`) 67 | .setLabel('Next Page') 68 | .setDisabled(!(contestsCount > contestsPerPage)) 69 | .setStyle(ButtonStyle.Primary), 70 | ); 71 | 72 | // Don't add previous and next page buttons if total contests is less than contestsPerPage 73 | const rows = []; 74 | if (data.length > contestsPerPage) rows.push(row1); 75 | 76 | // Create the embed 77 | let embed = new EmbedBuilder() 78 | .setColor(0x1089DF) 79 | .setTitle(`CONTESTS IN ${days} DAYS`) 80 | .setDescription(respStr + "** **") 81 | .setImage("https://i.imgur.com/Sj1bgx5.jpg") 82 | 83 | // Set current page number in footer if paginated 84 | if (data.length > contestsPerPage) embed.setFooter({ text: `Current page ${pageNum + 1}/${Math.trunc(data.length / contestsPerPage) + 1}` }); 85 | 86 | // Send the embed with the components 87 | await interaction.editReply({ embeds: [embed], components: rows }); 88 | } 89 | 90 | module.exports = contestsInPaginate; -------------------------------------------------------------------------------- /utility/embed message.js: -------------------------------------------------------------------------------- 1 | const { EmbedBuilder } = require("discord.js"); 2 | 3 | async function embedMessage(interaction, title, description, isError = true, url, returnEmbed = false) { 4 | let color = (isError) ? 0xED4245 : 0x1089DF; 5 | let embed = new EmbedBuilder() 6 | .setTitle(title) 7 | .setDescription(description) 8 | .setColor(color); 9 | if (url) embed.setURL(url); 10 | if(returnEmbed) return embed; 11 | else await interaction.editReply({ embeds: [embed] }); 12 | } 13 | 14 | module.exports = embedMessage; 15 | -------------------------------------------------------------------------------- /utility/joining message.js: -------------------------------------------------------------------------------- 1 | const embedMessage = require('./embed message'); 2 | const { PermissionFlagsBits } = require('discord.js'); 3 | 4 | async function joiningMessage(guild) { 5 | try { 6 | let channelToSend; 7 | guild.channels.cache.forEach(channel => { 8 | const hasPermission = channel.permissionsFor(client.user).has(PermissionFlagsBits.SendMessages); 9 | if (channel.type === 0 && !channelToSend && hasPermission) channelToSend = channel; 10 | }); 11 | 12 | // Return if no channel with send messages permission found 13 | if (!channelToSend) return; 14 | 15 | // Create and send the embed 16 | const embed = await embedMessage(null, `Hello ${guild.name}`, "Thank you for inviting the Coding Contests Companion to this server. It can show ongoing and upcoming contest information, send notifications before one is about to start, and send daily problems from various popular coding platforms. Use the **/help** command for the user guide. If you're facing any difficulties, feel free to **[join the support server.](https://discord.gg/9sDtq74DMn)**", false, "https://github.com/roshan1337d/coding-contests-companion", true); 17 | await channelToSend.send({ embeds: [embed] }); 18 | } catch (error) { 19 | console.log("Joining message error: " + error); 20 | } 21 | } 22 | 23 | module.exports = joiningMessage; 24 | --------------------------------------------------------------------------------