package network import ( "context" "errors" "fmt" "strings" "github.com/flowy-live/llink/internal/utils" "github.com/jackc/pgx/v5/pgxpool" ) var ErrNotFound = errors.New("network not found") var ErrInvalidName = errors.New("name cannot be empty") var ErrCapacityExceeded = errors.New("active stream capacity exceeded") var ErrInvalidRetentionHours = errors.New("message retention hours must be between 24 and 336") type Service interface { // Create creates a network and adds adminHumanId as the first member. Returns ErrInvalidName if name is empty. Create(ctx context.Context, name, adminHumanId string) (*Network, error) // GetByID returns ErrNotFound if network doesn't exist. GetByID(ctx context.Context, id string) (*Network, error) // SetName returns ErrNotFound or ErrInvalidName. SetName(ctx context.Context, id, name string) error AddMembers(ctx context.Context, networkID string, humanIds []string) error RemoveMember(ctx context.Context, networkID, humanId string) error ListForHuman(ctx context.Context, humanId string) ([]*Network, error) IsMember(ctx context.Context, networkID, humanId string) (bool, error) // ListAll returns all networks with their members ListAll(ctx context.Context) ([]*Network, error) // Invitations (email-based, for users who haven't registered yet) InviteByEmail(ctx context.Context, networkID string, emails []string) error ListInvitationsForEmail(ctx context.Context, email string) ([]*Invitation, error) ListInvitationsForNetwork(ctx context.Context, networkID string) ([]*Invitation, error) AcceptInvitation(ctx context.Context, networkID, email, humanId string) error RevokeInvitation(ctx context.Context, networkID, email string) error } type serviceImpl struct { repo repository } func NewService(pool *pgxpool.Pool) Service { return &serviceImpl{repo: newRepository(pool)} } func (s *serviceImpl) Create(ctx context.Context, name, adminHumanId string) (*Network, error) { name = strings.TrimSpace(name) if name == "" { return nil, ErrInvalidName } network, err := s.repo.create(ctx, name, adminHumanId) if err != nil { return nil, err } err = s.AddMembers(ctx, network.ID, []string{adminHumanId}) if err != nil { return nil, err } return network, nil } func (s *serviceImpl) GetByID(ctx context.Context, id string) (*Network, error) { n, err := s.repo.getByID(ctx, id) if errors.Is(err, errNotFound) { return nil, ErrNotFound } return n, err } func (s *serviceImpl) SetName(ctx context.Context, id, name string) error { name = strings.TrimSpace(name) if name == "" { return ErrInvalidName } err := s.repo.updateName(ctx, id, name) if errors.Is(err, errNotFound) { return ErrNotFound } return err } func (s *serviceImpl) AddMembers(ctx context.Context, networkID string, humanIds []string) error { for _, humanId := range humanIds { if humanId == "" { return fmt.Errorf("invalid humanId") } if err := s.repo.addMember(ctx, networkID, humanId); err != nil { return err } } return nil } func (s *serviceImpl) RemoveMember(ctx context.Context, networkID, humanId string) error { if humanId == "" { return fmt.Errorf("invalid humanId") } return s.repo.removeMember(ctx, networkID, humanId) } func (s *serviceImpl) ListForHuman(ctx context.Context, humanId string) ([]*Network, error) { if humanId == "" { return nil, fmt.Errorf("invalid humanId") } return s.repo.getNetworksForHuman(ctx, humanId) } func (s *serviceImpl) IsMember(ctx context.Context, networkID, humanId string) (bool, error) { if humanId == "" { return false, fmt.Errorf("invalid humanId") } return s.repo.isMember(ctx, networkID, humanId) } func (s *serviceImpl) ListAll(ctx context.Context) ([]*Network, error) { return s.repo.listAll(ctx) } // Invitation methods func (s *serviceImpl) InviteByEmail(ctx context.Context, networkID string, emails []string) error { for _, email := range emails { normalized, err := utils.NormalizeEmail(email) if err != nil { return fmt.Errorf("invalid email %q: %w", email, err) } if err := s.repo.createInvitation(ctx, networkID, normalized); err != nil { return err } } return nil } func (s *serviceImpl) ListInvitationsForEmail(ctx context.Context, email string) ([]*Invitation, error) { normalized, err := utils.NormalizeEmail(email) if err != nil { return nil, fmt.Errorf("invalid email: %w", err) } return s.repo.getInvitationsByEmail(ctx, normalized) } func (s *serviceImpl) ListInvitationsForNetwork(ctx context.Context, networkID string) ([]*Invitation, error) { return s.repo.getInvitationsByNetwork(ctx, networkID) } func (s *serviceImpl) AcceptInvitation(ctx context.Context, networkID, email, humanId string) error { normalized, err := utils.NormalizeEmail(email) if err != nil { return fmt.Errorf("invalid email: %w", err) } if humanId == "" { return fmt.Errorf("invalid humanId") } if err := s.repo.deleteInvitation(ctx, networkID, normalized); err != nil { return err } return s.repo.addMember(ctx, networkID, humanId) } func (s *serviceImpl) RevokeInvitation(ctx context.Context, networkID, email string) error { normalized, err := utils.NormalizeEmail(email) if err != nil { return fmt.Errorf("invalid email: %w", err) } return s.repo.deleteInvitation(ctx, networkID, normalized) }