Files
derp/docs/API.md
user 9d4cb09069 feat: make SOCKS5 proxy configurable per adapter
Add `proxy` config option to server (IRC), teams, telegram, and mumble
sections. IRC defaults to false (preserving current direct-connect
behavior); all others default to true. The `derp.http` module now
accepts `proxy=True/False` on urlopen, create_connection,
open_connection, and build_opener -- when false, uses stdlib directly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 21:19:22 +01:00

18 KiB

derp - Plugin API Reference

Stable public surface for plugin authors. Covers decorators, bot methods, IRC primitives, state persistence, and network helpers.

Stability Guarantee

Symbols documented below follow semver:

Change type Allowed in
Breaking (remove, rename, change signature) Major only (3.0, 4.0, ...)
Additions (new functions, parameters, fields) Minor (2.1, 2.2, ...)
Bug fixes (behavior corrections) Patch (2.0.1, 2.0.2, ...)

Deprecation policy: deprecated in a minor release, removed in the next major. Deprecated symbols emit a log warning.

Extension points: attributes prefixed with _ are documented for reference but considered unstable -- they may change in minor releases.


derp.plugin -- Decorators & Registry

Decorators

@command(name: str, help: str = "", admin: bool = False, tier: str = "")

Register an async function as a bot command. If tier is empty, it defaults to "admin" when admin=True, otherwise "user".

@event(event_type: str)

Register an async function as an IRC event handler. The event_type is uppercased automatically (e.g. "join" becomes "JOIN").

Constants

Name Type Value
TIERS tuple[str, ...] ("user", "trusted", "oper", "admin")

Handler dataclass

Field Type Default Description
name str -- Command or event name
callback Callable -- Async handler function
help str "" Help text
plugin str "" Source plugin name
admin bool False Legacy admin flag
tier str "user" Required permission tier

PluginRegistry

Method Signature Description
register_command (name, callback, help="", plugin="", admin=False, tier="user") Register a command handler
register_event (event_type, callback, plugin="") Register an event handler
load_plugin (path: Path) -> int Load plugin from .py file; returns handler count or -1
load_directory (dir_path: Path) -> None Load all .py plugins from a directory
unload_plugin (name: str) -> bool Unload plugin (refuses core); returns success
reload_plugin (name: str) -> tuple[bool, str] Reload from original path; returns (ok, reason)

Extension points (unstable):

Attribute Type Description
_modules dict[str, Any] Loaded plugin modules by name
_paths dict[str, Path] File paths of loaded plugins

derp.bot -- Bot Instance

Stable Attributes

Attribute Type Description
name str Server/bot instance name
config dict Merged TOML configuration
nick str Current IRC nick
prefix str Command prefix (e.g. "!")
state StateStore Persistent key-value storage
registry PluginRegistry Command and event registry

Sending Messages

Method Signature Description
send (target: str, text: str) -> None Send PRIVMSG (rate-limited, auto-split)
reply (msg: Message, text: str) -> None Reply to channel or PM source
long_reply (msg: Message, lines: list[str], *, label: str = "") -> None Reply with paste overflow for long output
action (target: str, text: str) -> None Send CTCP ACTION (/me)

IRC Control

Method Signature Description
join (channel: str) -> None Join a channel
part (channel: str, reason: str = "") -> None Part a channel
quit (reason: str = "bye") -> None Quit server and stop bot
kick (channel: str, nick: str, reason: str = "") -> None Kick user from channel
mode (target: str, mode_str: str, *args: str) -> None Set channel/user mode
set_topic (channel: str, topic: str) -> None Set channel topic

Utility

Method Signature Description
shorten_url (url: str) -> str Shorten URL via FlaskPaste; returns original on failure
load_plugins (plugins_dir: str | Path | None = None) -> None Load all plugins from directory
load_plugin (name: str) -> tuple[bool, str] Hot-load a plugin by name
reload_plugin (name: str) -> tuple[bool, str] Reload a plugin
unload_plugin (name: str) -> tuple[bool, str] Unload a plugin

