package logic import ( "context" "errors" "fmt" "time" groupentity "spark-server/internal/module/group/entity" msgentity "spark-server/internal/module/message/entity" userentity "spark-server/internal/module/user/entity" "spark-server/internal/pkg/errs" ) func (s *Service) ensureSampleGroup(ctx context.Context, bots, targets []*userentity.User) (groupID int64, sampleCount int, err error) { if len(bots) < userentity.SampleGroupBotCount { return 0, 0, errs.ErrAvailable } owner := bots[0] existing, err := s.groups.FindByOwnerAndName(ctx, owner.ID, userentity.SampleGroupName) if err != nil && !errors.Is(err, errs.ErrNotFound) { return 0, 0, err } var g *groupentity.Group if existing != nil { g = existing } else { g, err = s.createSampleGroup(ctx, bots[:userentity.SampleGroupBotCount]) if err != nil { return 0, 0, err } } if err := s.ensureGroupMembers(ctx, g, bots[:userentity.SampleGroupBotCount], targets); err != nil { return 0, 0, err } n, err := s.ensureSampleMessages(ctx, g, bots[:userentity.SampleGroupBotCount]) if err != nil { return 0, 0, err } return g.ID, n, nil } func (s *Service) createSampleGroup(ctx context.Context, bots []*userentity.User) (*groupentity.Group, error) { convID, err := s.ids.NextID() if err != nil { return nil, err } now := time.Now().UnixMilli() owner := bots[0] g := &groupentity.Group{ ConversationID: convID, Name: userentity.SampleGroupName, OwnerID: owner.ID, Status: groupentity.StatusActive, CreatedAtMS: now, UpdatedAtMS: now, } members := make([]*groupentity.Member, 0, len(bots)) for i, b := range bots { role := groupentity.RoleMember if i == 0 { role = groupentity.RoleOwner } members = append(members, &groupentity.Member{ UserID: b.ID, Role: role, JoinedAtMS: now, }) } if err := s.groups.CreateGroup(ctx, g, members); err != nil { if errors.Is(err, errs.ErrDuplicate) { return s.groups.FindByOwnerAndName(ctx, owner.ID, userentity.SampleGroupName) } return nil, err } return g, nil } func (s *Service) ensureGroupMembers(ctx context.Context, g *groupentity.Group, bots, targets []*userentity.User) error { now := time.Now().UnixMilli() add := func(uid int64) error { if _, err := s.groups.GetMember(ctx, g.ID, uid); err == nil { return nil } else if !errors.Is(err, errs.ErrNotFound) { return err } return ignoreDup(s.groups.AddMembers(ctx, g.ID, []*groupentity.Member{ {UserID: uid, Role: groupentity.RoleMember, JoinedAtMS: now}, })) } for _, b := range bots { if err := add(b.ID); err != nil { return err } } for _, u := range targets { if err := add(u.ID); err != nil { return err } } return nil } type sampleSpec struct { Key string Sender int // bot index within sample bots slice Type msgentity.Type Content *msgentity.Content } func (s *Service) ensureSampleMessages(ctx context.Context, g *groupentity.Group, bots []*userentity.User) (int, error) { specs := sampleSpecs() n := 0 for _, spec := range specs { senderID := int64(0) if spec.Type != msgentity.TypeSystem { senderID = bots[spec.Sender%len(bots)].ID } clientID := fmt.Sprintf("seed:group:sample:%s", spec.Key) if senderID > 0 { if _, err := s.msgs.GetByClientMsgID(ctx, senderID, g.ConversationID, clientID); err == nil { n++ continue } else if err != nil && !errors.Is(err, errs.ErrNotFound) { return n, err } } msgID, err := s.ids.NextID() if err != nil { return n, err } raw, err := spec.Content.Marshal() if err != nil { return n, err } now := time.Now().UnixMilli() msg := &msgentity.Message{ ID: msgID, ConversationID: g.ConversationID, SenderID: senderID, GroupID: g.ID, ClientMsgID: clientID, Type: spec.Type, Kind: msgentity.KindGroup, Content: raw, Status: msgentity.StatusNormal, CreatedAtMS: now, } if err := s.msgs.Create(ctx, msg); err != nil { if errors.Is(err, errs.ErrDuplicate) { n++ continue } return n, err } if err := s.groups.UpdateLastMsg(ctx, g.ConversationID, msgID, now); err != nil { return n, err } n++ } return n, nil } func sampleSpecs() []sampleSpec { return []sampleSpec{ {Key: "text", Sender: 0, Type: msgentity.TypeText, Content: msgentity.NewText("Hello from Spark Samples — multi-type English demo messages live here.")}, {Key: "image", Sender: 1, Type: msgentity.TypeImage, Content: msgentity.NewImage(&msgentity.Image{ Key: "seed/demo/image.jpg", Mime: "image/jpeg", Width: 800, Height: 600, Size: 120000, ThumbKey: "seed/demo/image_thumb.jpg", })}, {Key: "voice", Sender: 2, Type: msgentity.TypeVoice, Content: msgentity.NewVoice(&msgentity.Voice{ Key: "seed/demo/voice.m4a", DurationMS: 3200, Size: 48000, })}, {Key: "video", Sender: 3, Type: msgentity.TypeVideo, Content: msgentity.NewVideo(&msgentity.Video{ Key: "seed/demo/video.mp4", Mime: "video/mp4", Width: 1280, Height: 720, DurationMS: 8000, Size: 1500000, CoverKey: "seed/demo/video_cover.jpg", })}, {Key: "emoji", Sender: 4, Type: msgentity.TypeEmoji, Content: msgentity.NewEmoji(&msgentity.Emoji{Pack: "spark", ID: "wave"})}, {Key: "gif", Sender: 5, Type: msgentity.TypeGIF, Content: msgentity.NewGIF(&msgentity.GIF{ Key: "seed/demo/cheer.gif", Mime: "image/gif", Width: 240, Height: 240, Size: 90000, })}, {Key: "file", Sender: 6, Type: msgentity.TypeFile, Content: msgentity.NewFile(&msgentity.File{ Key: "seed/demo/readme.pdf", Name: "spark-samples.pdf", Size: 20480, Mime: "application/pdf", })}, {Key: "location", Sender: 7, Type: msgentity.TypeLocation, Content: msgentity.NewLocation(&msgentity.Location{ Lat: 37.7749, Lng: -122.4194, Name: "Spark HQ", Address: "San Francisco, CA", })}, {Key: "system", Sender: 0, Type: msgentity.TypeSystem, Content: msgentity.NewSystem(&msgentity.System{ Kind: groupentity.SystemKindCreated, Body: "Sample group created for English multi-type demos.", })}, } }