Bind history to the recipient session and channel, not the name (closes #148)
check / check (push) Waiting to run
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:
+36
-22
@@ -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 {
|
||||
|
||||
@@ -164,7 +164,7 @@ func TestFanOut(t *testing.T) {
|
||||
body, _ := json.Marshal([]string{"hello"}) //nolint:errchkjson
|
||||
|
||||
dbID, uuid, err := env.svc.FanOut(
|
||||
ctx, irc.CmdPrivmsg, "alice", "#test",
|
||||
ctx, irc.CmdPrivmsg, "alice", "#test", 0, 0,
|
||||
nil, body, nil,
|
||||
[]int64{sid1, sid2},
|
||||
)
|
||||
@@ -339,6 +339,78 @@ func TestBroadcastQuit(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestSendDirectMessageTargetQuits checks that a DM whose
|
||||
// target quits after its nick is looked up gets the same
|
||||
// error as a DM to an unknown nick. The trigger deletes the
|
||||
// target's session just before the DM is stored.
|
||||
func TestSendDirectMessageTargetQuits(t *testing.T) {
|
||||
env := newTestEnv(t)
|
||||
ctx := t.Context()
|
||||
|
||||
sid := createSession(ctx, t, env.db, "alice")
|
||||
createSession(ctx, t, env.db, "bob")
|
||||
|
||||
_, err := env.db.GetDB().ExecContext(ctx,
|
||||
`CREATE TRIGGER delete_recipient
|
||||
BEFORE INSERT ON messages
|
||||
BEGIN
|
||||
DELETE FROM sessions WHERE id = NEW.to_session_id;
|
||||
END`)
|
||||
if err != nil {
|
||||
t.Fatalf("create trigger: %v", err)
|
||||
}
|
||||
|
||||
body, _ := json.Marshal([]string{"hi"}) //nolint:errchkjson
|
||||
|
||||
_, err = env.svc.SendDirectMessage(
|
||||
ctx, sid, "alice", irc.CmdPrivmsg, "bob", body, nil,
|
||||
)
|
||||
|
||||
var ircErr *service.IRCError
|
||||
if !errors.As(err, &ircErr) || ircErr.Code != irc.ErrNoSuchNick {
|
||||
t.Fatalf("want ERR_NOSUCHNICK, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSendChannelMessageChannelDeleted checks that a channel
|
||||
// message whose channel is deleted after it is looked up gets
|
||||
// the same error as a message to an unknown channel. The
|
||||
// trigger deletes the channel just before the message is
|
||||
// stored.
|
||||
func TestSendChannelMessageChannelDeleted(t *testing.T) {
|
||||
env := newTestEnv(t)
|
||||
ctx := t.Context()
|
||||
|
||||
sid := createSession(ctx, t, env.db, "alice")
|
||||
|
||||
_, err := env.svc.JoinChannel(ctx, sid, "alice", "#gone", "")
|
||||
if err != nil {
|
||||
t.Fatalf("join alice: %v", err)
|
||||
}
|
||||
|
||||
_, err = env.db.GetDB().ExecContext(ctx,
|
||||
`CREATE TRIGGER delete_recipient
|
||||
BEFORE INSERT ON messages
|
||||
BEGIN
|
||||
DELETE FROM channels WHERE id = NEW.to_channel_id;
|
||||
END`)
|
||||
if err != nil {
|
||||
t.Fatalf("create trigger: %v", err)
|
||||
}
|
||||
|
||||
body, _ := json.Marshal([]string{"hi"}) //nolint:errchkjson
|
||||
|
||||
_, _, err = env.svc.SendChannelMessage(
|
||||
ctx, sid, "alice", irc.CmdPrivmsg, "#gone", body, nil,
|
||||
)
|
||||
|
||||
var ircErr *service.IRCError
|
||||
if !errors.As(err, &ircErr) ||
|
||||
ircErr.Code != irc.ErrNoSuchChannel {
|
||||
t.Fatalf("want ERR_NOSUCHCHANNEL, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendChannelMessage_Moderated(t *testing.T) {
|
||||
env := newTestEnv(t)
|
||||
ctx := t.Context()
|
||||
|
||||
Reference in New Issue
Block a user