1
Fork 0
mirror of https://github.com/RYGhub/royalnet.git synced 2024-11-23 19:44:20 +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 // Support: IE 8 - 9 only
// Console exists when dev tools are open, which can happen at any time // Console exists when dev tools are open, which can happen at any time
if ( window.console && window.console.warn && error && rerrorNames.test( error.name ) ) { 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 .generic import GenericBot
from ..commands import NullCommand from ..commands import NullCommand
from ..utils import asyncify, Call, Command from ..utils import asyncify, Call, Command
from ..error import UnregisteredError, NoneFoundError, TooManyFoundError, InvalidConfigError from ..error import UnregisteredError, NoneFoundError, TooManyFoundError, InvalidConfigError, RoyalnetResponseError
from ..network import Message, Reply, RoyalnetConfig from ..network import RoyalnetConfig, Request, Response, ResponseSuccess, ResponseError
from ..database import DatabaseConfig from ..database import DatabaseConfig
from ..audio import PlayMode, Playlist, RoyalPCMAudio from ..audio import PlayMode, Playlist, RoyalPCMAudio
@ -62,12 +62,20 @@ class DiscordBot(GenericBot):
.replace("[/p]", "```") .replace("[/p]", "```")
await call.channel.send(escaped_text) 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: if self.network is None:
raise InvalidConfigError("Royalnet is not enabled on this bot") 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() response.raise_on_error()
return response return response.data
async def get_author(call, error_if_none=False): async def get_author(call, error_if_none=False):
message: discord.Message = call.kwargs["message"] message: discord.Message = call.kwargs["message"]

View file

@ -4,7 +4,7 @@ import asyncio
import logging import logging
from ..utils import Command, NetworkHandler, Call from ..utils import Command, NetworkHandler, Call
from ..commands import NullCommand 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 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.""" """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") log.debug(f"Now generating commands")
self.commands: typing.Dict[str, typing.Type[Command]] = {} 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: for command in commands:
lower_command_name = command.command_name.lower() lower_command_name = command.command_name.lower()
self.commands[f"{command_prefix}{lower_command_name}"] = command self.commands[f"{command_prefix}{lower_command_name}"] = command
@ -47,25 +47,38 @@ class GenericBot:
log.debug(f"Running RoyalnetLink {self.network}") log.debug(f"Running RoyalnetLink {self.network}")
loop.create_task(self.network.run()) loop.create_task(self.network.run())
async def _network_handler(self, message: Message) -> Message: async def _network_handler(self, request_dict: dict) -> dict:
"""Handle a single :py:class:`royalnet.network.Message` received from the :py:class:`royalnet.network.RoyalnetLink`. """Handle a single :py:class:`dict` received from the :py:class:`royalnet.network.RoyalnetLink`.
Returns: Returns:
Another message, to be sent as :py:class:`royalnet.network.Reply`.""" Another :py:class:`dict`, formatted as a :py:class:`royalnet.network.Response`."""
log.debug(f"Received {message} from the RoyalnetLink") # Convert the dict to a Request
try: 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: except KeyError:
_, exc, tb = sys.exc_info() log.warning(f"Missing network_handler for {request.handler}")
log.debug(f"Missing network_handler for {message}") return ResponseError("no_handler", f"This Link is missing a network handler for {request.handler}.").to_dict()
raise Exception(f"Missing network_handler for {message}")
try: try:
log.debug(f"Using {network_handler} as handler for {message}") log.debug(f"Using {network_handler} as handler for {request.handler}")
return await getattr(network_handler, self.interface_name)(self, message) response: Response = await getattr(network_handler, self.interface_name)(self, request.data)
return response.to_dict()
except Exception: except Exception:
_, exc, _ = sys.exc_info() _, exc, _ = sys.exc_info()
log.debug(f"Exception {exc} in {network_handler}") 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): 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``.""" """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 import telegram
from telegram.utils.request import Request import telegram.utils.request
import asyncio import asyncio
import typing import typing
import logging as _logging import logging as _logging
from .generic import GenericBot from .generic import GenericBot
from ..commands import NullCommand from ..commands import NullCommand
from ..utils import asyncify, Call, Command from ..utils import asyncify, Call, Command
from ..error import UnregisteredError, InvalidConfigError from ..error import UnregisteredError, InvalidConfigError, RoyalnetResponseError
from ..network import Message, RoyalnetConfig, Reply from ..network import RoyalnetConfig, Request, Response, ResponseSuccess, ResponseError
from ..database import DatabaseConfig from ..database import DatabaseConfig
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
@ -27,7 +27,7 @@ class TelegramBot(GenericBot):
def _init_client(self): def _init_client(self):
"""Create the :py:class:`telegram.Bot`, and set the starting offset.""" """Create the :py:class:`telegram.Bot`, and set the starting offset."""
# https://github.com/python-telegram-bot/python-telegram-bot/issues/341 # 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.client = telegram.Bot(self._telegram_config.token, request=request)
self._offset: int = -100 self._offset: int = -100
@ -55,12 +55,20 @@ class TelegramBot(GenericBot):
.replace("[/p]", "</pre>") .replace("[/p]", "</pre>")
await asyncify(call.channel.send_message, escaped_text, parse_mode="HTML") 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: if self.network is None:
raise InvalidConfigError("Royalnet is not enabled on this bot") 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() response.raise_on_error()
return response return response.data
async def get_author(call, error_if_none=False): async def get_author(call, error_if_none=False):
update: telegram.Update = call.kwargs["update"] update: telegram.Update = call.kwargs["update"]

