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": false}`), "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, Username: uu.Username, Phone: uu.Tel, State: state, Access: types.DefaultAccess{ Auth: types.ModeCAuth, Anon: types.ModeNone, }, Public: parsePublic(&uu.Public, data.datapath), } if !uu.Trusted.IsZero() { user.Trusted = uu.Trusted } user.CreatedAt = getCreatedTime(uu.CreatedAt) // 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} }