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 nickMike--) - 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.ymlseeds 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 viachannel_pisg:inconfig.yml;rehash/reloadre-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,ShowHalfopstoggles - 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 installinto 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 installoutside a virtualenv. Always use.venvas 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).
User unit (recommended)#
# ~/.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.pyand add a section toCHANGELOG.md - After editing
README.md,DOCS.md,TRANSLATE.md,CHANGELOG.mdorversion.py, runpython tools/build_docs.pyto regenerate the documentation site (docs/index.html); a test fails if you forget - Update
DOCS.mdif 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.ymlis a seed file, not the source of truth. On first run, networks and channels fromconfig.ymlare 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#
- If
server_passwordis set,PASSis sent beforeNICK/USER. - If
saslis set,CAP REQ :saslis sent beforeNICK/USER. On success the bot sendsAUTHENTICATE PLAIN→ base64(user\0user\0pass) →CAP END. If the server rejects SASL, falls back to NickServ. - On
001(welcome), if SASL didn't auth, NickServIDENTIFYis sent. - If
ghost: trueand the primary nick is taken (433), the bot: - Switches toaltnick- SendsPRIVMSG NickServ :GHOST <nick> <password>- Watches for the confirmation notice and then sendsNICK <primary> on_connectcommands fire immediately on001, 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 againstnick!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/partvsaddchan/delchan:join/partare runtime-only and do not touch the database — use them for temporary actions.addchananddelchanmodify persistent tracking (anddelchandeletes stats).Destructive operations:
delnetanddelchancascade-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 whenShowBigNumberswas 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, soMike----awards −1 to nickMike--, not to nickMike. - 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#
- Copy
locale/en_US.potolocale/<lang>.po(e.g.locale/de_DE.po) Currently supported:en_US,pt_PT,fr_FR,it_IT,nl_NL - Update the
Language:header - Add your language code to
SUPPORTEDini18n.py - Translate every
msgstrline
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/theirin 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: offinconfig.ymldid nothing: YAML reads a bareoffasfalse, which was taken for an unknown value, so the map was shown (folded) anyway.offnow hides the map, and a bareonopens 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(closedby default,open, oroff) 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,RelationMinWeightandRelationPairs(see DOCS.md); the settingstats.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_urlchanges 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,
/meand 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
KeyErrorthe 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 with400, and?limit=is limited to 1–100 (-1meant "no limit" to SQLite and returned the whole table).
Fixed#
- Every line's
letterswere 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 listover 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,-jand-fin 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=abcand?limit=abcreturned HTTP 500.
Changed#
##channeland###channelhave their own page URLs.#lifelinestays/Network/lifeline/;##lifelineis/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
letterstotals 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"