diff --git a/docs/source/plugin.rst b/docs/source/plugin.rst index 44847c954..3ddc83009 100644 --- a/docs/source/plugin.rst +++ b/docs/source/plugin.rst @@ -6,6 +6,7 @@ Plugins: Developer Overview :titlesonly: plugin/what + plugin/tutorials plugin/anatomy plugin/bot plugin/time diff --git a/docs/source/plugin/tutorials.rst b/docs/source/plugin/tutorials.rst new file mode 100644 index 000000000..67a7d8b37 --- /dev/null +++ b/docs/source/plugin/tutorials.rst @@ -0,0 +1,37 @@ +========= +Tutorials +========= + +The key feature of Sopel is its plugin system: *everything* Sopel does is +through a plugin. Combining some basic Python knowledge with reading Sopel's +documentation, you can write a plugin too! + +These tutorials will guide and help you to begin your journey as a plugin +author, i.e. someone who can write plugins for Sopel. Not every plugin is +easy however, and you will probably need to hone your Python skills, learn more +about the IRC protocol, and learn more about software programming in general. +But let's not get ahead of ourselves; you are here for the basics. + +.. toctree:: + :titlesonly: + + tutorials/first-plugin + tutorials/playing-with-commands + tutorials/configuration-and-setup + + +Requirements +============ + +Before you can dive into these tutorials, you will need the following: + +* to install and run Sopel on your development environment +* to have write access to Sopel's configuration and plugin directory +* a beginner level in Python (e.g. how to write a function, what is a variable, + how to perform string formatting, how to access an object's attributes, how + to import a module, etc.) + +Since you'll be running Sopel, we invite you to create a configuration file +that connects to a friendly IRC server and joins a private testing channel. +That way, when you restart your bot or run your command for the hundredth +time, you won't spam other users. diff --git a/docs/source/plugin/tutorials/configuration-and-setup.rst b/docs/source/plugin/tutorials/configuration-and-setup.rst new file mode 100644 index 000000000..f5d5156bd --- /dev/null +++ b/docs/source/plugin/tutorials/configuration-and-setup.rst @@ -0,0 +1,113 @@ +============================== +Configuration and plugin setup +============================== + +Maybe you :doc:`played with commands ` for your +plugin and now you want to make your plugin configurable. If you run an +instance of Sopel yourself, you probably had to open and edit its +:doc:`configuration` file. + +Usually located in the ``.sopel/`` folder under your home directory, the +configuration file is an INI file with sections defined by Sopel's core and by +plugins. In this tutorial, let's see how to declare and use a configuration +section dedicated to your plugin. + + +Declaring your configuration +============================ + +To declare a configuration section, you must first create a subclass of +:class:`~sopel.config.types.StaticSection`, and define attributes:: + + from sopel.config import types + + class MyPluginSection(types.StaticSection): + fruits = types.ListAttribute('fruits') + + +Telling Sopel about it +====================== + +Now, having a class in your plugin doesn't achieve much: you need to tell the +bot about it by using the :meth:`~sopel.config.Config.define_section` method. +The best place to do so is in the :func:`setup` function hook of your plugin:: + + def setup(bot): + bot.settings.define_section('myplugin', MyPluginSection) + +This way, you tell Sopel that the ``[myplugin]`` section in the **configuration +file** is used by your plugin, and to parse this section Sopel must use your +class, i.e. ``MyPluginSection``. + + +Using your section +================== + +Now that you have told Sopel about your custom section, you can add the +following lines in your configuration file: + +.. code-block:: ini + + [myplugin] + fruits = + banana + apple + peach + strawberry + +And Sopel will expose that for you through ``bot.settings.myplugin``. For +example, you can write this command:: + + import random + + @plugin.command('fruits') + def fruits(bot, trigger): + fruit = random.choice(bot.settings.myplugin.fruits) + bot.say(f'I want a {fruit}!') + +And whenever someone triggers this command, the bot will say that it wants one +of the configured fruits. If you want to list 50 fruits or only 2 is up to you, +and to the bot owners who will install your plugin. + + +Putting everything together +=========================== + +We can combine all of this into one plugin file, located at the same place as +before (``~/.sopel/plugins/myplugin.py``, assuming the default location):: + + import random + from sopel.config import types + + + class MyPluginSection(types.StaticSection): + """Declaration of your plugin's configuration.""" + fruits = types.ListAttribute('fruits') + + + def setup(bot): + """Telling the bot about the plugin's configuration.""" + bot.settings.define_section('myplugin', MyPluginSection) + + + @plugin.command('fruits') + def fruits(bot, trigger): + """Using the plugin's configuration in our command.""" + fruit = random.choice(bot.settings.myplugin.fruits) + bot.say(f'I want a {fruit}!') + +As you can see, there are **several steps** when it comes to configuration: + +* creating a class to represent your configuration section +* telling Sopel about it in a ``setup`` function +* using your plugin's configuration in your plugin + +Sopel tries to make it as straightforward and flexible as possible for you to +declare and to setup your plugin configuration, and you can read more about +:ref:`plugin configuration `, +which includes a section about the configuration wizard as well. You can also +see Sopel's own configuration in +:doc:`the configuration chapter `. + +Once you are familiar with the concept, you can also read deeper into the +reference documentation for the :mod:`sopel.config` module. diff --git a/docs/source/plugin/tutorials/first-plugin.rst b/docs/source/plugin/tutorials/first-plugin.rst new file mode 100644 index 000000000..5fbdf83d6 --- /dev/null +++ b/docs/source/plugin/tutorials/first-plugin.rst @@ -0,0 +1,52 @@ +================= +Your first plugin +================= + +Sopel's most interesting features come from its plugins, either published by +Sopel's developers or by third-party developers, and you can write your own +plugins. But where do you start? + +Here is a very short example of code for your first plugin that contains one +and only one command:: + + from sopel import plugin + + @plugin.command('hello') + def hello(bot, trigger): + """Reply with Hello!""" + bot.reply('Hello!') + +You can put this code in a Python file, placed into your Sopel plugin +directory, such as ``~/.sopel/plugins/myplugin.py``. Once this is done, you can +check if the bot can see the plugin, by using the ``sopel-plugins`` command +line tool:: + + $ sopel-plugins show myplugin + Plugin: myplugin + Status: enabled + Type: python-file + Source: /path/to/home/.sopel/plugins/myplugin.py + Label: myplugin plugin + Loaded successfully + Setup: no + Shutdown: no + Configure: no + +Notice how the filename (without the extension) is also the name of the plugin: +if you were to name your file ``hello.py``, it would be the ``hello`` plugin. + +If ``status`` is not ``enabled``, you can enable your plugin with +``sopel-plugins enable hello``. + +Then, you can start your bot and trigger the command like this:: + + .hello + YourNick: Hello! + +And voilà! This is your first plugin. Sure, it doesn't do much, and yet it uses +the key elements that you'll need to understand to write your own plugins. + +.. seealso:: + + To interact with the list of plugins installed, read the documentation + of :ref:`sopel-plugins`. diff --git a/docs/source/plugin/tutorials/playing-with-commands.rst b/docs/source/plugin/tutorials/playing-with-commands.rst new file mode 100644 index 000000000..2f01f6413 --- /dev/null +++ b/docs/source/plugin/tutorials/playing-with-commands.rst @@ -0,0 +1,158 @@ +===================== +Playing with commands +===================== + +Now that you have started :doc:`your first plugin `, maybe you +want to write a more interesting command than the basic ``.hello`` one. Not +that there is anything wrong with that command! The emoticons plugin is +composed of commands like this one:: + + @plugin.command('shrug') + @plugin.action_command('shrugs') + def shrug(bot, trigger): + bot.say('¯\\_(ツ)_/¯') + +Which is one of the maintainers' favorite commands to use. However, let's see +if we can do something a bit more *complex* than that. + + +Greeting a user by their name +============================= + +Have you noticed that a :ref:`plugin callable ` takes **two +arguments?** The first one is the ``bot``, an instance of Sopel that you can +use to :doc:`interact with the bot `. + +In the previous tutorial, we used ``bot.reply``, which is convenient when +responding directly to a user, but not always what you want. Maybe you want the +bot to say something more complex:: + + .hello + Hello YourNick, have a nice day! + +For that, you need **the second argument**: the ``trigger``. It is an object +with information about the message that triggered your +callable, such as the **message** itself, the **channel**, the type of message, +etc.—and what we need for now is the +:attr:`trigger.nick ` attribute:: + + from sopel import plugin + + @plugin.command('hello') + def hello(bot, trigger): + """Say Hello , have a nice day!""" + bot.say(f'Hello {trigger.nick}, have a nice day!') + +.. important:: + + If you want to test this with your bot, and your bot is already running, + restart the bot so it will load the new version of your plugin. + +.. seealso:: + + You can learn much more about the :class:`trigger ` + object by reading its documentation. + + +Command with arguments +====================== + +The trigger object can do much more for you: if a user adds arguments to the +command, like ``.hello morning``, you can detect and use that argument:: + + from sopel import plugin + + @plugin.command('hello') + def hello(bot, trigger): + """Say Hello , have a nice day!""" + # group 1 is the name of the command that was triggered + # group 2 is the entire rest of the message + # groups 3 to 6 are the first, second, third, and fourth command arg + when = trigger.group(3) + # select a different greeting depending on when + greeting = { + 'morning': 'and good morning!', + 'noon': 'are you having lunch?', + 'night': 'I hope it was a good day!', + 'evening': 'good evening to you!' + }.get(when, 'have a nice day!') # default to "nice day" + # say hello + bot.say(f'Hello {trigger.nick}, {greeting}') + +Now the command will be able to react a bit more to your user:: + + .hello morning + Hello YourNick, and good morning! + .hello noon + Hello YourNick, are you having lunch? + +How does that work? Well, the short version is that Sopel uses regex +(`REGular EXpressions`__) to match a message to a plugin callable, and the +``trigger`` object exposes the match result. + +.. seealso:: + + You can learn much more about the :class:`~sopel.plugin.command` decorator + by reading its documentation. + +.. note:: + + In the case of a command, the regex is entirely managed by Sopel itself, + while the generic :func:`@plugin.rule ` decorator + allows you to define your own regex. + +.. __: https://en.wikipedia.org/wiki/Regular_expression + + +And... action! +============== + +Some users say ``.hello`` out loud, and others will say it with an action. How +do you react to these? Let's go back to the example of the ``shrug`` command:: + + @plugin.command('shrug') + @plugin.action_command('shrugs') + def shrug(bot, trigger): + bot.say('¯\\_(ツ)_/¯') + +Notice that it also uses a second decorator, ``action_command('shrugs')``, +with a different name. How does that work? + +Sopel knows how to register the same plugin callable for different types of +trigger, so both ``.shrug`` and ``/me shrugs`` work. For example, you could do +this for your hello plugin:: + + @plugin.command('hello') + @plugin.action_command('waves') + def hello(bot, trigger): + ... + +And so, in chat, you will see that:: + + .hello + Hello YourNick, have a nice day! + * YourNick waves + Hello YourNick, have a nice day! + + +Summing it up +============= + +In this tutorial, we talked briefly about ``bot.say()`` and ``bot.reply()``, +and explored a few more ways to :doc:`interact with the bot `. + +We saw that you can use the :class:`trigger ` argument +of a plugin callable to get more information on the message that triggered the +command. Don't hesitate to read the documentation of that object and discover +all its properties. + +We also saw that you have more ways to trigger a callable, and you can read +more in :doc:`the plugin anatomy chapter ` (see +:ref:`how to define rules `, in particular). + +Throughout this tutorial, we also linked to various sections of the +documentation: as we improve the documentation with every release, we invite +you to read it to discover more features of Sopel and what is available to you +as a plugin author. + +And if you have come this far, thank you for reading this!