master 40854567f362
81 files
337.1 KB
76.5k tokens
Showing preview only (358K chars total). The displayed content is truncated. Use the JSON API for full output.
Repository: discordsuperutils/discord-super-utils
Branch: master
Commit: 40854567f362
Files: 81
Total size: 337.1 KB

Directory structure:
gitextract_eqyr9a2b/

├── .gitignore
├── LICENSE
├── MANIFEST.in
├── README.md
├── discordSuperUtils/
│   ├── __init__.py
│   ├── antispam.py
│   ├── ban.py
│   ├── base.py
│   ├── birthday.py
│   ├── client.py
│   ├── commandhinter.py
│   ├── convertors.py
│   ├── database.py
│   ├── economy.py
│   ├── fivem.py
│   ├── imaging.py
│   ├── infractions.py
│   ├── invitetracker.py
│   ├── kick.py
│   ├── leveling.py
│   ├── messagefilter.py
│   ├── modmail.py
│   ├── music/
│   │   ├── __init__.py
│   │   ├── constants.py
│   │   ├── enums.py
│   │   ├── exceptions.py
│   │   ├── lavalink/
│   │   │   ├── __init__.py
│   │   │   ├── equalizer.py
│   │   │   ├── lavalink.py
│   │   │   └── player.py
│   │   ├── music.py
│   │   ├── player.py
│   │   ├── playlist.py
│   │   ├── queue.py
│   │   └── utils.py
│   ├── mute.py
│   ├── paginator.py
│   ├── prefix.py
│   ├── punishments.py
│   ├── reactionroles.py
│   ├── slash_client.py
│   ├── spotify.py
│   ├── template.py
│   ├── twitch.py
│   └── youtube.py
├── docs/
│   ├── Makefile
│   ├── conf.py
│   ├── index.rst
│   ├── installation.rst
│   ├── make.bat
│   └── source/
│       └── discordSuperUtils.rst
├── examples/
│   ├── advance_music_cog.py
│   ├── antispam.py
│   ├── birthday.py
│   ├── command_hinter.py
│   ├── database.py
│   ├── economy.py
│   ├── fivem.py
│   ├── imaging.py
│   ├── invitetracker.py
│   ├── lavalinkmusic.py
│   ├── leveling.py
│   ├── leveling_cog.py
│   ├── message_filter.py
│   ├── moderation.py
│   ├── modmail.py
│   ├── music.py
│   ├── music_cog.py
│   ├── paginator.py
│   ├── prefix.py
│   ├── reaction_roles.py
│   ├── template.py
│   └── twitch.py
├── requirements.txt
├── setup.py
└── tests/
    ├── database.py
    ├── gather.py
    ├── spotify_fetching.py
    ├── tester.py
    ├── time_converts.py
    └── youtube.py

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
.vscode/
dist/
venv/
*.sqlite
*.egg-info
*.pyc
.idea/

