466 lines
13 KiB
Go
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(>.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}
|
|
}
|