3
0
mirror of https://github.com/ergochat/ergo.git synced 2024-11-25 13:29:27 +01:00
ergo/irc/znc.go

233 lines
7.1 KiB
Go
Raw Normal View History

2019-05-21 01:08:57 +02:00
// Copyright (c) 2019 Shivaram Lingamneni <slingamn@cs.stanford.edu>
// released under the MIT license
package irc
import (
"fmt"
"strconv"
"strings"
"time"
2021-05-25 06:34:38 +02:00
"github.com/ergochat/ergo/irc/history"
"github.com/ergochat/ergo/irc/utils"
2019-05-21 01:08:57 +02:00
)
const (
// #829, also see "Case 2" in the "three cases" below:
zncPlaybackCommandExpiration = time.Second * 30
zncPrefix = "*playback!znc@znc.in"
maxDMTargetsForAutoplay = 128
)
2019-05-21 01:08:57 +02:00
type zncCommandHandler func(client *Client, command string, params []string, rb *ResponseBuffer)
var zncHandlers = map[string]zncCommandHandler{
"*playback": zncPlaybackHandler,
}
func zncPrivmsgHandler(client *Client, command string, privmsg string, rb *ResponseBuffer) {
zncModuleHandler(client, command, strings.Fields(privmsg), rb)
}
func zncModuleHandler(client *Client, command string, params []string, rb *ResponseBuffer) {
command = strings.ToLower(command)
if subHandler, ok := zncHandlers[command]; ok {
subHandler(client, command, params, rb)
} else {
2019-05-21 02:08:06 +02:00
nick := rb.target.Nick()
rb.Add(nil, client.server.name, "NOTICE", nick, fmt.Sprintf(client.t("Oragono does not emulate the ZNC module %s"), command))
rb.Add(nil, "*status!znc@znc.in", "NOTICE", nick, fmt.Sprintf(client.t("No such module [%s]"), command))
2019-05-21 01:08:57 +02:00
}
}
// "number of seconds (floating point for millisecond precision) elapsed since January 1, 1970"
func zncWireTimeToTime(str string) (result time.Time) {
var secondsPortion, fracPortion string
dot := strings.IndexByte(str, '.')
if dot == -1 {
secondsPortion = str
} else {
secondsPortion = str[:dot]
fracPortion = str[dot+1:]
2019-05-21 01:08:57 +02:00
}
seconds, _ := strconv.ParseInt(secondsPortion, 10, 64)
// truncate to nanosecond resolution if necessary
if len(fracPortion) > 9 {
fracPortion = fracPortion[:9]
}
fracSeconds, _ := strconv.ParseInt(fracPortion, 10, 64)
for i := 0; i < (9 - len(fracPortion)); i++ {
fracSeconds *= 10
}
return time.Unix(seconds, fracSeconds).UTC()
2019-05-21 01:08:57 +02:00
}
2020-07-20 10:28:17 +02:00
func timeToZncWireTime(t time.Time) (result string) {
secs := t.Unix()
nano := t.UnixNano() - (secs * 1000000000)
return fmt.Sprintf("%d.%d", secs, nano)
}
2019-05-21 01:08:57 +02:00
type zncPlaybackTimes struct {
2020-02-28 01:07:49 +01:00
start time.Time
end time.Time
2022-03-30 06:44:51 +02:00
targets utils.HashSet[string] // nil for "*" (everything), otherwise the channel names
setAt time.Time
}
func (z *zncPlaybackTimes) ValidFor(target string) bool {
if z == nil {
return false
}
if time.Now().Sub(z.setAt) > zncPlaybackCommandExpiration {
return false
}
if z.targets == nil {
return true
}
return z.targets.Has(target)
2019-05-21 01:08:57 +02:00
}
// https://wiki.znc.in/Playback
2020-07-20 10:28:17 +02:00
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
}
}
2019-05-21 01:08:57 +02:00
// PRIVMSG *playback :play <target> [lower_bound] [upper_bound]
// e.g., PRIVMSG *playback :play * 1558374442
2020-07-20 10:28:17 +02:00
func zncPlaybackPlayHandler(client *Client, command string, params []string, rb *ResponseBuffer) {
2020-02-28 01:07:49 +01:00
if len(params) < 2 || len(params) > 4 {
2019-05-21 01:08:57 +02:00
return
}
targetString := params[1]
2020-02-28 01:07:49 +01:00
now := time.Now().UTC()
var start, end time.Time
switch len(params) {
2020-07-21 22:33:17 +02:00
case 2:
// #1205: this should have the same semantics as `LATEST *`
2020-02-28 01:07:49 +01:00
case 3:
// #831: this should have the same semantics as `LATEST timestamp=qux`,
// or equivalently `BETWEEN timestamp=$now timestamp=qux`, as opposed to
// `AFTER timestamp=qux` (this matters in the case where there are
// more than znc-maxmessages available)
start = now
end = zncWireTimeToTime(params[2])
case 4:
start = zncWireTimeToTime(params[2])
end = zncWireTimeToTime(params[3])
2019-05-21 01:08:57 +02:00
}
2022-03-30 06:44:51 +02:00
var targets utils.HashSet[string]
var nickTargets []string
2019-05-21 01:08:57 +02:00
2019-05-30 01:23:46 +02:00
// three cases:
// 1. the user's PMs get played back immediately upon receiving this
// 2. if this is a new connection (from the server's POV), save the information
2020-01-29 21:45:50 +01:00
// and use it to process subsequent joins. (This is the Textual behavior:
// first send the playback PRIVMSG, then send the JOIN lines.)
2019-05-30 01:23:46 +02:00
// 3. if this is a reattach (from the server's POV), immediately play back
// history for channels that the client is already joined to. In this scenario,
// there are three total attempts to play the history:
// 3.1. During the initial reattach (no-op because the *playback privmsg
// hasn't been received yet, but they negotiated the znc.in/playback
// cap so we know we're going to receive it later)
// 3.2 Upon receiving the *playback privmsg, i.e., now: we should play
// the relevant history lines
// 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
2020-07-20 19:45:52 +02:00
playPrivmsgs := false
2019-05-21 01:08:57 +02:00
if params[1] == "*" {
2020-07-20 19:45:52 +02:00
playPrivmsgs = true // XXX nil `targets` means "every channel"
2019-05-21 01:08:57 +02:00
} else {
2022-03-30 06:44:51 +02:00
targets = make(utils.HashSet[string])
2019-05-21 01:08:57 +02:00
for _, targetName := range strings.Split(targetString, ",") {
if strings.HasPrefix(targetName, "#") {
if cfTarget, err := CasefoldChannel(targetName); err == nil {
targets.Add(cfTarget)
}
} else {
if cfNick, err := CasefoldName(targetName); err == nil {
nickTargets = append(nickTargets, cfNick)
}
2019-05-21 01:08:57 +02:00
}
}
}
2020-07-20 19:45:52 +02:00
if playPrivmsgs {
zncPlayPrivmsgsFromAll(client, rb, start, end)
2020-07-20 19:45:52 +02:00
}
2019-05-21 01:08:57 +02:00
rb.session.zncPlaybackTimes = &zncPlaybackTimes{
2020-02-28 01:07:49 +01:00
start: start,
end: end,
2019-05-21 01:08:57 +02:00
targets: targets,
setAt: time.Now().UTC(),
2019-05-21 01:08:57 +02:00
}
2019-05-30 01:23:46 +02:00
for _, channel := range client.Channels() {
2020-01-29 21:45:50 +01:00
if targets == nil || targets.Has(channel.NameCasefolded()) {
channel.autoReplayHistory(client, rb, "")
rb.Flush(true)
}
2019-05-30 01:23:46 +02:00
}
for _, cfNick := range nickTargets {
zncPlayPrivmsgsFrom(client, rb, cfNick, start, end)
rb.Flush(true)
}
2019-05-21 01:08:57 +02:00
}
func zncPlayPrivmsgsFrom(client *Client, rb *ResponseBuffer, target string, start, end time.Time) {
_, sequence, err := client.server.GetHistorySequence(nil, client, target)
if sequence == nil || err != nil {
return
}
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)
}
}
func zncPlayPrivmsgsFromAll(client *Client, rb *ResponseBuffer, start, end time.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)
}
}
2020-07-20 10:28:17 +02:00
// PRIVMSG *playback :list
func zncPlaybackListHandler(client *Client, command string, params []string, rb *ResponseBuffer) {
limit := client.server.Config().History.ChathistoryMax
2021-04-07 11:40:39 +02:00
correspondents, err := client.listTargets(history.Selector{}, history.Selector{}, limit)
if err != nil {
2021-04-07 11:40:39 +02:00
client.server.logger.Error("internal", "couldn't get history for ZNC list", err.Error())
return
}
2021-04-07 11:40:39 +02:00
nick := client.Nick()
for _, correspondent := range correspondents {
stamp := timeToZncWireTime(correspondent.Time)
2021-04-07 11:40:39 +02:00
unfoldedTarget := client.server.UnfoldName(correspondent.CfName)
rb.Add(nil, zncPrefix, "PRIVMSG", nick, fmt.Sprintf("%s 0 %s", unfoldedTarget, stamp))
}
2020-07-20 10:28:17 +02:00
}