================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2021 koyashie07 & adam7100

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: MANIFEST.in
================================================
include README.md
include LICENSE
include requirements.txt
include discordSuperUtils/*
include discordSuperUtils/assets/*
include discordSuperUtils/music/*
include discordSuperUtils/music/lavalink/*


================================================
FILE: README.md
================================================
<h1 align="center">discord-super-utils</h1>

<p align="center">
  <a href="https://codefactor.io/repository/github/discordsuperutils/discord-super-utils/"><img src="https://img.shields.io/codefactor/grade/github/discordsuperutils/discord-super-utils?style=flat-square" /></a>
  <a href="https://discord.gg/zhwcpTBBeC"><img src="https://img.shields.io/discord/863388828734586880?logo=discord&color=blue&style=flat-square" /></a>
  <a href="https://pepy.tech/project/discordsuperutils"><img src="https://img.shields.io/pypi/dm/discordSuperUtils?color=green&style=flat-square" /></a>
  <a href="https://pypi.org/project/discordSuperUtils/"><img src="https://img.shields.io/pypi/v/discordSuperUtils?style=flat-square" /></a>
  <a href=""><img src="https://img.shields.io/pypi/l/discordSuperUtils?style=flat-square" /></a>
  <a href="https://github.com/psf/black"><img src="https://img.shields.io/badge/code%20style-black-000000.svg?style=flat-square">
    <br/>
  <a href="https://discord-super-utils.gitbook.io/discord-super-utils/">Documentation</a>
  <a href="https://discordsuperutils.readthedocs.io/en/latest/">Secondary Documentation</a>
</p>

<p align="center">
   A modern python module including many useful features that make discord bot programming extremely easy.
    <br/>
   <b>The documentation is not done. if you have any questions, feel free to ask them in our <a href="https://discord.gg/zhwcpTBBeC">discord server.</a></b>
</p>

Features
-------------


- Very easy to use and user-friendly.
- Object Oriented.
- Modern Leveling Manager.
- Modern Music/Audio playing manager. [Lavalink and FFmpeg support]
- Modern Async Database Manager (SQLite, MongoDB, PostgreSQL, MySQL, MariaDB).
- Modern Paginator.
- Modern Reaction Manager.
- Modern Economy Manager.
- Modern Image Manager (PIL).
- Modern Invite Tracker.
- Modern Command Hinter.
- Modern FiveM Server Parser.
- Modern Birthday Manager.
- Modern Prefix Manager.
- Includes easy to use convertors.
- Modern spotify client that is optimized for player fetching.
- Modern Punishment Manager (Kick, Ban, Infractions, Mutes)
- Modern Template Manager.
- Modern CogManager that supports usage of managers in discord cogs.
- Modern MessageFilter and AntiSpam.
- Customizable ModMail Manager
- Modern Youtube client that is optimized for player fetching.
- And many more!
(MORE COMING SOON!)

Installation
--------------

Installing discordSuperUtils is very easy.

```sh
python -m pip install discordSuperUtils
```

Examples
--------------

### Leveling Example (With Role Manager) ###

```py
import discord
from discord.ext import commands

import discordSuperUtils

bot = commands.Bot(command_prefix="-", intents=discord.Intents.all())
LevelingManager = discordSuperUtils.LevelingManager(bot, award_role=True)
ImageManager = (
    discordSuperUtils.ImageManager()
)  # LevelingManager uses ImageManager to create the rank command.


@bot.event
async def on_ready():
    database = discordSuperUtils.DatabaseManager.connect(...)
    await LevelingManager.connect_to_database(database, ["xp", "roles", "role_list"])

    print("Leveling manager is ready.", bot.user)


@LevelingManager.event()
async def on_level_up(message, member_data, roles):
    await message.reply(
        f"You are now level {await member_data.level()}"
        + (f", you have received the {roles[0]}" f" role." if roles else "")
    )


@bot.command()
async def rank(ctx):
    member_data = await LevelingManager.get_account(ctx.author)

    if not member_data:
        await ctx.send(f"I am still creating your account! please wait a few seconds.")
        return

    guild_leaderboard = await LevelingManager.get_leaderboard(ctx.guild)
    member = [x for x in guild_leaderboard if x.member == ctx.author.id]

    image = await ImageManager.create_leveling_profile(
        ctx.author,
        member_data,
        discordSuperUtils.Backgrounds.GALAXY,
        (127, 255, 0),
        guild_leaderboard.index(member[0]) + 1 if member else -1,
        outline=5,
    )
    await ctx.send(file=image)


@bot.command()
async def set_roles(ctx, interval: int, *roles: discord.Role):
    await LevelingManager.set_interval(ctx.guild, interval)
    await LevelingManager.set_roles(ctx.guild, roles)

    await ctx.send(
        f"Successfully set the interval to {interval} and role list to {', '.join(role.name for role in roles)}"
    )


@bot.command()
async def leaderboard(ctx):
    guild_leaderboard = await LevelingManager.get_leaderboard(ctx.guild)
    formatted_leaderboard = [
        f"Member: {x.member}, XP: {await x.xp()}" for x in guild_leaderboard
    ]

    await discordSuperUtils.PageManager(
        ctx,
        discordSuperUtils.generate_embeds(
            formatted_leaderboard,
            title="Leveling Leaderboard",
            fields=25,
            description=f"Leaderboard of {ctx.guild}",
        ),
    ).run()


bot.run("token")
```

![Leveling Manager Example](https://media.giphy.com/media/ey1Iv2HlYYLPy0bm9p/giphy.gif)

### Playing Example ### 

```py
from math import floor

from discord.ext import commands

import discordSuperUtils
from discordSuperUtils import MusicManager
import discord

client_id = ""
client_secret = ""

bot = commands.Bot(command_prefix="-", intents=discord.Intents.all())
# MusicManager = MusicManager(bot, spotify_support=False)


MusicManager = MusicManager(
    bot, client_id=client_id, client_secret=client_secret, spotify_support=True
)


# if using spotify support use this instead ^^^


@MusicManager.event()
async def on_music_error(ctx, error):
    raise error  # add your error handling here! Errors are listed in the documentation.


@MusicManager.event()
async def on_queue_end(ctx):
    print(f"The queue has ended in {ctx}")
    # You could wait and check activity, etc...


@MusicManager.event()
async def on_inactivity_disconnect(ctx):
    print(f"I have left {ctx} due to inactivity..")


@MusicManager.event()
async def on_play(ctx, player):
    await ctx.send(f"Playing {player}")


@bot.event
async def on_ready():
    # database = discordSuperUtils.DatabaseManager.connect(...)
    # await MusicManager.connect_to_database(database, ["playlists"])

    print("Music manager is ready.", bot.user)


@bot.command()
async def leave(ctx):
    if await MusicManager.leave(ctx):
        await ctx.send("Left Voice Channel")


@bot.command()
async def np(ctx):
    if player := await MusicManager.now_playing(ctx):
        duration_played = await MusicManager.get_player_played_duration(ctx, player)
        # You can format it, of course.

        await ctx.send(
            f"Currently playing: {player}, \n"
            f"Duration: {duration_played}/{player.duration}"
        )


@bot.command()
async def join(ctx):
    if await MusicManager.join(ctx):
        await ctx.send("Joined Voice Channel")


@bot.group(invoke_without_command=True)
async def playlists(ctx, user: discord.User):
    user_playlists = await MusicManager.get_user_playlists(user)

    formatted_playlists = [
        f"ID: '{user_playlist.id}'\nTitle: '{user_playlist.playlist.title}'\nTotal Songs: {len(user_playlist.playlist.songs)}"
        for user_playlist in user_playlists
    ]

    embeds = discordSuperUtils.generate_embeds(
        formatted_playlists,
        f"Playlists of {user}",
        f"Shows {user.mention}'s playlists.",
        25,
        string_format="{}",
    )

    page_manager = discordSuperUtils.PageManager(ctx, embeds, public=True)
    await page_manager.run()


@playlists.command()
async def add(ctx, url: str):
    added_playlist = await MusicManager.add_playlist(ctx.author, url)

    if not added_playlist:
        await ctx.send("Playlist URL not found!")
        return

    await ctx.send(f"Playlist added with ID {added_playlist.id}")


@playlists.command()
async def play(ctx, playlist_id: str):
    # This command is just an example, and not something you should do.
    # The saved playlist system is supposed to provide fast, easy and simple playing, and the user should not look for
    # the right playlist id before playing, as that defeats the whole point.
    # Instead of playing using a playlist id, I recommend playing using indexes.
    # Please, if you are playing using indexes, find the playlist id you need by getting all the user's playlists
    # and then finding the id from there.
    # Find the user's playlists using MusicManager.get_user_playlists(ctx.author, partial=True).
    # Make sure partial is True to speed up the fetching progress (incase you want to access the playlist data,
    # you can set it to False, of course).
    # Using these playlists, find the id the user wants, and play it (or whatever else you want to do with it).
    # Be creative!

    user_playlist = await MusicManager.get_playlist(ctx.author, playlist_id)

    if not user_playlist:
        await ctx.send("That playlist does not exist!")
        return

    if not ctx.voice_client or not ctx.voice_client.is_connected():
        await MusicManager.join(ctx)

    async with ctx.typing():
        players = await MusicManager.create_playlist_players(
            user_playlist.playlist, ctx.author
        )

    if players:
        if await MusicManager.queue_add(
            players=players, ctx=ctx
        ) and not await MusicManager.play(ctx):
            await ctx.send(f"Added playlist {user_playlist.playlist.title}")

    else:
        await ctx.send("Query not found.")


@playlists.command()
async def remove(ctx, playlist_id: str):
    user_playlist = await MusicManager.get_playlist(ctx.author, playlist_id)

    if not user_playlist:
        await ctx.send(f"Playlist with id {playlist_id} is not found.")
        return

    await user_playlist.delete()
    await ctx.send(f"Playlist {user_playlist.playlist.title} has been deleted")


@bot.command()
async def play(ctx, *, query: str):
    if not ctx.voice_client or not ctx.voice_client.is_connected():
        await MusicManager.join(ctx)

    async with ctx.typing():
        players = await MusicManager.create_player(query, ctx.author)

    if players:
        if await MusicManager.queue_add(
            players=players, ctx=ctx
        ) and not await MusicManager.play(ctx):
            await ctx.send("Added to queue")

    else:
        await ctx.send("Query not found.")


@bot.command()
async def lyrics(ctx, query: str = None):
    if response := await MusicManager.lyrics(ctx, query):
        title, author, query_lyrics = response

        splitted = query_lyrics.split("\n")
        res = []
        current = ""
        for i, split in enumerate(splitted):
            if len(splitted) <= i + 1 or len(current) + len(splitted[i + 1]) > 1024:
                res.append(current)
                current = ""
                continue
            current += split + "\n"

        page_manager = discordSuperUtils.PageManager(
            ctx,
            [
                discord.Embed(
                    title=f"Lyrics for '{title}' by '{author}', (Page {i + 1}/{len(res)})",
                    description=x,
                )
                for i, x in enumerate(res)
            ],
            public=True,
        )
        await page_manager.run()
    else:
        await ctx.send("No lyrics found.")


@bot.command()
async def pause(ctx):
    if await MusicManager.pause(ctx):
        await ctx.send("Player paused.")


@bot.command()
async def resume(ctx):
    if await MusicManager.resume(ctx):
        await ctx.send("Player resumed.")


@bot.command()
async def volume(ctx, volume: int):
    await MusicManager.volume(ctx, volume)


@bot.command()
async def loop(ctx):
    is_loop = await MusicManager.loop(ctx)

    if is_loop is not None:
        await ctx.send(f"Looping toggled to {is_loop}")


@bot.command()
async def shuffle(ctx):
    is_shuffle = await MusicManager.shuffle(ctx)

    if is_shuffle is not None:
        await ctx.send(f"Shuffle toggled to {is_shuffle}")


@bot.command()
async def autoplay(ctx):
    is_autoplay = await MusicManager.autoplay(ctx)

    if is_autoplay is not None:
        await ctx.send(f"Autoplay toggled to {is_autoplay}")


@bot.command()
async def queueloop(ctx):
    is_loop = await MusicManager.queueloop(ctx)

    if is_loop is not None:
        await ctx.send(f"Queue looping toggled to {is_loop}")


@bot.command()
async def complete_queue(ctx):
    if ctx_queue := await MusicManager.get_queue(ctx):
        formatted_queue = [
            f"Title: '{x.title}'\nRequester: {x.requester and x.requester.mention}\n"
            f"Position: {i - ctx_queue.pos}"
            for i, x in enumerate(ctx_queue.queue)
        ]

        num_of_fields = 25

        embeds = discordSuperUtils.generate_embeds(
            formatted_queue,
            "Complete Song Queue",
            "Shows the complete song queue.",
            num_of_fields,
            string_format="{}",
        )

        page_manager = discordSuperUtils.PageManager(
            ctx, embeds, public=True, index=floor(ctx_queue.pos / 25)
        )
        await page_manager.run()


@bot.command()
async def goto(ctx, position: int):
    if ctx_queue := await MusicManager.get_queue(ctx):
        new_pos = ctx_queue.pos + position
        if not 0 <= new_pos < len(ctx_queue.queue):
            await ctx.send("Position is out of bounds.")
            return

        await MusicManager.goto(ctx, new_pos)
        await ctx.send(f"Moved to position {position}")


@bot.command()
async def history(ctx):
    if ctx_queue := await MusicManager.get_queue(ctx):
        formatted_history = [
            f"Title: '{x.title}'\nRequester: {x.requester and x.requester.mention}"
            for x in ctx_queue.history
        ]

        embeds = discordSuperUtils.generate_embeds(
            formatted_history,
            "Song History",
            "Shows all played songs",
            25,
            string_format="{}",
        )

        page_manager = discordSuperUtils.PageManager(ctx, embeds, public=True)
        await page_manager.run()


@bot.command()
async def skip(ctx, index: int = None):
    await MusicManager.skip(ctx, index)


@bot.command()
async def queue(ctx):
    if ctx_queue := await MusicManager.get_queue(ctx):
        formatted_queue = [
            f"Title: '{x.title}\nRequester: {x.requester and x.requester.mention}"
            for x in ctx_queue.queue[ctx_queue.pos + 1 :]
        ]

        embeds = discordSuperUtils.generate_embeds(
            formatted_queue,
            "Queue",
            f"Now Playing: {await MusicManager.now_playing(ctx)}",
            25,
            string_format="{}",
        )

        page_manager = discordSuperUtils.PageManager(ctx, embeds, public=True)
        await page_manager.run()


@bot.command()
async def rewind(ctx, index: int = None):
    await MusicManager.previous(ctx, index, no_autoplay=True)


@bot.command()
async def ls(ctx):
    if queue := await MusicManager.get_queue(ctx):
        loop = queue.loop
        loop_status = None

        if loop == discordSuperUtils.Loops.LOOP:
            loop_status = "Looping enabled."

        elif loop == discordSuperUtils.Loops.QUEUE_LOOP:
            loop_status = "Queue looping enabled."

        elif loop == discordSuperUtils.Loops.NO_LOOP:
            loop_status = "No loop enabled."

        if loop_status:
            await ctx.send(loop_status)


@bot.command()
async def move(ctx, player_index: int, index: int):
    await MusicManager.move(ctx, player_index, index)


bot.run("token")
```

![MusicManager Example](https://media.giphy.com/media/SF6K0zIVHl6RCQ0Aqk/giphy.gif)

More examples are listed in the examples folder.

Known Issues
--------------

- Removing an animated emoji wont be recognized as a reaction role, as it shows up as not animated for some reason, breaking the reaction matcher. (Discord API Related)

Support
--------------

- **[Support Server](https://discord.gg/zhwcpTBBeC)**
- **[Documentation](https://discord-super-utils.gitbook.io/discord-super-utils/)**


================================================
FILE: discordSuperUtils/__init__.py
================================================
from .antispam import SpamDetectionGenerator, SpamManager
from .ban import BanManager
from .base import CogManager, questionnaire
from .birthday import BirthdayManager
from .commandhinter import CommandHinter, CommandResponseGenerator
from .convertors import TimeConvertor
from .database import DatabaseManager, create_mysql
from .economy import EconomyManager, EconomyAccount
from .fivem import FiveMServer
from .imaging import ImageManager, Backgrounds
from .infractions import InfractionManager
from .invitetracker import InviteTracker
from .kick import KickManager
from .leveling import LevelingManager
from .messagefilter import MessageFilter, MessageResponseGenerator
from .modmail import ModMailManager
from .music import LavalinkMusicManager
from .music.exceptions import *
from .music.lavalink.lavalink import LavalinkMusicManager
from .music.music import *
from .music.player import Player
from .music.enums import *
from .music.lavalink.equalizer import Equalizer
from .music.queue import QueueManager
from .mute import MuteManager, AlreadyMuted
from .paginator import PageManager, generate_embeds, ButtonsPageManager
from .prefix import PrefixManager
from .punishments import Punishment
from .reactionroles import ReactionManager
from .spotify import SpotifyClient
from .template import TemplateManager
from .youtube import YoutubeClient
from .client import DatabaseClient, ExtendedClient, ManagerClient
from .twitch import TwitchManager, get_twitch_oauth_key
from .slash_client import SlashClient

__title__ = "discordSuperUtils"
__version__ = "0.3.0"
__author__ = "Adam7100 & Koyashie07"
__license__ = "MIT"


================================================
FILE: discordSuperUtils/antispam.py
================================================
from __future__ import annotations

from abc import ABC, abstractmethod
from datetime import timedelta
from difflib import SequenceMatcher
from typing import TYPE_CHECKING, List, Union, Any, Iterable

import discord

from .base import EventManager, get_generator_response, CacheBased
from .punishments import get_relevant_punishment

if TYPE_CHECKING:
    from discord.ext import commands
    from .punishments import Punishment

__all__ = ("SpamManager", "SpamDetectionGenerator", "DefaultSpamDetectionGenerator")


class SpamDetectionGenerator(ABC):
    """
    Represents a SpamManager that filters messages to find spam.
    """

    __slots__ = ()

    @abstractmethod
    def generate(self, last_messages: List[discord.Message]) -> Union[bool, Any]:
        """
        This function is an abstract method.
        The generate function of the generator.

        :param last_messages: The last messages sent (5 is max).
        :type last_messages: List[discord.Message]
        :return: A boolean representing if the message is spam.
        :rtype: Union[bool, Any]
        """


class DefaultSpamDetectionGenerator(SpamDetectionGenerator):
    def generate(self, last_messages: List[discord.Message]) -> Union[bool, Any]:
        member = last_messages[0].author
        if member.guild_permissions.administrator:
            return False

        return (
            SpamManager.get_messages_similarity(
                [message.content for message in last_messages]
            )
            > 0.70
        )


class SpamManager(CacheBased, EventManager):
    """
    Represents a SpamManager which detects spam.
    """

    __slots__ = ("bot", "generator", "punishments", "_last_messages")

    def __init__(
        self,
        bot: commands.Bot,
        generator: SpamDetectionGenerator = None,
        wipe_cache_delay: timedelta = timedelta(seconds=5),
    ):
        CacheBased.__init__(self, bot, wipe_cache_delay)
        EventManager.__init__(self)

        self.generator = (
            generator if generator is not None else DefaultSpamDetectionGenerator
        )
        self.punishments = []

        self.bot.add_listener(self.__handle_messages, "on_message")
        self.bot.add_listener(self.__handle_messages, "on_message_edit")

    @staticmethod
    def get_messages_similarity(messages: Iterable[str]) -> float:
        """
        Gets the similarity between messages.

        :param messages: Messages to compare.
        :type messages: Iterable[str]
        :rtype: float
        :return: The similarity between messages (0-1)
        """

        results = []

        for i, message in enumerate(messages):
            for second_index, second_message in enumerate(messages):
                if i != second_index:
                    results.append(
                        SequenceMatcher(None, message, second_message).ratio()
                    )

        return sum(results) / len(results) if results else 0

    def add_punishments(self, punishments: List[Punishment]) -> None:
        self.punishments = punishments

    async def __handle_messages(self, message, edited_message=None):
        message = edited_message or message

        if not message.guild or message.author.bot:
            return

        member_last_messages = list(
            filter(lambda x: x.author == message.author, self.bot.cached_messages)
        )[-5:]

        if len(member_last_messages) <= 3 or not get_generator_response(
            self.generator, SpamDetectionGenerator, member_last_messages
        ):
            return

        # member_warnings are the number of times the member has spammed.
        member_warnings = (
            self._cache.setdefault(message.guild.id, {}).get(message.author.id, 0) + 1
        )
        self._cache[message.guild.id][message.author.id] = member_warnings

        await self.call_event("on_message_spam", member_last_messages, member_warnings)

        if punishment := get_relevant_punishment(self.punishments, member_warnings):
            await punishment.punishment_manager.punish(
                message, message.author, punishment
            )


================================================
FILE: discordSuperUtils/ban.py
================================================
from __future__ import annotations

import asyncio
from datetime import datetime
from typing import TYPE_CHECKING, Union, Optional, List, Dict, Any

import discord

from .base import DatabaseChecker
from .punishments import Punisher

if TYPE_CHECKING:
    from .punishments import Punishment
    from discord.ext import commands


__all__ = ("UnbanFailure", "BanManager")


class UnbanFailure(Exception):
    """Raises an exception when the user tries to unban a discord.User without passing the guild."""


class BanManager(DatabaseChecker, Punisher):
    """
    A BanManager that manages guild bans.
    """

    __slots__ = ("bot",)

    def __init__(self, bot: commands.Bot):
        super().__init__(
            [
                {
                    "guild": "snowflake",
                    "member": "snowflake",
                    "reason": "string",
                    "timestamp": "snowflake",
                }
            ],
            ["bans"],
        )
        self.bot = bot

        self.add_event(self._on_database_connect, "on_database_connect")

    async def _on_database_connect(self):
        self.bot.loop.create_task(self.__check_bans())

    @DatabaseChecker.uses_database
    async def get_banned_members(self) -> List[Dict[str, Any]]:
        """
        |coro|

        This function returns all the members that are supposed to be unbanned but are banned.

        :return: The list of unbanned members.
        :rtype: List[Dict[str, Any]]
        """

        return [
            x
            for x in await self.database.select(self.tables["bans"], [], fetchall=True)
            if x["timestamp"] <= datetime.utcnow().timestamp()
        ]

    async def __check_bans(self) -> None:
        """
        |coro|

        A loop that ensures that members are unbanned when they need to.

        :return: None
        :rtype: None
        """

        await self.bot.wait_until_ready()

        while not self.bot.is_closed():
            for banned_member in await self.get_banned_members():
                guild = self.bot.get_guild(banned_member["guild"])

                if guild is None:
                    continue

                user = await self.bot.fetch_user(banned_member["member"])

                if await self.unban(user, guild):
                    await self.call_event("on_unban", user, banned_member["reason"])

            await asyncio.sleep(300)

    async def punish(
        self, ctx: commands.Context, member: discord.Member, punishment: Punishment
    ) -> None:
        try:
            self.bot.loop.create_task(
                self.ban(
                    member,
                    punishment.punishment_reason,
                    punishment.punishment_time.total_seconds(),
                )
            )
        except discord.errors.Forbidden as e:
            raise e
        else:
            await self.call_event("on_punishment", ctx, member, punishment)

    @staticmethod
    async def get_ban(
        member: Union[discord.Member, discord.User], guild: discord.Guild
    ) -> Optional[discord.User]:
        """
        |coro|

        This function returns the user object of the member if he is banned from the guild.

        :param member: The banned member.
        :type member: discord.Member
        :param guild: The guild.
        :type guild: discord.Guild
        :return: The user object if found.
        :rtype: Optional[discord.User]
        """

        banned = await guild.bans()
        for x in banned:
            if x.user.id == member.id:
                return x.user

    @DatabaseChecker.uses_database
    async def unban(
        self, member: Union[discord.Member, discord.User], guild: discord.Guild = None
    ) -> bool:
        """
        |coro|

        Unbans the member from the guild.

        :param Union[discord.Member, discord.User] member: The member or user to unban.
        :param discord.Guild guild: The guild to unban the member from.
        :return: A bool representing if the unban was successful.
        :rtype: bool
        :raises: UnbanFailure: Cannot unban a discord.User without a guild.
        """

        if isinstance(member, discord.User) and not guild:
            raise UnbanFailure("Cannot unban a discord.User without a guild.")

        guild = guild if guild is not None else member.guild
        await self.database.delete(
            self.tables["bans"], {"guild": guild.id, "member": member.id}
        )

        if user := await self.get_ban(member, guild):
            await guild.unban(user)
            return True

    async def __handle_unban(
        self, time_of_ban: Union[int, float], member: discord.Member, reason: str
    ) -> None:
        """
        |coro|

        A function that handles the member's unban that runs separately from the ban method so it wont be blocked.

        :param Union[int, float] time_of_ban: The time until the member's unban timestamp.
        :param discord.Member member: The member to unban.
        :param str reason: The reason of the mute.
        :return: None
        :rtype: None
        """

        await asyncio.sleep(time_of_ban)

        if await self.unban(member):
            await self.call_event("on_unban", member, reason)

    @DatabaseChecker.uses_database
    async def ban(
        self,
        member: discord.Member,
        reason: str = "No reason provided.",
        time_of_ban: Union[int, float] = 0,
    ) -> None:
        """
        |coro|

        Bans the member from the guild.

        :param member: The member to ban.
        :type member: discord.Member
        :param reason: The reason of the ban.
        :type reason: str
        :param time_of_ban: The time of ban.
        :type time_of_ban: Union[int, float]
        :return: None
        :rtype: None
        """

        await member.ban(reason=reason)

        if time_of_ban <= 0:
            return

        await self.database.insert(
            self.tables["bans"],
            {
                "guild": member.guild.id,
                "member": member.id,
                "reason": reason,
                "timestamp": datetime.utcnow().timestamp() + time_of_ban,
            },
        )

        self.bot.loop.create_task(self.__handle_unban(time_of_ban, member, reason))


================================================
FILE: discordSuperUtils/base.py
================================================
from __future__ import annotations

import asyncio
import dataclasses
import inspect
import logging
from dataclasses import dataclass
from typing import (
    List,
    Any,
    Iterable,
    Optional,
    TYPE_CHECKING,
    Union,
    Tuple,
    Callable,
    Dict,
    Coroutine,
)

import aiomysql

try:
    import aiopg
except ImportError:
    aiopg = None
    logging.warning(
        "Aiopg is not installed correctly, postgres databases are not supported."
    )

import aiosqlite
import discord
from motor import motor_asyncio

if TYPE_CHECKING:
    from discord.ext import commands
    from .database import Database
    from datetime import timedelta


__all__ = (
    "COLUMN_TYPES",
    "DatabaseNotConnected",
    "InvalidGenerator",
    "get_generator_response",
    "maybe_coroutine",
    "generate_column_types",
    "questionnaire",
    "EventManager",
    "create_task",
    "CogManager",
    "DatabaseChecker",
    "CacheBased",
)


COLUMN_TYPES = {
    motor_asyncio.AsyncIOMotorDatabase: None,  # mongo does not require any columns
    aiosqlite.core.Connection: {
        "snowflake": "INTEGER",
        "string": "TEXT",
        "number": "INTEGER",
        "smallnumber": "INTEGER",
    },
    aiomysql.pool.Pool: {
        "snowflake": "BIGINT",
        "string": "TEXT",
        "number": "INT",
        "smallnumber": "SMALLINT",
    },
}

if aiopg:
    COLUMN_TYPES[aiopg.pool.Pool] = {
        "snowflake": "BIGINT",
        "string": "TEXT",
        "number": "INT",
        "smallnumber": "SMALLINT",
    }


class DatabaseNotConnected(Exception):
    """Raises an error when the user tries to use a method of a manager without a database connected to it."""


@dataclass
class CacheBased:
    """
    Represents a cache manager that manages member cache.
    """

    bot: commands.Bot
    wipe_cache_delay: timedelta
    _cache: dict = dataclasses.field(default_factory=dict, init=False, repr=False)

    def __post_init__(self):
        asyncio.get_event_loop().create_task(self.__wipe_cache())

    async def __wipe_cache(self) -> None:
        """
        |coro|

        This function is responsible for wiping the member cache.

        :return: None
        :rtype: None
        """

        while not self.bot.is_closed():
            await asyncio.sleep(self.wipe_cache_delay.total_seconds())

            self._cache = {}


class InvalidGenerator(Exception):
    """
    Raises an exception when the user passes an invalid generator.
    """

    __slots__ = ("generator",)

    def __init__(self, generator):
        self.generator = generator
        super().__init__(
            f"Generator of type {type(self.generator)!r} is not supported."
        )


async def maybe_coroutine(function: Callable, *args, **kwargs) -> Any:
    """
    |coro|

    Returns the return value of the function.

    :param Callable function: The function to call.
    :param args: The arguments.
    :param kwargs: The key arguments:
    :return: The value.
    :rtype: Any
    """

    value = function(*args, **kwargs)

    if inspect.isawaitable(value):
        return await value

    return value


def get_generator_response(generator: Any, generator_type: Any, *args, **kwargs) -> Any:
    """
    Returns the generator response with the arguments.

    :param generator: The generator to get the response from.
    :type generator: Any
    :param generator_type: The generator type. (Should be same as the generator type.
    :type generator_type: Any
    :param args: The arguments of the generator.
    :param kwargs: The key arguments of the generator
    :return: The generator response.
    :rtype: Any
    """

    if inspect.isclass(generator) and issubclass(generator, generator_type):
        if inspect.ismethod(generator.generate):
            return generator.generate(*args, **kwargs)

        return generator().generate(*args, **kwargs)

    if isinstance(generator, generator_type):
        return generator.generate(*args, **kwargs)

    raise InvalidGenerator(generator)


def generate_column_types(
    types: Iterable[str], database_type: Any
) -> Optional[List[str]]:
    """
    Generates the column type names that are suitable for the database type.

    :param types: The column types.
    :type types: Iterable[str]
    :param database_type: The database type.
    :type database_type: Any
    :return: The suitable column types for the database types.
    :rtype: Optional[List[str]]
    """

    database_type_configuration = COLUMN_TYPES.get(database_type)

    if database_type_configuration is None:
        return

    return [database_type_configuration[x] for x in types]


async def questionnaire(
    ctx: commands.Context,
    questions: Iterable[Union[str, discord.Embed]],
    public: bool = False,
    timeout: Union[float, int] = 30,
    member: discord.Member = None,
) -> Tuple[List[str], bool]:
    """
    |coro|

    Questions the member using a "quiz" and returns the answers.
    The questionnaire can be used without a specific member and be public.
    If no member was passed and the questionnaire public argument is true, a ValueError will be raised.

    :raises: ValueError: The questionnaire is private and no member was provided.
    :param ctx: The context (where the questionnaire will ask the questions).
    :type ctx: commands.Context
    :param questions: The questions the questionnaire will ask.
    :type questions: Iterable[Union[str, discord.Embed]]
    :param public: A bool indicating if the questionnaire is public.
    :type public: bool
    :param timeout: The number of seconds until the questionnaire will stop and time out.
    :type timeout: Union[float, int]
    :param member: The member the questionnaire will get the answers from.
    :type member: discord.Member
    :return: The answers and a boolean indicating if the questionnaire timed out.
    :rtype: Tuple[List[str], bool]
    """

    answers = []
    timed_out = False

    if not public and not member:
        raise ValueError("The questionnaire is private and no member was provided.")

    def checks(msg):
        return (
            msg.channel == ctx.channel
            if public
            else msg.channel == ctx.channel and msg.author == member
        )

    for question in questions:
        if isinstance(question, str):
            await ctx.send(question)
        elif isinstance(question, discord.Embed):
            await ctx.send(embed=question)
        else:
            raise TypeError("Question must be of type 'str' or 'discord.Embed'.")

        try:
            message = await ctx.bot.wait_for("message", check=checks, timeout=timeout)
        except asyncio.TimeoutError:
            timed_out = True
            break

        answers.append(message.content)

    return answers, timed_out


@dataclass
class EventManager:
    """
    An event manager that manages events for managers.
    """

    events: dict = dataclasses.field(default_factory=dict, init=False)

    async def call_event(self, name: str, *args, **kwargs) -> None:
        """
        Calls the event name with the arguments

        :param name: The event name.
        :type name: str
        :param args: The arguments.
        :param kwargs: The key arguments.
        :return: None
        :rtype: None
        """

        if name in self.events:
            for event in self.events[name]:
                await event(*args, **kwargs)

    def event(self, name: str = None) -> Callable:
        """
        A decorator which adds an event listener.

        :param name: The event name.
        :type name: str
        :return: The inner function.
        :rtype: Callable
        """

        def inner(func):
            self.add_event(func, name)
            return func

        return inner

    def add_event(self, func: Callable, name: str = None) -> None:
        """
        Adds an event to the event dictionary.

        :param func: The event callback.
        :type func: Callable
        :param name: The event name.
        :type name: str
        :return: None
        :rtype: None
        :raises: TypeError: The listener isn't async.
        """

        name = func.__name__ if not name else name

        if not asyncio.iscoroutinefunction(func):
            raise TypeError("Listeners must be async.")

        if name in self.events:
            self.events[name].append(func)
        else:
            self.events[name] = [func]

    def remove_event(self, func: Callable, name: str = None) -> None:
        """
        Removes an event from the event dictionary.

        :param func: The event callback.
        :type func: Callable
        :param name: The event name.
        :type name: str
        :return: None
        :rtype: None
        """

        name = func.__name__ if not name else name

        if name in self.events:
            self.events[name].remove(func)


def handle_task_exceptions(task: asyncio.Task) -> None:
    """
    Handles the task's exceptions.

    :param asyncio.Task task: The task.
    :return: None
    :rtype: None
    """

    try:
        task.result()
    except asyncio.CancelledError:
        pass
    except Exception as e:
        raise e


def create_task(loop: asyncio.AbstractEventLoop, coroutine: Coroutine) -> None:
    """
    Creates a task and handles exceptions.

    :param asyncio.AbstractEventLoop loop: The loop to run the coroutine on.
    :param Coroutine coroutine: The coroutine.
    :return: None
    :rtype: None
    """

    try:
        task = loop.create_task(coroutine)
        task.add_done_callback(handle_task_exceptions)
    except RuntimeError:
        pass


class CogManager:
    """
    A CogManager which helps the user use the managers inside discord cogs.
    """

    class Cog:
        """
        The internal Cog class.
        """

        def __init__(self, managers: List = None):
            listeners = {}
            managers = [] if managers is None else managers

            attribute_objects = [getattr(self, attr) for attr in dir(self)]

            for attr in attribute_objects:
                listener_type = getattr(attr, "_listener_type", None)
                if listener_type:
                    if listener_type in listeners:
                        listeners[listener_type].append(attr)
                    else:
                        listeners[listener_type] = [attr]

            managers = managers or [
                attr for attr in attribute_objects if type(attr) in listeners
            ]
            for event_type in listeners:
                for manager in managers:
                    for event in listeners[event_type]:
                        manager.add_event(event)

    @staticmethod
    def event(manager_type: Any) -> Callable:
        """
        Adds an event to the Cog event list.

        :param manager_type: The manager type of the event.
        :type manager_type: Any
        :rtype: Callable
        :return: The inner function.
        :raises: TypeError: The listener isn't async.
        """

        def decorator(func):
            if not inspect.iscoroutinefunction(func):
                raise TypeError("Listeners must be async.")

            func._listener_type = manager_type

            return func

        return decorator


@dataclass
class DatabaseChecker(EventManager):
    """
    A database checker which makes sure the database is connected to a manager and handles the table creation.
    """

    tables_column_data: List[Dict[str, str]]
    table_identifiers: List[str]
    database: Optional[Database] = dataclasses.field(default=None, init=False)
    tables: Dict[str, str] = dataclasses.field(default_factory=dict, init=False)

    @staticmethod
    def uses_database(func):
        def inner(self, *args, **kwargs):
            self._check_database()
            return func(self, *args, **kwargs)

        return inner

    def _check_database(self, raise_error: bool = True) -> bool:
        """
        A function which checks if the database is connected.

        :param raise_error: A bool indicating if the function should raise an error if the database is not connected.
        :type raise_error: bool
        :rtype: bool
        :return: If the database is connected.
        :raises: DatabaseNotConnected: The database is not connected.
        """

        if not self.database:
            if raise_error:
                raise DatabaseNotConnected(
                    f"Database not connected."
                    f" Connect this manager to a database using 'connect_to_database'"
                )

            return False

        return True

    async def connect_to_database(
        self, database: Database, tables: List[str] = None
    ) -> None:
        """
        Connects to the database.
        Calls on_database_connect when connected.

        :param database: The database to connect to.
        :type database: Database
        :param tables: The tables to create (incase they do not exist).
        :type tables: List[str]
        :rtype: None
        :return: None
        """

        if not tables or len(tables) != len(self.table_identifiers):
            tables = self.table_identifiers

        for table, table_data, identifier in zip(
            tables, self.tables_column_data, self.table_identifiers
        ):
            types = generate_column_types(table_data.values(), type(database.database))

            await database.create_table(
                table, dict(zip(list(table_data), types)) if types else None, True
            )

            self.database = database
            self.tables[identifier] = table

        await self.call_event("on_database_connect")


================================================
FILE: discordSuperUtils/birthday.py
================================================
from __future__ import annotations

import asyncio
from dataclasses import dataclass
from datetime import datetime, timedelta, tzinfo
from typing import Dict, List, Optional, Any

import discord
import pytz
from discord.ext import commands

from .base import DatabaseChecker


__all__ = ("PartialBirthdayMember", "BirthdayManager", "BirthdayMember")


@dataclass
class PartialBirthdayMember:
    """
    Represents a partial birthday member.
    """

    member: discord.Member
    birthday_date: datetime
    timezone: tzinfo


@dataclass
class BirthdayMember:
    """
    Represents a birthday member.
    """

    birthday_manager: BirthdayManager
    member: discord.Member

    def __post_init__(self):
        self.table = self.birthday_manager.tables["birthdays"]

    def __hash__(self):
        return id(self)

    @property
    def __checks(self) -> Dict[str, int]:
        return {"guild": self.member.guild.id, "member": self.member.id}

    async def birthday_date(self) -> datetime:
        """
        |coro|

        Returns the birthday date in UTC of the member.

        :return: The birthday.
        :rtype: datetime
        """

        birthday_data = await self.birthday_manager.database.select(
            self.table, ["utc_birthday"], self.__checks
        )
        return datetime.utcfromtimestamp(
            birthday_data["utc_birthday"]
        )

    async def next_birthday(self) -> datetime:
        """
        |coro|

        Returns the next birthday of the member.

        :return: The next birthday of the member.
        :rtype: datetime
        """

        current_datetime = datetime.now(await self.timezone())

        new_date = (await self.birthday_date()).replace(year=current_datetime.year)
        if new_date.timestamp() - current_datetime.timestamp() < 0:
            new_date = new_date.replace(year=current_datetime.year + 1)

        return new_date

    async def timezone(self) -> tzinfo:
        """
        |coro|

        Returns the timezone.

        :return: The timezone.
        :rtype: str
        """

        timezone_data = await self.birthday_manager.database.select(
            self.table, ["timezone"], self.__checks
        )
        return pytz.timezone(timezone_data["timezone"])

    async def delete(self) -> PartialBirthdayMember:
        """
        |coro|

        Deletes the birthday and returns a PartialBirthdayMember.

        :return: The partial birthday.
        :rtype: PartialBirthdayMember
        """

        partial = PartialBirthdayMember(
            self.member, await self.birthday_date(), await self.timezone()
        )
        await self.birthday_manager.database.delete(self.table, self.__checks)
        return partial

    async def set_birthday_date(self, timestamp: float) -> None:
        """
        |coro|

        Sets the birthday date.

        :param float timestamp: The timestamp.
        :return: None
        :rtype: None
        """

        await self.birthday_manager.database.update(
            self.table, {"utc_birthday": timestamp}, self.__checks
        )

    async def set_timezone(self, timezone: str) -> None:
        """
        |coro|

        Sets the member's timezone.

        :param str timezone: The timezone.
        :return: None
        :rtype: None
        """

        await self.birthday_manager.database.update(
            self.table, {"timezone": timezone}, self.__checks
        )

    async def age(self) -> int:
        """
        |coro|

        Returns the current age of the member.

        :return: The member age.
        :rtype: int
        """

        born = await self.birthday_date()
        today = datetime.now(await self.timezone())

        return (
            today.year - born.year - ((today.month, today.day) < (born.month, born.day))
        )


class BirthdayManager(DatabaseChecker):
    """
    Represents a birthday manager.
    """

    def __init__(self, bot: commands.Bot):
        """
        :param commands.Bot bot: The bot.
        """

        super().__init__(
            [
                {
                    "guild": "snowflake",
                    "member": "snowflake",
                    "utc_birthday": "snowflake",
                    "timezone": "string",
                }
            ],
            ["birthdays"],
        )
        self.bot = bot
        self.add_event(self._on_database_connect, "on_database_connect")

    async def _on_database_connect(self):
        self.bot.loop.create_task(self.__detect_birthdays())

    @DatabaseChecker.uses_database
    async def create_birthday(
        self, member: discord.Member, member_birthday: float, timezone: str = "UTC"
    ) -> None:
        """
        |coro|

        Makes a birthday for the member.

        :param discord.Member member: The member.
        :param float member_birthday: The member birthday timestamp in UTC.
        :param str timezone: The timezone.
        :return: None
        :rtype: None
        """

        await self.database.insertifnotexists(
            self.tables["birthdays"],
            dict(
                zip(
                    self.tables_column_data[0],
                    [member.guild.id, member.id, member_birthday, timezone],
                )
            ),
            {"guild": member.guild.id, "member": member.id},
        )

    @DatabaseChecker.uses_database
    async def get_birthday(self, member: discord.Member) -> Optional[BirthdayMember]:
        """
        |coro|

        Returns the BirthdayMember object of the member.

        :param discord.Member member: The member.
        :return: The BirthdayMember object if applicable.
        :rtype: Optional[BirthdayMember]
        """

        member_data = await self.database.select(
            self.tables["birthdays"],
            [],
            {"guild": member.guild.id, "member": member.id},
            True,
        )

        if member_data:
            return BirthdayMember(self, member)

        return None

    @DatabaseChecker.uses_database
    async def get_upcoming(self, guild: discord.Guild) -> List[BirthdayMember]:
        """
        |coro|

        Returns the upcoming birthdays in the guild.

        :param discord.Guild guild: The guild.
        :return: The birthdays, sorted by their nearest birthday date.
        :rtype: List[BirthdayMember]
        """

        member_data = await self.database.select(
            self.tables["birthdays"], [], {"guild": guild.id}, fetchall=True
        )

        birthdays = {}

        for birthday_member in member_data:
            member = guild.get_member(birthday_member["member"])

            if member:
                birthday = BirthdayMember(self, member)
                birthdays[birthday] = await birthday.next_birthday()
                # Did this instead of running the method until complete in the sorted key lambda

        return sorted(birthdays, key=lambda x: birthdays[x])

    @staticmethod
    def get_midnight_timezones() -> List[str]:
        """
        This method returns a list of timezones where the current time is 12 am.

        :return: The list of timezones.
        :rtype: List[str]
        """

        current_utc_time = datetime.utcnow()
        utc_offset = -(current_utc_time.hour % 24)

        minutes = 30 if current_utc_time.minute > 5 else 0
        if minutes == 30:
            utc_offset -= 1

        checks = (
            timedelta(hours=utc_offset, minutes=minutes),
            timedelta(
                hours=24 - -utc_offset if current_utc_time.hour != 0 else 0,
                minutes=minutes,
            ),
        )

        return [
            tz.zone
            for tz in map(pytz.timezone, pytz.all_timezones_set)
            if current_utc_time.astimezone(tz).utcoffset() in checks
        ]

    @DatabaseChecker.uses_database
    async def get_members_with_birthday(
        self, timezones: List[str]
    ) -> List[Dict[str, Any]]:
        """
        |coro|

        This function receives a list of timezones and returns a list of members that have birthdays in that date
        and timezone.

        :param List[str] timezones: The timezones.
        :return: Returns the members that have a birthday.
        :rtype: List[Dict[str, Any]]
        """

        result_members = []
        registered_members = await self.database.select(
            self.tables["birthdays"], [], fetchall=True
        )

        birthday_members = [x for x in registered_members if x["timezone"] in timezones]
        for birthday_member in birthday_members:
            timezone_time = datetime.now(pytz.timezone(birthday_member["timezone"]))
            date_of_birth = datetime.fromtimestamp(birthday_member["utc_birthday"])

            if (
                date_of_birth.month == timezone_time.month
                and date_of_birth.day == timezone_time.day
            ):
                result_members.append(birthday_member)

        return result_members

    @staticmethod
    def round_to_nearest(timedelta_to_round: timedelta) -> float:
        """
        This function receives a timedelta to round to and gets the amount of seconds before that timestamp.

        :param timedelta timedelta_to_round: The timedelta to round to.
        :return: The seconds until the nearest rounded timedelta.
        :rtype: float
        """

        now = datetime.now()
        nearest = now + (datetime.min - now) % timedelta_to_round
        return nearest.timestamp() - now.timestamp()

    async def __detect_birthdays(self) -> None:
        await self.bot.wait_until_ready()

        while not self.bot.is_closed():
            await asyncio.sleep(self.round_to_nearest(timedelta(minutes=30)))

            for birthday_member in await self.get_members_with_birthday(
                self.get_midnight_timezones()
            ):
                guild = self.bot.get_guild(birthday_member["guild"])

                if guild:
                    member = guild.get_member(birthday_member["member"])

                    if member:
                        await self.call_event(
                            "on_member_birthday", BirthdayMember(self, member)
                        )


================================================
FILE: discordSuperUtils/client.py
================================================
from __future__ import annotations

import asyncio
import logging
import os
import time
from typing import Optional, TYPE_CHECKING, List, Tuple

from discord.ext import commands

if TYPE_CHECKING:
    from .base import DatabaseChecker
    from .database import Database

__all__ = ("ExtendedClient", "DatabaseClient", "ManagerClient")


class ExtendedClient(commands.Bot):
    """
    Represents an extended commands,Bot client.
    Adds a token attribute, replaces methods, loads cogs, etc.
    """

    __slots__ = ("token", "start_time")

    def __init__(self, token: str, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.token = token
        self.start_time = time.time()

    def load_cogs(self, directory: str, ignore_prefix: str = "__") -> None:
        """
        Loads all the cog extensions in the directory.

        :param str ignore_prefix: The prefix to ignore, files that start with that prefix will not be loaded.
        :param str directory: The directory.
        :return: None
        :rtype: None
        """

        extension_directory = directory.replace("/", ".")
        if extension_directory:
            extension_directory += "."

        working_directory = os.getcwd()

        slash = "/" if "/" in working_directory else "\\"

        for file in os.listdir(working_directory + f"{slash}{directory}"):
            if not file.endswith(".py") or file.startswith(ignore_prefix):
                continue

            try:
                self.load_extension(f"{extension_directory}{file.replace('.py', '')}")
                logging.info(f"Loaded cog {file}")
            except Exception as e:
                logging.error(f"Failed to load cog {file}")
                raise e

    def run(self, cogs_directory: Optional[str] = "cogs") -> None:
        """
        Runs the bot and loads the cogs automatically.

        :param Optional[str] cogs_directory: The directory to load the cogs from.
        :return: None
        :rtype: None
        """

        if cogs_directory is not None:  # Might be an empty string.
            self.load_cogs(cogs_directory)

        super().run(self.token)


class DatabaseClient(ExtendedClient):
    """
    Represents an extended client that has a database.
    """

    __slots__ = ("database",)

    def __init__(self, token: str, *args, **kwargs):
        super().__init__(token, *args, **kwargs)
        self.database: Optional[Database] = None

    async def wait_until_database_connection(self) -> None:
        """
        Waits until the database is connected.

        :return: None
        :rtype: None
        """

        while not self.database:
            await asyncio.sleep(0.1)


class ManagerClient(DatabaseClient):
    """
    Represents an extended database client that has managers.
    """

    __slots__ = ("managers",)

    def __init__(self, token: str, *args, **kwargs):
        super().__init__(token, *args, **kwargs)
        self.managers: List[Tuple[DatabaseChecker, Optional[List[str]]]] = []

        self.add_listener(self.on_ready)  # Doesnt do this automatically.

    def add_manager(self, manager: DatabaseChecker, tables: List[str] = None) -> None:
        self.managers.append((manager, tables))

    async def on_ready(self):
        await self.wait_until_database_connection()

        for manager, tables in self.managers:
            await manager.connect_to_database(self.database, tables or [])


================================================
FILE: discordSuperUtils/commandhinter.py
================================================
from abc import ABC, abstractmethod
from difflib import SequenceMatcher
from typing import List, Union, Optional

import discord
from discord.ext import commands

from .base import get_generator_response

__all__ = ("CommandResponseGenerator", "DefaultResponseGenerator", "CommandHinter")


class CommandResponseGenerator(ABC):
    """
    Represents the default abstract CommandResponseGenerator.
    """

    __slots__ = ()

    @abstractmethod
    def generate(
        self, invalid_command: str, suggestions: List[str]
    ) -> Optional[Union[str, discord.Embed]]:
        """
        The generate method of the generator.

        :param str invalid_command: The invalid command.
        :param List[str] suggestions: The list of suggestions.
        :return: The generator response.
        :rtype: Optional[Union[str, discord.Embed]]
        """


class DefaultResponseGenerator(CommandResponseGenerator):
    __slots__ = ()

    def generate(self, invalid_command: str, suggestions: List[str]) -> discord.Embed:
        """
        The default generate method of the generator.

        :param str invalid_command: The invalid command.
        :param List[str] suggestions: The list of suggestions.
        :return: The generator response.
        :rtype: discord.Embed
        """

        embed = discord.Embed(
            title="Invalid command!",
            description=f"**`{invalid_command}`** is invalid. Did you mean:",
            color=0x00FF00,
        )

        for index, suggestion in enumerate(suggestions[:3]):
            embed.add_field(
                name=f"**{index + 1}.**", value=f"**`{suggestion}`**", inline=False
            )

        return embed


class CommandHinter:
    """
    Represents a command hinter.
    """

    __slots__ = ("bot", "generator")

    def __init__(
        self, bot: commands.Bot, generator: Optional[CommandResponseGenerator] = None
    ):
        """
        :param commands.Bot bot: The bot.
        :param Optional[CommandResponseGenerator] generator: The command response generator.
        """

        self.bot = bot
        self.generator = DefaultResponseGenerator if generator is None else generator

        self.bot.add_listener(self.__handle_hinter, "on_command_error")

    @property
    def command_names(self) -> List[str]:
        """
        Returns the command names of all commands of the bot.

        :return: The command names.
        :rtype: List[str]
        """

        names = []

        for command in self.bot.commands:
            if isinstance(command, commands.Group):
                names += [command.name] + list(command.aliases)
                for inner_command in command.commands:
                    names += [inner_command.name] + list(inner_command.aliases)

            else:
                names += [command.name] + list(command.aliases)

        return names

    async def __handle_hinter(self, ctx: commands.Context, error) -> None:
        if isinstance(error, commands.CommandNotFound):
            command_similarity = {}
            command_used = ctx.message.content.lstrip(ctx.prefix)[
                : max([len(c) for c in self.command_names])
            ]

            for command in self.command_names:
                command_similarity[
                    SequenceMatcher(None, command, command_used).ratio()
                ] = command

            generated_message = get_generator_response(
                self.generator,
                CommandResponseGenerator,
                command_used,
                [x[1] for x in sorted(command_similarity.items(), reverse=True)],
            )

            if not generated_message:
                return

            if isinstance(generated_message, discord.Embed):
                await ctx.send(embed=generated_message)
            elif isinstance(generated_message, str):
                await ctx.send(generated_message)
            else:
                raise TypeError(
                    "The generated message must be of type 'discord.Embed' or 'str'."
                )


================================================
FILE: discordSuperUtils/convertors.py
================================================
from typing import Optional, Union

from discord.ext import commands


def isfloat(string: str) -> bool:
    """
    This function receives a string and returns if it is a float or not.

    :param str string: The string to check.
    :return: A boolean representing if the string is a float.
    :rtype: bool
    """

    try:
        float(string)
        return True
    except (ValueError, TypeError):
        return False


class TimeConvertor(commands.Converter):
    """
    Converts a given argument to an int that represents time in seconds.

    Examples
    ----------
        7d: 604800 (7 days in seconds)

        1m: 60 (1 minute in seconds)

        heyh: BadArgument ('hey' is not an int)

        100j: BadArgument ('j' is not a valid time multiplier)
    """

    async def convert(
        self, ctx: commands.Context, argument: str
    ) -> Optional[Union[int, float]]:
        time_multipliers = {
            "s": 1,
            "m": 60,
            "h": 60 * 60,
            "d": 60 * 60 * 24,
            "w": 60 * 60 * 24 * 7,
        }

        permanent = ["permanent", "perm", "0"]
        if argument.lower() in permanent:
            return 0

        if not isfloat(argument[:-1]) or argument[-1] not in time_multipliers.keys():
            raise commands.BadArgument(
                f"Invalid time argument provided, cannot convert '{argument}' to time."
            )

        return float(argument[:-1]) * time_multipliers[argument[-1]]


================================================
FILE: discordSuperUtils/database.py
================================================
import asyncio
import sys
from abc import ABC, abstractmethod
from typing import Dict, Any, Optional, List, Union

import aiomysql

try:
    import aiopg
except ImportError:
    aiopg = None

import aiosqlite
from motor import motor_asyncio

if sys.version_info >= (3, 8) and sys.platform.lower().startswith("win"):
    # Aiopg requires the event loop policy to be WindowsSelectorEventLoop, if it is not, aiopg raises an error.

    asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())


async def create_mysql(
    host: str, port: int, user: str, password: str, dbname: str
) -> aiomysql.pool.Pool:
    """
    |coro|

    Returns a mysql connection pool.

    :param str host: The host address.
    :param int port: The port.
    :param str user: The user.
    :param str password: The password.
    :param str dbname: The dbname.
    :return: The pool
    :rtype: aiomysql.pool.Pool
    """

    # Created this function to make sure the user has autocommit enabled.
    # we must make sure autocommit is enabled because manual commits are not working on aiomysql :)
    return await aiomysql.create_pool(
        host=host, port=port, user=user, password=password, db=dbname, autocommit=True
    )


class UnsupportedDatabase(Exception):
    """Raises error when the user tries to use an unsupported database."""


class Database(ABC):
    __slots__ = ("database",)

    def __init__(self, database):
        self.database = database

    @abstractmethod
    async def close(self):
        pass

    @abstractmethod
    async def insertifnotexists(
        self, table_name: str, data: Dict[str, Any], checks: Dict[str, Any]
    ):
        pass

    @abstractmethod
    async def insert(self, table_name: str, data: Dict[str, Any]):
        pass

    @abstractmethod
    async def create_table(
        self,
        table_name: str,
        columns: Optional[Dict[str, str]] = None,
        exists: Optional[bool] = False,
    ):
        pass

    @abstractmethod
    async def update(
        self, table_name: str, data: Dict[str, Any], checks: Dict[str, Any]
    ):
        pass

    @abstractmethod
    async def updateorinsert(
        self,
        table_name: str,
        data: Dict[str, Any],
        checks: Dict[str, Any],
        insert_data: Dict[str, Any],
    ):
        pass

    @abstractmethod
    async def delete(self, table_name: str, checks: Dict[str, Any]):
        pass

    @abstractmethod
    async def select(
        self,
        table_name: str,
        keys: List[str],
        checks: Optional[Dict[str, Any]] = None,
        fetchall: Optional[bool] = False,
    ):
        pass

    @abstractmethod
    async def execute(
        self, sql_query: str, values: List[Any], fetchall: bool = True
    ) -> Union[List[Dict[str, Any]], Dict[str, Any]]:
        pass


class _MongoDatabase(Database):
    def __str__(self):
        return f"<{self.__class__.__name__} '{self.name}'>"

    @property
    def name(self):
        return self.database.name

    async def close(self):
        self.database.client.close()

    async def insertifnotexists(self, table_name, data, checks):
        response = await self.select(table_name, [], checks, True)

        if not response:
            return await self.insert(table_name, data)

    async def insert(self, table_name, data):
        return await self.database[table_name].insert_one(data)

    async def create_table(self, table_name, _=None, exists=False):
        # create_table has an unused positional parameter to make the methods consistent between database types.

        if exists and table_name in await self.database.list_collection_names():
            return

        return await self.database.create_collection(table_name)

    async def update(self, table_name, data, checks):
        return await self.database[table_name].update_one(checks, {"$set": data})

    async def updateorinsert(self, table_name, data, checks, insert_data):
        response = await self.select(table_name, [], checks, True)

        if len(response) == 1:
            return await self.update(table_name, data, checks)

        return await self.insert(table_name, insert_data)

    async def delete(self, table_name, checks=None):
        return await self.database[table_name].delete_one(
            {} if checks is None else checks
        )

    async def select(self, table_name, keys, checks=None, fetchall=False):
        checks = {} if checks is None else checks

        if fetchall:
            fetch = self.database[table_name].find(checks)
            result = []

            async for doc in fetch:
                current_doc = {}

                for key, value in doc.items():
                    if not keys or key in keys:
                        current_doc[key] = value

                result.append(current_doc)
        else:
            fetch = await self.database[table_name].find_one(checks)
            result = {}

            if fetch is not None:
                for key, value in fetch.items():
                    if not keys or key in keys:
                        result[key] = value
            else:
                result = None

        return result

    async def execute(
        self, sql_query: str, values: List[Any], fetchall: bool = True
    ) -> Union[List[Dict[str, Any]], Dict[str, Any]]:
        raise NotImplementedError("NoSQL databases cannot execute sql queries.")


class _SqlDatabase(Database):
    def __str__(self):
        return f"<{self.__class__.__name__}>"

    def with_commit(func):
        async def inner(self, *args, **kwargs):
            resp = await func(self, *args, **kwargs)
            if self.commit_needed:
                await self.commit()

            return resp

        return inner

    def with_cursor(func):
        async def inner(self, *args, **kwargs):
            database = await self.database.acquire() if self.pool else self.database

            if self.cursor_context:
                async with database.cursor() as cursor:
                    resp = await func(self, cursor, *args, **kwargs)

            else:
                cursor = await database.cursor()
                resp = await func(self, cursor, *args, **kwargs)
                await cursor.close()

            if self.pool:
                self.database.release(database)

            return resp

        return inner

    def __init__(self, database):
        super().__init__(database)
        self.place_holder = DATABASE_TYPES[type(database)]["placeholder"]
        self.cursor_context = DATABASE_TYPES[type(database)]["cursorcontext"]
        self.commit_needed = DATABASE_TYPES[type(database)]["commit"]
        self.quote = DATABASE_TYPES[type(database)]["quotes"]
        self.pool = DATABASE_TYPES[type(database)]["pool"]

    async def commit(self):
        if not self.pool:
            await self.database.commit()

    async def close(self):
        await self.database.close()

    async def insertifnotexists(self, table_name, data, checks):
        response = await self.select(table_name, [], checks, True)

        if not response:
            return await self.insert(table_name, data)

    @with_cursor
    @with_commit
    async def insert(self, cursor, table_name, data):
        query = f"INSERT INTO {table_name} ({', '.join(data.keys())}) VALUES ({', '.join([self.place_holder] * len(data.values()))})"
        await cursor.execute(query, list(data.values()))

    @with_cursor
    @with_commit
    async def create_table(self, cursor, table_name, columns=None, exists=False):
        query = f'CREATE TABLE {"IF NOT EXISTS" if exists else ""} {self.quote}{table_name}{self.quote} ('
        columns = [] if columns is None else columns

        for column in columns:
            query += f"\n{self.quote}{column}{self.quote} {columns[column]},"

        query = query[:-1]

        query += "\n);"
        await cursor.execute(query)

    @with_cursor
    @with_commit
    async def update(self, cursor, table_name, data, checks):
        query = f"UPDATE {table_name} SET "

        if data:
            for key in data:
                query += f"{key} = {self.place_holder}, "
            query = query[:-2]

        if checks:
            query += " WHERE "
            for check in checks:
                query += f"{check} = {self.place_holder} AND "

            query = query[:-4]

        await cursor.execute(query, list(data.values()) + list(checks.values()))

    async def updateorinsert(self, table_name, data, checks, insert_data):
        response = await self.select(table_name, [], checks, True)

        if len(response) == 1:
            return await self.update(table_name, data, checks)

        return await self.insert(table_name, insert_data)

    @with_cursor
    @with_commit
    async def delete(self, cursor, table_name, checks=None):
        checks = {} if checks is None else checks

        query = f"DELETE FROM {table_name} "

        if checks:
            query += "WHERE "
            for check in checks:
                query += f"{check} = {self.place_holder} AND "

            query = query[:-4]

        await cursor.execute(query, list(checks.values()))

    @with_cursor
    async def select(self, cursor, table_name, keys, checks=None, fetchall=False):
        checks = {} if checks is None else checks

        keys = "*" if not keys else keys
        query = f"SELECT {','.join(keys)} FROM {table_name} "

        if checks:
            query += "WHERE "
            for check in checks:
                query += f"{check} = {self.place_holder} AND "

            query = query[:-4]

        await cursor.execute(query, list(checks.values()))
        columns = [x[0] for x in cursor.description]

        result = await cursor.fetchall() if fetchall else await cursor.fetchone()
        if not result:
            return result

        return (
            [dict(zip(columns, x)) for x in result]
            if fetchall
            else dict(zip(columns, result))
        )

    @with_cursor
    @with_commit
    async def execute(
        self, cursor, sql_query: str, values: List[Any] = None, fetchall: bool = True
    ) -> Union[List[Dict[str, Any]], Dict[str, Any]]:
        await cursor.execute(sql_query, values if values is not None else [])

        result = await cursor.fetchall() if fetchall else await cursor.fetchone()
        columns = [x[0] for x in cursor.description]
        if not result:
            return result

        return (
            [dict(zip(columns, x)) for x in result]
            if fetchall
            else dict(zip(columns, result))
        )


DATABASE_TYPES: Dict[Any, Dict[str, Any]] = {
    motor_asyncio.AsyncIOMotorDatabase: {"class": _MongoDatabase, "placeholder": None},
    aiosqlite.core.Connection: {
        "class": _SqlDatabase,
        "placeholder": "?",
        "cursorcontext": True,
        "commit": True,
        "quotes": '"',
        "pool": False,
    },
    aiomysql.pool.Pool: {
        "class": _SqlDatabase,
        "placeholder": "%s",
        "cursorcontext": True,
        "commit": False,
        "quotes": "`",
        "pool": True,
    },
}

if aiopg:
    DATABASE_TYPES[aiopg.pool.Pool] = {
        "class": _SqlDatabase,
        "placeholder": "%s",
        "cursorcontext": True,
        "commit": True,
        "quotes": '"',
        "pool": True,
    }

DATABASES: List = [_SqlDatabase, _MongoDatabase]


class DatabaseManager:
    """
    Represents a DatabaseManager.
    """

    @staticmethod
    def connect(database: Any) -> Optional[Database]:
        """
        Connects to a database.

        :param Any database: The database.
        :return: The database, if applicable.
        :rtype: Optional[Database]
        """

        if type(database) not in DATABASE_TYPES:
            raise UnsupportedDatabase(
                f"Database of type {type(database)} is not supported by the database manager."
            )

        return DATABASE_TYPES[type(database)]["class"](database)


================================================
FILE: discordSuperUtils/economy.py
================================================
from __future__ import annotations

from dataclasses import dataclass

import discord
from typing import List, Optional

from .base import DatabaseChecker


@dataclass
class EconomyAccount:
    """
    Represents an EconomyAccount.
    """

    economy_manager: EconomyManager
    member: discord.Member

    def __post_init__(self):
        self.table = self.economy_manager.tables["economy"]

    @property
    def __checks(self):
        return EconomyManager.generate_checks(self.member)

    async def currency(self):
        currency_data = await self.economy_manager.database.select(
            self.table, ["currency"], self.__checks
        )
        return currency_data["currency"]

    async def bank(self):
        bank_data = await self.economy_manager.database.select(
            self.table, ["bank"], self.__checks
        )
        return bank_data["bank"]

    async def net(self):
        return await self.bank() + await self.currency()

    async def change_currency(self, amount: int):
        currency = await self.currency()
        await self.economy_manager.database.update(
            self.table, {"currency": currency + amount}, self.__checks
        )

    async def change_bank(self, amount: int):
        bank_amount = await self.bank()
        await self.economy_manager.database.update(
            self.table, {"bank": bank_amount + amount}, self.__checks
        )


class EconomyManager(DatabaseChecker):
    def __init__(self, bot):
        super().__init__(
            [
                {
                    "guild": "snowflake",
                    "member": "snowflake",
                    "currency": "snowflake",
                    "bank": "snowflake",
                }
            ],
            ["economy"],
        )
        self.bot = bot

    @staticmethod
    def generate_checks(member: discord.Member):
        return {"guild": member.guild.id, "member": member.id}

    async def create_account(self, member: discord.Member) -> None:
        self._check_database()

        await self.database.insertifnotexists(
            self.tables["economy"],
            {"guild": member.guild.id, "member": member.id, "currency": 0, "bank": 0},
            self.generate_checks(member),
        )

    async def get_account(self, member: discord.Member) -> Optional[EconomyAccount]:
        self._check_database()

        member_data = await self.database.select(
            self.tables["economy"],
            [],
            self.generate_checks(member),
            True,
        )

        if member_data:
            return EconomyAccount(self, member)

        return None

    @DatabaseChecker.uses_database
    async def get_leaderboard(self, guild: discord.Guild) -> List[EconomyAccount]:
        guild_info = sorted(
            await self.database.select(
                self.tables["economy"], [], {"guild": guild.id}, True
            ),
            key=lambda x: x["bank"] + x["currency"],
            reverse=True,
        )

        members = []
        for member_info in guild_info:
            member = guild.get_member(member_info["member"])
            if member:
                members.append(EconomyAccount(self, member))

        return members


================================================
FILE: discordSuperUtils/fivem.py
================================================
from __future__ import annotations

from dataclasses import dataclass
from typing import Dict, List, Optional

import aiohttp
import aiohttp.client_exceptions


__all__ = ("ServerNotFound", "FiveMPlayer", "FiveMServer")


class ServerNotFound(Exception):
    """Raises an error when a server is invalid or offline."""


@dataclass
class FiveMPlayer:
    """
    Represents a FiveM player.
    """

    id: int
    identifiers: Dict[str, str]
    name: str
    ping: int

    @classmethod
    def from_dict(cls, player_dict: dict) -> FiveMPlayer:
        """
        Creates a FiveM player object from a dict.

        :param dict player_dict: The player information.
        :return: The FiveM player.
        :rtype: FiveMPlayer
        """

        identifiers = dict([x.split(":") for x in player_dict["identifiers"]])
        return cls(
            player_dict["id"], identifiers, player_dict["name"], player_dict["ping"]
        )


@dataclass
class FiveMServer:
    """
    Represents a FiveM server.
    """

    ip: str
    resources: List[str]
    players: List[FiveMPlayer]
    name: str
    variables: Dict[str, str]

    @classmethod
    async def fetch(cls, ip: str) -> Optional[FiveMServer]:
        """
        |coro|

        Fetches the server and returns the server object.
        The server object includes players, resources, name, variables

        :param ip: The server IP.
        :return: The FiveM server.
        :rtype: Optional[FiveMServer]
        """

        base_address = "http://" + ip + "/"

        async with aiohttp.ClientSession() as session:
            try:
                await session.get(base_address)  # Server status check
            except (
                aiohttp.client_exceptions.ClientConnectorError,
                aiohttp.client_exceptions.InvalidURL,
            ):
                raise ServerNotFound(f"Server '{ip}' is invalid or offline.")

            players_request = await session.get(base_address + "players.json")
            info_request = await session.get(base_address + "info.json")
            dynamic_info_request = await session.get(base_address + "dynamic.json")

            info = await info_request.json(content_type=None)
            players = await players_request.json(content_type=None)
            dynamic = await dynamic_info_request.json(content_type=None)
            # This is still included in the session because parsing the json outside of it sometimes doesnt work
            # and block the program (?) :(

        return cls(
            ip,
            info["resources"],
            [FiveMPlayer.from_dict(player) for player in players],
            dynamic["hostname"],
            info["vars"],
        )


================================================
FILE: discordSuperUtils/imaging.py
================================================
from __future__ import annotations

import datetime
import os
import textwrap
import time
from enum import Enum
from io import BytesIO
from typing import Optional, Tuple, Union, TYPE_CHECKING

import PIL
import PIL.ImageShow
import aiohttp
import discord
from PIL import Image, ImageDraw, ImageFont
from PIL.ImageFont import FreeTypeFont

if TYPE_CHECKING:
    from .leveling import LevelingAccount


__all__ = ("ImageManager", "Backgrounds")


class ImageManager:
    """
    An image manager that manages picture creation.
    """

    __slots__ = ()

    DEFAULT_COLOR = (127, 255, 0)

    @staticmethod
    def load_asset(name: str) -> str:
        """
        Returns the asset path of the asset.

        :param str name: The asset.
        :return: The asset path.
        :rtype: str
        """

        return os.path.join(os.path.dirname(__file__), "assets", name)

    @staticmethod
    async def make_request(url: str) -> Optional[bytes]:
        """
        Returns the bytes of the URL response, if applicable.

        :param str url: The url.
        :return: The response bytes.
        :rtype: Optional[bytes]
        """

        async with aiohttp.ClientSession() as session:
            async with session.get(url) as response:
                return await response.read()

    @classmethod
    async def convert_image(cls, url: str) -> Image.Image:
        """
        Converts the image to a PIL image.

        :param str url: The URL.
        :return: The converted image.
        :rtype: Image.Image
        """

        return PIL.Image.open(BytesIO(await cls.make_request(url))).convert("RGBA")

    @staticmethod
    def human_format(num: int) -> str:
        """
        Converts the number to a human readable format.

        :param int num: The number.
        :return: The human readable format.
        :rtype: str
        """

        original_num = num

        num = float("{:.3g}".format(num))
        magnitude = 0
        matches = ["", "K", "M", "B", "T", "Qua", "Qui"]
        while abs(num) >= 1000:
            if magnitude >= 5:
                break

            magnitude += 1
            num /= 1000.0

        try:
            return "{}{}".format(
                "{:f}".format(num).rstrip("0").rstrip("."), matches[magnitude]
            )
        except IndexError:
            return str(original_num)

    @staticmethod
    def multiline_text(
        card: Image.Image,
        text: str,
        font: FreeTypeFont,
        text_color: Tuple[int, int, int],
        start_height: Union[int, float],
        width: int,
    ) -> None:
        """
        Draws multiline text on the card.

        :param Image.Image card: The card to draw on.
        :param str text: The text to write.
        :param FreeTypeFont font: The font.
        :param Tuple[int, int, int] text_color: The text color.
        :param Union[int, float] start_height:
            The start height of the text, the text will start there, and make its way downwards.
        :param int width: The width of the wrap.
        :return: None
        :rtype: None
        """

        draw = ImageDraw.Draw(card)
        image_width, image_height = card.size

        y_text = start_height
        lines = textwrap.wrap(text, width=width)

        for line in lines:
            line_width, line_height = font.getsize(line)

            draw.text(
                ((image_width - line_width) / 2, y_text),
                line,
                font=font,
                fill=text_color,
            )

            y_text += line_height

    async def draw_profile_picture(
        self,
        card: Image.Image,
        member: discord.Member,
        location: Tuple[int, int],
        size: int = 180,
        outline_thickness: int = 5,
        status: bool = True,
        outline_color: Tuple[int, int, int] = (255, 255, 255),
    ) -> Image.Image:
        """
        |coro|

        Pastes the profile picture on the card.

        :param Image.Image card: The card.
        :param discord.Member member: The member to get the profile picture from.
        :param Tuple[int, int] location: The center of the picture.
        :param int size: The size of the pasted profile picture.
        :param int outline_thickness: The outline thickness.
        :param bool status: A bool indicating if it should paste the member's status icon.
        :param Tuple[int, int, int] outline_color: The outline color.
        :return: The result image.
        :rtype: Image.Image
        """

        blank = Image.new("RGBA", card.size, (255, 255, 255, 0))

        location = tuple(
            round(x - size / 2) if i <= 1 else round(x + size / 2)
            for i, x in enumerate(location + location)
        )

        outline_dimensions = tuple(
            x - outline_thickness if i <= 1 else x + outline_thickness
            for i, x in enumerate(location)
        )

        size_dimensions = (size, size)
        status_dimensions = tuple(round(x / 4) for x in size_dimensions)

        mask = Image.new("RGBA", card.size, 0)
        ImageDraw.Draw(mask).ellipse(location, fill=(255, 25, 255, 255))

        avatar = (await self.convert_image(str(member.avatar_url))).resize(
            size_dimensions
        )
        profile_pic_holder = Image.new("RGBA", card.size, (255, 255, 255, 255))

        ImageDraw.Draw(card).ellipse(outline_dimensions, fill=outline_color)

        profile_pic_holder.paste(avatar, location)
        pre_card = Image.composite(profile_pic_holder, card, mask)
        pre_card = pre_card.convert("RGBA")

        if status:
            status_picture = Image.open(self.load_asset(f"{member.status.name}.png"))
            status_picture = status_picture.convert("RGBA").resize(status_dimensions)

            blank.paste(
                status_picture, tuple(x - status_dimensions[0] for x in location[2:])
            )

        return Image.alpha_composite(pre_card, blank)

    async def create_welcome_card(
        self,
        member: discord.Member,
        background: Union[Backgrounds, str],
        title: str,
        description: str,
        title_color: Tuple[int, int, int] = (255, 255, 255),
        description_color: Tuple[int, int, int] = (255, 255, 255),
        font_path: str = None,
        outline: int = 5,
        transparency: int = 0,
    ) -> discord.File:
        """
        |coro|

        Creates a welcome image for the member and returns it as a discord.File.

        :param discord.Member member: The joined member.
        :param Union[Backgrounds, str] background: The background of the image, can be a Backgrounds enum or a URL.
        :param str title: The title.
        :param str description: The description.
        :param Tuple[int, int, int] title_color: The color of the title.
        :param Tuple[int, int, int] description_color: The color of the description.
        :param str font_path: The font path, uses the default font if not passed.
        :param int outline: The outline thickness.
        :param int transparency: The transparency of the background made.
        :return: The discord file.
        :rtype: discord.File
        """

        result_bytes = BytesIO()

        card = (
            Image.open(background.value)
            if isinstance(background, Backgrounds)
            else await self.convert_image(background)
        )
        card = card.resize((1024, 500))

        font_path = font_path if font_path else self.load_asset("font.ttf")

        big_font = ImageFont.truetype(font_path, 36)
        small_font = ImageFont.truetype(font_path, 30)

        draw = ImageDraw.Draw(card, "RGBA")
        if transparency and isinstance(background, Backgrounds):
            draw.rectangle((30, 30, 994, 470), fill=(0, 0, 0, transparency))

        draw.text((512, 360), title, title_color, font=big_font, anchor="ms")
        self.multiline_text(card, description, small_font, description_color, 380, 60)

        final_card = await self.draw_profile_picture(
            card, member, (512, 180), 260, outline_thickness=outline
        )

        final_card.save(result_bytes, format="PNG")
        result_bytes.seek(0)
        return discord.File(result_bytes, filename="welcome_card.png")

    async def create_leveling_profile(
        self,
        member: discord.Member,
        member_account: LevelingAccount,
        background: Union[Backgrounds, str],
        rank: int,
        name_color: Tuple[int, int, int] = DEFAULT_COLOR,
        rank_color: Tuple[int, int, int] = DEFAULT_COLOR,
        level_color: Tuple[int, int, int] = DEFAULT_COLOR,
        xp_color: Tuple[int, int, int] = DEFAULT_COLOR,
        bar_outline_color: Tuple[int, int, int] = (255, 255, 255),
        bar_fill_color: Tuple[int, int, int] = DEFAULT_COLOR,
        bar_blank_color: Tuple[int, int, int] = (255, 255, 255),
        profile_outline_color: Tuple[int, int, int] = DEFAULT_COLOR,
        font_path: str = None,
        outline: int = 5,
    ) -> discord.File:
        """
        |coro|

        Creates a leveling image, converted to a discord.File.

        :param discord.Member member: The member.
        :param LevelingAccount member_account: The leveling account of the member.
        :param Union[Backgrounds, str] background: The background of the image.
        :param int rank: The guild rank of the member.
        :param Tuple[int, int, int] name_color: The color of the member's name.
        :param Tuple[int, int, int] rank_color: The color of the member's rank.
        :param Tuple[int, int, int] level_color: The color of the member's level.
        :param Tuple[int, int, int] xp_color: The color of the member's xp.
        :param Tuple[int, int, int] bar_outline_color: The color of the member's progress bar outline.
        :param Tuple[int, int, int] bar_fill_color: The color of the member's progress bar fill.
        :param Tuple[int, int, int] bar_blank_color: The color of the member's progress bar blank.
        :param Tuple[int, int, int] profile_outline_color: The color of the member's outliine.
        :param str font_path: The font path, uses the default font if not passed.
        :param int outline: The outline thickness.
        :return: The image, converted to a discord.File.
        :rtype: discord.File
        """

        result_bytes = BytesIO()

        card = (
            Image.open(background.value)
            if isinstance(background, Backgrounds)
            else await self.convert_image(background)
        )
        card = card.resize((850, 238))

        font_path = font_path if font_path else self.load_asset("font.ttf")
        font_big = ImageFont.truetype(font_path, 36)
        font_medium = ImageFont.truetype(font_path, 30)
        font_normal = ImageFont.truetype(font_path, 25)
        font_small = ImageFont.truetype(font_path, 20)

        draw = ImageDraw.Draw(card)
        draw.text((245, 90), str(member), name_color, font=font_big, anchor="ls")
        draw.text((800, 90), f"Rank #{rank}", rank_color, font=font_medium, anchor="rs")
        draw.text(
            (245, 165),
            f"Level {await member_account.level()}",
            level_color,
            font=font_normal,
            anchor="ls",
        )
        draw.text(
            (800, 165),
            f"{self.human_format(await member_account.xp())} /"
            f" {self.human_format(await member_account.next_level())} XP",
            xp_color,
            font=font_small,
            anchor="rs",
        )

        draw.rounded_rectangle(
            (242, 182, 803, 208),
            fill=bar_blank_color,
            outline=bar_outline_color,
            radius=13,
            width=3,
        )

        length_of_bar = await member_account.percentage_next_level() * 5.5 + 250
        draw.rounded_rectangle(
            (245, 185, length_of_bar, 205), fill=bar_fill_color, radius=10
        )

        final_card = await self.draw_profile_picture(
            card,
            member,
            (109, 119),
            outline_thickness=outline,
            outline_color=profile_outline_color,
        )

        final_card.save(result_bytes, format="PNG")
        result_bytes.seek(0)
        return discord.File(result_bytes, filename="rankcard.png")

    async def create_spotify_card(
        self, spotify_activity: discord.Spotify, font_path: str = None
    ) -> discord.File:
        """
        |coro|

        Creates a Spotify activity image for the Spotify song and returns it as a discord.File.

        :param discord.Spotify spotify_activity: The Spotify activity.
        :param str font_path: The font path, uses the default font if not passed.
        :return: The discord file.
        :rtype: discord.File
        """

        result_bytes = BytesIO()

        album_image = await self.convert_image(spotify_activity.album_cover_url)

        # Background colour
        paletted = album_image.convert(
            "P", palette=Image.ADAPTIVE, colors=1
        )  # Reduce to palette
        # Finding dominant background color
        palette = paletted.getpalette()
        color_counts = paletted.getcolors()
        palette_index = color_counts[0][1]
        dominant_color = tuple(palette[palette_index * 3 : palette_index * 3 + 3])

        # Checking brightness of bg color
        brightness = (
            (0.21 * dominant_color[0])
            + (0.72 * dominant_color[1])
            + (0.07 * dominant_color[2])
        )

        if brightness < 100:
            text_color = "white"
            bg_img = "spotify_white.png"
        else:
            text_color = "black"
            bg_img = "spotify_black.png"

        track_background_image = Image.open(self.load_asset(bg_img))

        font_path = font_path if font_path else self.load_asset("font.ttf")

        # Fonts
        title_font = ImageFont.truetype(font_path, 16)
        artist_font = ImageFont.truetype(font_path, 14)
        album_font = ImageFont.truetype(font_path, 14)
        start_duration_font = ImageFont.truetype(font_path, 12)
        end_duration_font = ImageFont.truetype(font_path, 12)

        # Positions
        title_text_position = 150, 30
        artist_text_position = 150, 60
        album_text_position = 150, 80
        start_duration_text_position = 150, 119
        end_duration_text_position = 508, 119

        played_duration = (
            datetime.datetime.utcnow() - spotify_activity.start
        ).total_seconds()
        total_duration = spotify_activity.duration.total_seconds()

        played = played_duration / total_duration

        start_duration = time.strftime("%H:%M:%S", time.gmtime(played_duration))
        end_duration = time.strftime("%H:%M:%S", time.gmtime(total_duration))

        # Draws
        draw_on_image = ImageDraw.Draw(track_background_image)

        draw_on_image.text(
            title_text_position, spotify_activity.title, text_color, font=title_font
        )

        draw_on_image.text(
            artist_text_position,
            f"by {spotify_activity.artist}",
            text_color,
            font=artist_font,
        )

        draw_on_image.text(
            album_text_position, spotify_activity.album, text_color, font=album_font
        )

        draw_on_image.text(
            start_duration_text_position,
            start_duration,
            text_color,
            font=start_duration_font,
        )

        draw_on_image.text(
            end_duration_text_position, end_duration, text_color, font=end_duration_font
        )

        draw_on_image.rounded_rectangle(
            (198, 125, 198 + 300 * played, 129),
            fill=text_color,
            outline=None,
            radius=3,
            width=0,
        )

        background_image_color = Image.new(
            "RGBA", track_background_image.size, dominant_color
        )
        background_image_color.paste(
            track_background_image, (0, 0), track_background_image
        )

        # Resize
        album_image_resize = album_image.resize((140, 160))
        background_image_color.paste(album_image_resize, (0, 0), album_image_resize)

        # Save image
        background_image_color.convert("RGB")

        background_image_color.save(result_bytes, format="PNG")
        result_bytes.seek(0)
        return discord.File(result_bytes, filename="spotify.png")


class Backgrounds(Enum):
    GALAXY = ImageManager.load_asset("1.png")
    BLANK_GRAY = ImageManager.load_asset("2.png")
    GAMING = ImageManager.load_asset("3.png")


================================================
FILE: discordSuperUtils/infractions.py
================================================
from __future__ import annotations

import uuid
from dataclasses import dataclass
from datetime import datetime
from typing import List, TYPE_CHECKING, Optional, Dict, Union

from .base import DatabaseChecker
from .punishments import Punisher, get_relevant_punishment

if TYPE_CHECKING:
    from .punishments import Punishment
    import discord
    from discord.ext import commands


__all__ = ("PartialInfraction", "Infraction", "InfractionManager")


@dataclass
class PartialInfraction:
    """
    A partial infraction.
    """

    member: discord.Member
    id: str
    reason: str
    date_of_infraction: datetime


@dataclass
class Infraction:
    """
    An infraction object.
    """

    infraction_manager: InfractionManager
    member: discord.Member
    id: str

    def __post_init__(self):
        self.table = self.infraction_manager.tables["infractions"]

    @property
    def __checks(self) -> Dict[str, int]:
        return {
            "guild": self.member.guild.id,
            "member": self.member.id,
            "id": self.id,
        }

    async def datetime(self) -> Optional[datetime]:
        timestamp_data = await self.infraction_manager.database.select(
            self.table, ["timestamp"], self.__checks
        )
        if timestamp_data:
            return datetime.utcfromtimestamp(timestamp_data["timestamp"])

    async def reason(self) -> Optional[str]:
        reason_data = await self.infraction_manager.database.select(
            self.table, ["reason"], self.__checks
        )
        if reason_data:
            return reason_data["reason"]

    async def set_reason(self, new_reason: str) -> None:
        await self.infraction_manager.database.update(
            self.table, {"reason": new_reason}, self.__checks
        )

    async def delete(self) -> PartialInfraction:
        partial = PartialInfraction(
            self.member, self.id, await self.reason(), await self.datetime()
        )
        await self.infraction_manager.database.delete(self.table, self.__checks)
        return partial


class InfractionManager(DatabaseChecker, Punisher):
    def __init__(self, bot: commands.Bot):
        super().__init__(
            [
                {
                    "guild": "snowflake",
                    "member": "snowflake",
                    "timestamp": "snowflake",
                    "id": "string",
                    "reason": "string",
                }
            ],
            ["infractions"],
        )
        self.punishments = []
        self.bot = bot

    def add_punishments(self, punishments: List[Punishment]) -> None:
        self.punishments = punishments

    @DatabaseChecker.uses_database
    async def warn(
        self, ctx: commands.Context, member: discord.Member, reason: str
    ) -> Infraction:
        generated_id = str(uuid.uuid4())
        await self.database.insert(
            self.tables["infractions"],
            {
                "guild": member.guild.id,
                "member": member.id,
                "timestamp": datetime.utcnow().timestamp(),
                "id": generated_id,
                "reason": reason,
            },
        )

        if punishment := get_relevant_punishment(
            self.punishments, len(await self.get_infractions(member))
        ):
            await punishment.punishment_manager.punish(ctx, member, punishment)

        return Infraction(self, member, generated_id)

    async def punish(
        self, ctx: commands.Context, member: discord.Member, punishment: Punishment
    ) -> None:
        await self.warn(ctx, member, punishment.punishment_reason)
        await self.call_event("on_punishment", ctx, member, punishment)

    @DatabaseChecker.uses_database
    async def get_infractions(
        self,
        member: discord.Member,
        infraction_id: str = None,
        from_timestamp: Union[int, float] = 0,
    ) -> List[Infraction]:
        checks = {"guild": member.guild.id, "member": member.id}
        if infraction_id:
            checks["id"] = infraction_id

        warnings = await self.database.select(
            self.tables["infractions"], [], checks, fetchall=True
        )

        return [
            Infraction(self, member, infraction["id"])
            for infraction in warnings
            if infraction["timestamp"] > from_timestamp
        ]


================================================
FILE: discordSuperUtils/invitetracker.py
================================================
""""
If InviteTracker is used in any way that breaks Discord TOS we, (the DiscordSuperUtils team)
are not responsible or liable in any way.
InviteTracker by DiscordSuperUtils was not intended to violate Discord TOS in any way.
In case we are contacted by Discord, we will remove any and all features that violate the Discord ToS.
Please feel free to read the Discord Terms of Service https://discord.com/terms.
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import TYPE_CHECKING, Union, Optional
import discord

from .base import DatabaseChecker

if TYPE_CHECKING:
    from discord.ext import commands


@dataclass
class InviteAccount:
    """
    Represents an InviteAccount.
    """

    invite_tracker: InviteTracker
    member: discord.Member

    async def get_invited_users(self):
        return await self.invite_tracker.get_members_invited(
            self.member, self.member.guild
        )


class InviteTracker(DatabaseChecker):
    def __init__(self, bot: commands.Bot):
        super().__init__(
            [
                {
                    "guild": "snowflake",
                    "member": "snowflake",
                    "members_invited": "string",
                }
            ],
            ["invites"],
        )
        self.bot = bot
        self.cache = {}

        self.bot.loop.create_task(self.__initialize_cache())

        self.bot.add_listener(self.__cleanup_guild_cache, "on_guild_remove")
        self.bot.add_listener(self.__update_guild_cache, "on_guild_add")
        self.bot.add_listener(self.__track_invite, "on_invite_create")
        self.bot.add_listener(self.__cleanup_invite, "on_invite_delete")

    async def get_invite(self, member: discord.Member) -> Optional[discord.Invite]:
        for inv in await member.guild.invites():
            for invite in self.cache[member.guild.id]:
                if invite.revoked:
                    self.cache[invite.guild.id].remove(invite)
                    return

                if invite.code == inv.code and inv.uses - invite.uses == 1:
                    await self.__update_guild_cache(member.guild)
                    return inv

    @DatabaseChecker.uses_database
    async def get_members_invited(
        self, user: Union[discord.User, discord.Member], guild: discord.Guild
    ):
        invited_members = await self.database.select(
            self.tables["invites"],
            ["members_invited"],
            {"guild": guild.id, "member": user.id},
        )

        if not invited_members:
            return []

        invited_members = invited_members["members_invited"]
        if isinstance(invited_members, str):
            return [
                int(invited_member)
                for invited_member in invited_members.split("\0")
                if invited_member
            ]

    async def fetch_inviter(
        self, invite: discord.Invite
    ) -> Union[discord.Member, discord.User]:
        inviter = invite.guild.get_member(invite.inviter.id)
        return inviter if inviter else await self.bot.get_user(invite.inviter.id)

    @DatabaseChecker.uses_database
    async def register_invite(
        self,
        invite: discord.Invite,
        member: discord.Member,
        inviter: Union[discord.Member, discord.User],
    ) -> None:
        invited_members = await self.get_members_invited(inviter, invite.guild)
        if member.id in invited_members:
            return

        invited_members.append(member.id)
        invited_members_sql = "\0".join(
            str(invited_member) for invited_member in invited_members
        )

        await self.database.updateorinsert(
            self.tables["invites"],
            {"members_invited": invited_members_sql},
            {"guild": invite.guild.id, "member": inviter.id},
            {
                "guild": invite.guild.id,
                "member": inviter.id,
                "members_invited": invited_members_sql,
            },
        )

    async def __initialize_cache(self) -> None:
        await self.bot.wait_until_ready()

        for guild in self.bot.guilds:
            try:
                self.cache[guild.id] = await guild.invites()
            except discord.Forbidden:
                pass

    async def __update_guild_cache(self, guild: discord.Guild) -> None:
        try:
            self.cache[guild.id] = await guild.invites()
        except discord.Forbidden:
            pass

    async def __track_invite(self, invite: discord.Invite) -> None:
        self.cache[invite.guild.id].append(invite)

    async def __cleanup_invite(self, invite: discord.Invite) -> None:
        if invite in self.cache[invite.guild.id]:
            self.cache[invite.guild.id].remove(invite)

    async def __cleanup_guild_cache(self, guild: discord.Guild) -> None:
        self.cache.pop(guild.id)

    @DatabaseChecker.uses_database
    def get_user_info(self, member: discord.Member) -> InviteAccount:
        return InviteAccount(self, member)


================================================
FILE: discordSuperUtils/kick.py
================================================
from __future__ import annotations

from typing import TYPE_CHECKING

import discord

from .base import EventManager
from .punishments import Punisher

if TYPE_CHECKING:
    from discord.ext import commands
    from .punishments import Punishment


__all__ = ("KickManager",)


class KickManager(EventManager, Punisher):
    """
    A KickManager that manages kicks for guilds.
    """

    __slots__ = ("bot",)

    def __init__(self, bot: commands.Bot):
        super().__init__()
        self.bot = bot

    async def punish(
        self, ctx: commands.Context, member: discord.Member, punishment: Punishment
    ) -> None:
        try:
            await member.kick(reason=punishment.punishment_reason)
        except discord.errors.Forbidden as e:
            raise e
        else:
            await self.call_event("on_punishment", ctx, member, punishment)


================================================
FILE: discordSuperUtils/leveling.py
================================================
from __future__ import annotations

import math
import time
from dataclasses import dataclass
from typing import Iterable, TYPE_CHECKING, List

from .base import DatabaseChecker

if TYPE_CHECKING:
    import discord


@dataclass
class LevelingAccount:
    """
    Represents a LevelingAccount.
    """

    leveling_manager: LevelingManager
    member: discord.Member

    def __post_init__(self):
        self.table = self.leveling_manager.tables["xp"]

    @property
    def __checks(self):
        return LevelingManager.generate_checks(self.member)

    async def xp(self):
        xp_data = await self.leveling_manager.database.select(
            self.table, ["xp"], self.__checks
        )
        return xp_data["xp"]

    async def level(self):
        rank_data = await self.leveling_manager.database.select(
            self.table, ["rank"], self.__checks
        )
        return rank_data["rank"]

    async def next_level(self):
        level_up_data = await self.leveling_manager.database.select(
            self.table, ["level_up"], self.__checks
        )
        return level_up_data["level_up"]

    async def percentage_next_level(self):
        level_up = await self.next_level()
        xp = await self.xp()
        initial_xp = await self.initial_rank_xp()

        return min(
            abs(math.floor(abs(xp - initial_xp) / (level_up - initial_xp) * 100)), 100
        )

    async def initial_rank_xp(self):
        next_level = await self.next_level()
        return (
            0
            if next_level == 50
            else next_level / self.leveling_manager.rank_multiplier
        )

    async def set_xp(self, value):
        await self.leveling_manager.database.update(
            self.table, {"xp": value}, self.__checks
        )

    async def set_level(self, value):
        await self.leveling_manager.database.update(
            self.table, {"rank": value}, self.__checks
        )

    async def set_next_level(self, value):
        await self.leveling_manager.database.update(
            self.table, {"level_up": value}, self.__checks
        )


class LevelingManager(DatabaseChecker):
    def __init__(
        self,
        bot,
        award_role: bool = False,
        default_role_interval: int = 5,
        xp_on_message=5,
        rank_multiplier=1.5,
        xp_cooldown=60,
    ):
        super().__init__(
            [
                {
                    "guild": "snowflake",
                    "member": "snowflake",
                    "rank": "number",
                    "xp": "number",
                    "level_up": "number",
                },
                {"guild": "snowflake", "interval": "smallnumber"},
                {"guild": "snowflake", "role": "snowflake"},
            ],
            ["xp", "roles", "role_list"],
        )

        self.bot = bot
        self.award_role = award_role
        self.default_role_interval = default_role_interval
        self.xp_on_message = xp_on_message
        self.rank_multiplier = rank_multiplier
        self.xp_cooldown = xp_cooldown

        self.cooldown_members = {}
        self.add_event(self.on_database_connect)

    @DatabaseChecker.uses_database
    async def set_interval(self, guild: discord.Guild, interval: int = None) -> None:
        """
        Set the role interval of a guild.

        :param interval: The interval to set.
        :type interval: int
        :param guild: The guild to set the role interval in.
        :type guild: discord.Guild
        :return:
        :rtype: None
        """

        interval = interval if interval is not None else self.default_role_interval

        if 0 >= interval:
            raise ValueError("The interval must be greater than 0.")

        sql_insert_data = {"guild": guild.id, "interval": interval}

        await self.database.updateorinsert(
            self.tables["roles"], sql_insert_data, {"guild": guild.id}, sql_insert_data
        )

    @DatabaseChecker.uses_database
    async def get_roles(self, guild: discord.Guild) -> List[int]:
        """
        Returns the role IDs of the guild.

        :param guild: The guild to get the roles from.
        :type guild: discord.Guild
        :return:
        :rtype: List[int]
        """

        return [
            role["role"]
            for role in await self.database.select(
                self.tables["role_list"], ["role"], {"guild": guild.id}, fetchall=True
            )
        ]

    @DatabaseChecker.uses_database
    async def set_roles(
        self, guild: discord.Guild, roles: Iterable[discord.Role]
    ) -> None:
        """
        Sets the roles of the guild.

        :param guild: The guild to set the roles in.
        :type guild: discord.Guild
        :param roles: The roles to set.
        :type roles: Iterable[discord.Role]
        :return:
        :rtype: None
        """

        await self.database.delete(self.tables["role_list"], {"guild": guild.id})

        for role in roles:
            await self.database.insert(
                self.tables["role_list"], {"guild": guild.id, "role": role.id}
            )

    async def on_database_connect(self):
        self.bot.add_listener(self.__handle_experience, "on_message")

    @staticmethod
    def generate_checks(member: discord.Member):
        return {"guild": member.guild.id, "member": member.id}

    async def __handle_experience(self, message):
        self._check_database()

        if not message.guild or message.author.bot:
            return

        member_cooldown = self.cooldown_members.setdefault(message.guild.id, {}).get(
            message.author.id, 0
        )

        if (time.time() - member_cooldown) >= self.xp_cooldown:
            await self.create_account(message.author)
            member_account = await self.get_account(message.author)

            await member_account.set_xp(await member_account.xp() + self.xp_on_message)
            self.cooldown_members[message.guild.id][message.author.id] = time.time()

            leveled_up = False
            while await member_account.xp() >= await member_account.next_level():
                await member_account.set_next_level(
                    await member_account.next_level() * self.rank_multiplier
                )
                await member_account.set_level(await member_account.level() + 1)
                leveled_up = True

            if leveled_up:
                roles = []
                if self.award_role:
                    role_ids = await self.get_roles(message.guild)
                    interval = await self.database.select(
                        self.tables["roles"], ["interval"], {"guild": message.guild.id}
                    )
                    interval = (
                        interval["interval"] if interval else self.default_role_interval
                    )

                    if role_ids:
                        member_level = await member_account.level()

                        if (
                            member_level % interval == 0
                            and member_level // interval <= len(role_ids)
                        ):
                            roles = [
                                message.guild.get_role(role_id) for role_id in role_ids
                            ][: await member_account.level() // interval]
                            roles.reverse()
                            roles = [role for role in roles if role]

                await self.call_event("on_level_up", message, member_account, roles)

                if roles:
                    await message.author.add_roles(*roles)

    @DatabaseChecker.uses_database
    async def create_account(self, member):
        await self.database.insertifnotexists(
            self.tables["xp"],
            dict(
                zip(self.tables_column_data[0], [member.guild.id, member.id, 1, 0, 50])
            ),
            self.generate_checks(member),
        )

    @DatabaseChecker.uses_database
    async def get_account(self, member):
        member_data = await self.database.select(
            self.tables["xp"], [], self.generate_checks(member), True
        )

        if member_data:
            return LevelingAccount(self, member)

        return None

    @DatabaseChecker.uses_database
    async def get_leaderboard(self, guild: discord.Guild):
        guild_info = sorted(
            await self.database.select(
                self.tables["xp"], [], {"guild": guild.id}, True
            ),
            key=lambda x: x["xp"],
            reverse=True,
        )

        members = []
        for member_info in guild_info:
            member = guild.get_member(member_info["member"])
            if member:
                members.append(LevelingAccount(self, member))

        return members


================================================
FILE: discordSuperUtils/messagefilter.py
================================================
from __future__ import annotations

import re
from abc import ABC, abstractmethod
from datetime import timedelta
from typing import TYPE_CHECKING, Union, Any, List

from .base import get_generator_response, EventManager, CacheBased
from .punishments import get_relevant_punishment

if TYPE_CHECKING:
    from discord.ext import commands
    import discord
    from .punishments import Punishment

__all__ = (
    "MessageFilter",
    "MessageResponseGenerator",
    "DefaultMessageResponseGenerator",
)


class MessageResponseGenerator(ABC):
    """
    Represents a URL response generator that filters messages and checks if they contain URLs or anything
    inappropriate.
    """

    __slots__ = ()

    @abstractmethod
    def generate(self, message: discord.Message) -> Union[bool, Any]:
        """
        This function is an abstract method.
        The generate function of the generator.

        :param message: The message to filter.
        :type message: discord.Message
        :return: A boolean representing if the message contains inappropriate content.
        :rtype: Union[bool, Any]
        """


class DefaultMessageResponseGenerator(MessageResponseGenerator):
    URL_RE = re.compile(
        r"(https?://(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|www\.[a-zA-Z0-9]["
        r"a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|https?://(?:www\.|(?!www))[a-zA-Z0-9]+\.[^\s]{2,"
        r"}|www\.[a-zA-Z0-9]+\.[^\s]{2,})"
    )
    DISCORD_INVITE_RE = re.compile(
        r"(?:(?:http|https)://)?(?:www.)?(?:disco|discord|discordapp).("
        r"?:com|gg|io|li|me|net|org)(?:/(?:invite))?/([a-z0-9-.]+)"
    )

    def generate(self, message: discord.Message) -> Union[bool, Any]:
        if message.author.guild_permissions.administrator:
            return False

        return self.URL_RE.match(message.content) or self.DISCORD_INVITE_RE.match(
            message.content
        )


class MessageFilter(EventManager, CacheBased):
    """
    Represents a discordSuperUtils message filter that filters messages and finds inappropriate content.
    """

    __slots__ = ("bot", "generator", "punishments")

    def __init__(
        self,
        bot: commands.Bot,
        generator: MessageResponseGenerator = None,
        delete_message: bool = True,
        wipe_cache_delay: timedelta = timedelta(minutes=5),
    ):
        CacheBased.__init__(self, bot, wipe_cache_delay)
        EventManager.__init__(self)

        self.generator = (
            generator if generator is not None else DefaultMessageResponseGenerator
        )
        self.delete_message = delete_message
        self.punishments = []

        self.bot.add_listener(self.__handle_messages, "on_message")
        self.bot.add_listener(self.__handle_messages, "on_message_edit")

    def add_punishments(self, punishments: List[Punishment]) -> None:
        self.punishments = punishments

    async def __handle_messages(self, message, edited_message=None):
        """
        This function is the main logic of the MessageFilter,
        Handled events: on_message, on_message_edit

        :param message: The on_message message passed by the event.
        :type message: discord.Message
        :param edited_message: The edited messages passed by the on_message_edit event, this function will use this
        incase it is not None.
        :type edited_message: discord.Message
        :return:
        """

        message = edited_message or message

        if not message.guild or message.author.bot:
            return

        if not get_generator_response(
            self.generator, MessageResponseGenerator, message
        ):
            return

        if self.delete_message:
            await message.delete()

        member_warnings = (
            self._cache.setdefault(message.guild.id, {}).get(message.author.id, 0) + 1
        )
        self._cache[message.guild.id][message.author.id] = member_warnings

        await self.call_event("on_inappropriate_message", message, member_warnings)

        if punishment := get_relevant_punishment(self.punishments, member_warnings):
            await punishment.punishment_manager.punish(
                message, message.author, punishment
            )


================================================
FILE: discordSuperUtils/modmail.py
================================================
from __future__ import annotations

import discord
from discord.ext import commands
from typing import Union, List, Optional

from .base import DatabaseChecker


class ModMailManager(DatabaseChecker):
    def __init__(self, bot: Union[commands.Bot, commands.AutoShardedBot], trigger: str):
        self.bot = bot
        self.trigger = trigger

        super().__init__([{"guild": "snowflake", "channel": "snowflake"}], ["modmail"])

        self.bot.add_listener(self._handle_modmail_requests, "on_message")

    async def _handle_modmail_requests(self, message: discord.Message):
        if not isinstance(message.channel, discord.DMChannel):
            return

        if message.author.id == self.bot.user.id:
            return

        if self.trigger.lower() == (message.content.split())[0].lower():
            await self.call_event(
                "on_modmail_request",
                await self.bot.get_context(message=message, cls=commands.Context),
            )

    async def set_channel(self, channel: discord.TextChannel) -> None:
        """
        :param channel: Channel that ModMail is sent to.
        :type channel: discord.TextChannel
        :return:
        :rtype: None
        """

        self._check_database()

        table_data = {"guild": channel.guild.id, "channel": channel.id}

        await self.database.updateorinsert(
            self.tables["modmail"], table_data, {"guild": channel.guild.id}, table_data
        )

    async def get_channel(self, guild: discord.Guild) -> discord.TextChannel:
        """
        :param guild: The guild to fetch the ModMail Channel object for
        :type guild: discord.Guild
        :return:
        :rtype: discord.TextChannel
        """

        channel_id = await self.database.select(
            self.tables["modmail"], ["channel"], {"guild": guild.id}, fetchall=False
        )
        return self.bot.get_channel(channel_id["channel"])

    async def get_mutual_guilds(self, user: discord.User) -> List[discord.Guild]:
        """
        :param user: User to fetch the mutual guilds with the bot.
        :type user: discord.User
        :return:
        :rtype: List[discord.Guild]
        """

        return [x for x in self.bot.guilds if discord.utils.get(x.members, id=user.id)]

    async def get_modmail_guild(
        self, ctx: commands.Context, guilds: List[discord.Guild]
    ) -> Optional[discord.Guild]:
        """
        :param ctx: Used to fetch channel
        :type ctx: commands.Context
        :param guilds: List of all mutual guilds
        :type guilds: List[discord.Guild]
        :return:
        :rtype: discord.Guild
        """
        embed = discord.Embed(
            title="ModMail",
            description="Please type the Guild ID to send modmail to that server",
        )
        for guild in guilds:
            embed.add_field(name=f"{guild}", value=f"{guild.id}")

        def check(message):
            if isinstance(message.channel, discord.DMChannel):
                return message.author.id == ctx.author.id

        await ctx.send(embed=embed)
        msg = await self.bot.wait_for("message", check=check, timeout=60)

        guildids = [guild.id for guild in guilds]

        if int(msg.content) in guildids:
            return self.bot.get_guild(int(msg.content))
        return None

    async def get_message(self, ctx: commands.Context) -> str:
        """
        :param ctx: fetch channel
        :type ctx: commands.Context
        :return:
        :rtype: str
        """

        embed = discord.Embed(
            title="ModMail", description="Please type your message to the Mods."
        )
        await ctx.send(embed=embed)

        def check(message):
            if isinstance(message.channel, discord.DMChannel):
                return message.author.id == ctx.author.id

        msg = await self.bot.wait_for("message", check=check, timeout=60)
        return msg.content


================================================
FILE: discordSuperUtils/music/__init__.py
================================================
from .exceptions import *
from .playlist import *
from .enums import *
from .lavalink import *
from .music import *
from .utils import *


================================================
FILE: discordSuperUtils/music/constants.py
================================================
import re

import youtube_dl

FFMPEG_OPTIONS = {
    "before_options": "-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5",
    "options": "-vn",
}

SPOTIFY_RE = re.compile("^https://open.spotify.com/")
DEEZER_RE = re.compile("^https://deezer.page.link/")
SOUNDCLOUD_RE = re.compile("^https://soundcloud.com/")

YTDL_OPTS = {
    "format": "bestaudio/best",
    "restrictfilenames": True,
    "noplaylist": False,
    "nocheckcertificate": True,
    "ignoreerrors": False,
    "logtostderr": False,
    "quiet": True,
    "no_warnings": True,
    "default_search": "auto",
}
YTDL = youtube_dl.YoutubeDL(YTDL_OPTS)


================================================
FILE: discordSuperUtils/music/enums.py
================================================
from enum import Enum


__all__ = ("Loops", "PlaylistType", "ManagerType")


class Loops(Enum):
    NO_LOOP = 0
    LOOP = 1
    QUEUE_LOOP = 2


class PlaylistType(Enum):
    SPOTIFY = 0
    YOUTUBE = 1


class ManagerType(Enum):
    FFMPEG = 0
    LAVALINK = 1


================================================
FILE: discordSuperUtils/music/exceptions.py
================================================
__all__ = (
    "NotPlaying",
    "NotConnected",
    "NotPaused",
    "QueueEmpty",
    "AlreadyConnected",
    "AlreadyPaused",
    "RemoveIndexInvalid",
    "SkipError",
    "UserNotConnected",
    "InvalidSkipIndex",
    "InvalidPreviousIndex",
)


class NotPlaying(Exception):
    """Raises error when client is not playing"""


class NotConnected(Exception):
    """Raises error when client is not connected to a voice channel"""


class InvalidPreviousIndex(Exception):
    """Raises error when the previous index is < 0"""


class NotPaused(Exception):
    """Raises error when player is not paused"""


class QueueEmpty(Exception):
    """Raises error when queue is empty"""


class AlreadyConnected(Exception):
    """Raises error when client is already connected to voice"""


class AlreadyPaused(Exception):
    """Raises error when player is already paused."""


class RemoveIndexInvalid(Exception):
    """Raises error when the queue player remove index is invalid"""


class SkipError(Exception):
    """Raises error when there is no song to skip to"""


class UserNotConnected(Exception):
    """Raises error when user is not connected to channel"""


class InvalidSkipIndex(Exception):
    """Raises error when the skip index is < 0"""


================================================
FILE: discordSuperUtils/music/lavalink/__init__.py
================================================
from .lavalink import *
from .equalizer import *
from .player import *


================================================
FILE: discordSuperUtils/music/lavalink/equalizer.py
================================================
from __future__ import annotations

from dataclasses import dataclass
from typing import List, Dict, Any

__all__ = ("Equalizer",)


@dataclass
class Equalizer:
    """
    Represents an Equalizer that supports different voice effects.
    """

    raw: List[float]
    name: str

    @property
    def eq(self) -> List[Dict[str, Any]]:
        return [{"band": index, "gain": gain} for index, gain in enumerate(self.raw)]

    @classmethod
    def flat(cls) -> Equalizer:
        levels = [0.0 for i in range(15)]

        return cls(levels, "Flat")

    @classmethod
    def boost(cls):
        levels = [
            0.08,
            0.12,
            0.2,
            0.18,
            0.15,
            0.1,
            0.05,
            0.0,
            0.02,
            -0.04,
            -0.06,
            -0.08,
            -0.10,
            -0.12,
            -0.14,
        ]

        return cls(levels, "Boost")

    @classmethod
    def metal(cls):
        levels = [
            0.0,
            0.1,
            0.1,
            0.15,
            0.13,
            0.1,
            0.0,
            0.125,
            0.175,
            0.175,
            0.125,
            0.125,
            0.1,
            0.075,
            0.0,
        ]

        return cls(levels, "Metal")

    @classmethod
    def piano(cls):
        levels = [
            -0.25,
            -0.25,
            -0.125,
            0.0,
            0.25,
            0.25,
            0.0,
            -0.25,
            -0.25,
            0.0,
            0.0,
            0.5,
            0.25,
            -0.025,
        ]

        return cls(levels, "Piano")
    
    @classmethod
    def jazz(cls):
        levels = [
            -0.13,
            -0.11,
            -0.1,
            -0.1,
            0.14,
            0.2,
            -0.18,
            0.0,
            0.24,
            0.22,
            0.2,
            0.0,
            0.0,
            0.0,
            0.0,
        ]

        return cls(levels, "Jazz")


    @classmethod
    def pop(cls):
        levels = [
            -0.02,
            -0.01,
            0.08,
            0.1,
            0.15,
            0.1,
            0.03,
            -0.02,
            -0.035,
            -0.05,
            -0.05,
            -0.05,
            -0.05,
            -0.05,
            -0.05,
        ]

        return cls(levels, "Pop")

    @classmethod
    def treble(cls):
        levels = [
            -0.1, 
            -0.12, 
            -0.12, 
            -0.12, 
            -0.08, 
            -0.04, 
            0.0, 
            0.3, 
            0.34, 
            0.4, 
            0.35, 
            0.3, 
            0.3, 
            0.3, 
            0.3,
        ]

        return cls(levels, "Treble")


================================================
FILE: discordSuperUtils/music/lavalink/lavalink.py
================================================
from __future__ import annotations

from typing import Optional, TYPE_CHECKING, Dict

import wavelink

from .equalizer import Equalizer
from ..enums import ManagerType
from ..music import MusicManager

if TYPE_CHECKING:
    from discord.ext import commands

__all__ = ("LavalinkMusicManager",)


class LavalinkMusicManager(MusicManager):
    """
    Represents a lavalink music manager.
    """

    def __init__(
        self,
        bot: commands.Bot,
        spotify_support: bool = True,
        inactivity_timeout: int = 60,
        minimum_users: int = 1,
        **kwargs,
    ):
        super().__init__(
            bot, spotify_support, inactivity_timeout, minimum_users, **kwargs
        )

        self.default_volume *= 100
        self.type = ManagerType.LAVALINK
        self.contexts: Dict[int, commands.Context] = {}
        self.bot.add_listener(self.__on_song_end, "on_wavelink_track_end")
        self.add_event(self.__on_queue_end, "on_queue_end")

    @staticmethod
    async def __on_queue_end(ctx):
        # MusicManager stops the voice client because the wavelink library assumes it is still playing.
        await ctx.voice_client.stop()

    async def __on_song_end(self, player: wavelink.Player, track, reason):
        await self._check_queue(self.contexts[player.guild.id])

    async def connect_node(
        self,
        host: str,
        password: str,
        port: int,
        identifier: str = "LavaLinkMusicManager",
    ) -> wavelink.Node:
        return await wavelink.NodePool.create_node(
            host=host, password=password, port=port, identifier=identifier, bot=self.bot
        )

    async def _check_queue(self, ctx: commands.Context) -> None:
        try:
            if not ctx.voice_client or not ctx.voice_client.is_connected():
                return

            queue = self.queue[ctx.guild.id]
            player = await self.get_next_player(ctx, queue)
            if not player:
                return

            await ctx.voice_client.set_volume(queue.volume)
            await ctx.voice_client.play(
                (await wavelink.YouTubeTrack.search(player.url))[0]
            )
            self.contexts[ctx.guild.id] = ctx

            queue.played_history.append(player)
            queue.vote_skips = []
            await self.call_event("on_play", ctx, player)

        except (IndexError, KeyError):
            await self.cleanup(None, ctx.guild)
            await self.call_event("on_queue_end", ctx)

    @MusicManager.ensure_connection()
    async def get_player_played_duration(
        self, ctx: commands.Context, _=None
    ) -> Optional[float]:
        return ctx.voice_client.position

    @MusicManager.ensure_connection(check_playing=True, check_queue=True)
    async def volume(
        self, ctx: commands.Context, volume: int = None
    ) -> Optional[float]:
        if volume is None:
            return ctx.voice_client.volume

        await ctx.voice_client.set_volume(volume)
        self.queue[ctx.guild.id].volume = volume
        return ctx.voice_client.volume

    @MusicManager.ensure_connection(check_playing=True)
    async def get_equalizer(self, ctx: commands.Context) -> Optional[Equalizer]:
        """
        Returns the ctx's equalizer.

        :param commands.Context ctx: The context.
        :return: The equalizer.
        :rtype: Optional[Equalizer]
        """

        return ctx.voice_client.equalizer or Equalizer.flat()

    @MusicManager.ensure_connection(check_playing=True)
    async def set_equalizer(self, ctx: commands.Context, equalizer: Equalizer) -> bool:
        """
        |coro|

        Sets the ctx's equalizer.

        :param commands.Context ctx: The context.
        :param Equalizer equalizer: The equalizer.
        :return: A bool indicating if the set was successful,
        :rtype: Optional[bool]
        """

        await ctx.voice_client.set_eq(equalizer)
        return True

    @MusicManager.ensure_connection(check_playing=True)
    async def seek(self, ctx: commands.Context, position: int = 0) -> Optional[bool]:
        """
        |coro|

        Seeks the current player to the position (ms)

        :param ctx: The context
        :param position: time to seek to (ms)
        :return: A bool indicating if the seek was successful
        :rtype: Optional[bool]
        """

        await ctx.voice_client.seek(position=position)
        return True


================================================
FILE: discordSuperUtils/music/lavalink/player.py
================================================
from dataclasses import dataclass

import wavelink

from .equalizer import Equalizer


@dataclass(init=False)
class LavalinkPlayer(wavelink.Player):
    """
    Represents a LavalinkPlayer.
    """

    equalizer: Equalizer = Equalizer.flat()

    async def set_eq(self, equalizer: Equalizer) -> None:
        await self.node._websocket.send(
            op="equalizer", guildId=str(self.guild.id), bands=equalizer.eq
        )
        self.equalizer = equalizer


================================================
FILE: discordSuperUtils/music/music.py
================================================
from __future__ import annotations

import asyncio
import random
import time
import uuid
from typing import Optional, TYPE_CHECKING, List, Tuple, Dict, Callable, Union

import aiohttp
import discord

from .constants import *
from .enums import Loops, ManagerType
from .exceptions import (
    QueueEmpty,
    NotPlaying,
    NotConnected,
    RemoveIndexInvalid,
    AlreadyPaused,
    NotPaused,
    InvalidSkipIndex,
    SkipError,
    AlreadyConnected,
    UserNotConnected,
    InvalidPreviousIndex,
)
from .lavalink.player import LavalinkPlayer
from .player import Player
from .playlist import Playlist, UserPlaylist
from .queue import QueueManager
from .utils import get_playlist
from ..base import create_task, DatabaseChecker, maybe_coroutine
from ..spotify import SpotifyClient
from ..youtube import YoutubeClient

if TYPE_CHECKING:
    from .. import SlashClient
    from discord.ext import commands

__all__ = ("MusicManager",)


class MusicManager(DatabaseChecker):
    """
    Represents a MusicManager.
    """

    __slots__ = (
        "bot",
        "client_id",
        "client_secret",
        "spotify_support",
        "inactivity_timeout",
        "queue",
        "spotify",
    )

    def __init__(
        self,
        bot: SlashClient,
        spotify_support: bool = True,
        inactivity_timeout: int = 60,
        minimum_users: int = 1,
        opus_players: bool = False,
        **kwargs,
    ):
        super().__init__(
            [
                {
                    "user": "snowflake",
                    "playlist_url": "string",
                    "id": "string",
                }
            ],
            ["playlists"],
        )
        self.bot = bot
        setattr(bot, self._on_voice_state_update.__name__, self._on_voice_state_update)

        self.client_id = kwargs.get("client_id")
        self.client_secret = kwargs.get("client_secret")
        self.default_volume = kwargs.get("default_volume") or 0.1
        self.executable = kwargs.get("executable") or "ffmpeg"
        self.spotify_support = spotify_support
        self.inactivity_timeout = 0 if not inactivity_timeout else inactivity_timeout
        self.minimum_users = minimum_users

        self.queue: Dict[int, QueueManager] = {}
        self.youtube = YoutubeClient(loop=self.bot.loop)
        self.opus_players = opus_players

        self._load_opus()

        if spotify_support:
            self.spotify = SpotifyClient(
                client_id=self.client_id,
                client_secret=self.client_secret,
                loop=self.bot.loop,
            )

        self.type = ManagerType.FFMPEG

    @staticmethod
    def _load_opus() -> None:
        """
        Ensures the opus library is loaded.

        :return: None
        :rtype: None
        :raises: RuntimeError: Could not find opus on the machine.
        """

        if not discord.opus.is_loaded():
            try:
                discord.opus._load_default()
            except OSError:
                raise RuntimeError("Could not find opus on the machine.")

    async def cleanup(
        self, voice_client: Optional[discord.VoiceClient], guild: discord.Guild
    ):
        """
        |coro|

        Cleans up after a guild.

        :param discord.Guild guild: The guild to cleanup.
        :param Optional[discord.VoiceClient] voice_client: The voice client.
        :return: None
        :rtype: None
        """

        if voice_client:
            try:
                await voice_client.disconnect(force=True)
            except ValueError:
                # Raised from wavelink
                pass

        if guild.id in self.queue:
            queue = self.queue.pop(guild.id)
            queue.cleanup()
            del queue

    @DatabaseChecker.uses_database
    async def add_playlist(
        self, user: discord.User, url: str
    ) -> Optional[UserPlaylist]:
        """
        |coro|

        Adds a playlist to the user's account.
        Saves the playlist in the database.

        :param discord.User user: The owner of the playlist.
        :param str url: The playlist URL.
        :return: None
        :rtype: None
        """

        playlist = await get_playlist(self.spotify, self.youtube, url)

        if not playlist:
            return

        generated_id = str(uuid.uuid4())
        await self.database.insertifnotexists(
            self.tables["playlists"],
            {"user": user.id, "playlist_url": url, "id": generated_id},
            {"user": user.id, "playlist_url": url},
        )

        return UserPlaylist(self, user, generated_id, playlist)

    @DatabaseChecker.uses_database
    async def get_playlist(
        self, user: discord.User, playlist_id: str, partial: bool = False
    ) -> Optional[UserPlaylist]:
        """
        |coro|

        Gets a user playlist by id.

        :param str playlist_id: The playlist id.
        :param bool partial: Indicating if the function should not fetch the playlist data.
        :param discord.User user: The user.
        :return: The user playlist.
        :rtype: Optional[UserPlaylist]
        """

        playlist = await self.database.select(
            self.tables["playlists"], [], {"user": user.id, "id": playlist_id}
        )

        if playlist:
            return UserPlaylist(
                self,
                user,
                playlist_id,
                await get_playlist(self.spotify, self.youtube, playlist["playlist_url"])
                if not partial
                else None,
            )

    @DatabaseChecker.uses_database
    async def get_user_playlists(
        self, user: discord.User, partial: bool = False
    ) -> List[UserPlaylist]:
        """
        |coro|

        Returns the user's playlists.

        :param discord.User user: The user.
        :param bool partial: Indicating if the function should not fetch the playlist data.
        :return: The list of user playlists.
        :rtype: List[UserPlaylist]
        """

        user_playlist_ids = await self.database.select(
            self.tables["playlists"], ["id"], {"user": user.id}, True
        )

        return list(
            await asyncio.gather(
                *[
                    self.get_playlist(user, user_playlist_id["id"], partial)
                    for user_playlist_id in user_playlist_ids
                ]
            )
        )

    async def _on_voice_state_update(self, member, before, after):
        voice_client = member.guild.voice_client
        channel_change = before.channel != after.channel

        if member == self.bot.user and channel_change and before.channel:
            await self.cleanup(voice_client, member.guild)

        elif voice_client and channel_change:
            voice_members = list(
                filter(lambda x: not x.bot, voice_client.channel.members)
            )

            if len(voice_members) < self.minimum_users:
                await asyncio.sleep(self.inactivity_timeout)

                await self.cleanup(voice_client, member.guild)

    async def ensure_activity(self, ctx: commands.Context) -> None:
        """
        |coro|

        Waits the inactivity timeout and ensures the voice client in ctx is playing a song.
        If no song is playing, it disconnects and calls the on_inactivity_timeout event.

        :param ctx: The context.
        :type ctx: commands.Context
        :return: None
        :rtype: None
        """

        if self.inactivity_timeout is None:
            return

        await asyncio.sleep(self.inactivity_timeout)

        if (
            ctx.voice_client
            and ctx.voice_client.is_connected()
            and not ctx.voice_client.is_playing()
        ):
            await self.cleanup(ctx.voice_client, ctx.guild)

            await self.call_event("on_inactivity_disconnect", ctx)

    async def _check_connection(
        self,
        ctx: commands.Context,
        check_playing: bool = False,
        check_queue: bool = False,
    ) -> Optional[bool]:
        """
        |coro|

        Checks the connection state of the voice client in ctx.

        :param ctx: The context.
        :type ctx: commands.Context
        :param check_playing: A bool indicating if the function should check if a song is playing.
        :type check_playing: bool
        :param check_queue: A bool indicating if the function should check if a queue exists.
        :type check_queue: bool
        :return: True if all the checks passed.
        :rtype: bool
        """

        if not ctx.voice_client or not ctx.voice_client.is_connected():
            await self.call_event(
                "on_music_error",
                ctx,
                NotConnected("Client is not connected to a voice channel"),
            )
            return

        if check_playing and not ctx.voice_client.is_playing():
            await self.call_event(
                "on_music_error",
                ctx,
                NotPlaying("Client is not playing anything currently"),
            )
            return

        if check_queue and ctx.guild.id not in self.queue:
            await self.call_event("on_music_error", ctx, QueueEmpty("Queue is empty"))
            return

        return True

    def ensure_connection(*d_args, **d_kwargs) -> Callable:
        """
        A decorator which ensures there is a proper connection before invoking the decorated function.

        :param d_args: The connection arguments.
        :param d_kwargs: The connection key arguments.
        :return: The decorator.
        :rtype: Callable
        """

        def decorator(function):
            async def wrapper(self, ctx, *args, **kwargs):
                if await self._check_connection(ctx, *d_args, **d_kwargs):
                    return await function(self, ctx, *args, **kwargs)

            return wrapper

        return decorator

    async def get_next_player(self, ctx: commands.Context, queue: QueueManager) -> Optional[Player]:
        """
        |coro|

        Gets the next player in the queue.

        :param ctx: The context.
        :type ctx: commands.Context
        :param queue: The queue manager.
        :type queue: QueueManager
        :return: The next player.
        :rtype: Optional[Player]
        """

        player = await queue.get_next_player(self.youtube)
        if not player:
            await self.cleanup(None, ctx.guild)
            await self.call_event("on_queue_end", ctx)
            return None

        return player

    async def _check_queue(self, ctx: commands.Context) -> None:
        """
        |coro|

        Plays the next song in the queue, handles looping, queue looping, autoplay, etc.

        :param ctx: The context of the voice client.
        :type ctx: commands.Context
        :return: None
        :rtype: None
        """

        try:
            if not ctx.voice_client or not ctx.voice_client.is_connected():
                return

            queue = self.queue[ctx.guild.id]
            player = await self.get_next_player(ctx, queue)
            if not player:
                return

            player.source = (
                discord.PCMVolumeTransformer(
                    discord.FFmpegPCMAudio(
                        player.stream_url, **FFMPEG_OPTIONS, executable=self.executable
                    ),
                    queue.volume,
                )
                if not self.opus_players
                else discord.FFmpegOpusAudio(
                    player.stream_url, **FFMPEG_OPTIONS, executable=self.executable
                )
            )

            ctx.voice_client.play(
                player.source,
                after=lambda x: create_task(self.bot.loop, self._check_queue(ctx)),
            )

            player.start_timestamp = time.time()

            queue.played_history.append(player)
            queue.vote_skips = []
            await self.call_event("on_play", ctx, player)

        except (IndexError, KeyError):
            await self.cleanup(None, ctx.guild)
            await self.call_event("on_queue_end", ctx)

    async def get_player_playlist(self, player: Player) -> Optional[Playlist]:
        """
        |coro|

        Returns the player's playlist, if applicable.

        :param Player player: The player.
        :return: The player's playlist.
        :rtype: Optional[Playlist]
        """

        return await get_playlist(self.spotify, self.youtube, player.used_query)

    @ensure_connection()
    async def get_player_played_duration(
        self, ctx: commands.Context, player: Player
    ) -> Optional[float]:
        """
        |coro|

        Returns the played duration of a player.

        :param ctx: The context.
        :type ctx: commands.Context
        :param player: The player.
        :type player: Player
        :return: The played duration of the player in seconds.
        :rtype: Optional[float]
        """

        start_timestamp = player.start_timestamp
        if ctx.voice_client.is_paused():
            start_timestamp = (
                player.start_timestamp + time.time() - player.last_pause_timestamp
            )

        time_played = time.time() - start_timestamp
        return min(
            time_played, time_played if player.duration == "LIVE" else player.duration
        )

    async def create_playlist_players(
        self, playlist: Playlist, requester: discord.Member
    ) -> List[Player]:
        """
        |coro|

        Returns a list of players from the playlist.

        :param Playlist playlist: The playlist.
        :param discord.Member requester: The requester.
        :return: The list of created players.
        :rtype: List[Player]
        """

        return await Player.make_multiple_players(
            self.youtube,
            playlist.url,
            [
                str(song) for song in playlist.songs
            ],  # Converts the song to str to convert any spotify tracks.
            requester,
        )

    @staticmethod
    async def fetch_ytdl_data(url: str) -> Optional[dict]:
        """
        |coro|

        Fetches the data from an url.

        :param str url: The url to fetch the data from.
        :return: The data from the url if applicable.
        :rtype: Optional[dict]
        """

        try:
            loop = asyncio.get_event_loop()
            return await loop.run_in_executor(
                None, lambda: YTDL.extract_info(url, download=False)
            )
        except youtube_dl.utils.DownloadError:
            return None

    async def create_player(
        self, query: str, requester: discord.Member
    ) -> List[Player]:
        """
        |coro|

        Creates a list of players from the query.
        This function supports Spotify and all YTDL supported links.

        :param requester: The requester.
        :type requester: discord.Member
        :param query: The query.
        :type query: str
        :return: The list of players.
        :rtype: List[Player]
        """

        if SPOTIFY_RE.match(query) and self.spotify_support:
            return await Player.make_multiple_players(
                self.youtube,
                query,
                [song for song in await self.spotify.get_songs(query)],
                requester,
            )

        if DEEZER_RE.match(query) or SOUNDCLOUD_RE.match(query):
            data = await self.fetch_ytdl_data(query)
            if not data:
                return []

            return [
                Player(
                    requester,
                    query,
                    data["title"],
                    data["url"],
                    data["webpage_url"],
                    data.get("duration", 30),
                    data,
                    True,
                )
            ]

        return await Player.make_players(self.youtube, query, requester)

    @ensure_connection()
    async def queue_add(
        self,
        ctx: commands.Context,
        players: List[Player],
    ) -> Optional[bool]:
        """
        |coro|

        Adds a list of players to the ctx queue.
        If a queue does not exist in ctx, it creates one.

        :param players: The list of players.
        :type players: List[Player]
        :param ctx: The context.
        :type ctx: commands.Context
        :return: A bool indicating if it was successful
        :rtype: Optional[bool]
        """

        if ctx.guild.id in self.queue:
            self.queue[ctx.guild.id].queue += players
        else:
            self.queue[ctx.guild.id] = QueueManager(self.default_volume, players)

        return True

    @ensure_connection(check_queue=True)
    async def queue_remove(self, ctx: commands.Context, index: int) -> Optional[Player]:
        """
        |coro|

        Removes a player from the queue in ctx at the specified index.
        Calls on_music_error with RemoveIndexInvalid if index is invalid.

        :param ctx: The context.
        :type ctx: commands.Context
        :param index: The index.
        :type index: int
        :return: The player that was removed, if applicable.
        :rtype: Optional[Player]
        """

        try:
            queue = self.queue[ctx.guild.id]

            return queue.remove(queue.pos + index)
        except IndexError:
            await self.call_event(
                "on_music_error",
                ctx,
                RemoveIndexInvalid("Failure when removing player from queue"),
            )

    async def lyrics(
        self, ctx: commands.Context, query: str = None
    ) -> Optional[Tuple[str, str, str]]:
        """
        |coro|

        Returns the lyrics from the query or the currently playing song.

        :param ctx: The context.
        :type ctx: commands.Context
        :param query: The query.
        :type query: str
        :return: The lyrics and the song name.
        :rtype: Optional[Tuple[str, str, str]]
        """

        query = await self.now_playing(ctx) if query is None else query
        if not query:
            return

        url = f"https://some-random-api.ml/lyrics?title={query}"

        async with aiohttp.ClientSession() as session:
            request = await session.get(url)
            request_json = await request.json(content_type=None)

            authors = request_json.get("author")
            title = request_json.get("title")
            lyrics = request_json.get("lyrics")

            return (title, authors, lyrics) if lyrics else None

    @ensure_connection()
    async def play(
        self,
        ctx: commands.Context,
    ) -> Optional[bool]:
        """
        |coro|

        Plays the player or the next song in the queue.

        :param ctx: The context.
        :type ctx: commands.Context
        :return: A bool indicating if the play was successful
        :rtype: Optional[bool]
        """

        if not ctx.voice_client.is_playing():
            await self._check_queue(ctx)
            return True

    @ensure_connection()
    async def pause(self, ctx: commands.Context) -> Optional[bool]:
        """
        |coro|

        Pauses the currently playing song in ctx.
        Calls on_music_error with AlreadyPaused if already paused.

        :param ctx: The context.
        :type ctx: commands.Context
        :return: A bool indicating if the pause was successful
        :rtype: Optional[bool]
        """

        if ctx.voice_client.is_paused():
            await self.call_event(
                "on_music_error", ctx, AlreadyPaused("Player is already paused.")
            )
            return

        if self.type == ManagerType.LAVALINK:
            await ctx.voice_client.set_pause(pause=True)
        else:
            (await self.now_playing(ctx)).last_pause_timestamp = time.time()
            ctx.voice_client.pause()

        create_task(self.bot.loop, self.ensure_activity(ctx))
        return True

    @ensure_connection()
    async def resume(self, ctx: commands.Context) -> Optional[bool]:
        """
        |coro|

        Resumes the currently paused song in ctx.
        Calls on_music_error with NotPaused if not paused.

        :param ctx: The context.
        :type ctx: commands.Context
        :return: A bool indicating if the resume was successful
        :rtype: Optional[bool]
        """

        if not ctx.voice_client.is_paused():
            await self.call_event(
                "on_music_error", ctx, NotPaused("Player is not paused")
            )
            return

        if self.type == ManagerType.LAVALINK:
            await ctx.voice_client.set_pause(pause=False)
        else:
            ctx.voice_client.resume()

            now_playing = await self.now_playing(ctx)
            now_playing.start_timestamp += (
                time.time() - now_playing.last_pause_timestamp
            )

        return True

    @ensure_connection(check_playing=True, check_queue=True)
    async def previous(
        self, ctx: commands.Context, index: int = None, no_autoplay: bool = False
    ) -> Optional[List[Player]]:
        """
        |coro|

        Plays the (index) player from the history.

        :param commands.Context ctx: The ctx.
        :param bool no_autoplay: A bool indicating if autoplayed songs should be added back to the queue.
        :param int index: The index.
        :return: The list of Players that have been added back.
        :rtype: Optional[List[Player]]
        """

        queue = self.queue[ctx.guild.id]

        previous_index = 2 if index is None else index + 1
        if 0 >= previous_index:
            if index:
                await self.call_event(
                    "on_music_error",
                    ctx,
                    InvalidPreviousIndex("Previous index invalid."),
                )
                return

        original_queue_position = queue.pos
        queue.pos -= previous_index
        previous_players = queue.queue[queue.pos + 1 : original_queue_position]

        if no_autoplay:
            for player in previous_players[:]:
                if not player.requester:
                    previous_players.remove(player)
                    queue.queue.remove(player)

        await maybe_coroutine(ctx.voice_client.stop)
        return previous_players

    @ensure_connection(check_playing=True, check_queue=True)
    async def goto(self, ctx: commands.Context, index: int = 0) -> None:
        queue = self.queue[ctx.guild.id]

        queue.pos = index
        await maybe_coroutine(ctx.voice_client.stop)

    @ensure_connection(check_playing=True, check_queue=True)
    async def skip(self, ctx: commands.Context, index: int = None) -> Optional[Player]:
        """
        |coro|

        Skips to the index in ctx.
        Calls on_music_error with InvalidSkipIndex or SkipError.

        :param index: The index to skip to.
        :type index: int
        :param ctx: The context.
        :type ctx: commands.Context
        :return: The skipped player if applicable.
        :rtype: Optional[Player]
        """

        queue = self.queue[ctx.guild.id]

        # Created duplicate to make sure InvalidSkipIndex isn't raised when the user does pass an index and the queue
        # is empty.
        skip_index = 0 if index is None else index - 1
        if not skip_index < len(queue.queue) and not queue.pos < skip_index:
            if index:
                await self.call_event(
                    "on_music_error", ctx, InvalidSkipIndex("Skip index invalid.")
                )
                return

        if (
            not queue.autoplay
            and queue.loop != Loops.QUEUE_LOOP
            and (len(queue.queue) - 1) <= queue.pos + skip_index
        ):
            await self.call_event(
                "on_music_error", ctx, SkipError("No song to skip to.")
            )
            return

        original_position = queue.pos
        queue.pos += skip_index

        if queue.autoplay:
            last_video_id = queue.played_history[-1].data["videoDetails"]["videoId"]
            player = (await Player.get_similar_videos(last_video_id, self.youtube))[0]
            queue.add(player)
        else:
            player = queue.queue[original_position]

        await maybe_coroutine(ctx.voice_client.stop)
        return player

    @ensure_connection(check_playing=True, check_queue=True)
    async def volume(
        self, ctx: commands.Context, volume: int = None
    ) -> Optional[float]:
        """
        |coro|

        Sets the volume in ctx.
        Returns the current volume if volume is None.

        :param volume: The volume to set.
        :type volume: int
        :param ctx: The context.
        :type ctx: commands.Context
        :return: The new volume.
        :rtype: Optional[float]
        """

        if volume is None:
            return ctx.voice_client.source.volume * 100

        ctx.voice_client.source.volume = volume / 100
        self.queue[ctx.guild.id].volume = volume / 100
        return ctx.voice_client.source.volume * 100

    async def join(self, ctx: commands.Context) -> Optional[discord.VoiceChannel]:
        """
        |coro|

        Joins the ctx voice channel.
        Calls on_music_error with AlreadyConnected or UserNotConnected.

        :param ctx: The context.
        :type ctx: commands.Context
        :return: The voice channel it joined.
        :rtype: Optional[discord.VoiceChannel]
        """

        if ctx.voice_client and ctx.voice_client.is_connected():
            await self.call_event(
                "on_music_error",
                ctx,
                AlreadyConnected("Client is already connected to a voice channel"),
            )
            return

        if not ctx.author.voice:
            await self.call_event(
                "on_music_error",
                ctx,
                UserNotConnected("User is not connected to a voice channel"),
            )
            return

        channel = ctx.author.voice.channel
        await channel.connect(
            cls=LavalinkPlayer
            if self.type == ManagerType.LAVALINK
            else discord.VoiceClient
        )
        return channel

    @ensure_connection()
    async def leave(self, ctx: commands.Context) -> Optional[discord.VoiceChannel]:
        """
        |coro|

        Leaves the voice channel in ctx.

        :param ctx: The context.
        :type ctx: commands.Context
        :return: The voice channel it left.
        :rtype: Optional[discord.VoiceChannel]
        """

        if ctx.guild.id in self.queue:
            self.queue[ctx.guild.id].cleanup()
            del self.queue[ctx.guild.id]

        await maybe_coroutine(ctx.voice_client.stop)

        channel = ctx.voice_client.channel
        await ctx.voice_client.disconnect(force=True)
        return channel

    @ensure_connection(check_queue=True)
    async def now_playing(self, ctx: commands.Context) -> Optional[Player]:
        """
        |coro|

        Returns the currently playing player.

        :param ctx: The context.
        :type ctx: commands.Context
        :return: The currently playing player.
        :rtype: Optional[Player]
        """

        now_playing = self.queue[ctx.guild.id].now_playing
        if not ctx.voice_client.is_playing() and not ctx.voice_client.is_paused():
            await self.call_event(
                "on_music_error",
                ctx,
                NotPlaying("Client is not playing anything currently"),
            )

        return now_playing

    @ensure_connection(check_playing=True, check_queue=True)
    async def queueloop(self, ctx: commands.Context) -> Optional[bool]:
        """
        |coro|

        Toggles the queue loop.

        :param ctx: The context
        :type ctx: commands.Context
        :return: A bool indicating if the queue loop is now enabled or disabled.
        :rtype: Optional[bool]
        """

        queue = self.queue[ctx.guild.id]

        queue.loop = (
            Loops.QUEUE_LOOP
            if self.queue[ctx.guild.id].loop != Loops.QUEUE_LOOP
            else Loops.NO_LOOP
        )

        if queue.loop == Loops.QUEUE_LOOP:
            queue.queue_loop_start = queue.pos

        return queue.loop == Loops.QUEUE_LOOP

    @ensure_connection(check_playing=True, check_queue=True)
    async def shuffle(self, ctx: commands.Context) -> Optional[bool]:
        """
        |coro|

        Toggles the shuffle feature.

        :param commands.Context ctx: The context
        :return: A bool indicating if the queue loop is now enabled or disabled.
        :rtype: Optional[bool]
        """

        queue = self.queue[ctx.guild.id]

        queue.shuffle = not queue.shuffle
        if queue.shuffle:
            queue.original_queue = queue.queue

            play_queue = queue.queue[queue.pos + 1 :]
            shuffled_queue = random.sample(play_queue, len(play_queue))
            queue.queue = (
                queue.queue[: queue.pos] + [queue.now_playing] + shuffled_queue
            )

        return queue.shuffle

    @ensure_connection(check_playing=True, check_queue=True)
    async def autoplay(self, ctx: commands.Context) -> Optional[bool]:
        """
        |coro|

        Toggles the autoplay feature.

        :param commands.Context ctx: The context
        :return: A bool indicating if autoplay is now enabled or disabled.
        :rtype: Optional[bool]
        """

        self.queue[ctx.guild.id].autoplay = not self.queue[ctx.guild.id].autoplay
        return self.queue[ctx.guild.id].autoplay

    @ensure_connection(check_playing=True, check_queue=True)
    async def loop(self, ctx: commands.Context) -> Optional[bool]:
        """
        |coro|

        Toggles the loop.

        :param ctx: The context
        :type ctx: commands.Context
        :return: A bool indicating if the loop is now enabled or disabled.
        :rtype: Optional[bool]
        """

        self.queue[ctx.guild.id].loop = (
            Loops.LOOP if self.queue[ctx.guild.id].loop != Loops.LOOP else Loops.NO_LOOP
        )
        return self.queue[ctx.guild.id].loop == Loops.LOOP

    @ensure_connection(check_queue=True)
    async def get_queue(self, ctx: commands.Context) -> Optional[QueueManager]:
        """
        |coro|

        Returns the queue of ctx.

        :param ctx: The context.
        :type ctx: commands.Context
        :return: The queue.
        :rtype: Optional[QueueManager]
        """

        return self.queue[ctx.guild.id]

    @staticmethod
    def parse_duration(duration: Union[str, float], hour_format: bool = True) -> str:
        """
        |coro|

        Returns parsed duration.

        :param bool hour_format: A bool indicating if the parse should contain hours.
        :param duration: The duration.
        :type duration: Union[str, float]
        :param duration: Format Hours.
        :type duration: bool
        :return: The parsed duration.
        :rtype: str
        """

        if duration == "LIVE":
            return duration

        time_format = "%H:%M:%S" if hour_format else "%M:%S"

        return time.strftime(time_format, time.gmtime(round(duration)))

    @ensure_connection(check_queue=True)
    async def move(
        self, ctx: commands.Context, player_index: int, new_index: int
    ) -> Optional[Player]:
        """

        :param player_index: The index of the player that you want to move.
        :param new_index: The index you want to move the player to.
        :param ctx: Context to fetch the queue from
        :return: The player object that was moved
        :rtype: Optional[Player]
        """

        queue = await self.get_queue(ctx)
        player_index += queue.pos
        new_index += queue.pos

        if new_index > len(queue.queue) or player_index > len(queue.queue):
            return await self.call_event(
                "on_music_error", ctx, InvalidSkipIndex("Skip index is invalid")
            )

        player = queue.remove(player_index)
        queue.queue.insert(new_index, player)
        return player


================================================
FILE: discordSuperUtils/music/player.py
================================================
from __future__ import annotations

import asyncio
from typing import Optional, TYPE_CHECKING, List, Iterable

if TYPE_CHECKING:
    from ..youtube import YoutubeClient
    import discord


__all__ = ("Player",)


class Player:
    """
    Represents a music player.
    """

    __slots__ = (
        "data",
        "title",
        "stream_url",
        "url",
        "start_timestamp",
        "last_pause_timestamp",
        "duration",
        "requester",
        "source",
        "autoplayed",
        "used_query",
        "youtube_dl",
    )

    def __init__(
        self,
        requester: Optional[discord.Member],
        used_query: str,
        title: str,
        stream_url: str,
        url: str,
        duration: int,
        data: dict,
        youtube_dl: bool = False,
    ):
        self.source = None
        self.data = data
        self.requester = requester
        self.title = title
        self.stream_url = stream_url
        self.url = url
        self.used_query = used_query

        self.autoplayed = False
        self.start_timestamp = 0
        self.last_pause_timestamp = 0

        self.youtube_dl = youtube_dl
        self.duration = duration if duration != 0 else "LIVE"

    def __str__(self):
        return self.title

    def __repr__(self):
        return f"<{self.__class__.__name__} requester={self.requester}, title={self.title}, duration={self.duration}>"

    @staticmethod
    def _get_stream_url(player: dict) -> Optional[str]:
        """
        Returns the stream url of a player.

        :param dict player: The player.
        :return: The stream url.
        :rtype: Optional[str]
        """

        if "adaptiveFormats" not in player["streamingData"]:
            return None

        stream_urls = [
            x
            for x in sorted(
                player["streamingData"]["adaptiveFormats"],
                key=lambda x: x.get("averageBitrate", 0),
                reverse=True,
            )
            if "audio" in x["mimeType"] and "opus" not in x["mimeType"]
        ]

        return player["streamingData"].get("hlsManifestUrl") or stream_urls[0]["url"]

    @classmethod
    async def make_multiple_players(
        cls,
        youtube: YoutubeClient,
        used_query: str,
        songs: Iterable[str],
        requester: Optional[discord.Member],
    ) -> List[Player]:
        """
        |coro|

        Returns a list of players from a iterable of queries.

        :param str used_query: The used query.
        :param YoutubeClient youtube: The youtube client.
        :param requester: The requester.
        :type requester: Optional[discord.Member]
        :param songs: The queries.
        :type songs: Iterable[str]
        :return: The list of created players.
        :rtype: List[Player]
        """

        tasks = [cls.fetch_song(youtube, song, playlist=False) for song in songs]

        songs = await asyncio.gather(*tasks)

        return [cls.create_player(requester, used_query, x) for x in songs if x]

    @classmethod
    async def get_similar_videos(
        cls, video_id: str, youtube: YoutubeClient
    ) -> List[Player]:
        """
        |coro|

        Creates similar videos related to the video id.

        :param str video_id: The video id
        :param YoutubeClient youtube: The youtube client.
        :return: The list of similar players
        :rtype: List[Player]
        """

        similar_video = await youtube.get_similar_videos(video_id)
        players = await cls.make_players(
            youtube, f"https://youtube.com/watch/?v={similar_video[0]}", None, False
        )
        for player in players:
            player.autoplayed = True

        return players

    @staticmethod
    async def fetch_data(
        youtube: YoutubeClient, query: str, playlist: bool = True
    ) -> List[dict]:
        """
        |coro|

        Fetches the youtube data of the query.

        :param YoutubeClient youtube: The youtube client.
        :param bool playlist: Indicating if it should fetch playlists.
        :param str query: The query.
        :return: The youtube data.
        :rtype: Optional[dict]
        """

        return [
            x
            for x in await youtube.get_videos(
                await youtube.get_query_id(query), playlist
            )
            if "streamingData" in x
        ]

    @classmethod
    async def fetch_song(
        cls, youtube: YoutubeClient, query: str, playlist: bool = True
    ) -> List[dict]:
        """
        |coro|

        Fetches the song's or playlist's data.
        Will return the first song in the playlist if playlist is False.

        :param YoutubeClient youtube: The youtube client.
        :param query: The query.
        :type query: str
        :param playlist: A bool indicating if the function should fetch playlists or get the first video.
        :type playlist: bool
        :return: The list of songs.
        :rtype: List[dict]
        """

        data = await cls.fetch_data(youtube, query, playlist)
        if not data:
            return []

        players = []
        for player in data:
            player["url"] = url = cls._get_stream_url(player)

            if url:
                players.append(
                    {x: y for x, y in player.items() if x in ["url", "videoDetails"]}
                )

        return players

    @classmethod
    def create_player(
        cls, requester: discord.Member, query: str, player: dict
    ) -> Player:
        if isinstance(player, list):
            player = player[0]

        return cls(
            requester,
            query,
            player["videoDetails"]["title"],
            player.get("url"),
            "https://youtube.com/watch/?v=" + player["videoDetails"]["videoId"],
            int(player["videoDetails"]["lengthSeconds"]),
            data=player,
        )

    @classmethod
    async def make_players(
        cls,
        youtube: YoutubeClient,
        query: str,
        requester: Optional[discord.Member],
        playlist: bool = True,
    ) -> List[Player]:
        """
        |coro|

        Returns a list of players from the query.

        :param YoutubeClient youtube: The youtube client.
        :param Optional[discord.Member] requester: The song requester.
        :param query: The query.
        :type query: str
        :param playlist: A bool indicating if the function should fetch playlists or get the first video.
        :type playlist: bool
        :return: The list of created players.
        :rtype: List[Player]
        """

        return [
            cls.create_player(requester, query, player)
            for player in await cls.fetch_song(youtube, query, playlist)
        ]


================================================
FILE: discordSuperUtils/music/playlist.py
================================================
from __future__ import annotations

from dataclasses import dataclass
from typing import Dict, List, Any, TYPE_CHECKING, Union, Optional
from .enums import PlaylistType

if TYPE_CHECKING:
    import discord
    from .music import MusicManager

__slots__ = ("SpotifyTrack", "YoutubeAuthor", "Playlist", "UserPlaylist")


@dataclass
class SpotifyTrack:
    """
    Represents a spotify track.
    """

    name: str
    authors: List[str]

    def __str__(self):
        return f"{self.name} by {self.authors[0]}"

    @classmethod
    def from_dict(cls, dictionary: Dict[str, Any]) -> SpotifyTrack:
        """
        Creates a Spotify track from the dictionary.

        :param Dict[str, Any] dictionary: The dictionary.
        :return: The spotify track.
        :rtype: SpotifyTrack
        """

        return cls(
            dictionary["track"]["name"],
            [artist["name"] for artist in dictionary["track"]["artists"]],
        )


@dataclass
class YoutubeAuthor:
    """
    Represents a YouTube author / channel.
    """

    name: str
    id: str

    @classmethod
    def from_dict(cls, dictionary: Dict[str, str]) -> YoutubeAuthor:
        """
        Creates a YouTube author from the dictionary.

        :param Dict[str, str] dictionary: The dictionary.
        :return: The YouTube author.
        :rtype: YoutubeAuthor
        """

        return cls(dictionary["name"], dictionary["id"])


@dataclass
class Playlist:
    """
    Represents a playlist.
    Supports Spotify and YouTube.
    """

    title: str
    author: Optional[YoutubeAuthor]
    songs: List[Union[str, SpotifyTrack]]
    url: str
    type: PlaylistType

    @classmethod
    def from_youtube_dict(cls, dictionary: Dict[str, Any]) -> Playlist:
        """
        Creates a playlist object from the YouTube dictionary

        :param Dict[str, Any] dictionary: The YouTube dictionary.
        :return: The playlist.
        :rtype: Playlist
        """

        return cls(
            dictionary["title"],
            YoutubeAuthor.from_dict(dictionary["channel"]),
            dictionary["songs"],
            f"https://www.youtube.com/watch?v={dictionary['songs'][0]}&list={dictionary['playlistId']}",
            PlaylistType.YOUTUBE,
        )

    @classmethod
    def from_spotify_dict(cls, dictionary: Dict[str, Any]) -> Playlist:
        """
        Creates a playlist object from the Spotify dictionary

        :param Dict[str, Any] dictionary: The spotify dictionary.
        :return: The playlist.
        :rtype: Playlist
        """

        return cls(
            dictionary["name"],
            None,
            [SpotifyTrack.from_dict(track) for track in dictionary["tracks"]],
            dictionary["url"],
            PlaylistType.SPOTIFY,
        )


@dataclass
class UserPlaylist:
    """
    Represents a playlist stored in the database.
    """

    music_manager: MusicManager
    owner: discord.User
    id: str
    playlist: Playlist

    async def delete(self) -> None:
        """
        |coro|

        Deletes the playlist from the database.

        :return: None
        :rtype: None
        """

        await self.music_manager.database.delete(
            self.music_manager.tables["playlists"],
            {"user": self.owner.id, "id": self.id},
        )


================================================
FILE: discordSuperUtils/music/queue.py
================================================
from __future__ import annotations

from typing import List, TYPE_CHECKING, Union, Any

from .enums import Loops

if TYPE_CHECKING:
    from ..youtube import YoutubeClient
    import discord
    from .player import Player


__all__ = ("QueueManager",)


class QueueManager:
    """
    Represents a queue manager that manages a queue.
    """

    __slots__ = (
        "queue",
        "volume",
        "pos",
        "loop",
        "autoplay",
        "shuffle",
        "vote_skips",
        "played_history",
        "queue_loop_start",
        "original_queue",
    )

    def __init__(self, volume: float, queue: List[Player]):
        self.pos = -1
        self.queue = queue
        self.volume = volume
        self.autoplay = False
        self.shuffle = False
        self.queue_loop_start = 0
        self.loop = Loops.NO_LOOP
        self.vote_skips = []
        self.played_history: List[Player] = []
        self.original_queue: List[Player] = []

    async def get_next_player(self, youtube: YoutubeClient) -> Player:
        """
        |coro|

        Returns the next player that should be played from the queue.

        :param YoutubeClient youtube: The youtube client.
        :return: The player.
        :rtype: Player
        """

        if self.loop != Loops.LOOP:
            self.pos += 1

        if self.loop == Loops.LOOP:
            player = self.now_playing

        elif self.loop == Loops.QUEUE_LOOP:
            if self.is_finished():
                self.pos = self.queue_loop_start

            player = self.queue[self.pos]

        else:
            if not self.queue and self.autoplay:
                last_video_id = self.played_history[-1].data["videoDetails"]["videoId"]
                player = (await Player.get_similar_videos(last_video_id, youtube))[0]

            else:
                player = self.queue[self.pos]

        return player

    def is_finished(self) -> bool:
        """
        Returns a boolean representing if the queue is finished.

        :return: A boolean representing if the queue is finished.
        :rtype: bool
        """

        return self.pos >= len(self.queue)

    @property
    def player_queue(self) -> List[Player]:
        """
        Returns the player queue.

        :return: The list of players
        :rtype: List[Player]
        """

        return self.queue[self.pos + 1 :]

    @property
    def now_playing(self) -> Player:
        """
        Returns the currently playing song.

        :return: The currently playing song.
        :rtype: Player
        """

        return self.queue[self.pos]

    @property
    def history(self) -> List[Player]:
        """
        Returns the player history.

        :return: The history.
        :rtype: List[Player]
        """

        return self.queue[: self.pos]

    def add(self, player: Player) -> None:
        """
        Adds a player to the queue.

        :param player: The player to add.
        :type player: Player
        :return: None
        :rtype: None
        """

        self.queue.append(player)

    def clear(self) -> None:
        """
        Clears the queue.

        :return: None
        :rtype: None
        """

        self.queue = self.queue[: self.pos + 1]

    def remove(self, index: int) -> Union[Player, Any]:
        """
        Removes and element from the queue at the specified index, and returns the element's value.

        :param index: The index.
        :type index: int
        :return: The element's value
        :rtype: Union[Player, Any]
        """

        return self.queue.pop(index)

    def remove_member(self, member: discord.Member) -> List[Player]:
        """
        Removes the member from the queue.

        :param discord.Member member: The member to remove from the queue.
        :return: The players removed.
        :rtype: None
        """

        removed_players = []
        for player in self.player_queue:
            if player.requester == member:
                removed_players.append(player)
                self.queue.remove(player)

        return removed_players

    def cleanup(self):
        """
        Clears the queue.

        :return: None
        :rtype: None
        """

        self.clear()
        self.history.clear()
        del self.played_history
        del self.queue


================================================
FILE: discordSuperUtils/music/utils.py
================================================
from __future__ import annotations

from typing import TYPE_CHECKING, Optional

from .playlist import Playlist
from .constants import *

if TYPE_CHECKING:
    from ..spotify import SpotifyClient
    from ..youtube import YoutubeClient


async def get_playlist(
    spotify: SpotifyClient, youtube: YoutubeClient, url: str
) -> Optional[Playlist]:
    if SPOTIFY_RE.match(url) and spotify:
        spotify_info = await spotify.fetch_full_playlist(url)
        return Playlist.from_spotify_dict(spotify_info) if spotify_info else None

    playlist_info = await youtube.get_playlist_information(
        await youtube.get_query_id(url)
    )
    return Playlist.from_youtube_dict(playlist_info) if playlist_info else None


================================================
FILE: discordSuperUtils/mute.py
================================================
from __future__ import annotations

import asyncio
from datetime import datetime
from typing import TYPE_CHECKING, Union, Optional, List, Any, Dict

import discord
import discord.utils

from .base import DatabaseChecker
from .punishments import Punisher

if TYPE_CHECKING:
    from discord.ext import commands
    from .punishments import Punishment


__all__ = ("AlreadyMuted", "MuteManager")


class AlreadyMuted(Exception):
    """Raises an error when a user is already muted."""


class MuteManager(DatabaseChecker, Punisher):
    """
    A MuteManager that handles mutes for guilds.
    """

    __slots__ = ("bot",)

    def __init__(self, bot: commands.Bot, muted_role_name: str = "Muted") -> None:
        super().__init__(
            [
                {
                    "guild": "snowflake",
                    "member": "snowflake",
                    "timestamp_of_mute": "snowflake",
                    "timestamp_of_unmute": "snowflake",
                    "reason": "string",
                }
            ],
            ["mutes"],
        )
        self.bot = bot
        self.muted_role_name = muted_role_name

        self.add_event(self.on_database_connect)

    async def on_database_connect(self):
        self.bot.loop.create_task(self.__check_mutes())
        self.bot.add_listener(self.on_member_join)

    @DatabaseChecker.uses_database
    async def get_muted_members(self) -> List[Dict[str, Any]]:
        """
        |coro|

        This function returns all the members that are supposed to be unmuted but are muted.

        :return: The unmuted members.
        :rtype: List[Dict[str, Any]]
        """

        return [
            x
            for x in await self.database.select(self.tables["mutes"], [], fetchall=True)
            if x["timestamp_of_unmute"] <= datetime.utcnow().timestamp()
        ]

    async def on_member_join(self, member: discord.Member) -> None:
        """
        |coro|

        The on_member_join event callback.
        Used so the member cant leave the guild, join back and be unmuted.

        :param member: The member that joined.
        :type member: discord.Member
        :return: None
        :rtype: None
        """

        self._check_database()  # Not using the decorator as it breaks the coroutine check

        muted_members = [
            x
            for x in await self.database.select(
                self.tables["mutes"],
                ["timestamp_of_unmute", "member"],
                {"guild": member.guild.id, "member": member.id},
                fetchall=True,
            )
            if x["timestamp_of_unmute"] > datetime.utcnow().timestamp()
        ]

        if any([muted_member["member"] == member.id for muted_member in muted_members]):
            muted_role = discord.utils.get(
                member.guild.roles, name=self.muted_role_name
            )

            if muted_role:
                await member.add_roles(muted_role)

    async def __check_mutes(self) -> None:
        """
        |coro|

        A loop that makes sure the members are unmuted when they are supposed to.

        :return: None
        :rtype: None
        """

        await self.bot.wait_until_ready()

        while not self.bot.is_closed():
            for muted_member in await self.get_muted_members():
                guild = self.bot.get_guild(muted_member["guild"])

                if guild is None:
                    continue

                member = guild.get_member(muted_member["member"])

                if await self.unmute(member):
                    await self.call_event("on_unmute", member, muted_member["reason"])

            await asyncio.sleep(300)

    async def punish(
        self, ctx: commands.Context, member: discord.Member, punishment: Punishment
    ) -> None:
        try:
            await self.mute(member)
        except discord.errors.Forbidden as e:
            raise e
        else:
            await self.call_event("on_punishment", ctx, member, punishment)

    @staticmethod
    async def ensure_permissions(
        guild: discord.Guild, muted_role: discord.Role
    ) -> None:
        """
        |coro|

        This function loops through the guild's channels and ensures the muted_role is not allowed to
        send messages or speak in that channel.

        :param guild: The guild to get the channels from.
        :type guild: discord.Guild
        :param muted_role: The muted role.
        :type muted_role: discord.Role
        :return: None
        """

        channels_to_mute = [
            channel
            for channel in guild.channels
            if channel.overwrites_for(muted_role).send_messages is not False
        ]
        # Now, you might say what the heck, why don't you test if the value is True instead of checking if it
        # is not False? I am doing it this way because permissions have 3 values,
        # None, True and False.
        # Now, lets say we have a permission that is set to None, if i test it for a False value, (if not value) it will
        # return False which is incorrect and it should return True.

        await asyncio.gather(
            *[
                channel.set_permissions(muted_role, send_messages=False, speak=False)
                for channel in channels_to_mute
            ]
        )

    async def __handle_unmute(
        self, time_of_mute: Union[int, float], member: discord.Member, reason: str
    ) -> None:
        """
        |coro|

        A function that handles the member's unmute that runs separately from mute so it wont be blocked.

        :param time_of_mute: The time until the member's unmute timestamp.
        :type time_of_mute: Union[int, float]
        :param member: The member to unmute.
        :type member: discord.Member
        :param reason: The reason of the mute.
        :type reason: str
        :return: None
        """

        await asyncio.sleep(time_of_mute)

        if await self.unmute(member):
            await self.call_event("on_unmute", member, reason)

    @DatabaseChecker.uses_database
    async def mute(
        self,
        member: discord.Member,
        reason: str = "No reason provided.",
        time_of_mute: Union[int, float] = 0,
    ) -> None:
        """
        |coro|

        Mutes a member.

        :raises: AlreadyMuted: The member is already muted.
        :param member: The member to mute.
        :type member: discord.Member
        :param reason: The reason of the mute.
        :type reason: str
        :param time_of_mute: The time of mute.
        :type time_of_mute: Union[int, float]
        :return: None,
        :rtype: None
        """

        muted_role = discord.utils.get(member.guild.roles, name=self.muted_role_name)
        if not muted_role:
            muted_role = await member.guild.create_role(
                name="Muted",
                permissions=discord.Permissions(send_messages=False, speak=False),
            )

        if muted_role in member.roles:
            raise AlreadyMuted(f"{member} is already muted.")

        await member.add_roles(muted_role, reason=reason)

        self.bot.loop.create_task(self.ensure_permissions(member.guild, muted_role))

        if time_of_mute <= 0:
            return

        await self.database.insert(
            self.tables["mutes"],
            {
                "guild": member.guild.id,
                "member": member.id,
                "timestamp_of_mute": datetime.utcnow().timestamp(),
                "timestamp_of_unmute": datetime.utcnow().timestamp() + time_of_mute,
                "reason": reason,
            },
        )

        self.bot.loop.create_task(self.__handle_unmute(time_of_mute, member, reason))

    @DatabaseChecker.uses_database
    async def unmute(self, member: discord.Member) -> Optional[bool]:
        """
        |coro|

        Unmutes a member.

        :param member: The member to unmute.
        :type member: discord.Member
        :rtype: Optional[bool]
        :return: A bool indicating if the unmute was successful
        """

        await self.database.delete(
            self.tables["mutes"], {"guild": member.guild.id, "member": member.id}
        )
        muted_role = discord.utils.get(member.guild.roles, name=self.muted_role_name)
        if not muted_role:
            return

        if muted_role not in member.roles:
            return

        await member.remove_roles(muted_role)
        return True


================================================
FILE: discordSuperUtils/paginator.py
================================================
from __future__ import annotations

import asyncio
from math import ceil
from typing import TYPE_CHECKING

import discord

if TYPE_CHECKING:
    from datetime import datetime

__all__ = ("generate_embeds", "EmojiError", "PageManager", "ButtonsPageManager")


def generate_embeds(
    list_to_generate,
    title,
    description,
    fields=25,
    color=0xFF0000,
    string_format="{}",
    footer: str = "",
    display_page_in_footer=False,
    timestamp: datetime = None,
    page_format: str = "(Page {}/{})",
):
    num_of_embeds = ceil((len(list_to_generate) + 1) / fields)

    embeds = []

    for i in range(1, num_of_embeds + 1):
        embeds.append(
            discord.Embed(
                title=title
                if display_page_in_footer
                else f"{title} {page_format.format(i, num_of_embeds)}",
                description=description,
                color=color,
                timestamp=timestamp,
            ).set_footer(
                text=f"{footer} {page_format.format(i, num_of_embeds)}"
                if display_page_in_footer
                else footer
            )
        )

    embed_index = 0
    for index, element in enumerate(list_to_generate):
        embeds[embed_index].add_field(
            name=f"**{index + 1}.**", value=string_format.format(element), inline=False
        )

        if (index + 1) % fields == 0:
            embed_index += 1

    return embeds


class ButtonError(Exception):
    pass


class EmojiError(Exception):
    pass


class ButtonsPageManager:
    __slots__ = (
        "ctx",
        "messages",
        "timeout",
        "buttons",
        "public",
        "index",
        "button_color",
    )

    def __init__(
        self,
        ctx,
        messages,
        timeout=60,
        buttons=None,
        public=False,
        index=0,
        button_color=None,
    ):
        self.ctx = ctx
        self.messages = messages
        self.timeout = timeout
        self.buttons = buttons if buttons is not None else ["⏪", "◀️", "▶️", "⏩"]
        self.public = public
        self.index = index
        self.button_color = button_color

    async def run(self):
        if len(self.buttons) != 4:
            raise ButtonError(f"Passed {len(self.buttons)} buttons when 4 are needed.")

        self.index = 0 if not -1 < self.index < len(self.messages) else self.index

        from discord_components import ActionRow, Button, ButtonStyle, DiscordComponents

        DiscordComponents(self.ctx.bot)

        components = ActionRow(
            [
                Button(
                    style=self.button_color or ButtonStyle.red,
                    label=button,
                    custom_id=button,
                )
                for button in self.buttons
            ]
        )

        message_to_send = self.messages[self.index]
        # message_to_send must be of type embed, sadly, discord_components breaks the Messageable.send method
        # And breaks the file parameter, too
        message = await self.ctx.send(embed=message_to_send, components=components)

        while True:
            try:
                interaction = await self.ctx.bot.wait
gitextract_eqyr9a2b/

├── .gitignore
├── LICENSE
├── MANIFEST.in
├── README.md
├── discordSuperUtils/
│   ├── __init__.py
│   ├── antispam.py
│   ├── ban.py
│   ├── base.py
│   ├── birthday.py
│   ├── client.py
│   ├── commandhinter.py
│   ├── convertors.py
│   ├── database.py
│   ├── economy.py
│   ├── fivem.py
│   ├── imaging.py
│   ├── infractions.py
│   ├── invitetracker.py
│   ├── kick.py
│   ├── leveling.py
│   ├── messagefilter.py
│   ├── modmail.py
│   ├── music/
│   │   ├── __init__.py
│   │   ├── constants.py
│   │   ├── enums.py
│   │   ├── exceptions.py
│   │   ├── lavalink/
│   │   │   ├── __init__.py
│   │   │   ├── equalizer.py
│   │   │   ├── lavalink.py
│   │   │   └── player.py
│   │   ├── music.py
│   │   ├── player.py
│   │   ├── playlist.py
│   │   ├── queue.py
│   │   └── utils.py
│   ├── mute.py
│   ├── paginator.py
│   ├── prefix.py
│   ├── punishments.py
│   ├── reactionroles.py
│   ├── slash_client.py
│   ├── spotify.py
│   ├── template.py
│   ├── twitch.py
│   └── youtube.py
├── docs/
│   ├── Makefile
│   ├── conf.py
│   ├── index.rst
│   ├── installation.rst
│   ├── make.bat
│   └── source/
│       └── discordSuperUtils.rst
├── examples/
│   ├── advance_music_cog.py
│   ├── antispam.py
│   ├── birthday.py
│   ├── command_hinter.py
│   ├── database.py
│   ├── economy.py
│   ├── fivem.py
│   ├── imaging.py
│   ├── invitetracker.py
│   ├── lavalinkmusic.py
│   ├── leveling.py
│   ├── leveling_cog.py
│   ├── message_filter.py
│   ├── moderation.py
│   ├── modmail.py
│   ├── music.py
│   ├── music_cog.py
│   ├── paginator.py
│   ├── prefix.py
│   ├── reaction_roles.py
│   ├── template.py
│   └── twitch.py
├── requirements.txt
├── setup.py
└── tests/
    ├── database.py
    ├── gather.py
    ├── spotify_fetching.py
    ├── tester.py
    ├── time_converts.py
    └── youtube.py
Condensed preview — 81 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (367K chars).
[
  {
    "path": ".gitignore",
    "chars": 53,
    "preview": ".vscode/\ndist/\nvenv/\n*.sqlite\n*.egg-info\n*.pyc\n.idea/"
  },
  {
    "path": "LICENSE",
    "chars": 1078,
    "preview": "MIT License\n\nCopyright (c) 2021 koyashie07 & adam7100\n\nPermission is hereby granted, free of charge, to any person obtai..."
  },
  {
    "path": "MANIFEST.in",
    "chars": 199,
    "preview": "include README.md\ninclude LICENSE\ninclude requirements.txt\ninclude discordSuperUtils/*\ninclude discordSuperUtils/assets/..."
  },
  {
    "path": "README.md",
    "chars": 15989,
    "preview": "<h1 align=\"center\">discord-super-utils</h1>\n\n<p align=\"center\">\n  <a href=\"https://codefactor.io/repository/github/disco..."
  },
  {
    "path": "discordSuperUtils/__init__.py",
    "chars": 1622,
    "preview": "from .antispam import SpamDetectionGenerator, SpamManager\nfrom .ban import BanManager\nfrom .base import CogManager, ques..."
  },
  {
    "path": "discordSuperUtils/antispam.py",
    "chars": 4142,
    "preview": "from __future__ import annotations\n\nfrom abc import ABC, abstractmethod\nfrom datetime import timedelta\nfrom difflib impo..."
  },
  {
    "path": "discordSuperUtils/ban.py",
    "chars": 6297,
    "preview": "from __future__ import annotations\n\nimport asyncio\nfrom datetime import datetime\nfrom typing import TYPE_CHECKING, Union..."
  },
  {
    "path": "discordSuperUtils/base.py",
    "chars": 13700,
    "preview": "from __future__ import annotations\n\nimport asyncio\nimport dataclasses\nimport inspect\nimport logging\nfrom dataclasses imp..."
  },
  {
    "path": "discordSuperUtils/birthday.py",
    "chars": 10200,
    "preview": "from __future__ import annotations\n\nimport asyncio\nfrom dataclasses import dataclass\nfrom datetime import datetime, time..."
  },
  {
    "path": "discordSuperUtils/client.py",
    "chars": 3442,
    "preview": "from __future__ import annotations\n\nimport asyncio\nimport logging\nimport os\nimport time\nfrom typing import Optional, TYP..."
  },
  {
    "path": "discordSuperUtils/commandhinter.py",
    "chars": 4066,
    "preview": "from abc import ABC, abstractmethod\nfrom difflib import SequenceMatcher\nfrom typing import List, Union, Optional\n\nimport..."
  },
  {
    "path": "discordSuperUtils/convertors.py",
    "chars": 1473,
    "preview": "from typing import Optional, Union\n\nfrom discord.ext import commands\n\n\ndef isfloat(string: str) -> bool:\n    \"\"\"\n    Thi..."
  },
  {
    "path": "discordSuperUtils/database.py",
    "chars": 12017,
    "preview": "import asyncio\nimport sys\nfrom abc import ABC, abstractmethod\nfrom typing import Dict, Any, Optional, List, Union\n\nimpor..."
  },
  {
    "path": "discordSuperUtils/economy.py",
    "chars": 3337,
    "preview": "from __future__ import annotations\r\n\r\nfrom dataclasses import dataclass\r\n\r\nimport discord\r\nfrom typing import List, Opti..."
  },
  {
    "path": "discordSuperUtils/fivem.py",
    "chars": 2706,
    "preview": "from __future__ import annotations\n\nfrom dataclasses import dataclass\nfrom typing import Dict, List, Optional\n\nimport ai..."
  },
  {
    "path": "discordSuperUtils/imaging.py",
    "chars": 16539,
    "preview": "from __future__ import annotations\n\nimport datetime\nimport os\nimport textwrap\nimport time\nfrom enum import Enum\nfrom io..."
  },
  {
    "path": "discordSuperUtils/infractions.py",
    "chars": 4353,
    "preview": "from __future__ import annotations\n\nimport uuid\nfrom dataclasses import dataclass\nfrom datetime import datetime\nfrom typ..."
  },
  {
    "path": "discordSuperUtils/invitetracker.py",
    "chars": 5010,
    "preview": "\"\"\"\"\nIf InviteTracker is used in any way that breaks Discord TOS we, (the DiscordSuperUtils team)\nare not responsible or..."
  },
  {
    "path": "discordSuperUtils/kick.py",
    "chars": 864,
    "preview": "from __future__ import annotations\n\nfrom typing import TYPE_CHECKING\n\nimport discord\n\nfrom .base import EventManager\nfro..."
  },
  {
    "path": "discordSuperUtils/leveling.py",
    "chars": 8798,
    "preview": "from __future__ import annotations\n\nimport math\nimport time\nfrom dataclasses import dataclass\nfrom typing import Iterabl..."
  },
  {
    "path": "discordSuperUtils/messagefilter.py",
    "chars": 4235,
    "preview": "from __future__ import annotations\n\nimport re\nfrom abc import ABC, abstractmethod\nfrom datetime import timedelta\nfrom ty..."
  },
  {
    "path": "discordSuperUtils/modmail.py",
    "chars": 3933,
    "preview": "from __future__ import annotations\n\nimport discord\nfrom discord.ext import commands\nfrom typing import Union, List, Opti..."
  },
  {
    "path": "discordSuperUtils/music/__init__.py",
    "chars": 137,
    "preview": "from .exceptions import *\nfrom .playlist import *\nfrom .enums import *\nfrom .lavalink import *\nfrom .music import *\nfrom..."
  },
  {
    "path": "discordSuperUtils/music/constants.py",
    "chars": 622,
    "preview": "import re\n\nimport youtube_dl\n\nFFMPEG_OPTIONS = {\n    \"before_options\": \"-reconnect 1 -reconnect_streamed 1 -reconnect_de..."
  },
  {
    "path": "discordSuperUtils/music/enums.py",
    "chars": 263,
    "preview": "from enum import Enum\n\n\n__all__ = (\"Loops\", \"PlaylistType\", \"ManagerType\")\n\n\nclass Loops(Enum):\n    NO_LOOP = 0\n    LOOP..."
  },
  {
    "path": "discordSuperUtils/music/exceptions.py",
    "chars": 1253,
    "preview": "__all__ = (\n    \"NotPlaying\",\n    \"NotConnected\",\n    \"NotPaused\",\n    \"QueueEmpty\",\n    \"AlreadyConnected\",\n    \"Alread..."
  },
  {
    "path": "discordSuperUtils/music/lavalink/__init__.py",
    "chars": 71,
    "preview": "from .lavalink import *\nfrom .equalizer import *\nfrom .player import *\n"
  },
  {
    "path": "discordSuperUtils/music/lavalink/equalizer.py",
    "chars": 2800,
    "preview": "from __future__ import annotations\n\nfrom dataclasses import dataclass\nfrom typing import List, Dict, Any\n\n__all__ = (\"Eq..."
  },
  {
    "path": "discordSuperUtils/music/lavalink/lavalink.py",
    "chars": 4413,
    "preview": "from __future__ import annotations\n\nfrom typing import Optional, TYPE_CHECKING, Dict\n\nimport wavelink\n\nfrom .equalizer i..."
  },
  {
    "path": "discordSuperUtils/music/lavalink/player.py",
    "chars": 463,
    "preview": "from dataclasses import dataclass\n\nimport wavelink\n\nfrom .equalizer import Equalizer\n\n\n@dataclass(init=False)\nclass Lava..."
  },
  {
    "path": "discordSuperUtils/music/music.py",
    "chars": 32080,
    "preview": "from __future__ import annotations\n\nimport asyncio\nimport random\nimport time\nimport uuid\nfrom typing import Optional, TY..."
  },
  {
    "path": "discordSuperUtils/music/player.py",
    "chars": 6737,
    "preview": "from __future__ import annotations\n\nimport asyncio\nfrom typing import Optional, TYPE_CHECKING, List, Iterable\n\nif TYPE_C..."
  },
  {
    "path": "discordSuperUtils/music/playlist.py",
    "chars": 3295,
    "preview": "from __future__ import annotations\n\nfrom dataclasses import dataclass\nfrom typing import Dict, List, Any, TYPE_CHECKING,..."
  },
  {
    "path": "discordSuperUtils/music/queue.py",
    "chars": 4324,
    "preview": "from __future__ import annotations\n\nfrom typing import List, TYPE_CHECKING, Union, Any\n\nfrom .enums import Loops\n\nif TYP..."
  },
  {
    "path": "discordSuperUtils/music/utils.py",
    "chars": 720,
    "preview": "from __future__ import annotations\n\nfrom typing import TYPE_CHECKING, Optional\n\nfrom .playlist import Playlist\nfrom .con..."
  },
  {
    "path": "discordSuperUtils/mute.py",
    "chars": 8456,
    "preview": "from __future__ import annotations\n\nimport asyncio\nfrom datetime import datetime\nfrom typing import TYPE_CHECKING, Union..."
  },
  {
    "path": "discordSuperUtils/paginator.py",
    "chars": 6886,
    "preview": "from __future__ import annotations\n\nimport asyncio\nfrom math import ceil\nfrom typing import TYPE_CHECKING\n\nimport discor..."
  },
  {
    "path": "discordSuperUtils/prefix.py",
    "chars": 3191,
    "preview": "from typing import Union, Any, Tuple, Iterable\n\nimport discord\nfrom discord.ext import commands\n\nfrom .base import Datab..."
  },
  {
    "path": "discordSuperUtils/punishments.py",
    "chars": 1826,
    "preview": "from __future__ import annotations\n\nfrom abc import ABC, abstractmethod\nfrom dataclasses import dataclass\nfrom datetime..."
  },
  {
    "path": "discordSuperUtils/reactionroles.py",
    "chars": 3803,
    "preview": "from .base import DatabaseChecker\nfrom .paginator import EmojiError\n\n\nclass ReactionManager(DatabaseChecker):\n    def __..."
  },
  {
    "path": "discordSuperUtils/slash_client.py",
    "chars": 781,
    "preview": "from __future__ import annotations\n\nimport discord\nfrom discord import app_commands\nimport typing\n\nif typing.TYPE_CHECKI..."
  },
  {
    "path": "discordSuperUtils/spotify.py",
    "chars": 4532,
    "preview": "import asyncio\nfrom typing import Optional, List, Dict, Union, Any\n\nimport spotipy\nfrom spotipy import SpotifyClientCred..."
  },
  {
    "path": "discordSuperUtils/template.py",
    "chars": 23586,
    "preview": "from __future__ import annotations\n\nimport asyncio\nimport uuid\nfrom abc import ABC, abstractmethod\nfrom typing import Li..."
  },
  {
    "path": "discordSuperUtils/twitch.py",
    "chars": 7459,
    "preview": "from __future__ import annotations\n\nimport asyncio\nfrom datetime import datetime\nfrom typing import TYPE_CHECKING, List,..."
  },
  {
    "path": "discordSuperUtils/youtube.py",
    "chars": 7561,
    "preview": "import asyncio\nimport re\nfrom typing import Dict, Any, List, Optional\nfrom urllib import parse\n\nimport aiohttp\n\n__all__..."
  },
  {
    "path": "docs/Makefile",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "docs/conf.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "docs/index.rst",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "docs/installation.rst",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "docs/make.bat",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "docs/source/discordSuperUtils.rst",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "examples/advance_music_cog.py",
    "chars": 23444,
    "preview": "from typing import Optional\n\nimport discord\nfrom discord.ext import commands\n\nimport discordSuperUtils\nfrom discordSuper..."
  },
  {
    "path": "examples/antispam.py",
    "chars": 1551,
    "preview": "from typing import List\n\nimport discord\nfrom discord.ext import commands\n\nimport discordSuperUtils\n\n\nclass MySpamDetecto..."
  },
  {
    "path": "examples/birthday.py",
    "chars": 6246,
    "preview": "from datetime import datetime, timezone\n\nimport discord\nimport pytz\nfrom discord.ext import commands\n\nimport discordSupe..."
  },
  {
    "path": "examples/command_hinter.py",
    "chars": 749,
    "preview": "from typing import List\n\nfrom discord.ext import commands\n\nimport discordSuperUtils\n\n\nclass MyCommandGenerator(discordSu..."
  },
  {
    "path": "examples/database.py",
    "chars": 1338,
    "preview": "import asyncio\n\nimport aiopg\nimport aiosqlite\nfrom motor import motor_asyncio\n\nimport discordSuperUtils\n\n\nasync def data..."
  },
  {
    "path": "examples/economy.py",
    "chars": 1356,
    "preview": "import discord\nfrom discord.ext import commands\n\nimport discordSuperUtils\n\nbot = commands.Bot(command_prefix=\"-\", intent..."
  },
  {
    "path": "examples/fivem.py",
    "chars": 269,
    "preview": "import discordSuperUtils\nimport asyncio\n\n\nasync def fivem_test():\n    fivem_server = await discordSuperUtils.FiveMServer..."
  },
  {
    "path": "examples/imaging.py",
    "chars": 728,
    "preview": "import discord\nfrom discord.ext import commands\n\nimport discordSuperUtils\n\nbot = commands.Bot(command_prefix=\"-\", intent..."
  },
  {
    "path": "examples/invitetracker.py",
    "chars": 1382,
    "preview": "import discord\nfrom discord.ext import commands\n\nimport discordSuperUtils\n\nbot = commands.Bot(command_prefix=\"-\", intent..."
  },
  {
    "path": "examples/lavalinkmusic.py",
    "chars": 6892,
    "preview": "from discord.ext import commands\n\nimport discordSuperUtils\nfrom discordSuperUtils import LavalinkMusicManager\nimport dis..."
  },
  {
    "path": "examples/leveling.py",
    "chars": 2891,
    "preview": "import discord\r\nfrom discord.ext import commands\r\n\r\nimport discordSuperUtils\r\n\r\nbot = commands.Bot(command_prefix=\"-\", i..."
  },
  {
    "path": "examples/leveling_cog.py",
    "chars": 3809,
    "preview": "import discord\nfrom discord.ext import commands\n\nimport discordSuperUtils\n\nbot = commands.Bot(command_prefix=\"-\", intent..."
  },
  {
    "path": "examples/message_filter.py",
    "chars": 1303,
    "preview": "import discord\nfrom discord.ext import commands\n\nimport discordSuperUtils\n\n\nclass MyMessageGenerator(discordSuperUtils.M..."
  },
  {
    "path": "examples/moderation.py",
    "chars": 7461,
    "preview": "from datetime import datetime\n\nimport discord\n\nimport discordSuperUtils\n\nbot = discordSuperUtils.ManagerClient(\n    \"tok..."
  },
  {
    "path": "examples/modmail.py",
    "chars": 996,
    "preview": "import discord\nfrom discord.ext.commands import Bot, Context\n\nimport discordSuperUtils\nfrom discordSuperUtils import Mod..."
  },
  {
    "path": "examples/music.py",
    "chars": 10827,
    "preview": "from math import floor\r\n\r\nfrom discord.ext import commands\r\n\r\nimport discordSuperUtils\r\nfrom discordSuperUtils import Mu..."
  },
  {
    "path": "examples/music_cog.py",
    "chars": 4909,
    "preview": "import discordSuperUtils\nfrom discord.ext import commands\nfrom discordSuperUtils import MusicManager\n\n\nbot = commands.Bo..."
  },
  {
    "path": "examples/paginator.py",
    "chars": 489,
    "preview": "import discord\nfrom discord.ext import commands\n\nimport discordSuperUtils\n\nbot = commands.Bot(command_prefix=\"-\")\n\n\n@bot..."
  },
  {
    "path": "examples/prefix.py",
    "chars": 816,
    "preview": "from discord.ext import commands\n\nimport discordSuperUtils\n\nbot = commands.Bot(command_prefix=\"-\")\nPrefixManager = disco..."
  },
  {
    "path": "examples/reaction_roles.py",
    "chars": 945,
    "preview": "import discord\nfrom discord.ext import commands\n\nimport discordSuperUtils\n\nbot = commands.Bot(command_prefix=\"-\", intent..."
  },
  {
    "path": "examples/template.py",
    "chars": 2479,
    "preview": "from discord.ext import commands\n\nimport discordSuperUtils\n\nbot = commands.Bot(command_prefix=\"-\")\nTemplateManager = dis..."
  },
  {
    "path": "examples/twitch.py",
    "chars": 2796,
    "preview": "from typing import List\n\nimport discord\n\nfrom discord.ext import commands\n\nimport discordSuperUtils\n\nbot = commands.Bot(..."
  },
  {
    "path": "requirements.txt",
    "chars": 229,
    "preview": "discord~=1.7.3\naiohttp>=3.6.3\nPillow>=8.3.0\nrequests~=2.25.1\nspotipy~=2.16.1\naiosqlite>=0.17.0\nmotor>=2.5.0\naiomysql~=0...."
  },
  {
    "path": "setup.py",
    "chars": 2173,
    "preview": "from setuptools import setup\n\nf = open(\"README.md\", \"r\")\nREADME = f.read()\n\nsetup(\n    name=\"discordSuperUtils\",\n    pac..."
  },
  {
    "path": "tests/database.py",
    "chars": 2010,
    "preview": "import asyncio\n\nimport discordSuperUtils.Base\nfrom tester import Tester\n\n\nasync def get_database():\n    return discordSu..."
  },
  {
    "path": "tests/gather.py",
    "chars": 520,
    "preview": "import asyncio\n\nfrom tester import Tester\n\n\nasync def start_testing():\n    tester = Tester()\n    tester.add_test(do_task..."
  },
  {
    "path": "tests/spotify_fetching.py",
    "chars": 1381,
    "preview": "import asyncio\n\nfrom spotify_dl import spotify\n\nimport discordSuperUtils\nfrom tester import Tester\n\nclient_id = ...\nclie..."
  },
  {
    "path": "tests/tester.py",
    "chars": 3021,
    "preview": "import asyncio\nimport time\nfrom threading import Thread\n\n\nclass Test:\n    def __init__(self, func, ignored_exception, *a..."
  },
  {
    "path": "tests/time_converts.py",
    "chars": 1448,
    "preview": "import asyncio\n\nfrom discord.ext.commands import BadArgument\n\nimport discordSuperUtils\nfrom tester import Tester\n\n\nasync..."
  },
  {
    "path": "tests/youtube.py",
    "chars": 1377,
    "preview": "import asyncio\n\nimport youtube_dl as youtube_dl\n\nimport discordSuperUtils\nfrom discordSuperUtils.music import YTDL_OPTS..."
  }
]

About this extraction

This page contains the full source code of the discordsuperutils/discord-super-utils GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 81 files (337.1 KB), approximately 76.5k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!