Skip to content

Add App/Plugin frontend and backend#1232

Merged
beastoin merged 29 commits intomainfrom
add-app
Nov 5, 2024
Merged

Add App/Plugin frontend and backend#1232
beastoin merged 29 commits intomainfrom
add-app

Conversation

@mdmohsin7
Copy link
Copy Markdown
Member

@mdmohsin7 mdmohsin7 commented Nov 1, 2024

Closes #1128

  • Add app functionality on the front and also improve it
  • Routes to perform basic operations (add/delete/update/change-visibility)
  • New routes for plugins for chat
  • Upload logos to GCS bucket
  • Script to move plugins from json file to firebase
  • Make triggers work
  • Ensure backwards compatibility
  • Test chat, memory, external integration plugins

Note: Before deploying these changes, migrate the plugins again (after deleting the plugins_data collection and also the images in the bucket) to have fresh and new copy of data and images

add_plugin_demo.mp4
plugin_trigger_demo.mp4

Summary by CodeRabbit

Release Notes

  • New Features

    • Introduced a new page for submitting app details, including form validation and image upload capabilities.
    • Added functionality to fetch app categories and trigger events.
    • Implemented a confirmation dialog for app submissions based on user preferences.
    • Enhanced app listing to display privacy status directly in the UI.
    • Added a new snackbar method for displaying success messages.
    • Integrated a new plugin management system with enhanced functionality for adding and retrieving plugins.
  • Bug Fixes

    • Improved error handling for app enabling and integration processes.
    • Refined navigation logic for integration instructions.
    • Enhanced feedback for unavailable apps in the UI.
  • Chores

    • Updated dependencies in the project configuration for enhanced functionality.
    • Cleaned up unused imports and optimized code structure across various files.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Nov 1, 2024

Walkthrough

The changes in this pull request involve significant updates across multiple files, primarily enhancing the functionality related to app submission and plugin management. Key modifications include new API endpoints, improved error handling, and the introduction of new classes and methods for managing app data. A new form for app submission has been implemented, allowing immediate testing for submitters while maintaining a review process for broader access. Additionally, several utility functions for validation and data retrieval have been added, streamlining the overall architecture.

Changes

File Path Change Summary
app/lib/backend/http/api/apps.dart Updated API endpoints for retrieving apps, enabling app servers, and simplified markdown URL. Added functions for submitting apps, fetching categories, and trigger events.
app/lib/backend/preferences.dart Added getter and setter for app submission confirmation preference.
app/lib/backend/schema/app.dart Modified App class to include a new private field. Introduced Category and TriggerEvent classes with JSON handling methods.
app/lib/main.dart Integrated AddAppProvider into the provider architecture.
app/lib/pages/apps/add_app.dart Introduced AddAppPage for submitting app details with validation and confirmation dialog.
app/lib/pages/apps/app_detail.dart Enhanced navigation logic for integration instructions based on URL source.
app/lib/pages/apps/list_item.dart Modified app name display to indicate privacy status.
app/lib/pages/apps/page.dart Added interactive banner prompting users to feature their app, linking to AddAppPage.
app/lib/pages/apps/providers/add_app_provider.dart Introduced AddAppProvider for managing app submission state and logic.
app/lib/pages/apps/widgets/capabilities_chips_widget.dart Added widget for selectable capability chips.
app/lib/pages/apps/widgets/external_trigger_fields_widget.dart Created widget for external trigger fields with validation.
app/lib/pages/apps/widgets/prompt_text_field.dart Introduced reusable text input field with validation.
app/lib/pages/chat/page.dart Updated logic for determining message sender application.
app/lib/pages/memory_detail/widgets.dart Enhanced UI components and error handling for app responses.
app/lib/providers/app_provider.dart Improved loading state management for apps.
app/lib/providers/developer_mode_provider.dart Removed URL validation methods, relying on external validators.
app/lib/providers/message_provider.dart Added method for retrieving sender application based on app ID.
app/lib/utils/alerts/app_snackbar.dart Introduced method for displaying success snackbar.
app/lib/utils/other/validators.dart Added validation functions for URL, WebSocket, and email formats.
app/pubspec.yaml Added dependencies for dotted_border and image_picker.
backend/database/plugins.py Introduced new functions for plugin management and retrieval.
backend/routers/chat.py Updated message handling and plugin integration logic.
backend/routers/plugins.py Added new endpoints for managing plugins and their states.
backend/utils/memories/process_memory.py Enhanced plugin data retrieval and error handling in memory processing.
backend/utils/other/storage.py Added function for uploading plugin logos to storage.
backend/utils/plugins.py Introduced caching and database access for plugin data retrieval.
backend/.env.template Added declaration for BUCKET_PLUGINS_LOGOS environment variable.

Assessment against linked issues

Objective Addressed Explanation
Build a simple form for submitting apps that allows immediate testing. (1128)
Ensure submitted apps are available for submitters right away. (1128)
Implement a review process for submitted apps before wider availability. (1128)

Possibly related PRs

Poem

🐰 In the meadow where apps take flight,
A form appears, oh what a sight!
Submit your dreams, let them soar,
Test them quickly, then explore!
With each new plugin, joy we find,
In Omi's world, we're all aligned! 🌼✨


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@mdmohsin7 mdmohsin7 changed the title Add App/Plugin frontend Add App/Plugin frontend and backend Nov 2, 2024
@mdmohsin7 mdmohsin7 marked this pull request as ready for review November 4, 2024 16:26
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 43

🧹 Outside diff range and nitpick comments (42)
app/lib/utils/alerts/app_snackbar.dart (1)

Line range hint 1-29: Consider improving testability and null safety.

The class's dependency on MyApp.navigatorKey makes it challenging to test and could potentially throw runtime errors. Consider these improvements:

  1. Inject the BuildContext instead of using a global key:
class AppSnackbar {
  static void showSnackbar(
    BuildContext context,
    String message, {
    Color? color,
    Duration? duration,
  }) {
    if (message.isEmpty) return;
    
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(
        content: Text(message),
        backgroundColor: color,
        duration: duration ?? const Duration(seconds: 2),
      ),
    );
  }

  static void showSnackbarError(
    BuildContext context,
    String message, {
    Duration? duration,
  }) {
    showSnackbar(
      context,
      message,
      color: Colors.red,
      duration: duration,
    );
  }

  static void showSnackbarSuccess(
    BuildContext context,
    String message, {
    Duration? duration,
  }) {
    showSnackbar(
      context,
      message,
      color: Colors.green,
      duration: duration,
    );
  }
}
  1. Alternatively, consider using a service-based approach with dependency injection:
abstract class SnackbarService {
  void showSnackbar(String message, {Color? color, Duration? duration});
  void showError(String message, {Duration? duration});
  void showSuccess(String message, {Duration? duration});
}

class AppSnackbarService implements SnackbarService {
  final GlobalKey<NavigatorState> navigatorKey;
  
  AppSnackbarService(this.navigatorKey);
  
  @override
  void showSnackbar(String message, {Color? color, Duration? duration}) {
    final context = navigatorKey.currentState?.context;
    if (context == null || message.isEmpty) return;
    
    ScaffoldMessenger.of(context).showSnackBar(/*...*/);
  }
  
  // Implement other methods...
}
app/lib/utils/other/validators.dart (1)

1-20: Consider implementing a more robust validation framework

The current implementation could benefit from a more structured approach:

  1. Consider creating a Validator class to encapsulate these methods and provide detailed validation results
  2. Add specific error messages for different validation failures
  3. Implement unit tests covering edge cases

Example structure:

class ValidationResult {
  final bool isValid;
  final String? errorMessage;
  
  const ValidationResult(this.isValid, [this.errorMessage]);
}

class Validator {
  static ValidationResult validateUrl(String url) {
    // Implementation
  }
  
  static ValidationResult validateEmail(String email) {
    // Implementation
  }
  
  // ... other validation methods
}

Would you like me to help create comprehensive unit tests for these validators?

app/lib/pages/apps/widgets/prompt_text_field.dart (1)

3-7: Consider making the widget more generic and reusable.

The widget name PromptTextField and validation message suggest it's specifically for prompts, but it appears to be a generic text field component. Consider renaming it to something like CustomTextField and making the validation message configurable.

