Files
im_server/tinode-db/gendb.go
T

466 lines
13 KiB
Go

package main
import (
"fmt"
"log"
"math/rand"
"os"
"path/filepath"
"strings"
"time"
"github.com/tinode/chat/server/auth"
_ "github.com/tinode/chat/server/auth/basic"
"github.com/tinode/chat/server/store"
"github.com/tinode/chat/server/store/types"
)
func genDb(data *Data, p2pDel bool, tenantID types.TenantID) {
var err error
var botAccount string
if len(data.Users) == 0 {
log.Println("No data provided, stopping")
return
}
// Add authentication record
authHandler := store.Store.GetAuthHandler("basic")
authHandler.Init([]byte(`{"add_to_tags": true}`), "basic")
nameIndex := make(map[string]string, len(data.Users))
log.Println("Generating users...")
for _, uu := range data.Users {
state, err := types.NewObjState(uu.State)
if err != nil {
log.Fatal(err)
}
user := types.User{
TenantID: tenantID,
State: state,
Access: types.DefaultAccess{
Auth: types.ModeCAuth,
Anon: types.ModeNone,
},
Tags: uu.Tags,
Public: parsePublic(&uu.Public, data.datapath),
}
if !uu.Trusted.IsZero() {
user.Trusted = uu.Trusted
}
user.CreatedAt = getCreatedTime(uu.CreatedAt)
user.Tags = append(user.Tags, "basic:"+uu.Username)
if uu.Email != "" {
user.Tags = append(user.Tags, "email:"+uu.Email)
}
if uu.Tel != "" {
user.Tags = append(user.Tags, "tel:"+uu.Tel)
}
// store.Users.Create will subscribe user to !me topic but won't create a !me topic
if _, err := store.Users.Create(tenantID, &user, uu.Private); err != nil {
log.Fatal(err)
}
// Save credentials: email and phone number as if they were confirmed.
if uu.Email != "" {
if _, err := store.Users.UpsertCred(tenantID, &types.Credential{
User: user.Id,
Method: "email",
Value: uu.Email,
Done: true,
}); err != nil {
log.Fatal(err)
}
}
if uu.Tel != "" {
if _, err := store.Users.UpsertCred(tenantID, &types.Credential{
User: user.Id,
Method: "tel",
Value: uu.Tel,
Done: true,
}); err != nil {
log.Fatal(err)
}
}
authLevel := auth.LevelAuth
if uu.AuthLevel != "" {
authLevel = auth.ParseAuthLevel(uu.AuthLevel)
if authLevel == auth.LevelNone {
log.Fatal("Unknown authLevel", uu.AuthLevel)
}
}
// Add authentication record
authHandler := store.Store.GetAuthHandler("basic")
passwd := uu.Password
if passwd == "(random)" {
// Generate random password
passwd = getPassword(8)
botAccount = uu.Username
}
if _, err := authHandler.AddRecord(auth.AuthContext{TenantID: tenantID},
&auth.Rec{TenantID: tenantID, Uid: user.Uid(), AuthLevel: authLevel},
[]byte(uu.Username+":"+passwd)); err != nil {
log.Fatal(err)
}
nameIndex[uu.Username] = user.Id
// Add address book as fnd.private
if len(uu.AddressBook) > 0 {
if err := store.Subs.Update(tenantID, user.Uid().FndName(), user.Uid(),
map[string]any{"Private": strings.Join(uu.AddressBook, ",")}); err != nil {
log.Fatal(err)
}
}
fmt.Println("usr;" + uu.Username + ";" + user.Uid().UserId() + ";" + passwd)
}
if botAccount == "" && len(data.Users) > 0 {
botAccount = data.Users[0].Username
}
log.Println("Generating group topics...")
for _, gt := range data.Grouptopics {
name := genTopicName()
nameIndex[gt.Name] = name
accessAuth := types.ModeCPublic
if gt.Access.Auth != "" {
if err := accessAuth.UnmarshalText([]byte(gt.Access.Auth)); err != nil {
log.Fatal("Invalid Auth access mode", gt.Access.Auth, err)
}
}
accessAnon := types.ModeCReadOnly
if gt.Access.Anon != "" {
if err := accessAnon.UnmarshalText([]byte(gt.Access.Anon)); err != nil {
log.Fatal("Invalid Anon access mode", gt.Access.Anon, err)
}
}
topic := &types.Topic{
ObjHeader: types.ObjHeader{Id: name},
TenantID: tenantID,
Access: types.DefaultAccess{
Auth: accessAuth,
Anon: accessAnon,
},
UseBt: gt.Channel,
Tags: gt.Tags,
Public: parsePublic(&gt.Public, data.datapath),
}
if !gt.Trusted.IsZero() {
topic.Trusted = gt.Trusted
}
var owner types.Uid
if gt.Owner != "" {
owner = types.ParseUid(nameIndex[gt.Owner])
if owner.IsZero() {
log.Fatal("Invalid owner", gt.Owner, "for topic", gt.Name)
}
topic.GiveAccess(owner, types.ModeCFull, types.ModeCFull)
}
topic.CreatedAt = getCreatedTime(gt.CreatedAt)
if err = store.Topics.Create(tenantID, topic, owner, gt.OwnerPrivate); err != nil {
log.Fatal(err)
}
fmt.Println("grp;" + gt.Name + ";" + name)
}
log.Println("Generating P2P subscriptions...")
for i, ss := range data.P2psubs {
if ss.Users[0].Name < ss.Users[1].Name {
ss.pair = ss.Users[0].Name + ":" + ss.Users[1].Name
} else {
ss.pair = ss.Users[1].Name + ":" + ss.Users[0].Name
}
uid1 := types.ParseUid(nameIndex[ss.Users[0].Name])
uid2 := types.ParseUid(nameIndex[ss.Users[1].Name])
topic := uid1.P2PName(uid2)
created := getCreatedTime(ss.CreatedAt)
// Assign default access mode
defaultMode := types.ModeCP2P
if p2pDel {
defaultMode = types.ModeCP2PD
}
s0want := defaultMode
s0given := defaultMode
s1want := defaultMode
s1given := defaultMode
// Check of non-default access mode was provided
if ss.Users[0].Want != "" {
if err := s0want.UnmarshalText([]byte(ss.Users[0].Want)); err != nil {
log.Fatal(err)
}
}
if ss.Users[0].Have != "" {
if err := s0given.UnmarshalText([]byte(ss.Users[0].Have)); err != nil {
log.Fatal(err)
}
}
if ss.Users[1].Want != "" {
if err := s1want.UnmarshalText([]byte(ss.Users[1].Want)); err != nil {
log.Fatal(err)
}
}
if ss.Users[1].Have != "" {
if err := s1given.UnmarshalText([]byte(ss.Users[1].Have)); err != nil {
log.Fatal(err)
}
}
err := store.Topics.CreateP2P(tenantID,
&types.Subscription{
ObjHeader: types.ObjHeader{CreatedAt: created},
TenantID: tenantID,
User: uid1.String(),
Topic: topic,
ModeWant: s0want,
ModeGiven: s0given,
Private: ss.Users[0].Private},
&types.Subscription{
ObjHeader: types.ObjHeader{CreatedAt: created},
TenantID: tenantID,
User: uid2.String(),
Topic: topic,
ModeWant: s1want,
ModeGiven: s1given,
Private: ss.Users[1].Private})
if err != nil {
log.Fatal(err)
}
data.P2psubs[i].pair = ss.pair
nameIndex[ss.pair] = topic
fmt.Println("p2p;" + ss.pair + ";" + topic)
}
log.Println("Generating group subscriptions...")
for _, ss := range data.Groupsubs {
var want, given types.AccessMode
if ss.AsChan {
want = types.ModeCChnReader
given = types.ModeCChnReader
} else {
want = types.ModeCPublic
given = types.ModeCPublic
}
if ss.Want != "" {
if err := want.UnmarshalText([]byte(ss.Want)); err != nil {
log.Fatal(err)
}
}
if ss.Have != "" {
if err := given.UnmarshalText([]byte(ss.Have)); err != nil {
log.Fatal(err)
}
}
tname := nameIndex[ss.Topic]
if ss.AsChan {
tname = types.GrpToChn(tname)
}
if err = store.Subs.Create(tenantID, &types.Subscription{
ObjHeader: types.ObjHeader{CreatedAt: getCreatedTime(ss.CreatedAt)},
TenantID: tenantID,
User: nameIndex[ss.User],
Topic: tname,
ModeWant: want,
ModeGiven: given,
Private: ss.Private}); err != nil {
log.Fatal(err)
}
}
seqIds := map[string]int{}
now := types.TimeNow().Add(-time.Minute * 10)
messageCount := len(data.Messages)
if messageCount > 0 {
log.Println("Inserting messages...")
if messageCount > 1 {
// Shuffle messages
rand.Shuffle(len(data.Messages), func(i, j int) {
data.Messages[i], data.Messages[j] = data.Messages[j], data.Messages[i]
})
// Starting 4 days ago.
timestamp := now.Add(time.Hour * time.Duration(-24*4))
toInsert := 96 // 96 is the maximum, otherwise messages may appear in the future
// Initial maximum increment of the message sent time in milliseconds
increment := 3600 * 1000
subIdx := rand.Intn(len(data.Groupsubs) + len(data.P2psubs)*2)
for i := range toInsert {
// At least 20% of subsequent messages should come from the same user in the same topic.
if rand.Intn(5) > 0 {
subIdx = rand.Intn(len(data.Groupsubs) + len(data.P2psubs)*2)
}
var topic string
var from types.Uid
if subIdx < len(data.Groupsubs) {
if data.Groupsubs[subIdx].AsChan {
// Channel readers should not have any published messages.
continue
}
topic = nameIndex[data.Groupsubs[subIdx].Topic]
from = types.ParseUid(nameIndex[data.Groupsubs[subIdx].User])
} else {
idx := (subIdx - len(data.Groupsubs)) / 2
usr := (subIdx - len(data.Groupsubs)) % 2
sub := data.P2psubs[idx]
topic = nameIndex[sub.pair]
from = types.ParseUid(nameIndex[sub.Users[usr].Name])
}
seqIds[topic]++
seqId := seqIds[topic]
str := data.Messages[i%len(data.Messages)]
// Max time between messages is 2 hours, averate - 1 hour, time is increasing as seqId increases
timestamp = timestamp.Add(time.Microsecond * time.Duration(rand.Intn(increment)))
if timestamp.After(now) {
now = timestamp
}
if _, err = store.Messages.Save(tenantID, &types.Message{
ObjHeader: types.ObjHeader{CreatedAt: timestamp},
TenantID: tenantID,
SeqId: seqId,
Topic: topic,
From: from.String(),
Content: str,
}, nil, true); err != nil {
log.Fatal("Failed to insert message: ", err)
}
// New increment: remaining time until 'now' divided by the number of messages to be inserted,
// then converted to milliseconds.
increment = int(now.Sub(timestamp).Nanoseconds() / int64(toInsert-i) / 1000000)
// log.Printf("Msg.seq=%d at %v, topic='%s' from='%s'", msg.SeqId, msg.CreatedAt, topic, from.UserId())
}
} else {
// Only one message is provided. Just insert it into every topic.
now := time.Now().UTC().Add(-time.Minute).Round(time.Millisecond)
for _, gt := range data.Grouptopics {
seqIds[nameIndex[gt.Name]] = 1
if _, err = store.Messages.Save(tenantID, &types.Message{
ObjHeader: types.ObjHeader{CreatedAt: now},
TenantID: tenantID,
SeqId: 1,
Topic: nameIndex[gt.Name],
From: nameIndex[gt.Owner],
Content: data.Messages[0],
}, nil, true); err != nil {
log.Fatal("Failed to insert message: ", err)
}
}
usedp2p := make(map[string]bool)
for i := 0; len(usedp2p) < len(data.P2psubs)/2; i++ {
sub := data.P2psubs[i]
if usedp2p[nameIndex[sub.pair]] {
continue
}
usedp2p[nameIndex[sub.pair]] = true
seqIds[nameIndex[sub.pair]] = 1
if _, err = store.Messages.Save(tenantID, &types.Message{
ObjHeader: types.ObjHeader{CreatedAt: now},
TenantID: tenantID,
SeqId: 1,
Topic: nameIndex[sub.pair],
From: nameIndex[sub.Users[0].Name],
Content: data.Messages[0],
}, nil, true); err != nil {
log.Fatal("Failed to insert message: ", err)
}
}
}
}
if len(data.Forms) != 0 {
from := nameIndex[botAccount]
log.Println("Inserting forms as ", botAccount, from)
ts := now
for _, form := range data.Forms {
for _, sub := range data.P2psubs {
ts = ts.Add(time.Second)
seqIds[nameIndex[sub.pair]]++
seqId := seqIds[nameIndex[sub.pair]]
if sub.Users[0].Name == botAccount || sub.Users[1].Name == botAccount {
if _, err = store.Messages.Save(tenantID, &types.Message{
ObjHeader: types.ObjHeader{CreatedAt: ts},
TenantID: tenantID,
SeqId: seqId,
Topic: nameIndex[sub.pair],
Head: types.KVMap{"mime": "text/x-drafty"},
From: from,
Content: form,
}, nil, true); err != nil {
log.Fatal("Failed to insert form: ", err)
}
}
}
}
}
log.Println("Sample data processing completed.")
}
// Go json cannot unmarshal Duration from a string, thus this hack.
func getCreatedTime(delta string) time.Time {
dd, err := time.ParseDuration(delta)
if err != nil && delta != "" {
log.Fatal("Invalid duration string", delta)
}
return time.Now().UTC().Round(time.Millisecond).Add(dd)
}
type photoStruct struct {
Type string `json:"type" db:"type"`
Data []byte `json:"data" db:"data"`
}
type card struct {
Fn string `json:"fn" db:"fn"`
Photo *photoStruct `json:"photo,omitempty" db:"photo"`
}
// {"fn": "Alice Johnson", "photo": "alice-128.jpg"}
func parsePublic(public *theCard, path string) *card {
var photo *photoStruct
var err error
if public.Fn == "" && public.Photo == "" {
return nil
}
if fname := public.Photo; fname != "" {
photo = &photoStruct{Type: public.Type}
dir, _ := filepath.Split(fname)
if dir == "" {
dir = path
}
photo.Data, err = os.ReadFile(filepath.Join(dir, fname))
if err != nil {
log.Fatal(err)
}
}
return &card{Fn: public.Fn, Photo: photo}
}