From 9889163e755899de41ff127f4be2f58b3d86bbd3 Mon Sep 17 00:00:00 2001 From: Humorous Baby Date: Sat, 30 Mar 2019 02:27:40 -0400 Subject: [PATCH] core: perform commands on connect Adds a setting in `core` called `commands_on_connect` based on the updated `ListAttribute` from #1460. This setting stores a comma separated list of raw IRC commands (`\`-escaped commas in commands) to execute upon successful connection to the server. As @dgw put it, "Think ZNC's perform module, but without the ability to add/remove/rearrange lines from an IRC query" in #1455. The commands in the `commands_on_connect` list are executed at the end of the `startup` procedure. They can also be called by a bot admin with the `.execute` command. Note: two `TODO`s were added to adjust docstrings and docs once #1628 is accepted, since it will change the `ListAttribute` delimiter from commas to newlines. Closes #1455 --- docs/source/configuration.rst | 40 +++++++++++++++++++++++++++++++++++ sopel/config/core_section.py | 17 +++++++++++++++ sopel/coretasks.py | 22 +++++++++++++++++++ test/test_coretasks.py | 25 ++++++++++++++++++++++ 4 files changed, 104 insertions(+) diff --git a/docs/source/configuration.rst b/docs/source/configuration.rst index 3a1c8ac167..0fe2eb94c9 100644 --- a/docs/source/configuration.rst +++ b/docs/source/configuration.rst @@ -223,7 +223,47 @@ policy. options have been added: ``flood_burst_lines``, ``flood_empty_wait``, and ``flood_refill_rate``. +Perform commands on connect +--------------------------- +The bot can be configured to send custom commands upon successful connection to +the IRC server. This can be used in situations where the bot's built-in +capabilities are not sufficient, or further automation is desired. The list of +commands to send is set with :attr:`~CoreSection.commands_on_connect`. + +For example, the following configuration: + +.. code-block:: ini + + [core] + commands_on_connect = PRIVMSG X@Channels.undernet.org :LOGIN MyUserName A$_Strong\,*pasSWord,PRIVMSG IDLEBOT :login IdleUsername idLEPasswoRD + + +will, upon connection: + + 1) identify to Undernet services + 2) login with ``IDLEBOT`` + +.. important:: + + Commas are used to delimit separate commands, so any comma found within a + command must be escaped with ``\``. In the example above, the password + ``A$_Strong,*pasSWord`` is escaped as ``A$_Strong\,pasSWord`` (note the + escaped comma in the middle of the password, but not immediately following, + which is delimiting the next command). + + No other text needs to be escaped. + +.. + TODO: update this note (and the example config) once #1628 is merged in, + changing the delimiter to newlines (from commas). + +.. seealso:: + + This functionality is analogous to ZNC's ``perform`` module: + https://wiki.znc.in/Perform + + Authentication ============== diff --git a/sopel/config/core_section.py b/sopel/config/core_section.py index 8ec1bf95a0..32d6da75bd 100644 --- a/sopel/config/core_section.py +++ b/sopel/config/core_section.py @@ -49,6 +49,10 @@ def configure(config): 'channels', 'Enter the channels to connect to at startup, separated by commas.' ) + config.core.configure_setting( + 'commands_on_connect', + 'Enter commands to perform on successful connection to server (one per \'?\' prompt).' + ) class CoreSection(StaticSection): @@ -347,6 +351,19 @@ def homedir(self): capabilities. """ + commands_on_connect = ListAttribute('commands_on_connect') + r"""A list of commands to perform upon successful connection to IRC server. + + When entered using the config wizard, commas will be escaped automatically. + Otherwise, commas must be escaped, e.g.: ``PRIVMSG Q@CServe.quakenet.org + :AUTH my_username MyPassword\,HasAComma@#$%!`` Nothing else needs to be + escaped. + + .. versionadded:: 7.0 + """ + # TODO: update the docstring above in/after #1628 removes commas as + # delimiters for `ListAttribute`s. + pid_dir = FilenameAttribute('pid_dir', directory=True, default='.') """The directory in which to put the file Sopel uses to track its process ID. diff --git a/sopel/coretasks.py b/sopel/coretasks.py index dbe9f69f31..22f656170f 100644 --- a/sopel/coretasks.py +++ b/sopel/coretasks.py @@ -62,6 +62,26 @@ def auth_after_register(bot): auth_target or 'UserServ') +def execute_perform(bot): + """Execute commands specified to perform on IRC server connect.""" + if not bot.connection_registered: + # How did you even get this command, bot? + raise Exception('Bot must be connected to server to perform commands.') + + LOGGER.debug('{} commands to execute:'.format(len(bot.config.core.commands_on_connect))) + for i, command in enumerate(bot.config.core.commands_on_connect): + LOGGER.debug(command) + bot.write((command,)) + + +@sopel.module.require_privmsg("This command only works as a private message.") +@sopel.module.require_admin("This command requires admin privileges.") +@sopel.module.commands('execute') +def _execute_perform(bot, trigger): + """Execute commands specified to perform on IRC server connect.""" + execute_perform(bot) + + @sopel.module.event(events.RPL_WELCOME, events.RPL_LUSERCLIENT) @sopel.module.thread(False) @sopel.module.unblockable @@ -111,6 +131,8 @@ def startup(bot, trigger): ).format(bot.config.core.help_prefix) bot.say(msg, bot.config.core.owner) + execute_perform(bot) + @sopel.module.require_privmsg() @sopel.module.require_owner() diff --git a/test/test_coretasks.py b/test/test_coretasks.py index 00da15a061..2de4bd2f08 100644 --- a/test/test_coretasks.py +++ b/test/test_coretasks.py @@ -4,6 +4,7 @@ import pytest +from sopel import coretasks from sopel.module import VOICE, HALFOP, OP, ADMIN, OWNER from sopel.tools import Identifier from sopel.tests import rawlist @@ -111,3 +112,27 @@ def test_mode_colon(mockbot, ircfactory): assert mockbot.channels["#test"].privileges[Identifier("Uvoice")] == VOICE assert mockbot.channels["#test"].privileges[Identifier("Uadmin")] == ADMIN + + +def test_execute_perform_raise_not_connected(mockbot): + """Ensure bot will not execute ``commands_on_connect`` unless connected.""" + with pytest.raises(Exception): + coretasks.execute_perform(mockbot) + + +def test_execute_perform_send_commands(mockbot): + """Ensure bot sends ``commands_on_connect`` as specified in config.""" + commands = [ + # Example command for identifying to services on Undernet + 'PRIVMSG X@Channels.undernet.org :LOGIN my_username my_password', + # Set modes on connect + 'MODE some_nick +Xx', + # Oper on connect + 'OPER oper_username oper_password', + ] + + mockbot.config.core.commands_on_connect = commands + mockbot.connection_registered = True + + coretasks.execute_perform(mockbot) + assert mockbot.backend.message_sent == rawlist(*commands)