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

A lot of new stuff is coming!

This commit is contained in:
Steffo 2019-10-04 12:18:17 +02:00
parent 22c3cd6351
commit 9cbcce32da
37 changed files with 608 additions and 273 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -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 <https://regexr.com/>`_.
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
---------------------------------------------

View file

@ -3,4 +3,60 @@
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
<https://keyring.readthedocs.io/en/latest/#using-keyring-on-headless-linux-systems>`_.
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 <https://t.me/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 <https://discordapp.com/developers/applications/>`_. ::
Discord Bot API token []: AAAAAAAAAAAAAAAAAAAAAAAA.AAAAAA.AAAAAAAAAAAAAAAAAAAAAAAAAAA
Now the configurator will ask you for a Imgur API token.
`Register an application <https://api.imgur.com/oauth2/addclient>`_ 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

View file

@ -286,11 +286,6 @@
<code class="sig-name descname">convert_to_pcm</code><span class="sig-paren">(</span><span class="sig-paren">)</span> &#x2192; None<a class="headerlink" href="#royalnet.audio.YtdlDiscord.convert_to_pcm" title="Permalink to this definition"></a></dt>
<dd></dd></dl>
<dl class="method">
<dt id="royalnet.audio.YtdlDiscord.create_and_ready_from_url">
<em class="property">classmethod </em><code class="sig-name descname">create_and_ready_from_url</code><span class="sig-paren">(</span><em class="sig-param">url</em>, <em class="sig-param">**ytdl_args</em><span class="sig-paren">)</span> &#x2192; List[royalnet.audio.ytdldiscord.YtdlDiscord]<a class="headerlink" href="#royalnet.audio.YtdlDiscord.create_and_ready_from_url" title="Permalink to this definition"></a></dt>
<dd></dd></dl>
<dl class="method">
<dt id="royalnet.audio.YtdlDiscord.create_from_url">
<em class="property">classmethod </em><code class="sig-name descname">create_from_url</code><span class="sig-paren">(</span><em class="sig-param">url</em>, <em class="sig-param">**ytdl_args</em><span class="sig-paren">)</span> &#x2192; List[royalnet.audio.ytdldiscord.YtdlDiscord]<a class="headerlink" href="#royalnet.audio.YtdlDiscord.create_from_url" title="Permalink to this definition"></a></dt>
@ -573,7 +568,7 @@ find the chain that links the <code class="docutils literal notranslate"><span c
<dl class="method">
<dt id="royalnet.bots.GenericBot.run_blocking">
<code class="sig-name descname">run_blocking</code><span class="sig-paren">(</span><span class="sig-paren">)</span><a class="headerlink" href="#royalnet.bots.GenericBot.run_blocking" title="Permalink to this definition"></a></dt>
<code class="sig-name descname">run_blocking</code><span class="sig-paren">(</span><em class="sig-param">verbose=False</em><span class="sig-paren">)</span><a class="headerlink" href="#royalnet.bots.GenericBot.run_blocking" title="Permalink to this definition"></a></dt>
<dd></dd></dl>
<dl class="method">
@ -657,6 +652,13 @@ find the chain that links the <code class="docutils literal notranslate"><span c
<dt id="royalnet.commands.Command">
<em class="property">class </em><code class="sig-prename descclassname">royalnet.commands.</code><code class="sig-name descname">Command</code><span class="sig-paren">(</span><em class="sig-param">interface: royalnet.commands.commandinterface.CommandInterface</em><span class="sig-paren">)</span><a class="headerlink" href="#royalnet.commands.Command" title="Permalink to this definition"></a></dt>
<dd><dl class="attribute">
<dt id="royalnet.commands.Command.aliases">
<code class="sig-name descname">aliases</code><em class="property"> = NotImplemented</em><a class="headerlink" href="#royalnet.commands.Command.aliases" title="Permalink to this definition"></a></dt>
<dd><p>A list of possible aliases for a command.
To have <code class="docutils literal notranslate"><span class="pre">/e</span></code> as alias for <code class="docutils literal notranslate"><span class="pre">/example</span></code>, one should set aliases to <code class="docutils literal notranslate"><span class="pre">[&quot;e&quot;]</span></code>.</p>
</dd></dl>
<dl class="attribute">
<dt id="royalnet.commands.Command.description">
<code class="sig-name descname">description</code><em class="property"> = NotImplemented</em><a class="headerlink" href="#royalnet.commands.Command.description" title="Permalink to this definition"></a></dt>
<dd><p>A small description of the command, to be displayed when the command is being autocompleted.</p>
@ -683,7 +685,7 @@ To have <code class="docutils literal notranslate"><span class="pre">/example</s
<dl class="attribute">
<dt id="royalnet.commands.Command.syntax">
<code class="sig-name descname">syntax</code><em class="property"> = ''</em><a class="headerlink" href="#royalnet.commands.Command.syntax" title="Permalink to this definition"></a></dt>
<dd><p>The syntax of the command, to be displayed when a <a class="reference internal" href="#royalnet.error.InvalidInputError" title="royalnet.error.InvalidInputError"><code class="xref py py-exc docutils literal notranslate"><span class="pre">royalnet.error.InvalidInputError</span></code></a> is raised,
<dd><p>The syntax of the command, to be displayed when a <code class="xref py py-exc docutils literal notranslate"><span class="pre">royalnet.error.InvalidInputError</span></code> is raised,
in the format <code class="docutils literal notranslate"><span class="pre">(required_arg)</span> <span class="pre">[optional_arg]</span></code>.</p>
</dd></dl>
@ -694,12 +696,12 @@ in the format <code class="docutils literal notranslate"><span class="pre">(requ
<em class="property">class </em><code class="sig-prename descclassname">royalnet.commands.</code><code class="sig-name descname">CommandData</code><a class="headerlink" href="#royalnet.commands.CommandData" title="Permalink to this definition"></a></dt>
<dd><dl class="method">
<dt id="royalnet.commands.CommandData.delete_invoking">
<em class="property">async </em><code class="sig-name descname">delete_invoking</code><span class="sig-paren">(</span><em class="sig-param">error_if_unavailable=False</em><span class="sig-paren">)</span><a class="headerlink" href="#royalnet.commands.CommandData.delete_invoking" title="Permalink to this definition"></a></dt>
<em class="property">async </em><code class="sig-name descname">delete_invoking</code><span class="sig-paren">(</span><em class="sig-param">error_if_unavailable=False</em><span class="sig-paren">)</span> &#x2192; None<a class="headerlink" href="#royalnet.commands.CommandData.delete_invoking" title="Permalink to this definition"></a></dt>
<dd><p>Delete the invoking message, if supported by the interface.</p>
<p>The invoking message is the message send by the user that contains the command.</p>
<dl class="field-list simple">
<dt class="field-odd">Parameters</dt>
<dd class="field-odd"><p><strong>error_if_unavailable</strong> if True, raise NotImplementedError() if the message cannot been deleted</p>
<dd class="field-odd"><p><strong>error_if_unavailable</strong> if True, raise an exception if the message cannot been deleted.</p>
</dd>
</dl>
</dd></dl>
@ -711,10 +713,7 @@ in the format <code class="docutils literal notranslate"><span class="pre">(requ
That probably means, the database row identifying the user.</p>
<dl class="field-list simple">
<dt class="field-odd">Parameters</dt>
<dd class="field-odd"><p><strong>error_if_none</strong> Raise a <a class="reference internal" href="#royalnet.error.UnregisteredError" title="royalnet.error.UnregisteredError"><code class="xref py py-exc docutils literal notranslate"><span class="pre">royalnet.error.UnregisteredError</span></code></a> if this is True and the call has no author.</p>
</dd>
<dt class="field-even">Raises</dt>
<dd class="field-even"><p><a class="reference internal" href="#royalnet.error.UnregisteredError" title="royalnet.error.UnregisteredError"><strong>royalnet.error.UnregisteredError</strong></a> </p>
<dd class="field-odd"><p><strong>error_if_none</strong> Raise an exception if this is True and the call has no author.</p>
</dd>
</dl>
</dd></dl>
@ -749,7 +748,7 @@ That probably means, the database row identifying the user.</p>
<dd><p>Arguments can be accessed with an array notation, such as <code class="docutils literal notranslate"><span class="pre">args[0]</span></code>.</p>
<dl class="field-list simple">
<dt class="field-odd">Raises</dt>
<dd class="field-odd"><p><a class="reference internal" href="#royalnet.error.InvalidInputError" title="royalnet.error.InvalidInputError"><strong>royalnet.error.InvalidInputError</strong></a> if the requested argument does not exist.</p>
<dd class="field-odd"><p><strong>royalnet.error.InvalidInputError</strong> if the requested argument does not exist.</p>
</dd>
</dl>
</dd></dl>
@ -760,10 +759,10 @@ That probably means, the database row identifying the user.</p>
<dd><p>Get the arguments as a space-joined string.</p>
<dl class="field-list simple">
<dt class="field-odd">Parameters</dt>
<dd class="field-odd"><p><strong>require_at_least</strong> the minimum amount of arguments required, will raise <a class="reference internal" href="#royalnet.error.InvalidInputError" title="royalnet.error.InvalidInputError"><code class="xref py py-exc docutils literal notranslate"><span class="pre">royalnet.error.InvalidInputError</span></code></a> if the requirement is not fullfilled.</p>
<dd class="field-odd"><p><strong>require_at_least</strong> the minimum amount of arguments required, will raise <code class="xref py py-exc docutils literal notranslate"><span class="pre">royalnet.error.InvalidInputError</span></code> if the requirement is not fullfilled.</p>
</dd>
<dt class="field-even">Raises</dt>
<dd class="field-even"><p><a class="reference internal" href="#royalnet.error.InvalidInputError" title="royalnet.error.InvalidInputError"><strong>royalnet.error.InvalidInputError</strong></a> if there are less than <code class="docutils literal notranslate"><span class="pre">require_at_least</span></code> arguments.</p>
<dd class="field-even"><p><strong>royalnet.error.InvalidInputError</strong> if there are less than <code class="docutils literal notranslate"><span class="pre">require_at_least</span></code> arguments.</p>
</dd>
<dt class="field-odd">Returns</dt>
<dd class="field-odd"><p>The space-joined string.</p>
@ -780,7 +779,7 @@ That probably means, the database row identifying the user.</p>
<dd class="field-odd"><p><strong>pattern</strong> The regex pattern to be passed to <a class="reference external" href="https://docs.python.org/3.7/library/re.html#re.match" title="(in Python v3.7)"><code class="xref py py-func docutils literal notranslate"><span class="pre">re.match()</span></code></a>.</p>
</dd>
<dt class="field-even">Raises</dt>
<dd class="field-even"><p><a class="reference internal" href="#royalnet.error.InvalidInputError" title="royalnet.error.InvalidInputError"><strong>royalnet.error.InvalidInputError</strong></a> if the pattern doesnt match.</p>
<dd class="field-even"><p><strong>royalnet.error.InvalidInputError</strong> if the pattern doesnt match.</p>
</dd>
<dt class="field-odd">Returns</dt>
<dd class="field-odd"><p>The matched groups, as returned by <code class="xref py py-func docutils literal notranslate"><span class="pre">re.Match.groups()</span></code>.</p>
@ -807,6 +806,33 @@ That probably means, the database row identifying the user.</p>
</dd></dl>
<dl class="exception">
<dt id="royalnet.commands.CommandError">
<em class="property">exception </em><code class="sig-prename descclassname">royalnet.commands.</code><code class="sig-name descname">CommandError</code><span class="sig-paren">(</span><em class="sig-param">message=''</em><span class="sig-paren">)</span><a class="headerlink" href="#royalnet.commands.CommandError" title="Permalink to this definition"></a></dt>
<dd><p>Something went wrong during the execution of this command.</p>
<p>Display an error message to the user, explaining what went wrong.</p>
</dd></dl>
<dl class="exception">
<dt id="royalnet.commands.InvalidInputError">
<em class="property">exception </em><code class="sig-prename descclassname">royalnet.commands.</code><code class="sig-name descname">InvalidInputError</code><span class="sig-paren">(</span><em class="sig-param">message=''</em><span class="sig-paren">)</span><a class="headerlink" href="#royalnet.commands.InvalidInputError" title="Permalink to this definition"></a></dt>
<dd><p>The command has received invalid input and cannot complete.</p>
<p>Display an error message to the user, along with the correct syntax for the command.</p>
</dd></dl>
<dl class="exception">
<dt id="royalnet.commands.UnsupportedError">
<em class="property">exception </em><code class="sig-prename descclassname">royalnet.commands.</code><code class="sig-name descname">UnsupportedError</code><span class="sig-paren">(</span><em class="sig-param">message=''</em><span class="sig-paren">)</span><a class="headerlink" href="#royalnet.commands.UnsupportedError" title="Permalink to this definition"></a></dt>
<dd><p>A requested feature is not available on this interface.</p>
<p>Display an error message to the user, telling them to use another interface.</p>
</dd></dl>
<dl class="exception">
<dt id="royalnet.commands.KeyboardExpiredError">
<em class="property">exception </em><code class="sig-prename descclassname">royalnet.commands.</code><code class="sig-name descname">KeyboardExpiredError</code><span class="sig-paren">(</span><em class="sig-param">message=''</em><span class="sig-paren">)</span><a class="headerlink" href="#royalnet.commands.KeyboardExpiredError" title="Permalink to this definition"></a></dt>
<dd><p>A special type of exception that can be raised in keyboard handlers to mark a specific keyboard as expired.</p>
</dd></dl>
</div>
<div class="section" id="module-royalnet.database">
<span id="database"></span><h2>Database<a class="headerlink" href="#module-royalnet.database" title="Permalink to this headline"></a></h2>
@ -896,7 +922,7 @@ That probably means, the database row identifying the user.</p>
<dl class="method">
<dt id="royalnet.network.NetworkLink.run">
<em class="property">async </em><code class="sig-name descname">run</code><span class="sig-paren">(</span><em class="sig-param">loops: numbers.Real = inf</em><span class="sig-paren">)</span><a class="headerlink" href="#royalnet.network.NetworkLink.run" title="Permalink to this definition"></a></dt>
<em class="property">async </em><code class="sig-name descname">run</code><span class="sig-paren">(</span><span class="sig-paren">)</span><a class="headerlink" href="#royalnet.network.NetworkLink.run" title="Permalink to this definition"></a></dt>
<dd><p>Blockingly run the Link.</p>
</dd></dl>
@ -1031,19 +1057,14 @@ Contains info about the source and the destination.</p>
<dd><p>Executed every time a package is received and must be routed somewhere.</p>
</dd></dl>
<dl class="method">
<dt id="royalnet.network.NetworkServer.run">
<em class="property">async </em><code class="sig-name descname">run</code><span class="sig-paren">(</span><span class="sig-paren">)</span><a class="headerlink" href="#royalnet.network.NetworkServer.run" title="Permalink to this definition"></a></dt>
<dd></dd></dl>
<dl class="method">
<dt id="royalnet.network.NetworkServer.run_blocking">
<code class="sig-name descname">run_blocking</code><span class="sig-paren">(</span><span class="sig-paren">)</span><a class="headerlink" href="#royalnet.network.NetworkServer.run_blocking" title="Permalink to this definition"></a></dt>
<code class="sig-name descname">run_blocking</code><span class="sig-paren">(</span><em class="sig-param">verbose=False</em><span class="sig-paren">)</span><a class="headerlink" href="#royalnet.network.NetworkServer.run_blocking" title="Permalink to this definition"></a></dt>
<dd></dd></dl>
<dl class="method">
<dt id="royalnet.network.NetworkServer.serve">
<em class="property">async </em><code class="sig-name descname">serve</code><span class="sig-paren">(</span><span class="sig-paren">)</span><a class="headerlink" href="#royalnet.network.NetworkServer.serve" title="Permalink to this definition"></a></dt>
<code class="sig-name descname">serve</code><span class="sig-paren">(</span><span class="sig-paren">)</span><a class="headerlink" href="#royalnet.network.NetworkServer.serve" title="Permalink to this definition"></a></dt>
<dd></dd></dl>
</dd></dl>
@ -1347,48 +1368,6 @@ Use with caution?</p>
</div>
<div class="section" id="module-royalnet.error">
<span id="error"></span><h2>Error<a class="headerlink" href="#module-royalnet.error" title="Permalink to this headline"></a></h2>
<dl class="exception">
<dt id="royalnet.error.CurrentlyDisabledError">
<em class="property">exception </em><code class="sig-prename descclassname">royalnet.error.</code><code class="sig-name descname">CurrentlyDisabledError</code><a class="headerlink" href="#royalnet.error.CurrentlyDisabledError" title="Permalink to this definition"></a></dt>
<dd><p>This feature is temporarely disabled and is not available right now.</p>
</dd></dl>
<dl class="exception">
<dt id="royalnet.error.ExternalError">
<em class="property">exception </em><code class="sig-prename descclassname">royalnet.error.</code><code class="sig-name descname">ExternalError</code><a class="headerlink" href="#royalnet.error.ExternalError" title="Permalink to this definition"></a></dt>
<dd><p>Something went wrong in a non-Royalnet component and the command execution cannot be completed.</p>
</dd></dl>
<dl class="exception">
<dt id="royalnet.error.FileTooBigError">
<em class="property">exception </em><code class="sig-prename descclassname">royalnet.error.</code><code class="sig-name descname">FileTooBigError</code><a class="headerlink" href="#royalnet.error.FileTooBigError" title="Permalink to this definition"></a></dt>
<dd><p>The file to be downloaded would be too big to store; therefore, it has been skipped.</p>
</dd></dl>
<dl class="exception">
<dt id="royalnet.error.InvalidConfigError">
<em class="property">exception </em><code class="sig-prename descclassname">royalnet.error.</code><code class="sig-name descname">InvalidConfigError</code><a class="headerlink" href="#royalnet.error.InvalidConfigError" title="Permalink to this definition"></a></dt>
<dd><p>The bot has not been configured correctly, therefore the command can not function.</p>
</dd></dl>
<dl class="exception">
<dt id="royalnet.error.InvalidInputError">
<em class="property">exception </em><code class="sig-prename descclassname">royalnet.error.</code><code class="sig-name descname">InvalidInputError</code><a class="headerlink" href="#royalnet.error.InvalidInputError" title="Permalink to this definition"></a></dt>
<dd><p>The command has received invalid input and cannot complete.</p>
</dd></dl>
<dl class="exception">
<dt id="royalnet.error.KeyboardExpiredError">
<em class="property">exception </em><code class="sig-prename descclassname">royalnet.error.</code><code class="sig-name descname">KeyboardExpiredError</code><a class="headerlink" href="#royalnet.error.KeyboardExpiredError" title="Permalink to this definition"></a></dt>
<dd><p>A special type of exception that can be raised in keyboard handlers to mark a specific keyboard as expired.</p>
</dd></dl>
<dl class="exception">
<dt id="royalnet.error.NoneFoundError">
<em class="property">exception </em><code class="sig-prename descclassname">royalnet.error.</code><code class="sig-name descname">NoneFoundError</code><a class="headerlink" href="#royalnet.error.NoneFoundError" title="Permalink to this definition"></a></dt>
<dd><p>The element that was being looked for was not found.</p>
</dd></dl>
<dl class="exception">
<dt id="royalnet.error.RoyalnetRequestError">
<em class="property">exception </em><code class="sig-prename descclassname">royalnet.error.</code><code class="sig-name descname">RoyalnetRequestError</code><span class="sig-paren">(</span><em class="sig-param">error: ResponseError</em><span class="sig-paren">)</span><a class="headerlink" href="#royalnet.error.RoyalnetRequestError" title="Permalink to this definition"></a></dt>
@ -1407,24 +1386,6 @@ Use with caution?</p>
<dd><p>The <a class="reference internal" href="#royalnet.network.Response" title="royalnet.network.Response"><code class="xref py py-class docutils literal notranslate"><span class="pre">royalnet.network.Response</span></code></a> that was received is invalid.</p>
</dd></dl>
<dl class="exception">
<dt id="royalnet.error.TooManyFoundError">
<em class="property">exception </em><code class="sig-prename descclassname">royalnet.error.</code><code class="sig-name descname">TooManyFoundError</code><a class="headerlink" href="#royalnet.error.TooManyFoundError" title="Permalink to this definition"></a></dt>
<dd><p>Multiple elements matching the request were found, and only one was expected.</p>
</dd></dl>
<dl class="exception">
<dt id="royalnet.error.UnregisteredError">
<em class="property">exception </em><code class="sig-prename descclassname">royalnet.error.</code><code class="sig-name descname">UnregisteredError</code><a class="headerlink" href="#royalnet.error.UnregisteredError" title="Permalink to this definition"></a></dt>
<dd><p>The command required a registered user, and the user was not registered.</p>
</dd></dl>
<dl class="exception">
<dt id="royalnet.error.UnsupportedError">
<em class="property">exception </em><code class="sig-prename descclassname">royalnet.error.</code><code class="sig-name descname">UnsupportedError</code><a class="headerlink" href="#royalnet.error.UnsupportedError" title="Permalink to this definition"></a></dt>
<dd><p>The command is not supported for the specified interface.</p>
</dd></dl>
</div>
</div>

View file

@ -92,6 +92,7 @@
<li class="toctree-l3"><a class="reference internal" href="#regular-expressions">Regular expressions</a></li>
</ul>
</li>
<li class="toctree-l2"><a class="reference internal" href="#raising-errors">Raising errors</a></li>
<li class="toctree-l2"><a class="reference internal" href="#running-code-at-the-initialization-of-the-bot">Running code at the initialization of the bot</a></li>
<li class="toctree-l2"><a class="reference internal" href="#coroutines-and-slow-operations">Coroutines and slow operations</a></li>
<li class="toctree-l2"><a class="reference internal" href="#delete-the-invoking-message">Delete the invoking message</a></li>
@ -256,7 +257,7 @@ ones.</p>
<p>You can consider arguments as if they were separated by spaces.</p>
<p>You can then access command arguments directly by number as if the args object was a list of <a class="reference external" href="https://docs.python.org/3.7/library/stdtypes.html#str" title="(in Python v3.7)"><code class="xref py py-class docutils literal notranslate"><span class="pre">str</span></code></a>.</p>
<p>If you request an argument with a certain number, but the argument does not exist, an
<a class="reference internal" href="apireference.html#royalnet.error.InvalidInputError" title="royalnet.error.InvalidInputError"><code class="xref py py-exc docutils literal notranslate"><span class="pre">royalnet.error.InvalidInputError</span></code></a> is raised, making the arguments accessed in this way <strong>required</strong>.</p>
<code class="xref py py-exc docutils literal notranslate"><span class="pre">royalnet.error.InvalidInputError</span></code> is raised, making the arguments accessed in this way <strong>required</strong>.</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="n">args</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span>
<span class="c1"># &quot;carbonara&quot;</span>
@ -295,7 +296,7 @@ will return <code class="xref py py-const docutils literal notranslate"><span cl
<span class="c1"># &quot;carbonara al-dente&quot;</span>
</pre></div>
</div>
<p>You can specify a minimum number of arguments too, so that an <a class="reference internal" href="apireference.html#royalnet.error.InvalidInputError" title="royalnet.error.InvalidInputError"><code class="xref py py-exc docutils literal notranslate"><span class="pre">royalnet.error.InvalidInputError</span></code></a> will be
<p>You can specify a minimum number of arguments too, so that an <a class="reference internal" href="apireference.html#royalnet.commands.InvalidInputError" title="royalnet.commands.InvalidInputError"><code class="xref py py-exc docutils literal notranslate"><span class="pre">InvalidInputError</span></code></a> will be
raised if not enough arguments are present:</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="n">args</span><span class="o">.</span><span class="n">joined</span><span class="p">(</span><span class="n">require_at_least</span><span class="o">=</span><span class="mi">3</span><span class="p">)</span>
<span class="c1"># InvalidInputError() is raised</span>
@ -306,7 +307,7 @@ raised if not enough arguments are present:</p>
<h3>Regular expressions<a class="headerlink" href="#regular-expressions" title="Permalink to this headline"></a></h3>
<p>For more complex commands, you may want to get arguments through <a class="reference external" href="https://regexr.com/">regular expressions</a>.</p>
<p>You can then use the <a class="reference internal" href="apireference.html#royalnet.commands.CommandArgs.match" title="royalnet.commands.CommandArgs.match"><code class="xref py py-meth docutils literal notranslate"><span class="pre">CommandArgs.match()</span></code></a> method, which tries to match a pattern to the command argument string,
which returns a tuple of the matched groups and raises an <a class="reference internal" href="apireference.html#royalnet.error.InvalidInputError" title="royalnet.error.InvalidInputError"><code class="xref py py-exc docutils literal notranslate"><span class="pre">royalnet.error.InvalidInputError</span></code></a> if there is no match.</p>
which returns a tuple of the matched groups and raises an <a class="reference internal" href="apireference.html#royalnet.commands.InvalidInputError" title="royalnet.commands.InvalidInputError"><code class="xref py py-exc docutils literal notranslate"><span class="pre">InvalidInputError</span></code></a> if there is no match.</p>
<p>To match a pattern, <a class="reference external" href="https://docs.python.org/3.7/library/re.html#re.match" title="(in Python v3.7)"><code class="xref py py-func docutils literal notranslate"><span class="pre">re.match()</span></code></a> is used, meaning that Python will try to match only at the beginning of the string.</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="n">args</span><span class="o">.</span><span class="n">match</span><span class="p">(</span><span class="sa">r</span><span class="s2">&quot;(carb\w+)&quot;</span><span class="p">)</span>
<span class="c1"># (&quot;carbonara&quot;,)</span>
@ -323,6 +324,25 @@ which returns a tuple of the matched groups and raises an <a class="reference in
</div>
</div>
</div>
<div class="section" id="raising-errors">
<h2>Raising errors<a class="headerlink" href="#raising-errors" title="Permalink to this headline"></a></h2>
<p>If you want to display an error message to the user, you can raise a <a class="reference internal" href="apireference.html#royalnet.commands.CommandError" title="royalnet.commands.CommandError"><code class="xref py py-exc docutils literal notranslate"><span class="pre">CommandError</span></code></a> using the error message as argument:</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="k">if</span> <span class="ow">not</span> <span class="n">kitchen</span><span class="o">.</span><span class="n">is_open</span><span class="p">():</span>
<span class="k">raise</span> <span class="n">CommandError</span><span class="p">(</span><span class="s2">&quot;The kitchen is closed. Come back later!&quot;</span><span class="p">)</span>
</pre></div>
</div>
<p>You can also manually raise <a class="reference internal" href="apireference.html#royalnet.commands.InvalidInputError" title="royalnet.commands.InvalidInputError"><code class="xref py py-exc docutils literal notranslate"><span class="pre">InvalidInputError</span></code></a> to redisplay the command syntax, along with your error message:</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="k">if</span> <span class="n">args</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">allowed_pasta</span><span class="p">:</span>
<span class="k">raise</span> <span class="n">InvalidInputError</span><span class="p">(</span><span class="s2">&quot;The specified pasta type is invalid.&quot;</span><span class="p">)</span>
</pre></div>
</div>
<p>If you need a Royalnet feature thats not available on the current interface, you can raise an
<a class="reference internal" href="apireference.html#royalnet.commands.UnsupportedError" title="royalnet.commands.UnsupportedError"><code class="xref py py-exc docutils literal notranslate"><span class="pre">UnsupportedError</span></code></a> with a brief description of whats missing:</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="k">if</span> <span class="n">interface</span><span class="o">.</span><span class="n">name</span> <span class="o">!=</span> <span class="s2">&quot;telegram&quot;</span><span class="p">:</span>
<span class="k">raise</span> <span class="n">UnsupportedError</span><span class="p">(</span><span class="s2">&quot;This command can only be run on Telegram interfaces.&quot;</span><span class="p">)</span>
</pre></div>
</div>
</div>
<div class="section" id="running-code-at-the-initialization-of-the-bot">
<h2>Running code at the initialization of the bot<a class="headerlink" href="#running-code-at-the-initialization-of-the-bot" title="Permalink to this headline"></a></h2>
<p>You can run code while the bot is starting by overriding the <code class="xref py py-meth docutils literal notranslate"><span class="pre">Command.__init__()</span></code> function.</p>

View file

@ -155,7 +155,6 @@
| <a href="#B"><strong>B</strong></a>
| <a href="#C"><strong>C</strong></a>
| <a href="#D"><strong>D</strong></a>
| <a href="#E"><strong>E</strong></a>
| <a href="#F"><strong>F</strong></a>
| <a href="#G"><strong>G</strong></a>
| <a href="#H"><strong>H</strong></a>
@ -260,10 +259,12 @@
<li><a href="apireference.html#royalnet.bots.DiscordBot.advance_music_data">advance_music_data() (royalnet.bots.DiscordBot method)</a>
</li>
<li><a href="apireference.html#royalnet.database.Alchemy">Alchemy (class in royalnet.database)</a>
</li>
<li><a href="apireference.html#royalnet.commands.CommandInterface.alchemy">alchemy (royalnet.commands.CommandInterface attribute)</a>
</li>
</ul></td>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="apireference.html#royalnet.commands.CommandInterface.alchemy">alchemy (royalnet.commands.CommandInterface attribute)</a>
<li><a href="apireference.html#royalnet.commands.Command.aliases">aliases (royalnet.commands.Command attribute)</a>
</li>
<li><a href="apireference.html#royalnet.utils.andformat">andformat() (in module royalnet.utils)</a>
</li>
@ -292,25 +293,23 @@
<li><a href="apireference.html#royalnet.commands.CommandArgs">CommandArgs (class in royalnet.commands)</a>
</li>
<li><a href="apireference.html#royalnet.commands.CommandData">CommandData (class in royalnet.commands)</a>
</li>
<li><a href="apireference.html#royalnet.commands.CommandError">CommandError</a>
</li>
<li><a href="apireference.html#royalnet.commands.CommandInterface">CommandInterface (class in royalnet.commands)</a>
</li>
<li><a href="apireference.html#royalnet.network.NetworkLink.connect">connect() (royalnet.network.NetworkLink method)</a>
</li>
<li><a href="apireference.html#royalnet.network.ConnectionClosedError">ConnectionClosedError</a>
</li>
</ul></td>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="apireference.html#royalnet.network.ConnectionClosedError">ConnectionClosedError</a>
</li>
<li><a href="apireference.html#royalnet.audio.YtdlMp3.convert_to_mp3">convert_to_mp3() (royalnet.audio.YtdlMp3 method)</a>
</li>
<li><a href="apireference.html#royalnet.audio.YtdlDiscord.convert_to_pcm">convert_to_pcm() (royalnet.audio.YtdlDiscord method)</a>
</li>
<li><a href="apireference.html#royalnet.audio.YtdlDiscord.create_and_ready_from_url">create_and_ready_from_url() (royalnet.audio.YtdlDiscord class method)</a>
<ul>
<li><a href="apireference.html#royalnet.audio.YtdlMp3.create_and_ready_from_url">(royalnet.audio.YtdlMp3 class method)</a>
<li><a href="apireference.html#royalnet.audio.YtdlMp3.create_and_ready_from_url">create_and_ready_from_url() (royalnet.audio.YtdlMp3 class method)</a>
</li>
</ul></li>
<li><a href="apireference.html#royalnet.web.create_app">create_app() (in module royalnet.web)</a>
</li>
<li><a href="apireference.html#royalnet.audio.YtdlDiscord.create_from_url">create_from_url() (royalnet.audio.YtdlDiscord class method)</a>
@ -319,8 +318,6 @@
<li><a href="apireference.html#royalnet.audio.YtdlMp3.create_from_url">(royalnet.audio.YtdlMp3 class method)</a>
</li>
</ul></li>
<li><a href="apireference.html#royalnet.error.CurrentlyDisabledError">CurrentlyDisabledError</a>
</li>
</ul></td>
</tr></table>
@ -354,22 +351,12 @@
</ul></td>
</tr></table>
<h2 id="E">E</h2>
<table style="width: 100%" class="indextable genindextable"><tr>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="apireference.html#royalnet.error.ExternalError">ExternalError</a>
</li>
</ul></td>
</tr></table>
<h2 id="F">F</h2>
<table style="width: 100%" class="indextable genindextable"><tr>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="apireference.html#royalnet.audio.FileAudioSource">FileAudioSource (class in royalnet.audio)</a>
</li>
<li><a href="apireference.html#royalnet.utils.fileformat">fileformat() (in module royalnet.utils)</a>
</li>
<li><a href="apireference.html#royalnet.error.FileTooBigError">FileTooBigError</a>
</li>
<li><a href="apireference.html#royalnet.network.NetworkServer.find_client">find_client() (royalnet.network.NetworkServer method)</a>
</li>
@ -435,9 +422,7 @@
</ul></li>
</ul></td>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="apireference.html#royalnet.error.InvalidConfigError">InvalidConfigError</a>
</li>
<li><a href="apireference.html#royalnet.error.InvalidInputError">InvalidInputError</a>
<li><a href="apireference.html#royalnet.commands.InvalidInputError">InvalidInputError</a>
</li>
<li><a href="apireference.html#royalnet.audio.YtdlFile.is_downloaded">is_downloaded() (royalnet.audio.YtdlFile method)</a>
</li>
@ -461,7 +446,7 @@
</li>
</ul></td>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="apireference.html#royalnet.error.KeyboardExpiredError">KeyboardExpiredError</a>
<li><a href="apireference.html#royalnet.commands.KeyboardExpiredError">KeyboardExpiredError</a>
</li>
</ul></td>
</tr></table>
@ -504,16 +489,14 @@
<li><a href="apireference.html#royalnet.network.NetworkConfig">NetworkConfig (class in royalnet.network)</a>
</li>
<li><a href="apireference.html#royalnet.network.NetworkError">NetworkError</a>
</li>
<li><a href="apireference.html#royalnet.utils.NetworkHandler">NetworkHandler (class in royalnet.utils)</a>
</li>
</ul></td>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="apireference.html#royalnet.utils.NetworkHandler">NetworkHandler (class in royalnet.utils)</a>
</li>
<li><a href="apireference.html#royalnet.network.NetworkLink">NetworkLink (class in royalnet.network)</a>
</li>
<li><a href="apireference.html#royalnet.network.NetworkServer">NetworkServer (class in royalnet.network)</a>
</li>
<li><a href="apireference.html#royalnet.error.NoneFoundError">NoneFoundError</a>
</li>
<li><a href="apireference.html#royalnet.network.NotConnectedError">NotConnectedError</a>
</li>
@ -643,8 +626,6 @@
<li><a href="apireference.html#royalnet.commands.Command.run">(royalnet.commands.Command method)</a>
</li>
<li><a href="apireference.html#royalnet.network.NetworkLink.run">(royalnet.network.NetworkLink method)</a>
</li>
<li><a href="apireference.html#royalnet.network.NetworkServer.run">(royalnet.network.NetworkServer method)</a>
</li>
</ul></li>
<li><a href="apireference.html#royalnet.bots.GenericBot.run_blocking">run_blocking() (royalnet.bots.GenericBot method)</a>
@ -708,8 +689,6 @@
<li><a href="apireference.html#royalnet.network.Package.to_json_bytes">to_json_bytes() (royalnet.network.Package method)</a>
</li>
<li><a href="apireference.html#royalnet.network.Package.to_json_string">to_json_string() (royalnet.network.Package method)</a>
</li>
<li><a href="apireference.html#royalnet.error.TooManyFoundError">TooManyFoundError</a>
</li>
</ul></td>
</tr></table>
@ -720,12 +699,10 @@
<li><a href="apireference.html#royalnet.commands.CommandInterface.unregister_keyboard_key">unregister_keyboard_key() (royalnet.commands.CommandInterface method)</a>
</li>
<li><a href="apireference.html#royalnet.commands.CommandInterface.unregister_net_handler">unregister_net_handler() (royalnet.commands.CommandInterface method)</a>
</li>
<li><a href="apireference.html#royalnet.error.UnregisteredError">UnregisteredError</a>
</li>
</ul></td>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="apireference.html#royalnet.error.UnsupportedError">UnsupportedError</a>
<li><a href="apireference.html#royalnet.commands.UnsupportedError">UnsupportedError</a>
</li>
<li><a href="apireference.html#royalnet.bots.DiscordBot.update_activity_with_source_title">update_activity_with_source_title() (royalnet.bots.DiscordBot method)</a>
</li>

View file

@ -153,7 +153,11 @@
<p>Welcome to the documentation of Royalnet!</p>
<div class="toctree-wrapper compound">
<ul>
<li class="toctree-l1"><a class="reference internal" href="runningroyalnet.html">Running Royalnet</a></li>
<li class="toctree-l1"><a class="reference internal" href="runningroyalnet.html">Running Royalnet</a><ul>
<li class="toctree-l2"><a class="reference internal" href="runningroyalnet.html#the-keyring">The Keyring</a></li>
<li class="toctree-l2"><a class="reference internal" href="runningroyalnet.html#running-the-bots">Running the bots</a></li>
</ul>
</li>
<li class="toctree-l1"><a class="reference internal" href="creatingacommand.html">Royalnet Commands</a><ul>
<li class="toctree-l2"><a class="reference internal" href="creatingacommand.html#creating-a-new-command">Creating a new Command</a></li>
<li class="toctree-l2"><a class="reference internal" href="creatingacommand.html#command-arguments">Command arguments</a><ul>
@ -163,6 +167,7 @@
<li class="toctree-l3"><a class="reference internal" href="creatingacommand.html#regular-expressions">Regular expressions</a></li>
</ul>
</li>
<li class="toctree-l2"><a class="reference internal" href="creatingacommand.html#raising-errors">Raising errors</a></li>
<li class="toctree-l2"><a class="reference internal" href="creatingacommand.html#running-code-at-the-initialization-of-the-bot">Running code at the initialization of the bot</a></li>
<li class="toctree-l2"><a class="reference internal" href="creatingacommand.html#coroutines-and-slow-operations">Coroutines and slow operations</a></li>
<li class="toctree-l2"><a class="reference internal" href="creatingacommand.html#delete-the-invoking-message">Delete the invoking message</a></li>

Binary file not shown.

View file

@ -82,7 +82,11 @@
<ul class="current">
<li class="toctree-l1 current"><a class="current reference internal" href="#">Running Royalnet</a></li>
<li class="toctree-l1 current"><a class="current reference internal" href="#">Running Royalnet</a><ul>
<li class="toctree-l2"><a class="reference internal" href="#the-keyring">The Keyring</a></li>
<li class="toctree-l2"><a class="reference internal" href="#running-the-bots">Running the bots</a></li>
</ul>
</li>
<li class="toctree-l1"><a class="reference internal" href="creatingacommand.html">Royalnet Commands</a></li>
<li class="toctree-l1"><a class="reference internal" href="apireference.html">API Reference</a></li>
</ul>
@ -151,7 +155,56 @@
<div class="section" id="running-royalnet">
<h1>Running Royalnet<a class="headerlink" href="#running-royalnet" title="Permalink to this headline"></a></h1>
<p>This documentation page hasnt been written yet, please refer to the README until then.</p>
<p>To run a <code class="docutils literal notranslate"><span class="pre">royalnet</span></code> instance, you have first to download the package from <code class="docutils literal notranslate"><span class="pre">pip</span></code>:</p>
<div class="section" id="the-keyring">
<h2>The Keyring<a class="headerlink" href="#the-keyring" title="Permalink to this headline"></a></h2>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="n">pip</span> <span class="n">install</span> <span class="n">royalnet</span>
</pre></div>
</div>
<p>To run <code class="docutils literal notranslate"><span class="pre">royalnet</span></code>, youll have to setup the system keyring.</p>
<p>On Windows and desktop Linux, this is already configured;
on a headless Linux instance, youll need to <a class="reference external" href="https://keyring.readthedocs.io/en/latest/#using-keyring-on-headless-linux-systems">manually start and unlock the keyring daemon</a>.</p>
<p>Now you have to create a new <code class="docutils literal notranslate"><span class="pre">royalnet</span></code> configuration. Start the configuration wizard:</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="n">python</span> <span class="o">-</span><span class="n">m</span> <span class="n">royalnet</span><span class="o">.</span><span class="n">configurator</span>
</pre></div>
</div>
<p>Youll 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 youll be able to remember.</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="n">Desired</span> <span class="n">secrets</span> <span class="n">name</span> <span class="p">[</span><span class="n">__default__</span><span class="p">]:</span> <span class="n">royalgames</span>
</pre></div>
</div>
<p>Youll then be asked for a network password.</p>
<p>This password is used to connect to the rest of the <a class="reference internal" href="apireference.html#module-royalnet.network" title="royalnet.network"><code class="xref py py-mod docutils literal notranslate"><span class="pre">royalnet.network</span></code></a>, or, if youre hosting a local Network,
it will be the necessary password to connect to it:</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="n">Network</span> <span class="n">password</span> <span class="p">[]:</span> <span class="n">cosafaunapesuunafoglia</span>
</pre></div>
</div>
<p>Then youll be asked for a Telegram Bot API token.
You can get one from <a class="reference external" href="https://t.me/BotFather">&#64;BotFather</a>.</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="n">Telegram</span> <span class="n">Bot</span> <span class="n">API</span> <span class="n">token</span> <span class="p">[]:</span> <span class="mi">000000000</span><span class="p">:</span><span class="n">AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA</span>
</pre></div>
</div>
<p>The next prompt will ask for a Discord Bot API token.
You can get one at the <a class="reference external" href="https://discordapp.com/developers/applications/">Discord Developers Portal</a>.</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="n">Discord</span> <span class="n">Bot</span> <span class="n">API</span> <span class="n">token</span> <span class="p">[]:</span> <span class="n">AAAAAAAAAAAAAAAAAAAAAAAA</span><span class="o">.</span><span class="n">AAAAAA</span><span class="o">.</span><span class="n">AAAAAAAAAAAAAAAAAAAAAAAAAAA</span>
</pre></div>
</div>
<p>Now the configurator will ask you for a Imgur API token.
<a class="reference external" href="https://api.imgur.com/oauth2/addclient">Register an application</a> on Imgur to be supplied one.
The token should be of type “anonymous usage without user authorization”.</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="n">Imgur</span> <span class="n">API</span> <span class="n">token</span> <span class="p">[]:</span> <span class="n">aaaaaaaaaaaaaaa</span>
</pre></div>
</div>
<p>Next, youll be asked for a Sentry DSN. You probably wont have one, so just ignore it and press enter.</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="n">Sentry</span> <span class="n">DSN</span> <span class="p">[]:</span>
</pre></div>
</div>
<p>Now that all tokens are configured, youre ready to launch the bot!</p>
</div>
<div class="section" id="running-the-bots">
<h2>Running the bots<a class="headerlink" href="#running-the-bots" title="Permalink to this headline"></a></h2>
<p>TODO</p>
</div>
</div>

File diff suppressed because one or more lines are too long

View file

@ -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 <https://regexr.com/>`_.
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
---------------------------------------------

View file

@ -3,4 +3,60 @@
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
<https://keyring.readthedocs.io/en/latest/#using-keyring-on-headless-linux-systems>`_.
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 <https://t.me/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 <https://discordapp.com/developers/applications/>`_. ::
Discord Bot API token []: AAAAAAAAAAAAAAAAAAAAAAAA.AAAAAA.AAAAAAAAAAAAAAAAAAAAAAAAAAA
Now the configurator will ask you for a Imgur API token.
`Register an application <https://api.imgur.com/oauth2/addclient>`_ 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

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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,

View file

@ -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,12 +17,13 @@ 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")
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)
@ -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)

View file

@ -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"]

View file

@ -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__)

View file

@ -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,

View file

@ -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"])}'

View file

@ -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: <https://discordapp.com/channels/{channel.guild.id}/{channel.id}>\n[b]Attenzione: la modalità video non funziona su Discord per Android e iOS![/b]")

View file

@ -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

View file

@ -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]

View file

@ -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:

View file

@ -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.")

View file

@ -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()

View file

@ -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:

View file

@ -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()

View file

@ -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]")

View file

@ -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()

View file

@ -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]")

View file

@ -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: