fix: use client nick for synthetic JOINs and own-nick rewriting
irssi (and other IRC clients) only open a channel window when they see a JOIN from their own nick. The synthetic JOINs were using the network nick (e.g. pagumowa) but the client registered as tester -- mismatch. Three changes: - Synthetic JOIN prefix is now client_nick!user@bouncer - 001 welcome uses the client's registered nick - encode_nick/encode_message accept client_nick param to rewrite own nicks from any network to the client's nick, so irssi recognizes all self-actions consistently Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -56,6 +56,11 @@ class Client:
|
|||||||
self._pass_raw: str = ""
|
self._pass_raw: str = ""
|
||||||
self._addr = writer.get_extra_info("peername", ("?", 0))
|
self._addr = writer.get_extra_info("peername", ("?", 0))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def nick(self) -> str:
|
||||||
|
"""The nick the client registered with."""
|
||||||
|
return self._nick
|
||||||
|
|
||||||
async def handle(self) -> None:
|
async def handle(self) -> None:
|
||||||
"""Main client session loop."""
|
"""Main client session loop."""
|
||||||
log.info("client connected from %s", self._addr)
|
log.info("client connected from %s", self._addr)
|
||||||
@@ -161,9 +166,8 @@ class Client:
|
|||||||
async def _send_welcome(self) -> None:
|
async def _send_welcome(self) -> None:
|
||||||
"""Send IRC welcome sequence and channel state from all networks."""
|
"""Send IRC welcome sequence and channel state from all networks."""
|
||||||
server_name = "bouncer"
|
server_name = "bouncer"
|
||||||
# Pick a representative nick for the welcome numerics
|
nick = self._nick
|
||||||
own_nicks = self._router.get_own_nicks()
|
self_prefix = f"{nick}!{self._user}@bouncer"
|
||||||
nick = next(iter(own_nicks.values()), self._nick)
|
|
||||||
|
|
||||||
networks = ", ".join(self._router.network_names())
|
networks = ", ".join(self._router.network_names())
|
||||||
self._send_msg(IRCMessage(
|
self._send_msg(IRCMessage(
|
||||||
@@ -185,15 +189,14 @@ class Client:
|
|||||||
|
|
||||||
# Send namespaced channel state from every network
|
# Send namespaced channel state from every network
|
||||||
for net_name, network in sorted(self._router.networks.items()):
|
for net_name, network in sorted(self._router.networks.items()):
|
||||||
net_nick = network.nick
|
|
||||||
for channel in sorted(network.channels):
|
for channel in sorted(network.channels):
|
||||||
ns_channel = encode_channel(channel, net_name)
|
ns_channel = encode_channel(channel, net_name)
|
||||||
|
|
||||||
# Synthetic JOIN so the client knows we're in this channel
|
# Synthetic JOIN from client's own nick so irssi opens the window
|
||||||
self._send_msg(IRCMessage(
|
self._send_msg(IRCMessage(
|
||||||
command="JOIN",
|
command="JOIN",
|
||||||
params=[ns_channel],
|
params=[ns_channel],
|
||||||
prefix=net_nick,
|
prefix=self_prefix,
|
||||||
))
|
))
|
||||||
|
|
||||||
# Topic
|
# Topic
|
||||||
|
|||||||
@@ -28,17 +28,31 @@ def decode_channel(namespaced: str) -> tuple[str, str | None]:
|
|||||||
return namespaced[:idx], namespaced[idx + 1:]
|
return namespaced[:idx], namespaced[idx + 1:]
|
||||||
|
|
||||||
|
|
||||||
def encode_nick(nick: str, network: str, own_nicks: dict[str, str]) -> str:
|
def encode_nick(
|
||||||
"""Suffix a nick unless it belongs to us on *any* network."""
|
nick: str,
|
||||||
|
network: str,
|
||||||
|
own_nicks: dict[str, str],
|
||||||
|
client_nick: str | None = None,
|
||||||
|
) -> str:
|
||||||
|
"""Suffix a nick unless it belongs to us on *any* network.
|
||||||
|
|
||||||
|
If *client_nick* is set, own nicks are rewritten to match the client's
|
||||||
|
registered nick so IRC clients recognise them as "self".
|
||||||
|
"""
|
||||||
if nick in own_nicks.values():
|
if nick in own_nicks.values():
|
||||||
return nick
|
return client_nick if client_nick else nick
|
||||||
return f"{nick}{SEPARATOR}{network}"
|
return f"{nick}{SEPARATOR}{network}"
|
||||||
|
|
||||||
|
|
||||||
def encode_prefix(prefix: str, network: str, own_nicks: dict[str, str]) -> str:
|
def encode_prefix(
|
||||||
|
prefix: str,
|
||||||
|
network: str,
|
||||||
|
own_nicks: dict[str, str],
|
||||||
|
client_nick: str | None = None,
|
||||||
|
) -> str:
|
||||||
"""Namespace the nick portion of ``nick!user@host``."""
|
"""Namespace the nick portion of ``nick!user@host``."""
|
||||||
nick, user, host = parse_prefix(prefix)
|
nick, user, host = parse_prefix(prefix)
|
||||||
enc = encode_nick(nick, network, own_nicks)
|
enc = encode_nick(nick, network, own_nicks, client_nick=client_nick)
|
||||||
if user and host:
|
if user and host:
|
||||||
return f"{enc}!{user}@{host}"
|
return f"{enc}!{user}@{host}"
|
||||||
if user:
|
if user:
|
||||||
@@ -66,16 +80,22 @@ def _is_channel(target: str) -> bool:
|
|||||||
return target.startswith(("#", "&", "+", "!"))
|
return target.startswith(("#", "&", "+", "!"))
|
||||||
|
|
||||||
|
|
||||||
def encode_message(msg: IRCMessage, network: str, own_nicks: dict[str, str]) -> IRCMessage:
|
def encode_message(
|
||||||
|
msg: IRCMessage,
|
||||||
|
network: str,
|
||||||
|
own_nicks: dict[str, str],
|
||||||
|
client_nick: str | None = None,
|
||||||
|
) -> IRCMessage:
|
||||||
"""Namespace an entire IRC message for delivery to a client.
|
"""Namespace an entire IRC message for delivery to a client.
|
||||||
|
|
||||||
- Channels in params get ``/network`` suffix
|
- Channels in params get ``/network`` suffix
|
||||||
- Prefix nick gets ``/network`` suffix (unless it's our own)
|
- Prefix nick gets ``/network`` suffix (unless it's our own)
|
||||||
|
- Own nicks rewritten to *client_nick* when set
|
||||||
- Special handling for 353 (NAMREPLY nick list) and NICK (new nick in trailing)
|
- Special handling for 353 (NAMREPLY nick list) and NICK (new nick in trailing)
|
||||||
"""
|
"""
|
||||||
prefix = msg.prefix
|
prefix = msg.prefix
|
||||||
if prefix:
|
if prefix:
|
||||||
prefix = encode_prefix(prefix, network, own_nicks)
|
prefix = encode_prefix(prefix, network, own_nicks, client_nick=client_nick)
|
||||||
|
|
||||||
params = list(msg.params)
|
params = list(msg.params)
|
||||||
|
|
||||||
@@ -91,13 +111,15 @@ def encode_message(msg: IRCMessage, network: str, own_nicks: dict[str, str]) ->
|
|||||||
while bare and bare[0] in "@+%~&":
|
while bare and bare[0] in "@+%~&":
|
||||||
mode_prefix += bare[0]
|
mode_prefix += bare[0]
|
||||||
bare = bare[1:]
|
bare = bare[1:]
|
||||||
encoded.append(mode_prefix + encode_nick(bare, network, own_nicks))
|
encoded.append(mode_prefix + encode_nick(
|
||||||
|
bare, network, own_nicks, client_nick=client_nick,
|
||||||
|
))
|
||||||
params[3] = " ".join(encoded)
|
params[3] = " ".join(encoded)
|
||||||
|
|
||||||
elif msg.command == "NICK" and params:
|
elif msg.command == "NICK" and params:
|
||||||
# NICK: params[0] is the new nick
|
# NICK: params[0] is the new nick
|
||||||
new_nick = params[0]
|
new_nick = params[0]
|
||||||
params[0] = encode_nick(new_nick, network, own_nicks)
|
params[0] = encode_nick(new_nick, network, own_nicks, client_nick=client_nick)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# Generic: namespace channel-like params[0]
|
# Generic: namespace channel-like params[0]
|
||||||
|
|||||||
@@ -176,13 +176,14 @@ class Router:
|
|||||||
if max_msgs > 0:
|
if max_msgs > 0:
|
||||||
await self.backlog.prune(network_name, keep=max_msgs)
|
await self.backlog.prune(network_name, keep=max_msgs)
|
||||||
|
|
||||||
# Namespace and forward to all clients
|
# Namespace and forward to all clients (per-client: own nicks -> client nick)
|
||||||
own_nicks = self.get_own_nicks()
|
own_nicks = self.get_own_nicks()
|
||||||
namespaced = encode_message(msg, network_name, own_nicks)
|
|
||||||
data = namespaced.format()
|
|
||||||
for client in self.clients:
|
for client in self.clients:
|
||||||
try:
|
try:
|
||||||
client.write(data)
|
namespaced = encode_message(
|
||||||
|
msg, network_name, own_nicks, client_nick=client.nick,
|
||||||
|
)
|
||||||
|
client.write(namespaced.format())
|
||||||
except Exception:
|
except Exception:
|
||||||
log.exception("failed to write to client")
|
log.exception("failed to write to client")
|
||||||
|
|
||||||
@@ -203,7 +204,9 @@ class Router:
|
|||||||
params=[entry.target, entry.content],
|
params=[entry.target, entry.content],
|
||||||
prefix=entry.sender,
|
prefix=entry.sender,
|
||||||
)
|
)
|
||||||
namespaced = encode_message(msg, network_name, own_nicks)
|
namespaced = encode_message(
|
||||||
|
msg, network_name, own_nicks, client_nick=client.nick,
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
client.write(namespaced.format())
|
client.write(namespaced.format())
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|||||||
@@ -44,6 +44,12 @@ class TestEncodeNick:
|
|||||||
# Own nick on another network still shown bare
|
# Own nick on another network still shown bare
|
||||||
assert encode_nick("mybot2", "libera", OWN) == "mybot2"
|
assert encode_nick("mybot2", "libera", OWN) == "mybot2"
|
||||||
|
|
||||||
|
def test_own_nick_rewritten_to_client_nick(self):
|
||||||
|
assert encode_nick("mybot", "libera", OWN, client_nick="tester") == "tester"
|
||||||
|
|
||||||
|
def test_foreign_nick_unaffected_by_client_nick(self):
|
||||||
|
assert encode_nick("user123", "libera", OWN, client_nick="tester") == "user123/libera"
|
||||||
|
|
||||||
|
|
||||||
class TestEncodePrefix:
|
class TestEncodePrefix:
|
||||||
def test_full_prefix(self):
|
def test_full_prefix(self):
|
||||||
@@ -141,3 +147,22 @@ class TestEncodeMessage:
|
|||||||
)
|
)
|
||||||
out = encode_message(msg, "libera", OWN)
|
out = encode_message(msg, "libera", OWN)
|
||||||
assert out.raw is None # raw must not carry over
|
assert out.raw is None # raw must not carry over
|
||||||
|
|
||||||
|
def test_own_prefix_rewritten_to_client_nick(self):
|
||||||
|
msg = IRCMessage(
|
||||||
|
command="PRIVMSG",
|
||||||
|
params=["#test", "hello"],
|
||||||
|
prefix="mybot!ident@host",
|
||||||
|
)
|
||||||
|
out = encode_message(msg, "libera", OWN, client_nick="tester")
|
||||||
|
assert out.prefix == "tester!ident@host"
|
||||||
|
|
||||||
|
def test_namreply_own_nick_rewritten(self):
|
||||||
|
msg = IRCMessage(
|
||||||
|
command="353",
|
||||||
|
params=["mybot", "=", "#test", "@op mybot"],
|
||||||
|
)
|
||||||
|
out = encode_message(msg, "libera", OWN, client_nick="tester")
|
||||||
|
names = out.params[3].split()
|
||||||
|
assert names[0] == "@op/libera"
|
||||||
|
assert names[1] == "tester"
|
||||||
|
|||||||
Reference in New Issue
Block a user