3
0
mirror of https://github.com/ergochat/ergo.git synced 2024-11-10 22:19:31 +01:00

improvements to message replay code

This commit is contained in:
Shivaram Lingamneni 2019-05-06 23:17:57 -04:00
parent 939729a7c0
commit b11bf503e7
10 changed files with 338 additions and 165 deletions

View File

@ -159,6 +159,12 @@ CAPDEFS = [
url="https://wiki.znc.in/Query_buffers", url="https://wiki.znc.in/Query_buffers",
standard="ZNC vendor", standard="ZNC vendor",
), ),
CapDef(
identifier="EventPlayback",
name="draft/event-playback",
url="https://github.com/ircv3/ircv3-specifications/pull/362",
standard="Draft IRCv3",
),
] ]
def validate_defs(): def validate_defs():

View File

@ -7,7 +7,7 @@ package caps
const ( const (
// number of recognized capabilities: // number of recognized capabilities:
numCapabs = 24 numCapabs = 25
// length of the uint64 array that represents the bitset: // length of the uint64 array that represents the bitset:
bitsetLen = 1 bitsetLen = 1
) )
@ -108,6 +108,10 @@ const (
// ZNCSelfMessage is the ZNC vendor capability named "znc.in/self-message": // ZNCSelfMessage is the ZNC vendor capability named "znc.in/self-message":
// https://wiki.znc.in/Query_buffers // https://wiki.znc.in/Query_buffers
ZNCSelfMessage Capability = iota ZNCSelfMessage Capability = iota
// EventPlayback is the Draft IRCv3 capability named "draft/event-playback":
// https://github.com/ircv3/ircv3-specifications/pull/362
EventPlayback Capability = iota
) )
// `capabilityNames[capab]` is the string name of the capability `capab` // `capabilityNames[capab]` is the string name of the capability `capab`
@ -137,5 +141,6 @@ var (
"userhost-in-names", "userhost-in-names",
"oragono.io/bnc", "oragono.io/bnc",
"znc.in/self-message", "znc.in/self-message",
"draft/event-playback",
} }
) )

View File

