1
Fork 0
mirror of https://github.com/RYGhub/royalnet.git synced 2024-11-23 19:44:20 +00:00

Start work on a universal summon command

This commit is contained in:
Steffo 2019-11-16 02:31:07 +01:00
parent 192b4cf3d6
commit 34d682f0e3
7 changed files with 115 additions and 58 deletions

View file

@ -1,11 +1,13 @@
# Imports go here! # Imports go here!
from .ping import PingCommand from .ping import PingCommand
from .version import VersionCommand from .version import VersionCommand
from .summon import SummonCommand
# Enter the commands of your Pack here! # Enter the commands of your Pack here!
available_commands = [ available_commands = [
PingCommand, PingCommand,
VersionCommand VersionCommand,
SummonCommand,
] ]
# Don't change this, it should automatically generate __all__ # Don't change this, it should automatically generate __all__

View file

@ -0,0 +1,54 @@
from royalnet.commands import *
from typing import TYPE_CHECKING, Optional, List
import asyncio
try:
import discord
except ImportError:
discord = None
if TYPE_CHECKING:
from royalnet.serf.discord import DiscordSerf
class SummonCommand(Command):
name: str = "summon"
description = "Connect the bot to a Discord voice channel."
syntax = "[channelname]"
async def run(self, args: CommandArgs, data: CommandData) -> None:
# This command only runs on Discord!
if self.interface.name != "discord":
raise UnsupportedError()
# noinspection PyUnresolvedReferences
message: discord.Message = data.message
serf: DiscordSerf = self.interface.bot
channel_name: Optional[str] = args.joined()
# If the channel name was passed as an argument...
if channel_name != "":
# Try to find the specified channel
channels: List[discord.abc.GuildChannel] = serf.client.find_channel(channel_name)
# TODO: if there are multiple channels, try to find the most appropriate one
# TODO: ensure that the channel is a voice channel
if len(channels) != 1:
raise CommandError("Couldn't decide on a channel to connect to.")
else:
channel = channels[0]
else:
# Try to use the channel in which the command author is in
voice: Optional[discord.VoiceState] = message.author.voice
if voice is None:
raise CommandError("You must be connected to a voice channel to summon the bot without any arguments.")
channel: discord.VoiceChannel = voice.channel
# Try to connect to the voice channel
try:
await channel.connect()
except asyncio.TimeoutError:
raise ExternalError("Timed out while trying to connect to the channel")
except discord.opus.OpusNotLoaded:
raise ConfigurationError("[c]libopus[/c] is not loaded in the serf")
except discord.ClientException as e:
# TODO: handle this someway
raise

View file

