Services Suite

Last updated: July 25, 2026

dIRC Services β€” Services Suite

The 14 pseudoclients implemented by the dircservices binary, grouped by milestone. Each service is a single services/<svc>/<svc>.go package registered with services.Registry and started in cmd/dircservices/main.go. Per-service command docs live in dircservices/docs/EN.<Svc>.md (and per-language translations), surfaced end-user via the HelpServ service.

See the canonical overview for architecture, build, runtime, and the S2S wire protocol. This page is the per-service command/feature reference.

M2 services (foundational)

Global β€” network notices

Real name: dLAN Global Noticer Β· Source: services/global/global.go

  • GLOBAL <text> β€” broadcast to every online user (oper-only)
  • LOGONNEWS ADD/DEL/LIST β€” in-memory v1 welcome notices
  • OPERNEWS ADD/DEL/LIST β€” oper-class network news (added 07-24)
  • HELP

NickServ β€” nickname registration & profiles

Real name: dLAN Nickname Service Β· Source: services/nickserv/nickserv.go Β· Store: store/store.go (NickRow, NickGet/NickCreate/NickUpdateField/NickDelete/NickListByPattern/NickAccess*/NickAjoin*/NickListChannels), store/schema.go (nicks, nick_aliases)

Full 17-command implementation (rewrite 2026-07-24 from 47-line M2 skeleton):

  • Registration lifecycle: REGISTER <pw> [email], IDENTIFY <pw> (one-arg self) or IDENTIFY <nick> <pw> (two-arg), LOGOUT, GHOST, RECOVER, RELEASE, REGAIN, DROP, FDROP (forced, oper-only), INFO [nick], STATUS, ACC
  • SET sub-commands (oper/admin class + profiles): DISPLAYNAME, DOB (ISO YYYY-MM-DD), FAVCHANNEL, TWITTER, FACEBOOK, LINKEDIN, WEBSITE, TIMEZONE, LANGUAGE (alias LANG), KILL QUICK mode, AUTOOP ON|OFF, VHOST ON|OFF
  • ACCESS (ADD/DEL/LIST/CLEAR) β€” alternate auth masks
  • AJOIN (ADD/DEL/LIST) β€” auto-join list
  • LISTCHANS β€” channels you own via ChanServ
  • LIST [pattern] (oper-only) β€” registered nick search
  • HELP [cmd]

Identified state lives in a shared pseudoclient.IdentifiedMap (sync.Map) keyed by protocol.UID, set on REGISTER/IDENTIFY, read by ChanServ + GroupServ + HostServ.

Profile schema (8 columns added 2026-07-24 session 2): display_name, dob, fav_channel, twitter, facebook, linkedin, website, timezone. Consumed by HelpServ's NickGetLanguage for per-user language resolution.

OperServ β€” operator toolkit

Real name: dLAN Operator Service Β· Source: services/operserv/operserv.go Β· Store: store/store.go (AkillRow/ForbidRow/OperRow)

Essentials (M2) + AKILL/SNLINE/SQLINE/FORBID/OPER expansion (2026-07-24):

  • INFO, STATS [AKILL|UPLINK|UPTIME|ALL|RESET]
  • SHUTDOWN / RESTART / QUIT β€” signals stopCh (main.go reads SIGINT/SIGTERM/stopCh)
  • UPDATE β€” store.ForceBackup() (snapshot + prune)
  • SET <key> <on|off> β€” in-memory toggles: READONLY, DEBUG, NOEXPIRE, SUPERADMIN
  • AKILL ADD/DEL/LIST/VIEW/CLEAR, CHANKILL (store + member iteration via ChannelRegistry)
  • SNLINE ADD/DEL/LIST, SQLINE ADD/DEL/LIST, FORBID ADD/DEL/LIST
  • MODE, KICK (uses Protocol interface Kick added commit e210c2eb)
  • OPER ADD/DEL/LIST + OPER INFO <nick>
  • LOGSEARCH β€” grep of services log file
  • SVSNICK/SVSJOIN/SVSPART/SVSMODE (resolve UID via UserRegistry)
  • HELP

Permissions via access.OperType glob (path.Match) + pseudoclient.IsOper(cfg, nick).

ChanServ β€” channel registration & ACL

Real name: dLAN Channel Service Β· Source: services/chanserv/chanserv.go Β· Store: store/chanserv.go

