diff --git a/irc/channel.go b/irc/channel.go index 4d66ee60..30ab0db5 100644 --- a/irc/channel.go +++ b/irc/channel.go @@ -813,16 +813,19 @@ func (channel *Channel) autoReplayHistory(client *Client, rb *ResponseBuffer, sk // autoreplay any messages as necessary var items []history.Item + hasAutoreplayTimestamps := false var start, end time.Time if rb.session.zncPlaybackTimes.ValidFor(channel.NameCasefolded()) { + hasAutoreplayTimestamps = true start, end = rb.session.zncPlaybackTimes.start, rb.session.zncPlaybackTimes.end } else if !rb.session.autoreplayMissedSince.IsZero() { // we already checked for history caps in `playReattachMessages` + hasAutoreplayTimestamps = true start = time.Now().UTC() end = rb.session.autoreplayMissedSince } - if !start.IsZero() || !end.IsZero() { + if hasAutoreplayTimestamps { _, seq, _ := channel.server.GetHistorySequence(channel, client, "") if seq != nil { zncMax := channel.server.Config().History.ZNCMax diff --git a/irc/handlers.go b/irc/handlers.go index 28e576a7..48391eb8 100644 --- a/irc/handlers.go +++ b/irc/handlers.go @@ -3131,7 +3131,12 @@ func whowasHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re // ZNC [params] func zncHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool { - zncModuleHandler(client, msg.Params[0], msg.Params[1:], rb) + params := msg.Params[1:] + // #1205: compatibility with Palaver, which sends `ZNC *playback :play ...` + if len(params) == 1 && strings.IndexByte(params[0], ' ') != -1 { + params = strings.Fields(params[0]) + } + zncModuleHandler(client, msg.Params[0], params, rb) return false } diff --git a/irc/history/queries.go b/irc/history/queries.go index 771a1df0..078e7270 100644 --- a/irc/history/queries.go +++ b/irc/history/queries.go @@ -7,8 +7,7 @@ import ( "time" ) -// Selector represents a parameter to a CHATHISTORY command; -// at most one of Msgid or Time may be nonzero +// Selector represents a parameter to a CHATHISTORY command type Selector struct { Msgid string Time time.Time diff --git a/irc/responsebuffer.go b/irc/responsebuffer.go index 9387a380..4dde2050 100644 --- a/irc/responsebuffer.go +++ b/irc/responsebuffer.go @@ -25,7 +25,7 @@ const ( type ResponseBuffer struct { Label string // label if this is a labeled response batch batchID string // ID of the labeled response batch, if one has been initiated - batchType string // type of the labeled response batch (possibly `history` or `chathistory`) + batchType string // type of the labeled response batch (currently either `labeled-response` or `chathistory`) // stack of batch IDs of nested batches, which are handled separately // from the underlying labeled-response batch. starting a new nested batch @@ -200,9 +200,7 @@ func (rb *ResponseBuffer) EndNestedBatch(batchID string) { // supported by the client (`history`, `chathistory`, or no batch, in descending order). func (rb *ResponseBuffer) StartNestedHistoryBatch(params ...string) (batchID string) { var batchType string - if rb.session.capabilities.Has(caps.EventPlayback) { - batchType = "history" - } else if rb.session.capabilities.Has(caps.Batch) { + if rb.session.capabilities.Has(caps.Batch) { batchType = "chathistory" } if batchType != "" { diff --git a/irc/znc.go b/irc/znc.go index d6698e84..95bd800f 100644 --- a/irc/znc.go +++ b/irc/znc.go @@ -53,6 +53,12 @@ func zncWireTimeToTime(str string) (result time.Time) { return time.Unix(seconds, int64(fraction*1000000000)).UTC() } +func timeToZncWireTime(t time.Time) (result string) { + secs := t.Unix() + nano := t.UnixNano() - (secs * 1000000000) + return fmt.Sprintf("%d.%d", secs, nano) +} + type zncPlaybackTimes struct { start time.Time end time.Time @@ -77,19 +83,33 @@ func (z *zncPlaybackTimes) ValidFor(target string) bool { } // https://wiki.znc.in/Playback +func zncPlaybackHandler(client *Client, command string, params []string, rb *ResponseBuffer) { + if len(params) == 0 { + return + } + switch strings.ToLower(params[0]) { + case "play": + zncPlaybackPlayHandler(client, command, params, rb) + case "list": + zncPlaybackListHandler(client, command, params, rb) + default: + return + } +} + // PRIVMSG *playback :play [lower_bound] [upper_bound] // e.g., PRIVMSG *playback :play * 1558374442 -func zncPlaybackHandler(client *Client, command string, params []string, rb *ResponseBuffer) { +func zncPlaybackPlayHandler(client *Client, command string, params []string, rb *ResponseBuffer) { if len(params) < 2 || len(params) > 4 { return - } else if strings.ToLower(params[0]) != "play" { - return } targetString := params[1] now := time.Now().UTC() var start, end time.Time switch len(params) { + case 2: + // #1205: this should have the same semantics as `LATEST *` case 3: // #831: this should have the same semantics as `LATEST timestamp=qux`, // or equivalently `BETWEEN timestamp=$now timestamp=qux`, as opposed to @@ -121,12 +141,15 @@ func zncPlaybackHandler(client *Client, command string, params []string, rb *Res // 3.3 When the client sends a subsequent redundant JOIN line for those // channels; redundant JOIN is a complete no-op so we won't replay twice + playPrivmsgs := false if params[1] == "*" { - zncPlayPrivmsgs(client, rb, "*", start, end) + playPrivmsgs = true // XXX nil `targets` means "every channel" } else { targets = make(StringSet) for _, targetName := range strings.Split(targetString, ",") { - if strings.HasPrefix(targetName, "#") { + if targetName == "*self" { + playPrivmsgs = true + } else if strings.HasPrefix(targetName, "#") { if cfTarget, err := CasefoldChannel(targetName); err == nil { targets.Add(cfTarget) } @@ -138,6 +161,10 @@ func zncPlaybackHandler(client *Client, command string, params []string, rb *Res } } + if playPrivmsgs { + zncPlayPrivmsgs(client, rb, "*", start, end) + } + rb.session.zncPlaybackTimes = &zncPlaybackTimes{ start: start, end: end, @@ -169,3 +196,22 @@ func zncPlayPrivmsgs(client *Client, rb *ResponseBuffer, target string, after, b client.replayPrivmsgHistory(rb, items, "", true) } } + +// PRIVMSG *playback :list +func zncPlaybackListHandler(client *Client, command string, params []string, rb *ResponseBuffer) { + nick := client.Nick() + for _, channel := range client.Channels() { + _, sequence, err := client.server.GetHistorySequence(channel, client, "") + if err != nil { + client.server.logger.Error("internal", "couldn't get history sequence for ZNC list", err.Error()) + continue + } + items, _, err := sequence.Between(history.Selector{}, history.Selector{}, 1) // i.e., LATEST * 1 + if err != nil { + client.server.logger.Error("internal", "couldn't query history for ZNC list", err.Error()) + } else if len(items) != 0 { + stamp := timeToZncWireTime(items[0].Message.Time) + rb.Add(nil, "*playback!znc@znc.in", "PRIVMSG", nick, fmt.Sprintf("%s 0 %s", channel.Name(), stamp)) + } + } +}