Merge pull request #41 from r-Norge/restructure_bot

Restructure bot
This commit is contained in:
Ev-1 authored and GitHub committed 2022-05-30 18:32:45 +02:00
commit a1ea0b607b
48 files changed
+679 -620

No files matched your search

+4 -4
View File
@@ -5,7 +5,7 @@ jobs:
flake8:
runs-on: ubuntu-latest
env:
CheckFiles: "bot.py cogs/"
CheckFiles: "bot.py musicbot/"
steps:
- uses: actions/checkout@v2
@@ -64,18 +64,18 @@ jobs:
if: github.ref == 'refs/heads/master'
id: tagger
run: |
version=$(grep -i bot_version cogs/utils/bot_version.py | cut -d" " -f3 | tr -d \")
version=$(grep -i bot_version musicbot/cogs/utils/bot_version.py | cut -d" " -f3 | tr -d \")
a=(${version//./ })
((a[2]++))
new_version=$(echo "${a[0]}.${a[1]}.${a[2]}")
sed -i "s/${version}/${new_version}/g" cogs/utils/bot_version.py
sed -i "s/${version}/${new_version}/g" musicbot/cogs/utils/bot_version.py
echo "::set-output name=tag::${new_version}"
- name: Push changed files, and tags
if: github.ref == 'refs/heads/master'
uses: EndBug/add-and-commit@v4
with:
add: cogs/utils/bot_version.py
add: musicbot/cogs/utils/bot_version.py
author_name: MarlinBotCode
message: Bump Version
tag: v${{ steps.tagger.outputs.tag }}
+6 -6
View File
@@ -5,11 +5,10 @@
## Features
- Translations, get the bot in your preferred langauge!
- Translations, get the bot in your preferred langauge! Works for both commands and replies.
- Round Robin style queue, you won't have to wait forever for your songs to play.
- Noice per server customization options.
- Noice per server customization options, including per server localization.
- Toit embeds.
- More stuff to come?
- Lyrics command that sometimes gives the correct lyrics
- DJ roles
@@ -17,10 +16,11 @@
1. Change or copy data/config.yaml.example to config.yaml
2. Create or find a [lavalink](https://github.com/Frederikam/Lavalink) server you can use.
3. Add your bot token and lavalink server.
3. Add your bot token and lavalink server to the bot settings in your config.yaml.
4. Edit any other settings you want.
5. install requirements `python -m pip install -r requirements.txt`
6. run the bot :)
5. (Optional but recommended) Create a virtual environment and activate it.
6. install requirements `python -m pip install -r requirements.txt`
7. run the bot `python bot.py` :)
### Docker
+10 -13
View File
@@ -12,23 +12,20 @@ from argparse import ArgumentParser, RawTextHelpFormatter
import aiohttp
import yaml
# Bot Utilities
from cogs.utils.alias import Aliaser
from cogs.utils.context import Context
from cogs.utils.localizer import Localizer, LocalizerWrapper
from cogs.utils.logger import BotLogger
from cogs.utils.settingsmanager import Settings
from musicbot.utils.localisation import Aliaser, LocalizedContext, Localizer, LocalizerWrapper
from musicbot.utils.logger import BotLogger
from musicbot.utils.settingsmanager import Settings
initial_extensions = [
'cogs.errors',
'cogs.cogmanager',
'cogs.settings',
'cogs.misc',
'cogs.helpformatter'
'musicbot.cogs.errors',
'musicbot.cogs.cogmanager',
'musicbot.cogs.settings',
'musicbot.cogs.misc',
'musicbot.cogs.helpformatter'
]
on_ready_extensions = [
'cogs.nodemanager'
'musicbot.cogs.nodemanager'
]
@@ -73,7 +70,7 @@ class Bot(commands.Bot):
await self.process_commands(message)
async def process_commands(self, message):
ctx = await self.get_context(message, cls=Context)
ctx = await self.get_context(message, cls=LocalizedContext)
# Replace aliases with commands
ctx = self.aliaser.get_command(ctx)
-1
View File
@@ -1 +0,0 @@
bot_version = "0.1.12"
-297
View File
@@ -1,297 +0,0 @@
# Discord Packages
import discord
import asyncio
class CantScroll(Exception):
pass
class BasePaginator:
def __init__(self, max_size=2000, max_units=10):
self._pages = []
self._current_page_size = 0
self._current_units = 0
self._max_size = max_size
self._max_units = max_units
def close_page(self):
pass
def add_page_indicator(self, localizer, localizer_str=None, **kvpairs):
self.close_page()
if localizer_str:
for i, page in enumerate(self._pages, start=1):
page.set_footer(text=localizer.format_str(localizer_str, _current=i,
_total=len(self._pages), **kvpairs))
else:
for i, page in enumerate(self._pages, start=1):
page.set_footer(text=f"{i}/{len(self._pages)}")
def append_paginator(self, paginator):
if not isinstance(paginator, BasePaginator):
raise TypeError('Paginator needs to be a subclass of BasePaginator.')
self.close_page()
for page in paginator.pages:
self.pages.append(page)
@property
def pages(self):
if self._current_page_size > 0:
self.close_page()
return self._pages
class TextPaginator(BasePaginator):
def __init__(self, max_size=2000, max_lines=10, **embed_base):
super().__init__(max_size=max_size, max_units=max_lines)
self._current_page = []
self.embed_base = embed_base
def close_page(self):
if self._current_page_size > 0:
embed = discord.Embed(**self.embed_base)
embed.description = '\n'.join(self._current_page)
self._pages.append(embed)
self._current_page_size = 0
self._current_units = 0
self._current_page = []
def add_line(self, line='', *, empty=False):
if len(line) > self._max_size:
raise RuntimeError(f'Line exceeds maximum page size {self._max_size}')
if self._current_page_size + len(line) + 1 > self._max_size:
self.close_page()
if self._current_units >= self._max_units:
self.close_page()
self._current_page_size += len(line) + 1
self._current_units += 1
self._current_page.append(line)
if empty:
self._current_page.append('')
self._current_page_size += 1
class FieldPaginator(BasePaginator):
def __init__(self, max_size=5000, max_fields=8, **embed_base):
super().__init__(max_size=max_size, max_units=max_fields)
self._embed_base = embed_base
self._current_page = discord.Embed(**self._embed_base)
def close_page(self):
if self._current_page_size > 0:
self._pages.append(self._current_page)
self._current_page_size = 0
self._current_units = 0
self._current_page = discord.Embed(**self._embed_base)
def add_field(self, name, value, inline=False):
field = {"name": name, "value": value}
fieldsize = sum([len(val) for _, val in field.items()])
if len(name) > 256 or len(value) > 1024:
raise RuntimeError('Maximum field size exceeded, max name length: 256. max value length: 1024.')
if self._current_page_size + fieldsize > self._max_size:
self.close_page()
if self._current_units >= self._max_units:
self.close_page()
field["inline"] = inline
self._current_page_size += fieldsize
self._current_units += 1
self._current_page.add_field(**field)
class QueuePaginator(TextPaginator):
def __init__(self, localizer, player, color, member: discord.Member = None, include_current: bool = False):
self.localizer = localizer
queue = player.global_queue() if not member else player.user_queue(member.id, dual=True)
duration = player.queue_duration(member=member, include_current=include_current)
if member:
title = localizer.format_str("{queue.userqueue}", _user=member.display_name, _length=len(queue),
_duration=duration)
else:
title = localizer.format_str("{queue.length}", _length=len(queue), _duration=duration)
super().__init__(max_lines=10, **{"color": color, "title": title})
for index, temp in enumerate(queue):
if member is None:
track = temp
queued_track = localizer.format_str("{queue.globaltrack}", _index=index+1, _title=track.title,
_uri=track.uri, _user_id=track.requester)
else:
track, globpos = temp
queued_track = localizer.format_str("{queue.usertrack}", _index=index+1, _globalindex=globpos+1,
_title=track.title, _uri=track.uri)
self.add_line(queued_track)
self.add_page_indicator(self.localizer, "{queue.pageindicator}")
class HelpPaginator(FieldPaginator):
def __init__(self, **kwargs):
super().__init__(**kwargs)
def force_close_page(self):
self._pages.append(self._current_page)
self._current_page_size = 0
self._current_units = 0
self._current_page = discord.Embed(**self._embed_base)
def add_command_field(self, cmd_dict):
if not isinstance(cmd_dict, dict):
return
aliases = cmd_dict.get('aliases', [])
args = cmd_dict.get('args', '')
description = cmd_dict.get('description', '')
cmd = str(aliases[0])
cmd += ' ' + args
self.add_field(name=cmd, value=description, inline=False)
# Maybe slightly very copied from rdanny
# Thanks danny <3
class Scroller:
def __init__(self, ctx, paginator, timeout=120.0, clear_on_timeout=False, clear_on_exit=True, clear_command=True):
if not isinstance(paginator, BasePaginator):
raise TypeError('Paginator needs to be a subclass of BasePaginator.')
self.bot = ctx.bot
self.pages = paginator.pages
self.cmdmsg = ctx.message
self.message = ctx.message
self.channel = ctx.channel
self.author = ctx.author
self.timeout = timeout
self.timeout_delete = clear_on_timeout
self.exit_delete = clear_on_exit
self.clear_command = clear_command
if len(self.pages) > 1:
self.scrolling = True
else:
self.scrolling = False
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),
('❌', self.stop_scrolling),
]
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 CantScroll('Bot does not have embed links permission.')
if not self.permissions.send_messages:
raise CantScroll('Bot cannot send messages.')
if self.scrolling:
# verify we can actually use the pagination session
if not self.permissions.add_reactions:
raise CantScroll('Bot does not have add reactions permission.')
if not self.permissions.read_message_history:
raise CantScroll('Bot does not have Read Message History permission.')
async def send(self):
self.current_page = 0
# No embeds to scroll through
if not self.pages:
return
if not self.scrolling:
return await self.channel.send(embed=self.pages[0])
self.message = await self.channel.send(embed=self.pages[0])
for (reaction, _) in self.reaction_emojis:
if len(self.pages) == 2 and reaction in ('\u23ed', '\u23ee'):
continue
await self.message.add_reaction(reaction)
async def scroll(self, page):
if page < 0 or page >= len(self.pages):
return
self.current_page = page
await self.message.edit(embed=self.pages[page])
async def first_page(self):
await self.scroll(0)
async def last_page(self):
await self.scroll(len(self.pages) - 1)
async def next_page(self):
await self.scroll(self.current_page + 1)
async def previous_page(self):
await self.scroll(self.current_page - 1)
async def stop_scrolling(self):
self.scrolling = False
if self.exit_delete:
await self.message.delete()
if self.clear_command:
await self.cmdmsg.delete()
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
for (emoji, func) in self.reaction_emojis:
if reaction.emoji == emoji:
self.match = func
return True
return False
async def start_scrolling(self):
if not self.scrolling:
await self.send()
else:
self.bot.loop.create_task(self.send())
while self.scrolling:
try:
reaction, user = await self.bot.wait_for('reaction_add', check=self.react_check, timeout=self.timeout)
except asyncio.TimeoutError:
self.scrolling = False
try:
await self.message.clear_reactions()
if self.timeout_delete:
await self.message.delete()
if self.clear_command:
await self.cmdmsg.delete()
except discord.Forbidden:
pass
finally:
break
try:
await self.message.remove_reaction(reaction, user)
except discord.Forbidden:
pass
await self.match()
+3
View File
@@ -91,6 +91,9 @@ scrub:
reconnect:
aliases: [rc]
description: 'disconnects then reconnects the bot in case of bugged songs'
forcedisconnect:
aliases: [fd]
description: 'Forces the bot to disconnect'
forceplay:
aliases: [forceplay, fp]
args: '<query>' # This is just a cosmetic representation of the arguments.
@@ -3,9 +3,8 @@ from discord.ext import commands
import traceback
# Bot Utilities
from cogs.helpformatter import commandhelper
from cogs.utils.paginator import Scroller
from ..utils.userinteraction import Scroller
from .helpformatter import commandhelper
class CogManager(commands.Cog):
+3 -5
View File
@@ -4,11 +4,9 @@ from discord.ext import commands
import traceback
from cogs.music.music_errors import WrongTextChannelError, WrongVoiceChannelError
# Bot Utilities
from cogs.helpformatter import commandhelper
from cogs.utils.paginator import Scroller
from ..utils.userinteraction import Scroller
from .helpformatter import commandhelper
from .music.music_errors import WrongTextChannelError, WrongVoiceChannelError
class Errors(commands.Cog):
+9
View File
@@ -0,0 +1,9 @@
from .help import Help
from .helpformatter import commandhelper
__all__ = ['Help', 'commandhelper']
def setup(bot):
bot.remove_command("help")
bot.add_cog(Help(bot))
+36
View File
@@ -0,0 +1,36 @@
# Discord Packages
from discord.ext import commands
from ...utils.userinteraction import Scroller
from .helpformatter import coghelper, commandhelper, helper, prefix_cleaner
class Help(commands.Cog):
"""Help command"""
def __init__(self, bot):
self.bot = bot
@commands.command(hidden=True)
async def help(self, ctx): # Takes no args because reasons(using the view directly)
ctx.view.skip_ws()
v = ctx.view
invoker = v.buffer[v.index:v.end]
ctx = self.bot.aliaser.get_subcommand(ctx, group=None, parents=[])
command = v.buffer[v.index:v.end]
ctx = prefix_cleaner(ctx)
if not command:
paginator = await helper(ctx)
if command:
thing = ctx.bot.get_cog(command) or ctx.bot.get_command(command)
if not thing:
return await ctx.send(ctx.localizer.format_str("{notcommand}", _command=command))
if isinstance(thing, commands.Command):
paginator = commandhelper(ctx, thing, invoker)
else:
paginator = await coghelper(ctx, thing)
scroller = Scroller(ctx, paginator)
await scroller.start_scrolling()
@@ -3,8 +3,7 @@ from discord.ext import commands
import re
# Bot Utilities
from cogs.utils.paginator import HelpPaginator, Scroller
from ...utils.userinteraction import HelpPaginator
usermention = r"<@!?\d{17,19}>"
@@ -110,39 +109,3 @@ def prefix_cleaner(ctx):
pref = pref[0]
ctx.prefix = pref
return ctx
class Help(commands.Cog):
"""Help command"""
def __init__(self, bot):
self.bot = bot
@commands.command(hidden=True)
async def help(self, ctx): # Takes no args because reasons(using the view directly)
ctx.view.skip_ws()
v = ctx.view
invoker = v.buffer[v.index:v.end]
ctx = self.bot.aliaser.get_subcommand(ctx, group=None, parents=[])
command = v.buffer[v.index:v.end]
ctx = prefix_cleaner(ctx)
if not command:
paginator = await helper(ctx)
if command:
thing = ctx.bot.get_cog(command) or ctx.bot.get_command(command)
if not thing:
return await ctx.send(ctx.localizer.format_str("{notcommand}", _command=command))
if isinstance(thing, commands.Command):
paginator = commandhelper(ctx, thing, invoker)
else:
paginator = await coghelper(ctx, thing)
scroller = Scroller(ctx, paginator)
await scroller.start_scrolling()
def setup(bot):
bot.remove_command("help")
bot.add_cog(Help(bot))
+1 -2
View File
@@ -6,8 +6,7 @@ from lavalink import __version__ as LavalinkVersion
import platform
import time
# Bot Utilities
from cogs.utils import bot_version
from ..utils import bot_version
class Misc(commands.Cog):
@@ -3,7 +3,7 @@ import discord
import lavalink
from discord.ext import commands
from ..utils import thumbnailer, timeformatter
from ...utils import thumbnailer, timeformatter
from .music_errors import WrongTextChannelError
@@ -9,9 +9,8 @@ from discord.ext import commands
import asyncio
import re
from ..utils import checks, timeformatter
from ..utils.paginator import QueuePaginator, Scroller
from ..utils.selector import Selector
from ...utils import checks, timeformatter
from ...utils.userinteraction import QueuePaginator, Scroller, Selector
from .decorators import require_playing, require_queue, require_voice_connection, voteable
time_rx = re.compile('[0-9]+')
@@ -7,9 +7,8 @@ import functools
import inspect
import math
from cogs.music.music_errors import WrongVoiceChannelError
from ..utils.checks import is_dj
from ...utils.checks import is_dj
from .music_errors import WrongVoiceChannelError
def require_voice_connection(should_connect=False):
File renamed without changes.
@@ -14,8 +14,8 @@ import urllib
from bs4 import BeautifulSoup
from ..utils import checks
from ..utils.paginator import Scroller, TextPaginator
from ...utils import checks
from ...utils.userinteraction import Scroller, TextPaginator
from .decorators import require_playing, require_voice_connection, voteable
File renamed without changes.
@@ -8,10 +8,9 @@ import codecs
import yaml
# Bot Utilities
from cogs.helpformatter import commandhelper
from cogs.utils.paginator import Scroller
from .utils.mixplayer import MixPlayer
from ..utils.mixplayer import MixPlayer
from ..utils.userinteraction import Scroller
from .helpformatter import commandhelper
class NodeManager(commands.Cog):
@@ -21,7 +20,7 @@ class NodeManager(commands.Cog):
self.logger = self.bot.main_logger.bot_logger.getChild("NodeManager")
music_extensions = [
'cogs.music',
'musicbot.cogs.music',
]
if not hasattr(bot, 'lavalink'):
@@ -2,10 +2,9 @@
import discord
from discord.ext import commands
# Bot Utilities
from cogs.helpformatter import commandhelper
from cogs.utils import checks
from cogs.utils.paginator import Scroller
from ..utils import checks
from ..utils.userinteraction import Scroller
from .helpformatter import commandhelper
class Settings(commands.Cog):
+1
View File
@@ -0,0 +1 @@
bot_version = "0.2.12"
File renamed without changes.
+5
View File
@@ -0,0 +1,5 @@
from .alias import Aliaser
from .localizedcontext import LocalizedContext
from .localizer import Localizer, LocalizerWrapper
__all__ = ['Aliaser', 'LocalizerWrapper', 'Localizer', 'LocalizedContext']
File renamed without changes.
@@ -2,7 +2,7 @@
from discord.ext import commands
class Context(commands.Context):
class LocalizedContext(commands.Context):
def __init__(self, **kwargs):
super().__init__(**kwargs)
if self.message.guild:
@@ -0,0 +1,4 @@
from .localizer import Localizer
from .localizerwrapper import LocalizerWrapper
__all__ = ['LocalizerWrapper', 'Localizer']
@@ -11,7 +11,7 @@ def flatten(d, parent_key='', sep='/'):
items = []
for k, v in d.items():
new_key = parent_key + sep + k if parent_key else k
if isinstance(v, collections.MutableMapping):
if isinstance(v, collections.abc.MutableMapping):
items.extend(flatten(v, new_key, sep=sep).items())
else:
items.append((new_key, v))
@@ -8,8 +8,7 @@ from os import path
import yaml
# Bot Utilities
from cogs.utils.dict_utils import SafeDict, flatten
from .dict_utils import SafeDict, flatten
"""
Localizer for bot
@@ -152,19 +151,3 @@ class Localizer:
def format_embed(self, embed, lang=None, prefix=None, **kvpairs):
raw = embed.to_dict()
return Embed.from_dict(self.format_dict(raw, lang, prefix, **kvpairs))
class LocalizerWrapper:
def __init__(self, localizer, lang=None, prefix=None):
self.localizer = localizer
self.lang = lang
self.prefix = prefix
def format_str(self, s, **kvpairs):
return self.localizer.format_str(s, self.lang, self.prefix, **kvpairs)
def format_dict(self, d, **kvpairs):
return self.localizer.format_dict(d, self.lang, self.prefix, **kvpairs)
def format_embed(self, embed, **kvpairs):
return self.localizer.format_embed(embed, self.lang, self.prefix, **kvpairs)
@@ -0,0 +1,14 @@
class LocalizerWrapper:
def __init__(self, localizer, lang=None, prefix=None):
self.localizer = localizer
self.lang = lang
self.prefix = prefix
def format_str(self, s, **kvpairs):
return self.localizer.format_str(s, self.lang, self.prefix, **kvpairs)
def format_dict(self, d, **kvpairs):
return self.localizer.format_dict(d, self.lang, self.prefix, **kvpairs)
def format_embed(self, embed, **kvpairs):
return self.localizer.format_embed(embed, self.lang, self.prefix, **kvpairs)
File renamed without changes.
+3
View File
@@ -0,0 +1,3 @@
from .player import MixPlayer
__all__ = ['MixPlayer']
@@ -1,189 +1,11 @@
# Discord Packages
import discord
import lavalink
from lavalink import AudioTrack, DefaultPlayer, Node
from lavalink.events import QueueEndEvent, TrackEndEvent, TrackExceptionEvent, TrackStartEvent, TrackStuckEvent
from lavalink import AudioTrack
import typing
from collections import OrderedDict, deque
from itertools import chain, cycle, islice
from random import shuffle
class MixPlayer(DefaultPlayer):
def __init__(self, guild_id: int, node: Node):
super().__init__(guild_id, node)
self.queue = MixQueue()
self.listeners = set()
self.voteables = {}
self.skip_voters = set()
self.boosted = False
def add(self, requester: int, track: typing.Union[dict, AudioTrack], pos: int = None):
""" Adds a track to the queue. """
return self.queue.add_track(requester, track, pos)
def add_next(self, requester: int, track: typing.Union[dict, AudioTrack], pos: int = None):
""" Adds a track to beginning of the queue """
self.queue.add_next_track(track)
def move_user_track(self, requester: int, initial: int, final: int):
""" Moves a track in a users queue"""
return self.queue.move_user_track(requester, initial, final)
def remove_user_queue(self, requester: int):
self.queue.remove_user_queue(requester)
def remove_user_track(self, requester: int, pos: int):
""" Removes the song at <pos> from the queue of requester """
return self.queue.remove_user_track(requester, pos)
def remove_global_track(self, pos: int):
""" Removes the song at <pos> in the global queue """
return self.queue.remove_global_track(pos)
def shuffle_user_queue(self, requester: int):
""" Randomly reorders the queue of requester """
self.queue.shuffle_user_queue(requester)
def user_queue(self, user: int, dual: bool = False):
return self.queue.get_user_queue(user, dual)
def global_queue(self):
return self.queue.get_queue()
def get_history(self):
return self.queue.history
def queue_duration(self, include_current: bool = False, member: discord.Member = None, end_pos: int = None):
duration = 0
queue = self.user_queue(member.id) if member else self.queue
for i, track in enumerate(queue):
if i == end_pos:
break
duration += int(track.duration)
remaining = self.current.duration - self.position
if include_current:
if self.current:
remaining = self.current.duration - self.position
return lavalink.utils.format_time(duration + remaining)
return lavalink.utils.format_time(duration)
async def play(self, track: AudioTrack = None, start_time: int = 0):
self.current = None
self.last_update = 0
self.last_position = 0
self.position_timestamp = 0
self.paused = False
if not track:
if self.queue.empty:
self.boosted = False
await self.stop()
await self.node._dispatch_event(QueueEndEvent(self))
return
else:
track = self.queue.pop_first()
self.current = track
await self.node._send(op='play', guildId=self.guild_id,
track=track.track, startTime=start_time)
await self.node._dispatch_event(TrackStartEvent(self, track))
async def skip(self, pos: int = 0):
""" Plays the next track in the queue, if any. """
for i in range(pos):
_ = self.queue.pop_first()
self.clear_votes()
await self.play()
async def stop(self):
""" Stops the player. """
await self.node._send(op='stop', guildId=self.guild_id)
self.current = None
self.queue.looping = False
self.clear_votes()
def update_listeners(self, member, voice_state):
if self.is_connected:
vc = int(self.channel_id)
if voice_state.channel is None or voice_state.channel.id != vc:
self.listeners.discard(member)
self.remove_member_votes(member)
else:
if voice_state.deaf or voice_state.self_deaf:
self.listeners.discard(member)
self.remove_member_votes(member)
else:
self.listeners.add(member)
def clear_listeners(self):
self.listeners.clear()
def add_vote(self, category, member):
if member in self.listeners:
if category not in self.voteables:
self.voteables[category] = set()
self.voteables[category].add(member)
def remove_member_votes(self, member):
for category, votes in self.voteables.items():
votes.discard(member)
def get_voters(self, category):
if category not in self.voteables:
self.voteables[category] = set()
return self.voteables[category]
def clear_votes(self):
for category, votes in self.voteables.items():
votes.clear()
def enable_looping(self, looping):
if (not self.queue.looping) and looping:
self.queue.enable_looping(looping)
if track := self.current:
if looping:
self.queue.add_track(track.requester, track)
elif not looping and self.queue.looping:
self.queue.enable_looping(looping)
async def handle_event(self, event):
""" Handles the given event as necessary. """
if isinstance(event, (TrackStuckEvent, TrackExceptionEvent)) or \
isinstance(event, TrackEndEvent) and event.reason == 'FINISHED':
self.skip_voters.clear()
for category, votes in self.voteables.items():
votes.clear()
await self.play()
async def bassboost(self, boost: bool = False):
if boost:
gains = [
(0, 0.15),
(1, 0.15),
(2, 0.25),
(3, 0.15),
(4, -0.15),
(5, -0.1),
(6, -0.05)
]
self.boosted = True
await self.set_gains(*gains)
else:
self.boosted = False
await self.reset_equalizer()
@property
def looping(self):
return self.queue.looping
def roundrobin(*iterables):
"""roundrobin('ABC', 'D', 'EF') --> A D E B F C"""
# Recipe credited to George Sakkis
+183
View File
@@ -0,0 +1,183 @@
# Discord Packages
import discord
import lavalink
from lavalink import AudioTrack, DefaultPlayer, Node
from lavalink.events import QueueEndEvent, TrackEndEvent, TrackExceptionEvent, TrackStartEvent, TrackStuckEvent
import typing
from .mixqueue import MixQueue
class MixPlayer(DefaultPlayer):
def __init__(self, guild_id: int, node: Node):
super().__init__(guild_id, node)
self.queue = MixQueue()
self.listeners = set()
self.voteables = {}
self.skip_voters = set()
self.boosted = False
def add(self, requester: int, track: typing.Union[dict, AudioTrack], pos: int = None):
""" Adds a track to the queue. """
return self.queue.add_track(requester, track, pos)
def add_next(self, requester: int, track: typing.Union[dict, AudioTrack], pos: int = None):
""" Adds a track to beginning of the queue """
self.queue.add_next_track(track)
def move_user_track(self, requester: int, initial: int, final: int):
""" Moves a track in a users queue"""
return self.queue.move_user_track(requester, initial, final)
def remove_user_queue(self, requester: int):
self.queue.remove_user_queue(requester)
def remove_user_track(self, requester: int, pos: int):
""" Removes the song at <pos> from the queue of requester """
return self.queue.remove_user_track(requester, pos)
def remove_global_track(self, pos: int):
""" Removes the song at <pos> in the global queue """
return self.queue.remove_global_track(pos)
def shuffle_user_queue(self, requester: int):
""" Randomly reorders the queue of requester """
self.queue.shuffle_user_queue(requester)
def user_queue(self, user: int, dual: bool = False):
return self.queue.get_user_queue(user, dual)
def global_queue(self):
return self.queue.get_queue()
def get_history(self):
return self.queue.history
def queue_duration(self, include_current: bool = False, member: discord.Member = None, end_pos: int = None):
duration = 0
queue = self.user_queue(member.id) if member else self.queue
for i, track in enumerate(queue):
if i == end_pos:
break
duration += int(track.duration)
remaining = self.current.duration - self.position
if include_current:
if self.current:
remaining = self.current.duration - self.position
return lavalink.utils.format_time(duration + remaining)
return lavalink.utils.format_time(duration)
async def play(self, track: AudioTrack = None, start_time: int = 0):
self.current = None
self.last_update = 0
self.last_position = 0
self.position_timestamp = 0
self.paused = False
if not track:
if self.queue.empty:
self.boosted = False
await self.stop()
await self.node._dispatch_event(QueueEndEvent(self))
return
else:
track = self.queue.pop_first()
self.current = track
await self.node._send(op='play', guildId=self.guild_id,
track=track.track, startTime=start_time)
await self.node._dispatch_event(TrackStartEvent(self, track))
async def skip(self, pos: int = 0):
""" Plays the next track in the queue, if any. """
for i in range(pos):
_ = self.queue.pop_first()
self.clear_votes()
await self.play()
async def stop(self):
""" Stops the player. """
await self.node._send(op='stop', guildId=self.guild_id)
self.current = None
self.queue.looping = False
self.clear_votes()
def update_listeners(self, member, voice_state):
if self.is_connected:
vc = int(self.channel_id)
if voice_state.channel is None or voice_state.channel.id != vc:
self.listeners.discard(member)
self.remove_member_votes(member)
else:
if voice_state.deaf or voice_state.self_deaf:
self.listeners.discard(member)
self.remove_member_votes(member)
else:
self.listeners.add(member)
def clear_listeners(self):
self.listeners.clear()
def add_vote(self, category, member):
if member in self.listeners:
if category not in self.voteables:
self.voteables[category] = set()
self.voteables[category].add(member)
def remove_member_votes(self, member):
for category, votes in self.voteables.items():
votes.discard(member)
def get_voters(self, category):
if category not in self.voteables:
self.voteables[category] = set()
return self.voteables[category]
def clear_votes(self):
for category, votes in self.voteables.items():
votes.clear()
def enable_looping(self, looping):
if (not self.queue.looping) and looping:
self.queue.enable_looping(looping)
if track := self.current:
if looping:
self.queue.add_track(track.requester, track)
elif not looping and self.queue.looping:
self.queue.enable_looping(looping)
async def handle_event(self, event):
""" Handles the given event as necessary. """
if isinstance(event, (TrackStuckEvent, TrackExceptionEvent)) or \
isinstance(event, TrackEndEvent) and event.reason == 'FINISHED':
self.skip_voters.clear()
for category, votes in self.voteables.items():
votes.clear()
await self.play()
async def bassboost(self, boost: bool = False):
if boost:
gains = [
(0, 0.15),
(1, 0.15),
(2, 0.25),
(3, 0.15),
(4, -0.15),
(5, -0.1),
(6, -0.05)
]
self.boosted = True
await self.set_gains(*gains)
else:
self.boosted = False
await self.reset_equalizer()
@property
def looping(self):
return self.queue.looping
@@ -0,0 +1,3 @@
from .settingsmanager import Settings
__all__ = ['Settings']
@@ -0,0 +1,39 @@
class DictMapper:
"""
Class for creating and interacting with nested dictionaries through lists of keys
E.g. mapping ["a", "b", "c"] to example_dictionary["a"]["b"]["c"].
DictMapper.get(d["a"], ["b", "c"]) and DictMapper.get(d, ["a", "b", "c"]) are equivalent.
"""
@staticmethod
def set(d, keys, val):
key = keys[0]
if len(keys) == 1:
if val is None:
try:
d.pop(key)
except KeyError:
pass
else:
d[key] = val
return
if key in d.keys():
if not isinstance(d[key], dict):
d[key] = {}
DictMapper.set(d[key], keys[1:], val)
else:
d[key] = {}
DictMapper.set(d[key], keys[1:], val)
@staticmethod
def get(d, keys):
key = keys[0]
try:
if len(keys) > 1 and isinstance(d[key], dict):
return DictMapper.get(d[key], keys[1:])
else:
return d[key]
except KeyError:
return None
@@ -7,8 +7,10 @@ import os
import yaml
from .dictmapper import DictMapper
class Settings:
class Settings():
def __init__(self, datadir, **default_settings):
self._DATA_PATH = f"{datadir}/bot/"
self._SETTINGS_PATH = self._DATA_PATH + 'settings.yaml'
@@ -34,35 +36,6 @@ class Settings:
with codecs.open(self._SETTINGS_PATH, "r", encoding='utf8') as f:
self.settings = yaml.load(f, Loader=yaml.SafeLoader)
def _set(self, d, keys, val):
key = keys[0]
if len(keys) == 1:
if val is None:
try:
d.pop(key)
except KeyError:
pass
else:
d[key] = val
return
if key in d.keys():
if not isinstance(d[key], dict):
d[key] = {}
self._set(d[key], keys[1:], val)
else:
d[key] = {}
self._set(d[key], keys[1:], val)
def _get(self, d, keys):
key = keys[0]
try:
if len(keys) > 1 and isinstance(d[key], dict):
return self._get(d[key], keys[1:])
else:
return d[key]
except KeyError:
return None
def set(self, identifier, setting, value):
""" Set value in settings, will overwrite any existing values. """
guild_name = None
@@ -75,7 +48,7 @@ class Settings:
if guild_name:
self.settings[identifier]["_servername"] = guild_name
self._set(self.settings[identifier], setting.split('.'), value)
DictMapper.set(self.settings[identifier], setting.split('.'), value)
with codecs.open(self._SETTINGS_PATH, 'w', encoding='utf8') as f:
yaml.dump(self.settings, f, indent=2)
@@ -95,7 +68,7 @@ class Settings:
if identifier not in self.settings.keys():
return default
value = self._get(self.settings[identifier], setting.split('.'))
value = DictMapper.get(self.settings[identifier], setting.split('.'))
if value is not None:
return value
else:
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,6 @@
from .paginators import BasePaginator, CantScrollException, FieldPaginator, HelpPaginator, QueuePaginator, TextPaginator
from .scroller import Scroller
from .selector import Selector
__all__ = ['Scroller', 'Selector', 'QueuePaginator', 'HelpPaginator',
'TextPaginator', 'FieldPaginator', 'CantScrollException', 'BasePaginator']
@@ -0,0 +1,7 @@
from .basepaginator import BasePaginator, CantScrollException
from .fieldpaginator import FieldPaginator
from .helppaginator import HelpPaginator
from .queuepaginator import QueuePaginator
from .textpaginator import TextPaginator
__all__ = ['QueuePaginator', 'HelpPaginator', 'TextPaginator', 'FieldPaginator', 'CantScrollException', 'BasePaginator']
@@ -0,0 +1,37 @@
class CantScrollException(Exception):
pass
class BasePaginator:
def __init__(self, max_size=2000, max_units=10):
self._pages = []
self._current_page_size = 0
self._current_units = 0
self._max_size = max_size
self._max_units = max_units
def close_page(self):
pass
def add_page_indicator(self, localizer, localizer_str=None, **kvpairs):
self.close_page()
if localizer_str:
for i, page in enumerate(self._pages, start=1):
page.set_footer(text=localizer.format_str(localizer_str, _current=i,
_total=len(self._pages), **kvpairs))
else:
for i, page in enumerate(self._pages, start=1):
page.set_footer(text=f"{i}/{len(self._pages)}")
def append_paginator(self, paginator):
if not isinstance(paginator, BasePaginator):
raise TypeError('Paginator needs to be a subclass of BasePaginator.')
self.close_page()
for page in paginator.pages:
self.pages.append(page)
@property
def pages(self):
if self._current_page_size > 0:
self.close_page()
return self._pages
@@ -0,0 +1,35 @@
# Discord Packages
import discord
from .basepaginator import BasePaginator
class FieldPaginator(BasePaginator):
def __init__(self, max_size=5000, max_fields=8, **embed_base):
super().__init__(max_size=max_size, max_units=max_fields)
self._embed_base = embed_base
self._current_page = discord.Embed(**self._embed_base)
def close_page(self):
if self._current_page_size > 0:
self._pages.append(self._current_page)
self._current_page_size = 0
self._current_units = 0
self._current_page = discord.Embed(**self._embed_base)
def add_field(self, name, value, inline=False):
field = {"name": name, "value": value}
fieldsize = sum([len(val) for _, val in field.items()])
if len(name) > 256 or len(value) > 1024:
raise RuntimeError('Maximum field size exceeded, max name length: 256. max value length: 1024.')
if self._current_page_size + fieldsize > self._max_size:
self.close_page()
if self._current_units >= self._max_units:
self.close_page()
field["inline"] = inline
self._current_page_size += fieldsize
self._current_units += 1
self._current_page.add_field(**field)
@@ -0,0 +1,27 @@
# Discord Packages
import discord
from .fieldpaginator import FieldPaginator
class HelpPaginator(FieldPaginator):
def __init__(self, **kwargs):
super().__init__(**kwargs)
def force_close_page(self):
self._pages.append(self._current_page)
self._current_page_size = 0
self._current_units = 0
self._current_page = discord.Embed(**self._embed_base)
def add_command_field(self, cmd_dict):
if not isinstance(cmd_dict, dict):
return
aliases = cmd_dict.get('aliases', [])
args = cmd_dict.get('args', '')
description = cmd_dict.get('description', '')
cmd = str(aliases[0])
cmd += ' ' + args
self.add_field(name=cmd, value=description, inline=False)
@@ -0,0 +1,33 @@
# Discord Packages
import discord
from .textpaginator import TextPaginator
class QueuePaginator(TextPaginator):
def __init__(self, localizer, player, color, member: discord.Member = None, include_current: bool = False):
self.localizer = localizer
queue = player.global_queue() if not member else player.user_queue(member.id, dual=True)
duration = player.queue_duration(member=member, include_current=include_current)
if member:
title = localizer.format_str("{queue.userqueue}", _user=member.display_name, _length=len(queue),
_duration=duration)
else:
title = localizer.format_str("{queue.length}", _length=len(queue), _duration=duration)
super().__init__(max_lines=10, **{"color": color, "title": title})
for index, temp in enumerate(queue):
if member is None:
track = temp
queued_track = localizer.format_str("{queue.globaltrack}", _index=index+1, _title=track.title,
_uri=track.uri, _user_id=track.requester)
else:
track, globpos = temp
queued_track = localizer.format_str("{queue.usertrack}", _index=index+1, _globalindex=globpos+1,
_title=track.title, _uri=track.uri)
self.add_line(queued_track)
self.add_page_indicator(self.localizer, "{queue.pageindicator}")
@@ -0,0 +1,39 @@
# Discord Packages
import discord
from .basepaginator import BasePaginator
class TextPaginator(BasePaginator):
def __init__(self, max_size=2000, max_lines=10, **embed_base):
super().__init__(max_size=max_size, max_units=max_lines)
self._current_page = []
self.embed_base = embed_base
def close_page(self):
if self._current_page_size > 0:
embed = discord.Embed(**self.embed_base)
embed.description = '\n'.join(self._current_page)
self._pages.append(embed)
self._current_page_size = 0
self._current_units = 0
self._current_page = []
def add_line(self, line='', *, empty=False):
if len(line) > self._max_size:
raise RuntimeError(f'Line exceeds maximum page size {self._max_size}')
if self._current_page_size + len(line) + 1 > self._max_size:
self.close_page()
if self._current_units >= self._max_units:
self.close_page()
self._current_page_size += len(line) + 1
self._current_units += 1
self._current_page.append(line)
if empty:
self._current_page.append('')
self._current_page_size += 1
+143
View File
@@ -0,0 +1,143 @@
# This was at some point *heavliy* inspired by the paginator implementation by Rapptz for r.Danny
# https://github.com/Rapptz/RoboDanny/blob/b0401e046814f42597c6e0280fef2432cc49cc5d/cogs/utils/paginator.py
# Discord Packages
import discord
import asyncio
from .paginators import BasePaginator, CantScrollException
class Scroller:
def __init__(self, ctx, paginator, timeout=120.0, clear_on_timeout=False, clear_on_exit=True, clear_command=True):
if not isinstance(paginator, BasePaginator):
raise TypeError('Paginator needs to be a subclass of BasePaginator.')
self.bot = ctx.bot
self.pages = paginator.pages
self.cmdmsg = ctx.message
self.message = ctx.message
self.channel = ctx.channel
self.author = ctx.author
self.timeout = timeout
self.timeout_delete = clear_on_timeout
self.exit_delete = clear_on_exit
self.clear_command = clear_command
if len(self.pages) > 1:
self.scrolling = True
else:
self.scrolling = False
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),
('❌', self.stop_scrolling),
]
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 CantScrollException('Bot does not have embed links permission.')
if not self.permissions.send_messages:
raise CantScrollException('Bot cannot send messages.')
if self.scrolling:
# verify we can actually use the pagination session
if not self.permissions.add_reactions:
raise CantScrollException('Bot does not have add reactions permission.')
if not self.permissions.read_message_history:
raise CantScrollException('Bot does not have Read Message History permission.')
async def send(self):
self.current_page = 0
# No embeds to scroll through
if not self.pages:
return
if not self.scrolling:
return await self.channel.send(embed=self.pages[0])
self.message = await self.channel.send(embed=self.pages[0])
for (reaction, _) in self.reaction_emojis:
if len(self.pages) == 2 and reaction in ('\u23ed', '\u23ee'):
continue
await self.message.add_reaction(reaction)
async def scroll(self, page):
if page < 0 or page >= len(self.pages):
return
self.current_page = page
await self.message.edit(embed=self.pages[page])
async def first_page(self):
await self.scroll(0)
async def last_page(self):
await self.scroll(len(self.pages) - 1)
async def next_page(self):
await self.scroll(self.current_page + 1)
async def previous_page(self):
await self.scroll(self.current_page - 1)
async def stop_scrolling(self):
self.scrolling = False
if self.exit_delete:
await self.message.delete()
if self.clear_command:
await self.cmdmsg.delete()
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
for (emoji, func) in self.reaction_emojis:
if reaction.emoji == emoji:
self.match = func
return True
return False
async def start_scrolling(self):
if not self.scrolling:
await self.send()
else:
self.bot.loop.create_task(self.send())
while self.scrolling:
try:
reaction, user = await self.bot.wait_for('reaction_add', check=self.react_check, timeout=self.timeout)
except asyncio.TimeoutError:
self.scrolling = False
try:
await self.message.clear_reactions()
if self.timeout_delete:
await self.message.delete()
if self.clear_command:
await self.cmdmsg.delete()
except discord.Forbidden:
pass
finally:
break
try:
await self.message.remove_reaction(reaction, user)
except discord.Forbidden:
pass
await self.match()
@@ -4,7 +4,7 @@ import discord
import asyncio
import inspect
from .paginator import TextPaginator
from .paginators import TextPaginator
class Selector(TextPaginator):
+1 -1
View File
@@ -1,5 +1,5 @@
discord.py == 1.7.*
git+https://github.com/Devoxin/Lavalink.py@dev
lavalink == 3.1.*
asyncio
pyyaml
BeautifulSoup4