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 = ''
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.'
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:
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.'
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])
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']:
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
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:
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:
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])
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:
# 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 :?
# 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
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:
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]
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
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
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
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
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
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
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
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)@+]
# 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 <