package creator import ( "context" "database/sql" "errors" "time" ) func (s *Store) CheckOwnedAccountDeletion(ctx context.Context, accountID string) error { if accountID == "" { return ErrInvalid } var exists bool if err := s.db.QueryRowContext(ctx, `SELECT EXISTS (SELECT 1 FROM social_account WHERE id = $1)`, accountID).Scan(&exists); err != nil { return databaseError(err) } if !exists { return ErrNotFound } var active bool if err := s.db.QueryRowContext(ctx, ` SELECT EXISTS ( SELECT 1 FROM creator_operation WHERE account_id = $1 AND state = 'processing' UNION ALL SELECT 1 FROM creator_event WHERE (receiving_account_id = $1 OR execution_account_id = $1) AND state = 'processing' UNION ALL SELECT 1 FROM creator_collection_checkpoint WHERE source_type = 'owned' AND source_id = $1 AND status = 'running' UNION ALL SELECT 1 FROM creator_source_sync_lease WHERE source_type = 'owned' AND source_id = $1 AND lease_until > now() )`, accountID).Scan(&active); err != nil { return databaseError(err) } if active { return ErrConflict } return nil } func (s *Store) DeleteOwnedAccountData(ctx context.Context, accountID string) error { if accountID == "" { return ErrInvalid } tx, err := s.db.BeginTx(ctx, nil) if err != nil { return errors.New("begin creator account deletion") } defer tx.Rollback() if err := lockCreatorAccount(ctx, tx, accountID); err != nil { return err } if err := checkOwnedAccountDeletionTx(ctx, tx, accountID); err != nil { return err } var secretID, secretKey string var hasSecret bool if err := tx.QueryRowContext(ctx, ` SELECT secret_reference_id, secret_key FROM creator_account_password WHERE account_id = $1`, accountID).Scan(&secretID, &secretKey); err == nil { hasSecret = true } else if !errors.Is(err, sql.ErrNoRows) { return databaseError(err) } if hasSecret && s.secrets == nil { return errors.New("creator secret bridge is unavailable") } if _, err := tx.ExecContext(ctx, ` DELETE FROM creator_operation WHERE account_id = $1 OR strategy_id IN (SELECT id FROM creator_strategy WHERE big_account_id = $1 OR execution_account_id = $1) OR event_id IN (SELECT id FROM creator_event WHERE receiving_account_id = $1 OR execution_account_id = $1)`, accountID); err != nil { return databaseError(err) } if _, err := tx.ExecContext(ctx, ` DELETE FROM creator_event WHERE receiving_account_id = $1 OR execution_account_id = $1`, accountID); err != nil { return databaseError(err) } if _, err := tx.ExecContext(ctx, ` DELETE FROM creator_cooldown WHERE big_account_id = $1 OR execution_account_id = $1`, accountID); err != nil { return databaseError(err) } if _, err := tx.ExecContext(ctx, ` DELETE FROM creator_strategy WHERE big_account_id = $1 OR execution_account_id = $1`, accountID); err != nil { return databaseError(err) } if _, err := tx.ExecContext(ctx, ` DELETE FROM creator_relation WHERE big_account_id = $1 OR small_account_id = $1`, accountID); err != nil { return databaseError(err) } if _, err := tx.ExecContext(ctx, `DELETE FROM creator_conversation WHERE account_id = $1`, accountID); err != nil { return databaseError(err) } if _, err := tx.ExecContext(ctx, `DELETE FROM creator_listener_boundary WHERE account_id = $1`, accountID); err != nil { return databaseError(err) } if _, err := tx.ExecContext(ctx, `DELETE FROM creator_listener_state WHERE account_id = $1`, accountID); err != nil { return databaseError(err) } if err := deleteCreatorSourceTx(ctx, tx, SourceOwned, accountID); err != nil { return err } if _, err := tx.ExecContext(ctx, `DELETE FROM creator_account_password WHERE account_id = $1`, accountID); err != nil { return databaseError(err) } if _, err := tx.ExecContext(ctx, `DELETE FROM creator_account_profile WHERE account_id = $1`, accountID); err != nil { return databaseError(err) } if err := tx.Commit(); err != nil { return errors.New("commit creator account deletion") } if hasSecret { if err := s.secrets.Delete(context.WithoutCancel(ctx), SecretReference{ID: secretID, Provider: "os_keyring"}, secretKey); err != nil { return errors.Join(errors.New("delete creator account password"), err) } } return nil } func (s *Store) DeleteCompetitor(ctx context.Context, competitorID string) error { if competitorID == "" { return ErrInvalid } tx, err := s.db.BeginTx(ctx, nil) if err != nil { return errors.New("begin competitor deletion") } defer tx.Rollback() var syncStatus string var syncLease sql.NullTime if err := tx.QueryRowContext(ctx, ` SELECT sync_status, sync_lease_until FROM creator_competitor WHERE id = $1 FOR UPDATE`, competitorID).Scan(&syncStatus, &syncLease); err != nil { return rowError(err) } if syncStatus == "running" || (syncLease.Valid && syncLease.Time.After(time.Now())) { return ErrConflict } var active bool if err := tx.QueryRowContext(ctx, ` SELECT EXISTS ( SELECT 1 FROM creator_collection_checkpoint WHERE source_type = 'competitor' AND source_id = $1 AND (status = 'running' OR (lease_until IS NOT NULL AND lease_until > now())) UNION ALL SELECT 1 FROM creator_source_sync_lease WHERE source_type = 'competitor' AND source_id = $1 AND lease_until > now() )`, competitorID).Scan(&active); err != nil { return databaseError(err) } if active { return ErrConflict } if err := deleteCreatorSourceTx(ctx, tx, SourceCompetitor, competitorID); err != nil { return err } if _, err := tx.ExecContext(ctx, `DELETE FROM creator_competitor WHERE id = $1`, competitorID); err != nil { return databaseError(err) } if err := tx.Commit(); err != nil { return errors.New("commit competitor deletion") } return nil } func lockCreatorAccount(ctx context.Context, tx *sql.Tx, accountID string) error { var lockedID string if err := tx.QueryRowContext(ctx, `SELECT id FROM social_account WHERE id = $1 FOR UPDATE`, accountID).Scan(&lockedID); err != nil { return rowError(err) } return nil } func checkOwnedAccountDeletionTx(ctx context.Context, tx *sql.Tx, accountID string) error { var active bool if err := tx.QueryRowContext(ctx, ` SELECT EXISTS ( SELECT 1 FROM creator_operation WHERE account_id = $1 AND state = 'processing' UNION ALL SELECT 1 FROM creator_event WHERE (receiving_account_id = $1 OR execution_account_id = $1) AND state = 'processing' UNION ALL SELECT 1 FROM creator_collection_checkpoint WHERE source_type = 'owned' AND source_id = $1 AND status = 'running' UNION ALL SELECT 1 FROM creator_source_sync_lease WHERE source_type = 'owned' AND source_id = $1 AND lease_until > now() )`, accountID).Scan(&active); err != nil { return databaseError(err) } if active { return ErrConflict } return nil } func deleteCreatorSourceTx(ctx context.Context, tx *sql.Tx, sourceType, sourceID string) error { if _, err := tx.ExecContext(ctx, ` CREATE TEMP TABLE creator_delete_work_ids (id text PRIMARY KEY) ON COMMIT DROP`); err != nil { return databaseError(err) } if _, err := tx.ExecContext(ctx, ` INSERT INTO creator_delete_work_ids (id) SELECT id FROM creator_work WHERE source_type = $1 AND source_id = $2 UNION SELECT work_id FROM creator_work_source WHERE source_type = $1 AND source_id = $2`, sourceType, sourceID); err != nil { return databaseError(err) } var active bool if err := tx.QueryRowContext(ctx, ` SELECT EXISTS ( SELECT 1 FROM creator_operation WHERE state = 'processing' AND (target_work_id IN (SELECT id FROM creator_delete_work_ids) OR target_comment_id IN (SELECT id FROM creator_comment WHERE work_id IN (SELECT id FROM creator_delete_work_ids))) UNION ALL SELECT 1 FROM creator_event WHERE state = 'processing' AND work_id IN (SELECT id FROM creator_delete_work_ids) )`).Scan(&active); err != nil { return databaseError(err) } if active { return ErrConflict } if _, err := tx.ExecContext(ctx, `DELETE FROM creator_work_source WHERE source_type = $1 AND source_id = $2`, sourceType, sourceID); err != nil { return databaseError(err) } if _, err := tx.ExecContext(ctx, `DELETE FROM creator_source_sync_lease WHERE source_type = $1 AND source_id = $2`, sourceType, sourceID); err != nil { return databaseError(err) } if _, err := tx.ExecContext(ctx, `DELETE FROM creator_collection_checkpoint WHERE source_type = $1 AND source_id = $2`, sourceType, sourceID); err != nil { return databaseError(err) } if _, err := tx.ExecContext(ctx, ` UPDATE creator_work work SET source_type = source.source_type, source_id = source.source_id FROM ( SELECT DISTINCT ON (work_id) work_id, source_type, source_id FROM creator_work_source ORDER BY work_id, created_at, source_type, source_id ) source WHERE work.id = source.work_id AND work.source_type = $1 AND work.source_id = $2`, sourceType, sourceID); err != nil { return databaseError(err) } if _, err := tx.ExecContext(ctx, ` DELETE FROM creator_operation WHERE target_work_id IN ( SELECT id FROM creator_delete_work_ids WHERE NOT EXISTS (SELECT 1 FROM creator_work_source WHERE work_id = creator_delete_work_ids.id) ) OR target_comment_id IN ( SELECT comment.id FROM creator_comment comment WHERE comment.work_id IN ( SELECT id FROM creator_delete_work_ids WHERE NOT EXISTS (SELECT 1 FROM creator_work_source WHERE work_id = creator_delete_work_ids.id) ) )`); err != nil { return databaseError(err) } if _, err := tx.ExecContext(ctx, ` DELETE FROM creator_event WHERE work_id IN ( SELECT id FROM creator_delete_work_ids WHERE NOT EXISTS (SELECT 1 FROM creator_work_source WHERE work_id = creator_delete_work_ids.id) )`); err != nil { return databaseError(err) } if _, err := tx.ExecContext(ctx, ` DELETE FROM creator_work WHERE id IN ( SELECT id FROM creator_delete_work_ids WHERE NOT EXISTS (SELECT 1 FROM creator_work_source WHERE work_id = creator_delete_work_ids.id) )`); err != nil { return databaseError(err) } return nil }