From 50f3ded0985783739d4d0e5964ba7003aef3b459 Mon Sep 17 00:00:00 2001 From: Christian Heller Date: Wed, 3 Sep 2025 23:20:28 +0200 Subject: [PATCH] Move fundamental message parsing code into msg_parse_expectations module. --- ircplom/client.py | 157 +++------ ircplom/msg_parse_expectations.py | 530 ++++++++++++++++++------------ 2 files changed, 372 insertions(+), 315 deletions(-) diff --git a/ircplom/client.py b/ircplom/client.py index 342b4eb..509f8d3 100644 --- a/ircplom/client.py +++ b/ircplom/client.py @@ -14,7 +14,7 @@ from ircplom.events import ( from ircplom.irc_conn import ( BaseIrcConnection, IrcConnAbortException, IrcMessage, ILLEGAL_NICK_CHARS, ILLEGAL_NICK_FIRSTCHARS, ISUPPORT_DEFAULTS, PORT_SSL) -from ircplom.msg_parse_expectations import MsgTok, MSG_EXPECTATIONS +from ircplom.msg_parse_expectations import MSG_EXPECTATIONS _NAMES_DESIRED_SERVER_CAPS = ('sasl',) @@ -419,13 +419,23 @@ class _NickUserHost(NickUserHost): else: super().__setattr__(key, value) + @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('!') - assert len(toks) == 2 toks = toks[0:1] + toks[1].split('@') - assert len(toks) == 3 return cls(*toks) @property @@ -499,11 +509,17 @@ class _ClientDb(_UpdatingMixin, SharedClientDbFields): if id_ not in to_keep]: del self.users[user_id] - @property - def illegal_nick_firstchars(self) -> str: - 'Calculated from hardcoded constants and .isupport.' - return (ILLEGAL_NICK_CHARS + ILLEGAL_NICK_FIRSTCHARS - + self.isupport['CHANTYPES'] + self._get_membership_prefixes()) + def is_nick(self, nick: str) -> bool: + 'Tests name to match rules for nicknames.' + if len(nick) == 0: + return False + if nick[0] in (ILLEGAL_NICK_FIRSTCHARS + + self.isupport['CHANTYPES'] + + self._get_membership_prefixes()): + return False + for c in [c for c in nick if c in ILLEGAL_NICK_CHARS]: + return False + return True def _get_membership_prefixes(self) -> str: 'Registered possible membership nickname prefixes.' @@ -678,95 +694,30 @@ class Client(ABC, ClientQueueMixin): self._log(to_log, scope=log_target) self._log(msg.raw, scope=LogScope.RAW, out=True) - def _match_msg(self, msg: IrcMessage) -> dict[str, Any]: - 'Test .source, .verb, .params.' - tok_type = str | _NickUserHost | tuple[str, ...] - - def param_match(ex_tok: str | MsgTok, msg_tok: str | list[str] - ) -> Optional[tok_type | tuple[tok_type, ...]]: - if isinstance(msg_tok, list): - to_return = [] - for item in msg_tok: - result = param_match(ex_tok, item) - if not isinstance(result, tok_type): - return None - to_return += [result] - return tuple(to_return) - if isinstance(ex_tok, str): - return msg_tok if msg_tok == ex_tok else None - if ex_tok is MsgTok.NONE: - return msg_tok if msg_tok == '' else None - if ex_tok is MsgTok.SERVER: - return msg_tok if ('.' in msg_tok - and not set('@!') & set(msg_tok)) else None - if ex_tok is MsgTok.CHANNEL: - return msg_tok if self.db.is_chan_name(msg_tok) else None - if ex_tok is MsgTok.NICKNAME: - return (msg_tok - if msg_tok[0] not in self.db.illegal_nick_firstchars - else None) - if ex_tok is MsgTok.NICK_USER_HOST: - try: - return _NickUserHost.from_str(msg_tok) - except AssertionError: - return None - if ex_tok is MsgTok.LIST: - return tuple(msg_tok.split()) - return msg_tok - - def into_tasks_and_key(code: str, to_ret: dict[str, Any]) -> str: - tasks = to_ret['_tasks'] - cmds_str, key = code.split(':', maxsplit=1) if code else ('', '') - for command in [t for t in cmds_str.split(',') if t]: - tasks[command] = tasks.get(command, []) + [key] - return key - - for ex in [ex for ex in MSG_EXPECTATIONS if ex.verb == msg.verb]: - to_return: dict[str, Any] = {'verb': ex.verb, '_tasks': {}} - ex_tok_fields = tuple([ex.source] + list(ex.params)) - msg_params: list[str | list[str]] - if ex.idx_into_list < 0: - msg_params = list(msg.params) - else: - idx_remainders = len(msg.params) + 1 - (len(ex.params) - - ex.idx_into_list) - msg_params = list(msg.params[:ex.idx_into_list])\ - + [list(msg.params[ex.idx_into_list:idx_remainders])]\ - + list(msg.params[idx_remainders:]) - msg_tok_fields = tuple([msg.source] + msg_params) - if ex.params and len(ex_tok_fields) != len(msg_tok_fields): - continue - passing = True - for idx, ex_tok in enumerate(ex_tok_fields): - ex_tok, code = ((ex_tok[0], ex_tok[1]) - if isinstance(ex_tok, tuple) else (ex_tok, '')) - key = into_tasks_and_key(code, to_return) - to_return[key] = param_match(ex_tok, msg_tok_fields[idx]) - if to_return[key] is None: - passing = False - break - if passing: - for code in ex.bonus_tasks: - into_tasks_and_key(code, to_return) - return to_return - return {} - def handle_msg(self, msg: IrcMessage) -> None: 'Log msg.raw, then process incoming msg into appropriate client steps.' self._log(msg.raw, scope=LogScope.RAW, out=False) - ret = self._match_msg(msg) - if 'verb' not in ret: + ret = {} + 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 + break + if '_verb' not in ret: self._log(f'PLEASE IMPLEMENT HANDLER FOR: {msg.raw}') return for task, tok_names in ret['_tasks'].items(): - task_verb, target_name = task.split('_', maxsplit=1) - if task_verb == 'set' and target_name == 'user': + if task.verb == 'set' and task.path == ('user',): for tok_name in tok_names: self.db.user_id(ret[tok_name]) continue - path_toks = target_name.split('.') node = self - for step in [t for t in path_toks if t]: + for step in task.path: node = (node[ret[step] if step.isupper() else step] if isinstance(node, Dict) else getattr(node, step)) @@ -774,41 +725,41 @@ class Client(ABC, ClientQueueMixin): # FIXME: alphabetical sorting of tok_names merely hack to parse # TOPIC messages, to ensure any setattr_topic:what be processed # before any setattr_topic:who, i.e. properly completing .topic - if task_verb == 'setattr': + if task.verb == 'setattr': setattr(node, tok_name, ret[tok_name]) - elif task_verb == 'do': + elif task.verb == 'do': getattr(node, tok_name)() - if ret['verb'] == '005': # RPL_ISUPPORT + if ret['_verb'] == '005': # RPL_ISUPPORT for item in ret['isupport']: if item[0] == '-': del self.db.isupport[item[1:]] else: key, data = _Dict.key_val_from_eq_str(item) self.db.isupport[key] = data - elif ret['verb'] == '353': # RPL_NAMREPLY + elif ret['_verb'] == '353': # RPL_NAMREPLY self.db.channels[ret['channel']].add_from_namreply(ret['names']) - elif ret['verb'] == '372': # RPL_MOTD + elif ret['_verb'] == '372': # RPL_MOTD self.db.motd.append(ret['line']) - elif ret['verb'] == '401': # ERR_NOSUCHNICK + elif ret['_verb'] == '401': # ERR_NOSUCHNICK self._log(f'{ret["target"]} not online', scope=LogScope.CHAT, target=ret['target'], alert=True) - elif ret['verb'] == '432': # ERR_ERRONEOUSNICKNAME + elif ret['_verb'] == '432': # ERR_ERRONEOUSNICKNAME alert = 'nickname refused for bad format' if 'nick' not in ret: alert += ', giving up' self.close() self._log(alert, alert=True) - elif ret['verb'] == '433': # ERR_NICKNAMEINUSE + elif ret['_verb'] == '433': # ERR_NICKNAMEINUSE self._log('nickname already in use, trying increment', alert=True) self.send(IrcMessage( 'NICK', (_NickUserHost(nick=ret['used']).incremented,))) - elif ret['verb'] == 'AUTHENTICATE': + elif ret['_verb'] == 'AUTHENTICATE': auth = b64encode((self.db.nick_wanted + '\0' + self.db.nick_wanted + '\0' + self.db.password ).encode('utf-8')).decode('utf-8') self.send(IrcMessage('AUTHENTICATE', (auth,))) - elif ret['verb'] == 'CAP': + elif ret['_verb'] == 'CAP': if (self.caps.process_msg(verb=ret['subverb'], items=ret['items'], complete='tbc' not in ret) and 'sasl' in self.db.caps.keys() @@ -818,15 +769,15 @@ class Client(ABC, ClientQueueMixin): self.send(IrcMessage('AUTHENTICATE', ('PLAIN',))) else: self.caps.end_negotiation() - elif ret['verb'] == 'JOIN'\ + elif ret['_verb'] == 'JOIN'\ and ret['joiner'].nick != self.db.users['me'].nick: self.db.channels[ret['channel']].append_nick(ret['joiner']) - elif ret['verb'] == 'NICK': + elif ret['_verb'] == 'NICK': user_id = self.db.user_id(ret['named']) self.db.users[user_id].nick = ret['nick'] if user_id == 'me': self.db.nick_wanted = ret['nick'] - elif ret['verb'] in {'NOTICE', 'PRIVMSG'}: + elif ret['_verb'] in {'NOTICE', 'PRIVMSG'}: kw: dict[str, bool | str | LogScope] = { 'as_notice': msg.verb == 'NOTICE'} if 'sender' in ret: # not just server message @@ -834,16 +785,16 @@ class Client(ABC, ClientQueueMixin): 'target': (ret['sender'].nick if 'nick' in ret else ret['channel'])} self._log(ret['message'], out=False, **kw) - elif ret['verb'] == 'PART': + elif ret['_verb'] == 'PART': self.db.channels[ret['channel']].remove_nick(ret['parter']) if 'message' in ret: self._log(f'{ret["parter"]} parts: {ret["message"]}', LogScope.CHAT, target=ret['channel']) if ret['parter'] == self.db.users['me']: del self.db.channels[ret['channel']] - elif ret['verb'] == 'PING': + elif ret['_verb'] == 'PING': self.send(IrcMessage(verb='PONG', params=(ret['reply'],))) - elif ret['verb'] == 'QUIT': + elif ret['_verb'] == 'QUIT': for ch_name, ch in self.db.chans_of_user(ret['quitter']).items(): ch.remove_nick(ret['quitter']) self._log(f'{ret["quitter"]} quits: {ret["message"]}', diff --git a/ircplom/msg_parse_expectations.py b/ircplom/msg_parse_expectations.py index 35c3606..93ad0be 100644 --- a/ircplom/msg_parse_expectations.py +++ b/ircplom/msg_parse_expectations.py @@ -1,9 +1,10 @@ 'Structured expectations and processing hints for server messages.' from enum import Enum, auto -from typing import NamedTuple +from typing import Any, Callable, NamedTuple, Optional, Self +from ircplom.irc_conn import IrcMessage -class MsgTok(Enum): +class _MsgTok(Enum): 'Server message token classifications.' ANY = auto() CHANNEL = auto() @@ -14,15 +15,120 @@ class MsgTok(Enum): NICK_USER_HOST = auto() -_MsgTokGuide = str | MsgTok | tuple[str | MsgTok, str] - - -class _MsgParseExpectation(NamedTuple): - verb: str - source: _MsgTokGuide - params: tuple[_MsgTokGuide, ...] = tuple() - idx_into_list: int = -1 - bonus_tasks: tuple[str, ...] = tuple() +_MsgTokGuide = str | _MsgTok | tuple[str | _MsgTok, str] + + +class _Command: + + def __init__(self, input_: str) -> None: + self.verb, path_str = input_.split('_', maxsplit=1) + self.path = tuple(path_str.split('.')) + + def __str__(self) -> str: + return f'{self.verb}_{".".join(self.path)}' + + def __hash__(self) -> int: + return hash(str(self)) + + def __eq__(self, other) -> bool: + return hash(self) == hash(other) + + +class _MsgParseExpectation: + + def __init__(self, + verb: str, + source: _MsgTokGuide, + params: tuple[_MsgTokGuide, ...] = tuple(), + idx_into_list: int = -1, + bonus_tasks: tuple[str, ...] = tuple() + ) -> None: + + class _Code(NamedTuple): + title: str + commands: tuple[_Command, ...] + + @classmethod + def from_(cls, input_: str) -> Self: + 'Split by ":" into commands (further split by ","), title.' + toks = input_.split(':', maxsplit=1) + title = toks[1] + commands = [_Command(t) for t in toks[0].split(',') if t] + return cls(title, tuple(commands)) + + class _TokExpectation(NamedTuple): + type_: _MsgTok | str + code: Optional[_Code] + + @classmethod + def from_(cls, value: _MsgTokGuide) -> Self: + 'Standardize value into .type_, (potentially empty) code.' + type_, code = ((value[0], _Code.from_(value[1])) + if isinstance(value, tuple) + else (value, None)) + return cls(type_, code) + + self.verb = verb + self.source = _TokExpectation.from_(source) + self.params = tuple(_TokExpectation.from_(param) for param in params) + self.idx_into_list = idx_into_list + 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, ...]] + if self.idx_into_list < 0: + cmp_params = list(msg.params) + else: + idx_after = len(msg.params) + 1 - (len(self.params) + - self.idx_into_list) + cmp_params = (list(msg.params[:self.idx_into_list]) + + [msg.params[self.idx_into_list:idx_after]] + + list(msg.params[idx_after:])) + 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]] = [] + for ex_tok, cmp_tok in [(ex_tok, cmp_fields[idx]) + for idx, ex_tok in enumerate(ex_fields)]: + 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: + parsed[ex_tok.code.title] = ( + cmp_tok if (isinstance(ex_tok.type_, str) + or ex_tok.type_ not in parsers) + else parsers[ex_tok.type_](cmp_tok)) + singled_tasks += [(cmd, ex_tok.code.title) + for cmd in ex_tok.code.commands] + 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} MSG_EXPECTATIONS: list[_MsgParseExpectation] = [ @@ -31,419 +137,419 @@ MSG_EXPECTATIONS: list[_MsgParseExpectation] = [ _MsgParseExpectation( '001', # RPL_WELCOME - MsgTok.SERVER, - ((MsgTok.NICKNAME, 'setattr_db.users.me:nick'), - MsgTok.ANY)), + _MsgTok.SERVER, + ((_MsgTok.NICKNAME, 'setattr_db.users.me:nick'), + _MsgTok.ANY)), _MsgParseExpectation( '002', # RPL_YOURHOST - MsgTok.SERVER, - ((MsgTok.NICKNAME, 'setattr_db.users.me:nick'), - MsgTok.ANY)), + _MsgTok.SERVER, + ((_MsgTok.NICKNAME, 'setattr_db.users.me:nick'), + _MsgTok.ANY)), _MsgParseExpectation( '003', # RPL_CREATED - MsgTok.SERVER, - ((MsgTok.NICKNAME, 'setattr_db.users.me:nick'), - MsgTok.ANY)), + _MsgTok.SERVER, + ((_MsgTok.NICKNAME, 'setattr_db.users.me:nick'), + _MsgTok.ANY)), _MsgParseExpectation( '004', # RPL_MYINFO - MsgTok.SERVER, - ((MsgTok.NICKNAME, 'setattr_db.users.me:nick'), - MsgTok.ANY, - MsgTok.ANY, - MsgTok.ANY, - MsgTok.ANY, - MsgTok.ANY)), + _MsgTok.SERVER, + ((_MsgTok.NICKNAME, 'setattr_db.users.me:nick'), + _MsgTok.ANY, + _MsgTok.ANY, + _MsgTok.ANY, + _MsgTok.ANY, + _MsgTok.ANY)), _MsgParseExpectation( '250', # RPL_STATSDLINE / RPL_STATSCONN - MsgTok.SERVER, - ((MsgTok.NICKNAME, 'setattr_db.users.me:nick'), - MsgTok.ANY)), + _MsgTok.SERVER, + ((_MsgTok.NICKNAME, 'setattr_db.users.me:nick'), + _MsgTok.ANY)), _MsgParseExpectation( '251', # RPL_LUSERCLIENT - MsgTok.SERVER, - ((MsgTok.NICKNAME, 'setattr_db.users.me:nick'), - MsgTok.ANY)), + _MsgTok.SERVER, + ((_MsgTok.NICKNAME, 'setattr_db.users.me:nick'), + _MsgTok.ANY)), _MsgParseExpectation( '252', # RPL_LUSEROP - MsgTok.SERVER, - ((MsgTok.NICKNAME, 'setattr_db.users.me:nick'), - MsgTok.ANY, - MsgTok.ANY)), + _MsgTok.SERVER, + ((_MsgTok.NICKNAME, 'setattr_db.users.me:nick'), + _MsgTok.ANY, + _MsgTok.ANY)), _MsgParseExpectation( '253', # RPL_LUSERUNKNOWN - MsgTok.SERVER, - ((MsgTok.NICKNAME, 'setattr_db.users.me:nick'), - MsgTok.ANY, - MsgTok.ANY)), + _MsgTok.SERVER, + ((_MsgTok.NICKNAME, 'setattr_db.users.me:nick'), + _MsgTok.ANY, + _MsgTok.ANY)), _MsgParseExpectation( '254', # RPL_LUSERCHANNELS - MsgTok.SERVER, - ((MsgTok.NICKNAME, 'setattr_db.users.me:nick'), - MsgTok.ANY, - MsgTok.ANY)), + _MsgTok.SERVER, + ((_MsgTok.NICKNAME, 'setattr_db.users.me:nick'), + _MsgTok.ANY, + _MsgTok.ANY)), _MsgParseExpectation( '255', # RPL_LUSERME - MsgTok.SERVER, - ((MsgTok.NICKNAME, 'setattr_db.users.me:nick'), - MsgTok.ANY)), + _MsgTok.SERVER, + ((_MsgTok.NICKNAME, 'setattr_db.users.me:nick'), + _MsgTok.ANY)), _MsgParseExpectation( '265', # RPL_LOCALUSERS - MsgTok.SERVER, - ((MsgTok.NICKNAME, 'setattr_db.users.me:nick'), - MsgTok.ANY)), + _MsgTok.SERVER, + ((_MsgTok.NICKNAME, 'setattr_db.users.me:nick'), + _MsgTok.ANY)), _MsgParseExpectation( '265', # RPL_LOCALUSERS - MsgTok.SERVER, - ((MsgTok.NICKNAME, 'setattr_db.users.me:nick'), - MsgTok.ANY, - MsgTok.ANY, - MsgTok.ANY)), + _MsgTok.SERVER, + ((_MsgTok.NICKNAME, 'setattr_db.users.me:nick'), + _MsgTok.ANY, + _MsgTok.ANY, + _MsgTok.ANY)), _MsgParseExpectation( '266', # RPL_GLOBALUSERS - MsgTok.SERVER, - ((MsgTok.NICKNAME, 'setattr_db.users.me:nick'), - MsgTok.ANY)), + _MsgTok.SERVER, + ((_MsgTok.NICKNAME, 'setattr_db.users.me:nick'), + _MsgTok.ANY)), _MsgParseExpectation( '266', # RPL_GLOBALUSERS - MsgTok.SERVER, - ((MsgTok.NICKNAME, 'setattr_db.users.me:nick'), - MsgTok.ANY, - MsgTok.ANY, - MsgTok.ANY)), + _MsgTok.SERVER, + ((_MsgTok.NICKNAME, 'setattr_db.users.me:nick'), + _MsgTok.ANY, + _MsgTok.ANY, + _MsgTok.ANY)), _MsgParseExpectation( '375', # RPL_MOTDSTART already implied by 1st 372 - MsgTok.SERVER, - ((MsgTok.NICKNAME, 'setattr_db.users.me:nick'), - MsgTok.ANY)), + _MsgTok.SERVER, + ((_MsgTok.NICKNAME, 'setattr_db.users.me:nick'), + _MsgTok.ANY)), # various login stuff _MsgParseExpectation( '005', # RPL_ISUPPORT - MsgTok.SERVER, - ((MsgTok.NICKNAME, 'setattr_db.users.me:nick'), - (MsgTok.ANY, ':isupport'), - MsgTok.ANY), # comment + _MsgTok.SERVER, + ((_MsgTok.NICKNAME, 'setattr_db.users.me:nick'), + (_MsgTok.ANY, ':isupport'), + _MsgTok.ANY), # comment idx_into_list=1), _MsgParseExpectation( '372', # RPL_MOTD - MsgTok.SERVER, - ((MsgTok.NICKNAME, 'setattr_db.users.me:nick'), - (MsgTok.ANY, ':line'))), + _MsgTok.SERVER, + ((_MsgTok.NICKNAME, 'setattr_db.users.me:nick'), + (_MsgTok.ANY, ':line'))), _MsgParseExpectation( '376', # RPL_ENDOFMOTD - MsgTok.SERVER, - ((MsgTok.NICKNAME, 'setattr_db.users.me:nick'), - MsgTok.ANY), # comment + _MsgTok.SERVER, + ((_MsgTok.NICKNAME, 'setattr_db.users.me:nick'), + _MsgTok.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 + _MsgTok.SERVER, + ((_MsgTok.NICKNAME, 'setattr_db.users.me:nick'), + (_MsgTok.SERVER, 'setattr_db.users.me:host'), + _MsgTok.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 + _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 _MsgParseExpectation( '903', # RPL_SASLSUCCESS - MsgTok.SERVER, - ((MsgTok.NICKNAME, 'setattr_db.users.me:nick'), - (MsgTok.ANY, 'setattr_db:sasl_auth_state')), + _MsgTok.SERVER, + ((_MsgTok.NICKNAME, 'setattr_db.users.me:nick'), + (_MsgTok.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')), + _MsgTok.SERVER, + ((_MsgTok.NICKNAME, 'setattr_db.users.me:nick'), + (_MsgTok.ANY, 'setattr_db:sasl_auth_state')), bonus_tasks=('do_caps:end_negotiation',)), _MsgParseExpectation( 'AUTHENTICATE', - MsgTok.NONE, + _MsgTok.NONE, ('+',)), # capability negotation _MsgParseExpectation( 'CAP', - MsgTok.SERVER, - ((MsgTok.NICKNAME, 'setattr_db.users.me:nick'), + _MsgTok.SERVER, + ((_MsgTok.NICKNAME, 'setattr_db.users.me:nick'), ('NEW', ':subverb'), - (MsgTok.LIST, ':items'))), + (_MsgTok.LIST, ':items'))), _MsgParseExpectation( 'CAP', - MsgTok.SERVER, - ((MsgTok.NICKNAME, 'setattr_db.users.me:nick'), + _MsgTok.SERVER, + ((_MsgTok.NICKNAME, 'setattr_db.users.me:nick'), ('DEL', ':subverb'), - (MsgTok.LIST, ':items'))), + (_MsgTok.LIST, ':items'))), _MsgParseExpectation( 'CAP', - MsgTok.SERVER, + _MsgTok.SERVER, ('*', ('ACK', ':subverb'), - (MsgTok.LIST, ':items'))), + (_MsgTok.LIST, ':items'))), _MsgParseExpectation( 'CAP', - MsgTok.SERVER, - ((MsgTok.NICKNAME, 'setattr_db.users.me:nick'), + _MsgTok.SERVER, + ((_MsgTok.NICKNAME, 'setattr_db.users.me:nick'), ('ACK', ':subverb'), - (MsgTok.LIST, ':items'))), + (_MsgTok.LIST, ':items'))), _MsgParseExpectation( 'CAP', - MsgTok.SERVER, + _MsgTok.SERVER, ('*', ('NAK', ':subverb'), - (MsgTok.LIST, ':items'))), + (_MsgTok.LIST, ':items'))), _MsgParseExpectation( 'CAP', - MsgTok.SERVER, - ((MsgTok.NICKNAME, 'setattr_db.users.me:nick'), + _MsgTok.SERVER, + ((_MsgTok.NICKNAME, 'setattr_db.users.me:nick'), ('NAK', ':subverb'), - (MsgTok.LIST, ':items'))), + (_MsgTok.LIST, ':items'))), _MsgParseExpectation( 'CAP', - MsgTok.SERVER, + _MsgTok.SERVER, ('*', ('LS', ':subverb'), - (MsgTok.LIST, ':items'))), + (_MsgTok.LIST, ':items'))), _MsgParseExpectation( 'CAP', - MsgTok.SERVER, + _MsgTok.SERVER, ('*', ('LS', ':subverb'), ('*', ':tbc'), - (MsgTok.LIST, ':items'))), + (_MsgTok.LIST, ':items'))), _MsgParseExpectation( 'CAP', - MsgTok.SERVER, - ((MsgTok.NICKNAME, 'setattr_db.users.me:nick'), + _MsgTok.SERVER, + ((_MsgTok.NICKNAME, 'setattr_db.users.me:nick'), ('LS', ':subverb'), - (MsgTok.LIST, ':items'))), + (_MsgTok.LIST, ':items'))), _MsgParseExpectation( 'CAP', - MsgTok.SERVER, - ((MsgTok.NICKNAME, 'setattr_db.users.me:nick'), + _MsgTok.SERVER, + ((_MsgTok.NICKNAME, 'setattr_db.users.me:nick'), ('LS', ':subverb'), ('*', ':tbc'), - (MsgTok.LIST, ':items'))), + (_MsgTok.LIST, ':items'))), _MsgParseExpectation( 'CAP', - MsgTok.SERVER, + _MsgTok.SERVER, ('*', ('LIST', ':subverb'), - (MsgTok.LIST, ':items'))), + (_MsgTok.LIST, ':items'))), _MsgParseExpectation( 'CAP', - MsgTok.SERVER, + _MsgTok.SERVER, ('*', ('LIST', ':subverb'), ('*', ':tbc'), - (MsgTok.LIST, ':items'))), + (_MsgTok.LIST, ':items'))), _MsgParseExpectation( 'CAP', - MsgTok.SERVER, - ((MsgTok.NICKNAME, 'setattr_db.users.me:nick'), + _MsgTok.SERVER, + ((_MsgTok.NICKNAME, 'setattr_db.users.me:nick'), ('LIST', ':subverb'), - (MsgTok.LIST, ':items'))), + (_MsgTok.LIST, ':items'))), _MsgParseExpectation( 'CAP', - MsgTok.SERVER, - ((MsgTok.NICKNAME, 'setattr_db.users.me:nick'), + _MsgTok.SERVER, + ((_MsgTok.NICKNAME, 'setattr_db.users.me:nick'), ('LIST', ':subverb'), ('*', ':tbc'), - (MsgTok.LIST, ':items'))), + (_MsgTok.LIST, ':items'))), # nickname management _MsgParseExpectation( '432', # ERR_ERRONEOUSNICKNAME - MsgTok.SERVER, + _MsgTok.SERVER, ('*', - MsgTok.NICKNAME, # no need to re-use the bad one - MsgTok.ANY)), # comment + _MsgTok.NICKNAME, # no need to re-use the bad one + _MsgTok.ANY)), # comment _MsgParseExpectation( '432', # ERR_ERRONEOUSNICKNAME - MsgTok.SERVER, - ((MsgTok.NICKNAME, 'setattr_db.users.me:nick'), - MsgTok.NICKNAME, # no need to re-use the bad one - MsgTok.ANY)), # comment + _MsgTok.SERVER, + ((_MsgTok.NICKNAME, 'setattr_db.users.me:nick'), + _MsgTok.NICKNAME, # no need to re-use the bad one + _MsgTok.ANY)), # comment _MsgParseExpectation( '433', # ERR_NICKNAMEINUSE - MsgTok.SERVER, + _MsgTok.SERVER, ('*', - (MsgTok.NICKNAME, ':used'), - MsgTok.ANY)), # comment + (_MsgTok.NICKNAME, ':used'), + _MsgTok.ANY)), # comment _MsgParseExpectation( '433', # ERR_NICKNAMEINUSE - MsgTok.SERVER, - (MsgTok.NICKNAME, # we rather go for incrementation - (MsgTok.NICKNAME, ':used'), - MsgTok.ANY)), # comment + _MsgTok.SERVER, + (_MsgTok.NICKNAME, # we rather go for incrementation + (_MsgTok.NICKNAME, ':used'), + _MsgTok.ANY)), # comment _MsgParseExpectation( 'NICK', - (MsgTok.NICK_USER_HOST, ':named'), - ((MsgTok.NICKNAME, ':nick'),)), + (_MsgTok.NICK_USER_HOST, ':named'), + ((_MsgTok.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'))), + _MsgTok.SERVER, + ((_MsgTok.NICKNAME, 'setattr_db.users.me:nick'), + (_MsgTok.CHANNEL, ':CHAN'), + (_MsgTok.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, 'setattr_db.channels.CHAN.topic:who'), - (MsgTok.ANY, ':timestamp'))), + _MsgTok.SERVER, + ((_MsgTok.NICKNAME, 'setattr_db.users.me:nick'), + (_MsgTok.CHANNEL, ':CHAN'), + (_MsgTok.NICK_USER_HOST, 'setattr_db.channels.CHAN.topic:who'), + (_MsgTok.ANY, ':timestamp'))), _MsgParseExpectation( '353', # RPL_NAMREPLY - MsgTok.SERVER, - ((MsgTok.NICKNAME, 'setattr_db.users.me:nick'), + _MsgTok.SERVER, + ((_MsgTok.NICKNAME, 'setattr_db.users.me:nick'), '@', - (MsgTok.CHANNEL, ':channel'), - (MsgTok.LIST, ':names'))), + (_MsgTok.CHANNEL, ':channel'), + (_MsgTok.LIST, ':names'))), _MsgParseExpectation( '353', # RPL_NAMREPLY - MsgTok.SERVER, - ((MsgTok.NICKNAME, 'setattr_db.users.me:nick'), + _MsgTok.SERVER, + ((_MsgTok.NICKNAME, 'setattr_db.users.me:nick'), '=', - (MsgTok.CHANNEL, ':channel'), - (MsgTok.LIST, ':names'))), + (_MsgTok.CHANNEL, ':channel'), + (_MsgTok.LIST, ':names'))), _MsgParseExpectation( '366', # RPL_ENDOFNAMES - MsgTok.SERVER, - ((MsgTok.NICKNAME, 'setattr_db.users.me:nick'), - (MsgTok.CHANNEL, ':CHAN'), - MsgTok.ANY), # comment + _MsgTok.SERVER, + ((_MsgTok.NICKNAME, 'setattr_db.users.me:nick'), + (_MsgTok.CHANNEL, ':CHAN'), + _MsgTok.ANY), # comment bonus_tasks=('do_db.channels.CHAN.user_ids:complete',)), _MsgParseExpectation( 'JOIN', - (MsgTok.NICK_USER_HOST, ':joiner'), - ((MsgTok.CHANNEL, ':channel'),)), + (_MsgTok.NICK_USER_HOST, ':joiner'), + ((_MsgTok.CHANNEL, ':channel'),)), _MsgParseExpectation( 'PART', - (MsgTok.NICK_USER_HOST, ':parter'), - ((MsgTok.CHANNEL, ':channel'),)), + (_MsgTok.NICK_USER_HOST, ':parter'), + ((_MsgTok.CHANNEL, ':channel'),)), _MsgParseExpectation( 'PART', - (MsgTok.NICK_USER_HOST, ':parter'), - ((MsgTok.CHANNEL, ':channel'), - (MsgTok.ANY, ':message'))), - - _MsgParseExpectation( - 'TOPIC', - (MsgTok.NICK_USER_HOST, 'setattr_db.channels.CHAN.topic:who'), - ((MsgTok.CHANNEL, ':CHAN'), - (MsgTok.ANY, 'setattr_db.channels.CHAN.topic:what'))), + (_MsgTok.NICK_USER_HOST, ':parter'), + ((_MsgTok.CHANNEL, ':channel'), + (_MsgTok.ANY, ':message'))), # messaging _MsgParseExpectation( '401', # ERR_NOSUCKNICK - MsgTok.SERVER, - ((MsgTok.NICKNAME, 'setattr_db.users.me:nick'), - (MsgTok.NICKNAME, ':target'), - MsgTok.ANY)), # comment + _MsgTok.SERVER, + ((_MsgTok.NICKNAME, 'setattr_db.users.me:nick'), + (_MsgTok.NICKNAME, ':target'), + _MsgTok.ANY)), # comment _MsgParseExpectation( 'NOTICE', - MsgTok.SERVER, + _MsgTok.SERVER, ('*', - (MsgTok.ANY, ':message'))), + (_MsgTok.ANY, ':message'))), _MsgParseExpectation( 'NOTICE', - MsgTok.SERVER, - ((MsgTok.NICKNAME, 'setattr_db.users.me:nick'), - (MsgTok.ANY, ':message'))), + _MsgTok.SERVER, + ((_MsgTok.NICKNAME, 'setattr_db.users.me:nick'), + (_MsgTok.ANY, ':message'))), _MsgParseExpectation( 'NOTICE', - (MsgTok.NICK_USER_HOST, 'set_user:sender'), - ((MsgTok.NICKNAME, 'setattr_db.users.me:nick'), - (MsgTok.ANY, ':message'))), + (_MsgTok.NICK_USER_HOST, 'set_user:sender'), + ((_MsgTok.NICKNAME, 'setattr_db.users.me:nick'), + (_MsgTok.ANY, ':message'))), _MsgParseExpectation( 'NOTICE', - (MsgTok.NICK_USER_HOST, 'set_user:sender'), - ((MsgTok.CHANNEL, ':channel'), - (MsgTok.ANY, ':message'))), + (_MsgTok.NICK_USER_HOST, 'set_user:sender'), + ((_MsgTok.CHANNEL, ':channel'), + (_MsgTok.ANY, ':message'))), _MsgParseExpectation( 'PRIVMSG', - (MsgTok.NICK_USER_HOST, 'set_user:sender'), - ((MsgTok.NICKNAME, 'setattr_db.users.me:nick'), - (MsgTok.ANY, ':message'))), + (_MsgTok.NICK_USER_HOST, 'set_user:sender'), + ((_MsgTok.NICKNAME, 'setattr_db.users.me:nick'), + (_MsgTok.ANY, ':message'))), _MsgParseExpectation( 'PRIVMSG', - (MsgTok.NICK_USER_HOST, 'set_user:sender'), - ((MsgTok.CHANNEL, ':channel'), - (MsgTok.ANY, ':message'))), + (_MsgTok.NICK_USER_HOST, 'set_user:sender'), + ((_MsgTok.CHANNEL, ':channel'), + (_MsgTok.ANY, ':message'))), # misc. _MsgParseExpectation( 'ERROR', - MsgTok.NONE, - ((MsgTok.ANY, 'setattr_db:connection_state'),), + _MsgTok.NONE, + ((_MsgTok.ANY, 'setattr_db:connection_state'),), bonus_tasks=('do_:close',)), _MsgParseExpectation( 'MODE', - (MsgTok.NICK_USER_HOST, 'setattr_db.users.me:nickuserhost'), - ((MsgTok.NICKNAME, 'setattr_db.users.me:nick'), - (MsgTok.ANY, 'setattr_db:user_modes'))), + (_MsgTok.NICK_USER_HOST, 'setattr_db.users.me:nickuserhost'), + ((_MsgTok.NICKNAME, 'setattr_db.users.me:nick'), + (_MsgTok.ANY, 'setattr_db:user_modes'))), _MsgParseExpectation( 'MODE', - MsgTok.NICKNAME, - ((MsgTok.NICKNAME, 'setattr_db.users.me:nick'), - (MsgTok.ANY, 'setattr_db:user_modes'))), + _MsgTok.NICKNAME, + ((_MsgTok.NICKNAME, 'setattr_db.users.me:nick'), + (_MsgTok.ANY, 'setattr_db:user_modes'))), _MsgParseExpectation( 'PING', - MsgTok.NONE, - ((MsgTok.ANY, ':reply'),)), + _MsgTok.NONE, + ((_MsgTok.ANY, ':reply'),)), + + _MsgParseExpectation( + 'TOPIC', + (_MsgTok.NICK_USER_HOST, 'setattr_db.channels.CHAN.topic:who'), + ((_MsgTok.CHANNEL, ':CHAN'), + (_MsgTok.ANY, 'setattr_db.channels.CHAN.topic:what'))), _MsgParseExpectation( 'QUIT', - (MsgTok.NICK_USER_HOST, ':quitter'), - ((MsgTok.ANY, ':message'),)), + (_MsgTok.NICK_USER_HOST, ':quitter'), + ((_MsgTok.ANY, ':message'),)), ] -- 2.30.2