From 2bc106e6c2aa7903db97188f797ce6466731b623 Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Thu, 16 Jul 2026 06:51:56 +0000 Subject: [PATCH] fix #2424 Add a ChanServ setting to suppress storage of non-message events (JOIN/PART/QUIT can be especially spammy) --- CHANGELOG.md | 3 ++- irc/channel.go | 27 ++++++++++++++++++++++----- irc/chanserv.go | 15 +++++++++++++++ irc/config.go | 34 ++++++++++++++++++++++++++++++++++ irc/handlers.go | 2 +- irc/server.go | 4 ++-- 6 files changed, 76 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 168959dc..d34bdcbb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/irc/channel.go b/irc/channel.go index 6a8db288..e5e0215c 100644 --- a/irc/channel.go +++ b/irc/channel.go @@ -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 { diff --git a/irc/chanserv.go b/irc/chanserv.go index 7c9e3105..786abc43 100644 --- a/irc/chanserv.go +++ b/irc/chanserv.go @@ -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 { diff --git a/irc/config.go b/irc/config.go index 305a4238..ab53eb12 100644 --- a/irc/config.go +++ b/irc/config.go @@ -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 { diff --git a/irc/handlers.go b/irc/handlers.go index 98731770..47d92cc0 100644 --- a/irc/handlers.go +++ b/irc/handlers.go @@ -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 diff --git a/irc/server.go b/irc/server.go index 19152017..e526ec38 100644 --- a/irc/server.go +++ b/irc/server.go @@ -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 } }