1 Commits
Author SHA1 Message Date
sneak 882bab2e1c Relay INVITE to IRC listener users as an INVITE line (closes #147)
check / check (push) Canceled after 0s
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
2026-10-21 04:17:05 +00:00
3 changed files with 6 additions and 57 deletions
+1 -6
View File
@@ -359,16 +359,11 @@ func (c *Conn) deliverKickMsg(
}
// deliverInviteMsg sends an INVITE line from the inviter
// for the channel named in text, or nothing when text names
// no channel.
// for the channel named in text.
func (c *Conn) deliverInviteMsg(
msg *db.IRCMessage,
text string,
) {
if text == "" {
return
}
c.mu.Lock()
nick := c.nick
c.mu.Unlock()
-32
View File
@@ -211,38 +211,6 @@ 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
+5 -19
View File
@@ -300,14 +300,11 @@ function App() {
nickRef.current = nick;
}, [nick]);
// Persist joined channels, once logged in: before then only the
// Server tab exists, and writing would erase the saved channels
// that onLogin rejoins.
// Persist joined channels.
useEffect(() => {
if (!loggedIn) return;
const channels = tabs.filter((t) => t.type === "channel").map((t) => t.name);
localStorage.setItem("neoirc_channels", JSON.stringify(channels));
}, [loggedIn, tabs]);
}, [tabs]);
// Clear unread on tab switch.
useEffect(() => {
@@ -466,15 +463,6 @@ function App() {
break;
}
case "MODE": {
// The body is the change, with its value where it has one:
// ["+o bob"], ["+m"], ["+k", "secretpass"].
const modes = Array.isArray(msg.body) ? msg.body.join(" ") : "";
const text = `${msg.from} sets mode ${modes} on ${msg.to}`;
if (msg.to) addMessage(msg.to, { ...base, text, system: true });
break;
}
case 353: {
// RPL_NAMREPLY — each name is a hostmask (nick!user@host),
// prefixed with @ for an operator or + for a voiced member.
@@ -771,11 +759,6 @@ function App() {
const onLogin = useCallback(
async (userNick, isResumed) => {
// Read before setLoggedIn(true): from then on the effect under
// "Persist joined channels" writes the open channel tabs over it.
const saved = JSON.parse(
localStorage.getItem("neoirc_channels") || "[]",
);
setNick(userNick);
setLoggedIn(true);
addSystemMessage("Server", `Connected as ${userNick}`);
@@ -800,6 +783,9 @@ function App() {
}
// Fresh session — join any previously saved channels.
const saved = JSON.parse(
localStorage.getItem("neoirc_channels") || "[]",
);
for (const ch of saved) {
try {
await api("/messages", {