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:
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user