From e340f6e9a25339cfc9cf84f05eb51f9a049a8ba9 Mon Sep 17 00:00:00 2001 From: James Lu Date: Tue, 26 Jun 2018 21:35:41 -0700 Subject: [PATCH] writing-plugins.md: rewrite for clarity for conciseness Also document the introduction of hook handler return values and irc.error() [skip ci] --- docs/technical/writing-plugins.md | 63 ++++++++++++++++++------------- 1 file changed, 37 insertions(+), 26 deletions(-) diff --git a/docs/technical/writing-plugins.md b/docs/technical/writing-plugins.md index 025fd8b..0020352 100644 --- a/docs/technical/writing-plugins.md +++ b/docs/technical/writing-plugins.md @@ -1,44 +1,54 @@ # Writing plugins for PyLink -PyLink plugins are modules that extend its functionality by giving it something to do. Without any plugins loaded, PyLink can only sit on a server and do absolutely nothing. +Most features in PyLink (Relay, Automode, etc.) are implemented as plugins, which can be mix-and-matched on any particular instance. Without any plugins loaded, PyLink can connect to servers but won't accomplish anything useful. -This guide, along with the sample [`example.py`](../../plugins/example.py) plugin aim to show the basics of writing plugins for PyLink. +This guide, along with the sample plugin [`example.py`](../../plugins/example.py) aim to show the basics of writing plugins for PyLink. ## Receiving data from IRC -Plugins have two ways of communicating with IRC: hooks, and commands sent in PM to the main PyLink client. A simple plugin can use one, or any mixture of these. +Plugins have two ways of communicating with IRC: hooks, and commands directed towards service clients. Any plugin can use one or a combination of these. -### Hooks +### Hook events -Hooks are probably the most versatile form of communication. The data in each hook payload is formatted as a Python `dict`, with different data keys depending on the command. -For example, a `PRIVMSG` payload would give you the fields `target` and `text`, while a `PART` payload would only give you `channels` and `reason` fields. +PyLink's hooks system is designed as a protocol-independent method for protocol modules to communicate with plugins (and to a lesser extend, for plugins to communicate with each other). Hook events are the most versatile form of communication available, with each individual event generally corresponding to a specific chat or server event (e.g. `PRIVMSG`, `JOIN`, `KICK`). Each hook payload includes 4 parts: -There are many hook types available (one for each supported IRC command), and you can read more about them in the [PyLink hooks reference](hooks-reference.md). +1) The corresponding network object (IRC object) where the event took place (**type**: a subclass of `pylinkirc.classes.PyLinkNetworkCore`) +2) The numeric ID† of the sender (**type**: `str`) +3) An identifier for the command name, which may or may not be the same as the name of the hook depending on context (**type**: `str`) +4) A freeform `dict` of arguments, where data keys vary by command - see the [PyLink hooks reference](hooks-reference.md) for what's available where. -Plugins can bind to hooks using the `utils.add_hook()` function like so: `utils.add_hook(function_name, 'PRIVMSG')`, where `function_name` is your function definition, and `PRIVMSG` is whatever hook name you want to bind to. Once set up, `function_name` will be called whenever the protocol module receives a `PRIVMSG` command. +Functions intended to be hook handlers therefore take in 4 arguments corresponding to the ones listed above: `irc`, `source`, `command`, and `args`. -Each hook-bound function takes 4 arguments: `irc, source, command, args`. -- **irc**: The IRC object where the hook was called. Plugins are globally loaded, so there will be one of these per network. -- **source**: The numeric of the sender. This will usually be a UID (for users) or a SID (for server). -- **command**: The true command name where the hook originates. This may or may not be the same as the name of the hook, depending on context. -- **args**: The hook data (a `dict`) associated with the command. Again, the available data keys differ by hook name -(see the [hooks reference](hooks-reference.md) for a list of which can be used). +#### Return codes for hook handlers -Hook functions do not return anything, and can raise exceptions to be caught by the core. +As of PyLink 2.0-alpha3, the return value of hook handlers are used to determine how the original event will be passed on to further handlers (that is, those created by plugins loaded later, or hook handlers registered with a lower priority). + +The following return values are supported so far: + +- `None` or `True`: passthrough the event unchanged to further handlers (the default behavior) +- `False`: block the event from reaching other handlers + +Hook handlers may raise exceptions without blocking the event from reaching further handlers; these are caught by PyLink and logged appropriately. + +### Hook priorities +TODO ### Bot commands -For plugins that interact with regular users, you can also write commands for the PyLink bot, or [create service bots with their own command set](services-api.md). This section only details the former: +Plugins can also define service bot commands, either for the main PyLink service bot or for one created by the plugin itself. This section only details the former - see the [Services API Guide](services-api.md) for details on the latter. -Plugins can add commands by including something like `utils.add_cmd(testcommand, "hello")`. Here, `testcommand` is the name of your function, and `hello` is the (optional) name of the command. If no command name is specified, it will use the same name as the function. -Now, your command function will be called whenever someone PMs the PyLink client with the command (e.g. `/msg PyLink hello`, case-insensitive). +Commands are registered by calling `utils.add_cmd()` with one or two arguments. Ex) +- `utils.add_cmd(testcommand, "hello")` registers a function named `testcommand` as the command handler for `hello` (i.e. `/msg PyLink hello`) +- `utils.add_cmd(testcommand)` registers a function named `testcommand` as the command handler for `testcommand`. -Each command function takes 3 arguments: `irc, source, args`. -- **irc**: The IRC object where the command was called. -- **source**: The numeric of the sender. This will usually be a UID (for users) or a SID (for server). -- **args**: A `list` of space-separated command arguments (excluding the command name) that the command was called with. For example, `/msg PyLink hello world 1234` would give an `args` list of `['world', '1234']` +Decorator syntax (`@utils.add_cmd`) can also be used for the second case. -As of PyLink 1.2, there are two ways for a plugin to parse arguments: as a raw list of strings, or with `utils.IRCParser` (an [argparse](https://docs.python.org/3/library/argparse.html) wrapper). More information on using `utils.IRCParser()` can be found in the page ["using IRCParser"](using-ircparser.md). +Each command handler function takes 3 arguments: `irc, source, args`. +- **irc**: The network object where the command was called. +- **source**: The numeric ID (or pseudo-ID) of the sender. +- **args**: A `list` of command arguments (not including the command name) that the command was called with. For example, `/msg PyLink hello world 1234` would give an `args` list of `['world', '1234']` + +As of PyLink 1.2, there are two ways for a plugin to parse arguments: as a raw list of strings, or with `utils.IRCParser` (an [argparse](https://docs.python.org/3/library/argparse.html) wrapper). `IRCParser()` is documented in the ["using IRCParser"](using-ircparser.md) page. Command handlers do not return anything and can raise exceptions, which are caught by the core and automatically return an error message. @@ -46,18 +56,19 @@ Command handlers do not return anything and can raise exceptions, which are caug Plugins receive data from the underlying protocol module, and communicate back using outgoing [command functions](pmodule-spec.md) implemented by the protocol module. They should *never* send raw data directly back to IRC, because that wouldn't be portable across different IRCds. -These functions are usually called in this fashion: `irc.command(arg1, arg2, ...)`. For example, the command `irc.join('10XAAAAAB', '#bots')` would join a PyLink client with UID `10XAAAAAB` to channel `#bots`. +These functions are called in the form: `irc.command(arg1, arg2, ...)`. For example, the command `irc.join('10XAAAAAB', '#bots')` would join a PyLink client with UID `10XAAAAAB` to the channel `#bots`. For sending messages (e.g. replies to commands), simpler forms of: - `irc.reply(text, notice=False, source=None)` +- `irc.error(text, notice=False, source=None)` - and `irc.msg(targetUID, text, notice=False, source=None)` are preferred. -`irc.reply()` is a special form of `irc.msg` in that it automatically finds the target to reply to. If the command was called in a channel using fantasy, it will send the reply in that channel. Otherwise, the reply will be sent in a PM to the caller. +`irc.reply()` is a frontend to `irc.msg()` which automatically finds the right target to reply to: that is, the channel for fantasy commands and the caller for PMs. `irc.error()` is in turn a wrapper around `irc.reply()` which prefixes the given text with `Error: `. -The sender UID for both can be set using the `source` argument, and defaults to the main PyLink client. +The sender UID for all of these can be set using the `source` argument, and defaults to the main PyLink client. ## Access checking for commands