mirror of
				https://github.com/ergochat/ergo.git
				synced 2025-10-31 05:47:22 +01:00 
			
		
		
		
	add CHATHISTORY and HISTORY implementations
This commit is contained in:
		
							parent
							
								
									ea07d99074
								
							
						
					
					
						commit
						f6b3008f8f
					
				| @ -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 { | ||||
|  | ||||
| @ -475,7 +475,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 | ||||
| @ -537,28 +537,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() | ||||
|  | ||||
| @ -90,6 +90,10 @@ func init() { | ||||
| 			usablePreReg: true, | ||||
| 			minParams:    1, | ||||
| 		}, | ||||
| 		"CHATHISTORY": { | ||||
| 			handler:   chathistoryHandler, | ||||
| 			minParams: 3, | ||||
| 		}, | ||||
| 		"DEBUG": { | ||||
| 			handler:   debugHandler, | ||||
| 			minParams: 1, | ||||
| @ -108,6 +112,10 @@ func init() { | ||||
| 			handler:   helpHandler, | ||||
| 			minParams: 0, | ||||
| 		}, | ||||
| 		"HISTORY": { | ||||
| 			handler:   historyHandler, | ||||
| 			minParams: 1, | ||||
| 		}, | ||||
| 		"INFO": { | ||||
| 			handler: infoHandler, | ||||
| 		}, | ||||
|  | ||||
| @ -213,6 +213,7 @@ type Limits struct { | ||||
| 	NickLen        int           `yaml:"nicklen"` | ||||
| 	TopicLen       int           `yaml:"topiclen"` | ||||
| 	WhowasEntries  int           `yaml:"whowas-entries"` | ||||
| 	ChathistoryMax int           `yaml:"chathistory-maxmessages"` | ||||
| } | ||||
| 
 | ||||
| // STSConfig controls the STS configuration/ | ||||
|  | ||||
| @ -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 | ||||
|  | ||||
							
								
								
									
										263
									
								
								irc/handlers.go
									
									
									
									
									
								
							
							
						
						
									
										263
									
								
								irc/handlers.go
									
									
									
									
									
								
							| @ -526,6 +526,227 @@ 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) { | ||||
| 	// 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 { | ||||
| 		cftarget, _ := Casefold(target) | ||||
| 		if cftarget == "me" || cftarget == "self" || cftarget == client.NickCasefolded() { | ||||
| 			hist = client.history | ||||
| 		} else { | ||||
| 			return | ||||
| 		} | ||||
| 	} else { | ||||
| 		hist = &channel.history | ||||
| 	} | ||||
| 
 | ||||
| 	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 := server.Config().Limits.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]) | ||||
| @ -783,6 +1004,48 @@ 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 { | ||||
| 	target := msg.Params[0] | ||||
| 	var hist *history.Buffer | ||||
| 	channel := server.channels.Get(target) | ||||
| 	if channel == nil { | ||||
| 		cftarget, _ := Casefold(target) | ||||
| 		if cftarget == "me" || cftarget == "self" || cftarget == client.NickCasefolded() { | ||||
| 			hist = client.history | ||||
| 		} else { | ||||
| 			rb.Add(nil, server.name, ERR_NOSUCHCHANNEL, client.Nick(), target, client.t("No such channel")) | ||||
| 			return false | ||||
| 		} | ||||
| 	} else { | ||||
| 		hist = &channel.history | ||||
| 	} | ||||
| 
 | ||||
| 	limit := 10 | ||||
| 	maxChathistoryLimit := server.Config().Limits.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. | ||||
|  | ||||
							
								
								
									
										13
									
								
								irc/help.go
									
									
									
									
									
								
							
							
						
						
									
										13
									
								
								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,12 @@ 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: `HISTSERV <target> [limit] | ||||
| 
 | ||||
| Replay message history. <target> can be a channel name, or "self" or "me" | ||||
| to replay direct message history. 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,7 +45,7 @@ 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() | ||||
| 		return | ||||
| 	} | ||||
| @ -81,7 +81,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 +100,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 +135,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 +146,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.Limits.ChathistoryMax > 0 { | ||||
| 		isupport.Add("CHATHISTORY", strconv.Itoa(config.Limits.ChathistoryMax)) | ||||
| 	} | ||||
| 	isupport.Add("CHANNELLEN", strconv.Itoa(config.Limits.ChannelLen)) | ||||
| 	isupport.Add("CHANTYPES", "#") | ||||
| 	isupport.Add("ELIST", "U") | ||||
|  | ||||
| @ -439,6 +439,10 @@ limits: | ||||
|     # maximum length of channel lists (beI modes) | ||||
|     chan-list-modes: 60 | ||||
| 
 | ||||
|     # maximum number of CHATHISTORY messages that can be | ||||
|     # requested at once (0 disables support for CHATHISTORY) | ||||
|     chathistory-maxmessages: 100 | ||||
| 
 | ||||
|     # maximum length of IRC lines | ||||
|     # this should generally be 1024-2048, and will only apply when negotiated by clients | ||||
|     linelen: | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Shivaram Lingamneni
						Shivaram Lingamneni