Skip to content

Commit

Permalink
core: perform commands on connect
Browse files Browse the repository at this point in the history
Adds a setting in `core` called `commands_on_connect` based on the updated
`ListAttribute` from sopel-irc#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 sopel-irc#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 sopel-irc#1628 is
accepted, since it will change the `ListAttribute` delimiter from
commas to newlines.

Closes sopel-irc#1455
  • Loading branch information
HumorBaby authored and Exirel committed Dec 10, 2019
1 parent 684a848 commit 9889163
Show file tree
Hide file tree
Showing 4 changed files with 104 additions and 0 deletions.
40 changes: 40 additions & 0 deletions docs/source/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
==============

Expand Down
17 changes: 17 additions & 0 deletions sopel/config/core_section.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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.
Expand Down
22 changes: 22 additions & 0 deletions sopel/coretasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down
25 changes: 25 additions & 0 deletions test/test_coretasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)

0 comments on commit 9889163

Please sign in to comment.