_CHAR_CONTEXT_SEP = ' '
_CHAR_ID_TYPE_SEP = ':'
_CHAR_PROMPT = '>'
+_CHAR_SERVER_MSG = '<'
_CHAR_RANGE = ':'
_CHAR_RANGE_DATA_SEP = ' '
_CHAR_WIN_ID_SEP = ','
+_TOK_REPEAT = 'repeat'
class _Playbook:
self._get_client = get_client
with path.open('r', encoding='utf8') as f:
self._lines = [line.rstrip() for line in f.readlines()]
- while True:
+
+ def expand_parsed(marker, parse_into, **kwargs) -> bool:
inserts: list[str] = []
- anchors: dict[str, int] = {}
for idx, line in enumerate(self._lines):
- if line[:1] == _CHAR_ANCHOR:
- anchors[line[1:]] = idx
- for idx, line in enumerate(self._lines):
- split = self._split_active_line(line)
- if (not split) or split[0] != 'repeat':
+ if not line.startswith(marker):
continue
- range_data = split[1].split(_CHAR_RANGE_DATA_SEP, maxsplit=2)
- start_key, end_key = range_data[:2]
- start = anchors[start_key] + 1
- end = anchors[end_key]
- inserts = self._lines[int(start):int(end)]
- if len(range_data) == 3:
- for jdx, insert in enumerate(inserts):
- if (res := self._split_active_line(insert)):
- inserts[jdx] = ' '.join([range_data[2]] + [res[1]])
+ inserts = parse_into(line, **kwargs)
self._lines =\
self._lines[:idx] + inserts + self._lines[idx + 1:]
break
- if not inserts:
+ return bool(inserts)
+
+ def split_server_put_and_log(line: str, **_) -> list[str]:
+ context, msg = line.split(_CHAR_CONTEXT_SEP, maxsplit=1)
+ assert msg[0] in '.*' and msg[1:3] == f'{LOG_PREFIX_IN} '
+ c_id, win_ids = context[1:].split(_CHAR_ID_TYPE_SEP, maxsplit=1)
+ return [f'{_CHAR_SERVER_MSG}{c_id}{_CHAR_CONTEXT_SEP}{msg[3:]}',
+ win_ids + _CHAR_CONTEXT_SEP + msg]
+
+ def repeat(line: str, anchors: dict[str, int], **_) -> list[str]:
+ range_data = line[len(_TOK_REPEAT) + 1:].split(
+ _CHAR_RANGE_DATA_SEP, maxsplit=2)
+ start_key, end_key = range_data[:2]
+ start = anchors[start_key] + 1
+ end = anchors[end_key]
+ inserts = self._lines[int(start):int(end)]
+ if len(range_data) == 2:
+ return inserts
+ for jdx, insert in enumerate(inserts):
+ if (result := self._split_active_line(insert)):
+ inserts[jdx] = _CHAR_CONTEXT_SEP.join([range_data[2]]
+ + [result[1]])
+ return inserts
+
+ while expand_parsed(_CHAR_ID_TYPE_SEP, split_server_put_and_log):
+ pass
+ while True:
+ anchors: dict[str, int] = {}
+ for idx, line in enumerate(self._lines):
+ if line[:1] == _CHAR_ANCHOR:
+ anchors[line[1:]] = idx
+ if not expand_parsed(_TOK_REPEAT, repeat, anchors=anchors):
break
+ self._lines = [ln for ln in self._lines if ln[:1] != _CHAR_ANCHOR]
self._idx = 0
@property
for c in msg:
self.put_keypress(c)
self.put_keypress('KEY_ENTER')
- continue
- cmd_prefix = f'.{LOG_PREFIX_IN} '
- if _CHAR_ID_TYPE_SEP in context and msg.startswith(cmd_prefix):
- client_id, win_ids = context.split(_CHAR_ID_TYPE_SEP)
- client = self._get_client(int(client_id))
+ elif context.startswith(_CHAR_SERVER_MSG):
+ client = self._get_client(int(context[1:]))
assert isinstance(client.conn, _FakeIrcConnection)
- client.conn.put_server_msg(msg[len(cmd_prefix):])
- if not win_ids:
- continue
- break
+ client.conn.put_server_msg(msg)
+ else:
+ break
@staticmethod
def _split_active_line(line: str) -> Optional[tuple[str, ...]]:
- 'Return two-items tuple of split line, or None if inactive one.'
- if line[:1] in {_CHAR_COMMENT, _CHAR_ANCHOR}\
- or _CHAR_CONTEXT_SEP not in line:
+ 'Return 2-items tuple of split line, or None if empty/comment/anchor.'
+ if line[:1] == _CHAR_COMMENT or _CHAR_CONTEXT_SEP not in line:
return None
return tuple(line.split(_CHAR_CONTEXT_SEP, maxsplit=1))
1 .> NICK :foo
# expect some NOTICE and PING to process/reply during initiation
-0:1 .< :*.?.net NOTICE * :*** Looking up your ident...
+:0:1 .< :*.?.net NOTICE * :*** Looking up your ident...
2 .< *** [ server] *** Looking up your ident...
-0:1 .< :*.?.net NOTICE * :*** Looking up your hostname...
+:0:1 .< :*.?.net NOTICE * :*** Looking up your hostname...
2 .< *** [ server] *** Looking up your hostname...
-0:1 .< :*.?.net NOTICE * :*** Found your hostname (foo.bar.baz)
+:0:1 .< :*.?.net NOTICE * :*** Found your hostname (foo.bar.baz)
2 .< *** [ server] *** Found your hostname (foo.bar.baz)
-0:1 .< PING :?
+:0:1 .< PING :?
1 .> PONG :?
# handle 433
-0:1 .< :*.?.net 433 * foo :Nickname already in use
+:0:1 .< :*.?.net 433 * foo :Nickname already in use
1 .!$ nickname already in use, trying increment
1 .> NICK :foo0
-0:1 .< :*.?.net 433 * foo0 :Nickname already in use
+:0:1 .< :*.?.net 433 * foo0 :Nickname already in use
1 .!$ nickname already in use, trying increment
1 .> NICK :foo1
# collect server capabilities
-0:1 .< :*.?.net CAP * LS : foo bar sasl=PLAIN,EXTERNAL baz cap-notify
+:0:1 .< :*.?.net CAP * LS : foo bar sasl=PLAIN,EXTERNAL baz cap-notify
1 .> CAP REQ :sasl
1 .> CAP :LIST
-0:1 .< :*.?.net CAP * ACK :sasl
-0:1 .< :*.?.net CAP * LIST :cap-notify sasl
+:0:1 .< :*.?.net CAP * ACK :sasl
+:0:1 .< :*.?.net CAP * LIST :cap-notify sasl
1 .$ caps:bar:data set to: []
1 .$ caps:baz:data set to: []
1 .$ caps:cap-notify:data set to: []
# authenticate via SASL, collect items of user identity
1 .$ sasl_auth_state set to: [attempting]
1 .> AUTHENTICATE :PLAIN
-0:1 .< AUTHENTICATE +
+:0:1 .< AUTHENTICATE +
1 .> AUTHENTICATE :Zm9vAGZvbwBiYXI=
-0:1 .< :foo.bar.baz 900 foo1 foo1!foobarbazq@baz.bar.foo foo :You are now logged in as foo
+:0:1 .< :foo.bar.baz 900 foo1 foo1!foobarbazq@baz.bar.foo foo :You are now logged in as foo
1 .$ users:me:nick set to: [?]
1 .$ users:me:nick set to: [foo1]
1 .$ users:me:user set to: [foobarbazq]
1 .$ users:me:host set to: [baz.bar.foo]
1 .$ sasl_account set to: [foo]
-0:1 .< :foo.bar.baz 903 foo1 :SASL authentication successful
+:0:1 .< :foo.bar.baz 903 foo1 :SASL authentication successful
1 .$ sasl_auth_state set to: [SASL authentication successful]
# finish CAP negotation, thus login procedure
1 .> CAP :END
# of all pre-MOTD greeting messages, only process isupports
-0:1 .< :foo.bar.baz 001 foo1 :Welcome to the foo.bar.baz network
+:0:1 .< :foo.bar.baz 001 foo1 :Welcome to the foo.bar.baz network
|conn3
-0:1 .< :foo.bar.baz 002 foo1 :Your host is foo.bar.baz
-0:1 .< :foo.bar.baz 003 foo1 :This server was created Jan 1 2020
-0:1 .< :foo.bar.baz 004 foo1 foo.bar.baz ircserver-1.0 abc def ghi
-0:1 .< :foo.bar.baz 005 foo1 ABC=DEF GHI=JKL :are supported by this server
+:0:1 .< :foo.bar.baz 002 foo1 :Your host is foo.bar.baz
+:0:1 .< :foo.bar.baz 003 foo1 :This server was created Jan 1 2020
+:0:1 .< :foo.bar.baz 004 foo1 foo.bar.baz ircserver-1.0 abc def ghi
+:0:1 .< :foo.bar.baz 005 foo1 ABC=DEF GHI=JKL :are supported by this server
1 .$ isupport:ABC set to: [DEF]
1 .$ isupport:GHI set to: [JKL]
-0:1 .< :foo.bar.baz 005 foo1 MNO=PQR STU=VWX Y=Z :are supported by this server
+:0:1 .< :foo.bar.baz 005 foo1 MNO=PQR STU=VWX Y=Z :are supported by this server
1 .$ isupport:MNO set to: [PQR]
1 .$ isupport:STU set to: [VWX]
1 .$ isupport:Y set to: [Z]
-0:1 .< :foo.bar.baz 251 foo1 :There are 10 users and 1000 invisible on 5 servers
-0:1 .< :foo.bar.baz 252 foo1 7 :IRC Operators online
-0:1 .< :foo.bar.baz 253 foo1 4 :unknown connection(s)
-0:1 .< :foo.bar.baz 254 foo1 800 :channels formed
-0:1 .< :foo.bar.baz 255 foo1 :I have 100 clients and 1 serveres
-0:1 .< :foo.bar.baz 265 foo1 100 150 :Current local users 100, max 150
-0:1 .< :foo.bar.baz 266 foo1 1010 1050 :Current global users 1010, max 1050
-0:1 .< :foo.bar.baz 250 foo1 :Highest connection count: 151 (150 clients) (1080 connections received)
+:0:1 .< :foo.bar.baz 251 foo1 :There are 10 users and 1000 invisible on 5 servers
+:0:1 .< :foo.bar.baz 252 foo1 7 :IRC Operators online
+:0:1 .< :foo.bar.baz 253 foo1 4 :unknown connection(s)
+:0:1 .< :foo.bar.baz 254 foo1 800 :channels formed
+:0:1 .< :foo.bar.baz 255 foo1 :I have 100 clients and 1 serveres
+:0:1 .< :foo.bar.baz 265 foo1 100 150 :Current local users 100, max 150
+:0:1 .< :foo.bar.baz 266 foo1 1010 1050 :Current global users 1010, max 1050
+:0:1 .< :foo.bar.baz 250 foo1 :Highest connection count: 151 (150 clients) (1080 connections received)
# collect MOTD into a single output (rather than line-by-line)
-0:1 .< :foo.bar.baz 375 foo1 :- foo.bar.baz Message of the Day -
-0:1 .< :foo.bar.baz 372 foo1 :- Howdy! -
-0:1 .< :foo.bar.baz 372 foo1 :- Welcome! -
-0:1 .< :foo.bar.baz 372 foo1 :- (to this server) -
-0:1 .< :foo.bar.baz 376 foo1 :End of /MOTD command
+:0:1 .< :foo.bar.baz 375 foo1 :- foo.bar.baz Message of the Day -
+:0:1 .< :foo.bar.baz 372 foo1 :- Howdy! -
+:0:1 .< :foo.bar.baz 372 foo1 :- Welcome! -
+:0:1 .< :foo.bar.baz 372 foo1 :- (to this server) -
+:0:1 .< :foo.bar.baz 376 foo1 :End of /MOTD command
1 .$ motd set to:
1 .$ - Howdy! -
1 .$ - Welcome! -
1 .$ - (to this server) -
# collect user mode
-0:1 .< :foo1 MODE foo1 :+Ziw
+:0:1 .< :foo1 MODE foo1 :+Ziw
1 .$ users:me:modes set to: [+Ziw]
# handle bot query NOTICE
-0:1 .< :SaslServ!SaslServ@services.bar.baz NOTICE foo1 :Last login from ~foobarbaz@foo.bar.baz on Jan 1 22:00:00 2021 +0000.
+:0:1 .< :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.
|conn4
1 .# /window.reconnect
# test lack of implementation
-0:1 .< foo bar baz
+:0:1 .< foo bar baz
1 .!$ No handler implemented for: foo bar baz
# test recoverable 432
> /nick @foo
1 .> NICK :@foo
-0:1 .< :*.?.net 432 foo1 @foo :Erroneous nickname
+:0:1 .< :*.?.net 432 foo1 @foo :Erroneous nickname
1 .!$ nickname refused for bad format, keeping current one
# join channel, collect topic, residents; update me:user from JOIN message; ensure topic.who not affecting users DB
> /join #test
1 .> JOIN :#test
-0:1 .< :foo1!~foobarbaz@baz.bar.foo JOIN #test
+:0:1 .< :foo1!~foobarbaz@baz.bar.foo JOIN #test
1 .$ users:me:user set to: [~foobarbaz]
-0:1 .< :foo.bar.baz 332 foo1 #test :foo bar baz
+:0:1 .< :foo.bar.baz 332 foo1 #test :foo bar baz
1 .$ channels:#test:exits cleared
-0:1 .< :foo.bar.baz 333 foo1 #test bar!~bar@OLD.bar.bar 1234567890
+:0:1 .< :foo.bar.baz 333 foo1 #test bar!~bar@OLD.bar.bar 1234567890
1 .$ channels:#test:topic set to: [Topic(what='foo bar baz', who=NickUserHost(nick='bar', user='~bar', host='OLD.bar.bar'))]
4 .$ bar!~bar@OLD.bar.bar set topic: foo bar baz
-0:1 .< :foo.bar.baz 353 foo1 @ #test :foo1 @bar
+:0:1 .< :foo.bar.baz 353 foo1 @ #test :foo1 @bar
1 .$ users:1:nick set to: [?]
1 .$ users:1:nick set to: [bar]
-0:1 .< :foo.bar.baz 366 foo1 #test :End of /NAMES list.
+:0:1 .< :foo.bar.baz 366 foo1 #test :End of /NAMES list.
1 .$ channels:#test:user_ids set to:
1 .$ 1
1 .$ me
4 .$ residents: bar, foo1
# deliver PRIVMSG to channel window, update sender's user+host from metadata
-0:1 .< :bar!~bar@bar.bar PRIVMSG #test :hi there
+:0:1 .< :bar!~bar@bar.bar PRIVMSG #test :hi there
1 .$ users:1:user set to: [~bar]
1 .$ users:1:host set to: [bar.bar]
4 .< [bar] hi there
# check _changing_ TOPIC message is communicated to channel window, as long as either content or who change
-0:1 .< :bar!~bar@bar.bar TOPIC #test :foo bar baz
+:0:1 .< :bar!~bar@bar.bar TOPIC #test :foo bar baz
1 .$ 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
-0:1 .< :bar!~bar@bar.bar TOPIC #test :foo bar baz
-0:1 .< :bar!~bar@bar.bar TOPIC #test :abc def ghi
+:0:1 .< :bar!~bar@bar.bar TOPIC #test :foo bar baz
+:0:1 .< :bar!~bar@bar.bar TOPIC #test :abc def ghi
1 .$ 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
# process non-self channel JOIN
-0:1 .< :baz!~baz@baz.baz JOIN :#test
+:0:1 .< :baz!~baz@baz.baz JOIN :#test
1 .$ users:2:nick set to: [?]
1 .$ users:2:nick set to: [baz]
1 .$ users:2:user set to: [~baz]
# join second channel with partial residents identity to compare distribution of resident-specific messages
> /join #testtest
1 .> JOIN :#testtest
-0:1 .< :foo1!~foobarbaz@baz.bar.foo JOIN #testtest
-0:1 .< :foo.bar.baz 332 foo1 #testtest :baz bar foo
+:0:1 .< :foo1!~foobarbaz@baz.bar.foo JOIN #testtest
+:0:1 .< :foo.bar.baz 332 foo1 #testtest :baz bar foo
1 .$ channels:#testtest:exits cleared
-0:1 .< :foo.bar.baz 333 foo1 #testtest bar!~bar@OLD.bar.bar 1234567890
+:0:1 .< :foo.bar.baz 333 foo1 #testtest bar!~bar@OLD.bar.bar 1234567890
1 .$ channels:#testtest:topic set to: [Topic(what='baz bar foo', who=NickUserHost(nick='bar', user='~bar', host='OLD.bar.bar'))]
5 .$ bar!~bar@OLD.bar.bar set topic: baz bar foo
-0:1 .< :foo.bar.baz 353 foo1 @ #testtest :foo1 baz
-0:1 .< :foo.bar.baz 366 foo1 #testtest :End of /NAMES list.
+:0:1 .< :foo.bar.baz 353 foo1 @ #testtest :foo1 baz
+:0:1 .< :foo.bar.baz 366 foo1 #testtest :End of /NAMES list.
1 .$ channels:#testtest:user_ids set to:
1 .$ 2
1 .$ me
5 .$ residents: baz, foo1
# handle query window with known user
-0:1 .< :baz!~baz@baz.baz PRIVMSG foo1 :hi there
+:0:1 .< :baz!~baz@baz.baz PRIVMSG foo1 :hi there
6 .< [baz] hi there
> /privmsg baz hello, how is it going
1 .> PRIVMSG baz :hello, how is it going
6 .> [foo1] hello, how is it going
-0:1 .< :baz!~baz@baz.baz PRIVMSG foo1 :fine!
+:0:1 .< :baz!~baz@baz.baz PRIVMSG foo1 :fine!
6 .< [baz] fine!
# handle failure to query absent user
> /privmsg barbar hello!
1 .> PRIVMSG barbar :hello!
7 .> [foo1] hello!
-0:1 .< :*.?.net 401 foo1 barbar :No such nick/channel
+:0:1 .< :*.?.net 401 foo1 barbar :No such nick/channel
7 .!$ barbar not online
# handle non-self renaming
-0:1 .< :baz!~baz@baz.baz NICK :bazbaz
+:0:1 .< :baz!~baz@baz.baz NICK :bazbaz
1 .$ users:2:nick set to: [bazbaz]
4,5,6 .$ baz!~baz@baz.baz renames bazbaz
# handle non-self PART in one of two inhabited channels, preserve identity into re-JOIN
-0:1 .< :bazbaz!~baz@baz.baz PART :#test
+:0:1 .< :bazbaz!~baz@baz.baz PART :#test
1 .$ channels:#test:exits:2 set to: [P]
1 .$ channels:#test:user_ids set to:
1 .$ 1
1 .$ me
4 .$ bazbaz!~baz@baz.baz parts
1 .$ channels:#test:exits:2 cleared
-0:1 .< :bazbaz!~baz@baz.baz JOIN :#test
+:0:1 .< :bazbaz!~baz@baz.baz JOIN :#test
1 .$ channels:#test:user_ids set to:
1 .$ 1
1 .$ 2
4 .$ bazbaz!~baz@baz.baz joins
# handle non-self PART in only inhabited channel, lose identity, re-join as new identity
-0:1 .< :bar!~bar@bar.bar PART :#test
+:0:1 .< :bar!~bar@bar.bar PART :#test
1 .$ channels:#test:exits:1 set to: [P]
1 .$ channels:#test:user_ids set to:
1 .$ 2
4 .$ bar!~bar@bar.bar parts
1 .$ channels:#test:exits:1 cleared
1 .$ users:1 cleared
-0:1 .< :bar!~bar@bar.bar JOIN :#test
+:0:1 .< :bar!~bar@bar.bar JOIN :#test
1 .$ users:3:nick set to: [?]
1 .$ users:3:nick set to: [bar]
1 .$ users:3:user set to: [~bar]
4 .$ bar!~bar@bar.bar joins
# handle non-self QUIT
-0:1 .< :bazbaz!~baz@baz.baz QUIT :Client Quit
+:0:1 .< :bazbaz!~baz@baz.baz QUIT :Client Quit
1 .$ users:2:exit_msg set to: [QClient Quit]
6 .$ bazbaz!~baz@baz.baz quits: Client Quit
1 .$ channels:#test:exits:2 set to: [QClient Quit]
1 .$ users:2 cleared
# handle self-PART: clear channel, and its squatters
-0:1 .< :foo1!~foobarbaz@baz.bar.foo PART :#test
+:0:1 .< :foo1!~foobarbaz@baz.bar.foo PART :#test
1 .$ channels:#test:exits:me set to: [P]
1 .$ channels:#test:user_ids set to:
1 .$ 3
# handle /disconnect, clear all
> /disconnect
1 .> QUIT :ircplom says bye
-0:1 .< :foo1!~foobarbaz@baz.bar.foo QUIT :Client Quit
+:0:1 .< :foo1!~foobarbaz@baz.bar.foo QUIT :Client Quit
1 .$ users:me:exit_msg set to: [QClient Quit]
2,3,6,7 .$ foo1!~foobarbaz@baz.bar.foo quits: Client Quit
1 .$ channels:#testtest:exits:me set to: [QClient Quit]
1 .$ channels:#testtest:user_ids set to:
5 .$ foo1!~foobarbaz@baz.bar.foo quits: Client Quit
1 .$ channels:#testtest:exits:me cleared
-0:1 .< ERROR :Closing link: (~foobarbaz@baz.bar.foo) [Quit: ircplom says bye]
+:0:1 .< ERROR :Closing link: (~foobarbaz@baz.bar.foo) [Quit: ircplom says bye]
1 .$ connection_state set to: [Closing link: (~foobarbaz@baz.bar.foo) [Quit: ircplom says bye]]
repeat isupport-clear-in isupport-clear-out
1 .$ caps cleared
1:8 .> CAP LS :302
1:8 .> USER foo 0 * :foo
1:8 .> NICK :?foo
-1:8 .< :*.?.net 432 * ?foo :Erroneous nickname
+:1:8 .< :*.?.net 432 * ?foo :Erroneous nickname
repeat isupport-clear-in isupport-clear-out 8
8 .$ connection_state set to: []
, .$ DISCONNECTED
2:9 .> CAP LS :302
2:9 .> USER baz 0 * :baz
2:9 .> NICK :baz
-2: .< FAKE_IRC_CONN_ABORT_EXCEPTION
+<2 FAKE_IRC_CONN_ABORT_EXCEPTION
9 .$ connection_state set to: [broken: FAKE_IRC_CONN_ABORT_EXCEPTION]
repeat isupport-clear-in isupport-clear-out 9
9 .$ connection_state set to: []