Browse Source

refactor of channel persistence to use UUIDs

tags/v2.12.0-rc1
Shivaram Lingamneni 1 year ago
parent
commit
7ce0636276
18 changed files with 797 additions and 646 deletions
  1. 1
    23
      irc/accounts.go
  2. 106
    0
      irc/bunt/bunt_datastore.go
  3. 51
    49
      irc/channel.go
  4. 161
    132
      irc/channelmanager.go
  5. 16
    359
      irc/channelreg.go
  6. 12
    23
      irc/chanserv.go
  7. 133
    22
      irc/database.go
  8. 45
    0
      irc/datastore/datastore.go
  9. 1
    0
      irc/errors.go
  10. 6
    0
      irc/getters.go
  11. 1
    1
      irc/handlers.go
  12. 1
    1
      irc/hostserv.go
  13. 32
    29
      irc/import.go
  14. 121
    0
      irc/legacy.go
  15. 2
    2
      irc/nickserv.go
  16. 37
    0
      irc/serde.go
  17. 15
    5
      irc/server.go
  18. 56
    0
      irc/utils/uuid.go

+ 1
- 23
irc/accounts.go View File

@@ -39,7 +39,6 @@ const (
39 39
 	keyAccountSettings         = "account.settings %s"
40 40
 	keyAccountVHost            = "account.vhost %s"
41 41
 	keyCertToAccount           = "account.creds.certfp %s"
42
-	keyAccountChannels         = "account.channels %s" // channels registered to the account
43 42
 	keyAccountLastSeen         = "account.lastseen %s"
44 43
 	keyAccountReadMarkers      = "account.readmarkers %s"
45 44
 	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 {
1765 1764
 	nicksKey := fmt.Sprintf(keyAccountAdditionalNicks, casefoldedAccount)
1766 1765
 	settingsKey := fmt.Sprintf(keyAccountSettings, casefoldedAccount)
1767 1766
 	vhostKey := fmt.Sprintf(keyAccountVHost, casefoldedAccount)
1768
-	channelsKey := fmt.Sprintf(keyAccountChannels, casefoldedAccount)
1769 1767
 	joinedChannelsKey := fmt.Sprintf(keyAccountChannelToModes, casefoldedAccount)
1770 1768
 	lastSeenKey := fmt.Sprintf(keyAccountLastSeen, casefoldedAccount)
1771 1769
 	readMarkersKey := fmt.Sprintf(keyAccountReadMarkers, casefoldedAccount)
@@ -1781,10 +1779,9 @@ func (am *AccountManager) Unregister(account string, erase bool) error {
1781 1779
 		am.killClients(clients)
1782 1780
 	}()
1783 1781
 
1784
-	var registeredChannels []string
1785 1782
 	// on our way out, unregister all the account's channels and delete them from the db
1786 1783
 	defer func() {
1787
-		for _, channelName := range registeredChannels {
1784
+		for _, channelName := range am.server.channels.ChannelsForAccount(casefoldedAccount) {
1788 1785
 			err := am.server.channels.SetUnregistered(channelName, casefoldedAccount)
1789 1786
 			if err != nil {
1790 1787
 				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 {
1799 1796
 	defer am.serialCacheUpdateMutex.Unlock()
1800 1797
 
1801 1798
 	var accountName string
1802
-	var channelsStr string
1803 1799
 	keepProtections := false
1804 1800
 	am.server.store.Update(func(tx *buntdb.Tx) error {
1805 1801
 		// get the unfolded account name; for an active account, this is
@@ -1827,8 +1823,6 @@ func (am *AccountManager) Unregister(account string, erase bool) error {
1827 1823
 		credText, err = tx.Get(credentialsKey)
1828 1824
 		tx.Delete(credentialsKey)
1829 1825
 		tx.Delete(vhostKey)
1830
-		channelsStr, _ = tx.Get(channelsKey)
1831
-		tx.Delete(channelsKey)
1832 1826
 		tx.Delete(joinedChannelsKey)
1833 1827
 		tx.Delete(lastSeenKey)
1834 1828
 		tx.Delete(readMarkersKey)
@@ -1858,7 +1852,6 @@ func (am *AccountManager) Unregister(account string, erase bool) error {
1858 1852
 
1859 1853
 	skeleton, _ := Skeleton(accountName)
1860 1854
 	additionalNicks := unmarshalReservedNicks(rawNicks)
1861
-	registeredChannels = unmarshalRegisteredChannels(channelsStr)
1862 1855
 
1863 1856
 	am.Lock()
1864 1857
 	defer am.Unlock()
@@ -1890,21 +1883,6 @@ func unmarshalRegisteredChannels(channelsStr string) (result []string) {
1890 1883
 	return
1891 1884
 }
1892 1885
 
1893
-func (am *AccountManager) ChannelsForAccount(account string) (channels []string) {
1894
-	cfaccount, err := CasefoldName(account)
1895
-	if err != nil {
1896
-		return
1897
-	}
1898
-
1899
-	var channelStr string
1900
-	key := fmt.Sprintf(keyAccountChannels, cfaccount)
1901
-	am.server.store.View(func(tx *buntdb.Tx) error {
1902
-		channelStr, _ = tx.Get(key)
1903
-		return nil
1904
-	})
1905
-	return unmarshalRegisteredChannels(channelStr)
1906
-}
1907
-
1908 1886
 func (am *AccountManager) AuthenticateByCertificate(client *Client, certfp string, peerCerts []*x509.Certificate, authzid string) (err error) {
1909 1887
 	if certfp == "" {
1910 1888
 		return errAccountInvalidCredentials

+ 106
- 0
irc/bunt/bunt_datastore.go View File

@@ -0,0 +1,106 @@
1
+// Copyright (c) 2022 Shivaram Lingamneni
2
+// released under the MIT license
3
+
4
+package bunt
5
+
6
+import (
7
+	"fmt"
8
+	"strings"
9
+	"time"
10
+
11
+	"github.com/tidwall/buntdb"
12
+
13
+	"github.com/ergochat/ergo/irc/datastore"
14
+	"github.com/ergochat/ergo/irc/logger"
15
+	"github.com/ergochat/ergo/irc/utils"
16
+)
17
+
18
+// BuntKey yields a string key corresponding to a (table, UUID) pair.
19
+// Ideally this would not be public, but some of the migration code
20
+// needs it.
21
+func BuntKey(table datastore.Table, uuid utils.UUID) string {
22
+	return fmt.Sprintf("%x %s", table, uuid.String())
23
+}
24
+
25
+// buntdbDatastore implements datastore.Datastore using a buntdb.
26
+type buntdbDatastore struct {
27
+	db     *buntdb.DB
28
+	logger *logger.Manager
29
+}
30
+
31
+// NewBuntdbDatastore returns a datastore.Datastore backed by buntdb.
32
+func NewBuntdbDatastore(db *buntdb.DB, logger *logger.Manager) datastore.Datastore {
33
+	return &buntdbDatastore{
34
+		db:     db,
35
+		logger: logger,
36
+	}
37
+}
38
+
39
+func (b *buntdbDatastore) Backoff() time.Duration {
40
+	return 0
41
+}
42
+
43
+func (b *buntdbDatastore) GetAll(table datastore.Table) (result []datastore.KV, err error) {
44
+	tablePrefix := fmt.Sprintf("%x ", table)
45
+	err = b.db.View(func(tx *buntdb.Tx) error {
46
+		err := tx.AscendGreaterOrEqual("", tablePrefix, func(key, value string) bool {
47
+			if !strings.HasPrefix(key, tablePrefix) {
48
+				return false
49
+			}
50
+			uuid, err := utils.DecodeUUID(strings.TrimPrefix(key, tablePrefix))
51
+			if err == nil {
52
+				result = append(result, datastore.KV{UUID: uuid, Value: []byte(value)})
53
+			} else {
54
+				b.logger.Error("datastore", "invalid uuid", key)
55
+			}
56
+			return true
57
+		})
58
+		return err
59
+	})
60
+	return
61
+}
62
+
63
+func (b *buntdbDatastore) Get(table datastore.Table, uuid utils.UUID) (value []byte, err error) {
64
+	buntKey := BuntKey(table, uuid)
65
+	var result string
66
+	err = b.db.View(func(tx *buntdb.Tx) error {
67
+		result, err = tx.Get(buntKey)
68
+		return err
69
+	})
70
+	return []byte(result), err
71
+}
72
+
73
+func (b *buntdbDatastore) Set(table datastore.Table, uuid utils.UUID, value []byte, expiration time.Time) (err error) {
74
+	buntKey := BuntKey(table, uuid)
75
+	var setOptions *buntdb.SetOptions
76
+	if !expiration.IsZero() {
77
+		ttl := time.Until(expiration)
78
+		if ttl > 0 {
79
+			setOptions = &buntdb.SetOptions{Expires: true, TTL: ttl}
80
+		} else {
81
+			return nil // it already expired, i guess?
82
+		}
83
+	}
84
+	strVal := string(value)
85
+
86
+	err = b.db.Update(func(tx *buntdb.Tx) error {
87
+		_, _, err := tx.Set(buntKey, strVal, setOptions)
88
+		return err
89
+	})
90
+	return
91
+}
92
+
93
+func (b *buntdbDatastore) Delete(table datastore.Table, key utils.UUID) (err error) {
94
+	buntKey := BuntKey(table, key)
95
+	err = b.db.Update(func(tx *buntdb.Tx) error {
96
+		_, err := tx.Delete(buntKey)
97
+		return err
98
+	})
99
+	// deleting a nonexistent key is not considered an error
100
+	switch err {
101
+	case buntdb.ErrNotFound:
102
+		return nil
103
+	default:
104
+		return err
105
+	}
106
+}

+ 51
- 49
irc/channel.go View File

@@ -16,6 +16,7 @@ import (
16 16
 	"github.com/ergochat/irc-go/ircutils"
17 17
 
18 18
 	"github.com/ergochat/ergo/irc/caps"
19
+	"github.com/ergochat/ergo/irc/datastore"
19 20
 	"github.com/ergochat/ergo/irc/history"
20 21
 	"github.com/ergochat/ergo/irc/modes"
21 22
 	"github.com/ergochat/ergo/irc/utils"
@@ -50,14 +51,14 @@ type Channel struct {
50 51
 	stateMutex        sync.RWMutex // tier 1
51 52
 	writebackLock     sync.Mutex   // tier 1.5
52 53
 	joinPartMutex     sync.Mutex   // tier 3
53
-	ensureLoaded      utils.Once   // manages loading stored registration info from the database
54 54
 	dirtyBits         uint
55 55
 	settings          ChannelSettings
56
+	uuid              utils.UUID
56 57
 }
57 58
 
58 59
 // NewChannel creates a new channel from a `Server` and a `name`
59 60
 // string, which must be unique on the server.
60
-func NewChannel(s *Server, name, casefoldedName string, registered bool) *Channel {
61
+func NewChannel(s *Server, name, casefoldedName string, registered bool, regInfo RegisteredChannel) *Channel {
61 62
 	config := s.Config()
62 63
 
63 64
 	channel := &Channel{
@@ -71,14 +72,15 @@ func NewChannel(s *Server, name, casefoldedName string, registered bool) *Channe
71 72
 	channel.initializeLists()
72 73
 	channel.history.Initialize(0, 0)
73 74
 
74
-	if !registered {
75
+	if registered {
76
+		channel.applyRegInfo(regInfo)
77
+	} else {
75 78
 		channel.resizeHistory(config)
76 79
 		for _, mode := range config.Channels.defaultModes {
77 80
 			channel.flags.SetMode(mode, true)
78 81
 		}
79
-		// no loading to do, so "mark" the load operation as "done":
80
-		channel.ensureLoaded.Do(func() {})
81
-	} // else: modes will be loaded before first join
82
+		channel.uuid = utils.GenerateUUIDv4()
83
+	}
82 84
 
83 85
 	return channel
84 86
 }
@@ -92,24 +94,6 @@ func (channel *Channel) initializeLists() {
92 94
 	channel.accountToUMode = make(map[string]modes.Mode)
93 95
 }
94 96
 
95
-// EnsureLoaded blocks until the channel's registration info has been loaded
96
-// from the database.
97
-func (channel *Channel) EnsureLoaded() {
98
-	channel.ensureLoaded.Do(func() {
99
-		nmc := channel.NameCasefolded()
100
-		info, err := channel.server.channelRegistry.LoadChannel(nmc)
101
-		if err == nil {
102
-			channel.applyRegInfo(info)
103
-		} else {
104
-			channel.server.logger.Error("internal", "couldn't load channel", nmc, err.Error())
105
-		}
106
-	})
107
-}
108
-
109
-func (channel *Channel) IsLoaded() bool {
110
-	return channel.ensureLoaded.Done()
111
-}
112
-
113 97
 func (channel *Channel) resizeHistory(config *Config) {
114 98
 	status, _, _ := channel.historyStatus(config)
115 99
 	if status == HistoryEphemeral {
@@ -126,6 +110,7 @@ func (channel *Channel) applyRegInfo(chanReg RegisteredChannel) {
126 110
 	channel.stateMutex.Lock()
127 111
 	defer channel.stateMutex.Unlock()
128 112
 
113
+	channel.uuid = chanReg.UUID
129 114
 	channel.registeredFounder = chanReg.Founder
130 115
 	channel.registeredTime = chanReg.RegisteredAt
131 116
 	channel.topic = chanReg.Topic
@@ -150,38 +135,41 @@ func (channel *Channel) applyRegInfo(chanReg RegisteredChannel) {
150 135
 }
151 136
 
152 137
 // obtain a consistent snapshot of the channel state that can be persisted to the DB
153
-func (channel *Channel) ExportRegistration(includeFlags uint) (info RegisteredChannel) {
138
+func (channel *Channel) ExportRegistration() (info RegisteredChannel) {
154 139
 	channel.stateMutex.RLock()
155 140
 	defer channel.stateMutex.RUnlock()
156 141
 
157 142
 	info.Name = channel.name
158
-	info.NameCasefolded = channel.nameCasefolded
143
+	info.UUID = channel.uuid
159 144
 	info.Founder = channel.registeredFounder
160 145
 	info.RegisteredAt = channel.registeredTime
161 146
 
162
-	if includeFlags&IncludeTopic != 0 {
163
-		info.Topic = channel.topic
164
-		info.TopicSetBy = channel.topicSetBy
165
-		info.TopicSetTime = channel.topicSetTime
166
-	}
147
+	info.Topic = channel.topic
148
+	info.TopicSetBy = channel.topicSetBy
149
+	info.TopicSetTime = channel.topicSetTime
167 150
 
168
-	if includeFlags&IncludeModes != 0 {
169
-		info.Key = channel.key
170
-		info.Forward = channel.forward
171
-		info.Modes = channel.flags.AllModes()
172
-		info.UserLimit = channel.userLimit
173
-	}
151
+	info.Key = channel.key
152
+	info.Forward = channel.forward
153
+	info.Modes = channel.flags.AllModes()
154
+	info.UserLimit = channel.userLimit
174 155
 
175
-	if includeFlags&IncludeLists != 0 {
176
-		info.Bans = channel.lists[modes.BanMask].Masks()
177
-		info.Invites = channel.lists[modes.InviteMask].Masks()
178
-		info.Excepts = channel.lists[modes.ExceptMask].Masks()
179
-		info.AccountToUMode = utils.CopyMap(channel.accountToUMode)
180
-	}
156
+	info.Bans = channel.lists[modes.BanMask].Masks()
157
+	info.Invites = channel.lists[modes.InviteMask].Masks()
158
+	info.Excepts = channel.lists[modes.ExceptMask].Masks()
159
+	info.AccountToUMode = utils.CopyMap(channel.accountToUMode)
181 160
 
182
-	if includeFlags&IncludeSettings != 0 {
183
-		info.Settings = channel.settings
184
-	}
161
+	info.Settings = channel.settings
162
+
163
+	return
164
+}
165
+
166
+func (channel *Channel) exportSummary() (info RegisteredChannel) {
167
+	channel.stateMutex.RLock()
168
+	defer channel.stateMutex.RUnlock()
169
+
170
+	info.Name = channel.name
171
+	info.Founder = channel.registeredFounder
172
+	info.RegisteredAt = channel.registeredTime
185 173
 
186 174
 	return
187 175
 }
@@ -288,9 +276,19 @@ func (channel *Channel) performWrite(additionalDirtyBits uint) (err error) {
288 276
 		return
289 277
 	}
290 278
 
291
-	info := channel.ExportRegistration(dirtyBits)
292
-	err = channel.server.channelRegistry.StoreChannel(info, dirtyBits)
293
-	if err != nil {
279
+	var success bool
280
+	info := channel.ExportRegistration()
281
+	if b, err := info.Serialize(); err == nil {
282
+		if err := channel.server.dstore.Set(datastore.TableChannels, info.UUID, b, time.Time{}); err == nil {
283
+			success = true
284
+		} else {
285
+			channel.server.logger.Error("internal", "couldn't persist channel", info.Name, err.Error())
286
+		}
287
+	} else {
288
+		channel.server.logger.Error("internal", "couldn't serialize channel", info.Name, err.Error())
289
+	}
290
+
291
+	if !success {
294 292
 		channel.stateMutex.Lock()
295 293
 		channel.dirtyBits = channel.dirtyBits | dirtyBits
296 294
 		channel.stateMutex.Unlock()
@@ -314,6 +312,7 @@ func (channel *Channel) SetRegistered(founder string) error {
314 312
 
315 313
 // SetUnregistered deletes the channel's registration information.
316 314
 func (channel *Channel) SetUnregistered(expectedFounder string) {
315
+	uuid := utils.GenerateUUIDv4()
317 316
 	channel.stateMutex.Lock()
318 317
 	defer channel.stateMutex.Unlock()
319 318
 
@@ -324,6 +323,9 @@ func (channel *Channel) SetUnregistered(expectedFounder string) {
324 323
 	var zeroTime time.Time
325 324
 	channel.registeredTime = zeroTime
326 325
 	channel.accountToUMode = make(map[string]modes.Mode)
326
+	// reset the UUID so that any re-registration will persist under
327
+	// a separate key:
328
+	channel.uuid = uuid
327 329
 }
328 330
 
329 331
 // implements `CHANSERV CLEAR #chan ACCESS` (resets bans, invites, excepts, and amodes)

+ 161
- 132
irc/channelmanager.go View File

@@ -6,7 +6,9 @@ package irc
6 6
 import (
7 7
 	"sort"
8 8
 	"sync"
9
+	"time"
9 10
 
11
+	"github.com/ergochat/ergo/irc/datastore"
10 12
 	"github.com/ergochat/ergo/irc/utils"
11 13
 )
12 14
 
@@ -25,85 +27,75 @@ type channelManagerEntry struct {
25 27
 type ChannelManager struct {
26 28
 	sync.RWMutex // tier 2
27 29
 	// chans is the main data structure, mapping casefolded name -> *Channel
28
-	chans               map[string]*channelManagerEntry
29
-	chansSkeletons      utils.HashSet[string] // skeletons of *unregistered* chans
30
-	registeredChannels  utils.HashSet[string] // casefolds of registered chans
31
-	registeredSkeletons utils.HashSet[string] // skeletons of registered chans
32
-	purgedChannels      utils.HashSet[string] // casefolds of purged chans
33
-	server              *Server
30
+	chans          map[string]*channelManagerEntry
31
+	chansSkeletons utils.HashSet[string]
32
+	purgedChannels map[string]ChannelPurgeRecord // casefolded name to purge record
33
+	server         *Server
34 34
 }
35 35
 
36 36
 // NewChannelManager returns a new ChannelManager.
37
-func (cm *ChannelManager) Initialize(server *Server) {
37
+func (cm *ChannelManager) Initialize(server *Server, config *Config) (err error) {
38 38
 	cm.chans = make(map[string]*channelManagerEntry)
39 39
 	cm.chansSkeletons = make(utils.HashSet[string])
40 40
 	cm.server = server
41
-
42
-	// purging should work even if registration is disabled
43
-	cm.purgedChannels = cm.server.channelRegistry.PurgedChannels()
44
-	cm.loadRegisteredChannels(server.Config())
41
+	return cm.loadRegisteredChannels(config)
45 42
 }
46 43
 
47
-func (cm *ChannelManager) loadRegisteredChannels(config *Config) {
48
-	if !config.Channels.Registration.Enabled {
44
+func (cm *ChannelManager) loadRegisteredChannels(config *Config) (err error) {
45
+	allChannels, err := FetchAndDeserializeAll[RegisteredChannel](datastore.TableChannels, cm.server.dstore, cm.server.logger)
46
+	if err != nil {
47
+		return
48
+	}
49
+	allPurgeRecords, err := FetchAndDeserializeAll[ChannelPurgeRecord](datastore.TableChannelPurges, cm.server.dstore, cm.server.logger)
50
+	if err != nil {
49 51
 		return
50 52
 	}
51
-
52
-	var newChannels []*Channel
53
-	var collisions []string
54
-	defer func() {
55
-		for _, ch := range newChannels {
56
-			ch.EnsureLoaded()
57
-			cm.server.logger.Debug("channels", "initialized registered channel", ch.Name())
58
-		}
59
-		for _, collision := range collisions {
60
-			cm.server.logger.Warning("channels", "registered channel collides with existing channel", collision)
61
-		}
62
-	}()
63
-
64
-	rawNames := cm.server.channelRegistry.AllChannels()
65 53
 
66 54
 	cm.Lock()
67 55
 	defer cm.Unlock()
68 56
 
69
-	cm.registeredChannels = make(utils.HashSet[string], len(rawNames))
70
-	cm.registeredSkeletons = make(utils.HashSet[string], len(rawNames))
71
-	for _, name := range rawNames {
72
-		cfname, err := CasefoldChannel(name)
73
-		if err == nil {
74
-			cm.registeredChannels.Add(cfname)
57
+	cm.purgedChannels = make(map[string]ChannelPurgeRecord, len(allPurgeRecords))
58
+	for _, purge := range allPurgeRecords {
59
+		cm.purgedChannels[purge.NameCasefolded] = purge
60
+	}
61
+
62
+	for _, regInfo := range allChannels {
63
+		cfname, err := CasefoldChannel(regInfo.Name)
64
+		if err != nil {
65
+			cm.server.logger.Error("channels", "couldn't casefold registered channel, skipping", regInfo.Name, err.Error())
66
+			continue
67
+		} else {
68
+			cm.server.logger.Debug("channels", "initializing registered channel", regInfo.Name)
75 69
 		}
76
-		skeleton, err := Skeleton(name)
70
+		skeleton, err := Skeleton(regInfo.Name)
77 71
 		if err == nil {
78
-			cm.registeredSkeletons.Add(skeleton)
72
+			cm.chansSkeletons.Add(skeleton)
79 73
 		}
80 74
 
81
-		if !cm.purgedChannels.Has(cfname) {
82
-			if _, ok := cm.chans[cfname]; !ok {
83
-				ch := NewChannel(cm.server, name, cfname, true)
84
-				cm.chans[cfname] = &channelManagerEntry{
85
-					channel:      ch,
86
-					pendingJoins: 0,
87
-				}
88
-				newChannels = append(newChannels, ch)
89
-			} else {
90
-				collisions = append(collisions, name)
75
+		if _, ok := cm.purgedChannels[cfname]; !ok {
76
+			ch := NewChannel(cm.server, regInfo.Name, cfname, true, regInfo)
77
+			cm.chans[cfname] = &channelManagerEntry{
78
+				channel:      ch,
79
+				pendingJoins: 0,
80
+				skeleton:     skeleton,
91 81
 			}
92 82
 		}
93 83
 	}
84
+
85
+	return nil
94 86
 }
95 87
 
96 88
 // Get returns an existing channel with name equivalent to `name`, or nil
97 89
 func (cm *ChannelManager) Get(name string) (channel *Channel) {
98 90
 	name, err := CasefoldChannel(name)
99
-	if err == nil {
100
-		cm.RLock()
101
-		defer cm.RUnlock()
102
-		entry := cm.chans[name]
103
-		// if the channel is still loading, pretend we don't have it
104
-		if entry != nil && entry.channel.IsLoaded() {
105
-			return entry.channel
106
-		}
91
+	if err != nil {
92
+		return nil
93
+	}
94
+	cm.RLock()
95
+	defer cm.RUnlock()
96
+	entry := cm.chans[name]
97
+	if entry != nil {
98
+		return entry.channel
107 99
 	}
108 100
 	return nil
109 101
 }
@@ -122,33 +114,26 @@ func (cm *ChannelManager) Join(client *Client, name string, key string, isSajoin
122 114
 		cm.Lock()
123 115
 		defer cm.Unlock()
124 116
 
125
-		if cm.purgedChannels.Has(casefoldedName) {
117
+		// check purges first; a registered purged channel will still be present in `chans`
118
+		if _, ok := cm.purgedChannels[casefoldedName]; ok {
126 119
 			return nil, errChannelPurged, false
127 120
 		}
128 121
 		entry := cm.chans[casefoldedName]
129 122
 		if entry == nil {
130
-			registered := cm.registeredChannels.Has(casefoldedName)
131
-			// enforce OpOnlyCreation
132
-			if !registered && server.Config().Channels.OpOnlyCreation &&
123
+			if server.Config().Channels.OpOnlyCreation &&
133 124
 				!(isSajoin || client.HasRoleCapabs("chanreg")) {
134 125
 				return nil, errInsufficientPrivs, false
135 126
 			}
136 127
 			// enforce confusables
137
-			if !registered && (cm.chansSkeletons.Has(skeleton) || cm.registeredSkeletons.Has(skeleton)) {
128
+			if cm.chansSkeletons.Has(skeleton) {
138 129
 				return nil, errConfusableIdentifier, false
139 130
 			}
140 131
 			entry = &channelManagerEntry{
141
-				channel:      NewChannel(server, name, casefoldedName, registered),
132
+				channel:      NewChannel(server, name, casefoldedName, false, RegisteredChannel{}),
142 133
 				pendingJoins: 0,
143 134
 			}
144
-			if !registered {
145
-				// for an unregistered channel, we already have the correct unfolded name
146
-				// and therefore the final skeleton. for a registered channel, we don't have
147
-				// the unfolded name yet (it needs to be loaded from the db), but we already
148
-				// have the final skeleton in `registeredSkeletons` so we don't need to track it
149
-				cm.chansSkeletons.Add(skeleton)
150
-				entry.skeleton = skeleton
151
-			}
135
+			cm.chansSkeletons.Add(skeleton)
136
+			entry.skeleton = skeleton
152 137
 			cm.chans[casefoldedName] = entry
153 138
 			newChannel = true
154 139
 		}
@@ -160,7 +145,6 @@ func (cm *ChannelManager) Join(client *Client, name string, key string, isSajoin
160 145
 		return err, ""
161 146
 	}
162 147
 
163
-	channel.EnsureLoaded()
164 148
 	err, forward = channel.Join(client, key, isSajoin || newChannel, rb)
165 149
 
166 150
 	cm.maybeCleanup(channel, true)
@@ -252,13 +236,6 @@ func (cm *ChannelManager) SetRegistered(channelName string, account string) (err
252 236
 	if err != nil {
253 237
 		return err
254 238
 	}
255
-	// transfer the skeleton from chansSkeletons to registeredSkeletons
256
-	skeleton := entry.skeleton
257
-	delete(cm.chansSkeletons, skeleton)
258
-	entry.skeleton = ""
259
-	cm.chans[cfname] = entry
260
-	cm.registeredChannels.Add(cfname)
261
-	cm.registeredSkeletons.Add(skeleton)
262 239
 	return nil
263 240
 }
264 241
 
@@ -268,17 +245,13 @@ func (cm *ChannelManager) SetUnregistered(channelName string, account string) (e
268 245
 		return err
269 246
 	}
270 247
 
271
-	info, err := cm.server.channelRegistry.LoadChannel(cfname)
272
-	if err != nil {
273
-		return err
274
-	}
275
-	if info.Founder != account {
276
-		return errChannelNotOwnedByAccount
277
-	}
248
+	var uuid utils.UUID
278 249
 
279 250
 	defer func() {
280 251
 		if err == nil {
281
-			err = cm.server.channelRegistry.Delete(info)
252
+			if delErr := cm.server.dstore.Delete(datastore.TableChannels, uuid); delErr != nil {
253
+				cm.server.logger.Error("datastore", "couldn't delete channel registration", cfname, delErr.Error())
254
+			}
282 255
 		}
283 256
 	}()
284 257
 
@@ -286,15 +259,11 @@ func (cm *ChannelManager) SetUnregistered(channelName string, account string) (e
286 259
 	defer cm.Unlock()
287 260
 	entry := cm.chans[cfname]
288 261
 	if entry != nil {
289
-		entry.channel.SetUnregistered(account)
290
-		delete(cm.registeredChannels, cfname)
291
-		// transfer the skeleton from registeredSkeletons to chansSkeletons
292
-		if skel, err := Skeleton(entry.channel.Name()); err == nil {
293
-			delete(cm.registeredSkeletons, skel)
294
-			cm.chansSkeletons.Add(skel)
295
-			entry.skeleton = skel
296
-			cm.chans[cfname] = entry
262
+		if entry.channel.Founder() != account {
263
+			return errChannelNotOwnedByAccount
297 264
 		}
265
+		uuid = entry.channel.UUID()
266
+		entry.channel.SetUnregistered(account) // changes the UUID
298 267
 		// #1619: if the channel has 0 members and was only being retained
299 268
 		// because it was registered, clean it up:
300 269
 		cm.maybeCleanupInternal(cfname, entry, false)
@@ -322,12 +291,11 @@ func (cm *ChannelManager) Rename(name string, newName string) (err error) {
322 291
 	var info RegisteredChannel
323 292
 	defer func() {
324 293
 		if channel != nil && info.Founder != "" {
325
-			channel.Store(IncludeAllAttrs)
326
-			if oldCfname != newCfname {
327
-				// we just flushed the channel under its new name, therefore this delete
328
-				// cannot be overwritten by a write to the old name:
329
-				cm.server.channelRegistry.Delete(info)
330
-			}
294
+			channel.MarkDirty(IncludeAllAttrs)
295
+		}
296
+		// always-on clients need to update their saved channel memberships
297
+		for _, member := range channel.Members() {
298
+			member.markDirty(IncludeChannels)
331 299
 		}
332 300
 	}()
333 301
 
@@ -335,11 +303,11 @@ func (cm *ChannelManager) Rename(name string, newName string) (err error) {
335 303
 	defer cm.Unlock()
336 304
 
337 305
 	entry := cm.chans[oldCfname]
338
-	if entry == nil || !entry.channel.IsLoaded() {
306
+	if entry == nil {
339 307
 		return errNoSuchChannel
340 308
 	}
341 309
 	channel = entry.channel
342
-	info = channel.ExportRegistration(IncludeInitial)
310
+	info = channel.ExportRegistration()
343 311
 	registered := info.Founder != ""
344 312
 
345 313
 	oldSkeleton, err := Skeleton(info.Name)
@@ -348,13 +316,13 @@ func (cm *ChannelManager) Rename(name string, newName string) (err error) {
348 316
 	}
349 317
 
350 318
 	if newCfname != oldCfname {
351
-		if cm.chans[newCfname] != nil || cm.registeredChannels.Has(newCfname) {
319
+		if cm.chans[newCfname] != nil {
352 320
 			return errChannelNameInUse
353 321
 		}
354 322
 	}
355 323
 
356 324
 	if oldSkeleton != newSkeleton {
357
-		if cm.chansSkeletons.Has(newSkeleton) || cm.registeredSkeletons.Has(newSkeleton) {
325
+		if cm.chansSkeletons.Has(newSkeleton) {
358 326
 			return errConfusableIdentifier
359 327
 		}
360 328
 	}
@@ -364,15 +332,8 @@ func (cm *ChannelManager) Rename(name string, newName string) (err error) {
364 332
 		entry.skeleton = newSkeleton
365 333
 	}
366 334
 	cm.chans[newCfname] = entry
367
-	if registered {
368
-		delete(cm.registeredChannels, oldCfname)
369
-		cm.registeredChannels.Add(newCfname)
370
-		delete(cm.registeredSkeletons, oldSkeleton)
371
-		cm.registeredSkeletons.Add(newSkeleton)
372
-	} else {
373
-		delete(cm.chansSkeletons, oldSkeleton)
374
-		cm.chansSkeletons.Add(newSkeleton)
375
-	}
335
+	delete(cm.chansSkeletons, oldSkeleton)
336
+	cm.chansSkeletons.Add(newSkeleton)
376 337
 	entry.channel.Rename(newName, newCfname)
377 338
 	return nil
378 339
 }
@@ -390,7 +351,18 @@ func (cm *ChannelManager) Channels() (result []*Channel) {
390 351
 	defer cm.RUnlock()
391 352
 	result = make([]*Channel, 0, len(cm.chans))
392 353
 	for _, entry := range cm.chans {
393
-		if entry.channel.IsLoaded() {
354
+		result = append(result, entry.channel)
355
+	}
356
+	return
357
+}
358
+
359
+// ListableChannels returns a slice of all non-purged channels.
360
+func (cm *ChannelManager) ListableChannels() (result []*Channel) {
361
+	cm.RLock()
362
+	defer cm.RUnlock()
363
+	result = make([]*Channel, 0, len(cm.chans))
364
+	for cfname, entry := range cm.chans {
365
+		if _, ok := cm.purgedChannels[cfname]; !ok {
394 366
 			result = append(result, entry.channel)
395 367
 		}
396 368
 	}
@@ -403,29 +375,46 @@ func (cm *ChannelManager) Purge(chname string, record ChannelPurgeRecord) (err e
403 375
 	if err != nil {
404 376
 		return errInvalidChannelName
405 377
 	}
406
-	skel, err := Skeleton(chname)
378
+
379
+	record.NameCasefolded = chname
380
+	record.UUID = utils.GenerateUUIDv4()
381
+
382
+	channel, err := func() (channel *Channel, err error) {
383
+		cm.Lock()
384
+		defer cm.Unlock()
385
+
386
+		if _, ok := cm.purgedChannels[chname]; ok {
387
+			return nil, errChannelPurgedAlready
388
+		}
389
+
390
+		entry := cm.chans[chname]
391
+		// atomically prevent anyone from rejoining
392
+		cm.purgedChannels[chname] = record
393
+		if entry != nil {
394
+			channel = entry.channel
395
+		}
396
+		return
397
+	}()
398
+
407 399
 	if err != nil {
408
-		return errInvalidChannelName
400
+		return err
409 401
 	}
410 402
 
411
-	cm.Lock()
412
-	cm.purgedChannels.Add(chname)
413
-	entry := cm.chans[chname]
414
-	if entry != nil {
415
-		delete(cm.chans, chname)
416
-		if entry.channel.Founder() != "" {
417
-			delete(cm.registeredSkeletons, skel)
418
-		} else {
419
-			delete(cm.chansSkeletons, skel)
420
-		}
403
+	if channel != nil {
404
+		// actually kick everyone off the channel
405
+		channel.Purge("")
421 406
 	}
422
-	cm.Unlock()
423 407
 
424
-	cm.server.channelRegistry.PurgeChannel(chname, record)
425
-	if entry != nil {
426
-		entry.channel.Purge("")
408
+	var purgeBytes []byte
409
+	if purgeBytes, err = record.Serialize(); err != nil {
410
+		cm.server.logger.Error("internal", "couldn't serialize purge record", channel.Name(), err.Error())
427 411
 	}
428
-	return nil
412
+	// TODO we need a better story about error handling for later
413
+	if err = cm.server.dstore.Set(datastore.TableChannelPurges, record.UUID, purgeBytes, time.Time{}); err != nil {
414
+		cm.server.logger.Error("datastore", "couldn't store purge record", chname, err.Error())
415
+	}
416
+
417
+	return
429 418
 }
430 419
 
431 420
 // IsPurged queries whether a channel is purged.
@@ -436,7 +425,7 @@ func (cm *ChannelManager) IsPurged(chname string) (result bool) {
436 425
 	}
437 426
 
438 427
 	cm.RLock()
439
-	result = cm.purgedChannels.Has(chname)
428
+	_, result = cm.purgedChannels[chname]
440 429
 	cm.RUnlock()
441 430
 	return
442 431
 }
@@ -449,14 +438,16 @@ func (cm *ChannelManager) Unpurge(chname string) (err error) {
449 438
 	}
450 439
 
451 440
 	cm.Lock()
452
-	found := cm.purgedChannels.Has(chname)
441
+	record, found := cm.purgedChannels[chname]
453 442
 	delete(cm.purgedChannels, chname)
454 443
 	cm.Unlock()
455 444
 
456
-	cm.server.channelRegistry.UnpurgeChannel(chname)
457 445
 	if !found {
458 446
 		return errNoSuchChannel
459 447
 	}
448
+	if err := cm.server.dstore.Delete(datastore.TableChannelPurges, record.UUID); err != nil {
449
+		cm.server.logger.Error("datastore", "couldn't delete purge record", chname, err.Error())
450
+	}
460 451
 	return nil
461 452
 }
462 453
 
@@ -475,8 +466,46 @@ func (cm *ChannelManager) UnfoldName(cfname string) (result string) {
475 466
 	cm.RLock()
476 467
 	entry := cm.chans[cfname]
477 468
 	cm.RUnlock()
478
-	if entry != nil && entry.channel.IsLoaded() {
469
+	if entry != nil {
479 470
 		return entry.channel.Name()
480 471
 	}
481 472
 	return cfname
482 473
 }
474
+
475
+func (cm *ChannelManager) LoadPurgeRecord(cfchname string) (record ChannelPurgeRecord, err error) {
476
+	cm.RLock()
477
+	defer cm.RUnlock()
478
+
479
+	if record, ok := cm.purgedChannels[cfchname]; ok {
480
+		return record, nil
481
+	} else {
482
+		return record, errNoSuchChannel
483
+	}
484
+}
485
+
486
+func (cm *ChannelManager) ChannelsForAccount(account string) (channels []string) {
487
+	cm.RLock()
488
+	defer cm.RUnlock()
489
+
490
+	for cfname, entry := range cm.chans {
491
+		if entry.channel.Founder() == account {
492
+			channels = append(channels, cfname)
493
+		}
494
+	}
495
+
496
+	return
497
+}
498
+
499
+// AllChannels returns the uncasefolded names of all registered channels.
500
+func (cm *ChannelManager) AllRegisteredChannels() (result []string) {
501
+	cm.RLock()
502
+	defer cm.RUnlock()
503
+
504
+	for cfname, entry := range cm.chans {
505
+		if entry.channel.Founder() != "" {
506
+			result = append(result, cfname)
507
+		}
508
+	}
509
+
510
+	return
511
+}

+ 16
- 359
irc/channelreg.go View File

@@ -5,13 +5,8 @@ package irc
5 5
 
6 6
 import (
7 7
 	"encoding/json"
8
-	"fmt"
9
-	"strconv"
10
-	"strings"
11 8
 	"time"
12 9
 
13
-	"github.com/tidwall/buntdb"
14
-
15 10
 	"github.com/ergochat/ergo/irc/modes"
16 11
 	"github.com/ergochat/ergo/irc/utils"
17 12
 )
@@ -19,48 +14,6 @@ import (
19 14
 // this is exclusively the *persistence* layer for channel registration;
20 15
 // channel creation/tracking/destruction is in channelmanager.go
21 16
 
22
-const (
23
-	keyChannelExists         = "channel.exists %s"
24
-	keyChannelName           = "channel.name %s" // stores the 'preferred name' of the channel, not casemapped
25
-	keyChannelRegTime        = "channel.registered.time %s"
26
-	keyChannelFounder        = "channel.founder %s"
27
-	keyChannelTopic          = "channel.topic %s"
28
-	keyChannelTopicSetBy     = "channel.topic.setby %s"
29
-	keyChannelTopicSetTime   = "channel.topic.settime %s"
30
-	keyChannelBanlist        = "channel.banlist %s"
31
-	keyChannelExceptlist     = "channel.exceptlist %s"
32
-	keyChannelInvitelist     = "channel.invitelist %s"
33
-	keyChannelPassword       = "channel.key %s"
34
-	keyChannelModes          = "channel.modes %s"
35
-	keyChannelAccountToUMode = "channel.accounttoumode %s"
36
-	keyChannelUserLimit      = "channel.userlimit %s"
37
-	keyChannelSettings       = "channel.settings %s"
38
-	keyChannelForward        = "channel.forward %s"
39
-
40
-	keyChannelPurged = "channel.purged %s"
41
-)
42
-
43
-var (
44
-	channelKeyStrings = []string{
45
-		keyChannelExists,
46
-		keyChannelName,
47
-		keyChannelRegTime,
48
-		keyChannelFounder,
49
-		keyChannelTopic,
50
-		keyChannelTopicSetBy,
51
-		keyChannelTopicSetTime,
52
-		keyChannelBanlist,
53
-		keyChannelExceptlist,
54
-		keyChannelInvitelist,
55
-		keyChannelPassword,
56
-		keyChannelModes,
57
-		keyChannelAccountToUMode,
58
-		keyChannelUserLimit,
59
-		keyChannelSettings,
60
-		keyChannelForward,
61
-	}
62
-)
63
-
64 17
 // these are bit flags indicating what part of the channel status is "dirty"
65 18
 // and needs to be read from memory and written to the db
66 19
 const (
@@ -80,8 +33,8 @@ const (
80 33
 type RegisteredChannel struct {
81 34
 	// Name of the channel.
82 35
 	Name string
83
-	// Casefolded name of the channel.
84
-	NameCasefolded string
36
+	// UUID for the datastore.
37
+	UUID utils.UUID
85 38
 	// RegisteredAt represents the time that the channel was registered.
86 39
 	RegisteredAt time.Time
87 40
 	// Founder indicates the founder of the channel.
@@ -112,322 +65,26 @@ type RegisteredChannel struct {
112 65
 	Settings ChannelSettings
113 66
 }
114 67
 
115
-type ChannelPurgeRecord struct {
116
-	Oper     string
117
-	PurgedAt time.Time
118
-	Reason   string
119
-}
120
-
121
-// ChannelRegistry manages registered channels.
122
-type ChannelRegistry struct {
123
-	server *Server
68
+func (r *RegisteredChannel) Serialize() ([]byte, error) {
69
+	return json.Marshal(r)
124 70
 }
125 71
 
126
-// NewChannelRegistry returns a new ChannelRegistry.
127
-func (reg *ChannelRegistry) Initialize(server *Server) {
128
-	reg.server = server
129
-}
130
-
131
-// AllChannels returns the uncasefolded names of all registered channels.
132
-func (reg *ChannelRegistry) AllChannels() (result []string) {
133
-	prefix := fmt.Sprintf(keyChannelName, "")
134
-	reg.server.store.View(func(tx *buntdb.Tx) error {
135
-		return tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool {
136
-			if !strings.HasPrefix(key, prefix) {
137
-				return false
138
-			}
139
-			result = append(result, value)
140
-			return true
141
-		})
142
-	})
143
-
144
-	return
72
+func (r *RegisteredChannel) Deserialize(b []byte) (err error) {
73
+	return json.Unmarshal(b, r)
145 74
 }
146 75
 
147
-// PurgedChannels returns the set of all casefolded channel names that have been purged
148
-func (reg *ChannelRegistry) PurgedChannels() (result utils.HashSet[string]) {
149
-	result = make(utils.HashSet[string])
150
-
151
-	prefix := fmt.Sprintf(keyChannelPurged, "")
152
-	reg.server.store.View(func(tx *buntdb.Tx) error {
153
-		return tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool {
154
-			if !strings.HasPrefix(key, prefix) {
155
-				return false
156
-			}
157
-			channel := strings.TrimPrefix(key, prefix)
158
-			result.Add(channel)
159
-			return true
160
-		})
161
-	})
162
-	return
163
-}
164
-
165
-// StoreChannel obtains a consistent view of a channel, then persists it to the store.
166
-func (reg *ChannelRegistry) StoreChannel(info RegisteredChannel, includeFlags uint) (err error) {
167
-	if !reg.server.ChannelRegistrationEnabled() {
168
-		return
169
-	}
170
-
171
-	if info.Founder == "" {
172
-		// sanity check, don't try to store an unregistered channel
173
-		return
174
-	}
175
-
176
-	reg.server.store.Update(func(tx *buntdb.Tx) error {
177
-		reg.saveChannel(tx, info, includeFlags)
178
-		return nil
179
-	})
180
-
181
-	return nil
182
-}
183
-
184
-// LoadChannel loads a channel from the store.
185
-func (reg *ChannelRegistry) LoadChannel(nameCasefolded string) (info RegisteredChannel, err error) {
186
-	if !reg.server.ChannelRegistrationEnabled() {
187
-		err = errFeatureDisabled
188
-		return
189
-	}
190
-
191
-	channelKey := nameCasefolded
192
-	// nice to have: do all JSON (de)serialization outside of the buntdb transaction
193
-	err = reg.server.store.View(func(tx *buntdb.Tx) error {
194
-		_, dberr := tx.Get(fmt.Sprintf(keyChannelExists, channelKey))
195
-		if dberr == buntdb.ErrNotFound {
196
-			// chan does not already exist, return
197
-			return errNoSuchChannel
198
-		}
199
-
200
-		// channel exists, load it
201
-		name, _ := tx.Get(fmt.Sprintf(keyChannelName, channelKey))
202
-		regTime, _ := tx.Get(fmt.Sprintf(keyChannelRegTime, channelKey))
203
-		regTimeInt, _ := strconv.ParseInt(regTime, 10, 64)
204
-		founder, _ := tx.Get(fmt.Sprintf(keyChannelFounder, channelKey))
205
-		topic, _ := tx.Get(fmt.Sprintf(keyChannelTopic, channelKey))
206
-		topicSetBy, _ := tx.Get(fmt.Sprintf(keyChannelTopicSetBy, channelKey))
207
-		var topicSetTime time.Time
208
-		topicSetTimeStr, _ := tx.Get(fmt.Sprintf(keyChannelTopicSetTime, channelKey))
209
-		if topicSetTimeInt, topicSetTimeErr := strconv.ParseInt(topicSetTimeStr, 10, 64); topicSetTimeErr == nil {
210
-			topicSetTime = time.Unix(0, topicSetTimeInt).UTC()
211
-		}
212
-		password, _ := tx.Get(fmt.Sprintf(keyChannelPassword, channelKey))
213
-		modeString, _ := tx.Get(fmt.Sprintf(keyChannelModes, channelKey))
214
-		userLimitString, _ := tx.Get(fmt.Sprintf(keyChannelUserLimit, channelKey))
215
-		forward, _ := tx.Get(fmt.Sprintf(keyChannelForward, channelKey))
216
-		banlistString, _ := tx.Get(fmt.Sprintf(keyChannelBanlist, channelKey))
217
-		exceptlistString, _ := tx.Get(fmt.Sprintf(keyChannelExceptlist, channelKey))
218
-		invitelistString, _ := tx.Get(fmt.Sprintf(keyChannelInvitelist, channelKey))
219
-		accountToUModeString, _ := tx.Get(fmt.Sprintf(keyChannelAccountToUMode, channelKey))
220
-		settingsString, _ := tx.Get(fmt.Sprintf(keyChannelSettings, channelKey))
221
-
222
-		modeSlice := make([]modes.Mode, len(modeString))
223
-		for i, mode := range modeString {
224
-			modeSlice[i] = modes.Mode(mode)
225
-		}
226
-
227
-		userLimit, _ := strconv.Atoi(userLimitString)
228
-
229
-		var banlist map[string]MaskInfo
230
-		_ = json.Unmarshal([]byte(banlistString), &banlist)
231
-		var exceptlist map[string]MaskInfo
232
-		_ = json.Unmarshal([]byte(exceptlistString), &exceptlist)
233
-		var invitelist map[string]MaskInfo
234
-		_ = json.Unmarshal([]byte(invitelistString), &invitelist)
235
-		accountToUMode := make(map[string]modes.Mode)
236
-		_ = json.Unmarshal([]byte(accountToUModeString), &accountToUMode)
237
-
238
-		var settings ChannelSettings
239
-		_ = json.Unmarshal([]byte(settingsString), &settings)
240
-
241
-		info = RegisteredChannel{
242
-			Name:           name,
243
-			NameCasefolded: nameCasefolded,
244
-			RegisteredAt:   time.Unix(0, regTimeInt).UTC(),
245
-			Founder:        founder,
246
-			Topic:          topic,
247
-			TopicSetBy:     topicSetBy,
248
-			TopicSetTime:   topicSetTime,
249
-			Key:            password,
250
-			Modes:          modeSlice,
251
-			Bans:           banlist,
252
-			Excepts:        exceptlist,
253
-			Invites:        invitelist,
254
-			AccountToUMode: accountToUMode,
255
-			UserLimit:      int(userLimit),
256
-			Settings:       settings,
257
-			Forward:        forward,
258
-		}
259
-		return nil
260
-	})
261
-
262
-	return
263
-}
264
-
265
-// Delete deletes a channel corresponding to `info`. If no such channel
266
-// is present in the database, no error is returned.
267
-func (reg *ChannelRegistry) Delete(info RegisteredChannel) (err error) {
268
-	if !reg.server.ChannelRegistrationEnabled() {
269
-		return
270
-	}
271
-
272
-	reg.server.store.Update(func(tx *buntdb.Tx) error {
273
-		reg.deleteChannel(tx, info.NameCasefolded, info)
274
-		return nil
275
-	})
276
-	return nil
277
-}
278
-
279
-// delete a channel, unless it was overwritten by another registration of the same channel
280
-func (reg *ChannelRegistry) deleteChannel(tx *buntdb.Tx, key string, info RegisteredChannel) {
281
-	_, err := tx.Get(fmt.Sprintf(keyChannelExists, key))
282
-	if err == nil {
283
-		regTime, _ := tx.Get(fmt.Sprintf(keyChannelRegTime, key))
284
-		regTimeInt, _ := strconv.ParseInt(regTime, 10, 64)
285
-		registeredAt := time.Unix(0, regTimeInt).UTC()
286
-		founder, _ := tx.Get(fmt.Sprintf(keyChannelFounder, key))
287
-
288
-		// to see if we're deleting the right channel, confirm the founder and the registration time
289
-		if founder == info.Founder && registeredAt.Equal(info.RegisteredAt) {
290
-			for _, keyFmt := range channelKeyStrings {
291
-				tx.Delete(fmt.Sprintf(keyFmt, key))
292
-			}
293
-
294
-			// remove this channel from the client's list of registered channels
295
-			channelsKey := fmt.Sprintf(keyAccountChannels, info.Founder)
296
-			channelsStr, err := tx.Get(channelsKey)
297
-			if err == buntdb.ErrNotFound {
298
-				return
299
-			}
300
-			registeredChannels := unmarshalRegisteredChannels(channelsStr)
301
-			var nowRegisteredChannels []string
302
-			for _, channel := range registeredChannels {
303
-				if channel != key {
304
-					nowRegisteredChannels = append(nowRegisteredChannels, channel)
305
-				}
306
-			}
307
-			tx.Set(channelsKey, strings.Join(nowRegisteredChannels, ","), nil)
308
-		}
309
-	}
310
-}
311
-
312
-func (reg *ChannelRegistry) updateAccountToChannelMapping(tx *buntdb.Tx, channelInfo RegisteredChannel) {
313
-	channelKey := channelInfo.NameCasefolded
314
-	chanFounderKey := fmt.Sprintf(keyChannelFounder, channelKey)
315
-	founder, existsErr := tx.Get(chanFounderKey)
316
-	if existsErr == buntdb.ErrNotFound || founder != channelInfo.Founder {
317
-		// add to new founder's list
318
-		accountChannelsKey := fmt.Sprintf(keyAccountChannels, channelInfo.Founder)
319
-		alreadyChannels, _ := tx.Get(accountChannelsKey)
320
-		newChannels := channelKey // this is the casefolded channel name
321
-		if alreadyChannels != "" {
322
-			newChannels = fmt.Sprintf("%s,%s", alreadyChannels, newChannels)
323
-		}
324
-		tx.Set(accountChannelsKey, newChannels, nil)
325
-	}
326
-	if existsErr == nil && founder != channelInfo.Founder {
327
-		// remove from old founder's list
328
-		accountChannelsKey := fmt.Sprintf(keyAccountChannels, founder)
329
-		alreadyChannelsRaw, _ := tx.Get(accountChannelsKey)
330
-		var newChannels []string
331
-		if alreadyChannelsRaw != "" {
332
-			for _, chname := range strings.Split(alreadyChannelsRaw, ",") {
333
-				if chname != channelInfo.NameCasefolded {
334
-					newChannels = append(newChannels, chname)
335
-				}
336
-			}
337
-		}
338
-		tx.Set(accountChannelsKey, strings.Join(newChannels, ","), nil)
339
-	}
340
-}
341
-
342
-// saveChannel saves a channel to the store.
343
-func (reg *ChannelRegistry) saveChannel(tx *buntdb.Tx, channelInfo RegisteredChannel, includeFlags uint) {
344
-	channelKey := channelInfo.NameCasefolded
345
-	// maintain the mapping of account -> registered channels
346
-	reg.updateAccountToChannelMapping(tx, channelInfo)
347
-
348
-	if includeFlags&IncludeInitial != 0 {
349
-		tx.Set(fmt.Sprintf(keyChannelExists, channelKey), "1", nil)
350
-		tx.Set(fmt.Sprintf(keyChannelName, channelKey), channelInfo.Name, nil)
351
-		tx.Set(fmt.Sprintf(keyChannelRegTime, channelKey), strconv.FormatInt(channelInfo.RegisteredAt.UnixNano(), 10), nil)
352
-		tx.Set(fmt.Sprintf(keyChannelFounder, channelKey), channelInfo.Founder, nil)
353
-	}
354
-
355
-	if includeFlags&IncludeTopic != 0 {
356
-		tx.Set(fmt.Sprintf(keyChannelTopic, channelKey), channelInfo.Topic, nil)
357
-		var topicSetTimeStr string
358
-		if !channelInfo.TopicSetTime.IsZero() {
359
-			topicSetTimeStr = strconv.FormatInt(channelInfo.TopicSetTime.UnixNano(), 10)
360
-		}
361
-		tx.Set(fmt.Sprintf(keyChannelTopicSetTime, channelKey), topicSetTimeStr, nil)
362
-		tx.Set(fmt.Sprintf(keyChannelTopicSetBy, channelKey), channelInfo.TopicSetBy, nil)
363
-	}
364
-
365
-	if includeFlags&IncludeModes != 0 {
366
-		tx.Set(fmt.Sprintf(keyChannelPassword, channelKey), channelInfo.Key, nil)
367
-		modeString := modes.Modes(channelInfo.Modes).String()
368
-		tx.Set(fmt.Sprintf(keyChannelModes, channelKey), modeString, nil)
369
-		tx.Set(fmt.Sprintf(keyChannelUserLimit, channelKey), strconv.Itoa(channelInfo.UserLimit), nil)
370
-		tx.Set(fmt.Sprintf(keyChannelForward, channelKey), channelInfo.Forward, nil)
371
-	}
372
-
373
-	if includeFlags&IncludeLists != 0 {
374
-		banlistString, _ := json.Marshal(channelInfo.Bans)
375
-		tx.Set(fmt.Sprintf(keyChannelBanlist, channelKey), string(banlistString), nil)
376
-		exceptlistString, _ := json.Marshal(channelInfo.Excepts)
377
-		tx.Set(fmt.Sprintf(keyChannelExceptlist, channelKey), string(exceptlistString), nil)
378
-		invitelistString, _ := json.Marshal(channelInfo.Invites)
379
-		tx.Set(fmt.Sprintf(keyChannelInvitelist, channelKey), string(invitelistString), nil)
380
-		accountToUModeString, _ := json.Marshal(channelInfo.AccountToUMode)
381
-		tx.Set(fmt.Sprintf(keyChannelAccountToUMode, channelKey), string(accountToUModeString), nil)
382
-	}
383
-
384
-	if includeFlags&IncludeSettings != 0 {
385
-		settingsString, _ := json.Marshal(channelInfo.Settings)
386
-		tx.Set(fmt.Sprintf(keyChannelSettings, channelKey), string(settingsString), nil)
387
-	}
388
-}
389
-
390
-// PurgeChannel records a channel purge.
391
-func (reg *ChannelRegistry) PurgeChannel(chname string, record ChannelPurgeRecord) (err error) {
392
-	serialized, err := json.Marshal(record)
393
-	if err != nil {
394
-		return err
395
-	}
396
-	serializedStr := string(serialized)
397
-	key := fmt.Sprintf(keyChannelPurged, chname)
398
-
399
-	return reg.server.store.Update(func(tx *buntdb.Tx) error {
400
-		tx.Set(key, serializedStr, nil)
401
-		return nil
402
-	})
76
+type ChannelPurgeRecord struct {
77
+	NameCasefolded string `json:"Name"`
78
+	UUID           utils.UUID
79
+	Oper           string
80
+	PurgedAt       time.Time
81
+	Reason         string
403 82
 }
404 83
 
405
-// LoadPurgeRecord retrieves information about whether and how a channel was purged.
406
-func (reg *ChannelRegistry) LoadPurgeRecord(chname string) (record ChannelPurgeRecord, err error) {
407
-	var rawRecord string
408
-	key := fmt.Sprintf(keyChannelPurged, chname)
409
-	reg.server.store.View(func(tx *buntdb.Tx) error {
410
-		rawRecord, _ = tx.Get(key)
411
-		return nil
412
-	})
413
-	if rawRecord == "" {
414
-		err = errNoSuchChannel
415
-		return
416
-	}
417
-	err = json.Unmarshal([]byte(rawRecord), &record)
418
-	if err != nil {
419
-		reg.server.logger.Error("internal", "corrupt purge record", chname, err.Error())
420
-		err = errNoSuchChannel
421
-		return
422
-	}
423
-	return
84
+func (c *ChannelPurgeRecord) Serialize() ([]byte, error) {
85
+	return json.Marshal(c)
424 86
 }
425 87
 
426
-// UnpurgeChannel deletes the record of a channel purge.
427
-func (reg *ChannelRegistry) UnpurgeChannel(chname string) (err error) {
428
-	key := fmt.Sprintf(keyChannelPurged, chname)
429
-	return reg.server.store.Update(func(tx *buntdb.Tx) error {
430
-		tx.Delete(key)
431
-		return nil
432
-	})
88
+func (c *ChannelPurgeRecord) Deserialize(b []byte) error {
89
+	return json.Unmarshal(b, c)
433 90
 }

+ 12
- 23
irc/chanserv.go View File

@@ -459,7 +459,7 @@ func csRegisterHandler(service *ircService, server *Server, client *Client, comm
459 459
 // check whether a client has already registered too many channels
460 460
 func checkChanLimit(service *ircService, client *Client, rb *ResponseBuffer) (ok bool) {
461 461
 	account := client.Account()
462
-	channelsAlreadyRegistered := client.server.accounts.ChannelsForAccount(account)
462
+	channelsAlreadyRegistered := client.server.channels.ChannelsForAccount(account)
463 463
 	ok = len(channelsAlreadyRegistered) < client.server.Config().Channels.Registration.MaxChannelsPerAccount || client.HasRoleCapabs("chanreg")
464 464
 	if !ok {
465 465
 		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
496 496
 		return
497 497
 	}
498 498
 
499
-	info := channel.ExportRegistration(0)
500
-	channelKey := info.NameCasefolded
499
+	info := channel.exportSummary()
500
+	channelKey := channel.NameCasefolded()
501 501
 	if !csPrivsCheck(service, info, client, rb) {
502 502
 		return
503 503
 	}
@@ -519,7 +519,7 @@ func csClearHandler(service *ircService, server *Server, client *Client, command
519 519
 		service.Notice(rb, client.t("Channel does not exist"))
520 520
 		return
521 521
 	}
522
-	if !csPrivsCheck(service, channel.ExportRegistration(0), client, rb) {
522
+	if !csPrivsCheck(service, channel.exportSummary(), client, rb) {
523 523
 		return
524 524
 	}
525 525
 
@@ -550,7 +550,7 @@ func csTransferHandler(service *ircService, server *Server, client *Client, comm
550 550
 		service.Notice(rb, client.t("Channel does not exist"))
551 551
 		return
552 552
 	}
553
-	regInfo := channel.ExportRegistration(0)
553
+	regInfo := channel.exportSummary()
554 554
 	chname = regInfo.Name
555 555
 	account := client.Account()
556 556
 	isFounder := account != "" && account == regInfo.Founder
@@ -729,11 +729,6 @@ func csPurgeListHandler(service *ircService, client *Client, rb *ResponseBuffer)
729 729
 }
730 730
 
731 731
 func csListHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
732
-	if !client.HasRoleCapabs("chanreg") {
733
-		service.Notice(rb, client.t("Insufficient privileges"))
734
-		return
735
-	}
736
-
737 732
 	var searchRegex *regexp.Regexp
738 733
 	if len(params) > 0 {
739 734
 		var err error
@@ -746,7 +741,7 @@ func csListHandler(service *ircService, server *Server, client *Client, command
746 741
 
747 742
 	service.Notice(rb, ircfmt.Unescape(client.t("*** $bChanServ LIST$b ***")))
748 743
 
749
-	channels := server.channelRegistry.AllChannels()
744
+	channels := server.channels.AllRegisteredChannels()
750 745
 	for _, channel := range channels {
751 746
 		if searchRegex == nil || searchRegex.MatchString(channel) {
752 747
 			service.Notice(rb, fmt.Sprintf("    %s", channel))
@@ -771,7 +766,7 @@ func csInfoHandler(service *ircService, server *Server, client *Client, command
771 766
 
772 767
 	// purge status
773 768
 	if client.HasRoleCapabs("chanreg") {
774
-		purgeRecord, err := server.channelRegistry.LoadPurgeRecord(chname)
769
+		purgeRecord, err := server.channels.LoadPurgeRecord(chname)
775 770
 		if err == nil {
776 771
 			service.Notice(rb, fmt.Sprintf(client.t("Channel %s was purged by the server operators and cannot be used"), chname))
777 772
 			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
789 784
 	var chinfo RegisteredChannel
790 785
 	channel := server.channels.Get(params[0])
791 786
 	if channel != nil {
792
-		chinfo = channel.ExportRegistration(0)
793
-	} else {
794
-		chinfo, err = server.channelRegistry.LoadChannel(chname)
795
-		if err != nil && !(err == errNoSuchChannel || err == errFeatureDisabled) {
796
-			service.Notice(rb, client.t("An error occurred"))
797
-			return
798
-		}
787
+		chinfo = channel.exportSummary()
799 788
 	}
800 789
 
801 790
 	// channel exists but is unregistered, or doesn't exist:
@@ -835,12 +824,12 @@ func csGetHandler(service *ircService, server *Server, client *Client, command s
835 824
 		service.Notice(rb, client.t("No such channel"))
836 825
 		return
837 826
 	}
838
-	info := channel.ExportRegistration(IncludeSettings)
827
+	info := channel.exportSummary()
839 828
 	if !csPrivsCheck(service, info, client, rb) {
840 829
 		return
841 830
 	}
842 831
 
843
-	displayChannelSetting(service, setting, info.Settings, client, rb)
832
+	displayChannelSetting(service, setting, channel.Settings(), client, rb)
844 833
 }
845 834
 
846 835
 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
850 839
 		service.Notice(rb, client.t("No such channel"))
851 840
 		return
852 841
 	}
853
-	info := channel.ExportRegistration(IncludeSettings)
854
-	settings := info.Settings
842
+	info := channel.exportSummary()
855 843
 	if !csPrivsCheck(service, info, client, rb) {
856 844
 		return
857 845
 	}
858 846
 
847
+	settings := channel.Settings()
859 848
 	var err error
860 849
 	switch strings.ToLower(setting) {
861 850
 	case "history":

+ 133
- 22
irc/database.go View File

@@ -14,6 +14,8 @@ import (
14 14
 	"strings"
15 15
 	"time"
16 16
 
17
+	"github.com/ergochat/ergo/irc/bunt"
18
+	"github.com/ergochat/ergo/irc/datastore"
17 19
 	"github.com/ergochat/ergo/irc/modes"
18 20
 	"github.com/ergochat/ergo/irc/utils"
19 21
 
@@ -21,12 +23,19 @@ import (
21 23
 )
22 24
 
23 25
 const (
26
+	// TODO migrate metadata keys as well
27
+
24 28
 	// 'version' of the database schema
25
-	keySchemaVersion = "db.version"
26 29
 	// latest schema of the db
27
-	latestDbSchema = 22
30
+	latestDbSchema = 23
31
+)
28 32
 
29
-	keyCloakSecret = "crypto.cloak_secret"
33
+var (
34
+	schemaVersionUUID = utils.UUID{0, 255, 85, 13, 212, 10, 191, 121, 245, 152, 142, 89, 97, 141, 219, 87}    // AP9VDdQKv3n1mI5ZYY3bVw
35
+	cloakSecretUUID   = utils.UUID{170, 214, 184, 208, 116, 181, 67, 75, 161, 23, 233, 16, 113, 251, 94, 229} // qta40HS1Q0uhF-kQcfte5Q
36
+
37
+	keySchemaVersion = bunt.BuntKey(datastore.TableMetadata, schemaVersionUUID)
38
+	keyCloakSecret   = bunt.BuntKey(datastore.TableMetadata, cloakSecretUUID)
30 39
 )
31 40
 
32 41
 type SchemaChanger func(*Config, *buntdb.Tx) error
@@ -99,10 +108,7 @@ func openDatabaseInternal(config *Config, allowAutoupgrade bool) (db *buntdb.DB,
99 108
 	// read the current version string
100 109
 	var version int
101 110
 	err = db.View(func(tx *buntdb.Tx) (err error) {
102
-		vStr, err := tx.Get(keySchemaVersion)
103
-		if err == nil {
104
-			version, err = strconv.Atoi(vStr)
105
-		}
111
+		version, err = retrieveSchemaVersion(tx)
106 112
 		return err
107 113
 	})
108 114
 	if err != nil {
@@ -130,6 +136,17 @@ func openDatabaseInternal(config *Config, allowAutoupgrade bool) (db *buntdb.DB,
130 136
 	}
131 137
 }
132 138
 
139
+func retrieveSchemaVersion(tx *buntdb.Tx) (version int, err error) {
140
+	if val, err := tx.Get(keySchemaVersion); err == nil {
141
+		return strconv.Atoi(val)
142
+	}
143
+	// legacy key:
144
+	if val, err := tx.Get("db.version"); err == nil {
145
+		return strconv.Atoi(val)
146
+	}
147
+	return 0, buntdb.ErrNotFound
148
+}
149
+
133 150
 func performAutoUpgrade(currentVersion int, config *Config) (err error) {
134 151
 	path := config.Datastore.Path
135 152
 	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) {
167 184
 	var version int
168 185
 	err = store.Update(func(tx *buntdb.Tx) error {
169 186
 		for {
170
-			vStr, _ := tx.Get(keySchemaVersion)
171
-			version, _ = strconv.Atoi(vStr)
187
+			if version == 0 {
188
+				version, err = retrieveSchemaVersion(tx)
189
+				if err != nil {
190
+					return err
191
+				}
192
+			}
172 193
 			if version == latestDbSchema {
173 194
 				// success!
174 195
 				break
@@ -183,11 +204,12 @@ func UpgradeDB(config *Config) (err error) {
183 204
 			if err != nil {
184 205
 				return err
185 206
 			}
186
-			_, _, err = tx.Set(keySchemaVersion, strconv.Itoa(change.TargetVersion), nil)
207
+			version = change.TargetVersion
208
+			_, _, err = tx.Set(keySchemaVersion, strconv.Itoa(version), nil)
187 209
 			if err != nil {
188 210
 				return err
189 211
 			}
190
-			log.Printf("successfully updated schema to version %d\n", change.TargetVersion)
212
+			log.Printf("successfully updated schema to version %d\n", version)
191 213
 		}
192 214
 		return nil
193 215
 	})
@@ -198,19 +220,17 @@ func UpgradeDB(config *Config) (err error) {
198 220
 	return err
199 221
 }
200 222
 
201
-func LoadCloakSecret(db *buntdb.DB) (result string) {
202
-	db.View(func(tx *buntdb.Tx) error {
203
-		result, _ = tx.Get(keyCloakSecret)
204
-		return nil
205
-	})
206
-	return
223
+func LoadCloakSecret(dstore datastore.Datastore) (result string, err error) {
224
+	val, err := dstore.Get(datastore.TableMetadata, cloakSecretUUID)
225
+	if err != nil {
226
+		return
227
+	}
228
+	return string(val), nil
207 229
 }
208 230
 
209
-func StoreCloakSecret(db *buntdb.DB, secret string) {
210
-	db.Update(func(tx *buntdb.Tx) error {
211
-		tx.Set(keyCloakSecret, secret, nil)
212
-		return nil
213
-	})
231
+func StoreCloakSecret(dstore datastore.Datastore, secret string) {
232
+	// TODO error checking
233
+	dstore.Set(datastore.TableMetadata, cloakSecretUUID, []byte(secret), time.Time{})
214 234
 }
215 235
 
216 236
 func schemaChangeV1toV2(config *Config, tx *buntdb.Tx) error {
@@ -1112,6 +1132,92 @@ func schemaChangeV21To22(config *Config, tx *buntdb.Tx) error {
1112 1132
 	return nil
1113 1133
 }
1114 1134
 
1135
+// first phase of document-oriented database refactor: channels
1136
+func schemaChangeV22ToV23(config *Config, tx *buntdb.Tx) error {
1137
+	keyChannelExists := "channel.exists "
1138
+	var channelNames []string
1139
+	tx.AscendGreaterOrEqual("", keyChannelExists, func(key, value string) bool {
1140
+		if !strings.HasPrefix(key, keyChannelExists) {
1141
+			return false
1142
+		}
1143
+		channelNames = append(channelNames, strings.TrimPrefix(key, keyChannelExists))
1144
+		return true
1145
+	})
1146
+	for _, channelName := range channelNames {
1147
+		channel, err := loadLegacyChannel(tx, channelName)
1148
+		if err != nil {
1149
+			log.Printf("error loading legacy channel %s: %v", channelName, err)
1150
+			continue
1151
+		}
1152
+		channel.UUID = utils.GenerateUUIDv4()
1153
+		newKey := bunt.BuntKey(datastore.TableChannels, channel.UUID)
1154
+		j, err := json.Marshal(channel)
1155
+		if err != nil {
1156
+			log.Printf("error marshaling channel %s: %v", channelName, err)
1157
+			continue
1158
+		}
1159
+		tx.Set(newKey, string(j), nil)
1160
+		deleteLegacyChannel(tx, channelName)
1161
+	}
1162
+
1163
+	// purges
1164
+	keyChannelPurged := "channel.purged "
1165
+	var purgeKeys []string
1166
+	var channelPurges []ChannelPurgeRecord
1167
+	tx.AscendGreaterOrEqual("", keyChannelPurged, func(key, value string) bool {
1168
+		if !strings.HasPrefix(key, keyChannelPurged) {
1169
+			return false
1170
+		}
1171
+		purgeKeys = append(purgeKeys, key)
1172
+		cfname := strings.TrimPrefix(key, keyChannelPurged)
1173
+		var record ChannelPurgeRecord
1174
+		err := json.Unmarshal([]byte(value), &record)
1175
+		if err != nil {
1176
+			log.Printf("error unmarshaling channel purge for %s: %v", cfname, err)
1177
+			return true
1178
+		}
1179
+		record.NameCasefolded = cfname
1180
+		record.UUID = utils.GenerateUUIDv4()
1181
+		channelPurges = append(channelPurges, record)
1182
+		return true
1183
+	})
1184
+	for _, record := range channelPurges {
1185
+		newKey := bunt.BuntKey(datastore.TableChannelPurges, record.UUID)
1186
+		j, err := json.Marshal(record)
1187
+		if err != nil {
1188
+			log.Printf("error marshaling channel purge %s: %v", record.NameCasefolded, err)
1189
+			continue
1190
+		}
1191
+		tx.Set(newKey, string(j), nil)
1192
+	}
1193
+	for _, purgeKey := range purgeKeys {
1194
+		tx.Delete(purgeKey)
1195
+	}
1196
+
1197
+	// clean up denormalized account-to-channels mapping
1198
+	keyAccountChannels := "account.channels "
1199
+	var accountToChannels []string
1200
+	tx.AscendGreaterOrEqual("", keyAccountChannels, func(key, value string) bool {
1201
+		if !strings.HasPrefix(key, keyAccountChannels) {
1202
+			return false
1203
+		}
1204
+		accountToChannels = append(accountToChannels, key)
1205
+		return true
1206
+	})
1207
+	for _, key := range accountToChannels {
1208
+		tx.Delete(key)
1209
+	}
1210
+
1211
+	// migrate cloak secret
1212
+	val, _ := tx.Get("crypto.cloak_secret")
1213
+	tx.Set(keyCloakSecret, val, nil)
1214
+
1215
+	// bump the legacy version key to mark the database as downgrade-incompatible
1216
+	tx.Set("db.version", "23", nil)
1217
+
1218
+	return nil
1219
+}
1220
+
1115 1221
 func getSchemaChange(initialVersion int) (result SchemaChange, ok bool) {
1116 1222
 	for _, change := range allChanges {
1117 1223
 		if initialVersion == change.InitialVersion {
@@ -1227,4 +1333,9 @@ var allChanges = []SchemaChange{
1227 1333
 		TargetVersion:  22,
1228 1334
 		Changer:        schemaChangeV21To22,
1229 1335
 	},
1336
+	{
1337
+		InitialVersion: 22,
1338
+		TargetVersion:  23,
1339
+		Changer:        schemaChangeV22ToV23,
1340
+	},
1230 1341
 }

+ 45
- 0
irc/datastore/datastore.go View File

@@ -0,0 +1,45 @@
1
+// Copyright (c) 2022 Shivaram Lingamneni <slingamn@cs.stanford.edu>
2
+// released under the MIT license
3
+
4
+package datastore
5
+
6
+import (
7
+	"time"
8
+
9
+	"github.com/ergochat/ergo/irc/utils"
10
+)
11
+
12
+type Table uint16
13
+
14
+// XXX these are persisted and must remain stable;
15
+// do not reorder, when deleting use _ to ensure that the deleted value is skipped
16
+const (
17
+	TableMetadata Table = iota
18
+	TableChannels
19
+	TableChannelPurges
20
+)
21
+
22
+type KV struct {
23
+	UUID  utils.UUID
24
+	Value []byte
25
+}
26
+
27
+// A Datastore provides the following abstraction:
28
+// 1. Tables, each keyed on a UUID (the implementation is free to merge
29
+// the table name and the UUID into a single key as long as the rest of
30
+// the contract can be satisfied). Table names are [a-z0-9_]+
31
+// 2. The ability to efficiently enumerate all uuid-value pairs in a table
32
+// 3. Gets, sets, and deletes for individual (table, uuid) keys
33
+type Datastore interface {
34
+	Backoff() time.Duration
35
+
36
+	GetAll(table Table) ([]KV, error)
37
+
38
+	// This is rarely used because it would typically lead to TOCTOU races
39
+	Get(table Table, key utils.UUID) (value []byte, err error)
40
+
41
+	Set(table Table, key utils.UUID, value []byte, expiration time.Time) error
42
+
43
+	// Note that deleting a nonexistent key is not considered an error
44
+	Delete(table Table, key utils.UUID) error
45
+}

+ 1
- 0
irc/errors.go View File

@@ -51,6 +51,7 @@ var (
51 51
 	errNoExistingBan                  = errors.New("Ban does not exist")
52 52
 	errNoSuchChannel                  = errors.New(`No such channel`)
53 53
 	errChannelPurged                  = errors.New(`This channel was purged by the server operators and cannot be used`)
54
+	errChannelPurgedAlready           = errors.New(`This channel was already purged and cannot be purged again`)
54 55
 	errConfusableIdentifier           = errors.New("This identifier is confusable with one already in use")
55 56
 	errInsufficientPrivs              = errors.New("Insufficient privileges")
56 57
 	errInvalidUsername                = errors.New("Invalid username")

+ 6
- 0
irc/getters.go View File

@@ -638,3 +638,9 @@ func (channel *Channel) getAmode(cfaccount string) (result modes.Mode) {
638 638
 	defer channel.stateMutex.RUnlock()
639 639
 	return channel.accountToUMode[cfaccount]
640 640
 }
641
+
642
+func (channel *Channel) UUID() utils.UUID {
643
+	channel.stateMutex.RLock()
644
+	defer channel.stateMutex.RUnlock()
645
+	return channel.uuid
646
+}

+ 1
- 1
irc/handlers.go View File

@@ -1718,7 +1718,7 @@ func listHandler(server *Server, client *Client, msg ircmsg.Message, rb *Respons
1718 1718
 
1719 1719
 	clientIsOp := client.HasRoleCapabs("sajoin")
1720 1720
 	if len(channels) == 0 {
1721
-		for _, channel := range server.channels.Channels() {
1721
+		for _, channel := range server.channels.ListableChannels() {
1722 1722
 			if !clientIsOp && channel.flags.HasMode(modes.Secret) && !channel.hasClient(client) {
1723 1723
 				continue
1724 1724
 			}

+ 1
- 1
irc/hostserv.go View File

@@ -193,6 +193,6 @@ func hsSetCloakSecretHandler(service *ircService, server *Server, client *Client
193 193
 		service.Notice(rb, fmt.Sprintf(client.t("To confirm, run this command: %s"), fmt.Sprintf("/HS SETCLOAKSECRET %s %s", secret, expectedCode)))
194 194
 		return
195 195
 	}
196
-	StoreCloakSecret(server.store, secret)
196
+	StoreCloakSecret(server.dstore, secret)
197 197
 	service.Notice(rb, client.t("Rotated the cloak secret; you must rehash or restart the server for it to take effect"))
198 198
 }

+ 32
- 29
irc/import.go View File

@@ -9,9 +9,13 @@ import (
9 9
 	"log"
10 10
 	"os"
11 11
 	"strconv"
12
+	"time"
12 13
 
13 14
 	"github.com/tidwall/buntdb"
14 15
 
16
+	"github.com/ergochat/ergo/irc/bunt"
17
+	"github.com/ergochat/ergo/irc/datastore"
18
+	"github.com/ergochat/ergo/irc/modes"
15 19
 	"github.com/ergochat/ergo/irc/utils"
16 20
 )
17 21
 
@@ -20,7 +24,7 @@ const (
20 24
 	// XXX instead of referencing, e.g., keyAccountExists, we should write in the string literal
21 25
 	// (to ensure that no matter what code changes happen elsewhere, we're still producing a
22 26
 	// db of the hardcoded version)
23
-	importDBSchemaVersion = 22
27
+	importDBSchemaVersion = 23
24 28
 )
25 29
 
26 30
 type userImport struct {
@@ -54,8 +58,8 @@ type databaseImport struct {
54 58
 	Channels map[string]channelImport
55 59
 }
56 60
 
57
-func serializeAmodes(raw map[string]string, validCfUsernames utils.HashSet[string]) (result []byte, err error) {
58
-	processed := make(map[string]int, len(raw))
61
+func convertAmodes(raw map[string]string, validCfUsernames utils.HashSet[string]) (result map[string]modes.Mode, err error) {
62
+	result = make(map[string]modes.Mode)
59 63
 	for accountName, mode := range raw {
60 64
 		if len(mode) != 1 {
61 65
 			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
64 68
 		if err != nil || !validCfUsernames.Has(cfname) {
65 69
 			log.Printf("skipping invalid amode recipient %s\n", accountName)
66 70
 		} else {
67
-			processed[cfname] = int(mode[0])
71
+			result[cfname] = modes.Mode(mode[0])
68 72
 		}
69 73
 	}
70
-	result, err = json.Marshal(processed)
71 74
 	return
72 75
 }
73 76
 
@@ -147,8 +150,9 @@ func doImportDBGeneric(config *Config, dbImport databaseImport, credsType Creden
147 150
 		cfUsernames.Add(cfUsername)
148 151
 	}
149 152
 
153
+	// TODO fix this:
150 154
 	for chname, chInfo := range dbImport.Channels {
151
-		cfchname, err := CasefoldChannel(chname)
155
+		_, err := CasefoldChannel(chname)
152 156
 		if err != nil {
153 157
 			log.Printf("invalid channel name %s: %v", chname, err)
154 158
 			continue
@@ -158,43 +162,42 @@ func doImportDBGeneric(config *Config, dbImport databaseImport, credsType Creden
158 162
 			log.Printf("invalid founder %s for channel %s: %v", chInfo.Founder, chname, err)
159 163
 			continue
160 164
 		}
161
-		tx.Set(fmt.Sprintf(keyChannelExists, cfchname), "1", nil)
162
-		tx.Set(fmt.Sprintf(keyChannelName, cfchname), chname, nil)
163
-		tx.Set(fmt.Sprintf(keyChannelRegTime, cfchname), strconv.FormatInt(chInfo.RegisteredAt, 10), nil)
164
-		tx.Set(fmt.Sprintf(keyChannelFounder, cfchname), cffounder, nil)
165
-		accountChannelsKey := fmt.Sprintf(keyAccountChannels, cffounder)
166
-		founderChannels, fcErr := tx.Get(accountChannelsKey)
167
-		if fcErr != nil || founderChannels == "" {
168
-			founderChannels = cfchname
169
-		} else {
170
-			founderChannels = fmt.Sprintf("%s,%s", founderChannels, cfchname)
171
-		}
172
-		tx.Set(accountChannelsKey, founderChannels, nil)
165
+		var regInfo RegisteredChannel
166
+		regInfo.Name = chname
167
+		regInfo.UUID = utils.GenerateUUIDv4()
168
+		regInfo.Founder = cffounder
169
+		regInfo.RegisteredAt = time.Unix(0, chInfo.RegisteredAt).UTC()
173 170
 		if chInfo.Topic != "" {
174
-			tx.Set(fmt.Sprintf(keyChannelTopic, cfchname), chInfo.Topic, nil)
175
-			tx.Set(fmt.Sprintf(keyChannelTopicSetTime, cfchname), strconv.FormatInt(chInfo.TopicSetAt, 10), nil)
176
-			tx.Set(fmt.Sprintf(keyChannelTopicSetBy, cfchname), chInfo.TopicSetBy, nil)
171
+			regInfo.Topic = chInfo.Topic
172
+			regInfo.TopicSetBy = chInfo.TopicSetBy
173
+			regInfo.TopicSetTime = time.Unix(0, chInfo.TopicSetAt).UTC()
177 174
 		}
175
+
178 176
 		if len(chInfo.Amode) != 0 {
179
-			m, err := serializeAmodes(chInfo.Amode, cfUsernames)
177
+			m, err := convertAmodes(chInfo.Amode, cfUsernames)
180 178
 			if err == nil {
181
-				tx.Set(fmt.Sprintf(keyChannelAccountToUMode, cfchname), string(m), nil)
179
+				regInfo.AccountToUMode = m
182 180
 			} else {
183
-				log.Printf("couldn't serialize amodes for %s: %v", chname, err)
181
+				log.Printf("couldn't process amodes for %s: %v", chname, err)
184 182
 			}
185 183
 		}
186
-		tx.Set(fmt.Sprintf(keyChannelModes, cfchname), chInfo.Modes, nil)
187
-		if chInfo.Key != "" {
188
-			tx.Set(fmt.Sprintf(keyChannelPassword, cfchname), chInfo.Key, nil)
184
+		for _, mode := range chInfo.Modes {
185
+			regInfo.Modes = append(regInfo.Modes, modes.Mode(mode))
189 186
 		}
187
+		regInfo.Key = chInfo.Key
190 188
 		if chInfo.Limit > 0 {
191
-			tx.Set(fmt.Sprintf(keyChannelUserLimit, cfchname), strconv.Itoa(chInfo.Limit), nil)
189
+			regInfo.UserLimit = chInfo.Limit
192 190
 		}
193 191
 		if chInfo.Forward != "" {
194 192
 			if _, err := CasefoldChannel(chInfo.Forward); err == nil {
195
-				tx.Set(fmt.Sprintf(keyChannelForward, cfchname), chInfo.Forward, nil)
193
+				regInfo.Forward = chInfo.Forward
196 194
 			}
197 195
 		}
196
+		if j, err := json.Marshal(regInfo); err == nil {
197
+			tx.Set(bunt.BuntKey(datastore.TableChannels, regInfo.UUID), string(j), nil)
198
+		} else {
199
+			log.Printf("couldn't serialize channel %s: %v", chname, err)
200
+		}
198 201
 	}
199 202
 
200 203
 	if warnSkeletons {

+ 121
- 0
irc/legacy.go View File

@@ -4,7 +4,15 @@ package irc
4 4
 
5 5
 import (
6 6
 	"encoding/base64"
7
+	"encoding/json"
7 8
 	"errors"
9
+	"fmt"
10
+	"strconv"
11
+	"time"
12
+
13
+	"github.com/tidwall/buntdb"
14
+
15
+	"github.com/ergochat/ergo/irc/modes"
8 16
 )
9 17
 
10 18
 var (
@@ -25,3 +33,116 @@ func decodeLegacyPasswordHash(hash string) ([]byte, error) {
25 33
 		return nil, errInvalidPasswordHash
26 34
 	}
27 35
 }
36
+
37
+// legacy channel registration code
38
+
39
+const (
40
+	keyChannelExists         = "channel.exists %s"
41
+	keyChannelName           = "channel.name %s" // stores the 'preferred name' of the channel, not casemapped
42
+	keyChannelRegTime        = "channel.registered.time %s"
43
+	keyChannelFounder        = "channel.founder %s"
44
+	keyChannelTopic          = "channel.topic %s"
45
+	keyChannelTopicSetBy     = "channel.topic.setby %s"
46
+	keyChannelTopicSetTime   = "channel.topic.settime %s"
47
+	keyChannelBanlist        = "channel.banlist %s"
48
+	keyChannelExceptlist     = "channel.exceptlist %s"
49
+	keyChannelInvitelist     = "channel.invitelist %s"
50
+	keyChannelPassword       = "channel.key %s"
51
+	keyChannelModes          = "channel.modes %s"
52
+	keyChannelAccountToUMode = "channel.accounttoumode %s"
53
+	keyChannelUserLimit      = "channel.userlimit %s"
54
+	keyChannelSettings       = "channel.settings %s"
55
+	keyChannelForward        = "channel.forward %s"
56
+
57
+	keyChannelPurged = "channel.purged %s"
58
+)
59
+
60
+func deleteLegacyChannel(tx *buntdb.Tx, nameCasefolded string) {
61
+	tx.Delete(fmt.Sprintf(keyChannelExists, nameCasefolded))
62
+	tx.Delete(fmt.Sprintf(keyChannelName, nameCasefolded))
63
+	tx.Delete(fmt.Sprintf(keyChannelRegTime, nameCasefolded))
64
+	tx.Delete(fmt.Sprintf(keyChannelFounder, nameCasefolded))
65
+	tx.Delete(fmt.Sprintf(keyChannelTopic, nameCasefolded))
66
+	tx.Delete(fmt.Sprintf(keyChannelTopicSetBy, nameCasefolded))
67
+	tx.Delete(fmt.Sprintf(keyChannelTopicSetTime, nameCasefolded))
68
+	tx.Delete(fmt.Sprintf(keyChannelBanlist, nameCasefolded))
69
+	tx.Delete(fmt.Sprintf(keyChannelExceptlist, nameCasefolded))
70
+	tx.Delete(fmt.Sprintf(keyChannelInvitelist, nameCasefolded))
71
+	tx.Delete(fmt.Sprintf(keyChannelPassword, nameCasefolded))
72
+	tx.Delete(fmt.Sprintf(keyChannelModes, nameCasefolded))
73
+	tx.Delete(fmt.Sprintf(keyChannelAccountToUMode, nameCasefolded))
74
+	tx.Delete(fmt.Sprintf(keyChannelUserLimit, nameCasefolded))
75
+	tx.Delete(fmt.Sprintf(keyChannelSettings, nameCasefolded))
76
+	tx.Delete(fmt.Sprintf(keyChannelForward, nameCasefolded))
77
+}
78
+
79
+func loadLegacyChannel(tx *buntdb.Tx, nameCasefolded string) (info RegisteredChannel, err error) {
80
+	channelKey := nameCasefolded
81
+	// nice to have: do all JSON (de)serialization outside of the buntdb transaction
82
+	_, dberr := tx.Get(fmt.Sprintf(keyChannelExists, channelKey))
83
+	if dberr == buntdb.ErrNotFound {
84
+		// chan does not already exist, return
85
+		err = errNoSuchChannel
86
+		return
87
+	}
88
+
89
+	// channel exists, load it
90
+	name, _ := tx.Get(fmt.Sprintf(keyChannelName, channelKey))
91
+	regTime, _ := tx.Get(fmt.Sprintf(keyChannelRegTime, channelKey))
92
+	regTimeInt, _ := strconv.ParseInt(regTime, 10, 64)
93
+	founder, _ := tx.Get(fmt.Sprintf(keyChannelFounder, channelKey))
94
+	topic, _ := tx.Get(fmt.Sprintf(keyChannelTopic, channelKey))
95
+	topicSetBy, _ := tx.Get(fmt.Sprintf(keyChannelTopicSetBy, channelKey))
96
+	var topicSetTime time.Time
97
+	topicSetTimeStr, _ := tx.Get(fmt.Sprintf(keyChannelTopicSetTime, channelKey))
98
+	if topicSetTimeInt, topicSetTimeErr := strconv.ParseInt(topicSetTimeStr, 10, 64); topicSetTimeErr == nil {
99
+		topicSetTime = time.Unix(0, topicSetTimeInt).UTC()
100
+	}
101
+	password, _ := tx.Get(fmt.Sprintf(keyChannelPassword, channelKey))
102
+	modeString, _ := tx.Get(fmt.Sprintf(keyChannelModes, channelKey))
103
+	userLimitString, _ := tx.Get(fmt.Sprintf(keyChannelUserLimit, channelKey))
104
+	forward, _ := tx.Get(fmt.Sprintf(keyChannelForward, channelKey))
105
+	banlistString, _ := tx.Get(fmt.Sprintf(keyChannelBanlist, channelKey))
106
+	exceptlistString, _ := tx.Get(fmt.Sprintf(keyChannelExceptlist, channelKey))
107
+	invitelistString, _ := tx.Get(fmt.Sprintf(keyChannelInvitelist, channelKey))
108
+	accountToUModeString, _ := tx.Get(fmt.Sprintf(keyChannelAccountToUMode, channelKey))
109
+	settingsString, _ := tx.Get(fmt.Sprintf(keyChannelSettings, channelKey))
110
+
111
+	modeSlice := make([]modes.Mode, len(modeString))
112
+	for i, mode := range modeString {
113
+		modeSlice[i] = modes.Mode(mode)
114
+	}
115
+
116
+	userLimit, _ := strconv.Atoi(userLimitString)
117
+
118
+	var banlist map[string]MaskInfo
119
+	_ = json.Unmarshal([]byte(banlistString), &banlist)
120
+	var exceptlist map[string]MaskInfo
121
+	_ = json.Unmarshal([]byte(exceptlistString), &exceptlist)
122
+	var invitelist map[string]MaskInfo
123
+	_ = json.Unmarshal([]byte(invitelistString), &invitelist)
124
+	accountToUMode := make(map[string]modes.Mode)
125
+	_ = json.Unmarshal([]byte(accountToUModeString), &accountToUMode)
126
+
127
+	var settings ChannelSettings
128
+	_ = json.Unmarshal([]byte(settingsString), &settings)
129
+
130
+	info = RegisteredChannel{
131
+		Name:           name,
132
+		RegisteredAt:   time.Unix(0, regTimeInt).UTC(),
133
+		Founder:        founder,
134
+		Topic:          topic,
135
+		TopicSetBy:     topicSetBy,
136
+		TopicSetTime:   topicSetTime,
137
+		Key:            password,
138
+		Modes:          modeSlice,
139
+		Bans:           banlist,
140
+		Excepts:        exceptlist,
141
+		Invites:        invitelist,
142
+		AccountToUMode: accountToUMode,
143
+		UserLimit:      int(userLimit),
144
+		Settings:       settings,
145
+		Forward:        forward,
146
+	}
147
+	return info, nil
148
+}

+ 2
- 2
irc/nickserv.go View File

@@ -954,9 +954,9 @@ func nsInfoHandler(service *ircService, server *Server, client *Client, command
954 954
 
955 955
 func listRegisteredChannels(service *ircService, accountName string, rb *ResponseBuffer) {
956 956
 	client := rb.session.client
957
-	channels := client.server.accounts.ChannelsForAccount(accountName)
957
+	channels := client.server.channels.ChannelsForAccount(accountName)
958 958
 	service.Notice(rb, fmt.Sprintf(client.t("Account %s has %d registered channel(s)."), accountName, len(channels)))
959
-	for _, channel := range rb.session.client.server.accounts.ChannelsForAccount(accountName) {
959
+	for _, channel := range channels {
960 960
 		service.Notice(rb, fmt.Sprintf(client.t("Registered channel: %s"), channel))
961 961
 	}
962 962
 }

+ 37
- 0
irc/serde.go View File

@@ -0,0 +1,37 @@
1
+// Copyright (c) 2022 Shivaram Lingamneni
2
+// released under the MIT license
3
+
4
+package irc
5
+
6
+import (
7
+	"strconv"
8
+
9
+	"github.com/ergochat/ergo/irc/datastore"
10
+	"github.com/ergochat/ergo/irc/logger"
11
+)
12
+
13
+type Serializable interface {
14
+	Serialize() ([]byte, error)
15
+	Deserialize([]byte) error
16
+}
17
+
18
+func FetchAndDeserializeAll[T any, C interface {
19
+	*T
20
+	Serializable
21
+}](table datastore.Table, dstore datastore.Datastore, log *logger.Manager) (result []T, err error) {
22
+	rawRecords, err := dstore.GetAll(table)
23
+	if err != nil {
24
+		return
25
+	}
26
+	result = make([]T, len(rawRecords))
27
+	pos := 0
28
+	for _, record := range rawRecords {
29
+		err := C(&result[pos]).Deserialize(record.Value)
30
+		if err != nil {
31
+			log.Error("internal", "deserialization error", strconv.Itoa(int(table)), record.UUID.String(), err.Error())
32
+			continue
33
+		}
34
+		pos++
35
+	}
36
+	return result[:pos], nil
37
+}

+ 15
- 5
irc/server.go View File

@@ -22,9 +22,12 @@ import (
22 22
 
23 23
 	"github.com/ergochat/irc-go/ircfmt"
24 24
 	"github.com/okzk/sdnotify"
25
+	"github.com/tidwall/buntdb"
25 26
 
27
+	"github.com/ergochat/ergo/irc/bunt"
26 28
 	"github.com/ergochat/ergo/irc/caps"
27 29
 	"github.com/ergochat/ergo/irc/connection_limits"
30
+	"github.com/ergochat/ergo/irc/datastore"
28 31
 	"github.com/ergochat/ergo/irc/flatip"
29 32
 	"github.com/ergochat/ergo/irc/flock"
30 33
 	"github.com/ergochat/ergo/irc/history"
@@ -33,7 +36,6 @@ import (
33 36
 	"github.com/ergochat/ergo/irc/mysql"
34 37
 	"github.com/ergochat/ergo/irc/sno"
35 38
 	"github.com/ergochat/ergo/irc/utils"
36
-	"github.com/tidwall/buntdb"
37 39
 )
38 40
 
39 41
 const (
@@ -66,7 +68,6 @@ type Server struct {
66 68
 	accepts           AcceptManager
67 69
 	accounts          AccountManager
68 70
 	channels          ChannelManager
69
-	channelRegistry   ChannelRegistry
70 71
 	clients           ClientManager
71 72
 	config            atomic.Pointer[Config]
72 73
 	configFilename    string
@@ -87,6 +88,7 @@ type Server struct {
87 88
 	tracebackSignal   chan os.Signal
88 89
 	snomasks          SnoManager
89 90
 	store             *buntdb.DB
91
+	dstore            datastore.Datastore
90 92
 	historyDB         mysql.MySQL
91 93
 	torLimiter        connection_limits.TorLimiter
92 94
 	whoWas            WhoWasList
@@ -98,6 +100,10 @@ type Server struct {
98 100
 
99 101
 // NewServer returns a new Oragono server.
100 102
 func NewServer(config *Config, logger *logger.Manager) (*Server, error) {
103
+	// sanity check that kernel randomness is available; on modern Linux,
104
+	// this will block until it is, on other platforms it may panic:
105
+	utils.GenerateUUIDv4()
106
+
101 107
 	// initialize data structures
102 108
 	server := &Server{
103 109
 		ctime:           time.Now().UTC(),
@@ -716,7 +722,11 @@ func (server *Server) applyConfig(config *Config) (err error) {
716 722
 	// now that the datastore is initialized, we can load the cloak secret from it
717 723
 	// XXX this modifies config after the initial load, which is naughty,
718 724
 	// but there's no data race because we haven't done SetConfig yet
719
-	config.Server.Cloaks.SetSecret(LoadCloakSecret(server.store))
725
+	cloakSecret, err := LoadCloakSecret(server.dstore)
726
+	if err != nil {
727
+		return fmt.Errorf("Could not load cloak secret: %w", err)
728
+	}
729
+	config.Server.Cloaks.SetSecret(cloakSecret)
720 730
 
721 731
 	// activate the new config
722 732
 	server.config.Store(config)
@@ -837,6 +847,7 @@ func (server *Server) loadDatastore(config *Config) error {
837 847
 	db, err := OpenDatabase(config)
838 848
 	if err == nil {
839 849
 		server.store = db
850
+		server.dstore = bunt.NewBuntdbDatastore(db, server.logger)
840 851
 		return nil
841 852
 	} else {
842 853
 		return fmt.Errorf("Failed to open datastore: %s", err.Error())
@@ -849,8 +860,7 @@ func (server *Server) loadFromDatastore(config *Config) (err error) {
849 860
 	server.loadDLines()
850 861
 	server.loadKLines()
851 862
 
852
-	server.channelRegistry.Initialize(server)
853
-	server.channels.Initialize(server)
863
+	server.channels.Initialize(server, config)
854 864
 	server.accounts.Initialize(server)
855 865
 
856 866
 	if config.Datastore.MySQL.Enabled {

+ 56
- 0
irc/utils/uuid.go View File

@@ -0,0 +1,56 @@
1
+// Copyright (c) 2022 Shivaram Lingamneni <slingamn@cs.stanford.edu>
2
+// released under the MIT license
3
+
4
+package utils
5
+
6
+import (
7
+	"crypto/rand"
8
+	"encoding/base64"
9
+	"errors"
10
+)
11
+
12
+var (
13
+	ErrInvalidUUID = errors.New("Invalid uuid")
14
+)
15
+
16
+// Technically a UUIDv4 has version bits set, but this doesn't matter in practice
17
+type UUID [16]byte
18
+
19
+func (u UUID) MarshalJSON() (b []byte, err error) {
20
+	b = make([]byte, 24)
21
+	b[0] = '"'
22
+	base64.RawURLEncoding.Encode(b[1:], u[:])
23
+	b[23] = '"'
24
+	return
25
+}
26
+
27
+func (u *UUID) UnmarshalJSON(b []byte) (err error) {
28
+	if len(b) != 24 {
29
+		return ErrInvalidUUID
30
+	}
31
+	readLen, err := base64.RawURLEncoding.Decode(u[:], b[1:23])
32
+	if readLen != 16 {
33
+		return ErrInvalidUUID
34
+	}
35
+	return nil
36
+}
37
+
38
+func (u *UUID) String() string {
39
+	return base64.RawURLEncoding.EncodeToString(u[:])
40
+}
41
+
42
+func GenerateUUIDv4() (result UUID) {
43
+	_, err := rand.Read(result[:])
44
+	if err != nil {
45
+		panic(err)
46
+	}
47
+	return
48
+}
49
+
50
+func DecodeUUID(ustr string) (result UUID, err error) {
51
+	length, err := base64.RawURLEncoding.Decode(result[:], []byte(ustr))
52
+	if err == nil && length != 16 {
53
+		err = ErrInvalidUUID
54
+	}
55
+	return
56
+}

Loading…
Cancel
Save