- simplicity
- little boilerplate
- minimal magic
- power
- multithreading
- automatic reloading
- extensibility
- Multithreaded dispatch and the ability to connect to multiple networks at a time.
- Easy plugin development with automatic reloading and a simple hooking API.
To install dependencies, run:
pip install -r requirements.txt
Skybot defaults to SQLite (stored under persist/).
To use PostgreSQL instead, add this to config.json:
"database": {
"type": "postgres",
"postgres": {
"dsn": "postgresql://USER:PASSWORD@HOST:5432/DBNAME",
"schema_prefix": "skybot"
}
}
Install the driver:
pip install "psycopg[binary]"
Skybot runs on Python 2.7, 3.7 and Python 3.13.(WIP in some areas to full update code to 3.13, for now partial support.)
Skybot supports a small but useful subset of IRCv3:
message-tags— parses incoming IRCv3 message tags (@key=value;flag) and exposes them to pluginsbatch— enables related-message groups (used by features like chat history)cap-notify— server can notify clients about new/removed capabilitieslabeled-response— lets clients label commands and correlate server responses via@label=...away-notify— notifies you when users set/unset away viaAWAYserver-time— enables atimetag on servers that support itecho-message— server echoes your own PRIVMSG/NOTICE back to you (useful for message tags likemsgid/timeon your own messages)setname— allows changing realname on an active connection viaSETNAME+draft/multiline— work-in-progress multiline messages viaBATCH(messages can include line breaks)account-tag— enables anaccounttag on servers that support itaccount-notify— sendsACCOUNTmessages when a user's account status changeschghost— sendsCHGHOSTmessages when a user's user/host changesextended-join— JOIN messages may include account/realname fieldsinvite-notify— notifies you when you are invited to channelsinspircd.org/stats-tags— InspIRCd vendor capability adding extra message-tags to some stats outputmulti-prefix— NAMES replies can include multiple user prefixes (e.g.@+nick)userhost-in-names— NAMES replies may includenick!user@hostentriesstandard-replies— server may use standardized numeric replies for common errors (varies by server)extended-monitor— extends MONITOR to send metadata change notifications (away/account/chghost/setname) for monitored nickschathistory/draft/chathistory— allows requesting chat history via theCHATHISTORYcommand (server support varies)msgid— when supported, servers attach amsgidtag to messages (often delivered viamessage-tags)
Related (non-CAP) extensions:
WHOX(ISUPPORT token) — extendedWHOreplies (numeric354). Skybot tracks ISUPPORT and can send WHOX-style queries viaconn.who(mask, fields=..., token=...).MONITOR(ISUPPORT token) — server-side online/offline notifications via theMONITORcommand (numerics730-734). Skybot provides helpers:conn.monitor_add([...]),conn.monitor_remove([...]),conn.monitor_list(),conn.monitor_status(),conn.monitor_clear().UTF8ONLY(ISUPPORT token) — server enforces UTF-8-only traffic; servers may useFAIL/WARN ... INVALID_UTF8whenstandard-repliesis enabled.BOT(ISUPPORT token) — advertises the bot user mode (often+B) used by the bot-mode extension. Skybot will set it on connect when available (disable with connection setting"bot_mode": false).
IRCv3 capability configuration is per-connection (inside the connections object).
Default behavior: Skybot requests message-tags, batch, cap-notify, labeled-response, away-notify, server-time, echo-message, setname, draft/multiline, account-tag, account-notify, chghost, extended-join, invite-notify, inspircd.org/stats-tags, multi-prefix, userhost-in-names, standard-replies, and extended-monitor.
Note: When echo-message is enabled, Skybot ignores its own echoed PRIVMSG/NOTICE lines to prevent double-processing (e.g., command loops when the bot speaks with its own prefix).
Note: chathistory / draft/chathistory is supported by Skybot but is intentionally opt-in (some servers may auto-send history on join when the capability is enabled). Add it to ircv3.caps only if you want it.
Some networks may still send server-driven BATCH ... chathistory on join even when you don't request the capability. Skybot suppresses these batches by default so plugins/logging don't treat history as new messages.
Per connection you can control this in config.json:
ignore_chathistory_batches(bool, defaulttrue): suppress allchathistorybatches.ignore_chathistory_channels(list of strings): if set, suppress only these targets.allow_chathistory_channels(list of strings): if set, allow only these targets (everything else suppressed).
To override the requested capabilities:
"connections": {
"network name": {
"server": "irc.example.net",
"nick": "Skybot",
"ircv3": {
"caps": ["message-tags", "batch", "cap-notify", "labeled-response", "server-time", "echo-message", "account-tag", "account-notify", "chghost", "extended-join", "invite-notify", "inspircd.org/stats-tags", "multi-prefix", "userhost-in-names", "standard-replies", "extended-monitor", "msgid", "draft/msgid"]
}
}
}
("caps": [...] at the top level of the connection is also accepted for compatibility.)
For every incoming line, plugins receive a tags dict:
input.tags— mapping of tag name to value- tags without a value (flag tags) map to
None - tags with an explicit empty value (
key=) map to""
- tags without a value (flag tags) map to
Example:
@hook.regex(r"^\\.whoami$")
def whoami(inp, reply=None, tags=None, **kwargs):
reply("account=%s time=%s" % (tags.get("account"), tags.get("time")))
If you want to use labeled-response from a plugin:
label = inp.conn.cmd_labeled("WHO", [inp.nick])
inp.reply("sent WHO with label=%s" % label)
If you want to request account/host info using WHOX (when the server supports it):
# Request: account (a), user (u), host (h), nick (n)
inp.conn.who("#channel", fields="auhn", token=42)
# Listen for numeric 354 (RPL_WHOSPCRPL) in a plugin:
@hook.event("354")
def whox_reply(inp, **kwargs):
# inp.paraml contains the WHOX fields in server-defined order.
pass
Skybot does not automatically fetch history on connect; it only provides the plumbing.
If the server supports CHATHISTORY, a plugin can request history using the connection object:
conn.chathistory_latest("#channel", limit=50)conn.chathistory_before("#channel", reference="2025-12-17T00:00:00.000Z", limit=50)conn.chathistory_after("#channel", reference="2025-12-17T00:00:00.000Z", limit=50)
Servers deliver results using BATCH plus @batch=... message tags on the enclosed lines.
You can read the batch association from input.tags.get("batch").
The feeds plugin (plugins/feeds.py) can watch RSS/Atom feeds and announce new items into IRC channels.
Top-level feeds settings:
"feeds": {
"poll_interval": 300,
"max_items_per_poll": 3
}
Notes:
- Watches are stored in the database, so they survive restarts and work with both SQLite and PostgreSQL.
- The plugin polls on normal bot activity (event-driven), so in a totally idle network it may not poll until some messages/events are seen.
.feed add <url> [#channel]— start watching a feed (defaults to the current channel).feed remove <url> [#channel]— stop watching a feed.feed list— list watched feeds.feed info— show plugin status
Crowdcontrol (plugins/crowdcontrol.py) applies moderation rules to channel messages.
Rules are configured under the top-level crowdcontrol array. A rule can match by:
re— a regular expressionbadness— mojibake/spam heuristic score (matches whenbadness(message) >= threshold)flood— per-user flood control (matches when a user sends more thancountlines withinseconds)
Common fields:
msg— message used either as a warning (reply) or as a kick reasonkick—1to kick,0to only warnban_length—0no ban,-1ban without unbanning,>0ban then unban after N seconds
Temporary bans: when ban_length > 0, the plugin schedules an unban in the database and unbans asynchronously (no time.sleep() blocking). This means unbans survive bot restarts.
Optional tuning (defaults are fine):
"crowdcontrol_unban": {
"poll_interval": 10,
"batch": 50
}
"crowdcontrol": [
{
"flood": {
"count": 5,
"seconds": 8,
"escalate": {"ban_after": 2, "window": 600, "ban_length": 300}
},
"msg": "Flood in #{channel} ({flood_strikes}/{flood_ban_after}). Action={flood_action}.",
"kick": 1,
"ban_length": 0
}
]
Flood escalation (kick first, ban on repeat):
escalate.ban_after— strike count to start banning (default2)escalate.window— seconds in which strikes accumulate before resetting (default600)escalate.ban_length— seconds to ban onceban_afteris reached (default300)
Notes:
- Keep the rule-level
ban_lengthas0for flood rules. Escalation controls bans. - Flood escalation uses the same identity key as the flood limiter (
nick!user@host), scoped per server+channel.
Flood backend:
-
Default is in-memory (fast, bounded with LRU).
-
Optional DB-backed mode exists if you really want no per-user state in the bot process, but it does a DB read+write per message and may not scale well on very busy channels.
"crowdcontrol": [ { "flood": {"backend": "db", "count": 5, "seconds": 8}, "msg": "Flood in #{channel} (>{flood_count}/{flood_seconds}s via {flood_backend}).", "kick": 1, "ban_length": 0 } ]
Flood tuning keys (in-memory backend only):
flood.max_keys— maximum tracked identities (default50000)flood.idle_ttl— evict state for idle identities after N seconds (defaultmax(300, seconds*10))
Flood tuning keys (DB backend):
flood.idle_ttl— controls cleanup of old DB rows
"crowdcontrol": [
{
"badness": 2,
"msg": "Mojibake/spam in #{channel} (score={badness} threshold={threshold}).",
"kick": 1,
"ban_length": 60
}
]
In msg, you can use placeholders (unknown placeholders are left as-is):
{channel}/{chan}(also supports Ruby-style#{channel}){nick}{user}{host}{server}{message}{badness}{threshold}{flood_backend}{flood_count}{flood_seconds}{flood_hits}{flood_tokens}{flood_max_keys}{flood_strikes}{flood_ban_after}{flood_strike_window}{flood_action}{flood_ban_length}
Crowdcontrol creates the following tables automatically:
crowdcontrol_unbans— scheduled unbans (used whenban_length > 0)crowdcontrol_flood— flood token buckets (only when usingflood.backend: "db")crowdcontrol_flood_strikes— strike counters for flood escalation (only when usingflood.backend: "db")
Skybot is public domain. If you find a way to make money using it, I'll be very impressed.
See LICENSE for precise terms.