feat: Q bot auth for QuakeNet, configurable auth_service

Add auth_service config field ("nickserv", "qbot", "none") to support
networks with non-standard auth systems. QuakeNet uses Q bot AUTH
instead of NickServ. Also bumps NickServ timeout from 15s to 30s.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
user
2026-02-21 14:42:46 +01:00
parent 0e06a18851
commit 246b77e90a
3 changed files with 83 additions and 3 deletions

View File

@@ -533,6 +533,7 @@ async def _cmd_addnetwork(router: Router, arg: str) -> list[str]:
nick=kvs.get("nick", ""),
channels=channels,
password=kvs.get("password"),
auth_service=kvs.get("auth_service", "nickserv"),
)
await router.add_network(cfg)

View File

@@ -47,6 +47,7 @@ class NetworkConfig:
password: str | None = None
proxy_host: str | None = None
proxy_port: int | None = None
auth_service: str = "nickserv" # "nickserv", "qbot", or "none"
@dataclass(slots=True)
@@ -104,6 +105,7 @@ def load(path: Path) -> Config:
password=net_raw.get("password"),
proxy_host=net_raw.get("proxy_host"),
proxy_port=net_raw.get("proxy_port"),
auth_service=net_raw.get("auth_service", "nickserv"),
)
if not networks:

View File

@@ -486,6 +486,22 @@ class Network:
await self._nickserv_complete()
return
# Skip auth for networks with no services
if self.cfg.auth_service == "none":
self._status(f"ready as {self.nick} (no auth service)")
return
# Q bot auth (QuakeNet)
if self.cfg.auth_service == "qbot":
self._nickserv_done = asyncio.Event()
await self._qbot_auth()
try:
await asyncio.wait_for(self._nickserv_done.wait(), timeout=30)
except asyncio.TimeoutError:
log.warning("[%s] Q bot did not respond in 30s", self.cfg.name)
await self._nickserv_complete()
return
# Check for a pending registration from a previous session
if await self._resume_pending_verification():
# Pending verification resumed -- skip normal NickServ flow
@@ -496,11 +512,11 @@ class Network:
self._nickserv_done = asyncio.Event()
await self._nickserv_identify()
# If NickServ doesn't respond within 15s, move on
# If NickServ doesn't respond within 30s, move on
try:
await asyncio.wait_for(self._nickserv_done.wait(), timeout=15)
await asyncio.wait_for(self._nickserv_done.wait(), timeout=30)
except asyncio.TimeoutError:
log.warning("[%s] NickServ did not respond in 15s", self.cfg.name)
log.warning("[%s] NickServ did not respond in 30s", self.cfg.name)
self._nickserv_pending = ""
await self._nickserv_complete()
@@ -573,6 +589,63 @@ class Network:
"""Signal that NickServ interaction is finished."""
self._nickserv_done.set()
# -- Q bot auth (QuakeNet) -------------------------------------------
_QBOT = "Q@CServe.quakenet.org"
async def _qbot_auth(self) -> None:
"""Authenticate with QuakeNet's Q bot using stored credentials.
Looks up creds by network name. If found, sends AUTH to Q.
No registration path -- QuakeNet accounts are web-only.
"""
if not self.backlog:
log.info("[%s] no backlog, skipping Q auth", self.cfg.name)
await self._nickserv_complete()
return
creds = await self.backlog.get_nickserv_creds_by_network(self.cfg.name)
if not creds:
log.info("[%s] no stored Q creds, skipping auth", self.cfg.name)
self._status("no Q account (register at quakenet.org)")
await self._nickserv_complete()
return
stored_nick, stored_pass = creds
self._nickserv_pending = "qbot_auth"
self._nickserv_password = stored_pass
log.info("[%s] authenticating with Q as %s", self.cfg.name, stored_nick)
self._status(f"authenticating with Q as {stored_nick}")
await self.send_raw("PRIVMSG", self._QBOT, f"AUTH {stored_nick} {stored_pass}")
async def _handle_qbot(self, text: str) -> None:
"""Process Q bot NOTICE responses."""
lower = text.lower()
log.info("[%s] Q: %s", self.cfg.name, text)
if self._nickserv_pending != "qbot_auth":
return
if "you are now logged in" in lower:
self._status(f"Q auth successful")
log.info("[%s] Q AUTH succeeded", self.cfg.name)
self._nickserv_pending = ""
# Switch to configured nick if set
if self.cfg.nick and self.cfg.nick != self.nick:
self._nick_confirmed.clear()
await self.send_raw("NICK", self.cfg.nick)
try:
await asyncio.wait_for(self._nick_confirmed.wait(), timeout=10)
except asyncio.TimeoutError:
log.warning("[%s] nick change to %s not confirmed",
self.cfg.name, self.cfg.nick)
await self._nickserv_complete()
elif "incorrect password" in lower or "auth failed" in lower:
self._status("Q auth failed (wrong password)")
log.warning("[%s] Q AUTH failed: %s", self.cfg.name, text)
self._nickserv_pending = ""
await self._nickserv_complete()
async def _verify_email_code(self) -> None:
"""Poll temp email for NickServ verification code and confirm."""
if not self._nickserv_email:
@@ -890,6 +963,10 @@ class Network:
if sender == "nickserv":
await self._handle_nickserv(text)
# Q bot response handling (QuakeNet)
elif sender == "q":
await self._handle_qbot(text)
# Extract hostname from server notices during connect
elif "Found your hostname" in text:
# "*** Found your hostname: some.host.example.com"