home · contact · privacy
In _MsgParseExpectation explicitly label under what key a value to return by.
authorChristian Heller <c.heller@plomlompom.de>
Wed, 20 Aug 2025 19:42:15 +0000 (21:42 +0200)
committerChristian Heller <c.heller@plomlompom.de>
Wed, 20 Aug 2025 19:42:15 +0000 (21:42 +0200)
ircplom/client.py

index db728003c0c2248e2801858ac6f386d5e4ccf30a..dd448f13fc409515b0467d54fe126b5668d13847 100644 (file)
@@ -44,74 +44,129 @@ class _MsgTok(Enum):
     CHANNEL = auto()
     LIST = auto()
     NICKNAME = auto()
-    NICKNAME_ME = auto()
     NONE = auto()
     SERVER = auto()
     USER_ADDRESS = auto()
-    USER_ADDRESS_ME = auto()
+
+
+_MsgTokGuide = str | _MsgTok | tuple[_MsgTok, str]
 
 
 class _MsgParseExpectation(NamedTuple):
-    source: _MsgTok
+    source: _MsgTokGuide
     verb: str
+    params: tuple[_MsgTokGuide, ...] = tuple()
     len_min_params: int = 0
     len_max_params: int = 0
-    params: tuple[str | _MsgTok, ...] = tuple()
 
 
 _EXPECTATIONS: tuple[_MsgParseExpectation, ...] = (
-   _MsgParseExpectation(_MsgTok.SERVER, '005', 3, 15),
-   _MsgParseExpectation(_MsgTok.SERVER, '353',
-                        params=(_MsgTok.NICKNAME_ME, '=', _MsgTok.CHANNEL,
-                                _MsgTok.LIST,)),
-   _MsgParseExpectation(_MsgTok.SERVER, '366',
-                        params=(_MsgTok.NICKNAME_ME, _MsgTok.CHANNEL,
-                                _MsgTok.ANY)),
-   _MsgParseExpectation(_MsgTok.SERVER, '372',
-                        params=(_MsgTok.NICKNAME_ME, _MsgTok.ANY)),
-   _MsgParseExpectation(_MsgTok.SERVER, '376',
-                        params=(_MsgTok.NICKNAME_ME, _MsgTok.ANY)),
-   _MsgParseExpectation(_MsgTok.SERVER, '396', 3),
-   _MsgParseExpectation(_MsgTok.SERVER, '401', 3),
-   _MsgParseExpectation(_MsgTok.SERVER, '432', 3),
-   _MsgParseExpectation(_MsgTok.SERVER, '433', 3),
-   _MsgParseExpectation(_MsgTok.SERVER, '900', 4),
-   _MsgParseExpectation(_MsgTok.SERVER, '903',
-                        params=(_MsgTok.NICKNAME_ME, _MsgTok.ANY)),
-   _MsgParseExpectation(_MsgTok.SERVER, '904',
-                        params=(_MsgTok.NICKNAME_ME, _MsgTok.ANY)),
-   _MsgParseExpectation(_MsgTok.NONE, 'AUTHENTICATE', params=('+',)),
-   _MsgParseExpectation(_MsgTok.SERVER, 'CAP', 3, 15),
-   _MsgParseExpectation(_MsgTok.NONE, 'ERROR', params=(_MsgTok.ANY,)),
-   _MsgParseExpectation(_MsgTok.USER_ADDRESS_ME, 'JOIN',
-                        params=(_MsgTok.CHANNEL,)),
-   _MsgParseExpectation(_MsgTok.USER_ADDRESS, 'JOIN',
-                        params=(_MsgTok.CHANNEL,)),
-   _MsgParseExpectation(_MsgTok.NICKNAME_ME, 'MODE',
-                        params=(_MsgTok.NICKNAME_ME, _MsgTok.ANY)),
-   _MsgParseExpectation(_MsgTok.USER_ADDRESS_ME, 'MODE',
-                        params=(_MsgTok.NICKNAME_ME, _MsgTok.ANY)),
-   _MsgParseExpectation(_MsgTok.USER_ADDRESS_ME, 'NICK',
-                        params=(_MsgTok.NICKNAME,)),
-   _MsgParseExpectation(_MsgTok.USER_ADDRESS, 'NICK',
-                        params=(_MsgTok.NICKNAME,)),
-   _MsgParseExpectation(_MsgTok.SERVER, 'NOTICE', params=('*', _MsgTok.ANY)),
-   _MsgParseExpectation(_MsgTok.SERVER, 'NOTICE',
-                        params=(_MsgTok.NICKNAME, _MsgTok.ANY)),
-   _MsgParseExpectation(_MsgTok.USER_ADDRESS, 'NOTICE',
-                        params=(_MsgTok.NICKNAME_ME, _MsgTok.ANY)),
-   _MsgParseExpectation(_MsgTok.USER_ADDRESS_ME, 'PART',
-                        params=(_MsgTok.CHANNEL,)),
-   _MsgParseExpectation(_MsgTok.USER_ADDRESS, 'PART',
-                        params=(_MsgTok.CHANNEL,)),
-   _MsgParseExpectation(_MsgTok.USER_ADDRESS, 'PART',
-                        params=(_MsgTok.CHANNEL, _MsgTok.ANY)),
-   _MsgParseExpectation(_MsgTok.NONE, 'PING', params=(_MsgTok.ANY,)),
-   _MsgParseExpectation(_MsgTok.USER_ADDRESS, 'PRIVMSG',
-                        params=(_MsgTok.NICKNAME_ME, _MsgTok.ANY)),
-   _MsgParseExpectation(_MsgTok.USER_ADDRESS, 'PRIVMSG',
-                        params=(_MsgTok.CHANNEL, _MsgTok.ANY)),
-   _MsgParseExpectation(_MsgTok.USER_ADDRESS, 'QUIT', params=(_MsgTok.ANY,)),
+   _MsgParseExpectation(_MsgTok.SERVER, '005', tuple(), 3, 15),
+
+   _MsgParseExpectation(_MsgTok.SERVER,
+                        '353',
+                        (_MsgTok.NICKNAME,
+                         '=',
+                         (_MsgTok.CHANNEL, 'channel'),
+                         (_MsgTok.LIST, 'names'))),
+   _MsgParseExpectation(_MsgTok.SERVER,
+                        '366',
+                        (_MsgTok.NICKNAME,
+                         (_MsgTok.CHANNEL, 'channel'),
+                         _MsgTok.ANY)),
+
+   _MsgParseExpectation(_MsgTok.SERVER,
+                        '372',
+                        (_MsgTok.NICKNAME,
+                         (_MsgTok.ANY, 'line'))),
+   _MsgParseExpectation(_MsgTok.SERVER,
+                        '376',
+                        (_MsgTok.NICKNAME,
+                         _MsgTok.ANY)),
+
+   _MsgParseExpectation(_MsgTok.SERVER, '396', tuple(), 3),
+   _MsgParseExpectation(_MsgTok.SERVER, '401', tuple(), 3),
+   _MsgParseExpectation(_MsgTok.SERVER, '432', tuple(), 3),
+   _MsgParseExpectation(_MsgTok.SERVER, '433', tuple(), 3),
+   _MsgParseExpectation(_MsgTok.SERVER, '900', tuple(), 4),
+
+   _MsgParseExpectation(_MsgTok.SERVER,
+                        '903',
+                        (_MsgTok.NICKNAME,
+                         (_MsgTok.ANY, 'result'))),
+   _MsgParseExpectation(_MsgTok.SERVER,
+                        '904',
+                        (_MsgTok.NICKNAME,
+                         (_MsgTok.ANY, 'result'))),
+
+   _MsgParseExpectation(_MsgTok.NONE,
+                        'AUTHENTICATE',
+                        ('+',)),
+
+   _MsgParseExpectation(_MsgTok.SERVER, 'CAP', tuple(), 3, 15),
+
+   _MsgParseExpectation(_MsgTok.NONE,
+                        'ERROR',
+                        ((_MsgTok.ANY, 'reason'),)),
+
+   _MsgParseExpectation((_MsgTok.USER_ADDRESS, 'joiner'),
+                        'JOIN',
+                        ((_MsgTok.CHANNEL, 'channel'),)),
+
+   _MsgParseExpectation(_MsgTok.NICKNAME,
+                        'MODE',
+                        (_MsgTok.NICKNAME,
+                         (_MsgTok.ANY, 'mode'))),
+   _MsgParseExpectation(_MsgTok.USER_ADDRESS,
+                        'MODE',
+                        (_MsgTok.NICKNAME,
+                         (_MsgTok.ANY, 'mode'))),
+
+   _MsgParseExpectation((_MsgTok.USER_ADDRESS, 'named'),
+                        'NICK',
+                        ((_MsgTok.NICKNAME, 'nickname'),)),
+
+   _MsgParseExpectation(_MsgTok.SERVER,
+                        'NOTICE',
+                        ('*',
+                         (_MsgTok.ANY, 'message'))),
+   _MsgParseExpectation(_MsgTok.SERVER,
+                        'NOTICE',
+                        ((_MsgTok.NICKNAME, 'nickname'),
+                         (_MsgTok.ANY, 'message'))),
+   _MsgParseExpectation((_MsgTok.USER_ADDRESS, 'sender'),
+                        'NOTICE',
+                        ((_MsgTok.NICKNAME, 'nickname'),
+                         (_MsgTok.ANY, 'message'))),
+   _MsgParseExpectation((_MsgTok.USER_ADDRESS, 'sender'),
+                        'NOTICE',
+                        ((_MsgTok.CHANNEL, 'channel'),
+                         (_MsgTok.ANY, 'message'))),
+
+   _MsgParseExpectation((_MsgTok.USER_ADDRESS, 'parter'),
+                        'PART',
+                        ((_MsgTok.CHANNEL, 'channel'),)),
+   _MsgParseExpectation((_MsgTok.USER_ADDRESS, 'parter'),
+                        'PART',
+                        ((_MsgTok.CHANNEL, 'channel'),
+                         (_MsgTok.ANY, 'reason'))),
+
+   _MsgParseExpectation(_MsgTok.NONE,
+                        'PING',
+                        ((_MsgTok.ANY, 'reply'),)),
+
+   _MsgParseExpectation((_MsgTok.USER_ADDRESS, 'sender'),
+                        'PRIVMSG',
+                        ((_MsgTok.NICKNAME, 'nickname'),
+                         (_MsgTok.ANY, 'message'))),
+   _MsgParseExpectation((_MsgTok.USER_ADDRESS, 'sender'),
+                        'PRIVMSG',
+                        ((_MsgTok.CHANNEL, 'channel'),
+                         (_MsgTok.ANY, 'message'))),
+
+   _MsgParseExpectation((_MsgTok.USER_ADDRESS, 'quitter'),
+                        'QUIT',
+                        ((_MsgTok.ANY, 'message'),)),
 )
 
 
