home · contact · privacy
Refactor ClientDb usage.
authorChristian Heller <c.heller@plomlompom.de>
Wed, 13 Aug 2025 14:37:29 +0000 (16:37 +0200)
committerChristian Heller <c.heller@plomlompom.de>
Wed, 13 Aug 2025 14:37:29 +0000 (16:37 +0200)
ircplom/client.py
ircplom/client_tui.py

index 96c40c2c90f31aff168456f323ed7275574d3678..1dfd5f8a06afc0e42567d47d8c621f576919d1d2 100644 (file)
@@ -84,8 +84,7 @@ class _CapsManager:
 
     def clear(self) -> None:
         'Reset all negotiation knowledge to zero.'
-        self.sasl_wait = False
-        self._sent_challenges.clear()
+        self._reset_negotation_steps()
         self._db.set('caps_LS', [], confirm=False)
         self._db.set('caps_LIST', [], confirm=False)
 
@@ -101,15 +100,10 @@ class _CapsManager:
                     data=data)
         return d
 
-    def _unconfirmed_caps_list(self, key) -> list[str]:
-        ret = self._db.get_force(key)[0]
-        assert isinstance(ret, list)
-        return ret
-
     @property
     def _availables(self) -> dict[str, str]:
         avs: dict[str, str] = {}
-        for item in self._unconfirmed_caps_list('caps_LS'):
+        for item in self._db.caps_LS:
             toks = item.split('=', maxsplit=1)
             avs[toks[0]] = '' if len(toks) == 1 else toks[1]
         return avs
@@ -119,25 +113,22 @@ class _CapsManager:
         match params[0]:
             case 'NEW':
                 for param in params[-1].split():
-                    self._db.caps_LS.append(param)
+                    self._db.append('caps_LS', param)
             case 'DEL':
                 for param in params[-1].split():
                     del self._db.caps_LS[param]
             case 'ACK' | 'NAK':
                 for name in params[-1].split():
                     if params[0] == 'ACK':
-                        self._unconfirmed_caps_list('caps_LIST').append(name)
+                        self._db.append('caps_LIST', name)
             case 'LS' | 'LIST':
                 key = f'caps_{params[0]}'
-                caps_list, has_finished = self._db.get_force(key)
-                assert isinstance(caps_list, list)
-                if has_finished:
-                    caps_list.clear()
-                    self._db.set(key, caps_list, confirm=False)
+                if getattr(self._db, key) is not None:
+                    self._db.set(key, [], confirm=False)
                 for item in params[-1].strip().split():
-                    caps_list.append(item)
+                    self._db.append(key, item)
                 if params[1] != '*':