Roughly 95% coverage after the 2026-07-24 SET-system expansion. Access levels: 0=voice, 100=halfop, 200=op, 300=protect, 400=founder/owner.

  • Mgmt: REGISTER, DROP, INFO, FDROP (oper), LIST (oper), HELP
  • ACCESS: ADD/DEL/LIST/SET/CLEAR, with AOP/HOP/VOP/SOP/FSOP shortcuts
  • AKICK: ADD/DEL/LIST/VIEW/ENFORCE/CLEAR
  • User modes: OP/DEOP/HALFOP/DEHALFOP/VOICE/DEVOICE/OWNER/DEOWNER/PROTECT/DEPROTECT
  • Channel ops: MODE, KICK, KICKBAN, BAN, UNBAN, TOPIC, TOPICAPPEND, TOPICPREPEND, INVITE, GETKEY
  • SET (19 sub-commands): FOUNDER, SUCCESSOR, PASSWORD (bcrypt), DESC, URL, EMAIL, ENTRYMSG, MLOCK, TOPICLOCK, KEEPTOPIC, SECUREOPS, RESTRICTED, VERBOSE, PRIVATE, FANTASY, GAMESERV, PREFIX, GUARD, PROPERTY. MLOCK/TOPICLOCK/KEEPTOPIC are now actually enforced (2026-07-25 fix β€” all three were previously stored and silently ignored). Also fixed: DESC and TOPICAPPEND/TOPICPREPEND used to share one DB column (description), so setting one clobbered the other β€” topic now has its own column.
  • Utility: ENFORCE, SYNC, STATUS, WHY, UP, DOWN, RECOVER

NickServ integration via shared IdentifiedMap; REGISTER/DROP require identified caller. REGISTER also requires the caller currently be a member and opped in the channel (ChannelRegistry.IsMember/IsOp, 2026-07-25) β€” previously missing entirely, and the old syntax additionally (confusingly) re-verified the caller's NickServ password as a second REGISTER argument, which is what "password incorrect" on REGISTER actually meant. Channel modes/raw ops use direct SendMessage protocol.Message.

HostServ β€” virtual hosts

Real name: dLAN VHost Service Β· Source: services/hostserv/hostserv.go Β· Store: store/vhost.go (VhostRow, VhostRequest, VhostOffer)

Expanded to ~100% coverage in 2026-07-24 session 3:

  • Self-service: SET/GET/DEL/LIST/ON/OFF/ASSIGN (SET/ASSIGN are aliases)
  • Approval queue: REQUEST, WAITING, ACTIVATE, REJECT
  • Oper bulk: SETALL, DELALL, GROUP, VHOSTNICK
  • Shared vhost pool: OFFER, OFFERLIST, TAKE, UNOFFER
  • HELP

Schema additions: vhost_requests + vhost_offers tables. v1 stores vhosts only β€” actual SVSMODE-based Unreal vhost application is not wired yet (per design Β§6.6).

M3 services

BotServ β€” channel bot assignment & kick enforcement

Real name: dLAN Bot Service Β· Source: services/botserv/botserv.go Β· Store: store/botserv.go (BotRow, BotAssign, BadwordRow, KickRow, channel setting TEXT in channel_property)

  • Bot mgmt (oper): BOT ADD, BOT CHANGE, BOT DEL, BOTLIST
  • Assignment: ASSIGN, UNASSIGN, ACT (ACTION via assigned bot), SAY (PRIVMSG via assigned bot)
  • Badwords: BADWORDS ADD/DEL/LIST/CLEAR (match types SINGLE/START/END)
  • KICK settings: 9 types β€” CAPS, FLOOD, REPEAT, BOLDS, COLORS, ITALICS, REVERSES, UNDERLINES, AMSG
  • SET: FANTASY, NOBOT, PRIVATE
  • INFO, HELP

Kick Enforcement Engine

Wired via proto.OnPrivmsg hook β€” fires on every channel PRIVMSG:

Detection Mechanism
CAPS Uppercase-char ratio vs percent threshold + min-chars floor
FLOOD Per-user-per-channel sliding timestamp window vs lines threshold
REPEAT Last-N messages ring buffer per user, identical-message count
BOLDS/COLORS/ITALICS/REVERSES/UNDERLINES IRC formatting code count per message
AMSG Cross-channel same-message detection within time window

In-memory trackers (no DB rows for live state): floodTracker (timestamp slices), repeatTracker (string slices), amsgTracker (nick→chan→time).

Bot-join tracking via SVSJOIN on ASSIGN, SVSPART on UNASSIGN/DEL β€” joinedBots map. SVSJOIN alone grants no channel status, so ASSIGN follows it with a MODE +o on the bot (2026-07-25) β€” otherwise it can't enforce KICK settings. SAY/ACT send via PrivmsgAs(botUID, channel, text) so the message visibly comes from the assigned bot, not the bare server SID (was previously misdirected at the bot's own UID instead of the channel β€” never actually worked).

Fantasy routing (wired 2026-07-24): SET FANTASY ON + channel message starting !cmd β†’ dispatches to GameServ.HandleFantasyCommand (set via bsSvc.SetFantasyHandler in main.go).

ALIS β€” channel list service

