From e827bc0f9cc8e2ebf63f791bdea02eeff0e92bee Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Mon, 20 Jul 2020 04:28:17 -0400 Subject: [PATCH 1/4] fix #1205 --- irc/handlers.go | 7 ++++++- irc/history/queries.go | 3 +-- irc/znc.go | 46 +++++++++++++++++++++++++++++++++++++++--- 3 files changed, 50 insertions(+), 6 deletions(-) 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/znc.go b/irc/znc.go index d6698e84..aae95892 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,13 +83,25 @@ 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] @@ -123,6 +141,9 @@ func zncPlaybackHandler(client *Client, command string, params []string, rb *Res if params[1] == "*" { zncPlayPrivmsgs(client, rb, "*", start, end) + } else if params[1] == "*self" { + zncPlayPrivmsgs(client, rb, "*", start, end) + targets = make(StringSet) // XXX non-nil but empty channel set means "no channels" } else { targets = make(StringSet) for _, targetName := range strings.Split(targetString, ",") { @@ -169,3 +190,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)) + } + } +} From 562b85c1c0c0845fd2c8dfba3fc8918d3e85cb8a Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Mon, 20 Jul 2020 13:45:52 -0400 Subject: [PATCH 2/4] simplify some logic --- irc/znc.go | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/irc/znc.go b/irc/znc.go index aae95892..68e9d9d2 100644 --- a/irc/znc.go +++ b/irc/znc.go @@ -139,15 +139,15 @@ func zncPlaybackPlayHandler(client *Client, command string, params []string, rb // 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) - } else if params[1] == "*self" { - zncPlayPrivmsgs(client, rb, "*", start, end) - targets = make(StringSet) // XXX non-nil but empty channel set means "no channels" + 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) } @@ -159,6 +159,10 @@ func zncPlaybackPlayHandler(client *Client, command string, params []string, rb } } + if playPrivmsgs { + zncPlayPrivmsgs(client, rb, "*", start, end) + } + rb.session.zncPlaybackTimes = &zncPlaybackTimes{ start: start, end: end, From e6e55bbf2976480a622011998ced3bb83f74cd9c Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Tue, 21 Jul 2020 15:26:03 -0400 Subject: [PATCH 3/4] remove 'history' batch type This was from ircv3-specifications #362, which is now obsolete. --- irc/responsebuffer.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) 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 != "" { From af009a5bc177e16ecd162a6838e50a8bea9e6157 Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Tue, 21 Jul 2020 16:33:17 -0400 Subject: [PATCH 4/4] fix 2-parameter case for *playback --- irc/channel.go | 5 ++++- irc/znc.go | 2 ++ 2 files changed, 6 insertions(+), 1 deletion(-) 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/znc.go b/irc/znc.go index 68e9d9d2..95bd800f 100644 --- a/irc/znc.go +++ b/irc/znc.go @@ -108,6 +108,8 @@ func zncPlaybackPlayHandler(client *Client, command string, params []string, rb 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