@@ -601,7 +656,7 @@ class Client(ABC, ClientQueueMixin):
                     continue
                 if (not ex.len_max_params) and len_p != ex.len_min_params:
                     continue
-            to_return: dict[str, Any] = {'': ''}
+            to_return: dict[str, Any] = {'': ''}  # non-emtpy so boolish True
             ex_tok_fields = tuple([ex.source] + list(ex.params))
             msg_tok_fields = tuple([msg.source] + list(msg.params))
             if ex.params and len(ex_tok_fields) != len(msg_tok_fields):
@@ -609,38 +664,34 @@ class Client(ABC, ClientQueueMixin):
             passing = True
             for idx, ex_tok in enumerate(ex_tok_fields):
                 passing = False
-                msg_tok = msg_tok_fields[idx]
+                ex_tok, key = ((ex_tok[0], ex_tok[1])
+                               if isinstance(ex_tok, tuple) else (ex_tok, ''))
+                val_to_ret: str | tuple[str, ...] | dict[str, str | _ChannelDb]
+                val_to_ret = msg_tok = msg_tok_fields[idx]
                 if ex_tok is _MsgTok.NONE and msg_tok != '':
                     break
                 if ex_tok is _MsgTok.SERVER\
                         and ('.' not in msg_tok or set('@!') & set(msg_tok)):
                     break
