mirror of
https://github.com/ergochat/ergo.git
synced 2024-11-26 13:59:44 +01:00
Merge pull request #2028 from slingamn/channels_taketwo.1
refactor of channel persistence to use UUIDs
This commit is contained in:
commit
4317016a09
@ -39,7 +39,6 @@ const (
|
|||||||
keyAccountSettings = "account.settings %s"
|
keyAccountSettings = "account.settings %s"
|
||||||
keyAccountVHost = "account.vhost %s"
|
keyAccountVHost = "account.vhost %s"
|
||||||
keyCertToAccount = "account.creds.certfp %s"
|
keyCertToAccount = "account.creds.certfp %s"
|
||||||
keyAccountChannels = "account.channels %s" // channels registered to the account
|
|
||||||
keyAccountLastSeen = "account.lastseen %s"
|
keyAccountLastSeen = "account.lastseen %s"
|
||||||
keyAccountReadMarkers = "account.readmarkers %s"
|
keyAccountReadMarkers = "account.readmarkers %s"
|
||||||
keyAccountModes = "account.modes %s" // user modes for the always-on client as a string
|
keyAccountModes = "account.modes %s" // user modes for the always-on client as a string
|
||||||
@ -1765,7 +1764,6 @@ func (am *AccountManager) Unregister(account string, erase bool) error {
|
|||||||
nicksKey := fmt.Sprintf(keyAccountAdditionalNicks, casefoldedAccount)
|
nicksKey := fmt.Sprintf(keyAccountAdditionalNicks, casefoldedAccount)
|
||||||
settingsKey := fmt.Sprintf(keyAccountSettings, casefoldedAccount)
|
settingsKey := fmt.Sprintf(keyAccountSettings, casefoldedAccount)
|
||||||
vhostKey := fmt.Sprintf(keyAccountVHost, casefoldedAccount)
|
vhostKey := fmt.Sprintf(keyAccountVHost, casefoldedAccount)
|
||||||
channelsKey := fmt.Sprintf(keyAccountChannels, casefoldedAccount)
|
|
||||||
joinedChannelsKey := fmt.Sprintf(keyAccountChannelToModes, casefoldedAccount)
|
joinedChannelsKey := fmt.Sprintf(keyAccountChannelToModes, casefoldedAccount)
|
||||||
lastSeenKey := fmt.Sprintf(keyAccountLastSeen, casefoldedAccount)
|
lastSeenKey := fmt.Sprintf(keyAccountLastSeen, casefoldedAccount)
|
||||||
readMarkersKey := fmt.Sprintf(keyAccountReadMarkers, casefoldedAccount)
|
readMarkersKey := fmt.Sprintf(keyAccountReadMarkers, casefoldedAccount)
|
||||||
@ -1781,10 +1779,9 @@ func (am *AccountManager) Unregister(account string, erase bool) error {
|
|||||||
am.killClients(clients)
|
am.killClients(clients)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
var registeredChannels []string
|
|
||||||
// on our way out, unregister all the account's channels and delete them from the db
|
// on our way out, unregister all the account's channels and delete them from the db
|
||||||
defer func() {
|
defer func() {
|
||||||
for _, channelName := range registeredChannels {
|
for _, channelName := range am.server.channels.ChannelsForAccount(casefoldedAccount) {
|
||||||
err := am.server.channels.SetUnregistered(channelName, casefoldedAccount)
|
err := am.server.channels.SetUnregistered(channelName, casefoldedAccount)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
am.server.logger.Error("internal", "couldn't unregister channel", channelName, err.Error())
|
am.server.logger.Error("internal", "couldn't unregister channel", channelName, err.Error())
|
||||||
@ -1799,7 +1796,6 @@ func (am *AccountManager) Unregister(account string, erase bool) error {
|
|||||||
defer am.serialCacheUpdateMutex.Unlock()
|
defer am.serialCacheUpdateMutex.Unlock()
|
||||||
|
|
||||||
var accountName string
|
var accountName string
|
||||||
var channelsStr string
|
|
||||||
keepProtections := false
|
keepProtections := false
|
||||||
am.server.store.Update(func(tx *buntdb.Tx) error {
|
am.server.store.Update(func(tx *buntdb.Tx) error {
|
||||||
// get the unfolded account name; for an active account, this is
|
// get the unfolded account name; for an active account, this is
|
||||||
@ -1827,8 +1823,6 @@ func (am *AccountManager) Unregister(account string, erase bool) error {
|
|||||||
credText, err = tx.Get(credentialsKey)
|
credText, err = tx.Get(credentialsKey)
|
||||||
tx.Delete(credentialsKey)
|
tx.Delete(credentialsKey)
|
||||||
tx.Delete(vhostKey)
|
tx.Delete(vhostKey)
|
||||||
channelsStr, _ = tx.Get(channelsKey)
|
|
||||||
tx.Delete(channelsKey)
|
|
||||||
tx.Delete(joinedChannelsKey)
|
tx.Delete(joinedChannelsKey)
|
||||||
tx.Delete(lastSeenKey)
|
tx.Delete(lastSeenKey)
|
||||||
tx.Delete(readMarkersKey)
|
tx.Delete(readMarkersKey)
|
||||||
@ -1858,7 +1852,6 @@ func (am *AccountManager) Unregister(account string, erase bool) error {
|
|||||||
|
|
||||||
skeleton, _ := Skeleton(accountName)
|
skeleton, _ := Skeleton(accountName)
|
||||||
additionalNicks := unmarshalReservedNicks(rawNicks)
|
additionalNicks := unmarshalReservedNicks(rawNicks)
|
||||||
registeredChannels = unmarshalRegisteredChannels(channelsStr)
|
|
||||||
|
|
||||||
am.Lock()
|
am.Lock()
|
||||||
defer am.Unlock()
|
defer am.Unlock()
|
||||||
@ -1890,21 +1883,6 @@ func unmarshalRegisteredChannels(channelsStr string) (result []string) {
|
|||||||
return
|
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) AuthenticateByCertificate(client *Client, certfp string, peerCerts []*x509.Certificate, authzid string) (err error) {
|
func (am *AccountManager) AuthenticateByCertificate(client *Client, certfp string, peerCerts []*x509.Certificate, authzid string) (err error) {
|
||||||
if certfp == "" {
|
if certfp == "" {
|
||||||
return errAccountInvalidCredentials
|
return errAccountInvalidCredentials
|
||||||
|
106
irc/bunt/bunt_datastore.go
Normal file
106
irc/bunt/bunt_datastore.go
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
// Copyright (c) 2022 Shivaram Lingamneni
|
||||||
|
// released under the MIT license
|
||||||
|
|
||||||
|
package bunt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/tidwall/buntdb"
|
||||||
|
|
||||||
|
"github.com/ergochat/ergo/irc/datastore"
|
||||||
|
"github.com/ergochat/ergo/irc/logger"
|
||||||
|
"github.com/ergochat/ergo/irc/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BuntKey yields a string key corresponding to a (table, UUID) pair.
|
||||||
|
// Ideally this would not be public, but some of the migration code
|
||||||
|
// needs it.
|
||||||
|
func BuntKey(table datastore.Table, uuid utils.UUID) string {
|
||||||
|
return fmt.Sprintf("%x %s", table, uuid.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// buntdbDatastore implements datastore.Datastore using a buntdb.
|
||||||
|
type buntdbDatastore struct {
|
||||||
|
db *buntdb.DB
|
||||||
|
logger *logger.Manager
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBuntdbDatastore returns a datastore.Datastore backed by buntdb.
|
||||||
|
func NewBuntdbDatastore(db *buntdb.DB, logger *logger.Manager) datastore.Datastore {
|
||||||
|
return &buntdbDatastore{
|
||||||
|
db: db,
|
||||||
|
logger: logger,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *buntdbDatastore) Backoff() time.Duration {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *buntdbDatastore) GetAll(table datastore.Table) (result []datastore.KV, err error) {
|
||||||
|
tablePrefix := fmt.Sprintf("%x ", table)
|
||||||
|
err = b.db.View(func(tx *buntdb.Tx) error {
|
||||||
|
err := tx.AscendGreaterOrEqual("", tablePrefix, func(key, value string) bool {
|
||||||
|
if !strings.HasPrefix(key, tablePrefix) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
uuid, err := utils.DecodeUUID(strings.TrimPrefix(key, tablePrefix))
|
||||||
|
if err == nil {
|
||||||
|
result = append(result, datastore.KV{UUID: uuid, Value: []byte(value)})
|
||||||
|
} else {
|
||||||
|
b.logger.Error("datastore", "invalid uuid", key)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *buntdbDatastore) Get(table datastore.Table, uuid utils.UUID) (value []byte, err error) {
|
||||||
|
buntKey := BuntKey(table, uuid)
|
||||||
|
var result string
|
||||||
|
err = b.db.View(func(tx *buntdb.Tx) error {
|
||||||
|
result, err = tx.Get(buntKey)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
return []byte(result), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *buntdbDatastore) Set(table datastore.Table, uuid utils.UUID, value []byte, expiration time.Time) (err error) {
|
||||||
|
buntKey := BuntKey(table, uuid)
|
||||||
|
var setOptions *buntdb.SetOptions
|
||||||
|
if !expiration.IsZero() {
|
||||||
|
ttl := time.Until(expiration)
|
||||||
|
if ttl > 0 {
|
||||||
|
setOptions = &buntdb.SetOptions{Expires: true, TTL: ttl}
|
||||||
|
} else {
|
||||||
|
return nil // it already expired, i guess?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
strVal := string(value)
|
||||||
|
|
||||||
|
err = b.db.Update(func(tx *buntdb.Tx) error {
|
||||||
|
_, _, err := tx.Set(buntKey, strVal, setOptions)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *buntdbDatastore) Delete(table datastore.Table, key utils.UUID) (err error) {
|
||||||
|
buntKey := BuntKey(table, key)
|
||||||
|
err = b.db.Update(func(tx *buntdb.Tx) error {
|
||||||
|
_, err := tx.Delete(buntKey)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
// deleting a nonexistent key is not considered an error
|
||||||
|
switch err {
|
||||||
|
case buntdb.ErrNotFound:
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
@ -16,6 +16,7 @@ import (
|
|||||||
"github.com/ergochat/irc-go/ircutils"
|
"github.com/ergochat/irc-go/ircutils"
|
||||||
|
|
||||||
"github.com/ergochat/ergo/irc/caps"
|
"github.com/ergochat/ergo/irc/caps"
|
||||||
|
"github.com/ergochat/ergo/irc/datastore"
|
||||||
"github.com/ergochat/ergo/irc/history"
|
"github.com/ergochat/ergo/irc/history"
|
||||||
"github.com/ergochat/ergo/irc/modes"
|
"github.com/ergochat/ergo/irc/modes"
|
||||||
"github.com/ergochat/ergo/irc/utils"
|
"github.com/ergochat/ergo/irc/utils"
|
||||||
@ -50,14 +51,14 @@ type Channel struct {
|
|||||||
stateMutex sync.RWMutex // tier 1
|
stateMutex sync.RWMutex // tier 1
|
||||||
writebackLock sync.Mutex // tier 1.5
|
writebackLock sync.Mutex // tier 1.5
|
||||||
joinPartMutex sync.Mutex // tier 3
|
joinPartMutex sync.Mutex // tier 3
|
||||||
ensureLoaded utils.Once // manages loading stored registration info from the database
|
|
||||||
dirtyBits uint
|
dirtyBits uint
|
||||||
settings ChannelSettings
|
settings ChannelSettings
|
||||||
|
uuid utils.UUID
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewChannel creates a new channel from a `Server` and a `name`
|
// NewChannel creates a new channel from a `Server` and a `name`
|
||||||
// string, which must be unique on the server.
|
// string, which must be unique on the server.
|
||||||
func NewChannel(s *Server, name, casefoldedName string, registered bool) *Channel {
|
func NewChannel(s *Server, name, casefoldedName string, registered bool, regInfo RegisteredChannel) *Channel {
|
||||||
config := s.Config()
|
config := s.Config()
|
||||||
|
|
||||||
channel := &Channel{
|
channel := &Channel{
|
||||||
@ -71,14 +72,15 @@ func NewChannel(s *Server, name, casefoldedName string, registered bool) *Channe
|
|||||||
channel.initializeLists()
|
channel.initializeLists()
|
||||||
channel.history.Initialize(0, 0)
|
channel.history.Initialize(0, 0)
|
||||||
|
|
||||||
if !registered {
|
if registered {
|
||||||
|
channel.applyRegInfo(regInfo)
|
||||||
|
} else {
|
||||||
channel.resizeHistory(config)
|
channel.resizeHistory(config)
|
||||||
for _, mode := range config.Channels.defaultModes {
|
for _, mode := range config.Channels.defaultModes {
|
||||||
channel.flags.SetMode(mode, true)
|
channel.flags.SetMode(mode, true)
|
||||||
}
|
}
|
||||||
// no loading to do, so "mark" the load operation as "done":
|
channel.uuid = utils.GenerateUUIDv4()
|
||||||
channel.ensureLoaded.Do(func() {})
|
}
|
||||||
} // else: modes will be loaded before first join
|
|
||||||
|
|
||||||
return channel
|
return channel
|
||||||
}
|
}
|
||||||
@ -92,24 +94,6 @@ func (channel *Channel) initializeLists() {
|
|||||||
channel.accountToUMode = make(map[string]modes.Mode)
|
channel.accountToUMode = make(map[string]modes.Mode)
|
||||||
}
|
}
|
||||||
|
|
||||||
// EnsureLoaded blocks until the channel's registration info has been loaded
|
|
||||||
// from the database.
|
|
||||||
func (channel *Channel) EnsureLoaded() {
|
|
||||||
channel.ensureLoaded.Do(func() {
|
|
||||||
nmc := channel.NameCasefolded()
|
|
||||||
info, err := channel.server.channelRegistry.LoadChannel(nmc)
|
|
||||||
if err == nil {
|
|
||||||
channel.applyRegInfo(info)
|
|
||||||
} else {
|
|
||||||
channel.server.logger.Error("internal", "couldn't load channel", nmc, err.Error())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (channel *Channel) IsLoaded() bool {
|
|
||||||
return channel.ensureLoaded.Done()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (channel *Channel) resizeHistory(config *Config) {
|
func (channel *Channel) resizeHistory(config *Config) {
|
||||||
status, _, _ := channel.historyStatus(config)
|
status, _, _ := channel.historyStatus(config)
|
||||||
if status == HistoryEphemeral {
|
if status == HistoryEphemeral {
|
||||||
@ -126,6 +110,7 @@ func (channel *Channel) applyRegInfo(chanReg RegisteredChannel) {
|
|||||||
channel.stateMutex.Lock()
|
channel.stateMutex.Lock()
|
||||||
defer channel.stateMutex.Unlock()
|
defer channel.stateMutex.Unlock()
|
||||||
|
|
||||||
|
channel.uuid = chanReg.UUID
|
||||||
channel.registeredFounder = chanReg.Founder
|
channel.registeredFounder = chanReg.Founder
|
||||||
channel.registeredTime = chanReg.RegisteredAt
|
channel.registeredTime = chanReg.RegisteredAt
|
||||||
channel.topic = chanReg.Topic
|
channel.topic = chanReg.Topic
|
||||||
@ -150,38 +135,41 @@ func (channel *Channel) applyRegInfo(chanReg RegisteredChannel) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// obtain a consistent snapshot of the channel state that can be persisted to the DB
|
// obtain a consistent snapshot of the channel state that can be persisted to the DB
|
||||||
func (channel *Channel) ExportRegistration(includeFlags uint) (info RegisteredChannel) {
|
func (channel *Channel) ExportRegistration() (info RegisteredChannel) {
|
||||||
channel.stateMutex.RLock()
|
channel.stateMutex.RLock()
|
||||||
defer channel.stateMutex.RUnlock()
|
defer channel.stateMutex.RUnlock()
|
||||||
|
|
||||||
info.Name = channel.name
|
info.Name = channel.name
|
||||||
info.NameCasefolded = channel.nameCasefolded
|
info.UUID = channel.uuid
|
||||||
info.Founder = channel.registeredFounder
|
info.Founder = channel.registeredFounder
|
||||||
info.RegisteredAt = channel.registeredTime
|
info.RegisteredAt = channel.registeredTime
|
||||||
|
|
||||||
if includeFlags&IncludeTopic != 0 {
|
|
||||||
info.Topic = channel.topic
|
info.Topic = channel.topic
|
||||||
info.TopicSetBy = channel.topicSetBy
|
info.TopicSetBy = channel.topicSetBy
|
||||||
info.TopicSetTime = channel.topicSetTime
|
info.TopicSetTime = channel.topicSetTime
|
||||||
}
|
|
||||||
|
|
||||||
if includeFlags&IncludeModes != 0 {
|
|
||||||
info.Key = channel.key
|
info.Key = channel.key
|
||||||
info.Forward = channel.forward
|
info.Forward = channel.forward
|
||||||
info.Modes = channel.flags.AllModes()
|
info.Modes = channel.flags.AllModes()
|
||||||
info.UserLimit = channel.userLimit
|
info.UserLimit = channel.userLimit
|
||||||
}
|
|
||||||
|
|
||||||
if includeFlags&IncludeLists != 0 {
|
|
||||||
info.Bans = channel.lists[modes.BanMask].Masks()
|
info.Bans = channel.lists[modes.BanMask].Masks()
|
||||||
info.Invites = channel.lists[modes.InviteMask].Masks()
|
info.Invites = channel.lists[modes.InviteMask].Masks()
|
||||||
info.Excepts = channel.lists[modes.ExceptMask].Masks()
|
info.Excepts = channel.lists[modes.ExceptMask].Masks()
|
||||||
info.AccountToUMode = utils.CopyMap(channel.accountToUMode)
|
info.AccountToUMode = utils.CopyMap(channel.accountToUMode)
|
||||||
|
|
||||||
|
info.Settings = channel.settings
|
||||||
|
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if includeFlags&IncludeSettings != 0 {
|
func (channel *Channel) exportSummary() (info RegisteredChannel) {
|
||||||
info.Settings = channel.settings
|
channel.stateMutex.RLock()
|
||||||
}
|
defer channel.stateMutex.RUnlock()
|
||||||
|
|
||||||
|
info.Name = channel.name
|
||||||
|
info.Founder = channel.registeredFounder
|
||||||
|
info.RegisteredAt = channel.registeredTime
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -288,9 +276,19 @@ func (channel *Channel) performWrite(additionalDirtyBits uint) (err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
info := channel.ExportRegistration(dirtyBits)
|
var success bool
|
||||||
err = channel.server.channelRegistry.StoreChannel(info, dirtyBits)
|
info := channel.ExportRegistration()
|
||||||
if err != nil {
|
if b, err := info.Serialize(); err == nil {
|
||||||
|
if err := channel.server.dstore.Set(datastore.TableChannels, info.UUID, b, time.Time{}); err == nil {
|
||||||
|
success = true
|
||||||
|
} else {
|
||||||
|
channel.server.logger.Error("internal", "couldn't persist channel", info.Name, err.Error())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
channel.server.logger.Error("internal", "couldn't serialize channel", info.Name, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if !success {
|
||||||
channel.stateMutex.Lock()
|
channel.stateMutex.Lock()
|
||||||
channel.dirtyBits = channel.dirtyBits | dirtyBits
|
channel.dirtyBits = channel.dirtyBits | dirtyBits
|
||||||
channel.stateMutex.Unlock()
|
channel.stateMutex.Unlock()
|
||||||
@ -314,6 +312,7 @@ func (channel *Channel) SetRegistered(founder string) error {
|
|||||||
|
|
||||||
// SetUnregistered deletes the channel's registration information.
|
// SetUnregistered deletes the channel's registration information.
|
||||||
func (channel *Channel) SetUnregistered(expectedFounder string) {
|
func (channel *Channel) SetUnregistered(expectedFounder string) {
|
||||||
|
uuid := utils.GenerateUUIDv4()
|
||||||
channel.stateMutex.Lock()
|
channel.stateMutex.Lock()
|
||||||
defer channel.stateMutex.Unlock()
|
defer channel.stateMutex.Unlock()
|
||||||
|
|
||||||
@ -324,6 +323,9 @@ func (channel *Channel) SetUnregistered(expectedFounder string) {
|
|||||||
var zeroTime time.Time
|
var zeroTime time.Time
|
||||||
channel.registeredTime = zeroTime
|
channel.registeredTime = zeroTime
|
||||||
channel.accountToUMode = make(map[string]modes.Mode)
|
channel.accountToUMode = make(map[string]modes.Mode)
|
||||||
|
// reset the UUID so that any re-registration will persist under
|
||||||
|
// a separate key:
|
||||||
|
channel.uuid = uuid
|
||||||
}
|
}
|
||||||
|
|
||||||
// implements `CHANSERV CLEAR #chan ACCESS` (resets bans, invites, excepts, and amodes)
|
// implements `CHANSERV CLEAR #chan ACCESS` (resets bans, invites, excepts, and amodes)
|
||||||
|
@ -6,7 +6,9 @@ package irc
|
|||||||
import (
|
import (
|
||||||
"sort"
|
"sort"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ergochat/ergo/irc/datastore"
|
||||||
"github.com/ergochat/ergo/irc/utils"
|
"github.com/ergochat/ergo/irc/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -26,85 +28,75 @@ type ChannelManager struct {
|
|||||||
sync.RWMutex // tier 2
|
sync.RWMutex // tier 2
|
||||||
// chans is the main data structure, mapping casefolded name -> *Channel
|
// chans is the main data structure, mapping casefolded name -> *Channel
|
||||||
chans map[string]*channelManagerEntry
|
chans map[string]*channelManagerEntry
|
||||||
chansSkeletons utils.HashSet[string] // skeletons of *unregistered* chans
|
chansSkeletons utils.HashSet[string]
|
||||||
registeredChannels utils.HashSet[string] // casefolds of registered chans
|
purgedChannels map[string]ChannelPurgeRecord // casefolded name to purge record
|
||||||
registeredSkeletons utils.HashSet[string] // skeletons of registered chans
|
|
||||||
purgedChannels utils.HashSet[string] // casefolds of purged chans
|
|
||||||
server *Server
|
server *Server
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewChannelManager returns a new ChannelManager.
|
// NewChannelManager returns a new ChannelManager.
|
||||||
func (cm *ChannelManager) Initialize(server *Server) {
|
func (cm *ChannelManager) Initialize(server *Server, config *Config) (err error) {
|
||||||
cm.chans = make(map[string]*channelManagerEntry)
|
cm.chans = make(map[string]*channelManagerEntry)
|
||||||
cm.chansSkeletons = make(utils.HashSet[string])
|
cm.chansSkeletons = make(utils.HashSet[string])
|
||||||
cm.server = server
|
cm.server = server
|
||||||
|
return cm.loadRegisteredChannels(config)
|
||||||
// purging should work even if registration is disabled
|
|
||||||
cm.purgedChannels = cm.server.channelRegistry.PurgedChannels()
|
|
||||||
cm.loadRegisteredChannels(server.Config())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cm *ChannelManager) loadRegisteredChannels(config *Config) {
|
func (cm *ChannelManager) loadRegisteredChannels(config *Config) (err error) {
|
||||||
if !config.Channels.Registration.Enabled {
|
allChannels, err := FetchAndDeserializeAll[RegisteredChannel](datastore.TableChannels, cm.server.dstore, cm.server.logger)
|
||||||
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
allPurgeRecords, err := FetchAndDeserializeAll[ChannelPurgeRecord](datastore.TableChannelPurges, cm.server.dstore, cm.server.logger)
|
||||||
var newChannels []*Channel
|
if err != nil {
|
||||||
var collisions []string
|
return
|
||||||
defer func() {
|
|
||||||
for _, ch := range newChannels {
|
|
||||||
ch.EnsureLoaded()
|
|
||||||
cm.server.logger.Debug("channels", "initialized registered channel", ch.Name())
|
|
||||||
}
|
}
|
||||||
for _, collision := range collisions {
|
|
||||||
cm.server.logger.Warning("channels", "registered channel collides with existing channel", collision)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
rawNames := cm.server.channelRegistry.AllChannels()
|
|
||||||
|
|
||||||
cm.Lock()
|
cm.Lock()
|
||||||
defer cm.Unlock()
|
defer cm.Unlock()
|
||||||
|
|
||||||
cm.registeredChannels = make(utils.HashSet[string], len(rawNames))
|
cm.purgedChannels = make(map[string]ChannelPurgeRecord, len(allPurgeRecords))
|
||||||
cm.registeredSkeletons = make(utils.HashSet[string], len(rawNames))
|
for _, purge := range allPurgeRecords {
|
||||||
for _, name := range rawNames {
|
cm.purgedChannels[purge.NameCasefolded] = purge
|
||||||
cfname, err := CasefoldChannel(name)
|
|
||||||
if err == nil {
|
|
||||||
cm.registeredChannels.Add(cfname)
|
|
||||||
}
|
|
||||||
skeleton, err := Skeleton(name)
|
|
||||||
if err == nil {
|
|
||||||
cm.registeredSkeletons.Add(skeleton)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !cm.purgedChannels.Has(cfname) {
|
for _, regInfo := range allChannels {
|
||||||
if _, ok := cm.chans[cfname]; !ok {
|
cfname, err := CasefoldChannel(regInfo.Name)
|
||||||
ch := NewChannel(cm.server, name, cfname, true)
|
if err != nil {
|
||||||
|
cm.server.logger.Error("channels", "couldn't casefold registered channel, skipping", regInfo.Name, err.Error())
|
||||||
|
continue
|
||||||
|
} else {
|
||||||
|
cm.server.logger.Debug("channels", "initializing registered channel", regInfo.Name)
|
||||||
|
}
|
||||||
|
skeleton, err := Skeleton(regInfo.Name)
|
||||||
|
if err == nil {
|
||||||
|
cm.chansSkeletons.Add(skeleton)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := cm.purgedChannels[cfname]; !ok {
|
||||||
|
ch := NewChannel(cm.server, regInfo.Name, cfname, true, regInfo)
|
||||||
cm.chans[cfname] = &channelManagerEntry{
|
cm.chans[cfname] = &channelManagerEntry{
|
||||||
channel: ch,
|
channel: ch,
|
||||||
pendingJoins: 0,
|
pendingJoins: 0,
|
||||||
}
|
skeleton: skeleton,
|
||||||
newChannels = append(newChannels, ch)
|
|
||||||
} else {
|
|
||||||
collisions = append(collisions, name)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get returns an existing channel with name equivalent to `name`, or nil
|
// Get returns an existing channel with name equivalent to `name`, or nil
|
||||||
func (cm *ChannelManager) Get(name string) (channel *Channel) {
|
func (cm *ChannelManager) Get(name string) (channel *Channel) {
|
||||||
name, err := CasefoldChannel(name)
|
name, err := CasefoldChannel(name)
|
||||||
if err == nil {
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
cm.RLock()
|
cm.RLock()
|
||||||
defer cm.RUnlock()
|
defer cm.RUnlock()
|
||||||
entry := cm.chans[name]
|
entry := cm.chans[name]
|
||||||
// if the channel is still loading, pretend we don't have it
|
if entry != nil {
|
||||||
if entry != nil && entry.channel.IsLoaded() {
|
|
||||||
return entry.channel
|
return entry.channel
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -122,33 +114,26 @@ func (cm *ChannelManager) Join(client *Client, name string, key string, isSajoin
|
|||||||
cm.Lock()
|
cm.Lock()
|
||||||
defer cm.Unlock()
|
defer cm.Unlock()
|
||||||
|
|
||||||
if cm.purgedChannels.Has(casefoldedName) {
|
// check purges first; a registered purged channel will still be present in `chans`
|
||||||
|
if _, ok := cm.purgedChannels[casefoldedName]; ok {
|
||||||
return nil, errChannelPurged, false
|
return nil, errChannelPurged, false
|
||||||
}
|
}
|
||||||
entry := cm.chans[casefoldedName]
|
entry := cm.chans[casefoldedName]
|
||||||
if entry == nil {
|
if entry == nil {
|
||||||
registered := cm.registeredChannels.Has(casefoldedName)
|
if server.Config().Channels.OpOnlyCreation &&
|
||||||
// enforce OpOnlyCreation
|
|
||||||
if !registered && server.Config().Channels.OpOnlyCreation &&
|
|
||||||
!(isSajoin || client.HasRoleCapabs("chanreg")) {
|
!(isSajoin || client.HasRoleCapabs("chanreg")) {
|
||||||
return nil, errInsufficientPrivs, false
|
return nil, errInsufficientPrivs, false
|
||||||
}
|
}
|
||||||
// enforce confusables
|
// enforce confusables
|
||||||
if !registered && (cm.chansSkeletons.Has(skeleton) || cm.registeredSkeletons.Has(skeleton)) {
|
if cm.chansSkeletons.Has(skeleton) {
|
||||||
return nil, errConfusableIdentifier, false
|
return nil, errConfusableIdentifier, false
|
||||||
}
|
}
|
||||||
entry = &channelManagerEntry{
|
entry = &channelManagerEntry{
|
||||||
channel: NewChannel(server, name, casefoldedName, registered),
|
channel: NewChannel(server, name, casefoldedName, false, RegisteredChannel{}),
|
||||||
pendingJoins: 0,
|
pendingJoins: 0,
|
||||||
}
|
}
|
||||||
if !registered {
|
|
||||||
// for an unregistered channel, we already have the correct unfolded name
|
|
||||||
// and therefore the final skeleton. for a registered channel, we don't have
|
|
||||||
// the unfolded name yet (it needs to be loaded from the db), but we already
|
|
||||||
// have the final skeleton in `registeredSkeletons` so we don't need to track it
|
|
||||||
cm.chansSkeletons.Add(skeleton)
|
cm.chansSkeletons.Add(skeleton)
|
||||||
entry.skeleton = skeleton
|
entry.skeleton = skeleton
|
||||||
}
|
|
||||||
cm.chans[casefoldedName] = entry
|
cm.chans[casefoldedName] = entry
|
||||||
newChannel = true
|
newChannel = true
|
||||||
}
|
}
|
||||||
@ -160,7 +145,6 @@ func (cm *ChannelManager) Join(client *Client, name string, key string, isSajoin
|
|||||||
return err, ""
|
return err, ""
|
||||||
}
|
}
|
||||||
|
|
||||||
channel.EnsureLoaded()
|
|
||||||
err, forward = channel.Join(client, key, isSajoin || newChannel, rb)
|
err, forward = channel.Join(client, key, isSajoin || newChannel, rb)
|
||||||
|
|
||||||
cm.maybeCleanup(channel, true)
|
cm.maybeCleanup(channel, true)
|
||||||
@ -252,13 +236,6 @@ func (cm *ChannelManager) SetRegistered(channelName string, account string) (err
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// transfer the skeleton from chansSkeletons to registeredSkeletons
|
|
||||||
skeleton := entry.skeleton
|
|
||||||
delete(cm.chansSkeletons, skeleton)
|
|
||||||
entry.skeleton = ""
|
|
||||||
cm.chans[cfname] = entry
|
|
||||||
cm.registeredChannels.Add(cfname)
|
|
||||||
cm.registeredSkeletons.Add(skeleton)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -268,17 +245,13 @@ func (cm *ChannelManager) SetUnregistered(channelName string, account string) (e
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
info, err := cm.server.channelRegistry.LoadChannel(cfname)
|
var uuid utils.UUID
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if info.Founder != account {
|
|
||||||
return errChannelNotOwnedByAccount
|
|
||||||
}
|
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
err = cm.server.channelRegistry.Delete(info)
|
if delErr := cm.server.dstore.Delete(datastore.TableChannels, uuid); delErr != nil {
|
||||||
|
cm.server.logger.Error("datastore", "couldn't delete channel registration", cfname, delErr.Error())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@ -286,15 +259,11 @@ func (cm *ChannelManager) SetUnregistered(channelName string, account string) (e
|
|||||||
defer cm.Unlock()
|
defer cm.Unlock()
|
||||||
entry := cm.chans[cfname]
|
entry := cm.chans[cfname]
|
||||||
if entry != nil {
|
if entry != nil {
|
||||||
entry.channel.SetUnregistered(account)
|
if entry.channel.Founder() != account {
|
||||||
delete(cm.registeredChannels, cfname)
|
return errChannelNotOwnedByAccount
|
||||||
// transfer the skeleton from registeredSkeletons to chansSkeletons
|
|
||||||
if skel, err := Skeleton(entry.channel.Name()); err == nil {
|
|
||||||
delete(cm.registeredSkeletons, skel)
|
|
||||||
cm.chansSkeletons.Add(skel)
|
|
||||||
entry.skeleton = skel
|
|
||||||
cm.chans[cfname] = entry
|
|
||||||
}
|
}
|
||||||
|
uuid = entry.channel.UUID()
|
||||||
|
entry.channel.SetUnregistered(account) // changes the UUID
|
||||||
// #1619: if the channel has 0 members and was only being retained
|
// #1619: if the channel has 0 members and was only being retained
|
||||||
// because it was registered, clean it up:
|
// because it was registered, clean it up:
|
||||||
cm.maybeCleanupInternal(cfname, entry, false)
|
cm.maybeCleanupInternal(cfname, entry, false)
|
||||||
@ -322,12 +291,11 @@ func (cm *ChannelManager) Rename(name string, newName string) (err error) {
|
|||||||
var info RegisteredChannel
|
var info RegisteredChannel
|
||||||
defer func() {
|
defer func() {
|
||||||
if channel != nil && info.Founder != "" {
|
if channel != nil && info.Founder != "" {
|
||||||
channel.Store(IncludeAllAttrs)
|
channel.MarkDirty(IncludeAllAttrs)
|
||||||
if oldCfname != newCfname {
|
|
||||||
// we just flushed the channel under its new name, therefore this delete
|
|
||||||
// cannot be overwritten by a write to the old name:
|
|
||||||
cm.server.channelRegistry.Delete(info)
|
|
||||||
}
|
}
|
||||||
|
// always-on clients need to update their saved channel memberships
|
||||||
|
for _, member := range channel.Members() {
|
||||||
|
member.markDirty(IncludeChannels)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@ -335,11 +303,11 @@ func (cm *ChannelManager) Rename(name string, newName string) (err error) {
|
|||||||
defer cm.Unlock()
|
defer cm.Unlock()
|
||||||
|
|
||||||
entry := cm.chans[oldCfname]
|
entry := cm.chans[oldCfname]
|
||||||
if entry == nil || !entry.channel.IsLoaded() {
|
if entry == nil {
|
||||||
return errNoSuchChannel
|
return errNoSuchChannel
|
||||||
}
|
}
|
||||||
channel = entry.channel
|
channel = entry.channel
|
||||||
info = channel.ExportRegistration(IncludeInitial)
|
info = channel.ExportRegistration()
|
||||||
registered := info.Founder != ""
|
registered := info.Founder != ""
|
||||||
|
|
||||||
oldSkeleton, err := Skeleton(info.Name)
|
oldSkeleton, err := Skeleton(info.Name)
|
||||||
@ -348,13 +316,13 @@ func (cm *ChannelManager) Rename(name string, newName string) (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if newCfname != oldCfname {
|
if newCfname != oldCfname {
|
||||||
if cm.chans[newCfname] != nil || cm.registeredChannels.Has(newCfname) {
|
if cm.chans[newCfname] != nil {
|
||||||
return errChannelNameInUse
|
return errChannelNameInUse
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if oldSkeleton != newSkeleton {
|
if oldSkeleton != newSkeleton {
|
||||||
if cm.chansSkeletons.Has(newSkeleton) || cm.registeredSkeletons.Has(newSkeleton) {
|
if cm.chansSkeletons.Has(newSkeleton) {
|
||||||
return errConfusableIdentifier
|
return errConfusableIdentifier
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -364,15 +332,8 @@ func (cm *ChannelManager) Rename(name string, newName string) (err error) {
|
|||||||
entry.skeleton = newSkeleton
|
entry.skeleton = newSkeleton
|
||||||
}
|
}
|
||||||
cm.chans[newCfname] = entry
|
cm.chans[newCfname] = entry
|
||||||
if registered {
|
|
||||||
delete(cm.registeredChannels, oldCfname)
|
|
||||||
cm.registeredChannels.Add(newCfname)
|
|
||||||
delete(cm.registeredSkeletons, oldSkeleton)
|
|
||||||
cm.registeredSkeletons.Add(newSkeleton)
|
|
||||||
} else {
|
|
||||||
delete(cm.chansSkeletons, oldSkeleton)
|
delete(cm.chansSkeletons, oldSkeleton)
|
||||||
cm.chansSkeletons.Add(newSkeleton)
|
cm.chansSkeletons.Add(newSkeleton)
|
||||||
}
|
|
||||||
entry.channel.Rename(newName, newCfname)
|
entry.channel.Rename(newName, newCfname)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -390,7 +351,18 @@ func (cm *ChannelManager) Channels() (result []*Channel) {
|
|||||||
defer cm.RUnlock()
|
defer cm.RUnlock()
|
||||||
result = make([]*Channel, 0, len(cm.chans))
|
result = make([]*Channel, 0, len(cm.chans))
|
||||||
for _, entry := range cm.chans {
|
for _, entry := range cm.chans {
|
||||||
if entry.channel.IsLoaded() {
|
result = append(result, entry.channel)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListableChannels returns a slice of all non-purged channels.
|
||||||
|
func (cm *ChannelManager) ListableChannels() (result []*Channel) {
|
||||||
|
cm.RLock()
|
||||||
|
defer cm.RUnlock()
|
||||||
|
result = make([]*Channel, 0, len(cm.chans))
|
||||||
|
for cfname, entry := range cm.chans {
|
||||||
|
if _, ok := cm.purgedChannels[cfname]; !ok {
|
||||||
result = append(result, entry.channel)
|
result = append(result, entry.channel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -403,29 +375,46 @@ func (cm *ChannelManager) Purge(chname string, record ChannelPurgeRecord) (err e
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return errInvalidChannelName
|
return errInvalidChannelName
|
||||||
}
|
}
|
||||||
skel, err := Skeleton(chname)
|
|
||||||
if err != nil {
|
|
||||||
return errInvalidChannelName
|
|
||||||
}
|
|
||||||
|
|
||||||
|
record.NameCasefolded = chname
|
||||||
|
record.UUID = utils.GenerateUUIDv4()
|
||||||
|
|
||||||
|
channel, err := func() (channel *Channel, err error) {
|
||||||
cm.Lock()
|
cm.Lock()
|
||||||
cm.purgedChannels.Add(chname)
|
defer cm.Unlock()
|
||||||
entry := cm.chans[chname]
|
|
||||||
if entry != nil {
|
|
||||||
delete(cm.chans, chname)
|
|
||||||
if entry.channel.Founder() != "" {
|
|
||||||
delete(cm.registeredSkeletons, skel)
|
|
||||||
} else {
|
|
||||||
delete(cm.chansSkeletons, skel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cm.Unlock()
|
|
||||||
|
|
||||||
cm.server.channelRegistry.PurgeChannel(chname, record)
|
if _, ok := cm.purgedChannels[chname]; ok {
|
||||||
if entry != nil {
|
return nil, errChannelPurgedAlready
|
||||||
entry.channel.Purge("")
|
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
|
entry := cm.chans[chname]
|
||||||
|
// atomically prevent anyone from rejoining
|
||||||
|
cm.purgedChannels[chname] = record
|
||||||
|
if entry != nil {
|
||||||
|
channel = entry.channel
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if channel != nil {
|
||||||
|
// actually kick everyone off the channel
|
||||||
|
channel.Purge("")
|
||||||
|
}
|
||||||
|
|
||||||
|
var purgeBytes []byte
|
||||||
|
if purgeBytes, err = record.Serialize(); err != nil {
|
||||||
|
cm.server.logger.Error("internal", "couldn't serialize purge record", channel.Name(), err.Error())
|
||||||
|
}
|
||||||
|
// TODO we need a better story about error handling for later
|
||||||
|
if err = cm.server.dstore.Set(datastore.TableChannelPurges, record.UUID, purgeBytes, time.Time{}); err != nil {
|
||||||
|
cm.server.logger.Error("datastore", "couldn't store purge record", chname, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsPurged queries whether a channel is purged.
|
// IsPurged queries whether a channel is purged.
|
||||||
@ -436,7 +425,7 @@ func (cm *ChannelManager) IsPurged(chname string) (result bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cm.RLock()
|
cm.RLock()
|
||||||
result = cm.purgedChannels.Has(chname)
|
_, result = cm.purgedChannels[chname]
|
||||||
cm.RUnlock()
|
cm.RUnlock()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -449,14 +438,16 @@ func (cm *ChannelManager) Unpurge(chname string) (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cm.Lock()
|
cm.Lock()
|
||||||
found := cm.purgedChannels.Has(chname)
|
record, found := cm.purgedChannels[chname]
|
||||||
delete(cm.purgedChannels, chname)
|
delete(cm.purgedChannels, chname)
|
||||||
cm.Unlock()
|
cm.Unlock()
|
||||||
|
|
||||||
cm.server.channelRegistry.UnpurgeChannel(chname)
|
|
||||||
if !found {
|
if !found {
|
||||||
return errNoSuchChannel
|
return errNoSuchChannel
|
||||||
}
|
}
|
||||||
|
if err := cm.server.dstore.Delete(datastore.TableChannelPurges, record.UUID); err != nil {
|
||||||
|
cm.server.logger.Error("datastore", "couldn't delete purge record", chname, err.Error())
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -475,8 +466,46 @@ func (cm *ChannelManager) UnfoldName(cfname string) (result string) {
|
|||||||
cm.RLock()
|
cm.RLock()
|
||||||
entry := cm.chans[cfname]
|
entry := cm.chans[cfname]
|
||||||
cm.RUnlock()
|
cm.RUnlock()
|
||||||
if entry != nil && entry.channel.IsLoaded() {
|
if entry != nil {
|
||||||
return entry.channel.Name()
|
return entry.channel.Name()
|
||||||
}
|
}
|
||||||
return cfname
|
return cfname
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (cm *ChannelManager) LoadPurgeRecord(cfchname string) (record ChannelPurgeRecord, err error) {
|
||||||
|
cm.RLock()
|
||||||
|
defer cm.RUnlock()
|
||||||
|
|
||||||
|
if record, ok := cm.purgedChannels[cfchname]; ok {
|
||||||
|
return record, nil
|
||||||
|
} else {
|
||||||
|
return record, errNoSuchChannel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cm *ChannelManager) ChannelsForAccount(account string) (channels []string) {
|
||||||
|
cm.RLock()
|
||||||
|
defer cm.RUnlock()
|
||||||
|
|
||||||
|
for cfname, entry := range cm.chans {
|
||||||
|
if entry.channel.Founder() == account {
|
||||||
|
channels = append(channels, cfname)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// AllChannels returns the uncasefolded names of all registered channels.
|
||||||
|
func (cm *ChannelManager) AllRegisteredChannels() (result []string) {
|
||||||
|
cm.RLock()
|
||||||
|
defer cm.RUnlock()
|
||||||
|
|
||||||
|
for cfname, entry := range cm.chans {
|
||||||
|
if entry.channel.Founder() != "" {
|
||||||
|
result = append(result, cfname)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
@ -5,13 +5,8 @@ package irc
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/tidwall/buntdb"
|
|
||||||
|
|
||||||
"github.com/ergochat/ergo/irc/modes"
|
"github.com/ergochat/ergo/irc/modes"
|
||||||
"github.com/ergochat/ergo/irc/utils"
|
"github.com/ergochat/ergo/irc/utils"
|
||||||
)
|
)
|
||||||
@ -19,48 +14,6 @@ import (
|
|||||||
// this is exclusively the *persistence* layer for channel registration;
|
// this is exclusively the *persistence* layer for channel registration;
|
||||||
// channel creation/tracking/destruction is in channelmanager.go
|
// 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
|
|
||||||
keyChannelRegTime = "channel.registered.time %s"
|
|
||||||
keyChannelFounder = "channel.founder %s"
|
|
||||||
keyChannelTopic = "channel.topic %s"
|
|
||||||
keyChannelTopicSetBy = "channel.topic.setby %s"
|
|
||||||
keyChannelTopicSetTime = "channel.topic.settime %s"
|
|
||||||
keyChannelBanlist = "channel.banlist %s"
|
|
||||||
keyChannelExceptlist = "channel.exceptlist %s"
|
|
||||||
keyChannelInvitelist = "channel.invitelist %s"
|
|
||||||
keyChannelPassword = "channel.key %s"
|
|
||||||
keyChannelModes = "channel.modes %s"
|
|
||||||
keyChannelAccountToUMode = "channel.accounttoumode %s"
|
|
||||||
keyChannelUserLimit = "channel.userlimit %s"
|
|
||||||
keyChannelSettings = "channel.settings %s"
|
|
||||||
keyChannelForward = "channel.forward %s"
|
|
||||||
|
|
||||||
keyChannelPurged = "channel.purged %s"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
channelKeyStrings = []string{
|
|
||||||
keyChannelExists,
|
|
||||||
keyChannelName,
|
|
||||||
keyChannelRegTime,
|
|
||||||
keyChannelFounder,
|
|
||||||
keyChannelTopic,
|
|
||||||
keyChannelTopicSetBy,
|
|
||||||
keyChannelTopicSetTime,
|
|
||||||
keyChannelBanlist,
|
|
||||||
keyChannelExceptlist,
|
|
||||||
keyChannelInvitelist,
|
|
||||||
keyChannelPassword,
|
|
||||||
keyChannelModes,
|
|
||||||
keyChannelAccountToUMode,
|
|
||||||
keyChannelUserLimit,
|
|
||||||
keyChannelSettings,
|
|
||||||
keyChannelForward,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// these are bit flags indicating what part of the channel status is "dirty"
|
// these are bit flags indicating what part of the channel status is "dirty"
|
||||||
// and needs to be read from memory and written to the db
|
// and needs to be read from memory and written to the db
|
||||||
const (
|
const (
|
||||||
@ -80,8 +33,8 @@ const (
|
|||||||
type RegisteredChannel struct {
|
type RegisteredChannel struct {
|
||||||
// Name of the channel.
|
// Name of the channel.
|
||||||
Name string
|
Name string
|
||||||
// Casefolded name of the channel.
|
// UUID for the datastore.
|
||||||
NameCasefolded string
|
UUID utils.UUID
|
||||||
// RegisteredAt represents the time that the channel was registered.
|
// RegisteredAt represents the time that the channel was registered.
|
||||||
RegisteredAt time.Time
|
RegisteredAt time.Time
|
||||||
// Founder indicates the founder of the channel.
|
// Founder indicates the founder of the channel.
|
||||||
@ -112,322 +65,26 @@ type RegisteredChannel struct {
|
|||||||
Settings ChannelSettings
|
Settings ChannelSettings
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *RegisteredChannel) Serialize() ([]byte, error) {
|
||||||
|
return json.Marshal(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RegisteredChannel) Deserialize(b []byte) (err error) {
|
||||||
|
return json.Unmarshal(b, r)
|
||||||
|
}
|
||||||
|
|
||||||
type ChannelPurgeRecord struct {
|
type ChannelPurgeRecord struct {
|
||||||
|
NameCasefolded string `json:"Name"`
|
||||||
|
UUID utils.UUID
|
||||||
Oper string
|
Oper string
|
||||||
PurgedAt time.Time
|
PurgedAt time.Time
|
||||||
Reason string
|
Reason string
|
||||||
}
|
}
|
||||||
|
|
||||||
// ChannelRegistry manages registered channels.
|
func (c *ChannelPurgeRecord) Serialize() ([]byte, error) {
|
||||||
type ChannelRegistry struct {
|
return json.Marshal(c)
|
||||||
server *Server
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewChannelRegistry returns a new ChannelRegistry.
|
func (c *ChannelPurgeRecord) Deserialize(b []byte) error {
|
||||||
func (reg *ChannelRegistry) Initialize(server *Server) {
|
return json.Unmarshal(b, c)
|
||||||
reg.server = server
|
|
||||||
}
|
|
||||||
|
|
||||||
// AllChannels returns the uncasefolded names of all registered channels.
|
|
||||||
func (reg *ChannelRegistry) AllChannels() (result []string) {
|
|
||||||
prefix := fmt.Sprintf(keyChannelName, "")
|
|
||||||
reg.server.store.View(func(tx *buntdb.Tx) error {
|
|
||||||
return tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool {
|
|
||||||
if !strings.HasPrefix(key, prefix) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
result = append(result, value)
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// PurgedChannels returns the set of all casefolded channel names that have been purged
|
|
||||||
func (reg *ChannelRegistry) PurgedChannels() (result utils.HashSet[string]) {
|
|
||||||
result = make(utils.HashSet[string])
|
|
||||||
|
|
||||||
prefix := fmt.Sprintf(keyChannelPurged, "")
|
|
||||||
reg.server.store.View(func(tx *buntdb.Tx) error {
|
|
||||||
return tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool {
|
|
||||||
if !strings.HasPrefix(key, prefix) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
channel := strings.TrimPrefix(key, prefix)
|
|
||||||
result.Add(channel)
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// StoreChannel obtains a consistent view of a channel, then persists it to the store.
|
|
||||||
func (reg *ChannelRegistry) StoreChannel(info RegisteredChannel, includeFlags uint) (err error) {
|
|
||||||
if !reg.server.ChannelRegistrationEnabled() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
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, info, includeFlags)
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoadChannel loads a channel from the store.
|
|
||||||
func (reg *ChannelRegistry) LoadChannel(nameCasefolded string) (info RegisteredChannel, err error) {
|
|
||||||
if !reg.server.ChannelRegistrationEnabled() {
|
|
||||||
err = errFeatureDisabled
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
channelKey := nameCasefolded
|
|
||||||
// nice to have: do all JSON (de)serialization outside of the buntdb transaction
|
|
||||||
err = reg.server.store.View(func(tx *buntdb.Tx) error {
|
|
||||||
_, dberr := tx.Get(fmt.Sprintf(keyChannelExists, channelKey))
|
|
||||||
if dberr == buntdb.ErrNotFound {
|
|
||||||
// chan does not already exist, return
|
|
||||||
return errNoSuchChannel
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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))
|
|
||||||
var topicSetTime time.Time
|
|
||||||
topicSetTimeStr, _ := tx.Get(fmt.Sprintf(keyChannelTopicSetTime, channelKey))
|
|
||||||
if topicSetTimeInt, topicSetTimeErr := strconv.ParseInt(topicSetTimeStr, 10, 64); topicSetTimeErr == nil {
|
|
||||||
topicSetTime = time.Unix(0, topicSetTimeInt).UTC()
|
|
||||||
}
|
|
||||||
password, _ := tx.Get(fmt.Sprintf(keyChannelPassword, channelKey))
|
|
||||||
modeString, _ := tx.Get(fmt.Sprintf(keyChannelModes, channelKey))
|
|
||||||
userLimitString, _ := tx.Get(fmt.Sprintf(keyChannelUserLimit, channelKey))
|
|
||||||
forward, _ := tx.Get(fmt.Sprintf(keyChannelForward, channelKey))
|
|
||||||
banlistString, _ := tx.Get(fmt.Sprintf(keyChannelBanlist, channelKey))
|
|
||||||
exceptlistString, _ := tx.Get(fmt.Sprintf(keyChannelExceptlist, channelKey))
|
|
||||||
invitelistString, _ := tx.Get(fmt.Sprintf(keyChannelInvitelist, channelKey))
|
|
||||||
accountToUModeString, _ := tx.Get(fmt.Sprintf(keyChannelAccountToUMode, channelKey))
|
|
||||||
settingsString, _ := tx.Get(fmt.Sprintf(keyChannelSettings, channelKey))
|
|
||||||
|
|
||||||
modeSlice := make([]modes.Mode, len(modeString))
|
|
||||||
for i, mode := range modeString {
|
|
||||||
modeSlice[i] = modes.Mode(mode)
|
|
||||||
}
|
|
||||||
|
|
||||||
userLimit, _ := strconv.Atoi(userLimitString)
|
|
||||||
|
|
||||||
var banlist map[string]MaskInfo
|
|
||||||
_ = json.Unmarshal([]byte(banlistString), &banlist)
|
|
||||||
var exceptlist map[string]MaskInfo
|
|
||||||
_ = json.Unmarshal([]byte(exceptlistString), &exceptlist)
|
|
||||||
var invitelist map[string]MaskInfo
|
|
||||||
_ = json.Unmarshal([]byte(invitelistString), &invitelist)
|
|
||||||
accountToUMode := make(map[string]modes.Mode)
|
|
||||||
_ = json.Unmarshal([]byte(accountToUModeString), &accountToUMode)
|
|
||||||
|
|
||||||
var settings ChannelSettings
|
|
||||||
_ = json.Unmarshal([]byte(settingsString), &settings)
|
|
||||||
|
|
||||||
info = RegisteredChannel{
|
|
||||||
Name: name,
|
|
||||||
NameCasefolded: nameCasefolded,
|
|
||||||
RegisteredAt: time.Unix(0, regTimeInt).UTC(),
|
|
||||||
Founder: founder,
|
|
||||||
Topic: topic,
|
|
||||||
TopicSetBy: topicSetBy,
|
|
||||||
TopicSetTime: topicSetTime,
|
|
||||||
Key: password,
|
|
||||||
Modes: modeSlice,
|
|
||||||
Bans: banlist,
|
|
||||||
Excepts: exceptlist,
|
|
||||||
Invites: invitelist,
|
|
||||||
AccountToUMode: accountToUMode,
|
|
||||||
UserLimit: int(userLimit),
|
|
||||||
Settings: settings,
|
|
||||||
Forward: forward,
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete deletes a channel corresponding to `info`. If no such channel
|
|
||||||
// is present in the database, no error is returned.
|
|
||||||
func (reg *ChannelRegistry) Delete(info RegisteredChannel) (err error) {
|
|
||||||
if !reg.server.ChannelRegistrationEnabled() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
reg.server.store.Update(func(tx *buntdb.Tx) error {
|
|
||||||
reg.deleteChannel(tx, info.NameCasefolded, info)
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
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(0, regTimeInt).UTC()
|
|
||||||
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.Equal(info.RegisteredAt) {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (reg *ChannelRegistry) updateAccountToChannelMapping(tx *buntdb.Tx, channelInfo RegisteredChannel) {
|
|
||||||
channelKey := channelInfo.NameCasefolded
|
|
||||||
chanFounderKey := fmt.Sprintf(keyChannelFounder, channelKey)
|
|
||||||
founder, existsErr := tx.Get(chanFounderKey)
|
|
||||||
if existsErr == buntdb.ErrNotFound || founder != channelInfo.Founder {
|
|
||||||
// add to new founder's list
|
|
||||||
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 existsErr == nil && founder != channelInfo.Founder {
|
|
||||||
// remove from old founder's list
|
|
||||||
accountChannelsKey := fmt.Sprintf(keyAccountChannels, founder)
|
|
||||||
alreadyChannelsRaw, _ := tx.Get(accountChannelsKey)
|
|
||||||
var newChannels []string
|
|
||||||
if alreadyChannelsRaw != "" {
|
|
||||||
for _, chname := range strings.Split(alreadyChannelsRaw, ",") {
|
|
||||||
if chname != channelInfo.NameCasefolded {
|
|
||||||
newChannels = append(newChannels, chname)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tx.Set(accountChannelsKey, strings.Join(newChannels, ","), nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// saveChannel saves a channel to the store.
|
|
||||||
func (reg *ChannelRegistry) saveChannel(tx *buntdb.Tx, channelInfo RegisteredChannel, includeFlags uint) {
|
|
||||||
channelKey := channelInfo.NameCasefolded
|
|
||||||
// maintain the mapping of account -> registered channels
|
|
||||||
reg.updateAccountToChannelMapping(tx, channelInfo)
|
|
||||||
|
|
||||||
if includeFlags&IncludeInitial != 0 {
|
|
||||||
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.UnixNano(), 10), nil)
|
|
||||||
tx.Set(fmt.Sprintf(keyChannelFounder, channelKey), channelInfo.Founder, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
if includeFlags&IncludeTopic != 0 {
|
|
||||||
tx.Set(fmt.Sprintf(keyChannelTopic, channelKey), channelInfo.Topic, nil)
|
|
||||||
var topicSetTimeStr string
|
|
||||||
if !channelInfo.TopicSetTime.IsZero() {
|
|
||||||
topicSetTimeStr = strconv.FormatInt(channelInfo.TopicSetTime.UnixNano(), 10)
|
|
||||||
}
|
|
||||||
tx.Set(fmt.Sprintf(keyChannelTopicSetTime, channelKey), topicSetTimeStr, nil)
|
|
||||||
tx.Set(fmt.Sprintf(keyChannelTopicSetBy, channelKey), channelInfo.TopicSetBy, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
if includeFlags&IncludeModes != 0 {
|
|
||||||
tx.Set(fmt.Sprintf(keyChannelPassword, channelKey), channelInfo.Key, nil)
|
|
||||||
modeString := modes.Modes(channelInfo.Modes).String()
|
|
||||||
tx.Set(fmt.Sprintf(keyChannelModes, channelKey), modeString, nil)
|
|
||||||
tx.Set(fmt.Sprintf(keyChannelUserLimit, channelKey), strconv.Itoa(channelInfo.UserLimit), nil)
|
|
||||||
tx.Set(fmt.Sprintf(keyChannelForward, channelKey), channelInfo.Forward, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
if includeFlags&IncludeLists != 0 {
|
|
||||||
banlistString, _ := json.Marshal(channelInfo.Bans)
|
|
||||||
tx.Set(fmt.Sprintf(keyChannelBanlist, channelKey), string(banlistString), nil)
|
|
||||||
exceptlistString, _ := json.Marshal(channelInfo.Excepts)
|
|
||||||
tx.Set(fmt.Sprintf(keyChannelExceptlist, channelKey), string(exceptlistString), nil)
|
|
||||||
invitelistString, _ := json.Marshal(channelInfo.Invites)
|
|
||||||
tx.Set(fmt.Sprintf(keyChannelInvitelist, channelKey), string(invitelistString), nil)
|
|
||||||
accountToUModeString, _ := json.Marshal(channelInfo.AccountToUMode)
|
|
||||||
tx.Set(fmt.Sprintf(keyChannelAccountToUMode, channelKey), string(accountToUModeString), nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
if includeFlags&IncludeSettings != 0 {
|
|
||||||
settingsString, _ := json.Marshal(channelInfo.Settings)
|
|
||||||
tx.Set(fmt.Sprintf(keyChannelSettings, channelKey), string(settingsString), nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// PurgeChannel records a channel purge.
|
|
||||||
func (reg *ChannelRegistry) PurgeChannel(chname string, record ChannelPurgeRecord) (err error) {
|
|
||||||
serialized, err := json.Marshal(record)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
serializedStr := string(serialized)
|
|
||||||
key := fmt.Sprintf(keyChannelPurged, chname)
|
|
||||||
|
|
||||||
return reg.server.store.Update(func(tx *buntdb.Tx) error {
|
|
||||||
tx.Set(key, serializedStr, nil)
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoadPurgeRecord retrieves information about whether and how a channel was purged.
|
|
||||||
func (reg *ChannelRegistry) LoadPurgeRecord(chname string) (record ChannelPurgeRecord, err error) {
|
|
||||||
var rawRecord string
|
|
||||||
key := fmt.Sprintf(keyChannelPurged, chname)
|
|
||||||
reg.server.store.View(func(tx *buntdb.Tx) error {
|
|
||||||
rawRecord, _ = tx.Get(key)
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
if rawRecord == "" {
|
|
||||||
err = errNoSuchChannel
|
|
||||||
return
|
|
||||||
}
|
|
||||||
err = json.Unmarshal([]byte(rawRecord), &record)
|
|
||||||
if err != nil {
|
|
||||||
reg.server.logger.Error("internal", "corrupt purge record", chname, err.Error())
|
|
||||||
err = errNoSuchChannel
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnpurgeChannel deletes the record of a channel purge.
|
|
||||||
func (reg *ChannelRegistry) UnpurgeChannel(chname string) (err error) {
|
|
||||||
key := fmt.Sprintf(keyChannelPurged, chname)
|
|
||||||
return reg.server.store.Update(func(tx *buntdb.Tx) error {
|
|
||||||
tx.Delete(key)
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
@ -459,7 +459,7 @@ func csRegisterHandler(service *ircService, server *Server, client *Client, comm
|
|||||||
// check whether a client has already registered too many channels
|
// check whether a client has already registered too many channels
|
||||||
func checkChanLimit(service *ircService, client *Client, rb *ResponseBuffer) (ok bool) {
|
func checkChanLimit(service *ircService, client *Client, rb *ResponseBuffer) (ok bool) {
|
||||||
account := client.Account()
|
account := client.Account()
|
||||||
channelsAlreadyRegistered := client.server.accounts.ChannelsForAccount(account)
|
channelsAlreadyRegistered := client.server.channels.ChannelsForAccount(account)
|
||||||
ok = len(channelsAlreadyRegistered) < client.server.Config().Channels.Registration.MaxChannelsPerAccount || client.HasRoleCapabs("chanreg")
|
ok = len(channelsAlreadyRegistered) < client.server.Config().Channels.Registration.MaxChannelsPerAccount || client.HasRoleCapabs("chanreg")
|
||||||
if !ok {
|
if !ok {
|
||||||
service.Notice(rb, client.t("You have already registered the maximum number of channels; try dropping some with /CS UNREGISTER"))
|
service.Notice(rb, client.t("You have already registered the maximum number of channels; try dropping some with /CS UNREGISTER"))
|
||||||
@ -496,8 +496,8 @@ func csUnregisterHandler(service *ircService, server *Server, client *Client, co
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
info := channel.ExportRegistration(0)
|
info := channel.exportSummary()
|
||||||
channelKey := info.NameCasefolded
|
channelKey := channel.NameCasefolded()
|
||||||
if !csPrivsCheck(service, info, client, rb) {
|
if !csPrivsCheck(service, info, client, rb) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -519,7 +519,7 @@ func csClearHandler(service *ircService, server *Server, client *Client, command
|
|||||||
service.Notice(rb, client.t("Channel does not exist"))
|
service.Notice(rb, client.t("Channel does not exist"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !csPrivsCheck(service, channel.ExportRegistration(0), client, rb) {
|
if !csPrivsCheck(service, channel.exportSummary(), client, rb) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -550,7 +550,7 @@ func csTransferHandler(service *ircService, server *Server, client *Client, comm
|
|||||||
service.Notice(rb, client.t("Channel does not exist"))
|
service.Notice(rb, client.t("Channel does not exist"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
regInfo := channel.ExportRegistration(0)
|
regInfo := channel.exportSummary()
|
||||||
chname = regInfo.Name
|
chname = regInfo.Name
|
||||||
account := client.Account()
|
account := client.Account()
|
||||||
isFounder := account != "" && account == regInfo.Founder
|
isFounder := account != "" && account == regInfo.Founder
|
||||||
@ -729,11 +729,6 @@ func csPurgeListHandler(service *ircService, client *Client, rb *ResponseBuffer)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func csListHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
|
func csListHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
|
||||||
if !client.HasRoleCapabs("chanreg") {
|
|
||||||
service.Notice(rb, client.t("Insufficient privileges"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var searchRegex *regexp.Regexp
|
var searchRegex *regexp.Regexp
|
||||||
if len(params) > 0 {
|
if len(params) > 0 {
|
||||||
var err error
|
var err error
|
||||||
@ -746,7 +741,7 @@ func csListHandler(service *ircService, server *Server, client *Client, command
|
|||||||
|
|
||||||
service.Notice(rb, ircfmt.Unescape(client.t("*** $bChanServ LIST$b ***")))
|
service.Notice(rb, ircfmt.Unescape(client.t("*** $bChanServ LIST$b ***")))
|
||||||
|
|
||||||
channels := server.channelRegistry.AllChannels()
|
channels := server.channels.AllRegisteredChannels()
|
||||||
for _, channel := range channels {
|
for _, channel := range channels {
|
||||||
if searchRegex == nil || searchRegex.MatchString(channel) {
|
if searchRegex == nil || searchRegex.MatchString(channel) {
|
||||||
service.Notice(rb, fmt.Sprintf(" %s", channel))
|
service.Notice(rb, fmt.Sprintf(" %s", channel))
|
||||||
@ -771,7 +766,7 @@ func csInfoHandler(service *ircService, server *Server, client *Client, command
|
|||||||
|
|
||||||
// purge status
|
// purge status
|
||||||
if client.HasRoleCapabs("chanreg") {
|
if client.HasRoleCapabs("chanreg") {
|
||||||
purgeRecord, err := server.channelRegistry.LoadPurgeRecord(chname)
|
purgeRecord, err := server.channels.LoadPurgeRecord(chname)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
service.Notice(rb, fmt.Sprintf(client.t("Channel %s was purged by the server operators and cannot be used"), chname))
|
service.Notice(rb, fmt.Sprintf(client.t("Channel %s was purged by the server operators and cannot be used"), chname))
|
||||||
service.Notice(rb, fmt.Sprintf(client.t("Purged by operator: %s"), purgeRecord.Oper))
|
service.Notice(rb, fmt.Sprintf(client.t("Purged by operator: %s"), purgeRecord.Oper))
|
||||||
@ -789,13 +784,7 @@ func csInfoHandler(service *ircService, server *Server, client *Client, command
|
|||||||
var chinfo RegisteredChannel
|
var chinfo RegisteredChannel
|
||||||
channel := server.channels.Get(params[0])
|
channel := server.channels.Get(params[0])
|
||||||
if channel != nil {
|
if channel != nil {
|
||||||
chinfo = channel.ExportRegistration(0)
|
chinfo = channel.exportSummary()
|
||||||
} else {
|
|
||||||
chinfo, err = server.channelRegistry.LoadChannel(chname)
|
|
||||||
if err != nil && !(err == errNoSuchChannel || err == errFeatureDisabled) {
|
|
||||||
service.Notice(rb, client.t("An error occurred"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// channel exists but is unregistered, or doesn't exist:
|
// channel exists but is unregistered, or doesn't exist:
|
||||||
@ -835,12 +824,12 @@ func csGetHandler(service *ircService, server *Server, client *Client, command s
|
|||||||
service.Notice(rb, client.t("No such channel"))
|
service.Notice(rb, client.t("No such channel"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
info := channel.ExportRegistration(IncludeSettings)
|
info := channel.exportSummary()
|
||||||
if !csPrivsCheck(service, info, client, rb) {
|
if !csPrivsCheck(service, info, client, rb) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
displayChannelSetting(service, setting, info.Settings, client, rb)
|
displayChannelSetting(service, setting, channel.Settings(), client, rb)
|
||||||
}
|
}
|
||||||
|
|
||||||
func csSetHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
|
func csSetHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
|
||||||
@ -850,12 +839,12 @@ func csSetHandler(service *ircService, server *Server, client *Client, command s
|
|||||||
service.Notice(rb, client.t("No such channel"))
|
service.Notice(rb, client.t("No such channel"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
info := channel.ExportRegistration(IncludeSettings)
|
info := channel.exportSummary()
|
||||||
settings := info.Settings
|
|
||||||
if !csPrivsCheck(service, info, client, rb) {
|
if !csPrivsCheck(service, info, client, rb) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
settings := channel.Settings()
|
||||||
var err error
|
var err error
|
||||||
switch strings.ToLower(setting) {
|
switch strings.ToLower(setting) {
|
||||||
case "history":
|
case "history":
|
||||||
|
157
irc/database.go
157
irc/database.go
@ -14,6 +14,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/ergochat/ergo/irc/bunt"
|
||||||
|
"github.com/ergochat/ergo/irc/datastore"
|
||||||
"github.com/ergochat/ergo/irc/modes"
|
"github.com/ergochat/ergo/irc/modes"
|
||||||
"github.com/ergochat/ergo/irc/utils"
|
"github.com/ergochat/ergo/irc/utils"
|
||||||
|
|
||||||
@ -21,12 +23,19 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// 'version' of the database schema
|
// TODO migrate metadata keys as well
|
||||||
keySchemaVersion = "db.version"
|
|
||||||
// latest schema of the db
|
|
||||||
latestDbSchema = 22
|
|
||||||
|
|
||||||
keyCloakSecret = "crypto.cloak_secret"
|
// 'version' of the database schema
|
||||||
|
// latest schema of the db
|
||||||
|
latestDbSchema = 23
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
schemaVersionUUID = utils.UUID{0, 255, 85, 13, 212, 10, 191, 121, 245, 152, 142, 89, 97, 141, 219, 87} // AP9VDdQKv3n1mI5ZYY3bVw
|
||||||
|
cloakSecretUUID = utils.UUID{170, 214, 184, 208, 116, 181, 67, 75, 161, 23, 233, 16, 113, 251, 94, 229} // qta40HS1Q0uhF-kQcfte5Q
|
||||||
|
|
||||||
|
keySchemaVersion = bunt.BuntKey(datastore.TableMetadata, schemaVersionUUID)
|
||||||
|
keyCloakSecret = bunt.BuntKey(datastore.TableMetadata, cloakSecretUUID)
|
||||||
)
|
)
|
||||||
|
|
||||||
type SchemaChanger func(*Config, *buntdb.Tx) error
|
type SchemaChanger func(*Config, *buntdb.Tx) error
|
||||||
@ -99,10 +108,7 @@ func openDatabaseInternal(config *Config, allowAutoupgrade bool) (db *buntdb.DB,
|
|||||||
// read the current version string
|
// read the current version string
|
||||||
var version int
|
var version int
|
||||||
err = db.View(func(tx *buntdb.Tx) (err error) {
|
err = db.View(func(tx *buntdb.Tx) (err error) {
|
||||||
vStr, err := tx.Get(keySchemaVersion)
|
version, err = retrieveSchemaVersion(tx)
|
||||||
if err == nil {
|
|
||||||
version, err = strconv.Atoi(vStr)
|
|
||||||
}
|
|
||||||
return err
|
return err
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -130,6 +136,17 @@ func openDatabaseInternal(config *Config, allowAutoupgrade bool) (db *buntdb.DB,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func retrieveSchemaVersion(tx *buntdb.Tx) (version int, err error) {
|
||||||
|
if val, err := tx.Get(keySchemaVersion); err == nil {
|
||||||
|
return strconv.Atoi(val)
|
||||||
|
}
|
||||||
|
// legacy key:
|
||||||
|
if val, err := tx.Get("db.version"); err == nil {
|
||||||
|
return strconv.Atoi(val)
|
||||||
|
}
|
||||||
|
return 0, buntdb.ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
func performAutoUpgrade(currentVersion int, config *Config) (err error) {
|
func performAutoUpgrade(currentVersion int, config *Config) (err error) {
|
||||||
path := config.Datastore.Path
|
path := config.Datastore.Path
|
||||||
log.Printf("attempting to auto-upgrade schema from version %d to %d\n", currentVersion, latestDbSchema)
|
log.Printf("attempting to auto-upgrade schema from version %d to %d\n", currentVersion, latestDbSchema)
|
||||||
@ -167,8 +184,12 @@ func UpgradeDB(config *Config) (err error) {
|
|||||||
var version int
|
var version int
|
||||||
err = store.Update(func(tx *buntdb.Tx) error {
|
err = store.Update(func(tx *buntdb.Tx) error {
|
||||||
for {
|
for {
|
||||||
vStr, _ := tx.Get(keySchemaVersion)
|
if version == 0 {
|
||||||
version, _ = strconv.Atoi(vStr)
|
version, err = retrieveSchemaVersion(tx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
if version == latestDbSchema {
|
if version == latestDbSchema {
|
||||||
// success!
|
// success!
|
||||||
break
|
break
|
||||||
@ -183,11 +204,12 @@ func UpgradeDB(config *Config) (err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
_, _, err = tx.Set(keySchemaVersion, strconv.Itoa(change.TargetVersion), nil)
|
version = change.TargetVersion
|
||||||
|
_, _, err = tx.Set(keySchemaVersion, strconv.Itoa(version), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
log.Printf("successfully updated schema to version %d\n", change.TargetVersion)
|
log.Printf("successfully updated schema to version %d\n", version)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
@ -198,19 +220,17 @@ func UpgradeDB(config *Config) (err error) {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadCloakSecret(db *buntdb.DB) (result string) {
|
func LoadCloakSecret(dstore datastore.Datastore) (result string, err error) {
|
||||||
db.View(func(tx *buntdb.Tx) error {
|
val, err := dstore.Get(datastore.TableMetadata, cloakSecretUUID)
|
||||||
result, _ = tx.Get(keyCloakSecret)
|
if err != nil {
|
||||||
return nil
|
|
||||||
})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
return string(val), nil
|
||||||
|
}
|
||||||
|
|
||||||
func StoreCloakSecret(db *buntdb.DB, secret string) {
|
func StoreCloakSecret(dstore datastore.Datastore, secret string) {
|
||||||
db.Update(func(tx *buntdb.Tx) error {
|
// TODO error checking
|
||||||
tx.Set(keyCloakSecret, secret, nil)
|
dstore.Set(datastore.TableMetadata, cloakSecretUUID, []byte(secret), time.Time{})
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func schemaChangeV1toV2(config *Config, tx *buntdb.Tx) error {
|
func schemaChangeV1toV2(config *Config, tx *buntdb.Tx) error {
|
||||||
@ -1112,6 +1132,92 @@ func schemaChangeV21To22(config *Config, tx *buntdb.Tx) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// first phase of document-oriented database refactor: channels
|
||||||
|
func schemaChangeV22ToV23(config *Config, tx *buntdb.Tx) error {
|
||||||
|
keyChannelExists := "channel.exists "
|
||||||
|
var channelNames []string
|
||||||
|
tx.AscendGreaterOrEqual("", keyChannelExists, func(key, value string) bool {
|
||||||
|
if !strings.HasPrefix(key, keyChannelExists) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
channelNames = append(channelNames, strings.TrimPrefix(key, keyChannelExists))
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
for _, channelName := range channelNames {
|
||||||
|
channel, err := loadLegacyChannel(tx, channelName)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error loading legacy channel %s: %v", channelName, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
channel.UUID = utils.GenerateUUIDv4()
|
||||||
|
newKey := bunt.BuntKey(datastore.TableChannels, channel.UUID)
|
||||||
|
j, err := json.Marshal(channel)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error marshaling channel %s: %v", channelName, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
tx.Set(newKey, string(j), nil)
|
||||||
|
deleteLegacyChannel(tx, channelName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// purges
|
||||||
|
keyChannelPurged := "channel.purged "
|
||||||
|
var purgeKeys []string
|
||||||
|
var channelPurges []ChannelPurgeRecord
|
||||||
|
tx.AscendGreaterOrEqual("", keyChannelPurged, func(key, value string) bool {
|
||||||
|
if !strings.HasPrefix(key, keyChannelPurged) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
purgeKeys = append(purgeKeys, key)
|
||||||
|
cfname := strings.TrimPrefix(key, keyChannelPurged)
|
||||||
|
var record ChannelPurgeRecord
|
||||||
|
err := json.Unmarshal([]byte(value), &record)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error unmarshaling channel purge for %s: %v", cfname, err)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
record.NameCasefolded = cfname
|
||||||
|
record.UUID = utils.GenerateUUIDv4()
|
||||||
|
channelPurges = append(channelPurges, record)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
for _, record := range channelPurges {
|
||||||
|
newKey := bunt.BuntKey(datastore.TableChannelPurges, record.UUID)
|
||||||
|
j, err := json.Marshal(record)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error marshaling channel purge %s: %v", record.NameCasefolded, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
tx.Set(newKey, string(j), nil)
|
||||||
|
}
|
||||||
|
for _, purgeKey := range purgeKeys {
|
||||||
|
tx.Delete(purgeKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// clean up denormalized account-to-channels mapping
|
||||||
|
keyAccountChannels := "account.channels "
|
||||||
|
var accountToChannels []string
|
||||||
|
tx.AscendGreaterOrEqual("", keyAccountChannels, func(key, value string) bool {
|
||||||
|
if !strings.HasPrefix(key, keyAccountChannels) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
accountToChannels = append(accountToChannels, key)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
for _, key := range accountToChannels {
|
||||||
|
tx.Delete(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// migrate cloak secret
|
||||||
|
val, _ := tx.Get("crypto.cloak_secret")
|
||||||
|
tx.Set(keyCloakSecret, val, nil)
|
||||||
|
|
||||||
|
// bump the legacy version key to mark the database as downgrade-incompatible
|
||||||
|
tx.Set("db.version", "23", nil)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func getSchemaChange(initialVersion int) (result SchemaChange, ok bool) {
|
func getSchemaChange(initialVersion int) (result SchemaChange, ok bool) {
|
||||||
for _, change := range allChanges {
|
for _, change := range allChanges {
|
||||||
if initialVersion == change.InitialVersion {
|
if initialVersion == change.InitialVersion {
|
||||||
@ -1227,4 +1333,9 @@ var allChanges = []SchemaChange{
|
|||||||
TargetVersion: 22,
|
TargetVersion: 22,
|
||||||
Changer: schemaChangeV21To22,
|
Changer: schemaChangeV21To22,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
InitialVersion: 22,
|
||||||
|
TargetVersion: 23,
|
||||||
|
Changer: schemaChangeV22ToV23,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
45
irc/datastore/datastore.go
Normal file
45
irc/datastore/datastore.go
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
// Copyright (c) 2022 Shivaram Lingamneni <slingamn@cs.stanford.edu>
|
||||||
|
// released under the MIT license
|
||||||
|
|
||||||
|
package datastore
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ergochat/ergo/irc/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Table uint16
|
||||||
|
|
||||||
|
// XXX these are persisted and must remain stable;
|
||||||
|
// do not reorder, when deleting use _ to ensure that the deleted value is skipped
|
||||||
|
const (
|
||||||
|
TableMetadata Table = iota
|
||||||
|
TableChannels
|
||||||
|
TableChannelPurges
|
||||||
|
)
|
||||||
|
|
||||||
|
type KV struct {
|
||||||
|
UUID utils.UUID
|
||||||
|
Value []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// A Datastore provides the following abstraction:
|
||||||
|
// 1. Tables, each keyed on a UUID (the implementation is free to merge
|
||||||
|
// the table name and the UUID into a single key as long as the rest of
|
||||||
|
// the contract can be satisfied). Table names are [a-z0-9_]+
|
||||||
|
// 2. The ability to efficiently enumerate all uuid-value pairs in a table
|
||||||
|
// 3. Gets, sets, and deletes for individual (table, uuid) keys
|
||||||
|
type Datastore interface {
|
||||||
|
Backoff() time.Duration
|
||||||
|
|
||||||
|
GetAll(table Table) ([]KV, error)
|
||||||
|
|
||||||
|
// This is rarely used because it would typically lead to TOCTOU races
|
||||||
|
Get(table Table, key utils.UUID) (value []byte, err error)
|
||||||
|
|
||||||
|
Set(table Table, key utils.UUID, value []byte, expiration time.Time) error
|
||||||
|
|
||||||
|
// Note that deleting a nonexistent key is not considered an error
|
||||||
|
Delete(table Table, key utils.UUID) error
|
||||||
|
}
|
@ -51,6 +51,7 @@ var (
|
|||||||
errNoExistingBan = errors.New("Ban does not exist")
|
errNoExistingBan = errors.New("Ban does not exist")
|
||||||
errNoSuchChannel = errors.New(`No such channel`)
|
errNoSuchChannel = errors.New(`No such channel`)
|
||||||
errChannelPurged = errors.New(`This channel was purged by the server operators and cannot be used`)
|
errChannelPurged = errors.New(`This channel was purged by the server operators and cannot be used`)
|
||||||
|
errChannelPurgedAlready = errors.New(`This channel was already purged and cannot be purged again`)
|
||||||
errConfusableIdentifier = errors.New("This identifier is confusable with one already in use")
|
errConfusableIdentifier = errors.New("This identifier is confusable with one already in use")
|
||||||
errInsufficientPrivs = errors.New("Insufficient privileges")
|
errInsufficientPrivs = errors.New("Insufficient privileges")
|
||||||
errInvalidUsername = errors.New("Invalid username")
|
errInvalidUsername = errors.New("Invalid username")
|
||||||
|
@ -638,3 +638,9 @@ func (channel *Channel) getAmode(cfaccount string) (result modes.Mode) {
|
|||||||
defer channel.stateMutex.RUnlock()
|
defer channel.stateMutex.RUnlock()
|
||||||
return channel.accountToUMode[cfaccount]
|
return channel.accountToUMode[cfaccount]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (channel *Channel) UUID() utils.UUID {
|
||||||
|
channel.stateMutex.RLock()
|
||||||
|
defer channel.stateMutex.RUnlock()
|
||||||
|
return channel.uuid
|
||||||
|
}
|
||||||
|
@ -1719,7 +1719,7 @@ func listHandler(server *Server, client *Client, msg ircmsg.Message, rb *Respons
|
|||||||
|
|
||||||
clientIsOp := client.HasRoleCapabs("sajoin")
|
clientIsOp := client.HasRoleCapabs("sajoin")
|
||||||
if len(channels) == 0 {
|
if len(channels) == 0 {
|
||||||
for _, channel := range server.channels.Channels() {
|
for _, channel := range server.channels.ListableChannels() {
|
||||||
if !clientIsOp && channel.flags.HasMode(modes.Secret) && !channel.hasClient(client) {
|
if !clientIsOp && channel.flags.HasMode(modes.Secret) && !channel.hasClient(client) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -193,6 +193,6 @@ func hsSetCloakSecretHandler(service *ircService, server *Server, client *Client
|
|||||||
service.Notice(rb, fmt.Sprintf(client.t("To confirm, run this command: %s"), fmt.Sprintf("/HS SETCLOAKSECRET %s %s", secret, expectedCode)))
|
service.Notice(rb, fmt.Sprintf(client.t("To confirm, run this command: %s"), fmt.Sprintf("/HS SETCLOAKSECRET %s %s", secret, expectedCode)))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
StoreCloakSecret(server.store, secret)
|
StoreCloakSecret(server.dstore, secret)
|
||||||
service.Notice(rb, client.t("Rotated the cloak secret; you must rehash or restart the server for it to take effect"))
|
service.Notice(rb, client.t("Rotated the cloak secret; you must rehash or restart the server for it to take effect"))
|
||||||
}
|
}
|
||||||
|
@ -9,9 +9,13 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/tidwall/buntdb"
|
"github.com/tidwall/buntdb"
|
||||||
|
|
||||||
|
"github.com/ergochat/ergo/irc/bunt"
|
||||||
|
"github.com/ergochat/ergo/irc/datastore"
|
||||||
|
"github.com/ergochat/ergo/irc/modes"
|
||||||
"github.com/ergochat/ergo/irc/utils"
|
"github.com/ergochat/ergo/irc/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -20,7 +24,7 @@ const (
|
|||||||
// XXX instead of referencing, e.g., keyAccountExists, we should write in the string literal
|
// XXX instead of referencing, e.g., keyAccountExists, we should write in the string literal
|
||||||
// (to ensure that no matter what code changes happen elsewhere, we're still producing a
|
// (to ensure that no matter what code changes happen elsewhere, we're still producing a
|
||||||
// db of the hardcoded version)
|
// db of the hardcoded version)
|
||||||
importDBSchemaVersion = 22
|
importDBSchemaVersion = 23
|
||||||
)
|
)
|
||||||
|
|
||||||
type userImport struct {
|
type userImport struct {
|
||||||
@ -54,8 +58,8 @@ type databaseImport struct {
|
|||||||
Channels map[string]channelImport
|
Channels map[string]channelImport
|
||||||
}
|
}
|
||||||
|
|
||||||
func serializeAmodes(raw map[string]string, validCfUsernames utils.HashSet[string]) (result []byte, err error) {
|
func convertAmodes(raw map[string]string, validCfUsernames utils.HashSet[string]) (result map[string]modes.Mode, err error) {
|
||||||
processed := make(map[string]int, len(raw))
|
result = make(map[string]modes.Mode)
|
||||||
for accountName, mode := range raw {
|
for accountName, mode := range raw {
|
||||||
if len(mode) != 1 {
|
if len(mode) != 1 {
|
||||||
return nil, fmt.Errorf("invalid mode %s for account %s", mode, accountName)
|
return nil, fmt.Errorf("invalid mode %s for account %s", mode, accountName)
|
||||||
@ -64,10 +68,9 @@ func serializeAmodes(raw map[string]string, validCfUsernames utils.HashSet[strin
|
|||||||
if err != nil || !validCfUsernames.Has(cfname) {
|
if err != nil || !validCfUsernames.Has(cfname) {
|
||||||
log.Printf("skipping invalid amode recipient %s\n", accountName)
|
log.Printf("skipping invalid amode recipient %s\n", accountName)
|
||||||
} else {
|
} else {
|
||||||
processed[cfname] = int(mode[0])
|
result[cfname] = modes.Mode(mode[0])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
result, err = json.Marshal(processed)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -147,8 +150,9 @@ func doImportDBGeneric(config *Config, dbImport databaseImport, credsType Creden
|
|||||||
cfUsernames.Add(cfUsername)
|
cfUsernames.Add(cfUsername)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO fix this:
|
||||||
for chname, chInfo := range dbImport.Channels {
|
for chname, chInfo := range dbImport.Channels {
|
||||||
cfchname, err := CasefoldChannel(chname)
|
_, err := CasefoldChannel(chname)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("invalid channel name %s: %v", chname, err)
|
log.Printf("invalid channel name %s: %v", chname, err)
|
||||||
continue
|
continue
|
||||||
@ -158,43 +162,42 @@ func doImportDBGeneric(config *Config, dbImport databaseImport, credsType Creden
|
|||||||
log.Printf("invalid founder %s for channel %s: %v", chInfo.Founder, chname, err)
|
log.Printf("invalid founder %s for channel %s: %v", chInfo.Founder, chname, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
tx.Set(fmt.Sprintf(keyChannelExists, cfchname), "1", nil)
|
var regInfo RegisteredChannel
|
||||||
tx.Set(fmt.Sprintf(keyChannelName, cfchname), chname, nil)
|
regInfo.Name = chname
|
||||||
tx.Set(fmt.Sprintf(keyChannelRegTime, cfchname), strconv.FormatInt(chInfo.RegisteredAt, 10), nil)
|
regInfo.UUID = utils.GenerateUUIDv4()
|
||||||
tx.Set(fmt.Sprintf(keyChannelFounder, cfchname), cffounder, nil)
|
regInfo.Founder = cffounder
|
||||||
accountChannelsKey := fmt.Sprintf(keyAccountChannels, cffounder)
|
regInfo.RegisteredAt = time.Unix(0, chInfo.RegisteredAt).UTC()
|
||||||
founderChannels, fcErr := tx.Get(accountChannelsKey)
|
|
||||||
if fcErr != nil || founderChannels == "" {
|
|
||||||
founderChannels = cfchname
|
|
||||||
} else {
|
|
||||||
founderChannels = fmt.Sprintf("%s,%s", founderChannels, cfchname)
|
|
||||||
}
|
|
||||||
tx.Set(accountChannelsKey, founderChannels, nil)
|
|
||||||
if chInfo.Topic != "" {
|
if chInfo.Topic != "" {
|
||||||
tx.Set(fmt.Sprintf(keyChannelTopic, cfchname), chInfo.Topic, nil)
|
regInfo.Topic = chInfo.Topic
|
||||||
tx.Set(fmt.Sprintf(keyChannelTopicSetTime, cfchname), strconv.FormatInt(chInfo.TopicSetAt, 10), nil)
|
regInfo.TopicSetBy = chInfo.TopicSetBy
|
||||||
tx.Set(fmt.Sprintf(keyChannelTopicSetBy, cfchname), chInfo.TopicSetBy, nil)
|
regInfo.TopicSetTime = time.Unix(0, chInfo.TopicSetAt).UTC()
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(chInfo.Amode) != 0 {
|
if len(chInfo.Amode) != 0 {
|
||||||
m, err := serializeAmodes(chInfo.Amode, cfUsernames)
|
m, err := convertAmodes(chInfo.Amode, cfUsernames)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
tx.Set(fmt.Sprintf(keyChannelAccountToUMode, cfchname), string(m), nil)
|
regInfo.AccountToUMode = m
|
||||||
} else {
|
} else {
|
||||||
log.Printf("couldn't serialize amodes for %s: %v", chname, err)
|
log.Printf("couldn't process amodes for %s: %v", chname, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tx.Set(fmt.Sprintf(keyChannelModes, cfchname), chInfo.Modes, nil)
|
for _, mode := range chInfo.Modes {
|
||||||
if chInfo.Key != "" {
|
regInfo.Modes = append(regInfo.Modes, modes.Mode(mode))
|
||||||
tx.Set(fmt.Sprintf(keyChannelPassword, cfchname), chInfo.Key, nil)
|
|
||||||
}
|
}
|
||||||
|
regInfo.Key = chInfo.Key
|
||||||
if chInfo.Limit > 0 {
|
if chInfo.Limit > 0 {
|
||||||
tx.Set(fmt.Sprintf(keyChannelUserLimit, cfchname), strconv.Itoa(chInfo.Limit), nil)
|
regInfo.UserLimit = chInfo.Limit
|
||||||
}
|
}
|
||||||
if chInfo.Forward != "" {
|
if chInfo.Forward != "" {
|
||||||
if _, err := CasefoldChannel(chInfo.Forward); err == nil {
|
if _, err := CasefoldChannel(chInfo.Forward); err == nil {
|
||||||
tx.Set(fmt.Sprintf(keyChannelForward, cfchname), chInfo.Forward, nil)
|
regInfo.Forward = chInfo.Forward
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if j, err := json.Marshal(regInfo); err == nil {
|
||||||
|
tx.Set(bunt.BuntKey(datastore.TableChannels, regInfo.UUID), string(j), nil)
|
||||||
|
} else {
|
||||||
|
log.Printf("couldn't serialize channel %s: %v", chname, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if warnSkeletons {
|
if warnSkeletons {
|
||||||
|
121
irc/legacy.go
121
irc/legacy.go
@ -4,7 +4,15 @@ package irc
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/tidwall/buntdb"
|
||||||
|
|
||||||
|
"github.com/ergochat/ergo/irc/modes"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -25,3 +33,116 @@ func decodeLegacyPasswordHash(hash string) ([]byte, error) {
|
|||||||
return nil, errInvalidPasswordHash
|
return nil, errInvalidPasswordHash
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// legacy channel registration code
|
||||||
|
|
||||||
|
const (
|
||||||
|
keyChannelExists = "channel.exists %s"
|
||||||
|
keyChannelName = "channel.name %s" // stores the 'preferred name' of the channel, not casemapped
|
||||||
|
keyChannelRegTime = "channel.registered.time %s"
|
||||||
|
keyChannelFounder = "channel.founder %s"
|
||||||
|
keyChannelTopic = "channel.topic %s"
|
||||||
|
keyChannelTopicSetBy = "channel.topic.setby %s"
|
||||||
|
keyChannelTopicSetTime = "channel.topic.settime %s"
|
||||||
|
keyChannelBanlist = "channel.banlist %s"
|
||||||
|
keyChannelExceptlist = "channel.exceptlist %s"
|
||||||
|
keyChannelInvitelist = "channel.invitelist %s"
|
||||||
|
keyChannelPassword = "channel.key %s"
|
||||||
|
keyChannelModes = "channel.modes %s"
|
||||||
|
keyChannelAccountToUMode = "channel.accounttoumode %s"
|
||||||
|
keyChannelUserLimit = "channel.userlimit %s"
|
||||||
|
keyChannelSettings = "channel.settings %s"
|
||||||
|
keyChannelForward = "channel.forward %s"
|
||||||
|
|
||||||
|
keyChannelPurged = "channel.purged %s"
|
||||||
|
)
|
||||||
|
|
||||||
|
func deleteLegacyChannel(tx *buntdb.Tx, nameCasefolded string) {
|
||||||
|
tx.Delete(fmt.Sprintf(keyChannelExists, nameCasefolded))
|
||||||
|
tx.Delete(fmt.Sprintf(keyChannelName, nameCasefolded))
|
||||||
|
tx.Delete(fmt.Sprintf(keyChannelRegTime, nameCasefolded))
|
||||||
|
tx.Delete(fmt.Sprintf(keyChannelFounder, nameCasefolded))
|
||||||
|
tx.Delete(fmt.Sprintf(keyChannelTopic, nameCasefolded))
|
||||||
|
tx.Delete(fmt.Sprintf(keyChannelTopicSetBy, nameCasefolded))
|
||||||
|
tx.Delete(fmt.Sprintf(keyChannelTopicSetTime, nameCasefolded))
|
||||||
|
tx.Delete(fmt.Sprintf(keyChannelBanlist, nameCasefolded))
|
||||||
|
tx.Delete(fmt.Sprintf(keyChannelExceptlist, nameCasefolded))
|
||||||
|
tx.Delete(fmt.Sprintf(keyChannelInvitelist, nameCasefolded))
|
||||||
|
tx.Delete(fmt.Sprintf(keyChannelPassword, nameCasefolded))
|
||||||
|
tx.Delete(fmt.Sprintf(keyChannelModes, nameCasefolded))
|
||||||
|
tx.Delete(fmt.Sprintf(keyChannelAccountToUMode, nameCasefolded))
|
||||||
|
tx.Delete(fmt.Sprintf(keyChannelUserLimit, nameCasefolded))
|
||||||
|
tx.Delete(fmt.Sprintf(keyChannelSettings, nameCasefolded))
|
||||||
|
tx.Delete(fmt.Sprintf(keyChannelForward, nameCasefolded))
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadLegacyChannel(tx *buntdb.Tx, nameCasefolded string) (info RegisteredChannel, err error) {
|
||||||
|
channelKey := nameCasefolded
|
||||||
|
// nice to have: do all JSON (de)serialization outside of the buntdb transaction
|
||||||
|
_, dberr := tx.Get(fmt.Sprintf(keyChannelExists, channelKey))
|
||||||
|
if dberr == buntdb.ErrNotFound {
|
||||||
|
// chan does not already exist, return
|
||||||
|
err = errNoSuchChannel
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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))
|
||||||
|
var topicSetTime time.Time
|
||||||
|
topicSetTimeStr, _ := tx.Get(fmt.Sprintf(keyChannelTopicSetTime, channelKey))
|
||||||
|
if topicSetTimeInt, topicSetTimeErr := strconv.ParseInt(topicSetTimeStr, 10, 64); topicSetTimeErr == nil {
|
||||||
|
topicSetTime = time.Unix(0, topicSetTimeInt).UTC()
|
||||||
|
}
|
||||||
|
password, _ := tx.Get(fmt.Sprintf(keyChannelPassword, channelKey))
|
||||||
|
modeString, _ := tx.Get(fmt.Sprintf(keyChannelModes, channelKey))
|
||||||
|
userLimitString, _ := tx.Get(fmt.Sprintf(keyChannelUserLimit, channelKey))
|
||||||
|
forward, _ := tx.Get(fmt.Sprintf(keyChannelForward, channelKey))
|
||||||
|
banlistString, _ := tx.Get(fmt.Sprintf(keyChannelBanlist, channelKey))
|
||||||
|
exceptlistString, _ := tx.Get(fmt.Sprintf(keyChannelExceptlist, channelKey))
|
||||||
|
invitelistString, _ := tx.Get(fmt.Sprintf(keyChannelInvitelist, channelKey))
|
||||||
|
accountToUModeString, _ := tx.Get(fmt.Sprintf(keyChannelAccountToUMode, channelKey))
|
||||||
|
settingsString, _ := tx.Get(fmt.Sprintf(keyChannelSettings, channelKey))
|
||||||
|
|
||||||
|
modeSlice := make([]modes.Mode, len(modeString))
|
||||||
|
for i, mode := range modeString {
|
||||||
|
modeSlice[i] = modes.Mode(mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
userLimit, _ := strconv.Atoi(userLimitString)
|
||||||
|
|
||||||
|
var banlist map[string]MaskInfo
|
||||||
|
_ = json.Unmarshal([]byte(banlistString), &banlist)
|
||||||
|
var exceptlist map[string]MaskInfo
|
||||||
|
_ = json.Unmarshal([]byte(exceptlistString), &exceptlist)
|
||||||
|
var invitelist map[string]MaskInfo
|
||||||
|
_ = json.Unmarshal([]byte(invitelistString), &invitelist)
|
||||||
|
accountToUMode := make(map[string]modes.Mode)
|
||||||
|
_ = json.Unmarshal([]byte(accountToUModeString), &accountToUMode)
|
||||||
|
|
||||||
|
var settings ChannelSettings
|
||||||
|
_ = json.Unmarshal([]byte(settingsString), &settings)
|
||||||
|
|
||||||
|
info = RegisteredChannel{
|
||||||
|
Name: name,
|
||||||
|
RegisteredAt: time.Unix(0, regTimeInt).UTC(),
|
||||||
|
Founder: founder,
|
||||||
|
Topic: topic,
|
||||||
|
TopicSetBy: topicSetBy,
|
||||||
|
TopicSetTime: topicSetTime,
|
||||||
|
Key: password,
|
||||||
|
Modes: modeSlice,
|
||||||
|
Bans: banlist,
|
||||||
|
Excepts: exceptlist,
|
||||||
|
Invites: invitelist,
|
||||||
|
AccountToUMode: accountToUMode,
|
||||||
|
UserLimit: int(userLimit),
|
||||||
|
Settings: settings,
|
||||||
|
Forward: forward,
|
||||||
|
}
|
||||||
|
return info, nil
|
||||||
|
}
|
||||||
|
@ -954,9 +954,9 @@ func nsInfoHandler(service *ircService, server *Server, client *Client, command
|
|||||||
|
|
||||||
func listRegisteredChannels(service *ircService, accountName string, rb *ResponseBuffer) {
|
func listRegisteredChannels(service *ircService, accountName string, rb *ResponseBuffer) {
|
||||||
client := rb.session.client
|
client := rb.session.client
|
||||||
channels := client.server.accounts.ChannelsForAccount(accountName)
|
channels := client.server.channels.ChannelsForAccount(accountName)
|
||||||
service.Notice(rb, fmt.Sprintf(client.t("Account %s has %d registered channel(s)."), accountName, len(channels)))
|
service.Notice(rb, fmt.Sprintf(client.t("Account %s has %d registered channel(s)."), accountName, len(channels)))
|
||||||
for _, channel := range rb.session.client.server.accounts.ChannelsForAccount(accountName) {
|
for _, channel := range channels {
|
||||||
service.Notice(rb, fmt.Sprintf(client.t("Registered channel: %s"), channel))
|
service.Notice(rb, fmt.Sprintf(client.t("Registered channel: %s"), channel))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
37
irc/serde.go
Normal file
37
irc/serde.go
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
// Copyright (c) 2022 Shivaram Lingamneni
|
||||||
|
// released under the MIT license
|
||||||
|
|
||||||
|
package irc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/ergochat/ergo/irc/datastore"
|
||||||
|
"github.com/ergochat/ergo/irc/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Serializable interface {
|
||||||
|
Serialize() ([]byte, error)
|
||||||
|
Deserialize([]byte) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func FetchAndDeserializeAll[T any, C interface {
|
||||||
|
*T
|
||||||
|
Serializable
|
||||||
|
}](table datastore.Table, dstore datastore.Datastore, log *logger.Manager) (result []T, err error) {
|
||||||
|
rawRecords, err := dstore.GetAll(table)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
result = make([]T, len(rawRecords))
|
||||||
|
pos := 0
|
||||||
|
for _, record := range rawRecords {
|
||||||
|
err := C(&result[pos]).Deserialize(record.Value)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("internal", "deserialization error", strconv.Itoa(int(table)), record.UUID.String(), err.Error())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
pos++
|
||||||
|
}
|
||||||
|
return result[:pos], nil
|
||||||
|
}
|
@ -22,9 +22,12 @@ import (
|
|||||||
|
|
||||||
"github.com/ergochat/irc-go/ircfmt"
|
"github.com/ergochat/irc-go/ircfmt"
|
||||||
"github.com/okzk/sdnotify"
|
"github.com/okzk/sdnotify"
|
||||||
|
"github.com/tidwall/buntdb"
|
||||||
|
|
||||||
|
"github.com/ergochat/ergo/irc/bunt"
|
||||||
"github.com/ergochat/ergo/irc/caps"
|
"github.com/ergochat/ergo/irc/caps"
|
||||||
"github.com/ergochat/ergo/irc/connection_limits"
|
"github.com/ergochat/ergo/irc/connection_limits"
|
||||||
|
"github.com/ergochat/ergo/irc/datastore"
|
||||||
"github.com/ergochat/ergo/irc/flatip"
|
"github.com/ergochat/ergo/irc/flatip"
|
||||||
"github.com/ergochat/ergo/irc/flock"
|
"github.com/ergochat/ergo/irc/flock"
|
||||||
"github.com/ergochat/ergo/irc/history"
|
"github.com/ergochat/ergo/irc/history"
|
||||||
@ -33,7 +36,6 @@ import (
|
|||||||
"github.com/ergochat/ergo/irc/mysql"
|
"github.com/ergochat/ergo/irc/mysql"
|
||||||
"github.com/ergochat/ergo/irc/sno"
|
"github.com/ergochat/ergo/irc/sno"
|
||||||
"github.com/ergochat/ergo/irc/utils"
|
"github.com/ergochat/ergo/irc/utils"
|
||||||
"github.com/tidwall/buntdb"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -66,7 +68,6 @@ type Server struct {
|
|||||||
accepts AcceptManager
|
accepts AcceptManager
|
||||||
accounts AccountManager
|
accounts AccountManager
|
||||||
channels ChannelManager
|
channels ChannelManager
|
||||||
channelRegistry ChannelRegistry
|
|
||||||
clients ClientManager
|
clients ClientManager
|
||||||
config atomic.Pointer[Config]
|
config atomic.Pointer[Config]
|
||||||
configFilename string
|
configFilename string
|
||||||
@ -87,6 +88,7 @@ type Server struct {
|
|||||||
tracebackSignal chan os.Signal
|
tracebackSignal chan os.Signal
|
||||||
snomasks SnoManager
|
snomasks SnoManager
|
||||||
store *buntdb.DB
|
store *buntdb.DB
|
||||||
|
dstore datastore.Datastore
|
||||||
historyDB mysql.MySQL
|
historyDB mysql.MySQL
|
||||||
torLimiter connection_limits.TorLimiter
|
torLimiter connection_limits.TorLimiter
|
||||||
whoWas WhoWasList
|
whoWas WhoWasList
|
||||||
@ -98,6 +100,10 @@ type Server struct {
|
|||||||
|
|
||||||
// NewServer returns a new Oragono server.
|
// NewServer returns a new Oragono server.
|
||||||
func NewServer(config *Config, logger *logger.Manager) (*Server, error) {
|
func NewServer(config *Config, logger *logger.Manager) (*Server, error) {
|
||||||
|
// sanity check that kernel randomness is available; on modern Linux,
|
||||||
|
// this will block until it is, on other platforms it may panic:
|
||||||
|
utils.GenerateUUIDv4()
|
||||||
|
|
||||||
// initialize data structures
|
// initialize data structures
|
||||||
server := &Server{
|
server := &Server{
|
||||||
ctime: time.Now().UTC(),
|
ctime: time.Now().UTC(),
|
||||||
@ -716,7 +722,11 @@ func (server *Server) applyConfig(config *Config) (err error) {
|
|||||||
// now that the datastore is initialized, we can load the cloak secret from it
|
// now that the datastore is initialized, we can load the cloak secret from it
|
||||||
// XXX this modifies config after the initial load, which is naughty,
|
// XXX this modifies config after the initial load, which is naughty,
|
||||||
// but there's no data race because we haven't done SetConfig yet
|
// but there's no data race because we haven't done SetConfig yet
|
||||||
config.Server.Cloaks.SetSecret(LoadCloakSecret(server.store))
|
cloakSecret, err := LoadCloakSecret(server.dstore)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Could not load cloak secret: %w", err)
|
||||||
|
}
|
||||||
|
config.Server.Cloaks.SetSecret(cloakSecret)
|
||||||
|
|
||||||
// activate the new config
|
// activate the new config
|
||||||
server.config.Store(config)
|
server.config.Store(config)
|
||||||
@ -837,6 +847,7 @@ func (server *Server) loadDatastore(config *Config) error {
|
|||||||
db, err := OpenDatabase(config)
|
db, err := OpenDatabase(config)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
server.store = db
|
server.store = db
|
||||||
|
server.dstore = bunt.NewBuntdbDatastore(db, server.logger)
|
||||||
return nil
|
return nil
|
||||||
} else {
|
} else {
|
||||||
return fmt.Errorf("Failed to open datastore: %s", err.Error())
|
return fmt.Errorf("Failed to open datastore: %s", err.Error())
|
||||||
@ -849,8 +860,7 @@ func (server *Server) loadFromDatastore(config *Config) (err error) {
|
|||||||
server.loadDLines()
|
server.loadDLines()
|
||||||
server.loadKLines()
|
server.loadKLines()
|
||||||
|
|
||||||
server.channelRegistry.Initialize(server)
|
server.channels.Initialize(server, config)
|
||||||
server.channels.Initialize(server)
|
|
||||||
server.accounts.Initialize(server)
|
server.accounts.Initialize(server)
|
||||||
|
|
||||||
if config.Datastore.MySQL.Enabled {
|
if config.Datastore.MySQL.Enabled {
|
||||||
|
56
irc/utils/uuid.go
Normal file
56
irc/utils/uuid.go
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
// Copyright (c) 2022 Shivaram Lingamneni <slingamn@cs.stanford.edu>
|
||||||
|
// released under the MIT license
|
||||||
|
|
||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrInvalidUUID = errors.New("Invalid uuid")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Technically a UUIDv4 has version bits set, but this doesn't matter in practice
|
||||||
|
type UUID [16]byte
|
||||||
|
|
||||||
|
func (u UUID) MarshalJSON() (b []byte, err error) {
|
||||||
|
b = make([]byte, 24)
|
||||||
|
b[0] = '"'
|
||||||
|
base64.RawURLEncoding.Encode(b[1:], u[:])
|
||||||
|
b[23] = '"'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UUID) UnmarshalJSON(b []byte) (err error) {
|
||||||
|
if len(b) != 24 {
|
||||||
|
return ErrInvalidUUID
|
||||||
|
}
|
||||||
|
readLen, err := base64.RawURLEncoding.Decode(u[:], b[1:23])
|
||||||
|
if readLen != 16 {
|
||||||
|
return ErrInvalidUUID
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UUID) String() string {
|
||||||
|
return base64.RawURLEncoding.EncodeToString(u[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenerateUUIDv4() (result UUID) {
|
||||||
|
_, err := rand.Read(result[:])
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func DecodeUUID(ustr string) (result UUID, err error) {
|
||||||
|
length, err := base64.RawURLEncoding.Decode(result[:], []byte(ustr))
|
||||||
|
if err == nil && length != 16 {
|
||||||
|
err = ErrInvalidUUID
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user