Add a ChanServ setting to suppress storage of non-message events
(JOIN/PART/QUIT can be especially spammy)
This commit is contained in:
Shivaram Lingamneni
2026-07-16 06:51:56 +00:00
parent f0767c95da
commit 2bc106e6c2
6 changed files with 76 additions and 9 deletions
+2 -1
View File
@@ -7,7 +7,7 @@ We're pleased to be publishing the release candidate for v2.19.0 (the official r
This release includes changes to the config file format, one of which is not backwards-compatible (see below to determine whether you are affected). It includes no changes to the database file format.
Many thanks to [@andymandias](https://github.com/andymandias), [@emersion](https://github.com/emersion), [@englut](https://github.com/englut), [@Jokler](https://github.com/Jokler), [@jwheare](https://github.com/jwheare), [@KlaasT](https://github.com/KlaasT), [@SAY-5](https://github.com/SAY-5), [@skizzerz](https://github.com/skizzerz), [@ValwareIRC](https://github.com/ValwareIRC), and [@whitequark](https://github.com/whitequark) for helpful discussions, contributing patches, reporting issues, and helping test.
Many thanks to [@andymandias](https://github.com/andymandias), [@emersion](https://github.com/emersion), [@englut](https://github.com/englut), [@Jokler](https://github.com/Jokler), [@jwheare](https://github.com/jwheare), [@KlaasT](https://github.com/KlaasT), [@luxaritas](https://github.com/luxaritas), [@SAY-5](https://github.com/SAY-5), [@skizzerz](https://github.com/skizzerz), [@ValwareIRC](https://github.com/ValwareIRC), and [@whitequark](https://github.com/whitequark) for helpful discussions, contributing patches, reporting issues, and helping test.
### Compatibility breaks
* The `extjwt` configuration block now takes `algorithm` (`hmac`, `rsa`, or `eddsa`) and either `key` or `key-file` to configure the signing key (see `default.yaml` for examples). If you are using `extjwt`, the suggested upgrade path is to add the new keys before upgrading (duplicating the legacy keys `secret` and/or `rsa-private-key-file`), upgrade Ergo, then once the new version is confirmed stable, delete the legacy keys. (#2385)
@@ -29,6 +29,7 @@ Many thanks to [@andymandias](https://github.com/andymandias), [@emersion](https
* Added support for [draft/whoami](https://github.com/ircv3/ircv3-specifications/pull/603), a proposed IRCv3 extension allowing clients to track their message source ("NUH") (#2417)
* HTTP cookies are harvested from the initial websocket handshake; if a websocket client sends `SASL EXTERNAL`, they can be passed to an `auth-script` for validation (future versions of Ergo may implement some form of native HTTP cookie authentication) (#2185, thanks [@emersion](https://github.com/emersion)!)
* Added new API endpoints: `/v1/whois` (analogue of the `WHOIS` command to get information about an active nickname), `/v1/ns/saget` (retrieves user account settings), and `/v1/ns/saset` (modifies user account settings) (#2387, #2421, thanks [@KlaasT](https://github.com/KlaasT)!)
* Added a new ChanServ setting `store-events` to optionally suppress history storage of events like `JOIN` and `QUIT` (#2424, thanks [@luxaritas](https://github.com/luxaritas)!)
### Fixed
* Fixed a race condition where an always-on client's channel join might not be persisted (#2398)
+22 -5
View File
@@ -28,6 +28,7 @@ import (
type ChannelSettings struct {
History HistoryStatus
QueryCutoff HistoryCutoff
StoreEvents StoreEvents
}
// Channel represents a channel that clients can join.
@@ -101,7 +102,7 @@ func (channel *Channel) initializeLists() {
}
func (channel *Channel) resizeHistory(config *Config) {
status, _, _ := channel.historyStatus(config)
status, _, _, _ := channel.historyStatus(config)
if status == HistoryEphemeral {
channel.history.Resize(config.History.ChannelLength, time.Duration(config.History.AutoresizeWindow))
} else {
@@ -674,9 +675,9 @@ func (channel *Channel) IsEmpty() bool {
// figure out where history is being stored: persistent, ephemeral, or neither
// target is only needed if we're doing persistent history
func (channel *Channel) historyStatus(config *Config) (status HistoryStatus, target string, restrictions HistoryCutoff) {
func (channel *Channel) historyStatus(config *Config) (status HistoryStatus, storeEvents StoreEvents, target string, restrictions HistoryCutoff) {
if !config.History.Enabled {
return HistoryDisabled, "", HistoryCutoffNone
return HistoryDisabled, StoreEventsAll, "", HistoryCutoffNone
}
channel.stateMutex.RLock()
@@ -690,7 +691,7 @@ func (channel *Channel) historyStatus(config *Config) (status HistoryStatus, tar
restrictions = config.History.Restrictions.queryCutoff
}
return channelHistoryStatus(config, registered, settings.History), target, restrictions
return channelHistoryStatus(config, registered, settings.History), settings.StoreEvents, target, restrictions
}
func (channel *Channel) joinTimeCutoff(client *Client) (present bool, cutoff time.Time) {
@@ -726,12 +727,28 @@ func channelHistoryStatus(config *Config, registered bool, storedStatus HistoryS
}
}
func channelEventIsStorable(itemType history.ItemType, storeEvents StoreEvents) bool {
switch storeEvents {
case StoreEventsAll:
return true
case StoreEventsNoJoins:
return !(itemType == history.Join || itemType == history.Part || itemType == history.Quit)
case StoreEventsNone:
return itemType == history.Privmsg || itemType == history.Notice || itemType == history.Tagmsg
default:
return true
}
}
func (channel *Channel) AddHistoryItem(item history.Item, account string) (err error) {
if !itemIsStorable(&item, channel.server.Config()) {
return
}
status, target, _ := channel.historyStatus(channel.server.Config())
status, storeEvents, target, _ := channel.historyStatus(channel.server.Config())
if !channelEventIsStorable(item.Type, storeEvents) {
return
}
if status == HistoryPersistent {
err = channel.server.historyDB.AddChannelItem(target, item, account)
if err != nil {
+15
View File
@@ -183,6 +183,12 @@ by unprivileged users. Your options are:
channel; note that history will be effectively
unavailable to clients that are not always-on]
4. 'default' [use the server default]`,
`$bSTORE-EVENTS$b
'store-events' lets you control whether non-message events (like JOIN or
QUIT) are stored in your channel's history. Your options are:
1. 'all' [store all events, the default]
2. 'no-joins' [don't store JOIN/PART/QUIT]
3. 'none' [don't store any events, including TOPIC/KICK]`,
},
enabled: chanregEnabled,
minParams: 3,
@@ -813,6 +819,8 @@ func displayChannelSetting(service *ircService, settingName string, settings Cha
}
service.Notice(rb, fmt.Sprintf(client.t("The stored channel history query cutoff setting is: %s"), historyCutoffToString(settings.QueryCutoff)))
service.Notice(rb, fmt.Sprintf(client.t("Given current server settings, the channel history query cutoff setting is: %s"), historyCutoffToString(effectiveValue)))
case "store-events":
service.Notice(rb, fmt.Sprintf(client.t("The stored channel setting for store-events is: %s"), storeEventsToString(settings.StoreEvents)))
default:
service.Notice(rb, client.t("Invalid params"))
}
@@ -863,6 +871,13 @@ func csSetHandler(service *ircService, server *Server, client *Client, command s
break
}
channel.SetSettings(settings)
case "store-events":
settings.StoreEvents, err = storeEventsFromString(value)
if err != nil {
err = errInvalidParams
break
}
channel.SetSettings(settings)
}
switch err {
+34
View File
@@ -242,6 +242,40 @@ func historyStatusToString(status HistoryStatus) string {
}
}
type StoreEvents uint
const (
StoreEventsAll StoreEvents = iota // default, store all events
StoreEventsNoJoins // don't store JOIN/QUIT/PART, do store TOPIC and NICK
StoreEventsNone // don't store any events, only HISTORY/PRIVMSG/TAGMSG
)
func storeEventsFromString(str string) (status StoreEvents, err error) {
switch strings.ToLower(str) {
case "all", "default", "on", "true", "yes":
return StoreEventsAll, nil
case "no-joins":
return StoreEventsNoJoins, nil
case "none":
return StoreEventsNone, nil
default:
return StoreEventsAll, errInvalidParams
}
}
func storeEventsToString(storeEvents StoreEvents) string {
switch storeEvents {
case StoreEventsAll:
return "all"
case StoreEventsNoJoins:
return "no-joins"
case StoreEventsNone:
return "none"
default:
return "unknown"
}
}
// XXX you must have already checked History.Enabled before calling this
func historyEnabled(serverSetting PersistentStatus, localSetting HistoryStatus) (result HistoryStatus) {
switch serverSetting {
+1 -1
View File
@@ -3721,7 +3721,7 @@ func renameHandler(server *Server, client *Client, msg ircmsg.Message, rb *Respo
}
config := server.Config()
status, _, _ := channel.historyStatus(config)
status, _, _, _ := channel.historyStatus(config)
if status == HistoryPersistent {
rb.Add(nil, server.name, "FAIL", "RENAME", "CANNOT_RENAME", oldName, utils.SafeErrorParam(newName), client.t("Channels with persistent history cannot be renamed"))
return false
+2 -2
View File
@@ -1177,7 +1177,7 @@ func (server *Server) GetHistorySequence(providedChannel *Channel, client *Clien
err = errInsufficientPrivs
return
}
status, target, restriction = channel.historyStatus(config)
status, _, target, restriction = channel.historyStatus(config)
switch status {
case HistoryEphemeral:
hist = &channel.history
@@ -1279,7 +1279,7 @@ func (server *Server) DeleteMessage(target, msgid, accountName string) (err erro
if target[0] == '#' {
channel := server.channels.Get(target)
if channel != nil {
if status, _, _ := channel.historyStatus(config); status == HistoryEphemeral {
if status, _, _, _ := channel.historyStatus(config); status == HistoryEphemeral {
hist = &channel.history
}
}