home · contact · privacy
Clean up message parsing code.
authorChristian Heller <c.heller@plomlompom.de>
Sun, 23 Nov 2025 21:38:51 +0000 (22:38 +0100)
committerChristian Heller <c.heller@plomlompom.de>
Sun, 23 Nov 2025 21:38:51 +0000 (22:38 +0100)
src/ircplom/client.py
src/ircplom/client_tui.py
src/ircplom/irc_conn.py
src/ircplom/msg_parse_expectations.py

index a273230741d8307426cf7f3587fa355434b7124b..1c2444f57f42ca0094329dce00ca85121032f392 100644 (file)
@@ -17,8 +17,9 @@ from ircplom.db_primitives import (
 from ircplom.events import (
     AffectiveEvent, CrashingException, ExceptionEvent, QueueMixin)
 from ircplom.irc_conn import (
-    BaseIrcConnection, IrcConnException, IrcMessage, ERR_STR_TIMEOUT,
-    ILLEGAL_NICK_CHARS, ILLEGAL_NICK_FIRSTCHARS, ISUPPORT_DEFAULTS, PORT_SSL)
+    BaseIrcConnection, IrcConnException, IrcMessage, NickUserHost,
+    ERR_STR_TIMEOUT, ILLEGAL_NICK_CHARS, ILLEGAL_NICK_FIRSTCHARS,
+    ISUPPORT_DEFAULTS, PORT_SSL)
 from ircplom.msg_parse_expectations import MSG_EXPECTATIONS
 
 
@@ -94,17 +95,6 @@ class SharedClientDbFields(IrcConnSetup):
         return name[0] in self.isupport['CHANTYPES']
 
 
-@dataclass
-class NickUserHost:
-    'Combination of nickname, username on host, and host.'
-    nick: str = '?'
-    user: str = '?'
-    host: str = '?'
-
-    def __str__(self) -> str:
-        return f'{self.nick}!{self.user}@{self.host}'
-
-
 class User(NickUserHost):
     'Adds to NickUserHost non-naming-specific attributes.'
     modes: str = '?'
@@ -172,7 +162,7 @@ class _CompletableTopic(Completable, Topic):
 
     def complete(self) -> None:
         assert self.who is not None
-        copy = _NickUserHost.from_str(str(self.who))
+        copy = NickUserHost.from_str(str(self.who))
         self.completed = Topic(self.what,
                                NickUserHost(copy.nick, copy.user, copy.host))
 
@@ -286,25 +276,6 @@ class _SetNickuserhostMixin:
 
 class _NickUserHost(NickUserHost):
 
-    @staticmethod
-    def possible_from(value: str) -> bool:
-        'If class instance could be parsed from value.'
-        toks = value.split('!')
-        if not len(toks) == 2:
-            return False
-        toks = toks[1].split('@')
-        if not len(toks) == 2:
-            return False
-        return True
-
-    @classmethod
-    def from_str(cls, value: str) -> Self:
-        'Produce from string assumed to fit _!_@_ pattern.'
-        assert cls.possible_from(value)
-        toks = value.split('!')
-        toks = toks[0:1] + toks[1].split('@')
-        return cls(*toks)
-
     @property
     def incremented(self) -> str:
         'Return .nick with number suffix incremented, or "0" if none.'
@@ -837,27 +808,22 @@ class Client(ABC, ClientQueueMixin):
 
     def handle_msg(self, msg: IrcMessage) -> None:
         'Log msg.raw, then process incoming msg into appropriate client steps.'
-        ret = {}
+        ret: Optional[dict[str, Any]] = None
         for ex in [ex for ex in MSG_EXPECTATIONS if ex.verb == msg.verb]:
-            result = ex.parse_msg(
-                    msg=msg,
-                    is_chan_name=self.db.is_chan_name,
-                    is_nick=self.db.is_nick,
-                    possible_nickuserhost=_NickUserHost.possible_from,
-                    into_nickuserhost=_NickUserHost.from_str)
-            if result is not None:
-                ret = result
+            ret = ex.parse_msg(msg, self.db.is_chan_name, self.db.is_nick)
+            if ret is not None:
                 break
-        if '_verb' not in ret:
+        if ret is None:
             raise ImplementationFail(msg.raw)
         for n_u_h in ret['_nickuserhosts']:  # update, turn into proper users
             if ret['_verb'] == 'QUIT' and 'me' not in self.db.users.keys():
                 # a QUIT before server gave us names should refer to us (and
                 # we need a "me" user to consequently call its .quit)
                 self.db.users['me'].nickuserhost = n_u_h
-            if (id_ := self.db.users.id_for_nickuserhost(
-                    n_u_h, allow_none=True, updating=True)):
-                for ret_name in [k for k in ret if ret[k] is n_u_h]:
+            if (id_ := self.db.users.id_for_nickuserhost(n_u_h,
+                                                         allow_none=True,
+                                                         updating=True)):
+                for ret_name in [k for k in ret if ret[k] == n_u_h]:
                     ret[ret_name] = self.db.users[id_]
         for verb in ('setattr', 'do', 'doafter'):
             for task, tok_names in [t for t in ret['_tasks'].items()
index 63e0779d145b0ad701d0c9daa9f13050eff7c5a6..04e90a2c7ee937e0bad609bdf778295920a46160 100644 (file)
@@ -7,10 +7,10 @@ from typing import Any, Callable, Optional, Sequence
 # ourselves
 from ircplom.client import (
         Channel, ChatMessage, Client, ClientQueueMixin, ImplementationFail,
-        IrcConnSetup, NewClientEvent, NickUserHost, SendFail,
-        ServerCapability, SharedClientDbFields, TargetUserOffline, User)
+        IrcConnSetup, NewClientEvent, SendFail, ServerCapability,
+        SharedClientDbFields, TargetUserOffline, User)
 from ircplom.db_primitives import AutoAttrMixin, Dict, DictItem
-from ircplom.irc_conn import IrcMessage
+from ircplom.irc_conn import IrcMessage, NickUserHost
 from ircplom.tui_base import (
         BaseTui, FormattingString, PromptWidget, TuiEvent, Window,
         CMD_SHORTCUTS, LOG_FMT_ATTRS, LOG_FMT_TAG_ALERT, LOG_PREFIX_DEFAULT)
index 3e15e04bc3fbea8aeda0998bf4f917df7d4f846f..21a74e494b70ef1df463695f9fb7e1f8136300fd 100644 (file)
@@ -1,6 +1,7 @@
-'Low-level IRC protocol / server connection management.'
+'Low-level IRC protocol / server connection management, data primitives.'
 # built-ins
 from abc import ABC, abstractmethod
