├── Procfile ├── alembic ├── README ├── script.py.mako ├── env.py └── versions │ └── b6b145d0b35d_.py ├── utils ├── data │ ├── Roboto-Bold.ttf │ ├── Roboto-Italic.ttf │ ├── images │ │ ├── rankcard.png │ │ ├── winterrankcard.png │ │ └── halloweenrankcard.png │ ├── opensans-semibold.ttf │ └── releases.json ├── checks.py ├── http.py ├── formats.py ├── assorted.py ├── config.py ├── cache.py ├── exceptions.py ├── rankcard.py ├── wrappers │ └── OpenWeatherMap.py ├── shortcuts.py ├── bot.py ├── db_models.py ├── timeconversions.py └── pagination.py ├── MANIFEST.in ├── .dockerignore ├── .gitignore ├── main.py ├── Dockerfile ├── requirements.txt ├── secrets.env.template ├── .github └── FUNDING.yml ├── LICENSE ├── cogs ├── Password.py ├── AI.py ├── Help.py ├── Search.py ├── Lewis.py ├── Starboard.py ├── Memes.py ├── Redditbot.py ├── Code.py ├── Snipe.py ├── Github.py ├── Welcome.py ├── Math.py ├── Timezone.py ├── Animals.py ├── Info.py ├── Stats.py ├── Support.py ├── Error_handler.py ├── Image.py ├── Tickets.py ├── Birthdays.py ├── Developer.py └── Blacklist.py ├── docker-compose.yml.template ├── README.md ├── alembic.ini └── CONTRIBUTING.md /Procfile: -------------------------------------------------------------------------------- 1 | worker: python main.py 2 | -------------------------------------------------------------------------------- /alembic/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration with an async dbapi. -------------------------------------------------------------------------------- /utils/data/Roboto-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LewisProjects/Ogiroid/HEAD/utils/data/Roboto-Bold.ttf -------------------------------------------------------------------------------- /utils/data/Roboto-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LewisProjects/Ogiroid/HEAD/utils/data/Roboto-Italic.ttf -------------------------------------------------------------------------------- /utils/data/images/rankcard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LewisProjects/Ogiroid/HEAD/utils/data/images/rankcard.png -------------------------------------------------------------------------------- /utils/data/opensans-semibold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LewisProjects/Ogiroid/HEAD/utils/data/opensans-semibold.ttf -------------------------------------------------------------------------------- /utils/data/images/winterrankcard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LewisProjects/Ogiroid/HEAD/utils/data/images/winterrankcard.png -------------------------------------------------------------------------------- /utils/data/images/halloweenrankcard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LewisProjects/Ogiroid/HEAD/utils/data/images/halloweenrankcard.png -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include LICENSE 3 | include requirements.txt 4 | include discord/bin/*.dll 5 | include discord/py.typed 6 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | *.log 3 | *.egg-info 4 | venv 5 | .venv 6 | docs/_build 7 | docs/crowdin.py 8 | *.buildinfo 9 | *.mp3 10 | *.m4a 11 | *.wav 12 | *.png 13 | *.flac 14 | *.mo 15 | /.coverage 16 | build/* 17 | *.db 18 | .idea 19 | __pycache__/ -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | *.log 3 | *.egg-info 4 | venv 5 | .venv 6 | docs/_build 7 | docs/crowdin.py 8 | *.buildinfo 9 | *.mp3 10 | *.m4a 11 | *.wav 12 | *.png 13 | *.jpg 14 | *.flac 15 | *.mo 16 | /.coverage 17 | build/* 18 | *.db 19 | .idea 20 | .env 21 | *.env 22 | __pycache__/ 23 | -------------------------------------------------------------------------------- /utils/checks.py: -------------------------------------------------------------------------------- 1 | from disnake import ApplicationCommandInteraction 2 | from disnake.ext.commands import check 3 | 4 | 5 | def is_dev(): 6 | devs = [ 7 | 511724576674414600, 8 | 662656158129192961, 9 | 963860161976467498, 10 | 627450451297566733, 11 | ] 12 | 13 | def predicate(inter: ApplicationCommandInteraction): 14 | return inter.author.id in devs 15 | 16 | return check(predicate) 17 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from dotenv import load_dotenv 4 | 5 | load_dotenv("secrets.env") 6 | from utils.bot import OGIROID 7 | 8 | bot = OGIROID() 9 | TOKEN = bot.config.tokens.bot 10 | 11 | 12 | def main(): 13 | for filename in os.listdir("./cogs"): 14 | if filename.endswith(".py"): 15 | bot.load_extension(f"cogs.{filename[:-3]}") 16 | bot.run(TOKEN) 17 | 18 | 19 | if __name__ == "__main__": 20 | main() 21 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.11-slim 2 | LABEL org.opencontainers.image.authors="Jason Cameron , Levani Vashadze " 3 | LABEL org.opencontainers.image.source="https://github.com/LewisProjects/Ogiroid" 4 | ENV PYTHONDONTWRITEBYTECODE 1 5 | ENV PYTHONUNBUFFERED 1 6 | COPY requirements.txt /app/ 7 | WORKDIR /app 8 | RUN pip install -r requirements.txt --no-cache-dir 9 | COPY . . 10 | CMD ["python3", "-O", "main.py"] 11 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp~=3.11.18 2 | asyncpg==0.29.0 3 | akinator~=1.1.1 4 | disnake==2.9.1 5 | parsedatetime==2.6 6 | python-dotenv==0.21.1 7 | requests==2.32.2 8 | textdistance~=4.5.0 9 | aiocache==0.12.0 10 | Pillow==10.3.0 11 | expr.py~=0.3.0 12 | cachetools==5.3.0 13 | python-dateutil~=2.8.2 14 | pytz==2022.7.1 15 | discord_together==1.2.6 16 | better_profanity==0.7.0 17 | sqlalchemy[asyncio]==2.0.23 18 | alembic==1.13.1 19 | matplotlib==3.8.2 20 | alembic==1.13.1 21 | -------------------------------------------------------------------------------- /secrets.env.template: -------------------------------------------------------------------------------- 1 | # Insert a bots token 2 | TOKEN= 3 | # needed for one command can be ignored in development 4 | SRA_API_TOKEN= 5 | # Used for weather command can be ignored in development 6 | OPEN_WEATHER_MAP_API_KEY= 7 | # Used for the upload checker can be ignored in development 8 | YT_API_KEY= 9 | # for db 10 | POSTGRES_CONNECTION_STRING= 11 | # Needs to be set to true or false 12 | DEVELOPMENT= 13 | # for currency command, can be ignored in development(exchangerates.org)(APP_ID) 14 | CURRENCY_API_KEY= 15 | # for ai command, can be ignored in development 16 | HUGGINGFACE_API_KEY= 17 | # for the image to text reddit bot support, can be found in https://api-ninjas.com/profile 18 | API-NINJAS-KEY= -------------------------------------------------------------------------------- /utils/data/releases.json: -------------------------------------------------------------------------------- 1 | { 2 | "1.6.0": { 3 | "contributors": [ 4 | "JasonLovesDoggo", 5 | "ImmaHarry", 6 | "LevaniVashadze", 7 | "FreebieII", 8 | "DWAA1660" 9 | ], 10 | "date": "2022-08-07", 11 | "description": "This is the second release of OGIROID with a massive new set of features with are listed in the changelog", 12 | "changelog": [ 13 | "Revamped Tag system", 14 | "Blacklist system", 15 | "Tickets system", 16 | "Flag quiz system", 17 | "polls", 18 | "tons of fun and assorted commands" 19 | ], 20 | "commands": [ 21 | "whois", 22 | "serverinfo", 23 | "math" 24 | ] 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /alembic/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from typing import Sequence, Union 9 | 10 | from alembic import op 11 | import sqlalchemy as sa 12 | ${imports if imports else ""} 13 | 14 | # revision identifiers, used by Alembic. 15 | revision: str = ${repr(up_revision)} 16 | down_revision: Union[str, None] = ${repr(down_revision)} 17 | branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} 18 | depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} 19 | 20 | 21 | def upgrade() -> None: 22 | ${upgrades if upgrades else "pass"} 23 | 24 | 25 | def downgrade() -> None: 26 | ${downgrades if downgrades else "pass"} 27 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [JasonLovesDoggo] # if any major contributer wants me to add them LMK 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /utils/http.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import aiohttp 4 | 5 | from .cache import async_cache 6 | 7 | 8 | # Removes the aiohttp ClientSession instance warning. 9 | class HTTPSession(aiohttp.ClientSession): 10 | """Abstract class for aiohttp.""" 11 | 12 | def __init__(self, loop=None): 13 | super().__init__(loop=loop or asyncio.get_event_loop()) 14 | 15 | 16 | session = HTTPSession() 17 | 18 | 19 | @async_cache() 20 | async def query(url, method="get", res_method="text", *args, **kwargs): 21 | async with getattr(session, method.lower())(url, *args, **kwargs) as res: 22 | return await getattr(res, res_method)() 23 | 24 | 25 | async def get(url, *args, **kwargs): 26 | return await query(url, "get", *args, **kwargs) 27 | 28 | 29 | async def post(url, *args, **kwargs): 30 | return await query(url, "post", *args, **kwargs) 31 | -------------------------------------------------------------------------------- /utils/formats.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from typing import Sequence, Optional 3 | 4 | 5 | def human_join( 6 | seq: Sequence[str], delim: str = ", ", final: str = "or" 7 | ) -> str: 8 | size = len(seq) 9 | if size == 0: 10 | return "" 11 | 12 | if size == 1: 13 | return seq[0] 14 | 15 | if size == 2: 16 | return f"{seq[0]} {final} {seq[1]}" 17 | 18 | return delim.join(seq[:-1]) + f" {final} {seq[-1]}" 19 | 20 | 21 | def format_dt(dt: datetime.datetime, style: Optional[str] = None) -> str: 22 | if dt.tzinfo is None: 23 | dt = dt.replace(tzinfo=datetime.timezone.utc) 24 | 25 | if style is None: 26 | return f"" 27 | return f"" 28 | 29 | 30 | class plural: 31 | def __init__(self, value: int): 32 | self.value: int = value 33 | 34 | def __format__(self, format_spec: str) -> str: 35 | v = self.value 36 | singular, sep, plural = format_spec.partition("|") 37 | plural = plural or f"{singular}s" 38 | if abs(v) != 1: 39 | return f"{v} {plural}" 40 | return f"{v} {singular}" 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022 Ogiroid Development Team 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a 6 | copy of this software and associated documentation files (the "Software"), 7 | to deal in the Software without restriction, including without limitation 8 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | and/or sell copies of the Software, and to permit persons to whom the 10 | Software is furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 16 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /cogs/Password.py: -------------------------------------------------------------------------------- 1 | import secrets 2 | import string 3 | 4 | from disnake.ext import commands 5 | 6 | from utils.bot import OGIROID 7 | 8 | 9 | class Password(commands.Cog): 10 | """Generates a random password""" 11 | 12 | def __init__(self, bot: OGIROID): 13 | self.bot = bot 14 | 15 | @commands.slash_command( 16 | name="password", 17 | aliases=["pass"], 18 | description="Generates a random password & DM's it!", 19 | ) 20 | async def password(self, inter, length: int): 21 | """Generate a random password & DMs it!""" 22 | if length > 100: 23 | length = 100 24 | password = "".join( 25 | secrets.choice(string.ascii_letters + string.digits) for _ in range(length) 26 | ) 27 | # try to DM if fails send the password to the channel 28 | try: 29 | await inter.author.send(f"Your password is: ||{password}||") 30 | await inter.send("Your password has been sent!") 31 | # If DMs are closed, send the password to the channel 32 | except: 33 | await inter.send(f"Your password is: ||{password}||", ephemeral=True) 34 | 35 | 36 | def setup(bot): 37 | bot.add_cog(Password(bot)) 38 | -------------------------------------------------------------------------------- /utils/assorted.py: -------------------------------------------------------------------------------- 1 | import traceback 2 | 3 | 4 | def traceback_maker(err, advance: bool = True): 5 | """A way to debug your code anywhere""" 6 | _traceback = "".join(traceback.format_tb(err.__traceback__)) 7 | error = "```py\n{1}{0}: {2}\n```".format( 8 | type(err).__name__, _traceback, err 9 | ) 10 | return error if advance else f"{type(err).__name__}: {err}" 11 | 12 | 13 | def renderBar( 14 | value: int, 15 | *, 16 | gap: int = 0, 17 | length: int = 32, 18 | point: str = "", 19 | fill: str = "-", 20 | empty: str = "-", 21 | ) -> str: 22 | # make the bar not wider than 32 even with gaps > 0 23 | length = int(length / int(gap + 1)) 24 | 25 | # handles fill and empty's length 26 | fillLength = int(length * value / 100) 27 | emptyLength = length - fillLength 28 | 29 | # handles gaps 30 | gapFill = " " * gap if gap else "" 31 | 32 | return gapFill.join( 33 | [fill] * (fillLength - len(point)) + [point] + [empty] * emptyLength 34 | ) 35 | 36 | 37 | def getPosition(num: int): 38 | pos_map = {1: "🥇", 2: "🥈", 3: "🥉", 0: "🏅"} 39 | if num in pos_map: 40 | return pos_map[num] 41 | else: 42 | return pos_map[0] 43 | -------------------------------------------------------------------------------- /cogs/AI.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import time 3 | from io import BytesIO 4 | from better_profanity import profanity 5 | import disnake 6 | from disnake.ext import commands 7 | 8 | from utils.bot import OGIROID 9 | 10 | 11 | class AI(commands.Cog): 12 | def __init__(self, bot: OGIROID): 13 | self.bot = bot 14 | 15 | @commands.cooldown(1, 5, commands.BucketType.user) 16 | @commands.slash_command(description="Generates ai art") 17 | async def ai_art(self, inter: disnake.ApplicationCommandInteraction, text: str): 18 | if profanity.contains_profanity(text): 19 | return await inter.send(f"NSFW requests are not allowed!", ephemeral=True) 20 | if "bot" in inter.channel.name or "command" in inter.channel.name: 21 | hidden = False 22 | else: 23 | hidden = True 24 | ETA = int(time.time() + 15) 25 | await inter.send( 26 | f"This might take a bit of time... ETA: ", 27 | ephemeral=hidden, 28 | ) 29 | response = await self.bot.session.post( 30 | "https://api-inference.huggingface.co/models/stabilityai/stable-diffusion-xl-base-1.0", 31 | json={"inputs": text}, 32 | headers={"Authorization": f"Bearer {self.bot.config.tokens.huggingface}"}, 33 | ) 34 | images = [disnake.File(BytesIO(await response.read()), "image.png")] 35 | 36 | await inter.edit_original_response(files=images, content="Here you go!") 37 | 38 | 39 | def setup(bot): 40 | bot.add_cog(AI(bot)) 41 | -------------------------------------------------------------------------------- /docker-compose.yml.template: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | services: 3 | postgres: 4 | restart: always 5 | image: postgres:15-alpine 6 | container_name: "postgres" 7 | environment: 8 | POSTGRES_MULTIPLE_DATABASES: ogiroid,dashboard 9 | POSTGRES_PASSWORD: password 10 | replace with password 11 | POSTGRES_USER: ogiroid 12 | networks: 13 | - postgres 14 | ports: 15 | - 5432:5432 16 | volumes: 17 | - ./pg-init-scripts/create-multi-pg.sh:/docker-entrypoint-initdb.d/create-multi-pg.sh 18 | - postgres-data:/var/lib/postgresql/data 19 | healthcheck: 20 | test: ["CMD-SHELL", "pg_isready -U ogiroid"] 21 | interval: 2s 22 | timeout: 1s 23 | retries: 5 24 | bot: 25 | links: 26 | - postgres 27 | restart: unless-stopped 28 | build: 29 | context: Bot 30 | volumes: 31 | - ./Bot:/bot:ro 32 | tty: true 33 | networks: 34 | - postgres 35 | depends_on: 36 | - postgres 37 | 38 | dashboard: 39 | restart: unless-stopped 40 | build: 41 | context: Dashboard 42 | ports: 43 | - 4000:4000 44 | links: 45 | - caddy 46 | - postgres 47 | - bot 48 | 49 | caddy: 50 | image: caddy:2.7.6-alpine 51 | restart: always 52 | cap_add: 53 | - NET_ADMIN 54 | ports: 55 | - "80:80" 56 | - "443:443" 57 | - "443:443/udp" 58 | volumes: 59 | - $PWD/Caddyfile:/etc/caddy/Caddyfile 60 | volumes: 61 | postgres-data: # allow data to persist in docker managed dir 62 | networks: 63 | postgres: # postgres <-> bot 64 | web: # bot <-> web 65 | -------------------------------------------------------------------------------- /utils/config.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import os 4 | 5 | from utils.CONSTANTS import * 6 | 7 | 8 | @dataclass 9 | class Tokens: 10 | SRA: str = os.getenv("SRA_API_KEY") 11 | bot: str = os.getenv("TOKEN") 12 | weathermap: str = os.getenv("OPEN_WEATHER_MAP_API_KEY") 13 | yt_api_key: str = os.getenv("YT_API_KEY") 14 | currency: str = os.getenv("CURRENCY_API_KEY") 15 | huggingface: str = os.getenv("HUGGINGFACE_API_KEY") 16 | api_ninjas_key: str = os.getenv("API-NINJAS-KEY") 17 | 18 | 19 | @dataclass 20 | class Database: # Todo switch to rockdb info 21 | connection_string = os.getenv("POSTGRES_CONNECTION_STRING") 22 | 23 | @classmethod 24 | def dev(cls): 25 | cls.database = os.getenv("POSTGRES_CONNECTION_STRING") 26 | return cls 27 | 28 | 29 | @dataclass 30 | class Config: 31 | if os.getenv("DEVELOPMENT").lower() == "true": 32 | Development: bool = True 33 | elif os.getenv("DEVELOPMENT").lower() == "false": 34 | Development: bool = False 35 | else: 36 | raise ValueError("DEVELOPMENT in secrets.env must be set to true or false") 37 | colors = Colors 38 | colours = colors 39 | tokens = Tokens 40 | Database = Database 41 | if Development: 42 | print("Using Development Config variables") 43 | channels = Channels.dev() 44 | roles = Roles.dev() 45 | emojis = Emojis.dev() 46 | guilds = Guilds.dev() 47 | debug = True 48 | Database = Database.dev() 49 | else: 50 | emojis = Emojis 51 | channels = Channels 52 | roles = Roles 53 | guilds = Guilds 54 | debug = False 55 | -------------------------------------------------------------------------------- /cogs/Help.py: -------------------------------------------------------------------------------- 1 | import disnake 2 | from disnake.ext import commands 3 | 4 | from utils.bot import OGIROID 5 | from utils.pagination import CreatePaginator 6 | 7 | 8 | class HelpCommand(commands.Cog, name="Help"): 9 | """Help Command""" 10 | 11 | def __init__(self, bot: OGIROID): 12 | self.bot = bot 13 | self.COLOUR = 0xFFFFFF 14 | 15 | @commands.slash_command(name="help", description="Lists all commands") 16 | async def help(self, inter): 17 | """Lists all commands""" 18 | embeds = [] 19 | 20 | cogs = self.bot.cogs.items() 21 | for cog_name, cog in cogs: 22 | embed = disnake.Embed(title=cog.qualified_name, colour=self.COLOUR) 23 | if cog is None: 24 | continue 25 | cmds = cog.get_slash_commands() 26 | name = cog.qualified_name 27 | 28 | value = "" 29 | for cmd in cmds: 30 | value += f"`/{cmd.qualified_name}` - {cmd.description}\n" 31 | if cmd.children: 32 | for children, sub_command in cmd.children.items(): 33 | try: 34 | value += f"`/{sub_command.qualified_name}` - {sub_command.description}\n" 35 | except AttributeError: 36 | pass 37 | 38 | if value == "": 39 | continue 40 | 41 | embed.description = f"{cog.description}\n\n{value}" 42 | embeds.append(embed) 43 | 44 | paginator = CreatePaginator(embeds, inter.author.id, timeout=300.0) 45 | await inter.send(embed=embeds[0], view=paginator) 46 | 47 | 48 | def setup(bot: commands.Bot): 49 | bot.add_cog(HelpCommand(bot)) 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Ogiroid - the most handsome Discord bot!

2 |
3 | img 4 |
5 |

Introduction:

6 | Ogiroid is a multipurpose Discord Bot (who is very handsome 😋😋) developed for the YouTuber, Lewis Menelaws' Discord Server. 7 |
8 |

Bot Development Information:

9 | Based off of: edoC 10 | 11 |

Written in: Python
Made using Disnake, a Discord.py fork!

12 |

NOTE: you need to use python 3.8+ for this bot

13 | 14 | Main Contributors/Developers: 15 | 21 | The bot is currently on release: 2.5
22 | License: MIT
23 | 24 |

Contributing

25 | Wish to Contribute to this bot? Checkout: contribution guidelines 26 |
27 | 28 |

Changelog 26-10-2024:

29 |
    30 |
  1. Custom Role managment trough the bot
  2. 31 |
  3. Changed how levels are stored: Level+xp to total_xp
  4. 32 |
  5. Add new commands to automatically fix and assign level roles
  6. 33 |
  7. Add XP command
  8. 34 | 35 | 36 |

    Changelog 29-12-2023:

    37 |
      38 |
    1. The bot was migrated to PostgreSQL.
    2. 39 |
    3. All the SQL queries were updated to use the SQLAlchemy ORM.
    4. 40 |
    5. Bitcoin Command
    6. 41 |
    7. Starboard Fixes and enhancements
    8. 42 |
    9. Lots of bug fixes.
    10. 43 |
    11. Start on bot dashboard.
    12. 44 |
    45 |
    46 | -------------------------------------------------------------------------------- /utils/cache.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | 3 | from aiocache import SimpleMemoryCache 4 | 5 | 6 | def async_cache(maxsize=128): 7 | """ 8 | args: 9 | maxsize: int - the maximum size of the cache (default: 128) set to 0 for unlimited 10 | """ 11 | _cache = {} 12 | 13 | def decorator(func): 14 | @wraps(func) 15 | async def inner(*args, no_cache=False, **kwargs): 16 | if no_cache: 17 | return await func(*args, **kwargs) 18 | 19 | key_base = "_".join(str(x) for x in args) 20 | key_end = "_".join(f"{k}:{v}" for k, v in kwargs.items()) 21 | key = f"{key_base}-{key_end}" 22 | 23 | if key in _cache: 24 | return _cache[key] 25 | 26 | res = await func(*args, **kwargs) 27 | 28 | if maxsize != 0: 29 | if len(_cache) > maxsize: 30 | del _cache[list(_cache.keys())[0]] 31 | _cache[key] = res 32 | 33 | return res 34 | 35 | return inner 36 | 37 | return decorator 38 | 39 | 40 | class AsyncTTL(SimpleMemoryCache): 41 | def __init__(self, ttl: int = 3600, *args, **kwargs): 42 | super().__init__(*args, **kwargs) 43 | self.ttl = ttl 44 | 45 | async def try_get(self, key): 46 | """ 47 | Try to get a value from cache. If it's not there then it returns False. 48 | """ 49 | value = await self.get(key, default=False) 50 | return value 51 | 52 | async def get(self, key, *args, **kwargs): 53 | await super().expire(key, self.ttl) 54 | return await super().get(key, *args, **kwargs) 55 | 56 | async def set(self, key, value, *args, **kwargs): 57 | await super().set(key, value, ttl=self.ttl, *args, **kwargs) 58 | 59 | async def add(self, key, value, *args, **kwargs): 60 | await super().add(key, value, ttl=self.ttl, *args, **kwargs) 61 | 62 | async def remove(self, key, *args, **kwargs): 63 | await super().delete(key, *args, **kwargs) 64 | -------------------------------------------------------------------------------- /cogs/Search.py: -------------------------------------------------------------------------------- 1 | from urllib.parse import quote_plus 2 | 3 | from disnake.ext import commands 4 | 5 | from utils.bot import OGIROID 6 | 7 | 8 | class Search(commands.Cog): 9 | """Search stuff""" 10 | 11 | def __init__(self, bot: OGIROID): 12 | self.bot = bot 13 | 14 | @commands.slash_command(description="Returns a search for your query") 15 | async def search( 16 | self, 17 | inter, 18 | engine: str = commands.Param( 19 | description="Which Search engine to use", 20 | choices=["google", "duckduckgo", "bing", "letmegoogle"], 21 | ), 22 | query: str = commands.Param(description="The query to search for"), 23 | ): 24 | """Searches the keyword entered""" 25 | query = quote_plus(query.rstrip().rstrip()) 26 | if engine == "google": 27 | await inter.send(f"https://google.com/search?q={query}") 28 | elif engine == "duckduckgo": 29 | await inter.send(f"https://duckduckgo.com/?q={query}") 30 | elif engine == "bing": 31 | await inter.send(f"https://bing.com/search?q={query}") 32 | elif engine == "letmegoogle": 33 | await inter.send(f"https://letmegooglethat.com/?q={query}") 34 | 35 | @commands.slash_command( 36 | name="feeling-lucky", 37 | description="Returns the first google result for your query", 38 | ) 39 | async def lucky(self, inter, query): 40 | """Googles the keyword entered and returns the first result""" 41 | query = query.rstrip().replace(" ", "+") 42 | await inter.send(f"https://www.google.com/search?q={query}&btnI") 43 | 44 | @commands.slash_command( 45 | description="Returns a StackOverflow search for your query" 46 | ) 47 | async def stackoverflow(self, inter, query): 48 | """Searches StackOverflow for the query""" 49 | query = query.rstrip().replace(" ", "+") 50 | await inter.send(f"https://stackoverflow.com/search?q={query}") 51 | 52 | 53 | def setup(bot: commands.Bot): 54 | bot.add_cog(Search(bot)) 55 | -------------------------------------------------------------------------------- /utils/exceptions.py: -------------------------------------------------------------------------------- 1 | from disnake.ext.commands import CheckFailure 2 | 3 | 4 | class BotException(BaseException): 5 | pass 6 | 7 | 8 | class BlacklistException(BotException): 9 | pass 10 | 11 | 12 | class UserNotFound(BotException): 13 | pass 14 | 15 | 16 | class UsersNotFound(BotException): 17 | pass 18 | 19 | 20 | class TagException(BotException): 21 | pass 22 | 23 | 24 | class AliasException(TagException): 25 | pass 26 | 27 | 28 | class FlagQuizException(BotException): 29 | pass 30 | 31 | 32 | class RoleReactionException(BotException): 33 | pass 34 | 35 | 36 | class BirthdayException(BotException): 37 | pass 38 | 39 | 40 | class TagNotFound(TagException, KeyError): 41 | pass 42 | 43 | 44 | class TagsNotFound(TagException): 45 | pass # used when no tags are found i.e. when the user has no tags or when there are no tags in the database 46 | 47 | 48 | class TagAlreadyExists(TagException): 49 | pass 50 | 51 | 52 | class AliasAlreadyExists(AliasException): 53 | pass 54 | 55 | 56 | class AliasNotFound(AliasException): 57 | pass 58 | 59 | 60 | class AliasLimitReached(AliasException): 61 | pass 62 | 63 | 64 | class FlagQuizUsersNotFound(FlagQuizException): 65 | pass 66 | 67 | 68 | class ReactionAlreadyExists(RoleReactionException): 69 | pass 70 | 71 | 72 | class ReactionNotFound(RoleReactionException): 73 | pass 74 | 75 | 76 | class BlacklistNotFound(BlacklistException): 77 | pass 78 | 79 | 80 | class UserBlacklisted(CheckFailure, BlacklistException): 81 | async def __call__(self, ctx): 82 | pass 83 | 84 | def __init__(self, *args, **kwargs): 85 | pass 86 | 87 | 88 | class CityNotFound(BotException): 89 | def __init__(self, city): 90 | super().__init__(f"City '{city}' not found!") 91 | 92 | 93 | class InvalidAPIKEY(BotException): 94 | def __init__(self): 95 | super().__init__(f"You have an invalid API key!") 96 | 97 | 98 | class LevelingSystemError(BotException): 99 | pass 100 | 101 | 102 | class UserAlreadyExists(BirthdayException): 103 | pass 104 | -------------------------------------------------------------------------------- /cogs/Lewis.py: -------------------------------------------------------------------------------- 1 | import datetime as dt 2 | 3 | from disnake.ext import commands, tasks 4 | 5 | from utils.bot import OGIROID 6 | 7 | 8 | class Lewis(commands.Cog, name="Lewis"): 9 | def __init__(self, bot: OGIROID): 10 | self.bot = bot 11 | self.upload_check.start() 12 | self.youtube_channel_id = "UCWI-ohtRu8eEeDj93hmUsUQ" 13 | self.youtube_api_key = self.bot.config.tokens.yt_api_key 14 | self.youtube_api_url = f"https://www.googleapis.com/youtube/v3/search?part=snippet&channelId={self.youtube_channel_id}&maxResults=1&order=date&type=video&key={self.youtube_api_key}" 15 | 16 | def cog_unload(self): 17 | self.upload_check.cancel() 18 | 19 | @tasks.loop(minutes=30) 20 | async def upload_check(self): 21 | if self.youtube_api_key is None: 22 | print("No YouTube API key found, skipping upload check") 23 | return 24 | check_time = dt.datetime.utcnow() 25 | channel = self.bot.get_channel(self.bot.config.channels.uploads) 26 | if channel is None: 27 | return 28 | 29 | response = await self.bot.session.get(self.youtube_api_url) 30 | response = await response.json() 31 | try: 32 | video_id = response["items"][0]["id"]["videoId"] 33 | except KeyError: 34 | print("Issue with request, skipping upload check") 35 | return 36 | video_url = f"https://www.youtube.com/watch?v={video_id}" 37 | 38 | video_release_time = response["items"][0]["snippet"]["publishedAt"] 39 | year = video_release_time.split("-")[0] 40 | month = video_release_time.split("-")[1] 41 | day = video_release_time.split("-")[2].split("T")[0] 42 | hour = video_release_time.split("T")[1].split(":")[0] 43 | minute = video_release_time.split("T")[1].split(":")[1] 44 | second = video_release_time.split("T")[1].split(":")[2].split("Z")[0] 45 | time = dt.datetime( 46 | int(year), 47 | int(month), 48 | int(day), 49 | int(hour), 50 | int(minute), 51 | int(second), 52 | ) 53 | if check_time - time < dt.timedelta(minutes=30): 54 | return await channel.send( 55 | f"Hey, Lewis posted a new video! <@&{self.bot.config.roles.yt_announcements}>\n{video_url}" 56 | ) 57 | else: 58 | return 59 | 60 | 61 | def setup(bot): 62 | bot.add_cog(Lewis(bot)) 63 | -------------------------------------------------------------------------------- /cogs/Starboard.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | import disnake 4 | from disnake.ext import commands 5 | 6 | from utils.bot import OGIROID 7 | 8 | 9 | class Starboard(commands.Cog): 10 | def __init__(self, bot: OGIROID): 11 | self.bot = bot 12 | self.starboard_channel_id = self.bot.config.channels.starboard 13 | self.star_emoji = "⭐" 14 | self.num_of_stars = 4 15 | 16 | @commands.Cog.listener() 17 | async def on_raw_reaction_add(self, payload): 18 | channel = self.bot.get_channel(payload.channel_id) 19 | if ( 20 | channel.guild.id != self.bot.config.guilds.main_guild 21 | or channel.id == self.starboard_channel_id 22 | ): 23 | return 24 | message = await channel.fetch_message(payload.message_id) 25 | starboard_channel = message.guild.get_channel(self.starboard_channel_id) 26 | if payload.emoji.name == self.star_emoji and not channel == starboard_channel: 27 | for reaction in message.reactions: 28 | if ( 29 | reaction.emoji == self.star_emoji 30 | and reaction.count == self.num_of_stars 31 | ): 32 | channel_history = await starboard_channel.history( 33 | limit=100 34 | ).flatten() 35 | # check if message is already in starboard 36 | for msg in channel_history: 37 | if msg.embeds: 38 | if ( 39 | msg.embeds[0].description.split("\n\n")[0] 40 | == message.content 41 | ): 42 | return 43 | embed = disnake.Embed( 44 | description=f"{message.content}\n\n**[Jump to message]({message.jump_url})**", 45 | color=disnake.Color.gold(), 46 | timestamp=datetime.now(), 47 | ) 48 | if message.attachments: 49 | embed.set_image(url=message.attachments[0].url) 50 | embed.set_author( 51 | name=message.author, 52 | icon_url=message.author.display_avatar.url, 53 | ) 54 | await starboard_channel.send(embed=embed) 55 | 56 | 57 | def setup(bot): 58 | bot.add_cog(Starboard(bot)) 59 | -------------------------------------------------------------------------------- /cogs/Memes.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | import disnake 4 | from disnake.ext import commands 5 | 6 | from utils.bot import OGIROID 7 | 8 | 9 | class Memes(commands.Cog): 10 | """Meme Commands""" 11 | 12 | def __init__(self, bot: OGIROID): 13 | self.bot = bot 14 | 15 | @commands.slash_command(name="onlyfans", description="Lewis' OnlyFans") 16 | async def onlyfans(self, inter): 17 | """Lewis' Onlyfans""" 18 | await inter.send( 19 | "https://media.wired.com/photos/59548ac98e8cc150fa8ec379/master/w_2560%2Cc_limit/GettyImages-56196238.jpg" 20 | ) 21 | 22 | @commands.slash_command( 23 | name="meme", 24 | aliases=["dankmeme"], 25 | description="Random meme from r/memes", 26 | ) 27 | async def meme(self, inter): 28 | """Random meme from r/memes""" 29 | subreddit = "memes" 30 | await self.get_posts(inter, subreddit) 31 | 32 | @commands.slash_command( 33 | name="programmerhumor", 34 | aliases=["progmeme", "programmermeme", "memeprogrammer"], 35 | description="Random meme from r/programmerhumor", 36 | ) 37 | async def programmerhumor(self, inter): 38 | """Random meme from r/programmerhumor""" 39 | subreddit = "ProgrammerHumor" 40 | await self.get_posts(inter, subreddit) 41 | 42 | async def get_posts(self, inter, subreddit): 43 | url = f"https://api.reddit.com/r/{subreddit}/random" 44 | response = await self.bot.session.get(url) 45 | r = await response.json() 46 | upvotes = r[0]["data"]["children"][0]["data"]["ups"] 47 | embed = disnake.Embed( 48 | title=f'{r[0]["data"]["children"][0]["data"]["title"]}', 49 | description=f'{r[0]["data"]["children"][0]["data"]["selftext"]}', 50 | colour=0x00B8FF, 51 | timestamp=datetime.now(), 52 | url=f"https://www.reddit.com{r[0]['data']['children'][0]['data']['permalink']}", 53 | ) 54 | embed.set_image(url=r[0]["data"]["children"][0]["data"]["url"]) 55 | embed.set_footer( 56 | text=f"{upvotes} Upvotes ", 57 | icon_url="https://cdn.discordapp.com/attachments/925750381840064525/925755794669047899/PngItem_715538.png", 58 | ) 59 | await inter.send(embed=embed) 60 | 61 | @commands.slash_command(name="freemoney", description="Get free money!") 62 | async def free_money(self, inter): 63 | """Get free money""" 64 | await inter.send( 65 | "Free money hack!\n[Click here for free money]()" 66 | ) 67 | 68 | 69 | def setup(bot: OGIROID): 70 | bot.add_cog(Memes(bot)) 71 | -------------------------------------------------------------------------------- /cogs/Redditbot.py: -------------------------------------------------------------------------------- 1 | import disnake 2 | from disnake.ext import commands 3 | 4 | from utils.bot import OGIROID 5 | 6 | 7 | class RedditBot(commands.Cog, name="Reddit Bot"): 8 | """All the Reddit Bot related commands!""" 9 | 10 | def __init__(self, bot: OGIROID): 11 | self.bot = bot 12 | 13 | # Get Information Related to the GitHub of the Bot 14 | @commands.slash_command( 15 | name="rbgithub", 16 | description="Get Information Related to the GitHub of the Reddit Bot", 17 | ) 18 | @commands.guild_only() 19 | async def rbgithub(self, ctx): 20 | url = await self.bot.session.get( 21 | "https://api.github.com/repos/elebumm/RedditVideoMakerBot" 22 | ) 23 | json = await url.json() 24 | if url.status == 200: 25 | # Creat an embed with the information: Name, Description, URL, Stars, Gazers, Forks, Last Updated 26 | embed = disnake.Embed( 27 | title=f"{json['name']} information", 28 | description=f"{json['description']}", 29 | color=0xFFFFFF, 30 | ) 31 | embed.set_thumbnail(url=f"{json['owner']['avatar_url']}") 32 | embed.add_field( 33 | name="GitHub Link: ", 34 | value=f"**[Link to the Reddit Bot]({json['html_url']})**", 35 | inline=True, 36 | ) 37 | embed.add_field( 38 | name="Stars <:starr:990647250847940668>: ", 39 | value=f"{json['stargazers_count']}", 40 | inline=True, 41 | ) 42 | embed.add_field( 43 | name="Gazers <:gheye:990645707427950593>: ", 44 | value=f"{json['subscribers_count']}", 45 | inline=True, 46 | ) 47 | embed.add_field( 48 | name="Forks <:fork:990644980773187584>: ", 49 | value=f"{json['forks_count']}", 50 | inline=True, 51 | ) 52 | embed.add_field( 53 | name="Open Issues <:issue:990645996918808636>: ", 54 | value=f"{json['open_issues_count']}", 55 | inline=True, 56 | ) 57 | embed.add_field( 58 | name="License <:license:990646337118818404>: ", 59 | value=f"{json['license']['spdx_id']}", 60 | inline=True, 61 | ) 62 | embed.add_field( 63 | name="Clone Command <:clone:990646924640153640>: ", 64 | value=f"```git clone {json['clone_url']}```", 65 | inline=False, 66 | ) 67 | await ctx.send(embed=embed) 68 | 69 | 70 | def setup(bot): 71 | bot.add_cog(RedditBot(bot)) 72 | -------------------------------------------------------------------------------- /alembic/env.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import os 3 | from logging.config import fileConfig 4 | from dotenv import load_dotenv 5 | 6 | from sqlalchemy import pool 7 | from sqlalchemy.engine import Connection 8 | from sqlalchemy.ext.asyncio import async_engine_from_config 9 | 10 | from alembic import context 11 | 12 | # this is the Alembic Config object, which provides 13 | # access to the values within the .ini file in use. 14 | config = context.config 15 | 16 | # Interpret the config file for Python logging. 17 | # This line sets up loggers basically. 18 | if config.config_file_name is not None: 19 | fileConfig(config.config_file_name) 20 | 21 | # add your model's MetaData object here 22 | # for 'autogenerate' support 23 | # from myapp import mymodel 24 | # target_metadata = mymodel.Base.metadata 25 | from utils.db_models import Base 26 | 27 | target_metadata = Base.metadata 28 | 29 | # other values from the config, defined by the needs of env.py, 30 | # can be acquired: 31 | # my_important_option = config.get_main_option("my_important_option") 32 | # ... etc. 33 | load_dotenv("secrets.env") 34 | config.set_main_option( 35 | "sqlalchemy.url", 36 | os.environ["POSTGRES_CONNECTION_STRING"].replace( 37 | "postgresql://", "postgresql+asyncpg://" 38 | ), 39 | ) 40 | 41 | 42 | def run_migrations_offline() -> None: 43 | """Run migrations in 'offline' mode. 44 | 45 | This configures the context with just a URL 46 | and not an Engine, though an Engine is acceptable 47 | here as well. By skipping the Engine creation 48 | we don't even need a DBAPI to be available. 49 | 50 | Calls to context.execute() here emit the given string to the 51 | script output. 52 | 53 | """ 54 | url = config.get_main_option("sqlalchemy.url") 55 | context.configure( 56 | url=url, 57 | target_metadata=target_metadata, 58 | literal_binds=True, 59 | dialect_opts={"paramstyle": "named"}, 60 | ) 61 | 62 | with context.begin_transaction(): 63 | context.run_migrations() 64 | 65 | 66 | def do_run_migrations(connection: Connection) -> None: 67 | context.configure(connection=connection, target_metadata=target_metadata) 68 | 69 | with context.begin_transaction(): 70 | context.run_migrations() 71 | 72 | 73 | async def run_async_migrations() -> None: 74 | """In this scenario we need to create an Engine 75 | and associate a connection with the context. 76 | 77 | """ 78 | 79 | connectable = async_engine_from_config( 80 | config.get_section( 81 | config.config_ini_section, 82 | {}, 83 | ), 84 | prefix="sqlalchemy.", 85 | poolclass=pool.NullPool, 86 | ) 87 | 88 | async with connectable.connect() as connection: 89 | await connection.run_sync(do_run_migrations) 90 | 91 | await connectable.dispose() 92 | 93 | 94 | def run_migrations_online() -> None: 95 | """Run migrations in 'online' mode.""" 96 | 97 | asyncio.run(run_async_migrations()) 98 | 99 | 100 | if context.is_offline_mode(): 101 | run_migrations_offline() 102 | else: 103 | run_migrations_online() 104 | -------------------------------------------------------------------------------- /utils/rankcard.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from io import BytesIO 4 | from typing import Union, Tuple, Dict 5 | 6 | import disnake 7 | from PIL import Image 8 | from PIL import ImageDraw 9 | 10 | from utils.http import session 11 | 12 | 13 | async def getavatar(user: Union[disnake.User, disnake.Member]) -> bytes: 14 | disver = str(disnake.__version__) 15 | if disver.startswith("1"): 16 | async with session.get(str(user.avatar_url)) as response: 17 | avatarbytes = await response.read() 18 | await session.close() 19 | elif disver.startswith("2"): 20 | async with session.get(str(user.display_avatar.url)) as response: 21 | avatarbytes = await response.read() 22 | await session.close() 23 | return avatarbytes 24 | 25 | 26 | def strip_num(num: int | str): 27 | num = int(num) 28 | if num >= 1000: 29 | if num >= 1000000: 30 | num = f"{round(num / 1000000, 1)}M" 31 | else: 32 | num = f"{round(num / 1000, 1)}K" 33 | return num 34 | 35 | 36 | class Rankcard: 37 | def __init__(self): 38 | self.POSITIONS: Dict[str, Tuple] = { 39 | "AVATAR_DIAM": (189, 189), 40 | "AVATAR_POS": (107, -186), 41 | "TEXT_POS": (-177, -348), 42 | "STATUS_POS": (290, 330, -40, 20), 43 | } 44 | 45 | async def getavatar( 46 | self, user: Union[disnake.User, disnake.Member] 47 | ) -> bytes: 48 | async with session.get(str(user.display_avatar.url)) as response: 49 | avatarbytes = await response.read() 50 | with Image.open(BytesIO(avatarbytes)) as im: 51 | USER_IMG = im.resize(self.POSITIONS["AVATAR_DIAM"]) 52 | return USER_IMG 53 | 54 | async def create_img( 55 | self, 56 | user: disnake.Member | disnake.User, 57 | level: int | str, 58 | xp: Tuple[int, int], 59 | rank: int | str, 60 | ) -> bytes: 61 | """ 62 | args: 63 | user: the user that the rankcard is for 64 | level: the user's current level 65 | xp: A tuple of (currentXP, NeededXP) 66 | """ 67 | level = f"{level:,d}" 68 | currentxp = xp[0] 69 | neededxp = xp[1] 70 | missingxp = xp[1] - xp[0] 71 | USER_IMG = await self.getavatar(user) 72 | # todo change text to using \n 73 | text = f""" 74 | Username: {user.name} 75 | Experience: {currentxp:,d} / {neededxp:,d} ({missingxp:,d} missing) 76 | Level {level} 77 | Rank #{rank:,d}""" 78 | BACKGROUND = Image.open("./assets/images/rank/background.png") 79 | draw = ImageDraw.Draw(BACKGROUND) 80 | 81 | draw.ellipse((160, 170, 208, 218), fill=0) # status outline 82 | 83 | # status 84 | try: 85 | if user.status == disnake.Status.online: 86 | draw.ellipse(self.POSITIONS["STATUS_POS"], fill=(67, 181, 129)) 87 | elif user.status == disnake.Status.offline: 88 | draw.ellipse( 89 | self.POSITIONS["STATUS_POS"], fill=(116, 127, 141) 90 | ) 91 | elif user.status == disnake.Status.dnd: 92 | draw.ellipse(self.POSITIONS["STATUS_POS"], fill=(240, 71, 71)) 93 | elif user.status == disnake.Status.idle: 94 | draw.ellipse(self.POSITIONS["STATUS_POS"], fill=(250, 166, 26)) 95 | except: 96 | draw.ellipse((165, 175, 204, 214), fill=(114, 137, 218)) 97 | 98 | final_buffer = BytesIO() 99 | 100 | BACKGROUND.save(final_buffer, "png") 101 | final_buffer.seek(0) 102 | return final_buffer 103 | -------------------------------------------------------------------------------- /alembic.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # path to migration scripts 5 | script_location = alembic 6 | 7 | # template used to generate migration file names; The default value is %%(rev)s_%%(slug)s 8 | # Uncomment the line below if you want the files to be prepended with date and time 9 | # file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s 10 | 11 | # sys.path path, will be prepended to sys.path if present. 12 | # defaults to the current working directory. 13 | prepend_sys_path = . 14 | 15 | # timezone to use when rendering the date within the migration file 16 | # as well as the filename. 17 | # If specified, requires the python>=3.9 or backports.zoneinfo library. 18 | # Any required deps can installed by adding `alembic[tz]` to the pip requirements 19 | # string value is passed to ZoneInfo() 20 | # leave blank for localtime 21 | # timezone = 22 | 23 | # max length of characters to apply to the 24 | # "slug" field 25 | # truncate_slug_length = 40 26 | 27 | # set to 'true' to run the environment during 28 | # the 'revision' command, regardless of autogenerate 29 | # revision_environment = false 30 | 31 | # set to 'true' to allow .pyc and .pyo files without 32 | # a source .py file to be detected as revisions in the 33 | # versions/ directory 34 | # sourceless = false 35 | 36 | # version location specification; This defaults 37 | # to alembic/versions. When using multiple version 38 | # directories, initial revisions must be specified with --version-path. 39 | # The path separator used here should be the separator specified by "version_path_separator" below. 40 | # version_locations = %(here)s/bar:%(here)s/bat:alembic/versions 41 | 42 | # version path separator; As mentioned above, this is the character used to split 43 | # version_locations. The default within new alembic.ini files is "os", which uses os.pathsep. 44 | # If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas. 45 | # Valid values for version_path_separator are: 46 | # 47 | # version_path_separator = : 48 | # version_path_separator = ; 49 | # version_path_separator = space 50 | version_path_separator = os # Use os.pathsep. Default configuration used for new projects. 51 | 52 | # set to 'true' to search source files recursively 53 | # in each "version_locations" directory 54 | # new in Alembic version 1.10 55 | # recursive_version_locations = false 56 | 57 | # the output encoding used when revision files 58 | # are written from script.py.mako 59 | # output_encoding = utf-8 60 | 61 | sqlalchemy.url = driver://user:pass@localhost/dbname 62 | 63 | 64 | [post_write_hooks] 65 | # post_write_hooks defines scripts or Python functions that are run 66 | # on newly generated revision scripts. See the documentation for further 67 | # detail and examples 68 | 69 | # format using "black" - use the console_scripts runner, against the "black" entrypoint 70 | # hooks = black 71 | # black.type = console_scripts 72 | # black.entrypoint = black 73 | # black.options = -l 79 REVISION_SCRIPT_FILENAME 74 | 75 | # lint with attempts to fix using "ruff" - use the exec runner, execute a binary 76 | # hooks = ruff 77 | # ruff.type = exec 78 | # ruff.executable = %(here)s/.venv/bin/ruff 79 | # ruff.options = --fix REVISION_SCRIPT_FILENAME 80 | 81 | # Logging configuration 82 | [loggers] 83 | keys = root,sqlalchemy,alembic 84 | 85 | [handlers] 86 | keys = console 87 | 88 | [formatters] 89 | keys = generic 90 | 91 | [logger_root] 92 | level = WARN 93 | handlers = console 94 | qualname = 95 | 96 | [logger_sqlalchemy] 97 | level = WARN 98 | handlers = 99 | qualname = sqlalchemy.engine 100 | 101 | [logger_alembic] 102 | level = INFO 103 | handlers = 104 | qualname = alembic 105 | 106 | [handler_console] 107 | class = StreamHandler 108 | args = (sys.stderr,) 109 | level = NOTSET 110 | formatter = generic 111 | 112 | [formatter_generic] 113 | format = %(levelname)-5.5s [%(name)s] %(message)s 114 | datefmt = %H:%M:%S 115 | -------------------------------------------------------------------------------- /utils/wrappers/OpenWeatherMap.py: -------------------------------------------------------------------------------- 1 | import aiohttp 2 | 3 | from utils.exceptions import CityNotFound, InvalidAPIKEY 4 | 5 | 6 | class Temperature: 7 | def __init__(self, temperature): 8 | """Uses kelvin by default""" 9 | self._temperature = round(temperature, 2) 10 | 11 | @property 12 | def temperature(self): 13 | return self._temperature 14 | 15 | def __str__(self): 16 | return "{} K".format(round(self.temperature, 2)) 17 | 18 | def __repr__(self): 19 | return "Temperature=({}, default_type=K)".format( 20 | round(self.temperature, 2) 21 | ) 22 | 23 | @property 24 | def kelvin(self): 25 | return round(self._temperature, 2) 26 | 27 | @property 28 | def fahrenheit(self): 29 | return round((self._temperature - 273.15) * 1.8 + 32, 2) 30 | 31 | @property 32 | def celcius(self): 33 | return round(self._temperature - 273.15, 2) 34 | 35 | 36 | class Wind: 37 | __slots__ = ("speed", "degree") 38 | 39 | def __init__(self, windData): 40 | """Speed uses m/s by default.""" 41 | self.speed = windData["speed"] 42 | self.degree = windData["deg"] 43 | 44 | def __str__(self): 45 | return "{}m/s".format(self.speed) 46 | 47 | def __repr__(self): 48 | return "" 49 | 50 | 51 | class Weather: 52 | __slots__ = ( 53 | "rawData", 54 | "city", 55 | "country", 56 | "wind", 57 | "icon", 58 | "iconUrl", 59 | "temp", 60 | "tempMin", 61 | "tempMax", 62 | "tempFeels", 63 | "weather", 64 | "weatherDetail", 65 | ) 66 | 67 | def __init__(self, data): 68 | self.rawData = data 69 | self.city = data["name"] 70 | self.country = data["sys"]["country"] 71 | self.wind = Wind(data["wind"]) 72 | self.icon = data["weather"][0]["icon"] 73 | self.iconUrl = "https://openweathermap.org/img/wn/{}@2x.png".format( 74 | self.icon 75 | ) 76 | self.temp = Temperature(data["main"]["temp"]) 77 | self.tempMin = Temperature(data["main"]["temp_min"]) 78 | self.tempMax = Temperature(data["main"]["temp_max"]) 79 | self.tempFeels = Temperature(data["main"]["feels_like"]) 80 | self.weather = data["weather"][0]["main"] 81 | self.weatherDetail = str(data["weather"][0]["description"]).title() 82 | 83 | def __str__(self): 84 | return self.weather 85 | 86 | def __repr__(self): 87 | return "200 - {} ({})".format(self.weather, self.temp) 88 | 89 | @property 90 | def humidity(self): 91 | return "{}%".format(self.rawData["main"]["humidity"]) 92 | 93 | @property 94 | def temperature(self): 95 | return self.temp 96 | 97 | @property 98 | def feelslike(self): 99 | return self.feelslike 100 | 101 | 102 | class OpenWeatherAPI: 103 | def __init__(self, key, session=None): 104 | """Wrapper for OpenWeather's API. 105 | Parameter 106 | --------- 107 | key = Your openweather api key 108 | """ 109 | self.apiKey = key 110 | self.session = session or aiohttp.ClientSession() 111 | self.baseUrl = "https://api.openweathermap.org/data/2.5/weather?{type}={query}&appid={key}" 112 | 113 | async def get(self, _type, query): 114 | """Get weather report.""" 115 | async with self.session.get( 116 | self.baseUrl.format(type=_type, query=query, key=self.apiKey) 117 | ) as res: 118 | weatherData = await res.json() 119 | if weatherData["cod"] == "404": 120 | raise CityNotFound(query) 121 | elif weatherData["cod"] == "401": 122 | raise InvalidAPIKEY 123 | return Weather(weatherData) 124 | 125 | async def get_from_city(self, city): 126 | """Get weather report from a city name.""" 127 | return await self.get("q", city) 128 | 129 | async def get_from_zip(self, zipCode): 130 | """Get weather report from a zip code.""" 131 | return await self.get("zip", zipCode) 132 | -------------------------------------------------------------------------------- /utils/shortcuts.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import asyncio 4 | from datetime import datetime 5 | 6 | import disnake 7 | from disnake import Embed, ApplicationCommandInteraction 8 | 9 | 10 | async def wait_until(timestamp): 11 | now = datetime.now() 12 | dt = datetime.fromtimestamp(timestamp) 13 | await asyncio.sleep((dt - now).total_seconds()) 14 | 15 | 16 | def get_expiry(time: int): 17 | time = int(time) 18 | return f"" if str(time) != str(9999999999) else "never" 19 | 20 | 21 | async def permsEmb(inter: ApplicationCommandInteraction, *, permissions: str): 22 | """@summary creates a disnake embed, so I can send it with x details easier""" 23 | emb = Embed( 24 | title=":x: You are missing permissions", 25 | description=f"You need the following permission(s) to use /{inter.application_command.qualified_name}:\n{permissions}", 26 | color=disnake.Color.red(), 27 | ) 28 | await inter.send( 29 | embed=emb, 30 | ephemeral=True, 31 | allowed_mentions=disnake.AllowedMentions( 32 | everyone=False, users=False, roles=False, replied_user=True 33 | ), 34 | ) 35 | 36 | 37 | async def errorEmb( 38 | inter: ApplicationCommandInteraction, text, ephemeral=True, *args, **kwargs 39 | ): 40 | emb = Embed(description=text, color=disnake.Color.red(), *args, **kwargs) 41 | await inter.send( 42 | embed=emb, 43 | ephemeral=ephemeral, 44 | allowed_mentions=disnake.AllowedMentions( 45 | everyone=False, users=False, roles=False, replied_user=True 46 | ), 47 | ) 48 | 49 | 50 | async def warning_embed(inter: ApplicationCommandInteraction, user, reason): 51 | emb = Embed( 52 | title=f"Warned {user}", 53 | description=f"{user.mention} has been warned by {inter.author.mention} for {reason if reason else 'no reason specified'}", 54 | color=disnake.Color.red(), 55 | ) 56 | emb.set_thumbnail(url=user.display_avatar) 57 | emb.set_footer(text=f"Warned by {inter.author}") 58 | emb.timestamp = datetime.now() 59 | await inter.send(embed=emb) 60 | 61 | 62 | async def warnings_embed( 63 | inter: ApplicationCommandInteraction, member, warnings 64 | ): 65 | embed = disnake.Embed(title=f"{member.name}'s warnings", color=0xFFFFFF) 66 | warning_string = "" 67 | i = 0 68 | for warning in warnings: 69 | i += 1 70 | warning_string += ( 71 | f"{i}. Reason: {warning.reason if warning.reason else 'unknown'} • " 72 | f"Warned by {await inter.guild.fetch_member(warning.moderator_id)}\n" 73 | ) 74 | 75 | embed.description = warning_string 76 | embed.set_thumbnail(url=member.display_avatar) 77 | embed.add_field(name="Total Warnings", value=len(warnings)) 78 | embed.set_footer(text=f"Called by {inter.author}") 79 | embed.timestamp = datetime.now() 80 | await inter.send(embed=embed) 81 | 82 | 83 | async def sucEmb(inter: ApplicationCommandInteraction, text, ephemeral=True): 84 | emb = Embed(description=text, color=disnake.Color.green()) 85 | await inter.send( 86 | embed=emb, 87 | ephemeral=ephemeral, 88 | allowed_mentions=disnake.AllowedMentions( 89 | everyone=False, users=False, roles=False, replied_user=True 90 | ), 91 | ) 92 | 93 | 94 | class QuickEmb: 95 | def __init__( 96 | self, inter: ApplicationCommandInteraction, msg, color=0xFFFFFF 97 | ): 98 | self.inter = inter 99 | self.msg = msg 100 | self.color = color 101 | 102 | def error(self): 103 | self.color = disnake.Color.red() 104 | return self 105 | 106 | def success(self): 107 | self.color = disnake.Color.green() 108 | return self 109 | 110 | async def send(self): 111 | emb = Embed(description=self.msg, color=self.color) 112 | await self.inter.send(embed=emb) 113 | 114 | 115 | def manage_messages_perms(inter): 116 | return inter.channel.permissions_for(inter.author).manage_messages 117 | 118 | def manage_role_perms(inter): 119 | return inter.channel.permissions_for(inter.author).manage_roles 120 | 121 | -------------------------------------------------------------------------------- /cogs/Code.py: -------------------------------------------------------------------------------- 1 | import disnake 2 | from disnake import Embed 3 | from disnake import TextInputStyle, Color 4 | from disnake.ext import commands 5 | from disnake.ext.commands import Cog 6 | 7 | from utils.CONSTANTS import VALID_CODE_LANGUAGES 8 | from utils.bot import OGIROID 9 | from utils.http import session 10 | from utils.shortcuts import errorEmb 11 | 12 | 13 | class CodeExec(Cog, name="Code"): 14 | """ 15 | 💻 Run code and get results instantly! 16 | """ 17 | 18 | def __init__(self, bot: OGIROID): 19 | self.bot = bot 20 | 21 | @commands.slash_command( 22 | name="code", 23 | description="Run code and get results instantly. Window for code will pop up.", 24 | ) 25 | async def code(self, inter: disnake.ApplicationCommandInteraction): 26 | """ 27 | Run code and get results instantly 28 | """ 29 | await inter.response.send_modal(modal=CodeModal()) 30 | 31 | 32 | class CodeModal(disnake.ui.Modal): 33 | def __init__(self): 34 | # The details of the modal, and its components 35 | components = [ 36 | disnake.ui.TextInput( 37 | label="Language", 38 | placeholder="Language", 39 | custom_id="language", 40 | style=TextInputStyle.short, 41 | max_length=15, 42 | ), 43 | disnake.ui.TextInput( 44 | label="Code", 45 | placeholder="Write your code here", 46 | custom_id="code", 47 | style=TextInputStyle.paragraph, 48 | ), 49 | ] 50 | super().__init__( 51 | title="Run Code", 52 | custom_id="run_code", 53 | components=components, 54 | ) 55 | 56 | # The callback received when the user input is completed. 57 | async def callback(self, inter: disnake.ModalInteraction): 58 | language = inter.text_values["language"].strip() 59 | if not self._check_valid_lang(language): 60 | embed = disnake.Embed( 61 | title=f"{language} is not a valid language", colour=Color.red() 62 | ) 63 | return await inter.send(embed=embed) 64 | 65 | embed = disnake.Embed(title="Running Code") 66 | embed.add_field( 67 | name="Language", 68 | value=language.capitalize(), 69 | inline=False, 70 | ) 71 | embed.add_field( 72 | name="Code", 73 | value=f"```{language}\n" f"{inter.text_values['code'][:999]}\n" f"```", 74 | inline=False, 75 | ) 76 | await inter.send(embed=embed) 77 | result = await self.run_code(lang=language, code=inter.text_values["code"]) 78 | await self._send_result(inter, result) 79 | 80 | @staticmethod 81 | async def run_code(*, lang: str, code: str): 82 | code = await session.post( 83 | "https://emkc.org/api/v1/piston/execute", 84 | json={"language": lang, "source": code}, 85 | ) 86 | return await code.json() 87 | 88 | @staticmethod 89 | async def _send_result(inter, result: dict): 90 | try: 91 | output = result["output"] 92 | except KeyError: 93 | return await errorEmb(inter, result["message"]) 94 | # if len(output) > 2000: HAVE TO FIX THIS!!!! 95 | # url = await create_guest_paste_bin(self.session, output) 96 | # return await ctx.reply("Your output was too long, so here's the pastebin link " + url) 97 | embed = Embed(title=f"Ran your {result['language']} code", color=0xFFFFFF) 98 | output = output[:500].strip() 99 | shortened = len(output) > 500 100 | lines = output.splitlines() 101 | shortened = shortened or (len(lines) > 15) 102 | output = "\n".join(lines[:15]) 103 | output += shortened * "\n\n**Output shortened**" 104 | embed.add_field(name="Output", value=f"```\n{output}\n```" or "****") 105 | 106 | await inter.send(embed=embed) 107 | 108 | @staticmethod 109 | def _check_valid_lang(param): 110 | if str(param).casefold() not in VALID_CODE_LANGUAGES: 111 | return False 112 | return True 113 | 114 | 115 | def setup(bot: OGIROID): 116 | bot.add_cog(CodeExec(bot)) 117 | -------------------------------------------------------------------------------- /cogs/Snipe.py: -------------------------------------------------------------------------------- 1 | import disnake 2 | from disnake import ApplicationCommandInteraction 3 | from disnake.ext import commands 4 | from disnake.ext.commands import Cog 5 | 6 | from utils.bot import OGIROID 7 | from utils.shortcuts import sucEmb, errorEmb 8 | 9 | 10 | class Utilities(commands.Cog): 11 | def __init__(self, bot: OGIROID): 12 | """Utility Commands""" 13 | self.bot = bot 14 | self.delete_snipes = dict() 15 | self.edit_snipes = dict() 16 | self.delete_snipes_attachments = dict() 17 | 18 | @Cog.listener() 19 | async def on_message_delete(self, message): 20 | self.delete_snipes[message.channel] = message 21 | self.delete_snipes_attachments[message.channel] = message.attachments 22 | 23 | @Cog.listener() 24 | async def on_message_edit(self, before, after): 25 | self.edit_snipes[after.channel] = (before, after) 26 | 27 | @commands.slash_command( 28 | name="snipe", 29 | description="Get the most recently deleted message in a channel", 30 | ) 31 | async def snipe_group(self, inter: ApplicationCommandInteraction): 32 | """Get the most recently deleted message in a channel""" 33 | 34 | try: 35 | sniped_message = self.delete_snipes[inter.channel] 36 | except KeyError: 37 | await errorEmb( 38 | inter, "There are no deleted messages in this channel to snipe!" 39 | ) 40 | else: 41 | if not sniped_message or not sniped_message.content: 42 | await errorEmb( 43 | inter, "There are no deleted messages in this channel to snipe!" 44 | ) 45 | return 46 | result = disnake.Embed( 47 | color=disnake.Color.red(), 48 | description=sniped_message.content[:1024], 49 | timestamp=sniped_message.created_at, 50 | ) 51 | result.set_author( 52 | name=sniped_message.author.display_name, 53 | icon_url=sniped_message.author.avatar.url, 54 | ) 55 | try: 56 | result.set_image( 57 | url=self.delete_snipes_attachments[inter.channel][0].url 58 | ) 59 | except: 60 | pass 61 | is_staff = disnake.utils.find( 62 | lambda r: r.name.lower() == "staff", inter.guild.roles 63 | ) 64 | if is_staff in inter.author.roles: 65 | await inter.send(embed=result) 66 | else: 67 | await inter.send(embed=result, delete_after=15) 68 | 69 | @commands.slash_command( 70 | name="editsnipe", 71 | description="Get the most recently edited message in the channel, before and after.", 72 | ) 73 | async def editsnipe(self, inter: ApplicationCommandInteraction): 74 | try: 75 | before, after = self.edit_snipes[inter.channel] 76 | except KeyError: 77 | await inter.send("There are no message edits in this channel to snipe!") 78 | else: 79 | result = disnake.Embed(color=disnake.Color.red(), timestamp=after.edited_at) 80 | result.add_field(name="Before", value=before.content[:1024], inline=False) 81 | result.add_field(name="After", value=after.content[:1024], inline=False) 82 | result.set_author( 83 | name=after.author.display_name, 84 | icon_url=after.author.avatar.url, 85 | ) 86 | is_staff = disnake.utils.find( 87 | lambda r: r.name.lower() == "staff", inter.guild.roles 88 | ) 89 | if is_staff in inter.author.roles: 90 | await inter.send(embed=result) 91 | else: 92 | await inter.send(embed=result, delete_after=15) 93 | 94 | @commands.slash_command(name="clearsnipe", description="Clears the snipe cache") 95 | @commands.has_permissions(manage_messages=True) 96 | async def clearsnipe(self, inter: ApplicationCommandInteraction): 97 | self.delete_snipes[inter.channel] = None 98 | self.edit_snipes[inter.channel] = None 99 | self.delete_snipes_attachments[inter.channel] = None 100 | await sucEmb(inter, "Snipe cache cleared!") 101 | 102 | 103 | def setup(bot: commands.Bot): 104 | bot.add_cog(Utilities(bot)) 105 | -------------------------------------------------------------------------------- /cogs/Github.py: -------------------------------------------------------------------------------- 1 | import disnake 2 | from disnake.ext import commands 3 | 4 | from utils.bot import OGIROID 5 | 6 | 7 | class GitHub(commands.Cog): 8 | """Commands involving GitHub! :)""" 9 | 10 | def __init__(self, bot: OGIROID): 11 | self.bot = bot 12 | 13 | # Command to get information about a GitHub user 14 | @commands.slash_command( 15 | name="ghperson", description="Gets the Profile of the github person." 16 | ) 17 | async def ghperson(self, inter, ghuser: str): 18 | person_raw = await self.bot.session.get( 19 | f"https://api.github.com/users/{ghuser}" 20 | ) 21 | if person_raw.status != 200: 22 | return await inter.send("User not found!") 23 | else: 24 | person = await person_raw.json() 25 | # Returning an Embed containing all the information: 26 | embed = disnake.Embed( 27 | title=f"GitHub Profile: {person['login']}", 28 | description=f"**Bio:** {person['bio']}", 29 | color=0xFFFFFF, 30 | ) 31 | embed.set_thumbnail(url=f"{person['avatar_url']}") 32 | embed.add_field( 33 | name="Username 📛: ", value=f"{person['name']}", inline=True 34 | ) 35 | # embed.add_field(name="Email ✉: ", value=f"{person['email']}", inline=True) Commented due to GitHub not responding with the correct email 36 | embed.add_field( 37 | name="Repos 📁: ", value=f"{person['public_repos']}", inline=True 38 | ) 39 | embed.add_field( 40 | name="Location 📍: ", value=f"{person['location']}", inline=True 41 | ) 42 | embed.add_field( 43 | name="Company 🏢: ", value=f"{person['company']}", inline=True 44 | ) 45 | embed.add_field( 46 | name="Followers 👥: ", value=f"{person['followers']}", inline=True 47 | ) 48 | embed.add_field( 49 | name="Website 🖥️: ", value=f"{person['blog']}", inline=True 50 | ) 51 | button = disnake.ui.Button( 52 | label="Link", style=disnake.ButtonStyle.url, url=person["html_url"] 53 | ) 54 | await inter.send(embed=embed, components=button) 55 | 56 | # Command to get search for GitHub repositories: 57 | @commands.slash_command( 58 | name="ghsearchrepo", description="Searches for the specified repo." 59 | ) 60 | async def ghsearchrepo(self, inter, query: str): 61 | pages = 1 62 | url = f"https://api.github.com/search/repositories?q={query}&{pages}" 63 | repos_raw = await self.bot.session.get(url) 64 | if repos_raw.status != 200: 65 | return await inter.send("Repo not found!") 66 | else: 67 | repos = ( 68 | await repos_raw.json() 69 | ) # Getting first repository from the query 70 | repo = repos["items"][0] 71 | # Returning an Embed containing all the information: 72 | embed = disnake.Embed( 73 | title=f"GitHub Repository: {repo['name']}", 74 | description=f"**Description:** {repo['description']}", 75 | color=0xFFFFFF, 76 | ) 77 | embed.set_thumbnail(url=f"{repo['owner']['avatar_url']}") 78 | embed.add_field( 79 | name="Author 🖊:", 80 | value=f"__[{repo['owner']['login']}]({repo['owner']['html_url']})__", 81 | inline=True, 82 | ) 83 | embed.add_field( 84 | name="Stars ⭐:", value=f"{repo['stargazers_count']}", inline=True 85 | ) 86 | embed.add_field( 87 | name="Forks 🍴:", value=f"{repo['forks_count']}", inline=True 88 | ) 89 | embed.add_field( 90 | name="Language 💻:", value=f"{repo['language']}", inline=True 91 | ) 92 | embed.add_field( 93 | name="Size 🗃️:", 94 | value=f"{round(repo['size'] / 1000, 2)} MB", 95 | inline=True, 96 | ) 97 | if repo["license"]: 98 | spdx_id = repo["license"]["spdx_id"] 99 | embed.add_field( 100 | name="License name 📃:", 101 | value=f"{spdx_id if spdx_id != 'NOASSERTION' else repo['license']['name']}", 102 | inline=True, 103 | ) 104 | else: 105 | embed.add_field( 106 | name="License name 📃:", 107 | value=f"This Repo doesn't have a license", 108 | inline=True, 109 | ) 110 | button = disnake.ui.Button( 111 | label="Link", style=disnake.ButtonStyle.url, url=repo["html_url"] 112 | ) 113 | await inter.send(embed=embed, components=button) 114 | 115 | 116 | def setup(bot): 117 | bot.add_cog(GitHub(bot)) 118 | -------------------------------------------------------------------------------- /cogs/Welcome.py: -------------------------------------------------------------------------------- 1 | import random 2 | from datetime import datetime 3 | 4 | import disnake 5 | from disnake.ext.commands import Cog 6 | 7 | from utils.bot import OGIROID 8 | 9 | 10 | class Welcome(Cog): 11 | def __init__(self, bot: OGIROID): 12 | self.bot = bot 13 | self.get_channel = self.bot.get_channel 14 | 15 | @Cog.listener() 16 | async def on_member_join(self, member: disnake.Member): 17 | if member.guild.id != self.bot.config.guilds.main_guild: 18 | return 19 | if member.dm_channel is None: 20 | introduction = self.bot.get_channel( 21 | self.bot.config.channels.introduction 22 | ) 23 | general = self.bot.get_channel(self.bot.config.channels.general) 24 | roles = self.bot.get_channel(self.bot.config.channels.roles) 25 | reddit_bot = self.bot.get_channel( 26 | self.bot.config.channels.reddit_bot 27 | ) 28 | rules = self.bot.get_channel(self.bot.config.channels.rules) 29 | 30 | await member.create_dm() 31 | embed = disnake.Embed( 32 | title="Welcome to Lewis Menelaws' Official Discord Server", 33 | description=f"Welcome to the official Discord server {member.name}," 34 | f" please checkout the designated channels.We hope you have a great time here.", 35 | color=0xFFFFFF, 36 | ) 37 | embed.add_field( 38 | name="Chat with other members:", 39 | value=f"Chat with the members, {general.mention}", 40 | inline=True, 41 | ) 42 | embed.add_field( 43 | name="Introductions:", 44 | value=f"Introduce yourself, {introduction.mention}", 45 | inline=True, 46 | ) 47 | embed.add_field( 48 | name="Roles:", 49 | value=f"Select some roles, {roles.mention}", 50 | inline=True, 51 | ) 52 | embed.add_field( 53 | name="Reddit Bot Related:", 54 | value=f"Here for the Reddit bot? Checkout {reddit_bot.mention}", 55 | inline=True, 56 | ) 57 | embed.add_field( 58 | name="Rules:", 59 | value=f"Checkout the rules, {rules.mention}", 60 | inline=True, 61 | ) 62 | embed.set_thumbnail(url=member.display_avatar) 63 | try: 64 | await member.dm_channel.send(embed=embed) 65 | except disnake.Forbidden: 66 | pass # DMs are closed or something else went wrong so we just ignore it and move on with our lives :D 67 | else: 68 | pass 69 | 70 | greetings = [ 71 | "Hello", 72 | "Hi", 73 | "Greetings", 74 | "Hola", 75 | "Bonjour", 76 | "Konnichiwa", 77 | ] 78 | secondary_greeting = [ 79 | "Welcome to Lewis Menelaws' Official Discord Server! Feel free to look around & introduce yourself.", 80 | "Welcome to the server! We wish you have a great time here, make sure you tell us a little bit about yourself.", 81 | "Hope you are doing well! Welcome to the server. How about start by introducing yourself?", 82 | "It's great to have you here, please feel free to look around & introduce yourself.", 83 | "Woohoo! You have made it, please introduce yourself.", 84 | "You have arrived! Feels great to have you here, maybe look around & introduce yourself?", 85 | ] 86 | greeting_emojis = ["👋", "🎊", "🎉", "💻", "🙏", "🤝"] 87 | chan = self.get_channel(self.bot.config.channels.welcome) 88 | 89 | welcome_msg = f"{random.choice(greetings)} {member.mention}! {random.choice(secondary_greeting)}\nWe are now at: {len(member.guild.members)} members!" 90 | msg = await chan.send(welcome_msg) 91 | 92 | await msg.add_reaction(random.choice(greeting_emojis)) 93 | 94 | @Cog.listener() 95 | async def on_member_remove(self, member): 96 | if member.guild.id != self.bot.config.guilds.main_guild: 97 | return 98 | channel = self.get_channel(self.bot.config.channels.logs) 99 | embed = disnake.Embed( 100 | title="Goodbye :(", 101 | description=f"{member.mention} has left the server. There are now `{member.guild.member_count}` members", 102 | color=0xFF0000, 103 | timestamp=datetime.now(), 104 | ) 105 | embed.set_author( 106 | name="Member Left!", 107 | url=f"{member.display_avatar}", 108 | icon_url=f"{member.display_avatar}", 109 | ) 110 | embed.set_image(url=member.display_avatar) 111 | embed.set_footer(text="Member Left") 112 | await channel.send(f"{member.mention} has left!", embed=embed) 113 | 114 | 115 | def setup(bot): 116 | bot.add_cog(Welcome(bot)) 117 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Ogiroid 🤝! 2 | 🎊👏 Firstly, thank you so much for taking out time to contribute to Ogiroid! 👏🎊 3 | 4 | The following is a set of guidelines for contributing to Ogiroid, which is a part of [Lewis Projects](https://github.com/LewisProjects/) on GitHub. These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request. 5 | 6 | ### Table of Contents: 7 | 1. [Code of Conduct](#code-of-conduct) 8 | 2. [What do I require before getting started?](#prerequisites) 9 | 3. [What can I do to contribute?](#contribution) 10 | 4. [Additional Notes]() 11 | 12 |
    13 | 14 | ## [Code of Conduct](#code-of-conduct) 15 | > This project and everyone participating in it is governed by the Ogiroid code of conduct. By participating, you are expected to uphold this code. Please report unacceptable behavior to the developers. 16 | 17 | ## [What do I require before getting started?](#prerequisites) 18 | We expect you to be experienced with [Python](https://www.python.org/) and [Disnake](https://disnake.dev/) (a fork of [Discord.py](https://github.com/Rapptz/discord.py)). Having experience with Discord.py should also get you sailing, disnake and discord.py are almost the same, or you could also take a look at the very helpful disnake documentation. 19 | 20 | ## [What can I do to contribute?](#contribution) 21 | **1. Reporting any bugs 🐞:** 22 | 23 | Cannot emphasize enough how much bug reporting/bug fixing helps us! It saves us numerous hours of painful code scouting. So you might ask yourself, how do I submit a bug report for Ogiroid? Quite simple honestly! First of all, did you: 24 | + Come across a bug while using the Discord bot? 25 | + Come across a bug in the code? 26 | 27 | **Case 1: While using the Discord Bot:** 28 | 29 | You need to headover to a channel in [Lewis' Menelaws discord server](https://discord.com/5uw4eCQf6Z) and then type: ``/reportbug``, you will then be greeted by a modal looking something like this: 30 | 31 | ![MODAL](https://cdn.discordapp.com/attachments/1005117336761675847/1007706426896040077/unknown.png) 32 | 33 | Fill this form & click **submit**. Our developer team will then recieve your bug report, we can then look into it 😇! Thank you! 34 | 35 | **Case 2: A bug in the code (hmmm... quite rare 😜):** 36 | 37 | You can raise an [issue](https://github.com/LewisProjects/Ogiroid/issues) and we will look into it. Please ensure you follow the proper [format we have](#) and be as specific as you can while reporting a bug. Obviously, there can be somee cases where the format might not be as important as in other cases, please use your judgement in such cases. 38 | 39 | Alternatively, you can headover to our [Discord Server](https://discord.com/5uw4eCQf6Z) and report the bug using the first method (scroll up if you missed it). 40 | 41 | **__📚📚 Bug Reporting Guidelines 📚📚:__** 42 | 43 | + Please refrain from reporting your bug multiple times, this will lead to us blacklisting you (and potentially banning you permanently on the Discord server) 😒. 44 | + Please check all the [issues](https://github.com/LewisProjects/Ogiroid/issues) before posting your bug, your bug report could really just be a duplicate 👀! 45 | + Please ensure you be as specific as you can, this saves us a ton of time ⌚. 46 | + Please use your right judgement 🎓. 47 | 48 | With that being said, you have officially reported a bug! Thank you so much 🤩! 49 | 50 | 51 | 52 | ### Contributing Code 53 | 54 |
    55 | 56 | #### Get the repository 57 | 58 | Fork the repository and then clone it using: (make sure to insert your username) 59 | 60 | ```git clone https://github.com/YOURGITHUBUSERNAME/Ogiroid.git``` 61 | 62 | After this get into the folder you cloned the repository to. 63 | We always work on the development branch so make sure you are on the dev branch. 64 | 65 | ```git checkout development``` 66 | 67 | Now you need to create a Discord Bot if you don't already have one. Please look up a guide for how to do this. 68 | Invite the bot to a test server you own or create a test server. 69 | 70 | Now copy secrets.env.template and rename the copy to secrets.env 71 | Insert your bots token in the correct field. 72 | Add a postgres database URL we use a database from neon.tech for testing you can use a local database. 73 | Set development to true. The rest can be ignored for now. 74 | 75 | #### Install the requirements 76 | 77 | To install the requirements: 78 | 79 | ```pip install -r requirements.txt``` 80 | 81 | #### Run the Bot 82 | 83 | And finally to run the bot: 84 | 85 | ```python main.py``` 86 | 87 | utils/CONSTANTS.py stores the ids for various channels, for the main server and for the official development server, to wich you will gain access after a significant contribution. 88 | 89 | Use ```black . --safe``` for formatting (need to install black with pip) 90 | 91 | #### Database Migrations 92 | 93 | ```alembic revision --autogenerate``` 94 | 95 | ```alembic upgrade head``` 96 | 97 | #### Help 98 | 99 | If you need any help contact us on discord or open an issue. 100 | 101 | After you have finished writing the code open a PR with the base branch being development. 102 | -------------------------------------------------------------------------------- /utils/bot.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import os 3 | from datetime import datetime 4 | 5 | from dotenv import load_dotenv 6 | from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker 7 | 8 | import disnake 9 | from disnake import ApplicationCommandInteraction, OptionType 10 | from disnake.ext import commands 11 | 12 | from utils.CONSTANTS import __VERSION__ 13 | from utils.DBhandlers import BlacklistHandler 14 | from utils.cache import async_cache 15 | from utils.config import Config 16 | from utils.exceptions import UserBlacklisted 17 | from utils.http import HTTPSession 18 | from utils.shortcuts import errorEmb 19 | 20 | 21 | class OGIROID(commands.InteractionBot): 22 | def __init__(self, *args, **kwargs): 23 | super().__init__( 24 | intents=disnake.Intents.all(), 25 | command_sync_flags=commands.CommandSyncFlags(sync_commands_debug=True), 26 | *args, 27 | **kwargs, 28 | ) 29 | self._ready_ = False 30 | self.uptime = datetime.now() 31 | self.session = HTTPSession(loop=self.loop) 32 | self.config = Config() 33 | self.commands_ran = {} 34 | self.total_commands_ran = {} 35 | self.db = None 36 | self.blacklist: BlacklistHandler = None 37 | self.add_app_command_check( 38 | self.blacklist_check, slash_commands=True, call_once=True 39 | ) 40 | 41 | async def blacklist_check(self, ctx): 42 | try: 43 | await self.wait_until_ready() 44 | if await self.blacklist.blacklisted(ctx.author.id): 45 | await errorEmb(ctx, "You are blacklisted from using this bot!") 46 | raise UserBlacklisted 47 | return True 48 | except AttributeError: 49 | pass # DB hasn't loaded yet 50 | 51 | @async_cache(maxsize=0) 52 | async def on_slash_command(self, inter: ApplicationCommandInteraction): 53 | COMMAND_STRUCT = [inter.data] 54 | do_break = False 55 | while True: 56 | COMMAND = COMMAND_STRUCT[-1] 57 | if not COMMAND.options: 58 | if inter.data == COMMAND: 59 | COMMAND_STRUCT = [inter.data] 60 | break 61 | COMMAND_STRUCT = [inter.data, COMMAND] 62 | break 63 | for option in COMMAND.options: 64 | if option.options: 65 | COMMAND_STRUCT.append(option) 66 | do_break = False 67 | elif option.type in [ 68 | OptionType.sub_command_group, 69 | OptionType.sub_command, 70 | ]: 71 | COMMAND_STRUCT.append(option) 72 | else: 73 | do_break = True 74 | break 75 | if do_break: 76 | break 77 | 78 | COMMAND_NAME = " ".join([command.name for command in COMMAND_STRUCT]) 79 | 80 | try: 81 | self.total_commands_ran[inter.guild.id] += 1 82 | except KeyError: 83 | self.total_commands_ran[inter.guild.id] = 1 84 | 85 | if self.commands_ran.get(inter.guild.id) is None: 86 | self.commands_ran[inter.guild.id] = {} 87 | 88 | try: 89 | self.commands_ran[inter.guild.id][COMMAND_NAME] += 1 90 | except KeyError: 91 | self.commands_ran[inter.guild.id][COMMAND_NAME] = 1 92 | 93 | async def on_ready(self): 94 | if not self._ready_: 95 | await self.wait_until_ready() 96 | await self._setup() 97 | await self.change_presence( 98 | activity=disnake.Activity( 99 | type=disnake.ActivityType.listening, name="the users!" 100 | ) 101 | ) 102 | print( 103 | "--------------------------------------------------------------------------------" 104 | ) 105 | print("Bot is ready! Logged in as: " + self.user.name) 106 | print("Bot devs: HarryDaDev | FreebieII | JasonLovesDoggo | Levani") 107 | print(f"Bot version: {__VERSION__}") 108 | print( 109 | "--------------------------------------------------------------------------------" 110 | ) 111 | await asyncio.sleep( 112 | 5 113 | ) # Wait 5 seconds for the bot to load the database and setup 114 | self._ready_ = True 115 | 116 | else: 117 | print("Bot reconnected") 118 | 119 | async def _setup(self): 120 | # for command in self.application_commands: 121 | # self.commands_ran[f"{command.qualified_name}"] = 0 122 | self.blacklist: BlacklistHandler = BlacklistHandler(self, self.db) 123 | await self.blacklist.startup() 124 | 125 | async def load_db(self): 126 | pass 127 | 128 | async def start(self, *args, **kwargs): 129 | engine = create_async_engine( 130 | self.config.Database.connection_string, pool_pre_ping=True 131 | ) 132 | self.db = async_sessionmaker(engine, expire_on_commit=False) 133 | await super().start(*args, **kwargs) 134 | 135 | @property 136 | def ready_(self): 137 | return self._ready_ 138 | -------------------------------------------------------------------------------- /cogs/Math.py: -------------------------------------------------------------------------------- 1 | from io import BytesIO 2 | 3 | import disnake 4 | import expr 5 | from PIL import Image 6 | from better_profanity import profanity 7 | from disnake import TextInputStyle 8 | from disnake.ext import commands 9 | import urllib.parse 10 | 11 | from utils.http import session 12 | from utils.bot import OGIROID 13 | from utils.shortcuts import QuickEmb, errorEmb 14 | 15 | 16 | class Math(commands.Cog): 17 | """Do Math""" 18 | 19 | def __init__(self, bot: OGIROID): 20 | self.bot = bot 21 | 22 | @commands.slash_command(description="Evaluates a math equation.") 23 | async def math(self, inter, equation): 24 | """Evaluates a math equation""" 25 | equation = equation.replace("×", "*") 26 | try: 27 | answer = expr.evaluate(equation) 28 | except expr.errors.InvalidSyntax: 29 | await errorEmb( 30 | inter, 31 | "You used invalid syntax in your equation.\n" 32 | "If you need help use the ``/mathhelp`` command.\n" 33 | "If this is a bug report it with the ``/reportbug`` command.", 34 | ) 35 | except expr.errors.DivisionByZero: 36 | await errorEmb(inter, "You know that you can't divide by zero.") 37 | except expr.errors.Gibberish: 38 | await errorEmb( 39 | inter, 40 | "Are you sure that is a valid equation?\n" 41 | "If you need help use the ``/mathhelp`` command.\n" 42 | "If this is a bug report it with the ``/reportbug`` command.", 43 | ) 44 | except Exception as e: 45 | await errorEmb( 46 | inter, 47 | f"{e}\n" 48 | f"If you need help use the ``/mathhelp`` command." 49 | "If this is a bug report it with the ``/reportbug`` command.", 50 | ) 51 | else: 52 | await QuickEmb(inter, f"Result: ```{answer}```").success().send() 53 | 54 | @commands.slash_command( 55 | name="mathhelp", description="Help for the ``/math`` command" 56 | ) 57 | async def math_help(self, inter): 58 | await QuickEmb( 59 | inter, 60 | "The following operations are supported:\n" 61 | " `+` (addition)\n `-` (subtraction)\n `*` (multiplication)\n `/` (division) \n" 62 | "`//` (floor division)\n `%` (modulo)\n `^` (exponentation)\n `!` (factorial)\n" 63 | "Aswell as these:\n" 64 | " `sqrt` | `cbrt` | `log` | `log10` | `ln` | `rad` | `sin` | `cos` | `tan` | `asin` | `acos` | `atan`", 65 | ).send() 66 | 67 | @commands.slash_command(description="Latex to Image") 68 | async def latex(self, inter): 69 | """Latex to Image""" 70 | await inter.response.send_modal(modal=LatexModal()) 71 | 72 | 73 | class LatexModal(disnake.ui.Modal): 74 | def __init__(self): 75 | # The details of the modal, and its components 76 | components = [ 77 | disnake.ui.TextInput( 78 | label="Latex", 79 | placeholder="Latex format here", 80 | custom_id="latex", 81 | style=TextInputStyle.paragraph, 82 | max_length=4000, 83 | ), 84 | ] 85 | super().__init__( 86 | title="Convert", 87 | custom_id="convert", 88 | components=components, 89 | ) 90 | 91 | # The callback received when the user input is completed. 92 | async def callback(self, inter: disnake.ModalInteraction): 93 | try: 94 | await inter.response.defer() 95 | latex = inter.text_values["latex"].strip() 96 | if profanity.contains_profanity(latex): 97 | return await errorEmb(inter, "No profanity allowed.") 98 | async with session.post( 99 | r"https://latex.codecogs.com/png.latex?\dpi{180}\bg_white\large" 100 | + urllib.parse.quote(" " + latex) 101 | ) as resp: 102 | # Read the content from the response 103 | image_data = await resp.read() 104 | 105 | # Open the image using PIL 106 | with Image.open(BytesIO(image_data)) as image: 107 | # Add 5 pixels of padding on all sides 108 | padding_size = 5 109 | padded_image = Image.new( 110 | "RGB", 111 | ( 112 | image.width + 2 * padding_size, 113 | image.height + 2 * padding_size, 114 | ), 115 | "white", 116 | ) 117 | padded_image.paste(image, (padding_size, padding_size)) 118 | 119 | # Save the image to a BytesIO buffer 120 | image_buffer = BytesIO() 121 | padded_image.save(image_buffer, "png") 122 | image_buffer.seek(0) 123 | 124 | # Send the image 125 | await inter.send(file=disnake.File(image_buffer, filename="latex.png")) 126 | 127 | except Exception as e: 128 | print(e) 129 | await errorEmb(inter, f"Error: {e}") 130 | 131 | 132 | def setup(bot: OGIROID): 133 | bot.add_cog(Math(bot)) 134 | -------------------------------------------------------------------------------- /cogs/Timezone.py: -------------------------------------------------------------------------------- 1 | import datetime as dt 2 | 3 | import disnake 4 | import pytz 5 | from disnake.ext import commands 6 | 7 | from utils.CONSTANTS import timezones 8 | from utils.DBhandlers import TimezoneHandler 9 | from utils.bot import OGIROID 10 | from utils.exceptions import UserAlreadyExists, UserNotFound 11 | from utils.shortcuts import QuickEmb, sucEmb, errorEmb 12 | 13 | 14 | async def autocomplete_timezones(inter, user_input: str): 15 | return [tz for tz in timezones if user_input.lower() in tz.lower()][0:25] 16 | 17 | 18 | class Timezone(commands.Cog): 19 | def __init__(self, bot: OGIROID): 20 | self.bot = bot 21 | self.db_timezone: TimezoneHandler = None 22 | 23 | @commands.Cog.listener() 24 | async def on_ready(self): 25 | if not self.bot.ready_: 26 | self.db_timezone: TimezoneHandler = TimezoneHandler(self.bot, self.bot.db) 27 | 28 | @commands.slash_command(name="timezone", description="Timezone base command") 29 | async def timezone(self, inter: disnake.ApplicationCommandInteraction): 30 | pass 31 | 32 | @timezone.sub_command(name="set", description="Set your timezone.") 33 | async def set( 34 | self, 35 | inter, 36 | timezone=commands.Param( 37 | name="timezone", 38 | description="Your timezone. Start typing a major city around you or a continent.", 39 | autocomplete=autocomplete_timezones, 40 | ), 41 | ): 42 | await inter.response.defer() 43 | if timezone is None: 44 | return await errorEmb(inter, "You need to provide a timezone") 45 | elif timezone not in timezones: 46 | return await errorEmb(inter, "The timezone you provided is not valid") 47 | elif timezone == "Europe/Tbilisi": 48 | timezone = "Asia/Tbilisi" 49 | display_timezone = "Europe/Tbilisi" 50 | else: 51 | display_timezone = timezone 52 | 53 | try: 54 | await self.db_timezone.create_user(inter.author.id, timezone) 55 | except UserAlreadyExists: 56 | return await errorEmb(inter, "You already have a timezone set") 57 | 58 | await sucEmb( 59 | inter, 60 | f"Your timezone has been set to {display_timezone}." 61 | f" Its should be {dt.datetime.now(pytz.timezone(timezone)).strftime('%H:%M')} at your location.", 62 | ) 63 | 64 | @timezone.sub_command(name="edit", description="Edit your timezone.") 65 | async def edit( 66 | self, 67 | inter, 68 | timezone=commands.Param( 69 | name="timezone", 70 | description="Your timezone. Start typing a major city around you or a continent.", 71 | autocomplete=autocomplete_timezones, 72 | ), 73 | ): 74 | await inter.response.defer() 75 | if timezone is None: 76 | return await errorEmb(inter, "You need to provide a timezone") 77 | elif timezone not in timezones: 78 | return await errorEmb(inter, "The timezone you provided is not valid") 79 | # handles tbilisi cause its annoying me 80 | elif timezone == "Europe/Tbilisi": 81 | timezone = "Asia/Tbilisi" 82 | display_timezone = "Europe/Tbilisi" 83 | else: 84 | display_timezone = timezone 85 | 86 | try: 87 | await self.db_timezone.update_user(inter.author.id, timezone) 88 | await sucEmb( 89 | inter, 90 | f"Your timezone has been set to {display_timezone}." 91 | f" It should be {dt.datetime.now(pytz.timezone(timezone)).strftime('%H:%M')} at your location.", 92 | ) 93 | except UserNotFound: 94 | return await errorEmb(inter, "The User doesn't have a timezone set") 95 | 96 | @timezone.sub_command(name="remove", description="Remove your timezone.") 97 | async def remove( 98 | self, 99 | inter: disnake.ApplicationCommandInteraction, 100 | ): 101 | await inter.response.defer() 102 | try: 103 | await self.db_timezone.delete_user(inter.author.id) 104 | except UserNotFound: 105 | return await errorEmb(inter, "This user doesn't have a timezone set") 106 | 107 | await sucEmb(inter, "The timezone has been removed") 108 | 109 | @timezone.sub_command(name="get", description="Get the timezone of a user") 110 | async def get( 111 | self, 112 | inter, 113 | user: disnake.User = commands.Param(name="user", default=None), 114 | ): 115 | await inter.response.defer() 116 | if user is None: 117 | user = inter.author 118 | else: 119 | user = await self.bot.fetch_user(user.id) 120 | 121 | timezone = await self.db_timezone.get_user(user.id) 122 | if timezone is None: 123 | return await errorEmb(inter, "This user doesn't have a timezone set") 124 | 125 | # Handles tbilisi naming cause its annoying me. 126 | if timezone.timezone == "Asia/Tbilisi": 127 | display_timezone = "Europe/Tbilisi" 128 | else: 129 | display_timezone = timezone.timezone 130 | 131 | await QuickEmb( 132 | inter, 133 | f"{user.mention}'s timezone is {display_timezone}." 134 | f" Its currently {dt.datetime.now(pytz.timezone(timezone.timezone)).strftime('%H:%M')} for them", 135 | ).send() 136 | 137 | 138 | def setup(bot): 139 | bot.add_cog(Timezone(bot)) 140 | -------------------------------------------------------------------------------- /cogs/Animals.py: -------------------------------------------------------------------------------- 1 | import disnake 2 | from disnake.ext import commands 3 | from utils.bot import OGIROID 4 | 5 | 6 | class Animals(commands.Cog): 7 | """Animals related commands!""" 8 | 9 | def __init__(self, bot: OGIROID): 10 | self.bot = bot 11 | 12 | @commands.slash_command(description="Gets a random picture of the specified animal") 13 | @commands.cooldown(1, 3, commands.BucketType.user) 14 | async def animal(self, inter): 15 | pass 16 | 17 | @animal.sub_command(name="catfact", description="Get a random cat fact") 18 | async def catfact(self, inter): 19 | async with self.bot.session.get("https://catfact.ninja/fact") as response: 20 | data = await response.json() 21 | fact = data["fact"] 22 | length = data["length"] 23 | embed = disnake.Embed( 24 | title=f"Random Cat Fact Number: {length}", 25 | description=f"Cat Fact: {fact}", 26 | color=0x400080, 27 | ) 28 | embed.set_footer( 29 | text=f"Command issued by: {inter.author.name}", 30 | icon_url=inter.author.display_avatar, 31 | ) 32 | await inter.send(embed=embed) 33 | 34 | @animal.sub_command(name="cat", description="Get a random cat picture") 35 | async def cat(self, inter): 36 | """Get a random cat picture!""" 37 | response = await self.bot.session.get("https://some-random-api.com/animal/cat") 38 | data = await response.json() 39 | embed = disnake.Embed( 40 | title="Cat Picture! 🐱", 41 | description="Get a picture of a cat!", 42 | color=0xFFFFFF, 43 | ) 44 | embed.set_image(url=data["image"]) 45 | embed.set_footer( 46 | text=f"Command issued by: {inter.author.name}", 47 | icon_url=inter.author.display_avatar, 48 | ) 49 | await inter.send(f"**Fun Fact: **" + data["fact"], embed=embed) 50 | 51 | @animal.sub_command(name="dog", description="Get a random dog picture") 52 | async def dog(self, inter): 53 | """Get a random dog picture!""" 54 | response = await self.bot.session.get("https://some-random-api.com/animal/dog") 55 | data = await response.json() 56 | embed = disnake.Embed( 57 | title="Dog Picture! 🐶", 58 | description="Get a picture of a dog!", 59 | color=0xFFFFFF, 60 | ) 61 | embed.set_image(url=data["image"]) 62 | embed.set_footer( 63 | text=f"Command issued by: {inter.author.name}", 64 | icon_url=inter.author.display_avatar, 65 | ) 66 | await inter.send("**Fun Fact: **" + data["fact"], embed=embed) 67 | 68 | @animal.sub_command(name="bird", description="Get a random bird picture") 69 | async def bird(self, inter): 70 | """Get a random bird picture!""" 71 | response = await self.bot.session.get("https://some-random-api.com/animal/bird") 72 | data = await response.json() 73 | embed = disnake.Embed( 74 | title="Bird Picture! 🐦", 75 | description="Get a picture of a bird!", 76 | color=0xFFFFFF, 77 | ) 78 | embed.set_image(url=data["image"]) 79 | embed.set_footer( 80 | text=f"Command issued by: {inter.author.name}", 81 | icon_url=inter.author.display_avatar, 82 | ) 83 | await inter.send("**Fun Fact: **" + data["fact"], embed=embed) 84 | 85 | @animal.sub_command(name="fox", description="Get a random fox picture") 86 | async def fox(self, inter): 87 | """Get a random fox picture!""" 88 | response = await self.bot.session.get("https://some-random-api.com/animal/fox") 89 | data = await response.json() 90 | embed = disnake.Embed( 91 | title="Fox Picture! 🦊", 92 | description="Get a picture of a fox!", 93 | color=0xFFFFFF, 94 | ) 95 | embed.set_image(url=data["image"]) 96 | embed.set_footer( 97 | text=f"Command issued by: {inter.author.name}", 98 | icon_url=inter.author.display_avatar, 99 | ) 100 | await inter.send("**Fun Fact: **" + data["fact"], embed=embed) 101 | 102 | @animal.sub_command(name="panda", description="Get a random panda picture") 103 | async def panda(self, inter): 104 | """Get a random panda picture!""" 105 | response = await self.bot.session.get( 106 | "https://some-random-api.com/animal/panda" 107 | ) 108 | data = await response.json() 109 | embed = disnake.Embed( 110 | title="Panda Picture! 🐼", 111 | description="Get a picture of a panda!", 112 | color=0xFFFFFF, 113 | ) 114 | embed.set_image(url=data["image"]) 115 | embed.set_footer( 116 | text=f"Command issued by: {inter.author.name}", 117 | icon_url=inter.author.display_avatar, 118 | ) 119 | await inter.send("**Fun Fact: **" + data["fact"], embed=embed) 120 | 121 | @animal.sub_command(name="koala", description="Get a random cat picture") 122 | async def koala(self, inter): 123 | """Get a random koala picture!""" 124 | response = await self.bot.session.get( 125 | "https://some-random-api.com/animal/koala" 126 | ) 127 | data = await response.json() 128 | embed = disnake.Embed( 129 | title="Koala Picture! 🐨", 130 | description="Get a picture of a koala!", 131 | color=0xFFFFFF, 132 | ) 133 | embed.set_image(url=data["image"]) 134 | embed.set_footer( 135 | text=f"Command issued by: {inter.author.name}", 136 | icon_url=inter.author.display_avatar, 137 | ) 138 | await inter.send("**Fun Fact: **" + data["fact"], embed=embed) 139 | 140 | 141 | def setup(bot): 142 | bot.add_cog(Animals(bot)) 143 | -------------------------------------------------------------------------------- /cogs/Info.py: -------------------------------------------------------------------------------- 1 | import disnake 2 | from disnake import ApplicationCommandInteraction 3 | from disnake.ext import commands 4 | 5 | from utils.bot import OGIROID 6 | from utils.exceptions import CityNotFound 7 | from utils.shortcuts import errorEmb 8 | from utils.wrappers.OpenWeatherMap import OpenWeatherAPI 9 | import requests 10 | from utils.CONSTANTS import CURRENCIES 11 | 12 | 13 | async def autocomplete_currencies(inter, user_input: str): 14 | return [x for x in CURRENCIES.values() if user_input.lower() in x.lower()][0:25] 15 | 16 | 17 | class Info(commands.Cog): 18 | def __init__(self, bot: OGIROID): 19 | self.bot = bot 20 | self.openweathermap_api_key = self.bot.config.tokens.weathermap 21 | self.openweather = OpenWeatherAPI( 22 | key=self.openweathermap_api_key, session=self.bot.session 23 | ) 24 | 25 | @commands.slash_command( 26 | description="Get current weather for specific city", 27 | ) 28 | @commands.cooldown(1, 5, commands.BucketType.user) 29 | async def weather( 30 | self, 31 | inter, 32 | *, 33 | city, 34 | private: bool = commands.Param( 35 | False, description="Send weather privately, not exposing location" 36 | ), 37 | ): 38 | if not self.openweather.apiKey: 39 | return await errorEmb( 40 | inter, 41 | "OpenWeather's API Key is not set! Please use the ``/reportbug`` command to report this issue", 42 | ) 43 | 44 | try: 45 | weatherData = await self.openweather.get_from_city(city) 46 | except CityNotFound as err: 47 | return await errorEmb(inter, str(err)) 48 | 49 | e = disnake.Embed( 50 | title=f"{weatherData.city}, {weatherData.country}", 51 | description=f"Feels like {round(weatherData.tempFeels.celcius)}\N{DEGREE SIGN}C, {weatherData.weatherDetail}", 52 | colour=disnake.Colour(0xEA6D4A), 53 | ) 54 | e.set_author( 55 | name="OpenWeather", 56 | icon_url="https://openweathermap.org/themes/openweathermap/assets/vendor/owm/img/icons/logo_60x60.png", 57 | ) 58 | e.add_field( 59 | name="Temperature", 60 | value=f"{round(weatherData.temp.celcius)}\N{DEGREE SIGN}C", 61 | ) 62 | e.add_field(name="Humidity", value=weatherData.humidity) 63 | e.add_field(name="Wind", value=str(weatherData.wind)) 64 | e.set_thumbnail(url=weatherData.iconUrl) 65 | if private: 66 | await inter.send(embed=e, ephemeral=True) 67 | else: 68 | await inter.send(embed=e) 69 | 70 | @commands.slash_command(description="Display current price of BTC") 71 | @commands.cooldown(1, 5, commands.BucketType.user) 72 | async def btc(self, inter): 73 | response = requests.get("https://shoppy.gg/api/v1/public/ticker") 74 | data = response.json() 75 | btc_prices = [] 76 | for ticker in data.get("ticker", []): 77 | if ticker.get("coin") == "BTC": 78 | btc_prices = ticker.get("value", {}) 79 | btc_price_usd = btc_prices.get("USD") 80 | 81 | embed = disnake.Embed( 82 | title=f"Current BTC Price", 83 | description=f"Bitcoin (USD): ${btc_price_usd}", 84 | color=self.bot.config.colors.white, 85 | ) 86 | await inter.send(embed=embed) 87 | 88 | @commands.slash_command(description="Convert currencies to other currencies") 89 | @commands.cooldown(1, 5, commands.BucketType.user) 90 | async def exchange( 91 | self, 92 | inter: ApplicationCommandInteraction, 93 | base=commands.Param( 94 | description="Base currency to convert from", 95 | autocomplete=autocomplete_currencies, 96 | ), 97 | target=commands.Param( 98 | description="Target currency to convert to", 99 | autocomplete=autocomplete_currencies, 100 | ), 101 | amount=commands.Param(description="Amount of money to convert", default=1), 102 | ): 103 | await inter.response.defer() 104 | try: 105 | amount = float(amount) 106 | except ValueError: 107 | return await errorEmb(inter, "Amount must be a number") 108 | 109 | try: 110 | base = base.upper().split(" ")[0] 111 | target = target.upper().split(" ")[0] 112 | except AttributeError or IndexError: 113 | return await errorEmb(inter, "Currency must be a string") 114 | 115 | if base not in CURRENCIES.values() and base not in CURRENCIES.keys(): 116 | return await errorEmb(inter, "Base currency not found") 117 | if target not in CURRENCIES.values() and target not in CURRENCIES.keys(): 118 | return await errorEmb(inter, "Target currency not found") 119 | 120 | response = requests.get( 121 | f"https://openexchangerates.org/api/latest.json?app_id={self.bot.config.tokens.currency}&symbols={target}" 122 | ) 123 | data = response.json() 124 | 125 | rates = data.get("rates") 126 | target_rate = rates.get(target) 127 | usd_amount = amount * target_rate 128 | # Since it doesn't support changing the base currency on the free tier, we have to do this math stuff 129 | if not base == "USD": 130 | response = requests.get( 131 | f"https://openexchangerates.org/api/latest.json?app_id={self.bot.config.tokens.currency}&symbols={base}" 132 | ) 133 | data = response.json() 134 | rates = data.get("rates") 135 | base_rate = rates.get(base) 136 | converted_amount = usd_amount / base_rate 137 | else: 138 | converted_amount = usd_amount 139 | 140 | embed = disnake.Embed( 141 | title=f"Currency Conversion", 142 | description=f"{amount} {base} = {round(converted_amount, 2)} {target}", 143 | color=self.bot.config.colors.white, 144 | ) 145 | embed.timestamp = disnake.utils.utcnow() 146 | 147 | await inter.send(embed=embed) 148 | 149 | 150 | def setup(bot): 151 | bot.add_cog(Info(bot)) 152 | -------------------------------------------------------------------------------- /utils/db_models.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from sqlalchemy import ( 4 | Column, 5 | Integer, 6 | BigInteger, 7 | Text, 8 | Boolean, 9 | UniqueConstraint, 10 | ARRAY, 11 | ) 12 | from sqlalchemy.orm import declarative_base 13 | 14 | from utils.CONSTANTS import LEVELS_AND_XP 15 | from utils.shortcuts import get_expiry 16 | 17 | Base = declarative_base() 18 | 19 | 20 | class Tag(Base): 21 | __tablename__ = "tags" 22 | name = Column(Text, primary_key=True) 23 | content = Column(Text) 24 | owner = Column(BigInteger) 25 | created_at = Column(BigInteger) 26 | views = Column(Integer) 27 | 28 | 29 | class TagRelations(Base): 30 | __tablename__ = "tag_relations" 31 | id = Column(Integer, primary_key=True) 32 | name = Column(Text) 33 | alias = Column(Text) 34 | 35 | 36 | class Blacklist(Base): 37 | __tablename__ = "blacklist" 38 | id = Column(Integer, primary_key=True) 39 | user_id = Column(BigInteger) 40 | reason = Column(Text) 41 | bot = Column(Boolean) 42 | tickets = Column(Boolean) 43 | tags = Column(Boolean) 44 | expires = Column(BigInteger) 45 | 46 | def is_expired(self): 47 | if self.expires == 9999999999: 48 | return False 49 | return int(time.time()) > self.expires 50 | 51 | @property 52 | def get_expiry(self): 53 | return get_expiry(self.expires) 54 | 55 | 56 | class FlagQuiz(Base): 57 | __tablename__ = "flag_quiz" 58 | id = Column(BigInteger, primary_key=True) 59 | user_id = Column(BigInteger) 60 | tries = Column(Integer) 61 | correct = Column(Integer) 62 | completed = Column(Integer) 63 | guild_id = Column(BigInteger) 64 | 65 | 66 | class Trivia(Base): 67 | __tablename__ = "trivia" 68 | id = Column(BigInteger, primary_key=True) 69 | user_id = Column(BigInteger) 70 | correct = Column(Integer) 71 | incorrect = Column(Integer) 72 | streak = Column(Integer) 73 | longest_streak = Column(Integer) 74 | 75 | 76 | class ReactionRole(Base): 77 | __tablename__ = "reaction_roles" 78 | id = Column(Integer, primary_key=True) 79 | message_id = Column(BigInteger) 80 | role_id = Column(BigInteger) 81 | emoji = Column(Text) 82 | roles_given = Column(Integer, default=0) 83 | 84 | 85 | class Warnings(Base): 86 | __tablename__ = "warnings" 87 | warning_id = Column(Integer, primary_key=True) 88 | user_id = Column(BigInteger) 89 | moderator_id = Column(BigInteger) 90 | reason = Column(Text) 91 | guild_id = Column(BigInteger) 92 | 93 | 94 | class Levels(Base): 95 | __tablename__ = "levels" 96 | id = Column(Integer, primary_key=True) 97 | guild_id = Column(BigInteger) 98 | user_id = Column(BigInteger) 99 | total_xp = Column(Integer, default=0) 100 | 101 | @property 102 | def level(self): 103 | # get users level based on total xp 104 | for level, xp in LEVELS_AND_XP.items(): 105 | if self.total_xp < xp: 106 | return level - 1 107 | 108 | @property 109 | def xp(self): 110 | # get users xp based on total xp 111 | for level, xp in LEVELS_AND_XP.items(): 112 | if self.total_xp < xp: 113 | return self.total_xp - LEVELS_AND_XP[level - 1] 114 | 115 | 116 | class CustomRoles(Base): 117 | __tablename__ = "custom_roles" 118 | id = Column(Integer, primary_key=True) 119 | guild_id = Column(BigInteger) 120 | role_id = Column(BigInteger) 121 | user_id = Column(BigInteger) 122 | 123 | 124 | class RoleReward(Base): 125 | __tablename__ = "role_rewards" 126 | id = Column(Integer, primary_key=True) 127 | guild_id = Column(BigInteger) 128 | role_id = Column(BigInteger) 129 | required_lvl = Column(Integer, default=0) 130 | 131 | 132 | class Birthday(Base): 133 | __tablename__ = "birthday" 134 | id = Column(Integer, primary_key=True) 135 | user_id = Column(BigInteger) 136 | birthday = Column(Text, default=None) 137 | birthday_last_changed = Column(BigInteger, default=None) 138 | 139 | 140 | class Timezone(Base): 141 | __tablename__ = "timezone" 142 | id = Column(Integer, primary_key=True) 143 | user_id = Column(BigInteger) 144 | timezone = Column(Text, default=None) 145 | timezone_last_changed = Column(BigInteger, default=None) 146 | 147 | 148 | class Config(Base): 149 | __tablename__ = "config" 150 | guild_id = Column(BigInteger, primary_key=True) 151 | xp_boost = Column(Integer, default=1) 152 | xp_boost_expiry = Column(BigInteger, default=0) 153 | xp_boost_enabled = Column(Boolean, default=True) 154 | custom_roles_threshold = Column(Integer, default=20) 155 | min_required_lvl = Column(Integer, default=5) 156 | position_role_id = Column(BigInteger, default=None) 157 | 158 | @property 159 | def boost_expired(self): 160 | from time import time 161 | 162 | now = int(time()) 163 | if self.xp_boost_expiry >= now: 164 | return False 165 | return True 166 | 167 | @property 168 | def boost_time_left(self): 169 | from time import time 170 | 171 | now = int(time()) 172 | return self.xp_boost_expiry - now 173 | 174 | @property 175 | def get_boost(self): 176 | return self.xp_boost 177 | 178 | @property 179 | def xp_boost_active(self) -> bool: 180 | return bool(self.xp_boost_enabled) and not self.boost_expired 181 | 182 | 183 | class Commands(Base): 184 | __tablename__ = "commands" 185 | __table_args__ = (UniqueConstraint("guild_id", "command"),) 186 | id = Column(Integer, primary_key=True) 187 | guild_id = Column(BigInteger) 188 | command = Column(Text) 189 | command_used = Column(Integer, default=0) 190 | 191 | 192 | class TotalCommands(Base): 193 | __tablename__ = "total_commands" 194 | id = Column(Integer, primary_key=True) 195 | guild_id = Column(BigInteger, unique=True) 196 | total_commands_used = Column(Integer, default=0) 197 | 198 | 199 | class AutoResponseMessages(Base): 200 | __tablename__ = "auto_response_messages" 201 | # needs to be list of strings and list of regex strings, channels, guild and response 202 | id = Column(Integer, primary_key=True) 203 | guild_id = Column(BigInteger) 204 | channel_ids = Column(ARRAY(BigInteger)) 205 | regex_strings = Column(ARRAY(Text)) 206 | strings = Column(ARRAY(Text)) 207 | response = Column(Text) 208 | case_sensitive = Column(Boolean, default=False) 209 | enabled = Column(Boolean, default=True) 210 | -------------------------------------------------------------------------------- /cogs/Stats.py: -------------------------------------------------------------------------------- 1 | import io 2 | import disnake 3 | from disnake.ext import commands, tasks 4 | from sqlalchemy import select 5 | from sqlalchemy.dialects.postgresql import insert as pg_insert 6 | from sqlalchemy.ext.asyncio import AsyncSession 7 | 8 | from utils.bot import OGIROID 9 | from utils.db_models import Commands, TotalCommands 10 | 11 | 12 | def get_color_gradient(c1, c2, n): 13 | """ 14 | Given two rgb colors, returns a color gradient 15 | with n colors. 16 | """ 17 | assert n > 1 18 | c1_rgb = [val / 255 for val in c1] 19 | c2_rgb = [val / 255 for val in c2] 20 | mix_pcts = [x / (n - 1) for x in range(n)] 21 | rgb_colors = [ 22 | [(1 - mix) * c1_val + (mix * c2_val) for c1_val, c2_val in zip(c1_rgb, c2_rgb)] 23 | for mix in mix_pcts 24 | ] 25 | return [ 26 | "#" + "".join([format(int(round(val * 255)), "02x") for val in item]) 27 | for item in rgb_colors 28 | ] 29 | 30 | 31 | class Stats(commands.Cog): 32 | def __init__(self, bot: OGIROID): 33 | self.bot = bot 34 | self.update_stats.start() 35 | 36 | def cog_unload(self): 37 | self.update_stats.cancel() 38 | 39 | @tasks.loop(hours=1) 40 | async def update_stats(self): 41 | # add command usage to db 42 | commands_ran = self.bot.commands_ran.copy() 43 | total_commands_ran = self.bot.total_commands_ran.copy() 44 | 45 | for guild_id, guild_commands_ran in commands_ran.items(): 46 | for command, count in guild_commands_ran.items(): 47 | async with self.bot.db.begin() as session: 48 | stmt = pg_insert(Commands).values( 49 | guild_id=guild_id, command=command, command_used=count 50 | ) 51 | stmt = stmt.on_conflict_do_update( 52 | index_elements=["guild_id", "command"], 53 | set_={"command_used": Commands.command_used + count}, 54 | ) 55 | await session.execute(stmt) 56 | 57 | for guild_id, count in total_commands_ran.items(): 58 | async with self.bot.db.begin() as session: 59 | stmt = pg_insert(TotalCommands).values( 60 | guild_id=guild_id, total_commands_used=count 61 | ) 62 | stmt = stmt.on_conflict_do_update( 63 | index_elements=["guild_id"], 64 | set_={ 65 | "total_commands_used": TotalCommands.total_commands_used + count 66 | }, 67 | ) 68 | await session.execute(stmt) 69 | 70 | # reset command usage 71 | self.bot.commands_ran = {} 72 | self.bot.total_commands_ran = {} 73 | 74 | @commands.slash_command(description="Stats about the commands that have been ran") 75 | @commands.cooldown(1, 5, commands.BucketType.user) 76 | async def cmdstats(self, inter): 77 | await inter.response.defer() 78 | 79 | async with self.bot.db.begin() as session: 80 | session: AsyncSession = session 81 | cmdsran = await session.execute( 82 | select(Commands.command, Commands.command_used).filter_by( 83 | guild_id=inter.guild.id 84 | ) 85 | ) 86 | cmdsran = dict(cmdsran.all()) 87 | 88 | stmt = select(TotalCommands.total_commands_used).filter_by( 89 | guild_id=inter.guild.id 90 | ) 91 | total_commands_ran = await session.execute(stmt) 92 | total_commands_ran = total_commands_ran.scalar() 93 | 94 | sortdict = dict(sorted(cmdsran.items(), key=lambda x: x[1], reverse=True)) 95 | value_iterator = iter(sortdict.values()) 96 | key_iterator = iter(sortdict.keys()) 97 | emby = disnake.Embed( 98 | title=f"{self.bot.user.display_name} command Stats", 99 | description=f"{total_commands_ran} Commands ran in total.\nUpdated hourly.", 100 | color=self.bot.config.colors.white, 101 | ) 102 | if len(cmdsran) < 2: 103 | return await inter.send( 104 | embed=disnake.Embed( 105 | title=f"{self.bot.user.display_name} command Stats", 106 | description=f"{total_commands_ran} Commands ran in total.\n", 107 | color=self.bot.config.colors.white, 108 | ) 109 | ) 110 | 111 | text = ( 112 | f"🥇: /{next(key_iterator)} ({next(value_iterator)} uses)\n" 113 | + f"🥈: /{next(key_iterator)} ({next(value_iterator)} uses)\n" 114 | + f"🥉: /{next(key_iterator)} ({next(value_iterator)} uses)\n" 115 | ) 116 | i = 2 117 | for key in key_iterator: 118 | text += f"🏅: /{key} ({next(value_iterator)} uses)\n" 119 | i += 1 120 | # total 10 121 | if i == 10: 122 | break 123 | 124 | emby.add_field(name="Top 10 commands ran", value=text) 125 | # add bots avatar 126 | emby.set_footer( 127 | text=self.bot.user.display_name, icon_url=self.bot.user.avatar.url 128 | ) 129 | emby.timestamp = disnake.utils.utcnow() 130 | 131 | colors = get_color_gradient((0, 0, 0), (225, 225, 225), 10) 132 | 133 | # they are imported here to save memory 134 | from matplotlib.axes import Axes 135 | from matplotlib.figure import Figure 136 | from matplotlib.ticker import MaxNLocator 137 | 138 | fig = Figure(figsize=(5, 5), dpi=180) 139 | ax: Axes = fig.subplots() 140 | x = list(sortdict.keys())[:10] 141 | y = list(sortdict.values())[:10] 142 | ax.bar(x, y, color=colors) 143 | ax.set_xlabel("Command") 144 | ax.set_ylabel("Times used") 145 | ax.yaxis.set_major_locator(MaxNLocator(integer=True)) 146 | ax.set_xticks(range(len(x))) # Set the x-ticks to the number of bars 147 | ax.set_xticklabels( 148 | x, rotation=45, ha="right" 149 | ) # Set the x-tick labels and rotate them 150 | ax.set_axisbelow(True) 151 | ax.grid(axis="y", linestyle="-") 152 | ax.set_axisbelow(True) 153 | fig.tight_layout() 154 | 155 | with io.BytesIO() as buf: 156 | fig.savefig(buf, format="png") 157 | buf.seek(0) 158 | emby.set_image(url="attachment://plot.png") 159 | 160 | await inter.send( 161 | embed=emby, 162 | file=disnake.File( 163 | buf, 164 | filename="plot.png", 165 | ), 166 | ) 167 | 168 | 169 | def setup(bot): 170 | bot.add_cog(Stats(bot)) 171 | -------------------------------------------------------------------------------- /cogs/Support.py: -------------------------------------------------------------------------------- 1 | import disnake 2 | from disnake import TextInputStyle, Option, ApplicationCommandInteraction 3 | from disnake.ext import commands 4 | 5 | from utils.bot import OGIROID 6 | 7 | 8 | class BugModal(disnake.ui.Modal): 9 | def __init__(self, bot: OGIROID, bug_report_type: str): 10 | # The details of the modal, and its components 11 | self.bot = bot 12 | self.bug_report_type = bug_report_type 13 | components = [ 14 | # disnake.ui.Select( 15 | # placeholder="Bug Report for:", 16 | # options=["Reddit-Bot", "Ogiroid"], 17 | # custom_id="type", 18 | # ), 19 | disnake.ui.TextInput( 20 | label="Bug Title", 21 | placeholder="Title", 22 | custom_id="title", 23 | style=TextInputStyle.short, 24 | max_length=50, 25 | ), 26 | disnake.ui.TextInput( 27 | label="Expected Result", 28 | placeholder="Your expected result", 29 | custom_id="expected", 30 | style=TextInputStyle.paragraph, 31 | max_length=500, 32 | ), 33 | disnake.ui.TextInput( 34 | label="Actual Result", 35 | placeholder="Your actual result", 36 | custom_id="actual", 37 | style=TextInputStyle.paragraph, 38 | max_length=500, 39 | ), 40 | disnake.ui.TextInput( 41 | label="Further Explanation", 42 | placeholder="", 43 | custom_id="description", 44 | style=TextInputStyle.paragraph, 45 | required=False, 46 | ), 47 | ] 48 | super().__init__( 49 | title="Report Bug", 50 | custom_id="bug", 51 | components=components, 52 | ) 53 | 54 | # The callback received when the user input is completed. 55 | async def callback(self, inter: disnake.ModalInteraction): 56 | bug_report_type = ( 57 | self.bug_report_type 58 | ) # inter.data["components"][0]["components"][0]["values"][0] 59 | embed = disnake.Embed(title="Bug Report") 60 | embed.add_field(name="From:", value=inter.author) 61 | 62 | embed.add_field(name="Type:", value=bug_report_type) 63 | 64 | embed.add_field( 65 | name="Bug Title:", value=inter.text_values["title"], inline=False 66 | ) 67 | 68 | embed.add_field( 69 | name="Expected Result: ", 70 | value=inter.text_values["expected"], 71 | inline=False, 72 | ) 73 | 74 | embed.add_field( 75 | name="Actual Result:", 76 | value=inter.text_values["actual"], 77 | inline=False, 78 | ) 79 | 80 | embed.add_field( 81 | name="Further Explanation:", 82 | value=inter.text_values["description"] 83 | if inter.text_values["description"] 84 | else "No explanation provided", 85 | inline=False, 86 | ) 87 | 88 | if bug_report_type == "Reddit-Bot": 89 | channel = self.bot.get_channel( 90 | self.bot.config.channels.bug_report_reddit_bot 91 | ) 92 | else: 93 | channel = self.bot.get_channel(self.bot.config.channels.bug_report_ogiroid) 94 | 95 | await channel.send(embed=embed) 96 | await inter.send( 97 | "Sent bug report.\nThank you for pointing it out.", ephemeral=True 98 | ) 99 | 100 | 101 | class SuggestionModal(disnake.ui.Modal): 102 | def __init__(self, bot: OGIROID, suggestion_type: str): 103 | # The details of the modal, and its components 104 | self.bot = bot 105 | self.suggestion_type = suggestion_type 106 | components = [ 107 | # disnake.ui.Select( 108 | # placeholder="Suggestion for:", 109 | # options=["Reddit-Bot", "Ogiroid"], 110 | # custom_id="type", 111 | # ), 112 | disnake.ui.TextInput( 113 | label="Suggestion Title", 114 | placeholder="Title", 115 | custom_id="title", 116 | style=TextInputStyle.short, 117 | max_length=50, 118 | ), 119 | disnake.ui.TextInput( 120 | label="Description", 121 | placeholder="Describe your suggestion here", 122 | custom_id="description", 123 | style=TextInputStyle.paragraph, 124 | ), 125 | ] 126 | super().__init__( 127 | title="Suggestion", 128 | custom_id="suggest", 129 | components=components, 130 | ) 131 | 132 | # The callback received when the user input is completed. 133 | async def callback(self, inter: disnake.ModalInteraction): 134 | suggestion_type = ( 135 | self.suggestion_type 136 | ) # inter.data["components"][0]["components"][0]["values"][0] 137 | embed = disnake.Embed(title="Suggestion") 138 | embed.add_field(name="From:", value=inter.author) 139 | 140 | embed.add_field(name="Type:", value=suggestion_type) 141 | 142 | embed.add_field(name="Title:", value=inter.text_values["title"], inline=False) 143 | 144 | embed.add_field( 145 | name="Description:", 146 | value=inter.text_values["description"], 147 | inline=False, 148 | ) 149 | 150 | if suggestion_type == "Reddit-Bot": 151 | channel = self.bot.get_channel( 152 | self.bot.config.channels.suggestion_reddit_bot 153 | ) 154 | else: 155 | channel = self.bot.get_channel(self.bot.config.channels.suggestion_ogiroid) 156 | await channel.send(embed=embed) 157 | await inter.send( 158 | "Sent suggestion.\nThank you for your suggestion.", ephemeral=True 159 | ) 160 | 161 | 162 | class BotSupport(commands.Cog, name="Bot Support"): 163 | """Bot Support used for reporting bugs and suggesting features""" 164 | 165 | def __init__(self, bot: OGIROID): 166 | self.bot = bot 167 | 168 | @commands.slash_command( 169 | name="reportbug", 170 | description="Report a bug", 171 | options=[ 172 | Option( 173 | name="for", 174 | required=True, 175 | description="Select for what this bug report is.", 176 | choices=["Reddit-Bot", "Ogiroid"], 177 | ) 178 | ], 179 | connectors={"for": "bug_report_for"}, 180 | ) 181 | async def bug(self, inter: ApplicationCommandInteraction, bug_report_for: str): 182 | await inter.response.send_modal(modal=BugModal(self.bot, bug_report_for)) 183 | 184 | @commands.slash_command( 185 | name="suggest", 186 | description="Suggest something for the bot", 187 | options=[ 188 | Option( 189 | name="for", 190 | required=True, 191 | description="Select for what this suggestion is.", 192 | choices=["Reddit-Bot", "Ogiroid"], 193 | ) 194 | ], 195 | connectors={"for": "suggestion_for"}, 196 | ) 197 | async def suggestion(self, inter, suggestion_for: str): 198 | await inter.response.send_modal(modal=SuggestionModal(self.bot, suggestion_for)) 199 | 200 | 201 | def setup(bot): 202 | bot.add_cog(BotSupport(bot)) 203 | -------------------------------------------------------------------------------- /alembic/versions/b6b145d0b35d_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: b6b145d0b35d 4 | Revises: 5 | Create Date: 2025-04-25 19:08:07.834949 6 | 7 | """ 8 | from typing import Sequence, Union 9 | 10 | from alembic import op 11 | import sqlalchemy as sa 12 | 13 | 14 | # revision identifiers, used by Alembic. 15 | revision: str = 'b6b145d0b35d' 16 | down_revision: Union[str, None] = None 17 | branch_labels: Union[str, Sequence[str], None] = None 18 | depends_on: Union[str, Sequence[str], None] = None 19 | 20 | 21 | def upgrade() -> None: 22 | # ### commands auto generated by Alembic - please adjust! ### 23 | op.create_table('auto_response_messages', 24 | sa.Column('id', sa.Integer(), nullable=False), 25 | sa.Column('guild_id', sa.BigInteger(), nullable=True), 26 | sa.Column('channel_ids', sa.ARRAY(sa.BigInteger()), nullable=True), 27 | sa.Column('regex_strings', sa.ARRAY(sa.Text()), nullable=True), 28 | sa.Column('strings', sa.ARRAY(sa.Text()), nullable=True), 29 | sa.Column('response', sa.Text(), nullable=True), 30 | sa.Column('case_sensitive', sa.Boolean(), nullable=True), 31 | sa.Column('enabled', sa.Boolean(), nullable=True), 32 | sa.PrimaryKeyConstraint('id') 33 | ) 34 | op.create_table('birthday', 35 | sa.Column('id', sa.Integer(), nullable=False), 36 | sa.Column('user_id', sa.BigInteger(), nullable=True), 37 | sa.Column('birthday', sa.Text(), nullable=True), 38 | sa.Column('birthday_last_changed', sa.BigInteger(), nullable=True), 39 | sa.PrimaryKeyConstraint('id') 40 | ) 41 | op.create_table('blacklist', 42 | sa.Column('id', sa.Integer(), nullable=False), 43 | sa.Column('user_id', sa.BigInteger(), nullable=True), 44 | sa.Column('reason', sa.Text(), nullable=True), 45 | sa.Column('bot', sa.Boolean(), nullable=True), 46 | sa.Column('tickets', sa.Boolean(), nullable=True), 47 | sa.Column('tags', sa.Boolean(), nullable=True), 48 | sa.Column('expires', sa.BigInteger(), nullable=True), 49 | sa.PrimaryKeyConstraint('id') 50 | ) 51 | op.create_table('commands', 52 | sa.Column('id', sa.Integer(), nullable=False), 53 | sa.Column('guild_id', sa.BigInteger(), nullable=True), 54 | sa.Column('command', sa.Text(), nullable=True), 55 | sa.Column('command_used', sa.Integer(), nullable=True), 56 | sa.PrimaryKeyConstraint('id'), 57 | sa.UniqueConstraint('guild_id', 'command') 58 | ) 59 | op.create_table('config', 60 | sa.Column('guild_id', sa.BigInteger(), nullable=False), 61 | sa.Column('xp_boost', sa.Integer(), nullable=True), 62 | sa.Column('xp_boost_expiry', sa.BigInteger(), nullable=True), 63 | sa.Column('xp_boost_enabled', sa.Boolean(), nullable=True), 64 | sa.Column('custom_roles_threshold', sa.Integer(), nullable=True), 65 | sa.Column('min_required_lvl', sa.Integer(), nullable=True), 66 | sa.Column('position_role_id', sa.BigInteger(), nullable=True), 67 | sa.PrimaryKeyConstraint('guild_id') 68 | ) 69 | op.create_table('custom_roles', 70 | sa.Column('id', sa.Integer(), nullable=False), 71 | sa.Column('guild_id', sa.BigInteger(), nullable=True), 72 | sa.Column('role_id', sa.BigInteger(), nullable=True), 73 | sa.Column('user_id', sa.BigInteger(), nullable=True), 74 | sa.PrimaryKeyConstraint('id') 75 | ) 76 | op.create_table('flag_quiz', 77 | sa.Column('id', sa.BigInteger(), nullable=False), 78 | sa.Column('user_id', sa.BigInteger(), nullable=True), 79 | sa.Column('tries', sa.Integer(), nullable=True), 80 | sa.Column('correct', sa.Integer(), nullable=True), 81 | sa.Column('completed', sa.Integer(), nullable=True), 82 | sa.Column('guild_id', sa.BigInteger(), nullable=True), 83 | sa.PrimaryKeyConstraint('id') 84 | ) 85 | op.create_table('levels', 86 | sa.Column('id', sa.Integer(), nullable=False), 87 | sa.Column('guild_id', sa.BigInteger(), nullable=True), 88 | sa.Column('user_id', sa.BigInteger(), nullable=True), 89 | sa.Column('total_xp', sa.Integer(), nullable=True), 90 | sa.PrimaryKeyConstraint('id') 91 | ) 92 | op.create_table('reaction_roles', 93 | sa.Column('id', sa.Integer(), nullable=False), 94 | sa.Column('message_id', sa.BigInteger(), nullable=True), 95 | sa.Column('role_id', sa.BigInteger(), nullable=True), 96 | sa.Column('emoji', sa.Text(), nullable=True), 97 | sa.Column('roles_given', sa.Integer(), nullable=True), 98 | sa.PrimaryKeyConstraint('id') 99 | ) 100 | op.create_table('role_rewards', 101 | sa.Column('id', sa.Integer(), nullable=False), 102 | sa.Column('guild_id', sa.BigInteger(), nullable=True), 103 | sa.Column('role_id', sa.BigInteger(), nullable=True), 104 | sa.Column('required_lvl', sa.Integer(), nullable=True), 105 | sa.PrimaryKeyConstraint('id') 106 | ) 107 | op.create_table('tag_relations', 108 | sa.Column('id', sa.Integer(), nullable=False), 109 | sa.Column('name', sa.Text(), nullable=True), 110 | sa.Column('alias', sa.Text(), nullable=True), 111 | sa.PrimaryKeyConstraint('id') 112 | ) 113 | op.create_table('tags', 114 | sa.Column('name', sa.Text(), nullable=False), 115 | sa.Column('content', sa.Text(), nullable=True), 116 | sa.Column('owner', sa.BigInteger(), nullable=True), 117 | sa.Column('created_at', sa.BigInteger(), nullable=True), 118 | sa.Column('views', sa.Integer(), nullable=True), 119 | sa.PrimaryKeyConstraint('name') 120 | ) 121 | op.create_table('timezone', 122 | sa.Column('id', sa.Integer(), nullable=False), 123 | sa.Column('user_id', sa.BigInteger(), nullable=True), 124 | sa.Column('timezone', sa.Text(), nullable=True), 125 | sa.Column('timezone_last_changed', sa.BigInteger(), nullable=True), 126 | sa.PrimaryKeyConstraint('id') 127 | ) 128 | op.create_table('total_commands', 129 | sa.Column('id', sa.Integer(), nullable=False), 130 | sa.Column('guild_id', sa.BigInteger(), nullable=True), 131 | sa.Column('total_commands_used', sa.Integer(), nullable=True), 132 | sa.PrimaryKeyConstraint('id'), 133 | sa.UniqueConstraint('guild_id') 134 | ) 135 | op.create_table('trivia', 136 | sa.Column('id', sa.BigInteger(), nullable=False), 137 | sa.Column('user_id', sa.BigInteger(), nullable=True), 138 | sa.Column('correct', sa.Integer(), nullable=True), 139 | sa.Column('incorrect', sa.Integer(), nullable=True), 140 | sa.Column('streak', sa.Integer(), nullable=True), 141 | sa.Column('longest_streak', sa.Integer(), nullable=True), 142 | sa.PrimaryKeyConstraint('id') 143 | ) 144 | op.create_table('warnings', 145 | sa.Column('warning_id', sa.Integer(), nullable=False), 146 | sa.Column('user_id', sa.BigInteger(), nullable=True), 147 | sa.Column('moderator_id', sa.BigInteger(), nullable=True), 148 | sa.Column('reason', sa.Text(), nullable=True), 149 | sa.Column('guild_id', sa.BigInteger(), nullable=True), 150 | sa.PrimaryKeyConstraint('warning_id') 151 | ) 152 | # ### end Alembic commands ### 153 | 154 | 155 | def downgrade() -> None: 156 | # ### commands auto generated by Alembic - please adjust! ### 157 | op.drop_table('warnings') 158 | op.drop_table('trivia') 159 | op.drop_table('total_commands') 160 | op.drop_table('timezone') 161 | op.drop_table('tags') 162 | op.drop_table('tag_relations') 163 | op.drop_table('role_rewards') 164 | op.drop_table('reaction_roles') 165 | op.drop_table('levels') 166 | op.drop_table('flag_quiz') 167 | op.drop_table('custom_roles') 168 | op.drop_table('config') 169 | op.drop_table('commands') 170 | op.drop_table('blacklist') 171 | op.drop_table('birthday') 172 | op.drop_table('auto_response_messages') 173 | # ### end Alembic commands ### 174 | -------------------------------------------------------------------------------- /cogs/Error_handler.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import datetime as dt 4 | import traceback 5 | from datetime import datetime 6 | 7 | import disnake 8 | from disnake import Embed, ApplicationCommandInteraction, HTTPException 9 | from disnake.ext.commands import * 10 | 11 | from utils.CONSTANTS import IGNORE_EXCEPTIONS 12 | from utils.bot import OGIROID 13 | from utils.shortcuts import errorEmb, permsEmb 14 | 15 | 16 | class ErrorHandler(Cog): 17 | def __init__(self, bot: OGIROID): 18 | self.bot = bot 19 | self.debug_mode = self.bot.config.debug 20 | self.waitTime = 25 21 | 22 | def TimeSinceStart(self) -> float: 23 | return round((datetime.now() - self.bot.uptime).total_seconds(), ndigits=1) 24 | 25 | # noinspection PyUnboundLocalVariable 26 | @Cog.listener() 27 | async def on_slash_command_error(self, inter: ApplicationCommandInteraction, error): 28 | try: 29 | if hasattr(inter.application_command, "on_error"): 30 | return 31 | elif error.__class__.__name__ in IGNORE_EXCEPTIONS: 32 | return 33 | 34 | # Databases and internal caches might not be fully loaded yet. 35 | # In debug mode we don't want to wait for them cause its fucking annoying. 36 | if self.TimeSinceStart() < self.waitTime and not self.debug_mode: 37 | return await errorEmb( 38 | inter, 39 | f"The bot just started, please wait {round(self.waitTime - self.TimeSinceStart(), ndigits=2)}s.", 40 | ) 41 | 42 | # non real error handling 43 | if isinstance(error, CommandNotFound): 44 | return await errorEmb( 45 | inter, 46 | "Command not found! use /help for a list of commands", 47 | ) 48 | elif isinstance(error, NotOwner): 49 | await errorEmb( 50 | inter, 51 | f"You must be the owner of {inter.me.display_name} to use `{inter.application_command.name}`", 52 | ) 53 | elif isinstance(error, HTTPException): 54 | await errorEmb(inter, error.text) 55 | return await self.send_traceback(inter, error) 56 | elif isinstance(error, MissingPermissions): 57 | return await permsEmb( 58 | inter, 59 | permissions=f"{', '.join(error.missing_permissions)}", 60 | ) 61 | elif isinstance(error, MissingRole): 62 | return await permsEmb(inter, permissions=f"Role: {error.missing_role}") 63 | elif isinstance(error, MaxConcurrencyReached): 64 | return await errorEmb( 65 | inter, 66 | "You've reached max capacity of command usage at once, please finish the previous one...", 67 | ) 68 | elif isinstance(error, CommandOnCooldown): 69 | return await errorEmb( 70 | inter, 71 | f"This command is on cooldown... try again in {error.retry_after:.2f} seconds.", 72 | ) 73 | elif isinstance(error, GuildNotFound): 74 | return await errorEmb( 75 | error, f"You can only use this command in a server" 76 | ) 77 | elif isinstance(error, CheckFailure): 78 | if self.bot.uptime - dt.timedelta(seconds=10) < datetime.now(): 79 | return await errorEmb( 80 | inter, 81 | "wait a few seconds before using this command again", 82 | ) 83 | return await errorEmb( 84 | inter, "You don't have permission to use this command" 85 | ) 86 | elif self.debug_mode: 87 | traceback_nice = "".join( 88 | traceback.format_exception( 89 | type(error), error, error.__traceback__, 16 90 | ) 91 | ) 92 | print(traceback_nice) 93 | traceback.print_exc() 94 | return await errorEmb(inter, "check console for error") 95 | else: # actual error not just a check failure 96 | embed = await self.create_error_message(inter, error) 97 | await inter.send(embed=embed, ephemeral=True) 98 | await self.send_traceback(inter, error) 99 | 100 | except Exception as e: 101 | error_channel = self.bot.get_channel(self.bot.config.channels.errors) 102 | embed = await self.create_error_message(inter, e) 103 | await inter.send(embed=embed, ephemeral=True) 104 | e_traceback = traceback.format_exception(type(e), e, e.__traceback__) 105 | if self.debug_mode: 106 | print(e_traceback) 107 | e_embed = disnake.Embed( 108 | title="Error Traceback", 109 | description=f"See below!\n\n{e_traceback[:1024]}", 110 | timestamp=datetime.now(), 111 | ) 112 | 113 | await error_channel.send(embed=e_embed) 114 | 115 | # Debug Info 116 | traceback_nice_e = "".join( 117 | traceback.format_exception(type(e), e, e.__traceback__, 4) 118 | ).replace("```", "") 119 | 120 | debug_info_e = ( 121 | f"```\n{inter.author} {inter.author.id}: /{inter.application_command.name}"[ 122 | :200 123 | ] 124 | + "```" 125 | + f"```py\n{traceback_nice_e}"[: 2000 - 206] 126 | + "```" 127 | ) 128 | await error_channel.send(debug_info_e) 129 | 130 | async def send_traceback(self, inter, error): 131 | error_channel = self.bot.get_channel(self.bot.config.channels.errors) 132 | bot_errors = traceback.format_exception(type(error), error, error.__traceback__) 133 | 134 | error_embed = disnake.Embed( 135 | title="Error Traceback", 136 | description=f"See below!\n\n{bot_errors[:1024]}", 137 | timestamp=datetime.now(), 138 | ) 139 | await error_channel.send(embed=error_embed) 140 | traceback_nice = "".join( 141 | traceback.format_exception(type(error), error, error.__traceback__, 4) 142 | ).replace("```", "```") 143 | 144 | options = " ".join( 145 | [f"{name}: {value}" for name, value in inter.options.items()] 146 | ) 147 | 148 | debug_info = ( 149 | f"```\n{inter.author} {inter.author.id}: /{inter.application_command.name}{' ' + options if options != '' else options}"[ 150 | :200 151 | ] 152 | + "```" 153 | + f"```py\n{traceback_nice}"[: 2000 - 206] 154 | + "```" 155 | ) 156 | await error_channel.send(debug_info) 157 | 158 | @staticmethod 159 | async def create_error_message(inter, error) -> Embed: 160 | embed = disnake.Embed( 161 | title=f"❌An error occurred while executing: ``/{inter.application_command.qualified_name}``", 162 | description=f"{error}", 163 | colour=disnake.Color.blurple(), 164 | timestamp=datetime.now(), 165 | ) 166 | embed.add_field( 167 | name="Something not right?", 168 | value="\nUse the ``/reportbug`` command to report a bug.", 169 | ) 170 | embed.set_footer( 171 | text=f"Executed by {inter.author}", 172 | icon_url="https://cdn.discordapp.com/attachments/985729550732394536/987287532146393109/discord-avatar-512-NACNJ.png", 173 | ) 174 | return embed 175 | 176 | 177 | def setup(bot): 178 | bot.add_cog(ErrorHandler(bot)) 179 | -------------------------------------------------------------------------------- /cogs/Image.py: -------------------------------------------------------------------------------- 1 | import io 2 | import os 3 | import random 4 | import textwrap 5 | from io import BytesIO 6 | 7 | import disnake 8 | from PIL import Image, ImageDraw, ImageFont 9 | from disnake import ApplicationCommandInteraction 10 | from disnake.ext import commands 11 | 12 | from utils.bot import OGIROID 13 | 14 | 15 | class ImageCommands(commands.Cog, name="Image"): 16 | """Image Commands!""" 17 | 18 | def __init__(self, bot: OGIROID): 19 | self.bot = bot 20 | 21 | @commands.slash_command( 22 | name="trigger", 23 | brief="Trigger", 24 | description="For when you're feeling triggered.", 25 | ) 26 | @commands.cooldown(1, 10, commands.BucketType.user) 27 | async def triggered( 28 | self, 29 | inter: ApplicationCommandInteraction, 30 | member: disnake.Member = None, 31 | ): 32 | """Time to get triggered.""" 33 | if not member: 34 | member = inter.author 35 | trigImg = await self.bot.session.get( 36 | f"https://some-random-api.com/canvas/triggered?avatar={member.display_avatar.url}" 37 | ) 38 | imageData = io.BytesIO(await trigImg.read()) 39 | await inter.send(file=disnake.File(imageData, "triggered.gif")) 40 | 41 | @commands.slash_command( 42 | name="sus", 43 | brief="Sus-Inator4200", 44 | description="Check if your friend is kinda ***SUS***", 45 | ) 46 | @commands.cooldown(1, 10, commands.BucketType.user) 47 | async def amongus( 48 | self, 49 | inter: ApplicationCommandInteraction, 50 | member: disnake.Member = None, 51 | ): 52 | """Check if your friends are sus or not""" 53 | await inter.send("Testing for sus-ness...") 54 | if not member: 55 | member = inter.author 56 | impostor = random.choice(["true", "false"]) 57 | apikey = os.getenv("SRA_API_KEY") 58 | uri = f"https://some-random-api.com/premium/amongus?username={member.name}&avatar={member.display_avatar.url}&impostor={impostor}&key={apikey}" 59 | resp = await self.bot.session.get(uri) 60 | if 300 > resp.status >= 200: 61 | fp = io.BytesIO(await resp.read()) 62 | await inter.send(file=disnake.File(fp, "amogus.gif")) 63 | else: 64 | await inter.send("Couldnt get image :(") 65 | 66 | @commands.slash_command( 67 | name="invert", 68 | brief="invert", 69 | description="Invert the colours of your icon", 70 | ) 71 | @commands.cooldown(1, 5, commands.BucketType.user) 72 | async def invert( 73 | self, 74 | inter: ApplicationCommandInteraction, 75 | member: disnake.Member = None, 76 | ): 77 | """Invert your profile picture.""" 78 | if not member: 79 | member = inter.author 80 | trigImg = await self.bot.session.get( 81 | f"https://some-random-api.com/canvas/invert/?avatar={member.display_avatar.url}" 82 | ) 83 | imageData = io.BytesIO(await trigImg.read()) 84 | await inter.send(file=disnake.File(imageData, "invert.png")) 85 | 86 | @commands.slash_command( 87 | name="pixelate", 88 | brief="pixelate", 89 | description="Turn yourself into 144p!", 90 | ) 91 | @commands.cooldown(1, 5, commands.BucketType.user) 92 | async def pixelate( 93 | self, 94 | inter: ApplicationCommandInteraction, 95 | member: disnake.Member = None, 96 | ): 97 | """Turn yourself into pixels""" 98 | if not member: 99 | member = inter.author 100 | trigImg = await self.bot.session.get( 101 | f"https://some-random-api.com/canvas/pixelate/?avatar={member.display_avatar.url}" 102 | ) 103 | imageData = io.BytesIO(await trigImg.read()) 104 | await inter.send(file=disnake.File(imageData, "pixelate.png")) 105 | 106 | @commands.slash_command( 107 | name="jail", brief="jail", description="Go to jail!" 108 | ) 109 | @commands.cooldown(1, 5, commands.BucketType.user) 110 | async def jail( 111 | self, 112 | inter: ApplicationCommandInteraction, 113 | member: disnake.Member = None, 114 | ): 115 | """Go to horny jail""" 116 | if not member: 117 | member = inter.author 118 | 119 | trigImg = await self.bot.session.get( 120 | f"https://some-random-api.com/canvas/jail?avatar={member.display_avatar.url}" 121 | ) 122 | imageData = io.BytesIO(await trigImg.read()) 123 | await inter.send(file=disnake.File(imageData, "jail.png")) 124 | 125 | @commands.slash_command( 126 | name="urltoqr", description="Converts a URL to a QR code." 127 | ) 128 | async def urltoqr( 129 | self, inter: ApplicationCommandInteraction, url: str, size: int 130 | ): 131 | url = url.replace("http://", "").replace("https://", "") 132 | qr = f"https://api.qrserver.com/v1/create-qr-code/?size={size}x{size}&data={url}" 133 | embed = disnake.Embed(title=f"URL created for: {url}", color=0xFFFFFF) 134 | embed.set_image(url=qr) 135 | embed.set_footer(text=f"Requested by: {inter.author.name}") 136 | return await inter.send(embed=embed) 137 | 138 | @staticmethod 139 | def draw_multiple_line_text( 140 | image, text, font, text_color, text_start_height 141 | ): 142 | draw = ImageDraw.Draw(image) 143 | image_width, image_height = image.size 144 | y_text = text_start_height 145 | lines = textwrap.wrap(text, width=45) 146 | for line in lines: 147 | nothing1, nothing2, line_width, line_height = font.getbbox(line) 148 | # draw shadow on text 149 | draw.text( 150 | ((image_width - line_width) / 2 + 2, y_text + 2), 151 | line, 152 | font=font, 153 | fill=(0, 0, 0), 154 | ) 155 | draw.text( 156 | ((image_width - line_width) / 2, y_text), 157 | line, 158 | font=font, 159 | fill=text_color, 160 | ) 161 | y_text += line_height 162 | # Return the bottom pixel of the text 163 | return y_text 164 | 165 | # Command to get information about a Quote user 166 | @commands.slash_command( 167 | name="quote", 168 | description="Generates an image with a quote and random background", 169 | ) 170 | async def quote(self, inter): 171 | """Generates an image with a quote and random background""" 172 | await inter.response.defer() 173 | # Use api.quotable.io/random to get a random quote 174 | main = await self.bot.session.get("https://api.quotable.io/random") 175 | data = await main.json() 176 | quote = data["content"] 177 | author = data["author"] 178 | 179 | # Use unsplash.com to get a random image 180 | resolution = "1080x1080" 181 | response = await self.bot.session.get( 182 | f"https://source.unsplash.com/random/{resolution}" 183 | ) 184 | image_bytes = io.BytesIO(await response.read()) 185 | image = Image.open(image_bytes) 186 | 187 | draw = ImageDraw.Draw(image) 188 | font = ImageFont.truetype("utils/data/Roboto-Italic.ttf", 50) 189 | font2 = ImageFont.truetype("utils/data/Roboto-Bold.ttf", 50) 190 | if len(quote) > 350: 191 | text_start_height = ( 192 | image.height - font.getbbox(quote)[3] 193 | ) / 2 - 500 194 | elif len(quote) > 250: 195 | text_start_height = ( 196 | image.height - font.getbbox(quote)[3] 197 | ) / 2 - 200 198 | elif len(quote) > 150: 199 | text_start_height = ( 200 | image.height - font.getbbox(quote)[3] 201 | ) / 2 - 50 202 | else: 203 | text_start_height = (image.height - font.getbbox(quote)[3]) / 2 204 | end = self.draw_multiple_line_text( 205 | image, 206 | quote, 207 | font, 208 | text_color=(255, 255, 255), 209 | text_start_height=text_start_height, 210 | ) 211 | # Draw the author shadow 212 | draw.text( 213 | ((image.width - font2.getbbox(author)[2]) / 2 + 2, end + 50), 214 | author, 215 | font=font2, 216 | fill=(0, 0, 0), 217 | ) 218 | # Draw the author 219 | draw.text( 220 | ((image.width - font2.getbbox(author)[2]) / 2, end + 50), 221 | author, 222 | font=font2, 223 | fill=(255, 255, 255), 224 | ) 225 | with BytesIO() as image_binary: 226 | image.save(image_binary, "PNG") 227 | image_binary.seek(0) 228 | await inter.send( 229 | file=disnake.File(fp=image_binary, filename="image.png") 230 | ) 231 | 232 | 233 | def setup(bot): 234 | bot.add_cog(ImageCommands(bot)) 235 | -------------------------------------------------------------------------------- /cogs/Tickets.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from datetime import datetime 3 | 4 | import disnake 5 | from disnake.ext import commands 6 | 7 | from utils.CONSTANTS import TICKET_PERMS 8 | from utils.bot import OGIROID 9 | from utils.shortcuts import errorEmb, sucEmb 10 | 11 | 12 | class Tickets(commands.Cog): 13 | """🎫 Ticketing system commands (Staff)""" 14 | 15 | def __init__(self, bot: OGIROID): 16 | self.message = None 17 | self.bot = bot 18 | self.ticket_channel = self.bot.config.channels.tickets 19 | 20 | @commands.Cog.listener() 21 | async def on_ready(self): 22 | if not self.bot.ready_: 23 | ticket_channel = self.bot.get_channel(self.ticket_channel) 24 | 25 | exists = False 26 | async for channel_message in ticket_channel.history(limit=100): 27 | if channel_message.author.id == self.bot.application_id: 28 | self.message = channel_message 29 | exists = True 30 | break 31 | 32 | if not exists: 33 | await self.send_message() 34 | 35 | async def send_message(self): 36 | ticket_channel = self.bot.get_channel(self.ticket_channel) 37 | await ticket_channel.send( 38 | "Create a Ticket.", 39 | components=disnake.ui.Button( 40 | emoji=disnake.PartialEmoji.from_str("📩"), 41 | label="Create a Ticket", 42 | custom_id="ticket_button", 43 | ), 44 | ) 45 | 46 | @commands.slash_command( 47 | name="edit-ticket-message", description="Update the ticket message." 48 | ) 49 | @commands.guild_only() 50 | @commands.has_permissions(manage_roles=True) 51 | async def edit_ticket_message(self, inter): 52 | await inter.send("Please send the new message", ephemeral=True) 53 | 54 | def check(m): 55 | return m.author == inter.author and m.channel == inter.channel 56 | 57 | try: 58 | msg = await self.bot.wait_for("message", check=check, timeout=300.0) 59 | except asyncio.exceptions.TimeoutError: 60 | return await errorEmb( 61 | inter, "Due to no response the operation was canceled" 62 | ) 63 | 64 | await msg.delete() 65 | 66 | text = msg.content 67 | 68 | try: 69 | await self.message.edit(content=text) 70 | except disnake.errors.Forbidden or disnake.errors.HTTPException: 71 | return await errorEmb( 72 | inter, "I do not have permission to edit this message." 73 | ) 74 | 75 | await sucEmb(inter, "Edited!") 76 | 77 | @commands.Cog.listener("on_button_click") 78 | async def on_button_click(self, inter): 79 | ticket_channel = self.bot.get_channel(self.ticket_channel) 80 | category = ticket_channel.category 81 | guild = self.bot.get_guild(inter.guild_id) 82 | user = guild.get_member(inter.author.id) 83 | if user.id == self.bot.application_id: 84 | return print("Added reaction from bot") 85 | if not inter.component.custom_id == "ticket_button": 86 | return 87 | 88 | staff = guild.get_role(self.bot.config.roles.staff) 89 | 90 | # checks if user has a ticket already open 91 | for channel in guild.channels: 92 | try: 93 | if int(channel.name.strip().replace("ticket-", "")) == int(user.id): 94 | await errorEmb( 95 | inter, 96 | "You already have a ticket open. Please close it before opening a new one", 97 | ) 98 | return 99 | except ValueError: 100 | pass 101 | 102 | ticket = await category.create_text_channel(f"ticket-{user.id}") 103 | await ticket.edit(topic=f"Ticket opened by {user.name}.") 104 | await ticket.set_permissions( 105 | inter.guild.get_role(inter.guild.id), read_messages=False 106 | ) 107 | await ticket.set_permissions(user, **TICKET_PERMS) 108 | await ticket.set_permissions(staff, **TICKET_PERMS) 109 | message_content = "Thank you for contacting support! A staff member will be here shortly!\nTo close the the ticket use ``/close``" 110 | em = disnake.Embed( 111 | title=f"Ticket made by {user.name}", 112 | description=f"{message_content}", 113 | color=0x26FF00, 114 | ) 115 | em.set_footer(text=f"{user}") 116 | em.timestamp = datetime.now() 117 | await ticket.send(embed=em) 118 | await inter.send( 119 | f"Created Ticket. Your ticket: {ticket.mention}", ephemeral=True 120 | ) 121 | 122 | @commands.slash_command(description="Close ticket") 123 | async def close(self, inter): 124 | await inter.response.defer() 125 | if self.check_if_ticket_channel(inter): 126 | # send log of chat in ticket to log channel 127 | ticket_log_channel = self.bot.get_channel( 128 | self.bot.config.channels.ticket_logs 129 | ) 130 | log_emb = disnake.Embed( 131 | title=f"Ticket closed by {inter.author.name}", 132 | description=f"Ticket closed by {inter.author.mention}", 133 | color=self.bot.config.colors.white, 134 | ) 135 | # get all users in ticket channel 136 | user_text = "" 137 | for user in inter.channel.members: 138 | user_text += f"{user.mention} " 139 | 140 | log_emb.add_field(name="Users in Channel", value=user_text, inline=False) 141 | 142 | # get all messages in ticket channel 143 | fields = 2 144 | async for message in inter.channel.history(limit=100, oldest_first=True): 145 | if fields == 25: 146 | await ticket_log_channel.send(embed=log_emb) 147 | log_emb = disnake.Embed( 148 | color=self.bot.config.colors.white, 149 | ) 150 | fields = 1 151 | log_emb.add_field( 152 | name=f"{message.author.name}", 153 | value=message.content[:1024], 154 | inline=False, 155 | ) 156 | if len(message.content) > 1024: 157 | log_emb.add_field( 158 | name=f"{message.author.name} (cont.)", 159 | value=message.content[1024:2048], 160 | inline=False, 161 | ) 162 | fields += 1 163 | log_emb.set_footer(text=f"{inter.author}") 164 | log_emb.timestamp = datetime.now() 165 | await ticket_log_channel.send(embed=log_emb) 166 | await inter.channel.delete() 167 | else: 168 | await errorEmb(inter, "This is not a ticket channel.") 169 | 170 | @commands.slash_command(name="adduser", description="Add user to channel") 171 | @commands.has_permissions(manage_roles=True) 172 | async def add_user(self, inter, member: disnake.Member): 173 | if self.check_if_ticket_channel(inter): 174 | await inter.channel.set_permissions( 175 | member, 176 | send_messages=True, 177 | read_messages=True, 178 | add_reactions=True, 179 | embed_links=True, 180 | attach_files=True, 181 | read_message_history=True, 182 | external_emojis=True, 183 | ) 184 | em = disnake.Embed( 185 | title="Add", 186 | description=f"{inter.author.mention} has added {member.mention} to {inter.channel.mention}", 187 | ) 188 | await inter.send(embed=em) 189 | else: 190 | await errorEmb(inter, "This is not a ticket channel.") 191 | 192 | @commands.slash_command(name="removeuser", description="Remove user from channel") 193 | @commands.has_permissions(manage_roles=True) 194 | async def remove_user(self, inter, member: disnake.Member): 195 | if self.check_if_ticket_channel(inter): 196 | await inter.channel.set_permissions(member, overwrite=None) 197 | em = disnake.Embed( 198 | title="Remove", 199 | description=f"{inter.author.mention} has removed {member.mention} from {inter.channel.mention}", 200 | ) 201 | await inter.send(embed=em) 202 | else: 203 | await errorEmb(inter, "This is not a ticket channel.") 204 | 205 | @staticmethod 206 | def check_if_ticket_channel(inter): 207 | if ( 208 | "ticket-" in inter.channel.name 209 | and len(inter.channel.name) > 10 210 | and any(char.isdigit() for char in inter.channel.name) 211 | ): 212 | return True 213 | else: 214 | return False 215 | 216 | 217 | def setup(bot: OGIROID): 218 | bot.add_cog(Tickets(bot)) 219 | -------------------------------------------------------------------------------- /cogs/Birthdays.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import datetime as dt 3 | import random 4 | 5 | import disnake 6 | from disnake.ext import commands, tasks 7 | 8 | from utils.CONSTANTS import months, congrats_messages 9 | from utils.DBhandlers import BirthdayHandler 10 | from utils.bot import OGIROID 11 | from utils.exceptions import UserAlreadyExists, UserNotFound 12 | from utils.shortcuts import QuickEmb, sucEmb, errorEmb 13 | 14 | 15 | async def get_days_until_birthday(user_data) -> (int, str): 16 | """returns the days until the next birthday and the next birthday date formatted for discord""" 17 | next_birthday = datetime.datetime.strptime( 18 | user_data.birthday + f"/{dt.datetime.now().year}", "%d/%m/%Y" 19 | ) 20 | if next_birthday < datetime.datetime.now(): 21 | next_birthday = datetime.datetime.strptime( 22 | user_data.birthday + f"/{dt.datetime.now().year + 1}", "%d/%m/%Y" 23 | ) 24 | return ( 25 | next_birthday - datetime.datetime.now() 26 | ).days, f"" 27 | 28 | 29 | class Birthday(commands.Cog): 30 | def __init__(self, bot: OGIROID): 31 | self.bot = bot 32 | self.birthday: BirthdayHandler = None 33 | self.birthday_check.start() 34 | 35 | @commands.Cog.listener() 36 | async def on_ready(self): 37 | if not self.bot.ready_: 38 | self.birthday: BirthdayHandler = BirthdayHandler(self.bot, self.bot.db) 39 | 40 | def cog_unload(self): 41 | self.birthday_check.cancel() 42 | 43 | @commands.slash_command(name="birthday", description="Birthdays base command") 44 | async def birthday(self, inter: disnake.ApplicationCommandInteraction): 45 | pass 46 | 47 | @birthday.sub_command( 48 | name="set", 49 | description="Set your birthday. Cant be removed without Staff.", 50 | ) 51 | async def set( 52 | self, 53 | inter, 54 | day: int = commands.Param( 55 | name="day", 56 | ge=1, 57 | le=31, 58 | description="The day of your birthday. Select carefully.", 59 | ), 60 | month: str = commands.Param( 61 | name="month", 62 | description="The month of your birthday. Select carefully.", 63 | choices=months, 64 | ), 65 | ): 66 | await inter.response.defer() 67 | if month is None or day is None: 68 | return await errorEmb(inter, "You need to provide a month and a day") 69 | if day < 1 or day > 31: 70 | return await errorEmb(inter, "The day must be between 1 and 31") 71 | 72 | birth_date = f"{day}/{month}" 73 | try: 74 | await self.birthday.create_user(inter.author.id, birth_date) 75 | except UserAlreadyExists: 76 | return await errorEmb(inter, "You already have a birthday set") 77 | 78 | await sucEmb(inter, f"Your birthday has been set to {birth_date}") 79 | 80 | @commands.has_permissions(manage_roles=True) 81 | @birthday.sub_command( 82 | name="edit", 83 | description="Edit a users birthday. Can only be done by Staff.", 84 | ) 85 | async def edit( 86 | self, 87 | inter, 88 | day: int = commands.Param( 89 | name="day", ge=1, le=31, description="The day of the birthday." 90 | ), 91 | month: str = commands.Param( 92 | name="month", 93 | description="The month of the birthday.", 94 | choices=months, 95 | ), 96 | user: disnake.User = commands.Param( 97 | name="user", description="User to edit the birthday of." 98 | ), 99 | ): 100 | await inter.response.defer() 101 | try: 102 | await self.birthday.update_user(user.id, f"{day}/{month}") 103 | return await sucEmb(inter, f"Birthday has been updated to {day}/{month}") 104 | except UserNotFound: 105 | return await errorEmb(inter, "The User doesn't have a birthday set") 106 | 107 | @commands.has_permissions(manage_roles=True) 108 | @birthday.sub_command( 109 | name="remove", 110 | description="Remove a birthday. Can only be done by Staff.", 111 | ) 112 | async def remove( 113 | self, 114 | inter: disnake.ApplicationCommandInteraction, 115 | user: disnake.User = commands.Param( 116 | name="user", description="Removes the birthday of this user" 117 | ), 118 | ): 119 | await inter.response.defer() 120 | try: 121 | await self.birthday.delete_user(user.id) 122 | except UserNotFound: 123 | return await errorEmb(inter, "This user doesn't have a birthday set") 124 | 125 | await sucEmb(inter, "The birthday has been removed") 126 | 127 | @birthday.sub_command(name="get", description="Get the birthday of a user") 128 | async def get( 129 | self, 130 | inter, 131 | user: disnake.User = commands.Param(name="user", default=None), 132 | ): 133 | await inter.response.defer() 134 | if user is None: 135 | user = inter.author 136 | else: 137 | user = await self.bot.fetch_user(user.id) 138 | 139 | birthday = await self.birthday.get_user(user.id) 140 | if birthday is None: 141 | return await errorEmb(inter, "This user doesn't have a birthday set") 142 | 143 | days, discord_date = await get_days_until_birthday(birthday) 144 | await QuickEmb( 145 | inter, 146 | f"{user.mention}'s birthday is in {days} Days." f"{discord_date}", 147 | ).send() 148 | 149 | @birthday.sub_command(name="next", description="Get the next birthday") 150 | async def next(self, inter: disnake.ApplicationCommandInteraction): 151 | await inter.response.defer() 152 | upcoming_birthdays = [] 153 | # loop gets next birthday 154 | for user in await self.birthday.get_users(): 155 | # gets days until birthday and the discord date 156 | days, discord_date = await get_days_until_birthday(user) 157 | # checks if user is in the guild 158 | upcoming_birthdays.append( 159 | {"days": days, "user": user, "discord_date": discord_date} 160 | ) 161 | 162 | # sorts birthdays by days 163 | upcoming_birthdays.sort(key=lambda x: x["days"]) 164 | # gets the next birthday's user 165 | next_birthday = upcoming_birthdays[0]["user"] 166 | # checks if user is in the guild 167 | while await inter.guild.getch_member(next_birthday.user_id) is None: 168 | upcoming_birthdays.pop(0) 169 | if len(upcoming_birthdays) == 0: 170 | return await errorEmb(inter, "There are no birthdays set in this guild") 171 | next_birthday = upcoming_birthdays[0]["user"] 172 | 173 | member = await self.bot.fetch_user(next_birthday.user_id) 174 | 175 | if next_birthday is None: 176 | return await errorEmb(inter, "There are no birthdays set") 177 | 178 | days, discord_date = await get_days_until_birthday(next_birthday) 179 | await QuickEmb( 180 | inter, 181 | f"{member.mention}'s birthday is in {days} Days." f"{discord_date}", 182 | ).send() 183 | 184 | # @tasks.loop(time=[dt.time(dt.datetime.utcnow().hour, dt.datetime.utcnow().minute, dt.datetime.utcnow().second + 10)]) 185 | # ^ use this when testing birthdays 186 | @tasks.loop(time=[dt.time(8, 0, 0)]) 187 | # loops every day at 8:00 UTC time 188 | async def birthday_check(self): 189 | channel = await self.bot.fetch_channel(self.bot.config.channels.birthdays) 190 | guild = await self.bot.fetch_guild(self.bot.config.guilds.main_guild) 191 | if channel is None: 192 | return 193 | today = dt.datetime.utcnow().strftime("%d/%m") 194 | # Gets all users from the db 195 | users = await self.birthday.get_users() 196 | for user in users: 197 | member = await guild.getch_member(user.user_id) 198 | # if the member is None, the user is not in the server anymore 199 | if member is None: 200 | continue 201 | 202 | # if the birthday is today, congratulate the user 203 | if user.birthday == today: 204 | await member.add_roles(guild.get_role(self.bot.config.roles.birthday)) 205 | congrats_msg = await channel.send( 206 | f"{random.choice(congrats_messages)} {member.mention}! 🎂" 207 | ) 208 | await congrats_msg.add_reaction("🥳") 209 | # If the birthday isn't today and the user still has the birthday role, remove it 210 | elif ( 211 | user.birthday != today 212 | and member.get_role(self.bot.config.roles.birthday) is not None 213 | ): 214 | await member.remove_roles( 215 | guild.get_role(self.bot.config.roles.birthday) 216 | ) 217 | 218 | 219 | def setup(bot): 220 | bot.add_cog(Birthday(bot)) 221 | -------------------------------------------------------------------------------- /cogs/Developer.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | import io 3 | import os 4 | import textwrap 5 | import traceback 6 | from contextlib import redirect_stdout 7 | 8 | import disnake 9 | from disnake import ApplicationCommandInteraction 10 | from disnake.ext import commands 11 | from disnake.ext.commands import Cog, Param 12 | 13 | from utils import checks 14 | from utils.assorted import traceback_maker 15 | from utils.bot import OGIROID 16 | from utils.pagination import CreatePaginator 17 | from utils.shortcuts import errorEmb 18 | 19 | 20 | class Dev(Cog): 21 | def __init__(self, bot: OGIROID): 22 | self.bot = bot 23 | 24 | self._last_result = None 25 | 26 | def cleanup_code(self, content): 27 | """Automatically removes code blocks from the code.""" 28 | # remove ```py\n``` 29 | if content.startswith("```") and content.endswith("```"): 30 | return "\n".join(content.split("\n")[1:-1]) 31 | 32 | # remove `foo` 33 | return content.strip("` \n") 34 | 35 | @commands.slash_command() 36 | @checks.is_dev() 37 | async def restart(self, inter): 38 | """Restarts the bot""" 39 | await inter.send("Restarting...") 40 | await self.eval( 41 | inter, 42 | body="exec(type((lambda: 0).__code__)(0, 0, 0, 0, 0, 0, b'\x053', (), (), (), '', '', 0, b''))", 43 | ) 44 | 45 | @commands.slash_command() 46 | @checks.is_dev() 47 | async def eval(self, inter, *, body: str): 48 | """Evaluates a code snippet""" 49 | await inter.response.defer() 50 | env = { 51 | "bot": self.bot, 52 | "inter": inter, 53 | "channel": inter.channel, 54 | "author": inter.author, 55 | "guild": inter.guild, 56 | "message": await inter.original_message(), 57 | "_": self._last_result, 58 | } 59 | 60 | env.update(globals()) 61 | 62 | body = self.cleanup_code(body) 63 | stdout = io.StringIO() 64 | 65 | to_compile = f'async def func():\n{textwrap.indent(body, " ")}' 66 | 67 | try: 68 | exec(to_compile, env) 69 | except Exception as e: 70 | return await inter.send(f"```py\n{e.__class__.__name__}: {e}\n```") 71 | 72 | func = env["func"] 73 | try: 74 | with redirect_stdout(stdout): 75 | ret = await func() 76 | except Exception as e: 77 | value = stdout.getvalue() 78 | await inter.send(f"```py\n{value}{traceback.format_exc()}\n```") 79 | else: 80 | value = stdout.getvalue() 81 | try: 82 | await (await inter.original_message()).add_reaction("\u2705") 83 | except: 84 | pass 85 | 86 | if ret is None: 87 | if value: 88 | await inter.send(f"```py\n{value}\n```") 89 | else: 90 | self._last_result = ret 91 | await inter.send(f"```py\n{value}{ret}\n```") 92 | 93 | @staticmethod 94 | def autocomplete(inter: ApplicationCommandInteraction, option_name: str): 95 | """Autocomplete for the reload command""" 96 | options = os.listdir("cogs") 97 | options = [option[:-3] for option in options if option.endswith(".py")] 98 | return [ 99 | option 100 | for option in options 101 | if option.startswith(inter.data.options[0].value) 102 | ] 103 | 104 | @staticmethod 105 | def autocomplete_util(inter: ApplicationCommandInteraction, option_name: str): 106 | """Autocomplete for the reload command""" 107 | options = os.listdir("utils") 108 | options = [option[:-3] for option in options if option.endswith(".py")] 109 | return [ 110 | option 111 | for option in options 112 | if option.startswith(inter.data.options[0].value) 113 | ] 114 | 115 | @commands.slash_command() 116 | @checks.is_dev() 117 | async def say( 118 | self, 119 | inter: ApplicationCommandInteraction, 120 | *, 121 | what_to_say: str, 122 | channel: disnake.TextChannel = None, 123 | times: int = 1, 124 | allow_mentions: bool = False, 125 | ): 126 | """Repeats text, optionally in a different channel and a maximum of 10 times""" 127 | await inter.response.defer() 128 | await (await inter.original_message()).delete() 129 | t_channel = channel or inter.channel 130 | allowed_mentions = ( 131 | disnake.AllowedMentions.none() 132 | if not allow_mentions 133 | else disnake.AllowedMentions.all() 134 | ) 135 | if allow_mentions and times > 1: 136 | return await errorEmb( 137 | inter, "You can't allow mentions and repeat more than once" 138 | ) 139 | print(min(abs(times), 10)) 140 | if abs(times) > 1: 141 | for _ in range(min(abs(times), 10)): 142 | await t_channel.send(what_to_say, allowed_mentions=allowed_mentions) 143 | else: 144 | await t_channel.send(f"{what_to_say}", allowed_mentions=allowed_mentions) 145 | 146 | @commands.slash_command() 147 | @checks.is_dev() 148 | async def load( 149 | self, 150 | inter: ApplicationCommandInteraction, 151 | name: str = Param(autocomplete=autocomplete), 152 | ): 153 | """Loads an extension""" 154 | name = name.title() 155 | try: 156 | self.bot.load_extension(f"cogs.{name}") 157 | except Exception as e: 158 | return await inter.send(traceback_maker(e)) 159 | await inter.send(f"Loaded extension **{name}.py**") 160 | 161 | @commands.slash_command() 162 | @checks.is_dev() 163 | async def unload( 164 | self, 165 | inter: ApplicationCommandInteraction, 166 | name: str = Param(autocomplete=autocomplete), 167 | ): 168 | """Unloads an extension.""" 169 | name = name.title() 170 | try: 171 | self.bot.unload_extension(f"cogs.{name}") 172 | except Exception as e: 173 | return await inter.send(traceback_maker(e)) 174 | await inter.send(f"Unloaded extension **{name}.py**") 175 | 176 | @commands.slash_command() 177 | @checks.is_dev() 178 | async def reload( 179 | self, 180 | inter: ApplicationCommandInteraction, 181 | name: str = Param(autocomplete=autocomplete), 182 | ): 183 | """Reloads an extension.""" 184 | name = name.title() 185 | try: 186 | self.bot.reload_extension(f"cogs.{name}") 187 | except Exception as e: 188 | return await inter.send(traceback_maker(e)) 189 | await inter.send(f"Reloaded extension **{name}.py**") 190 | 191 | @commands.slash_command() 192 | @checks.is_dev() 193 | async def reloadall(self, inter: ApplicationCommandInteraction): 194 | """Reloads all extensions.""" 195 | error_collection = [] 196 | for file in os.listdir("cogs"): 197 | if file.endswith(".py"): 198 | name = file[:-3] 199 | try: 200 | self.bot.reload_extension(f"cogs.{name}") 201 | except Exception as e: 202 | error_collection.append([file, traceback_maker(e, advance=False)]) 203 | 204 | if error_collection: 205 | output = "\n".join( 206 | [f"**{g[0]}** ```diff\n- {g[1]}```" for g in error_collection] 207 | ) 208 | return await inter.send( 209 | f"Attempted to reload all extensions, was able to reload, " 210 | f"however the following failed...\n\n{output}" 211 | ) 212 | 213 | await inter.send("Successfully reloaded all extensions") 214 | 215 | @commands.slash_command() 216 | @checks.is_dev() 217 | async def reloadutils( 218 | self, 219 | inter: ApplicationCommandInteraction, 220 | name: str = Param(autocomplete=autocomplete_util), 221 | ): 222 | """Reloads a utils module.""" 223 | name_maker = f"utils/{name}.py" 224 | try: 225 | module_name = importlib.import_module(f"utils.{name}") 226 | importlib.reload(module_name) 227 | except ModuleNotFoundError: 228 | return await inter.send(f"Couldn't find module named **{name_maker}**") 229 | except Exception as e: 230 | error = traceback_maker(e) 231 | return await inter.send( 232 | f"Module **{name_maker}** returned error and was not reloaded...\n{error}" 233 | ) 234 | await inter.send(f"Reloaded module **{name_maker}**") 235 | 236 | @checks.is_dev() 237 | @commands.slash_command(description="Command ID help") 238 | async def dev_help(self, inter): 239 | embeds = [] 240 | 241 | for n in range(0, len(self.bot.global_slash_commands), 10): 242 | embed = disnake.Embed(title="Commands", color=self.bot.config.colors.white) 243 | cmds = self.bot.global_slash_commands[n : n + 10] 244 | 245 | value = "" 246 | for cmd in cmds: 247 | value += f"`/{cmd.name}` - `{cmd.id}`\n" 248 | 249 | if value == "": 250 | continue 251 | 252 | embed.description = f"{value}" 253 | embeds.append(embed) 254 | 255 | paginator = CreatePaginator(embeds, inter.author.id, timeout=300.0) 256 | await inter.send(embed=embeds[0], view=paginator) 257 | 258 | 259 | def setup(bot: OGIROID): 260 | bot.add_cog(Dev(bot)) 261 | -------------------------------------------------------------------------------- /utils/timeconversions.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import datetime 4 | import re 5 | import time 6 | from typing import TYPE_CHECKING, Optional 7 | 8 | import parsedatetime as pdt 9 | from dateutil.relativedelta import relativedelta 10 | from disnake.ext import commands 11 | from disnake.ext.commands import Context 12 | 13 | from .formats import plural, human_join, format_dt as format_dt 14 | 15 | # Monkey patch mins and secs into the units 16 | units = pdt.pdtLocales["en_US"].units 17 | units["minutes"].append("mins") 18 | units["seconds"].append("secs") 19 | 20 | if TYPE_CHECKING: 21 | from typing_extensions import Self 22 | 23 | 24 | class ShortTime: 25 | compiled = re.compile( 26 | """ 27 | (?:(?P[0-9])(?:years?|y))? # e.g. 2y 28 | (?:(?P[0-9]{1,2})(?:months?|mo))? # e.g. 2months 29 | (?:(?P[0-9]{1,4})(?:weeks?|w))? # e.g. 10w 30 | (?:(?P[0-9]{1,5})(?:days?|d))? # e.g. 14d 31 | (?:(?P[0-9]{1,5})(?:hours?|h))? # e.g. 12h 32 | (?:(?P[0-9]{1,5})(?:minutes?|m))? # e.g. 10m 33 | (?:(?P[0-9]{1,5})(?:seconds?|s))? # e.g. 15s 34 | """, 35 | re.VERBOSE, 36 | ) 37 | 38 | def __init__( 39 | self, argument: str, *, now: Optional[datetime.datetime] = None 40 | ): 41 | match = self.compiled.fullmatch(argument) 42 | if match is None or not match.group(0): 43 | raise commands.BadArgument("invalid time provided") 44 | 45 | data = {k: int(v) for k, v in match.groupdict(default=0).items()} 46 | now = now or datetime.datetime.now(datetime.timezone.utc) 47 | self.dt: datetime.datetime = now + relativedelta(**data) 48 | 49 | @classmethod 50 | async def convert(cls, ctx: Context, argument: str) -> Self: 51 | return cls(argument, now=ctx.message.created_at) 52 | 53 | 54 | class HumanTime: 55 | calendar = pdt.Calendar(version=pdt.VERSION_CONTEXT_STYLE) 56 | 57 | def __init__( 58 | self, argument: str, *, now: Optional[datetime.datetime] = None 59 | ): 60 | now = now or datetime.datetime.utcnow() 61 | dt, status = self.calendar.parseDT(argument, sourceTime=now) 62 | if not status.hasDateOrTime: 63 | raise commands.BadArgument( 64 | 'invalid time provided, try e.g. "tomorrow" or "3 days"' 65 | ) 66 | 67 | if not status.hasTime: 68 | # replace it with the current time 69 | dt = dt.replace( 70 | hour=now.hour, 71 | minute=now.minute, 72 | second=now.second, 73 | microsecond=now.microsecond, 74 | ) 75 | 76 | self.dt: datetime.datetime = dt 77 | self._past: bool = dt < now 78 | 79 | @classmethod 80 | async def convert(cls, ctx: Context, argument: str) -> Self: 81 | return cls(argument, now=ctx.message.created_at) 82 | 83 | 84 | class Time(HumanTime): 85 | def __init__( 86 | self, argument: str, *, now: Optional[datetime.datetime] = None 87 | ): 88 | try: 89 | o = ShortTime(argument, now=now) 90 | except Exception: 91 | super().__init__(argument) 92 | else: 93 | self.dt = o.dt 94 | self._past = False 95 | 96 | 97 | class FutureTime(Time): 98 | def __init__( 99 | self, argument: str, *, now: Optional[datetime.datetime] = None 100 | ): 101 | super().__init__(argument, now=now) 102 | 103 | if self._past: 104 | raise commands.BadArgument("this time is in the past") 105 | 106 | 107 | class FriendlyTimeResult: 108 | dt: datetime.datetime 109 | arg: str 110 | 111 | __slots__ = ("dt", "arg") 112 | 113 | def __init__(self, dt: datetime.datetime): 114 | self.dt = dt 115 | self.arg = "" 116 | 117 | async def ensure_constraints( 118 | self, now: datetime.datetime, remaining: str 119 | ) -> None: 120 | if self.dt.replace(tzinfo=None) < now.replace(tzinfo=None): 121 | raise commands.BadArgument("This time is in the past.") 122 | 123 | self.arg = remaining 124 | 125 | 126 | def human_timedelta( 127 | dt: datetime.datetime, 128 | *, 129 | source: Optional[datetime.datetime] = None, 130 | accuracy: Optional[int] = 3, 131 | brief: bool = False, 132 | suffix: bool = True, 133 | ) -> str: 134 | now = source or datetime.datetime.now(datetime.timezone.utc) 135 | if dt.tzinfo is None: 136 | dt = dt.replace(tzinfo=datetime.timezone.utc) 137 | 138 | if now.tzinfo is None: 139 | now = now.replace(tzinfo=datetime.timezone.utc) 140 | 141 | # Microsecond free zone 142 | now = now.replace(microsecond=0) 143 | dt = dt.replace(microsecond=0) 144 | # This implementation uses relativedelta instead of the much more obvious 145 | # divmod approach with seconds because the seconds approach is not entirely 146 | # accurate once you go over 1 week in terms of accuracy since you have to 147 | # hardcode a month as 30 or 31 days. 148 | # A query like "11 months" can be interpreted as "!1 months and 6 days" 149 | if dt > now: 150 | delta = relativedelta(dt, now) 151 | output_suffix = "" 152 | else: 153 | delta = relativedelta(now, dt) 154 | output_suffix = " ago" if suffix else "" 155 | 156 | attrs = [ 157 | ("year", "y"), 158 | ("month", "mo"), 159 | ("day", "d"), 160 | ("hour", "h"), 161 | ("minute", "m"), 162 | ("second", "s"), 163 | ] 164 | 165 | output = [] 166 | for attr, brief_attr in attrs: 167 | elem = getattr(delta, attr + "s") 168 | if not elem: 169 | continue 170 | 171 | if attr == "day": 172 | weeks = delta.weeks 173 | if weeks: 174 | elem -= weeks * 7 175 | if not brief: 176 | output.append(format(plural(weeks), "week")) 177 | else: 178 | output.append(f"{weeks}w") 179 | 180 | if elem <= 0: 181 | continue 182 | 183 | if brief: 184 | output.append(f"{elem}{brief_attr}") 185 | else: 186 | output.append(format(plural(elem), attr)) 187 | 188 | if accuracy is not None: 189 | output = output[:accuracy] 190 | 191 | if len(output) == 0: 192 | return "now" 193 | else: 194 | if not brief: 195 | return human_join(output, final="and") + output_suffix 196 | else: 197 | return " ".join(output) + output_suffix 198 | 199 | 200 | def format_relative(dt: datetime.datetime) -> str: 201 | return format_dt(dt, "R") 202 | 203 | 204 | async def convert(argument: str) -> FriendlyTimeResult: 205 | try: 206 | if argument.casefold() == "never": 207 | return FriendlyTimeResult( 208 | datetime.datetime.fromtimestamp(9999999999) 209 | ) 210 | calendar = HumanTime.calendar 211 | regex = ShortTime.compiled 212 | now = datetime.datetime.fromtimestamp(int(time.time())) 213 | match = regex.match(argument) 214 | if match is not None and match.group(0): 215 | data = {k: int(v) for k, v in match.groupdict(default=0).items()} 216 | remaining = argument[match.end() :].strip() 217 | result = FriendlyTimeResult(now + relativedelta(**data)) 218 | await result.ensure_constraints(now, remaining) 219 | return result 220 | # apparently nlp does not like "from now" 221 | # it likes "from x" in other cases though so let me handle the 'now' case 222 | if argument.endswith("from now"): 223 | argument = argument[:-8].strip() 224 | if argument[0:2] == "me": 225 | # starts with "me to", "me in", or "me at " 226 | if argument[0:6] in ("me to ", "me in ", "me at "): 227 | argument = argument[6:] 228 | elements = calendar.nlp(argument, sourceTime=now) 229 | if elements is None or len(elements) == 0: 230 | raise commands.BadArgument( 231 | 'Invalid time provided, try e.g. "tomorrow" or "3 days".' 232 | ) 233 | # handle the following cases: 234 | # "date time" foo 235 | # date time foo 236 | # foo date time 237 | # first the first two cases: 238 | dt, status, begin, end, dt_string = elements[0] 239 | if not status.hasDateOrTime: 240 | raise commands.BadArgument( 241 | 'Invalid time provided, try e.g. "tomorrow" or "3 days".' 242 | ) 243 | if begin not in (0, 1) and end != len(argument): 244 | raise commands.BadArgument( 245 | "Time is either in an inappropriate location, which " 246 | "must be either at the end or beginning of your input, " 247 | "or I just flat out did not understand what you meant. Sorry." 248 | ) 249 | if not status.hasTime: 250 | # replace it with the current time 251 | dt = dt.replace( 252 | hour=now.hour, 253 | minute=now.minute, 254 | second=now.second, 255 | microsecond=now.microsecond, 256 | ) 257 | # if midnight is provided, just default to next day 258 | if status.accuracy == pdt.pdtContext.ACU_HALFDAY: 259 | dt = dt.replace(day=now.day + 1) 260 | result = FriendlyTimeResult(dt.replace(tzinfo=datetime.timezone.utc)) 261 | remaining = "" 262 | if begin in (0, 1): 263 | if begin == 1: 264 | # check if it's quoted: 265 | if argument[0] != '"': 266 | raise commands.BadArgument( 267 | "Expected quote before time input..." 268 | ) 269 | if not (end < len(argument) and argument[end] == '"'): 270 | raise commands.BadArgument( 271 | "If the time is quoted, you must unquote it." 272 | ) 273 | remaining = argument[end + 1 :].lstrip(" ,.!") 274 | else: 275 | remaining = argument[end:].lstrip(" ,.!") 276 | elif len(argument) == end: 277 | remaining = argument[:begin].strip() 278 | await result.ensure_constraints(now, remaining) 279 | return result 280 | except Exception: 281 | import traceback 282 | 283 | traceback.print_exc() 284 | raise 285 | -------------------------------------------------------------------------------- /cogs/Blacklist.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import time 3 | from typing import TYPE_CHECKING 4 | 5 | import disnake 6 | from disnake import Member, Embed, Option, OptionType 7 | from disnake.ext import commands, tasks 8 | from disnake.ext.commands import Cog 9 | 10 | from utils import timeconversions 11 | from utils.CONSTANTS import timings 12 | from utils.DBhandlers import BlacklistHandler 13 | from utils.db_models import Blacklist 14 | from utils.pagination import CreatePaginator 15 | from utils.shortcuts import sucEmb, errorEmb, get_expiry, wait_until 16 | 17 | if TYPE_CHECKING: 18 | from utils.bot import OGIROID 19 | 20 | 21 | class Blacklist(Cog): 22 | def __init__(self, bot: "OGIROID"): 23 | self.bot = bot 24 | self.blacklist: BlacklistHandler = None 25 | self.del_que = [] 26 | 27 | def get_user(self, user_id): 28 | return self.bot.get_user(user_id) 29 | 30 | async def run_at(self, dt, coro, _id): 31 | await wait_until(dt) 32 | self.del_que.remove(_id) 33 | await coro(_id) 34 | 35 | @tasks.loop(minutes=59) 36 | async def check_blacklist(self): 37 | await asyncio.sleep(5) # ample time for other blacklist module to load 38 | for user in self.bot.blacklist.blacklist: 39 | await self.check_user_removal(user) 40 | 41 | async def check_user_removal(self, user: Blacklist): 42 | if user.id in self.del_que: 43 | return # already being removed 44 | elif user.is_expired(): 45 | await self.blacklist.remove(user.id) 46 | elif int(time.time()) <= user.expires <= (int(time.time()) + timings.HOUR): 47 | self.del_que.append(user.id) 48 | await self.run_at(user.expires, self.blacklist.remove, user.id) 49 | 50 | @commands.Cog.listener() 51 | async def on_ready(self): 52 | if not self.bot.ready_: 53 | await self.bot.wait_until_ready() 54 | self.blacklist: BlacklistHandler = self.bot.blacklist 55 | self.check_blacklist.start() 56 | 57 | @commands.slash_command(description="Blacklist base command") 58 | async def blacklist(self, inter): 59 | pass 60 | 61 | @commands.cooldown(1, 5, commands.BucketType.user) 62 | @blacklist.sub_command(name="info", description="Get info about a blacklisted user") 63 | async def blacklist_info(self, inter, user: Member): 64 | await inter.response.defer() 65 | if not await self.blacklist.blacklisted(user.id): 66 | return await errorEmb(inter, f"{user.mention} is not in the blacklist") 67 | bl_user = await self.blacklist.get_user(user.id) 68 | embed = Embed( 69 | title=f"Blacklisted user: {user.name}", 70 | color=disnake.Color.random(seed=user.name), 71 | ) 72 | embed.add_field(name="Reason", value=bl_user.reason, inline=False) 73 | embed.add_field(name="Expires", value=bl_user.get_expiry, inline=False) 74 | embed.add_field(name="Bot", value=bl_user.bot) 75 | embed.add_field(name="Tickets", value=bl_user.tickets) 76 | embed.add_field(name="Tags", value=bl_user.tags) 77 | await inter.send(embed=embed) 78 | 79 | @blacklist.sub_command_group( 80 | name="edit", description="Edit a user in the blacklist" 81 | ) 82 | async def edit(self, inter): 83 | pass 84 | 85 | @commands.has_permissions(manage_messages=True) 86 | @edit.sub_command( 87 | name="flags", 88 | description="Edit the user's blacklist flags in the blacklist", 89 | ) 90 | async def flags(self, inter, user: Member, bot: bool, tickets: bool, tags: bool): 91 | await inter.response.defer() 92 | if not await self.blacklist.blacklisted(user.id): 93 | return await errorEmb(inter, f"{user.mention} is not in the blacklist") 94 | await self.blacklist.edit_flags(user.id, bot, tickets, tags) 95 | await sucEmb( 96 | inter, 97 | f"Edited {user.mention}'s blacklist flags to\nBot: {bot}, Tickets: {tickets}, Tags: {tags}", 98 | ) 99 | 100 | @commands.has_permissions(manage_messages=True) 101 | @edit.sub_command( 102 | name="reason", 103 | description="Edit the user's blacklist reason in the blacklist", 104 | ) 105 | async def reason(self, inter, user: Member, reason: str): 106 | await inter.response.defer() 107 | if not await self.blacklist.blacklisted(user.id): 108 | return await errorEmb(inter, f"{user.mention} is not in the blacklist") 109 | await self.blacklist.edit_reason(user.id, reason) 110 | await sucEmb( 111 | inter, 112 | f"Edited {user.mention}'s reason in the blacklist to see the full reason use /blacklist info {user.mention}", 113 | ) 114 | 115 | @commands.has_permissions(manage_messages=True) 116 | @edit.sub_command( 117 | name="expiry", 118 | description="Edit the user's blacklist expiry in the blacklist", 119 | ) 120 | async def expiry(self, inter, user: Member, expires: str): 121 | await inter.response.defer() 122 | if not await self.blacklist.blacklisted(user.id): 123 | return await errorEmb(inter, f"{user.mention} is not in the blacklist") 124 | expiry = int((await timeconversions.convert(expires)).dt.timestamp()) 125 | await self.blacklist.edit_expiry(user.id, expiry) 126 | await sucEmb( 127 | inter, 128 | f"Edited the expiry of {user.mention}'s blacklist to expire {get_expiry(expiry)}", 129 | ) 130 | await self.check_user_removal(await self.blacklist.get_user(user.id)) 131 | 132 | @commands.has_permissions(manage_messages=True) 133 | @blacklist.sub_command( 134 | name="remove", description="Remove a user from the blacklist" 135 | ) 136 | async def remove(self, inter, user: Member): 137 | await inter.response.defer() 138 | if not await self.blacklist.blacklisted(user.id): 139 | return await errorEmb(inter, f"{user.mention} is not in the blacklist") 140 | await self.blacklist.remove(user.id) 141 | await sucEmb( 142 | inter, 143 | f"{user.mention} has been removed from blacklist", 144 | ephemeral=False, 145 | ) 146 | 147 | @commands.has_permissions(manage_messages=True) 148 | @blacklist.sub_command( 149 | name="add", 150 | description="Add a user to the blacklist", 151 | options=[ 152 | Option( 153 | "user", 154 | description="User to blacklist", 155 | type=OptionType.user, 156 | required=True, 157 | ), 158 | Option( 159 | "bot", 160 | description="Whether to blacklist the user from using the entire bot", 161 | type=OptionType.boolean, 162 | required=True, 163 | ), 164 | Option( 165 | "tickets", 166 | description="Whether to blacklist the user from using tickets", 167 | type=OptionType.boolean, 168 | required=True, 169 | ), 170 | Option( 171 | "tags", 172 | description="Whether to blacklist the user from using tags", 173 | type=OptionType.boolean, 174 | required=True, 175 | ), 176 | Option( 177 | "expires", 178 | description="When the blacklist should expire (e.g. 1w, 5m, never)", 179 | type=OptionType.string, 180 | required=False, 181 | ), 182 | Option( 183 | "reason", 184 | description="Reason for blacklisting the user", 185 | type=OptionType.string, 186 | required=False, 187 | ), 188 | ], 189 | ) 190 | async def blacklist_add( 191 | self, 192 | inter, 193 | user, 194 | bot, 195 | tickets, 196 | tags, 197 | reason="No Reason Specified", 198 | expires="never", 199 | ): 200 | await inter.response.defer() 201 | if not any((bot, tickets, tags)): 202 | return await errorEmb( 203 | inter, 204 | "You can't blacklist a user without specifying either bot, tickets and/or tags", 205 | ) 206 | elif len(reason) > 900: 207 | return await errorEmb(inter, "Reason must be under 900 chars") 208 | elif user.id == inter.author.id: 209 | return await errorEmb(inter, "You can't blacklist yourself") 210 | elif user.id == self.bot.user.id: 211 | return await errorEmb(inter, "You can't blacklist me") 212 | elif user.id == "511724576674414600": 213 | return await errorEmb(inter, "You can't blacklist my creator :D") 214 | elif await self.blacklist.blacklisted(user.id): 215 | return await errorEmb(inter, f"{user.mention} is already in the blacklist") 216 | expires = (await timeconversions.convert(expires)).dt.timestamp() 217 | await self.blacklist.add(user.id, reason, bot, tickets, tags, expires) 218 | await sucEmb( 219 | inter, 220 | f"{user.mention} added to blacklist\nthe user's blacklist will {f'expire on ' if str(expires) != str(9999999999) else 'never expire'}", 221 | ephemeral=False, 222 | ) 223 | await self.check_user_removal(await self.blacklist.get_user(user.id)) 224 | 225 | @commands.cooldown(1, 30, commands.BucketType.user) 226 | @blacklist.sub_command(name="list", description="List all blacklisted users") 227 | async def blacklist_list(self, inter): 228 | await inter.response.defer() 229 | try: 230 | blacklist_count = await self.blacklist.count() 231 | except AttributeError: 232 | return await errorEmb(inter, "wait for the bot to load") 233 | if blacklist_count == 0: 234 | return await errorEmb(inter, "There are no blacklisted users") 235 | blacklist_reason_count = 0 236 | nested_blacklisted = [[]] 237 | blacklist_embs = [] 238 | nested_count = 0 239 | for user in self.blacklist.blacklist: 240 | if (len(user.reason) + blacklist_reason_count) <= 1990: 241 | blacklist_reason_count += len(user.reason) 242 | if isinstance(nested_blacklisted[nested_count], Blacklist): 243 | nested_count += 1 244 | nested_blacklisted.append([]) 245 | nested_blacklisted[nested_count].append(user) 246 | else: 247 | blacklist_reason_count = 0 248 | nested_blacklisted.append(user) 249 | nested_count += 1 250 | 251 | for blacklist_list in nested_blacklisted: 252 | if not blacklist_list: 253 | continue 254 | 255 | if isinstance(blacklist_list, list): 256 | emb = Embed(color=self.bot.config.colors.invis, description="") 257 | for user in blacklist_list: 258 | emb.add_field( 259 | name=f"**{self.get_user(user.id).name}**", 260 | value=f"Expires: {user.get_expiry}\nReason: {user.reason}\n", 261 | ) 262 | 263 | blacklist_embs.append(emb) 264 | elif isinstance(blacklist_list, Blacklist): 265 | emb = Embed(color=self.bot.config.colors.invis, description="") 266 | emb.title = f"**{self.get_user(blacklist_list.id).name}**" 267 | emb.description = f"Expires: {blacklist_list.get_expiry}\nReason: {blacklist_list.reason}\n" 268 | blacklist_embs.append(emb) 269 | 270 | blacklist_embs.append( 271 | Embed(color=self.bot.config.colors.invis, description="The end ;D") 272 | ) 273 | start_emb = Embed(title="Blacklist", color=self.bot.config.colors.invis) 274 | start_emb.description = f"There are currently {blacklist_count:,d} blacklisted user{'s' if blacklist_count > 1 else ''}, use the arrows below to navigate through them" 275 | blacklist_embs.insert(0, start_emb) 276 | await inter.send( 277 | embed=blacklist_embs[0], 278 | view=CreatePaginator(blacklist_embs, inter.author.id), 279 | ) 280 | 281 | 282 | def setup(bot): 283 | bot.add_cog(Blacklist(bot)) 284 | -------------------------------------------------------------------------------- /utils/pagination.py: -------------------------------------------------------------------------------- 1 | import datetime as dt 2 | import math 3 | from typing import TYPE_CHECKING 4 | 5 | from disnake import ui, ButtonStyle, Embed, MessageInteraction 6 | 7 | from utils.exceptions import UserNotFound 8 | from utils.shortcuts import errorEmb 9 | 10 | if TYPE_CHECKING: 11 | from cogs.Levels import LevelsController 12 | 13 | 14 | class CreatePaginator(ui.View): 15 | """ 16 | Paginator for Embeds. 17 | Parameters: 18 | ---------- 19 | embeds: List[Embed] 20 | List of embeds which are in the Paginator. Paginator starts from first embed. 21 | author: int 22 | The ID of the author who can interact with the buttons. Anyone can interact with the Paginator Buttons if not specified. 23 | timeout: float 24 | How long the Paginator should time out in, after the last interaction. 25 | 26 | """ 27 | 28 | def __init__(self, embeds: list, author: int = 123, timeout: float = None): 29 | self.embeds = embeds 30 | self.author = author 31 | self.CurrentEmbed = 0 32 | super().__init__(timeout=timeout if timeout else 180) 33 | 34 | @ui.button(emoji="⏮️", style=ButtonStyle.grey) 35 | async def front(self, button, inter): 36 | try: 37 | if inter.author.id != self.author and self.author != 123: 38 | return await inter.send( 39 | "You cannot interact with these buttons.", ephemeral=True 40 | ) 41 | elif self.CurrentEmbed == 0: 42 | return await inter.send( 43 | "You are already on the first page.", ephemeral=True 44 | ) 45 | elif self.CurrentEmbed: 46 | await inter.response.defer() 47 | await inter.edit_original_response(embed=self.embeds[0]) 48 | self.CurrentEmbed = 0 49 | else: 50 | raise () 51 | 52 | except Exception as e: 53 | print(e) 54 | await inter.send("Unable to change the page.", ephemeral=True) 55 | 56 | @ui.button(emoji="⬅️", style=ButtonStyle.grey) 57 | async def previous(self, button, inter): 58 | try: 59 | if inter.author.id != self.author and self.author != 123: 60 | return await inter.send( 61 | "You cannot interact with these buttons.", ephemeral=True 62 | ) 63 | elif self.CurrentEmbed == 0: 64 | return await inter.send( 65 | "You are already on the first page.", ephemeral=True 66 | ) 67 | if self.CurrentEmbed: 68 | await inter.response.defer() 69 | await inter.edit_original_response( 70 | embed=self.embeds[self.CurrentEmbed - 1] 71 | ) 72 | self.CurrentEmbed = self.CurrentEmbed - 1 73 | else: 74 | raise () 75 | 76 | except Exception as e: 77 | print(e) 78 | await inter.send("Unable to change the page.", ephemeral=True) 79 | 80 | @ui.button(emoji="➡️", style=ButtonStyle.grey) 81 | async def next(self, button, inter): 82 | try: 83 | if inter.author.id != self.author and self.author != 123: 84 | return await inter.send( 85 | "You cannot interact with these buttons.", ephemeral=True 86 | ) 87 | elif self.CurrentEmbed == len(self.embeds) - 1: 88 | return await inter.send("you are already at the end", ephemeral=True) 89 | await inter.response.defer() 90 | await inter.edit_original_response(embed=self.embeds[self.CurrentEmbed + 1]) 91 | self.CurrentEmbed += 1 92 | 93 | except Exception as e: 94 | print(e) 95 | await inter.send("Unable to change the page.", ephemeral=True) 96 | 97 | @ui.button(emoji="⏭️", style=ButtonStyle.grey) 98 | async def end(self, button, inter): 99 | try: 100 | if inter.author.id != self.author and self.author != 123: 101 | return await inter.send( 102 | "You cannot interact with these buttons.", ephemeral=True 103 | ) 104 | elif self.CurrentEmbed == len(self.embeds) - 1: 105 | return await inter.send("you are already at the end", ephemeral=True) 106 | await inter.response.defer() 107 | await inter.edit_original_response(embed=self.embeds[len(self.embeds) - 1]) 108 | self.CurrentEmbed = len(self.embeds) - 1 109 | 110 | except Exception as e: 111 | print(e) 112 | await inter.send("Unable to change the page.", ephemeral=True) 113 | 114 | 115 | class LeaderboardView(ui.View): 116 | """ 117 | Paginator for Leaderboards. 118 | Parameters: 119 | ---------- 120 | author: int 121 | The ID of the author who can interact with the buttons. Anyone can interact with the Paginator Buttons if not specified. 122 | UserPicked: bool 123 | If True, the user has already been shown and no need to add an extra field 124 | """ 125 | 126 | def __init__( 127 | self, 128 | controller: "LevelsController", 129 | firstemb: Embed, 130 | author: int = 123, 131 | set_user: bool = False, 132 | timeout: float = None, 133 | ): 134 | self.controller = controller 135 | self.author = author 136 | self.CurrentEmbed = 0 137 | self.firstemb = firstemb 138 | self.user_set = set_user 139 | super().__init__( 140 | timeout=timeout if timeout else 180, 141 | ) 142 | 143 | async def at_last_page(self, inter): 144 | records = await self.controller.get_count(inter.guild.id) 145 | if records % 10 == 0: 146 | last_page = records // 10 - 1 147 | else: 148 | last_page = records // 10 149 | if self.CurrentEmbed == last_page: 150 | return True 151 | else: 152 | return False 153 | 154 | async def create_page(self, inter, page_num) -> Embed: 155 | if page_num == 0: 156 | return self.firstemb 157 | else: 158 | offset = page_num * 10 159 | records = await self.controller.get_leaderboard( 160 | inter.guild, limit=10, offset=offset 161 | ) 162 | try: 163 | cmd_user = await self.controller.get_user(inter.author) 164 | except UserNotFound: 165 | cmd_user = None 166 | 167 | if not records: 168 | return await errorEmb(inter, text="No records found!") 169 | embed = Embed(title="Leaderboard", color=0x00FF00) 170 | 171 | for i, record in enumerate(records): 172 | user = await inter.bot.fetch_user(record.user_id) 173 | if record.user_id == inter.author.id: 174 | embed.add_field( 175 | name=f"{i + 1 + offset}. {user} ~ You ", 176 | value=f"Level: {record.level}\nTotal XP: {record.total_xp:,}", 177 | inline=False, 178 | ) 179 | self.user_set = True 180 | else: 181 | embed.add_field( 182 | name=f"{i + 1 + offset}. {user}", 183 | value=f"Level: {record.level}\nTotal XP: {record.total_xp:,}", 184 | inline=False, 185 | ) 186 | if not self.user_set: 187 | rank = await self.controller.get_rank(inter.guild.id, cmd_user) 188 | embed.add_field( 189 | name=f"{rank}. You", 190 | value=f"Level: {cmd_user.level}\nTotal XP: {cmd_user.total_xp:,}", 191 | inline=False, 192 | ) 193 | 194 | embed.set_footer(text=f"{inter.author}", icon_url=inter.author.avatar.url) 195 | embed.timestamp = dt.datetime.now() 196 | return embed 197 | 198 | @ui.button(emoji="⏮️", style=ButtonStyle.grey) 199 | async def front(self, button, inter): 200 | try: 201 | if inter.author.id != self.author and self.author != 123: 202 | return await inter.send( 203 | "You cannot interact with these buttons.", ephemeral=True 204 | ) 205 | elif self.CurrentEmbed == 0: 206 | return await inter.send( 207 | "You are already on the first page.", ephemeral=True 208 | ) 209 | elif self.CurrentEmbed: 210 | await inter.response.defer() 211 | await inter.edit_original_response( 212 | embed=await self.create_page(inter, 0) 213 | ) 214 | self.CurrentEmbed = 0 215 | else: 216 | raise () 217 | except Exception as e: 218 | await inter.send("Unable to change the page.", ephemeral=True) 219 | 220 | @ui.button(emoji="⬅️", style=ButtonStyle.grey) 221 | async def previous(self, button, inter): 222 | try: 223 | if inter.author.id != self.author and self.author != 123: 224 | return await inter.send( 225 | "You cannot interact with these buttons.", ephemeral=True 226 | ) 227 | elif self.CurrentEmbed == 0: 228 | return await inter.send( 229 | "You are already on the first page.", ephemeral=True 230 | ) 231 | if self.CurrentEmbed: 232 | await inter.response.defer() 233 | await inter.edit_original_response( 234 | embed=await self.create_page(inter, self.CurrentEmbed - 1) 235 | ) 236 | self.CurrentEmbed = self.CurrentEmbed - 1 237 | else: 238 | raise () 239 | 240 | except Exception as e: 241 | print(e) 242 | await inter.send("Unable to change the page.", ephemeral=True) 243 | 244 | @ui.button(emoji="➡️", style=ButtonStyle.grey) 245 | async def next(self, button, inter: MessageInteraction): 246 | try: 247 | if inter.author.id != self.author and self.author != 123: 248 | return await inter.send( 249 | "You cannot interact with these buttons.", ephemeral=True 250 | ) 251 | elif await self.at_last_page(inter): 252 | return await inter.send("you are already at the end", ephemeral=True) 253 | await inter.response.defer() 254 | await inter.edit_original_response( 255 | embed=await self.create_page(inter, self.CurrentEmbed + 1) 256 | ) 257 | self.CurrentEmbed += 1 258 | except Exception as e: 259 | print(e) 260 | await inter.send("Unable to change the page.", ephemeral=True) 261 | 262 | @ui.button(emoji="⏭️", style=ButtonStyle.grey) 263 | async def end(self, button, inter): 264 | try: 265 | if inter.author.id != self.author and self.author != 123: 266 | return await inter.send( 267 | "You cannot interact with these buttons.", ephemeral=True 268 | ) 269 | elif await self.at_last_page(inter): 270 | return await inter.send("you are already at the end", ephemeral=True) 271 | await inter.response.defer() 272 | record_count = await self.controller.get_count(inter.guild.id) 273 | if ( 274 | record_count % 10 == 0 275 | ): # if the number of records is divisible by 10 e.g. 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 etc. then we must subtract 1 from the number of records to get the last page 276 | last_page = record_count // 10 - 1 277 | else: 278 | last_page = math.floor( 279 | record_count // 10 280 | ) # if the number of records is not divisible by 10 e.g. 11, 12, 13, 14, 15, 16, 17, 18, 19, 21, 22, 23, 24, 25, 26, 27, 28, 29 etc. then we can just divide the number of records by 10 to get the last page 281 | await inter.edit_original_response( 282 | embed=await self.create_page(inter, last_page) 283 | ) 284 | self.CurrentEmbed = last_page 285 | 286 | except Exception as e: 287 | await inter.send("Unable to change the page.", ephemeral=True) 288 | --------------------------------------------------------------------------------