-                key_nick = 'sender' if not idx else 'nickname'
                 if ex_tok is _MsgTok.CHANNEL:
                     if msg_tok[0] != '#':
                         break
-                    to_return |= {'ch_name': msg_tok,
-                                  'channel': self._db.chan(msg_tok)}
-                elif ex_tok in {_MsgTok.NICKNAME, _MsgTok.NICKNAME_ME}:
+                    val_to_ret = {'id': msg_tok, 'db': self._db.chan(msg_tok)}
+                elif ex_tok is _MsgTok.NICKNAME:
                     if msg_tok[0] in '~&@%+# *':
                         break
-                    to_return[key_nick] = msg_tok
-                elif ex_tok in {_MsgTok.USER_ADDRESS, _MsgTok.USER_ADDRESS_ME}:
+                elif ex_tok is _MsgTok.USER_ADDRESS:
                     toks = msg_tok.split('!')
                     if len(toks) != 2:
                         break
                     toks = toks[0:1] + toks[1].split('@')
                     if len(toks) != 3:
                         break
-                    to_return[key_nick] = toks[0]
-                elif ex_tok is _MsgTok.ANY:
-                    to_return['any'] = msg_tok
+                    val_to_ret = tuple(toks)
                 elif ex_tok is _MsgTok.LIST:
