├── .deepsource.toml ├── .github └── workflows │ ├── black.yml │ └── codeql-analysis.yml ├── .gitignore ├── LICENSE ├── README.md ├── bot.py ├── cogs ├── admin.py ├── animals.py ├── apis.py ├── background_tasks.py ├── compsci.py ├── crypto.py ├── economy.py ├── events.py ├── games.py ├── help.py ├── images.py ├── information.py ├── misc.py ├── moderation.py ├── music.py ├── owner.py ├── stocks.py ├── useful.py └── utils │ ├── __init__.py │ ├── calculation.py │ ├── color.py │ ├── database.py │ └── time.py ├── requirements.txt ├── run_tests.py └── tests ├── __init__.py ├── helpers.py ├── test_cogs.py └── test_helpers.py /.deepsource.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | 3 | [[analyzers]] 4 | name = "python" 5 | enabled = true 6 | 7 | [analyzers.meta] 8 | runtime_version = "3.x.x" 9 | -------------------------------------------------------------------------------- /.github/workflows/black.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | lint: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - uses: actions/setup-python@v2 11 | - uses: psf/black@stable 12 | with: 13 | args: ". --check" 14 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | schedule: 21 | - cron: '41 13 * * 1' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | language: [ 'python' ] 32 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 33 | # Learn more: 34 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 35 | 36 | steps: 37 | - name: Checkout repository 38 | uses: actions/checkout@v2 39 | 40 | # Initializes the CodeQL tools for scanning. 41 | - name: Initialize CodeQL 42 | uses: github/codeql-action/init@v1 43 | with: 44 | languages: ${{ matrix.language }} 45 | # If you wish to specify custom queries, you can do so here or in a config file. 46 | # By default, queries listed here will override any specified in a config file. 47 | # Prefix the list here with "+" to use these queries and those in the config file. 48 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 49 | 50 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 51 | # If this step fails, then you should remove it and run the build manually (see below) 52 | - name: Autobuild 53 | uses: github/codeql-action/autobuild@v1 54 | 55 | # ℹ️ Command-line programs to run using the OS shell. 56 | # 📚 https://git.io/JvXDl 57 | 58 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 59 | # and modify them (or add more) to build your code if your project 60 | # uses a compiled language 61 | 62 | #- run: | 63 | # make bootstrap 64 | # make release 65 | 66 | - name: Perform CodeQL Analysis 67 | uses: github/codeql-action/analyze@v1 68 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | discord.log 3 | 4 | # Json files 5 | json/ 6 | *.json 7 | 8 | # Database 9 | database.sqlite3 10 | db/ 11 | 12 | # Token 13 | config.py 14 | 15 | # Byte-compiled / optimized / DLL files 16 | __pycache__/ 17 | *.py[cod] 18 | *$py.class 19 | 20 | # Distribution / packaging 21 | .Python 22 | build/ 23 | develop-eggs/ 24 | dist/ 25 | downloads/ 26 | eggs/ 27 | .eggs/ 28 | lib/ 29 | lib64/ 30 | parts/ 31 | sdist/ 32 | var/ 33 | wheels/ 34 | share/python-wheels/ 35 | *.egg-info/ 36 | .installed.cfg 37 | *.egg 38 | MANIFEST 39 | 40 | # PyInstaller 41 | # Usually these files are written by a python script from a template 42 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 43 | *.manifest 44 | *.spec 45 | 46 | # Installer logs 47 | pip-log.txt 48 | pip-delete-this-directory.txt 49 | 50 | # PyBuilder 51 | .pybuilder/ 52 | target/ 53 | 54 | # IPython 55 | profile_default/ 56 | ipython_config.py 57 | 58 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 59 | __pypackages__/ 60 | 61 | # Celery stuff 62 | celerybeat-schedule 63 | celerybeat.pid 64 | 65 | # SageMath parsed files 66 | *.sage.py 67 | 68 | # Environments 69 | .env 70 | .venv 71 | env/ 72 | venv/ 73 | ENV/ 74 | env.bak/ 75 | venv.bak/ 76 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Singularitat 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## snakebot 2 | A discord.py bot that tries to do everything 3 | 4 | Code size GitHub repo size Lines of Code 5 | 6 | ## Running 7 | 8 | 1. **Python 3.10 or higher** 9 | 10 | 2. **Install dependencies** 11 | 12 | ```bash 13 | pip install -U -r requirements.txt 14 | ``` 15 | 16 | If plyvel fails to install on Windows install Visual Studio Build Tools 2019 17 | 18 | If plyvel fails to install on Debian or Ubuntu try 19 | ```bash 20 | apt-get install libleveldb1v5 libleveldb-dev 21 | ``` 22 | 23 | 3. **Setup configuration** 24 | 25 | The next step is just to create a file named `config.py` in the root directory where 26 | the [bot.py](/bot.py) file is with the following template: 27 | 28 | ```py 29 | token = '' # your bot's token 30 | ``` 31 | 32 |   33 | 34 | **Notes:** 35 | 36 | You will probably want to remove my discord id from the owner_ids in [bot.py](/bot.py#L30) and replace it with your own 37 | 38 | If you want the downvote command to work you should change the downvote emoji in [events.py](/cogs/events.py) 39 | 40 | If you want the music cog to work you will need [ffmpeg](https://ffmpeg.org/download.html) either on your PATH or in the root directory where 41 | the [bot.py](/bot.py) file is 42 | 43 |   44 | 45 | ## Requirements 46 | 47 | - [Python 3.10+](https://www.python.org/downloads) 48 | - [pycord](https://github.com/Pycord-Development/pycord) 49 | - [lxml](https://github.com/lxml/lxml) 50 | - [psutil](https://github.com/giampaolo/psutil) 51 | - [orjson](https://github.com/ijl/orjson) 52 | - [yt-dlp](https://github.com/yt-dlp/yt-dlp) 53 | - [plyvel](https://github.com/wbolster/plyvel) 54 | - [pillow](https://github.com/python-pillow/Pillow) 55 | -------------------------------------------------------------------------------- /bot.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import asyncio 4 | import logging 5 | import os 6 | import subprocess 7 | from contextlib import suppress 8 | 9 | import aiohttp 10 | import discord 11 | from discord.ext import commands 12 | from discord.gateway import DiscordWebSocket 13 | 14 | import config 15 | from cogs.utils.database import Database 16 | 17 | log = logging.getLogger() 18 | log.setLevel(50) 19 | 20 | handler = logging.FileHandler(filename="bot.log", encoding="utf-8", mode="a") 21 | handler.setFormatter( 22 | logging.Formatter("%(message)s; %(asctime)s", datefmt="%m-%d %H:%M:%S") 23 | ) 24 | 25 | log.addHandler(handler) 26 | 27 | 28 | class MonkeyWebSocket(DiscordWebSocket): 29 | async def send_as_json(self, data): 30 | if data.get("op") == self.IDENTIFY: 31 | if data.get("d", {}).get("properties", {}).get("$browser") is not None: 32 | data["d"]["properties"]["$browser"] = "Discord Android" 33 | data["d"]["properties"]["$device"] = "Discord Android" 34 | await super().send_as_json(data) 35 | 36 | 37 | DiscordWebSocket.from_client = MonkeyWebSocket.from_client 38 | 39 | 40 | class Bot(commands.Bot): 41 | """A subclass of discord.ext.commands.Bot.""" 42 | 43 | def __init__(self, *args, **kwargs): 44 | super().__init__(*args, **kwargs) 45 | 46 | self.client_session = None 47 | self.cache = {} 48 | self.DB = Database() 49 | 50 | async def get_prefix(self, message: discord.Message) -> str: 51 | default = "." 52 | 53 | if not message.guild: 54 | return default 55 | 56 | prefix = self.DB.main.get(f"{message.guild.id}-prefix".encode()) 57 | 58 | if not prefix: 59 | return default 60 | 61 | return prefix.decode() 62 | 63 | @classmethod 64 | def create(cls) -> commands.Bot: 65 | """Create and return an instance of a Bot.""" 66 | loop = asyncio.new_event_loop() 67 | 68 | intents = discord.Intents.all() 69 | intents.dm_typing = False 70 | intents.webhooks = False 71 | intents.integrations = False 72 | 73 | return cls( 74 | loop=loop, 75 | command_prefix=commands.when_mentioned_or(cls.get_prefix), 76 | activity=discord.Game(name="Tax Evasion Simulator"), 77 | case_insensitive=True, 78 | allowed_mentions=discord.AllowedMentions(everyone=False), 79 | intents=intents, 80 | owner_ids=(225708387558490112,), 81 | ) 82 | 83 | def load_extensions(self) -> None: 84 | """Load all extensions.""" 85 | for extension in [f.name[:-3] for f in os.scandir("cogs") if f.is_file()]: 86 | try: 87 | self.load_extension(f"cogs.{extension}") 88 | except Exception as e: 89 | print(f"Failed to load extension {extension}.\n{e} \n") 90 | 91 | async def get_json(self, url: str) -> dict: 92 | """Gets and loads json from a url. 93 | 94 | url: str 95 | The url to fetch the json from. 96 | """ 97 | try: 98 | async with self.client_session.get(url) as response: 99 | return await response.json() 100 | except ( 101 | asyncio.exceptions.TimeoutError, 102 | aiohttp.client_exceptions.ContentTypeError, 103 | ): 104 | return None 105 | 106 | async def run_process(self, command, raw=False) -> list | str: 107 | """Runs a shell command and returns the output. 108 | 109 | command: str 110 | The command to run. 111 | raw: bool 112 | If True returns the result just decoded. 113 | """ 114 | try: 115 | process = await asyncio.create_subprocess_shell( 116 | command, stdout=subprocess.PIPE, stderr=subprocess.PIPE 117 | ) 118 | result = await process.communicate() 119 | except NotImplementedError: 120 | process = subprocess.Popen( 121 | command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE 122 | ) 123 | result = await self.loop.run_in_executor(None, process.communicate) 124 | 125 | if raw: 126 | return [output.decode() for output in result] 127 | 128 | return "".join([output.decode() for output in result]).split() 129 | 130 | def remove_from_cache(self, search): 131 | """Deletes a search from the cache. 132 | 133 | search: str 134 | """ 135 | try: 136 | self.cache.pop(search) 137 | except KeyError: 138 | return 139 | 140 | async def close(self) -> None: 141 | """Close the Discord connection and the aiohttp session.""" 142 | for ext in list(self.extensions): 143 | with suppress(Exception): 144 | self.unload_extension(ext) 145 | 146 | for cog in list(self.cogs): 147 | with suppress(Exception): 148 | self.remove_cog(cog) 149 | 150 | await super().close() 151 | 152 | if self.client_session: 153 | await self.client_session.close() 154 | 155 | async def login(self, *args, **kwargs) -> None: 156 | """Setup the client_session before logging in.""" 157 | self.client_session = aiohttp.ClientSession( 158 | timeout=aiohttp.ClientTimeout(total=10) 159 | ) 160 | 161 | await super().login(*args, **kwargs) 162 | 163 | 164 | if __name__ == "__main__": 165 | bot = Bot.create() 166 | bot.load_extensions() 167 | bot.run(config.token) 168 | -------------------------------------------------------------------------------- /cogs/admin.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | import discord 4 | import orjson 5 | from discord.ext import commands 6 | 7 | from cogs.utils.time import parse_time 8 | 9 | 10 | class RoleButton(discord.ui.Button["ButtonRoles"]): 11 | def __init__(self, role: discord.Role, name: str, custom_id: str, row: int): 12 | self.role = role 13 | 14 | super().__init__( 15 | style=discord.ButtonStyle.secondary, 16 | label=name or role.name, 17 | custom_id=custom_id, 18 | row=row, 19 | ) 20 | 21 | async def callback(self, interaction: discord.Interaction): 22 | user = interaction.user 23 | 24 | if user.get_role(self.role.id): 25 | await user.remove_roles(self.role) 26 | await interaction.response.send_message( 27 | f"Removed {self.role.name} role", ephemeral=True 28 | ) 29 | else: 30 | await user.add_roles(self.role) 31 | await interaction.response.send_message( 32 | f"Added {self.role.name} role", ephemeral=True 33 | ) 34 | 35 | 36 | class ButtonRoles(discord.ui.View): 37 | def __init__( 38 | self, bot: commands.Bot, guild: int, roles: list[(int, str)], message_id: str 39 | ): 40 | super().__init__(timeout=None) 41 | guild = bot.get_guild(guild) 42 | count = 0 43 | row = 0 44 | 45 | if not guild: 46 | return 47 | 48 | self.guild = guild 49 | 50 | for role, name in roles: 51 | if role == "break": 52 | row += 1 53 | count = 0 54 | continue 55 | 56 | role = guild.get_role(role) 57 | 58 | if role: 59 | self.add_item(RoleButton(role, name, f"{message_id}-{role.id}", row)) 60 | count += 1 61 | if count % 5 == 0: 62 | row += 1 63 | 64 | 65 | class admin(commands.Cog): 66 | """Administrative commands.""" 67 | 68 | def __init__(self, bot: commands.Bot) -> None: 69 | self.bot = bot 70 | self.DB = bot.DB 71 | self.loop = bot.loop 72 | 73 | async def cog_check(self, ctx): 74 | """Checks if the member is an administrator. 75 | 76 | ctx: commands.Context 77 | """ 78 | if isinstance(ctx.author, discord.User): 79 | return ctx.author.id in self.bot.owner_ids 80 | return ctx.author.guild_permissions.administrator 81 | 82 | def on_ready(self): 83 | for message_id, data in self.DB.rrole: 84 | message_id = message_id.decode() 85 | data = orjson.loads(data) 86 | 87 | self.bot.add_view( 88 | ButtonRoles(self.bot, data["guild"], data["roles"], message_id) 89 | ) 90 | 91 | @commands.command() 92 | async def antispam(self, ctx): 93 | """Toggles antispam on or off.""" 94 | key = f"anti_spam-{ctx.guild.id}".encode() 95 | anti_spam = self.DB.main.get(key) 96 | embed = discord.Embed(color=discord.Color.blurple()) 97 | 98 | if anti_spam: 99 | self.DB.main.delete(key) 100 | embed.title = "Turned off anti spam" 101 | return await ctx.send(embed=embed) 102 | 103 | self.DB.main.put(key, b"1") 104 | embed.title = "Turned on anti spam" 105 | await ctx.send(embed=embed) 106 | 107 | @commands.command() 108 | async def role(self, ctx, *, information): 109 | """Creates a button role message. 110 | 111 | Example usage: 112 | .role `\u200B`\u200B`\u200Bless 113 | **Pronoun Role Menu** 114 | Click a button for a role 115 | he/him | he/him 116 | she/her | 117 | break 118 | they/them | 119 | 950348151674511360 | 120 | `\u200B`\u200B`\u200B 121 | 122 | Code blocks are optional. 123 | If a line doesn't have a | or is just a break then it is included in the title. 124 | To move to the next row of buttons early use break. 125 | Before the | is either an id or role name and after is the button label. 126 | If you don't give a button label the role name is used. 127 | """ 128 | information = re.sub(r"```\w+\n|```", "", information) 129 | 130 | title = "" 131 | roles = [] 132 | failed = [] 133 | 134 | for line in information.split("\n"): 135 | role_name, *display = line.split("|") 136 | 137 | if role_name == "break": 138 | roles.append(("break", None)) 139 | continue 140 | 141 | if not display: 142 | title += f"{role_name}\n" 143 | continue 144 | 145 | if not role_name: 146 | continue 147 | 148 | try: 149 | role_id = int(role_name) 150 | except ValueError: 151 | role = discord.utils.get(ctx.guild.roles, name=role_name.strip()) 152 | 153 | if not role: 154 | failed.append(role_name) 155 | continue # failed to get role 156 | 157 | role_id = role.id 158 | 159 | roles.append((role_id, display[0].strip())) 160 | 161 | message_id = str(ctx.message.id) 162 | 163 | await ctx.send( 164 | title, view=ButtonRoles(self.bot, ctx.guild.id, roles, message_id) 165 | ) 166 | if failed: 167 | await ctx.send(f"Failed to find the following roles: {failed}") 168 | data = { 169 | "guild": ctx.guild.id, 170 | "roles": roles, 171 | } 172 | self.DB.rrole.put(message_id.encode(), orjson.dumps(data)) 173 | 174 | @commands.command() 175 | async def prefix(self, ctx, prefix=None): 176 | """Changes the bot prefix in a guild. 177 | 178 | prefix: str 179 | """ 180 | embed = discord.Embed(color=discord.Color.blurple()) 181 | key = f"{ctx.guild.id}-prefix".encode() 182 | if not prefix: 183 | embed.description = ( 184 | f"```xl\nCurrent prefix is: {self.DB.main.get(key, b'.').decode()}```" 185 | ) 186 | return await ctx.send(embed=embed) 187 | self.DB.main.put(key, prefix.encode()) 188 | embed.description = f"```prolog\nChanged prefix to {prefix}```" 189 | await ctx.send(embed=embed) 190 | 191 | @commands.command() 192 | async def unsnipe(self, ctx): 193 | """Unsnipes the last deleted message.""" 194 | self.DB.main.delete(f"{ctx.guild.id}-snipe_message".encode()) 195 | 196 | @commands.command() 197 | async def sudoin(self, ctx, channel: discord.TextChannel, *, command: str): 198 | """Runs a command in another channel. 199 | 200 | channel: discord.TextChannel 201 | command: str 202 | """ 203 | ctx.message.channel = channel 204 | ctx.message.content = f"{ctx.prefix}{command}" 205 | new_ctx = await self.bot.get_context(ctx.message, cls=type(ctx)) 206 | new_ctx.reply = new_ctx.send # Can't reply to messages in other channels 207 | await self.bot.invoke(new_ctx) 208 | 209 | @commands.command(name="removereact") 210 | async def remove_reaction(self, ctx, message: discord.Message, reaction): 211 | """Removes a reaction from a message. 212 | 213 | message: discord.Message 214 | The id of the message you want to remove the reaction from. 215 | reaction: Union[discord.Emoji, str] 216 | The reaction to remove. 217 | """ 218 | await message.clear_reaction(reaction) 219 | 220 | @commands.command(name="removereacts") 221 | async def remove_reactions(self, ctx, message: discord.Message): 222 | """Removes all reactions from a message. 223 | 224 | message: discord.Message 225 | The id of the message you want to remove the reaction from. 226 | """ 227 | await message.clear_reactions() 228 | 229 | @commands.command() 230 | async def togglelog(self, ctx): 231 | """Toggles logging to the logs channel.""" 232 | key = f"{ctx.guild.id}-logging".encode() 233 | if self.DB.main.get(key): 234 | self.DB.main.delete(key) 235 | state = "Enabled" 236 | else: 237 | self.DB.main.put(key, b"1") 238 | state = "Disabled" 239 | 240 | embed = discord.Embed(color=discord.Color.blurple()) 241 | embed.description = f"```{state} logging```" 242 | await ctx.send(embed=embed) 243 | 244 | @commands.command(name="removerule") 245 | async def remove_rule(self, ctx, number: int): 246 | """Removes a rule from the server rules. 247 | 248 | number: int 249 | The number of the rule to delete starting from 1. 250 | """ 251 | key = f"{ctx.guild.id}-rules".encode() 252 | rules = self.DB.main.get(key) 253 | embed = discord.Embed(color=discord.Color.blurple()) 254 | 255 | if not rules: 256 | embed.description = "```No rules added yet.```" 257 | return await ctx.send(embed=embed) 258 | 259 | rules = orjson.loads(rules) 260 | 261 | if 0 < number - 1 < len(rules): 262 | embed.description = "```No rule found.```" 263 | return await ctx.send(embed=embed) 264 | 265 | rule = rules.pop(number - 1) 266 | self.DB.main.put(key, orjson.dumps(rules)) 267 | embed.description = f"```Removed rule {rule}.```" 268 | await ctx.send(embed=embed) 269 | 270 | @commands.command(name="addrule") 271 | async def add_rule(self, ctx, *, rule): 272 | """Adds a rule to the server rules. 273 | 274 | rule: str 275 | The rule to add. 276 | """ 277 | key = f"{ctx.guild.id}-rules".encode() 278 | rules = self.DB.main.get(key) 279 | 280 | if not rules: 281 | rules = [] 282 | else: 283 | rules = orjson.loads(rules) 284 | 285 | rules.append(rule) 286 | await ctx.send( 287 | embed=discord.Embed( 288 | color=discord.Color.blurple(), 289 | description=f"```Added rule {len(rules)}\n{rule}```", 290 | ) 291 | ) 292 | self.DB.main.put(key, orjson.dumps(rules)) 293 | 294 | @commands.command(aliases=["disablech", "disablechannel"]) 295 | async def disable_channel(self, ctx, channel: discord.TextChannel = None): 296 | """Disables commands from being used in a channel. 297 | 298 | channel: discord.TextChannel 299 | """ 300 | channel = channel or ctx.channel 301 | guild = str(ctx.guild.id) 302 | key = f"{guild}-disabled_channels".encode() 303 | 304 | disabled = self.DB.main.get(key) 305 | 306 | if not disabled: 307 | disabled = [] 308 | else: 309 | disabled = orjson.loads(disabled) 310 | 311 | if channel.id in disabled: 312 | disabled.remove(channel.id) 313 | state = "enabled" 314 | else: 315 | disabled.append(channel.id) 316 | state = "disabled" 317 | 318 | embed = discord.Embed(color=discord.Color.blurple()) 319 | embed.description = f"```Commands {state} in {channel}```" 320 | 321 | await ctx.send(embed=embed) 322 | self.DB.main.put(key, orjson.dumps(disabled)) 323 | 324 | @commands.command() 325 | async def lockall(self, ctx, toggle: bool = True): 326 | """Removes the send messages permissions from @everyone in every category. 327 | 328 | toggle: bool 329 | Use False to let @everyone send messages again. 330 | """ 331 | state = not toggle if toggle else None 332 | 333 | for channel in ctx.guild.text_channels: 334 | perms = channel.overwrites_for(ctx.guild.default_role) 335 | key = f"{ctx.guild.id}-{channel.id}-lock".encode() 336 | 337 | if perms.send_messages is False and state is False: 338 | self.DB.main.put(key, b"1") 339 | elif perms.send_messages is True and state is False: 340 | self.DB.main.put(key, b"0") 341 | perms.send_messages = False 342 | await channel.set_permissions(ctx.guild.default_role, overwrite=perms) 343 | elif (data := self.DB.main.get(key)) == b"0": 344 | perms.send_messages = True 345 | await channel.set_permissions(ctx.guild.default_role, overwrite=perms) 346 | self.DB.main.delete(key) 347 | elif not data: 348 | perms.send_messages = state 349 | await channel.set_permissions(ctx.guild.default_role, overwrite=perms) 350 | else: 351 | self.DB.main.delete(key) 352 | 353 | embed = discord.Embed(color=discord.Color.blurple()) 354 | if toggle: 355 | embed.description = "```Set all channels to read only.```" 356 | else: 357 | embed.description = "```Reset channel read permissions to default.```" 358 | await ctx.send(embed=embed) 359 | 360 | @commands.command() 361 | async def lockall_catagories(self, ctx, toggle: bool = True): 362 | for category in ctx.guild.categories: 363 | await category.set_permissions( 364 | ctx.guild.default_role, send_messages=not toggle if toggle else None 365 | ) 366 | 367 | embed = discord.Embed(color=discord.Color.blurple()) 368 | 369 | if toggle: 370 | embed.description = "```Set all categories to read only.```" 371 | else: 372 | embed.description = "```Reset categories read permissions to default.```" 373 | await ctx.send(embed=embed) 374 | 375 | @commands.command() 376 | async def toggle(self, ctx, *, command): 377 | """Toggles a command in the current guild.""" 378 | embed = discord.Embed(color=discord.Color.blurple()) 379 | 380 | if not self.bot.get_command(command): 381 | embed.description = "```Command not found.```" 382 | return await ctx.send(embed=embed) 383 | 384 | key = f"{ctx.guild.id}-t-{command}".encode() 385 | state = self.DB.main.get(key) 386 | 387 | if not state: 388 | self.DB.main.put(key, b"1") 389 | embed.description = f"```Disabled the {command} command```" 390 | return await ctx.send(embed=embed) 391 | 392 | self.DB.main.delete(key) 393 | embed.description = f"```Enabled the {command} command```" 394 | return await ctx.send(embed=embed) 395 | 396 | @commands.command() 397 | async def emojis(self, ctx): 398 | """Shows a list of the current emojis being voted on.""" 399 | emojis = self.DB.main.get(b"emoji_submissions") 400 | 401 | embed = discord.Embed(color=discord.Color.blurple()) 402 | 403 | if not emojis: 404 | embed.description = "```No emojis found```" 405 | return await ctx.send(embed=embed) 406 | 407 | emojis = orjson.loads(emojis) 408 | 409 | if not emojis: 410 | embed.description = "```No emojis found```" 411 | return await ctx.send(embed=embed) 412 | 413 | msg = "" 414 | 415 | for name, users in emojis.items(): 416 | msg += f"{name}: {users}\n" 417 | 418 | embed.description = f"```{msg}```" 419 | await ctx.send(embed=embed) 420 | 421 | @commands.command(aliases=["demoji", "delemoji"]) 422 | async def delete_emoji(self, ctx, message_id): 423 | """Deletes an emoji from the emojis being voted on. 424 | 425 | message_id: str 426 | Id of the message to remove from the db. 427 | """ 428 | emojis = self.DB.main.get(b"emoji_submissions") 429 | 430 | if not emojis: 431 | emojis = {} 432 | else: 433 | emojis = orjson.loads(emojis) 434 | 435 | try: 436 | emojis.pop(message_id) 437 | except KeyError: 438 | await ctx.send(f"Message {message_id} not found in emojis") 439 | 440 | self.DB.main.put(b"emoji_submissions", orjson.dumps(emojis)) 441 | 442 | @commands.command(aliases=["aemoji", "addemoji"]) 443 | async def add_emoji(self, ctx, message_id, name): 444 | """Adds a emoji to be voted on. 445 | 446 | message_id: int 447 | Id of the message you are adding the emoji of. 448 | """ 449 | emojis = self.DB.main.get(b"emoji_submissions") 450 | 451 | if not emojis: 452 | emojis = {} 453 | else: 454 | emojis = orjson.loads(emojis) 455 | 456 | emojis[message_id] = {"name": name, "users": []} 457 | 458 | self.DB.main.put(b"emoji_submissions", orjson.dumps(emojis)) 459 | 460 | @commands.command() 461 | async def edit(self, ctx, message: discord.Message, *, content): 462 | """Edits the content of a bot message. 463 | 464 | message: discord.Message 465 | The message you want to edit. 466 | content: str 467 | What the content of the message will be changed to. 468 | """ 469 | await message.edit(content=content) 470 | 471 | @commands.command(name="embededit") 472 | async def embed_edit(self, ctx, message: discord.Message, *, json): 473 | """Edits the embed of a bot message. 474 | 475 | example: 476 | .embed { 477 | "description": "description", 478 | "title": "title", 479 | "fields": [{"name": "name", "value": "value"}] 480 | } 481 | 482 | You only need either the title or description 483 | and fields are alaways optional 484 | 485 | json: str 486 | """ 487 | await message.edit(embed=discord.Embed.from_dict(orjson.loads(json))) 488 | 489 | @commands.command() 490 | async def embed(self, ctx, *, json): 491 | """Sends an embed. 492 | 493 | example: 494 | .embed { 495 | "description": "description", 496 | "title": "title", 497 | "fields": [{"name": "name", "value": "value"}] 498 | } 499 | 500 | You only need either the title or description 501 | and fields are alaways optional 502 | 503 | json: str 504 | """ 505 | await ctx.send(embed=discord.Embed.from_dict(orjson.loads(json))) 506 | 507 | @commands.command() 508 | async def downvote(self, ctx, member: discord.Member = None, *, duration=None): 509 | """Automatically downvotes someone. 510 | 511 | member: discord.Member 512 | The downvoted member. 513 | duration: str 514 | How long to downvote the user for e.g 5d 10h 25m 5s 515 | """ 516 | embed = discord.Embed(color=discord.Color.blurple()) 517 | 518 | if not member: 519 | for member_id in self.DB.blacklist.iterator(include_value=False): 520 | member_id = member_id.decode().split("-") 521 | 522 | if len(member_id) > 1: 523 | guild, member_id = member_id 524 | guild = self.bot.get_guild(int(guild)) 525 | else: 526 | guild, member_id = "Global", member_id[0] 527 | 528 | embed.add_field( 529 | name="User:", 530 | value=f"{guild}: {member_id}", 531 | ) 532 | if not embed.fields: 533 | embed.title = "No downvoted users" 534 | return await ctx.send(embed=embed) 535 | 536 | embed.title = "Downvoted users" 537 | return await ctx.send(embed=embed) 538 | 539 | if member.bot: 540 | embed.description = "Bots cannot be added to the downvote list" 541 | return await ctx.send(embed=embed) 542 | 543 | member_id = f"{ctx.guild.id}-{str(member.id)}".encode() 544 | 545 | if self.DB.blacklist.get(member_id): 546 | self.DB.blacklist.delete(member_id) 547 | 548 | embed.title = "User Undownvoted" 549 | embed.description = ( 550 | f"***{member}*** has been removed from the downvote list" 551 | ) 552 | return await ctx.send(embed=embed) 553 | 554 | await member.edit(voice_channel=None) 555 | 556 | if not duration: 557 | self.DB.blacklist.put(member_id, b"1") 558 | embed.title = "User Downvoted" 559 | embed.description = f"**{member}** has been added to the downvote list" 560 | return await ctx.send(embed=embed) 561 | 562 | seconds = (parse_time(duration) - discord.utils.utcnow()).total_seconds() 563 | 564 | if not seconds: 565 | embed.description = "```Invalid duration. Example: '3d 5h 10m'```" 566 | return await ctx.send(embed=embed) 567 | 568 | self.DB.blacklist.put(member_id, b"1") 569 | self.loop.call_later(seconds, self.DB.blacklist.delete, member_id) 570 | 571 | embed.title = "User Undownvoted" 572 | embed.description = f"***{member}*** has been added from the downvote list" 573 | await ctx.send(embed=embed) 574 | 575 | @commands.command() 576 | async def blacklist(self, ctx, user: discord.User = None): 577 | """Blacklists someone from using the bot. 578 | 579 | user: discord.User 580 | The blacklisted user. 581 | """ 582 | embed = discord.Embed(color=discord.Color.blurple()) 583 | if not user: 584 | for member_id in self.DB.blacklist.iterator(include_value=False): 585 | member_id = member_id.decode().split("-") 586 | 587 | if len(member_id) > 1: 588 | guild, member_id = member_id 589 | guild = self.bot.get_guild(int(guild)) 590 | else: 591 | guild, member_id = "Global", member_id[0] 592 | 593 | embed.add_field( 594 | name="User:", 595 | value=f"{guild}: {member_id}", 596 | ) 597 | if not embed.fields: 598 | embed.title = "No blacklisted users" 599 | return await ctx.send(embed=embed) 600 | 601 | embed.title = "Blacklisted users" 602 | return await ctx.send(embed=embed) 603 | 604 | user_id = f"{ctx.guild.id}-{str(user.id)}".encode() 605 | if self.DB.blacklist.get(user_id): 606 | self.DB.blacklist.delete(user_id) 607 | 608 | embed.title = "User Unblacklisted" 609 | embed.description = f"***{user}*** has been unblacklisted" 610 | return await ctx.send(embed=embed) 611 | 612 | self.DB.blacklist.put(user_id, b"2") 613 | embed.title = "User Blacklisted" 614 | embed.description = f"**{user}** has been added to the blacklist" 615 | 616 | await ctx.send(embed=embed) 617 | 618 | 619 | def setup(bot: commands.Bot) -> None: 620 | """Starts admin cog.""" 621 | bot.add_cog(admin(bot)) 622 | -------------------------------------------------------------------------------- /cogs/animals.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import random 4 | from io import BytesIO 5 | 6 | import discord 7 | from discord.ext import commands 8 | 9 | 10 | class animals(commands.Cog): 11 | """For commands related to animals.""" 12 | 13 | def __init__(self, bot: commands.Bot) -> None: 14 | self.bot = bot 15 | 16 | async def get(self, ctx, url: str, key: str | int, subkey: str | int = None): 17 | """Returns json response from url or sends error embed.""" 18 | with ctx.typing(): 19 | resp = await self.bot.get_json(url) 20 | 21 | if not resp: 22 | return await ctx.send( 23 | embed=discord.Embed( 24 | color=discord.Color.dark_red(), description="Failed to reach api" 25 | ).set_footer( 26 | text="api may be temporarily down or experiencing high trafic" 27 | ) 28 | ) 29 | await ctx.send(resp[key] if not subkey else resp[key][subkey]) 30 | 31 | async def get_multiple(self, ctx, arg_tuples): 32 | with ctx.typing(): 33 | for args in arg_tuples: 34 | url, key, subkey, prefix = *args, *((None,) * abs(len(args) - 4)) 35 | resp = await self.bot.get_json(url) 36 | 37 | if resp: 38 | break 39 | else: 40 | return await ctx.send( 41 | embed=discord.Embed( 42 | color=discord.Color.dark_red(), 43 | description="Failed to reach any api", 44 | ).set_footer( 45 | text="apis may be temporarily down or experiencing high trafic" 46 | ) 47 | ) 48 | return await ctx.send( 49 | prefix if prefix else "" + (resp[key] if not subkey else resp[key][subkey]) 50 | ) 51 | 52 | @commands.command() 53 | async def horse(self, ctx): 54 | """This horse doesn't exist.""" 55 | url = "https://thishorsedoesnotexist.com" 56 | 57 | async with ctx.typing(), self.bot.client_session.get(url) as resp: 58 | with BytesIO((await resp.read())) as image_binary: 59 | await ctx.send(file=discord.File(fp=image_binary, filename="image.png")) 60 | 61 | @commands.command() 62 | async def lizard(self, ctx): 63 | """Gets a random lizard image.""" 64 | await self.get(ctx, "https://nekos.life/api/v2/img/lizard", "url") 65 | 66 | @commands.command() 67 | async def duck(self, ctx): 68 | """Gets a random duck image.""" 69 | await self.get(ctx, "https://random-d.uk/api/v2/random", "url") 70 | 71 | @commands.command(name="duckstatus") 72 | async def duck_status(self, ctx, status=404): 73 | """Gets a duck image for status codes e.g 404. 74 | 75 | status: str 76 | """ 77 | await ctx.send(f"https://random-d.uk/api/http/{status}.jpg") 78 | 79 | @commands.command() 80 | async def bunny(self, ctx): 81 | """Gets a random bunny image.""" 82 | await self.get( 83 | ctx, "https://api.bunnies.io/v2/loop/random/?media=webm", "media", "webm" 84 | ) 85 | 86 | @commands.command() 87 | async def whale(self, ctx): 88 | """Gets a random whale image.""" 89 | await self.get(ctx, "https://some-random-api.ml/img/whale", "link") 90 | 91 | @commands.command() 92 | async def snake(self, ctx): 93 | """Gets a random snake image.""" 94 | await ctx.send( 95 | "https://raw.githubusercontent.com/Singularitat/snake-api/master/images/{}.jpg".format( 96 | random.randint(1, 769) 97 | ) 98 | ) 99 | 100 | @commands.command() 101 | async def racoon(self, ctx): 102 | """Gets a random racoon image.""" 103 | await self.get(ctx, "https://some-random-api.ml/img/racoon", "link") 104 | 105 | @commands.command() 106 | async def kangaroo(self, ctx): 107 | """Gets a random kangaroo image.""" 108 | await self.get(ctx, "https://some-random-api.ml/img/kangaroo", "link") 109 | 110 | @commands.command() 111 | async def koala(self, ctx): 112 | """Gets a random koala image.""" 113 | await self.get(ctx, "https://some-random-api.ml/img/koala", "link") 114 | 115 | @commands.command() 116 | async def bird(self, ctx): 117 | """Gets a random bird image.""" 118 | await self.get_multiple( 119 | ctx, 120 | ( 121 | ("https://some-random-api.ml/img/birb", "link"), 122 | ("http://shibe.online/api/birds", 0), 123 | ("https://api.alexflipnote.dev/birb", "file"), 124 | ), 125 | ) 126 | 127 | @commands.command() 128 | async def redpanda(self, ctx): 129 | """Gets a random red panda image.""" 130 | await self.get(ctx, "https://some-random-api.ml/img/red_panda", "link") 131 | 132 | @commands.command() 133 | async def panda(self, ctx): 134 | """Gets a random panda image.""" 135 | await self.get(ctx, "https://some-random-api.ml/img/panda", "link") 136 | 137 | @commands.command() 138 | async def fox(self, ctx): 139 | """Gets a random fox image.""" 140 | await self.get_multiple( 141 | ctx, 142 | ( 143 | ("https://randomfox.ca/floof", "image"), 144 | ("https://wohlsoft.ru/images/foxybot/randomfox.php", "file"), 145 | ("https://some-random-api.ml/img/fox", "link"), 146 | ), 147 | ) 148 | 149 | @commands.command() 150 | async def cat(self, ctx): 151 | """Gets a random cat image.""" 152 | await self.get_multiple( 153 | ctx, 154 | ( 155 | ("https://api.thecatapi.com/v1/images/search", 0, "url"), 156 | ("https://cataas.com/cat?json=true", "url", None, "https://cataas.com"), 157 | ("https://thatcopy.pw/catapi/rest", "webpurl"), 158 | ("http://shibe.online/api/cats", "0"), 159 | ("https://aws.random.cat/meow", "file"), 160 | ), 161 | ) 162 | 163 | @commands.command() 164 | async def catstatus(self, ctx, status=404): 165 | """Gets a cat image for a status e.g 404. 166 | 167 | status: str 168 | """ 169 | await ctx.send(f"https://http.cat/{status}") 170 | 171 | @commands.command() 172 | async def dog(self, ctx, breed=None): 173 | """Gets a random dog image.""" 174 | if breed: 175 | url = f"https://dog.ceo/api/breed/{breed}/images/random" 176 | return await self.get(ctx, url, "message") 177 | 178 | await self.get_multiple( 179 | ctx, 180 | ( 181 | ("https://dog.ceo/api/breeds/image/random", "message"), 182 | ("https://random.dog/woof.json", "url"), 183 | ( 184 | "https://api.thedogapi.com/v1/images/search?sub_id=demo-3d4325", 185 | 0, 186 | "url", 187 | ), 188 | ), 189 | ) 190 | 191 | @commands.command() 192 | async def dogstatus(self, ctx, status=404): 193 | """Gets a dog image for a status e.g 404. 194 | 195 | status: str 196 | """ 197 | await ctx.send(f"https://http.dog/{status}.jpg") 198 | 199 | @commands.command() 200 | async def shibe(self, ctx): 201 | """Gets a random dog image.""" 202 | await self.get(ctx, "http://shibe.online/api/shibes", 0) 203 | 204 | @commands.command() 205 | async def capybara(self, ctx): 206 | """Gets a random dog image.""" 207 | await self.get(ctx, "https://api.capy.lol/v1/capybara?json=true", "data", "url") 208 | 209 | 210 | def setup(bot: commands.Bot) -> None: 211 | """Starts the animals cog.""" 212 | bot.add_cog(animals(bot)) 213 | -------------------------------------------------------------------------------- /cogs/background_tasks.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import os 3 | from datetime import datetime 4 | 5 | import discord 6 | import lxml.html 7 | import orjson 8 | from discord.ext import commands, tasks 9 | 10 | 11 | class background_tasks(commands.Cog): 12 | """Commands related to the background tasks of the bot.""" 13 | 14 | def __init__(self, bot: commands.Bot) -> None: 15 | self.bot = bot 16 | self.DB = bot.DB 17 | self.start_tasks() 18 | 19 | def cog_unload(self): 20 | """When the cog is unloaded stop all running tasks.""" 21 | for task in self.tasks.values(): 22 | task.cancel() 23 | 24 | async def cog_check(self, ctx): 25 | """Checks if the member is an owner. 26 | 27 | ctx: commands.Context 28 | """ 29 | return ctx.author.id in self.bot.owner_ids 30 | 31 | def start_tasks(self): 32 | """Finds all the tasks in the cog and starts them. 33 | This also builds a dictionary of the tasks so we can access them later. 34 | """ 35 | self.tasks = {} 36 | 37 | for name, task_obj in vars(background_tasks).items(): 38 | if isinstance(task_obj, tasks.Loop): 39 | task = getattr(self, name) 40 | task.start() 41 | self.tasks[name] = task 42 | 43 | @commands.group(hidden=True) 44 | async def task(self, ctx): 45 | """The task command group.""" 46 | if not ctx.invoked_subcommand: 47 | embed = discord.Embed(color=discord.Color.blurple()) 48 | task_name = ctx.subcommand_passed 49 | 50 | if task_name not in self.tasks: 51 | embed.description = ( 52 | f"```Usage: {ctx.prefix}task [restart/start/stop/list]```" 53 | ) 54 | return await ctx.send(embed=embed) 55 | 56 | task = self.tasks[task_name] 57 | embed.title = f"{task_name.replace('_', ' ').title()} Task" 58 | embed.add_field(name="Running", value=task.is_running()) 59 | embed.add_field(name="Failed", value=task.failed()) 60 | embed.add_field(name="Count", value=task.current_loop) 61 | if task.next_iteration: 62 | embed.add_field( 63 | name="Next Loop", 64 | value=f"****", 65 | ) 66 | embed.add_field( 67 | name="Interval", 68 | value=f"{task.hours:.0f}h {task.minutes:.0f}m {task.seconds:.0f}s", 69 | ) 70 | await ctx.send(embed=embed) 71 | 72 | @task.command() 73 | async def restart(self, ctx, task_name=None): 74 | """Restarts a background task. 75 | 76 | task_name: str 77 | The name of the task to restart. 78 | If not passed in then all tasks are restarted 79 | """ 80 | embed = discord.Embed(color=discord.Color.blurple()) 81 | 82 | if not task_name: 83 | for task in self.tasks.values(): 84 | task.restart() 85 | embed.description = "```Restarted all tasks```" 86 | return await ctx.send(embed=embed) 87 | 88 | if task_name not in self.tasks: 89 | embed.description = "```Task not found```" 90 | return await ctx.send(embed=embed) 91 | 92 | self.tasks[task_name].restart() 93 | embed.description = f"{task_name} restarted" 94 | await ctx.send(embed=embed) 95 | 96 | @task.command() 97 | async def start(self, ctx, task_name=None): 98 | """Starts a background task. 99 | 100 | task_name: str 101 | The name of the task to start. 102 | If not passed in then all tasks are started 103 | """ 104 | embed = discord.Embed(color=discord.Color.blurple()) 105 | 106 | if not task_name: 107 | for name, task in self.tasks.items(): 108 | task.cancel() 109 | embed.add_field( 110 | name=name, value=f">>> ```ahk\nRunning: {task.is_running()}```" 111 | ) 112 | embed.description = "```Tried to start all tasks```" 113 | return await ctx.send(embed=embed) 114 | 115 | if task_name not in self.tasks: 116 | embed.description = "```Task not found```" 117 | return await ctx.send(embed=embed) 118 | 119 | self.tasks[task_name].start() 120 | embed.description = f"{task_name} started" 121 | await ctx.send(embed=embed) 122 | 123 | @task.command() 124 | async def stop(self, ctx, task_name=None): 125 | """Stops a background task. 126 | 127 | Unlike cancel it waits for the task to finish its current loop 128 | 129 | task_name: str 130 | The name of the task to stop. 131 | If not passed in then all tasks are stopped 132 | """ 133 | embed = discord.Embed(color=discord.Color.blurple()) 134 | 135 | if not task_name: 136 | for name, task in self.tasks.items(): 137 | task.cancel() 138 | embed.add_field( 139 | name=name, value=f">>> ```ahk\nRunning: {task.is_running()}```" 140 | ) 141 | embed.description = "```Tried to stop all tasks```" 142 | return await ctx.send(embed=embed) 143 | 144 | if task_name not in self.tasks: 145 | embed.description = "```Task not found```" 146 | return await ctx.send(embed=embed) 147 | 148 | self.tasks[task_name].stop() 149 | embed.description = f"{task_name} stopped" 150 | await ctx.send(embed=embed) 151 | 152 | @task.command() 153 | async def cancel(self, ctx, task_name=None): 154 | """Cancels a background task. 155 | 156 | Unlike stop it ends the task immediately 157 | 158 | task_name: str 159 | The name of the task to stop. 160 | If not passed in then all tasks are canceled 161 | """ 162 | embed = discord.Embed(color=discord.Color.blurple()) 163 | 164 | if not task_name: 165 | for name, task in self.tasks.items(): 166 | task.cancel() 167 | embed.add_field( 168 | name=name, value=f">>> ```ahk\nRunning: {task.is_running()}```" 169 | ) 170 | embed.description = "```Tried to cancel all tasks```" 171 | return await ctx.send(embed=embed) 172 | 173 | if task_name not in self.tasks: 174 | embed.description = "```Task not found```" 175 | return await ctx.send(embed=embed) 176 | 177 | self.tasks[task_name].cancel() 178 | embed.description = f"{task_name} canceled" 179 | await ctx.send(embed=embed) 180 | 181 | @task.command() 182 | async def list(self, ctx): 183 | """Lists background tasks. 184 | 185 | Example 186 | 187 | Name: Interval: Running: Failed: Count: 188 | 189 | get_stocks 0h 30m 0s True False 161 190 | update_bot 0h 5m 0s True False 970 191 | backup 6h 0m 0s True False 13 192 | get_languages 0h 0m 0s False False 0 193 | get_crypto 0h 30m 0s True False 161 194 | get_domain 24h 0m 0s True False 3 195 | """ 196 | embed = discord.Embed(color=discord.Color.blurple()) 197 | 198 | msg = "Name: Interval: Running: Failed: Count:\n\n" 199 | for name, task in self.tasks.items(): 200 | msg += "{:<20}{:<4}{:<4}{:<5}{:<9}{:<8}{}\n".format( 201 | name, 202 | f"{task.hours:.0f}h", 203 | f"{task.minutes:.0f}m", 204 | f"{task.seconds:.0f}s", 205 | str(task.is_running()), 206 | str(task.failed()), 207 | task.current_loop, 208 | ) 209 | 210 | embed.description = f"```prolog\n{msg}```" 211 | await ctx.send(embed=embed) 212 | 213 | @tasks.loop(hours=1) 214 | async def get_stocks(self): 215 | """Updates stock data every hour.""" 216 | url = "https://api.nasdaq.com/api/screener/stocks?limit=50000" 217 | headers = { 218 | "authority": "api.nasdaq.com", 219 | "cache-control": "max-age=0", 220 | "sec-ch-ua": '" Not A;Brand";v="99", "Chromium";v="96"', 221 | "sec-ch-ua-mobile": "?0", 222 | "sec-ch-ua-platform": '"Windows"', 223 | "upgrade-insecure-requests": "1", 224 | "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.45 Safari/537.36", 225 | "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9", 226 | "sec-fetch-site": "none", 227 | "sec-fetch-mode": "navigate", 228 | "sec-fetch-user": "?1", 229 | "sec-fetch-dest": "document", 230 | "accept-language": "en-US,en;q=0.9", 231 | } 232 | current_cookies = self.DB.main.get(b"stock-cookies") 233 | if not current_cookies: 234 | current_cookies = {} 235 | else: 236 | current_cookies = orjson.loads(current_cookies) 237 | 238 | async with self.bot.client_session.get( 239 | url, headers=headers, cookies=current_cookies, timeout=30 240 | ) as resp: 241 | next_cookies = {} 242 | for header, value in resp.raw_headers: 243 | if header != b"Set-Cookie": 244 | continue 245 | name, cookie = value.decode().split("=", 1) 246 | next_cookies[name] = cookie.split(":", 1)[0] 247 | self.DB.main.put(b"stock-cookies", orjson.dumps(next_cookies)) 248 | stocks = await resp.json() 249 | 250 | if not stocks: 251 | return 252 | 253 | with self.DB.stocks.write_batch() as wb: 254 | for stock in stocks["data"]["table"]["rows"]: 255 | stock_data = { 256 | "name": stock["name"], 257 | "price": stock["lastsale"][1:], 258 | "change": stock["netchange"], 259 | "%change": stock["pctchange"][:-1] 260 | if stock["pctchange"] != "--" 261 | else 0, 262 | "cap": stock["marketCap"], 263 | } 264 | 265 | wb.put( 266 | stock["symbol"].encode(), 267 | orjson.dumps(stock_data), 268 | ) 269 | 270 | @tasks.loop(minutes=5) 271 | async def update_bot(self): 272 | """Tries to update every 5 minutes and then reloads if needed.""" 273 | pull = await self.bot.run_process("git pull") 274 | 275 | if pull[:4] == ["Already", "up", "to", "date."]: 276 | return 277 | 278 | diff = await self.bot.run_process("git diff --name-only HEAD@{0} HEAD@{1}") 279 | 280 | if "requirements.txt" in diff: 281 | await self.bot.run_process("pip install -r ./requirements.txt") 282 | 283 | for ext in ( 284 | file.removesuffix(".py") 285 | for file in os.listdir("cogs") 286 | if file.endswith(".py") and f"cogs/{file}" in diff 287 | ): 288 | try: 289 | self.bot.reload_extension(f"cogs.{ext}") 290 | except Exception as e: 291 | if isinstance(e, commands.errors.ExtensionNotLoaded): 292 | self.bot.load_extension(f"cogs.{ext}") 293 | 294 | @tasks.loop(hours=6) 295 | async def backup(self): 296 | """Makes a backup of the db every 6 hours.""" 297 | if self.DB.main.get(b"restart") == b"1": 298 | return 299 | 300 | number = self.DB.main.get(b"backup_number") 301 | 302 | if not number: 303 | number = -1 304 | else: 305 | number = int(number.decode()) 306 | 307 | number = (number + 1) % 11 308 | 309 | self.DB.main.put(b"backup_number", str(number).encode()) 310 | 311 | os.makedirs("backup/", exist_ok=True) 312 | with open(f"backup/{number}backup.json", "w", encoding="utf-8") as file: 313 | database = {} 314 | 315 | excluded = ( 316 | b"crypto", 317 | b"stocks", 318 | b"boot_times", 319 | b"tiolanguages", 320 | b"helloworlds", 321 | b"docs", 322 | ) 323 | 324 | for key, value in self.DB.main: 325 | if key.split(b"-")[0] not in excluded: 326 | if value[:1] in [b"{", b"["]: 327 | value = orjson.loads(value) 328 | else: 329 | value = value.decode() 330 | database[key.decode()] = value 331 | 332 | file.write(str(database)) 333 | 334 | @tasks.loop(count=1) 335 | async def get_languages(self): 336 | """Updates pistons supported languages for the run command.""" 337 | url = "https://emkc.org/api/v2/piston/runtimes" 338 | data = await self.bot.get_json(url) 339 | 340 | if data: 341 | aliases = set() 342 | languages = set() 343 | 344 | for language in data: 345 | aliases.update(language["aliases"]) 346 | aliases.add(language["language"]) 347 | languages.add(language["language"]) 348 | 349 | self.DB.main.put(b"languages", orjson.dumps(list(languages))) 350 | self.DB.main.put(b"aliases", orjson.dumps(list(aliases))) 351 | 352 | url = "https://tio.run/languages.json" 353 | data = await self.bot.get_json(url) 354 | 355 | if not data: 356 | return 357 | 358 | self.DB.main.put(b"tiolanguages", orjson.dumps([*data])) 359 | 360 | hello_worlds = {} 361 | 362 | for language in data: 363 | for request in data[language]["tests"]["helloWorld"]["request"]: 364 | if request["command"] == "F" and ".code.tio" in request["payload"]: 365 | hello_worlds[language] = request["payload"][".code.tio"] 366 | 367 | self.DB.main.put(b"helloworlds", orjson.dumps(hello_worlds)) 368 | 369 | @tasks.loop(minutes=30) 370 | async def get_crypto(self): 371 | """Updates crypto currency data every 30 minutes.""" 372 | url = "https://api.coinmarketcap.com/data-api/v3/cryptocurrency/listing?limit=50000&convert=NZD&cryptoType=coins" 373 | crypto = await self.bot.get_json(url) 374 | 375 | if not crypto: 376 | return 377 | 378 | with self.DB.crypto.write_batch() as wb: 379 | for coin in crypto["data"]["cryptoCurrencyList"]: 380 | if "price" not in coin["quotes"][0]: 381 | continue 382 | 383 | timestamp = datetime.fromisoformat( 384 | coin["quotes"][0]["lastUpdated"][:-1] 385 | ).timestamp() 386 | 387 | wb.put( 388 | coin["symbol"].encode(), 389 | orjson.dumps( 390 | { 391 | "name": coin["name"], 392 | "id": coin["id"], 393 | "price": coin["quotes"][0]["price"], 394 | "circulating_supply": int(coin["circulatingSupply"]), 395 | "max_supply": int(coin.get("maxSupply", 0)), 396 | "market_cap": coin["quotes"][0].get("marketCap", 0), 397 | "change_24h": coin["quotes"][0]["percentChange24h"], 398 | "volume_24h": coin["quotes"][0].get("volume24h", 0), 399 | "timestamp": int(timestamp), 400 | } 401 | ), 402 | ) 403 | 404 | @tasks.loop(hours=24) 405 | async def get_domain(self): 406 | """Updates the domain used for the tempmail command.""" 407 | url = "https://api.mail.tm/domains?page=1" 408 | async with self.bot.client_session.get(url) as resp: 409 | data = await resp.json() 410 | 411 | domain = data["hydra:member"][0]["domain"] 412 | self.DB.main.put(b"tempdomain", domain.encode()) 413 | 414 | @tasks.loop(hours=2) 415 | async def get_currencies(self): 416 | url = "https://api.vatcomply.com/rates?base=NZD" 417 | rates = await self.bot.get_json(url) 418 | 419 | url = "https://api.vatcomply.com/currencies" 420 | symbols = await self.bot.get_json(url) 421 | 422 | if not symbols or not rates: 423 | return 424 | 425 | for key, rate in rates["rates"].items(): 426 | symbols[key]["rate"] = rate 427 | 428 | symbols["NZD"]["symbol"] = "$" 429 | symbols["CAD"]["symbol"] = "$" 430 | symbols["AUD"]["symbol"] = "$" 431 | 432 | self.DB.main.put(b"currencies", orjson.dumps(symbols)) 433 | 434 | def find_courses(self, courses, soup): 435 | element_class = '"course-card w3-panel w3-white w3-card w3-round w3-display-container p-3 pl-4 pr-4"' 436 | 437 | for course in soup.xpath(f".//div[@class={element_class}]"): 438 | title = course.find( 439 | './/h4[@class="w3-show-inline-block course-code search-text-region"]' 440 | ).text.strip() 441 | semester = " ".join( 442 | course.find('.//div[@class="mr-2 mb-3"]').text.split()[:-1] 443 | ) 444 | 445 | if title not in courses: 446 | prescription = course.find( 447 | './/div[@class="mr-2 mb-3 course-prescription"]' 448 | ) 449 | if prescription is not None: 450 | prescription = prescription.text.strip() 451 | restrictions = course.find( 452 | './/div[@class="mr-2 mb-3 requirement-description"]' 453 | ) 454 | if restrictions is not None: 455 | restrictions = restrictions.text.strip() 456 | 457 | courses[title] = [ 458 | [semester], 459 | prescription, 460 | restrictions, 461 | ] 462 | else: 463 | courses[title][0].append(semester) 464 | 465 | @tasks.loop(count=1) 466 | async def get_courses(self): 467 | """Gets information about compsci courses at the University of Auckland.""" 468 | if self.DB.main.get(b"restart") == b"1": 469 | await self.delayed_delete() 470 | 471 | year = str(datetime.now().year)[-2:] 472 | 473 | url = ( 474 | "https://courseoutline.auckland.ac.nz/dco/course/advanceSearch" 475 | f"?facultyId=4000&termCodeYear=1{year}&organisationCode=COMSCI" 476 | ) 477 | courses = {} 478 | 479 | async with self.bot.client_session.get(url) as resp: 480 | soup = lxml.html.fromstring(await resp.text()) 481 | 482 | self.find_courses(courses, soup) 483 | 484 | links = soup.xpath('.//div[@id="pagination"]//a/@href')[:-1] 485 | 486 | for link in links: 487 | async with self.bot.client_session.get(link) as resp: 488 | soup = lxml.html.fromstring(await resp.text()) 489 | 490 | self.find_courses(courses, soup) 491 | 492 | self.DB.main.put(b"courses", orjson.dumps(courses)) 493 | 494 | async def delayed_delete(self): 495 | await asyncio.sleep(1) 496 | 497 | self.DB.main.delete(b"restart") 498 | 499 | 500 | def setup(bot): 501 | """Starts the background tasks cog""" 502 | bot.add_cog(background_tasks(bot)) 503 | -------------------------------------------------------------------------------- /cogs/crypto.py: -------------------------------------------------------------------------------- 1 | import textwrap 2 | from decimal import Decimal 3 | 4 | import discord 5 | import orjson 6 | from discord.ext import commands, pages 7 | 8 | 9 | class crypto(commands.Cog): 10 | """Crypto related commands.""" 11 | 12 | def __init__(self, bot: commands.Bot) -> None: 13 | self.bot = bot 14 | self.DB = bot.DB 15 | 16 | @commands.group(aliases=["coin"]) 17 | async def crypto(self, ctx): 18 | """Gets some information about crypto currencies.""" 19 | if ctx.invoked_subcommand: 20 | return 21 | 22 | embed = discord.Embed(colour=discord.Colour.blurple()) 23 | 24 | if not ctx.subcommand_passed: 25 | embed = discord.Embed(color=discord.Color.blurple()) 26 | embed.description = ( 27 | f"```Usage: {ctx.prefix}coin [buy/sell/bal/profile/list/history]" 28 | f" or {ctx.prefix}coin [token]```" 29 | ) 30 | return await ctx.send(embed=embed) 31 | 32 | symbol = ctx.subcommand_passed.upper() 33 | crypto = self.DB.get_crypto(symbol) 34 | 35 | if not crypto: 36 | embed.description = f"```Couldn't find {symbol}```" 37 | return await ctx.send(embed=embed) 38 | 39 | sign = "+" if crypto["change_24h"] >= 0 else "" 40 | 41 | embed.set_author( 42 | name=f"{crypto['name']} [{symbol}]", 43 | icon_url=f"https://s2.coinmarketcap.com/static/img/coins/64x64/{crypto['id']}.png", 44 | ) 45 | embed.add_field(name="Price", value=f"```${crypto['price']:,.2f}```") 46 | embed.add_field( 47 | name="Circulating/Max Supply", 48 | value=f"```{crypto['circulating_supply']:,}/{crypto['max_supply']:,}```", 49 | ) 50 | embed.add_field(name="Market Cap", value=f"```${crypto['market_cap']:,.2f}```") 51 | embed.add_field( 52 | name="24h Change", value=f"```diff\n{sign}{crypto['change_24h']}%```" 53 | ) 54 | embed.add_field(name="24h Volume", value=f"```{crypto['volume_24h']:,.2f}```") 55 | embed.add_field(name="Last updated", value=f"") 56 | embed.set_image( 57 | url=f"https://s3.coinmarketcap.com/generated/sparklines/web/1d/usd/{crypto['id']}.png" 58 | ) 59 | 60 | await ctx.send(embed=embed) 61 | 62 | @crypto.command(aliases=["b"]) 63 | async def buy(self, ctx, symbol: str, cash: float): 64 | """Buys an amount of crypto. 65 | 66 | coin: str 67 | The symbol of the crypto. 68 | cash: int 69 | How much money you want to invest in the coin. 70 | """ 71 | embed = discord.Embed(color=discord.Color.blurple()) 72 | 73 | if cash < 0: 74 | embed.description = "```You can't buy a negative amount of crypto```" 75 | return await ctx.send(embed=embed) 76 | 77 | symbol = symbol.upper() 78 | data = self.DB.get_crypto(symbol) 79 | 80 | if not data: 81 | embed.description = f"```Couldn't find crypto {symbol}```" 82 | return await ctx.send(embed=embed) 83 | 84 | price = float(data["price"]) 85 | member_id = str(ctx.author.id).encode() 86 | bal = self.DB.get_bal(member_id) 87 | 88 | if bal < cash: 89 | embed.description = "```You don't have enough cash```" 90 | return await ctx.send(embed=embed) 91 | 92 | amount = cash / price 93 | 94 | cryptobal = self.DB.get_cryptobal(member_id) 95 | 96 | if symbol not in cryptobal: 97 | cryptobal[symbol] = {"total": 0, "history": [(amount, cash)]} 98 | else: 99 | cryptobal[symbol]["history"].append((amount, cash)) 100 | 101 | cryptobal[symbol]["total"] += amount 102 | bal -= Decimal(cash) 103 | 104 | embed = discord.Embed( 105 | title=f"You bought {amount:.2f} {data['name']}", 106 | color=discord.Color.blurple(), 107 | ) 108 | embed.set_footer(text=f"Balance: ${bal:,f}") 109 | 110 | await ctx.send(embed=embed) 111 | 112 | self.DB.put_bal(member_id, bal) 113 | self.DB.put_cryptobal(member_id, cryptobal) 114 | 115 | @crypto.command(aliases=["s"]) 116 | async def sell(self, ctx, symbol, amount): 117 | """Sells crypto. 118 | 119 | symbol: str 120 | The symbol of the crypto to sell. 121 | amount: float 122 | The amount to sell. 123 | """ 124 | embed = discord.Embed(color=discord.Color.blurple()) 125 | 126 | symbol = symbol.upper() 127 | price = self.DB.get_crypto(symbol) 128 | 129 | if not price: 130 | embed.description = f"```Couldn't find {symbol}```" 131 | return await ctx.send(embed=embed) 132 | 133 | price = price["price"] 134 | member_id = str(ctx.author.id).encode() 135 | cryptobal = self.DB.get_cryptobal(member_id) 136 | 137 | if not cryptobal: 138 | embed.description = "```You haven't invested.```" 139 | return await ctx.send(embed=embed) 140 | 141 | if symbol not in cryptobal: 142 | embed.description = f"```You haven't invested in {symbol}.```" 143 | return await ctx.send(embed=embed) 144 | 145 | if amount[-1] == "%": 146 | amount = cryptobal[symbol]["total"] * ((float(amount[:-1])) / 100) 147 | else: 148 | amount = float(amount) 149 | 150 | if amount < 0: 151 | embed.description = "```You can't sell a negative amount of crypto```" 152 | return await ctx.send(embed=embed) 153 | 154 | if cryptobal[symbol]["total"] < amount: 155 | embed.description = ( 156 | f"```Not enough {symbol} you have: {cryptobal[symbol]['total']}```" 157 | ) 158 | return await ctx.send(embed=embed) 159 | 160 | bal = self.DB.get_bal(member_id) 161 | cash = amount * float(price) 162 | 163 | cryptobal[symbol]["total"] -= amount 164 | 165 | if cryptobal[symbol]["total"] == 0: 166 | cryptobal.pop(symbol, None) 167 | else: 168 | cryptobal[symbol]["history"].append((-amount, cash)) 169 | 170 | bal += Decimal(cash) 171 | 172 | embed.title = f"Sold {amount:.2f} {symbol} for ${cash:.2f}" 173 | embed.set_footer(text=f"Balance: ${bal:,f}") 174 | 175 | await ctx.send(embed=embed) 176 | 177 | self.DB.put_bal(member_id, bal) 178 | self.DB.put_cryptobal(member_id, cryptobal) 179 | 180 | @crypto.command(aliases=["p"]) 181 | async def profile(self, ctx, member: discord.Member = None): 182 | """Gets someone's crypto profile. 183 | 184 | member: discord.Member 185 | The member whose crypto profile will be shown. 186 | """ 187 | member = member or ctx.author 188 | 189 | member_id = str(member.id).encode() 190 | cryptobal = self.DB.get_cryptobal(member_id) 191 | embed = discord.Embed(color=discord.Color.blurple()) 192 | 193 | if not cryptobal: 194 | embed.description = "```You haven't invested.```" 195 | return await ctx.send(embed=embed) 196 | 197 | net_value = 0 198 | msg = ( 199 | f"{member.display_name}'s crypto profile:\n\n" 200 | "Name: Amount: Price: Percent Gain:\n" 201 | ) 202 | 203 | for crypto in cryptobal: 204 | data = self.DB.get_crypto(crypto) 205 | 206 | trades = [ 207 | trade[1] / trade[0] 208 | for trade in cryptobal[crypto]["history"] 209 | if trade[0] > 0 210 | ] 211 | change = ((data["price"] / (sum(trades) / len(trades))) - 1) * 100 212 | color = "31" if change < 0 else "32" 213 | 214 | msg += ( 215 | f"[2;{color}m{crypto + ':':<8} {cryptobal[crypto]['total']:<13.2f}" 216 | f"${data['price']:<17.2f} {change:.2f}%\n" 217 | ) 218 | 219 | net_value += cryptobal[crypto]["total"] * float(data["price"]) 220 | 221 | embed.description = f"```ansi\n{msg}\nNet Value: ${net_value:.2f}```" 222 | await ctx.send(embed=embed) 223 | 224 | @crypto.command() 225 | async def bal(self, ctx, symbol: str): 226 | """Shows how much of a crypto you have. 227 | 228 | symbol: str 229 | The symbol of the crypto to find. 230 | """ 231 | symbol = symbol.upper() 232 | member_id = str(ctx.author.id).encode() 233 | 234 | cryptobal = self.DB.get_cryptobal(member_id) 235 | embed = discord.Embed(color=discord.Color.blurple()) 236 | 237 | if not cryptobal: 238 | embed.description = "```You haven't invested.```" 239 | return await ctx.send(embed=embed) 240 | 241 | if symbol not in cryptobal: 242 | embed.description = f"```You haven't invested in {symbol}```" 243 | return await ctx.send(embed=embed) 244 | 245 | crypto = self.DB.get_crypto(symbol) 246 | 247 | trades = [ 248 | trade[1] / trade[0] 249 | for trade in cryptobal[symbol]["history"] 250 | if trade[0] > 0 251 | ] 252 | change = ((crypto["price"] / (sum(trades) / len(trades))) - 1) * 100 253 | sign = "" if crypto["change_24h"] < 0 else "+" 254 | 255 | embed.set_author( 256 | name=f"{crypto['name']} [{symbol}]", 257 | icon_url=f"https://s2.coinmarketcap.com/static/img/coins/64x64/{crypto['id']}.png", 258 | ) 259 | embed.description = textwrap.dedent( 260 | f""" 261 | ```diff 262 | Bal: {cryptobal[symbol]['total']} 263 | 264 | Percent Gain/Loss: 265 | {"" if change < 0 else "+"}{change:.2f}% 266 | 267 | Price: 268 | ${crypto['price']:,.2f} 269 | 270 | 24h Change: 271 | {sign}{crypto['change_24h']}% 272 | ``` 273 | """ 274 | ) 275 | embed.set_image( 276 | url=f"https://s3.coinmarketcap.com/generated/sparklines/web/1d/usd/{crypto['id']}.png" 277 | ) 278 | 279 | await ctx.send(embed=embed) 280 | 281 | @crypto.command() 282 | async def list(self, ctx): 283 | """Shows the prices of crypto with pagination.""" 284 | messages = [] 285 | cryptos = "" 286 | for i, (crypto, price) in enumerate(self.DB.crypto, start=1): 287 | price = orjson.loads(price)["price"] 288 | 289 | if not i % 3: 290 | cryptos += f"{crypto.decode():}: ${float(price):.2f}\n" 291 | else: 292 | cryptos += f"{crypto.decode():}: ${float(price):.2f}\t".expandtabs() 293 | 294 | if not i % 99: 295 | messages.append(discord.Embed(description=f"```prolog\n{cryptos}```")) 296 | cryptos = "" 297 | 298 | if i % 99: 299 | messages.append(discord.Embed(description=f"```prolog\n{cryptos}```")) 300 | 301 | paginator = pages.Paginator(pages=messages) 302 | await paginator.send(ctx) 303 | 304 | @crypto.command(aliases=["h"]) 305 | async def history(self, ctx, member: discord.Member = None, amount=10): 306 | """Gets a members crypto transaction history. 307 | 308 | member: discord.Member 309 | amount: int 310 | How many transactions to get 311 | """ 312 | member = member or ctx.author 313 | 314 | embed = discord.Embed(color=discord.Color.blurple()) 315 | cryptobal = self.DB.get_cryptobal(str(member.id).encode()) 316 | 317 | if not cryptobal: 318 | embed.description = "```You haven't invested.```" 319 | return await ctx.send(embed=embed) 320 | 321 | msg = "" 322 | 323 | for crypto_name, crypto_data in cryptobal.items(): 324 | msg += f"{crypto_name}:\n" 325 | for trade in crypto_data["history"]: 326 | if trade[0] < 0: 327 | kind = "Sold" 328 | else: 329 | kind = "Bought" 330 | msg += f"{kind} {abs(trade[0]):.2f} for ${trade[1]:.2f}\n" 331 | msg += "\n" 332 | 333 | embed.description = f"```{msg}```" 334 | await ctx.send(embed=embed) 335 | 336 | 337 | def setup(bot: commands.Bot) -> None: 338 | """Starts crypto cog.""" 339 | bot.add_cog(crypto(bot)) 340 | -------------------------------------------------------------------------------- /cogs/economy.py: -------------------------------------------------------------------------------- 1 | import random 2 | from decimal import Decimal 3 | 4 | import discord 5 | import orjson 6 | from discord.ext import commands 7 | 8 | 9 | class Card: 10 | def __init__(self, suit, name, value): 11 | self.suit = suit 12 | self.name = name 13 | self.value = value 14 | 15 | 16 | class Deck: 17 | def __init__(self): 18 | suits = { 19 | "Spades": "\u2664", 20 | "Hearts": "\u2661", 21 | "Clubs": "\u2667", 22 | "Diamonds": "\u2662", 23 | } 24 | 25 | cards = { 26 | "A": 11, 27 | "2": 2, 28 | "3": 3, 29 | "4": 4, 30 | "5": 5, 31 | "6": 6, 32 | "7": 7, 33 | "8": 8, 34 | "9": 9, 35 | "10": 10, 36 | "J": 10, 37 | "Q": 10, 38 | "K": 10, 39 | } 40 | 41 | self.items = [] 42 | for suit in suits: 43 | for card, value in cards.items(): 44 | self.items.append(Card(suits[suit], card, value)) 45 | random.shuffle(self.items) 46 | 47 | self.member = [self.items.pop(), self.items.pop()] 48 | self.dealer = [self.items.pop(), self.items.pop()] 49 | 50 | @staticmethod 51 | def score(cards): 52 | score = sum(card.value for card in cards) 53 | if score > 21: 54 | for card in cards: 55 | if card.name == "A": 56 | score -= 10 57 | if score < 21: 58 | return score 59 | return score 60 | 61 | def is_win(self): 62 | if (m_score := self.score(self.member)) > 21: 63 | return False 64 | 65 | while (score := self.score(self.dealer)) < 16 or score < m_score: 66 | self.dealer.append(self.items.pop()) 67 | 68 | if score > 21 or m_score > score: 69 | return True 70 | if score == m_score: 71 | return None 72 | return False 73 | 74 | def get_embed(self, bet, hidden=True, win=False): 75 | embed = discord.Embed(color=discord.Color.blurple()) 76 | 77 | if hidden: 78 | embed.title = f"Blackjack game (${bet:,.2f})" 79 | elif win is None: 80 | embed.title = f"You tied! (${bet:,.2f})" 81 | elif win: 82 | embed.title = f"You won! (${bet:,.2f})" 83 | else: 84 | embed.title = f"You lost! (${bet:,.2f})" 85 | 86 | embed.description = """ 87 | **Your Hand: {}** 88 | {} 89 | **Dealers Hand: {}** 90 | {} 91 | """.format( 92 | self.score(self.member), 93 | " ".join([f"`{c.name}{c.suit}`" for c in self.member]), 94 | self.score(self.dealer) if not hidden else "", 95 | " ".join([f"`{c.name}{c.suit}`" for c in self.dealer]) 96 | if not hidden 97 | else f"`{self.dealer[0].name}{self.dealer[0].suit}` `##`", 98 | ) 99 | return embed 100 | 101 | 102 | class BlackJack(discord.ui.View): 103 | def __init__(self, db, user: discord.User, bet): 104 | super().__init__(timeout=1200.0) 105 | self.user = user 106 | self.DB = db 107 | 108 | self.bet = bet 109 | self.deck = Deck() 110 | 111 | self.user_key = str(user.id).encode() 112 | 113 | if self.deck.score(self.deck.member) == 21: 114 | if self.deck.score(self.deck.dealer) != 21: 115 | bal = self.DB.get_bal(self.user_key) + bet 116 | self.DB.put_bal(self.user_key, bal) 117 | 118 | for child in self.children: 119 | child.disabled = True 120 | 121 | self.stop() 122 | else: 123 | bal = self.DB.get_bal(self.user_key) - bet 124 | self.DB.put_bal(self.user_key, bal) 125 | 126 | @discord.ui.button(label="🇭", style=discord.ButtonStyle.blurple) 127 | async def hit(self, button, interaction): 128 | if interaction.user == self.user: 129 | self.deck.member.append(self.deck.items.pop()) 130 | 131 | if self.deck.score(self.deck.member) >= 21: 132 | is_win = self.deck.is_win() 133 | 134 | if is_win is True: 135 | bal = self.DB.get_bal(self.user_key) + self.bet * 2 136 | 137 | self.DB.put_bal(self.user_key, bal) 138 | 139 | for child in self.children: 140 | child.disabled = True 141 | 142 | return await interaction.response.edit_message( 143 | view=self, embed=self.get_embed(False, is_win) 144 | ) 145 | 146 | await interaction.response.edit_message(view=self, embed=self.get_embed()) 147 | 148 | @discord.ui.button(label="🇸", style=discord.ButtonStyle.blurple) 149 | async def stand(self, button, interaction): 150 | if interaction.user == self.user: 151 | is_win = self.deck.is_win() 152 | 153 | if is_win is True: 154 | bal = self.DB.get_bal(self.user_key) + self.bet * 2 155 | 156 | self.DB.put_bal(self.user_key, bal) 157 | 158 | for child in self.children: 159 | child.disabled = True 160 | 161 | return await interaction.response.edit_message( 162 | view=self, embed=self.get_embed(False, is_win) 163 | ) 164 | 165 | def get_embed(self, hidden=True, is_win=False): 166 | return self.deck.get_embed( 167 | self.bet, False if self.children[0].disabled else hidden, is_win 168 | ) 169 | 170 | 171 | class economy(commands.Cog): 172 | """Commands related to the economy.""" 173 | 174 | def __init__(self, bot: commands.Bot) -> None: 175 | self.bot = bot 176 | self.DB = bot.DB 177 | 178 | @staticmethod 179 | def get_amount(bal, bet): 180 | try: 181 | if bet[-1] == "%": 182 | return bal * (Decimal(bet[:-1]) / 100) 183 | return Decimal(bet.replace(",", "")) 184 | except ValueError: 185 | return None 186 | 187 | @commands.command(aliases=["bj"]) 188 | async def blackjack(self, ctx, bet="0"): 189 | """Starts a game of blackjack. 190 | 191 | bet: float 192 | """ 193 | embed = discord.Embed(color=discord.Color.blurple()) 194 | 195 | member = str(ctx.author.id).encode() 196 | bal = self.DB.get_bal(member) 197 | bet = self.get_amount(bal, bet) 198 | 199 | if bet is None: 200 | embed.description = f"```Invalid bet. e.g {ctx.prefix}blackjack 1000```" 201 | return await ctx.send(embed=embed) 202 | 203 | if bet < 0: 204 | embed.title = "Bet must be positive" 205 | return await ctx.send(embed=embed) 206 | 207 | if bal < bet: 208 | embed.title = "You don't have enough cash" 209 | return await ctx.send(embed=embed) 210 | 211 | blackjack = BlackJack(self.DB, ctx.author, bet) 212 | 213 | await ctx.send(embed=blackjack.get_embed(), view=blackjack) 214 | 215 | @commands.command(aliases=["coinf", "cf"]) 216 | async def coinflip(self, ctx, choice="h", bet="0"): 217 | """Flips a coin. 218 | 219 | choice: str 220 | bet: int 221 | """ 222 | embed = discord.Embed(color=discord.Color.red()) 223 | choice = choice[0].lower() 224 | if choice not in ("h", "t"): 225 | embed.title = "Must be [h]eads or [t]ails" 226 | return await ctx.send(embed=embed) 227 | 228 | member = str(ctx.author.id).encode() 229 | bal = self.DB.get_bal(member) 230 | bet = self.get_amount(bal, bet) 231 | 232 | if bet is None: 233 | embed.description = f"```Invalid bet. e.g {ctx.prefix}coinflip 1000```" 234 | return await ctx.send(embed=embed) 235 | 236 | if bet < 0: 237 | embed.title = "Bet must be positive" 238 | return await ctx.send(embed=embed) 239 | 240 | if bal <= 1: 241 | bal += 1 242 | 243 | if bal < bet: 244 | embed.title = "You don't have enough cash" 245 | return await ctx.send(embed=embed) 246 | 247 | images = { 248 | "heads": "https://i.imgur.com/168G0Cr.jpg", 249 | "tails": "https://i.imgur.com/EdBBcsz.jpg", 250 | } 251 | 252 | result = random.choice(["heads", "tails"]) 253 | 254 | embed.set_author(name=result.capitalize(), icon_url=images[result]) 255 | 256 | if choice == result[0]: 257 | embed.color = discord.Color.blurple() 258 | embed.description = f"You won ${bet:,.2f}" 259 | bal += bet 260 | else: 261 | embed.description = f"You lost ${bet:,.2f}" 262 | bal -= bet 263 | 264 | self.DB.put_bal(member, bal) 265 | 266 | embed.set_footer(text=f"Balance: ${bal:,.2f}") 267 | await ctx.send(embed=embed) 268 | 269 | @commands.command() 270 | async def lottery(self, ctx, bet="0"): 271 | """Lottery with a 1/99 chance of winning 99 times the bet. 272 | 273 | bet: float 274 | The amount of money you are betting. 275 | """ 276 | embed = discord.Embed(color=discord.Color.blurple()) 277 | 278 | member = str(ctx.author.id).encode() 279 | bal = self.DB.get_bal(member) 280 | bet = self.get_amount(bal, bet) 281 | 282 | if bet is None: 283 | embed.description = f"```Invalid bet. e.g {ctx.prefix}lottery 1000```" 284 | return await ctx.send(embed=embed) 285 | 286 | if bet <= 0: 287 | embed.title = "Bet must be positive" 288 | return await ctx.send(embed=embed) 289 | 290 | if bal < bet: 291 | embed.title = "You don't have enough cash" 292 | return await ctx.send(embed=embed) 293 | 294 | if random.randint(1, 100) == 50: 295 | bal += bet * 99 296 | self.DB.put_bal(member, bal) 297 | embed.title = f"You won ${bet * 99:,.2f}" 298 | embed.set_footer(text=f"Balance: ${bal:,.2f}") 299 | return await ctx.send(embed=embed) 300 | 301 | self.DB.put_bal(member, bal - bet) 302 | embed.title = f"You lost ${bet:,.2f}" 303 | embed.set_footer(text=f"Balance: ${bal - bet:,.2f}") 304 | embed.color = discord.Color.red() 305 | await ctx.send(embed=embed) 306 | 307 | async def streak_update(self, member, result): 308 | data = self.DB.wins.get(member) 309 | 310 | if not data: 311 | data = { 312 | "currentwin": 0, 313 | "currentlose": 0, 314 | "highestwin": 0, 315 | "highestlose": 0, 316 | "totallose": 0, 317 | "totalwin": 0, 318 | } 319 | else: 320 | data = orjson.loads(data.decode()) 321 | 322 | if result == "won": 323 | data["highestlose"] = max(data["highestlose"], data["currentlose"]) 324 | data["totalwin"] += 1 325 | data["currentwin"] += 1 326 | data["currentlose"] = 0 327 | else: 328 | data["highestwin"] = max(data["highestwin"], data["currentwin"]) 329 | data["totallose"] += 1 330 | data["currentlose"] += 1 331 | data["currentwin"] = 0 332 | self.DB.wins.put(member, orjson.dumps(data)) 333 | 334 | @commands.command(aliases=["slots"]) 335 | async def slot(self, ctx, bet="0", silent: bool = False): 336 | """Rolls the slot machine. 337 | 338 | bet: str 339 | The amount of money you are betting. 340 | silent: bool 341 | If the final message should be sent 342 | """ 343 | embed = discord.Embed(color=discord.Color.red()) 344 | 345 | member = str(ctx.author.id).encode() 346 | bal = self.DB.get_bal(member) 347 | bet = self.get_amount(bal, bet) 348 | 349 | if bet is None: 350 | embed.description = f"```Invalid bet. e.g {ctx.prefix}slot 1000```" 351 | return await ctx.send(embed=embed) 352 | 353 | if bet < 0: 354 | embed.title = "Bet must be positive" 355 | return await ctx.send(embed=embed) 356 | 357 | if bal <= 1: 358 | bal += 1 359 | 360 | if bal < bet: 361 | embed.title = "You don't have enough cash" 362 | return await ctx.send(embed=embed) 363 | 364 | emojis = ( 365 | ":apple:", 366 | ":tangerine:", 367 | ":pear:", 368 | ":lemon:", 369 | ":watermelon:", 370 | ":grapes:", 371 | ":strawberry:", 372 | ":cherries:", 373 | ":kiwi:", 374 | ":pineapple:", 375 | ":coconut:", 376 | ":peach:", 377 | ":mango:", 378 | ) 379 | 380 | a, b, c, d = random.choices(emojis, k=4) 381 | 382 | result = "won" 383 | embed.color = discord.Color.blurple() 384 | if a == b == c == d: 385 | winnings = 100 386 | elif (a == b == c) or (a == c == d) or (a == b == d) or (b == c == d): 387 | winnings = 10 388 | elif (a == b) and (d == c) or (b == c) and (d == a) or (d == b) and (a == c): 389 | winnings = 10 390 | elif (a == b) or (a == c) or (b == c) or (d == c) or (d == b) or (d == a): 391 | winnings = 1 392 | else: 393 | winnings = -1 394 | result = "lost" 395 | embed.color = discord.Color.red() 396 | 397 | bal += bet * winnings 398 | self.DB.put_bal(member, bal) 399 | 400 | if not silent: 401 | embed.title = f"[ {a} {b} {c} {d} ]" 402 | embed.description = f"You {result} ${bet*(abs(winnings)):,.2f}" 403 | embed.set_footer(text=f"Balance: ${bal:,.2f}") 404 | 405 | await ctx.reply(embed=embed, mention_author=False) 406 | 407 | await self.streak_update(member, result) 408 | 409 | @commands.command(aliases=["streaks"]) 410 | async def streak(self, ctx, user: discord.User = None): 411 | """Gets a users streaks on the slot machine. 412 | 413 | user: discord.User 414 | The user to get streaks of defaults to the command author.""" 415 | if user: 416 | user = str(user.id).encode() 417 | else: 418 | user = str(ctx.author.id).encode() 419 | 420 | wins = self.DB.wins.get(user) 421 | 422 | if not wins: 423 | return 424 | 425 | wins = orjson.loads(wins.decode()) 426 | 427 | embed = discord.Embed(color=discord.Color.blurple()) 428 | embed.add_field( 429 | name="**Wins/Losses**", 430 | value=f""" 431 | **Total Wins:** {wins["totalwin"]} 432 | **Total Losses:** {wins["totallose"]} 433 | **Current Wins:** {wins["currentwin"]} 434 | **Current Losses:** {wins["currentlose"]} 435 | **Highest Win Streak:** {wins["highestwin"]} 436 | **Highest Loss Streak:** {wins["highestlose"]} 437 | """, 438 | ) 439 | await ctx.send(embed=embed) 440 | 441 | @commands.command() 442 | async def chances(self, ctx): 443 | """Sends pre simulated chances based off one hundred billion runs of the slot command.""" 444 | await ctx.send( 445 | embed=discord.Embed(color=discord.Color.blurple()) 446 | .add_field(name="Quad:", value="0.0455%") 447 | .add_field(name="Triple:", value="2.1848%") 448 | .add_field(name="Double Double:", value="1.6386%") 449 | .add_field(name="Double:", value="36.0491%") 450 | .add_field(name="None:", value="60.082%") 451 | .add_field(name="Percentage gain/loss:", value="18.7531%") 452 | .set_footer( 453 | text="Based off one hundred billion simulated runs of the slot command" 454 | ) 455 | ) 456 | 457 | @commands.command(aliases=["bal"]) 458 | async def balance(self, ctx, user: discord.User = None): 459 | """Gets a members balance. 460 | 461 | user: discord.User 462 | The user whose balance will be returned. 463 | """ 464 | user = user or ctx.author 465 | 466 | user_id = str(user.id).encode() 467 | bal = self.DB.get_bal(user_id) 468 | 469 | embed = discord.Embed(color=discord.Color.blurple()) 470 | embed.add_field(name=f"{user.display_name}'s balance", value=f"${bal:,.2f}") 471 | 472 | await ctx.send(embed=embed) 473 | 474 | @commands.command() 475 | async def baltop(self, ctx, amount: int = 10): 476 | """Gets members with the highest balances. 477 | 478 | amount: int 479 | The amount of balances to get defaulting to 10. 480 | """ 481 | baltop = [] 482 | for member, bal in self.DB.bal: 483 | member = self.bot.get_user(int(member)) 484 | if member: 485 | baltop.append((float(bal), member.display_name)) 486 | 487 | baltop = sorted(baltop, reverse=True)[:amount] 488 | 489 | embed = discord.Embed( 490 | color=discord.Color.blurple(), 491 | title=f"Top {len(baltop)} Balances", 492 | description="\n".join( 493 | [f"**{member}:** ${bal:,.2f}" for bal, member in baltop] 494 | ), 495 | ) 496 | await ctx.send(embed=embed) 497 | 498 | @commands.command(aliases=["net"]) 499 | async def networth(self, ctx, member: discord.Member = None): 500 | """Gets a members net worth. 501 | 502 | members: discord.Member 503 | The member whose net worth will be returned. 504 | """ 505 | member = member or ctx.author 506 | 507 | member_id = str(member.id).encode() 508 | bal = self.DB.get_bal(member_id) 509 | 510 | embed = discord.Embed(color=discord.Color.blurple()) 511 | 512 | def get_value(values, db): 513 | if values: 514 | return Decimal( 515 | sum( 516 | [ 517 | stock[1]["total"] 518 | * float(orjson.loads(db.get(stock[0].encode()))["price"]) 519 | for stock in values.items() 520 | ] 521 | ) 522 | ) 523 | 524 | return 0 525 | 526 | stock_value = get_value(self.DB.get_stockbal(member_id), self.DB.stocks) 527 | crypto_value = get_value(self.DB.get_cryptobal(member_id), self.DB.crypto) 528 | 529 | embed.add_field( 530 | name=f"{member.display_name}'s net worth", 531 | value=f"${bal + stock_value + crypto_value:,.2f}", 532 | ) 533 | 534 | embed.set_footer( 535 | text="Crypto: ${:,.2f}\nStocks: ${:,.2f}\nBalance: ${:,.2f}".format( 536 | crypto_value, stock_value, bal 537 | ) 538 | ) 539 | 540 | await ctx.send(embed=embed) 541 | 542 | @commands.command() 543 | async def nettop(self, ctx, amount: int = 10): 544 | """Gets members with the highest net worth 545 | 546 | amount: int 547 | The amount of members to get 548 | """ 549 | 550 | def get_value(values, db): 551 | if values: 552 | return sum( 553 | [ 554 | stock[1]["total"] 555 | * float(orjson.loads(db.get(stock[0].encode()))["price"]) 556 | for stock in values.items() 557 | ] 558 | ) 559 | 560 | return 0 561 | 562 | net_top = [] 563 | 564 | for member_id, value in self.DB.bal: 565 | stock_value = get_value(self.DB.get_stockbal(member_id), self.DB.stocks) 566 | crypto_value = get_value(self.DB.get_cryptobal(member_id), self.DB.crypto) 567 | # fmt: off 568 | if (member := self.bot.get_user(int(member_id))): 569 | net_top.append( 570 | (float(value) + stock_value + crypto_value, member.display_name) 571 | ) 572 | # fmt: on 573 | 574 | net_top = sorted(net_top, reverse=True)[:amount] 575 | embed = discord.Embed(color=discord.Color.blurple()) 576 | 577 | embed.title = f"Top {len(net_top)} Richest Members" 578 | embed.description = "\n".join( 579 | [f"**{member}:** ${bal:,.2f}" for bal, member in net_top] 580 | ) 581 | await ctx.send(embed=embed) 582 | 583 | @commands.command(aliases=["give", "donate"]) 584 | async def pay(self, ctx, user: discord.User, amount): 585 | """Pays a user from your balance. 586 | 587 | user: discord.User 588 | The member you are paying. 589 | amount: str 590 | The amount you are paying. 591 | """ 592 | embed = discord.Embed(color=discord.Color.blurple()) 593 | 594 | if ctx.author == user: 595 | embed.description = "```You can't pay yourself.```" 596 | return await ctx.send(embed=embed) 597 | 598 | sender = str(ctx.author.id).encode() 599 | sender_bal = self.DB.get_bal(sender) 600 | 601 | amount = self.get_amount(sender_bal, amount) 602 | 603 | if amount < 0: 604 | embed.title = "You cannot pay a negative amount" 605 | return await ctx.send(embed=embed) 606 | 607 | if sender_bal < amount: 608 | embed.title = "You don't have enough cash" 609 | return await ctx.send(embed=embed) 610 | 611 | self.DB.add_bal(str(user.id).encode(), amount) 612 | sender_bal -= Decimal(amount) 613 | self.DB.put_bal(sender, sender_bal) 614 | 615 | embed.title = f"Sent ${amount:,.2f} to {user.display_name}" 616 | embed.set_footer(text=f"New Balance: ${sender_bal:,.2f}") 617 | 618 | await ctx.send(embed=embed) 619 | 620 | @commands.command() 621 | @commands.cooldown(1, 21600, commands.BucketType.user) 622 | async def salary(self, ctx): 623 | """Gives you a salary of 1000 on a 6 hour cooldown.""" 624 | member = str(ctx.author.id).encode() 625 | bal = self.DB.add_bal(member, 1000) 626 | 627 | embed = discord.Embed( 628 | title=f"Paid {ctx.author.display_name} $1000", color=discord.Color.blurple() 629 | ) 630 | embed.set_footer(text=f"Balance: ${bal:,.2f}") 631 | 632 | await ctx.send(embed=embed) 633 | 634 | 635 | def setup(bot: commands.Bot) -> None: 636 | """Starts economy cog.""" 637 | bot.add_cog(economy(bot)) 638 | -------------------------------------------------------------------------------- /cogs/help.py: -------------------------------------------------------------------------------- 1 | import difflib 2 | from itertools import islice 3 | 4 | import discord 5 | from discord.ext import commands, pages 6 | 7 | 8 | def chunks(items, split): 9 | for i in range(0, len(items), split): 10 | chunk = islice(items, i, i + split) 11 | if chunk: 12 | yield chunk 13 | 14 | 15 | class PaginatedHelpCommand(commands.HelpCommand): 16 | def __init__(self): 17 | super().__init__( 18 | command_attrs={ 19 | "cooldown": commands.CooldownMapping( 20 | commands.Cooldown(1, 5.0), commands.BucketType.member 21 | ), 22 | "help": "Shows help about the bot, a command, or a category", 23 | "hidden": True, 24 | } 25 | ) 26 | 27 | @staticmethod 28 | def format_commands(cog, commands): 29 | if cog.description: 30 | short_doc = cog.description.split("\n", 1)[0] + "\n" 31 | else: 32 | short_doc = "No help found...\n" 33 | 34 | current_count = len(short_doc) 35 | ending_note = "+%d not shown" 36 | ending_length = len(ending_note) 37 | 38 | page = [] 39 | for command in commands: 40 | parent = getattr(command, "parent", None) 41 | value = f"`{parent.name + ' ' if parent else ''}{command.name}`" 42 | count = len(value) + 1 # The space 43 | if count + current_count < 800: 44 | current_count += count 45 | page.append(value) 46 | else: 47 | if current_count + ending_length + 1 > 800: 48 | page.pop() 49 | break 50 | 51 | if len(page) == len(commands): 52 | return short_doc + " ".join(page) 53 | 54 | hidden = len(commands) - len(page) 55 | return short_doc + " ".join(page) + "\n" + (ending_note % hidden) 56 | 57 | async def format_cogs(self, cogs): 58 | prefix = self.context.prefix 59 | description = ( 60 | f'Use "{prefix}help command" for more info on a command.\n' 61 | f'Use "{prefix}help category" for more info on a category.\n' 62 | ) 63 | 64 | embed = discord.Embed( 65 | title="Categories", description=description, colour=discord.Colour.blurple() 66 | ) 67 | 68 | for i, (cog, items) in enumerate(cogs): 69 | value = self.format_commands(cog, items) 70 | embed.add_field(name=cog.qualified_name, value=value) 71 | 72 | if not i % 2: 73 | embed.add_field(name="\u200b", value="\u200b") 74 | return embed 75 | 76 | def format_group(self, title, description, commands): 77 | embed = discord.Embed( 78 | title=title, 79 | description=description, 80 | colour=discord.Colour.blurple(), 81 | ) 82 | 83 | for command in commands: 84 | signature = f"{command.qualified_name} {command.signature}" 85 | embed.add_field( 86 | name=signature, 87 | value=f"```{command.short_doc}```" 88 | if command.short_doc 89 | else "```No help given...```", 90 | inline=False, 91 | ) 92 | 93 | embed.set_footer( 94 | text=f'Use "{self.context.prefix}help command" for more info on a command.' 95 | ) 96 | return embed 97 | 98 | def command_not_found(self, command): 99 | all_commands = [ 100 | str(command) 101 | for command in self.context.bot.walk_commands() 102 | if not command.hidden 103 | ] 104 | matches = difflib.get_close_matches(command, all_commands, cutoff=0) 105 | 106 | return discord.Embed( 107 | color=discord.Color.dark_red(), 108 | title=f"Command {command} not found.", 109 | description="```Did you mean:\n\n{}```".format("\n".join(matches)), 110 | ) 111 | 112 | async def send_error_message(self, error): 113 | if isinstance(error, discord.Embed): 114 | await self.context.channel.send(embed=error) 115 | else: 116 | await self.context.channel.send(error) 117 | 118 | @staticmethod 119 | def get_command_signature(command): 120 | parent = command.full_parent_name 121 | if len(command.aliases) > 0: 122 | aliases = "|".join(command.aliases) 123 | fmt = f"[{command.name}|{aliases}]" 124 | if parent: 125 | fmt = f"{parent} {fmt}" 126 | alias = fmt 127 | else: 128 | alias = command.name if not parent else f"{parent} {command.name}" 129 | return f"{alias} {command.signature}" 130 | 131 | async def send_bot_help(self, mapping): 132 | embeds = [] 133 | 134 | mapping.pop(None) # Why is there just None in the mapping? 135 | 136 | for cog in [*mapping.keys()]: 137 | commands = await self.filter_commands(mapping[cog], sort=True) 138 | if not commands: 139 | mapping.pop(cog) 140 | else: 141 | mapping[cog] = commands 142 | 143 | for chunk in chunks(mapping.items(), 4): 144 | embeds.append(await self.format_cogs(chunk)) 145 | 146 | paginator = pages.Paginator(pages=embeds) 147 | await paginator.send(self.context) 148 | 149 | async def send_cog_help(self, cog): 150 | entries = await self.filter_commands(cog.get_commands(), sort=True) 151 | 152 | title = f"{cog.qualified_name} Commands" 153 | embeds = [] 154 | 155 | for chunk in chunks(entries, 6): 156 | embeds.append(self.format_group(title, cog.description, chunk)) 157 | 158 | paginator = pages.Paginator(pages=embeds) 159 | await paginator.send(self.context) 160 | 161 | def common_command_formatting(self, embed_like, command): 162 | embed_like.title = f"{self.context.prefix}{self.get_command_signature(command)}" 163 | if command.description: 164 | embed_like.description = f"```{command.description}\n\n{command.help}```" 165 | else: 166 | embed_like.description = ( 167 | f"```{command.help}```" if command.help else "```No help found...```" 168 | ) 169 | 170 | async def send_command_help(self, command): 171 | embed = discord.Embed(colour=discord.Colour.blurple()) 172 | self.common_command_formatting(embed, command) 173 | await self.context.send(embed=embed) 174 | 175 | async def send_group_help(self, group): 176 | subcommands = group.commands 177 | if len(subcommands) == 0: 178 | return await self.send_command_help(group) 179 | 180 | entries = await self.filter_commands(subcommands, sort=True) 181 | if len(entries) == 0: 182 | return await self.send_command_help(group) 183 | 184 | title = f"{group.qualified_name} Commands" 185 | embeds = [] 186 | 187 | for chunk in chunks(entries, 6): 188 | embeds.append(self.format_group(title, group.description, chunk)) 189 | 190 | paginator = pages.Paginator(pages=embeds) 191 | await paginator.send(self.context) 192 | 193 | 194 | class _help(commands.Cog, name="help"): 195 | """For the help command.""" 196 | 197 | def __init__(self, bot: commands.Bot) -> None: 198 | self.bot = bot 199 | self.old_help_command = bot.help_command 200 | bot.help_command = PaginatedHelpCommand() 201 | bot.help_command.cog = self 202 | 203 | def cog_unload(self): 204 | self.bot.help_command = self.old_help_command 205 | 206 | 207 | def setup(bot: commands.Bot) -> None: 208 | """Starts help cog.""" 209 | bot.add_cog(_help(bot)) 210 | -------------------------------------------------------------------------------- /cogs/images.py: -------------------------------------------------------------------------------- 1 | import base64 2 | from io import BytesIO 3 | 4 | import discord 5 | from discord.ext import commands 6 | 7 | 8 | class images(commands.Cog): 9 | """Image manipulation commands.""" 10 | 11 | def __init__(self, bot: commands.Bot) -> None: 12 | self.bot = bot 13 | 14 | async def process_url(self, ctx, url): 15 | if not url: 16 | if ctx.message.attachments: 17 | return ctx.message.attachments[0].url 18 | 19 | if ctx.message.reference and (message := ctx.message.reference.resolved): 20 | if message.attachments: 21 | return message.attachments[0].url 22 | 23 | if message.embeds: 24 | return message.embeds[0].url 25 | 26 | return ctx.author.display_avatar.url 27 | 28 | try: 29 | user = await commands.UserConverter().convert(ctx, url) 30 | return user.display_avatar.url 31 | except commands.UserNotFound: 32 | return url 33 | 34 | async def dagpi(self, ctx, method, image_url): 35 | image_url = await self.process_url(ctx, image_url) 36 | 37 | url = "https://dagpi.xyz/api/routes/dagpi-manip" 38 | data = { 39 | "method": method, 40 | "token": "", 41 | "url": image_url, 42 | } 43 | headers = { 44 | "content-type": "text/plain;charset=UTF-8", 45 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.67 Safari/537.36", 46 | } 47 | 48 | async with ctx.typing(), self.bot.client_session.post( 49 | url, json=data, headers=headers, timeout=30 50 | ) as resp: 51 | if resp.content_type == "text/plain": 52 | return await ctx.reply( 53 | embed=discord.Embed( 54 | color=discord.Color.blurple(), 55 | description=f"```ml\n{(await resp.text())}```", 56 | ) 57 | ) 58 | resp = await resp.json() 59 | 60 | if "response" in resp: 61 | return await ctx.reply( 62 | embed=discord.Embed( 63 | color=discord.Color.blurple(), 64 | description=f"```\n{resp['response']}```", 65 | ) 66 | ) 67 | 68 | with BytesIO(base64.b64decode(resp["image"][22:])) as image: 69 | filename = f"image.{resp['format']}" 70 | await ctx.reply(file=discord.File(fp=image, filename=filename)) 71 | 72 | async def jeyy(self, ctx, endpoint, url): 73 | url = await self.process_url(ctx, url) 74 | 75 | api_url = f"https://api.jeyy.xyz/image/{endpoint}?image_url={url}" 76 | 77 | async with ctx.typing(), self.bot.client_session.get( 78 | api_url, timeout=30 79 | ) as resp: 80 | if resp.status != 200: 81 | return await ctx.reply( 82 | embed=discord.Embed( 83 | color=discord.Color.blurple(), 84 | description="```Couldn't process image```", 85 | ).set_footer(text=f"Status code was {resp.status}") 86 | ) 87 | image = BytesIO() 88 | 89 | async for chunk in resp.content.iter_chunked(8 * 1024): 90 | image.write(chunk) 91 | 92 | image.seek(0) 93 | await ctx.reply(file=discord.File(fp=image, filename=f"{endpoint}.gif")) 94 | 95 | @commands.command() 96 | async def deepfry(self, ctx, url: str = None): 97 | """Deepfrys an image. 98 | 99 | url: str 100 | """ 101 | await self.dagpi(ctx, "deepfry", url) 102 | 103 | @commands.command() 104 | async def pixelate(self, ctx, url: str = None): 105 | """Pixelates an image. 106 | 107 | url: str 108 | """ 109 | await self.dagpi(ctx, "pixel", url) 110 | 111 | @commands.command(name="ascii") 112 | async def _ascii(self, ctx, url: str = None): 113 | """Turns an image into ascii text. 114 | 115 | url: str 116 | """ 117 | await self.dagpi(ctx, "ascii", url) 118 | 119 | @commands.command() 120 | async def sketch(self, ctx, url: str = None): 121 | """Make a gif of sketching the image. 122 | 123 | url: str 124 | """ 125 | await self.dagpi(ctx, "sketch", url) 126 | 127 | @commands.command() 128 | async def sobel(self, ctx, url: str = None): 129 | """Uses the Sobel operator on an image. 130 | 131 | url: str 132 | """ 133 | await self.dagpi(ctx, "sobel", url) 134 | 135 | @commands.command() 136 | async def magik(self, ctx, url: str = None): 137 | """Does magik on an image. 138 | 139 | url: str 140 | """ 141 | await self.dagpi(ctx, "magik", url) 142 | 143 | @commands.command() 144 | async def colors(self, ctx, url: str = None): 145 | """Shows the colors present in the image. 146 | 147 | url: str 148 | """ 149 | await self.dagpi(ctx, "colors", url) 150 | 151 | @commands.command() 152 | async def invert(self, ctx, url: str = None): 153 | """Inverts the colors of an image. 154 | 155 | url: str 156 | """ 157 | await self.dagpi(ctx, "invert", url) 158 | 159 | @commands.command() 160 | async def mirror(self, ctx, url: str = None): 161 | """Mirror an image on the y axis. 162 | 163 | url: str 164 | """ 165 | await self.dagpi(ctx, "mirror", url) 166 | 167 | @commands.command() 168 | async def lego(self, ctx, url: str = None): 169 | """Makes an image look like it is made out of lego. 170 | 171 | url: str 172 | """ 173 | await self.dagpi(ctx, "lego", url) 174 | 175 | @commands.command() 176 | async def flip(self, ctx, url: str = None): 177 | """Flips an image upsidedown. 178 | 179 | url: str 180 | """ 181 | await self.dagpi(ctx, "flip", url) 182 | 183 | @commands.command() 184 | async def mosaic(self, ctx, url: str = None): 185 | """Makes an image look like an roman mosaic. 186 | 187 | url: str 188 | """ 189 | await self.dagpi(ctx, "mosiac", url) 190 | 191 | @commands.command() 192 | async def rgb(self, ctx, url: str = None): 193 | """Get an RGB graph of an image's colors. 194 | 195 | url: str 196 | """ 197 | await self.dagpi(ctx, "rgb", url) 198 | 199 | @commands.command() 200 | async def paint(self, ctx, url: str = None): 201 | """Makes an image look like a painting. 202 | 203 | url: str 204 | """ 205 | await self.dagpi(ctx, "paint", url) 206 | 207 | @commands.command() 208 | async def grayscale(self, ctx, url: str = None): 209 | """Grayscales an image. 210 | 211 | url: str 212 | """ 213 | await self.dagpi(ctx, "comic", url) 214 | 215 | @commands.command() 216 | async def cow(self, ctx, url: str = None): 217 | """Projects an image onto a cow. 218 | 219 | url: str 220 | """ 221 | await self.jeyy(ctx, "cow", url) 222 | 223 | @commands.command() 224 | async def balls(self, ctx, url: str = None): 225 | """Turns an image into balls that are dropped. 226 | 227 | url: str 228 | """ 229 | await self.jeyy(ctx, "balls", url) 230 | 231 | @commands.command() 232 | async def glitch(self, ctx, url: str = None): 233 | """Adds glitches to an image as a gif. 234 | 235 | url: str 236 | """ 237 | await self.jeyy(ctx, "glitch", url) 238 | 239 | @commands.command() 240 | async def cartoon(self, ctx, url: str = None): 241 | """Makes an image look like a cartoon image. 242 | 243 | url: str 244 | """ 245 | await self.jeyy(ctx, "cartoon", url) 246 | 247 | @commands.command() 248 | async def canny(self, ctx, url: str = None): 249 | """Canny edge detection on an image. 250 | 251 | url: str 252 | """ 253 | await self.jeyy(ctx, "canny", url) 254 | 255 | @commands.command() 256 | async def warp(self, ctx, url: str = None): 257 | """Warps an image. 258 | 259 | url: str 260 | """ 261 | await self.jeyy(ctx, "warp", url) 262 | 263 | @commands.command() 264 | async def earthquake(self, ctx, url: str = None): 265 | """Shakes an image like an earthquake. 266 | 267 | url: str 268 | """ 269 | await self.jeyy(ctx, "earthquake", url) 270 | 271 | @commands.command(aliases=["bomb"]) 272 | async def nuke(self, ctx, url: str = None): 273 | """Nukes an image. 274 | 275 | url: str 276 | """ 277 | await self.jeyy(ctx, "bomb", url) 278 | 279 | @commands.command() 280 | async def shock(self, ctx, url: str = None): 281 | """Pulses an image like a heartbeat. 282 | 283 | url: str 284 | """ 285 | await self.jeyy(ctx, "shock", url) 286 | 287 | @commands.command(aliases=["kill"]) 288 | async def shoot(self, ctx, url: str = None): 289 | """Shoots someone. 290 | 291 | url: str 292 | """ 293 | await self.jeyy(ctx, "shoot", url) 294 | 295 | @commands.command() 296 | async def bubbles(self, ctx, url: str = None): 297 | """Turns an image into a gif of bubbles. 298 | 299 | url: str 300 | """ 301 | await self.jeyy(ctx, "bubble", url) 302 | 303 | @commands.command() 304 | async def iso(self, ctx, *, codes=None): 305 | """Uses jeyy.xyz to draw isometric blocks based on inputted codes. 306 | 307 | - 0 = blank block - g = Gold Block 308 | - 1 = Grass Block - p = Purple Block 309 | - 2 = Water - l = Leaf Block 310 | - 3 = Sand Block - o = Log Block 311 | - 4 = Stone block - c = Coal Block 312 | - 5 = Wood Planks - d = Diamond Block 313 | - 6 = Glass Block - v = Lava 314 | - 7 = Redstone Block - h = Hay Bale 315 | - 8 = Iron Block - s = Snow Layer 316 | - 9 = Brick Block - f = Wooden Fence 317 | - w = Redstone Dust - r = Redstone Lamp 318 | - e = Lever (off) - # = Lever (on) 319 | - k = Cake - y = Poppy 320 | 321 | Example usage: 322 | .iso 401 133 332 - 1 0 5 - 6 323 | .iso 11111-o555o-o555o-o555o 11111-55555-55555-55555 11111-65556-55555-55555 11111 324 | """ 325 | if not codes: 326 | return await ctx.reply( 327 | embed=discord.Embed( 328 | color=discord.Color.blurple(), 329 | description="Example usage: `.iso 401 133 332 - 1 0 5 - 6`" 330 | "\n\nUse `.help iso` for full block list", 331 | ) 332 | ) 333 | url = "https://api.jeyy.xyz/isometric" 334 | params = {"iso_code": codes} 335 | 336 | async with self.bot.client_session.get(url, params=params, timeout=30) as resp: 337 | image = BytesIO() 338 | 339 | async for chunk in resp.content.iter_chunked(8 * 1024): 340 | image.write(chunk) 341 | 342 | image.seek(0) 343 | await ctx.reply(file=discord.File(fp=image, filename="isometric_draw.png")) 344 | 345 | @commands.command() 346 | async def images(self, ctx): 347 | """Shows all the image manipulation commands.""" 348 | image_commands = [] 349 | for item in sorted(dir(self)): 350 | item = getattr(self, item) 351 | if isinstance(item, commands.core.Command): 352 | image_commands.append( 353 | "`{}{}` ({})".format(ctx.prefix, item, item.help.split("\n", 1)[0]) 354 | ) 355 | 356 | await ctx.send( 357 | embed=discord.Embed( 358 | color=discord.Color.blurple(), 359 | title="Image Manipulation Commands", 360 | description="\n".join(image_commands), 361 | ) 362 | ) 363 | 364 | 365 | def setup(bot: commands.Bot) -> None: 366 | """Starts the image cog.""" 367 | bot.add_cog(images(bot)) 368 | -------------------------------------------------------------------------------- /cogs/information.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import inspect 4 | import os 5 | import platform 6 | import textwrap 7 | from datetime import datetime 8 | from io import StringIO 9 | 10 | import discord 11 | import orjson 12 | import psutil 13 | from discord.ext import commands 14 | 15 | 16 | class information(commands.Cog): 17 | """Commands that give information about the bot or server.""" 18 | 19 | def __init__(self, bot: commands.Bot) -> None: 20 | self.bot = bot 21 | self.DB = bot.DB 22 | self.process = psutil.Process() 23 | 24 | @commands.command() 25 | async def changes(self, ctx): 26 | """Gets the last 12 commits.""" 27 | url = "https://api.github.com/repos/Singularitat/snakebot/commits?per_page=24" 28 | 29 | async with ctx.typing(): 30 | commits = await self.bot.get_json(url) 31 | 32 | embed = discord.Embed(color=discord.Color.blurple()) 33 | 34 | count = 0 35 | for commit in commits: 36 | if len(commit["parents"]) > 1: 37 | continue 38 | if count == 12: 39 | break 40 | count += 1 41 | 42 | timestamp = int( 43 | datetime.fromisoformat( 44 | commit["commit"]["author"]["date"][:-1] 45 | ).timestamp() 46 | ) 47 | embed.add_field( 48 | name=f"", 49 | value=f"[**{commit['commit']['message']}**]({commit['html_url']})", 50 | ) 51 | await ctx.send(embed=embed) 52 | 53 | @commands.command() 54 | async def about(self, ctx): 55 | """Shows information about the bot.""" 56 | embed = discord.Embed(color=discord.Color.blurple()) 57 | embed.add_field(name="Total Commands", value=len(self.bot.commands)) 58 | embed.add_field( 59 | name="Source", value="[github](https://github.com/Singularitat/snakebot)" 60 | ) 61 | embed.add_field(name="Uptime", value=f"Since ****") 62 | embed.add_field(name="Pycord version", value=discord.__version__) 63 | embed.add_field(name="Python version", value=platform.python_version()) 64 | embed.add_field( 65 | name="OS", value=f"{platform.system()} {platform.release()}({os.name})" 66 | ) 67 | await ctx.send(embed=embed) 68 | 69 | @commands.command(aliases=["newest"]) 70 | @commands.guild_only() 71 | async def oldest(self, ctx, amount: int = 10): 72 | """Gets the oldest accounts in a server. 73 | Run with the `newest` alias to get the newest members 74 | 75 | amount: int 76 | """ 77 | amount = max(0, min(50, amount)) 78 | 79 | reverse = ctx.invoked_with.lower() == "newest" 80 | top = sorted(ctx.guild.members, key=lambda member: member.id, reverse=reverse)[ 81 | :amount 82 | ] 83 | 84 | description = "\n".join([f"**{member}:** {member.id}" for member in top]) 85 | embed = discord.Embed(color=discord.Color.blurple()) 86 | 87 | if len(description) > 2048: 88 | embed.description = "```Message is too large to send.```" 89 | return await ctx.send(embed=embed) 90 | 91 | embed.title = f"{'Youngest' if reverse else 'Oldest'} Accounts" 92 | embed.description = description 93 | 94 | await ctx.send(embed=embed) 95 | 96 | @commands.command(aliases=["msgtop"]) 97 | @commands.guild_only() 98 | async def message_top(self, ctx, amount=None): 99 | """Gets the users with the most messages in a server. 100 | 101 | Maximum amount that can be shown in the graph is 250 102 | 103 | amount: str 104 | """ 105 | msgtop = [] 106 | guild = str(ctx.guild.id).encode() 107 | 108 | for member, count in self.DB.message_count: 109 | if member.startswith(guild): 110 | msgtop.append((int(count), member.decode())) 111 | 112 | msgtop.sort(reverse=True) 113 | 114 | amount = 10 if not amount else 250 if amount.lower() == "all" else int(amount) 115 | 116 | total_lines = 0 117 | members = [] 118 | counts = [] 119 | lines = "" 120 | 121 | for count, member in msgtop: 122 | user = self.bot.get_user(int(member.split("-")[1])) 123 | if user: 124 | total_lines += 1 125 | 126 | members.append(user.display_name) 127 | counts.append(count) 128 | 129 | if total_lines < 30: 130 | lines += f"**{user.display_name}:** {count} messages\n" 131 | 132 | if total_lines == amount: 133 | break 134 | 135 | data = { 136 | "c": { 137 | "type": "bar", 138 | "data": { 139 | "labels": members, 140 | "datasets": [{"label": "Users", "data": counts}], 141 | }, 142 | }, 143 | "backgroundColor": "#202225", 144 | "format": "png", 145 | } 146 | 147 | url = "https://quickchart.io/chart/create" 148 | async with self.bot.client_session.get(url, json=data) as resp: 149 | resp = await resp.json() 150 | 151 | await ctx.send( 152 | embed=discord.Embed( 153 | color=discord.Color.blurple(), 154 | description=lines, 155 | title=f"Top {total_lines} chatters", 156 | ).set_image(url=resp["url"]) 157 | ) 158 | 159 | @commands.command() 160 | async def rule(self, ctx, number: int): 161 | """Shows the rules of the server. 162 | 163 | number: int 164 | Which rule to get. 165 | """ 166 | rules = self.DB.main.get(f"{ctx.guild.id}-rules".encode()) 167 | embed = discord.Embed(color=discord.Color.blurple()) 168 | 169 | if not rules: 170 | embed.description = "```No rules added yet.```" 171 | return await ctx.send(embed=embed) 172 | 173 | rules = orjson.loads(rules) 174 | 175 | if number not in range(1, len(rules) + 1): 176 | embed.description = "```No rule found.```" 177 | return await ctx.send(embed=embed) 178 | 179 | embed.description = f"```{rules[number-1]}```" 180 | await ctx.send(embed=embed) 181 | 182 | @commands.command() 183 | async def rules(self, ctx): 184 | """Shows all the rules of the server""" 185 | rules = self.DB.main.get(f"{ctx.guild.id}-rules".encode()) 186 | embed = discord.Embed(color=discord.Color.blurple()) 187 | 188 | if not rules: 189 | embed.description = "```No rules added yet.```" 190 | return await ctx.send(embed=embed) 191 | 192 | rules = orjson.loads(rules) 193 | embed.title = "Server Rules" 194 | for index, rule in enumerate(rules, start=1): 195 | embed.add_field(name=f"Rule {index}", value=rule, inline=False) 196 | 197 | await ctx.send(embed=embed) 198 | 199 | @commands.command(aliases=["perms"]) 200 | @commands.guild_only() 201 | async def permissions( 202 | self, ctx, member: discord.Member = None, channel: discord.TextChannel = None 203 | ): 204 | """Shows a member's permissions in a specific channel. 205 | 206 | member: discord.Member 207 | The member to get permissions of. 208 | channel: discord.TextChannel 209 | The channel to get the permissions in. 210 | """ 211 | channel = channel or ctx.channel 212 | member = member or ctx.author 213 | 214 | permissions = channel.permissions_for(member) 215 | embed = discord.Embed(color=member.color) 216 | embed.set_author(name=str(member), icon_url=member.avatar) 217 | 218 | allowed, denied = [], [] 219 | for name, value in permissions: 220 | name = name.replace("_", " ").replace("guild", "server").title() 221 | (allowed if value else denied).append(name) 222 | 223 | allowed = "\n".join(allowed) 224 | denied = "\n".join(denied) 225 | 226 | embed.add_field(name="Allowed", value=f"```ansi\n{allowed}```") 227 | embed.add_field(name="Denied", value=f"```ansi\n{denied}```") 228 | await ctx.send(embed=embed) 229 | 230 | @commands.command() 231 | async def invite(self, ctx): 232 | """Sends the invite link of the bot.""" 233 | # View Channels, Send Messages, Send Messages in Threads, Embed Links 234 | # Attach Files, Use External Emoji, Read Message History, Connect, Speak 235 | general_perms = discord.utils.oauth_url( 236 | self.bot.user.id, permissions=discord.Permissions(274881432576) 237 | ) 238 | # Manage Emojis and Stickers, Kick Members, Ban Members Manage Messages 239 | # Manage Threads + General Perms 240 | mod_perms = discord.utils.oauth_url( 241 | self.bot.user.id, permissions=discord.Permissions(293134789638) 242 | ) 243 | # Administrator 244 | admin_perms = discord.utils.oauth_url( 245 | self.bot.user.id, permissions=discord.Permissions(8) 246 | ) 247 | view = discord.ui.View( 248 | discord.ui.Button(label="Admin Perms", url=admin_perms), 249 | discord.ui.Button(label="Moderator Perms", url=mod_perms), 250 | discord.ui.Button(label="General Perms", url=general_perms), 251 | ) 252 | embed = discord.Embed(color=discord.Color.blurple()) 253 | embed.add_field( 254 | name="Admin Perms", 255 | value="Gives the bot Administrator\n" 256 | "[Full Perms](https://discordapi.com/permissions.html#8)", 257 | ) 258 | embed.add_field( 259 | name="Mod Perms", 260 | value="Kick, Ban and Manage Messages\n" 261 | "[Full Perms](https://discordapi.com/permissions.html#293134789638)", 262 | ) 263 | embed.add_field( 264 | name="General Perms", 265 | value="Send Messages, Read Messages, Connect to voice and Speak\n" 266 | "[Full Perms](https://discordapi.com/permissions.html#274881432576)", 267 | ) 268 | await ctx.send(embed=embed, view=view) 269 | 270 | @commands.command() 271 | async def ping(self, ctx): 272 | """Check how the bot is doing.""" 273 | latency = ( 274 | discord.utils.utcnow() - ctx.message.created_at 275 | ).total_seconds() * 1000 276 | 277 | if latency <= 0.05: 278 | latency = "Clock is out of sync" 279 | else: 280 | latency = f"`{latency:.2f} ms`" 281 | 282 | embed = discord.Embed(color=discord.Color.blurple()) 283 | embed.add_field(name="Command Latency", value=latency, inline=False) 284 | embed.add_field( 285 | name="Discord API Latency", value=f"`{self.bot.latency*1000:.2f} ms`" 286 | ) 287 | 288 | await ctx.send(embed=embed) 289 | 290 | @commands.command() 291 | async def usage(self, ctx): 292 | """Shows the bot's memory and cpu usage.""" 293 | memory_usage = self.process.memory_full_info().rss / 1024**2 294 | cpu_usage = self.process.cpu_percent() 295 | 296 | embed = discord.Embed(color=discord.Color.blurple()) 297 | embed.add_field(name="Memory Usage: ", value=f"**{memory_usage:.2f} MB**") 298 | embed.add_field(name="CPU Usage:", value=f"**{cpu_usage}%**") 299 | await ctx.send(embed=embed) 300 | 301 | @commands.command() 302 | async def source(self, ctx, *, command: str = None): 303 | """Gets the source code of a command from github. 304 | 305 | command: str 306 | The command to find the source code of. 307 | """ 308 | if not command: 309 | return await ctx.send("https://github.com/Singularitat/snakebot") 310 | 311 | if command == "help": 312 | src = type(self.bot.help_command) 313 | filename = inspect.getsourcefile(src) 314 | else: 315 | obj = self.bot.get_command(command) 316 | if not obj: 317 | embed = discord.Embed( 318 | color=discord.Color.blurple(), 319 | description="```Couldn't find command.```", 320 | ) 321 | return await ctx.send(embed=embed) 322 | 323 | src = obj.callback.__code__ 324 | filename = src.co_filename 325 | 326 | lines, lineno = inspect.getsourcelines(src) 327 | cog = os.path.relpath(filename).replace("\\", "/") 328 | 329 | link = f"" 330 | # The replace replaces the backticks with a backtick and a zero width space 331 | code = textwrap.dedent("".join(lines)).replace("`", "`\u200b") 332 | 333 | if len(code) >= 1990 - len(link): 334 | return await ctx.send( 335 | link, file=discord.File(StringIO(code), f"{command}.py") 336 | ) 337 | 338 | await ctx.send(f"{link}\n```py\n{code}```") 339 | 340 | @commands.command() 341 | async def uptime(self, ctx): 342 | """Shows the bots uptime.""" 343 | await ctx.send(f"Bot has been up since ****") 344 | 345 | @commands.command() 346 | @commands.guild_only() 347 | async def server(self, ctx): 348 | """Shows information about the current server.""" 349 | guild = ctx.guild 350 | offline_u, online_u, dnd_u, idle_u, bots = 0, 0, 0, 0, 0 351 | for member in guild.members: 352 | if member.bot: 353 | bots += 1 354 | if member.status is discord.Status.offline: 355 | offline_u += 1 356 | elif member.status is discord.Status.online: 357 | online_u += 1 358 | elif member.status is discord.Status.dnd: 359 | dnd_u += 1 360 | elif member.status is discord.Status.idle: 361 | idle_u += 1 362 | 363 | offline = "<:offline:766076363048222740>" 364 | online = "<:online:766076316512157768>" 365 | dnd = "<:dnd:766197955597959208>" 366 | idle = "<:idle:766197981955096608>" 367 | 368 | embed = discord.Embed(colour=discord.Colour.blurple()) 369 | embed.description = f""" 370 | **Server Information** 371 | Created: **** 372 | Owner: {guild.owner.mention} 373 | 374 | **Member Counts** 375 | Members: {guild.member_count:,} ({bots} bots) 376 | Roles: {len(guild.roles)} 377 | 378 | **Member Statuses** 379 | {online} {online_u:,} {dnd} {dnd_u:,} {idle} {idle_u:,} {offline} {offline_u:,} 380 | """ 381 | 382 | if guild.icon: 383 | embed.set_thumbnail(url=guild.icon) 384 | 385 | await ctx.send(embed=embed) 386 | 387 | @commands.command(aliases=["member"]) 388 | async def user(self, ctx, user: discord.Member | discord.User = None): 389 | """Sends info about a member. 390 | 391 | member: typing.Union[discord.Member, discord.User] 392 | The member to get info of defulting to the invoker. 393 | """ 394 | user = user or ctx.author 395 | created = f"" 396 | 397 | embed = discord.Embed( 398 | title=(str(user) + (" `[BOT]`" if user.bot else "")), 399 | color=discord.Color.random(), 400 | ) 401 | 402 | embed.add_field( 403 | name="User information", 404 | value=f"Created: **{created}**\nProfile: {user.mention}\nID: `{user.id}`", 405 | inline=False, 406 | ) 407 | 408 | if isinstance(user, discord.Member): 409 | length = len(user.roles) - 1 410 | if length > 10: 411 | roles = ( 412 | ", ".join(role.mention for role in user.roles[1:11]) 413 | + f" + {length - 10} more roles" 414 | ) 415 | else: 416 | roles = ", ".join(role.mention for role in user.roles[1:]) 417 | 418 | joined = f"****" 419 | if roles and user.top_role.colour.value != 0: 420 | embed.color = user.top_role.colour 421 | embed.title = f"{user.nick} ({user})" if user.nick else embed.title 422 | 423 | embed.add_field( 424 | name="Member information", 425 | value=f"Joined: {joined}\nRoles: {roles or None}\n", 426 | inline=False, 427 | ) 428 | des = "ini" if user.desktop_status.value != "offline" else "css" 429 | mob = "ini" if user.mobile_status.value != "offline" else "css" 430 | web = "ini" if user.web_status.value != "offline" else "css" 431 | 432 | embed.add_field( 433 | name="Desktop", value=f"```{des}\n[{user.desktop_status}]```" 434 | ) 435 | embed.add_field(name="Mobile", value=f"```{mob}\n[{user.mobile_status}]```") 436 | embed.add_field(name="Web", value=f"```{web}\n[{user.web_status}]```") 437 | 438 | embed.set_thumbnail(url=user.display_avatar) 439 | 440 | await ctx.send(embed=embed) 441 | 442 | @commands.command(aliases=["avatar"]) 443 | async def icon(self, ctx, user: discord.User = None): 444 | """Sends a members avatar url. 445 | 446 | user: discord.User 447 | The member to show the avatar of. 448 | """ 449 | user = user or ctx.author 450 | await ctx.send(user.display_avatar) 451 | 452 | @commands.command() 453 | async def banner(self, ctx, user: discord.User = None): 454 | """Sends a members banner url. 455 | 456 | user: discord.User 457 | The member to show the banner of. 458 | """ 459 | user = user or ctx.author 460 | 461 | if not user.banner: 462 | return await ctx.send( 463 | embed=discord.Embed( 464 | color=discord.Color.blurple(), 465 | description="```User doesn't have a banner```", 466 | ) 467 | ) 468 | 469 | await ctx.send(user.banner.url) 470 | 471 | 472 | def setup(bot: commands.Bot) -> None: 473 | """Starts information cog.""" 474 | bot.add_cog(information(bot)) 475 | -------------------------------------------------------------------------------- /cogs/owner.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import os 3 | import textwrap 4 | import time 5 | import traceback 6 | from contextlib import redirect_stdout 7 | from io import StringIO 8 | 9 | import discord 10 | import orjson 11 | from discord.ext import commands, pages 12 | 13 | 14 | class PerformanceMocker: 15 | """A mock object that can also be used in await expressions.""" 16 | 17 | def __init__(self): 18 | self.loop = asyncio.get_running_loop() 19 | 20 | def permissions_for(self, obj): 21 | perms = discord.Permissions.all() 22 | perms.embed_links = False 23 | return perms 24 | 25 | def __getattr__(self, attr): 26 | return self 27 | 28 | def __call__(self, *args, **kwargs): 29 | return self 30 | 31 | def __repr__(self): 32 | return "" 33 | 34 | def __await__(self): 35 | future = self.loop.create_future() 36 | future.set_result(self) 37 | return future.__await__() 38 | 39 | async def __aenter__(self): 40 | return self 41 | 42 | async def __aexit__(self, *args): 43 | return self 44 | 45 | def __len__(self): 46 | return 0 47 | 48 | def __bool__(self): 49 | return False 50 | 51 | 52 | class owner(commands.Cog): 53 | """Administrative commands.""" 54 | 55 | def __init__(self, bot: commands.Bot) -> None: 56 | self.bot = bot 57 | self.DB = bot.DB 58 | 59 | async def cog_check(self, ctx): 60 | """Checks if the member is an owner. 61 | 62 | ctx: commands.Context 63 | """ 64 | return ctx.author.id in self.bot.owner_ids 65 | 66 | @commands.command() 67 | async def logs(self, ctx): 68 | """Paginates over the logs.""" 69 | with open("bot.log") as file: 70 | lines = file.readlines() 71 | 72 | embeds = [] 73 | 74 | for i in range(0, len(lines), 20): 75 | chunk = "".join(lines[i : i + 20]) 76 | embeds.append( 77 | discord.Embed( 78 | color=discord.Color.blurple(), description=f"```{chunk}```" 79 | ) 80 | ) 81 | 82 | paginator = pages.Paginator(pages=embeds) 83 | await paginator.send(ctx) 84 | 85 | @commands.command(aliases=["type"]) 86 | async def findtype(self, ctx, snowflake: int): 87 | async def found_message(type_name: str) -> None: 88 | await ctx.send( 89 | embed=discord.Embed( 90 | color=discord.Color.blurple(), 91 | description=f"**ID**: `{snowflake}`\n" 92 | f"**Type:** `{type_name.capitalize()}`\n" 93 | f"**Created:** > 22) + 1420070400000)//1000}>", 94 | ) 95 | ) 96 | 97 | await ctx.trigger_typing() 98 | 99 | emoji = await self.bot.client_session.head( 100 | f"https://cdn.discordapp.com/emojis/{snowflake}" 101 | ) 102 | if emoji.status == 200: 103 | return await found_message("emoji") 104 | 105 | try: 106 | if await ctx.fetch_message(snowflake): 107 | return await found_message("message") 108 | except discord.NotFound: 109 | pass 110 | 111 | types = ( 112 | ("channel", True), 113 | ("user", True), 114 | ("guild", True), 115 | ("sticker", True), 116 | ("stage_instance", True), 117 | ("webhook", False), 118 | ("widget", False), 119 | ) 120 | 121 | for obj_type, has_get_method in types: 122 | if has_get_method and getattr(self.bot, f"get_{obj_type}")(snowflake): 123 | return await found_message(obj_type) 124 | try: 125 | if await getattr(self.bot, f"fetch_{obj_type}")(snowflake): 126 | return await found_message(obj_type) 127 | except discord.Forbidden: 128 | if ( 129 | obj_type != "guild" 130 | ): # Even if the guild doesn't exist it says it is forbidden rather than not found 131 | return await found_message(obj_type) 132 | except discord.NotFound: 133 | pass 134 | 135 | await ctx.reply("Cannot find type of object that this id is for") 136 | 137 | @commands.command(pass_context=True, hidden=True, name="eval") 138 | async def _eval(self, ctx, *, code: str): 139 | """Evaluates code. 140 | 141 | code: str 142 | """ 143 | env = { 144 | "bot": self.bot, 145 | "ctx": ctx, 146 | "channel": ctx.channel, 147 | "author": ctx.author, 148 | "guild": ctx.guild, 149 | "message": ctx.message, 150 | } 151 | 152 | env.update(globals()) 153 | 154 | if code.startswith("```") and code.endswith("```"): 155 | code = "\n".join(code.split("\n")[1:-1]) 156 | else: 157 | code = code.strip("` \n") 158 | 159 | stdout = StringIO() 160 | 161 | func = f'async def func():\n{textwrap.indent(code, " ")}' 162 | 163 | try: 164 | exec(func, env) 165 | except Exception as e: 166 | return await ctx.send(f"```ml\n{e.__class__.__name__}: {e}\n```") 167 | 168 | func = env["func"] 169 | 170 | try: 171 | with redirect_stdout(stdout): 172 | ret = await func() 173 | except Exception: 174 | value = stdout.getvalue() 175 | return await ctx.send(f"```py\n{value}{traceback.format_exc()}\n```") 176 | 177 | embed = discord.Embed(color=discord.Color.blurple()) 178 | embed.add_field(name="stdout", value=stdout.getvalue() or "None", inline=False) 179 | embed.add_field(name="Return Value", value=ret, inline=False) 180 | 181 | return await ctx.send(embed=embed) 182 | 183 | @commands.group(invoke_without_command=True) 184 | async def db(self, ctx): 185 | await ctx.send( 186 | embed=discord.Embed( 187 | color=discord.Color.blurple(), 188 | description=f"```Usage: {ctx.prefix}db [del/show/get/put/pre]```", 189 | ) 190 | ) 191 | 192 | @db.command() 193 | async def put(self, ctx, key, *, value=None): 194 | """Puts a value in the database 195 | 196 | key: str 197 | value: str 198 | """ 199 | embed = discord.Embed(color=discord.Color.blurple()) 200 | 201 | if not value: 202 | if not ctx.message.attachments: 203 | embed.description = "```You need to attach a file or input a value.```" 204 | return await ctx.send(embed=embed) 205 | 206 | value = (await ctx.message.attachments[0].read()).decode() 207 | 208 | self.DB.main.put(key.encode(), value.encode()) 209 | 210 | length = len(value) 211 | if length < 1986: 212 | embed.description = f"```Put {value} at {key}```" 213 | else: 214 | embed.description = f"```Put {length} characters at {key}```" 215 | await ctx.send(embed=embed) 216 | 217 | @db.command(name="delete", aliases=["del"]) 218 | async def db_delete(self, ctx, key): 219 | """Deletes an item from the database. 220 | 221 | key: str 222 | """ 223 | self.DB.main.delete(key.encode()) 224 | 225 | await ctx.send( 226 | embed=discord.Embed( 227 | color=discord.Color.blurple(), 228 | description=f"```Deleted {key} from database```", 229 | ) 230 | ) 231 | 232 | @db.command() 233 | async def get(self, ctx, key): 234 | """Shows an item from the database. 235 | 236 | key: str 237 | """ 238 | item = self.DB.main.get(key.encode()) 239 | 240 | if not item: 241 | return await ctx.send( 242 | embed=discord.Embed( 243 | color=discord.Color.blurple(), 244 | description="```Key not found in database```", 245 | ) 246 | ) 247 | 248 | file = StringIO(item.decode()) 249 | 250 | await ctx.send(file=discord.File(file, "data.txt")) 251 | 252 | @db.command() 253 | async def show(self, ctx, exclude=True): 254 | """Sends a json of the entire database.""" 255 | database = {} 256 | 257 | if exclude: 258 | excluded = ( 259 | b"crypto", 260 | b"stocks", 261 | b"message_count", 262 | b"invites", 263 | b"karma", 264 | b"boot_times", 265 | b"aliases", 266 | ) 267 | 268 | for key, value in self.DB.main: 269 | if key.split(b"-")[0] not in excluded: 270 | if value[:1] in [b"{", b"["]: 271 | value = orjson.loads(value) 272 | else: 273 | value = value.decode() 274 | database[key.decode()] = value 275 | else: 276 | for key, value in self.DB.main: 277 | if value[:1] in [b"{", b"["]: 278 | value = orjson.loads(value) 279 | else: 280 | value = value.decode() 281 | database[key.decode()] = value 282 | 283 | file = StringIO(str(database)) 284 | await ctx.send(file=discord.File(file, "data.json")) 285 | 286 | @db.command(aliases=["pre"]) 287 | async def show_prefixed(self, ctx, prefixed): 288 | """Sends a json of the entire database.""" 289 | if not hasattr(self.DB, prefixed): 290 | return await ctx.send( 291 | embed=discord.Embed( 292 | color=discord.Color.blurple(), 293 | description=f"```Prefixed DB {prefixed} not found```", 294 | ) 295 | ) 296 | 297 | database = { 298 | key.decode(): value.decode() for key, value in getattr(self.DB, prefixed) 299 | } 300 | 301 | file = StringIO(str(database)) 302 | 303 | await ctx.send(file=discord.File(file, "data.json")) 304 | 305 | @commands.command(aliases=["removeinf"]) 306 | @commands.guild_only() 307 | async def remove_infraction( 308 | self, ctx, member: discord.Member, infraction: str, index: int 309 | ): 310 | """Removes an infraction at an index from a member. 311 | 312 | member: discord.Member 313 | type: str 314 | The type of infraction to remove e.g warnings, mutes, kicks, bans 315 | index: int 316 | The index of the infraction to remove e.g 0, 1, 2 317 | """ 318 | member_id = f"{ctx.guild.id}-{member.id}".encode() 319 | infractions = self.DB.infractions.get(member_id) 320 | 321 | embed = discord.Embed(color=discord.Color.blurple()) 322 | 323 | if not infractions: 324 | embed.description = "No infractions found for member" 325 | return await ctx.send(embed=embed) 326 | 327 | inf = orjson.loads(infractions) 328 | infraction = inf[infraction].pop(index) 329 | 330 | embed.description = f"Deleted infraction [{infraction}] from {member}" 331 | await ctx.send(embed=embed) 332 | 333 | self.DB.infractions.put(member_id, orjson.dumps(infractions)) 334 | 335 | @commands.command(name="gblacklist") 336 | async def global_blacklist(self, ctx, user: discord.User): 337 | """Globally blacklists someone from the bot. 338 | 339 | user: discord.user 340 | """ 341 | embed = discord.Embed(color=discord.Color.blurple()) 342 | 343 | user_id = str(user.id).encode() 344 | if self.DB.blacklist.get(user_id): 345 | self.DB.blacklist.delete(user_id) 346 | 347 | embed.title = "User Unblacklisted" 348 | embed.description = f"***{user}*** has been unblacklisted" 349 | return await ctx.send(embed=embed) 350 | 351 | self.DB.blacklist.put(user_id, b"2") 352 | embed.title = "User Blacklisted" 353 | embed.description = f"**{user}** has been added to the blacklist" 354 | 355 | await ctx.send(embed=embed) 356 | 357 | @commands.command(name="gdownvote") 358 | async def global_downvote(self, ctx, user: discord.User): 359 | """Globally downvotes someones. 360 | 361 | user: discord.user 362 | """ 363 | embed = discord.Embed(color=discord.Color.blurple()) 364 | 365 | user_id = str(user.id).encode() 366 | if self.DB.blacklist.get(user_id): 367 | self.DB.blacklist.delete(user_id) 368 | 369 | embed.title = "User Undownvoted" 370 | embed.description = f"***{user}*** has been undownvoted" 371 | return await ctx.send(embed=embed) 372 | 373 | self.DB.blacklist.put(user_id, b"1") 374 | embed.title = "User Downvoted" 375 | embed.description = f"**{user}** has been added to the downvote list" 376 | 377 | await ctx.send(embed=embed) 378 | 379 | @commands.command() 380 | async def backup(self, ctx, number: int = None): 381 | """Sends the bot database backup as a json file. 382 | 383 | number: int 384 | Which backup to get. 385 | """ 386 | if not number: 387 | number = int(self.DB.main.get(b"backup_number").decode()) 388 | 389 | with open(f"backup/{number}backup.json", "rb") as file: 390 | return await ctx.send(file=discord.File(file, "backup.json")) 391 | 392 | number = min(10, max(number, 0)) 393 | 394 | with open(f"backup/{number}backup.json", "rb") as file: 395 | await ctx.send(file=discord.File(file, "backup.json")) 396 | 397 | @commands.command(name="boot") 398 | async def boot_times(self, ctx): 399 | """Shows the average fastest and slowest boot times of the bot.""" 400 | boot_times = self.DB.main.get(b"boot_times") 401 | 402 | embed = discord.Embed(color=discord.Color.blurple()) 403 | 404 | if not boot_times: 405 | embed.description = "No boot times found" 406 | return await ctx.send(embed=embed) 407 | 408 | boot_times = orjson.loads(boot_times) 409 | 410 | msg = ( 411 | f"\n\nAverage: {(sum(boot_times) / len(boot_times)):.5f}s" 412 | f"\nSlowest: {max(boot_times):.5f}s" 413 | f"\nFastest: {min(boot_times):.5f}s" 414 | f"\nLast Three: {boot_times[-3:]}" 415 | ) 416 | 417 | embed.description = f"```{msg}```" 418 | await ctx.send(embed=embed) 419 | 420 | @commands.group(invoke_without_command=True) 421 | async def cache(self, ctx): 422 | """Command group for interacting with the cache.""" 423 | await ctx.send( 424 | embed=discord.Embed( 425 | color=discord.Color.blurple(), 426 | description=f"```Usage: {ctx.prefix}cache [wipe/list]```", 427 | ) 428 | ) 429 | 430 | @cache.command() 431 | async def wipe(self, ctx): 432 | """Wipes cache from the db.""" 433 | self.bot.cache.clear() 434 | 435 | await ctx.send( 436 | embed=discord.Embed( 437 | color=discord.Color.blurple(), description="```prolog\nWiped Cache```" 438 | ) 439 | ) 440 | 441 | @cache.command(name="list") 442 | async def _list(self, ctx): 443 | """Lists the cached items in the db.""" 444 | embed = discord.Embed(color=discord.Color.blurple()) 445 | cache = self.bot.cache 446 | 447 | if not cache: 448 | embed.description = "```Nothing has been cached```" 449 | return await ctx.send(embed=embed) 450 | 451 | embed.description = "```\n{}```".format("\n".join(cache)) 452 | await ctx.send(embed=embed) 453 | 454 | @commands.command() 455 | async def disable(self, ctx, *, command): 456 | """Disables the use of a command for every guild. 457 | 458 | command: str 459 | The name of the command to disable. 460 | """ 461 | command = self.bot.get_command(command) 462 | embed = discord.Embed(color=discord.Color.blurple()) 463 | 464 | if not command: 465 | embed.description = "```Command not found```" 466 | return await ctx.send(embed=embed) 467 | 468 | command.enabled = not command.enabled 469 | ternary = "enabled" if command.enabled else "disabled" 470 | 471 | embed.description = ( 472 | f"```Successfully {ternary} the {command.qualified_name} command```" 473 | ) 474 | await ctx.send(embed=embed) 475 | 476 | @commands.command() 477 | async def perf(self, ctx, *, command): 478 | """Checks the timing of a command, while attempting to suppress HTTP calls. 479 | 480 | p.s just the command itself with nothing in it takes about 0.02ms 481 | 482 | command: str 483 | The command to run including arguments. 484 | """ 485 | ctx.message.content = f"{ctx.prefix}{command}" 486 | new_ctx = await self.bot.get_context(ctx.message, cls=type(ctx)) 487 | new_ctx.reply = new_ctx.send # Reply ignores the PerformanceMocker 488 | 489 | # Intercepts the Messageable interface a bit 490 | new_ctx._state = PerformanceMocker() 491 | new_ctx.channel = PerformanceMocker() 492 | 493 | embed = discord.Embed(color=discord.Color.blurple()) 494 | 495 | if not new_ctx.command: 496 | embed.description = "```No command found```" 497 | return await ctx.send(embed=embed) 498 | 499 | start = time.perf_counter() 500 | 501 | try: 502 | await new_ctx.command.invoke(new_ctx) 503 | except commands.CommandError: 504 | end = time.perf_counter() 505 | result = "Failed" 506 | error = traceback.format_exc().replace("`", "`\u200b") 507 | 508 | await ctx.send(f"```py\n{error}\n```") 509 | else: 510 | end = time.perf_counter() 511 | result = "Success" 512 | 513 | embed.description = f"```css\n{result}: {(end - start) * 1000:.2f}ms```" 514 | await ctx.send(embed=embed) 515 | 516 | @commands.command() 517 | @commands.guild_only() 518 | async def suin( 519 | self, ctx, channel: discord.TextChannel, member: discord.Member, *, command: str 520 | ): 521 | """Run a command as another user in another channel. 522 | 523 | channel: discord.TextChannel 524 | The channel to run the command in. 525 | member: discord.Member 526 | The member to run the command as. 527 | command: str 528 | The command name. 529 | """ 530 | ctx.message.channel = channel 531 | ctx.message.author = member 532 | ctx.message.content = f"{ctx.prefix}{command}" 533 | new_ctx = await self.bot.get_context(ctx.message, cls=type(ctx)) 534 | new_ctx.reply = new_ctx.send # Can't reply to messages in other channels 535 | await self.bot.invoke(new_ctx) 536 | 537 | @commands.command() 538 | async def sudo(self, ctx, member: discord.Member | discord.User, *, command: str): 539 | """Run a command as another user. 540 | 541 | member: discord.Member 542 | The member to run the command as. 543 | command: str 544 | The command name. 545 | """ 546 | ctx.message.author = member 547 | ctx.message.content = f"{ctx.prefix}{command}" 548 | new_ctx = await self.bot.get_context(ctx.message, cls=type(ctx)) 549 | await self.bot.invoke(new_ctx) 550 | 551 | @commands.command() 552 | async def status(self, ctx): 553 | await self.bot.run_process("git fetch") 554 | status = await self.bot.run_process("git status", True) 555 | 556 | embed = discord.Embed(color=discord.Color.blurple()) 557 | embed.description = f"```ahk\n{' '.join(status)}```" 558 | 559 | await ctx.send(embed=embed) 560 | 561 | @commands.command() 562 | async def load(self, ctx, extension: str): 563 | """Loads an extension. 564 | 565 | extension: str 566 | The extension to load. 567 | """ 568 | embed = discord.Embed(color=discord.Color.blurple()) 569 | 570 | try: 571 | self.bot.load_extension(f"cogs.{extension}") 572 | except (AttributeError, ImportError) as e: 573 | embed.description = f"```{type(e).__name__}: {e}```" 574 | return await ctx.send(embed=embed) 575 | 576 | embed.title = f"{extension} loaded." 577 | await ctx.send(embed=embed) 578 | 579 | @commands.command() 580 | async def unload(self, ctx, ext: str): 581 | """Unloads an extension. 582 | 583 | extension: str 584 | The extension to unload. 585 | """ 586 | self.bot.unload_extension(f"cogs.{ext}") 587 | await ctx.send( 588 | embed=discord.Embed(title=f"{ext} unloaded.", color=discord.Color.blurple()) 589 | ) 590 | 591 | @commands.command() 592 | async def reload(self, ctx, ext: str): 593 | """Reloads an extension. 594 | 595 | extension: str 596 | The extension to reload. 597 | """ 598 | self.bot.reload_extension(f"cogs.{ext}") 599 | await ctx.send( 600 | embed=discord.Embed(title=f"{ext} reloaded.", color=discord.Color.blurple()) 601 | ) 602 | 603 | @commands.command() 604 | async def restart(self, ctx): 605 | """Restarts all extensions.""" 606 | embed = discord.Embed(color=discord.Color.blurple()) 607 | self.DB.main.put(b"restart", b"1") 608 | 609 | for ext in [f[:-3] for f in os.listdir("cogs") if f.endswith(".py")]: 610 | try: 611 | self.bot.reload_extension(f"cogs.{ext}") 612 | except Exception as e: 613 | if isinstance(e, discord.errors.ExtensionNotLoaded): 614 | self.bot.load_extension(f"cogs.{ext}") 615 | embed.description = f"```{type(e).__name__}: {e}```" 616 | return await ctx.send(embed=embed) 617 | 618 | embed.title = "Extensions restarted." 619 | await ctx.send(embed=embed) 620 | 621 | 622 | def setup(bot: commands.Bot) -> None: 623 | """Starts owner cog.""" 624 | bot.add_cog(owner(bot)) 625 | -------------------------------------------------------------------------------- /cogs/stocks.py: -------------------------------------------------------------------------------- 1 | import textwrap 2 | from decimal import Decimal 3 | 4 | import discord 5 | import orjson 6 | from discord.ext import commands, pages 7 | 8 | 9 | class stocks(commands.Cog): 10 | """Stock related commands.""" 11 | 12 | def __init__(self, bot: commands.Bot) -> None: 13 | self.bot = bot 14 | self.DB = bot.DB 15 | 16 | @commands.group() 17 | async def stock(self, ctx): 18 | """Gets the current price of a stock. 19 | 20 | symbol: str 21 | The symbol of the stock to find. 22 | """ 23 | if ctx.invoked_subcommand: 24 | return 25 | 26 | embed = discord.Embed(colour=discord.Colour.blurple()) 27 | 28 | if not ctx.subcommand_passed: 29 | embed = discord.Embed(color=discord.Color.blurple()) 30 | embed.description = ( 31 | f"```Usage: {ctx.prefix}stock [buy/sell/bal/profile/list/history]" 32 | f" or {ctx.prefix}stock [ticker]```" 33 | ) 34 | return await ctx.send(embed=embed) 35 | 36 | symbol = ctx.subcommand_passed.upper() 37 | stock = self.DB.get_stock(symbol) 38 | embed = discord.Embed(color=discord.Color.blurple()) 39 | 40 | if not stock: 41 | embed.description = f"```No stock found for {symbol}```" 42 | return await ctx.send(embed=embed) 43 | 44 | change = stock["change"] 45 | sign = "" if change[0] == "-" else "+" 46 | 47 | embed.title = f"{symbol} [{stock['name']}]" 48 | embed.add_field(name="Price", value=f"```${stock['price']}```") 49 | embed.add_field(name="Market Cap", value=f"```${stock['cap']}```", inline=False) 50 | embed.add_field(name="24h Change", value=f"```diff\n{sign}{change}```") 51 | embed.add_field( 52 | name="Percent 24h Change", value=f"```diff\n{sign}{stock['%change']}%```" 53 | ) 54 | embed.set_image(url=f"https://charts2.finviz.com/chart.ashx?s=l&p=w&t={symbol}") 55 | 56 | await ctx.send(embed=embed) 57 | 58 | @stock.command() 59 | async def sell(self, ctx, symbol, amount): 60 | """Sells stock. 61 | 62 | symbol: str 63 | The symbol of the stock to sell. 64 | amount: float 65 | The amount of stock to sell. 66 | """ 67 | embed = discord.Embed(color=discord.Color.blurple()) 68 | 69 | symbol = symbol.upper() 70 | price = self.DB.get_stock(symbol) 71 | 72 | if not price: 73 | embed.description = f"```Couldn't find stock {symbol}```" 74 | return await ctx.send(embed=embed) 75 | 76 | price = price["price"] 77 | member_id = str(ctx.author.id).encode() 78 | stockbal = self.DB.get_stockbal(member_id) 79 | 80 | if not stockbal: 81 | embed.description = f"```You have never invested in {symbol}```" 82 | return await ctx.send(embed=embed) 83 | 84 | if amount[-1] == "%": 85 | amount = stockbal[symbol]["total"] * ((float(amount[:-1])) / 100) 86 | else: 87 | amount = float(amount) 88 | 89 | if amount < 0: 90 | embed.description = "```You can't sell a negative amount of stocks```" 91 | return await ctx.send(embed=embed) 92 | 93 | if stockbal[symbol]["total"] < amount: 94 | embed.description = ( 95 | f"```Not enough stock you have: {stockbal[symbol]['total']}```" 96 | ) 97 | return await ctx.send(embed=embed) 98 | 99 | bal = self.DB.get_bal(member_id) 100 | 101 | cash = amount * float(price) 102 | 103 | stockbal[symbol]["total"] -= amount 104 | 105 | if stockbal[symbol]["total"] == 0: 106 | stockbal.pop(symbol, None) 107 | else: 108 | stockbal[symbol]["history"].append((-amount, cash)) 109 | 110 | bal += Decimal(cash) 111 | 112 | embed = discord.Embed( 113 | title=f"Sold {amount:.2f} stocks for ${cash:.2f}", 114 | color=discord.Color.blurple(), 115 | ) 116 | embed.set_footer(text=f"Balance: ${bal:,}") 117 | 118 | await ctx.send(embed=embed) 119 | 120 | self.DB.put_bal(member_id, bal) 121 | self.DB.put_stockbal(member_id, stockbal) 122 | 123 | @stock.command(aliases=["buy"]) 124 | async def invest(self, ctx, symbol, cash: float): 125 | """Buys stock or if nothing is passed in it shows the price of some stocks. 126 | symbol: str 127 | The symbol of the stock to buy. 128 | cash: int 129 | The amount of money to invest. 130 | """ 131 | embed = discord.Embed(color=discord.Color.blurple()) 132 | 133 | if cash < 0: 134 | embed.description = "```You can't buy a negative amount of stocks```" 135 | return await ctx.send(embed=embed) 136 | 137 | symbol = symbol.upper() 138 | stock = self.DB.get_stock(symbol) 139 | 140 | if not stock: 141 | embed.description = f"```Couldn't find stock {symbol}```" 142 | return await ctx.send(embed=embed) 143 | 144 | stock = stock["price"] 145 | member_id = str(ctx.author.id).encode() 146 | bal = self.DB.get_bal(member_id) 147 | 148 | if bal < cash: 149 | embed.description = "```You don't have enough cash```" 150 | return await ctx.send(embed=embed) 151 | 152 | amount = cash / float(stock) 153 | 154 | stockbal = self.DB.get_stockbal(member_id) 155 | 156 | if symbol not in stockbal: 157 | stockbal[symbol] = {"total": 0, "history": [(amount, cash)]} 158 | else: 159 | stockbal[symbol]["history"].append((amount, cash)) 160 | 161 | stockbal[symbol]["total"] += amount 162 | bal -= Decimal(cash) 163 | 164 | embed = discord.Embed( 165 | title=f"You bought {amount:.2f} stocks in {symbol}", 166 | color=discord.Color.blurple(), 167 | ) 168 | embed.set_footer(text=f"Balance: ${bal:,}") 169 | 170 | await ctx.send(embed=embed) 171 | 172 | self.DB.put_bal(member_id, bal) 173 | self.DB.put_stockbal(member_id, stockbal) 174 | 175 | @stock.command(aliases=["balance"]) 176 | async def bal(self, ctx, symbol): 177 | """Shows the amount of stocks you have bought in a stock. 178 | 179 | symbol: str 180 | The symbol of the stock to find. 181 | """ 182 | symbol = symbol.upper() 183 | member_id = str(ctx.author.id).encode() 184 | stockbal = self.DB.get_stockbal(member_id) 185 | embed = discord.Embed(color=discord.Color.blurple()) 186 | 187 | if not stockbal: 188 | embed.description = "```You have never invested```" 189 | return await ctx.send(embed=embed) 190 | 191 | if symbol not in stockbal: 192 | embed.description = f"```You have never invested in {symbol}```" 193 | return await ctx.send(embed=embed) 194 | 195 | stock = self.DB.get_stock(symbol) 196 | 197 | trades = [ 198 | trade[1] / trade[0] for trade in stockbal[symbol]["history"] if trade[0] > 0 199 | ] 200 | change = ((float(stock["price"]) / (sum(trades) / len(trades))) - 1) * 100 201 | 202 | embed.description = textwrap.dedent( 203 | f""" 204 | ```diff 205 | You have {stockbal[symbol]['total']:.2f} stocks in {symbol} 206 | 207 | Price: {stock['price']} 208 | 209 | Percent Gain/Loss: 210 | {"" if change < 0 else "+"}{change:.2f}% 211 | 212 | Market Cap: {stock['cap']} 213 | ``` 214 | """ 215 | ) 216 | 217 | await ctx.send(embed=embed) 218 | 219 | @stock.command(aliases=["p"]) 220 | async def profile(self, ctx, member: discord.Member = None): 221 | """Gets someone's stock profile. 222 | 223 | member: discord.Member 224 | The member whose stockprofile will be shown 225 | """ 226 | member = member or ctx.author 227 | 228 | member_id = str(member.id).encode() 229 | stockbal = self.DB.get_stockbal(member_id) 230 | embed = discord.Embed(color=discord.Color.blurple()) 231 | 232 | if not stockbal: 233 | embed.description = "```You have never invested```" 234 | return await ctx.send(embed=embed) 235 | 236 | if len(stockbal) == 0: 237 | embed.description = "```You have sold all your stocks.```" 238 | return await ctx.send(embed=embed) 239 | 240 | net_value = 0 241 | msg = ( 242 | f"{member.display_name}'s stock profile:\n\n" 243 | "Name: Amount: Price: Percent Gain:\n" 244 | ) 245 | 246 | for stock in stockbal: 247 | data = self.DB.get_stock(stock) 248 | price = float(data["price"]) 249 | 250 | trades = [ 251 | trade[1] / trade[0] 252 | for trade in stockbal[stock]["history"] 253 | if trade[0] > 0 254 | ] 255 | change = ((price / (sum(trades) / len(trades))) - 1) * 100 256 | color = "31" if change < 0 else "32" 257 | 258 | msg += ( 259 | f"[2;{color}m{stock + ':':<8} {stockbal[stock]['total']:<13.2f}" 260 | f"${price:<17.2f} {change:.2f}%\n" 261 | ) 262 | 263 | net_value += stockbal[stock]["total"] * price 264 | 265 | embed.description = f"```ansi\n{msg}\nNet Value: ${net_value:.2f}```" 266 | await ctx.send(embed=embed) 267 | 268 | @stock.command() 269 | async def list(self, ctx): 270 | """Shows the prices of stocks from the nasdaq api.""" 271 | messages = [] 272 | stocks_ = "" 273 | for i, (stock, price) in enumerate(self.DB.stocks, start=1): 274 | price = orjson.loads(price)["price"] 275 | 276 | if not i % 3: 277 | stocks_ += f"{stock.decode():}: ${float(price):.2f}\n" 278 | else: 279 | stocks_ += f"{stock.decode():}: ${float(price):.2f}\t".expandtabs() 280 | 281 | if not i % 99: 282 | messages.append(discord.Embed(description=f"```prolog\n{stocks_}```")) 283 | stocks_ = "" 284 | 285 | if i % 99: 286 | messages.append(discord.Embed(description=f"```prolog\n{stocks_}```")) 287 | 288 | paginator = pages.Paginator(pages=messages) 289 | await paginator.send(ctx) 290 | 291 | @stock.command(aliases=["h"]) 292 | async def history(self, ctx, member: discord.Member = None, amount=10): 293 | """Gets a members crypto transaction history. 294 | 295 | member: discord.Member 296 | amount: int 297 | How many transactions to get 298 | """ 299 | member = member or ctx.author 300 | 301 | embed = discord.Embed(color=discord.Color.blurple()) 302 | stockbal = self.DB.get_stockbal(str(member.id).encode()) 303 | 304 | if not stockbal: 305 | embed.description = "```You haven't invested.```" 306 | return await ctx.send(embed=embed) 307 | 308 | msg = "" 309 | 310 | for stock_name, stock_data in stockbal.items(): 311 | msg += f"{stock_name}:\n" 312 | for trade in stock_data["history"]: 313 | if trade[0] < 0: 314 | kind = "Sold" 315 | else: 316 | kind = "Bought" 317 | msg += f"{kind} {abs(trade[0]):.2f} for ${trade[1]:.2f}\n" 318 | msg += "\n" 319 | 320 | embed.description = f"```{msg}```" 321 | await ctx.send(embed=embed) 322 | 323 | 324 | def setup(bot: commands.Bot) -> None: 325 | """Starts stocks cog.""" 326 | bot.add_cog(stocks(bot)) 327 | -------------------------------------------------------------------------------- /cogs/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Singularitat/snakebot/7a24219b41176b79f1370333b0fac3352fce2ef7/cogs/utils/__init__.py -------------------------------------------------------------------------------- /cogs/utils/calculation.py: -------------------------------------------------------------------------------- 1 | import ast 2 | import math 3 | from decimal import Decimal 4 | 5 | 6 | def add(a, b): 7 | return a + b 8 | 9 | 10 | def sub(a, b): 11 | return a - b 12 | 13 | 14 | def mul(a, b): 15 | return a * b 16 | 17 | 18 | def truediv(a, b): 19 | return a / b 20 | 21 | 22 | def floordiv(a, b): 23 | return a // b 24 | 25 | 26 | def mod(a, b): 27 | return a % b 28 | 29 | 30 | def lshift(a, b): 31 | return a << b 32 | 33 | 34 | def rshift(a, b): 35 | return a >> b 36 | 37 | 38 | def or_(a, b): 39 | return a | b 40 | 41 | 42 | def and_(a, b): 43 | return a & b 44 | 45 | 46 | def xor(a, b): 47 | return a ^ b 48 | 49 | 50 | def logical_implication(a, b): 51 | return (not a) or b 52 | 53 | 54 | def invert(a): 55 | return ~a 56 | 57 | 58 | def _not(a): 59 | return not a 60 | 61 | 62 | def negate(a): 63 | return -a 64 | 65 | 66 | def pos(a): 67 | return +a 68 | 69 | 70 | def _and(*values): 71 | return all(values) 72 | 73 | 74 | def _or(*values): 75 | return any(values) 76 | 77 | 78 | def safe_comb(n, k): 79 | if n > 10000: 80 | raise ValueError("Too large to calculate") 81 | return math.comb(n, k) 82 | 83 | 84 | def safe_factorial(x): 85 | if x > 5000: 86 | raise ValueError("Too large to calculate") 87 | return math.factorial(x) 88 | 89 | 90 | def safe_perm(n, k=None): 91 | if n > 5000: 92 | raise ValueError("Too large to calculate") 93 | return math.perm(n, k) 94 | 95 | 96 | OPERATIONS = { 97 | ast.Add: add, 98 | ast.Sub: sub, 99 | ast.Mult: mul, 100 | ast.Div: truediv, 101 | ast.FloorDiv: floordiv, 102 | ast.Pow: pow, 103 | ast.Mod: mod, 104 | ast.LShift: lshift, 105 | ast.RShift: rshift, 106 | ast.BitOr: or_, 107 | ast.BitAnd: and_, 108 | ast.BitXor: xor, 109 | ast.MatMult: logical_implication, # This is used for the truth command for logical implications 110 | } 111 | 112 | BOOLOPS = { 113 | ast.And: _and, 114 | ast.Or: _or, 115 | } 116 | 117 | UNARYOPS = { 118 | ast.Invert: invert, 119 | ast.Not: _not, 120 | ast.USub: negate, 121 | ast.UAdd: pos, 122 | } 123 | 124 | CONSTANTS = { 125 | "pi": math.pi, 126 | "e": math.e, 127 | "tau": math.tau, 128 | } 129 | 130 | FUNCTIONS = { 131 | "ceil": math.ceil, 132 | "comb": safe_comb, 133 | "fact": safe_factorial, 134 | "gcd": math.gcd, 135 | "lcm": math.lcm, 136 | "perm": safe_perm, 137 | "log": math.log, 138 | "log2": math.log2, 139 | "log10": math.log10, 140 | "sqrt": math.sqrt, 141 | "acos": math.acos, 142 | "asin": math.asin, 143 | "atan": math.atan, 144 | "cos": math.cos, 145 | "sin": math.sin, 146 | "tan": math.tan, 147 | } 148 | 149 | 150 | def bin_float(number: float): 151 | exponent = 0 152 | shifted_num = number 153 | 154 | while shifted_num != int(shifted_num): 155 | shifted_num *= 2 156 | exponent += 1 157 | 158 | if not exponent: 159 | return f"{int(number):b}" 160 | 161 | binary = f"{int(shifted_num):0{exponent + 1}b}" 162 | return f"{binary[:-exponent]}.{binary[-exponent:].rstrip('0')}" 163 | 164 | 165 | def hex_float(number: float): 166 | exponent = 0 167 | shifted_num = number 168 | 169 | while shifted_num != int(shifted_num): 170 | shifted_num *= 16 171 | exponent += 1 172 | 173 | if not exponent: 174 | return f"{int(number):X}" 175 | 176 | hexadecimal = f"{int(shifted_num):0{exponent + 1}X}" 177 | return f"{hexadecimal[:-exponent]}.{hexadecimal[-exponent:]}" 178 | 179 | 180 | def oct_float(number: float): 181 | exponent = 0 182 | shifted_num = number 183 | 184 | while shifted_num != int(shifted_num): 185 | shifted_num *= 8 186 | exponent += 1 187 | 188 | if not exponent: 189 | return f"{int(number):o}" 190 | 191 | octal = f"{int(shifted_num):0{exponent + 1}o}" 192 | return f"{octal[:-exponent]}.{octal[-exponent:]}" 193 | 194 | 195 | def safe_eval(node): 196 | if isinstance(node, ast.Num): 197 | return node.n if isinstance(node.n, int) else Decimal(str(node.n)) 198 | 199 | if isinstance(node, ast.UnaryOp): 200 | return UNARYOPS[node.op.__class__](safe_eval(node.operand)) 201 | 202 | if isinstance(node, ast.BinOp): 203 | left = safe_eval(node.left) 204 | right = safe_eval(node.right) 205 | if isinstance(node.op, ast.Pow) and len(str(left)) * right > 1000: 206 | raise ValueError("Too large to calculate") 207 | return OPERATIONS[node.op.__class__](left, right) 208 | 209 | if isinstance(node, ast.BoolOp): 210 | return BOOLOPS[node.op.__class__](*[safe_eval(value) for value in node.values]) 211 | 212 | if isinstance(node, ast.Compare): 213 | left = safe_eval(node.left) 214 | for op in node.ops: 215 | if not isinstance(op, ast.Eq): 216 | raise ValueError("Calculation failed") 217 | return all(left == safe_eval(comp) for comp in node.comparators) 218 | 219 | if isinstance(node, ast.Name): 220 | return CONSTANTS[node.id] 221 | 222 | if isinstance(node, ast.Call): 223 | return FUNCTIONS[node.func.id](*[safe_eval(arg) for arg in node.args]) 224 | 225 | print(type(node)) 226 | raise ValueError("Calculation failed") 227 | -------------------------------------------------------------------------------- /cogs/utils/color.py: -------------------------------------------------------------------------------- 1 | def hsslv(r, g, b): 2 | """Gets HSL and HSV values from rgb and returns them. 3 | 4 | r: int 5 | g: int 6 | b: int 7 | """ 8 | maxc = max(r, g, b) 9 | minc = min(r, g, b) 10 | sumc = maxc + minc 11 | 12 | rangec = maxc - minc 13 | 14 | lum = sumc / 2.0 15 | 16 | if minc == maxc: 17 | return 0.0, 0.0, 0.0, lum, maxc 18 | sv = rangec / maxc 19 | if lum <= 0.5: 20 | sl = sv 21 | else: 22 | sl = rangec / (2.0 - sumc) 23 | 24 | rc = (maxc - r) / rangec 25 | gc = (maxc - g) / rangec 26 | bc = (maxc - b) / rangec 27 | 28 | if r == maxc: 29 | h = bc - gc 30 | elif g == maxc: 31 | h = 2.0 + rc - bc 32 | else: 33 | h = 4.0 + gc - rc 34 | h = (h / 6.0) % 1.0 35 | 36 | return h, sv, sl, lum, maxc 37 | -------------------------------------------------------------------------------- /cogs/utils/database.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | from decimal import setcontext, Decimal, Context, MAX_EMAX, MAX_PREC, MIN_EMIN 3 | 4 | import orjson 5 | import plyvel 6 | 7 | prefixed_dbs = ( 8 | "infractions", 9 | "karma", 10 | "blacklist", 11 | "rrole", 12 | "deleted", 13 | "edited", 14 | "invites", 15 | "nicks", 16 | "cryptobal", 17 | "crypto", 18 | "stocks", 19 | "stockbal", 20 | "bal", 21 | "wins", 22 | "message_count", 23 | "cookies", 24 | "reminders", 25 | "trivia_wins", 26 | ) 27 | 28 | 29 | class Database: 30 | def __init__(self): 31 | self.main = plyvel.DB( 32 | f"{pathlib.Path(__file__).parent.parent.parent}/db", create_if_missing=True 33 | ) 34 | for db in prefixed_dbs: 35 | setattr(self, db, self.main.prefixed_db(f"{db}-".encode())) 36 | setcontext(Context(prec=MAX_PREC, Emax=MAX_EMAX, Emin=MIN_EMIN)) 37 | 38 | def add_karma(self, member_id: int, amount: int): 39 | """Adds or removes an amount from a members karma. 40 | 41 | member_id: int 42 | amount: int 43 | """ 44 | member_id = str(member_id).encode() 45 | member_karma = self.karma.get(member_id) 46 | 47 | if not member_karma: 48 | member_karma = amount 49 | else: 50 | member_karma = int(member_karma) + amount 51 | 52 | self.karma.put(member_id, str(member_karma).encode()) 53 | 54 | def get_blacklist(self, member_id, guild=None): 55 | """Returns whether someone is blacklisted. 56 | 57 | member_id: int 58 | """ 59 | if state := self.blacklist.get(str(member_id).encode()): 60 | return state 61 | 62 | if guild and (state := self.blacklist.get(f"{guild}-{member_id}".encode())): 63 | return state 64 | 65 | def get_bal(self, member_id: bytes) -> Decimal: 66 | """Gets the balance of an member. 67 | 68 | member_id: bytes 69 | """ 70 | balance = self.bal.get(member_id) 71 | 72 | if balance: 73 | return Decimal(balance.decode()) 74 | 75 | return Decimal(1000.0) 76 | 77 | def put_bal(self, member_id: bytes, balance: Decimal): 78 | """Sets the balance of an member. 79 | 80 | member_id: bytes 81 | balance: Decimal 82 | """ 83 | if balance == balance.to_integral(): 84 | balance = balance.quantize(Decimal(1)) 85 | else: 86 | balance = balance.normalize() 87 | 88 | balance_bytes = f"{balance:50f}".lstrip(" ").encode() 89 | self.bal.put(member_id, balance_bytes or b"0.0") 90 | return balance 91 | 92 | def add_bal(self, member_id: bytes, amount: Decimal): 93 | """Adds to the balance of an member. 94 | 95 | member_id: bytes 96 | amount: Decimal 97 | """ 98 | if amount < 0: 99 | raise ValueError("You can't pay a negative amount") 100 | return self.put_bal(member_id, self.get_bal(member_id) + Decimal(amount)) 101 | 102 | def get_stock(self, symbol: bytes): 103 | """Returns the data of a stock. 104 | 105 | symbol: bytes 106 | """ 107 | stock = self.stocks.get(symbol.encode()) 108 | 109 | if stock: 110 | return orjson.loads(stock) 111 | return None 112 | 113 | def put_stock(self, symbol: bytes, data: dict): 114 | """Sets the data of a stock. 115 | 116 | symbol: bytes 117 | data: dict 118 | """ 119 | self.stocks.put(symbol.encode(), orjson.dumps(data)) 120 | 121 | def get_stockbal(self, member_id: bytes) -> dict | None: 122 | """Returns a members stockbal. 123 | 124 | member_id: bytes 125 | """ 126 | data = self.stockbal.get(member_id) 127 | 128 | if data: 129 | return orjson.loads(data) 130 | return {} 131 | 132 | def put_stockbal(self, member_id: bytes, data: dict): 133 | """Sets a members stockbal. 134 | 135 | member_id: bytes 136 | data: dict 137 | """ 138 | self.stockbal.put(member_id, orjson.dumps(data)) 139 | 140 | def get_crypto(self, symbol: bytes) -> dict | None: 141 | """Returns the data of a crypto. 142 | 143 | symbol: bytes 144 | """ 145 | data = self.crypto.get(symbol.encode()) 146 | 147 | if data: 148 | return orjson.loads(data) 149 | return None 150 | 151 | def put_crypto(self, symbol, data): 152 | """Sets the data of a crypto. 153 | 154 | symbol: bytes 155 | data: dict 156 | """ 157 | data = orjson.dumps(data) 158 | self.crypto.put(symbol.encode(), data) 159 | 160 | def get_cryptobal(self, member_id): 161 | """Returns a members cryptobal. 162 | 163 | member_id: bytes 164 | """ 165 | data = self.cryptobal.get(member_id) 166 | 167 | if data: 168 | return orjson.loads(data) 169 | return {} 170 | 171 | def put_cryptobal(self, member_id, data): 172 | """Sets a members cryptobal. 173 | 174 | member_id: bytes 175 | data: dict 176 | """ 177 | self.cryptobal.put(member_id, orjson.dumps(data)) 178 | -------------------------------------------------------------------------------- /cogs/utils/time.py: -------------------------------------------------------------------------------- 1 | import calendar 2 | import datetime 3 | import re 4 | from math import copysign 5 | 6 | TIME_REGEX = re.compile( 7 | "(?:(?P[0-9])(?:years?|y))?" 8 | "(?:(?P[0-9]{1,2})(?:months?|mo))?" 9 | "(?:(?P[0-9]{1,4})(?:weeks?|w))?" 10 | "(?:(?P[0-9]{1,5})(?:days?|d))?" 11 | "(?:(?P[0-9]{1,5})(?:hours?|h))?" 12 | "(?:(?P[0-9]{1,5})(?:minutes?|m))?" 13 | "(?:(?P[0-9]{1,5})(?:seconds?|s))?", 14 | re.VERBOSE, 15 | ) 16 | 17 | 18 | def parse_date(date: str) -> datetime.datetime: 19 | """Parses a date string. 20 | 21 | >>> parse_date("13-10-2020") 22 | datetime.datetime(2020, 10, 13, 0, 0) 23 | 24 | >>> parse_date("2020-10-13") 25 | datetime.datetime(2020, 10, 13, 0, 0) 26 | 27 | >>> parse_date("13.10.2020") 28 | datetime.datetime(2020, 10, 13, 0, 0) 29 | 30 | >>> parse_date("2020/10/13") 31 | datetime.datetime(2020, 10, 13, 0, 0) 32 | """ 33 | for seperator in ("-", ".", "/"): 34 | if seperator in date: 35 | day, month, year = map(int, date.split(seperator)) 36 | if day > year: 37 | day, year = year, day 38 | return datetime.datetime(year, month, day, tzinfo=datetime.timezone.utc) 39 | 40 | 41 | def parse_time(time_string: str) -> datetime.datetime: 42 | match = TIME_REGEX.fullmatch(time_string.replace(" ", "")) 43 | 44 | if not match: 45 | return None 46 | 47 | data = {k: int(v) for k, v in match.groupdict(default=0).items()} 48 | return relativedelta(**data) + datetime.datetime.now(datetime.timezone.utc) 49 | 50 | 51 | class relativedelta: 52 | def __init__( 53 | self, 54 | years=0, 55 | months=0, 56 | days=0, 57 | leapdays=0, 58 | weeks=0, 59 | hours=0, 60 | minutes=0, 61 | seconds=0, 62 | microseconds=0, 63 | ): 64 | self.years = years 65 | self.months = months 66 | self.days = days + weeks * 7 67 | self.leapdays = leapdays 68 | self.hours = hours 69 | self.minutes = minutes 70 | self.seconds = seconds 71 | self.microseconds = microseconds 72 | 73 | self._fix() 74 | 75 | def _fix(self): 76 | if abs(self.microseconds) > 999999: 77 | s = _sign(self.microseconds) 78 | div, mod = divmod(self.microseconds * s, 1000000) 79 | self.microseconds = mod * s 80 | self.seconds += div * s 81 | if abs(self.seconds) > 59: 82 | s = _sign(self.seconds) 83 | div, mod = divmod(self.seconds * s, 60) 84 | self.seconds = mod * s 85 | self.minutes += div * s 86 | if abs(self.minutes) > 59: 87 | s = _sign(self.minutes) 88 | div, mod = divmod(self.minutes * s, 60) 89 | self.minutes = mod * s 90 | self.hours += div * s 91 | if abs(self.hours) > 23: 92 | s = _sign(self.hours) 93 | div, mod = divmod(self.hours * s, 24) 94 | self.hours = mod * s 95 | self.days += div * s 96 | if abs(self.months) > 11: 97 | s = _sign(self.months) 98 | div, mod = divmod(self.months * s, 12) 99 | self.months = mod * s 100 | self.years += div * s 101 | 102 | def __add__(self, other): 103 | year = other.year + self.years 104 | month = other.month 105 | if self.months: 106 | assert 1 <= abs(self.months) <= 12 107 | month += self.months 108 | if month > 12: 109 | year += 1 110 | month -= 12 111 | elif month < 1: 112 | year -= 1 113 | month += 12 114 | day = min(calendar.monthrange(year, month)[1], other.day) 115 | days = self.days 116 | if self.leapdays and month > 2 and calendar.isleap(year): 117 | days += self.leapdays 118 | return other.replace(year=year, month=month, day=day) + datetime.timedelta( 119 | days=days, 120 | hours=self.hours, 121 | minutes=self.minutes, 122 | seconds=self.seconds, 123 | microseconds=self.microseconds, 124 | ) 125 | 126 | 127 | def _sign(x): 128 | return int(copysign(1, x)) 129 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | git+https://github.com/Pycord-Development/pycord 2 | aiohttp[speedups] 3 | lxml 4 | psutil 5 | yt-dlp 6 | orjson 7 | plyvel-wheels 8 | PyNaCl -------------------------------------------------------------------------------- /run_tests.py: -------------------------------------------------------------------------------- 1 | SKIP_API_TESTS = True 2 | SKIP_IMAGE_TESTS = True 3 | 4 | if __name__ == "__main__": 5 | import unittest 6 | 7 | loader = unittest.TestLoader() 8 | 9 | runner = unittest.TextTestRunner() 10 | 11 | runner.run(loader.discover("tests/")) 12 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Singularitat/snakebot/7a24219b41176b79f1370333b0fac3352fce2ef7/tests/__init__.py -------------------------------------------------------------------------------- /tests/helpers.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import collections 4 | import itertools 5 | import logging 6 | import unittest.mock 7 | from asyncio import AbstractEventLoop 8 | from typing import Iterable, Optional 9 | 10 | import discord 11 | from discord.ext.commands import Bot, Context 12 | 13 | Bot.user = None 14 | 15 | for logger in logging.Logger.manager.loggerDict.values(): 16 | if not isinstance(logger, logging.Logger): 17 | continue 18 | 19 | logger.setLevel(logging.CRITICAL) 20 | 21 | 22 | class HashableMixin(discord.mixins.EqualityComparable): 23 | def __hash__(self): 24 | return self.id 25 | 26 | 27 | class ColourMixin: 28 | @property 29 | def color(self) -> discord.Colour: 30 | return self.colour 31 | 32 | @color.setter 33 | def color(self, color: discord.Colour) -> None: 34 | self.colour = color 35 | 36 | 37 | class CustomMockMixin: 38 | child_mock_type = unittest.mock.MagicMock 39 | discord_id = itertools.count(0) 40 | spec_set = None 41 | additional_spec_asyncs = None 42 | 43 | def __init__(self, **kwargs): 44 | name = kwargs.pop("name", None) 45 | super().__init__(spec_set=self.spec_set, **kwargs) 46 | 47 | if self.additional_spec_asyncs: 48 | self._spec_asyncs.extend(self.additional_spec_asyncs) 49 | 50 | if name: 51 | self.name = name 52 | 53 | def _get_child_mock(self, **kw): 54 | _new_name = kw.get("_new_name") 55 | if _new_name in self.__dict__["_spec_asyncs"]: 56 | return unittest.mock.AsyncMock(**kw) 57 | 58 | _type = type(self) 59 | if ( 60 | issubclass(_type, unittest.mock.MagicMock) 61 | and _new_name in unittest.mock._async_method_magics 62 | ): 63 | klass = unittest.mock.AsyncMock 64 | else: 65 | klass = self.child_mock_type 66 | 67 | if self._mock_sealed: 68 | attribute = "." + kw["name"] if "name" in kw else "()" 69 | mock_name = self._extract_mock_name() + attribute 70 | raise AttributeError(mock_name) 71 | 72 | return klass(**kw) 73 | 74 | 75 | guild_data = { 76 | "id": 1, 77 | "name": "guild", 78 | "verification_level": 2, 79 | "default_notications": 1, 80 | "afk_timeout": 100, 81 | "icon": "icon.png", 82 | "banner": "banner.png", 83 | "mfa_level": 1, 84 | "splash": "splash.png", 85 | "system_channel_id": 464033278631084042, 86 | "description": "Go Away", 87 | "max_presences": 10_000, 88 | "max_members": 100_000, 89 | "preferred_locale": "UTC", 90 | "owner_id": 1, 91 | "afk_channel_id": 464033278631084042, 92 | } 93 | guild_instance = discord.Guild(data=guild_data, state=unittest.mock.MagicMock()) 94 | 95 | 96 | class MockGuild(CustomMockMixin, unittest.mock.Mock, HashableMixin): 97 | spec_set = guild_instance 98 | 99 | def __init__(self, roles: Optional[Iterable[MockRole]] = None, **kwargs) -> None: 100 | default_kwargs = {"id": next(self.discord_id), "members": []} 101 | super().__init__(**collections.ChainMap(kwargs, default_kwargs)) 102 | 103 | self.created_at.timestamp = unittest.mock.Mock(return_value=0) 104 | self.member_count = 52899 105 | 106 | self.roles = [MockRole(name="@everyone", position=1, id=0)] 107 | if roles: 108 | self.roles.extend(roles) 109 | self.me = MockMember() 110 | 111 | 112 | role_data = {"name": "role", "id": 1} 113 | role_instance = discord.Role( 114 | guild=guild_instance, state=unittest.mock.MagicMock(), data=role_data 115 | ) 116 | 117 | 118 | class MockRole(CustomMockMixin, unittest.mock.Mock, ColourMixin, HashableMixin): 119 | spec_set = role_instance 120 | 121 | def __init__(self, **kwargs) -> None: 122 | default_kwargs = { 123 | "id": next(self.discord_id), 124 | "name": "role", 125 | "position": 1, 126 | "colour": discord.Colour(0xDEADBF), 127 | "permissions": discord.Permissions(), 128 | } 129 | super().__init__(**collections.ChainMap(kwargs, default_kwargs)) 130 | 131 | if isinstance(self.colour, int): 132 | self.colour = discord.Colour(self.colour) 133 | 134 | if isinstance(self.permissions, int): 135 | self.permissions = discord.Permissions(self.permissions) 136 | 137 | if "mention" not in kwargs: 138 | self.mention = f"&{self.name}" 139 | 140 | def __lt__(self, other): 141 | return self.position < other.position 142 | 143 | def __ge__(self, other): 144 | return self.position >= other.position 145 | 146 | 147 | member_data = {"user": "lemon", "roles": [1]} 148 | state_mock = unittest.mock.MagicMock() 149 | member_instance = discord.Member( 150 | data=member_data, guild=guild_instance, state=state_mock 151 | ) 152 | 153 | 154 | class MockMember(CustomMockMixin, unittest.mock.Mock, ColourMixin, HashableMixin): 155 | spec_set = member_instance 156 | 157 | def __init__(self, roles: Optional[Iterable[MockRole]] = None, **kwargs) -> None: 158 | default_kwargs = { 159 | "name": "member", 160 | "id": next(self.discord_id), 161 | "bot": False, 162 | "pending": False, 163 | "color": discord.Color.random(), 164 | } 165 | super().__init__(**collections.ChainMap(kwargs, default_kwargs)) 166 | 167 | self.created_at.timestamp = unittest.mock.Mock(return_value=0) 168 | self.joined_at.timestamp = unittest.mock.Mock(return_value=0) 169 | 170 | self.roles = [MockRole(name="@everyone", position=1, id=0)] 171 | if roles: 172 | self.roles.extend(roles) 173 | 174 | if "mention" not in kwargs: 175 | self.mention = f"@{self.name}" 176 | 177 | 178 | _user_data_mock = collections.defaultdict(unittest.mock.MagicMock, {"accent_color": 0}) 179 | user_instance = discord.User( 180 | data=unittest.mock.MagicMock( 181 | get=unittest.mock.Mock(side_effect=_user_data_mock.get) 182 | ), 183 | state=unittest.mock.MagicMock(), 184 | ) 185 | 186 | 187 | class MockUser(CustomMockMixin, unittest.mock.Mock, ColourMixin, HashableMixin): 188 | spec_set = user_instance 189 | 190 | def __init__(self, **kwargs) -> None: 191 | default_kwargs = {"name": "user", "id": next(self.discord_id), "bot": False} 192 | super().__init__(**collections.ChainMap(kwargs, default_kwargs)) 193 | 194 | if "mention" not in kwargs: 195 | self.mention = f"@{self.name}" 196 | 197 | 198 | def _get_mock_loop() -> unittest.mock.Mock: 199 | loop = unittest.mock.create_autospec(spec=AbstractEventLoop, spec_set=True) 200 | loop.create_task.side_effect = lambda coroutine: coroutine.close() 201 | return loop 202 | 203 | 204 | class MockBot(CustomMockMixin, unittest.mock.MagicMock): 205 | spec_set = Bot( 206 | command_prefix=".", 207 | loop=_get_mock_loop(), 208 | ) 209 | additional_spec_asyncs = "wait_for" 210 | 211 | def __init__(self, **kwargs) -> None: 212 | super().__init__(**kwargs) 213 | 214 | self.loop = _get_mock_loop() 215 | 216 | 217 | channel_data = { 218 | "id": 1, 219 | "type": "TextChannel", 220 | "name": "channel", 221 | "parent_id": 1234567890, 222 | "topic": "topic", 223 | "position": 1, 224 | "nsfw": False, 225 | "last_message_id": 1, 226 | } 227 | state = unittest.mock.MagicMock() 228 | guild = unittest.mock.MagicMock() 229 | text_channel_instance = discord.TextChannel(state=state, guild=guild, data=channel_data) 230 | 231 | channel_data["type"] = "VoiceChannel" 232 | voice_channel_instance = discord.VoiceChannel( 233 | state=state, guild=guild, data=channel_data 234 | ) 235 | 236 | 237 | class MockTextChannel(CustomMockMixin, unittest.mock.Mock, HashableMixin): 238 | spec_set = text_channel_instance 239 | 240 | def __init__(self, **kwargs) -> None: 241 | default_kwargs = { 242 | "id": next(self.discord_id), 243 | "name": "channel", 244 | "guild": MockGuild(), 245 | } 246 | super().__init__(**collections.ChainMap(kwargs, default_kwargs)) 247 | 248 | if "mention" not in kwargs: 249 | self.mention = f"#{self.name}" 250 | 251 | 252 | class MockVoiceChannel(CustomMockMixin, unittest.mock.Mock, HashableMixin): 253 | spec_set = voice_channel_instance 254 | 255 | def __init__(self, **kwargs) -> None: 256 | default_kwargs = { 257 | "id": next(self.discord_id), 258 | "name": "channel", 259 | "guild": MockGuild(), 260 | } 261 | super().__init__(**collections.ChainMap(kwargs, default_kwargs)) 262 | 263 | if "mention" not in kwargs: 264 | self.mention = f"#{self.name}" 265 | 266 | 267 | state = unittest.mock.MagicMock() 268 | me = unittest.mock.MagicMock() 269 | dm_channel_data = {"id": 1, "recipients": [unittest.mock.MagicMock()]} 270 | dm_channel_instance = discord.DMChannel(me=me, state=state, data=dm_channel_data) 271 | 272 | 273 | class MockDMChannel(CustomMockMixin, unittest.mock.Mock, HashableMixin): 274 | spec_set = dm_channel_instance 275 | 276 | def __init__(self, **kwargs) -> None: 277 | default_kwargs = { 278 | "id": next(self.discord_id), 279 | "recipient": MockUser(), 280 | "me": MockUser(), 281 | } 282 | super().__init__(**collections.ChainMap(kwargs, default_kwargs)) 283 | 284 | 285 | category_channel_data = { 286 | "id": 1, 287 | "type": discord.ChannelType.category, 288 | "name": "category", 289 | "position": 1, 290 | } 291 | 292 | state = unittest.mock.MagicMock() 293 | guild = unittest.mock.MagicMock() 294 | category_channel_instance = discord.CategoryChannel( 295 | state=state, guild=guild, data=category_channel_data 296 | ) 297 | 298 | 299 | class MockCategoryChannel(CustomMockMixin, unittest.mock.Mock, HashableMixin): 300 | def __init__(self, **kwargs) -> None: 301 | default_kwargs = {"id": next(self.discord_id)} 302 | super().__init__(**collections.ChainMap(default_kwargs, kwargs)) 303 | 304 | 305 | message_data = { 306 | "id": 1, 307 | "webhook_id": 431341013479718912, 308 | "attachments": [], 309 | "embeds": [], 310 | "application": "Bot Testing", 311 | "activity": "mocking", 312 | "channel": unittest.mock.MagicMock(), 313 | "edited_timestamp": "2020-10-14T15:33:48+00:00", 314 | "type": "message", 315 | "pinned": False, 316 | "mention_everyone": False, 317 | "tts": None, 318 | "content": "content", 319 | "nonce": None, 320 | } 321 | state = unittest.mock.MagicMock() 322 | channel = unittest.mock.MagicMock() 323 | message_instance = discord.Message(state=state, channel=channel, data=message_data) 324 | 325 | 326 | context_instance = Context( 327 | message=unittest.mock.MagicMock(), 328 | prefix=".", 329 | bot=MockBot(), 330 | view=unittest.mock.MagicMock(), 331 | ) 332 | context_instance.invoked_from_error_handler = None 333 | 334 | 335 | class MockContext(CustomMockMixin, unittest.mock.MagicMock): 336 | spec_set = context_instance 337 | 338 | def __init__(self, **kwargs) -> None: 339 | super().__init__(**kwargs) 340 | self.bot = kwargs.get("bot", MockBot()) 341 | self.guild = kwargs.get("guild", MockGuild()) 342 | self.author = kwargs.get("author", MockMember()) 343 | self.channel = kwargs.get("channel", MockTextChannel()) 344 | self.message = kwargs.get("message", MockMessage()) 345 | self.invoked_from_error_handler = kwargs.get( 346 | "invoked_from_error_handler", False 347 | ) 348 | 349 | 350 | attachment_instance = discord.Attachment( 351 | data=unittest.mock.MagicMock(id=1), state=unittest.mock.MagicMock() 352 | ) 353 | 354 | 355 | class MockAttachment(CustomMockMixin, unittest.mock.MagicMock): 356 | spec_set = attachment_instance 357 | 358 | def __init__(self, **kwargs) -> None: 359 | super().__init__(**kwargs) 360 | if "url" in kwargs: 361 | self.url = kwargs["url"] 362 | 363 | 364 | class MockMessage(CustomMockMixin, unittest.mock.MagicMock): 365 | spec_set = message_instance 366 | 367 | def __init__(self, **kwargs) -> None: 368 | default_kwargs = {"attachments": []} 369 | super().__init__(**collections.ChainMap(kwargs, default_kwargs)) 370 | self.author = kwargs.get("author", MockMember()) 371 | self.channel = kwargs.get("channel", MockTextChannel()) 372 | 373 | 374 | emoji_data = {"require_colons": True, "managed": True, "id": 1, "name": "hyperlemon"} 375 | emoji_instance = discord.Emoji( 376 | guild=MockGuild(), state=unittest.mock.MagicMock(), data=emoji_data 377 | ) 378 | 379 | 380 | class MockEmoji(CustomMockMixin, unittest.mock.MagicMock): 381 | spec_set = emoji_instance 382 | 383 | def __init__(self, **kwargs) -> None: 384 | super().__init__(**kwargs) 385 | self.guild = kwargs.get("guild", MockGuild()) 386 | 387 | 388 | partial_emoji_instance = discord.PartialEmoji(animated=False, name="guido") 389 | 390 | 391 | class MockPartialEmoji(CustomMockMixin, unittest.mock.MagicMock): 392 | spec_set = partial_emoji_instance 393 | 394 | 395 | reaction_instance = discord.Reaction( 396 | message=MockMessage(), data={"me": True}, emoji=MockEmoji() 397 | ) 398 | 399 | 400 | class MockReaction(CustomMockMixin, unittest.mock.MagicMock): 401 | spec_set = reaction_instance 402 | 403 | def __init__(self, **kwargs) -> None: 404 | _users = kwargs.pop("users", []) 405 | super().__init__(**kwargs) 406 | self.emoji = kwargs.get("emoji", MockEmoji()) 407 | self.message = kwargs.get("message", MockMessage()) 408 | 409 | user_iterator = unittest.mock.AsyncMock() 410 | user_iterator.__aiter__.return_value = _users 411 | self.users.return_value = user_iterator 412 | 413 | self.__str__.return_value = str(self.emoji) 414 | -------------------------------------------------------------------------------- /tests/test_helpers.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import unittest 3 | import unittest.mock 4 | 5 | import discord 6 | import helpers 7 | 8 | 9 | class DiscordMocksTests(unittest.TestCase): 10 | def test_mock_role_default_initialization(self): 11 | role = helpers.MockRole() 12 | 13 | # The `spec` argument makes sure `isistance` checks with `discord.Role` pass 14 | self.assertIsInstance(role, discord.Role) 15 | 16 | self.assertEqual(role.name, "role") 17 | self.assertEqual(role.position, 1) 18 | self.assertEqual(role.mention, "&role") 19 | 20 | def test_mock_role_alternative_arguments(self): 21 | role = helpers.MockRole( 22 | name="Admins", 23 | id=90210, 24 | position=10, 25 | ) 26 | 27 | self.assertEqual(role.name, "Admins") 28 | self.assertEqual(role.id, 90210) 29 | self.assertEqual(role.position, 10) 30 | self.assertEqual(role.mention, "&Admins") 31 | 32 | def test_mock_role_accepts_dynamic_arguments(self): 33 | role = helpers.MockRole( 34 | guild="Dino Man", 35 | hoist=True, 36 | ) 37 | 38 | self.assertEqual(role.guild, "Dino Man") 39 | self.assertTrue(role.hoist) 40 | 41 | def test_mock_role_uses_position_for_less_than_greater_than(self): 42 | role_one = helpers.MockRole(position=1) 43 | role_two = helpers.MockRole(position=2) 44 | role_three = helpers.MockRole(position=3) 45 | 46 | self.assertLess(role_one, role_two) 47 | self.assertLess(role_one, role_three) 48 | self.assertLess(role_two, role_three) 49 | self.assertGreater(role_three, role_two) 50 | self.assertGreater(role_three, role_one) 51 | self.assertGreater(role_two, role_one) 52 | 53 | def test_mock_member_default_initialization(self): 54 | member = helpers.MockMember() 55 | 56 | # The `spec` argument makes sure `isistance` checks with `discord.Member` pass 57 | self.assertIsInstance(member, discord.Member) 58 | 59 | self.assertEqual(member.name, "member") 60 | self.assertListEqual( 61 | member.roles, [helpers.MockRole(name="@everyone", position=1, id=0)] 62 | ) 63 | self.assertEqual(member.mention, "@member") 64 | 65 | def test_mock_member_alternative_arguments(self): 66 | core_developer = helpers.MockRole(name="Core Developer", position=2) 67 | member = helpers.MockMember(name="Mark", id=12345, roles=[core_developer]) 68 | 69 | self.assertEqual(member.name, "Mark") 70 | self.assertEqual(member.id, 12345) 71 | self.assertListEqual( 72 | member.roles, 73 | [helpers.MockRole(name="@everyone", position=1, id=0), core_developer], 74 | ) 75 | self.assertEqual(member.mention, "@Mark") 76 | 77 | def test_mock_member_accepts_dynamic_arguments(self): 78 | member = helpers.MockMember( 79 | nick="Dino Man", 80 | colour=discord.Colour.default(), 81 | ) 82 | 83 | self.assertEqual(member.nick, "Dino Man") 84 | self.assertEqual(member.colour, discord.Colour.default()) 85 | 86 | def test_mock_guild_default_initialization(self): 87 | guild = helpers.MockGuild() 88 | 89 | # The `spec` argument makes sure `isistance` checks with `discord.Guild` pass 90 | self.assertIsInstance(guild, discord.Guild) 91 | 92 | self.assertListEqual( 93 | guild.roles, [helpers.MockRole(name="@everyone", position=1, id=0)] 94 | ) 95 | self.assertListEqual(guild.members, []) 96 | 97 | def test_mock_guild_alternative_arguments(self): 98 | core_developer = helpers.MockRole(name="Core Developer", position=2) 99 | guild = helpers.MockGuild( 100 | roles=[core_developer], 101 | members=[helpers.MockMember(id=54321)], 102 | ) 103 | 104 | self.assertListEqual( 105 | guild.roles, 106 | [helpers.MockRole(name="@everyone", position=1, id=0), core_developer], 107 | ) 108 | self.assertListEqual(guild.members, [helpers.MockMember(id=54321)]) 109 | 110 | def test_mock_guild_accepts_dynamic_arguments(self): 111 | guild = helpers.MockGuild( 112 | emojis=(":hyperjoseph:", ":pensive_ela:"), 113 | premium_subscription_count=15, 114 | ) 115 | 116 | self.assertTupleEqual(guild.emojis, (":hyperjoseph:", ":pensive_ela:")) 117 | self.assertEqual(guild.premium_subscription_count, 15) 118 | 119 | def test_mock_bot_default_initialization(self): 120 | bot = helpers.MockBot() 121 | 122 | # The `spec` argument makes sure `isistance` checks with `discord.ext.commands.Bot` pass 123 | self.assertIsInstance(bot, discord.ext.commands.Bot) 124 | 125 | def test_mock_context_default_initialization(self): 126 | context = helpers.MockContext() 127 | 128 | # The `spec` argument makes sure `isistance` checks with `discord.ext.commands.Context` pass 129 | self.assertIsInstance(context, discord.ext.commands.Context) 130 | 131 | self.assertIsInstance(context.bot, helpers.MockBot) 132 | self.assertIsInstance(context.guild, helpers.MockGuild) 133 | self.assertIsInstance(context.author, helpers.MockMember) 134 | 135 | def test_mocks_allows_access_to_attributes_part_of_spec(self): 136 | mocks = ( 137 | (helpers.MockGuild(), "name"), 138 | (helpers.MockRole(), "hoist"), 139 | (helpers.MockMember(), "display_name"), 140 | (helpers.MockBot(), "user"), 141 | (helpers.MockContext(), "invoked_with"), 142 | (helpers.MockTextChannel(), "last_message"), 143 | (helpers.MockMessage(), "mention_everyone"), 144 | ) 145 | 146 | for mock, valid_attribute in mocks: 147 | with self.subTest(mock=mock): 148 | try: 149 | getattr(mock, valid_attribute) 150 | except AttributeError: 151 | msg = f"accessing valid attribute `{valid_attribute}` raised an AttributeError" 152 | self.fail(msg) 153 | 154 | @unittest.mock.patch(f"{__name__}.DiscordMocksTests.subTest") 155 | @unittest.mock.patch(f"{__name__}.getattr") 156 | def test_mock_allows_access_to_attributes_test(self, mock_getattr, mock_subtest): 157 | mock_getattr.side_effect = AttributeError 158 | 159 | msg = "accessing valid attribute `name` raised an AttributeError" 160 | with self.assertRaises(AssertionError, msg=msg): 161 | self.test_mocks_allows_access_to_attributes_part_of_spec() 162 | 163 | def test_mocks_rejects_access_to_attributes_not_part_of_spec(self): 164 | mocks = ( 165 | helpers.MockGuild(), 166 | helpers.MockRole(), 167 | helpers.MockMember(), 168 | helpers.MockBot(), 169 | helpers.MockContext(), 170 | helpers.MockTextChannel(), 171 | helpers.MockMessage(), 172 | ) 173 | 174 | for mock in mocks: 175 | with self.subTest(mock=mock), self.assertRaises(AttributeError): 176 | mock.the_cake_is_a_lie 177 | 178 | def test_mocks_use_mention_when_provided_as_kwarg(self): 179 | test_cases = ( 180 | (helpers.MockRole, "role mention"), 181 | (helpers.MockMember, "member mention"), 182 | (helpers.MockTextChannel, "channel mention"), 183 | ) 184 | 185 | for mock_type, mention in test_cases: 186 | with self.subTest(mock_type=mock_type, mention=mention): 187 | mock = mock_type(mention=mention) 188 | self.assertEqual(mock.mention, mention) 189 | 190 | def test_create_test_on_mock_bot_closes_passed_coroutine(self): 191 | async def dementati(): 192 | """Dummy coroutine for testing purposes.""" 193 | 194 | coroutine_object = dementati() 195 | 196 | bot = helpers.MockBot() 197 | bot.loop.create_task(coroutine_object) 198 | with self.assertRaises( 199 | RuntimeError, msg="cannot reuse already awaited coroutine" 200 | ): 201 | asyncio.run(coroutine_object) 202 | 203 | def test_user_mock_uses_explicitly_passed_mention_attribute(self): 204 | user = helpers.MockUser(mention="hello") 205 | self.assertEqual(user.mention, "hello") 206 | 207 | 208 | class MockObjectTests(unittest.TestCase): 209 | @classmethod 210 | def setUpClass(cls): 211 | cls.hashable_mocks = (helpers.MockRole, helpers.MockMember, helpers.MockGuild) 212 | 213 | def test_colour_mixin(self): 214 | class MockHemlock(unittest.mock.MagicMock, helpers.ColourMixin): 215 | pass 216 | 217 | hemlock = MockHemlock() 218 | hemlock.color = 1 219 | self.assertEqual(hemlock.colour, 1) 220 | self.assertEqual(hemlock.colour, hemlock.color) 221 | 222 | def test_hashable_mixin_hash_returns_id(self): 223 | class MockScragly(unittest.mock.Mock, helpers.HashableMixin): 224 | pass 225 | 226 | scragly = MockScragly() 227 | scragly.id = 10 228 | self.assertEqual(hash(scragly), scragly.id) 229 | 230 | def test_hashable_mixin_uses_id_for_equality_comparison(self): 231 | class MockScragly(helpers.HashableMixin): 232 | pass 233 | 234 | scragly = MockScragly() 235 | scragly.id = 10 236 | eevee = MockScragly() 237 | eevee.id = 10 238 | python = MockScragly() 239 | python.id = 20 240 | 241 | self.assertTrue(scragly == eevee) 242 | self.assertFalse(scragly == python) 243 | 244 | def test_hashable_mixin_uses_id_for_nonequality_comparison(self): 245 | class MockScragly(helpers.HashableMixin): 246 | pass 247 | 248 | scragly = MockScragly() 249 | scragly.id = 10 250 | eevee = MockScragly() 251 | eevee.id = 10 252 | python = MockScragly() 253 | python.id = 20 254 | 255 | self.assertTrue(scragly != python) 256 | self.assertFalse(scragly != eevee) 257 | 258 | def test_mock_class_with_hashable_mixin_uses_id_for_hashing(self): 259 | for mock in self.hashable_mocks: 260 | with self.subTest(mock_class=mock): 261 | instance = helpers.MockRole(id=100) 262 | self.assertEqual(hash(instance), instance.id) 263 | 264 | def test_mock_class_with_hashable_mixin_uses_id_for_equality(self): 265 | for mock_class in self.hashable_mocks: 266 | with self.subTest(mock_class=mock_class): 267 | instance_one = mock_class() 268 | instance_two = mock_class() 269 | instance_three = mock_class() 270 | 271 | instance_one.id = 10 272 | instance_two.id = 10 273 | instance_three.id = 20 274 | 275 | self.assertTrue(instance_one == instance_two) 276 | self.assertFalse(instance_one == instance_three) 277 | 278 | def test_mock_class_with_hashable_mixin_uses_id_for_nonequality(self): 279 | for mock_class in self.hashable_mocks: 280 | with self.subTest(mock_class=mock_class): 281 | instance_one = mock_class() 282 | instance_two = mock_class() 283 | instance_three = mock_class() 284 | 285 | instance_one.id = 10 286 | instance_two.id = 10 287 | instance_three.id = 20 288 | 289 | self.assertFalse(instance_one != instance_two) 290 | self.assertTrue(instance_one != instance_three) 291 | 292 | def test_custom_mock_mixin_accepts_mock_seal(self): 293 | class MyMock(helpers.CustomMockMixin, unittest.mock.MagicMock): 294 | child_mock_type = unittest.mock.MagicMock 295 | 296 | mock = MyMock() 297 | unittest.mock.seal(mock) 298 | with self.assertRaises(AttributeError, msg="MyMock.shirayuki"): 299 | mock.shirayuki = "hello!" 300 | 301 | def test_spec_propagation_of_mock_subclasses(self): 302 | test_values = ( 303 | (helpers.MockRole, "mentionable"), 304 | (helpers.MockMember, "display_name"), 305 | (helpers.MockBot, "owner_id"), 306 | (helpers.MockContext, "command_failed"), 307 | (helpers.MockMessage, "mention_everyone"), 308 | (helpers.MockEmoji, "managed"), 309 | (helpers.MockPartialEmoji, "url"), 310 | (helpers.MockReaction, "me"), 311 | ) 312 | 313 | for mock_type, valid_attribute in test_values: 314 | with self.subTest(mock_type=mock_type, attribute=valid_attribute): 315 | mock = mock_type() 316 | self.assertTrue(isinstance(mock, mock_type)) 317 | attribute = getattr(mock, valid_attribute) 318 | self.assertTrue(isinstance(attribute, mock_type.child_mock_type)) 319 | 320 | def test_custom_mock_mixin_mocks_async_magic_methods_with_async_mock(self): 321 | class MyMock(helpers.CustomMockMixin, unittest.mock.MagicMock): 322 | pass 323 | 324 | mock = MyMock() 325 | self.assertIsInstance(mock.__aenter__, unittest.mock.AsyncMock) 326 | 327 | 328 | if __name__ == "__main__": 329 | unittest.main() 330 | --------------------------------------------------------------------------------