diff --git a/irc/caps/constants.go b/irc/caps/constants.go index 2b6a38e4..9eb2624d 100644 --- a/irc/caps/constants.go +++ b/irc/caps/constants.go @@ -63,8 +63,9 @@ const ( // BOT mode: https://ircv3.net/specs/extensions/bot-mode BotTagName = "bot" // https://ircv3.net/specs/extensions/chathistory - ChathistoryTargetsBatchType = "draft/chathistory-targets" - ExtendedISupportBatchType = "draft/isupport" + ChathistoryTargetsBatchType = "draft/chathistory-targets" + ExtendedISupportBatchType = "draft/isupport" + ChathistoryEndOfPaginationTag = "draft/chathistory-end" ) func init() { diff --git a/irc/channel.go b/irc/channel.go index e745c5a2..60ed4a52 100644 --- a/irc/channel.go +++ b/irc/channel.go @@ -979,7 +979,7 @@ func (channel *Channel) autoReplayHistory(client *Client, rb *ResponseBuffer, sk } } if 0 < numItems { - channel.replayHistoryItems(rb, items, false) + channel.replayHistoryItems(rb, items, false, false) rb.Flush(true) } } @@ -1069,7 +1069,7 @@ func (channel *Channel) Part(client *Client, message string, rb *ResponseBuffer) client.server.logger.Debug("channels", fmt.Sprintf("%s left channel %s", details.nick, chname)) } -func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.Item, chathistoryCommand bool) { +func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.Item, chathistoryCommand, endOfPagination bool) { // send an empty batch if necessary, as per the CHATHISTORY spec chname := channel.Name() client := rb.target @@ -1089,7 +1089,11 @@ func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.I } } - batchID := rb.StartNestedBatch("chathistory", chname) + var batchTags map[string]string + if chathistoryCommand && endOfPagination { + batchTags = endOfPaginationTag + } + batchID := rb.StartNestedBatch(batchTags, "chathistory", chname) defer rb.EndNestedBatch(batchID) for _, item := range items { diff --git a/irc/client.go b/irc/client.go index bf150d82..913f4ffd 100644 --- a/irc/client.go +++ b/irc/client.go @@ -939,14 +939,18 @@ func (session *Session) Ping() { session.Send(nil, "", "PING", session.client.Nick()) } -func (client *Client) replayPrivmsgHistory(rb *ResponseBuffer, items []history.Item, target string, chathistoryCommand bool) { +func (client *Client) replayPrivmsgHistory(rb *ResponseBuffer, items []history.Item, target string, chathistoryCommand, endOfPagination bool) { var batchID string details := client.Details() nick := details.nick if target == "" { target = nick } - batchID = rb.StartNestedBatch("chathistory", target) + var batchTags map[string]string + if chathistoryCommand && endOfPagination { + batchTags = endOfPaginationTag + } + batchID = rb.StartNestedBatch(batchTags, "chathistory", target) isSelfMessage := func(item *history.Item) bool { // XXX: Params[0] is the message target. if the source of this message is an in-memory diff --git a/irc/handlers.go b/irc/handlers.go index b7e174cb..92baa845 100644 --- a/irc/handlers.go +++ b/irc/handlers.go @@ -38,6 +38,12 @@ import ( "github.com/ergochat/ergo/irc/webpush" ) +var ( + endOfPaginationTag = map[string]string{ + caps.ChathistoryEndOfPaginationTag: "", + } +) + // helper function to parse ACC callbacks, e.g., mailto:person@example.com, tel:16505551234 func parseCallback(spec string, config *Config) (callbackNamespace string, callbackValue string, err error) { // XXX if we don't require verification, ignore any callback that was passed here @@ -702,6 +708,7 @@ func chathistoryHandler(server *Server, client *Client, msg ircmsg.Message, rb * var err error var disabled, listTargets bool var targets []history.TargetListing + var endOfPagination bool defer func() { // errors are sent either without a batch, or in a draft/labeled-response batch as usual if disabled { @@ -715,7 +722,11 @@ func chathistoryHandler(server *Server, client *Client, msg ircmsg.Message, rb * } else { // successful responses are sent as a chathistory or history batch if listTargets { - batchID := rb.StartNestedBatch(caps.ChathistoryTargetsBatchType) + var batchTags map[string]string + if endOfPagination { + batchTags = endOfPaginationTag + } + batchID := rb.StartNestedBatch(batchTags, caps.ChathistoryTargetsBatchType) defer rb.EndNestedBatch(batchID) for _, target := range targets { name := server.UnfoldName(target.CfName) @@ -723,9 +734,9 @@ func chathistoryHandler(server *Server, client *Client, msg ircmsg.Message, rb * target.Time.Format(utils.IRCv3TimestampFormat)) } } else if channel != nil { - channel.replayHistoryItems(rb, items, true) + channel.replayHistoryItems(rb, items, true, endOfPagination) } else { - client.replayPrivmsgHistory(rb, items, target, true) + client.replayPrivmsgHistory(rb, items, target, true, endOfPagination) } } }() @@ -850,6 +861,12 @@ func chathistoryHandler(server *Server, client *Client, msg ircmsg.Message, rb * return } targets, err = client.listTargets(start.Time, end.Time, limit) + // adding the end-of-pagination tag is best effort; it's OK if we omit it + // in the edge case where we're at the end of the window but we coincidentally + // filled the whole limit (the client will just incur an additional roundtrip + // to page one more time). in contrast, a false positive would be problematic + // because the client would stop paging. + endOfPagination = (err == nil) && len(targets) < limit } else { channel, sequence, err = server.GetHistorySequence(nil, client, target) if err != nil || sequence == nil { @@ -860,6 +877,7 @@ func chathistoryHandler(server *Server, client *Client, msg ircmsg.Message, rb * } else { items, err = sequence.Between(start, end, limit) } + endOfPagination = (err == nil) && len(items) < limit } return } @@ -1246,9 +1264,9 @@ func historyHandler(server *Server, client *Client, msg ircmsg.Message, rb *Resp if len(items) != 0 { if channel != nil { - channel.replayHistoryItems(rb, items, true) + channel.replayHistoryItems(rb, items, true, false) } else { - client.replayPrivmsgHistory(rb, items, "", true) + client.replayPrivmsgHistory(rb, items, "", true, false) } } return false @@ -3285,7 +3303,7 @@ func metadataRegisteredHandler(client *Client, config *Config, subcommand string return } - batchId := rb.StartNestedBatch("metadata", target) + batchId := rb.StartNestedBatch(nil, "metadata", target) defer rb.EndNestedBatch(batchId) for _, key := range params[2:] { @@ -3429,7 +3447,7 @@ func metadataSubsHandler(client *Client, subcommand string, params []string, rb subs := rb.session.MetadataSubscriptions() - batchID := rb.StartNestedBatch("metadata-subs") + batchID := rb.StartNestedBatch(nil, "metadata-subs") defer rb.EndNestedBatch(batchID) chunked := utils.ChunkifyParams(maps.Keys(subs), lineLength) diff --git a/irc/metadata.go b/irc/metadata.go index 80b0b063..737bf705 100644 --- a/irc/metadata.go +++ b/irc/metadata.go @@ -67,7 +67,7 @@ func broadcastMetadataUpdate(server *Server, sessions iter.Seq[*Session], origin } func syncClientMetadata(server *Server, rb *ResponseBuffer, target *Client) { - batchId := rb.StartNestedBatch("metadata", target.Nick()) + batchId := rb.StartNestedBatch(nil, "metadata", target.Nick()) defer rb.EndNestedBatch(batchId) subs := rb.session.MetadataSubscriptions() @@ -81,7 +81,7 @@ func syncClientMetadata(server *Server, rb *ResponseBuffer, target *Client) { } func syncChannelMetadata(server *Server, rb *ResponseBuffer, channel *Channel) { - batchId := rb.StartNestedBatch("metadata", channel.Name()) + batchId := rb.StartNestedBatch(nil, "metadata", channel.Name()) defer rb.EndNestedBatch(batchId) subs := rb.session.MetadataSubscriptions() @@ -107,7 +107,7 @@ func syncChannelMetadata(server *Server, rb *ResponseBuffer, channel *Channel) { } func playMetadataList(rb *ResponseBuffer, nick, target string, values map[string]string) { - batchId := rb.StartNestedBatch("metadata", target) + batchId := rb.StartNestedBatch(nil, "metadata", target) defer rb.EndNestedBatch(batchId) for key, val := range values { @@ -117,7 +117,7 @@ func playMetadataList(rb *ResponseBuffer, nick, target string, values map[string } func playMetadataVerbBatch(rb *ResponseBuffer, target string, values map[string]string) { - batchId := rb.StartNestedBatch("metadata", target) + batchId := rb.StartNestedBatch(nil, "metadata", target) defer rb.EndNestedBatch(batchId) for key, val := range values { diff --git a/irc/responsebuffer.go b/irc/responsebuffer.go index 6dfeade8..cb59db89 100644 --- a/irc/responsebuffer.go +++ b/irc/responsebuffer.go @@ -192,7 +192,7 @@ func (rb *ResponseBuffer) sendBatchEnd(blocking bool) { // Starts a nested batch (see the ResponseBuffer struct definition for a description of // how this works) -func (rb *ResponseBuffer) StartNestedBatch(batchType string, params ...string) (batchID string) { +func (rb *ResponseBuffer) StartNestedBatch(tags map[string]string, batchType string, params ...string) (batchID string) { if !rb.session.capabilities.Has(caps.Batch) { return } @@ -201,7 +201,7 @@ func (rb *ResponseBuffer) StartNestedBatch(batchType string, params ...string) ( msgParams[0] = "+" + batchID msgParams[1] = batchType copy(msgParams[2:], params) - rb.AddMessage(ircmsg.MakeMessage(nil, rb.target.server.name, "BATCH", msgParams...)) + rb.AddMessage(ircmsg.MakeMessage(tags, rb.target.server.name, "BATCH", msgParams...)) rb.nestedBatches = append(rb.nestedBatches, batchID) return } diff --git a/irc/server.go b/irc/server.go index e81c0373..adc77018 100644 --- a/irc/server.go +++ b/irc/server.go @@ -535,7 +535,7 @@ func (server *Server) RplISupport(client *Client, rb *ResponseBuffer) { func (server *Server) sendRplISupportLines(client *Client, rb *ResponseBuffer, lines [][]string) { if rb.session.capabilities.Has(caps.ExtendedISupport) { - batchID := rb.StartNestedBatch(caps.ExtendedISupportBatchType) + batchID := rb.StartNestedBatch(nil, caps.ExtendedISupportBatchType) defer rb.EndNestedBatch(batchID) } finalText := "are supported by this server" diff --git a/irc/znc.go b/irc/znc.go index 27c4df2c..71bc49e6 100644 --- a/irc/znc.go +++ b/irc/znc.go @@ -203,7 +203,7 @@ func zncPlayPrivmsgsFrom(client *Client, rb *ResponseBuffer, target string, star zncMax := client.server.Config().History.ZNCMax items, err := sequence.Between(history.Selector{Time: start}, history.Selector{Time: end}, zncMax) if err == nil && len(items) != 0 { - client.replayPrivmsgHistory(rb, items, target, false) + client.replayPrivmsgHistory(rb, items, target, false, false) } } @@ -211,7 +211,7 @@ func zncPlayPrivmsgsFromAll(client *Client, rb *ResponseBuffer, start, end time. zncMax := client.server.Config().History.ZNCMax items, err := client.privmsgsBetween(start, end, maxDMTargetsForAutoplay, zncMax) if err == nil && len(items) != 0 { - client.replayPrivmsgHistory(rb, items, "", false) + client.replayPrivmsgHistory(rb, items, "", false, false) } } diff --git a/irctest b/irctest index 17fac53c..c85b5747 160000 --- a/irctest +++ b/irctest @@ -1 +1 @@ -Subproject commit 17fac53c5cdfe78caecb601399512574f242cc85 +Subproject commit c85b574765d67a0270d9c7b35f37ab57e7f95774