diff --git a/docs/doctrees/apireference.doctree b/docs/doctrees/apireference.doctree
index f14ba02f..46fb5e7e 100644
Binary files a/docs/doctrees/apireference.doctree and b/docs/doctrees/apireference.doctree differ
diff --git a/docs/doctrees/creatingacommand.doctree b/docs/doctrees/creatingacommand.doctree
index 5798cb98..136be5ed 100644
Binary files a/docs/doctrees/creatingacommand.doctree and b/docs/doctrees/creatingacommand.doctree differ
diff --git a/docs/doctrees/environment.pickle b/docs/doctrees/environment.pickle
index e09f62b1..11a44a23 100644
Binary files a/docs/doctrees/environment.pickle and b/docs/doctrees/environment.pickle differ
diff --git a/docs/doctrees/index.doctree b/docs/doctrees/index.doctree
index 54662b5a..87e2c3a7 100644
Binary files a/docs/doctrees/index.doctree and b/docs/doctrees/index.doctree differ
diff --git a/docs/doctrees/runningroyalnet.doctree b/docs/doctrees/runningroyalnet.doctree
index b5402576..c2722e31 100644
Binary files a/docs/doctrees/runningroyalnet.doctree and b/docs/doctrees/runningroyalnet.doctree differ
diff --git a/docs/html/_sources/creatingacommand.rst.txt b/docs/html/_sources/creatingacommand.rst.txt
index 248a6068..037557cd 100644
--- a/docs/html/_sources/creatingacommand.rst.txt
+++ b/docs/html/_sources/creatingacommand.rst.txt
@@ -137,7 +137,7 @@ If you want the full argument string, you can use the :py:meth:`CommandArgs.join
args.joined()
# "carbonara al-dente"
-You can specify a minimum number of arguments too, so that an :py:exc:`royalnet.error.InvalidInputError` will be
+You can specify a minimum number of arguments too, so that an :py:exc:`InvalidInputError` will be
raised if not enough arguments are present: ::
args.joined(require_at_least=3)
@@ -149,7 +149,7 @@ Regular expressions
For more complex commands, you may want to get arguments through `regular expressions A list of possible aliases for a command.
+To have A small description of the command, to be displayed when the command is being autocompleted. The syntax of the command, to be displayed when a The syntax of the command, to be displayed when a Delete the invoking message, if supported by the interface. The invoking message is the message send by the user that contains the command. error_if_unavailable – if True, raise NotImplementedError() if the message cannot been deleted error_if_unavailable – if True, raise an exception if the message cannot been deleted.convert_to_pcm
() → None¶
-
-
-
create_and_ready_from_url
(url, **ytdl_args) → List[royalnet.audio.ytdldiscord.YtdlDiscord]¶
create_from_url
(url, **ytdl_args) → List[royalnet.audio.ytdldiscord.YtdlDiscord]¶
run_blocking
()¶run_blocking
(verbose=False)¶
@@ -657,6 +652,13 @@ find the chain that links the
class
royalnet.commands.
Command
(interface: royalnet.commands.commandinterface.CommandInterface)¶
+
+
+aliases
= NotImplemented¶/e
as alias for /example
, one should set aliases to ["e"]
.
@@ -694,12 +696,12 @@ in the format description
= NotImplemented¶/example
syntax
= ''¶royalnet.error.InvalidInputError
is raised,
+royalnet.error.InvalidInputError
is raised,
in the format (required_arg) [optional_arg]
.(requ
class
royalnet.commands.
CommandData
¶
@@ -711,10 +713,7 @@ in the format delete_invoking
(error_if_unavailable=False)¶delete_invoking
(error_if_unavailable=False) → None¶
(requ
That probably means, the database row identifying the user.
error_if_none – Raise a royalnet.error.UnregisteredError
if this is True and the call has no author.
royalnet.error.UnregisteredError –
+error_if_none – Raise an exception if this is True and the call has no author.
Arguments can be accessed with an array notation, such as args[0]
.
royalnet.error.InvalidInputError – if the requested argument does not exist.
+royalnet.error.InvalidInputError – if the requested argument does not exist.
Get the arguments as a space-joined string.
require_at_least – the minimum amount of arguments required, will raise royalnet.error.InvalidInputError
if the requirement is not fullfilled.
require_at_least – the minimum amount of arguments required, will raise royalnet.error.InvalidInputError
if the requirement is not fullfilled.
royalnet.error.InvalidInputError – if there are less than require_at_least
arguments.
royalnet.error.InvalidInputError – if there are less than require_at_least
arguments.
The space-joined string.
@@ -780,7 +779,7 @@ That probably means, the database row identifying the user.pattern – The regex pattern to be passed to re.match()
.
royalnet.error.InvalidInputError – if the pattern doesn’t match.
+royalnet.error.InvalidInputError – if the pattern doesn’t match.
The matched groups, as returned by re.Match.groups()
.
royalnet.commands.
CommandError
(message='')¶Something went wrong during the execution of this command.
+Display an error message to the user, explaining what went wrong.
+royalnet.commands.
InvalidInputError
(message='')¶The command has received invalid input and cannot complete.
+Display an error message to the user, along with the correct syntax for the command.
+royalnet.commands.
UnsupportedError
(message='')¶A requested feature is not available on this interface.
+Display an error message to the user, telling them to use another interface.
+royalnet.commands.
KeyboardExpiredError
(message='')¶A special type of exception that can be raised in keyboard handlers to mark a specific keyboard as expired.
+Executed every time a package is received and must be routed somewhere.
run
()¶royalnet.error.
CurrentlyDisabledError
¶This feature is temporarely disabled and is not available right now.
-royalnet.error.
ExternalError
¶Something went wrong in a non-Royalnet component and the command execution cannot be completed.
-royalnet.error.
FileTooBigError
¶The file to be downloaded would be too big to store; therefore, it has been skipped.
-royalnet.error.
InvalidConfigError
¶The bot has not been configured correctly, therefore the command can not function.
-royalnet.error.
InvalidInputError
¶The command has received invalid input and cannot complete.
-royalnet.error.
KeyboardExpiredError
¶A special type of exception that can be raised in keyboard handlers to mark a specific keyboard as expired.
-royalnet.error.
NoneFoundError
¶The element that was being looked for was not found.
-royalnet.error.
RoyalnetRequestError
(error: ResponseError)¶The royalnet.network.Response
that was received is invalid.
royalnet.error.
TooManyFoundError
¶Multiple elements matching the request were found, and only one was expected.
-royalnet.error.
UnregisteredError
¶The command required a registered user, and the user was not registered.
-royalnet.error.
UnsupportedError
¶The command is not supported for the specified interface.
-You can consider arguments as if they were separated by spaces.
You can then access command arguments directly by number as if the args object was a list of str
.
If you request an argument with a certain number, but the argument does not exist, an
-royalnet.error.InvalidInputError
is raised, making the arguments accessed in this way required.
royalnet.error.InvalidInputError
is raised, making the arguments accessed in this way required.
args[0]
# "carbonara"
@@ -295,7 +296,7 @@ will return # "carbonara al-dente"
You can specify a minimum number of arguments too, so that an royalnet.error.InvalidInputError
will be
+
You can specify a minimum number of arguments too, so that an InvalidInputError
will be
raised if not enough arguments are present:
args.joined(require_at_least=3)
# InvalidInputError() is raised
@@ -306,7 +307,7 @@ raised if not enough arguments are present:
Regular expressions¶
For more complex commands, you may want to get arguments through regular expressions.
You can then use the CommandArgs.match()
method, which tries to match a pattern to the command argument string,
-which returns a tuple of the matched groups and raises an royalnet.error.InvalidInputError
if there is no match.
+which returns a tuple of the matched groups and raises an InvalidInputError
if there is no match.
To match a pattern, re.match()
is used, meaning that Python will try to match only at the beginning of the string.
args.match(r"(carb\w+)")
# ("carbonara",)
@@ -323,6 +324,25 @@ which returns a tuple of the matched groups and raises an
+Raising errors¶
+If you want to display an error message to the user, you can raise a CommandError
using the error message as argument:
+if not kitchen.is_open():
+ raise CommandError("The kitchen is closed. Come back later!")
+
+
+You can also manually raise InvalidInputError
to redisplay the command syntax, along with your error message:
+if args[0] not in allowed_pasta:
+ raise InvalidInputError("The specified pasta type is invalid.")
+
+
+If you need a Royalnet feature that’s not available on the current interface, you can raise an
+UnsupportedError
with a brief description of what’s missing:
+if interface.name != "telegram":
+ raise UnsupportedError("This command can only be run on Telegram interfaces.")
+
+
+
Running code at the initialization of the bot¶
You can run code while the bot is starting by overriding the Command.__init__()
function.
diff --git a/docs/html/genindex.html b/docs/html/genindex.html
index 835626ca..8574095f 100644
--- a/docs/html/genindex.html
+++ b/docs/html/genindex.html
@@ -155,7 +155,6 @@
| B
| C
| D
- | E
| F
| G
| H
@@ -260,10 +259,12 @@
advance_music_data() (royalnet.bots.DiscordBot method)
Alchemy (class in royalnet.database)
+
+ alchemy (royalnet.commands.CommandInterface attribute)
- - alchemy (royalnet.commands.CommandInterface attribute)
+
- aliases (royalnet.commands.Command attribute)
- andformat() (in module royalnet.utils)
@@ -292,25 +293,23 @@
- CommandArgs (class in royalnet.commands)
- CommandData (class in royalnet.commands)
+
+ - CommandError
- CommandInterface (class in royalnet.commands)
- connect() (royalnet.network.NetworkLink method)
-
- - ConnectionClosedError
+ - ConnectionClosedError
+
- convert_to_mp3() (royalnet.audio.YtdlMp3 method)
- convert_to_pcm() (royalnet.audio.YtdlDiscord method)
- - create_and_ready_from_url() (royalnet.audio.YtdlDiscord class method)
-
-
- create_app() (in module royalnet.web)
- create_from_url() (royalnet.audio.YtdlDiscord class method)
@@ -319,8 +318,6 @@
- (royalnet.audio.YtdlMp3 class method)
- CurrentlyDisabledError
-
@@ -354,22 +351,12 @@
-E
-
-
- - ExternalError
-
-
-
-
F
- - InvalidConfigError
-
- - InvalidInputError
+
- InvalidInputError
- is_downloaded() (royalnet.audio.YtdlFile method)
@@ -461,7 +446,7 @@
@@ -504,16 +489,14 @@
NetworkConfig (class in royalnet.network)
NetworkError
-
- NetworkHandler (class in royalnet.utils)
+ - NetworkHandler (class in royalnet.utils)
+
- NetworkLink (class in royalnet.network)
- NetworkServer (class in royalnet.network)
-
- - NoneFoundError
- NotConnectedError
@@ -643,8 +626,6 @@
- (royalnet.commands.Command method)
- (royalnet.network.NetworkLink method)
-
- - (royalnet.network.NetworkServer method)
run_blocking() (royalnet.bots.GenericBot method)
@@ -708,8 +689,6 @@
to_json_bytes() (royalnet.network.Package method)
to_json_string() (royalnet.network.Package method)
-
- TooManyFoundError
@@ -720,12 +699,10 @@
unregister_keyboard_key() (royalnet.commands.CommandInterface method)
unregister_net_handler() (royalnet.commands.CommandInterface method)
-
- UnregisteredError
- - UnsupportedError
+
- UnsupportedError
- update_activity_with_source_title() (royalnet.bots.DiscordBot method)
diff --git a/docs/html/index.html b/docs/html/index.html
index 3a5b897b..b5c0ea49 100644
--- a/docs/html/index.html
+++ b/docs/html/index.html
@@ -153,7 +153,11 @@
Welcome to the documentation of Royalnet!
-- Running Royalnet
+- Running Royalnet
+
- Royalnet Commands
- Creating a new Command
- Command arguments
@@ -163,6 +167,7 @@
- Regular expressions
+- Raising errors
- Running code at the initialization of the bot
- Coroutines and slow operations
- Delete the invoking message
diff --git a/docs/html/objects.inv b/docs/html/objects.inv
index a7819aa5..f9a3aae8 100644
Binary files a/docs/html/objects.inv and b/docs/html/objects.inv differ
diff --git a/docs/html/runningroyalnet.html b/docs/html/runningroyalnet.html
index f822c635..35718563 100644
--- a/docs/html/runningroyalnet.html
+++ b/docs/html/runningroyalnet.html
@@ -82,7 +82,11 @@
@@ -151,7 +155,56 @@
Running Royalnet¶
-This documentation page hasn’t been written yet, please refer to the README until then.
+To run a royalnet
instance, you have first to download the package from pip
:
+
+The Keyring¶
+pip install royalnet
+
+
+To run royalnet
, you’ll have to setup the system keyring.
+On Windows and desktop Linux, this is already configured;
+on a headless Linux instance, you’ll need to manually start and unlock the keyring daemon.
+Now you have to create a new royalnet
configuration. Start the configuration wizard:
+python -m royalnet.configurator
+
+
+You’ll be prompted to enter a “secrets name”: this is the name of the group of API keys that will be associated with
+your bot. Enter a name that you’ll be able to remember.
+Desired secrets name [__default__]: royalgames
+
+
+You’ll then be asked for a network password.
+This password is used to connect to the rest of the royalnet.network
, or, if you’re hosting a local Network,
+it will be the necessary password to connect to it:
+Network password []: cosafaunapesuunafoglia
+
+
+Then you’ll be asked for a Telegram Bot API token.
+You can get one from @BotFather.
+Telegram Bot API token []: 000000000:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+
+
+The next prompt will ask for a Discord Bot API token.
+You can get one at the Discord Developers Portal.
+Discord Bot API token []: AAAAAAAAAAAAAAAAAAAAAAAA.AAAAAA.AAAAAAAAAAAAAAAAAAAAAAAAAAA
+
+
+Now the configurator will ask you for a Imgur API token.
+Register an application on Imgur to be supplied one.
+The token should be of type “anonymous usage without user authorization”.
+Imgur API token []: aaaaaaaaaaaaaaa
+
+
+Next, you’ll be asked for a Sentry DSN. You probably won’t have one, so just ignore it and press enter.
+Sentry DSN []:
+
+
+Now that all tokens are configured, you’re ready to launch the bot!
+
+
+Running the bots¶
+TODO
+
diff --git a/docs/html/searchindex.js b/docs/html/searchindex.js
index f0039614..64d68fae 100644
--- a/docs/html/searchindex.js
+++ b/docs/html/searchindex.js
@@ -1 +1 @@
-Search.setIndex({docnames:["apireference","creatingacommand","index","runningroyalnet"],envversion:{"sphinx.domains.c":1,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":1,"sphinx.domains.javascript":1,"sphinx.domains.math":2,"sphinx.domains.python":1,"sphinx.domains.rst":1,"sphinx.domains.std":1,"sphinx.ext.intersphinx":1,sphinx:56},filenames:["apireference.rst","creatingacommand.rst","index.rst","runningroyalnet.rst"],objects:{"royalnet.audio":{FileAudioSource:[0,1,1,""],YtdlDiscord:[0,1,1,""],YtdlFile:[0,1,1,""],YtdlInfo:[0,1,1,""],YtdlMp3:[0,1,1,""]},"royalnet.audio.FileAudioSource":{is_opus:[0,2,1,""],read:[0,2,1,""]},"royalnet.audio.YtdlDiscord":{"delete":[0,2,1,""],convert_to_pcm:[0,2,1,""],create_and_ready_from_url:[0,2,1,""],create_from_url:[0,2,1,""],info:[0,2,1,""],pcm_available:[0,2,1,""],ready_up:[0,2,1,""],spawn_audiosource:[0,2,1,""]},"royalnet.audio.YtdlFile":{"delete":[0,2,1,""],_default_ytdl_args:[0,3,1,""],download_file:[0,2,1,""],download_from_url:[0,2,1,""],has_info:[0,2,1,""],is_downloaded:[0,2,1,""],open:[0,2,1,""],update_info:[0,2,1,""]},"royalnet.audio.YtdlInfo":{__init__:[0,2,1,""],_default_ytdl_args:[0,3,1,""],retrieve_for_url:[0,2,1,""],to_discord_embed:[0,2,1,""]},"royalnet.audio.YtdlMp3":{"delete":[0,2,1,""],convert_to_mp3:[0,2,1,""],create_and_ready_from_url:[0,2,1,""],create_from_url:[0,2,1,""],info:[0,2,1,""],pcm_available:[0,2,1,""],ready_up:[0,2,1,""]},"royalnet.bots":{DiscordBot:[0,1,1,""],GenericBot:[0,1,1,""],TelegramBot:[0,1,1,""]},"royalnet.bots.DiscordBot":{_bot_factory:[0,2,1,""],_data_factory:[0,2,1,""],_init_client:[0,2,1,""],_init_voice:[0,2,1,""],_initialize:[0,2,1,""],_interface_factory:[0,2,1,""],add_to_music_data:[0,2,1,""],advance_music_data:[0,2,1,""],interface_name:[0,3,1,""],run:[0,2,1,""],update_activity_with_source_title:[0,2,1,""]},"royalnet.bots.GenericBot":{_data_factory:[0,2,1,""],_init_commands:[0,2,1,""],_init_database:[0,2,1,""],_init_loop:[0,2,1,""],_init_network:[0,2,1,""],_init_sentry:[0,2,1,""],_initialize:[0,2,1,""],_interface_factory:[0,2,1,""],_network_handler:[0,2,1,""],get_secret:[0,2,1,""],interface_name:[0,3,1,""],run:[0,2,1,""],run_blocking:[0,2,1,""],set_secret:[0,2,1,""]},"royalnet.bots.TelegramBot":{_data_factory:[0,2,1,""],_handle_callback_query:[0,2,1,""],_handle_message:[0,2,1,""],_handle_update:[0,2,1,""],_init_client:[0,2,1,""],_initialize:[0,2,1,""],_interface_factory:[0,2,1,""],interface_name:[0,3,1,""],run:[0,2,1,""],safe_api_call:[0,2,1,""]},"royalnet.commands":{Command:[0,1,1,""],CommandArgs:[0,1,1,""],CommandData:[0,1,1,""],CommandInterface:[0,1,1,""]},"royalnet.commands.Command":{description:[0,3,1,""],name:[0,3,1,""],require_alchemy_tables:[0,3,1,""],run:[0,2,1,""],syntax:[0,3,1,""]},"royalnet.commands.CommandArgs":{__getitem__:[0,2,1,""],joined:[0,2,1,""],match:[0,2,1,""],optional:[0,2,1,""]},"royalnet.commands.CommandData":{delete_invoking:[0,2,1,""],get_author:[0,2,1,""],keyboard:[0,2,1,""],reply:[0,2,1,""]},"royalnet.commands.CommandInterface":{alchemy:[0,3,1,""],bot:[0,3,1,""],loop:[0,3,1,""],name:[0,3,1,""],net_request:[0,2,1,""],prefix:[0,3,1,""],register_keyboard_key:[0,2,1,""],register_net_handler:[0,2,1,""],unregister_keyboard_key:[0,2,1,""],unregister_net_handler:[0,2,1,""]},"royalnet.database":{Alchemy:[0,1,1,""],DatabaseConfig:[0,1,1,""],relationshiplinkchain:[0,4,1,""]},"royalnet.database.Alchemy":{__init__:[0,2,1,""],_create_tables:[0,2,1,""],session_acm:[0,2,1,""],session_cm:[0,2,1,""]},"royalnet.error":{CurrentlyDisabledError:[0,5,1,""],ExternalError:[0,5,1,""],FileTooBigError:[0,5,1,""],InvalidConfigError:[0,5,1,""],InvalidInputError:[0,5,1,""],KeyboardExpiredError:[0,5,1,""],NoneFoundError:[0,5,1,""],RoyalnetRequestError:[0,5,1,""],RoyalnetResponseError:[0,5,1,""],TooManyFoundError:[0,5,1,""],UnregisteredError:[0,5,1,""],UnsupportedError:[0,5,1,""]},"royalnet.error.RoyalnetRequestError":{args:[0,2,1,""]},"royalnet.network":{ConnectionClosedError:[0,5,1,""],NetworkConfig:[0,1,1,""],NetworkError:[0,5,1,""],NetworkLink:[0,1,1,""],NetworkServer:[0,1,1,""],NotConnectedError:[0,5,1,""],NotIdentifiedError:[0,5,1,""],Package:[0,1,1,""],Request:[0,1,1,""],Response:[0,1,1,""],ResponseError:[0,1,1,""],ResponseSuccess:[0,1,1,""]},"royalnet.network.NetworkLink":{connect:[0,2,1,""],identify:[0,2,1,""],receive:[0,2,1,""],request:[0,2,1,""],run:[0,2,1,""],send:[0,2,1,""]},"royalnet.network.NetworkServer":{find_client:[0,2,1,""],find_destination:[0,2,1,""],listener:[0,2,1,""],route_package:[0,2,1,""],run:[0,2,1,""],run_blocking:[0,2,1,""],serve:[0,2,1,""]},"royalnet.network.Package":{__init__:[0,2,1,""],from_dict:[0,2,1,""],from_json_bytes:[0,2,1,""],from_json_string:[0,2,1,""],reply:[0,2,1,""],to_dict:[0,2,1,""],to_json_bytes:[0,2,1,""],to_json_string:[0,2,1,""]},"royalnet.network.Request":{from_dict:[0,2,1,""],to_dict:[0,2,1,""]},"royalnet.network.Response":{from_dict:[0,2,1,""],raise_on_error:[0,2,1,""],to_dict:[0,2,1,""]},"royalnet.network.ResponseError":{raise_on_error:[0,2,1,""]},"royalnet.network.ResponseSuccess":{raise_on_error:[0,2,1,""]},"royalnet.utils":{NetworkHandler:[0,1,1,""],andformat:[0,4,1,""],asyncify:[0,4,1,""],cdj:[0,4,1,""],discord_escape:[0,4,1,""],fileformat:[0,4,1,""],numberemojiformat:[0,4,1,""],ordinalformat:[0,4,1,""],parse_5etools_entry:[0,4,1,""],plusformat:[0,4,1,""],safeformat:[0,4,1,""],sleep_until:[0,4,1,""],splitstring:[0,4,1,""],telegram_escape:[0,4,1,""],ytdldateformat:[0,4,1,""]},"royalnet.utils.NetworkHandler":{message_type:[0,3,1,""]},"royalnet.web":{Royalprint:[0,1,1,""],create_app:[0,4,1,""]},royalnet:{audio:[0,0,0,"-"],bots:[0,0,0,"-"],commands:[0,0,0,"-"],database:[0,0,0,"-"],error:[0,0,0,"-"],network:[0,0,0,"-"],utils:[0,0,0,"-"],web:[0,0,0,"-"]}},objnames:{"0":["py","module","Python module"],"1":["py","class","Python class"],"2":["py","method","Python method"],"3":["py","attribute","Python attribute"],"4":["py","function","Python function"],"5":["py","exception","Python exception"]},objtypes:{"0":"py:module","1":"py:class","2":"py:method","3":"py:attribute","4":"py:function","5":"py:exception"},terms:{"20m":0,"byte":0,"class":[0,1],"default":[0,1],"final":0,"function":[0,1],"import":1,"int":0,"new":[0,2],"null":0,"return":[0,1],"short":1,"static":[0,1],"super":1,"true":[0,1],"try":[0,1],"while":[0,1],Adding:2,And:1,For:1,Not:1,That:0,The:[0,1],Then:[0,1],These:[0,1],Use:[0,1],Using:2,__default__:0,__dict__:0,__doc__:0,__getitem__:0,__init__:[0,1],__module__:0,__slots__:0,__weakref__:0,_bot_factori:0,_create_t:0,_data_factori:0,_default_ytdl_arg:0,_handle_callback_queri:0,_handle_messag:0,_handle_upd:0,_init_cli:0,_init_command:0,_init_databas:0,_init_loop:0,_init_network:0,_init_sentri:0,_init_voic:0,_initi:0,_interface_factori:0,_network_handl:0,abl:0,about:[0,1],abstracteventloop:0,access:[0,2],accur:0,add:[0,1],add_to_music_data:0,added:[0,1],adding:[0,1],addit:0,addition:1,address:0,advance_music_data:0,after:1,akin:0,alchemi:[0,2],all:[0,1],allow:[0,1],alreadi:1,also:0,altern:0,alwai:[0,1],amount:0,andformat:0,ani:[0,1],anoth:0,anymor:0,anystr:0,api:2,app:0,append:1,applic:0,arg:[0,1],argument:[0,2],around:0,arrai:0,ascend:1,async:[0,1],asyncifi:[0,1],asyncio:[0,1],asyncron:0,attempt:0,attribut:[0,1],audio:2,audiosourc:0,author:0,autocomplet:0,automat:0,avail:[0,1],avoid:1,await:1,banana:1,base:0,becaus:1,been:[0,3],befor:1,begin:1,being:[0,1],between:[0,1],big:0,block:0,blockingli:0,blueprint:0,bool:0,bot:2,both:0,bracket:1,brief:1,bufferediobas:0,bug:1,call:[0,1],callabl:0,callback:0,can:[0,1],cancel:0,cannot:0,carb:1,carbonara:1,caus:1,caution:0,cdj:0,certain:1,chain:0,chang:[0,1],change_pres:0,channel:0,charact:0,chat:[0,1],check:0,class_:0,classmethod:0,client:0,close:0,clue:0,code:[0,2],column:1,command:2,commandarg:[0,1],commanddata:[0,1],commandinterfac:0,commun:0,comparis:1,complet:0,complex:1,compon:0,comun:2,config:0,config_obj:0,configur:0,connect:[0,1],connectedcli:0,connectionclosederror:0,consid:1,consum:0,contain:[0,1],context:0,convers:0,convert:0,convert_to_mp3:0,convert_to_pcm:0,core:0,coroutin:[0,2],correctli:0,correspond:0,could:1,creat:[0,2],create_and_ready_from_url:0,create_app:0,create_from_url:0,current:0,currentlydisablederror:0,custom:0,dai:0,data:[0,1],databas:2,database_config:0,database_uri:0,databaseconfig:0,date:0,datetim:0,db_path:0,declar:0,def:1,delet:[0,2],delete_invok:[0,1],dent:1,desc:1,descend:1,describ:0,descript:[0,1],destin:0,destination_conv_id:0,detail:0,dfile:0,dict:0,dictionari:0,differ:1,direct:2,directli:1,disabl:0,discord:0,discord_escap:0,discordbot:0,discordcli:0,displai:0,doc:0,docstr:0,document:[1,2,3],doe:[0,1],doesn:0,don:[0,1],done:1,download:0,download_1_terabyte_of_spaghetti:1,download_fil:0,download_from_url:0,dynam:0,eas:0,edit:0,effect:1,either:0,element:[0,1],els:1,emb:0,emoji:1,empti:0,encod:0,end:0,ending_class:0,engin:[0,1],enough:1,ensur:0,entri:0,envvar:0,epoch:0,error:[1,2],error_data:0,error_if_non:0,error_if_unavail:[0,1],escap:0,establish:1,even:0,event:0,everi:[0,1],everyth:0,exact:1,exampl:[0,1],except:[0,1],execut:[0,1],exist:[0,1],expect:[0,1],expir:0,express:2,ext:0,extern:1,externalerror:0,extra_info:0,extract:0,extract_info:0,fals:0,featur:0,fetch:[0,2],field:1,file:[0,1],fileaudiosourc:0,fileformat:0,filenam:0,filetoobigerror:0,filter:2,find:0,find_client:0,find_destin:0,finish:1,first:1,flag:0,flask:0,follow:0,format:0,found:[0,1],from:[0,1],from_dict:0,from_json_byt:0,from_json_str:0,from_object:0,full:2,fullfil:0,gener:0,genericbot:0,get:[0,1],get_author:0,get_secret:0,github:2,greater:[0,1],group:[0,1],guild:0,handl:0,handler:0,has:[0,1],has_info:0,hasn:3,have:[0,1],here:1,how:0,html:0,http:0,identifi:0,identity_column_nam:0,identity_t:0,ifi:0,ignor:[0,1],ignoreerror:0,imag:1,import_nam:0,incom:0,incomplet:0,index:[0,2],inf:0,info:0,inform:0,inherit:[0,1],initi:[0,2],input:0,insid:1,instanc:[0,1],instead:[0,1],interact:1,interfac:[0,1],interface_nam:0,invalid:0,invalidconfigerror:0,invalidinputerror:[0,1],invok:[0,2],is_download:0,is_opu:0,isn:[0,1],itali:1,item:0,join:[0,1],json:0,jsonabl:0,just:[0,1],keep:1,kei:0,key_nam:0,keyboard:0,keyboardexpirederror:0,keyword:1,kind:1,kwarg:0,last:0,less:0,like:[0,1],link:0,link_typ:0,list:[0,1],listen:0,login:0,look:[0,1],loop:0,made:0,mai:1,main:0,mainli:0,maintain:0,make:[0,1],manag:0,mark:0,markup:0,master_secret:0,master_t:0,master_uri:0,match:[0,1],max:0,mean:[0,1],meantim:1,member:1,memori:0,mention:1,messag:[0,2],message_typ:0,metadata:1,method:[0,1],middl:0,might:0,minimum:[0,1],miscellan:0,miss:0,month:0,more:[0,2],multipl:[0,1],music_data:0,must:0,name:[0,1],need:[0,1],net_request:0,network:2,network_config:0,network_handl:0,networkconfig:0,networkerror:0,networkhandl:0,networklink:0,networkserv:0,next:[0,1],nid:0,no_warn:0,nobodi:0,node:0,non:0,none:[0,1],nonefounderror:0,noplaylist:0,normal:1,notat:0,notconnectederror:0,noth:[0,1],notic:1,notidentifiederror:0,notimpl:0,notimplementederror:0,now:[0,1],number:[0,1],numberemojiformat:0,object:[0,1],offset:0,onc:[0,1],one:[0,1],one_or_non:1,ones:[0,1],onli:[0,1],open:0,oper:2,option:[0,2],optional_arg:0,opu:0,order:2,order_bi:1,ordinalformat:0,org:0,orm:1,other:[0,1],otherwis:[0,1],outdat:0,output:0,outtmpl:0,overrid:1,packag:0,packet:0,page:[0,3],paramet:[0,1],parenthes:1,parse_5etools_entri:0,part:1,pass:[0,1],password:0,pasta:1,path:0,pattern:[0,1],pcm:0,pcm_avail:0,pickl:0,ping:1,pingcommand:1,plai:[0,1],pleas:3,plusformat:0,pong:1,port:0,possibl:[0,1],postgresql:1,prefix:0,prepar:0,prepend:0,presenc:0,present:1,press:0,previou:1,previous:1,probabl:0,properli:1,properti:0,python:1,queri:2,quiet:0,rais:[0,1],raise_on_error:0,read:[0,1],readi:1,readm:3,ready_up:0,real:0,realli:0,receiv:[0,1],reciev:0,recogn:1,recreat:0,refer:[1,2,3],regex:0,regist:0,register_keyboard_kei:0,register_net_handl:0,regular:2,relat:0,relationshiplinkchain:0,rememb:1,remov:0,replac:0,repli:[0,1],request:[0,1],request_dict:0,request_handl:0,requested_pasta:1,requestedpasta:1,requir:[0,1],require_alchemy_t:[0,1],require_at_least:[0,1],required_arg:0,required_secret:0,required_t:0,requrire_alchemy_t:1,respect:1,respons:0,responseerror:0,responsesuccess:0,rest:1,restart:1,result:[0,2],retriev:0,retrieve_for_url:0,right:0,right_now:1,role:1,root_path:0,round:1,rout:0,route_packag:0,row:[0,1],royal:1,royalcod:0,royalnet:0,royalnetlink:0,royalnetrequesterror:0,royalnetresponseerror:0,royalprint:0,royalt:1,run:[0,2],run_block:0,safe_api_cal:0,safeformat:0,same:1,script:1,second:0,secret:0,secret_kei:0,secrets_nam:0,section:1,select:0,self:[0,1],send:[0,1],sent:[0,1],sentry_dsn:0,separ:[0,1],sequenc:0,serv:0,server:0,session:1,session_acm:0,session_cm:0,set:[0,1],set_secret:0,share:1,should:[0,1],side:1,signal:0,simpl:1,singl:[0,1],skip:0,sleep:1,sleep_until:0,slow:2,small:[0,1],some:1,someth:0,somewher:0,song:0,soon:1,sort:1,sourc:0,source_conv_id:0,space:[0,1],spaghetti:1,spaghetticommand:1,spawn_audiosourc:0,special:[0,1],specif:[0,1],specifi:[0,1],splitstr:0,sqlalchemi:[0,1],squar:1,stai:1,start:[0,1],starting_class:0,statement:0,static_fold:0,static_url_path:0,statu:0,stop:1,store:[0,1],str:[0,1],stream:0,string:[0,2],subdomain:0,submodul:0,success:0,support:[0,1],syntax:[0,1],tabl:[0,1],take:1,task:[0,1],telegram:[0,1],telegram_escap:0,telegrambot:0,tell:1,template_fold:0,temporar:0,text:0,than:[0,1],thei:[0,1],them:[0,1],therefor:0,thi:[0,1,3],thing:1,think:1,thought:1,through:[0,1],time:[0,1],titl:0,to_dict:0,to_discord_emb:0,to_json_byt:0,to_json_str:0,too:[0,1],toomanyfounderror:0,tri:1,tupl:[0,1],two:0,type:[0,1],underscor:0,undescrib:0,unexpect:0,unexpectedli:0,union:[0,1],unregister_keyboard_kei:0,unregister_net_handl:0,unregisterederror:0,unsupportederror:[0,1],until:[0,1,3],updat:0,update_activity_with_source_titl:0,update_info:0,uri:0,url:0,url_default:0,url_prefix:0,use:[0,1],used:[0,1],useful:[0,1],user:[0,1],usernam:[0,1],uses:0,using:[0,1],usual:0,utf8:0,util:[1,2],uuid:0,valid:0,valu:0,variabl:0,variou:0,via:2,video:0,voic:0,wai:[0,1],wait:0,want:[0,1],wasn:1,web:2,websit:1,websocket:0,websocketserverprotocol:0,weird:0,welcom:2,went:0,were:[0,1],what:1,when:[0,1],whenev:1,where:0,which:[0,1],why:0,without:0,won:1,word:0,work:[0,1],worth:0,would:0,wrap:1,wrapper:0,written:[0,3],wrong:0,year:0,yet:[0,3],you:[0,1],your:1,youtub:0,youtube_dl:0,youtubedl:0,ytdl_arg:0,ytdl_file:0,ytdldateformat:0,ytdldiscord:0,ytdlfile:0,ytdlinfo:0,ytdlmp3:0,yyyi:0,yyyymmdd:0},titles:["API Reference","Royalnet Commands","royalnet","Running Royalnet"],titleterms:{"new":1,Adding:1,Using:1,access:1,alchemi:1,api:0,argument:1,audio:0,bot:[0,1],code:1,command:[0,1],comun:1,coroutin:1,creat:1,databas:[0,1],delet:1,direct:1,error:0,express:1,fetch:1,filter:1,full:1,initi:1,invok:1,link:2,messag:1,more:1,network:0,oper:1,option:1,order:1,queri:1,refer:0,regular:1,result:1,royalnet:[1,2,3],run:[1,3],slow:1,some:2,string:1,useful:2,util:0,via:1,web:0}})
\ No newline at end of file
+Search.setIndex({docnames:["apireference","creatingacommand","index","runningroyalnet"],envversion:{"sphinx.domains.c":1,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":1,"sphinx.domains.javascript":1,"sphinx.domains.math":2,"sphinx.domains.python":1,"sphinx.domains.rst":1,"sphinx.domains.std":1,"sphinx.ext.intersphinx":1,sphinx:56},filenames:["apireference.rst","creatingacommand.rst","index.rst","runningroyalnet.rst"],objects:{"royalnet.audio":{FileAudioSource:[0,1,1,""],YtdlDiscord:[0,1,1,""],YtdlFile:[0,1,1,""],YtdlInfo:[0,1,1,""],YtdlMp3:[0,1,1,""]},"royalnet.audio.FileAudioSource":{is_opus:[0,2,1,""],read:[0,2,1,""]},"royalnet.audio.YtdlDiscord":{"delete":[0,2,1,""],convert_to_pcm:[0,2,1,""],create_from_url:[0,2,1,""],info:[0,2,1,""],pcm_available:[0,2,1,""],ready_up:[0,2,1,""],spawn_audiosource:[0,2,1,""]},"royalnet.audio.YtdlFile":{"delete":[0,2,1,""],_default_ytdl_args:[0,3,1,""],download_file:[0,2,1,""],download_from_url:[0,2,1,""],has_info:[0,2,1,""],is_downloaded:[0,2,1,""],open:[0,2,1,""],update_info:[0,2,1,""]},"royalnet.audio.YtdlInfo":{__init__:[0,2,1,""],_default_ytdl_args:[0,3,1,""],retrieve_for_url:[0,2,1,""],to_discord_embed:[0,2,1,""]},"royalnet.audio.YtdlMp3":{"delete":[0,2,1,""],convert_to_mp3:[0,2,1,""],create_and_ready_from_url:[0,2,1,""],create_from_url:[0,2,1,""],info:[0,2,1,""],pcm_available:[0,2,1,""],ready_up:[0,2,1,""]},"royalnet.bots":{DiscordBot:[0,1,1,""],GenericBot:[0,1,1,""],TelegramBot:[0,1,1,""]},"royalnet.bots.DiscordBot":{_bot_factory:[0,2,1,""],_data_factory:[0,2,1,""],_init_client:[0,2,1,""],_init_voice:[0,2,1,""],_initialize:[0,2,1,""],_interface_factory:[0,2,1,""],add_to_music_data:[0,2,1,""],advance_music_data:[0,2,1,""],interface_name:[0,3,1,""],run:[0,2,1,""],update_activity_with_source_title:[0,2,1,""]},"royalnet.bots.GenericBot":{_data_factory:[0,2,1,""],_init_commands:[0,2,1,""],_init_database:[0,2,1,""],_init_loop:[0,2,1,""],_init_network:[0,2,1,""],_init_sentry:[0,2,1,""],_initialize:[0,2,1,""],_interface_factory:[0,2,1,""],_network_handler:[0,2,1,""],get_secret:[0,2,1,""],interface_name:[0,3,1,""],run:[0,2,1,""],run_blocking:[0,2,1,""],set_secret:[0,2,1,""]},"royalnet.bots.TelegramBot":{_data_factory:[0,2,1,""],_handle_callback_query:[0,2,1,""],_handle_message:[0,2,1,""],_handle_update:[0,2,1,""],_init_client:[0,2,1,""],_initialize:[0,2,1,""],_interface_factory:[0,2,1,""],interface_name:[0,3,1,""],run:[0,2,1,""],safe_api_call:[0,2,1,""]},"royalnet.commands":{Command:[0,1,1,""],CommandArgs:[0,1,1,""],CommandData:[0,1,1,""],CommandError:[0,4,1,""],CommandInterface:[0,1,1,""],InvalidInputError:[0,4,1,""],KeyboardExpiredError:[0,4,1,""],UnsupportedError:[0,4,1,""]},"royalnet.commands.Command":{aliases:[0,3,1,""],description:[0,3,1,""],name:[0,3,1,""],require_alchemy_tables:[0,3,1,""],run:[0,2,1,""],syntax:[0,3,1,""]},"royalnet.commands.CommandArgs":{__getitem__:[0,2,1,""],joined:[0,2,1,""],match:[0,2,1,""],optional:[0,2,1,""]},"royalnet.commands.CommandData":{delete_invoking:[0,2,1,""],get_author:[0,2,1,""],keyboard:[0,2,1,""],reply:[0,2,1,""]},"royalnet.commands.CommandInterface":{alchemy:[0,3,1,""],bot:[0,3,1,""],loop:[0,3,1,""],name:[0,3,1,""],net_request:[0,2,1,""],prefix:[0,3,1,""],register_keyboard_key:[0,2,1,""],register_net_handler:[0,2,1,""],unregister_keyboard_key:[0,2,1,""],unregister_net_handler:[0,2,1,""]},"royalnet.database":{Alchemy:[0,1,1,""],DatabaseConfig:[0,1,1,""],relationshiplinkchain:[0,5,1,""]},"royalnet.database.Alchemy":{__init__:[0,2,1,""],_create_tables:[0,2,1,""],session_acm:[0,2,1,""],session_cm:[0,2,1,""]},"royalnet.error":{RoyalnetRequestError:[0,4,1,""],RoyalnetResponseError:[0,4,1,""]},"royalnet.error.RoyalnetRequestError":{args:[0,2,1,""]},"royalnet.network":{ConnectionClosedError:[0,4,1,""],NetworkConfig:[0,1,1,""],NetworkError:[0,4,1,""],NetworkLink:[0,1,1,""],NetworkServer:[0,1,1,""],NotConnectedError:[0,4,1,""],NotIdentifiedError:[0,4,1,""],Package:[0,1,1,""],Request:[0,1,1,""],Response:[0,1,1,""],ResponseError:[0,1,1,""],ResponseSuccess:[0,1,1,""]},"royalnet.network.NetworkLink":{connect:[0,2,1,""],identify:[0,2,1,""],receive:[0,2,1,""],request:[0,2,1,""],run:[0,2,1,""],send:[0,2,1,""]},"royalnet.network.NetworkServer":{find_client:[0,2,1,""],find_destination:[0,2,1,""],listener:[0,2,1,""],route_package:[0,2,1,""],run_blocking:[0,2,1,""],serve:[0,2,1,""]},"royalnet.network.Package":{__init__:[0,2,1,""],from_dict:[0,2,1,""],from_json_bytes:[0,2,1,""],from_json_string:[0,2,1,""],reply:[0,2,1,""],to_dict:[0,2,1,""],to_json_bytes:[0,2,1,""],to_json_string:[0,2,1,""]},"royalnet.network.Request":{from_dict:[0,2,1,""],to_dict:[0,2,1,""]},"royalnet.network.Response":{from_dict:[0,2,1,""],raise_on_error:[0,2,1,""],to_dict:[0,2,1,""]},"royalnet.network.ResponseError":{raise_on_error:[0,2,1,""]},"royalnet.network.ResponseSuccess":{raise_on_error:[0,2,1,""]},"royalnet.utils":{NetworkHandler:[0,1,1,""],andformat:[0,5,1,""],asyncify:[0,5,1,""],cdj:[0,5,1,""],discord_escape:[0,5,1,""],fileformat:[0,5,1,""],numberemojiformat:[0,5,1,""],ordinalformat:[0,5,1,""],parse_5etools_entry:[0,5,1,""],plusformat:[0,5,1,""],safeformat:[0,5,1,""],sleep_until:[0,5,1,""],splitstring:[0,5,1,""],telegram_escape:[0,5,1,""],ytdldateformat:[0,5,1,""]},"royalnet.utils.NetworkHandler":{message_type:[0,3,1,""]},"royalnet.web":{Royalprint:[0,1,1,""],create_app:[0,5,1,""]},royalnet:{audio:[0,0,0,"-"],bots:[0,0,0,"-"],commands:[0,0,0,"-"],database:[0,0,0,"-"],error:[0,0,0,"-"],network:[0,0,0,"-"],utils:[0,0,0,"-"],web:[0,0,0,"-"]}},objnames:{"0":["py","module","Python module"],"1":["py","class","Python class"],"2":["py","method","Python method"],"3":["py","attribute","Python attribute"],"4":["py","exception","Python exception"],"5":["py","function","Python function"]},objtypes:{"0":"py:module","1":"py:class","2":"py:method","3":"py:attribute","4":"py:exception","5":"py:function"},terms:{"20m":0,"byte":0,"class":[0,1],"default":[0,1],"final":0,"function":[0,1],"import":1,"int":0,"new":[0,2,3],"null":0,"return":[0,1],"short":1,"static":[0,1],"super":1,"true":[0,1],"try":[0,1],"while":[0,1],Adding:2,And:1,For:1,Not:1,That:0,The:[0,1,2],Then:[0,1,3],These:[0,1],Use:[0,1],Using:2,__default__:[0,3],__dict__:0,__doc__:0,__getitem__:0,__init__:[0,1],__module__:0,__slots__:0,__weakref__:0,_bot_factori:0,_create_t:0,_data_factori:0,_default_ytdl_arg:0,_handle_callback_queri:0,_handle_messag:0,_handle_upd:0,_init_cli:0,_init_command:0,_init_databas:0,_init_loop:0,_init_network:0,_init_sentri:0,_init_voic:0,_initi:0,_interface_factori:0,_network_handl:0,aaaaaa:3,aaaaaaaaaaaaaaa:3,aaaaaaaaaaaaaaaaaaaaaaaa:3,aaaaaaaaaaaaaaaaaaaaaaaaaaa:3,aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:3,abl:[0,3],about:[0,1],abstracteventloop:0,access:[0,2],accur:0,add:[0,1],add_to_music_data:0,added:[0,1],adding:[0,1],addit:0,addition:1,address:0,advance_music_data:0,after:1,akin:0,alchemi:[0,2],alia:0,alias:0,all:[0,1,3],allow:[0,1],allowed_pasta:1,along:[0,1],alreadi:[1,3],also:[0,1],altern:0,alwai:[0,1],amount:0,andformat:0,ani:[0,1],anonym:3,anoth:0,anymor:0,anystr:0,api:[2,3],app:0,append:1,applic:[0,3],arg:[0,1],argument:[0,2],around:0,arrai:0,ascend:1,ask:3,associ:3,async:[0,1],asyncifi:[0,1],asyncio:[0,1],asyncron:0,attempt:0,attribut:[0,1],audio:2,audiosourc:0,author:[0,3],autocomplet:0,automat:0,avail:[0,1],avoid:1,await:1,back:1,banana:1,base:0,becaus:1,been:0,befor:1,begin:1,being:[0,1],between:[0,1],block:0,blockingli:0,blueprint:0,bool:0,bot:2,botfath:3,both:0,bracket:1,brief:1,bufferediobas:0,bug:1,call:[0,1],callabl:0,callback:0,can:[0,1,3],cancel:0,cannot:0,carb:1,carbonara:1,caus:1,caution:0,cdj:0,certain:1,chain:0,chang:[0,1],change_pres:0,channel:0,charact:0,chat:[0,1],check:0,class_:0,classmethod:0,client:0,close:[0,1],clue:0,code:[0,2],column:1,come:1,command:2,commandarg:[0,1],commanddata:[0,1],commanderror:[0,1],commandinterfac:0,commun:0,comparis:1,complet:0,complex:1,compon:0,comun:2,config:0,config_obj:0,configur:[0,3],connect:[0,1,3],connectedcli:0,connectionclosederror:0,consid:1,consum:0,contain:[0,1],context:0,convers:0,convert:0,convert_to_mp3:0,convert_to_pcm:0,core:0,coroutin:[0,2],correct:0,correspond:0,cosafaunapesuunafoglia:3,could:1,creat:[0,2,3],create_and_ready_from_url:0,create_app:0,create_from_url:0,current:[0,1],custom:0,daemon:3,dai:0,data:[0,1],databas:2,database_config:0,database_uri:0,databaseconfig:0,date:0,datetim:0,db_path:0,declar:0,def:1,delet:[0,2],delete_invok:[0,1],dent:1,desc:1,descend:1,describ:0,descript:[0,1],desir:3,desktop:3,destin:0,destination_conv_id:0,detail:0,develop:3,dfile:0,dict:0,dictionari:0,differ:1,direct:2,directli:1,discord:[0,3],discord_escap:0,discordbot:0,discordcli:0,displai:[0,1],doc:0,docstr:0,document:[1,2],doe:[0,1],doesn:0,don:[0,1],done:1,download:[0,3],download_1_terabyte_of_spaghetti:1,download_fil:0,download_from_url:0,dsn:3,dure:0,dynam:0,eas:0,edit:0,effect:1,either:0,element:[0,1],els:1,emb:0,emoji:1,empti:0,encod:0,end:0,ending_class:0,engin:[0,1],enough:1,ensur:0,enter:3,entri:0,envvar:0,epoch:0,error:2,error_data:0,error_if_non:0,error_if_unavail:[0,1],escap:0,establish:1,even:0,event:0,everi:[0,1],everyth:0,exact:1,exampl:[0,1],except:[0,1],execut:[0,1],exist:[0,1],expect:1,expir:0,explain:0,express:2,ext:0,extern:1,extra_info:0,extract:0,extract_info:0,fals:0,featur:[0,1],fetch:[0,2],field:1,file:[0,1],fileaudiosourc:0,fileformat:0,filenam:0,filter:2,find:0,find_client:0,find_destin:0,finish:1,first:[1,3],flag:0,flask:0,follow:0,format:0,found:[0,1],from:[0,1,3],from_dict:0,from_json_byt:0,from_json_str:0,from_object:0,full:2,fullfil:0,gener:0,genericbot:0,get:[0,1,3],get_author:0,get_secret:0,github:2,greater:[0,1],group:[0,1,3],guild:0,handl:0,handler:0,has:[0,1],has_info:0,have:[0,1,3],headless:3,here:1,host:3,how:0,html:0,http:0,identifi:0,identity_column_nam:0,identity_t:0,ifi:0,ignor:[0,1,3],ignoreerror:0,imag:1,imgur:3,import_nam:0,incom:0,incomplet:0,index:[0,2],info:0,inform:0,inherit:[0,1],initi:[0,2],input:0,insid:1,instal:3,instanc:[0,1,3],instead:[0,1],interact:1,interfac:[0,1],interface_nam:0,invalid:[0,1],invalidinputerror:[0,1],invok:[0,2],is_download:0,is_open:1,is_opu:0,isn:[0,1],itali:1,item:0,join:[0,1],json:0,jsonabl:0,just:[0,1,3],keep:1,kei:[0,3],key_nam:0,keyboard:0,keyboardexpirederror:0,keyr:2,keyword:1,kind:1,kitchen:1,kwarg:0,last:0,later:1,launch:3,less:0,like:[0,1],link:0,link_typ:0,linux:3,list:[0,1],listen:0,local:3,login:0,look:1,loop:0,made:0,mai:1,main:0,mainli:0,maintain:0,make:[0,1],manag:0,manual:[1,3],mark:0,markup:0,master_secret:0,master_t:0,master_uri:0,match:[0,1],max:0,mean:[0,1],meantim:1,member:1,memori:0,mention:1,messag:[0,2],message_typ:0,metadata:1,method:[0,1],middl:0,might:0,minimum:[0,1],miscellan:0,miss:[0,1],month:0,more:[0,2],multipl:[0,1],music_data:0,must:0,name:[0,1,3],necessari:3,need:[0,1,3],net_request:0,network:[2,3],network_config:0,network_handl:0,networkconfig:0,networkerror:0,networkhandl:0,networklink:0,networkserv:0,next:[0,1,3],nid:0,no_warn:0,nobodi:0,node:0,non:0,none:[0,1],noplaylist:0,normal:1,notat:0,notconnectederror:0,noth:[0,1],notic:1,notidentifiederror:0,notimpl:0,now:[1,3],number:[0,1],numberemojiformat:0,object:[0,1],offset:0,onc:[0,1],one:[0,1,3],one_or_non:1,ones:[0,1],onli:[0,1],open:0,oper:2,option:[0,2],optional_arg:0,opu:0,order:2,order_bi:1,ordinalformat:0,org:0,orm:1,other:[0,1],otherwis:[0,1],outdat:0,output:0,outtmpl:0,overrid:1,packag:[0,3],packet:0,page:0,paramet:[0,1],parenthes:1,parse_5etools_entri:0,part:1,pass:[0,1],password:[0,3],pasta:1,path:0,pattern:[0,1],pcm:0,pcm_avail:0,pickl:0,ping:1,pingcommand:1,pip:3,plai:[0,1],plusformat:0,pong:1,port:0,portal:3,possibl:[0,1],postgresql:1,prefix:0,prepar:0,prepend:0,presenc:0,present:1,press:[0,3],previou:1,previous:1,probabl:[0,3],prompt:3,properli:1,properti:0,python:[1,3],queri:2,quiet:0,rais:[0,2],raise_on_error:0,read:[0,1],readi:[1,3],ready_up:0,realli:0,receiv:[0,1],reciev:0,recogn:1,recreat:0,redisplai:1,refer:[1,2],regex:0,regist:[0,3],register_keyboard_kei:0,register_net_handl:0,regular:2,relat:0,relationshiplinkchain:0,rememb:[1,3],remov:0,replac:0,repli:[0,1],request:[0,1],request_dict:0,request_handl:0,requested_pasta:1,requestedpasta:1,requir:[0,1],require_alchemy_t:[0,1],require_at_least:[0,1],required_arg:0,required_secret:0,required_t:0,requrire_alchemy_t:1,respect:1,respons:0,responseerror:0,responsesuccess:0,rest:[1,3],restart:1,result:[0,2],retriev:0,retrieve_for_url:0,right_now:1,role:1,root_path:0,round:1,rout:0,route_packag:0,row:[0,1],royal:1,royalcod:0,royalgam:3,royalnet:0,royalnetlink:0,royalnetrequesterror:0,royalnetresponseerror:0,royalprint:0,royalt:1,run:[0,2],run_block:0,safe_api_cal:0,safeformat:0,same:1,script:1,second:0,secret:[0,3],secret_kei:0,secrets_nam:0,section:1,select:0,self:[0,1],send:[0,1],sent:[0,1],sentri:3,sentry_dsn:0,separ:[0,1],sequenc:0,serv:0,server:0,session:1,session_acm:0,session_cm:0,set:[0,1],set_secret:0,setup:3,share:1,should:[0,1,3],side:1,signal:0,simpl:1,singl:[0,1],sleep:1,sleep_until:0,slow:2,small:[0,1],some:1,someth:0,somewher:0,song:0,soon:1,sort:1,sourc:0,source_conv_id:0,space:[0,1],spaghetti:1,spaghetticommand:1,spawn_audiosourc:0,special:[0,1],specif:[0,1],specifi:[0,1],splitstr:0,sqlalchemi:[0,1],squar:1,stai:1,start:[0,1,3],starting_class:0,statement:0,static_fold:0,static_url_path:0,statu:0,stop:1,store:1,str:[0,1],stream:0,string:[0,2],subdomain:0,submodul:0,success:0,suppli:3,support:[0,1],syntax:[0,1],system:3,tabl:[0,1],take:1,task:[0,1],telegram:[0,1,3],telegram_escap:0,telegrambot:0,tell:[0,1],template_fold:0,text:0,than:[0,1],thei:[0,1],them:[0,1],thi:[0,1,3],thing:1,think:1,thought:1,through:[0,1],time:[0,1],titl:0,to_dict:0,to_discord_emb:0,to_json_byt:0,to_json_str:0,todo:3,token:3,too:1,tri:1,tupl:[0,1],two:0,type:[0,1,3],underscor:0,undescrib:0,unexpect:0,unexpectedli:0,union:[0,1],unlock:3,unregister_keyboard_kei:0,unregister_net_handl:0,unsupportederror:[0,1],until:[0,1],updat:0,update_activity_with_source_titl:0,update_info:0,uri:0,url:0,url_default:0,url_prefix:0,usag:3,use:[0,1],used:[0,1,3],useful:[0,1],user:[0,1,3],usernam:[0,1],uses:0,using:[0,1],usual:0,utf8:0,util:[1,2],uuid:0,valid:0,valu:0,variabl:0,variou:0,verbos:0,via:2,video:0,voic:0,wai:[0,1],wait:0,want:[0,1],wasn:1,web:2,websit:1,websocket:0,websocketserverprotocol:0,weird:0,welcom:2,went:0,were:[0,1],what:[0,1],when:[0,1],whenev:1,where:0,which:[0,1],why:0,window:3,without:[0,3],wizard:3,won:[1,3],word:0,work:[0,1],worth:0,wrap:1,wrapper:0,written:0,wrong:0,year:0,yet:0,you:[0,1,3],your:[1,3],youtub:0,youtube_dl:0,youtubedl:0,ytdl_arg:0,ytdl_file:0,ytdldateformat:0,ytdldiscord:0,ytdlfile:0,ytdlinfo:0,ytdlmp3:0,yyyi:0,yyyymmdd:0},titles:["API Reference","Royalnet Commands","royalnet","Running Royalnet"],titleterms:{"new":1,Adding:1,The:3,Using:1,access:1,alchemi:1,api:0,argument:1,audio:0,bot:[0,1,3],code:1,command:[0,1],comun:1,coroutin:1,creat:1,databas:[0,1],delet:1,direct:1,error:[0,1],express:1,fetch:1,filter:1,full:1,initi:1,invok:1,keyr:3,link:2,messag:1,more:1,network:0,oper:1,option:1,order:1,queri:1,rais:1,refer:0,regular:1,result:1,royalnet:[1,2,3],run:[1,3],slow:1,some:2,string:1,useful:2,util:0,via:1,web:0}})
\ No newline at end of file
diff --git a/docs_source/creatingacommand.rst b/docs_source/creatingacommand.rst
index 248a6068..037557cd 100644
--- a/docs_source/creatingacommand.rst
+++ b/docs_source/creatingacommand.rst
@@ -137,7 +137,7 @@ If you want the full argument string, you can use the :py:meth:`CommandArgs.join
args.joined()
# "carbonara al-dente"
-You can specify a minimum number of arguments too, so that an :py:exc:`royalnet.error.InvalidInputError` will be
+You can specify a minimum number of arguments too, so that an :py:exc:`InvalidInputError` will be
raised if not enough arguments are present: ::
args.joined(require_at_least=3)
@@ -149,7 +149,7 @@ Regular expressions
For more complex commands, you may want to get arguments through `regular expressions `_.
You can then use the :py:meth:`CommandArgs.match` method, which tries to match a pattern to the command argument string,
-which returns a tuple of the matched groups and raises an :py:exc:`royalnet.error.InvalidInputError` if there is no match.
+which returns a tuple of the matched groups and raises an :py:exc:`InvalidInputError` if there is no match.
To match a pattern, :py:func:`re.match` is used, meaning that Python will try to match only at the beginning of the string. ::
@@ -165,6 +165,25 @@ To match a pattern, :py:func:`re.match` is used, meaning that Python will try to
args.match(r"\s*(carb\w+)\s*(al-\w+)")
# ("carbonara", "al-dente")
+Raising errors
+---------------------------------------------
+
+If you want to display an error message to the user, you can raise a :py:exc:`CommandError` using the error message as argument: ::
+
+ if not kitchen.is_open():
+ raise CommandError("The kitchen is closed. Come back later!")
+
+You can also manually raise :py:exc:`InvalidInputError` to redisplay the command syntax, along with your error message: ::
+
+ if args[0] not in allowed_pasta:
+ raise InvalidInputError("The specified pasta type is invalid.")
+
+If you need a Royalnet feature that's not available on the current interface, you can raise an
+:py:exc:`UnsupportedError` with a brief description of what's missing: ::
+
+ if interface.name != "telegram":
+ raise UnsupportedError("This command can only be run on Telegram interfaces.")
+
Running code at the initialization of the bot
---------------------------------------------
diff --git a/docs_source/runningroyalnet.rst b/docs_source/runningroyalnet.rst
index 91376c26..519fe1a0 100644
--- a/docs_source/runningroyalnet.rst
+++ b/docs_source/runningroyalnet.rst
@@ -3,4 +3,60 @@
Running Royalnet
====================================
-This documentation page hasn't been written yet, please refer to the README until then.
\ No newline at end of file
+To run a ``royalnet`` instance, you have first to download the package from ``pip``:
+
+The Keyring
+------------------------------------
+::
+
+ pip install royalnet
+
+
+To run ``royalnet``, you'll have to setup the system keyring.
+
+On Windows and desktop Linux, this is already configured;
+on a headless Linux instance, you'll need to `manually start and unlock the keyring daemon
+`_.
+
+Now you have to create a new ``royalnet`` configuration. Start the configuration wizard: ::
+
+ python -m royalnet.configurator
+
+You'll be prompted to enter a "secrets name": this is the name of the group of API keys that will be associated with
+your bot. Enter a name that you'll be able to remember. ::
+
+ Desired secrets name [__default__]: royalgames
+
+You'll then be asked for a network password.
+
+This password is used to connect to the rest of the :py:mod:`royalnet.network`, or, if you're hosting a local Network,
+it will be the necessary password to connect to it: ::
+
+ Network password []: cosafaunapesuunafoglia
+
+Then you'll be asked for a Telegram Bot API token.
+You can get one from `@BotFather `_. ::
+
+ Telegram Bot API token []: 000000000:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+
+The next prompt will ask for a Discord Bot API token.
+You can get one at the `Discord Developers Portal `_. ::
+
+ Discord Bot API token []: AAAAAAAAAAAAAAAAAAAAAAAA.AAAAAA.AAAAAAAAAAAAAAAAAAAAAAAAAAA
+
+Now the configurator will ask you for a Imgur API token.
+`Register an application `_ on Imgur to be supplied one.
+The token should be of type "anonymous usage without user authorization". ::
+
+ Imgur API token []: aaaaaaaaaaaaaaa
+
+Next, you'll be asked for a Sentry DSN. You probably won't have one, so just ignore it and press enter. ::
+
+ Sentry DSN []:
+
+Now that all tokens are configured, you're ready to launch the bot!
+
+Running the bots
+------------------------------------
+
+TODO
\ No newline at end of file
diff --git a/royalnet/bots/discord.py b/royalnet/bots/discord.py
index f88a2393..a17feffe 100644
--- a/royalnet/bots/discord.py
+++ b/royalnet/bots/discord.py
@@ -52,7 +52,7 @@ class DiscordBot(GenericBot):
query = query.filter(self.identity_column == user.id)
result = await asyncify(query.one_or_none)
if result is None and error_if_none:
- raise UnregisteredError("Author is not registered")
+ raise CommandError("You must be registered to use this command.")
return result
async def delete_invoking(data, error_if_unavailable=False):
@@ -115,52 +115,42 @@ class DiscordBot(GenericBot):
# Call the command
log.debug(f"Calling command '{command.name}'")
with message.channel.typing():
+ # Run the command
try:
- await command.run(CommandArgs(parameters), data=data)
+ await command.run(CommandArgs(parameters), data)
except InvalidInputError as e:
- await data.reply(f":warning: {' '.join(e.args)}\n"
- f"Syntax: [c]!{command.name} {command.syntax}[/c]")
+ await data.reply(f"⚠️ {e.message}\n"
+ f"Syntax: [c]/{command.name} {command.syntax}[/c]")
+ except UnsupportedError as e:
+ await data.reply(f"⚠️ {e.message}")
+ except CommandError as e:
+ await data.reply(f"⚠️ {e.message}")
except Exception as e:
sentry_sdk.capture_exception(e)
- error_message = f"🦀 {e.__class__.__name__} 🦀\n"
+ error_message = f"🦀 [b]{e.__class__.__name__}[/b] 🦀\n"
error_message += '\n'.join(e.args)
- log.error(f"Error in {command.name}: {error_message}")
- await data.reply(f"{error_message}")
- if __debug__:
- raise
+ await data.reply(error_message)
- async def on_ready(cli):
+ async def on_ready(cli) -> None:
log.debug("Connection successful, client is ready")
await cli.change_presence(status=discord.Status.online)
- def find_guild_by_name(cli, name: str) -> discord.Guild:
- """Find the :py:class:`discord.Guild` with the specified name. Case-insensitive.
-
- Raises:
- :py:exc:`NoneFoundError` if no channels are found.
- :py:exc:`TooManyFoundError` if more than one is found."""
+ def find_guild_by_name(cli, name: str) -> typing.List[discord.Guild]:
+ """Find the :py:class:`discord.Guild` with the specified name (case insensitive)."""
all_guilds: typing.List[discord.Guild] = cli.guilds
matching_channels: typing.List[discord.Guild] = []
for guild in all_guilds:
if guild.name.lower() == name.lower():
matching_channels.append(guild)
- if len(matching_channels) == 0:
- raise NoneFoundError("No channels were found")
- elif len(matching_channels) > 1:
- raise TooManyFoundError("Too many channels were found")
- return matching_channels[0]
+ return matching_channels
def find_channel_by_name(cli,
name: str,
- guild: typing.Optional[discord.Guild] = None) -> discord.abc.GuildChannel:
+ guild: typing.Optional[discord.Guild] = None) -> typing.List[discord.abc.GuildChannel]:
"""Find the :py:class:`TextChannel`, :py:class:`VoiceChannel` or :py:class:`CategoryChannel` with the
- specified name.
+ specified name (case insensitive).
- Case-insensitive.
-
- Guild is optional, but the method will raise a :py:exc:`TooManyFoundError` if none is specified and
- there is more than one channel with the same name. Will also raise a :py:exc:`NoneFoundError` if no
- channels are found. """
+ You can specify a guild to only find channels in that specific guild."""
if guild is not None:
all_channels = guild.channels
else:
@@ -173,21 +163,14 @@ class DiscordBot(GenericBot):
continue
if channel.name.lower() == name.lower():
matching_channels.append(channel)
- if len(matching_channels) == 0:
- raise NoneFoundError("No channels were found")
- elif len(matching_channels) > 1:
- raise TooManyFoundError("Too many channels were found")
- return matching_channels[0]
+ return matching_channels
- def find_voice_client_by_guild(cli, guild: discord.Guild):
- """Find the :py:class:`discord.VoiceClient` belonging to a specific :py:class:`discord.Guild`.
-
- Raises:
- :py:exc:`NoneFoundError` if the :py:class:`discord.Guild` currently has no :py:class:`discord.VoiceClient`."""
+ def find_voice_client_by_guild(cli, guild: discord.Guild) -> typing.Optional[discord.VoiceClient]:
+ """Find the :py:class:`discord.VoiceClient` belonging to a specific :py:class:`discord.Guild`."""
for voice_client in cli.voice_clients:
if voice_client.guild == guild:
return voice_client
- raise NoneFoundError("No voice clients found")
+ return None
return DiscordClient
diff --git a/royalnet/bots/generic.py b/royalnet/bots/generic.py
index 2c6163a2..0378834a 100644
--- a/royalnet/bots/generic.py
+++ b/royalnet/bots/generic.py
@@ -103,18 +103,15 @@ class GenericBot:
log.debug(f"Using {network_handler} as handler for {request.handler}")
response: Response = await getattr(network_handler, self.interface_name)(self, request.data)
return response.to_dict()
- except Exception:
- if __debug__:
- raise
- exit(1)
- _, exc, _ = sys.exc_info()
- log.debug(f"Exception {exc} in {network_handler}")
+ except Exception as e:
+ sentry_sdk.capture_exception(e)
+ log.debug(f"Exception {e} in {network_handler}")
return ResponseError("exception_in_handler",
f"An exception was raised in {network_handler} for {request.handler}. Check "
f"extra_info for details.",
extra_info={
- "type": exc.__class__.__name__,
- "str": str(exc)
+ "type": e.__class__.__name__,
+ "str": str(e)
}).to_dict()
def _init_database(self):
@@ -127,8 +124,8 @@ class GenericBot:
required_tables = required_tables.union(command.require_alchemy_tables)
log.debug(f"Found {len(required_tables)} required tables")
self.alchemy = Alchemy(self.uninitialized_database_config.database_uri, required_tables)
- self.master_table = self.alchemy.__getattribute__(self.uninitialized_database_config.master_table.__name__)
- self.identity_table = self.alchemy.__getattribute__(self.uninitialized_database_config.identity_table.__name__)
+ self.master_table = self.alchemy.__getattribute__(self.uninitialized_database_config.master_table.__qualname__)
+ self.identity_table = self.alchemy.__getattribute__(self.uninitialized_database_config.identity_table.__qualname__)
self.identity_column = self.identity_table.__getattribute__(self.identity_table,
self.uninitialized_database_config.identity_column_name)
self.identity_chain = relationshiplinkchain(self.master_table, self.identity_table)
diff --git a/royalnet/bots/telegram.py b/royalnet/bots/telegram.py
index 3c102d1e..1d03a8ba 100644
--- a/royalnet/bots/telegram.py
+++ b/royalnet/bots/telegram.py
@@ -166,15 +166,17 @@ class TelegramBot(GenericBot):
try:
await command.run(CommandArgs(parameters), data)
except InvalidInputError as e:
- await data.reply(f"⚠️ {' '.join(e.args)}\n"
+ await data.reply(f"⚠️ {e.message}\n"
f"Syntax: [c]/{command.name} {command.syntax}[/c]")
+ except UnsupportedError as e:
+ await data.reply(f"⚠️ {e.message}")
+ except CommandError as e:
+ await data.reply(f"⚠️ {e.message}")
except Exception as e:
sentry_sdk.capture_exception(e)
error_message = f"🦀 [b]{e.__class__.__name__}[/b] 🦀\n"
error_message += '\n'.join(e.args)
await data.reply(error_message)
- if __debug__:
- raise
async def _handle_callback_query(self, update: telegram.Update):
query: telegram.CallbackQuery = update.callback_query
diff --git a/royalnet/commands/royalgames/__init__.py b/royalnet/commands/royalgames/__init__.py
index aa8b0f85..7485c79c 100644
--- a/royalnet/commands/royalgames/__init__.py
+++ b/royalnet/commands/royalgames/__init__.py
@@ -8,17 +8,11 @@ from .color import ColorCommand
from .cv import CvCommand
from .diario import DiarioCommand
from .mp3 import Mp3Command
-from .pause import PauseCommand
from .ping import PingCommand
-from .play import PlayCommand
-from .playmode import PlaymodeCommand
-from .queue import QueueCommand
from .rage import RageCommand
from .reminder import ReminderCommand
from .ship import ShipCommand
-from .skip import SkipCommand
from .smecds import SmecdsCommand
-from .summon import SummonCommand
from .videochannel import VideochannelCommand
from .dnditem import DnditemCommand
from .dndspell import DndspellCommand
@@ -33,17 +27,11 @@ commands = [
CvCommand,
DiarioCommand,
Mp3Command,
- PauseCommand,
PingCommand,
- PlayCommand,
- PlaymodeCommand,
- QueueCommand,
RageCommand,
ReminderCommand,
ShipCommand,
- SkipCommand,
SmecdsCommand,
- SummonCommand,
VideochannelCommand,
DnditemCommand,
DndspellCommand,
diff --git a/royalnet/commands/royalgames/cv.py b/royalnet/commands/royalgames/cv.py
index 79b22760..585997b7 100644
--- a/royalnet/commands/royalgames/cv.py
+++ b/royalnet/commands/royalgames/cv.py
@@ -7,7 +7,7 @@ from ..commanddata import CommandData
from ...network import Request, ResponseSuccess
from ...utils import NetworkHandler, andformat
from ...bots import DiscordBot
-from ...error import *
+from ..commanderrors import CommandError
class CvNH(NetworkHandler):
@@ -17,13 +17,14 @@ class CvNH(NetworkHandler):
async def discord(cls, bot: "DiscordBot", data: dict):
# Find the matching guild
if data["guild_name"]:
- guild: discord.Guild = bot.client.find_guild_by_name(data["guild_name"])
+ guilds: typing.List[discord.Guild] = bot.client.find_guild_by_name(data["guild_name"])
else:
- if len(bot.client.guilds) == 0:
- raise NoneFoundError("No guilds found")
- if len(bot.client.guilds) > 1:
- raise TooManyFoundError("Multiple guilds found")
- guild = list(bot.client.guilds)[0]
+ guilds = bot.client.guilds
+ if len(guilds) == 0:
+ raise CommandError("No guilds with the specified name found.")
+ if len(guilds) > 1:
+ raise CommandError("Multiple guilds with the specified name found.")
+ guild = list(bot.client.guilds)[0]
# Edit the message, sorted by channel
discord_members = list(guild.members)
channels = {0: None}
@@ -119,7 +120,7 @@ class CvCommand(Command):
description: str = "Elenca le persone attualmente connesse alla chat vocale."
- syntax: str = "[guildname] "
+ syntax: str = "[guildname] ['all']"
def __init__(self, interface: CommandInterface):
super().__init__(interface)
diff --git a/royalnet/commands/royalgames/diario.py b/royalnet/commands/royalgames/diario.py
index f12a9b12..0a1ffee6 100644
--- a/royalnet/commands/royalgames/diario.py
+++ b/royalnet/commands/royalgames/diario.py
@@ -8,7 +8,7 @@ from ..commandargs import CommandArgs
from ..commanddata import CommandData
from ...database.tables import User, Diario, Alias
from ...utils import asyncify
-from ...error import *
+from ..commanderrors import CommandError, InvalidInputError
async def to_imgur(imgur_api_key, photosizes: typing.List[telegram.PhotoSize], caption="") -> str:
@@ -27,7 +27,7 @@ async def to_imgur(imgur_api_key, photosizes: typing.List[telegram.PhotoSize], c
}) as request:
response = await request.json()
if not response["success"]:
- raise ExternalError("imgur returned an error in the image upload.")
+ raise CommandError("Imgur returned an error in the image upload.")
return response["data"]["link"]
diff --git a/royalnet/commands/royalgames/mm.py b/royalnet/commands/royalgames/mm.py
index f3b130c2..4569036a 100644
--- a/royalnet/commands/royalgames/mm.py
+++ b/royalnet/commands/royalgames/mm.py
@@ -4,11 +4,12 @@ import telegram
import asyncio
import re
import logging
+import typing
from ..command import Command
from ..commandargs import CommandArgs
from ..commanddata import CommandData
from ...database.tables import MMEvent, MMDecision, MMResponse
-from ...error import *
+from ..commanderrors import InvalidInputError, UnsupportedError
from ...utils import asyncify, telegram_escape, sleep_until
log = logging.getLogger(__name__)
diff --git a/royalnet/commands/royalgames/reminder.py b/royalnet/commands/royalgames/reminder.py
index c7148b13..ba914756 100644
--- a/royalnet/commands/royalgames/reminder.py
+++ b/royalnet/commands/royalgames/reminder.py
@@ -11,8 +11,7 @@ from ..commandinterface import CommandInterface
from ..commanddata import CommandData
from ...utils import sleep_until, asyncify, telegram_escape, discord_escape
from ...database.tables import Reminder
-from ...error import *
-
+from ..commanderrors import InvalidInputError, UnsupportedError
class ReminderCommand(Command):
name: str = "reminder"
@@ -76,7 +75,7 @@ class ReminderCommand(Command):
elif self.interface.name == "discord":
interface_data = pickle.dumps(data.message.channel.id)
else:
- raise UnsupportedError("Interface not supported")
+ raise UnsupportedError("This command does not support the current interface.")
creator = await data.get_author()
reminder = self.interface.alchemy.Reminder(creator=creator,
interface_name=self.interface.name,
diff --git a/royalnet/commands/royalgames/trivia.py b/royalnet/commands/royalgames/trivia.py
index b6385af2..b29da884 100644
--- a/royalnet/commands/royalgames/trivia.py
+++ b/royalnet/commands/royalgames/trivia.py
@@ -9,7 +9,7 @@ from ..commandargs import CommandArgs
from ..commanddata import CommandData
from ..commandinterface import CommandInterface
from ...utils import asyncify
-from ...error import *
+from ..commanderrors import CommandError, KeyboardExpiredError
from ...database.tables import TriviaScore
@@ -39,7 +39,7 @@ class TriviaCommand(Command):
j = await response.json()
# Parse the question
if j["response_code"] != 0:
- raise ExternalError(f"OpenTDB returned {j['response_code']} response_code")
+ raise CommandError(f"OpenTDB returned an error response_code ({j['response_code']}).")
question = j["results"][0]
text = f'❓ [b]{question["category"]} - {question["difficulty"].capitalize()}[/b]\n' \
f'{html.unescape(question["question"])}'
diff --git a/royalnet/commands/royalgames/videochannel.py b/royalnet/commands/royalgames/videochannel.py
index 3d153675..ab58bb81 100644
--- a/royalnet/commands/royalgames/videochannel.py
+++ b/royalnet/commands/royalgames/videochannel.py
@@ -3,7 +3,7 @@ import discord
from ..command import Command
from ..commandargs import CommandArgs
from ..commanddata import CommandData
-from ...error import *
+from ..commanderrors import CommandError, UnsupportedError
class VideochannelCommand(Command):
@@ -30,18 +30,15 @@ class VideochannelCommand(Command):
if channel.name == channel_name:
matching_channels.append(channel)
if len(matching_channels) == 0:
- await data.reply("⚠️ Non esiste alcun canale vocale con il nome specificato.")
- return
+ raise CommandError("Non esiste alcun canale vocale con il nome specificato.")
elif len(matching_channels) > 1:
- await data.reply("⚠️ Esiste più di un canale vocale con il nome specificato.")
- return
+ raise CommandError("Esiste più di un canale vocale con il nome specificato.")
channel = matching_channels[0]
else:
author: discord.Member = message.author
voice: typing.Optional[discord.VoiceState] = author.voice
if voice is None:
- await data.reply("⚠️ Non sei connesso a nessun canale vocale!")
- return
+ raise CommandError("Non sei connesso a nessun canale vocale.")
channel = voice.channel
if author.is_on_mobile():
await data.reply(f"📹 Per entrare in modalità video, clicca qui: \n[b]Attenzione: la modalità video non funziona su Discord per Android e iOS![/b]")
diff --git a/royalnet/commands/royalgames/zawarudo.py b/royalnet/commands/royalgames/zawarudo.py
index 3918ab93..d85b644c 100644
--- a/royalnet/commands/royalgames/zawarudo.py
+++ b/royalnet/commands/royalgames/zawarudo.py
@@ -8,7 +8,7 @@ from ..commandargs import CommandArgs
from ..commanddata import CommandData
from ...utils import NetworkHandler, asyncify
from ...network import Request, ResponseSuccess
-from ...error import *
+from ..commanderrors import CommandError, InvalidInputError, UnsupportedError, KeyboardExpiredError
from ...audio import YtdlDiscord
from ...audio.playmodes import Playlist
if typing.TYPE_CHECKING:
@@ -27,13 +27,14 @@ class ZawarudoNH(NetworkHandler):
async def discord(cls, bot: "DiscordBot", data: dict):
# Find the matching guild
if data["guild_name"]:
- guild = bot.client.find_guild(data["guild_name"])
+ guilds: typing.List[discord.Guild] = bot.client.find_guild_by_name(data["guild_name"])
else:
- if len(bot.music_data) == 0:
- raise NoneFoundError("No voice clients active")
- if len(bot.music_data) > 1:
- raise TooManyFoundError("Multiple guilds found")
- guild = list(bot.music_data)[0]
+ guilds = bot.client.guilds
+ if len(guilds) == 0:
+ raise CommandError("No guilds with the specified name found.")
+ if len(guilds) > 1:
+ raise CommandError("Multiple guilds with the specified name found.")
+ guild = list(bot.client.guilds)[0]
# Ensure the guild has a PlayMode before adding the file to it
if not bot.music_data.get(guild):
# TODO: change Exception
diff --git a/royalnet/commands/royalmusic/__init__.py b/royalnet/commands/royalmusic/__init__.py
index e69de29b..c34e0a16 100644
--- a/royalnet/commands/royalmusic/__init__.py
+++ b/royalnet/commands/royalmusic/__init__.py
@@ -0,0 +1,24 @@
+"""Commands that can be used in bots.
+
+These probably won't suit your needs, as they are tailored for the bots of the User Games gaming community, but they
+ may be useful to develop new ones."""
+
+from .pause import PauseCommand
+from .play import PlayCommand
+from .playmode import PlaymodeCommand
+from .queue import QueueCommand
+from .skip import SkipCommand
+from .summon import SummonCommand
+
+
+commands = [
+ PauseCommand,
+ PlayCommand,
+ PlaymodeCommand,
+ QueueCommand,
+ SkipCommand,
+ SummonCommand,
+]
+
+
+__all__ = [command.__name__ for command in commands]
diff --git a/royalnet/commands/royalmusic/pause.py b/royalnet/commands/royalmusic/pause.py
index 00f42d29..57c16510 100644
--- a/royalnet/commands/royalmusic/pause.py
+++ b/royalnet/commands/royalmusic/pause.py
@@ -6,7 +6,7 @@ from ..commandargs import CommandArgs
from ..commanddata import CommandData
from ...utils import NetworkHandler
from ...network import Request, ResponseSuccess
-from ...error import NoneFoundError
+from ..commanderrors import CommandError
if typing.TYPE_CHECKING:
from ...bots import DiscordBot
@@ -19,17 +19,18 @@ class PauseNH(NetworkHandler):
async def discord(cls, bot: "DiscordBot", data: dict):
# Find the matching guild
if data["guild_name"]:
- guild = bot.client.find_guild_by_name(data["guild_name"])
+ guilds: typing.List[discord.Guild] = bot.client.find_guild_by_name(data["guild_name"])
else:
- if len(bot.music_data) == 0:
- raise NoneFoundError("No voice clients active")
- if len(bot.music_data) > 1:
- raise TooManyFoundError("Multiple guilds found")
- guild = list(bot.music_data)[0]
+ guilds = bot.client.guilds
+ if len(guilds) == 0:
+ raise CommandError("No guilds with the specified name found.")
+ if len(guilds) > 1:
+ raise CommandError("Multiple guilds with the specified name found.")
+ guild = list(bot.client.guilds)[0]
# Set the currently playing source as ended
voice_client: discord.VoiceClient = bot.client.find_voice_client_by_guild(guild)
if not (voice_client.is_playing() or voice_client.is_paused()):
- raise NoneFoundError("Nothing to pause")
+ raise CommandError("There is nothing to pause.")
# Toggle pause
resume = voice_client._player.is_paused()
if resume:
diff --git a/royalnet/commands/royalmusic/play.py b/royalnet/commands/royalmusic/play.py
index dfcc6eae..b8494fe3 100644
--- a/royalnet/commands/royalmusic/play.py
+++ b/royalnet/commands/royalmusic/play.py
@@ -1,13 +1,14 @@
import typing
import pickle
import datetime
+import discord
from ..command import Command
from ..commandinterface import CommandInterface
from ..commandargs import CommandArgs
from ..commanddata import CommandData
from ...utils import NetworkHandler, asyncify
from ...network import Request, ResponseSuccess
-from ...error import *
+from ..commanderrors import CommandError
from ...audio import YtdlDiscord
if typing.TYPE_CHECKING:
from ...bots import DiscordBot
@@ -21,13 +22,14 @@ class PlayNH(NetworkHandler):
"""Handle a play Royalnet request. That is, add audio to a PlayMode."""
# Find the matching guild
if data["guild_name"]:
- guild = bot.client.find_guild(data["guild_name"])
+ guilds: typing.List[discord.Guild] = bot.client.find_guild_by_name(data["guild_name"])
else:
- if len(bot.music_data) == 0:
- raise NoneFoundError("No voice clients active")
- if len(bot.music_data) > 1:
- raise TooManyFoundError("Multiple guilds found")
- guild = list(bot.music_data)[0]
+ guilds = bot.client.guilds
+ if len(guilds) == 0:
+ raise CommandError("No guilds with the specified name found.")
+ if len(guilds) > 1:
+ raise CommandError("Multiple guilds with the specified name found.")
+ guild = list(bot.client.guilds)[0]
# Ensure the guild has a PlayMode before adding the file to it
if not bot.music_data.get(guild):
# TODO: change Exception
@@ -53,7 +55,7 @@ class PlayNH(NetworkHandler):
class PlayCommand(Command):
name: str = "play"
- description: str = "Aggiunge una canzone alla coda della chat vocale."
+ description: str = "Aggiunge un url alla coda della chat vocale."
syntax = "[ [guild] ] (url)"
@@ -64,7 +66,9 @@ class PlayCommand(Command):
async def run(self, args: CommandArgs, data: CommandData) -> None:
guild_name, url = args.match(r"(?:\[(.+)])?\s*(.+)>?")
if not (url.startswith("http://") or url.startswith("https://")):
- raise
+ raise CommandError("PlayCommand only accepts URLs.\n"
+ "If you want to search a song on YouTube or Soundcloud, please use YoutubeCommand"
+ " or SoundcloudCommand!")
response = await self.interface.net_request(Request("music_play", {"url": url, "guild_name": guild_name}), "discord")
if len(response["videos"]) == 0:
await data.reply(f"⚠️ Nessun video trovato.")
diff --git a/royalnet/commands/royalmusic/playmode.py b/royalnet/commands/royalmusic/playmode.py
index 989d7d95..bd1a514d 100644
--- a/royalnet/commands/royalmusic/playmode.py
+++ b/royalnet/commands/royalmusic/playmode.py
@@ -1,12 +1,13 @@
import typing
import pickle
+import discord
from ..command import Command
from ..commandinterface import CommandInterface
from ..commandargs import CommandArgs
from ..commanddata import CommandData
from ...utils import NetworkHandler
from ...network import Request, ResponseSuccess
-from ...error import *
+from ..commanderrors import CommandError
from ...audio.playmodes import Playlist, Pool, Layers
if typing.TYPE_CHECKING:
from ...bots import DiscordBot
@@ -20,13 +21,14 @@ class PlaymodeNH(NetworkHandler):
"""Handle a playmode Royalnet request. That is, change current PlayMode."""
# Find the matching guild
if data["guild_name"]:
- guild = bot.client.find_guild(data["guild_name"])
+ guilds: typing.List[discord.Guild] = bot.client.find_guild_by_name(data["guild_name"])
else:
- if len(bot.music_data) == 0:
- raise NoneFoundError("No voice clients active")
- if len(bot.music_data) > 1:
- raise TooManyFoundError("Multiple guilds found")
- guild = list(bot.music_data)[0]
+ guilds = bot.client.guilds
+ if len(guilds) == 0:
+ raise CommandError("No guilds with the specified name found.")
+ if len(guilds) > 1:
+ raise CommandError("Multiple guilds with the specified name found.")
+ guild = list(bot.client.guilds)[0]
# Delete the previous PlayMode, if it exists
if bot.music_data[guild] is not None:
bot.music_data[guild].delete()
@@ -38,7 +40,7 @@ class PlaymodeNH(NetworkHandler):
elif data["mode_name"] == "layers":
bot.music_data[guild] = Layers()
else:
- raise ValueError("No such PlayMode")
+ raise CommandError("Unknown PlayMode specified.")
return ResponseSuccess()
diff --git a/royalnet/commands/royalmusic/queue.py b/royalnet/commands/royalmusic/queue.py
index 04865bad..a6f94979 100644
--- a/royalnet/commands/royalmusic/queue.py
+++ b/royalnet/commands/royalmusic/queue.py
@@ -1,12 +1,13 @@
import typing
import pickle
+import discord
from ..command import Command
from ..commandinterface import CommandInterface
from ..commandargs import CommandArgs
from ..commanddata import CommandData
from ...utils import NetworkHandler, numberemojiformat
from ...network import Request, ResponseSuccess
-from ...error import *
+from ..commanderrors import CommandError
if typing.TYPE_CHECKING:
from ...bots import DiscordBot
@@ -18,13 +19,14 @@ class QueueNH(NetworkHandler):
async def discord(cls, bot: "DiscordBot", data: dict):
# Find the matching guild
if data["guild_name"]:
- guild = bot.client.find_guild_by_name(data["guild_name"])
+ guilds: typing.List[discord.Guild] = bot.client.find_guild_by_name(data["guild_name"])
else:
- if len(bot.music_data) == 0:
- raise NoneFoundError("No voice clients active")
- if len(bot.music_data) > 1:
- raise TooManyFoundError("Multiple guilds found")
- guild = list(bot.music_data)[0]
+ guilds = bot.client.guilds
+ if len(guilds) == 0:
+ raise CommandError("No guilds with the specified name found.")
+ if len(guilds) > 1:
+ raise CommandError("Multiple guilds with the specified name found.")
+ guild = list(bot.client.guilds)[0]
# Check if the guild has a PlayMode
playmode = bot.music_data.get(guild)
if not playmode:
diff --git a/royalnet/commands/royalmusic/skip.py b/royalnet/commands/royalmusic/skip.py
index 74201e42..2b86058b 100644
--- a/royalnet/commands/royalmusic/skip.py
+++ b/royalnet/commands/royalmusic/skip.py
@@ -7,7 +7,7 @@ from ..commandargs import CommandArgs
from ..commanddata import CommandData
from ...utils import NetworkHandler, asyncify
from ...network import Request, ResponseSuccess
-from ...error import *
+from ..commanderrors import CommandError
from ...audio import YtdlDiscord
if typing.TYPE_CHECKING:
from ...bots import DiscordBot
@@ -20,17 +20,18 @@ class SkipNH(NetworkHandler):
async def discord(cls, bot: "DiscordBot", data: dict):
# Find the matching guild
if data["guild_name"]:
- guild = bot.client.find_guild_by_name(data["guild_name"])
+ guilds: typing.List[discord.Guild] = bot.client.find_guild_by_name(data["guild_name"])
else:
- if len(bot.music_data) == 0:
- raise NoneFoundError("No voice clients active")
- if len(bot.music_data) > 1:
- raise TooManyFoundError("Multiple guilds found")
- guild = list(bot.music_data)[0]
+ guilds = bot.client.guilds
+ if len(guilds) == 0:
+ raise CommandError("No guilds with the specified name found.")
+ if len(guilds) > 1:
+ raise CommandError("Multiple guilds with the specified name found.")
+ guild = list(bot.client.guilds)[0]
# Set the currently playing source as ended
voice_client: discord.VoiceClient = bot.client.find_voice_client_by_guild(guild)
if not (voice_client.is_playing() or voice_client.is_paused()):
- raise NoneFoundError("Nothing to skip")
+ raise CommandError("Nothing to skip")
# noinspection PyProtectedMember
voice_client._player.stop()
return ResponseSuccess()
diff --git a/royalnet/commands/royalmusic/soundcloud.py b/royalnet/commands/royalmusic/soundcloud.py
new file mode 100644
index 00000000..8950a2c7
--- /dev/null
+++ b/royalnet/commands/royalmusic/soundcloud.py
@@ -0,0 +1,84 @@
+import typing
+import pickle
+import datetime
+import discord
+from ..command import Command
+from ..commandinterface import CommandInterface
+from ..commandargs import CommandArgs
+from ..commanddata import CommandData
+from ...utils import NetworkHandler, asyncify
+from ...network import Request, ResponseSuccess
+from ..commanderrors import CommandError
+from ...audio import YtdlDiscord
+if typing.TYPE_CHECKING:
+ from ...bots import DiscordBot
+
+
+class SoundcloudNH(NetworkHandler):
+ message_type = "music_soundcloud"
+
+ @classmethod
+ async def discord(cls, bot: "DiscordBot", data: dict):
+ """Handle a play Royalnet request. That is, add audio to a PlayMode."""
+ # Find the matching guild
+ if data["guild_name"]:
+ guilds: typing.List[discord.Guild] = bot.client.find_guild_by_name(data["guild_name"])
+ else:
+ guilds = bot.client.guilds
+ if len(guilds) == 0:
+ raise CommandError("No guilds with the specified name found.")
+ if len(guilds) > 1:
+ raise CommandError("Multiple guilds with the specified name found.")
+ guild = list(bot.client.guilds)[0]
+ # Ensure the guild has a PlayMode before adding the file to it
+ if not bot.music_data.get(guild):
+ raise KeyError("No music data available for this guild.")
+ # Create url
+ ytdl_args = {
+ "format": "bestaudio",
+ "outtmpl": f"./downloads/{datetime.datetime.now().timestamp()}_%(title)s.%(ext)s"
+ }
+ # Start downloading
+ dfiles: typing. List[YtdlDiscord] = await asyncify(YtdlDiscord.create_from_url, f'scsearch:{data["search"]}',
+ **ytdl_args)
+ await bot.add_to_music_data(dfiles, guild)
+ # Create response dictionary
+ response = {
+ "videos": [{
+ "title": dfile.info.title,
+ "discord_embed_pickle": str(pickle.dumps(dfile.info.to_discord_embed()))
+ } for dfile in dfiles]
+ }
+ return ResponseSuccess(response)
+
+
+class SoundcloudCommand(Command):
+ name: str = "soundcloud"
+
+ aliases = ["sc"]
+
+ description: str = "Cerca una canzone su Soundcloud e la aggiunge alla coda della chat vocale."
+
+ syntax = "[ [guild] ] (url)"
+
+ def __init__(self, interface: CommandInterface):
+ super().__init__(interface)
+ interface.register_net_handler(SoundcloudNH.message_type, SoundcloudNH)
+
+ async def run(self, args: CommandArgs, data: CommandData) -> None:
+ guild_name, search = args.match(r"(?:\[(.+)])?\s*(.+)>?")
+ if search.startswith("http://") or search.startswith("https://"):
+ raise CommandError("YoutubeCommand only accepts search queries, and you've sent an URL.\n"
+ "If you want to add a song from an url, please use PlayCommand!")
+ response = await self.interface.net_request(Request("music_soundcloud", {"search": search,
+ "guild_name": guild_name}),
+ "discord")
+ if len(response["videos"]) == 0:
+ await data.reply(f"⚠️ Nessun video trovato.")
+ for video in response["videos"]:
+ if self.interface.name == "discord":
+ # This is one of the unsafest things ever
+ embed = pickle.loads(eval(video["discord_embed_pickle"]))
+ await data.message.channel.send(content="▶️ Aggiunto alla coda:", embed=embed)
+ else:
+ await data.reply(f"▶️ Aggiunto alla coda: [i]{video['title']}[/i]")
diff --git a/royalnet/commands/royalmusic/summon.py b/royalnet/commands/royalmusic/summon.py
index 99ed8733..3d855b4c 100644
--- a/royalnet/commands/royalmusic/summon.py
+++ b/royalnet/commands/royalmusic/summon.py
@@ -6,7 +6,7 @@ from ..commandargs import CommandArgs
from ..commanddata import CommandData
from ...utils import NetworkHandler
from ...network import Request, ResponseSuccess
-from ...error import NoneFoundError
+from ..commanderrors import CommandError
if typing.TYPE_CHECKING:
from ...bots import DiscordBot
@@ -18,9 +18,10 @@ class SummonNH(NetworkHandler):
async def discord(cls, bot: "DiscordBot", data: dict):
"""Handle a summon Royalnet request.
That is, join a voice channel, or move to a different one if that is not possible."""
- channel = bot.client.find_channel_by_name(data["channel_name"])
+ channels = bot.client.find_channel_by_name(data["channel_name"])
+ channel = channels[0]
if not isinstance(channel, discord.VoiceChannel):
- raise NoneFoundError("Channel is not a voice channel")
+ raise CommandError("Channel is not a voice channel")
bot.loop.create_task(bot.client.vc_connect_or_move(channel))
return ResponseSuccess()
diff --git a/royalnet/commands/royalmusic/youtube.py b/royalnet/commands/royalmusic/youtube.py
new file mode 100644
index 00000000..99fae98e
--- /dev/null
+++ b/royalnet/commands/royalmusic/youtube.py
@@ -0,0 +1,83 @@
+import typing
+import pickle
+import datetime
+import discord
+from ..command import Command
+from ..commandinterface import CommandInterface
+from ..commandargs import CommandArgs
+from ..commanddata import CommandData
+from ...utils import NetworkHandler, asyncify
+from ...network import Request, ResponseSuccess
+from ..commanderrors import CommandError
+from ...audio import YtdlDiscord
+if typing.TYPE_CHECKING:
+ from ...bots import DiscordBot
+
+
+class YoutubeNH(NetworkHandler):
+ message_type = "music_youtube"
+
+ @classmethod
+ async def discord(cls, bot: "DiscordBot", data: dict):
+ """Handle a play Royalnet request. That is, add audio to a PlayMode."""
+ # Find the matching guild
+ if data["guild_name"]:
+ guilds: typing.List[discord.Guild] = bot.client.find_guild_by_name(data["guild_name"])
+ else:
+ guilds = bot.client.guilds
+ if len(guilds) == 0:
+ raise CommandError("No guilds with the specified name found.")
+ if len(guilds) > 1:
+ raise CommandError("Multiple guilds with the specified name found.")
+ guild = list(bot.client.guilds)[0]
+ # Ensure the guild has a PlayMode before adding the file to it
+ if not bot.music_data.get(guild):
+ raise KeyError("No music data available for this guild.")
+ # Create url
+ ytdl_args = {
+ "format": "bestaudio",
+ "outtmpl": f"./downloads/{datetime.datetime.now().timestamp()}_%(title)s.%(ext)s"
+ }
+ # Start downloading
+ dfiles: typing. List[YtdlDiscord] = await asyncify(YtdlDiscord.create_from_url, f'ytsearch:{data["search"]}', **ytdl_args)
+ await bot.add_to_music_data(dfiles, guild)
+ # Create response dictionary
+ response = {
+ "videos": [{
+ "title": dfile.info.title,
+ "discord_embed_pickle": str(pickle.dumps(dfile.info.to_discord_embed()))
+ } for dfile in dfiles]
+ }
+ return ResponseSuccess(response)
+
+
+class YoutubeCommand(Command):
+ name: str = "youtube"
+
+ aliases = ["yt"]
+
+ description: str = "Cerca un video su YouTube e lo aggiunge alla coda della chat vocale."
+
+ syntax = "[ [guild] ] (url)"
+
+ def __init__(self, interface: CommandInterface):
+ super().__init__(interface)
+ interface.register_net_handler(YoutubeNH.message_type, YoutubeNH)
+
+ async def run(self, args: CommandArgs, data: CommandData) -> None:
+ guild_name, search = args.match(r"(?:\[(.+)])?\s*(.+)>?")
+ if search.startswith("http://") or search.startswith("https://"):
+ raise CommandError("YoutubeCommand only accepts search queries, and you've sent an URL.\n"
+ "If you want to add a song from an url, please use PlayCommand!")
+ response = await self.interface.net_request(Request("music_youtube", {"search": search,
+ "guild_name": guild_name}),
+ "discord")
+ if len(response["videos"]) == 0:
+ await data.reply(f"⚠️ Nessun video trovato.")
+ for video in response["videos"]:
+ if self.interface.name == "discord":
+ # This is one of the unsafest things ever
+ embed = pickle.loads(eval(video["discord_embed_pickle"]))
+ await data.message.channel.send(content="▶️ Aggiunto alla coda:", embed=embed)
+ else:
+ await data.reply(f"▶️ Aggiunto alla coda: [i]{video['title']}[/i]")
diff --git a/royalnet/database/alchemy.py b/royalnet/database/alchemy.py
index 74a3a499..2f164368 100644
--- a/royalnet/database/alchemy.py
+++ b/royalnet/database/alchemy.py
@@ -4,8 +4,6 @@ from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, scoped_session
from contextlib import contextmanager, asynccontextmanager
from ..utils import asyncify
-# noinspection PyUnresolvedReferences
-from ..error import InvalidConfigError
class Alchemy: