mirror of
https://github.com/ergochat/ergo.git
synced 2024-11-29 15:40:02 +01:00
Merge pull request #504 from slingamn/playback.4
support znc.in/playback
This commit is contained in:
commit
678c8606b6
@ -165,6 +165,12 @@ CAPDEFS = [
|
|||||||
url="https://github.com/ircv3/ircv3-specifications/pull/362",
|
url="https://github.com/ircv3/ircv3-specifications/pull/362",
|
||||||
standard="Proposed IRCv3",
|
standard="Proposed IRCv3",
|
||||||
),
|
),
|
||||||
|
CapDef(
|
||||||
|
identifier="ZNCPlayback",
|
||||||
|
name="znc.in/playback",
|
||||||
|
url="https://wiki.znc.in/Playback",
|
||||||
|
standard="ZNC vendor",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
def validate_defs():
|
def validate_defs():
|
||||||
|
@ -7,7 +7,7 @@ package caps
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
// number of recognized capabilities:
|
// number of recognized capabilities:
|
||||||
numCapabs = 25
|
numCapabs = 26
|
||||||
// length of the uint64 array that represents the bitset:
|
// length of the uint64 array that represents the bitset:
|
||||||
bitsetLen = 1
|
bitsetLen = 1
|
||||||
)
|
)
|
||||||
@ -112,6 +112,10 @@ const (
|
|||||||
// EventPlayback is the Proposed IRCv3 capability named "draft/event-playback":
|
// EventPlayback is the Proposed IRCv3 capability named "draft/event-playback":
|
||||||
// https://github.com/ircv3/ircv3-specifications/pull/362
|
// https://github.com/ircv3/ircv3-specifications/pull/362
|
||||||
EventPlayback Capability = iota
|
EventPlayback Capability = iota
|
||||||
|
|
||||||
|
// ZNCPlayback is the ZNC vendor capability named "znc.in/playback":
|
||||||
|
// https://wiki.znc.in/Playback
|
||||||
|
ZNCPlayback Capability = iota
|
||||||
)
|
)
|
||||||
|
|
||||||
// `capabilityNames[capab]` is the string name of the capability `capab`
|
// `capabilityNames[capab]` is the string name of the capability `capab`
|
||||||
@ -142,5 +146,6 @@ var (
|
|||||||
"oragono.io/bnc",
|
"oragono.io/bnc",
|
||||||
"znc.in/self-message",
|
"znc.in/self-message",
|
||||||
"draft/event-playback",
|
"draft/event-playback",
|
||||||
|
"znc.in/playback",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -620,6 +620,12 @@ 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
|
// TODO #259 can be implemented as Flush(false) (i.e., nonblocking) while holding joinPartMutex
|
||||||
rb.Flush(true)
|
rb.Flush(true)
|
||||||
|
|
||||||
|
// autoreplay any messages as necessary
|
||||||
|
config := channel.server.Config()
|
||||||
|
var items []history.Item
|
||||||
|
if rb.session.zncPlaybackTimes != nil && (rb.session.zncPlaybackTimes.targets == nil || rb.session.zncPlaybackTimes.targets[chcfname]) {
|
||||||
|
items, _ = channel.history.Between(rb.session.zncPlaybackTimes.after, rb.session.zncPlaybackTimes.before, false, config.History.ChathistoryMax)
|
||||||
|
} else {
|
||||||
var replayLimit int
|
var replayLimit int
|
||||||
customReplayLimit := client.AccountSettings().AutoreplayLines
|
customReplayLimit := client.AccountSettings().AutoreplayLines
|
||||||
if customReplayLimit != nil {
|
if customReplayLimit != nil {
|
||||||
@ -632,14 +638,24 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp
|
|||||||
replayLimit = channel.server.Config().History.AutoreplayOnJoin
|
replayLimit = channel.server.Config().History.AutoreplayOnJoin
|
||||||
}
|
}
|
||||||
if 0 < replayLimit {
|
if 0 < replayLimit {
|
||||||
// TODO don't replay the client's own JOIN line?
|
items = channel.history.Latest(replayLimit)
|
||||||
items := channel.history.Latest(replayLimit)
|
}
|
||||||
if 0 < len(items) {
|
}
|
||||||
|
// 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 == message.Msgid {
|
||||||
|
// 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, true)
|
channel.replayHistoryItems(rb, items, true)
|
||||||
rb.Flush(true)
|
rb.Flush(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// plays channel join messages (the JOIN line, topic, and names) to a session.
|
// 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
|
// this is used when attaching a new session to an existing client that already has
|
||||||
|
@ -113,6 +113,8 @@ type Session struct {
|
|||||||
maxlenRest uint32
|
maxlenRest uint32
|
||||||
capState caps.State
|
capState caps.State
|
||||||
capVersion caps.Version
|
capVersion caps.Version
|
||||||
|
|
||||||
|
zncPlaybackTimes *zncPlaybackTimes
|
||||||
}
|
}
|
||||||
|
|
||||||
// sets the session quit message, if there isn't one already
|
// sets the session quit message, if there isn't one already
|
||||||
|
@ -321,6 +321,10 @@ func init() {
|
|||||||
handler: whowasHandler,
|
handler: whowasHandler,
|
||||||
minParams: 1,
|
minParams: 1,
|
||||||
},
|
},
|
||||||
|
"ZNC": {
|
||||||
|
handler: zncHandler,
|
||||||
|
minParams: 1,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
initializeServices()
|
initializeServices()
|
||||||
|
@ -2001,12 +2001,16 @@ func messageHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *R
|
|||||||
}
|
}
|
||||||
channel.SendSplitMessage(msg.Command, lowestPrefix, clientOnlyTags, client, splitMsg, rb)
|
channel.SendSplitMessage(msg.Command, lowestPrefix, clientOnlyTags, client, splitMsg, rb)
|
||||||
} else {
|
} else {
|
||||||
if service, isService := OragonoServices[strings.ToLower(targetString)]; isService {
|
|
||||||
// NOTICE and TAGMSG to services are ignored
|
// NOTICE and TAGMSG to services are ignored
|
||||||
if histType == history.Privmsg {
|
if histType == history.Privmsg {
|
||||||
|
lowercaseTarget := strings.ToLower(targetString)
|
||||||
|
if service, isService := OragonoServices[lowercaseTarget]; isService {
|
||||||
servicePrivmsgHandler(service, server, client, message, rb)
|
servicePrivmsgHandler(service, server, client, message, rb)
|
||||||
}
|
|
||||||
continue
|
continue
|
||||||
|
} else if _, isZNC := zncHandlers[lowercaseTarget]; isZNC {
|
||||||
|
zncPrivmsgHandler(client, lowercaseTarget, message, rb)
|
||||||
|
continue
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
user := server.clients.Get(targetString)
|
user := server.clients.Get(targetString)
|
||||||
@ -2746,3 +2750,9 @@ func whowasHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re
|
|||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ZNC <module> [params]
|
||||||
|
func zncHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
|
||||||
|
zncModuleHandler(client, msg.Params[0], msg.Params[1:], rb)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
@ -540,6 +540,13 @@ Returns information for the given user(s).`,
|
|||||||
|
|
||||||
Returns historical information on the last user with the given nickname.`,
|
Returns historical information on the last user with the given nickname.`,
|
||||||
},
|
},
|
||||||
|
"znc": {
|
||||||
|
text: `ZNC <module> [params]
|
||||||
|
|
||||||
|
Used to emulate features of the ZNC bouncer. This command is not intended
|
||||||
|
for direct use by end users.`,
|
||||||
|
duplicate: true,
|
||||||
|
},
|
||||||
|
|
||||||
// Informational
|
// Informational
|
||||||
"modes": {
|
"modes": {
|
||||||
|
17
irc/misc_test.go
Normal file
17
irc/misc_test.go
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
// 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), t)
|
||||||
|
assertEqual(zncWireTimeToTime("1558338348.9"), time.Unix(1558338348, 900000000), t)
|
||||||
|
assertEqual(zncWireTimeToTime("1558338348"), time.Unix(1558338348, 0), t)
|
||||||
|
assertEqual(zncWireTimeToTime(".988"), time.Unix(0, 988000000), t)
|
||||||
|
assertEqual(zncWireTimeToTime("garbage"), time.Unix(0, 0), t)
|
||||||
|
}
|
98
irc/znc.go
Normal file
98
irc/znc.go
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
// Copyright (c) 2019 Shivaram Lingamneni <slingamn@cs.stanford.edu>
|
||||||
|
// released under the MIT license
|
||||||
|
|
||||||
|
package irc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
|
||||||
|
type zncPlaybackTimes struct {
|
||||||
|
after time.Time
|
||||||
|
before time.Time
|
||||||
|
targets map[string]bool // nil for "*" (everything), otherwise the channel names
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://wiki.znc.in/Playback
|
||||||
|
// PRIVMSG *playback :play <target> [lower_bound] [upper_bound]
|
||||||
|
// e.g., PRIVMSG *playback :play * 1558374442
|
||||||
|
func zncPlaybackHandler(client *Client, command string, params []string, rb *ResponseBuffer) {
|
||||||
|
if len(params) < 2 {
|
||||||
|
return
|
||||||
|
} else if strings.ToLower(params[0]) != "play" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
targetString := params[1]
|
||||||
|
|
||||||
|
var after, before time.Time
|
||||||
|
if 2 < len(params) {
|
||||||
|
after = zncWireTimeToTime(params[2])
|
||||||
|
}
|
||||||
|
if 3 < len(params) {
|
||||||
|
before = zncWireTimeToTime(params[3])
|
||||||
|
}
|
||||||
|
|
||||||
|
var targets map[string]bool
|
||||||
|
|
||||||
|
// OK: the user's PMs get played back immediately on receiving this,
|
||||||
|
// then we save the timestamps in the session to handle replay on future channel joins
|
||||||
|
config := client.server.Config()
|
||||||
|
if params[1] == "*" {
|
||||||
|
items, _ := client.history.Between(after, before, false, config.History.ChathistoryMax)
|
||||||
|
client.replayPrivmsgHistory(rb, items, true)
|
||||||
|
} else {
|
||||||
|
for _, targetName := range strings.Split(targetString, ",") {
|
||||||
|
if cfTarget, err := CasefoldChannel(targetName); err == nil {
|
||||||
|
if targets == nil {
|
||||||
|
targets = make(map[string]bool)
|
||||||
|
}
|
||||||
|
targets[cfTarget] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rb.session.zncPlaybackTimes = &zncPlaybackTimes{
|
||||||
|
after: after,
|
||||||
|
before: before,
|
||||||
|
targets: targets,
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user