mirror of
https://github.com/ergochat/ergo.git
synced 2024-12-22 10:42:52 +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) {
|
||||
items, complete := channel.history.Between(after, before)
|
||||
items, complete := channel.history.Between(after, before, false, 0)
|
||||
rb := NewResponseBuffer(newClient)
|
||||
channel.replayHistoryItems(rb, items)
|
||||
if !complete && !newClient.resumeDetails.HistoryIncomplete {
|
||||
|
@ -479,7 +479,7 @@ func (client *Client) TryResume() {
|
||||
privmsgMatcher := func(item history.Item) bool {
|
||||
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()
|
||||
if lastDiscarded.Before(oldestLostMessage) {
|
||||
oldestLostMessage = lastDiscarded
|
||||
@ -541,28 +541,39 @@ func (client *Client) tryResumeChannels() {
|
||||
// replay direct PRIVSMG history
|
||||
if !details.Timestamp.IsZero() {
|
||||
now := time.Now()
|
||||
nick := client.Nick()
|
||||
items, complete := client.history.Between(details.Timestamp, now)
|
||||
for _, item := range items {
|
||||
var command string
|
||||
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"))
|
||||
}
|
||||
items, complete := client.history.Between(details.Timestamp, now, false, 0)
|
||||
rb := NewResponseBuffer(client)
|
||||
client.replayPrivmsgHistory(rb, items, complete)
|
||||
rb.Send(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
|
||||
func (client *Client) copyResumeData(oldClient *Client) {
|
||||
oldClient.stateMutex.RLock()
|
||||
|
@ -92,6 +92,10 @@ func init() {
|
||||
usablePreReg: true,
|
||||
minParams: 1,
|
||||
},
|
||||
"CHATHISTORY": {
|
||||
handler: chathistoryHandler,
|
||||
minParams: 3,
|
||||
},
|
||||
"DEBUG": {
|
||||
handler: debugHandler,
|
||||
minParams: 1,
|
||||
@ -110,6 +114,10 @@ func init() {
|
||||
handler: helpHandler,
|
||||
minParams: 0,
|
||||
},
|
||||
"HISTORY": {
|
||||
handler: historyHandler,
|
||||
minParams: 1,
|
||||
},
|
||||
"INFO": {
|
||||
handler: infoHandler,
|
||||
},
|
||||
|
@ -322,6 +322,7 @@ type Config struct {
|
||||
ChannelLength int `yaml:"channel-length"`
|
||||
ClientLength int `yaml:"client-length"`
|
||||
AutoreplayOnJoin int `yaml:"autoreplay-on-join"`
|
||||
ChathistoryMax int `yaml:"chathistory-maxmessages"`
|
||||
}
|
||||
|
||||
Filename string
|
||||
|
@ -41,6 +41,7 @@ var (
|
||||
errResumeTokenAlreadySet = errors.New("Client was already assigned a resume token")
|
||||
errInvalidUsername = errors.New("Invalid username")
|
||||
errFeatureDisabled = errors.New("That feature is disabled")
|
||||
errInvalidParams = errors.New("Invalid parameters")
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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>
|
||||
func debugHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
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.
|
||||
|
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]
|
||||
|
||||
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": {
|
||||
text: `CS <subcommand> [params]
|
||||
@ -187,6 +194,14 @@ Get an explanation of <argument>, or "index" for a list of help topics.`,
|
||||
text: `HELPOP <argument>
|
||||
|
||||
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": {
|
||||
text: `HOSTSERV <command> [params]
|
||||
|
@ -36,6 +36,13 @@ type Item struct {
|
||||
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)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
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 {
|
||||
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
|
||||
// because some of that period was discarded. A zero value of `before` is considered
|
||||
// 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() {
|
||||
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 list.matchInternal(satisfies, 0), complete
|
||||
return list.matchInternal(satisfies, ascending, limit), complete
|
||||
}
|
||||
|
||||
// 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;
|
||||
// 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() {
|
||||
return
|
||||
}
|
||||
@ -154,28 +165,42 @@ func (list *Buffer) Match(predicate Predicate, limit int) (results []Item) {
|
||||
list.RLock()
|
||||
defer list.RUnlock()
|
||||
|
||||
return list.matchInternal(predicate, limit)
|
||||
return list.matchInternal(predicate, ascending, limit)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
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 {
|
||||
if predicate(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
|
||||
}
|
||||
pos = list.prev(pos)
|
||||
if ascending {
|
||||
pos = list.next(pos)
|
||||
} else {
|
||||
pos = list.prev(pos)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO sort by time instead?
|
||||
reverse(results)
|
||||
if !ascending {
|
||||
Reverse(results)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@ -183,7 +208,7 @@ func (list *Buffer) matchInternal(predicate Predicate, limit int) (results []Ite
|
||||
// it returns all items.
|
||||
func (list *Buffer) Latest(limit int) (results []Item) {
|
||||
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
|
||||
@ -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
|
||||
func (list *Buffer) Resize(size int) {
|
||||
newbuffer := make([]Item, size)
|
||||
|
@ -25,7 +25,7 @@ func TestEmptyBuffer(t *testing.T) {
|
||||
Nick: "testnick",
|
||||
})
|
||||
|
||||
since, complete := buf.Between(pastTime, time.Now())
|
||||
since, complete := buf.Between(pastTime, time.Now(), false, 0)
|
||||
if len(since) != 0 {
|
||||
t.Error("shouldn't be able to add to disabled buf")
|
||||
}
|
||||
@ -37,13 +37,13 @@ func TestEmptyBuffer(t *testing.T) {
|
||||
if !buf.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(len(since), 0, t)
|
||||
buf.Add(Item{
|
||||
Nick: "testnick",
|
||||
})
|
||||
since, complete = buf.Between(pastTime, time.Now())
|
||||
since, complete = buf.Between(pastTime, time.Now(), false, 0)
|
||||
if len(since) != 1 {
|
||||
t.Error("should be able to store items in a nonempty buffer")
|
||||
}
|
||||
@ -57,7 +57,7 @@ func TestEmptyBuffer(t *testing.T) {
|
||||
buf.Add(Item{
|
||||
Nick: "testnick2",
|
||||
})
|
||||
since, complete = buf.Between(pastTime, time.Now())
|
||||
since, complete = buf.Between(pastTime, time.Now(), false, 0)
|
||||
if len(since) != 1 {
|
||||
t.Error("expect exactly 1 item")
|
||||
}
|
||||
@ -68,7 +68,7 @@ func TestEmptyBuffer(t *testing.T) {
|
||||
t.Error("retrieved junk data")
|
||||
}
|
||||
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) {
|
||||
@ -112,7 +112,7 @@ func TestBuffer(t *testing.T) {
|
||||
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(toNicks(since), []string{"testnick0", "testnick1", "testnick2"}, t)
|
||||
|
||||
@ -121,20 +121,20 @@ func TestBuffer(t *testing.T) {
|
||||
Nick: "testnick3",
|
||||
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(toNicks(since), []string{"testnick1", "testnick2", "testnick3"}, t)
|
||||
// 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(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(toNicks(since), []string{"testnick1"}, t)
|
||||
|
||||
// shrink the buffer, cutting off testnick1
|
||||
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(toNicks(since), []string{"testnick2", "testnick3"}, t)
|
||||
|
||||
@ -151,7 +151,11 @@ func TestBuffer(t *testing.T) {
|
||||
Nick: "testnick6",
|
||||
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(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 (
|
||||
// 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.
|
||||
@ -45,8 +45,10 @@ func NewResponseBuffer(target *Client) *ResponseBuffer {
|
||||
// Add adds a standard new message to our queue.
|
||||
func (rb *ResponseBuffer) Add(tags *map[string]ircmsg.TagValue, prefix string, command string, params ...string) {
|
||||
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()
|
||||
// TODO(dan): send a NOTICE to the end user with a string representation of the message,
|
||||
// for debugging purposes
|
||||
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 != "" {
|
||||
// batch already initialized
|
||||
return
|
||||
@ -92,7 +102,9 @@ func (rb *ResponseBuffer) sendBatchStart(blocking bool) {
|
||||
rb.batchID = utils.GenerateSecretToken()
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@ -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.
|
||||
// If `final` is true, it also sends `BATCH -` (if necessary).
|
||||
func (rb *ResponseBuffer) flushInternal(final bool, blocking bool) error {
|
||||
if rb.finalized {
|
||||
return nil
|
||||
}
|
||||
|
||||
useLabel := rb.target.capabilities.Has(caps.LabeledResponse) && rb.Label != ""
|
||||
// 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
|
||||
@ -132,10 +148,10 @@ func (rb *ResponseBuffer) flushInternal(final bool, blocking bool) error {
|
||||
useBatch := useLabel && (len(rb.messages) > 1 || !final)
|
||||
|
||||
// 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)
|
||||
} else if useBatch {
|
||||
rb.sendBatchStart(blocking)
|
||||
rb.sendBatchStart(defaultBatchType, blocking)
|
||||
}
|
||||
|
||||
// send each message out
|
||||
|
@ -157,6 +157,9 @@ func (server *Server) setISupport() (err error) {
|
||||
isupport.Add("AWAYLEN", strconv.Itoa(config.Limits.AwayLen))
|
||||
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()}, ","))
|
||||
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("CHANTYPES", "#")
|
||||
isupport.Add("ELIST", "U")
|
||||
|
@ -482,6 +482,10 @@ fakelag:
|
||||
# message history tracking, for the RESUME extension and possibly other uses in future
|
||||
history:
|
||||
# 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
|
||||
|
||||
# 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):
|
||||
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