mirror of
https://github.com/ergochat/ergo.git
synced 2024-11-22 11:59:40 +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"
|
||||
keyAccountVHost = "account.vhost %s"
|
||||
keyCertToAccount = "account.creds.certfp %s"
|
||||
keyAccountChannels = "account.channels %s" // channels registered to the account
|
||||
keyAccountLastSeen = "account.lastseen %s"
|
||||
keyAccountReadMarkers = "account.readmarkers %s"
|
||||
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)
|
||||
settingsKey := fmt.Sprintf(keyAccountSettings, casefoldedAccount)
|
||||
vhostKey := fmt.Sprintf(keyAccountVHost, casefoldedAccount)
|
||||
channelsKey := fmt.Sprintf(keyAccountChannels, casefoldedAccount)
|
||||
joinedChannelsKey := fmt.Sprintf(keyAccountChannelToModes, casefoldedAccount)
|
||||
lastSeenKey := fmt.Sprintf(keyAccountLastSeen, casefoldedAccount)
|
||||
readMarkersKey := fmt.Sprintf(keyAccountReadMarkers, casefoldedAccount)
|
||||
@ -1781,10 +1779,9 @@ func (am *AccountManager) Unregister(account string, erase bool) error {
|
||||
am.killClients(clients)
|
||||
}()
|
||||
|
||||
var registeredChannels []string
|
||||
// on our way out, unregister all the account's channels and delete them from the db
|
||||
defer func() {
|
||||
for _, channelName := range registeredChannels {
|
||||
for _, channelName := range am.server.channels.ChannelsForAccount(casefoldedAccount) {
|
||||
err := am.server.channels.SetUnregistered(channelName, casefoldedAccount)
|
||||
if err != nil {
|
||||
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()
|
||||
|
||||
var accountName string
|
||||
var channelsStr string
|
||||
keepProtections := false
|
||||
am.server.store.Update(func(tx *buntdb.Tx) error {
|
||||
// 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)
|
||||
tx.Delete(credentialsKey)
|
||||
tx.Delete(vhostKey)
|
||||
channelsStr, _ = tx.Get(channelsKey)
|
||||
tx.Delete(channelsKey)
|
||||
tx.Delete(joinedChannelsKey)
|
||||
tx.Delete(lastSeenKey)
|
||||
tx.Delete(readMarkersKey)
|
||||
@ -1858,7 +1852,6 @@ func (am *AccountManager) Unregister(account string, erase bool) error {
|
||||
|
||||
skeleton, _ := Skeleton(accountName)
|
||||
additionalNicks := unmarshalReservedNicks(rawNicks)
|
||||
registeredChannels = unmarshalRegisteredChannels(channelsStr)
|
||||
|
||||
am.Lock()
|
||||
defer am.Unlock()
|
||||
@ -1890,21 +1883,6 @@ func unmarshalRegisteredChannels(channelsStr string) (result []string) {
|
||||
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) {
|
||||
if certfp == "" {
|
||||
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
|
||||
}
|
||||
}
|
100
irc/channel.go
100
irc/channel.go
@ -16,6 +16,7 @@ import (
|
||||
"github.com/ergochat/irc-go/ircutils"
|
||||
|
||||
"github.com/ergochat/ergo/irc/caps"
|
||||
"github.com/ergochat/ergo/irc/datastore"
|
||||
"github.com/ergochat/ergo/irc/history"
|
||||
"github.com/ergochat/ergo/irc/modes"
|
||||
"github.com/ergochat/ergo/irc/utils"
|
||||
@ -50,14 +51,14 @@ type Channel struct {
|
||||
stateMutex sync.RWMutex // tier 1
|
||||
writebackLock sync.Mutex // tier 1.5
|
||||
joinPartMutex sync.Mutex // tier 3
|
||||
ensureLoaded utils.Once // manages loading stored registration info from the database
|
||||
dirtyBits uint
|
||||
settings ChannelSettings
|
||||
uuid utils.UUID
|
||||
}
|
||||
|
||||
// NewChannel creates a new channel from a `Server` and a `name`
|
||||
// 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()
|
||||
|
||||
channel := &Channel{
|
||||
@ -71,14 +72,15 @@ func NewChannel(s *Server, name, casefoldedName string, registered bool) *Channe
|
||||
channel.initializeLists()
|
||||
channel.history.Initialize(0, 0)
|
||||
|
||||
if !registered {
|
||||
if registered {
|
||||
channel.applyRegInfo(regInfo)
|
||||
} else {
|
||||
channel.resizeHistory(config)
|
||||
for _, mode := range config.Channels.defaultModes {
|
||||
channel.flags.SetMode(mode, true)
|
||||
}
|
||||
// no loading to do, so "mark" the load operation as "done":
|
||||
channel.ensureLoaded.Do(func() {})
|
||||
} // else: modes will be loaded before first join
|
||||
channel.uuid = utils.GenerateUUIDv4()
|
||||
}
|
||||
|
||||
return channel
|
||||
}
|
||||
@ -92,24 +94,6 @@ func (channel *Channel) initializeLists() {
|
||||
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) {
|
||||
status, _, _ := channel.historyStatus(config)
|
||||
if status == HistoryEphemeral {
|
||||
@ -126,6 +110,7 @@ func (channel *Channel) applyRegInfo(chanReg RegisteredChannel) {
|
||||
channel.stateMutex.Lock()
|
||||
defer channel.stateMutex.Unlock()
|
||||
|
||||
channel.uuid = chanReg.UUID
|
||||
channel.registeredFounder = chanReg.Founder
|
||||
channel.registeredTime = chanReg.RegisteredAt
|
||||
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
|
||||
func (channel *Channel) ExportRegistration(includeFlags uint) (info RegisteredChannel) {
|
||||
func (channel *Channel) ExportRegistration() (info RegisteredChannel) {
|
||||
channel.stateMutex.RLock()
|
||||
defer channel.stateMutex.RUnlock()
|
||||
|
||||
info.Name = channel.name
|
||||
info.NameCasefolded = channel.nameCasefolded
|
||||
info.UUID = channel.uuid
|
||||
info.Founder = channel.registeredFounder
|
||||
info.RegisteredAt = channel.registeredTime
|
||||
|
||||
if includeFlags&IncludeTopic != 0 {
|
||||
info.Topic = channel.topic
|
||||
info.TopicSetBy = channel.topicSetBy
|
||||
info.TopicSetTime = channel.topicSetTime
|
||||
}
|
||||
info.Topic = channel.topic
|
||||
info.TopicSetBy = channel.topicSetBy
|
||||
info.TopicSetTime = channel.topicSetTime
|
||||
|
||||
if includeFlags&IncludeModes != 0 {
|
||||
info.Key = channel.key
|
||||
info.Forward = channel.forward
|
||||
info.Modes = channel.flags.AllModes()
|
||||
info.UserLimit = channel.userLimit
|
||||
}
|
||||
info.Key = channel.key
|
||||
info.Forward = channel.forward
|
||||
info.Modes = channel.flags.AllModes()
|
||||
info.UserLimit = channel.userLimit
|
||||
|
||||
if includeFlags&IncludeLists != 0 {
|
||||
info.Bans = channel.lists[modes.BanMask].Masks()
|
||||
info.Invites = channel.lists[modes.InviteMask].Masks()
|
||||
info.Excepts = channel.lists[modes.ExceptMask].Masks()
|
||||
info.AccountToUMode = utils.CopyMap(channel.accountToUMode)
|
||||
}
|
||||
info.Bans = channel.lists[modes.BanMask].Masks()
|
||||
info.Invites = channel.lists[modes.InviteMask].Masks()
|
||||
info.Excepts = channel.lists[modes.ExceptMask].Masks()
|
||||
info.AccountToUMode = utils.CopyMap(channel.accountToUMode)
|
||||
|
||||
if includeFlags&IncludeSettings != 0 {
|
||||
info.Settings = channel.settings
|
||||
}
|
||||
info.Settings = channel.settings
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (channel *Channel) exportSummary() (info RegisteredChannel) {
|
||||
channel.stateMutex.RLock()
|
||||
defer channel.stateMutex.RUnlock()
|
||||
|
||||
info.Name = channel.name
|
||||
info.Founder = channel.registeredFounder
|
||||
info.RegisteredAt = channel.registeredTime
|
||||
|
||||
return
|
||||
}
|
||||
@ -288,9 +276,19 @@ func (channel *Channel) performWrite(additionalDirtyBits uint) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
info := channel.ExportRegistration(dirtyBits)
|
||||
err = channel.server.channelRegistry.StoreChannel(info, dirtyBits)
|
||||
if err != nil {
|
||||
var success bool
|
||||
info := channel.ExportRegistration()
|
||||
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.dirtyBits = channel.dirtyBits | dirtyBits
|
||||
channel.stateMutex.Unlock()
|
||||
@ -314,6 +312,7 @@ func (channel *Channel) SetRegistered(founder string) error {
|
||||
|
||||
// SetUnregistered deletes the channel's registration information.
|
||||
func (channel *Channel) SetUnregistered(expectedFounder string) {
|
||||
uuid := utils.GenerateUUIDv4()
|
||||
channel.stateMutex.Lock()
|
||||
defer channel.stateMutex.Unlock()
|
||||
|
||||
@ -324,6 +323,9 @@ func (channel *Channel) SetUnregistered(expectedFounder string) {
|
||||
var zeroTime time.Time
|
||||
channel.registeredTime = zeroTime
|
||||
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)
|
||||
|
@ -6,7 +6,9 @@ package irc
|
||||
import (
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ergochat/ergo/irc/datastore"
|
||||
"github.com/ergochat/ergo/irc/utils"
|
||||
)
|
||||
|
||||
@ -25,85 +27,75 @@ type channelManagerEntry struct {
|
||||
type ChannelManager struct {
|
||||
sync.RWMutex // tier 2
|
||||
// chans is the main data structure, mapping casefolded name -> *Channel
|
||||
chans map[string]*channelManagerEntry
|
||||
chansSkeletons utils.HashSet[string] // skeletons of *unregistered* chans
|
||||
registeredChannels utils.HashSet[string] // casefolds of registered chans
|
||||
registeredSkeletons utils.HashSet[string] // skeletons of registered chans
|
||||
purgedChannels utils.HashSet[string] // casefolds of purged chans
|
||||
server *Server
|
||||
chans map[string]*channelManagerEntry
|
||||
chansSkeletons utils.HashSet[string]
|
||||
purgedChannels map[string]ChannelPurgeRecord // casefolded name to purge record
|
||||
server *Server
|
||||
}
|
||||
|
||||
// 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.chansSkeletons = make(utils.HashSet[string])
|
||||
cm.server = server
|
||||
|
||||
// purging should work even if registration is disabled
|
||||
cm.purgedChannels = cm.server.channelRegistry.PurgedChannels()
|
||||
cm.loadRegisteredChannels(server.Config())
|
||||
return cm.loadRegisteredChannels(config)
|
||||
}
|
||||
|
||||
func (cm *ChannelManager) loadRegisteredChannels(config *Config) {
|
||||
if !config.Channels.Registration.Enabled {
|
||||
func (cm *ChannelManager) loadRegisteredChannels(config *Config) (err error) {
|
||||
allChannels, err := FetchAndDeserializeAll[RegisteredChannel](datastore.TableChannels, cm.server.dstore, cm.server.logger)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
allPurgeRecords, err := FetchAndDeserializeAll[ChannelPurgeRecord](datastore.TableChannelPurges, cm.server.dstore, cm.server.logger)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var newChannels []*Channel
|
||||
var collisions []string
|
||||
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()
|
||||
defer cm.Unlock()
|
||||
|
||||
cm.registeredChannels = make(utils.HashSet[string], len(rawNames))
|
||||
cm.registeredSkeletons = make(utils.HashSet[string], len(rawNames))
|
||||
for _, name := range rawNames {
|
||||
cfname, err := CasefoldChannel(name)
|
||||
if err == nil {
|
||||
cm.registeredChannels.Add(cfname)
|
||||
cm.purgedChannels = make(map[string]ChannelPurgeRecord, len(allPurgeRecords))
|
||||
for _, purge := range allPurgeRecords {
|
||||
cm.purgedChannels[purge.NameCasefolded] = purge
|
||||
}
|
||||
|
||||
for _, regInfo := range allChannels {
|
||||
cfname, err := CasefoldChannel(regInfo.Name)
|
||||
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(name)
|
||||
skeleton, err := Skeleton(regInfo.Name)
|
||||
if err == nil {
|
||||
cm.registeredSkeletons.Add(skeleton)
|
||||
cm.chansSkeletons.Add(skeleton)
|
||||
}
|
||||
|
||||
if !cm.purgedChannels.Has(cfname) {
|
||||
if _, ok := cm.chans[cfname]; !ok {
|
||||
ch := NewChannel(cm.server, name, cfname, true)
|
||||
cm.chans[cfname] = &channelManagerEntry{
|
||||
channel: ch,
|
||||
pendingJoins: 0,
|
||||
}
|
||||
newChannels = append(newChannels, ch)
|
||||
} else {
|
||||
collisions = append(collisions, name)
|
||||
if _, ok := cm.purgedChannels[cfname]; !ok {
|
||||
ch := NewChannel(cm.server, regInfo.Name, cfname, true, regInfo)
|
||||
cm.chans[cfname] = &channelManagerEntry{
|
||||
channel: ch,
|
||||
pendingJoins: 0,
|
||||
skeleton: skeleton,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get returns an existing channel with name equivalent to `name`, or nil
|
||||
func (cm *ChannelManager) Get(name string) (channel *Channel) {
|
||||
name, err := CasefoldChannel(name)
|
||||
if err == nil {
|
||||
cm.RLock()
|
||||
defer cm.RUnlock()
|
||||
entry := cm.chans[name]
|
||||
// if the channel is still loading, pretend we don't have it
|
||||
if entry != nil && entry.channel.IsLoaded() {
|
||||
return entry.channel
|
||||
}
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
cm.RLock()
|
||||
defer cm.RUnlock()
|
||||
entry := cm.chans[name]
|
||||
if entry != nil {
|
||||
return entry.channel
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -122,33 +114,26 @@ func (cm *ChannelManager) Join(client *Client, name string, key string, isSajoin
|
||||
cm.Lock()
|
||||
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
|
||||
}
|
||||
entry := cm.chans[casefoldedName]
|
||||
if entry == nil {
|
||||
registered := cm.registeredChannels.Has(casefoldedName)
|
||||
// enforce OpOnlyCreation
|
||||
if !registered && server.Config().Channels.OpOnlyCreation &&
|
||||
if server.Config().Channels.OpOnlyCreation &&
|
||||
!(isSajoin || client.HasRoleCapabs("chanreg")) {
|
||||
return nil, errInsufficientPrivs, false
|
||||
}
|
||||
// enforce confusables
|
||||
if !registered && (cm.chansSkeletons.Has(skeleton) || cm.registeredSkeletons.Has(skeleton)) {
|
||||
if cm.chansSkeletons.Has(skeleton) {
|
||||
return nil, errConfusableIdentifier, false
|
||||
}
|
||||
entry = &channelManagerEntry{
|
||||
channel: NewChannel(server, name, casefoldedName, registered),
|
||||
channel: NewChannel(server, name, casefoldedName, false, RegisteredChannel{}),
|
||||
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)
|
||||
entry.skeleton = skeleton
|
||||
}
|
||||
cm.chansSkeletons.Add(skeleton)
|
||||
entry.skeleton = skeleton
|
||||
cm.chans[casefoldedName] = entry
|
||||
newChannel = true
|
||||
}
|
||||
@ -160,7 +145,6 @@ func (cm *ChannelManager) Join(client *Client, name string, key string, isSajoin
|
||||
return err, ""
|
||||
}
|
||||
|
||||
channel.EnsureLoaded()
|
||||
err, forward = channel.Join(client, key, isSajoin || newChannel, rb)
|
||||
|
||||
cm.maybeCleanup(channel, true)
|
||||
@ -252,13 +236,6 @@ func (cm *ChannelManager) SetRegistered(channelName string, account string) (err
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
@ -268,17 +245,13 @@ func (cm *ChannelManager) SetUnregistered(channelName string, account string) (e
|
||||
return err
|
||||
}
|
||||
|
||||
info, err := cm.server.channelRegistry.LoadChannel(cfname)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if info.Founder != account {
|
||||
return errChannelNotOwnedByAccount
|
||||
}
|
||||
var uuid utils.UUID
|
||||
|
||||
defer func() {
|
||||
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()
|
||||
entry := cm.chans[cfname]
|
||||
if entry != nil {
|
||||
entry.channel.SetUnregistered(account)
|
||||
delete(cm.registeredChannels, cfname)
|
||||
// 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
|
||||
if entry.channel.Founder() != account {
|
||||
return errChannelNotOwnedByAccount
|
||||
}
|
||||
uuid = entry.channel.UUID()
|
||||
entry.channel.SetUnregistered(account) // changes the UUID
|
||||
// #1619: if the channel has 0 members and was only being retained
|
||||
// because it was registered, clean it up:
|
||||
cm.maybeCleanupInternal(cfname, entry, false)
|
||||
@ -322,12 +291,11 @@ func (cm *ChannelManager) Rename(name string, newName string) (err error) {
|
||||
var info RegisteredChannel
|
||||
defer func() {
|
||||
if channel != nil && info.Founder != "" {
|
||||
channel.Store(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)
|
||||
}
|
||||
channel.MarkDirty(IncludeAllAttrs)
|
||||
}
|
||||
// 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()
|
||||
|
||||
entry := cm.chans[oldCfname]
|
||||
if entry == nil || !entry.channel.IsLoaded() {
|
||||
if entry == nil {
|
||||
return errNoSuchChannel
|
||||
}
|
||||
channel = entry.channel
|
||||
info = channel.ExportRegistration(IncludeInitial)
|
||||
info = channel.ExportRegistration()
|
||||
registered := info.Founder != ""
|
||||
|
||||
oldSkeleton, err := Skeleton(info.Name)
|
||||
@ -348,13 +316,13 @@ func (cm *ChannelManager) Rename(name string, newName string) (err error) {
|
||||
}
|
||||
|
||||
if newCfname != oldCfname {
|
||||
if cm.chans[newCfname] != nil || cm.registeredChannels.Has(newCfname) {
|
||||
if cm.chans[newCfname] != nil {
|
||||
return errChannelNameInUse
|
||||
}
|
||||
}
|
||||
|
||||
if oldSkeleton != newSkeleton {
|
||||
if cm.chansSkeletons.Has(newSkeleton) || cm.registeredSkeletons.Has(newSkeleton) {
|
||||
if cm.chansSkeletons.Has(newSkeleton) {
|
||||
return errConfusableIdentifier
|
||||
}
|
||||
}
|
||||
@ -364,15 +332,8 @@ func (cm *ChannelManager) Rename(name string, newName string) (err error) {
|
||||
entry.skeleton = newSkeleton
|
||||
}
|
||||
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)
|
||||
cm.chansSkeletons.Add(newSkeleton)
|
||||
}
|
||||
delete(cm.chansSkeletons, oldSkeleton)
|
||||
cm.chansSkeletons.Add(newSkeleton)
|
||||
entry.channel.Rename(newName, newCfname)
|
||||
return nil
|
||||
}
|
||||
@ -390,7 +351,18 @@ func (cm *ChannelManager) Channels() (result []*Channel) {
|
||||
defer cm.RUnlock()
|
||||
result = make([]*Channel, 0, len(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)
|
||||
}
|
||||
}
|
||||
@ -403,29 +375,46 @@ func (cm *ChannelManager) Purge(chname string, record ChannelPurgeRecord) (err e
|
||||
if err != nil {
|
||||
return errInvalidChannelName
|
||||
}
|
||||
skel, err := Skeleton(chname)
|
||||
if err != nil {
|
||||
return errInvalidChannelName
|
||||
}
|
||||
|
||||
cm.Lock()
|
||||
cm.purgedChannels.Add(chname)
|
||||
entry := cm.chans[chname]
|
||||
if entry != nil {
|
||||
delete(cm.chans, chname)
|
||||
if entry.channel.Founder() != "" {
|
||||
delete(cm.registeredSkeletons, skel)
|
||||
} else {
|
||||
delete(cm.chansSkeletons, skel)
|
||||
record.NameCasefolded = chname
|
||||
record.UUID = utils.GenerateUUIDv4()
|
||||
|
||||
channel, err := func() (channel *Channel, err error) {
|
||||
cm.Lock()
|
||||
defer cm.Unlock()
|
||||
|
||||
if _, ok := cm.purgedChannels[chname]; ok {
|
||||
return nil, errChannelPurgedAlready
|
||||
}
|
||||
}
|
||||
cm.Unlock()
|
||||
|
||||
cm.server.channelRegistry.PurgeChannel(chname, record)
|
||||
if entry != nil {
|
||||
entry.channel.Purge("")
|
||||
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
|
||||
}
|
||||
return nil
|
||||
|
||||
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.
|
||||
@ -436,7 +425,7 @@ func (cm *ChannelManager) IsPurged(chname string) (result bool) {
|
||||
}
|
||||
|
||||
cm.RLock()
|
||||
result = cm.purgedChannels.Has(chname)
|
||||
_, result = cm.purgedChannels[chname]
|
||||
cm.RUnlock()
|
||||
return
|
||||
}
|
||||
@ -449,14 +438,16 @@ func (cm *ChannelManager) Unpurge(chname string) (err error) {
|
||||
}
|
||||
|
||||
cm.Lock()
|
||||
found := cm.purgedChannels.Has(chname)
|
||||
record, found := cm.purgedChannels[chname]
|
||||
delete(cm.purgedChannels, chname)
|
||||
cm.Unlock()
|
||||
|
||||
cm.server.channelRegistry.UnpurgeChannel(chname)
|
||||
if !found {
|
||||
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
|
||||
}
|
||||
|
||||
@ -475,8 +466,46 @@ func (cm *ChannelManager) UnfoldName(cfname string) (result string) {
|
||||
cm.RLock()
|
||||
entry := cm.chans[cfname]
|
||||
cm.RUnlock()
|
||||
if entry != nil && entry.channel.IsLoaded() {
|
||||
if entry != nil {
|
||||
return entry.channel.Name()
|
||||
}
|
||||
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 (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/tidwall/buntdb"
|
||||
|
||||
"github.com/ergochat/ergo/irc/modes"
|
||||
"github.com/ergochat/ergo/irc/utils"
|
||||
)
|
||||
@ -19,48 +14,6 @@ import (
|
||||
// this is exclusively the *persistence* layer for channel registration;
|
||||
// channel creation/tracking/destruction is in channelmanager.go
|
||||
|
||||
const (
|
||||
keyChannelExists = "channel.exists %s"
|
||||
keyChannelName = "channel.name %s" // stores the 'preferred name' of the channel, not casemapped
|
||||
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"
|
||||
// and needs to be read from memory and written to the db
|
||||
const (
|
||||
@ -80,8 +33,8 @@ const (
|
||||
type RegisteredChannel struct {
|
||||
// Name of the channel.
|
||||
Name string
|
||||
// Casefolded name of the channel.
|
||||
NameCasefolded string
|
||||
// UUID for the datastore.
|
||||
UUID utils.UUID
|
||||
// RegisteredAt represents the time that the channel was registered.
|
||||
RegisteredAt time.Time
|
||||
// Founder indicates the founder of the channel.
|
||||
@ -112,322 +65,26 @@ type RegisteredChannel struct {
|
||||
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 {
|
||||
Oper string
|
||||
PurgedAt time.Time
|
||||
Reason string
|
||||
NameCasefolded string `json:"Name"`
|
||||
UUID utils.UUID
|
||||
Oper string
|
||||
PurgedAt time.Time
|
||||
Reason string
|
||||
}
|
||||
|
||||
// ChannelRegistry manages registered channels.
|
||||
type ChannelRegistry struct {
|
||||
server *Server
|
||||
func (c *ChannelPurgeRecord) Serialize() ([]byte, error) {
|
||||
return json.Marshal(c)
|
||||
}
|
||||
|
||||
// NewChannelRegistry returns a new ChannelRegistry.
|
||||
func (reg *ChannelRegistry) Initialize(server *Server) {
|
||||
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
|
||||
})
|
||||
func (c *ChannelPurgeRecord) Deserialize(b []byte) error {
|
||||
return json.Unmarshal(b, c)
|
||||
}
|
||||
|
@ -459,7 +459,7 @@ func csRegisterHandler(service *ircService, server *Server, client *Client, comm
|
||||
// check whether a client has already registered too many channels
|
||||
func checkChanLimit(service *ircService, client *Client, rb *ResponseBuffer) (ok bool) {
|
||||
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")
|
||||
if !ok {
|
||||
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
|
||||
}
|
||||
|
||||
info := channel.ExportRegistration(0)
|
||||
channelKey := info.NameCasefolded
|
||||
info := channel.exportSummary()
|
||||
channelKey := channel.NameCasefolded()
|
||||
if !csPrivsCheck(service, info, client, rb) {
|
||||
return
|
||||
}
|
||||
@ -519,7 +519,7 @@ func csClearHandler(service *ircService, server *Server, client *Client, command
|
||||
service.Notice(rb, client.t("Channel does not exist"))
|
||||
return
|
||||
}
|
||||
if !csPrivsCheck(service, channel.ExportRegistration(0), client, rb) {
|
||||
if !csPrivsCheck(service, channel.exportSummary(), client, rb) {
|
||||
return
|
||||
}
|
||||
|
||||
@ -550,7 +550,7 @@ func csTransferHandler(service *ircService, server *Server, client *Client, comm
|
||||
service.Notice(rb, client.t("Channel does not exist"))
|
||||
return
|
||||
}
|
||||
regInfo := channel.ExportRegistration(0)
|
||||
regInfo := channel.exportSummary()
|
||||
chname = regInfo.Name
|
||||
account := client.Account()
|
||||
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) {
|
||||
if !client.HasRoleCapabs("chanreg") {
|
||||
service.Notice(rb, client.t("Insufficient privileges"))
|
||||
return
|
||||
}
|
||||
|
||||
var searchRegex *regexp.Regexp
|
||||
if len(params) > 0 {
|
||||
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 ***")))
|
||||
|
||||
channels := server.channelRegistry.AllChannels()
|
||||
channels := server.channels.AllRegisteredChannels()
|
||||
for _, channel := range channels {
|
||||
if searchRegex == nil || searchRegex.MatchString(channel) {
|
||||
service.Notice(rb, fmt.Sprintf(" %s", channel))
|
||||
@ -771,7 +766,7 @@ func csInfoHandler(service *ircService, server *Server, client *Client, command
|
||||
|
||||
// purge status
|
||||
if client.HasRoleCapabs("chanreg") {
|
||||
purgeRecord, err := server.channelRegistry.LoadPurgeRecord(chname)
|
||||
purgeRecord, err := server.channels.LoadPurgeRecord(chname)
|
||||
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("Purged by operator: %s"), purgeRecord.Oper))
|
||||
@ -789,13 +784,7 @@ func csInfoHandler(service *ircService, server *Server, client *Client, command
|
||||
var chinfo RegisteredChannel
|
||||
channel := server.channels.Get(params[0])
|
||||
if channel != nil {
|
||||
chinfo = channel.ExportRegistration(0)
|
||||
} else {
|
||||
chinfo, err = server.channelRegistry.LoadChannel(chname)
|
||||
if err != nil && !(err == errNoSuchChannel || err == errFeatureDisabled) {
|
||||
service.Notice(rb, client.t("An error occurred"))
|
||||
return
|
||||
}
|
||||
chinfo = channel.exportSummary()
|
||||
}
|
||||
|
||||
// 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"))
|
||||
return
|
||||
}
|
||||
info := channel.ExportRegistration(IncludeSettings)
|
||||
info := channel.exportSummary()
|
||||
if !csPrivsCheck(service, info, client, rb) {
|
||||
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) {
|
||||
@ -850,12 +839,12 @@ func csSetHandler(service *ircService, server *Server, client *Client, command s
|
||||
service.Notice(rb, client.t("No such channel"))
|
||||
return
|
||||
}
|
||||
info := channel.ExportRegistration(IncludeSettings)
|
||||
settings := info.Settings
|
||||
info := channel.exportSummary()
|
||||
if !csPrivsCheck(service, info, client, rb) {
|
||||
return
|
||||
}
|
||||
|
||||
settings := channel.Settings()
|
||||
var err error
|
||||
switch strings.ToLower(setting) {
|
||||
case "history":
|
||||
|
159
irc/database.go
159
irc/database.go
@ -14,6 +14,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ergochat/ergo/irc/bunt"
|
||||
"github.com/ergochat/ergo/irc/datastore"
|
||||
"github.com/ergochat/ergo/irc/modes"
|
||||
"github.com/ergochat/ergo/irc/utils"
|
||||
|
||||
@ -21,12 +23,19 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
// 'version' of the database schema
|
||||
keySchemaVersion = "db.version"
|
||||
// latest schema of the db
|
||||
latestDbSchema = 22
|
||||
// TODO migrate metadata keys as well
|
||||
|
||||
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
|
||||
@ -99,10 +108,7 @@ func openDatabaseInternal(config *Config, allowAutoupgrade bool) (db *buntdb.DB,
|
||||
// read the current version string
|
||||
var version int
|
||||
err = db.View(func(tx *buntdb.Tx) (err error) {
|
||||
vStr, err := tx.Get(keySchemaVersion)
|
||||
if err == nil {
|
||||
version, err = strconv.Atoi(vStr)
|
||||
}
|
||||
version, err = retrieveSchemaVersion(tx)
|
||||
return err
|
||||
})
|
||||
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) {
|
||||
path := config.Datastore.Path
|
||||
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
|
||||
err = store.Update(func(tx *buntdb.Tx) error {
|
||||
for {
|
||||
vStr, _ := tx.Get(keySchemaVersion)
|
||||
version, _ = strconv.Atoi(vStr)
|
||||
if version == 0 {
|
||||
version, err = retrieveSchemaVersion(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if version == latestDbSchema {
|
||||
// success!
|
||||
break
|
||||
@ -183,11 +204,12 @@ func UpgradeDB(config *Config) (err error) {
|
||||
if err != nil {
|
||||
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 {
|
||||
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
|
||||
})
|
||||
@ -198,19 +220,17 @@ func UpgradeDB(config *Config) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
func LoadCloakSecret(db *buntdb.DB) (result string) {
|
||||
db.View(func(tx *buntdb.Tx) error {
|
||||
result, _ = tx.Get(keyCloakSecret)
|
||||
return nil
|
||||
})
|
||||
return
|
||||
func LoadCloakSecret(dstore datastore.Datastore) (result string, err error) {
|
||||
val, err := dstore.Get(datastore.TableMetadata, cloakSecretUUID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return string(val), nil
|
||||
}
|
||||
|
||||
func StoreCloakSecret(db *buntdb.DB, secret string) {
|
||||
db.Update(func(tx *buntdb.Tx) error {
|
||||
tx.Set(keyCloakSecret, secret, nil)
|
||||
return nil
|
||||
})
|
||||
func StoreCloakSecret(dstore datastore.Datastore, secret string) {
|
||||
// TODO error checking
|
||||
dstore.Set(datastore.TableMetadata, cloakSecretUUID, []byte(secret), time.Time{})
|
||||
}
|
||||
|
||||
func schemaChangeV1toV2(config *Config, tx *buntdb.Tx) error {
|
||||
@ -1112,6 +1132,92 @@ func schemaChangeV21To22(config *Config, tx *buntdb.Tx) error {
|
||||
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) {
|
||||
for _, change := range allChanges {
|
||||
if initialVersion == change.InitialVersion {
|
||||
@ -1227,4 +1333,9 @@ var allChanges = []SchemaChange{
|
||||
TargetVersion: 22,
|
||||
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")
|
||||
errNoSuchChannel = errors.New(`No such channel`)
|
||||
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")
|
||||
errInsufficientPrivs = errors.New("Insufficient privileges")
|
||||
errInvalidUsername = errors.New("Invalid username")
|
||||
|
@ -638,3 +638,9 @@ func (channel *Channel) getAmode(cfaccount string) (result modes.Mode) {
|
||||
defer channel.stateMutex.RUnlock()
|
||||
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")
|
||||
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) {
|
||||
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)))
|
||||
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"))
|
||||
}
|
||||
|
@ -9,9 +9,13 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
@ -20,7 +24,7 @@ const (
|
||||
// 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
|
||||
// db of the hardcoded version)
|
||||
importDBSchemaVersion = 22
|
||||
importDBSchemaVersion = 23
|
||||
)
|
||||
|
||||
type userImport struct {
|
||||
@ -54,8 +58,8 @@ type databaseImport struct {
|
||||
Channels map[string]channelImport
|
||||
}
|
||||
|
||||
func serializeAmodes(raw map[string]string, validCfUsernames utils.HashSet[string]) (result []byte, err error) {
|
||||
processed := make(map[string]int, len(raw))
|
||||
func convertAmodes(raw map[string]string, validCfUsernames utils.HashSet[string]) (result map[string]modes.Mode, err error) {
|
||||
result = make(map[string]modes.Mode)
|
||||
for accountName, mode := range raw {
|
||||
if len(mode) != 1 {
|
||||
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) {
|
||||
log.Printf("skipping invalid amode recipient %s\n", accountName)
|
||||
} else {
|
||||
processed[cfname] = int(mode[0])
|
||||
result[cfname] = modes.Mode(mode[0])
|
||||
}
|
||||
}
|
||||
result, err = json.Marshal(processed)
|
||||
return
|
||||
}
|
||||
|
||||
@ -147,8 +150,9 @@ func doImportDBGeneric(config *Config, dbImport databaseImport, credsType Creden
|
||||
cfUsernames.Add(cfUsername)
|
||||
}
|
||||
|
||||
// TODO fix this:
|
||||
for chname, chInfo := range dbImport.Channels {
|
||||
cfchname, err := CasefoldChannel(chname)
|
||||
_, err := CasefoldChannel(chname)
|
||||
if err != nil {
|
||||
log.Printf("invalid channel name %s: %v", chname, err)
|
||||
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)
|
||||
continue
|
||||
}
|
||||
tx.Set(fmt.Sprintf(keyChannelExists, cfchname), "1", nil)
|
||||
tx.Set(fmt.Sprintf(keyChannelName, cfchname), chname, nil)
|
||||
tx.Set(fmt.Sprintf(keyChannelRegTime, cfchname), strconv.FormatInt(chInfo.RegisteredAt, 10), nil)
|
||||
tx.Set(fmt.Sprintf(keyChannelFounder, cfchname), cffounder, nil)
|
||||
accountChannelsKey := fmt.Sprintf(keyAccountChannels, cffounder)
|
||||
founderChannels, fcErr := tx.Get(accountChannelsKey)
|
||||
if fcErr != nil || founderChannels == "" {
|
||||
founderChannels = cfchname
|
||||
} else {
|
||||
founderChannels = fmt.Sprintf("%s,%s", founderChannels, cfchname)
|
||||
}
|
||||
tx.Set(accountChannelsKey, founderChannels, nil)
|
||||
var regInfo RegisteredChannel
|
||||
regInfo.Name = chname
|
||||
regInfo.UUID = utils.GenerateUUIDv4()
|
||||
regInfo.Founder = cffounder
|
||||
regInfo.RegisteredAt = time.Unix(0, chInfo.RegisteredAt).UTC()
|
||||
if chInfo.Topic != "" {
|
||||
tx.Set(fmt.Sprintf(keyChannelTopic, cfchname), chInfo.Topic, nil)
|
||||
tx.Set(fmt.Sprintf(keyChannelTopicSetTime, cfchname), strconv.FormatInt(chInfo.TopicSetAt, 10), nil)
|
||||
tx.Set(fmt.Sprintf(keyChannelTopicSetBy, cfchname), chInfo.TopicSetBy, nil)
|
||||
regInfo.Topic = chInfo.Topic
|
||||
regInfo.TopicSetBy = chInfo.TopicSetBy
|
||||
regInfo.TopicSetTime = time.Unix(0, chInfo.TopicSetAt).UTC()
|
||||
}
|
||||
|
||||
if len(chInfo.Amode) != 0 {
|
||||
m, err := serializeAmodes(chInfo.Amode, cfUsernames)
|
||||
m, err := convertAmodes(chInfo.Amode, cfUsernames)
|
||||
if err == nil {
|
||||
tx.Set(fmt.Sprintf(keyChannelAccountToUMode, cfchname), string(m), nil)
|
||||
regInfo.AccountToUMode = m
|
||||
} 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)
|
||||
if chInfo.Key != "" {
|
||||
tx.Set(fmt.Sprintf(keyChannelPassword, cfchname), chInfo.Key, nil)
|
||||
for _, mode := range chInfo.Modes {
|
||||
regInfo.Modes = append(regInfo.Modes, modes.Mode(mode))
|
||||
}
|
||||
regInfo.Key = chInfo.Key
|
||||
if chInfo.Limit > 0 {
|
||||
tx.Set(fmt.Sprintf(keyChannelUserLimit, cfchname), strconv.Itoa(chInfo.Limit), nil)
|
||||
regInfo.UserLimit = chInfo.Limit
|
||||
}
|
||||
if chInfo.Forward != "" {
|
||||
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 {
|
||||
|
121
irc/legacy.go
121
irc/legacy.go
@ -4,7 +4,15 @@ package irc
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/tidwall/buntdb"
|
||||
|
||||
"github.com/ergochat/ergo/irc/modes"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -25,3 +33,116 @@ func decodeLegacyPasswordHash(hash string) ([]byte, error) {
|
||||
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) {
|
||||
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)))
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
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/okzk/sdnotify"
|
||||
"github.com/tidwall/buntdb"
|
||||
|
||||
"github.com/ergochat/ergo/irc/bunt"
|
||||
"github.com/ergochat/ergo/irc/caps"
|
||||
"github.com/ergochat/ergo/irc/connection_limits"
|
||||
"github.com/ergochat/ergo/irc/datastore"
|
||||
"github.com/ergochat/ergo/irc/flatip"
|
||||
"github.com/ergochat/ergo/irc/flock"
|
||||
"github.com/ergochat/ergo/irc/history"
|
||||
@ -33,7 +36,6 @@ import (
|
||||
"github.com/ergochat/ergo/irc/mysql"
|
||||
"github.com/ergochat/ergo/irc/sno"
|
||||
"github.com/ergochat/ergo/irc/utils"
|
||||
"github.com/tidwall/buntdb"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -66,7 +68,6 @@ type Server struct {
|
||||
accepts AcceptManager
|
||||
accounts AccountManager
|
||||
channels ChannelManager
|
||||
channelRegistry ChannelRegistry
|
||||
clients ClientManager
|
||||
config atomic.Pointer[Config]
|
||||
configFilename string
|
||||
@ -87,6 +88,7 @@ type Server struct {
|
||||
tracebackSignal chan os.Signal
|
||||
snomasks SnoManager
|
||||
store *buntdb.DB
|
||||
dstore datastore.Datastore
|
||||
historyDB mysql.MySQL
|
||||
torLimiter connection_limits.TorLimiter
|
||||
whoWas WhoWasList
|
||||
@ -98,6 +100,10 @@ type Server struct {
|
||||
|
||||
// NewServer returns a new Oragono server.
|
||||
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
|
||||
server := &Server{
|
||||
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
|
||||
// XXX this modifies config after the initial load, which is naughty,
|
||||
// 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
|
||||
server.config.Store(config)
|
||||
@ -837,6 +847,7 @@ func (server *Server) loadDatastore(config *Config) error {
|
||||
db, err := OpenDatabase(config)
|
||||
if err == nil {
|
||||
server.store = db
|
||||
server.dstore = bunt.NewBuntdbDatastore(db, server.logger)
|
||||
return nil
|
||||
} else {
|
||||
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.loadKLines()
|
||||
|
||||
server.channelRegistry.Initialize(server)
|
||||
server.channels.Initialize(server)
|
||||
server.channels.Initialize(server, config)
|
||||
server.accounts.Initialize(server)
|
||||
|
||||
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