1 Commits
Author SHA1 Message Date
sneak d90962c960 Make an IRC listener connection follow its session's nick (closes #184)
check / check (push) Waiting to run
When the session's nick was changed from another client, such as the
web client over the HTTP API, the IRC listener connection kept the old
nick and sent its messages, joins and later NICK changes under it. Now,
whenever the relay passes on a NICK line, the connection first takes
the nick its session holds in the database. It does not go by the
line's old nick, as the connection may have taken that nick after
another user gave it up. As the relay goroutine can now change the
nick, the command handlers read it under the connection's lock,
through currentNick, as the relay already did.

Model: opus-5-5
2026-10-21 14:04:22 +00:00
3 changed files with 72 additions and 8 deletions
+19
View File
@@ -264,6 +264,25 @@ func (database *Database) GetSessionByNick(
return sessionID, nil
}
// GetSessionNick returns the nick a session holds.
func (database *Database) GetSessionNick(
ctx context.Context,
sessionID int64,
) (string, error) {
var nick string
err := database.conn.QueryRowContext(
ctx,
"SELECT nick FROM sessions WHERE id = ?",
sessionID,
).Scan(&nick)
if err != nil {
return "", fmt.Errorf("get session nick: %w", err)
}
return nick, nil
}
// SessionHostInfo holds the username, hostname, and IP
// for a session.
type SessionHostInfo struct {
+13 -8
View File
@@ -92,7 +92,7 @@ func (c *Conn) drainQueue(ctx context.Context) bool {
//
//nolint:cyclop // dispatch table
func (c *Conn) deliverIRCMessage(
_ context.Context,
ctx context.Context,
msg *db.IRCMessage,
) {
command := msg.Command
@@ -120,7 +120,7 @@ func (c *Conn) deliverIRCMessage(
case command == irc.CmdPart:
c.deliverPart(msg, text)
case command == irc.CmdNick:
c.deliverNickChange(msg, text)
c.deliverNickChange(ctx, msg, text)
case command == irc.CmdQuit:
c.deliverQuitMsg(msg, text)
case command == irc.CmdTopic:
@@ -230,18 +230,23 @@ func (c *Conn) deliverPart(msg *db.IRCMessage, text string) {
}
}
// deliverNickChange sends a NICK change notification. When
// the old nick is the connection's own, the change was made
// from another client of its session, as nicks are unique,
// and the connection takes the new nick too.
// deliverNickChange sends a NICK change notification. The
// change may be its own session's, made from another client,
// so the connection first takes the nick its session holds
// now. It does not go by the line's old nick: the connection
// may have taken that nick after another user gave it up. It
// reads the nick under c.mu, so that a NICK sent on the
// connection meanwhile is not undone.
func (c *Conn) deliverNickChange(
ctx context.Context,
msg *db.IRCMessage,
newNick string,
) {
c.mu.Lock()
if msg.From == c.nick {
c.nick = newNick
nick, err := c.database.GetSessionNick(ctx, c.sessionID)
if err == nil {
c.nick = nick
}
c.mu.Unlock()
+40
View File
@@ -655,3 +655,43 @@ func TestRelayNickChangeFromHTTPAPI(t *testing.T) {
}
}
}
// TestRelayNickChangeKeepsTakenNick checks that an IRC
// listener connection which takes a nick another user has
// given up keeps it when that user's change reaches it
// afterwards: what it then sends goes out under the nick its
// session holds.
func TestRelayNickChangeKeepsTakenNick(t *testing.T) {
t.Parallel()
env := newTestEnv(t)
bob := env.dial(t)
bob.register("bob")
bob.sendAndExpect("NICK bob2", " NICK bob2")
alice := env.dial(t)
alice.register("alice")
alice.sendAndExpect("NICK bob", " NICK bob")
alice.joinAndDrain("#chan")
bob.joinAndDrain("#chan")
// Bob's change reaches alice only now, as it does when she
// takes his old nick before her connection has passed the
// change on. She holds bob now, so it is queued to bob.
env.queueMessage(t, "bob", "NICK", "bob", "", []string{"bob2"})
alice.readUntil(func(l string) bool {
return l == ":bob!bob@* NICK bob2"
})
alice.send("PRIVMSG #chan :hi there")
const want = ":bob!bob@* PRIVMSG #chan :hi there"
got := bob.readLine()
if got != want {
t.Errorf("got %q, want %q", got, want)
}
}