├── .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 |
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[0m"
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[2;32m{allowed}[0m```")
227 | embed.add_field(name="Denied", value=f"```ansi\n[2;31m{denied}[0m```")
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[0m"
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 |
--------------------------------------------------------------------------------