-class PromptTextField extends StatelessWidget {
+class CustomTextField extends StatelessWidget {
   final TextEditingController controller;
   final String label;
   final IconData icon;
-  const PromptTextField({super.key, required this.controller, required this.label, required this.icon});
+  final String? validationMessage;
+  const CustomTextField({
+    super.key,
+    required this.controller,
+    required this.label,
+    required this.icon,
+    this.validationMessage = 'Please enter a valid value',
+  });
app/lib/pages/apps/widgets/capabilities_chips_widget.dart (1)

12-15: Consider using Row instead of ListView.

Since you have a fixed number of chips (3), using a Row widget would be more appropriate and performant than a ListView. While ListView with shrinkWrap: true works, it adds unnecessary overhead for this use case.

-    return ListView(
-      shrinkWrap: true,
-      scrollDirection: Axis.horizontal,
-      children: [
+    return Row(
+      children: [
app/lib/backend/http/api/messages.dart (2)

62-64: Consider error message improvement.

The error handling is basic but functional. Consider enhancing the error message to include the plugin_id for better debugging.

Here's a suggested improvement:

-      throw Exception('Failed to send message');
+      throw Exception('Failed to get initial message for plugin: $appId');

Line range hint 9-14: Add pagination to message retrieval.

The TODO comment indicates missing pagination. This could lead to performance issues with large message histories.

Would you like me to help implement pagination for the message retrieval endpoint? This would include:

  1. Adding limit/offset parameters
  2. Implementing cursor-based pagination
  3. Adding proper response metadata
app/lib/providers/app_provider.dart (4)

Line range hint 89-127: Add bounds checking in toggleApp method

The toggleApp method assumes idx is valid but doesn't verify this assumption, which could lead to runtime errors.

Add validation at the start of the method:

 Future<void> toggleApp(String appId, bool isEnabled, int idx) async {
+  if (idx < 0 || idx >= appLoading.length) {
+    AppDialog.show(
+      title: 'Error',
+      content: 'Invalid app index',
+      singleButton: true,
+    );
+    return;
+  }
   if (appLoading[idx]) return;

50-50: Refactor duplicate initialization logic

The appLoading list initialization is duplicated in both getApps and initialize methods.

Consider extracting this into a private method:

+ void _initializeAppLoading() {
+   appLoading = List.filled(apps.length, false);
+ }

Then call this method instead of duplicating the initialization.

Also applies to: 82-82


Line range hint 1-14: Consider implementing proper disposal of resources

The provider manages state but doesn't implement disposal logic. This could lead to memory leaks, especially with the growing appLoading list.

Add disposal logic:

+ @override
+ void dispose() {
+   apps.clear();
+   appLoading.clear();
+   super.dispose();
+ }

Line range hint 89-127: Improve error handling in toggleApp

The error handling in toggleApp could be more informative and consistent.

Consider enhancing the error dialog:

- AppDialog.show(
-   title: 'Error activating the app',
-   content: 'If this is an integration app, make sure the setup is completed.',
-   singleButton: true,
- );
+ AppDialog.show(
+   title: 'Error Activating ${apps[idx].name}',
+   content: '''
+     Unable to activate the app. Please ensure:
+     - The app setup is complete
+     - You have necessary permissions
+     - The server is responding
+     
+     Try again later or contact support if the issue persists.
+   ''',
+   singleButton: true,
+ );
app/lib/providers/message_provider.dart (1)

133-135: Add documentation for the new method.

The implementation looks good and follows the existing patterns in the codebase. Consider adding documentation to explain the method's purpose and parameters.

+  /// Returns the App associated with the given appId.
+  /// If either appId is null or no matching app is found, returns null.
+  /// @param appId The unique identifier of the app to find
+  /// @return The matching App object or null if not found
   App? messageSenderApp(String? appId) {
     return appProvider?.apps.firstWhereOrNull((p) => p.id == appId);
   }
app/pubspec.yaml (1)

96-96: Consider organizing image_picker under an appropriate category.

The image_picker dependency should be grouped with other similar packages for better maintainability. Consider moving it under a relevant category section (e.g., "# Utils" or create a new "# Media" category).

  # Utils
  collection: 1.18.0
  equatable: 2.0.5
+ image_picker: ^1.1.2
  flutter_blue_plus: ^1.33.4
  http: ^1.2.1
-
-
-  image_picker: ^1.1.2
app/lib/backend/http/api/apps.dart (2)

167-184: Consider adding caching for app categories.

The categories are likely to change infrequently. Consider caching the results in SharedPreferences to:

  1. Improve performance
  2. Reduce server load
  3. Support offline functionality

Here's a suggested implementation:

 Future<List<Category>> getAppCategories() async {
+  // Try to get cached categories first
+  final cached = SharedPreferencesUtil().getAppCategories();
+  if (cached.isNotEmpty) {
+    return cached;
+  }
+
   var response = await makeApiCall(
     url: '${Env.apiBaseUrl}v1/plugin-categories',
     headers: {},
     body: '',
     method: 'GET',
   );
   try {
     if (response == null || response.statusCode != 200) return [];
     log('getAppCategories: ${response.body}');
     var res = jsonDecode(response.body);
-    return Category.fromJsonList(res);
+    final categories = Category.fromJsonList(res);
+    // Cache the results
+    SharedPreferencesUtil().setAppCategories(categories);
+    return categories;
   } catch (e, stackTrace) {
     debugPrint(e.toString());
     CrashReporting.reportHandledCrash(e, stackTrace);
     return [];
   }
 }

186-203: Improve naming consistency and consider caching.

  1. The Server suffix in the function name isn't consistent with other API functions in this file. Consider renaming to getTriggerEvents for consistency.
  2. Like categories, trigger events could benefit from caching.

Here's a suggested improvement:

-Future<List<TriggerEvent>> getTriggerEventsServer() async {
+Future<List<TriggerEvent>> getTriggerEvents() async {
+  // Try to get cached trigger events first
+  final cached = SharedPreferencesUtil().getTriggerEvents();
+  if (cached.isNotEmpty) {
+    return cached;
+  }
+
   var response = await makeApiCall(
     url: '${Env.apiBaseUrl}v1/plugin-triggers',
     headers: {},
     body: '',
     method: 'GET',
   );
   try {
     if (response == null || response.statusCode != 200) return [];
     log('getTriggerEvents: ${response.body}');
     var res = jsonDecode(response.body);
-    return TriggerEvent.fromJsonList(res);
+    final events = TriggerEvent.fromJsonList(res);
+    // Cache the results
+    SharedPreferencesUtil().setTriggerEvents(events);
+    return events;
   } catch (e, stackTrace) {
     debugPrint(e.toString());
     CrashReporting.reportHandledCrash(e, stackTrace);
     return [];
   }
 }
app/lib/pages/apps/list_item.dart (2)

61-61: Consider improving privacy status visibility.

While adding "(private)" to the app name works, it might not be the most user-friendly way to display privacy status. Consider using a dedicated privacy indicator icon or badge for better visual hierarchy.

-app.name + (app.private ? ' (private)' : ''),
+Row(
+  children: [
+    Flexible(child: Text(app.name, maxLines: 1)),
+    if (app.private)
+      Padding(
+        padding: const EdgeInsets.only(left: 4),
+        child: Icon(Icons.lock, size: 16, color: Colors.grey),
+      ),
+  ],
+),

Line range hint 134-157: Enhance external app authorization security.

The authorization dialog for external apps should provide more detailed information about:

  1. What specific data will be shared
  2. How the data will be used
  3. The external server's identity

Consider enhancing the dialog message to be more specific:

-'Do you allow this app to access your memories, transcripts, and recordings? Your data will be sent to the app\'s server for processing.',
+'Do you authorize "${app.name}" to access your:\n\n' +
+'• Chat memories\n' +
+'• Voice transcripts\n' +
+'• Audio recordings\n\n' +
+'This data will be processed on ${app.serverDomain}. The app\'s privacy policy and terms of service apply to shared data.',
app/lib/backend/schema/app.dart (2)

212-218: Enhance image URL handling robustness

While the conditional logic for handling both full URLs and relative paths is good, there are a few improvements to consider:

Consider these enhancements:

 String getImageUrl() {
+  if (image.isEmpty) {
+    return ''; // or a default image URL
+  }
   if (image.startsWith('http')) {
     return image;
   } else {
-    return 'https://raw.githubusercontent.com/BasedHardware/Omi/main$image';
+    final baseUrl = const String.fromEnvironment('IMAGE_BASE_URL', 
+      defaultValue: 'https://raw.githubusercontent.com/BasedHardware/Omi/main');
+    return '$baseUrl$image';
   }
 }

244-269: Add documentation and field validation to Category class

The Category class implementation is clean but could benefit from additional documentation and validation.

Consider these improvements:

+/// Represents a category for grouping apps
+/// 
+/// Used by the app store to organize apps into different sections
 class Category {
+  /// The display title of the category
   String title;
+  /// Unique identifier for the category
   String id;
   
   Category({
     required this.title,
     required this.id,
   });

   factory Category.fromJson(Map<String, dynamic> json) {
+    if (!json.containsKey('title') || !json.containsKey('id')) {
+      throw FormatException('Missing required fields in Category JSON');
+    }
     return Category(
       title: json['title'],
       id: json['id'],
     );
   }
app/lib/pages/apps/widgets/external_trigger_fields_widget.dart (2)

9-14: Consider improving type safety and maintainability.

The capability check uses a string literal which could lead to typos or maintenance issues.

Consider defining capabilities as constants or an enum:

// In a separate constants file
class AppCapabilities {
  static const String externalIntegration = 'external_integration';
  // ... other capabilities
}

// Then use it in the widget
if (!provider.isCapabilitySelected(AppCapabilities.externalIntegration))

111-151: Maintain consistent error messages and validation.

The error message differs from other URL fields and could be more specific to the setup completed URL context.

Update the error message to be more specific:

 validator: (value) {
   if (value == null || !isValidUrl(value)) {
-    return 'Please enter a valid URL';
+    return 'Please enter a valid setup completed URL';
   }
   return null;
 },
backend/utils/plugins.py (1)

38-70: Consider implementing a Plugin Service layer.

The current implementation mixes concerns across data retrieval, caching, and business logic. Consider:

  1. Creating a dedicated Plugin Service to encapsulate plugin-related operations
  2. Implementing proper error boundaries and monitoring
  3. Adding metrics for cache hit/miss ratios and plugin usage patterns
  4. Using dependency injection for better testability

This would improve maintainability and make the system more robust.

app/lib/providers/developer_mode_provider.dart (4)

Line range hint 196-203: Remove commented-out WebSocket code.

The commented WebSocket-related code should be removed if it's no longer needed. If it's for future implementation, consider tracking it with a TODO issue instead.


Line range hint 204-218: Consider implementing structured error handling and validation.

The current implementation has several areas for improvement:

  1. Sequential validations could be grouped for better maintainability
  2. No rollback mechanism if some webhook updates fail
  3. Error handling could be more structured

Consider refactoring to:

  1. Group validations into a dedicated method
  2. Implement a transaction-like pattern for webhook updates
  3. Use structured error types

Example approach:

Future<ValidationResult> validateSettings() async {
  final errors = <String>[];
  
  // Group all validations
  if (gcpCredentialsController.text.isNotEmpty && 
      gcpBucketNameController.text.isNotEmpty) {
    try {
      await authenticateGCP(base64: gcpCredentialsController.text.trim());
    } catch (e) {
      errors.add('Invalid GCP credentials or bucket name');
    }
  }
  
  // Group webhook validations
  final webhooks = {
    'audio_bytes': webhookAudioBytes.text,
    'realtime_transcript': webhookOnTranscriptReceived.text,
    'memory_created': webhookOnMemoryCreated.text,
    'day_summary': webhookDaySummary.text,
  };
  
  for (final entry in webhooks.entries) {
    if (entry.value.isNotEmpty && !isValidUrl(entry.value)) {
      errors.add('Invalid ${entry.key} webhook URL');
    }
  }
  
  return ValidationResult(errors.isEmpty, errors);
}

Line range hint 219-237: Improve webhook update error handling.

The current implementation using Future.wait could fail silently if some webhook updates fail. Consider implementing proper error handling and rollback mechanism.

-    try {
-      Future.wait([w1, w2, w3, w4]);
+    try {
+      final results = await Future.wait([w1, w2, w3, w4], eagerError: true);
       prefs.webhookAudioBytes = webhookAudioBytes.text;
       prefs.webhookAudioBytesDelay = webhookAudioBytesDelay.text;
       prefs.webhookOnTranscriptReceived = webhookOnTranscriptReceived.text;
       prefs.webhookOnMemoryCreated = webhookOnMemoryCreated.text;
       prefs.webhookDaySummary = webhookDaySummary.text;
-    } catch (e) {
+    } catch (e, stackTrace) {
       Logger.error('Error occurred while updating endpoints: $e');
+      Logger.error('Stack trace: $stackTrace');
+      AppSnackbar.showSnackbarError('Failed to update some webhook settings');
+      // Rollback to previous values
+      await initialize();
+      return;
     }

Based on the search results, I can now generate the final response as we have sufficient information about the audio bytes webhook delay implementation:

Add validation for audio bytes webhook delay is necessary

The code shows that:

  • The delay value is used in backend calculations (sample_rate * audio_bytes_webhook_delay_seconds * 2)
  • The input field uses TextInputType.number but lacks client-side validation
  • Invalid values could cause unexpected behavior in audio processing

The suggested validation in the review comment is appropriate as it would:

  • Prevent null or negative values that could break the audio buffer calculations
  • Provide clear feedback to users through the snackbar
  • Maintain consistency with other webhook validations in the file
🔗 Analysis chain

Line range hint 180-184: Add validation for audio bytes webhook delay.

The delay value is set to '5' by default but lacks validation for valid numeric input.

Consider adding validation:

   if (webhookAudioBytes.text.isNotEmpty && webhookAudioBytesDelay.text.isEmpty) {
     webhookAudioBytesDelay.text = '5';
   }
+  if (webhookAudioBytesDelay.text.isNotEmpty) {
+    final delay = int.tryParse(webhookAudioBytesDelay.text);
+    if (delay == null || delay < 0) {
+      AppSnackbar.showSnackbarError('Invalid delay value. Please enter a positive number.');
+      setIsLoading(false);
+      return;
+    }
+  }
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check if there's any validation for the delay value in the validators utility
rg -A 5 "webhookAudioBytesDelay|audio.*delay" 

Length of output: 8412

app/lib/pages/apps/page.dart (1)

3-3: Review usage of temp.dart utility.

The import from temp.dart suggests temporary/transitional code. Consider moving the routeToPage utility to a permanent location in the codebase, such as a dedicated navigation utilities file.

Also applies to: 5-5, 8-8

backend/utils/memories/process_memory.py (1)

Line range hint 105-119: Add thread safety, timeouts, and error handling for plugin execution.

While the plugin execution logic is functional, consider these improvements for robustness:

  1. Use thread-safe operations for appending results
  2. Add timeout mechanism for plugin threads
  3. Implement error handling for plugin execution

Consider this implementation:

 def _trigger_plugins(uid: str, memory: Memory, is_reprocess: bool = False):
     plugins: List[Plugin] = get_plugins_data_from_db(uid)
     filtered_plugins = [plugin for plugin in plugins if plugin.works_with_memories() and plugin.enabled]
-    memory.plugins_results = []
+    from threading import Lock
+    from queue import Queue
+    results_queue = Queue()
+    results_lock = Lock()
     threads = []
 
     def execute_plugin(plugin):
-        if result := get_plugin_result(memory.get_transcript(False), plugin).strip():
-            memory.plugins_results.append(PluginResult(plugin_id=plugin.id, content=result))
-            if not is_reprocess:
-                record_plugin_usage(uid, plugin.id, UsageHistoryType.memory_created_prompt, memory_id=memory.id)
+        try:
+            if result := get_plugin_result(memory.get_transcript(False), plugin).strip():
+                results_queue.put(PluginResult(plugin_id=plugin.id, content=result))
+                if not is_reprocess:
+                    record_plugin_usage(uid, plugin.id, UsageHistoryType.memory_created_prompt, memory_id=memory.id)
+        except Exception as e:
+            print(f"Error executing plugin {plugin.id}: {str(e)}")
 
     for plugin in filtered_plugins:
-        threads.append(threading.Thread(target=execute_plugin, args=(plugin,)))
+        thread = threading.Thread(target=execute_plugin, args=(plugin,))
+        thread.daemon = True  # Allow main program to exit even if threads are running
+        threads.append(thread)
 
     [t.start() for t in threads]
-    [t.join() for t in threads]
+    # Wait for threads with timeout
+    timeout = 30  # seconds
+    [t.join(timeout) for t in threads]
+    
+    # Collect results
+    memory.plugins_results = []
+    while not results_queue.empty():
+        memory.plugins_results.append(results_queue.get())
🧰 Tools
🪛 Ruff

104-104: Memory may be undefined, or defined from star imports

(F405)


105-105: List may be undefined, or defined from star imports

(F405)

app/lib/backend/preferences.dart (1)

132-135: Consider documenting the preference's purpose

While the implementation is correct, adding documentation comments would help other developers understand the purpose and usage of this preference setting.

Consider adding documentation:

+  /// Whether to show a confirmation dialog when submitting an app.
+  /// Defaults to true if not set.
   bool get showSubmitAppConfirmation => getBool('showSubmitAppConfirmation') ?? true;

+  /// Sets whether to show a confirmation dialog when submitting an app.
   set showSubmitAppConfirmation(bool value) => saveBool('showSubmitAppConfirmation', value);
app/lib/pages/chat/page.dart (2)

Line range hint 359-359: Track streaming capabilities restoration.

This TODO appears important for the chat experience, especially with the new app/plugin functionality being added.

Would you like me to:

  1. Create a GitHub issue to track the restoration of streaming capabilities?
  2. Help implement the streaming functionality with an initial empty message approach?

Line range hint 1-424: Consider further architectural improvements.

To enhance maintainability, consider:

  1. Extracting message sending logic to a dedicated service
  2. Creating constants for magic numbers (e.g., padding values)
  3. Using a ScrollController provider to manage scroll state
app/lib/pages/apps/app_detail.dart (2)

282-292: Improve URL handling robustness.

While the conditional handling of different URL types is good, consider these improvements:

  1. Use a more robust method to determine if it's a GitHub raw URL, such as parsing the URL and checking the host.
  2. Add error handling for URL launches.

Consider this implementation:

 if (widget.app.externalIntegration != null) {
-  if (widget.app.externalIntegration!.setupInstructionsFilePath
-      .contains('raw.githubusercontent.com')) {
+  final uri = Uri.tryParse(widget.app.externalIntegration!.setupInstructionsFilePath);
+  if (uri?.host == 'raw.githubusercontent.com') {
     await routeToPage(
       context,
       AppSetupInstructions(markdown: instructionsMarkdown ?? ''),
     );
   } else {
-    await launchUrl(Uri.parse(widget.app.externalIntegration!.setupInstructionsFilePath));
+    try {
+      final success = await launchUrl(
+        Uri.parse(widget.app.externalIntegration!.setupInstructionsFilePath)
+      );
+      if (!success && mounted) {
+        ScaffoldMessenger.of(context).showSnackBar(
+          const SnackBar(content: Text('Could not open URL')),
+        );
+      }
+    } catch (e) {
+      if (mounted) {
+        ScaffoldMessenger.of(context).showSnackBar(
+          const SnackBar(content: Text('Invalid URL')),
+        );
+      }
+    }
   }
 }

Line range hint 456-498: Enhance error handling in app toggling.

The error handling for app activation is good, but consider these improvements:

  1. Add specific error messages based on failure reasons
  2. Add error handling for app disabling
  3. Consider adding retry logic for failed network operations

Consider this implementation:

 Future<void> _toggleApp(String appId, bool isEnabled) async {
   var prefs = SharedPreferencesUtil();
   setState(() => appLoading = true);
   if (isEnabled) {
     var enabled = await enableAppServer(appId);
     if (!enabled) {
       if (mounted) {
         showDialog(
           context: context,
           builder: (c) => getDialog(
             context,
             () => Navigator.pop(context),
             () => Navigator.pop(context),
-            'Error activating the app',
-            'If this is an integration app, make sure the setup is completed.',
+            'Failed to activate the app',
+            widget.app.worksExternally()
+                ? 'Please complete the setup steps and try again.'
+                : 'Please check your internet connection and try again.',
             singleButton: true,
           ),
         );
       }

       setState(() => appLoading = false);
       return;
     }

     prefs.enableApp(appId);
     MixpanelManager().appEnabled(appId);
   } else {
     prefs.disableApp(appId);
-    await enableAppServer(appId);
+    try {
+      final success = await enableAppServer(appId);
+      if (!success && mounted) {
+        showDialog(
+          context: context,
+          builder: (c) => getDialog(
+            context,
+            () => Navigator.pop(context),
+            () {
+              Navigator.pop(context);
+              // Revert local state since server update failed
+              prefs.enableApp(appId);
+              setState(() => widget.app.enabled = true);
+            },
+            'Failed to disable the app',
+            'The app will remain enabled. Please try again.',
+            singleButton: true,
+          ),
+        );
+        return;
+      }
+    } catch (e) {
+      if (mounted) {
+        showDialog(
+          context: context,
+          builder: (c) => getDialog(
+            context,
+            () => Navigator.pop(context),
+            () {
+              Navigator.pop(context);
+              // Revert local state since server update failed
+              prefs.enableApp(appId);
+              setState(() => widget.app.enabled = true);
+            },
+            'Error',
+            'An unexpected error occurred. Please try again.',
+            singleButton: true,
+          ),
+        );
+        return;
+      }
+    }
     MixpanelManager().appDisabled(appId);
   }
   setState(() => widget.app.enabled = isEnabled);
   setState(() => appLoading = false);
 }
app/lib/pages/memory_detail/widgets.dart (1)

530-582: Refactor the unknown app ListTile implementation

While the implementation provides good user feedback, there are several improvements that could be made:

  1. Extract hardcoded asset paths to constants
  2. Extract duplicate copy functionality into a shared method
  3. Define container dimensions as constants
+ // At the top of the file
+ const kBackgroundImagePath = "assets/images/background.png";
+ const kHeroLogoPath = "assets/images/herologo.png";
+ const kUnknownAppContainerSize = 30.0;
+ const kUnknownAppLogoSize = 24.0;
+
+ void _copyAppResponse(BuildContext context, String content, ServerMemory memory) {
+   Clipboard.setData(ClipboardData(text: content.trim()));
+   ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
+     content: Text('App response copied to clipboard'),
+   ));
+   MixpanelManager().copiedMemoryDetails(memory, source: 'App Response');
+ }

  // In the widget
  ListTile(
    contentPadding: EdgeInsets.zero,
    leading: Container(
      decoration: const BoxDecoration(
        image: DecorationImage(
-         image: AssetImage("assets/images/background.png"),
+         image: AssetImage(kBackgroundImagePath),
          fit: BoxFit.cover,
        ),
        borderRadius: BorderRadius.all(Radius.circular(16.0)),
      ),
-     height: 30,
-     width: 30,
+     height: kUnknownAppContainerSize,
+     width: kUnknownAppContainerSize,
      child: Stack(
        alignment: Alignment.center,
        children: [
          Image.asset(
-           "assets/images/herologo.png",
-           height: 24,
-           width: 24,
+           kHeroLogoPath,
+           height: kUnknownAppLogoSize,
+           width: kUnknownAppLogoSize,
          ),
        ],
      ),
    ),
    // ... other properties ...
    trailing: IconButton(
      icon: const Icon(Icons.copy_rounded, color: Colors.white, size: 20),
      onPressed: () {
-       Clipboard.setData(ClipboardData(text: appResponse.content.trim()));
-       ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
-         content: Text('App response copied to clipboard'),
-       ));
-       MixpanelManager()
-           .copiedMemoryDetails(provider.memory, source: 'App Response');
+       _copyAppResponse(context, appResponse.content, provider.memory);
      },
    ),
  ),
backend/routers/chat.py (1)

8-8: Import statement order and consistency.

Ensure that imports are grouped and ordered consistently for better readability. Standard library imports should be separated from third-party and local imports.

Consider rearranging the imports like this:

 import uuid
 from datetime import datetime, timezone
 from typing import List, Optional

+from fastapi import APIRouter, Depends, HTTPException
+
 import database.chat as chat_db
 from database.plugins import record_plugin_usage, get_plugin_by_id_db
 from models.chat import Message, SendMessageRequest, MessageSender
 from models.plugin import UsageHistoryType, Plugin
 from models.memory import Memory
+from utils.llm import initial_chat_message
+from utils.other import endpoints as auth
+from utils.plugins import get_plugin_by_id
+from utils.retrieval.graph import execute_graph_chat
app/lib/pages/apps/providers/add_app_provider.dart (4)

47-52: Perform asynchronous initialization tasks in parallel to improve efficiency.

In the init method, getCategories() and getTriggerEvents() are called sequentially using await, causing the second call to wait for the first to complete. Since these two methods are independent, running them concurrently can reduce initialization time and improve the user experience.

Apply this diff to run asynchronous tasks in parallel:

 def init() async {
-   await getCategories();
-   await getTriggerEvents();
+   await Future.wait([
+     getCategories(),
+     getTriggerEvents(),
+   ]);
    creatorNameController.text = SharedPreferencesUtil().givenName;
    creatorEmailController.text = SharedPreferencesUtil().email;
  }

87-144: Refactor the validateForm method to improve readability and maintainability.

The validateForm method contains multiple repeated code blocks for checking if fields are empty and displaying error messages. Extracting common validation logic into helper methods can reduce code duplication and enhance readability.

Create a helper method for validating text fields:

bool validateTextField(TextEditingController controller, String errorMessage) {
  if (controller.text.isEmpty) {
    AppSnackbar.showSnackbarError(errorMessage);
    return false;
  }
  return true;
}

Refactor the validateForm method to use the helper:

bool validateForm() {
  if (formKey.currentState!.validate()) {
    if (!termsAgreed) {
      AppSnackbar.showSnackbarError('Please agree to the terms and conditions to proceed');
      return false;
    }
    if (!capabilitySelected()) {
      AppSnackbar.showSnackbarError('Please select at least one capability for your app');
      return false;
    }
    if (imageFile == null) {
      AppSnackbar.showSnackbarError('Please select a logo for your app');
      return false;
    }
    if (isCapabilitySelected('external_integration')) {
      if (triggerEvent == null) {
        AppSnackbar.showSnackbarError('Please select a trigger event for your app');
        return false;
      }
      if (!validateTextField(webhookUrlController, 'Please enter a webhook URL for your app')) {
        return false;
      }
      if (!validateTextField(setupCompletedController, 'Please enter a setup completed URL for your app')) {
        return false;
      }
      if (!validateTextField(instructionsController, 'Please enter a setup instructions URL for your app')) {
        return false;
      }
    }
    if (isCapabilitySelected('chat')) {
      if (!validateTextField(chatPromptController, 'Please enter a chat prompt for your app')) {
        return false;
      }
    }
    if (isCapabilitySelected('memories')) {
      if (!validateTextField(memoryPromptController, 'Please enter a memory prompt for your app')) {
        return false;
      }
    }
    if (appCategory == null) {
      AppSnackbar.showSnackbarError('Please select a category for your app');
      return false;
    }
    if (privacyLevel == null) {
      AppSnackbar.showSnackbarError('Please select a privacy level for your app');
      return false;
    }
    return true;
  } else {
    AppSnackbar.showSnackbarError('Please fill in all the required fields correctly');
    return false;
  }
}

188-202: Enhance exception handling in the pickImage method.

Currently, the pickImage method only catches PlatformException for photo_access_denied. Expanding the exception handling to catch all exceptions will ensure that unexpected errors are handled gracefully, improving app stability.

Apply this diff to enhance exception handling:

 def pickImage() async {
    ImagePicker imagePicker = ImagePicker();
    try {
      var file = await imagePicker.pickImage(source: ImageSource.gallery);
      if (file != null) {
        imageFile = File(file.path);
      }
-     notifyListeners();
-   } on PlatformException catch (e) {
-     if (e.code == 'photo_access_denied') {
+     notifyListeners();
+   } catch (e) {
+     if (e is PlatformException && e.code == 'photo_access_denied') {
        AppSnackbar.showSnackbarError('Photos permission denied. Please allow access to photos to select an image');
      } else {
        AppSnackbar.showSnackbarError('An unexpected error occurred while selecting an image');
      }
    }
    notifyListeners();
  }

190-202: Optimize notifyListeners() calls in the pickImage method.

The notifyListeners() method is called twice in pickImage, once inside the try block and once after the catch block. Since it needs to be called regardless of the outcome, you can move it outside the try-catch block to avoid redundancy.

Apply this diff to streamline the code:

 def pickImage() async {
    ImagePicker imagePicker = ImagePicker();
    try {
      var file = await imagePicker.pickImage(source: ImageSource.gallery);
      if (file != null) {
        imageFile = File(file.path);
      }
-     notifyListeners();
    } on PlatformException catch (e) {
      if (e.code == 'photo_access_denied') {
        AppSnackbar.showSnackbarError('Photos permission denied. Please allow access to photos to select an image');
      }
    }
-   notifyListeners();
+   notifyListeners();
  }
backend/routers/plugins.py (2)

173-173: Remove unnecessary f-string prefix

The string at line 173 does not contain any placeholders, so the f prefix is unnecessary. Removing it improves readability.

Apply this diff:

-os.makedirs(f'_temp/plugins', exist_ok=True)
+os.makedirs('_temp/plugins', exist_ok=True)
🧰 Tools
🪛 Ruff

173-173: f-string without any placeholders

Remove extraneous f prefix

(F541)


166-167: Sanitize plugin IDs to avoid invalid characters

When generating data['id'] from data['name'], special characters in the name might result in invalid IDs. Sanitize the name to include only allowed characters.

Consider using:

import re
data['id'] = re.sub(r'[^a-z0-9-]', '', data['name'].replace(' ', '-').lower())
app/lib/pages/apps/add_app.dart (2)

109-111: Handle errors during image selection

When the user taps to upload an app icon, there is no error handling for potential issues during image selection. It's important to handle exceptions to provide a smooth user experience.

Apply this diff to handle exceptions:

  onTap: () async {
-   await provider.pickImage();
+   try {
+     await provider.pickImage();
+   } catch (e) {
+     ScaffoldMessenger.of(context).showSnackBar(
+       SnackBar(content: Text('Image selection failed: \$e')),
+     );
+   }
  },

26-28: Handle exceptions during initialization

In the initState() method, the asynchronous call to provider.init() might fail. It's advisable to handle potential exceptions to prevent the app from crashing.

Apply this diff to catch exceptions:

WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
- await Provider.of<AddAppProvider>(context, listen: false).init();
+ try {
+   await Provider.of<AddAppProvider>(context, listen: false).init();
+ } catch (e) {
+   ScaffoldMessenger.of(context).showSnackBar(
+     SnackBar(content: Text('Initialization failed: \$e')),
+   );
+ }
});
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 01e4eca and 78821c7.

📒 Files selected for processing (27)
  • app/lib/backend/http/api/apps.dart (5 hunks)
  • app/lib/backend/http/api/messages.dart (2 hunks)
  • app/lib/backend/preferences.dart (1 hunks)
  • app/lib/backend/schema/app.dart (4 hunks)
  • app/lib/main.dart (2 hunks)
  • app/lib/pages/apps/add_app.dart (1 hunks)
  • app/lib/pages/apps/app_detail.dart (1 hunks)
  • app/lib/pages/apps/list_item.dart (1 hunks)
  • app/lib/pages/apps/page.dart (2 hunks)
  • app/lib/pages/apps/providers/add_app_provider.dart (1 hunks)
  • app/lib/pages/apps/widgets/capabilities_chips_widget.dart (1 hunks)
  • app/lib/pages/apps/widgets/external_trigger_fields_widget.dart (1 hunks)
  • app/lib/pages/apps/widgets/prompt_text_field.dart (1 hunks)
  • app/lib/pages/chat/page.dart (1 hunks)
  • app/lib/pages/memory_detail/widgets.dart (2 hunks)
  • app/lib/providers/app_provider.dart (1 hunks)
  • app/lib/providers/developer_mode_provider.dart (1 hunks)
  • app/lib/providers/message_provider.dart (1 hunks)
  • app/lib/utils/alerts/app_snackbar.dart (1 hunks)
  • app/lib/utils/other/validators.dart (1 hunks)
  • app/pubspec.yaml (2 hunks)
  • backend/database/plugins.py (2 hunks)
  • backend/routers/chat.py (2 hunks)
  • backend/routers/plugins.py (2 hunks)
  • backend/utils/memories/process_memory.py (2 hunks)
  • backend/utils/other/storage.py (1 hunks)
  • backend/utils/plugins.py (4 hunks)
🧰 Additional context used
🪛 Ruff
backend/routers/chat.py

111-111: Redefinition of unused send_message from line 99

(F811)


116-116: Redefinition of unused send_message from line 111

(F811)

backend/routers/plugins.py

122-122: Redefinition of unused enable_plugin_endpoint from line 25

(F811)


138-138: Redefinition of unused disable_plugin_endpoint from line 40

(F811)


148-148: Redefinition of unused get_plugins from line 60

(F811)


163-163: Do not perform function call File in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


163-163: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


173-173: f-string without any placeholders

Remove extraneous f prefix

(F541)

backend/utils/memories/process_memory.py

28-28: utils.plugins.get_plugins_data imported but unused

Remove unused import: utils.plugins.get_plugins_data

(F401)


105-105: List may be undefined, or defined from star imports

(F405)

🔇 Additional comments (27)
app/lib/utils/alerts/app_snackbar.dart (1)

23-29: LGTM! The implementation follows consistent patterns.

The new showSnackbarSuccess method maintains consistency with existing methods and provides a clear way to show success notifications.

app/lib/pages/apps/widgets/prompt_text_field.dart (1)

1-54: ⚠️ Potential issue

Add input sanitization for security.

Since this widget is used in app/plugin submission forms, consider adding input sanitization to prevent potential security issues like XSS when the input is used in HTML contexts or SQL injection if stored in a database.

Let's check if there are any existing input sanitization utilities in the codebase:

app/lib/pages/apps/widgets/capabilities_chips_widget.dart (3)

1-5: LGTM! Clean and well-organized imports.

The imports are minimal and follow Flutter conventions.


6-8: LGTM! Well-structured class declaration.

The class follows Flutter best practices with proper constructor implementation.


11-60: Verify error handling and validation requirements.

Please consider the following aspects:

  1. How should the widget handle cases when AddAppProvider is not available?
  2. Are there any requirements for minimum/maximum number of capabilities that can be selected?
  3. Should the widget show any feedback when selection limits are reached?

Let's check if there are any existing validation rules:

app/lib/backend/http/api/messages.dart (2)

40-42: Verify plugin_id parameter handling.

The endpoint now includes a plugin_id query parameter. Ensure that the backend properly validates this parameter.

#!/bin/bash
# Description: Check plugin_id validation in the backend
# Expected: Find validation logic for plugin_id parameter

# Search for plugin_id validation in Python route handlers
echo "Checking plugin_id validation:"
rg -B 2 -A 5 "plugin_id.*=.*request\.(args|query_params|params)\.get\(['\"]plugin_id['\"]" --type py

Line range hint 1-38: Inconsistent API versioning across endpoints.

Some endpoints are still using v1 while others have been upgraded to v2. This inconsistency could lead to maintenance issues.

Let's verify the API versioning across the codebase:

app/lib/providers/app_provider.dart (1)

50-50: Verify usage patterns of AppProvider

Let's verify how this provider is used across the codebase to ensure our changes won't cause issues.

✅ Verification successful

Based on the shell script results, I can now provide a final response:

AppProvider's appLoading array initialization is safe

The initialization of appLoading array is properly synchronized with the apps list:

  • The array is initialized in getApps() method right after apps are retrieved
  • The toggleApp() method has proper index bounds checking and loading state management
  • No concurrent modifications of the array were found in the codebase
  • The array is only accessed within the AppProvider class with proper state management
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for AppProvider usage patterns
echo "Searching for AppProvider instantiation and usage..."
rg "AppProvider" -A 5

echo "\nChecking for concurrent operations on apps..."
rg "toggleApp|getApps" -A 5

echo "\nLooking for potential race conditions..."
ast-grep --pattern 'Future<void> $method($_) {
  $$$
  appLoading[$_] = $_;
  $$$
}'

Length of output: 18708

app/lib/providers/message_provider.dart (1)

133-135: Verify the usage of messageSenderApp method.

Let's ensure this new method is being used consistently across the codebase where app lookups are needed.

✅ Verification successful

Based on the search results, I can generate the final response as we have clear evidence about both the implementation and usage of messageSenderApp:

The messageSenderApp method is being used appropriately and consistently

The codebase scan shows that:

  • The method is only implemented once in message_provider.dart
  • It's being used in chat/page.dart for app lookup
  • No other direct app lookups using similar pattern (firstWhereOrNull with app ID) exist in the codebase
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Find all potential places where app lookups are performed to ensure consistent usage

# Search for direct app lookups that could use messageSenderApp instead
echo "Searching for potential places to use messageSenderApp..."
rg -l "firstWhereOrNull.*p\.id.*appId" --type dart

# Search for current usage of messageSenderApp
echo "Checking current usage of messageSenderApp..."
rg "messageSenderApp" --type dart

Length of output: 559

app/pubspec.yaml (1)

33-33: LGTM: dotted_border dependency addition.

The addition of dotted_border: ^2.1.0 is appropriately placed under the UI section and aligns with the PR's app management UI enhancements.

app/lib/pages/apps/list_item.dart (1)

Line range hint 1-157: Verify app schema compatibility.

The code assumes the App model has a private field. Let's verify this assumption and ensure backward compatibility.

✅ Verification successful

The private field is properly defined and used in the App model

The verification confirms that:

  • The private field is properly defined as a boolean property in the App class
  • It's initialized with a default value of false
  • The field is properly handled in JSON deserialization with a fallback logic: private: json['private'] ?? json['id'].toString().contains('private')

The code's assumption about the private field is correct and there are no compatibility issues.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify App model schema and usage
# Expected: Find the private field definition and any migration code

# Check App model definition
echo "Checking App model definition..."
rg -A 5 "class App" app/lib/backend/schema/

# Check for migration code
echo "Checking migration handling..."
rg -l "private.*migration" app/lib/backend/

Length of output: 1407


Script:

#!/bin/bash
# Let's check the complete App class definition and its properties
echo "Checking complete App class definition..."
rg -A 30 "^class App" app/lib/backend/schema/app.dart

# Also search for any private field usage in the App class
echo "Checking private field usage..."
rg "private" app/lib/backend/schema/app.dart

Length of output: 2115

app/lib/backend/schema/app.dart (1)

142-142: 🛠️ Refactor suggestion

Review the fallback logic for private field initialization

The fallback logic json['id'].toString().contains('private') for determining privacy status based on the ID string seems fragile. This could lead to false positives if an app's ID happens to contain the word "private" for other reasons.

Let's check if there are any existing apps that might be affected:

Consider one of these alternatives:

  1. Remove the fallback and default to false
  2. Add a migration script to explicitly set the private field
  3. Document why this fallback exists if it's intentionally handling legacy data

Also applies to: 176-176, 208-208

app/lib/pages/apps/widgets/external_trigger_fields_widget.dart (1)

1-8: LGTM! Clean imports and proper widget declaration.

The imports are well-organized and the widget is correctly declared as a StatelessWidget since it relies on Provider for state management.

backend/utils/other/storage.py (1)

239-244: Verify plugin logo cleanup process.

Based on the PR objectives mentioning "deleting the plugins_data collection and the associated images in the bucket", we should ensure there's a cleanup function for plugin logos.

Let's check if there's a cleanup function elsewhere in the codebase:

Would you like me to generate a complementary function to handle plugin logo deletion?

backend/utils/plugins.py (2)

8-8: LGTM: Import changes align with new database-driven approach.

The addition of database-specific imports supports the transition from external URL-based plugin data retrieval to database-driven retrieval.


147-148: LGTM: Usage tracking implementation.

The plugin usage tracking is correctly implemented with proper parameters.

app/lib/providers/developer_mode_provider.dart (1)

9-9: LGTM! Good architectural improvement.

Moving URL validation to a dedicated validators utility improves code organization and ensures consistent validation across the application.

backend/utils/memories/process_memory.py (2)

Line range hint 166-182: Verify plugin data consistency after migration.

The PR objectives mention that plugins need to be migrated before deployment. Consider adding validation checks to ensure plugin data consistency.

Let's verify plugin data structure:

#!/bin/bash
# Check plugin data structure in the codebase
echo "Analyzing plugin data structure..."
rg -A 5 "class Plugin" --type py

# Look for plugin validation code
echo "Checking for plugin validation..."
rg -l "validate.*plugin" --type py

Consider implementing these safeguards:

  1. Add a version field to plugin data to track migrations
  2. Implement validation checks for plugin data structure
  3. Add a mechanism to verify plugin data consistency after migration
🧰 Tools
🪛 Ruff

28-28: utils.plugins.get_plugins_data imported but unused

Remove unused import: utils.plugins.get_plugins_data

(F401)


Line range hint 166-182: Add error handling for threaded operations and consider plugin migration.

  1. The threaded operations for vector storage and fact extraction lack error handling
  2. The PR objectives mention the need to migrate plugins before deployment, but there's no mechanism to handle failed plugin data migration

Let's verify the plugin migration requirements:

Consider adding error handling for threaded operations:

     if not discarded:
         _trigger_plugins(uid, memory, is_reprocess=is_reprocess)
-        threading.Thread(target=save_structured_vector, args=(uid, memory,)).start() if not is_reprocess else None
-        threading.Thread(target=_extract_facts, args=(uid, memory)).start()
+        def run_with_error_handling(func, *args):
+            try:
+                func(*args)
+            except Exception as e:
+                print(f"Error in {func.__name__}: {str(e)}")
+                # Consider adding error reporting mechanism
+
+        if not is_reprocess:
+            thread = threading.Thread(target=run_with_error_handling, args=(save_structured_vector, uid, memory))
+            thread.daemon = True
+            thread.start()
+
+        facts_thread = threading.Thread(target=run_with_error_handling, args=(_extract_facts, uid, memory))
+        facts_thread.daemon = True
+        facts_thread.start()
🧰 Tools
🪛 Ruff

28-28: utils.plugins.get_plugins_data imported but unused

Remove unused import: utils.plugins.get_plugins_data

(F401)

app/lib/main.dart (2)

18-18: LGTM: Import follows proper conventions

The import statement follows Flutter's conventions and maintains proper code organization.


194-198: Verify provider dependencies and ordering

The AddAppProvider configuration looks good and follows the established pattern for proxy providers. However, let's verify:

  1. If any other providers need access to AddAppProvider
  2. If the provider order affects any dependent providers

Let's check for potential provider dependencies:

✅ Verification successful

Let me check the provider dependencies more thoroughly by examining the actual usage context:


Based on the results, I can now generate the final response:

Provider configuration is correctly placed and integrated

The AddAppProvider configuration is properly positioned and integrated:

  • It only depends on AppProvider which is initialized early in the provider chain
  • No other providers depend on AddAppProvider
  • It's used correctly in its consumer widgets (add_app.dart, capabilities_chips_widget.dart, external_trigger_fields_widget.dart)
  • The provider order is appropriate as it's placed after its dependency (AppProvider)
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Search for AddAppProvider usage in other providers
# Look for imports and class dependencies

echo "Checking for AddAppProvider imports:"
rg -l "import.*add_app_provider"

echo -e "\nChecking for AddAppProvider usage in provider classes:"
rg -l "AddAppProvider" --type dart

Length of output: 670


Script:

#!/bin/bash
# Check how AddAppProvider is used in the files
echo "=== Checking AddAppProvider usage context ==="
rg "AddAppProvider" -B 2 -A 2 --type dart

# Check provider order in main.dart
echo -e "\n=== Checking provider configuration in main.dart ==="
rg "Provider[^a-zA-Z]" -A 1 app/lib/main.dart

Length of output: 6574

app/lib/backend/preferences.dart (2)

132-133: LGTM: Getter follows established patterns

The getter implementation follows the class's established patterns for boolean preferences, including proper default value handling.


134-135: LGTM: Setter follows established patterns

The setter implementation correctly uses the existing saveBool helper method and follows the class's established patterns for preference persistence.

app/lib/pages/chat/page.dart (1)

220-220: LGTM! Good refactoring to centralize app sender logic.

Moving the app sender determination logic to the provider improves separation of concerns and maintainability.

Let's verify the provider implementation to ensure consistent behavior:

✅ Verification successful

Let me try a different search pattern to locate the provider implementation.


Let me check the appProvider implementation to ensure the apps list is properly maintained.


Refactoring verified - implementation maintains consistent behavior

The verification shows that:

  • messageSenderApp in MessageProvider correctly uses AppProvider to look up apps
  • Apps list is properly maintained through:
    • Initial loading from API (retrieveApps)
    • Persistent storage via SharedPreferences
    • Updates through notifyListeners pattern
  • The refactoring preserves the same app lookup logic while improving code organization
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify MessageProvider's messageSenderApp implementation
# Expected: Should find the implementation and any related tests

# Look for the messageSenderApp method implementation
echo "Searching for messageSenderApp implementation..."
ast-grep --pattern 'messageSenderApp($id) {
  $$$
}'

# Look for related tests
echo "Searching for related tests..."
fd -e dart -e test "message_provider_test" --exec rg -l "messageSenderApp"

Length of output: 305


Script:

#!/bin/bash
# Search for any class that contains messageSenderApp method
ast-grep --pattern 'class $_ {
  $$$
  App? messageSenderApp($_) {
    $$$
  }
  $$$
}'

# Also try to find any usage of messageSenderApp
rg "messageSenderApp" -A 3

Length of output: 836


Script:

#!/bin/bash
# Find the AppProvider class implementation
ast-grep --pattern 'class AppProvider $_$ {
  $$$
}'

# Look for where apps list is populated/updated
rg "apps =" -A 3

Length of output: 3034

app/lib/pages/memory_detail/widgets.dart (1)

204-204: LGTM: Checkbox shape optimization

Making the shape constant is a good optimization as it prevents unnecessary widget rebuilds.

backend/routers/chat.py (1)

151-152: 🛠️ Refactor suggestion

Ensure plugin is valid before instantiating Plugin.

Similar to the previous issue, plugin might be None, which will raise an exception when unpacked. Validate plugin before proceeding.

Apply this diff to handle the potential None value:

 plugin = get_plugin_by_id_db(plugin_id, uid)
+if plugin is None:
+    raise HTTPException(status_code=404, detail='Plugin not found')
 plugin = Plugin(**plugin)

Likely invalid or redundant comment.

app/lib/pages/apps/providers/add_app_provider.dart (1)

13-252: Overall, the code is well-structured and implements the required functionality effectively.

The AddAppProvider class is comprehensive, handling state management, form validation, and interaction with backend services appropriately. The use of ChangeNotifier and separation of concerns enhances maintainability.

Comment on lines +17 to +20
bool isValidEmail(String email) {
const emailPattern = r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$';
return RegExp(emailPattern).hasMatch(email);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Update email validation to follow RFC 5322 standards

The current email validation is overly restrictive and doesn't comply with email standards:

  1. Doesn't support valid characters like +, quotes, or international characters
  2. Incorrect TLD length restriction
  3. Doesn't handle comments or special cases defined in RFC 5322

Consider this improved implementation:

 bool isValidEmail(String email) {
+  if (email.length > 254) return false;  // RFC 5321
+  const pattern = r'^[\w!#$%&\'*+/=?^`{|}~-]+(?:\.[\w!#$%&\'*+/=?^`{|}~-]+)*'
+      r'@(?:[\w](?:[\w-]*[\w])?\.)+[\w](?:[\w-]*[\w])?$';
-  const emailPattern = r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$';
-  return RegExp(emailPattern).hasMatch(email);
+  return RegExp(pattern, unicode: true).hasMatch(email);
 }

This implementation:

  • Follows RFC 5322 standards
  • Supports international characters
  • Includes proper length validation
  • Allows all valid special characters
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
bool isValidEmail(String email) {
const emailPattern = r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$';
return RegExp(emailPattern).hasMatch(email);
}
bool isValidEmail(String email) {
if (email.length > 254) return false; // RFC 5321
const pattern = r'^[\w!#$%&\'*+/=?^`{|}~-]+(?:\.[\w!#$%&\'*+/=?^`{|}~-]+)*'
r'@(?:[\w](?:[\w-]*[\w])?\.)+[\w](?:[\w-]*[\w])?$';
return RegExp(pattern, unicode: true).hasMatch(email);
}

Comment on lines +9 to +15
bool isValidWebSocketUrl(String url) {
const webSocketPattern = r'^(wss?:\/\/)?([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)?'
r'((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}'
r'(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)|'
r'([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,63}(:[0-9]+)?(\/.*)?$';
return RegExp(webSocketPattern).hasMatch(url);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve WebSocket URL validation robustness

The WebSocket URL validation has similar limitations to the URL validator and needs stricter protocol enforcement.

Consider this improved implementation:

 bool isValidWebSocketUrl(String url) {
+  if (url.length > 2048) return false;
+  try {
+    final uri = Uri.parse(url);
+    return uri.hasScheme && 
+           (uri.scheme == 'ws' || uri.scheme == 'wss') &&
+           uri.host.isNotEmpty;
+  } catch (e) {
+    return false;
+  }
-  const webSocketPattern = r'^(wss?:\/\/)?([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)?'
-      r'((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}'
-      r'(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)|'
-      r'([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,63}(:[0-9]+)?(\/.*)?$';
-  return RegExp(webSocketPattern).hasMatch(url);
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
bool isValidWebSocketUrl(String url) {
const webSocketPattern = r'^(wss?:\/\/)?([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)?'
r'((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}'
r'(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)|'
r'([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,63}(:[0-9]+)?(\/.*)?$';
return RegExp(webSocketPattern).hasMatch(url);
}
bool isValidWebSocketUrl(String url) {
if (url.length > 2048) return false;
try {
final uri = Uri.parse(url);
return uri.hasScheme &&
(uri.scheme == 'ws' || uri.scheme == 'wss') &&
uri.host.isNotEmpty;
} catch (e) {
return false;
}
}

Comment on lines +1 to +7
bool isValidUrl(String url) {
const urlPattern = r'^(https?:\/\/)?([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)?'
r'((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}'
r'(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)|'
r'([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,63}(:[0-9]+)?(\/.*)?$';
return RegExp(urlPattern).hasMatch(url);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Enhance URL validation for better security and compliance

The current URL validation has several limitations that could affect security and functionality:

  1. Missing IPv6 support
  2. No maximum length validation
  3. Ambiguous protocol handling
  4. Limited support for percent-encoded characters
  5. Potential for invalid TLD acceptance

Consider using this improved implementation:

 bool isValidUrl(String url) {
+  if (url.length > 2048) return false;  // Common URL length limit
+  try {
+    final uri = Uri.parse(url);
+    return uri.hasScheme && 
+           (uri.scheme == 'http' || uri.scheme == 'https') &&
+           uri.host.isNotEmpty;
+  } catch (e) {
+    return false;
+  }
-  const urlPattern = r'^(https?:\/\/)?([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)?'
-      r'((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}'
-      r'(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)|'
-      r'([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,63}(:[0-9]+)?(\/.*)?$';
-  return RegExp(urlPattern).hasMatch(url);
 }

This implementation:

  • Uses Dart's built-in Uri parser for robust URL validation
  • Enforces HTTPS/HTTP schemes
  • Handles IPv6, percent-encoding, and international domains correctly
  • Includes length validation
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
bool isValidUrl(String url) {
const urlPattern = r'^(https?:\/\/)?([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)?'
r'((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}'
r'(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)|'
r'([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,63}(:[0-9]+)?(\/.*)?$';
return RegExp(urlPattern).hasMatch(url);
}
bool isValidUrl(String url) {
if (url.length > 2048) return false; // Common URL length limit
try {
final uri = Uri.parse(url);
return uri.hasScheme &&
(uri.scheme == 'http' || uri.scheme == 'https') &&
uri.host.isNotEmpty;
} catch (e) {
return false;
}
}

Comment on lines +11 to +19
return TextFormField(
maxLines: null,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter a valid prompt';
}
return null;
},
controller: controller,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Enhance input validation and constraints.

The current validation is basic and could be improved in several ways:

  1. Add trim() to prevent whitespace-only inputs
  2. Consider adding maxLength constraint
  3. Add optional validation pattern support
 return TextFormField(
   maxLines: null,
+  maxLength: 1000, // Or any appropriate limit
   validator: (value) {
-    if (value == null || value.isEmpty) {
-      return 'Please enter a valid prompt';
+    if (value == null || value.trim().isEmpty) {
+      return validationMessage;
     }
     return null;
   },

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +20 to +51
decoration: InputDecoration(
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(10)),
),
focusedBorder: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(10)),
borderSide: BorderSide(
color: Colors.white,
),
),
label: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon,
color: WidgetStateColor.resolveWith(
(states) => states.contains(WidgetState.focused) ? Colors.white : Colors.grey)),
const SizedBox(
width: 8,
),
Text(
label,
),
],
),
alignLabelWithHint: true,
labelStyle: const TextStyle(
color: Colors.grey,
),
floatingLabelStyle: const TextStyle(
color: Colors.white,
),
),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Use theme-based styling instead of hard-coded colors.