Extension Points (unstable)

Attribute / Method Description
_pstate Per-bot plugin runtime state dict
_get_tier(msg) Determine sender's permission tier
_is_admin(msg) Check if sender is admin
_dispatch_command(msg) Parse and dispatch a command from PRIVMSG
_spawn(coro, *, name=) Spawn a tracked background task
registry._modules Direct access to loaded plugin modules

derp.irc -- IRC Protocol

Message dataclass

Field Type Description
raw str Original IRC line
prefix str | None Sender prefix (nick!user@host)
nick str | None Sender nick (extracted from prefix)
command str IRC command (uppercased)
params list[str] Command parameters
tags dict[str, str] IRCv3 message tags
Property Type Description
target str | None First param (channel or nick)
text str | None Trailing text (last param)
is_channel bool Whether target starts with # or &

Functions

Function Signature Description
parse (line: str) -> Message Parse a raw IRC line
format_msg (command: str, *params: str) -> str Format an IRC command

IRCConnection

Method Signature Description
connect () -> None Open TCP/TLS connection
send (line: str) -> None Send raw IRC line (appends CRLF)
readline () -> str | None Read one line; None on EOF
close () -> None Close connection
connected (property) -> bool Whether connection is open

derp.state -- Persistent Storage

StateStore

SQLite-backed key-value store. Each plugin gets its own namespace.

Method Signature Description
get (plugin: str, key: str, default: str | None = None) -> str | None Get a value
set (plugin: str, key: str, value: str) -> None Set a value (upsert)
delete (plugin: str, key: str) -> bool Delete a key; returns True if removed
keys (plugin: str) -> list[str] List all keys for a plugin
clear (plugin: str) -> int Delete all state for a plugin; returns count
close () -> None Close the database connection

derp.http -- HTTP & Network

HTTP/TCP helpers with optional SOCKS5 proxy routing. All functions accept a proxy parameter (default True) to toggle SOCKS5.

Function Signature Description
urlopen (req, *, timeout=None, context=None, retries=None, proxy=True) HTTP request with optional SOCKS5, connection pooling, retries
build_opener (*handlers, context=None, proxy=True) Build URL opener, optionally with SOCKS5 handler
create_connection (address, *, timeout=None, proxy=True) TCP socket.create_connection with optional SOCKS5, retries
open_connection (host, port, *, timeout=None, proxy=True) Async asyncio.open_connection with optional SOCKS5, retries

derp.dns -- DNS Helpers

Wire-format encode/decode for raw DNS queries and responses.

Constants

Name Type Description
QTYPES dict[str, int] Query type name to number (A, NS, CNAME, SOA, PTR, MX, TXT, AAAA)
QTYPE_NAMES dict[int, str] Reverse mapping (number to name)
RCODES dict[int, str] Response code to name
TOR_DNS_ADDR str Tor DNS resolver address
TOR_DNS_PORT int Tor DNS resolver port

Functions

Function Signature Description
get_resolver () -> str First IPv4 nameserver from /etc/resolv.conf
encode_name (name: str) -> bytes Encode domain to DNS wire format
decode_name (data: bytes, offset: int) -> tuple[str, int] Decode DNS name with pointer compression
build_query (name: str, qtype: int) -> bytes Build a DNS query packet
parse_rdata (rtype: int, data: bytes, offset: int, rdlength: int) -> str Parse an RR's rdata to string
parse_response (data: bytes) -> tuple[int, list[str]] Parse DNS response; returns (rcode, values)
reverse_name (addr: str) -> str Convert IP to reverse DNS name

derp.teams -- Teams Adapter

Alternative bot adapter for Microsoft Teams via outgoing/incoming webhooks. Exposes the same plugin API as derp.bot.Bot so protocol-agnostic plugins work without modification.

