home · contact · privacy
Fix /part for one channel appearing in windows for all.
authorChristian Heller <c.heller@plomlompom.de>
Fri, 19 Sep 2025 19:22:56 +0000 (21:22 +0200)
committerChristian Heller <c.heller@plomlompom.de>
Fri, 19 Sep 2025 19:22:56 +0000 (21:22 +0200)
ircplom/client.py
ircplom/client_tui.py
ircplom/testing.py
test.txt

index 38db44eac88cacd32b7ca9fcbea510520491a28c..7df5eaa62672750356c70c261028b74abfeeb743 100644 (file)
@@ -314,6 +314,7 @@ class Channel:
     'Collects .topic, and in .user_ids inhabitant IDs.'
     topic: Topic
     user_ids: Iterable[str]
+    exits: Dict[str]
 
 
 class LogScope(Enum):
@@ -323,6 +324,7 @@ class LogScope(Enum):
     RAW = auto()
     CHAT = auto()
     USER = auto()
+    USER_NO_CHANNELS = auto()
     SAME = auto()
 
 
@@ -374,6 +376,7 @@ class _CompletableTopic(_Completable, Topic):
 class _Channel(Channel):
     user_ids: _CompletableStringsSet
     topic: _CompletableTopic
+    exits: _Dict[str]
 
     def __init__(self,
                  userid_for_nickuserhost: Callable,
@@ -399,9 +402,11 @@ class _Channel(Channel):
                                                 updating=True)
         self.user_ids.completable_add(user_id, on_complete=True)
 
-    def remove_user(self, user: '_User') -> None:
+    def remove_user(self, user: '_User', msg: str) -> None:
         'From .user_ids remove .nickname, keep .user_ids declared complete.'
+        self.exits[user.id_] = msg
         self.user_ids.completable_remove(user.id_, on_complete=True)
+        del self.exits[user.id_]
         self.purge_users()
 
 
@@ -454,21 +459,18 @@ class _User(_SetNickuserhostMixin, User):
                  remove_from_channels: Callable,
                  **kwargs) -> None:
         self.names_channels = lambda: names_channels_of_user(self)
-        self._remove_from_channels = lambda name='': remove_from_channels(self,
-                                                                          name)
+        self._remove_from_channels = lambda target, msg: remove_from_channels(
+                self, target, msg)
         super().__init__(**kwargs)
 
     def part(self, channel_name: str, exit_msg: str) -> None:
         'First set .exit_msg, then remove from channel of channel_name.'
-        self.exit_msg = f'P{exit_msg}'
-        self.exit_msg = ''
-        self._remove_from_channels(channel_name)
+        self._remove_from_channels(channel_name, f'P{exit_msg}')
 
     def quit(self, exit_msg: str) -> None:
         'First set .exit_msg, then remove from any channels.'
         self.exit_msg = f'Q{exit_msg}'
-        self.exit_msg = ''
-        self._remove_from_channels()
+        self._remove_from_channels('', self.exit_msg)
 
     @property
     def id_(self) -> str:
@@ -491,6 +493,7 @@ class _UpdatingCompletableTopic(_UpdatingCompletable, _CompletableTopic):
 class _UpdatingChannel(_UpdatingAttrsMixin, _Channel):
     user_ids: _UpdatingCompletableStringsSet
     topic: _UpdatingCompletableTopic
+    exits: _UpdatingDict[str]
 
 
 class _UpdatingUser(_UpdatingAttrsMixin, _User):
@@ -567,13 +570,13 @@ class _UpdatingChannelsDict(_UpdatingDict[_UpdatingChannel]):
         'Return names of channels listing user as member.'
         return tuple(self._of_user(user).keys())
 
-    def remove_user(self, user: _User, target: str) -> None:
+    def remove_user(self, user: _User, target: str, msg: str) -> None:
         'Remove user from channel named "target", or all with user if empty.'
         if target:
-            self[target].remove_user(user)
+            self[target].remove_user(user, msg)
         else:
             for channel in self._of_user(user).values():
-                channel.remove_user(user)
+                channel.remove_user(user, msg)
 
 
 class _UpdatingIsupportDict(_UpdatingDict[str]):
index 92a14299384cd596ff2f439139003c381a12ddb3..b7086897f76d08e01202921f8eeb4617cb6d52c6 100644 (file)
@@ -214,6 +214,7 @@ class _QueryWindow(_ChatWindow):
 class _UpdatingChannel(_UpdatingNode, Channel):
     log_scopes = {'': LogScope.CHAT}
     user_ids: set[str]
+    exits: _UpdatingDict[str]
 
     def recursive_set_and_report_change(self, update: _Update) -> None:
         super().recursive_set_and_report_change(update)
@@ -230,10 +231,19 @@ class _UpdatingChannel(_UpdatingNode, Channel):
                 for id_ in (id_ for id_ in update.value
                             if id_ not in update.old_value):
                     update.results += [(scope, [':joining: ', f'NUH:{id_}'])]
+                for id_ in (id_ for id_ in update.old_value
+                            if id_ not in update.value):
+                    quits = self.exits[id_][0] == 'Q'
+                    part_msg = self.exits[id_][1:]
+                    exit_msg = [':' + ('quits' if quits else 'parts') + ': ',
+                                f'NUH:{id_}']
+                    if part_msg:
+                        exit_msg += [f':: {part_msg}']
+                    update.results += [(scope, exit_msg)]
 
 
 class _UpdatingUser(_UpdatingNode, User):
