1
Fork 0
mirror of https://github.com/RYGhub/royalnet.git synced 2024-11-27 13:34:28 +00:00

I'm amazed this actually works. Closes #53!

This commit is contained in:
Steffo 2019-05-22 16:58:53 +02:00
parent e0e1290ecd
commit a9e90f5554
16 changed files with 193 additions and 133 deletions

View file

@ -3857,7 +3857,7 @@ jQuery.Deferred.exceptionHook = function( error, stack ) {
// Support: IE 8 - 9 only
// Console exists when dev tools are open, which can happen at any time
if ( window.console && window.console.warn && error && rerrorNames.test( error.name ) ) {
window.console.warn( "jQuery.Deferred exception: " + error.message, error.stack, stack );
window.console.warn( "jQuery.Deferred exception: " + error.data, error.stack, stack );
}
};

File diff suppressed because one or more lines are too long

View file

@ -5,8 +5,8 @@ import logging as _logging
from .generic import GenericBot
from ..commands import NullCommand
from ..utils import asyncify, Call, Command
from ..error import UnregisteredError, NoneFoundError, TooManyFoundError, InvalidConfigError
from ..network import Message, Reply, RoyalnetConfig
from ..error import UnregisteredError, NoneFoundError, TooManyFoundError, InvalidConfigError, RoyalnetResponseError
from ..network import RoyalnetConfig, Request, Response, ResponseSuccess, ResponseError
from ..database import DatabaseConfig
from ..audio import PlayMode, Playlist, RoyalPCMAudio
@ -62,12 +62,20 @@ class DiscordBot(GenericBot):
.replace("[/p]", "```")
await call.channel.send(escaped_text)
async def net_request(call, message: Message, destination: str):
async def net_request(call, request: Request, destination: str) -> dict:
if self.network is None:
raise InvalidConfigError("Royalnet is not enabled on this bot")
response: Reply = await self.network.request(message, destination)
response_dict: dict = await self.network.request(request.to_dict(), destination)
if "type" not in response_dict:
raise RoyalnetResponseError("Response is missing a type")
elif response_dict["type"] == "ResponseSuccess":
response: typing.Union[ResponseSuccess, ResponseError] = ResponseSuccess.from_dict(response_dict)
elif response_dict["type"] == "ResponseError":
response = ResponseError.from_dict(response_dict)
else:
raise RoyalnetResponseError("Response type is unknown")
response.raise_on_error()
return response
return response.data
async def get_author(call, error_if_none=False):
message: discord.Message = call.kwargs["message"]

View file

