├── .env.example ├── .github └── workflows │ └── docker-image.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── assets └── helpguide.txt ├── cogs ├── autopaste.py ├── bot_linking.py ├── database.py ├── discorddoc.py ├── docs.py ├── etc.py ├── help.py ├── roles.py ├── stars.py └── utils │ ├── fuzzy.py │ └── split_txtfile.py ├── docker-compose.yml ├── main.py ├── poetry.lock └── pyproject.toml /.env.example: -------------------------------------------------------------------------------- 1 | TOKEN=... # The bot's token 2 | HELP_CHANNEL_ID=... # The channel with the help view 3 | HELP_LOG_CHANNEL_ID=... # The channel with the help logs 4 | HELP_NOTIFICATION_ROLE_ID=... # The role that gets pinged in every help thread 5 | HELP_MOD_ROLE_ID=... # The role that is allowed to moderate help channels 6 | GUILD_ID=... # The guild id the bot uses 7 | BOT_LINKING_LOG_CHANNEL_ID=... # Logs for actions by the bot linking cog 8 | BOOSTER_ROLE_ID=... # The booster role. Currently used for booster bots 9 | STARS_CHANNEL_ID=... # The voice channel with the GitHub star count 10 | AUTO_THREAD_CHANNEL_ID=... # Channel in which all messages are automatically turned into threads 11 | HELP_BANNED_ROLE_ID=... # THe role that is not allowed to interact with the help channel 12 | CONSUL_ADDR=... # Consul address for the consul database 13 | CONSUL_TOKEN=... # Consul token for the consul database 14 | ASSIGNABLE_ROLE_IDS=... # A list of roles split by `,` that are assignable by members -------------------------------------------------------------------------------- /.github/workflows/docker-image.yml: -------------------------------------------------------------------------------- 1 | name: Docker Image CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | 7 | jobs: 8 | build-and-publish-head: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v2 # Checking out the repo 13 | 14 | - name: Build and Publish head Docker image 15 | uses: VaultVulp/gp-docker-action@1.2.0 16 | with: 17 | github-token: ${{ secrets.GH_REGISTRY_TOKEN }} # Provide GITHUB_TOKEN to login into the GitHub Packages 18 | image-name: previous # Provide Docker image name 19 | image-tag: ${{ github.sha }} # Provide Docker image tag 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | #.history/ folder in gitignore is here for vscode users like me :) 3 | .history/ 4 | *.pyc 5 | # pycharm :) 6 | .idea -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.10-slim-buster 2 | 3 | WORKDIR /bot 4 | 5 | RUN pip install poetry 6 | 7 | COPY pyproject.toml poetry.lock ./ 8 | 9 | RUN poetry install --no-root --no-dev 10 | 11 | COPY . . 12 | 13 | ENTRYPOINT ["poetry", "run", "python3"] 14 | CMD ["main.py"] 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021-present tag-epic 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a 6 | copy of this software and associated documentation files (the "Software"), 7 | to deal in the Software without restriction, including without limitation 8 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | and/or sell copies of the Software, and to permit persons to whom the 10 | Software is furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 16 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Previous 2 | 3 | The bot managing the Official [Nextcord Discord Server][NEXTCORDSERVER]. 4 | 5 | ## Features 6 | - **Autopaste:** 7 | Automatically paste the contents of a file or codeblock to [our paste service][PASTESERVICE]. \ 8 | (Source: [cogs/autopaste.py][AUTOPASTEPY]) 9 | 10 | - **Automatic stars update:** 11 | Automatically pull the stars from the [Nextcord][NEXTCORDREPOSITORY] and [Nextcord v3][NEXTCORDREPOSITORYV3] repoitories and update the [STARS_CHANNEL_ID] channel. \ 12 | (Source: [cogs/stars.py][STARSPY]) 13 | 14 | - **Auto thread:** 15 | Automatically create a thread for each message sent in the [AUTO_THREAD_CHANNEL_ID] channel. \ 16 | (Source: [cogs/autothread.py][AUTOTHREADPY]) 17 | 18 | 19 | - **Documentation:** 20 | Search through the [Discord][DISCORDDOCS] and [Python][PYTHONDOCS] & [Nextcord][NEXTCORDDOCS] documentation. \ 21 | (Source: [cogs/discorddoc.py ][DDOCSPY] (Discord) | [cogs/docs.py][DOCSPY] (Python & Nextcord)) 22 | 23 | - **Help System:** 24 | A help system using buttons for the [Nextcord server][NEXTCORDSERVER]. \ 25 | (Source: [cogs/help.py][HELPPY])) 26 | 27 | - **Others:** 28 | - Database: simple database powered by [consul.io][CONSUL]. 29 | (Source: [cogs/database.py][DATABASEPY]) 30 | - Bot linking: stores which users bot are added to the nextcord server by boosting. 31 | (Source: [cogs/botlink.py][BOTLINKPY]) 32 | - Charinfo: command to get information about a (unicode) character. 33 | (Source: [cogs/etc.py][ETCPY]) 34 | 35 | ## Running the bot 36 | 37 | ### Development 38 | 39 | 1. [Install docker][DOCKER] 40 | 2. Copy [.env.example][ENVFILE] to .env 41 | 3. Set each variable to your values 42 | `CONSUL_HOST` by default in dev mode is `http://consul:8500` 43 | `CONSUL_TOKEN` is empty in dev mode, set it blank 44 | 4. Start docker 45 | `docker-compose up --build` (use `-d` for no output to console but to `docker logs`) 46 | 47 | #### Stopping 48 | 49 | ```bash 50 | docker-compose down (or ctrl + c when attached) 51 | ``` 52 | 53 | Any further help regarding setting up the bot and getting everything working is not provided. 54 | ## Contributing 55 | Refer to [Running the bot](#running-the-bot) for the steps on how to run the bot and contribute. 56 | 57 | ## Credits 58 | - [Rapptz](https://github.com/Rapptz)' open sourced [RoboDanny](https://github.com/Rapptz/RoboDanny) discord bot for the following commands: 59 | - [charinfo][ETCPY] 60 | - [docs][DOCSPY] 61 | 62 | 63 | [DOCKER]: https://docs.docker.com/get-docker/ 64 | [CONSUL]: https://www.consul.io/ 65 | [NEXTCORDSERVER]: https://discord.gg/nextcord 66 | [PASTESERVICE]: https://paste.nextcord.dev 67 | [ENVFILE]: ./.env.example 68 | [AUTO_THREAD_CHANNEL_ID]: ./.env.example#L10 69 | [STARS_CHANNEL_ID]: ./.env.example#L9 70 | [DISCORDDOCS]: https://discord.com/developers/docs/intro 71 | [PYTHONDOCS]: https://docs.python.org/ 72 | [NEXTCORDDOCS]: https://docs.nextcord.dev/ 73 | [NEXTCORDREPOSITORY]: https://github.com/nextcord/nextcord 74 | [NEXTCORDREPOSITORYV3]: https://github.com/nextcord/nextcord-v3 75 | [AUTOPASTEPY]: ./cogs/autopaste.py 76 | [AUTOTHREADPY]: ./cogs/autothread.py 77 | [HELPPY]: ./cogs/help.py 78 | [DDOCSPY]: ./cogs/discorddoc.py 79 | [DOCSPY]: ./cogs/docs.py 80 | [STARSPY]: ./cogs/stars.py 81 | [ETCPY]: ./cogs/etc.py 82 | [DATABASEPY]: ./cogs/database.py 83 | [BOTLINKPY]: ./cogs/bot_linking.py 84 | -------------------------------------------------------------------------------- /assets/helpguide.txt: -------------------------------------------------------------------------------- 1 | :scroll: **When asking for help, we ask that you please follow these guidelines:** 2 | 3 | **Do not ping <@&891630950813925377> in your thread for help. This role is mentionable strictly for moderation incidents. Helpers will be added to the thread automatically.** 4 | 5 | **1. Please include your error message!** (if relevant) 6 | If your question is regarding your code not working, error messages assist the helpers in narrowing down the cause of the issue (what line it's on, what module is causing it, etc.). Not including any errors, tracebacks, etc. can slow things down considerably. 7 | 8 | **2. If you don't get an error message, please explain further!** 9 | Saying that your code throws _no_ error messages is still helpful, but the helpers still need more information to understand the issue you're having. For example, if your code is meant to execute a specific line of code but it does not, or you expect your code to show one thing but it instead shows something different or weird-looking, let us know! 10 | 11 | **3. Show your code with paste.nextcord.dev!** 12 | In order to give accurate assistance, the helpers need to see your code (or at least a small snippet of the relevant code). Please upload your code to [our paste service](https://paste.nextcord.dev) to help speed up the support process. We ask that you specifically upload your code to [our paste service](https://paste.nextcord.dev) instead of uploading a file or sending a code block on Discord because some helpers may be using a small screen (i.e. mobile devices) that has trouble accurately showing long lines of code. 13 | 14 | **4. Try some simple debugging!** 15 | If you're having trouble finding where the issue lies, one thing you can try is placing `print()` or `breakpoint()` statements at a few points in your code to see if a certain line is reached. If this doesn't help you fix the issue, it'll assist you in asking more specific questions to the helpers - [this relevant guide from Real Python is very helpful](https://realpython.com/lessons/print-and-breakpoint/). 16 | -------------------------------------------------------------------------------- /cogs/autopaste.py: -------------------------------------------------------------------------------- 1 | import re 2 | from typing import List, Optional, Tuple 3 | 4 | from nextcord import Message, Thread 5 | from nextcord.enums import ButtonStyle 6 | from nextcord.errors import HTTPException, NotFound 7 | from nextcord.ext.commands import Cog 8 | from nextcord.ext.commands.bot import Bot 9 | from nextcord.interactions import Interaction 10 | from nextcord.member import Member 11 | from nextcord.mentions import AllowedMentions 12 | from nextcord.ui import View, button 13 | 14 | from .help import HELP_CHANNEL_ID, HELP_MOD_ID, get_thread_author 15 | 16 | codeblock_regex = re.compile(r"`{3}(\w*) *\n(.*)\n`{3}", flags=re.DOTALL) 17 | 18 | discord_to_workbin = {"py": "python", "js": "javascript"} 19 | other_paste_services = ["pastebin.com", "sourceb.in", "srcb.in"] 20 | supported_content_types: List[str] = [ 21 | "text/plain", 22 | "text/markdown", 23 | "text/x-python", 24 | "application/json", 25 | "application/javascript", 26 | ] 27 | content_type_to_lang = { 28 | "text/plain": "text", 29 | "text/markdown": "markdown", 30 | "text/x-python": "python", 31 | "application/json": "json", 32 | "application/javascript": "javascript", 33 | } 34 | 35 | 36 | class DeleteMessage(View): 37 | def __init__(self, message_author: Member): 38 | self.message: Optional[Message] = None 39 | self.message_author = message_author 40 | super().__init__(timeout=300) # 300 seconds == 5 minutes 41 | 42 | @button(emoji="❌", style=ButtonStyle.secondary, custom_id="delete_autopaste") 43 | async def delete_autopaste(self, _, interaction: Interaction) -> None: 44 | try: 45 | await interaction.message.delete() # type: ignore 46 | except (HTTPException, NotFound): 47 | pass 48 | 49 | await self.on_timeout() 50 | 51 | async def on_timeout(self) -> None: 52 | self.stop() 53 | try: 54 | await self.message.edit(view=None) # type: ignore 55 | except (AttributeError, HTTPException, NotFound): 56 | pass 57 | 58 | async def interaction_check(self, interaction: Interaction) -> bool: 59 | if ( 60 | not interaction.guild 61 | or not interaction.user 62 | or not isinstance(interaction.user, Member) 63 | or not interaction.channel 64 | ): 65 | await self.on_timeout() 66 | return False 67 | 68 | if interaction.channel.permissions_for(interaction.user).manage_messages: # type: ignore 69 | return True 70 | 71 | if isinstance(interaction.channel, Thread) and interaction.channel.parent_id == HELP_CHANNEL_ID: 72 | thread_author = await get_thread_author(interaction.channel) 73 | if interaction.user.get_role(HELP_MOD_ID): 74 | return True 75 | elif thread_author.id == self.message_author.id and thread_author.id == interaction.user.id: 76 | return False 77 | elif interaction.user.id == self.message_author.id: 78 | return True 79 | 80 | return False 81 | 82 | 83 | class AutoPaste(Cog): 84 | def __init__(self, bot) -> None: 85 | self.bot: Bot = bot 86 | 87 | async def do_upload(self, content: str, language: str) -> str: 88 | res = await self.bot.session.post( # type: ignore 89 | url="https://paste.nextcord.dev/api/new", 90 | json={"content": str(content), "language": language}, 91 | headers={"Content-Type": "application/json"}, 92 | ) 93 | paste_id = (await res.json())["key"] 94 | return f"" 95 | 96 | @Cog.listener() 97 | async def on_message(self, message: Message): 98 | if message.author.bot: 99 | return 100 | if "pre-ignore" in message.content: 101 | return 102 | if message.content.startswith("!"): 103 | return 104 | 105 | delete_view: DeleteMessage = DeleteMessage(message.author) # type: ignore 106 | 107 | # Files 108 | if message.attachments: 109 | uploaded_files: List[Tuple[str, str]] = [] 110 | for attachment in message.attachments: 111 | if not attachment.content_type: 112 | continue 113 | 114 | content_type = attachment.content_type.split(";")[0].strip() 115 | if content_type not in supported_content_types: 116 | continue 117 | 118 | file_bytes = await attachment.read() 119 | if not bool(file_bytes.decode("utf-8")): 120 | # file is empty 121 | continue 122 | 123 | file_content = str(file_bytes.decode("utf-8")) 124 | language = content_type_to_lang.get(content_type, "text") 125 | if content_type == "text/plain": 126 | # yaml does not have its own type. 127 | if attachment.filename.endswith("yaml"): 128 | language = "yaml" 129 | elif ( 130 | isinstance(message.channel, Thread) 131 | and message.channel.parent_id == HELP_CHANNEL_ID 132 | ): 133 | language = "python" 134 | 135 | url = await self.do_upload(file_content, language) 136 | uploaded_files.append((attachment.filename, url)) 137 | 138 | if uploaded_files: 139 | if len(uploaded_files) == 1: 140 | message_content = ( 141 | f"Please avoid files for code. Posted to {uploaded_files[0][1]}" 142 | ) 143 | else: 144 | join_files = "\n".join( 145 | f"**{file_name}**:\n{url}" for file_name, url in uploaded_files 146 | ) 147 | message_content = f"Please avoid files for code. Posted {len(uploaded_files)} files:\n{join_files}" 148 | 149 | delete_view.message = await message.reply( 150 | message_content, 151 | allowed_mentions=AllowedMentions.none(), 152 | view=delete_view, 153 | ) 154 | return 155 | 156 | # Codeblocks 157 | regex_result = codeblock_regex.search(message.content) 158 | if regex_result is None: 159 | # Check for bad paste services 160 | for paste_service in other_paste_services: 161 | if paste_service in message.content: 162 | delete_view.message = await message.reply( 163 | "Please avoid other paste services than .", 164 | view=delete_view, 165 | ) 166 | return 167 | return 168 | 169 | language = regex_result.group(1) or "python" 170 | language = discord_to_workbin.get(language, language) 171 | code = regex_result.group(2) 172 | 173 | url = await self.do_upload(code, language) 174 | delete_view.message = await message.reply( 175 | f"Please avoid codeblocks for code. Posted to -> {url}", 176 | allowed_mentions=AllowedMentions.none(), 177 | view=delete_view, 178 | ) 179 | 180 | 181 | def setup(bot): 182 | bot.add_cog(AutoPaste(bot)) 183 | -------------------------------------------------------------------------------- /cogs/bot_linking.py: -------------------------------------------------------------------------------- 1 | from os import environ as env 2 | from typing import Literal 3 | 4 | import nextcord 5 | from nextcord.ext import commands, tasks 6 | from nextcord.mentions import AllowedMentions 7 | 8 | LOG_CHANNEL_ID = int(env["BOT_LINKING_LOG_CHANNEL_ID"]) 9 | GUILD_ID = int(env["GUILD_ID"]) 10 | BOOSTER_ROLE_ID = int(env["BOOSTER_ROLE_ID"]) 11 | 12 | 13 | class BotLinking(commands.Cog): 14 | def __init__(self, bot): 15 | self.bot = bot 16 | self.prune_loop.start() 17 | 18 | @commands.group() 19 | async def link(self, ctx): 20 | if ctx.invoked_subcommand is None: 21 | await ctx.send("No.") 22 | 23 | @link.command() 24 | @commands.has_permissions(administrator=True) 25 | async def add( 26 | self, 27 | ctx, 28 | status: Literal["booster", "admin", "special"], 29 | bot: nextcord.User, 30 | owner: nextcord.User, 31 | ): 32 | db = self.bot.get_cog("Database") 33 | 34 | if not bot.bot or owner.bot: 35 | await ctx.send("You fucked up the order. Good job.") 36 | return 37 | 38 | await db.set(f"bots/{bot.id}", {"owner_id": owner.id, "status": status}) 39 | 40 | await ctx.send(f"Successfully linked <@{bot.id}> to <@{owner.id}>.") 41 | 42 | @link.command() 43 | @commands.has_permissions(administrator=True) 44 | async def remove(self, ctx, bot: nextcord.Object): 45 | db = self.bot.get_cog("Database") 46 | 47 | current = await db.get(f"bots/{bot.id}") 48 | 49 | if not current: 50 | await ctx.send(f"This bot is not linked to anyone.") 51 | return 52 | 53 | await db.delete(f"bots/{bot.id}") 54 | 55 | await ctx.send(f"Successfully unlinked <@{bot.id}>.") 56 | 57 | @link.command() 58 | @commands.has_permissions(administrator=True) 59 | async def list(self, ctx): 60 | db = self.bot.get_cog("Database") 61 | 62 | bots = await db.list("bots/") 63 | 64 | if bots is None: 65 | await ctx.send("No bots linked.") 66 | return 67 | 68 | text = "" 69 | 70 | for bot_id, metadata in bots.items(): 71 | bot_id = bot_id.removeprefix("bots/") 72 | text += f"<@{bot_id}> linked to <@{metadata['owner_id']}> ({metadata['status']})\n" 73 | 74 | await ctx.send(text, allowed_mentions=AllowedMentions(users=[])) 75 | 76 | @link.command() 77 | @commands.has_permissions(administrator=True) 78 | async def prune(self, ctx): 79 | await self.prune_bots() 80 | await ctx.send("ok") 81 | 82 | @tasks.loop(seconds=30) 83 | async def prune_loop(self): 84 | await self.prune_bots() 85 | 86 | async def prune_bots(self): 87 | await self.bot.wait_until_ready() 88 | 89 | guild = self.bot.get_guild(GUILD_ID) 90 | log_channel = await guild.fetch_channel(LOG_CHANNEL_ID) 91 | 92 | db = self.bot.get_cog("Database") 93 | 94 | for user in guild.members: 95 | if not user.bot: 96 | continue 97 | metadata = await db.get(f"bots/{user.id}") 98 | if not metadata: 99 | # Bot is not linked, kick it 100 | try: 101 | await user.kick(reason="Unlinked bot") 102 | except nextcord.Forbidden: 103 | await log_channel.send( 104 | f"Failed to kick {user.mention} (bot not linked)" 105 | ) 106 | continue 107 | await log_channel.send( 108 | f"Unlinked bot {user.mention} found, bot has been kicked. Please use previous' link command." 109 | ) 110 | continue 111 | if metadata["status"] == "booster": 112 | owner = guild.get_member(metadata["owner_id"]) 113 | if owner is None: 114 | # Owner not in server? 115 | try: 116 | await user.kick(reason="Owner not in server") 117 | except nextcord.Forbidden: 118 | await log_channel.send( 119 | f"Failed to kick {user.mention} (owner not in server)" 120 | ) 121 | continue 122 | await log_channel.send( 123 | f"Owner <@{metadata['owner_id']}> not in server, {user.mention} has been kicked." 124 | ) 125 | continue 126 | if owner.get_role(BOOSTER_ROLE_ID) is None: 127 | # Owner unboosted 128 | try: 129 | await user.kick(reason="Owner unboosted") 130 | except nextcord.Forbidden: 131 | await log_channel.send( 132 | f"Failed to kick {user.mention} (owner unboosted)" 133 | ) 134 | continue 135 | await log_channel.send( 136 | f"Owner <@{metadata['owner_id']}> unboosted, {user.mention} has been kicked." 137 | ) 138 | 139 | try: 140 | await owner.send( 141 | f"As a result of you unboosting nextcord, your bot {user.mention} has been kicked. Please re-boost to get your bot added back." 142 | ) 143 | except nextcord.Forbidden: # 400 error lol? 144 | pass 145 | 146 | 147 | def setup(bot): 148 | bot.add_cog(BotLinking(bot)) 149 | -------------------------------------------------------------------------------- /cogs/database.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from base64 import b64decode 4 | from json import loads 5 | from os import environ as env 6 | 7 | from nextcord.ext import commands 8 | 9 | 10 | class Database(commands.Cog): 11 | def __init__(self, bot): 12 | self.bot = bot 13 | self.api_base: str = env["CONSUL_ADDR"] 14 | self.consul_token: str = env["CONSUL_TOKEN"] 15 | 16 | async def get(self, key: str) -> dict | None: 17 | r = await self.bot.session.get( 18 | f"{self.api_base}/v1/kv/previous/{key}", 19 | headers={"X-Consul-Token": self.consul_token}, 20 | ) 21 | 22 | if r.status == 404: 23 | return None 24 | 25 | data = await r.json() 26 | value = data[0]["Value"] 27 | 28 | return loads(b64decode(value).decode("utf-8")) 29 | 30 | async def set(self, key: str, value: dict) -> None: 31 | r = await self.bot.session.put( 32 | f"{self.api_base}/v1/kv/previous/{key}", 33 | headers={"X-Consul-Token": self.consul_token}, 34 | json=value, 35 | ) 36 | r.raise_for_status() 37 | 38 | async def delete(self, key: str) -> None: 39 | r = await self.bot.session.delete( 40 | f"{self.api_base}/v1/kv/previous/{key}", 41 | headers={"X-Consul-Token": self.consul_token}, 42 | ) 43 | r.raise_for_status() 44 | 45 | async def list(self, prefix: str) -> dict | None: 46 | r = await self.bot.session.get( 47 | f"{self.api_base}/v1/kv/previous/{prefix}", 48 | headers={"X-Consul-Token": self.consul_token}, 49 | params={"recurse": "true"}, 50 | ) 51 | 52 | if r.status == 404: 53 | return None 54 | 55 | r.raise_for_status() 56 | 57 | data = await r.json() 58 | return { 59 | item["Key"].removeprefix("previous/"): loads( 60 | b64decode(item["Value"]).decode("utf-8") 61 | ) 62 | for item in data 63 | } 64 | 65 | 66 | def setup(bot): 67 | bot.add_cog(Database(bot)) 68 | -------------------------------------------------------------------------------- /cogs/discorddoc.py: -------------------------------------------------------------------------------- 1 | import nextcord 2 | from algoliasearch.search_client import SearchClient 3 | from nextcord.ext import commands 4 | 5 | 6 | class DiscordHelp(commands.Cog): 7 | def __init__(self, bot: commands.Bot): 8 | self.bot = bot 9 | ## Fill out from trying a search on the ddevs portal 10 | app_id = "BH4D9OD16A" 11 | api_key = "f37d91bd900bbb124c8210cca9efcc01" 12 | self.search_client = SearchClient.create(app_id, api_key) 13 | self.index = self.search_client.init_index("discord") 14 | 15 | @commands.command(help="discord api docs") 16 | async def ddoc(self, ctx, *, search_term): 17 | results = await self.index.search_async(search_term) 18 | description = "" 19 | hits = [] 20 | for hit in results["hits"]: 21 | title = self.get_level_str(hit["hierarchy"]) 22 | if title in hits: 23 | continue 24 | hits.append(title) 25 | url = hit["url"].replace( 26 | "https://discord.com/developers/docs", "https://discord.dev" 27 | ) 28 | description += f"[{title}]({url})\n" 29 | if len(hits) == 10: 30 | break 31 | embed = nextcord.Embed( 32 | title="Your help has arrived!", 33 | description=description, 34 | color=nextcord.Color.random(), 35 | ) 36 | await ctx.send(embed=embed) 37 | 38 | def get_level_str(self, levels): 39 | last = "" 40 | for level in levels.values(): 41 | if level is not None: 42 | last = level 43 | return last 44 | 45 | 46 | def setup(bot): 47 | bot.add_cog(DiscordHelp(bot)) 48 | -------------------------------------------------------------------------------- /cogs/docs.py: -------------------------------------------------------------------------------- 1 | """ 2 | Mozilla Public License Version 2.0 3 | ================================== 4 | 5 | 1. Definitions 6 | -------------- 7 | 8 | 1.1. "Contributor" 9 | means each individual or legal entity that creates, contributes to 10 | the creation of, or owns Covered Software. 11 | 12 | 1.2. "Contributor Version" 13 | means the combination of the Contributions of others (if any) used 14 | by a Contributor and that particular Contributor's Contribution. 15 | 16 | 1.3. "Contribution" 17 | means Covered Software of a particular Contributor. 18 | 19 | 1.4. "Covered Software" 20 | means Source Code Form to which the initial Contributor has attached 21 | the notice in Exhibit A, the Executable Form of such Source Code 22 | Form, and Modifications of such Source Code Form, in each case 23 | including portions thereof. 24 | 25 | 1.5. "Incompatible With Secondary Licenses" 26 | means 27 | 28 | (a) that the initial Contributor has attached the notice described 29 | in Exhibit B to the Covered Software; or 30 | 31 | (b) that the Covered Software was made available under the terms of 32 | version 1.1 or earlier of the License, but not also under the 33 | terms of a Secondary License. 34 | 35 | 1.6. "Executable Form" 36 | means any form of the work other than Source Code Form. 37 | 38 | 1.7. "Larger Work" 39 | means a work that combines Covered Software with other material, in 40 | a separate file or files, that is not Covered Software. 41 | 42 | 1.8. "License" 43 | means this document. 44 | 45 | 1.9. "Licensable" 46 | means having the right to grant, to the maximum extent possible, 47 | whether at the time of the initial grant or subsequently, any and 48 | all of the rights conveyed by this License. 49 | 50 | 1.10. "Modifications" 51 | means any of the following: 52 | 53 | (a) any file in Source Code Form that results from an addition to, 54 | deletion from, or modification of the contents of Covered 55 | Software; or 56 | 57 | (b) any new file in Source Code Form that contains any Covered 58 | Software. 59 | 60 | 1.11. "Patent Claims" of a Contributor 61 | means any patent claim(s), including without limitation, method, 62 | process, and apparatus claims, in any patent Licensable by such 63 | Contributor that would be infringed, but for the grant of the 64 | License, by the making, using, selling, offering for sale, having 65 | made, import, or transfer of either its Contributions or its 66 | Contributor Version. 67 | 68 | 1.12. "Secondary License" 69 | means either the GNU General Public License, Version 2.0, the GNU 70 | Lesser General Public License, Version 2.1, the GNU Affero General 71 | Public License, Version 3.0, or any later versions of those 72 | licenses. 73 | 74 | 1.13. "Source Code Form" 75 | means the form of the work preferred for making modifications. 76 | 77 | 1.14. "You" (or "Your") 78 | means an individual or a legal entity exercising rights under this 79 | License. For legal entities, "You" includes any entity that 80 | controls, is controlled by, or is under common control with You. For 81 | purposes of this definition, "control" means (a) the power, direct 82 | or indirect, to cause the direction or management of such entity, 83 | whether by contract or otherwise, or (b) ownership of more than 84 | fifty percent (50%) of the outstanding shares or beneficial 85 | ownership of such entity. 86 | 87 | 2. License Grants and Conditions 88 | -------------------------------- 89 | 90 | 2.1. Grants 91 | 92 | Each Contributor hereby grants You a world-wide, royalty-free, 93 | non-exclusive license: 94 | 95 | (a) under intellectual property rights (other than patent or trademark) 96 | Licensable by such Contributor to use, reproduce, make available, 97 | modify, display, perform, distribute, and otherwise exploit its 98 | Contributions, either on an unmodified basis, with Modifications, or 99 | as part of a Larger Work; and 100 | 101 | (b) under Patent Claims of such Contributor to make, use, sell, offer 102 | for sale, have made, import, and otherwise transfer either its 103 | Contributions or its Contributor Version. 104 | 105 | 2.2. Effective Date 106 | 107 | The licenses granted in Section 2.1 with respect to any Contribution 108 | become effective for each Contribution on the date the Contributor first 109 | distributes such Contribution. 110 | 111 | 2.3. Limitations on Grant Scope 112 | 113 | The licenses granted in this Section 2 are the only rights granted under 114 | this License. No additional rights or licenses will be implied from the 115 | distribution or licensing of Covered Software under this License. 116 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 117 | Contributor: 118 | 119 | (a) for any code that a Contributor has removed from Covered Software; 120 | or 121 | 122 | (b) for infringements caused by: (i) Your and any other third party's 123 | modifications of Covered Software, or (ii) the combination of its 124 | Contributions with other software (except as part of its Contributor 125 | Version); or 126 | 127 | (c) under Patent Claims infringed by Covered Software in the absence of 128 | its Contributions. 129 | 130 | This License does not grant any rights in the trademarks, service marks, 131 | or logos of any Contributor (except as may be necessary to comply with 132 | the notice requirements in Section 3.4). 133 | 134 | 2.4. Subsequent Licenses 135 | 136 | No Contributor makes additional grants as a result of Your choice to 137 | distribute the Covered Software under a subsequent version of this 138 | License (see Section 10.2) or under the terms of a Secondary License (if 139 | permitted under the terms of Section 3.3). 140 | 141 | 2.5. Representation 142 | 143 | Each Contributor represents that the Contributor believes its 144 | Contributions are its original creation(s) or it has sufficient rights 145 | to grant the rights to its Contributions conveyed by this License. 146 | 147 | 2.6. Fair Use 148 | 149 | This License is not intended to limit any rights You have under 150 | applicable copyright doctrines of fair use, fair dealing, or other 151 | equivalents. 152 | 153 | 2.7. Conditions 154 | 155 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 156 | in Section 2.1. 157 | 158 | 3. Responsibilities 159 | ------------------- 160 | 161 | 3.1. Distribution of Source Form 162 | 163 | All distribution of Covered Software in Source Code Form, including any 164 | Modifications that You create or to which You contribute, must be under 165 | the terms of this License. You must inform recipients that the Source 166 | Code Form of the Covered Software is governed by the terms of this 167 | License, and how they can obtain a copy of this License. You may not 168 | attempt to alter or restrict the recipients' rights in the Source Code 169 | Form. 170 | 171 | 3.2. Distribution of Executable Form 172 | 173 | If You distribute Covered Software in Executable Form then: 174 | 175 | (a) such Covered Software must also be made available in Source Code 176 | Form, as described in Section 3.1, and You must inform recipients of 177 | the Executable Form how they can obtain a copy of such Source Code 178 | Form by reasonable means in a timely manner, at a charge no more 179 | than the cost of distribution to the recipient; and 180 | 181 | (b) You may distribute such Executable Form under the terms of this 182 | License, or sublicense it under different terms, provided that the 183 | license for the Executable Form does not attempt to limit or alter 184 | the recipients' rights in the Source Code Form under this License. 185 | 186 | 3.3. Distribution of a Larger Work 187 | 188 | You may create and distribute a Larger Work under terms of Your choice, 189 | provided that You also comply with the requirements of this License for 190 | the Covered Software. If the Larger Work is a combination of Covered 191 | Software with a work governed by one or more Secondary Licenses, and the 192 | Covered Software is not Incompatible With Secondary Licenses, this 193 | License permits You to additionally distribute such Covered Software 194 | under the terms of such Secondary License(s), so that the recipient of 195 | the Larger Work may, at their option, further distribute the Covered 196 | Software under the terms of either this License or such Secondary 197 | License(s). 198 | 199 | 3.4. Notices 200 | 201 | You may not remove or alter the substance of any license notices 202 | (including copyright notices, patent notices, disclaimers of warranty, 203 | or limitations of liability) contained within the Source Code Form of 204 | the Covered Software, except that You may alter any license notices to 205 | the extent required to remedy known factual inaccuracies. 206 | 207 | 3.5. Application of Additional Terms 208 | 209 | You may choose to offer, and to charge a fee for, warranty, support, 210 | indemnity or liability obligations to one or more recipients of Covered 211 | Software. However, You may do so only on Your own behalf, and not on 212 | behalf of any Contributor. You must make it absolutely clear that any 213 | such warranty, support, indemnity, or liability obligation is offered by 214 | You alone, and You hereby agree to indemnify every Contributor for any 215 | liability incurred by such Contributor as a result of warranty, support, 216 | indemnity or liability terms You offer. You may include additional 217 | disclaimers of warranty and limitations of liability specific to any 218 | jurisdiction. 219 | 220 | 4. Inability to Comply Due to Statute or Regulation 221 | --------------------------------------------------- 222 | 223 | If it is impossible for You to comply with any of the terms of this 224 | License with respect to some or all of the Covered Software due to 225 | statute, judicial order, or regulation then You must: (a) comply with 226 | the terms of this License to the maximum extent possible; and (b) 227 | describe the limitations and the code they affect. Such description must 228 | be placed in a text file included with all distributions of the Covered 229 | Software under this License. Except to the extent prohibited by statute 230 | or regulation, such description must be sufficiently detailed for a 231 | recipient of ordinary skill to be able to understand it. 232 | 233 | 5. Termination 234 | -------------- 235 | 236 | 5.1. The rights granted under this License will terminate automatically 237 | if You fail to comply with any of its terms. However, if You become 238 | compliant, then the rights granted under this License from a particular 239 | Contributor are reinstated (a) provisionally, unless and until such 240 | Contributor explicitly and finally terminates Your grants, and (b) on an 241 | ongoing basis, if such Contributor fails to notify You of the 242 | non-compliance by some reasonable means prior to 60 days after You have 243 | come back into compliance. Moreover, Your grants from a particular 244 | Contributor are reinstated on an ongoing basis if such Contributor 245 | notifies You of the non-compliance by some reasonable means, this is the 246 | first time You have received notice of non-compliance with this License 247 | from such Contributor, and You become compliant prior to 30 days after 248 | Your receipt of the notice. 249 | 250 | 5.2. If You initiate litigation against any entity by asserting a patent 251 | infringement claim (excluding declaratory judgment actions, 252 | counter-claims, and cross-claims) alleging that a Contributor Version 253 | directly or indirectly infringes any patent, then the rights granted to 254 | You by any and all Contributors for the Covered Software under Section 255 | 2.1 of this License shall terminate. 256 | 257 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 258 | end user license agreements (excluding distributors and resellers) which 259 | have been validly granted by You or Your distributors under this License 260 | prior to termination shall survive termination. 261 | 262 | ************************************************************************ 263 | * * 264 | * 6. Disclaimer of Warranty * 265 | * ------------------------- * 266 | * * 267 | * Covered Software is provided under this License on an "as is" * 268 | * basis, without warranty of any kind, either expressed, implied, or * 269 | * statutory, including, without limitation, warranties that the * 270 | * Covered Software is free of defects, merchantable, fit for a * 271 | * particular purpose or non-infringing. The entire risk as to the * 272 | * quality and performance of the Covered Software is with You. * 273 | * Should any Covered Software prove defective in any respect, You * 274 | * (not any Contributor) assume the cost of any necessary servicing, * 275 | * repair, or correction. This disclaimer of warranty constitutes an * 276 | * essential part of this License. No use of any Covered Software is * 277 | * authorized under this License except under this disclaimer. * 278 | * * 279 | ************************************************************************ 280 | 281 | ************************************************************************ 282 | * * 283 | * 7. Limitation of Liability * 284 | * -------------------------- * 285 | * * 286 | * Under no circumstances and under no legal theory, whether tort * 287 | * (including negligence), contract, or otherwise, shall any * 288 | * Contributor, or anyone who distributes Covered Software as * 289 | * permitted above, be liable to You for any direct, indirect, * 290 | * special, incidental, or consequential damages of any character * 291 | * including, without limitation, damages for lost profits, loss of * 292 | * goodwill, work stoppage, computer failure or malfunction, or any * 293 | * and all other commercial damages or losses, even if such party * 294 | * shall have been informed of the possibility of such damages. This * 295 | * limitation of liability shall not apply to liability for death or * 296 | * personal injury resulting from such party's negligence to the * 297 | * extent applicable law prohibits such limitation. Some * 298 | * jurisdictions do not allow the exclusion or limitation of * 299 | * incidental or consequential damages, so this exclusion and * 300 | * limitation may not apply to You. * 301 | * * 302 | ************************************************************************ 303 | 304 | 8. Litigation 305 | ------------- 306 | 307 | Any litigation relating to this License may be brought only in the 308 | courts of a jurisdiction where the defendant maintains its principal 309 | place of business and such litigation shall be governed by laws of that 310 | jurisdiction, without reference to its conflict-of-law provisions. 311 | Nothing in this Section shall prevent a party's ability to bring 312 | cross-claims or counter-claims. 313 | 314 | 9. Miscellaneous 315 | ---------------- 316 | 317 | This License represents the complete agreement concerning the subject 318 | matter hereof. If any provision of this License is held to be 319 | unenforceable, such provision shall be reformed only to the extent 320 | necessary to make it enforceable. Any law or regulation which provides 321 | that the language of a contract shall be construed against the drafter 322 | shall not be used to construe this License against a Contributor. 323 | 324 | 10. Versions of the License 325 | --------------------------- 326 | 327 | 10.1. New Versions 328 | 329 | Mozilla Foundation is the license steward. Except as provided in Section 330 | 10.3, no one other than the license steward has the right to modify or 331 | publish new versions of this License. Each version will be given a 332 | distinguishing version number. 333 | 334 | 10.2. Effect of New Versions 335 | 336 | You may distribute the Covered Software under the terms of the version 337 | of the License under which You originally received the Covered Software, 338 | or under the terms of any subsequent version published by the license 339 | steward. 340 | 341 | 10.3. Modified Versions 342 | 343 | If you create software not governed by this License, and you want to 344 | create a new license for such software, you may create and use a 345 | modified version of this License if you rename the license and remove 346 | any references to the name of the license steward (except to note that 347 | such modified license differs from this License). 348 | 349 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 350 | Licenses 351 | 352 | If You choose to distribute Source Code Form that is Incompatible With 353 | Secondary Licenses under the terms of this version of the License, the 354 | notice described in Exhibit B of this License must be attached. 355 | 356 | Exhibit A - Source Code Form License Notice 357 | ------------------------------------------- 358 | 359 | This Source Code Form is subject to the terms of the Mozilla Public 360 | License, v. 2.0. If a copy of the MPL was not distributed with this 361 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 362 | 363 | If it is not possible or desirable to put the notice in a particular 364 | file, then You may include the notice in a location (such as a LICENSE 365 | file in a relevant directory) where a recipient would be likely to look 366 | for such a notice. 367 | 368 | You may add additional accurate notices of copyright ownership. 369 | 370 | Exhibit B - "Incompatible With Secondary Licenses" Notice 371 | --------------------------------------------------------- 372 | 373 | This Source Code Form is "Incompatible With Secondary Licenses", as 374 | defined by the Mozilla Public License, v. 2.0. 375 | """ # R. Danny (https://github.com/Rapptz/RoboDanny) license 376 | 377 | 378 | import io 379 | import os 380 | import re 381 | import zlib 382 | from typing import Dict 383 | 384 | import nextcord as discord 385 | from nextcord.ext import commands 386 | 387 | from .utils import fuzzy 388 | 389 | 390 | class SphinxObjectFileReader: 391 | # Inspired by Sphinx's InventoryFileReader 392 | BUFSIZE = 16 * 1024 393 | 394 | def __init__(self, buffer): 395 | self.stream = io.BytesIO(buffer) 396 | 397 | def readline(self): 398 | return self.stream.readline().decode("utf-8") 399 | 400 | def skipline(self): 401 | self.stream.readline() 402 | 403 | def read_compressed_chunks(self): 404 | decompressor = zlib.decompressobj() 405 | while True: 406 | chunk = self.stream.read(self.BUFSIZE) 407 | if len(chunk) == 0: 408 | break 409 | yield decompressor.decompress(chunk) 410 | yield decompressor.flush() 411 | 412 | def read_compressed_lines(self): 413 | buf = b"" 414 | for chunk in self.read_compressed_chunks(): 415 | buf += chunk 416 | pos = buf.find(b"\n") 417 | while pos != -1: 418 | yield buf[:pos].decode("utf-8") 419 | buf = buf[pos + 1 :] 420 | pos = buf.find(b"\n") 421 | 422 | 423 | class Docs(commands.Cog): 424 | # full credit to https://github.com/Rapptz/RoboDanny 425 | def __init__(self, bot): 426 | self.bot = bot 427 | 428 | def parse_object_inv(self, stream: SphinxObjectFileReader, url: str) -> Dict: 429 | result = {} 430 | inv_version = stream.readline().rstrip() 431 | 432 | if inv_version != "# Sphinx inventory version 2": 433 | raise RuntimeError("Invalid objects.inv file version.") 434 | 435 | projname = stream.readline().rstrip()[11:] 436 | version = stream.readline().rstrip()[11:] # not needed 437 | 438 | line = stream.readline() 439 | if "zlib" not in line: 440 | raise RuntimeError("Invalid objects.inv file, not z-lib compatible.") 441 | 442 | entry_regex = re.compile(r"(?x)(.+?)\s+(\S*:\S*)\s+(-?\d+)\s+(\S+)\s+(.*)") 443 | for line in stream.read_compressed_lines(): 444 | match = entry_regex.match(line.rstrip()) 445 | if not match: 446 | continue 447 | 448 | name, directive, prio, location, dispname = match.groups() 449 | domain, _, subdirective = directive.partition(":") 450 | if directive == "py:module" and name in result: 451 | continue 452 | 453 | if directive == "std:doc": 454 | subdirective = "label" 455 | 456 | if location.endswith("$"): 457 | location = location[:-1] + name 458 | 459 | key = name if dispname == "-" else dispname 460 | prefix = f"{subdirective}:" if domain == "std" else "" 461 | 462 | key = ( 463 | key.replace("nextcord.ext.commands.", "") 464 | .replace("nextcord.ext.menus.", "") 465 | .replace("nextcord.ext.ipc.", "") 466 | .replace("nextcord.", "") 467 | ) 468 | 469 | result[f"{prefix}{key}"] = os.path.join(url, location) 470 | 471 | return result 472 | 473 | async def build_docs_lookup_table(self, page_types): 474 | cache = {} 475 | for key, page in page_types.items(): 476 | sub = cache[key] = {} 477 | async with self.bot.session.get(page + "/objects.inv") as resp: 478 | if resp.status != 200: 479 | raise RuntimeError( 480 | "Cannot build docs lookup table, try again later." 481 | ) 482 | 483 | stream = SphinxObjectFileReader(await resp.read()) 484 | cache[key] = self.parse_object_inv(stream, page) 485 | 486 | self._docs_cache = cache 487 | 488 | async def do_docs(self, ctx, key, obj): 489 | page_types = { 490 | "master": "https://docs.nextcord.dev/en/stable", 491 | "menus": "https://nextcord-ext-menus.readthedocs.io/en/latest", 492 | "ipc": "https://nextcord-ext-ipc.readthedocs.io/en/latest", 493 | "python": "https://docs.python.org/3", 494 | } 495 | 496 | if obj is None: 497 | await ctx.send(page_types[key]) 498 | return 499 | 500 | if not hasattr(self, "_docs_cache"): 501 | await ctx.trigger_typing() 502 | await self.build_docs_lookup_table(page_types) 503 | 504 | obj = re.sub(r"^(?:discord\.(?:ext\.)?)?(?:commands\.)?(.+)", r"\1", obj) 505 | obj = re.sub(r"^(?:nextcord\.(?:ext\.)?)?(?:commands\.)?(.+)", r"\1", obj) 506 | 507 | if key.startswith("master"): 508 | # point the abc.Messageable types properly: 509 | q = obj.lower() 510 | for name in dir(discord.abc.Messageable): 511 | if name[0] == "_": 512 | continue 513 | if q == name: 514 | obj = f"abc.Messageable.{name}" 515 | break 516 | 517 | cache = list(self._docs_cache[key].items()) 518 | 519 | def transform(tup): 520 | return tup[0] 521 | 522 | matches = fuzzy.finder(obj, cache, key=lambda t: t[0], lazy=False)[:8] 523 | 524 | e = discord.Embed(colour=discord.Colour.blurple()) 525 | if len(matches) == 0: 526 | return await ctx.send("Could not find anything. Sorry.") 527 | 528 | e.description = "\n".join(f"[`{key}`]({url})" for key, url in matches) 529 | ref = ctx.message.reference 530 | refer = None 531 | if ref and isinstance(ref.resolved, discord.Message): 532 | refer = ref.resolved.to_reference() 533 | await ctx.send(embed=e, reference=refer) 534 | 535 | @commands.group(name="docs", help="python docs", invoke_without_command=True) 536 | async def docs_group(self, ctx: commands.Context, *, obj: str = None): 537 | await self.do_docs(ctx, "master", obj) 538 | 539 | @docs_group.command(name="menus") 540 | async def docs_menu_cmd(self, ctx: commands.Context, *, obj: str = None): 541 | await self.do_docs(ctx, "menus", obj) 542 | 543 | @docs_group.command(name="ipc") 544 | async def docs_ipc_cmd(self, ctx: commands.Context, *, obj: str = None): 545 | await self.do_docs(ctx, "ipc", obj) 546 | 547 | @docs_group.command(name="python", aliases=["py"]) 548 | async def docs_python_cmd(self, ctx: commands.Context, *, obj: str = None): 549 | await self.do_docs(ctx, "python", obj) 550 | 551 | @commands.command( 552 | help="delete cache of docs (owner only)", aliases=["purge-docs", "deldocs"] 553 | ) 554 | @commands.is_owner() 555 | async def docscache(self, ctx: commands.Context): 556 | del self._docs_cache 557 | embed = discord.Embed(title="Purged docs cache.", color=discord.Color.blurple()) 558 | await ctx.send(embed=embed) 559 | 560 | 561 | def setup(bot): 562 | bot.add_cog(Docs(bot)) 563 | -------------------------------------------------------------------------------- /cogs/etc.py: -------------------------------------------------------------------------------- 1 | """ 2 | Mozilla Public License Version 2.0 3 | ================================== 4 | 5 | 1. Definitions 6 | -------------- 7 | 8 | 1.1. "Contributor" 9 | means each individual or legal entity that creates, contributes to 10 | the creation of, or owns Covered Software. 11 | 12 | 1.2. "Contributor Version" 13 | means the combination of the Contributions of others (if any) used 14 | by a Contributor and that particular Contributor's Contribution. 15 | 16 | 1.3. "Contribution" 17 | means Covered Software of a particular Contributor. 18 | 19 | 1.4. "Covered Software" 20 | means Source Code Form to which the initial Contributor has attached 21 | the notice in Exhibit A, the Executable Form of such Source Code 22 | Form, and Modifications of such Source Code Form, in each case 23 | including portions thereof. 24 | 25 | 1.5. "Incompatible With Secondary Licenses" 26 | means 27 | 28 | (a) that the initial Contributor has attached the notice described 29 | in Exhibit B to the Covered Software; or 30 | 31 | (b) that the Covered Software was made available under the terms of 32 | version 1.1 or earlier of the License, but not also under the 33 | terms of a Secondary License. 34 | 35 | 1.6. "Executable Form" 36 | means any form of the work other than Source Code Form. 37 | 38 | 1.7. "Larger Work" 39 | means a work that combines Covered Software with other material, in 40 | a separate file or files, that is not Covered Software. 41 | 42 | 1.8. "License" 43 | means this document. 44 | 45 | 1.9. "Licensable" 46 | means having the right to grant, to the maximum extent possible, 47 | whether at the time of the initial grant or subsequently, any and 48 | all of the rights conveyed by this License. 49 | 50 | 1.10. "Modifications" 51 | means any of the following: 52 | 53 | (a) any file in Source Code Form that results from an addition to, 54 | deletion from, or modification of the contents of Covered 55 | Software; or 56 | 57 | (b) any new file in Source Code Form that contains any Covered 58 | Software. 59 | 60 | 1.11. "Patent Claims" of a Contributor 61 | means any patent claim(s), including without limitation, method, 62 | process, and apparatus claims, in any patent Licensable by such 63 | Contributor that would be infringed, but for the grant of the 64 | License, by the making, using, selling, offering for sale, having 65 | made, import, or transfer of either its Contributions or its 66 | Contributor Version. 67 | 68 | 1.12. "Secondary License" 69 | means either the GNU General Public License, Version 2.0, the GNU 70 | Lesser General Public License, Version 2.1, the GNU Affero General 71 | Public License, Version 3.0, or any later versions of those 72 | licenses. 73 | 74 | 1.13. "Source Code Form" 75 | means the form of the work preferred for making modifications. 76 | 77 | 1.14. "You" (or "Your") 78 | means an individual or a legal entity exercising rights under this 79 | License. For legal entities, "You" includes any entity that 80 | controls, is controlled by, or is under common control with You. For 81 | purposes of this definition, "control" means (a) the power, direct 82 | or indirect, to cause the direction or management of such entity, 83 | whether by contract or otherwise, or (b) ownership of more than 84 | fifty percent (50%) of the outstanding shares or beneficial 85 | ownership of such entity. 86 | 87 | 2. License Grants and Conditions 88 | -------------------------------- 89 | 90 | 2.1. Grants 91 | 92 | Each Contributor hereby grants You a world-wide, royalty-free, 93 | non-exclusive license: 94 | 95 | (a) under intellectual property rights (other than patent or trademark) 96 | Licensable by such Contributor to use, reproduce, make available, 97 | modify, display, perform, distribute, and otherwise exploit its 98 | Contributions, either on an unmodified basis, with Modifications, or 99 | as part of a Larger Work; and 100 | 101 | (b) under Patent Claims of such Contributor to make, use, sell, offer 102 | for sale, have made, import, and otherwise transfer either its 103 | Contributions or its Contributor Version. 104 | 105 | 2.2. Effective Date 106 | 107 | The licenses granted in Section 2.1 with respect to any Contribution 108 | become effective for each Contribution on the date the Contributor first 109 | distributes such Contribution. 110 | 111 | 2.3. Limitations on Grant Scope 112 | 113 | The licenses granted in this Section 2 are the only rights granted under 114 | this License. No additional rights or licenses will be implied from the 115 | distribution or licensing of Covered Software under this License. 116 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 117 | Contributor: 118 | 119 | (a) for any code that a Contributor has removed from Covered Software; 120 | or 121 | 122 | (b) for infringements caused by: (i) Your and any other third party's 123 | modifications of Covered Software, or (ii) the combination of its 124 | Contributions with other software (except as part of its Contributor 125 | Version); or 126 | 127 | (c) under Patent Claims infringed by Covered Software in the absence of 128 | its Contributions. 129 | 130 | This License does not grant any rights in the trademarks, service marks, 131 | or logos of any Contributor (except as may be necessary to comply with 132 | the notice requirements in Section 3.4). 133 | 134 | 2.4. Subsequent Licenses 135 | 136 | No Contributor makes additional grants as a result of Your choice to 137 | distribute the Covered Software under a subsequent version of this 138 | License (see Section 10.2) or under the terms of a Secondary License (if 139 | permitted under the terms of Section 3.3). 140 | 141 | 2.5. Representation 142 | 143 | Each Contributor represents that the Contributor believes its 144 | Contributions are its original creation(s) or it has sufficient rights 145 | to grant the rights to its Contributions conveyed by this License. 146 | 147 | 2.6. Fair Use 148 | 149 | This License is not intended to limit any rights You have under 150 | applicable copyright doctrines of fair use, fair dealing, or other 151 | equivalents. 152 | 153 | 2.7. Conditions 154 | 155 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 156 | in Section 2.1. 157 | 158 | 3. Responsibilities 159 | ------------------- 160 | 161 | 3.1. Distribution of Source Form 162 | 163 | All distribution of Covered Software in Source Code Form, including any 164 | Modifications that You create or to which You contribute, must be under 165 | the terms of this License. You must inform recipients that the Source 166 | Code Form of the Covered Software is governed by the terms of this 167 | License, and how they can obtain a copy of this License. You may not 168 | attempt to alter or restrict the recipients' rights in the Source Code 169 | Form. 170 | 171 | 3.2. Distribution of Executable Form 172 | 173 | If You distribute Covered Software in Executable Form then: 174 | 175 | (a) such Covered Software must also be made available in Source Code 176 | Form, as described in Section 3.1, and You must inform recipients of 177 | the Executable Form how they can obtain a copy of such Source Code 178 | Form by reasonable means in a timely manner, at a charge no more 179 | than the cost of distribution to the recipient; and 180 | 181 | (b) You may distribute such Executable Form under the terms of this 182 | License, or sublicense it under different terms, provided that the 183 | license for the Executable Form does not attempt to limit or alter 184 | the recipients' rights in the Source Code Form under this License. 185 | 186 | 3.3. Distribution of a Larger Work 187 | 188 | You may create and distribute a Larger Work under terms of Your choice, 189 | provided that You also comply with the requirements of this License for 190 | the Covered Software. If the Larger Work is a combination of Covered 191 | Software with a work governed by one or more Secondary Licenses, and the 192 | Covered Software is not Incompatible With Secondary Licenses, this 193 | License permits You to additionally distribute such Covered Software 194 | under the terms of such Secondary License(s), so that the recipient of 195 | the Larger Work may, at their option, further distribute the Covered 196 | Software under the terms of either this License or such Secondary 197 | License(s). 198 | 199 | 3.4. Notices 200 | 201 | You may not remove or alter the substance of any license notices 202 | (including copyright notices, patent notices, disclaimers of warranty, 203 | or limitations of liability) contained within the Source Code Form of 204 | the Covered Software, except that You may alter any license notices to 205 | the extent required to remedy known factual inaccuracies. 206 | 207 | 3.5. Application of Additional Terms 208 | 209 | You may choose to offer, and to charge a fee for, warranty, support, 210 | indemnity or liability obligations to one or more recipients of Covered 211 | Software. However, You may do so only on Your own behalf, and not on 212 | behalf of any Contributor. You must make it absolutely clear that any 213 | such warranty, support, indemnity, or liability obligation is offered by 214 | You alone, and You hereby agree to indemnify every Contributor for any 215 | liability incurred by such Contributor as a result of warranty, support, 216 | indemnity or liability terms You offer. You may include additional 217 | disclaimers of warranty and limitations of liability specific to any 218 | jurisdiction. 219 | 220 | 4. Inability to Comply Due to Statute or Regulation 221 | --------------------------------------------------- 222 | 223 | If it is impossible for You to comply with any of the terms of this 224 | License with respect to some or all of the Covered Software due to 225 | statute, judicial order, or regulation then You must: (a) comply with 226 | the terms of this License to the maximum extent possible; and (b) 227 | describe the limitations and the code they affect. Such description must 228 | be placed in a text file included with all distributions of the Covered 229 | Software under this License. Except to the extent prohibited by statute 230 | or regulation, such description must be sufficiently detailed for a 231 | recipient of ordinary skill to be able to understand it. 232 | 233 | 5. Termination 234 | -------------- 235 | 236 | 5.1. The rights granted under this License will terminate automatically 237 | if You fail to comply with any of its terms. However, if You become 238 | compliant, then the rights granted under this License from a particular 239 | Contributor are reinstated (a) provisionally, unless and until such 240 | Contributor explicitly and finally terminates Your grants, and (b) on an 241 | ongoing basis, if such Contributor fails to notify You of the 242 | non-compliance by some reasonable means prior to 60 days after You have 243 | come back into compliance. Moreover, Your grants from a particular 244 | Contributor are reinstated on an ongoing basis if such Contributor 245 | notifies You of the non-compliance by some reasonable means, this is the 246 | first time You have received notice of non-compliance with this License 247 | from such Contributor, and You become compliant prior to 30 days after 248 | Your receipt of the notice. 249 | 250 | 5.2. If You initiate litigation against any entity by asserting a patent 251 | infringement claim (excluding declaratory judgment actions, 252 | counter-claims, and cross-claims) alleging that a Contributor Version 253 | directly or indirectly infringes any patent, then the rights granted to 254 | You by any and all Contributors for the Covered Software under Section 255 | 2.1 of this License shall terminate. 256 | 257 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 258 | end user license agreements (excluding distributors and resellers) which 259 | have been validly granted by You or Your distributors under this License 260 | prior to termination shall survive termination. 261 | 262 | ************************************************************************ 263 | * * 264 | * 6. Disclaimer of Warranty * 265 | * ------------------------- * 266 | * * 267 | * Covered Software is provided under this License on an "as is" * 268 | * basis, without warranty of any kind, either expressed, implied, or * 269 | * statutory, including, without limitation, warranties that the * 270 | * Covered Software is free of defects, merchantable, fit for a * 271 | * particular purpose or non-infringing. The entire risk as to the * 272 | * quality and performance of the Covered Software is with You. * 273 | * Should any Covered Software prove defective in any respect, You * 274 | * (not any Contributor) assume the cost of any necessary servicing, * 275 | * repair, or correction. This disclaimer of warranty constitutes an * 276 | * essential part of this License. No use of any Covered Software is * 277 | * authorized under this License except under this disclaimer. * 278 | * * 279 | ************************************************************************ 280 | 281 | ************************************************************************ 282 | * * 283 | * 7. Limitation of Liability * 284 | * -------------------------- * 285 | * * 286 | * Under no circumstances and under no legal theory, whether tort * 287 | * (including negligence), contract, or otherwise, shall any * 288 | * Contributor, or anyone who distributes Covered Software as * 289 | * permitted above, be liable to You for any direct, indirect, * 290 | * special, incidental, or consequential damages of any character * 291 | * including, without limitation, damages for lost profits, loss of * 292 | * goodwill, work stoppage, computer failure or malfunction, or any * 293 | * and all other commercial damages or losses, even if such party * 294 | * shall have been informed of the possibility of such damages. This * 295 | * limitation of liability shall not apply to liability for death or * 296 | * personal injury resulting from such party's negligence to the * 297 | * extent applicable law prohibits such limitation. Some * 298 | * jurisdictions do not allow the exclusion or limitation of * 299 | * incidental or consequential damages, so this exclusion and * 300 | * limitation may not apply to You. * 301 | * * 302 | ************************************************************************ 303 | 304 | 8. Litigation 305 | ------------- 306 | 307 | Any litigation relating to this License may be brought only in the 308 | courts of a jurisdiction where the defendant maintains its principal 309 | place of business and such litigation shall be governed by laws of that 310 | jurisdiction, without reference to its conflict-of-law provisions. 311 | Nothing in this Section shall prevent a party's ability to bring 312 | cross-claims or counter-claims. 313 | 314 | 9. Miscellaneous 315 | ---------------- 316 | 317 | This License represents the complete agreement concerning the subject 318 | matter hereof. If any provision of this License is held to be 319 | unenforceable, such provision shall be reformed only to the extent 320 | necessary to make it enforceable. Any law or regulation which provides 321 | that the language of a contract shall be construed against the drafter 322 | shall not be used to construe this License against a Contributor. 323 | 324 | 10. Versions of the License 325 | --------------------------- 326 | 327 | 10.1. New Versions 328 | 329 | Mozilla Foundation is the license steward. Except as provided in Section 330 | 10.3, no one other than the license steward has the right to modify or 331 | publish new versions of this License. Each version will be given a 332 | distinguishing version number. 333 | 334 | 10.2. Effect of New Versions 335 | 336 | You may distribute the Covered Software under the terms of the version 337 | of the License under which You originally received the Covered Software, 338 | or under the terms of any subsequent version published by the license 339 | steward. 340 | 341 | 10.3. Modified Versions 342 | 343 | If you create software not governed by this License, and you want to 344 | create a new license for such software, you may create and use a 345 | modified version of this License if you rename the license and remove 346 | any references to the name of the license steward (except to note that 347 | such modified license differs from this License). 348 | 349 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 350 | Licenses 351 | 352 | If You choose to distribute Source Code Form that is Incompatible With 353 | Secondary Licenses under the terms of this version of the License, the 354 | notice described in Exhibit B of this License must be attached. 355 | 356 | Exhibit A - Source Code Form License Notice 357 | ------------------------------------------- 358 | 359 | This Source Code Form is subject to the terms of the Mozilla Public 360 | License, v. 2.0. If a copy of the MPL was not distributed with this 361 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 362 | 363 | If it is not possible or desirable to put the notice in a particular 364 | file, then You may include the notice in a location (such as a LICENSE 365 | file in a relevant directory) where a recipient would be likely to look 366 | for such a notice. 367 | 368 | You may add additional accurate notices of copyright ownership. 369 | 370 | Exhibit B - "Incompatible With Secondary Licenses" Notice 371 | --------------------------------------------------------- 372 | 373 | This Source Code Form is "Incompatible With Secondary Licenses", as 374 | defined by the Mozilla Public License, v. 2.0. 375 | """ # R. Danny (https://github.com/Rapptz/RoboDanny) license 376 | 377 | import unicodedata 378 | 379 | from discord.ext import commands 380 | 381 | 382 | class Etc(commands.Cog): 383 | @commands.command() 384 | async def charinfo(self, ctx, *, characters: str): 385 | """ 386 | Shows you information about a number of characters. 387 | Only up to 25 characters at a time. 388 | """ 389 | 390 | def to_string(c): 391 | digit = f"{ord(c):x}" 392 | name = unicodedata.name(c, "Name not found.") 393 | return f"`\\U{digit:>08}`: {name} - {c} \N{EM DASH} " 394 | 395 | msg = "\n".join(map(to_string, characters)) 396 | if len(msg) > 2000: 397 | return await ctx.send("Output too long to display.") 398 | await ctx.send(msg) 399 | 400 | 401 | def setup(bot): 402 | bot.add_cog(Etc()) 403 | -------------------------------------------------------------------------------- /cogs/help.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from typing import Dict, Union 3 | 4 | from asyncio import TimeoutError 5 | from datetime import timedelta 6 | from os import environ as env 7 | from re import match 8 | 9 | from nextcord import ( 10 | Button, 11 | ButtonStyle, 12 | ChannelType, 13 | ClientUser, 14 | Colour, 15 | Embed, 16 | Forbidden, 17 | HTTPException, 18 | Interaction, 19 | Member, 20 | Message, 21 | MessageType, 22 | Thread, 23 | ThreadMember, 24 | ui, 25 | utils, 26 | slash_command, 27 | SlashOption, 28 | ) 29 | 30 | from nextcord.ext import commands, tasks 31 | from nextcord.ext.application_checks import has_role as application_checks_has_role 32 | 33 | from .utils.split_txtfile import split_txtfile 34 | 35 | HELP_CHANNEL_ID: int = int(env["HELP_CHANNEL_ID"]) 36 | HELP_LOGS_CHANNEL_ID: int = int(env["HELP_LOG_CHANNEL_ID"]) 37 | HELPER_ROLE_ID: int = int(env["HELP_NOTIFICATION_ROLE_ID"]) 38 | HELP_MOD_ID: int = int(env["HELP_MOD_ROLE_ID"]) 39 | HELP_BANNED_ID: int = int(env["HELP_BANNED_ROLE_ID"]) 40 | GUILD_ID: int = int(env["GUILD_ID"]) 41 | CUSTOM_ID_PREFIX: str = "help:" 42 | NAME_TOPIC_REGEX: str = r"^(?P.*?) \((?P[^)]*[^(]*)\)$" 43 | WAIT_FOR_TIMEOUT: int = 1800 # 30 minutes 44 | NO_HELP_MESSAGE: str = "You are banned from creating help threads. DM Modmail if you want to appeal it." 45 | HELP_TOPIC_EMOJIS: Dict[str, str] = { 46 | "🤚": "Help Needed", 47 | "🔥": "Active", 48 | "⏳": "Stalled", 49 | "🛠️": "Development Version", 50 | "🐛": "Bug Report", 51 | "📌": "Pinned", 52 | "⚠️": "ToS", 53 | } 54 | 55 | closing_message = ( 56 | "If your question has not been answered or your issue not " 57 | "resolved, we suggest taking a look at [Python Discord's Guide to " 58 | "Asking Good Questions](https://www.pythondiscord.com/pages/guides/pydis-guides/asking-good-questions/) " 59 | "to get more effective help." 60 | ) 61 | 62 | 63 | async def get_thread_author(channel: Thread) -> Member: 64 | history = channel.history(oldest_first=True, limit=1) 65 | history_flat = await history.flatten() 66 | user = history_flat[0].mentions[0] 67 | return user 68 | 69 | 70 | async def close_help_thread( 71 | method: str, 72 | thread_channel: Thread, 73 | thread_author: Member, 74 | closed_by: Union[Member, ClientUser], 75 | ): 76 | """Closes a help thread. Is called from either the close button or the 77 | =close command. 78 | """ 79 | 80 | # no need to do any of this if the thread is already closed. 81 | if thread_channel.locked or thread_channel.archived: 82 | return 83 | 84 | if not thread_channel.last_message or not thread_channel.last_message_id: 85 | _last_msg = (await thread_channel.history(limit=1).flatten())[0] 86 | else: 87 | _last_msg = thread_channel.get_partial_message(thread_channel.last_message_id) 88 | 89 | thread_jump_url = _last_msg.jump_url 90 | topic = match(NAME_TOPIC_REGEX, thread_channel.name).group("topic") # type: ignore 91 | 92 | embed_reply = Embed( 93 | title="This thread has now been closed", 94 | description=closing_message, 95 | colour=Colour.dark_theme(), 96 | ) 97 | 98 | await thread_channel.send(embed=embed_reply) # Send the closing message to the help thread 99 | await thread_channel.edit(locked=True, archived=True) # Lock thread 100 | # Send log 101 | embed_log = Embed( 102 | title=":x: Closed help thread", 103 | url=thread_channel.jump_url, 104 | description=( 105 | f"{thread_channel.mention}\n\nHelp thread created by {thread_author.mention} has been closed by {closed_by.mention} " 106 | f"using **{method}**.\n\n" 107 | f"Thread author: `{thread_author} ({thread_author.id})`\n" 108 | f"Closed by: `{closed_by} ({closed_by.id})`" 109 | ), 110 | colour=0xDD2E44, # Red 111 | ) 112 | await thread_channel.guild.get_channel(HELP_LOGS_CHANNEL_ID).send(embed=embed_log) 113 | # Make some slight changes to the previous thread-closer embed 114 | # to send to the user via DM. 115 | embed_reply.title = "Your help thread in the Nextcord server has been closed." 116 | embed_reply.description += f"\n\nTopic: **{topic}**\n\nYou can use [**this link**]({thread_jump_url}) to access the archived thread for future reference." 117 | if thread_channel.guild.icon: 118 | embed_reply.set_thumbnail(url=thread_channel.guild.icon.url) 119 | try: 120 | await thread_author.send(embed=embed_reply) 121 | except (HTTPException, Forbidden): 122 | pass 123 | 124 | 125 | class HelpButton(ui.Button["HelpView"]): 126 | def __init__(self, help_type: str, *, style: ButtonStyle, custom_id: str): 127 | super().__init__( 128 | label=f"{help_type} help", 129 | style=style, 130 | custom_id=f"{CUSTOM_ID_PREFIX}{custom_id}", 131 | ) 132 | self._help_type: str = help_type 133 | 134 | async def create_help_thread(self, interaction: Interaction) -> Thread: 135 | thread = await interaction.channel.create_thread( 136 | name=f"{self._help_type} help ({interaction.user})", 137 | type=ChannelType.public_thread, 138 | ) 139 | 140 | # Send log 141 | embed_log = Embed( 142 | title=":white_check_mark: Help thread created", 143 | url=thread.jump_url, 144 | description=( 145 | f"{thread.mention}\n\n" 146 | f"Help thread for **{self._help_type}** created by {interaction.user.mention}!\n\n" 147 | f"Created by: `{interaction.user} ({interaction.user.id})`" 148 | ), 149 | colour=0x77B255, # Green 150 | ) 151 | await interaction.guild.get_channel(HELP_LOGS_CHANNEL_ID).send(embed=embed_log) 152 | 153 | type_to_colour: Dict[str, Colour] = { 154 | "Nextcord": Colour.blurple(), 155 | "Python": Colour.green(), 156 | } 157 | 158 | em = Embed( 159 | title=f"{self._help_type} Help thread", 160 | colour=type_to_colour.get(self._help_type, Colour.blurple()), 161 | description=( 162 | "Please explain your issue in detail, helpers will respond as soon as possible." 163 | "\n\n**Please include the following in your initial message:**" 164 | "\n- Relevant code\n- Error (if present)\n- Expected behavior" 165 | f"\n\nRefer for more to our help guildlines in <#{HELP_CHANNEL_ID}>" 166 | ), 167 | ) 168 | em.set_footer(text="You can close this thread with the button or =close command.") 169 | 170 | close_button_view = ThreadCloseView() 171 | 172 | msg = await thread.send(content=interaction.user.mention, embed=em, view=close_button_view) 173 | # it's a persistent view, we only need the button. 174 | close_button_view.stop() 175 | await msg.pin(reason="First message in help thread with the close button.") 176 | return thread 177 | 178 | async def __launch_wait_for_message(self, thread: Thread, interaction: Interaction) -> None: 179 | assert self.view is not None 180 | 181 | def is_allowed(message: Message) -> bool: 182 | return message.author.id == interaction.user.id and message.channel.id == thread.id and not thread.archived # type: ignore 183 | 184 | try: 185 | await self.view.bot.wait_for("message", timeout=WAIT_FOR_TIMEOUT, check=is_allowed) 186 | except TimeoutError: 187 | await close_help_thread( 188 | "TIMEOUT [launch_wait_for_message]", 189 | thread, 190 | interaction.user, 191 | self.view.bot.user, 192 | ) 193 | return 194 | else: 195 | await thread.send(f"<@&{HELPER_ROLE_ID}>", delete_after=5) 196 | return 197 | 198 | async def callback(self, interaction: Interaction): 199 | confirm_view = ConfirmView() 200 | 201 | def disable_all_buttons(): 202 | for _item in confirm_view.children: 203 | _item.disabled = True 204 | 205 | confirm_content = f"Are you really sure you want to make a {self._help_type} help thread?" 206 | await interaction.send(content=confirm_content, ephemeral=True, view=confirm_view) 207 | await confirm_view.wait() 208 | if confirm_view.value is False or confirm_view.value is None: 209 | disable_all_buttons() 210 | content = "Ok, cancelled." if confirm_view.value is False else f"~~{confirm_content}~~ I guess not..." 211 | await interaction.edit_original_message(content=content, view=confirm_view) 212 | else: 213 | disable_all_buttons() 214 | await interaction.edit_original_message(content="Created!", view=confirm_view) 215 | created_thread = await self.create_help_thread(interaction) 216 | await self.__launch_wait_for_message(created_thread, interaction) 217 | 218 | 219 | class HelpView(ui.View): 220 | def __init__(self, bot: commands.Bot): 221 | super().__init__(timeout=None) 222 | self.bot: commands.Bot = bot 223 | 224 | self.add_item(HelpButton("Nextcord", style=ButtonStyle.blurple, custom_id="nextcord")) 225 | self.add_item(HelpButton("Python", style=ButtonStyle.green, custom_id="python")) 226 | 227 | async def interaction_check(self, interaction: Interaction): 228 | if interaction.user.get_role(HELP_BANNED_ID) is not None: 229 | await interaction.send(NO_HELP_MESSAGE, ephemeral=True) 230 | return False 231 | 232 | return True 233 | 234 | 235 | class ConfirmButton(ui.Button["ConfirmView"]): 236 | def __init__(self, label: str, style: ButtonStyle, *, custom_id: str): 237 | super().__init__(label=label, style=style, custom_id=f"{CUSTOM_ID_PREFIX}{custom_id}") 238 | 239 | async def callback(self, interaction: Interaction): 240 | self.view.value = True if self.custom_id == f"{CUSTOM_ID_PREFIX}confirm_button" else False 241 | self.view.stop() 242 | 243 | 244 | class ConfirmView(ui.View): 245 | def __init__(self): 246 | super().__init__(timeout=10.0) 247 | self.value = None 248 | self.add_item(ConfirmButton("Yes", ButtonStyle.green, custom_id="confirm_button")) 249 | self.add_item(ConfirmButton("No", ButtonStyle.red, custom_id="decline_button")) 250 | 251 | 252 | class ThreadCloseView(ui.View): 253 | def __init__(self): 254 | super().__init__(timeout=None) 255 | 256 | @ui.button(label="Close", style=ButtonStyle.red, custom_id=f"{CUSTOM_ID_PREFIX}thread_close") # type: ignore 257 | async def thread_close_button(self, button: Button, interaction: Interaction): 258 | button.disabled = True 259 | await interaction.response.edit_message(view=self) 260 | thread_author = await get_thread_author(interaction.channel) # type: ignore 261 | await close_help_thread("BUTTON", interaction.channel, thread_author, interaction.user) 262 | 263 | async def interaction_check(self, interaction: Interaction) -> bool: 264 | 265 | # because we aren't assigning the persistent view to a message_id. 266 | if not isinstance(interaction.channel, Thread) or interaction.channel.parent_id != HELP_CHANNEL_ID: 267 | return False 268 | 269 | if interaction.channel.archived or interaction.channel.locked: # type: ignore 270 | return False 271 | 272 | thread_author = await get_thread_author(interaction.channel) # type: ignore 273 | if interaction.user.id == thread_author.id or interaction.user.get_role(HELP_MOD_ID): # type: ignore 274 | return True 275 | else: 276 | await interaction.send("You are not allowed to close this thread.", ephemeral=True) 277 | return False 278 | 279 | 280 | class HelpCog(commands.Cog): 281 | def __init__(self, bot: commands.Bot): 282 | self.bot = bot 283 | self.close_empty_threads.start() 284 | self.bot.loop.create_task(self.create_views()) 285 | 286 | async def create_views(self): 287 | if getattr(self.bot, "help_view_set", False) is False: 288 | self.bot.help_view_set = True 289 | self.bot.add_view(HelpView(self.bot)) 290 | self.bot.add_view(ThreadCloseView()) 291 | 292 | @commands.Cog.listener() 293 | async def on_message(self, message): 294 | if message.channel.id == HELP_CHANNEL_ID and message.type is MessageType.thread_created: 295 | await message.delete(delay=5) 296 | if ( 297 | isinstance(message.channel, Thread) 298 | and message.channel.parent_id == HELP_CHANNEL_ID 299 | and message.type is MessageType.pins_add 300 | ): 301 | await message.delete(delay=10) 302 | 303 | @commands.Cog.listener() 304 | async def on_thread_member_remove(self, member: ThreadMember): 305 | thread = member.thread 306 | if thread.parent_id != HELP_CHANNEL_ID or thread.archived: 307 | return 308 | 309 | thread_author = await get_thread_author(thread) 310 | if member.id != thread_author.id: 311 | return 312 | 313 | await close_help_thread("EVENT [thread_member_remove]", thread, thread_author, self.bot.user) 314 | 315 | @tasks.loop(hours=24) 316 | async def close_empty_threads(self): 317 | await self.bot.wait_until_ready() 318 | 319 | guild = self.bot.get_guild(GUILD_ID) 320 | active_help_threads = [ 321 | thread 322 | for thread in await guild.active_threads() 323 | if thread.parent_id == HELP_CHANNEL_ID and (not thread.locked and not thread.archived) 324 | ] 325 | 326 | thread: Thread 327 | for thread in active_help_threads: 328 | thread_created_at = utils.snowflake_time(thread.id) 329 | 330 | # We don't want to close it before the wait_for. 331 | if utils.utcnow() - timedelta(seconds=WAIT_FOR_TIMEOUT) <= thread_created_at: 332 | continue 333 | 334 | all_messages = [ 335 | message 336 | for message in (await thread.history(limit=3, oldest_first=True).flatten()) 337 | if message.type is MessageType.default 338 | ] 339 | # can happen, ignore. 340 | if not all_messages or not (all_messages and all_messages[0].mentions): 341 | continue 342 | 343 | thread_author = all_messages[0].mentions[0] 344 | if len(all_messages) >= 2: 345 | members = [x.id for x in await thread.fetch_members()] 346 | if all_messages[1].author == thread_author and members == [ 347 | thread_author.id, 348 | guild.me.id, 349 | ]: 350 | await thread.send(f"<@&{HELPER_ROLE_ID}>", delete_after=5) 351 | continue 352 | else: 353 | await close_help_thread("TASK [close_empty_threads]", thread, thread_author, self.bot.user) 354 | continue 355 | 356 | @commands.command() 357 | @commands.is_owner() 358 | async def help_menu(self, ctx): 359 | for section in split_txtfile("helpguide.txt"): 360 | await ctx.send(embed=Embed(description=section, color=Colour.yellow())) 361 | 362 | await ctx.send( 363 | content="**:white_check_mark: If you've read the guidelines above, click a button to create a help thread!**", 364 | view=HelpView(self.bot), 365 | ) 366 | 367 | @commands.command() 368 | async def close(self, ctx): 369 | if not isinstance(ctx.channel, Thread) or ctx.channel.parent_id != HELP_CHANNEL_ID: 370 | return 371 | 372 | thread_author = await get_thread_author(ctx.channel) 373 | if not (ctx.author.id == thread_author.id or ctx.author.get_role(HELP_MOD_ID)): 374 | return await ctx.send("You are not allowed to close this thread.") 375 | 376 | await close_help_thread("COMMAND", ctx.channel, thread_author, ctx.author) 377 | 378 | @commands.command() 379 | @commands.has_role(HELP_MOD_ID) 380 | async def transfer(self, ctx, *, new_author: Member): 381 | if not (isinstance(ctx.channel, Thread) and ctx.channel.parent_id == HELP_CHANNEL_ID): # type: ignore 382 | return await ctx.send("This command can only be used in help threads!") 383 | 384 | topic = match(NAME_TOPIC_REGEX, ctx.channel.name).group("topic") # type: ignore 385 | first_thread_message = (await ctx.channel.history(limit=1, oldest_first=True).flatten())[0] 386 | old_author = first_thread_message.mentions[0] 387 | 388 | await ctx.channel.edit(name=f"{topic} ({new_author})") 389 | await first_thread_message.edit(content=new_author.mention) 390 | # Send log 391 | embed_log = Embed( 392 | title=":arrow_right: Help thread transferred", 393 | url=ctx.channel.jump_url, 394 | description=( 395 | f"{ctx.channel.mention}\n\nHelp thread created by {old_author.mention} " 396 | f"has been transferred to {new_author.mention} by {ctx.author.mention}.\n\n" 397 | f"Thread author: `{old_author} ({old_author.id})`\n" 398 | f"New author: `{new_author} ({new_author.id})`\n" 399 | f"Transferred by: `{ctx.author} ({ctx.author.id})`" 400 | ), 401 | colour=0x3B88C3, # Blue 402 | ) 403 | await ctx.guild.get_channel(HELP_LOGS_CHANNEL_ID).send(embed=embed_log) 404 | 405 | @slash_command(guild_ids=[GUILD_ID]) 406 | @application_checks_has_role(HELP_MOD_ID) 407 | async def topic( 408 | self, 409 | interaction: Interaction, 410 | *, 411 | topic: str = SlashOption( 412 | description="The thread's topic.", 413 | ), 414 | emoji: str = SlashOption( 415 | description="The emoji to use for the topic.", 416 | choices={f"{key} {value}": key for key, value in HELP_TOPIC_EMOJIS.items()}, 417 | ), 418 | ) -> None: 419 | """Set the topic of a help thread.""" 420 | if not (isinstance(interaction.channel, Thread) and interaction.channel.parent.id == HELP_CHANNEL_ID): # type: ignore # channel can't be None here 421 | await interaction.send( 422 | f"This command can only be used in channel inherited from <#{HELP_CHANNEL_ID}>", ephemeral=True 423 | ) 424 | return 425 | 426 | # helper to ignore any error while editing 427 | async def _edit_message(message, new_content: str) -> None: 428 | try: 429 | await message.edit(content=new_content) 430 | except Exception: 431 | pass 432 | 433 | # Prevent "application did not respond" client error by responding first, 434 | # easier to handle errors this way. 435 | message = await interaction.send(f"Setting the topic, please wait...", ephemeral=True) 436 | 437 | previous_values = match(NAME_TOPIC_REGEX, interaction.channel.name) 438 | if not previous_values: 439 | await _edit_message( 440 | message, "❌ **Error:** This help thread has an invalid name. Please contact a moderator." 441 | ) 442 | return 443 | 444 | current_topic, author = previous_values.groups() 445 | # max channel name length is 100 446 | # 5 = "[emoji]< >[topic]< ><(>[author]<)>" + 1 extra space to be sure 447 | allowed_length = 100 - (len(author) + len(emoji) + 5) 448 | new_topic = f"{emoji} {topic}" 449 | 450 | if new_topic == current_topic: 451 | await _edit_message(message, f"❌ **Error:** That is the current topic!") 452 | return 453 | 454 | if len(topic) > allowed_length: 455 | await _edit_message( 456 | message, 457 | ( 458 | f"❌ **Error:** That topic is too long! The maximum length is {allowed_length} characters. " 459 | f"You have {len(topic)} characters." 460 | ), 461 | ) 462 | return 463 | 464 | colour = interaction.user.colour # type: ignore 465 | if colour == Colour.default(): 466 | colour = Colour.blurple() 467 | 468 | author_embed = Embed( 469 | description=f"🗨️ {interaction.user.mention} changed the topic of this thread to:\n**{emoji} {topic} ({author})**", # type: ignore 470 | colour=colour, 471 | ) 472 | 473 | try: 474 | # wait 10 seconds for it to return something 475 | # then assume we're being rate limited 476 | # the lib waits for it to run out and tries again 477 | # editing shouldn't take that long 478 | await asyncio.wait_for(interaction.channel.edit(name=f"{new_topic} ({author})"), timeout=10) 479 | except asyncio.TimeoutError: 480 | await _edit_message( 481 | message, 482 | ( 483 | "❌ **Error:** We are being rate limited. There is a ratelimit of 2 changes per 10 minutes. " 484 | "Please try again in ~10 minutes." 485 | ), 486 | ) 487 | return 488 | except Exception as e: 489 | await _edit_message( 490 | message, f"❌ **Error:** Something went wrong while changing the topic of this thread.\n\n{e}" 491 | ) 492 | return 493 | else: 494 | await _edit_message(message, "✅ **Success:** Topic changed!") 495 | # this falls back to Interaction.followup so is safe to use here. 496 | await interaction.send(embed=author_embed) 497 | 498 | 499 | def setup(bot): 500 | bot.add_cog(HelpCog(bot)) 501 | -------------------------------------------------------------------------------- /cogs/roles.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from os import environ as env 4 | from typing import TYPE_CHECKING 5 | 6 | from nextcord import Interaction, Member, Object, SelectOption, slash_command 7 | from nextcord.ext.commands import Cog 8 | from nextcord.ui import Select, View 9 | from nextcord.utils import get 10 | 11 | if TYPE_CHECKING: 12 | from nextcord.abc import Snowflake 13 | 14 | GUILD_ID = int(env["GUILD_ID"]) 15 | ASSIGNABLE_ROLE_IDS = {int(r) for r in env["ASSIGNABLE_ROLE_IDS"].split(",")} 16 | 17 | 18 | class RolesView(View): 19 | def __init__(self, *, member: Member): 20 | super().__init__(timeout=None) 21 | 22 | self.add_item(RolesSelect(member=member)) 23 | 24 | 25 | class RolesSelect(Select["RolesView"]): 26 | def __init__(self, *, member: Member): 27 | super().__init__( 28 | placeholder="Select your new roles", 29 | min_values=0, 30 | max_values=len(ASSIGNABLE_ROLE_IDS), 31 | options=[ 32 | SelectOption( 33 | label=member.guild.get_role(role_id).name, # type: ignore 34 | value=str(role_id), 35 | default=member.get_role(role_id) is not None, 36 | ) 37 | for role_id in ASSIGNABLE_ROLE_IDS 38 | ], 39 | ) 40 | 41 | async def callback(self, interaction: Interaction): 42 | assert isinstance(interaction.user, Member) 43 | 44 | roles: list[Snowflake] = interaction.user.roles # type: ignore 45 | # since list is invariant, it cannot be a union 46 | # but apparently Role does not implement Snowflake, this may need a fix 47 | 48 | for role_id in ASSIGNABLE_ROLE_IDS: 49 | if ( 50 | interaction.user.get_role(role_id) is None 51 | and str(role_id) in self.values 52 | ): 53 | # user does not have the role but wants it 54 | roles.append(Object(role_id)) 55 | option = get(self.options, value=str(role_id)) 56 | if option is not None: 57 | option.default = True 58 | elif ( 59 | interaction.user.get_role(role_id) is not None 60 | and str(role_id) not in self.values 61 | ): 62 | # user has the role but does not want it 63 | role_ids = [r.id for r in roles] 64 | roles.pop(role_ids.index(role_id)) 65 | option = get(self.options, value=str(role_id)) 66 | if option is not None: 67 | option.default = False 68 | 69 | await interaction.user.edit(roles=roles) 70 | 71 | new_roles = [ 72 | interaction.guild.get_role(int(value)).name # type: ignore 73 | for value in self.values 74 | ] 75 | 76 | await interaction.edit( 77 | content=f"You now have {', '.join(new_roles) or 'no roles'}", view=self.view 78 | ) 79 | 80 | 81 | class Roles(Cog): 82 | def __init__(self, bot): 83 | self.bot = bot 84 | 85 | @slash_command(guild_ids=[GUILD_ID], description="Self assign roles") 86 | async def roles(self, interaction: Interaction): 87 | assert isinstance(interaction.user, Member) 88 | # this shoud never assert as it cannot be used in guilds 89 | # it serves as a way to let the type checker know this is a member 90 | 91 | await interaction.send( 92 | "Select your new roles", 93 | view=RolesView(member=interaction.user), 94 | ephemeral=True, 95 | ) 96 | 97 | 98 | def setup(bot): 99 | bot.add_cog(Roles(bot)) 100 | -------------------------------------------------------------------------------- /cogs/stars.py: -------------------------------------------------------------------------------- 1 | import aiohttp 2 | from os import environ as env 3 | from nextcord.ext import commands 4 | from nextcord.ext.tasks import loop 5 | 6 | 7 | class GitHubStars(commands.Cog): 8 | """Run a task loop to update a channel with the current stars for nextcord/nextcord and nextcord/nextcord-v3""" 9 | 10 | STARS_CHANNEL_ID = int(env["STARS_CHANNEL_ID"]) 11 | 12 | def __init__(self, bot: commands.Bot): 13 | self.__bot = bot 14 | self.update_stars.start() 15 | 16 | async def get_stars(self, repo: str) -> int: 17 | """Get number of GitHub stars for a given repo in the form 'owner/repository'""" 18 | async with aiohttp.ClientSession() as session: 19 | async with session.get(f"https://api.github.com/repos/{repo}") as resp: 20 | data = await resp.json() 21 | return int(data["stargazers_count"]) 22 | 23 | @loop(minutes=30) 24 | async def update_stars(self): 25 | """Loop to check and update stars""" 26 | nextcord_stars = await self.get_stars("nextcord/nextcord") 27 | nextcord_v3_stars = await self.get_stars("nextcord/nextcord-v3") 28 | channel_name = f"v2 {nextcord_stars}🌟| v3 {nextcord_v3_stars}🌟" 29 | 30 | # update channel name if it has changed 31 | if self.__channel.name != channel_name: 32 | await self.__channel.edit(name=channel_name) 33 | 34 | @update_stars.before_loop 35 | async def before_update_stars(self): 36 | """Before the loop starts, get the channel""" 37 | await self.__bot.wait_until_ready() 38 | self.__channel = self.__bot.get_channel(self.STARS_CHANNEL_ID) 39 | 40 | def cog_unload(self): 41 | self.update_stars.cancel() 42 | 43 | 44 | def setup(bot: commands.Bot): 45 | bot.add_cog(GitHubStars(bot)) 46 | -------------------------------------------------------------------------------- /cogs/utils/fuzzy.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | This Source Code Form is subject to the terms of the Mozilla Public 5 | License, v. 2.0. If a copy of the MPL was not distributed with this 6 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | """ 8 | 9 | # help with: http://chairnerd.seatgeek.com/fuzzywuzzy-fuzzy-string-matching-in-python/ 10 | 11 | import heapq 12 | import re 13 | from difflib import SequenceMatcher 14 | 15 | 16 | def ratio(a, b): 17 | m = SequenceMatcher(None, a, b) 18 | return int(round(100 * m.ratio())) 19 | 20 | 21 | def quick_ratio(a, b): 22 | m = SequenceMatcher(None, a, b) 23 | return int(round(100 * m.quick_ratio())) 24 | 25 | 26 | def partial_ratio(a, b): 27 | short, long = (a, b) if len(a) <= len(b) else (b, a) 28 | m = SequenceMatcher(None, short, long) 29 | 30 | blocks = m.get_matching_blocks() 31 | 32 | scores = [] 33 | for i, j, n in blocks: 34 | start = max(j - i, 0) 35 | end = start + len(short) 36 | o = SequenceMatcher(None, short, long[start:end]) 37 | r = o.ratio() 38 | 39 | if 100 * r > 99: 40 | return 100 41 | scores.append(r) 42 | 43 | return int(round(100 * max(scores))) 44 | 45 | 46 | _word_regex = re.compile(r"\W", re.IGNORECASE) 47 | 48 | 49 | def _sort_tokens(a): 50 | a = _word_regex.sub(" ", a).lower().strip() 51 | return " ".join(sorted(a.split())) 52 | 53 | 54 | def token_sort_ratio(a, b): 55 | a = _sort_tokens(a) 56 | b = _sort_tokens(b) 57 | return ratio(a, b) 58 | 59 | 60 | def quick_token_sort_ratio(a, b): 61 | a = _sort_tokens(a) 62 | b = _sort_tokens(b) 63 | return quick_ratio(a, b) 64 | 65 | 66 | def partial_token_sort_ratio(a, b): 67 | a = _sort_tokens(a) 68 | b = _sort_tokens(b) 69 | return partial_ratio(a, b) 70 | 71 | 72 | def _extraction_generator(query, choices, scorer=quick_ratio, score_cutoff=0): 73 | try: 74 | for key, value in choices.items(): 75 | score = scorer(query, key) 76 | if score >= score_cutoff: 77 | yield key, score, value 78 | except AttributeError: 79 | for choice in choices: 80 | score = scorer(query, choice) 81 | if score >= score_cutoff: 82 | yield choice, score 83 | 84 | 85 | def extract(query, choices, *, scorer=quick_ratio, score_cutoff=0, limit=10): 86 | it = _extraction_generator(query, choices, scorer, score_cutoff) 87 | key = lambda t: t[1] 88 | if limit is not None: 89 | return heapq.nlargest(limit, it, key=key) 90 | return sorted(it, key=key, reverse=True) 91 | 92 | 93 | def extract_one(query, choices, *, scorer=quick_ratio, score_cutoff=0): 94 | it = _extraction_generator(query, choices, scorer, score_cutoff) 95 | key = lambda t: t[1] 96 | try: 97 | return max(it, key=key) 98 | except Exception: 99 | # iterator could return nothing 100 | return None 101 | 102 | 103 | def extract_or_exact(query, choices, *, limit=None, scorer=quick_ratio, score_cutoff=0): 104 | matches = extract( 105 | query, choices, scorer=scorer, score_cutoff=score_cutoff, limit=limit 106 | ) 107 | if len(matches) == 0: 108 | return [] 109 | 110 | if len(matches) == 1: 111 | return matches 112 | 113 | top = matches[0][1] 114 | second = matches[1][1] 115 | 116 | # check if the top one is exact or more than 30% more correct than the top 117 | if top == 100 or top > (second + 30): 118 | return [matches[0]] 119 | 120 | return matches 121 | 122 | 123 | def extract_matches(query, choices, *, scorer=quick_ratio, score_cutoff=0): 124 | matches = extract( 125 | query, choices, scorer=scorer, score_cutoff=score_cutoff, limit=None 126 | ) 127 | if len(matches) == 0: 128 | return [] 129 | 130 | top_score = matches[0][1] 131 | to_return = [] 132 | index = 0 133 | while True: 134 | try: 135 | match = matches[index] 136 | except IndexError: 137 | break 138 | else: 139 | index += 1 140 | 141 | if match[1] != top_score: 142 | break 143 | 144 | to_return.append(match) 145 | return to_return 146 | 147 | 148 | def finder(text, collection, *, key=None, lazy=True): 149 | suggestions = [] 150 | text = str(text) 151 | pat = ".*?".join(map(re.escape, text)) 152 | regex = re.compile(pat, flags=re.IGNORECASE) 153 | for item in collection: 154 | to_search = key(item) if key else item 155 | r = regex.search(to_search) 156 | if r: 157 | suggestions.append((len(r.group()), r.start(), item)) 158 | 159 | def sort_key(tup): 160 | if key: 161 | return tup[0], tup[1], key(tup[2]) 162 | return tup 163 | 164 | if lazy: 165 | return (z for _, _, z in sorted(suggestions, key=sort_key)) 166 | else: 167 | return [z for _, _, z in sorted(suggestions, key=sort_key)] 168 | 169 | 170 | def find(text, collection, *, key=None): 171 | try: 172 | return finder(text, collection, key=key, lazy=False)[0] 173 | except IndexError: 174 | return None 175 | -------------------------------------------------------------------------------- /cogs/utils/split_txtfile.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | def split_txtfile(in_filename: str, chunk_len=4000) -> list: 5 | """Takes a long plaintext file from the assets directory and splits it up 6 | into 4000 character-long chunks to be sent over Discord and stay under the 7 | embed content character limit (which is actually 4096 but shhh). 8 | """ 9 | in_file_fullpath = os.path.join(os.getcwd(), "assets", in_filename) 10 | all_chunks = [] # Each item is a <4000-character-long chunk of the infile. 11 | current_chunk = "" # Store temp chunks here before adding to the above. 12 | 13 | with open(in_file_fullpath) as in_file: 14 | for line in in_file: 15 | # Before we concatenate the current chunk and the current line, we 16 | # need to check if the resulting chunk is over 4000 chars long. 17 | # If so, add that chunk to the list of chunks and start a brand new 18 | # chunk to add to. 19 | if len(current_chunk + line) >= chunk_len: 20 | all_chunks.append(current_chunk) 21 | current_chunk = "" 22 | current_chunk += line 23 | 24 | # Once we finish iterating over each line in the input file, the last 25 | # chunk is probably gonna be under 4000 chars and therefore won't be 26 | # added to the chunk list. We have to add it here in case that happens. 27 | all_chunks.append(current_chunk) 28 | return all_chunks 29 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | bot: 4 | build: 5 | context: . 6 | dockerfile: Dockerfile 7 | env_file: 8 | - .env 9 | links: [ consul ] 10 | consul: 11 | image: consul:1.12 12 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from os import environ as env 2 | from re import compile 3 | 4 | import os 5 | import aiohttp 6 | import nextcord 7 | 8 | from nextcord import Intents, Interaction 9 | from nextcord.ext import commands 10 | from nextcord.ext.commands import errors 11 | from nextcord.ext.application_checks import errors as application_errors 12 | 13 | bot = commands.Bot( 14 | "=", intents=Intents(messages=True, guilds=True, members=True, message_content=True) 15 | ) 16 | bot.load_extension("jishaku") 17 | 18 | issue_regex = compile(r"##(\d+)") 19 | discord_regex = compile(r"#!(\d+)") 20 | 21 | 22 | @bot.event 23 | async def on_command_error(ctx, error): 24 | if isinstance(error, errors.CommandNotFound): 25 | return 26 | elif isinstance(error, errors.TooManyArguments): 27 | await ctx.send("You are giving too many arguments!") 28 | return 29 | elif isinstance(error, errors.BadArgument): 30 | await ctx.send( 31 | "The library ran into an error attempting to parse your argument." 32 | ) 33 | return 34 | elif isinstance(error, errors.MissingRequiredArgument): 35 | await ctx.send("You're missing a required argument.") 36 | # kinda annoying and useless error. 37 | elif isinstance(error, nextcord.NotFound) and "Unknown interaction" in str(error): 38 | return 39 | elif isinstance(error, errors.MissingRole): 40 | role = ctx.guild.get_role(int(error.missing_role)) # type: ignore 41 | await ctx.send(f'"{role.name}" is required to use this command.') # type: ignore 42 | return 43 | else: 44 | await ctx.send( 45 | f"This command raised an exception: `{type(error)}:{str(error)}`" 46 | ) 47 | 48 | 49 | @bot.event 50 | async def on_application_command_error( 51 | interaction: Interaction, error: Exception 52 | ) -> None: 53 | if isinstance(error, application_errors.ApplicationMissingRole): 54 | role = interaction.guild.get_role(int(error.missing_role)) # type: ignore 55 | await interaction.send(f"{role.mention} role is required to use this command.", ephemeral=True) # type: ignore 56 | return 57 | else: 58 | await interaction.send( 59 | f"This command raised an exception: `{type(error)}:{str(error)}`", 60 | ephemeral=True, 61 | ) 62 | 63 | 64 | @bot.listen() 65 | async def on_message(message): 66 | if result := issue_regex.search(message.content): 67 | issue_id = result.groups()[0] 68 | await message.channel.send( 69 | f"https://github.com/nextcord/nextcord/issues/{issue_id}" 70 | ) 71 | if result := discord_regex.search(message.content): 72 | issue_id = result.groups()[0] 73 | await message.channel.send( 74 | f"https://github.com/rapptz/discord.py/issues/{issue_id}" 75 | ) 76 | 77 | 78 | @bot.command() 79 | async def todo(ctx): 80 | await ctx.send( 81 | "https://github.com/nextcord/nextcord/projects/1 and going through all the issues" 82 | ) 83 | 84 | 85 | for filename in os.listdir("./cogs"): 86 | if filename.endswith(".py"): 87 | bot.load_extension(f"cogs.{filename[:-3]}") 88 | elif os.path.isfile(filename): 89 | print(f"Unable to load {filename[:-3]}") 90 | 91 | 92 | async def startup(): 93 | bot.session = aiohttp.ClientSession() 94 | 95 | 96 | bot.loop.create_task(startup()) 97 | bot.run(env["TOKEN"]) 98 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "aiohttp" 3 | version = "3.8.1" 4 | description = "Async http client/server framework (asyncio)" 5 | category = "main" 6 | optional = false 7 | python-versions = ">=3.6" 8 | 9 | [package.dependencies] 10 | aiosignal = ">=1.1.2" 11 | async-timeout = ">=4.0.0a3,<5.0" 12 | attrs = ">=17.3.0" 13 | charset-normalizer = ">=2.0,<3.0" 14 | frozenlist = ">=1.1.1" 15 | multidict = ">=4.5,<7.0" 16 | yarl = ">=1.0,<2.0" 17 | 18 | [package.extras] 19 | speedups = ["aiodns", "brotli", "cchardet"] 20 | 21 | [[package]] 22 | name = "aiosignal" 23 | version = "1.2.0" 24 | description = "aiosignal: a list of registered asynchronous callbacks" 25 | category = "main" 26 | optional = false 27 | python-versions = ">=3.6" 28 | 29 | [package.dependencies] 30 | frozenlist = ">=1.1.0" 31 | 32 | [[package]] 33 | name = "algoliasearch" 34 | version = "2.6.2" 35 | description = "Algolia Search API Client for Python." 36 | category = "main" 37 | optional = false 38 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" 39 | 40 | [package.dependencies] 41 | requests = ">=2.21,<3.0" 42 | 43 | [[package]] 44 | name = "astunparse" 45 | version = "1.6.3" 46 | description = "An AST unparser for Python" 47 | category = "main" 48 | optional = false 49 | python-versions = "*" 50 | 51 | [package.dependencies] 52 | six = ">=1.6.1,<2.0" 53 | 54 | [[package]] 55 | name = "async-timeout" 56 | version = "4.0.2" 57 | description = "Timeout context manager for asyncio programs" 58 | category = "main" 59 | optional = false 60 | python-versions = ">=3.6" 61 | 62 | [[package]] 63 | name = "attrs" 64 | version = "21.4.0" 65 | description = "Classes Without Boilerplate" 66 | category = "main" 67 | optional = false 68 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 69 | 70 | [package.extras] 71 | dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] 72 | docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] 73 | tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] 74 | tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"] 75 | 76 | [[package]] 77 | name = "black" 78 | version = "22.3.0" 79 | description = "The uncompromising code formatter." 80 | category = "dev" 81 | optional = false 82 | python-versions = ">=3.6.2" 83 | 84 | [package.dependencies] 85 | click = ">=8.0.0" 86 | mypy-extensions = ">=0.4.3" 87 | pathspec = ">=0.9.0" 88 | platformdirs = ">=2" 89 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 90 | typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} 91 | 92 | [package.extras] 93 | colorama = ["colorama (>=0.4.3)"] 94 | d = ["aiohttp (>=3.7.4)"] 95 | jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] 96 | uvloop = ["uvloop (>=0.15.2)"] 97 | 98 | [[package]] 99 | name = "braceexpand" 100 | version = "0.1.7" 101 | description = "Bash-style brace expansion for Python" 102 | category = "main" 103 | optional = false 104 | python-versions = "*" 105 | 106 | [[package]] 107 | name = "certifi" 108 | version = "2022.6.15" 109 | description = "Python package for providing Mozilla's CA Bundle." 110 | category = "main" 111 | optional = false 112 | python-versions = ">=3.6" 113 | 114 | [[package]] 115 | name = "charset-normalizer" 116 | version = "2.0.12" 117 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 118 | category = "main" 119 | optional = false 120 | python-versions = ">=3.5.0" 121 | 122 | [package.extras] 123 | unicode_backport = ["unicodedata2"] 124 | 125 | [[package]] 126 | name = "click" 127 | version = "8.1.3" 128 | description = "Composable command line interface toolkit" 129 | category = "main" 130 | optional = false 131 | python-versions = ">=3.7" 132 | 133 | [package.dependencies] 134 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 135 | 136 | [[package]] 137 | name = "colorama" 138 | version = "0.4.5" 139 | description = "Cross-platform colored terminal text." 140 | category = "main" 141 | optional = false 142 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 143 | 144 | [[package]] 145 | name = "frozenlist" 146 | version = "1.3.0" 147 | description = "A list-like structure which implements collections.abc.MutableSequence" 148 | category = "main" 149 | optional = false 150 | python-versions = ">=3.7" 151 | 152 | [[package]] 153 | name = "idna" 154 | version = "3.3" 155 | description = "Internationalized Domain Names in Applications (IDNA)" 156 | category = "main" 157 | optional = false 158 | python-versions = ">=3.5" 159 | 160 | [[package]] 161 | name = "import-expression" 162 | version = "1.1.4" 163 | description = "Parses a superset of Python allowing for inline module import expressions" 164 | category = "main" 165 | optional = false 166 | python-versions = "*" 167 | 168 | [package.dependencies] 169 | astunparse = ">=1.6.3,<2.0.0" 170 | 171 | [package.extras] 172 | test = ["pytest", "pytest-cov"] 173 | 174 | [[package]] 175 | name = "importlib-metadata" 176 | version = "4.12.0" 177 | description = "Read metadata from Python packages" 178 | category = "main" 179 | optional = false 180 | python-versions = ">=3.7" 181 | 182 | [package.dependencies] 183 | zipp = ">=0.5" 184 | 185 | [package.extras] 186 | docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)"] 187 | perf = ["ipython"] 188 | testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.3)", "packaging", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)", "importlib-resources (>=1.3)"] 189 | 190 | [[package]] 191 | name = "isort" 192 | version = "5.10.1" 193 | description = "A Python utility / library to sort Python imports." 194 | category = "dev" 195 | optional = false 196 | python-versions = ">=3.6.1,<4.0" 197 | 198 | [package.extras] 199 | pipfile_deprecated_finder = ["pipreqs", "requirementslib"] 200 | requirements_deprecated_finder = ["pipreqs", "pip-api"] 201 | colors = ["colorama (>=0.4.3,<0.5.0)"] 202 | plugins = ["setuptools"] 203 | 204 | [[package]] 205 | name = "jishaku" 206 | version = "2.4.0" 207 | description = "A discord.py extension including useful tools for bot development and debugging." 208 | category = "main" 209 | optional = false 210 | python-versions = ">=3.8.0" 211 | 212 | [package.dependencies] 213 | braceexpand = ">=0.1.7" 214 | click = ">=8.0.1" 215 | import-expression = ">=1.0.0,<2.0.0" 216 | importlib-metadata = {version = ">=3.7.0", markers = "python_version < \"3.10\""} 217 | typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} 218 | 219 | [package.extras] 220 | discordpy = ["discord.py (>=1.7.3)"] 221 | docs = ["Sphinx (>=4.4.0)", "sphinxcontrib-asyncio (>=0.3.0)"] 222 | procinfo = ["psutil (>=5.8.0)"] 223 | publish = ["Jinja2 (>=3.0.3)"] 224 | test = ["coverage (>=6.3.2)", "flake8 (>=4.0.1)", "isort (>=5.10.1)", "pylint (>=2.11.1)", "pytest (>=7.0.1)", "pytest-asyncio (>=0.18.1)", "pytest-cov (>=3.0.0)", "pytest-mock (>=3.7.0)"] 225 | voice = ["yt-dlp (>=2022.3.8)"] 226 | 227 | [[package]] 228 | name = "multidict" 229 | version = "6.0.2" 230 | description = "multidict implementation" 231 | category = "main" 232 | optional = false 233 | python-versions = ">=3.7" 234 | 235 | [[package]] 236 | name = "mypy-extensions" 237 | version = "0.4.3" 238 | description = "Experimental type system extensions for programs checked with the mypy typechecker." 239 | category = "dev" 240 | optional = false 241 | python-versions = "*" 242 | 243 | [[package]] 244 | name = "nextcord" 245 | version = "2.0.0rc2" 246 | description = "A Python wrapper for the Discord API forked from discord.py" 247 | category = "main" 248 | optional = false 249 | python-versions = ">=3.8.0" 250 | 251 | [package.dependencies] 252 | aiohttp = ">=3.8.0,<4.0.0" 253 | 254 | [package.extras] 255 | docs = ["sphinx (==4.0.2)", "sphinxcontrib_trio (==1.1.2)", "sphinxcontrib-websupport"] 256 | speed = ["orjson (>=3.5.4)", "aiodns (>=1.1)", "brotli", "cchardet"] 257 | voice = ["PyNaCl (>=1.3.0,<1.5)"] 258 | 259 | [[package]] 260 | name = "pathspec" 261 | version = "0.9.0" 262 | description = "Utility library for gitignore style pattern matching of file paths." 263 | category = "dev" 264 | optional = false 265 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 266 | 267 | [[package]] 268 | name = "platformdirs" 269 | version = "2.5.2" 270 | description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 271 | category = "dev" 272 | optional = false 273 | python-versions = ">=3.7" 274 | 275 | [package.extras] 276 | docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)", "sphinx (>=4)"] 277 | test = ["appdirs (==1.4.4)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)", "pytest (>=6)"] 278 | 279 | [[package]] 280 | name = "requests" 281 | version = "2.28.0" 282 | description = "Python HTTP for Humans." 283 | category = "main" 284 | optional = false 285 | python-versions = ">=3.7, <4" 286 | 287 | [package.dependencies] 288 | certifi = ">=2017.4.17" 289 | charset-normalizer = ">=2.0.0,<2.1.0" 290 | idna = ">=2.5,<4" 291 | urllib3 = ">=1.21.1,<1.27" 292 | 293 | [package.extras] 294 | socks = ["PySocks (>=1.5.6,!=1.5.7)"] 295 | use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] 296 | 297 | [[package]] 298 | name = "six" 299 | version = "1.16.0" 300 | description = "Python 2 and 3 compatibility utilities" 301 | category = "main" 302 | optional = false 303 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 304 | 305 | [[package]] 306 | name = "tomli" 307 | version = "2.0.1" 308 | description = "A lil' TOML parser" 309 | category = "dev" 310 | optional = false 311 | python-versions = ">=3.7" 312 | 313 | [[package]] 314 | name = "typing-extensions" 315 | version = "4.2.0" 316 | description = "Backported and Experimental Type Hints for Python 3.7+" 317 | category = "main" 318 | optional = false 319 | python-versions = ">=3.7" 320 | 321 | [[package]] 322 | name = "urllib3" 323 | version = "1.26.9" 324 | description = "HTTP library with thread-safe connection pooling, file post, and more." 325 | category = "main" 326 | optional = false 327 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" 328 | 329 | [package.extras] 330 | brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"] 331 | secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] 332 | socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] 333 | 334 | [[package]] 335 | name = "yarl" 336 | version = "1.7.2" 337 | description = "Yet another URL library" 338 | category = "main" 339 | optional = false 340 | python-versions = ">=3.6" 341 | 342 | [package.dependencies] 343 | idna = ">=2.0" 344 | multidict = ">=4.0" 345 | 346 | [[package]] 347 | name = "zipp" 348 | version = "3.8.0" 349 | description = "Backport of pathlib-compatible object wrapper for zip files" 350 | category = "main" 351 | optional = false 352 | python-versions = ">=3.7" 353 | 354 | [package.extras] 355 | docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)"] 356 | testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"] 357 | 358 | [metadata] 359 | lock-version = "1.1" 360 | python-versions = "^3.8" 361 | content-hash = "5cf1f44acad2e314f5772e75d00c4659e2cabed2cf11dc68fbc960627b838330" 362 | 363 | [metadata.files] 364 | aiohttp = [ 365 | {file = "aiohttp-3.8.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1ed0b6477896559f17b9eaeb6d38e07f7f9ffe40b9f0f9627ae8b9926ae260a8"}, 366 | {file = "aiohttp-3.8.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7dadf3c307b31e0e61689cbf9e06be7a867c563d5a63ce9dca578f956609abf8"}, 367 | {file = "aiohttp-3.8.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a79004bb58748f31ae1cbe9fa891054baaa46fb106c2dc7af9f8e3304dc30316"}, 368 | {file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12de6add4038df8f72fac606dff775791a60f113a725c960f2bab01d8b8e6b15"}, 369 | {file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6f0d5f33feb5f69ddd57a4a4bd3d56c719a141080b445cbf18f238973c5c9923"}, 370 | {file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eaba923151d9deea315be1f3e2b31cc39a6d1d2f682f942905951f4e40200922"}, 371 | {file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:099ebd2c37ac74cce10a3527d2b49af80243e2a4fa39e7bce41617fbc35fa3c1"}, 372 | {file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2e5d962cf7e1d426aa0e528a7e198658cdc8aa4fe87f781d039ad75dcd52c516"}, 373 | {file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:fa0ffcace9b3aa34d205d8130f7873fcfefcb6a4dd3dd705b0dab69af6712642"}, 374 | {file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:61bfc23df345d8c9716d03717c2ed5e27374e0fe6f659ea64edcd27b4b044cf7"}, 375 | {file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:31560d268ff62143e92423ef183680b9829b1b482c011713ae941997921eebc8"}, 376 | {file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:01d7bdb774a9acc838e6b8f1d114f45303841b89b95984cbb7d80ea41172a9e3"}, 377 | {file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:97ef77eb6b044134c0b3a96e16abcb05ecce892965a2124c566af0fd60f717e2"}, 378 | {file = "aiohttp-3.8.1-cp310-cp310-win32.whl", hash = "sha256:c2aef4703f1f2ddc6df17519885dbfa3514929149d3ff900b73f45998f2532fa"}, 379 | {file = "aiohttp-3.8.1-cp310-cp310-win_amd64.whl", hash = "sha256:713ac174a629d39b7c6a3aa757b337599798da4c1157114a314e4e391cd28e32"}, 380 | {file = "aiohttp-3.8.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:473d93d4450880fe278696549f2e7aed8cd23708c3c1997981464475f32137db"}, 381 | {file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99b5eeae8e019e7aad8af8bb314fb908dd2e028b3cdaad87ec05095394cce632"}, 382 | {file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3af642b43ce56c24d063325dd2cf20ee012d2b9ba4c3c008755a301aaea720ad"}, 383 | {file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3630c3ef435c0a7c549ba170a0633a56e92629aeed0e707fec832dee313fb7a"}, 384 | {file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4a4a4e30bf1edcad13fb0804300557aedd07a92cabc74382fdd0ba6ca2661091"}, 385 | {file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6f8b01295e26c68b3a1b90efb7a89029110d3a4139270b24fda961893216c440"}, 386 | {file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:a25fa703a527158aaf10dafd956f7d42ac6d30ec80e9a70846253dd13e2f067b"}, 387 | {file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:5bfde62d1d2641a1f5173b8c8c2d96ceb4854f54a44c23102e2ccc7e02f003ec"}, 388 | {file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:51467000f3647d519272392f484126aa716f747859794ac9924a7aafa86cd411"}, 389 | {file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:03a6d5349c9ee8f79ab3ff3694d6ce1cfc3ced1c9d36200cb8f08ba06bd3b782"}, 390 | {file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:102e487eeb82afac440581e5d7f8f44560b36cf0bdd11abc51a46c1cd88914d4"}, 391 | {file = "aiohttp-3.8.1-cp36-cp36m-win32.whl", hash = "sha256:4aed991a28ea3ce320dc8ce655875e1e00a11bdd29fe9444dd4f88c30d558602"}, 392 | {file = "aiohttp-3.8.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b0e20cddbd676ab8a64c774fefa0ad787cc506afd844de95da56060348021e96"}, 393 | {file = "aiohttp-3.8.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:37951ad2f4a6df6506750a23f7cbabad24c73c65f23f72e95897bb2cecbae676"}, 394 | {file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c23b1ad869653bc818e972b7a3a79852d0e494e9ab7e1a701a3decc49c20d51"}, 395 | {file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:15b09b06dae900777833fe7fc4b4aa426556ce95847a3e8d7548e2d19e34edb8"}, 396 | {file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:477c3ea0ba410b2b56b7efb072c36fa91b1e6fc331761798fa3f28bb224830dd"}, 397 | {file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2f2f69dca064926e79997f45b2f34e202b320fd3782f17a91941f7eb85502ee2"}, 398 | {file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ef9612483cb35171d51d9173647eed5d0069eaa2ee812793a75373447d487aa4"}, 399 | {file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6d69f36d445c45cda7b3b26afef2fc34ef5ac0cdc75584a87ef307ee3c8c6d00"}, 400 | {file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:55c3d1072704d27401c92339144d199d9de7b52627f724a949fc7d5fc56d8b93"}, 401 | {file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:b9d00268fcb9f66fbcc7cd9fe423741d90c75ee029a1d15c09b22d23253c0a44"}, 402 | {file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:07b05cd3305e8a73112103c834e91cd27ce5b4bd07850c4b4dbd1877d3f45be7"}, 403 | {file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c34dc4958b232ef6188c4318cb7b2c2d80521c9a56c52449f8f93ab7bc2a8a1c"}, 404 | {file = "aiohttp-3.8.1-cp37-cp37m-win32.whl", hash = "sha256:d2f9b69293c33aaa53d923032fe227feac867f81682f002ce33ffae978f0a9a9"}, 405 | {file = "aiohttp-3.8.1-cp37-cp37m-win_amd64.whl", hash = "sha256:6ae828d3a003f03ae31915c31fa684b9890ea44c9c989056fea96e3d12a9fa17"}, 406 | {file = "aiohttp-3.8.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0c7ebbbde809ff4e970824b2b6cb7e4222be6b95a296e46c03cf050878fc1785"}, 407 | {file = "aiohttp-3.8.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8b7ef7cbd4fec9a1e811a5de813311ed4f7ac7d93e0fda233c9b3e1428f7dd7b"}, 408 | {file = "aiohttp-3.8.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c3d6a4d0619e09dcd61021debf7059955c2004fa29f48788a3dfaf9c9901a7cd"}, 409 | {file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:718626a174e7e467f0558954f94af117b7d4695d48eb980146016afa4b580b2e"}, 410 | {file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:589c72667a5febd36f1315aa6e5f56dd4aa4862df295cb51c769d16142ddd7cd"}, 411 | {file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2ed076098b171573161eb146afcb9129b5ff63308960aeca4b676d9d3c35e700"}, 412 | {file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:086f92daf51a032d062ec5f58af5ca6a44d082c35299c96376a41cbb33034675"}, 413 | {file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:11691cf4dc5b94236ccc609b70fec991234e7ef8d4c02dd0c9668d1e486f5abf"}, 414 | {file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:31d1e1c0dbf19ebccbfd62eff461518dcb1e307b195e93bba60c965a4dcf1ba0"}, 415 | {file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:11a67c0d562e07067c4e86bffc1553f2cf5b664d6111c894671b2b8712f3aba5"}, 416 | {file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:bb01ba6b0d3f6c68b89fce7305080145d4877ad3acaed424bae4d4ee75faa950"}, 417 | {file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:44db35a9e15d6fe5c40d74952e803b1d96e964f683b5a78c3cc64eb177878155"}, 418 | {file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:844a9b460871ee0a0b0b68a64890dae9c415e513db0f4a7e3cab41a0f2fedf33"}, 419 | {file = "aiohttp-3.8.1-cp38-cp38-win32.whl", hash = "sha256:7d08744e9bae2ca9c382581f7dce1273fe3c9bae94ff572c3626e8da5b193c6a"}, 420 | {file = "aiohttp-3.8.1-cp38-cp38-win_amd64.whl", hash = "sha256:04d48b8ce6ab3cf2097b1855e1505181bdd05586ca275f2505514a6e274e8e75"}, 421 | {file = "aiohttp-3.8.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f5315a2eb0239185af1bddb1abf472d877fede3cc8d143c6cddad37678293237"}, 422 | {file = "aiohttp-3.8.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a996d01ca39b8dfe77440f3cd600825d05841088fd6bc0144cc6c2ec14cc5f74"}, 423 | {file = "aiohttp-3.8.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:13487abd2f761d4be7c8ff9080de2671e53fff69711d46de703c310c4c9317ca"}, 424 | {file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea302f34477fda3f85560a06d9ebdc7fa41e82420e892fc50b577e35fc6a50b2"}, 425 | {file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2f635ce61a89c5732537a7896b6319a8fcfa23ba09bec36e1b1ac0ab31270d2"}, 426 | {file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e999f2d0e12eea01caeecb17b653f3713d758f6dcc770417cf29ef08d3931421"}, 427 | {file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0770e2806a30e744b4e21c9d73b7bee18a1cfa3c47991ee2e5a65b887c49d5cf"}, 428 | {file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d15367ce87c8e9e09b0f989bfd72dc641bcd04ba091c68cd305312d00962addd"}, 429 | {file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6c7cefb4b0640703eb1069835c02486669312bf2f12b48a748e0a7756d0de33d"}, 430 | {file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:71927042ed6365a09a98a6377501af5c9f0a4d38083652bcd2281a06a5976724"}, 431 | {file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:28d490af82bc6b7ce53ff31337a18a10498303fe66f701ab65ef27e143c3b0ef"}, 432 | {file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:b6613280ccedf24354406caf785db748bebbddcf31408b20c0b48cb86af76866"}, 433 | {file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:81e3d8c34c623ca4e36c46524a3530e99c0bc95ed068fd6e9b55cb721d408fb2"}, 434 | {file = "aiohttp-3.8.1-cp39-cp39-win32.whl", hash = "sha256:7187a76598bdb895af0adbd2fb7474d7f6025d170bc0a1130242da817ce9e7d1"}, 435 | {file = "aiohttp-3.8.1-cp39-cp39-win_amd64.whl", hash = "sha256:1c182cb873bc91b411e184dab7a2b664d4fea2743df0e4d57402f7f3fa644bac"}, 436 | {file = "aiohttp-3.8.1.tar.gz", hash = "sha256:fc5471e1a54de15ef71c1bc6ebe80d4dc681ea600e68bfd1cbce40427f0b7578"}, 437 | ] 438 | aiosignal = [ 439 | {file = "aiosignal-1.2.0-py3-none-any.whl", hash = "sha256:26e62109036cd181df6e6ad646f91f0dcfd05fe16d0cb924138ff2ab75d64e3a"}, 440 | {file = "aiosignal-1.2.0.tar.gz", hash = "sha256:78ed67db6c7b7ced4f98e495e572106d5c432a93e1ddd1bf475e1dc05f5b7df2"}, 441 | ] 442 | algoliasearch = [ 443 | {file = "algoliasearch-2.6.2-py2.py3-none-any.whl", hash = "sha256:8efe9cab6e4806f9a233fd5db4f56ed00ebae55e9825ec3fd698df6413c0c80b"}, 444 | {file = "algoliasearch-2.6.2.tar.gz", hash = "sha256:46a27fa77636221d68787e9fbb3613f29d1283f328e855efba1a90ba49265949"}, 445 | ] 446 | astunparse = [ 447 | {file = "astunparse-1.6.3-py2.py3-none-any.whl", hash = "sha256:c2652417f2c8b5bb325c885ae329bdf3f86424075c4fd1a128674bc6fba4b8e8"}, 448 | {file = "astunparse-1.6.3.tar.gz", hash = "sha256:5ad93a8456f0d084c3456d059fd9a92cce667963232cbf763eac3bc5b7940872"}, 449 | ] 450 | async-timeout = [ 451 | {file = "async-timeout-4.0.2.tar.gz", hash = "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15"}, 452 | {file = "async_timeout-4.0.2-py3-none-any.whl", hash = "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"}, 453 | ] 454 | attrs = [ 455 | {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, 456 | {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, 457 | ] 458 | black = [ 459 | {file = "black-22.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2497f9c2386572e28921fa8bec7be3e51de6801f7459dffd6e62492531c47e09"}, 460 | {file = "black-22.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5795a0375eb87bfe902e80e0c8cfaedf8af4d49694d69161e5bd3206c18618bb"}, 461 | {file = "black-22.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e3556168e2e5c49629f7b0f377070240bd5511e45e25a4497bb0073d9dda776a"}, 462 | {file = "black-22.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67c8301ec94e3bcc8906740fe071391bce40a862b7be0b86fb5382beefecd968"}, 463 | {file = "black-22.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:fd57160949179ec517d32ac2ac898b5f20d68ed1a9c977346efbac9c2f1e779d"}, 464 | {file = "black-22.3.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:cc1e1de68c8e5444e8f94c3670bb48a2beef0e91dddfd4fcc29595ebd90bb9ce"}, 465 | {file = "black-22.3.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d2fc92002d44746d3e7db7cf9313cf4452f43e9ea77a2c939defce3b10b5c82"}, 466 | {file = "black-22.3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:a6342964b43a99dbc72f72812bf88cad8f0217ae9acb47c0d4f141a6416d2d7b"}, 467 | {file = "black-22.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:328efc0cc70ccb23429d6be184a15ce613f676bdfc85e5fe8ea2a9354b4e9015"}, 468 | {file = "black-22.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06f9d8846f2340dfac80ceb20200ea5d1b3f181dd0556b47af4e8e0b24fa0a6b"}, 469 | {file = "black-22.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:ad4efa5fad66b903b4a5f96d91461d90b9507a812b3c5de657d544215bb7877a"}, 470 | {file = "black-22.3.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8477ec6bbfe0312c128e74644ac8a02ca06bcdb8982d4ee06f209be28cdf163"}, 471 | {file = "black-22.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:637a4014c63fbf42a692d22b55d8ad6968a946b4a6ebc385c5505d9625b6a464"}, 472 | {file = "black-22.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:863714200ada56cbc366dc9ae5291ceb936573155f8bf8e9de92aef51f3ad0f0"}, 473 | {file = "black-22.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10dbe6e6d2988049b4655b2b739f98785a884d4d6b85bc35133a8fb9a2233176"}, 474 | {file = "black-22.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:cee3e11161dde1b2a33a904b850b0899e0424cc331b7295f2a9698e79f9a69a0"}, 475 | {file = "black-22.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5891ef8abc06576985de8fa88e95ab70641de6c1fca97e2a15820a9b69e51b20"}, 476 | {file = "black-22.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:30d78ba6bf080eeaf0b7b875d924b15cd46fec5fd044ddfbad38c8ea9171043a"}, 477 | {file = "black-22.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ee8f1f7228cce7dffc2b464f07ce769f478968bfb3dd1254a4c2eeed84928aad"}, 478 | {file = "black-22.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ee227b696ca60dd1c507be80a6bc849a5a6ab57ac7352aad1ffec9e8b805f21"}, 479 | {file = "black-22.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:9b542ced1ec0ceeff5b37d69838106a6348e60db7b8fdd245294dc1d26136265"}, 480 | {file = "black-22.3.0-py3-none-any.whl", hash = "sha256:bc58025940a896d7e5356952228b68f793cf5fcb342be703c3a2669a1488cb72"}, 481 | {file = "black-22.3.0.tar.gz", hash = "sha256:35020b8886c022ced9282b51b5a875b6d1ab0c387b31a065b84db7c33085ca79"}, 482 | ] 483 | braceexpand = [ 484 | {file = "braceexpand-0.1.7-py2.py3-none-any.whl", hash = "sha256:91332d53de7828103dcae5773fb43bc34950b0c8160e35e0f44c4427a3b85014"}, 485 | {file = "braceexpand-0.1.7.tar.gz", hash = "sha256:e6e539bd20eaea53547472ff94f4fb5c3d3bf9d0a89388c4b56663aba765f705"}, 486 | ] 487 | certifi = [ 488 | {file = "certifi-2022.6.15-py3-none-any.whl", hash = "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412"}, 489 | {file = "certifi-2022.6.15.tar.gz", hash = "sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d"}, 490 | ] 491 | charset-normalizer = [ 492 | {file = "charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"}, 493 | {file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"}, 494 | ] 495 | click = [ 496 | {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, 497 | {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, 498 | ] 499 | colorama = [ 500 | {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"}, 501 | {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"}, 502 | ] 503 | frozenlist = [ 504 | {file = "frozenlist-1.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d2257aaba9660f78c7b1d8fea963b68f3feffb1a9d5d05a18401ca9eb3e8d0a3"}, 505 | {file = "frozenlist-1.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4a44ebbf601d7bac77976d429e9bdb5a4614f9f4027777f9e54fd765196e9d3b"}, 506 | {file = "frozenlist-1.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:45334234ec30fc4ea677f43171b18a27505bfb2dba9aca4398a62692c0ea8868"}, 507 | {file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47be22dc27ed933d55ee55845d34a3e4e9f6fee93039e7f8ebadb0c2f60d403f"}, 508 | {file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:03a7dd1bfce30216a3f51a84e6dd0e4a573d23ca50f0346634916ff105ba6e6b"}, 509 | {file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:691ddf6dc50480ce49f68441f1d16a4c3325887453837036e0fb94736eae1e58"}, 510 | {file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bde99812f237f79eaf3f04ebffd74f6718bbd216101b35ac7955c2d47c17da02"}, 511 | {file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a202458d1298ced3768f5a7d44301e7c86defac162ace0ab7434c2e961166e8"}, 512 | {file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b9e3e9e365991f8cc5f5edc1fd65b58b41d0514a6a7ad95ef5c7f34eb49b3d3e"}, 513 | {file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:04cb491c4b1c051734d41ea2552fde292f5f3a9c911363f74f39c23659c4af78"}, 514 | {file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:436496321dad302b8b27ca955364a439ed1f0999311c393dccb243e451ff66aa"}, 515 | {file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:754728d65f1acc61e0f4df784456106e35afb7bf39cfe37227ab00436fb38676"}, 516 | {file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6eb275c6385dd72594758cbe96c07cdb9bd6becf84235f4a594bdf21e3596c9d"}, 517 | {file = "frozenlist-1.3.0-cp310-cp310-win32.whl", hash = "sha256:e30b2f9683812eb30cf3f0a8e9f79f8d590a7999f731cf39f9105a7c4a39489d"}, 518 | {file = "frozenlist-1.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:f7353ba3367473d1d616ee727945f439e027f0bb16ac1a750219a8344d1d5d3c"}, 519 | {file = "frozenlist-1.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:88aafd445a233dbbf8a65a62bc3249a0acd0d81ab18f6feb461cc5a938610d24"}, 520 | {file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4406cfabef8f07b3b3af0f50f70938ec06d9f0fc26cbdeaab431cbc3ca3caeaa"}, 521 | {file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8cf829bd2e2956066dd4de43fd8ec881d87842a06708c035b37ef632930505a2"}, 522 | {file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:603b9091bd70fae7be28bdb8aa5c9990f4241aa33abb673390a7f7329296695f"}, 523 | {file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:25af28b560e0c76fa41f550eacb389905633e7ac02d6eb3c09017fa1c8cdfde1"}, 524 | {file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94c7a8a9fc9383b52c410a2ec952521906d355d18fccc927fca52ab575ee8b93"}, 525 | {file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:65bc6e2fece04e2145ab6e3c47428d1bbc05aede61ae365b2c1bddd94906e478"}, 526 | {file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:3f7c935c7b58b0d78c0beea0c7358e165f95f1fd8a7e98baa40d22a05b4a8141"}, 527 | {file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd89acd1b8bb4f31b47072615d72e7f53a948d302b7c1d1455e42622de180eae"}, 528 | {file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:6983a31698490825171be44ffbafeaa930ddf590d3f051e397143a5045513b01"}, 529 | {file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:adac9700675cf99e3615eb6a0eb5e9f5a4143c7d42c05cea2e7f71c27a3d0846"}, 530 | {file = "frozenlist-1.3.0-cp37-cp37m-win32.whl", hash = "sha256:0c36e78b9509e97042ef869c0e1e6ef6429e55817c12d78245eb915e1cca7468"}, 531 | {file = "frozenlist-1.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:57f4d3f03a18facacb2a6bcd21bccd011e3b75d463dc49f838fd699d074fabd1"}, 532 | {file = "frozenlist-1.3.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:8c905a5186d77111f02144fab5b849ab524f1e876a1e75205cd1386a9be4b00a"}, 533 | {file = "frozenlist-1.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b5009062d78a8c6890d50b4e53b0ddda31841b3935c1937e2ed8c1bda1c7fb9d"}, 534 | {file = "frozenlist-1.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2fdc3cd845e5a1f71a0c3518528bfdbfe2efaf9886d6f49eacc5ee4fd9a10953"}, 535 | {file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92e650bd09b5dda929523b9f8e7f99b24deac61240ecc1a32aeba487afcd970f"}, 536 | {file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:40dff8962b8eba91fd3848d857203f0bd704b5f1fa2b3fc9af64901a190bba08"}, 537 | {file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:768efd082074bb203c934e83a61654ed4931ef02412c2fbdecea0cff7ecd0274"}, 538 | {file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:006d3595e7d4108a12025ddf415ae0f6c9e736e726a5db0183326fd191b14c5e"}, 539 | {file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:871d42623ae15eb0b0e9df65baeee6976b2e161d0ba93155411d58ff27483ad8"}, 540 | {file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:aff388be97ef2677ae185e72dc500d19ecaf31b698986800d3fc4f399a5e30a5"}, 541 | {file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:9f892d6a94ec5c7b785e548e42722e6f3a52f5f32a8461e82ac3e67a3bd073f1"}, 542 | {file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:e982878792c971cbd60ee510c4ee5bf089a8246226dea1f2138aa0bb67aff148"}, 543 | {file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:c6c321dd013e8fc20735b92cb4892c115f5cdb82c817b1e5b07f6b95d952b2f0"}, 544 | {file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:30530930410855c451bea83f7b272fb1c495ed9d5cc72895ac29e91279401db3"}, 545 | {file = "frozenlist-1.3.0-cp38-cp38-win32.whl", hash = "sha256:40ec383bc194accba825fbb7d0ef3dda5736ceab2375462f1d8672d9f6b68d07"}, 546 | {file = "frozenlist-1.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:f20baa05eaa2bcd5404c445ec51aed1c268d62600362dc6cfe04fae34a424bd9"}, 547 | {file = "frozenlist-1.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:0437fe763fb5d4adad1756050cbf855bbb2bf0d9385c7bb13d7a10b0dd550486"}, 548 | {file = "frozenlist-1.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b684c68077b84522b5c7eafc1dc735bfa5b341fb011d5552ebe0968e22ed641c"}, 549 | {file = "frozenlist-1.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:93641a51f89473837333b2f8100f3f89795295b858cd4c7d4a1f18e299dc0a4f"}, 550 | {file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6d32ff213aef0fd0bcf803bffe15cfa2d4fde237d1d4838e62aec242a8362fa"}, 551 | {file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31977f84828b5bb856ca1eb07bf7e3a34f33a5cddce981d880240ba06639b94d"}, 552 | {file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3c62964192a1c0c30b49f403495911298810bada64e4f03249ca35a33ca0417a"}, 553 | {file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4eda49bea3602812518765810af732229b4291d2695ed24a0a20e098c45a707b"}, 554 | {file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acb267b09a509c1df5a4ca04140da96016f40d2ed183cdc356d237286c971b51"}, 555 | {file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e1e26ac0a253a2907d654a37e390904426d5ae5483150ce3adedb35c8c06614a"}, 556 | {file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f96293d6f982c58ebebb428c50163d010c2f05de0cde99fd681bfdc18d4b2dc2"}, 557 | {file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:e84cb61b0ac40a0c3e0e8b79c575161c5300d1d89e13c0e02f76193982f066ed"}, 558 | {file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:ff9310f05b9d9c5c4dd472983dc956901ee6cb2c3ec1ab116ecdde25f3ce4951"}, 559 | {file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d26b650b71fdc88065b7a21f8ace70175bcf3b5bdba5ea22df4bfd893e795a3b"}, 560 | {file = "frozenlist-1.3.0-cp39-cp39-win32.whl", hash = "sha256:01a73627448b1f2145bddb6e6c2259988bb8aee0fb361776ff8604b99616cd08"}, 561 | {file = "frozenlist-1.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:772965f773757a6026dea111a15e6e2678fbd6216180f82a48a40b27de1ee2ab"}, 562 | {file = "frozenlist-1.3.0.tar.gz", hash = "sha256:ce6f2ba0edb7b0c1d8976565298ad2deba6f8064d2bebb6ffce2ca896eb35b0b"}, 563 | ] 564 | idna = [ 565 | {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, 566 | {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, 567 | ] 568 | import-expression = [ 569 | {file = "import_expression-1.1.4-py3-none-any.whl", hash = "sha256:292099910a4dcc65ba562377cd2265487ba573dd63d73bdee5deec36ca49555b"}, 570 | {file = "import_expression-1.1.4.tar.gz", hash = "sha256:06086a6ab3bfa528b1c478e633d6adf2b3a990e31440f6401b0f3ea12b0659a9"}, 571 | ] 572 | importlib-metadata = [ 573 | {file = "importlib_metadata-4.12.0-py3-none-any.whl", hash = "sha256:7401a975809ea1fdc658c3aa4f78cc2195a0e019c5cbc4c06122884e9ae80c23"}, 574 | {file = "importlib_metadata-4.12.0.tar.gz", hash = "sha256:637245b8bab2b6502fcbc752cc4b7a6f6243bb02b31c5c26156ad103d3d45670"}, 575 | ] 576 | isort = [ 577 | {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"}, 578 | {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, 579 | ] 580 | jishaku = [ 581 | {file = "jishaku-2.4.0-py3-none-any.whl", hash = "sha256:d61e26d1b5380f76a0c92eb61869e6daff906f029be5c1a4fc76205ee1b2d767"}, 582 | {file = "jishaku-2.4.0-py3.10.egg", hash = "sha256:e60e593d81e84774f9fcb470e3ca8e7d2f0be543c2d774164340709ba09cc9a2"}, 583 | {file = "jishaku-2.4.0-py3.8.egg", hash = "sha256:ad647937093d694d2c61a54b1c34eb6d96a7533e694883e4cfd2398ad6c09f12"}, 584 | {file = "jishaku-2.4.0-py3.9.egg", hash = "sha256:d118c9a5cc557d2828ab2425d3ea6c8afcab99b099ab971e46afc5f0fdc02a16"}, 585 | {file = "jishaku-2.4.0.tar.gz", hash = "sha256:784d8d3896e62250fef2e504c53340e5a76ef5bc53a390b2aad1cbbb2e05b172"}, 586 | ] 587 | multidict = [ 588 | {file = "multidict-6.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b9e95a740109c6047602f4db4da9949e6c5945cefbad34a1299775ddc9a62e2"}, 589 | {file = "multidict-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac0e27844758d7177989ce406acc6a83c16ed4524ebc363c1f748cba184d89d3"}, 590 | {file = "multidict-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:041b81a5f6b38244b34dc18c7b6aba91f9cdaf854d9a39e5ff0b58e2b5773b9c"}, 591 | {file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5fdda29a3c7e76a064f2477c9aab1ba96fd94e02e386f1e665bca1807fc5386f"}, 592 | {file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3368bf2398b0e0fcbf46d85795adc4c259299fec50c1416d0f77c0a843a3eed9"}, 593 | {file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4f052ee022928d34fe1f4d2bc743f32609fb79ed9c49a1710a5ad6b2198db20"}, 594 | {file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:225383a6603c086e6cef0f2f05564acb4f4d5f019a4e3e983f572b8530f70c88"}, 595 | {file = "multidict-6.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50bd442726e288e884f7be9071016c15a8742eb689a593a0cac49ea093eef0a7"}, 596 | {file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:47e6a7e923e9cada7c139531feac59448f1f47727a79076c0b1ee80274cd8eee"}, 597 | {file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:0556a1d4ea2d949efe5fd76a09b4a82e3a4a30700553a6725535098d8d9fb672"}, 598 | {file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:626fe10ac87851f4cffecee161fc6f8f9853f0f6f1035b59337a51d29ff3b4f9"}, 599 | {file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:8064b7c6f0af936a741ea1efd18690bacfbae4078c0c385d7c3f611d11f0cf87"}, 600 | {file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2d36e929d7f6a16d4eb11b250719c39560dd70545356365b494249e2186bc389"}, 601 | {file = "multidict-6.0.2-cp310-cp310-win32.whl", hash = "sha256:fcb91630817aa8b9bc4a74023e4198480587269c272c58b3279875ed7235c293"}, 602 | {file = "multidict-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:8cbf0132f3de7cc6c6ce00147cc78e6439ea736cee6bca4f068bcf892b0fd658"}, 603 | {file = "multidict-6.0.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:05f6949d6169878a03e607a21e3b862eaf8e356590e8bdae4227eedadacf6e51"}, 604 | {file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2c2e459f7050aeb7c1b1276763364884595d47000c1cddb51764c0d8976e608"}, 605 | {file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d0509e469d48940147e1235d994cd849a8f8195e0bca65f8f5439c56e17872a3"}, 606 | {file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:514fe2b8d750d6cdb4712346a2c5084a80220821a3e91f3f71eec11cf8d28fd4"}, 607 | {file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19adcfc2a7197cdc3987044e3f415168fc5dc1f720c932eb1ef4f71a2067e08b"}, 608 | {file = "multidict-6.0.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b9d153e7f1f9ba0b23ad1568b3b9e17301e23b042c23870f9ee0522dc5cc79e8"}, 609 | {file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:aef9cc3d9c7d63d924adac329c33835e0243b5052a6dfcbf7732a921c6e918ba"}, 610 | {file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:4571f1beddff25f3e925eea34268422622963cd8dc395bb8778eb28418248e43"}, 611 | {file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:d48b8ee1d4068561ce8033d2c344cf5232cb29ee1a0206a7b828c79cbc5982b8"}, 612 | {file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:45183c96ddf61bf96d2684d9fbaf6f3564d86b34cb125761f9a0ef9e36c1d55b"}, 613 | {file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:75bdf08716edde767b09e76829db8c1e5ca9d8bb0a8d4bd94ae1eafe3dac5e15"}, 614 | {file = "multidict-6.0.2-cp37-cp37m-win32.whl", hash = "sha256:a45e1135cb07086833ce969555df39149680e5471c04dfd6a915abd2fc3f6dbc"}, 615 | {file = "multidict-6.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6f3cdef8a247d1eafa649085812f8a310e728bdf3900ff6c434eafb2d443b23a"}, 616 | {file = "multidict-6.0.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0327292e745a880459ef71be14e709aaea2f783f3537588fb4ed09b6c01bca60"}, 617 | {file = "multidict-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e875b6086e325bab7e680e4316d667fc0e5e174bb5611eb16b3ea121c8951b86"}, 618 | {file = "multidict-6.0.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:feea820722e69451743a3d56ad74948b68bf456984d63c1a92e8347b7b88452d"}, 619 | {file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cc57c68cb9139c7cd6fc39f211b02198e69fb90ce4bc4a094cf5fe0d20fd8b0"}, 620 | {file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:497988d6b6ec6ed6f87030ec03280b696ca47dbf0648045e4e1d28b80346560d"}, 621 | {file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:89171b2c769e03a953d5969b2f272efa931426355b6c0cb508022976a17fd376"}, 622 | {file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:684133b1e1fe91eda8fa7447f137c9490a064c6b7f392aa857bba83a28cfb693"}, 623 | {file = "multidict-6.0.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd9fc9c4849a07f3635ccffa895d57abce554b467d611a5009ba4f39b78a8849"}, 624 | {file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e07c8e79d6e6fd37b42f3250dba122053fddb319e84b55dd3a8d6446e1a7ee49"}, 625 | {file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4070613ea2227da2bfb2c35a6041e4371b0af6b0be57f424fe2318b42a748516"}, 626 | {file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:47fbeedbf94bed6547d3aa632075d804867a352d86688c04e606971595460227"}, 627 | {file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:5774d9218d77befa7b70d836004a768fb9aa4fdb53c97498f4d8d3f67bb9cfa9"}, 628 | {file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2957489cba47c2539a8eb7ab32ff49101439ccf78eab724c828c1a54ff3ff98d"}, 629 | {file = "multidict-6.0.2-cp38-cp38-win32.whl", hash = "sha256:e5b20e9599ba74391ca0cfbd7b328fcc20976823ba19bc573983a25b32e92b57"}, 630 | {file = "multidict-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:8004dca28e15b86d1b1372515f32eb6f814bdf6f00952699bdeb541691091f96"}, 631 | {file = "multidict-6.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2e4a0785b84fb59e43c18a015ffc575ba93f7d1dbd272b4cdad9f5134b8a006c"}, 632 | {file = "multidict-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6701bf8a5d03a43375909ac91b6980aea74b0f5402fbe9428fc3f6edf5d9677e"}, 633 | {file = "multidict-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a007b1638e148c3cfb6bf0bdc4f82776cef0ac487191d093cdc316905e504071"}, 634 | {file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:07a017cfa00c9890011628eab2503bee5872f27144936a52eaab449be5eaf032"}, 635 | {file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c207fff63adcdf5a485969131dc70e4b194327666b7e8a87a97fbc4fd80a53b2"}, 636 | {file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:373ba9d1d061c76462d74e7de1c0c8e267e9791ee8cfefcf6b0b2495762c370c"}, 637 | {file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfba7c6d5d7c9099ba21f84662b037a0ffd4a5e6b26ac07d19e423e6fdf965a9"}, 638 | {file = "multidict-6.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19d9bad105dfb34eb539c97b132057a4e709919ec4dd883ece5838bcbf262b80"}, 639 | {file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:de989b195c3d636ba000ee4281cd03bb1234635b124bf4cd89eeee9ca8fcb09d"}, 640 | {file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7c40b7bbece294ae3a87c1bc2abff0ff9beef41d14188cda94ada7bcea99b0fb"}, 641 | {file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:d16cce709ebfadc91278a1c005e3c17dd5f71f5098bfae1035149785ea6e9c68"}, 642 | {file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:a2c34a93e1d2aa35fbf1485e5010337c72c6791407d03aa5f4eed920343dd360"}, 643 | {file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:feba80698173761cddd814fa22e88b0661e98cb810f9f986c54aa34d281e4937"}, 644 | {file = "multidict-6.0.2-cp39-cp39-win32.whl", hash = "sha256:23b616fdc3c74c9fe01d76ce0d1ce872d2d396d8fa8e4899398ad64fb5aa214a"}, 645 | {file = "multidict-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:4bae31803d708f6f15fd98be6a6ac0b6958fcf68fda3c77a048a4f9073704aae"}, 646 | {file = "multidict-6.0.2.tar.gz", hash = "sha256:5ff3bd75f38e4c43f1f470f2df7a4d430b821c4ce22be384e1459cb57d6bb013"}, 647 | ] 648 | mypy-extensions = [ 649 | {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, 650 | {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, 651 | ] 652 | nextcord = [ 653 | {file = "nextcord-2.0.0rc2.tar.gz", hash = "sha256:ed43897227b1d0b221a038858380dca02f4196ad273c928ddfbe6629802b68c6"}, 654 | ] 655 | pathspec = [ 656 | {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"}, 657 | {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, 658 | ] 659 | platformdirs = [ 660 | {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"}, 661 | {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"}, 662 | ] 663 | requests = [ 664 | {file = "requests-2.28.0-py3-none-any.whl", hash = "sha256:bc7861137fbce630f17b03d3ad02ad0bf978c844f3536d0edda6499dafce2b6f"}, 665 | {file = "requests-2.28.0.tar.gz", hash = "sha256:d568723a7ebd25875d8d1eaf5dfa068cd2fc8194b2e483d7b1f7c81918dbec6b"}, 666 | ] 667 | six = [ 668 | {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, 669 | {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, 670 | ] 671 | tomli = [ 672 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, 673 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, 674 | ] 675 | typing-extensions = [ 676 | {file = "typing_extensions-4.2.0-py3-none-any.whl", hash = "sha256:6657594ee297170d19f67d55c05852a874e7eb634f4f753dbd667855e07c1708"}, 677 | {file = "typing_extensions-4.2.0.tar.gz", hash = "sha256:f1c24655a0da0d1b67f07e17a5e6b2a105894e6824b92096378bb3668ef02376"}, 678 | ] 679 | urllib3 = [ 680 | {file = "urllib3-1.26.9-py2.py3-none-any.whl", hash = "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14"}, 681 | {file = "urllib3-1.26.9.tar.gz", hash = "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e"}, 682 | ] 683 | yarl = [ 684 | {file = "yarl-1.7.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f2a8508f7350512434e41065684076f640ecce176d262a7d54f0da41d99c5a95"}, 685 | {file = "yarl-1.7.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:da6df107b9ccfe52d3a48165e48d72db0eca3e3029b5b8cb4fe6ee3cb870ba8b"}, 686 | {file = "yarl-1.7.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a1d0894f238763717bdcfea74558c94e3bc34aeacd3351d769460c1a586a8b05"}, 687 | {file = "yarl-1.7.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfe4b95b7e00c6635a72e2d00b478e8a28bfb122dc76349a06e20792eb53a523"}, 688 | {file = "yarl-1.7.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c145ab54702334c42237a6c6c4cc08703b6aa9b94e2f227ceb3d477d20c36c63"}, 689 | {file = "yarl-1.7.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1ca56f002eaf7998b5fcf73b2421790da9d2586331805f38acd9997743114e98"}, 690 | {file = "yarl-1.7.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1d3d5ad8ea96bd6d643d80c7b8d5977b4e2fb1bab6c9da7322616fd26203d125"}, 691 | {file = "yarl-1.7.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:167ab7f64e409e9bdd99333fe8c67b5574a1f0495dcfd905bc7454e766729b9e"}, 692 | {file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:95a1873b6c0dd1c437fb3bb4a4aaa699a48c218ac7ca1e74b0bee0ab16c7d60d"}, 693 | {file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6152224d0a1eb254f97df3997d79dadd8bb2c1a02ef283dbb34b97d4f8492d23"}, 694 | {file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:5bb7d54b8f61ba6eee541fba4b83d22b8a046b4ef4d8eb7f15a7e35db2e1e245"}, 695 | {file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:9c1f083e7e71b2dd01f7cd7434a5f88c15213194df38bc29b388ccdf1492b739"}, 696 | {file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f44477ae29025d8ea87ec308539f95963ffdc31a82f42ca9deecf2d505242e72"}, 697 | {file = "yarl-1.7.2-cp310-cp310-win32.whl", hash = "sha256:cff3ba513db55cc6a35076f32c4cdc27032bd075c9faef31fec749e64b45d26c"}, 698 | {file = "yarl-1.7.2-cp310-cp310-win_amd64.whl", hash = "sha256:c9c6d927e098c2d360695f2e9d38870b2e92e0919be07dbe339aefa32a090265"}, 699 | {file = "yarl-1.7.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9b4c77d92d56a4c5027572752aa35082e40c561eec776048330d2907aead891d"}, 700 | {file = "yarl-1.7.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c01a89a44bb672c38f42b49cdb0ad667b116d731b3f4c896f72302ff77d71656"}, 701 | {file = "yarl-1.7.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c19324a1c5399b602f3b6e7db9478e5b1adf5cf58901996fc973fe4fccd73eed"}, 702 | {file = "yarl-1.7.2-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3abddf0b8e41445426d29f955b24aeecc83fa1072be1be4e0d194134a7d9baee"}, 703 | {file = "yarl-1.7.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6a1a9fe17621af43e9b9fcea8bd088ba682c8192d744b386ee3c47b56eaabb2c"}, 704 | {file = "yarl-1.7.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8b0915ee85150963a9504c10de4e4729ae700af11df0dc5550e6587ed7891e92"}, 705 | {file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:29e0656d5497733dcddc21797da5a2ab990c0cb9719f1f969e58a4abac66234d"}, 706 | {file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:bf19725fec28452474d9887a128e98dd67eee7b7d52e932e6949c532d820dc3b"}, 707 | {file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:d6f3d62e16c10e88d2168ba2d065aa374e3c538998ed04996cd373ff2036d64c"}, 708 | {file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:ac10bbac36cd89eac19f4e51c032ba6b412b3892b685076f4acd2de18ca990aa"}, 709 | {file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:aa32aaa97d8b2ed4e54dc65d241a0da1c627454950f7d7b1f95b13985afd6c5d"}, 710 | {file = "yarl-1.7.2-cp36-cp36m-win32.whl", hash = "sha256:87f6e082bce21464857ba58b569370e7b547d239ca22248be68ea5d6b51464a1"}, 711 | {file = "yarl-1.7.2-cp36-cp36m-win_amd64.whl", hash = "sha256:ac35ccde589ab6a1870a484ed136d49a26bcd06b6a1c6397b1967ca13ceb3913"}, 712 | {file = "yarl-1.7.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a467a431a0817a292121c13cbe637348b546e6ef47ca14a790aa2fa8cc93df63"}, 713 | {file = "yarl-1.7.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ab0c3274d0a846840bf6c27d2c60ba771a12e4d7586bf550eefc2df0b56b3b4"}, 714 | {file = "yarl-1.7.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d260d4dc495c05d6600264a197d9d6f7fc9347f21d2594926202fd08cf89a8ba"}, 715 | {file = "yarl-1.7.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fc4dd8b01a8112809e6b636b00f487846956402834a7fd59d46d4f4267181c41"}, 716 | {file = "yarl-1.7.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c1164a2eac148d85bbdd23e07dfcc930f2e633220f3eb3c3e2a25f6148c2819e"}, 717 | {file = "yarl-1.7.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:67e94028817defe5e705079b10a8438b8cb56e7115fa01640e9c0bb3edf67332"}, 718 | {file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:89ccbf58e6a0ab89d487c92a490cb5660d06c3a47ca08872859672f9c511fc52"}, 719 | {file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:8cce6f9fa3df25f55521fbb5c7e4a736683148bcc0c75b21863789e5185f9185"}, 720 | {file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:211fcd65c58bf250fb994b53bc45a442ddc9f441f6fec53e65de8cba48ded986"}, 721 | {file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c10ea1e80a697cf7d80d1ed414b5cb8f1eec07d618f54637067ae3c0334133c4"}, 722 | {file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:52690eb521d690ab041c3919666bea13ab9fbff80d615ec16fa81a297131276b"}, 723 | {file = "yarl-1.7.2-cp37-cp37m-win32.whl", hash = "sha256:695ba021a9e04418507fa930d5f0704edbce47076bdcfeeaba1c83683e5649d1"}, 724 | {file = "yarl-1.7.2-cp37-cp37m-win_amd64.whl", hash = "sha256:c17965ff3706beedafd458c452bf15bac693ecd146a60a06a214614dc097a271"}, 725 | {file = "yarl-1.7.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:fce78593346c014d0d986b7ebc80d782b7f5e19843ca798ed62f8e3ba8728576"}, 726 | {file = "yarl-1.7.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c2a1ac41a6aa980db03d098a5531f13985edcb451bcd9d00670b03129922cd0d"}, 727 | {file = "yarl-1.7.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:39d5493c5ecd75c8093fa7700a2fb5c94fe28c839c8e40144b7ab7ccba6938c8"}, 728 | {file = "yarl-1.7.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1eb6480ef366d75b54c68164094a6a560c247370a68c02dddb11f20c4c6d3c9d"}, 729 | {file = "yarl-1.7.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ba63585a89c9885f18331a55d25fe81dc2d82b71311ff8bd378fc8004202ff6"}, 730 | {file = "yarl-1.7.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e39378894ee6ae9f555ae2de332d513a5763276a9265f8e7cbaeb1b1ee74623a"}, 731 | {file = "yarl-1.7.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c0910c6b6c31359d2f6184828888c983d54d09d581a4a23547a35f1d0b9484b1"}, 732 | {file = "yarl-1.7.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6feca8b6bfb9eef6ee057628e71e1734caf520a907b6ec0d62839e8293e945c0"}, 733 | {file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8300401dc88cad23f5b4e4c1226f44a5aa696436a4026e456fe0e5d2f7f486e6"}, 734 | {file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:788713c2896f426a4e166b11f4ec538b5736294ebf7d5f654ae445fd44270832"}, 735 | {file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:fd547ec596d90c8676e369dd8a581a21227fe9b4ad37d0dc7feb4ccf544c2d59"}, 736 | {file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:737e401cd0c493f7e3dd4db72aca11cfe069531c9761b8ea474926936b3c57c8"}, 737 | {file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:baf81561f2972fb895e7844882898bda1eef4b07b5b385bcd308d2098f1a767b"}, 738 | {file = "yarl-1.7.2-cp38-cp38-win32.whl", hash = "sha256:ede3b46cdb719c794427dcce9d8beb4abe8b9aa1e97526cc20de9bd6583ad1ef"}, 739 | {file = "yarl-1.7.2-cp38-cp38-win_amd64.whl", hash = "sha256:cc8b7a7254c0fc3187d43d6cb54b5032d2365efd1df0cd1749c0c4df5f0ad45f"}, 740 | {file = "yarl-1.7.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:580c1f15500e137a8c37053e4cbf6058944d4c114701fa59944607505c2fe3a0"}, 741 | {file = "yarl-1.7.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3ec1d9a0d7780416e657f1e405ba35ec1ba453a4f1511eb8b9fbab81cb8b3ce1"}, 742 | {file = "yarl-1.7.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3bf8cfe8856708ede6a73907bf0501f2dc4e104085e070a41f5d88e7faf237f3"}, 743 | {file = "yarl-1.7.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1be4bbb3d27a4e9aa5f3df2ab61e3701ce8fcbd3e9846dbce7c033a7e8136746"}, 744 | {file = "yarl-1.7.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:534b047277a9a19d858cde163aba93f3e1677d5acd92f7d10ace419d478540de"}, 745 | {file = "yarl-1.7.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6ddcd80d79c96eb19c354d9dca95291589c5954099836b7c8d29278a7ec0bda"}, 746 | {file = "yarl-1.7.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9bfcd43c65fbb339dc7086b5315750efa42a34eefad0256ba114cd8ad3896f4b"}, 747 | {file = "yarl-1.7.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f64394bd7ceef1237cc604b5a89bf748c95982a84bcd3c4bbeb40f685c810794"}, 748 | {file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:044daf3012e43d4b3538562da94a88fb12a6490652dbc29fb19adfa02cf72eac"}, 749 | {file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:368bcf400247318382cc150aaa632582d0780b28ee6053cd80268c7e72796dec"}, 750 | {file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:bab827163113177aee910adb1f48ff7af31ee0289f434f7e22d10baf624a6dfe"}, 751 | {file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0cba38120db72123db7c58322fa69e3c0efa933040ffb586c3a87c063ec7cae8"}, 752 | {file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:59218fef177296451b23214c91ea3aba7858b4ae3306dde120224cfe0f7a6ee8"}, 753 | {file = "yarl-1.7.2-cp39-cp39-win32.whl", hash = "sha256:1edc172dcca3f11b38a9d5c7505c83c1913c0addc99cd28e993efeaafdfaa18d"}, 754 | {file = "yarl-1.7.2-cp39-cp39-win_amd64.whl", hash = "sha256:797c2c412b04403d2da075fb93c123df35239cd7b4cc4e0cd9e5839b73f52c58"}, 755 | {file = "yarl-1.7.2.tar.gz", hash = "sha256:45399b46d60c253327a460e99856752009fcee5f5d3c80b2f7c0cae1c38d56dd"}, 756 | ] 757 | zipp = [ 758 | {file = "zipp-3.8.0-py3-none-any.whl", hash = "sha256:c4f6e5bbf48e74f7a38e7cc5b0480ff42b0ae5178957d564d18932525d5cf099"}, 759 | {file = "zipp-3.8.0.tar.gz", hash = "sha256:56bf8aadb83c24db6c4b577e13de374ccfb67da2078beba1d037c17980bf43ad"}, 760 | ] 761 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "previous" 3 | version = "0.1.0" 4 | description = "The support bot for nextcord" 5 | authors = ["Tag-Epic "] 6 | 7 | [tool.poetry.dependencies] 8 | python = "^3.8" 9 | nextcord = "git+https://github.com/nextcord/nextcord.git#master" 10 | jishaku = "2.4.0" 11 | algoliasearch = "^2.6.2" 12 | 13 | [tool.poetry.dev-dependencies] 14 | black = "^22.3.0" 15 | isort = "^5.10.1" 16 | 17 | [build-system] 18 | requires = ["poetry-core>=1.0.0"] 19 | build-backend = "poetry.core.masonry.api" 20 | --------------------------------------------------------------------------------