diff --git a/irc/channel.go b/irc/channel.go index 686d7645..10af4d46 100644 --- a/irc/channel.go +++ b/irc/channel.go @@ -6,6 +6,7 @@ package irc import ( + "errors" "fmt" "strconv" "time" @@ -14,7 +15,10 @@ import ( "github.com/goshuirc/irc-go/ircmsg" "github.com/oragono/oragono/irc/caps" - "github.com/tidwall/buntdb" +) + +var ( + ChannelAlreadyRegistered = errors.New("Channel is already registered") ) // Channel represents a channel that clients can join. @@ -29,6 +33,8 @@ type Channel struct { nameCasefolded string server *Server createdTime time.Time + registeredFounder string + registeredTime time.Time stateMutex sync.RWMutex topic string topicSetBy string @@ -38,7 +44,7 @@ type Channel struct { // NewChannel creates a new channel from a `Server` and a `name` // string, which must be unique on the server. -func NewChannel(s *Server, name string, addDefaultModes bool) *Channel { +func NewChannel(s *Server, name string, addDefaultModes bool, regInfo *RegisteredChannel) *Channel { casefoldedName, err := CasefoldChannel(name) if err != nil { s.logger.Error("internal", fmt.Sprintf("Bad channel name %s: %v", name, err)) @@ -46,7 +52,8 @@ func NewChannel(s *Server, name string, addDefaultModes bool) *Channel { } channel := &Channel{ - flags: make(ModeSet), + createdTime: time.Now(), // may be overwritten by applyRegInfo + flags: make(ModeSet), lists: map[Mode]*UserMaskSet{ BanMask: NewUserMaskSet(), ExceptMask: NewUserMaskSet(), @@ -64,9 +71,80 @@ func NewChannel(s *Server, name string, addDefaultModes bool) *Channel { } } + if regInfo != nil { + channel.applyRegInfo(regInfo) + } + return channel } +// read in channel state that was persisted in the DB +func (channel *Channel) applyRegInfo(chanReg *RegisteredChannel) { + channel.registeredFounder = chanReg.Founder + channel.registeredTime = chanReg.RegisteredAt + channel.topic = chanReg.Topic + channel.topicSetBy = chanReg.TopicSetBy + channel.topicSetTime = chanReg.TopicSetTime + channel.name = chanReg.Name + channel.createdTime = chanReg.RegisteredAt + for _, mask := range chanReg.Banlist { + channel.lists[BanMask].Add(mask) + } + for _, mask := range chanReg.Exceptlist { + channel.lists[ExceptMask].Add(mask) + } + for _, mask := range chanReg.Invitelist { + channel.lists[InviteMask].Add(mask) + } +} + +// obtain a consistent snapshot of the channel state that can be persisted to the DB +func (channel *Channel) ExportRegistration(includeLists bool) (info RegisteredChannel) { + channel.stateMutex.RLock() + defer channel.stateMutex.RUnlock() + + info.Name = channel.name + info.Topic = channel.topic + info.TopicSetBy = channel.topicSetBy + info.TopicSetTime = channel.topicSetTime + info.Founder = channel.registeredFounder + info.RegisteredAt = channel.registeredTime + + if includeLists { + for mask := range channel.lists[BanMask].masks { + info.Banlist = append(info.Banlist, mask) + } + for mask := range channel.lists[ExceptMask].masks { + info.Exceptlist = append(info.Exceptlist, mask) + } + for mask := range channel.lists[InviteMask].masks { + info.Invitelist = append(info.Invitelist, mask) + } + } + + return +} + +// SetRegistered registers the channel, returning an error if it was already registered. +func (channel *Channel) SetRegistered(founder string) error { + channel.stateMutex.Lock() + defer channel.stateMutex.Unlock() + + if channel.registeredFounder != "" { + return ChannelAlreadyRegistered + } + channel.registeredFounder = founder + channel.registeredTime = time.Now() + return nil +} + +// IsRegistered returns whether the channel is registered. +func (channel *Channel) IsRegistered() bool { + channel.stateMutex.RLock() + defer channel.stateMutex.RUnlock() + return channel.registeredFounder != "" +} + func (channel *Channel) regenerateMembersCache() { // this is eventually consistent even without holding stateMutex.Lock() // throughout the update; all updates to `members` while holding Lock() @@ -340,59 +418,25 @@ func (channel *Channel) Join(client *Client, key string) { client.addChannel(channel) // give channel mode if necessary - var newChannel bool + newChannel := firstJoin && !channel.IsRegistered() var givenMode *Mode - client.server.registeredChannelsMutex.Lock() - defer client.server.registeredChannelsMutex.Unlock() - client.server.store.Update(func(tx *buntdb.Tx) error { - chanReg := client.server.loadChannelNoMutex(tx, channel.nameCasefolded) - - if chanReg == nil { - if firstJoin { - channel.stateMutex.Lock() - channel.createdTime = time.Now() - channel.members[client][ChannelOperator] = true - channel.stateMutex.Unlock() - givenMode = &ChannelOperator - newChannel = true - } - } else { - // we should only do this on registered channels - if client.account != nil && client.account.Name == chanReg.Founder { - channel.stateMutex.Lock() - channel.members[client][ChannelFounder] = true - channel.stateMutex.Unlock() - givenMode = &ChannelFounder - } - if firstJoin { - // apply other details if new channel - channel.stateMutex.Lock() - channel.topic = chanReg.Topic - channel.topicSetBy = chanReg.TopicSetBy - channel.topicSetTime = chanReg.TopicSetTime - channel.name = chanReg.Name - channel.createdTime = chanReg.RegisteredAt - for _, mask := range chanReg.Banlist { - channel.lists[BanMask].Add(mask) - } - for _, mask := range chanReg.Exceptlist { - channel.lists[ExceptMask].Add(mask) - } - for _, mask := range chanReg.Invitelist { - channel.lists[InviteMask].Add(mask) - } - channel.stateMutex.Unlock() - } - } - return nil - }) + if client.AccountName() == channel.registeredFounder { + givenMode = &ChannelFounder + } else if newChannel { + givenMode = &ChannelOperator + } + if givenMode != nil { + channel.stateMutex.Lock() + channel.members[client][*givenMode] = true + channel.stateMutex.Unlock() + } if client.capabilities.Has(caps.ExtendedJoin) { client.Send(nil, client.nickMaskString, "JOIN", channel.name, client.account.Name, client.realname) } else { client.Send(nil, client.nickMaskString, "JOIN", channel.name) } - // don't sent topic when it's an entirely new channel + // don't send topic when it's an entirely new channel if !newChannel { channel.SendTopic(client) } @@ -468,23 +512,7 @@ func (channel *Channel) SetTopic(client *Client, topic string) { member.Send(nil, client.nickMaskString, "TOPIC", channel.name, topic) } - // update saved channel topic for registered chans - client.server.registeredChannelsMutex.Lock() - defer client.server.registeredChannelsMutex.Unlock() - - client.server.store.Update(func(tx *buntdb.Tx) error { - chanInfo := client.server.loadChannelNoMutex(tx, channel.nameCasefolded) - - if chanInfo == nil { - return nil - } - - chanInfo.Topic = topic - chanInfo.TopicSetBy = client.nickMaskString - chanInfo.TopicSetTime = time.Now() - client.server.saveChannelNoMutex(tx, channel.nameCasefolded, *chanInfo) - return nil - }) + go channel.server.channelRegistry.StoreChannel(channel, false) } // CanSpeak returns true if the client can speak on this channel. diff --git a/irc/channelmanager.go b/irc/channelmanager.go index 8eff60dc..ddc25c71 100644 --- a/irc/channelmanager.go +++ b/irc/channelmanager.go @@ -59,11 +59,21 @@ func (cm *ChannelManager) Join(client *Client, name string, key string) error { cm.Lock() entry := cm.chans[casefoldedName] if entry == nil { - entry = &channelManagerEntry{ - channel: NewChannel(server, name, true), - pendingJoins: 0, + // XXX give up the lock to check for a registration, then check again + // to see if we need to create the channel. we could solve this by doing LoadChannel + // outside the lock initially on every join, so this is best thought of as an + // optimization to avoid that. + cm.Unlock() + info := client.server.channelRegistry.LoadChannel(casefoldedName) + cm.Lock() + entry = cm.chans[casefoldedName] + if entry == nil { + entry = &channelManagerEntry{ + channel: NewChannel(server, name, true, info), + pendingJoins: 0, + } + cm.chans[casefoldedName] = entry } - cm.chans[casefoldedName] = entry } entry.pendingJoins += 1 cm.Unlock() @@ -85,7 +95,12 @@ func (cm *ChannelManager) maybeCleanup(entry *channelManagerEntry, afterJoin boo if afterJoin { entry.pendingJoins -= 1 } - if entry.channel.IsEmpty() && entry.pendingJoins == 0 { + // TODO(slingamn) right now, registered channels cannot be cleaned up. + // this is because once ChannelManager becomes the source of truth about a channel, + // we can't move the source of truth back to the database unless we do an ACID + // store while holding the ChannelManager's Lock(). This is pending more decisions + // about where the database transaction lock fits into the overall lock model. + if !entry.channel.IsRegistered() && entry.channel.IsEmpty() && entry.pendingJoins == 0 { // reread the name, handling the case where the channel was renamed casefoldedName := entry.channel.NameCasefolded() delete(cm.chans, casefoldedName) diff --git a/irc/channelreg.go b/irc/channelreg.go index ab772a0a..fd236282 100644 --- a/irc/channelreg.go +++ b/irc/channelreg.go @@ -4,9 +4,9 @@ package irc import ( - "errors" "fmt" "strconv" + "sync" "time" "encoding/json" @@ -14,6 +14,9 @@ import ( "github.com/tidwall/buntdb" ) +// this is exclusively the *persistence* layer for channel registration; +// channel creation/tracking/destruction is in channelmanager.go + const ( keyChannelExists = "channel.exists %s" keyChannelName = "channel.name %s" // stores the 'preferred name' of the channel, not casemapped @@ -28,7 +31,18 @@ const ( ) var ( - errChanExists = errors.New("Channel already exists") + channelKeyStrings = []string{ + keyChannelExists, + keyChannelName, + keyChannelRegTime, + keyChannelFounder, + keyChannelTopic, + keyChannelTopicSetBy, + keyChannelTopicSetTime, + keyChannelBanlist, + keyChannelExceptlist, + keyChannelInvitelist, + } ) // RegisteredChannel holds details about a given registered channel. @@ -53,62 +67,142 @@ type RegisteredChannel struct { Invitelist []string } -// deleteChannelNoMutex deletes a given channel from our store. -func (server *Server) deleteChannelNoMutex(tx *buntdb.Tx, channelKey string) { - tx.Delete(fmt.Sprintf(keyChannelExists, channelKey)) - server.registeredChannels[channelKey] = nil +type ChannelRegistry struct { + // this serializes operations of the form (read channel state, synchronously persist it); + // this is enough to guarantee eventual consistency of the database with the + // ChannelManager and Channel objects, which are the source of truth. + // Wwe could use the buntdb RW transaction lock for this purpose but we share + // that with all the other modules, so let's not. + sync.Mutex // tier 2 + server *Server + channels map[string]*RegisteredChannel } -// loadChannelNoMutex loads a channel from the store. -func (server *Server) loadChannelNoMutex(tx *buntdb.Tx, channelKey string) *RegisteredChannel { - // return loaded chan if it already exists - if server.registeredChannels[channelKey] != nil { - return server.registeredChannels[channelKey] +func NewChannelRegistry(server *Server) *ChannelRegistry { + return &ChannelRegistry{ + server: server, } - _, err := tx.Get(fmt.Sprintf(keyChannelExists, channelKey)) - if err == buntdb.ErrNotFound { - // chan does not already exist, return +} + +// StoreChannel obtains a consistent view of a channel, then persists it to the store. +func (reg *ChannelRegistry) StoreChannel(channel *Channel, includeLists bool) { + if !reg.server.ChannelRegistrationEnabled() { + return + } + + reg.Lock() + defer reg.Unlock() + + key := channel.NameCasefolded() + info := channel.ExportRegistration(includeLists) + if info.Founder == "" { + // sanity check, don't try to store an unregistered channel + return + } + + reg.server.store.Update(func(tx *buntdb.Tx) error { + reg.saveChannel(tx, key, info, includeLists) + return nil + }) +} + +// LoadChannel loads a channel from the store. +func (reg *ChannelRegistry) LoadChannel(nameCasefolded string) (info *RegisteredChannel) { + if !reg.server.ChannelRegistrationEnabled() { return nil } - // channel exists, load it - name, _ := tx.Get(fmt.Sprintf(keyChannelName, channelKey)) - regTime, _ := tx.Get(fmt.Sprintf(keyChannelRegTime, channelKey)) - regTimeInt, _ := strconv.ParseInt(regTime, 10, 64) - founder, _ := tx.Get(fmt.Sprintf(keyChannelFounder, channelKey)) - topic, _ := tx.Get(fmt.Sprintf(keyChannelTopic, channelKey)) - topicSetBy, _ := tx.Get(fmt.Sprintf(keyChannelTopicSetBy, channelKey)) - topicSetTime, _ := tx.Get(fmt.Sprintf(keyChannelTopicSetTime, channelKey)) - topicSetTimeInt, _ := strconv.ParseInt(topicSetTime, 10, 64) - banlistString, _ := tx.Get(fmt.Sprintf(keyChannelBanlist, channelKey)) - exceptlistString, _ := tx.Get(fmt.Sprintf(keyChannelExceptlist, channelKey)) - invitelistString, _ := tx.Get(fmt.Sprintf(keyChannelInvitelist, channelKey)) + channelKey := nameCasefolded + // nice to have: do all JSON (de)serialization outside of the buntdb transaction + reg.server.store.View(func(tx *buntdb.Tx) error { + _, err := tx.Get(fmt.Sprintf(keyChannelExists, channelKey)) + if err == buntdb.ErrNotFound { + // chan does not already exist, return + return nil + } - var banlist []string - _ = json.Unmarshal([]byte(banlistString), &banlist) - var exceptlist []string - _ = json.Unmarshal([]byte(exceptlistString), &exceptlist) - var invitelist []string - _ = json.Unmarshal([]byte(invitelistString), &invitelist) + // channel exists, load it + name, _ := tx.Get(fmt.Sprintf(keyChannelName, channelKey)) + regTime, _ := tx.Get(fmt.Sprintf(keyChannelRegTime, channelKey)) + regTimeInt, _ := strconv.ParseInt(regTime, 10, 64) + founder, _ := tx.Get(fmt.Sprintf(keyChannelFounder, channelKey)) + topic, _ := tx.Get(fmt.Sprintf(keyChannelTopic, channelKey)) + topicSetBy, _ := tx.Get(fmt.Sprintf(keyChannelTopicSetBy, channelKey)) + topicSetTime, _ := tx.Get(fmt.Sprintf(keyChannelTopicSetTime, channelKey)) + topicSetTimeInt, _ := strconv.ParseInt(topicSetTime, 10, 64) + banlistString, _ := tx.Get(fmt.Sprintf(keyChannelBanlist, channelKey)) + exceptlistString, _ := tx.Get(fmt.Sprintf(keyChannelExceptlist, channelKey)) + invitelistString, _ := tx.Get(fmt.Sprintf(keyChannelInvitelist, channelKey)) - chanInfo := RegisteredChannel{ - Name: name, - RegisteredAt: time.Unix(regTimeInt, 0), - Founder: founder, - Topic: topic, - TopicSetBy: topicSetBy, - TopicSetTime: time.Unix(topicSetTimeInt, 0), - Banlist: banlist, - Exceptlist: exceptlist, - Invitelist: invitelist, - } - server.registeredChannels[channelKey] = &chanInfo + var banlist []string + _ = json.Unmarshal([]byte(banlistString), &banlist) + var exceptlist []string + _ = json.Unmarshal([]byte(exceptlistString), &exceptlist) + var invitelist []string + _ = json.Unmarshal([]byte(invitelistString), &invitelist) - return &chanInfo + info = &RegisteredChannel{ + Name: name, + RegisteredAt: time.Unix(regTimeInt, 0), + Founder: founder, + Topic: topic, + TopicSetBy: topicSetBy, + TopicSetTime: time.Unix(topicSetTimeInt, 0), + Banlist: banlist, + Exceptlist: exceptlist, + Invitelist: invitelist, + } + return nil + }) + + return info } -// saveChannelNoMutex saves a channel to the store. -func (server *Server) saveChannelNoMutex(tx *buntdb.Tx, channelKey string, channelInfo RegisteredChannel) { +// 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) { + if !reg.server.ChannelRegistrationEnabled() { + return + } + + reg.Lock() + defer reg.Unlock() + + includeLists := true + oldKey := casefoldedOldName + key := channel.NameCasefolded() + info := channel.ExportRegistration(includeLists) + if info.Founder == "" { + return + } + + reg.server.store.Update(func(tx *buntdb.Tx) error { + reg.deleteChannel(tx, oldKey, info) + reg.saveChannel(tx, key, info, includeLists) + return nil + }) +} + +// delete a channel, unless it was overwritten by another registration of the same channel +func (reg *ChannelRegistry) deleteChannel(tx *buntdb.Tx, key string, info RegisteredChannel) { + _, err := tx.Get(fmt.Sprintf(keyChannelExists, key)) + if err == nil { + regTime, _ := tx.Get(fmt.Sprintf(keyChannelRegTime, key)) + regTimeInt, _ := strconv.ParseInt(regTime, 10, 64) + registeredAt := time.Unix(regTimeInt, 0) + founder, _ := tx.Get(fmt.Sprintf(keyChannelFounder, key)) + + // to see if we're deleting the right channel, confirm the founder and the registration time + if founder == info.Founder && registeredAt == info.RegisteredAt { + for _, keyFmt := range channelKeyStrings { + tx.Delete(fmt.Sprintf(keyFmt, key)) + } + } + } +} + +// saveChannel saves a channel to the store. +func (reg *ChannelRegistry) saveChannel(tx *buntdb.Tx, channelKey string, channelInfo RegisteredChannel, includeLists bool) { tx.Set(fmt.Sprintf(keyChannelExists, channelKey), "1", nil) tx.Set(fmt.Sprintf(keyChannelName, channelKey), channelInfo.Name, nil) tx.Set(fmt.Sprintf(keyChannelRegTime, channelKey), strconv.FormatInt(channelInfo.RegisteredAt.Unix(), 10), nil) @@ -117,12 +211,12 @@ func (server *Server) saveChannelNoMutex(tx *buntdb.Tx, channelKey string, chann tx.Set(fmt.Sprintf(keyChannelTopicSetBy, channelKey), channelInfo.TopicSetBy, nil) tx.Set(fmt.Sprintf(keyChannelTopicSetTime, channelKey), strconv.FormatInt(channelInfo.TopicSetTime.Unix(), 10), nil) - banlistString, _ := json.Marshal(channelInfo.Banlist) - tx.Set(fmt.Sprintf(keyChannelBanlist, channelKey), string(banlistString), nil) - exceptlistString, _ := json.Marshal(channelInfo.Exceptlist) - tx.Set(fmt.Sprintf(keyChannelExceptlist, channelKey), string(exceptlistString), nil) - invitelistString, _ := json.Marshal(channelInfo.Invitelist) - tx.Set(fmt.Sprintf(keyChannelInvitelist, channelKey), string(invitelistString), nil) - - server.registeredChannels[channelKey] = &channelInfo + if includeLists { + banlistString, _ := json.Marshal(channelInfo.Banlist) + tx.Set(fmt.Sprintf(keyChannelBanlist, channelKey), string(banlistString), nil) + exceptlistString, _ := json.Marshal(channelInfo.Exceptlist) + tx.Set(fmt.Sprintf(keyChannelExceptlist, channelKey), string(exceptlistString), nil) + invitelistString, _ := json.Marshal(channelInfo.Invitelist) + tx.Set(fmt.Sprintf(keyChannelInvitelist, channelKey), string(invitelistString), nil) + } } diff --git a/irc/chanserv.go b/irc/chanserv.go index eb9523c4..841ce68c 100644 --- a/irc/chanserv.go +++ b/irc/chanserv.go @@ -6,12 +6,10 @@ package irc import ( "fmt" "strings" - "time" "github.com/goshuirc/irc-go/ircfmt" "github.com/goshuirc/irc-go/ircmsg" "github.com/oragono/oragono/irc/sno" - "github.com/tidwall/buntdb" ) // csHandler handles the /CS and /CHANSERV commands @@ -56,9 +54,6 @@ func (server *Server) chanservReceivePrivmsg(client *Client, message string) { return } - server.registeredChannelsMutex.Lock() - defer server.registeredChannelsMutex.Unlock() - channelName := params[1] channelKey, err := CasefoldChannel(channelName) if err != nil { @@ -67,57 +62,41 @@ func (server *Server) chanservReceivePrivmsg(client *Client, message string) { } channelInfo := server.channels.Get(channelKey) - if channelInfo == nil { + if channelInfo == nil || !channelInfo.ClientIsAtLeast(client, ChannelOperator) { client.ChanServNotice("You must be an oper on the channel to register it") return } - if !channelInfo.ClientIsAtLeast(client, ChannelOperator) { - client.ChanServNotice("You must be an oper on the channel to register it") + if client.account == &NoAccount { + client.ChanServNotice("You must be logged in to register a channel") return } - server.store.Update(func(tx *buntdb.Tx) error { - currentChan := server.loadChannelNoMutex(tx, channelKey) - if currentChan != nil { - client.ChanServNotice("Channel is already registered") - return nil + // this provides the synchronization that allows exactly one registration of the channel: + err = channelInfo.SetRegistered(client.AccountName()) + if err != nil { + client.ChanServNotice(err.Error()) + return + } + + // registration was successful: make the database reflect it + go server.channelRegistry.StoreChannel(channelInfo, true) + + client.ChanServNotice(fmt.Sprintf("Channel %s successfully registered", channelName)) + + server.logger.Info("chanserv", fmt.Sprintf("Client %s registered channel %s", client.nick, channelName)) + server.snomasks.Send(sno.LocalChannels, fmt.Sprintf(ircfmt.Unescape("Channel registered $c[grey][$r%s$c[grey]] by $c[grey][$r%s$c[grey]]"), channelName, client.nickMaskString)) + + // give them founder privs + change := channelInfo.applyModeMemberNoMutex(client, ChannelFounder, Add, client.NickCasefolded()) + if change != nil { + //TODO(dan): we should change the name of String and make it return a slice here + //TODO(dan): unify this code with code in modes.go + args := append([]string{channelName}, strings.Split(change.String(), " ")...) + for _, member := range channelInfo.Members() { + member.Send(nil, fmt.Sprintf("ChanServ!services@%s", client.server.name), "MODE", args...) } - - account := client.account - if account == &NoAccount { - client.ChanServNotice("You must be logged in to register a channel") - return nil - } - - chanRegInfo := RegisteredChannel{ - Name: channelName, - RegisteredAt: time.Now(), - Founder: account.Name, - Topic: channelInfo.topic, - TopicSetBy: channelInfo.topicSetBy, - TopicSetTime: channelInfo.topicSetTime, - } - server.saveChannelNoMutex(tx, channelKey, chanRegInfo) - - client.ChanServNotice(fmt.Sprintf("Channel %s successfully registered", channelName)) - - server.logger.Info("chanserv", fmt.Sprintf("Client %s registered channel %s", client.nick, channelName)) - server.snomasks.Send(sno.LocalChannels, fmt.Sprintf(ircfmt.Unescape("Channel registered $c[grey][$r%s$c[grey]] by $c[grey][$r%s$c[grey]]"), channelName, client.nickMaskString)) - - // give them founder privs - change := channelInfo.applyModeMemberNoMutex(client, ChannelFounder, Add, client.nickCasefolded) - if change != nil { - //TODO(dan): we should change the name of String and make it return a slice here - //TODO(dan): unify this code with code in modes.go - args := append([]string{channelName}, strings.Split(change.String(), " ")...) - for _, member := range channelInfo.Members() { - member.Send(nil, fmt.Sprintf("ChanServ!services@%s", client.server.name), "MODE", args...) - } - } - - return nil - }) + } } else { client.ChanServNotice("Sorry, I don't know that command") } diff --git a/irc/getters.go b/irc/getters.go index 0dac3aba..e65d232b 100644 --- a/irc/getters.go +++ b/irc/getters.go @@ -47,6 +47,12 @@ func (server *Server) DefaultChannelModes() Modes { return server.defaultChannelModes } +func (server *Server) ChannelRegistrationEnabled() bool { + server.configurableStateMutex.RLock() + defer server.configurableStateMutex.RUnlock() + return server.channelRegistrationEnabled +} + func (client *Client) Nick() string { client.stateMutex.RLock() defer client.stateMutex.RUnlock() @@ -95,6 +101,12 @@ func (client *Client) Destroyed() bool { return client.isDestroyed } +func (client *Client) AccountName() string { + client.stateMutex.RLock() + defer client.stateMutex.RUnlock() + return client.account.Name +} + func (client *Client) HasMode(mode Mode) bool { client.stateMutex.RLock() defer client.stateMutex.RUnlock() @@ -174,6 +186,12 @@ func (channel *Channel) HasMode(mode Mode) bool { return channel.flags[mode] } +func (channel *Channel) Founder() string { + channel.stateMutex.RLock() + defer channel.stateMutex.RUnlock() + return channel.registeredFounder +} + // set a channel mode, return whether it was already set func (channel *Channel) setMode(mode Mode, enable bool) (already bool) { channel.stateMutex.Lock() diff --git a/irc/modes.go b/irc/modes.go index 0ea5e544..ee767ad2 100644 --- a/irc/modes.go +++ b/irc/modes.go @@ -11,7 +11,6 @@ import ( "github.com/goshuirc/irc-go/ircmsg" "github.com/oragono/oragono/irc/sno" - "github.com/tidwall/buntdb" ) // ModeOp is an operation performed with modes @@ -645,39 +644,9 @@ func cmodeHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool { } } - server.registeredChannelsMutex.Lock() - if 0 < len(applied) && server.registeredChannels[channel.nameCasefolded] != nil && (banlistUpdated || exceptlistUpdated || invexlistUpdated) { - server.store.Update(func(tx *buntdb.Tx) error { - chanInfo := server.loadChannelNoMutex(tx, channel.nameCasefolded) - - if banlistUpdated { - var banlist []string - for mask := range channel.lists[BanMask].masks { - banlist = append(banlist, mask) - } - chanInfo.Banlist = banlist - } - if exceptlistUpdated { - var exceptlist []string - for mask := range channel.lists[ExceptMask].masks { - exceptlist = append(exceptlist, mask) - } - chanInfo.Exceptlist = exceptlist - } - if invexlistUpdated { - var invitelist []string - for mask := range channel.lists[InviteMask].masks { - invitelist = append(invitelist, mask) - } - chanInfo.Invitelist = invitelist - } - - server.saveChannelNoMutex(tx, channel.nameCasefolded, *chanInfo) - - return nil - }) + if (banlistUpdated || exceptlistUpdated || invexlistUpdated) && channel.IsRegistered() { + go server.channelRegistry.StoreChannel(channel, true) } - server.registeredChannelsMutex.Unlock() // send out changes if len(applied) > 0 { diff --git a/irc/server.go b/irc/server.go index eb40dddd..2dba2393 100644 --- a/irc/server.go +++ b/irc/server.go @@ -84,6 +84,7 @@ type Server struct { accounts map[string]*ClientAccount channelRegistrationEnabled bool channels *ChannelManager + channelRegistry *ChannelRegistry checkIdent bool clients *ClientLookupSet commands chan Command @@ -112,8 +113,6 @@ type Server struct { password []byte passwords *passwd.SaltedManager recoverFromErrors bool - registeredChannels map[string]*RegisteredChannel - registeredChannelsMutex sync.RWMutex rehashMutex sync.Mutex rehashSignal chan os.Signal proxyAllowedFrom []string @@ -158,7 +157,6 @@ func NewServer(config *Config, logger *logger.Manager) (*Server, error) { logger: logger, monitorManager: NewMonitorManager(), newConns: make(chan clientConn), - registeredChannels: make(map[string]*RegisteredChannel), rehashSignal: make(chan os.Signal, 1), signals: make(chan os.Signal, len(ServerExitSignals)), snomasks: NewSnoManager(), @@ -558,10 +556,6 @@ func pongHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool { func renameHandler(server *Server, client *Client, msg ircmsg.IrcMessage) (result bool) { result = false - // TODO(slingamn, #152) clean up locking here - server.registeredChannelsMutex.Lock() - defer server.registeredChannelsMutex.Unlock() - errorResponse := func(err error, name string) { // TODO: send correct error codes, e.g., ERR_CANNOTRENAME, ERR_CHANNAMEINUSE var code string @@ -591,11 +585,6 @@ func renameHandler(server *Server, client *Client, msg ircmsg.IrcMessage) (resul errorResponse(InvalidChannelName, oldName) return } - casefoldedNewName, err := CasefoldChannel(newName) - if err != nil { - errorResponse(InvalidChannelName, newName) - return - } reason := "No reason" if 2 < len(msg.Params) { @@ -613,20 +602,8 @@ func renameHandler(server *Server, client *Client, msg ircmsg.IrcMessage) (resul return } - var canEdit bool - server.store.Update(func(tx *buntdb.Tx) error { - chanReg := server.loadChannelNoMutex(tx, casefoldedOldName) - if chanReg == nil || !client.LoggedIntoAccount() || client.account.Name == chanReg.Founder { - canEdit = true - } - - chanReg = server.loadChannelNoMutex(tx, casefoldedNewName) - if chanReg != nil { - canEdit = false - } - return nil - }) - if !canEdit { + founder := channel.Founder() + if founder != "" && founder != client.AccountName() { //TODO(dan): Change this to ERR_CANNOTRENAME client.Send(nil, server.name, ERR_UNKNOWNERROR, client.nick, "RENAME", oldName, "Only channel founders can change registered channels") return false @@ -639,20 +616,8 @@ func renameHandler(server *Server, client *Client, msg ircmsg.IrcMessage) (resul return } - // rename stored channel info if any exists - server.store.Update(func(tx *buntdb.Tx) error { - chanReg := server.loadChannelNoMutex(tx, casefoldedOldName) - if chanReg == nil { - return nil - } - - server.deleteChannelNoMutex(tx, casefoldedOldName) - - chanReg.Name = newName - - server.saveChannelNoMutex(tx, casefoldedNewName, *chanReg) - return nil - }) + // rename succeeded, persist it + go server.channelRegistry.Rename(channel, casefoldedOldName) // send RENAME messages for _, mcl := range channel.Members() { @@ -1494,6 +1459,9 @@ func (server *Server) loadDatastore(datastorePath string) error { if err != nil { return fmt.Errorf("Could not load salt: %s", err.Error()) } + + server.channelRegistry = NewChannelRegistry(server) + return nil }