181 lines
5.4 KiB
Go
181 lines
5.4 KiB
Go
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)
|
||
// SetMessageRetentionHours sets how long messages remain visible (24–336 hours).
|
||
SetMessageRetentionHours(ctx context.Context, id string, hours int) 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) SetMessageRetentionHours(ctx context.Context, id string, hours int) error {
|
||
if hours < 24 || hours > 336 {
|
||
return ErrInvalidRetentionHours
|
||
}
|
||
err := s.repo.updateMessageRetentionHours(ctx, id, hours)
|
||
if errors.Is(err, errNotFound) {
|
||
return ErrNotFound
|
||
}
|
||
return err
|
||
}
|
||
|
||
// 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)
|
||
}
|