-    log_scopes = {'exit_msg': LogScope.USER}
+    log_scopes = {'exit_msg': LogScope.USER_NO_CHANNELS}
     prev_nick = '?'
 
     def recursive_set_and_report_change(self, update: _Update) -> None:
@@ -248,10 +258,9 @@ class _UpdatingUser(_UpdatingNode, User):
             elif update.key == 'exit_msg':
                 update.results.clear()
                 if update.value:
-                    msg = f':{self} '
-                    msg += 'quits' if update.value[0] == 'Q' else 'parts'
+                    msg = f':{self} quits'
                     if len(update.value) > 1:
-                        msg += ': ' + update.value[1:]
+                        msg += f': {update.value[1:]}'
                     update.results += [(self._scope(update.key), [msg])]
 
     @property
@@ -307,14 +316,16 @@ class _ClientWindowsManager:
             return win
         return self._new_win(scope=scope, chatname=chatname)
 
-    def windows_for_userid(self, user_id: str) -> list[_ClientWindow]:
+    def windows_for_userid(self, user_id: str, w_channels = True
+                           ) -> list[_ClientWindow]:
         'Return windows interacting with userid (all if "me").'
         if user_id == 'me':
             return self.windows[:]
         chan_names = [c for c, v in self.db.channels.items()
                       if user_id in v.user_ids]
         return [w for w in self.windows
-                if ((isinstance(w, _ChannelWindow)
+                if ((w_channels
+                     and isinstance(w, _ChannelWindow)
                      and w.chatname in chan_names)
                     or (isinstance(w, _QueryWindow)
                         and w.chatname in {
@@ -344,7 +355,8 @@ class _ClientWindowsManager:
             return False
         for scope, result in update.results:
             log_kwargs: dict[str, Any] = {'scope': scope}
-            if scope in {LogScope.CHAT, LogScope.USER}:
+            if scope in {LogScope.CHAT, LogScope.USER,
+                         LogScope.USER_NO_CHANNELS}:
                 log_kwargs |= {'target': update.full_path[1]}
             if isinstance(result, Topic):
                 self.log(f'{result.who} set topic: {result.what}',
@@ -397,6 +409,8 @@ class ClientTui(BaseTui):
                 return [m.window(LogScope.CHAT, chatname=chatname)]
             if scope == LogScope.USER:
                 return m.windows_for_userid(kwargs['target'])
+            if scope == LogScope.USER_NO_CHANNELS:
+                return m.windows_for_userid(kwargs['target'], w_channels=False)
             return [m.window(scope)]
         return super()._log_target_wins(**kwargs)
 
index 497eab15524efc46927f36b31b8ef4eb07517096..eddd5ff630e3d4c39eef38d88ac6eac25a9ff954 100644 (file)
@@ -119,6 +119,7 @@ class TestingClientTui(ClientTui):
         assert expected_msg == msg_sans_time, info
         assert expected_win_ids == win_ids, info
         self._play_till_next_log()
+        print(win_ids, msg)
         return win_ids, logged_msg
 
     def _play_till_next_log(self) -> None:
index 5dacdb0301866116e959dd2b66276ff0e6077856..3570853574fa5eabcf33940a5c397895eb687929 100644 (file)
--- a/test.txt
+++ b/test.txt
 2 < :foo!~foobarbaz@baz.bar.foo JOIN #test
 1,2 $ users:me:user set to: [~foobarbaz]
 2 < :foo.bar.baz 332 foo #test :foo bar baz
+4 $ channels:#test:exits cleared
 2 < :foo.bar.baz 333 foo #test bar!~bar@bar.bar 1234567890
 4 $ bar!~bar@bar.bar set topic: foo bar baz
 2 < :foo.bar.baz 353 foo @ #test :foo @bar
 
 # handle non-self PART
 2 < :bazbaz!~baz@baz.baz PART :#test
-4 $ bazbaz!~baz@baz.baz parts
+1,2 $ channels:#test:exits:2 set to: [P]
+4 $ parts: bazbaz!~baz@baz.baz
+1,2 $ channels:#test:exits:2 cleared
 1,2 $ users:2 cleared
 
 # handle re-join, treat as new user for lack of identity continuity reliability
 
 # handle non-self QUIT
 2 < :bazbaz!~baz@baz.baz QUIT :Client Quit
-4 $ bazbaz!~baz@baz.baz quits: Client Quit
+, $ bazbaz!~baz@baz.baz quits: Client Quit
+1,2 $ channels:#test:exits:3 set to: [QClient Quit]
+4 $ quits: bazbaz!~baz@baz.baz: Client Quit
+1,2 $ channels:#test:exits:3 cleared
 1,2 $ users:3 cleared
 
 # handle self-PART: clear channel, and its squatters
 2 < :foo!~foobarbaz@baz.bar.foo PART :#test
-1,2,3,4 $ foo!~foobarbaz@baz.bar.foo parts
+1,2 $ channels:#test:exits:me set to: [P]
+4 $ parts: foo!~foobarbaz@baz.bar.foo
+1,2 $ channels:#test:exits:me cleared
 1,2 $ channels:#test cleared
 1,2 $ users:1 cleared
 
 1,2,3,4 $ connection_state set to: [connecting]
 1,2,3,4 $ connection_state set to: [connected]
 repeat 64:147
-repeat 158:265
+repeat 158:273
 
 > /quit
 0 <