obby_client
The extension module Python imports as obby_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")
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.
Tell the engine its transport died. The model survives, so a reconnect can resume from it.
Advance the clock. monotonic_ms drives every deadline; unix_ms only stamps a message
the server did not stamp itself with server-time.
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.
Do something on this connection. command is JSON text with the same shape as
obby_client::Command.
Join a channel.
client.join("#obby")
client.join("#staff", key="hunter2")
Say something to a channel or a person.
client.send_message("#obby", "hello there")
client.send_message("alice", "a private word")
Send a notice, which by convention must never be auto-replied to.
Say we are typing, so others can show it. state is one of "active", "paused",
"done".
Tell the server how far we have read, in milliseconds since the Unix epoch.
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.
Ask to be told when these metadata keys change on anyone we can see.
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.
Rename a channel, keeping everyone in it and everything said in it.
Make an invitation link to a channel, or to the network when no channel is named.
Redeem an invitation code. Only before registering, which is the point of it.
Mint a bearer token for one of the network's services, such as its file host.
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.
Send a line we do not model. The escape hatch, so a host is never stuck waiting for us.
Feed whatever the transport read. Partial lines are held until the rest arrives.
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.
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"])
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.