obby_client

The extension module Python imports as obby_client.

1from .obby_client import *
2
3__doc__ = obby_client.__doc__
4if hasattr(obby_client, "__all__"):
5    __all__ = obby_client.__all__
class Client:

One connection, wrapped for Python.

No method here can panic: a bad argument becomes a ValueError with a message, because a panic that unwinds into CPython aborts the interpreter instead of raising an exception.

import socket, time
from obby_client import Client

sock = socket.create_connection(("irc.example.org", 6667))
started = time.monotonic()
client = Client("mynick")
client.handle_connected()

while True:
    while (out := client.poll_transmit()) is not None:
        sock.sendall(out)

    client.handle_bytes(sock.recv(4096))
    client.tick(int((time.monotonic() - started) * 1000), int(time.time() * 1000))

    for event in client.poll_events():
        if event["type"] == "registered":
            client.join("#obby")
            client.send_message("#obby", "hello")
def from_config(config):

Build an engine from a whole config at once, for a host that already holds one as a dict or as JSON text, rather than as separate arguments.

config has the same shape as obby_client::Config. Only nick is required.

def handle_connected(self, /):

Tell the engine the transport is up. Queues the registration burst.

def handle_disconnected(self, /):

Tell the engine its transport died. The model survives, so a reconnect can resume from it.

def tick(monotonic_ms, unix_ms):

Advance the clock. monotonic_ms drives every deadline; unix_ms only stamps a message the server did not stamp itself with server-time.

def poll_timeout(self, /):

When [Self::tick] next has something to do, as a monotonic instant, or None when nothing is scheduled. A host can set one timer for exactly this instant instead of polling on an interval.

def command(command):

Do something on this connection. command is JSON text with the same shape as obby_client::Command.

def join(self, /, channel, key=None):

Join a channel.

client.join("#obby")
client.join("#staff", key="hunter2")
def part(self, /, channel, reason=None):

Leave a channel.

def send_message(self, /, target, text):

Say something to a channel or a person.

client.send_message("#obby", "hello there")
client.send_message("alice", "a private word")
def send_notice(self, /, target, text):

Send a notice, which by convention must never be auto-replied to.

def send_action(self, /, target, text):

Send a CTCP ACTION, the third-person form.

def set_nick(self, /, nick):

Change our nick.

def set_topic(self, /, channel, topic=None):

Set or clear a channel topic.

def set_away(self, /, message=None):

Mark ourselves away, or come back.

def set_typing(self, /, target, state):

Say we are typing, so others can show it. state is one of "active", "paused", "done".

def add_reaction(self, /, target, msgid, emoji):

React to a message with an emoji.

def remove_reaction(self, /, target, msgid, emoji):

Take a reaction back.

def redact_message(self, /, target, msgid, reason=None):

Ask the server to delete a message.

def mark_read(self, /, target, at_ms):

Tell the server how far we have read, in milliseconds since the Unix epoch.

def fetch_history(self, /, target, before_msgid=None, limit=50):

Ask for older messages than the ones we hold. With no before_msgid, this asks for the most recent, which is what a fresh window wants.

def set_metadata(self, /, key, value=None):

Set one of our own metadata keys, or clear it.

def subscribe_metadata(self, /, keys):

Ask to be told when these metadata keys change on anyone we can see.

def whois(self, /, nick):

Ask the server everything it will say about someone. The record lands in the model under the folded nick and arrives as one whois_received change when the reply finishes.

def rename_channel(self, /, channel, new_name, reason=None):

Rename a channel, keeping everyone in it and everything said in it.

def redeem_invite_code(self, /, code):

Redeem an invitation code. Only before registering, which is the point of it.

def generate_token(self, /, service):

Mint a bearer token for one of the network's services, such as its file host.

def watch_nicks(self, /, nicks):

Watch these nicks, so the server says when they come and go.

def unwatch_nicks(self, /, nicks):

Stop watching these nicks.

def send_voice_signal(self, /, channel, signal):

Send a voice signalling frame to a room. The frame is the host's to build: everything in it comes from the media stack the core deliberately knows nothing about.

def quit(self, /, reason=None):

Leave the network.

def send_raw_line(self, /, line):

Send a line we do not model. The escape hatch, so a host is never stuck waiting for us.

def handle_bytes(data):

Feed whatever the transport read. Partial lines are held until the rest arrives.

def poll_transmit(self, /):

Bytes the host should write to the transport, or None when there are none.

One chunk per call, unlike [Self::poll_events]: a chunk is already the smallest unit a socket writes, so there is nothing to gain from batching it.

def poll_events(self, /):

Every event the engine has queued since the last call, as a Python list.

Draining a batch instead of one event per call is what keeps this binding cheap: a call across the GIL costs the same whether it carries one event or a hundred, so paying that cost once per drain rather than once per event is what actually saves work.

An event's type names it, in snake_case, and the rest of the dict is that event's fields.

for event in client.poll_events():
    if event["type"] == "registered":
        print("registered as", event["nick"])
    elif event["type"] == "model_changed":
        print(event["change"])
    elif event["type"] == "server_reply":
        print(event["severity"], event["code"], event["text"])
def model(self, /):

Everything the connection knows: channels, members, conversations and messages. For a host that only wants the model, not a diff of what changed.

A call rather than a property, because it serialises the whole model, and an attribute would hide that cost from a caller reading two fields in a row.