mirror of
https://github.com/ergochat/ergo.git
synced 2024-12-22 02:32:39 +01:00
remove registeredChannelsMutex
This moves channel registration to an eventual consistency model, where the in-memory datastructures (Channel and ChannelManager) are the exclusive source of truth, and updates to them get persisted asynchronously to the DB.
This commit is contained in:
parent
d5832bf765
commit
d4cb15354f
160
irc/channel.go
160
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.
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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()
|
||||
|
35
irc/modes.go
35
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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user