10000 chore: refactor incoming_message_baileys_service using composition and fix some failure points by CayoPOliveira · Pull Request #53 · fazer-ai/chatwoot · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

chore: refactor incoming_message_baileys_service using composition and fix some failure points #53

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintaine 8000 rs and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Conversation

CayoPOliveira
Copy link
Collaborator
@CayoPOliveira CayoPOliveira commented May 25, 2025

Pull Request Template

Description

Please include a summary of the change and issue(s) fixed. Also, mention relevant motivation, context, and any dependencies that this change requires.
Fixes # (issue)

Type of change

Please delete options that are not relevant.

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality not to work as expected)
  • This change requires a documentation update

How Has This Been Tested?

Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration.

Checklist:

  • My code follows the style guidelines of this project
  • I have performed a self-review of my code
  • I have commented on my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been merged and published in downstream modules

This change is Reviewable

Summary by CodeRabbit

  • New Features

    • Enhanced WhatsApp integration with improved processing of connection updates, message statuses, and incoming messages.
    • Added support for media attachments, message reactions, and edited messages in WhatsApp conversations.
  • Refactor

    • Simplified WhatsApp event handling by delegating logic to dedicated modules for better maintainability and clarity.
  • Tests

    • Added test coverage for handling attachment download failures during image message processing, ensuring graceful error handling and message creation.
    • Added tests verifying message content updates and edit tracking for edited WhatsApp messages.

@CayoPOliveira CayoPOliveira self-assigned this May 25, 2025
@CayoPOliveira CayoPOliveira added enhancement New feature or request review needed labels May 25, 2025
@gabrieljablonski
Copy link

Copy link
coderabbitai bot commented May 25, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

The codebase has been refactored to extract WhatsApp Baileys event handling logic from Whatsapp::IncomingMessageBaileysService into dedicated handler modules. New modules for connection updates, message upserts, message updates, and helper methods have been introduced, with the service class now delegating event processing to these modules.

Changes

File(s) Change Summary
app/services/whatsapp/baileys_handlers/connection_update.rb Added module for processing WhatsApp Baileys connection updates, updating connection state and logging errors.
app/services/whatsapp/baileys_handlers/helpers.rb Introduced helper module for extracting and interpreting WhatsApp message attributes, including message type, content, and caching.
app/services/whatsapp/baileys_handlers/messages_update.rb Added module for handling WhatsApp Baileys message update events, including status mapping, status transitions, and edited content.
app/services/whatsapp/baileys_handlers/messages_upsert.rb Added module for processing WhatsApp Baileys message upserts, including contact/conversation setup, media attachment, and message creation.
app/services/whatsapp/incoming_message_baileys_service.rb Refactored to delegate all event processing to new handler modules; removed internal implementation and error classes.
spec/services/whatsapp/incoming_message_baileys_service_spec.rb Added tests for attachment download failure handling and message content update on edited messages.
app/controllers/webhooks/whatsapp_controller.rb Removed rescue clause for AttachmentNotFoundError in webhook controller, allowing exception to propagate.

Sequence Diagram(s)

sequenceDiagram
    participant Webhook
    participant IncomingMessageBaileysService
    participant ConnectionUpdate
    participant MessagesUpsert
    participant MessagesUpdate

    Webhook->>IncomingMessageBaileysService: perform(event_name, params)
    alt event_name == "connection.update"
        IncomingMessageBaileysService->>ConnectionUpdate: process_connection_update(params)
    else event_name == "messages.upsert"
        IncomingMessageBaileysService->>MessagesUpsert: process_messages_upsert(params)
    else event_name == "messages.update"
        IncomingMessageBaileysService->>MessagesUpdate: process_messages_update(params)
    end
Loading

Possibly related PRs

Poem

🐇 Hopping through code with nimble feet,
Splitting handlers to make it neat.
Connections, messages, helpers too,
Each module knows just what to do.
Logs and updates now in their place,
A cleaner service, swift in race!
✨📱🐰

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 RuboCop (1.75.5)
app/services/whatsapp/baileys_handlers/helpers.rb

