ergo/irc/import.go

252 lines
7.3 KiB
Go
Raw Permalink Normal View History

2020-10-02 22:48:37 +02:00
// Copyright (c) 2020 Shivaram Lingamneni <slingamn@cs.stanford.edu>
// released under the MIT license
package irc
import (
"encoding/json"
"fmt"
"log"
2021-02-17 21:11:54 +01:00
"os"
2020-10-02 22:48:37 +02:00
"strconv"
"time"
2020-10-02 22:48:37 +02:00
"github.com/tidwall/buntdb"
"github.com/ergochat/ergo/irc/bunt"
"github.com/ergochat/ergo/irc/datastore"
"github.com/ergochat/ergo/irc/modes"
2021-05-25 06:34:38 +02:00
"github.com/ergochat/ergo/irc/utils"
2020-10-02 22:48:37 +02:00
)
2020-10-27 19:05:59 +01:00
const (
// produce a hardcoded version of the database schema
// XXX instead of referencing, e.g., keyAccountExists, we should write in the string literal
// (to ensure that no matter what code changes happen elsewhere, we're still producing a
// db of the hardcoded version)
importDBSchemaVersion = 23
2020-10-27 19:05:59 +01:00
)
2020-10-02 22:48:37 +02:00
type userImport struct {
2020-10-12 21:06:17 +02:00
Name string
Hash string
Email string
RegisteredAt int64 `json:"registeredAt"`
Vhost string
AdditionalNicks []string `json:"additionalNicks"`
2020-12-04 10:37:24 +01:00
Certfps []string
2020-10-02 22:48:37 +02:00
}
type channelImport struct {
Name string
Founder string
RegisteredAt int64 `json:"registeredAt"`
Topic string
TopicSetBy string `json:"topicSetBy"`
TopicSetAt int64 `json:"topicSetAt"`
2020-10-12 21:06:17 +02:00
Amode map[string]string
Modes string
Key string
Limit int
Forward string
2020-10-02 22:48:37 +02:00
}
type databaseImport struct {
Version int
Source string
Users map[string]userImport
Channels map[string]channelImport
}
func convertAmodes(raw map[string]string, validCfUsernames utils.HashSet[string]) (result map[string]modes.Mode, err error) {
result = make(map[string]modes.Mode)
2020-10-12 21:06:17 +02:00
for accountName, mode := range raw {
if len(mode) != 1 {
return nil, fmt.Errorf("invalid mode %s for account %s", mode, accountName)
}
cfname, err := CasefoldName(accountName)
2020-12-04 10:50:40 +01:00
if err != nil || !validCfUsernames.Has(cfname) {
log.Printf("skipping invalid amode recipient %s\n", accountName)
} else {
result[cfname] = modes.Mode(mode[0])
2020-10-12 21:06:17 +02:00
}
}
return
}
func doImportDBGeneric(config *Config, dbImport databaseImport, credsType CredentialsVersion, tx *buntdb.Tx) (err error) {
2020-10-02 22:48:37 +02:00
requiredVersion := 1
if dbImport.Version != requiredVersion {
return fmt.Errorf("unsupported version of the db for import: version %d is required", requiredVersion)
}
2020-10-27 19:05:59 +01:00
tx.Set(keySchemaVersion, strconv.Itoa(importDBSchemaVersion), nil)
2020-10-02 22:48:37 +02:00
tx.Set(keyCloakSecret, utils.GenerateSecretKey(), nil)
2022-03-30 06:44:51 +02:00
cfUsernames := make(utils.HashSet[string])
skeletonToUsername := make(map[string]string)
warnSkeletons := false
2020-12-04 10:50:40 +01:00
2020-10-02 22:48:37 +02:00
for username, userInfo := range dbImport.Users {
cfUsername, err := CasefoldName(username)
skeleton, skErr := Skeleton(username)
if err != nil || skErr != nil {
log.Printf("invalid username %s: %v\n", username, err)
2020-10-02 22:48:37 +02:00
continue
}
if existingSkelUser, ok := skeletonToUsername[skeleton]; ok {
log.Printf("Users %s and %s have confusable nicknames; this may render one or both accounts unusable\n", username, existingSkelUser)
warnSkeletons = true
} else {
skeletonToUsername[skeleton] = username
}
2020-12-04 10:37:24 +01:00
var certfps []string
for _, certfp := range userInfo.Certfps {
normalizedCertfp, err := utils.NormalizeCertfp(certfp)
if err == nil {
certfps = append(certfps, normalizedCertfp)
} else {
log.Printf("invalid certfp %s for %s\n", username, certfp)
}
}
2020-10-02 22:48:37 +02:00
credentials := AccountCredentials{
2020-10-12 21:06:17 +02:00
Version: credsType,
2020-10-02 22:48:37 +02:00
PassphraseHash: []byte(userInfo.Hash),
2020-12-04 10:37:24 +01:00
Certfps: certfps,
2020-10-02 22:48:37 +02:00
}
marshaledCredentials, err := json.Marshal(&credentials)
if err != nil {
log.Printf("invalid credentials for %s: %v\n", username, err)
2020-10-02 22:48:37 +02:00
continue
}
tx.Set(fmt.Sprintf(keyAccountExists, cfUsername), "1", nil)
tx.Set(fmt.Sprintf(keyAccountVerified, cfUsername), "1", nil)
tx.Set(fmt.Sprintf(keyAccountName, cfUsername), userInfo.Name, nil)
settings := AccountSettings{Email: userInfo.Email}
settingsBytes, _ := json.Marshal(settings)
tx.Set(fmt.Sprintf(keyAccountSettings, cfUsername), string(settingsBytes), nil)
2020-10-02 22:48:37 +02:00
tx.Set(fmt.Sprintf(keyAccountCredentials, cfUsername), string(marshaledCredentials), nil)
tx.Set(fmt.Sprintf(keyAccountRegTime, cfUsername), strconv.FormatInt(userInfo.RegisteredAt, 10), nil)
if userInfo.Vhost != "" {
2020-12-07 08:20:08 +01:00
vhinfo := VHostInfo{
Enabled: true,
ApprovedVHost: userInfo.Vhost,
}
vhBytes, err := json.Marshal(vhinfo)
if err == nil {
tx.Set(fmt.Sprintf(keyAccountVHost, cfUsername), string(vhBytes), nil)
} else {
log.Printf("couldn't serialize vhost for %s: %v\n", username, err)
}
2020-10-02 22:48:37 +02:00
}
if len(userInfo.AdditionalNicks) != 0 {
tx.Set(fmt.Sprintf(keyAccountAdditionalNicks, cfUsername), marshalReservedNicks(userInfo.AdditionalNicks), nil)
}
2020-12-04 10:37:24 +01:00
for _, certfp := range certfps {
tx.Set(fmt.Sprintf(keyCertToAccount, certfp), cfUsername, nil)
}
2020-12-04 10:50:40 +01:00
cfUsernames.Add(cfUsername)
2020-10-02 22:48:37 +02:00
}
// TODO fix this:
2020-10-02 22:48:37 +02:00
for chname, chInfo := range dbImport.Channels {
_, err := CasefoldChannel(chname)
2020-10-02 22:48:37 +02:00
if err != nil {
log.Printf("invalid channel name %s: %v", chname, err)
continue
}
2020-10-12 21:06:17 +02:00
cffounder, err := CasefoldName(chInfo.Founder)
if err != nil {
log.Printf("invalid founder %s for channel %s: %v", chInfo.Founder, chname, err)
continue
}
var regInfo RegisteredChannel
regInfo.Name = chname
regInfo.UUID = utils.GenerateUUIDv4()
regInfo.Founder = cffounder
regInfo.RegisteredAt = time.Unix(0, chInfo.RegisteredAt).UTC()
2020-10-02 22:48:37 +02:00
if chInfo.Topic != "" {
regInfo.Topic = chInfo.Topic
regInfo.TopicSetBy = chInfo.TopicSetBy
regInfo.TopicSetTime = time.Unix(0, chInfo.TopicSetAt).UTC()
2020-10-02 22:48:37 +02:00
}
2020-10-02 22:48:37 +02:00
if len(chInfo.Amode) != 0 {
m, err := convertAmodes(chInfo.Amode, cfUsernames)
2020-10-02 22:48:37 +02:00
if err == nil {
regInfo.AccountToUMode = m
2020-10-02 22:48:37 +02:00
} else {
log.Printf("couldn't process amodes for %s: %v", chname, err)
2020-10-02 22:48:37 +02:00
}
}
for _, mode := range chInfo.Modes {
regInfo.Modes = append(regInfo.Modes, modes.Mode(mode))
2020-10-12 21:06:17 +02:00
}
regInfo.Key = chInfo.Key
2020-10-12 21:06:17 +02:00
if chInfo.Limit > 0 {
regInfo.UserLimit = chInfo.Limit
2020-10-12 21:06:17 +02:00
}
if chInfo.Forward != "" {
if _, err := CasefoldChannel(chInfo.Forward); err == nil {
regInfo.Forward = chInfo.Forward
}
}
if j, err := json.Marshal(regInfo); err == nil {
tx.Set(bunt.BuntKey(datastore.TableChannels, regInfo.UUID), string(j), nil)
} else {
log.Printf("couldn't serialize channel %s: %v", chname, err)
}
2020-10-02 22:48:37 +02:00
}
if warnSkeletons {
log.Printf("NOTE: you may be able to avoid confusability issues by changing the server casemapping setting to `ascii`\n")
log.Printf("However, this will prevent the use of non-ASCII Unicode characters in nicknames\n")
}
2020-10-02 22:48:37 +02:00
return nil
}
func doImportDB(config *Config, dbImport databaseImport, tx *buntdb.Tx) (err error) {
switch dbImport.Source {
case "atheme":
2020-10-12 21:06:17 +02:00
return doImportDBGeneric(config, dbImport, CredentialsAtheme, tx)
case "anope":
return doImportDBGeneric(config, dbImport, CredentialsAnope, tx)
2020-10-02 22:48:37 +02:00
default:
2020-10-12 21:06:17 +02:00
return fmt.Errorf("unsupported import source: %s", dbImport.Source)
2020-10-02 22:48:37 +02:00
}
}
func ImportDB(config *Config, infile string) (err error) {
2021-02-17 21:11:54 +01:00
data, err := os.ReadFile(infile)
2020-10-02 22:48:37 +02:00
if err != nil {
return
}
var dbImport databaseImport
err = json.Unmarshal(data, &dbImport)
if err != nil {
return err
}
err = checkDBReadyForInit(config.Datastore.Path)
if err != nil {
return err
}
db, err := buntdb.Open(config.Datastore.Path)
if err != nil {
return err
}
performImport := func(tx *buntdb.Tx) (err error) {
return doImportDB(config, dbImport, tx)
}
err = db.Update(performImport)
db.Close()
return
2020-10-02 22:48:37 +02:00
}