View file

@ -1,14 +1,6 @@
import logging as _logging import logging as _logging
from ..utils import Command, Call from ..utils import Command, Call
from ..error import NoneFoundError, \ from ..error import *
TooManyFoundError, \
UnregisteredError, \
UnsupportedError, \
InvalidInputError, \
InvalidConfigError, \
RoyalnetError, \
ExternalError
log = _logging.getLogger(__name__) log = _logging.getLogger(__name__)
@ -41,11 +33,15 @@ class ErrorHandlerCommand(Command):
if isinstance(exception, InvalidConfigError): 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]") await call.reply(f"⚠️ Il bot non è stato configurato correttamente, quindi questo comando non può essere eseguito.\n[p]{exception}[/p]")
return return
if isinstance(exception, RoyalnetError): if isinstance(exception, RoyalnetRequestError):
await call.reply(f"⚠️ La richiesta a Royalnet ha restituito un errore: [p]{exception.exc}[/p]") await call.reply(f"⚠️ La richiesta a Royalnet ha restituito un errore: [p]{exception.error}[/p]")
return return
if isinstance(exception, ExternalError): 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]") 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 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}") 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 youtube_dl
import ffmpeg import ffmpeg
from ..utils import Command, Call, NetworkHandler, asyncify from ..utils import Command, Call, NetworkHandler, asyncify
from ..network import Request, Data from ..network import Request, ResponseSuccess
from ..error import TooManyFoundError, NoneFoundError from ..error import TooManyFoundError, NoneFoundError
from ..audio import RoyalPCMAudio, YtdlInfo from ..audio import RoyalPCMAudio
if typing.TYPE_CHECKING: if typing.TYPE_CHECKING:
from ..bots import DiscordBot from ..bots import DiscordBot
@ -13,28 +13,15 @@ if typing.TYPE_CHECKING:
loop = asyncio.get_event_loop() 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): class PlayNH(NetworkHandler):
message_type = PlayMessage message_type = "music_play"
@classmethod @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.""" """Handle a play Royalnet request. That is, add audio to a PlayMode."""
# Find the matching guild # Find the matching guild
if message.guild_name: if data["guild_name"]:
guild = bot.client.find_guild(message.guild_name) guild = bot.client.find_guild(data["guild_name"])
else: else:
if len(bot.music_data) == 0: if len(bot.music_data) == 0:
raise NoneFoundError("No voice clients active") raise NoneFoundError("No voice clients active")
@ -46,12 +33,12 @@ class PlayNH(NetworkHandler):
# TODO: change Exception # TODO: change Exception
raise Exception("No music_data for this guild") raise Exception("No music_data for this guild")
# Start downloading # Start downloading
if message.url.startswith("http://") or message.url.startswith("https://"): if data["url"].startswith("http://") or data["url"].startswith("https://"):
audio_sources: typing.List[RoyalPCMAudio] = await asyncify(RoyalPCMAudio.create_from_url, message.url) audio_sources: typing.List[RoyalPCMAudio] = await asyncify(RoyalPCMAudio.create_from_url, data["url"])
else: 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) 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): async def notify_on_timeout(call: Call, url: str, time: float, repeat: bool = False):
@ -72,11 +59,11 @@ class PlayCommand(Command):
@classmethod @classmethod
async def common(cls, call: Call): async def common(cls, call: Call):
guild, url = call.args.match(r"(?:\[(.+)])?\s*(.+)") guild_name, url = call.args.match(r"(?:\[(.+)])?\s*(.+)")
download_task = loop.create_task(call.net_request(PlayMessage(url, guild), "discord")) 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)) notify_task = loop.create_task(notify_on_timeout(call, url, time=30, repeat=True))
try: try:
response: PlaySuccessful = await download_task data: dict = await download_task
except Exception as exc: except Exception as exc:
# RoyalPCMFile errors # RoyalPCMFile errors
if isinstance(exc, FileExistsError): if isinstance(exc, FileExistsError):
@ -114,5 +101,5 @@ class PlayCommand(Command):
raise raise
finally: finally:
notify_task.cancel() notify_task.cancel()
for info in response.info_list: for title in data["title_list"]:
await call.reply(f"⬇️ Download di [i]{info.title}[/i] completato.") await call.reply(f"⬇️ Download di [i]{title}[/i] completato.")

View file

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

View file

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

View file

@ -2,7 +2,7 @@ import typing
import discord import discord
import asyncio import asyncio
from ..utils import Command, Call, NetworkHandler from ..utils import Command, Call, NetworkHandler
from ..network import Message, RequestSuccessful, RequestError from ..network import Request, ResponseSuccess
from ..error import NoneFoundError from ..error import NoneFoundError
if typing.TYPE_CHECKING: if typing.TYPE_CHECKING:
from ..bots import DiscordBot from ..bots import DiscordBot
@ -11,24 +11,17 @@ if typing.TYPE_CHECKING:
loop = asyncio.get_event_loop() 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): class SummonNH(NetworkHandler):
message_type = SummonMessage message_type = "music_summon"
@classmethod @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.""" """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): if not isinstance(channel, discord.VoiceChannel):
raise NoneFoundError("Channel is not a voice channel") raise NoneFoundError("Channel is not a voice channel")
loop.create_task(bot.client.vc_connect_or_move(channel)) loop.create_task(bot.client.vc_connect_or_move(channel))
return RequestSuccessful() return ResponseSuccess()
class SummonCommand(Command): class SummonCommand(Command):
@ -42,8 +35,7 @@ class SummonCommand(Command):
@classmethod @classmethod
async def common(cls, call: Call): async def common(cls, call: Call):
channel_name: str = call.args[0].lstrip("#") channel_name: str = call.args[0].lstrip("#")
response: typing.Union[RequestSuccessful, RequestError] = await call.net_request(SummonMessage(channel_name), "discord") await call.net_request(Request("music_summon", {"channel_name": channel_name}), "discord")
response.raise_on_error()
await call.reply(f"✅ Mi sono connesso in [c]#{channel_name}[/c].") await call.reply(f"✅ Mi sono connesso in [c]#{channel_name}[/c].")
@classmethod @classmethod

View file

@ -1,3 +1,8 @@
import typing
if typing.TYPE_CHECKING:
from .network import ResponseError
class NoneFoundError(Exception): class NoneFoundError(Exception):
"""The element that was being looked for was not found.""" """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.""" """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. """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): This exception contains the :py:class:`royalnet.network.ResponseError` that was returned by the other Link."""
self.exc: Exception = exc 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): class ExternalError(Exception):

