home · contact · privacy
Use exception to communicate 401 from Client to TUI, move LogScope into client_tui.py.
authorChristian Heller <c.heller@plomlompom.de>
Tue, 23 Sep 2025 15:02:01 +0000 (17:02 +0200)
committerChristian Heller <c.heller@plomlompom.de>
Tue, 23 Sep 2025 15:02:01 +0000 (17:02 +0200)
ircplom/client.py
ircplom/client_tui.py

index d48e37efdb78c373c192284abc44f9ad156389d9..3eb934d7a5a2b27bf6ae4ab8852c412e75525f5a 100644 (file)
@@ -3,7 +3,6 @@
 from abc import ABC, abstractmethod
 from base64 import b64encode
 from dataclasses import dataclass, InitVar
-from enum import Enum, auto
 from getpass import getuser
 from threading import Thread
 from typing import (Any, Callable, Collection, Generic, Iterable, Iterator,
@@ -24,6 +23,10 @@ class SendFail(BaseException):
     'When Client.send fails.'
 
 
+class TargetUserOffline(BaseException):
+    'When according to server our target user is not to be found.'
+
+
 class ImplementationFail(BaseException):
     'When no matching parser found for server message.'
 
@@ -341,16 +344,6 @@ class Channel:
     exits: Dict[str]
 
 
-class LogScope(Enum):
-    'Where log messages should go.'
-    ALL = auto()
-    DEBUG = auto()
-    RAW = auto()
-    CHAT = auto()
-    USER = auto()
-    USER_NO_CHANNELS = auto()
-
-
 @dataclass
 class _ClientIdMixin:
     'Collects a Client\'s ID at .client_id.'
@@ -869,8 +862,7 @@ class Client(ABC, ClientQueueMixin):
         pass
 
     @abstractmethod
-    def _log(self, msg: str, scope: Optional[LogScope] = None, **kwargs
-             ) -> None:
+    def _log(self, msg: str, **kwargs) -> None:
         pass
 
     def send(self, verb: str, *args) -> IrcMessage:
@@ -927,8 +919,7 @@ class Client(ABC, ClientQueueMixin):
         elif ret['_verb'] == '372':  # RPL_MOTD
             self.db.motd.append(ret['line'])
         elif ret['_verb'] == '401':  # ERR_NOSUCHNICK
-            self._log(f'{ret["missing"]} not online', scope=LogScope.CHAT,
-                      log_target=ret['missing'], alert=True)
+            raise TargetUserOffline(ret['missing'])
         elif ret['_verb'] == '432':  # ERR_ERRONEOUSNICKNAME
             alert = 'nickname refused for bad format'
             if 'nick' not in ret:
index b9f00ae9b68ec25370a603635fd24a8123e2d99d..e7adda2485f4627374269f439f19a6a0e7d66b65 100644 (file)
@@ -1,14 +1,15 @@
 'TUI adaptions to Client.'
 # built-ins
+from enum import Enum, auto
 from getpass import getuser
 from typing import Any, Callable, Optional, Sequence
 # ourselves
 from ircplom.tui_base import (BaseTui, PromptWidget, TuiEvent, Window,
                               CMD_SHORTCUTS)
 from ircplom.client import (
-        AutoAttrMixin, Channel, ChatMessage, Client, ClientQueueMixin, Dict,
-        DictItem, ImplementationFail, IrcConnSetup, LogScope, NewClientEvent,
-        NickUserHost, SendFail, ServerCapability, SharedClientDbFields, User)
+    AutoAttrMixin, Channel, ChatMessage, Client, ClientQueueMixin, Dict,
+    DictItem, ImplementationFail, IrcConnSetup, NewClientEvent, NickUserHost,
+    SendFail, ServerCapability, SharedClientDbFields, TargetUserOffline, User)
 from ircplom.irc_conn import IrcMessage
 
 CMD_SHORTCUTS['disconnect'] = 'window.disconnect'
@@ -24,21 +25,31 @@ _LOG_PREFIX_OUT = '>'
 _LOG_PREFIX_IN = '<'
 
 
+class _LogScope(Enum):
+    'Where log messages should go.'
+    ALL = auto()
+    DEBUG = auto()
+    RAW = auto()
+    CHAT = auto()
+    USER = auto()
+    USER_NO_CHANNELS = auto()
+
+
 class _ClientWindow(Window, ClientQueueMixin):
 
-    def __init__(self, scope: LogScope, log: Callable, **kwargs) -> None:
+    def __init__(self, scope: _LogScope, log: Callable, **kwargs) -> None:
         self.scope = scope
         self._log = log
         super().__init__(**kwargs)
         self._title = f'{self.client_id} '\
-            + f':{"DEBUG" if self.scope == LogScope.DEBUG else "RAW"}'
+            + f':{"DEBUG" if self.scope == _LogScope.DEBUG else "RAW"}'
 
     def _send_msg(self, verb: str, params: tuple[str, ...]) -> None:
         self._client_trigger('send_w_params_tuple', verb=verb, params=params)
 
     def cmd__disconnect(self, quit_msg: str = 'ircplom says bye') -> None:
         'Send QUIT command to server.'
