mirror of
https://github.com/RYGhub/royalnet.git
synced 2024-11-27 13:34:28 +00:00
#53: Use JSON for Royalnet communication
This commit is contained in:
commit
990011dadf
22 changed files with 455 additions and 306 deletions
2
docs/html/_static/jquery-3.2.1.js
vendored
2
docs/html/_static/jquery-3.2.1.js
vendored
|
@ -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 );
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
2
docs/html/_static/jquery.js
vendored
2
docs/html/_static/jquery.js
vendored
File diff suppressed because one or more lines are too long
|
@ -1,5 +0,0 @@
|
||||||
from . import audio, bots, commands, database, network, utils, error
|
|
||||||
|
|
||||||
version = "5.0a7"
|
|
||||||
|
|
||||||
__all__ = ["audio", "bots", "commands", "database", "network", "utils", "error"]
|
|
|
@ -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"]
|
||||||
|
|
|
@ -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``."""
|
||||||
|
|
|
@ -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"]
|
||||||
|
|
|
@ -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]")
|
||||||
|
|
|
@ -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 Message, RequestSuccessful
|
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,26 +13,15 @@ if typing.TYPE_CHECKING:
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
|
|
||||||
|
|
||||||
class PlayMessage(Message):
|
|
||||||
def __init__(self, url: str, guild_name: typing.Optional[str] = None):
|
|
||||||
self.url: str = url
|
|
||||||
self.guild_name: typing.Optional[str] = guild_name
|
|
||||||
|
|
||||||
|
|
||||||
class PlaySuccessful(RequestSuccessful):
|
|
||||||
def __init__(self, info_list: typing.List[YtdlInfo]):
|
|
||||||
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")
|
||||||
|
@ -44,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):
|
||||||
|
@ -70,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):
|
||||||
|
@ -112,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.")
|
||||||
|
|
|
@ -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].")
|
||||||
|
|
|
@ -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.")
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -1,23 +1,20 @@
|
||||||
"""Royalnet realated classes."""
|
"""Royalnet realated classes."""
|
||||||
|
from .request import Request
|
||||||
from .messages import Message, ServerErrorMessage, InvalidSecretEM, InvalidDestinationEM, InvalidPackageEM, RequestSuccessful, RequestError, Reply
|
from .response import Response, ResponseSuccess, ResponseError
|
||||||
from .packages import Package
|
from .package import Package
|
||||||
from .royalnetlink import RoyalnetLink, NetworkError, NotConnectedError, NotIdentifiedError
|
from .royalnetlink import RoyalnetLink, NetworkError, NotConnectedError, NotIdentifiedError, ConnectionClosedError
|
||||||
from .royalnetserver import RoyalnetServer
|
from .royalnetserver import RoyalnetServer
|
||||||
from .royalnetconfig import RoyalnetConfig
|
from .royalnetconfig import RoyalnetConfig
|
||||||
|
|
||||||
__all__ = ["Message",
|
__all__ = ["RoyalnetLink",
|
||||||
"ServerErrorMessage",
|
|
||||||
"InvalidSecretEM",
|
|
||||||
"InvalidDestinationEM",
|
|
||||||
"InvalidPackageEM",
|
|
||||||
"RoyalnetLink",
|
|
||||||
"NetworkError",
|
"NetworkError",
|
||||||
"NotConnectedError",
|
"NotConnectedError",
|
||||||
"NotIdentifiedError",
|
"NotIdentifiedError",
|
||||||
"Package",
|
"Package",
|
||||||
"RoyalnetServer",
|
"RoyalnetServer",
|
||||||
"RequestSuccessful",
|
|
||||||
"RequestError",
|
|
||||||
"RoyalnetConfig",
|
"RoyalnetConfig",
|
||||||
"Reply"]
|
"ConnectionClosedError",
|
||||||
|
"Request",
|
||||||
|
"Response",
|
||||||
|
"ResponseSuccess",
|
||||||
|
"ResponseError"]
|
||||||
|
|
|
@ -1,78 +0,0 @@
|
||||||
import typing
|
|
||||||
import pickle
|
|
||||||
from ..error import RoyalnetError
|
|
||||||
|
|
||||||
|
|
||||||
class Message:
|
|
||||||
"""A message sent through the Royalnet."""
|
|
||||||
def __repr__(self):
|
|
||||||
return f"<{self.__class__.__name__}>"
|
|
||||||
|
|
||||||
|
|
||||||
class IdentifySuccessfulMessage(Message):
|
|
||||||
"""The Royalnet identification step was successful."""
|
|
||||||
|
|
||||||
|
|
||||||
class ServerErrorMessage(Message):
|
|
||||||
"""Something went wrong in the connection to the :py:class:`royalnet.network.RoyalnetServer`."""
|
|
||||||
def __init__(self, reason):
|
|
||||||
super().__init__()
|
|
||||||
self.reason = reason
|
|
||||||
|
|
||||||
|
|
||||||
class InvalidSecretEM(ServerErrorMessage):
|
|
||||||
"""The sent secret was incorrect.
|
|
||||||
|
|
||||||
This message terminates connection to the :py:class:`royalnet.network.RoyalnetServer`."""
|
|
||||||
|
|
||||||
|
|
||||||
class InvalidPackageEM(ServerErrorMessage):
|
|
||||||
"""The sent :py:class:`royalnet.network.Package` was invalid."""
|
|
||||||
|
|
||||||
|
|
||||||
class InvalidDestinationEM(InvalidPackageEM):
|
|
||||||
"""The :py:class:`royalnet.network.Package` destination was invalid or not found."""
|
|
||||||
|
|
||||||
|
|
||||||
class Reply(Message):
|
|
||||||
"""A reply to a request sent through the Royalnet."""
|
|
||||||
|
|
||||||
def raise_on_error(self) -> None:
|
|
||||||
"""If the reply is an error, raise an error, otherwise, do nothing.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
A :py:exc:`RoyalnetError`, if the Reply is an error, otherwise, nothing."""
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
|
|
||||||
class RequestSuccessful(Reply):
|
|
||||||
"""The sent request was successful."""
|
|
||||||
|
|
||||||
def raise_on_error(self) -> None:
|
|
||||||
"""If the reply is an error, raise an error, otherwise, do nothing.
|
|
||||||
|
|
||||||
Does nothing."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class RequestError(Reply):
|
|
||||||
"""The sent request wasn't successful."""
|
|
||||||
|
|
||||||
def __init__(self, exc: typing.Optional[Exception] = None):
|
|
||||||
"""Create a RequestError.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
exc: The exception that caused the error in the request."""
|
|
||||||
try:
|
|
||||||
pickle.dumps(exc)
|
|
||||||
except TypeError:
|
|
||||||
self.exc: Exception = Exception(repr(exc))
|
|
||||||
else:
|
|
||||||
self.exc = exc
|
|
||||||
|
|
||||||
def raise_on_error(self) -> None:
|
|
||||||
"""If the reply is an error, raise an error, otherwise, do nothing.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
Always raises a :py:exc:`royalnet.error.RoyalnetError`, containing the exception that caused the error."""
|
|
||||||
raise RoyalnetError(exc=self.exc)
|
|
111
royalnet/network/package.py
Normal file
111
royalnet/network/package.py
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
import json
|
||||||
|
import uuid
|
||||||
|
import typing
|
||||||
|
|
||||||
|
|
||||||
|
class Package:
|
||||||
|
"""A Royalnet package, the data type with which a :py:class:`royalnet.network.RoyalnetLink` communicates with a :py:class:`royalnet.network.RoyalnetServer` or another link.
|
||||||
|
Contains info about the source and the destination."""
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
data: dict,
|
||||||
|
*,
|
||||||
|
source: str,
|
||||||
|
destination: str,
|
||||||
|
source_conv_id: typing.Optional[str] = None,
|
||||||
|
destination_conv_id: typing.Optional[str] = None):
|
||||||
|
"""Create a Package.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
data: The data that should be sent. Usually a :py:class:`royalnet.network.Message`.
|
||||||
|
source: The ``nid`` of the node that created this Package.
|
||||||
|
destination: The ``link_type`` of the destination node, or alternatively, the ``nid`` of the node. Can also be the ``NULL`` value to send the message to nobody.
|
||||||
|
source_conv_id: The conversation id of the node that created this package. Akin to the sequence number on IP packets.
|
||||||
|
destination_conv_id: The conversation id of the node that this Package is a reply to."""
|
||||||
|
# TODO: something is not right in these type hints. Check them.
|
||||||
|
self.data: dict = data
|
||||||
|
self.source: str = source
|
||||||
|
self.source_conv_id: str = source_conv_id or str(uuid.uuid4())
|
||||||
|
self.destination: str = destination
|
||||||
|
self.destination_conv_id: typing.Optional[str] = destination_conv_id
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<Package {self.source} ({self.source_conv_id}) to {self.destination} ({self.destination_conv_id}>"
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
if isinstance(other, Package):
|
||||||
|
return (self.data == other.data) and \
|
||||||
|
(self.source == other.source) and \
|
||||||
|
(self.destination == other.destination) and \
|
||||||
|
(self.source_conv_id == other.source_conv_id) and \
|
||||||
|
(self.destination_conv_id == other.destination_conv_id)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def reply(self, data) -> "Package":
|
||||||
|
"""Reply to this Package with another Package.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
data: The data that should be sent. Usually a :py:class:`royalnet.network.Message`.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The reply Package."""
|
||||||
|
return Package(data,
|
||||||
|
source=self.destination,
|
||||||
|
destination=self.source,
|
||||||
|
source_conv_id=self.destination_conv_id or str(uuid.uuid4()),
|
||||||
|
destination_conv_id=self.source_conv_id)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_dict(d) -> "Package":
|
||||||
|
"""Create a Package from a dictionary."""
|
||||||
|
if "source" not in d:
|
||||||
|
raise ValueError("Missing source field")
|
||||||
|
if "nid" not in d["source"]:
|
||||||
|
raise ValueError("Missing source.nid field")
|
||||||
|
if "conv_id" not in d["source"]:
|
||||||
|
raise ValueError("Missing source.conv_id field")
|
||||||
|
if "destination" not in d:
|
||||||
|
raise ValueError("Missing destination field")
|
||||||
|
if "nid" not in d["destination"]:
|
||||||
|
raise ValueError("Missing destination.nid field")
|
||||||
|
if "conv_id" not in d["destination"]:
|
||||||
|
raise ValueError("Missing destination.conv_id field")
|
||||||
|
if "data" not in d:
|
||||||
|
raise ValueError("Missing data field")
|
||||||
|
return Package(d["data"],
|
||||||
|
source=d["source"]["nid"],
|
||||||
|
destination=d["destination"]["nid"],
|
||||||
|
source_conv_id=d["source"]["conv_id"],
|
||||||
|
destination_conv_id=d["destination"]["conv_id"])
|
||||||
|
|
||||||
|
def to_dict(self) -> dict:
|
||||||
|
"""Convert the Package into a dictionary."""
|
||||||
|
return {
|
||||||
|
"source": {
|
||||||
|
"nid": self.source,
|
||||||
|
"conv_id": self.source_conv_id
|
||||||
|
},
|
||||||
|
"destination": {
|
||||||
|
"nid": self.destination,
|
||||||
|
"conv_id": self.destination_conv_id
|
||||||
|
},
|
||||||
|
"data": self.data
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_json_string(string: str) -> "Package":
|
||||||
|
"""Create a Package from a JSON string."""
|
||||||
|
return Package.from_dict(json.loads(string))
|
||||||
|
|
||||||
|
def to_json_string(self) -> str:
|
||||||
|
"""Convert the Package into a JSON string."""
|
||||||
|
return json.dumps(self.to_dict())
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_json_bytes(b: bytes) -> "Package":
|
||||||
|
"""Create a Package from UTF8-encoded JSON bytes."""
|
||||||
|
return Package.from_json_string(str(b, encoding="utf8"))
|
||||||
|
|
||||||
|
def to_json_bytes(self) -> bytes:
|
||||||
|
"""Convert the Package into UTF8-encoded JSON bytes."""
|
||||||
|
return bytes(self.to_json_string(), encoding="utf8")
|
|
@ -1,44 +0,0 @@
|
||||||
import pickle
|
|
||||||
import uuid
|
|
||||||
|
|
||||||
|
|
||||||
class Package:
|
|
||||||
"""A Royalnet package, the data type with which a :py:class:`royalnet.network.RoyalnetLink` communicates with a :py:class:`royalnet.network.RoyalnetServer` or another link. """
|
|
||||||
|
|
||||||
def __init__(self, data, destination: str, source: str, *, source_conv_id: str = None, destination_conv_id: str = None):
|
|
||||||
"""Create a Package.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
data: The data that should be sent. Usually a :py:class:`royalnet.network.Message`.
|
|
||||||
destination: The ``link_type`` of the destination node, or alternatively, the ``nid`` of the node. Can also be the ``NULL`` value to send the message to nobody.
|
|
||||||
source: The ``nid`` of the node that created this Package.
|
|
||||||
source_conv_id: The conversation id of the node that created this package. Akin to the sequence number on IP packets.
|
|
||||||
destination_conv_id: The conversation id of the node that this Package is a reply to."""
|
|
||||||
# TODO: something is not right in these type hints. Check them.
|
|
||||||
self.data = data
|
|
||||||
self.destination: str = destination
|
|
||||||
self.source: str = source
|
|
||||||
self.source_conv_id: str = source_conv_id or str(uuid.uuid4())
|
|
||||||
self.destination_conv_id: str = destination_conv_id
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return f"<Package to {self.destination}: {self.data.__class__.__name__}>"
|
|
||||||
|
|
||||||
def reply(self, data) -> "Package":
|
|
||||||
"""Reply to this Package with another Package.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
data: The data that should be sent. Usually a :py:class:`royalnet.network.Message`.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The reply Package."""
|
|
||||||
return Package(data, self.source, self.destination,
|
|
||||||
source_conv_id=str(uuid.uuid4()),
|
|
||||||
destination_conv_id=self.source_conv_id)
|
|
||||||
|
|
||||||
def pickle(self) -> bytes:
|
|
||||||
""":py:mod:`pickle` this Package.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The pickled package in form of bytes."""
|
|
||||||
return pickle.dumps(self)
|
|
24
royalnet/network/request.py
Normal file
24
royalnet/network/request.py
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
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."""
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
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})"
|
61
royalnet/network/response.py
Normal file
61
royalnet/network/response.py
Normal 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)
|
|
@ -2,11 +2,11 @@ import asyncio
|
||||||
import websockets
|
import websockets
|
||||||
import uuid
|
import uuid
|
||||||
import functools
|
import functools
|
||||||
import typing
|
import math
|
||||||
import pickle
|
import numbers
|
||||||
import logging as _logging
|
import logging as _logging
|
||||||
from .messages import Message, ServerErrorMessage, RequestError
|
import typing
|
||||||
from .packages import Package
|
from .package import Package
|
||||||
|
|
||||||
default_loop = asyncio.get_event_loop()
|
default_loop = asyncio.get_event_loop()
|
||||||
log = _logging.getLogger(__name__)
|
log = _logging.getLogger(__name__)
|
||||||
|
@ -20,16 +20,24 @@ class NotIdentifiedError(Exception):
|
||||||
"""The :py:class:`royalnet.network.RoyalnetLink` has not identified yet to a :py:class:`royalnet.network.RoyalnetServer`."""
|
"""The :py:class:`royalnet.network.RoyalnetLink` has not identified yet to a :py:class:`royalnet.network.RoyalnetServer`."""
|
||||||
|
|
||||||
|
|
||||||
|
class ConnectionClosedError(Exception):
|
||||||
|
"""The :py:class:`royalnet.network.RoyalnetLink`'s connection was closed unexpectedly. The link can't be used anymore."""
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidServerResponseError(Exception):
|
||||||
|
"""The :py:class:`royalnet.network.RoyalnetServer` sent invalid data to the :py:class:`royalnet.network.RoyalnetLink`."""
|
||||||
|
|
||||||
|
|
||||||
class NetworkError(Exception):
|
class NetworkError(Exception):
|
||||||
def __init__(self, error_msg: ServerErrorMessage, *args):
|
def __init__(self, error_data: dict, *args):
|
||||||
super().__init__(*args)
|
super().__init__(*args)
|
||||||
self.error_msg: ServerErrorMessage = error_msg
|
self.error_data: dict = error_data
|
||||||
|
|
||||||
|
|
||||||
class PendingRequest:
|
class PendingRequest:
|
||||||
def __init__(self, *, loop=default_loop):
|
def __init__(self, *, loop=default_loop):
|
||||||
self.event: asyncio.Event = asyncio.Event(loop=loop)
|
self.event: asyncio.Event = asyncio.Event(loop=loop)
|
||||||
self.data: typing.Optional[Message] = None
|
self.data: typing.Optional[dict] = None
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
if self.event.is_set():
|
if self.event.is_set():
|
||||||
|
@ -44,7 +52,7 @@ class PendingRequest:
|
||||||
def requires_connection(func):
|
def requires_connection(func):
|
||||||
@functools.wraps(func)
|
@functools.wraps(func)
|
||||||
async def new_func(self, *args, **kwargs):
|
async def new_func(self, *args, **kwargs):
|
||||||
await self._connect_event.wait()
|
await self.connect_event.wait()
|
||||||
return await func(self, *args, **kwargs)
|
return await func(self, *args, **kwargs)
|
||||||
return new_func
|
return new_func
|
||||||
|
|
||||||
|
@ -67,30 +75,37 @@ class RoyalnetLink:
|
||||||
self.secret: str = secret
|
self.secret: str = secret
|
||||||
self.websocket: typing.Optional[websockets.WebSocketClientProtocol] = None
|
self.websocket: typing.Optional[websockets.WebSocketClientProtocol] = None
|
||||||
self.request_handler = request_handler
|
self.request_handler = request_handler
|
||||||
self._pending_requests: typing.Dict[str, typing.Optional[Message]] = {}
|
self._pending_requests: typing.Dict[str, PendingRequest] = {}
|
||||||
self._loop: asyncio.AbstractEventLoop = loop
|
self._loop: asyncio.AbstractEventLoop = loop
|
||||||
self._connect_event: asyncio.Event = asyncio.Event(loop=self._loop)
|
self.error_event: asyncio.Event = asyncio.Event(loop=self._loop)
|
||||||
|
self.connect_event: asyncio.Event = asyncio.Event(loop=self._loop)
|
||||||
self.identify_event: asyncio.Event = asyncio.Event(loop=self._loop)
|
self.identify_event: asyncio.Event = asyncio.Event(loop=self._loop)
|
||||||
|
|
||||||
async def connect(self):
|
async def connect(self):
|
||||||
|
"""Connect to the :py:class:`royalnet.network.RoyalnetServer` at ``self.master_uri``."""
|
||||||
log.info(f"Connecting to {self.master_uri}...")
|
log.info(f"Connecting to {self.master_uri}...")
|
||||||
self.websocket = await websockets.connect(self.master_uri, loop=self._loop)
|
self.websocket = await websockets.connect(self.master_uri, loop=self._loop)
|
||||||
self._connect_event.set()
|
self.connect_event.set()
|
||||||
log.info(f"Connected!")
|
log.info(f"Connected!")
|
||||||
|
|
||||||
@requires_connection
|
@requires_connection
|
||||||
async def receive(self) -> Package:
|
async def receive(self) -> Package:
|
||||||
|
"""Recieve a :py:class:`Package` from the :py:class:`royalnet.network.RoyalnetServer`.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
:py:exc:`royalnet.network.royalnetlink.ConnectionClosedError` if the connection closes."""
|
||||||
try:
|
try:
|
||||||
raw_pickle = await self.websocket.recv()
|
jbytes: bytes = await self.websocket.recv()
|
||||||
|
package: Package = Package.from_json_bytes(jbytes)
|
||||||
except websockets.ConnectionClosed:
|
except websockets.ConnectionClosed:
|
||||||
self.websocket = None
|
self.error_event.set()
|
||||||
self._connect_event.clear()
|
self.connect_event.clear()
|
||||||
self.identify_event.clear()
|
self.identify_event.clear()
|
||||||
log.info(f"Connection to {self.master_uri} was closed.")
|
log.info(f"Connection to {self.master_uri} was closed.")
|
||||||
# What to do now? Let's just reraise.
|
# What to do now? Let's just reraise.
|
||||||
raise
|
raise ConnectionClosedError()
|
||||||
package: typing.Union[Package, Package] = pickle.loads(raw_pickle)
|
if self.identify_event.is_set() and package.destination != self.nid:
|
||||||
assert package.destination == self.nid
|
raise InvalidServerResponseError("Package is not addressed to this RoyalnetLink.")
|
||||||
log.debug(f"Received package: {package}")
|
log.debug(f"Received package: {package}")
|
||||||
return package
|
return package
|
||||||
|
|
||||||
|
@ -98,37 +113,42 @@ class RoyalnetLink:
|
||||||
async def identify(self) -> None:
|
async def identify(self) -> None:
|
||||||
log.info(f"Identifying to {self.master_uri}...")
|
log.info(f"Identifying to {self.master_uri}...")
|
||||||
await self.websocket.send(f"Identify {self.nid}:{self.link_type}:{self.secret}")
|
await self.websocket.send(f"Identify {self.nid}:{self.link_type}:{self.secret}")
|
||||||
response_package = await self.receive()
|
response: Package = await self.receive()
|
||||||
response = response_package.data
|
if not response.source == "<server>":
|
||||||
if isinstance(response, ServerErrorMessage):
|
raise InvalidServerResponseError("Received a non-service package before identification.")
|
||||||
raise NetworkError(response, "Server returned error while identifying self")
|
if "type" not in response.data:
|
||||||
|
raise InvalidServerResponseError("Missing 'type' in response data")
|
||||||
|
if response.data["type"] == "error":
|
||||||
|
raise ConnectionClosedError(f"Identification error: {response.data['type']}")
|
||||||
|
assert response.data["type"] == "success"
|
||||||
self.identify_event.set()
|
self.identify_event.set()
|
||||||
log.info(f"Identified successfully!")
|
log.info(f"Identified successfully!")
|
||||||
|
|
||||||
@requires_identification
|
@requires_identification
|
||||||
async def send(self, package: Package):
|
async def send(self, package: Package):
|
||||||
raw_pickle: bytes = pickle.dumps(package)
|
await self.websocket.send(package.to_json_bytes())
|
||||||
await self.websocket.send(raw_pickle)
|
|
||||||
log.debug(f"Sent package: {package}")
|
log.debug(f"Sent package: {package}")
|
||||||
|
|
||||||
@requires_identification
|
@requires_identification
|
||||||
async def request(self, message, destination):
|
async def request(self, message, destination):
|
||||||
package = Package(message, destination, self.nid)
|
package = Package(message, source=self.nid, destination=destination)
|
||||||
request = PendingRequest(loop=self._loop)
|
request = PendingRequest(loop=self._loop)
|
||||||
self._pending_requests[package.source_conv_id] = request
|
self._pending_requests[package.source_conv_id] = request
|
||||||
await self.send(package)
|
await self.send(package)
|
||||||
log.debug(f"Sent request: {message} -> {destination}")
|
log.debug(f"Sent request: {message} -> {destination}")
|
||||||
await request.event.wait()
|
await request.event.wait()
|
||||||
result: Message = request.data
|
response: dict = request.data
|
||||||
log.debug(f"Received response: {request} -> {result}")
|
log.debug(f"Received response: {request} -> {response}")
|
||||||
if isinstance(result, ServerErrorMessage):
|
return response
|
||||||
raise NetworkError(result, "Server returned error while requesting something")
|
|
||||||
return result
|
|
||||||
|
|
||||||
async def run(self):
|
async def run(self, loops: numbers.Real = math.inf):
|
||||||
|
"""Blockingly run the Link."""
|
||||||
log.debug(f"Running main client loop for {self.nid}.")
|
log.debug(f"Running main client loop for {self.nid}.")
|
||||||
while True:
|
if self.error_event.is_set():
|
||||||
if self.websocket is None:
|
raise ConnectionClosedError("RoyalnetLinks can't be rerun after an error.")
|
||||||
|
while loops:
|
||||||
|
loops -= 1
|
||||||
|
if not self.connect_event.is_set():
|
||||||
await self.connect()
|
await self.connect()
|
||||||
if not self.identify_event.is_set():
|
if not self.identify_event.is_set():
|
||||||
await self.identify()
|
await self.identify()
|
||||||
|
@ -141,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)
|
|
||||||
assert isinstance(response, Message)
|
|
||||||
except Exception as exc:
|
|
||||||
response = RequestError(exc=exc)
|
|
||||||
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}")
|
||||||
|
|
|
@ -2,12 +2,10 @@ import typing
|
||||||
import websockets
|
import websockets
|
||||||
import re
|
import re
|
||||||
import datetime
|
import datetime
|
||||||
import pickle
|
|
||||||
import uuid
|
import uuid
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging as _logging
|
import logging as _logging
|
||||||
from .messages import InvalidPackageEM, InvalidSecretEM, IdentifySuccessfulMessage
|
from .package import Package
|
||||||
from .packages import Package
|
|
||||||
|
|
||||||
default_loop = asyncio.get_event_loop()
|
default_loop = asyncio.get_event_loop()
|
||||||
log = _logging.getLogger(__name__)
|
log = _logging.getLogger(__name__)
|
||||||
|
@ -26,9 +24,14 @@ class ConnectedClient:
|
||||||
"""Has the client sent a valid identification package?"""
|
"""Has the client sent a valid identification package?"""
|
||||||
return bool(self.nid)
|
return bool(self.nid)
|
||||||
|
|
||||||
|
async def send_service(self, msg_type: str, message: str):
|
||||||
|
await self.send(Package({"type": msg_type, "service": message},
|
||||||
|
source="<server>",
|
||||||
|
destination=self.nid))
|
||||||
|
|
||||||
async def send(self, package: Package):
|
async def send(self, package: Package):
|
||||||
"""Send a :py:class:`royalnet.network.Package` to the :py:class:`royalnet.network.RoyalnetLink`."""
|
"""Send a :py:class:`royalnet.network.Package` to the :py:class:`royalnet.network.RoyalnetLink`."""
|
||||||
await self.socket.send(package.pickle())
|
await self.socket.send(package.to_json_bytes())
|
||||||
|
|
||||||
|
|
||||||
class RoyalnetServer:
|
class RoyalnetServer:
|
||||||
|
@ -49,38 +52,38 @@ class RoyalnetServer:
|
||||||
matching = [client for client in self.identified_clients if client.link_type == link_type]
|
matching = [client for client in self.identified_clients if client.link_type == link_type]
|
||||||
return matching or []
|
return matching or []
|
||||||
|
|
||||||
async def listener(self, websocket: websockets.server.WebSocketServerProtocol, request_uri: str):
|
async def listener(self, websocket: websockets.server.WebSocketServerProtocol, path):
|
||||||
log.info(f"{websocket.remote_address} connected to the server.")
|
log.info(f"{websocket.remote_address} connected to the server.")
|
||||||
connected_client = ConnectedClient(websocket)
|
connected_client = ConnectedClient(websocket)
|
||||||
# Wait for identification
|
# Wait for identification
|
||||||
identify_msg = await websocket.recv()
|
identify_msg = await websocket.recv()
|
||||||
log.debug(f"{websocket.remote_address} identified itself with: {identify_msg}.")
|
log.debug(f"{websocket.remote_address} identified itself with: {identify_msg}.")
|
||||||
if not isinstance(identify_msg, str):
|
if not isinstance(identify_msg, str):
|
||||||
await websocket.send(InvalidPackageEM("Invalid identification message (not a str)"))
|
await connected_client.send_service("error", "Invalid identification message (not a str)")
|
||||||
return
|
return
|
||||||
identification = re.match(r"Identify ([^:\s]+):([^:\s]+):([^:\s]+)", identify_msg)
|
identification = re.match(r"Identify ([^:\s]+):([^:\s]+):([^:\s]+)", identify_msg)
|
||||||
if identification is None:
|
if identification is None:
|
||||||
await websocket.send(InvalidPackageEM("Invalid identification message (regex failed)"))
|
await connected_client.send_service("error", "Invalid identification message (regex failed)")
|
||||||
return
|
return
|
||||||
secret = identification.group(3)
|
secret = identification.group(3)
|
||||||
if secret != self.required_secret:
|
if secret != self.required_secret:
|
||||||
await websocket.send(InvalidSecretEM("Invalid secret"))
|
await connected_client.send_service("error", "Invalid secret")
|
||||||
return
|
return
|
||||||
# Identification successful
|
# Identification successful
|
||||||
connected_client.nid = identification.group(1)
|
connected_client.nid = identification.group(1)
|
||||||
connected_client.link_type = identification.group(2)
|
connected_client.link_type = identification.group(2)
|
||||||
self.identified_clients.append(connected_client)
|
self.identified_clients.append(connected_client)
|
||||||
log.debug(f"{websocket.remote_address} identified successfully as {connected_client.nid} ({connected_client.link_type}).")
|
log.debug(f"{websocket.remote_address} identified successfully as {connected_client.nid} ({connected_client.link_type}).")
|
||||||
await connected_client.send(Package(IdentifySuccessfulMessage(), connected_client.nid, "__master__"))
|
await connected_client.send_service("success", "Identification successful!")
|
||||||
log.debug(f"{connected_client.nid}'s identification confirmed.")
|
log.debug(f"{connected_client.nid}'s identification confirmed.")
|
||||||
# Main loop
|
# Main loop
|
||||||
while True:
|
while True:
|
||||||
# Receive packages
|
# Receive packages
|
||||||
raw_pickle = await websocket.recv()
|
raw_bytes = await websocket.recv()
|
||||||
package: Package = pickle.loads(raw_pickle)
|
package: Package = Package.from_json_bytes(raw_bytes)
|
||||||
log.debug(f"Received package: {package}")
|
log.debug(f"Received package: {package}")
|
||||||
# Check if the package destination is the server itself.
|
# Check if the package destination is the server itself.
|
||||||
if package.destination == "__master__":
|
if package.destination == "<server>":
|
||||||
# TODO: do stuff
|
# TODO: do stuff
|
||||||
pass
|
pass
|
||||||
# Otherwise, route the package to its destination
|
# Otherwise, route the package to its destination
|
||||||
|
@ -97,7 +100,7 @@ class RoyalnetServer:
|
||||||
A :py:class:`list` of :py:class:`ConnectedClients` to send the package to."""
|
A :py:class:`list` of :py:class:`ConnectedClients` to send the package to."""
|
||||||
# Parse destination
|
# Parse destination
|
||||||
# Is it nothing?
|
# Is it nothing?
|
||||||
if package.destination == "NULL":
|
if package.destination == "<none>":
|
||||||
return []
|
return []
|
||||||
# Is it a valid nid?
|
# Is it a valid nid?
|
||||||
try:
|
try:
|
||||||
|
@ -114,7 +117,10 @@ class RoyalnetServer:
|
||||||
destinations = self.find_destination(package)
|
destinations = self.find_destination(package)
|
||||||
log.debug(f"Routing package: {package} -> {destinations}")
|
log.debug(f"Routing package: {package} -> {destinations}")
|
||||||
for destination in destinations:
|
for destination in destinations:
|
||||||
specific_package = Package(package.data, destination.nid, package.source,
|
# This may have some consequences
|
||||||
|
specific_package = Package(package.data,
|
||||||
|
source=package.source,
|
||||||
|
destination=destination.nid,
|
||||||
source_conv_id=package.source_conv_id,
|
source_conv_id=package.source_conv_id,
|
||||||
destination_conv_id=package.destination_conv_id)
|
destination_conv_id=package.destination_conv_id)
|
||||||
await destination.send(specific_package)
|
await destination.send(specific_package)
|
||||||
|
@ -123,7 +129,7 @@ class RoyalnetServer:
|
||||||
await websockets.serve(self.listener, host=self.address, port=self.port)
|
await websockets.serve(self.listener, host=self.address, port=self.port)
|
||||||
|
|
||||||
async def start(self):
|
async def start(self):
|
||||||
log.debug(f"Starting main server loop for __master__ on ws://{self.address}:{self.port}")
|
log.debug(f"Starting main server loop for <server> on ws://{self.address}:{self.port}")
|
||||||
# noinspection PyAsyncCall
|
# noinspection PyAsyncCall
|
||||||
self._loop.create_task(self.serve())
|
self._loop.create_task(self.serve())
|
||||||
# Just to be sure it has started on Linux
|
# Just to be sure it has started on Linux
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import typing
|
import typing
|
||||||
import asyncio
|
import asyncio
|
||||||
from ..network import Message, Reply
|
|
||||||
from .command import Command
|
from .command import Command
|
||||||
from .commandargs import CommandArgs
|
from .commandargs import CommandArgs
|
||||||
if typing.TYPE_CHECKING:
|
if typing.TYPE_CHECKING:
|
||||||
|
@ -26,7 +25,7 @@ class Call:
|
||||||
text: The text to be sent, possibly formatted in the weird undescribed markup that I'm using."""
|
text: The text to be sent, possibly formatted in the weird undescribed markup that I'm using."""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
async def net_request(self, message, destination: str) -> Reply:
|
async def net_request(self, message, destination: str) -> dict:
|
||||||
"""Send data through a :py:class:`royalnet.network.RoyalnetLink` and wait for a :py:class:`royalnet.network.Reply`.
|
"""Send data through a :py:class:`royalnet.network.RoyalnetLink` and wait for a :py:class:`royalnet.network.Reply`.
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
|
|
57
tests/test_network.py
Normal file
57
tests/test_network.py
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
import pytest
|
||||||
|
import uuid
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
from royalnet.network import Package, RoyalnetLink, RoyalnetServer, ConnectionClosedError, Request
|
||||||
|
|
||||||
|
|
||||||
|
log = logging.root
|
||||||
|
stream_handler = logging.StreamHandler()
|
||||||
|
stream_handler.formatter = logging.Formatter("{asctime}\t{name}\t{levelname}\t{message}", style="{")
|
||||||
|
log.addHandler(stream_handler)
|
||||||
|
log.setLevel(logging.WARNING)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def async_loop():
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
yield loop
|
||||||
|
loop.close()
|
||||||
|
|
||||||
|
|
||||||
|
async def echo_request_handler(message):
|
||||||
|
return message
|
||||||
|
|
||||||
|
|
||||||
|
def test_package_serialization():
|
||||||
|
pkg = Package({"ciao": "ciao"},
|
||||||
|
source=str(uuid.uuid4()),
|
||||||
|
destination=str(uuid.uuid4()),
|
||||||
|
source_conv_id=str(uuid.uuid4()),
|
||||||
|
destination_conv_id=str(uuid.uuid4()))
|
||||||
|
assert pkg == Package.from_dict(pkg.to_dict())
|
||||||
|
assert pkg == Package.from_json_string(pkg.to_json_string())
|
||||||
|
assert pkg == Package.from_json_bytes(pkg.to_json_bytes())
|
||||||
|
|
||||||
|
|
||||||
|
def test_request_creation():
|
||||||
|
request = Request("pytest", {"testing": "is fun", "bugs": "are less fun"})
|
||||||
|
assert request == Request.from_dict(request.to_dict())
|
||||||
|
|
||||||
|
|
||||||
|
def test_links(async_loop: asyncio.AbstractEventLoop):
|
||||||
|
address, port = "127.0.0.1", 1235
|
||||||
|
master = RoyalnetServer(address, port, "test")
|
||||||
|
async_loop.run_until_complete(master.start())
|
||||||
|
# Test invalid secret
|
||||||
|
wrong_secret_link = RoyalnetLink("ws://127.0.0.1:1235", "invalid", "test", echo_request_handler, loop=async_loop)
|
||||||
|
with pytest.raises(ConnectionClosedError):
|
||||||
|
async_loop.run_until_complete(wrong_secret_link.run())
|
||||||
|
# Test regular connection
|
||||||
|
link1 = RoyalnetLink("ws://127.0.0.1:1235", "test", "one", echo_request_handler, loop=async_loop)
|
||||||
|
async_loop.create_task(link1.run())
|
||||||
|
link2 = RoyalnetLink("ws://127.0.0.1:1235", "test", "two", echo_request_handler, loop=async_loop)
|
||||||
|
async_loop.create_task(link2.run())
|
||||||
|
message = {"ciao": "ciao"}
|
||||||
|
response = async_loop.run_until_complete(link1.request(message, "two"))
|
||||||
|
assert message == response
|
Loading…
Reference in a new issue