Bind history to the recipient session and channel, not the name (closes #148)
check / check (push) Canceled after 0s

History was selected by msg_to, so whoever next held a released nick, or
created a deleted channel again under its old name, could read the
earlier holder's messages. Each message row now records the session a
DM or INVITE was sent to (to_session_id) and the channel a channel
message was sent to (to_channel_id), and GET /api/v1/history selects by
the one that applies, using its index. A session's DM history now also
follows it across nick changes. A DM or channel message whose recipient
is deleted before it is stored gets the unknown nick or channel error.

Model: opus-5-5
This commit is contained in:
2026-10-20 16:29:55 +00:00
parent 5100b9a665
commit 48d6308c22
11 changed files with 438 additions and 97 deletions
+111
View File
@@ -1654,9 +1654,25 @@ func TestHistoryUnknownBefore(t *testing.T) {
)
}
// History holds only PRIVMSG, so the id of the JOIN to
// #here is not in it.
var joinID string
queued, _ := tserver.pollMessages(token, 0)
for _, msg := range queued {
if msg[commandKey] == joinCmd && msg[toKey] == "#here" {
joinID, _ = msg["id"].(string)
}
}
if joinID == "" {
t.Fatal("expected a JOIN to #here in the queue")
}
cases := []struct{ name, before string }{
{"made-up id", "no-such-message"},
{"id from another channel", elsewhere[0].ID},
{"id of a JOIN", joinID},
}
for _, tc := range cases {
@@ -1671,6 +1687,101 @@ func TestHistoryUnknownBefore(t *testing.T) {
}
}
// TestHistoryReusedNick checks that a user who takes a nick
// released by QUIT does not get the DMs sent to its previous
// holder.
func TestHistoryReusedNick(t *testing.T) {
const victim = "victimV"
tserver := newTestServer(t)
senderToken := tserver.createSession("dmA")
victimToken := tserver.createSession(victim)
newcomerToken := tserver.createSession("newcomer")
tserver.sendCommand(senderToken, map[string]any{
commandKey: privmsgCmd,
toKey: victim,
bodyKey: []string{"for the first victimV only"},
})
_, msgs := tserver.getHistory(victimToken, victim, "", 10)
if len(msgs) != 1 {
t.Fatalf("expected 1 DM for %s, got %d", victim, len(msgs))
}
tserver.sendCommand(victimToken, map[string]any{
commandKey: irc.CmdQuit,
})
_, result := tserver.sendCommand(newcomerToken, map[string]any{
commandKey: irc.CmdNick,
bodyKey: []string{victim},
})
if result[nickKey] != victim {
t.Fatalf("expected nick %s, got %v", victim, result)
}
status, msgs := tserver.getHistory(newcomerToken, victim, "", 10)
if status != http.StatusOK {
t.Fatalf("expected 200, got %d", status)
}
if len(msgs) != 0 {
t.Fatalf("expected no history, got %v", msgs)
}
}
// TestHistoryRecreatedChannel checks that a channel created
// again under the name of a deleted one starts with no
// history, even when the old one had a key.
func TestHistoryRecreatedChannel(t *testing.T) {
const channel = "#priv"
tserver := newTestServer(t)
ownerToken := tserver.createSession("roomR")
strangerToken := tserver.createSession("stranger")
tserver.sendCommand(ownerToken, map[string]any{
commandKey: joinCmd, toKey: channel,
})
tserver.sendCommand(ownerToken, map[string]any{
commandKey: irc.CmdMode,
toKey: channel,
bodyKey: []string{"+k", "topsecret"},
})
tserver.sendCommand(ownerToken, map[string]any{
commandKey: privmsgCmd,
toKey: channel,
bodyKey: []string{"members only"},
})
_, msgs := tserver.getHistory(ownerToken, channel, "", 10)
if len(msgs) != 1 {
t.Fatalf("expected 1 message, got %d", len(msgs))
}
// The last member leaving deletes the channel.
tserver.sendCommand(ownerToken, map[string]any{
commandKey: irc.CmdPart, toKey: channel,
})
_, result := tserver.sendCommand(strangerToken, map[string]any{
commandKey: joinCmd, toKey: channel,
})
if result[statusKey] != joinedStatus {
t.Fatalf("expected the join to succeed, got %v", result)
}
status, msgs := tserver.getHistory(strangerToken, channel, "", 10)
if status != http.StatusOK {
t.Fatalf("expected 200, got %d", status)
}
if len(msgs) != 0 {
t.Fatalf("expected no history, got %v", msgs)
}
}
func TestChannelList(t *testing.T) {
tserver := newTestServer(t)
token := tserver.createSession("lister")