No description
Find a file
forgejo-actions c216f66689 chore(release): 1.52.0 [skip ci]
# [1.52.0](http://forgejo:3000/ircforever/managebot/compare/v1.51.0...v1.52.0) (2026-10-10)

### Features

* **globe:** search lists only people online on IRC, merged across layers ([26bc1aa](26bc1aa4a3))
2026-10-10 13:09:58 +00:00
.forgejo/workflows feat: self-update from releases + publish OpenBSD binary, version stamping 2026-06-17 12:29:22 -04:00
cmd Initial commit: managebot — IRCNow BNC auto-approval bot 2026-06-16 10:05:21 -04:00
config feat(portal): serve the set-password page on account.4evr.sh, not the dashboard domain 2026-10-09 20:19:59 -04:00
db feat(verify): newest link wins and no password strength policy 2026-10-09 16:49:24 -04:00
internal feat(portal): serve the set-password page on account.4evr.sh, not the dashboard domain 2026-10-09 20:19:59 -04:00
test/integration feat(shells): let users pick their own <subdomain>.4evr.sh 2026-06-20 04:13:26 -04:00
web feat(globe): search lists only people online on IRC, merged across layers 2026-10-10 09:03:18 -04:00
.dockerignore Add Logto-OIDC dashboard, management console, and activity log 2026-06-17 08:37:03 -04:00
.env.example feat(irc): welcome joiners of the public channel and say so in !help 2026-10-03 09:38:09 -04:00
.gitignore feat(dashboard): show v6 assignment on request detail page 2026-06-19 05:42:20 +00:00
.releaserc.json feat: self-update from releases + publish OpenBSD binary, version stamping 2026-06-17 12:29:22 -04:00
CHANGELOG.md chore(release): 1.52.0 [skip ci] 2026-10-10 13:09:58 +00:00
docker-compose.yml Initial commit: managebot — IRCNow BNC auto-approval bot 2026-06-16 10:05:21 -04:00
Dockerfile fix(ci): retry bun install on flaky tarball extraction 2026-06-23 06:22:45 -04:00
go.mod feat(verify): staff email re-verification without changing account status 2026-10-09 16:09:50 -04:00
go.sum feat(dashboard): add IP intel via GeoLite2 + on-demand /lookup page 2026-06-19 12:57:58 +00:00
Makefile Initial commit: managebot — IRCNow BNC auto-approval bot 2026-06-16 10:05:21 -04:00
README.md feat(mail): send every email as HTML (ircforever.org style) with text fallback 2026-10-09 19:54:33 -04:00

managebot

An IRC bot that automates IRCNow bouncer (BNC) approvals. It replaces the manual !bnc approve workflow with two questions answered automatically:

  1. Is this a real person? — email verification (click a link or !verify <code>), plus MX/disposable-domain checks and IRC client signals (TLS, CTCP).
  2. Is this someone farming multiple accounts? — correlation across IP, subnet (/24 & /64), normalized email (catches j.o.h.n+x@gmail), and similar nicks, plus DNSBL / Tor-exit / hosting-ASN reputation on the connecting IP.

Each request gets an additive risk score. Below CHECK_APPROVE_THRESHOLD it is auto-approved; at/above CHECK_REJECT_THRESHOLD it is auto-rejected; in between it is escalated to staff in the team channel with the evidence and !approve / !reject buttons.

The only thing kept compatible with the old system is the ZNC control convention: accounts are created via *controlpanel AddUser, gated with *blockuser block (created blocked), approval = unblock, rejection = DelUser.

Architecture

Layered (per team Go standards): cmd → commands → services → repositories → db. Repositories are only used by services; everything is interface-driven and wired by DI in internal/commands/serve.go.

cmd/main.go                       entry point
config/                           env-driven config (configor)
db/                               embedded migrations (golang-migrate)
internal/
  commands/                       cobra CLI: serve, migrate up|down
  db/                             gorm connection + repositories (request, sighting, decision)
  checks/                         signal detectors: ip (DNSBL/Tor/ASN), email (MX/disposable/normalize), similarity
  services/risk/                  aggregates checks + correlation -> verdict
  services/approval/              lifecycle orchestrator (intake -> verify -> assess -> act)
  znc/                            ZNC control-module driver
  irc/                            girc client: command routing + WHOIS enrichment
  http/                           click-to-verify web endpoint + healthz
  mail/                           SMTP verification emails

Verification (either method works)

On !bnc <username> <email> the bot creates the account blocked, emails a code, and waits. The user activates with either:

  • clicking HTTP_BASE_URL/verify?token=... from the email, or
  • typing !verify <code> on IRC.

