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")
```

### 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")
```

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.