Skip to main content

asyncio-based rfc2812-compliant IRC Client

Project description

Documentation PyPi GitHub License GitHub Issues or Pull Requests

bottom 3.0.0

bottom is a small no-dependency async library for running simple or complex IRC clients and requires python 3.12+

It's easy to get started with built-in support for common commands, and extensible enough to support any capabilities, including custom encryption, local events, bridging, replication, custom- and multi- syntax compatibility, and more.

Installation

pip install bottom

Documentation

The user guide and API reference are available here including examples for regex based routing of privmsg, custom encryption, and a full list of rfc2812 commands that are supported by default.

Quick Start

The following example creates a client that will:

  • connect, identify itself, wait for MOTD, and join a channel
  • respond to PING automatically
  • respond to any PRIVMSG sent directly to it, or in a channel
import asyncio
import bottom

host = "irc.libera.chat"
port = 6697
ssl = True

NICK = "bottom-bot"
CHANNEL = "#bottom-dev"

bot = bottom.Client(host=host, port=port, ssl=ssl)


@bot.on('CLIENT_CONNECT')
async def connect(**kwargs):
    await bot.send('nick', nick=NICK)
    await bot.send('user', user=NICK,
                realname='https://github.com/numberoverzero/bottom')

    # Don't try to join channels until we're past the MOTD
    await bottom.wait_for(bot, ["RPL_ENDOFMOTD", "ERR_NOMOTD"])

    await bot.send('join', channel=CHANNEL)


@bot.on('PING')
async def keepalive(message: str, **kwargs):
    await bot.send('pong', message=message)


@bot.on('PRIVMSG')
async def message(nick: str, target: str, message: str, **kwargs):
    if nick == NICK:
        return  # bot sent this message, ignore
    if target == NICK:
        target = nick  # direct message, respond directly
    # else: respond in channel
    await bot.send("privmsg", target=target, message=f"echo: {message}")


async def main():
    await bot.connect()
    try:
        # serve until the connection drops...
        await bot.wait("client_disconnect")
        print("\ndisconnected by remote")
    except asyncio.CancelledError:
        # ...or we hit ctrl+c
        await bot.disconnect()
        print("\ndisconnected after ctrl+c")


if __name__ == "__main__":
    asyncio.run(main())

API

The public API that you'll typically interact with is small: the Client class and possibly register_pattern. It is built around sending commands with send(cmd, **kw) (or send_message(msg) for raw IRC lines) and processing events with @on(event) and wait(event).

If you need to customize serialization, message handling, or signal processing, those are all available with examples in the Extensions documentation.

class Client:
    # true when the underlying connection is closed or closing
    is_closing() -> bool:

    # connects to the given host, port, and optionally over ssl.
    async connect() -> None

    # start disconnecting if connected.  safe to call multiple times.
    async disconnect() -> None

    # send a known rfc2812 command, formatting kwargs for you
    async send(command: str, **kwargs) -> None

    # decorate a function (sync or async) to handle an event.
    # these can be rfc2812 events (privmsg, ping, notice) or built-in
    # events (client_connect, client_disconnect) or your own signals
    @on(event: str)(async handler)

    # manually trigger an event to be processed by any registered handlers
    # for example, to simulate receiving a message:
    #     my_client.trigger("privmsg", nick=...)
    # or send a local-only message to another part of your system:
    #     trigger("backup-local", backend="s3", session=...)
    trigger(event: str, **kwargs) -> asyncio.Task

    # wait for an event to be triggered.
    async wait(event: str) -> dict

    # send raw IRC line.  bypasses rfc2812 parsing and validation,
    # so you can support custom IRC messages or extensions, like SASL.
    async send_message(message: str) -> None

    # functions that handle the inbound raw IRC lines.
    # by default, Client includes an rfc2812 handler that triggers
    # events caught by @Client.on
    message_handlers: list[ClientMessageHandler]


# register a new pattern for outbound serialization eg.
#   register_pattern("MYCMD", "MYCMD {nick} {target}")
#   client.send("MYCMD", nick="n0", target="remote.net")
def register_pattern(command: str, template: str)


# wait for the client to emit one or more events.  when mode is "first"
# this returns the events that finished first (more than one event can be triggered
# in a single loop step) and cancels the rest.  when mode is "all" this waits
# for all events to trigger.
async def wait_for(client, events: list[str], mode: "first"|"all") -> list[dict]


# helper classes to customize [de]serializing `dict <--> IRC line`
class CommandSerializer:
    # register a new serialization pattern.  these are sorted and looked up
    # when trying to match input params against a valid command format.
    # (some commands have multiple formats, like "TOPIC")

    def register(command: str, template: str) -> SerializedTemplate

    # format a dict of params into the best match template for a given command.
    # searches all registered templates for that command from most args -> least args
    # until a match is found and applied.
    def serialize(command: str, params: dict) -> str

class SerializedTemplate:
    # like string.format() but less overhead.  applies custom formatters.
    def format(params: dict) -> str

    # returns an optimized version of string.format() that can use custom formatters
    # eg. "{foo:myfunc} said {bar:join_commas}"
    @classmethod
    def parse(template: str, formatters: dict[str, Callable]) -> SerializedTemplate


# type hints for message handlers
type NextMessageHandler[T: Client] = Callable[[bytes], T, Coroutine[Any, Any, Any]]
type ClientMessageHandler[T: Client] = Callable[[NextMessageHandler[T], T, bytes], Coroutine[Any, Any, Any]]

Contributors

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

bottom-3.0.0.tar.gz (26.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

bottom-3.0.0-py3-none-any.whl (25.9 kB view details)

Uploaded Python 3

File details

Details for the file bottom-3.0.0.tar.gz.

File metadata

  • Download URL: bottom-3.0.0.tar.gz
  • Upload date:
  • Size: 26.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.12.3

File hashes

Hashes for bottom-3.0.0.tar.gz
Algorithm Hash digest
SHA256 04b87ea92ef1e213d0c8f2af22e46048d1e8cc5bf08e2a19924e94de0610fc34
MD5 377220ba0e27b8d15e11f59a4f6b454d
BLAKE2b-256 7027bae725dfeaa4e510a544eff2eaefc518dd655b8bfba60cde8cfffef0a152

See more details on using hashes here.

File details

Details for the file bottom-3.0.0-py3-none-any.whl.

File metadata

  • Download URL: bottom-3.0.0-py3-none-any.whl
  • Upload date:
  • Size: 25.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.12.3

File hashes

Hashes for bottom-3.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 7f3a3733f14f248ff1a3b92d3888cbf6b862e0348a1feffd8ee0c6d601d53d47
MD5 ae7256fb39385df38b1470e5ad7165ac
BLAKE2b-256 51988f7dff56ea38e02f787e2ce2b6dac0a413aa7cf16906407568a7b6666ab2

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page