├── .github ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── codeql-analysis.yml │ └── python-app.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── LICENSE ├── Makefile ├── README.md ├── flake8_requirements.txt ├── pm2.json ├── poetry.lock ├── poetry.toml ├── pyproject.toml ├── requirements.txt ├── src ├── aerich.ini ├── bot.py ├── cogs │ ├── esports │ │ ├── __init__.py │ │ ├── errors.py │ │ ├── events │ │ │ ├── __init__.py │ │ │ ├── scrims.py │ │ │ ├── slots.py │ │ │ ├── ssverify.py │ │ │ ├── tags.py │ │ │ └── tourneys.py │ │ ├── helpers │ │ │ ├── __init__.py │ │ │ ├── converters.py │ │ │ ├── image.py │ │ │ ├── tourney.py │ │ │ └── utils.py │ │ ├── menus.py │ │ ├── slash │ │ │ ├── __init__.py │ │ │ └── scrims.py │ │ └── views │ │ │ ├── __init__.py │ │ │ ├── base.py │ │ │ ├── groupm │ │ │ ├── __init__.py │ │ │ ├── _paginator.py │ │ │ ├── _refresh.py │ │ │ └── main.py │ │ │ ├── idp │ │ │ └── __init__.py │ │ │ ├── paginator │ │ │ └── __init__.py │ │ │ ├── points │ │ │ ├── __init__.py │ │ │ ├── conts.py │ │ │ └── main.py │ │ │ ├── scrims │ │ │ ├── __init__.py │ │ │ ├── _ac.py │ │ │ ├── _ban.py │ │ │ ├── _base.py │ │ │ ├── _btns.py │ │ │ ├── _cdn.py │ │ │ ├── _days.py │ │ │ ├── _design.py │ │ │ ├── _edit.py │ │ │ ├── _formatter.py │ │ │ ├── _pages.py │ │ │ ├── _reserve.py │ │ │ ├── _slotlist.py │ │ │ ├── _toggle.py │ │ │ ├── _wiz.py │ │ │ ├── main.py │ │ │ └── selector.py │ │ │ ├── slotm │ │ │ ├── __init__.py │ │ │ ├── editor.py │ │ │ ├── public │ │ │ │ ├── __init__.py │ │ │ │ ├── _cancel.py │ │ │ │ ├── _claim.py │ │ │ │ ├── _idp.py │ │ │ │ └── _reminder.py │ │ │ ├── scrimsedit.py │ │ │ ├── setup.py │ │ │ └── time.py │ │ │ ├── smslotlist │ │ │ ├── __init__.py │ │ │ ├── button.py │ │ │ ├── editor.py │ │ │ └── select.py │ │ │ ├── ssmod │ │ │ ├── __init__.py │ │ │ ├── _buttons.py │ │ │ ├── _edit.py │ │ │ ├── _setup.py │ │ │ ├── _type.py │ │ │ └── _wiz.py │ │ │ ├── tagcheck │ │ │ └── __init__.py │ │ │ └── tourney │ │ │ ├── __init__.py │ │ │ ├── _base.py │ │ │ ├── _buttons.py │ │ │ ├── _editor.py │ │ │ ├── _partner.py │ │ │ ├── _select.py │ │ │ ├── _wiz.py │ │ │ ├── groups.py │ │ │ ├── main.py │ │ │ └── slotm.py │ ├── events │ │ ├── __init__.py │ │ ├── cmds.py │ │ ├── errors.py │ │ ├── interaction.py │ │ ├── main.py │ │ ├── tasks.py │ │ └── votes.py │ ├── mod │ │ ├── __init__.py │ │ ├── events │ │ │ ├── __init__.py │ │ │ ├── lockdown.py │ │ │ └── roles.py │ │ ├── utils.py │ │ └── views │ │ │ ├── __init__.py │ │ │ └── role.py │ ├── premium │ │ ├── __init__.py │ │ ├── expire.py │ │ └── views.py │ ├── quomisc │ │ ├── __init__.py │ │ ├── alerts.py │ │ ├── dev.py │ │ ├── helper.py │ │ └── views.py │ ├── reminder │ │ └── __init__.py │ └── utility │ │ ├── __init__.py │ │ ├── events │ │ ├── __init__.py │ │ ├── autopurge.py │ │ └── reminder.py │ │ ├── functions.py │ │ └── views │ │ ├── __init__.py │ │ └── embeds.py ├── constants.py ├── core │ ├── Bot.py │ ├── Cog.py │ ├── Context.py │ ├── Help.py │ ├── __init__.py │ ├── _pages.py │ ├── cache.py │ ├── cooldown.py │ ├── decorators.py │ ├── embeds.py │ └── views.py ├── data │ ├── font │ │ ├── Ubuntu-Regular.ttf │ │ ├── robo-bold.ttf │ │ └── robo-italic.ttf │ └── img │ │ ├── ptable1.jpg │ │ ├── ptable10.jpg │ │ ├── ptable11.jpg │ │ ├── ptable12.jpg │ │ ├── ptable13.jpg │ │ ├── ptable14.jpg │ │ ├── ptable15.jpg │ │ ├── ptable16.jpg │ │ ├── ptable17.jpg │ │ ├── ptable18.jpg │ │ ├── ptable19.jpg │ │ ├── ptable2.jpg │ │ ├── ptable20.jpg │ │ ├── ptable3.jpg │ │ ├── ptable4.jpg │ │ ├── ptable5.jpg │ │ ├── ptable6.jpg │ │ ├── ptable7.jpg │ │ ├── ptable8.jpg │ │ ├── ptable9.jpg │ │ ├── rect2.png │ │ ├── rect3.png │ │ └── rectangle.png ├── example_config.py ├── models │ ├── __init__.py │ ├── esports │ │ ├── __init__.py │ │ ├── ptable.py │ │ ├── reserve.py │ │ ├── scrims.py │ │ ├── slotm.py │ │ ├── ssverify.py │ │ ├── tagcheck.py │ │ └── tourney.py │ ├── helpers │ │ ├── __init__.py │ │ ├── cfields.py │ │ ├── functions.py │ │ └── validators.py │ └── misc │ │ ├── AutoPurge.py │ │ ├── Autorole.py │ │ ├── Commands.py │ │ ├── Lockdown.py │ │ ├── Snipe.py │ │ ├── Tag.py │ │ ├── Timer.py │ │ ├── User.py │ │ ├── Votes.py │ │ ├── __init__.py │ │ ├── alerts.py │ │ ├── guild.py │ │ └── premium.py ├── server │ ├── __init__.py │ ├── app │ │ ├── __init__.py │ │ └── payment.py │ └── templates │ │ ├── payu.html │ │ └── response.html ├── sockets │ ├── __init__.py │ ├── app │ │ ├── __init__.py │ │ └── app.py │ ├── events │ │ ├── __init__.py │ │ ├── dashgate.py │ │ ├── guilds.py │ │ ├── premium.py │ │ ├── scrims.py │ │ ├── settings.py │ │ └── tourney.py │ └── schemas │ │ ├── __init__.py │ │ ├── _guild.py │ │ ├── _resp.py │ │ ├── _scrim.py │ │ └── _tourney.py └── utils │ ├── __init__.py │ ├── buttons.py │ ├── checks.py │ ├── converters.py │ ├── default.py │ ├── emote.py │ ├── exceptions.py │ ├── formats.py │ ├── inputs.py │ ├── paginator.py │ ├── regex.py │ └── time.py └── tests ├── black.jpg ├── img.py ├── img2.py ├── img3.py ├── outline.ttf ├── pil_img.py ├── rect.png ├── robo-bold.ttf ├── script.py ├── slicer.py ├── slot-rect.png ├── test.http ├── test.json ├── test.py └── useless.py /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Fixes # (issue) 2 | 3 | ### Description: 4 | Please include a summary of the change and which issue is fixed. Please also include relevant context and screenshots of the change if applicable. 5 | 6 | ___ 7 | 8 | ### Checklist: 9 | - [ ] I have performed a self-review of my own code. 10 | - [ ] I have commented my code, particularly in hard-to-understand areas. 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | schedule: 21 | - cron: '0 1 * * *' 22 | 23 | jobs: 24 | analyze: 25 | if: github.event.pull_request.user.type != 'Bot' && !contains(github.event.pull_request.labels.*.name, 'skip-ci') 26 | # we hate bots 27 | name: Analyze 28 | runs-on: ubuntu-latest 29 | permissions: 30 | actions: read 31 | contents: read 32 | security-events: write 33 | 34 | strategy: 35 | fail-fast: false 36 | matrix: 37 | language: [ 'python' ] 38 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 39 | # Learn more: 40 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 41 | 42 | steps: 43 | - name: Checkout repository 44 | uses: actions/checkout@v2 45 | 46 | # Initializes the CodeQL tools for scanning. 47 | - name: Initialize CodeQL 48 | uses: github/codeql-action/init@v1 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 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 55 | 56 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 57 | # If this step fails, then you should remove it and run the build manually (see below) 58 | - name: Autobuild 59 | uses: github/codeql-action/autobuild@v1 60 | 61 | # ℹ️ Command-line programs to run using the OS shell. 62 | # 📚 https://git.io/JvXDl 63 | 64 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 65 | # and modify them (or add more) to build your code if your project 66 | # uses a compiled language 67 | 68 | #- run: | 69 | # make bootstrap 70 | # make release 71 | 72 | - name: Perform CodeQL Analysis 73 | uses: github/codeql-action/analyze@v1 74 | -------------------------------------------------------------------------------- /.github/workflows/python-app.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a single version of Python 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Python application 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | pull_request: 10 | branches: [ main ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Cache 20 | uses: actions/cache@v2.1.7 21 | with: 22 | path: ~/.cache/pip 23 | key: ${{ runner.os }}-pip-${{ hashFiles('**/flake8_requirements.txt') }} 24 | restore-keys: | 25 | ${{ runner.os }}-pip- 26 | 27 | - name: Lines of codes 28 | run: | 29 | find . -name '*.py' | xargs wc -l 30 | 31 | - name: Set up Python 3.10 32 | uses: actions/setup-python@v2 33 | with: 34 | python-version: "3.10" 35 | 36 | - name: Install dependencies 37 | run: | 38 | python -m pip install --upgrade pip 39 | pip install flake8 40 | if [ -f flake8_requirements.txt ]; then pip install -r flake8_requirements.txt; fi 41 | 42 | - name: Lint with flake8 43 | run: | 44 | # stop the build if there are Python syntax errors or undefined names 45 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide, but we are ok with 150 46 | flake8 . --count --select E9,F63,F7,F82 --ignore E203,F --show-source --exit-zero --max-complexity 35 --max-line-length 150 --statistics -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | .vscode 3 | .idea 4 | *.py[cod] 5 | env/* 6 | .venv/* 7 | config.py 8 | *.log 9 | migrations/ 10 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | up: 2 | ./env/bin/python3.8 src/bot.py 3 | 4 | prod: 5 | pm2 start ./pm2.json 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Language](https://img.shields.io/badge/lang-Python%203.8-green) 2 | ![discord.py Version](https://img.shields.io/badge/lib-discord.py%202.0-blue) 3 | ![Db](https://img.shields.io/badge/db-PostgreSQL-blue) 4 | ![Library](https://img.shields.io/badge/orm-Tortoise%20ORM-purple) 5 | 6 | Logo 7 | 8 | ## Quotient - The Ultimate Discord Bot for Esports Management 9 | 10 | 11 | Quotient is the ultimate open-source Discord bot designed specifically for esports servers. Our goal is to empower esports communities by simplifying and streamlining the organization and management of scrims, tournaments, and other events. 12 | > The source here is only for educational purposes. 13 | 14 | ## Features 15 | Quotient is a multi-functional bot that provides a comprehensive range of features, including: 16 | ``` 17 | - Automated Scrims Management 18 | - Automated Tournaments Management 19 | - Easy to use Web-Dashboard 20 | - Community engagement tools 21 | - and much much more... 22 | ``` 23 | ## Installation 24 | To install Quotient, simply add the bot to your Discord server using the following link: [`Add Quotient to your server`](https://discord.com/oauth2/authorize?client_id=746348747918934096&scope=applications.commands%20bot&permissions=536737213566).
25 | We would rather prefer you not running a direct cloned instance of Quotient. It would be a ton better to just Invite the running instance. 26 | 27 | If you decide to edit, compile or use this code in any way. Kindly respect the [`LICENSE`](LICENSE). 28 | 29 | ## How do I contribute? 30 | 31 | Contributions are Welcome:) kindly open an issue first for discussion. 32 | It's also a good option to join the [`Support Server`](https://discord.gg/aBM5xz6) 33 | 34 | ## Contact Us 35 | If you have any questions or feedback, please feel free to reach out to us on our [`Support Server`](https://discord.gg/aBM5xz6) or create an issue on this repository. Thank you for choosing Quotient! 36 | 37 | ## License 38 | This project is licensed under the MPL-2.0 license - see the [LICENSE](LICENSE) file for details. 39 | ___ 40 | ### Contributors 👥 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /flake8_requirements.txt: -------------------------------------------------------------------------------- 1 | git+https://github.com/Rapptz/discord.py.git 2 | tortoise-orm 3 | colorama 4 | asyncpg 5 | dateparser 6 | aerich 7 | parsedatetime 8 | Pillow 9 | mystbin.py 10 | prettytable 11 | psutil 12 | PyNaCl 13 | Quart 14 | requests 15 | dblpy 16 | jishaku 17 | humanize 18 | pygit2 19 | async-property 20 | colour 21 | imgkit 22 | aiocache 23 | ujson 24 | ImageHash 25 | pytesseract 26 | python-socketio 27 | aiohttp-asgi 28 | fastapi 29 | python-multipart 30 | lru-dict -------------------------------------------------------------------------------- /pm2.json: -------------------------------------------------------------------------------- 1 | { 2 | "apps": [ 3 | { 4 | "name": "quotient", 5 | "autorestart": true, 6 | "watch": false, 7 | "ignore_watch": ["logs"], 8 | "script": "./src/bot.py", 9 | "interpreter": ".venv/bin/python3.8", 10 | "error_file": "./logs/pm2error.log", 11 | "out_file": "./logs/pm2out.log" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /poetry.toml: -------------------------------------------------------------------------------- 1 | [virtualenvs] 2 | in-project = true 3 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "quotient-bot" 3 | version = "3.5.0" 4 | description = "" 5 | authors = ["deadaf "] 6 | 7 | [tool.poetry.dependencies] 8 | python = "^3.8" 9 | "discord.py" = {git = "https://github.com/Rapptz/discord.py.git"} 10 | tortoise-orm = "^0.17.6" 11 | colorama = "^0.4.4" 12 | asyncpg = "^0.22.0" 13 | dateparser = "^1.0.0" 14 | discord-ext-menus = "^1.1" 15 | aerich = "^0.5.3" 16 | parsedatetime = "^2.6" 17 | Pillow = "^8.2.0" 18 | "mystbin.py" = "^2.1.3" 19 | prettytable = "^2.1.0" 20 | psutil = "^5.8.0" 21 | PyNaCl = "^1.4.0" 22 | Quart = "^0.15.1" 23 | requests = "^2.25.1" 24 | dblpy = "^0.4.0" 25 | jishaku = "^2.2.0" 26 | humanize = "^3.11.0" 27 | pygit2 = "^1.6.1" 28 | async-property = "^0.2.1" 29 | colour = "^0.1.5" 30 | imgkit = "^1.2.2" 31 | aiocache = {extras = ["msgpack"], version = "^0.11.1"} 32 | ujson = "^4.2.0" 33 | ImageHash = "^4.2.1" 34 | pytesseract = "^0.3.8" 35 | python-socketio = {extras = ["asyncio_client"], version = "^5.5.0"} 36 | aiohttp-asgi = "^0.4.0" 37 | fastapi = "^0.73.0" 38 | python-multipart = "^0.0.5" 39 | lru-dict = "^1.1.7" 40 | 41 | [tool.poetry.dev-dependencies] 42 | black = {version = "^22.0.0", allow-prereleases = true} 43 | 44 | [build-system] 45 | requires = ["poetry-core>=1.0.0"] 46 | build-backend = "poetry.core.masonry.api" 47 | 48 | [tool.black] 49 | line-length = 122 -------------------------------------------------------------------------------- /src/aerich.ini: -------------------------------------------------------------------------------- 1 | [aerich] 2 | tortoise_orm = config.TORTOISE 3 | location = ./migrations 4 | 5 | -------------------------------------------------------------------------------- /src/bot.py: -------------------------------------------------------------------------------- 1 | if __name__ == "__main__": 2 | from core import bot 3 | 4 | bot.run(bot.config.DISCORD_TOKEN) 5 | 6 | -------------------------------------------------------------------------------- /src/cogs/esports/events/__init__.py: -------------------------------------------------------------------------------- 1 | from .scrims import ScrimEvents 2 | from .slots import SlotManagerEvents 3 | from .ssverify import Ssverification 4 | from .tags import TagEvents 5 | from .tourneys import TourneyEvents 6 | -------------------------------------------------------------------------------- /src/cogs/esports/events/slots.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import typing 4 | 5 | if typing.TYPE_CHECKING: 6 | from core import Quotient 7 | 8 | import discord 9 | from core import Cog 10 | from models import Scrim, ScrimsSlotManager, Timer 11 | 12 | 13 | class SlotManagerEvents(Cog): 14 | def __init__(self, bot: Quotient): 15 | self.bot = bot 16 | 17 | @Cog.listener() 18 | async def on_scrim_match_timer_complete(self, timer: Timer): 19 | scrim_id = timer.kwargs["scrim_id"] 20 | 21 | scrim = await Scrim.get_or_none(pk=scrim_id) 22 | if not scrim: 23 | return 24 | 25 | if not scrim.match_time == timer.expires: 26 | return 27 | 28 | record = await ScrimsSlotManager.get_or_none(guild_id=scrim.guild_id, scrim_ids__contains=scrim.id) 29 | if record: 30 | await record.refresh_public_message() 31 | 32 | @Cog.listener() 33 | async def on_guild_channel_delete(self, channel: discord.TextChannel): 34 | record = await ScrimsSlotManager.get_or_none(main_channel_id=channel.id) 35 | if not record: 36 | return 37 | await record.full_delete() 38 | 39 | @Cog.listener() 40 | async def on_raw_message_delete(self, payload: discord.RawMessageDeleteEvent): 41 | if not payload.guild_id: 42 | return 43 | 44 | record = await ScrimsSlotManager.get_or_none(message_id=payload.message_id) 45 | if not record: 46 | return 47 | 48 | await record.full_delete() 49 | -------------------------------------------------------------------------------- /src/cogs/esports/helpers/__init__.py: -------------------------------------------------------------------------------- 1 | from .converters import * 2 | from .tourney import * 3 | from .utils import * 4 | -------------------------------------------------------------------------------- /src/cogs/esports/helpers/converters.py: -------------------------------------------------------------------------------- 1 | from discord.ext import commands 2 | from discord.ext.commands import Converter 3 | from models import * 4 | 5 | from utils import QuoMember 6 | 7 | 8 | class EasyMemberConverter(Converter): 9 | async def convert(self, ctx, argument: str): 10 | try: 11 | member = await QuoMember().convert(ctx, argument) 12 | return getattr(member, "mention") 13 | except commands.MemberNotFound: 14 | return "Invalid Member!" 15 | -------------------------------------------------------------------------------- /src/cogs/esports/helpers/tourney.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import re 4 | from contextlib import suppress 5 | from typing import Iterable, List, Optional 6 | 7 | import discord 8 | from constants import EsportsRole, RegDeny 9 | from models import TMSlot, Tourney 10 | 11 | 12 | def get_tourney_slots(slots: List[TMSlot]) -> Iterable[int]: 13 | for slot in slots: 14 | yield slot.leader_id 15 | 16 | 17 | def tourney_work_role(tourney: Tourney, _type: EsportsRole): 18 | 19 | if _type == EsportsRole.ping: 20 | role = tourney.ping_role 21 | 22 | elif _type == EsportsRole.open: 23 | role = tourney.open_role 24 | 25 | if not role: 26 | return None 27 | 28 | if role == tourney.guild.default_role: 29 | return "@everyone" 30 | 31 | return getattr(role, "mention", "role-deleted") 32 | 33 | 34 | def before_registrations(message: discord.Message, role: discord.Role) -> bool: 35 | assert message.guild is not None 36 | 37 | me = message.guild.me 38 | channel = message.channel 39 | 40 | if not role: 41 | return False 42 | 43 | if not all( 44 | ( 45 | me.guild_permissions.manage_roles, 46 | role < message.guild.me.top_role, 47 | channel.permissions_for(me).add_reactions, # type: ignore 48 | channel.permissions_for(me).use_external_emojis, # type: ignore 49 | ) 50 | ): 51 | return False 52 | return True 53 | 54 | 55 | async def check_tourney_requirements(bot, message: discord.Message, tourney: Tourney) -> bool: 56 | _bool = True 57 | 58 | if tourney.teamname_compulsion: 59 | teamname = re.search(r"team.*", message.content) 60 | if not teamname or not teamname.group().strip(): 61 | _bool = False 62 | bot.dispatch("tourney_registration_deny", message, RegDeny.noteamname, tourney) 63 | 64 | if tourney.required_mentions and not all(map(lambda m: not m.bot, message.mentions)): 65 | _bool = False 66 | bot.dispatch("tourney_registration_deny", message, RegDeny.botmention, tourney) 67 | 68 | elif not len(message.mentions) >= tourney.required_mentions: 69 | _bool = False 70 | bot.dispatch("tourney_registration_deny", message, RegDeny.nomention, tourney) 71 | 72 | elif message.author.id in tourney.banned_users: 73 | _bool = False 74 | bot.dispatch("tourney_registration_deny", message, RegDeny.banned, tourney) 75 | 76 | elif len(message.content.splitlines()) < tourney.required_lines: 77 | _bool = False 78 | bot.dispatch("tourney_registration_deny", message, RegDeny.nolines, tourney) 79 | 80 | return _bool 81 | 82 | 83 | async def t_ask_embed(ctx, value, description: str): 84 | embed = discord.Embed( 85 | color=ctx.bot.color, 86 | title=f"🛠️ Tournament Manager ({value}/5)", 87 | description=description, 88 | ) 89 | embed.set_footer(text=f'Reply with "cancel" to stop the process.') 90 | await ctx.send(embed=embed, embed_perms=True) 91 | 92 | 93 | async def update_confirmed_message(tourney: Tourney, link: str): 94 | _ids = [int(i) for i in link.split("/")[5:]] 95 | 96 | message = None 97 | 98 | with suppress(discord.HTTPException, IndexError): 99 | message = await tourney.guild.get_channel(_ids[0]).fetch_message(_ids[1]) 100 | 101 | if message: 102 | e = message.embeds[0] 103 | 104 | e.description = "~~" + e.description.strip() + "~~" 105 | e.title = "Cancelled Slot" 106 | e.color = discord.Color.red() 107 | 108 | await message.edit(embed=e) 109 | 110 | 111 | async def get_tourney_from_channel(guild_id: int, channel_id: int) -> Optional[Tourney]: 112 | tourneys = await Tourney.filter(guild_id=guild_id) 113 | 114 | for tourney in tourneys: 115 | if await tourney.media_partners.filter(pk=channel_id).exists(): 116 | return tourney 117 | 118 | return None 119 | -------------------------------------------------------------------------------- /src/cogs/esports/slash/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import typing as T 4 | 5 | if T.TYPE_CHECKING: 6 | from core import Quotient 7 | 8 | from core import Cog 9 | from .scrims import * 10 | 11 | __all__ = ("SlashCog",) 12 | 13 | 14 | class SlashCog(Cog): 15 | def __init__(self, bot: Quotient): 16 | self.bot = bot 17 | 18 | async def cog_load(self) -> None: 19 | await self.bot.add_cog(ScrimsSlash(self.bot)) 20 | -------------------------------------------------------------------------------- /src/cogs/esports/views/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import * 2 | from .groupm import * 3 | from .slotm import * 4 | from .smslotlist import * 5 | from .ssmod import * 6 | from .tourney import * 7 | from .idp import * 8 | -------------------------------------------------------------------------------- /src/cogs/esports/views/base.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import typing 4 | 5 | if typing.TYPE_CHECKING: 6 | from core import Quotient 7 | 8 | from contextlib import suppress 9 | 10 | import discord 11 | 12 | from core import Context 13 | 14 | 15 | class EsportsBaseView(discord.ui.View): 16 | message: discord.Message 17 | custom_id: str 18 | 19 | def __init__(self, ctx: Context, **kwargs): 20 | super().__init__(timeout=kwargs.get("timeout", 60)) 21 | 22 | self.ctx = ctx 23 | self.title = kwargs.get("title", "") 24 | self.bot: Quotient = ctx.bot 25 | self.check = lambda msg: msg.channel == self.ctx.channel and msg.author == self.ctx.author 26 | 27 | async def interaction_check(self, interaction: discord.Interaction) -> bool: 28 | if interaction.user.id != self.ctx.author.id: 29 | await interaction.response.send_message( 30 | "Sorry, you can't use this interaction as it is not started by you.", ephemeral=True 31 | ) 32 | return False 33 | return True 34 | 35 | async def on_timeout(self) -> None: 36 | if hasattr(self, "message"): 37 | for b in self.children: 38 | if isinstance(b, discord.ui.Button) and not b.style == discord.ButtonStyle.link: 39 | b.style, b.disabled = discord.ButtonStyle.grey, True 40 | 41 | with suppress(discord.HTTPException): 42 | await self.message.edit(view=self) 43 | 44 | async def on_error(self, interaction: discord.Interaction, error: Exception, item) -> None: 45 | print("Esports view error:", error) 46 | self.ctx.bot.dispatch("command_error", self.ctx, error) 47 | 48 | async def ask_embed(self, desc: str, *, image=None): 49 | embed = discord.Embed(color=self.bot.color, description=desc, title=self.title) 50 | if image: 51 | embed.set_image(url=image) 52 | embed.set_footer(text=f"Reply with 'cancel' to stop this process.") 53 | 54 | return await self.ctx.send(embed=embed, embed_perms=True) 55 | 56 | async def error_embed(self, desc: str, *, footer: str = None, delete_after=3): 57 | embed = discord.Embed(color=discord.Color.red(), title="Whoopsi-Doopsi", description=desc) 58 | if footer: 59 | embed.set_footer(text=footer) 60 | await self.ctx.send(embed=embed, delete_after=delete_after, embed_perms=True) 61 | 62 | def red_embed(self, description: str) -> discord.Embed: 63 | return discord.Embed(color=discord.Color.red(), title=self.title, description=description) 64 | -------------------------------------------------------------------------------- /src/cogs/esports/views/groupm/__init__.py: -------------------------------------------------------------------------------- 1 | from ._refresh import * 2 | from .main import * 3 | -------------------------------------------------------------------------------- /src/cogs/esports/views/idp/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import discord 4 | 5 | __all__ = ("IdpView",) 6 | 7 | 8 | class IdpView(discord.ui.View): 9 | def __init__(self, room_id: str, password: str, map: str): 10 | self.room_id = room_id 11 | self.password = password 12 | self.map = map 13 | super().__init__(timeout=None) 14 | 15 | @discord.ui.button(label="Get in Copy Format", style=discord.ButtonStyle.green) 16 | async def copy_format(self, interaction: discord.Interaction, button: discord.Button): 17 | await interaction.response.send_message( 18 | "ID: {}\nPassword: {}\nMap: {}".format(self.room_id, self.password, self.map), ephemeral=True 19 | ) 20 | -------------------------------------------------------------------------------- /src/cogs/esports/views/paginator/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import discord 4 | 5 | from utils import integer_input 6 | 7 | from ...views.base import Context, EsportsBaseView 8 | 9 | 10 | class NextButton(discord.ui.Button): 11 | view: "EsportsBaseView" 12 | 13 | def __init__(self): 14 | super().__init__(emoji="<:double_right:878668437193359392>") 15 | 16 | async def callback(self, interaction: discord.Interaction): 17 | await interaction.response.defer() 18 | 19 | self.view.current_page += 1 20 | await self.view.refresh_view() 21 | 22 | 23 | class PrevButton(discord.ui.Button): 24 | view: "EsportsBaseView" 25 | 26 | def __init__(self): 27 | super().__init__(emoji="<:double_left:878668594530099220>") 28 | 29 | async def callback(self, interaction: discord.Interaction): 30 | await interaction.response.defer() 31 | 32 | self.view.current_page -= 1 33 | await self.view.refresh_view() 34 | 35 | 36 | class SkipToButton(discord.ui.Button): 37 | view: "EsportsBaseView" 38 | 39 | def __init__(self, ctx: Context): 40 | super().__init__(label="Skip to page ...") 41 | self.ctx = ctx 42 | 43 | async def callback(self, interaction: discord.Interaction): 44 | await interaction.response.defer() 45 | 46 | m = await self.ctx.simple( 47 | "Please enter the page number you want to skip to. (`1` to `{}`)".format(len(self.view.records)) 48 | ) 49 | _page = await integer_input(self.ctx, timeout=30, delete_after=True, limits=(1, len(self.view.records))) 50 | await self.ctx.safe_delete(m) 51 | 52 | self.view.current_page = _page 53 | 54 | await self.view.refresh_view() 55 | 56 | 57 | class StopButton(discord.ui.Button): 58 | view: "EsportsBaseView" 59 | 60 | def __init__(self): 61 | super().__init__(emoji="⏹️") 62 | 63 | async def callback(self, interaction: discord.Interaction): 64 | 65 | await self.view.on_timeout() 66 | -------------------------------------------------------------------------------- /src/cogs/esports/views/points/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deadaf/Quotient-Bot/b5f88056889f1faef9d8c04d96684e47e3f79c3d/src/cogs/esports/views/points/__init__.py -------------------------------------------------------------------------------- /src/cogs/esports/views/points/conts.py: -------------------------------------------------------------------------------- 1 | from uuid import UUID, uuid4 2 | from pydantic import BaseModel, Field 3 | 4 | 5 | class Team(BaseModel): 6 | id: str = Field(default_factory=uuid4) 7 | name: str 8 | matches: str 9 | kills: int 10 | placepts: int 11 | totalpts: int 12 | -------------------------------------------------------------------------------- /src/cogs/esports/views/scrims/__init__.py: -------------------------------------------------------------------------------- 1 | from ._edit import * 2 | from .main import * 3 | from .selector import * 4 | -------------------------------------------------------------------------------- /src/cogs/esports/views/scrims/_base.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import discord 4 | 5 | from models import Scrim 6 | 7 | from ...views.base import EsportsBaseView 8 | 9 | __all__ = ("ScrimsView", "ScrimsButton") 10 | 11 | 12 | class ScrimsView(EsportsBaseView): 13 | record: Scrim 14 | # scrim:Scrim 15 | 16 | def __init__(self, ctx, **kwargs): 17 | super().__init__(ctx, **kwargs) 18 | 19 | async def on_error(self, interaction: discord.Interaction, error: Exception, item) -> None: 20 | print("Scrims View Error:", error) 21 | self.ctx.bot.dispatch("command_error", self.ctx, error) 22 | 23 | 24 | class ScrimsButton(discord.ui.Button): 25 | view: ScrimsView 26 | 27 | def __init__(self, **kwargs): 28 | super().__init__(**kwargs) 29 | -------------------------------------------------------------------------------- /src/cogs/esports/views/scrims/_cdn.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import typing as T 4 | 5 | import discord 6 | from pydantic import BaseModel 7 | 8 | import config 9 | from core import Context 10 | from core.embeds import EmbedBuilder 11 | from models import Scrim 12 | from utils import integer_input 13 | from utils import keycap_digit as kd 14 | 15 | from ._base import ScrimsView 16 | 17 | __all__ = ("ScrimsCDN",) 18 | 19 | 20 | class CDN(BaseModel): 21 | status: bool = False 22 | countdown: int = 5 23 | msg: dict = ... 24 | 25 | 26 | class ScrimsCDN(ScrimsView): 27 | def __init__(self, ctx: Context, scrim: Scrim): 28 | super().__init__(ctx, timeout=60.0) 29 | 30 | self.scrim = scrim 31 | 32 | @property 33 | def initial_embed(self): 34 | _e = discord.Embed( 35 | color=self.bot.color, 36 | ) 37 | _e.description = "**Registration open countdown editor -** {0}".format(self.scrim) 38 | 39 | fields = { 40 | "ON / OFF": ("`OFF`", "`ON`")[self.scrim.cdn["status"]], 41 | "Countdown": f"`{self.scrim.cdn['countdown']}s`", 42 | "Message": "`Click to view or edit`", 43 | } 44 | 45 | for idx, (name, value) in enumerate(fields.items(), 1): 46 | _e.add_field( 47 | name=f"{kd(idx)} {name}:", 48 | value=value, 49 | inline=False, 50 | ) 51 | 52 | return _e 53 | 54 | async def refresh_view(self, **kwargs): 55 | await self.scrim.make_changes(**kwargs) 56 | self.message = await self.message.edit(embed=self.initial_embed, view=self) 57 | 58 | @discord.ui.button(emoji=kd(1)) 59 | async def change_status(self, inter: discord.Interaction ,btn: discord.Button): 60 | await inter.response.defer() 61 | 62 | self.scrim.cdn["status"] = not self.scrim.cdn["status"] 63 | await self.refresh_view(cdn=self.scrim.cdn) 64 | 65 | @discord.ui.button(emoji=kd(2)) 66 | async def set_time(self, inter: discord.Interaction ,btn: discord.Button): 67 | await inter.response.defer() 68 | 69 | _m = await self.ctx.simple("How many seconds should the countdown be? (Min: `3` Max: `10`)") 70 | self.scrim.cdn["countdown"] = await integer_input(self.ctx, limits=(5, 15), delete_after=True) 71 | await self.ctx.safe_delete(_m) 72 | await self.refresh_view(cdn=self.scrim.cdn) 73 | 74 | @discord.ui.button(emoji=kd(3)) 75 | async def set_msg(self, inter: discord.Interaction ,btn: discord.Button): 76 | await inter.response.defer() 77 | await self.scrim.refresh_from_db() 78 | 79 | from ._design import BackBtn, MsgType, SaveMessageBtn, ScrimDesign, SetDefault 80 | 81 | if len(self.scrim.cdn["msg"]) <= 1: 82 | _e = ScrimDesign.default_countdown_msg() 83 | 84 | else: 85 | _e = discord.Embed.from_dict(self.scrim.cdn["msg"]) 86 | 87 | self.stop() 88 | 89 | embed = discord.Embed(color=self.bot.color, title="Click Me if you need Help", url=self.bot.config.SERVER_LINK) 90 | embed.description = ( 91 | f"\n*You are editing registration close message for {self.scrim}*\n\n" 92 | "**__Keywords you can use in design:__**\n" 93 | "`<>` - Seconds left in opening reg (counter).\n" 94 | ) 95 | await self.message.edit(embed=embed, content="", view=None) 96 | 97 | _v = EmbedBuilder( 98 | self.ctx, 99 | items=[ 100 | SaveMessageBtn(self.ctx, self.scrim, MsgType.countdown, self.message), 101 | BackBtn(self.ctx, self.scrim, self.message), 102 | SetDefault(self.ctx, self.scrim, MsgType.countdown), 103 | ], 104 | ) 105 | 106 | await _v.rendor(embed=_e) 107 | 108 | @discord.ui.button(style=discord.ButtonStyle.red, label="Back") 109 | async def go_back(self, inter: discord.Interaction ,btn: discord.Button): 110 | await inter.response.defer() 111 | 112 | from ._design import ScrimDesign 113 | 114 | self.stop() 115 | v = ScrimDesign(self.ctx, self.scrim) 116 | v.message = await self.message.edit(embed=v.initial_embed, view=v) 117 | -------------------------------------------------------------------------------- /src/cogs/esports/views/scrims/_days.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import discord 4 | 5 | from constants import Day 6 | from utils import keycap_digit 7 | 8 | __all__ = ("WeekDays",) 9 | 10 | 11 | class WeekDays(discord.ui.Select): 12 | def __init__(self, placeholder="Select the weekdays for registrations", max=7): 13 | _o = [] 14 | for idx, day in enumerate(Day, start=1): 15 | _o.append(discord.SelectOption(label=day.name.title(), value=day.name, emoji=keycap_digit(idx))) 16 | 17 | super().__init__(placeholder=placeholder, max_values=max, options=_o) 18 | 19 | async def callback(self, interaction: discord.Interaction): 20 | await interaction.response.defer() 21 | self.view.stop() 22 | 23 | self.view.custom_id = [Day(_) for _ in self.values] 24 | -------------------------------------------------------------------------------- /src/cogs/esports/views/scrims/_formatter.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import discord 4 | 5 | from core import Context 6 | from core.embeds import EmbedBuilder 7 | from models import Scrim 8 | 9 | DEFAULT_MSG = Scrim.default_slotlist_format() 10 | 11 | __all__ = ("show_slotlist_formatter",) 12 | 13 | 14 | async def show_slotlist_formatter(ctx: Context, scrim: Scrim, view_msg: discord.Message): 15 | await scrim.refresh_from_db() 16 | 17 | embed = discord.Embed(color=ctx.bot.color, title="Click me to Get Help", url=ctx.config.SERVER_LINK) 18 | embed.description = ( 19 | f"\n*You are editing slotlist design for {scrim}*\n\n" 20 | "**__Keywords you can use in design:__**\n" 21 | "`<>` - Slot number and team names (**Most Important**)\n" 22 | "`<>` - Name of the scrim\n" 23 | "`<>` - Next day's registration open time.\n" 24 | "`<>` - Time taken in registration.\n" 25 | ) 26 | 27 | await view_msg.edit(embed=embed, content="", view=None) 28 | 29 | if len(scrim.slotlist_format) <= 1: 30 | embed = DEFAULT_MSG 31 | else: 32 | embed = discord.Embed.from_dict(scrim.slotlist_format) 33 | 34 | _v = EmbedBuilder( 35 | ctx, 36 | items=[ 37 | SaveBtn(ctx, scrim, view_msg), 38 | BackBtn(ctx, scrim, view_msg), 39 | SetDefault(ctx, scrim), 40 | ], 41 | ) 42 | 43 | await _v.rendor(embed=embed) 44 | 45 | 46 | class SaveBtn(discord.ui.Button): 47 | view: EmbedBuilder 48 | 49 | def __init__(self, ctx: Context, scrim: Scrim, msg: discord.Message = None): 50 | super().__init__(style=discord.ButtonStyle.green, label="Save this design") 51 | 52 | self.ctx = ctx 53 | self.scrim = scrim 54 | self.msg = msg 55 | 56 | async def callback(self, interaction: discord.Interaction): 57 | await interaction.response.defer() 58 | 59 | await self.ctx.simple(f"Saving Changes...", 2) 60 | 61 | await self.scrim.make_changes(slotlist_format=self.view.formatted) 62 | await self.scrim.confirm_all_scrims(self.ctx, slotlist_format=self.view.formatted) 63 | 64 | await self.ctx.success(f"Saved your new design!", 2) 65 | self.view.stop() 66 | 67 | if self.msg: 68 | await self.ctx.safe_delete(self.msg) 69 | 70 | from .main import ScrimsMain 71 | 72 | v = ScrimsMain(self.ctx) 73 | v.message = await self.view.message.edit(content="", embed=await v.initial_embed(), view=v) 74 | 75 | 76 | class BackBtn(discord.ui.Button): 77 | view: EmbedBuilder 78 | 79 | def __init__(self, ctx: Context, scrim: Scrim, msg: discord.Message = None): 80 | super().__init__(style=discord.ButtonStyle.red, label="Exit") 81 | self.ctx = ctx 82 | self.scrim = scrim 83 | 84 | self.msg = msg 85 | 86 | async def callback(self, interaction: discord.Interaction): 87 | await interaction.response.defer() 88 | prompt = await self.ctx.prompt("All unsaved changes will be lost forever. Do you still want to continue?") 89 | if not prompt: 90 | return await self.ctx.simple("OK. Not Exiting.", 4) 91 | 92 | self.view.stop() 93 | 94 | if self.msg: 95 | await self.ctx.safe_delete(self.msg) 96 | 97 | from .main import ScrimsMain 98 | 99 | v = ScrimsMain(self.ctx) 100 | v.message = await self.view.message.edit(content="", embed=await v.initial_embed(), view=v) 101 | 102 | 103 | class SetDefault(discord.ui.Button): 104 | view: EmbedBuilder 105 | 106 | def __init__(self, ctx: Context, scrim: Scrim): 107 | super().__init__(style=discord.ButtonStyle.blurple, label="Reset to default") 108 | 109 | self.scrim = scrim 110 | self.ctx = ctx 111 | 112 | async def callback(self, interaction: discord.Interaction): 113 | await interaction.response.defer() 114 | prompt = await self.ctx.prompt("All changes will be lost. Do you still want to continue?") 115 | if not prompt: 116 | return await self.ctx.simple("OK, not reseting.", 3) 117 | 118 | self.view.embed = DEFAULT_MSG 119 | 120 | self.view.content = "" 121 | await self.view.refresh_view() 122 | await self.ctx.success("Slotlist design set to default. Click `Save` to save this design.", 4) 123 | -------------------------------------------------------------------------------- /src/cogs/esports/views/scrims/_pages.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import discord 4 | 5 | from core import Context 6 | from models import Scrim 7 | 8 | from ._base import ScrimsButton 9 | 10 | __all__ = "Next", "Prev", "SkipTo" 11 | 12 | 13 | class Next(ScrimsButton): 14 | def __init__(self, ctx: Context, row: int = None): 15 | super().__init__(emoji="<:double_right:878668437193359392>", row=row) 16 | self.ctx = ctx 17 | 18 | async def callback(self, interaction: discord.Interaction): 19 | await interaction.response.defer() 20 | 21 | _ids = [_.pk async for _ in Scrim.filter(guild_id=self.ctx.guild.id).order_by("open_time")] 22 | current = _ids.index(self.view.record.pk) 23 | 24 | try: 25 | next_id = _ids[current + 1] 26 | except IndexError: 27 | next_id = _ids[0] 28 | 29 | new_scrim = await Scrim.get(pk=next_id) 30 | if not self.view.record == new_scrim: 31 | self.view.record = new_scrim 32 | await self.view.refresh_view() 33 | 34 | 35 | class Prev(ScrimsButton): 36 | def __init__(self, ctx: Context, row: int = None): 37 | super().__init__(emoji="<:double_left:878668594530099220>", row=row) 38 | self.ctx = ctx 39 | 40 | async def callback(self, interaction: discord.Interaction): 41 | await interaction.response.defer() 42 | 43 | _ids = [_.pk async for _ in Scrim.filter(guild_id=self.ctx.guild.id).order_by("open_time")] 44 | current = _ids.index(self.view.record.pk) 45 | 46 | try: 47 | next_id = _ids[current - 1] 48 | except IndexError: 49 | next_id = _ids[-1] 50 | 51 | new_scrim = await Scrim.get(pk=next_id) 52 | if not self.view.record == new_scrim: 53 | self.view.record = new_scrim 54 | await self.view.refresh_view() 55 | 56 | 57 | class SkipTo(ScrimsButton): 58 | def __init__(self, ctx: Context, row: int = None): 59 | super().__init__(label="Skip to...", row=row) 60 | self.ctx = ctx 61 | 62 | async def callback(self, interaction: discord.Interaction): 63 | await interaction.response.defer() 64 | -------------------------------------------------------------------------------- /src/cogs/esports/views/scrims/_slotlist.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from contextlib import suppress 4 | 5 | import discord 6 | 7 | from core import Context 8 | from models import Scrim 9 | from utils import emote, inputs 10 | 11 | from ._base import ScrimsView 12 | from ._formatter import show_slotlist_formatter 13 | 14 | __all__ = ("ManageSlotlist",) 15 | 16 | 17 | class ManageSlotlist(discord.ui.Select): 18 | view: ScrimsView 19 | 20 | def __init__(self, ctx: Context, scrim: Scrim): 21 | super().__init__( 22 | placeholder="Select an option to manage slotlist.", 23 | options=[ 24 | discord.SelectOption( 25 | label="Repost Slotlist", 26 | emoji="🔁", 27 | description="Respost slotlist to a channel", 28 | value="repost", 29 | ), 30 | discord.SelectOption( 31 | label="Change Design", 32 | description="Design slotlist of any scrim.", 33 | emoji="⚙️", 34 | value="format", 35 | ), 36 | discord.SelectOption( 37 | label="Edit Slotlist", 38 | description="Edit slotlist (Remove/Add New teams).", 39 | emoji=emote.edit, 40 | value="edit", 41 | ), 42 | discord.SelectOption( 43 | label="Go Back", 44 | description="Move back to Main Menu", 45 | emoji=emote.exit, 46 | value="back", 47 | ), 48 | ], 49 | ) 50 | 51 | self.ctx = ctx 52 | self.record = scrim 53 | 54 | async def callback(self, interaction: discord.Interaction): 55 | await interaction.response.defer() 56 | 57 | if (selected := self.values[0]) == "repost": 58 | m = await self.ctx.simple("Mention the channel to send slotlist.") 59 | channel = await inputs.channel_input(self.ctx, delete_after=True) 60 | await self.ctx.safe_delete(m) 61 | 62 | if not await self.record.teams_registered.count(): 63 | return await self.ctx.error("No registrations found in {0}.".format(self.record), 5) 64 | 65 | m = await self.record.send_slotlist(channel) 66 | await self.ctx.success("Slotlist sent! [Click to Jump]({0})".format(m.jump_url), 5) 67 | 68 | from .main import ScrimsMain 69 | 70 | self.view.stop() 71 | v = ScrimsMain(self.ctx) 72 | v.message = await self.view.message.edit(content="", embed=await v.initial_embed(), view=v) 73 | 74 | elif selected == "format": 75 | self.view.stop() 76 | await show_slotlist_formatter(self.ctx, self.record, self.view.message) 77 | 78 | elif selected == "edit": 79 | if self.record.slotlist_message_id == None: 80 | return await self.ctx.error("Slotlist not found. Please repost.", 5) 81 | 82 | msg = None 83 | with suppress(discord.HTTPException): 84 | msg = await self.ctx.bot.get_or_fetch_message( 85 | self.record.slotlist_channel, self.record.slotlist_message_id 86 | ) 87 | if not msg: 88 | return await self.ctx.error("Slotlist Message not found. Repost first.", 5) 89 | 90 | return await self.ctx.success("Click `Edit` button under [slotlist message]({0}).".format(msg.jump_url), 6) 91 | 92 | elif selected == "back": 93 | from .main import ScrimsMain 94 | 95 | self.view.stop() 96 | v = ScrimsMain(self.ctx) 97 | v.message = await self.view.message.edit(content="", embed=await v.initial_embed(), view=v) 98 | -------------------------------------------------------------------------------- /src/cogs/esports/views/scrims/_toggle.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import discord 4 | 5 | from core import Context 6 | from models import Scrim 7 | 8 | from ._base import ScrimsButton, ScrimsView 9 | from ._btns import Discard 10 | from ._pages import * 11 | 12 | __all__ = ("ScrimsToggle",) 13 | 14 | 15 | class ScrimsToggle(ScrimsView): 16 | def __init__(self, ctx: Context, scrim: Scrim): 17 | self.ctx = ctx 18 | self.record = scrim 19 | 20 | super().__init__(ctx) 21 | 22 | @property 23 | async def initial_message(self): 24 | _e = discord.Embed(color=self.bot.color) 25 | _e.description = "**Start / Stop scrim registration of {}**".format(self.record) 26 | _e.set_author(name=f"Page - {' / '.join(await self.record.scrim_posi())}", icon_url=self.bot.user.avatar.url) 27 | return _e 28 | 29 | async def refresh_view(self): 30 | await self._add_buttons() 31 | try: 32 | self.message = await self.message.edit(embed=await self.initial_message, view=self) 33 | except discord.HTTPException: 34 | await self.on_timeout() 35 | 36 | async def _add_buttons(self): 37 | self.clear_items() 38 | 39 | if await Scrim.filter(guild_id=self.ctx.guild.id).count() >= 2: 40 | self.add_item(Prev(self.ctx)) 41 | self.add_item(SkipTo(self.ctx)) 42 | self.add_item(Next(self.ctx)) 43 | 44 | self.add_item(StartReg()) 45 | self.add_item(StopReg()) 46 | self.add_item(Discard(self.ctx, "Main Menu", 2)) 47 | 48 | 49 | class StartReg(ScrimsButton): 50 | def __init__(self): 51 | super().__init__(label="Start Reg", style=discord.ButtonStyle.green, row=2) 52 | 53 | async def callback(self, interaction: discord.Interaction): 54 | await interaction.response.defer() 55 | 56 | if not self.view.record.closed_at: 57 | return await self.view.ctx.error("Registration is already open. To restart, pls stop registration first.", 4) 58 | 59 | try: 60 | await self.view.record.start_registration() 61 | await self.view.ctx.success(f"Registration opened {self.view.record}.", 5) 62 | except Exception as e: 63 | return await self.view.ctx.error(e, 10) 64 | 65 | 66 | class StopReg(ScrimsButton): 67 | def __init__(self): 68 | super().__init__(label="Stop Reg", style=discord.ButtonStyle.red, row=2) 69 | 70 | async def callback(self, interaction: discord.Interaction): 71 | await interaction.response.defer() 72 | 73 | if not self.view.record.opened_at: 74 | return await self.view.ctx.error("Registration is already closed.", 5) 75 | 76 | await self.view.record.close_registration() 77 | await self.view.ctx.success(f"Registration closed {self.view.record}.", 5) 78 | -------------------------------------------------------------------------------- /src/cogs/esports/views/scrims/_wiz.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import typing as T 4 | from string import ascii_uppercase 5 | 6 | import discord 7 | 8 | from core import Context 9 | from models import Scrim 10 | from utils import discord_timestamp as dt 11 | 12 | from ._base import ScrimsView 13 | from ._btns import * 14 | 15 | __all__ = ("ScrimSetup",) 16 | 17 | 18 | class ScrimSetup(ScrimsView): 19 | def __init__(self, ctx: Context): 20 | super().__init__(ctx, timeout=60) 21 | 22 | self.ctx = ctx 23 | self.record: Scrim = None 24 | 25 | self.add_item(RegChannel(ctx, "a")) 26 | self.add_item(SlotChannel(ctx, "b")) 27 | self.add_item(SetRole(ctx, "c")) 28 | self.add_item(SetMentions(ctx, "d")) 29 | self.add_item(TotalSlots(ctx, "e")) 30 | self.add_item(OpenTime(ctx, "f")) 31 | 32 | self.add_item(OpenDays(ctx, "g")) 33 | self.add_item(SetEmojis(ctx, "h")) 34 | self.add_item(Discard(ctx, "Cancel")) 35 | self.add_item(SaveScrim(ctx)) 36 | 37 | def initial_message(self): 38 | if not self.record: 39 | self.record = Scrim(guild_id=self.ctx.guild.id, host_id=self.ctx.author.id) 40 | 41 | d_link = "https://quotientbot.xyz/dashboard/{0}/scrims/create".format(self.ctx.guild.id) 42 | 43 | _e = discord.Embed(color=0x00FFB3, title="Enter details & Press Save", url=self.bot.config.SERVER_LINK) 44 | _e.description = f"[`Scrim Creation is a piece of cake through dashboard, Click Me`]({d_link})\n\n" 45 | 46 | fields = { 47 | "Reg. Channel": getattr(self.record.registration_channel, "mention", "`Not-Set`"), 48 | "Slotlist Channel": getattr(self.record.slotlist_channel, "mention", "`Not-Set`"), 49 | "Success Role": getattr(self.record.role, "mention", "`Not-Set`"), 50 | "Req. Mentions": f"`{self.record.required_mentions}`", 51 | "Total Slots": f"`{self.record.total_slots or 'Not-Set'}`", 52 | "Open Time": f"{dt(self.record.open_time,'t')} ({dt(self.record.open_time)})" 53 | if self.record.open_time 54 | else "`Not-Set`", 55 | "Scrim Days": ", ".join(map(lambda x: "`{0}`".format(x.name.title()[:2]), self.record.open_days)), 56 | f"Reactions {self.bot.config.PRIME_EMOJI}": f"{self.record.check_emoji},{self.record.cross_emoji}", 57 | } 58 | 59 | for idx, (name, value) in enumerate(fields.items()): 60 | _e.add_field( 61 | name=f"{ri(ascii_uppercase[idx])} {name}:", 62 | value=value, 63 | ) 64 | _e.add_field(name="\u200b", value="\u200b") 65 | _e.set_footer( 66 | text="Quotient Premium servers can set custom reactions.", icon_url=self.ctx.guild.me.display_avatar.url 67 | ) 68 | 69 | return _e 70 | 71 | async def refresh_view(self): 72 | _e = self.initial_message() 73 | 74 | if all( 75 | ( 76 | self.record.registration_channel_id, 77 | self.record.slotlist_channel_id, 78 | self.record.role_id, 79 | self.record.total_slots, 80 | self.record.open_time, 81 | ) 82 | ): 83 | self.children[-1].disabled = False 84 | 85 | try: 86 | self.message = await self.message.edit(embed=_e, view=self) 87 | except discord.HTTPException: 88 | await self.on_timeout() 89 | -------------------------------------------------------------------------------- /src/cogs/esports/views/slotm/__init__.py: -------------------------------------------------------------------------------- 1 | from .editor import * 2 | from .public import * 3 | from .setup import * 4 | -------------------------------------------------------------------------------- /src/cogs/esports/views/slotm/public/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import typing as T 4 | 5 | import discord as d 6 | from models import Scrim, ScrimsSlotManager 7 | 8 | __all__ = ("ScrimsSlotmPublicView",) 9 | 10 | 11 | class ScrimsSlotmPublicView(d.ui.View): 12 | children: T.List[d.ui.Button] 13 | 14 | def __init__(self, record: ScrimsSlotManager): 15 | super().__init__(timeout=None) 16 | 17 | self.record = record 18 | self.bot = record.bot 19 | self.claimable: T.List[Scrim] = [] 20 | 21 | from ._cancel import ScrimsCancel 22 | from ._claim import ScrimsClaim 23 | from ._idp import IdpTransfer 24 | from ._reminder import ScrimsRemind 25 | 26 | self.add_item(ScrimsCancel(style=d.ButtonStyle.danger, custom_id="scrims_slot_cancel", label="Cancel Slot")) 27 | self.add_item(ScrimsClaim(style=d.ButtonStyle.green, custom_id="scrims_slot_claim", label="Claim Slot")) 28 | self.add_item(ScrimsRemind(label="Remind Me", custom_id="scrims_slot_reminder", emoji="🔔")) 29 | self.add_item( 30 | IdpTransfer(label="Transfer IDP Role", custom_id="scrims_transfer_idp_role", style=d.ButtonStyle.green) 31 | ) 32 | 33 | self.bot.loop.create_task(self.__refresh_cache()) 34 | 35 | async def on_error(self, interaction: d.Interaction, error: Exception, item: d.ui.Item[T.Any]) -> None: 36 | if isinstance(error, d.NotFound): 37 | return 38 | print("Scrims Slotm Public View Error:", error) 39 | 40 | async def __refresh_cache(self): 41 | async for scrim in self.record.claimable_slots: 42 | self.claimable.append(scrim) 43 | -------------------------------------------------------------------------------- /src/cogs/esports/views/slotm/public/_cancel.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import typing as T 4 | from contextlib import suppress 5 | 6 | import discord 7 | from models import ArrayAppend, AssignedSlot, Scrim 8 | from utils import BaseSelector, Prompt, plural, emote 9 | 10 | from ..public import ScrimsSlotmPublicView 11 | 12 | __all__ = ("ScrimsCancel",) 13 | 14 | 15 | class ScrimsCancel(discord.ui.Button): 16 | view: ScrimsSlotmPublicView 17 | 18 | def __init__(self, **kwargs): 19 | super().__init__(**kwargs) 20 | 21 | async def callback(self, interaction: discord.Interaction) -> T.Any: 22 | await interaction.response.defer(thinking=True, ephemeral=True) 23 | 24 | if not (slots := await self.view.record.user_slots(interaction.user.id)): 25 | return await interaction.followup.send("You have no slots that can be cancelled.", ephemeral=True) 26 | 27 | cancel_view = BaseSelector(interaction.user.id, CancelSlotSelector, bot=self.view.bot, records=slots) 28 | await interaction.followup.send("Select the slots you want to remove:", view=cancel_view, ephemeral=True) 29 | await cancel_view.wait() 30 | if not cancel_view.custom_id: 31 | return 32 | 33 | p_str = f"{plural(cancel_view.custom_id):slot|slots}" 34 | prompt = Prompt(interaction.user.id) 35 | await interaction.followup.send( 36 | f"Your `{p_str}` will be cancelled.\n" "*`Are you sure you want to continue?`*", 37 | view=prompt, 38 | ephemeral=True, 39 | ) 40 | await prompt.wait() 41 | if not prompt.value: 42 | return await interaction.followup.send("Alright, Aborting.", ephemeral=True) 43 | 44 | m = await interaction.followup.send(f"Cancelling your `{p_str}`... {emote.loading}", ephemeral=True) 45 | for _ in cancel_view.custom_id: 46 | scrim_id, slot_id = _.split(":") 47 | 48 | scrim = await Scrim.get_or_none(pk=scrim_id) 49 | if not scrim: 50 | continue 51 | 52 | if not await scrim.assigned_slots.filter(user_id=interaction.user.id, pk__not=slot_id).exists(): 53 | with suppress(discord.HTTPException): 54 | if interaction.user._roles.has(scrim.role_id): 55 | await interaction.user.remove_roles(discord.Object(id=scrim.role_id)) 56 | 57 | _slot = await AssignedSlot.filter(pk=slot_id).first() 58 | 59 | await AssignedSlot.filter(pk=slot_id).update(team_name="Cancelled Slot") 60 | await scrim.refresh_slotlist_message() 61 | await _slot.delete() 62 | 63 | await Scrim.filter(pk=scrim_id).update(available_slots=ArrayAppend("available_slots", _slot.num)) 64 | 65 | link = f"https://discord.com/channels/{scrim.guild_id}/{interaction.channel_id}/{self.view.record.message_id}" 66 | await scrim.dispatch_reminders(interaction.channel, link) 67 | with suppress(discord.HTTPException, AttributeError): 68 | user = interaction.user 69 | await scrim.logschan.send( 70 | embed=discord.Embed( 71 | title="Slot-Cancelled", 72 | color=self.view.bot.color, 73 | description=f"{user} ({user.mention}) cancelled their `Slot {_slot.num}` from {scrim}.", 74 | ) 75 | ) 76 | 77 | await m.edit(content=f"Alright, Cancelled your `{p_str}`.") 78 | await self.view.record.refresh_public_message() 79 | 80 | 81 | class CancelSlotSelector(discord.ui.Select): 82 | view: BaseSelector 83 | 84 | def __init__(self, bot, records): 85 | _options = [] 86 | for record in records[:25]: 87 | reg_channel = bot.get_channel(record["registration_channel_id"]) 88 | _options.append( 89 | discord.SelectOption( 90 | label=f"Slot {record['num']} ─ #{getattr(reg_channel,'name','deleted-channel')}", 91 | description=f"{record['team_name']} (ID: {record['scrim_id']})", 92 | value=f"{record['scrim_id']}:{record['assigned_slot_id']}", 93 | emoji="📇", 94 | ) 95 | ) 96 | 97 | super().__init__(placeholder="Select slot(s) from this dropdown...", options=_options, max_values=len(records)) 98 | 99 | async def callback(self, interaction: discord.Interaction) -> T.Any: 100 | await interaction.response.defer() 101 | self.view.stop() 102 | self.view.custom_id = interaction.data["values"] 103 | -------------------------------------------------------------------------------- /src/cogs/esports/views/slotm/scrimsedit.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import discord 4 | 5 | from cogs.esports.views.scrims import ScrimSelectorView 6 | from core import Context 7 | from models import Scrim, ScrimsSlotManager 8 | from utils import emote 9 | 10 | from ...views.base import EsportsBaseView 11 | 12 | __all__ = ("SlotmScrimsEditor",) 13 | 14 | 15 | class SlotmScrimsEditor(EsportsBaseView): 16 | def __init__(self, ctx: Context, record: ScrimsSlotManager): 17 | super().__init__(ctx, timeout=100, title="Slot-M Editor") 18 | 19 | self.ctx = ctx 20 | self.record = record 21 | 22 | def initial_embed(self) -> discord.Embed: 23 | _e = discord.Embed(color=0x00FFB3) 24 | _e.description = ( 25 | "Do you want to add scrims of remove scrims from this slot-m?\n\n" 26 | f"Current scrims: {', '.join(f'`{str(_)}`' for _ in self.record.scrim_ids)}" 27 | ) 28 | 29 | return _e 30 | 31 | @discord.ui.button(custom_id="slotm_scrims_add", emoji=emote.add) 32 | async def add_new_scrims(self, interaction: discord.Interaction, button: discord.Button): 33 | await interaction.response.defer() 34 | await self.record.refresh_from_db() 35 | 36 | scrims = await self.record.available_scrims(self.ctx.guild) 37 | if not scrims: 38 | return await self.ctx.error("All scrims are already added to this or another slot-m.", 3) 39 | 40 | scrims = scrims[:25] 41 | _view = ScrimSelectorView(interaction.user, scrims, placeholder="Select scrims to add to this slot-manager ...") 42 | await interaction.followup.send("Choose the scrims you want to add to this slotm.", view=_view, ephemeral=True) 43 | 44 | await _view.wait() 45 | if _view.custom_id: 46 | _q = "UPDATE slot_manager SET scrim_ids = scrim_ids || $1 WHERE id = $2" 47 | await self.ctx.bot.db.execute(_q, [int(i) for i in _view.custom_id], self.record.id) 48 | await self.record.refresh_public_message() 49 | await self.ctx.success("Successfully added new scrims.", 3) 50 | 51 | @discord.ui.button(custom_id="slotm_scrims_remove", emoji=emote.remove) 52 | async def remove_scrims(self, interaction: discord.Interaction, button: discord.Button): 53 | await interaction.response.defer() 54 | 55 | await self.record.refresh_from_db() 56 | if not self.record.scrim_ids: 57 | return await self.ctx.error("There are no scrims added to this slot-m.", 3) 58 | 59 | scrims = await Scrim.filter(pk__in=self.record.scrim_ids).limit(25) 60 | _view = ScrimSelectorView( 61 | interaction.user, scrims, placeholder="Select scrims to remove from this slot-manager ..." 62 | ) 63 | 64 | await interaction.followup.send( 65 | "Choose the scrims you want to remove from this slotm.", view=_view, ephemeral=True 66 | ) 67 | await _view.wait() 68 | if _view.custom_id: 69 | _q = "UPDATE slot_manager SET scrim_ids = $1 WHERE id = $2" 70 | await self.ctx.bot.db.execute( 71 | _q, [_ for _ in self.record.scrim_ids if not str(_) in _view.custom_id], self.record.id 72 | ) 73 | await self.record.refresh_public_message() 74 | await self.ctx.success("Successfully removed selected scrims.", 3) 75 | -------------------------------------------------------------------------------- /src/cogs/esports/views/slotm/time.py: -------------------------------------------------------------------------------- 1 | from contextlib import suppress 2 | from datetime import timedelta 3 | 4 | import dateparser 5 | import discord 6 | from discord.ext.commands import ChannelNotFound, TextChannelConverter 7 | 8 | from core import Context 9 | from models import Scrim 10 | from utils import emote, string_input 11 | 12 | __all__ = ("MatchTimeEditor",) 13 | 14 | 15 | class MatchTimeEditor(discord.ui.Button): 16 | def __init__(self, ctx: Context): 17 | self.ctx = ctx 18 | 19 | super().__init__(label="Set Match Time", style=discord.ButtonStyle.green) 20 | 21 | async def callback(self, interaction: discord.Interaction): 22 | await interaction.response.defer() 23 | 24 | _e = discord.Embed(color=0x00FFB3) 25 | _e.description = ( 26 | "Please enter the time of matches/scrims in the following format:\n" 27 | "`#registration_channel match_time`\n\n" 28 | "Note that slotmanager will automatically lock for the scrim at specified time. This means " 29 | "that `users will not be able to cancel/claim after the specified time.`\n\n" 30 | "**Separate multiple match time with a new line.**" 31 | ) 32 | _e.set_image(url="https://cdn.discordapp.com/attachments/851846932593770496/931035634464849970/unknown.png") 33 | _e.set_footer(text="You only have to enter match time once, I'll handle the rest automatically.") 34 | await interaction.followup.send(embed=_e, ephemeral=True) 35 | match_times = await string_input( 36 | self.ctx, 37 | lambda x: x.author == interaction.user and x.channel == self.ctx.channel, 38 | delete_after=True, 39 | timeout=300, 40 | ) 41 | 42 | match_times = match_times.strip().split("\n") 43 | for _ in match_times: 44 | with suppress(AttributeError, ValueError, ChannelNotFound, TypeError): 45 | channel, time = _.strip().split() 46 | if not all((channel, time)): 47 | continue 48 | 49 | _c = await TextChannelConverter().convert(self.ctx, channel.strip()) 50 | parsed = dateparser.parse( 51 | time, 52 | settings={ 53 | "RELATIVE_BASE": self.ctx.bot.current_time, 54 | "TIMEZONE": "Asia/Kolkata", 55 | "RETURN_AS_TIMEZONE_AWARE": True, 56 | }, 57 | ) 58 | 59 | parsed = parsed + timedelta(hours=24) if parsed < self.ctx.bot.current_time else parsed 60 | 61 | if not all((_c, parsed, parsed > self.ctx.bot.current_time)): 62 | continue 63 | 64 | scrim = await Scrim.get_or_none(guild_id=self.ctx.guild.id, registration_channel_id=_c.id) 65 | if scrim: 66 | await self.ctx.bot.reminders.create_timer(parsed, "scrim_match", scrim_id=scrim.id) 67 | await Scrim.filter(pk=scrim.pk).update(match_time=parsed) 68 | 69 | await interaction.followup.send(f"{emote.check} Done, click Match-Time button to see changes.", ephemeral=True) 70 | -------------------------------------------------------------------------------- /src/cogs/esports/views/smslotlist/__init__.py: -------------------------------------------------------------------------------- 1 | from .button import * 2 | from .editor import * 3 | from .select import * 4 | -------------------------------------------------------------------------------- /src/cogs/esports/views/smslotlist/select.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import List 4 | 5 | import discord 6 | 7 | import config 8 | from models import AssignedSlot 9 | from utils import emote 10 | from utils import keycap_digit as kd 11 | from utils import truncate_string as ts 12 | 13 | 14 | class ScrimSlotSelector(discord.ui.Select): 15 | def __init__(self, slots: List[AssignedSlot], *, placeholder: str, multiple=False): 16 | 17 | _options = [] 18 | for slot in slots: 19 | 20 | _options.append( 21 | discord.SelectOption( 22 | label=f"Slot {slot.num}", description=ts(slot.team_name, 22), emoji=emote.TextChannel, value=slot.id 23 | ) 24 | ) 25 | 26 | super().__init__(options=_options, placeholder=placeholder, max_values=len(_options) if multiple else 1) 27 | 28 | async def callback(self, interaction: discord.Interaction): 29 | await interaction.response.defer() 30 | self.view.stop() 31 | self.view.custom_id = interaction.data["values"][0] if not self.max_values > 1 else interaction.data["values"] 32 | 33 | 34 | async def prompt_slot_selection(slots: List[AssignedSlot], placeholder: str, multiple: bool = False): 35 | first, rest = slots[:25], slots[25:] 36 | 37 | _view = discord.ui.View(timeout=60) 38 | _view.custom_id = None 39 | 40 | _view.add_item(ScrimSlotSelector(first, placeholder=placeholder, multiple=multiple)) 41 | 42 | if rest: 43 | _view.add_item(ScrimSlotSelector(rest, placeholder=placeholder, multiple=multiple)) 44 | 45 | return _view 46 | 47 | 48 | class BanOptions(discord.ui.View): 49 | def __init__(self): 50 | super().__init__(timeout=60) 51 | self.value: str = None 52 | 53 | def initial_embed(self): 54 | _e = discord.Embed(color=0x00FFB3, title="Ban karne ka style choose karo :)", url=config.SERVER_LINK) 55 | _e.description = ( 56 | f"{kd(1)} - Ban Team leader from this scrim.\n\n" 57 | f"{kd(2)} - Ban whole team from this scrim.\n\n" 58 | f"{kd(3)} - Ban Team leader from all scrims.\n\n" 59 | f"{kd(4)} - Ban whole team from all scrims." 60 | ) 61 | return _e 62 | 63 | @discord.ui.button(emoji=kd(1)) 64 | async def on_one(self, interaction: discord.Interaction, button: discord.Button): 65 | await interaction.response.defer() 66 | self.value = "1" 67 | self.stop() 68 | 69 | @discord.ui.button(emoji=kd(2)) 70 | async def on_two(self, interaction: discord.Interaction, button: discord.Button): 71 | await interaction.response.defer() 72 | self.value = "2" 73 | self.stop() 74 | 75 | @discord.ui.button(emoji=kd(3)) 76 | async def on_three(self, interaction: discord.Interaction, button: discord.Button): 77 | await interaction.response.defer() 78 | self.value = "3" 79 | self.stop() 80 | 81 | @discord.ui.button(emoji=kd(4)) 82 | async def on_four(self, interaction: discord.Interaction, button: discord.Button): 83 | await interaction.response.defer() 84 | self.value = "4" 85 | self.stop() 86 | -------------------------------------------------------------------------------- /src/cogs/esports/views/ssmod/__init__.py: -------------------------------------------------------------------------------- 1 | from ._edit import * # noqa: F401, F403 2 | from ._setup import * # noqa: F401, F403 3 | -------------------------------------------------------------------------------- /src/cogs/esports/views/ssmod/_edit.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING, List 4 | 5 | if TYPE_CHECKING: 6 | from core import Quotient 7 | 8 | import config 9 | from core import Context 10 | from models import SSVerify 11 | 12 | from ...views.base import EsportsBaseView 13 | from ..paginator import NextButton, PrevButton, StopButton 14 | from ._buttons import * 15 | 16 | 17 | class SSmodEditor(EsportsBaseView): 18 | def __init__(self, ctx: Context, records: List[SSVerify]): 19 | super().__init__(ctx) 20 | 21 | self.ctx = ctx 22 | self.bot: Quotient = ctx.bot 23 | 24 | self.records = records 25 | 26 | self.record = self.records[0] 27 | 28 | self.current_page = 1 29 | 30 | async def refresh_view(self): 31 | 32 | self.record = self.records[self.current_page - 1] 33 | 34 | _d = dict(self.record) 35 | 36 | del _d["id"] 37 | del _d["keywords"] 38 | 39 | await SSVerify.filter(pk=self.record.pk).update(**_d) 40 | 41 | _e = await self.initial_embed(self.record) 42 | 43 | await self._add_buttons(self.ctx) 44 | 45 | try: 46 | self.message = await self.message.edit(embed=_e, view=self) 47 | except discord.HTTPException: 48 | await self.on_timeout() 49 | 50 | async def initial_embed(self, record: SSVerify): 51 | _index = self.records.index(record) 52 | await record.refresh_from_db() 53 | self.records[_index] = record 54 | 55 | _e = discord.Embed(color=0x00FFB3, title=f"Screenshots Manager - Edit Config", url=config.SERVER_LINK) 56 | 57 | fields = { 58 | "Channel": getattr(record.channel, "mention", "`deleted-channel`"), 59 | "Role": getattr(record.role, "mention", "`deleted-role`"), 60 | "Required ss": f"`{record.required_ss}`", 61 | "Screenshot Type": f"`{record.ss_type.value.title()}`", 62 | "Page Name": f"`{record.channel_name}`", 63 | "Page URL": f"[Click Here]({record.channel_link})", 64 | "Allow Same SS": "`Yes`" if record.allow_same else "`No`", 65 | f"Success Message {config.PRIME_EMOJI}": "`Click to view or edit`", 66 | } 67 | 68 | for _idx, (name, value) in enumerate(fields.items(), start=1): 69 | _e.add_field( 70 | name=f"{kd(_idx)} {name}:", 71 | value=value, 72 | ) 73 | _e.add_field(name="\u200b", value="\u200b") 74 | _e.set_footer(text=f"Page {self.current_page}/{len(self.records)}") 75 | 76 | return _e 77 | 78 | async def _add_buttons(self, ctx): 79 | self.clear_items() 80 | 81 | cur_page = self.current_page - 1 82 | 83 | if cur_page > 0: 84 | self.add_item(PrevButton()) 85 | 86 | self.add_item(StopButton()) 87 | 88 | if len(self.records) > 1 and cur_page < len(self.records) - 1: 89 | self.add_item(NextButton()) 90 | 91 | self.add_item(SetChannel(ctx)) 92 | self.add_item(SetRole(ctx)) 93 | self.add_item(RequiredSS(ctx)) 94 | self.add_item(ScreenshotType(ctx)) 95 | self.add_item(PageName(ctx)) 96 | self.add_item(PageLink(ctx)) 97 | self.add_item(AllowSame()) 98 | 99 | self.add_item(SuccessMessage(ctx)) 100 | self.add_item(DeleteButton(ctx, self.record)) 101 | 102 | if not await self.ctx.is_premium_guild(): 103 | self.children[-2].disabled = True 104 | -------------------------------------------------------------------------------- /src/cogs/esports/views/ssmod/_setup.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING 4 | 5 | if TYPE_CHECKING: 6 | from core import Quotient 7 | 8 | import discord 9 | 10 | from core import Context 11 | from models import SSVerify 12 | from utils import emote 13 | 14 | from ...views.base import EsportsBaseView 15 | from ._edit import SSmodEditor 16 | from ._wiz import SetupWizard 17 | 18 | 19 | class SsmodMainView(EsportsBaseView): 20 | def __init__(self, ctx: Context): 21 | super().__init__(ctx, timeout=90, title="Screenshots Manager") 22 | 23 | self.ctx = ctx 24 | self.bot: Quotient = ctx.bot 25 | 26 | async def initial_message(self): 27 | records = await SSVerify.filter(guild_id=self.ctx.guild.id).order_by("id") 28 | if not records: 29 | self.children[-2].disabled = True 30 | 31 | _to_show = [f"`{idx}.` {_.__str__()}" for idx, _ in enumerate(records, start=1)] 32 | 33 | _sm = "\n".join(_to_show) if _to_show else "```Click Setup button for new ssverify.```" 34 | 35 | _e = discord.Embed(color=0x00FFB3, title=f"Advanced Screenshots Manager", url=self.ctx.config.SERVER_LINK) 36 | _e.set_thumbnail(url=self.bot.user.display_avatar.url) 37 | _e.description = _sm 38 | _e.set_footer(text="When in doubt, press '?' :)", icon_url=getattr(self.ctx.author, "url", None)) 39 | return _e 40 | 41 | @discord.ui.button(label="Setup ssverify", custom_id="setup_ssverify_button", emoji=emote.add) 42 | async def setup_ssverify_button(self, interaction: discord.Interaction, button: discord.Button): 43 | await interaction.response.defer() 44 | 45 | if not await self.ctx.is_premium_guild(): 46 | if await SSVerify.filter(guild_id=self.ctx.guild.id).exists(): 47 | return await self.ctx.premium_mango("You need Quotient Premium to setup more than 1 ssverify.") 48 | 49 | view = SetupWizard(self.ctx) 50 | _e = view.initial_message() 51 | view.message = await interaction.followup.send(embed=_e, view=view) 52 | 53 | @discord.ui.button(label="Change Settings", custom_id="edit_ssmod_config", emoji="⚒️") 54 | async def edit_ssmod_config(self, interaction: discord.Interaction, button: discord.Button): 55 | await interaction.response.defer() 56 | 57 | records = await SSVerify.filter(guild_id=self.ctx.guild.id).order_by("id") 58 | _view = SSmodEditor(self.ctx, records) 59 | await _view._add_buttons(self.ctx) 60 | _view.message = await interaction.followup.send(embed=await _view.initial_embed(records[0]), view=_view) 61 | 62 | @discord.ui.button(emoji="❔", custom_id="info_ssmod_button") 63 | async def stop_ssmod_button(self, interaction: discord.Interaction, button: discord.Button): 64 | _e = discord.Embed(color=0x00FFB3, title="Screenshots Manager FAQ", url=self.ctx.config.SERVER_LINK) 65 | _e.description = ( 66 | "**How to setup Quotient ssverification?**\n" 67 | "> Click the `Setup ssverify` button to set up ssverify.\n\n" 68 | "**What is Custom Filter?**\n" 69 | "> Custom Filter allows you to set ssverification for any app or for any type of ss.\n\n" 70 | "**My question isn't listed here. What should I do?**\n" 71 | "> You can talk to us directly in the support server: {0}".format(self.ctx.config.SERVER_LINK) 72 | ) 73 | return await interaction.response.send_message(embed=_e, ephemeral=True) 74 | -------------------------------------------------------------------------------- /src/cogs/esports/views/ssmod/_type.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import discord 4 | 5 | from constants import SSType 6 | from utils import BaseSelector 7 | 8 | 9 | class SStypeSelector(discord.ui.Select): 10 | view: BaseSelector 11 | 12 | def __init__(self): 13 | super().__init__( 14 | placeholder="Select the type of screenshots ... ", 15 | options=[ 16 | discord.SelectOption( 17 | label="Youtube", 18 | emoji="<:youtube:938835185976344576>", 19 | value=SSType.yt.value, 20 | description="Youtube Channel Screenshots", 21 | ), 22 | discord.SelectOption( 23 | label="Instagram", 24 | emoji="<:instagram:938834438656249896>", 25 | value=SSType.insta.value, 26 | description="Instagram Screenshots (Premium only)", 27 | ), 28 | discord.SelectOption( 29 | label="Rooter", 30 | emoji="<:rooter:938834226483171418>", 31 | value=SSType.rooter.value, 32 | description="Rooter Screenshots (Premium only)", 33 | ), 34 | discord.SelectOption( 35 | label="Loco", 36 | emoji="<:loco:938834181088219146>", 37 | value=SSType.loco.value, 38 | description="Loco Screenshots (Premium only)", 39 | ), 40 | discord.SelectOption( 41 | label="Any SS", 42 | emoji="<:hehe:874303673981878272>", 43 | value=SSType.anyss.value, 44 | description="Verify any Image (Premium only)", 45 | ), 46 | discord.SelectOption( 47 | label="Create Custom Filter", 48 | emoji="", 49 | value=SSType.custom.value, 50 | description="For anything like app installation, any mobile app,etc.", 51 | ), 52 | ], 53 | ) 54 | 55 | async def callback(self, interaction: discord.Interaction): 56 | await interaction.response.defer() 57 | self.view.stop() 58 | self.view.custom_id = interaction.data["values"][0] 59 | -------------------------------------------------------------------------------- /src/cogs/esports/views/ssmod/_wiz.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import discord 4 | 5 | import config 6 | from core import Context 7 | from models import SSVerify 8 | from utils import keycap_digit as kd 9 | 10 | from ...views.base import EsportsBaseView 11 | from ._buttons import * 12 | 13 | __all__ = ("SetupWizard",) 14 | 15 | 16 | class SetupWizard(EsportsBaseView): 17 | def __init__(self, ctx: Context): 18 | super().__init__(ctx) 19 | 20 | self.ctx = ctx 21 | self.record = None 22 | 23 | self.add_item(SetChannel(ctx)) 24 | self.add_item(SetRole(ctx)) 25 | self.add_item(RequiredSS(ctx)) 26 | self.add_item(ScreenshotType(ctx)) 27 | self.add_item(PageName(ctx)) 28 | self.add_item(PageLink(ctx)) 29 | self.add_item(AllowSame()) 30 | self.add_item(DiscardButton()) 31 | self.add_item(SaveButton(ctx)) 32 | 33 | def initial_message(self): 34 | if not self.record: 35 | self.record = SSVerify(guild_id=self.ctx.guild.id) 36 | 37 | _e = discord.Embed(color=0x00FFB3, title="Enter details & Press Save", url=config.SERVER_LINK) 38 | 39 | fields = { 40 | "Channel": getattr(self.record.channel, "mention", "`Not-Set`"), 41 | "Role": getattr(self.record.role, "mention", "`Not-Set`"), 42 | "Required ss": f"`{self.record.required_ss}`", 43 | "Screenshot Type": "`Not-Set`" if not self.record.ss_type else f"`{self.record.ss_type.value.title()}`", 44 | "Page Name": f"`{self.record.channel_name or '`Not-Set`'}`", 45 | "Page URL": "`Not-Set (Not Required)`" 46 | if self.record.channel_link == config.SERVER_LINK 47 | else f"[Click Here]({self.record.channel_link})", 48 | "Allow Same SS": "`Yes`" if self.record.allow_same else "`No`", 49 | } 50 | 51 | for _idx, (name, value) in enumerate(fields.items(), start=1): 52 | _e.add_field( 53 | name=f"{kd(_idx)} {name}:", 54 | value=value, 55 | ) 56 | 57 | return _e 58 | 59 | async def refresh_view(self): 60 | _e = self.initial_message() 61 | 62 | if all( 63 | ( 64 | self.record.channel_id, 65 | self.record.role_id, 66 | self.record.required_ss, 67 | self.record.ss_type, 68 | self.record.channel_name, 69 | ) 70 | ): 71 | self.children[-1].disabled = False 72 | 73 | try: 74 | self.message = await self.message.edit(embed=_e, view=self) 75 | except discord.HTTPException: 76 | await self.on_timeout() 77 | -------------------------------------------------------------------------------- /src/cogs/esports/views/tagcheck/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING 4 | 5 | from ...views.base import EsportsBaseView 6 | 7 | if TYPE_CHECKING: 8 | from core import Quotient 9 | 10 | import discord 11 | 12 | from core import Context 13 | from models import TagCheck 14 | 15 | 16 | class TagCheckView(EsportsBaseView): 17 | def __init__(self, ctx: Context): 18 | super().__init__(ctx) 19 | 20 | self.ctx = ctx 21 | self.bot: Quotient = ctx.bot 22 | 23 | async def initial_embed(self): 24 | records = await TagCheck.filter(guild_id=self.ctx.guild.id) 25 | to_show = [f"`{idx}.` {_.__str__()}" for idx, _ in enumerate(records, start=1)] 26 | _m = "\n".join(to_show) if to_show else "```No TagCheck channels found.```" 27 | _e = discord.Embed(color=0x00FFB3, title="TagCheck Editor") 28 | _e.description = "**Current TagCheck channels:**\n" + _m 29 | _e.set_footer(text="Click Add Channel to set up a new TagCheck channel.") 30 | return _e 31 | 32 | @discord.ui.button(label="Add Channel", custom_id="add_tc_channel") 33 | async def add_tc_channel(self, interaction: discord.Interaction, button: discord.Button): 34 | await interaction.response.defer() 35 | 36 | @discord.ui.button(label="Remove Channel", custom_id="remove_tc_channel") 37 | async def remove_tc_channel(self, interaction: discord.Interaction, button: discord.Button): 38 | await interaction.response.defer() 39 | -------------------------------------------------------------------------------- /src/cogs/esports/views/tourney/__init__.py: -------------------------------------------------------------------------------- 1 | from .groups import * 2 | from .slotm import * 3 | -------------------------------------------------------------------------------- /src/cogs/esports/views/tourney/_base.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import discord 4 | 5 | from models import Tourney 6 | 7 | from ...views.base import EsportsBaseView 8 | 9 | 10 | class TourneyView(EsportsBaseView): 11 | record: Tourney 12 | 13 | def __init__(self, ctx, **kwargs): 14 | super().__init__(ctx, **kwargs) 15 | 16 | 17 | class TourneyButton(discord.ui.Button): 18 | view: TourneyView 19 | 20 | def __init__(self, **kwargs): 21 | super().__init__(**kwargs) 22 | -------------------------------------------------------------------------------- /src/cogs/esports/views/tourney/_select.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import typing as T 4 | 5 | import discord 6 | 7 | from core import QuotientView 8 | from models import TMSlot, Tourney 9 | from utils import emote 10 | 11 | 12 | class TourneySelector(discord.ui.Select): 13 | view: QuotientView 14 | 15 | def __init__(self, placeholder: str, tourneys: T.List[Tourney]): 16 | 17 | _options = [] 18 | for tourney in tourneys: 19 | _options.append( 20 | discord.SelectOption( 21 | label=f"{getattr(tourney.registration_channel,'name','channel-deleted')} - (ID:{tourney.id})", 22 | emoji=emote.TextChannel, 23 | value=tourney.id, 24 | ) 25 | ) 26 | 27 | super().__init__(placeholder=placeholder, options=_options) 28 | 29 | async def callback(self, interaction: discord.Interaction): 30 | await interaction.response.defer() 31 | self.view.custom_id = self.values[0] 32 | 33 | self.view.stop() 34 | 35 | 36 | class TourneySlotSelec(discord.ui.Select): 37 | view: QuotientView 38 | 39 | def __init__(self, slots: T.List[TMSlot], placeholder: str = "Select a slot to cancel"): 40 | _options = [] 41 | 42 | for slot in slots: 43 | _options.append( 44 | discord.SelectOption( 45 | emoji=emote.TextChannel, 46 | label=f"Slot {slot.num} - {slot.team_name}", 47 | description=f"#{getattr(slot.tourney.registration_channel,'name','channel-deleted')} - (ID:{slot.tourney.id})", 48 | value=f"{slot.id}:{slot.tourney.id}", 49 | ) 50 | ) 51 | 52 | super().__init__(options=_options, placeholder=placeholder, max_values=len(_options)) 53 | 54 | async def callback(self, interaction: discord.Interaction): 55 | await interaction.response.defer() 56 | self.view.stop() 57 | self.view.custom_id = interaction.data["values"] 58 | -------------------------------------------------------------------------------- /src/cogs/esports/views/tourney/_wiz.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from string import ascii_uppercase 4 | 5 | from core import Context 6 | from models import Tourney 7 | 8 | from ._base import TourneyView 9 | from ._buttons import * 10 | 11 | 12 | class TourneySetupWizard(TourneyView): 13 | record: Tourney 14 | 15 | def __init__(self, ctx: Context): 16 | super().__init__(ctx) 17 | 18 | self.ctx = ctx 19 | self.record = None 20 | 21 | self.add_item(RegChannel(ctx, "a")) 22 | self.add_item(ConfirmChannel(ctx, "b")) 23 | self.add_item(SetRole(ctx, "c")) 24 | self.add_item(SetMentions(ctx, "d")) 25 | self.add_item(SetGroupSize(ctx, "e")) 26 | self.add_item(SetSlots(ctx, "f")) 27 | self.add_item(SetEmojis(ctx, "g")) 28 | self.add_item(DiscardButton(ctx)) 29 | self.add_item(SaveTourney(ctx)) 30 | 31 | def initial_message(self): 32 | if not self.record: 33 | self.record = Tourney(guild_id=self.ctx.guild.id, host_id=self.ctx.author.id) 34 | 35 | fields = { 36 | "Registration Channel": getattr(self.record.registration_channel, "mention", "`Not-Set`"), 37 | "Confirm Channel": getattr(self.record.confirm_channel, "mention", "`Not-Set`"), 38 | "Success Role": getattr(self.record.role, "mention", "`Not-Set`"), 39 | "Required Mentions": f"`{self.record.required_mentions}`", 40 | "Teams per Group": f"`{self.record.group_size or 'Not-Set'}`", 41 | "Total Slots": f"`{self.record.total_slots or 'Not-Set'}`", 42 | f"Reactions {self.bot.config.PRIME_EMOJI}": f"{self.record.check_emoji},{self.record.cross_emoji}", 43 | } 44 | 45 | _e = discord.Embed(color=0x00FFB3, title="Enter details & Press Save", url=self.bot.config.SERVER_LINK) 46 | 47 | for idx, (name, value) in enumerate(fields.items()): 48 | _e.add_field( 49 | name=f"{ri(ascii_uppercase[idx])} {name}:", 50 | value=value, 51 | ) 52 | 53 | return _e 54 | 55 | async def refresh_view(self): 56 | _e = self.initial_message() 57 | 58 | if all( 59 | ( 60 | self.record.registration_channel_id, 61 | self.record.role_id, 62 | self.record.confirm_channel_id, 63 | self.record.total_slots, 64 | self.record.group_size, 65 | ) 66 | ): 67 | self.children[-1].disabled = False 68 | 69 | try: 70 | self.message = await self.message.edit(embed=_e, view=self) 71 | except discord.HTTPException: 72 | await self.on_timeout() 73 | -------------------------------------------------------------------------------- /src/cogs/events/__init__.py: -------------------------------------------------------------------------------- 1 | from .cmds import CmdEvents 2 | from .errors import Errors 3 | from .main import MainEvents 4 | from .tasks import QuoTasks 5 | from .votes import VotesCog 6 | from .interaction import InteractionErrors 7 | 8 | 9 | async def setup(bot): 10 | await bot.add_cog(MainEvents(bot)) 11 | await bot.add_cog(QuoTasks(bot)) 12 | await bot.add_cog(CmdEvents(bot)) 13 | await bot.add_cog(VotesCog(bot)) 14 | await bot.add_cog(Errors(bot)) 15 | await bot.add_cog(InteractionErrors(bot)) 16 | -------------------------------------------------------------------------------- /src/cogs/events/cmds.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import typing 4 | 5 | if typing.TYPE_CHECKING: 6 | from core import Quotient 7 | 8 | from contextlib import suppress 9 | 10 | import discord 11 | from core import Cog, Context 12 | from models import ArrayRemove, Autorole, Commands 13 | 14 | 15 | class CmdEvents(Cog): 16 | def __init__(self, bot: Quotient): 17 | self.bot = bot 18 | 19 | async def bot_check(self, ctx: Context): 20 | if ctx.author.id in self.bot.config.DEVS: 21 | return True 22 | 23 | if self.bot.lockdown is True: 24 | t = ( 25 | "**Quotient is getting new features** 🥳\n" 26 | "Dear user, Quotient is updating and is not accepting any commands.\n" 27 | "It will back within **2 minutes**.\n" 28 | ) 29 | 30 | if self.bot.lockdown_msg: 31 | t += f"\n\n**Message from developer:**\n{self.bot.lockdown_msg} ~ deadshot#7999" 32 | 33 | await ctx.error(t) 34 | return False 35 | 36 | if not ctx.guild: 37 | return False 38 | 39 | return True 40 | 41 | @Cog.listener() 42 | async def on_command_completion(self, ctx: Context): 43 | if not ctx.command or not ctx.guild: 44 | return 45 | 46 | cmd = ctx.command.qualified_name 47 | 48 | await Commands.create( 49 | guild_id=ctx.guild.id, 50 | channel_id=ctx.channel.id, 51 | user_id=ctx.author.id, 52 | cmd=cmd, 53 | prefix=ctx.prefix, 54 | failed=ctx.command_failed, 55 | ) 56 | 57 | @Cog.listener(name="on_member_join") 58 | async def on_autorole(self, member: discord.Member): 59 | guild = member.guild 60 | 61 | with suppress(discord.HTTPException): 62 | record = await Autorole.get_or_none(guild_id=guild.id) 63 | if not record: 64 | return 65 | 66 | if not member.bot and record.humans: 67 | for role in record.humans: 68 | try: 69 | await member.add_roles(discord.Object(id=role), reason="Quotient's autorole") 70 | except (discord.NotFound, discord.Forbidden): 71 | await Autorole.filter(guild_id=guild.id).update(humans=ArrayRemove("humans", role)) 72 | continue 73 | 74 | elif member.bot and record.bots: 75 | for role in record.bots: 76 | try: 77 | await member.add_roles(discord.Object(id=role), reason="Quotient's autorole") 78 | except (discord.Forbidden, discord.NotFound): 79 | await Autorole.filter(guild_id=guild.id).update(bots=ArrayRemove("bots", role)) 80 | continue 81 | else: 82 | return 83 | -------------------------------------------------------------------------------- /src/cogs/events/interaction.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import typing as T 4 | 5 | if T.TYPE_CHECKING: 6 | from core import Quotient 7 | 8 | import discord 9 | from core import Cog 10 | from discord.app_commands import AppCommandError 11 | 12 | __all__ = ("InteractionErrors",) 13 | 14 | 15 | class InteractionErrors(Cog): 16 | def __init__(self, bot: Quotient): 17 | self.bot = bot 18 | self.bot.tree.interaction_check = self.global_interaction_check 19 | self.bot.tree.on_error = self.on_app_command_error 20 | 21 | async def global_interaction_check(self, interaction: discord.Interaction) -> bool: 22 | if not interaction.guild_id: 23 | await interaction.response.send_message( 24 | embed=discord.Embed( 25 | color=discord.Color.red(), 26 | description="Application commands can not be used in Private Messages.", 27 | ), 28 | ephemeral=True, 29 | ) 30 | 31 | return False 32 | 33 | return True 34 | 35 | async def on_app_command_error(self, interaction: discord.Interaction, error: AppCommandError): 36 | if isinstance(error, discord.NotFound): 37 | return # these unknown interactions are annoying :pain: 38 | 39 | # rest later 40 | -------------------------------------------------------------------------------- /src/cogs/events/main.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import typing 4 | 5 | if typing.TYPE_CHECKING: 6 | from core import Quotient 7 | 8 | import re 9 | from contextlib import suppress 10 | 11 | import config 12 | import discord 13 | from constants import random_greeting 14 | from core import Cog, Context 15 | from models import Guild 16 | 17 | 18 | class MainEvents(Cog, name="Main Events"): 19 | def __init__(self, bot: Quotient) -> None: 20 | self.bot = bot 21 | 22 | # incomplete?, I know 23 | @Cog.listener() 24 | async def on_guild_join(self, guild: discord.Guild) -> None: 25 | with suppress(AttributeError): 26 | g, b = await Guild.get_or_create(guild_id=guild.id) 27 | self.bot.cache.guild_data[guild.id] = { 28 | "prefix": g.prefix, 29 | "color": g.embed_color or self.bot.color, 30 | "footer": g.embed_footer or config.FOOTER, 31 | } 32 | self.bot.loop.create_task(guild.chunk()) 33 | 34 | @Cog.listener() 35 | async def on_message(self, message: discord.Message) -> None: 36 | if message.author.bot or message.guild is None: 37 | return 38 | if re.match(f"^<@!?{self.bot.user.id}>$", message.content): 39 | ctx: Context = await self.bot.get_context(message) 40 | self.bot.dispatch("mention", ctx) 41 | 42 | @Cog.listener() 43 | async def on_mention(self, ctx: Context) -> None: 44 | prefix: str = self.bot.cache.guild_data[ctx.guild.id].get("prefix", "q") 45 | await ctx.send( 46 | f"{random_greeting()} You seem lost. Are you?\n" 47 | f"Current prefix for this server is: `{prefix}`.\n\nUse it like: `{prefix}help`" 48 | ) 49 | -------------------------------------------------------------------------------- /src/cogs/events/tasks.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING 4 | 5 | if TYPE_CHECKING: 6 | from core import Quotient 7 | 8 | import config 9 | from core import Cog 10 | from discord.ext import tasks 11 | 12 | 13 | class QuoTasks(Cog): 14 | def __init__(self, bot: Quotient): 15 | self.bot = bot 16 | 17 | self.insert_guilds.start() 18 | 19 | @tasks.loop(count=1) 20 | async def insert_guilds(self): 21 | query = "INSERT INTO guild_data (guild_id , prefix , embed_color , embed_footer) VALUES ($1 , $2 , $3, $4) ON CONFLICT DO NOTHING" 22 | for guild in self.bot.guilds: 23 | await self.bot.db.execute(query, guild.id, config.PREFIX, config.COLOR, config.FOOTER) 24 | 25 | @insert_guilds.before_loop 26 | async def before_loops(self): 27 | await self.bot.wait_until_ready() 28 | -------------------------------------------------------------------------------- /src/cogs/events/votes.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import typing 4 | 5 | if typing.TYPE_CHECKING: 6 | from core import Quotient 7 | 8 | import constants 9 | import discord 10 | from core import Cog 11 | from discord import Webhook 12 | from models import Timer, User, Votes 13 | 14 | 15 | class VotesCog(Cog): 16 | def __init__(self, bot: Quotient): 17 | self.bot = bot 18 | self.hook = Webhook.from_url(self.bot.config.PUBLIC_LOG, session=self.bot.session) 19 | 20 | @Cog.listener() 21 | async def on_member_join(self, member: discord.Member): 22 | """we grant users voter, premium role if they join later.""" 23 | 24 | if not member.guild or not member.guild.id == self.bot.config.SERVER_ID: 25 | return 26 | 27 | if await Votes.get(user_id=member.id, is_voter=True).exists(): 28 | await member.add_roles(discord.Object(id=self.bot.config.VOTER_ROLE)) 29 | 30 | if await User.get(pk=member.id, is_premium=True).exists(): 31 | await member.add_roles(discord.Object(id=self.bot.config.PREMIUM_ROLE)) 32 | 33 | @Cog.listener() 34 | async def on_vote_timer_complete(self, timer: Timer): 35 | user_id = timer.kwargs["user_id"] 36 | vote = await Votes.get(user_id=user_id) 37 | 38 | await Votes.get(pk=user_id).update(is_voter=False, notified=False) 39 | 40 | member = self.bot.server.get_member(user_id) 41 | if member is not None: 42 | await member.remove_roles(discord.Object(id=self.bot.config.VOTER_ROLE), reason="Their vote expired.") 43 | 44 | else: 45 | member = await self.bot.getch(self.bot.get_user, self.bot.fetch_user, user_id) 46 | 47 | if vote.reminder: 48 | embed = discord.Embed( 49 | color=self.bot.color, 50 | description=f"{constants.random_greeting()}, You asked me to remind you to vote.", 51 | title="Vote Expired!", 52 | url="https://quotientbot.xyz/vote", 53 | ) 54 | try: 55 | await member.send(embed=embed) 56 | except discord.Forbidden: 57 | pass 58 | -------------------------------------------------------------------------------- /src/cogs/mod/events/__init__.py: -------------------------------------------------------------------------------- 1 | from .lockdown import LockEvents 2 | from .roles import RoleEvents 3 | -------------------------------------------------------------------------------- /src/cogs/mod/events/lockdown.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import typing 4 | 5 | if typing.TYPE_CHECKING: 6 | from core import Quotient 7 | 8 | from constants import LockType 9 | from core import Cog 10 | from models import Lockdown, Timer 11 | 12 | 13 | class LockEvents(Cog): 14 | def __init__(self, bot: Quotient): 15 | self.bot = bot 16 | 17 | @Cog.listener() 18 | async def on_lockdown_timer_complete(self, timer: Timer): 19 | _type = timer.kwargs["_type"] 20 | 21 | if _type == LockType.channel.value: 22 | channel_id = timer.kwargs["channel_id"] 23 | 24 | check = await Lockdown.get_or_none(channel_id=channel_id, type=LockType.channel) 25 | if not check or check.expire_time != timer.expires: 26 | return 27 | 28 | channel = self.bot.get_channel(channel_id) 29 | if not channel or not channel.permissions_for(channel.guild.me).manage_channels: 30 | return 31 | 32 | perms = channel.overwrites_for(channel.guild.default_role) 33 | perms.send_messages = True 34 | await channel.set_permissions( 35 | channel.guild.default_role, overwrite=perms, reason="Lockdown timer complete!" 36 | ) 37 | await Lockdown.filter(channel_id=channel.id).delete() 38 | await channel.send(f"Unlocked **{channel}**") 39 | 40 | elif _type == LockType.guild.value: 41 | 42 | guild_id = timer.kwargs["guild_id"] 43 | 44 | check = await Lockdown.get_or_none(guild_id=guild_id, type=LockType.guild) 45 | if not check or check.expire_time != timer.expires: 46 | return 47 | 48 | for channel in check.channels: 49 | if ( 50 | channel is not None 51 | and channel.permissions_for(channel.guild.me).manage_channels 52 | ): 53 | 54 | perms = channel.overwrites_for(channel.guild.default_role) 55 | perms.send_messages = True 56 | await channel.set_permissions( 57 | channel.guild.default_role, 58 | overwrite=perms, 59 | reason="Lockdown timer complete!", 60 | ) 61 | await Lockdown.filter(guild_id=guild_id, type=LockType.guild).delete() 62 | channel = self.bot.get_channel(check.channel_id) 63 | if channel is not None and channel.permissions_for(channel.guild.me).send_messages: 64 | await channel.send(f"Unlocked **server**.") 65 | -------------------------------------------------------------------------------- /src/cogs/mod/events/roles.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import typing 4 | 5 | if typing.TYPE_CHECKING: 6 | from core import Quotient 7 | 8 | from core import Cog 9 | 10 | 11 | class RoleEvents(Cog): 12 | def __init__(self, bot: Quotient): 13 | self.bot = bot 14 | -------------------------------------------------------------------------------- /src/cogs/mod/utils.py: -------------------------------------------------------------------------------- 1 | from collections import Counter 2 | 3 | import discord 4 | from core import Context 5 | 6 | 7 | async def _self_clean_system(ctx: Context, search: int) -> dict: 8 | count = 0 9 | async for msg in ctx.history(limit=search, before=ctx.message): 10 | if msg.author == ctx.me: 11 | await msg.delete() 12 | count += 1 13 | return {"Bot": count} 14 | 15 | 16 | async def _complex_cleanup_strategy(ctx: Context, search) -> Counter: 17 | def check(m: discord.Message): 18 | return m.author == ctx.me or m.content.startswith(ctx.prefix) 19 | 20 | deleted = await ctx.channel.purge(limit=search, check=check, before=ctx.message) 21 | return Counter(m.author.display_name for m in deleted) 22 | 23 | 24 | async def do_removal(ctx: Context, limit, predicate, *, before=None, after=None): 25 | if limit > 2000: 26 | return await ctx.error(f"Too many messages to search given ({limit}/2000)") 27 | 28 | if before is None: 29 | before = ctx.message 30 | else: 31 | before = discord.Object(id=before) 32 | 33 | if after is not None: 34 | after = discord.Object(id=after) 35 | 36 | try: 37 | deleted = await ctx.channel.purge(limit=limit, before=before, after=after, check=predicate) 38 | except discord.Forbidden as e: 39 | return await ctx.error("I do not have permissions to delete messages.") 40 | except discord.HTTPException as e: 41 | return await ctx.error(f"Error: {e} (try a smaller search?)") 42 | 43 | spammers = Counter(m.author.display_name for m in deleted) 44 | deleted = len(deleted) 45 | messages = [f'{deleted} message{" was" if deleted == 1 else "s were"} removed.'] 46 | if deleted: 47 | messages.append("") 48 | spammers = sorted(spammers.items(), key=lambda t: t[1], reverse=True) 49 | messages.extend(f"**{name}**: {count}" for name, count in spammers) 50 | 51 | to_send = "\n".join(messages) 52 | 53 | if len(to_send) > 2000: 54 | await ctx.send(f"Successfully removed {deleted} messages.", delete_after=10) 55 | else: 56 | await ctx.send(to_send, delete_after=10) 57 | -------------------------------------------------------------------------------- /src/cogs/mod/views/__init__.py: -------------------------------------------------------------------------------- 1 | from .role import * # noqa: F401, F403 2 | -------------------------------------------------------------------------------- /src/cogs/mod/views/role.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import typing 4 | 5 | if typing.TYPE_CHECKING: 6 | from core import Quotient 7 | 8 | from contextlib import suppress 9 | 10 | import discord 11 | from core import Context 12 | from utils import emote 13 | 14 | 15 | class RoleRevertButton(discord.ui.Button): 16 | def __init__(self, ctx: Context, *, role: discord.Role, members: typing.List[discord.Member], take_role=True): 17 | super().__init__() 18 | 19 | self.emoji = emote.exit 20 | self.label = "Take Back" if take_role else "Give Back" 21 | self.custom_id = "role_revert_action_button" 22 | 23 | self.ctx = ctx 24 | self.role = role 25 | self.members = members 26 | self.take_role = take_role 27 | 28 | async def callback(self, interaction: discord.Interaction): 29 | await interaction.response.defer() 30 | await self.view.on_timeout() 31 | 32 | for _ in self.members: 33 | with suppress(discord.HTTPException): 34 | await _.remove_roles(self.role) if self.take_role else await _.add_roles(self.role) 35 | 36 | return await self.ctx.success("Succesfully reverted the action.") 37 | 38 | 39 | class RoleCancelButton(discord.ui.Button): 40 | def __init__(self, ctx: Context, *, role: discord.Role, members: typing.List[discord.Member]): 41 | super().__init__() 42 | self.ctx = ctx 43 | self.role = role 44 | self.members = members 45 | -------------------------------------------------------------------------------- /src/cogs/premium/views.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | import config 4 | import discord 5 | from utils import emote 6 | from models import PremiumPlan, PremiumTxn 7 | 8 | 9 | class PlanSelector(discord.ui.Select): 10 | def __init__(self, plans: List[PremiumPlan]): 11 | super().__init__(placeholder="Select a Quotient Premium Plan... ") 12 | 13 | for _ in plans: 14 | self.add_option(label=f"{_.name} - ₹{_.price}", description=_.description, value=_.id) 15 | 16 | async def callback(self, interaction: discord.Interaction): 17 | await interaction.response.defer() 18 | self.view.plan = self.values[0] 19 | self.view.stop() 20 | 21 | 22 | class PremiumPurchaseBtn(discord.ui.Button): 23 | def __init__(self, label="Get Quotient Pro", emoji=emote.diamond, style=discord.ButtonStyle.grey): 24 | super().__init__(style=style, label=label, emoji=emoji) 25 | 26 | async def callback(self, interaction: discord.Interaction): 27 | await interaction.response.defer() 28 | v = discord.ui.View(timeout=100) 29 | v.plan: str = None 30 | 31 | v.add_item(PlanSelector(await PremiumPlan.all().order_by("id"))) 32 | await interaction.followup.send("Please select the Quotient Pro plan, you want to opt:", view=v, ephemeral=True) 33 | await v.wait() 34 | 35 | if not v.plan: 36 | return 37 | 38 | txn = await PremiumTxn.create( 39 | txnid=await PremiumTxn.gen_txnid(), 40 | user_id=interaction.user.id, 41 | guild_id=interaction.guild.id, 42 | plan_id=v.plan, 43 | ) 44 | _link = config.PAY_LINK + "getpremium" + "?txnId=" + txn.txnid 45 | 46 | await interaction.followup.send( 47 | f"You are about to purchase Quotient Premium for **{interaction.guild.name}**.\n" 48 | "If you want to purchase for another server, use `qpremium` or `/premium` command in that server.\n\n" 49 | f"[*Click Me to Complete the Payment*]({_link})", 50 | ephemeral=True, 51 | ) 52 | 53 | 54 | class PremiumView(discord.ui.View): 55 | def __init__(self, text="This feature requires Quotient Premium.", *, label="Get Quotient Pro"): 56 | super().__init__(timeout=None) 57 | self.text = text 58 | self.add_item(PremiumPurchaseBtn(label=label)) 59 | 60 | @property 61 | def premium_embed(self) -> discord.Embed: 62 | _e = discord.Embed( 63 | color=0x00FFB3, description=f"**You discovered a premium feature **" 64 | ) 65 | _e.description = ( 66 | f"\n*`{self.text}`*\n\n" 67 | "__Perks you get with Quotient Pro:__\n" 68 | f"{emote.check} Access to `Quotient Pro` bot.\n" 69 | f"{emote.check} Unlimited Scrims.\n" 70 | f"{emote.check} Unlimited Tournaments.\n" 71 | f"{emote.check} Custom Reactions for Regs.\n" 72 | f"{emote.check} Smart SSverification.\n" 73 | f"{emote.check} Cancel-Claim Panel.\n" 74 | f"{emote.check} Premium Role + more...\n" 75 | ) 76 | return _e 77 | -------------------------------------------------------------------------------- /src/cogs/quomisc/alerts.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from os import truncate 3 | 4 | import typing as T 5 | 6 | if T.TYPE_CHECKING: 7 | from core import Quotient 8 | 9 | from core import Cog, embeds, Context, QuotientView 10 | from discord.ext import commands 11 | from datetime import timedelta 12 | from models import Timer, Alert, Prompt, Read 13 | import discord 14 | 15 | from utils import QuoPaginator, discord_timestamp 16 | 17 | __all__ = ("QuoAlerts",) 18 | 19 | 20 | class PromptView(QuotientView): 21 | def __init__(self, ctx: Context, alert: Alert): 22 | super().__init__(ctx, timeout=300) 23 | self.ctx = ctx 24 | self.alert = alert 25 | 26 | @discord.ui.button(style=discord.ButtonStyle.green, label="Read Now") 27 | async def read_now(self, inter: discord.Interaction, btn: discord.Button): 28 | _e = discord.Embed.from_dict(self.alert.message) 29 | await inter.response.send_message(embed=_e, ephemeral=True) 30 | 31 | self.stop() 32 | await self.message.delete(delay=0) 33 | 34 | await self.alert.refresh_from_db() 35 | read = await Read.create(user_id=inter.user.id) 36 | await self.alert.reads.add(read) 37 | 38 | @discord.ui.button(style=discord.ButtonStyle.red, label="Dismiss") 39 | async def dismiss(self, inter: discord.Interaction, btn: discord.Button): 40 | self.stop() 41 | await self.message.delete(delay=0) 42 | 43 | 44 | class CreateAlert(discord.ui.Button): 45 | view: embeds.EmbedBuilder 46 | 47 | def __init__(self, ctx: Context): 48 | super().__init__(label="Create Alert", style=discord.ButtonStyle.green) 49 | self.ctx = ctx 50 | 51 | async def callback(self, interaction: discord.Interaction): 52 | await interaction.response.defer() 53 | 54 | await Alert.filter(active=True).update(active=False) 55 | record = await Alert.create(author_id=interaction.user.id, message=self.view.formatted) 56 | await self.ctx.bot.reminders.create_timer(record.created_at + timedelta(days=10), "alert", alert_id=record.id) 57 | 58 | await self.ctx.success("Created a new alert with `ID: {}`".format(record.id)) 59 | 60 | 61 | class QuoAlerts(Cog): 62 | def __init__(self, bot: Quotient): 63 | self.bot = bot 64 | 65 | def cog_check(self, ctx: Context): 66 | return ctx.author.id in ctx.config.DEVS 67 | 68 | @Cog.listener() 69 | async def on_command_completion(self, ctx: Context): 70 | record = await Alert.filter(active=True).order_by("-created_at").first() 71 | if not record: 72 | return 73 | 74 | if await record.reads.filter(user_id=ctx.author.id).exists(): 75 | return 76 | 77 | user_prompts = await record.prompts.filter(user_id=ctx.author.id).order_by("-prompted_at") 78 | if len(user_prompts) >= 3: 79 | return 80 | 81 | if user_prompts and user_prompts[0].prompted_at > (ctx.bot.current_time - timedelta(minutes=5)): 82 | return 83 | 84 | _e = discord.Embed( 85 | color=self.bot.color, title="You have an unread alert!", description="Click `Read Now` to read it." 86 | ) 87 | _e.set_thumbnail(url="https://cdn.discordapp.com/attachments/851846932593770496/1031240353489109112/alert.gif") 88 | v = PromptView(ctx, record) 89 | v.message = await ctx.message.reply(embed=_e, view=v) 90 | 91 | prompt = await Prompt.create(user_id=ctx.author.id) 92 | await record.prompts.add(prompt) 93 | 94 | @Cog.listener() 95 | async def on_alert_timer_complete(self, timer: Timer): 96 | record_id = timer.kwargs["alert_id"] 97 | await Alert.filter(pk=record_id).update(active=False) 98 | 99 | @commands.group(hidden=True, invoke_without_command=True) 100 | async def alr(self, ctx: Context): 101 | await ctx.send_help(ctx.command) 102 | 103 | @alr.command(name="list") 104 | async def alr_list(self, ctx: Context): 105 | records = await Alert.all().order_by("created_at") 106 | if not records: 107 | return await ctx.error("No alerts present at the moment, create one.") 108 | 109 | paginator = QuoPaginator(ctx, title="List of Alerts") 110 | for idx, record in enumerate(records, start=1): 111 | paginator.add_line(f"`{idx:02}` Created: {discord_timestamp(record.created_at)} (ID: `{record.pk}`)") 112 | 113 | await paginator.start() 114 | 115 | @alr.command(name="create") 116 | async def alr_create(self, ctx: Context): 117 | _v = embeds.EmbedBuilder(ctx, items=[CreateAlert(ctx)]) 118 | 119 | await _v.rendor() 120 | -------------------------------------------------------------------------------- /src/cogs/quomisc/helper.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import io 3 | 4 | import discord 5 | 6 | 7 | class TabularData: 8 | def __init__(self): 9 | self._widths = [] 10 | self._columns = [] 11 | self._rows = [] 12 | 13 | def set_columns(self, columns): 14 | self._columns = columns 15 | self._widths = [len(c) + 2 for c in columns] 16 | 17 | def add_row(self, row): 18 | rows = [str(r) for r in row] 19 | self._rows.append(rows) 20 | for index, element in enumerate(rows): 21 | width = len(element) + 2 22 | if width > self._widths[index]: 23 | self._widths[index] = width 24 | 25 | def add_rows(self, rows): 26 | for row in rows: 27 | self.add_row(row) 28 | 29 | def render(self): 30 | """Renders a table in rST format. 31 | Example: 32 | +-------+-----+ 33 | | Name | Age | 34 | +-------+-----+ 35 | | Alice | 24 | 36 | | Bob | 19 | 37 | +-------+-----+ 38 | """ 39 | sep = "+".join("-" * w for w in self._widths) 40 | sep = f"+{sep}+" 41 | 42 | to_draw = [sep] 43 | 44 | def get_entry(d): 45 | elem = "|".join(f"{e:^{self._widths[i]}}" for i, e in enumerate(d)) 46 | return f"|{elem}|" 47 | 48 | to_draw.append(get_entry(self._columns)) 49 | to_draw.append(sep) 50 | 51 | for row in self._rows: 52 | to_draw.append(get_entry(row)) 53 | 54 | to_draw.append(sep) 55 | return "\n".join(to_draw) 56 | 57 | 58 | async def tabulate_query(ctx, query, *args): 59 | records = await ctx.db.fetch(query, *args) 60 | 61 | if len(records) == 0: 62 | return await ctx.send("No results found.") 63 | 64 | headers = list(records[0].keys()) 65 | table = TabularData() 66 | table.set_columns(headers) 67 | table.add_rows(list(r.values()) for r in records) 68 | render = table.render() 69 | 70 | fmt = f"```\n{render}\n```" 71 | if len(fmt) > 2000: 72 | fp = io.BytesIO(fmt.encode("utf-8")) 73 | await ctx.send("Too many results...", file=discord.File(fp, "results.txt")) 74 | else: 75 | await ctx.send(fmt) 76 | 77 | 78 | def format_dt(dt, style=None): 79 | if dt.tzinfo is None: 80 | dt = dt.replace(tzinfo=datetime.timezone.utc) 81 | 82 | if style is None: 83 | return f"" 84 | return f"" 85 | 86 | 87 | def format_relative(dt): 88 | return format_dt(dt, "R") 89 | 90 | 91 | # async def find_query(ctx, query): 92 | # record = await FAQ.filter(aliases__icontains=query).all().first() 93 | # if record: 94 | # return record 95 | 96 | # return "\n".join(get_close_matches(query, (faq.aliases for faq in await FAQ.all()))) 97 | -------------------------------------------------------------------------------- /src/cogs/quomisc/views.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import typing 4 | from datetime import datetime, timedelta 5 | 6 | from models import User 7 | 8 | if typing.TYPE_CHECKING: 9 | from core import Quotient 10 | 11 | from contextlib import suppress 12 | 13 | import discord 14 | from constants import IST 15 | from core import Context, QuotientView 16 | from utils import emote 17 | 18 | 19 | class BaseView(discord.ui.View): 20 | def __init__(self, ctx: Context, *, timeout=30.0): 21 | 22 | self.ctx = ctx 23 | self.message: typing.Optional[discord.Message] = None 24 | self.bot: Quotient = ctx.bot 25 | 26 | super().__init__(timeout=timeout) 27 | 28 | async def interaction_check(self, interaction: discord.Interaction) -> bool: 29 | if interaction.user.id != self.ctx.author.id: 30 | await interaction.response.send_message( 31 | "Sorry, you can't use this interaction as it is not started by you.", 32 | ephemeral=True, 33 | ) 34 | return False 35 | return True 36 | 37 | async def on_timeout(self) -> None: 38 | if hasattr(self, "message"): 39 | for b in self.children: 40 | if isinstance(b, discord.ui.Button) and not b.style == discord.ButtonStyle.link: 41 | b.style, b.disabled = discord.ButtonStyle.grey, True 42 | 43 | with suppress(discord.HTTPException): 44 | if self.message is not None: 45 | await self.message.edit(view=self) 46 | 47 | 48 | class VoteButton(BaseView): 49 | def __init__(self, ctx: Context): 50 | super().__init__(ctx, timeout=None) 51 | 52 | self.add_item( 53 | discord.ui.Button( 54 | style=discord.ButtonStyle.link, 55 | url="https://quotientbot.xyz/vote", 56 | label="Click Here", 57 | ) 58 | ) 59 | 60 | 61 | class MoneyButton(BaseView): 62 | def __init__(self, ctx: Context): 63 | super().__init__(ctx) 64 | 65 | self.ctx = ctx 66 | self.bot: Quotient = ctx.bot 67 | 68 | @discord.ui.button( 69 | style=discord.ButtonStyle.green, custom_id="claim_prime", label="Claim Prime (120 coins)" 70 | ) 71 | async def claim_premium(self, interaction: discord.Interaction, button: discord.Button): 72 | await interaction.response.defer(ephemeral=True) 73 | 74 | self.children[0].disabled = True 75 | await self.message.edit(view=self) 76 | 77 | _u = await User.get(pk=self.ctx.author.id) 78 | if not _u.money >= 120: 79 | return await interaction.followup.send( 80 | f"{emote.error} Insufficient Quo Coins in your account.", ephemeral=True 81 | ) 82 | 83 | end_time = ( 84 | _u.premium_expire_time + timedelta(days=30) 85 | if _u.is_premium 86 | else datetime.now(tz=IST) + timedelta(days=30) 87 | ) 88 | 89 | await User.get(pk=self.ctx.author.id).update( 90 | is_premium=True, 91 | premium_expire_time=end_time, 92 | money=_u.money - 120, 93 | premiums=_u.premiums + 1, 94 | ) 95 | 96 | member = self.bot.server.get_member(self.ctx.author.id) 97 | if member is not None: 98 | await member.add_roles( 99 | discord.Object(id=self.bot.config.PREMIUM_ROLE), reason="They purchased premium." 100 | ) 101 | 102 | await self.ctx.success( 103 | "Credited Quotient Prime for 1 Month to your account,\n\n" 104 | "Use `qboost` in any server to upgrade it with Prime." 105 | ) 106 | 107 | 108 | class SetupButtonView(QuotientView): 109 | def __init__(self, ctx: Context): 110 | super().__init__(ctx, timeout=None) 111 | self.ctx = ctx 112 | 113 | @discord.ui.button(label="setup scrims", custom_id="setup_scrims_button") 114 | async def setup_scrims_button(self, interaction: discord.Interaction, button: discord.Button): 115 | await interaction.response.defer() 116 | return await self.ctx.simple(f"Kindly use `{self.ctx.prefix}sm setup` to setup a scrim.") 117 | 118 | @discord.ui.button(label="setup tourney", custom_id="setup_tourney_button") 119 | async def setup_tourney_button(self, interaction: discord.Interaction, button: discord.Button): 120 | return await self.ctx.simple( 121 | f"Kindly use `{self.ctx.prefix}t setup` to setup a tournament." 122 | ) 123 | -------------------------------------------------------------------------------- /src/cogs/reminder/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import typing 4 | 5 | if typing.TYPE_CHECKING: 6 | from core import Quotient 7 | 8 | import asyncio 9 | from datetime import datetime, timedelta 10 | 11 | import asyncpg 12 | import discord 13 | from core import Cog 14 | from models import Timer 15 | from utils import IST 16 | 17 | 18 | class Reminders(Cog): 19 | """Reminders to do something.""" 20 | 21 | def __init__(self, bot: Quotient): 22 | self.bot = bot 23 | self._have_data = asyncio.Event(loop=bot.loop) 24 | self._current_timer = None 25 | self._task = bot.loop.create_task(self.dispatch_timers()) 26 | 27 | def cog_unload(self): 28 | self._task.cancel() 29 | 30 | async def get_active_timer(self, *, days=7): 31 | return await Timer.filter(expires__lte=datetime.now(tz=IST) + timedelta(days=days)).order_by("expires").first() 32 | 33 | async def wait_for_active_timers(self, *, days=7): 34 | timer = await self.get_active_timer(days=days) 35 | if timer is not None: 36 | self._have_data.set() 37 | return timer 38 | 39 | self._have_data.clear() 40 | self._current_timer = None 41 | await self._have_data.wait() 42 | return await self.get_active_timer(days=days) 43 | 44 | async def call_timer(self, timer: Timer): 45 | # delete the timer 46 | deleted = await Timer.filter(pk=timer.id, expires=timer.expires).delete() 47 | 48 | if deleted == 0: # Probably a task is already deleted or its expire time changed. 49 | return 50 | 51 | # dispatch the event 52 | event_name = f"{timer.event}_timer_complete" 53 | self.bot.dispatch(event_name, timer) 54 | 55 | async def dispatch_timers(self): 56 | try: 57 | while not self.bot.is_closed(): 58 | timer = self._current_timer = await self.wait_for_active_timers(days=40) 59 | 60 | now = datetime.now(tz=IST) 61 | 62 | # print(now, timer.expires) 63 | 64 | if timer.expires >= now: 65 | to_sleep = (timer.expires - now).total_seconds() 66 | # print(to_sleep) 67 | await asyncio.sleep(to_sleep) 68 | 69 | await self.call_timer(timer) 70 | except (OSError, discord.ConnectionClosed, asyncpg.PostgresConnectionError): 71 | self._task.cancel() 72 | self._task = self.bot.loop.create_task(self.dispatch_timers()) 73 | 74 | async def short_timer_optimisation(self, seconds, timer: Timer): 75 | await asyncio.sleep(seconds) 76 | event_name = f"{timer.event}_timer_complete" 77 | self.bot.dispatch(event_name, timer) 78 | 79 | async def create_timer(self, *args, **kwargs): 80 | when, event, *args = args 81 | 82 | try: 83 | now = kwargs.pop("created") 84 | except KeyError: 85 | now = datetime.now(tz=IST) 86 | 87 | delta = (when - now).total_seconds() 88 | 89 | timer = await Timer.create( 90 | expires=when, 91 | created=now, 92 | event=event, 93 | extra={"kwargs": kwargs, "args": args}, 94 | ) 95 | 96 | # only set the data check if it can be waited on 97 | if delta <= (86400 * 40): # 40 days 98 | self._have_data.set() 99 | 100 | # check if this timer is earlier than our currently run timer 101 | if self._current_timer and when < self._current_timer.expires: 102 | # cancel the task and re-run it 103 | self._task.cancel() 104 | self._task = self.bot.loop.create_task(self.dispatch_timers()) 105 | 106 | return timer 107 | 108 | 109 | async def setup(bot: Quotient): 110 | await bot.add_cog(Reminders(bot)) 111 | -------------------------------------------------------------------------------- /src/cogs/utility/events/__init__.py: -------------------------------------------------------------------------------- 1 | from .autopurge import AutoPurgeEvents 2 | from .reminder import ReminderEvents -------------------------------------------------------------------------------- /src/cogs/utility/events/autopurge.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import typing 4 | 5 | if typing.TYPE_CHECKING: 6 | from core import Quotient 7 | 8 | from contextlib import suppress 9 | from datetime import datetime, timedelta 10 | 11 | import discord 12 | from constants import IST 13 | from core import Cog 14 | from models import AutoPurge, Snipe, Timer 15 | 16 | 17 | class AutoPurgeEvents(Cog): 18 | def __init__(self, bot: Quotient): 19 | self.bot = bot 20 | self.bot.loop.create_task(self.delete_older_snipes()) 21 | 22 | async def delete_older_snipes(self): # we delete snipes that are older than 10 days 23 | await self.bot.wait_until_ready() 24 | await Snipe.filter(delete_time__lte=(datetime.now(tz=IST) - timedelta(days=10))).delete() 25 | 26 | @Cog.listener() 27 | async def on_message_delete(self, message: discord.Message): 28 | if not message.guild or message.author.bot: 29 | return 30 | 31 | channel = message.channel 32 | content = message.content if message.content else "*[Content Unavailable]*" 33 | 34 | if not channel.type == discord.ChannelType.text: 35 | return 36 | 37 | await Snipe.update_or_create( 38 | channel_id=channel.id, 39 | defaults={ 40 | "author_id": message.author.id, 41 | "content": content, 42 | "nsfw": channel.is_nsfw(), 43 | }, 44 | ) 45 | 46 | @Cog.listener() 47 | async def on_message(self, message: discord.Message): 48 | if not message.guild or not message.channel.id in self.bot.cache.autopurge_channels: 49 | return 50 | 51 | record = await AutoPurge.get_or_none(channel_id=message.channel.id) 52 | if not record: 53 | return self.bot.cache.autopurge_channels.discard(message.channel.id) 54 | 55 | await self.bot.reminders.create_timer( 56 | datetime.now(tz=IST) + timedelta(seconds=record.delete_after), 57 | "autopurge", 58 | message_id=message.id, 59 | channel_id=message.channel.id, 60 | ) 61 | 62 | @Cog.listener() 63 | async def on_autopurge_timer_complete(self, timer: Timer): 64 | 65 | message_id, channel_id = timer.kwargs["message_id"], timer.kwargs["channel_id"] 66 | 67 | check = await AutoPurge.get_or_none(channel_id=channel_id) 68 | if not check: 69 | return 70 | 71 | channel = check.channel 72 | if not channel: 73 | return 74 | 75 | message = channel.get_partial_message(message_id) 76 | with suppress(discord.NotFound, discord.Forbidden, discord.HTTPException): 77 | msg = await message.fetch() 78 | if not msg.pinned: 79 | await msg.delete() 80 | 81 | @Cog.listener() 82 | async def on_guild_channel_delete(self, channel: discord.TextChannel): 83 | if channel.id in self.bot.cache.autopurge_channels: 84 | await AutoPurge.filter(channel_id=channel.id).delete() 85 | self.bot.cache.autopurge_channels.discard(channel.id) 86 | -------------------------------------------------------------------------------- /src/cogs/utility/events/reminder.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import typing 4 | 5 | if typing.TYPE_CHECKING: 6 | from core import Quotient 7 | 8 | from contextlib import suppress 9 | 10 | import discord 11 | from core import Cog 12 | from models import Timer 13 | from utils import discord_timestamp 14 | 15 | 16 | class ReminderEvents(Cog): 17 | def __init__(self, bot: Quotient): 18 | self.bot = bot 19 | 20 | @Cog.listener() 21 | async def on_reminder_timer_complete(self, timer: Timer): 22 | author_id, channel_id, message = timer.args 23 | 24 | try: 25 | channel = self.bot.get_channel(channel_id) or ( 26 | await self.bot.fetch_channel(channel_id) 27 | ) 28 | except discord.HTTPException: 29 | return 30 | 31 | guild_id = ( 32 | channel.guild.id 33 | if isinstance(channel, (discord.TextChannel, discord.Thread)) 34 | else "@me" 35 | ) 36 | message_id = timer.kwargs["message_id"] 37 | msg = f"{discord_timestamp(timer.created)}: {message}" 38 | 39 | if message_id: 40 | msg = f"{msg}\n\n**Original Message**\n" 41 | 42 | embed = discord.Embed( 43 | color=self.bot.color, title=f"Reminders #{timer.id}", description=msg 44 | ) 45 | 46 | with suppress(discord.HTTPException, discord.Forbidden, AttributeError): 47 | await channel.send(f"<@{author_id}>", embed=embed) 48 | -------------------------------------------------------------------------------- /src/cogs/utility/views/__init__.py: -------------------------------------------------------------------------------- 1 | from .embeds import * 2 | -------------------------------------------------------------------------------- /src/cogs/utility/views/embeds.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from core.embeds import EmbedBuilder 3 | import discord 4 | from utils import emote 5 | import typing as T 6 | 7 | __all__ = ("EmbedSend", "EmbedCancel") 8 | 9 | 10 | class EmbedSend(discord.ui.Button): 11 | view: EmbedBuilder 12 | 13 | def __init__(self, channel: discord.TextChannel): 14 | self.channel = channel 15 | super().__init__(label="Send to #{0}".format(channel.name), style=discord.ButtonStyle.green) 16 | 17 | async def callback(self, interaction: discord.Interaction) -> T.Any: 18 | try: 19 | m: T.Optional[discord.Message] = await self.channel.send(embed=self.view.embed) 20 | 21 | except Exception as e: 22 | await interaction.response.send_message(f"An error occured: {e}", ephemeral=True) 23 | 24 | else: 25 | await interaction.response.send_message( 26 | f"{emote.check} | Embed was sent to {self.channel.mention} ([Jump URL](<{m.jump_url}>))", ephemeral=True 27 | ) 28 | await self.view.on_timeout() 29 | 30 | 31 | class EmbedCancel(discord.ui.Button): 32 | view: EmbedBuilder 33 | 34 | def __init__(self): 35 | super().__init__(label="Cancel", style=discord.ButtonStyle.red) 36 | 37 | async def callback(self, interaction: discord.Interaction) -> T.Any: 38 | await interaction.response.send_message(f"{emote.xmark} | Embed sending cancelled.", ephemeral=True) 39 | await self.view.on_timeout() 40 | -------------------------------------------------------------------------------- /src/core/Cog.py: -------------------------------------------------------------------------------- 1 | from discord.ext import commands 2 | 3 | __all__ = ("Cog",) 4 | 5 | 6 | class Cog(commands.Cog): 7 | """A custom implementation of commands.Cog class.""" 8 | 9 | def __init__(self, *args, **kwargs): 10 | super().__init__(*args, **kwargs) 11 | 12 | def __str__(self): 13 | return "{0.__class__.__name__}".format(self) 14 | -------------------------------------------------------------------------------- /src/core/__init__.py: -------------------------------------------------------------------------------- 1 | from .Bot import Quotient, bot 2 | from .Cog import Cog 3 | from .Context import Context 4 | from .cooldown import * 5 | from .decorators import * 6 | from .views import * 7 | -------------------------------------------------------------------------------- /src/core/cache.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from datetime import datetime 4 | from typing import TYPE_CHECKING 5 | 6 | import config 7 | from constants import IST 8 | from models import AutoPurge, EasyTag, Guild, Scrim, SSVerify, TagCheck, Tourney 9 | 10 | 11 | class CacheManager: 12 | def __init__(self, bot): 13 | if TYPE_CHECKING: 14 | from .Bot import Quotient 15 | 16 | self.bot: Quotient = bot 17 | 18 | self.guild_data = {} 19 | self.eztagchannels = set() 20 | self.tagcheck = set() 21 | self.scrim_channels = set() 22 | self.tourney_channels = set() 23 | self.autopurge_channels = set() 24 | self.media_partner_channels = set() 25 | self.ssverify_channels = set() 26 | 27 | async def fill_temp_cache(self): 28 | 29 | async for record in Guild.all(): 30 | self.guild_data[record.guild_id] = { 31 | "prefix": record.prefix, 32 | "color": record.embed_color or config.COLOR, 33 | "footer": record.embed_footer or config.FOOTER, 34 | } 35 | 36 | async for record in EasyTag.all(): 37 | self.eztagchannels.add(record.channel_id) 38 | 39 | async for record in TagCheck.all(): 40 | self.tagcheck.add(record.channel_id) 41 | 42 | async for record in Scrim.filter(opened_at__lte=datetime.now(tz=IST)).all(): 43 | self.scrim_channels.add(record.registration_channel_id) 44 | 45 | async for record in Tourney.filter(started_at__not_isnull=True): 46 | self.tourney_channels.add(record.registration_channel_id) 47 | 48 | async for record in AutoPurge.all(): 49 | self.autopurge_channels.add(record.channel_id) 50 | 51 | async for record in Tourney.all(): 52 | async for partner in record.media_partners.all(): 53 | self.media_partner_channels.add(partner.channel_id) 54 | 55 | async for record in SSVerify.all(): 56 | self.ssverify_channels.add(record.channel_id) 57 | 58 | def guild_color(self, guild_id: int): 59 | return self.guild_data.get(guild_id, {}).get("color", config.COLOR) 60 | 61 | def guild_footer(self, guild_id: int): 62 | return self.guild_data.get(guild_id, {}).get("footer", config.FOOTER) 63 | 64 | async def update_guild_cache(self, guild_id: int, *, set_default=False) -> None: 65 | if set_default: 66 | await Guild.get(pk=guild_id).update( 67 | prefix=config.PREFIX, embed_color=config.COLOR, embed_footer=config.FOOTER 68 | ) 69 | 70 | _g = await Guild.get(pk=guild_id) 71 | self.guild_data[guild_id] = { 72 | "prefix": _g.prefix, 73 | "color": _g.embed_color or config.COLOR, 74 | "footer": _g.embed_footer or config.FOOTER, 75 | } 76 | 77 | # @staticmethod 78 | # @cached(ttl=10, serializer=JsonSerializer()) 79 | # async def match_bot_guild(guild_id: int, bot_id: int) -> bool: 80 | # return await Guild.filter(pk=guild_id, bot_id=bot_id).exists() 81 | -------------------------------------------------------------------------------- /src/core/cooldown.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import typing as T 4 | 5 | import discord 6 | from discord.ext import commands 7 | from collections import defaultdict 8 | 9 | 10 | __all__ = ("QuotientRatelimiter", "UserCommandLimits") 11 | 12 | 13 | class CooldownByMember(commands.CooldownMapping): 14 | def _bucket_key(self, member: discord.Member): 15 | return member.id 16 | 17 | 18 | class CooldownByGuild(commands.CooldownMapping): 19 | def _bucket_key(self, member: discord.Guild): 20 | return member.id 21 | 22 | 23 | class QuotientRatelimiter: 24 | def __init__(self, rate: float, per: float): 25 | self.by_member = CooldownByMember.from_cooldown(rate, per, commands.BucketType.member) 26 | self.by_guild = CooldownByGuild.from_cooldown(rate, per, commands.BucketType.guild) 27 | 28 | def is_ratelimited(self, obj: T.Union[discord.Guild, discord.Member]) -> bool: 29 | if isinstance(obj, discord.Guild): 30 | return self.by_guild.get_bucket(obj).update_rate_limit() 31 | 32 | return self.by_member.get_bucket(obj).update_rate_limit() 33 | 34 | 35 | class UserCommandLimits(defaultdict): 36 | def __missing__(self, key): 37 | r = self[key] = QuotientRatelimiter(2, 10) 38 | return r 39 | -------------------------------------------------------------------------------- /src/core/decorators.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from contextlib import suppress 4 | from functools import wraps 5 | from typing import TYPE_CHECKING, Any, Callable 6 | 7 | import discord 8 | 9 | from core.Context import Context 10 | 11 | from .cache import CacheManager 12 | from .Cog import Cog 13 | 14 | __all__ = ("right_bot_check", "event_bot_check", "role_command_check") 15 | 16 | 17 | class right_bot_check: 18 | def __call__(self, fn: Callable) -> Callable: 19 | @wraps(fn) 20 | async def wrapper(*args: Any, **kwargs: Any): 21 | if TYPE_CHECKING: 22 | from .Bot import Quotient 23 | 24 | if isinstance(args[0], Cog): 25 | bot: Quotient = args[0].bot 26 | 27 | else: 28 | bot: Quotient = args[0] # type: ignore 29 | 30 | with suppress(AttributeError): 31 | for arg in args: 32 | # check for both guild and guild_id 33 | if hasattr(arg, "guild"): 34 | 35 | guild_id = arg.guild.id 36 | break 37 | elif hasattr(arg, "guild_id"): 38 | guild_id = arg.guild_id 39 | break 40 | else: 41 | _obj = kwargs.get("guild") or kwargs.get("guild_id") 42 | # guild id can be none here 43 | guild_id = _obj.id if isinstance(_obj, discord.Guild) else _obj 44 | 45 | if guild_id is not None and not await CacheManager.match_bot_guild( 46 | guild_id, bot.user.id 47 | ): 48 | return 49 | 50 | return await fn(*args, **kwargs) 51 | 52 | return wrapper 53 | 54 | 55 | class event_bot_check: 56 | def __init__(self, bot_id: int): 57 | self.bot_id = bot_id 58 | 59 | def __call__(self, fn: Callable) -> Callable: 60 | @wraps(fn) 61 | async def wrapper(*args: Any, **kwargs: Any): 62 | bot_id: int = args[0].bot.user.id 63 | return await fn(*args, **kwargs) if bot_id == self.bot_id else None 64 | 65 | return wrapper 66 | 67 | 68 | class role_command_check: 69 | def __call__(self, fn: Callable) -> Callable: 70 | @wraps(fn) 71 | async def wrapper(*args: Any, **kwargs: Any): 72 | _, ctx, *role = args 73 | 74 | role: discord.Role = role[0] if isinstance(role, list) else role # type: ignore 75 | ctx: Context # type: ignore 76 | 77 | if role.managed: 78 | return await ctx.error(f"Role is an integrated role and cannot be added manually.") 79 | 80 | if ctx.me.top_role.position <= role.position: 81 | return await ctx.error( 82 | f"The position of {role.mention} is above my toprole ({ctx.me.top_role.mention})" 83 | ) 84 | 85 | if not ctx.author == ctx.guild.owner and ctx.author.top_role.position <= role.position: 86 | return await ctx.error( 87 | f"The position of {role.mention} is above your top role ({ctx.author.top_role.mention})" 88 | ) 89 | 90 | if role.permissions > ctx.author.guild_permissions: 91 | return await ctx.error(f"{role.mention} has higher permissions than you.") 92 | 93 | if role.permissions.administrator: 94 | return await ctx.error(f"{role.mention} has administrator permissions.") 95 | 96 | return await fn(*args, **kwargs) 97 | 98 | return wrapper 99 | -------------------------------------------------------------------------------- /src/core/views.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from contextlib import suppress 4 | from typing import Optional, TYPE_CHECKING 5 | 6 | import config 7 | import discord 8 | from utils import emote 9 | 10 | if TYPE_CHECKING: 11 | from .Context import Context 12 | 13 | __all__ = ("QuotientView", "QuoInput") 14 | 15 | 16 | class QuotientView(discord.ui.View): 17 | message: discord.Message 18 | custom_id = None 19 | 20 | def __init__(self, ctx: Context, *, timeout: Optional[float]=30): 21 | super().__init__(timeout=timeout) 22 | self.ctx = ctx 23 | self.bot = ctx.bot 24 | 25 | async def interaction_check(self, interaction: discord.Interaction) -> bool: 26 | if interaction.user.id != self.ctx.author.id: 27 | await interaction.response.send_message( 28 | "Sorry, you can't use this interaction as it is not started by you.", 29 | ephemeral=True, 30 | ) 31 | return False 32 | return True 33 | 34 | async def on_timeout(self) -> None: 35 | if hasattr(self, "message"): 36 | for b in self.children: 37 | if isinstance(b, discord.ui.Button) and not b.style == discord.ButtonStyle.link: 38 | b.style, b.disabled = discord.ButtonStyle.grey, True 39 | 40 | elif isinstance(b, discord.ui.Select): 41 | b.disabled = True 42 | 43 | with suppress(discord.HTTPException): 44 | await self.message.edit(view=self) 45 | return 46 | 47 | async def on_error(self, interaction: discord.Interaction, error: Exception, item) -> None: 48 | print("Quotient View Error:", error) 49 | self.ctx.bot.dispatch("command_error", self.ctx, error) 50 | 51 | @staticmethod 52 | def tricky_invite_button(): # yes lmao 53 | return discord.ui.Button(emoji=emote.info, url=config.SERVER_LINK) 54 | 55 | 56 | class QuoInput(discord.ui.Modal): 57 | def __init__(self, title: str): 58 | super().__init__(title=title) 59 | 60 | async def on_submit(self, interaction: discord.Interaction) -> None: 61 | with suppress(discord.NotFound): 62 | await interaction.response.defer() 63 | -------------------------------------------------------------------------------- /src/data/font/Ubuntu-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deadaf/Quotient-Bot/b5f88056889f1faef9d8c04d96684e47e3f79c3d/src/data/font/Ubuntu-Regular.ttf -------------------------------------------------------------------------------- /src/data/font/robo-bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deadaf/Quotient-Bot/b5f88056889f1faef9d8c04d96684e47e3f79c3d/src/data/font/robo-bold.ttf -------------------------------------------------------------------------------- /src/data/font/robo-italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deadaf/Quotient-Bot/b5f88056889f1faef9d8c04d96684e47e3f79c3d/src/data/font/robo-italic.ttf -------------------------------------------------------------------------------- /src/data/img/ptable1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deadaf/Quotient-Bot/b5f88056889f1faef9d8c04d96684e47e3f79c3d/src/data/img/ptable1.jpg -------------------------------------------------------------------------------- /src/data/img/ptable10.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deadaf/Quotient-Bot/b5f88056889f1faef9d8c04d96684e47e3f79c3d/src/data/img/ptable10.jpg -------------------------------------------------------------------------------- /src/data/img/ptable11.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deadaf/Quotient-Bot/b5f88056889f1faef9d8c04d96684e47e3f79c3d/src/data/img/ptable11.jpg -------------------------------------------------------------------------------- /src/data/img/ptable12.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deadaf/Quotient-Bot/b5f88056889f1faef9d8c04d96684e47e3f79c3d/src/data/img/ptable12.jpg -------------------------------------------------------------------------------- /src/data/img/ptable13.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deadaf/Quotient-Bot/b5f88056889f1faef9d8c04d96684e47e3f79c3d/src/data/img/ptable13.jpg -------------------------------------------------------------------------------- /src/data/img/ptable14.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deadaf/Quotient-Bot/b5f88056889f1faef9d8c04d96684e47e3f79c3d/src/data/img/ptable14.jpg -------------------------------------------------------------------------------- /src/data/img/ptable15.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deadaf/Quotient-Bot/b5f88056889f1faef9d8c04d96684e47e3f79c3d/src/data/img/ptable15.jpg -------------------------------------------------------------------------------- /src/data/img/ptable16.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deadaf/Quotient-Bot/b5f88056889f1faef9d8c04d96684e47e3f79c3d/src/data/img/ptable16.jpg -------------------------------------------------------------------------------- /src/data/img/ptable17.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deadaf/Quotient-Bot/b5f88056889f1faef9d8c04d96684e47e3f79c3d/src/data/img/ptable17.jpg -------------------------------------------------------------------------------- /src/data/img/ptable18.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deadaf/Quotient-Bot/b5f88056889f1faef9d8c04d96684e47e3f79c3d/src/data/img/ptable18.jpg -------------------------------------------------------------------------------- /src/data/img/ptable19.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deadaf/Quotient-Bot/b5f88056889f1faef9d8c04d96684e47e3f79c3d/src/data/img/ptable19.jpg -------------------------------------------------------------------------------- /src/data/img/ptable2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deadaf/Quotient-Bot/b5f88056889f1faef9d8c04d96684e47e3f79c3d/src/data/img/ptable2.jpg -------------------------------------------------------------------------------- /src/data/img/ptable20.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deadaf/Quotient-Bot/b5f88056889f1faef9d8c04d96684e47e3f79c3d/src/data/img/ptable20.jpg -------------------------------------------------------------------------------- /src/data/img/ptable3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deadaf/Quotient-Bot/b5f88056889f1faef9d8c04d96684e47e3f79c3d/src/data/img/ptable3.jpg -------------------------------------------------------------------------------- /src/data/img/ptable4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deadaf/Quotient-Bot/b5f88056889f1faef9d8c04d96684e47e3f79c3d/src/data/img/ptable4.jpg -------------------------------------------------------------------------------- /src/data/img/ptable5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deadaf/Quotient-Bot/b5f88056889f1faef9d8c04d96684e47e3f79c3d/src/data/img/ptable5.jpg -------------------------------------------------------------------------------- /src/data/img/ptable6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deadaf/Quotient-Bot/b5f88056889f1faef9d8c04d96684e47e3f79c3d/src/data/img/ptable6.jpg -------------------------------------------------------------------------------- /src/data/img/ptable7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deadaf/Quotient-Bot/b5f88056889f1faef9d8c04d96684e47e3f79c3d/src/data/img/ptable7.jpg -------------------------------------------------------------------------------- /src/data/img/ptable8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deadaf/Quotient-Bot/b5f88056889f1faef9d8c04d96684e47e3f79c3d/src/data/img/ptable8.jpg -------------------------------------------------------------------------------- /src/data/img/ptable9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deadaf/Quotient-Bot/b5f88056889f1faef9d8c04d96684e47e3f79c3d/src/data/img/ptable9.jpg -------------------------------------------------------------------------------- /src/data/img/rect2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deadaf/Quotient-Bot/b5f88056889f1faef9d8c04d96684e47e3f79c3d/src/data/img/rect2.png -------------------------------------------------------------------------------- /src/data/img/rect3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deadaf/Quotient-Bot/b5f88056889f1faef9d8c04d96684e47e3f79c3d/src/data/img/rect3.png -------------------------------------------------------------------------------- /src/data/img/rectangle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deadaf/Quotient-Bot/b5f88056889f1faef9d8c04d96684e47e3f79c3d/src/data/img/rectangle.png -------------------------------------------------------------------------------- /src/example_config.py: -------------------------------------------------------------------------------- 1 | # for tortoise-orm 2 | 3 | TORTOISE = {} 4 | 5 | 6 | POSTGRESQL = {} 7 | 8 | EXTENSIONS = () 9 | 10 | DISCORD_TOKEN = "" 11 | 12 | COLOR = 0x00FFB3 13 | 14 | FOOTER = "quo is lub!" 15 | 16 | PREFIX = "q" 17 | 18 | SERVER_LINK = "" 19 | 20 | BOT_INVITE = "" 21 | 22 | WEBSITE = "" 23 | 24 | REPOSITORY = "" 25 | 26 | DEVS = () 27 | 28 | # LOGS 29 | SHARD_LOG = "" 30 | ERROR_LOG = "" 31 | PUBLIC_LOG = "" 32 | -------------------------------------------------------------------------------- /src/models/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import typing 4 | 5 | if typing.TYPE_CHECKING: 6 | from core import Quotient 7 | 8 | from tortoise import models 9 | 10 | 11 | class BaseDbModel(models.Model): 12 | """Base Model for all tortoise models""" 13 | 14 | class Meta: 15 | abstract = True 16 | 17 | bot: Quotient 18 | 19 | 20 | from .esports import * 21 | from .helpers import * 22 | from .misc import * 23 | -------------------------------------------------------------------------------- /src/models/esports/__init__.py: -------------------------------------------------------------------------------- 1 | from .scrims import * 2 | from .slotm import * 3 | from .ssverify import * 4 | from .tagcheck import * 5 | from .tourney import * 6 | -------------------------------------------------------------------------------- /src/models/esports/ptable.py: -------------------------------------------------------------------------------- 1 | from tortoise import fields, models 2 | 3 | from models.helpers import * 4 | 5 | 6 | class PtableTourney(models.Model): 7 | class Meta: 8 | table = "ptable_tourney" 9 | 10 | id = fields.IntField(pk=True) 11 | guild_id = fields.BigIntField() 12 | associative_id = fields.CharField(max_length=10) 13 | title = fields.CharField(max_length=100) 14 | secondary_title = fields.CharField(max_length=100) 15 | footer = fields.CharField(max_length=100) 16 | 17 | per_kill = fields.IntField(default=0) 18 | postion_pts = fields.JSONField(default=dict) 19 | 20 | background_image = fields.CharField(max_length=200) 21 | colors = fields.JSONField(default=dict) 22 | 23 | created_at = fields.DatetimeField(auto_now_add=True) 24 | teams: fields.ManyToManyRelation["PtableTeam"] = fields.ManyToManyField("models.PtableTeam") 25 | matches: fields.ManyToManyRelation["PtableMatch"] = fields.ManyToManyField("models.PtableMatch") 26 | 27 | 28 | class PtableTeam(models.Model): 29 | class Meta: 30 | table = "ptable_teams" 31 | 32 | id = fields.IntField(pk=True) 33 | team_name = fields.CharField(max_length=100) 34 | email = fields.CharField(max_length=100, null=True) 35 | phone = fields.CharField(max_length=10, null=True) 36 | logo = fields.CharField(max_length=200, null=True) 37 | added_by = fields.BigIntField() 38 | team_owner = fields.BigIntField() 39 | players = ArrayField(fields.BigIntField(default=list)) 40 | last_used = fields.DatetimeField(auto_now=True) 41 | 42 | 43 | class PtableMatch(models.Model): 44 | class Meta: 45 | table = "ptable_match" 46 | 47 | id = fields.IntField(pk=True) 48 | name = fields.CharField(max_length=100) 49 | created_at = fields.DatetimeField(auto_now=True) 50 | created_by = fields.BigIntField() 51 | results = fields.JSONField() 52 | -------------------------------------------------------------------------------- /src/models/esports/reserve.py: -------------------------------------------------------------------------------- 1 | from tortoise import fields, models 2 | 3 | from models.helpers import * 4 | 5 | 6 | class Subscriptions(models.Model): 7 | class Meta: 8 | table = "subscriptions" 9 | 10 | guild_id = fields.BigIntField(pk=True) 11 | log_channel_id = fields.BigIntField() 12 | slug = fields.CharField(max_length=20) 13 | balance = fields.IntField(default=0) 14 | upi_id = fields.CharField(max_length=25) 15 | 16 | plans: fields.ManyToManyRelation["SubPlan"] = fields.ManyToManyField("models.SubPlan") 17 | logs: fields.ManyToManyRelation["SubLog"] = fields.ManyToManyField("models.SubLog") 18 | 19 | 20 | class SubPlan(models.Model): 21 | class Meta: 22 | table = "subscription_plans" 23 | 24 | id = fields.IntField(pk=True) 25 | name = fields.CharField(max_length=30) 26 | theme_color = fields.CharField(max_length=7) 27 | days = fields.IntField(default=1) 28 | slots = fields.IntField(default=1) 29 | perks = ArrayField(fields.CharField(max_length=100)) 30 | price = fields.IntField() 31 | 32 | 33 | class SubLog(models.Model): 34 | class Meta: 35 | table = "subscription_logs" 36 | 37 | id = fields.IntField(pk=True) 38 | -------------------------------------------------------------------------------- /src/models/esports/tagcheck.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | import discord 4 | from tortoise import fields 5 | 6 | from models import BaseDbModel 7 | from models.helpers import * 8 | 9 | 10 | class TagCheck(BaseDbModel): 11 | class Meta: 12 | table = "tagcheck" 13 | 14 | id = fields.BigIntField(pk=True) 15 | guild_id = fields.BigIntField() 16 | channel_id = fields.BigIntField() 17 | required_mentions = fields.IntField(default=0) 18 | delete_after = fields.BooleanField(default=False) 19 | 20 | @property 21 | def _guild(self) -> Optional[discord.Guild]: 22 | return self.bot.get_guild(self.guild_id) 23 | 24 | @property 25 | def channel(self) -> Optional[discord.TextChannel]: 26 | return self.bot.get_channel(self.channel_id) 27 | 28 | @property 29 | def ignorerole(self) -> Optional[discord.Role]: 30 | if not self._guild is None: 31 | return discord.utils.get(self._guild.roles, name="quotient-tag-ignore") 32 | 33 | def __str__(self): 34 | return f"{getattr(self.channel,'mention','channel-not-found')} (Mentions: `{self.required_mentions}`)" 35 | 36 | 37 | class EasyTag(BaseDbModel): 38 | class Meta: 39 | table = "easytags" 40 | 41 | id = fields.BigIntField(pk=True) 42 | guild_id = fields.BigIntField() 43 | channel_id = fields.BigIntField(index=True) 44 | delete_after = fields.BooleanField(default=False) 45 | 46 | @property 47 | def _guild(self) -> Optional[discord.Guild]: 48 | return self.bot.get_guild(self.guild_id) 49 | 50 | @property 51 | def channel(self) -> Optional[discord.TextChannel]: 52 | return self.bot.get_channel(self.channel_id) 53 | 54 | @property 55 | def ignorerole(self) -> Optional[discord.Role]: 56 | if not self._guild is None: 57 | return discord.utils.get(self._guild.roles, name="quotient-tag-ignore") 58 | -------------------------------------------------------------------------------- /src/models/helpers/__init__.py: -------------------------------------------------------------------------------- 1 | from .cfields import * # noqa: F401, F403 2 | from .functions import * # noqa: F401, F403 3 | from .validators import * # noqa: F401, F403 4 | -------------------------------------------------------------------------------- /src/models/helpers/cfields.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from tortoise.fields.base import Field 4 | 5 | 6 | class ArrayField(Field, list): 7 | def __init__(self, field: Field, **kwargs) -> None: 8 | super().__init__(**kwargs) 9 | self.sub_field = field 10 | self.SQL_TYPE = "%s[]" % field.SQL_TYPE 11 | 12 | def to_python_value(self, value: Any) -> Any: 13 | return list(map(self.sub_field.to_python_value, value)) 14 | 15 | def to_db_value(self, value: Any, instance: Any) -> Any: 16 | return [self.sub_field.to_db_value(val, instance) for val in value] 17 | -------------------------------------------------------------------------------- /src/models/helpers/functions.py: -------------------------------------------------------------------------------- 1 | import typing 2 | from enum import Enum 3 | 4 | from pypika.terms import Function 5 | from tortoise.expressions import F 6 | 7 | __all__ = ( 8 | "ArrayAppend", 9 | "ArrayRemove", 10 | ) 11 | 12 | 13 | class ArrayAppend(Function): 14 | def __init__(self, field: str, value: typing.Any) -> None: 15 | if isinstance(value, Enum): 16 | value = value.value 17 | 18 | super().__init__("ARRAY_APPEND", F(field), str(value)) 19 | 20 | 21 | class ArrayRemove(Function): 22 | def __init__(self, field: str, value: typing.Any) -> None: 23 | if isinstance(value, Enum): 24 | value = value.value 25 | 26 | super().__init__("ARRAY_REMOVE", F(field), str(value)) 27 | -------------------------------------------------------------------------------- /src/models/helpers/validators.py: -------------------------------------------------------------------------------- 1 | from tortoise.exceptions import ValidationError 2 | from tortoise.validators import Validator 3 | 4 | 5 | class ValueRangeValidator(Validator): 6 | """ 7 | A validator to validate whether the given value is in given range or not. 8 | """ 9 | 10 | def __init__(self, _range: range): 11 | self._range = _range 12 | 13 | def __call__(self, value: int): 14 | if not value in self._range: 15 | raise ValidationError(f"The value must be a number between `{self._range.start}` and `{self._range.stop}`.") 16 | -------------------------------------------------------------------------------- /src/models/misc/AutoPurge.py: -------------------------------------------------------------------------------- 1 | from tortoise import fields, models 2 | 3 | 4 | class AutoPurge(models.Model): 5 | class Meta: 6 | table = "autopurge" 7 | 8 | id = fields.BigIntField(pk=True) 9 | guild_id = fields.BigIntField() 10 | channel_id = fields.BigIntField() 11 | delete_after = fields.IntField(default=10) 12 | 13 | @property 14 | def channel(self): 15 | return self.bot.get_channel(self.channel_id) 16 | -------------------------------------------------------------------------------- /src/models/misc/Autorole.py: -------------------------------------------------------------------------------- 1 | from models.helpers import ArrayField 2 | from tortoise import fields, models 3 | from typing import Optional 4 | import discord 5 | 6 | 7 | class Autorole(models.Model): 8 | class Meta: 9 | table = "autoroles" 10 | 11 | guild_id = fields.BigIntField(pk=True, index=True) 12 | humans = ArrayField(fields.BigIntField(), default=list) 13 | bots = ArrayField(fields.BigIntField(), default=list) 14 | 15 | @property 16 | def _guild(self) -> Optional[discord.Guild]: 17 | return self.bot.get_guild(self.guild_id) 18 | 19 | @property 20 | def human_roles(self): 21 | if self._guild is not None: 22 | return tuple(map(lambda x: getattr(self._guild.get_role(x), "mention", "Deleted"), self.humans)) 23 | 24 | @property 25 | def bot_roles(self): 26 | if self._guild is not None: 27 | return tuple(map(lambda x: getattr(self._guild.get_role(x), "mention", "Deleted"), self.bots)) 28 | -------------------------------------------------------------------------------- /src/models/misc/Commands.py: -------------------------------------------------------------------------------- 1 | from tortoise import fields, models 2 | 3 | 4 | class Commands(models.Model): 5 | class Meta: 6 | table = "commands" 7 | 8 | id = fields.BigIntField(pk=True) 9 | guild_id = fields.BigIntField(index=True) 10 | channel_id = fields.BigIntField() 11 | user_id = fields.BigIntField(index=True) 12 | cmd = fields.CharField(max_length=100, index=True) 13 | used_at = fields.DatetimeField(auto_now=True) 14 | prefix = fields.CharField(max_length=100) 15 | failed = fields.BooleanField(default=False) 16 | -------------------------------------------------------------------------------- /src/models/misc/Lockdown.py: -------------------------------------------------------------------------------- 1 | import constants 2 | from models.helpers import ArrayField 3 | from tortoise import fields, models 4 | 5 | 6 | class Lockdown(models.Model): 7 | class Meta: 8 | table = "lockdown" 9 | 10 | id = fields.BigIntField(pk=True) 11 | guild_id = fields.BigIntField(index=True) 12 | type = fields.CharEnumField(constants.LockType, max_length=20) 13 | role_id = fields.BigIntField(null=True) 14 | channel_id = fields.BigIntField(null=True) 15 | channel_ids = ArrayField(fields.BigIntField(), default=list, index=True) 16 | expire_time = fields.DatetimeField(null=True) 17 | author_id = fields.BigIntField() 18 | 19 | @property 20 | def _guild(self): 21 | return self.bot.get_guild(self.guild_id) 22 | 23 | @property 24 | def roles(self): 25 | if self._guild is not None: 26 | return self._guild.get_role(self.role_id) 27 | 28 | @property 29 | def channels(self): 30 | return map(self.bot.get_channel, self.channel_ids) 31 | -------------------------------------------------------------------------------- /src/models/misc/Snipe.py: -------------------------------------------------------------------------------- 1 | from tortoise import fields, models 2 | 3 | 4 | class Snipe(models.Model): 5 | class Meta: 6 | table = "snipes" 7 | 8 | channel_id = fields.BigIntField(pk=True) 9 | author_id = fields.BigIntField() 10 | content = fields.TextField() 11 | delete_time = fields.DatetimeField(auto_now=True) 12 | nsfw = fields.BooleanField(default=False) 13 | 14 | @property 15 | def author(self): 16 | return self.bot.get_user(self.author_id) 17 | -------------------------------------------------------------------------------- /src/models/misc/Tag.py: -------------------------------------------------------------------------------- 1 | from tortoise import fields, models 2 | 3 | 4 | class Tag(models.Model): 5 | class Meta: 6 | table = "tags" 7 | 8 | id = fields.BigIntField(pk=True) 9 | guild_id = fields.BigIntField() 10 | name = fields.CharField(max_length=100) 11 | content = fields.TextField() 12 | is_embed = fields.BooleanField(default=False) 13 | is_nsfw = fields.BooleanField(default=False) 14 | owner_id = fields.BigIntField() 15 | created_at = fields.DatetimeField(auto_now=True) 16 | usage = fields.IntField(default=0) 17 | 18 | @property 19 | def owner(self): 20 | return self.bot.get_user(self.owner_id) 21 | -------------------------------------------------------------------------------- /src/models/misc/Timer.py: -------------------------------------------------------------------------------- 1 | from tortoise import fields, models 2 | 3 | 4 | class Timer(models.Model): 5 | id = fields.BigIntField(pk=True) 6 | expires = fields.DatetimeField(index=True) 7 | created = fields.DatetimeField(auto_now=True) 8 | event = fields.TextField() 9 | extra = fields.JSONField(default=dict) 10 | 11 | @property 12 | def kwargs(self): 13 | return self.extra.get("kwargs", {}) 14 | 15 | @property 16 | def args(self): 17 | return self.extra.get("args", ()) 18 | -------------------------------------------------------------------------------- /src/models/misc/User.py: -------------------------------------------------------------------------------- 1 | from tortoise import fields, models 2 | from models.helpers import ArrayField 3 | 4 | 5 | # TODO: make manytomany field in user_data for redeem codes. 6 | class User(models.Model): 7 | class Meta: 8 | table = "user_data" 9 | 10 | user_id = fields.BigIntField(pk=True, index=True) 11 | is_premium = fields.BooleanField(default=False, index=True) 12 | premium_expire_time = fields.DatetimeField(null=True) 13 | made_premium = ArrayField(fields.BigIntField(), default=list) # a list of servers this user boosted 14 | premiums = fields.IntField(default=0) 15 | premium_notified = fields.BooleanField(default=False) 16 | public_profile = fields.BooleanField(default=True) 17 | # badges = CharVarArrayField(default=list) 18 | money = fields.IntField(default=0) -------------------------------------------------------------------------------- /src/models/misc/Votes.py: -------------------------------------------------------------------------------- 1 | from tortoise import fields, models 2 | 3 | 4 | class Votes(models.Model): 5 | class Meta: 6 | table = "votes" 7 | 8 | user_id = fields.BigIntField(pk=True) 9 | is_voter = fields.BooleanField(default=False, index=True) 10 | expire_time = fields.DatetimeField(null=True) 11 | reminder = fields.BooleanField(default=False) 12 | notified = fields.BooleanField(default=False, index=True) 13 | public_profile = fields.BooleanField(default=True) 14 | total_votes = fields.IntField(default=0) 15 | -------------------------------------------------------------------------------- /src/models/misc/__init__.py: -------------------------------------------------------------------------------- 1 | from .guild import * # noqa: F401, F403 2 | from .alerts import * 3 | from .premium import * 4 | from .AutoPurge import * 5 | from .Autorole import * 6 | from .Commands import * 7 | from .Lockdown import * 8 | from .Snipe import * 9 | from .Tag import * 10 | from .Timer import * 11 | from .User import * 12 | from .Votes import * 13 | -------------------------------------------------------------------------------- /src/models/misc/alerts.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from models import BaseDbModel 4 | from tortoise import fields 5 | from models.helpers import * 6 | 7 | 8 | class Alert(BaseDbModel): 9 | class Meta: 10 | table = "alerts" 11 | 12 | id = fields.IntField(pk=True) 13 | author_id = fields.BigIntField() 14 | created_at = fields.DatetimeField(auto_now=True) 15 | active = fields.BooleanField(default=True) 16 | message = fields.JSONField(default=dict) 17 | conditions = ArrayField(fields.CharField(max_length=100), default=list) 18 | prompts: fields.ManyToManyRelation["Prompt"] = fields.ManyToManyField("models.Prompt") 19 | reads: fields.ManyToManyRelation["Read"] = fields.ManyToManyField("models.Read") 20 | 21 | 22 | class Prompt(BaseDbModel): 23 | class Meta: 24 | table = "alert_prompts" 25 | 26 | id = fields.IntField(pk=True) 27 | user_id = fields.BigIntField() 28 | prompted_at = fields.DatetimeField(auto_now=True) 29 | 30 | 31 | class Read(BaseDbModel): 32 | class Meta: 33 | table = "alert_reads" 34 | 35 | id = fields.IntField(pk=True) 36 | user_id = fields.BigIntField() 37 | read_at = fields.DatetimeField(auto_now=True) 38 | -------------------------------------------------------------------------------- /src/models/misc/guild.py: -------------------------------------------------------------------------------- 1 | import discord 2 | from tortoise import fields 3 | 4 | import config 5 | from models import BaseDbModel 6 | from models.helpers import * 7 | 8 | _dict = {"embed": [], "scrims": [], "tourney": [], "slotm": []} 9 | 10 | 11 | class Guild(BaseDbModel): 12 | class Meta: 13 | table = "guild_data" 14 | 15 | guild_id = fields.BigIntField(pk=True, index=True) 16 | 17 | prefix = fields.CharField(default="q", max_length=5) 18 | embed_color = fields.IntField(default=65459, null=True) 19 | embed_footer = fields.TextField(default=config.FOOTER) 20 | 21 | tag_enabled_for_everyone = fields.BooleanField(default=True) # ye naam maine ni rkha sachi 22 | 23 | is_premium = fields.BooleanField(default=False) 24 | made_premium_by = fields.BigIntField(null=True) 25 | premium_end_time = fields.DatetimeField(null=True) 26 | premium_notified = fields.BooleanField(default=False) 27 | 28 | public_profile = fields.BooleanField(default=True) # whether to list the server on global leaderboards 29 | 30 | private_channel = fields.BigIntField(null=True) 31 | 32 | dashboard_access = fields.JSONField(default=_dict) 33 | 34 | @property 35 | def _guild(self) -> discord.Guild: 36 | return self.bot.get_guild(self.guild_id) 37 | 38 | @property 39 | def private_ch(self) -> discord.TextChannel: 40 | if (g := self._guild) is not None: 41 | return g.get_channel(self.private_channel) 42 | 43 | @property 44 | def booster(self): 45 | return self.bot.get_user(self.made_premium_by) 46 | -------------------------------------------------------------------------------- /src/models/misc/premium.py: -------------------------------------------------------------------------------- 1 | from models import BaseDbModel 2 | from datetime import timedelta 3 | from tortoise import fields 4 | import os 5 | 6 | __all__ = ("PremiumTxn", "PremiumPlan") 7 | 8 | 9 | class PremiumPlan(BaseDbModel): 10 | class Meta: 11 | table = "premium_plans" 12 | 13 | id = fields.IntField(pk=True) 14 | name = fields.CharField(max_length=50) 15 | description = fields.CharField(max_length=250, null=True) 16 | price = fields.IntField() 17 | duration = fields.TimeDeltaField() 18 | 19 | @staticmethod 20 | async def insert_plans(): 21 | await PremiumPlan.all().delete() 22 | await PremiumPlan.create(name="Trial (7d)", description="Duration: 7 days", price=29, duration=timedelta(days=7)) 23 | await PremiumPlan.create( 24 | name="Basic (1m)", description="Duration: 28 days", price=79, duration=timedelta(days=28) 25 | ) 26 | await PremiumPlan.create( 27 | name="Professional (3m)", description="Duration: 84 days", price=229, duration=timedelta(days=84) 28 | ) 29 | await PremiumPlan.create( 30 | name="Enterprise (6m)", description="Duration: 168 days", price=469, duration=timedelta(days=168) 31 | ) 32 | await PremiumPlan.create( 33 | name="GodLike (Lifetime)", description="Duration: 69 years", price=4999, duration=timedelta(days=25185) 34 | ) 35 | 36 | 37 | class PremiumTxn(BaseDbModel): 38 | class Meta: 39 | table = "premium_txns" 40 | 41 | id = fields.IntField(pk=True) 42 | txnid = fields.CharField(max_length=100) 43 | user_id = fields.BigIntField() 44 | guild_id = fields.BigIntField() 45 | plan_id = fields.IntField() 46 | 47 | created_at = fields.DatetimeField(auto_now=True) 48 | completed_at = fields.DatetimeField(null=True) 49 | raw_data = fields.JSONField(default=dict) 50 | 51 | @staticmethod 52 | async def gen_txnid() -> str: 53 | txnid = None 54 | 55 | while txnid == None: 56 | _id = "QP_" + os.urandom(16).hex() 57 | if not await PremiumTxn.filter(txnid=_id).exists(): 58 | txnid = _id 59 | 60 | return txnid 61 | -------------------------------------------------------------------------------- /src/server/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import typing as T 3 | 4 | if T.TYPE_CHECKING: 5 | from core import Quotient 6 | 7 | from discord.ext.commands import Cog 8 | from aiohttp import web 9 | from aiohttp_asgi import ASGIResource 10 | from .app import fastapi_app 11 | import config 12 | 13 | 14 | class ApiServer(Cog): 15 | app: web.Application 16 | app_started: bool = False 17 | webserver: web.TCPSite 18 | 19 | def __init__(self, bot: Quotient): 20 | self.bot = bot 21 | 22 | async def cog_load(self) -> None: 23 | self.app, self.webserver = await self.init_application() 24 | self.app_started = True 25 | 26 | async def cog_unload(self) -> None: 27 | if self.app_started: 28 | print("[Server] Closing...") 29 | await self.webserver.stop() 30 | await self.app.shutdown() 31 | await self.app.cleanup() 32 | 33 | async def init_application(self) -> T.Tuple[web.Application, web.TCPSite]: 34 | aiohttp_app = web.Application() 35 | asgi_resource = ASGIResource(fastapi_app, root_path="") # type: ignore 36 | aiohttp_app.router.register_resource(asgi_resource) 37 | 38 | runner = web.AppRunner(app=aiohttp_app) 39 | await runner.setup() 40 | webserver = web.TCPSite(runner, "0.0.0.0", config.SERVER_PORT) 41 | await webserver.start() 42 | print("[Server] Asgi server started") 43 | return aiohttp_app, webserver 44 | 45 | 46 | async def setup(bot: Quotient): 47 | await bot.add_cog(ApiServer(bot)) 48 | -------------------------------------------------------------------------------- /src/server/app/__init__.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | 3 | fastapi_app = FastAPI() 4 | 5 | 6 | @fastapi_app.get("/") 7 | async def root(): 8 | return {"ping": "pong"} 9 | 10 | 11 | from .payment import router as payment_router 12 | 13 | fastapi_app.include_router(payment_router) 14 | -------------------------------------------------------------------------------- /src/server/app/payment.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Request 2 | from fastapi.templating import Jinja2Templates 3 | import hashlib 4 | import config 5 | from models import PremiumTxn, PremiumPlan, User, Guild, ArrayAppend 6 | import constants 7 | from datetime import datetime 8 | 9 | router = APIRouter() 10 | template = Jinja2Templates(directory="src/server/templates") 11 | 12 | 13 | def create_hash(txnId: str, amount: str, productInfo: str, firstName: str, email: str): 14 | # sha512(key|txnid|amount|productinfo|firstname|email|udf1|udf2|udf3|udf4|udf5||||||salt) 15 | 16 | return hashlib.sha512( 17 | f"{config.PAYU_KEY}|{txnId}|{amount}|{productInfo}|{firstName}|{email}|||||||||||{config.PAYU_SALT}".encode( 18 | "utf-8" 19 | ) 20 | ).hexdigest() 21 | 22 | 23 | @router.get("/getpremium") 24 | async def get_premium(request: Request, txnId: str): 25 | record = await PremiumTxn.get_or_none(txnid=txnId) 26 | 27 | if not record: 28 | return {"error": "Invalid Transaction ID"} 29 | 30 | if record.completed_at: 31 | return {"error": "Transaction already completed"} 32 | 33 | plan = await PremiumPlan.get(pk=record.plan_id) 34 | 35 | payu_hash = create_hash(txnId, plan.price, "premium", record.user_id, "abcd@gmail.com") 36 | 37 | data = { 38 | "key": config.PAYU_KEY, 39 | "txnid": txnId, 40 | "amount": plan.price, 41 | "productinfo": "premium", 42 | "firstname": record.user_id, 43 | "email": "abcd@gmail.com", 44 | "surl": f"{config.SUCCESS_URL}{txnId}", 45 | "furl": f"{config.FAILED_URL}{txnId}", 46 | "phone": "9999999999", 47 | "action": config.PAYU_PAYMENT_LINK, 48 | "hash": payu_hash, 49 | } 50 | 51 | return template.TemplateResponse("payu.html", {"request": request, "posted": data}) 52 | 53 | 54 | @router.post("/premium_success") 55 | async def premium_success(request: Request, txnId: str): 56 | from core import bot 57 | 58 | try: 59 | form = await request.form() 60 | except: 61 | return {"error": "Invalid Request."} 62 | 63 | if not "payu" in request.headers.get("origin"): 64 | return {"error": "Invalid Request Origin."} 65 | 66 | if not form.get("status") == "success": 67 | return {"error": f"Transaction Status: {form.get('status')}"} 68 | 69 | record = await PremiumTxn.get_or_none(txnid=txnId) 70 | if not record: 71 | return {"error": "Transaction Id is invalid."} 72 | 73 | if record.completed_at: 74 | return {"error": "Transaction is already complete."} 75 | 76 | await PremiumTxn.get(txnid=txnId).update(raw_data=dict(form), completed_at=datetime.now(constants.IST)) 77 | u, b = await User.get_or_create(user_id=record.user_id) 78 | plan = await PremiumPlan.get(pk=record.plan_id) 79 | 80 | end_time = u.premium_expire_time + plan.duration if u.is_premium else datetime.now(constants.IST) + plan.duration 81 | 82 | await User.get(pk=u.pk).update(is_premium=True, premium_expire_time=end_time) 83 | await User.get(pk=u.user_id).update(made_premium=ArrayAppend("made_premium", u.user_id)) 84 | 85 | bot.dispatch("premium_purchase", record.txnid) 86 | 87 | guild = await Guild.get(pk=record.guild_id) 88 | end_time = guild.premium_end_time + plan.duration if guild.is_premium else datetime.now(constants.IST) + plan.duration 89 | await Guild.get(pk=guild.pk).update(is_premium=True, premium_end_time=end_time, made_premium_by=u.user_id) 90 | 91 | return {"success": "Transaction was successful. Please return to discord App."} 92 | 93 | 94 | @router.post("/premium_failed") 95 | async def premium_failed(request: Request, txnId: str): 96 | try: 97 | form = await request.form() 98 | except: 99 | return {"error": "Invalid Request."} 100 | 101 | if not "payu" in request.headers.get("origin"): 102 | return {"error": "Invalid Request Origin."} 103 | 104 | await PremiumTxn.get(txnid=txnId).update(completed_at=datetime.now(constants.IST), raw_data=dict(form)) 105 | 106 | return {"error": "Transaction Cancelled."} 107 | -------------------------------------------------------------------------------- /src/server/templates/payu.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Loading... 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | -------------------------------------------------------------------------------- /src/server/templates/response.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Payment Response 8 | 42 | 43 | 44 | 45 |
46 | {% if success %} 47 |