The current implementation uses hard-coded colors which may not adapt well to different themes or color schemes. Consider:

  1. Using Theme.of(context) for colors
  2. Extracting repeated border radius to a constant
  3. Supporting both light and dark modes
+  static const borderRadius = BorderRadius.all(Radius.circular(10));
+
   decoration: InputDecoration(
     border: const OutlineInputBorder(
-      borderRadius: BorderRadius.all(Radius.circular(10)),
+      borderRadius: borderRadius,
     ),
     focusedBorder: const OutlineInputBorder(
-      borderRadius: BorderRadius.all(Radius.circular(10)),
+      borderRadius: borderRadius,
       borderSide: BorderSide(
-        color: Colors.white,
+        color: Theme.of(context).colorScheme.primary,
       ),
     ),
     // ... other decorations ...
     labelStyle: TextStyle(
-      color: Colors.grey,
+      color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
     ),
     floatingLabelStyle: TextStyle(
-      color: Colors.white,
+      color: Theme.of(context).colorScheme.primary,
     ),

Committable suggestion skipped: line range outside the PR's diff.

Comment thread backend/routers/plugins.py Outdated
Comment on lines +122 to +135
def enable_plugin_endpoint(plugin_id: str, uid: str = Depends(auth.get_current_user_uid)):
plugin = get_plugin_by_id_db(plugin_id, uid)
plugin = Plugin(**plugin)
if not plugin:
raise HTTPException(status_code=404, detail='Plugin not found')
if plugin.works_externally() and plugin.external_integration.setup_completed_url:
res = requests.get(plugin.external_integration.setup_completed_url + f'?uid={uid}')
print('enable_plugin_endpoint', res.status_code, res.content)
if res.status_code != 200 or not res.json().get('is_setup_completed', False):
raise HTTPException(status_code=400, detail='Plugin setup is not completed')
increase_plugin_installs_count(plugin_id)
enable_plugin(uid, plugin_id)
return {'status': 'ok'}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Avoid redefining enable_plugin_endpoint; function already defined earlier

Redefining enable_plugin_endpoint at line 122 overwrites the existing function defined earlier, which can lead to unexpected behavior. Consider renaming the new function to prevent conflicts.

Apply this diff to rename the function:

-def enable_plugin_endpoint(plugin_id: str, uid: str = Depends(auth.get_current_user_uid)):
+def enable_plugin_v2_endpoint(plugin_id: str, uid: str = Depends(auth.get_current_user_uid)):
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def enable_plugin_endpoint(plugin_id: str, uid: str = Depends(auth.get_current_user_uid)):
plugin = get_plugin_by_id_db(plugin_id, uid)
plugin = Plugin(**plugin)
if not plugin:
raise HTTPException(status_code=404, detail='Plugin not found')
if plugin.works_externally() and plugin.external_integration.setup_completed_url:
res = requests.get(plugin.external_integration.setup_completed_url + f'?uid={uid}')
print('enable_plugin_endpoint', res.status_code, res.content)
if res.status_code != 200 or not res.json().get('is_setup_completed', False):
raise HTTPException(status_code=400, detail='Plugin setup is not completed')
increase_plugin_installs_count(plugin_id)
enable_plugin(uid, plugin_id)
return {'status': 'ok'}
def enable_plugin_v2_endpoint(plugin_id: str, uid: str = Depends(auth.get_current_user_uid)):
plugin = get_plugin_by_id_db(plugin_id, uid)
plugin = Plugin(**plugin)
if not plugin:
raise HTTPException(status_code=404, detail='Plugin not found')
if plugin.works_externally() and plugin.external_integration.setup_completed_url:
res = requests.get(plugin.external_integration.setup_completed_url + f'?uid={uid}')
print('enable_plugin_endpoint', res.status_code, res.content)
if res.status_code != 200 or not res.json().get('is_setup_completed', False):
raise HTTPException(status_code=400, detail='Plugin setup is not completed')
increase_plugin_installs_count(plugin_id)
enable_plugin(uid, plugin_id)
return {'status': 'ok'}
🧰 Tools
🪛 Ruff

122-122: Redefinition of unused enable_plugin_endpoint from line 25

(F811)

Comment on lines +204 to +206
color: WidgetStateColor.resolveWith(
(states) => states.contains(WidgetState.focused) ? Colors.white : Colors.grey)),
const SizedBox(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Incorrect usage of WidgetStateColor and WidgetState

The classes WidgetStateColor and WidgetState do not exist in Flutter. Instead, you should use MaterialStateColor and MaterialState to properly handle color changes based on widget states.

Apply this diff to fix the issue:

- color: WidgetStateColor.resolveWith(
-   (states) => states.contains(WidgetState.focused) ? Colors.white : Colors.grey)),
+ color: MaterialStateColor.resolveWith(
+   (states) => states.contains(MaterialState.focused) ? Colors.white : Colors.grey)),