Only after verification does the risk engine run.

Email verifications with an action

Separately from registration, email_verifications holds verifications that gate an action ("purpose"). Each account has at most one pending link of any purpose: issuing a new one invalidates the old. Tokens are stored only as SHA-256 digests.

  • reverify (dashboard "re-verify email"): staff ask an existing user to re-confirm their address. Completing it (link or !verify <code>) records the confirmation and changes nothing else: status, services and ZNC are untouched. Links last 7 days.
  • password_reset (!bncreset <username>): emails the account owner a link to choose a new bouncer password. Only the person who can read that inbox can complete it:
    • 256-bit single-use tokens, claimed atomically, valid for 1 hour.
    • Opening the link only shows the form; the password is accepted only by the form POST, never over IRC, so mail-scanner prefetches can't use or burn it.
    • The IRC reply is identical for every outcome (no account enumeration) and each nick has a 1-minute cooldown. A new request invalidates the old link.
    • No strength policy. Passwords are only refused if they can't reach ZNC intact: empty, line breaks or control characters, leading/trailing spaces, or over 300 bytes.
    • Only approved accounts with an active ZNC service qualify. Changing the account's email cancels pending links.
    • The new password is never stored or logged; the old generated one is wiped, and the owner is emailed a "password changed" notice.

Emails

Every email is sent as multipart/alternative: the plain-text body plus an HTML version in the ircforever.org style (dark terminal window on a light page). The HTML is built from MJML at development time and committed, so production needs only Go:

cd internal/mail/templates
npm install                    # once; mjml is a dev-only dependency
node build.mjs window html     # src/*.mjml + themes/window.json -> html/*.html

Colours live in themes/*.json (all pairs meet WCAG AA 4.5:1). Values are filled by Go's html/template; if a template ever fails to render, the email still goes out as text.

Running

# Local dev
cp .env.example .env            # fill in IRC_PASSWORD etc.
make setup                      # starts postgres (compose) + runs migrations
make run                        # go run cmd/main.go serve

# Docker (production)
docker compose up -d            # postgres + managebot
docker compose run --rm migrate # apply migrations (profile: tools)

Note: docker-compose.yml publishes Postgres on 5432. If that port is busy, change the published port or run migrations through the migrate service which talks to the container directly.

Configuration

All via environment variables — see .env.example and config/config.go. Key knobs:

Var Default Purpose
IRC_SERVER / IRC_PORT / IRC_PASSWORD — ZNC/IRC login (user/network:pass)
IRC_STAFF — comma-separated nicks allowed !approve/!reject
IRC_PUBLIC_CHANNEL #ircforever public channel the bot sits in and greets joiners of
IRC_WELCOME / IRC_WELCOME_MESSAGE true / built-in NOTICE new joiners of the public channel to stay for updates + support ({nick}, {channel} substituted)
HTTP_BASE_URL http://localhost:8080 origin used in verification links
SMTP_* mail.ircforever.org:25 verification email delivery
CHECK_APPROVE_THRESHOLD 20 auto-approve at/below
CHECK_REJECT_THRESHOLD 60 auto-reject at/above
CHECK_FLAGGED_ASNS — hosting/VPN AS numbers to penalize
CHECK_DNSBLS spamhaus,barracuda,dronebl DNS blocklists

Tests

make test                                   # unit tests
go test -race ./...                         # unit tests, race detector

# Integration (build-tagged `integration`):
go test -tags=integration ./test/integration/...
  • Unit: pure check logic (normalize/disposable/similarity/subnet), the ZNC command contract, the risk verdict thresholds + dedupe signals (mocked detectors), and the full approval lifecycle (in-memory fakes).
  • Integration:
    • postgres_test.go — real Postgres via env (DBHOST/DBPORT/...), runs migrations and exercises every repository. Skips if the DB is unreachable.
    • irc_znc_test.go — spins up an in-process IRC server, connects a real girc client, injects a !bnc, and asserts the bot WHOISes the user and emits the correct *controlpanel/*blockuser control commands end to end.

Commands (IRC)

Command Who Effect
!bnc <username> <email> anyone request a bouncer; creates blocked account + sends verification
!verify <code> requester confirm email, triggers checks
!bncreset <username> anyone email the account owner a 1-hour link to choose a new bouncer password
!approve <username> staff override-approve (unblock)
!reject <username> staff override-reject (deluser)
!help anyone list commands; reminds users that updates/support only happen in the public channel