-                    to_return['list'] = msg_tok.split()
-                if ex_tok in {_MsgTok.NICKNAME_ME, _MsgTok.USER_ADDRESS_ME}:
-                    if to_return[key_nick] != self._db.nickname:
-                        break
-                    to_return[f'{key_nick}_me'] = to_return[key_nick]
+                    val_to_ret = tuple(msg_tok.split())
+                if key:
+                    to_return[key] = val_to_ret
                 passing = True
             if passing:
                 return to_return
@@ -655,15 +706,19 @@ class Client(ABC, ClientQueueMixin):
             return
         if self._match_msg(msg, '005'):  # RPL_ISUPPORT
             self._db.process_isupport(msg.params[1:-1])
+
         elif (ret := self._match_msg(msg, '353')):  # RPL_NAMREPLY
-            for usr in ret['list']:
-                ret['channel'].append_completable('users', usr.lstrip('~&@%+'))
-        elif self._match_msg(msg, '366'):  # RPL_ENDOFNAMES
-            self._db.chan(msg.params[1]).declare_complete('users')
+            for name in ret['names']:
+                ret['channel']['db'].append_completable('users',
+                                                        name.lstrip('~&@%+'))
+        elif (ret := self._match_msg(msg, '366')):  # RPL_ENDOFNAMES
+            ret['channel']['db'].declare_complete('users')
+
         elif (ret := self._match_msg(msg, '372')):  # RPL_MOTD
-            self._db.append_completable('motd', ret['any'])
+            self._db.append_completable('motd', ret['line'])
         elif self._match_msg(msg, '376'):  # RPL_ENDOFMOTD
             self._db.declare_complete('motd')
+
         elif self._match_msg(msg, '396'):  # RPL_VISIBLEHOST
             # '@'-split because <https://defs.ircdocs.horse/defs/numerics>
             # claims: "<hostname> can also be in the form <user@hostname>"
@@ -686,16 +741,19 @@ class Client(ABC, ClientQueueMixin):
             self._db.nickname, remainder = msg.params[1].split('!', maxsplit=1)
             self._db.username, self._db.client_host = remainder.split('@')
             self._db.sasl_account = msg.params[2]
+
         elif ((ret := self._match_msg(msg, '903'))  # RPL_SASLSUCCESS
                 or (ret := self._match_msg(msg, '904'))):  # ERR_SASLFAIL
-            self._db.sasl_auth_state = ret['any']
+            self._db.sasl_auth_state = ret['result']
             self._caps.end_negotiation()
