From: Christian Heller Date: Sun, 17 Aug 2025 15:51:33 +0000 (+0200) Subject: Refactor IrcMessage handling. X-Git-Url: https://plomlompom.com/repos/blog?a=commitdiff_plain;h=6d227583e8664f6124b6613f80035f4d5e9c7572;p=ircplom Refactor IrcMessage handling. --- diff --git a/ircplom/client.py b/ircplom/client.py index 1a81847..b45dabe 100644 --- a/ircplom/client.py +++ b/ircplom/client.py @@ -35,6 +35,31 @@ _NUMERICS_TO_IGNORE = ( ) +class _IrcMsg(IrcMessage): + 'Extends IrcMessage with some conveniences.' + + def match(self, verb: str, len_params=1, len_is_min=False) -> bool: + 'Test .verb, len(.params).' + n_msg_params = len(self.params) + return (self.verb == verb + and (n_msg_params == len_params + or len_is_min and n_msg_params > len_params)) + + def nick_conforms(self, db: '_ClientDb') -> bool: + 'Test .nick_from_source == db.nickname.' + return self.nick_from_source == db.nickname + + def confirm_nick(self, db: '_ClientDb') -> None: + 'Assume .params[0] confirms nickname.' + db.nickname = self.params[0] + db.nickname_confirmed = True + + @property + def nick_from_source(self) -> str: + 'Parse .source into user nickname.' + return self.source.split('!')[0] + + class LogScope(Enum): 'Where log messages should go.' ALL = auto() @@ -84,7 +109,7 @@ class _IrcConnection(BaseIrcConnection, ClientIdMixin): def _make_recv_event(self, msg: IrcMessage) -> ClientEvent: return ClientEvent.affector('handle_msg', client_id=self.client_id - ).kw(msg=msg) + ).kw(msg=_IrcMsg.from_raw(msg.raw)) def _on_handled_loop_exception(self, e: IrcConnAbortException ) -> ClientEvent: @@ -377,6 +402,15 @@ class _ClientDb(_Db, IrcConnSetup): on_update=lambda k: self._on_update(name, k)) return self._channels[name] + def process_isupport(self, params: tuple[str, ...]) -> None: + 'Process 005 RPL_ISUPPORT params into dictionary updates.' + for param in params[1:-1]: + toks = param.split('=', maxsplit=1) + if toks[0][0] == '-': + del self.isupports[toks[0][1:]] + else: + self.isupports[toks[0]] = toks[1] if len(toks) > 1 else '' + class Client(ABC, ClientQueueMixin): 'Abstracts socket connection, loop over it, and handling messages from it.' @@ -462,92 +496,76 @@ class Client(ABC, ClientQueueMixin): self._log(f'connection broken: {e}', alert=True) self.close() - def handle_msg(self, msg: IrcMessage) -> None: + def handle_msg(self, msg: _IrcMsg) -> None: 'Log msg.raw, then process incoming msg into appropriate client steps.' self._log(msg.raw, scope=LogScope.RAW, out=False) if _NumericsToConfirmNickname.contain(msg.verb): - self._db.nickname = msg.params[0] - self._db.nickname_confirmed = True + msg.confirm_nick(self._db) if _NumericsToIgnore.contain(msg.verb): return - if msg.verb in {'JOIN', 'PART'} and len(msg.params) >= 1: - scope = LogScope.CHAT + if msg.match('005', 2, True): # RPL_ISUPPORT + self._db.process_isupport(msg.params[1:-1]) + elif msg.match('353', 4): # RPL_NAMREPLY + for user in msg.params[3].split(): + self._db.chan(msg.params[2]).append_completable('users', user) + elif msg.match('366', 3): # RPL_ENDOFNAMES + self._db.chan(msg.params[1]).declare_complete('users') + elif msg.match('372', 2): # RPL_MOTD + self._db.append_completable('motd', msg.params[1]) + elif msg.match('376', 2): # RPL_ENDOFMOTD + self._db.declare_complete('motd') + elif msg.match('396', 3): # RPL_VISIBLEHOST + # '@'-split because + # claims: " can also be in the form " + self._db.client_host = msg.params[1].split('@')[-1] + elif msg.match('903', 2): # RPL_SASLSUCESS + self._log('SASL auth succeeded') + self._caps.end_negotiation() + elif msg.match('904', 2): # ERR_SASLFAIL + self._log('SASL auth failed', alert=True) + self._caps.end_negotiation() + elif msg.match('AUTHENTICATE') and msg.params[0] == '+': + auth = b64encode((self._db.nickname + '\0' + + self._db.nickname + '\0' + + self._db.password + ).encode('utf-8')).decode('utf-8') + self.send(IrcMessage('AUTHENTICATE', (auth,))) + elif msg.match('CAP', len_is_min=True)\ + and self._caps.process_msg(msg.params[1:])\ + and self._db.caps.has('sasl')\ + and 'PLAIN' in self._db.caps['sasl'].data.split(','): + if self._db.password: + self._log('trying to authenticate via SASL/plain') + self.send(IrcMessage('AUTHENTICATE', ('PLAIN',))) + else: + self._caps.end_negotiation() + elif msg.match('ERROR'): + self.close() + elif msg.match('MODE', 2) and msg.params[0] == self._db.nickname: + self._db.user_modes = msg.params[1] + elif msg.match('NICK') and msg.nick_conforms(self._db): + msg.confirm_nick(self._db) + elif msg.match('NOTICE', 2) or msg.match('PRIVMSG', 2): + scope = LogScope.CHAT if '!' in msg.source else LogScope.SERVER + self._log(msg.params[-1], scope=scope, channel=msg.params[0], + out=False, as_notice=msg.verb == 'NOTICE', + sender=msg.nick_from_source) + elif msg.match('PART', len_is_min=True) or msg.match('JOIN'): channel = msg.params[0] log_msg = f'{msg.nick_from_source} {msg.verb.lower()}s {channel}' - match msg.verb: - case '005' if len(msg.params) > 2: # RPL_ISUPPORT - for param in msg.params[1:-1]: - toks = param.split('=', maxsplit=1) - if toks[0][0] == '-': - del self._db.isupports[toks[0][1:]] - else: - self._db.isupports[toks[0]] = ( - toks[1] if len(toks) > 1 else '') - case '353' if len(msg.params) == 4: # RPL_NAMREPLY - for user in msg.params[3].split(): - self._db.chan(msg.params[2] - ).append_completable('users', user) - case '366' if len(msg.params) == 3: # RPL_ENDOFNAMES - self._db.chan(msg.params[1]).declare_complete('users') - case '372' if len(msg.params) == 2: # RPL_MOTD - self._db.append_completable('motd', msg.params[1]) - case '376' if len(msg.params) == 2: # RPL_ENDOFMOTD - self._db.declare_complete('motd') - case '396' if len(msg.params) == 3: # RPL_VISIBLEHOST - # '@'-split because - # claims: " can also be in the form " - self._db.client_host = msg.params[1].split('@')[-1] - case '903' | '904' if len(msg.params) == 2: # RPL_SASLSUCESS - alert = msg.verb == '904' # ERR_SASLFAIL - self._log(f'SASL auth {"failed" if alert else "succeeded"}', - alert=alert) - self._caps.end_negotiation() - case 'AUTHENTICATE' if msg.params == ('+',): - auth = b64encode((self._db.nickname + '\0' + - self._db.nickname + '\0' + - self._db.password - ).encode('utf-8')).decode('utf-8') - self.send(IrcMessage('AUTHENTICATE', (auth,))) - case 'CAP' if len(msg.params) > 1: - if (self._caps.process_msg(msg.params[1:]) - and self._db.caps.has('sasl') - and (sasl_caps := self._db.caps['sasl']) - and ('PLAIN' in sasl_caps.data.split(','))): - if self._db.password: - self._log('trying to authenticate via SASL/plain') - self.send(IrcMessage('AUTHENTICATE', ('PLAIN',))) - else: - self._caps.end_negotiation() - case 'ERROR' if len(msg.params) == 1: - self.close() - case 'JOIN' if len(msg.params) == 1: - if msg.nick_from_source != self._db.nickname: - self._db.chan(channel).users.append(msg.nick_from_source) - self._log(log_msg, scope=scope, channel=channel) - case 'MODE' if (len(msg.params) == 2 - and msg.params[0] == self._db.nickname): - self._db.user_modes = msg.params[1] - case 'NICK' if (len(msg.params) == 1 - and msg.nick_from_source == self._db.nickname): - self._db.nickname = msg.params[0] - self._db.nickname_confirmed = True - case 'NOTICE' | 'PRIVMSG' if len(msg.params) == 2: - scope = LogScope.CHAT if '!' in msg.source else LogScope.SERVER - self._log(msg.params[-1], scope=scope, channel=msg.params[0], - out=False, as_notice=msg.verb == 'NOTICE', - sender=msg.nick_from_source) - case 'PART' if len(msg.params) >= 1: - if len(msg.params) > 1: - log_msg += f': {msg.params[1]}' - self._log(log_msg, scope=scope, channel=channel) - if msg.nick_from_source == self._db.nickname: - self._db.del_chan(channel) - else: - self._db.chan(channel).users.remove(msg.nick_from_source) - case 'PING' if len(msg.params) == 1: - self.send(IrcMessage(verb='PONG', params=(msg.params[0],))) - case _: - self._log(f'PLEASE IMPLEMENT HANDLER FOR: {msg.raw}') + if msg.match('PART', 2, True): + log_msg += f': {msg.params[1]}' + self._log(log_msg, scope=LogScope.CHAT, channel=channel) + if msg.verb == 'JOIN': + self._db.chan(channel).users.append(msg.nick_from_source) + elif msg.nick_conforms(self._db): + self._db.del_chan(channel) + else: + self._db.chan(channel).users.remove(msg.nick_from_source) + elif msg.match('PING'): + self.send(IrcMessage(verb='PONG', params=(msg.params[0],))) + else: + self._log(f'PLEASE IMPLEMENT HANDLER FOR: {msg.raw}') @dataclass diff --git a/ircplom/irc_conn.py b/ircplom/irc_conn.py index 197c733..5e09c1a 100644 --- a/ircplom/irc_conn.py +++ b/ircplom/irc_conn.py @@ -98,11 +98,6 @@ class IrcMessage: msg._raw = raw_msg return msg - @property - def nick_from_source(self) -> str: - 'Parse .source into user nickname.' - return self.source.split('!')[0] - @property def raw(self) -> str: 'Return raw message code – create from known fields if necessary.'