An IRC toolkit built upon Python 3's asyncio module
Python Makefile
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Failed to load latest commit information.
framewirc
tests
.gitignore
.isort.cfg
.travis.yml
CHANGELOG.md
LICENSE
Makefile
README.md
requirements.txt
setup.cfg
setup.py

README.md

FramewIRC

An IRC framework built upon Python 3's asyncio module.

Still immature, but a good foundation (I think).

Focuses on allowing the developer to pick only as much of the stack as required.

Installation

pip install framewirc

Quickstart

Here's a simple bot. Let's call it "snarkbot". Whenever it hears a particular phrase, it responds with a gif.

It probably gets old rather quickly. (I think I may be writing autobiographical documentation.)

import asyncio

from framewirc import filters, parsers
from framewirc.client import Client
from framewirc.commands import PRIVMSG
from framewirc.handlers import basic_handlers
from framewirc.strings import to_unicode


quips = {
    'shame': 'https://raw.githubusercontent.com/meshy/gifs/master/shame-bell.gif',
    'YOLO': 'https://raw.githubusercontent.com/meshy/gifs/master/who-said-yolo.gif',
}


@filters.allow(PRIVMSG)
@parsers.apply_message_parser(parsers.privmsg)
def snarky_response(client, channel, raw_body, **kwargs):
    body = to_unicode(raw_body)

    for trigger, riposte in quips.items():
        if trigger in body:
            client.privmsg(channel, riposte)


class SnarkyClient(Client):
    handlers = basic_handlers + (snarky_response,)
    nick = 'snarkbot'
    real_name = 'framewirc bot'


SnarkyClient().connect_to('irc.example.com')
asyncio.get_event_loop().run_forever()

Concepts

Unfortunately, there's no "One True Way" when it comes to IRC. Many networks and servers have their own way of doing things that are not entirely compatible with others. There's nothing wrong with this, exactly, but it does mean that until this library is a little more mature, it might not correctly deal with all these eccentricities. If you find an issue that is not correctly dealt with, please log an issue on github.

IRC message format

It's probably useful to have some idea of how the IRC protocol works. All messages in both directions must adhere to the (simple) rules:

  • Messages are represented as bytes.

    There is no default encoding, so sometimes one just has to guess! To guess how to turn these streams of bytes into Python strings, we have elected to use cChardet in strings.to_unicode when utf8 fails. If you know the encoding, you can override this behaviour.

  • Messages have a relatively simple structure.

    Generally, a message can be seen to have four distinct parts: a prefix, a command, a number of parameters, and a suffix. The parts that are required depend on the command. Only the command is always required. This is the structure of a message:

    :colon-indicated-prefix COMMAND some parameters :Suffix following the colon
    

    We represent the messages that come from the network with our own subclass of bytes called ReceivedMessage. It has prefix, command, parameters, and suffix added for convinience. The prefix and command parts are represented as unicode strings, parametersis a tuple of unicode strings, but suffix is a bytestring. This allows for custom logic in decoding arbitrary text from the network.

  • Messages have a maximum length of 512 bytes.

    This means that when we want to send something longer to the network, we have to split it up into smaller chunks. This can be done using messages.chunk_message.

Connecting

To connect to an IRC network, create a Client object, and call its connect_to method to join the network of your choice. This creates a Connection, and an asyncio.Task that will be invoked in the event loop. The Client will be responsible for sending a nick and real name once it has.

If there are any actions that need to be completed on connection, this is probably the time to do it. The Client.on_connect method can be overridden to add things like connecting to particular rooms, or sending a password to an authentication bot. (Don't forget to call super though!)

Handling commands from the network

When the Connection receives a message, the Client will send that message (and itself) to every one of its handlers in turn. This allows them to make a decision about how they will deal with the message.

As most of your handlers will not need to deal with every message that comes in, we can remove the boilerplate of if message.command == MYCOMMAND through use of the decorators in the filters module. The allow filter only allows messages through to the handler that are in its whitelist. The deny filter does the opposite. eg:

# Rather than this:
def my_command_handler(client, message):
    if message.command != MYCOMMAND:
        return
    do_useful_logic(message)

# It's nicer to have this:
@allow(MYCOMMAND)
def my_simpler_command_handler(client, message):
    do_useful_logic(message)

Sending commands to the network

The Client has a couple of helper methods for sending commands to the network. You can send messages to users or channels with Client.privmsg(), and change your nick with Client.set_nick(). As there are a number of other very common actions, expect this part of the API to change and expand.

To send other messages to the network, you need to construct an appropriate byte string, and pass it to Connection.send. You will probably not want to do this by hand, so use the messages.build_message method to help you.

Still to come

Features that I am hoping to implement in future:

  • More message parsers

    At the moment, we only have a special parser for PRIVMSG messages, but there is room for loads more.

  • Handle more text encodings.

    At the moment, the to_unicode method is a little prone to errors when cChardet provides a non standard encoding.

  • Find a nicely overridable way to remove the basic_handlers boilerplate from Client subclasses that still allows ways to customise the behaviour.

  • Full API documentation.

    I would really like to document the full API, not just the basics. I'd also like to explain my rationale for the design decisions I took, and how the library contrasts to what is currently available elsewhere.