Real name: dLAN Advanced List Service Β· Source: services/alis/alis.go Β· State: state/channels.go ChannelRegistry

Single LIST command with filters β€” pattern glob, -min/-max member count, -topic substring, -mode all-must-match glob, -noinvite/-nosecret exclusions. 10s rate limit per UID; HELP.

ALIS reads live channel state from the shared ChannelRegistry (populated from the protocol's OnChannelJoin/Part/Kick/Mode/Topic hooks β€” not from ChanServ DB).

GroupServ β€” account groups

Real name: dLAN Group Service Β· Source: services/groupserv/groupserv.go Β· Store: store/groupserv.go (GroupRow, GroupMember, GroupInvite)

Case-insensitive (lower()-based) lookups; permission model uses string flags column (bare runes "rFc"):

  • REGISTER <group> β€” identified-only; inserts groups row + GroupMember(founder, "F")
  • DROP <group> β€” founder OR oper; cascades members + invites in a tx
  • JOIN <group> β€” open group auto-add, closed group consumes a GroupInvite row
  • PART <group> β€” founder cannot PART own group
  • INVITE <group> <nick> β€” closed only; founder OR +r
  • FLAGS <group> <nick> <r|F|c> β€” founder OR +F; +F only by founder
  • SET: OPEN|PUBLIC|DESCRIPTION|PASSWORD|JOINFLAGS|GROUPFLAGS
  • INFO, LIST [pattern], HELP

Flags: r=invite/register, F=founder (single, founder-gated), c=chanserv-acl-binding (stored only, not enforced v1). 2026-07-24 added group_members.group→group_name rename to dodge the SQLite reserved word.

M4 services

StatServ β€” network telemetry

Real name: dLAN Statistics Service Β· Source: services/statserv/statserv.go Β· Store: store/statserv.go (StatServSnapshot, ChannelSnapshot, UserSnapshot)

Snapshot loop: every N minutes (default 5m) β†’ count online users/channels/ops β†’ INSERT into statserv_snapshots + per-channel + per-user rows; resets per-user lines_spoken counters. Wires OnPrivmsg to count lines.

Commands: NETWORKINFO, CHANNELINFO <#chan>, TOP [channels|users] [by=messages|time|users] [limit], REPORT <#chan> [limit], HELP.

SecureServ β€” anti-abuse defense

Real name: dLAN Security Service Β· Source: services/secureserv/secureserv.go (+ clone.go, dnsbl.go, flood.go, proxyscan.go) Β· Store: store/secureserv.go

Four subsystems wired at startup via services' OnUIDIntroduce/OnPrivmsg hooks:

Subsystem Hook Behavior
DNSBL OnUIDIntroduce Reverse-DNS lookup of user IP against configured providers (3 defaults). Hit β†’ AKILL via TKLAdd. Respects exempt CIDRs.
Clone detection OnUIDIntroduce + OnQuit Per-host connection counter (sync.Mutex map). Thresholds: warn (oper notify), kill (SVSKILL), akill (TKLAdd).
Flood defense OnPrivmsg Sliding-window per-user + per-channel line counters. Escalation: warn β†’ mute (SVSMODE +m, auto-lift) β†’ kill β†’ akill.
Proxyscan OnUIDIntroduce (off by default) Dial user IP on common proxy ports; AKILL on accept.

Commands: STATS, EXEMPT ADD/DEL/LIST, PROVIDER ADD/DEL/LIST, CLONE LIST/SET, FLOOD STATS/SET, HELP.

Notification by WALLOPS oper broadcast (replaces direct NOTICE β€” see session 3 devlog).

Default thresholds (from config/defaults.go): clone warn=3/kill=5/akill=8; flood user=5 lines/3s, channel=20/5s.

M5 services

GameServ β€” channel games

Real name: dLAN Game Service Β· Source: services/gameserv/ (8 files: gameserv.go, calc.go, eightball.go, roll.go, rps.go, namegen.go, lottery.go, trivia.go) Β· Store: store/gameserv.go (Lottery/Ticket/TriviaScore) Β· Data: dircservices/data/trivia/{general,movies,science}.txt

Commands (8):

  • CALC <expr> β€” recursive-descent math parser (numbers, +/-/*//^, parens; no eval/reflect)
  • EIGHTBALL β€” 20 classic Magic 8-Ball answers
  • ROLL <spec> β€” XdY+Z dice + 4dF fudge; shows individual rolls + total
  • RPS β€” rock/paper/scissors w/ emoji
  • NAMEGEN β€” Markov-chain fantasy names from embedded syllable bank
  • LOTTERY BUY/STATUS/DRAW β€” per-channel rolling jackpot; weighted random draw
  • TRIVIA START/STOP/SCORE/CATEGORIES β€” text-file quiz (Question|Answer per line)
  • HELP

2026-07-24 fix: fantasy + trivia/lottery now keyed by actual IRC channel, not virtual nick-context β€” option for chanserv SET FANTASY ON + !cmd to fire GameServ.

IdleRPGServ β€” idle-to-level RPG

Real name: dLAN IdleRPG Β· Source: services/idlerpg/ (idlerpg.go, register.go, events.go, quest.go, top.go, story.go, admin.go) Β· Store: store/idlerpg.go (IdleRPGChar, Item, Quest, Story)

Characters idle in #IdleRPG to level up. Mechanics:

  • 30s idle-tracking tick loop; event roll timer (base + jitter)
  • Talking/parting/quitting channel = penalty
  • Event rolls: battle (attack vs monster defense), treasure (find item), quest (start timed 30-90min quest)
  • Wires OnChannelJoin/Part/Kick/Privmsg for #IdleRPG
  • IdleRPGServ itself joins (and self-ops in) #IdleRPG at startup (2026-07-25); REGISTER now SVSJoins the caller straight into the channel instead of just telling them to go there
  • Battle/treasure/quest outcomes are announced in #IdleRPG (via ReplyChannel, so they visibly come from IdleRPGServ) in addition to the existing private NOTICE to the player
  • Auto-recognize on join (2026-07-25): anyone (re)joining #IdleRPG with an existing registered character gets a "Welcome back" NOTICE with their class/level/idle time β€” piggybacks on the existing OnChannelJoin idle-timer wiring, no new event hook needed

Commands: REGISTER, WHOAMI, STATUS, QUEST, PENALTY, TOP (leaderboard by level), STORY (recent event log), ADMIN FREEZE/UNFREEZE/RESET/SETLEVEL (oper-only), HELP.

Bcrypt for REGISTER. Standalone β€” no coupling to the dmud MUD package.

LoveServ β€” fun gestures

Real name: dLAN Love Service Β· Source: services/loveserv/ (loveserv.go, gestures.go, leaderboard.go) Β· Store: store/loveserv.go (LeaderboardRow)

16 gesture commands (3 random response variants each): ROSE, HUG, KISS, SLAP, POKE, BEER, COFFEE, COOKIE, PIZZA, HEART, FORTUNE, LOVE, HIGHFIVE, FACEPALM, SHRUG, DANCE.

Tracking: given/received counts per nick. Tracks muted channels (per-channel MUTE added 2026-07-24). LOVE STATS + LOVE HELP + most-slapped leaderboard category + Weekly reset loop.

HelpServ β€” centralized documentation (added 2026-07-24)

Real name: dLAN Help Service Β· Source: services/helpserv/helpserv.go

The 14th service, wired last in main.go. Centralizes all per-service documentation under dircservices/docs/:

  • LIST β€” shows available docs, hides <LANG>. prefixed files (e.g. hides EN.NickServ.md, only shows base NickServ.md)
  • HELP <service> [command] β€” loads doc file, parses ## Command headers, returns the matching section (or whole doc)
  • Multi-language: resolves caller UID β†’ nick β†’ NickGetLanguage(nick) β†’ try {LANG}.{Service}.md then base {Service}.md (case-insensitive) then EN.{Service}.md fallback

13 EN.*.md canonical files (one per non-HelpServ service) + FR.NickServ.md, ES.NickServ.md skeletons shipped. All docs rewritten in the 2026-07-24 session 3 to cover every command + sub-command in compact 1-line-per-command format.

Build & Dev Workflow

# Build full services binary
go build -o dircservices ./dircservices/cmd/dircservices/

# Tests (4 unreal4 dispatch/parse round-trip tests)
go test ./dircservices/...

# Vet
go vet ./dircservices/...

# Smoke against live irc.dlan:6700 (verify link + 14 UID bursts)
timeout 90 ./dircservices

Configure via dircservices/config.example.json (LAN plaintext creds shipped β€” set::plaintext-policy::server allow on ircd). Operator types live in cfg.OperTypes and are installed via access.InstallOperTypes at startup.

Adding a New Service

  1. Implement Service interface in services/<svc>/<svc>.go (use a recent one like loveserv as template β€” embed *pseudoclient.Pseudoclient)
  2. Wire SetUserRegistry / SetChannelRegistry / SetIdentifiedMap setter pattern (consistent across all existing services)
  3. Add store/<svc>.go for persistence; bump schema.go migrateSchema (schema_version INSERT OR IGNORE)
  4. Write docs/EN.<Svc>.md (and translations as desired)
  5. Import + register in cmd/dircservices/main.go, bump the count in the start log
  6. Document here (overview table + a section above)

The protocol's On* setters allow multiple subscribers safely β€” per-event RLock + snapshot-copy + dispatch loop means new services can append event handlers without race risk.

Cross-References