Procházet zdrojové kódy

refactor of channel persistence to use UUIDs

tags/v2.12.0-rc1
Shivaram Lingamneni před 1 rokem
rodič
revize
7ce0636276
18 změnil soubory, kde provedl 797 přidání a 646 odebrání
  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 Zobrazit soubor

39
 	keyAccountSettings         = "account.settings %s"
39
 	keyAccountSettings         = "account.settings %s"
40
 	keyAccountVHost            = "account.vhost %s"
40
 	keyAccountVHost            = "account.vhost %s"
41
 	keyCertToAccount           = "account.creds.certfp %s"
41
 	keyCertToAccount           = "account.creds.certfp %s"
42
-	keyAccountChannels         = "account.channels %s" // channels registered to the account
43
 	keyAccountLastSeen         = "account.lastseen %s"
42
 	keyAccountLastSeen         = "account.lastseen %s"
44
 	keyAccountReadMarkers      = "account.readmarkers %s"
43
 	keyAccountReadMarkers      = "account.readmarkers %s"
45
 	keyAccountModes            = "account.modes %s"     // user modes for the always-on client as a string
44
 	keyAccountModes            = "account.modes %s"     // user modes for the always-on client as a string
1765
 	nicksKey := fmt.Sprintf(keyAccountAdditionalNicks, casefoldedAccount)
1764
 	nicksKey := fmt.Sprintf(keyAccountAdditionalNicks, casefoldedAccount)
1766
 	settingsKey := fmt.Sprintf(keyAccountSettings, casefoldedAccount)
1765
 	settingsKey := fmt.Sprintf(keyAccountSettings, casefoldedAccount)
1767
 	vhostKey := fmt.Sprintf(keyAccountVHost, casefoldedAccount)
1766
 	vhostKey := fmt.Sprintf(keyAccountVHost, casefoldedAccount)
1768
-	channelsKey := fmt.Sprintf(keyAccountChannels, casefoldedAccount)
1769
 	joinedChannelsKey := fmt.Sprintf(keyAccountChannelToModes, casefoldedAccount)
1767
 	joinedChannelsKey := fmt.Sprintf(keyAccountChannelToModes, casefoldedAccount)
1770
 	lastSeenKey := fmt.Sprintf(keyAccountLastSeen, casefoldedAccount)
1768
 	lastSeenKey := fmt.Sprintf(keyAccountLastSeen, casefoldedAccount)
1771
 	readMarkersKey := fmt.Sprintf(keyAccountReadMarkers, casefoldedAccount)
1769
 	readMarkersKey := fmt.Sprintf(keyAccountReadMarkers, casefoldedAccount)
1781
 		am.killClients(clients)
1779
 		am.killClients(clients)
1782
 	}()
1780
 	}()
1783
 
1781
 
1784
-	var registeredChannels []string
1785
 	// on our way out, unregister all the account's channels and delete them from the db
1782
 	// on our way out, unregister all the account's channels and delete them from the db