@ -4,7 +4,7 @@ import asyncio
import logging
from ..utils import Command, NetworkHandler, Call
from ..commands import NullCommand
from ..network import RoyalnetLink, Message, RoyalnetConfig
from ..network import RoyalnetLink, Request, Response, ResponseSuccess, ResponseError, RoyalnetConfig
from ..database import Alchemy, DatabaseConfig, relationshiplinkchain
@ -24,7 +24,7 @@ class GenericBot:
"""Generate the ``commands`` dictionary required to handle incoming messages, and the ``network_handlers`` dictionary required to handle incoming requests."""
log.debug(f"Now generating commands")
self.commands: typing.Dict[str, typing.Type[Command]] = {}
self.network_handlers: typing.Dict[typing.Type[Message], typing.Type[NetworkHandler]] = {}
self.network_handlers: typing.Dict[str, typing.Type[NetworkHandler]] = {}
for command in commands:
lower_command_name = command.command_name.lower()
self.commands[f"{command_prefix}{lower_command_name}"] = command
@ -47,25 +47,38 @@ class GenericBot:
log.debug(f"Running RoyalnetLink {self.network}")
loop.create_task(self.network.run())
async def _network_handler(self, message: Message) -> Message:
"""Handle a single :py:class:`royalnet.network.Message` received from the :py:class:`royalnet.network.RoyalnetLink`.
async def _network_handler(self, request_dict: dict) -> dict:
"""Handle a single :py:class:`dict` received from the :py:class:`royalnet.network.RoyalnetLink`.
Returns:
Another message, to be sent as :py:class:`royalnet.network.Reply`."""
log.debug(f"Received {message} from the RoyalnetLink")
Another :py:class:`dict`, formatted as a :py:class:`royalnet.network.Response`."""
# Convert the dict to a Request
try:
network_handler = self.network_handlers[message.__class__]
request: Request = Request.from_dict(request_dict)
except TypeError:
log.warning(f"Invalid request received: {request_dict}")
return ResponseError("invalid_request",
f"The Request that you sent was invalid. Check extra_info to see what you sent.",
extra_info={"you_sent": request_dict}).to_dict()
log.debug(f"Received {request} from the RoyalnetLink")
try:
network_handler = self.network_handlers[request.handler]
except KeyError:
_, exc, tb = sys.exc_info()
log.debug(f"Missing network_handler for {message}")
raise Exception(f"Missing network_handler for {message}")
log.warning(f"Missing network_handler for {request.handler}")
return ResponseError("no_handler", f"This Link is missing a network handler for {request.handler}.").to_dict()
try:
log.debug(f"Using {network_handler} as handler for {message}")
return await getattr(network_handler, self.interface_name)(self, message)
log.debug(f"Using {network_handler} as handler for {request.handler}")
response: Response = await getattr(network_handler, self.interface_name)(self, request.data)
return response.to_dict()
except Exception:
_, exc, _ = sys.exc_info()
log.debug(f"Exception {exc} in {network_handler}")
raise
return ResponseError("exception_in_handler",
f"An exception was raised in {network_handler} for {request.handler}. Check extra_info for details.",
extra_info={
"type": exc.__class__.__name__,
"str": str(exc)
}).to_dict()
def _init_database(self, commands: typing.List[typing.Type[Command]], database_config: DatabaseConfig):
"""Create an :py:class:`royalnet.database.Alchemy` with the tables required by the commands. Then, find the chain that links the ``master_table`` to the ``identity_table``."""

View file

@ -1,13 +1,13 @@
import telegram
from telegram.utils.request import Request
import telegram.utils.request
import asyncio
import typing
import logging as _logging
from .generic import GenericBot
from ..commands import NullCommand
from ..utils import asyncify, Call, Command
from ..error import UnregisteredError, InvalidConfigError
from ..network import Message, RoyalnetConfig, Reply
from ..error import UnregisteredError, InvalidConfigError, RoyalnetResponseError
from ..network import RoyalnetConfig, Request, Response, ResponseSuccess, ResponseError
from ..database import DatabaseConfig
loop = asyncio.get_event_loop()
@ -27,7 +27,7 @@ class TelegramBot(GenericBot):
def _init_client(self):
"""Create the :py:class:`telegram.Bot`, and set the starting offset."""
# https://github.com/python-telegram-bot/python-telegram-bot/issues/341
request = Request(5)
request = telegram.utils.request.Request(5)
self.client = telegram.Bot(self._telegram_config.token, request=request)
self._offset: int = -100
@ -55,12 +55,20 @@ class TelegramBot(GenericBot):
.replace("[/p]", "</pre>")
await asyncify(call.channel.send_message, escaped_text, parse_mode="HTML")
async def net_request(call, message: Message, destination: str):
async def net_request(call, request: Request, destination: str) -> dict:
if self.network is None:
raise InvalidConfigError("Royalnet is not enabled on this bot")
response: Reply = await self.network.request(message, destination)
response_dict: dict = await self.network.request(request.to_dict(), destination)
if "type" not in response_dict:
raise RoyalnetResponseError("Response is missing a type")
elif response_dict["type"] == "ResponseSuccess":
response: typing.Union[ResponseSuccess, ResponseError] = ResponseSuccess.from_dict(response_dict)
elif response_dict["type"] == "ResponseError":
response = ResponseError.from_dict(response_dict)
else:
raise RoyalnetResponseError("Response type is unknown")
response.raise_on_error()
return response
return response.data
async def get_author(call, error_if_none=False):
update: telegram.Update = call.kwargs["update"]

View file

