Bind history to the recipient session and channel, not the name (closes #148)
check / check (push) Waiting to run

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 was merged in pull request #161.
This commit is contained in:
2026-10-20 19:48:35 +02:00
parent 5100b9a665
commit 164fc93a48
11 changed files with 569 additions and 135 deletions
+36 -22
View File
@@ -311,7 +311,7 @@ func (s *Service) SendWallops(
}
_, _, err = s.FanOut(
ctx, irc.CmdWallops, nick, "*",
ctx, irc.CmdWallops, nick, "*", 0, 0,
nil, body, nil, recipients,
)
if err != nil {
@@ -344,15 +344,19 @@ type DirectMsgResult struct {
}
// FanOut inserts a message and enqueues it to all given
// session IDs, notifying each via the broker.
// session IDs, notifying each via the broker. toSessionID
// and toChannelID are the recipient InsertMessage records
// for history, 0 when there is none.
func (s *Service) FanOut(
ctx context.Context,
command, from, to string,
toSessionID, toChannelID int64,
params, body, meta json.RawMessage,
sessionIDs []int64,
) (int64, string, error) {
dbID, msgUUID, err := s.db.InsertMessage(
ctx, command, from, to, params, body, meta,
ctx, command, from, to,
toSessionID, toChannelID, params, body, meta,
)
if err != nil {
return 0, "", fmt.Errorf("insert message: %w", err)
@@ -396,13 +400,13 @@ func (s *Service) SendChannelMessage(
nick, command, channel string,
body, meta json.RawMessage,
) (int64, string, error) {
noSuchChannel := &IRCError{
irc.ErrNoSuchChannel, []string{channel}, msgNoSuchChannel,
}
chID, err := s.db.GetChannelByName(ctx, channel)
if err != nil {
return 0, "", &IRCError{
irc.ErrNoSuchChannel,
[]string{channel},
msgNoSuchChannel,
}
return 0, "", noSuchChannel
}
if command == irc.CmdPrivmsg {
@@ -459,9 +463,14 @@ func (s *Service) SendChannelMessage(
recipients := excludeSession(memberIDs, sessionID)
dbID, uuid, fanErr := s.FanOut(
ctx, command, nick, channel,
ctx, command, nick, channel, 0, chID,
nil, body, meta, recipients,
)
// The channel was deleted after the lookup above.
if db.IsForeignKeyConstraintError(fanErr) {
return 0, "", noSuchChannel
}
if fanErr != nil {
return 0, "", fanErr
}
@@ -478,13 +487,13 @@ func (s *Service) SendDirectMessage(
nick, command, target string,
body, meta json.RawMessage,
) (*DirectMsgResult, error) {
noSuchNick := &IRCError{
irc.ErrNoSuchNick, []string{target}, "No such nick",
}
targetSID, err := s.db.GetSessionByNick(ctx, target)
if err != nil {
return nil, &IRCError{
irc.ErrNoSuchNick,
[]string{target},
"No such nick",
}
return nil, noSuchNick
}
away, _ := s.db.GetAway(ctx, targetSID)
@@ -495,9 +504,14 @@ func (s *Service) SendDirectMessage(
}
_, uuid, fanErr := s.FanOut(
ctx, command, nick, target,
ctx, command, nick, target, targetSID, 0,
nil, body, meta, recipients,
)
// The target's session was deleted after the lookup above.
if db.IsForeignKeyConstraintError(fanErr) {
return nil, noSuchNick
}
if fanErr != nil {
return nil, fanErr
}
@@ -561,7 +575,7 @@ func (s *Service) JoinChannel(
body, _ := json.Marshal([]string{channel}) //nolint:errchkjson
_, _, _ = s.FanOut( //nolint:dogsled // fire-and-forget broadcast
ctx, irc.CmdJoin, nick, channel,
ctx, irc.CmdJoin, nick, channel, 0, chID,
nil, body, nil, memberIDs,
)
@@ -604,7 +618,7 @@ func (s *Service) PartChannel(
body, _ := json.Marshal([]string{reason}) //nolint:errchkjson
_, _, _ = s.FanOut( //nolint:dogsled // fire-and-forget broadcast
ctx, irc.CmdPart, nick, channel,
ctx, irc.CmdPart, nick, channel, 0, chID,
nil, body, nil, recipients,
)
@@ -666,7 +680,7 @@ func (s *Service) SetTopic(
body, _ := json.Marshal([]string{topic}) //nolint:errchkjson
_, _, _ = s.FanOut( //nolint:dogsled // fire-and-forget broadcast
ctx, irc.CmdTopic, nick, channel,
ctx, irc.CmdTopic, nick, channel, 0, chID,
nil, body, nil, memberIDs,
)
@@ -730,7 +744,7 @@ func (s *Service) KickUser(
)
_, _, _ = s.FanOut( //nolint:dogsled // fire-and-forget broadcast
ctx, irc.CmdKick, nick, channel,
ctx, irc.CmdKick, nick, channel, 0, chID,
params, body, nil, memberIDs,
)
@@ -886,7 +900,7 @@ func (s *Service) BroadcastQuit(
body, _ := json.Marshal([]string{reason}) //nolint:errchkjson
_, _, _ = s.FanOut(
ctx, irc.CmdQuit, nick, "",
ctx, irc.CmdQuit, nick, "", 0, 0,
nil, body, nil, recipients,
)
}
@@ -1074,7 +1088,7 @@ func (s *Service) BroadcastMode(
body, _ := json.Marshal([]string{modeText}) //nolint:errchkjson
_, _, _ = s.FanOut( //nolint:dogsled // fire-and-forget broadcast
ctx, irc.CmdMode, nick, channel,
ctx, irc.CmdMode, nick, channel, 0, chID,
nil, body, nil, memberIDs,
)
}
@@ -1351,7 +1365,7 @@ func (s *Service) broadcastNickChange(
notified := make(map[int64]bool)
dbID, _, insErr := s.db.InsertMessage(
ctx, irc.CmdNick, oldNick, "",
ctx, irc.CmdNick, oldNick, "", 0, 0,
nil, body, nil,
)
if insErr != nil {