@ -536,6 +536,8 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp
client.server.logger.Debug("join", fmt.Sprintf("%s joined channel %s", details.nick, chname)) client.server.logger.Debug("join", fmt.Sprintf("%s joined channel %s", details.nick, chname))
var message utils.SplitMessage
givenMode := func() (givenMode modes.Mode) { givenMode := func() (givenMode modes.Mode) {
channel.joinPartMutex.Lock() channel.joinPartMutex.Lock()
defer channel.joinPartMutex.Unlock() defer channel.joinPartMutex.Unlock()
@ -559,14 +561,15 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp
channel.regenerateMembersCache() channel.regenerateMembersCache()
message := utils.SplitMessage{} message = utils.MakeSplitMessage("", true)
message.Msgid = details.realname histItem := history.Item{
channel.history.Add(history.Item{
Type: history.Join, Type: history.Join,
Nick: details.nickMask, Nick: details.nickMask,
AccountName: details.accountName, AccountName: details.accountName,
Message: message, Message: message,
}) }
histItem.Params[0] = details.realname
channel.history.Add(histItem)
return return
}() }()
@ -587,9 +590,10 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp
continue continue
} }
if session.capabilities.Has(caps.ExtendedJoin) { if session.capabilities.Has(caps.ExtendedJoin) {
session.Send(nil, details.nickMask, "JOIN", chname, details.accountName, details.realname) session.sendFromClientInternal(false, message.Time, message.Msgid, details.nickMask, details.accountName, nil, "JOIN", chname, details.accountName, details.realname)
} else { } else {
session.Send(nil, details.nickMask, "JOIN", chname) session.Send(nil, details.nickMask, "JOIN", chname)
session.sendFromClientInternal(false, message.Time, message.Msgid, details.nickMask, details.accountName, nil, "JOIN", chname)
} }
if givenMode != 0 { if givenMode != 0 {
session.Send(nil, client.server.name, "MODE", chname, modestr, details.nick) session.Send(nil, client.server.name, "MODE", chname, modestr, details.nick)
@ -598,9 +602,9 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp
} }
if rb.session.capabilities.Has(caps.ExtendedJoin) { if rb.session.capabilities.Has(caps.ExtendedJoin) {
rb.Add(nil, details.nickMask, "JOIN", chname, details.accountName, details.realname) rb.AddFromClient(message.Time, message.Msgid, details.nickMask, details.accountName, nil, "JOIN", chname, details.accountName, details.realname)
} else { } else {
rb.Add(nil, details.nickMask, "JOIN", chname) rb.AddFromClient(message.Time, message.Msgid, details.nickMask, details.accountName, nil, "JOIN", chname)
} }
if rb.session.client == client { if rb.session.client == client {
@ -613,10 +617,13 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp
rb.Flush(true) rb.Flush(true)
replayLimit := channel.server.Config().History.AutoreplayOnJoin replayLimit := channel.server.Config().History.AutoreplayOnJoin
if replayLimit > 0 { if 0 < replayLimit {
// TODO don't replay the client's own JOIN line?
items := channel.history.Latest(replayLimit) items := channel.history.Latest(replayLimit)
channel.replayHistoryItems(rb, items) if 0 < len(items) {
rb.Flush(true) channel.replayHistoryItems(rb, items, true)
rb.Flush(true)
}
} }
} }
@ -647,14 +654,16 @@ func (channel *Channel) Part(client *Client, message string, rb *ResponseBuffer)
channel.Quit(client) channel.Quit(client)
splitMessage := utils.MakeSplitMessage(message, true)
details := client.Details() details := client.Details()
for _, member := range channel.Members() { for _, member := range channel.Members() {
member.Send(nil, details.nickMask, "PART", chname, message) member.sendFromClientInternal(false, splitMessage.Time, splitMessage.Msgid, details.nickMask, details.accountName, nil, "PART", chname, message)
} }
rb.Add(nil, details.nickMask, "PART", chname, message) rb.AddFromClient(splitMessage.Time, splitMessage.Msgid, details.nickMask, details.accountName, nil, "PART", chname, message)
for _, session := range client.Sessions() { for _, session := range client.Sessions() {
if session != rb.session { if session != rb.session {
session.Send(nil, details.nickMask, "PART", chname, message) session.sendFromClientInternal(false, splitMessage.Time, splitMessage.Msgid, details.nickMask, details.accountName, nil, "PART", chname, message)
} }
} }
@ -662,7 +671,7 @@ func (channel *Channel) Part(client *Client, message string, rb *ResponseBuffer)
Type: history.Part, Type: history.Part,
Nick: details.nickMask, Nick: details.nickMask,
AccountName: details.accountName, AccountName: details.accountName,
Message: utils.MakeSplitMessage(message, true), Message: splitMessage,
}) })
client.server.logger.Debug("part", fmt.Sprintf("%s left channel %s", details.nick, chname)) client.server.logger.Debug("part", fmt.Sprintf("%s left channel %s", details.nick, chname))
@ -748,7 +757,7 @@ func (channel *Channel) resumeAndAnnounce(newClient, oldClient *Client) {
func (channel *Channel) replayHistoryForResume(newClient *Client, after time.Time, before time.Time) { func (channel *Channel) replayHistoryForResume(newClient *Client, after time.Time, before time.Time) {
items, complete := channel.history.Between(after, before, false, 0) items, complete := channel.history.Between(after, before, false, 0)
rb := NewResponseBuffer(newClient.Sessions()[0]) rb := NewResponseBuffer(newClient.Sessions()[0])
channel.replayHistoryItems(rb, items) channel.replayHistoryItems(rb, items, false)
if !complete && !newClient.resumeDetails.HistoryIncomplete { if !complete && !newClient.resumeDetails.HistoryIncomplete {
// warn here if we didn't warn already // warn here if we didn't warn already
rb.Add(nil, "HistServ", "NOTICE", channel.Name(), newClient.t("Some additional message history may have been lost")) rb.Add(nil, "HistServ", "NOTICE", channel.Name(), newClient.t("Some additional message history may have been lost"))
@ -759,50 +768,93 @@ func (channel *Channel) replayHistoryForResume(newClient *Client, after time.Tim
func stripMaskFromNick(nickMask string) (nick string) { func stripMaskFromNick(nickMask string) (nick string) {
index := strings.Index(nickMask, "!") index := strings.Index(nickMask, "!")
if index == -1 { if index == -1 {
return return nickMask
} }
return nickMask[0:index] return nickMask[0:index]
} }
func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.Item) { // munge the msgid corresponding to a replayable event,
// yielding a consistent msgid for the fake PRIVMSG from HistServ
func mungeMsgidForHistserv(token string) (result string) {
return fmt.Sprintf("_%s", token)
}
func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.Item, autoreplay bool) {
chname := channel.Name() chname := channel.Name()
client := rb.target client := rb.target
serverTime := rb.session.capabilities.Has(caps.ServerTime) eventPlayback := rb.session.capabilities.Has(caps.EventPlayback)
extendedJoin := rb.session.capabilities.Has(caps.ExtendedJoin)
if len(items) == 0 {
return
}
batchID := rb.StartNestedHistoryBatch(chname)
defer rb.EndNestedBatch(batchID)
for _, item := range items { for _, item := range items {
var tags map[string]string nick := stripMaskFromNick(item.Nick)
if serverTime {
tags = map[string]string{"time": item.Time.Format(IRCv3TimestampFormat)}
}
// TODO(#437) support history.Tagmsg
switch item.Type { switch item.Type {
case history.Privmsg: case history.Privmsg:
rb.AddSplitMessageFromClient(item.Nick, item.AccountName, tags, "PRIVMSG", chname, item.Message) rb.AddSplitMessageFromClient(item.Nick, item.AccountName, item.Tags, "PRIVMSG", chname, item.Message)
case history.Notice: case history.Notice:
rb.AddSplitMessageFromClient(item.Nick, item.AccountName, tags, "NOTICE", chname, item.Message) rb.AddSplitMessageFromClient(item.Nick, item.AccountName, item.Tags, "NOTICE", chname, item.Message)
case history.Join: case history.Tagmsg:
nick := stripMaskFromNick(item.Nick) if rb.session.capabilities.Has(caps.MessageTags) {
var message string rb.AddSplitMessageFromClient(item.Nick, item.AccountName, item.Tags, "TAGMSG", chname, item.Message)
if item.AccountName == "*" { }
message = fmt.Sprintf(client.t("%s joined the channel"), nick) case history.Join:
} else { if eventPlayback {
message = fmt.Sprintf(client.t("%[1]s [account: %[2]s] joined the channel"), nick, item.AccountName) if extendedJoin {
rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "HEVENT", "JOIN", chname, item.AccountName, item.Params[0])
} else {
rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "HEVENT", "JOIN", chname)
}
} else {
if autoreplay {
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, mungeMsgidForHistserv(item.Message.Msgid), "HistServ", "*", nil, "PRIVMSG", chname, message)
} }
rb.Add(tags, "HistServ", "PRIVMSG", chname, message)
case history.Part: case history.Part:
nick := stripMaskFromNick(item.Nick) if eventPlayback {
message := fmt.Sprintf(client.t("%[1]s left the channel (%[2]s)"), nick, item.Message.Message) rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "HEVENT", "PART", chname, item.Message.Message)
rb.Add(tags, "HistServ", "PRIVMSG", chname, message) } else {
case history.Quit: if autoreplay {
nick := stripMaskFromNick(item.Nick) continue // #474
message := fmt.Sprintf(client.t("%[1]s quit (%[2]s)"), nick, item.Message.Message) }
rb.Add(tags, "HistServ", "PRIVMSG", chname, message) message := fmt.Sprintf(client.t("%[1]s left the channel (%[2]s)"), nick, item.Message.Message)
rb.AddFromClient(item.Message.Time, mungeMsgidForHistserv(item.Message.Msgid), "HistServ", "*", nil, "PRIVMSG", chname, message)
}
case history.Kick: case history.Kick:
nick := stripMaskFromNick(item.Nick) if eventPlayback {
// XXX Msgid is the kick target rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "HEVENT", "KICK", chname, item.Params[0], item.Message.Message)
message := fmt.Sprintf(client.t("%[1]s kicked %[2]s (%[3]s)"), nick, item.Message.Msgid, item.Message.Message) } else {
rb.Add(tags, "HistServ", "PRIVMSG", chname, message) message := fmt.Sprintf(client.t("%[1]s kicked %[2]s (%[3]s)"), nick, item.Params[0], item.Message.Message)
rb.AddFromClient(item.Message.Time, mungeMsgidForHistserv(item.Message.Msgid), "HistServ", "*", nil, "PRIVMSG", chname, message)
}
case history.Quit:
if eventPlayback {
rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "HEVENT", "QUIT", item.Message.Message)
} else {
if autoreplay {
continue // #474
}
message := fmt.Sprintf(client.t("%[1]s quit (%[2]s)"), nick, item.Message.Message)
rb.AddFromClient(item.Message.Time, mungeMsgidForHistserv(item.Message.Msgid), "HistServ", "*", nil, "PRIVMSG", chname, message)
}
case history.Nick:
if eventPlayback {
rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "HEVENT", "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, mungeMsgidForHistserv(item.Message.Msgid), "HistServ", "*", nil, "PRIVMSG", chname, message)
}
} }
} }
} }
@ -934,7 +986,7 @@ func (channel *Channel) SendSplitMessage(command string, minPrefixMode modes.Mod
tagsToUse = clientOnlyTags tagsToUse = clientOnlyTags
} }
if histType == history.Tagmsg && rb.session.capabilities.Has(caps.MessageTags) { if histType == history.Tagmsg && rb.session.capabilities.Has(caps.MessageTags) {
rb.AddFromClient(message.Msgid, nickmask, account, tagsToUse, command, chname) rb.AddFromClient(message.Time, message.Msgid, nickmask, account, tagsToUse, command, chname)
} else { } else {
rb.AddSplitMessageFromClient(nickmask, account, tagsToUse, command, chname, message) rb.AddSplitMessageFromClient(nickmask, account, tagsToUse, command, chname, message)
} }
@ -986,7 +1038,7 @@ func (channel *Channel) SendSplitMessage(command string, minPrefixMode modes.Mod
Message: message, Message: message,
Nick: nickmask, Nick: nickmask,
AccountName: account, AccountName: account,
Time: now, Tags: clientOnlyTags,
}) })
} }
@ -1110,27 +1162,29 @@ func (channel *Channel) Kick(client *Client, target *Client, comment string, rb
comment = comment[:kicklimit] comment = comment[:kicklimit]
} }
message := utils.MakeSplitMessage(comment, true)
clientMask := client.NickMaskString() clientMask := client.NickMaskString()
clientAccount := client.AccountName()
targetNick := target.Nick() targetNick := target.Nick()
chname := channel.Name() chname := channel.Name()
for _, member := range channel.Members() { for _, member := range channel.Members() {
for _, session := range member.Sessions() { for _, session := range member.Sessions() {
if session != rb.session { if session != rb.session {
session.Send(nil, clientMask, "KICK", chname, targetNick, comment) session.sendFromClientInternal(false, message.Time, message.Msgid, clientMask, clientAccount, nil, "KICK", chname, targetNick, comment)
} }
} }
} }
rb.Add(nil, clientMask, "KICK", chname, targetNick, comment) rb.Add(nil, clientMask, "KICK", chname, targetNick, comment)
message := utils.SplitMessage{} histItem := history.Item{
message.Message = comment
message.Msgid = targetNick // XXX abuse this field
channel.history.Add(history.Item{
Type: history.Kick, Type: history.Kick,
Nick: clientMask, Nick: clientMask,
AccountName: target.AccountName(), AccountName: target.AccountName(),
Message: message, Message: message,
}) }
histItem.Params[0] = targetNick
channel.history.Add(histItem)
channel.Quit(target) channel.Quit(target)
} }

