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 -24
View File
@@ -566,7 +566,7 @@ func (hdlr *Handlers) enqueueNumeric(
}
dbID, _, insertErr := hdlr.params.Database.InsertMessage(
ctx, command, hdlr.serverName(), nick,
ctx, command, hdlr.serverName(), nick, 0, 0,
paramsJSON, json.RawMessage(body), nil,
)
if insertErr != nil {
@@ -652,7 +652,7 @@ func (hdlr *Handlers) initChannelState(
dbID, _, insErr := hdlr.params.Database.
InsertMessage(
ctx, "JOIN", nick, chanInfo.Name,
nil, nil, nil,
0, chanInfo.ID, nil, nil, nil,
)
if insErr != nil {
hdlr.log.Error(
@@ -2404,7 +2404,7 @@ func (hdlr *Handlers) executeBanChange(
)
if mErr == nil {
_ = hdlr.fanOutSilent(
request, irc.CmdMode, nick, channel,
request, irc.CmdMode, nick, channel, 0, chID,
json.RawMessage(modeBody), memberIDs,
)
}
@@ -2522,7 +2522,7 @@ func (hdlr *Handlers) setChannelKeyMode(
modeBody, mErr := json.Marshal([]string{"+k", key})
if mErr == nil {
_ = hdlr.fanOutSilent(
request, irc.CmdMode, nick, channel,
request, irc.CmdMode, nick, channel, 0, chID,
json.RawMessage(modeBody), memberIDs,
)
}
@@ -2571,7 +2571,7 @@ func (hdlr *Handlers) clearChannelKeyMode(
modeBody, mErr := json.Marshal([]string{"-k", "*"})
if mErr == nil {
_ = hdlr.fanOutSilent(
request, irc.CmdMode, nick, channel,
request, irc.CmdMode, nick, channel, 0, chID,
json.RawMessage(modeBody), memberIDs,
)
}
@@ -2646,7 +2646,7 @@ func (hdlr *Handlers) setChannelLimitMode(
)
if mErr == nil {
_ = hdlr.fanOutSilent(
request, irc.CmdMode, nick, channel,
request, irc.CmdMode, nick, channel, 0, chID,
json.RawMessage(modeBody), memberIDs,
)
}
@@ -2696,7 +2696,7 @@ func (hdlr *Handlers) clearChannelLimitMode(
modeBody, mErr := json.Marshal([]string{"-l"})
if mErr == nil {
_ = hdlr.fanOutSilent(
request, irc.CmdMode, nick, channel,
request, irc.CmdMode, nick, channel, 0, chID,
json.RawMessage(modeBody), memberIDs,
)
}
@@ -2755,7 +2755,7 @@ func (hdlr *Handlers) handleInvite(
if mErr == nil {
_ = hdlr.fanOutSilent(
request, irc.CmdInvite, nick, targetNick,
json.RawMessage(invBody),
targetSID, 0, json.RawMessage(invBody),
[]int64{targetSID},
)
}
@@ -3250,9 +3250,10 @@ func (hdlr *Handlers) HandleGetHistory() http.HandlerFunc {
return
}
if !hdlr.canAccessHistory(
toSessionID, toChannelID, ok := hdlr.canAccessHistory(
writer, request, sessionID, nick, target,
) {
)
if !ok {
return
}
@@ -3264,7 +3265,7 @@ func (hdlr *Handlers) HandleGetHistory() http.HandlerFunc {
}
msgs, err := hdlr.params.Database.GetHistory(
request.Context(), target,
request.Context(), toSessionID, toChannelID,
request.URL.Query().Get("before"), limit,
)
if errors.Is(err, sql.ErrNoRows) {
@@ -3297,17 +3298,23 @@ func (hdlr *Handlers) HandleGetHistory() http.HandlerFunc {
}
// canAccessHistory verifies the user can read history
// for the given target (channel or DM participant).
// for the given target (channel or DM participant) and
// returns the session id and channel id to pass to
// GetHistory: the user's own session for its nick, or the
// channel's id, with 0 for the other. On false it has
// written the error response.
func (hdlr *Handlers) canAccessHistory(
writer http.ResponseWriter,
request *http.Request,
sessionID int64,
nick, target string,
) bool {
) (int64, int64, bool) {
if strings.HasPrefix(target, "#") {
return hdlr.canAccessChannelHistory(
chID, ok := hdlr.canAccessChannelHistory(
writer, request, sessionID, target,
)
return 0, chID, ok
}
// DM history: only allow if the target is the
@@ -3319,18 +3326,20 @@ func (hdlr *Handlers) canAccessHistory(
http.StatusForbidden,
)
return false
return 0, 0, false
}
return true
return sessionID, 0, true
}
// canAccessChannelHistory returns the id of the channel
// named target if the user is a member of it.
func (hdlr *Handlers) canAccessChannelHistory(
writer http.ResponseWriter,
request *http.Request,
sessionID int64,
target string,
) bool {
) (int64, bool) {
chID, err := hdlr.params.Database.GetChannelByName(
request.Context(), target,
)
@@ -3341,7 +3350,7 @@ func (hdlr *Handlers) canAccessChannelHistory(
http.StatusNotFound,
)
return false
return 0, false
}
isMember, err := hdlr.params.Database.IsChannelMember(
@@ -3357,7 +3366,7 @@ func (hdlr *Handlers) canAccessChannelHistory(
http.StatusInternalServerError,
)
return false
return 0, false
}
if !isMember {
@@ -3367,10 +3376,10 @@ func (hdlr *Handlers) canAccessChannelHistory(
http.StatusForbidden,
)
return false
return 0, false
}
return true
return chID, true
}
// HandleLogout deletes the authenticated client's token
@@ -3679,17 +3688,19 @@ func (hdlr *Handlers) deliverWhoisIdle(
}
// fanOut inserts a message and enqueues it to all given
// sessions.
// sessions. toSessionID and toChannelID are the recipient
// InsertMessage records for history, 0 when there is none.
func (hdlr *Handlers) fanOut(
request *http.Request,
command, from, target string,
toSessionID, toChannelID int64,
body json.RawMessage,
meta json.RawMessage,
sessionIDs []int64,
) (string, error) {
dbID, msgUUID, err := hdlr.params.Database.InsertMessage(
request.Context(), command, from, target,
nil, body, meta,
toSessionID, toChannelID, nil, body, meta,
)
if err != nil {
return "", fmt.Errorf("insert message: %w", err)
@@ -3714,12 +3725,13 @@ func (hdlr *Handlers) fanOut(
func (hdlr *Handlers) fanOutSilent(
request *http.Request,
command, from, target string,
toSessionID, toChannelID int64,
body json.RawMessage,
sessionIDs []int64,
) error {
_, err := hdlr.fanOut(
request, command, from, target,
body, nil, sessionIDs,
toSessionID, toChannelID, body, nil, sessionIDs,
)
return err