Compare commits

..

13 Commits

12 changed files with 160 additions and 60 deletions

View File

@ -38,9 +38,9 @@ def range_type(string, min=0, max=100):
try: try:
value = int(string) value = int(string)
except ValueError: except ValueError:
raise argparse.ArgumentTypeError(f"value not a valid integer") raise argparse.ArgumentTypeError("value is not a valid integer")
if min <= value <= max: if min <= value <= max:
return value return value
else: else:
raise argparse.ArgumentTypeError(f"value not in range {min}-{max}") raise argparse.ArgumentTypeError(f"value is not in range {min}-{max}")

View File

@ -1,5 +1,16 @@
from . import bot, tools, utils, voice from . import bot, tools, utils, voice
from .utils import * from .utils import Command, match, match_token, tokenize
__all__ = [
"bot",
"tools",
"utils",
"voice",
"Command",
"match",
"match_token",
"tokenize",
]
def __reload_module__(): def __reload_module__():

View File

@ -1,7 +1,6 @@
import re import re
import arguments import arguments
import commands import commands
import utils import utils
@ -67,7 +66,7 @@ async def clear(message):
if args.delete_command: if args.delete_command:
try: try:
await message.delete() await message.delete()
except: except Exception:
pass pass
regex = None regex = None
@ -101,5 +100,5 @@ async def clear(message):
message, message,
f"purged **{messages}/{args.count} {'message' if args.count == 1 else 'messages'}**", f"purged **{messages}/{args.count} {'message' if args.count == 1 else 'messages'}**",
) )
except: except Exception:
pass pass

View File

@ -11,7 +11,7 @@ import youtubedl
from state import client, players from state import client, players
async def queue_or_play(message): async def queue_or_play(message, edited=False):
await ensure_joined(message) await ensure_joined(message)
if not command_allowed(message): if not command_allowed(message):
return return
@ -73,6 +73,15 @@ async def queue_or_play(message):
if not (args := await parser.parse_args(message, tokens)): if not (args := await parser.parse_args(message, tokens)):
return return
if edited:
found = None
for queued in players[message.guild.id].queue:
if queued.trigger_message.id == message.id:
found = queued
break
if found:
players[message.guild.id].queue.remove(found)
if args.clear: if args.clear:
players[message.guild.id].queue.clear() players[message.guild.id].queue.clear()
await utils.add_check_reaction(message) await utils.add_check_reaction(message)
@ -117,10 +126,11 @@ async def queue_or_play(message):
) )
) )
>= 5 >= 5
and not len(message.guild.voice_client.channel.members) == 2
): ):
await utils.reply( await utils.reply(
message, message,
"you can only queue **5 songs** without the manage channels permission!", "you can only queue **5 items** without the manage channels permission!",
) )
return return
@ -171,7 +181,6 @@ async def queue_or_play(message):
def embed(description): def embed(description):
e = disnake.Embed( e = disnake.Embed(
title="Queued",
description=description, description=description,
color=constants.EMBED_COLOR, color=constants.EMBED_COLOR,
) )
@ -247,6 +256,7 @@ async def join(message):
return await message.guild.voice_client.move_to(message.channel) return await message.guild.voice_client.move_to(message.channel)
await message.channel.connect() await message.channel.connect()
await utils.add_check_reaction(message)
async def leave(message): async def leave(message):
@ -254,6 +264,7 @@ async def leave(message):
return return
await message.guild.voice_client.disconnect() await message.guild.voice_client.disconnect()
await utils.add_check_reaction(message)
async def resume(message): async def resume(message):
@ -288,9 +299,6 @@ async def volume(message):
if not command_allowed(message): if not command_allowed(message):
return return
if not message.guild.voice_client:
return
tokens = commands.tokenize(message.content) tokens = commands.tokenize(message.content)
parser = arguments.ArgumentParser(tokens[0], "set the current volume level") parser = arguments.ArgumentParser(tokens[0], "set the current volume level")
parser.add_argument( parser.add_argument(
@ -303,10 +311,7 @@ async def volume(message):
return return
if not message.guild.voice_client.source: if not message.guild.voice_client.source:
await utils.reply( await utils.reply(message, "nothing is playing!")
message,
f"nothing is playing!",
)
return return
if args.volume is None: if args.volume is None:
@ -319,6 +324,17 @@ async def volume(message):
await utils.add_check_reaction(message) await utils.add_check_reaction(message)
def delete_queued(messages):
found = []
for message in messages:
for queued in players[message.guild.id].queue:
if queued.trigger_message.id == message.id:
found.append(queued)
for queued in found:
if queued in players[messages[0].guild.id].queue:
players[messages[0].guild.id].queue.remove(queued)
def play_after_callback(e, message, once): def play_after_callback(e, message, once):
if e: if e:
print(f"player error: {e}") print(f"player error: {e}")
@ -336,11 +352,11 @@ def play_next(message, once=False):
) )
except Exception as e: except Exception as e:
client.loop.create_task( client.loop.create_task(
message.channel.send(f"error while trying to play: `{e}`") utils.channel_send(message, f"error while trying to play: `{e}`")
) )
return return
client.loop.create_task( client.loop.create_task(
message.channel.send(f"**0.** {queued.format(show_queuer=True)}") utils.channel_send(message, f"**0.** {queued.format(show_queuer=True)}")
) )

