Commit Graph

6345 Commits

Author SHA1 Message Date
Vishnu Narayanan
de0bd8e71b
fix(perf): disable tags counter cache to prevent label deadlocks (#14021)
Label attach/detach against a shared label no longer deadlocks under
parallel load. During high-concurrency label writes (for example, a
broadcast script attaching a campaign label to many conversations at
once), Chatwoot previously hit periodic `ActiveRecord::Deadlocked`
errors and tail-latency spikes on the tags table. This PR removes the
contention by disabling the `acts-as-taggable-on` counter cache, which
Chatwoot never reads.

## Closes

Fixes [INF-68](https://linear.app/chatwoot/issue/INF-68) (event 2)

## How to reproduce

1. Seed an account with ~20 conversations and 5 labels.
2. Spawn 20 parallel threads, each calling
`conversation.update!(label_list: shared_labels.shuffle)` against
different conversations.
3. Observe `ActiveRecord::Deadlocked` exceptions and p99 label-write
latency well above 1s.

With the counter cache disabled, the deadlock cycle cannot form.

## How this was tested

- Ran a 20-thread synthetic load test locally, each thread attaching 5
shared labels (shuffled per request) to different conversations. With
the counter cache enabled: 8 deadlocks across 300 attempts, p99 ~2.2s.
With the counter cache disabled: zero deadlocks, p99 ~306ms (roughly 85%
tail-latency reduction). The `UPDATE tags SET taggings_count = ...`
statement disappears from the SQL log entirely.
- Verified at boot via `rails runner` that
`ActsAsTaggableOn::Tagging.reflect_on_association(:tag).options[:counter_cache]`
returns `false` after the initializer runs. The gem wires `belongs_to
:tag, counter_cache: ActsAsTaggableOn.tags_counter` at class-load time,
so the initializer must sit ahead of the `Tagging` autoload path; this
confirms it does.
2026-04-10 17:32:13 +05:30
Tanmay Deep Sharma
224b1f98b0
fix: handle ioerror in imap fetch (#13960)
## Description

The IMAP email fetch job (Inboxes::FetchImapEmailsJob) crashes with an
unhandled IOError: closed stream when the mail server's SSL socket is
closed mid-write during Net::IMAP#fetch. This error was being reported
to Sentry because the rescue clause only caught EOFError, not its parent
class IOError.

Fixes
[CW-6689](https://linear.app/chatwoot/issue/CW-6689/ioerror-closed-stream-ioerror)

Widened the rescue in fetch_imap_emails_job.rb from EOFError to IOError.

In Ruby's exception hierarchy, EOFError is a subclass of IOError:
```
StandardError
  └── IOError
        └── EOFError
```
The Sentry stacktrace shows a plain IOError: closed stream raised from
OpenSSL::Buffering#do_write → Net::IMAP#put_string → Net::IMAP#fetch.
Since this is an IOError (not EOFError), it bypassed the existing rescue
and fell through to the StandardError catch-all, which reported it to
Sentry as an unhandled exception.

Rescuing IOError now catches both:

IOError: closed stream — the reported crash (parent class)
EOFError — the previously handled case (still caught as a subclass)

## Type of change

- [ ] Bug fix (non-breaking change which fixes an issue)



## 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: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 13:31:28 +05:30
Pranav
3190b29fe9
fix(revert): "fix: Ignore RoutingError in New Relic error reporting (#14030)" (#14038)
This reverts commit 42163946eb.
2026-04-10 12:27:15 +05:30
Pranav
42163946eb
fix: Ignore RoutingError in New Relic error reporting (#14030)
Routing errors (404s) are expected in production and don't represent
actionable issues. Reporting them to New Relic creates noise and makes
it harder to spot real errors. Adds ActionController::RoutingError to
the New Relic error_collector.ignore_errors list so these are no longer
tracked as exceptions.
2026-04-10 11:42:44 +05:30
Gabriel Jablonski
b0a8fa70d0
fix(signatures): allow admins to manage inbox signatures without explicit membership (#260)
Administrators have access to all inboxes in an account but the
validate_inbox_access filter only checked InboxMember records, returning
401 for admins not explicitly added as inbox members.
2026-04-09 16:17:20 -03:00
Aakash Bakhle
f13f3ba446
fix: log only on system api key failures (#13968)
Removes sentry flooding of unnecessary rubyllm logs of wrong API key.
Logs only system api key error since it would be P0.

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 18:04:52 +05:30
Tanmay Deep Sharma
f1da7b8afa
feat: enable assignment v2 by default for new accounts (#14031)
## Description

Enable assignment v2 by default for new accounts

## Type of change

- [ ] New feature (non-breaking change which adds functionality)


## 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
2026-04-09 16:14:17 +05:30
Sivin Varghese
bd14e96ed9
chore: allow article to create without content (#14007) 2026-04-09 10:40:37 +05:30
Gabriel Jablonski
8cf6e8907f
release v4.12.0-fazer-ai.47 (#259)
* fix(whatsapp): add idempotent message sending to prevent duplicates on timeout retry

When sending media messages via Baileys, Net::ReadTimeout causes Sidekiq to
retry the job, potentially sending the same message multiple times. This adds
a chatwootMessageId parameter to the Baileys API request, enabling server-side
deduplication via Redis. Also increases HTTP timeout to 120s and channel lock
to 130s to reduce false timeouts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address review feedback

- Use error.class.name assertions for parallel/reloading safety
- Assert reconnect endpoint was not called on 409 (stronger assertion)

* fix: address review feedback (round 2)

- Only release channel lock in ensure if it was actually acquired (prevents
  clearing another worker's lock on timeout)
- Assert chatwootMessageId in reproduction spec body matcher

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 20:52:26 -03:00
zip-fa
00837019b5
fix(captain): display handoff message to customer in V2 flow (#13885)
HandoffTool changes conversation status but only posts a private note.
ResponseBuilderJob now detects the tool flag and creates the public
handoff message that was previously only shown in V1.

# Pull Request Template

## Description

Captain V2 was silently forwarding conversations to humans without
showing a handoff message to the customer. The conversation appeared to
just stop
responding.

Root cause: In V2, HandoffTool calls bot_handoff! during agent
execution, which changes conversation status from pending to open. By
the time control returns
to ResponseBuilderJob#process_response, the conversation_pending? guard
returns early - skipping create_handoff_message entirely. The V1 flow
didn't have this
problem because AssistantChatService just returns a string token
(conversation_handoff) and lets ResponseBuilderJob handle everything.

What changed:

1. AgentRunnerService now surfaces the handoff_tool_called flag (already
tracked internally for usage metadata) in its response hash.
2. ResponseBuilderJob#handoff_requested? detects handoffs from both V1
(response token) and V2 (tool flag).
3. ResponseBuilderJob#process_response checks handoff_requested? before
the conversation_pending? guard, so V2 handoffs are processed even when
the status has
already changed.
4. ResponseBuilderJob#process_action('handoff') captures
conversation_pending? before calling bot_handoff! and uses that snapshot
to guard both bot_handoff!
and the OOO message - preventing double-execution when V2's HandoffTool
already ran them.

New V2 handoff flow:
AgentRunnerService
  → agent calls HandoffTool (creates private note, calls bot_handoff!)
  → returns response with handoff_tool_called: true

ResponseBuilderJob#process_response
  → handoff_requested? detects the flag
  → process_action('handoff')
    → create_handoff_message (public message for customer)
    → bot_handoff! skipped (conversation_pending? is false)
    → OOO skipped (conversation_pending? is false)

Fixes #13881

## 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 existing response_builder_job_spec.rb covering the V2 handoff
path, V2 normal response path, and V1 regression
- Updated existing agent_runner_service_spec.rb expectations for the new
handoff_tool_called key and added a context for when the flag is true

## 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
- [x] Any dependent changes have been merged and published in downstream
modules

---------

Co-authored-by: Aakash Bakhle <48802744+aakashb95@users.noreply.github.com>
Co-authored-by: aakashb95 <aakashbakhle@gmail.com>
2026-04-08 17:30:07 +05:30
YJack0000
45124c3b41
fix(i18n): improve zh-TW translation coverage and quality (#14004)
Comprehensive update to Traditional Chinese (Taiwan) translations. As a
native zh-TW speaker and active user based in Taiwan, I found the
existing translations were quite incomplete (~54% overall) with many
strings still in English. Some existing translations also used
Simplified Chinese terms or unnatural phrasing.

I chose to submit this as a direct PR rather than going through Crowdin
because working through all the files at once is much faster and lets me
ensure consistent terminology across the entire locale.

Closes #14003

## What changed

**Backend (`config/locales/zh_TW.yml`)**
- Translated all ~259 previously untranslated strings (was ~19%
complete, now 100%)
- Covers: error messages, notifications, activity logs, integration
descriptions, Captain AI, public portal, reports

**Frontend (42 JSON files under `dashboard/i18n/locale/zh_TW/`)**
- Translated ~2,627 previously untranslated strings (was ~50% complete,
now ~100%)
- Most impacted files: `inboxMgmt.json`, `integrations.json`,
`settings.json`, `conversation.json`, `contact.json`, `report.json`

**Quality fixes across all files**
- Replaced Simplified Chinese terms mixed into zh-TW: 账→帳, 获→取得, 模板→範本,
收件箱→收件匣, 重置→重設, 自定義→自訂
- Standardized terminology for consistency: 客服人員 (agent), 延後 (snooze),
稽核 (audit), 巨集 (macro)
- Fixed incorrect translations (e.g., audit log table headers were
swapped, availability label was wrong)

## How to test

1. Set account/user language to 中文(台灣)
2. Navigate through the dashboard — settings, inbox management,
integrations, reports, conversations
3. Verify strings display in natural Traditional Chinese with no
remaining English gaps
4. Check that all placeholders (names, counts, dates) render correctly
2026-04-08 13:42:20 +05:30
Sivin Varghese
699b12b1d3
fix: Block inline images in message signatures (#13772)
# Pull Request Template

## Description

This PR includes, block inline images in message signatures and prevent
auto signature insertion when editor is disabled.

- Strip inline base64 images from signature on save and show warning
message
- Add `INLINE_IMAGE_WARNING` translation key for signature inline image
removal notification
- Add disabled check to `addSignature()` to prevent signature insertion
when editor is disabled
- Add `isEditorDisabled` checks to signature toggle logic in
`toggleSignatureForDraft()`, `replaceText()`, and `clearMessage()`
- Remove unused `replaceText` from the codebase, which belongs to old
`textarea` editor

Fixes
https://linear.app/chatwoot/issue/CW-6588/the-browser-hangs-when-the-message-signature-contains-inline-image

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## How Has This Been Tested?

### Loom video
https://www.loom.com/share/fb556b46a12a4308a737eed732d5ed73


## 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
- [ ] 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: Muhsin Keloth <muhsinkeramam@gmail.com>
2026-04-08 12:17:19 +05:30
Shivam Mishra
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."
2026-04-08 11:16:52 +05:30
Shivam Mishra
871f2f4d56
fix: harden fetching on upload endpoint (#14012) 2026-04-08 10:47:54 +05:30
Gabriel Jablonski
dba5379d5e
fix(whatsapp): include baileys and z-api in multi-attachment split (#256)
The ReplyBox multi-attachment split only covered Twilio, Cloud, and
360Dialog providers. Baileys and Z-API were added later and were
missing from the check, causing extra attachments to be silently
dropped. Use the generic isAWhatsAppChannel flag instead.
2026-04-07 20:13:29 -03:00
Shivam Mishra
4f94ad4a75
feat: ensure signup verification [UPM-14] (#13858)
Previously, signing up gave immediate access to the app. Now,
unconfirmed users are redirected to a verification page where they can
resend the confirmation email.

- After signup, the user is routed to `/auth/verify-email` instead of
the dashboard
- After login, unconfirmed users are redirected to the verification page
- The dashboard route guard catches unconfirmed users and redirects them
- `active_for_authentication?` is removed from the sessions controller
so unconfirmed users can authenticate — the frontend gates access
instead
- If the user visits the verification page after already confirming,
they're automatically redirected to the dashboard
- No session is issued until the user is verified

<details><summary>Demo</summary>
<p>

#### Fresh Signup


https://github.com/user-attachments/assets/abb735e5-7c8e-44a2-801c-96d9e4823e51

#### Google Fresh Signup


https://github.com/user-attachments/assets/ab9e389a-a604-4a9d-b492-219e6d94ee3f


#### Create new account from Dashboard


https://github.com/user-attachments/assets/c456690d-1946-4e0b-834b-ad8efcea8369



</p>
</details>

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2026-04-07 13:45:17 +05:30
Aakash Bakhle
fbe3560b7a
feat(captain): Add paywall and expose Custom Tools (#13977)
# Pull Request Template

## Description

Custom tools is now discoverable on all plans

## Type of change
- [x] Bug fix (non-breaking change which fixes an issue)

## How Has This Been Tested?

Before:
<img width="390" height="446" alt="CleanShot 2026-04-02 at 13 40 11@2x"
src="https://github.com/user-attachments/assets/0a751954-f3ad-47d6-85b8-1e2f1476a646"
/>


After:

<img width="392" height="522" alt="CleanShot 2026-04-02 at 13 40 47@2x"
src="https://github.com/user-attachments/assets/62a252f6-2551-47a9-b50c-be949f08c456"
/>

<img width="1826" height="638" alt="CleanShot 2026-04-02 at 13 37 39@2x"
src="https://github.com/user-attachments/assets/77dc2a75-3d76-44cf-8579-8d3457879bd0"
/>


## 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

---------

Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
2026-04-07 10:58:29 +05:30
gabrieljablonski
94c0827e50 feat(conversations): add per_page support to filter service
Allow customizable page size for conversation filter results via
per_page parameter, capped at 100. Defaults to CONVERSATION_RESULTS_PER_PAGE
env var or 25.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 19:09:09 -03:00
Muhsin Keloth
118270d2e8
fix(agent-bot): Update listener spec to match signed webhook arguments (#14006)
Fixes failing `agent_bot_listener_spec.rb` tests for
`conversation_status_changed` events. After #13892 added webhook
signing, `process_webhook_bot_event` passes `:agent_bot_webhook` and
`secret:`/`delivery_id:` kwargs to
`AgentBots::WebhookJob.perform_later`, but two spec expectations were
not updated to match the new call signature.

## What changed
- Updated `perform_later` expectations in `conversation_status_changed`
specs to include the `:agent_bot_webhook` type and `secret` keyword
arguments.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 16:45:47 +04:00
Captain
8c0c0fd32c
chore: Update translations (#13990)
Co-authored-by: Sojan Jose <sojan.official@gmail.com>
Co-authored-by: Sojan Jose <sojan@pepalo.com>
2026-04-06 15:35:59 +05:30
Muhsin Keloth
50d6ebaaca
fix(agent-bot): Dispatch conversation_status_changed event to agent bots (#14002)
Agent bots assigned to a conversation were not receiving
`conversation_status_changed` webhook events. This meant bots could not
react to status transitions like moving a conversation to **pending**.
The deprecated `conversation_opened` and `conversation_resolved` events
were still being delivered, but the newer unified
`conversation_status_changed` event was silently dropped because
`AgentBotListener` had no handler for it.


## What changed

- Added `conversation_status_changed` handler to `AgentBotListener`,
matching the pattern already used by `WebhookListener`. The payload
includes `changed_attributes` so bots know which status transition
occurred.

## How to test

1. Configure an agent bot with an `outgoing_url` (e.g. a webhook.site
endpoint).
2. Assign the bot to an inbox or conversation.
3. Change a conversation's status to **pending** (or any other status).
4. Verify the bot receives a `conversation_status_changed` event with
the correct `changed_attributes`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 14:05:50 +04:00
Shivam Mishra
95463230cb
feat: sign webhooks for API channel and agentbots (#13892)
Account webhooks sign outgoing payloads with HMAC-SHA256, but agent bot
and API inbox webhooks were delivered unsigned. This PR adds the same
signing to both.

Each model gets a dedicated `secret` column rather than reusing the
agent bot's `access_token` (for API auth back into Chatwoot) or the API
inbox's `hmac_token` (for inbound contact identity verification). These
serve different trust boundaries and shouldn't be coupled — rotating a
signing secret shouldn't invalidate API access or contact verification.

The existing `Webhooks::Trigger` already signs when a secret is present,
so the backend change is just passing `secret:` through to the jobs.
Shared token logic is extracted into a `WebhookSecretable` concern
included by `Webhook`, `AgentBot`, and `Channel::Api`. The frontend
reuses the existing `AccessToken` component for secret display. Secrets
are admin-only and excluded from enterprise audit logs.

### How to test

Point an agent bot or API inbox webhook URL at a request inspector. Send
a message and verify `X-Chatwoot-Signature` and `X-Chatwoot-Timestamp`
headers are present. Reset the secret from settings and confirm
subsequent deliveries use the new value.

---------

Co-authored-by: Sojan Jose <sojan@pepalo.com>
2026-04-06 15:28:25 +05:30
Muhsin Keloth
f4d66566d0
fix(agent-bot): Include changed_attributes in conversation_updated webhook (#14001)
The `conversation_updated` webhook sent to AgentBots did not include
`changed_attributes`, making it impossible for bots to distinguish
between different types of conversation updates (e.g. bot assignment vs
label change vs status change).

This aligns the AgentBot webhook payload with the existing
`WebhookListener` behavior, which already includes `changed_attributes`.

## How to reproduce

1. Assign an AgentBot to a conversation
2. Then update the conversation (e.g. add a label)
3. **Before fix:** Both events arrive with identical payload structure —
bot cannot tell them apart
4. **After fix:** Each event includes `changed_attributes` showing
exactly what changed

## What changed

- **`AgentBotListener#conversation_updated`**: Added
`changed_attributes` to the webhook payload using
`extract_changed_attributes` (same pattern as `WebhookListener`)

## How to test

1. Assign an AgentBot to a conversation via API
2. Check the webhook payload — should include:
   ```json
   "changed_attributes": [
{ "assignee_agent_bot_id": { "previous_value": null, "current_value": 7
} }
   ]
   ```
3. Update the conversation (e.g. add a label)
4. Check the webhook payload — `changed_attributes` should reflect the
label change, not bot assignment

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 11:14:09 +04:00
Tanmay Deep Sharma
5fd3d5e036
feat: allow zero conversation limit capacity policy (#13964)
## Description

Two improvements to Agent Capacity Policy:

**1. Support exclusion via zero conversation limit**
Allow `conversation_limit` to be `0` on inbox capacity limits. Agents
with a zero limit are excluded from auto-assignment for that inbox while
remaining members for manual assignment.

**2. Fix exclusion rules duration input**
- Default changed from `10` to `null` so time-based exclusion isn't
applied unless explicitly set.
- Minimum lowered from 10 to 1 minute.
- `DurationInput` updated to handle `null` values correctly.

## 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)

## How Has This Been Tested?

- Added model and capacity service specs for zero-limit exclusion
behavior.
- Tested manually via UI flows 

## 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>
2026-04-06 11:39:14 +05:30
Gabriel Jablonski
053e684261
feat(mailer): add i18n support for transactional emails with pt-BR (#255)
* feat(mailer): add i18n support for all transactional emails with pt-BR translations

- Create Liquid i18n filter (LiquidFilters::I18nFilter) exposing I18n.t() as `t` filter
  with interpolation support via positional args
- Replace hardcoded English strings in all 26 Liquid email templates with i18n keys
- Replace hardcoded English strings in all 5 Devise ERB email templates (OSS + enterprise)
  with t() calls, using raw output (<%==) to prevent HTML-encoding of apostrophes
- Translate all mailer subjects to use I18n.t() for locale-aware rendering
- Add comprehensive pt-BR translations for all transactional emails
- Add SSO-specific i18n keys for enterprise confirmation instructions
- Fix ApplicationRecord#to_drop to walk STI hierarchy (e.g. SuperAdmin -> UserDrop)

* fix: address CodeRabbit review feedback

- Use _html suffix for Devise translation keys to prevent XSS while
  keeping apostrophes unencoded (Rails escapes interpolations automatically)
- Replace hardcoded English in SLA templates with full_body i18n keys
- Fix _target="blank" typo to target="_blank" in attachment links
- Add i18n for tiktok_disconnect subject and create liquid template
- Extract brand_name to private method in AccountNotificationMailer

* fix: address round 2 review feedback

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 15:48:52 -03:00
gabrieljablonski
0b6bc691ad revert: undo mailer i18n commits for PR-based rework 2026-04-03 13:49:06 -03:00
gabrieljablonski
ca765f5645 fix(mailer): update enterprise Devise template and fix HTML escaping in email translations
- Add i18n to enterprise confirmation_instructions.html.erb (SAML/SSO support)
- Add SSO-specific translation keys (en + pt_BR)
- Use raw output (<%==) in Devise ERB templates to prevent apostrophe HTML-encoding
2026-04-03 13:43:49 -03:00
Gabriel Jablonski
60839b9dd6
feat(mailer): add i18n support for all transactional emails with pt-BR translations (#254)
- Create Liquid i18n filter (LiquidFilters::I18nFilter) exposing I18n.t() as `t` filter
- Replace hardcoded English strings in all 26 Liquid email templates with i18n keys
- Replace hardcoded English strings in all 4 Devise ERB email templates with t() calls
- Translate all mailer subjects to use I18n.t() for locale-aware rendering
- Add comprehensive pt-BR translations for all transactional emails
- Fix ApplicationRecord#to_drop to walk STI hierarchy (SuperAdmin -> User)
2026-04-03 13:32:34 -03:00
Gabriel Jablonski
84242aae7b
release v4.12.0-fazer-ai.44 (#253)
* fix(whatsapp): use payload filename for documents to avoid Content-Disposition parsing issues

When receiving documents via the WhatsApp Cloud API, filenames with
spaces or special characters were mangled due to the Down gem's
case-sensitive Content-Disposition header parsing. Now uses the
filename from the WhatsApp message payload when available, falling
back to Content-Disposition for other attachment types.

* fix: scope payload filename preference to document messages only

* fix: revert document-only scope, apply payload filename for all attachment types
2026-04-03 12:03:12 -03:00
Tanmay Deep Sharma
6f5ad8f372
fix: strip manually_managed_features from params in super admin account create (#13983)
## Summary

When a Super Admin creates a new account via the Administrate dashboard,
the `manually_managed_features` field (a virtual attribute stored in
`internal_attributes` JSON) is passed to `Account.new(...)`, raising
`ActiveModel::UnknownAttributeError`. The existing `update` action
already strips this param — this fix adds the same handling to `create`.

Closes -> https://linear.app/chatwoot/issue/INF-66
Related Sentry ->
https://chatwoot-p3.sentry.io/issues/7168237533/?project=6382945&referrer=Linear

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## How to reproduce

1. Log in as Super Admin
2. Navigate to Accounts → New
3. Fill in the form (with or without manually managed features selected)
4. Submit → `ActiveModel::UnknownAttributeError: unknown attribute
'manually_managed_features' for Account`

## What changed

- Added a `create` override in
`Enterprise::SuperAdmin::AccountsController` that strips
`manually_managed_features` from params before calling `super`, then
persists them via `InternalAttributesService` after the account is
saved.
2026-04-02 19:58:43 +05:30
Pranav
441fe4db11
fix: scope external_url override to Instagram DM conversations only (#13982)
Previously, all incoming messages from Facebook channel with
instagram_id had their attachment data_url and thumb_url overridden with
external_url. This caused issues for non-Instagram conversations
originating from Facebook Message where the file URL should be used
instead.

Narrows the override to only apply when the conversation type is
instagram_direct_message, which is the only case where Instagram's CDN
URLs need to be used directly.

Fixes
https://linear.app/chatwoot/issue/CW-6722/videos-are-missing-in-facebook-conversation

---------

Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 19:56:23 +05:30
Shivam Mishra
b9b5a18767
revert: html background for widget (#13981)
Reverts chatwoot/chatwoot#13955
2026-04-02 16:02:22 +05:30
Muhsin Keloth
b815eb9ce0
fix(agent-bot): Dispatch webhook event on agent bot assignment (#13975)
When an AgentBot is assigned to a conversation after the first message
has already been received, the bot does not respond because it never
receives any event. The `message_created` event fires before the bot is
assigned, and the bot has no way to know it was assigned.

Chatwoot already dispatches a `CONVERSATION_UPDATED` event when
`assignee_agent_bot_id` changes, but `AgentBotListener` wasn't listening
for it. This fix adds a `conversation_updated` handler so the bot
receives a webhook with the conversation context when assigned.

## How to reproduce

1. Customer sends a message → conversation created, `message_created`
fires
2. System processes the message (adds labels, custom attributes)
3. System assigns an AgentBot to the conversation via API
4. **Before fix:** Bot receives no event and never responds
5. **After fix:** Bot receives `conversation_updated` event with
conversation payload

## What changed

- **`AgentBotListener`**: Added `conversation_updated` handler that
sends the conversation webhook payload to the assigned bot when the
conversation is updated

## How to test

1. Create an AgentBot with an `outgoing_url` pointing to a webhook
inspector (e.g. webhook.site)
2. Send a message to create a conversation
3. Assign the AgentBot to the conversation via API:
   ```
   POST /api/v1/accounts/{id}/conversations/{id}/assignments
   { "assignee_id": <bot_id>, "assignee_type": "AgentBot" }
   ```
4. Verify the bot receives a `conversation_updated` event at its webhook
URL

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 13:55:05 +04:00
Muhsin Keloth
b3d0af84c4
fix(widget): Queue SDK-set conversation attributes and labels for first message (#13912)
### Description

When integrating the web widget via the JS SDK, customers call
setConversationCustomAttributes and setLabel on chatwoot:ready — before
any conversation exists. These API calls silently fail because the
backend endpoints require an existing conversation. When the visitor
sends their first message, the conversation is created without those
attributes/labels, so the message_created webhook payload is missing the
expected metadata.

This change queues SDK-set conversation custom attributes and labels in
the widget store when no conversation exists yet, and includes them in
the API request when the first message (or attachment) creates the
conversation. The backend now permits and applies these params during
conversation creation — before the message is saved and webhooks fire.

###  How to test

  1. Configure a web widget without a pre-chat form.
2. Open the widget on a test page and run the following in the browser
console after chatwoot:ready:
`window.$chatwoot.setConversationCustomAttributes({ plan: 'enterprise'
});`
`window.$chatwoot.setLabel('vip');` // must be a label that exists in
the account
  3. Send the first message from the widget.
4. Verify in the Chatwoot dashboard that the conversation has plan:
enterprise in custom attributes and the vip label applied.
5. Set up a webhook subscriber for `message_created` confirm the first
payload includes the conversation metadata.
6. Verify that calling `setConversationCustomAttributes` / `setLabel` on
an existing conversation still works as before (direct API path, no
regression).
  7. Verify the pre-chat form flow still works as expected.
2026-04-02 12:09:24 +04:00
Muhsin Keloth
d83beb2148
fix: Populate extension and include content_type in attachment webhook payload (#13945)
Attachment webhook event payloads (`message_created`) were missing the
file extension and content type. The `extension` column existed but was
never populated, and `content_type` was not included in the payload at
all.

## What changed

- Added `before_save :set_extension` callback to extract file extension
from the filename when saving an attachment.
- Added `content_type` (from ActiveStorage) to the `file_metadata` used
in `push_event_data`.

### Before
```json
{
  "extension": null,
  "data_url": "...",
  "file_size": 11960
}
```

### After
```json
{
  "extension": "pdf",
  "content_type": "application/pdf",
  "data_url": "...",
  "file_size": 11960
}
```

## How to reproduce
1. Send a message with a file attachment (e.g., PDF) via any channel
2. Inspect the `message_created` webhook payload
3. Observe `extension` is `null` and `content_type` is missing

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 11:13:11 +04:00
Aakash Bakhle
8daf6cf6cb
feat: captain custom tools v1 (#13890)
# Pull Request Template

## Description

Adds custom tool support to v1

## Type of change
- [x] New feature (non-breaking change which adds functionality)


## 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.

<img width="1816" height="958" alt="CleanShot 2026-03-24 at 11 37 33@2x"
src="https://github.com/user-attachments/assets/2777a953-8b65-4a2d-88ec-39f395b3fb47"
/>

<img width="378" height="488" alt="CleanShot 2026-03-24 at 11 38 18@2x"
src="https://github.com/user-attachments/assets/f6973c99-efd0-40e4-90fe-4472a2f63cea"
/>

<img width="1884" height="1452" alt="CleanShot 2026-03-24 at 11 38
32@2x"
src="https://github.com/user-attachments/assets/9fba4fc4-0c33-46da-888a-52ec6bad6130"
/>



## Checklist:

- [x] 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: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
2026-04-02 12:40:11 +05:30
Shivam Mishra
211fb1102d
chore: rotate oauth password if unconfirmed (#13878)
When a user signs up with an email they don't own and sets a password,
that password remains valid even after the real owner later signs in via
OAuth. This means the original registrant — who never proved ownership
of the email — retains working credentials on the account. This change
closes that gap by rotating the password to a random value whenever an
unconfirmed user completes an OAuth sign-in.

The check (`oauth_user_needs_password_reset?`) is evaluated before
`skip_confirmation!` runs, since confirmation would flip `confirmed_at`
and mask the condition. If the user was unconfirmed, the stored password
is replaced with a secure random string that satisfies the password
policy. This applies to both the web and mobile OAuth callback paths, as
well as the sign-up path where the password is rotated before the reset
token is generated.

Users who lose access to password-based login as a side effect can
recover through the standard "Forgot password" flow at any time. Since
they've already proven email ownership via OAuth, this is a low-friction
recovery path
2026-04-02 11:26:29 +05:30
Sivin Varghese
7b09b033ef
fix: Markdown tables don't render properly in help centre (#13971)
# Pull Request Template

## Description

This PR fixes an issue where markdown tables were not rendering
correctly in the Help Center.

The issue was caused by a backslash `(\)` being appended after table row
separators `(|)`, which breaks the markdown table parsing.

The issue was introduced after recent editor changes made to preserve
new lines, which unintentionally affected how table markdown is parsed
and displayed.

### https://github.com/chatwoot/prosemirror-schema/pull/44

Fixes
https://linear.app/chatwoot/issue/CW-6714/markdown-tables-dont-render-properly-in-help-centre-preview

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## How Has This Been Tested?

**Before**
```
| Type         | What you provide              |\
|--------------|-------------------------------|\
| None         | No authentication             |\
| Bearer Token | A token string                |\
| Basic Auth   | Username and password         |\
| API Key      | A custom header name and value|
```

**After**
```
| Type         | What you provide              | 
|--------------|-------------------------------| 
| None         | No authentication             | 
| Bearer Token | A token string                | 
| Basic Auth   | Username and password         | 
| API Key      | A custom header name and value|
```




## 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
- [ ] 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
2026-04-02 11:02:21 +05:30
Gabriel Jablonski
79c193ee9e
fix(whatsapp): resolve phone_number conflict when converting inbox between providers (#252)
* fix(whatsapp): resolve phone_number conflict when converting inbox between providers

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: move transfer_identifier_to inside transaction for atomic consolidation

* fix: reassign message sender before destroying merged contact

* fix: resolve identifier conflicts account-wide in adopt and consolidate paths

* fix: scope sender reassignment to moved conversations only

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 16:51:38 -03:00
Vishnu Narayanan
65867b8b36
fix: exclude MutexApplicationJob::LockAcquisitionError from Sentry (#13965)
## Summary
- Add `MutexApplicationJob::LockAcquisitionError` to Sentry's
`excluded_exceptions`
- This error is expected control flow (mutex lock contention during
webhook processing), not a bug
- Generated ~131K Sentry events in March 2026, 100% from
`InstagramEventsJob`

Fixes https://linear.app/chatwoot/issue/INF-58
2026-04-01 18:02:19 +05:30
Muhsin Keloth
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>
2026-04-01 17:29:12 +05:30
Tanmay Deep Sharma
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>
2026-04-01 16:55:49 +05:30
Sivin Varghese
8824efe0e1
fix(sentry): syntaxError: No error message (#13954) 2026-03-31 21:09:02 +05:30
Sivin Varghese
5de7ae492c
fix: html/body background not applied in appearance mode (#13955)
# Pull Request Template

## Description

This PR fixes the white background bleed visible in the widget, widget
article viewer and help center when dark mode is active.

**What was happening**

While scrolling, the `<body>` element retained a white background in
dark mode. This occurred because dark mode classes were only applied to
inner container elements, not the root.

**What changed**

* **Widget:** Updated the `useDarkMode` composable to sync the `dark`
class to `<html>` using `watchEffect`, allowing `<body>` to inherit dark
theme variables. Also added background styles to `html`, `body`, and
`#app` in `woot.scss`.
* **Help center portal:** Moved `bg-white dark:bg-slate-900` from
`<main>` to `<body>` in the portal layout so the entire page background
responds correctly to dark mode, including within the widget iframe.
* **ArticleViewer:** Replaced hardcoded `bg-white` with `bg-n-solid-1`
to ensure better theming.


Fixes
https://linear.app/chatwoot/issue/CW-6704/widget-body-colour-not-implemented

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## How Has This Been Tested?

### Screencasts

### Before

**Widget**


https://github.com/user-attachments/assets/e0224ad1-81a6-440a-a824-e115fb806728

**Help center**


https://github.com/user-attachments/assets/40a8ded5-5360-474d-9ec5-fd23e037c845



### After

**Widget**


https://github.com/user-attachments/assets/dd37cc68-99fc-4d60-b2ae-cf41f9d4d38c

**Help center**


https://github.com/user-attachments/assets/bc998c4e-ef77-46fa-ac7f-4ea16d912ce3




## 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
- [ ] 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
2026-03-31 16:55:21 +05:30
Aakash Bakhle
b4b5de9b46
fix: conservative hand_off prompt on auto-resolution (#13953)
# Pull Request Template

## Description

The initial version of prompt deciding to resolve or hand-off to human
agents was too conservative especially in cases where a link or an
action was told to customer. If the customer didn't respond, Captain was
told to hand it off to the agent, but customer may actually have solved
the issue. If not, they can come back and continue the conversation.

Removed two lines about the same and now we should not see needless
handoffs.

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## 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.
locally

## 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
- [ ] 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
2026-03-31 11:10:12 +05:30
Tanmay Deep Sharma
1987ac3d97
fix: remove bulk_auto_assignment_job cron schedule (#13877) 2026-03-31 10:56:59 +05:30
Sivin Varghese
0012fa2c35
fix: align message trimming with configured maxLength (#13947)
# Pull Request Template

## Description

This PR fixes 
1. Messages being trimmed to the default 1024 limit in `trimContent`
method, instead of channel-specific limits for drafts and AI tasks.
2. Telegram messages are allowed up to 10,000 characters in config, but
the API supports only 4096, causing failures for oversized messages.

Fixes
https://linear.app/chatwoot/issue/CW-6694/captain-ai-rewrite-tasks-truncate-draft-to-1024-chars-trimcontent
https://github.com/chatwoot/chatwoot/issues/13919

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## How Has This Been Tested?

### Loom video

**Before**
https://www.loom.com/share/00e9d6b4d19247febf35dffa99da3805

**After**
https://www.loom.com/share/c4900e9effc345c79bcd8a5aa1ee277b


## 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
- [ ] 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
2026-03-31 10:39:54 +05:30
Aakash Bakhle
b4ce59eea8
feat: reclaim response_bot flag for custom_tools (#13897)
Repurpose the deprecated response_bot feature flag slot for
custom_tools.

Migration disables the flag on any accounts that had response_bot
enabled so the repurposed slot starts in its default-off state.

Pre-deploy: run the disable script on production using the old flag name
(response_bot) before deploying this migration.
2026-03-31 10:35:50 +05:30
Sivin Varghese
42441dbd28
feat: add GuideJar embed support in HC (#13944) 2026-03-30 14:19:02 +05:30
Alok Dangre
b9f824b43b
fix(ui): resolve unreadable select options in dark mode (#13207) 2026-03-30 13:05:28 +05:30