View File

@ -487,7 +487,7 @@ func (client *Client) tryResume() (success bool) {
} }
} }
privmsgMatcher := func(item history.Item) bool { privmsgMatcher := func(item history.Item) bool {
return item.Type == history.Privmsg || item.Type == history.Notice return item.Type == history.Privmsg || item.Type == history.Notice || item.Type == history.Tagmsg
} }
privmsgHistory := oldClient.history.Match(privmsgMatcher, false, 0) privmsgHistory := oldClient.history.Match(privmsgMatcher, false, 0)
lastDiscarded := oldClient.history.LastDiscarded() lastDiscarded := oldClient.history.LastDiscarded()
@ -495,8 +495,7 @@ func (client *Client) tryResume() (success bool) {
oldestLostMessage = lastDiscarded oldestLostMessage = lastDiscarded
} }
for _, item := range privmsgHistory { for _, item := range privmsgHistory {
// TODO this is the nickmask, fix that sender := server.clients.Get(stripMaskFromNick(item.Nick))
sender := server.clients.Get(item.Nick)
if sender != nil { if sender != nil {
friends.Add(sender) friends.Add(sender)
} }
@ -561,8 +560,13 @@ func (client *Client) tryResumeChannels() {
} }
func (client *Client) replayPrivmsgHistory(rb *ResponseBuffer, items []history.Item, complete bool) { func (client *Client) replayPrivmsgHistory(rb *ResponseBuffer, items []history.Item, complete bool) {
var batchID string
nick := client.Nick() nick := client.Nick()
serverTime := rb.session.capabilities.Has(caps.ServerTime) if 0 < len(items) {
batchID = rb.StartNestedHistoryBatch(nick)
}
allowTags := rb.session.capabilities.Has(caps.MessageTags)
for _, item := range items { for _, item := range items {
var command string var command string
switch item.Type { switch item.Type {
@ -570,15 +574,23 @@ func (client *Client) replayPrivmsgHistory(rb *ResponseBuffer, items []history.I
command = "PRIVMSG" command = "PRIVMSG"
case history.Notice: case history.Notice:
command = "NOTICE" command = "NOTICE"
case history.Tagmsg:
if allowTags {
command = "TAGMSG"
} else {
continue
}
default: default:
continue continue
} }
var tags map[string]string var tags map[string]string
if serverTime { if allowTags {
tags = map[string]string{"time": item.Time.Format(IRCv3TimestampFormat)} tags = item.Tags
} }
rb.AddSplitMessageFromClient(item.Nick, item.AccountName, tags, command, nick, item.Message) rb.AddSplitMessageFromClient(item.Nick, item.AccountName, tags, command, nick, item.Message)
} }
rb.EndNestedBatch(batchID)
if !complete { if !complete {
rb.Add(nil, "HistServ", "NOTICE", nick, client.t("Some additional message history may have been lost")) rb.Add(nil, "HistServ", "NOTICE", nick, client.t("Some additional message history may have been lost"))
} }
@ -934,19 +946,21 @@ func (client *Client) destroy(beingResumed bool, session *Session) {
return return
} }
details := client.Details()
// see #235: deduplicating the list of PART recipients uses (comparatively speaking) // see #235: deduplicating the list of PART recipients uses (comparatively speaking)
// a lot of RAM, so limit concurrency to avoid thrashing // a lot of RAM, so limit concurrency to avoid thrashing
client.server.semaphores.ClientDestroy.Acquire() client.server.semaphores.ClientDestroy.Acquire()
defer client.server.semaphores.ClientDestroy.Release() defer client.server.semaphores.ClientDestroy.Release()
if beingResumed { if beingResumed {
client.server.logger.Debug("quit", fmt.Sprintf("%s is being resumed", client.nick)) client.server.logger.Debug("quit", fmt.Sprintf("%s is being resumed", details.nick))
} else { } else {
client.server.logger.Debug("quit", fmt.Sprintf("%s is no longer on the server", client.nick)) client.server.logger.Debug("quit", fmt.Sprintf("%s is no longer on the server", details.nick))
} }
if !beingResumed { if !beingResumed {
client.server.whoWas.Append(client.WhoWas()) client.server.whoWas.Append(details.WhoWas)
} }
// remove from connection limits // remove from connection limits
@ -963,6 +977,7 @@ func (client *Client) destroy(beingResumed bool, session *Session) {
// clean up monitor state // clean up monitor state
client.server.monitorManager.RemoveAll(client) client.server.monitorManager.RemoveAll(client)
splitQuitMessage := utils.MakeSplitMessage(quitMessage, true)
// clean up channels // clean up channels
friends := make(ClientSet) friends := make(ClientSet)
for _, channel := range client.Channels() { for _, channel := range client.Channels() {
@ -972,7 +987,7 @@ func (client *Client) destroy(beingResumed bool, session *Session) {
Type: history.Quit, Type: history.Quit,
Nick: nickMaskString, Nick: nickMaskString,
AccountName: accountName, AccountName: accountName,
Message: utils.MakeSplitMessage(quitMessage, true), Message: splitQuitMessage,
}) })
} }
for _, member := range channel.Members() { for _, member := range channel.Members() {
@ -1007,14 +1022,14 @@ func (client *Client) destroy(beingResumed bool, session *Session) {
if quitMessage == "" { if quitMessage == "" {
quitMessage = "Exited" quitMessage = "Exited"
} }
friend.Send(nil, client.nickMaskString, "QUIT", quitMessage) friend.sendFromClientInternal(false, splitQuitMessage.Time, splitQuitMessage.Msgid, details.nickMask, details.accountName, nil, "QUIT", quitMessage)
} }
} }
if !client.exitedSnomaskSent { if !client.exitedSnomaskSent {
if beingResumed { if beingResumed {
client.server.snomasks.Send(sno.LocalQuits, fmt.Sprintf(ircfmt.Unescape("%s$r is resuming their connection, old client has been destroyed"), client.nick)) client.server.snomasks.Send(sno.LocalQuits, fmt.Sprintf(ircfmt.Unescape("%s$r is resuming their connection, old client has been destroyed"), client.nick))
} else { } else {
client.server.snomasks.Send(sno.LocalQuits, fmt.Sprintf(ircfmt.Unescape("%s$r exited the network"), client.nick)) client.server.snomasks.Send(sno.LocalQuits, fmt.Sprintf(ircfmt.Unescape("%s$r exited the network"), details.nick))
} }
} }
} }
@ -1031,15 +1046,7 @@ func (session *Session) sendSplitMsgFromClientInternal(blocking bool, serverTime
} }
} }
// SendFromClient sends an IRC line coming from a specific client. // Sends a line with `nickmask` as the prefix, adding `time` and `account` tags if supported
// Adds account-tag to the line as well.
func (client *Client) SendFromClient(msgid string, from *Client, tags map[string]string, command string, params ...string) error {
return client.sendFromClientInternal(false, time.Time{}, msgid, from.NickMaskString(), from.AccountName(), tags, command, params...)
}
// this is SendFromClient, but directly exposing nickmask and accountName,
// for things like history replay and CHGHOST where they no longer (necessarily)
// correspond to the current state of a client
func (client *Client) sendFromClientInternal(blocking bool, serverTime time.Time, msgid string, nickmask, accountName string, tags map[string]string, command string, params ...string) (err error) { func (client *Client) sendFromClientInternal(blocking bool, serverTime time.Time, msgid string, nickmask, accountName string, tags map[string]string, command string, params ...string) (err error) {
for _, session := range client.Sessions() { for _, session := range client.Sessions() {
err_ := session.sendFromClientInternal(blocking, serverTime, msgid, nickmask, accountName, tags, command, params...) err_ := session.sendFromClientInternal(blocking, serverTime, msgid, nickmask, accountName, tags, command, params...)
@ -1062,7 +1069,10 @@ func (session *Session) sendFromClientInternal(blocking bool, serverTime time.Ti
} }
// attach server-time // attach server-time
if session.capabilities.Has(caps.ServerTime) { if session.capabilities.Has(caps.ServerTime) {
msg.SetTag("time", time.Now().UTC().Format(IRCv3TimestampFormat)) if serverTime.IsZero() {
serverTime = time.Now().UTC()
}
msg.SetTag("time", serverTime.Format(IRCv3TimestampFormat))
} }
return session.SendRawMessage(msg, blocking) return session.SendRawMessage(msg, blocking)

View File

@ -585,36 +585,36 @@ func capHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Respo
// e.g., CHATHISTORY #ircv3 BETWEEN timestamp=YYYY-MM-DDThh:mm:ss.sssZ timestamp=YYYY-MM-DDThh:mm:ss.sssZ + 100 // e.g., CHATHISTORY #ircv3 BETWEEN timestamp=YYYY-MM-DDThh:mm:ss.sssZ timestamp=YYYY-MM-DDThh:mm:ss.sssZ + 100
func chathistoryHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) (exiting bool) { func chathistoryHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) (exiting bool) {
config := server.Config() config := server.Config()
// batch type is chathistory; send an empty batch if necessary
rb.InitializeBatch("chathistory", true)
var items []history.Item var items []history.Item
success := false success := false
var hist *history.Buffer var hist *history.Buffer
var channel *Channel var channel *Channel
defer func() { defer func() {
if success { // successful responses are sent as a chathistory or history batch
if success && 0 < len(items) {
batchType := "chathistory"
if rb.session.capabilities.Has(caps.EventPlayback) {
batchType = "history"
}
rb.ForceBatchStart(batchType, true)
if channel == nil { if channel == nil {
client.replayPrivmsgHistory(rb, items, true) client.replayPrivmsgHistory(rb, items, true)
} else { } else {
channel.replayHistoryItems(rb, items) channel.replayHistoryItems(rb, items, false)
} }
}
rb.Send(true) // terminate the chathistory batch
if success && len(items) > 0 {
return return
} }
newRb := NewResponseBuffer(rb.session)
newRb.Label = rb.Label // same label, new batch // errors are sent either without a batch, or in a draft/labeled-response batch as usual
// TODO: send `WARN CHATHISTORY MAX_MESSAGES_EXCEEDED` when appropriate // TODO: send `WARN CHATHISTORY MAX_MESSAGES_EXCEEDED` when appropriate
if hist == nil { if hist == nil {
newRb.Add(nil, server.name, "ERR", "CHATHISTORY", "NO_SUCH_CHANNEL") rb.Add(nil, server.name, "ERR", "CHATHISTORY", "NO_SUCH_CHANNEL")
} else if len(items) == 0 { } else if len(items) == 0 {
newRb.Add(nil, server.name, "ERR", "CHATHISTORY", "NO_TEXT_TO_SEND") rb.Add(nil, server.name, "ERR", "CHATHISTORY", "NO_TEXT_TO_SEND")
} else if !success { } else if !success {
newRb.Add(nil, server.name, "ERR", "CHATHISTORY", "NEED_MORE_PARAMS") rb.Add(nil, server.name, "ERR", "CHATHISTORY", "NEED_MORE_PARAMS")
} }
newRb.Send(true)
}() }()
target := msg.Params[0] target := msg.Params[0]
@ -744,7 +744,7 @@ func chathistoryHandler(server *Server, client *Client, msg ircmsg.IrcMessage, r
} }
} else { } else {
matches = func(item history.Item) bool { matches = func(item history.Item) bool {
return before == item.Time.Before(timestamp) return before == item.Message.Time.Before(timestamp)
} }
} }
items = hist.Match(matches, !before, limit) items = hist.Match(matches, !before, limit)
@ -767,7 +767,7 @@ func chathistoryHandler(server *Server, client *Client, msg ircmsg.IrcMessage, r
} }
} else { } else {
matches = func(item history.Item) bool { matches = func(item history.Item) bool {
return item.Time.After(timestamp) return item.Message.Time.After(timestamp)
} }
} }
items = hist.Match(matches, false, limit) items = hist.Match(matches, false, limit)
@ -790,16 +790,16 @@ func chathistoryHandler(server *Server, client *Client, msg ircmsg.IrcMessage, r
} }
} else { } else {
initialMatcher = func(item history.Item) (result bool) { initialMatcher = func(item history.Item) (result bool) {
return item.Time.Before(timestamp) return item.Message.Time.Before(timestamp)
} }
} }
var halfLimit int var halfLimit int
halfLimit = (limit + 1) / 2 halfLimit = (limit + 1) / 2
firstPass := hist.Match(initialMatcher, false, halfLimit) firstPass := hist.Match(initialMatcher, false, halfLimit)
if len(firstPass) > 0 { if len(firstPass) > 0 {
timeWindowStart := firstPass[0].Time timeWindowStart := firstPass[0].Message.Time
items = hist.Match(func(item history.Item) bool { items = hist.Match(func(item history.Item) bool {
return item.Time.Equal(timeWindowStart) || item.Time.After(timeWindowStart) return item.Message.Time.Equal(timeWindowStart) || item.Message.Time.After(timeWindowStart)
}, true, limit) }, true, limit)
} }
success = true success = true
@ -1109,7 +1109,7 @@ func historyHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *R
items := hist.Latest(limit) items := hist.Latest(limit)
if channel != nil { if channel != nil {
channel.replayHistoryItems(rb, items) channel.replayHistoryItems(rb, items, false)
} else { } else {
client.replayPrivmsgHistory(rb, items, true) client.replayPrivmsgHistory(rb, items, true)
} }
@ -1960,7 +1960,6 @@ func messageHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *R
for i, targetString := range targets { for i, targetString := range targets {
// each target gets distinct msgids // each target gets distinct msgids
splitMsg := utils.MakeSplitMessage(message, !rb.session.capabilities.Has(caps.MaxLine)) splitMsg := utils.MakeSplitMessage(message, !rb.session.capabilities.Has(caps.MaxLine))
now := time.Now().UTC()
// max of four targets per privmsg // max of four targets per privmsg
if i > maxTargets-1 { if i > maxTargets-1 {
@ -2009,17 +2008,17 @@ func messageHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *R
if histType == history.Tagmsg { if histType == history.Tagmsg {
// don't send TAGMSG at all if they don't have the tags cap // don't send TAGMSG at all if they don't have the tags cap
if session.capabilities.Has(caps.MessageTags) { if session.capabilities.Has(caps.MessageTags) {
session.sendFromClientInternal(false, now, splitMsg.Msgid, nickMaskString, accountName, clientOnlyTags, msg.Command, tnick) session.sendFromClientInternal(false, splitMsg.Time, splitMsg.Msgid, nickMaskString, accountName, clientOnlyTags, msg.Command, tnick)
} }
} else { } else {
session.sendSplitMsgFromClientInternal(false, now, nickMaskString, accountName, clientOnlyTags, msg.Command, tnick, splitMsg) session.sendSplitMsgFromClientInternal(false, splitMsg.Time, nickMaskString, accountName, clientOnlyTags, msg.Command, tnick, splitMsg)
} }
} }
} }
// an echo-message may need to be included in the response: // an echo-message may need to be included in the response:
if rb.session.capabilities.Has(caps.EchoMessage) { if rb.session.capabilities.Has(caps.EchoMessage) {
if histType == history.Tagmsg && rb.session.capabilities.Has(caps.MessageTags) { if histType == history.Tagmsg && rb.session.capabilities.Has(caps.MessageTags) {
rb.AddFromClient(splitMsg.Msgid, nickMaskString, accountName, clientOnlyTags, msg.Command, tnick) rb.AddFromClient(splitMsg.Time, splitMsg.Msgid, nickMaskString, accountName, clientOnlyTags, msg.Command, tnick)
} else { } else {
rb.AddSplitMessageFromClient(nickMaskString, accountName, clientOnlyTags, msg.Command, tnick, splitMsg) rb.AddSplitMessageFromClient(nickMaskString, accountName, clientOnlyTags, msg.Command, tnick, splitMsg)
} }
@ -2030,9 +2029,9 @@ func messageHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *R
continue continue
} }
if histType == history.Tagmsg && rb.session.capabilities.Has(caps.MessageTags) { if histType == history.Tagmsg && rb.session.capabilities.Has(caps.MessageTags) {
session.sendFromClientInternal(false, now, splitMsg.Msgid, nickMaskString, accountName, clientOnlyTags, msg.Command, tnick) session.sendFromClientInternal(false, splitMsg.Time, splitMsg.Msgid, nickMaskString, accountName, clientOnlyTags, msg.Command, tnick)
} else { } else {
session.sendSplitMsgFromClientInternal(false, now, nickMaskString, accountName, clientOnlyTags, msg.Command, tnick, splitMsg) session.sendSplitMsgFromClientInternal(false, splitMsg.Time, nickMaskString, accountName, clientOnlyTags, msg.Command, tnick, splitMsg)
} }
} }
if histType != history.Notice && user.Away() { if histType != history.Notice && user.Away() {

View File

@ -22,25 +22,52 @@ const (
Quit Quit
Mode Mode
Tagmsg Tagmsg
Nick
) )
// a Tagmsg that consists entirely of junk tags is not stored
var junkTags = map[string]bool{
"+draft/typing": true,
"+typing": true, // future-proofing
}
// Item represents an event (e.g., a PRIVMSG or a JOIN) and its associated data // Item represents an event (e.g., a PRIVMSG or a JOIN) and its associated data
type Item struct { type Item struct {
Type ItemType Type ItemType
Time time.Time
Nick string Nick string
// this is the uncasefolded account name, if there's no account it should be set to "*" // this is the uncasefolded account name, if there's no account it should be set to "*"
AccountName string AccountName string
Message utils.SplitMessage
// for non-privmsg items, we may stuff some other data in here // for non-privmsg items, we may stuff some other data in here
Message utils.SplitMessage
Tags map[string]string
Params [1]string
} }
// HasMsgid tests whether a message has the message id `msgid`. // HasMsgid tests whether a message has the message id `msgid`.
func (item *Item) HasMsgid(msgid string) bool { func (item *Item) HasMsgid(msgid string) bool {
// XXX we stuff other data in the Msgid field sometimes, if item.Message.Msgid == msgid {
// don't match it by accident return true
return (item.Type == Privmsg || item.Type == Notice) && item.Message.Msgid == msgid }
for _, pair := range item.Message.Wrapped {
if pair.Msgid == msgid {
return true
}
}
return false
}
func (item *Item) isStorable() bool {
if item.Type == Tagmsg {
for name := range item.Tags {
if !junkTags[name] {
return true
}
}
return false // all tags were blacklisted
} else {
return true
}
} }
type Predicate func(item Item) (matches bool) type Predicate func(item Item) (matches bool)
@ -94,8 +121,12 @@ func (list *Buffer) Add(item Item) {
return return
} }
if item.Time.IsZero() { if !item.isStorable() {
item.Time = time.Now().UTC() return
}
if item.Message.Time.IsZero() {
item.Message.Time = time.Now().UTC()
} }
list.Lock() list.Lock()
@ -114,8 +145,8 @@ func (list *Buffer) Add(item Item) {
list.end = (list.end + 1) % len(list.buffer) list.end = (list.end + 1) % len(list.buffer)
list.start = list.end // advance start as well, overwriting first entry list.start = list.end // advance start as well, overwriting first entry
// record the timestamp of the overwritten item // record the timestamp of the overwritten item
if list.lastDiscarded.Before(list.buffer[pos].Time) { if list.lastDiscarded.Before(list.buffer[pos].Message.Time) {
list.lastDiscarded = list.buffer[pos].Time list.lastDiscarded = list.buffer[pos].Message.Time
} }
} }
@ -144,7 +175,7 @@ func (list *Buffer) Between(after, before time.Time, ascending bool, limit int)
complete = after.Equal(list.lastDiscarded) || after.After(list.lastDiscarded) complete = after.Equal(list.lastDiscarded) || after.After(list.lastDiscarded)
satisfies := func(item Item) bool { satisfies := func(item Item) bool {
return (after.IsZero() || item.Time.After(after)) && (before.IsZero() || item.Time.Before(before)) return (after.IsZero() || item.Message.Time.After(after)) && (before.IsZero() || item.Message.Time.Before(before))
} }
return list.matchInternal(satisfies, ascending, limit), complete return list.matchInternal(satisfies, ascending, limit), complete
@ -264,8 +295,8 @@ func (list *Buffer) Resize(size int) {
} }
// update lastDiscarded for discarded entries // update lastDiscarded for discarded entries
for i := list.start; i != start; i = (i + 1) % len(list.buffer) { for i := list.start; i != start; i = (i + 1) % len(list.buffer) {
if list.lastDiscarded.Before(list.buffer[i].Time) { if list.lastDiscarded.Before(list.buffer[i].Message.Time) {
list.lastDiscarded = list.buffer[i].Time list.lastDiscarded = list.buffer[i].Message.Time
} }
} }
} }