+
         elif self._match_msg(msg, '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 self._match_msg(msg, 'CAP'):
             if (self._caps.process_msg(msg.params[1:])
                     and self._db.caps.has('sasl')
@@ -705,51 +763,64 @@ class Client(ABC, ClientQueueMixin):
                     self.send(IrcMessage('AUTHENTICATE', ('PLAIN',)))
                 else:
                     self._caps.end_negotiation()
+
         elif (ret := self._match_msg(msg, 'ERROR')):
-            self._db.connection_state = ret['any']
+            self._db.connection_state = ret['reason']
             self.close()
+
         elif (ret := self._match_msg(msg, 'JOIN')):
-            log_msg = f'{ret["sender"]} {msg.verb.lower()}s {ret["ch_name"]}'
-            self._log(log_msg, scope=LogScope.CHAT, target=ret['ch_name'])
-            if 'sender_me' not in ret:
-                ret['channel'].append_completable('users', ret['sender'],
-                                                  stay_complete=True)
+            self._log(f'{ret["joiner"][0]} {msg.verb.lower()}s '
+                      + f'{ret["channel"]["id"]}',
+                      scope=LogScope.CHAT, target=ret['channel']['id'])
+            if ret['joiner'][0] != self._db.nickname:
+                ret['channel']['db'].append_completable('users',
+                                                        ret['joiner'][0], True)
+
         elif (ret := self._match_msg(msg, 'MODE')):
-            self._db.user_modes = ret['any']
+            self._db.user_modes = ret['mode']
+
         elif (ret := self._match_msg(msg, 'NICK')):
-            if 'sender_me' in ret:
+            if ret['named'][0] == self._db.nickname:
                 self.set_nick(ret['nickname'], confirmed=True)
             else:
-                for nom, chan in self._db.chans_of_user(ret['sender']).items():
-                    chan.remove_completable('users', ret['sender'], True)
-                    chan.append_completable('users', ret['nickname'], True)
-                    self._log(f'{ret["sender"]} becomes {ret["nickname"]}',
-                              scope=LogScope.CHAT, target=nom)
+                for id_, ch in self._db.chans_of_user(ret['named'][0]).items():
+                    ch.remove_completable('users', ret['named'][0], True)
+                    ch.append_completable('users', ret['nickname'], True)
+                    self._log(f'{ret["named"][0]} becomes {ret["nickname"]}',
+                              scope=LogScope.CHAT, target=id_)
+
         elif (ret := self._match_msg(msg, 'NOTICE'))\
                 or (ret := self._match_msg(msg, 'PRIVMSG')):
-            kw = {'sender': ret['sender'], 'scope': LogScope.CHAT,
-                  'target': ret.get('ch_name', ret['sender']),
-                  } if 'sender' in ret else {}
-            if msg.verb == 'NOTICE':
-                if 'nickname' in ret and 'nickname_me' not in ret:
-                    self.set_nick(ret['nickname'], confirmed=True)
-                kw |= {'as_notice': True}
-            self._log(ret['any'], out=False, **kw)
+            if 'nickname' in ret and ret['nickname'] != self._db.nickname:
+                self.set_nick(ret['nickname'], confirmed=True)
+            kw: dict[str, bool | str | LogScope] = {
+                    'as_notice': msg.verb == 'NOTICE'}
+            if 'sender' in ret:  # not just server message
+                kw |= {'sender': ret['sender'][0], 'scope': LogScope.CHAT,
+                       'target': (ret['sender'][0] if 'nickname' in ret
+                                  else ret['channel']['id'])}
+            self._log(ret['message'], out=False, **kw)
+
         elif (ret := self._match_msg(msg, 'PART')):
-            log_msg = f'{ret["sender"]} {msg.verb.lower()}s {ret["ch_name"]}'
-            log_msg += f': {ret["any"]}' if 'any' in ret else ''
-            self._log(log_msg, scope=LogScope.CHAT, target=ret['ch_name'])
-            if 'sender_me' in ret:
+            reason = f': ret["reason"]' if 'reason' in ret else ''
+            self._log(f'{ret["parter"][0]} {msg.verb.lower()}s '
+                      + f'{ret["channel"]["id"]}{reason}',
+                      scope=LogScope.CHAT, target=ret['channel']['id'])
+            if ret['parter'][0] == self._db.nickname:
                 self._db.del_chan(ret['ch_name'])
             else:
-                ret['channel'].remove_completable('users', ret['sender'], True)
+                ret['channel']['db'].remove_completable('users',
+                                                        ret['parter'][0], True)
+
         elif (ret := self._match_msg(msg, 'PING')):
-            self.send(IrcMessage(verb='PONG', params=(ret['any'],)))
+            self.send(IrcMessage(verb='PONG', params=(ret['reply'],)))
+
         elif (ret := self._match_msg(msg, 'QUIT')):
-            for nom, chan in self._db.chans_of_user(ret['sender']).items():
-                chan.remove_completable('users', ret['sender'], True)
-                self._log(f'{ret["sender"]} quits: {ret["any"]}',
-                          LogScope.CHAT, target=nom)
+            for id_, chan in self._db.chans_of_user(ret['quitter'][0]).items():
+                chan.remove_completable('users', ret['quitter'][0], True)
+                self._log(f'{ret["quitter"][0]} quits: {ret["message"]}',
+                          LogScope.CHAT, target=id_)
+
         else:
             self._log(f'PLEASE IMPLEMENT HANDLER FOR: {msg.raw}')