diff --git a/README.md b/README.md index 7a31347..600fd51 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/internal/ircserver/commands.go b/internal/ircserver/commands.go index 77d1eb3..359a2df 100644 --- a/internal/ircserver/commands.go +++ b/internal/ircserver/commands.go @@ -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, diff --git a/internal/ircserver/integration_test.go b/internal/ircserver/integration_test.go index 2dde41f..db4b4f2 100644 --- a/internal/ircserver/integration_test.go +++ b/internal/ircserver/integration_test.go @@ -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) { diff --git a/internal/ircserver/relay.go b/internal/ircserver/relay.go index bf88c2e..96125a0 100644 --- a/internal/ircserver/relay.go +++ b/internal/ircserver/relay.go @@ -358,16 +358,24 @@ func (c *Conn) deliverKickMsg( } } -// deliverInviteMsg sends an INVITE notification. +// deliverInviteMsg sends an INVITE line from the inviter +// for the channel named in text, or nothing when text names +// no channel. func (c *Conn) deliverInviteMsg( - _ *db.IRCMessage, + msg *db.IRCMessage, text string, ) { + if text == "" { + return + } + 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. diff --git a/internal/ircserver/relay_test.go b/internal/ircserver/relay_test.go index 2b6a89e..c0d4b22 100644 --- a/internal/ircserver/relay_test.go +++ b/internal/ircserver/relay_test.go @@ -211,6 +211,38 @@ func TestRelayModeParameters(t *testing.T) { } } +// TestRelayInviteWithoutChannel checks that an INVITE whose +// body names no channel sends the invited IRC listener user +// no INVITE line. +func TestRelayInviteWithoutChannel(t *testing.T) { + t.Parallel() + + env := newTestEnv(t) + + bob := env.dial(t) + bob.register("bob") + + // A nil body is stored as JSON null, which is not an array. + for _, body := range [][]string{nil, {}, {""}} { + env.queueMessage(t, "bob", "INVITE", "carol", "bob", body) + } + + // The relay sends a user's queue in order, so this arrives + // after anything sent for the INVITEs above. + env.queueMessage(t, "bob", "PRIVMSG", "carol", "bob", + []string{"the end"}) + + lines := bob.readUntil(func(l string) bool { + return strings.HasSuffix(l, " :the end") + }) + + for _, line := range lines { + if strings.Contains(line, " INVITE ") { + t.Errorf("got %q, want no INVITE line", line) + } + } +} + // TestRelayLineBreaks checks that a CR, LF or NUL in a stored // body line, key or mask never reaches an IRC listener user // as a line break, so no extra line arrives, and that a body