@ -1,14 +1,6 @@
import logging as _logging
from ..utils import Command, Call
from ..error import NoneFoundError, \
TooManyFoundError, \
UnregisteredError, \
UnsupportedError, \
InvalidInputError, \
InvalidConfigError, \
RoyalnetError, \
ExternalError
from ..error import *
log = _logging.getLogger(__name__)
@ -41,11 +33,15 @@ class ErrorHandlerCommand(Command):
if isinstance(exception, InvalidConfigError):
await call.reply(f"⚠️ Il bot non è stato configurato correttamente, quindi questo comando non può essere eseguito.\n[p]{exception}[/p]")
return
if isinstance(exception, RoyalnetError):
await call.reply(f"⚠️ La richiesta a Royalnet ha restituito un errore: [p]{exception.exc}[/p]")
if isinstance(exception, RoyalnetRequestError):
await call.reply(f"⚠️ La richiesta a Royalnet ha restituito un errore: [p]{exception.error}[/p]")
return
if isinstance(exception, ExternalError):
await call.reply(f"⚠️ Una risorsa esterna necessaria per l'esecuzione del comando non ha funzionato correttamente, quindi il comando è stato annullato.\n[p]{exception}[/p]")
return
await call.reply(f"❌ Eccezione non gestita durante l'esecuzione del comando:\n[b]{exception.__class__.__name__}[/b]\n[p]{exception}[/p]")
if isinstance(exception, RoyalnetResponseError):
log.warning(f"Invalid response from Royalnet - {exception.__class__.__name__}: {exception}")
await call.reply(f"❌ La risposta ricevuta da Royalnet non è valida: [p]{exception}[/p]")
return
log.error(f"Unhandled exception - {exception.__class__.__name__}: {exception}")
await call.reply(f"❌ Eccezione non gestita durante l'esecuzione del comando:\n[b]{exception.__class__.__name__}[/b]\n[p]{exception}[/p]")

View file

@ -3,9 +3,9 @@ import asyncio
import youtube_dl
import ffmpeg
from ..utils import Command, Call, NetworkHandler, asyncify
from ..network import Request, Data
from ..network import Request, ResponseSuccess
from ..error import TooManyFoundError, NoneFoundError
from ..audio import RoyalPCMAudio, YtdlInfo
from ..audio import RoyalPCMAudio
if typing.TYPE_CHECKING:
from ..bots import DiscordBot
@ -13,28 +13,15 @@ if typing.TYPE_CHECKING:
loop = asyncio.get_event_loop()
class PlayMessage(Data):
def __init__(self, url: str, guild_name: typing.Optional[str] = None):
super().__init__()
self.url: str = url
self.guild_name: typing.Optional[str] = guild_name
class PlaySuccessful(Data):
def __init__(self, info_list: typing.List[YtdlInfo]):
super().__init__()
self.info_list: typing.List[YtdlInfo] = info_list
class PlayNH(NetworkHandler):
message_type = PlayMessage
message_type = "music_play"
@classmethod
async def discord(cls, bot: "DiscordBot", message: PlayMessage):
async def discord(cls, bot: "DiscordBot", data: dict):
"""Handle a play Royalnet request. That is, add audio to a PlayMode."""
# Find the matching guild
if message.guild_name:
guild = bot.client.find_guild(message.guild_name)
if data["guild_name"]:
guild = bot.client.find_guild(data["guild_name"])
else:
if len(bot.music_data) == 0:
raise NoneFoundError("No voice clients active")
@ -46,12 +33,12 @@ class PlayNH(NetworkHandler):
# TODO: change Exception
raise Exception("No music_data for this guild")
# Start downloading
if message.url.startswith("http://") or message.url.startswith("https://"):
audio_sources: typing.List[RoyalPCMAudio] = await asyncify(RoyalPCMAudio.create_from_url, message.url)
if data["url"].startswith("http://") or data["url"].startswith("https://"):
audio_sources: typing.List[RoyalPCMAudio] = await asyncify(RoyalPCMAudio.create_from_url, data["url"])
else:
audio_sources = await asyncify(RoyalPCMAudio.create_from_ytsearch, message.url)
audio_sources = await asyncify(RoyalPCMAudio.create_from_ytsearch, data["url"])
await bot.add_to_music_data(audio_sources, guild)
return PlaySuccessful(info_list=[source.rpf.info for source in audio_sources])
return ResponseSuccess({"title_list": [source.rpf.info.title for source in audio_sources]})
async def notify_on_timeout(call: Call, url: str, time: float, repeat: bool = False):
@ -72,11 +59,11 @@ class PlayCommand(Command):
@classmethod
async def common(cls, call: Call):
guild, url = call.args.match(r"(?:\[(.+)])?\s*(.+)")
download_task = loop.create_task(call.net_request(PlayMessage(url, guild), "discord"))
guild_name, url = call.args.match(r"(?:\[(.+)])?\s*(.+)")
download_task = loop.create_task(call.net_request(Request("music_play", {"url": url, "guild_name": guild_name}), "discord"))
notify_task = loop.create_task(notify_on_timeout(call, url, time=30, repeat=True))
try:
response: PlaySuccessful = await download_task
data: dict = await download_task
except Exception as exc:
# RoyalPCMFile errors
if isinstance(exc, FileExistsError):
@ -114,5 +101,5 @@ class PlayCommand(Command):
raise
finally:
notify_task.cancel()
for info in response.info_list:
await call.reply(f"⬇️ Download di [i]{info.title}[/i] completato.")
for title in data["title_list"]:
await call.reply(f"⬇️ Download di [i]{title}[/i] completato.")