View File

@ -13,6 +13,7 @@ RELOADABLE_MODULES = [
"constants", "constants",
"core", "core",
"events", "events",
"tasks",
"utils", "utils",
"voice", "voice",
"youtubedl", "youtubedl",

27
core.py
View File

@ -4,18 +4,20 @@ import importlib
import inspect import inspect
import io import io
import textwrap import textwrap
import time
import traceback import traceback
import disnake
import disnake_paginator import disnake_paginator
import commands import commands
import constants import constants
import core import core
import utils import utils
from state import client, command_locks, executed_messages from state import client, command_locks, idle_tracker
async def on_message(message): async def on_message(message, edited=False):
if not message.content.startswith(constants.PREFIX) or message.author.bot: if not message.content.startswith(constants.PREFIX) or message.author.bot:
return return
@ -26,6 +28,11 @@ async def on_message(message):
if not matched: if not matched:
return return
idle_tracker["last_used"] = time.time()
if idle_tracker["is_idle"]:
idle_tracker["is_idle"] = False
await client.change_presence(status=disnake.Status.online)
if len(matched) > 1: if len(matched) > 1:
await utils.reply( await utils.reply(
message, message,
@ -81,7 +88,7 @@ async def on_message(message):
if len(output) > 2000: if len(output) > 2000:
output = output.replace("`", "\\`") output = output.replace("`", "\\`")
await disnake_paginator.ButtonPaginator( await disnake_paginator.ButtonPaginator(
prefix=f"```\n", prefix="```\n",
suffix="```", suffix="```",
invalid_user_function=utils.invalid_user_handler, invalid_user_function=utils.invalid_user_handler,
color=constants.EMBED_COLOR, color=constants.EMBED_COLOR,
@ -92,11 +99,7 @@ async def on_message(message):
elif len(output.strip()) == 0: elif len(output.strip()) == 0:
await utils.add_check_reaction(message) await utils.add_check_reaction(message)
else: else:
if message.id in executed_messages: await utils.channel_send(message, output)
await executed_messages[message.id].edit(output)
else:
response = await message.channel.send(output)
executed_messages[message.id] = response
case C.CLEAR | C.PURGE if message.author.id in constants.OWNERS: case C.CLEAR | C.PURGE if message.author.id in constants.OWNERS:
await commands.tools.clear(message) await commands.tools.clear(message)
case C.JOIN: case C.JOIN:
@ -105,7 +108,7 @@ async def on_message(message):
await commands.voice.leave(message) await commands.voice.leave(message)
case C.QUEUE | C.PLAY: case C.QUEUE | C.PLAY:
async with command_locks[message.guild.id]: async with command_locks[message.guild.id]:
await commands.voice.queue_or_play(message) await commands.voice.queue_or_play(message, edited)
case C.SKIP: case C.SKIP:
async with command_locks[message.guild.id]: async with command_locks[message.guild.id]:
await commands.voice.skip(message) await commands.voice.skip(message)
@ -129,9 +132,9 @@ async def on_message(message):
async def on_voice_state_update(_, before, after): async def on_voice_state_update(_, before, after):
is_empty = lambda channel: [m.id for m in (channel.members if channel else [])] == [ def is_empty(channel):
client.user.id return [m.id for m in (channel.members if channel else [])] == [client.user.id]
]
c = None c = None
if is_empty(before.channel): if is_empty(before.channel):
c = before.channel c = before.channel

View File

@ -1,47 +1,52 @@
import asyncio
import threading
import commands
import core import core
import events import tasks
from state import client from state import client
dynamic_handlers = {}
async def on_ready():
async def trigger_dynamic_handlers(event_type: str, *data): threading.Thread(
if event_type in dynamic_handlers: name="cleanup",
for dynamic_handler in dynamic_handlers[event_type]: target=asyncio.run_coroutine_threadsafe,
try: args=(
await dynamic_handler(*data) tasks.cleanup(),
except Exception as e: client.loop,
print( ),
f"error in dynamic event handler {dynamic_handler} for {event_type}: {e}" ).start()
)
async def on_message(message): async def on_message(message):
await events.trigger_dynamic_handlers("on_message", message)
await core.on_message(message) await core.on_message(message)
async def on_message_edit(before, after): async def on_message_edit(before, after):
await events.trigger_dynamic_handlers("on_message_edit", before, after)
if before.content == after.content: if before.content == after.content:
return return
await core.on_message(after) await core.on_message(after, edited=True)
async def on_message_delete(message):
commands.voice.delete_queued([message])
async def on_bulk_message_delete(messages):
commands.voice.delete_queued(messages)
async def on_voice_state_update(member, before, after): async def on_voice_state_update(member, before, after):
await events.trigger_dynamic_handlers(
"on_voice_state_update", member, before, after
)
await core.on_voice_state_update(member, before, after) await core.on_voice_state_update(member, before, after)
for k, v in client.get_listeners().items(): for k, v in client.get_listeners().items():
for f in v: for f in v:
client.remove_listener(f, k) client.remove_listener(f, k)
client.add_listener(on_bulk_message_delete, "on_bulk_message_delete")
client.add_listener(on_message, "on_message") client.add_listener(on_message, "on_message")
client.add_listener(on_message_delete, "on_message_delete")
client.add_listener(on_message_edit, "on_message_edit") client.add_listener(on_message_edit, "on_message_edit")
client.add_listener(on_voice_state_update, "on_voice_state_update") client.add_listener(on_voice_state_update, "on_voice_state_update")

View File

@ -7,8 +7,9 @@ from state import client, start_time
@client.event @client.event
async def on_ready(): async def on_ready():
await events.trigger_dynamic_handlers("on_ready")
print(f"logged in as {client.user} in {round(time.time() - start_time, 1)}s") print(f"logged in as {client.user} in {round(time.time() - start_time, 1)}s")
await events.on_ready()
client.run(constants.SECRETS["TOKEN"]) client.run(constants.SECRETS["TOKEN"])

View File

@ -1,14 +1,32 @@
import collections
import time import time
import disnake import disnake
command_locks = {}
executed_messages = {} class LimitedSizeDict(collections.OrderedDict):
players = {} def __init__(self, *args, **kwds):
self.size_limit = kwds.pop("size_limit", 1000)
super().__init__(*args, **kwds)
self._check_size_limit()
def __setitem__(self, key, value):
super().__setitem__(key, value)
self._check_size_limit()
def _check_size_limit(self):
if self.size_limit is not None:
while len(self) > self.size_limit:
self.popitem(last=False)
intents = disnake.Intents.default() intents = disnake.Intents.default()
intents.message_content = True intents.message_content = True
intents.members = True intents.members = True
client = disnake.Client(intents=intents) client = disnake.Client(intents=intents)
command_locks = LimitedSizeDict()
idle_tracker = {"is_idle": False, "last_used": time.time()}
message_responses = LimitedSizeDict()
players = {}
start_time = time.time() start_time = time.time()

25
tasks.py Normal file
View File

@ -0,0 +1,25 @@
import asyncio
import time
import disnake
from state import client, idle_tracker, players
async def cleanup():
while True:
await asyncio.sleep(3600)
targets = []
for id, player in players:
if len(player.queue) == 0:
targets.append(id)
for target in targets:
del players[target]
if (
not idle_tracker["is_idle"]
and time.time() - idle_tracker["last_used"] >= 3600
):
await client.change_presence(status=disnake.Status.idle)
idle_tracker["is_idle"] = True

View File

@ -1,10 +1,13 @@
import disnake import disnake
import constants import constants
from state import message_responses
def format_duration(duration: int): def format_duration(duration: int):
format_plural = lambda noun, count: noun if count == 1 else noun + "s" def format_plural(noun, count):
return noun if count == 1 else noun + "s"
segments = [] segments = []
weeks, duration = divmod(duration, 604800) weeks, duration = divmod(duration, 604800)
@ -34,9 +37,27 @@ async def add_check_reaction(message):
async def reply(message, *args, **kwargs): async def reply(message, *args, **kwargs):
await message.reply( if message.id in message_responses:
*args, **kwargs, allowed_mentions=disnake.AllowedMentions.none() await message_responses[message.id].edit(
) *args, **kwargs, allowed_mentions=disnake.AllowedMentions.none()
)
else:
response = await message.reply(
*args, **kwargs, allowed_mentions=disnake.AllowedMentions.none()
)
message_responses[message.id] = response
async def channel_send(message, *args, **kwargs):
if message.id in message_responses:
await message_responses[message.id].edit(
*args, **kwargs, allowed_mentions=disnake.AllowedMentions.none()
)
else:
response = await message.channel.send(
*args, **kwargs, allowed_mentions=disnake.AllowedMentions.none()
)
message_responses[message.id] = response
async def invalid_user_handler(interaction): async def invalid_user_handler(interaction):

View File

@ -68,7 +68,7 @@ class QueuedSong:
) )
else: else:
return ( return (
f"[`{self.player.title}`]({'<' if hide_preview else ''}{self.player.original_url}{'>' if hide_preview else ''}) [{self.format_duration(self.player.duration) if self.player.duration else 'live'}]" f"[`{self.player.title}`]({'<' if hide_preview else ''}{self.player.original_url}{'>' if hide_preview else ''}) **[{self.format_duration(self.player.duration) if self.player.duration else 'live'}]**"
+ (f" (<@{self.trigger_message.author.id}>)" if show_queuer else "") + (f" (<@{self.trigger_message.author.id}>)" if show_queuer else "")
) )