1// Copyright (c) 2012-2014 Jeremy Latt
2// Copyright (c) 2016 Daniel Oaks <daniel@danieloaks.net>
3// released under the MIT license
4
5package irc
6
7import (
8	"encoding/base64"
9	"encoding/json"
10	"fmt"
11	"log"
12	"os"
13	"strconv"
14	"strings"
15	"time"
16
17	"github.com/ergochat/ergo/irc/modes"
18	"github.com/ergochat/ergo/irc/utils"
19
20	"github.com/tidwall/buntdb"
21)
22
23const (
24	// 'version' of the database schema
25	keySchemaVersion = "db.version"
26	// latest schema of the db
27	latestDbSchema = 22
28
29	keyCloakSecret = "crypto.cloak_secret"
30)
31
32type SchemaChanger func(*Config, *buntdb.Tx) error
33
34type SchemaChange struct {
35	InitialVersion int // the change will take this version
36	TargetVersion  int // and transform it into this version
37	Changer        SchemaChanger
38}
39
40func checkDBReadyForInit(path string) error {
41	_, err := os.Stat(path)
42	if err == nil {
43		return fmt.Errorf("Datastore already exists (delete it manually to continue): %s", path)
44	} else if !os.IsNotExist(err) {
45		return fmt.Errorf("Datastore path %s is inaccessible: %w", path, err)
46	}
47	return nil
48}
49
50// InitDB creates the database, implementing the `oragono initdb` command.
51func InitDB(path string) error {
52	if err := checkDBReadyForInit(path); err != nil {
53		return err
54	}
55
56	if err := initializeDB(path); err != nil {
57		return fmt.Errorf("Could not save datastore: %w", err)
58	}
59	return nil
60}
61
62// internal database initialization code
63func initializeDB(path string) error {
64	store, err := buntdb.Open(path)
65	if err != nil {
66		return err
67	}
68	defer store.Close()
69
70	err = store.Update(func(tx *buntdb.Tx) error {
71		// set schema version
72		tx.Set(keySchemaVersion, strconv.Itoa(latestDbSchema), nil)
73		tx.Set(keyCloakSecret, utils.GenerateSecretKey(), nil)
74		return nil
75	})
76
77	return err
78}
79
80// OpenDatabase returns an existing database, performing a schema version check.
81func OpenDatabase(config *Config) (*buntdb.DB, error) {
82	return openDatabaseInternal(config, config.Datastore.AutoUpgrade)
83}
84
85// open the database, giving it at most one chance to auto-upgrade the schema
86func openDatabaseInternal(config *Config, allowAutoupgrade bool) (db *buntdb.DB, err error) {
87	db, err = buntdb.Open(config.Datastore.Path)
88	if err != nil {
89		return
90	}
91
92	defer func() {
93		if err != nil && db != nil {
94			db.Close()
95			db = nil
96		}
97	}()
98
99	// read the current version string
100	var version int
101	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		}
106		return err
107	})
108	if err != nil {
109		return
110	}
111
112	if version == latestDbSchema {
113		// success
114		return
115	}
116
117	// XXX quiesce the DB so we can be sure it's safe to make a backup copy
118	db.Close()
119	db = nil
120	if allowAutoupgrade {
121		err = performAutoUpgrade(version, config)
122		if err != nil {
123			return
124		}
125		// successful autoupgrade, let's try this again:
126		return openDatabaseInternal(config, false)
127	} else {
128		err = &utils.IncompatibleSchemaError{CurrentVersion: version, RequiredVersion: latestDbSchema}
129		return
130	}
131}
132
133func performAutoUpgrade(currentVersion int, config *Config) (err error) {
134	path := config.Datastore.Path
135	log.Printf("attempting to auto-upgrade schema from version %d to %d\n", currentVersion, latestDbSchema)
136	timestamp := time.Now().UTC().Format("2006-01-02-15:04:05.000Z")
137	backupPath := fmt.Sprintf("%s.v%d.%s.bak", path, currentVersion, timestamp)
138	log.Printf("making a backup of current database at %s\n", backupPath)
139	err = utils.CopyFile(path, backupPath)
140	if err != nil {
141		return err
142	}
143
144	err = UpgradeDB(config)
145	if err != nil {
146		// database upgrade is a single transaction, so we don't need to restore the backup;
147		// we can just delete it
148		os.Remove(backupPath)
149	}
150	return err
151}
152
153// UpgradeDB upgrades the datastore to the latest schema.
154func UpgradeDB(config *Config) (err error) {
155	// #715: test that the database exists
156	_, err = os.Stat(config.Datastore.Path)
157	if err != nil {
158		return err
159	}
160
161	store, err := buntdb.Open(config.Datastore.Path)
162	if err != nil {
163		return err
164	}
165	defer store.Close()
166
167	var version int
168	err = store.Update(func(tx *buntdb.Tx) error {
169		for {
170			vStr, _ := tx.Get(keySchemaVersion)
171			version, _ = strconv.Atoi(vStr)
172			if version == latestDbSchema {
173				// success!
174				break
175			}
176			change, ok := getSchemaChange(version)
177			if !ok {
178				// unable to upgrade to the desired version, roll back
179				return &utils.IncompatibleSchemaError{CurrentVersion: version, RequiredVersion: latestDbSchema}
180			}
181			log.Printf("attempting to update schema from version %d\n", version)
182			err := change.Changer(config, tx)
183			if err != nil {
184				return err
185			}
186			_, _, err = tx.Set(keySchemaVersion, strconv.Itoa(change.TargetVersion), nil)
187			if err != nil {
188				return err
189			}
190			log.Printf("successfully updated schema to version %d\n", change.TargetVersion)
191		}
192		return nil
193	})
194
195	if err != nil {
196		log.Printf("database upgrade failed and was rolled back: %v\n", err)
197	}
198	return err
199}
200
201func 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
207}
208
209func StoreCloakSecret(db *buntdb.DB, secret string) {
210	db.Update(func(tx *buntdb.Tx) error {
211		tx.Set(keyCloakSecret, secret, nil)
212		return nil
213	})
214}
215
216func schemaChangeV1toV2(config *Config, tx *buntdb.Tx) error {
217	// == version 1 -> 2 ==
218	// account key changes and account.verified key bugfix.
219
220	var keysToRemove []string
221	newKeys := make(map[string]string)
222
223	tx.AscendKeys("account *", func(key, value string) bool {
224		keysToRemove = append(keysToRemove, key)
225		splitkey := strings.Split(key, " ")
226
227		// work around bug
228		if splitkey[2] == "exists" {
229			// manually create new verified key
230			newVerifiedKey := fmt.Sprintf("%s.verified %s", splitkey[0], splitkey[1])
231			newKeys[newVerifiedKey] = "1"
232		} else if splitkey[1] == "%s" {
233			return true
234		}
235
236		newKey := fmt.Sprintf("%s.%s %s", splitkey[0], splitkey[2], splitkey[1])
237		newKeys[newKey] = value
238
239		return true
240	})
241
242	for _, key := range keysToRemove {
243		tx.Delete(key)
244	}
245	for key, value := range newKeys {
246		tx.Set(key, value, nil)
247	}
248
249	return nil
250}
251
252// 1. channel founder names should be casefolded
253// 2. founder should be explicitly granted the ChannelFounder user mode
254// 3. explicitly initialize stored channel modes to the server default values
255func schemaChangeV2ToV3(config *Config, tx *buntdb.Tx) error {
256	var channels []string
257	prefix := "channel.exists "
258	tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool {
259		if !strings.HasPrefix(key, prefix) {
260			return false
261		}
262		chname := strings.TrimPrefix(key, prefix)
263		channels = append(channels, chname)
264		return true
265	})
266
267	// founder names should be casefolded
268	// founder should be explicitly granted the ChannelFounder user mode
269	for _, channel := range channels {
270		founderKey := "channel.founder " + channel
271		founder, _ := tx.Get(founderKey)
272		if founder != "" {
273			founder, err := CasefoldName(founder)
274			if err == nil {
275				tx.Set(founderKey, founder, nil)
276				accountToUmode := map[string]modes.Mode{
277					founder: modes.ChannelFounder,
278				}
279				atustr, _ := json.Marshal(accountToUmode)
280				tx.Set("channel.accounttoumode "+channel, string(atustr), nil)
281			}
282		}
283	}
284
285	// explicitly store the channel modes
286	defaultModes := config.Channels.defaultModes
287	modeStrings := make([]string, len(defaultModes))
288	for i, mode := range defaultModes {
289		modeStrings[i] = string(mode)
290	}
291	defaultModeString := strings.Join(modeStrings, "")
292	for _, channel := range channels {
293		tx.Set("channel.modes "+channel, defaultModeString, nil)
294	}
295
296	return nil
297}
298
299// 1. ban info format changed (from `legacyBanInfo` below to `IPBanInfo`)
300// 2. dlines against individual IPs are normalized into dlines against the appropriate /128 network
301func schemaChangeV3ToV4(config *Config, tx *buntdb.Tx) error {
302	type ipRestrictTime struct {
303		Duration time.Duration
304		Expires  time.Time
305	}
306	type legacyBanInfo struct {
307		Reason     string          `json:"reason"`
308		OperReason string          `json:"oper_reason"`
309		OperName   string          `json:"oper_name"`
310		Time       *ipRestrictTime `json:"time"`
311	}
312
313	now := time.Now()
314	legacyToNewInfo := func(old legacyBanInfo) (new_ IPBanInfo) {
315		new_.Reason = old.Reason
316		new_.OperReason = old.OperReason
317		new_.OperName = old.OperName
318
319		if old.Time == nil {
320			new_.TimeCreated = now
321			new_.Duration = 0
322		} else {
323			new_.TimeCreated = old.Time.Expires.Add(-1 * old.Time.Duration)
324			new_.Duration = old.Time.Duration
325		}
326		return
327	}
328
329	var keysToDelete []string
330
331	prefix := "bans.dline "
332	dlines := make(map[string]IPBanInfo)
333	tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool {
334		if !strings.HasPrefix(key, prefix) {
335			return false
336		}
337		keysToDelete = append(keysToDelete, key)
338
339		var lbinfo legacyBanInfo
340		id := strings.TrimPrefix(key, prefix)
341		err := json.Unmarshal([]byte(value), &lbinfo)
342		if err != nil {
343			log.Printf("error unmarshaling legacy dline: %v\n", err)
344			return true
345		}
346		// legacy keys can be either an IP or a CIDR
347		hostNet, err := utils.NormalizedNetFromString(id)
348		if err != nil {
349			log.Printf("error unmarshaling legacy dline network: %v\n", err)
350			return true
351		}
352		dlines[utils.NetToNormalizedString(hostNet)] = legacyToNewInfo(lbinfo)
353
354		return true
355	})
356
357	setOptions := func(info IPBanInfo) *buntdb.SetOptions {
358		if info.Duration == 0 {
359			return nil
360		}
361		ttl := info.TimeCreated.Add(info.Duration).Sub(now)
362		return &buntdb.SetOptions{Expires: true, TTL: ttl}
363	}
364
365	// store the new dlines
366	for id, info := range dlines {
367		b, err := json.Marshal(info)
368		if err != nil {
369			log.Printf("error marshaling migrated dline: %v\n", err)
370			continue
371		}
372		tx.Set(fmt.Sprintf("bans.dlinev2 %s", id), string(b), setOptions(info))
373	}
374
375	// same operations against klines
376	prefix = "bans.kline "
377	klines := make(map[string]IPBanInfo)
378	tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool {
379		if !strings.HasPrefix(key, prefix) {
380			return false
381		}
382		keysToDelete = append(keysToDelete, key)
383		mask := strings.TrimPrefix(key, prefix)
384		var lbinfo legacyBanInfo
385		err := json.Unmarshal([]byte(value), &lbinfo)
386		if err != nil {
387			log.Printf("error unmarshaling legacy kline: %v\n", err)
388			return true
389		}
390		klines[mask] = legacyToNewInfo(lbinfo)
391		return true
392	})
393
394	for mask, info := range klines {
395		b, err := json.Marshal(info)
396		if err != nil {
397			log.Printf("error marshaling migrated kline: %v\n", err)
398			continue
399		}
400		tx.Set(fmt.Sprintf("bans.klinev2 %s", mask), string(b), setOptions(info))
401	}
402
403	// clean up all the old entries
404	for _, key := range keysToDelete {
405		tx.Delete(key)
406	}
407
408	return nil
409}
410
411// create new key tracking channels that belong to an account
412func schemaChangeV4ToV5(config *Config, tx *buntdb.Tx) error {
413	founderToChannels := make(map[string][]string)
414	prefix := "channel.founder "
415	tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool {
416		if !strings.HasPrefix(key, prefix) {
417			return false
418		}
419		channel := strings.TrimPrefix(key, prefix)
420		founderToChannels[value] = append(founderToChannels[value], channel)
421		return true
422	})
423
424	for founder, channels := range founderToChannels {
425		tx.Set(fmt.Sprintf("account.channels %s", founder), strings.Join(channels, ","), nil)
426	}
427	return nil
428}
429
430// custom nick enforcement was a separate db key, now it's part of settings
431func schemaChangeV5ToV6(config *Config, tx *buntdb.Tx) error {
432	accountToEnforcement := make(map[string]NickEnforcementMethod)
433	prefix := "account.customenforcement "
434	tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool {
435		if !strings.HasPrefix(key, prefix) {
436			return false
437		}
438		account := strings.TrimPrefix(key, prefix)
439		method, err := nickReservationFromString(value)
440		if err == nil {
441			accountToEnforcement[account] = method
442		} else {
443			log.Printf("skipping corrupt custom enforcement value for %s\n", account)
444		}
445		return true
446	})
447
448	for account, method := range accountToEnforcement {
449		var settings AccountSettings
450		settings.NickEnforcement = method
451		text, err := json.Marshal(settings)
452		if err != nil {
453			return err
454		}
455		tx.Delete(prefix + account)
456		tx.Set(fmt.Sprintf("account.settings %s", account), string(text), nil)
457	}
458	return nil
459}
460
461type maskInfoV7 struct {
462	TimeCreated     time.Time
463	CreatorNickmask string
464	CreatorAccount  string
465}
466
467func schemaChangeV6ToV7(config *Config, tx *buntdb.Tx) error {
468	now := time.Now().UTC()
469	var channels []string
470	prefix := "channel.exists "
471	tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool {
472		if !strings.HasPrefix(key, prefix) {
473			return false
474		}
475		channels = append(channels, strings.TrimPrefix(key, prefix))
476		return true
477	})
478
479	converter := func(key string) {
480		oldRawValue, err := tx.Get(key)
481		if err != nil {
482			return
483		}
484		var masks []string
485		err = json.Unmarshal([]byte(oldRawValue), &masks)
486		if err != nil {
487			return
488		}
489		newCookedValue := make(map[string]maskInfoV7)
490		for _, mask := range masks {
491			normalizedMask, err := CanonicalizeMaskWildcard(mask)
492			if err != nil {
493				continue
494			}
495			newCookedValue[normalizedMask] = maskInfoV7{
496				TimeCreated:     now,
497				CreatorNickmask: "*",
498				CreatorAccount:  "*",
499			}
500		}
501		newRawValue, err := json.Marshal(newCookedValue)
502		if err != nil {
503			return
504		}
505		tx.Set(key, string(newRawValue), nil)
506	}
507
508	prefixes := []string{
509		"channel.banlist %s",
510		"channel.exceptlist %s",
511		"channel.invitelist %s",
512	}
513	for _, channel := range channels {
514		for _, prefix := range prefixes {
515			converter(fmt.Sprintf(prefix, channel))
516		}
517	}
518	return nil
519}
520
521type accountSettingsLegacyV7 struct {
522	AutoreplayLines *int
523	NickEnforcement NickEnforcementMethod
524	AllowBouncer    MulticlientAllowedSetting
525	AutoreplayJoins bool
526}
527
528type accountSettingsLegacyV8 struct {
529	AutoreplayLines *int
530	NickEnforcement NickEnforcementMethod
531	AllowBouncer    MulticlientAllowedSetting
532	ReplayJoins     ReplayJoinsSetting
533}
534
535// #616: change autoreplay-joins to replay-joins
536func schemaChangeV7ToV8(config *Config, tx *buntdb.Tx) error {
537	prefix := "account.settings "
538	var accounts, blobs []string
539	tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool {
540		var legacy accountSettingsLegacyV7
541		var current accountSettingsLegacyV8
542		if !strings.HasPrefix(key, prefix) {
543			return false
544		}
545		account := strings.TrimPrefix(key, prefix)
546		err := json.Unmarshal([]byte(value), &legacy)
547		if err != nil {
548			log.Printf("corrupt record for %s: %v\n", account, err)
549			return true
550		}
551		current.AutoreplayLines = legacy.AutoreplayLines
552		current.NickEnforcement = legacy.NickEnforcement
553		current.AllowBouncer = legacy.AllowBouncer
554		if legacy.AutoreplayJoins {
555			current.ReplayJoins = ReplayJoinsAlways
556		} else {
557			current.ReplayJoins = ReplayJoinsCommandsOnly
558		}
559		blob, err := json.Marshal(current)
560		if err != nil {
561			log.Printf("could not marshal record for %s: %v\n", account, err)
562			return true
563		}
564		accounts = append(accounts, account)
565		blobs = append(blobs, string(blob))
566		return true
567	})
568	for i, account := range accounts {
569		tx.Set(prefix+account, blobs[i], nil)
570	}
571	return nil
572}
573
574type accountCredsLegacyV8 struct {
575	Version        uint
576	PassphraseSalt []byte // legacy field, not used by v1 and later
577	PassphraseHash []byte
578	Certificate    string
579}
580
581type accountCredsLegacyV9 struct {
582	Version        uint
583	PassphraseSalt []byte // legacy field, not used by v1 and later
584	PassphraseHash []byte
585	Certfps        []string
586}
587
588// #530: support multiple client certificate fingerprints
589func schemaChangeV8ToV9(config *Config, tx *buntdb.Tx) error {
590	prefix := "account.credentials "
591	var accounts, blobs []string
592	tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool {
593		var legacy accountCredsLegacyV8
594		var current accountCredsLegacyV9
595		if !strings.HasPrefix(key, prefix) {
596			return false
597		}
598		account := strings.TrimPrefix(key, prefix)
599		err := json.Unmarshal([]byte(value), &legacy)
600		if err != nil {
601			log.Printf("corrupt record for %s: %v\n", account, err)
602			return true
603		}
604		current.Version = legacy.Version
605		current.PassphraseSalt = legacy.PassphraseSalt // ugh can't get rid of this
606		current.PassphraseHash = legacy.PassphraseHash
607		if legacy.Certificate != "" {
608			current.Certfps = []string{legacy.Certificate}
609		}
610		blob, err := json.Marshal(current)
611		if err != nil {
612			log.Printf("could not marshal record for %s: %v\n", account, err)
613			return true
614		}
615		accounts = append(accounts, account)
616		blobs = append(blobs, string(blob))
617		return true
618	})
619	for i, account := range accounts {
620		tx.Set(prefix+account, blobs[i], nil)
621	}
622	return nil
623}
624
625// #836: account registration time at nanosecond resolution
626// (mostly to simplify testing)
627func schemaChangeV9ToV10(config *Config, tx *buntdb.Tx) error {
628	prefix := "account.registered.time "
629	var accounts, times []string
630	tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool {
631		if !strings.HasPrefix(key, prefix) {
632			return false
633		}
634		account := strings.TrimPrefix(key, prefix)
635		accounts = append(accounts, account)
636		times = append(times, value)
637		return true
638	})
639	for i, account := range accounts {
640		time, err := strconv.ParseInt(times[i], 10, 64)
641		if err != nil {
642			log.Printf("corrupt registration time entry for %s: %v\n", account, err)
643			continue
644		}
645		time = time * 1000000000
646		tx.Set(prefix+account, strconv.FormatInt(time, 10), nil)
647	}
648	return nil
649}
650
651// #952: move the cloak secret into the database,
652// generate a new one if necessary
653func schemaChangeV10ToV11(config *Config, tx *buntdb.Tx) error {
654	cloakSecret := config.Server.Cloaks.LegacySecretValue
655	if cloakSecret == "" || cloakSecret == "siaELnk6Kaeo65K3RCrwJjlWaZ-Bt3WuZ2L8MXLbNb4" {
656		cloakSecret = utils.GenerateSecretKey()
657	}
658	_, _, err := tx.Set(keyCloakSecret, cloakSecret, nil)
659	return err
660}
661
662// #1027: NickEnforcementTimeout (2) was removed,
663// NickEnforcementStrict was 3 and is now 2
664func schemaChangeV11ToV12(config *Config, tx *buntdb.Tx) error {
665	prefix := "account.settings "
666	var accounts, rawSettings []string
667	tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool {
668		if !strings.HasPrefix(key, prefix) {
669			return false
670		}
671		account := strings.TrimPrefix(key, prefix)
672		accounts = append(accounts, account)
673		rawSettings = append(rawSettings, value)
674		return true
675	})
676
677	for i, account := range accounts {
678		var settings AccountSettings
679		err := json.Unmarshal([]byte(rawSettings[i]), &settings)
680		if err != nil {
681			log.Printf("corrupt account settings entry for %s: %v\n", account, err)
682			continue
683		}
684		// upgrade NickEnforcementTimeout (which was 2) to NickEnforcementStrict (currently 2),
685		// fix up the old value of NickEnforcementStrict (3) to the current value (2)
686		if int(settings.NickEnforcement) == 3 {
687			settings.NickEnforcement = NickEnforcementMethod(2)
688			text, err := json.Marshal(settings)
689			if err != nil {
690				return err
691			}
692			tx.Set(prefix+account, string(text), nil)
693		}
694	}
695	return nil
696}
697
698type accountCredsLegacyV13 struct {
699	Version        CredentialsVersion
700	PassphraseHash []byte
701	Certfps        []string
702}
703
704// see #212 / #284. this packs the legacy salts into a single passphrase hash,
705// allowing legacy passphrases to be verified using the new API `checkLegacyPassphrase`.
706func schemaChangeV12ToV13(config *Config, tx *buntdb.Tx) error {
707	salt, err := tx.Get("crypto.salt")
708	if err != nil {
709		return nil // no change required
710	}
711	tx.Delete("crypto.salt")
712	rawSalt, err := base64.StdEncoding.DecodeString(salt)
713	if err != nil {
714		return nil // just throw away the creds at this point
715	}
716	prefix := "account.credentials "
717	var accounts []string
718	var credentials []accountCredsLegacyV13
719	tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool {
720		if !strings.HasPrefix(key, prefix) {
721			return false
722		}
723		account := strings.TrimPrefix(key, prefix)
724
725		var credsOld accountCredsLegacyV9
726		err = json.Unmarshal([]byte(value), &credsOld)
727		if err != nil {
728			return true
729		}
730		// skip if these aren't legacy creds!
731		if credsOld.Version != 0 {
732			return true
733		}
734
735		var credsNew accountCredsLegacyV13
736		credsNew.Version = 0 // mark hash for migration
737		credsNew.Certfps = credsOld.Certfps
738		credsNew.PassphraseHash = append(credsNew.PassphraseHash, rawSalt...)
739		credsNew.PassphraseHash = append(credsNew.PassphraseHash, credsOld.PassphraseSalt...)
740		credsNew.PassphraseHash = append(credsNew.PassphraseHash, credsOld.PassphraseHash...)
741
742		accounts = append(accounts, account)
743		credentials = append(credentials, credsNew)
744		return true
745	})
746
747	for i, account := range accounts {
748		bytesOut, err := json.Marshal(credentials[i])
749		if err != nil {
750			return err
751		}
752		_, _, err = tx.Set(prefix+account, string(bytesOut), nil)
753		if err != nil {
754			return err
755		}
756	}
757
758	return nil
759}
760
761// channel registration time and topic set time at nanosecond resolution
762func schemaChangeV13ToV14(config *Config, tx *buntdb.Tx) error {
763	prefix := "channel.registered.time "
764	var channels, times []string
765	tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool {
766		if !strings.HasPrefix(key, prefix) {
767			return false
768		}
769		channel := strings.TrimPrefix(key, prefix)
770		channels = append(channels, channel)
771		times = append(times, value)
772		return true
773	})
774
775	billion := int64(time.Second)
776	for i, channel := range channels {
777		regTime, err := strconv.ParseInt(times[i], 10, 64)
778		if err != nil {
779			log.Printf("corrupt registration time entry for %s: %v\n", channel, err)
780			continue
781		}
782		regTime = regTime * billion
783		tx.Set(prefix+channel, strconv.FormatInt(regTime, 10), nil)
784
785		topicTimeKey := "channel.topic.settime " + channel
786		topicSetAt, err := tx.Get(topicTimeKey)
787		if err == nil {
788			if setTime, err := strconv.ParseInt(topicSetAt, 10, 64); err == nil {
789				tx.Set(topicTimeKey, strconv.FormatInt(setTime*billion, 10), nil)
790			}
791		}
792	}
793	return nil
794}
795
796// #1327: delete any invalid klines
797func schemaChangeV14ToV15(config *Config, tx *buntdb.Tx) error {
798	prefix := "bans.klinev2 "
799	var keys []string
800	tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool {
801		if !strings.HasPrefix(key, prefix) {
802			return false
803		}
804		if key != strings.TrimSpace(key) {
805			keys = append(keys, key)
806		}
807		return true
808	})
809	// don't bother trying to fix these up
810	for _, key := range keys {
811		tx.Delete(key)
812	}
813	return nil
814}
815
816// #1330: delete any stale realname records
817func schemaChangeV15ToV16(config *Config, tx *buntdb.Tx) error {
818	prefix := "account.realname "
819	verifiedPrefix := "account.verified "
820	var keys []string
821	tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool {
822		if !strings.HasPrefix(key, prefix) {
823			return false
824		}
825		acct := strings.TrimPrefix(key, prefix)
826		verifiedKey := verifiedPrefix + acct
827		_, verifiedErr := tx.Get(verifiedKey)
828		if verifiedErr != nil {
829			keys = append(keys, key)
830		}
831		return true
832	})
833	for _, key := range keys {
834		tx.Delete(key)
835	}
836	return nil
837}
838
839// #1346: remove vhost request queue
840func schemaChangeV16ToV17(config *Config, tx *buntdb.Tx) error {
841	prefix := "vhostQueue "
842	var keys []string
843	tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool {
844		if !strings.HasPrefix(key, prefix) {
845			return false
846		}
847		keys = append(keys, key)
848		return true
849	})
850
851	for _, key := range keys {
852		tx.Delete(key)
853	}
854	return nil
855}
856
857// #1274: we used to suspend accounts by deleting their "verified" key,
858// now we save some metadata under a new key
859func schemaChangeV17ToV18(config *Config, tx *buntdb.Tx) error {
860	now := time.Now().UTC()
861
862	exists := "account.exists "
863	suspended := "account.suspended "
864	verif := "account.verified "
865	verifCode := "account.verificationcode "
866
867	var accounts []string
868
869	tx.AscendGreaterOrEqual("", exists, func(key, value string) bool {
870		if !strings.HasPrefix(key, exists) {
871			return false
872		}
873		account := strings.TrimPrefix(key, exists)
874		_, verifiedErr := tx.Get(verif + account)
875		_, verifCodeErr := tx.Get(verifCode + account)
876		if verifiedErr != nil && verifCodeErr != nil {
877			// verified key not present, but there's no code either,
878			// this is a suspension
879			accounts = append(accounts, account)
880		}
881		return true
882	})
883
884	type accountSuspensionV18 struct {
885		TimeCreated time.Time
886		Duration    time.Duration
887		OperName    string
888		Reason      string
889	}
890
891	for _, account := range accounts {
892		var sus accountSuspensionV18
893		sus.TimeCreated = now
894		sus.OperName = "*"
895		sus.Reason = "[unknown]"
896		susBytes, err := json.Marshal(sus)
897		if err != nil {
898			return err
899		}
900		tx.Set(suspended+account, string(susBytes), nil)
901	}
902
903	return nil
904}
905
906// #1345: persist the channel-user modes of always-on clients
907func schemaChangeV18To19(config *Config, tx *buntdb.Tx) error {
908	channelToAmodesCache := make(map[string]map[string]modes.Mode)
909	joinedto := "account.joinedto "
910	var accounts []string
911	var channels [][]string
912	tx.AscendGreaterOrEqual("", joinedto, func(key, value string) bool {
913		if !strings.HasPrefix(key, joinedto) {
914			return false
915		}
916		accounts = append(accounts, strings.TrimPrefix(key, joinedto))
917		var ch []string
918		if value != "" {
919			ch = strings.Split(value, ",")
920		}
921		channels = append(channels, ch)
922		return true
923	})
924
925	for i := 0; i < len(accounts); i++ {
926		account := accounts[i]
927		channels := channels[i]
928		tx.Delete(joinedto + account)
929		newValue := make(map[string]string, len(channels))
930		for _, channel := range channels {
931			chcfname, err := CasefoldChannel(channel)
932			if err != nil {
933				continue
934			}
935			// get amodes from the channelToAmodesCache, fill if necessary
936			amodes, ok := channelToAmodesCache[chcfname]
937			if !ok {
938				amodeStr, _ := tx.Get("channel.accounttoumode " + chcfname)
939				if amodeStr != "" {
940					jErr := json.Unmarshal([]byte(amodeStr), &amodes)
941					if jErr != nil {
942						log.Printf("error retrieving amodes for %s: %v\n", channel, jErr)
943						amodes = nil
944					}
945				}
946				// setting/using the nil value here is ok
947				channelToAmodesCache[chcfname] = amodes
948			}
949			if mode, ok := amodes[account]; ok {
950				newValue[channel] = string(mode)
951			} else {
952				newValue[channel] = ""
953			}
954		}
955		newValueBytes, jErr := json.Marshal(newValue)
956		if jErr != nil {
957			log.Printf("couldn't serialize new mode values for v19: %v\n", jErr)
958			continue
959		}
960		tx.Set("account.channeltomodes "+account, string(newValueBytes), nil)
961	}
962
963	return nil
964}
965
966// #1490: start tracking join times for always-on clients
967func schemaChangeV19To20(config *Config, tx *buntdb.Tx) error {
968	type joinData struct {
969		Modes    string
970		JoinTime int64
971	}
972
973	var accounts []string
974	var data []string
975
976	now := time.Now().UnixNano()
977
978	prefix := "account.channeltomodes "
979	tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool {
980		if !strings.HasPrefix(key, prefix) {
981			return false
982		}
983		accounts = append(accounts, strings.TrimPrefix(key, prefix))
984		data = append(data, value)
985		return true
986	})
987
988	for i, account := range accounts {
989		var existingMap map[string]string
990		err := json.Unmarshal([]byte(data[i]), &existingMap)
991		if err != nil {
992			return err
993		}
994		newMap := make(map[string]joinData)
995		for channel, modeStr := range existingMap {
996			newMap[channel] = joinData{
997				Modes:    modeStr,
998				JoinTime: now,
999			}
1000		}
1001		serialized, err := json.Marshal(newMap)
1002		if err != nil {
1003			return err
1004		}
1005		tx.Set(prefix+account, string(serialized), nil)
1006	}
1007
1008	return nil
1009}
1010
1011// #734: move the email address into the settings object,
1012// giving people a way to change it
1013func schemaChangeV20To21(config *Config, tx *buntdb.Tx) error {
1014	type accountSettingsv21 struct {
1015		AutoreplayLines  *int
1016		NickEnforcement  NickEnforcementMethod
1017		AllowBouncer     MulticlientAllowedSetting
1018		ReplayJoins      ReplayJoinsSetting
1019		AlwaysOn         PersistentStatus
1020		AutoreplayMissed bool
1021		DMHistory        HistoryStatus
1022		AutoAway         PersistentStatus
1023		Email            string
1024	}
1025	var accounts []string
1026	var emails []string
1027	callbackPrefix := "account.callback "
1028	tx.AscendGreaterOrEqual("", callbackPrefix, func(key, value string) bool {
1029		if !strings.HasPrefix(key, callbackPrefix) {
1030			return false
1031		}
1032		account := strings.TrimPrefix(key, callbackPrefix)
1033		if _, err := tx.Get("account.verified " + account); err != nil {
1034			return true
1035		}
1036		if strings.HasPrefix(value, "mailto:") {
1037			accounts = append(accounts, account)
1038			emails = append(emails, strings.TrimPrefix(value, "mailto:"))
1039		}
1040		return true
1041	})
1042	for i, account := range accounts {
1043		var settings accountSettingsv21
1044		email := emails[i]
1045		settingsKey := "account.settings " + account
1046		settingsStr, err := tx.Get(settingsKey)
1047		if err == nil && settingsStr != "" {
1048			json.Unmarshal([]byte(settingsStr), &settings)
1049		}
1050		settings.Email = email
1051		settingsBytes, err := json.Marshal(settings)
1052		if err != nil {
1053			log.Printf("couldn't marshal settings for %s: %v\n", account, err)
1054		} else {
1055			tx.Set(settingsKey, string(settingsBytes), nil)
1056		}
1057		tx.Delete(callbackPrefix + account)
1058	}
1059	return nil
1060}
1061
1062// #1676: we used to have ReplayJoinsNever, now it's desupported
1063func schemaChangeV21To22(config *Config, tx *buntdb.Tx) error {
1064	type accountSettingsv22 struct {
1065		AutoreplayLines  *int
1066		NickEnforcement  NickEnforcementMethod
1067		AllowBouncer     MulticlientAllowedSetting
1068		ReplayJoins      ReplayJoinsSetting
1069		AlwaysOn         PersistentStatus
1070		AutoreplayMissed bool
1071		DMHistory        HistoryStatus
1072		AutoAway         PersistentStatus
1073		Email            string
1074	}
1075
1076	var accounts []string
1077	var serializedSettings []string
1078	settingsPrefix := "account.settings "
1079	tx.AscendGreaterOrEqual("", settingsPrefix, func(key, value string) bool {
1080		if !strings.HasPrefix(key, settingsPrefix) {
1081			return false
1082		}
1083		if value == "" {
1084			return true
1085		}
1086		account := strings.TrimPrefix(key, settingsPrefix)
1087		if _, err := tx.Get("account.verified " + account); err != nil {
1088			return true
1089		}
1090		var settings accountSettingsv22
1091		err := json.Unmarshal([]byte(value), &settings)
1092		if err != nil {
1093			log.Printf("error (v21-22) processing settings for %s: %v\n", account, err)
1094			return true
1095		}
1096		// if necessary, change ReplayJoinsNever (2) to ReplayJoinsCommandsOnly (0)
1097		if settings.ReplayJoins == ReplayJoinsSetting(2) {
1098			settings.ReplayJoins = ReplayJoinsSetting(0)
1099			if b, err := json.Marshal(settings); err == nil {
1100				accounts = append(accounts, account)
1101				serializedSettings = append(serializedSettings, string(b))
1102			} else {
1103				log.Printf("error (v21-22) processing settings for %s: %v\n", account, err)
1104			}
1105		}
1106		return true
1107	})
1108
1109	for i, account := range accounts {
1110		tx.Set(settingsPrefix+account, serializedSettings[i], nil)
1111	}
1112	return nil
1113}
1114
1115func getSchemaChange(initialVersion int) (result SchemaChange, ok bool) {
1116	for _, change := range allChanges {
1117		if initialVersion == change.InitialVersion {
1118			return change, true
1119		}
1120	}
1121	return
1122}
1123
1124var allChanges = []SchemaChange{
1125	{
1126		InitialVersion: 1,
1127		TargetVersion:  2,
1128		Changer:        schemaChangeV1toV2,
1129	},
1130	{
1131		InitialVersion: 2,
1132		TargetVersion:  3,
1133		Changer:        schemaChangeV2ToV3,
1134	},
1135	{
1136		InitialVersion: 3,
1137		TargetVersion:  4,
1138		Changer:        schemaChangeV3ToV4,
1139	},
1140	{
1141		InitialVersion: 4,
1142		TargetVersion:  5,
1143		Changer:        schemaChangeV4ToV5,
1144	},
1145	{
1146		InitialVersion: 5,
1147		TargetVersion:  6,
1148		Changer:        schemaChangeV5ToV6,
1149	},
1150	{
1151		InitialVersion: 6,
1152		TargetVersion:  7,
1153		Changer:        schemaChangeV6ToV7,
1154	},
1155	{
1156		InitialVersion: 7,
1157		TargetVersion:  8,
1158		Changer:        schemaChangeV7ToV8,
1159	},
1160	{
1161		InitialVersion: 8,
1162		TargetVersion:  9,
1163		Changer:        schemaChangeV8ToV9,
1164	},
1165	{
1166		InitialVersion: 9,
1167		TargetVersion:  10,
1168		Changer:        schemaChangeV9ToV10,
1169	},
1170	{
1171		InitialVersion: 10,
1172		TargetVersion:  11,
1173		Changer:        schemaChangeV10ToV11,
1174	},
1175	{
1176		InitialVersion: 11,
1177		TargetVersion:  12,
1178		Changer:        schemaChangeV11ToV12,
1179	},
1180	{
1181		InitialVersion: 12,
1182		TargetVersion:  13,
1183		Changer:        schemaChangeV12ToV13,
1184	},
1185	{
1186		InitialVersion: 13,
1187		TargetVersion:  14,
1188		Changer:        schemaChangeV13ToV14,
1189	},
1190	{
1191		InitialVersion: 14,
1192		TargetVersion:  15,
1193		Changer:        schemaChangeV14ToV15,
1194	},
1195	{
1196		InitialVersion: 15,
1197		TargetVersion:  16,
1198		Changer:        schemaChangeV15ToV16,
1199	},
1200	{
1201		InitialVersion: 16,
1202		TargetVersion:  17,
1203		Changer:        schemaChangeV16ToV17,
1204	},
1205	{
1206		InitialVersion: 17,
1207		TargetVersion:  18,
1208		Changer:        schemaChangeV17ToV18,
1209	},
1210	{
1211		InitialVersion: 18,
1212		TargetVersion:  19,
1213		Changer:        schemaChangeV18To19,
1214	},
1215	{
1216		InitialVersion: 19,
1217		TargetVersion:  20,
1218		Changer:        schemaChangeV19To20,
1219	},
1220	{
1221		InitialVersion: 20,
1222		TargetVersion:  21,
1223		Changer:        schemaChangeV20To21,
1224	},
1225	{
1226		InitialVersion: 21,
1227		TargetVersion:  22,
1228		Changer:        schemaChangeV21To22,
1229	},
1230}
1231