View file

@ -1,7 +1,7 @@
import typing
import asyncio
from ..utils import Command, Call, NetworkHandler
from ..network import Message, RequestSuccessful
from ..network import Request, ResponseSuccess
from ..error import NoneFoundError, TooManyFoundError
from ..audio import Playlist, Pool
if typing.TYPE_CHECKING:
@ -11,21 +11,15 @@ if typing.TYPE_CHECKING:
loop = asyncio.get_event_loop()
class PlaymodeMessage(Message):
def __init__(self, mode_name: str, guild_name: typing.Optional[str] = None):
self.mode_name: str = mode_name
self.guild_name: typing.Optional[str] = guild_name
class PlaymodeNH(NetworkHandler):
message_type = PlaymodeMessage
message_type = "music_playmode"
@classmethod
async def discord(cls, bot: "DiscordBot", message: PlaymodeMessage):
async def discord(cls, bot: "DiscordBot", data: dict):
"""Handle a playmode Royalnet request. That is, change current PlayMode."""
# Find the matching guild
if message.guild_name:
guild = bot.client.find_guild(message.guild_name)
if data["guild_name"]:
guild = bot.client.find_guild(data["guild_name"])
else:
if len(bot.music_data) == 0:
raise NoneFoundError("No voice clients active")
@ -36,13 +30,13 @@ class PlaymodeNH(NetworkHandler):
if bot.music_data[guild] is not None:
bot.music_data[guild].delete()
# Create the new PlayMode
if message.mode_name == "playlist":
if data["mode_name"] == "playlist":
bot.music_data[guild] = Playlist()
elif message.mode_name == "pool":
elif data["mode_name"] == "pool":
bot.music_data[guild] = Pool()
else:
raise ValueError("No such PlayMode")
return RequestSuccessful()
return ResponseSuccess()
class PlaymodeCommand(Command):
@ -54,6 +48,6 @@ class PlaymodeCommand(Command):
@classmethod
async def common(cls, call: Call):
guild, mode_name = call.args.match(r"(?:\[(.+)])?\s*(\S+)\s*")
await call.net_request(PlaymodeMessage(mode_name, guild), "discord")
await call.reply(f"Richiesto di passare alla modalità di riproduzione [c]{mode_name}[/c].")
guild_name, mode_name = call.args.match(r"(?:\[(.+)])?\s*(\S+)\s*")
await call.net_request(Request("music_playmode", {"mode_name": mode_name, "guild_name": guild_name}), "discord")
await call.reply(f"Modalità di riproduzione [c]{mode_name}[/c].")

View file

