Skip to content
This repository has been archived by the owner on Jan 30, 2024. It is now read-only.

Getting Started

Amber Yust edited this page Nov 26, 2015 · 35 revisions

Welcome! This page is designed for someone who has never used KitnIRC before and wants to get a sense of its general layout and functionality. It is designed to complement the example code provided within the repository.

Overall Structure

KitnIRC has two major components:

  • The Client, which handles the IRC protocol communications with the server (and can be used on its own if you just want a wrapper around IRC commands and messages).
  • The Controller, which provides a system for building independent modules that can interact with the IRC server.

The IRC Client

The kitnirc.client.Client object represents a single IRC connection.

Creating a connection

To create a working connection, 3 steps are necessary:

  1. Instantiate a Client object, passing it the host (and optionally, port - default 6667) of the IRC server.
  2. Call the .connect() method of the Client object, passing it the nick you want to use.
  3. Eventually, call the .run() method of the Client object, which will block and process communications to and from the IRC server.

.run() will continue blocking until the connection is broken, either by the server, network issues, or calling .disconnect().

Handling incoming events

A given Client has a set of event handlers to which it will dispatch events based on communications with the IRC server - for instance, the WELCOME event is dispatched once the client has joined the IRC server as a valid user. A list of client events can be found here. (Note that these are NOT the same as the raw IRC events specified by the RFC and listed in events.py for internal use by KitnIRC, though many of them have 1:1 mappings.)

You can add a new handler for a given event by calling the add_handler method on a Client instance, passing it the name of the event and a function to call when that event is triggered. Event handler functions are called with the Client instance that raised the event as their first argument, followed by zero or more additional arguments depending on which event they are handling. (To look up the exact arguments passed for a given event, see the COMMAND PARSERS section of client.py.)

If more than one handler is added for the same event, they will be called in the order in which they were added. If a handler returns a value that is boolean true, no further handlers will be called for that event. Event handlers are blocking, so if they need to do any significant amount of work they should probably spawn off a background thread to do the actual work while the handler itself returns quickly.

Sending commands

Client also provides a number of methods for sending commands to the IRC server. The most bare-bones of these is .send(), which simply sends a raw line to the server. It will take any number of arguments and automatically join them together with spaces and add a newline at the end before sending the line to the server. (As a sanity check, it will raise an exception if your arguments contain any newlines.)

Other methods include most of the common actions you'd expect an IRC bot to do - .nick() attempts to change the client's nickname, .msg() sends a message to a channel or user, and so on. You can find the full set in client.py below the definition of the .send() method; all of them have docstrings describing their function.

Entity classes

A Client uses a handful of other classes to encapsulate certain types of data and provide utility functions related to that data. For instance, the kitnirc.user.User instance represents an individual user entity, a kitnirc.client.Channel instance represents a channel, and a kitnirc.client.Host instance represents a server.

Host instances tend to contain one or more Channel instances in their .channels property; Channel instances tend to contain one or more User instances in their .members property.

In almost all cases, you should treat the data stored in these classes as read-only. The values are set automatically by KitnIRC as it keeps track of the state of the connection, and modifying them manually may result in incorrect or duplicate entries.

The Module Controller

The kitnirc.modular.Controller (and its associated Module interface) provides a means to have many different independently-(re)loadable pieces of functionality that all interact with the same connection. You don't have to use the Controller or modules if you don't want to - the Client handles all basic IRC functionality. Modules are just there as an extra helper.

Creating a Controller

A Controller is instantiated by passing it a Client instance (and usually, a path to a configuration file). Typically the Controller is created after the Client is created, but before the client's .connect() method is called. This ensures that the Controller can properly hook into the Client object before any IRC events start flowing.

Configuration files

While it's possible to create a Controller without specifying a configuration file, this results in an empty controller with no modules loaded by default. KitnIRC will automatically load any modules you list in the configuration file, which is in ConfigParser format. A simple configuration file might look like this:

[modules]
kitnirc.contrib.healthcheck = 1

Let's break this down a bit:

  • The [modules] line defines a section of the configuration - in this case, the particular section that the Controller looks for to determine which modules to load.
  • The key kitnirc.contrib.healthcheck is the Python import path for the package of the module we want to load. (Module packages can be located anywhere from which Python is able to import.)
  • The value 1 is simply a number that allows us to specify a consistent order in which to load modules, even if the configuration file's lines get reordered. Modules with lower numbers will always be loaded before modules with higher numbers. More than one module may have the same number.

The Controller will only pay attention to the [modules] section within the specified configuration file(s). Feel free to use the same file(s) to store other kinds of configuration for different parts of your program (e.g. server connection information, user credentials, etc) in separate sections.

Starting the Controller

After specifying configuration but before calling the Client instance's .connect() method, you'll need to call the .start() method on the Controller instance. This will first load the modules from the specified configuration, and then call each Module instance's .start() method allow them to run any startup and initialization logic they may have. (Even if you don't specify any configuration load modules manually, you still need to call .start() on the Controller so that it will pass along events.)

Creating your own Module

A KitnIRC module is a Python package that contains two things:

  1. A class definition that subclasses from kitnirc.modular.Module.
  2. A top-level variable named module which contains that class definition.

In other words, the simplest KitnIRC module in the world looks about like this:

from kitnirc.modular import Module

class FooModule(Module):
    pass

module = FooModule

Of course, that module wouldn't actually do much. In order to give a module functionality, you'll need to add code that either runs when the module is started or runs when an event is received (or both).

Handling events in modules

Handling events in a module works very similarly to handling them directly from a Client. The Module interface provides its own .add_handler() method, which works identically to the Client method of the same name (except for the caveat that a Module may only specify a single handler for a given event).

There's another even easier way to specify event handlers in a module: use the Module.handle() decorator:

class FooModule(Module):
    @Module.handle("WELCOME")
    def join_some_channels(self, client):
        client.join("#somechannel")

Module handlers for a given event will be called in the order the modules were loaded (and this ordering is preserved across reloads; newly loaded modules are added to the end by default). As with client event handlers, returning a boolean true value from an event handler will stop further processing of that event by other modules.

Running startup logic in a module

You can customize your module's startup process by overriding its .start() method. This method is called by the Controller and passed a single argument reloading, which is set to False if the module is starting for the first time within this Controller, or True if it is starting up after having been reloaded by the Controller.

If you do override this method, make sure to call the parent version as well via super(YourModuleName, self).start(*args, **kwargs).

Running shutdown logic in a module

When the Controller is unloading or reloading a module, it will first call that module's .stop() method, again with the reloading argument set to either False or True respectively. This method can also be overridden to add custom shutdown functionality.

However, you should not rely solely on this shutdown functionality to save persistent state, as it will not be called if the entire program is exiting. Instead, use it for clean up tasks such as stopping any extra threads the module has started.

Dynamically loading, reloading, and unloading modules

This functionality is handled by the following methods on the Controller instance:

  • .load_module() takes fully-qualified module name (aka Python package import path) and attempts to load it.
  • .unload_module() takes a fully-qualified module name and attempts to unload it.
  • .reload_module() takes a fully-qualified module name and attempts to reload it, preserving its position within the module ordering.
  • .reload_modules() takes no arguments and attempts to reload all currently loaded modules, resetting their ordering to whatever is specified in the configuration.

The KitnIRC module kitnirc.contrib.admintools provides a basic IRC-based interface for triggering loads, reloads, and unloads of modules. It can be used by adding it to the configuration file's list of modules to load (or manually loading it via code).