View File

@ -87,6 +87,12 @@ func easyParse(timestamp string) time.Time {
return result return result
} }
func easyItem(nick string, timestamp string) (result Item) {
result.Message.Time = easyParse(timestamp)
result.Nick = nick
return
}
func assertEqual(supplied, expected interface{}, t *testing.T) { func assertEqual(supplied, expected interface{}, t *testing.T) {
if !reflect.DeepEqual(supplied, expected) { if !reflect.DeepEqual(supplied, expected) {
t.Errorf("expected %v but got %v", expected, supplied) t.Errorf("expected %v but got %v", expected, supplied)
@ -97,30 +103,19 @@ func TestBuffer(t *testing.T) {
start := easyParse("2006-01-01 00:00:00Z") start := easyParse("2006-01-01 00:00:00Z")
buf := NewHistoryBuffer(3) buf := NewHistoryBuffer(3)
buf.Add(Item{ buf.Add(easyItem("testnick0", "2006-01-01 15:04:05Z"))
Nick: "testnick0",
Time: easyParse("2006-01-01 15:04:05Z"),
})
buf.Add(Item{ buf.Add(easyItem("testnick1", "2006-01-02 15:04:05Z"))
Nick: "testnick1",
Time: easyParse("2006-01-02 15:04:05Z"),
})
buf.Add(Item{ buf.Add(easyItem("testnick2", "2006-01-03 15:04:05Z"))
Nick: "testnick2",
Time: easyParse("2006-01-03 15:04:05Z"),
})
since, complete := buf.Between(start, time.Now(), false, 0) since, complete := buf.Between(start, time.Now(), false, 0)
assertEqual(complete, true, t) assertEqual(complete, true, t)
assertEqual(toNicks(since), []string{"testnick0", "testnick1", "testnick2"}, t) assertEqual(toNicks(since), []string{"testnick0", "testnick1", "testnick2"}, t)
// add another item, evicting the first // add another item, evicting the first
buf.Add(Item{ buf.Add(easyItem("testnick3", "2006-01-04 15:04:05Z"))
Nick: "testnick3",
Time: easyParse("2006-01-04 15:04:05Z"),
})
since, complete = buf.Between(start, time.Now(), false, 0) since, complete = buf.Between(start, time.Now(), false, 0)
assertEqual(complete, false, t) assertEqual(complete, false, t)
assertEqual(toNicks(since), []string{"testnick1", "testnick2", "testnick3"}, t) assertEqual(toNicks(since), []string{"testnick1", "testnick2", "testnick3"}, t)
@ -139,18 +134,9 @@ func TestBuffer(t *testing.T) {
assertEqual(toNicks(since), []string{"testnick2", "testnick3"}, t) assertEqual(toNicks(since), []string{"testnick2", "testnick3"}, t)
buf.Resize(5) buf.Resize(5)
buf.Add(Item{ buf.Add(easyItem("testnick4", "2006-01-05 15:04:05Z"))
Nick: "testnick4", buf.Add(easyItem("testnick5", "2006-01-06 15:04:05Z"))
Time: easyParse("2006-01-05 15:04:05Z"), buf.Add(easyItem("testnick6", "2006-01-07 15:04:05Z"))
})
buf.Add(Item{
Nick: "testnick5",
Time: easyParse("2006-01-06 15:04:05Z"),
})
buf.Add(Item{
Nick: "testnick6",
Time: easyParse("2006-01-07 15:04:05Z"),
})
since, complete = buf.Between(easyParse("2006-01-03 00:00:00Z"), time.Now(), false, 0) since, complete = buf.Between(easyParse("2006-01-03 00:00:00Z"), time.Now(), false, 0)
assertEqual(complete, true, t) assertEqual(complete, true, t)
assertEqual(toNicks(since), []string{"testnick2", "testnick3", "testnick4", "testnick5", "testnick6"}, t) assertEqual(toNicks(since), []string{"testnick2", "testnick3", "testnick4", "testnick5", "testnick6"}, t)

View File

@ -11,7 +11,9 @@ import (
"strings" "strings"
"github.com/goshuirc/irc-go/ircfmt" "github.com/goshuirc/irc-go/ircfmt"
"github.com/oragono/oragono/irc/history"
"github.com/oragono/oragono/irc/sno" "github.com/oragono/oragono/irc/sno"
"github.com/oragono/oragono/irc/utils"
) )
var ( var (
@ -44,7 +46,7 @@ func performNickChange(server *Server, client *Client, target *Client, session *
hadNick := target.HasNick() hadNick := target.HasNick()
origNickMask := target.NickMaskString() origNickMask := target.NickMaskString()
whowas := target.WhoWas() details := target.Details()
err = client.server.clients.SetNick(target, session, nickname) err = client.server.clients.SetNick(target, session, nickname)
if err == errNicknameInUse { if err == errNicknameInUse {
rb.Add(nil, server.name, ERR_NICKNAMEINUSE, currentNick, nickname, client.t("Nickname is already in use")) rb.Add(nil, server.name, ERR_NICKNAMEINUSE, currentNick, nickname, client.t("Nickname is already in use"))
@ -57,18 +59,31 @@ func performNickChange(server *Server, client *Client, target *Client, session *
return false return false
} }
message := utils.MakeSplitMessage("", true)
histItem := history.Item{
Type: history.Nick,
Nick: origNickMask,
AccountName: details.accountName,
Message: message,
}
histItem.Params[0] = nickname
client.server.logger.Debug("nick", fmt.Sprintf("%s changed nickname to %s [%s]", origNickMask, nickname, cfnick)) client.server.logger.Debug("nick", fmt.Sprintf("%s changed nickname to %s [%s]", origNickMask, nickname, cfnick))
if hadNick { if hadNick {
target.server.snomasks.Send(sno.LocalNicks, fmt.Sprintf(ircfmt.Unescape("$%s$r changed nickname to %s"), whowas.nick, nickname)) target.server.snomasks.Send(sno.LocalNicks, fmt.Sprintf(ircfmt.Unescape("$%s$r changed nickname to %s"), details.nick, nickname))
target.server.whoWas.Append(whowas) target.server.whoWas.Append(details.WhoWas)
rb.Add(nil, origNickMask, "NICK", nickname) rb.AddFromClient(message.Time, message.Msgid, origNickMask, details.accountName, nil, "NICK", nickname)
for session := range target.Friends() { for session := range target.Friends() {
if session != rb.session { if session != rb.session {
session.Send(nil, origNickMask, "NICK", nickname) session.sendFromClientInternal(false, message.Time, message.Msgid, origNickMask, details.accountName, nil, "NICK", nickname)
} }
} }
} }
for _, channel := range client.Channels() {
channel.history.Add(histItem)
}
target.nickTimer.Touch(rb) target.nickTimer.Touch(rb)
if target.Registered() { if target.Registered() {

View File

@ -23,8 +23,19 @@ const (
// buffer will silently create a batch if required and label the outgoing messages as // buffer will silently create a batch if required and label the outgoing messages as
// necessary (or leave it off and simply tag the outgoing message). // necessary (or leave it off and simply tag the outgoing message).
type ResponseBuffer struct { type ResponseBuffer struct {
Label string Label string // label if this is a labeled response batch
batchID string 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`)
// stack of batch IDs of nested batches, which are handled separately
// from the underlying labeled-response batch. starting a new nested batch
// unconditionally enqueues its batch start message; subsequent messages
// are tagged with the nested batch ID, until nested batch end.
// (the nested batch start itself may have no batch tag, or the batch tag of the
// underlying labeled-response batch, or the batch tag of the next outermost
// nested batch.)
nestedBatches []string
messages []ircmsg.IrcMessage messages []ircmsg.IrcMessage
finalized bool finalized bool
target *Client target *Client
@ -40,8 +51,9 @@ func GetLabel(msg ircmsg.IrcMessage) string {
// NewResponseBuffer returns a new ResponseBuffer. // NewResponseBuffer returns a new ResponseBuffer.
func NewResponseBuffer(session *Session) *ResponseBuffer { func NewResponseBuffer(session *Session) *ResponseBuffer {
return &ResponseBuffer{ return &ResponseBuffer{
session: session, session: session,
target: session.client, target: session.client,
batchType: defaultBatchType,
} }
} }
@ -54,6 +66,9 @@ func (rb *ResponseBuffer) AddMessage(msg ircmsg.IrcMessage) {
return return
} }
if 0 < len(rb.nestedBatches) {
msg.SetTag("batch", rb.nestedBatches[len(rb.nestedBatches)-1])
}
rb.messages = append(rb.messages, msg) rb.messages = append(rb.messages, msg)
} }
@ -63,9 +78,11 @@ func (rb *ResponseBuffer) Add(tags map[string]string, prefix string, command str
} }
// AddFromClient adds a new message from a specific client to our queue. // AddFromClient adds a new message from a specific client to our queue.
func (rb *ResponseBuffer) AddFromClient(msgid string, fromNickMask string, fromAccount string, tags map[string]string, command string, params ...string) { func (rb *ResponseBuffer) AddFromClient(time time.Time, msgid string, fromNickMask string, fromAccount string, tags map[string]string, command string, params ...string) {
msg := ircmsg.MakeMessage(nil, fromNickMask, command, params...) msg := ircmsg.MakeMessage(nil, fromNickMask, command, params...)
msg.UpdateTags(tags) if rb.session.capabilities.Has(caps.MessageTags) {
msg.UpdateTags(tags)
}
// attach account-tag // attach account-tag
if rb.session.capabilities.Has(caps.AccountTag) && fromAccount != "*" { if rb.session.capabilities.Has(caps.AccountTag) && fromAccount != "*" {
@ -75,6 +92,10 @@ func (rb *ResponseBuffer) AddFromClient(msgid string, fromNickMask string, fromA
if len(msgid) > 0 && rb.session.capabilities.Has(caps.MessageTags) { if len(msgid) > 0 && rb.session.capabilities.Has(caps.MessageTags) {
msg.SetTag("draft/msgid", msgid) msg.SetTag("draft/msgid", msgid)
} }
// attach server-time
if rb.session.capabilities.Has(caps.ServerTime) && !msg.HasTag("time") {
msg.SetTag("time", time.UTC().Format(IRCv3TimestampFormat))
}
rb.AddMessage(msg) rb.AddMessage(msg)
} }
@ -82,33 +103,31 @@ func (rb *ResponseBuffer) AddFromClient(msgid string, fromNickMask string, fromA
// AddSplitMessageFromClient adds a new split message from a specific client to our queue. // AddSplitMessageFromClient adds a new split message from a specific client to our queue.
func (rb *ResponseBuffer) AddSplitMessageFromClient(fromNickMask string, fromAccount string, tags map[string]string, command string, target string, message utils.SplitMessage) { func (rb *ResponseBuffer) AddSplitMessageFromClient(fromNickMask string, fromAccount string, tags map[string]string, command string, target string, message utils.SplitMessage) {
if rb.session.capabilities.Has(caps.MaxLine) || message.Wrapped == nil { if rb.session.capabilities.Has(caps.MaxLine) || message.Wrapped == nil {
rb.AddFromClient(message.Msgid, fromNickMask, fromAccount, tags, command, target, message.Message) rb.AddFromClient(message.Time, message.Msgid, fromNickMask, fromAccount, tags, command, target, message.Message)
} else { } else {
for _, messagePair := range message.Wrapped { for _, messagePair := range message.Wrapped {
rb.AddFromClient(messagePair.Msgid, fromNickMask, fromAccount, tags, command, target, messagePair.Message) rb.AddFromClient(message.Time, messagePair.Msgid, fromNickMask, fromAccount, tags, command, target, messagePair.Message)
} }
} }
} }
// InitializeBatch forcibly starts a batch of batch `batchType`. // ForceBatchStart forcibly starts a batch of batch `batchType`.
// Normally, Send/Flush will decide automatically whether to start a batch // Normally, Send/Flush will decide automatically whether to start a batch
// of type draft/labeled-response. This allows changing the batch type // of type draft/labeled-response. This allows changing the batch type
// and forcing the creation of a possibly empty batch. // and forcing the creation of a possibly empty batch.
func (rb *ResponseBuffer) InitializeBatch(batchType string, blocking bool) { func (rb *ResponseBuffer) ForceBatchStart(batchType string, blocking bool) {
rb.sendBatchStart(batchType, blocking) rb.batchType = batchType
rb.sendBatchStart(blocking)
} }
func (rb *ResponseBuffer) sendBatchStart(batchType string, blocking bool) { func (rb *ResponseBuffer) sendBatchStart(blocking bool) {
if rb.batchID != "" { if rb.batchID != "" {
// batch already initialized // batch already initialized
return return
} }
// formerly this combined time.Now.UnixNano() in base 36 with an incrementing counter,
// also in base 36. but let's just use a uuidv4-alike (26 base32 characters):
rb.batchID = utils.GenerateSecretToken() rb.batchID = utils.GenerateSecretToken()
message := ircmsg.MakeMessage(nil, rb.target.server.name, "BATCH", "+"+rb.batchID, rb.batchType)
message := ircmsg.MakeMessage(nil, rb.target.server.name, "BATCH", "+"+rb.batchID, batchType)
if rb.Label != "" { if rb.Label != "" {
message.SetTag(caps.LabelTagName, rb.Label) message.SetTag(caps.LabelTagName, rb.Label)
} }
@ -125,6 +144,50 @@ func (rb *ResponseBuffer) sendBatchEnd(blocking bool) {
rb.session.SendRawMessage(message, blocking) rb.session.SendRawMessage(message, blocking)
} }
// Starts a nested batch (see the ResponseBuffer struct definition for a description of
// how this works)
func (rb *ResponseBuffer) StartNestedBatch(batchType string, params ...string) (batchID string) {
batchID = utils.GenerateSecretToken()
msgParams := make([]string, len(params)+2)
msgParams[0] = "+" + batchID
msgParams[1] = batchType
copy(msgParams[2:], params)
rb.AddMessage(ircmsg.MakeMessage(nil, rb.target.server.name, "BATCH", msgParams...))
rb.nestedBatches = append(rb.nestedBatches, batchID)
return
}
// Ends a nested batch
func (rb *ResponseBuffer) EndNestedBatch(batchID string) {
if batchID == "" {
return
}
if 0 == len(rb.nestedBatches) || rb.nestedBatches[len(rb.nestedBatches)-1] != batchID {
rb.target.server.logger.Error("internal", "inconsistent batch nesting detected")
debug.PrintStack()
return
}
rb.nestedBatches = rb.nestedBatches[0 : len(rb.nestedBatches)-1]
rb.AddMessage(ircmsg.MakeMessage(nil, rb.target.server.name, "BATCH", "-"+batchID))
}
// Convenience to start a nested batch for history lines, at the highest level
// 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) {
batchType = "chathistory"
}
if batchType != "" {
batchID = rb.StartNestedBatch(batchType, params...)
}
return
}
// Send sends all messages in the buffer to the client. // Send sends all messages in the buffer to the client.
// Afterwards, the buffer is in an undefined state and MUST NOT be used further. // Afterwards, the buffer is in an undefined state and MUST NOT be used further.
// If `blocking` is true you MUST be sending to the client from its own goroutine. // If `blocking` is true you MUST be sending to the client from its own goroutine.
@ -158,7 +221,7 @@ func (rb *ResponseBuffer) flushInternal(final bool, blocking bool) error {
if useLabel && !useBatch && len(rb.messages) == 1 && rb.batchID == "" { if useLabel && !useBatch && len(rb.messages) == 1 && rb.batchID == "" {
rb.messages[0].SetTag(caps.LabelTagName, rb.Label) rb.messages[0].SetTag(caps.LabelTagName, rb.Label)
} else if useBatch { } else if useBatch {
rb.sendBatchStart(defaultBatchType, blocking) rb.sendBatchStart(blocking)
} }
// send each message out // send each message out
@ -168,8 +231,9 @@ func (rb *ResponseBuffer) flushInternal(final bool, blocking bool) error {
message.SetTag("time", time.Now().UTC().Format(IRCv3TimestampFormat)) message.SetTag("time", time.Now().UTC().Format(IRCv3TimestampFormat))
} }
// attach batch ID // attach batch ID, unless this message was part of a nested batch and is
if rb.batchID != "" { // already tagged
if rb.batchID != "" && !message.HasTag("batch") {
message.SetTag("batch", rb.batchID) message.SetTag("batch", rb.batchID)
} }

View File

@ -4,6 +4,7 @@
package utils package utils
import "bytes" import "bytes"
import "time"
// WordWrap wraps the given text into a series of lines that don't exceed lineWidth characters. // WordWrap wraps the given text into a series of lines that don't exceed lineWidth characters.
func WordWrap(text string, lineWidth int) []string { func WordWrap(text string, lineWidth int) []string {
@ -59,6 +60,7 @@ type MessagePair struct {
type SplitMessage struct { type SplitMessage struct {
MessagePair MessagePair
Wrapped []MessagePair // if this is nil, `Message` didn't need wrapping and can be sent to anyone Wrapped []MessagePair // if this is nil, `Message` didn't need wrapping and can be sent to anyone
Time time.Time
} }
const defaultLineWidth = 400 const defaultLineWidth = 400
@ -66,6 +68,7 @@ const defaultLineWidth = 400
func MakeSplitMessage(original string, origIs512 bool) (result SplitMessage) { func MakeSplitMessage(original string, origIs512 bool) (result SplitMessage) {
result.Message = original result.Message = original
result.Msgid = GenerateSecretToken() result.Msgid = GenerateSecretToken()
result.Time = time.Now().UTC()
if !origIs512 && defaultLineWidth < len(original) { if !origIs512 && defaultLineWidth < len(original) {
wrapped := WordWrap(original, defaultLineWidth) wrapped := WordWrap(original, defaultLineWidth)