@ -1,25 +1,20 @@
import typing
import discord
from ..network import Message, RequestSuccessful
from ..network import Request, ResponseSuccess
from ..utils import Command, Call, NetworkHandler
from ..error import TooManyFoundError, NoneFoundError
if typing.TYPE_CHECKING:
from ..bots import DiscordBot
class SkipMessage(Message):
def __init__(self, guild_name: typing.Optional[str] = None):
self.guild_name: typing.Optional[str] = guild_name
class SkipNH(NetworkHandler):
message_type = SkipMessage
message_type = "music_skip"
@classmethod
async def discord(cls, bot: "DiscordBot", message: SkipMessage):
async def discord(cls, bot: "DiscordBot", data: dict):
# Find the matching guild
if message.guild_name:
guild = bot.client.find_guild_by_name(message.guild_name)
if data["guild_name"]:
guild = bot.client.find_guild_by_name(data["guild_name"])
else:
if len(bot.music_data) == 0:
raise NoneFoundError("No voice clients active")
@ -32,7 +27,7 @@ class SkipNH(NetworkHandler):
raise NoneFoundError("Nothing to skip")
# noinspection PyProtectedMember
voice_client._player.stop()
return RequestSuccessful()
return ResponseSuccess()
class SkipCommand(Command):
@ -46,5 +41,5 @@ class SkipCommand(Command):
@classmethod
async def common(cls, call: Call):
guild, = call.args.match(r"(?:\[(.+)])?")
await call.net_request(SkipMessage(guild), "discord")
await call.net_request(Request("music_skip", {"guild_name": guild}), "discord")
await call.reply(f"✅ Richiesto lo skip della canzone attuale.")

View file

@ -2,7 +2,7 @@ import typing
import discord
import asyncio
from ..utils import Command, Call, NetworkHandler
from ..network import Message, RequestSuccessful, RequestError
from ..network import Request, ResponseSuccess
from ..error import NoneFoundError
if typing.TYPE_CHECKING:
from ..bots import DiscordBot
@ -11,24 +11,17 @@ if typing.TYPE_CHECKING:
loop = asyncio.get_event_loop()
class SummonMessage(Message):
def __init__(self, channel_identifier: typing.Union[int, str],
guild_identifier: typing.Optional[typing.Union[int, str]] = None):
self.channel_name = channel_identifier
self.guild_identifier = guild_identifier
class SummonNH(NetworkHandler):
message_type = SummonMessage
message_type = "music_summon"
@classmethod
async def discord(cls, bot: "DiscordBot", message: SummonMessage):
async def discord(cls, bot: "DiscordBot", data: dict):
"""Handle a summon Royalnet request. That is, join a voice channel, or move to a different one if that is not possible."""
channel = bot.client.find_channel_by_name(message.channel_name)
channel = bot.client.find_channel_by_name(data["channel_name"])
if not isinstance(channel, discord.VoiceChannel):
raise NoneFoundError("Channel is not a voice channel")
loop.create_task(bot.client.vc_connect_or_move(channel))
return RequestSuccessful()
return ResponseSuccess()
class SummonCommand(Command):
@ -42,8 +35,7 @@ class SummonCommand(Command):
@classmethod
async def common(cls, call: Call):
channel_name: str = call.args[0].lstrip("#")
response: typing.Union[RequestSuccessful, RequestError] = await call.net_request(SummonMessage(channel_name), "discord")
response.raise_on_error()
await call.net_request(Request("music_summon", {"channel_name": channel_name}), "discord")
await call.reply(f"✅ Mi sono connesso in [c]#{channel_name}[/c].")
@classmethod

View file

