def load_extension(self, name, lib=None): """ Reworked implementation of the default bot extension loader It works exactly the same unless you provide the lib argument manually That way you can import the lib before this method to check for errors in the file and pass the returned lib to this function if no errors were thrown. Args: name: str name of the extension lib: Module lib returned from `import_module` Raises: ClientException Raised when no setup function is found or when lib isn't a valid module """ if lib is None: # Fall back to default implementation when lib isn't provided return self.bot.load_extension(name) if name in self.bot.extensions: return if not isinstance(lib, ModuleType): raise discord.ClientException("lib isn't a valid module") lib = import_module(name) if not hasattr(lib, 'setup'): del lib del sys.modules[name] raise discord.ClientException('extension does not have a setup function') lib.setup(self.bot) self.bot.extensions[name] = lib
def add_command(self, command): """Adds a :class:`.Command` or its superclasses into the internal list of commands. This is usually not called, instead the :meth:`~.GroupMixin.command` or :meth:`~.GroupMixin.group` shortcut decorators are used instead. Parameters ----------- command The command to add. Raises ------- :exc:`.ClientException` If the command is already registered. TypeError If the command passed is not a subclass of :class:`.Command`. """ if not isinstance(command, Command): raise TypeError('The command passed must be a subclass of Command') if isinstance(self, Command): command.parent = self if command.name in self.all_commands: raise discord.ClientException('Command {0.name} is already registered.'.format(command)) self.all_commands[command.name] = command for alias in command.aliases: if alias in self.all_commands: raise discord.ClientException('The alias {} is already an existing command or alias.'.format(alias)) self.all_commands[alias] = command
def __init__(self, source, *, executable='ffmpeg', pipe=False, stderr=None, before_options=None, options=None): stdin = None if not pipe else source args = [executable] if isinstance(before_options, str): args.extend(shlex.split(before_options)) args.append('-i') args.append('-' if pipe else source) args.extend(('-f', 's16le', '-ar', '48000', '-ac', '2', '-loglevel', 'warning')) if isinstance(options, str): args.extend(shlex.split(options)) args.append('pipe:1') self._process = None try: self._process = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=stderr) self._stdout = io.BytesIO( self._process.communicate(input=stdin)[0]) except FileNotFoundError: raise discord.ClientException(executable + ' was not found.') from None except subprocess.SubprocessError as exc: raise discord.ClientException( 'Popen failed: {0.__class__.__name__}: {0}'.format( exc)) from exc
def setup_config_role(self, name, role_map, checks: Iterable = tuple()): logger.info("Setting up managed role from config: {}".format(name)) logger.debug("With configuration: {!r}".format(role_map)) group = role_map.get('group', tuple()) kwargs = copy.deepcopy(role_map) # Recursively get the groups logger.debug("Finding group.") current_group = self.bot # type: commands.GroupMixin for command_name in group: try: current_group = current_group.commands[command_name] except KeyError: logger.warning( "Group '{}' does not exist: making dummy group.".format( command_name)) self._make_dummy_group(current_group, command_name) except AttributeError: raise discord.ClientException(( "Cannot group role management command: parent " "command '{0.name}' is not a group").format(current_group)) else: kwargs['group'] = current_group kwargs['checks'] = checks try: self.add_managed_role(role_name=name, **kwargs) except TypeError as e: raise discord.ClientException( "Configuration error for managed role '{}': {}".format( name, e.args[0]))
async def _parse_arguments(self, ctx: Context): ctx.args = args = [ctx] if self.instance is None else [ self.instance, ctx ] ctx.kwargs = kwargs = {} view = ctx.view iterator = iter(self.params.items()) if self.instance is not None: # we have 'self' as the first parameter so just advance # the iterator and resume parsing try: next(iterator) except StopIteration: raise discord.ClientException( f'Callback for {self.name} command is missing "self" parameter.' ) # next we have the 'ctx' as the next parameter try: next(iterator) except StopIteration: raise discord.ClientException( f'Callback for {self.name} command is missing "ctx" parameter.' ) for name, param in iterator: if param.kind == param.POSITIONAL_OR_KEYWORD: transformed = await self.transform(ctx, param) args.append(transformed) elif param.kind == param.KEYWORD_ONLY: # kwarg only param denotes "consume rest" semantics if self.rest_is_raw: converter = self._get_converter(param) argument = view.read_rest() kwargs[name] = await self.do_conversion( ctx, converter, argument) break kwargs[name] = await self.transform(ctx, param) elif param.kind == param.VAR_POSITIONAL: while not view.eof: try: transformed = await self.transform(ctx, param) args.append(transformed) except RuntimeError: break if not self.ignore_extra: if not view.eof: raise commands.TooManyArguments( f'Too many arguments passed to {self.qualified_name}')
async def _parse_arguments(self, ctx): ctx.args = [ctx] if self.cog is None else [self.cog, ctx] ctx.kwargs = {} args = ctx.args kwargs = ctx.kwargs view = ctx.view iterator = iter(self.params.items()) if self.cog is not None: # we have 'self' as the first parameter so just advance # the iterator and resume parsing try: next(iterator) except StopIteration: raise discord.ClientException( f'Callback for {self.name} command is missing "self" parameter.' ) # next we have the 'ctx' as the next parameter try: next(iterator) except StopIteration: raise discord.ClientException( f'Callback for {self.name} command is missing "ctx" parameter.' ) for name, param in iterator: if param.kind == param.POSITIONAL_OR_KEYWORD: transformed = await self.transform(ctx, param) args.append(transformed) elif param.kind == param.KEYWORD_ONLY: # kwarg only param denotes "consume rest" semantics kwargs[ name] = await self.callback.__lightning_argparser__.parse_args( ctx) break elif param.kind == param.VAR_POSITIONAL: if view.eof and self.require_var_positional: raise commands.MissingRequiredArgument(param) while not view.eof: try: transformed = await self.transform(ctx, param) args.append(transformed) except RuntimeError: break elif param.kind == param.VAR_KEYWORD: await self._parse_flag_args(ctx) break if not self.ignore_extra: if not view.eof: raise commands.TooManyArguments( 'Too many arguments passed to ' + self.qualified_name)
async def _parse_arguments(self, ctx): """ .. Warning:: Argument converting had to change a lot for reaction commands. No way to get input so there is **no conversion** or parsing done here unless it was invoked from a message. When :class:`.ReactionCommand` or :class:`.ReactionGroup` is invoked from a reaction, args will be filled with their default value or ``None``. """ is_reaction = getattr(ctx, 'reaction_command', False) if is_reaction: ctx.args = [ctx] if self.cog is None else [self.cog, ctx] ctx.kwargs = {} args = ctx.args kwargs = ctx.kwargs iterator = iter(self.params.items()) if self.cog is not None: # we have 'self' as the first parameter so just advance # the iterator and resume parsing try: next(iterator) except StopIteration: raise discord.ClientException( f'Callback for {self.name} command is missing "self" parameter.' ) # next we have the 'ctx' as the next parameter try: next(iterator) except StopIteration: raise discord.ClientException( f'Callback for {self.name} command is missing "ctx" parameter.' ) for name, param in iterator: converter = get_converter(param) if hasattr(converter, '__commands_is_flag__'): arg = (await converter._construct_default(ctx) ) if converter._can_be_constructible() else None else: arg = None if param.default is param.empty else param.default if param.kind == param.KEYWORD_ONLY: kwargs[name] = arg else: args.append(arg) else: await super()._parse_arguments(ctx)
async def register(self, ctx, *user_info): """Makes a request to register a new user to the database providing their username and spotify ID""" if len(user_info) < 2: print(f"Error using the command: too few arguments ({ctx.author})") raise discord.ClientException("Improper command usage") display_name = " ".join(user_info[:-1]) user_id = user_info[-1] if user_id is None or display_name is None: await ctx.send( "--help\nProper usage:\n$register <spotify id> <display name>" ) raise discord.ClientException("Improper command usage: too few arguments") confirm = self.db.register_new_user(user_id, display_name) print(f"User registration: {user_id} as {display_name}\n{confirm}") await ctx.send(f"User {display_name} now registered.")
def add_auto_response(self, auto_response): """Adds a :class:`extensions.core.AutoResponse` into the internal list of auto responses. Parameters ----------- auto_response The auto response to add. Raises ------- discord.ClientException If the auto response is already registered. TypeError If the auto response passed is not a subclass of :class:`extensions.core.AutoResponse`. """ if not isinstance(auto_response, AutoResponse): raise TypeError( 'The auto response passed must be a subclass of AutoResponse') if auto_response.name in self.auto_responses: raise discord.ClientException( 'AutoResponse {0.name} is already registered.'.format( auto_response)) self.auto_responses.append(auto_response)
def __init__(self, bot: PikalaxBOT): super().__init__(bot) self.connections = {} self.load_opus() with open(os.devnull, 'w') as DEVNULL: for executable in ('ffmpeg', 'avconv'): try: subprocess.run([executable, '-h'], stdout=DEVNULL, stderr=DEVNULL, check=True) except FileNotFoundError: continue self.ffmpeg = executable self.__ffmpeg_options['executable'] = executable break else: raise discord.ClientException('ffmpeg or avconv not installed') self.executor = ThreadPoolExecutor() self.__ytdl_extractor = youtube_dl.YoutubeDL( self.__ytdl_format_options) self.timeout_tasks = {} self.yt_players = defaultdict( lambda: YouTubePlaylistHandler(self, loop=self.bot.loop))
def add_cog(self, cog): if not isinstance(cog, Cog): raise discord.ClientException( f'cog must be an instance of {Cog.__qualname__}') # cog aliases for alias in cog.__aliases__: if alias in self.cog_aliases: raise discord.ClientException( f'"{alias}" already has a cog registered') self.cog_aliases[alias.lower()] = cog super().add_cog(cog) cog_name = cog.__class__.__name__ self.cog_aliases[cog.__class__.name.lower()] = self.cogs[ cog_name.lower()] = self.cogs.pop(cog_name)
def _spawn_process(self, args, **subprocess_kwargs): # Creation flags only work in Windows if sys.platform == "win32": subprocess_kwargs["creationflags"] = self.creationflags try: return subprocess.Popen(args, **subprocess_kwargs) except FileNotFoundError: if isinstance(args, str): executable = args.partition(" ")[0] else: executable = args[0] message = f"{executable} was not found." raise discord.ClientException(message) from None except subprocess.SubprocessError as exc: message = f"Popen failed: {type(exc).__name__}: {exc}" raise discord.ClientException(message) from exc
def __init__(self, command_prefix, formatter=None, description=None, pm_help=False, **options): super().__init__(**options) self.command_prefix = command_prefix self.extra_events = {} self.cogs = {} self.extensions = {} self._checks = [] self._check_once = [] self._before_invoke = None self._after_invoke = None self.description = inspect.cleandoc(description) if description else '' self.pm_help = pm_help self.owner_id = options.get('owner_id') self.command_not_found = options.pop('command_not_found', 'No command called "{}" found.') self.command_has_no_subcommands = options.pop('command_has_no_subcommands', 'Command {0.name} has no subcommands.') if options.pop('self_bot', False): self._skip_check = lambda x, y: x != y else: self._skip_check = lambda x, y: x == y self.help_attrs = options.pop('help_attrs', {}) if 'name' not in self.help_attrs: self.help_attrs['name'] = 'help' if formatter is not None: if not isinstance(formatter, HelpFormatter): raise discord.ClientException('Formatter must be a subclass of HelpFormatter') self.formatter = formatter else: self.formatter = HelpFormatter() # pay no mind to this ugliness. self.command(**self.help_attrs)(_default_help_command)
def before_invoke(self, coro): """A decorator that registers a coroutine as a pre-invoke hook. A pre-invoke hook is called directly before the command is called. This makes it a useful function to set up database connections or any type of set up required. This pre-invoke hook takes a sole parameter, a :class:`.Context`. See :meth:`.Bot.before_invoke` for more info. Parameters ----------- coro The coroutine to register as the pre-invoke hook. Raises ------- :exc:`.ClientException` The coroutine is not actually a coroutine. """ if not asyncio.iscoroutinefunction(coro): raise discord.ClientException( "The pre-invoke hook must be a coroutine.") self._before_invoke = coro return coro
async def load_extension(self, spec: ModuleSpec): name = spec.name.split(".")[-1] if name in self.extensions: raise discord.ClientException(f"there is already a package named {name} loaded") lib = spec.loader.load_module() if not hasattr(lib, "setup"): del lib raise discord.ClientException(f"extension {name} does not have a setup function") if asyncio.iscoroutinefunction(lib.setup): await lib.setup(self) else: lib.setup(self) self.extensions[name] = lib
def __init__(self, command_prefix, formatter=None, description=None, pm_help=False, **options): super().__init__(**options) self.command_prefix = command_prefix self.extra_events = {} self.cogs = {} self.extensions = {} self.description = inspect.cleandoc(description) if description else '' self.pm_help = pm_help self.command_not_found = options.pop('command_not_found', 'No command called "{}" found.') self.command_has_no_subcommands = options.pop( 'command_has_no_subcommands', 'Command {0.name} has no subcommands.') self.help_attrs = options.pop('help_attrs', {}) self.help_attrs['pass_context'] = True if 'name' not in self.help_attrs: self.help_attrs['name'] = 'help' if formatter is not None: if not isinstance(formatter, HelpFormatter): raise discord.ClientException( 'Formatter must be a subclass of HelpFormatter') self.formatter = formatter else: self.formatter = HelpFormatter() # pay no mind to this ugliness. self.command(**self.help_attrs)(_default_help_command)
def add_listener(self, func, name=None): """The non decorator alternative to :meth:`listen`. Parameters ----------- func : coroutine The extra event to listen to. name : Optional[str] The name of the command to use. Defaults to ``func.__name__``. Example -------- .. code-block:: python async def on_ready(): pass async def my_message(message): pass bot.add_listener(on_ready) bot.add_listener(my_message, 'on_message') """ name = func.__name__ if name is None else name if not asyncio.iscoroutinefunction(func): raise discord.ClientException('Listeners must be coroutines') if name in self.extra_events: self.extra_events[name].append(func) else: self.extra_events[name] = [func]
def after_invoke(self, coro): r"""A decorator that registers a coroutine as a post-invoke hook. A post-invoke hook is called directly after the command is called. This makes it a useful function to clean-up database connections or any type of clean up required. This post-invoke hook takes a sole parameter, a :class:`.Context`. .. note:: Similar to :meth:`~.Bot.before_invoke`\, this is not called unless checks and argument parsing procedures succeed. This hook is, however, **always** called regardless of the internal command callback raising an error (i.e. :exc:`.CommandInvokeError`\). This makes it ideal for clean-up scenarios. Parameters ----------- coro The coroutine to register as the post-invoke hook. Raises ------- :exc:`.ClientException` The coroutine is not actually a coroutine. """ if not asyncio.iscoroutinefunction(coro): raise discord.ClientException( 'The post-invoke hook must be a coroutine.') self._after_invoke = coro return coro
def load_ext(bot, name, settings): lib = importlib.import_module(name) if not hasattr(lib, 'setup'): print(lib) raise discord.ClientException('Extension does not have a setup function.') lib.setup(bot, settings)
async def spotify(self, ctx, *, query="Today's Top Hits"): """Searches Spotify's playlists to unpack (default: Today's Top Hits)""" message = await ctx.send(f"Searching Spotify for `{query}`...") playlist = SpotifyPlaylist.from_file(query, loop=self.bot.loop) await message.edit( content= f"Should I add `{playlist}` to the queue? Or would you prefer the playlist url?" ) await message.add_reaction("\U00002705") # check mark await message.add_reaction("\U0000274E") # cross mark await message.add_reaction("\U0001F517") # link emoji def check(reaction, user): """Checks whether the user reacted and whether the reaction was valid""" if user == ctx.author: return str(reaction) in ("\U00002705", "\U0000274E", "\U0001F517" ) # cross or check mark or link return False try: reaction, user = await self.bot.wait_for("reaction_add", timeout=30.0, check=check) except asyncio.TimeoutError: return await ctx.send("User failed to respond in 30 seconds") finally: await message.delete() if str( reaction ) == "\U0000274E": # if the user reacted with a cross mark (i.e. no) return elif str(reaction ) == "\U0001F517": # if the user reacted with link (i.e. url) match = re.search( r"https:\/\/api\.spotify\.com\/v1\/users\/(.*?)\/playlists\/(.*?)$", playlist.data.get("href")) user_id, playlist_id = match.group(1), match.group(2) return await ctx.send( f"https://open.spotify.com/user/{user_id}/playlist/{playlist_id}" ) if ctx.author.voice is None: raise discord.ClientException("You aren't in a voice channel") message = await ctx.send(f"Unpacking `{playlist}`...") state = self.get_voice_state(ctx.guild) await state.join_voice_channel(ctx.author.voice.channel) async for song in playlist.songs( ): # unpack the songs into the queue as a batch job success = state.add_song_to_playlist(song, context=ctx, batch_job=True) if not success: break state.batch_job = False # end the batch job await message.edit(content=f"`{playlist}` has been unpacked")
async def playlist(self, ctx, *playlist_info): """Finds a playlist by its name and its owner's username and sends its URL in an embedded message""" if len(playlist_info) < 2: await ctx.send( "--help\nCommand `playlist`:\n\tReturns a link to the spotify playlist requested.\nProper usage:\n\t`$playlist <display name> <playlist name>`" ) raise discord.ClientException("Improper command usage") else: try: user, keyword = playlist_info except ValueError as e: await ctx.send(str(e)) await ctx.send( "--help\nCommand `playlist`:\n\tReturns a link to the spotify playlist requested.\nProper usage:\n\t`$playlist <display name> <playlist name>`" ) ( pl_id, url, pl_name, ) = self.get_user_playlist_by_keyword_and_display_name(user, keyword) print(f"Playlist found: {pl_id}") pl_embed = discord.Embed(Title=pl_name, description="Playlist request") pl_embed.add_field(name="Requested by", value=user, inline=True) pl_embed.add_field(name="Link", value=url, inline=True) await ctx.send(embed=pl_embed)
def before_invoke(self, coro): """A decorator that registers a coroutine as a pre-invoke hook. A pre-invoke hook is called directly before the command is called. This makes it a useful function to set up database connections or any type of set up required. This pre-invoke hook takes a sole parameter, a :class:`.Context`. .. note:: The :meth:`~.Bot.before_invoke` and :meth:`~.Bot.after_invoke` hooks are only called if all checks and argument parsing procedures pass without error. If any check or argument parsing procedures fail then the hooks are not called. Parameters ----------- coro The coroutine to register as the pre-invoke hook. Raises ------- :exc:`.ClientException` The coroutine is not actually a coroutine. """ if not asyncio.iscoroutinefunction(coro): raise discord.ClientException( 'The pre-invoke hook must be a coroutine.') self._before_invoke = coro return coro
async def play_from_playlist(self, ctx, *request_info): """Fetches a list of the songs in a playlist, given it's name and its owner's name""" if len(request_info) != 2: await ctx.send( "--help\nCommand `play_from`:\n\tPlays songs from the spotify playlist requested.\nProper usage:\n\t`$play_from <display name> <playlist name>`" ) raise discord.ClientException("Improper command usage") else: user, keyword = request_info ( pl_id, url, npl_name, ) = self.get_user_playlist_by_keyword_and_display_name(user, keyword) tracks = self.playlist_items( pl_id, offset=0, fields="items.track.id,items.track.name,items.track.artists,total", additional_types=["track"], ) pl_embed = discord.Embed(title="Results", description="Query Results") addToQueue = [] for track in tracks["items"]: name = track["track"]["name"] artists = track["track"]["artists"] artists_name = " ".join(artist["name"] for artist in artists) addToQueue.append(f"{name} - {artists_name}") total = tracks["total"] await ctx.send(f"{total} tracks queued!") return addToQueue
def after_invoke(self, coro): """A decorator that registers a coroutine as a post-invoke hook. A post-invoke hook is called directly after the command is called. This makes it a useful function to clean-up database connections or any type of clean up required. This post-invoke hook takes a sole parameter, a :class:`.Context`. See :meth:`.Bot.after_invoke` for more info. Parameters ----------- coro The coroutine to register as the post-invoke hook. Raises ------- :exc:`.ClientException` The coroutine is not actually a coroutine. """ if not asyncio.iscoroutinefunction(coro): raise discord.ClientException( 'The error handler must be a coroutine.') self._after_invoke = coro return coro
def get_prefix(self, message): """|coro| Retrieves the prefix the bot is listening to with the message as a context. Parameters ----------- message: :class:`discord.Message` The message context to get the prefix of. Raises -------- :exc:`.ClientException` The prefix was invalid. This could be if the prefix function returned None, the prefix list returned no elements that aren't None, or the prefix string is empty. Returns --------s Union[List[str], str] A list of prefixes or a single prefix that the bot is listening for. """ prefix = ret = self.command_prefix if callable(prefix): ret = prefix(self, message) if asyncio.iscoroutine(ret): ret = yield from ret if isinstance(ret, (list, tuple)): if not(isinstance(message.channel, discord.DMChannel)): ret = [p for p in ret if p] if not ret: raise discord.ClientException('invalid prefix (could be an empty string, empty list, or None)') return ret
def load_extension(self, name): """Loads an extension. An extension is a python module that contains commands, cogs, or listeners. An extension must have a global function, ``setup`` defined as the entry point on what to do when the extension is loaded. This entry point must have a single argument, the ``bot``. Parameters ------------ name: str The extension name to load. It must be dot separated like regular Python imports if accessing a sub-module. e.g. ``foo.test`` if you want to import ``foo/test.py``. Raises -------- ClientException The extension does not have a setup function. ImportError The extension could not be imported. """ if name in self.extensions: return lib = importlib.import_module(name) if not hasattr(lib, 'setup'): del lib del sys.modules[name] raise discord.ClientException('extension does not have a setup function') lib.setup(self) self.extensions[name] = lib if name.startswith('salieri'): return try: cog_name = name[name.rfind('.')+1:] with open('%s/i18n/%s.sal' % (self.root_folder, cog_name)) as f: self.i18n[cog_name] = {} for row in f: if row == '\n': continue r = row[:-1].split('|') r[0], r[1] = r[0].strip(), r[1].strip() if r[0] == 'lang': language = r[1] self.i18n[cog_name][language] = {} continue self.i18n[cog_name][language][r[0]] = r[1] except: pass
def __init__(self, callback: TaskFunction, is_unique=True): if not asyncio.iscoroutinefunction(callback): raise discord.ClientException("Task callback must be a coroutine.") self.callback = callback self.is_unique = is_unique self.instance = None # instance the last time this Task was accessed as a descriptor self.on_error = None # type: Callable[[Exception, TaskInstance], Awaitable[None]] self.on_cancel = None # type: Callable[[TaskInstance], Awaitable[None]]
def update_project(wizard: ProjectWizard) -> Project: """ Update user's current active project with the passed data. """ user = get_or_make_user(discord.Object(wizard.user_id)) if not user.active_project: raise discord.ClientException( "Can't edit: you don't have an active (selected) project.") for k, v in wizard.items(): if v is not None: setattr(user.active_project, k, v) return user.active_project
def load_extension(self, name): if name in self.extensions: return lib = importlib.import_module(name) if not hasattr(lib, 'setup'): raise discord.ClientException( 'extension does not have a setup function') lib.setup(self) self.extensions[name] = lib
def _get_converter(self, param): converter = param.annotation if converter is param.empty: if param.default is not param.empty: converter = str if param.default is None else type(param.default) else: converter = str elif not inspect.isclass(type(converter)): raise discord.ClientException('Function annotation must be a type') return converter