Transaction was Successful!

48 | 49 |

Please return to the Discord App.

50 | {% else %} 51 |

An Error occurred during the Transaction.

52 | 53 |

Please try again later or contact support.

54 | {% endif %} 55 |
56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /src/sockets/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import typing 4 | 5 | if typing.TYPE_CHECKING: 6 | from core import Quotient 7 | 8 | from socketio import AsyncClient 9 | 10 | from core import Cog 11 | 12 | from .app import sio 13 | from .events import (DashboardGate, SocketScrims, SockGuild, SockPrime, 14 | SockSettings) 15 | 16 | 17 | class SocketConnection(Cog): 18 | connected: bool = False 19 | sio: AsyncClient 20 | 21 | def __init__(self, bot: Quotient): 22 | self.bot = bot 23 | self.task = self.bot.loop.create_task(self.__make_connection()) 24 | 25 | def cog_unload(self) -> None: 26 | self.bot.loop.create_task(self.__close_connection()) 27 | 28 | async def __make_connection(self): 29 | await sio.connect(self.bot.config.SOCKET_URL, auth={"token": self.bot.config.SOCKET_AUTH}) 30 | 31 | sio.bot, self.bot.sio = self.bot, sio 32 | self.connected = True 33 | 34 | async def __close_connection(self): 35 | if self.connected: 36 | await sio.disconnect() 37 | self.connected = False 38 | 39 | 40 | async def setup(bot: Quotient): 41 | await bot.add_cog(SocketConnection(bot)) 42 | await bot.add_cog(DashboardGate(bot)) 43 | await bot.add_cog(SocketScrims(bot)) 44 | await bot.add_cog(SockSettings(bot)) 45 | await bot.add_cog(SockPrime(bot)) 46 | await bot.add_cog(SockGuild(bot)) 47 | -------------------------------------------------------------------------------- /src/sockets/app/__init__.py: -------------------------------------------------------------------------------- 1 | from .app import sio 2 | -------------------------------------------------------------------------------- /src/sockets/app/app.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import typing 4 | 5 | if typing.TYPE_CHECKING: 6 | from core import Quotient 7 | 8 | import socketio 9 | 10 | 11 | class QuoSocket(socketio.AsyncClient): 12 | bot: Quotient 13 | 14 | def __init__(self, **kwargs): 15 | super().__init__(**kwargs) 16 | 17 | async def emit(self, event, data=None, namespace=None, callback=None): 18 | return await super().emit( 19 | "response__" + event, data=data, namespace=namespace, callback=callback 20 | ) 21 | 22 | async def request(self, event, data=None, namespace=None, callback=None): 23 | return await super().emit(event, data=data, namespace=namespace, callback=callback) 24 | 25 | @staticmethod 26 | def int_parse(data): 27 | if not isinstance(data, dict): 28 | return data 29 | 30 | for x, y in data.items(): 31 | if isinstance(y, str) and y.isdigit(): 32 | data[x] = int(y) 33 | 34 | return data 35 | 36 | 37 | sio = QuoSocket(logger=True, engineio_logger=True) 38 | ignored = ("update_total_votes", "update_votes_leaderboard") 39 | 40 | 41 | @sio.on("*") 42 | async def catch_all(event, data): 43 | if event in ignored: 44 | return 45 | 46 | data = QuoSocket.int_parse(data) 47 | 48 | r, e, u = event.split("__") 49 | data["user__id"] = u 50 | sio.bot.dispatch(r + "__" + e, u, data) 51 | -------------------------------------------------------------------------------- /src/sockets/events/__init__.py: -------------------------------------------------------------------------------- 1 | from .dashgate import * # noqa: F401, F403 2 | from .scrims import * # noqa: F401, F403 3 | from .settings import * # noqa: F401, F403 4 | from .premium import * # noqa: F401, F403 5 | from .guilds import * # noqa: F401, F403 6 | -------------------------------------------------------------------------------- /src/sockets/events/dashgate.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING 4 | 5 | if TYPE_CHECKING: 6 | from core import Quotient 7 | 8 | from core import Cog 9 | from models import Guild 10 | 11 | __all__ = ("DashboardGate",) 12 | 13 | 14 | class DashboardGate(Cog): 15 | def __init__(self, bot: Quotient): 16 | self.bot = bot 17 | 18 | @Cog.listener() 19 | async def on_request__latency(self, u, data): 20 | return await self.bot.sio.emit("latency__{0}".format(u), {}) 21 | 22 | @Cog.listener() 23 | async def on_request__guild_permissions(self, u, data): 24 | 25 | guild_ids = data["guild_ids"] 26 | user_id = data["user_id"] 27 | 28 | result = {} 29 | 30 | for guild_id in guild_ids: 31 | guild_id = int(guild_id) 32 | 33 | guild = self.bot.get_guild(guild_id) 34 | if not guild: 35 | result[guild_id] = -1 36 | continue 37 | 38 | if not guild.chunked: 39 | self.bot.loop.create_task(guild.chunk()) 40 | 41 | member = await self.bot.get_or_fetch_member(guild, user_id) 42 | if not member: 43 | result[guild_id] = -1 44 | continue 45 | 46 | perms = 1 47 | 48 | if member.guild_permissions.manage_guild: 49 | result[guild_id] = 2 50 | continue 51 | 52 | g_record = await Guild.get(pk=guild_id) 53 | _roles = [str(_.id) for _ in member.roles] 54 | 55 | if any(i in g_record.dashboard_access["embed"] for i in _roles): 56 | perms *= 3 57 | 58 | if any(i in g_record.dashboard_access["scrims"] for i in _roles): 59 | perms *= 5 60 | 61 | if any(i in g_record.dashboard_access["tourney"] for i in _roles): 62 | perms *= 7 63 | 64 | if any(i in g_record.dashboard_access["slotm"] for i in _roles): 65 | perms *= 11 66 | 67 | result[guild_id] = perms 68 | 69 | await self.bot.sio.emit(f"guild_permissions__{u}", result) 70 | -------------------------------------------------------------------------------- /src/sockets/events/guilds.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import typing as T 4 | 5 | import discord 6 | 7 | if T.TYPE_CHECKING: 8 | from core import Quotient 9 | 10 | from core import Cog 11 | from models import Guild 12 | 13 | from ..schemas import QGuild 14 | 15 | __all__ = ("SockGuild",) 16 | 17 | 18 | class SockGuild(Cog): 19 | def __init__(self, bot: Quotient): 20 | self.bot = bot 21 | 22 | @Cog.listener() 23 | async def on_request__get_guilds(self, u, data: dict): 24 | guild_ids = data["guild_ids"] 25 | user_id = data["user_id"] 26 | 27 | results: T.Dict[str, T.List[QGuild]] = {} 28 | 29 | for _id in guild_ids: 30 | guild = self.bot.get_guild(int(_id)) 31 | if not guild: 32 | continue 33 | 34 | member = await self.bot.get_or_fetch_member(guild, user_id) 35 | if not member: 36 | continue 37 | 38 | results[str(_id)] = ( 39 | await QGuild.from_guild(guild, await self.__guild_permissions(guild, member)) 40 | ).dict() 41 | 42 | await self.bot.sio.emit("get_guilds__{0}".format(u), results) 43 | 44 | async def __guild_permissions(self, guild: discord.Guild, user: discord.Member): 45 | perms = 1 46 | 47 | if user.guild_permissions.manage_guild: 48 | return 2 49 | 50 | g = await Guild.get(pk=guild.id) 51 | _roles = [str(_.id) for _ in user.roles] 52 | 53 | if any(i in g.dashboard_access["embed"] for i in _roles): 54 | perms *= 3 55 | 56 | if any(i in g.dashboard_access["scrims"] for i in _roles): 57 | perms *= 5 58 | 59 | if any(i in g.dashboard_access["tourney"] for i in _roles): 60 | perms *= 7 61 | 62 | if any(i in g.dashboard_access["slotm"] for i in _roles): 63 | perms *= 11 64 | 65 | return perms 66 | -------------------------------------------------------------------------------- /src/sockets/events/scrims.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import typing 4 | 5 | if typing.TYPE_CHECKING: 6 | from core import Quotient 7 | 8 | from core import Cog 9 | from models import Scrim 10 | 11 | from ..schemas import BaseScrim, SockResponse 12 | 13 | __all__ = ("SocketScrims",) 14 | 15 | 16 | class SocketScrims(Cog): 17 | def __init__(self, bot: Quotient): 18 | self.bot = bot 19 | 20 | @Cog.listener() 21 | async def on_request__bot_scrim_create(self, u: str, data: dict): 22 | data: BaseScrim = BaseScrim(**data) 23 | 24 | _v = await data.validate_perms(self.bot) 25 | 26 | if all(_v): 27 | _v = await data.create_scrim(self.bot) 28 | 29 | if not all(_v): 30 | return await self.bot.sio.emit( 31 | "bot_scrim_create__{0}".format(u), SockResponse(ok=False, error=_v[1]).dict() 32 | ) 33 | 34 | await self.bot.sio.emit( 35 | "bot_scrim_create__{0}".format(u), SockResponse(data={"id": _v[1].id}).dict() 36 | ) 37 | 38 | @Cog.listener() 39 | async def on_request__bot_scrim_edit(self, u: str, data: dict): 40 | data: BaseScrim = BaseScrim(**data) 41 | 42 | _v = await data.validate_perms(self.bot) 43 | 44 | if not all(_v): 45 | return await self.bot.sio.emit( 46 | "bot_scrim_edit__{0}".format(u), SockResponse(ok=False, error=_v[1]).dict() 47 | ) 48 | 49 | await data.update_scrim(self.bot) 50 | await self.bot.sio.emit(f"bot_scrim_edit__{u}", SockResponse().dict()) 51 | 52 | @Cog.listener() 53 | async def on_request__bot_scrim_delete(self, u: str, data: dict): 54 | guild_id, scrim_id = data["guild_id"], data["scrim_id"] 55 | if scrim_id: 56 | scrim = await Scrim.get_or_none(pk=scrim_id) 57 | if scrim: 58 | await scrim.full_delete() 59 | 60 | else: 61 | scrims = await Scrim.filter(guild_id=guild_id) 62 | for scrim in scrims: 63 | await scrim.full_delete() 64 | 65 | return await self.bot.sio.emit(f"bot_scrim_delete__{u}", SockResponse().dict()) 66 | -------------------------------------------------------------------------------- /src/sockets/events/settings.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING 4 | 5 | if TYPE_CHECKING: 6 | from core import Quotient 7 | 8 | from contextlib import suppress 9 | 10 | import constants 11 | import discord 12 | from core import Cog 13 | from models import Votes 14 | 15 | from ..schemas import SockResponse 16 | 17 | 18 | class SockSettings(Cog): 19 | def __init__(self, bot: Quotient): 20 | self.bot = bot 21 | self.hook = discord.Webhook.from_url(self.bot.config.PUBLIC_LOG, session=self.bot.session) 22 | 23 | @Cog.listener() 24 | async def on_request__prefix_change(self, u, data: dict): 25 | guild_id = data.get("guild_id") 26 | await self.bot.cache.update_guild_cache(int(guild_id)) 27 | await self.bot.sio.emit("prefix_change__{0}".format(u), SockResponse().dict()) 28 | 29 | @Cog.listener() 30 | async def on_request__new_vote(self, u, data: dict): 31 | user_id = int(data.get("user_id")) 32 | record = await Votes.get(pk=user_id) 33 | 34 | await self.bot.reminders.create_timer(record.expire_time, "vote", user_id=record.user_id) 35 | 36 | member = self.bot.server.get_member(record.user_id) 37 | if member is not None: 38 | await member.add_roles(discord.Object(id=self.bot.config.VOTER_ROLE), reason="They voted for me.") 39 | 40 | else: 41 | member = await self.bot.getch(self.bot.get_user, self.bot.fetch_user, record.pk) 42 | 43 | with suppress(discord.HTTPException, AttributeError): 44 | 45 | embed = discord.Embed(color=discord.Color.green(), description=f"Thanks **{member}** for voting.") 46 | embed.set_image(url=constants.random_thanks()) 47 | embed.set_footer(text=f"Your total votes: {record.total_votes}") 48 | await self.hook.send(embed=embed, username="vote-logs", avatar_url=self.bot.user.display_avatar.url) 49 | 50 | @Cog.listener() 51 | async def on_request__get_usernames(self, u, data: dict): 52 | _dict = {} 53 | for _ in data.get("users"): 54 | _dict[str(_)] = str(await self.bot.getch(self.bot.get_user, self.bot.fetch_user, int(_))) 55 | 56 | await self.bot.sio.emit("get_usernames__{0}".format(u), SockResponse(data=_dict).dict()) 57 | -------------------------------------------------------------------------------- /src/sockets/events/tourney.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING 4 | 5 | if TYPE_CHECKING: 6 | from core import Quotient 7 | 8 | from core import Cog 9 | from models import Tourney 10 | 11 | from ..schemas import SockTourney 12 | 13 | __all__ = ("SockTourney",) 14 | 15 | 16 | class SockTourney(Cog): 17 | def __init__(self, bot: Quotient): 18 | self.bot = bot 19 | 20 | @Cog.listener() 21 | async def on_request__bot_tourney_create(self, u: str, data: dict): 22 | data: SockTourney = SockTourney(**data) 23 | 24 | @Cog.listener() 25 | async def on_request__bot_tourney_edit(self, u: str, data: dict): 26 | data: SockTourney = SockTourney(**data) 27 | 28 | @Cog.listener() 29 | async def on_request__bot_tourney_delete(self, u: str, data: dict): 30 | guild_id, tourney_id = data["guild_id"], data["tourney_id"] 31 | if tourney_id: 32 | tourney = await Tourney.get_or_none(pk=tourney_id) 33 | if tourney: 34 | await tourney.full_delete() 35 | 36 | else: 37 | tourneys = await Tourney.filter(guild_id=guild_id) 38 | for tourney in tourneys: 39 | await tourney.full_delete() 40 | 41 | return await self.bot.sio.emit("bot_tourney_delete__{0}".format(u), SockTourney().dict()) 42 | -------------------------------------------------------------------------------- /src/sockets/schemas/__init__.py: -------------------------------------------------------------------------------- 1 | from ._guild import * 2 | from ._resp import * 3 | from ._scrim import * 4 | from ._tourney import * 5 | -------------------------------------------------------------------------------- /src/sockets/schemas/_guild.py: -------------------------------------------------------------------------------- 1 | import typing as T 2 | 3 | import discord 4 | from models import Guild 5 | from pydantic import BaseModel 6 | 7 | __all__ = ("QGuild",) 8 | 9 | 10 | class QGuild(BaseModel): 11 | id: str 12 | name: str 13 | icon: str 14 | channels: T.List[dict] 15 | roles: T.List[dict] 16 | boosted_by: dict 17 | dashboard_access: int 18 | 19 | @staticmethod 20 | async def from_guild(guild: discord.Guild, perms: int): 21 | _d = { 22 | "id": str(guild.id), 23 | "name": guild.name, 24 | "dashboard_access": perms, 25 | "icon": getattr(guild.icon, "url", "https://cdn.discordapp.com/embed/avatars/0.png"), 26 | } 27 | 28 | _d["channels"] = [{"id": str(c.id), "name": c.name} for c in guild.text_channels] 29 | 30 | _d["roles"] = [ 31 | {"id": str(r.id), "name": r.name, "color": int(r.color), "managed": r.managed} for r in guild.roles 32 | ] 33 | _d["boosted_by"] = {} 34 | 35 | record = await Guild.get(pk=guild.id) 36 | 37 | if record.is_premium: 38 | booster = await record.bot.get_or_fetch_member(guild, record.made_premium_by) 39 | _d["boosted_by"] = { 40 | "id": str(getattr(booster, "id", 12345)), 41 | "username": getattr(booster, "name", "Unknown User"), 42 | "discriminator": getattr(booster, "discriminator", "#0000"), 43 | "avatar": booster.display_avatar.url if booster else "https://cdn.discordapp.com/embed/avatars/0.png", 44 | } 45 | 46 | return QGuild(**_d) 47 | -------------------------------------------------------------------------------- /src/sockets/schemas/_resp.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | __all__ = ("SockResponse",) 4 | 5 | 6 | class SockResponse(BaseModel): 7 | ok: bool = True 8 | error: str = None 9 | data: dict = None 10 | -------------------------------------------------------------------------------- /src/sockets/schemas/_tourney.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import List, Optional 4 | 5 | from pydantic import BaseModel 6 | 7 | 8 | class SockTourney(BaseModel): 9 | id: Optional[int] = None 10 | guild_id: int 11 | name: str = "Quotient-Tourney" 12 | registration_channel_id: int 13 | confirm_channel_id: int 14 | role_id: int 15 | required_mentions: int = 4 16 | total_slots: int 17 | banned_users: List[int] 18 | host_id: int 19 | multiregister: bool = False 20 | open_role_id: Optional[int] = None 21 | teamname_compulsion: bool = False 22 | ping_role_id: Optional[int] = None 23 | no_duplicate_name: bool = True 24 | autodelete_rejected: bool = True 25 | success_message: Optional[str] = None 26 | -------------------------------------------------------------------------------- /src/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from .buttons import * 2 | from .converters import * 3 | from .default import * 4 | from .emote import * 5 | from .formats import * 6 | from .inputs import * 7 | from .paginator import * 8 | from .time import * 9 | -------------------------------------------------------------------------------- /src/utils/buttons.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import List, NamedTuple, Optional, Union 4 | 5 | import discord 6 | 7 | from .emote import TextChannel, VoiceChannel 8 | 9 | 10 | class LinkType(NamedTuple): 11 | name: Optional[str] = None 12 | url: Optional[str] = None 13 | emoji: Optional[str] = None 14 | 15 | 16 | class LinkButton(discord.ui.View): 17 | def __init__(self, links: Union[LinkType, List[LinkType]]): 18 | super().__init__() 19 | 20 | links = links if isinstance(links, list) else [links] 21 | 22 | for link in links: 23 | self.add_item(discord.ui.Button(label=link.name, url=link.url, emoji=link.emoji)) 24 | 25 | 26 | class Prompt(discord.ui.View): 27 | def __init__(self, user_id, timeout=30.0): 28 | super().__init__(timeout=timeout) 29 | self.user_id = user_id 30 | self.value = None 31 | 32 | async def interaction_check(self, interaction: discord.Interaction) -> bool: 33 | if interaction.user.id != self.user_id: 34 | await interaction.response.send_message( 35 | "Sorry, you can't use this interaction as it is not started by you.", ephemeral=True 36 | ) 37 | return False 38 | return True 39 | 40 | @discord.ui.button(label="Confirm", style=discord.ButtonStyle.green) 41 | async def confirm(self, interaction: discord.Interaction, _: discord.ui.Button): 42 | await interaction.response.defer() 43 | self.value = True 44 | self.stop() 45 | 46 | @discord.ui.button(label="Cancel", style=discord.ButtonStyle.grey) 47 | async def cancel(self, interaction: discord.Interaction, _: discord.ui.Button): 48 | await interaction.response.defer() 49 | self.value = False 50 | self.stop() 51 | 52 | 53 | class BaseSelector(discord.ui.View): 54 | message: discord.Message 55 | 56 | def __init__(self, author_id, selector: discord.ui.Select, **kwargs): 57 | self.author_id = author_id 58 | self.custom_id = None 59 | super().__init__(timeout=30.0) 60 | 61 | self.add_item(selector(**kwargs)) 62 | 63 | async def interaction_check(self, interaction: discord.Interaction) -> bool: 64 | if interaction.user.id != self.author_id: 65 | await interaction.response.send_message( 66 | "Sorry, you can't use this interaction as it is not started by you.", ephemeral=True 67 | ) 68 | return False 69 | return True 70 | 71 | async def on_timeout(self) -> None: 72 | if hasattr(self, "message"): 73 | await self.message.delete() 74 | 75 | 76 | class ChannelSelector(discord.ui.Select): 77 | def __init__(self, placeholder: str, channels: List[Union[discord.TextChannel, discord.VoiceChannel]]): 78 | 79 | _options = [] 80 | for channel in channels: 81 | _options.append( 82 | discord.SelectOption( 83 | label=channel.name, 84 | value=channel.id, 85 | description=f"{channel.name} ({channel.id})", 86 | emoji=TextChannel if isinstance(channel, discord.TextChannel) else VoiceChannel, 87 | ) 88 | ) 89 | 90 | super().__init__(placeholder=placeholder, options=_options) 91 | 92 | async def callback(self, interaction: discord.Interaction): 93 | await interaction.response.defer() 94 | self.view.custom_id = interaction.data["values"][0] 95 | self.view.stop() 96 | 97 | 98 | class CustomSelector(discord.ui.Select): 99 | def __init__(self, placeholder: str, options: List[discord.SelectOption]): 100 | super().__init__(placeholder=placeholder, options=options) 101 | 102 | async def callback(self, interaction: discord.Interaction): 103 | await interaction.response.defer() 104 | self.view.custom_id = interaction.data["values"][0] 105 | self.view.stop() 106 | -------------------------------------------------------------------------------- /src/utils/checks.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | 3 | from discord.ext import commands 4 | from discord.ext.commands import CheckFailure, Context, has_any_role 5 | 6 | from models import Guild, User 7 | 8 | from .exceptions import * 9 | 10 | 11 | def has_done_setup(): 12 | async def predicate(ctx: Context): 13 | check = await Guild.get_or_none(pk=ctx.guild.id) 14 | if not check.private_ch: 15 | raise NotSetup() 16 | 17 | else: 18 | return True 19 | 20 | return commands.check(predicate) 21 | 22 | 23 | def is_premium_guild(): 24 | async def predictate(ctx: Context): 25 | check = await Guild.get_or_none(guild_id=ctx.guild.id) 26 | if not check or check.is_premium is False: 27 | raise NotPremiumGuild() 28 | 29 | else: 30 | return True 31 | 32 | return commands.check(predictate) 33 | 34 | 35 | def is_premium_user(): 36 | async def predicate(ctx: Context): 37 | check = await User.get_or_none(user_id=ctx.author.id) 38 | if not check or check.is_premium is False: 39 | raise NotPremiumUser() 40 | 41 | else: 42 | return True 43 | 44 | return commands.check(predicate) 45 | 46 | 47 | def can_use_sm(): 48 | """ 49 | Returns True if the user has manage roles or scrim-mod role in the server. 50 | """ 51 | 52 | async def predicate(ctx: Context): 53 | if ctx.author.guild_permissions.manage_guild or "scrims-mod" in (role.name.lower() for role in ctx.author.roles): 54 | return True 55 | raise SMNotUsable() 56 | 57 | return commands.check(predicate) 58 | 59 | 60 | def can_use_tm(): 61 | """ 62 | Returns True if the user has manage roles or scrim-mod role in the server. 63 | """ 64 | 65 | async def predicate(ctx: Context): 66 | if ctx.author.guild_permissions.manage_guild or "tourney-mod" in (role.name.lower() for role in ctx.author.roles): 67 | return True 68 | raise TMNotUsable() 69 | 70 | return commands.check(predicate) 71 | 72 | 73 | async def has_any_role_check(ctx: Context, *roles: Union[str, int]) -> bool: 74 | """ 75 | Returns True if the context's author has any of the specified roles. 76 | `roles` are the names or IDs of the roles for which to check. 77 | False is always returns if the context is outside a guild. 78 | """ 79 | try: 80 | return await has_any_role(*roles).predicate(ctx) 81 | except CheckFailure: 82 | return False 83 | 84 | 85 | async def check_guild_permissions(ctx: Context, perms, *, check=all): 86 | is_owner = await ctx.bot.is_owner(ctx.author) 87 | if is_owner: 88 | return True 89 | 90 | if ctx.guild is None: 91 | return False 92 | 93 | resolved = ctx.author.guild_permissions 94 | return check(getattr(resolved, name, None) == value for name, value in perms.items()) 95 | 96 | 97 | def is_mod(): 98 | async def pred(ctx): 99 | return await check_guild_permissions(ctx, {"manage_guild": True}) 100 | 101 | return commands.check(pred) 102 | 103 | 104 | def is_admin(): 105 | async def pred(ctx): 106 | return await check_guild_permissions(ctx, {"administrator": True}) 107 | 108 | return commands.check(pred) 109 | 110 | 111 | async def check_permissions(ctx: Context, perms, *, check=all): 112 | is_owner = await ctx.bot.is_owner(ctx.author) 113 | if is_owner: 114 | return True 115 | 116 | resolved = ctx.channel.permissions_for(ctx.author) 117 | return check(getattr(resolved, name, None) == value for name, value in perms.items()) 118 | 119 | 120 | def has_permissions(*, check=all, **perms): 121 | async def pred(ctx): 122 | return await check_permissions(ctx, perms, check=check) 123 | 124 | return commands.check(pred) 125 | -------------------------------------------------------------------------------- /src/utils/default.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import re 4 | from datetime import datetime 5 | from itertools import islice 6 | from typing import Union 7 | from unicodedata import normalize as nm 8 | import discord 9 | 10 | from constants import IST 11 | 12 | 13 | def get_chunks(iterable, size: int): 14 | it = iter(iterable) 15 | return iter(lambda: tuple(islice(it, size)), ()) 16 | 17 | 18 | def split_list(data: list, per_list: int): 19 | data = list(data) 20 | 21 | new = [] 22 | 23 | for i in range(0, len(data), per_list): 24 | new.append(data[i : i + per_list]) 25 | 26 | return new 27 | 28 | 29 | def find_team(message: discord.Message): 30 | """Finds team name from a message""" 31 | content = message.content.lower() 32 | author = message.author 33 | teamname = re.search(r"team.*", content) 34 | if teamname is None: 35 | return f"{author}'s team" 36 | 37 | # teamname = (re.sub(r"\b[0-9]+\b\s*|team|name|[^\w\s]", "", teamname.group())).strip() 38 | teamname: str = re.sub(r"<@*#*!*&*\d+>|team|name|[^\w\s]", "", teamname.group()).strip() 39 | 40 | teamname = f"Team {teamname.title()}" if teamname else f"{author}'s team" 41 | return teamname 42 | 43 | 44 | def regional_indicator(c: str) -> str: 45 | """Returns a regional indicator emoji given a character.""" 46 | return chr(0x1F1E6 - ord("A") + ord(c.upper())) 47 | 48 | 49 | def keycap_digit(c: Union[int, str]) -> str: 50 | """Returns a keycap digit emoji given a character.""" 51 | c = int(c) 52 | if 0 < c < 10: 53 | return str(c) + "\U0000FE0F\U000020E3" 54 | if c == 10: 55 | return "\U000FE83B" 56 | raise ValueError("Invalid keycap digit") 57 | 58 | 59 | async def aenumerate(asequence, start=0): 60 | """Asynchronously enumerate an async iterator from a given start value""" 61 | n = start 62 | async for elem in asequence: 63 | yield n, elem 64 | n += 1 65 | 66 | 67 | def get_ipm(bot): 68 | """Returns Quotient's cmds invoke rate per minute""" 69 | time = (datetime.now(tz=IST) - bot.start_time).total_seconds() 70 | per_second = bot.cmd_invokes / time 71 | per_minute = per_second * 60 72 | return per_minute 73 | -------------------------------------------------------------------------------- /src/utils/emote.py: -------------------------------------------------------------------------------- 1 | # never ask me to remove these :c 2 | red = "<:red:775586906599456779>" 3 | green = "<:green:775586905580240946>" 4 | yellow = "<:yellow:775586904439128064>" 5 | invisible = "<:invisible:775586907680931860>" 6 | 7 | scrimscheck = "<:scrimscheck:839554647861755946>" 8 | scrimsxmark = "<:scrimscross:839554689778712576>" 9 | 10 | info = "<:info2:899020593188462693>" 11 | trash = "<:trashcan:896382424529907742>" 12 | exit = "<:exit:926048897548300339>" 13 | 14 | 15 | add = "<:add:844825523003850772>" 16 | remove = "<:remove:844825861295046661>" 17 | edit = "<:edit:844826616735727616>" 18 | 19 | diamond = "" 20 | 21 | error = "❗" 22 | 23 | server = "<:server:775586933396078612>" 24 | privacy = "<:privacy:775586938659799060>" 25 | 26 | one = "1️⃣" 27 | two = "2️⃣" 28 | three = "3️⃣" 29 | four = "4️⃣" 30 | five = "5️⃣" 31 | 32 | 33 | rps = "<:rps:833993433415811083>" 34 | 35 | 36 | bravery = "<:bravery:833991097222299678>" 37 | brilliance = "<:brilliance:833991154839191573>" 38 | balance = "<:balance:833991183087829012>" 39 | 40 | supporter = "<:supporter:833991451872198696>" 41 | staff = "<:staff:833991411313147915>" 42 | partner = "<:partner:833991367130087464>" 43 | hypesquad = "<:hypesquad:833991496545206282>" 44 | hypesquad_events = "<:hypesquad_events:833991530418667540>" 45 | bug_hunter = "<:bug_hunter:833991572613234718>" 46 | BugHunterLvl2 = "<:bug_hunter2:833991610357514240>" 47 | bot = "<:bot:833991659133992991>" 48 | bot_devloper = "<:bot_devloper:833991714637217803>" 49 | verified_bot = "<:verifiedbot1:833991773248290817><:verifiedbot2:833991793683070996>" 50 | 51 | check = "<:check:807913701151342592>" 52 | xmark = "<:xmark:807913737805234176>" 53 | loading = "" 54 | VoiceChannel = "<:voice:815827186116198430>" 55 | TextChannel = "<:text:815827264679706624>" 56 | category = "<:category:815831557507776583>" 57 | pain = "<:blobpain:831771526368985098>" 58 | settings_yes = "<:settings_mark_off:815169498319159337><:settings_check_on:815169424566517791>" 59 | settings_no = "<:set_no_on:815169465259786241><:set_yes_off:815169360393404436>" 60 | 61 | 62 | pstop = "<:stop:829602188593856574>" 63 | pprevious = "<:previous:829602188565151744>" 64 | pnext = "<:next:829602188653101056>" 65 | plast = "<:last:829602188435128331>" 66 | pfirst = "<:first:829602188598312990>" 67 | 68 | 69 | red1 = "<:red1:870909062227845122>" 70 | red2 = "<:red2:870909062513061899>" 71 | red3 = "<:red3:870909062877966376>" 72 | red4 = "<:red4:870909062341099572>" 73 | red5 = "<:red5:870909062437548072>" 74 | 75 | 76 | green1 = "<:green1:870909061514792991>" 77 | green2 = "<:green2:870909061225390131>" 78 | green3 = "<:green3:870909061661605969>" 79 | green4 = "<:green4:870909062320099329>" 80 | green5 = "<:green5:870909062345281546>" 81 | 82 | BADGES = { 83 | "creator": "<:creator:807911084069617674>", 84 | "dev": "<:dev:807911284040531999>", 85 | "donator": "", 86 | "messenger": "", 87 | "premium": "", 88 | "contributor": "", 89 | "staff": "<:staff:807911358549065738>", 90 | "top_user": "", 91 | "voter": "<:voter:807912082142003220>", 92 | } 93 | 94 | p = "\U0001f1f5" 95 | e = "\U0001f1ea" 96 | r = "\U0001f1f7" 97 | eye = "👀" 98 | -------------------------------------------------------------------------------- /src/utils/exceptions.py: -------------------------------------------------------------------------------- 1 | from discord.ext import commands 2 | 3 | 4 | class QuotientError(commands.CheckFailure): 5 | pass 6 | 7 | 8 | class NotSetup(QuotientError): 9 | def __init__(self): 10 | super().__init__( 11 | "This command requires you to have Quotient's private channel.\nKindly run `{ctx.prefix}setup` and try again." 12 | ) 13 | 14 | 15 | class NotPremiumGuild(QuotientError): 16 | def __init__(self): 17 | super().__init__( 18 | "This command requires this server to be premium.\n\nCheckout Quotient Premium [here]({ctx.bot.prime_link})" 19 | ) 20 | 21 | 22 | class NotPremiumUser(QuotientError): 23 | def __init__(self): 24 | super().__init__( 25 | "This command requires you to be a premium user.\nCheckout Quotient Premium [here]({ctx.bot.prime_link})" 26 | ) 27 | 28 | 29 | class InputError(QuotientError): 30 | pass 31 | 32 | 33 | class SMNotUsable(QuotientError): 34 | def __init__(self): 35 | super().__init__(f"You need either the `scrims-mod` role or `manage_guild` permissions to use this command.") 36 | 37 | 38 | class TMNotUsable(QuotientError): 39 | def __init__(self): 40 | super().__init__(f"You need either the `tourney-mod` role or `manage_guild` permissions to use tourney manager.") 41 | 42 | 43 | class PastTime(QuotientError): 44 | def __init__(self): 45 | super().__init__( 46 | f"The time you entered seems to be in past.\n\nKindly try again, use times like: `tomorrow` , `friday 9pm`" 47 | ) 48 | 49 | 50 | TimeInPast = PastTime 51 | 52 | 53 | class InvalidTime(QuotientError): 54 | def __init__(self): 55 | super().__init__(f"The time you entered seems to be invalid.\n\nKindly try again.") 56 | -------------------------------------------------------------------------------- /src/utils/formats.py: -------------------------------------------------------------------------------- 1 | def truncate_string(value, max_length=128, suffix="..."): 2 | string_value = str(value) 3 | string_truncated = string_value[: min(len(string_value), (max_length - len(suffix)))] 4 | suffix = suffix if len(string_value) > max_length else "" 5 | return string_truncated + suffix 6 | 7 | 8 | class plural: 9 | def __init__(self, value): 10 | self.value = value 11 | 12 | if isinstance(self.value, list): 13 | self.value = len(self.value) 14 | 15 | def __format__(self, format_spec): 16 | v = self.value 17 | singular, sep, plural = format_spec.partition("|") 18 | plural = plural or f"{singular}s" 19 | if abs(v) != 1: 20 | return f"{v} {plural}" 21 | return f"{v} {singular}" 22 | -------------------------------------------------------------------------------- /src/utils/regex.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | """ 4 | Regex compiled by pythondiscord.com 5 | Helper regex for moderation 6 | """ 7 | 8 | INVITE_RE = re.compile( 9 | r"(?:discord(?:[\.,]|dot)gg|" # Could be discord.gg/ 10 | r"discord(?:[\.,]|dot)com(?:\/|slash)invite|" # or discord.com/invite/ 11 | r"discordapp(?:[\.,]|dot)com(?:\/|slash)invite|" # or discordapp.com/invite/ 12 | r"discord(?:[\.,]|dot)me|" # or discord.me 13 | r"discord(?:[\.,]|dot)io" # or discord.io. 14 | r")(?:[\/]|slash)" # / or 'slash' 15 | r"([a-zA-Z0-9\-]+)", # the invite code itself 16 | flags=re.IGNORECASE, 17 | ) 18 | 19 | 20 | TIME_REGEX = re.compile(r"(?:(\d{1,5})(h|s|m|d))+?") 21 | -------------------------------------------------------------------------------- /tests/black.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deadaf/Quotient-Bot/b5f88056889f1faef9d8c04d96684e47e3f79c3d/tests/black.jpg -------------------------------------------------------------------------------- /tests/img.py: -------------------------------------------------------------------------------- 1 | from PIL import Image, ImageDraw, ImageFont 2 | 3 | 4 | def add_watermark(image): 5 | text = "Quotient • quotientbot.xyz" 6 | draw = ImageDraw.Draw(image) 7 | 8 | font = ImageFont.truetype("myfont.ttf", 25) 9 | textwidth, textheight = draw.textsize(text, font) 10 | 11 | margin = 20 12 | x = width - textwidth - margin 13 | y = height - textheight - margin 14 | 15 | draw.text((x, y), text, font=font) 16 | 17 | 18 | def add_title(image): 19 | text = "Bahot-Hard ESPORTS" 20 | font = ImageFont.truetype("theboldfont.ttf", 90) 21 | 22 | d1 = ImageDraw.Draw(image) 23 | w, h = d1.textsize(text, font) 24 | 25 | left = (image.width - w) / 2 26 | top = 50 27 | 28 | d1.text((left, top), text, font=font) 29 | 30 | # second title 31 | w, h = d1.textsize("Overall Standings", font) 32 | left = (image.width - w) / 2 33 | d1.text((left, 150), "Overall Standings", font=font) 34 | 35 | 36 | def add_rectangles(image, rect): 37 | top = 320 38 | rect = rect.resize((round(rect.size[0] / 2.8), round(rect.size[1] / 2.8))) 39 | 40 | image.paste(rect, (40, 260), rect) 41 | for i in range(10): 42 | image.paste(rect, (40, top), rect) 43 | top += 50 44 | 45 | top = 273 46 | d1 = ImageDraw.Draw(image) 47 | font = ImageFont.truetype("theboldfont.ttf", 30) 48 | d1.text((55, top), "Rank", (0, 0, 0), font=font) 49 | d1.text((300, top), "Team Name", (0, 0, 0), font=font) 50 | d1.text((652, top), "Posi Pt.", (0, 0, 0), font=font) 51 | d1.text((800, top), "Kill Pt.", (0, 0, 0), font=font) 52 | d1.text((950, top), "Total", (0, 0, 0), font=font) 53 | d1.text((1100, top), "Win", (0, 0, 0), font=font) 54 | 55 | 56 | image = Image.open("test.jpg") 57 | rect = Image.open("rectangle.png") 58 | 59 | 60 | rect = rect.convert("RGBA") 61 | image = image.resize((1250, 938)) 62 | 63 | width, height = image.size 64 | 65 | 66 | add_title(image) 67 | add_watermark(image) 68 | add_rectangles(image, rect) 69 | 70 | 71 | image.show() 72 | -------------------------------------------------------------------------------- /tests/img2.py: -------------------------------------------------------------------------------- 1 | from PIL import Image, ImageDraw, ImageFont 2 | 3 | 4 | def add_watermark(image): 5 | text = "Quotient • quotientbot.xyz" 6 | draw = ImageDraw.Draw(image) 7 | 8 | font = ImageFont.truetype("myfont.ttf", 25) 9 | textwidth, textheight = draw.textsize(text, font) 10 | 11 | margin = 20 12 | x = width - textwidth - margin 13 | y = height - textheight - margin 14 | 15 | draw.text((x, y), text, font=font) 16 | 17 | 18 | def add_title(image): 19 | text = "Yo Bro ESPORTS" 20 | font = ImageFont.truetype("theboldfont.ttf", 90) 21 | 22 | d1 = ImageDraw.Draw(image) 23 | w, h = d1.textsize(text, font) 24 | 25 | left = (image.width - w) / 2 26 | top = 50 27 | 28 | d1.text((left, top), text, font=font) 29 | 30 | # second title 31 | w, h = d1.textsize("Overall Standings", font) 32 | left = (image.width - w) / 2 33 | d1.text((left, 150), "Overall Standings", font=font) 34 | 35 | 36 | image = Image.open("rectangle.png") 37 | image = image.convert("RGBA") 38 | 39 | image = image.resize((round(image.size[0] / 2.8), round(image.size[1] / 2.8))) 40 | 41 | draw = ImageDraw.Draw(image) 42 | font = ImageFont.truetype("roboto/italic.ttf", 30) 43 | 44 | top = 8 45 | fill = (0, 0, 0) 46 | 47 | draw.text((18, top), "Rank", fill, font=font) 48 | draw.text((250, top), "Team Name", fill, font=font) 49 | draw.text((610, top), "Place Pt", fill, font=font) 50 | draw.text((770, top), "Kills Pt", fill, font=font) 51 | 52 | draw.text((905, top), "Total Pt", fill, font=font) 53 | 54 | draw.text((1060, top), "Win?", fill, font=font) 55 | 56 | _list = [] 57 | 58 | _list.append(image) 59 | 60 | 61 | _dict = { 62 | "quotient": [1, 20, 20, 40], 63 | "butterfly": [2, 14, 14, 28], 64 | "4pandas": [3, 10, 8, 18], 65 | "kite": [4, 10, 5, 15], 66 | "quotient2": [1, 20, 20, 40], 67 | "butterfly2": [2, 14, 14, 28], 68 | "4pandas2": [3, 10, 8, 18], 69 | "kite2": [4, 10, 5, 15], 70 | "quotient3": [1, 20, 20, 40], 71 | "butterfly3": [2, 14, 14, 28], 72 | } 73 | 74 | font = ImageFont.truetype("roboto/Roboto-Bold.ttf", 30) 75 | 76 | top = 10 77 | left = 35 78 | 79 | for idx, item in enumerate(_dict.items(), start=1): 80 | 81 | image = Image.open("rectangle.png") 82 | image = image.convert("RGBA") 83 | 84 | image = image.resize((round(image.size[0] / 2.8), round(image.size[1] / 2.8))) 85 | 86 | draw = ImageDraw.Draw(image) 87 | 88 | team = item[0] 89 | win, place, kill, total = item[1] 90 | 91 | draw.text((left, top), f"#{idx:02}", fill, font=font) 92 | draw.text((left + 150, top), f"Team {team.title()}", fill, font=font) 93 | draw.text((left + 610, top), str(place), fill, font=font) 94 | draw.text((left + 760, top), str(kill), fill, font=font) 95 | draw.text((left + 897, top), str(total), fill, font=font) 96 | draw.text((left + 1025, top), f"{'Yes!' if win else 'No!'}", fill, font=font) 97 | 98 | _list.append(image) 99 | 100 | 101 | image = Image.open("test.jpg") 102 | image = image.resize((1250, 938)) 103 | width, height = image.size 104 | top = 320 105 | 106 | for i in _list: 107 | if _list.index(i) == 0: 108 | image.paste(i, (40, 260), i) 109 | else: 110 | image.paste(i, (40, top), i) 111 | top += 50 112 | 113 | add_watermark(image) 114 | add_title(image) 115 | 116 | 117 | image.save("points.jpg") 118 | # for image in _list: 119 | # image.show() 120 | 121 | 122 | _list = [ 123 | {"a": [1, 20, 20, 40], "b": [0, 14, 14, 28]}, 124 | {"a": [2, 20, 20, 40], "b": [2, 14, 14, 28]}, 125 | {"c": [1, 20, 20, 40], "d": [0, 14, 14, 28]}, 126 | ] 127 | -------------------------------------------------------------------------------- /tests/img3.py: -------------------------------------------------------------------------------- 1 | from PIL import Image, ImageDraw, ImageFont 2 | 3 | image = Image.open("ptable1.jpg") 4 | image = image.resize((1250, 938)) 5 | width, height = image.size 6 | 7 | 8 | rect = Image.open("rect2.png") 9 | rect = rect.convert("RGBA") 10 | rect = rect.resize((round(rect.size[0] / 1.5), round(rect.size[1] / 1.4))) 11 | 12 | draw = ImageDraw.Draw(rect) 13 | font = ImageFont.truetype("robo-italic.ttf", 16) 14 | 15 | top = 71 16 | fill = (0, 0, 0) 17 | 18 | draw.text((16, top), "RANK", fill, font=font) 19 | draw.text((220, top), "TEAM NAME", fill, font=font) 20 | draw.text((485, top), "MATCHES", fill, font=font) 21 | draw.text((616, top), "KILL POINTS", fill, font=font) 22 | draw.text((740, top), "PLACE POINTS", fill, font=font) 23 | 24 | draw.text((870, top), "TOTAL POINTS", fill, font=font) 25 | 26 | draw.text((1021, top), "WINS", fill, font=font) 27 | 28 | image.paste(rect, (40, 220), rect) 29 | 30 | top = 280 31 | for i in range(11): 32 | image.paste(rect, (40, top), rect) 33 | top += 50 34 | 35 | image.show() 36 | -------------------------------------------------------------------------------- /tests/outline.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deadaf/Quotient-Bot/b5f88056889f1faef9d8c04d96684e47e3f79c3d/tests/outline.ttf -------------------------------------------------------------------------------- /tests/pil_img.py: -------------------------------------------------------------------------------- 1 | from PIL import Image, ImageDraw, ImageFont 2 | 3 | _dict = { 4 | "1": "Basanti", 5 | "2": "saways", 6 | "3": "Changed", 7 | } 8 | 9 | image = Image.open("slot-rect.png") 10 | draw = ImageDraw.Draw(image) 11 | font = ImageFont.truetype("robo-bold.ttf", 80) 12 | 13 | draw.text((95, 55), "01", font=font) 14 | draw.text((325, 55), "Team is something bro", (0, 0, 0), font=font) 15 | image.show() 16 | -------------------------------------------------------------------------------- /tests/rect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deadaf/Quotient-Bot/b5f88056889f1faef9d8c04d96684e47e3f79c3d/tests/rect.png -------------------------------------------------------------------------------- /tests/robo-bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deadaf/Quotient-Bot/b5f88056889f1faef9d8c04d96684e47e3f79c3d/tests/robo-bold.ttf -------------------------------------------------------------------------------- /tests/script.py: -------------------------------------------------------------------------------- 1 | import PIL.Image 2 | 3 | # ASCII_CHARS = ["@", "#", "S", "%", "?", "*", "+", ";", ":", ",", "."] 4 | ASCII_CHARS = [" ", "#", "%", "?", "*", "+", ":", ","] 5 | 6 | 7 | def resize_image(image, new_width=50): 8 | width, height = image.size 9 | ratio = height / width 10 | new_height = int(new_width * ratio - 10) 11 | return image.resize((new_width, new_height)) 12 | 13 | 14 | def greyify(image): 15 | return image.convert("L") 16 | 17 | 18 | def pixels_to_ascii(image): 19 | pixels = image.getdata() 20 | return "".join([ASCII_CHARS[pixel // 25] for pixel in pixels]) 21 | 22 | 23 | def main(new_width=50): 24 | path = input("enter image path bruh: ") 25 | try: 26 | image = PIL.Image.open(path) 27 | image = image.convert("RGBA") 28 | except: 29 | print(path, " is not a valid path") 30 | 31 | new_image_data = pixels_to_ascii(greyify(resize_image(image))) 32 | 33 | count = len(new_image_data) 34 | ascii_image = "\n".join(new_image_data[i : (i + new_width)] for i in range(0, count, new_width)) 35 | 36 | print(ascii_image) 37 | 38 | 39 | main() 40 | -------------------------------------------------------------------------------- /tests/slicer.py: -------------------------------------------------------------------------------- 1 | # from PIL import Image 2 | 3 | 4 | # # def slice_image(filename, N): 5 | 6 | # # i = Image.open(filename) 7 | 8 | 9 | # # width = i.width 10 | # # height = i.height 11 | 12 | # # global _l 13 | # # _l = [] 14 | 15 | # # for x in range(N): 16 | 17 | # # for y in range(N): 18 | # # img = i.crop((x * width / N, y * height / N, x * width / N + width / N, y * height / N + height / N)) 19 | 20 | # # _l.append(img) 21 | 22 | 23 | # # slice_image("ss3.jpg", 2) 24 | 25 | # # for i in _l: 26 | # # i.show() 27 | # def crop(infile, height, width): 28 | 29 | # global _l 30 | # _l = [] 31 | # im = Image.open(infile) 32 | # imgwidth, imgheight = im.size 33 | # for i in range(imgheight // height): 34 | # for j in range(imgwidth // imgwidth): 35 | # box = (j * imgwidth, i * height, (j + 1) * imgwidth, (i + 1) * height) 36 | # _l.append(im.crop(box)) 37 | 38 | # return _l 39 | 40 | 41 | # crop("ss1.png", 400, 200) 42 | 43 | # for i in _l: 44 | # i.show() 45 | -------------------------------------------------------------------------------- /tests/slot-rect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deadaf/Quotient-Bot/b5f88056889f1faef9d8c04d96684e47e3f79c3d/tests/slot-rect.png -------------------------------------------------------------------------------- /tests/test.http: -------------------------------------------------------------------------------- 1 | POST http://0.0.0.0:5000/ssverify 2 | 3 | Content-Type: application/json 4 | 5 | { 6 | "user_id":548163406537162782, 7 | "guild_id":746337818388987967, 8 | "msg_channel_id":855424955944402984, 9 | "log_channel_id":855424955944402984, 10 | "role_id":857153828200775700, 11 | "mod_role_id":857153828200775700, 12 | "required_ss":1, 13 | "channel_name":"bb", 14 | "channel_link":"bb", 15 | "ss_type":"youtube", 16 | "success_message":"hi babies", 17 | "delete_after":0, 18 | "sstoggle":true 19 | } -------------------------------------------------------------------------------- /tests/test.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /tests/test.py: -------------------------------------------------------------------------------- 1 | points = """ 2 | 1 = 20, 3 | 2 = 14, 4 | 3-5 = 10 5 | """ 6 | 7 | result = {} 8 | for line in points.replace("\n", "").split(","): 9 | line_values = [value.strip() for value in line.split("=")] 10 | if "-" in line_values[0]: 11 | range_idx = line_values[0].split("-") 12 | num_range = list(range(int(range_idx[0]), int(range_idx[1]) + 1)) 13 | for key in num_range: 14 | result[key] = int(line_values[1]) 15 | else: 16 | result[int(line_values[0])] = int(line_values[1]) 17 | 18 | 19 | print(result) 20 | -------------------------------------------------------------------------------- /tests/useless.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deadaf/Quotient-Bot/b5f88056889f1faef9d8c04d96684e47e3f79c3d/tests/useless.py --------------------------------------------------------------------------------