Statsbotv1.5.0 GitHub

Guide

Warning

Update if your bot's latest commit is older than 2026-09-30. Earlier versions have a security hole in the public stats page: text from IRC (quotes, topics, kick reasons, /me lines, words, URLs) was not escaped, so anyone who can talk in a channel the bot tracks could run script in the browser of everybody who opens that channel's page. Host-mask auto-login was also broken (and would have matched on a bare nick).

Version numbers only start with 1.1.0, so an older bot cannot tell you its version (python main.py --version does not exist there). Check the date of its latest commit instead:

git log -1 --format='%h %cs'      # date of your latest commit; it must be 2026-09-30 or later
[[ $(git log -1 --format=%cs) < 2026-09-30 ]] && echo "OUTDATED: update now" || echo "up to date"

To update:

git pull
pip install -r requirements.txt
# restart the bot; the database updates itself
python main.py --version        # 1.1.0 or newer

Details and upgrade notes: CHANGELOG.md.

A modern IRC statistics bot inspired by pisg, built for the 21st century. Instead of parsing log files after the fact, Statsbot connects to IRC as a bot, collects statistics in real time, and serves a live web dashboard — no log files, no cron jobs, no static HTML generation.


Note

Statsbot is, pretty much, feature-complete — but the door is always open.
After all these commits, Statsbot does everything the classic PISG does, except it doesn't need log files,
doesn't need a cron job, doesn't generate static HTML, and doesn't make you feel like it's 2003.
It connects to IRC, watches what happens, and serves live stats — no logs, no hassle, no excuses.
Is it finished? Pretty much. Is it perfect? Probably not. Found a bug? Open an issue. Have a wild idea? Open a PR.
Speak French, Italian or Portuguese and spotted something that could sound better? You know what to do.
The IRC scene survived the death of mIRC scripts, the rise of Discord, and seventeen different attempts to replace IRC with something "better".
Statsbot will survive your pull request.


Why Statsbot instead of pisg?#

Compared with pisg 1.0a (2026-09-21). pisg is the model for the page, and it does things Statsbot does not (see the end of this file); the difference is where the numbers come from.

pisg Statsbot
Data source Parses IRC log files Live IRC connection
Setup A bot that writes logs, a scheduler, and pisg (its guided setup.pl helps) Just run Statsbot
Log formats 46 parsers, one per client / bot format, each to keep in step with its client Not needed: Statsbot is the client, it reads the IRC protocol itself
Output Static HTML, regenerated on a schedule Live web server
Stats periods Whatever the logs cover, plus a chart of the last N days All-time, today, week, month (tabs)
Peak users ✗ ✓ with timestamp
Live user count ✗ ✓ updates every 30s
Karma (nick++ / nick--) ✓ ±1 per pair, nick== resets ✓ cumulative, no reset
Op/voice/halfop stats ✓ ✓
Several networks ✓ one config can list channels of different networks, each read from its own log ✓ one live process connects to all of them
Managed from IRC Users can set their own info (optional Eggdrop script) Channels, networks, ignores and page options over PM
Per-channel page options ✓ <channel> blocks override <set> ✓ via PM or config.yml, live rehash
Languages 30 (the new 1.0a sections: English and French only) 5 (en, pt, fr, it, nl), per channel
Colour schemes 14 One, light or dark

If you already know pisg, the pisg: section in config.yml uses the same option names — ActiveNicks, ShowBigNumbers, WordHistory, etc. — so the pisg docs apply directly.