View file

@ -1,6 +1,6 @@
"""Royalnet realated classes.""" """Royalnet realated classes."""
from .data import Data
from .request import Request from .request import Request
from .response import Response, ResponseSuccess, ResponseError
from .package import Package from .package import Package
from .royalnetlink import RoyalnetLink, NetworkError, NotConnectedError, NotIdentifiedError, ConnectionClosedError from .royalnetlink import RoyalnetLink, NetworkError, NotConnectedError, NotIdentifiedError, ConnectionClosedError
from .royalnetserver import RoyalnetServer from .royalnetserver import RoyalnetServer
@ -14,5 +14,7 @@ __all__ = ["RoyalnetLink",
"RoyalnetServer", "RoyalnetServer",
"RoyalnetConfig", "RoyalnetConfig",
"ConnectionClosedError", "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.
It contains the name of the requested handler, in addition to the data."""
class Request(Data):
"""A Royalnet request. It contains the name of the requested handler, in addition to the data."""
def __init__(self, handler: str, data: dict): def __init__(self, handler: str, data: dict):
super().__init__() super().__init__()
self.handler: str = handler self.handler: str = handler
self.data: dict = data self.data: dict = data
def to_dict(self):
return self.__dict__
@staticmethod @staticmethod
def from_dict(d: dict): def from_dict(d: dict):
return Request(**d) return Request(**d)
@ -17,3 +19,6 @@ class Request(Data):
if isinstance(other, Request): if isinstance(other, Request):
return self.handler == other.handler and self.data == other.data return self.handler == other.handler and self.data == other.data
return False 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 # Package is a request
assert isinstance(package, Package) assert isinstance(package, Package)
log.debug(f"Received request {package.source_conv_id}: {package}") log.debug(f"Received request {package.source_conv_id}: {package}")
try: response = await self.request_handler(package.data)
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) response_package: Package = package.reply(response)
await self.send(response_package) await self.send(response_package)
log.debug(f"Replied to request {response_package.source_conv_id}: {response_package}") log.debug(f"Replied to request {response_package.source_conv_id}: {response_package}")