TeamsMessage dataclass

Duck-typed compatible with IRC Message:

Field Type Description
raw dict Original Activity JSON
nick str | None Sender display name
prefix str | None Sender AAD object ID (for ACL)
text str | None Message body (stripped of @mention)
target str | None Conversation/channel ID
is_channel bool Always True (outgoing webhooks)
command str Always "PRIVMSG" (compat shim)
params list[str] [target, text]
tags dict[str, str] Empty dict (no IRCv3 tags)
_replies list[str] Reply buffer (unstable)

TeamsBot

Same stable attributes and methods as Bot:

Attribute Type Description
name str Always "teams"
config dict Merged TOML configuration
nick str Bot display name (teams.bot_name)
prefix str Command prefix (from [bot])
state StateStore Persistent key-value storage
registry PluginRegistry Shared command and event registry

Sending messages -- same signatures, different transport:

Method Behaviour
send(target, text) POST to incoming webhook URL
reply(msg, text) Append to msg._replies (HTTP response)
long_reply(msg, lines, *, label="") Paste overflow, appends to replies
action(target, text) Italic text via incoming webhook
shorten_url(url) Same FlaskPaste integration

IRC no-ops (debug log, no error):

join, part, kick, mode, set_topic

Plugin management -- delegates to shared registry:

load_plugins, load_plugin, reload_plugin, unload_plugin

Permission tiers -- same model, exact AAD object ID matching:

_get_tier(msg), _is_admin(msg)


derp.telegram -- Telegram Adapter

Alternative bot adapter for Telegram via long-polling (getUpdates). All HTTP routed through SOCKS5 proxy. Exposes the same plugin API as derp.bot.Bot so protocol-agnostic plugins work without modification.

TelegramMessage dataclass

Duck-typed compatible with IRC Message:

Field Type Description
raw dict Original Telegram Update
nick str | None Sender first_name (or username fallback)
prefix str | None Sender user_id as string (for ACL)
text str | None Message body (stripped of @bot suffix)
target str | None chat_id as string
is_channel bool True for groups, False for DMs
command str Always "PRIVMSG" (compat shim)
params list[str] [target, text]
tags dict[str, str] Empty dict (no IRCv3 tags)

TelegramBot

Same stable attributes and methods as Bot:

Attribute Type Description
name str Always "telegram"
config dict Merged TOML configuration
nick str Bot display name (from getMe)
prefix str Command prefix (from [telegram] or [bot])
state StateStore Persistent key-value storage
registry PluginRegistry Shared command and event registry

Sending messages -- same signatures, Telegram API transport:

Method Behaviour
send(target, text) sendMessage API call (proxied, rate-limited)
reply(msg, text) send(msg.target, text)
long_reply(msg, lines, *, label="") Paste overflow, same logic as IRC
action(target, text) Italic Markdown text via sendMessage
shorten_url(url) Same FlaskPaste integration

Message splitting: messages > 4096 chars split at line boundaries.

IRC no-ops (debug log, no error):

join, part, kick, mode, set_topic

Plugin management -- delegates to shared registry:

load_plugins, load_plugin, reload_plugin, unload_plugin

Permission tiers -- same model, exact user_id string matching:

_get_tier(msg), _is_admin(msg)

Helper Functions

Function Signature Description
_strip_bot_suffix (text: str, bot_username: str) -> str Strip @username from command text
_build_telegram_message (update: dict, bot_username: str) -> TelegramMessage | None Parse Telegram Update into message
_split_message (text: str, max_len: int = 4096) -> list[str] Split long text at line boundaries

derp.mumble -- Mumble Adapter

Alternative bot adapter for Mumble via TCP/TLS protobuf control channel (text chat only). All TCP routed through SOCKS5 proxy. Uses a minimal built-in protobuf encoder/decoder (no external dependency). Exposes the same plugin API as derp.bot.Bot.

MumbleMessage dataclass

