Relay INVITE to IRC listener users as an INVITE line (closes #147)
check / check (push) Waiting to run

An invite for an IRC listener user arrived as a server NOTICE that did
not name the inviter, so no IRC client treated it as an invite. The
relay now sends it as an INVITE line from the inviter, with the channel
taken from the message body. The IRC listener now queues the INVITE
with the channel as its body, as the HTTP API already did, so an invite
from either transport has the same shape. The README's IRC listener
section says how an invite arrives.

Model: opus-5-5
This commit is contained in:
2026-10-21 04:17:05 +00:00
parent 3cbdac7bda
commit 882bab2e1c
4 changed files with 48 additions and 8 deletions
+3 -1
View File
@@ -2719,7 +2719,9 @@ Each line of a multi-line `body` reaches an IRC listener user as its own
is sent, so it cannot start a line of its own. A mode change made over the HTTP
API reaches IRC listener members as an IRC `MODE` line with its value as a
separate parameter, e.g. `MODE #general +k secretpass` or
`MODE #general +o bob`.
`MODE #general +o bob`. An `INVITE` from either transport reaches the invited
IRC listener user as an IRC `INVITE` line from the inviter, e.g.
`:alice!alice@* INVITE bob #general`.
### Docker Usage
+3 -4
View File
@@ -1261,10 +1261,9 @@ func (c *Conn) handleInvite(
irc.RplInviting, targetNick, channel,
)
// Send INVITE notice to target via service fan-out.
body, _ := json.Marshal( //nolint:errchkjson
[]string{"You have been invited to " + channel},
)
// Queue the INVITE to the target with the body the HTTP
// API uses.
body, _ := json.Marshal([]string{channel}) //nolint:errchkjson
_, _, _ = c.svc.FanOut( //nolint:dogsled // fire-and-forget broadcast
ctx, "INVITE", c.nick, targetNick, targetID, 0,
+36
View File
@@ -1373,6 +1373,42 @@ func TestIntegrationInviteUserOnChannel(t *testing.T) {
)
}
// TestIntegrationInviteLine checks that an IRC listener user
// invited by an IRC listener user, and then over the HTTP
// API, gets an INVITE line from the inviter for the channel
// each time.
func TestIntegrationInviteLine(t *testing.T) {
t.Parallel()
env := newTestEnv(t)
alice := env.dial(t)
alice.register("alice")
alice.joinAndDrain("#inv")
bob := env.dial(t)
bob.register("bob")
alice.sendAndExpect("INVITE bob #inv", " 341 ")
got := bob.readLine()
want := ":alice!alice@* INVITE bob #inv"
if got != want {
t.Errorf("from the IRC listener: got %q, want %q", got, want)
}
// The HTTP API queues an INVITE with the channel as its body.
env.queueMessage(t, "bob", "INVITE", "carol", "bob", []string{"#inv"})
got = bob.readLine()
want = ":carol!carol@* INVITE bob #inv"
if got != want {
t.Errorf("over the HTTP API: got %q, want %q", got, want)
}
}
// TestIntegrationModeSecretList checks that LIST shows a +s
// channel only to its members.
func TestIntegrationModeSecretList(t *testing.T) {
+6 -3
View File
@@ -358,16 +358,19 @@ func (c *Conn) deliverKickMsg(
}
}
// deliverInviteMsg sends an INVITE notification.
// deliverInviteMsg sends an INVITE line from the inviter
// for the channel named in text.
func (c *Conn) deliverInviteMsg(
_ *db.IRCMessage,
msg *db.IRCMessage,
text string,
) {
c.mu.Lock()
nick := c.nick
c.mu.Unlock()
c.sendFromServer("NOTICE", nick, text)
prefix := msg.From + "!" + msg.From + "@*"
c.send(FormatMessage(prefix, "INVITE", nick, text))
}
// deliverWallops sends a WALLOPS notification.