diff --git a/irc/accounts.go b/irc/accounts.go index 061caeec..ccf3a426 100644 --- a/irc/accounts.go +++ b/irc/accounts.go @@ -33,6 +33,7 @@ const ( keyAccountEnforcement = "account.customenforcement %s" keyAccountVHost = "account.vhost %s" keyCertToAccount = "account.creds.certfp %s" + keyAccountChannels = "account.channels %s" keyVHostQueueAcctToId = "vhostQueue %s" vhostRequestIdx = "vhostQueue" @@ -836,9 +837,15 @@ func (am *AccountManager) Unregister(account string) error { nicksKey := fmt.Sprintf(keyAccountAdditionalNicks, casefoldedAccount) vhostKey := fmt.Sprintf(keyAccountVHost, casefoldedAccount) vhostQueueKey := fmt.Sprintf(keyVHostQueueAcctToId, casefoldedAccount) + channelsKey := fmt.Sprintf(keyAccountChannels, casefoldedAccount) var clients []*Client + var registeredChannels []string + defer func() { + am.server.channelRegistry.deleteByAccount(casefoldedAccount, registeredChannels) + }() + var credText string var rawNicks string @@ -846,6 +853,7 @@ func (am *AccountManager) Unregister(account string) error { defer am.serialCacheUpdateMutex.Unlock() var accountName string + var channelsStr string am.server.store.Update(func(tx *buntdb.Tx) error { tx.Delete(accountKey) accountName, _ = tx.Get(accountNameKey) @@ -859,6 +867,9 @@ func (am *AccountManager) Unregister(account string) error { credText, err = tx.Get(credentialsKey) tx.Delete(credentialsKey) tx.Delete(vhostKey) + channelsStr, _ = tx.Get(channelsKey) + tx.Delete(channelsKey) + _, err := tx.Delete(vhostQueueKey) am.decrementVHostQueueCount(casefoldedAccount, err) return nil @@ -879,6 +890,7 @@ func (am *AccountManager) Unregister(account string) error { skeleton, _ := Skeleton(accountName) additionalNicks := unmarshalReservedNicks(rawNicks) + registeredChannels = unmarshalRegisteredChannels(channelsStr) am.Lock() defer am.Unlock() @@ -899,9 +911,32 @@ func (am *AccountManager) Unregister(account string) error { if err != nil { return errAccountDoesNotExist } + return nil } +func unmarshalRegisteredChannels(channelsStr string) (result []string) { + if channelsStr != "" { + result = strings.Split(channelsStr, ",") + } + return +} + +func (am *AccountManager) ChannelsForAccount(account string) (channels []string) { + cfaccount, err := CasefoldName(account) + if err != nil { + return + } + + var channelStr string + key := fmt.Sprintf(keyAccountChannels, cfaccount) + am.server.store.View(func(tx *buntdb.Tx) error { + channelStr, _ = tx.Get(key) + return nil + }) + return unmarshalRegisteredChannels(channelStr) +} + func (am *AccountManager) AuthenticateByCertFP(client *Client) error { if client.certfp == "" { return errAccountInvalidCredentials diff --git a/irc/channelreg.go b/irc/channelreg.go index 723cfdaa..e9792335 100644 --- a/irc/channelreg.go +++ b/irc/channelreg.go @@ -215,6 +215,17 @@ func (reg *ChannelRegistry) Delete(casefoldedName string, info RegisteredChannel }) } +// deleteByAccount is a helper to delete all channel registrations corresponding to a user account. +func (reg *ChannelRegistry) deleteByAccount(cfaccount string, cfchannels []string) { + for _, cfchannel := range cfchannels { + info := reg.LoadChannel(cfchannel) + if info == nil || info.Founder != cfaccount { + continue + } + reg.Delete(cfchannel, *info) + } +} + // Rename handles the persistence part of a channel rename: the channel is // persisted under its new name, and the old name is cleaned up if necessary. func (reg *ChannelRegistry) Rename(channel *Channel, casefoldedOldName string) { @@ -254,14 +265,43 @@ func (reg *ChannelRegistry) deleteChannel(tx *buntdb.Tx, key string, info Regist for _, keyFmt := range channelKeyStrings { tx.Delete(fmt.Sprintf(keyFmt, key)) } + + // remove this channel from the client's list of registered channels + channelsKey := fmt.Sprintf(keyAccountChannels, info.Founder) + channelsStr, err := tx.Get(channelsKey) + if err == buntdb.ErrNotFound { + return + } + registeredChannels := unmarshalRegisteredChannels(channelsStr) + var nowRegisteredChannels []string + for _, channel := range registeredChannels { + if channel != key { + nowRegisteredChannels = append(nowRegisteredChannels, channel) + } + } + tx.Set(channelsKey, strings.Join(nowRegisteredChannels, ","), nil) } } } // saveChannel saves a channel to the store. func (reg *ChannelRegistry) saveChannel(tx *buntdb.Tx, channelKey string, channelInfo RegisteredChannel, includeFlags uint) { + // maintain the mapping of account -> registered channels + chanExistsKey := fmt.Sprintf(keyChannelExists, channelKey) + _, existsErr := tx.Get(chanExistsKey) + if existsErr == buntdb.ErrNotFound { + // this is a new registration, need to update account-to-channels + accountChannelsKey := fmt.Sprintf(keyAccountChannels, channelInfo.Founder) + alreadyChannels, _ := tx.Get(accountChannelsKey) + newChannels := channelKey // this is the casefolded channel name + if alreadyChannels != "" { + newChannels = fmt.Sprintf("%s,%s", alreadyChannels, newChannels) + } + tx.Set(accountChannelsKey, newChannels, nil) + } + if includeFlags&IncludeInitial != 0 { - tx.Set(fmt.Sprintf(keyChannelExists, channelKey), "1", nil) + tx.Set(chanExistsKey, "1", nil) tx.Set(fmt.Sprintf(keyChannelName, channelKey), channelInfo.Name, nil) tx.Set(fmt.Sprintf(keyChannelRegTime, channelKey), strconv.FormatInt(channelInfo.RegisteredAt.Unix(), 10), nil) tx.Set(fmt.Sprintf(keyChannelFounder, channelKey), channelInfo.Founder, nil) diff --git a/irc/chanserv.go b/irc/chanserv.go index 9b502161..6ba17931 100644 --- a/irc/chanserv.go +++ b/irc/chanserv.go @@ -224,6 +224,13 @@ func csRegisterHandler(server *Server, client *Client, command string, params [] return } + account := client.Account() + channelsAlreadyRegistered := server.accounts.ChannelsForAccount(account) + if server.Config().Channels.Registration.MaxChannelsPerAccount <= len(channelsAlreadyRegistered) { + csNotice(rb, client.t("You have already registered the maximum number of channels; try dropping some with /CS UNREGISTER")) + return + } + // this provides the synchronization that allows exactly one registration of the channel: err = channelInfo.SetRegistered(client.Account()) if err != nil { diff --git a/irc/config.go b/irc/config.go index 01a18976..81f70d1b 100644 --- a/irc/config.go +++ b/irc/config.go @@ -181,7 +181,8 @@ type NickReservationConfig struct { // ChannelRegistrationConfig controls channel registration. type ChannelRegistrationConfig struct { - Enabled bool + Enabled bool + MaxChannelsPerAccount int `yaml:"max-channels-per-account"` } // OperClassConfig defines a specific operator class. @@ -789,6 +790,10 @@ func LoadConfig(filename string) (config *Config, err error) { config.Accounts.Registration.BcryptCost = passwd.DefaultCost } + if config.Channels.Registration.MaxChannelsPerAccount == 0 { + config.Channels.Registration.MaxChannelsPerAccount = 10 + } + // in the current implementation, we disable history by creating a history buffer // with zero capacity. but the `enabled` config option MUST be respected regardless // of this detail diff --git a/irc/database.go b/irc/database.go index 27974668..c85cebc0 100644 --- a/irc/database.go +++ b/irc/database.go @@ -22,7 +22,7 @@ const ( // 'version' of the database schema keySchemaVersion = "db.version" // latest schema of the db - latestDbSchema = "4" + latestDbSchema = "5" ) type SchemaChanger func(*Config, *buntdb.Tx) error @@ -390,6 +390,25 @@ func schemaChangeV3ToV4(config *Config, tx *buntdb.Tx) error { return nil } +// create new key tracking channels that belong to an account +func schemaChangeV4ToV5(config *Config, tx *buntdb.Tx) error { + founderToChannels := make(map[string][]string) + prefix := "channel.founder " + tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool { + if !strings.HasPrefix(key, prefix) { + return false + } + channel := strings.TrimPrefix(key, prefix) + founderToChannels[value] = append(founderToChannels[value], channel) + return true + }) + + for founder, channels := range founderToChannels { + tx.Set(fmt.Sprintf("account.channels %s", founder), strings.Join(channels, ","), nil) + } + return nil +} + func init() { allChanges := []SchemaChange{ { @@ -407,6 +426,11 @@ func init() { TargetVersion: "4", Changer: schemaChangeV3ToV4, }, + { + InitialVersion: "4", + TargetVersion: "5", + Changer: schemaChangeV4ToV5, + }, } // build the index diff --git a/irc/nickserv.go b/irc/nickserv.go index 8ebe0b52..ddb3923b 100644 --- a/irc/nickserv.go +++ b/irc/nickserv.go @@ -318,6 +318,9 @@ func nsInfoHandler(server *Server, client *Client, command string, params []stri for _, nick := range account.AdditionalNicks { nsNotice(rb, fmt.Sprintf(client.t("Additional grouped nick: %s"), nick)) } + for _, channel := range server.accounts.ChannelsForAccount(accountName) { + nsNotice(rb, fmt.Sprintf(client.t("Registered channel: %s"), channel)) + } } func nsRegisterHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) { diff --git a/oragono.yaml b/oragono.yaml index a63e9475..1f49900b 100644 --- a/oragono.yaml +++ b/oragono.yaml @@ -280,6 +280,9 @@ channels: # can users register new channels? enabled: true + # how many channels can each account register? + max-channels-per-account: 10 + # operator classes oper-classes: # local operator