Make this change in all the affected lines where WidgetStateColor and WidgetState are used:

  • Lines 204-206
  • Lines 247-249
  • Lines 299-301
  • Lines 343-345
  • Lines 387-389
  • Lines 467-469

Also applies to: 247-249, 299-301, 343-345, 387-389, 467-469

Comment on lines +182 to +189
TextFormField(
controller: provider.appNameController,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter app name';
}
return null;
},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add validation to ensure an app icon is uploaded

Currently, there's no validation to check if an app icon has been uploaded before form submission. This could lead to apps without icons being submitted.

Consider adding a validator in the validateForm method to check if provider.imageFile is not null. For example:

if (provider.imageFile == null) {
  // Show an error message
  return false;
}

Comment on lines +503 to +505
value: provider.termsAgreed,
onChanged: provider.setTermsAgreed,
),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Ensure terms are agreed before submission

There is no validation to confirm that the user has agreed to the Terms of Service and Privacy Policy before submitting the app.

Add validation to check provider.termsAgreed in the validateForm method and prevent form submission if it's false.

Example:

// In validateForm method
if (!termsAgreed) {
  // Show an error message
  return false;
}

And provide feedback to the user in case they haven't agreed:

if (!provider.termsAgreed) {
  ScaffoldMessenger.of(context).showSnackBar(
    SnackBar(content: Text('You must agree to the Terms of Service and Privacy Policy')),
  );
  return;
}