@ -1,3 +1,8 @@
import typing
if typing.TYPE_CHECKING:
from .network import ResponseError
class NoneFoundError(Exception):
"""The element that was being looked for was not found."""
@ -22,11 +27,16 @@ class InvalidConfigError(Exception):
"""The bot has not been configured correctly, therefore the command can not function."""
class RoyalnetError(Exception):
class RoyalnetRequestError(Exception):
"""An error was raised while handling the Royalnet request.
This exception contains the exception that was raised during the handling."""
def __init__(self, exc: Exception):
self.exc: Exception = exc
This exception contains the :py:class:`royalnet.network.ResponseError` that was returned by the other Link."""
def __init__(self, error: "ResponseError"):
self.error: "ResponseError" = error
class RoyalnetResponseError(Exception):
"""The :py:class:`royalnet.network.Response` that was received is invalid."""
class ExternalError(Exception):

View file

@ -1,6 +1,6 @@
"""Royalnet realated classes."""
from .data import Data
from .request import Request
from .response import Response, ResponseSuccess, ResponseError
from .package import Package
from .royalnetlink import RoyalnetLink, NetworkError, NotConnectedError, NotIdentifiedError, ConnectionClosedError
from .royalnetserver import RoyalnetServer
@ -14,5 +14,7 @@ __all__ = ["RoyalnetLink",
"RoyalnetServer",
"RoyalnetConfig",
"ConnectionClosedError",
"Data",
"Request"]
"Request",
"Response",
"ResponseSuccess",
"ResponseError"]

View file

@ -1,7 +0,0 @@
class Data:
"""Royalnet data. All fields in this class will be converted to a dict when about to be sent."""
def __init__(self):
pass
def to_dict(self):
return self.__dict__

View file

@ -1,14 +1,16 @@
from .data import Data
class Request:
"""A request sent from a :py:class:`royalnet.network.RoyalnetLink` to another.
class Request(Data):
"""A Royalnet request. It contains the name of the requested handler, in addition to the data."""
It contains the name of the requested handler, in addition to the data."""
def __init__(self, handler: str, data: dict):
super().__init__()
self.handler: str = handler
self.data: dict = data
def to_dict(self):
return self.__dict__
@staticmethod
def from_dict(d: dict):
return Request(**d)
@ -17,3 +19,6 @@ class Request(Data):
if isinstance(other, Request):
return self.handler == other.handler and self.data == other.data
return False
def __repr__(self):
return f"royalnet.network.Request(handler={self.handler}, data={self.data})"

View file

@ -0,0 +1,61 @@
import typing
from ..error import RoyalnetRequestError
class Response:
"""A base class to be inherited by all other response types."""
def to_dict(self) -> dict:
"""Prepare the Response to be sent by converting it to a JSONable :py:class:`dict`."""
return {
"type": self.__class__.__name__,
**self.__dict__
}
def __eq__(self, other):
if isinstance(other, Response):
return self.to_dict() == other.to_dict()
return False
@classmethod
def from_dict(cls, d: dict) -> "Response":
"""Recreate the response from a received :py:class:`dict`."""
# Ignore type in dict
del d["type"]
# noinspection PyArgumentList
return cls(**d)
def raise_on_error(self):
"""Raise an :py:class:`Exception` if the Response is an error, do nothing otherwise."""
raise NotImplementedError("Please override Response.raise_on_error()")
class ResponseSuccess(Response):
"""A response to a successful :py:class:`royalnet.network.Request`."""
def __init__(self, data: typing.Optional[dict] = None):
if data is None:
self.data = {}
else:
self.data = data
def __repr__(self):
return f"royalnet.network.ResponseSuccess(data={self.data})"
def raise_on_error(self):
pass
class ResponseError(Response):
"""A response to a invalid :py:class:`royalnet.network.Request`."""
def __init__(self, name: str, description: str, extra_info: typing.Optional[dict] = None):
self.name: str = name
self.description: str = description
self.extra_info: typing.Optional[dict] = extra_info
def __repr__(self):
return f"royalnet.network.ResponseError(name={self.name}, description={self.description}, extra_info={self.extra_info})"
def raise_on_error(self):
raise RoyalnetRequestError(self)

View file

@ -161,11 +161,7 @@ class RoyalnetLink:
# Package is a request
assert isinstance(package, Package)
log.debug(f"Received request {package.source_conv_id}: {package}")
try:
response = await self.request_handler(package.data)
except Exception as exc:
response = {"error": "Exception in request_handler",
"exception": exc.__class__.__name__}
response_package: Package = package.reply(response)
await self.send(response_package)
log.debug(f"Replied to request {response_package.source_conv_id}: {response_package}")