Duck-typed compatible with IRC Message:

Field Type Description
raw dict Decoded protobuf fields
nick str | None Sender username (from session lookup)
prefix str | None Sender username (for ACL)
text str | None Message body (HTML stripped)
target str | None channel_id as string (or "dm")
is_channel bool True for channel msgs, False for DMs
command str Always "PRIVMSG" (compat shim)
params list[str] [target, text]
tags dict[str, str] Empty dict (no IRCv3 tags)

MumbleBot

Same stable attributes and methods as Bot:

Attribute Type Description
name str Always "mumble"
config dict Merged TOML configuration
nick str Bot username (from config)
prefix str Command prefix (from [mumble] or [bot])
state StateStore Persistent key-value storage
registry PluginRegistry Shared command and event registry

Sending messages -- same signatures, Mumble protobuf transport:

Method Behaviour
send(target, text) TextMessage to channel (HTML-escaped)
reply(msg, text) send(msg.target, text)
long_reply(msg, lines, *, label="") Paste overflow, same logic as IRC
action(target, text) Italic HTML text (<i>...</i>)
shorten_url(url) Same FlaskPaste integration

IRC no-ops (debug log, no error):

join, part, kick, mode, set_topic

Plugin management -- delegates to shared registry:

load_plugins, load_plugin, reload_plugin, unload_plugin

Permission tiers -- same model, exact username string matching:

_get_tier(msg), _is_admin(msg)

Protobuf Codec (internal)

Minimal protobuf wire format helpers -- not part of the stable API:

Function Description
_encode_varint(value) Encode unsigned int as protobuf varint
_decode_varint(data, offset) Decode varint, returns (value, offset)
_encode_field(num, wire_type, value) Encode a single protobuf field
_decode_fields(data) Decode payload into {field_num: [values]}
_pack_msg(msg_type, payload) Wrap payload in 6-byte Mumble header
_unpack_header(data) Unpack header into (msg_type, length)

Helper Functions

Function Signature Description
_strip_html (text: str) -> str Strip HTML tags and unescape entities
_escape_html (text: str) -> str Escape text for Mumble HTML messages
_build_mumble_message (fields, users, our_session) -> MumbleMessage | None Build message from decoded TextMessage fields

Handler Signatures

All command and event handlers are async functions receiving bot and message:

async def cmd_name(bot: Bot, message: Message) -> None: ...
async def on_event(bot: Bot, message: Message) -> None: ...

The message.text contains the full message text including the command prefix and name. To extract arguments:

args = message.text.split(None, 1)[1] if " " in message.text else ""

Plugin Boilerplate

Minimal command plugin

from derp.plugin import command

@command("greet", help="Say hello")
async def cmd_greet(bot, message):
    await bot.reply(message, f"Hello, {message.nick}!")

Stateful plugin with _ps(bot) pattern

Plugins that need per-bot runtime state use a _ps(bot) helper to namespace state in bot._pstate:

from derp.plugin import command, event

_NS = "myplugin"

def _ps(bot):
    """Return per-bot plugin state, initialising on first call."""
    if _NS not in bot._pstate:
        bot._pstate[_NS] = {"counter": 0}
    return bot._pstate[_NS]

@command("count", help="Increment counter")
async def cmd_count(bot, message):
    ps = _ps(bot)
    ps["counter"] += 1
    await bot.reply(message, f"Count: {ps['counter']}")

@event("JOIN")
async def on_join(bot, message):
    if message.nick != bot.nick:
        ps = _ps(bot)
        ps["counter"] += 1

Persistent state

Use bot.state for data that survives restarts:

@command("note", help="Save a note")
async def cmd_note(bot, message):
    args = message.text.split(None, 2)
    if len(args) < 3:
        await bot.reply(message, "Usage: !note <key> <value>")
        return
    bot.state.set("note", args[1], args[2])
    await bot.reply(message, f"Saved: {args[1]}")