Folders and files
| Name | Name | Last commit date | ||
|---|---|---|---|---|
Repository files navigation
This is internet IRC bot pyBurlyBot Handy ----- `sudo docker logs --tail 200 --timestamps pyburlybot` External word services ---------------------- The `wordsapi` module uses Free Dictionary API v2 for English definitions and embedded synonyms. Replies include the supplied Wiktionary source URL and license name; the returned content is generally CC BY-SA 3.0. See https://dictionaryapi.dev/ and https://creativecommons.org/licenses/by-sa/3.0/. Spelling suggestions and synonym fallback use the Datamuse API. Datamuse asks public applications to acknowledge the service; see https://www.datamuse.com/api/. No key is required through 2026. Datamuse has announced that keys will become mandatory on 1 January 2027, but has not yet documented how issued keys are sent with requests. Request a key through its API feedback form before that date and update this module when Datamuse supplies the integration details. When schedules -------------- Load `when` (which requires `whenapi`) to link a When schedule to an IRC channel. With `.` as the configured command prefix: - `.when ~link <URL>` validates and saves the channel's schedule link. - `.when` lists current and upcoming availability and includes the schedule URL: `Alice (for 2h18m), Bob (in 1h12m for 3h0m)`. `maybe`, unavailable, and blank cells are not counted as available. - `.when ~unlink` removes the channel's link. These commands are channel-only; anyone in the channel can link or unlink. Links persist in SQLite, separately for each network and channel. A failed link attempt preserves the existing link. Use your configured command prefix in place of `.` (the example configuration uses `!`). Set `moduleopts.when.lookahead_hours` to control how far ahead to look for availability starts (default `1.7`, range `0` to `168`; `0` shows only people available now). This option can also be scoped to a channel. Each person is listed once, with their current continuous period or the next one starting within the lookahead. Current durations are time remaining from now; future durations cover the entire period, including time beyond the lookahead. Times are displayed as `XhYm`, rounded to the nearest minute. Adjacent available cells are joined, but gaps and exceptions break the period. Duration calculation looks eight days past the lookahead; a period still continuing then is shown as `for at least XhYm` rather than claiming it ends there. The `whenapi` helper uses the existing public Convex `schedules:get` query: POST `/api/query` with `path: "schedules:get"`, `args: {scheduleId: "..."}`, and `format: "json"`. No credentials or When backend changes are required. Defaults are `site_url: https://when.nyanya.org` and `api_url: https://when-convex.nyanya.org`; override these under `moduleopts.whenapi` for another deployment. Only schedule links from the configured frontend origin are accepted. Availability uses each selection's timezone and the current date, including DST, linked saved availability, and per-participant date exceptions (including calendar conflicts already synced by When). Schedule date limits, disallowed slots, and blocked participants are respected. A locked or nominated time does not itself make a participant available. The bot reads a fresh schedule snapshot on each request; it does not trigger a calendar sync. API helper modules ------------------ API helper modules (`googleapi`, `wordsapi`, `openweathermap_api`, ...) own the HTTP call, API-key handling, and error reporting for a service, and return parsed, typed structures — small tuples or frozen dataclasses with type annotations as the contract — never raw JSON dicts. Consumers should not need to know the wire format. `wordsapi`'s frozen dataclasses are the preferred style for anything beyond a couple of fields. Known stragglers to remediate when touched: `openweathermap_api` returns raw `dict[str, Any]` from `get_weather`/`get_forecast` (weather.py does the JSON digging), and `googleapi.google_youtube_details` leaks a raw dict. AI chat harness (bbm) --------------------- The `bbm` module answers channel lines that mention the bot's nickname (and private messages) with a reply generated by an AI model, through one of two backends selected with `moduleopts.bbm.backend`: - `openai` (default) uses the `openai_api` helper, which owns the key and HTTP for the Chat Completions endpoint (`API_KEY`, `api_base`, `timeout` options; `api_base` can point at any OpenAI-compatible service). - `codex` uses the `codex_api` helper, which speaks the official codex app-server protocol (JSON-RPC 2.0 over a hand-rolled `util/ws.py` WebSocket) to a `codex app-server` service backed by a ChatGPT subscription. Options: `url` (default `ws://codex:4500`), `token` (the server's capability token, sent as a Bearer credential), `model`/`effort` (empty uses server defaults), `timeout`. Each reply runs in a fresh ephemeral thread (nothing persists server-side) with approvals and command-running disabled; a recreated codex container only ever fails the request in flight. With no credential configured for the selected backend, bbm loads but stays silent. Behaviour is configured through `moduleopts.bbm`: `model` and the model knobs (`temperature`, `max_completion_tokens`, and `extra_params`, a JSON object merged into the request payload, e.g. `{"reasoning_effort": "low"}`); `system_prompt` for personality/tone; `context_lines` for how many recent channel lines (including the bot's own, and /me actions) are provided as context; and `nicknames`/`ignore` for extra mention names and nicks never answered. Since module options are channel-scopable, all of these can differ per channel. After the bot answers someone, that person may continue the conversation without repeating the nick for `max_followups` messages within `followup_window` seconds; mentioning the nick again resets the budget. This is how "ask a follow-up question" flows work, e.g. the weather tool reporting an unknown location, the bot asking where you are, and a plain "Lansing" reply completing the request. Replies are limited to `max_lines` IRC lines (default 2). If the model exceeds that, or a line will not fit, the full text is published through the "paste" addon (`selfpaste`) and the channel gets the first line plus the paste URL; without a paste addon the overflow is dropped. With the `openai` backend the model can call tools, resolved for up to `tool_rounds` rounds per reply. A built-in `sleep` tool mutes the harness in the current channel ("bbm, shut up"; `sleep_time` is the default duration). With the `codex` backend the harness tools are not advertised - codex brings its own (web search; command running is disabled) - and the sleep behaviour is a reply directive instead: the model is instructed to start such replies with `[sleep N]`, which the harness strips and applies. Tool implementations live in the `pyburlybot_modules/ai_tools/` package - not bot modules, but plain submodules exposing `get_tools(bot)`; the ones named in the `tools` option (default `calculator`, `weather`, `websearch`) are loaded at init. Each tool declares the bot modules it needs (`websearch` needs `googleapi`; `weather` needs `location`, `users`, and `openweathermap_api`) and is only advertised to the model when those are loaded. Any bot module can also contribute tools at runtime: declare `REQUIRES = ("bbm",)` and call `bot.getModule("bbm").register_tool(bot, AITool(...))` from `init()`, with `AITool`/`ToolContext` imported from `pyburlybot_modules.ai_tools`. Outbound HTTP ------------- Modules share the synchronous client in `util/http.py`. Mapped event and command handlers run in Twisted's worker pool, as do timer callbacks, so HTTP and parsing work must stay behind those dispatcher/timer boundaries rather than running directly in the reactor thread. Use the shared `http` client for outbound requests. All shared HTTP users stream responses under the same 2 MiB byte ceiling and a single request deadline. The client rejects unsafe URL forms, validates every DNS answer, connects only to those validated addresses with failover, revalidates redirects, strips credentials on cross-origin redirects, and retains the original hostname for HTTP Host and TLS verification. Module-posted events and webhooks --------------------------------- Modules can raise their own events with `bot.postEvent("name", **attributes)` (`broadcast=True` posts to every server). They dispatch like IRC events: to `Mapping(types=["name"])` handlers and to `send_and_wait` generators. Handlers receive the plain container (no reply target) unless the poster supplies one. Every `Event` carries `authorized` (default False); posted events may set it when the source really acts for the bot owner, and handlers of privileged actions must check it. The `webhook` module exposes `http://<listen_host>:<listen_port>/hooks/<name>` (the `/hooks/` prefix is the `path_prefix` option). Each request posts a `webhook` event (`event.hook`, `event.json`, `event.body`, `event.remote`, ...) to all servers; names in the `event_hooks` option are also posted as their own event type (default: `reload`, `update`). Secrets are per hook (`secrets` option: `{"reload": "...", "update": "..."}`); a request that proves the secret *for that hook* (`Authorization: Bearer <secret>` or an `X-Hub-Signature-256: sha256=<hmac>` body signature) is delivered with `event.authorized=True`; anything else still arrives, unauthorized. A secret never authorizes another hook. The `reload` module reloads the config file on an authorized `reload` event, so an orchestrator that edits `BurlyBot.json` externally can apply it with: curl -X POST -H "Authorization: Bearer $SECRET" http://127.0.0.1:8642/hooks/reload Likewise `updaterelaunch` runs its git update on an authorized `update` event (debounced by `update_debounce` seconds), which is how a GitHub push webhook pointed at `/hooks/update` deploys merges to `git_branch` without polling. See `docs/examples/modules/webhookexample.py` and `docs/deploy/docker.md`. Command smoke-test client ------------------------- `microirc.py` is a deliberately small IRC client that reuses the bot's JSON server, TLS, encoding, command-prefix, nickname, module, and channel settings. It joins each configured channel and runs the explicit cases in `TEST_COMMANDS`. Each case supplies a useful argument payload and records whether the command can produce a multiline reply. Admin-only and hidden commands are not exercised. Only one command is in flight at a time. A single-line command advances as soon as the bot replies. A multiline command advances once the bot has been quiet for `--multiline-idle` seconds. Either kind advances after `--reply-timeout` if the bot does not reply; this is expected for intentionally silent commands such as `print`. Run it while the bot is connected: ``` python microirc.py BurlyBot.json ``` By default every configured server and channel is tested. A run can be narrowed, paced differently, or supplied explicit command bodies (useful when the help module is disabled): ``` python microirc.py BurlyBot.json --server libera --channel '#burlybot-test' python microirc.py BurlyBot.json --multiline-idle 2 --reply-timeout 30 python microirc.py BurlyBot.json --command 'calc 1+1' --command 'help calc' python microirc.py BurlyBot.json --dry-run ``` Use `--verbose` to show raw IRC traffic. The client prints bot replies while it runs and exits non-zero when configuration, connection, registration, joining, or sending fails. Edit `TEST_COMMANDS` to add pathological payloads or change the reply metadata for a command. End-to-end local harness ------------------------ `microirc_harness.py` wires the complete test together: it starts the local IRC server, launches `pyBurlyBot.py`, waits until the bot has joined, runs `microirc.py`, then shuts down the bot and server. Both child processes use the same Python interpreter/environment that launched the harness. ``` python microirc_harness.py --port 0 python microirc_harness.py --port 0 --command 'md5 microIRC' --verbose ``` `microirc_harness.conf` is the committed, secret-free configuration template. It points at the loopback microIRC server and enables the production commands and their helper modules. The harness excludes explicit example/debug modules, the deprecated Steam and Weather Underground integrations, the Rizon-specific nick recovery module, the redundant remote paste provider, and the source-tree updater. At runtime, database, search-index, paste, and generated-config files live in a private temporary directory. If root `BurlyBot.json` exists, only the `API_KEY` and `CSE_ID` slots declared by the harness template are copied into that runtime config; passwords and tokens are never copied. The generated config has mode `0600` and is deleted after the run. Use `--no-secrets` to disable the overlay or `--secrets-config` to select another source. Local micro IRC server ---------------------- `microirc_server.py` is a plaintext local relay for testing the bot and harness without a public IRC network. It implements only registration, nick changes, joins/names, parts/quits, PING/PONG, MODE queries, and channel/direct `PRIVMSG`/`NOTICE` relay. Malformed, overlong, and unsupported messages receive basic IRC errors. Point a bot configuration at `127.0.0.1`, port `6667` (without the TLS `+`), then run the server, bot, and harness in separate terminals: ``` python microirc_server.py --verbose python pyBurlyBot.py LocalBot.json python microirc.py LocalBot.json ``` The bot and harness should use the same channel; their configured nicknames are already distinct because the harness appends `Test` by default.