package social import ( "context" "database/sql" "errors" "fmt" "github.com/go-jet/jet/v2/postgres" "github.com/go-jet/jet/v2/qrm" "github.com/google/uuid" "scrabble/backend/internal/postgres/jet/backend/model" "scrabble/backend/internal/postgres/jet/backend/table" ) // Block records that blockerID has blocked blockedID. Blocking severs any // friendship or pending request between the two and, through the mutual block // checks, suppresses chat visibility and new requests/invitations in both // directions. It is idempotent. func (svc *Service) Block(ctx context.Context, blockerID, blockedID uuid.UUID) error { if blockerID == blockedID { return ErrSelfRelation } return svc.store.insertBlock(ctx, blockerID, blockedID) } // Unblock removes blockerID's block on blockedID. It is idempotent. func (svc *Service) Unblock(ctx context.Context, blockerID, blockedID uuid.UUID) error { return svc.store.deleteBlock(ctx, blockerID, blockedID) } // ListBlocks returns the account IDs blockerID has blocked. func (svc *Service) ListBlocks(ctx context.Context, blockerID uuid.UUID) ([]uuid.UUID, error) { return svc.store.listBlocks(ctx, blockerID) } // IsBlocked reports whether a block stands between a and b in either direction. func (svc *Service) IsBlocked(ctx context.Context, a, b uuid.UUID) (bool, error) { return svc.store.isBlocked(ctx, a, b) } // isBlocked reports whether a block row exists between a and b in either direction. func (s *Store) isBlocked(ctx context.Context, a, b uuid.UUID) (bool, error) { stmt := postgres.SELECT(table.Blocks.BlockerID). FROM(table.Blocks). WHERE( table.Blocks.BlockerID.EQ(postgres.UUID(a)).AND(table.Blocks.BlockedID.EQ(postgres.UUID(b))). OR(table.Blocks.BlockerID.EQ(postgres.UUID(b)).AND(table.Blocks.BlockedID.EQ(postgres.UUID(a)))), ).LIMIT(1) var row model.Blocks if err := stmt.QueryContext(ctx, s.db, &row); err != nil { if errors.Is(err, qrm.ErrNoRows) { return false, nil } return false, fmt.Errorf("social: is blocked: %w", err) } return true, nil } // insertBlock severs any friendship between the pair and inserts the block, in one // transaction; a duplicate block is ignored. func (s *Store) insertBlock(ctx context.Context, blocker, blocked uuid.UUID) error { return withTx(ctx, s.db, func(tx *sql.Tx) error { del := table.Friendships.DELETE().WHERE(edgeEither(blocker, blocked)) if _, err := del.ExecContext(ctx, tx); err != nil { return fmt.Errorf("clear friendship on block: %w", err) } ins := table.Blocks. INSERT(table.Blocks.BlockerID, table.Blocks.BlockedID). VALUES(blocker, blocked). ON_CONFLICT(table.Blocks.BlockerID, table.Blocks.BlockedID).DO_NOTHING() if _, err := ins.ExecContext(ctx, tx); err != nil { return fmt.Errorf("insert block: %w", err) } return nil }) } // deleteBlock removes a block. It is idempotent. func (s *Store) deleteBlock(ctx context.Context, blocker, blocked uuid.UUID) error { stmt := table.Blocks.DELETE().WHERE( table.Blocks.BlockerID.EQ(postgres.UUID(blocker)). AND(table.Blocks.BlockedID.EQ(postgres.UUID(blocked))), ) if _, err := stmt.ExecContext(ctx, s.db); err != nil { return fmt.Errorf("social: delete block: %w", err) } return nil } // listBlocks returns the accounts blocker has blocked. func (s *Store) listBlocks(ctx context.Context, blocker uuid.UUID) ([]uuid.UUID, error) { stmt := postgres.SELECT(table.Blocks.BlockedID). FROM(table.Blocks). WHERE(table.Blocks.BlockerID.EQ(postgres.UUID(blocker))) var rows []model.Blocks if err := stmt.QueryContext(ctx, s.db, &rows); err != nil { return nil, fmt.Errorf("social: list blocks: %w", err) } out := make([]uuid.UUID, 0, len(rows)) for _, r := range rows { out = append(out, r.BlockedID) } return out, nil }