From a807610053f530a2481c36f149a271575b28f8a0 Mon Sep 17 00:00:00 2001 From: Christian Heller Date: Mon, 22 Sep 2025 04:47:49 +0200 Subject: [PATCH] Move NOTICE/PRIVMSG processing into msg_parse_expectations/client:ChatMessage. --- ircplom/client.py | 55 ++++++++-- ircplom/client_tui.py | 27 +++-- ircplom/msg_parse_expectations.py | 26 ++--- test.txt | 163 ++++++++++++++++-------------- 4 files changed, 167 insertions(+), 104 deletions(-) diff --git a/ircplom/client.py b/ircplom/client.py index 75e375f..e79399a 100644 --- a/ircplom/client.py +++ b/ircplom/client.py @@ -274,6 +274,21 @@ class IrcConnSetup: password: str = '' +class ChatMessage: + 'Collects all we want to know on incoming PRIVMSG or NOTICE chat message.' + content: str = '' + sender: str = '' + target: str = '' + is_notice: bool = False + + def __str__(self) -> str: + return f'{"N" if self.is_notice else "P"} '\ + + f'{self.sender} {self.target} :{self.content}' + + def __bool__(self) -> bool: + return bool(self.content + self.sender + self.target) | self.is_notice + + class SharedClientDbFields(IrcConnSetup): 'API for fields shared directly in name and type with TUI.' connection_state: str = '' @@ -281,6 +296,7 @@ class SharedClientDbFields(IrcConnSetup): motd: Iterable[str] sasl_account: str = '' sasl_auth_state: str = '' + message: ChatMessage = ChatMessage() def is_chan_name(self, name: str) -> bool: 'Tests name to match CHANTYPES prefixes.' @@ -418,6 +434,30 @@ class _Channel(Channel): self.purge_users() +class _ChatMessage(ChatMessage): + + def __init__(self, sender: str = '', db: Optional['_ClientDb'] = None + ) -> None: + self.sender = sender + self._db = db + + def to(self, target: str) -> Self: + 'Extend self with .target (empty if input as "me"), return self.' + self.target = '' if target == 'me' else target + return self + + def __setattr__(self, key: str, value: str) -> None: + if key in {'privmsg', 'notice'}: + assert self._db is not None + self.is_notice = key == 'notice' + self.content = value + self._db.message = self + # to clean update cache, enabling equal messages in direct sequence + self._db.message = ChatMessage() + else: + super().__setattr__(key, value) + + class _SetNickuserhostMixin: def __setattr__(self, key: str, value: NickUserHost | str) -> None: @@ -629,6 +669,11 @@ class _ClientDb(_Clearable, _UpdatingAttrsMixin, SharedClientDbFields): attr._create_if_none = {} return attr + def messaging(self, src: str | NickUserHost) -> ChatMessage: + 'Start input chain for chat message data.' + return _ChatMessage(sender=f':{src}' if isinstance(src, str) + else src.nick, db=self) + def into_endnode_updates(self, path: tuple[str, ...] ) -> list[tuple[tuple[str, ...], Any]]: 'Return path-value pairs for update-worthy (sub-)elements at path.' @@ -863,7 +908,8 @@ class Client(ABC, ClientQueueMixin): for step in task.path: key = ret[step] if step.isupper() else step node = (node[key] if isinstance(node, Dict) - else getattr(node, key)) + else (node(key) if callable(node) + else getattr(node, key))) for tok_name in tok_names: if task.verb == 'setattr': setattr(node, tok_name, ret[tok_name]) @@ -917,13 +963,6 @@ class Client(ABC, ClientQueueMixin): self.db.users[user_id].nick = ret['nick'] if user_id == 'me': self.db.nick_wanted = ret['nick'] - elif ret['_verb'] in {'NOTICE', 'PRIVMSG'}: - kw: dict[str, str | bool] = {'as_notice': ret['_verb'] == 'NOTICE'} - if (scope := LogScope.CHAT if 'sender' in ret else None): - kw['them'] = ret['sender'].nick - kw['log_target'] = (kw['them'] if 'nick' in ret - else ret['channel']) - self._log(ret['message'], out=False, scope=scope, **kw) elif ret['_verb'] == 'PART': ret['parter'].part(ret['channel'], ret.get('message', '')) if ret['parter'] is self.db.users['me']: diff --git a/ircplom/client_tui.py b/ircplom/client_tui.py index 4e72f68..1561620 100644 --- a/ircplom/client_tui.py +++ b/ircplom/client_tui.py @@ -6,8 +6,8 @@ from typing import Any, Callable, Optional, Sequence from ircplom.tui_base import (BaseTui, PromptWidget, TuiEvent, Window, CMD_SHORTCUTS) from ircplom.client import ( - AutoAttrMixin, Channel, Client, ClientQueueMixin, Dict, DictItem, - ImplementationFail, IrcConnSetup, LogScope, NewClientEvent, + AutoAttrMixin, Channel, ChatMessage, Client, ClientQueueMixin, Dict, + DictItem, ImplementationFail, IrcConnSetup, LogScope, NewClientEvent, NickUserHost, SendFail, ServerCapability, SharedClientDbFields, User) from ircplom.irc_conn import IrcMessage @@ -285,6 +285,9 @@ class _TuiClientDb(_UpdatingNode, SharedClientDbFields): update.results += [(LogScope.ALL, [':CONNECTED'])] elif not update.value: update.results += [(LogScope.ALL, [':DISCONNECTED'])] + elif update.key == 'message' and update.value: + assert isinstance(update.value, ChatMessage) + update.results += [(LogScope.CHAT, [':' + update.value.content])] class _ClientWindowsManager: @@ -360,7 +363,14 @@ class _ClientWindowsManager: log_kwargs: dict[str, Any] = {'scope': scope} if scope in {LogScope.CHAT, LogScope.USER, LogScope.USER_NO_CHANNELS}: - log_kwargs |= {'log_target': update.full_path[1]} + if update.full_path == ('message',): + log_kwargs['log_target'] = (update.value.target + or update.value.sender) + log_kwargs['them'] = update.value.sender + log_kwargs['as_notice'] = update.value.is_notice + log_kwargs['out'] = False + else: + log_kwargs['log_target'] = update.full_path[1] if isinstance(result, list): msg = '' for item in result: @@ -376,12 +386,12 @@ class _ClientWindowsManager: self.log(f'{log_path} cleared', **log_kwargs) else: announcement = f'{log_path} set to:' - if isinstance(result, tuple): + if isinstance(result, ChatMessage) or not isinstance(result, tuple): + self.log(f'{announcement} [{result}]', **log_kwargs) + else: self.log(announcement, **log_kwargs) for item in result: self.log(f' {item}', **log_kwargs) - else: - self.log(f'{announcement} [{result}]', **log_kwargs) for win in [w for w in self.windows if isinstance(w, _ChatWindow)]: win.set_prompt_prefix() return bool([w for w in self.windows if w.tainted]) @@ -404,10 +414,7 @@ class ClientTui(BaseTui): if scope == LogScope.DEBUG: return [m.window(LogScope.DEBUG), m.window(LogScope.RAW)] if scope == LogScope.CHAT: - chatname = (kwargs['log_target'] - if kwargs['log_target'] != m.db.users['me'].nick - else kwargs['them']) - return [m.window(LogScope.CHAT, chatname=chatname)] + 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: diff --git a/ircplom/msg_parse_expectations.py b/ircplom/msg_parse_expectations.py index bf8c3f6..dd39bd7 100644 --- a/ircplom/msg_parse_expectations.py +++ b/ircplom/msg_parse_expectations.py @@ -491,33 +491,35 @@ MSG_EXPECTATIONS: list[_MsgParseExpectation] = [ 'NOTICE', _MsgTok.SERVER, ('*', - (_MsgTok.ANY, ':message'))), + (_MsgTok.ANY, 'setattr_db.messaging.server.to.me:notice'))), _MsgParseExpectation( 'NOTICE', _MsgTok.SERVER, ((_MsgTok.NICKNAME, 'setattr_db.users.me:nick'), - (_MsgTok.ANY, ':message'))), + (_MsgTok.ANY, 'setattr_db.messaging.server.to.me:notice'))), + _MsgParseExpectation( 'NOTICE', - (_MsgTok.NICK_USER_HOST, ':sender'), + (_MsgTok.NICK_USER_HOST, ':USER'), ((_MsgTok.NICKNAME, 'setattr_db.users.me:nick'), - (_MsgTok.ANY, ':message'))), + (_MsgTok.ANY, 'setattr_db.messaging.USER.to.me:notice'))), + _MsgParseExpectation( 'NOTICE', - (_MsgTok.NICK_USER_HOST, ':sender'), - ((_MsgTok.CHANNEL, ':channel'), - (_MsgTok.ANY, ':message'))), + (_MsgTok.NICK_USER_HOST, ':USER'), + ((_MsgTok.CHANNEL, ':CHANNEL'), + (_MsgTok.ANY, 'setattr_db.messaging.USER.to.CHANNEL:notice'))), _MsgParseExpectation( 'PRIVMSG', - (_MsgTok.NICK_USER_HOST, ':sender'), + (_MsgTok.NICK_USER_HOST, ':USER'), ((_MsgTok.NICKNAME, 'setattr_db.users.me:nick'), - (_MsgTok.ANY, ':message'))), + (_MsgTok.ANY, 'setattr_db.messaging.USER.to.me:privmsg'))), _MsgParseExpectation( 'PRIVMSG', - (_MsgTok.NICK_USER_HOST, ':sender'), - ((_MsgTok.CHANNEL, ':channel'), - (_MsgTok.ANY, ':message'))), + (_MsgTok.NICK_USER_HOST, ':USER'), + ((_MsgTok.CHANNEL, ':CHANNEL'), + (_MsgTok.ANY, 'setattr_db.messaging.USER.to.CHANNEL:privmsg'))), # misc. diff --git a/test.txt b/test.txt index 583407b..0ab00d3 100644 --- a/test.txt +++ b/test.txt @@ -70,11 +70,17 @@ # expect some NOTICE and PING to process/reply during initiation 0:2 < :*.?.net NOTICE * :*** Looking up your ident... -1,2 $$$ *** Looking up your ident... +1,2 $ message set to: [N :server :*** Looking up your ident...] +3 <<< [:server] *** Looking up your ident... +1,2 $ message set to: [P :] 0:2 < :*.?.net NOTICE * :*** Looking up your hostname... -1,2 $$$ *** Looking up your hostname... +1,2 $ message set to: [N :server :*** Looking up your hostname...] +3 <<< [:server] *** Looking up your hostname... +1,2 $ message set to: [P :] 0:2 < :*.?.net NOTICE * :*** Found your hostname (baz.bar.foo) -1,2 $$$ *** Found your hostname (baz.bar.foo) +1,2 $ message set to: [N :server :*** Found your hostname (baz.bar.foo)] +3 <<< [:server] *** Found your hostname (baz.bar.foo) +1,2 $ message set to: [P :] 0:2 < PING :? 2 > PONG :? @@ -154,7 +160,9 @@ # handle bot query NOTICE 0:2 < :SaslServ!SaslServ@services.bar.baz NOTICE foo1 :Last login from ~foobarbaz@foo.bar.baz on Jan 1 22:00:00 2021 +0000. -3 <<< [SaslServ] Last login from ~foobarbaz@foo.bar.baz on Jan 1 22:00:00 2021 +0000. +1,2 $ message set to: [N SaslServ :Last login from ~foobarbaz@foo.bar.baz on Jan 1 22:00:00 2021 +0000.] +4 <<< [SaslServ] Last login from ~foobarbaz@foo.bar.baz on Jan 1 22:00:00 2021 +0000. +1,2 $ message set to: [P :] # check difference in available commands when switching to client window > /join #test @@ -164,7 +172,8 @@ 0 # 0) :start 0 # 1) foo.bar.baz :DEBUG 0 # 2) foo.bar.baz :RAW -0 # 3) foo.bar.baz SaslServ +0 # 3) foo.bar.baz :server +0 # 4) foo.bar.baz SaslServ > /window 1 > /help 1 # commands available in this window: @@ -211,7 +220,7 @@ 1,2 $ channels:#test:exits cleared 0:2 < :foo.bar.baz 333 foo1 #test bar!~bar@bar.bar 1234567890 1,2 $ channels:#test:topic set to: [Topic(what='foo bar baz', who=NickUserHost(nick='bar', user='~bar', host='bar.bar'))] -4 $ bar!~bar@bar.bar set topic: foo bar baz +5 $ bar!~bar@bar.bar set topic: foo bar baz 0:2 < :foo.bar.baz 353 foo1 @ #test :foo1 @bar 1,2 $ users:1:nick set to: [?] 1,2 $ users:1:nick set to: [bar] @@ -219,19 +228,21 @@ 1,2 $ channels:#test:user_ids set to: 1,2 $ 1 1,2 $ me -4 $ residents: bar, foo1 +5 $ residents: bar, foo1 # deliver PRIVMSG to channel window, update sender's user+host from metadata 0:2 < :bar!~bar@bar.bar PRIVMSG #test :hi there 1,2 $ users:1:user set to: [~bar] 1,2 $ users:1:host set to: [bar.bar] -4 < [bar] hi there +1,2 $ message set to: [P bar #test :hi there] +5 < [bar] hi there +1,2 $ message set to: [P :] # check _changing_ TOPIC message is communicated to channel window 0:2 < :bar!~bar@bar.bar TOPIC #test :foo bar baz 0:2 < :bar!~bar@bar.bar TOPIC #test :abc def ghi 1,2 $ channels:#test:topic set to: [Topic(what='abc def ghi', who=NickUserHost(nick='bar', user='~bar', host='bar.bar'))] -4 $ bar!~bar@bar.bar set topic: abc def ghi +5 $ bar!~bar@bar.bar set topic: abc def ghi # process non-self channel JOIN 0:2 < :baz!~baz@baz.baz JOIN :#test @@ -243,7 +254,7 @@ 1,2 $ 1 1,2 $ 2 1,2 $ me -4 $ baz!~baz@baz.baz joins +5 $ baz!~baz@baz.baz joins # join second channel with partial residents identity to compare distribution of resident-specific messages > /join #testtest @@ -255,28 +266,32 @@ 1,2 $ channels:#testtest:user_ids set to: 1,2 $ 2 1,2 $ me -5 $ residents: baz, foo1 +6 $ residents: baz, foo1 # handle query window with known user 0:2 < :baz!~baz@baz.baz PRIVMSG foo1 :hi there -6 < [baz] hi there +1,2 $ message set to: [P baz :hi there] +7 < [baz] hi there +1,2 $ message set to: [P :] > /privmsg baz hello, how is it going 2 > PRIVMSG baz :hello, how is it going -6 > [foo1] hello, how is it going +7 > [foo1] hello, how is it going 0:2 < :baz!~baz@baz.baz PRIVMSG foo1 :fine! -6 < [baz] fine! +1,2 $ message set to: [P baz :fine!] +7 < [baz] fine! +1,2 $ message set to: [P :] # handle failure to query absent user > /privmsg barbar hello! 2 > PRIVMSG barbar :hello! -7 > [foo1] hello! +8 > [foo1] hello! 0:2 < :*.?.net 401 foo1 barbar :No such nick/channel -7 !$ barbar not online +8 !$ barbar not online # handle non-self renaming 0:2 < :baz!~baz@baz.baz NICK :bazbaz 1,2 $ users:2:nick set to: [bazbaz] -4,5,6 $ baz!~baz@baz.baz renames bazbaz +5,6,7 $ baz!~baz@baz.baz renames bazbaz # handle non-self PART in one of two inhabited channels, preserve identity into re-JOIN 0:2 < :bazbaz!~baz@baz.baz PART :#test @@ -284,14 +299,14 @@ 1,2 $ channels:#test:user_ids set to: 1,2 $ 1 1,2 $ me -4 $ bazbaz!~baz@baz.baz parts +5 $ bazbaz!~baz@baz.baz parts 1,2 $ channels:#test:exits:2 cleared 0:2 < :bazbaz!~baz@baz.baz JOIN :#test 1,2 $ channels:#test:user_ids set to: 1,2 $ 1 1,2 $ 2 1,2 $ me -4 $ bazbaz!~baz@baz.baz joins +5 $ bazbaz!~baz@baz.baz joins # handle non-self PART in only inhabited channel, lose identity, re-join as new identity 0:2 < :bar!~bar@bar.bar PART :#test @@ -299,7 +314,7 @@ 1,2 $ channels:#test:user_ids set to: 1,2 $ 2 1,2 $ me -4 $ bar!~bar@bar.bar parts +5 $ bar!~bar@bar.bar parts 1,2 $ channels:#test:exits:1 cleared 1,2 $ users:1 cleared 0:2 < :bar!~bar@bar.bar JOIN :#test @@ -311,22 +326,22 @@ 1,2 $ 2 1,2 $ 3 1,2 $ me -4 $ bar!~bar@bar.bar joins +5 $ bar!~bar@bar.bar joins # handle non-self QUIT 0:2 < :bazbaz!~baz@baz.baz QUIT :Client Quit 1,2 $ users:2:exit_msg set to: [QClient Quit] -6 $ bazbaz!~baz@baz.baz quits: Client Quit +7 $ bazbaz!~baz@baz.baz quits: Client Quit 1,2 $ channels:#test:exits:2 set to: [QClient Quit] 1,2 $ channels:#test:user_ids set to: 1,2 $ 3 1,2 $ me -4 $ bazbaz!~baz@baz.baz quits: Client Quit +5 $ bazbaz!~baz@baz.baz quits: Client Quit 1,2 $ channels:#test:exits:2 cleared 1,2 $ channels:#testtest:exits:2 set to: [QClient Quit] 1,2 $ channels:#testtest:user_ids set to: 1,2 $ me -5 $ bazbaz!~baz@baz.baz quits: Client Quit +6 $ bazbaz!~baz@baz.baz quits: Client Quit 1,2 $ channels:#testtest:exits:2 cleared 1,2 $ users:2 cleared @@ -335,7 +350,7 @@ 1,2 $ channels:#test:exits:me set to: [P] 1,2 $ channels:#test:user_ids set to: 1,2 $ 3 -4 $ foo1!~foobarbaz@baz.bar.foo parts +5 $ foo1!~foobarbaz@baz.bar.foo parts 1,2 $ channels:#test:exits:me cleared 1,2 $ channels:#test cleared 1,2 $ users:3 cleared @@ -346,15 +361,15 @@ 2 > QUIT :ircplom says bye 0:2 < :foo1!~foobarbaz@baz.bar.foo QUIT :Client Quit 1,2 $ users:me:exit_msg set to: [QClient Quit] -3,6,7 $ foo1!~foobarbaz@baz.bar.foo quits: Client Quit +3,4,7,8 $ foo1!~foobarbaz@baz.bar.foo quits: Client Quit 1,2 $ channels:#testtest:exits:me set to: [QClient Quit] 1,2 $ channels:#testtest:user_ids set to: -5 $ foo1!~foobarbaz@baz.bar.foo quits: Client Quit +6 $ foo1!~foobarbaz@baz.bar.foo quits: Client Quit 1,2 $ channels:#testtest:exits:me cleared 0:2 < ERROR :Closing link: (~foobarbaz@baz.bar.foo) [Quit: ircplom says bye] 1,2 $ connection_state set to: [Closing link: (~foobarbaz@baz.bar.foo) [Quit: ircplom says bye]] 1,2 $ connection_state set to: [] -3,4,5,6,7 $ DISCONNECTED +3,4,5,6,7,8 $ DISCONNECTED 1,2 $ isupport cleared 1,2 $ isupport:CHANTYPES set to: [#&] 1,2 $ isupport:PREFIX set to: [(ov)@+] @@ -378,64 +393,64 @@ # test setting up second client, but 432 irrecoverably > /connect baz.bar.foo ?foo foo:foo -8,9 $ isupport cleared -8,9 $ isupport:CHANTYPES set to: [#&] -8,9 $ isupport:PREFIX set to: [(ov)@+] -8,9 $ isupport:USERLEN set to: [10] -8,9 $ hostname set to: [baz.bar.foo] -8,9 $ port set to: [-1] -8,9 $ nick_wanted set to: [?foo] -8,9 $ user_wanted set to: [foo] -8,9 $ realname set to: [foo] -8,9 $ port set to: [6697] -8,9 $ connection_state set to: [connecting] -8,9 $ connection_state set to: [connected] +9,10 $ isupport cleared +9,10 $ isupport:CHANTYPES set to: [#&] +9,10 $ isupport:PREFIX set to: [(ov)@+] +9,10 $ isupport:USERLEN set to: [10] +9,10 $ hostname set to: [baz.bar.foo] +9,10 $ port set to: [-1] +9,10 $ nick_wanted set to: [?foo] +9,10 $ user_wanted set to: [foo] +9,10 $ realname set to: [foo] +9,10 $ port set to: [6697] +9,10 $ connection_state set to: [connecting] +9,10 $ connection_state set to: [connected] , $ CONNECTED -1:9 > CAP LS :302 -1:9 > USER foo 0 * :foo -1:9 > NICK :?foo -1:9 < :*.?.net 432 * ?foo :Erroneous nickname -8,9 $ connection_state set to: [] +1:10 > CAP LS :302 +1:10 > USER foo 0 * :foo +1:10 > NICK :?foo +1:10 < :*.?.net 432 * ?foo :Erroneous nickname +9,10 $ connection_state set to: [] , $ DISCONNECTED -8,9 $ isupport cleared -8,9 $ isupport:CHANTYPES set to: [#&] -8,9 $ isupport:PREFIX set to: [(ov)@+] -8,9 $ isupport:USERLEN set to: [10] -8,9 !$ nickname refused for bad format, giving up +9,10 $ isupport cleared +9,10 $ isupport:CHANTYPES set to: [#&] +9,10 $ isupport:PREFIX set to: [(ov)@+] +9,10 $ isupport:USERLEN set to: [10] +9,10 !$ nickname refused for bad format, giving up # test failing third connection > /connect baz.baz.baz baz baz:baz -10,11 $ isupport cleared -10,11 $ isupport:CHANTYPES set to: [#&] -10,11 $ isupport:PREFIX set to: [(ov)@+] -10,11 $ isupport:USERLEN set to: [10] -10,11 $ hostname set to: [baz.baz.baz] -10,11 $ port set to: [-1] -10,11 $ nick_wanted set to: [baz] -10,11 $ user_wanted set to: [baz] -10,11 $ realname set to: [baz] -10,11 $ port set to: [6697] -10,11 $ connection_state set to: [connecting] -10,11 $ connection_state set to: [connected] +11,12 $ isupport cleared +11,12 $ isupport:CHANTYPES set to: [#&] +11,12 $ isupport:PREFIX set to: [(ov)@+] +11,12 $ isupport:USERLEN set to: [10] +11,12 $ hostname set to: [baz.baz.baz] +11,12 $ port set to: [-1] +11,12 $ nick_wanted set to: [baz] +11,12 $ user_wanted set to: [baz] +11,12 $ realname set to: [baz] +11,12 $ port set to: [6697] +11,12 $ connection_state set to: [connecting] +11,12 $ connection_state set to: [connected] , $ CONNECTED -2:11 > CAP LS :302 -2:11 > USER baz 0 * :baz -2:11 > NICK :baz +2:12 > CAP LS :302 +2:12 > USER baz 0 * :baz +2:12 > NICK :baz 2: < FAKE_IRC_CONN_ABORT_EXCEPTION -10,11 $ connection_state set to: [broken: FAKE_IRC_CONN_ABORT_EXCEPTION] -10,11 $ connection_state set to: [] +11,12 $ connection_state set to: [broken: FAKE_IRC_CONN_ABORT_EXCEPTION] +11,12 $ connection_state set to: [] , $ DISCONNECTED -10,11 $ isupport cleared -10,11 $ isupport:CHANTYPES set to: [#&] -10,11 $ isupport:PREFIX set to: [(ov)@+] -10,11 $ isupport:USERLEN set to: [10] +11,12 $ isupport cleared +11,12 $ isupport:CHANTYPES set to: [#&] +11,12 $ isupport:PREFIX set to: [(ov)@+] +11,12 $ isupport:USERLEN set to: [10] # check that (save TUI tests assuming start on window 0, and no 4 yet) on reconnect, all the same effects can be expected > /reconnect repeat 63:65 -3,4,5,6,7 $ CONNECTED -repeat 66:158 -repeat 168:368 +3,4,5,6,7,8 $ CONNECTED +repeat 66:166 +repeat 177:383 > /quit 0 < -- 2.30.2