// Package logic seeds demo bots, welcome messages, and the English sample group. package logic import ( "context" "errors" "fmt" friendentity "spark-server/internal/module/friend/entity" groupentity "spark-server/internal/module/group/entity" msgentity "spark-server/internal/module/message/entity" userentity "spark-server/internal/module/user/entity" usermodel "spark-server/internal/module/user/model" "spark-server/internal/pkg/errs" ) // IDGen issues Snowflake ids for users, conversations, and messages. type IDGen interface { NextID() (int64, error) } // Users loads and creates accounts used by the seed. type Users interface { GetByEmail(ctx context.Context, email string) (*userentity.User, error) CreateWithEmail(ctx context.Context, u *userentity.User, email string) error ListAccounts(ctx context.Context) ([]usermodel.Account, error) } // Friends writes directed friend edges. type Friends interface { GetRelation(ctx context.Context, userID, friendID int64) (*friendentity.Relation, error) AcceptFriend(ctx context.Context, in friendentity.AcceptParams) error } // Chats opens P2P conversations and bumps last_msg. type Chats interface { EnsureP2P(ctx context.Context, userA, userB int64) (int64, error) UpdateLastMsg(ctx context.Context, conversationID, msgID, atMS int64) error } // Messages persists private / group bodies. type Messages interface { Create(ctx context.Context, m *msgentity.Message) error GetByClientMsgID(ctx context.Context, senderID, conversationID int64, clientMsgID string) (*msgentity.Message, error) } // Groups creates the sample group and membership. type Groups interface { FindByOwnerAndName(ctx context.Context, ownerID int64, name string) (*groupentity.Group, error) CreateGroup(ctx context.Context, g *groupentity.Group, members []*groupentity.Member) error GetMember(ctx context.Context, groupID, userID int64) (*groupentity.Member, error) AddMembers(ctx context.Context, groupID int64, members []*groupentity.Member) error UpdateLastMsg(ctx context.Context, conversationID, msgID, atMS int64) error } // Service is the idempotent demo / onboarding seeder. type Service struct { users Users friends Friends chats Chats msgs Messages groups Groups ids IDGen } // NewService wires seed dependencies. func NewService(users Users, friends Friends, chats Chats, msgs Messages, groups Groups, ids IDGen) *Service { return &Service{users: users, friends: friends, chats: chats, msgs: msgs, groups: groups, ids: ids} } // Result summarizes what the seed pass did (created or already present). type Result struct { Bots int `json:"bots"` TestUsers int `json:"test_users"` Targets int `json:"targets"` GroupID int64 `json:"group_id"` SampleMsgs int `json:"sample_msgs"` } func (s *Service) ready() bool { return s != nil && s.users != nil && s.friends != nil && s.chats != nil && s.msgs != nil && s.groups != nil && s.ids != nil } // Run ensures bots, test accounts, per-user welcome/greeting, and the sample group. func (s *Service) Run(ctx context.Context) (*Result, error) { if !s.ready() { return nil, errs.ErrAvailable } bots, err := s.ensureBots(ctx) if err != nil { return nil, err } if err := s.ensureTestUsers(ctx); err != nil { return nil, err } targets, err := s.listTargets(ctx) if err != nil { return nil, err } welcome := bots[userentity.WelcomeBotIndex-1] for _, u := range targets { if err := s.onboardUser(ctx, u, welcome); err != nil { return nil, fmt.Errorf("onboard %d: %w", u.ID, err) } } gid, samples, err := s.ensureSampleGroup(ctx, bots, targets) if err != nil { return nil, err } return &Result{ Bots: len(bots), TestUsers: 10, Targets: len(targets), GroupID: gid, SampleMsgs: samples, }, nil } func (s *Service) listTargets(ctx context.Context) ([]*userentity.User, error) { accounts, err := s.users.ListAccounts(ctx) if err != nil { return nil, err } out := make([]*userentity.User, 0, len(accounts)) for _, a := range accounts { if a.User == nil || userentity.IsBotEmail(a.Email) { continue } if a.User.Status != userentity.StatusActive { continue } out = append(out, a.User) } return out, nil } func ignoreDup(err error) error { if err == nil || errors.Is(err, errs.ErrDuplicate) { return nil } return err }