Features#

  • Tracks words, lines, letters, actions, kicks, modes, bans, joins, topics, minutes online, smileys (happy + sad separately), questions, CAPS lines, violent actions, foul language, monologues
  • Who talks to whom — a map of the most connected nicks (people who mostly talk to each other are grouped in coloured arcs; built to stay readable on busy channels; click a nick to put it in the middle with the people it talks to around it) and a table of the closest pairs
  • Karma — nick++ / nick-- suffix syntax; only counts if the target is in the channel; nicks containing -- or ++ are handled correctly (e.g. Mike---- awards −1 to nick Mike--)
  • Big numbers — questions, shouting %, CAPS %, violence + victim tracking with example lines, smiles %, sad %, line lengths, monologues, words per line
  • Other interesting numbers — kicks given/received, most actions, most joins, most nick changes, foul language %
  • Most active by hour — 4-band table (0–5, 6–11, 12–17, 18–23)
  • Most used words — filterable by length and ignore list, with last-used-by nick
  • Most referenced nicks — who gets mentioned most in conversation, displayed with original casing
  • Smiley frequency table — which specific smiley used most, and by whom
  • Most referenced URLs — deduplicated with use count and last poster
  • Latest topics with setter and timestamp
  • Random quotes in the main table — length-filtered (MinQuote/MaxQuote), first message always logged so new speakers never show blank
  • Period tabs — all-time, today, this week, this month
  • Peak users with date
  • Live user count badge, updates every 30 seconds
  • Multi-network — connect to Libera, Undernet, PTirc simultaneously
  • Runtime network management — add/remove networks and channels live via PM commands, no restart required; config.yml seeds the DB on first run only
  • Per-channel pisg config — any pisg: option can be overridden per channel via PM (pisg #channel set <key> <value>) or via channel_pisg: in config.yml; rehash/reload re-reads the config live without restarting
  • Op/voice/halfop stats — who gave ops, who got deopped, who hands out voice; pisg-style prose sentences; ShowOps, ShowVoice, ShowHalfops toggles
  • Fully clickable cards on the landing and network pages
  • PM admin interface — identify, ignore management, master management
  • bcrypt password auth — session lasts until disconnect, works from any nick
  • Auto-auth via hostmask — silent authentication on join if host matches
  • Channel-scoped ignores — per-channel or network-wide
  • Automatic DB migrations — upgrading never requires manual schema changes

Requirements#

  • Python 3.11+
  • A virtualenv (required on Debian 12+, Ubuntu 24.04+, and any distro that ships PEP 668 — direct pip install into the system Python is blocked)

Quick start#

# 1. Clone
git clone https://github.com/TehPeGaSuS/Statsbot.git
cd Statsbot

# 2. Create and activate a virtualenv
python3 -m venv .venv
source .venv/bin/activate

# 3. Install dependencies
pip install -r requirements.txt

# 4. Configure
cp config/config.yml.example config/config.yml
nano config/config.yml          # set your networks, channels, nick

# 5. Set up master password(s) — interactive wizard
python main.py --setup

# 6. Run
python main.py

Debian 12+ / Ubuntu 24.04+ note: these distros enforce PEP 668 and block pip install outside a virtualenv. Always use .venv as shown above.

The web dashboard is at http://localhost:8033/ by default.


Configuration overview#

bot:        # nick, altnick, realname, ident
networks:   # list of IRC servers and channels
stats:      # tracking options, ignore lists, smiley lists
web:        # dashboard host/port/public_url
commands:   # prefix, flood protection
pisg:       # page layout (pisg-compatible option names)
database:   # SQLite path
logging:    # level and log file

See DOCS.md for the complete reference.


IRC commands#

Channel commands#

Command Description
!stats Link to the stats page for this channel
!top [n] Top N users by words (default 3, max 10)
!quote [nick] Random quote, optionally from a specific nick

PM commands (/msg statsbot <command>)#

Command Description
identify <master> <password> Authenticate — works from any nick on any network
logout / whoami / status Session management
ignore add [#chan] <pattern> Add ignore (network-wide if no #chan)
ignore del [#chan] <pattern> Remove ignore
ignore list [#chan] List ignores
master add <nick> Add master (bot asks for password interactively)
master del <nick> / master list Manage masters
set page [#chan] <url> Override !stats URL for a channel
setlang [-network <net>] #channel <lang> Set stats page language for a channel (en_US, pt_PT, fr_FR, it_IT)
nets List all networks (host, port, SSL status)
chans List channels tracked on the current network
addnet -name <n> -host <host> -port <port> [-ssl\|-plaintext] Add network and connect immediately (TLS by default)
delnet -name <n> Remove network and delete all its stats
addchan [-network <net>] #channel Join and track a channel
delchan [-network <net>] #channel Part channel and delete all its stats
join [-network <net>] #channel [key] Ad-hoc join without adding to DB (useful after a ban is lifted)
part [-network <net>] #channel [reason] Ad-hoc part without removing from DB

Stats page URL structure#

http://yourserver:8033/                        — all networks (clickable cards)
http://yourserver:8033/<network>/              — channels on a network (clickable cards)
http://yourserver:8033/<network>/<channel>/    — full pisg-style stats page
http://yourserver:8033/<network>/<channel>/?period=1   — today

<channel> is the channel name without its #. Channels with more than one # are three different channels, so they get the number of hashes in front:

Channel Address
#lifeline /<network>/lifeline/
##lifeline /<network>/2/lifeline/
###lifeline /<network>/3/lifeline/

Period values: 0 = all-time (default), 1 = today, 2 = this week, 3 = this month.

Set web.public_url in config so !stats generates proper external links:

web:
  public_url: "https://stats.yourserver.org"

Karma#

Users can award or deduct karma points by appending ++ or -- to a nick:

<Alice> Bob++
<Alice> Bob--

Rules: - Suffix only — nick++ and nick--; prefix forms (++nick) are not recognised - Channel membership — the target nick must currently be in the channel; random words ending in ++/-- are silently ignored - No self-karma — you cannot modify your own score - Nicks containing -- or ++ are handled correctly: Mike---- strips exactly the last two characters, awarding −1 to nick Mike--

Karma scores appear in the Karma section of the channel stats page, sorted highest to lowest. Negative scores are shown in red. Toggle with ShowKarma in the pisg: config section.


Running as a service#

Two options: a user unit (recommended — no root required, starts on login or boot if lingering is enabled) or a system unit (runs as a dedicated user, requires root to install).

# ~/.config/systemd/user/statsbot.service
[Unit]
Description=Statsbot IRC statistics bot
After=network.target

[Service]
Type=simple
WorkingDirectory=%h/Statsbot
ExecStart=%h/Statsbot/.venv/bin/python main.py
Restart=on-failure
RestartSec=30
Environment=PYTHONUNBUFFERED=1

[Install]
WantedBy=default.target
# Install and start
systemctl --user daemon-reload
systemctl --user enable --now statsbot

# Optional: keep running after logout (requires root once)
loginctl enable-linger $USER

# Useful commands
systemctl --user status statsbot
journalctl --user -u statsbot -f

%h expands to your home directory. Adjust WorkingDirectory and ExecStart if you cloned Statsbot somewhere other than ~/Statsbot.

System unit#

Use this if you want Statsbot to run as a dedicated system user (e.g. statsbot) rather than your own account.

# /etc/systemd/system/statsbot.service
[Unit]
Description=Statsbot IRC statistics bot
After=network.target

[Service]
Type=simple
User=statsbot
WorkingDirectory=/opt/statsbot
ExecStart=/opt/statsbot/.venv/bin/python main.py
Restart=on-failure
RestartSec=30
Environment=PYTHONUNBUFFERED=1

[Install]
WantedBy=multi-user.target
# Create a dedicated user (no login shell, no home directory)
sudo useradd -r -s /usr/sbin/nologin -d /opt/statsbot statsbot
sudo mkdir -p /opt/statsbot
sudo chown statsbot:statsbot /opt/statsbot

# Install and start (as root)
sudo systemctl daemon-reload
sudo systemctl enable --now statsbot

# Logs
sudo journalctl -u statsbot -f

Project structure#

Statsbot/
├── main.py                  # Entry point, --setup wizard, --version
├── version.py               # The version number (see CHANGELOG.md)
├── slugs.py                 # Channel name <-> URL path (#a, ##a and ###a differ)
├── CHANGELOG.md
├── tests/                   # pytest suite: python -m pytest
├── tools/build_docs.py      # builds docs/index.html from README/DOCS/TRANSLATE/CHANGELOG
├── docs/                    # the generated documentation site (what Cloudflare serves)
├── wrangler.jsonc           # Cloudflare Workers config for the documentation site
├── config/
│   └── config.yml           # All configuration
├── bot/
│   ├── auth.py              # Master auth — sessions, bcrypt, auto-auth via mask
│   ├── connector.py         # Async IRC connection, RFC 1459 parser, WHO handler
│   ├── parser.py            # Message parsing: words, smileys, caps, violent, foul
│   ├── sensors.py           # Event handlers — stats, quotes, karma, monologues
│   └── scheduler.py         # Daily/weekly/monthly stat resets
├── database/
│   └── models.py            # SQLite schema, all queries, auto-migrations
├── irc/
│   ├── commands.py          # Channel commands: !stats, !top, !quote
│   └── pm_commands.py       # PM admin: identify, ignore, master, set
└── web/
    ├── dashboard.py         # Flask: landing, network pages, JSON API, pisg config view
    ├── pisg_config_page.py  # Read-only per-channel pisg config page (token-gated)
    └── pisg_page.py         # Full pisg-style channel stats page

Contributing#

Issues and pull requests are welcome. If you're adding a feature, please:

  • Follow the existing code style (no external deps beyond requirements.txt)
  • Add a test in tests/ (pip install -r requirements-dev.txt && python -m pytest)
  • For a user-visible change, bump version.py and add a section to CHANGELOG.md
  • After editing README.md, DOCS.md, TRANSLATE.md, CHANGELOG.md or version.py, run python tools/build_docs.py to regenerate the documentation site (docs/index.html); a test fails if you forget
  • Update DOCS.md if you add or change a config option

Want to add or improve a translation? See TRANSLATE.md — it covers the rules, the placeholders, and what not to translate, in English, Portuguese, French and Italian.

If you're familiar with pisg, feature parity PRs are especially welcome — see What's not yet implemented for what's missing.

What's not yet implemented vs pisg#

pisg feature Status
User pictures Not yet, probably never will
Gender stats Not yet, probably never will
NickTracking / nick aliases Not yet, probably never will
Music charts (now playing:) Not yet, probably never will
pisg 1.0a: social roles, time personalities, "who carries the channel", signature words, channel overview Not yet
pisg 1.0a: extra colour schemes, section navigation menu, landing page that compares channels Not yet (Statsbot has a landing page and light / dark)

Everything else the classic pisg pages had is implemented. Contributions welcome.

Configuration reference

This document covers all configuration options for Statsbot. Option names in the pisg: section deliberately match pisg so that anyone familiar with pisg can migrate without reading much.


Bot settings#

bot:
  nick: "statsbot"       # Primary nick
  altnick: "statsbot_"   # Fallback nick if primary is taken
  realname: "IRC Stats Bot"
  ident: "statsbot"
  # Masters are configured via: python main.py --setup

Masters (users allowed to run admin commands) are stored in the database with bcrypt-hashed passwords. Use python main.py --setup to add them interactively. See PM commands for runtime management.


Network settings#

config.yml is a seed file, not the source of truth. On first run, networks and channels from config.yml are inserted into the database (INSERT OR IGNORE). After that the database wins. Add, remove, or modify networks and channels at runtime using PM commands — no restart required.

networks:
  - name: "libera"                  # Internal name — used in URLs and DB
    host: "irc.libera.chat"
    port: 6697                      # 6667 plain, 6697 SSL
    ssl: true
    channels:
      - "#yourchannel"

    # ── Authentication (pick one or combine) ──────────────────────────────

    # SASL PLAIN — preferred on modern networks (Libera, OFTC, hackint …)
    sasl:
      username: "statsbot"
      password: "your_password"

    # NickServ IDENTIFY — legacy, works on most networks including Undernet
    # nickserv_password: "your_password"

    # Nick ghosting — if primary nick is taken, switch to altnick and reclaim
    # the primary nick via NickServ. Requires nickserv_password or sasl.password.
    # ghost: true
    # ghost_command: "GHOST"   # Use "RELEASE" for DALnet (default: GHOST)

    # Server password — for BNCs or private servers that require PASS
    # server_password: "your_password"

    # ── Post-connect commands ─────────────────────────────────────────────
    # Sent after authentication completes. {nick} = current bot nick.
    # on_connect:
    #   - "MODE {nick} +x"      # request cloak (Undernet, others)
    #   - "UMODE2 +x"           # alternative cloak (UnrealIRCd)
    #   - "MODE {nick} +B"      # set bot flag

    # ── Per-channel pisg overrides ────────────────────────────────────────
    # Any key from the global pisg: section can be overridden here per channel.
    # These are applied when the stats page is rendered — no restart required
    # after a rehash. PM commands (pisg #channel set/reset) update the DB and
    # take effect immediately, independent of this section.
    #
    # channel_pisg:
    #   "#yourchannel":
    #     DailyActivity: 90      # 90-day graph instead of global 30
    #     ShowVoice: true        # enable voice section on this channel only
    #
    #   "#quietchannel":
    #     ActiveNicks: 10        # smaller top table
    #     ShowBigNumbers: false  # disable big numbers section entirely
    #     DailyActivity: 0       # disable daily activity graph
    # join_retries: 5            # max attempts per channel (default 5)
    # join_retry_delay: 30       # seconds between attempts (default 30)
    # join_confirm_timeout: 30   # retry a JOIN the server never answered (default 30)
    # channel_audit_interval: 120 # periodic re-JOIN sweep (default 120)
    # ping_interval: 120         # seconds of silence before we PING (default 120)
    # ping_timeout: 90           # seconds after that PING before dropping (default 90)

Channel join retries#

The bot tracks join state per channel. When the server confirms a self-JOIN the channel is marked joined and any pending retry is cancelled. If the join fails with one of the numerics below, a retry is scheduled with join_retry_delay seconds between attempts, up to join_retries total:

Numeric Meaning
437 Nick/channel temporarily unavailable (DALnet)
471 Channel is full (+l)
473 Invite-only (+i)
474 Banned from channel (+b)
475 Bad channel key (+k)
477 Channel requires a registered nick / SASL

Both options can also be set globally under bot: and are overridden per-network when present. Defaults: 5 retries, 30 second delay.

A JOIN can also get no answer at all — no join echo and no error numeric. That happens routinely during a netsplit, when the JOIN reaches a server that is desynced or still merging. join_confirm_timeout (default 30, 0 disables) arms a watchdog per JOIN; if nothing comes back in that window the join enters the same retry ladder above.

As a final backstop, every channel_audit_interval seconds (default 120, 0 disables) the bot sweeps its configured channel list and re-JOINs anything it is not in and has no retry pending for. Channels whose retry ladder has been fully exhausted are skipped by the audit, so a permanently banned channel isn't retried forever; that state is cleared on reconnect and on a manual join.

Connection liveness (netsplit recovery)#

A netsplit frequently leaves the TCP connection half-open: the peer stops sending but never sends FIN or RST. Nothing surfaces at the socket layer, so a bot that only reacts to server PINGs will block on the read forever, still believing it is connected and joined, until it is restarted by hand.

The bot guards against this at three levels:

Guard Behaviour
ping_interval (default 120) After this many seconds with no data from the server, send our own PING.
ping_timeout (default 90) If the server is still silent this long after that PING, declare the link dead, close the socket and let the reconnect loop run.
TCP keepalive Enabled on the socket (60s idle, 15s interval, 4 probes) as a second line of defence under the application-level check.

An ERROR line from the server — sent by most ircds immediately before they close a link — also tears the connection down at once rather than waiting for the socket to notice.

Both options can be set globally under bot: or per-network. Because any byte from the server counts as proof of life, a busy channel means the keepalive PING is rarely sent in practice.

Authentication flow#

  1. If server_password is set, PASS is sent before NICK/USER.
  2. If sasl is set, CAP REQ :sasl is sent before NICK/USER. On success the bot sends AUTHENTICATE PLAIN → base64(user\0user\0pass) → CAP END. If the server rejects SASL, falls back to NickServ.
  3. On 001 (welcome), if SASL didn't auth, NickServ IDENTIFY is sent.
  4. If ghost: true and the primary nick is taken (433), the bot: - Switches to altnick - Sends PRIVMSG NickServ :GHOST <nick> <password> - Watches for the confirmation notice and then sends NICK <primary>
  5. on_connect commands fire immediately on 001, independent of authentication. They run whether or not NickServ or SASL auth is configured.

Per-network identity#

Any of the four identity fields (nick, altnick, realname, ident) set inside a network entry override the global bot: defaults for that network only. This lets the bot present different nicks on different networks:

bot:
  nick: "statsbot"          # used everywhere unless overridden
  altnick: "statsbot_"

networks:
  - name: "libera"
    host: "irc.libera.chat"
    # inherits bot.nick = "statsbot"

  - name: "undernet"
    host: "irc.undernet.org"
    nick: "ChanStatsBot"    # overrides bot.nick on Undernet only
    altnick: "ChanStats_"

{nick} in on_connect commands substitutes the actual nick the bot is currently using on that network (could be altnick if primary was taken).

Multiple networks#

Add more entries to the networks: list — each connects independently:

networks:
  - name: "libera"
    host: "irc.libera.chat"
    port: 6697
    ssl: true
    channels:
      - "#bots"
    sasl:
      username: "statsbot"
      password: "libera_pass"

  - name: "undernet"
    host: "irc.undernet.org"
    port: 6667
    ssl: false
    channels:
      - "#allnitecafe"
    nickserv_password: "undernet_pass"
    ghost: true
    on_connect:
      - "MODE {nick} +x"

  - name: "ptirc"
    host: "irc.ptirc.org"
    port: 6667
    ssl: false
    channels:
      - "#lobby"
      - "#help"

Each network gets its own session, ignore list, and stats namespace. The web dashboard groups channels by network automatically.


Stats tracking#

stats:
  ignore:
    - "ChanServ"
    - "*Serv"
    - "*bot*"
    - "*!*@services.example.org"  # full hostmask pattern
  # Channel-specific ignores via PM: /msg statsbot ignore add #chan pattern

  auto_ignore_bots: false   # Auto-ignore nicks with IRC +B umode. Unreliable —
                             # real users can set +B. Default off; use ignore list.

  expire_days: 30           # Remove nicks not seen for X days (0 = never)
  log_wordstats: true       # Track word frequency per nick
  quote_frequency: 5        # Log every Nth message as a random quote.
                             # First message from any nick is always logged.
  kick_context: 5           # Lines of channel context saved with each kick

  happy_smileys: [...]      # List of smileys counted as happy (smileys stat)
  sad_smileys: [...]        # List of smileys counted as sad (sad stat)

Times are UTC#

Every time Statsbot keeps or shows is UTC, whatever the server's time zone: the hour buckets behind "Most active times", the dates of the daily activity chart, topic times, "Statistics generated on", the dashboard and the log file. The daily, weekly (Monday) and monthly (1st) resets happen at 00:00 UTC. Pages say so; nothing is shifted to the visitor's zone.

Ignore patterns#

Patterns support wildcards (*, ?). Two forms are recognised:

  • Nick pattern — no ! or @: matches against the nick only
  • ChanServ, *bot*, *Serv
  • Hostmask pattern — contains ! or @: matched against nick!user@host
  • *!*@services.ptirc.org, badnick!*@*

Config-level ignores apply network-wide. Channel-specific ignores can be added at runtime via PM commands.


Web dashboard#

web:
  enabled: true
  host: "0.0.0.0"
  port: 8033
  public_url: ""     # e.g. https://stats.yourserver.org
                     # Used by !stats to generate the channel link.
                     # If empty, falls back to http://localhost:PORT/
  title: "IRC Stats"
  project_url: "https://github.com/TehPeGaSuS/Statsbot"  # link shown in page footers
  docs_url: "https://statsbot-docs.v-m-m-l.workers.dev/"  # "Docs" link in page footers; "" hides it

The dashboard serves three pages:

URL Description
/ Landing page — network cards (whole card is clickable)
/<network>/ Network page — channel cards (whole card is clickable)
/<network>/<channel>/ Full pisg-style channel stats page

The landing page shows each network as a card displaying the network name, IRC server address, user count, and channel count. Clicking anywhere on the card navigates to the network page. The network page shows each channel as a card with tracked users, total words, and total lines; clicking navigates to the full channel stats page.

Live user count on the channel page polls /api/<network>/<channel>/online every 30 seconds via JavaScript.


IRC commands#

The default command prefix is !, set globally under commands.prefix in config.yml. If a network already uses ! for something else (e.g. Anope BotServ fantasy commands), you can override it per network with cmd_prefix:

commands:
  prefix: "!"               # global default

networks:
  - name: "libera"
    host: "irc.libera.chat"
    # inherits prefix "!"

  - name: "undernet"
    host: "irc.undernet.org"
    cmd_prefix: "."         # use . on this network instead

Channel commands (public)#

Command Description
!stats Link to stats page for this channel
!top [n] Top N users by words, single line (default 3, max 10)
!quote [nick] Random quote, optionally from a specific nick

PM commands (/msg statsbot <command>)#

Authentication:

Command Description
identify <master_nick> <password> Authenticate as a master. Works from any nick on any network. Session lasts until you disconnect.
logout End your session
whoami Show your current identity
status Show connected channels and user counts

Ignore management (requires auth):

Command Description
ignore add [#channel] <pattern> Add ignore. Omit #channel for network-wide.
ignore del [#channel] <pattern> Remove ignore
ignore list [#channel] List ignores, optionally filtered by channel

Master management (requires auth):

Command Description
master add <nick> Add a master (bot will ask for password interactively)
master del <nick> Remove a master
master list List all masters

Configuration (requires auth):

Command Description
set page [#channel] <url> Override the stats URL returned by !stats
setlang [-network <net>] #channel <lang> Set the stats page language (en_US, pt_PT, fr_FR, it_IT)
pisg #channel list Generate a short-lived link to view effective pisg config for a channel
pisg #channel set <key> <value> Set a per-channel pisg override — takes effect immediately
pisg #channel reset [key] Remove one override or all of them (no key = all)
rehash / reload Re-read config.yml from disk and apply changes live — no restart required

Network management (requires auth):

Command Description
nets List all networks in the database with host, port, SSL status
chans List channels tracked on the current network
addnet -name <n> -host <host> -port <port> [-ssl\|-plaintext] Add a new network and connect immediately. TLS is the default — pass -plaintext to disable.
delnet -name <n> Disconnect a network and permanently delete all its stats
addchan [-network <net>] #channel Join and start tracking a channel. -network is optional if you're already on that network.
delchan [-network <net>] #channel Part a channel and permanently delete all its stats
join [-network <net>] #channel [key] Ad-hoc join a channel without adding it to the database. Useful after a ban is lifted or to re-attempt a failed join. Optional key for +k channels.
part [-network <net>] #channel [reason] Ad-hoc part a channel without removing it from the database. The bot will rejoin it on the next reconnect.

join/part vs addchan/delchan: join/part are runtime-only and do not touch the database — use them for temporary actions. addchan and delchan modify persistent tracking (and delchan deletes stats).

Destructive operations: delnet and delchan cascade-delete all stats, quotes, URLs, topics, kick logs, karma, and hourly data for that network/channel. There is no undo.

Examples:

addnet -name SwiftIRC -host irc.swiftirc.net -port 6697
addnet -name Rizon -host irc.rizon.net -port 6667 -plaintext
delnet -name SwiftIRC

addchan #general                        (on current network)
addchan -network SwiftIRC #general      (on a different network)
delchan #general
delchan -network SwiftIRC #general

join #general                           (rejoin after a ban is lifted)
join #secret hunter2                    (with channel key)
join -network SwiftIRC #general
part #general
part #general going for lunch

pisg-style page options#

All options go under the pisg: section in config.yml. Names match pisg exactly so the pisg documentation applies.

Per-channel overrides#

Any option from this section can be overridden for a specific channel in two ways:

In config.yml under channel_pisg: inside a network entry (applied after rehash):

networks:
  - name: "libera"
    channels:
      - "#busy"
      - "#quiet"
    channel_pisg:
      "#busy":
        DailyActivity: 90
        ActiveNicks: 50
      "#quiet":
        ShowBigNumbers: false
        DailyActivity: 0

Via PM (takes effect immediately, stored in the DB):

/msg statsbot pisg #channel set DailyActivity 90
/msg statsbot pisg #channel set ShowBigNumbers false
/msg statsbot pisg #channel reset DailyActivity    ← remove override, global applies again
/msg statsbot pisg #channel reset                  ← remove all overrides for channel
/msg statsbot pisg #channel list                   ← get a link to view effective config

DB overrides (PM) take precedence over channel_pisg: in config.yml, which takes precedence over the global pisg: section.

Main nick table#

DailyActivity#

Number of days to show in the "Daily activity" bar chart below the hourly activity chart. Each bar represents one day; bars scroll horizontally if there are more than fit on screen. Set to 0 to disable the chart entirely. Default: 30

ActiveNicks#

Number of nicks shown in the "Most active nicks" table. Default: 25 — pisg default: 25

ActiveNicks2#

Number of nicks shown in the "These didn't make it to the top" secondary list. Default: 10 — pisg default: 30

SortByWords#

Sort the main nick table by words (true) or lines (false). Default: true — pisg default: false (lines)

ShowLines#

Show the line count column in the main nick table. Default: true

ShowWords#

Show the word count column in the main nick table. Default: true — pisg default: false

ShowWpl#

Show the words-per-line column. Default: true — pisg default: false

ShowCpl#

Show the characters-per-line column. Default: false — pisg default: false

ShowLastSeen#

Show when each nick was last seen (e.g. "2 days ago"). Default: true

ShowRandQuote#

Show a random quote from each nick in the main table. Default: true

MinQuote#

Minimum character length for a quote to be selected for display. Falls back to any quote if none found in range. Default: 25 — pisg default: 25

MaxQuote#

Maximum character length for a quote to be selected. Default: 65 — pisg default: 65


Big numbers#

ShowBigNumbers#

Master switch for the "Big numbers" and "Other interesting numbers" sections. Setting this to false hides both sections entirely. Default: true

BigNumbersThreshold#

Minimum lines a nick must have to appear in big numbers statistics (questions, CAPS, smileys, line lengths, sad faces). Set to "sqrt" for automatic threshold (square root of the most active nick's line count). Default: "sqrt" — pisg default: sqrt

ViolentWords#

Words considered aggressive/violent. Used to detect /me slaps style actions. The first nick mentioned after a violent word is recorded as the victim. Default: ["slaps", "beats", "kicks", "hits", "smacks", "stabs"] — pisg default: slaps beats smacks

FoulWords#

Words considered foul language. Tracked as a percentage of total words. Default: ["ass", "fuck", "shit", "bitch", "cunt", "cock", "dick"]

Note: In Statsbot, individual big number sub-sections (questions, CAPS, violence, smiles, etc.) are all controlled by ShowBigNumbers. In pisg they were always on when ShowBigNumbers was on. Future versions may add individual toggles.


Section toggles#

ShowTime#

Show the activity time-band bars in the main nick table — coloured bars indicating when each nick is most active (four 6-hour bands: night, morning, afternoon, evening). pisg equivalent: ShowTime. Default: true — pisg default: true

ShowMostActiveByHour#

Show the "Most active nicks by hour" table, split into four 6-hour bands (0–5, 6–11, 12–17, 18–23). pisg equivalent: ShowMostActiveByHour. Default: true — pisg default: false

ShowSmileys#

Show the smiley frequency table — which specific smileys were used most and by whom. Default: true — pisg default: false

ShowMrn#

Show the "Most referenced nicks" section — which nicks are mentioned most in conversation. Nick casing is preserved as it appears on IRC. Default: true — pisg default: true

ShowMuw#

Show the "Most used words" section. Default: true

ShowMru#

Show the "Most referenced URLs" section. URLs are deduplicated — repeated posts of the same URL increment the count. Default: true — pisg default: true

ShowOps#

Show the "Ops, voice and halfops" section — who gave ops most, who got deopped, who hands out voice, etc. Rendered as pisg-style prose sentences. Only appears if op events have actually been recorded; silently omitted on channels with no op activity. Default: true — pisg default: true

ShowVoice#

Show voice-related sentences within the ops section (+v/-v). Default: true — pisg default: true

ShowHalfops#

Show halfop sentences within the ops section (+h/-h). Halfops are supported on some IRC networks (UnrealIRCd, InspIRCd). Silently omitted if no halfop events have been recorded. Default: true — pisg default: true

ShowLegend#

Show the page legend/footer with totals (lines, nicks, avg wpl, avg cpl, topics set count). Default: true


History limits#

WordHistory#

Maximum number of words shown in "Most used words". Default: 10 — pisg default: 10

WordLength#

Minimum word length to appear in "Most used words". Words shorter than this are filtered from display (but still tracked in the DB). Default: 5 — pisg default: 5

IgnoreWords#

Space or list of words to exclude from "Most used words" and "Most referenced nicks" detection. Useful for filtering common words. Default: [] (empty)

IgnoreWords: ["there", "about", "think", "would", "going"]

NickHistory#

Maximum number of nicks shown in "Most referenced nicks". Default: 5 — pisg default: 5

SmileyHistory#

Maximum number of smileys shown in the smiley frequency table. Default: 10 — pisg default: 10

UrlHistory#

Maximum number of URLs shown in "Most referenced URLs". Default: 10 — pisg default: 5

TopicHistory#

Maximum number of topics shown in "Latest topics". Default: 5 — pisg default: 3

ActiveNicksByHour#

Number of rows in the "Most active nicks by hour" table (per band). Default: 10 — pisg default: 10


Relation map options#

The "Who talks to whom" section draws the nicks that talk to each other most, on a ring: people who mostly talk to each other are grouped in the same coloured arc, and the width of a link is the number of mentions. Below it, the "Closest pairs" table lists the strongest links. A link is counted each time a line mentions a nick that is in the channel. The section follows the period tabs.

Hover a nick to see its links. Click it, or pick it from the "Focus on" list, to put it in the middle with the people it talks to around it (closer means more mentions); click it again to go back to the ring. Dots are sized by the lines each nick said. This needs JavaScript; without it you still get the folded map and the "Closest pairs" table.

ShowRelations#

Show the section. Default: true

RelationNicks#

How many nicks the map draws (5-60), chosen by how connected they are. The reader can show fewer with a slider. Default: 40

RelationMinWeight#

Mentions a link needs to be visible at first; the reader can change it with a slider. 0 picks a value that shows about two links per nick. Default: 0

RelationPairs#

Rows in the "Closest pairs" table (0 hides it). Default: 10

RelationMap#

The map is a big picture, so it is folded away under "Show the map": closed (default), open to show it unfolded, or off to leave it out and keep only the pairs table. With fewer than four nicks there is no map. Default: closed

The pairs are kept in the database; ones seen fewer than three times and not for stats.pair_keep_days days (default 60) are forgotten every night.

Karma options#

ShowKarma#

Show the Karma leaderboard section on the channel stats page. Scores are sorted highest to lowest; positive scores are green, negative are red. Default: true

KarmaHistory#

Maximum number of nicks shown in the karma table. Default: 10

How karma works#

Users award or deduct karma by appending ++ or -- to a nick in any channel message:

<Alice> Bob++        ← Bob's score +1
<Alice> Bob--        ← Bob's score -1

Rules enforced by the bot:

  • Suffix only — nick++ / nick--. Prefix forms (++nick) are ignored.
  • Channel membership required — the target nick must be present in the channel at the time of the message. Messages referencing absent nicks or random words ending in ++/-- are silently ignored.
  • No self-karma — a user cannot modify their own score.
  • Nicks containing -- or ++ are handled correctly. The bot strips exactly the last two characters to find the nick, so Mike---- awards −1 to nick Mike--, not to nick Mike.
  • Karma is stored per-channel; a nick's score on one network/channel is independent of any other.

Database#

database:
  path: "data/stats.db"    # Path to SQLite database file

The database is SQLite — zero setup, zero dependencies beyond Python's built-in sqlite3. Schema migrations run automatically on startup so upgrading Statsbot never requires manual DB changes.


Logging#

logging:
  level: "INFO"             # DEBUG, INFO, WARNING, ERROR
  file: "data/ircstats.log"

Differences from pisg#

Compared with pisg 1.0a (2026-09-21).

Feature pisg Statsbot
Data source Log files Live IRC bot (real-time)
Log parsers 46 formats, one per client / bot Not needed — Statsbot is the client and receives the events directly, so no client's log format can break it
Tracking Per-mask (nick!user@host) Per-nick
Nick merging Aliases + NickTracking (an optional script merges nicks by account) Not supported (each nick is its own entry)
Output Static HTML file Live Flask web server
Periods Whatever the logs cover, plus a last-N-days chart All-time, today, this week, this month
Peak users Not tracked Tracked with timestamp
Live count Not possible Yes, polls every 30s
Several networks Channels of different networks in one config, each from its own log One live process for all networks
Per-channel options <channel> blocks override <set> Per channel via PM or config.yml
Languages 30 5 (en, pt, fr, it, nl)
Karma (nick++) Yes Yes
Op/voice/halfop stats Yes Yes
User pictures Yes Not yet
Gender stats Yes Not yet
Music charts Yes Not yet
Daily activity graph Yes Yes — scrollable bar chart, configurable per channel
NickTracking / aliases Yes Not yet
Relation map and closest pairs (new in 1.0a) Yes Yes — a ring with communities, meant for big channels
Social roles, time personalities (new in 1.0a) Yes Not yet
Colour schemes 14 One (light / dark)

Translating

Português · Français · Italiano · Nederlands — see below for your language.


Note

This page shows the English text. The guide is also written in Português, Français, Italiano and Nederlands: see TRANSLATE.md.

How it works#

Statsbot uses standard PO files (locale/<lang>.po). Each entry looks like:

msgid "{nick} is a very aggressive person. They attacked others {count} times."
msgstr "{nick} é uma pessoa muito agressiva. Atacou outros {count} vezes."

The msgid is the English string — do not translate or modify msgid lines. Only edit the msgstr line. en_US.po has empty msgstr throughout and serves as the reference catalogue; do not add translations to it.

Adding a new language#

  1. Copy locale/en_US.po to locale/<lang>.po (e.g. locale/de_DE.po) Currently supported: en_US, pt_PT, fr_FR, it_IT, nl_NL
  2. Update the Language: header
  3. Add your language code to SUPPORTED in i18n.py
  4. Translate every msgstr line

Rules#

Must keep — these will break the page if removed or renamed:

  • {nick} — IRC nickname, inserted by the bot
  • {nick2} — second IRC nickname (used in follower strings)
  • {count} — a number
  • {pct} — a percentage value (already formatted, e.g. 27.3)
  • {avg} — an average value
  • {n} — a number (days, nicks, etc.)
  • {channel} — channel name (e.g. #portugal)
  • {network} — network name (e.g. PTirc)
  • {url} — a URL
  • {list} — a formatted list
  • {date} — a formatted date string
  • {time} — a formatted time string
  • {by} — attribution string (may be empty)
  • {quote} — a random quote
  • {total} — total nick count

Placeholders can be reordered within the sentence to fit your language's grammar. You cannot rename them, remove them, or add new ones.

Must keep — these are not translatable:

  • The four time-band labels: 0-5, 6-11, 12-17, 18-23 — leave as-is.
  • Karma — proper noun, used as a section title and in the DB.
  • Nick (column header) — IRC terminology, universally understood.
  • URL, Smiley — universally understood abbreviations.
  • :-) :) :( — emoticons in strings like "Smileys :-)" and "happiest :)" — keep them exactly as they appear.

Weekdays and months:

These are comma-separated lists. Keep exactly 7 weekdays (Monday first) and exactly 12 months. Do not add spaces around commas.

msgid "Monday,Tuesday,Wednesday,Thursday,Friday,Saturday,Sunday"
msgstr "lundi,mardi,mercredi,jeudi,vendredi,samedi,dimanche"

msgid "January,February,...,December"
msgstr "janvier,février,...,décembre"

Singular vs plural strings:

Many stats have two versions — one for {count} = 1 and one for {count} > 1. Both must be translated, even if your language handles plurals differently. The bot always picks the right one based on the count.

msgid "{nick} took voice away {count} time — someone had to."
msgstr "{nick} retirou voice {count} vez — alguém tinha de o fazer."

msgid "{nick} took voice away {count} times — someone had to."
msgstr "{nick} retirou voice {count} vezes — alguém tinha de o fazer."

Style:

  • Keep the same tone — playful, slightly sarcastic, never mean
  • Maintain gender-neutral language (they/their in English; use equivalent neutral forms or inclusive suffixes in your language)
  • Keep the ! and ? punctuation where present — they contribute to the tone
  • IRC terms (op, halfop, voice, kick) should not be translated — they are universally understood on IRC


Changelog

All notable changes to Statsbot. The format follows Keep a Changelog, and versions follow Semantic Versioning. Everything released before this file existed is treated as 1.0.0. Check what you are running with python main.py --version (it is also shown in the footer of every stats page).

[1.5.0] - 2026-10-20#

Changed#

  • Every timestamp is UTC, whatever the server's time zone, and the pages say so ("All times are UTC." in the footer of every stats page, "UTC" after each time). The stats page no longer shifts the hourly chart, the activity bands, the daily chart or "Statistics generated on" to the visitor's browser zone; the dashboard and the log file use UTC too.
  • The daily, weekly and monthly resets happen at 00:00 UTC instead of the server's midnight.

Fixed#

  • The hourly activity was stored in the server's local hour while the page treated it as UTC, so on a server not running in UTC every hour bar was off by the server's offset. Lines from before this release keep the hour they were stored with.

[1.4.2] - 2026-10-18#

Fixed#

  • RelationMap: off in config.yml did nothing: YAML reads a bare off as false, which was taken for an unknown value, so the map was shown (folded) anyway. off now hides the map, and a bare on opens it.
  • "Other interesting numbers": the example kick line under "wasn't very popular, getting kicked N times" could be about somebody else, when the top kicked nick was not among the channel's last five kicks. It now shows that nick's own latest kick, or no example if none was logged.

[1.4.1] - 2026-10-01#

Changed#

  • "These didn't make it to the top" is now a compact grid of nick (number), like pisg's, instead of a second copy of the big table. The number is the one the table is ranked by (words by default). It also saves one database query per listed nick.

[1.4.0] - 2026-10-01#

Added#

  • Pick a nick on the "Who talks to whom" map. Click a nick, or choose it in the new "Focus on" list, and it moves to the middle with the people it talks to around it: the more they talk, the closer they sit. Everyone else fades. Click another nick to move on, click the middle one (or choose "everyone") to go back to the ring. Needs JavaScript; without it the map and the Closest pairs table work as before.
  • Dots on the map are sized by how many lines each nick said (before: by how many mentions it is part of).
  • Other interesting numbers: the nick that changed its nick most ("can't settle on a name"), shown when someone changed it at least twice. Translated into all five languages.

Changed#

  • The map's hint text now mentions the new focus view (translated).

[1.3.1] - 2026-10-01#

Changed#

  • The "Who talks to whom" map is folded away by default ("Show the map (N nicks)"): it is a big picture and pushed the rest of the page down. The compact Closest pairs table now comes first. The new option RelationMap (closed by default, open, or off) changes that per channel.
  • With fewer than four nicks there is no ring at all, only the pairs table; a smaller channel gets a smaller picture; a slider that could not move (few nicks, all links equally strong) is not shown.

[1.3.0] - 2026-10-01#

Added#

  • "Who talks to whom": a map of the most connected nicks and a "Closest pairs" table. It is meant for busy channels, where a free-form graph turns into a hairball: it draws only the core (40 nicks by default, 60 at most) on a ring, groups the people who mostly talk to each other into colour-coded arcs, orders them so that strong links are short, and fades weak links. Hover or click a nick to see only its links; two sliders show fewer nicks or only the stronger links. The layout is computed by the bot, so the drawing stays small however many people talk. It follows the all-time / today / week / month tabs.
  • Options ShowRelations, RelationNicks, RelationMinWeight and RelationPairs (see DOCS.md); the setting stats.pair_keep_days (default 60) says how long a pair seen fewer than 3 times is kept.
  • Translations of the new section (pt, fr, it, nl).

Upgrade notes#

  • A new table (nick_pairs) is created automatically. The map has no history: pairs are counted from the moment you update, so it fills up as people talk (a mention of a nick that is in the channel counts as a link).

[1.2.0] - 2026-09-30#

Added#

  • Documentation site: https://statsbot-docs.v-m-m-l.workers.dev/ (generated from README.md, DOCS.md, TRANSLATE.md and CHANGELOG.md by tools/build_docs.py).
  • A "Docs" link next to the version in the footer of every stats page. The new optional setting web.docs_url changes the address; set it to "" to hide the link.

[1.1.0] - 2026-09-30#

Security#

  • Stored cross-site scripting on the stats page. Text that came from IRC — quotes, topics, kick reasons, /me and example lines, words, URLs, smileys, nicks and channel names — was written into the page without being escaped, so anyone who could talk in a tracked channel could run script in the browser of everyone who opened the page. Everything is now escaped where it is written, and values placed in the page's inline script can no longer close the script tag.
  • Host-mask auto-login never worked, and matched too much. It crashed with a KeyError the moment a mask matched, and a mask without a host part (a bare nick) would have matched on the nick alone — anybody can take a nick on IRC. A mask now has to contain an @ (for example *!*@your.host.example).
  • ?period= outside 0–3 is answered with 400, and ?limit= is limited to 1–100 (-1 meant "no limit" to SQLite and returned the whole table).

Fixed#

  • Every line's letters were counted twice.
  • The daily activity chart filed a finished day under the next day's date, so today's bar showed yesterday's numbers.
  • master list over PM failed with an internal error.
  • Karma was ignored for someone who had spoken for the first time less than a minute earlier.
  • -l, -L, -j and -f in a mode string wrongly took the next argument.
  • A restart during the midnight hour reset the day (and on Mondays / the 1st the week / month) a second time. The last reset of each kind is now stored in the database, and the week check includes the year.
  • The "got beaten" example showed a line about somebody else.
  • www. links on the page were relative links.
  • ?period=abc and ?limit=abc returned HTTP 500.

Changed#

  • ##channel and ###channel have their own page URLs. #lifeline stays /Network/lifeline/; ##lifeline is /Network/2/lifeline/ and ###lifeline /Network/3/lifeline/ (before, all three shared one address and the wrong channel was shown). Other spellings redirect to the canonical URL.
  • The footer shows the version.

Added#

  • python main.py --version, version.py, this changelog.
  • A test suite (python -m pytest) and CI on Python 3.11–3.14, a weekly dependency audit and Dependabot.

Upgrade notes#

  • git pull, pip install -r requirements.txt, restart the bot. The database updates itself (one new table, scheduler_state). No manual step.
  • Statistics that were already stored are not rewritten: the letters totals collected before 1.1.0 stay doubled (so "chars per line" of older data is too high), and earlier days on the activity chart stay one day late.
  • If you set up host-mask auto-login before, check that each mask contains an @.

[1.0.0]#

Everything up to the introduction of version numbers (the last such commit is d3538ac, 2026-07-28). An older bot cannot report a version, so check the date of its latest commit: from 2026-09-30 on it has the 1.1.0 fixes.

git log -1 --format='%h %cs'
[[ $(git log -1 --format=%cs) < 2026-09-30 ]] && echo "OUTDATED: update now" || echo "up to date"