+from dataclasses import dataclass
 from socket import EAI_AGAIN, EBADF, socket, gaierror as socket_gaierror
 from ssl import create_default_context as create_ssl_context
 from datetime import datetime
@@ -32,6 +33,36 @@ _IRCSPEC_TAG_ESCAPES = ((r'\:', ';'),
                         (r'\\', '\\'))
 
 
+@dataclass
+class NickUserHost:
+    'Combination of nickname, username on host, and host.'
+    nick: str = '?'
+    user: str = '?'
+    host: str = '?'
+
+    def __str__(self) -> str:
+        return f'{self.nick}!{self.user}@{self.host}'
+
+    @staticmethod
+    def possible_from(value: str) -> bool:
+        'If class instance could be parsed from value.'
+        toks = value.split('!')
+        if not len(toks) == 2:
+            return False
+        toks = toks[1].split('@')
+        if not len(toks) == 2:
+            return False
+        return True
+
+    @classmethod
+    def from_str(cls, value: str) -> Self:
+        'Produce from string assumed to fit _!_@_ pattern.'
+        assert cls.possible_from(value)
+        toks = value.split('!')
+        toks = toks[0:1] + toks[1].split('@')
+        return cls(*toks)
+
+
 class IrcMessage:
     'Properly structured representation of IRC message as per IRCv3 spec.'
     _raw: Optional[str] = None
index e01da27910dbae01f0275b0425ca04bfda39d895..5b622e9d2cf1359478dbf0876627ca395bba22d2 100644 (file)
@@ -1,10 +1,13 @@
 'Structured expectations and processing hints for server messages.'
 from enum import Enum, auto
 from typing import Any, Callable, NamedTuple, Optional, Self
-from ircplom.irc_conn import IrcMessage
+from ircplom.irc_conn import IrcMessage, NickUserHost
 
 
-class _MsgTok(Enum):
+_TOK_SKIPNUH = 'SKIPNUH'
+
+
+class _MsgToken(Enum):
     'Server message token classifications.'
     ANY = auto()
     CHANNEL = auto()
@@ -15,7 +18,9 @@ class _MsgTok(Enum):
     NICK_USER_HOST = auto()
 
 
-_MsgTokGuide = str | _MsgTok | tuple[str | _MsgTok, str]
+_MsgTokenWithStr = _MsgToken | str
+_MsgTokenGuide = _MsgTokenWithStr | tuple[_MsgTokenWithStr, str]
+_MsgTokenValidatorDict = dict[_MsgTokenWithStr, Callable[[Any], bool]]
 
 
 class _Command(NamedTuple):
@@ -30,112 +35,122 @@ class _Command(NamedTuple):
                                if path_str))
 
 
-class _MsgParseExpectation:
+class _Code(NamedTuple):
+    tok_name: str = ''
+    commands: tuple[_Command, ...] = tuple()
+    skip_nuh: bool = False
 
-    def __init__(self,
-                 verb: str,
-                 source: _MsgTokGuide,
-                 params: tuple[_MsgTokGuide, ...] = tuple(),
-                 bonus_tasks: tuple[str, ...] = tuple()
-                 ) -> None:
+    def __bool__(self) -> bool:
+        return bool(self.tok_name) or bool(self.commands)
 
-        class _Code(NamedTuple):
-            title: str
-            commands: tuple[_Command, ...]
+    @classmethod
+    def from_(cls, input_: str) -> Self:
+        'Split by ":" into commands (further split by ","), tok_name.'
+        commands_str, tok_name = input_.split(':', maxsplit=1)
+        skip_nuh = False
+        commands: list[_Command] = []
+        for command_str in commands_str.split(','):
+            if command_str == _TOK_SKIPNUH:
+                skip_nuh = True
+            elif command_str:
+                commands += [_Command.from_(command_str)]
+        return cls(tok_name, tuple(commands), skip_nuh)
+
+
+class _TokenExpectation(NamedTuple):
+    type_: _MsgTokenWithStr
+    code: _Code
 
-            @classmethod
-            def from_(cls, input_: str) -> Self:
-                'Split by ":" into commands (further split by ","), title.'
-                cmdsstr, title = input_.split(':', maxsplit=1)
-                return cls(title, tuple(_Command.from_(t)
-                                        for t in cmdsstr.split(',') if t))
+    @classmethod
+    def from_(cls, val: _MsgTokenGuide) -> Self:
+        'Standardize value into .type_, (potentially empty) .code.'
+        t = ((val[0], _Code.from_(val[1])) if isinstance(val, tuple)
+             else (val, _Code()))
+        return cls(*t)
 
-        class _TokExpectation(NamedTuple):
-            type_: _MsgTok | str
-            code: Optional[_Code]
 
-            @classmethod
-            def from_(cls, val: _MsgTokGuide) -> Self:
-                'Standardize value into .type_, (potentially empty) .code.'
-                t = ((val[0], _Code.from_(val[1])) if isinstance(val, tuple)
-                     else (val, None))
-                return cls(*t)
+class _MsgParseExpectation:
+    VALIDATORS: _MsgTokenValidatorDict = {
+        _MsgToken.NONE: lambda tok: tok == '',
+        _MsgToken.NICK_USER_HOST: NickUserHost.possible_from,
+        _MsgToken.SERVER: lambda tok: ('.' in tok
+                                       and not set('@!') & set(tok))}
+    PARSERS: dict[_MsgTokenWithStr, Callable] = {
+        _MsgToken.LIST: lambda tok: tuple(tok.split()),
+        _MsgToken.NICK_USER_HOST: NickUserHost.from_str}
 
+    def __init__(self,
+                 verb: str,
+                 source: _MsgTokenGuide,
+                 params: tuple[_MsgTokenGuide, ...] = tuple(),
+                 bonus_tasks: tuple[str, ...] = tuple()
+                 ) -> None:
         self.verb = verb
-        self.source = _TokExpectation.from_(source)
-        self.params = tuple(_TokExpectation.from_(param) for param in params)
+        self.source = _TokenExpectation.from_(source)
+        self.params = tuple(_TokenExpectation.from_(param) for param in params)
         self.bonus_tasks = tuple(_Code.from_(item) for item in bonus_tasks)
 
     def parse_msg(self,
                   msg: IrcMessage,
                   is_chan_name: Callable,
                   is_nick: Callable,
-                  possible_nickuserhost: Callable,
-                  into_nickuserhost: Callable
                   ) -> Optional[dict[str, Any]]:
         'Try parsing msg into informative result dictionary, or None on fail.'
