From: Christian Heller Date: Mon, 28 Jul 2025 02:56:07 +0000 (+0200) Subject: Further re-organize CAP LS/LIST parsing. X-Git-Url: https://plomlompom.com/repos/booking/%7B%7Bdb.prefix%7D%7D/%7B%7B%20web_path%20%7D%7D/%7B%7Bprefix%7D%7D/todos?a=commitdiff_plain;ds=inline;p=ircplom Further re-organize CAP LS/LIST parsing. --- diff --git a/ircplom/irc_conn.py b/ircplom/irc_conn.py index ef7728d..3146693 100644 --- a/ircplom/irc_conn.py +++ b/ircplom/irc_conn.py @@ -1,6 +1,7 @@ 'IRC server connection management.' # built-ins from abc import ABC, abstractmethod +from dataclasses import dataclass from getpass import getuser from socket import socket, gaierror as socket_gaierror from threading import Thread @@ -152,7 +153,7 @@ class _ConnectedEvent(ClientEvent): def affect(self, target: 'Client') -> None: target.log(msg='# connected to server', chat=CHAT_GLOB) - target.send(IrcMessage(verb='CAP', params=('LS', '302'))) + target.try_send_cap('LS', ('302',)) target.send(IrcMessage(verb='USER', params=(getuser(), '0', '*', target.realname))) target.send(IrcMessage(verb='NICK', params=(target.nickname,))) @@ -190,6 +191,21 @@ class ClientQueueMixin(QueueMixin): **kwargs)) +@dataclass +class ServerCapability: + 'Store data collectable via CAPS LS/LIST/NEW.' + enabled: bool + data: str + + def str_for_log(self, name: str) -> str: + 'Optimized for Client.log per-line listing.' + listing = '+' if self.enabled else '-' + listing += f' {name}' + if self.data: + listing += f' ({self.data})' + return listing + + class Client(ABC, ClientQueueMixin): 'Abstracts socket connection, loop over it, and handling messages from it.' nick_confirmed: bool @@ -201,8 +217,8 @@ class Client(ABC, ClientQueueMixin): self._hostname = hostname self._socket: Optional[socket] = None self._recv_loop: Optional[Loop] = None - self.cap_neg_state: set[str] = set() - self.caps: dict[str, tuple[bool, str]] = {} + self._cap_neg_states: dict[str, bool] = {} + self.caps: dict[str, ServerCapability] = {} self.id_ = uuid4() self.assumed_open = False self.realname = realname @@ -232,31 +248,47 @@ class Client(ABC, ClientQueueMixin): Thread(target=connect, daemon=True, args=(self,)).start() + def cap_neg_done(self, negotiation_step: str) -> bool: + 'Whether negotiation_step is registered as finished.' + return self._cap_neg_states.get(negotiation_step, False) + + def cap_neg(self, negotiation_step: str) -> bool: + 'Whether negotiation_step is registered at all (started or finished).' + return negotiation_step in self._cap_neg_states + + def cap_neg_set(self, negotiation_step: str, done: bool = False) -> None: + 'Declare negotiation_step started, or (if done) finished.' + self._cap_neg_states[negotiation_step] = done + + def try_send_cap(self, *params, key_fused: bool = False) -> None: + 'Run CAP command with params, handle cap neg. state.' + neg_state_key = ':'.join(params) if key_fused else params[0] + if self.cap_neg(neg_state_key): + return + self.send(IrcMessage(verb='CAP', params=params)) + self.cap_neg_set(neg_state_key) + def collect_caps(self, params: tuple[str, ...]) -> None: 'Record available and enabled server capabilities.' verb = params[0] items = params[-1].strip().split() is_final_line = params[1] != '*' - doneness = f'{verb} done' - waiting = f'{verb} wait' - if doneness in self.cap_neg_state: + if self.cap_neg_done(verb): if verb == 'LS': self.caps.clear() else: - for name in self.caps: - self.caps[name] = (False, self.caps[name][1]) - self.cap_neg_state.remove(doneness) - if waiting not in self.cap_neg_state: - self.cap_neg_state.add(waiting) + for cap in self.caps.values(): + cap.enabled = False + self.cap_neg_set(verb) for item in items: if verb == 'LS': splitted = item.split('=', maxsplit=1) - self.caps[splitted[0]] = (False, ''.join(splitted[1:])) + self.caps[splitted[0]] = ServerCapability( + enabled=False, data=''.join(splitted[1:])) else: - self.caps[item] = (True, self.caps[item][1]) + self.caps[item].enabled = True if is_final_line: - self.cap_neg_state.remove(waiting) - self.cap_neg_state.add(doneness) + self.cap_neg_set(verb, done=True) @abstractmethod def log(self, msg: str, chat: str = '') -> None: @@ -351,14 +383,21 @@ class _RecvEvent(ClientEvent, PayloadMixin): elif msg.verb == 'CAP': if msg.params[1] in {'LS', 'LIST'}: target.collect_caps(msg.params[1:]) - if ('LIST done' in target.cap_neg_state - and 'listed' not in target.cap_neg_state): - target.send(IrcMessage(verb='CAP', params=('END',))) - target.log('# available server capabilities (enabled: "+"):') - for cap_name, config in target.caps.items(): - target.log(f'# {"+" if config[0] else "-"} {cap_name}' - + (f' ({config[1]})' if config[1] else '')) - target.cap_neg_state.add('listed') - elif ('LS done' in target.cap_neg_state - and 'LIST wait' not in target.cap_neg_state): - target.send(IrcMessage(verb='CAP', params=('LIST',))) + elif msg.params[1] == {'ACK', 'NAK'}: + cap_names = msg.params[-1].split() + for cap_name in cap_names: + target.cap_neg_set(f'REQ:{cap_name}', done=True) + target.caps[cap_name].enabled = msg.params[1] == 'ACK' + if target.cap_neg_done('LIST'): + target.try_send_cap('END') + if not target.cap_neg('printing'): + target.log('# server capabilities (enabled: "+"):') + for cap_name, cap in target.caps.items(): + target.log('# ' + cap.str_for_log(cap_name)) + target.cap_neg_set('printing', done=True) + elif target.cap_neg_done('LS'): + for cap_name in ('server-time', 'account-tag', 'sasl'): + if (cap_name in target.caps + and (not target.caps[cap_name].enabled)): + target.try_send_cap('REQ', cap_name, key_fused=True) + target.try_send_cap('LIST')