1786
 	defer func() {
1783
 	defer func() {
1787
-		for _, channelName := range registeredChannels {
1784
+		for _, channelName := range am.server.channels.ChannelsForAccount(casefoldedAccount) {
1788
 			err := am.server.channels.SetUnregistered(channelName, casefoldedAccount)
1785
 			err := am.server.channels.SetUnregistered(channelName, casefoldedAccount)
1789
 			if err != nil {
1786
 			if err != nil {
1790
 				am.server.logger.Error("internal", "couldn't unregister channel", channelName, err.Error())
1787
 				am.server.logger.Error("internal", "couldn't unregister channel", channelName, err.Error())
1799
 	defer am.serialCacheUpdateMutex.Unlock()
1796
 	defer am.serialCacheUpdateMutex.Unlock()
1800
 
1797
 
1801
 	var accountName string
1798
 	var accountName string
1802
-	var channelsStr string
1803
 	keepProtections := false
1799
 	keepProtections := false
1804
 	am.server.store.Update(func(tx *buntdb.Tx) error {
1800
 	am.server.store.Update(func(tx *buntdb.Tx) error {
1805
 		// get the unfolded account name; for an active account, this is
1801
 		// get the unfolded account name; for an active account, this is
1827
 		credText, err = tx.Get(credentialsKey)
1823
 		credText, err = tx.Get(credentialsKey)
1828
 		tx.Delete(credentialsKey)
1824
 		tx.Delete(credentialsKey)
1829
 		tx.Delete(vhostKey)
1825
 		tx.Delete(vhostKey)
1830
-		channelsStr, _ = tx.Get(channelsKey)
1831
-		tx.Delete(channelsKey)
1832
 		tx.Delete(joinedChannelsKey)
1826
 		tx.Delete(joinedChannelsKey)
1833
 		tx.Delete(lastSeenKey)
1827
 		tx.Delete(lastSeenKey)
1834
 		tx.Delete(readMarkersKey)
1828
 		tx.Delete(readMarkersKey)
1858
 
1852
 
1859
 	skeleton, _ := Skeleton(accountName)
1853
 	skeleton, _ := Skeleton(accountName)
1860
 	additionalNicks := unmarshalReservedNicks(rawNicks)
1854
 	additionalNicks := unmarshalReservedNicks(rawNicks)
1861
-	registeredChannels = unmarshalRegisteredChannels(channelsStr)
1862
 
1855
 
1863
 	am.Lock()
1856
 	am.Lock()
1864
 	defer am.Unlock()
1857
 	defer am.Unlock()
1890
 	return
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
 func (am *AccountManager) AuthenticateByCertificate(client *Client, certfp string, peerCerts []*x509.Certificate, authzid string) (err error) {
1886
 func (am *AccountManager) AuthenticateByCertificate(client *Client, certfp string, peerCerts []*x509.Certificate, authzid string) (err error) {
1909
 	if certfp == "" {
1887
 	if certfp == "" {
1910
 		return errAccountInvalidCredentials
1888
 		return errAccountInvalidCredentials

+ 106
- 0
irc/bunt/bunt_datastore.go Zobrazit soubor

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 Zobrazit soubor

16
 	"github.com/ergochat/irc-go/ircutils"
16
 	"github.com/ergochat/irc-go/ircutils"
17
 
17
 
18
 	"github.com/ergochat/ergo/irc/caps"
18
 	"github.com/ergochat/ergo/irc/caps"
19
+	"github.com/ergochat/ergo/irc/datastore"
19
 	"github.com/ergochat/ergo/irc/history"
20
 	"github.com/ergochat/ergo/irc/history"
20
 	"github.com/ergochat/ergo/irc/modes"
21
 	"github.com/ergochat/ergo/irc/modes"
21
 	"github.com/ergochat/ergo/irc/utils"
22
 	"github.com/ergochat/ergo/irc/utils"
50
 	stateMutex        sync.RWMutex // tier 1
51
 	stateMutex        sync.RWMutex // tier 1
51
 	writebackLock     sync.Mutex   // tier 1.5
52
 	writebackLock     sync.Mutex   // tier 1.5
52
 	joinPartMutex     sync.Mutex   // tier 3
53
 	joinPartMutex     sync.Mutex   // tier 3
53
-	ensureLoaded      utils.Once   // manages loading stored registration info from the database
54
 	dirtyBits         uint
54
 	dirtyBits         uint
55
 	settings          ChannelSettings
55
 	settings          ChannelSettings
56
+	uuid              utils.UUID
56
 }
57
 }
57
 
58
 
58
 // NewChannel creates a new channel from a `Server` and a `name`
59
 // NewChannel creates a new channel from a `Server` and a `name`
59
 // string, which must be unique on the server.
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
 	config := s.Config()
62
 	config := s.Config()
62
 
63
 
63
 	channel := &Channel{
64
 	channel := &Channel{
71
 	channel.initializeLists()
72
 	channel.initializeLists()
72
 	channel.history.Initialize(0, 0)
73
 	channel.history.Initialize(0, 0)
73
 
74
 
74
-	if !registered {
75
+	if registered {
76
+		channel.applyRegInfo(regInfo)
77
+	} else {
75
 		channel.resizeHistory(config)
78
 		channel.resizeHistory(config)
76
 		for _, mode := range config.Channels.defaultModes {
79
 		for _, mode := range config.Channels.defaultModes {
77
 			channel.flags.SetMode(mode, true)
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
 	return channel
85
 	return channel
84
 }
86
 }
92
 	channel.accountToUMode = make(map[string]modes.Mode)
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
 func (channel *Channel) resizeHistory(config *Config) {
97
 func (channel *Channel) resizeHistory(config *Config) {
114
 	status, _, _ := channel.historyStatus(config)
98
 	status, _, _ := channel.historyStatus(config)
115
 	if status == HistoryEphemeral {
99
 	if status == HistoryEphemeral {
126
 	channel.stateMutex.Lock()
110
 	channel.stateMutex.Lock()
127
 	defer channel.stateMutex.Unlock()
111
 	defer channel.stateMutex.Unlock()
128
 
112
 
113
+	channel.uuid = chanReg.UUID
129
 	channel.registeredFounder = chanReg.Founder
114
 	channel.registeredFounder = chanReg.Founder
130
 	channel.registeredTime = chanReg.RegisteredAt
115
 	channel.registeredTime = chanReg.RegisteredAt
131
 	channel.topic = chanReg.Topic
116
 	channel.topic = chanReg.Topic
150
 }
135
 }
151
 
136
 
152
 // obtain a consistent snapshot of the channel state that can be persisted to the DB
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
 	channel.stateMutex.RLock()
139
 	channel.stateMutex.RLock()
155
 	defer channel.stateMutex.RUnlock()
140
 	defer channel.stateMutex.RUnlock()
156
 
141
 
157
 	info.Name = channel.name
142
 	info.Name = channel.name
158
-	info.NameCasefolded = channel.nameCasefolded
143
+	info.UUID = channel.uuid
159
 	info.Founder = channel.registeredFounder
144
 	info.Founder = channel.registeredFounder
160
 	info.RegisteredAt = channel.registeredTime
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
 	return
174
 	return
187
 }
175
 }
288
 		return
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
 		channel.stateMutex.Lock()
292
 		channel.stateMutex.Lock()
295
 		channel.dirtyBits = channel.dirtyBits | dirtyBits
293
 		channel.dirtyBits = channel.dirtyBits | dirtyBits
296
 		channel.stateMutex.Unlock()
294
 		channel.stateMutex.Unlock()
314
 
312
 
315
 // SetUnregistered deletes the channel's registration information.
313
 // SetUnregistered deletes the channel's registration information.
316
 func (channel *Channel) SetUnregistered(expectedFounder string) {
314
 func (channel *Channel) SetUnregistered(expectedFounder string) {
315
+	uuid := utils.GenerateUUIDv4()
317
 	channel.stateMutex.Lock()
316
 	channel.stateMutex.Lock()
318
 	defer channel.stateMutex.Unlock()
317
 	defer channel.stateMutex.Unlock()
319
 
318
 
324
 	var zeroTime time.Time
323
 	var zeroTime time.Time
325
 	channel.registeredTime = zeroTime
324
 	channel.registeredTime = zeroTime
326
 	channel.accountToUMode = make(map[string]modes.Mode)
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
 // implements `CHANSERV CLEAR #chan ACCESS` (resets bans, invites, excepts, and amodes)
331
 // implements `CHANSERV CLEAR #chan ACCESS` (resets bans, invites, excepts, and amodes)

+ 161
- 132
irc/channelmanager.go Zobrazit soubor

6
 import (
6
 import (
7
 	"sort"
7
 	"sort"
8
 	"sync"
8
 	"sync"
9
+	"time"
9
 
10
 
11
+	"github.com/ergochat/ergo/irc/datastore"
10
 	"github.com/ergochat/ergo/irc/utils"
12
 	"github.com/ergochat/ergo/irc/utils"
11
 )
13
 )
12
 
14
 
25
 type ChannelManager struct {
27
 type ChannelManager struct {
26
 	sync.RWMutex // tier 2
28
 	sync.RWMutex // tier 2
27
 	// chans is the main data structure, mapping casefolded name -> *Channel
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
 // NewChannelManager returns a new ChannelManager.
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
 	cm.chans = make(map[string]*channelManagerEntry)
38
 	cm.chans = make(map[string]*channelManagerEntry)
39
 	cm.chansSkeletons = make(utils.HashSet[string])
39
 	cm.chansSkeletons = make(utils.HashSet[string])
40
 	cm.server = server
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
 		return
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
 	cm.Lock()
54
 	cm.Lock()
67
 	defer cm.Unlock()
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
 		if err == nil {
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
 // Get returns an existing channel with name equivalent to `name`, or nil
88
 // Get returns an existing channel with name equivalent to `name`, or nil
97
 func (cm *ChannelManager) Get(name string) (channel *Channel) {
89
 func (cm *ChannelManager) Get(name string) (channel *Channel) {
98
 	name, err := CasefoldChannel(name)
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
 	return nil
100
 	return nil
109
 }
101
 }
122
 		cm.Lock()
114
 		cm.Lock()
123
 		defer cm.Unlock()
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
 			return nil, errChannelPurged, false
119
 			return nil, errChannelPurged, false
127
 		}
120
 		}
128
 		entry := cm.chans[casefoldedName]
121
 		entry := cm.chans[casefoldedName]
129
 		if entry == nil {
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
 				!(isSajoin || client.HasRoleCapabs("chanreg")) {
124
 				!(isSajoin || client.HasRoleCapabs("chanreg")) {
134
 				return nil, errInsufficientPrivs, false
125
 				return nil, errInsufficientPrivs, false
135
 			}
126
 			}
136
 			// enforce confusables
127
 			// enforce confusables
137
-			if !registered && (cm.chansSkeletons.Has(skeleton) || cm.registeredSkeletons.Has(skeleton)) {
128
+			if cm.chansSkeletons.Has(skeleton) {
138
 				return nil, errConfusableIdentifier, false
129
 				return nil, errConfusableIdentifier, false
139
 			}
130
 			}
140
 			entry = &channelManagerEntry{
131
 			entry = &channelManagerEntry{
141
-				channel:      NewChannel(server, name, casefoldedName, registered),
132
+				channel:      NewChannel(server, name, casefoldedName, false, RegisteredChannel{}),
142
 				pendingJoins: 0,
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
 			cm.chans[casefoldedName] = entry
137
 			cm.chans[casefoldedName] = entry
153
 			newChannel = true
138
 			newChannel = true
154
 		}
139
 		}
160
 		return err, ""
145
 		return err, ""
161
 	}
146
 	}
162
 
147
 
163
-	channel.EnsureLoaded()
164
 	err, forward = channel.Join(client, key, isSajoin || newChannel, rb)
148
 	err, forward = channel.Join(client, key, isSajoin || newChannel, rb)
165
 
149
 
166
 	cm.maybeCleanup(channel, true)
150
 	cm.maybeCleanup(channel, true)
252
 	if err != nil {
236
 	if err != nil {
253
 		return err
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
 	return nil
239
 	return nil
263
 }
240
 }
264
 
241
 
268
 		return err
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
 	defer func() {
250
 	defer func() {
280
 		if err == nil {
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
 	defer cm.Unlock()
259
 	defer cm.Unlock()
287
 	entry := cm.chans[cfname]
260
 	entry := cm.chans[cfname]
288
 	if entry != nil {
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
 		// #1619: if the channel has 0 members and was only being retained
267
 		// #1619: if the channel has 0 members and was only being retained
299
 		// because it was registered, clean it up:
268
 		// because it was registered, clean it up:
300
 		cm.maybeCleanupInternal(cfname, entry, false)
269
 		cm.maybeCleanupInternal(cfname, entry, false)
322
 	var info RegisteredChannel
291
 	var info RegisteredChannel
323
 	defer func() {
292
 	defer func() {
324
 		if channel != nil && info.Founder != "" {
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
 	defer cm.Unlock()
303
 	defer cm.Unlock()
336
 
304
 
337
 	entry := cm.chans[oldCfname]
305
 	entry := cm.chans[oldCfname]
338
-	if entry == nil || !entry.channel.IsLoaded() {
306
+	if entry == nil {
339
 		return errNoSuchChannel
307
 		return errNoSuchChannel
340
 	}
308
 	}
341
 	channel = entry.channel
309
 	channel = entry.channel
342
-	info = channel.ExportRegistration(IncludeInitial)
310
+	info = channel.ExportRegistration()
343
 	registered := info.Founder != ""
311
 	registered := info.Founder != ""
344
 
312
 
345
 	oldSkeleton, err := Skeleton(info.Name)
313
 	oldSkeleton, err := Skeleton(info.Name)
348
 	}
316
 	}
349
 
317
 
350
 	if newCfname != oldCfname {
318
 	if newCfname != oldCfname {
351
-		if cm.chans[newCfname] != nil || cm.registeredChannels.Has(newCfname) {
319
+		if cm.chans[newCfname] != nil {
352
 			return errChannelNameInUse
320
 			return errChannelNameInUse
353
 		}
321
 		}
354
 	}
322
 	}
355
 
323
 
356
 	if oldSkeleton != newSkeleton {
324
 	if oldSkeleton != newSkeleton {
357
-		if cm.chansSkeletons.Has(newSkeleton) || cm.registeredSkeletons.Has(newSkeleton) {
325
+		if cm.chansSkeletons.Has(newSkeleton) {
358
 			return errConfusableIdentifier
326
 			return errConfusableIdentifier
359
 		}
327
 		}
360
 	}
328
 	}
364
 		entry.skeleton = newSkeleton
332
 		entry.skeleton = newSkeleton
365
 	}
333
 	}
366
 	cm.chans[newCfname] = entry
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
 	entry.channel.Rename(newName, newCfname)
337
 	entry.channel.Rename(newName, newCfname)
377
 	return nil
338
 	return nil
378
 }
339
 }
390
 	defer cm.RUnlock()
351
 	defer cm.RUnlock()
391
 	result = make([]*Channel, 0, len(cm.chans))
352
 	result = make([]*Channel, 0, len(cm.chans))
392
 	for _, entry := range cm.chans {
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
 			result = append(result, entry.channel)
366
 			result = append(result, entry.channel)
395
 		}
367
 		}
396
 	}
368
 	}
403
 	if err != nil {
375
 	if err != nil {
404
 		return errInvalidChannelName
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
 	if err != nil {
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
 // IsPurged queries whether a channel is purged.
420
 // IsPurged queries whether a channel is purged.
436
 	}
425
 	}
437
 
426
 
438
 	cm.RLock()
427
 	cm.RLock()
439
-	result = cm.purgedChannels.Has(chname)
428
+	_, result = cm.purgedChannels[chname]
440
 	cm.RUnlock()
429
 	cm.RUnlock()
441
 	return
430
 	return
442
 }
431
 }
449
 	}
438
 	}
450
 
439
 
451
 	cm.Lock()
440
 	cm.Lock()
452
-	found := cm.purgedChannels.Has(chname)
441
+	record, found := cm.purgedChannels[chname]
453
 	delete(cm.purgedChannels, chname)
442
 	delete(cm.purgedChannels, chname)
454
 	cm.Unlock()
443
 	cm.Unlock()
455
 
444
 
456
-	cm.server.channelRegistry.UnpurgeChannel(chname)
457
 	if !found {
445
 	if !found {
458
 		return errNoSuchChannel
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
 	return nil
451
 	return nil
461
 }
452
 }
462
 
453
 
475
 	cm.RLock()
466
 	cm.RLock()
476
 	entry := cm.chans[cfname]
467
 	entry := cm.chans[cfname]
477
 	cm.RUnlock()
468
 	cm.RUnlock()
478
-	if entry != nil && entry.channel.IsLoaded() {
469
+	if entry != nil {
479
 		return entry.channel.Name()
470
 		return entry.channel.Name()
480
 	}
471
 	}
481
 	return cfname
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 Zobrazit soubor

5
 
5
 
6
 import (
6
 import (
7
 	"encoding/json"
7
 	"encoding/json"
8
-	"fmt"
9
-	"strconv"
10
-	"strings"
11
 	"time"
8
 	"time"
12
 
9
 
13
-	"github.com/tidwall/buntdb"
14
-
15
 	"github.com/ergochat/ergo/irc/modes"
10
 	"github.com/ergochat/ergo/irc/modes"
16
 	"github.com/ergochat/ergo/irc/utils"
11
 	"github.com/ergochat/ergo/irc/utils"
17
 )
12
 )
19
 // this is exclusively the *persistence* layer for channel registration;
14
 // this is exclusively the *persistence* layer for channel registration;
20
 // channel creation/tracking/destruction is in channelmanager.go
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
 // these are bit flags indicating what part of the channel status is "dirty"
17
 // these are bit flags indicating what part of the channel status is "dirty"
65
 // and needs to be read from memory and written to the db
18
 // and needs to be read from memory and written to the db
66
 const (
19
 const (
80
 type RegisteredChannel struct {
33
 type RegisteredChannel struct {
81
 	// Name of the channel.
34
 	// Name of the channel.
82
 	Name string
35
 	Name string
83
-	// Casefolded name of the channel.
84
-	NameCasefolded string
36
+	// UUID for the datastore.
37
+	UUID utils.UUID
85
 	// RegisteredAt represents the time that the channel was registered.
38
 	// RegisteredAt represents the time that the channel was registered.
86
 	RegisteredAt time.Time
39
 	RegisteredAt time.Time
87
 	// Founder indicates the founder of the channel.
40
 	// Founder indicates the founder of the channel.
112
 	Settings ChannelSettings
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 Zobrazit soubor

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

+ 133
- 22
irc/database.go Zobrazit soubor

14
 	"strings"
14
 	"strings"
15
 	"time"
15
 	"time"
16
 
16
 
17
+	"github.com/ergochat/ergo/irc/bunt"
18
+	"github.com/ergochat/ergo/irc/datastore"
17
 	"github.com/ergochat/ergo/irc/modes"
19
 	"github.com/ergochat/ergo/irc/modes"
18
 	"github.com/ergochat/ergo/irc/utils"
20
 	"github.com/ergochat/ergo/irc/utils"
19
 
21
 
21
 )
23
 )
22
 
24
 
23
 const (
25
 const (
26
+	// TODO migrate metadata keys as well
27
+
24
 	// 'version' of the database schema
28
 	// 'version' of the database schema
25
-	keySchemaVersion = "db.version"
26
 	// latest schema of the db
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
 type SchemaChanger func(*Config, *buntdb.Tx) error
41
 type SchemaChanger func(*Config, *buntdb.Tx) error
99
 	// read the current version string
108
 	// read the current version string
100
 	var version int
109
 	var version int
101
 	err = db.View(func(tx *buntdb.Tx) (err error) {
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
 		return err
112
 		return err
107
 	})
113
 	})
108
 	if err != nil {
114
 	if err != nil {
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
 func performAutoUpgrade(currentVersion int, config *Config) (err error) {
150
 func performAutoUpgrade(currentVersion int, config *Config) (err error) {
134
 	path := config.Datastore.Path
151
 	path := config.Datastore.Path
135
 	log.Printf("attempting to auto-upgrade schema from version %d to %d\n", currentVersion, latestDbSchema)
152
 	log.Printf("attempting to auto-upgrade schema from version %d to %d\n", currentVersion, latestDbSchema)
167
 	var version int
184
 	var version int
168
 	err = store.Update(func(tx *buntdb.Tx) error {
185
 	err = store.Update(func(tx *buntdb.Tx) error {
169
 		for {
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
 			if version == latestDbSchema {
193
 			if version == latestDbSchema {
173
 				// success!
194
 				// success!
174
 				break
195
 				break
183
 			if err != nil {
204
 			if err != nil {
184
 				return err
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
 			if err != nil {
209
 			if err != nil {
188
 				return err
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
 		return nil
214
 		return nil
193
 	})
215
 	})
198
 	return err
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
 func schemaChangeV1toV2(config *Config, tx *buntdb.Tx) error {
236
 func schemaChangeV1toV2(config *Config, tx *buntdb.Tx) error {
1112
 	return nil
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
 func getSchemaChange(initialVersion int) (result SchemaChange, ok bool) {
1221
 func getSchemaChange(initialVersion int) (result SchemaChange, ok bool) {
1116
 	for _, change := range allChanges {
1222
 	for _, change := range allChanges {
1117
 		if initialVersion == change.InitialVersion {
1223
 		if initialVersion == change.InitialVersion {
1227
 		TargetVersion:  22,
1333
 		TargetVersion:  22,
1228
 		Changer:        schemaChangeV21To22,
1334
 		Changer:        schemaChangeV21To22,
1229
 	},
1335
 	},
1336
+	{
1337
+		InitialVersion: 22,
1338
+		TargetVersion:  23,
1339
+		Changer:        schemaChangeV22ToV23,
1340
+	},
1230
 }
1341
 }

+ 45
- 0
irc/datastore/datastore.go Zobrazit soubor

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 Zobrazit soubor

51
 	errNoExistingBan                  = errors.New("Ban does not exist")
51
 	errNoExistingBan                  = errors.New("Ban does not exist")
52
 	errNoSuchChannel                  = errors.New(`No such channel`)
52
 	errNoSuchChannel                  = errors.New(`No such channel`)
53
 	errChannelPurged                  = errors.New(`This channel was purged by the server operators and cannot be used`)
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
 	errConfusableIdentifier           = errors.New("This identifier is confusable with one already in use")
55
 	errConfusableIdentifier           = errors.New("This identifier is confusable with one already in use")
55
 	errInsufficientPrivs              = errors.New("Insufficient privileges")
56
 	errInsufficientPrivs              = errors.New("Insufficient privileges")
56
 	errInvalidUsername                = errors.New("Invalid username")
57
 	errInvalidUsername                = errors.New("Invalid username")

+ 6
- 0
irc/getters.go Zobrazit soubor

638
 	defer channel.stateMutex.RUnlock()
638
 	defer channel.stateMutex.RUnlock()
639
 	return channel.accountToUMode[cfaccount]
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 Zobrazit soubor

1718
 
1718
 
1719
 	clientIsOp := client.HasRoleCapabs("sajoin")
1719
 	clientIsOp := client.HasRoleCapabs("sajoin")
1720
 	if len(channels) == 0 {
1720
 	if len(channels) == 0 {
1721
-		for _, channel := range server.channels.Channels() {
1721
+		for _, channel := range server.channels.ListableChannels() {
1722
 			if !clientIsOp && channel.flags.HasMode(modes.Secret) && !channel.hasClient(client) {
1722
 			if !clientIsOp && channel.flags.HasMode(modes.Secret) && !channel.hasClient(client) {
1723
 				continue
1723
 				continue
1724
 			}
1724
 			}

+ 1
- 1
irc/hostserv.go Zobrazit soubor

193
 		service.Notice(rb, fmt.Sprintf(client.t("To confirm, run this command: %s"), fmt.Sprintf("/HS SETCLOAKSECRET %s %s", secret, expectedCode)))
193
 		service.Notice(rb, fmt.Sprintf(client.t("To confirm, run this command: %s"), fmt.Sprintf("/HS SETCLOAKSECRET %s %s", secret, expectedCode)))
194
 		return
194
 		return
195
 	}
195
 	}
196
-	StoreCloakSecret(server.store, secret)
196
+	StoreCloakSecret(server.dstore, secret)
197
 	service.Notice(rb, client.t("Rotated the cloak secret; you must rehash or restart the server for it to take effect"))
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 Zobrazit soubor

9
 	"log"
9
 	"log"
10
 	"os"
10
 	"os"
11
 	"strconv"
11
 	"strconv"
12
+	"time"
12
 
13
 
13
 	"github.com/tidwall/buntdb"
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
 	"github.com/ergochat/ergo/irc/utils"
19
 	"github.com/ergochat/ergo/irc/utils"
16
 )
20
 )
17
 
21
 
20
 	// XXX instead of referencing, e.g., keyAccountExists, we should write in the string literal
24
 	// XXX instead of referencing, e.g., keyAccountExists, we should write in the string literal
21
 	// (to ensure that no matter what code changes happen elsewhere, we're still producing a
25
 	// (to ensure that no matter what code changes happen elsewhere, we're still producing a
22
 	// db of the hardcoded version)
26
 	// db of the hardcoded version)
23
-	importDBSchemaVersion = 22
27
+	importDBSchemaVersion = 23
24
 )
28
 )
25
 
29
 
26
 type userImport struct {
30
 type userImport struct {
54
 	Channels map[string]channelImport
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
 	for accountName, mode := range raw {
63
 	for accountName, mode := range raw {
60
 		if len(mode) != 1 {
64
 		if len(mode) != 1 {
61
 			return nil, fmt.Errorf("invalid mode %s for account %s", mode, accountName)
65
 			return nil, fmt.Errorf("invalid mode %s for account %s", mode, accountName)
64
 		if err != nil || !validCfUsernames.Has(cfname) {
68
 		if err != nil || !validCfUsernames.Has(cfname) {
65
 			log.Printf("skipping invalid amode recipient %s\n", accountName)
69
 			log.Printf("skipping invalid amode recipient %s\n", accountName)
66
 		} else {
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
 	return
74
 	return
72
 }
75
 }
73
 
76
 
147
 		cfUsernames.Add(cfUsername)
150
 		cfUsernames.Add(cfUsername)
148
 	}
151
 	}
149
 
152
 
153
+	// TODO fix this:
150
 	for chname, chInfo := range dbImport.Channels {
154
 	for chname, chInfo := range dbImport.Channels {
151
-		cfchname, err := CasefoldChannel(chname)
155
+		_, err := CasefoldChannel(chname)
152
 		if err != nil {
156
 		if err != nil {
153
 			log.Printf("invalid channel name %s: %v", chname, err)
157
 			log.Printf("invalid channel name %s: %v", chname, err)
154
 			continue
158
 			continue
158
 			log.Printf("invalid founder %s for channel %s: %v", chInfo.Founder, chname, err)
162
 			log.Printf("invalid founder %s for channel %s: %v", chInfo.Founder, chname, err)
159
 			continue
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
 		if chInfo.Topic != "" {
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
 		if len(chInfo.Amode) != 0 {
176
 		if len(chInfo.Amode) != 0 {
179
-			m, err := serializeAmodes(chInfo.Amode, cfUsernames)
177
+			m, err := convertAmodes(chInfo.Amode, cfUsernames)
180
 			if err == nil {
178
 			if err == nil {
181
-				tx.Set(fmt.Sprintf(keyChannelAccountToUMode, cfchname), string(m), nil)
179
+				regInfo.AccountToUMode = m
182
 			} else {
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
 		if chInfo.Limit > 0 {
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
 		if chInfo.Forward != "" {
191
 		if chInfo.Forward != "" {
194
 			if _, err := CasefoldChannel(chInfo.Forward); err == nil {
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
 	if warnSkeletons {
203
 	if warnSkeletons {

+ 121
- 0
irc/legacy.go Zobrazit soubor

4
 
4
 
5
 import (
5
 import (
6
 	"encoding/base64"
6
 	"encoding/base64"
7
+	"encoding/json"
7
 	"errors"
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
 var (
18
 var (
25
 		return nil, errInvalidPasswordHash
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 Zobrazit soubor

954
 
954
 
955
 func listRegisteredChannels(service *ircService, accountName string, rb *ResponseBuffer) {
955
 func listRegisteredChannels(service *ircService, accountName string, rb *ResponseBuffer) {
956
 	client := rb.session.client
956
 	client := rb.session.client
957
-	channels := client.server.accounts.ChannelsForAccount(accountName)
957
+	channels := client.server.channels.ChannelsForAccount(accountName)
958
 	service.Notice(rb, fmt.Sprintf(client.t("Account %s has %d registered channel(s)."), accountName, len(channels)))
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
 		service.Notice(rb, fmt.Sprintf(client.t("Registered channel: %s"), channel))
960
 		service.Notice(rb, fmt.Sprintf(client.t("Registered channel: %s"), channel))
961
 	}
961
 	}
962
 }
962
 }

+ 37
- 0
irc/serde.go Zobrazit soubor

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 Zobrazit soubor

22
 
22
 
23
 	"github.com/ergochat/irc-go/ircfmt"
23
 	"github.com/ergochat/irc-go/ircfmt"
24
 	"github.com/okzk/sdnotify"
24
 	"github.com/okzk/sdnotify"
25
+	"github.com/tidwall/buntdb"
25
 
26
 
27
+	"github.com/ergochat/ergo/irc/bunt"
26
 	"github.com/ergochat/ergo/irc/caps"
28
 	"github.com/ergochat/ergo/irc/caps"
27
 	"github.com/ergochat/ergo/irc/connection_limits"
29
 	"github.com/ergochat/ergo/irc/connection_limits"
30
+	"github.com/ergochat/ergo/irc/datastore"
28
 	"github.com/ergochat/ergo/irc/flatip"
31
 	"github.com/ergochat/ergo/irc/flatip"
29
 	"github.com/ergochat/ergo/irc/flock"
32
 	"github.com/ergochat/ergo/irc/flock"
30
 	"github.com/ergochat/ergo/irc/history"
33
 	"github.com/ergochat/ergo/irc/history"
33
 	"github.com/ergochat/ergo/irc/mysql"
36
 	"github.com/ergochat/ergo/irc/mysql"
34
 	"github.com/ergochat/ergo/irc/sno"
37
 	"github.com/ergochat/ergo/irc/sno"
35
 	"github.com/ergochat/ergo/irc/utils"
38
 	"github.com/ergochat/ergo/irc/utils"
36
-	"github.com/tidwall/buntdb"
37
 )
39
 )
38
 
40
 
39
 const (
41
 const (
66
 	accepts           AcceptManager
68
 	accepts           AcceptManager
67
 	accounts          AccountManager
69
 	accounts          AccountManager
68
 	channels          ChannelManager
70
 	channels          ChannelManager
69
-	channelRegistry   ChannelRegistry
70
 	clients           ClientManager
71
 	clients           ClientManager
71
 	config            atomic.Pointer[Config]
72
 	config            atomic.Pointer[Config]
72
 	configFilename    string
73
 	configFilename    string
87
 	tracebackSignal   chan os.Signal
88
 	tracebackSignal   chan os.Signal
88
 	snomasks          SnoManager
89
 	snomasks          SnoManager
89
 	store             *buntdb.DB
90
 	store             *buntdb.DB
91
+	dstore            datastore.Datastore
90
 	historyDB         mysql.MySQL
92
 	historyDB         mysql.MySQL
91
 	torLimiter        connection_limits.TorLimiter
93
 	torLimiter        connection_limits.TorLimiter
92
 	whoWas            WhoWasList
94
 	whoWas            WhoWasList
98
 
100
 
99
 // NewServer returns a new Oragono server.
101
 // NewServer returns a new Oragono server.
100
 func NewServer(config *Config, logger *logger.Manager) (*Server, error) {
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
 	// initialize data structures
107
 	// initialize data structures
102
 	server := &Server{
108
 	server := &Server{
103
 		ctime:           time.Now().UTC(),
109
 		ctime:           time.Now().UTC(),
716
 	// now that the datastore is initialized, we can load the cloak secret from it
722
 	// now that the datastore is initialized, we can load the cloak secret from it
717
 	// XXX this modifies config after the initial load, which is naughty,
723
 	// XXX this modifies config after the initial load, which is naughty,
718
 	// but there's no data race because we haven't done SetConfig yet
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
 	// activate the new config
731
 	// activate the new config
722
 	server.config.Store(config)
732
 	server.config.Store(config)
837
 	db, err := OpenDatabase(config)
847
 	db, err := OpenDatabase(config)
838
 	if err == nil {
848
 	if err == nil {
839
 		server.store = db
849
 		server.store = db
850
+		server.dstore = bunt.NewBuntdbDatastore(db, server.logger)
840
 		return nil
851
 		return nil
841
 	} else {
852
 	} else {
842
 		return fmt.Errorf("Failed to open datastore: %s", err.Error())
853
 		return fmt.Errorf("Failed to open datastore: %s", err.Error())
849
 	server.loadDLines()
860
 	server.loadDLines()
850
 	server.loadKLines()
861
 	server.loadKLines()
851
 
862
 
852
-	server.channelRegistry.Initialize(server)
853
-	server.channels.Initialize(server)
863
+	server.channels.Initialize(server, config)
854
 	server.accounts.Initialize(server)
864
 	server.accounts.Initialize(server)
855
 
865
 
856
 	if config.Datastore.MySQL.Enabled {
866
 	if config.Datastore.MySQL.Enabled {

+ 56
- 0
irc/utils/uuid.go Zobrazit soubor

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
+}

Načítá se…
Zrušit
Uložit