-        cmp_params: list[str | tuple[str, ...]] = []
-        idx_after = 0
-        for idx, param in enumerate(self.params):
-            if param == _MsgTok.LIST or (isinstance(param, tuple)
-                                         and param[0] == _MsgTok.LIST):
-                idx_after = len(msg.params) + 1 - (len(self.params) - idx)
-                cmp_params += [' '.join(msg.params[idx:idx_after])]
-                cmp_params += list(msg.params[idx_after:])
-                break
-            cmp_params += msg.params[idx:idx + 1]
-        if (not idx_after) and len(cmp_params) != len(msg.params):
-            return None
-        cmp_fields = tuple([msg.source] + cmp_params)
-        ex_fields = tuple([self.source] + list(self.params))
-        if len(ex_fields) != len(cmp_fields):
-            return None
-        validators: dict[_MsgTok, Callable[[Any], bool]] = {
-            _MsgTok.NONE: lambda tok: tok == '',
-            _MsgTok.CHANNEL: is_chan_name,
-            _MsgTok.NICKNAME: is_nick,
-            _MsgTok.NICK_USER_HOST: possible_nickuserhost,
-            _MsgTok.SERVER: lambda tok: '.' in tok and not set('@!') & set(tok)
-        }
-        parsers: dict[_MsgTok, Callable[[Any], Any]] = {
-            _MsgTok.LIST: lambda tok: tuple(tok.split()),
-            _MsgTok.NICK_USER_HOST: into_nickuserhost
-        }
-        parsed: dict[str, str | tuple[str, ...]] = {}
-        singled_tasks: list[tuple[_Command, str]] = []
-        nickuserhosts = []
-        for ex_tok, cmp_tok in [(ex_tok, cmp_fields[idx])
-                                for idx, ex_tok in enumerate(ex_fields)]:
-            if isinstance(ex_tok.type_, str) and ex_tok.type_ != cmp_tok:
-                return None
-            if (not isinstance(ex_tok.type_, str))\
-                    and ex_tok.type_ in validators\
-                    and not validators[ex_tok.type_](cmp_tok):
-                return None
-            if ex_tok.code or ex_tok.type_ is _MsgTok.NICK_USER_HOST:
-                value = (cmp_tok if (isinstance(ex_tok.type_, str)
-                                     or ex_tok.type_ not in parsers)
-                         else parsers[ex_tok.type_](cmp_tok))
-                if ex_tok.type_ is _MsgTok.NICK_USER_HOST:
-                    if not (ex_tok.code and
-                            'skipnuh' in [cmd.verb
-                                          for cmd in ex_tok.code.commands]):
-                        nickuserhosts += [value]
-                if ex_tok.code:
-                    parsed[ex_tok.code.title] = value
-                    singled_tasks += [(cmd, ex_tok.code.title)
-                                      for cmd in ex_tok.code.commands
-                                      if cmd.verb != 'skipnuh']
-        for code in self.bonus_tasks:
-            singled_tasks += [(cmd, code.title) for cmd in code.commands]
-        tasks: dict[_Command, list[str]] = {}
-        for cmd, title in singled_tasks:
-            if cmd not in tasks:
-                tasks[cmd] = []
-            tasks[cmd] += [title]
-        return parsed | {'_verb': self.verb,
-                         '_tasks': tasks,
-                         '_nickuserhosts': nickuserhosts}
+        StrTuplable = str | tuple[str, ...]
+        RetDict = dict[str, StrTuplable | tuple[NickUserHost, ...]]
+        validators = self.VALIDATORS | {_MsgToken.CHANNEL: is_chan_name,
+                                        _MsgToken.NICKNAME: is_nick}
+
+        def divide_msg(msg: IrcMessage) -> Optional[tuple[StrTuplable, ...]]:
+            from_msg: list[StrTuplable] = []
+            idx_after_list = 0
+            for idx, param in enumerate(self.params):
+                if param.type_ == _MsgToken.LIST:
+                    remaining_exp_params = len(self.params) - idx
+                    idx_after_list = len(msg.params) - remaining_exp_params + 1
+                    from_msg += [' '.join(msg.params[idx:idx_after_list])]
+                    from_msg += list(msg.params[idx_after_list:])
+                    break
+                from_msg += msg.params[idx:idx + 1]  # collect [] if none there
+            if len(from_msg) < len(msg.params) and not idx_after_list:
+                return None  # not all msg.params collected
+            if len(self.params) != len(from_msg):
+                return None  # msg.params are too few or too many
+            return tuple([msg.source] + from_msg)
+
+        def harvest_msg(exp_fields: tuple[_TokenExpectation, ...],
+                        msg_fields: tuple[StrTuplable, ...],
+                        validators: _MsgTokenValidatorDict
+                        ) -> Optional[RetDict]:
+            def parsed(type_: _MsgToken | str, to_parse: StrTuplable):
+                return self.PARSERS.get(type_, lambda keep: keep)(to_parse)
+
+            d: RetDict = {}
+            nickuserhosts: list[NickUserHost] = []
+            for exp_tok, msg_tok in [(exp_tok, msg_fields[idx])
+                                     for idx, exp_tok
+                                     in enumerate(exp_fields)]:
+                if not validators.get(exp_tok.type_, lambda _: True)(msg_tok):
+                    return None  # validator found for this type, failed it
+                if isinstance(exp_tok.type_, str) and exp_tok.type_ != msg_tok:
+                    return None  # validator for "specific string"
+                if exp_tok.type_ is _MsgToken.NICK_USER_HOST\
+                        and not exp_tok.code.skip_nuh:
+                    nickuserhosts += [parsed(exp_tok.type_, msg_tok)]
+                if exp_tok.code.tok_name:
+                    d[exp_tok.code.tok_name] = parsed(exp_tok.type_, msg_tok)
+            return d | {'_nickuserhosts': tuple(nickuserhosts)}
+
+        if (msg_fields := divide_msg(msg)):
+            exp_fields = tuple([self.source] + list(self.params))
+            if (to_ret := harvest_msg(exp_fields, msg_fields, validators)):
+                tasks: dict[_Command, list[str]] = {}
+                for code in (tuple(exp_field.code for exp_field in exp_fields)
+                             + self.bonus_tasks):
+                    for cmd in code.commands:
+                        tasks[cmd] = tasks.get(cmd, []) + [code.tok_name]
+                return to_ret | {'_verb': self.verb, '_tasks': tasks}
+        return None
 
 
 MSG_EXPECTATIONS: list[_MsgParseExpectation] = [
@@ -144,160 +159,160 @@ MSG_EXPECTATIONS: list[_MsgParseExpectation] = [
 
     _MsgParseExpectation(
         '001',  # RPL_WELCOME
-        _MsgTok.SERVER,
-        ((_MsgTok.NICKNAME, 'setattr_db.users.me:nick'),
-         _MsgTok.ANY),
+        _MsgToken.SERVER,
+        ((_MsgToken.NICKNAME, 'setattr_db.users.me:nick'),
+         _MsgToken.ANY),
         bonus_tasks=('do_autojoin:',)),
 
     _MsgParseExpectation(
         '002',  # RPL_YOURHOST
-        _MsgTok.SERVER,
-        ((_MsgTok.NICKNAME, 'setattr_db.users.me:nick'),
-         _MsgTok.ANY)),
+        _MsgToken.SERVER,
+        ((_MsgToken.NICKNAME, 'setattr_db.users.me:nick'),
+         _MsgToken.ANY)),
 
     _MsgParseExpectation(
         '003',  # RPL_CREATED
-        _MsgTok.SERVER,
-        ((_MsgTok.NICKNAME, 'setattr_db.users.me:nick'),
-         _MsgTok.ANY)),
+        _MsgToken.SERVER,
+        ((_MsgToken.NICKNAME, 'setattr_db.users.me:nick'),
+         _MsgToken.ANY)),
 
     _MsgParseExpectation(
         '004',  # RPL_MYINFO
-        _MsgTok.SERVER,
-        ((_MsgTok.NICKNAME, 'setattr_db.users.me:nick'),
-         _MsgTok.ANY,
-         _MsgTok.ANY,
-         _MsgTok.ANY,
-         _MsgTok.ANY,
-         _MsgTok.ANY)),
+        _MsgToken.SERVER,
+        ((_MsgToken.NICKNAME, 'setattr_db.users.me:nick'),
+         _MsgToken.ANY,
+         _MsgToken.ANY,
+         _MsgToken.ANY,
+         _MsgToken.ANY,
+         _MsgToken.ANY)),
 
     _MsgParseExpectation(
         '250',  # RPL_STATSDLINE / RPL_STATSCONN
-        _MsgTok.SERVER,
-        ((_MsgTok.NICKNAME, 'setattr_db.users.me:nick'),
-         _MsgTok.ANY)),
+        _MsgToken.SERVER,
+        ((_MsgToken.NICKNAME, 'setattr_db.users.me:nick'),
+         _MsgToken.ANY)),
 
     _MsgParseExpectation(
         '251',  # RPL_LUSERCLIENT
-        _MsgTok.SERVER,
-        ((_MsgTok.NICKNAME, 'setattr_db.users.me:nick'),
-         _MsgTok.ANY)),
+        _MsgToken.SERVER,
+        ((_MsgToken.NICKNAME, 'setattr_db.users.me:nick'),
+         _MsgToken.ANY)),
 
     _MsgParseExpectation(
         '252',  # RPL_LUSEROP
-        _MsgTok.SERVER,
-        ((_MsgTok.NICKNAME, 'setattr_db.users.me:nick'),
-         _MsgTok.ANY,
-         _MsgTok.ANY)),
+        _MsgToken.SERVER,
+        ((_MsgToken.NICKNAME, 'setattr_db.users.me:nick'),
+         _MsgToken.ANY,
+         _MsgToken.ANY)),
 
     _MsgParseExpectation(
         '253',  # RPL_LUSERUNKNOWN
-        _MsgTok.SERVER,
-        ((_MsgTok.NICKNAME, 'setattr_db.users.me:nick'),
-         _MsgTok.ANY,
-         _MsgTok.ANY)),
+        _MsgToken.SERVER,
+        ((_MsgToken.NICKNAME, 'setattr_db.users.me:nick'),
+         _MsgToken.ANY,
+         _MsgToken.ANY)),
 
     _MsgParseExpectation(
         '254',  # RPL_LUSERCHANNELS
-        _MsgTok.SERVER,
-        ((_MsgTok.NICKNAME, 'setattr_db.users.me:nick'),
-         _MsgTok.ANY,
-         _MsgTok.ANY)),
+        _MsgToken.SERVER,
+        ((_MsgToken.NICKNAME, 'setattr_db.users.me:nick'),
+         _MsgToken.ANY,
+         _MsgToken.ANY)),
 
     _MsgParseExpectation(
         '255',  # RPL_LUSERME
-        _MsgTok.SERVER,
-        ((_MsgTok.NICKNAME, 'setattr_db.users.me:nick'),
-         _MsgTok.ANY)),
+        _MsgToken.SERVER,
+        ((_MsgToken.NICKNAME, 'setattr_db.users.me:nick'),
+         _MsgToken.ANY)),
 
     _MsgParseExpectation(
         '265',  # RPL_LOCALUSERS
-        _MsgTok.SERVER,
-        ((_MsgTok.NICKNAME, 'setattr_db.users.me:nick'),
-         _MsgTok.ANY)),
+        _MsgToken.SERVER,
+        ((_MsgToken.NICKNAME, 'setattr_db.users.me:nick'),
+         _MsgToken.ANY)),
     _MsgParseExpectation(
         '265',  # RPL_LOCALUSERS
-        _MsgTok.SERVER,
-        ((_MsgTok.NICKNAME, 'setattr_db.users.me:nick'),
-         _MsgTok.ANY,
-         _MsgTok.ANY,
-         _MsgTok.ANY)),
+        _MsgToken.SERVER,
+        ((_MsgToken.NICKNAME, 'setattr_db.users.me:nick'),
+         _MsgToken.ANY,
+         _MsgToken.ANY,
+         _MsgToken.ANY)),
 
     _MsgParseExpectation(
         '266',  # RPL_GLOBALUSERS
-        _MsgTok.SERVER,
-        ((_MsgTok.NICKNAME, 'setattr_db.users.me:nick'),
-         _MsgTok.ANY)),
+        _MsgToken.SERVER,
+        ((_MsgToken.NICKNAME, 'setattr_db.users.me:nick'),
+         _MsgToken.ANY)),
     _MsgParseExpectation(
         '266',  # RPL_GLOBALUSERS
-        _MsgTok.SERVER,
-        ((_MsgTok.NICKNAME, 'setattr_db.users.me:nick'),
-         _MsgTok.ANY,
-         _MsgTok.ANY,
-         _MsgTok.ANY)),
+        _MsgToken.SERVER,
+        ((_MsgToken.NICKNAME, 'setattr_db.users.me:nick'),
+         _MsgToken.ANY,
+         _MsgToken.ANY,
+         _MsgToken.ANY)),
 
     _MsgParseExpectation(
         '375',  # RPL_MOTDSTART already implied by 1st 372
-        _MsgTok.SERVER,
-        ((_MsgTok.NICKNAME, 'setattr_db.users.me:nick'),
-         _MsgTok.ANY)),
+        _MsgToken.SERVER,
+        ((_MsgToken.NICKNAME, 'setattr_db.users.me:nick'),
+         _MsgToken.ANY)),
 
     # various login stuff
 
     _MsgParseExpectation(
         '005',  # RPL_ISUPPORT
-        _MsgTok.SERVER,
-        ((_MsgTok.NICKNAME, 'setattr_db.users.me:nick'),
-         (_MsgTok.LIST, 'do_db.set_isupport_from_rpl:isupport'),
-         _MsgTok.ANY)),  # comment
+        _MsgToken.SERVER,
+        ((_MsgToken.NICKNAME, 'setattr_db.users.me:nick'),
+         (_MsgToken.LIST, 'do_db.set_isupport_from_rpl:isupport'),
+         _MsgToken.ANY)),  # comment
 
     _MsgParseExpectation(
         '372',  # RPL_MOTD
-        _MsgTok.SERVER,
-        ((_MsgTok.NICKNAME, 'setattr_db.users.me:nick'),
-         (_MsgTok.ANY, 'do_db.motd.append:line'))),
+        _MsgToken.SERVER,
+        ((_MsgToken.NICKNAME, 'setattr_db.users.me:nick'),
+         (_MsgToken.ANY, 'do_db.motd.append:line'))),
 
     _MsgParseExpectation(
         '376',  # RPL_ENDOFMOTD
-        _MsgTok.SERVER,
-        ((_MsgTok.NICKNAME, 'setattr_db.users.me:nick'),
-         _MsgTok.ANY),  # comment
+        _MsgToken.SERVER,
+        ((_MsgToken.NICKNAME, 'setattr_db.users.me:nick'),
+         _MsgToken.ANY),  # comment
         bonus_tasks=('do_db.motd.complete:',)),
 
     _MsgParseExpectation(
         '396',  # RPL_VISIBLEHOST
-        _MsgTok.SERVER,
-        ((_MsgTok.NICKNAME, 'setattr_db.users.me:nick'),
-         (_MsgTok.SERVER, 'setattr_db.users.me:host'),
-         _MsgTok.ANY)),  # comment
+        _MsgToken.SERVER,
+        ((_MsgToken.NICKNAME, 'setattr_db.users.me:nick'),
+         (_MsgToken.SERVER, 'setattr_db.users.me:host'),
+         _MsgToken.ANY)),  # comment
 
     # SASL
 
     _MsgParseExpectation(
         '900',  # RPL_LOGGEDIN
-        _MsgTok.SERVER,
-        ((_MsgTok.NICKNAME, 'setattr_db.users.me:nick'),
-         (_MsgTok.NICK_USER_HOST, 'setattr_db.users.me:nickuserhost'),
-         (_MsgTok.ANY, 'setattr_db:sasl_account'),
-         _MsgTok.ANY)),  # comment
+        _MsgToken.SERVER,
+        ((_MsgToken.NICKNAME, 'setattr_db.users.me:nick'),
+         (_MsgToken.NICK_USER_HOST, 'setattr_db.users.me:nickuserhost'),
+         (_MsgToken.ANY, 'setattr_db:sasl_account'),
+         _MsgToken.ANY)),  # comment
 
     _MsgParseExpectation(
         '903',  # RPL_SASLSUCCESS
-        _MsgTok.SERVER,
-        ((_MsgTok.NICKNAME, 'setattr_db.users.me:nick'),
-         (_MsgTok.ANY, 'setattr_db:sasl_auth_state')),
+        _MsgToken.SERVER,
+        ((_MsgToken.NICKNAME, 'setattr_db.users.me:nick'),
+         (_MsgToken.ANY, 'setattr_db:sasl_auth_state')),
         bonus_tasks=('do_caps.end_negotiation:',)),
 
     _MsgParseExpectation(
         '904',  # ERR_SASLFAIL
-        _MsgTok.SERVER,
-        ((_MsgTok.NICKNAME, 'setattr_db.users.me:nick'),
-         (_MsgTok.ANY, 'setattr_db:sasl_auth_state')),
+        _MsgToken.SERVER,
+        ((_MsgToken.NICKNAME, 'setattr_db.users.me:nick'),
+         (_MsgToken.ANY, 'setattr_db:sasl_auth_state')),
         bonus_tasks=('do_caps.end_negotiation:',)),
 
     _MsgParseExpectation(
         'AUTHENTICATE',
-        _MsgTok.NONE,
+        _MsgToken.NONE,
         ('+',),
         bonus_tasks=('do_send_authentication:',)),
 