@ -2,7 +2,12 @@ from .commandinterface import CommandInterface
from .command import Command from .command import Command
from .commanddata import CommandData from .commanddata import CommandData
from .commandargs import CommandArgs from .commandargs import CommandArgs
from .errors import CommandError, InvalidInputError, UnsupportedError, KeyboardExpiredError, ConfigurationError from .errors import CommandError, \
InvalidInputError, \
UnsupportedError, \
ConfigurationError, \
ExternalError, \
UserError
__all__ = [ __all__ = [
"CommandInterface", "CommandInterface",
@ -12,6 +17,7 @@ __all__ = [
"CommandError", "CommandError",
"InvalidInputError", "InvalidInputError",
"UnsupportedError", "UnsupportedError",
"KeyboardExpiredError",
"ConfigurationError", "ConfigurationError",
"ExternalError",
"UserError",
] ]

View file

@ -9,21 +9,22 @@ class CommandError(Exception):
return f"{self.__class__.__qualname__}({repr(self.message)})" return f"{self.__class__.__qualname__}({repr(self.message)})"
class InvalidInputError(CommandError): class UserError(CommandError):
"""The command has received invalid input and cannot complete. """The command failed to execute, and the error is because of something that the user did."""
Display an error message to the user, along with the correct syntax for the command."""
class InvalidInputError(UserError):
"""The command has received invalid input and cannot complete."""
class UnsupportedError(CommandError): class UnsupportedError(CommandError):
"""A requested feature is not available on this interface. """A requested feature is not available on this interface."""
Display an error message to the user, telling them to use another interface."""
class KeyboardExpiredError(CommandError):
"""A special type of exception that can be raised in keyboard handlers to mark a specific keyboard as expired."""
class ConfigurationError(CommandError): class ConfigurationError(CommandError):
"""The command is misconfigured and cannot work.""" """The command cannot work because of a wrong configuration by part of the Royalnet admin."""
class ExternalError(CommandError):
"""The command failed to execute, but the problem was because of an external factor (such as an external API going
down)."""

View file

@ -1,8 +1,7 @@
import asyncio import asyncio
import logging import logging
from typing import Type, Optional, List, Union from typing import Type, Optional, List, Union
from royalnet.commands import Command, CommandInterface, CommandData, CommandArgs, CommandError, InvalidInputError, \ from royalnet.commands import *
UnsupportedError
from royalnet.utils import asyncify from royalnet.utils import asyncify
from .escape import escape from .escape import escape
from ..serf import Serf from ..serf import Serf
@ -124,25 +123,11 @@ class DiscordSerf(Serf):
session = None session = None
# Prepare data # Prepare data
data = self.Data(interface=command.interface, session=session, loop=self.loop, message=message) data = self.Data(interface=command.interface, session=session, loop=self.loop, message=message)
try: # Call the command
# Run the command self.call(command, data, parameters)
await command.run(CommandArgs(parameters), data) # Close the alchemy session
except InvalidInputError as e: if session is not None:
await data.reply(f":warning: {e.message}\n" await asyncify(session.close)
f"Syntax: [c]/{command.name} {command.syntax}[/c]")
except UnsupportedError as e:
await data.reply(f":warning: {e.message}")
except CommandError as e:
await data.reply(f":warning: {e.message}")
except Exception as e:
self.sentry_exc(e)
error_message = f"🦀 [b]{e.__class__.__name__}[/b] 🦀\n" \
'\n'.join(e.args)
await data.reply(error_message)
finally:
# Close the alchemy session
if session is not None:
await asyncify(session.close)
def bot_factory(self) -> Type[discord.Client]: def bot_factory(self) -> Type[discord.Client]:
"""Create a custom class inheriting from :py:class:`discord.Client`.""" """Create a custom class inheriting from :py:class:`discord.Client`."""
@ -172,7 +157,7 @@ class DiscordSerf(Serf):
def find_channel(cli, def find_channel(cli,
name: str, name: str,
guild: Optional[discord.Guild] = None) -> List[discord.abc.GuildChannel]: guild: Optional[discord.Guild] = None) -> List[discord.abc.GuildChannel]:
"""Find the :class:`TextChannel`, :class:`VoiceChannel` or :class:`CategoryChannel` with the """Find the :class:`TextChannel`s, :class:`VoiceChannel`s or :class:`CategoryChannel`s with the
specified name (case insensitive). specified name (case insensitive).
You can specify a guild to only search in that specific guild.""" You can specify a guild to only search in that specific guild."""

View file

@ -4,7 +4,7 @@ from typing import Type, Optional, Awaitable, Dict, List, Any, Callable, Union,
from keyring import get_password from keyring import get_password
from sqlalchemy.schema import Table from sqlalchemy.schema import Table
from royalnet import __version__ as version from royalnet import __version__ as version
from royalnet.commands import Command, CommandInterface, CommandData, CommandError, UnsupportedError from royalnet.commands import *
from .alchemyconfig import AlchemyConfig from .alchemyconfig import AlchemyConfig
try: try:
@ -284,6 +284,29 @@ class Serf:
username: the name of the secret that should be retrieved.""" username: the name of the secret that should be retrieved."""
return get_password(f"Royalnet/{self.secrets_name}", username) return get_password(f"Royalnet/{self.secrets_name}", username)
def call(self, command: Command, data: CommandData, parameters: List[str]):
try:
# Run the command
await command.run(CommandArgs(parameters), data)
except InvalidInputError as e:
await data.reply(f"⚠️ {e.message}\n"
f"Syntax: [c]{command.interface.prefix}{command.name} {command.syntax}[/c]")
except UserError as e:
await data.reply(f"⚠️ {e.message}")
except UnsupportedError as e:
await data.reply(f"🚫 {e.message}")
except ExternalError as e:
await data.reply(f"🚫 {e.message}")
except ConfigurationError as e:
await data.reply(f"⛔️ {e.message}")
except CommandError as e:
await data.reply(f"⛔️ {e.message}")
except Exception as e:
self.sentry_exc(e)
error_message = f"🦀 [b]{e.__class__.__name__}[/b] 🦀\n" \
'\n'.join(e.args)
await data.reply(error_message)
async def run(self): async def run(self):
"""A coroutine that starts the event loop and handles command calls.""" """A coroutine that starts the event loop and handles command calls."""
self.herald_task = self.loop.create_task(self.herald.run()) self.herald_task = self.loop.create_task(self.herald.run())

View file

@ -194,27 +194,13 @@ class TelegramSerf(Serf):
session = await asyncify(self.alchemy.Session) session = await asyncify(self.alchemy.Session)
else: else:
session = None session = None
try: # Prepare data
# Create the command data data = self.Data(interface=command.interface, session=session, loop=self.loop, message=message)
data = self.Data(interface=command.interface, session=session, loop=self.loop, update=update) # Call the command
try: self.call(command, data, parameters)
# Run the command # Close the alchemy session
await command.run(CommandArgs(parameters), data) if session is not None:
except InvalidInputError as e: await asyncify(session.close)
await data.reply(f"⚠️ {e.message}\n"
f"Syntax: [c]/{command.name} {command.syntax}[/c]")
except UnsupportedError as e:
await data.reply(f"⚠️ {e.message}")
except CommandError as e:
await data.reply(f"⚠️ {e.message}")
except Exception as e:
self.sentry_exc(e)
error_message = f"🦀 [b]{e.__class__.__name__}[/b] 🦀\n" \
'\n'.join(e.args)
await data.reply(error_message)
finally:
if session is not None:
await asyncify(session.close)
async def handle_edited_message(self, update: telegram.Update): async def handle_edited_message(self, update: telegram.Update):
pass pass