3
0
mirror of https://github.com/ergochat/ergo.git synced 2024-11-25 13:29:27 +01:00

add CHATHISTORY and HISTORY implementations

This commit is contained in:
Shivaram Lingamneni 2019-02-04 05:18:17 -05:00
parent ea07d99074
commit f6b3008f8f
12 changed files with 404 additions and 48 deletions

View File

@ -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 {

View File

@ -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()

View File

@ -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,
},

View File

@ -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/

View File

@ -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

View File

@ -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.

View File

@ -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]

View File

@ -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)

View File

@ -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)
}

View File

@ -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

View File

@ -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")

View File

@ -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: