mirror of
https://github.com/ergochat/ergo.git
synced 2024-11-10 22:19:31 +01:00
Merge pull request #344 from slingamn/history.4
add CHATHISTORY and HISTORY implementations
This commit is contained in:
commit
71a33890b8
@ -578,7 +578,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)
|
items, complete := channel.history.Between(after, before, false, 0)
|
||||||
rb := NewResponseBuffer(newClient)
|
rb := NewResponseBuffer(newClient)
|
||||||
channel.replayHistoryItems(rb, items)
|
channel.replayHistoryItems(rb, items)
|
||||||
if !complete && !newClient.resumeDetails.HistoryIncomplete {
|
if !complete && !newClient.resumeDetails.HistoryIncomplete {
|
||||||
|
@ -479,7 +479,7 @@ func (client *Client) TryResume() {
|
|||||||
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
|
||||||
}
|
}
|
||||||
privmsgHistory := oldClient.history.Match(privmsgMatcher, 0)
|
privmsgHistory := oldClient.history.Match(privmsgMatcher, false, 0)
|
||||||
lastDiscarded := oldClient.history.LastDiscarded()
|
lastDiscarded := oldClient.history.LastDiscarded()
|
||||||
if lastDiscarded.Before(oldestLostMessage) {
|
if lastDiscarded.Before(oldestLostMessage) {
|
||||||
oldestLostMessage = lastDiscarded
|
oldestLostMessage = lastDiscarded
|
||||||
@ -541,28 +541,39 @@ func (client *Client) tryResumeChannels() {
|
|||||||
// replay direct PRIVSMG history
|
// replay direct PRIVSMG history
|
||||||
if !details.Timestamp.IsZero() {
|
if !details.Timestamp.IsZero() {
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
nick := client.Nick()
|
items, complete := client.history.Between(details.Timestamp, now, false, 0)
|
||||||
items, complete := client.history.Between(details.Timestamp, now)
|
rb := NewResponseBuffer(client)
|
||||||
for _, item := range items {
|
client.replayPrivmsgHistory(rb, items, complete)
|
||||||
var command string
|
rb.Send(true)
|
||||||
switch item.Type {
|
|
||||||
case history.Privmsg:
|
|
||||||
command = "PRIVMSG"
|
|
||||||
case history.Notice:
|
|
||||||
command = "NOTICE"
|
|
||||||
default:
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
client.sendSplitMsgFromClientInternal(true, item.Time, item.Msgid, item.Nick, item.AccountName, nil, command, nick, item.Message)
|
|
||||||
}
|
|
||||||
if !complete {
|
|
||||||
client.Send(nil, "HistServ", "NOTICE", nick, client.t("Some additional message history may have been lost"))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
details.OldClient.destroy(true)
|
details.OldClient.destroy(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (client *Client) replayPrivmsgHistory(rb *ResponseBuffer, items []history.Item, complete bool) {
|
||||||
|
nick := client.Nick()
|
||||||
|
serverTime := client.capabilities.Has(caps.ServerTime)
|
||||||
|
for _, item := range items {
|
||||||
|
var command string
|
||||||
|
switch item.Type {
|
||||||
|
case history.Privmsg:
|
||||||
|
command = "PRIVMSG"
|
||||||
|
case history.Notice:
|
||||||
|
command = "NOTICE"
|
||||||
|
default:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var tags Tags
|
||||||
|
if serverTime {
|
||||||
|
tags = ensureTag(tags, "time", item.Time.Format(IRCv3TimestampFormat))
|
||||||
|
}
|
||||||
|
rb.AddSplitMessageFromClient(item.Msgid, item.Nick, item.AccountName, tags, command, nick, item.Message)
|
||||||
|
}
|
||||||
|
if !complete {
|
||||||
|
rb.Add(nil, "HistServ", "NOTICE", nick, client.t("Some additional message history may have been lost"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// copy applicable state from oldClient to client as part of a resume
|
// copy applicable state from oldClient to client as part of a resume
|
||||||
func (client *Client) copyResumeData(oldClient *Client) {
|
func (client *Client) copyResumeData(oldClient *Client) {
|
||||||
oldClient.stateMutex.RLock()
|
oldClient.stateMutex.RLock()
|
||||||
|
@ -92,6 +92,10 @@ func init() {
|
|||||||
usablePreReg: true,
|
usablePreReg: true,
|
||||||
minParams: 1,
|
minParams: 1,
|
||||||
},
|
},
|
||||||
|
"CHATHISTORY": {
|
||||||
|
handler: chathistoryHandler,
|
||||||
|
minParams: 3,
|
||||||
|
},
|
||||||
"DEBUG": {
|
"DEBUG": {
|
||||||
handler: debugHandler,
|
handler: debugHandler,
|
||||||
minParams: 1,
|
minParams: 1,
|
||||||
@ -110,6 +114,10 @@ func init() {
|
|||||||
handler: helpHandler,
|
handler: helpHandler,
|
||||||
minParams: 0,
|
minParams: 0,
|
||||||
},
|
},
|
||||||
|
"HISTORY": {
|
||||||
|
handler: historyHandler,
|
||||||
|
minParams: 1,
|
||||||
|
},
|
||||||
"INFO": {
|
"INFO": {
|
||||||
handler: infoHandler,
|
handler: infoHandler,
|
||||||
},
|
},
|
||||||
|
@ -322,6 +322,7 @@ type Config struct {
|
|||||||
ChannelLength int `yaml:"channel-length"`
|
ChannelLength int `yaml:"channel-length"`
|
||||||
ClientLength int `yaml:"client-length"`
|
ClientLength int `yaml:"client-length"`
|
||||||
AutoreplayOnJoin int `yaml:"autoreplay-on-join"`
|
AutoreplayOnJoin int `yaml:"autoreplay-on-join"`
|
||||||
|
ChathistoryMax int `yaml:"chathistory-maxmessages"`
|
||||||
}
|
}
|
||||||
|
|
||||||
Filename string
|
Filename string
|
||||||
|
@ -41,6 +41,7 @@ var (
|
|||||||
errResumeTokenAlreadySet = errors.New("Client was already assigned a resume token")
|
errResumeTokenAlreadySet = errors.New("Client was already assigned a resume token")
|
||||||
errInvalidUsername = errors.New("Invalid username")
|
errInvalidUsername = errors.New("Invalid username")
|
||||||
errFeatureDisabled = errors.New("That feature is disabled")
|
errFeatureDisabled = errors.New("That feature is disabled")
|
||||||
|
errInvalidParams = errors.New("Invalid parameters")
|
||||||
)
|
)
|
||||||
|
|
||||||
// Socket Errors
|
// Socket Errors
|
||||||
|
279
irc/handlers.go
279
irc/handlers.go
@ -522,6 +522,233 @@ func capHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Respo
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CHATHISTORY <target> <preposition> <query> [<limit>]
|
||||||
|
// e.g., CHATHISTORY #ircv3 AFTER id=ytNBbt565yt4r3err3 10
|
||||||
|
// CHATHISTORY <target> BETWEEN <query> <query> <direction> [<limit>]
|
||||||
|
// 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) {
|
||||||
|
config := server.Config()
|
||||||
|
// batch type is chathistory; send an empty batch if necessary
|
||||||
|
rb.InitializeBatch("chathistory", true)
|
||||||
|
|
||||||
|
var items []history.Item
|
||||||
|
success := false
|
||||||
|
var hist *history.Buffer
|
||||||
|
var channel *Channel
|
||||||
|
defer func() {
|
||||||
|
if success {
|
||||||
|
if channel == nil {
|
||||||
|
client.replayPrivmsgHistory(rb, items, true)
|
||||||
|
} else {
|
||||||
|
channel.replayHistoryItems(rb, items)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rb.Send(true) // terminate the chathistory batch
|
||||||
|
if success && len(items) > 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
newRb := NewResponseBuffer(client)
|
||||||
|
newRb.Label = rb.Label // same label, new batch
|
||||||
|
// TODO: send `WARN CHATHISTORY MAX_MESSAGES_EXCEEDED` when appropriate
|
||||||
|
if !success {
|
||||||
|
newRb.Add(nil, server.name, "ERR", "CHATHISTORY", "NEED_MORE_PARAMS")
|
||||||
|
} else if hist == nil {
|
||||||
|
newRb.Add(nil, server.name, "ERR", "CHATHISTORY", "NO_SUCH_CHANNEL")
|
||||||
|
} else if len(items) == 0 {
|
||||||
|
newRb.Add(nil, server.name, "ERR", "CHATHISTORY", "NO_TEXT_TO_SEND")
|
||||||
|
}
|
||||||
|
newRb.Send(true)
|
||||||
|
}()
|
||||||
|
|
||||||
|
target := msg.Params[0]
|
||||||
|
channel = server.channels.Get(target)
|
||||||
|
if channel != nil {
|
||||||
|
hist = &channel.history
|
||||||
|
} else {
|
||||||
|
targetClient := server.clients.Get(target)
|
||||||
|
if targetClient != nil {
|
||||||
|
myAccount := client.Account()
|
||||||
|
targetAccount := targetClient.Account()
|
||||||
|
if myAccount != "" && targetAccount != "" && myAccount == targetAccount {
|
||||||
|
hist = targetClient.history
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if hist == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
preposition := strings.ToLower(msg.Params[1])
|
||||||
|
|
||||||
|
parseQueryParam := func(param string) (msgid string, timestamp time.Time, err error) {
|
||||||
|
err = errInvalidParams
|
||||||
|
pieces := strings.SplitN(param, "=", 2)
|
||||||
|
if len(pieces) < 2 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
identifier, value := strings.ToLower(pieces[0]), pieces[1]
|
||||||
|
if identifier == "id" {
|
||||||
|
msgid, err = value, nil
|
||||||
|
return
|
||||||
|
} else if identifier == "timestamp" {
|
||||||
|
timestamp, err = time.Parse(IRCv3TimestampFormat, value)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
maxChathistoryLimit := config.History.ChathistoryMax
|
||||||
|
if maxChathistoryLimit == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
parseHistoryLimit := func(paramIndex int) (limit int) {
|
||||||
|
if len(msg.Params) < (paramIndex + 1) {
|
||||||
|
return maxChathistoryLimit
|
||||||
|
}
|
||||||
|
limit, err := strconv.Atoi(msg.Params[paramIndex])
|
||||||
|
if err != nil || limit == 0 || limit > maxChathistoryLimit {
|
||||||
|
limit = maxChathistoryLimit
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: as currently implemented, almost all of thes queries are worst-case O(n)
|
||||||
|
// in the number of stored history entries. Every one of them can be made O(1)
|
||||||
|
// if necessary, without too much difficulty. Some ideas:
|
||||||
|
// * Ensure that the ring buffer is sorted by time, enabling binary search for times
|
||||||
|
// * Maintain a map from msgid to position in the ring buffer
|
||||||
|
|
||||||
|
if preposition == "between" {
|
||||||
|
if len(msg.Params) >= 5 {
|
||||||
|
startMsgid, startTimestamp, startErr := parseQueryParam(msg.Params[2])
|
||||||
|
endMsgid, endTimestamp, endErr := parseQueryParam(msg.Params[3])
|
||||||
|
ascending := msg.Params[4] == "+"
|
||||||
|
limit := parseHistoryLimit(5)
|
||||||
|
if startErr != nil || endErr != nil {
|
||||||
|
success = false
|
||||||
|
} else if startMsgid != "" && endMsgid != "" {
|
||||||
|
inInterval := false
|
||||||
|
matches := func(item history.Item) (result bool) {
|
||||||
|
result = inInterval
|
||||||
|
if item.HasMsgid(startMsgid) {
|
||||||
|
if ascending {
|
||||||
|
inInterval = true
|
||||||
|
} else {
|
||||||
|
inInterval = false
|
||||||
|
return false // interval is exclusive
|
||||||
|
}
|
||||||
|
} else if item.HasMsgid(endMsgid) {
|
||||||
|
if ascending {
|
||||||
|
inInterval = false
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
inInterval = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
items = hist.Match(matches, ascending, limit)
|
||||||
|
success = true
|
||||||
|
} else if !startTimestamp.IsZero() && !endTimestamp.IsZero() {
|
||||||
|
items, _ = hist.Between(startTimestamp, endTimestamp, ascending, limit)
|
||||||
|
if !ascending {
|
||||||
|
history.Reverse(items)
|
||||||
|
}
|
||||||
|
success = true
|
||||||
|
}
|
||||||
|
// else: mismatched params, success = false, fail
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// before, after, latest, around
|
||||||
|
queryParam := msg.Params[2]
|
||||||
|
msgid, timestamp, err := parseQueryParam(queryParam)
|
||||||
|
limit := parseHistoryLimit(3)
|
||||||
|
before := false
|
||||||
|
switch preposition {
|
||||||
|
case "before":
|
||||||
|
before = true
|
||||||
|
fallthrough
|
||||||
|
case "after":
|
||||||
|
var matches history.Predicate
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
} else if msgid != "" {
|
||||||
|
inInterval := false
|
||||||
|
matches = func(item history.Item) (result bool) {
|
||||||
|
result = inInterval
|
||||||
|
if item.HasMsgid(msgid) {
|
||||||
|
inInterval = true
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
matches = func(item history.Item) bool {
|
||||||
|
return before == item.Time.Before(timestamp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
items = hist.Match(matches, !before, limit)
|
||||||
|
success = true
|
||||||
|
case "latest":
|
||||||
|
if queryParam == "*" {
|
||||||
|
items = hist.Latest(limit)
|
||||||
|
} else if err != nil {
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
var matches history.Predicate
|
||||||
|
if msgid != "" {
|
||||||
|
shouldStop := false
|
||||||
|
matches = func(item history.Item) bool {
|
||||||
|
if shouldStop {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
shouldStop = item.HasMsgid(msgid)
|
||||||
|
return !shouldStop
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
matches = func(item history.Item) bool {
|
||||||
|
return item.Time.After(timestamp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
items = hist.Match(matches, false, limit)
|
||||||
|
}
|
||||||
|
success = true
|
||||||
|
case "around":
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
var initialMatcher history.Predicate
|
||||||
|
if msgid != "" {
|
||||||
|
inInterval := false
|
||||||
|
initialMatcher = func(item history.Item) (result bool) {
|
||||||
|
if inInterval {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
inInterval = item.HasMsgid(msgid)
|
||||||
|
return inInterval
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
initialMatcher = func(item history.Item) (result bool) {
|
||||||
|
return item.Time.Before(timestamp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var halfLimit int
|
||||||
|
halfLimit = (limit + 1) / 2
|
||||||
|
firstPass := hist.Match(initialMatcher, false, halfLimit)
|
||||||
|
if len(firstPass) > 0 {
|
||||||
|
timeWindowStart := firstPass[0].Time
|
||||||
|
items = hist.Match(func(item history.Item) bool {
|
||||||
|
return item.Time.Equal(timeWindowStart) || item.Time.After(timeWindowStart)
|
||||||
|
}, true, limit)
|
||||||
|
}
|
||||||
|
success = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// DEBUG <subcmd>
|
// DEBUG <subcmd>
|
||||||
func debugHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
|
func debugHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
|
||||||
param := strings.ToUpper(msg.Params[0])
|
param := strings.ToUpper(msg.Params[0])
|
||||||
@ -779,6 +1006,58 @@ Get an explanation of <argument>, or "index" for a list of help topics.`), rb)
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HISTORY <target> [<limit>]
|
||||||
|
// e.g., HISTORY #ubuntu 10
|
||||||
|
// HISTORY me 15
|
||||||
|
func historyHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
|
||||||
|
config := server.Config()
|
||||||
|
target := msg.Params[0]
|
||||||
|
var hist *history.Buffer
|
||||||
|
channel := server.channels.Get(target)
|
||||||
|
if channel != nil {
|
||||||
|
hist = &channel.history
|
||||||
|
} else {
|
||||||
|
if strings.ToLower(target) == "me" {
|
||||||
|
hist = client.history
|
||||||
|
} else {
|
||||||
|
targetClient := server.clients.Get(target)
|
||||||
|
if targetClient != nil {
|
||||||
|
myAccount, targetAccount := client.Account(), targetClient.Account()
|
||||||
|
if myAccount != "" && targetAccount != "" && myAccount == targetAccount {
|
||||||
|
hist = targetClient.history
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if hist == nil {
|
||||||
|
rb.Add(nil, server.name, ERR_NOSUCHCHANNEL, client.Nick(), target, client.t("No such channel"))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
limit := 10
|
||||||
|
maxChathistoryLimit := config.History.ChathistoryMax
|
||||||
|
if len(msg.Params) > 1 {
|
||||||
|
providedLimit, err := strconv.Atoi(msg.Params[1])
|
||||||
|
if providedLimit > maxChathistoryLimit {
|
||||||
|
providedLimit = maxChathistoryLimit
|
||||||
|
}
|
||||||
|
if err == nil && providedLimit != 0 {
|
||||||
|
limit = providedLimit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
items := hist.Latest(limit)
|
||||||
|
|
||||||
|
if channel != nil {
|
||||||
|
channel.replayHistoryItems(rb, items)
|
||||||
|
} else {
|
||||||
|
client.replayPrivmsgHistory(rb, items, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// INFO
|
// INFO
|
||||||
func infoHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
|
func infoHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
|
||||||
// we do the below so that the human-readable lines in info can be translated.
|
// we do the below so that the human-readable lines in info can be translated.
|
||||||
|
15
irc/help.go
15
irc/help.go
@ -130,6 +130,13 @@ http://ircv3.net/specs/core/capability-negotiation-3.2.html`,
|
|||||||
text: `CHANSERV <subcommand> [params]
|
text: `CHANSERV <subcommand> [params]
|
||||||
|
|
||||||
ChanServ controls channel registrations.`,
|
ChanServ controls channel registrations.`,
|
||||||
|
},
|
||||||
|
"chathistory": {
|
||||||
|
text: `CHATHISTORY [params]
|
||||||
|
|
||||||
|
CHATHISTORY is an experimental history replay command. See these documents:
|
||||||
|
https://github.com/MuffinMedic/ircv3-specifications/blob/chathistory/extensions/chathistory.md
|
||||||
|
https://gist.github.com/DanielOaks/c104ad6e8759c01eb5c826d627caf80da`,
|
||||||
},
|
},
|
||||||
"cs": {
|
"cs": {
|
||||||
text: `CS <subcommand> [params]
|
text: `CS <subcommand> [params]
|
||||||
@ -187,6 +194,14 @@ Get an explanation of <argument>, or "index" for a list of help topics.`,
|
|||||||
text: `HELPOP <argument>
|
text: `HELPOP <argument>
|
||||||
|
|
||||||
Get an explanation of <argument>, or "index" for a list of help topics.`,
|
Get an explanation of <argument>, or "index" for a list of help topics.`,
|
||||||
|
},
|
||||||
|
"history": {
|
||||||
|
text: `HISTORY <target> [limit]
|
||||||
|
|
||||||
|
Replay message history. <target> can be a channel name, "me" to replay direct
|
||||||
|
message history, or a nickname to replay another client's direct message
|
||||||
|
history (they must be logged into the same account as you). At most [limit]
|
||||||
|
messages will be replayed.`,
|
||||||
},
|
},
|
||||||
"hostserv": {
|
"hostserv": {
|
||||||
text: `HOSTSERV <command> [params]
|
text: `HOSTSERV <command> [params]
|
||||||
|
@ -36,6 +36,13 @@ type Item struct {
|
|||||||
Msgid string
|
Msgid string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HasMsgid tests whether a message has the message id `msgid`.
|
||||||
|
func (item *Item) HasMsgid(msgid string) bool {
|
||||||
|
// XXX we stuff other data in the Msgid field sometimes,
|
||||||
|
// don't match it by accident
|
||||||
|
return (item.Type == Privmsg || item.Type == Notice) && item.Msgid == msgid
|
||||||
|
}
|
||||||
|
|
||||||
type Predicate func(item Item) (matches bool)
|
type Predicate func(item Item) (matches bool)
|
||||||
|
|
||||||
// Buffer is a ring buffer holding message/event history for a channel or user
|
// Buffer is a ring buffer holding message/event history for a channel or user
|
||||||
@ -115,7 +122,8 @@ func (list *Buffer) Add(item Item) {
|
|||||||
list.buffer[pos] = item
|
list.buffer[pos] = item
|
||||||
}
|
}
|
||||||
|
|
||||||
func reverse(results []Item) {
|
// Reverse reverses an []Item, in-place.
|
||||||
|
func Reverse(results []Item) {
|
||||||
for i, j := 0, len(results)-1; i < j; i, j = i+1, j-1 {
|
for i, j := 0, len(results)-1; i < j; i, j = i+1, j-1 {
|
||||||
results[i], results[j] = results[j], results[i]
|
results[i], results[j] = results[j], results[i]
|
||||||
}
|
}
|
||||||
@ -125,7 +133,7 @@ func reverse(results []Item) {
|
|||||||
// with an indication of whether the results are complete or are missing items
|
// with an indication of whether the results are complete or are missing items
|
||||||
// because some of that period was discarded. A zero value of `before` is considered
|
// because some of that period was discarded. A zero value of `before` is considered
|
||||||
// higher than all other times.
|
// higher than all other times.
|
||||||
func (list *Buffer) Between(after, before time.Time) (results []Item, complete bool) {
|
func (list *Buffer) Between(after, before time.Time, ascending bool, limit int) (results []Item, complete bool) {
|
||||||
if !list.Enabled() {
|
if !list.Enabled() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -139,14 +147,17 @@ func (list *Buffer) Between(after, before time.Time) (results []Item, complete b
|
|||||||
return (after.IsZero() || item.Time.After(after)) && (before.IsZero() || item.Time.Before(before))
|
return (after.IsZero() || item.Time.After(after)) && (before.IsZero() || item.Time.Before(before))
|
||||||
}
|
}
|
||||||
|
|
||||||
return list.matchInternal(satisfies, 0), complete
|
return list.matchInternal(satisfies, ascending, limit), complete
|
||||||
}
|
}
|
||||||
|
|
||||||
// Match returns all history items such that `predicate` returns true for them.
|
// Match returns all history items such that `predicate` returns true for them.
|
||||||
// Items are considered in reverse insertion order, up to a total of `limit` matches.
|
// Items are considered in reverse insertion order if `ascending` is false, or
|
||||||
|
// in insertion order if `ascending` is true, up to a total of `limit` matches
|
||||||
|
// if `limit` > 0 (unlimited otherwise).
|
||||||
// `predicate` MAY be a closure that maintains its own state across invocations;
|
// `predicate` MAY be a closure that maintains its own state across invocations;
|
||||||
// it MUST NOT acquire any locks or otherwise do anything weird.
|
// it MUST NOT acquire any locks or otherwise do anything weird.
|
||||||
func (list *Buffer) Match(predicate Predicate, limit int) (results []Item) {
|
// Results are always returned in insertion order.
|
||||||
|
func (list *Buffer) Match(predicate Predicate, ascending bool, limit int) (results []Item) {
|
||||||
if !list.Enabled() {
|
if !list.Enabled() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -154,28 +165,42 @@ func (list *Buffer) Match(predicate Predicate, limit int) (results []Item) {
|
|||||||
list.RLock()
|
list.RLock()
|
||||||
defer list.RUnlock()
|
defer list.RUnlock()
|
||||||
|
|
||||||
return list.matchInternal(predicate, limit)
|
return list.matchInternal(predicate, ascending, limit)
|
||||||
}
|
}
|
||||||
|
|
||||||
// you must be holding the read lock to call this
|
// you must be holding the read lock to call this
|
||||||
func (list *Buffer) matchInternal(predicate Predicate, limit int) (results []Item) {
|
func (list *Buffer) matchInternal(predicate Predicate, ascending bool, limit int) (results []Item) {
|
||||||
if list.start == -1 {
|
if list.start == -1 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
pos := list.prev(list.end)
|
var pos, stop int
|
||||||
|
if ascending {
|
||||||
|
pos = list.start
|
||||||
|
stop = list.prev(list.end)
|
||||||
|
} else {
|
||||||
|
pos = list.prev(list.end)
|
||||||
|
stop = list.start
|
||||||
|
}
|
||||||
|
|
||||||
for {
|
for {
|
||||||
if predicate(list.buffer[pos]) {
|
if predicate(list.buffer[pos]) {
|
||||||
results = append(results, list.buffer[pos])
|
results = append(results, list.buffer[pos])
|
||||||
}
|
}
|
||||||
if pos == list.start || (limit != 0 && len(results) == limit) {
|
if pos == stop || (limit != 0 && len(results) == limit) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
pos = list.prev(pos)
|
if ascending {
|
||||||
|
pos = list.next(pos)
|
||||||
|
} else {
|
||||||
|
pos = list.prev(pos)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO sort by time instead?
|
// TODO sort by time instead?
|
||||||
reverse(results)
|
if !ascending {
|
||||||
|
Reverse(results)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -183,7 +208,7 @@ func (list *Buffer) matchInternal(predicate Predicate, limit int) (results []Ite
|
|||||||
// it returns all items.
|
// it returns all items.
|
||||||
func (list *Buffer) Latest(limit int) (results []Item) {
|
func (list *Buffer) Latest(limit int) (results []Item) {
|
||||||
matchAll := func(item Item) bool { return true }
|
matchAll := func(item Item) bool { return true }
|
||||||
return list.Match(matchAll, limit)
|
return list.Match(matchAll, false, limit)
|
||||||
}
|
}
|
||||||
|
|
||||||
// LastDiscarded returns the latest time of any entry that was evicted
|
// LastDiscarded returns the latest time of any entry that was evicted
|
||||||
@ -204,6 +229,15 @@ func (list *Buffer) prev(index int) int {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (list *Buffer) next(index int) int {
|
||||||
|
switch index {
|
||||||
|
case len(list.buffer) - 1:
|
||||||
|
return 0
|
||||||
|
default:
|
||||||
|
return index + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Resize shrinks or expands the buffer
|
// Resize shrinks or expands the buffer
|
||||||
func (list *Buffer) Resize(size int) {
|
func (list *Buffer) Resize(size int) {
|
||||||
newbuffer := make([]Item, size)
|
newbuffer := make([]Item, size)
|
||||||
|
@ -25,7 +25,7 @@ func TestEmptyBuffer(t *testing.T) {
|
|||||||
Nick: "testnick",
|
Nick: "testnick",
|
||||||
})
|
})
|
||||||
|
|
||||||
since, complete := buf.Between(pastTime, time.Now())
|
since, complete := buf.Between(pastTime, time.Now(), false, 0)
|
||||||
if len(since) != 0 {
|
if len(since) != 0 {
|
||||||
t.Error("shouldn't be able to add to disabled buf")
|
t.Error("shouldn't be able to add to disabled buf")
|
||||||
}
|
}
|
||||||
@ -37,13 +37,13 @@ func TestEmptyBuffer(t *testing.T) {
|
|||||||
if !buf.Enabled() {
|
if !buf.Enabled() {
|
||||||
t.Error("the buffer of size 1 must be considered enabled")
|
t.Error("the buffer of size 1 must be considered enabled")
|
||||||
}
|
}
|
||||||
since, complete = buf.Between(pastTime, time.Now())
|
since, complete = buf.Between(pastTime, time.Now(), false, 0)
|
||||||
assertEqual(complete, true, t)
|
assertEqual(complete, true, t)
|
||||||
assertEqual(len(since), 0, t)
|
assertEqual(len(since), 0, t)
|
||||||
buf.Add(Item{
|
buf.Add(Item{
|
||||||
Nick: "testnick",
|
Nick: "testnick",
|
||||||
})
|
})
|
||||||
since, complete = buf.Between(pastTime, time.Now())
|
since, complete = buf.Between(pastTime, time.Now(), false, 0)
|
||||||
if len(since) != 1 {
|
if len(since) != 1 {
|
||||||
t.Error("should be able to store items in a nonempty buffer")
|
t.Error("should be able to store items in a nonempty buffer")
|
||||||
}
|
}
|
||||||
@ -57,7 +57,7 @@ func TestEmptyBuffer(t *testing.T) {
|
|||||||
buf.Add(Item{
|
buf.Add(Item{
|
||||||
Nick: "testnick2",
|
Nick: "testnick2",
|
||||||
})
|
})
|
||||||
since, complete = buf.Between(pastTime, time.Now())
|
since, complete = buf.Between(pastTime, time.Now(), false, 0)
|
||||||
if len(since) != 1 {
|
if len(since) != 1 {
|
||||||
t.Error("expect exactly 1 item")
|
t.Error("expect exactly 1 item")
|
||||||
}
|
}
|
||||||
@ -68,7 +68,7 @@ func TestEmptyBuffer(t *testing.T) {
|
|||||||
t.Error("retrieved junk data")
|
t.Error("retrieved junk data")
|
||||||
}
|
}
|
||||||
matchAll := func(item Item) bool { return true }
|
matchAll := func(item Item) bool { return true }
|
||||||
assertEqual(toNicks(buf.Match(matchAll, 0)), []string{"testnick2"}, t)
|
assertEqual(toNicks(buf.Match(matchAll, false, 0)), []string{"testnick2"}, t)
|
||||||
}
|
}
|
||||||
|
|
||||||
func toNicks(items []Item) (result []string) {
|
func toNicks(items []Item) (result []string) {
|
||||||
@ -112,7 +112,7 @@ func TestBuffer(t *testing.T) {
|
|||||||
Time: easyParse("2006-01-03 15:04:05Z"),
|
Time: easyParse("2006-01-03 15:04:05Z"),
|
||||||
})
|
})
|
||||||
|
|
||||||
since, complete := buf.Between(start, time.Now())
|
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)
|
||||||
|
|
||||||
@ -121,20 +121,20 @@ func TestBuffer(t *testing.T) {
|
|||||||
Nick: "testnick3",
|
Nick: "testnick3",
|
||||||
Time: easyParse("2006-01-04 15:04:05Z"),
|
Time: easyParse("2006-01-04 15:04:05Z"),
|
||||||
})
|
})
|
||||||
since, complete = buf.Between(start, time.Now())
|
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)
|
||||||
// now exclude the time of the discarded entry; results should be complete again
|
// now exclude the time of the discarded entry; results should be complete again
|
||||||
since, complete = buf.Between(easyParse("2006-01-02 00:00:00Z"), time.Now())
|
since, complete = buf.Between(easyParse("2006-01-02 00:00:00Z"), time.Now(), false, 0)
|
||||||
assertEqual(complete, true, t)
|
assertEqual(complete, true, t)
|
||||||
assertEqual(toNicks(since), []string{"testnick1", "testnick2", "testnick3"}, t)
|
assertEqual(toNicks(since), []string{"testnick1", "testnick2", "testnick3"}, t)
|
||||||
since, complete = buf.Between(easyParse("2006-01-02 00:00:00Z"), easyParse("2006-01-03 00:00:00Z"))
|
since, complete = buf.Between(easyParse("2006-01-02 00:00:00Z"), easyParse("2006-01-03 00:00:00Z"), false, 0)
|
||||||
assertEqual(complete, true, t)
|
assertEqual(complete, true, t)
|
||||||
assertEqual(toNicks(since), []string{"testnick1"}, t)
|
assertEqual(toNicks(since), []string{"testnick1"}, t)
|
||||||
|
|
||||||
// shrink the buffer, cutting off testnick1
|
// shrink the buffer, cutting off testnick1
|
||||||
buf.Resize(2)
|
buf.Resize(2)
|
||||||
since, complete = buf.Between(easyParse("2006-01-02 00:00:00Z"), time.Now())
|
since, complete = buf.Between(easyParse("2006-01-02 00:00:00Z"), time.Now(), false, 0)
|
||||||
assertEqual(complete, false, t)
|
assertEqual(complete, false, t)
|
||||||
assertEqual(toNicks(since), []string{"testnick2", "testnick3"}, t)
|
assertEqual(toNicks(since), []string{"testnick2", "testnick3"}, t)
|
||||||
|
|
||||||
@ -151,7 +151,11 @@ func TestBuffer(t *testing.T) {
|
|||||||
Nick: "testnick6",
|
Nick: "testnick6",
|
||||||
Time: easyParse("2006-01-07 15:04:05Z"),
|
Time: easyParse("2006-01-07 15:04:05Z"),
|
||||||
})
|
})
|
||||||
since, complete = buf.Between(easyParse("2006-01-03 00:00:00Z"), time.Now())
|
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)
|
||||||
|
|
||||||
|
// test ascending order
|
||||||
|
since, _ = buf.Between(easyParse("2006-01-03 00:00:00Z"), time.Now(), true, 2)
|
||||||
|
assertEqual(toNicks(since), []string{"testnick2", "testnick3"}, t)
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,7 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
// https://ircv3.net/specs/extensions/labeled-response.html
|
// https://ircv3.net/specs/extensions/labeled-response.html
|
||||||
batchType = "draft/labeled-response"
|
defaultBatchType = "draft/labeled-response"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ResponseBuffer - put simply - buffers messages and then outputs them to a given client.
|
// ResponseBuffer - put simply - buffers messages and then outputs them to a given client.
|
||||||
@ -45,8 +45,10 @@ func NewResponseBuffer(target *Client) *ResponseBuffer {
|
|||||||
// Add adds a standard new message to our queue.
|
// Add adds a standard new message to our queue.
|
||||||
func (rb *ResponseBuffer) Add(tags *map[string]ircmsg.TagValue, prefix string, command string, params ...string) {
|
func (rb *ResponseBuffer) Add(tags *map[string]ircmsg.TagValue, prefix string, command string, params ...string) {
|
||||||
if rb.finalized {
|
if rb.finalized {
|
||||||
rb.target.server.logger.Error("message added to finalized ResponseBuffer, undefined behavior")
|
rb.target.server.logger.Error("internal", "message added to finalized ResponseBuffer, undefined behavior")
|
||||||
debug.PrintStack()
|
debug.PrintStack()
|
||||||
|
// TODO(dan): send a NOTICE to the end user with a string representation of the message,
|
||||||
|
// for debugging purposes
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -81,7 +83,15 @@ func (rb *ResponseBuffer) AddSplitMessageFromClient(msgid string, fromNickMask s
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rb *ResponseBuffer) sendBatchStart(blocking bool) {
|
// InitializeBatch forcibly starts a batch of batch `batchType`.
|
||||||
|
// Normally, Send/Flush will decide automatically whether to start a batch
|
||||||
|
// of type draft/labeled-response. This allows changing the batch type
|
||||||
|
// and forcing the creation of a possibly empty batch.
|
||||||
|
func (rb *ResponseBuffer) InitializeBatch(batchType string, blocking bool) {
|
||||||
|
rb.sendBatchStart(batchType, blocking)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rb *ResponseBuffer) sendBatchStart(batchType string, blocking bool) {
|
||||||
if rb.batchID != "" {
|
if rb.batchID != "" {
|
||||||
// batch already initialized
|
// batch already initialized
|
||||||
return
|
return
|
||||||
@ -92,7 +102,9 @@ func (rb *ResponseBuffer) sendBatchStart(blocking bool) {
|
|||||||
rb.batchID = utils.GenerateSecretToken()
|
rb.batchID = utils.GenerateSecretToken()
|
||||||
|
|
||||||
message := ircmsg.MakeMessage(nil, rb.target.server.name, "BATCH", "+"+rb.batchID, batchType)
|
message := ircmsg.MakeMessage(nil, rb.target.server.name, "BATCH", "+"+rb.batchID, batchType)
|
||||||
message.Tags[caps.LabelTagName] = ircmsg.MakeTagValue(rb.Label)
|
if rb.Label != "" {
|
||||||
|
message.Tags[caps.LabelTagName] = ircmsg.MakeTagValue(rb.Label)
|
||||||
|
}
|
||||||
rb.target.SendRawMessage(message, blocking)
|
rb.target.SendRawMessage(message, blocking)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -125,6 +137,10 @@ func (rb *ResponseBuffer) Flush(blocking bool) error {
|
|||||||
// It sends the `BATCH +` message if the client supports it and it hasn't been sent already.
|
// It sends the `BATCH +` message if the client supports it and it hasn't been sent already.
|
||||||
// If `final` is true, it also sends `BATCH -` (if necessary).
|
// If `final` is true, it also sends `BATCH -` (if necessary).
|
||||||
func (rb *ResponseBuffer) flushInternal(final bool, blocking bool) error {
|
func (rb *ResponseBuffer) flushInternal(final bool, blocking bool) error {
|
||||||
|
if rb.finalized {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
useLabel := rb.target.capabilities.Has(caps.LabeledResponse) && rb.Label != ""
|
useLabel := rb.target.capabilities.Has(caps.LabeledResponse) && rb.Label != ""
|
||||||
// use a batch if we have a label, and we either currently have multiple messages,
|
// use a batch if we have a label, and we either currently have multiple messages,
|
||||||
// or we are doing a Flush() and we have to assume that there will be more messages
|
// or we are doing a Flush() and we have to assume that there will be more messages
|
||||||
@ -132,10 +148,10 @@ func (rb *ResponseBuffer) flushInternal(final bool, blocking bool) error {
|
|||||||
useBatch := useLabel && (len(rb.messages) > 1 || !final)
|
useBatch := useLabel && (len(rb.messages) > 1 || !final)
|
||||||
|
|
||||||
// if label but no batch, add label to first message
|
// if label but no batch, add label to first message
|
||||||
if useLabel && !useBatch && len(rb.messages) == 1 {
|
if useLabel && !useBatch && len(rb.messages) == 1 && rb.batchID == "" {
|
||||||
rb.messages[0].Tags[caps.LabelTagName] = ircmsg.MakeTagValue(rb.Label)
|
rb.messages[0].Tags[caps.LabelTagName] = ircmsg.MakeTagValue(rb.Label)
|
||||||
} else if useBatch {
|
} else if useBatch {
|
||||||
rb.sendBatchStart(blocking)
|
rb.sendBatchStart(defaultBatchType, blocking)
|
||||||
}
|
}
|
||||||
|
|
||||||
// send each message out
|
// send each message out
|
||||||
|
@ -157,6 +157,9 @@ func (server *Server) setISupport() (err error) {
|
|||||||
isupport.Add("AWAYLEN", strconv.Itoa(config.Limits.AwayLen))
|
isupport.Add("AWAYLEN", strconv.Itoa(config.Limits.AwayLen))
|
||||||
isupport.Add("CASEMAPPING", "ascii")
|
isupport.Add("CASEMAPPING", "ascii")
|
||||||
isupport.Add("CHANMODES", strings.Join([]string{modes.Modes{modes.BanMask, modes.ExceptMask, modes.InviteMask}.String(), "", modes.Modes{modes.UserLimit, modes.Key}.String(), modes.Modes{modes.InviteOnly, modes.Moderated, modes.NoOutside, modes.OpOnlyTopic, modes.ChanRoleplaying, modes.Secret}.String()}, ","))
|
isupport.Add("CHANMODES", strings.Join([]string{modes.Modes{modes.BanMask, modes.ExceptMask, modes.InviteMask}.String(), "", modes.Modes{modes.UserLimit, modes.Key}.String(), modes.Modes{modes.InviteOnly, modes.Moderated, modes.NoOutside, modes.OpOnlyTopic, modes.ChanRoleplaying, modes.Secret}.String()}, ","))
|
||||||
|
if config.History.Enabled && config.History.ChathistoryMax > 0 {
|
||||||
|
isupport.Add("draft/CHATHISTORY", strconv.Itoa(config.History.ChathistoryMax))
|
||||||
|
}
|
||||||
isupport.Add("CHANNELLEN", strconv.Itoa(config.Limits.ChannelLen))
|
isupport.Add("CHANNELLEN", strconv.Itoa(config.Limits.ChannelLen))
|
||||||
isupport.Add("CHANTYPES", "#")
|
isupport.Add("CHANTYPES", "#")
|
||||||
isupport.Add("ELIST", "U")
|
isupport.Add("ELIST", "U")
|
||||||
|
@ -482,6 +482,10 @@ fakelag:
|
|||||||
# message history tracking, for the RESUME extension and possibly other uses in future
|
# message history tracking, for the RESUME extension and possibly other uses in future
|
||||||
history:
|
history:
|
||||||
# should we store messages for later playback?
|
# should we store messages for later playback?
|
||||||
|
# the current implementation stores messages in RAM only; they do not persist
|
||||||
|
# across server restarts. however, you should disable this unless you understand
|
||||||
|
# how it interacts with the GDPR and/or any data privacy laws that apply
|
||||||
|
# in your country and the countries of your users.
|
||||||
enabled: true
|
enabled: true
|
||||||
|
|
||||||
# how many channel-specific events (messages, joins, parts) should be tracked per channel?
|
# how many channel-specific events (messages, joins, parts) should be tracked per channel?
|
||||||
@ -492,3 +496,7 @@ history:
|
|||||||
|
|
||||||
# number of messages to automatically play back on channel join (0 to disable):
|
# number of messages to automatically play back on channel join (0 to disable):
|
||||||
autoreplay-on-join: 0
|
autoreplay-on-join: 0
|
||||||
|
|
||||||
|
# maximum number of CHATHISTORY messages that can be
|
||||||
|
# requested at once (0 disables support for CHATHISTORY)
|
||||||
|
chathistory-maxmessages: 100
|
||||||
|
Loading…
Reference in New Issue
Block a user