@@ -305,286 +320,286 @@ MSG_EXPECTATIONS: list[_MsgParseExpectation] = [
 
     _MsgParseExpectation(
         'CAP',
-        _MsgTok.SERVER,
-        ((_MsgTok.NICKNAME, 'setattr_db.users.me:nick'),
+        _MsgToken.SERVER,
+        ((_MsgToken.NICKNAME, 'setattr_db.users.me:nick'),
          ('NEW', ':subverb'),
-         (_MsgTok.LIST, ':items'))),
+         (_MsgToken.LIST, ':items'))),
 
     _MsgParseExpectation(
         'CAP',
-        _MsgTok.SERVER,
-        ((_MsgTok.NICKNAME, 'setattr_db.users.me:nick'),
+        _MsgToken.SERVER,
+        ((_MsgToken.NICKNAME, 'setattr_db.users.me:nick'),
          ('DEL', ':subverb'),
-         (_MsgTok.LIST, ':items'))),
+         (_MsgToken.LIST, ':items'))),
 
     _MsgParseExpectation(
         'CAP',
-        _MsgTok.SERVER,
+        _MsgToken.SERVER,
         ('*',
          ('ACK', ':subverb'),
-         (_MsgTok.LIST, ':items'))),
+         (_MsgToken.LIST, ':items'))),
     _MsgParseExpectation(
         'CAP',
-        _MsgTok.SERVER,
-        ((_MsgTok.NICKNAME, 'setattr_db.users.me:nick'),
+        _MsgToken.SERVER,
+        ((_MsgToken.NICKNAME, 'setattr_db.users.me:nick'),
          ('ACK', ':subverb'),
-         (_MsgTok.LIST, ':items'))),
+         (_MsgToken.LIST, ':items'))),
 
     _MsgParseExpectation(
         'CAP',
-        _MsgTok.SERVER,
+        _MsgToken.SERVER,
         ('*',
          ('NAK', ':subverb'),
-         (_MsgTok.LIST, ':items'))),
+         (_MsgToken.LIST, ':items'))),
     _MsgParseExpectation(
         'CAP',
-        _MsgTok.SERVER,
-        ((_MsgTok.NICKNAME, 'setattr_db.users.me:nick'),
+        _MsgToken.SERVER,
+        ((_MsgToken.NICKNAME, 'setattr_db.users.me:nick'),
          ('NAK', ':subverb'),
-         (_MsgTok.LIST, ':items'))),
+         (_MsgToken.LIST, ':items'))),
 
     _MsgParseExpectation(
         'CAP',
-        _MsgTok.SERVER,
+        _MsgToken.SERVER,
         ('*',
          ('LS', ':subverb'),
          ('*', ':tbc'),
-         (_MsgTok.LIST, ':items'))),
+         (_MsgToken.LIST, ':items'))),
     _MsgParseExpectation(
         'CAP',
-        _MsgTok.SERVER,
+        _MsgToken.SERVER,
         ('*',
          ('LS', ':subverb'),
-         (_MsgTok.LIST, ':items'))),
+         (_MsgToken.LIST, ':items'))),
     _MsgParseExpectation(
         'CAP',
-        _MsgTok.SERVER,
-        ((_MsgTok.NICKNAME, 'setattr_db.users.me:nick'),
+        _MsgToken.SERVER,
+        ((_MsgToken.NICKNAME, 'setattr_db.users.me:nick'),
          ('LS', ':subverb'),
-         (_MsgTok.LIST, ':items'))),
+         (_MsgToken.LIST, ':items'))),
     _MsgParseExpectation(
         'CAP',
-        _MsgTok.SERVER,
-        ((_MsgTok.NICKNAME, 'setattr_db.users.me:nick'),
+        _MsgToken.SERVER,
+        ((_MsgToken.NICKNAME, 'setattr_db.users.me:nick'),
          ('LS', ':subverb'),
          ('*', ':tbc'),
-         (_MsgTok.LIST, ':items'))),
+         (_MsgToken.LIST, ':items'))),
 
     _MsgParseExpectation(
         'CAP',
-        _MsgTok.SERVER,
+        _MsgToken.SERVER,
         ('*',
          ('LIST', ':subverb'),
          ('*', ':tbc'),
-         (_MsgTok.LIST, ':items'))),
+         (_MsgToken.LIST, ':items'))),
     _MsgParseExpectation(
         'CAP',
-        _MsgTok.SERVER,
+        _MsgToken.SERVER,
         ('*',
          ('LIST', ':subverb'),
-         (_MsgTok.LIST, ':items'))),
+         (_MsgToken.LIST, ':items'))),
     _MsgParseExpectation(
         'CAP',
-        _MsgTok.SERVER,
-        ((_MsgTok.NICKNAME, 'setattr_db.users.me:nick'),
+        _MsgToken.SERVER,
+        ((_MsgToken.NICKNAME, 'setattr_db.users.me:nick'),
          ('LIST', ':subverb'),
          ('*', ':tbc'),
-         (_MsgTok.LIST, ':items'))),
+         (_MsgToken.LIST, ':items'))),
     _MsgParseExpectation(
         'CAP',
-        _MsgTok.SERVER,
-        ((_MsgTok.NICKNAME, 'setattr_db.users.me:nick'),
+        _MsgToken.SERVER,
+        ((_MsgToken.NICKNAME, 'setattr_db.users.me:nick'),
          ('LIST', ':subverb'),
-         (_MsgTok.LIST, ':items'))),
+         (_MsgToken.LIST, ':items'))),
 
     # nickname management
 
     _MsgParseExpectation(
         '432',  # ERR_ERRONEOUSNICKNAME
-        _MsgTok.SERVER,
+        _MsgToken.SERVER,
         ('*',
-         _MsgTok.ANY,  # bad one probably fails our NICKNAME tests
-         _MsgTok.ANY),  # comment
+         _MsgToken.ANY,  # bad one probably fails our NICKNAME tests
+         _MsgToken.ANY),  # comment
         bonus_tasks=('do_close:',)),
     _MsgParseExpectation(
         '432',  # ERR_ERRONEOUSNICKNAME
-        _MsgTok.SERVER,
-        ((_MsgTok.NICKNAME, 'setattr_db.users.me:nick'),
-         _MsgTok.ANY,  # bad one probably fails our NICKNAME tests
-         _MsgTok.ANY)),  # comment
+        _MsgToken.SERVER,
+        ((_MsgToken.NICKNAME, 'setattr_db.users.me:nick'),
+         _MsgToken.ANY,  # bad one probably fails our NICKNAME tests
+         _MsgToken.ANY)),  # comment
 
     _MsgParseExpectation(
         '433',  # ERR_NICKNAMEINUSE
-        _MsgTok.SERVER,
+        _MsgToken.SERVER,
         ('*',
-         (_MsgTok.NICKNAME, 'do_increment_for_nicknameinuse:rejected'),
-         _MsgTok.ANY)),  # comment
+         (_MsgToken.NICKNAME, 'do_increment_for_nicknameinuse:rejected'),
+         _MsgToken.ANY)),  # comment
     _MsgParseExpectation(
         '433',  # ERR_NICKNAMEINUSE
-        _MsgTok.SERVER,
-        (_MsgTok.NICKNAME,  # we rather go for incrementation
-         (_MsgTok.NICKNAME, 'do_increment_for_nicknameinuse:rejected'),
-         _MsgTok.ANY)),  # comment
+        _MsgToken.SERVER,
+        (_MsgToken.NICKNAME,  # we rather go for incrementation
+         (_MsgToken.NICKNAME, 'do_increment_for_nicknameinuse:rejected'),
+         _MsgToken.ANY)),  # comment
 
     _MsgParseExpectation(
         'NICK',
-        (_MsgTok.NICK_USER_HOST, ':named'),
-        ((_MsgTok.NICKNAME, ':nick'),)),
+        (_MsgToken.NICK_USER_HOST, ':named'),
+        ((_MsgToken.NICKNAME, ':nick'),)),
 
     # joining/leaving
 
     _MsgParseExpectation(
         '332',  # RPL_TOPIC
-        _MsgTok.SERVER,
-        ((_MsgTok.NICKNAME, 'setattr_db.users.me:nick'),
-         (_MsgTok.CHANNEL, ':CHAN'),
-         (_MsgTok.ANY, 'setattr_db.channels.CHAN.topic:what'))),
+        _MsgToken.SERVER,
+        ((_MsgToken.NICKNAME, 'setattr_db.users.me:nick'),
+         (_MsgToken.CHANNEL, ':CHAN'),
+         (_MsgToken.ANY, 'setattr_db.channels.CHAN.topic:what'))),
 
     _MsgParseExpectation(
         '333',  # RPL_TOPICWHOTIME
-        _MsgTok.SERVER,
-        ((_MsgTok.NICKNAME, 'setattr_db.users.me:nick'),
-         (_MsgTok.CHANNEL, ':CHAN'),
-         (_MsgTok.NICK_USER_HOST,
-          'skipnuh_,setattr_db.channels.CHAN.topic:who'),
-         (_MsgTok.ANY, ':timestamp')),
+        _MsgToken.SERVER,
+        ((_MsgToken.NICKNAME, 'setattr_db.users.me:nick'),
+         (_MsgToken.CHANNEL, ':CHAN'),
+         (_MsgToken.NICK_USER_HOST,
+          f'{_TOK_SKIPNUH},setattr_db.channels.CHAN.topic:who'),
+         (_MsgToken.ANY, ':timestamp')),
         bonus_tasks=('doafter_db.channels.CHAN.topic.complete:',)),
 
     _MsgParseExpectation(
         '353',  # RPL_NAMREPLY
-        _MsgTok.SERVER,
-        ((_MsgTok.NICKNAME, 'setattr_db.users.me:nick'),
+        _MsgToken.SERVER,
+        ((_MsgToken.NICKNAME, 'setattr_db.users.me:nick'),
          '@',
-         (_MsgTok.CHANNEL, ':CHANNEL'),
-         (_MsgTok.LIST, 'do_db.channels.CHANNEL.add_from_namreply:names'))),
+         (_MsgToken.CHANNEL, ':CHANNEL'),
+         (_MsgToken.LIST, 'do_db.channels.CHANNEL.add_from_namreply:names'))),
     _MsgParseExpectation(
         '353',  # RPL_NAMREPLY
-        _MsgTok.SERVER,
-        ((_MsgTok.NICKNAME, 'setattr_db.users.me:nick'),
+        _MsgToken.SERVER,
+        ((_MsgToken.NICKNAME, 'setattr_db.users.me:nick'),
          '=',
-         (_MsgTok.CHANNEL, ':CHANNEL'),
-         (_MsgTok.LIST, 'do_db.channels.CHANNEL.add_from_namreply:names'))),
+         (_MsgToken.CHANNEL, ':CHANNEL'),
+         (_MsgToken.LIST, 'do_db.channels.CHANNEL.add_from_namreply:names'))),
 
     _MsgParseExpectation(
         '366',  # RPL_ENDOFNAMES
-        _MsgTok.SERVER,
-        ((_MsgTok.NICKNAME, 'setattr_db.users.me:nick'),
-         (_MsgTok.CHANNEL, ':CHAN'),
-         _MsgTok.ANY),  # comment
+        _MsgToken.SERVER,
+        ((_MsgToken.NICKNAME, 'setattr_db.users.me:nick'),
+         (_MsgToken.CHANNEL, ':CHAN'),
+         _MsgToken.ANY),  # comment
         bonus_tasks=('doafter_db.channels.CHAN.user_ids.complete:',)),
 
     _MsgParseExpectation(
         'JOIN',
-        (_MsgTok.NICK_USER_HOST, 'do_db.channels.CHANNEL.join_user:user'),
-        ((_MsgTok.CHANNEL, ':CHANNEL'),)),
+        (_MsgToken.NICK_USER_HOST, 'do_db.channels.CHANNEL.join_user:user'),
+        ((_MsgToken.CHANNEL, ':CHANNEL'),)),
 
     _MsgParseExpectation(
         'PART',
-        (_MsgTok.NICK_USER_HOST, ':parter'),
-        ((_MsgTok.CHANNEL, ':channel'),)),
+        (_MsgToken.NICK_USER_HOST, ':parter'),
+        ((_MsgToken.CHANNEL, ':channel'),)),
     _MsgParseExpectation(
         'PART',
-        (_MsgTok.NICK_USER_HOST, ':parter'),
-        ((_MsgTok.CHANNEL, ':channel'),
-         (_MsgTok.ANY, ':message'))),
+        (_MsgToken.NICK_USER_HOST, ':parter'),
+        ((_MsgToken.CHANNEL, ':channel'),
+         (_MsgToken.ANY, ':message'))),
 
     # messaging
 
     _MsgParseExpectation(
         '401',  # ERR_NOSUCKNICK
-        _MsgTok.SERVER,
-        ((_MsgTok.NICKNAME, 'setattr_db.users.me:nick'),
-         (_MsgTok.NICKNAME, ':missing'),
-         _MsgTok.ANY)),  # comment
+        _MsgToken.SERVER,
+        ((_MsgToken.NICKNAME, 'setattr_db.users.me:nick'),
+         (_MsgToken.NICKNAME, ':missing'),
+         _MsgToken.ANY)),  # comment
 
     _MsgParseExpectation(
         'NOTICE',
-        _MsgTok.SERVER,
+        _MsgToken.SERVER,
         ('*',
-         (_MsgTok.ANY, 'setattr_db.messaging..to.:notice'))),
+         (_MsgToken.ANY, 'setattr_db.messaging..to.:notice'))),
     _MsgParseExpectation(
         'NOTICE',
-        _MsgTok.SERVER,
-        ((_MsgTok.NICKNAME, 'setattr_db.users.me:nick'),
-         (_MsgTok.ANY, 'setattr_db.messaging..to.:notice'))),
+        _MsgToken.SERVER,
+        ((_MsgToken.NICKNAME, 'setattr_db.users.me:nick'),
+         (_MsgToken.ANY, 'setattr_db.messaging..to.:notice'))),
 
     _MsgParseExpectation(
         'NOTICE',
-        (_MsgTok.SERVER, ':SERVER'),
-        ((_MsgTok.CHANNEL, ':CHANNEL'),
-         (_MsgTok.ANY, 'setattr_db.messaging.SERVER.to.CHANNEL:notice'))),
+        (_MsgToken.SERVER, ':SERVER'),
+        ((_MsgToken.CHANNEL, ':CHANNEL'),
+         (_MsgToken.ANY, 'setattr_db.messaging.SERVER.to.CHANNEL:notice'))),
 
     _MsgParseExpectation(
         'NOTICE',
-        (_MsgTok.NICK_USER_HOST, ':USER'),
-        ((_MsgTok.NICKNAME, 'setattr_db.users.me:nick'),
-         (_MsgTok.ANY, 'setattr_db.messaging.USER.to.:notice'))),
+        (_MsgToken.NICK_USER_HOST, ':USER'),
+        ((_MsgToken.NICKNAME, 'setattr_db.users.me:nick'),
+         (_MsgToken.ANY, 'setattr_db.messaging.USER.to.:notice'))),
 
     _MsgParseExpectation(
         'NOTICE',
-        (_MsgTok.NICK_USER_HOST, ':USER'),
-        ((_MsgTok.CHANNEL, ':CHANNEL'),
-         (_MsgTok.ANY, 'setattr_db.messaging.USER.to.CHANNEL:notice'))),
+        (_MsgToken.NICK_USER_HOST, ':USER'),
+        ((_MsgToken.CHANNEL, ':CHANNEL'),
+         (_MsgToken.ANY, 'setattr_db.messaging.USER.to.CHANNEL:notice'))),
 
     _MsgParseExpectation(
         'PRIVMSG',
-        (_MsgTok.NICK_USER_HOST, ':USER'),
-        ((_MsgTok.NICKNAME, 'setattr_db.users.me:nick'),
-         (_MsgTok.ANY, 'setattr_db.messaging.USER.to.:privmsg'))),
+        (_MsgToken.NICK_USER_HOST, ':USER'),
+        ((_MsgToken.NICKNAME, 'setattr_db.users.me:nick'),
+         (_MsgToken.ANY, 'setattr_db.messaging.USER.to.:privmsg'))),
     _MsgParseExpectation(
         'PRIVMSG',
-        (_MsgTok.NICK_USER_HOST, ':USER'),
-        ((_MsgTok.CHANNEL, ':CHANNEL'),
-         (_MsgTok.ANY, 'setattr_db.messaging.USER.to.CHANNEL:privmsg'))),
+        (_MsgToken.NICK_USER_HOST, ':USER'),
+        ((_MsgToken.CHANNEL, ':CHANNEL'),
+         (_MsgToken.ANY, 'setattr_db.messaging.USER.to.CHANNEL:privmsg'))),
 
     # connection state
 
     _MsgParseExpectation(
         'ERROR',
-        _MsgTok.NONE,
-        ((_MsgTok.ANY, 'setattr_db:connection_state'),),
+        _MsgToken.NONE,
+        ((_MsgToken.ANY, 'setattr_db:connection_state'),),
         bonus_tasks=('do_consider_retry:', 'doafter_close:',)),
 
     _MsgParseExpectation(
         'PING',
-        _MsgTok.NONE,
-        ((_MsgTok.ANY, 'do_pong:reply'),)),
+        _MsgToken.NONE,
+        ((_MsgToken.ANY, 'do_pong:reply'),)),
 
     _MsgParseExpectation(
         'PONG',
-        _MsgTok.SERVER,
-        (_MsgTok.SERVER,
-         (_MsgTok.ANY, 'do_check_pong:reply'),)),
+        _MsgToken.SERVER,
+        (_MsgToken.SERVER,
+         (_MsgToken.ANY, 'do_check_pong:reply'),)),
 
     # misc.
 
     _MsgParseExpectation(
         'MODE',
-        (_MsgTok.NICK_USER_HOST, 'setattr_db.users.me:nickuserhost'),
-        ((_MsgTok.NICKNAME, 'setattr_db.users.me:nick'),
-         (_MsgTok.ANY, 'setattr_db.users.me:modes'))),
+        (_MsgToken.NICK_USER_HOST, 'setattr_db.users.me:nickuserhost'),
+        ((_MsgToken.NICKNAME, 'setattr_db.users.me:nick'),
+         (_MsgToken.ANY, 'setattr_db.users.me:modes'))),
     _MsgParseExpectation(
         'MODE',
-        _MsgTok.NICKNAME,
-        ((_MsgTok.NICKNAME, 'setattr_db.users.me:nick'),
-         (_MsgTok.ANY, 'setattr_db.users.me:modes'))),
+        _MsgToken.NICKNAME,
+        ((_MsgToken.NICKNAME, 'setattr_db.users.me:nick'),
+         (_MsgToken.ANY, 'setattr_db.users.me:modes'))),
 
     _MsgParseExpectation(
         'MODE',
-        _MsgTok.SERVER,
-        ((_MsgTok.CHANNEL, ':channel'),
-         (_MsgTok.ANY, ':mode_on_nick'),
-         (_MsgTok.NICKNAME, ':nick'))),
+        _MsgToken.SERVER,
+        ((_MsgToken.CHANNEL, ':channel'),
+         (_MsgToken.ANY, ':mode_on_nick'),
+         (_MsgToken.NICKNAME, ':nick'))),
 
     _MsgParseExpectation(
         'TOPIC',
-        (_MsgTok.NICK_USER_HOST, 'setattr_db.channels.CHAN.topic:who'),
-        ((_MsgTok.CHANNEL, ':CHAN'),
-         (_MsgTok.ANY, 'setattr_db.channels.CHAN.topic:what')),
+        (_MsgToken.NICK_USER_HOST, 'setattr_db.channels.CHAN.topic:who'),
+        ((_MsgToken.CHANNEL, ':CHAN'),
+         (_MsgToken.ANY, 'setattr_db.channels.CHAN.topic:what')),
         bonus_tasks=('doafter_db.channels.CHAN.topic.complete:',)),
 
     _MsgParseExpectation(
         'QUIT',
-        (_MsgTok.NICK_USER_HOST, ':QUITTER'),
-        ((_MsgTok.ANY, 'do_QUITTER.quit:message'),)),
+        (_MsgToken.NICK_USER_HOST, ':QUITTER'),
+        ((_MsgToken.ANY, 'do_QUITTER.quit:message'),)),
 ]