-        self._log('requesting disconnect …', scope=LogScope.DEBUG)
+        self._log('requesting disconnect …', scope=_LogScope.DEBUG)
         self._send_msg('QUIT', (quit_msg,))
 
     def cmd__reconnect(self) -> None:
@@ -123,7 +134,7 @@ class _QueryWindow(_ChatWindow):
 
 class _Update:
     old_value: Any
-    results: list[tuple[LogScope, Any]]
+    results: list[tuple[_LogScope, Any]]
 
     def __init__(self, path: tuple[str, ...], value: Any) -> None:
         self.full_path = path
@@ -166,7 +177,7 @@ class _UpdatingNode(AutoAttrMixin):
                 self._set(update.key, update.value)
                 do_report |= True
             if do_report:
-                update.results += [(LogScope.DEBUG,
+                update.results += [(_LogScope.DEBUG,
                                     tuple(sorted(update.value))
                                     if isinstance(update.value, set)
                                     else update.value)]
@@ -213,22 +224,22 @@ class _UpdatingChannel(_UpdatingNode, Channel):
         super().recursive_set_and_report_change(update)
         if update.key == 'topic':
             msg = f':{self.topic.who} set topic: {self.topic.what}'
-            update.results += [(LogScope.CHAT, [msg])]
+            update.results += [(_LogScope.CHAT, [msg])]
         elif update.key == 'user_ids':
             if not update.old_value:
                 nicks = []
                 for id_ in sorted(update.value):
                     nicks += [f'NICK:{id_}', ':, ']
                 nicks.pop()
-                update.results += [(LogScope.CHAT, [':residents: '] + nicks)]
+                update.results += [(_LogScope.CHAT, [':residents: '] + nicks)]
             else:
                 for id_ in (id_ for id_ in update.value
                             if id_ not in update.old_value):
-                    update.results += [(LogScope.CHAT,
+                    update.results += [(_LogScope.CHAT,
                                         [f'NUH:{id_}', ': joins'])]
                 for id_ in (id_ for id_ in update.old_value
                             if id_ not in update.value):
-                    update.results += [(LogScope.CHAT,
+                    update.results += [(_LogScope.CHAT,
                                         _UpdatingUser.exit_msg_toks(
                                             f'NUH:{id_}', self.exits[id_]))]
 
@@ -253,11 +264,11 @@ class _UpdatingUser(_UpdatingNode, User):
                 self.prev_nick = update.old_value
                 if update.old_value != '?':
                     update.results += [
-                        (LogScope.USER,
+                        (_LogScope.USER,
                          [f':{self.prev} renames {update.value}'])]
             elif update.key == 'exit_msg':
                 if update.value:
-                    update.results += [(LogScope.USER_NO_CHANNELS,
+                    update.results += [(_LogScope.USER_NO_CHANNELS,
                                         self.exit_msg_toks(
                                             f':{self}', update.value))]
 
@@ -282,12 +293,12 @@ class _TuiClientDb(_UpdatingNode, SharedClientDbFields):
         super().recursive_set_and_report_change(update)
         if update.key == 'connection_state':
             if update.value == 'connected':
-                update.results += [(LogScope.ALL, [':CONNECTED'])]
+                update.results += [(_LogScope.ALL, [':CONNECTED'])]
             elif not update.value:
-                update.results += [(LogScope.ALL, [':DISCONNECTED'])]
+                update.results += [(_LogScope.ALL, [':DISCONNECTED'])]
         elif update.key == 'message' and update.value:
             assert isinstance(update.value, ChatMessage)
-            update.results += [(LogScope.CHAT, [':' + update.value.content])]
+            update.results += [(_LogScope.CHAT, [':' + update.value.content])]
 
 
 class _ClientWindowsManager:
@@ -297,12 +308,12 @@ class _ClientWindowsManager:
         self._tui_new_window = tui_new_window
         self.db = _TuiClientDb()
         self.windows: list[_ClientWindow] = []
-        for scope in (LogScope.DEBUG, LogScope.RAW):
+        for scope in (_LogScope.DEBUG, _LogScope.RAW):
             self._new_win(scope)
 
-    def _new_win(self, scope: LogScope, chatname: str = '') -> _ClientWindow:
+    def _new_win(self, scope: _LogScope, chatname: str = '') -> _ClientWindow:
         kwargs = {'scope': scope, 'log': self.log, 'win_cls': _ClientWindow}
-        if scope == LogScope.CHAT:
+        if scope == _LogScope.CHAT:
             kwargs['win_cls'] = (
                     _ChannelWindow if self.db.is_chan_name(chatname)
                     else _QueryWindow)
@@ -314,10 +325,10 @@ class _ClientWindowsManager:
         self.windows += [win]
         return win
 
-    def window(self, scope: LogScope, chatname: str = '') -> _ClientWindow:
+    def window(self, scope: _LogScope, chatname: str = '') -> _ClientWindow:
         'Return client window of scope.'
         for win in [w for w in self.windows if w.scope == scope]:
-            if scope == LogScope.CHAT:
+            if scope == _LogScope.CHAT:
                 if isinstance(win, _ChatWindow) and win.chatname == chatname:
                     return win
                 continue
@@ -338,14 +349,14 @@ class _ClientWindowsManager:
                             self.db.users[user_id].nick,
                             self.db.users[user_id].prev_nick})))]
 
-    def log(self, msg: str, scope: LogScope, **kwargs) -> None:
+    def log(self, msg: str, scope: _LogScope, **kwargs) -> None:
         'From parsing scope, kwargs, build prefix before sending to logger.'
         first_char = '$'
         sender_label = ''
         receiving: Optional[bool] = None
-        if scope == LogScope.RAW:
+        if scope == _LogScope.RAW:
             receiving = not kwargs.get('out', False)
-        if scope == LogScope.CHAT and 'sender' in kwargs:
+        if scope == _LogScope.CHAT and 'sender' in kwargs:
             receiving = bool(kwargs['sender'])
             sender_label = ' [' + (kwargs['sender'] if receiving
                                    else self.db.users['me'].nick) + ']'
@@ -364,8 +375,8 @@ class _ClientWindowsManager:
 
         for scope, result in update.results:
             log_kwargs: dict[str, Any] = {'scope': scope}
-            if scope in {LogScope.CHAT, LogScope.USER,
-                         LogScope.USER_NO_CHANNELS}:
+            if scope in {_LogScope.CHAT, _LogScope.USER,
+                         _LogScope.USER_NO_CHANNELS}:
                 if update.full_path == ('message',):
                     log_kwargs['log_target'] = (update.value.target
                                                 or update.value.sender)
@@ -412,16 +423,17 @@ class ClientTui(BaseTui):
     def _log_target_wins(self, **kwargs) -> Sequence[Window]:
         if (scope := kwargs.get('scope', None)):
             m = self._client_mngrs[kwargs['client_id']]
-            if scope == LogScope.ALL:
+            if scope == _LogScope.ALL:
                 return [w for w in m.windows
-                        if w.scope not in {LogScope.DEBUG, LogScope.RAW}]
-            if scope == LogScope.DEBUG:
-                return [m.window(LogScope.DEBUG), m.window(LogScope.RAW)]
-            if scope == LogScope.CHAT:
-                return [m.window(LogScope.CHAT, chatname=kwargs['log_target'])]
-            if scope == LogScope.USER:
+                        if w.scope not in {_LogScope.DEBUG, _LogScope.RAW}]
+            if scope == _LogScope.DEBUG:
+                return [m.window(_LogScope.DEBUG), m.window(_LogScope.RAW)]
+            if scope == _LogScope.CHAT:
+                return [m.window(_LogScope.CHAT,
+                                 chatname=kwargs['log_target'])]
+            if scope == _LogScope.USER:
                 return m.windows_for_userid(kwargs['log_target'])
-            if scope == LogScope.USER_NO_CHANNELS:
+            if scope == _LogScope.USER_NO_CHANNELS:
                 return m.windows_for_userid(kwargs['log_target'],
                                             w_channels=False)
             return [m.window(scope)]
@@ -514,22 +526,26 @@ class ClientKnowingTui(Client):
 
     def send(self, verb: str, *args) -> IrcMessage:
         msg = super().send(verb, *args)
-        self._log(msg.raw, scope=LogScope.RAW, out=True)
+        self._log(msg.raw, scope=_LogScope.RAW, out=True)
         return msg
 
     def handle_msg(self, msg: IrcMessage) -> None:
-        self._log(msg.raw, scope=LogScope.RAW, out=False)
+        self._log(msg.raw, scope=_LogScope.RAW, out=False)
         try:
             super().handle_msg(msg)
         except ImplementationFail as e:
             self._log(str(e), alert=True)
+        except TargetUserOffline as e:
+            name = f'{e}'
+            self._log(f'{name} not online', scope=_LogScope.CHAT,
+                      log_target=name, alert=True)
 
-    def _log(self, msg: str, scope: Optional[LogScope] = None, **kwargs
+    def _log(self, msg: str, scope: Optional[_LogScope] = None, **kwargs
              ) -> None:
         if not scope:
-            scope = LogScope.DEBUG
+            scope = _LogScope.DEBUG
         self._client_tui_trigger('log', scope=scope, msg=msg, **kwargs)
-        if scope == LogScope.RAW:
+        if scope == _LogScope.RAW:
             with open(f'{self.client_id}.log', 'a', encoding='utf8') as f:
                 f.write(('>' if kwargs['out'] else '<') + f' {msg}\n')