Also applies to: 531-563

Comment on lines +532 to +555
if (isValid) {
showDialog(
context: context,
builder: (ctx) {
return ConfirmationDialog(
title: 'Submit App?',
description: provider.privacyLevel == 'public'
? 'Your app will be reviewed and made public. You can start using it immediately, even during the review!'
: 'Your app will be reviewed and made available to you privately. You can start using it immediately, even during the review!',
checkboxText: "Don't show it again",
checkboxValue: !showSubmitAppConfirmation,
updateCheckboxValue: (value) {
if (value != null) {
setState(() {
showSubmitAppConfirmation = !value;
});
}
},
onConfirm: () async {
SharedPreferencesUtil().showSubmitAppConfirmation = showSubmitAppConfirmation;

Navigator.pop(context);
await provider.submitApp();
},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Handle errors during app submission

While submitting the app using provider.submitApp(), consider handling exceptions and providing user feedback in case of failures.

Apply this diff to enhance error handling:

onPressed: () {
  var isValid = provider.validateForm();
  if (isValid) {
+   try {
      if (showSubmitAppConfirmation) {
        // Existing code to show confirmation dialog
      } else {
        // Proceed with submission
+       await provider.submitApp();
+       // Provide success feedback
+       ScaffoldMessenger.of(context).showSnackBar(
+         SnackBar(content: Text('App submitted successfully')),
+       );
      }
+   } catch (e) {
+     // Handle submission error
+     ScaffoldMessenger.of(context).showSnackBar(
+       SnackBar(content: Text('Failed to submit app: \$e')),
+     );
+   }
  }
},

Committable suggestion skipped: line range outside the PR's diff.

@beastoin
Copy link
Copy Markdown
Collaborator

beastoin commented Nov 5, 2024

1/ provide steps to deploy please
2/ put omi_plugins bucket name to a constant
3/ why do we need the new route for /messages ?
4/ better api route defination for /unapproved-plugin/public, read some best practice about how to define a good api path.
5/ why do we need the api plugin migration ? would we use it twice or more ?

@mdmohsin7

@beastoin
Copy link
Copy Markdown
Collaborator

beastoin commented Nov 5, 2024

6/ could we navigate the submit.omi.me to the app? how about our web portal to submit new apps ?

@mdmohsin7

*i will not edit the previous comment to ensure you dont miss anything as updates wouldnt be sent as a notification.

@mdmohsin7
Copy link
Copy Markdown
Member Author

1/ Same existing backend deploy steps sir
2/ Yes totally forgot to change this
3/ You are right, we shouldn't be needing them (realised after your yesterday's message)
4/ Alright will do
5/ No I only added it temporarily because while this PR is being worked on, more plugins might get added to the json file, so did not remove it completely
6/ Yes we will have to setup deep links for that. Submit form on web portal is also possible, since we have the APIs ready

Will ping you once I make the necessary changes @beastoin

@mdmohsin7
Copy link
Copy Markdown
Member Author

Made the necessary changes, review pls @beastoin

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

🧹 Outside diff range and nitpick comments (3)
backend/database/plugins.py (2)

16-16: Define bucket name as a constant

Consider defining the bucket name as a module-level constant for better maintainability:

-omi_plugins_bucket = os.getenv('BUCKET_PLUGINS_LOGOS')
+PLUGINS_BUCKET_NAME = os.getenv('BUCKET_PLUGINS_LOGOS')
+if not PLUGINS_BUCKET_NAME:
+    raise ValueError("BUCKET_PLUGINS_LOGOS environment variable is not set")

Line range hint 1-126: Enhance code documentation and type safety

The codebase would benefit from:

  1. Adding docstrings to all functions
  2. Consistent type hints
  3. Return type annotations

Example for one function:

-def change_plugin_approval_status(plugin_id: str, approved: bool):
+def change_plugin_approval_status(plugin_id: str, approved: bool) -> None:
+    """Update the approval status of a plugin.
+    
+    Args:
+        plugin_id (str): The ID of the plugin to update
+        approved (bool): The new approval status
+    
+    Raises:
+        ValueError: If plugin_id is empty or plugin doesn't exist
+    """
+    if not plugin_id:
+        raise ValueError("plugin_id cannot be empty")
     plugin_ref = db.collection('plugins_data').document(plugin_id)
     plugin_ref.update({'approved': approved})
backend/routers/plugins.py (1)

218-218: Address TODO for audio_bytes trigger

The comment indicates that audio_bytes trigger functionality needs to be implemented.

Would you like me to help implement the audio_bytes trigger functionality or create a GitHub issue to track this task?

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 78821c7 and 77123c4.

📒 Files selected for processing (5)
  • backend/.env.template (1 hunks)
  • backend/database/plugins.py (2 hunks)
  • backend/routers/chat.py (4 hunks)
  • backend/routers/plugins.py (2 hunks)
  • backend/utils/other/storage.py (2 hunks)
✅ Files skipped from review due to trivial changes (1)
  • backend/.env.template
🚧 Files skipped from review as they are similar to previous changes (2)
  • backend/routers/chat.py
  • backend/utils/other/storage.py
🧰 Additional context used
🪛 Ruff
backend/routers/plugins.py

122-122: Redefinition of unused enable_plugin_endpoint from line 25

(F811)


138-138: Redefinition of unused disable_plugin_endpoint from line 40

(F811)


148-148: Redefinition of unused get_plugins from line 60

(F811)


163-163: Do not perform function call File in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


163-163: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


173-173: f-string without any placeholders

Remove extraneous f prefix

(F541)

🔇 Additional comments (2)
backend/database/plugins.py (1)

102-104: LGTM!

The implementation is correct and efficient.

backend/routers/plugins.py (1)

200-207: LGTM! Well-implemented visibility change endpoint

The implementation includes proper authentication, error handling, and maintains the plugin's visibility state correctly.

Comment on lines +80 to +99
def get_private_plugins_db(uid: str) -> List:
private_plugins = db.collection('users').document(uid).collection('plugins').stream()
data = [doc.to_dict() for doc in private_plugins]
return data


def get_unapproved_public_plugins_db() -> List:
public_plugins = db.collection('plugins_data').where('approved', '==', False).stream()
return [doc.to_dict() for doc in public_plugins]


def get_public_plugins_db(uid: str) -> List:
public_plugins = db.collection('plugins_data').where('approved', '==', True).stream()
data = [doc.to_dict() for doc in public_plugins]

# Include the doc if it is not approved but uid matches
unapproved = db.collection('plugins_data').where('approved', '==', False).where('uid', '==', uid).stream()
data.extend([doc.to_dict() for doc in unapproved])

return data
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Implement pagination for plugin retrieval

These functions fetch all documents at once, which could cause performance issues with large datasets. Consider implementing pagination.

-def get_public_plugins_db(uid: str) -> List:
+def get_public_plugins_db(uid: str, page_size: int = 20, last_doc = None) -> tuple[List, any]:
     public_plugins = db.collection('plugins_data').where('approved', '==', True)
+    if last_doc:
+        public_plugins = public_plugins.start_after(last_doc)
+    public_plugins = public_plugins.limit(page_size).stream()
-    data = [doc.to_dict() for doc in public_plugins]
+    docs = list(public_plugins)
+    data = [doc.to_dict() for doc in docs]
+    last_doc = docs[-1] if docs else None
-    return data
+    return data, last_doc

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +41 to +46
def get_plugin_by_id_db(plugin_id: str, uid: str):
if 'private' in plugin_id:
plugin_ref = db.collection('users').document(uid).collection('plugins').document(plugin_id)
else:
plugin_ref = db.collection('plugins_data').document(plugin_id)
return plugin_ref.get().to_dict()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add error handling and validation for plugin retrieval

The function needs proper error handling for non-existent documents and uid validation.

-def get_plugin_by_id_db(plugin_id: str, uid: str):
+def get_plugin_by_id_db(plugin_id: str, uid: str | None = None):
+    if not plugin_id:
+        raise ValueError("plugin_id cannot be empty")
+    if 'private' in plugin_id and not uid:
+        raise ValueError("uid is required for private plugins")
     if 'private' in plugin_id:
         plugin_ref = db.collection('users').document(uid).collection('plugins').document(plugin_id)
     else:
         plugin_ref = db.collection('plugins_data').document(plugin_id)
-    return plugin_ref.get().to_dict()
+    doc = plugin_ref.get()
+    if not doc.exists:
+        return None
+    return doc.to_dict()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def get_plugin_by_id_db(plugin_id: str, uid: str):
if 'private' in plugin_id:
plugin_ref = db.collection('users').document(uid).collection('plugins').document(plugin_id)
else:
plugin_ref = db.collection('plugins_data').document(plugin_id)
return plugin_ref.get().to_dict()
def get_plugin_by_id_db(plugin_id: str, uid: str | None = None):
if not plugin_id:
raise ValueError("plugin_id cannot be empty")
if 'private' in plugin_id and not uid:
raise ValueError("uid is required for private plugins")
if 'private' in plugin_id:
plugin_ref = db.collection('users').document(uid).collection('plugins').document(plugin_id)
else:
plugin_ref = db.collection('plugins_data').document(plugin_id)
doc = plugin_ref.get()
if not doc.exists:
return None
return doc.to_dict()

Comment on lines +64 to +78
def change_plugin_visibility_db(plugin_id: str, private: bool, was_public: bool, uid: str):
if was_public and private: # public -> private
plugin_ref = db.collection('plugins_data').document(plugin_id)
plugin = plugin_ref.get().to_dict()
plugin_ref.delete()
plugin_ref = db.collection('users').document(uid).collection('plugins').document(plugin_id)
plugin['private'] = private
plugin_ref.set(plugin)
elif not was_public and not private: # private -> public
plugin_ref = db.collection('users').document(uid).collection('plugins').document(plugin_id)
plugin = plugin_ref.get().to_dict()
plugin_ref.delete()
plugin_ref = db.collection('plugins_data').document(plugin_id)
plugin_ref.set(plugin)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Use transactions for atomic visibility changes

The current implementation risks data loss if the operation fails between delete and create operations. Use a transaction to ensure atomicity.

 def change_plugin_visibility_db(plugin_id: str, private: bool, was_public: bool, uid: str):
+    @db.transaction
+    def update_visibility_atomic(transaction):
         if was_public and private:  # public -> private
             plugin_ref = db.collection('plugins_data').document(plugin_id)
-            plugin = plugin_ref.get().to_dict()
+            plugin = plugin_ref.get(transaction=transaction).to_dict()
             plugin_ref.delete()
             plugin_ref = db.collection('users').document(uid).collection('plugins').document(plugin_id)
             plugin['private'] = private
-            plugin_ref.set(plugin)
+            transaction.set(plugin_ref, plugin)
         elif not was_public and not private:  # private -> public
             plugin_ref = db.collection('users').document(uid).collection('plugins').document(plugin_id)
-            plugin = plugin_ref.get().to_dict()
+            plugin = plugin_ref.get(transaction=transaction).to_dict()
             plugin_ref.delete()
             plugin_ref = db.collection('plugins_data').document(plugin_id)
-            plugin_ref.set(plugin)
+            transaction.set(plugin_ref, plugin)
+    
+    update_visibility_atomic()

Committable suggestion skipped: line range outside the PR's diff.

Comment thread backend/database/plugins.py Outdated
Comment on lines +107 to +126
# TODO: only temporary, to move from the json file to firestore. Remove after the migration
def add_plugin_script(plugin_data: dict):
img = requests.get("https://raw.githubusercontent.com/BasedHardware/Omi/main/" + plugin_data['image'], stream=True)
bucket = storage_client.bucket(omi_plugins_bucket)
path = plugin_data['image'].split('/plugins/logos/')[1]
blob = bucket.blob(path)
blob.upload_from_file(img.raw)
plugin_data['image'] = f'https://storage.googleapis.com/{omi_plugins_bucket}/{path}'
plugin_data['private'] = False
plugin_data['approved'] = True

if "external_integration" in plugin_data['capabilities']:
plugin_data['external_integration'][
'setup_instructions_file_path'] = "https://raw.githubusercontent.com/BasedHardware/Omi/main/" + \
plugin_data['external_integration']['setup_instructions_file_path']
plugin_ref = db.collection('plugins_data').document(plugin_data['id'])
if plugin_ref.get().exists:
plugin_ref.update(plugin_data)
else:
plugin_ref.set(plugin_data)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add comprehensive error handling and validation

The function needs better error handling and input validation to prevent potential issues.

 def add_plugin_script(plugin_data: dict):
+    if not isinstance(plugin_data, dict) or 'image' not in plugin_data or 'id' not in plugin_data:
+        raise ValueError("Invalid plugin data structure")
+
+    try:
         img = requests.get(
             "https://raw.githubusercontent.com/BasedHardware/Omi/main/" + plugin_data['image'],
             stream=True
         )
+        img.raise_for_status()
+    except requests.RequestException as e:
+        raise RuntimeError(f"Failed to download image: {e}")
+
+    try:
         bucket = storage_client.bucket(omi_plugins_bucket)
-        path = plugin_data['image'].split('/plugins/logos/')[1]
+        path = os.path.basename(plugin_data['image'])
         blob = bucket.blob(path)
         blob.upload_from_file(img.raw)
+    except Exception as e:
+        raise RuntimeError(f"Failed to upload image to storage: {e}")

Committable suggestion skipped: line range outside the PR's diff.

Comment thread backend/routers/plugins.py Outdated
Comment on lines +147 to +150
@router.get('/v3/plugins', tags=['v3'], response_model=List[Plugin])
def get_plugins(uid: str = Depends(auth.get_current_user_uid), include_reviews: bool = True):
return get_plugins_data_from_db(uid, include_reviews=include_reviews)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Rename v3 endpoint function to avoid conflicts

The get_plugins function is redefined from its v1 and v2 versions.

Apply this diff:

 @router.get('/v3/plugins', tags=['v3'], response_model=List[Plugin])
-def get_plugins(uid: str = Depends(auth.get_current_user_uid), include_reviews: bool = True):
+def get_plugins_v3(uid: str = Depends(auth.get_current_user_uid), include_reviews: bool = True):
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@router.get('/v3/plugins', tags=['v3'], response_model=List[Plugin])
def get_plugins(uid: str = Depends(auth.get_current_user_uid), include_reviews: bool = True):
return get_plugins_data_from_db(uid, include_reviews=include_reviews)
@router.get('/v3/plugins', tags=['v3'], response_model=List[Plugin])
def get_plugins_v3(uid: str = Depends(auth.get_current_user_uid), include_reviews: bool = True):
return get_plugins_data_from_db(uid, include_reviews=include_reviews)
🧰 Tools
🪛 Ruff

148-148: Redefinition of unused get_plugins from line 60

(F811)

Comment on lines +165 to +172
data['approved'] = False
data['id'] = data['name'].replace(' ', '-').lower()
data['uid'] = uid
if data['private']:
data['id'] = data['id'] + '-private'
else:
if plugin_id_exists_db(data['id']):
data['id'] = data['id'] + '-' + ''.join([str(random.randint(0, 9)) for _ in range(5)])
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Improve plugin ID generation security

The current ID generation method could be vulnerable to path traversal attacks and collisions.

Apply this diff to improve security:

     data['approved'] = False
-    data['id'] = data['name'].replace(' ', '-').lower()
+    # Sanitize plugin ID to prevent path traversal and ensure uniqueness
+    import re
+    import uuid
+    base_id = re.sub(r'[^a-z0-9-]', '-', data['name'].lower())
+    base_id = re.sub(r'-+', '-', base_id).strip('-')
+    data['id'] = f"{base_id}-{str(uuid.uuid4())[:8]}"
     data['uid'] = uid
     if data['private']:
         data['id'] = data['id'] + '-private'
-    else:
-        if plugin_id_exists_db(data['id']):
-            data['id'] = data['id'] + '-' + ''.join([str(random.randint(0, 9)) for _ in range(5)])
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
data['approved'] = False
data['id'] = data['name'].replace(' ', '-').lower()
data['uid'] = uid
if data['private']:
data['id'] = data['id'] + '-private'
else:
if plugin_id_exists_db(data['id']):
data['id'] = data['id'] + '-' + ''.join([str(random.randint(0, 9)) for _ in range(5)])
data['approved'] = False
# Sanitize plugin ID to prevent path traversal and ensure uniqueness
import re
import uuid
base_id = re.sub(r'[^a-z0-9-]', '-', data['name'].lower())
base_id = re.sub(r'-+', '-', base_id).strip('-')
data['id'] = f"{base_id}-{str(uuid.uuid4())[:8]}"
data['uid'] = uid
if data['private']:
data['id'] = data['id'] + '-private'

Comment thread backend/routers/plugins.py Outdated
Comment on lines +152 to +160
@router.get('/v1/migrate-plugins', tags=['v1'])
def migrate_plugins():
response = requests.get('https://raw.githubusercontent.com/BasedHardware/Omi/main/community-plugins.json')
if response.status_code != 200:
return []
data = response.json()
for plugin in data:
add_plugin_script(plugin)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add error handling for plugin migration

The migration endpoint lacks proper error handling for JSON parsing and validation of plugin data structure.

Apply this diff to add proper error handling:

 @router.get('/v1/migrate-plugins', tags=['v1'])
 def migrate_plugins():
     response = requests.get('https://raw.githubusercontent.com/BasedHardware/Omi/main/community-plugins.json')
     if response.status_code != 200:
         return []
-    data = response.json()
-    for plugin in data:
-        add_plugin_script(plugin)
+    try:
+        data = response.json()
+        for plugin in data:
+            if not isinstance(plugin, dict) or 'name' not in plugin:
+                continue
+            add_plugin_script(plugin)
+        return {'status': 'ok', 'count': len(data)}
+    except json.JSONDecodeError:
+        raise HTTPException(status_code=500, detail='Invalid JSON data from source')
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@router.get('/v1/migrate-plugins', tags=['v1'])
def migrate_plugins():
response = requests.get('https://raw.githubusercontent.com/BasedHardware/Omi/main/community-plugins.json')
if response.status_code != 200:
return []
data = response.json()
for plugin in data:
add_plugin_script(plugin)
@router.get('/v1/migrate-plugins', tags=['v1'])
def migrate_plugins():
response = requests.get('https://raw.githubusercontent.com/BasedHardware/Omi/main/community-plugins.json')
if response.status_code != 200:
return []
try:
data = response.json()
for plugin in data:
if not isinstance(plugin, dict) or 'name' not in plugin:
continue
add_plugin_script(plugin)
return {'status': 'ok', 'count': len(data)}
except json.JSONDecodeError:
raise HTTPException(status_code=500, detail='Invalid JSON data from source')

Comment thread backend/routers/plugins.py Outdated
Comment on lines +122 to +134
def enable_plugin_endpoint(plugin_id: str, uid: str = Depends(auth.get_current_user_uid)):
plugin = get_plugin_by_id_db(plugin_id, uid)
plugin = Plugin(**plugin)
if not plugin:
raise HTTPException(status_code=404, detail='Plugin not found')
if plugin.works_externally() and plugin.external_integration.setup_completed_url:
res = requests.get(plugin.external_integration.setup_completed_url + f'?uid={uid}')
print('enable_plugin_endpoint', res.status_code, res.content)
if res.status_code != 200 or not res.json().get('is_setup_completed', False):
raise HTTPException(status_code=400, detail='Plugin setup is not completed')
increase_plugin_installs_count(plugin_id)
enable_plugin(uid, plugin_id)
return {'status': 'ok'}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix critical error in plugin existence check

The error check is performed after attempting to create the Plugin instance, which could cause a runtime error if the plugin doesn't exist.

Apply this diff to fix the error handling flow:

 def enable_plugin_endpoint(plugin_id: str, uid: str = Depends(auth.get_current_user_uid)):
     plugin = get_plugin_by_id_db(plugin_id, uid)
-    plugin = Plugin(**plugin)
     if not plugin:
         raise HTTPException(status_code=404, detail='Plugin not found')
+    plugin = Plugin(**plugin)
     if plugin.works_externally() and plugin.external_integration.setup_completed_url:
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def enable_plugin_endpoint(plugin_id: str, uid: str = Depends(auth.get_current_user_uid)):
plugin = get_plugin_by_id_db(plugin_id, uid)
plugin = Plugin(**plugin)
if not plugin:
raise HTTPException(status_code=404, detail='Plugin not found')
if plugin.works_externally() and plugin.external_integration.setup_completed_url:
res = requests.get(plugin.external_integration.setup_completed_url + f'?uid={uid}')
print('enable_plugin_endpoint', res.status_code, res.content)
if res.status_code != 200 or not res.json().get('is_setup_completed', False):
raise HTTPException(status_code=400, detail='Plugin setup is not completed')
increase_plugin_installs_count(plugin_id)
enable_plugin(uid, plugin_id)
return {'status': 'ok'}
def enable_plugin_endpoint(plugin_id: str, uid: str = Depends(auth.get_current_user_uid)):
plugin = get_plugin_by_id_db(plugin_id, uid)
if not plugin:
raise HTTPException(status_code=404, detail='Plugin not found')
plugin = Plugin(**plugin)
if plugin.works_externally() and plugin.external_integration.setup_completed_url:
res = requests.get(plugin.external_integration.setup_completed_url + f'?uid={uid}')
print('enable_plugin_endpoint', res.status_code, res.content)
if res.status_code != 200 or not res.json().get('is_setup_completed', False):
raise HTTPException(status_code=400, detail='Plugin setup is not completed')
increase_plugin_installs_count(plugin_id)
enable_plugin(uid, plugin_id)
return {'status': 'ok'}
🧰 Tools
🪛 Ruff

122-122: Redefinition of unused enable_plugin_endpoint from line 25

(F811)

Comment thread backend/routers/plugins.py Outdated
Comment on lines +121 to +145
@router.post('/v2/plugins/enable')
def enable_plugin_endpoint(plugin_id: str, uid: str = Depends(auth.get_current_user_uid)):
plugin = get_plugin_by_id_db(plugin_id, uid)
plugin = Plugin(**plugin)
if not plugin:
raise HTTPException(status_code=404, detail='Plugin not found')
if plugin.works_externally() and plugin.external_integration.setup_completed_url:
res = requests.get(plugin.external_integration.setup_completed_url + f'?uid={uid}')
print('enable_plugin_endpoint', res.status_code, res.content)
if res.status_code != 200 or not res.json().get('is_setup_completed', False):
raise HTTPException(status_code=400, detail='Plugin setup is not completed')
increase_plugin_installs_count(plugin_id)
enable_plugin(uid, plugin_id)
return {'status': 'ok'}


@router.post('/v2/plugins/disable')
def disable_plugin_endpoint(plugin_id: str, uid: str = Depends(auth.get_current_user_uid)):
plugin = get_plugin_by_id_db(plugin_id, uid)
if not plugin:
raise HTTPException(status_code=404, detail='Plugin not found')
disable_plugin(uid, plugin_id)
decrease_plugin_installs_count(plugin_id)
return {'status': 'ok'}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Rename v2 endpoint functions to avoid conflicts

Both enable_plugin_endpoint and disable_plugin_endpoint are redefined from their v1 versions, which could lead to confusion and potential bugs.

Apply this diff to rename the functions:

 @router.post('/v2/plugins/enable')
-def enable_plugin_endpoint(plugin_id: str, uid: str = Depends(auth.get_current_user_uid)):
+def enable_plugin_v2_endpoint(plugin_id: str, uid: str = Depends(auth.get_current_user_uid)):

 @router.post('/v2/plugins/disable')
-def disable_plugin_endpoint(plugin_id: str, uid: str = Depends(auth.get_current_user_uid)):
+def disable_plugin_v2_endpoint(plugin_id: str, uid: str = Depends(auth.get_current_user_uid)):
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@router.post('/v2/plugins/enable')
def enable_plugin_endpoint(plugin_id: str, uid: str = Depends(auth.get_current_user_uid)):
plugin = get_plugin_by_id_db(plugin_id, uid)
plugin = Plugin(**plugin)
if not plugin:
raise HTTPException(status_code=404, detail='Plugin not found')
if plugin.works_externally() and plugin.external_integration.setup_completed_url:
res = requests.get(plugin.external_integration.setup_completed_url + f'?uid={uid}')
print('enable_plugin_endpoint', res.status_code, res.content)
if res.status_code != 200 or not res.json().get('is_setup_completed', False):
raise HTTPException(status_code=400, detail='Plugin setup is not completed')
increase_plugin_installs_count(plugin_id)
enable_plugin(uid, plugin_id)
return {'status': 'ok'}
@router.post('/v2/plugins/disable')
def disable_plugin_endpoint(plugin_id: str, uid: str = Depends(auth.get_current_user_uid)):
plugin = get_plugin_by_id_db(plugin_id, uid)
if not plugin:
raise HTTPException(status_code=404, detail='Plugin not found')
disable_plugin(uid, plugin_id)
decrease_plugin_installs_count(plugin_id)
return {'status': 'ok'}
@router.post('/v2/plugins/enable')
def enable_plugin_v2_endpoint(plugin_id: str, uid: str = Depends(auth.get_current_user_uid)):
plugin = get_plugin_by_id_db(plugin_id, uid)
plugin = Plugin(**plugin)
if not plugin:
raise HTTPException(status_code=404, detail='Plugin not found')
if plugin.works_externally() and plugin.external_integration.setup_completed_url:
res = requests.get(plugin.external_integration.setup_completed_url + f'?uid={uid}')
print('enable_plugin_endpoint', res.status_code, res.content)
if res.status_code != 200 or not res.json().get('is_setup_completed', False):
raise HTTPException(status_code=400, detail='Plugin setup is not completed')
increase_plugin_installs_count(plugin_id)
enable_plugin(uid, plugin_id)
return {'status': 'ok'}
@router.post('/v2/plugins/disable')
def disable_plugin_v2_endpoint(plugin_id: str, uid: str = Depends(auth.get_current_user_uid)):
plugin = get_plugin_by_id_db(plugin_id, uid)
if not plugin:
raise HTTPException(status_code=404, detail='Plugin not found')
disable_plugin(uid, plugin_id)
decrease_plugin_installs_count(plugin_id)
return {'status': 'ok'}
🧰 Tools
🪛 Ruff

122-122: Redefinition of unused enable_plugin_endpoint from line 25

(F811)


138-138: Redefinition of unused disable_plugin_endpoint from line 40

(F811)

@beastoin
Copy link
Copy Markdown
Collaborator

beastoin commented Nov 5, 2024

1/ deploy steps, for example:

  • create bucket storage
  • deploy new backend
  • run the plugin migration

please notice about the downtime, try avoiding it.

2/ ok
3/ ok
4/ ok but,

  • post /plugins not /plugins/add
  • patch to /plugins/:id/approve not post
  • ... please double check another routes

5/ ok, add the auth - admin only and create new ticket to remove it.
6/ do we have it yet ? i mean the web portal. also, we didnt have the deeplink yet - i guess

@mdmohsin7 fighting

@mdmohsin7
Copy link
Copy Markdown
Member Author

mdmohsin7 commented Nov 5, 2024

1/ Oh got it. It will be like this:

  • Create bucket
  • Run the migration command locally (this will upload the logos and also adds the plugins data). Locally because there isn't any requirement for it to be done on prod plus there will be zero downtime
  • Deploy the backend

4/ Alright will check and make the necessary changes
5/ I'll do one thing, I'll migrate the data now (since there aren't any open PRs for plugins) and will remove the endpoints
6/ No we don't have the web portal, and yes you are right, the deep link setup isn't there at the moment

@beastoin

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

🧹 Outside diff range and nitpick comments (3)
backend/database/plugins.py (1)

102-109: Consider caching plugin existence checks

These functions might be called frequently during plugin operations. Consider implementing caching to reduce database reads.

Consider using a distributed cache like Redis or Memcached to cache existence checks with a short TTL (e.g., 5 minutes).

app/lib/backend/http/api/apps.dart (1)

167-203: Improve error handling and response parsing.

Both functions could benefit from more robust error handling and response parsing:

  1. Add specific error types for better error handling
  2. Validate response structure before parsing
  3. Add retry mechanism for transient failures

Example improvement for getAppCategories (apply similar pattern to getTriggerEventsServer):

 Future<List<Category>> getAppCategories() async {
+  const maxRetries = 3;
+  var attempt = 0;
+  
+  while (attempt < maxRetries) {
+    try {
       var response = await makeApiCall(
         url: '${Env.apiBaseUrl}v1/plugin-categories',
         headers: {},
         body: '',
         method: 'GET',
       );
-      try {
-        if (response == null || response.statusCode != 200) return [];
-        log('getAppCategories: ${response.body}');
-        var res = jsonDecode(response.body);
+      if (response == null) {
+        throw ApiException('No response from server');
+      }
+      
+      if (response.statusCode != 200) {
+        throw ApiException('Server returned ${response.statusCode}');
+      }
+      
+      if (response.body.isEmpty) {
+        return [];
+      }
+      
+      log('getAppCategories: ${response.body}');
+      var res = jsonDecode(response.body);
+      
+      if (!res.containsKey('categories')) {
+        throw FormatException('Invalid response format');
+      }
+      
         return Category.fromJsonList(res);
-      } catch (e, stackTrace) {
+    } catch (e, stackTrace) {
+      attempt++;
+      if (e is ApiException || attempt >= maxRetries) {
         debugPrint(e.toString());
         CrashReporting.reportHandledCrash(e, stackTrace);
         return [];
-      }
+      }
+      // Wait before retry
+      await Future.delayed(Duration(seconds: attempt));
+    }
+  }
+  return [];
 }
backend/routers/plugins.py (1)

196-196: Track TODO for audio_bytes trigger implementation

The TODO comment indicates pending work for the audio_bytes trigger implementation.

Would you like me to create a GitHub issue to track this TODO item?

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 77123c4 and eab8951.

📒 Files selected for processing (3)
  • app/lib/backend/http/api/apps.dart (4 hunks)
  • backend/database/plugins.py (2 hunks)
  • backend/routers/plugins.py (3 hunks)
🧰 Additional context used
🪛 Ruff
backend/routers/plugins.py

135-135: Do not perform function call File in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


135-135: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


147-147: f-string without any placeholders

Remove extraneous f prefix

(F541)


163-163: Redefinition of unused get_plugins from line 63

(F811)

🔇 Additional comments (4)
app/lib/backend/http/api/apps.dart (1)

74-74: LGTM! URL simplification improves flexibility.

The direct use of appMarkdownPath removes hardcoding and allows for more flexible markdown source locations.

backend/routers/plugins.py (3)

178-185: LGTM! Well-implemented visibility management

The implementation includes proper authentication, error handling, and state tracking.


188-191: ⚠️ Potential issue

Add authentication for accessing unapproved plugins

The endpoint exposes unapproved plugins without authentication, which could leak sensitive information.

Apply this diff:

 @router.get('/v1/plugins/public/unapproved', tags=['v1'])
-def get_unapproved_public_plugins():
+def get_unapproved_public_plugins(uid: str = Depends(auth.get_current_user_uid)):
+    if not is_user_admin(uid):
+        raise HTTPException(status_code=403, detail='Not authorized to view unapproved plugins')
     plugins = get_unapproved_public_plugins_db()
     return plugins

Likely invalid or redundant comment.


166-175: ⚠️ Potential issue

Add authentication and authorization checks

The approve/reject endpoints lack authentication and authorization checks, allowing any user to modify plugin approval status.

Apply this diff to both endpoints:

 @router.post('/v1/plugins/{plugin_id}/approve', tags=['v1'])
-def approve_plugin(plugin_id: str):
+def approve_plugin(plugin_id: str, uid: str = Depends(auth.get_current_user_uid)):
+    if not is_user_admin(uid):
+        raise HTTPException(status_code=403, detail='Not authorized to approve plugins')
     change_plugin_approval_status(plugin_id, True)
     return {'status': 'ok'}

 @router.post('/v1/plugins/{plugin_id}/reject', tags=['v1'])
-def reject_plugin(plugin_id: str):
+def reject_plugin(plugin_id: str, uid: str = Depends(auth.get_current_user_uid)):
+    if not is_user_admin(uid):
+        raise HTTPException(status_code=403, detail='Not authorized to reject plugins')
     change_plugin_approval_status(plugin_id, False)
     return {'status': 'ok'}

Likely invalid or redundant comment.

# ********** CRUD *************
# *****************************

omi_plugins_bucket = os.getenv('BUCKET_PLUGINS_LOGOS')
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add environment variable validation

The omi_plugins_bucket environment variable is critical for plugin logo storage. Add validation to ensure it's properly set.

-omi_plugins_bucket = os.getenv('BUCKET_PLUGINS_LOGOS')
+omi_plugins_bucket = os.getenv('BUCKET_PLUGINS_LOGOS')
+if not omi_plugins_bucket:
+    raise ValueError('BUCKET_PLUGINS_LOGOS environment variable must be set')
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
omi_plugins_bucket = os.getenv('BUCKET_PLUGINS_LOGOS')
omi_plugins_bucket = os.getenv('BUCKET_PLUGINS_LOGOS')
if not omi_plugins_bucket:
raise ValueError('BUCKET_PLUGINS_LOGOS environment variable must be set')

Comment on lines +59 to +61
def change_plugin_approval_status(plugin_id: str, approved: bool):
plugin_ref = db.collection('plugins_data').document(plugin_id)
plugin_ref.update({'approved': approved})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add error handling for approval status changes

The function should verify the document exists and handle potential Firestore exceptions.

 def change_plugin_approval_status(plugin_id: str, approved: bool):
     plugin_ref = db.collection('plugins_data').document(plugin_id)
-    plugin_ref.update({'approved': approved})
+    doc = plugin_ref.get()
+    if not doc.exists:
+        raise ValueError(f"Plugin with id {plugin_id} does not exist")
+    try:
+        plugin_ref.update({'approved': approved})
+    except Exception as e:
+        raise RuntimeError(f"Failed to update plugin approval status: {e}")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def change_plugin_approval_status(plugin_id: str, approved: bool):
plugin_ref = db.collection('plugins_data').document(plugin_id)
plugin_ref.update({'approved': approved})
def change_plugin_approval_status(plugin_id: str, approved: bool):
plugin_ref = db.collection('plugins_data').document(plugin_id)
doc = plugin_ref.get()
if not doc.exists:
raise ValueError(f"Plugin with id {plugin_id} does not exist")
try:
plugin_ref.update({'approved': approved})
except Exception as e:
raise RuntimeError(f"Failed to update plugin approval status: {e}")

Comment on lines +86 to +88
def get_unapproved_public_plugins_db() -> List:
public_plugins = db.collection('plugins_data').where('approved', '==', False).stream()
return [doc.to_dict() for doc in public_plugins]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add access control for unapproved plugins

The get_unapproved_public_plugins_db function allows unrestricted access to unapproved plugins. This could expose sensitive information.

-def get_unapproved_public_plugins_db() -> List:
+def get_unapproved_public_plugins_db(admin_uid: str = None) -> List:
+    if not admin_uid:
+        raise ValueError("Admin UID is required to access unapproved plugins")
+    admin_ref = db.collection('admins').document(admin_uid).get()
+    if not admin_ref.exists:
+        raise ValueError("Unauthorized access to unapproved plugins")
     public_plugins = db.collection('plugins_data').where('approved', '==', False).stream()
     return [doc.to_dict() for doc in public_plugins]

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +113 to +119
def add_plugin_from_community_json(plugin_data: dict):
img = requests.get("https://raw.githubusercontent.com/BasedHardware/Omi/main/" + plugin_data['image'], stream=True)
bucket = storage_client.bucket(omi_plugins_bucket)
path = plugin_data['image'].split('/plugins/logos/')[1]
blob = bucket.blob(path)
blob.upload_from_file(img.raw)
plugin_data['image'] = f'https://storage.googleapis.com/{omi_plugins_bucket}/{path}'
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add file type validation for plugin logos

The function should validate the image file type and size before uploading to prevent security issues and ensure consistent quality.

 def add_plugin_from_community_json(plugin_data: dict):
+    ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'}
+    MAX_SIZE_BYTES = 5 * 1024 * 1024  # 5MB
+
+    # Validate image path
+    if not plugin_data.get('image'):
+        raise ValueError("Plugin image is required")
+    
+    file_ext = plugin_data['image'].split('.')[-1].lower()
+    if file_ext not in ALLOWED_EXTENSIONS:
+        raise ValueError(f"Invalid file type. Allowed types: {ALLOWED_EXTENSIONS}")
+
     img = requests.get("https://raw.githubusercontent.com/BasedHardware/Omi/main/" + plugin_data['image'], stream=True)
+    img.raise_for_status()
+    
+    # Check file size
+    content_length = int(img.headers.get('content-length', 0))
+    if content_length > MAX_SIZE_BYTES:
+        raise ValueError(f"File size exceeds maximum limit of {MAX_SIZE_BYTES/1024/1024}MB")
+
     bucket = storage_client.bucket(omi_plugins_bucket)
     path = plugin_data['image'].split('/plugins/logos/')[1]
     blob = bucket.blob(path)
     blob.upload_from_file(img.raw)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def add_plugin_from_community_json(plugin_data: dict):
img = requests.get("https://raw.githubusercontent.com/BasedHardware/Omi/main/" + plugin_data['image'], stream=True)
bucket = storage_client.bucket(omi_plugins_bucket)
path = plugin_data['image'].split('/plugins/logos/')[1]
blob = bucket.blob(path)
blob.upload_from_file(img.raw)
plugin_data['image'] = f'https://storage.googleapis.com/{omi_plugins_bucket}/{path}'
def add_plugin_from_community_json(plugin_data: dict):
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'}
MAX_SIZE_BYTES = 5 * 1024 * 1024 # 5MB
# Validate image path
if not plugin_data.get('image'):
raise ValueError("Plugin image is required")
file_ext = plugin_data['image'].split('.')[-1].lower()
if file_ext not in ALLOWED_EXTENSIONS:
raise ValueError(f"Invalid file type. Allowed types: {ALLOWED_EXTENSIONS}")
img = requests.get("https://raw.githubusercontent.com/BasedHardware/Omi/main/" + plugin_data['image'], stream=True)
img.raise_for_status()
# Check file size
content_length = int(img.headers.get('content-length', 0))
if content_length > MAX_SIZE_BYTES:
raise ValueError(f"File size exceeds maximum limit of {MAX_SIZE_BYTES/1024/1024}MB")
bucket = storage_client.bucket(omi_plugins_bucket)
path = plugin_data['image'].split('/plugins/logos/')[1]
blob = bucket.blob(path)
blob.upload_from_file(img.raw)
plugin_data['image'] = f'https://storage.googleapis.com/{omi_plugins_bucket}/{path}'

Future<List<App>> retrieveApps() async {
var response = await makeApiCall(
url: '${Env.apiBaseUrl}v2/plugins',
url: '${Env.apiBaseUrl}v3/plugins',
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Maintain API version consistency across endpoints.

The plugins endpoint has been upgraded to v3, but other related endpoints still use v1/v2. This inconsistency should be addressed:

  • submitAppServer: v3
  • enableAppServer: v1
  • Other plugin endpoints: v1

Please ensure all plugin-related endpoints are consistently versioned. If v3 introduces breaking changes, document the migration path for clients.

Comment on lines +141 to +165
Future<bool> submitAppServer(File file, Map<String, dynamic> appData) async {
var request = http.MultipartRequest(
'POST',
Uri.parse('${Env.apiBaseUrl}v3/plugins'),
);
request.files.add(await http.MultipartFile.fromPath('file', file.path, filename: basename(file.path)));
request.headers.addAll({'Authorization': await getAuthHeader()});
request.fields.addAll({'plugin_data': jsonEncode(appData)});
print(jsonEncode(appData));
try {
var streamedResponse = await request.send();
var response = await http.Response.fromStream(streamedResponse);

if (response.statusCode == 200) {
debugPrint('submitAppServer Response body: ${jsonDecode(response.body)}');
return true;
} else {
debugPrint('Failed to submit app. Status code: ${response.statusCode}');
return false;
}
} catch (e) {
debugPrint('An error occurred submitAppServer: $e');
return false;
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Enhance file upload security and robustness.

The file upload implementation needs several critical improvements:

  1. Add file validation:
    • File size limits
    • File type validation
    • Content type verification
  2. Improve error handling:
    • Replace debug prints with proper logging
    • Add specific error types
    • Add upload timeout
  3. Add security headers

Here's the suggested implementation:

 Future<bool> submitAppServer(File file, Map<String, dynamic> appData) async {
+  const maxSizeBytes = 10 * 1024 * 1024; // 10MB
+  if (await file.length() > maxSizeBytes) {
+    log('Error: File size exceeds maximum allowed size');
+    return false;
+  }
+
+  final allowedTypes = ['.zip', '.tar.gz'];
+  if (!allowedTypes.contains(extension(file.path).toLowerCase())) {
+    log('Error: Invalid file type');
+    return false;
+  }
+
   var request = http.MultipartRequest(
     'POST',
     Uri.parse('${Env.apiBaseUrl}v3/plugins'),
   );
   request.files.add(await http.MultipartFile.fromPath('file', file.path, filename: basename(file.path)));
-  request.headers.addAll({'Authorization': await getAuthHeader()});
+  request.headers.addAll({
+    'Authorization': await getAuthHeader(),
+    'Content-Type': 'multipart/form-data',
+    'Accept': 'application/json',
+  });
   request.fields.addAll({'plugin_data': jsonEncode(appData)});
-  print(jsonEncode(appData));
+  log('Submitting app data: ${jsonEncode(appData)}');
   try {
-    var streamedResponse = await request.send();
+    var streamedResponse = await request.send().timeout(
+      const Duration(minutes: 5),
+      onTimeout: () {
+        log('Error: Upload timeout');
+        throw TimeoutException('Upload timed out');
+      },
+    );
     var response = await http.Response.fromStream(streamedResponse);

     if (response.statusCode == 200) {
-      debugPrint('submitAppServer Response body: ${jsonDecode(response.body)}');
+      log('App submitted successfully');
       return true;
     } else {
-      debugPrint('Failed to submit app. Status code: ${response.statusCode}');
+      log('Error: Failed to submit app. Status: ${response.statusCode}, Body: ${response.body}');
       return false;
     }
   } catch (e) {
-    debugPrint('An error occurred submitAppServer: $e');
+    log('Error submitting app: $e');
     return false;
   }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Future<bool> submitAppServer(File file, Map<String, dynamic> appData) async {
var request = http.MultipartRequest(
'POST',
Uri.parse('${Env.apiBaseUrl}v3/plugins'),
);
request.files.add(await http.MultipartFile.fromPath('file', file.path, filename: basename(file.path)));
request.headers.addAll({'Authorization': await getAuthHeader()});
request.fields.addAll({'plugin_data': jsonEncode(appData)});
print(jsonEncode(appData));
try {
var streamedResponse = await request.send();
var response = await http.Response.fromStream(streamedResponse);
if (response.statusCode == 200) {
debugPrint('submitAppServer Response body: ${jsonDecode(response.body)}');
return true;
} else {
debugPrint('Failed to submit app. Status code: ${response.statusCode}');
return false;
}
} catch (e) {
debugPrint('An error occurred submitAppServer: $e');
return false;
}
}
Future<bool> submitAppServer(File file, Map<String, dynamic> appData) async {
const maxSizeBytes = 10 * 1024 * 1024; // 10MB
if (await file.length() > maxSizeBytes) {
log('Error: File size exceeds maximum allowed size');
return false;
}
final allowedTypes = ['.zip', '.tar.gz'];
if (!allowedTypes.contains(extension(file.path).toLowerCase())) {
log('Error: Invalid file type');
return false;
}
var request = http.MultipartRequest(
'POST',
Uri.parse('${Env.apiBaseUrl}v3/plugins'),
);
request.files.add(await http.MultipartFile.fromPath('file', file.path, filename: basename(file.path)));
request.headers.addAll({
'Authorization': await getAuthHeader(),
'Content-Type': 'multipart/form-data',
'Accept': 'application/json',
});
request.fields.addAll({'plugin_data': jsonEncode(appData)});
log('Submitting app data: ${jsonEncode(appData)}');
try {
var streamedResponse = await request.send().timeout(
const Duration(minutes: 5),
onTimeout: () {
log('Error: Upload timeout');
throw TimeoutException('Upload timed out');
},
);
var response = await http.Response.fromStream(streamedResponse);
if (response.statusCode == 200) {
log('App submitted successfully');
return true;
} else {
log('Error: Failed to submit app. Status: ${response.statusCode}, Body: ${response.body}');
return false;
}
} catch (e) {
log('Error submitting app: $e');
return false;
}
}

Comment on lines +27 to +28
plugin = get_plugin_by_id_db(plugin_id, uid)
plugin = Plugin(**plugin)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix potential runtime error in plugin existence check

The plugin instance is created before checking if the plugin exists, which could cause a runtime error if plugin is None.

Apply this diff to both enable and disable endpoints:

-    plugin = get_plugin_by_id_db(plugin_id, uid)
-    plugin = Plugin(**plugin)
+    plugin_data = get_plugin_by_id_db(plugin_id, uid)
     if not plugin:
         raise HTTPException(status_code=404, detail='Plugin not found')
+    plugin = Plugin(**plugin_data)

Also applies to: 43-44

Comment on lines +147 to +152
os.makedirs(f'_temp/plugins', exist_ok=True)
file_path = f"_temp/plugins/{file.filename}"
with open(file_path, 'wb') as f:
f.write(file.file.read())
imgUrl = upload_plugin_logo(file_path, data['id'])
data['image'] = imgUrl
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add cleanup for temporary files

The endpoint creates temporary files but doesn't clean them up after upload, which could lead to disk space issues.

Apply this diff:

     with open(file_path, 'wb') as f:
         f.write(file.file.read())
     imgUrl = upload_plugin_logo(file_path, data['id'])
+    try:
+        os.remove(file_path)
+    except OSError:
+        pass  # Best effort cleanup
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
os.makedirs(f'_temp/plugins', exist_ok=True)
file_path = f"_temp/plugins/{file.filename}"
with open(file_path, 'wb') as f:
f.write(file.file.read())
imgUrl = upload_plugin_logo(file_path, data['id'])
data['image'] = imgUrl
os.makedirs(f'_temp/plugins', exist_ok=True)
file_path = f"_temp/plugins/{file.filename}"
with open(file_path, 'wb') as f:
f.write(file.file.read())
imgUrl = upload_plugin_logo(file_path, data['id'])
try:
os.remove(file_path)
except OSError:
pass # Best effort cleanup
data['image'] = imgUrl
🧰 Tools
🪛 Ruff

147-147: f-string without any placeholders

Remove extraneous f prefix

(F541)

Comment on lines +138 to +146
data['id'] = data['name'].replace(' ', '-').lower()
data['uid'] = uid
if data['private']:
data['id'] = data['id'] + '-private'
if private_plugin_id_exists_db(data['id'], uid):
data['id'] = data['id'] + '-' + ''.join([str(random.randint(0, 9)) for _ in range(5)])
else:
if public_plugin_id_exists_db(data['id']):
data['id'] = data['id'] + '-' + ''.join([str(random.randint(0, 9)) for _ in range(5)])
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve plugin ID generation security

The current ID generation method could lead to collisions and potential security issues.

Consider using a more robust ID generation method:

-    data['id'] = data['name'].replace(' ', '-').lower()
+    import re
+    import uuid
+    # Sanitize and create unique ID
+    base_id = re.sub(r'[^a-z0-9-]', '-', data['name'].lower())
+    base_id = re.sub(r'-+', '-', base_id).strip('-')
+    data['id'] = f"{base_id}-{str(uuid.uuid4())[:8]}"
     data['uid'] = uid
     if data['private']:
         data['id'] = data['id'] + '-private'
-        if private_plugin_id_exists_db(data['id'], uid):
-            data['id'] = data['id'] + '-' + ''.join([str(random.randint(0, 9)) for _ in range(5)])
-    else:
-        if public_plugin_id_exists_db(data['id']):
-            data['id'] = data['id'] + '-' + ''.join([str(random.randint(0, 9)) for _ in range(5)])

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +135 to +137
def add_plugin(plugin_data: str = Form(...), file: UploadFile = File(...), uid=Depends(auth.get_current_user_uid)):
data = json.loads(plugin_data)
data['approved'] = False
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add file validation

The endpoint accepts file uploads without validating the file type or size, which could pose security risks.

Add validation before processing the file:

 def add_plugin(plugin_data: str = Form(...), file: UploadFile = File(...), uid=Depends(auth.get_current_user_uid)):
+    # Validate file size (e.g., limit to 1MB)
+    if len(await file.read()) > 1_048_576:
+        await file.seek(0)  # Reset file pointer
+        raise HTTPException(status_code=400, detail='File too large')
+
+    # Validate file type
+    allowed_types = ['image/png', 'image/jpeg', 'image/jpg']
+    if file.content_type not in allowed_types:
+        raise HTTPException(status_code=400, detail='Invalid file type')
+
     data = json.loads(plugin_data)

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 Ruff

135-135: Do not perform function call File in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


135-135: Do not perform function call Depends in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)

@mdmohsin7
Copy link
Copy Markdown
Member Author

All changes are done! Don't have to run the migration script as well. Covered some edge case as well. Final review please @beastoin

@beastoin
Copy link
Copy Markdown
Collaborator

beastoin commented Nov 5, 2024

lgtm @mdmohsin7 🥳

pull the main and leave it a final check.

rolling out to production in next 2 hours.

@beastoin beastoin merged commit 795748d into main Nov 5, 2024
@beastoin beastoin deleted the add-app branch November 5, 2024 09:01
@mdmohsin7
Copy link
Copy Markdown
Member Author

Alright will have a final check

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A simple way to add/submit apps (plugins)

2 participants