Fully eradicate: histserv and znc (tested working)
This commit is contained in:
parent
261104f067
commit
6760da5d68
171
irc/channel.go
171
irc/channel.go
@ -890,67 +890,9 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp
|
||||
// TODO #259 can be implemented as Flush(false) (i.e., nonblocking) while holding joinPartMutex
|
||||
rb.Flush(true)
|
||||
|
||||
channel.autoReplayHistory(client, rb, message.Msgid)
|
||||
return nil, ""
|
||||
}
|
||||
|
||||
func (channel *Channel) autoReplayHistory(client *Client, rb *ResponseBuffer, skipMsgid string) {
|
||||
// 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 hasAutoreplayTimestamps {
|
||||
_, seq, _ := channel.server.GetHistorySequence(channel, client, "")
|
||||
if seq != nil {
|
||||
zncMax := channel.server.Config().History.ZNCMax
|
||||
items, _ = seq.Between(history.Selector{Time: start}, history.Selector{Time: end}, zncMax)
|
||||
}
|
||||
} else if !rb.session.HasHistoryCaps() {
|
||||
var replayLimit int
|
||||
customReplayLimit := client.AccountSettings().AutoreplayLines
|
||||
if customReplayLimit != nil {
|
||||
replayLimit = *customReplayLimit
|
||||
maxLimit := channel.server.Config().History.ChathistoryMax
|
||||
if maxLimit < replayLimit {
|
||||
replayLimit = maxLimit
|
||||
}
|
||||
} else {
|
||||
replayLimit = channel.server.Config().History.AutoreplayOnJoin
|
||||
}
|
||||
if 0 < replayLimit {
|
||||
_, seq, _ := channel.server.GetHistorySequence(channel, client, "")
|
||||
if seq != nil {
|
||||
items, _ = seq.Between(history.Selector{}, history.Selector{}, replayLimit)
|
||||
}
|
||||
}
|
||||
}
|
||||
// remove the client's own JOIN line from the replay
|
||||
numItems := len(items)
|
||||
for i := len(items) - 1; 0 <= i; i-- {
|
||||
if items[i].Message.Msgid == skipMsgid {
|
||||
// zero'ed items will not be replayed because their `Type` field is not recognized
|
||||
items[i] = history.Item{}
|
||||
numItems--
|
||||
break
|
||||
}
|
||||
}
|
||||
if 0 < numItems {
|
||||
channel.replayHistoryItems(rb, items, false)
|
||||
rb.Flush(true)
|
||||
}
|
||||
}
|
||||
|
||||
// plays channel join messages (the JOIN line, topic, and names) to a session.
|
||||
// this is used when attaching a new session to an existing client that already has
|
||||
// channels, and also when one session of a client initiates a JOIN and the other
|
||||
@ -1029,119 +971,6 @@ 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) {
|
||||
// send an empty batch if necessary, as per the CHATHISTORY spec
|
||||
chname := channel.Name()
|
||||
client := rb.target
|
||||
eventPlayback := rb.session.capabilities.Has(caps.EventPlayback)
|
||||
extendedJoin := rb.session.capabilities.Has(caps.ExtendedJoin)
|
||||
var playJoinsAsPrivmsg bool
|
||||
if !eventPlayback {
|
||||
if chathistoryCommand {
|
||||
playJoinsAsPrivmsg = true
|
||||
} else {
|
||||
switch client.AccountSettings().ReplayJoins {
|
||||
case ReplayJoinsCommandsOnly:
|
||||
playJoinsAsPrivmsg = false
|
||||
case ReplayJoinsAlways:
|
||||
playJoinsAsPrivmsg = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
batchID := rb.StartNestedHistoryBatch(chname)
|
||||
defer rb.EndNestedBatch(batchID)
|
||||
|
||||
for _, item := range items {
|
||||
nick := NUHToNick(item.Nick)
|
||||
switch item.Type {
|
||||
case history.Privmsg:
|
||||
rb.AddSplitMessageFromClient(item.Nick, item.AccountName, item.IsBot, item.Tags, "PRIVMSG", chname, item.Message)
|
||||
case history.Notice:
|
||||
rb.AddSplitMessageFromClient(item.Nick, item.AccountName, item.IsBot, item.Tags, "NOTICE", chname, item.Message)
|
||||
case history.Tagmsg:
|
||||
if eventPlayback {
|
||||
rb.AddSplitMessageFromClient(item.Nick, item.AccountName, item.IsBot, item.Tags, "TAGMSG", chname, item.Message)
|
||||
} else if chathistoryCommand {
|
||||
// #1676, we have to send something here or else it breaks pagination
|
||||
rb.AddFromClient(item.Message.Time, history.HistservMungeMsgid(item.Message.Msgid), histservService.prefix, "*", false, nil, "PRIVMSG", chname, fmt.Sprintf(client.t("%s sent a TAGMSG"), nick))
|
||||
}
|
||||
case history.Join:
|
||||
if eventPlayback {
|
||||
if extendedJoin {
|
||||
rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, item.IsBot, nil, "JOIN", chname, item.AccountName, item.Params[0])
|
||||
} else {
|
||||
rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, item.IsBot, nil, "JOIN", chname)
|
||||
}
|
||||
} else {
|
||||
if !playJoinsAsPrivmsg {
|
||||
continue // #474
|
||||
}
|
||||
var message string
|
||||
if item.AccountName == "*" {
|
||||
message = fmt.Sprintf(client.t("%s joined the channel"), nick)
|
||||
} else {
|
||||
message = fmt.Sprintf(client.t("%[1]s [account: %[2]s] joined the channel"), nick, item.AccountName)
|
||||
}
|
||||
rb.AddFromClient(item.Message.Time, history.HistservMungeMsgid(item.Message.Msgid), histservService.prefix, "*", false, nil, "PRIVMSG", chname, message)
|
||||
}
|
||||
case history.Part:
|
||||
if eventPlayback {
|
||||
rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, item.IsBot, nil, "PART", chname, item.Message.Message)
|
||||
} else {
|
||||
if !playJoinsAsPrivmsg {
|
||||
continue // #474
|
||||
}
|
||||
message := fmt.Sprintf(client.t("%[1]s left the channel (%[2]s)"), nick, item.Message.Message)
|
||||
rb.AddFromClient(item.Message.Time, history.HistservMungeMsgid(item.Message.Msgid), histservService.prefix, "*", false, nil, "PRIVMSG", chname, message)
|
||||
}
|
||||
case history.Kick:
|
||||
if eventPlayback {
|
||||
rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, item.IsBot, nil, "KICK", chname, item.Params[0], item.Message.Message)
|
||||
} else {
|
||||
message := fmt.Sprintf(client.t("%[1]s kicked %[2]s (%[3]s)"), nick, item.Params[0], item.Message.Message)
|
||||
rb.AddFromClient(item.Message.Time, history.HistservMungeMsgid(item.Message.Msgid), histservService.prefix, "*", false, nil, "PRIVMSG", chname, message)
|
||||
}
|
||||
case history.Quit:
|
||||
if eventPlayback {
|
||||
rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, item.IsBot, nil, "QUIT", item.Message.Message)
|
||||
} else {
|
||||
if !playJoinsAsPrivmsg {
|
||||
continue // #474
|
||||
}
|
||||
message := fmt.Sprintf(client.t("%[1]s quit (%[2]s)"), nick, item.Message.Message)
|
||||
rb.AddFromClient(item.Message.Time, history.HistservMungeMsgid(item.Message.Msgid), histservService.prefix, "*", false, nil, "PRIVMSG", chname, message)
|
||||
}
|
||||
case history.Nick:
|
||||
if eventPlayback {
|
||||
rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, item.IsBot, nil, "NICK", item.Params[0])
|
||||
} else {
|
||||
message := fmt.Sprintf(client.t("%[1]s changed nick to %[2]s"), nick, item.Params[0])
|
||||
rb.AddFromClient(item.Message.Time, history.HistservMungeMsgid(item.Message.Msgid), histservService.prefix, "*", false, nil, "PRIVMSG", chname, message)
|
||||
}
|
||||
case history.Topic:
|
||||
if eventPlayback {
|
||||
rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, item.IsBot, nil, "TOPIC", chname, item.Message.Message)
|
||||
} else {
|
||||
message := fmt.Sprintf(client.t("%[1]s set the channel topic to: %[2]s"), nick, item.Message.Message)
|
||||
rb.AddFromClient(item.Message.Time, history.HistservMungeMsgid(item.Message.Msgid), histservService.prefix, "*", false, nil, "PRIVMSG", chname, message)
|
||||
}
|
||||
case history.Mode:
|
||||
params := make([]string, len(item.Message.Split)+1)
|
||||
params[0] = chname
|
||||
for i, pair := range item.Message.Split {
|
||||
params[i+1] = pair.Message
|
||||
}
|
||||
if eventPlayback {
|
||||
rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, item.IsBot, nil, "MODE", params...)
|
||||
} else {
|
||||
message := fmt.Sprintf(client.t("%[1]s set channel modes: %[2]s"), nick, strings.Join(params[1:], " "))
|
||||
rb.AddFromClient(item.Message.Time, history.HistservMungeMsgid(item.Message.Msgid), histservService.prefix, "*", false, nil, "PRIVMSG", chname, message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SendTopic sends the channel topic to the given client.
|
||||
// `sendNoTopic` controls whether RPL_NOTOPIC is sent when the topic is unset
|
||||
func (channel *Channel) SendTopic(client *Client, rb *ResponseBuffer, sendNoTopic bool) {
|
||||
|
@ -174,7 +174,6 @@ type Session struct {
|
||||
|
||||
registrationMessages int
|
||||
|
||||
zncPlaybackTimes *zncPlaybackTimes
|
||||
autoreplayMissedSince time.Time
|
||||
|
||||
batch MultilineBatch
|
||||
@ -730,15 +729,8 @@ func (client *Client) playReattachMessages(session *Session) {
|
||||
// because those caps disable autoreplay-on-join and they haven't sent the relevant
|
||||
// *playback PRIVMSG or CHATHISTORY command yet
|
||||
rb := NewResponseBuffer(session)
|
||||
channel.autoReplayHistory(client, rb, "")
|
||||
rb.Send(true)
|
||||
}
|
||||
if !session.autoreplayMissedSince.IsZero() && !hasHistoryCaps {
|
||||
rb := NewResponseBuffer(session)
|
||||
zncPlayPrivmsgsFromAll(client, rb, time.Now().UTC(), session.autoreplayMissedSince)
|
||||
rb.Send(true)
|
||||
}
|
||||
session.autoreplayMissedSince = time.Time{}
|
||||
}
|
||||
|
||||
//
|
||||
@ -856,73 +848,6 @@ func (session *Session) Ping() {
|
||||
session.Send(nil, "", "PING", session.client.Nick())
|
||||
}
|
||||
|
||||
func (client *Client) replayPrivmsgHistory(rb *ResponseBuffer, items []history.Item, target string, chathistoryCommand bool) {
|
||||
var batchID string
|
||||
details := client.Details()
|
||||
nick := details.nick
|
||||
if target == "" {
|
||||
target = nick
|
||||
}
|
||||
batchID = rb.StartNestedHistoryBatch(target)
|
||||
|
||||
isSelfMessage := func(item *history.Item) bool {
|
||||
// XXX: Params[0] is the message target. if the source of this message is an in-memory
|
||||
// buffer, then it's "" for an incoming message and the recipient's nick for an outgoing
|
||||
// message. if the source of the message is mysql, then mysql only sees one copy of the
|
||||
// message, and it's the version with the recipient's nick filled in. so this is an
|
||||
// incoming message if Params[0] (the recipient's nick) equals the client's nick:
|
||||
return item.Params[0] != "" && item.Params[0] != nick
|
||||
}
|
||||
|
||||
hasEventPlayback := rb.session.capabilities.Has(caps.EventPlayback)
|
||||
hasTags := rb.session.capabilities.Has(caps.MessageTags)
|
||||
for _, item := range items {
|
||||
var command string
|
||||
switch item.Type {
|
||||
case history.Invite:
|
||||
if isSelfMessage(&item) {
|
||||
continue
|
||||
}
|
||||
if hasEventPlayback {
|
||||
rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, item.IsBot, nil, "INVITE", nick, item.Message.Message)
|
||||
} else {
|
||||
rb.AddFromClient(item.Message.Time, history.HistservMungeMsgid(item.Message.Msgid), histservService.prefix, "*", false, nil, "PRIVMSG", fmt.Sprintf(client.t("%[1]s invited you to channel %[2]s"), NUHToNick(item.Nick), item.Message.Message))
|
||||
}
|
||||
continue
|
||||
case history.Privmsg:
|
||||
command = "PRIVMSG"
|
||||
case history.Notice:
|
||||
command = "NOTICE"
|
||||
case history.Tagmsg:
|
||||
if hasEventPlayback && hasTags {
|
||||
command = "TAGMSG"
|
||||
} else if chathistoryCommand {
|
||||
// #1676: send something for TAGMSG; we can't discard it entirely
|
||||
// because it'll break pagination
|
||||
rb.AddFromClient(item.Message.Time, history.HistservMungeMsgid(item.Message.Msgid), histservService.prefix, "*", false, nil, "PRIVMSG", fmt.Sprintf(client.t("%[1]s sent you a TAGMSG"), NUHToNick(item.Nick)))
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
default:
|
||||
// see #1676, this shouldn't happen
|
||||
continue
|
||||
}
|
||||
var tags map[string]string
|
||||
if hasTags {
|
||||
tags = item.Tags
|
||||
}
|
||||
if !isSelfMessage(&item) {
|
||||
rb.AddSplitMessageFromClient(item.Nick, item.AccountName, item.IsBot, tags, command, nick, item.Message)
|
||||
} else {
|
||||
// this message was sent *from* the client to another nick; the target is item.Params[0]
|
||||
// substitute client's current nickmask in case client changed nick
|
||||
rb.AddSplitMessageFromClient(details.nickMask, item.AccountName, item.IsBot, tags, command, item.Params[0], item.Message)
|
||||
}
|
||||
}
|
||||
|
||||
rb.EndNestedBatch(batchID)
|
||||
}
|
||||
|
||||
// IdleTime returns how long this client's been idle.
|
||||
func (client *Client) IdleTime() time.Duration {
|
||||
client.stateMutex.RLock()
|
||||
|
@ -2020,9 +2020,8 @@ func dispatchMessageToTarget(client *Client, tags map[string]string, histType hi
|
||||
} else {
|
||||
lowercaseTarget := strings.ToLower(target)
|
||||
service, isService := OragonoServices[lowercaseTarget]
|
||||
_, isZNC := zncHandlers[lowercaseTarget]
|
||||
|
||||
if isService || isZNC {
|
||||
if isService {
|
||||
details := client.Details()
|
||||
rb.addEchoMessage(tags, details.nickMask, details.accountName, command, target, message)
|
||||
if histType != history.Privmsg {
|
||||
@ -2030,8 +2029,6 @@ func dispatchMessageToTarget(client *Client, tags map[string]string, histType hi
|
||||
}
|
||||
if isService {
|
||||
servicePrivmsgHandler(service, server, client, message.Message, rb)
|
||||
} else if isZNC {
|
||||
zncPrivmsgHandler(client, lowercaseTarget, message.Message, rb)
|
||||
}
|
||||
return
|
||||
}
|
||||
@ -3315,17 +3312,6 @@ func whowasHandler(server *Server, client *Client, msg ircmsg.Message, rb *Respo
|
||||
return false
|
||||
}
|
||||
|
||||
// ZNC <module> [params]
|
||||
func zncHandler(server *Server, client *Client, msg ircmsg.Message, rb *ResponseBuffer) bool {
|
||||
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
|
||||
}
|
||||
|
||||
// fake handler for unknown commands
|
||||
func unknownCommandHandler(server *Server, client *Client, msg ircmsg.Message, rb *ResponseBuffer) bool {
|
||||
var message string
|
||||
|
@ -1,36 +0,0 @@
|
||||
// Copyright (c) 2020 Shivaram Lingamneni <slingamn@cs.stanford.edu>
|
||||
// released under the MIT license
|
||||
|
||||
package irc
|
||||
|
||||
const (
|
||||
histservHelp = `HistServ is a remnant of bad code. There is no history on ircd.chat.`
|
||||
)
|
||||
|
||||
func histservEnabled(config *Config) bool {
|
||||
return config.History.Enabled
|
||||
}
|
||||
|
||||
func historyComplianceEnabled(config *Config) bool {
|
||||
return config.History.Enabled && config.History.Persistent.Enabled && config.History.Retention.EnableAccountIndexing
|
||||
}
|
||||
|
||||
var (
|
||||
histservCommands = map[string]*serviceCommand{
|
||||
"forget": {
|
||||
handler: histservForgetHandler,
|
||||
help: `Syntax: $bFORGET <account>$b
|
||||
|
||||
FORGET deletes all history messages sent by an account.`,
|
||||
helpShort: `$bFORGET$b doesn't do anything because there is no history here.`,
|
||||
capabs: []string{"history"},
|
||||
enabled: histservEnabled,
|
||||
minParams: 1,
|
||||
maxParams: 1,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func histservForgetHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
|
||||
service.Notice(rb, client.t("ircd.chat does not keep history."))
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
// Copyright (c) 2019 Shivaram Lingamneni <slingamn@cs.stanford.edu>
|
||||
// released under the MIT license
|
||||
|
||||
package irc
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestZncTimestampParser(t *testing.T) {
|
||||
assertEqual(zncWireTimeToTime("1558338348.988"), time.Unix(1558338348, 988000000).UTC(), t)
|
||||
assertEqual(zncWireTimeToTime("1558338348.9"), time.Unix(1558338348, 900000000).UTC(), t)
|
||||
assertEqual(zncWireTimeToTime("1558338348"), time.Unix(1558338348, 0).UTC(), t)
|
||||
assertEqual(zncWireTimeToTime(".988"), time.Unix(0, 988000000).UTC(), t)
|
||||
assertEqual(zncWireTimeToTime("garbage"), time.Unix(0, 0).UTC(), t)
|
||||
}
|
@ -83,13 +83,6 @@ var (
|
||||
Commands: hostservCommands,
|
||||
HelpBanner: hostservHelp,
|
||||
}
|
||||
histservService = &ircService{
|
||||
Name: "HistServ",
|
||||
ShortName: "HISTSERV",
|
||||
CommandAliases: []string{"HISTSERV"},
|
||||
Commands: histservCommands,
|
||||
HelpBanner: histservHelp,
|
||||
}
|
||||
)
|
||||
|
||||
// OragonoServices all services, by lowercase name
|
||||
@ -97,7 +90,6 @@ var OragonoServices = map[string]*ircService{
|
||||
"nickserv": nickservService,
|
||||
"chanserv": chanservService,
|
||||
"hostserv": hostservService,
|
||||
"histserv": histservService,
|
||||
}
|
||||
|
||||
func (service *ircService) Notice(rb *ResponseBuffer, text string) {
|
||||
|
225
irc/znc.go
225
irc/znc.go
@ -1,225 +0,0 @@
|
||||
// Copyright (c) 2019 Shivaram Lingamneni <slingamn@cs.stanford.edu>
|
||||
// released under the MIT license
|
||||
|
||||
package irc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.tcp.direct/ircd/ircd/irc/history"
|
||||
"git.tcp.direct/ircd/ircd/irc/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
// #829, also see "Case 2" in the "three cases" below:
|
||||
zncPlaybackCommandExpiration = time.Second * 30
|
||||
|
||||
zncPrefix = "*playback!znc@znc.in"
|
||||
|
||||
maxDMTargetsForAutoplay = 128
|
||||
)
|
||||
|
||||
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 {
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
// "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:]
|
||||
}
|
||||
seconds, _ := strconv.ParseInt(secondsPortion, 10, 64)
|
||||
fraction, _ := strconv.ParseFloat(fracPortion, 64)
|
||||
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
|
||||
targets utils.StringSet // 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)
|
||||
}
|
||||
|
||||
// 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 <target> [lower_bound] [upper_bound]
|
||||
// e.g., PRIVMSG *playback :play * 1558374442
|
||||
func zncPlaybackPlayHandler(client *Client, command string, params []string, rb *ResponseBuffer) {
|
||||
if len(params) < 2 || len(params) > 4 {
|
||||
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
|
||||
// `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])
|
||||
}
|
||||
|
||||
var targets utils.StringSet
|
||||
var nickTargets []string
|
||||
|
||||
// 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
|
||||
// and use it to process subsequent joins. (This is the Textual behavior:
|
||||
// first send the playback PRIVMSG, then send the JOIN lines.)
|
||||
// 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
|
||||
|
||||
playPrivmsgs := false
|
||||
if params[1] == "*" {
|
||||
playPrivmsgs = true // XXX nil `targets` means "every channel"
|
||||
} else {
|
||||
targets = make(utils.StringSet)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if playPrivmsgs {
|
||||
zncPlayPrivmsgsFromAll(client, rb, start, end)
|
||||
}
|
||||
|
||||
rb.session.zncPlaybackTimes = &zncPlaybackTimes{
|
||||
start: start,
|
||||
end: end,
|
||||
targets: targets,
|
||||
setAt: time.Now().UTC(),
|
||||
}
|
||||
|
||||
for _, channel := range client.Channels() {
|
||||
if targets == nil || targets.Has(channel.NameCasefolded()) {
|
||||
channel.autoReplayHistory(client, rb, "")
|
||||
rb.Flush(true)
|
||||
}
|
||||
}
|
||||
|
||||
for _, cfNick := range nickTargets {
|
||||
zncPlayPrivmsgsFrom(client, rb, cfNick, start, end)
|
||||
rb.Flush(true)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// PRIVMSG *playback :list
|
||||
func zncPlaybackListHandler(client *Client, command string, params []string, rb *ResponseBuffer) {
|
||||
limit := client.server.Config().History.ChathistoryMax
|
||||
correspondents, err := client.listTargets(history.Selector{}, history.Selector{}, limit)
|
||||
if err != nil {
|
||||
client.server.logger.Error("internal", "couldn't get history for ZNC list", err.Error())
|
||||
return
|
||||
}
|
||||
nick := client.Nick()
|
||||
for _, correspondent := range correspondents {
|
||||
stamp := timeToZncWireTime(correspondent.Time)
|
||||
unfoldedTarget := client.server.UnfoldName(correspondent.CfName)
|
||||
rb.Add(nil, zncPrefix, "PRIVMSG", nick, fmt.Sprintf("%s 0 %s", unfoldedTarget, stamp))
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user