Merge pull request #6 from Matthww/dev

Dev
This commit is contained in:
2021-10-31 17:11:10 +01:00
committed by GitHub
7 changed files with 222 additions and 97 deletions
+43 -3
View File
@@ -1,7 +1,11 @@
import asyncio
from aioredis.client import Redis
import discord
from discord import ActivityType
from discord.colour import Color
from discord.ext import commands
from discord.ext import commands, tasks
import sys
from signal import SIGINT, SIGTERM
import json
from typing import Any, Dict, List, Sequence
from discord import Message
@@ -11,28 +15,47 @@ from discord.ext.commands.errors import (
ExtensionNotFound,
NoEntryPointError,
)
import lavalink
import aioredis
from aioredis import Redis
from context import CustomContext
class ChristmasBot(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,
description=self.config["info"].get("description", ""),
description=self.config["info"]["description"],
case_insensitive=False,
fetch_offline_members=False,
intents=intents,
slash_commands=True,
slash_command_guilds=[227431704426446848],
slash_command_guilds=slash_guilds,
)
self.loop.create_task(self.async_init())
@@ -69,6 +92,23 @@ class ChristmasBot(commands.Bot):
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 __name__ == "__main__":
try:
+4 -16
View File
@@ -6,24 +6,14 @@ from discord.ext.commands import Context
import humanize
import datetime
import lavalink
from bot import ChristmasBot
from utils.EmbedGenerator import EmbedGenerator
from utils.paginator import HelpPaginator
class InformationCog(commands.Cog, name="Information"):
def __init__(self, bot: commands.Bot):
def __init__(self, bot: ChristmasBot):
self.bot = bot
self.is_help_msg = True
self.update_status.start()
@tasks.loop(seconds=30.0)
async def update_status(self):
await self.bot.wait_until_ready()
title = "ck!connect | ck!help"
if self.is_help_msg:
title = "Tfoe broer"
self.is_help_msg = not self.is_help_msg
await self.bot.change_presence(activity=discord.Game(title))
@commands.cooldown(rate=1, per=5, type=commands.BucketType.user)
@commands.command(description="PONG!", aliases=["pong"])
@@ -53,9 +43,7 @@ class InformationCog(commands.Cog, name="Information"):
@commands.cooldown(rate=1, per=5, type=commands.BucketType.user)
async def invite(self, ctx: Context):
"""Gets the invite link!"""
link = f"https://discord.com/oauth2/authorize?client_id=643555373814382593&permissions=3230720&scope=bot%20applications.commands"
embed = await EmbedGenerator.Message(ctx, "Add our bot to your server:", link)
await ctx.send(embed=embed)
await EmbedGenerator.Message(ctx, "Add our bot to your server:", self.bot.invite_link)
@commands.command(
name="wlinfo",
@@ -129,6 +117,6 @@ class InformationCog(commands.Cog, name="Information"):
await ctx.send(embed=embed)
def setup(bot: commands.Bot):
def setup(bot: ChristmasBot):
bot.remove_command("help")
bot.add_cog(InformationCog(bot))
+141 -66
View File
@@ -1,45 +1,91 @@
import discord
from discord import channel
from discord.ext import commands
import re
import discord
from discord.ext.commands.context import Context
import lavalink
import asyncio
from discord.ext import commands
from lavalink.models import DefaultPlayer
from bot import ChristmasBot
from utils.EmbedGenerator import EmbedGenerator
url_rx = re.compile(r"https?://(?:www\.)?.+")
class MusicCog(commands.Cog, name="Music"):
def __init__(self, bot: commands.Bot):
class LavalinkVoiceClient(discord.VoiceClient):
def __init__(self, client: discord.Client, channel: discord.abc.Connectable):
self.client = client
self.channel = channel
# ensure there exists a client already
if hasattr(self.client, "lavalink"):
self.lavalink = self.client.lavalink
else:
self.client.lavalink = lavalink.Client(client.user.id)
self.client.lavalink.add_node(
"localhost", 2333, "youshallnotpass", "us", "default-node"
)
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(commands.Cog):
def __init__(self, bot: ChristmasBot):
self.bot = bot
self.stream = "https://azuracast.exobot.site/radio/8000/radio.opus"
if not hasattr(bot, "lavalink"):
bot.lavalink = lavalink.Client(bot.user.id)
bot.lavalink.add_node("de-1.rivalmc.net", 2333, "12345", "eu", "poggers")
bot.add_listener(
self.bot.lavalink.voice_update_handler, "on_socket_response"
@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"]
)
lavalink.add_event_hook(self.track_hook)
bot.loop.create_task(self.async_init())
async def async_init(self):
await self.bot.wait_until_ready()
channels = []
# We startup to fast #NOTPOGGERS
await asyncio.sleep(5)
for channel in channels:
guild = self.bot.get_guild(channel[0])
player = self.bot.lavalink.player_manager.create(
channel[0], endpoint=str(guild.region)
)
track = await player.node.get_tracks(self.stream)
if not player.is_playing:
await player.play(track["tracks"][0])
await self.connect_to(channel[0], channel[1])
self.bot.lavalink.add_event_hook(self.track_hook)
def cog_unload(self):
"""Cog unload handler. This removes any event hooks that were registered."""
@@ -48,18 +94,42 @@ class MusicCog(commands.Cog, name="Music"):
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 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, error):
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.
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)
)
should_connect = ctx.command.name in ("connect",)
# 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 ("play",)
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:
@@ -76,69 +146,74 @@ class MusicCog(commands.Cog, name="Music"):
)
player.store("channel", ctx.channel.id)
await self.connect_to(ctx.guild.id, str(ctx.author.voice.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)
await self.connect_to(guild_id, None)
guild = self.bot.get_guild(guild_id)
await guild.voice_client.disconnect(force=True)
async def connect_to(self, guild_id: int, channel_id: str):
"""Connects to the given voicechannel ID. A channel_id of `None` means disconnect."""
ws = self.bot._connection._get_websocket(guild_id)
await ws.voice_state(str(guild_id), channel_id)
@commands.command(name="play", aliases=["p", "connect", "join"])
@commands.guild_only()
async def play(self, ctx: Context):
"""Searches and plays a song from a given query."""
# Get the player for this guild from cache.
player: DefaultPlayer = self.bot.lavalink.player_manager.get(ctx.guild.id)
query = "https://youtu.be/VcIt_AcOPjs"
@commands.command(name="connect")
async def connect(self, ctx):
"""Starts vibing."""
player = self.bot.lavalink.player_manager.get(ctx.guild.id)
results = await player.node.get_tracks(self.stream)
# Get the results for the query from Lavalink.
results = await player.node.get_tracks(query)
# Results could be None if Lavalink returns an invalid response (non-JSON/non-200 (OK)).
# ALternatively, resullts['tracks'] could be an empty array if the query yielded no tracks.
if not results or not results["tracks"]:
return await ctx.send("Nothing found!")
return await EmbedGenerator.Title(ctx, "Nothing found!")
if results["loadType"] == "PLAYLIST_LOADED":
tracks = results["tracks"]
for track in tracks:
player.add(requester=ctx.author.id, track=track)
else:
# Theoretically songs will always be TRACK_LOADED
track = results["tracks"][0]
await EmbedGenerator.Message(
ctx, "Track Enqueued", f'[{track["info"]["title"]}]({track["info"]["uri"]})'
)
track = lavalink.models.AudioTrack(track, ctx.author.id, recommended=True)
player.add(requester=ctx.author.id, track=track)
if not player.is_playing:
await player.play()
@commands.command(aliases=["dc"])
async def disconnect(self, ctx):
@commands.command(name="disconnect", aliases=["dc", "stop"])
@commands.guild_only()
async def disconnect(self, ctx: Context):
"""Disconnects the player from the voice channel and clears its queue."""
player = self.bot.lavalink.player_manager.get(ctx.guild.id)
player: DefaultPlayer = self.bot.lavalink.player_manager.get(ctx.guild.id)
if not player.is_connected:
return await ctx.send("Not connected.")
return await EmbedGenerator.Title(ctx, "Not connected.")
if not ctx.author.voice or (
player.is_connected
and ctx.author.voice.channel.id != int(player.channel_id)
):
return await ctx.send("You're not in my voicechannel!")
# Abuse prevention. Users not in voice channels, or not in the same voice channel as the bot
# may not disconnect the bot.
return await EmbedGenerator.Title(ctx, "You're not in my voicechannel!")
# 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()
await self.connect_to(ctx.guild.id, None)
@commands.command(name="now", aliases=["playing"])
async def now_playing(self, ctx):
"""Stop and disconnect the player and controller."""
em = discord.Embed(color=self.bot.colors["embed"])
# em.set_thumbnail(url=np["thumbnail"])
em.add_field(name="Currently playing:", value="Some song")
await EmbedGenerator.SendWithFooter(ctx, em)
# Disconnect from the voice channel.
await ctx.voice_client.disconnect(force=True)
await EmbedGenerator.Title(ctx, "*⃣ | Disconnected.")
def setup(bot: ChristmasBot):
bot.add_cog(MusicCog(bot))
bot.add_cog(Music(bot))
+3 -8
View File
@@ -2,13 +2,7 @@
"token": "",
"owner_ids": [194545408960102400, 190875175460405249],
"prefixes": ["ck!"],
"database": {
"host": "",
"port": 3306,
"user": "",
"password": "",
"db": ""
},
"redis_url": "",
"lavalink": {
"host": "",
"port": 2333,
@@ -23,5 +17,6 @@
"name": "CloudKid Radio",
"description": "A sample bot description"
},
"cogs": ["cogs.owner", "cogs.settings", "cogs.information", "cogs.music"]
"cogs": ["cogs.owner", "cogs.settings", "cogs.information", "cogs.music"],
"slash_command_guilds": []
}
+7
View File
@@ -0,0 +1,7 @@
from aioredis.client import Redis
from discord.ext import commands
class CustomContext(commands.Context):
def get_redis(self) -> Redis:
return self.bot._redis_client
Generated
+21 -2
View File
@@ -16,6 +16,21 @@ yarl = ">=1.0,<1.6.0"
[package.extras]
speedups = ["aiodns", "brotlipy", "cchardet"]
[[package]]
name = "aioredis"
version = "2.0.0"
description = "asyncio (PEP 3156) Redis support"
category = "main"
optional = false
python-versions = ">=3.6"
[package.dependencies]
async-timeout = "*"
typing-extensions = "*"
[package.extras]
hiredis = ["hiredis (>=1.0)"]
[[package]]
name = "async-timeout"
version = "3.0.1"
@@ -258,7 +273,7 @@ python-versions = ">=3.6"
name = "typing-extensions"
version = "3.10.0.2"
description = "Backported and Experimental Type Hints for Python 3.5+"
category = "dev"
category = "main"
optional = false
python-versions = "*"
@@ -290,7 +305,7 @@ multidict = ">=4.0"
[metadata]
lock-version = "1.1"
python-versions = "^3.8"
content-hash = "9ecfdbfb13478abd79ab060516a2001a0706019f52c946f63e5ebb308e01622f"
content-hash = "3e8f3b5171be334aaa606cd70df03f9b0ac49cb2f78b69723ff49233ce9c1cd3"
[metadata.files]
aiohttp = [
@@ -308,6 +323,10 @@ aiohttp = [
{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"},
]
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"},
+1
View File
@@ -11,6 +11,7 @@ lavalink = "^3.1.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"
[tool.poetry.dev-dependencies]
black = {version = "^21.9b0", allow-prereleases = true}