mirror of
https://github.com/chatwoot/chatwoot.git
synced 2026-06-04 21:02:35 +08:00
develop
523 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
8e42307bdc
|
fix: improve email inbox IMAP and SMTP compatibility (#14589)
Fetch IMAP message content using `BODY.PEEK[]` instead of `RFC822` to avoid provider-specific parser failures while preserving unread state. This also applies the existing SMTP timeout configuration to custom SMTP email-channel replies, so provider SMTP responses have enough time to complete. Fixes: https://github.com/chatwoot/chatwoot/issues/12762 ## Why Some IMAP providers can return responses for `FETCH RFC822` that Ruby `net-imap` fails to parse with: `Net::IMAP::ResponseParseError: unexpected RPAR (expected ATOM or NIL)` We reproduced this with iCloud IMAP. Authentication, `INBOX` selection, and header fetches worked, but fetching full message content with `RFC822` failed before Chatwoot received a `Mail::Message`. The same mailbox successfully returned full message content when fetched with `BODY.PEEK[]`. > During end-to-end iCloud validation, inbound fetch worked after the IMAP change, but outbound replies through the custom SMTP settings could still fail with a socket read timeout. The OAuth SMTP path already used explicit SMTP timeout values; the custom SMTP path was relying on mailer defaults instead. ## What this change does - Replaces the full message fetch from `RFC822` to `BODY.PEEK[]` - Reads the returned message content from `BODY[]`, which is how `net-imap` exposes the response attribute - Keeps the existing `BODY.PEEK[HEADER]` header-fetch behavior unchanged - Applies `SMTP_OPEN_TIMEOUT` and `SMTP_READ_TIMEOUT` to custom SMTP email-channel replies - Defaults custom SMTP reply delivery to `open_timeout: 15` and `read_timeout: 30` - Updates IMAP service specs for standard and Microsoft IMAP fetch flows - Updates mailer specs for custom SMTP timeout settings `BODY.PEEK[]` is preferable here because it fetches the full message content without marking messages as read. ## Validation - Configured a local email inbox against iCloud IMAP and SMTP - Confirmed `FETCH RFC822` reproduces `Net::IMAP::ResponseParseError: unexpected RPAR (expected ATOM or NIL)` - Confirmed `BODY[]` and `BODY.PEEK[]` fetch the same mailbox successfully - Confirmed Chatwoot imports iCloud messages after the IMAP change - Sent two outbound replies from the Chatwoot UI through iCloud SMTP after applying the timeout settings - Confirmed both UI-created outbound messages were marked `sent`, had iCloud SMTP `source_id` values, and had no `external_error` - Ran `bundle exec rspec spec/services/imap/fetch_email_service_spec.rb spec/services/imap/microsoft_fetch_email_service_spec.rb` - Ran `bundle exec rspec spec/mailers/conversation_reply_mailer_spec.rb` |
||
|
|
7acbe8b3ff
|
fix(whatsapp): truncate location fallback_title to 255 chars to avoid silent message drop (#14517)
## Summary
`Whatsapp::IncomingMessageBaseService#attach_location` builds a
`fallback_title` by concatenating `location['name']` and
`location['address']` with no length cap, then stores it directly into
`Attachment#fallback_title`. `ApplicationRecord` enforces a generic
255-character limit on string columns, so any WhatsApp location whose
`"#{name}, #{address}"` exceeds 255 chars (a common case for Google
Places that include a long full address) raises
`ActiveRecord::RecordInvalid` deep inside the Sidekiq job. The message
and attachment INSERTs are part of the same transaction, so the whole
thing rolls back. Sidekiq retries once; the retry dedup-skips the wamid
silently and exits without an error. **Result: the message is
irrecoverably lost — no row in `messages`, no entry in the UI, no
outgoing webhook, no clue for the operator.**
Confirmed in `v4.13.0`, `v4.14.0`, and `develop` (commit `f33e469`,
2026-05-20). No upstream issue found before opening this PR.
## How to reproduce
1. From WhatsApp, share a Google Place whose `name + ", " + address` is
> 255 chars. The Spanish business address `Gremi de Fusters, 33,
Edificio VIP Asima, Piso 2, Local 2, Norte, 07009 Polígon industrial de
Son Castelló, Illes Balears, España` (132 chars) used as both `name` and
`address` is enough.
2. Sidekiq logs:
```
ERROR ActiveRecord::RecordInvalid: Validation failed:
Attachments fallback title is too long (maximum is 255 characters)
```
3. The `messages` table has no row. The conversation UI shows nothing
for that timestamp.
4. The first retry "Performed" successfully but creates nothing — the
dedup-by-source-id silently swallows the failure.
## Fix
Cap the existing concatenated title at 255 chars via `.first(255)`.
Minimal change, no behavioural difference for any message shorter than
the limit, prevents the silent data loss for any longer ones.
```diff
- location_name = location['name'] ? "#{location['name']}, #{location['address']}" : ''
+ location_name = (location['name'] ? "#{location['name']}, #{location['address']}" : '').first(255)
```
## Alternatives considered
- **Increase the validation limit on `Attachment#fallback_title`**: more
invasive; would touch other inbound channels and possibly require a DB
column change.
- **Use `name` alone (no concat)**: cleaner semantically (in many real
payloads `name == address`), but changes user-visible behaviour. Left as
a follow-up if desired.
- **Truncate with ellipsis**: cosmetic only; deferred.
This PR is intentionally minimal so it can be merged on its own.
---------
Co-authored-by: Sony Mathew <sony@chatwoot.com>
Co-authored-by: Sony Mathew <2040199+sony-mathew@users.noreply.github.com>
|
||
|
|
36a05097fa
|
fix(webhooks): strip trailing newlines from webhook message content (#14272)
The TipTap/ProseMirror editor stores agent messages with trailing paragraph nodes that produce trailing newlines (e.g. \`\n\n\n\`) in the \`content\` field. While Chatwoot's native channel delivery already handles this, webhook payloads and API responses were returning raw content with trailing whitespace — causing visible blank space below messages in every external integration that consumes Chatwoot webhooks (WhatsApp via Evolution API, Telegram bots, custom webhook consumers). Closes #13459 ## Root cause \`Messages::WebhookContentNormalizer\` already strips CommonMark hard line breaks (\`\\\` + newline) for webhook consumers, but it did not strip trailing whitespace. All webhook and API responses flow through this normaliser, so it is the single correct place to apply the fix without touching stored data. ## What changed Added \`.rstrip\` to \`Messages::WebhookContentNormalizer.normalize\`: \`\`\`ruby # before text.gsub(/\\\r?\n/, "\n") # after text.gsub(/\\\r?\n/, "\n").rstrip \`\`\` ## Trade-offs considered | Option | Decision | |---|---| | \`before_save\` on \`Message\` model | Would clean stored data but is a broader change affecting all message creation paths and would require a data migration for existing records. Out of scope for this bug. | | Trim in each channel's send path | DRY violation — many channels, each would need the same patch. | | Fix at normaliser level (chosen) | Single location, only affects webhook/API output, zero risk to stored data or native channel delivery. | **Known limitation:** existing messages in the database still have trailing newlines in storage. They will be delivered correctly through webhooks after this fix, but a follow-up migration could clean stored content if needed. ## How to reproduce 1. Send an agent reply from the Chatwoot UI 2. Inspect the \`content\` field of the outgoing \`message_created\` webhook payload 3. Observe trailing \`\n\n\n\` after the message text After this fix, the \`content\` field is trimmed before delivery. --------- Co-authored-by: Ramalau Debeila <rdebeila@datacentrix.co.za> Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> |
||
|
|
87df43bdd0
|
revert: restore conversation unread count feature flag (#14623)
This reverts #14610 so conversation unread counts are again controlled by the `conversation_unread_counts` feature flag across the API, ActionCable broadcasts, notifier/listener paths, and dashboard sidebar fetching. ## Closes - None ## What changed - Restores feature-flag checks for conversation unread count reads and broadcasts. - Restores the dashboard feature flag constant and sidebar/store behavior for disabled unread counts. - Restores the specs that cover disabled-feature behavior. ## How to test - In an account with `conversation_unread_counts` enabled, verify sidebar unread counts are fetched and updated in real time. - Disable `conversation_unread_counts` for the account and verify unread count requests/broadcasts are skipped. |
||
|
|
37eed5de1e
|
feat(whatsapp): Add support for voice messages (#14606)
> Reopened from #13613, now from a personal fork (`gabrieljablonski/chatwoot`) so maintainers can push edits — organization-owned forks don't support "Allow edits from maintainers". The previous PR is closed in favor of this one; same commits, same diff. ## Description This PR adds support for sending voice messages (voice notes) through the WhatsApp Cloud API. When agents record audio in Chatwoot, it is now transcoded in the browser from WebM/Opus to OGG/Opus and sent with the `voice: true` flag, so it appears as a native voice note bubble on WhatsApp — not as a file/document attachment. Closes #13283 **Key Changes:** - Added `webmOpusToOgg.js` — a pure JS EBML parser + OGG page builder that remuxes browser-recorded WebM/Opus audio into OGG/Opus entirely client-side, with no server-side dependencies. - Updated `AudioRecorder.vue` to use an explicit `mimeType` hint, proper resource cleanup, and an `AUDIO_EXTENSION_MAP` for correct file extensions. - Renamed `mp3ConversionUtils.js` → `audioConversionUtils.js` and added OGG conversion support via the new remuxer. - Updated `ReplyBox.vue` to request OGG format for WhatsApp channels, pass `isVoiceMessage` per-attachment, and handle recording errors with a user-facing alert. - Updated `MessageBuilder` to read the `is_voice_message` param and persist it in attachment metadata. - Updated `WhatsappCloudService` to: - Normalize `audio/opus` → `audio/ogg` content type on ActiveStorage blobs (works around Marcel gem re-detection). - Send the `voice: true` flag when the attachment is a voice message with `audio/ogg` content type. - Use WhatsApp Cloud API `v24.0` for the attachment endpoint. - Added `AUDIO_CONVERSION_FAILED` i18n key. **How it works:** 1. The browser records audio as WebM/Opus (Chrome/Firefox default). 2. `audioConversionUtils.js` remuxes it to OGG/Opus using the pure-JS `webmOpusToOgg` remuxer — no server transcoding needed. 3. The OGG file is uploaded with `is_voice_message: true` in the form payload. 4. `MessageBuilder` persists `is_voice_message` in the attachment's `meta` hash. 5. `WhatsappCloudService` normalizes the blob content type if needed, then sends the attachment with `voice: true` so WhatsApp renders it as a voice note. ## Type of change - [X] New feature (non-breaking change which adds functionality) ## How Has This Been Tested? 1. Record a voice message in a WhatsApp Cloud conversation. 2. Verify the audio is transcoded to OGG (check file extension in the attachment preview). 3. Verify the message arrives on WhatsApp as a voice note bubble (not a document/file). 4. Send an image or document attachment and verify it still works as before (no `voice` flag). 5. Send a regular (non-voice) audio file and verify it arrives without the voice flag. --------- Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com> Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
88e2661ca6
|
feat(conversations): remove unread count feature flag (CW-7237) (#14610)
## Description Make conversation unread counts always available at runtime by removing account feature checks from the API endpoint, unread-count listener, notifier, and ActionCable broadcast path. Update the dashboard to fetch sidebar unread counts for the active account without checking FEATURE_FLAGS.CONVERSATION_UNREAD_COUNTS, and remove the now-unused store clear action that only supported the disabled state. Keep the feature entry in config/features.yml to preserve flag bit order, but mark it enabled and deprecated so fresh installs default to the always-on behavior while feature-management UI hides it. Leave existing installation default rows untouched; no migration is included, so upgraded installs may still store the old flag value but runtime behavior no longer depends on it. Update specs around the new always-on contract and remove obsolete disabled-feature assertions. Fixes # CW-7237 ## Type of change Please delete options that are not relevant. - [x] 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? Update specs around the new always-on contract and remove obsolete disabled-feature assertions. Ran specs locally for the changes. ## Checklist: - [x] My code follows the style guidelines of this project - [x] 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 - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules |
||
|
|
f27bbef73b
|
feat: show processing status for one-off campaigns (#14592)
## Summary One-off SMS and WhatsApp campaigns now show a `Processing` state while the audience send is in progress. The campaign moves to `Completed` after processing finishes, and already-processing campaigns are skipped by the scheduler to avoid duplicate sends. ## Closes - [CW-6037: feat: Introduce an in-progress status for campaigns](https://linear.app/chatwoot/issue/CW-6037/feat-introduce-an-in-progress-status-for-campaigns) ## Screenshot SMS campaign card showing the new `Processing` status. <img width="3840" height="2160" alt="framed-campaign-processing-status" src="https://github.com/user-attachments/assets/de7913b5-65fb-4121-9034-24a568eb0382" /> ## What changed - Added `processing` as a campaign status. - Mark one-off campaigns as `processing` under a row lock before the send service runs. - Complete SMS, Twilio SMS, and WhatsApp one-off campaigns after audience processing finishes. - Keep campaigns in `processing` if an unexpected service error escapes, so the scheduler does not automatically resend the audience. - Added the `Processing` label for SMS and WhatsApp campaign cards. ## Known operational behavior If a worker is interrupted or an unexpected service error escapes after a campaign is marked `processing`, the campaign can remain in `processing`. This is intentional for now to avoid automatic full-audience resends. Installation admins can decide whether to mark the campaign completed or restart it manually from the Rails console after checking what was sent. ## How to test - Create a one-off SMS or WhatsApp campaign scheduled for now. - Run the scheduled job or trigger the campaign job. - Confirm the campaign card shows `Processing` while the audience is being processed. For small audiences, refresh during processing or use a larger audience so the state is observable. - Confirm the campaign moves to `Completed` after audience processing finishes. - Confirm an already-processing campaign is not enqueued again by the scheduled job. |
||
|
|
9c17a6f302
|
feat(onboarding): detect email provider from domain MX records (#14571)
During account enrichment, WebsiteBrandingService now probes the signup domain's MX records to infer whether email is hosted on Google Workspace or Microsoft 365, adding `email_provider` (`google`/`microsoft`/`nil`) to `brand_info`. Matching is anchored on a label boundary so lookalike domains aren't misclassified, and lookup failures fall back to `nil`. This lets downstream UI suggest the right mailbox integration (Gmail vs Outlook). |
||
|
|
b981ba766f
|
feat: support bulk label removal (#14534)
Adds bulk label removal alongside the existing assign-label action for conversations and contacts, so teams can clean up labels across selected records without opening each item individually. For conversations, the remove dropdown is scoped to labels that are actually applied across the current selection — so agents no longer see (or accidentally "remove") labels that aren't on any of the selected items. For contacts, the dropdown still lists all account labels for now; label data isn't carried on the contact list payload today, so scoping the contact remove menu cleanly is being tracked as a follow-up. ## Closes N/A ## How to test - Open the conversation list, select multiple conversations, open **Remove labels**, and confirm the dropdown only lists labels that are applied to at least one selected conversation. Pick a label and confirm it's removed from the selection. - Open Contacts, select multiple contacts, use **Remove Labels**, choose a label, and confirm the selected contacts are refreshed without that label. - Verify **Assign Labels** still works for conversations and contacts, and continues to show every available label. ## What changed - Adds an `action` prop to the shared `BulkLabelActions` dropdown so it can render in `assign` or `remove` mode. - Wires conversation bulk remove to the existing `labels.remove` backend path and filters the dropdown to the union of labels applied across the selected conversations. - Adds contact bulk remove support through `Contacts::BulkRemoveLabelsService`, routed by `Contacts::BulkActionService`. - Raises contact label save failures instead of reporting a successful bulk action when a contact update is invalid. ## Follow-ups - Scope the contact remove dropdown to applied labels (needs a lightweight endpoint, or eventually `cached_label_list` on `Contact`). ## Verification Conversation bulk remove selector: <img width="1680" height="1050" alt="Conversation bulk remove label selector" src="https://github.com/user-attachments/assets/2dba4a06-c497-45e1-85b0-e700164b6b2f" /> Contact bulk remove selector: <img width="1680" height="1050" alt="Contact bulk remove label selector" src="https://github.com/user-attachments/assets/b3b89959-5978-4064-b5f9-82b1a3e571dc" /> Video proof: https://github.com/user-attachments/assets/fffafe19-4e1c-4e2a-a135-c7182c06bb4d --------- Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Co-authored-by: iamsivin <iamsivin@gmail.com> |
||
|
|
56e30102eb
|
fix(whatsapp): store and surface unavailable coexistence messages (CW-7166) (#14547)
In WhatsApp coexistence setups (Business App + Cloud API on the same
number), some inbound customer messages arrive from Meta as `type:
unsupported` with error `131060` ("This message is unavailable") and no
content — typically the first message of a Click-to-WhatsApp /
Instagram-ad conversation, or a message synced from a companion device.
Chatwoot was dropping these webhooks entirely, so no contact,
conversation, or message was created. The conversation only surfaced
once an agent replied (via an `smb_message_echoes` event), starting
"headless" with zero customer context.
This change persists a placeholder message for these events so the
contact and conversation are created, and renders it with the dedicated
unsupported-message bubble that points agents to the WhatsApp app —
where the original message is still visible.
Fixes
https://linear.app/chatwoot/issue/CW-7166/whatsapp-coexistence-inbound-messages-are-silently-dropped
and https://github.com/chatwoot/chatwoot/issues/13464
<img width="3448" height="1604" alt="CleanShot 2026-05-22 at 17 49
35@2x"
src="https://github.com/user-attachments/assets/0a90ec84-9085-4cba-883d-08d9de33fa3c"
/>
## How to reproduce
1. Connect a WhatsApp Cloud (coexistence) inbox.
2. Receive an inbound message that Meta delivers as `type: unsupported`
with error `131060` (e.g. a Click-to-WhatsApp ad message, or a message
handled on a companion/primary device that fails to sync to the API).
3. **Before:** nothing is created — the conversation only appears after
an agent replies, with no record of the customer's first message.
4. **After:** the contact and conversation are created with an incoming
placeholder message rendered as the amber "unsupported" bubble: _"This
message is unsupported. You can view this message on the WhatsApp app."
---------
Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Sojan Jose <sojan@pepalo.com>
|
||
|
|
6fbff026eb
|
fix: skip AutoAssignment bulk loop when no agents are online (#14500)
## Description When an inbox has `enable_auto_assignment` and `assignment_v2` enabled but no agents are currently online, `AutoAssignment::AssignmentService#perform_bulk_assignment` still loaded up to 100 unassigned conversations and iterated each one, calling `inbox.available_agents` per conversation. Each call hits Redis presence lookups that return empty, no conversations get assigned, and the loop finishes having done only wasted work. For a busy inbox with a long unassigned backlog and offline agents, this is hundreds of Redis ops per job, multiplied by every `AutoAssignment::AssignmentJob` enqueue from the per-save handler. The pressure is significant when inbound volume is high. This adds a single early-return guard: if `inbox.available_agents.empty?`, return `0` immediately. Existing semantics are preserved (jobs are still enqueued on conversation events; they just exit cheaply when there is no one to assign to). ## Type of change - [x] Performance improvement (non-breaking change) ## Test coverage - [x] Added specs |
||
|
|
c4a6a19e9b
|
feat(voice): WhatsApp Cloud Calling — UI [6] (#14346)
Some checks failed
Frontend Lint & Test / test (push) Has been cancelled
Publish Chatwoot EE docker images / build (linux/amd64, ubuntu-latest) (push) Has been cancelled
Publish Chatwoot EE docker images / build (linux/arm64, ubuntu-22.04-arm) (push) Has been cancelled
Publish Chatwoot CE docker images / build (linux/amd64, ubuntu-latest) (push) Has been cancelled
Publish Chatwoot CE docker images / build (linux/arm64, ubuntu-22.04-arm) (push) Has been cancelled
Run Chatwoot CE spec / lint-backend (push) Has been cancelled
Run Chatwoot CE spec / lint-frontend (push) Has been cancelled
Run Chatwoot CE spec / frontend-tests (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (0, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (1, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (10, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (11, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (12, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (13, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (14, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (15, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (2, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (3, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (4, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (5, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (6, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (7, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (8, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (9, 16) (push) Has been cancelled
Publish Chatwoot EE docker images / merge (push) Has been cancelled
Publish Chatwoot CE docker images / merge (push) Has been cancelled
## Summary Frontend for WhatsApp Cloud Calling: header / contact-panel call buttons, ringing widget, accept/reject/hangup, mute, in-bubble audio player + transcript, recording-on-hangup upload, mid-call reload warning. WebRTC is browser-direct to Meta — no media server bridge. ## Closes - https://linear.app/chatwoot/issue/PLA-150 ## How to test Requires backend support — the controller, services, model changes, and routes ship in **#14334** (`feature/pla-150`). Merge / deploy that first (or simultaneously); the FE alone won't function without those endpoints. Then on staging, for a WhatsApp Cloud + embedded-signup inbox with the new \`Configuration → Enable voice calling\` toggle ON and webhook registered: 1. **Outbound** — open a conversation, click the phone icon in the conversation header (or contact panel), grant mic, your phone rings, answer, audio both ways, hang up. Recording + transcript land in the bubble within ~10s. 2. **Inbound** — call the business number from your phone. The FloatingCallWidget appears bottom-right with caller name. Click accept, audio both ways, hang up. Recording + transcript appear. 3. **Mute** — during an active WhatsApp call, click the mic icon next to hangup. Speech stops reaching Meta until you click again. 4. **Mid-call reload guard** — try `Cmd-R` during an active call; browser shows a confirm prompt. --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: iamsivin <iamsivin@gmail.com> Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> |
||
|
|
3cd8cf43ce
|
fix: atomically claim conversation to prevent duplicate assignment (#14495)
## Description Fixes a bug under Assignment V2 where a single conversation could be reassigned dozens of times in a row by the system, producing long stacks of "Assigned to X by Automation System via <policy>" activity messages alternating between agents. After this change each unassigned conversation is assigned exactly once, even on busy inboxes. ## Fixes # (issue) ## Type of change - [ ] Bug fix (non-breaking change which fixes an issue) ## How Has This Been Tested? ## How to reproduce 1. Enable `assignment_v2` on an account with at least 2 online agents in an inbox. 2. Generate sustained resolve/snooze activity in the inbox (each one enqueues `AutoAssignment::AssignmentJob` for the whole inbox). 3. Watch any one unassigned conversation while the jobs drain — pre-fix it picks up multiple back-to-back "Assigned to …" activity rows alternating between agents. ## 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 |
||
|
|
f33e469e9a
|
feat: Unread Count: Frontend changes for showing unread count badges (3/3)[CW-6851] (#14372)
# Pull Request Template ## Description This is the third and final PR in a series of PRs for Introducing unread counts in the sidebar for inboxes and labels. In this PR: * Added frontend changes to show the badges for unread counts for Inboxes and Labels * Added specs for the changes Issue: https://linear.app/chatwoot/issue/CW-6851/support-unread-conversation-counts ## Type of change Please delete options that are not relevant. - [ ] Bug fix (non-breaking change which fixes an issue) - [x] 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? Tested this locally. Cases to test: * Send a message from the widget and see if the count changes * Mark a conversation as unread and see the count change for inbox * Open an unread conversation as agent and see the count go down * Add a label to an unread conversation from sidebar right click action without opening the conversation and see the count of un-reads on the label change Added the screenshot of how it will look like <img width="614" height="990" alt="Screenshot 2026-05-05 at 7 00 11 PM" src="https://github.com/user-attachments/assets/99fbaa9f-bcf2-4d8d-86e2-5727f652a9dd" /> ## Checklist: - [x] My code follows the style guidelines of this project - [x] 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 - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules --------- Co-authored-by: Sojan Jose <sojan@pepalo.com> |
||
|
|
27f2c2b392
|
feat: Unread Count: added api, store refresher, invalidation and events (2/3)[CW-6851] (#14369)
Some checks failed
Frontend Lint & Test / test (push) Has been cancelled
Publish Chatwoot EE docker images / build (linux/amd64, ubuntu-latest) (push) Has been cancelled
Publish Chatwoot EE docker images / build (linux/arm64, ubuntu-22.04-arm) (push) Has been cancelled
Publish Chatwoot CE docker images / build (linux/amd64, ubuntu-latest) (push) Has been cancelled
Publish Chatwoot CE docker images / build (linux/arm64, ubuntu-22.04-arm) (push) Has been cancelled
Run Chatwoot CE spec / lint-backend (push) Has been cancelled
Run Chatwoot CE spec / lint-frontend (push) Has been cancelled
Run Chatwoot CE spec / frontend-tests (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (0, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (1, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (10, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (11, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (12, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (13, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (14, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (15, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (2, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (3, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (4, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (5, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (6, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (7, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (8, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (9, 16) (push) Has been cancelled
Publish Chatwoot EE docker images / merge (push) Has been cancelled
Publish Chatwoot CE docker images / merge (push) Has been cancelled
# Pull Request Template ## Description This is the second PR in a series of PRs for Introducing unread counts in the sidebar for inboxes and labels. In this PR: * added api for unread counts * Added the store refresher and invalidation with event listeners * Added action cable event * Added specs for the changes Issue: https://linear.app/chatwoot/issue/CW-6851/support-unread-conversation-counts ## Type of change Please delete options that are not relevant. - [ ] Bug fix (non-breaking change which fixes an issue) - [x] 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: - [x] My code follows the style guidelines of this project - [x] 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 - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules --------- Co-authored-by: Sojan Jose <sojan@pepalo.com> |
||
|
|
40deaef458
|
feat: Store WhatsApp BSUID identifiers from inbound webhooks (#14436)
Adds storage support for WhatsApp business-scoped user identifiers received from Meta Cloud API and Twilio WhatsApp webhooks. The change keeps existing phone-based behavior intact, stores BSUID and parent BSUID values as additional `contact_inboxes.source_id` rows for the same contact, and allows BSUID-only inbound messages to create contacts, conversations, and messages without requiring a phone number. Related: https://github.com/chatwoot/chatwoot/issues/13837 **What changed** - Extended WhatsApp source ID validation to accept regular BSUID and parent BSUID formats. - For Meta Cloud API, stores phone, `user_id`, and `parent_user_id` identifiers as contact inbox source IDs when they are present. - For Twilio WhatsApp, stores phone, `ExternalUserId`, and `ParentExternalUserId` identifiers as contact inbox source IDs while preserving the existing `whatsapp:` Twilio source ID shape. - Supports BSUID-only inbound messages by creating a contact, contact inbox, conversation, and message even when the phone number is missing. - Links phone-first and later BSUID-only messages to the same contact when the first payload contains both phone and BSUID. - Stores WhatsApp usernames in contact `additional_attributes`, matching existing social channel patterns. - Keeps existing phone-based outbound and new-conversation behavior unchanged for this milestone. **How to test** 1. Send a Meta Cloud webhook payload with both `wa_id` and `user_id`. 2. Verify Chatwoot creates or finds the phone `contact_inbox` and also creates a BSUID `contact_inbox` for the same contact. 3. Send a later Meta Cloud payload for the same user with only `user_id` / `from_user_id`. 4. Verify Chatwoot finds the BSUID `contact_inbox` and creates the inbound message without requiring a phone number. 5. Send a Twilio WhatsApp webhook with `From: whatsapp:+E164`, `ExternalUserId`, and optionally `ParentExternalUserId`. 6. Verify Chatwoot stores the Twilio phone and BSUID identifiers as `whatsapp:`-prefixed source IDs for the same contact. 7. Send a Twilio WhatsApp webhook where `From` is `whatsapp:<BSUID>` and there is no phone number. 8. Verify Chatwoot creates the contact, contact inbox, conversation, and message without a phone number. --------- Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com> |
||
|
|
3fae800936
|
feat: base layer for unread counts (store, counter and builder) (1/3)[CW-6851] (#14368)
## Description This is the first PR in a series of PRs for Introducing unread counts in the sidebar for inboxes and labels. In this PR: * Added the unread store, counter and builder modules * Added redis keys for unread count management * Added specs for all 3 modules, some specs are for testing enterprise only feature like specific roles and permissions which are added in the respective enterprise folder itself. **Note** None of this changes affect anything else and nothing is wired to existing modules. Issue: https://linear.app/chatwoot/issue/CW-6851/support-unread-conversation-counts ## Type of change Please delete options that are not relevant. - [ ] Bug fix (non-breaking change which fixes an issue) - [x] 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: - [x] My code follows the style guidelines of this project - [x] 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 - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules --------- Co-authored-by: Sojan Jose <sojan@pepalo.com> |
||
|
|
05bda5f742
|
feat: don't let onboarding write domain (#14442)
Stop the onboarding flow from writing the user's company website into `accounts.domain`. That column is reserved for the inbound email domain used to construct reply-to addresses (`reply+<uuid>@<domain>`), and silently overloading it from onboarding was breaking email continuity for accounts whose domain MX didn't point at Chatwoot's inbound — customer replies were going to an unreachable address. The website value now lives in `custom_attributes.website`, which is what the rest of the app already treats as the "company website" field. |
||
|
|
379e28df1f
|
fix: prevent bot metrics double-counting when handoff and resolution coexist [CW-6210] (#14032)
The bot metrics dashboard can show `handoff_rate + resolution_rate >
100%`. A single conversation can accumulate both
`conversation_bot_handoff` and `conversation_bot_resolved` events, and
the rate queries count them independently against a shared denominator.
## How it happens
```
Customer messages bot inbox
│
▼
┌──────────┐
│ pending │ (bot handling)
└────┬─────┘
│ bot can't help
▼
┌──────────┐
│ open │ (handed off → conversation_bot_handoff event created)
└────┬─────┘
│ agent clicks "Resolve" WITHOUT sending a message
▼
┌──────────┐
│ resolved │ conversation_resolved fires
└──────────┘
│
▼
create_bot_resolved_event guard checks:
✅ inbox.active_bot?
✅ no outgoing messages with sender_type: 'User' ← agent never messaged!
│
▼
conversation_bot_resolved event ALSO created ← BUG
│
▼
Same conversation counted in BOTH rates → sum exceeds 100%
```
## Why fix at the read path, not the write path
An earlier attempt added guards in the listener to make the two events
mutually exclusive per conversation — deleting `bot_resolved` when a
handoff fires, suppressing resolutions when a handoff exists. This was
rejected because conversations can be reopened across multiple cycles
(bot resolves on day 1, customer returns on day 5, bot hands off).
Deleting the day-1 resolution corrupts historical reports, and the async
event dispatcher makes listener-level guards vulnerable to race
conditions.
## What this PR does
Within a reporting window, if a conversation has both events, **handoff
wins** — the conversation is excluded from the resolution count. This is
applied via SQL subquery across all three read paths:
```
┌─────────────────────────┐
│ Reporting Events DB │
│ │
│ conv_bot_handoff: [A,B] │
│ conv_bot_resolved: [A,C]│
└────────┬────────────────┘
│
┌──────────────┼──────────────┐
▼ ▼ ▼
BotMetricsBuilder ReportHelper CountReportBuilder
(rate cards) (bot_summary) (timeseries charts)
│ │ │
▼ ▼ ▼
resolutions: resolutions: resolutions:
[A,C] minus [A,B] same logic same logic
= [C] only = [C] only = [C] only
Result: Conversation A → handoff only
Conversation B → handoff only
Conversation C → resolution only
```
For wide date ranges spanning multiple lifecycles, a conversation
bot-resolved in one cycle and handed off in a later cycle will only show
as a handoff. This is an acceptable tradeoff — the alternative (>100%
rates) is clearly worse, and narrow ranges handle this correctly since
the events fall into different windows. No reporting events are
modified, so historical data stays intact.
## Diagnostic tool
`rake bot_metrics:diagnose` — read-only task that prompts for account ID
and date range, shows a before/after rate comparison without modifying
data.
---------
Co-authored-by: aakashb95 <aakashbakhle@gmail.com>
Co-authored-by: Aakash Bakhle <48802744+aakashb95@users.noreply.github.com>
|
||
|
|
6c67eb9ba0
|
fix(notifications): Respect conversation access when notifying agents (#14412)
Agents with limited custom roles were receiving notifications (creation, assignment, mentions, new messages, SLA) for conversations they couldn't actually open. For example, an agent whose custom role only grants `conversation_unassigned_manage` was getting notified about conversations assigned to other agents. Notifications now go through the same `ConversationPolicy#show?` check that gates the conversation view itself, so an agent only gets notified for conversations they're permitted to see. Administrators and agents without custom roles are unaffected. --------- Co-authored-by: Sojan Jose <sojan@pepalo.com> Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com> |
||
|
|
de696a55cb
|
feat(voice): add WhatsApp inbound call webhook pipeline [3] (#14315)
Adds the server-side flow that turns Meta WhatsApp Cloud Calling webhooks into Chatwoot Calls, conversations, voice_call message bubbles, and ActionCable broadcasts. Stacked on top of #14312 (PR-2 — provider methods); intentionally does not include the HTTP controller, routes, or frontend (those land in PR-4 and PR-9). ## Closes - Part of the WhatsApp Cloud Calling rollout. Linear: TBD ## What changed **Webhook routing** - `app/jobs/webhooks/whatsapp_events_job.rb` — append `prepend_mod_with('Webhooks::WhatsappEventsJob')` so EE can extend it without forking. - `enterprise/app/jobs/enterprise/webhooks/whatsapp_events_job.rb` (new) — overlay that prepends `handle_message_events` to intercept `field: 'calls'` payloads (route to `Whatsapp::IncomingCallService`) and `interactive.call_permission_reply` messages (route to `Whatsapp::CallPermissionReplyService`); falls through with `super` for regular messages. **Services** - `enterprise/app/services/whatsapp/incoming_call_service.rb` (new) — gated on `provider_config['calling_enabled']`; processes `connect` (creates inbound call via `Voice::InboundCallBuilder` or transitions an existing outbound call to `in_progress`) and `terminate` events; updates conversation `additional_attributes` and broadcasts `voice_call.incoming`/`voice_call.outbound_connected`/`voice_call.ended`. - `enterprise/app/services/whatsapp/call_permission_reply_service.rb` (new) — handles WhatsApp interactive `call_permission_reply` replies; clears the conversation's `call_permission_requested_at` flag and broadcasts `voice_call.permission_granted` so the agent UI can re-enable the call button. **Builder/model adjustments** - `enterprise/app/services/voice/inbound_call_builder.rb` — provider-agnostic; accepts `provider:` and `extra_meta:` kwargs, drops `account:` (now derived from `inbox.account` to keep the param count under rubocop's ceiling without disabling cops), uses digits-only `source_id` for WhatsApp ContactInbox (validation requires `^\d{1,15}\z`), skips Twilio-only `conference_sid` for non-Twilio providers. - `enterprise/app/services/voice/call_message_builder.rb` — adds `create!`/`update_status!` API and `CALL_TO_VOICE_STATUS` map; uses direct `Message.create!` (bypasses `Messages::MessageBuilder`'s incoming-on-non-Api-inbox guard, which would otherwise reject the system bubble); content is `'WhatsApp Call'` for WhatsApp and `'Voice Call'` for Twilio. Backwards-compatible `perform!` retained for the existing Twilio call sites. - `enterprise/app/models/call.rb` — adds `default_ice_servers` (driven by `VOICE_CALL_STUN_URLS` env), `direction_label` alias for the `inbound`/`outbound` strings the FE expects, and `ringing?`/`in_progress?`/`terminal?` predicates used throughout the pipeline. **Outgoing-channel guard** - `app/services/base/send_on_channel_service.rb` — extends `invalid_message?` to skip messages with `content_type == 'voice_call'`. Without this, agent-initiated outbound calls (PR-4) would deliver \"WhatsApp Call\" as a text message to the contact every time. **Twilio call-site update** - `enterprise/app/controllers/twilio/voice_controller.rb` — drops the now-redundant `account: current_account` kwarg from the `Voice::InboundCallBuilder.perform!` call. **Tests** - New: `spec/enterprise/services/whatsapp/incoming_call_service_spec.rb` (5 examples — calling-disabled, inbound connect, outbound connect, terminate completed, terminate no-answer, unknown event). - New: `spec/enterprise/services/whatsapp/call_permission_reply_service_spec.rb` (3 examples — accept, reject, calling-disabled). - Updated: `spec/enterprise/services/voice/inbound_call_builder_spec.rb` and `spec/enterprise/controllers/twilio/voice_controller_spec.rb` to drop the `account:` kwarg from call expectations. ## How to test In `rails console` against an account with a WhatsApp inbox where `provider_config['calling_enabled']` is true: ```ruby inbox = Inbox.find(<id>) params = { calls: [{ id: 'wacid_test', from: '15550001111', event: 'connect', session: { sdp: 'v=0...', sdp_type: 'offer' } }] } Whatsapp::IncomingCallService.new(inbox: inbox, params: params).perform # => Conversation + Call (status: 'ringing', provider: 'whatsapp') + voice_call message bubble # => ActionCable broadcasts `voice_call.incoming` to the assignee or account-wide # Then terminate it: Whatsapp::IncomingCallService.new(inbox: inbox, params: { calls: [{ id: 'wacid_test', event: 'terminate', duration: 0, terminate_reason: 'no_answer' }] } ).perform # => Call status flips to 'no_answer', message bubble updates, `voice_call.ended` broadcast fires ``` End-to-end browser flow (Meta → cable → UI) requires the controller from PR-4 and the frontend from PR-9. --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
3489298726
|
feat: add WidgetCreationService for onboarding web widget setup (#14314)
Some checks failed
Frontend Lint & Test / test (push) Has been cancelled
Publish Chatwoot EE docker images / build (linux/amd64, ubuntu-latest) (push) Has been cancelled
Publish Chatwoot EE docker images / build (linux/arm64, ubuntu-22.04-arm) (push) Has been cancelled
Publish Chatwoot CE docker images / build (linux/amd64, ubuntu-latest) (push) Has been cancelled
Publish Chatwoot CE docker images / build (linux/arm64, ubuntu-22.04-arm) (push) Has been cancelled
Run Chatwoot CE spec / lint-backend (push) Has been cancelled
Run Chatwoot CE spec / lint-frontend (push) Has been cancelled
Run Chatwoot CE spec / frontend-tests (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (0, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (1, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (10, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (11, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (12, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (13, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (14, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (15, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (2, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (3, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (4, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (5, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (6, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (7, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (8, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (9, 16) (push) Has been cancelled
Publish Chatwoot EE docker images / merge (push) Has been cancelled
Publish Chatwoot CE docker images / merge (push) Has been cancelled
When a new account finishes onboarding we want to land them on a
dashboard with a working web widget already configured, branded, named,
and assigned to them, instead of an empty inbox list. This PR adds the
services that produce that widget. **No user-visible change yet:** the
services are dormant until the trigger and background job are wired up
in the follow-up PR.
## Context
Milestone 1 added `Account::BrandingEnrichmentJob`, which calls
context.dev during signup and stores brand data on
`account.custom_attributes['brand_info']`, plus the new onboarding form
that captures `domain`, `name`, `industry`, etc. Milestone 2 starts
using that data, and the first thing we want is a web widget
materialized automatically. Splitting the service layer from the
orchestration plumbing (Redis key, `onboarding_step` extension,
controller wiring, ActionCable) keeps this diff focused and lets the
LLM/widget logic merge independently.
## How to test
Run against an existing account that already has `brand_info` populated.
```ruby
account = Account.find(<account_id>)
user = account.administrators.first
inbox = WidgetCreationService.new(account, user).perform
inbox.channel.widget_color # color from brand_info, or '#1f93ff'
inbox.channel.welcome_title # brand_info[:title], or account.name
inbox.channel.welcome_tagline # LLM tagline (Enterprise + system key set),
# else brand_info[:slogan]/[:description]/nil
inbox.inbox_members.pluck(:user_id)
```
Toggle `InstallationConfig['CAPTAIN_OPEN_AI_API_KEY']` to flip between
LLM and brand-text tagline paths. To verify failure isolation, raise
inside `Captain::Llm::WidgetTaglineService#perform` and confirm widget
creation still succeeds with the fallback tagline.
|
||
|
|
bc768bf04f
|
chore: verbosely log errors for leadsquare activity failure (#14407) | ||
|
|
202403873d
|
feat: Ability to specify the authentication type for imap server (#12306)
# Pull Request Template ## Description This PR adds IMAP authentication mechanism selection to Chatwoot's email inbox configuration. Users can now choose between 'plain', 'login', and 'cram-md5' authentication methods when configuring IMAP settings, providing flexibility for different email providers that require specific authentication types. https://github.com/chatwoot/chatwoot/issues/8867 The implementation includes: - Frontend dropdown with numeric keys (1, 2, 3) matching SMTP auth style - Backend API validation for allowed authentication mechanisms - Consistent 'cram-md5' format throughout the codebase - Updated IMAP service to handle different auth types properly This feature maintains consistency with existing SMTP authentication options and follows the established UI/UX patterns in the application. ## Type of change Please delete options that are not relevant. - [x] New feature (non-breaking change which adds functionality) - [ ] Bug fix (non-breaking change which fixes an issue) - [ ] 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? ### Manual Testing: - Tested in Docker environment - Verified IMAP auth dropdown appears in inbox settings - Confirmed all three auth mechanisms (plain, login, cram-md5) can be selected and saved - Tested API validation by attempting to save invalid auth mechanisms ### Automated Testing: - Updated existing IMAP service tests to use consistent lowercase values - Updated API controller tests for authentication parameter handling - All tests pass locally with the new changes ### Test Configuration: - Tested with both new and existing inbox configurations ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [x] I have commented on my code, particularly in hard-to-understand areas - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [x] Any dependent changes have been merged and published in downstream modules ## Additional Notes - This feature is backward compatible and doesn't break existing IMAP configurations - The 'cram-md5' format is used consistently throughout (UI, API, storage, services) - Net::IMAP compatibility is maintained by converting to 'CRAM-MD5' internally - Follows the same pattern established by SMTP authentication configuration --------- Co-authored-by: João Santos <joao.santos@madigital.eu> Co-authored-by: Sony Mathew <sony@chatwoot.com> |
||
|
|
9c1d1c4070
|
feat(labels): remove label associations asynchronously on delete (#13531)
## Summary - Remove label deletion dependency on association cleanup by deleting immediately and enqueueing a background job. - Add `Labels::RemoveAssociationsJob` to strip deleted label references from tagged conversations and contacts. - Keep this version simple by removing the label count/prompt requirement requested. ## Implementation notes - Enqueue job from `Api::V1::Accounts::LabelsController#destroy` with label title + account id. - Background work performed in `Labels::DestroyService`. ## References - Linear issue: https://linear.app/chatwoot/issue/CW-4765/cw-2857-enhancement-removing-labels-is-inconsistent - GitHub issue: https://github.com/chatwoot/chatwoot/issues/1249 ## Testing - `bundle exec rspec spec/controllers/api/v1/accounts/labels_controller_spec.rb spec/services/labels/destroy_service_spec.rb spec/jobs/labels/remove_associations_job_spec.rb spec/services/labels/update_service_spec.rb` - `bundle exec rubocop app/controllers/api/v1/accounts/labels_controller.rb app/jobs/labels/remove_associations_job.rb spec/controllers/api/v1/accounts/labels_controller_spec.rb spec/jobs/labels/remove_associations_job_spec.rb spec/services/labels/destroy_service_spec.rb` --------- Co-authored-by: Sony Mathew <sony@chatwoot.com> Co-authored-by: Sony Mathew <2040199+sony-mathew@users.noreply.github.com> |
||
|
|
b8108b71c1
|
fix(tiktok): Resolve media upload failures and gate attachments by conversation capability (#13643)
This PR fixes TikTok attachment send failures and adds a capability-based guard so attachments are only enabled for conversations that support media sending. - Fixed TikTok media upload request formatting so TikTok accepts image uploads reliably. - Added TikTok capability check (IMAGE_SEND) during conversation creation. - Stored capability in conversation.additional_attributes.tiktok_capabilities. - Updated reply composer UI to disable/hide attachment upload for TikTok conversations where image_send is false. Fixes https://linear.app/chatwoot/issue/CW-6532/enable-attachments-based-on-the-conversation-capability and https://linear.app/chatwoot/issue/CW-6996/unable-to-send-image-attachments-to-tiktok-customer-400-parsing-error --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com> |
||
|
|
a01adf860a
|
fix: [CW-7001] Limit emails fetch (#14354)
This PR limits IMAP email fetching to 500 messages per sync run to avoid expensive/long-running mailbox scans. It also filters out already-imported emails and Chatwoot-generated notification emails during the header fetch phase, before fetching full email bodies, reducing unnecessary IMAP work. Fixes #CW-7001 (issue) : https://linear.app/chatwoot/issue/CW-7001/emails-not-syncing |
||
|
|
28ec1794f4
|
feat(voice): add WhatsApp Cloud Calling provider methods (#14312)
Adds the Meta WhatsApp Cloud API surface needed for browser-based calling. This is the second slice of the WhatsApp calling feature, sitting on top of `feat/voice-call-model-wiring` and consumed by later PRs (incoming-webhook pipeline, call service, frontend). This PR ships only the provider-level HTTP wrapper and one error class. It is feature-flag-free and does not change any user-visible behaviour on its own — without later PRs, no caller invokes these methods. ## Linear - https://linear.app/chatwoot/issue/PLA-148/pr-2-meta-cloud-api-provider-methods ## What changed - Add `Whatsapp::Providers::WhatsappCloudCallMethods` (`enterprise/app/services/whatsapp/providers/whatsapp_cloud_call_methods.rb`) wrapping six Meta endpoints: - `pre_accept_call`, `accept_call`, `reject_call`, `terminate_call` — `POST /{phone_id}/calls` with the relevant action payload. - `send_call_permission_request` — `POST /{phone_id}/messages` interactive `call_permission_request`. - `initiate_call` — `POST /{phone_id}/calls` with `audio`/`offer` session. - Prepend the module into `Whatsapp::Providers::WhatsappCloudService` only if defined, so OSS continues to work without the enterprise overlay. - Add `Voice::CallErrors::NoCallPermission` (`enterprise/lib/voice/call_errors.rb`) — raised when Meta returns error code `138006` from `initiate_call`. The remaining call-service errors (`NotRinging`, `AlreadyAccepted`, `CallFailed`) will land with PR-4. ## How to test There is no UI in this PR. Smoke-test from a Rails console with a WhatsApp inbox configured for calling: ```ruby inbox = Inbox.find(<id>) svc = inbox.channel.provider_service svc.respond_to?(:initiate_call) # => true svc.respond_to?(:send_call_permission_request) # => true # Optional live calls (require a real phone + Meta call-permission opt-in): svc.send_call_permission_request('15551234567') svc.initiate_call('15551234567', '<sdp_offer>') ``` Failure path: `initiate_call` against a contact who has not granted call permission should raise `Voice::CallErrors::NoCallPermission` with Meta's user-facing message. |
||
|
|
cd9c8e3303
|
fix: skip self-mention notification in private notes (#14318)
When an agent mentions themselves in a private note, they no longer receive a redundant notification for their own mention. Closes: #4096 # Pull Request Template ## Description Agents who mention themselves in a private note no longer receive a conversation_mention notification. Previously, the mention service would generate a notification for every mentioned user without checking whether the sender and the mentioned user were the same person. |
||
|
|
f8f0caf443
|
feat(campaigns): Add variable support to WhatsApp campaigns (#13649)
Fixes https://linear.app/chatwoot/issue/CW-5641/add-the-support-for-variables-in-whatsapp-campaign-templates This PR adds liquid variable support to WhatsApp campaigns, enabling dynamic per-contact personalization. It supports the same liquid variables as SMS campaigns ({{contact.name}}, {{contact.email}}, etc.). Variables are processed per-contact when the campaign executes, allowing personalized messages at scale. --------- Co-authored-by: Sojan Jose <sojan@pepalo.com> Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com> Co-authored-by: Sony Mathew <sony@chatwoot.com> |
||
|
|
16b8693e1b
|
fix: standardize contact company field on company_name (#14099)
Standardizes the contact company import/filter/automation contract on `company_name`. Closes #14096 Revives #9907 ## Why Contact company is read across the current CRM/contact UI from `additional_attributes['company_name']`, but CSV import and a few backend filter/automation paths still used the older `company` key. That meant imported company values could be saved in a place the dashboard, sorting, filters, and automation conditions did not consistently read from. Based on the production data check, the legacy `company` automation configuration is effectively dead: the affected account did not have contacts populated with `additional_attributes['company']`. So this PR intentionally avoids adding long-term fallback behavior and uses `company_name` as the single key going forward. ## What changed - Contact CSV import now writes only `company_name` into `additional_attributes['company_name']`. - The example contact import CSV now uses the `company_name` header. - Contact company sorting/filter config now uses `company_name`. - Automation condition config now uses `company_name`. - Existing standard automation conditions with `attribute_key: 'company'` are migrated to `company_name`. - Existing saved contact filters with standard `attribute_key: 'company'` are migrated to `company_name`. - Custom attributes named `company` are preserved and are not rewritten by the migration. ## How to test - Import a contact CSV with a `company_name` column and confirm the Contact Company field is populated. - Sort contacts by Company and confirm imported contacts are ordered correctly. - Create/edit an automation with Company as a condition and confirm it saves with `company_name`. - Verify existing saved contact filters and automation rules using the old standard `company` key are migrated to `company_name`. --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Sojan Jose <sojan@pepalo.com> |
||
|
|
6a9c44476e
|
feat(super-admin): Add push diagnostics tool (#14105)
Some checks failed
Frontend Lint & Test / test (push) Has been cancelled
Publish Chatwoot EE docker images / build (linux/amd64, ubuntu-latest) (push) Has been cancelled
Publish Chatwoot EE docker images / build (linux/arm64, ubuntu-22.04-arm) (push) Has been cancelled
Publish Chatwoot CE docker images / build (linux/amd64, ubuntu-latest) (push) Has been cancelled
Publish Chatwoot CE docker images / build (linux/arm64, ubuntu-22.04-arm) (push) Has been cancelled
Run Chatwoot CE spec / lint-backend (push) Has been cancelled
Run Chatwoot CE spec / lint-frontend (push) Has been cancelled
Run Chatwoot CE spec / frontend-tests (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (0, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (1, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (10, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (11, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (12, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (13, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (14, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (15, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (2, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (3, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (4, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (5, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (6, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (7, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (8, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (9, 16) (push) Has been cancelled
Publish Chatwoot EE docker images / merge (push) Has been cancelled
Publish Chatwoot CE docker images / merge (push) Has been cancelled
We're getting many customer reports saying "I'm not getting notifications." We can't always identify the root cause since there are multiple points of failure. Added a **Push Diagnostics** tool in Super Admin to help us investigate mobile/web push issues. Here's how it works: - Look up a user by email/ID → see all their registered subscriptions with device info (iOS/Android, brand, model), token freshness, and last-updated time - Send a customizable test push and read the raw FCM/web-push/relay response to see if the customer is receiving push notifications—if not, it will show proper errors. - Delete broken subscriptions so the mobile app re-registers on next launch <img width="3816" height="1974" alt="CleanShot 2026-04-20 at 12 56 56@2x" src="https://github.com/user-attachments/assets/08ecab6f-7ec3-44b3-a114-5e6eb8cf0879" /> Fixes https://linear.app/chatwoot/issue/CW-6892/push-diagnostics-tool --------- Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
6cbddbdb67
|
feat(rollup): report builder abstraction [2/3] (#13798)
## PR2: Report builder refactor — DataSource abstraction
The existing report builders (timeseries + summary) had their SQL
queries inlined — each builder constructed its own scopes, groupings,
and aggregations directly. This made it hard to swap the underlying data
source without duplicating builder logic.
This PR extracts all raw-event querying into a `Reports::RawDataSource`
behind a `Reports::DataSource` factory. Builders now call
`data_source.timeseries`, `.aggregate`, or `.summary` instead of
constructing queries themselves. Behavior is identical —
`DataSource.for(...)` returns `RawDataSource` in all cases today.
The timeseries path had two separate builders (`CountReportBuilder`,
`AverageReportBuilder`) that were selected via a metric-name case
statement in `Conversations::BaseReportBuilder`. These are replaced by a
single `ReportBuilder` that delegates to the data source. The metric
type (count vs average) is now decided inside the data source, not the
builder.
Summary builders similarly moved their inline SQL into
`RawDataSource#summary`, which returns a unified hash keyed by dimension
ID.
the rollup read path.
## Flow
### Before
```
ReportsController ──▶ case metric ──▶ AverageReportBuilder ──▶ inline SQL ──▶ DB
└──▶ CountReportBuilder ──▶ inline SQL ──▶ DB
SummaryController ──▶ AgentSummaryBuilder ──▶ inline SQL ──▶ DB
└──▶ InboxSummaryBuilder ──▶ inline SQL ──▶ DB
└──▶ TeamSummaryBuilder ──▶ inline SQL ──▶ DB
```
### After
```
ReportsController ──▶ ReportBuilder ──┐
├──▶ DataSource.for ──▶ RawDataSource ──▶ DB
SummaryController ──▶ SummaryBuilder ──┘
```
### Expected (after rollup read path)
```
ReportsController ──▶ ReportBuilder ──┐
├──▶ DataSource.for ──▶ RawDataSource ──▶ reporting_events
SummaryController ──▶ SummaryBuilder ──┘ └──▶ RollupDataSource ──▶ reporting_events_rollups
```
### What changed
- `Reports::DataSource` factory + `Reports::RawDataSource`
- `TimezoneHelper#timezone_name_from_params` — prefers IANA name, falls
back to offset
- Unified `Timeseries::ReportBuilder` replaces `CountReportBuilder` +
`AverageReportBuilder`
- Summary builders delegate to `DataSource` instead of querying directly
### How to test
This is a pure refactor — all existing report pages (Overview, Agent,
Inbox, Label, Team) should produce identical numbers. No feature flag or
new config needed.
---------
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Tanmay Deep Sharma <tanmaydeepsharma21@gmail.com>
Co-authored-by: Tanmay Deep Sharma <32020192+tds-1@users.noreply.github.com>
|
||
|
|
135be52431
|
feat: Introduce last responding agent option to automation assign agent (#12326)
Introduce a `Last Responding Agent` options to assign_agents action in automations to cover the following use cases. - Assign conversations to first responding agent : ( automation message created at , if assignee is nil, assign last responding agent ) - Ensure conversations are not resolved with out an assignee : ( automation conversation resolved at : if assignee is nil, assign last responding agent ) and potential other cases. fixes: #1592 |
||
|
|
aee979ee0b
|
fix: add explicit remove assignment actions to macros and automations (#12172)
This updates macros and automations so agents can explicitly remove assigned agents or teams, while keeping the existing `Assign -> None` flow working for backward compatibility. Fixes: #7551 Closes: #7551 ## Why The original macro change exposed unassignment only through `Assign -> None`, which made macros behave differently from automations and left the explicit remove actions inconsistent across the product. This keeps the lower-risk compatibility path and adds the explicit remove actions requested in review. ## What this change does - Adds `Remove Assigned Agent` and `Remove Assigned Team` as explicit actions in macros. - Adds the same explicit remove actions in automations. - Keeps `Assign Agent -> None` and `Assign Team -> None` working for existing behavior and stored payloads. - Preserves backward compatibility for existing macro and automation execution payloads. - Downmerges the latest `develop` and resolves the conflicts while keeping both the new remove actions and current `develop` behavior. ## Validation - Verified both remove actions are available and selectable in the macro editor. - Verified both remove actions are available and selectable in the automation builder. - Applied a disposable macro with `Remove Assigned Agent` and `Remove Assigned Team` on a real conversation and confirmed both fields were cleared. - Applied a disposable macro with `Assign Agent -> None` and `Assign Team -> None` on a real conversation and confirmed both fields were still cleared. |
||
|
|
48533e2a5d
|
fix: strip markdown hard-break backslashes from webhook payloads (#13950) | ||
|
|
3f9f054c43
|
fix: drop WhatsApp incoming messages from blocked contacts (#14061)
Some checks failed
Frontend Lint & Test / test (push) Has been cancelled
Publish Chatwoot EE docker images / build (linux/amd64, ubuntu-latest) (push) Has been cancelled
Publish Chatwoot EE docker images / build (linux/arm64, ubuntu-22.04-arm) (push) Has been cancelled
Publish Chatwoot CE docker images / build (linux/amd64, ubuntu-latest) (push) Has been cancelled
Publish Chatwoot CE docker images / build (linux/arm64, ubuntu-22.04-arm) (push) Has been cancelled
Run Chatwoot CE spec / lint-backend (push) Has been cancelled
Run Chatwoot CE spec / lint-frontend (push) Has been cancelled
Run Chatwoot CE spec / frontend-tests (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (0, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (1, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (10, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (11, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (12, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (13, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (14, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (15, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (2, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (3, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (4, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (5, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (6, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (7, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (8, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (9, 16) (push) Has been cancelled
Publish Chatwoot EE docker images / merge (push) Has been cancelled
Publish Chatwoot CE docker images / merge (push) Has been cancelled
## Linear ticket https://linear.app/chatwoot/issue/CW-6839/blocked-contact-can-still-send-messages-to-whatsapp-inbox ## Description Drop WhatsApp incoming messages from blocked contacts ## Type of change - [ ] Bug fix (non-breaking change which fixes an issue) ## How Has This Been Tested? - Incoming messages for blocked contacts ## 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 --------- Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com> |
||
|
|
45b6ea6b3f
|
feat: add automation condition to filter private notes (#12102)
## Summary Adds a new automation condition to filter private notes. This allows automation rules to explicitly include or exclude private notes instead of relying on implicit behavior. Fixes: #11208 ## Preview https://github.com/user-attachments/assets/c40f6910-7bbf-4e59-aae5-ad408602927a |
||
|
|
e5107604a0
|
feat: account enrichment using context.dev [UPM-27] (#13978)
## Account branding enrichment during signup This PR does the following ### Replace Firecrawl with Context.dev Switches the enterprise brand lookup from Firecrawl to Context.dev for better data quality, built-in caching, and automatic filtering of free/disposable email providers. The service interface changes from URL to email input to match Context.dev's email endpoint. OSS still falls back to basic HTML scraping with a normalized output shape across both paths. The enterprise path intentionally does not fall back to HTML scraping on failure — speed matters more than completeness. We want the user on the editable onboarding form fast, and a slow fallback scrape is worse than letting them fill it in. Requires `CONTEXT_DEV_API_KEY` in Super Admin → App Config. Without it, falls back to OSS HTML scraping. ### Add job to enrich account details After account creation, `Account::BrandingEnrichmentJob` looks up the signup email and pre-fills the account name, colors, logos, social links, and industry into `custom_attributes['brand_info']`. The job signals completion via a short-lived Redis key (30s TTL) + an ActionCable broadcast (`account.enrichment_completed`). The Redis key lets the frontend distinguish "still running" from "finished with no results." |
||
|
|
4cce7f6ad8
|
fix(line): Use non-expiring URLs for image and video messages (#13949)
Images and videos sent from Chatwoot to LINE inboxes fail to display on the LINE mobile app — users see expired markers, broken thumbnails, or missing images. This happens because LINE mobile lazy-loads images rather than downloading them immediately, and the ActiveStorage signed URLs expire after 5 minutes. Closes https://linear.app/chatwoot/issue/CW-6696/line-messaging-with-image-or-video-may-not-show-when-client-inactive ## How to reproduce 1. Create a LINE inbox and start a chat from the LINE mobile app 2. Close the LINE mobile app 3. Send an image from Chatwoot to that chat 4. Wait 7-8 minutes (past the 5-minute URL expiration) 5. Open the LINE mobile app — the image is broken/expired ## What changed - **`originalContentUrl`**: switched from `download_url` (signed, 5-min expiry) to `file_url` (permanent redirect-based URL) - **`previewImageUrl`**: switched to `thumb_url` (250px resized thumbnail meeting LINE's 1MB/240x240 recommendation), with fallback to `file_url` for non-image attachments like video 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Sojan Jose <sojan@pepalo.com> |
||
|
|
f2cb23d6e9
|
fix: handle Socket::ResolutionError in browser push notifications (#13957)
## Linear Ticket https://linear.app/chatwoot/issue/CW-6707/socketresolutionerror-failed-to-open-tcp-connection-to-permanently https://linear.app/chatwoot/issue/CW-6707/socketresolutionerror-failed-to-open-tcp-connection-to-permanently#comment-14e0f9ff ## Description Browser push notifications fail with Socket::ResolutionError when the push subscription endpoint's domain can't be resolved via DNS (e.g., defunct push service, transient DNS failure). This error wasn't handled in handle_browser_push_error, so it fell through to the catch-all else branch and got reported to Sentry on every notification attempt — 1,637 times in the last 7 days. The dead subscription was never cleaned up or the error suppressed, so every subsequent notification for the affected user triggered the same Sentry alert. Added Socket::ResolutionError to the existing transient network error handler alongside Errno::ECONNRESET, Net::OpenTimeout, and Net::ReadTimeout. The error is logged but not reported to Sentry, and the subscription is kept intact in case it's a temporary DNS blip. ## Type of change - [ ] Bug fix (non-breaking change which fixes an issue) ## How Has This Been Tested? - Verified that Socket::ResolutionError is a subclass of StandardError and matches the when clause ## 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 Co-authored-by: Vishnu Narayanan <iamwishnu@gmail.com> |
||
|
|
7651c18b48
|
feat: firecrawl branding api [UPM-15] (#13903)
Adds `WebsiteBrandingService` (OSS) with an Enterprise override using
Firecrawl v2 to extract branding and business data from a URL for
onboarding auto-fill.
OSS version uses HTTParty + Nokogiri to extract:
- Business name (og:site_name or title)
- Language (html lang)
- Favicon
- Social links from `<a>` tags
Enterprise version makes a single Firecrawl call to fetch:
- Structured JSON (name, language, industry via LLM)
- Branding (favicon, primary color)
- Page links
Falls back to OSS if Firecrawl is unavailable or fails.
Social handles (WhatsApp, Facebook, Instagram, Telegram, TikTok, LINE)
are parsed deterministically via a shared `SocialLinkParser`.
> We use links for socials, since the LLM extraction was unreliable,
mostly returned empty, and hallucinated in some rare scenarios
## How to test
```ruby
# OSS (no Firecrawl key needed)
WebsiteBrandingService.new('chatwoot.com').perform
# Enterprise (requires CAPTAIN_FIRECRAWL_API_KEY)
WebsiteBrandingService.new('notion.so').perform
WebsiteBrandingService.new('postman.com').perform
```
Verify the returned hash includes business_name, language,
industry_category, social_handles, and branding with
favicon/primary_color.
<img width="908" height="393" alt="image"
src="https://github.com/user-attachments/assets/e3696887-d366-485a-89a0-8e1a9698a788"
/>
|
||
|
|
d84ef4cfd6
|
fix(whatsapp): skip health check during reauthorization flow (#13911)
After a successful WhatsApp OAuth reauthorization, the health check runs immediately and finds the phone number in a pending provisioning state (`platform_type: NOT_APPLICABLE`). This incorrectly triggers `prompt_reauthorization!`, re-setting the Redis disconnect flag and sending a disconnect email — even though the reauth just succeeded. The fix skips the health check during reauthorization flows. It still runs for new channel creation. Closes https://github.com/chatwoot/chatwoot/pull/12556 ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) ## How to reproduce 1. Have a WhatsApp channel with a phone number in pending provisioning state (display name not yet approved by Meta) 2. Complete the OAuth reauthorization flow 3. Observe that the user receives a "success" response but immediately gets a disconnect email ## What changed - `Whatsapp::EmbeddedSignupService#perform` now skips `check_channel_health_and_prompt_reauth` when `inbox_id` is present (reauthorization flow) 🤖 Generated with [Claude Code](https://claude.com/claude-code) |
||
|
|
23786bcb52
|
chore: mark conversation notifications as read on visit (#13906) | ||
|
|
e4c3f0ac2f
|
feat: fallback on phone number to update lead (#13910)
When syncing contacts to LeadSquared, the `Lead.CreateOrUpdate` API defaults to searching by email. If a contact has no email (or a different email) but a phone number matching an existing lead, the API fails with `MXDuplicateEntryException` instead of finding and updating the existing lead. This accounted for ~69% of all LeadSquared integration errors, and cascaded into "Lead not found" failures when posting transcript and conversation activities (~14% of errors). ## What changed - `LeadClient#create_or_update_lead` now catches `MXDuplicateEntryException` and retries the request once with `SearchBy=Phone` appended to the body, telling the API to match on phone number instead - Once the retry succeeds, the returned lead ID is stored on the contact (existing behavior), so all future events use the direct `update_lead` path and never hit the duplicate error again ## How to reproduce 1. Create a lead in LeadSquared with phone number `+91-75076767676` and email `a@example.com` 2. In Chatwoot, create a contact with the same phone number but a different email (or no email) 3. Trigger a contact sync (via conversation creation or contact update) 4. Before fix: `MXDuplicateEntryException` error in logs, contact fails to sync 5. After fix: retry with `SearchBy=Phone` finds and updates the existing lead, stores the lead ID on the contact |
||
|
|
2b50909d9b
|
fix: use last_activity_at for orphan conversation cleanup timeframe (#13859)
## Description The RemoveOrphanConversationsService filters orphan conversations by a time window before deleting them. Previously it used created_at, which could miss old conversations that still had recent activity. Switching to last_activity_at ensures the cleanup window reflects actual conversation activity rather than creation time. ## Type of change Please delete options that are not relevant. - [ ] Bug fix (non-breaking change which fixes an issue) ## How Has This Been Tested? - By running Rake task - Run the job from console ## 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 |
||
|
|
9967101b48
|
feat(rollup): add models and write path [1/3] (#13796)
## PR#1: Reporting events rollup — model and write path Reporting queries currently hit the `reporting_events` table directly. This works, but the table grows linearly with event volume, and aggregation queries (counts, averages over date ranges) get progressively slower as accounts age. This PR introduces a pre-aggregated `reporting_events_rollups` table that stores daily per-metric, per-dimension (account/agent/inbox) totals. The write path is intentionally decoupled from the read path — rollup rows are written inline from the event listener via upsert, and a backfill service exists to rebuild historical data from raw events. Nothing reads from this table yet. The write path activates when an account has a `reporting_timezone` set (new account setting). The `reporting_events_rollup` feature flag controls only the future read path, not writes — so rollup data accumulates silently once timezone is configured. A `MetricRegistry` maps raw event names to rollup column semantics in one place, keeping the write and (future) read paths aligned. ### What changed - Migration for `reporting_events_rollups` with a unique composite index for upsert - `ReportingEventsRollup` model - `reporting_timezone` account setting with IANA timezone validation - `MetricRegistry` — single source of truth for event-to-metric mappings - `RollupService` — real-time upsert from event listener - `BackfillService` — rebuilds rollups for a given account + date from raw events - Rake tasks for interactive backfill and timezone setup - `reporting_events_rollup` feature flag (disabled by default) ### How to test 1. Set a `reporting_timezone` on an account (`Account.first.update!(reporting_timezone: 'Asia/Kolkata')`) 2. Resolve a conversation or trigger a first response 3. Check `ReportingEventsRollup.where(account_id: ...)` — rows should appear 4. Run backfill: `bundle exec rake reporting_events_rollup:backfill` and verify historical data populates --------- Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com> |
||
|
|
a452ce9e84
|
feat(whatsapp): add webhook registration and status endpoints (#13551)
## Description Adds webhook configuration management for WhatsApp Cloud API channels, allowing administrators to check webhook status and register webhooks directly from Chatwoot without accessing Meta Business Manager. ## Type of change - [ ] New feature (non-breaking change which adds functionality) ## Screenshots <img width="1130" height="676" alt="Screenshot 2026-03-05 at 7 04 18 PM" src="https://github.com/user-attachments/assets/f5dcd9dd-8827-42c5-a52b-1024012703c2" /> <img width="1101" height="651" alt="Screenshot 2026-03-05 at 7 04 29 PM" src="https://github.com/user-attachments/assets/e0bd59f9-2a90-4f24-87c0-b79f21e721ee" /> ## 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 --------- Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com> |
||
|
|
6e46be36c8
|
fix: Add fix to only allow confirmed agents to used in Agent Assingments at Macros/Automations (#13225)
Some checks failed
Frontend Lint & Test / test (push) Has been cancelled
Publish Chatwoot EE docker images / build (linux/amd64, ubuntu-latest) (push) Has been cancelled
Publish Chatwoot EE docker images / build (linux/arm64, ubuntu-22.04-arm) (push) Has been cancelled
Publish Chatwoot CE docker images / build (linux/amd64, ubuntu-latest) (push) Has been cancelled
Publish Chatwoot CE docker images / build (linux/arm64, ubuntu-22.04-arm) (push) Has been cancelled
Run Chatwoot CE spec / lint-backend (push) Has been cancelled
Run Chatwoot CE spec / lint-frontend (push) Has been cancelled
Run Chatwoot CE spec / frontend-tests (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (0, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (1, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (10, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (11, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (12, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (13, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (14, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (15, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (2, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (3, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (4, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (5, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (6, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (7, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (8, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (9, 16) (push) Has been cancelled
Publish Chatwoot EE docker images / merge (push) Has been cancelled
Publish Chatwoot CE docker images / merge (push) Has been cancelled
# Pull Request Template ## Description Unconfirmed agents (pending email verification) were incorrectly appearing in the "assign agent" dropdown for macros and automations. This fix filters out unconfirmed agents from these dropdowns and adds backend validation to prevent assignment of unconfirmed agents. Fixes #13223 ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) ## How Has This Been Tested? **Backend tests:** ```bash docker compose run --rm rails bundle exec rspec spec/services/action_service_spec.rb ``` - Added tests for confirmed agent assignment (should succeed) - Added tests for unconfirmed agent assignment (should be skipped) **Frontend tests:** ```bash docker compose run --rm rails pnpm test app/javascript/dashboard/composables/spec/useMacros.spec.js ``` - Updated mocks to use `getVerifiedAgents` getter **Manual testing:** 1. Create an unconfirmed agent via platform 2. Navigate to Settings → Macros → New Macro → Add "Assign Agent" action 3. Verify unconfirmed agent does NOT appear in dropdown 4. Navigate to Settings → Automations → New Automation → Add "Assign Agent" action 5. Verify unconfirmed agent does NOT appear in dropdown ## Checklist: - [x] My code follows the style guidelines of this project - [x] 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 - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules --------- Co-authored-by: Sojan Jose <sojan@pepalo.com> |
||
|
|
824164852c
|
refactor: extract custom attribute methods from FilterService (#13743)
- Extracted 6 custom attribute methods (`custom_attribute_query`, `attribute_model`, `attribute_data_type`, `build_custom_attr_query`, `custom_attribute`, `not_in_custom_attr_query`) into a new `Filters::CustomAttributeFilterHelper` module. - Added an inline `rubocop:disable` for the intentional `Lint/ShadowedException` in `coerce_lt_gt_value` — `Date::Error` is a subclass of `ArgumentError`, but both are listed explicitly for clarity. ## Why `app/services/filters/` The existing `Filters::FilterHelper` lives in `app/helpers/filters/`, but that location triggers `Rails/HelperInstanceVariable` for any module that uses instance variables. The extracted methods share state with `FilterService` via instance variables (`@attribute_key`, `@account`, `@custom_attribute`, etc.), so placing them in `app/helpers/` would require a cop disable. `app/services/filters/` is a better fit because: - The module is a service mixin, not a view helper — it's only included by `FilterService` and its subclasses (`Conversations::FilterService`, `Contacts::FilterService`, `AutomationRules::ConditionsFilterService`). - It sits alongside the services that use it. - No cop disables needed. --------- Co-authored-by: Vishnu Narayanan <iamwishnu@gmail.com> |