rubocop-performance extension supports plugin, specify plugins: rubocop-performance instead of require: rubocop-performance in /.rubocop.yml.
For more information, see https://docs.rubocop.org/rubocop/plugin_migration_guide.html.
rubocop-rails extension supports plugin, specify plugins: rubocop-rails instead of require: rubocop-rails in /.rubocop.yml.
For more information, see https://docs.rubocop.org/rubocop/plugin_migration_guide.html.
rubocop-rspec extension supports plugin, specify plugins: rubocop-rspec instead of require: rubocop-rspec in /.rubocop.yml.
For more information, see https://docs.rubocop.org/rubocop/plugin_migration_guide.html.
Error: RSpec/FactoryBot/* has been extracted to the rubocop-factory_bot gem.
(obsolete configuration found in .rubocop.yml, please update it)

app/services/whatsapp/baileys_handlers/messages_update.rb

rubocop-performance extension supports plugin, specify plugins: rubocop-performance instead of require: rubocop-performance in /.rubocop.yml.
For more information, see https://docs.rubocop.org/rubocop/plugin_migration_guide.html.
rubocop-rails extension supports plugin, specify plugins: rubocop-rails instead of require: rubocop-rails in /.rubocop.yml.
For more information, see https://docs.rubocop.org/rubocop/plugin_migration_guide.html.
rubocop-rspec extension supports plugin, specify plugins: rubocop-rspec instead of require: rubocop-rspec in /.rubocop.yml.
For more information, see https://docs.rubocop.org/rubocop/plugin_migration_guide.html.
Error: RSpec/FactoryBot/* has been extracted to the rubocop-factory_bot gem.
(obsolete configuration found in .rubocop.yml, please update it)

app/services/whatsapp/baileys_handlers/messages_upsert.rb

rubocop-performance extension supports plugin, specify plugins: rubocop-performance instead of require: rubocop-performance in /.rubocop.yml.
For more information, see https://docs.rubocop.org/rubocop/plugin_migration_guide.html.
rubocop-rails extension supports plugin, specify plugins: rubocop-rails instead of require: rubocop-rails in /.rubocop.yml.
For more information, see https://docs.rubocop.org/rubocop/plugin_migration_guide.html.
rubocop-rspec extension supports plugin, specify plugins: rubocop-rspec instead of require: rubocop-rspec in /.rubocop.yml.
For more information, see https://docs.rubocop.org/rubocop/plugin_migration_guide.html.
Error: RSpec/FactoryBot/* has been extracted to the rubocop-factory_bot gem.
(obsolete configuration found in .rubocop.yml, please update it)

  • 3 others

📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d922c2c and 5cbae02.

📒 Files selected for processing (7)
  • app/controllers/webhooks/whatsapp_controller.rb (0 hunks)
  • app/services/whatsapp/baileys_handlers/connection_update.rb (1 hunks)
  • app/services/whatsapp/baileys_handlers/helpers.rb (1 hunks)
  • app/services/whatsapp/baileys_handlers/messages_update.rb (1 hunks)
  • app/services/whatsapp/baileys_handlers/messages_upsert.rb (1 hunks)
  • app/services/whatsapp/incoming_message_baileys_service.rb (1 hunks)
  • spec/services/whatsapp/incoming_message_baileys_service_spec.rb (2 hunks)
✨ Finishing Touches
  • 📝 Generate Docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ 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.
    • Explain this complex logic.
    • 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 explain this code block.
    • @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 explain its main purpose.
    • @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.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

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 generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this 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.

Copy link
@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR refactors the IncomingMessageBaileysService to use composition by extracting connection, message upsert, and message update responsibilities into dedicated modules.

  • Introduces modules for connection updates, messages upsert, and messages update.
  • Moves common helper methods into a shared Helpers module.

Reviewed Changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
app/services/whatsapp/incoming_message_baileys_service.rb Removes duplicated logic and includes specialized handler modules.
app/services/whatsapp/baileys_handlers/messages_upsert.rb Extracts message-upsert logic with improved clarity and separation of concerns.
app/services/whatsapp/baileys_handlers/messages_update.rb Extracts message update logic into its own module.
app/services/whatsapp/baileys_handlers/helpers.rb Consolidates common helper methods used across Baileys handlers.
app/services/whatsapp/baileys_handlers/connection_update.rb New module to encapsulate connection update behavior.


def cache_message_source_id_in_redis
key = format(Redis::RedisKeys::MESSAGE_SOURCE_KEY, id: message_id)
::Redis::Alfred.setex(key, true)
Copy link
Preview
Copilot AI May 25, 2025

Choose a reason for hiding this comment

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

The call to setex appears to be missing a TTL parameter. Typically, setex requires a key, a TTL (in seconds), and a value; consider verifying this usage to ensure the key expires as expected.

Suggested change
::Redis::Alfred.setex(key, true)
ttl = 86400 # Set TTL to 24 hours (in seconds)
::Redis::Alfred.setex(key, ttl, true)

Copilot uses AI. Check for mistakes.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@copilot-pull-request-reviewer[bot] Ignore this, the default TTL is already set in lib/redis/alfred.rb

Code snippet:

def setex(key, value, expiry = 1.day)
  $alfred.with { |conn| conn.setex(key, expiry, value) }
end

def handle_attach_media
attachment_file = download_attachment_file
unless attachment_file
@message.save!
Copy link
Preview
Copilot AI May 25, 2025

Choose a reason for hiding this comment

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

Saving a message when the attachment file is missing (right before raising AttachmentNotFoundError) might lead to an inconsistent state; consider handling the error before persisting the incomplete message.

Copilot uses AI. Check for mistakes.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I decided to mark the message as unsupported and save it

Choose a reason for hiding this comment

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

Why assign + save! and not update!(is_unsupported: true)?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done

Copy link
@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: 4

🧹 Nitpick comments (6)
app/services/whatsapp/baileys_handlers/connection_update.rb (1)

1-21: Well-structured connection update handler with good separation of concerns.

The module cleanly handles connection state updates with appropriate use of I18n for error messages and defensive programming by preserving existing connection status when not provided in the data.

Consider adding error handling for edge cases where inbox or channel might be nil to prevent potential runtime errors.

Consider adding a guard clause:

 def process_connection_update
+  return unless inbox&.channel
   data = processed_params[:data]
app/services/whatsapp/baileys_handlers/messages_upsert.rb (2)

54-54: Remove duplicate protocol message check.

This check is redundant as message_type == 'protocol' is already checked in handle_message at line 21.

-def handle_create_message
-  return if message_type == 'protocol' ||
-            (message_type == 'reaction' && message_content.blank?)
+def handle_create_message
+  return if message_type == 'reaction' && message_content.blank?

41-41: Technical debt: Update source_id to complete JID.

The FIXME comment indicates that the source_id should be updated to use the complete JID format in the future. This might be important for proper message tracking and identification.

Would you like me to create an issue to track this technical debt item for future implementation?

app/services/whatsapp/incoming_message_baileys_service.rb (1)

15-15: TODO: Implement methods for all expected events.

The comment indicates that not all Baileys events are currently handled. Consider documenting which events are supported and which are pending implementation.

Would you like me to help create a comprehensive list of Baileys events that need to be implemented?

app/services/whatsapp/baileys_handlers/helpers.rb (2)

110-112: Address the TODO for Brazilian phone number handling.

The TODO comment indicates incomplete handling of denormalized Brazilian phone numbers, which could lead to incorrect self_message? detection for Brazilian users.

This could impact message processing logic. Would you like me to research the Brazilian phone number normalization requirements and generate a solution?


95-99: Complex phone number parsing may be fragile.

The phone number extraction logic handles multiple JID formats but could be brittle if the format changes. Consider adding validation or error handling.

  def phone_number_from_jid
    # NOTE: jid shape is `<user>_<agent>:<device>@<server>`
    # https://github.com/WhiskeySockets/Baileys/blob/v6.7.16/src/WABinary/jid-utils.ts#L19
-    @phone_number_from_jid ||= @raw_message[:key][:remoteJid].split('@').first.split(':').first.split('_').first
+    @phone_number_from_jid ||= begin
+      jid = @raw_message[:key][:remoteJid]
+      return nil unless jid.present?
+      
+      jid.split('@').first.split(':').first.split('_').first
+    rescue StandardError => e
+      Rails.logger.warn "Failed to parse phone number from JID: #{jid}, error: #{e.message}"
+      nil
+    end
  end
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 37c5d31 and fabb803.

📒 Files selected for processing (5)
  • app/services/whatsapp/baileys_handlers/connection_update.rb (1 hunks)
  • app/services/whatsapp/baileys_handlers/helpers.rb (1 hunks)
  • app/services/whatsapp/baileys_handlers/messages_update.rb (1 hunks)
  • app/services/whatsapp/baileys_handlers/messages_upsert.rb (1 hunks)
  • app/services/whatsapp/incoming_message_baileys_service.rb (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (3)
app/services/whatsapp/baileys_handlers/connection_update.rb (1)
app/models/channel/whatsapp.rb (1)
  • update_provider_connection! (67-71)
app/services/whatsapp/baileys_handlers/messages_upsert.rb (2)
app/services/whatsapp/baileys_handlers/helpers.rb (12)
  • jid_type (12-34)
  • message_id (4-6)
  • message_under_process? (114-117)
  • message_type (36-57)
  • cache_message_source_id_in_redis (119-122)
  • clear_message_source_id_from_redis (124-127)
  • contact_name (101-107)
  • phone_number_from_jid (95-99)
  • message_content (59-70)
  • incoming? (8-10)
  • file_content_type (72-78)
  • message_mimetype (80-93)
app/helpers/baileys_helper.rb (1)
  • extract_baileys_message_timestamp (2-12)
app/services/whatsapp/baileys_handlers/messages_update.rb (1)
app/services/whatsapp/baileys_handlers/helpers.rb (2)
  • message_id (4-6)
  • message_content (59-70)
🔇 Additional comments (6)
app/services/whatsapp/incoming_message_baileys_service.rb (1)

1-21: Excellent refactoring using the composition pattern!

The service class is now much cleaner and focused solely on webhook validation and event dispatching. The extraction of event handling logic into dedicated modules significantly improves maintainability and testability.

The dynamic event method dispatch (lines 12-19) is well-implemented with appropriate logging for unsupported events.

app/services/whatsapp/baileys_handlers/messages_update.rb (3)

27-51: LGTM: Comprehensive status mapping with proper logging.

The status mapping correctly handles all Baileys status codes according to the referenced documentation, with appropriate logging for unsupported statuses.


53-58: LGTM: Proper status transition validation.

The status transition logic correctly prevents status regression (e.g., from 'read' back to 'sent' or 'delivered') which maintains message status integrity.


16-16:

✅ Verification successful

Verify that find_message_by_source_id method is available.

The code calls find_message_by_source_id(message_id) but this method is not defined in the current module or the included Helpers module. This method must be defined in the class that includes this module.


🏁 Script executed:

#!/bin/bash
# Description: Search for the find_message_by_source_id method definition
# Expected: Find the method definition in the including service class

ast-grep --pattern 'def find_message_by_source_id($_)'

Length of output: 492


find_message_by_source_id is defined in Helpers

  • The method find_message_by_source_id is implemented in app/services/whatsapp/incoming_message_service_helpers.rb:94–98, so the call in messages_update.rb is valid and no missing-definition issue remains.
app/services/whatsapp/baileys_handlers/helpers.rb (2)

12-34: LGTM: Comprehensive JID type classification.

The JID type detection properly handles all WhatsApp entity types according to Baileys documentation with clear mapping logic.


119-127: LGTM: Proper Redis key management.

The Redis operations use consistent key formatting and provide both caching and cleanup methods for message source IDs.

Comment on lines 7 to 21
def process_messages_upsert
messages = processed_params[:data][:messages]
messages.each do |message|
@message = nil
@contact_inbox = nil
@contact = nil
@raw_message = message
handle_message
end
end
Copy link
@coderabbitai coderabbitai bot May 25, 2025

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider adding error handling for individual message failures.

The current implementation could fail the entire batch if one message encounters an error. This might cause valid messages to be lost.

Wrap individual message processing in error handling:

 def process_messages_upsert
   messages = processed_params[:data][:messages]
   messages.each do |message|
     @message = nil
     @contact_inbox = nil
     @contact = nil
     @raw_message = message
-    handle_message
+    begin
+      handle_message
+    rescue StandardError => e
+      Rails.logger.error "Failed to process message #{message[:key][:id]}: #{e.message}"
+      Rails.logger.error e.backtrace.join("\n")
+    end
   end
 end
📝 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 process_messages_upsert
messages = processed_params[:data][:messages]
messages.each do |message|
@message = nil
@contact_inbox = nil
@contact = nil
@raw_message = message
handle_message
end
end
def process_messages_upsert
messages = processed_params[:data][:messages]
messages.each do |message|
@message = nil
@contact_inbox = nil
@contact = nil
@raw_message = message
begin
handle_message
rescue StandardError => e
Rails.logger.error "Failed to process message #{message[:key][:id]}: #{e.message}"
Rails.logger.error e.backtrace.join("\n")
end
end
end
🤖 Prompt for AI Agents
In app/services/whatsapp/baileys_handlers/messages_upsert.rb around lines 7 to
16, the process_messages_upsert method processes multiple messages but lacks
error handling for individual messages, risking the failure of the entire batch
if one message errors. To fix this, wrap the processing of each message inside a
begin-rescue block to catch and log errors for that specific message, allowing
the loop to continue processing subsequent messages without interruption.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I want the error to propagate to the WhatsappController

Copy link

Choose a reason for hiding this comment

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

@CayoPOliveira, understood! You prefer to let errors propagate up to the WhatsappController level rather than handling them individually within the message processing loop. This approach allows for centralized error handling and ensures that any processing failures are handled consistently at the controller level.

With this approach, if any message in the batch fails, the entire operation will fail and the controller can decide how to respond (e.g., return an appropriate HTTP status code, log the error, etc.). This is a valid architectural choice for fail-fast error handling.


✏️ Learnings added
Learnt from: CayoPOliveira
PR: fazer-ai/chatwoot#53
File: app/services/whatsapp/baileys_handlers/messages_upsert.rb:7-16
Timestamp: 2025-05-26T20:14:33.170Z
Learning: In the Whatsapp Baileys handlers for this codebase, errors should propagate to the WhatsappController level rather than being caught and handled individually within message processing methods. The user prefers centralized error handling at the controller level over individual message-level error handling.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Comment on lines +104 to +111
def download_attachment_file
Down.download(@conversation.inbox.channel.media_url(@raw_message.dig(:key, :id)), headers: @conversation.inbox.channel.api_headers)
end
Copy link

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 attachment download failures.

The download operation could fail due to network issues, invalid URLs, or authentication problems, which would cause an unhandled exception.

 def download_attachment_file
-  Down.download(@conversation.inbox.channel.media_url(@raw_message.dig(:key, :id)), headers: @conversation.inbox.channel.api_headers)
+  begin
+    Down.download(@conversation.inbox.channel.media_url(@raw_message.dig(:key, :id)), headers: @conversation.inbox.channel.api_headers)
+  rescue Down::Error => e
+    Rails.logger.error "Failed to download attachment for message #{message_id}: #{e.message}"
+    nil
+  end
 end
📝 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 download_attachment_file
Down.download(@conversation.inbox.channel.media_url(@raw_message.dig(:key, :id)), headers: @conversation.inbox.channel.api_headers)
end
def download_attachment_file
begin
Down.download(
@conversation.inbox.channel.media_url(@raw_message.dig(:key, :id)),
headers: @conversation.inbox.channel.api_headers
)
rescue Down::Error => e
Rails.logger.error "Failed to download attachment for message #{message_id}: #{e.message}"
nil
end
end
🤖 Prompt for AI Agents
In app/services/whatsapp/baileys_handlers/messages_upsert.rb around lines 104 to
106, the download_attachment_file method lacks error handling for potential
failures during the download operation. Wrap the Down.download call in a
begin-rescue block to catch exceptions such as network errors, invalid URLs, or
authentication failures. Log the error or handle it appropriately to prevent
unhandled exceptions from crashing the application.

end

def handle_edited_content
@raw_message = @raw_message.dig(:update, :message, :editedMessage)
Copy link
@coderabbitai coderabbitai bot May 25, 2025

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Avoid reassigning instance variables mid-method.

Reassigning @raw_message to a nested value can be confusing and may break other method calls that depend on the original structure. Consider using a local variable instead.

-    @raw_message = @raw_message.dig(:update, :message, :editedMessage)
-    content = message_content
+    edited_message = @raw_message.dig(:update, :message, :editedMessage)
+    # Temporarily set @raw_message for message_content helper method
+    original_message = @raw_message
+    @raw_message = edited_message
+    content = message_content
+    @raw_message = original_message
📝 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
@raw_message = @raw_message.dig(:update, :message, :editedMessage)
edited_message = @raw_message.dig(:update, :message, :editedMessage)
# Temporarily set @raw_message for message_content helper method
original_message = @raw_message
@raw_message = edited_message
content = message_content
@raw_message = original_message
🤖 Prompt for AI Agents
In app/services/whatsapp/baileys_handlers/messages_update.rb at line 61, avoid
reassigning the instance variable @raw_message to a nested value as it may cause
confusion and break other method calls relying on the original data. Instead,
assign the nested value to a new local variable to preserve the original
@raw_message structure while still accessing the edited message data.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

There’s no problem doing this here; this method is executed at the end of the corresponding flow.

Copy link

Choose a reason for hiding this comment

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

@CayoPOliveira, thank you for the clarification! You're absolutely right - since handle_edited_content is executed at the end of the flow after update_status, reassigning @raw_message doesn't pose any risk of breaking subsequent method calls. I appreciate you providing this context about the execution order.

🐰💭 (Understanding the execution flow better...)


✏️ Learnings added
Learnt from: CayoPOliveira
PR: fazer-ai/chatwoot#53
File: app/services/whatsapp/baileys_handlers/messages_update.rb:61-61
Timestamp: 2025-05-26T20:14:34.792Z
Learning: In Whatsapp::BaileysHandlers::MessagesUpdate#handle_edited_content, reassigning @raw_message to the nested edited message data is acceptable because this method executes at the end of the corresponding flow, with no subsequent operations depending on the original @raw_message structure.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Choose a reason for hiding this comment

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

There’s no problem doing this here; this method is executed at the end of the corresponding flow.

What about when it isn't anymore? For example, if we start doing more stuff after handle_edited_content.

Original coderabbit suggestion should be followed.


Also, can we simplify to this? Editing should only be possible for text messages.

Suggestion:

  def handle_edited_content
    content = @raw_message.dig(:update, :message, :editedMessage, :conversation)

    @message.update!(content: content, is_edited: true, previous_content: @message.content)
  end

Copy link

Choose a reason for hiding this comment

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

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Choose a reason for hiding this comment

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

Previous suggestion was missing a hash key.

Suggestion:

  def handle_edited_content
    content = @raw_message.dig(:update, :message, :editedMessage, :message, :conversation)

    @message.update!(content: content, is_edited: true, previous_content: @message.content)
  end

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The text message could be conversation or extendedTextMessage, also its possible to edit image and video messages too

Comment on lines +59 to +72
def message_content
case message_type
when 'text'
@raw_message.dig(:message, :conversation) || @raw_message.dig(:message, :extendedTextMessage, :text)
when 'image'
@raw_message.dig(:message, :imageMessage, :caption)
when 'video'
@raw_message.dig(:message, :videoMessage, :caption)
when 'reaction'
@raw_message.dig(:message, :reactionMessage, :text)
end
end
Copy link
@coderabbitai coderabbitai bot May 25, 2025

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Incomplete message content extraction for all supported message types.

The message_content method only handles text, image, video, and reaction messages, but the message_type method supports additional types like audio, file, and sticker. This inconsistency could lead to nil content for supported message types.

  def message_content
    case message_type
    when 'text'
      @raw_message.dig(:message, :conversation) || @raw_message.dig(:message, :extendedTextMessage, :text)
    when 'image'
      @raw_message.dig(:message, :imageMessage, :caption)
    when 'video'
      @raw_message.dig(:message, :videoMessage, :caption)
+    when 'audio'
+      @raw_message.dig(:message, :audioMessage, :caption) || 'Audio message'
+    when 'file'
+      @raw_message.dig(:message, :documentMessage, :caption) || 'Document'
+    when 'sticker'
+      'Sticker'
    when 'reaction'
      @raw_message.dig(:message, :reactionMessage, :text)
+    when 'protocol'
+      'Protocol message'
+    else
+      'Unsupported message type'
    end
  end
📝 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 message_content
case message_type
when 'text'
@raw_message.dig(:message, :conversation) || @raw_message.dig(:message, :extendedTextMessage, :text)
when 'image'
@raw_message.dig(:message, :imageMessage, :caption)
when 'video'
@raw_message.dig(:message, :videoMessage, :caption)
when 'reaction'
@raw_message.dig(:message, :reactionMessage, :text)
end
end
def message_content
case message_type
when 'text'
@raw_message.dig(:message, :conversation) || @raw_message.dig(:message, :extendedTextMessage, :text)
when 'image'
@raw_message.dig(:message, :imageMessage, :caption)
when 'video'
@raw_message.dig(:message, :videoMessage, :caption)
when 'audio'
@raw_message.dig(:message, :audioMessage, :caption) || 'Audio message'
when 'file'
@raw_message.dig(:message, :documentMessage, :caption) || 'Document'
when 'sticker'
'Sticker'
when 'reaction'
@raw_message.dig(:message, :reactionMessage, :text)
when 'protocol'
'Protocol message'
else
'Unsupported message type'
end
end
🤖 Prompt for AI Agents
In app/services/whatsapp/baileys_handlers/helpers.rb around lines 59 to 70, the
message_content method only extracts content for text, image, video, and
reaction types, but does not handle other supported types like audio, file, and
sticker. Update the method to include extraction logic for these additional
message types by accessing the appropriate fields in @raw_message for audio,
file, and sticker messages to ensure content is correctly retrieved for all
supported types.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@coderabbitai[bot] you can ignore this types, they don't have content

Copy link

Choose a reason for hiding this comment

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

@CayoPOliveira Thank you for the clarification! You're absolutely right - those message types represent media or protocol content rather than extractable text content. The current implementation correctly handles only the message types that actually have text content to extract. I appreciate you sharing this domain knowledge about WhatsApp message structure.


✏️ Learnings added
Learnt from: CayoPOliveira
PR: fazer-ai/chatwoot#53
File: app/services/whatsapp/baileys_handlers/helpers.rb:59-70
Timestamp: 2025-05-26T20:14:33.133Z
Learning: In WhatsApp Baileys message handling, audio, file, sticker, protocol, and unsupported message types don't have extractable text content. Only text messages, image captions, video captions, and reaction text should be handled in message_content extraction methods.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@CayoPOliveira CayoPOliveira changed the title Cayo phellipe ramalho de oliveira/cu 86a8qe8w9/refatorar incomingmessagebaileysservice usando composition chore: refactor incoming_message_baileys_service using composition and fix some failure points May 26, 2025
Copy link
Collaborator Author
@CayoPOliveira CayoPOliveira left a comment

Choose a reason for hiding this comment

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

Reviewable status: 0 of 5 files reviewed, 2 unresolved discussions (waiting on @gabrieljablonski)

Comment on lines +59 to +72
def message_content
case message_type
when 'text'
@raw_message.dig(:message, :conversation) || @raw_message.dig(:message, :extendedTextMessage, :text)
when 'image'
@raw_message.dig(:message, :imageMessage, :caption)
when 'video'
@raw_message.dig(:message, :videoMessage, :caption)
when 'reaction'
@raw_message.dig(:message, :reactionMessage, :text)
end
end
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@coderabbitai[bot] you can ignore this types, they don't have content


def cache_message_source_id_in_redis
key = format(Redis::RedisKeys::MESSAGE_SOURCE_KEY, id: message_id)
::Redis::Alfred.setex(key, true)
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@copilot-pull-request-reviewer[bot] Ignore this, the default TTL is already set in lib/redis/alfred.rb

Code snippet:

def setex(key, value, expiry = 1.day)
  $alfred.with { |conn| conn.setex(key, expiry, value) }
end

end

def handle_edited_content
@raw_message = @raw_message.dig(:update, :message, :editedMessage)
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

There’s no problem doing this here; this method is executed at the end of the corresponding flow.

Comment on lines 7 to 21
def process_messages_upsert
messages = processed_params[:data][:messages]
messages.each do |message|
@message = nil
@contact_inbox = nil
@contact = nil
@raw_message = message
handle_message
end
end
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I want the error to propagate to the WhatsappController

def handle_attach_media
attachment_file = download_attachment_file
unless attachment_file
@message.save!
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I decided to mark the message as unsupported and save it

Copy link
@gabrieljablonski gabrieljablonski left a comment

Choose a reason for hiding this comment

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

looks mostly good, with a few suggestions.

-(review needed) +(questions/changes requested)

Reviewed 4 of 5 files at r1, 1 of 1 files at r2, all commit messages.
Dismissed @coderabbitai[bot] from a discussion.
Reviewable status: all files reviewed, 5 unresolved discussions (waiting on @CayoPOliveira)


app/services/whatsapp/baileys_handlers/messages_update.rb line 6 at r2 (raw file):

  class MessageNotFoundError < StandardError; end

  def process_messages_update

Suggestion:

module Whatsapp::BaileysHandlers::MessagesUpdate
  include Whatsapp::BaileysHandlers::Helpers

  class MessageNotFoundError < StandardError; end

  private

  def process_messages_update

app/services/whatsapp/baileys_handlers/helpers.rb line 4 at r2 (raw file):

  include Whatsapp::IncomingMessageServiceHelpers

  def message_id

Suggestion:

module Whatsapp::BaileysHandlers::Helpers # rubocop:disable Metrics/ModuleLength
  include Whatsapp::IncomingMessageServiceHelpers

  private

  def message_id

app/services/whatsapp/incoming_message_baileys_service.rb line 4 at r2 (raw file):

  include ::Whatsapp::BaileysHandlers::ConnectionUpdate
  include ::Whatsapp::BaileysHandlers::MessagesUpsert
  include ::Whatsapp::BaileysHandlers::MessagesUpdate

NIT

Leading :: are used to avoid namespace colisions. For instance, if Whatsapp::Whatsapp namespace existed, this would be needed.

Since that's not the case, can be simplified.

Suggestion:

  include Whatsapp::BaileysHandlers::ConnectionUpdate
  include Whatsapp::BaileysHandlers::MessagesUpsert
  include Whatsapp::BaileysHandlers::MessagesUpdate

app/services/whatsapp/baileys_handlers/connection_update.rb line 4 at r2 (raw file):

  include Whatsapp::BaileysHandlers::Helpers

  def process_connection_update

Method definitions should be private, as it was before applying composition.

Suggestion:

module Whatsapp::BaileysHandlers::ConnectionUpdate
  include Whatsapp::BaileysHandlers::Helpers

  private

  def process_connection_update

end

def handle_edited_content
@raw_message = @raw_message.dig(:update, :message, :editedMessage)

Choose a reason for hiding this comment

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

There’s no problem doing this here; this method is executed at the end of the corresponding flow.

What about when it isn't anymore? For example, if we start doing more stuff after handle_edited_content.

Original coderabbit suggestion should be followed.


Also, can we simplify to this? Editing should only be possible for text messages.

Suggestion:

  def handle_edited_content
    content = @raw_message.dig(:update, :message, :editedMessage, :conversation)

    @message.update!(content: content, is_edited: true, previous_content: @message.content)
  end

def handle_attach_media
attachment_file = download_attachment_file
unless attachment_file
@message.save!

Why assign + save! and not update!(is_unsupported: true)?

Copy link
@gabrieljablonski gabrieljablonski left a comment

Choose a reason for hiding this comment

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

Reviewable status: all files reviewed, 6 unresolved discussions (waiting on @CayoPOliveira)

end

def handle_edited_content
@raw_message = @raw_message.dig(:update, :message, :editedMessage)

Choose a reason for hiding this comment

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

Previous suggestion was missing a hash key.

Suggestion:

  def handle_edited_content
    content = @raw_message.dig(:update, :message, :editedMessage, :message, :conversation)

    @message.update!(content: content, is_edited: true, previous_content: @message.content)
  end

end

def handle_edited_content
@raw_message = @raw_message.dig(:update, :message, :editedMessage)
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The text message could be conversation or extendedTextMessage, also its possible to edit image and video messages too

Copy link
Collaborator Author
@CayoPOliveira CayoPOliveira left a comment

Choose a reason for hiding this comment

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

-(questions/changes requested) +(review needed)

Reviewed 2 of 5 files at r1.
Reviewable status: all files reviewed, 4 unresolved discussions (waiting on @gabrieljablonski)


app/services/whatsapp/baileys_handlers/connection_update.rb line 4 at r2 (raw file):

Previously, gabrieljablonski (Gabriel Jablonski) wrote…

Method definitions should be private, as it was before applying composition.

Done.


app/services/whatsapp/baileys_handlers/messages_update.rb line 6 at r2 (raw file):

  class MessageNotFoundError < StandardError; end

  def process_messages_update

Done.


app/services/whatsapp/baileys_handlers/helpers.rb line 4 at r2 (raw file):

  include Whatsapp::IncomingMessageServiceHelpers

  def message_id

Done.

def handle_attach_media
attachment_file = download_attachment_file
unless attachment_file
@message.save!
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done

Copy link
@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: 2

🔭 Outside diff range comments (1)
app/services/whatsapp/incoming_message_baileys_service.rb (1)

15-16: 💡 Verification agent

🧩 Analysis chain

Address the TODO comment or remove if no longer relevant.

The TODO comment suggests that not all expected event methods have been implemented. Since this refactoring moves event handling to dedicated modules, please verify:

  1. Are there any missing event handlers that still need to be implemented?
  2. Are all expected events now covered by the included modules?

If all events are properly handled by the modules, this TODO can be removed.


🏁 Script executed:

#!/bin/bash
# Description: Check what event types are mentioned in tests and logs to verify coverage
# Expected: All event types should have corresponding handler methods

# Search for event types in the codebase
rg -A 2 -B 2 "event.*:" --type rb

# Search for process_ methods that might indicate expected events
rg "process_.*event" --type rb

# Search for unsupported event log messages to see what events are encountered
rg "unsupported event" --type rb

Length of output: 184


Let’s first locate the IncomingMessageBaileysService file and inspect its includes to confirm which modules are handling events:


🏁 Script executed:

#!/bin/bash
# Locate the service file
service_file=$(rg -l 'class IncomingMessageBaileysService' -g '*.rb')
echo "Service file: $service_file"

# Show the first 50 lines to see included modules and comments/TODO
sed -n '1,50p' "$service_file"

Length of output: 173


Let’s first locate and inspect the service file to see which modules it includes (and whether the TODO is still needed):


🏁 Script executed:

#!/bin/bash
# Find the service file
filepath=$(find . -type f -name 'incoming_message_baileys_service.rb')
echo "Service file path: $filepath"

# Print the first 100 lines to show includes and the TODO
if [ -n "$filepath" ]; then
  sed -n '1,100p' "$filepath"
else
  echo "Error: incoming_message_baileys_service.rb not found"
fi

Length of output: 1312


🏁 Script executed:

#!/bin/bash
# List and display the handler modules to see which events they cover
handler_dir="app/services/whatsapp/baileys_handlers"
echo "Handler files found:"
ls "$handler_dir"/*.rb

echo
for file in "$handler_dir"/*.rb; do
  echo "=== $file ==="
  sed -n '1,200p' "$file"
  echo
done

Length of output: 13098


Remove the stale TODO in IncomingMessageBaileysService

The process_* handlers for the only supported Baileys events—

  • connection.update (ConnectionUpdate)
  • messages.upsert (MessagesUpsert)
  • messages.update (MessagesUpdate)

—are already implemented in app/services/whatsapp/baileys_handlers. Any other events will simply be logged as unsupported. You can safely delete the

# TODO: Implement the methods for all expected events

comment (and its surrounding comment block) in
app/services/whatsapp/incoming_message_baileys_service.rb (around line 15), and rely on the fallback log for unhandled events. Add new process_* methods only when you onboard additional Baileys event types.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9920b4e and 941c19e.

📒 Files selected for processing (6)
  • app/services/whatsapp/baileys_handlers/connection_update.rb (1 hunks)
  • app/services/whatsapp/baileys_handlers/helpers.rb (1 hunks)
  • app/services/whatsapp/baileys_handlers/messages_update.rb (1 hunks)
  • app/services/whatsapp/baileys_handlers/messages_upsert.rb (1 hunks)
  • app/services/whatsapp/incoming_message_baileys_service.rb (1 hunks)
  • spec/services/whatsapp/incoming_message_baileys_service_spec.rb (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • app/services/whatsapp/baileys_handlers/connection_update.rb
  • app/services/whatsapp/baileys_handlers/messages_update.rb
  • app/services/whatsapp/baileys_handlers/messages_upsert.rb
  • app/services/whatsapp/baileys_handlers/helpers.rb
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: CayoPOliveira
PR: fazer-ai/chatwoot#53
File: app/services/whatsapp/baileys_handlers/messages_upsert.rb:7-16
Timestamp: 2025-05-26T20:14:33.170Z
Learning: In the Whatsapp Baileys handlers for this codebase, errors should propagate to the WhatsappController level rather than being caught and handled individually within message processing methods. The user prefers centralized error handling at the controller level over individual message-level error handling.
app/services/whatsapp/incoming_message_baileys_service.rb (2)
Learnt from: CayoPOliveira
PR: fazer-ai/chatwoot#53
File: app/services/whatsapp/baileys_handlers/messages_upsert.rb:7-16
Timestamp: 2025-05-26T20:14:33.170Z
Learning: In the Whatsapp Baileys handlers for this codebase, errors should propagate to the WhatsappController level rather than being caught and handled individually within message processing methods. The user prefers centralized error handling at the controller level over individual message-level error handling.
Learnt from: CayoPOliveira
PR: fazer-ai/chatwoot#53
File: app/services/whatsapp/baileys_handlers/messages_update.rb:61-61
Timestamp: 2025-05-26T20:14:34.792Z
Learning: In Whatsapp::BaileysHandlers::MessagesUpdate#handle_edited_content, reassigning @raw_message to the nested edited message data is acceptable because this method executes at the end of the corresponding flow, with no subsequent operations depending on the original @raw_message structure.
🔇 Additional comments (1)
app/services/whatsapp/incoming_message_baileys_service.rb (1)

1-4: Excellent refactoring using composition pattern.

The refactoring successfully extracts event-specific logic into dedicated handler modules, following the single responsibility principle and improving maintainability. The composition approach is much cleaner than having all logic in a single large service class.

Comment on lines 427 to 437
it 'throws an error if attachment download fails' do
allow(Down).to receive(:download).with('https://baileys.api/media/msg_123', headers: inbox.channel.api_headers)
.and_raise(Down::ResponseError.new('Attachment not found'))

expect do
described_class.new(inbox: inbox, params: params).perform
end.to raise_error(Whatsapp::IncomingMessageBaileysService::AttachmentNotFoundError)

message = inbox.conversations.last.messages.last
expect(message).to be_present
expect(message.is_unsupported).to be(true)
end
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Verify error class accessibility after refactoring.

The new test case for attachment download failures looks well-structured and validates important error handling behavior. However, there's a potential issue with the error class reference.

The test expects Whatsapp::IncomingMessageBaileysService::AttachmentNotFoundError to be raised, but according to the AI summary, this error class has been moved to the MessagesUpsert handler module. This could cause the test to fail since the error class may no longer be accessible through the main service class namespace.


🏁 Script executed:

#!/bin/bash
# Description: Verify that AttachmentNotFoundError is accessible through the main service class
# Expected: The error class should be accessible either directly or through included modules

# Search for AttachmentNotFoundError definitions
ast-grep --pattern 'class AttachmentNotFoundError < $_'

# Search for error class references in the codebase
rg -A 3 'AttachmentNotFoundError'

Length of output: 1587


Correct error class namespace in spec

The test currently expects

raise_error(Whatsapp::IncomingMessageBaileysService::AttachmentNotFoundError)

but the exception is defined only in the handler module as
Whatsapp::BaileysHandlers::MessagesUpsert::AttachmentNotFoundError. Update the spec (and any rescue clauses in the controller) to reference the correct constant. For example:

- expect {
-   described_class.new(inbox: inbox, params: params).perform
- }.to raise_error(Whatsapp::IncomingMessageBaileysService::AttachmentNotFoundError)
+ expect {
+   described_class.new(inbox: inbox, params: params).perform
+ }.to raise_error(Whatsapp::BaileysHandlers::MessagesUpsert::AttachmentNotFoundError)

Alternatively, if you’d rather keep the service‐scoped constant, add an alias in the service class:

# app/services/whatsapp/incoming_message_baileys_service.rb
class Whatsapp::IncomingMessageBaileysService
  AttachmentNotFoundError = Whatsapp::BaileysHandlers::MessagesUpsert::AttachmentNotFoundError
  
end
🤖 Prompt for AI Agents
In spec/services/whatsapp/incoming_message_baileys_service_spec.rb around lines
427 to 438, the test expects the error AttachmentNotFoundError to be raised from
Whatsapp::IncomingMessageBaileysService, but this error class has been moved to
Whatsapp::BaileysHandlers::MessagesUpsert. Update the test to expect the error
from the correct namespace
Whatsapp::BaileysHandlers::MessagesUpsert::AttachmentNotFoundError, or
alternatively, add an alias for AttachmentNotFoundError inside the
IncomingMessageBaileysService class pointing to the new location to keep the
existing test unchanged.

class Whatsapp::IncomingMessageBaileysService < Whatsapp::IncomingMessageBaseService
include Whatsapp::BaileysHandlers::ConnectionUpdate
include Whatsapp::BaileysHandlers::MessagesUpsert
include Whatsapp::BaileysHandlers::MessagesUpdate

class InvalidWebhookVerifyToken < StandardError; end
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider exposing handler module error classes.

With the refactoring, custom error classes like AttachmentNotFoundError and MessageNotFoundError have been moved to their respective handler modules. However, existing tests (and potentially other code) expect these errors to be accessible through the main service class namespace.

Consider adding class aliases or delegating these error classes to maintain backward compatibility:

class Whatsapp::IncomingMessageBaileysService < Whatsapp::IncomingMessageBaseService
  include Whatsapp::BaileysHandlers::ConnectionUpdate
  include Whatsapp::BaileysHandlers::MessagesUpsert
  include Whatsapp::BaileysHandlers::MessagesUpdate

  class InvalidWebhookVerifyToken < StandardError; end
+ 
+  # Expose handler module error classes for backward compatibility
+  AttachmentNotFoundError = Whatsapp::BaileysHandlers::MessagesUpsert::AttachmentNotFoundError
+  MessageNotFoundError = Whatsapp::BaileysHandlers::MessagesUpdate::MessageNotFoundError
📝 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
class InvalidWebhookVerifyToken < StandardError; end
class Whatsapp::IncomingMessageBaileysService < Whatsapp::IncomingMessageBaseService
include Whatsapp::BaileysHandlers::ConnectionUpdate
include Whatsapp::BaileysHandlers::MessagesUpsert
include Whatsapp::BaileysHandlers::MessagesUpdate
class InvalidWebhookVerifyToken < StandardError; end
# Expose handler module error classes for backward compatibility
AttachmentNotFoundError = Whatsapp::BaileysHandlers::MessagesUpsert::AttachmentNotFoundError
MessageNotFoundError = Whatsapp::BaileysHandlers::MessagesUpdate::MessageNotFoundError
🤖 Prompt for AI Agents
In app/services/whatsapp/incoming_message_baileys_service.rb at line 6, the
custom error classes like AttachmentNotFoundError and MessageNotFoundError have
been moved to handler modules, but existing tests and code expect these errors
to be accessible via the main service class. To fix this, add class aliases or
delegate these error classes in the main service class to point to their new
locations in the handler modules, ensuring backward compatibility without
breaking existing references.

Copy link
@gabrieljablonski gabrieljablonski left a comment

Choose a reason for hiding this comment

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

-(review needed) +(approved for merge)

Reviewed 6 of 6 files at r3, 4 of 4 files at r4, all commit messages.
Dismissed @coderabbitai[bot] from 2 discussions.
Reviewable status: :shipit: complete! all files reviewed, all discussions resolved (waiting on @CayoPOliveira)

Copy link
@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: 1

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 941c19e and 3a3188a.

📒 Files selected for processing (4)
  • app/controllers/webhooks/whatsapp_controller.rb (0 hunks)
  • app/services/whatsapp/baileys_handlers/messages_update.rb (1 hunks)
  • app/services/whatsapp/baileys_handlers/messages_upsert.rb (1 hunks)
  • spec/services/whatsapp/incoming_message_baileys_service_spec.rb (1 hunks)
💤 Files with no reviewable changes (1)
  • app/controllers/webhooks/whatsapp_controller.rb
🚧 Files skipped from review as they are similar to previous changes (2)
  • spec/services/whatsapp/incoming_message_baileys_service_spec.rb
  • app/services/whatsapp/baileys_handlers/messages_update.rb
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: CayoPOliveira
PR: fazer-ai/chatwoot#53
File: app/services/whatsapp/baileys_handlers/messages_upsert.rb:7-16
Timestamp: 2025-05-26T20:14:33.177Z
Learning: In the Whatsapp Baileys handlers for this codebase, errors should propagate to the WhatsappController level rather than being caught and handled individually within message processing methods. The user prefers centralized error handling at the controller level over individual message-level error handling.
app/services/whatsapp/baileys_handlers/messages_upsert.rb (1)
Learnt from: CayoPOliveira
PR: fazer-ai/chatwoot#53
File: app/services/whatsapp/baileys_handlers/messages_upsert.rb:7-16
Timestamp: 2025-05-26T20:14:33.177Z
Learning: In the Whatsapp Baileys handlers for this codebase, errors should propagate to the WhatsappController level rather than being caught and handled individually within message processing methods. The user prefers centralized error handling at the controller level over individual message-level error handling.
🧬 Code Graph Analysis (1)
app/services/whatsapp/baileys_handlers/messages_upsert.rb (2)
app/services/whatsapp/baileys_handlers/helpers.rb (12)
  • jid_type (14-36)
  • message_id (6-8)
  • message_under_process? (116-119)
  • message_type (38-59)
  • cache_message_source_id_in_redis (121-124)
  • clear_message_source_id_from_redis (126-129)
  • contact_name (103-109)
  • phone_number_from_jid (97-101)
  • message_content (61-72)
  • incoming? (10-12)
  • file_content_type (74-80)
  • message_mimetype (82-95)
app/helpers/baileys_helper.rb (1)
  • extract_baileys_message_timestamp (2-12)
🔇 Additional comments (4)
app/services/whatsapp/baileys_handlers/messages_upsert.rb (4)

7-16: LGTM: Error handling aligns with established preferences

The method correctly allows errors to propagate up to the controller level, which aligns with the established preference for centralized error handling. The instance variable resets ensure clean state for each message.


18-36: Well-structured message processing flow with proper cleanup

The method has a clear flow with appropriate early returns and proper Redis cache management. The cleanup of Redis cache when contact is not found (line 27) prevents memory leaks and ensures processing state consistency.


89-102: Appropriate error handling for recoverable attachment failures

The error handling for Down::Error is well-implemented and aligns with the business logic of marking messages as unsupported when attachments can't be downloaded. This is a specific, recoverable error case that doesn't warrant failing the entire operation.


108-114: Robust filename generation with fallback logic

The method properly handles cases where the document filename is missing and generates a meaningful fallback using message metadata. The MIME type parsing is defensive with proper fallbacks.

account_id: @inbox.account_id,
inbox_id: @inbox.id,
source_id: message_id,
sender: incoming? ? @contact : @inbox.account.account_users.first.user,
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Potential nil reference risk with account users

The code assumes at least one account user exists when accessing @inbox.account.account_users.first.user. This could raise a NoMethodError if the account has no users.

Consider adding a guard clause or default fallback:

- sender: incoming? ? @contact : @inbox.account.account_users.first.user,
+ sender: incoming? ? @contact : @inbox.account.account_users.first&.user || @inbox.account.users.first,

🏁 Script executed:

#!/bin/bash
# Check if there are any safeguards or validations ensuring accounts have users
ast-grep --pattern 'account_users.first'

Length of output: 40


🏁 Script executed:

#!/bin/bash
# Locate all references to `account_users` in the repo
rg 'account_users' -n .

# Find the Account model and its user associations
rg 'class Account' -n app/models
rg 'has_many.*account_users' -n app/models
rg 'has_many.*users.*through' -n app/models

Length of output: 13627


🏁 Script executed:

#!/bin/bash
# Inspect the assignment and usage of @inbox and any guards around account_users in this service
rg -n '@inbox' -n app/services/whatsapp/baileys_handlers/messages_upsert.rb
sed -n '1,200p' app/services/whatsapp/baileys_handlers/messages_upsert.rb

Length of output: 3928


🏁 Script executed:

#!/bin/bash
# Inspect Account model for validations or callbacks related to account_users
sed -n '1,200p' app/models/account.rb
rg -n 'validate' -n app/models/account.rb
rg -n 'after_create' -n app/models/account.rb
rg -n 'before_save' -n app/models/account.rb

Length of output: 7550


🏁 Script executed:

#!/bin/bash
# Inspect the AccountsController for default account_user creation on account signup
rg -n 'class AccountsController' -n app/controllers/api/v1/accounts_controller.rb
rg -n 'def create' -n app/controllers/api/v1/accounts_controller.rb
sed -n '1,200p' app/controllers/api/v1/accounts_controller.rb

Length of output: 4116


Guard against empty account_users
The current implementation in app/services/whatsapp/baileys_handlers/messages_upsert.rb:66 assumes there’s always at least one user on the account:

sender: incoming? ? @contact : @inbox.account.account_users.first.user,

If account_users is empty, this will blow up with NoMethodError. Since nothing in the model or controller prevents an account from ending up without any users (they can all be destroyed via the API), we need to guard here.

Suggestions:

• At the top of create_message (before @conversation.messages.build), do:

# app/services/whatsapp/baileys_handlers/messages_upsert.rb

def create_message(attach_media: false)
  # Guard missing account_user
  account_user = @inbox.account.account_users.first&.user
  unless account_user
    Rails.logger.error("No account_user found for Account #{@inbox.account.id}, skipping outgoing message")
    return
  end

  @message = @conversation.messages.build(
    content: message_content,
    account_id: @inbox.account_id,
    inbox_id: @inbox.id,
    source_id: message_id,
    sender: incoming? ? @contact : account_user,
    sender_type: incoming? ? 'Contact' : 'User',
    message_type: incoming? ? :incoming : :outgoing,
    content_attributes: message_content_attributes
  )
  
end

• At minimum, switch to safe navigation to avoid the crash:

- sender: incoming? ? @contact : @inbox.account.account_users.first.user,
+ sender: incoming? ? @contact : @inbox.account.account_users.first&.user,

But the explicit guard and early return is preferred so you can log and handle the missing‐user case cleanly.

🤖 Prompt for AI Agents
In app/services/whatsapp/baileys_handlers/messages_upsert.rb at line 66, the
code assumes @inbox.account.account_users.first.user always exists, risking a
NoMethodError if account_users is empty. To fix this, add a guard clause at the
start of the create_message method to safely fetch the first account_user's user
with safe navigation, log an error if none is found, and return early to skip
message creation. Then use this guarded user variable in place of the direct
call in the sender assignment to prevent crashes and handle missing users
gracefully.

Copy link
@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: 1

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3a3188a and f6d1084.

📒 Files selected for processing (2)
  • app/services/whatsapp/baileys_handlers/messages_update.rb (1 hunks)
  • spec/services/whatsapp/incoming_message_baileys_service_spec.rb (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • spec/services/whatsapp/incoming_message_baileys_service_spec.rb
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: CayoPOliveira
PR: fazer-ai/chatwoot#53
File: app/services/whatsapp/baileys_handlers/messages_upsert.rb:7-16
Timestamp: 2025-05-26T20:14:33.177Z
Learning: In the Whatsapp Baileys handlers for this codebase, errors should propagate to the WhatsappController level rather than being caught and handled individually within message processing methods. The user prefers centralized error handling at the controller level over individual message-level error handling.
app/services/whatsapp/baileys_handlers/messages_update.rb (1)
Learnt from: CayoPOliveira
PR: fazer-ai/chatwoot#53
File: app/services/whatsapp/baileys_handlers/messages_update.rb:61-61
Timestamp: 2025-05-26T20:14:34.814Z
Learning: In Whatsapp::BaileysHandlers::MessagesUpdate#handle_edited_content, reassigning @raw_message to the nested edited message data is acceptable because this method executes at the end of the corresponding flow, with no subsequent operations depending on the original @raw_message structure.
🔇 Additional comments (6)
app/services/whatsapp/baileys_handlers/messages_update.rb (6)

1-5: LGTM! Good module structure and error handling setup.

The module follows clear naming conventions and includes the necessary helpers. The custom MessageNotFoundError provides specific error handling for message lookup failures.


8-15: LGTM! Clean iteration logic with proper variable reset.

The method properly resets instance variables for each update, ensuring clean state between iterations.


30-54: Excellent status mapping implementation!

The comprehensive documentation and mapping between Baileys and Chatwoot status codes is very helpful. The logging for unsupported statuses aids in monitoring and debugging.


56-62: LGTM! Appropriate use of update_columns for performance.

The use of update_columns is correctly flagged by rubocop and disabled with good reason - these timestamp updates don't need full model validations and callbacks.


64-69: LGTM! Sound business logic for status transitions.

The status transition rules correctly prevent regression (read → other statuses, delivered → sent) which maintains data integrity.


71-78: Good error handling for missing content.

The method appropriately handles cases where edited content might be missing and logs a warning for debugging purposes.

Note: The @raw_message reassignment on line 72 has been previously discussed extensively in past review comments. Based on the retrieved learning, this reassignment is acceptable since this method executes at the end of the flow.

Comment on lines 17 to 27
def handle_update
raise MessageNotFoundError unless find_message_by_source_id(message_id)

update_status if @raw_message.dig(:update, :status).present?
handle_edited_content if @raw_message.dig(:update, :message).present?
end
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Enhance error message with context for debugging.

The error handling logic is sound, but the raised exception lacks context about which message ID failed, making debugging more difficult.

-    raise MessageNotFoundError unless find_message_by_source_id(message_id)
+    raise MessageNotFoundError, "Message not found with ID: #{message_id}" unless find_message_by_source_id(message_id)
📝 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 handle_update
raise MessageNotFoundError unless find_message_by_source_id(message_id)
update_status if @raw_message.dig(:update, :status).present?
handle_edited_content if @raw_message.dig(:update, :message).present?
end
def handle_update
raise MessageNotFoundError, "Message not found with ID: #{message_id}" unless find_message_by_source_id(message_id)
update_status if @raw_message.dig(:update, :status).present?
handle_edited_content if @raw_message.dig(:update, :message).present?
end
🤖 Prompt for AI Agents
In app/services/whatsa
F438
pp/baileys_handlers/messages_update.rb around lines 17 to
22, the raised MessageNotFoundError does not include the message ID that caused
the failure. Modify the raise statement to include the message_id in the error
message or as part of the exception to provide context for debugging which
specific message was not found.

Copy link
Collaborator Author
@CayoPOliveira CayoPOliveira left a comment

Choose a reason for hiding this comment

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

Reviewed 4 of 6 files at r3, 1 of 4 files at r4, 2 of 2 files at r5, all commit messages.
Reviewable status: 6 of 7 files reviewed, 2 unresolved discussions (waiting on @CayoPOliveira)

Copy link
@gabrieljablonski gabrieljablonski left a comment

Choose a reason for hiding this comment

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

Reviewed 1 of 2 files at r5, 1 of 1 files at r6, all commit messages.
Dismissed @coderabbitai[bot] from 2 discussions.
Reviewable status: :shipit: complete! all files reviewed, all discussions resolved (waiting on @CayoPOliveira)

@gabrieljablonski gabrieljablonski force-pushed the Cayo-Phellipe-Ramalho-de-Oliveira/CU-86a8qe8w9/Refatorar-incomingmessagebaileysservice-usando-composition branch from d922c2c to 5cbae02 Compare May 29, 2025 13:54
Copy link
@gabrieljablonski gabrieljablonski left a comment

Choose a reason for hiding this comment

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

Reviewed 4 of 4 files at r7, all commit messages.
Reviewable status: :shipit: complete! all files reviewed, all discussions resolved (waiting on @CayoPOliveira)

@CayoPOliveira CayoPOliveira merged commit 76deea9 into main May 29, 2025
1 check was pending
@CayoPOliveira CayoPOliveira deleted the Cayo-Phellipe-Ramalho-de-Oliveira/CU-86a8qe8w9/Refatorar-incomingmessagebaileysservice-usando-composition branch May 29, 2025 13:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
approved for merge enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants
0