Skip to content

Latest commit

 

History

488 Commits

Folders and files

NameName
Last commit message
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.

About

IRC bort

Resources

Stars

5 stars

Watchers

3 watching

Forks

Releases

Packages

Used by

Contributors

Languages