-                    self._db.set(key, caps_list, confirm=True)
+                    self._db.set(key, None, confirm=True)
         if self.asdict is not None:
             self.sasl_wait = (
                     'sasl' in self.asdict
@@ -152,6 +143,15 @@ class _CapsManager:
             self.challenge('LIST')
         return False
 
+    def end_negotiation(self) -> None:
+        'Stop negotation, without emptying caps DB.'
+        self.challenge('END')
+        self._reset_negotation_steps()
+
+    def _reset_negotation_steps(self) -> None:
+        self.sasl_wait = False
+        self._sent_challenges.clear()
+
     def challenge(self, *params) -> None:
         'Ensure CAP message of params has been sent, store sended-ness.'
         fused = ' '.join(params)
@@ -183,18 +183,32 @@ class ClientDb:
     def _unconf_key(self, key) -> str:
         return f'_{key}'
 
-    def set(self, key: str, value: int | str | list[str], confirm=False
+    def set(self,
+            key: str,
+            value: Optional[int | str | list[str]],
+            confirm=False
             ) -> tuple[bool, bool]:
         'Ensures setting, returns if changed value or confirmation.'
-        retrieval = self.get_force(key)
-        confirm_changed = confirm != retrieval[1]
-        value_changed = retrieval[0] != value
-        if confirm_changed and retrieval[0] is not None:
-            del self._dict[self._unconf_key(key) if confirm else key]
-        if value_changed or confirm_changed:
-            self._dict[key if confirm else self._unconf_key(key)] = value
+        old_value, was_confirmed = self.get_force(key)
+        assert (old_value is not None) or (value is not None)
+        confirm_changed = confirm != was_confirmed
+        key_to_set = key if confirm else self._unconf_key(key)
+        key_to_unset = self._unconf_key(key) if confirm else key
+        value_changed = (value is not None) and value != old_value
+        if value is not None:
+            self._dict[key_to_set] = value
+        if confirm_changed and old_value is not None:
+            if value is None:
+                self._dict[key_to_set] = old_value
+            del self._dict[key_to_unset]
         return (value_changed, confirm_changed)
 
+    def append(self, key, value: str) -> None:
+        'To list[str] keyed by key, append value.'
+        appendable = self.get_force(key)[0]
+        assert isinstance(appendable, list)
+        appendable.append(value)
+
     def get_force(self, key: str) -> tuple[Optional[int | str | list[str]],
                                            bool]:
         'Get even if only stored unconfirmed, tell if confirmed was found..'
@@ -220,9 +234,9 @@ class Client(ABC, ClientQueueMixin):
         self._db = ClientDb()
         for k in conn_setup._fields:
             self._db.set(k, getattr(conn_setup, k), confirm=k != 'nickname')
-        if conn_setup.port <= 0:
+        if self._db.port <= 0:
             self._db.set('port', PORT_SSL, confirm=True)
-        self.client_id = conn_setup.hostname
+        self.client_id = self._db.hostname
         self._caps = _CapsManager(self.send, self._db)
         super().__init__(client_id=self.client_id, **kwargs)
         self._start_connecting()
@@ -281,7 +295,8 @@ class Client(ABC, ClientQueueMixin):
                 self._log(to_log, scope=log_target)
         self._log(msg.raw, scope=LogScope.RAW, out=True)
 
-    def _update_db(self, key: str, value: int | str, confirm: bool) -> None:
+    def _update_db(self, key: str, value: Optional[int | str], confirm: bool
+                   ) -> None:
         'Wrap ._db.set into something accessible to subclass extension.'
         self._db.set(key, value, confirm)
 
@@ -292,10 +307,7 @@ class Client(ABC, ClientQueueMixin):
         if self.conn:
             self.conn.close()
         self.conn = None
-        nick_key = 'nickname'
-        nickname = self._db.get_force('nickname')[0]
-        assert isinstance(nickname, str)
-        self._update_db(nick_key, value=nickname, confirm=False)
+        self._update_db('nickname', value=None, confirm=False)
 
     def on_handled_loop_exception(self, e: IrcConnAbortException) -> None:
         'Gracefully handle broken connection.'
@@ -313,13 +325,9 @@ class Client(ABC, ClientQueueMixin):
             case '375':
                 self._db.set('motd', [], False)
             case '372':
-                motd = self._db.get_force('motd')[0]
-                assert isinstance(motd, list)
-                motd += [msg.params[-1]]
+                self._db.append('motd', msg.params[-1])
             case '376':
-                motd = self._db.get_force('motd')[0]
-                assert isinstance(motd, list)
-                self._db.set('motd', motd, True)
+                self._db.set('motd', None, confirm=True)
                 for line in self._db.motd:
                     self._log(line, as_notice=True)
             case 'PING':
@@ -342,7 +350,7 @@ class Client(ABC, ClientQueueMixin):
                         self._log('trying to authenticate via SASL/plain')
                         self.send(IrcMessage('AUTHENTICATE', ('PLAIN',)))
                     else:
-                        self._caps.challenge('END')
+                        self._caps.end_negotiation()
             case 'AUTHENTICATE':
                 if msg.params == ('+',):
                     auth = b64encode((self._db.conn_setup.nickname + '\0' +
@@ -354,7 +362,7 @@ class Client(ABC, ClientQueueMixin):
                 alert = msg.verb == '904'
                 self._log(f'SASL auth {"failed" if alert else "succeeded"}',
                           alert=alert)
-                self._caps.challenge('END')
+                self._caps.end_negotiation()
             case 'JOIN' | 'PART':
                 user, channel = msg.nick_from_source, msg.params[-1]
                 self._log(f'{user} {msg.verb.lower()}s {channel}',
index 40d012f4ca90c1cfcb1e1fb0e35a28a6aebed6bd..6107dbc854079c7782424f92ae734c49225ba8d3 100644 (file)
@@ -167,7 +167,11 @@ class _ClientWindowsManager:
         prefix = f'{first_char}{sender_label}'
         self._tui_log(msg, scope=scope, prefix=prefix, **kwargs)
 
-    def update(self, key: str, value: str, confirmed: bool, scope: LogScope
+    def update(self,
+               key: str,
+               value: Optional[str],
+               confirmed: bool,
+               scope: LogScope
                ) -> bool:
         'Apply settings in kwargs, follow representation update triggers.'
         changes = self._db.set(key, value, confirmed)
@@ -280,7 +284,8 @@ class _ClientKnowingTui(Client):
                 with open(f'{self.client_id}.log', 'a', encoding='utf8') as f:
                     f.write(('>' if kwargs['out'] else '<') + f' {msg}\n')
 
-    def _update_db(self, key: str, value: int | str, confirm: bool) -> None:
+    def _update_db(self, key: str, value: Optional[int | str], confirm: bool
+                   ) -> None:
         super()._update_db(key, value, confirm)
         self._client_tui_trigger('update', scope=LogScope.SERVER,
                                  key=key, value=value, confirmed=confirm)