mirror of
https://github.com/Matthww/TuneBot.git
synced 2026-09-21 23:27:47 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
41a098d577 | ||
|
|
e25e7e78f8 | ||
|
|
c01e773c31 | ||
|
|
d2658578b5 | ||
|
|
d831251f13 | ||
|
|
80e699cb25 | ||
|
|
e0472bb545 | ||
|
|
62cb4d9f94 | ||
|
|
7fd7e5ffa7 | ||
|
|
765b23e8fe | ||
|
|
74f2cf6017 | ||
|
|
ffb7dc999f | ||
|
|
1b837c16a3 | ||
|
|
b92300142a | ||
|
|
19a44ea486 | ||
|
|
34cd05d421 | ||
|
|
89c2e12489 | ||
|
|
c80d17ca6f | ||
|
|
42bd63f962 | ||
|
|
20de0e8460 | ||
|
|
f1ff10dc68 | ||
|
|
de31c6b273 | ||
|
|
833bb61660 | ||
|
|
416207aa9f | ||
|
|
9f62a48bbd | ||
|
|
a4986a04fc | ||
|
|
2fb6d353f0 | ||
|
|
b702297b40 | ||
|
|
76bfd719c3 | ||
|
|
c45bd8ced3 | ||
|
|
b37c3f533a | ||
|
|
9c55bcdc36 | ||
|
|
1495d214a1 | ||
|
|
4f65522bd8 | ||
|
|
3e6ee9ecc9 | ||
|
|
8504d6abfd | ||
|
|
d2ecbbf6ae | ||
|
|
adb3fe11a6 | ||
|
|
ebb07960bf |
@@ -0,0 +1,61 @@
|
||||
server: # REST and WS server
|
||||
port: 2333
|
||||
address: 0.0.0.0
|
||||
lavalink:
|
||||
server:
|
||||
password: "youshallnotpass"
|
||||
sources:
|
||||
youtube: true
|
||||
bandcamp: true
|
||||
soundcloud: true
|
||||
twitch: true
|
||||
vimeo: true
|
||||
http: true
|
||||
local: false
|
||||
bufferDurationMs: 400 # The duration of the NAS buffer. Higher values fare better against longer GC pauses. Minimum of 40ms, lower values may introduce pauses.
|
||||
frameBufferDurationMs: 5000 # How many milliseconds of audio to keep buffered
|
||||
opusEncodingQuality: 10 # Opus encoder quality. Valid values range from 0 to 10, where 10 is best quality but is the most expensive on the CPU.
|
||||
resamplingQuality: LOW # Quality of resampling operations. Valid values are LOW, MEDIUM and HIGH, where HIGH uses the most CPU.
|
||||
trackStuckThresholdMs: 10000 # The threshold for how long a track can be stuck. A track is stuck if does not return any audio data.
|
||||
useSeekGhosting: true # Seek ghosting is the effect where whilst a seek is in progress, the audio buffer is read from until empty, or until seek is ready.
|
||||
youtubePlaylistLoadLimit: 6 # Number of pages at 100 each
|
||||
playerUpdateInterval: 5 # How frequently to send player updates to clients, in seconds
|
||||
youtubeSearchEnabled: true
|
||||
soundcloudSearchEnabled: true
|
||||
gc-warnings: true
|
||||
#ratelimit:
|
||||
#ipBlocks: ["1.0.0.0/8", "..."] # list of ip blocks
|
||||
#excludedIps: ["...", "..."] # ips which should be explicit excluded from usage by lavalink
|
||||
#strategy: "RotateOnBan" # RotateOnBan | LoadBalance | NanoSwitch | RotatingNanoSwitch
|
||||
#searchTriggersFail: true # Whether a search 429 should trigger marking the ip as failing
|
||||
#retryLimit: -1 # -1 = use default lavaplayer value | 0 = infinity | >0 = retry will happen this numbers times
|
||||
#youtubeConfig: # Required for avoiding all age restrictions by YouTube, some restricted videos still can be played without.
|
||||
#email: "" # Email of Google account
|
||||
#password: "" # Password of Google account
|
||||
#httpConfig: # Useful for blocking bad-actors from ip-grabbing your music node and attacking it, this way only the http proxy will be attacked
|
||||
#proxyHost: "localhost" # Hostname of the proxy, (ip or domain)
|
||||
#proxyPort: 3128 # Proxy port, 3128 is the default for squidProxy
|
||||
#proxyUser: "" # Optional user for basic authentication fields, leave blank if you don't use basic auth
|
||||
#proxyPassword: "" # Password for basic authentication
|
||||
|
||||
metrics:
|
||||
prometheus:
|
||||
enabled: false
|
||||
endpoint: /metrics
|
||||
|
||||
sentry:
|
||||
dsn: ""
|
||||
environment: ""
|
||||
|
||||
logging:
|
||||
file:
|
||||
path: ./logs/
|
||||
|
||||
level:
|
||||
root: INFO
|
||||
lavalink: INFO
|
||||
|
||||
logback:
|
||||
rollingpolicy:
|
||||
max-file-size: 1GB
|
||||
max-history: 30
|
||||
@@ -1,5 +1,5 @@
|
||||
default_language_version:
|
||||
python: python3.9
|
||||
python: python3.10
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.0.1
|
||||
|
||||
@@ -4,6 +4,8 @@ from typing import Any
|
||||
from typing import Dict
|
||||
from typing import List
|
||||
from typing import Sequence
|
||||
from typing import TYPE_CHECKING
|
||||
import itertools
|
||||
|
||||
import aioredis
|
||||
import discord
|
||||
@@ -21,94 +23,22 @@ from discord.ext.commands.errors import ExtensionNotFound
|
||||
from discord.ext.commands.errors import NoEntryPointError
|
||||
|
||||
from context import CustomContext
|
||||
from tunebot.redis import GlobalRedisAutoJoin
|
||||
from tunebot.redis import GlobalRedisPlaylist
|
||||
from tunebot.redis import GlobalRedisPlaylistSource
|
||||
from tunebot.redis import RedisAutoJoin
|
||||
from tunebot.redis import RedisPlaylistSource
|
||||
from utils.assets import process_colours
|
||||
from lavalink_player import CustomPlayer
|
||||
from utils.log import logger
|
||||
|
||||
|
||||
class TuneBot(commands.Bot):
|
||||
lavalink: lavalink.Client
|
||||
invite_link: str
|
||||
|
||||
def __init__(self, config: Dict[Any, Any]):
|
||||
intents = discord.Intents(
|
||||
voice_states=True, guild_messages=True, guilds=True, messages=True
|
||||
)
|
||||
|
||||
self.rpc_is_help_message = True
|
||||
self.update_status.start()
|
||||
|
||||
self.config = config
|
||||
self.initial_cog_names: List[str] = self.config.get("cogs", [])
|
||||
self.colors: Dict[str, Color] = self.process_colours(config.get("colors", []))
|
||||
|
||||
self._redis_client: Redis = aioredis.from_url(
|
||||
self.config["redis_url"], encoding="utf-8", decode_responses=True
|
||||
)
|
||||
self.invite_link: str = ""
|
||||
|
||||
slash_guilds = None
|
||||
if len(self.config["slash_command_guilds"]) > 0:
|
||||
slash_guilds = self.config["slash_command_guilds"]
|
||||
|
||||
super().__init__(
|
||||
command_prefix=self.prefix_callable,
|
||||
owner_ids=self.config["owner_ids"],
|
||||
description=self.config["info"]["description"],
|
||||
case_insensitive=False,
|
||||
fetch_offline_members=False,
|
||||
intents=intents,
|
||||
slash_commands=True,
|
||||
slash_command_guilds=slash_guilds,
|
||||
)
|
||||
|
||||
self.loop.create_task(self.async_init())
|
||||
|
||||
async def async_init(self):
|
||||
await self.load_cogs(self.initial_cog_names)
|
||||
|
||||
async def prefix_callable(self, _, msg: Message) -> List[str]:
|
||||
return commands.when_mentioned_or(*self.config["prefixes"])(self, msg)
|
||||
|
||||
async def load_cogs(self, cog_names: Sequence[str]):
|
||||
for cog in cog_names:
|
||||
try:
|
||||
self.load_extension(cog)
|
||||
print(f"Succesfully loaded extension {cog}.")
|
||||
except (
|
||||
ExtensionNotFound,
|
||||
ExtensionAlreadyLoaded,
|
||||
NoEntryPointError,
|
||||
ExtensionFailed,
|
||||
) as e:
|
||||
print(f"Failed to load extension {cog}.\n\t{e}", file=sys.stderr)
|
||||
|
||||
async def on_ready(self):
|
||||
self.invite_link = f"https://discord.com/oauth2/authorize?client_id={self.user.id}&permissions=3230720&scope=bot%20applications.commands"
|
||||
|
||||
print(f"Logged in as: {self.user}")
|
||||
print(f"Version: {discord.__version__}")
|
||||
print(f"Invite: {self.invite_link}")
|
||||
|
||||
def process_colours(self, colors: Dict[str, str]) -> Dict[str, Color]:
|
||||
colour_dict: Dict[str, Color] = {}
|
||||
for name, color in colors.items():
|
||||
colour_dict[name] = Color(int(color, 16))
|
||||
return colour_dict
|
||||
|
||||
async def get_context(self, message: Message, *, cls=CustomContext):
|
||||
return await super().get_context(message, cls=cls)
|
||||
|
||||
@tasks.loop(seconds=30)
|
||||
async def update_status(self):
|
||||
await self.wait_until_ready()
|
||||
|
||||
bot_prefix = self.config["prefixes"][0]
|
||||
if self.rpc_is_help_message:
|
||||
title = f"for {bot_prefix}connect | {bot_prefix}help"
|
||||
activity = discord.Activity(name=title, type=ActivityType.watching)
|
||||
else:
|
||||
activity = discord.Activity(name="Some song", type=ActivityType.playing)
|
||||
|
||||
self.rpc_is_help_message = not self.rpc_is_help_message
|
||||
await self.change_presence(activity=activity)
|
||||
if TYPE_CHECKING:
|
||||
from tunebot import AutoJoin
|
||||
from tunebot import PlaylistSource
|
||||
from tunebot import GlobalPlaylist
|
||||
from tunebot import GlobalPlaylistSource
|
||||
from tunebot import GlobalAutoJoin
|
||||
from tunebot.context import ContextLike
|
||||
|
||||
|
||||
config_path = "config.json"
|
||||
@@ -117,15 +47,123 @@ if len(sys.argv) > 1:
|
||||
|
||||
config = json.load(open(config_path, "r", encoding="utf-8"))
|
||||
redis_prefix = config["redis_prefix"]
|
||||
colors: Dict[str, Color] = process_colours(config["colors"])
|
||||
status_messages: Sequence[str] = config["info"]["status"]["messages"]
|
||||
status_interval: int = config["info"]["status"]["interval"]
|
||||
|
||||
|
||||
class TuneBot(commands.Bot):
|
||||
lavalink: "lavalink.Client"
|
||||
invite_link: str
|
||||
status_cycle = itertools.cycle(status_messages)
|
||||
|
||||
def __init__(self, config: Dict[Any, Any]):
|
||||
intents = discord.Intents(
|
||||
voice_states=True, guild_messages=True, guilds=True, messages=True
|
||||
)
|
||||
|
||||
self.config = config
|
||||
self.initial_cog_names: List[str] = self.config.get("cogs", [])
|
||||
|
||||
self.redis_prefix = self.config["redis_prefix"]
|
||||
self._redis_client: Redis = aioredis.from_url(
|
||||
self.config["redis_url"], encoding="utf-8", decode_responses=True
|
||||
)
|
||||
|
||||
self.global_autojoin: GlobalAutoJoin = GlobalRedisAutoJoin(
|
||||
self._redis_client,
|
||||
self.redis_prefix,
|
||||
)
|
||||
self.global_playlist: GlobalPlaylist = GlobalRedisPlaylist(
|
||||
self._redis_client,
|
||||
self.redis_prefix,
|
||||
)
|
||||
self.global_playlist_source: GlobalPlaylistSource = GlobalRedisPlaylistSource(
|
||||
self._redis_client,
|
||||
self.redis_prefix,
|
||||
)
|
||||
|
||||
self.invite_link: str = ""
|
||||
|
||||
self.colors = colors
|
||||
|
||||
super().__init__(
|
||||
command_prefix=self.prefix_callable,
|
||||
owner_ids=self.config["owner_ids"],
|
||||
description=self.config["info"]["description"],
|
||||
case_insensitive=False,
|
||||
fetch_offline_members=False,
|
||||
intents=intents,
|
||||
)
|
||||
|
||||
async def setup_hook(self) -> None:
|
||||
await self.load_cogs(self.initial_cog_names)
|
||||
self.update_status.start()
|
||||
|
||||
async def prefix_callable(self, _, msg: Message):
|
||||
logger.info(f"{self.config['prefix']=}")
|
||||
return commands.when_mentioned_or(self.config["prefix"])(self, msg)
|
||||
|
||||
async def load_cogs(self, cog_names: Sequence[str]):
|
||||
for cog in cog_names:
|
||||
try:
|
||||
await self.load_extension(cog)
|
||||
logger.info(f"Succesfully loaded extension {cog}.")
|
||||
except (
|
||||
ExtensionNotFound,
|
||||
ExtensionAlreadyLoaded,
|
||||
NoEntryPointError,
|
||||
ExtensionFailed,
|
||||
) as e:
|
||||
logger.info(f"Failed to load extension {cog}.\n\t{e}")
|
||||
|
||||
await self.tree.sync()
|
||||
|
||||
async def on_ready(self):
|
||||
self.invite_link = f"https://discord.com/oauth2/authorize?client_id={self.user.id}&permissions=3230720&scope=bot%20applications.commands"
|
||||
|
||||
logger.info(f"Logged in as: {self.user}")
|
||||
logger.info(f"Version: {discord.__version__}")
|
||||
logger.info(f"Invite: {self.invite_link}")
|
||||
|
||||
self.lavalink = self.create_lavalink(self.user.id)
|
||||
|
||||
def create_lavalink(self, user_id: int) -> "lavalink.Client":
|
||||
nodes = self.config["nodes"]
|
||||
|
||||
client: lavalink.Client = lavalink.Client(user_id, player=CustomPlayer)
|
||||
# get the lavalink nodes from the config
|
||||
for node in nodes:
|
||||
client.add_node(nodes[node]["host"], nodes[node]["port"], nodes[node]["password"], nodes[node]["region"], nodes[node]["name"])
|
||||
return client
|
||||
|
||||
async def get_context(self, message: Message, *, cls=CustomContext):
|
||||
return await super().get_context(message, cls=cls)
|
||||
|
||||
def autojoin_context(self, ctx: "ContextLike") -> "AutoJoin":
|
||||
return RedisAutoJoin(ctx)
|
||||
|
||||
def playlist_source_context(self, ctx: "ContextLike") -> "PlaylistSource":
|
||||
return RedisPlaylistSource(ctx)
|
||||
|
||||
@tasks.loop(seconds=status_interval)
|
||||
async def update_status(self):
|
||||
await self.wait_until_ready()
|
||||
name = next(self.status_cycle)
|
||||
activity = discord.Activity(name=name, type=ActivityType.listening)
|
||||
await self.change_presence(activity=activity)
|
||||
|
||||
|
||||
client = TuneBot(config)
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
import uvloop
|
||||
|
||||
uvloop.install()
|
||||
print("Succesfully initialized uvloop")
|
||||
logger.info("Succesfully initialized uvloop")
|
||||
except ModuleNotFoundError:
|
||||
pass
|
||||
|
||||
token = config.pop("token")
|
||||
TuneBot(config).run(token, reconnect=True)
|
||||
client.run(token, reconnect=True)
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
import datetime
|
||||
import time
|
||||
from typing import Optional
|
||||
|
||||
import discord
|
||||
import humanize
|
||||
import lavalink
|
||||
from discord.ext import commands
|
||||
from discord.ext import tasks
|
||||
from discord.ext.commands import Context
|
||||
|
||||
from bot import TuneBot
|
||||
from context import CustomContext
|
||||
from utils.classes import BaseCog
|
||||
from utils.database import AutoJoin
|
||||
from utils.EmbedGenerator import EmbedGenerator
|
||||
from utils.paginator import HelpPaginator
|
||||
|
||||
|
||||
class InformationCog(BaseCog, name="Information"):
|
||||
@commands.command(name="ping", aliases=["pong"])
|
||||
@commands.cooldown(rate=1, per=5, type=commands.BucketType.user)
|
||||
async def ping(self, ctx: Context):
|
||||
"""Test the latency"""
|
||||
avatar = ctx.author.avatar.with_static_format("jpeg")
|
||||
emoji = discord.utils.get(ctx.bot.emojis, name="loading")
|
||||
start = time.monotonic()
|
||||
msg = await ctx.send(
|
||||
embed=discord.Embed(description=f"{emoji} Calculating ping")
|
||||
)
|
||||
millis = (time.monotonic() - start) * 1000
|
||||
heartbeat = ctx.bot.latency * 1000
|
||||
embed = discord.Embed(color=discord.Color.blue())
|
||||
embed.add_field(
|
||||
name=":heartbeat: Heartbeat", value=f"`{heartbeat:,.2f}ms`", inline=True
|
||||
)
|
||||
embed.add_field(
|
||||
name=":file_cabinet: ACK", value=f"`{millis:,.2f}ms`", inline=True
|
||||
)
|
||||
embed.set_footer(text=f"Requested by: {ctx.author}", icon_url=f"{avatar}")
|
||||
await msg.edit(embed=embed)
|
||||
|
||||
@commands.command(name="invite")
|
||||
@commands.cooldown(rate=1, per=5, type=commands.BucketType.user)
|
||||
async def invite(self, ctx: Context):
|
||||
"""Gets the invite link"""
|
||||
await EmbedGenerator.Message(
|
||||
ctx, "Add our bot to your server:", self.bot.invite_link
|
||||
)
|
||||
|
||||
@commands.command(name="wlinfo")
|
||||
@commands.cooldown(rate=1, per=5, type=commands.BucketType.user)
|
||||
async def wlinfo(self, ctx: CustomContext):
|
||||
"""Retrieve various node/server/player information"""
|
||||
player = self.bot.lavalink.player_manager.get(ctx.guild.id)
|
||||
nodes = self.bot.lavalink.node_manager.available_nodes
|
||||
|
||||
used = humanize.naturalsize(sum([n.stats.memory_used for n in nodes]))
|
||||
total = humanize.naturalsize(sum([n.stats.memory_allocated for n in nodes]))
|
||||
free = humanize.naturalsize(sum([n.stats.memory_free for n in nodes]))
|
||||
cpu = sum([n.stats.cpu_cores for n in nodes])
|
||||
|
||||
fmt = (
|
||||
f"**Lavalink:** `{lavalink.__version__}`\n\n"
|
||||
f"Connected to `{len(self.bot.lavalink.node_manager.available_nodes)}` nodes.\n"
|
||||
#f"Best available Node `{self.bot.lavalink.node_manager.find_ideal_node().name.__repr__()}`\n"
|
||||
f"`{len(self.bot.lavalink.player_manager.players)}` players are distributed on nodes.\n"
|
||||
f"`{sum([n.stats.players for n in nodes])}` players are distributed on server.\n"
|
||||
f"`{sum([n.stats.playing_players for n in nodes])}` players are playing on server.\n\n"
|
||||
f"Server Memory: `{used}/{total}` | `({free} free)`\n"
|
||||
f"Server CPU: `{cpu}`\n\n"
|
||||
#f"Server Uptime: `{datetime.timedelta(milliseconds=node.stats.uptime)}`"
|
||||
)
|
||||
await ctx.send(fmt)
|
||||
AutoJoin.get_channels()
|
||||
|
||||
@commands.command(name="help", aliases=["about", "info"])
|
||||
@commands.cooldown(1, 1, commands.BucketType.user)
|
||||
async def about(
|
||||
self,
|
||||
ctx: Context,
|
||||
command: Optional[str] = commands.Option(
|
||||
description="Show help for a command or category"
|
||||
),
|
||||
):
|
||||
"""Retrieve a list of possible commands"""
|
||||
if command:
|
||||
entity = self.bot.get_cog(command) or self.bot.get_command(command)
|
||||
|
||||
if entity is None:
|
||||
clean = command.replace("@", "@\u200b")
|
||||
return await ctx.send(f'Command or category "{clean}" not found.')
|
||||
elif isinstance(entity, discord.ext.commands.Command):
|
||||
p = await HelpPaginator.from_command(ctx, entity)
|
||||
else:
|
||||
p = await HelpPaginator.from_cog(ctx, entity)
|
||||
return await p.paginate()
|
||||
|
||||
info = self.bot.config["info"]
|
||||
title = info["name"] + " Help"
|
||||
descr = info["description"]
|
||||
color = self.bot.colors["embed"]
|
||||
|
||||
embed = discord.Embed(color=color, title=title, description=descr)
|
||||
|
||||
display_cogs = {
|
||||
"Information": ":information_source:",
|
||||
"Music": ":musical_note:",
|
||||
"Settings": ":gear:",
|
||||
}
|
||||
|
||||
for cog_name, cog_icon in display_cogs.items():
|
||||
cog = self.bot.cogs.get(cog_name)
|
||||
if not cog:
|
||||
continue
|
||||
cogname_str = f"{cog_icon} {cog_name}"
|
||||
commands = [f"`{cmd.name}`" for cmd in cog.get_commands() if not cmd.hidden]
|
||||
commands_str = ", ".join(commands)
|
||||
embed.add_field(name=cogname_str, value=commands_str, inline=False)
|
||||
|
||||
avatar = ctx.author.avatar.with_static_format("jpeg")
|
||||
embed.set_footer(text=f"Requested by: {ctx.author}", icon_url=avatar)
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
|
||||
def setup(bot: TuneBot):
|
||||
bot.remove_command("help")
|
||||
bot.add_cog(InformationCog(bot))
|
||||
@@ -0,0 +1,17 @@
|
||||
import typing
|
||||
|
||||
from cogs.information.interactions import InfoCommands
|
||||
from utils.classes import BaseCog
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from bot import TuneBot
|
||||
|
||||
|
||||
class InformationCog(BaseCog, name="Information"):
|
||||
pass
|
||||
|
||||
|
||||
async def setup(bot: "TuneBot"):
|
||||
bot.remove_command("help")
|
||||
await bot.add_cog(InformationCog(bot))
|
||||
bot.tree.add_command(InfoCommands(bot), override=True)
|
||||
@@ -0,0 +1,64 @@
|
||||
import typing
|
||||
|
||||
import humanize
|
||||
import lavalink
|
||||
from discord import app_commands
|
||||
from discord import Interaction
|
||||
|
||||
from utils.embed import create_embed
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from bot import TuneBot
|
||||
|
||||
|
||||
class InfoCommands(app_commands.Group):
|
||||
def __init__(self, bot: "TuneBot"):
|
||||
super().__init__(name="bot", description="Some extra info")
|
||||
self.bot: "TuneBot" = bot
|
||||
|
||||
@app_commands.command(name="invite", description="I'd happily join your server")
|
||||
async def invite(self, ctx: Interaction):
|
||||
embed = create_embed(ctx.user)
|
||||
embed.title = "My invite link"
|
||||
embed.description = f"[{self.bot.invite_link}]({self.bot.invite_link})"
|
||||
await ctx.response.send_message(embed=embed)
|
||||
|
||||
@app_commands.command(
|
||||
name="stats", description="Some node/server/player information"
|
||||
)
|
||||
async def wlinfo(self, ctx: Interaction):
|
||||
embed = create_embed(ctx.user)
|
||||
nodes = self.bot.lavalink.node_manager.available_nodes
|
||||
|
||||
used = humanize.naturalsize(sum([n.stats.memory_used for n in nodes]))
|
||||
total = humanize.naturalsize(sum([n.stats.memory_allocated for n in nodes]))
|
||||
free = humanize.naturalsize(sum([n.stats.memory_free for n in nodes]))
|
||||
cpu = sum([n.stats.cpu_cores for n in nodes])
|
||||
|
||||
info = self.bot.config["info"]
|
||||
embed.title = info["name"] + " stats"
|
||||
|
||||
embed.description = (
|
||||
f"**Lavalink:** `{lavalink.__version__}`\n\n"
|
||||
f"Connected to `{len(self.bot.lavalink.node_manager.available_nodes)}` nodes.\n"
|
||||
f"`{len(self.bot.lavalink.player_manager.players)}` players are distributed on nodes.\n"
|
||||
f"`{sum([n.stats.players for n in nodes])}` players are distributed on server.\n"
|
||||
f"`{sum([n.stats.playing_players for n in nodes])}` players are playing on server.\n\n"
|
||||
f"Server Memory: `{used}/{total}` | `({free} free)`\n"
|
||||
f"Server CPU: `{cpu}`\n\n"
|
||||
)
|
||||
await ctx.response.send_message(embed=embed)
|
||||
|
||||
@app_commands.command(name="tos", description="Terms of Service")
|
||||
async def tos(self, ctx: Interaction):
|
||||
embed = create_embed(ctx.user)
|
||||
embed.title = "Terms of Service (ToS)"
|
||||
embed.description = "[https://exobot.site/static/tos.html](https://exobot.site/static/tos.html)"
|
||||
await ctx.response.send_message(embed=embed)
|
||||
|
||||
@app_commands.command(name="privacy", description="Privacy Policy")
|
||||
async def privacy(self, ctx: Interaction):
|
||||
embed = create_embed(ctx.user)
|
||||
embed.title = "Privacy Policy"
|
||||
embed.description = "[https://exobot.site/static/privacy.html](https://exobot.site/static/privacy.html)"
|
||||
await ctx.response.send_message(embed=embed)
|
||||
-307
@@ -1,307 +0,0 @@
|
||||
import asyncio
|
||||
import datetime
|
||||
import re
|
||||
from typing import Optional
|
||||
|
||||
import discord
|
||||
import lavalink
|
||||
from discord import Embed
|
||||
from discord.channel import TextChannel
|
||||
from discord.ext import commands
|
||||
from discord.ext.commands.context import Context
|
||||
from discord.ext.commands.errors import CommandError
|
||||
from lavalink.models import AudioTrack
|
||||
from lavalink.models import DefaultPlayer
|
||||
|
||||
from bot import TuneBot
|
||||
from context import CustomContext
|
||||
from utils.classes import BaseCog
|
||||
from utils.database import AutoJoin
|
||||
from utils.database import Playlist
|
||||
from utils.EmbedGenerator import EmbedGenerator
|
||||
from utils.exceptions import EmbeddedCommandException
|
||||
|
||||
url_rx = re.compile(r"https?://(?:www\.)?.+")
|
||||
|
||||
|
||||
class LavalinkVoiceClient(discord.VoiceClient):
|
||||
def __init__(self, client: discord.Client, channel: discord.abc.Connectable):
|
||||
self.client = client
|
||||
self.channel = channel
|
||||
self.lavalink = self.client.lavalink
|
||||
|
||||
async def on_voice_server_update(self, data):
|
||||
# the data needs to be transformed before being handed down to
|
||||
# voice_update_handler
|
||||
lavalink_data = {"t": "VOICE_SERVER_UPDATE", "d": data}
|
||||
await self.lavalink.voice_update_handler(lavalink_data)
|
||||
|
||||
async def on_voice_state_update(self, data):
|
||||
# the data needs to be transformed before being handed down to
|
||||
# voice_update_handler
|
||||
lavalink_data = {"t": "VOICE_STATE_UPDATE", "d": data}
|
||||
await self.lavalink.voice_update_handler(lavalink_data)
|
||||
|
||||
async def connect(self, *, timeout: float, reconnect: bool) -> None:
|
||||
"""
|
||||
Connect the bot to the voice channel and create a player_manager
|
||||
if it doesn't exist yet.
|
||||
"""
|
||||
# ensure there is a player_manager when creating a new voice_client
|
||||
self.lavalink.player_manager.create(guild_id=self.channel.guild.id)
|
||||
await self.channel.guild.change_voice_state(channel=self.channel)
|
||||
|
||||
async def disconnect(self, *, force: bool) -> None:
|
||||
"""
|
||||
Handles the disconnect.
|
||||
Cleans up running player and leaves the voice client.
|
||||
"""
|
||||
player = self.lavalink.player_manager.get(self.channel.guild.id)
|
||||
|
||||
# no need to disconnect if we are not connected
|
||||
if not force and not player.is_connected:
|
||||
return
|
||||
|
||||
# None means disconnect
|
||||
await self.channel.guild.change_voice_state(channel=None)
|
||||
|
||||
# update the channel_id of the player to None
|
||||
# this must be done because the on_voice_state_update that
|
||||
# would set channel_id to None doesn't get dispatched after the
|
||||
# disconnect
|
||||
player.channel_id = None
|
||||
self.cleanup()
|
||||
|
||||
|
||||
class Music(BaseCog):
|
||||
@commands.Cog.listener()
|
||||
async def on_ready(self):
|
||||
if not hasattr(
|
||||
self.bot, "lavalink"
|
||||
): # This ensures the client isn't overwritten during cog reloads.
|
||||
self.bot.lavalink = lavalink.Client(self.bot.user.id)
|
||||
|
||||
ll = self.bot.config["lavalink"]
|
||||
self.bot.lavalink.add_node(
|
||||
ll["host"], ll["port"], ll["password"], ll["region"], ll["name"]
|
||||
)
|
||||
|
||||
self.bot.lavalink.add_event_hook(self.track_hook)
|
||||
await self.async_init()
|
||||
|
||||
async def async_init(self):
|
||||
redis_result = await AutoJoin.get_channels(self.bot._redis_client)
|
||||
|
||||
while len(self.bot.lavalink.node_manager.available_nodes) == 0:
|
||||
await asyncio.sleep(1)
|
||||
|
||||
for guild_id, (voicechannel_id, textchannel_id) in redis_result.items():
|
||||
player = self.bot.lavalink.player_manager.create(guild_id)
|
||||
player.store("channel", textchannel_id)
|
||||
voice_channel = await self.bot.fetch_channel(voicechannel_id)
|
||||
await voice_channel.connect(cls=LavalinkVoiceClient)
|
||||
if not player.is_playing:
|
||||
await self.fill_player_queue(
|
||||
player, self.bot.config["queue_buffer_size"]
|
||||
)
|
||||
await player.play()
|
||||
|
||||
textchannel = await self.bot.fetch_channel(textchannel_id)
|
||||
await textchannel.send("Automatically joined the voice channel")
|
||||
|
||||
async def fill_player_queue(self, player: DefaultPlayer, buffer: Optional[int] = 1):
|
||||
queries = await Playlist.random(self.bot._redis_client, buffer)
|
||||
# Get the results for the query from Lavalink.
|
||||
for query in queries:
|
||||
result = await player.node.get_tracks(query)
|
||||
if not result or not result["tracks"]:
|
||||
continue
|
||||
|
||||
track = lavalink.models.AudioTrack(
|
||||
result["tracks"][0], self.bot.user.id, recommended=False
|
||||
)
|
||||
player.add(requester=self.bot.user.id, track=track)
|
||||
|
||||
async def create_track_embed(self, track: AudioTrack) -> Embed:
|
||||
embed_color = self.bot.colors["embed"]
|
||||
embed = discord.Embed(
|
||||
title=f"Now playing...",
|
||||
colour=embed_color,
|
||||
)
|
||||
embed.description = f"[{track.title}]({track.uri})"
|
||||
embed.set_thumbnail(
|
||||
url=f"https://i3.ytimg.com/vi/{track.identifier}/mqdefault.jpg"
|
||||
)
|
||||
|
||||
try:
|
||||
duration = str(datetime.timedelta(milliseconds=int(track.duration)))
|
||||
except OverflowError:
|
||||
duration = "🔴 LIVE"
|
||||
|
||||
embed.add_field(name="Duration", value=duration)
|
||||
embed.add_field(name="Author", value=track.author)
|
||||
return embed
|
||||
|
||||
def cog_unload(self):
|
||||
"""Cog unload handler. This removes any event hooks that were registered."""
|
||||
self.bot.lavalink._event_hooks.clear()
|
||||
|
||||
async def cog_before_invoke(self, ctx):
|
||||
"""Command before-invoke handler."""
|
||||
guild_check = ctx.guild is not None
|
||||
|
||||
# This is essentially the same as `@commands.guild_only()`
|
||||
# except it saves us repeating ourselves (and also a few lines).
|
||||
|
||||
if not hasattr(self.bot, "lavalink"):
|
||||
await ctx.send("Still starting please wait a moment.")
|
||||
|
||||
if guild_check:
|
||||
await self.ensure_voice(ctx)
|
||||
# Ensure that the bot and command author share a mutual voicechannel.
|
||||
|
||||
return guild_check
|
||||
|
||||
async def cog_command_error(self, ctx: CustomContext, error: CommandError):
|
||||
if isinstance(error, commands.CommandInvokeError):
|
||||
await ctx.send(error.original)
|
||||
# The above handles errors thrown in this cog and shows them to the user.
|
||||
# This shouldn't be a problem as the only errors thrown in this cog are from `ensure_voice`
|
||||
# which contain a reason string, such as "Join a voicechannel" etc. You can modify the above
|
||||
# if you want to do things differently.
|
||||
elif isinstance(error, EmbeddedCommandException):
|
||||
await error.send(ctx)
|
||||
|
||||
async def ensure_voice(self, ctx):
|
||||
"""This check ensures that the bot and command author are in the same voicechannel."""
|
||||
player = self.bot.lavalink.player_manager.create(
|
||||
ctx.guild.id, endpoint=str(ctx.guild.region)
|
||||
)
|
||||
# Create returns a player if one exists, otherwise creates.
|
||||
# This line is important because it ensures that a player always exists for a guild.
|
||||
|
||||
# Most people might consider this a waste of resources for guilds that aren't playing, but this is
|
||||
# the easiest and simplest way of ensuring players are created.
|
||||
|
||||
# These are commands that require the bot to join a voicechannel (i.e. initiating playback).
|
||||
# Commands such as volume/skip etc don't require the bot to be in a voicechannel so don't need listing here.
|
||||
should_connect = ctx.command.name in ("connect",)
|
||||
|
||||
if not ctx.author.voice or not ctx.author.voice.channel:
|
||||
# Our cog_command_error handler catches this and sends it to the voicechannel.
|
||||
# Exceptions allow us to "short-circuit" command invocation via checks so the
|
||||
# execution state of the command goes no further.
|
||||
raise commands.CommandInvokeError("Join a voicechannel first.")
|
||||
|
||||
if not player.is_connected:
|
||||
if not should_connect:
|
||||
bot_name = self.bot.config["info"]["name"]
|
||||
embed = await EmbedGenerator.Message(
|
||||
ctx,
|
||||
f"{bot_name} is not connected",
|
||||
"However you can start playing music using `/connect`",
|
||||
no_send=True,
|
||||
)
|
||||
raise EmbeddedCommandException(embed)
|
||||
|
||||
permissions = ctx.author.voice.channel.permissions_for(ctx.me)
|
||||
|
||||
if (
|
||||
not permissions.connect or not permissions.speak
|
||||
): # Check user limit too?
|
||||
raise commands.CommandInvokeError(
|
||||
"I need the `CONNECT` and `SPEAK` permissions."
|
||||
)
|
||||
|
||||
player.store("channel", ctx.channel.id)
|
||||
await ctx.author.voice.channel.connect(cls=LavalinkVoiceClient)
|
||||
else:
|
||||
if int(player.channel_id) != ctx.author.voice.channel.id:
|
||||
raise commands.CommandInvokeError("You need to be in my voicechannel.")
|
||||
|
||||
async def track_hook(self, event):
|
||||
if isinstance(event, lavalink.events.QueueEndEvent):
|
||||
# When this track_hook receives a "QueueEndEvent" from lavalink.py
|
||||
# it indicates that there are no tracks left in the player's queue.
|
||||
# To save on resources, we can tell the bot to disconnect from the voicechannel.
|
||||
guild_id = int(event.player.guild_id)
|
||||
guild = self.bot.get_guild(guild_id)
|
||||
await guild.voice_client.disconnect(force=True)
|
||||
elif isinstance(event, lavalink.events.TrackStartEvent):
|
||||
channel_id = int(event.player.fetch("channel"))
|
||||
channel: TextChannel = self.bot.get_channel(channel_id)
|
||||
embed = await self.create_track_embed(event.player.current)
|
||||
await channel.send(embed=embed)
|
||||
elif isinstance(event, lavalink.events.TrackEndEvent):
|
||||
await self.fill_player_queue(event.player, 1)
|
||||
|
||||
@commands.command(name="connect", aliases=["p", "play", "join"])
|
||||
async def play(self, ctx: CustomContext):
|
||||
"""Start the radio"""
|
||||
# Get the player for this guild from cache.
|
||||
if ctx.player.is_connected:
|
||||
await ctx.send("Already connected")
|
||||
return
|
||||
|
||||
await self.fill_player_queue(
|
||||
ctx.player, self.bot.config["queue_buffer_size"] + 1
|
||||
)
|
||||
|
||||
if not ctx.player.is_playing:
|
||||
await ctx.player.play()
|
||||
|
||||
await EmbedGenerator.Title(ctx, "*⃣ | Connected.")
|
||||
return
|
||||
|
||||
@commands.command(name="skip", aliases=["next"])
|
||||
async def skip(self, ctx: CustomContext):
|
||||
"""Skip the current song"""
|
||||
await ctx.player.skip()
|
||||
await ctx.send("Skipped current song")
|
||||
|
||||
@commands.command(name="queue")
|
||||
async def queue(self, ctx: CustomContext):
|
||||
"""Display the current radio queue"""
|
||||
embed_color = self.bot.colors["embed"]
|
||||
embed = Embed(title="Coming Up...", colour=embed_color)
|
||||
|
||||
if len(ctx.player.queue) > 0:
|
||||
embed.description = "\n".join(
|
||||
f"{index}. [{track.title}]({track.uri})"
|
||||
for index, track in enumerate(ctx.player.queue, 1)
|
||||
)
|
||||
else:
|
||||
embed.description = "We are still determining a playlist"
|
||||
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
@commands.command(name="disconnect", aliases=["dc", "stop"])
|
||||
async def disconnect(self, ctx: CustomContext):
|
||||
"""Disconnects the radio from the channel"""
|
||||
if not ctx.author.voice or (
|
||||
ctx.player.is_connected
|
||||
and ctx.author.voice.channel.id != int(ctx.player.channel_id)
|
||||
):
|
||||
# Abuse prevention. Users not in voice channels, or not in the same voice channel as the bot
|
||||
# may not disconnect the bot.
|
||||
await EmbedGenerator.Title(ctx, "You're not in my voicechannel!")
|
||||
return
|
||||
|
||||
# Clear the queue to ensure old tracks don't start playing
|
||||
# when someone else queues something.
|
||||
ctx.player.queue.clear()
|
||||
# Stop the current track so Lavalink consumes less resources.
|
||||
await ctx.player.stop()
|
||||
# Disconnect from the voice channel.
|
||||
await ctx.voice_client.disconnect(force=True)
|
||||
await EmbedGenerator.Title(ctx, "*⃣ | Disconnected.")
|
||||
|
||||
@commands.command(name="now")
|
||||
async def now_playing(self, ctx: CustomContext):
|
||||
"""Displays information about the currently played track"""
|
||||
embed = await self.create_track_embed(ctx.player.current)
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
|
||||
def setup(bot: TuneBot):
|
||||
bot.add_cog(Music(bot))
|
||||
@@ -0,0 +1,71 @@
|
||||
import asyncio
|
||||
import typing
|
||||
|
||||
import lavalink
|
||||
from discord.channel import TextChannel
|
||||
from discord.ext import commands
|
||||
|
||||
from bot import TuneBot
|
||||
from cogs.music import helper
|
||||
from cogs.music import track_embed
|
||||
from cogs.music.interactions import MusicCommands
|
||||
from cogs.music.interactions import QUEUE_SIZE
|
||||
from cogs.music.voice_client import LavalinkVoiceClient
|
||||
from utils.classes import BaseCog
|
||||
from utils.log import logger
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from bot import TuneBot
|
||||
|
||||
|
||||
class MusicCog(BaseCog, name="Music"):
|
||||
def __init__(self, bot: TuneBot):
|
||||
super().__init__(bot)
|
||||
if not hasattr(bot, "lavalink"):
|
||||
self.bot.lavalink = self.bot.create_lavalink(bot.user.id)
|
||||
|
||||
self.bot.lavalink.add_event_hook(self.track_hook)
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_ready(self):
|
||||
while not self.is_lavalink_ready():
|
||||
await asyncio.sleep(1)
|
||||
|
||||
redis_result = await self.bot.global_autojoin.fetch_channels()
|
||||
for guild_id, (voicechannel_id, textchannel_id) in redis_result.items():
|
||||
try:
|
||||
player = self.bot.lavalink.player_manager.create(guild_id)
|
||||
player.store("channel", textchannel_id)
|
||||
voice_channel = await self.bot.fetch_channel(voicechannel_id)
|
||||
await voice_channel.connect(cls=LavalinkVoiceClient)
|
||||
if not player.is_playing:
|
||||
await helper.fill_player_queue(self.bot, player, QUEUE_SIZE)
|
||||
await player.play()
|
||||
except:
|
||||
logger.error(f"Failed to autojoin guild {guild_id}.")
|
||||
|
||||
async def track_hook(self, event: lavalink.Event):
|
||||
if isinstance(event, lavalink.events.QueueEndEvent):
|
||||
guild_id = event.player.guild_id
|
||||
guild = self.bot.get_guild(guild_id)
|
||||
await guild.voice_client.disconnect(force=True)
|
||||
elif isinstance(event, lavalink.events.TrackEndEvent):
|
||||
channel_id = int(event.player.fetch("channel"))
|
||||
channel: TextChannel = self.bot.get_channel(channel_id)
|
||||
await helper.fill_player_queue(self.bot, event.player, 1)
|
||||
event.player.append_history(event.track)
|
||||
|
||||
if event.reason == "FINISHED":
|
||||
queue = event.player.queue[1:]
|
||||
current = event.player.queue[0]
|
||||
else:
|
||||
queue = event.player.queue
|
||||
current = event.player.current
|
||||
|
||||
embed = track_embed.create_track_embed(current, queue, event.player.history)
|
||||
await channel.send(embed=embed)
|
||||
|
||||
|
||||
async def setup(bot: "TuneBot"):
|
||||
await bot.add_cog(MusicCog(bot))
|
||||
bot.tree.add_command(MusicCommands(bot), override=True)
|
||||
@@ -0,0 +1,63 @@
|
||||
import datetime
|
||||
import typing
|
||||
|
||||
import discord
|
||||
import lavalink
|
||||
|
||||
from bot import colors
|
||||
from cogs.music.voice_client import LavalinkVoiceClient
|
||||
from utils.log import logger
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from lavalink import DefaultPlayer
|
||||
from lavalink_player import CustomPlayer
|
||||
from bot import TuneBot
|
||||
|
||||
|
||||
def get_player(bot: "TuneBot", guild_id: int) -> "CustomPlayer":
|
||||
if player := bot.lavalink.player_manager.get(guild_id=guild_id):
|
||||
return player
|
||||
|
||||
return bot.lavalink.player_manager.create(guild_id)
|
||||
|
||||
|
||||
async def fill_player_queue(
|
||||
bot: "TuneBot", player: "DefaultPlayer", amount: typing.Optional[int] = 1
|
||||
):
|
||||
queries = await bot.global_playlist.pick_random(amount)
|
||||
logger.info(f"Got queries for {player.guild_id} {queries=} {amount=}")
|
||||
|
||||
for query in queries:
|
||||
result = await player.node.get_tracks(query)
|
||||
if not result or not result["tracks"]:
|
||||
continue
|
||||
|
||||
track = lavalink.models.AudioTrack(
|
||||
result["tracks"][0], bot.user.id, recommended=False
|
||||
)
|
||||
player.add(requester=bot.user.id, track=track)
|
||||
|
||||
|
||||
async def ensure_voice(
|
||||
permissions: discord.Permissions,
|
||||
player: "DefaultPlayer",
|
||||
author: discord.Member,
|
||||
voice_client: typing.Any,
|
||||
channel_id: int,
|
||||
):
|
||||
"""This ensures that the bot and command author are in the same voicechannel."""
|
||||
if not author.voice or not author.voice.channel:
|
||||
raise Exception("Join a voicechannel first.")
|
||||
|
||||
if not voice_client:
|
||||
if not permissions.connect or not permissions.speak: # Check user limit too?
|
||||
raise Exception("I need to be able to join your channel and speak")
|
||||
|
||||
player.store("channel", channel_id)
|
||||
await author.voice.channel.connect(cls=LavalinkVoiceClient)
|
||||
else:
|
||||
if voice_client.channel.id != author.voice.channel.id:
|
||||
raise Exception("You need to be in my voicechannel")
|
||||
|
||||
|
||||
__all__ = ("fill_player_queue", "get_player", "ensure_voice")
|
||||
@@ -0,0 +1,115 @@
|
||||
import typing
|
||||
|
||||
from discord import app_commands
|
||||
from discord import Interaction
|
||||
|
||||
from bot import config
|
||||
from cogs.music import helper
|
||||
from utils.embed import create_embed
|
||||
from cogs.music import track_embed
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from bot import TuneBot
|
||||
|
||||
QUEUE_SIZE = config["queue_buffer_size"]
|
||||
|
||||
|
||||
class MusicCommands(app_commands.Group):
|
||||
def __init__(self, bot: "TuneBot"):
|
||||
super().__init__(name="radio", description="Never-ending stream of good vibes")
|
||||
self.bot: "TuneBot" = bot
|
||||
|
||||
@app_commands.command(name="connect", description="Start the radio")
|
||||
async def connect(self, ctx: Interaction):
|
||||
embed = create_embed(ctx.user)
|
||||
player = helper.get_player(ctx.client, ctx.guild_id)
|
||||
|
||||
try:
|
||||
await helper.ensure_voice(
|
||||
permissions=ctx.app_permissions,
|
||||
player=player,
|
||||
author=ctx.user,
|
||||
voice_client=ctx.guild.voice_client,
|
||||
channel_id=ctx.channel.id,
|
||||
)
|
||||
except Exception as exc:
|
||||
embed.title = str(exc)
|
||||
await ctx.response.send_message(embed=embed)
|
||||
return
|
||||
|
||||
if player.is_connected:
|
||||
embed.title = "Already connected."
|
||||
await ctx.response.send_message(embed=embed)
|
||||
return
|
||||
|
||||
await helper.fill_player_queue(ctx.client, player, QUEUE_SIZE)
|
||||
|
||||
if not player.is_playing:
|
||||
await player.play()
|
||||
|
||||
embed = track_embed.create_track_embed(player.current, player.queue, [])
|
||||
await ctx.response.send_message(embed=embed)
|
||||
|
||||
@app_commands.command(
|
||||
name="disconnect", description="I've had enough music for a while"
|
||||
)
|
||||
async def disconnect(self, ctx: Interaction):
|
||||
embed = create_embed(ctx.user)
|
||||
player = helper.get_player(ctx.client, ctx.guild_id)
|
||||
|
||||
if not ctx.user.voice or (
|
||||
player.is_connected and ctx.user.voice.channel.id != int(player.channel_id)
|
||||
):
|
||||
embed.title = "You're not in my voicechannel!"
|
||||
await ctx.response.send_message(embed=embed)
|
||||
return
|
||||
|
||||
# Clear the queue to ensure old tracks don't start playing
|
||||
# when someone else queues something.
|
||||
player.queue.clear()
|
||||
# Stop the current track so Lavalink consumes less resources.
|
||||
await player.stop()
|
||||
# Disconnect from the voice channel.
|
||||
await ctx.guild.voice_client.disconnect(force=True)
|
||||
embed.title = "*⃣ | Disconnected."
|
||||
await ctx.response.send_message(embed=embed)
|
||||
|
||||
@app_commands.command(
|
||||
name="current", description="Gives more details about what's currently playing."
|
||||
)
|
||||
async def current(self, ctx: Interaction):
|
||||
player = helper.get_player(ctx.client, ctx.guild_id)
|
||||
if not player.current:
|
||||
embed = create_embed(ctx.user)
|
||||
embed.title = "You're not listening to anything right now."
|
||||
await ctx.response.send_message(embed=embed)
|
||||
return
|
||||
|
||||
embed = track_embed.create_track_embed(
|
||||
player.current, player.queue, player.history
|
||||
)
|
||||
await ctx.response.send_message(embed=embed)
|
||||
|
||||
@app_commands.command(name="queue", description="See what's ahead")
|
||||
async def queue(self, ctx: Interaction):
|
||||
player = helper.get_player(ctx.client, ctx.guild_id)
|
||||
embed = create_embed(ctx.user)
|
||||
embed.title = "Coming up..."
|
||||
if len(player.queue) > 0:
|
||||
embed.description = "\n".join(
|
||||
f"{index}. [{track.title}]({track.uri})"
|
||||
for index, track in enumerate(player.queue, 1)
|
||||
)
|
||||
else:
|
||||
embed.description = "We are still determining a playlist"
|
||||
await ctx.response.send_message(embed=embed)
|
||||
|
||||
@app_commands.command(
|
||||
name="skip", description="You might like the next track better"
|
||||
)
|
||||
async def skip(self, ctx: Interaction):
|
||||
player = helper.get_player(ctx.client, ctx.guild_id)
|
||||
embed = create_embed(ctx.user)
|
||||
embed.title = "Skipped current song"
|
||||
await player.skip()
|
||||
await ctx.response.send_message(embed=embed)
|
||||
@@ -0,0 +1,60 @@
|
||||
import datetime
|
||||
import typing
|
||||
import discord
|
||||
from bot import colors
|
||||
from cogs.music import helper
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from lavalink.models import AudioTrack
|
||||
|
||||
|
||||
def format_track(track: "AudioTrack", max_length: int = 0):
|
||||
try:
|
||||
duration = str(datetime.timedelta(milliseconds=int(track.duration)))
|
||||
except OverflowError:
|
||||
duration = "0:00:00"
|
||||
|
||||
if max_length == 0:
|
||||
return f"`{duration}` [{track.title}]({track.uri})"
|
||||
|
||||
max_length -= len(duration) + 1
|
||||
if len(track.title) > max_length:
|
||||
track_title = track.title[: max_length - 3] + "..."
|
||||
else:
|
||||
track_title = track.title
|
||||
|
||||
return f"`{duration}` [{track_title}]({track.uri})"
|
||||
|
||||
|
||||
def create_track_embed(
|
||||
current: "AudioTrack",
|
||||
queue: typing.Sequence["AudioTrack"],
|
||||
history: typing.Sequence["AudioTrack"],
|
||||
) -> discord.Embed:
|
||||
embed_width = 52
|
||||
embed = discord.Embed(
|
||||
title="Now playing...",
|
||||
colour=colors["embed"],
|
||||
)
|
||||
|
||||
embed.description = format_track(current, max_length=embed_width)
|
||||
|
||||
if len(queue) > 0:
|
||||
upcoming_fmt = "\n".join(
|
||||
format_track(track, max_length=embed_width) for track in queue
|
||||
)
|
||||
else:
|
||||
upcoming_fmt = "No tracks have been queued yet..."
|
||||
embed.add_field(name="Coming up", value=upcoming_fmt, inline=False)
|
||||
|
||||
if len(history) > 0:
|
||||
history_fmt = "\n".join(
|
||||
format_track(track, max_length=embed_width) for track in history
|
||||
)
|
||||
else:
|
||||
history_fmt = "No history yet..."
|
||||
embed.add_field(name="Previously played", value=history_fmt, inline=False)
|
||||
|
||||
embed.set_image(url=f"https://i3.ytimg.com/vi/{current.identifier}/mqdefault.jpg")
|
||||
embed.set_footer(text=f"Uploaded by: {current.author}")
|
||||
return embed
|
||||
@@ -0,0 +1,64 @@
|
||||
import typing
|
||||
|
||||
import discord
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from bot import TuneBot
|
||||
|
||||
|
||||
class LavalinkVoiceClient(discord.VoiceClient):
|
||||
def __init__(self, client: "TuneBot", channel: discord.abc.Connectable):
|
||||
self.client = client
|
||||
self.channel = channel
|
||||
self.lavalink = self.client.lavalink
|
||||
|
||||
async def on_voice_server_update(self, data):
|
||||
# the data needs to be transformed before being handed down to
|
||||
# voice_update_handler
|
||||
lavalink_data = {"t": "VOICE_SERVER_UPDATE", "d": data}
|
||||
await self.lavalink.voice_update_handler(lavalink_data)
|
||||
|
||||
async def on_voice_state_update(self, data):
|
||||
# the data needs to be transformed before being handed down to
|
||||
# voice_update_handler
|
||||
lavalink_data = {"t": "VOICE_STATE_UPDATE", "d": data}
|
||||
await self.lavalink.voice_update_handler(lavalink_data)
|
||||
|
||||
async def connect(
|
||||
self,
|
||||
*,
|
||||
timeout: float,
|
||||
reconnect: bool,
|
||||
self_deaf: bool = False,
|
||||
self_mute: bool = False,
|
||||
) -> None:
|
||||
"""
|
||||
Connect the bot to the voice channel and create a player_manager
|
||||
if it doesn't exist yet.
|
||||
"""
|
||||
# ensure there is a player_manager when creating a new voice_client
|
||||
self.lavalink.player_manager.create(guild_id=self.channel.guild.id)
|
||||
await self.channel.guild.change_voice_state(
|
||||
channel=self.channel, self_mute=self_mute, self_deaf=self_deaf
|
||||
)
|
||||
|
||||
async def disconnect(self, *, force: bool) -> None:
|
||||
"""
|
||||
Handles the disconnect.
|
||||
Cleans up running player and leaves the voice client.
|
||||
"""
|
||||
player = self.lavalink.player_manager.get(self.channel.guild.id)
|
||||
|
||||
# no need to disconnect if we are not connected
|
||||
if not force and not player.is_connected:
|
||||
return
|
||||
|
||||
# None means disconnect
|
||||
await self.channel.guild.change_voice_state(channel=None)
|
||||
|
||||
# update the channel_id of the player to None
|
||||
# this must be done because the on_voice_state_update that
|
||||
# would set channel_id to None doesn't get dispatched after the
|
||||
# disconnect
|
||||
player.channel_id = None
|
||||
self.cleanup()
|
||||
-213
@@ -1,213 +0,0 @@
|
||||
import asyncio
|
||||
import io
|
||||
import textwrap
|
||||
import time
|
||||
import traceback
|
||||
from asyncio.subprocess import PIPE
|
||||
from contextlib import redirect_stdout
|
||||
from io import BytesIO
|
||||
from platform import python_version
|
||||
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
from discord.ext.commands.context import Context
|
||||
|
||||
from bot import TuneBot
|
||||
from utils.classes import BaseCog
|
||||
|
||||
|
||||
class OwnerCog(BaseCog):
|
||||
def __init__(self, bot: TuneBot):
|
||||
super().__init__(bot)
|
||||
self._last_result = None
|
||||
|
||||
@staticmethod
|
||||
def cleanup_code(content):
|
||||
if content.startswith("```") and content.endswith("```"):
|
||||
return "\n".join(content.split("\n")[1:-1])
|
||||
return content.strip("` \n")
|
||||
|
||||
# Hidden means it won't show up on the default help.
|
||||
@commands.command(name="load", hidden=True, slash_command=False)
|
||||
@commands.is_owner()
|
||||
async def _cog_load(self, ctx: Context, *, cog: str):
|
||||
"""Command which Loads a Module."""
|
||||
|
||||
try:
|
||||
self.bot.load_extension(cog)
|
||||
except Exception as e:
|
||||
await ctx.send(f"**`ERROR:`** {type(e).__name__} - {e}")
|
||||
else:
|
||||
await ctx.send("**`SUCCESS`**")
|
||||
|
||||
@commands.command(name="unload", hidden=True, slash_command=False)
|
||||
@commands.is_owner()
|
||||
async def _cog_unload(self, ctx, *, cog: str):
|
||||
"""Command which Unloads a Module."""
|
||||
|
||||
try:
|
||||
self.bot.unload_extension(cog)
|
||||
except Exception as e:
|
||||
await ctx.send(f"**`ERROR:`** {type(e).__name__} - {e}")
|
||||
else:
|
||||
await ctx.send("**`SUCCESS`**")
|
||||
|
||||
@commands.command(name="reload", hidden=True, slash_command=False)
|
||||
@commands.is_owner()
|
||||
async def _cog_reload(self, ctx, *, cog: str):
|
||||
"""Command which Reloads a Module."""
|
||||
|
||||
try:
|
||||
self.bot.unload_extension(cog)
|
||||
self.bot.load_extension(cog)
|
||||
except Exception as e:
|
||||
await ctx.send(f"**`ERROR:`** {type(e).__name__} - {e}")
|
||||
else:
|
||||
await ctx.send("**`SUCCESS`**")
|
||||
|
||||
@commands.command(name="shutdown", hidden=True, slash_command=False)
|
||||
@commands.is_owner()
|
||||
async def shutdown(self, ctx):
|
||||
"""Command which shutdowns the bot."""
|
||||
await ctx.bot.logout()
|
||||
|
||||
@commands.is_owner()
|
||||
@commands.command(
|
||||
pass_context=True,
|
||||
hidden=True,
|
||||
name="eval",
|
||||
aliases=["evaluate"],
|
||||
slash_command=False,
|
||||
)
|
||||
async def _eval(self, ctx, *, body: str):
|
||||
env = {
|
||||
"bot": self.bot,
|
||||
"ctx": ctx,
|
||||
"channel": ctx.channel,
|
||||
"author": ctx.author,
|
||||
"guild": ctx.guild,
|
||||
"message": ctx.message,
|
||||
"_": self._last_result,
|
||||
}
|
||||
|
||||
env.update(globals())
|
||||
|
||||
body = self.cleanup_code(body)
|
||||
stdout = io.StringIO()
|
||||
|
||||
to_compile = f'async def func():\n{textwrap.indent(body, " ")}'
|
||||
# await ctx.message.add_reaction('a:loading:452489773396000778')
|
||||
|
||||
try:
|
||||
exec(to_compile, env)
|
||||
except Exception as e:
|
||||
# await ctx.message.add_reaction('naokoerror:447495055603662849')
|
||||
fooem = discord.Embed(color=0xFF0000)
|
||||
fooem.add_field(
|
||||
name="Code evaluation was not successful.",
|
||||
value=f"```\n{e.__class__.__name__}: {e}\n```",
|
||||
)
|
||||
fooem.set_footer(
|
||||
text=f"Evaluated using Python {python_version()}",
|
||||
icon_url="http://i.imgur.com/9EftiVK.png",
|
||||
)
|
||||
await ctx.send(embed=fooem)
|
||||
# await ctx.message.remove_reaction('a:loading:452489773396000778', member=ctx.me)
|
||||
|
||||
func = env["func"]
|
||||
try:
|
||||
with redirect_stdout(stdout):
|
||||
ret = await func()
|
||||
except Exception as e:
|
||||
value = stdout.getvalue()
|
||||
# await ctx.message.add_reaction('naokoerror:447495055603662849')
|
||||
fooem = discord.Embed(color=0xFF0000)
|
||||
fooem.add_field(
|
||||
name="Code evaluation was not successful.",
|
||||
value=f"```\n{value}{traceback.format_exc()}\n```",
|
||||
)
|
||||
fooem.set_footer(
|
||||
text=f"Evaluated using Python {python_version()}",
|
||||
icon_url="http://i.imgur.com/9EftiVK.png",
|
||||
)
|
||||
await ctx.send(embed=fooem)
|
||||
try:
|
||||
# await ctx.message.remove_reaction('a:loading:452489773396000778', member=ctx.me)
|
||||
# await ctx.message.add_reaction('naokotick:447494238872141827')
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
value = stdout.getvalue()
|
||||
try:
|
||||
await ctx.message.remove_reaction(
|
||||
"a:loading:452489773396000778", member=ctx.me
|
||||
)
|
||||
# await ctx.message.add_reaction('naokotick:447494238872141827')
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if ret is None:
|
||||
if value:
|
||||
sfooem = discord.Embed(color=0x170041)
|
||||
sfooem.add_field(
|
||||
name="Code evaluation was successful!",
|
||||
value=f"```\n{value}\n```",
|
||||
)
|
||||
sfooem.set_footer(
|
||||
text=f"Evaluated using Python {python_version()}",
|
||||
icon_url="http://i.imgur.com/9EftiVK.png",
|
||||
)
|
||||
await ctx.send(embed=sfooem)
|
||||
else:
|
||||
self._last_result = ret
|
||||
ssfooem = discord.Embed(color=0x170041)
|
||||
ssfooem.add_field(
|
||||
name="Code evaluation was successful!",
|
||||
value=f"```\n{value}{ret}\n```",
|
||||
)
|
||||
ssfooem.set_footer(
|
||||
text=f"Evaluated using Python {python_version()}",
|
||||
icon_url="http://i.imgur.com/9EftiVK.png",
|
||||
)
|
||||
await ctx.send(embed=ssfooem)
|
||||
|
||||
@commands.is_owner()
|
||||
@commands.command(hidden=True, aliases=["exec"], slash_command=False)
|
||||
async def execute(self, ctx, *, text: str):
|
||||
"""Do a shell command."""
|
||||
message = await ctx.send(f"Loading...")
|
||||
proc = await asyncio.create_subprocess_shell(
|
||||
text, stdin=None, stderr=PIPE, stdout=PIPE
|
||||
)
|
||||
out = (await proc.stdout.read()).decode("utf-8").strip()
|
||||
err = (await proc.stderr.read()).decode("utf-8").strip()
|
||||
|
||||
if not out and not err:
|
||||
await message.delete()
|
||||
return await ctx.message.add_reaction("👌")
|
||||
|
||||
content = ""
|
||||
|
||||
if err:
|
||||
content += f"Error:\r\n{err}\r\n{'-' * 30}\r\n"
|
||||
if out:
|
||||
content += out
|
||||
|
||||
if len(content) > 1500:
|
||||
try:
|
||||
data = BytesIO(content.encode("utf-8"))
|
||||
await message.delete()
|
||||
await ctx.send(
|
||||
content=f"The result was a bit too long..",
|
||||
file=discord.File(data, filename=f"result_{int(time.time())}.txt"),
|
||||
)
|
||||
except asyncio.TimeoutError as e:
|
||||
await message.delete()
|
||||
return await ctx.send(e)
|
||||
else:
|
||||
await message.edit(content=f"```fix\n{content}\n```")
|
||||
|
||||
|
||||
def setup(bot: TuneBot):
|
||||
bot.add_cog(OwnerCog(bot))
|
||||
@@ -1,159 +0,0 @@
|
||||
from typing import Any
|
||||
|
||||
from discord.ext import commands
|
||||
from discord.message import Message
|
||||
|
||||
from bot import TuneBot
|
||||
from context import CustomContext
|
||||
from utils.classes import BaseCog
|
||||
from utils.database import AutoJoin
|
||||
from utils.database import Playlist
|
||||
from utils.database import PlaylistSource
|
||||
from utils.EmbedGenerator import EmbedGenerator
|
||||
|
||||
|
||||
class SettingsCog(BaseCog, name="Settings"):
|
||||
@commands.group(aliases=["aj"], invoke_without_command=True)
|
||||
@commands.cooldown(rate=1, per=5, type=commands.BucketType.user)
|
||||
async def autojoin(self, ctx: CustomContext):
|
||||
"""Enable/Disable the bot automatically joining"""
|
||||
await EmbedGenerator.Message(
|
||||
ctx,
|
||||
"Autojoin",
|
||||
f"Usage:\n\n`{ctx.prefix}autojoin set`\n`{ctx.prefix}autojoin unset`",
|
||||
)
|
||||
|
||||
@autojoin.command(name="enable")
|
||||
@commands.has_permissions(manage_channels=True)
|
||||
@commands.cooldown(rate=1, per=5, type=commands.BucketType.user)
|
||||
async def autojoin_set(self, ctx: CustomContext):
|
||||
"""Enable the bot automatically joining"""
|
||||
voicechannel_id = ctx.author.voice.channel.id
|
||||
textchannel_id = ctx.message.channel.id
|
||||
await AutoJoin.update_channel(
|
||||
ctx.redis, ctx.guild.id, voicechannel_id, textchannel_id
|
||||
)
|
||||
await EmbedGenerator.Message(ctx, "Autojoin", "`enabled`")
|
||||
|
||||
@autojoin.command(name="disable")
|
||||
@commands.has_permissions(manage_channels=True)
|
||||
@commands.cooldown(rate=1, per=5, type=commands.BucketType.user)
|
||||
async def autojoin_del(self, ctx: CustomContext):
|
||||
"""Disable the bot automatically joining"""
|
||||
await AutoJoin.del_channel(ctx.redis, ctx.guild.id)
|
||||
await EmbedGenerator.Message(ctx, "Autojoin", "`disabled`")
|
||||
|
||||
@commands.is_owner()
|
||||
@commands.group(
|
||||
name="source",
|
||||
aliases=["src"],
|
||||
invoke_without_command=True,
|
||||
slash_command=False,
|
||||
hidden=True,
|
||||
)
|
||||
async def source(self, ctx: CustomContext):
|
||||
"""Displays all possible options for the `source` command"""
|
||||
prefix = self.bot.config["prefixes"][0]
|
||||
embed = ctx.create_embed()
|
||||
embed.title = "All options:"
|
||||
embed.description = f"```{prefix}source list\n{prefix}source add <url>\n{prefix}source remove <url>\n{prefix}source sync```"
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
@commands.is_owner()
|
||||
@source.command(name="remove")
|
||||
async def source_remove(self, ctx: CustomContext, source_url: str):
|
||||
"""Removes a source from the bot"""
|
||||
if await PlaylistSource.remove(ctx.redis, source_url):
|
||||
prefix = self.bot.config["prefixes"][0]
|
||||
embed = ctx.create_embed()
|
||||
embed.title = "Removed source succesfully"
|
||||
embed.description = (
|
||||
f"Please use `{prefix}source sync` to persist these changes."
|
||||
)
|
||||
await ctx.send(embed=embed)
|
||||
return
|
||||
|
||||
embed = ctx.create_embed()
|
||||
embed.title = "Could not remove source, the specified source might not exist"
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
@commands.is_owner()
|
||||
@source.command(name="add")
|
||||
async def source_add(self, ctx: CustomContext, source_url: str):
|
||||
"""
|
||||
Add's a source to the bot
|
||||
|
||||
Supported sources: YouTube, SoundCloud, Bandcamp, Vimeo, Twitch and HTTP(S) URL's
|
||||
"""
|
||||
embed = ctx.create_embed()
|
||||
embed.title = "Started processing source"
|
||||
message = await ctx.send(embed=embed)
|
||||
|
||||
query_result: Any = await self.bot.lavalink.get_tracks(source_url)
|
||||
|
||||
if query_result["loadType"] == "LOAD_FAILED":
|
||||
embed = ctx.create_embed()
|
||||
embed.title = "The specified URL is not a valid source"
|
||||
embed.description = f"Supported sources: YouTube, SoundCloud, Bandcamp, Vimeo, Twitch and HTTP(S) URL's"
|
||||
if isinstance(message, Message):
|
||||
await message.edit(embed=embed)
|
||||
else:
|
||||
await ctx.send(embed=embed)
|
||||
return
|
||||
|
||||
await PlaylistSource.add(ctx.redis, source_url)
|
||||
track_urls = [str(track["info"]["uri"]) for track in query_result["tracks"]]
|
||||
await Playlist.add_bulk(ctx.redis, track_urls)
|
||||
|
||||
embed = ctx.create_embed()
|
||||
embed.title = "Finished processing source"
|
||||
embed.description = f"Added {len(track_urls)} tracks"
|
||||
if isinstance(message, Message):
|
||||
await message.edit(embed=embed)
|
||||
else:
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
@commands.is_owner()
|
||||
@source.command(name="list", aliases=["ls"])
|
||||
async def source_list(self, ctx: CustomContext):
|
||||
"""Display a list of sources"""
|
||||
# TODO: Implement pagination for sources
|
||||
sources = await PlaylistSource.get_all(ctx.redis)
|
||||
if len(sources) > 0:
|
||||
description = "\n".join([f"[{source}]({source})" for source in sources])
|
||||
else:
|
||||
description = "This bot has no sources yet"
|
||||
|
||||
embed = ctx.create_embed()
|
||||
embed.title = "All sources:"
|
||||
embed.description = description
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
@commands.is_owner()
|
||||
@source.command(name="sync")
|
||||
async def source_sync(self, ctx: CustomContext):
|
||||
"""Forcefully resyncs all sources"""
|
||||
failed_sources: list[str] = []
|
||||
await Playlist.clear(ctx.redis)
|
||||
sources = await PlaylistSource.get_all(ctx.redis)
|
||||
for source_url in sources:
|
||||
query_result: Any = await self.bot.lavalink.get_tracks(source_url)
|
||||
if query_result["loadType"] == "LOAD_FAILED":
|
||||
failed_sources.append(source_url)
|
||||
continue
|
||||
|
||||
track_urls = [str(track["info"]["uri"]) for track in query_result["tracks"]]
|
||||
await Playlist.add_bulk(ctx.redis, track_urls)
|
||||
|
||||
embed = ctx.create_embed()
|
||||
embed.title = f"Finished sync ({len(failed_sources)} issues)"
|
||||
if len(failed_sources) > 0:
|
||||
embed.description = "\n".join(
|
||||
[f"[{source_url}]({source_url})" for source_url in failed_sources]
|
||||
)
|
||||
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
|
||||
def setup(bot: TuneBot):
|
||||
bot.add_cog(SettingsCog(bot))
|
||||
@@ -0,0 +1,17 @@
|
||||
from discord.ext import commands
|
||||
|
||||
from bot import TuneBot
|
||||
from cogs.settings.interactions import SettingCommands
|
||||
from cogs.settings.interactions import SourceCommands
|
||||
from context import CustomContext
|
||||
from utils.classes import BaseCog
|
||||
|
||||
|
||||
class SettingsCog(BaseCog, name="Settings"):
|
||||
pass
|
||||
|
||||
|
||||
async def setup(bot: TuneBot):
|
||||
await bot.add_cog(SettingsCog(bot))
|
||||
bot.tree.add_command(SettingCommands(bot), override=True)
|
||||
bot.tree.add_command(SourceCommands(bot), override=True)
|
||||
@@ -0,0 +1,145 @@
|
||||
import typing
|
||||
|
||||
from discord import app_commands
|
||||
from discord import Interaction
|
||||
|
||||
from tunebot.context import ContextLike
|
||||
from utils.embed import create_embed
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from discord import VoiceState
|
||||
from bot import TuneBot
|
||||
|
||||
|
||||
async def is_source_owner(ctx: Interaction) -> bool:
|
||||
is_manager = ctx.user.id in ctx.client.config["manager_ids"]
|
||||
is_owner: bool = await ctx.client.is_owner(ctx.user)
|
||||
return is_manager or is_owner
|
||||
|
||||
|
||||
class SourceCommands(app_commands.Group):
|
||||
def __init__(self, bot: "TuneBot"):
|
||||
super().__init__(name="source", description="Manage the radio sources")
|
||||
self.bot: "TuneBot" = bot
|
||||
|
||||
@app_commands.command(name="list", description="List all radio sources")
|
||||
@app_commands.check(is_source_owner)
|
||||
async def list(self, ctx: Interaction):
|
||||
sources = await self.bot.global_playlist_source.fetch_sources()
|
||||
if len(sources) > 0:
|
||||
description = "\n".join([f"[{source}]({source})" for source in sources])
|
||||
else:
|
||||
description = "This bot has no sources yet"
|
||||
|
||||
embed = create_embed(ctx.user)
|
||||
embed.title = "All sources:"
|
||||
embed.description = description
|
||||
await ctx.response.send_message(embed=embed)
|
||||
|
||||
@app_commands.command(name="add", description="Add a new radio source")
|
||||
@app_commands.describe(url="The URL of the to be added source")
|
||||
@app_commands.check(is_source_owner)
|
||||
async def add(self, ctx: Interaction, url: str):
|
||||
embed = create_embed(ctx.user)
|
||||
clike = ContextLike.from_discord_interaction(ctx)
|
||||
playlist_source = self.bot.playlist_source_context(clike)
|
||||
|
||||
query_result: typing.Any = await self.bot.lavalink.get_tracks(url)
|
||||
|
||||
if query_result["loadType"] == "LOAD_FAILED":
|
||||
embed = create_embed(ctx.user)
|
||||
embed.title = "The specified URL is not a valid source"
|
||||
embed.description = "Supported sources: YouTube, SoundCloud, Bandcamp, Vimeo, Twitch and HTTP(S) URL's"
|
||||
await ctx.response.send_message(embed=embed)
|
||||
return
|
||||
|
||||
await playlist_source.add(url)
|
||||
|
||||
track_urls = [str(track["info"]["uri"]) for track in query_result["tracks"]]
|
||||
await self.bot.global_playlist.add_tracks(track_urls)
|
||||
|
||||
embed.title = "Finished processing source"
|
||||
embed.description = f"Added {len(track_urls)} tracks"
|
||||
await ctx.response.send_message(embed=embed)
|
||||
|
||||
@app_commands.command(name="remove", description="Remove a radio source")
|
||||
@app_commands.describe(url="The URL of the to be removed source")
|
||||
@app_commands.check(is_source_owner)
|
||||
async def remove(self, ctx: Interaction, url: str):
|
||||
embed = create_embed(ctx.user)
|
||||
clike = ContextLike.from_discord_interaction(ctx)
|
||||
playlist_source = self.bot.playlist_source_context(clike)
|
||||
if await playlist_source.remove(url):
|
||||
prefix = self.bot.config["prefixes"][0]
|
||||
embed.title = "Removed source succesfully"
|
||||
embed.description = (
|
||||
f"Please use `{prefix}source sync` to persist these changes."
|
||||
)
|
||||
await ctx.response.send_message(embed=embed)
|
||||
return
|
||||
|
||||
embed.title = "Could not remove source, the specified source might not exist"
|
||||
await ctx.response.send_message(embed=embed)
|
||||
|
||||
@app_commands.command(name="sync", description="Synchronize all sources")
|
||||
@app_commands.check(is_source_owner)
|
||||
async def sync(self, ctx: Interaction):
|
||||
embed = create_embed(ctx.user)
|
||||
failed_sources: list[str] = []
|
||||
await self.bot.global_playlist.clear()
|
||||
sources = await self.bot.global_playlist_source.fetch_sources()
|
||||
for source_url in sources:
|
||||
query_result: typing.Any = await self.bot.lavalink.get_tracks(source_url)
|
||||
if query_result["loadType"] == "LOAD_FAILED":
|
||||
failed_sources.append(source_url)
|
||||
continue
|
||||
|
||||
track_urls = [str(track["info"]["uri"]) for track in query_result["tracks"]]
|
||||
await self.bot.global_playlist.add_tracks(track_urls)
|
||||
|
||||
embed.title = f"Finished sync ({len(failed_sources)} issues)"
|
||||
if len(failed_sources) > 0:
|
||||
embed.description = "\n".join(
|
||||
[f"[{source_url}]({source_url})" for source_url in failed_sources]
|
||||
)
|
||||
|
||||
await ctx.response.send_message(embed=embed)
|
||||
|
||||
class SettingCommands(app_commands.Group):
|
||||
def __init__(self, bot: "TuneBot"):
|
||||
super().__init__(name="settings", description="Tweak and customize")
|
||||
self.bot: "TuneBot" = bot
|
||||
|
||||
@app_commands.command(
|
||||
name="autojoin",
|
||||
description="Let the bot automatically join a channel after occassional maintenance",
|
||||
)
|
||||
@app_commands.describe(state="enabled or disabled")
|
||||
@app_commands.choices(
|
||||
state=[
|
||||
app_commands.Choice(name="enable", value=0),
|
||||
app_commands.Choice(name="disable", value=1),
|
||||
]
|
||||
)
|
||||
@app_commands.check(is_source_owner)
|
||||
async def autojoin(self, ctx: Interaction, state: app_commands.Choice[int]):
|
||||
voice_state: "VoiceState" = ctx.user.voice
|
||||
embed = create_embed(ctx.user)
|
||||
if not voice_state:
|
||||
embed.title = "Please join a voice channel before running this command."
|
||||
await ctx.response.send_message(embed=embed)
|
||||
return
|
||||
|
||||
clike = ContextLike.from_discord_interaction(ctx)
|
||||
autojoin = self.bot.autojoin_context(clike)
|
||||
|
||||
enabled = state.value
|
||||
if not enabled:
|
||||
await autojoin.update(voice_state.channel.id, ctx.channel_id)
|
||||
embed.title = f"AutoJoin enabled for #{voice_state.channel.name}"
|
||||
await ctx.response.send_message(embed=embed)
|
||||
return
|
||||
|
||||
await autojoin.disable()
|
||||
embed.title = "AutoJoin disabled"
|
||||
await ctx.response.send_message(embed=embed)
|
||||
+29
-11
@@ -1,25 +1,43 @@
|
||||
{
|
||||
"token": "",
|
||||
"owner_ids": [194545408960102400, 190875175460405249],
|
||||
"prefixes": ["ck!"],
|
||||
"owner_ids": [
|
||||
194545408960102400,
|
||||
190875175460405250
|
||||
],
|
||||
"manager_ids": [],
|
||||
"prefix": "ck!",
|
||||
"redis_url": "",
|
||||
"redis_prefix": "",
|
||||
"lavalink": {
|
||||
"host": "",
|
||||
"port": 2333,
|
||||
"password": "",
|
||||
"region": "",
|
||||
"name": ""
|
||||
"nodes": {
|
||||
"example": {
|
||||
"host": "",
|
||||
"port": 2333,
|
||||
"password": "",
|
||||
"region": "",
|
||||
"name": ""
|
||||
}
|
||||
},
|
||||
"colors": {
|
||||
"embed": "7289da"
|
||||
},
|
||||
"info": {
|
||||
"name": "CloudKid Radio",
|
||||
"description": "A sample bot description"
|
||||
"description": "A sample bot description",
|
||||
"status": {
|
||||
"messages": [
|
||||
"bangers",
|
||||
"noises 🤖"
|
||||
],
|
||||
"interval": 12
|
||||
}
|
||||
},
|
||||
"cogs": ["cogs.owner", "cogs.settings", "cogs.information", "cogs.music"],
|
||||
"slash_command_guilds": [],
|
||||
"cogs": [
|
||||
"jishaku",
|
||||
"cogs.settings",
|
||||
"cogs.information",
|
||||
"cogs.music"
|
||||
],
|
||||
"queue_buffer_size": 5,
|
||||
"history_size": 2,
|
||||
"slash_descriptions": {}
|
||||
}
|
||||
|
||||
+25
-4
@@ -6,21 +6,28 @@ from discord.ext import commands
|
||||
from discord.ext.commands.errors import CommandInvokeError
|
||||
from lavalink.models import DefaultPlayer
|
||||
|
||||
from tunebot.context import ContextLike
|
||||
from tunebot.redis import RedisAutoJoin
|
||||
from tunebot.redis import RedisPlaylistSource
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bot import TuneBot
|
||||
from tunebot import PlaylistSource
|
||||
from tunebot import AutoJoin
|
||||
from utils.classes import BaseCog
|
||||
|
||||
|
||||
class CustomContext(commands.Context):
|
||||
bot: "TuneBot"
|
||||
cog: "BaseCog"
|
||||
|
||||
@property
|
||||
def redis(self) -> Redis:
|
||||
return self.bot._redis_client
|
||||
|
||||
@property
|
||||
def player(self) -> DefaultPlayer:
|
||||
if hasattr(self.bot, "lavalink"):
|
||||
return self.bot.lavalink.player_manager.get(self.guild.id)
|
||||
|
||||
raise CommandInvokeError("Lavalink is still starting up.")
|
||||
return self.bot.lavalink.player_manager.get(self.guild.id)
|
||||
|
||||
def create_embed(self) -> Embed:
|
||||
bot: TuneBot = self.bot
|
||||
@@ -33,3 +40,17 @@ class CustomContext(commands.Context):
|
||||
embed = Embed(color=color)
|
||||
embed.set_footer(text=f"Requested by: {self.author}", icon_url=avatar)
|
||||
return embed
|
||||
|
||||
@property
|
||||
def playlist_source(self) -> "PlaylistSource":
|
||||
if not hasattr(self, "_playlist_source"):
|
||||
ctx = ContextLike.from_discord_context(self)
|
||||
self._playlist_source = self.bot.playlist_source_context(ctx)
|
||||
return self._playlist_source
|
||||
|
||||
@property
|
||||
def autojoin(self) -> "AutoJoin":
|
||||
if not hasattr(self, "_autojoin"):
|
||||
ctx = ContextLike.from_discord_context(self)
|
||||
self._autojoin = self.bot.autojoin_context(ctx)
|
||||
return self._autojoin
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
redis:
|
||||
image: redis:7
|
||||
ports:
|
||||
- "6379:6379"
|
||||
lavalink:
|
||||
image: fredboat/lavalink:v3.6
|
||||
volumes:
|
||||
- ./.docker/lavalink.yml:/opt/Lavalink/application.yml
|
||||
ports:
|
||||
- "2333:2333"
|
||||
@@ -0,0 +1,21 @@
|
||||
import typing
|
||||
from lavalink import DefaultPlayer
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from lavalink import Node
|
||||
from lavalink.models import AudioTrack
|
||||
|
||||
|
||||
class CustomPlayer(DefaultPlayer):
|
||||
def __init__(self, guild_id: int, node: "Node"):
|
||||
from bot import config
|
||||
|
||||
self.history: typing.List["AudioTrack"] = []
|
||||
self.max_size = config["history_size"]
|
||||
|
||||
super().__init__(guild_id, node)
|
||||
|
||||
def append_history(self, track: "AudioTrack"):
|
||||
self.history.insert(0, track)
|
||||
if len(self.history) > self.max_size:
|
||||
self.history.pop(-1)
|
||||
Generated
+173
-415
@@ -1,17 +1,18 @@
|
||||
[[package]]
|
||||
name = "aiohttp"
|
||||
version = "3.6.3"
|
||||
version = "3.7.4.post0"
|
||||
description = "Async http client/server framework (asyncio)"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.5.3"
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.dependencies]
|
||||
async-timeout = ">=3.0,<4.0"
|
||||
attrs = ">=17.3.0"
|
||||
chardet = ">=2.0,<4.0"
|
||||
multidict = ">=4.5,<5.0"
|
||||
yarl = ">=1.0,<1.6.0"
|
||||
chardet = ">=2.0,<5.0"
|
||||
multidict = ">=4.5,<7.0"
|
||||
typing-extensions = ">=3.6.5"
|
||||
yarl = ">=1.0,<2.0"
|
||||
|
||||
[package.extras]
|
||||
speedups = ["aiodns", "brotlipy", "cchardet"]
|
||||
@@ -43,6 +44,17 @@ python-versions = ">=3.6"
|
||||
pytube = "*"
|
||||
urllib3 = "*"
|
||||
|
||||
[[package]]
|
||||
name = "astunparse"
|
||||
version = "1.6.3"
|
||||
description = "An AST unparser for Python"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[package.dependencies]
|
||||
six = ">=1.6.1,<2.0"
|
||||
|
||||
[[package]]
|
||||
name = "async-timeout"
|
||||
version = "3.0.1"
|
||||
@@ -104,6 +116,14 @@ jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
|
||||
python2 = ["typed-ast (>=1.4.3)"]
|
||||
uvloop = ["uvloop (>=0.15.2)"]
|
||||
|
||||
[[package]]
|
||||
name = "braceexpand"
|
||||
version = "0.1.7"
|
||||
description = "Bash-style brace expansion for Python"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2021.10.8"
|
||||
@@ -114,7 +134,7 @@ python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "cffi"
|
||||
version = "1.15.0"
|
||||
version = "1.15.1"
|
||||
description = "Foreign Function Interface for Python calling C code."
|
||||
category = "main"
|
||||
optional = false
|
||||
@@ -154,7 +174,7 @@ unicode_backport = ["unicodedata2"]
|
||||
name = "click"
|
||||
version = "8.0.3"
|
||||
description = "Composable command line interface toolkit"
|
||||
category = "dev"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
@@ -165,34 +185,27 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""}
|
||||
name = "colorama"
|
||||
version = "0.4.4"
|
||||
description = "Cross-platform colored terminal text."
|
||||
category = "dev"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
|
||||
[[package]]
|
||||
name = "discord.py"
|
||||
version = "2.0.0a3662+gd2adc6c0"
|
||||
version = "2.0.1"
|
||||
description = "A Python wrapper for the Discord API"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.8.0"
|
||||
develop = false
|
||||
|
||||
[package.dependencies]
|
||||
aiohttp = ">=3.6.0,<3.8.0"
|
||||
orjson = {version = ">=3.5.4", optional = true, markers = "extra == \"speed\""}
|
||||
PyNaCl = {version = ">=1.3.0,<1.5", optional = true, markers = "extra == \"voice\""}
|
||||
aiohttp = ">=3.7.4,<4"
|
||||
PyNaCl = {version = ">=1.3.0,<1.6", optional = true, markers = "extra == \"voice\""}
|
||||
|
||||
[package.extras]
|
||||
docs = ["sphinx (==4.0.2)", "sphinxcontrib-trio (==1.1.2)", "sphinxcontrib-websupport"]
|
||||
speed = ["orjson (>=3.5.4)"]
|
||||
voice = ["PyNaCl (>=1.3.0,<1.5)"]
|
||||
|
||||
[package.source]
|
||||
type = "git"
|
||||
url = "https://github.com/iDevision/enhanced-discord.py"
|
||||
reference = "2.0"
|
||||
resolved_reference = "d2adc6c05fafa761f7b8005ba8469ef4b78c188a"
|
||||
docs = ["sphinx (==4.4.0)", "sphinxcontrib-trio (==1.1.2)", "sphinxcontrib-websupport", "typing-extensions (>=4.3,<5)"]
|
||||
speed = ["orjson (>=3.5.4)", "aiodns (>=1.1)", "brotli", "cchardet (==2.1.7)"]
|
||||
test = ["coverage", "pytest", "pytest-asyncio", "pytest-cov", "pytest-mock", "typing-extensions (>=4.3,<5)"]
|
||||
voice = ["PyNaCl (>=1.3.0,<1.6)"]
|
||||
|
||||
[[package]]
|
||||
name = "distlib"
|
||||
@@ -245,19 +258,87 @@ optional = false
|
||||
python-versions = ">=3.5"
|
||||
|
||||
[[package]]
|
||||
name = "lavalink"
|
||||
version = "3.1.4"
|
||||
description = "A lavalink interface built for discord.py"
|
||||
name = "import-expression"
|
||||
version = "1.1.4"
|
||||
description = "Parses a superset of Python allowing for inline module import expressions"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[package.dependencies]
|
||||
aiohttp = ">=3.6.0,<3.7.0"
|
||||
astunparse = ">=1.6.3,<2.0.0"
|
||||
|
||||
[package.extras]
|
||||
test = ["pytest", "pytest-cov"]
|
||||
|
||||
[[package]]
|
||||
name = "importlib-metadata"
|
||||
version = "5.0.0"
|
||||
description = "Read metadata from Python packages"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[package.dependencies]
|
||||
zipp = ">=0.5"
|
||||
|
||||
[package.extras]
|
||||
docs = ["sphinx (>=3.5)", "jaraco.packaging (>=9)", "rst.linker (>=1.9)", "furo", "jaraco.tidelift (>=1.4)"]
|
||||
perf = ["ipython"]
|
||||
testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "flake8 (<5)", "pytest-cov", "pytest-enabler (>=1.3)", "packaging", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)", "importlib-resources (>=1.3)"]
|
||||
|
||||
[[package]]
|
||||
name = "jishaku"
|
||||
version = "2.5.1"
|
||||
description = "A discord.py extension including useful tools for bot development and debugging."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.8.0"
|
||||
|
||||
[package.dependencies]
|
||||
braceexpand = ">=0.1.7"
|
||||
click = ">=8.0.1"
|
||||
import-expression = ">=1.0.0,<2.0.0"
|
||||
importlib-metadata = {version = ">=3.7.0", markers = "python_version < \"3.10\""}
|
||||
line-profiler = ">=3.5.1"
|
||||
typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""}
|
||||
|
||||
[package.extras]
|
||||
discordpy = ["discord.py (>=1.7.3)"]
|
||||
docs = ["Sphinx (>=4.4.0)", "sphinxcontrib-asyncio (>=0.3.0)"]
|
||||
procinfo = ["psutil (>=5.8.0)"]
|
||||
publish = ["Jinja2 (>=3.0.3)"]
|
||||
test = ["coverage (>=6.3.2)", "flake8 (>=4.0.1)", "isort (>=5.10.1)", "pylint (>=2.11.1)", "pytest (>=7.0.1)", "pytest-asyncio (>=0.18.1)", "pytest-cov (>=3.0.0)", "pytest-mock (>=3.7.0)"]
|
||||
voice = ["yt-dlp (>=2022.3.8)"]
|
||||
|
||||
[[package]]
|
||||
name = "lavalink"
|
||||
version = "4.0.4"
|
||||
description = "A Lavalink WebSocket & API wrapper built around coverage, reliability and performance."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[package.dependencies]
|
||||
aiohttp = ">=3.7.4,<3.9.0"
|
||||
|
||||
[package.extras]
|
||||
development = ["pylint", "flake8"]
|
||||
docs = ["sphinx", "pygments", "guzzle-sphinx-theme"]
|
||||
docs = ["sphinx", "pygments", "guzzle-sphinx-theme", "enum-tools", "sphinx-toolbox"]
|
||||
|
||||
[[package]]
|
||||
name = "line-profiler"
|
||||
version = "3.5.1"
|
||||
description = "Line-by-line profiler."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[package.extras]
|
||||
all = ["cython", "scikit-build", "cmake", "ninja", "pytest (>=4.6.11)", "pytest-cov (>=2.10.1)", "coverage[toml] (>=5.3)", "ubelt (>=1.0.1)", "IPython (>=0.13,<7.17.0)", "IPython (>=0.13)"]
|
||||
build = ["cython", "scikit-build", "cmake", "ninja"]
|
||||
ipython = ["IPython (>=0.13,<7.17.0)", "IPython (>=0.13)"]
|
||||
tests = ["pytest (>=4.6.11)", "pytest-cov (>=2.10.1)", "coverage[toml] (>=5.3)", "ubelt (>=1.0.1)", "IPython (>=0.13,<7.17.0)", "IPython (>=0.13)"]
|
||||
|
||||
[[package]]
|
||||
name = "multidict"
|
||||
@@ -291,14 +372,6 @@ category = "dev"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "orjson"
|
||||
version = "3.6.4"
|
||||
description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[[package]]
|
||||
name = "pathspec"
|
||||
version = "0.9.0"
|
||||
@@ -353,15 +426,14 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
|
||||
[[package]]
|
||||
name = "pynacl"
|
||||
version = "1.4.0"
|
||||
version = "1.5.0"
|
||||
description = "Python binding to the Networking and Cryptography (NaCl) library"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.dependencies]
|
||||
cffi = ">=1.4.1"
|
||||
six = "*"
|
||||
|
||||
[package.extras]
|
||||
docs = ["sphinx (>=1.6.5)", "sphinx-rtd-theme"]
|
||||
@@ -478,14 +550,6 @@ dev = ["Cython (>=0.29.24,<0.30.0)", "pytest (>=3.6.0)", "Sphinx (>=4.1.2,<4.2.0
|
||||
docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)"]
|
||||
test = ["aiohttp", "flake8 (>=3.9.2,<3.10.0)", "psutil", "pycodestyle (>=2.7.0,<2.8.0)", "pyOpenSSL (>=19.0.0,<19.1.0)", "mypy (>=0.800)"]
|
||||
|
||||
[[package]]
|
||||
name = "websockets"
|
||||
version = "10.0"
|
||||
description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[[package]]
|
||||
name = "virtualenv"
|
||||
version = "20.10.0"
|
||||
@@ -505,6 +569,14 @@ six = ">=1.9.0,<2"
|
||||
docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=21.3)"]
|
||||
testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "packaging (>=20.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "websockets"
|
||||
version = "10.0"
|
||||
description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[[package]]
|
||||
name = "yarl"
|
||||
version = "1.5.1"
|
||||
@@ -530,119 +602,41 @@ mutagen = "*"
|
||||
pycryptodomex = "*"
|
||||
websockets = "*"
|
||||
|
||||
[[package]]
|
||||
name = "zipp"
|
||||
version = "3.10.0"
|
||||
description = "Backport of pathlib-compatible object wrapper for zip files"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[package.extras]
|
||||
docs = ["sphinx (>=3.5)", "jaraco.packaging (>=9)", "rst.linker (>=1.9)", "furo", "jaraco.tidelift (>=1.4)"]
|
||||
testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "flake8 (<5)", "pytest-cov", "pytest-enabler (>=1.3)", "jaraco.itertools", "func-timeout", "jaraco.functools", "more-itertools", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"]
|
||||
|
||||
[metadata]
|
||||
lock-version = "1.1"
|
||||
python-versions = "^3.8"
|
||||
content-hash = "87b61ad577bb9413177b2749b0c4b30026aa512bf3a6eb62e74c0ef7e2470ed4"
|
||||
content-hash = "7518d3e2ccfffad1b05bd93f5178cb700c0bbe71e177707b56f60bb2dc46c70d"
|
||||
|
||||
[metadata.files]
|
||||
aiohttp = [
|
||||
{file = "aiohttp-3.6.3-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:1a4160579ffbc1b69e88cb6ca8bb0fbd4947dfcbf9fb1e2a4fc4c7a4a986c1fe"},
|
||||
{file = "aiohttp-3.6.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:fb83326d8295e8840e4ba774edf346e87eca78ba8a89c55d2690352842c15ba5"},
|
||||
{file = "aiohttp-3.6.3-cp35-cp35m-win32.whl", hash = "sha256:470e4c90da36b601676fe50c49a60d34eb8c6593780930b1aa4eea6f508dfa37"},
|
||||
{file = "aiohttp-3.6.3-cp35-cp35m-win_amd64.whl", hash = "sha256:a885432d3cabc1287bcf88ea94e1826d3aec57fd5da4a586afae4591b061d40d"},
|
||||
{file = "aiohttp-3.6.3-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:c506853ba52e516b264b106321c424d03f3ddef2813246432fa9d1cefd361c81"},
|
||||
{file = "aiohttp-3.6.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:797456399ffeef73172945708810f3277f794965eb6ec9bd3a0c007c0476be98"},
|
||||
{file = "aiohttp-3.6.3-cp36-cp36m-win32.whl", hash = "sha256:60f4caa3b7f7a477f66ccdd158e06901e1d235d572283906276e3803f6b098f5"},
|
||||
{file = "aiohttp-3.6.3-cp36-cp36m-win_amd64.whl", hash = "sha256:2ad493de47a8f926386fa6d256832de3095ba285f325db917c7deae0b54a9fc8"},
|
||||
{file = "aiohttp-3.6.3-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:319b490a5e2beaf06891f6711856ea10591cfe84fe9f3e71a721aa8f20a0872a"},
|
||||
{file = "aiohttp-3.6.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:66d64486172b032db19ea8522328b19cfb78a3e1e5b62ab6a0567f93f073dea0"},
|
||||
{file = "aiohttp-3.6.3-cp37-cp37m-win32.whl", hash = "sha256:206c0ccfcea46e1bddc91162449c20c72f308aebdcef4977420ef329c8fcc599"},
|
||||
{file = "aiohttp-3.6.3-cp37-cp37m-win_amd64.whl", hash = "sha256:687461cd974722110d1763b45c5db4d2cdee8d50f57b00c43c7590d1dd77fc5c"},
|
||||
{file = "aiohttp-3.6.3.tar.gz", hash = "sha256:698cd7bc3c7d1b82bb728bae835724a486a8c376647aec336aa21a60113c3645"},
|
||||
]
|
||||
aioredis = [
|
||||
{file = "aioredis-2.0.0-py3-none-any.whl", hash = "sha256:9921d68a3df5c5cdb0d5b49ad4fc88a4cfdd60c108325df4f0066e8410c55ffb"},
|
||||
{file = "aioredis-2.0.0.tar.gz", hash = "sha256:3a2de4b614e6a5f8e104238924294dc4e811aefbe17ddf52c04a93cbf06e67db"},
|
||||
]
|
||||
aiotube = [
|
||||
{file = "aiotube-1.3.5-py3-none-any.whl", hash = "sha256:4dcef22dd27e6229b82186555c7f4779b62f751d669b9517b88d8d7af977338f"},
|
||||
{file = "aiotube-1.3.5.tar.gz", hash = "sha256:3b015060480136812249433b02b5d869ad62a82916a92fe741bf4f907b7a948b"},
|
||||
]
|
||||
async-timeout = [
|
||||
{file = "async-timeout-3.0.1.tar.gz", hash = "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f"},
|
||||
{file = "async_timeout-3.0.1-py3-none-any.whl", hash = "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3"},
|
||||
]
|
||||
attrs = [
|
||||
{file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"},
|
||||
{file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"},
|
||||
]
|
||||
"backports.entry-points-selectable" = [
|
||||
{file = "backports.entry_points_selectable-1.1.0-py2.py3-none-any.whl", hash = "sha256:a6d9a871cde5e15b4c4a53e3d43ba890cc6861ec1332c9c2428c92f977192acc"},
|
||||
{file = "backports.entry_points_selectable-1.1.0.tar.gz", hash = "sha256:988468260ec1c196dab6ae1149260e2f5472c9110334e5d51adcb77867361f6a"},
|
||||
]
|
||||
black = [
|
||||
{file = "black-21.10b0-py3-none-any.whl", hash = "sha256:6eb7448da9143ee65b856a5f3676b7dda98ad9abe0f87fce8c59291f15e82a5b"},
|
||||
{file = "black-21.10b0.tar.gz", hash = "sha256:a9952229092e325fe5f3dae56d81f639b23f7131eb840781947e4b2886030f33"},
|
||||
]
|
||||
aiohttp = []
|
||||
aioredis = []
|
||||
aiotube = []
|
||||
astunparse = []
|
||||
async-timeout = []
|
||||
attrs = []
|
||||
"backports.entry-points-selectable" = []
|
||||
black = []
|
||||
braceexpand = []
|
||||
certifi = [
|
||||
{file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"},
|
||||
{file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"},
|
||||
]
|
||||
cffi = [
|
||||
{file = "cffi-1.15.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:c2502a1a03b6312837279c8c1bd3ebedf6c12c4228ddbad40912d671ccc8a962"},
|
||||
{file = "cffi-1.15.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:23cfe892bd5dd8941608f93348c0737e369e51c100d03718f108bf1add7bd6d0"},
|
||||
{file = "cffi-1.15.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:41d45de54cd277a7878919867c0f08b0cf817605e4eb94093e7516505d3c8d14"},
|
||||
{file = "cffi-1.15.0-cp27-cp27m-win32.whl", hash = "sha256:4a306fa632e8f0928956a41fa8e1d6243c71e7eb59ffbd165fc0b41e316b2474"},
|
||||
{file = "cffi-1.15.0-cp27-cp27m-win_amd64.whl", hash = "sha256:e7022a66d9b55e93e1a845d8c9eba2a1bebd4966cd8bfc25d9cd07d515b33fa6"},
|
||||
{file = "cffi-1.15.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:14cd121ea63ecdae71efa69c15c5543a4b5fbcd0bbe2aad864baca0063cecf27"},
|
||||
{file = "cffi-1.15.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:d4d692a89c5cf08a8557fdeb329b82e7bf609aadfaed6c0d79f5a449a3c7c023"},
|
||||
{file = "cffi-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0104fb5ae2391d46a4cb082abdd5c69ea4eab79d8d44eaaf79f1b1fd806ee4c2"},
|
||||
{file = "cffi-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:91ec59c33514b7c7559a6acda53bbfe1b283949c34fe7440bcf917f96ac0723e"},
|
||||
{file = "cffi-1.15.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f5c7150ad32ba43a07c4479f40241756145a1f03b43480e058cfd862bf5041c7"},
|
||||
{file = "cffi-1.15.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:00c878c90cb53ccfaae6b8bc18ad05d2036553e6d9d1d9dbcf323bbe83854ca3"},
|
||||
{file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abb9a20a72ac4e0fdb50dae135ba5e77880518e742077ced47eb1499e29a443c"},
|
||||
{file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a5263e363c27b653a90078143adb3d076c1a748ec9ecc78ea2fb916f9b861962"},
|
||||
{file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f54a64f8b0c8ff0b64d18aa76675262e1700f3995182267998c31ae974fbc382"},
|
||||
{file = "cffi-1.15.0-cp310-cp310-win32.whl", hash = "sha256:c21c9e3896c23007803a875460fb786118f0cdd4434359577ea25eb556e34c55"},
|
||||
{file = "cffi-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:5e069f72d497312b24fcc02073d70cb989045d1c91cbd53979366077959933e0"},
|
||||
{file = "cffi-1.15.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:64d4ec9f448dfe041705426000cc13e34e6e5bb13736e9fd62e34a0b0c41566e"},
|
||||
{file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2756c88cbb94231c7a147402476be2c4df2f6078099a6f4a480d239a8817ae39"},
|
||||
{file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b96a311ac60a3f6be21d2572e46ce67f09abcf4d09344c49274eb9e0bf345fc"},
|
||||
{file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75e4024375654472cc27e91cbe9eaa08567f7fbdf822638be2814ce059f58032"},
|
||||
{file = "cffi-1.15.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:59888172256cac5629e60e72e86598027aca6bf01fa2465bdb676d37636573e8"},
|
||||
{file = "cffi-1.15.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:27c219baf94952ae9d50ec19651a687b826792055353d07648a5695413e0c605"},
|
||||
{file = "cffi-1.15.0-cp36-cp36m-win32.whl", hash = "sha256:4958391dbd6249d7ad855b9ca88fae690783a6be9e86df65865058ed81fc860e"},
|
||||
{file = "cffi-1.15.0-cp36-cp36m-win_amd64.whl", hash = "sha256:f6f824dc3bce0edab5f427efcfb1d63ee75b6fcb7282900ccaf925be84efb0fc"},
|
||||
{file = "cffi-1.15.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:06c48159c1abed75c2e721b1715c379fa3200c7784271b3c46df01383b593636"},
|
||||
{file = "cffi-1.15.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c2051981a968d7de9dd2d7b87bcb9c939c74a34626a6e2f8181455dd49ed69e4"},
|
||||
{file = "cffi-1.15.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:fd8a250edc26254fe5b33be00402e6d287f562b6a5b2152dec302fa15bb3e997"},
|
||||
{file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91d77d2a782be4274da750752bb1650a97bfd8f291022b379bb8e01c66b4e96b"},
|
||||
{file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:45db3a33139e9c8f7c09234b5784a5e33d31fd6907800b316decad50af323ff2"},
|
||||
{file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:263cc3d821c4ab2213cbe8cd8b355a7f72a8324577dc865ef98487c1aeee2bc7"},
|
||||
{file = "cffi-1.15.0-cp37-cp37m-win32.whl", hash = "sha256:17771976e82e9f94976180f76468546834d22a7cc404b17c22df2a2c81db0c66"},
|
||||
{file = "cffi-1.15.0-cp37-cp37m-win_amd64.whl", hash = "sha256:3415c89f9204ee60cd09b235810be700e993e343a408693e80ce7f6a40108029"},
|
||||
{file = "cffi-1.15.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4238e6dab5d6a8ba812de994bbb0a79bddbdf80994e4ce802b6f6f3142fcc880"},
|
||||
{file = "cffi-1.15.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0808014eb713677ec1292301ea4c81ad277b6cdf2fdd90fd540af98c0b101d20"},
|
||||
{file = "cffi-1.15.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:57e9ac9ccc3101fac9d6014fba037473e4358ef4e89f8e181f8951a2c0162024"},
|
||||
{file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b6c2ea03845c9f501ed1313e78de148cd3f6cad741a75d43a29b43da27f2e1e"},
|
||||
{file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:10dffb601ccfb65262a27233ac273d552ddc4d8ae1bf93b21c94b8511bffe728"},
|
||||
{file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:786902fb9ba7433aae840e0ed609f45c7bcd4e225ebb9c753aa39725bb3e6ad6"},
|
||||
{file = "cffi-1.15.0-cp38-cp38-win32.whl", hash = "sha256:da5db4e883f1ce37f55c667e5c0de439df76ac4cb55964655906306918e7363c"},
|
||||
{file = "cffi-1.15.0-cp38-cp38-win_amd64.whl", hash = "sha256:181dee03b1170ff1969489acf1c26533710231c58f95534e3edac87fff06c443"},
|
||||
{file = "cffi-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:45e8636704eacc432a206ac7345a5d3d2c62d95a507ec70d62f23cd91770482a"},
|
||||
{file = "cffi-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:31fb708d9d7c3f49a60f04cf5b119aeefe5644daba1cd2a0fe389b674fd1de37"},
|
||||
{file = "cffi-1.15.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6dc2737a3674b3e344847c8686cf29e500584ccad76204efea14f451d4cc669a"},
|
||||
{file = "cffi-1.15.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:74fdfdbfdc48d3f47148976f49fab3251e550a8720bebc99bf1483f5bfb5db3e"},
|
||||
{file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffaa5c925128e29efbde7301d8ecaf35c8c60ffbcd6a1ffd3a552177c8e5e796"},
|
||||
{file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f7d084648d77af029acb79a0ff49a0ad7e9d09057a9bf46596dac9514dc07df"},
|
||||
{file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ef1f279350da2c586a69d32fc8733092fd32cc8ac95139a00377841f59a3f8d8"},
|
||||
{file = "cffi-1.15.0-cp39-cp39-win32.whl", hash = "sha256:2a23af14f408d53d5e6cd4e3d9a24ff9e05906ad574822a10563efcef137979a"},
|
||||
{file = "cffi-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:3773c4d81e6e818df2efbc7dd77325ca0dcb688116050fb2b3011218eda36139"},
|
||||
{file = "cffi-1.15.0.tar.gz", hash = "sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954"},
|
||||
]
|
||||
cfgv = [
|
||||
{file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"},
|
||||
{file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"},
|
||||
]
|
||||
chardet = [
|
||||
{file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"},
|
||||
{file = "chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"},
|
||||
]
|
||||
charset-normalizer = [
|
||||
{file = "charset-normalizer-2.0.7.tar.gz", hash = "sha256:e019de665e2bcf9c2b64e2e5aa025fa991da8720daa3c1138cadd2fd1856aed0"},
|
||||
{file = "charset_normalizer-2.0.7-py3-none-any.whl", hash = "sha256:f7af805c321bfa1ce6714c51f254e0d5bb5e5834039bc17db7ebe3a4cec9492b"},
|
||||
]
|
||||
cffi = []
|
||||
cfgv = []
|
||||
chardet = []
|
||||
charset-normalizer = []
|
||||
click = [
|
||||
{file = "click-8.0.3-py3-none-any.whl", hash = "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3"},
|
||||
{file = "click-8.0.3.tar.gz", hash = "sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b"},
|
||||
@@ -652,157 +646,39 @@ colorama = [
|
||||
{file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"},
|
||||
]
|
||||
"discord.py" = []
|
||||
distlib = [
|
||||
{file = "distlib-0.3.3-py2.py3-none-any.whl", hash = "sha256:c8b54e8454e5bf6237cc84c20e8264c3e991e824ef27e8f1e81049867d861e31"},
|
||||
{file = "distlib-0.3.3.zip", hash = "sha256:d982d0751ff6eaaab5e2ec8e691d949ee80eddf01a62eaa96ddb11531fe16b05"},
|
||||
]
|
||||
filelock = [
|
||||
{file = "filelock-3.3.2-py3-none-any.whl", hash = "sha256:bb2a1c717df74c48a2d00ed625e5a66f8572a3a30baacb7657add1d7bac4097b"},
|
||||
{file = "filelock-3.3.2.tar.gz", hash = "sha256:7afc856f74fa7006a289fd10fa840e1eebd8bbff6bffb69c26c54a0512ea8cf8"},
|
||||
]
|
||||
humanize = [
|
||||
{file = "humanize-3.12.0-py3-none-any.whl", hash = "sha256:4c71c4381f0209715cd993058e717c1b74d58ae2f8c6da7bdb59ab66473b9ab0"},
|
||||
{file = "humanize-3.12.0.tar.gz", hash = "sha256:5ec1a66e230a3e31fb3f184aab9436ea13d4e37c168e0ffc345ae5bb57e58be6"},
|
||||
]
|
||||
identify = [
|
||||
{file = "identify-2.3.4-py2.py3-none-any.whl", hash = "sha256:4de55a93e0ba72bf917c840b3794eb1055a67272a1732351c557c88ec42011b1"},
|
||||
{file = "identify-2.3.4.tar.gz", hash = "sha256:595283a1c3a078ac5774ad4dc4d1bdd0c1602f60bcf11ae673b64cb2b1945762"},
|
||||
]
|
||||
distlib = []
|
||||
filelock = []
|
||||
humanize = []
|
||||
identify = []
|
||||
idna = [
|
||||
{file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"},
|
||||
{file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"},
|
||||
]
|
||||
lavalink = [
|
||||
{file = "lavalink-3.1.4.tar.gz", hash = "sha256:c030488391e27cdc1e3ee3093817c38848ebd0d1c7bcf0d6cd0f40b9b00a4e0c"},
|
||||
]
|
||||
multidict = [
|
||||
{file = "multidict-4.7.6-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:275ca32383bc5d1894b6975bb4ca6a7ff16ab76fa622967625baeebcf8079000"},
|
||||
{file = "multidict-4.7.6-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:1ece5a3369835c20ed57adadc663400b5525904e53bae59ec854a5d36b39b21a"},
|
||||
{file = "multidict-4.7.6-cp35-cp35m-win32.whl", hash = "sha256:5141c13374e6b25fe6bf092052ab55c0c03d21bd66c94a0e3ae371d3e4d865a5"},
|
||||
{file = "multidict-4.7.6-cp35-cp35m-win_amd64.whl", hash = "sha256:9456e90649005ad40558f4cf51dbb842e32807df75146c6d940b6f5abb4a78f3"},
|
||||
{file = "multidict-4.7.6-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:e0d072ae0f2a179c375f67e3da300b47e1a83293c554450b29c900e50afaae87"},
|
||||
{file = "multidict-4.7.6-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:3750f2205b800aac4bb03b5ae48025a64e474d2c6cc79547988ba1d4122a09e2"},
|
||||
{file = "multidict-4.7.6-cp36-cp36m-win32.whl", hash = "sha256:f07acae137b71af3bb548bd8da720956a3bc9f9a0b87733e0899226a2317aeb7"},
|
||||
{file = "multidict-4.7.6-cp36-cp36m-win_amd64.whl", hash = "sha256:6513728873f4326999429a8b00fc7ceddb2509b01d5fd3f3be7881a257b8d463"},
|
||||
{file = "multidict-4.7.6-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:feed85993dbdb1dbc29102f50bca65bdc68f2c0c8d352468c25b54874f23c39d"},
|
||||
{file = "multidict-4.7.6-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:fcfbb44c59af3f8ea984de67ec7c306f618a3ec771c2843804069917a8f2e255"},
|
||||
{file = "multidict-4.7.6-cp37-cp37m-win32.whl", hash = "sha256:4538273208e7294b2659b1602490f4ed3ab1c8cf9dbdd817e0e9db8e64be2507"},
|
||||
{file = "multidict-4.7.6-cp37-cp37m-win_amd64.whl", hash = "sha256:d14842362ed4cf63751648e7672f7174c9818459d169231d03c56e84daf90b7c"},
|
||||
{file = "multidict-4.7.6-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:c026fe9a05130e44157b98fea3ab12969e5b60691a276150db9eda71710cd10b"},
|
||||
{file = "multidict-4.7.6-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:51a4d210404ac61d32dada00a50ea7ba412e6ea945bbe992e4d7a595276d2ec7"},
|
||||
{file = "multidict-4.7.6-cp38-cp38-win32.whl", hash = "sha256:5cf311a0f5ef80fe73e4f4c0f0998ec08f954a6ec72b746f3c179e37de1d210d"},
|
||||
{file = "multidict-4.7.6-cp38-cp38-win_amd64.whl", hash = "sha256:7388d2ef3c55a8ba80da62ecfafa06a1c097c18032a501ffd4cabbc52d7f2b19"},
|
||||
{file = "multidict-4.7.6.tar.gz", hash = "sha256:fbb77a75e529021e7c4a8d4e823d88ef4d23674a202be4f5addffc72cbb91430"},
|
||||
]
|
||||
mutagen = [
|
||||
{file = "mutagen-1.45.1-py3-none-any.whl", hash = "sha256:9c9f243fcec7f410f138cb12c21c84c64fde4195481a30c9bfb05b5f003adfed"},
|
||||
{file = "mutagen-1.45.1.tar.gz", hash = "sha256:6397602efb3c2d7baebd2166ed85731ae1c1d475abca22090b7141ff5034b3e1"},
|
||||
]
|
||||
import-expression = []
|
||||
importlib-metadata = []
|
||||
jishaku = []
|
||||
lavalink = []
|
||||
line-profiler = []
|
||||
multidict = []
|
||||
mutagen = []
|
||||
mypy-extensions = [
|
||||
{file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
|
||||
{file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
|
||||
]
|
||||
nodeenv = [
|
||||
{file = "nodeenv-1.6.0-py2.py3-none-any.whl", hash = "sha256:621e6b7076565ddcacd2db0294c0381e01fd28945ab36bcf00f41c5daf63bef7"},
|
||||
{file = "nodeenv-1.6.0.tar.gz", hash = "sha256:3ef13ff90291ba2a4a7a4ff9a979b63ffdd00a464dbe04acf0ea6471517a4c2b"},
|
||||
]
|
||||
orjson = [
|
||||
{file = "orjson-3.6.4-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:fc01a15f3101628fd619158daec79b30d7461149735e73542ca8c13be6b835be"},
|
||||
{file = "orjson-3.6.4-cp310-cp310-manylinux_2_24_aarch64.whl", hash = "sha256:48a69fed90f551bf9e9bb7a63e363fed4f67fc7c6e6bfb057054dc78f6721e9e"},
|
||||
{file = "orjson-3.6.4-cp310-cp310-manylinux_2_24_x86_64.whl", hash = "sha256:3722f02f50861d5e2a6be9d50bfe8da27a5155bb60043118a4e1ceb8c7040cf7"},
|
||||
{file = "orjson-3.6.4-cp310-none-win_amd64.whl", hash = "sha256:231a99a728322d0271e970b149c57deb67315e6837e6cd4166cf51d30161700c"},
|
||||
{file = "orjson-3.6.4-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:6cd300421b41f7e84e388b1792a18c3fc4c440ae3039434b9320956be05f0102"},
|
||||
{file = "orjson-3.6.4-cp37-cp37m-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:e55ef66ee1d35b1c43db275aff3a1ba7e0408b31e624912a612bd799df14e73e"},
|
||||
{file = "orjson-3.6.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eef8d332af8e6f7d6d2c1f3b5384c8d239800c1405b136da5f1710e802918d57"},
|
||||
{file = "orjson-3.6.4-cp37-cp37m-manylinux_2_24_aarch64.whl", hash = "sha256:8896e242a92733e454378e22711bd43a55fda4e80604fcefcc064ca977623673"},
|
||||
{file = "orjson-3.6.4-cp37-cp37m-manylinux_2_24_x86_64.whl", hash = "sha256:bdfa6f29f7b6aad70ce14591b99fba651008afa6bc3759f158887bcdc568b452"},
|
||||
{file = "orjson-3.6.4-cp37-none-win_amd64.whl", hash = "sha256:7c16c44872d33da0b97050a9ea8f7bc04e930c56e8185657bc200e1875a671da"},
|
||||
{file = "orjson-3.6.4-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:b467551f3be1dd08aff70c261cc883b63483eb0e31861ffe2cd8dac4fec7cfa9"},
|
||||
{file = "orjson-3.6.4-cp38-cp38-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:7bf61afef12f6416db3ea377f3491ca8ac677d3cac6db1ebffb7a5fe92cce3ca"},
|
||||
{file = "orjson-3.6.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:014ea74d4a5dd6a7e98540768072d5bd8c2fedbcbbedcbbaecbb614e66080e81"},
|
||||
{file = "orjson-3.6.4-cp38-cp38-manylinux_2_24_aarch64.whl", hash = "sha256:705cb90c536b4b9336c06b4a62c3c62e50354ddf20a2e48eb62bf34fb93d5b1f"},
|
||||
{file = "orjson-3.6.4-cp38-cp38-manylinux_2_24_x86_64.whl", hash = "sha256:159e2240fc36720a5cb51a1cbc9905dcb8758aad50b3e7f14f6178ce2e842004"},
|
||||
{file = "orjson-3.6.4-cp38-none-win_amd64.whl", hash = "sha256:d2ae087866a1050de83c2a28490850badb41aeeb8a4605c84dd6004d4e58b5a4"},
|
||||
{file = "orjson-3.6.4-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:b4a7efe039b1154b23e5df8787ac01e4621213aed303b6304a5f8ad89c01455d"},
|
||||
{file = "orjson-3.6.4-cp39-cp39-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:7b24f97ed76005f447e152b0e493abce8c60f010131998295175446312a71caf"},
|
||||
{file = "orjson-3.6.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1121187e2a721864b52e5dbb3cf8dd4a4546519a5fef1e13fa777347fb8884a2"},
|
||||
{file = "orjson-3.6.4-cp39-cp39-manylinux_2_24_aarch64.whl", hash = "sha256:4edffd9e2298ff4f4f939aa67248eba043dc65c9e7d940c28a62c5502c6f2aa8"},
|
||||
{file = "orjson-3.6.4-cp39-cp39-manylinux_2_24_x86_64.whl", hash = "sha256:e236fe94d8a77532f0065870fe265bd53e229012f39af99f79f5f1d4a8b0067c"},
|
||||
{file = "orjson-3.6.4-cp39-none-win_amd64.whl", hash = "sha256:5448cc1edd4c4bafc968404f92f0e9a582b4326ca442346bd1d1179a6faf52d9"},
|
||||
{file = "orjson-3.6.4.tar.gz", hash = "sha256:f8dbc428fc6d7420f231a7133d8dff4c882e64acb585dcf2fda74bdcfe1a6d9d"},
|
||||
]
|
||||
nodeenv = []
|
||||
pathspec = [
|
||||
{file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"},
|
||||
{file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"},
|
||||
]
|
||||
platformdirs = [
|
||||
{file = "platformdirs-2.4.0-py3-none-any.whl", hash = "sha256:8868bbe3c3c80d42f20156f22e7131d2fb321f5bc86a2a345375c6481a67021d"},
|
||||
{file = "platformdirs-2.4.0.tar.gz", hash = "sha256:367a5e80b3d04d2428ffa76d33f124cf11e8fff2acdaa9b43d545f5c7d661ef2"},
|
||||
]
|
||||
pre-commit = [
|
||||
{file = "pre_commit-2.15.0-py2.py3-none-any.whl", hash = "sha256:a4ed01000afcb484d9eb8d504272e642c4c4099bbad3a6b27e519bd6a3e928a6"},
|
||||
{file = "pre_commit-2.15.0.tar.gz", hash = "sha256:3c25add78dbdfb6a28a651780d5c311ac40dd17f160eb3954a0c59da40a505a7"},
|
||||
]
|
||||
platformdirs = []
|
||||
pre-commit = []
|
||||
pycparser = [
|
||||
{file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"},
|
||||
{file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"},
|
||||
]
|
||||
pycryptodomex = [
|
||||
{file = "pycryptodomex-3.11.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:7abfd84a362e4411f7c5f5758c18cbf377a2a2be64b9232e78544d75640c677e"},
|
||||
{file = "pycryptodomex-3.11.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:6a76d7821ae43df8a0e814cca32114875916b9fc2158603b364853de37eb9002"},
|
||||
{file = "pycryptodomex-3.11.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:1580db5878b1d16a233550829f7c189c43005f7aa818f2f95c7dddbd6a7163cc"},
|
||||
{file = "pycryptodomex-3.11.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:c825611a951baad63faeb9ef1517ef96a20202d6029ae2485b729152cc703fab"},
|
||||
{file = "pycryptodomex-3.11.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:7cc5ee80b2d5ee8f59a761741cfb916a068c97cac5e700c8ce01e1927616aa2f"},
|
||||
{file = "pycryptodomex-3.11.0-cp27-cp27m-manylinux2014_aarch64.whl", hash = "sha256:fbe09e3ae95f47c7551a24781d2e348974cde4a0b33bc3b1566f6216479db2b1"},
|
||||
{file = "pycryptodomex-3.11.0-cp27-cp27m-win32.whl", hash = "sha256:9eace1e5420abc4f9e76de01e49caca349b7c80bda9c1643193e23a06c2a332c"},
|
||||
{file = "pycryptodomex-3.11.0-cp27-cp27m-win_amd64.whl", hash = "sha256:adc25aa8cfc537373dd46ae97863f16fd955edee14bf54d3eb52bde4e4ac8c7b"},
|
||||
{file = "pycryptodomex-3.11.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:cf30b5e03d974874185b989839c396d799f6e2d4b4d5b2d8bd3ba464eb3cc33f"},
|
||||
{file = "pycryptodomex-3.11.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:c91772cf6808cc2d80279e80b491c48cb688797b6d914ff624ca95d855c24ee5"},
|
||||
{file = "pycryptodomex-3.11.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:c391ec5c423a374a36b90f7c8805fdf51a0410a2b5be9cebd8990e0021cb6da4"},
|
||||
{file = "pycryptodomex-3.11.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:64a83ab6f54496ab968a6f21a41a620afe0a742573d609fd03dcab7210645153"},
|
||||
{file = "pycryptodomex-3.11.0-cp27-cp27mu-manylinux2014_aarch64.whl", hash = "sha256:252ac9c1e1ae1c256a75539e234be3096f2d100b9f4bae42ef88067787b9b249"},
|
||||
{file = "pycryptodomex-3.11.0-cp35-abi3-macosx_10_9_x86_64.whl", hash = "sha256:bf2ea67eaa1fff0aecef6da881144f0f91e314b4123491f9a4fa8df0598e48fe"},
|
||||
{file = "pycryptodomex-3.11.0-cp35-abi3-manylinux1_i686.whl", hash = "sha256:fe2b8c464ba335e71aed74f830bf2b2881913f8905d166f9c0fe06ca44a1cb5e"},
|
||||
{file = "pycryptodomex-3.11.0-cp35-abi3-manylinux1_x86_64.whl", hash = "sha256:ff0826f3886e85708a0e8ef7ec47020723b998cfed6ae47962d915fcb89ec780"},
|
||||
{file = "pycryptodomex-3.11.0-cp35-abi3-manylinux2010_i686.whl", hash = "sha256:1d4d13c59d2cfbc0863c725f5812d66ff0d6836ba738ef26a52e1291056a1c7c"},
|
||||
{file = "pycryptodomex-3.11.0-cp35-abi3-manylinux2010_x86_64.whl", hash = "sha256:2b586d13ef07fa6197b6348a48dbbe9525f4f496205de14edfa4e91d99e69672"},
|
||||
{file = "pycryptodomex-3.11.0-cp35-abi3-manylinux2014_aarch64.whl", hash = "sha256:f35ccfa44a1dd267e392cd76d8525cfcfabee61dd070e15ad2119c54c0c31ddf"},
|
||||
{file = "pycryptodomex-3.11.0-cp35-abi3-win32.whl", hash = "sha256:5baf690d27f39f2ba22f06e8e32c5f1972573ca65db6bdbb8b2c7177a0112dab"},
|
||||
{file = "pycryptodomex-3.11.0-cp35-abi3-win_amd64.whl", hash = "sha256:919cadcedad552e78349d1626115cfd246fc03ad469a4a62c91a12204f0f0d85"},
|
||||
{file = "pycryptodomex-3.11.0-pp27-pypy_73-macosx_10_9_x86_64.whl", hash = "sha256:c10b2f6bcbaa9aa51fe08207654100074786d423b03482c0cbe44406ca92d146"},
|
||||
{file = "pycryptodomex-3.11.0-pp27-pypy_73-manylinux1_x86_64.whl", hash = "sha256:91662b27f5aa8a6d2ad63be9a7d1a403e07bf3c2c5b265a7cc5cbadf6f988e06"},
|
||||
{file = "pycryptodomex-3.11.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:207e53bdbf3a26de6e9dcf3ebaf67ba70a61f733f84c464eca55d278211c1b71"},
|
||||
{file = "pycryptodomex-3.11.0-pp27-pypy_73-win32.whl", hash = "sha256:1dd4271d8d022216533c3547f071662b44d703fd5dbb632c4b5e77b3ee47567f"},
|
||||
{file = "pycryptodomex-3.11.0-pp36-pypy36_pp73-macosx_10_9_x86_64.whl", hash = "sha256:c43ddcff251e8b427b3e414b026636617276e008a9d78a44a9195d4bdfcaa0fe"},
|
||||
{file = "pycryptodomex-3.11.0-pp36-pypy36_pp73-manylinux1_x86_64.whl", hash = "sha256:ef25d682d0d9ab25c5022a298b5cba9084c7b148a3e71846df2c67ea664eacc7"},
|
||||
{file = "pycryptodomex-3.11.0-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:4c7c6418a3c08b2ebfc2cf50ce52de267618063b533083a2c73b40ec54a1b6f5"},
|
||||
{file = "pycryptodomex-3.11.0-pp36-pypy36_pp73-win32.whl", hash = "sha256:15d25c532de744648f0976c56bd10d07b2a44b7eb2a6261ffe2497980b1102d8"},
|
||||
{file = "pycryptodomex-3.11.0.tar.gz", hash = "sha256:0398366656bb55ebdb1d1d493a7175fc48ade449283086db254ac44c7d318d6d"},
|
||||
]
|
||||
pynacl = [
|
||||
{file = "PyNaCl-1.4.0-cp27-cp27m-macosx_10_10_x86_64.whl", hash = "sha256:ea6841bc3a76fa4942ce00f3bda7d436fda21e2d91602b9e21b7ca9ecab8f3ff"},
|
||||
{file = "PyNaCl-1.4.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:d452a6746f0a7e11121e64625109bc4468fc3100452817001dbe018bb8b08514"},
|
||||
{file = "PyNaCl-1.4.0-cp27-cp27m-win32.whl", hash = "sha256:2fe0fc5a2480361dcaf4e6e7cea00e078fcda07ba45f811b167e3f99e8cff574"},
|
||||
{file = "PyNaCl-1.4.0-cp27-cp27m-win_amd64.whl", hash = "sha256:f8851ab9041756003119368c1e6cd0b9c631f46d686b3904b18c0139f4419f80"},
|
||||
{file = "PyNaCl-1.4.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:7757ae33dae81c300487591c68790dfb5145c7d03324000433d9a2c141f82af7"},
|
||||
{file = "PyNaCl-1.4.0-cp35-abi3-macosx_10_10_x86_64.whl", hash = "sha256:757250ddb3bff1eecd7e41e65f7f833a8405fede0194319f87899690624f2122"},
|
||||
{file = "PyNaCl-1.4.0-cp35-abi3-manylinux1_x86_64.whl", hash = "sha256:30f9b96db44e09b3304f9ea95079b1b7316b2b4f3744fe3aaecccd95d547063d"},
|
||||
{file = "PyNaCl-1.4.0-cp35-abi3-win32.whl", hash = "sha256:4e10569f8cbed81cb7526ae137049759d2a8d57726d52c1a000a3ce366779634"},
|
||||
{file = "PyNaCl-1.4.0-cp35-abi3-win_amd64.whl", hash = "sha256:c914f78da4953b33d4685e3cdc7ce63401247a21425c16a39760e282075ac4a6"},
|
||||
{file = "PyNaCl-1.4.0-cp35-cp35m-win32.whl", hash = "sha256:06cbb4d9b2c4bd3c8dc0d267416aaed79906e7b33f114ddbf0911969794b1cc4"},
|
||||
{file = "PyNaCl-1.4.0-cp35-cp35m-win_amd64.whl", hash = "sha256:511d269ee845037b95c9781aa702f90ccc36036f95d0f31373a6a79bd8242e25"},
|
||||
{file = "PyNaCl-1.4.0-cp36-cp36m-win32.whl", hash = "sha256:11335f09060af52c97137d4ac54285bcb7df0cef29014a1a4efe64ac065434c4"},
|
||||
{file = "PyNaCl-1.4.0-cp36-cp36m-win_amd64.whl", hash = "sha256:cd401ccbc2a249a47a3a1724c2918fcd04be1f7b54eb2a5a71ff915db0ac51c6"},
|
||||
{file = "PyNaCl-1.4.0-cp37-cp37m-win32.whl", hash = "sha256:8122ba5f2a2169ca5da936b2e5a511740ffb73979381b4229d9188f6dcb22f1f"},
|
||||
{file = "PyNaCl-1.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:537a7ccbea22905a0ab36ea58577b39d1fa9b1884869d173b5cf111f006f689f"},
|
||||
{file = "PyNaCl-1.4.0-cp38-cp38-win32.whl", hash = "sha256:9c4a7ea4fb81536c1b1f5cc44d54a296f96ae78c1ebd2311bd0b60be45a48d96"},
|
||||
{file = "PyNaCl-1.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:7c6092102219f59ff29788860ccb021e80fffd953920c4a8653889c029b2d420"},
|
||||
{file = "PyNaCl-1.4.0.tar.gz", hash = "sha256:54e9a2c849c742006516ad56a88f5c74bf2ce92c9f67435187c3c5953b346505"},
|
||||
]
|
||||
pytube = [
|
||||
{file = "pytube-11.0.1-py3-none-any.whl", hash = "sha256:d4dfea7394d7662edac3831432b349b2afac984e0d0bc4bdb611ec1f3fc16318"},
|
||||
{file = "pytube-11.0.1.tar.gz", hash = "sha256:47643a6ff553cbc4d6be748ff14c9c45f79984e15005adff25d62b18110abe43"},
|
||||
]
|
||||
pycryptodomex = []
|
||||
pynacl = []
|
||||
pytube = []
|
||||
pyyaml = [
|
||||
{file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"},
|
||||
{file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"},
|
||||
@@ -838,86 +714,17 @@ pyyaml = [
|
||||
{file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"},
|
||||
{file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"},
|
||||
]
|
||||
redis = [
|
||||
{file = "redis-3.5.3-py2.py3-none-any.whl", hash = "sha256:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24"},
|
||||
{file = "redis-3.5.3.tar.gz", hash = "sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2"},
|
||||
]
|
||||
regex = [
|
||||
{file = "regex-2021.11.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:897c539f0f3b2c3a715be651322bef2167de1cdc276b3f370ae81a3bda62df71"},
|
||||
{file = "regex-2021.11.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:886f459db10c0f9d17c87d6594e77be915f18d343ee138e68d259eb385f044a8"},
|
||||
{file = "regex-2021.11.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:075b0fdbaea81afcac5a39a0d1bb91de887dd0d93bf692a5dd69c430e7fc58cb"},
|
||||
{file = "regex-2021.11.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c6238d30dcff141de076344cf7f52468de61729c2f70d776fce12f55fe8df790"},
|
||||
{file = "regex-2021.11.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7fab29411d75c2eb48070020a40f80255936d7c31357b086e5931c107d48306e"},
|
||||
{file = "regex-2021.11.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0148988af0182a0a4e5020e7c168014f2c55a16d11179610f7883dd48ac0ebe"},
|
||||
{file = "regex-2021.11.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:be30cd315db0168063a1755fa20a31119da91afa51da2907553493516e165640"},
|
||||
{file = "regex-2021.11.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e9cec3a62d146e8e122d159ab93ac32c988e2ec0dcb1e18e9e53ff2da4fbd30c"},
|
||||
{file = "regex-2021.11.2-cp310-cp310-win32.whl", hash = "sha256:41c66bd6750237a8ed23028a6c9173dc0c92dc24c473e771d3bfb9ee817700c3"},
|
||||
{file = "regex-2021.11.2-cp310-cp310-win_amd64.whl", hash = "sha256:0075fe4e2c2720a685fef0f863edd67740ff78c342cf20b2a79bc19388edf5db"},
|
||||
{file = "regex-2021.11.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:0ed3465acf8c7c10aa2e0f3d9671da410ead63b38a77283ef464cbb64275df58"},
|
||||
{file = "regex-2021.11.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab1fea8832976ad0bebb11f652b692c328043057d35e9ebc78ab0a7a30cf9a70"},
|
||||
{file = "regex-2021.11.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cb1e44d860345ab5d4f533b6c37565a22f403277f44c4d2d5e06c325da959883"},
|
||||
{file = "regex-2021.11.2-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9486ebda015913909bc28763c6b92fcc3b5e5a67dee4674bceed112109f5dfb8"},
|
||||
{file = "regex-2021.11.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20605bfad484e1341b2cbfea0708e4b211d233716604846baa54b94821f487cb"},
|
||||
{file = "regex-2021.11.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f20f9f430c33597887ba9bd76635476928e76cad2981643ca8be277b8e97aa96"},
|
||||
{file = "regex-2021.11.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1d85ca137756d62c8138c971453cafe64741adad1f6a7e63a22a5a8abdbd19fa"},
|
||||
{file = "regex-2021.11.2-cp36-cp36m-win32.whl", hash = "sha256:af23b9ca9a874ef0ec20e44467b8edd556c37b0f46f93abfa93752ea7c0e8d1e"},
|
||||
{file = "regex-2021.11.2-cp36-cp36m-win_amd64.whl", hash = "sha256:070336382ca92c16c45b4066c4ba9fa83fb0bd13d5553a82e07d344df8d58a84"},
|
||||
{file = "regex-2021.11.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:ef4e53e2fdc997d91f5b682f81f7dc9661db9a437acce28745d765d251902d85"},
|
||||
{file = "regex-2021.11.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35ed5714467fc606551db26f80ee5d6aa1f01185586a7bccd96f179c4b974a11"},
|
||||
{file = "regex-2021.11.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ee36d5113b6506b97f45f2e8447cb9af146e60e3f527d93013d19f6d0405f3b"},
|
||||
{file = "regex-2021.11.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4fba661a4966adbd2c3c08d3caad6822ecb6878f5456588e2475ae23a6e47929"},
|
||||
{file = "regex-2021.11.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77f9d16f7970791f17ecce7e7f101548314ed1ee2583d4268601f30af3170856"},
|
||||
{file = "regex-2021.11.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f6a28e87ba69f3a4f30d775b179aac55be1ce59f55799328a0d9b6df8f16b39d"},
|
||||
{file = "regex-2021.11.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9267e4fba27e6dd1008c4f2983cc548c98b4be4444e3e342db11296c0f45512f"},
|
||||
{file = "regex-2021.11.2-cp37-cp37m-win32.whl", hash = "sha256:d4bfe3bc3976ccaeb4ae32f51e631964e2f0e85b2b752721b7a02de5ce3b7f27"},
|
||||
{file = "regex-2021.11.2-cp37-cp37m-win_amd64.whl", hash = "sha256:2bb7cae741de1aa03e3dd3a7d98c304871eb155921ca1f0d7cc11f5aade913fd"},
|
||||
{file = "regex-2021.11.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:23f93e74409c210de4de270d4bf88fb8ab736a7400f74210df63a93728cf70d6"},
|
||||
{file = "regex-2021.11.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d8ee91e1c295beb5c132ebd78616814de26fedba6aa8687ea460c7f5eb289b72"},
|
||||
{file = "regex-2021.11.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e3ff69ab203b54ce5c480c3ccbe959394ea5beef6bd5ad1785457df7acea92e"},
|
||||
{file = "regex-2021.11.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e3c00cb5c71da655e1e5161481455479b613d500dd1bd252aa01df4f037c641f"},
|
||||
{file = "regex-2021.11.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4abf35e16f4b639daaf05a2602c1b1d47370e01babf9821306aa138924e3fe92"},
|
||||
{file = "regex-2021.11.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb11c982a849dc22782210b01d0c1b98eb3696ce655d58a54180774e4880ac66"},
|
||||
{file = "regex-2021.11.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e3755e0f070bc31567dfe447a02011bfa8444239b3e9e5cca6773a22133839"},
|
||||
{file = "regex-2021.11.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0621c90f28d17260b41838b22c81a79ff436141b322960eb49c7b3f91d1cbab6"},
|
||||
{file = "regex-2021.11.2-cp38-cp38-win32.whl", hash = "sha256:8fbe1768feafd3d0156556677b8ff234c7bf94a8110e906b2d73506f577a3269"},
|
||||
{file = "regex-2021.11.2-cp38-cp38-win_amd64.whl", hash = "sha256:f9ee98d658a146cb6507be720a0ce1b44f2abef8fb43c2859791d91aace17cd5"},
|
||||
{file = "regex-2021.11.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b3794cea825f101fe0df9af8a00f9fad8e119c91e39a28636b95ee2b45b6c2e5"},
|
||||
{file = "regex-2021.11.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3576e173e7b4f88f683b4de7db0c2af1b209bb48b2bf1c827a6f3564fad59a97"},
|
||||
{file = "regex-2021.11.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48b4f4810117a9072a5aa70f7fea5f86fa9efbe9a798312e0a05044bd707cc33"},
|
||||
{file = "regex-2021.11.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f5930d334c2f607711d54761956aedf8137f83f1b764b9640be21d25a976f3a4"},
|
||||
{file = "regex-2021.11.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:956187ff49db7014ceb31e88fcacf4cf63371e6e44d209cf8816cd4a2d61e11a"},
|
||||
{file = "regex-2021.11.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17e095f7f96a4b9f24b93c2c915f31a5201a6316618d919b0593afb070a5270e"},
|
||||
{file = "regex-2021.11.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a56735c35a3704603d9d7b243ee06139f0837bcac2171d9ba1d638ce1df0742a"},
|
||||
{file = "regex-2021.11.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:adf35d88d9cffc202e6046e4c32e1e11a1d0238b2fcf095c94f109e510ececea"},
|
||||
{file = "regex-2021.11.2-cp39-cp39-win32.whl", hash = "sha256:30fe317332de0e50195665bc61a27d46e903d682f94042c36b3f88cb84bd7958"},
|
||||
{file = "regex-2021.11.2-cp39-cp39-win_amd64.whl", hash = "sha256:85289c25f658e3260b00178757c87f033f3d4b3e40aa4abdd4dc875ff11a94fb"},
|
||||
{file = "regex-2021.11.2.tar.gz", hash = "sha256:5e85dcfc5d0f374955015ae12c08365b565c6f1eaf36dd182476a4d8e5a1cdb7"},
|
||||
]
|
||||
requests = [
|
||||
{file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"},
|
||||
{file = "requests-2.26.0.tar.gz", hash = "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"},
|
||||
]
|
||||
redis = []
|
||||
regex = []
|
||||
requests = []
|
||||
six = [
|
||||
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
|
||||
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
|
||||
]
|
||||
toml = [
|
||||
{file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
|
||||
{file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
|
||||
]
|
||||
tomli = [
|
||||
{file = "tomli-1.2.2-py3-none-any.whl", hash = "sha256:f04066f68f5554911363063a30b108d2b5a5b1a010aa8b6132af78489fe3aade"},
|
||||
{file = "tomli-1.2.2.tar.gz", hash = "sha256:c6ce0015eb38820eaf32b5db832dbc26deb3dd427bd5f6556cf0acac2c214fee"},
|
||||
]
|
||||
typing-extensions = [
|
||||
{file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"},
|
||||
{file = "typing_extensions-3.10.0.2-py3-none-any.whl", hash = "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"},
|
||||
{file = "typing_extensions-3.10.0.2.tar.gz", hash = "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e"},
|
||||
]
|
||||
urllib3 = [
|
||||
{file = "urllib3-1.26.7-py2.py3-none-any.whl", hash = "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844"},
|
||||
{file = "urllib3-1.26.7.tar.gz", hash = "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece"},
|
||||
]
|
||||
toml = []
|
||||
tomli = []
|
||||
typing-extensions = []
|
||||
urllib3 = []
|
||||
uvloop = [
|
||||
{file = "uvloop-0.16.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6224f1401025b748ffecb7a6e2652b17768f30b1a6a3f7b44660e5b5b690b12d"},
|
||||
{file = "uvloop-0.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:30ba9dcbd0965f5c812b7c2112a1ddf60cf904c1c160f398e7eed3a6b82dcd9c"},
|
||||
@@ -936,57 +743,8 @@ uvloop = [
|
||||
{file = "uvloop-0.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e5f2e2ff51aefe6c19ee98af12b4ae61f5be456cd24396953244a30880ad861"},
|
||||
{file = "uvloop-0.16.0.tar.gz", hash = "sha256:f74bc20c7b67d1c27c72601c78cf95be99d5c2cdd4514502b4f3eb0933ff1228"},
|
||||
]
|
||||
websockets = [
|
||||
{file = "websockets-10.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cd8c6f2ec24aedace251017bc7a414525171d4e6578f914acab9349362def4da"},
|
||||
{file = "websockets-10.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:1f6b814cff6aadc4288297cb3a248614829c6e4ff5556593c44a115e9dd49939"},
|
||||
{file = "websockets-10.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:01db0ecd1a0ca6702d02a5ed40413e18b7d22f94afb3bbe0d323bac86c42c1c8"},
|
||||
{file = "websockets-10.0-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:82b17524b1ce6ae7f7dd93e4d18e9b9474071e28b65dbf1dfe9b5767778db379"},
|
||||
{file = "websockets-10.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:8bbf8660c3f833ddc8b1afab90213f2e672a9ddac6eecb3cde968e6b2807c1c7"},
|
||||
{file = "websockets-10.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b8176deb6be540a46695960a765a77c28ac8b2e3ef2ec95d50a4f5df901edb1c"},
|
||||
{file = "websockets-10.0-cp37-cp37m-win32.whl", hash = "sha256:706e200fc7f03bed99ad0574cd1ea8b0951477dd18cc978ccb190683c69dba76"},
|
||||
{file = "websockets-10.0-cp37-cp37m-win_amd64.whl", hash = "sha256:5b2600e01c7ca6f840c42c747ffbe0254f319594ed108db847eb3d75f4aacb80"},
|
||||
{file = "websockets-10.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:085bb8a6e780d30eaa1ba48ac7f3a6707f925edea787cfb761ce5a39e77ac09b"},
|
||||
{file = "websockets-10.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:9a4d889162bd48588e80950e07fa5e039eee9deb76a58092e8c3ece96d7ef537"},
|
||||
{file = "websockets-10.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:b4ade7569b6fd17912452f9c3757d96f8e4044016b6d22b3b8391e641ca50456"},
|
||||
{file = "websockets-10.0-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:2a43072e434c041a99f2e1eb9b692df0232a38c37c61d00e9f24db79474329e4"},
|
||||
{file = "websockets-10.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:7f79f02c7f9a8320aff7d3321cd1c7e3a7dbc15d922ac996cca827301ee75238"},
|
||||
{file = "websockets-10.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:1ac35426fe3e7d3d0fac3d63c8965c76ed67a8fd713937be072bf0ce22808539"},
|
||||
{file = "websockets-10.0-cp38-cp38-win32.whl", hash = "sha256:ff59c6bdb87b31f7e2d596f09353d5a38c8c8ff571b0e2238e8ee2d55ad68465"},
|
||||
{file = "websockets-10.0-cp38-cp38-win_amd64.whl", hash = "sha256:d67646ddd17a86117ae21c27005d83c1895c0cef5d7be548b7549646372f868a"},
|
||||
{file = "websockets-10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:82bd921885231f4a30d9bc550552495b3fc36b1235add6d374e7c65c3babd805"},
|
||||
{file = "websockets-10.0-cp39-cp39-manylinux1_i686.whl", hash = "sha256:7d2e12e4f901f1bc062dfdf91831712c4106ed18a9a4cdb65e2e5f502124ca37"},
|
||||
{file = "websockets-10.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:71358c7816e2762f3e4af3adf0040f268e219f5a38cb3487a9d0fc2e554fef6a"},
|
||||
{file = "websockets-10.0-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:fe83b3ec9ef34063d86dfe1029160a85f24a5a94271036e5714a57acfdd089a1"},
|
||||
{file = "websockets-10.0-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:eb282127e9c136f860c6068a4fba5756eb25e755baffb5940b6f1eae071928b2"},
|
||||
{file = "websockets-10.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:62160772314920397f9d219147f958b33fa27a12c662d4455c9ccbba9a07e474"},
|
||||
{file = "websockets-10.0-cp39-cp39-win32.whl", hash = "sha256:e42a1f1e03437b017af341e9bbfdc09252cd48ef32a8c3c3ead769eab3b17368"},
|
||||
{file = "websockets-10.0-cp39-cp39-win_amd64.whl", hash = "sha256:c5880442f5fc268f1ef6d37b2c152c114deccca73f48e3a8c48004d2f16f4567"},
|
||||
{file = "websockets-10.0.tar.gz", hash = "sha256:c4fc9a1d242317892590abe5b61a9127f1a61740477bfb121743f290b8054002"},
|
||||
]
|
||||
virtualenv = [
|
||||
{file = "virtualenv-20.10.0-py2.py3-none-any.whl", hash = "sha256:4b02e52a624336eece99c96e3ab7111f469c24ba226a53ec474e8e787b365814"},
|
||||
{file = "virtualenv-20.10.0.tar.gz", hash = "sha256:576d05b46eace16a9c348085f7d0dc8ef28713a2cabaa1cf0aea41e8f12c9218"},
|
||||
]
|
||||
yarl = [
|
||||
{file = "yarl-1.5.1-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:db6db0f45d2c63ddb1a9d18d1b9b22f308e52c83638c26b422d520a815c4b3fb"},
|
||||
{file = "yarl-1.5.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:17668ec6722b1b7a3a05cc0167659f6c95b436d25a36c2d52db0eca7d3f72593"},
|
||||
{file = "yarl-1.5.1-cp35-cp35m-win32.whl", hash = "sha256:040b237f58ff7d800e6e0fd89c8439b841f777dd99b4a9cca04d6935564b9409"},
|
||||
{file = "yarl-1.5.1-cp35-cp35m-win_amd64.whl", hash = "sha256:f18d68f2be6bf0e89f1521af2b1bb46e66ab0018faafa81d70f358153170a317"},
|
||||
{file = "yarl-1.5.1-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:c52ce2883dc193824989a9b97a76ca86ecd1fa7955b14f87bf367a61b6232511"},
|
||||
{file = "yarl-1.5.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:ce584af5de8830d8701b8979b18fcf450cef9a382b1a3c8ef189bedc408faf1e"},
|
||||
{file = "yarl-1.5.1-cp36-cp36m-win32.whl", hash = "sha256:df89642981b94e7db5596818499c4b2219028f2a528c9c37cc1de45bf2fd3a3f"},
|
||||
{file = "yarl-1.5.1-cp36-cp36m-win_amd64.whl", hash = "sha256:3a584b28086bc93c888a6c2aa5c92ed1ae20932f078c46509a66dce9ea5533f2"},
|
||||
{file = "yarl-1.5.1-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:da456eeec17fa8aa4594d9a9f27c0b1060b6a75f2419fe0c00609587b2695f4a"},
|
||||
{file = "yarl-1.5.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:bc2f976c0e918659f723401c4f834deb8a8e7798a71be4382e024bcc3f7e23a8"},
|
||||
{file = "yarl-1.5.1-cp37-cp37m-win32.whl", hash = "sha256:4439be27e4eee76c7632c2427ca5e73703151b22cae23e64adb243a9c2f565d8"},
|
||||
{file = "yarl-1.5.1-cp37-cp37m-win_amd64.whl", hash = "sha256:48e918b05850fffb070a496d2b5f97fc31d15d94ca33d3d08a4f86e26d4e7c5d"},
|
||||
{file = "yarl-1.5.1-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:9b930776c0ae0c691776f4d2891ebc5362af86f152dd0da463a6614074cb1b02"},
|
||||
{file = "yarl-1.5.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:b3b9ad80f8b68519cc3372a6ca85ae02cc5a8807723ac366b53c0f089db19e4a"},
|
||||
{file = "yarl-1.5.1-cp38-cp38-win32.whl", hash = "sha256:f379b7f83f23fe12823085cd6b906edc49df969eb99757f58ff382349a3303c6"},
|
||||
{file = "yarl-1.5.1-cp38-cp38-win_amd64.whl", hash = "sha256:9102b59e8337f9874638fcfc9ac3734a0cfadb100e47d55c20d0dc6087fb4692"},
|
||||
{file = "yarl-1.5.1.tar.gz", hash = "sha256:c22c75b5f394f3d47105045ea551e08a3e804dc7e01b37800ca35b58f856c3d6"},
|
||||
]
|
||||
yt-dlp = [
|
||||
{file = "yt-dlp-2021.10.22.tar.gz", hash = "sha256:a24b9666bd2234149e4da8c4f16bb8e5f746c29428d12ee04fc1c11b5247a307"},
|
||||
{file = "yt_dlp-2021.10.22-py2.py3-none-any.whl", hash = "sha256:4900fdfffa3de0b09a74f3d7fe98e4d5e21f3ce74db0c106c942614f2ebc3368"},
|
||||
]
|
||||
virtualenv = []
|
||||
websockets = []
|
||||
yarl = []
|
||||
yt-dlp = []
|
||||
zipp = []
|
||||
|
||||
+3
-2
@@ -7,11 +7,12 @@ license = "GPL-v3.0"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.8"
|
||||
lavalink = "^3.1.4"
|
||||
lavalink = "4.0.4"
|
||||
humanize = "^3.12.0"
|
||||
"discord.py" = { git = "https://github.com/iDevision/enhanced-discord.py", branch = "2.0", extras = ["voice", "speed"] }
|
||||
uvloop = {version = "^0.16.0", optional = true}
|
||||
aioredis = "^2.0.0"
|
||||
"discord.py" = {version = "^2.0.1", extras = ["voice"]}
|
||||
jishaku = "^2.5.1"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
black = {version = "^21.9b0", allow-prereleases = true}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
from tunebot.abc import *
|
||||
@@ -0,0 +1,58 @@
|
||||
from abc import ABC
|
||||
from abc import abstractmethod
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class GlobalPlaylistSource(ABC):
|
||||
@abstractmethod
|
||||
async def fetch_sources(self) -> set[str]:
|
||||
pass
|
||||
|
||||
|
||||
class PlaylistSource(ABC):
|
||||
@abstractmethod
|
||||
async def add(self, source_url: str):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def remove(self, source_url: str) -> bool:
|
||||
pass
|
||||
|
||||
|
||||
class GlobalPlaylist(ABC):
|
||||
@abstractmethod
|
||||
async def pick_random(self, amount: Optional[int] = 1) -> set[str]:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def add_tracks(self, urls: list[str]):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def clear(self):
|
||||
pass
|
||||
|
||||
|
||||
class GlobalAutoJoin(ABC):
|
||||
@abstractmethod
|
||||
async def fetch_channels(self) -> dict[str, list[str]]:
|
||||
pass
|
||||
|
||||
|
||||
class AutoJoin(ABC):
|
||||
@abstractmethod
|
||||
async def update(self, voice_channel_id: int, text_channel_id: int):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def disable(self):
|
||||
pass
|
||||
|
||||
|
||||
__all__ = (
|
||||
"GlobalPlaylistSource",
|
||||
"PlaylistSource",
|
||||
"GlobalPlaylist",
|
||||
"GlobalAutoJoin",
|
||||
"AutoJoin",
|
||||
)
|
||||
@@ -0,0 +1,28 @@
|
||||
import typing
|
||||
from dataclasses import dataclass
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from aioredis import Redis
|
||||
from bot import TuneBot
|
||||
from discord import Interaction
|
||||
from context import CustomContext
|
||||
|
||||
|
||||
@dataclass
|
||||
class ContextLike:
|
||||
redis: "Redis"
|
||||
bot: "TuneBot"
|
||||
guild_id: int
|
||||
|
||||
@staticmethod
|
||||
def from_discord_context(ctx: "CustomContext") -> "ContextLike":
|
||||
return ContextLike(redis=ctx.redis, bot=ctx.bot, guild_id=ctx.guild.id)
|
||||
|
||||
@staticmethod
|
||||
def from_discord_interaction(ctx: "Interaction") -> "ContextLike":
|
||||
return ContextLike(
|
||||
redis=ctx.client._redis_client, bot=ctx.client, guild_id=ctx.guild_id
|
||||
)
|
||||
|
||||
|
||||
__all__ = ("ContextLike",)
|
||||
@@ -0,0 +1,4 @@
|
||||
from tunebot.redis.entity import * # noreorder
|
||||
from tunebot.redis.autojoin import *
|
||||
from tunebot.redis.playlist import *
|
||||
from tunebot.redis.playlist_source import *
|
||||
@@ -0,0 +1,41 @@
|
||||
from tunebot import AutoJoin
|
||||
from tunebot import GlobalAutoJoin
|
||||
from tunebot.redis import RedisBotEntity
|
||||
from tunebot.redis import RedisContextEntity
|
||||
|
||||
|
||||
class GlobalRedisAutoJoin(RedisBotEntity, GlobalAutoJoin):
|
||||
async def fetch_channels(self) -> dict[str, list[str]]:
|
||||
"""
|
||||
Retrieves all guilds (with their configurations) where AutoJoin is enabled
|
||||
"""
|
||||
channels: dict[str, str] = await self.redis.hgetall(self.key("autojoin"))
|
||||
return {key: value.split(":") for key, value in channels.items()}
|
||||
|
||||
|
||||
class RedisAutoJoin(RedisContextEntity, AutoJoin):
|
||||
async def update(self, voice_channel_id: int, text_channel_id: int):
|
||||
"""
|
||||
Upserts the configuration of an AutoJoin guild.
|
||||
|
||||
Args:
|
||||
voice_channel_id (int): [description]
|
||||
text_channel_id (int): [description]
|
||||
"""
|
||||
if not self.ctx.guild_id:
|
||||
raise Exception("This method can only be invoked inside of a guild.")
|
||||
|
||||
value = f"{voice_channel_id}:{text_channel_id}"
|
||||
await self.redis.hset(self.key("autojoin"), self.ctx.guild_id, value)
|
||||
|
||||
async def disable(self):
|
||||
"""
|
||||
Removes the AutoJoin configuration of a guild.
|
||||
"""
|
||||
if not self.ctx.guild_id:
|
||||
raise Exception("This method can only be invoked inside of a guild.")
|
||||
|
||||
await self.redis.hdel(self.key("autojoin"), self.ctx.guild_id)
|
||||
|
||||
|
||||
__all__ = ("GlobalRedisAutoJoin", "RedisAutoJoin")
|
||||
@@ -0,0 +1,49 @@
|
||||
from abc import ABC
|
||||
from abc import abstractmethod
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tunebot.context import ContextLike
|
||||
|
||||
from aioredis.client import Redis
|
||||
|
||||
|
||||
class RedisEntity(ABC):
|
||||
@property
|
||||
@abstractmethod
|
||||
def redis() -> Redis:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def key(self, name: str) -> str:
|
||||
pass
|
||||
|
||||
|
||||
class RedisBotEntity(RedisEntity):
|
||||
def __init__(self, redis: Redis, prefix: str) -> None:
|
||||
self._redis = redis
|
||||
self.prefix = prefix
|
||||
super().__init__()
|
||||
|
||||
@property
|
||||
def redis(self) -> Redis:
|
||||
return self._redis
|
||||
|
||||
def key(self, name: str) -> str:
|
||||
return ":".join([self.prefix, name])
|
||||
|
||||
|
||||
class RedisContextEntity(RedisEntity):
|
||||
def __init__(self, ctx: "ContextLike") -> None:
|
||||
self.ctx = ctx
|
||||
super().__init__()
|
||||
|
||||
@property
|
||||
def redis(self) -> Redis:
|
||||
return self.ctx.redis
|
||||
|
||||
def key(self, name: str) -> str:
|
||||
return ":".join([self.ctx.bot.redis_prefix, name])
|
||||
|
||||
|
||||
__all__ = ("RedisEntity", "RedisBotEntity", "RedisContextEntity")
|
||||
@@ -0,0 +1,33 @@
|
||||
from typing import Optional
|
||||
|
||||
from tunebot.abc import GlobalPlaylist
|
||||
from tunebot.redis import RedisBotEntity
|
||||
|
||||
|
||||
class GlobalRedisPlaylist(RedisBotEntity, GlobalPlaylist):
|
||||
async def pick_random(self, amount: Optional[int] = 1) -> set[str]:
|
||||
"""
|
||||
Picks an amount of random tracks from the playlist in Redis
|
||||
|
||||
Args:
|
||||
amount (Optional[int], optional): [description]. Defaults to 1.
|
||||
"""
|
||||
return await self.redis.srandmember(self.key("playlist"), amount)
|
||||
|
||||
async def add_tracks(self, urls: list[str]):
|
||||
"""
|
||||
Adds one or more urls to the playlist in Redis
|
||||
|
||||
Args:
|
||||
urls (list[str]): [description]
|
||||
"""
|
||||
await self.redis.sadd(self.key("playlist"), *urls)
|
||||
|
||||
async def clear(self):
|
||||
"""
|
||||
Clears the entire playlist in Redis
|
||||
"""
|
||||
await self.redis.delete(self.key("playlist"))
|
||||
|
||||
|
||||
__all__ = ("GlobalRedisPlaylist",)
|
||||
@@ -0,0 +1,35 @@
|
||||
from tunebot import GlobalPlaylistSource
|
||||
from tunebot import PlaylistSource
|
||||
from tunebot.redis import RedisBotEntity
|
||||
from tunebot.redis import RedisContextEntity
|
||||
|
||||
|
||||
class GlobalRedisPlaylistSource(RedisBotEntity, GlobalPlaylistSource):
|
||||
async def fetch_sources(self) -> set[str]:
|
||||
"""
|
||||
Fetches all sources
|
||||
"""
|
||||
return await self.redis.smembers(self.key("sources"))
|
||||
|
||||
|
||||
class RedisPlaylistSource(RedisContextEntity, PlaylistSource):
|
||||
async def add(self, source_url: str):
|
||||
"""
|
||||
Adds a playlist source to Redis
|
||||
|
||||
Args:
|
||||
source_url (str): [description]
|
||||
"""
|
||||
await self.redis.sadd(self.key("sources"), source_url)
|
||||
|
||||
async def remove(self, source_url: str) -> bool:
|
||||
"""
|
||||
Removes a playlist source from Redis
|
||||
|
||||
Args:
|
||||
source_url (str): [description]
|
||||
"""
|
||||
return await self.redis.srem(self.key("sources"), source_url)
|
||||
|
||||
|
||||
__all__ = ("GlobalRedisPlaylistSource", "RedisPlaylistSource")
|
||||
@@ -1,47 +0,0 @@
|
||||
from typing import Optional
|
||||
from typing import Union
|
||||
|
||||
import discord
|
||||
from discord import Embed
|
||||
from discord.ext.commands import Context
|
||||
|
||||
|
||||
class EmbedGenerator:
|
||||
@staticmethod
|
||||
async def Error(ctx: Context, message: str, **kwargs) -> Embed:
|
||||
color = ctx.bot.colors["embed"]
|
||||
em = Embed(title="Error:", description=message, color=color)
|
||||
return await EmbedGenerator.SendWithFooter(ctx, em, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
async def Message(
|
||||
ctx: Context, title: str, message: Optional[str] = "", **kwargs
|
||||
) -> Embed:
|
||||
color = ctx.bot.colors["embed"]
|
||||
em = Embed(title=title, description=message, color=color)
|
||||
return await EmbedGenerator.SendWithFooter(ctx, em, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
async def Image(
|
||||
ctx: Context, title: str, url: str, message: Optional[str] = "", **kwargs
|
||||
) -> Embed:
|
||||
color = ctx.bot.colors["embed"]
|
||||
em = Embed(title=title, description=message, url=url, color=color)
|
||||
em.set_image(url=url)
|
||||
return await EmbedGenerator.SendWithFooter(ctx, em, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
async def Title(ctx: Context, title: str, **kwargs) -> Embed:
|
||||
color = ctx.bot.colors["embed"]
|
||||
em = Embed(title=title, color=color)
|
||||
return await EmbedGenerator.SendWithFooter(ctx, em, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
async def SendWithFooter(
|
||||
ctx: Context, em: Embed, **kwargs
|
||||
) -> Union[discord.Message, Embed]:
|
||||
avatar = ctx.author.avatar.with_static_format("jpeg")
|
||||
em.set_footer(text=f"Requested by: {ctx.author}", icon_url=avatar)
|
||||
if kwargs.get("no_send", False):
|
||||
return em
|
||||
return await ctx.send(embed=em, **kwargs)
|
||||
@@ -0,0 +1,10 @@
|
||||
import typing
|
||||
|
||||
import discord
|
||||
|
||||
|
||||
def process_colours(colors: typing.Dict[str, str]) -> typing.Dict[str, discord.Color]:
|
||||
colour_dict: typing.Dict[str, discord.Color] = {}
|
||||
for name, color in colors.items():
|
||||
colour_dict[name] = discord.Color(int(color, 16))
|
||||
return colour_dict
|
||||
@@ -1,3 +1,4 @@
|
||||
import asyncio
|
||||
from typing import Dict
|
||||
|
||||
from discord.ext.commands import Cog
|
||||
@@ -13,3 +14,9 @@ class BaseCog(Cog):
|
||||
for command in self.walk_commands():
|
||||
if brief := slash_descriptions.get(command.qualified_name):
|
||||
command.brief = brief
|
||||
|
||||
def is_lavalink_ready(self) -> bool:
|
||||
return (
|
||||
hasattr(self.bot, "lavalink")
|
||||
and len(self.bot.lavalink.node_manager.available_nodes) > 0
|
||||
)
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
from typing import Dict
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
|
||||
from aioredis import Redis
|
||||
|
||||
from bot import redis_prefix
|
||||
|
||||
|
||||
class AutoJoin:
|
||||
@staticmethod
|
||||
async def get_channels(redis: Redis) -> Dict[str, str]:
|
||||
# return all channels
|
||||
channels = await redis.hgetall(f"{redis_prefix}:autojoin")
|
||||
return {key: value.split("-") for key, value in channels.items()}
|
||||
|
||||
@staticmethod
|
||||
async def update_channel(
|
||||
redis: Redis, guild_id: int, voice_channel_id: int, text_channel_id: int
|
||||
):
|
||||
await redis.hset(
|
||||
f"{redis_prefix}:autojoin",
|
||||
guild_id,
|
||||
f"{voice_channel_id}-{text_channel_id}",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
async def del_channel(redis: Redis, guild_id: int):
|
||||
await redis.hdel(f"{redis_prefix}:autojoin", guild_id)
|
||||
|
||||
|
||||
class Playlist:
|
||||
@staticmethod
|
||||
async def random(redis: Redis, amount: Optional[int] = 1) -> List[str]:
|
||||
return await redis.srandmember(f"{redis_prefix}:playlist", amount)
|
||||
|
||||
@staticmethod
|
||||
async def add_bulk(redis: Redis, urls: list[str]):
|
||||
await redis.sadd(f"{redis_prefix}:playlist", *urls)
|
||||
|
||||
@staticmethod
|
||||
async def clear(redis: Redis):
|
||||
await redis.delete(f"{redis_prefix}:playlist")
|
||||
|
||||
|
||||
class PlaylistSource:
|
||||
@staticmethod
|
||||
async def get_all(redis: Redis) -> list[str]:
|
||||
return await redis.smembers(f"{redis_prefix}:sources")
|
||||
|
||||
@staticmethod
|
||||
async def add(redis: Redis, source_url: str):
|
||||
await redis.sadd(f"{redis_prefix}:sources", source_url)
|
||||
|
||||
@staticmethod
|
||||
async def remove(redis: Redis, source_url: str) -> bool:
|
||||
return await redis.srem(f"{redis_prefix}:sources", source_url)
|
||||
@@ -0,0 +1,13 @@
|
||||
import discord
|
||||
|
||||
from bot import colors
|
||||
|
||||
|
||||
def create_embed(user: discord.Member | discord.User) -> discord.Embed:
|
||||
avatar = None
|
||||
if avatar_asset := user.avatar:
|
||||
avatar = avatar_asset.with_static_format("jpeg")
|
||||
|
||||
embed = discord.Embed(color=colors["embed"])
|
||||
embed.set_footer(text=f"Requested by: {user}", icon_url=avatar)
|
||||
return embed
|
||||
@@ -1,12 +0,0 @@
|
||||
from discord.embeds import Embed
|
||||
from discord.ext.commands import CommandError
|
||||
|
||||
from context import CustomContext
|
||||
|
||||
|
||||
class EmbeddedCommandException(CommandError):
|
||||
def __init__(self, embed: Embed) -> None:
|
||||
self.embed = embed
|
||||
|
||||
async def send(self, ctx: CustomContext):
|
||||
await ctx.send(embed=self.embed)
|
||||
@@ -0,0 +1,5 @@
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger("discord")
|
||||
|
||||
__all__ = ("logger",)
|
||||
@@ -1,843 +0,0 @@
|
||||
# Original work Copyright (c) 2015 Rapptz (https://github.com/Rapptz/RoboDanny)
|
||||
# Modified work Copyright (c) 2017 Perry Fraser
|
||||
#
|
||||
# Licensed under the MIT License. https://opensource.org/licenses/MIT
|
||||
# Stolen line for line from paginator.py in R. Danny's code
|
||||
# Added formatting and lots of blocking of inspections
|
||||
import asyncio
|
||||
import copy
|
||||
import inspect
|
||||
import itertools
|
||||
import re
|
||||
|
||||
import discord
|
||||
|
||||
|
||||
class CannotPaginate(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Pages:
|
||||
"""Implements a paginator that queries the user for the
|
||||
pagination interface.
|
||||
Pages are 1-index based, not 0-index based.
|
||||
If the user does not reply within 2 minutes then the pagination
|
||||
interface exits automatically.
|
||||
Parameters
|
||||
------------
|
||||
ctx: Context
|
||||
The context of the command.
|
||||
entries: List[str]
|
||||
A list of entries to paginate.
|
||||
per_page: int
|
||||
How many entries show up per page.
|
||||
show_entry_count: bool
|
||||
Whether to show an entry count in the footer.
|
||||
Attributes
|
||||
-----------
|
||||
embed: discord.Embed
|
||||
The embed object that is being used to send pagination info.
|
||||
Feel free to modify this externally. Only the description,
|
||||
footer fields, and colour are internally modified.
|
||||
permissions: discord.Permissions
|
||||
Our permissions for the channel.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, ctx, *, entries, per_page=12, show_entry_count=True, hide_no_results=False
|
||||
):
|
||||
self.hide_no_results = hide_no_results
|
||||
self.bot = ctx.bot
|
||||
self.entries = entries
|
||||
self.message = ctx.message
|
||||
self.channel = ctx.channel
|
||||
self.author = ctx.author
|
||||
self.per_page = per_page
|
||||
pages, left_over = divmod(len(self.entries), self.per_page)
|
||||
if left_over:
|
||||
pages += 1
|
||||
self.maximum_pages = pages
|
||||
self.embed = discord.Embed(color=0xDEADBF)
|
||||
self.paginating = len(entries) > per_page
|
||||
self.show_entry_count = show_entry_count
|
||||
self.reaction_emojis = [
|
||||
(
|
||||
"\N{BLACK LEFT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}",
|
||||
self.first_page,
|
||||
),
|
||||
("\N{BLACK LEFT-POINTING TRIANGLE}", self.previous_page),
|
||||
("\N{BLACK RIGHT-POINTING TRIANGLE}", self.next_page),
|
||||
(
|
||||
"\N{BLACK RIGHT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}",
|
||||
self.last_page,
|
||||
),
|
||||
("\N{INPUT SYMBOL FOR NUMBERS}", self.numbered_page),
|
||||
("\N{BLACK SQUARE FOR STOP}", self.stop_pages),
|
||||
("\N{INFORMATION SOURCE}", self.show_help),
|
||||
]
|
||||
|
||||
if ctx.guild is not None:
|
||||
self.permissions = self.channel.permissions_for(ctx.guild.me)
|
||||
else:
|
||||
self.permissions = self.channel.permissions_for(ctx.bot.user)
|
||||
|
||||
if not self.permissions.embed_links:
|
||||
raise CannotPaginate("Bot does not have embed links permission.")
|
||||
|
||||
if not self.permissions.send_messages:
|
||||
raise CannotPaginate("Bot cannot send messages.")
|
||||
|
||||
if self.paginating:
|
||||
# verify we can actually use the pagination session
|
||||
if not self.permissions.add_reactions:
|
||||
raise CannotPaginate("Bot does not have add reactions permission.")
|
||||
|
||||
if not self.permissions.read_message_history:
|
||||
raise CannotPaginate(
|
||||
"Bot does not have Read Message History permission."
|
||||
)
|
||||
|
||||
def get_page(self, page):
|
||||
base = (page - 1) * self.per_page
|
||||
return self.entries[base : base + self.per_page]
|
||||
|
||||
async def show_page(self, page, *, first=False):
|
||||
# noinspection PyAttributeOutsideInit
|
||||
self.current_page = page
|
||||
entries = self.get_page(page)
|
||||
p = []
|
||||
for index, entry in enumerate(entries, 1 + ((page - 1) * self.per_page)):
|
||||
p.append(f"{index}. {entry}")
|
||||
|
||||
if self.maximum_pages > 1:
|
||||
if self.show_entry_count:
|
||||
text = (
|
||||
f"Page {page}/{self.maximum_pages}"
|
||||
f" ({len(self.entries)} entries)"
|
||||
)
|
||||
else:
|
||||
text = f"Page {page}/{self.maximum_pages}"
|
||||
|
||||
self.embed.set_footer(text=text)
|
||||
|
||||
if not self.paginating:
|
||||
self.embed.description = "\n".join(p)
|
||||
return await self.channel.send(embed=self.embed)
|
||||
|
||||
if not first:
|
||||
self.embed.description = "\n".join(p)
|
||||
await self.message.edit(embed=self.embed)
|
||||
return
|
||||
|
||||
p.append("")
|
||||
p.append("Confused? React with \N{INFORMATION SOURCE} for more info.")
|
||||
self.embed.description = "\n".join(p)
|
||||
self.message = await self.channel.send(embed=self.embed)
|
||||
|
||||
await self.message.add_reaction("🔣")
|
||||
|
||||
async def add_rest_reactions(self):
|
||||
await self.message.remove_reaction("🔣", self.message.guild.me)
|
||||
for (reaction, _) in self.reaction_emojis:
|
||||
if self.maximum_pages == 2 and reaction in ("\u23ed", "\u23ee"):
|
||||
# no |<< or >>| buttons if we only have two pages
|
||||
# we can't forbid it if someone ends up using it but remove
|
||||
# it from the default set
|
||||
continue
|
||||
|
||||
await self.message.add_reaction(reaction)
|
||||
|
||||
async def checked_show_page(self, page):
|
||||
if page != 0 and page <= self.maximum_pages:
|
||||
await self.show_page(page)
|
||||
|
||||
async def first_page(self):
|
||||
"""goes to the first page"""
|
||||
await self.show_page(1)
|
||||
|
||||
async def last_page(self):
|
||||
"""goes to the last page"""
|
||||
await self.show_page(self.maximum_pages)
|
||||
|
||||
async def next_page(self):
|
||||
"""goes to the next page"""
|
||||
await self.checked_show_page(self.current_page + 1)
|
||||
|
||||
async def previous_page(self):
|
||||
"""goes to the previous page"""
|
||||
await self.checked_show_page(self.current_page - 1)
|
||||
|
||||
async def show_current_page(self):
|
||||
if self.paginating:
|
||||
await self.show_page(self.current_page)
|
||||
|
||||
async def numbered_page(self):
|
||||
"""lets you type a page number to go to"""
|
||||
# noinspection PyListCreation
|
||||
to_delete = []
|
||||
to_delete.append(await self.channel.send("What page do you want to go to?"))
|
||||
|
||||
def message_check(m):
|
||||
return (
|
||||
m.author == self.author
|
||||
and self.channel == m.channel
|
||||
and m.content.isdigit()
|
||||
)
|
||||
|
||||
try:
|
||||
msg = await self.bot.wait_for("message", check=message_check, timeout=30.0)
|
||||
except asyncio.TimeoutError:
|
||||
to_delete.append(await self.channel.send("Took too long."))
|
||||
await asyncio.sleep(5)
|
||||
else:
|
||||
page = int(msg.content)
|
||||
to_delete.append(msg)
|
||||
if page != 0 and page <= self.maximum_pages:
|
||||
await self.show_page(page)
|
||||
else:
|
||||
to_delete.append(
|
||||
await self.channel.send(
|
||||
f"Invalid page given. ({page}/{self.maximum_pages})"
|
||||
)
|
||||
)
|
||||
await asyncio.sleep(5)
|
||||
|
||||
# noinspection PyBroadException
|
||||
try:
|
||||
await self.channel.delete_messages(to_delete)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
async def show_help(self):
|
||||
"""shows this message"""
|
||||
messages = [
|
||||
"Welcome to the interactive paginator!\n",
|
||||
"This interactively allows you to see pages "
|
||||
"of text by navigating with "
|
||||
"reactions. They are as follows:\n",
|
||||
]
|
||||
|
||||
for (emoji, func) in self.reaction_emojis:
|
||||
messages.append(f"{emoji} {func.__doc__}")
|
||||
|
||||
self.embed.description = "\n".join(messages)
|
||||
self.embed.clear_fields()
|
||||
self.embed.set_footer(
|
||||
text=f"We were on page {self.current_page} before this message."
|
||||
)
|
||||
await self.message.edit(embed=self.embed)
|
||||
|
||||
async def go_back_to_current_page():
|
||||
await asyncio.sleep(60.0)
|
||||
await self.show_current_page()
|
||||
|
||||
self.bot.loop.create_task(go_back_to_current_page())
|
||||
|
||||
async def stop_pages(self):
|
||||
"""stops the interactive pagination session"""
|
||||
await self.message.delete()
|
||||
self.paginating = False
|
||||
|
||||
def react_check(self, reaction, user):
|
||||
if user is None or user.id != self.author.id:
|
||||
return False
|
||||
|
||||
if reaction.message.id != self.message.id:
|
||||
return False
|
||||
|
||||
if reaction.emoji == "🔣":
|
||||
self.match = self.add_rest_reactions
|
||||
return True
|
||||
|
||||
for (emoji, func) in self.reaction_emojis:
|
||||
if reaction.emoji == emoji:
|
||||
# noinspection PyAttributeOutsideInit
|
||||
self.match = func
|
||||
return True
|
||||
return False
|
||||
|
||||
async def paginate(self):
|
||||
"""Actually paginate the entries and
|
||||
run the interactive loop if necessary."""
|
||||
if not self.entries and not self.hide_no_results:
|
||||
# I just say no results found because that's my most common use
|
||||
# case.
|
||||
return await self.channel.send("No results found.")
|
||||
|
||||
first_page = self.show_page(1, first=True)
|
||||
if not self.paginating:
|
||||
await first_page
|
||||
else:
|
||||
# allow us to react to reactions right away if we're paginating
|
||||
self.bot.loop.create_task(first_page)
|
||||
|
||||
while self.paginating:
|
||||
try:
|
||||
reaction, user = await self.bot.wait_for(
|
||||
"reaction_add", check=self.react_check, timeout=120.0
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
self.paginating = False
|
||||
# noinspection PyBroadException
|
||||
try:
|
||||
await self.message.clear_reactions()
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
break
|
||||
|
||||
# noinspection PyBroadException
|
||||
try:
|
||||
await self.message.remove_reaction(reaction, user)
|
||||
except Exception:
|
||||
pass # can't remove it so don't bother doing so
|
||||
|
||||
await self.match()
|
||||
|
||||
|
||||
class EmbedPages:
|
||||
"""Similar to Pages, but you use [`discord.Embed`]"""
|
||||
|
||||
def __init__(self, ctx, *, embeds):
|
||||
self.bot = ctx.bot
|
||||
self.embeds = embeds
|
||||
self.message = ctx.message
|
||||
self.channel = ctx.channel
|
||||
self.author = ctx.author
|
||||
pages = len(self.embeds)
|
||||
self.maximum_pages = pages
|
||||
self.paginating = len(embeds) > 1
|
||||
self.reaction_emojis = [
|
||||
(
|
||||
"\N{BLACK LEFT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}",
|
||||
self.first_page,
|
||||
),
|
||||
("\N{BLACK LEFT-POINTING TRIANGLE}", self.previous_page),
|
||||
("\N{BLACK RIGHT-POINTING TRIANGLE}", self.next_page),
|
||||
(
|
||||
"\N{BLACK RIGHT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR}",
|
||||
self.last_page,
|
||||
),
|
||||
("\N{INPUT SYMBOL FOR NUMBERS}", self.numbered_page),
|
||||
("\N{BLACK SQUARE FOR STOP}", self.stop_pages),
|
||||
("\N{INFORMATION SOURCE}", self.show_help),
|
||||
]
|
||||
|
||||
if ctx.guild is not None:
|
||||
self.permissions = self.channel.permissions_for(ctx.guild.me)
|
||||
else:
|
||||
self.permissions = self.channel.permissions_for(ctx.bot.user)
|
||||
|
||||
if not self.permissions.embed_links:
|
||||
raise CannotPaginate("Bot does not have embed links permission.")
|
||||
|
||||
if not self.permissions.send_messages:
|
||||
raise CannotPaginate("Bot cannot send messages.")
|
||||
|
||||
if self.paginating:
|
||||
# verify we can actually use the pagination session
|
||||
if not self.permissions.add_reactions:
|
||||
raise CannotPaginate("Bot does not have add reactions permission.")
|
||||
|
||||
if not self.permissions.read_message_history:
|
||||
raise CannotPaginate(
|
||||
"Bot does not have Read Message History permission."
|
||||
)
|
||||
|
||||
async def show_page(self, page, *, first=False):
|
||||
# noinspection PyAttributeOutsideInit
|
||||
self.current_page = page
|
||||
embed = copy.copy(self.embeds[page - 1])
|
||||
p = []
|
||||
|
||||
if self.maximum_pages > 1:
|
||||
text = f"Page {page}/{self.maximum_pages}"
|
||||
|
||||
embed.set_footer(text=text)
|
||||
|
||||
if not self.paginating:
|
||||
return await self.channel.send(embed=embed)
|
||||
|
||||
if not first:
|
||||
return await self.message.edit(embed=embed)
|
||||
|
||||
p.append("")
|
||||
p.append("Confused? React with \N{INFORMATION SOURCE} for more info.")
|
||||
embed.description = (
|
||||
"" if embed.description == discord.Embed.Empty else embed.description
|
||||
)
|
||||
embed.description += "\n".join(p)
|
||||
self.message = await self.channel.send(embed=embed)
|
||||
|
||||
await self.message.add_reaction("🔣")
|
||||
|
||||
async def add_rest_reactions(self):
|
||||
await self.message.remove_reaction("🔣", self.message.guild.me)
|
||||
for (reaction, _) in self.reaction_emojis:
|
||||
if self.maximum_pages == 2 and reaction in ("\u23ed", "\u23ee"):
|
||||
# no |<< or >>| buttons if we only have two pages
|
||||
# we can't forbid it if someone ends up using it but remove
|
||||
# it from the default set
|
||||
continue
|
||||
|
||||
await self.message.add_reaction(reaction)
|
||||
|
||||
async def checked_show_page(self, page):
|
||||
if page != 0 and page <= self.maximum_pages:
|
||||
await self.show_page(page)
|
||||
|
||||
async def first_page(self):
|
||||
"""goes to the first page"""
|
||||
await self.show_page(1)
|
||||
|
||||
async def last_page(self):
|
||||
"""goes to the last page"""
|
||||
await self.show_page(self.maximum_pages)
|
||||
|
||||
async def next_page(self):
|
||||
"""goes to the next page"""
|
||||
await self.checked_show_page(self.current_page + 1)
|
||||
|
||||
async def previous_page(self):
|
||||
"""goes to the previous page"""
|
||||
await self.checked_show_page(self.current_page - 1)
|
||||
|
||||
async def show_current_page(self):
|
||||
if self.paginating:
|
||||
await self.show_page(self.current_page)
|
||||
|
||||
async def numbered_page(self):
|
||||
"""lets you type a page number to go to"""
|
||||
# noinspection PyListCreation
|
||||
to_delete = []
|
||||
to_delete.append(await self.channel.send("What page do you want to go to?"))
|
||||
|
||||
def message_check(m):
|
||||
return (
|
||||
m.author == self.author
|
||||
and self.channel == m.channel
|
||||
and m.content.isdigit()
|
||||
)
|
||||
|
||||
try:
|
||||
msg = await self.bot.wait_for("message", check=message_check, timeout=30.0)
|
||||
except asyncio.TimeoutError:
|
||||
to_delete.append(await self.channel.send("Took too long."))
|
||||
await asyncio.sleep(5)
|
||||
else:
|
||||
page = int(msg.content)
|
||||
to_delete.append(msg)
|
||||
if page != 0 and page <= self.maximum_pages:
|
||||
await self.show_page(page)
|
||||
else:
|
||||
to_delete.append(
|
||||
await self.channel.send(
|
||||
f"Invalid page given. ({page}/{self.maximum_pages})"
|
||||
)
|
||||
)
|
||||
await asyncio.sleep(5)
|
||||
|
||||
# noinspection PyBroadException
|
||||
try:
|
||||
await self.channel.delete_messages(to_delete)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
async def show_help(self):
|
||||
"""shows this message"""
|
||||
messages = [
|
||||
"Welcome to the interactive paginator!\n",
|
||||
"This interactively allows you to see pages "
|
||||
"of text by navigating with "
|
||||
"reactions. They are as follows:\n",
|
||||
]
|
||||
|
||||
for (emoji, func) in self.reaction_emojis:
|
||||
messages.append(f"{emoji} {func.__doc__}")
|
||||
|
||||
embed = discord.Embed()
|
||||
|
||||
embed.description = "\n".join(messages)
|
||||
embed.clear_fields()
|
||||
embed.set_footer(
|
||||
text=f"We were on page {self.current_page} before this message."
|
||||
)
|
||||
await self.message.edit(embed=embed)
|
||||
|
||||
async def go_back_to_current_page():
|
||||
await asyncio.sleep(60.0)
|
||||
await self.show_current_page()
|
||||
|
||||
self.bot.loop.create_task(go_back_to_current_page())
|
||||
|
||||
async def stop_pages(self):
|
||||
"""stops the interactive pagination session"""
|
||||
await self.message.delete()
|
||||
self.paginating = False
|
||||
|
||||
def react_check(self, reaction, user):
|
||||
if user is None or user.id != self.author.id:
|
||||
return False
|
||||
|
||||
if reaction.message.id != self.message.id:
|
||||
return False
|
||||
|
||||
if reaction.emoji == "🔣":
|
||||
self.match = self.add_rest_reactions
|
||||
return True
|
||||
|
||||
for (emoji, func) in self.reaction_emojis:
|
||||
if reaction.emoji == emoji:
|
||||
# noinspection PyAttributeOutsideInit
|
||||
self.match = func
|
||||
return True
|
||||
return False
|
||||
|
||||
async def paginate(self):
|
||||
"""Actually paginate the entries and
|
||||
run the interactive loop if necessary."""
|
||||
first_page = self.show_page(1, first=True)
|
||||
if not self.paginating:
|
||||
await first_page
|
||||
else:
|
||||
# allow us to react to reactions right away if we're paginating
|
||||
self.bot.loop.create_task(first_page)
|
||||
|
||||
while self.paginating:
|
||||
try:
|
||||
reaction, user = await self.bot.wait_for(
|
||||
"reaction_add", check=self.react_check, timeout=120.0
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
self.paginating = False
|
||||
# noinspection PyBroadException
|
||||
try:
|
||||
await self.message.clear_reactions()
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
break
|
||||
|
||||
# noinspection PyBroadException
|
||||
try:
|
||||
await self.message.remove_reaction(reaction, user)
|
||||
except Exception:
|
||||
pass # can't remove it so don't bother doing so
|
||||
|
||||
await self.match()
|
||||
|
||||
|
||||
class FieldPages(Pages):
|
||||
"""Similar to Pages except entries should be a list of
|
||||
tuples having (key, value) to show as embed fields instead.
|
||||
"""
|
||||
|
||||
async def show_page(self, page, *, first=False):
|
||||
# noinspection PyAttributeOutsideInit
|
||||
self.current_page = page
|
||||
entries = self.get_page(page)
|
||||
|
||||
self.embed.clear_fields()
|
||||
self.embed.description = discord.Embed.Empty
|
||||
|
||||
for key, value in entries:
|
||||
self.embed.add_field(name=key, value=value, inline=False)
|
||||
|
||||
if self.maximum_pages > 1:
|
||||
if self.show_entry_count:
|
||||
text = (
|
||||
f"Page {page}/{self.maximum_pages} "
|
||||
f"({len(self.entries)} entries)"
|
||||
)
|
||||
else:
|
||||
text = f"Page {page}/{self.maximum_pages}"
|
||||
|
||||
self.embed.set_footer(text=text)
|
||||
|
||||
if not self.paginating:
|
||||
return await self.channel.send(embed=self.embed)
|
||||
|
||||
if not first:
|
||||
await self.message.edit(embed=self.embed)
|
||||
return
|
||||
|
||||
self.message = await self.channel.send(embed=self.embed)
|
||||
for (reaction, _) in self.reaction_emojis:
|
||||
if self.maximum_pages == 2 and reaction in ("\u23ed", "\u23ee"):
|
||||
# no |<< or >>| buttons if we only have two pages
|
||||
# we can't forbid it if someone ends up using it but remove
|
||||
# it from the default set
|
||||
continue
|
||||
|
||||
await self.message.add_reaction(reaction)
|
||||
|
||||
|
||||
# ?help
|
||||
# ?help Cog
|
||||
# ?help command
|
||||
# -> could be a subcommand
|
||||
|
||||
_mention = re.compile(r"<@!?([0-9]{1,19})>")
|
||||
|
||||
|
||||
def cleanup_prefix(bot, prefix):
|
||||
m = _mention.match(prefix)
|
||||
if m:
|
||||
user = bot.get_user(int(m.group(1)))
|
||||
if user:
|
||||
return f"@{user.name} "
|
||||
return prefix
|
||||
|
||||
|
||||
async def _can_run(cmd, ctx):
|
||||
# noinspection PyBroadException
|
||||
try:
|
||||
return await cmd.can_run(ctx)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _command_signature(cmd):
|
||||
# this is modified from discord.py source
|
||||
# which I wrote myself
|
||||
|
||||
result = [cmd.qualified_name]
|
||||
if cmd.usage:
|
||||
result.append(cmd.usage)
|
||||
return " ".join(result)
|
||||
|
||||
params = cmd.clean_params
|
||||
if not params:
|
||||
return " ".join(result)
|
||||
|
||||
for name, param in params.items():
|
||||
if param.default is not param.empty:
|
||||
# We don't want None or '' to trigger the [name=value] case and
|
||||
# instead it should do [name] since [name=None] or [name=] are
|
||||
# not exactly useful for the user.
|
||||
should_print = (
|
||||
param.default
|
||||
if isinstance(param.default, str)
|
||||
else param.default is not None
|
||||
)
|
||||
if should_print:
|
||||
result.append(f"[{name}={param.default!r}]")
|
||||
else:
|
||||
result.append(f"[{name}]")
|
||||
elif param.kind == param.VAR_POSITIONAL:
|
||||
result.append(f"[{name}...]")
|
||||
else:
|
||||
result.append(f"<{name}>")
|
||||
|
||||
return " ".join(result)
|
||||
|
||||
|
||||
class HelpPaginator(Pages):
|
||||
def __init__(self, ctx, entries, *, per_page=4):
|
||||
super().__init__(ctx, entries=entries, per_page=per_page, hide_no_results=True)
|
||||
self.reaction_emojis.append(
|
||||
("\N{WHITE QUESTION MARK ORNAMENT}", self.show_bot_help)
|
||||
)
|
||||
self.total = len(entries)
|
||||
|
||||
@classmethod
|
||||
async def from_cog(cls, ctx, cog):
|
||||
cog_name = cog.__class__.__name__
|
||||
|
||||
# get the commands
|
||||
entries = sorted(cog.get_commands(), key=lambda c: c.name)
|
||||
|
||||
# remove the ones we can't run
|
||||
entries = [
|
||||
cmd for cmd in entries if (await _can_run(cmd, ctx)) and not cmd.hidden
|
||||
]
|
||||
|
||||
self = cls(ctx, entries)
|
||||
self.title = f"{cog_name} Commands".upper()
|
||||
self.description = inspect.getdoc(cog)
|
||||
self.prefix = cleanup_prefix(ctx.bot, ctx.prefix)
|
||||
|
||||
# no longer need the database
|
||||
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
async def from_command(cls, ctx, command):
|
||||
try:
|
||||
entries = sorted(command.commands, key=lambda c: c.name)
|
||||
except AttributeError:
|
||||
entries = []
|
||||
else:
|
||||
entries = [
|
||||
cmd for cmd in entries if (await _can_run(cmd, ctx)) and not cmd.hidden
|
||||
]
|
||||
|
||||
self = cls(ctx, entries)
|
||||
self.title = command.signature
|
||||
|
||||
if command.description:
|
||||
self.description = f"{command.description}\n\n{command.help}"
|
||||
else:
|
||||
self.description = command.help or "No help given."
|
||||
|
||||
self.prefix = cleanup_prefix(ctx.bot, ctx.prefix)
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
async def from_bot(cls, ctx):
|
||||
def key(c):
|
||||
return c.cog_name or "\u200bMisc"
|
||||
|
||||
entries = sorted(ctx.bot.commands, key=key)
|
||||
nested_pages = []
|
||||
per_page = 9
|
||||
|
||||
# 0: (cog, desc, commands) (max len == 9)
|
||||
# 1: (cog, desc, commands) (max len == 9)
|
||||
# ...
|
||||
|
||||
for cog, commands in itertools.groupby(entries, key=key):
|
||||
plausible = [
|
||||
cmd for cmd in commands if (await _can_run(cmd, ctx)) and not cmd.hidden
|
||||
]
|
||||
if len(plausible) == 0:
|
||||
continue
|
||||
|
||||
description = ctx.bot.get_cog(cog)
|
||||
if description is None:
|
||||
description = discord.Embed.Empty
|
||||
else:
|
||||
description = inspect.getdoc(description) or discord.Embed.Empty
|
||||
|
||||
nested_pages.extend(
|
||||
(cog, description, plausible[i : i + per_page])
|
||||
for i in range(0, len(plausible), per_page)
|
||||
)
|
||||
|
||||
self = cls(ctx, nested_pages, per_page=1) # this forces the pagination session
|
||||
self.prefix = cleanup_prefix(ctx.bot, ctx.prefix)
|
||||
|
||||
# swap the get_page implementation with
|
||||
# one that supports our style of pagination
|
||||
self.get_page = self.get_bot_page
|
||||
self._is_bot = True
|
||||
|
||||
# replace the actual total
|
||||
self.total = sum(len(o) for _, _, o in nested_pages)
|
||||
return self
|
||||
|
||||
# noinspection PyAttributeOutsideInit
|
||||
def get_bot_page(self, page):
|
||||
cog, description, commands = self.entries[page - 1]
|
||||
self.title = f"{cog} Commands"
|
||||
self.description = description
|
||||
return commands
|
||||
|
||||
async def show_page(self, page, *, first=False):
|
||||
# noinspection PyAttributeOutsideInit
|
||||
self.current_page = page
|
||||
entries = self.get_page(page)
|
||||
|
||||
self.embed.clear_fields()
|
||||
self.embed.description = self.description
|
||||
self.embed.title = self.title
|
||||
|
||||
# noinspection PyUnresolvedReferences
|
||||
self.embed.set_footer(
|
||||
text=f'Use "{self.prefix}help command" for more info on a command.'
|
||||
)
|
||||
|
||||
signature = _command_signature
|
||||
|
||||
for entry in entries:
|
||||
self.embed.add_field(
|
||||
name=signature(entry),
|
||||
value=entry.short_doc or "No help given",
|
||||
inline=False,
|
||||
)
|
||||
|
||||
if self.maximum_pages:
|
||||
self.embed.set_author(
|
||||
name=f"Page {page}/{self.maximum_pages} ({self.total} commands)"
|
||||
)
|
||||
|
||||
if not self.paginating:
|
||||
return await self.channel.send(embed=self.embed)
|
||||
|
||||
if not first:
|
||||
await self.message.edit(embed=self.embed)
|
||||
return
|
||||
|
||||
self.message = await self.channel.send(embed=self.embed)
|
||||
for (reaction, _) in self.reaction_emojis:
|
||||
if self.maximum_pages == 2 and reaction in ("\u23ed", "\u23ee"):
|
||||
# no |<< or >>| buttons if we only have two pages
|
||||
# we can't forbid it if someone ends up using it but remove
|
||||
# it from the default set
|
||||
continue
|
||||
|
||||
await self.message.add_reaction(reaction)
|
||||
|
||||
async def show_help(self):
|
||||
"""shows this message"""
|
||||
|
||||
self.embed.title = "Paginator help"
|
||||
self.embed.description = "Hello! Welcome to the help page."
|
||||
|
||||
messages = [f"{emoji} {func.__doc__}" for emoji, func in self.reaction_emojis]
|
||||
self.embed.clear_fields()
|
||||
self.embed.add_field(
|
||||
name="What are these reactions for?",
|
||||
value="\n".join(messages),
|
||||
inline=False,
|
||||
)
|
||||
|
||||
self.embed.set_footer(
|
||||
text=f"We were on page {self.current_page} before this message."
|
||||
)
|
||||
await self.message.edit(embed=self.embed)
|
||||
|
||||
async def go_back_to_current_page():
|
||||
await asyncio.sleep(30.0)
|
||||
await self.show_current_page()
|
||||
|
||||
self.bot.loop.create_task(go_back_to_current_page())
|
||||
|
||||
async def show_bot_help(self):
|
||||
"""shows how to use the bot"""
|
||||
|
||||
self.embed.title = "Using the bot"
|
||||
self.embed.description = "Hello! Welcome to the help page."
|
||||
self.embed.clear_fields()
|
||||
|
||||
entries = (
|
||||
("<argument>", "This means the argument is __**required**__."),
|
||||
("[argument]", "This means the argument is __**optional**__."),
|
||||
("[A|B]", "This means the it can be __**either A or B**__."),
|
||||
(
|
||||
"[argument...]",
|
||||
"This means you can have multiple arguments.\n"
|
||||
"Now that you know the basics, it should be "
|
||||
"noted that...\n"
|
||||
"__**You do not type in the brackets!**__",
|
||||
),
|
||||
)
|
||||
|
||||
self.embed.add_field(
|
||||
name="How do I use this bot?",
|
||||
value="Reading the bot signature is pretty simple.",
|
||||
)
|
||||
|
||||
for name, value in entries:
|
||||
self.embed.add_field(name=name, value=value, inline=False)
|
||||
|
||||
self.embed.set_footer(
|
||||
text=f"We were on page {self.current_page} before this message."
|
||||
)
|
||||
await self.message.edit(embed=self.embed)
|
||||
|
||||
async def go_back_to_current_page():
|
||||
await asyncio.sleep(30.0)
|
||||
await self.show_current_page()
|
||||
|
||||
self.bot.loop.create_task(go_back_to_current_page())
|
||||
Reference in New Issue
Block a user