Compare commits

...
Author SHA1 Message Date
Sony MathewandGitHub ba742e3431 Merge branch 'develop' into codex/update-msgpack-security 2026-06-30 14:43:00 +05:30
Sony Mathew bd2468477f chore(deps): update msgpack 2026-06-30 14:39:11 +05:30
Shivam MishraandGitHub 8670f66155 feat: broaden search scope for help center generation (#14880)
**Broaden the Firecrawl `map` search term list so help-center onboarding
doesn't skip sites with non-standard docs paths**

Help-center onboarding was skipping ~70% of new accounts, and ~60% of
those skips came from a single failure: `"map returned no links"`. The
root cause was an overly narrow hardcoded search query passed to
Firecrawl's `map` endpoint.

The curator (`Onboarding::HelpCenterCurator`) calls `Firecrawl.map(url,
search: MAP_SEARCH)` to discover candidate pages before the LLM curation
step. `MAP_SEARCH` was hardcoded to `"docs help support faq"` — a 4-term
list that only matched sites whose help content sat at `/docs`, `/help`,
`/support`, or `/faq`. Sites using `/resources`, `/guides`, `/kb`,
`/articles`, `/handbook`, `/learn`, `/how-to`, `/tutorial`,
`/troubleshooting`, or a docs subdomain found nothing, so the job raised
`CurationSkipped` and left the portal empty.

Firecrawl's `search` param is a grep-style substring filter across URL,
title, and description (not a semantic query), so the fix is to broaden
the term list rather than drop it. The LLM curator downstream
(`Captain::Llm::HelpCenterCurationService`) already filters returned
links by quality — it has the full URL-path-priority prompt and a
25-article hard ceiling — so a wider crawl net is safe and doesn't
change the final article quality bar.

**What changed**

- `enterprise/app/services/onboarding/help_center_curator.rb`:
`MAP_SEARCH` broadened from `"docs help support faq"` to a 13-term list
covering common help-content path hints. Comment added documenting why
the term list exists and that the LLM curator does the real filtering.
2026-06-30 14:37:15 +05:30
Sivin VargheseandGitHub 2767bd434b fix: Show a clear error when message translation fails (#14891) 2026-06-30 14:23:47 +05:30
Aakash BakhleandGitHub ce2e10e89e fix: tighten captain v2 (#14883)
# Pull Request Template

## Description

Tightens v2 prompt and config to match v1

## Type of change

Please delete options that are not relevant.

- [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-06-30 14:20:54 +05:30
Sivin VargheseandGitHub 56275b750e fix: respect companies feature flag for auto-association (#14886)
# Pull Request Template

## Description

This PR stops new contacts from getting an auto-assigned company name
when the Companies feature is disabled.

Since #14496, email-domain company auto-association also updates a
contact's `company_name`. However, the callback isn't gated behind the
Companies feature flag, so accounts without the feature enabled still
auto-create companies and overwrite any `company_name` provided via the
SDK/`setUser`.

This PR gates `should_associate_company?` behind
`account.feature_enabled?('companies')`, so auto-association only runs
when the Companies feature is enabled.

Fixes
https://linear.app/chatwoot/issue/CW-7462/setuser-overwrites-contact-company-name-for-accounts-that-dont-use

## Type of change

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


## 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
- [ ] Any dependent changes have been merged and published in downstream
modules
2026-06-29 17:23:31 -07:00
299bc6c0a4 fix: recover from stale LeadSquared lead ids on activity sync (#14818)
LeadSquared sync now recovers automatically when a contact's cached lead
has been deleted or merged on the LeadSquared side. Previously the stale
lead id was never cleared, so every new conversation or contact update
for that contact failed with "Lead not found"
(`MXInvalidEntityReferenceException`) indefinitely.

## What changed
- Activity sync: on a "Lead not found" error while posting a
conversation/transcript activity, clear the cached `leadsquared_id`,
re-resolve the contact to a fresh lead, and retry the activity once
(guarded against loops and duplicate leads).
- Contact sync: on the same error while updating an existing lead, clear
the cached id and create a fresh lead instead.
- Fix `get_lead_id` to actually return early for unidentifiable contacts
(the guard previously fell through).

## How to reproduce
1. For a LeadSquared-enabled account, point a contact's cached lead id
at a lead that no longer exists in LeadSquared.
2. Update the contact, or create/resolve a conversation for it.
3. Before: the sync fails repeatedly with "Lead not found" and never
self-corrects. After: the stale id is cleared, a fresh lead is
resolved/created, and subsequent syncs reuse the healed id.

---------

Co-authored-by: Tanmay Deep Sharma <32020192+tds-1@users.noreply.github.com>
2026-06-29 16:38:08 +05:30
7522457740 feat: v2 - generations get trace level attributes (#14878)
# Pull Request Template

~~Note: merge only after https://github.com/chatwoot/ai-agents/pull/74
has been merged~~

## Description

Before:
<img width="436" height="617" alt="image"
src="https://github.com/user-attachments/assets/fd5be8dc-abab-4e01-b251-366648b998ea"
/>


After:
<img width="446" height="577" alt="image"
src="https://github.com/user-attachments/assets/8d96d2f2-c126-4cca-b3a2-f2a62b204adf"
/>


<img width="441" height="558" alt="image"
src="https://github.com/user-attachments/assets/89da4157-48a5-4fdc-9c67-9bf987e31638"
/>



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

locally and specs

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

---------

Co-authored-by: Sony Mathew <sony@chatwoot.com>
2026-06-29 13:50:17 +05:30
Sony MathewandGitHub e4ef2de8c8 fix(security): Update crass to 1.0.7 (#14882)
## Description

Updates the transitive `crass` dependency from `1.0.6` to `1.0.7` so the
bundle-audit security check no longer flags the Crass denial-of-service
advisories published on June 25, 2026. `crass` is pulled in through
`rails-html-sanitizer -> loofah`, and this change only updates the
resolved lockfile version.

Fixes # N/A

## Type of change

- [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?

- `bundle exec bundle audit update && bundle exec bundle audit check -v`
- `bundle exec rspec spec/mailboxes/mailbox_helper_spec.rb
spec/mailboxes/reply_mailbox_spec.rb
spec/mailboxes/imap/imap_mailbox_spec.rb
spec/models/channel/telegram_spec.rb
spec/lib/integrations/slack/send_on_slack_service_spec.rb
spec/lib/integrations/slack/update_slack_message_service_spec.rb
spec/presenters/html_parser_spec.rb`
- `bundle exec rubocop Gemfile`

## 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-06-29 13:47:57 +05:30
Sojan JoseandGitHub d0b1c055e8 chore: Track cloud plan activation conversions (#14834)
## Summary
- track cloud plan activation conversions when an attributed account
moves from the configured default cloud plan to a paid plan
- use the Stripe webhook event time as the activation timestamp so the
30-day signup attribution window reflects the actual upgrade event
- send the Stripe subscription amount and currency for the conversion
value
- mark the account attribution after enqueueing so later plan updates do
not send duplicate activation conversions

## Notes
- Marketing tracker: https://linear.app/chatwoot/issue/MAR-113
- Cloud implementation: https://linear.app/chatwoot/issue/LEA-34
- Stripe billing stays responsible for subscription state and value
calculation.
- Cloud plan activation conversion tracking is handled by a small
dedicated service that owns the activation rule, duplicate marker, and
conversion enqueue.
- Website attribution cookie capture remains separate in the marketing
attribution service.
- There is no frontend change and no new user-facing configuration.
- Conversion upload still no-ops outside Chatwoot Cloud and when
attribution has no supported click identifier.
2026-06-25 18:41:43 -07:00
Sony MathewandGitHub cf134deb37 fix: Preserve Captain LLM defaults (#14858)
# Pull Request Template

## Description

Adjusts the Captain LLM feature defaults after feature routing so the
defaults stay intentional and avoid unintended high-cost model upgrades.
Assistant, copilot, and onboarding content generation now default to
`gpt-4.1`; audio transcription keeps `gpt-4o-mini-transcribe` as the
default while exposing `whisper-1` as an available account override
option.

Related: https://linear.app/chatwoot/issue/CW-7425/test-new-models

## Type of change

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

## How Has This Been Tested?

- `ruby -ryaml -e 'yaml = YAML.load_file("config/llm.yml"); features =
yaml.fetch("features"); models = yaml.fetch("models"); features.each {
|name, cfg| missing = Array(cfg["models"]) - models.keys; raise
"#{name}: missing #{missing.join(",")}" if missing.any?; raise "#{name}:
default not in models" unless
Array(cfg["models"]).include?(cfg["default"]) }'`
- `node -e
"JSON.parse(require('fs').readFileSync('app/javascript/dashboard/i18n/locale/en/settings.json',
'utf8'))"`
- `pnpm exec prettier --check
app/javascript/dashboard/i18n/locale/en/settings.json`
- `bundle exec rspec spec/lib/llm/feature_router_spec.rb
spec/enterprise/services/llm/base_ai_service_spec.rb
spec/enterprise/models/concerns/agentable_spec.rb
spec/enterprise/services/messages/audio_transcription_service_spec.rb
spec/models/concerns/captain_featurable_spec.rb
spec/models/account_spec.rb
spec/controllers/api/v1/accounts/captain/preferences_controller_spec.rb
spec/controllers/super_admin/accounts_controller_spec.rb`
- `bundle exec rspec
spec/enterprise/services/captain/llm/article_translation_service_spec.rb
spec/enterprise/services/messages/audio_transcription_service_spec.rb
spec/enterprise/services/llm/base_ai_service_spec.rb`
- `RUBOCOP_CACHE_ROOT=/private/tmp/rubocop_cache bundle exec rubocop
enterprise/app/services/captain/llm/article_translation_service.rb
enterprise/app/services/messages/audio_transcription_service.rb
spec/enterprise/services/captain/llm/article_translation_service_spec.rb
spec/enterprise/services/llm/base_ai_service_spec.rb
spec/enterprise/services/messages/audio_transcription_service_spec.rb`
- `git diff --check`

## 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
- [ ] Any dependent changes have been merged and published in downstream
modules
2026-06-25 21:35:09 +05:30
Sony MathewandGitHub b8b62ad0f1 feat: Harden model override preferences (5/6) (#14846)
## Description

Hardens the Captain model override preferences API so account-level
overrides follow the same feature-router contract used by runtime LLM
calls. The API now permits model and feature keys from `llm.yml`,
removes blank model overrides, rejects invalid saved model combinations,
and returns each feature's effective model, provider, and source for UI
clients.

Fixes https://linear.app/chatwoot/issue/CW-7425/test-new-models

## Type of change

- [ ] 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?

Verified the account preferences API and account model validation
behavior for valid overrides, invalid model values, unknown feature
keys, blank override removal, and effective model/provider/source
payload metadata.

- `eval "$(rbenv init -)" && bundle exec rspec
spec/controllers/api/v1/accounts/captain/preferences_controller_spec.rb
spec/models/account_spec.rb
spec/models/concerns/captain_featurable_spec.rb
spec/lib/llm/feature_router_spec.rb`
- `eval "$(rbenv init -)" && bundle exec rubocop
app/controllers/api/v1/accounts/captain/preferences_controller.rb
app/models/concerns/account_settings_schema.rb
app/models/concerns/captain_featurable.rb
spec/controllers/api/v1/accounts/captain/preferences_controller_spec.rb
spec/models/account_spec.rb
spec/models/concerns/captain_featurable_spec.rb
spec/lib/llm/feature_router_spec.rb`
- `git diff --check`

## 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
- [ ] 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
2026-06-25 17:38:11 +05:30
Sony MathewandGitHub 4e26c5b4bb feat: Route system LLM jobs (4/6) (#14843)
## Description

Routes the remaining system-only and legacy-sensitive LLM jobs through
feature-level model configuration, while preserving system credential
usage and usage-accounting behavior. This adds dedicated defaults for
help center article generation, onboarding content generation, query
translation, transcription, and search embeddings so these flows can be
configured per account without falling back to installation-wide model
settings.

Fixes https://linear.app/chatwoot/issue/CW-7425/test-new-models

## Type of change

- [ ] 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?

Verified the feature routing defaults and account overrides for the
touched Captain/system LLM paths, including the legacy OpenAI
transcription and paginated FAQ services.

- `eval "$(rbenv init -)" && bundle exec rspec
spec/lib/captain/base_task_service_spec.rb spec/lib/llm/models_spec.rb
spec/models/concerns/captain_featurable_spec.rb
spec/controllers/api/v1/accounts/captain/preferences_controller_spec.rb
spec/enterprise/services/captain/llm/paginated_faq_generator_service_spec.rb
spec/enterprise/services/captain/llm/pdf_processing_service_spec.rb
spec/enterprise/services/messages/audio_transcription_service_spec.rb
spec/enterprise/services/onboarding/help_center_article_builder_spec.rb
spec/enterprise/services/captain/onboarding/website_analyzer_service_spec.rb`
- `eval "$(rbenv init -)" && bundle exec rubocop
app/controllers/api/v1/accounts/captain/preferences_controller.rb
app/models/concerns/account_settings_schema.rb
lib/captain/base_task_service.rb
enterprise/app/services/captain/llm/article_translation_service.rb
enterprise/app/services/captain/llm/article_writer_service.rb
enterprise/app/services/captain/llm/embedding_service.rb
enterprise/app/services/captain/llm/help_center_curation_service.rb
enterprise/app/services/captain/llm/paginated_faq_generator_service.rb
enterprise/app/services/captain/llm/translate_query_service.rb
enterprise/app/services/captain/llm/widget_tagline_service.rb
enterprise/app/services/captain/onboarding/website_analyzer_service.rb
enterprise/app/services/messages/audio_transcription_service.rb
spec/enterprise/services/messages/audio_transcription_service_spec.rb
spec/lib/captain/base_task_service_spec.rb`
- `ruby -e "require 'yaml'; config = YAML.load_file('config/llm.yml');
%w[document_faq_generation help_center_article_generation
onboarding_content_generation help_center_query_translation
audio_transcription help_center_search].each { |feature| abort(%(missing
#{feature})) unless config.dig('features', feature) }; abort('wrong
article default') unless config.dig('features',
'help_center_article_generation', 'default') == 'gpt-5.2'; puts 'llm.yml
ok'"`
- `git diff --check`

## 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
- [ ] Any dependent changes have been merged and published in downstream
modules
2026-06-25 17:37:45 +05:30
Sony MathewandGitHub 43e0f8a141 feat: Route Enterprise LLM services (3/6) (#14841)
# Pull Request Template

## Description

Routes Enterprise assistant, copilot, FAQ, contact memory,
action-classifier, and false-promise detector LLM paths through
feature-specific model resolution. `Llm::BaseAiService` now accepts
feature/account context and uses `Llm::FeatureRouter` when that context
is present, while retaining the installation-model fallback for
unmigrated callers. This also adds a `document_faq_generation` feature
default for generative FAQ/document content.

Linear: https://linear.app/chatwoot/issue/CW-7425/test-new-models
Depends on #14840

## Type of change

- [ ] 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?

- `bundle exec rspec spec/lib/llm/models_spec.rb
spec/enterprise/services/llm/base_ai_service_spec.rb
spec/enterprise/services/captain/copilot/chat_service_spec.rb
spec/enterprise/services/captain/llm/assistant_chat_service_spec.rb
spec/enterprise/services/captain/llm/faq_generator_service_spec.rb
spec/enterprise/services/captain/llm/conversation_faq_service_spec.rb
spec/enterprise/services/captain/llm/assistant_action_classifier_service_spec.rb
spec/enterprise/services/captain/llm/assistant_false_promise_service_spec.rb
spec/enterprise/jobs/captain/conversation/response_builder_job_spec.rb`
passed with 112 examples, 0 failures.
- `bundle exec rspec spec/models/concerns/captain_featurable_spec.rb
spec/models/account_spec.rb
spec/controllers/api/v1/accounts/captain/preferences_controller_spec.rb
spec/lib/llm/feature_router_spec.rb` passed with 87 examples, 0
failures.
- `bundle exec rubocop enterprise/app/services/llm/base_ai_service.rb
enterprise/app/services/captain/copilot/chat_service.rb
enterprise/app/services/captain/llm/assistant_chat_service.rb
enterprise/app/services/captain/llm/faq_generator_service.rb
enterprise/app/services/captain/llm/conversation_faq_service.rb
enterprise/app/services/captain/llm/contact_notes_service.rb
enterprise/app/services/captain/llm/contact_attributes_service.rb
enterprise/app/services/captain/llm/assistant_action_classifier_service.rb
enterprise/app/services/captain/llm/assistant_false_promise_service.rb
spec/enterprise/services/llm/base_ai_service_spec.rb
spec/enterprise/services/captain/copilot/chat_service_spec.rb
spec/enterprise/services/captain/llm/assistant_chat_service_spec.rb
spec/enterprise/services/captain/llm/faq_generator_service_spec.rb
spec/enterprise/services/captain/llm/conversation_faq_service_spec.rb
spec/enterprise/services/captain/llm/assistant_action_classifier_service_spec.rb
spec/enterprise/services/captain/llm/assistant_false_promise_service_spec.rb
spec/enterprise/jobs/captain/conversation/response_builder_job_spec.rb`
passed with no offenses.
- `bundle exec ruby -e "require 'yaml'; config =
YAML.load_file('config/llm.yml'); abort('missing
document_faq_generation') unless config.dig('features',
'document_faq_generation'); abort('missing default') unless
config.dig('features', 'document_faq_generation', 'default'); puts
'llm.yml ok'"` passed.
- `git diff --check` passed.

## 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
- [ ] Any dependent changes have been merged and published in downstream
modules
2026-06-25 17:03:07 +05:30
Sony MathewandGitHub 91d8a4e2a3 feat: Route reply box LLM tasks (2/6) (#14840)
# Pull Request Template

## Description

Routes OSS reply-box and small Captain tasks through feature-specific
LLM model resolution. Rewrite, reply suggestion, summary, follow-up, and
CSAT utility analysis now resolve through the `editor` feature; label
suggestion resolves through `label_suggestion`. Existing credentials,
account OpenAI hook behavior, and instrumentation event names remain
unchanged.

Linear: https://linear.app/chatwoot/issue/CW-7425/test-new-models
Depends on #14839

## Type of change

- [ ] 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?

- `bundle exec rspec spec/lib/captain/base_task_service_spec.rb
spec/lib/captain/rewrite_service_spec.rb
spec/lib/captain/reply_suggestion_service_spec.rb
spec/lib/captain/summary_service_spec.rb
spec/lib/captain/label_suggestion_service_spec.rb
spec/lib/captain/csat_utility_analysis_service_spec.rb
spec/lib/captain/follow_up_service_spec.rb` passed with 81 examples, 0
failures.
- `bundle exec rubocop lib/captain/base_task_service.rb
lib/captain/rewrite_service.rb lib/captain/reply_suggestion_service.rb
lib/captain/summary_service.rb lib/captain/label_suggestion_service.rb
lib/captain/csat_utility_analysis_service.rb
lib/captain/follow_up_service.rb
enterprise/lib/enterprise/captain/reply_suggestion_service.rb
spec/lib/captain/base_task_service_spec.rb
spec/lib/captain/rewrite_service_spec.rb
spec/lib/captain/reply_suggestion_service_spec.rb
spec/lib/captain/summary_service_spec.rb
spec/lib/captain/label_suggestion_service_spec.rb
spec/lib/captain/csat_utility_analysis_service_spec.rb
spec/lib/captain/follow_up_service_spec.rb` passed with no offenses.
- `git diff --check` passed.

## 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
- [ ] Any dependent changes have been merged and published in downstream
modules
2026-06-25 17:01:05 +05:30
Sony MathewandGitHub 8b977b35a8 feat: Add LLM feature router (1/6) (#14839)
## Description

Adds the foundation for feature-specific LLM model routing so Captain AI
features can resolve their effective provider/model from code defaults
and account-level overrides. This fixes the provider metadata key in
`config/llm.yml`, adds `Llm::FeatureRouter`, and routes existing
`CaptainFeaturable` model defaults through the shared resolver.

Fixes https://linear.app/chatwoot/issue/CW-7425/test-new-models

## Type of change

- [ ] 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?

- `bundle exec rspec spec/lib/llm/models_spec.rb
spec/lib/llm/feature_router_spec.rb
spec/models/concerns/captain_featurable_spec.rb` - 23 examples, 0
failures
- `bundle exec rubocop lib/llm/models.rb lib/llm/feature_router.rb
app/models/concerns/captain_featurable.rb spec/lib/llm/models_spec.rb
spec/lib/llm/feature_router_spec.rb
spec/models/concerns/captain_featurable_spec.rb` - no offenses
- `bundle exec ruby -e "require 'yaml'; config =
YAML.load_file('config/llm.yml'); abort('missing providers') unless
config['providers']; abort('missing models') unless config['models'];
abort('missing features') unless config['features']; puts 'llm.yml ok'"`
- `git diff --check`

## 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
2026-06-25 17:00:07 +05:30
62cbeae95f feat: onboarding inboxes UI (#14565)
After entering their account details, new admins land on an **Inbox
setup** screen that shows what we've already set up for them and lets
them connect their conversation channels without leaving onboarding. It
surfaces the auto-created live chat widget (and Help Center on
Enterprise), highlights channels detected from their website, and offers
a **View all** dialog to connect any supported channel inline.

### Channel status

| Channel | How it connects | Status | PR |
|---|---|---|---|
| Live chat (Website) | Auto-created during setup |  Done |
https://github.com/chatwoot/chatwoot/pull/14314 |
| WhatsApp | Meta embedded signup |  Done |
https://github.com/chatwoot/chatwoot/pull/14619 |
| Facebook | Login + page picker |  Done |
https://github.com/chatwoot/chatwoot/pull/14619 |
| Instagram | OAuth redirect |  Done |
https://github.com/chatwoot/chatwoot/pull/14568 |
| TikTok | OAuth redirect |  Done |
https://github.com/chatwoot/chatwoot/pull/14569 |
| LINE | Inline credential form |  Done | — |
| Telegram | Inline credential form |  Done | — |
| Gmail / Outlook | OAuth (email) | ⚠️ Disabled — coming in a follow-up
| https://github.com/chatwoot/chatwoot/pull/14567 |
| SMS / API / Voice / Other email | — |  Unavailable | — |

---------

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
2026-06-25 14:54:07 +05:30
7c7459b734 fix(whatsapp): override webhook at phone number level (#13817)
## Description

Move WhatsApp webhook callback override from WABA level to phone number
level, allowing multiple phone numbers on the same WABA to have
independent callback URLs.

## Type of change

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

## How Has This Been Tested?

- Connect a WhatsApp Cloud inbox via embedded signup
- Verify webhook setup succeeds and messages are received
- Connect a second phone number on the same WABA — both should receive
messages independently
- Delete an inbox and verify only that phone number's override is
cleared

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

---------

Co-authored-by: tds-1 <tds-1@users.noreply.github.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-25 14:49:09 +05:30
Sojan JoseandGitHub 0849d2e070 chore: Track cloud signup conversions (#14852)
## Summary
- enqueue `cloud_signup` conversion tracking after website attribution
is stored on a new cloud account
- keep authenticated add-workspace requests out of
attribution/conversion tracking
- reuse the existing marketing conversion tracking service and config
already available on `develop`

## Notes
- This PR only wires the signup path.
- Billing/plan activation conversion tracking is intentionally left out
for a separate rollout.
- The conversion service still no-ops outside Chatwoot Cloud and when
stored attribution has no supported click identifier.
2026-06-24 20:04:42 -07:00
Sojan JoseandGitHub cd1a214d98 chore: Add marketing conversion tracking foundation (#14851)
## Summary

Adds the minimal foundation for Cloud marketing conversion tracking:

- locked internal installation config for conversion tracking
credentials and event mappings
- generic background job and service for uploading conversion events
from stored attribution
- focused coverage for Cloud gating, click-id selection, payload shape,
and optional conversion value

This PR intentionally does not wire signup or plan activation yet. The
next step is to validate the service from Rails console against existing
attributed accounts, then add the event hooks in a follow-up PR.

## Notes

The config remains locked and is surfaced under the Internal settings
group next to the existing Cloud plan configuration. The service assumes
the locked config is present and valid; config mistakes should surface
instead of being silently ignored.
2026-06-24 18:27:02 -07:00
f9cc702030 fix(help-center): documentation layout on custom domain root, locale (#14850)
Help centers using the documentation layout now render correctly in two
cases that previously fell back to the wrong output. Opening a portal at
its custom-domain root (e.g. docs.example.com/ ) now shows the full
documentation home
in place instead of the classic layout, and portals whose locale is a
region variant that Chatwoot doesn't ship a
translation for (e.g. th_TH , fr_ML ) now show their categories in the
sidebar on article pages.

Closes
https://linear.app/chatwoot/issue/CW-7437/portal-layout-is-not-properly-working-in-custom-domain

## How to test

1. Create a portal with the documentation layout and a custom domain
(e.g. example.chat.test ), with at least one
category containing a published article.
2. Visit the custom-domain root ( http://example.chat.test:3000/ ) → it
should show the full documentation home
(sidebar, topbar), not the classic layout, with no redirect.
3. Set the portal's locale to a region variant Chatwoot doesn't
translate, e.g. th_TH , with categories/articles
under that locale.
4. Open an article → the sidebar should list the article's category and
sibling articles. Before the fix the sidebar
was empty for these locales.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 16:12:55 -07:00
74db16158d feat: migrate dyte integration to cloudflare realtimekit (#14752)
Dyte is sunsetting its existing infrastructure after the Cloudflare
acquisition, so this migrates Chatwoot’s video call integration to
Cloudflare RealtimeKit.

The integration now uses Cloudflare Account ID, RealtimeKit App ID, and
a Cloudflare API token with Realtime Admin permissions. Meeting creation
and participant token generation now call Cloudflare’s RealtimeKit APIs,
while the existing Chatwoot call experience remains unchanged for agents
and customers.

This also adds setup-time credential validation, so admins get clearer
errors when the API token is invalid, the Cloudflare account or
permissions are incorrect, or the RealtimeKit App ID does not belong to
the selected account.

Fixes
https://linear.app/chatwoot/issue/PLA-176/migrate-dyte-integration-to-cloudflare-realtimekit

**How to test**

1. Go to Settings → Integrations → Cloudflare RealtimeKit.
2. Add a Cloudflare Account ID, RealtimeKit App ID, and API token with
Realtime Admin permissions.
3. Confirm the integration saves successfully with valid credentials.
4. Try invalid credentials and confirm the error identifies whether the
token, account/permissions, or app ID is wrong.
5. Start a video call from a conversation and confirm the RealtimeKit
meeting opens.

---------

Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
Co-authored-by: Sony Mathew <sony@chatwoot.com>
2026-06-24 11:04:08 +04:00
Sivin VargheseandGitHub e8edc9ebf5 feat: allow users to set an icon or emoji for teams (#14767) 2026-06-23 20:53:31 +05:30
Vishnu NarayananandGitHub e785620921 feat: read structured X-Chatwoot-* headers for mobile session metadata (#14762)
Phase 2 of [INF-75](https://linear.app/chatwoot/issue/INF-75). Follows
up on [#14753](https://github.com/chatwoot/chatwoot/pull/14753) (Phase 1
UA pattern fallback, already merged).

When the request carries an \`X-Chatwoot-Client-Name\` header,
\`UserSessionTrackingService\` now prefers the five structured
\`X-Chatwoot-*\` headers over the User-Agent for populating the
\`user_sessions\` row:

| Header | Mapped column |
|---|---|
| \`X-Chatwoot-Client-Name\` | \`browser_name\` |
| \`X-Chatwoot-Client-Version\` | \`browser_version\` |
| \`X-Chatwoot-Device-Model\` | \`platform_name\` |
| \`X-Chatwoot-Platform-Version\` | \`platform_version\` |
| \`X-Chatwoot-Platform\` (+ model) | \`device_name\` (\`iPhone\` /
\`iPad\` / \`Android\`) |

When the header is absent or blank, the existing \`Browser.new\` path
runs, with the Phase 1 legacy UA fallback still acting as the floor.
Real browsers are untouched.

A follow-up PR in
[chatwoot-mobile-app](https://github.com/chatwoot/chatwoot-mobile-app)
will start sending these headers from the React Native build. Until that
ships, this PR is a no-op for production traffic, so it can land
independently.

Fixes INF-75.
2026-06-23 20:08:27 +05:30
Aakash BakhleandGitHub 0af9900be2 fix: model for false promise harness (#14824)
fixes model for false promise harness
2026-06-23 18:42:34 +05:30
Aakash BakhleandGitHub 84fda8a323 fix(captain): move false promise harness setting to account (#14823)
# Pull Request Template

## Description

moves the harness to account settings rather than assistant settings

## Type of change
refactor

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

## 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-06-23 16:55:42 +05:30
Aakash BakhleandGitHub 1c7e60880d fix: add harness on false promises (#14672)
# Pull Request Template

## Description


https://linear.app/chatwoot/issue/AI-179/false-promise-soft-handoff-guard

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

Against negative cases identified by evals

## 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-06-23 16:30:14 +05:30
Aakash BakhleandGitHub 8e27ca4282 fix(firecrawl): get url in case sourceUrl is not available (#14820)
# Pull Request Template

## Description

Fixes inconsistency by adding a fallback to use `url` instead of
`sourceUrl` when Firecrawl returns empty `sourceUrl` in production.

We switched to v2 endpoints of Firecrawl in
https://github.com/chatwoot/chatwoot/pull/14624 and it worked locally,
but seems to fail for some cases in production.

## 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-06-23 13:40:31 +05:30
ee5e20551d fix(facebook): handle messenger post attachment type (#14813)
Facebook Messenger messages that share a post no longer break message
ingestion. Following the recent sticker change, Meta now sends a shared
post as a new `post` attachment type. Chatwoot didn't recognise `post`
as a valid attachment file type, so the webhook job crashed and the
message — and any others in the same batch — failed to sync. Shared
posts now appear in the conversation as a fallback link (title + URL),
the same way `share` attachments already do.

This is the same class of bug as #14793 (sticker), just a different
attachment type.

#### Root cause

`post` was neither skipped as unsupported nor mapped to a valid
`file_type`, so `attachment_params` produced `file_type: :post`. The
`Attachment#file_type` enum has no `post` value, so it raised
`ArgumentError: 'post' is not a valid file_type`, crashing
`Webhooks::FacebookEventsJob`.

#### Fix

The Facebook builder already maps `share -> :fallback` because shared
posts point to facebook.com page URLs rather than downloadable media. A
`post` is the same kind of attachment, so map both `share` and `post` to
`fallback` (stores the title/link, no download attempt). Also read the
fallback title from `payload.title`, since the post payload nests it
there rather than at the top level.

## How to reproduce

1. Connect a Facebook Page inbox.
2. Share a Facebook post into Messenger to that page.
3. Before this change: `Webhooks::FacebookEventsJob` raises
`ArgumentError: 'post' is not a valid file_type` and the message is
dropped.
4. After this change: the shared post shows up in the conversation as a
fallback link.

Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 11:03:04 +04:00
df79c0bbde feat: exclude stale conversations via assignment policy age threshold (#14766)
## Linear ticket
-
https://linear.app/chatwoot/issue/CW-7137/assignment-v2-backlog-flush-overloads-agents

## Description

Assignment policies now skip stale unassigned conversations
automatically. Each policy carries an age threshold (defaults to 7
days), so auto-assignment only picks up recent backlog instead of
draining very old, forgotten conversations. The threshold is
configurable per policy and can be cleared to assign conversations
regardless of age. Previously this control existed only on Enterprise
capacity policies (`exclude_older_than_hours`); it now lives on the
assignment policy itself, so every V2 inbox benefits without needing a
capacity policy.

## Type of change

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

## How Has This Been Tested?

- Local UI flows

## Screenshots? 

<img width="645" height="822" alt="image"
src="https://github.com/user-attachments/assets/ec58db86-c1fa-4f9e-be87-2e24ff02e077"
/>


## 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: Sony Mathew <sony@chatwoot.com>
2026-06-23 12:22:45 +05:30
647cfc2d83 feat: distinguish agent-declined calls with a rejected status (#14783)
## Linear ticket
-
https://linear.app/chatwoot/issue/CW-7374/agent-declined-voice-calls-miscounted-as-failed

## Description

Agent rejections were stored with status: failed, so call reports lumped
deliberate declines together with real technical failure.

Fix: give declines their own terminal rejected status (Twilio + WhatsApp
paths), keeping end_reason: agent_rejected. Genuine provider/network
failures stay failed. Frontend renders declines exactly as before;
existing rows backfilled via migration.

## Type of change

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

## How Has This Been Tested?

- Tested on UI

## 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: Sony Mathew <sony@chatwoot.com>
2026-06-23 12:22:29 +05:30
Sivin VargheseandGitHub 961ec86ba9 chore: support custom variables in auto-resolution message (#14782)
# Pull Request Template

## Description

This PR adds support for custom variables in auto-resolution messages
within Conversation Workflows.

Agents can insert variables (e.g. `{{contact.name}}`) into the custom
auto-resolution message using the variable picker by typing `{{`. These
variables are resolved with actual conversation data when the
auto-resolution message is sent.

The field uses the existing editor with the formatting toolbar hidden,
since auto-resolution messages are delivered across all channels,
including SMS, where formatting isn't supported. Existing message
content is preserved as-is, and typing `{{` opens the variable picker
for quick insertion.

Fixes
https://linear.app/chatwoot/issue/CW-7369/support-custom-variables-in-conversations-workflow-text-fields

## Type of change

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

## How Has This Been Tested?

### Screenshots
<img width="1262" height="764" alt="image"
src="https://github.com/user-attachments/assets/e43be22c-f86e-436a-9fdd-5bcb73c61ca3"
/>



## 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-06-23 11:35:52 +05:30
Sojan JoseandGitHub e86222034e fix(auth): record attribution for oauth signups (#14796)
Google OAuth signups now persist the same first-party attribution
cookies as email signups on Chatwoot Cloud.

This keeps attribution capture owned by the website and reuses the
existing Enterprise-only account attribution service. The OAuth callback
only records the already-shaped first-touch and last-touch cookie
payload after a new account is created.

## What changed

- Added Enterprise-only attribution persistence to the Google OAuth
signup account creation path.
- Reuses `Internal::Accounts::MarketingAttributionService`.
- Keeps attribution best-effort so failures do not interrupt OAuth
signup.
- Leaves existing email signup and SAML behavior unchanged.

## How to test

- Start from a Chatwoot Cloud-like setup with attribution cookies
present.
- Sign up using the Google OAuth button.
- Confirm the created account has
`internal_attributes['marketing_attribution']` with `first_touch` and
`last_touch`.
- Confirm existing Google OAuth login still redirects normally.

Validation run locally:

- `bundle exec rspec
spec/enterprise/controllers/enterprise/devise_overrides/google_oauth_attribution_spec.rb`
- `bundle exec rspec
spec/controllers/devise/omniauth_callbacks_controller_spec.rb
spec/enterprise/controllers/enterprise/devise_overrides/google_oauth_attribution_spec.rb`
- `bundle exec rubocop
enterprise/app/controllers/enterprise/devise_overrides/omniauth_callbacks_controller.rb
spec/enterprise/controllers/enterprise/devise_overrides/google_oauth_attribution_spec.rb`
2026-06-22 06:51:09 -07:00
2b37fef0e0 fix(companies): sync contact company names (#14759)
Fixes company-contact name drift when a company is renamed or deleted.

Closes: N/A

## Why
Contacts keep a denormalized `additional_attributes.company_name` for
display and filtering. Company rename/delete flows could leave that
copied value stale even though the actual `company_id` relationship
changed.

## What changed
- Enqueues an async company contact-name sync job when a company name
changes.
- Moves company deletion into `Companies::DeleteJob`.
- The delete job unlinks linked contacts, clears only the copied
`company_name`, and then deletes the company.
- Uses bulk JSON updates for the cleanup path so contact records are not
saved, which avoids contact update callbacks, webhook dispatch, and
automation side effects.

## How to test
- Link a contact to a company, rename the company, and confirm the
contact company name updates after the job runs.
- Delete a company with linked contacts and confirm the delete job
removes the company, unassigns linked contacts, and preserves other
contact additional attributes.

---------

Co-authored-by: Sony Mathew <sony@chatwoot.com>
Co-authored-by: Sony Mathew <2040199+sony-mathew@users.noreply.github.com>
2026-06-22 06:49:52 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Sony Mathew
44b32eacec chore(deps): bump faraday from 2.14.2 to 2.14.3 (#14811)
Bumps [faraday](https://github.com/lostisland/faraday) from 2.14.2 to
2.14.3.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/lostisland/faraday/releases">faraday's
releases</a>.</em></p>
<blockquote>
<h2>v2.14.3</h2>
<h2>Security Note</h2>
<p>This release contains a security fix, we recommend all users to
upgrade as soon as possible.
A Security Advisory with more details will be posted shortly.</p>
<h2>What's Changed</h2>
<ul>
<li>Upgrade CI lint step from Ruby 3 to 4 by <a
href="https://github.com/larouxn"><code>@​larouxn</code></a> in <a
href="https://redirect.github.com/lostisland/faraday/pull/1673">lostisland/faraday#1673</a></li>
<li>feat(test): add Stubs#clear to remove all stubs by <a
href="https://github.com/SAY-5"><code>@​SAY-5</code></a> in <a
href="https://redirect.github.com/lostisland/faraday/pull/1675">lostisland/faraday#1675</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/SAY-5"><code>@​SAY-5</code></a> made
their first contribution in <a
href="https://redirect.github.com/lostisland/faraday/pull/1675">lostisland/faraday#1675</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/lostisland/faraday/compare/v2.14.2...v2.14.3">https://github.com/lostisland/faraday/compare/v2.14.2...v2.14.3</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/lostisland/faraday/commit/f1ace878bae16c1df298b6af7c34657f2c4dcddb"><code>f1ace87</code></a>
Version bump to 2.14.3</li>
<li><a
href="https://github.com/lostisland/faraday/commit/36764bfc48aa9fc08336d36600ea67af149c80fa"><code>36764bf</code></a>
Merge commit from fork</li>
<li><a
href="https://github.com/lostisland/faraday/commit/59334e0e9b1918bd26b143f4a7bbc8c765de3ac4"><code>59334e0</code></a>
feat(test): add Stubs#clear to remove all stubs (<a
href="https://redirect.github.com/lostisland/faraday/issues/1675">#1675</a>)</li>
<li><a
href="https://github.com/lostisland/faraday/commit/469f25c793f25bcfdd416a54edeb454d045c08d6"><code>469f25c</code></a>
Upgrade CI lint step from Ruby 3 to 4 (<a
href="https://redirect.github.com/lostisland/faraday/issues/1673">#1673</a>)</li>
<li>See full diff in <a
href="https://github.com/lostisland/faraday/compare/v2.14.2...v2.14.3">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=faraday&package-manager=bundler&previous-version=2.14.2&new-version=2.14.3)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/chatwoot/chatwoot/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Sony Mathew <sony@chatwoot.com>
2026-06-22 17:36:08 +05:30
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Sony Mathew
5b7430f376 chore(deps): bump concurrent-ruby from 1.3.5 to 1.3.7 (#14810)
Bumps
[concurrent-ruby](https://github.com/ruby-concurrency/concurrent-ruby)
from 1.3.5 to 1.3.7.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/ruby-concurrency/concurrent-ruby/releases">concurrent-ruby's
releases</a>.</em></p>
<blockquote>
<h2>v1.3.7</h2>
<!-- raw HTML omitted -->
<p>There are 3 security fixes in this release, so updating is
recommended.
These security vulnerabilities are not very likely to be hit in practice
and have a corresponding <code>Low</code> severity score.</p>
<!-- raw HTML omitted -->
<h2>What's Changed</h2>
<ul>
<li><a
href="https://github.com/ruby-concurrency/concurrent-ruby/security/advisories/GHSA-h8w8-99g7-qmvj">CVE-2026-54904</a>
<code>AtomicReference#update</code> livelocks when the stored value is
<code>Float::NAN</code>. Fix by <a
href="https://github.com/joshuay03"><code>@​joshuay03</code></a> and <a
href="https://github.com/eregon"><code>@​eregon</code></a></li>
<li><a
href="https://github.com/ruby-concurrency/concurrent-ruby/security/advisories/GHSA-wv3x-4vxv-whpp">CVE-2026-54905</a>
<code>ReentrantReadWriteLock</code> read-count overflow grants a write
lock without exclusivity. Fix by <a
href="https://github.com/joshuay03"><code>@​joshuay03</code></a></li>
<li><a
href="https://github.com/ruby-concurrency/concurrent-ruby/security/advisories/GHSA-6wx8-w4f5-wwcr">CVE-2026-54906</a>
<code>ReadWriteLock</code> allows wrong-thread write release and stray
read-release counter corruption. Fix by <a
href="https://github.com/joshuay03"><code>@​joshuay03</code></a></li>
<li>concurrent-ruby-ext: fix build on Darwin 32-bit by <a
href="https://github.com/barracuda156"><code>@​barracuda156</code></a>
in <a
href="https://redirect.github.com/ruby-concurrency/concurrent-ruby/pull/1064">ruby-concurrency/concurrent-ruby#1064</a></li>
<li>Add SECURITY.md by <a
href="https://github.com/eregon"><code>@​eregon</code></a> in <a
href="https://redirect.github.com/ruby-concurrency/concurrent-ruby/pull/1104">ruby-concurrency/concurrent-ruby#1104</a></li>
<li>Add Ruby 4.0 in CI by <a
href="https://github.com/eregon"><code>@​eregon</code></a> in <a
href="https://redirect.github.com/ruby-concurrency/concurrent-ruby/pull/1106">ruby-concurrency/concurrent-ruby#1106</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/barracuda156"><code>@​barracuda156</code></a>
made their first contribution in <a
href="https://redirect.github.com/ruby-concurrency/concurrent-ruby/pull/1064">ruby-concurrency/concurrent-ruby#1064</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/ruby-concurrency/concurrent-ruby/compare/v1.3.6...v1.3.7">https://github.com/ruby-concurrency/concurrent-ruby/compare/v1.3.6...v1.3.7</a></p>
<h2>v1.3.6</h2>
<!-- raw HTML omitted -->
<h2>What's Changed</h2>
<ul>
<li>Run tests without the C extension in CI by <a
href="https://github.com/eregon"><code>@​eregon</code></a> in <a
href="https://redirect.github.com/ruby-concurrency/concurrent-ruby/pull/1081">ruby-concurrency/concurrent-ruby#1081</a></li>
<li>Fix typo in Promise docs by <a
href="https://github.com/danieldiekmeier"><code>@​danieldiekmeier</code></a>
in <a
href="https://redirect.github.com/ruby-concurrency/concurrent-ruby/pull/1083">ruby-concurrency/concurrent-ruby#1083</a></li>
<li>Correct word in readme by <a
href="https://github.com/wwahammy"><code>@​wwahammy</code></a> in <a
href="https://redirect.github.com/ruby-concurrency/concurrent-ruby/pull/1084">ruby-concurrency/concurrent-ruby#1084</a></li>
<li>Fix mistakes in MVar documentation by <a
href="https://github.com/trinistr"><code>@​trinistr</code></a> in <a
href="https://redirect.github.com/ruby-concurrency/concurrent-ruby/pull/1087">ruby-concurrency/concurrent-ruby#1087</a></li>
<li>Fix multi require concurrent/executor/cached_thread_pool by <a
href="https://github.com/OuYangJinTing"><code>@​OuYangJinTing</code></a>
in <a
href="https://redirect.github.com/ruby-concurrency/concurrent-ruby/pull/1085">ruby-concurrency/concurrent-ruby#1085</a></li>
<li>Use typed data APIs by <a
href="https://github.com/nobu"><code>@​nobu</code></a> in <a
href="https://redirect.github.com/ruby-concurrency/concurrent-ruby/pull/1096">ruby-concurrency/concurrent-ruby#1096</a></li>
<li>Add Joshua Young to the list of maintainers by <a
href="https://github.com/eregon"><code>@​eregon</code></a> in <a
href="https://redirect.github.com/ruby-concurrency/concurrent-ruby/pull/1097">ruby-concurrency/concurrent-ruby#1097</a></li>
<li>Asynchronous pruning for RubyThreadPoolExecutor by <a
href="https://github.com/joshuay03"><code>@​joshuay03</code></a> in <a
href="https://redirect.github.com/ruby-concurrency/concurrent-ruby/pull/1082">ruby-concurrency/concurrent-ruby#1082</a></li>
<li>Mark RubySingleThreadExecutor as a SerialExecutorService by <a
href="https://github.com/meineerde"><code>@​meineerde</code></a> in <a
href="https://redirect.github.com/ruby-concurrency/concurrent-ruby/pull/1070">ruby-concurrency/concurrent-ruby#1070</a></li>
<li>Allow TimerTask to be safely restarted after shutdown and avoid
duplicate tasks by <a
href="https://github.com/bensheldon"><code>@​bensheldon</code></a> in <a
href="https://redirect.github.com/ruby-concurrency/concurrent-ruby/pull/1001">ruby-concurrency/concurrent-ruby#1001</a></li>
<li>Flaky test fix: allow ThreadPool to shutdown before asserting
completed_task_count by <a
href="https://github.com/bensheldon"><code>@​bensheldon</code></a> in <a
href="https://redirect.github.com/ruby-concurrency/concurrent-ruby/pull/1098">ruby-concurrency/concurrent-ruby#1098</a></li>
<li><code>ThreadPoolExecutor#kill</code> will
<code>wait_for_termination</code> in JRuby; ensure <code>TimerSet</code>
timer thread shuts down cleanly by <a
href="https://github.com/bensheldon"><code>@​bensheldon</code></a> in <a
href="https://redirect.github.com/ruby-concurrency/concurrent-ruby/pull/1044">ruby-concurrency/concurrent-ruby#1044</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/danieldiekmeier"><code>@​danieldiekmeier</code></a>
made their first contribution in <a
href="https://redirect.github.com/ruby-concurrency/concurrent-ruby/pull/1083">ruby-concurrency/concurrent-ruby#1083</a></li>
<li><a href="https://github.com/wwahammy"><code>@​wwahammy</code></a>
made their first contribution in <a
href="https://redirect.github.com/ruby-concurrency/concurrent-ruby/pull/1084">ruby-concurrency/concurrent-ruby#1084</a></li>
<li><a href="https://github.com/trinistr"><code>@​trinistr</code></a>
made their first contribution in <a
href="https://redirect.github.com/ruby-concurrency/concurrent-ruby/pull/1087">ruby-concurrency/concurrent-ruby#1087</a></li>
<li><a
href="https://github.com/OuYangJinTing"><code>@​OuYangJinTing</code></a>
made their first contribution in <a
href="https://redirect.github.com/ruby-concurrency/concurrent-ruby/pull/1085">ruby-concurrency/concurrent-ruby#1085</a></li>
<li><a href="https://github.com/nobu"><code>@​nobu</code></a> made their
first contribution in <a
href="https://redirect.github.com/ruby-concurrency/concurrent-ruby/pull/1096">ruby-concurrency/concurrent-ruby#1096</a></li>
<li><a href="https://github.com/joshuay03"><code>@​joshuay03</code></a>
made their first contribution in <a
href="https://redirect.github.com/ruby-concurrency/concurrent-ruby/pull/1082">ruby-concurrency/concurrent-ruby#1082</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/ruby-concurrency/concurrent-ruby/compare/v1.3.5...v1.3.6">https://github.com/ruby-concurrency/concurrent-ruby/compare/v1.3.5...v1.3.6</a></p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/ruby-concurrency/concurrent-ruby/blob/master/CHANGELOG.md">concurrent-ruby's
changelog</a>.</em></p>
<blockquote>
<h2>Release v1.3.7 (16 June 2026)</h2>
<p>concurrent-ruby:</p>
<ul>
<li>See the <a
href="https://github.com/ruby-concurrency/concurrent-ruby/releases/tag/v1.3.7">release
notes on GitHub</a>.</li>
</ul>
<h2>Release v1.3.6 (13 December 2025)</h2>
<p>concurrent-ruby:</p>
<ul>
<li>See the <a
href="https://github.com/ruby-concurrency/concurrent-ruby/releases/tag/v1.3.6">release
notes on GitHub</a>.</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/ruby-concurrency/concurrent-ruby/commit/4c8fc28ab6bb9bd8258a4c0c2fa6d35ebe77b3cb"><code>4c8fc28</code></a>
Release 1.3.7</li>
<li><a
href="https://github.com/ruby-concurrency/concurrent-ruby/commit/d91ca9426cb819d6cc63f1bd64bfe54644d0beca"><code>d91ca94</code></a>
Fix AtomicReference#update livelock when stored value is Float::NAN on
JRuby ...</li>
<li><a
href="https://github.com/ruby-concurrency/concurrent-ruby/commit/7e4d711bacf7a1dac3ef6bda44004387be2dc7e6"><code>7e4d711</code></a>
Fix <code>ReentrantReadWriteLock</code> read hold overflow into
write-lock bit</li>
<li><a
href="https://github.com/ruby-concurrency/concurrent-ruby/commit/6e37e0644b83b182971dc540d2e4bee38df61386"><code>6e37e06</code></a>
Fix <code>AtomicReference#update</code> livelock when stored value is
<code>Float::NAN</code></li>
<li><a
href="https://github.com/ruby-concurrency/concurrent-ruby/commit/2825cfa12cb708b76557803957f76862eb1151a2"><code>2825cfa</code></a>
Cleanup spec</li>
<li><a
href="https://github.com/ruby-concurrency/concurrent-ruby/commit/3fd493283ca5f84f0ef4e84aabd43ad68df4626b"><code>3fd4932</code></a>
Fix <code>ReadWriteLock</code> wrong-thread write release and stray read
release</li>
<li><a
href="https://github.com/ruby-concurrency/concurrent-ruby/commit/1974b4772efc034ee8eaa562b4370343f4c5c54b"><code>1974b47</code></a>
Add Ruby 4.0 in CI</li>
<li><a
href="https://github.com/ruby-concurrency/concurrent-ruby/commit/df8706d40c483d76bbb0b3a35a633c68fa9e17be"><code>df8706d</code></a>
Add SECURITY.md (<a
href="https://redirect.github.com/ruby-concurrency/concurrent-ruby/issues/1104">#1104</a>)</li>
<li><a
href="https://github.com/ruby-concurrency/concurrent-ruby/commit/7a1b78941c081106c20a9ca0144ac73a48d254ab"><code>7a1b789</code></a>
Bump actions/upload-pages-artifact from 4 to 5</li>
<li><a
href="https://github.com/ruby-concurrency/concurrent-ruby/commit/9b2dbf712896a638a73d2fa221206961c8d6484d"><code>9b2dbf7</code></a>
Bump actions/deploy-pages from 4 to 5</li>
<li>Additional commits viewable in <a
href="https://github.com/ruby-concurrency/concurrent-ruby/compare/v1.3.5...v1.3.7">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=concurrent-ruby&package-manager=bundler&previous-version=1.3.5&new-version=1.3.7)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/chatwoot/chatwoot/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Sony Mathew <sony@chatwoot.com>
2026-06-22 16:08:06 +05:30
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Sony Mathew
5c00a1601a chore(deps): bump dompurify from 3.4.0 to 3.4.11 (#14798)
Bumps [dompurify](https://github.com/cure53/DOMPurify) from 3.4.0 to
3.4.11.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/cure53/DOMPurify/releases">dompurify's
releases</a>.</em></p>
<blockquote>
<h2>DOMPurify 3.4.11</h2>
<ul>
<li>Fixed an issue with a leaky config for hooks via
<code>setConfig</code>, thanks <a
href="https://github.com/trace37labs"><code>@​trace37labs</code></a></li>
<li>Bumped vulnerable development dependencies to arrive at plain 0 with
<code>npm audit</code></li>
<li>Updated the <code>osv-scanner</code> suppression list as no
vulnerable dependencies are left for now</li>
<li>Updated up the linting tool-chain and removed now-redundant lint
directives</li>
<li>Updated the documentation is several spots, README, wiki, etc.</li>
<li>Bumped several dependencies where possible</li>
</ul>
<h2>DOMPurify 3.4.10</h2>
<ul>
<li>Refactored codebase for clarity: extracted the public type
declarations into <code>types.ts</code></li>
<li>Decomposed the three largest sanitizer functions into focused
helpers</li>
<li>Removed duplicated defaults and dead branches, consolidated
<code>SAFE_FOR_TEMPLATES</code> scrubbing into single shared path</li>
<li>Improved per-node performance by hoisting the mXSS probe regexes and
testing <code>textContent</code> before <code>innerHTML</code></li>
<li>Added a deterministic micro-benchmark harness (<code>npm run
bench</code>) with a <code>--compare</code> mode</li>
<li>Reduced CI cost by running the full three-engine browser suite once
per PR</li>
<li>Refreshed the <code>demos/</code> folder so every demo runs again,
and added a SVG-via-<code>&lt;img&gt;</code> demo</li>
<li>Documented the bench and <code>test:happydom</code> scripts in the
README</li>
<li>Completed the Attack Classes &amp; Bypass History wiki page</li>
<li>Bumped several dependencies where possible</li>
</ul>
<h2>DOMPurify 3.4.9</h2>
<ul>
<li>Further improved the handling of Trusted Types config options,
thanks <a
href="https://github.com/offset"><code>@​offset</code></a></li>
<li>Further improved the handling of <code>IN_PLACE</code> sanitization,
thanks <a
href="https://github.com/mozfreddyb"><code>@​mozfreddyb</code></a></li>
<li>Added more test coverage for <code>IN_PLACE</code> and Trusted Types
related usage</li>
<li>Bumped several dependencies where possible</li>
<li>Updated README and wiki with more accurate documentation &amp;
attack samples</li>
</ul>
<h2>DOMPurify 3.4.8</h2>
<ul>
<li>Cleaned up the repository root, renamed some and removed unneeded
files</li>
<li>Fixed an issue with handling of Trusted Types policies, thanks <a
href="https://github.com/fulstadev"><code>@​fulstadev</code></a></li>
<li>Fixed the node iterator for better template scrubbing, thanks <a
href="https://github.com/IamLeandrooooo"><code>@​IamLeandrooooo</code></a></li>
<li>Included formerly missing LICENSE-MPL in published npm package,
thanks <a
href="https://github.com/asamuzaK"><code>@​asamuzaK</code></a></li>
<li>Bumped several dependencies where possible</li>
</ul>
<h2>DOMPurify 3.4.7</h2>
<ul>
<li>Hardened the handling of Shadow Roots when using
<code>IN_PLACE</code>, thanks <a
href="https://github.com/GameZoneHacker"><code>@​GameZoneHacker</code></a></li>
<li>Removed a problem leading to permanent hook pollution, thanks <a
href="https://github.com/offset"><code>@​offset</code></a></li>
<li>Refactored the test suite and expanded test coverage
significantly</li>
</ul>
<h2>DOMPurify 3.4.6</h2>
<ul>
<li>Fixed several issues with DOM Clobbering in <code>IN_PLACE</code>
mode, thanks <a
href="https://github.com/offset"><code>@​offset</code></a> &amp; <a
href="https://github.com/Bankde"><code>@​Bankde</code></a></li>
<li>Hardened the checks for cross-realm <code>IN_PLACE</code> and Shadow
DOM sanitization, thanks <a
href="https://github.com/offset"><code>@​offset</code></a> &amp; <a
href="https://github.com/Bankde"><code>@​Bankde</code></a></li>
<li>Added more test coverage for <code>IN_PLACE</code> and general DOM
Clobbering attacks</li>
<li>Bumped several dependencies where possible</li>
</ul>
<h2>DOMPurify 3.4.5</h2>
<ul>
<li>Fixed a bypass caused by the new HTML element
<code>selectedcontent</code> added in 3.4.4, thanks <a
href="https://github.com/KabirAcharya"><code>@​KabirAcharya</code></a></li>
</ul>
<p><strong>Note that this is a security release for an issue introduced
in 3.4.4 and should be upgraded to immediately.</strong></p>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/cure53/DOMPurify/commit/0cae5187403132f96a6d357649e4b15633fc210a"><code>0cae518</code></a>
release: 3.4.11 (<a
href="https://redirect.github.com/cure53/DOMPurify/issues/1494">#1494</a>)</li>
<li><a
href="https://github.com/cure53/DOMPurify/commit/6ee5716f8336989753611beeca364957c0eb0c3e"><code>6ee5716</code></a>
release: 3.4.10 (<a
href="https://redirect.github.com/cure53/DOMPurify/issues/1478">#1478</a>)</li>
<li><a
href="https://github.com/cure53/DOMPurify/commit/52102472d46035857c52df19e44285f8a1e102fc"><code>5210247</code></a>
release: 3.4.9 (<a
href="https://redirect.github.com/cure53/DOMPurify/issues/1459">#1459</a>)</li>
<li><a
href="https://github.com/cure53/DOMPurify/commit/bcdd8285412dc9c4c149652aed2d712e790d6ccf"><code>bcdd828</code></a>
release: 3.4.8 (<a
href="https://redirect.github.com/cure53/DOMPurify/issues/1439">#1439</a>)</li>
<li><a
href="https://github.com/cure53/DOMPurify/commit/ca30f070c360df162a3e3848e80e6fd3c9e74bff"><code>ca30f07</code></a>
release: 3.4.7 (<a
href="https://redirect.github.com/cure53/DOMPurify/issues/1414">#1414</a>)</li>
<li><a
href="https://github.com/cure53/DOMPurify/commit/bb7739e5bccec7e1ab3dae3f3e42d02db3acaaae"><code>bb7739e</code></a>
release: 3.4.6 (<a
href="https://redirect.github.com/cure53/DOMPurify/issues/1394">#1394</a>)</li>
<li><a
href="https://github.com/cure53/DOMPurify/commit/011b0c78f2a0f57ee54f5fcccb697a46ca6e63ea"><code>011b0c7</code></a>
release: 3.4.5 (<a
href="https://redirect.github.com/cure53/DOMPurify/issues/1382">#1382</a>)</li>
<li><a
href="https://github.com/cure53/DOMPurify/commit/5817ad969c15e67dfcd6cb37248d6e9c1553e7c3"><code>5817ad9</code></a>
release: 3.4.4 (<a
href="https://redirect.github.com/cure53/DOMPurify/issues/1374">#1374</a>)</li>
<li><a
href="https://github.com/cure53/DOMPurify/commit/520edb0371a9638f9b51f1798051299a250c686b"><code>520edb0</code></a>
release: 3.4.3 (<a
href="https://redirect.github.com/cure53/DOMPurify/issues/1352">#1352</a>)</li>
<li><a
href="https://github.com/cure53/DOMPurify/commit/6f67fd396a7b8c64294343999fe607ca1f5299c0"><code>6f67fd3</code></a>
Sync/3.4.2 (<a
href="https://redirect.github.com/cure53/DOMPurify/issues/1322">#1322</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/cure53/DOMPurify/compare/3.4.0...3.4.11">compare
view</a></li>
</ul>
</details>
<details>
<summary>Install script changes</summary>
<p>This version adds <code>prepare</code> script that runs during
installation. Review the package contents before updating.</p>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=dompurify&package-manager=npm_and_yarn&previous-version=3.4.0&new-version=3.4.11)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/chatwoot/chatwoot/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Sony Mathew <sony@chatwoot.com>
2026-06-22 14:58:07 +05:30
a4be38f8f3 fix(facebook): handle messenger sticker attachment type (#14793)
Facebook Messenger sticker messages no longer break message ingestion.
Meta recently changed its webhook payloads so a sticker now arrives as a
new `sticker` attachment type (alongside the existing `image` attachment
during the transition period). Chatwoot didn't recognise `sticker` as a
valid attachment file type, so the webhook job crashed and the message —
and any others in the same batch — failed to sync. Stickers now appear
in the conversation as a single image, just like before.

Fixes https://linear.app/chatwoot/issue/PLA-177

## How to reproduce

1. Connect a Facebook Page inbox.
2. Send a sticker from Messenger to that page.
3. Before this change: the `Webhooks::FacebookEventsJob` raises
`ArgumentError: 'sticker' is not a valid file_type` and the message is
dropped.
4. After this change: the sticker shows up in the conversation as a
single image attachment.

---------

Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 12:40:23 +04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Sony Mathew
6fcc888aee chore(deps): bump oj from 3.16.10 to 3.17.3 (#14809)
Bumps [oj](https://github.com/ohler55/oj) from 3.16.10 to 3.17.3.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/ohler55/oj/releases">oj's
releases</a>.</em></p>
<blockquote>
<h2>v3.17.3</h2>
<h2>What's Changed</h2>
<ul>
<li>Fix stack limits by <a
href="https://github.com/ohler55"><code>@​ohler55</code></a> in <a
href="https://redirect.github.com/ohler55/oj/pull/1014">ohler55/oj#1014</a></li>
<li>Fix intern.c and fast.c by <a
href="https://github.com/ohler55"><code>@​ohler55</code></a> in <a
href="https://redirect.github.com/ohler55/oj/pull/1015">ohler55/oj#1015</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/ohler55/oj/compare/v3.17.1...v3.17.3">https://github.com/ohler55/oj/compare/v3.17.1...v3.17.3</a></p>
<h2>v3.17.1</h2>
<h2>What's Changed</h2>
<ul>
<li>Add <code>max_integer_digits</code> limit for legacy integer parsing
by <a href="https://github.com/meinac"><code>@​meinac</code></a> in <a
href="https://redirect.github.com/ohler55/oj/pull/1009">ohler55/oj#1009</a></li>
<li>fix: remove unsafe exec() in fast.c by <a
href="https://github.com/orbisai0security"><code>@​orbisai0security</code></a>
in <a
href="https://redirect.github.com/ohler55/oj/pull/1011">ohler55/oj#1011</a></li>
<li>Revert &quot;fix: remove unsafe exec() in fast.c&quot; by <a
href="https://github.com/ohler55"><code>@​ohler55</code></a> in <a
href="https://redirect.github.com/ohler55/oj/pull/1012">ohler55/oj#1012</a></li>
<li>Fix reentrant parser by <a
href="https://github.com/ohler55"><code>@​ohler55</code></a> in <a
href="https://redirect.github.com/ohler55/oj/pull/1013">ohler55/oj#1013</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/orbisai0security"><code>@​orbisai0security</code></a>
made their first contribution in <a
href="https://redirect.github.com/ohler55/oj/pull/1011">ohler55/oj#1011</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/ohler55/oj/compare/v3.17.0...v3.17.1">https://github.com/ohler55/oj/compare/v3.17.0...v3.17.1</a></p>
<h2>v3.17.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Implement Oj::Parser.safe with configurable JSON safety limits by <a
href="https://github.com/meinac"><code>@​meinac</code></a> in <a
href="https://redirect.github.com/ohler55/oj/pull/1007">ohler55/oj#1007</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/ohler55/oj/compare/v3.16.17...v3.17.0">https://github.com/ohler55/oj/compare/v3.16.17...v3.17.0</a></p>
<h2>v3.16.17</h2>
<h2>What's Changed</h2>
<ul>
<li>Use fast_memcpy16 to quickly copy up to 16 bytes for tail handling
in ARM Neon code and small copies when generating JSON. by <a
href="https://github.com/samyron"><code>@​samyron</code></a> in <a
href="https://redirect.github.com/ohler55/oj/pull/998">ohler55/oj#998</a></li>
<li>Bump jidicula/clang-format-action from 4.15.0 to 4.17.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/ohler55/oj/pull/999">ohler55/oj#999</a></li>
<li>Array hash as json by <a
href="https://github.com/ohler55"><code>@​ohler55</code></a> in <a
href="https://redirect.github.com/ohler55/oj/pull/1003">ohler55/oj#1003</a></li>
<li>Bump jidicula/clang-format-action from 4.17.0 to 4.18.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/ohler55/oj/pull/1001">ohler55/oj#1001</a></li>
<li>Handle unterminated strings in usual parser by <a
href="https://github.com/meinac"><code>@​meinac</code></a> in <a
href="https://redirect.github.com/ohler55/oj/pull/1002">ohler55/oj#1002</a></li>
<li>Fix read() not handling partial reads for large files by <a
href="https://github.com/ursm"><code>@​ursm</code></a> in <a
href="https://redirect.github.com/ohler55/oj/pull/1004">ohler55/oj#1004</a></li>
<li>Raise error for incomplete primitive literals by <a
href="https://github.com/meinac"><code>@​meinac</code></a> in <a
href="https://redirect.github.com/ohler55/oj/pull/1005">ohler55/oj#1005</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/ursm"><code>@​ursm</code></a> made their
first contribution in <a
href="https://redirect.github.com/ohler55/oj/pull/1004">ohler55/oj#1004</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/ohler55/oj/compare/v3.16.16...v3.16.17">https://github.com/ohler55/oj/compare/v3.16.16...v3.16.17</a></p>
<h2>v3.16.14</h2>
<h2>What's Changed</h2>
<ul>
<li>Change first arg to oj_parse_options by <a
href="https://github.com/ohler55"><code>@​ohler55</code></a> in <a
href="https://redirect.github.com/ohler55/oj/pull/987">ohler55/oj#987</a></li>
<li>Fix illegal instruction error on CPUs without SSE4.2 support by <a
href="https://github.com/sebyx07"><code>@​sebyx07</code></a> in <a
href="https://redirect.github.com/ohler55/oj/pull/993">ohler55/oj#993</a></li>
<li>Optimize <code>oj_dump_cstr</code> using SSE4.2 and SSSE3. by <a
href="https://github.com/samyron"><code>@​samyron</code></a> in <a
href="https://redirect.github.com/ohler55/oj/pull/973">ohler55/oj#973</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/ohler55/oj/compare/v3.16.13...v3.16.14">https://github.com/ohler55/oj/compare/v3.16.13...v3.16.14</a></p>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/ohler55/oj/blob/develop/CHANGELOG.md">oj's
changelog</a>.</em></p>
<blockquote>
<h2>3.17.3 - 2026-06-04</h2>
<ul>
<li>Fixed issue in intern.c and fast.c.</li>
</ul>
<h2>3.17.2 - 2026-05-27</h2>
<ul>
<li>Fixed multiple issues related to extreme sizes.</li>
</ul>
<h2>3.17.1 - 2026-05-15</h2>
<ul>
<li>Fixed &quot;quoted string not terminated&quot; error.</li>
</ul>
<h2>3.17.0 - 2026-04-19</h2>
<ul>
<li>A &quot;safe&quot; parser has been added as a variation of the
Oj:Parser thanks to <a
href="https://github.com/meinac"><code>@​meinac</code></a>.</li>
</ul>
<h2>3.16.17 - 2026-04-12</h2>
<ul>
<li>
<p>Rails optimize for Hash and Array now overrides <code>as_json</code>
for those
classes. Note that when either is optimized with
<code>Oj.optimize_rails</code>
the <code>Array.as_json</code> and <code>Hash.as_json</code> will not be
called.</p>
</li>
<li>
<p>Add support for the rails encoder <code>:only</code> and
<code>:except</code> options.</p>
</li>
<li>
<p>Handle unterminated strings in usual parser (<a
href="https://redirect.github.com/ohler55/oj/issues/1002">#1002</a>)</p>
</li>
<li>
<p>Fix read() not handling partial reads for large files (<a
href="https://redirect.github.com/ohler55/oj/issues/1004">#1004</a>)</p>
</li>
<li>
<p>Raise error for incomplete primitive literals (<a
href="https://redirect.github.com/ohler55/oj/issues/1005">#1005</a>)</p>
</li>
</ul>
<h2>3.16.16 - 2026-03-13</h2>
<ul>
<li>Not closed arrays and objects are reported corrected in the usual
parser.</li>
</ul>
<h2>3.16.15 - 2026-02-05</h2>
<ul>
<li>Fixed by putting the ostruct dependency back until a better way is
found to conditionally include it.</li>
</ul>
<h2>3.16.14 - 2026-02-04</h2>
<ul>
<li>
<p>Fixed SSE issue <a
href="https://redirect.github.com/ohler55/oj/issues/989">#989</a>.</p>
</li>
<li>
<p>Removed ostruct dependency.</p>
</li>
<li>
<p>Removed generic object JSON gem tests.</p>
</li>
</ul>
<h2>3.16.13 - 2025-12-05</h2>
<ul>
<li>Fixed rails encoding for Hash and Array subclasses.</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/ohler55/oj/commit/bbde91a679728f94c4492ebc3683f4fa3309049f"><code>bbde91a</code></a>
Fix intern.c and fast.c (<a
href="https://redirect.github.com/ohler55/oj/issues/1015">#1015</a>)</li>
<li><a
href="https://github.com/ohler55/oj/commit/495cc38fc5a02681da2175960d4a667fae48f3c9"><code>495cc38</code></a>
Update changelog</li>
<li><a
href="https://github.com/ohler55/oj/commit/810a57acc7aaacef8410b460559c08762d0eb61a"><code>810a57a</code></a>
Update changelog</li>
<li><a
href="https://github.com/ohler55/oj/commit/ec368dbe936ef0104b782e4b0f67b17d6c7276f7"><code>ec368db</code></a>
Fix stack limits (<a
href="https://redirect.github.com/ohler55/oj/issues/1014">#1014</a>)</li>
<li><a
href="https://github.com/ohler55/oj/commit/4587e87e23adc9a4163834dc8c9ba9d7206c6501"><code>4587e87</code></a>
Fix reentrant parser (<a
href="https://redirect.github.com/ohler55/oj/issues/1013">#1013</a>)</li>
<li><a
href="https://github.com/ohler55/oj/commit/ea0c3b8fdc006807dfd768606548f6f15639b7dc"><code>ea0c3b8</code></a>
Fix compile warnings</li>
<li><a
href="https://github.com/ohler55/oj/commit/45d13099a64c37e231f6cd32bb126ce0270d81c4"><code>45d1309</code></a>
Revert &quot;fix: V-001 security vulnerability (<a
href="https://redirect.github.com/ohler55/oj/issues/1011">#1011</a>)&quot;
(<a
href="https://redirect.github.com/ohler55/oj/issues/1012">#1012</a>)</li>
<li><a
href="https://github.com/ohler55/oj/commit/93ce414fc0b9b1ad2a19311101b6a443ec666320"><code>93ce414</code></a>
fix: V-001 security vulnerability (<a
href="https://redirect.github.com/ohler55/oj/issues/1011">#1011</a>)</li>
<li><a
href="https://github.com/ohler55/oj/commit/d68a31ac5bcaaeeed22fb32aeff35aef467ab8d2"><code>d68a31a</code></a>
Add <code>max_integer_digits</code> limit for legacy integer parsing (<a
href="https://redirect.github.com/ohler55/oj/issues/1009">#1009</a>)</li>
<li><a
href="https://github.com/ohler55/oj/commit/babd7a11e78a221f6434c5e9e8b9ede9947243e3"><code>babd7a1</code></a>
Remove out of date performance comparisons</li>
<li>Additional commits viewable in <a
href="https://github.com/ohler55/oj/compare/v3.16.10...v3.17.3">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=oj&package-manager=bundler&previous-version=3.16.10&new-version=3.17.3)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/chatwoot/chatwoot/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Sony Mathew <sony@chatwoot.com>
2026-06-22 14:09:28 +05:30
Aakash BakhleandGitHub c27a5916bb fix: auto enable document auto-sync (#14806)
# Pull Request Template

## Description

Auto enables document auto-sync on paid plans

fixes:
https://linear.app/chatwoot/issue/AI-186/captain-auto-sync-doesnt-turn-on-automatically-on-subscription

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


## 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-06-22 13:39:43 +05:30
Sony MathewandGitHub 66609f06fd fix: Bump Nokogiri to 1.19.4 (#14807)
# Pull Request Template

## Description

This updates Nokogiri from `1.19.3` to `1.19.4` so the bundle-audit lint
step stops flagging the newly published Nokogiri advisories. Chatwoot's
direct Nokogiri usage appears limited to ordinary HTML/XML parsing and
selector traversal, but the locked dependency is below the patched
floor, so the safe remediation is the patch-level upgrade rather than an
advisory override.

Fixes
[CW-7397](https://linear.app/chatwoot/issue/CW-7397/upgrade-nokogiri-to-1194-for-bundle-audit-advisories)

## Type of change

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

## How Has This Been Tested?

Reference failed build in CI because of bundle audit:
https://app.circleci.com/pipelines/github/chatwoot/chatwoot/114392/workflows/8a252bf9-5e58-45fd-af18-a32dbebe978b/jobs/160423

- `bundle exec bundle audit update && bundle exec bundle audit check -v`
passed with no vulnerabilities found.
- `bundle exec rspec spec/services/website_branding_service_spec.rb
spec/presenters/html_parser_spec.rb
spec/enterprise/services/enterprise/website_branding_service_spec.rb
spec/enterprise/services/captain/tools/simple_page_crawl_service_spec.rb`
passed with 27 examples and 0 failures.
- `bundle exec rubocop app/services/website_branding_service.rb
app/presenters/html_parser.rb
enterprise/app/services/page_crawler_service.rb
enterprise/app/services/captain/tools/html_page_parser.rb
enterprise/app/services/captain/tools/simple_page_crawl_service.rb`
passed with no offenses.
- `git diff --check` passed.

Note: broad `bundle exec rubocop --parallel` still reports existing
generated DB/schema offenses unrelated to this lockfile-only dependency
bump.

## 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-06-22 12:07:58 +04:00
6a262287d2 feat(captain): allow agents to report Captain messages (#14799)
Adds a cloud-only flow for agents to flag incorrect or problematic
Captain (AI) responses. Right-clicking a Captain message surfaces a
"Report message" option that opens a dialog to pick a problem type and
add a description, persisted to a new captain_message_reports table for
the team to review.


<img width="636" height="542" alt="Screenshot 2026-06-20 at 9 15 56 AM"
src="https://github.com/user-attachments/assets/afaa233d-6bd6-455a-8a33-a3796a3e3ef6"
/>
<img width="580" height="502" alt="Screenshot 2026-06-20 at 9 16 03 AM"
src="https://github.com/user-attachments/assets/2d220d99-98cc-4c5e-a325-778ceb4f7bc9"
/>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 14:47:08 -07:00
Sojan JoseandGitHub dfd656a7d9 feat(account): persist signup attribution (#14761)
This keeps Chatwoot-side attribution persistence intentionally small and
Enterprise-only.

The website owns attribution capture, normalization, and source
classification. Chatwoot Cloud only reads the already-shaped first-party
attribution cookies during the current web signup account creation path
and stores the decoded payload in internal account metadata. Self-hosted
installs remain unchanged in this repo.

## What changed

- Added Enterprise-only attribution persistence to the current Cloud web
signup account creation path.
- Stores attribution only when `ChatwootApp.chatwoot_cloud?` is true.
- Reads the existing first-touch and last-touch attribution cookies.
- Saves only the documented scalar attribution fields under account
internal metadata.
- Preserves raw attribution values and leaves escaping to display
boundaries.
- Bounds stored attribution values to the website field-size limit.
- Keeps OSS controller code unchanged.
- Keeps signup attribution request coverage in Enterprise specs.
- Avoids backend attribution derivation, referrer parsing, or fallback
classification.
- Skips authenticated add-workspace flows so additional workspaces are
not counted as signup attribution.
- Does not hook unused account creation paths or OmniAuth account
creation.

## How to test

- `bundle exec rubocop
spec/controllers/api/v1/accounts_controller_spec.rb
spec/enterprise/controllers/api/v1/accounts_controller_spec.rb
enterprise/app/controllers/enterprise/api/v1/accounts_settings.rb
enterprise/app/services/internal/accounts/marketing_attribution_service.rb
spec/enterprise/services/internal/accounts/marketing_attribution_service_spec.rb`
- `bundle exec rspec spec/controllers/api/v1/accounts_controller_spec.rb
spec/enterprise/controllers/api/v1/accounts_controller_spec.rb
spec/enterprise/services/internal/accounts/marketing_attribution_service_spec.rb`
- On a cloud-like setup, create an account through the current web
signup path with attribution cookies and confirm account internal
metadata is populated.
- On a non-cloud setup or authenticated add-workspace flow, confirm
account-create behavior is unchanged and no attribution is stored.
2026-06-19 11:01:12 -07:00
Vishnu NarayananandGitHub b02e732dd1 fix: throttle DELETE conversation API per account (CW-7356) (#14747)
## Description

A single account deleted ~167,000 conversations via the API in 60
minutes (avg 46 rps, peak 151 rps/min) on 15 Jun 2026 21:00 to 22:00
IST. The cascade through `dependent: :destroy_async` on conversations,
then messages, then reporting_events, created enough job queue and DB
pressure to drive RDS CPU to ~93%, triggering 5xx alerts and
connection-pool timeouts.

This adds a `Rack::Attack` per-account throttle on `DELETE
/api/v1/accounts/:id/conversations/:id`:

- Default 60 req/min per account; configurable via the
`RATE_LIMIT_CONVERSATION_DELETE` env var.


Fixes [CW-7356](https://linear.app/chatwoot/issue/CW-7356)
2026-06-19 15:11:39 +05:30
80b132e155 feat(tiktok): add cloud warning (#14768)
Adds Cloud-only warning on the TikTok connect page while North American
account connections may be temporarily unavailable.

Fixes
https://linear.app/chatwoot/issue/CW-7322/cloud-show-tiktok-north-america-warning-on-connect-page

---------

Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
2026-06-19 12:29:07 +04:00
Sivin VargheseandGitHub 32b63ad0c0 feat: add search to Help Center admin pages (#14772) 2026-06-19 12:41:21 +05:30
Sivin VargheseandGitHub ac15339456 fix: Resolve Firefox input issues and persist advanced filters (#14781)
# Pull Request Template

## Description

This PR fixes a few issues in Global Search and removes a non-functional
control.

* Fixes an issue in Firefox where characters could be dropped while
typing in the search input. The search now uses the latest input value
directly, preventing searches from running one character behind.

* Removes a stale query sync in `SearchHeader` that could overwrite
recently typed characters during the debounce window, causing the input
to appear out of sync.

* Fixes advanced search filters being removed from the URL on page
reload. The search page now waits for account data to load before
parsing URL parameters, ensuring agent, inbox, and date range filters
are preserved.

* Removes the non-functional "Sort by relevance" button from the search
tabs bar, as it was disabled and had no effect.


Fixes https://github.com/chatwoot/chatwoot/issues/14684
[CW-7305](https://linear.app/chatwoot/issue/CW-7305/global-search-drops-characters-while-typing-in-firefox-query-truncated)
[CW-7370](https://linear.app/chatwoot/issue/CW-7370/remove-non-functional-relevance-placeholder-button-from-global-search)

## Type of change

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

## How Has This Been Tested?

### Screencast

**Before**


https://github.com/user-attachments/assets/48d72a4e-20f4-4f24-91f4-2c9a9c065eba




**After**


https://github.com/user-attachments/assets/0e41a803-b4fd-410d-a739-549f72d418d6





## 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
- [ ] 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-06-19 10:46:47 +04:00
Sony Mathew f99187646e Merge branch 'release/4.15.1' into develop 2026-06-17 18:07:48 +05:30
Sony Mathew 5ce6e00730 Bump version to 4.15.1 2026-06-17 18:06:42 +05:30
Vishnu NarayananandGitHub f66b551c7d revert: Sidebar unread counts for filters (CW-7262) (#14769)
## Description

Reverts [#14726](https://github.com/chatwoot/chatwoot/pull/14726)
(\"feat: Add sidebar unread counts for filters (CW-7262)\"), which
shipped in 4.15.0.

After 4.15.0 rolled out to prod the unread-counts-for-filters code path
caused a cascading incident:

- `Counter#ensure_filters_cache!` fires on every `/unread_counts/index`
and `update_last_seen` request.
- On cache miss it calls `Builder#build_filters_for!`, which:
- invokes `store.clear_user_filters!` -> `delete_matching` -> a Redis
`SCAN_each` over a per-user pattern keyspace, and
- runs 4 fresh SQL passes per user (mentions, participating, unattended,
and per-folder `Conversations::FilterService` queries).
- Threads blocked in the SCAN held their DB connections, the connection
pool exhausted, Sidekiq jobs were discarded with
`ActiveJob::DeserializationError: could not obtain a connection from the
pool`, and the enqueued queue blew past 200K.


Related:
[CW-7262](https://linear.app/chatwoot/issue/CW-7262/unread-counts-for-filters-folders)

## Type of change

- [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?


## 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
2026-06-17 14:58:48 +04:00
Sony Mathew c12e1f8348 Merge branch 'release/4.15.0' into develop 2026-06-17 00:11:59 +05:30
Sony Mathew 124782938d Bump version to 4.15.0 2026-06-17 00:10:47 +05:30
Vishnu NarayananandGitHub c70a57407d fix: label Chatwoot Mobile sessions instead of 'Unknown Device' (#14753)
## Description

Sessions created by logins from the Chatwoot Mobile app currently render
as "Unknown Device" in the dashboard sessions UI. The mobile app's HTTP
layer sends:

- Android: `User-Agent: okhttp/4.9.2`
- iOS: `User-Agent: Chatwoot/<build> CFNetwork/<v> Darwin/<v>`

Neither pattern is classifiable by the `browser` gem, so
\`browser_name\`, \`platform_name\`, and \`device_name\` all end up
"Unknown".

This change adds a backend-only fallback in
\`UserSessionTrackingService\`. When \`Browser.new(ua)\` returns
"Unknown Browser" and the UA matches a known Chatwoot Mobile pattern,
the labels are overridden to \`Chatwoot Mobile\` + \`Android\` /
\`iPhone\`. The Vue sessions list (\`ActiveSessions.vue\`) then renders
"Chatwoot Mobile on Android" or "Chatwoot Mobile on iPhone" with the
smartphone icon.

This is the immediate floor. A follow-up will add structured
\`X-Chatwoot-*\` headers from the mobile app so we can render full
version + device model.

Fixes INF-75.

## Type of change

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

## How Has This Been Tested?

- Added specs

## 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-06-17 00:05:29 +05:30
fe6368b42e feat: Add sidebar unread counts for filters (CW-7262) (#14726)
## Description

Extends the conversation unread-count system so the left sidebar can
show unread badges for Mentions, Participating, Unattended, and saved
conversation folders. Folder badges reuse the existing `custom_filters`
conversation filter semantics, store user-scoped Redis sets lazily, and
skip unsupported folder filters so invalid saved folders continue to
render without a badge. The Unattended badge counts all visible unread
open conversations that match the existing unattended conversation
scope.

Closes
-
[CW-7262](https://linear.app/chatwoot/issue/CW-7262/unread-counts-for-filters-folders)

## What changed

- Added user-scoped unread-count Redis keys and cache builders for
mentions, participating conversations, unattended conversations, and
saved folder filters.
- Reused `Conversations::FilterService` through a relation-returning
path so folder counts match the folder conversation list behavior.
- Invalidated user filter caches from mention, participant,
custom-filter, and relevant conversation update events.
- Extended the unread-count endpoint payload and sidebar Vuex/sidebar
rendering for the new badge counts, including the Unattended sidebar
item.
- Added Ruby, Enterprise, request, listener, and frontend store coverage
for the new unread-count dimensions.

## Type of change

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

## How Has This Been Tested?

- Created local validation folders for `john@acme.inc` and confirmed the
unread-count payload includes open, resolved, and high-priority folder
badges while excluding the invalid unsupported folder.
- Added coverage for the Unattended badge rule: all visible unread open
conversations matching `Conversation.unattended`.
- Ran focused unread-count Ruby specs, including service, listener,
request, and Enterprise counter coverage.
- Ran frontend unread-count store specs.
- Ran RuboCop on the touched Ruby files.
- Ran ESLint through the project script; it completed with warnings in
existing unrelated files and no errors.

<img width="369" height="525" alt="Screenshot 2026-06-13 at 10 51 39 PM"
src="https://github.com/user-attachments/assets/36b1d2c4-dac1-4f6f-9c0e-7ef5a6cc2975"
/>

## 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] Documentation changes are not required for this internal
unread-count behavior
- [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] No dependent downstream changes are required

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2026-06-16 23:46:49 +05:30
Sivin VargheseandGitHub 352f120c6a feat: add dedicated color tokens for voice call widget (#14745)
# Pull Request Template

## Description

This PR introduces dedicated color tokens for the floating voice call
widget instead of reusing generic design tokens. This allows the widget
to maintain a distinct dark-card appearance across both light and dark
themes.

Adds the following tokens and applies them to `CallCard`:

* `--call-widget`
* `--call-widget-border`
* `--call-widget-text`
* `--call-widget-sub-text`

Fixes
https://linear.app/chatwoot/issue/CW-7353/call-notification-window-theme-update-design

## Type of change

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

## How Has This Been Tested?

### Screenshots

**Light mode**
<img width="1466" height="815" alt="Screenshot 2026-06-16 at 3 47 32 PM"
src="https://github.com/user-attachments/assets/732164b1-5488-4cc1-8c71-01245d906842"
/>


**Dark mode**
<img width="1466" height="815" alt="Screenshot 2026-06-16 at 3 46 54 PM"
src="https://github.com/user-attachments/assets/536ab632-4c7c-486b-8fc4-a061ee40b22a"
/>



## 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
- [ ] 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-06-16 20:01:31 +05:30
Vishnu NarayananandGitHub a585853690 chore: expose advanced_search flag in super_admin UI for self-hosted (#14750)
## Description

Removes `chatwoot_internal: true` from the `advanced_search` feature
flag so self-hosted enterprise userss can toggle it from the super_admin
UI instead of going through a rails console.
2026-06-16 07:10:24 -07:00
64b0ebd8dc feat: Script to migrate images between providers (#9117)
# Pull Request Template

## Description

Storage migration functionality, which allows the transfer of images
from one on-premises provider to another (e.g. AWS, Google, etc.) using
Active Storage.
I ran the unit and integration tests and they all passed correctly.

**The command to execute the migration is `FROM=from_service
TO=to_service rake storage:migrate`**

Fixes #7907

## Type of change

Please delete options that are not relevant.

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

## How Has This Been Tested?

I have tested the storage migration functionality by running the
provided unit and integration tests. Additionally, I manually tested the
migration process in a local development environment by following these
steps:

Set up two storage services (e.g., AWS S3 and Google Cloud Storage) with
valid configurations.
Execute the `FROM=local TO=amazon rake storage:migrate` task with
appropriate FROM and TO arguments to migrate blobs between the services.
Verified that the blobs were successfully transferred to the target
storage service.
Checked for any error messages or unexpected behavior during the
migration process.


## 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: Sony Mathew <sony@chatwoot.com>
Co-authored-by: Sony Mathew <2040199+sony-mathew@users.noreply.github.com>
2026-06-16 19:12:49 +05:30
Vishnu NarayananandGitHub cf6ee13665 feat: scale email rate limits by agent count for paid plans (#13966)
Paid plan accounts with many agents were hitting the flat daily email
rate limit cap.
This multiplies the plan base limit by the account's agent seat count,
so larger teams
get proportionally higher limits. Free/hacker plan keeps the flat limit
unchanged.

Fixes https://linear.app/chatwoot/issue/CW-6664

## How to test
1. Set up `ACCOUNT_EMAILS_PLAN_LIMITS` config with plan limits (e.g.,
`{"hacker": 10, "startups": 20, "business": 30, "enterprise": 40}`)
2. Create an account on a paid plan (e.g., startups) with multiple agent
seats
3. Verify `account.email_rate_limit` returns `base_limit × agent_seats`
(e.g., 20 × 5 = 100)
4. Create an account on the hacker plan — verify limit stays flat (10)
5. Set a per-account override via super admin `limits.emails` — verify
it takes priority over the multiplied limit

## What changed
- `plan_email_limit` now multiplies the base plan limit by agent seat
count for paid plans
- Added `free_plan?` helper to skip the multiplier for the default/free
plan
2026-06-16 18:56:30 +05:30
41a3ab6dfa feat(companies): add contact company selector (#14496)
Adds a company selector to the contact details form so agents can
associate a contact with an existing company directly from the contact
page.

Closes

- None

Why

Contacts already expose company information through the CRM fields, but
the form only accepted free-text company names. As we split company CRM
work into smaller PRs, this keeps the contact page aligned with the
structured company model while preserving the existing company-name
behavior used by automations.

What changed

- Shows a company dropdown in the contact details form when the
Companies feature is enabled.
- Keeps legacy free-text company names editable when a contact has no
structured `company_id`.
- Allows Enterprise contact create/update APIs to accept account-scoped
`company_id`.
- Syncs `additional_attributes.company_name` when a contact is
associated with a company, including the existing email-domain
auto-association path.
- Serializes `company_id` in the contact model payload so the form can
show the current association.

How to test

1. Enable Companies for an account and open a contact details page.
2. In Edit contact details, use the Company field to select an existing
company.
3. Save the contact and refresh the page.
4. Confirm the selected company remains visible and the contact is
associated with that company.
5. Confirm contacts with only a legacy free-text company name still show
the text input instead of an empty selector.

---------

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
Co-authored-by: Sony Mathew <2040199+sony-mathew@users.noreply.github.com>
2026-06-16 15:29:27 +05:30
de137e8297 feat: Capture CTWA referral metadata for WhatsApp conversations (#14681)
Adds support for capturing Click-to-WhatsApp ad referral metadata from
incoming WhatsApp messages.

This stores Meta Cloud API `referral` payloads on the incoming message
`content_attributes` and normalizes Twilio `Referral*` callback fields
into the same shape. The UI display is intentionally deferred until
Instagram, Messenger, and TikTok referral payloads are captured as well,
so we can design one cross-channel referral surface instead of a
WhatsApp-only sidebar block.

Why message-level storage

Meta sends CTWA referral details as part of the inbound message payload,
not as a stable conversation-level webhook. The referral represents the
exact ad click that produced that specific customer message, and later
messages in the same conversation may not carry the same context.
Storing the normalized referral on the message preserves the original
webhook semantics, avoids adding conversation-level attribution that
could become stale or ambiguous, and keeps support for both Cloud API
and Twilio payloads aligned behind `content_attributes.referral`.
Fixes
https://linear.app/chatwoot/issue/CW-7090/surface-meta-ctwa-referral-on-incoming-whatsapp-messages-cloud-api
Closes https://github.com/chatwoot/chatwoot/issues/13995,
https://github.com/chatwoot/chatwoot/issues/12560,
https://github.com/chatwoot/chatwoot/issues/13006


Related community PRs
- https://github.com/chatwoot/chatwoot/pull/13130
- https://github.com/chatwoot/chatwoot/pull/14180
- https://github.com/chatwoot/chatwoot/pull/14121

Related follow-ups
-
https://linear.app/chatwoot/issue/CW-7301/capture-instagram-ad-referral-metadata-on-incoming-messages
-
https://linear.app/chatwoot/issue/CW-7302/capture-messenger-ad-referral-metadata-on-facebook-page-conversations
-
https://linear.app/chatwoot/issue/CW-7303/capture-tiktok-ad-referral-metadata-from-im-referral-msg-events

How to test
1. Send or replay a WhatsApp Cloud API inbound message that contains a
`messages[0].referral` payload from a Click-to-WhatsApp ad.
2. Confirm the generated incoming message stores the payload under
`content_attributes.referral`.
3. Repeat with a Twilio WhatsApp callback containing `Referral*` fields
and confirm the stored message has the same normalized
`content_attributes.referral` structure.

---------

Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
2026-06-16 09:58:29 +04:00
Sivin VargheseandGitHub a3d05ef55d chore: Update emoji picker in widget (#14741) 2026-06-16 10:39:47 +05:30
dda25c0b51 fix(portal): emit valid BCP 47 html lang attribute (#14680)
Locales are stored with underscores (e.g. pt_BR), but the HTML lang
attribute requires hyphens (pt-BR). Convert via a helper in the public
portal layouts.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
2026-06-15 14:34:52 -07:00
Sivin VargheseandGitHub 1713cabdf2 feat: Add emoji & icon picker for Help Center categories (#14702) 2026-06-16 02:10:05 +05:30
d8a16278b9 fix: added ordering capability for sidebar sections folders, teams, channels and labels (CW-7193) (#14609)
## Description

* Added the ability to sort for 4 sub-sections under conversations
folders, teams, channels and labels.
* the sort options are basically created at, alphabetical and unread
counts along with both directions.
* for folders we don't have an unread count, so we sort it by only
created and alphabetical.
* all the sort preferences are stored on the frontend - easiest
implementation for now.

Fixes # CW-7193

## 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 by visually verifying the changes. Also ran the
newly added tests for component level changes.

Here is the screenshot of the changes:

Added the sort option to sub sections in the conversation sidebar:
<img width="282" height="848" alt="Screenshot 2026-06-02 at 1 58 12 AM"
src="https://github.com/user-attachments/assets/4a7c6061-86e3-438a-92ae-ee643a0128b6"
/>

The sort options looks like this:
<img width="783" height="698" alt="Screenshot 2026-06-02 at 1 58 49 AM"
src="https://github.com/user-attachments/assets/a15bb0a7-b810-4423-a88c-fbd84d0476c0"
/>


## 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: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
2026-06-15 21:34:19 +05:30
Sony MathewandGitHub 4816e923b6 chore: added a new sort by unread option for conversations (CW-7152) (#14512)
## Description

Added a new option for the sort by option in conversation filters called
unread. This is to filter out unread conversations.
Fixes # CW-7152

## 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 manually in local.

<img width="1397" height="603" alt="Screenshot 2026-05-20 at 10 40
20 PM"
src="https://github.com/user-attachments/assets/6c60263e-907b-419f-a7ed-06010bbe8736"
/>


## 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
2026-06-15 17:58:04 +05:30
396631ad7d feat: enforce concurrent session limit with login picker (CW-7169) (#14621)
## Description

Cap active sessions at `MAX_USER_SESSIONS` which defaults to existing
value of `25` per user. This ensure existing user login behavior is not
affected for self-hosted installations. Browser users at the cap see a
session picker (409 response) to choose which session to end.
Non-browser clients and partially-tracked users get silent
oldest-session eviction.

Depends on #14556.

## Type of change

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

## How Has This Been Tested?

Specs cover: under limit, at limit (browser picker, non-browser
eviction), partial tracking fallback, revoke single/all sessions during
login, session row creation on successful login.

---------

Co-authored-by: Sony Mathew <sony@chatwoot.com>
2026-06-15 17:28:13 +05:30
Vishnu NarayananandGitHub ba0ba46c9c fix: skip session tracking and use short-lived token for impersonation (CW-7169) (#14622)
## Description

SuperAdmin impersonation SSO logins no longer create UserSession rows
visible to the customer. Impersonation tokens use a 2-day lifespan
instead of ~2 months, so they naturally evict first and don't linger in
the user's token list.

Server-side detection via Redis value (`'impersonation'` vs `'normal'`)
without changing the `valid_sso_auth_token?` signature. Backward
compatible with in-flight tokens.

Depends on #14556.

## Type of change

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

## How Has This Been Tested?

Specs cover: impersonation login skips UserSession creation,
impersonation token has short lifespan, normal SSO login still creates
UserSession row.

## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [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-06-15 17:14:34 +05:30
Vishnu NarayananandGitHub ee6382109a feat: prevent deleted email conversations from syncing again (#14612)
# Pull Request Template

## Description

Prevent deleted email conversations from being synced into Chatwoot
again while they are still within the IMAP sync window.

When an admin explicitly deletes an email conversation, the incoming
email message IDs are stored temporarily in Redis. IMAP sync checks
these recently deleted message IDs in addition to existing message
records. Each Redis key expires automatically after two days.

This applies only to explicit conversation deletion. Individual message
deletion, inbox deletion, and account deletion keep their existing
behavior.

Fixes
[CW-7214](https://linear.app/chatwoot/issue/CW-7214/deleted-mails-in-gmail-inbox-gets-synced-again)
2026-06-15 17:06:40 +05:30
Sivin VargheseandGitHub 35bef21f83 feat: add media view for contacts (#14393) 2026-06-15 15:42:11 +05:30
e5c140158e fix(new-conversation): stop writing literal "undefined" as mail_subject (#14383)
## Description

This PR fixes a data-corruption bug in the new conversation flow that
surfaces in conversation search results as `Subject: undefined`.

### The Problem
When creating a conversation through the "New conversation" modal
without typing a subject (every non-email channel never shows the
subject input, and email channels can be left blank), the conversation
gets persisted with `additional_attributes.mail_subject = "undefined"`
(the literal string). The bogus value shows up in conversation search
results as `Subject: undefined`, which started rendering after #10843.

### Root Cause
`createConversationPayload` in
`app/javascript/dashboard/store/modules/contactConversations.js` appends
the `mail_subject` field unconditionally:

```js
payload.append('additional_attributes[mail_subject]', mailSubject);
```

`composeConversationHelper.js` only sets `payload.mailSubject` when
`subject` is truthy, so when no subject is provided, `mailSubject` is
destructured as `undefined` in `createConversationPayload`.
`FormData.append` coerces `undefined` to the string `"undefined"`, and
the backend persists it as-is into the JSONB column.

### The Fix
Skip the append when `mailSubject` is falsy. The backend already treats
a missing key the same as an empty subject (the email mailer falls back
to a default subject when `mail_subject` is `nil`), so omitting the
field is safe across channels.

### Key Changes
- Guarded the `additional_attributes[mail_subject]` append in
`createConversationPayload`.
- Added a spec covering the case where `mailSubject` is omitted.

> Note: existing rows persisted before this fix will continue to show
`Subject: undefined` in search until cleaned up. A simple SQL cleanup
for non-email inboxes:
>
> ```sql
> UPDATE conversations
> SET additional_attributes = additional_attributes - 'mail_subject'
> FROM inboxes
> WHERE conversations.inbox_id = inboxes.id
>   AND inboxes.channel_type <> 'Channel::Email'
> AND conversations.additional_attributes->>'mail_subject' =
'undefined';
> ```
>
> I'm leaving any data-cleanup migration out of this PR since it's a
maintenance concern that may be handled differently per installation.

## Type of change

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

## How Has This Been Tested?

1. Open the "New conversation" modal on a non-email inbox (e.g.,
WhatsApp, SMS, API).
2. Create a conversation without filling any subject (field is not
present on those, so regular flow).
3. Open the global search and search for the conversation.
4. **Before:** the result card shows `Subject: undefined`.
   **After:** the subject row is hidden.
5. Repeat on an email inbox leaving the subject blank — same result.
6. On an email inbox, type a subject and verify it still persists and
renders correctly.
7. Run the new spec: `pnpm test contactConversations`.

## 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>
Co-authored-by: Sony Mathew <sony@chatwoot.com>
2026-06-14 08:57:38 +05:30
274e92e0e4 chore: collapse conversation sidebar sections (folders, teams, inboxes and labels) - CW-7059 (#14509)
## Description

Added ability to collapse conversation sidebar sections (folders, teams,
inboxes and labels)

Fixes #CW-7059

## 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 locally. Added specs. Attaching the loom for them same.


https://github.com/user-attachments/assets/40d613e7-6c82-4078-abf4-79739a00f718


## 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: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
2026-06-14 06:54:18 +05:30
27404b4a27 fix: redact sensitive integration secrets from API responses (#14147)
## Summary

The `GET /api/v1/accounts/:id/integrations/apps` endpoint returns raw
secret values (OpenAI API keys, Google service account private keys,
Linear refresh tokens, etc.) in hook settings within the JSON response.
Although gated behind an administrator check, these secrets are visible
in the browser network tab.

This PR filters hook settings through the existing `visible_properties`
whitelist defined in `config/integration/apps.yml`, and adds explicit
whitelists to integrations that were missing them.

Closes #14042

## Bug reproduction

**Setup:** Created an OpenAI integration hook with a fake API key
(`sk-test-secret-12345`).

**Step 1 — Browser Network tab shows raw secrets:**

<img width="1676" height="869" alt="Screenshot 2026-04-24 at 14 32 28"
src="https://github.com/user-attachments/assets/6fc69463-365e-458b-b216-98a7390bf528"
/>


Navigate to Settings > Integrations as an admin. Open DevTools Network
tab and observe the response from `GET
/api/v1/accounts/:id/integrations/apps`. The hook settings contain the
full API key in plaintext:

```json
"hooks": [
  {
    "id": 1,
    "app_id": "openai",
    "settings": {
      "api_key": "sk-test-secret-12345",
      "label_suggestion": false
    }
  }
]
```

**Step 2 — API call confirms the leak:**

```bash
curl -s "http://localhost:3000/api/v1/accounts/2/integrations/apps" \
  -H "access-token: <token>" \
  -H "client: <client>" \
  -H "uid: user@test.com" \
  | jq '.payload[] | select(.id == "openai") | {id, name, hooks: [.hooks[] | {id, app_id, settings}]}'
```

Response:

```json
{
  "id": "openai",
  "name": "OpenAI",
  "hooks": [
    {
      "id": 1,
      "app_id": "openai",
      "settings": {
        "api_key": "sk-test-secret-12345",
        "label_suggestion": false
      }
    }
  ]
}
```

Any admin user can extract the raw API key from the response. The same
applies to other integrations — Dialogflow exposes full Google service
account credentials, Linear exposes refresh tokens, etc.

## After fix

After applying this change, the GET
/api/v1/accounts/:id/integrations/apps endpoint no longer returns
sensitive secret values in hook settings. Instead, the response is
filtered using each integration’s visible_properties whitelist, ensuring
only safe, user-facing fields are exposed. For example, OpenAI
integrations return non-sensitive fields like label_suggestion while
excluding raw API keys. This prevents secrets from being exposed in the
browser network tab or API responses, even for authenticated admin
users.

```bash
curl -X GET "http://localhost:3000/api/v1/accounts/2/integrations/apps" \
  -H "Accept: application/json" \
  -H "Authorization: Bearer eyJhY2Nlc3MtdG9rZW4iOiJqZG5jOWg4TnljaWFJa3JlZkxGQzRnIiwidG9rZW4tdHlwZSI6IkJlYXJlciIsImNsaWVudCI6Ik81bjVnTDFEOVVhbGpwbWxjaHZNanciLCJleHBpcnkiOiIxNzgyMzMyNTA4IiwidWlkIjoidXNlckB0ZXN0LmNvbSJ9" \
  -H "access-token: jdnc9h8NyciaIkrefLFC4g" \
  -H "client: O5n5gL1D9UaljpmlchvMjw" \
  -H "uid: user@test.com" \
  | jq '.payload[] | select(.id == "openai") | {id, name, hooks: [.hooks[] | {id, app_id, settings}]}'
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 10174  100 10174    0     0  33232      0 --:--:-- --:--:-- --:--:-- 33357
{
  "id": "openai",
  "name": "OpenAI",
  "hooks": [
    {
      "id": 1,
      "app_id": "openai",
      "settings": {
        "label_suggestion": false
      }
    }
  ]
}
```

## What changed

- Added `visible_properties` accessor to `Integrations::App` model to
expose the whitelist from config
- Updated `_hook.json.jbuilder` to filter `resource.settings` through
the associated app's `visible_properties` instead of returning the full
hash
- Added `visible_properties` to integrations that were missing it:
  - Linear: `[]` (settings contain refresh_token)
  - Notion: `[]` (OAuth-based, no user-facing settings)
- Slack: `['channel_name']` (UI needs this to display connected channel)
  - Shopify: `[]` (no settings)

App config metadata (`_app.json.jbuilder`) is left unchanged —
hook_type, settings_form_schema, etc. are not secrets and the frontend
depends on them.

## How to test

1. Create an OpenAI integration hook with an API key
2. As an admin, call `GET /api/v1/accounts/:id/integrations/apps`
3. Verify hook settings include `api_key` (whitelisted) but not raw
credential objects
4. For Dialogflow hooks, verify `credentials` (private key JSON) is
excluded while `project_id` is included
5. For Slack hooks, verify `channel_name` is still returned
6. For Linear hooks, verify `refresh_token` is not returned

---------

Co-authored-by: Botshelo Nokoane (Konstruktors) <botshelo@entersekt.com>
Co-authored-by: Sony Mathew <sony@chatwoot.com>
Co-authored-by: Sony Mathew <2040199+sony-mathew@users.noreply.github.com>
2026-06-14 06:53:13 +05:30
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Sony MathewSony Mathew
006b529918 chore(deps): bump net-imap from 0.4.24 to 0.6.4.1 (#14688)
Bumps [net-imap](https://github.com/ruby/net-imap) from 0.4.24 to
0.6.4.1.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/ruby/net-imap/releases">net-imap's
releases</a>.</em></p>
<blockquote>
<h2>v0.5.15</h2>
<h2>What's Changed</h2>
<h3>🔒 Security</h3>
<p>This release fixes several more security vulnerabilities which are
related to the fixes in <code>v0.5.14</code>. Please see the linked
security advisories for more information.</p>
<ul>
<li><em>(moderate)</em> Command Injection via non-synchronizing literal
in &quot;raw&quot; argument (CVE-2026-47240, GHSA-8p34-64r3-mwg8)
This vulnerability depends how the server interprets non-synchronizing
literals.
The connection is <em>not</em> vulnerable if the server supports
non-synchronizing literals.
<ul>
<li>🥅 Validate non-synchronizing literals support by <a
href="https://github.com/nevans"><code>@​nevans</code></a> in <a
href="https://redirect.github.com/ruby/net-imap/pull/703">ruby/net-imap#703</a>
(backports <a
href="https://redirect.github.com/ruby/net-imap/pull/701">ruby/net-imap#701</a>)</li>
</ul>
</li>
<li><em>(moderate)</em> Command Injection via unvalidated ID and ENABLE
arguments (CVE-2026-47242, GHSA-46q3-7gv7-qmgg)
<ul>
<li>🥅 Validate <code>ID</code> values contain only valid bytes by <a
href="https://github.com/nevans"><code>@​nevans</code></a> in <a
href="https://redirect.github.com/ruby/net-imap/pull/703">ruby/net-imap#703</a>
(backports <a
href="https://redirect.github.com/ruby/net-imap/pull/698">ruby/net-imap#698</a>)</li>
<li>🥅 Validate <code>#enable</code> arguments are all atoms by <a
href="https://github.com/nevans"><code>@​nevans</code></a> in <a
href="https://redirect.github.com/ruby/net-imap/pull/703">ruby/net-imap#703</a>
(backport <a
href="https://redirect.github.com/ruby/net-imap/pull/699">ruby/net-imap#699</a>)
<strong>NOTE:</strong> <code>#enable</code> should
<strong><em>never</em></strong> be called with untrusted input.</li>
</ul>
</li>
<li><em>(low)</em> Denial of Service via incomplete &quot;raw&quot;
argument validation (CVE-2026-47241, GHSA-c4fp-cxrr-mj66)
This results in the affected command hanging until the connection is
closed. If another thread attempts to send a concurrent pipelined
command, the first thread will return with a syntax error and the second
thread will hang until the connection closes.
<ul>
<li><em>Reported by <a
href="https://github.com/fg0x0"><code>@​fg0x0</code></a></em></li>
<li>🐛 Prevent trailing <code>{0}</code> in RawData validation by <a
href="https://github.com/nevans"><code>@​nevans</code></a> in <a
href="https://redirect.github.com/ruby/net-imap/pull/703">ruby/net-imap#703</a>
(backports <a
href="https://redirect.github.com/ruby/net-imap/pull/700">ruby/net-imap#700</a>)</li>
</ul>
</li>
</ul>
<h3>Fixed</h3>
<ul>
<li>🥅 Validate that Atom and Flag are not empty by <a
href="https://github.com/nevans"><code>@​nevans</code></a> in <a
href="https://redirect.github.com/ruby/net-imap/pull/685">ruby/net-imap#685</a>
(backports <a
href="https://redirect.github.com/ruby/net-imap/pull/684">ruby/net-imap#684</a>)</li>
<li>🧵 Fix deadlock in <code>#disconnect</code> by <a
href="https://github.com/nevans"><code>@​nevans</code></a> in <a
href="https://redirect.github.com/ruby/net-imap/pull/697">ruby/net-imap#697</a>
(backports <a
href="https://redirect.github.com/ruby/net-imap/pull/686">ruby/net-imap#686</a>)</li>
</ul>
<h3>Documentation</h3>
<ul>
<li>⚠️ Boost visibility of raw data argument documentation warnings by
<a href="https://github.com/nevans"><code>@​nevans</code></a> in <a
href="https://redirect.github.com/ruby/net-imap/pull/696">ruby/net-imap#696</a>
(backports <a
href="https://redirect.github.com/ruby/net-imap/pull/677">ruby/net-imap#677</a>)</li>
</ul>
<h3>Other Changes</h3>
<ul>
<li>🏷️ Allow 64-bit Integer arguments in <a
href="https://redirect.github.com/ruby/net-imap/pull/696">ruby/net-imap#696</a>
(backports <a
href="https://redirect.github.com/ruby/net-imap/pull/675">ruby/net-imap#675</a>)</li>
<li>🥅 Ensure send_number_data input is an Integer in <a
href="https://redirect.github.com/ruby/net-imap/pull/696">ruby/net-imap#696</a>
(backports <a
href="https://redirect.github.com/ruby/net-imap/pull/676">ruby/net-imap#676</a>)</li>
<li>♻️ Improve <code>RawData.new</code>, Add <code>RawData.split</code>
by <a href="https://github.com/nevans"><code>@​nevans</code></a> in <a
href="https://redirect.github.com/ruby/net-imap/pull/696">ruby/net-imap#696</a>
(backports <a
href="https://redirect.github.com/ruby/net-imap/pull/679">ruby/net-imap#679</a>)</li>
<li>🥅 Validate response literal byte size format by <a
href="https://github.com/nevans"><code>@​nevans</code></a> in <a
href="https://redirect.github.com/ruby/net-imap/pull/696">ruby/net-imap#696</a>
(backports <a
href="https://redirect.github.com/ruby/net-imap/pull/681">ruby/net-imap#681</a>)</li>
</ul>
<h3>Miscellaneous</h3>
<ul>
<li> Improvements to tests' FakeServer in <a
href="https://redirect.github.com/ruby/net-imap/pull/696">ruby/net-imap#696</a>
(backports <a
href="https://redirect.github.com/ruby/net-imap/pull/678">ruby/net-imap#678</a>)</li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/ruby/net-imap/compare/v0.5.14...v0.5.15">https://github.com/ruby/net-imap/compare/v0.5.14...v0.5.15</a></p>
<h2>v0.5.14</h2>
<h2>What's Changed</h2>
<h3>🔒 Security</h3>
<p>This release contains fixes for <strong>multiple
vulnerabilities</strong> concerning <em><strong><code>STARTTLS</code>
stripping</strong></em>, argument validation, and denial of service
attacks.</p>
<blockquote>
<p>[!WARNING]
<a
href="https://redirect.github.com/ruby/net-imap/pull/665">ruby/net-imap#665</a>
fixes a <code>STARTTLS</code> stripping vulnerability
(GHSA-vcgp-9326-pqcp).
Without this fix, a man-in-the-middle attacker can cause
<code>Net::IMAP#starttls</code> to return &quot;successfully&quot;,
<strong><em>without starting TLS</em></strong>.</p>
</blockquote>
<blockquote>
<p>[!IMPORTANT]
Argument validation is significantly improved. Several command injection
vulnerabilities have been fixed:
<a
href="https://redirect.github.com/ruby/net-imap/pull/662">ruby/net-imap#662</a>
fixes CRLF/command/argument injection via Symbol arguments
(GHSA-75xq-5h9v-w6px).
<a
href="https://redirect.github.com/ruby/net-imap/pull/662">ruby/net-imap#662</a>
fixes CRLF/command/argument injection via the <code>attr</code> argument
to <code>#store</code>/<code>#uid_store</code> (GHSA-hm49-wcqc-g2xg)</p>
</blockquote>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/ruby/net-imap/commit/ce20fc8e9974ed87d0848dc411eef490623cdeb9"><code>ce20fc8</code></a>
🔖 Bump version to 0.5.15</li>
<li><a
href="https://github.com/ruby/net-imap/commit/0b7b83c19312dd3f44cb72be2e7681289324d9db"><code>0b7b83c</code></a>
🔀 Merge pull request <a
href="https://redirect.github.com/ruby/net-imap/issues/703">#703</a>
from ruby/backport/v0.5/security-patches</li>
<li><a
href="https://github.com/ruby/net-imap/commit/f22fd6cee01140b53cdb7fc8d42a352a734f4509"><code>f22fd6c</code></a>
🍒 pick 0ea9eba3 (<a
href="https://redirect.github.com/ruby/net-imap/issues/701">#701</a>): 
Fix flaky tests for MacOS, TruffleRuby</li>
<li><a
href="https://github.com/ruby/net-imap/commit/12460740451d064cef84f59dd8c91baba0388ae1"><code>1246074</code></a>
🍒 pick ae9f83b5 (<a
href="https://redirect.github.com/ruby/net-imap/issues/701">#701</a>):
♻️ Extract str.bytesize lvar in send_literal</li>
<li><a
href="https://github.com/ruby/net-imap/commit/a2f61af6b7a7e6f0e4f27691531de6e4861318cd"><code>a2f61af</code></a>
🍒 pick 62a0da6d (<a
href="https://redirect.github.com/ruby/net-imap/issues/701">#701</a>): 🥅
Validate non-synchronizing literals support</li>
<li><a
href="https://github.com/ruby/net-imap/commit/e33348ccdc639d0d4414f72e4ba7024bdb24bf29"><code>e33348c</code></a>
🍒 pick d6ddd294 (<a
href="https://redirect.github.com/ruby/net-imap/issues/700">#700</a>): 🐛
Prevent trailing <code>{0}</code> in RawData validation</li>
<li><a
href="https://github.com/ruby/net-imap/commit/4f81b69044297de02ada859fffba6ad9923a43a5"><code>4f81b69</code></a>
🍒 pick 1f97168b (<a
href="https://redirect.github.com/ruby/net-imap/issues/699">#699</a>): 🥅
Validate <code>#enable</code> arguments are all atoms</li>
<li><a
href="https://github.com/ruby/net-imap/commit/69da4a490dd148526fb287a5c82056bfd20c6f95"><code>69da4a4</code></a>
🍒 pick 8d9397ab (<a
href="https://redirect.github.com/ruby/net-imap/issues/698">#698</a>): 🥅
Validate QuotedString contains only valid bytes</li>
<li><a
href="https://github.com/ruby/net-imap/commit/7aab580ad35f2cad1404a58bf0ff015567f2593c"><code>7aab580</code></a>
🍒 pick e3c50fad (<a
href="https://redirect.github.com/ruby/net-imap/issues/698">#698</a>):
♻️ Refactor RawText, add improve test coverage</li>
<li><a
href="https://github.com/ruby/net-imap/commit/fac1733d45ec57da3675c4d266c771c73c7265bb"><code>fac1733</code></a>
🍒 pick aab64f92 (<a
href="https://redirect.github.com/ruby/net-imap/issues/686">#686</a>): 🧵
Fix deadlock in <code>#disconnect</code></li>
<li>Additional commits viewable in <a
href="https://github.com/ruby/net-imap/compare/v0.4.24...v0.5.15">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=net-imap&package-manager=bundler&previous-version=0.4.24&new-version=0.5.15)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/chatwoot/chatwoot/network/alerts).

</details>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Sony Mathew <sony@chatwoot.com>
Co-authored-by: Sony Mathew <2040199+sony-mathew@users.noreply.github.com>
2026-06-13 22:49:03 +05:30
Sony MathewandGitHub b1c2db5435 fix: Stabilize help center builder error spec (#14725)
## Description

Stabilizes the enterprise help center article builder source URL
validation spec by asserting the custom exception via its class name and
message text. This keeps the spec focused on the intended behavior while
avoiding brittle custom exception constant identity checks in
CI/reloading environments.

A bunch of builds on different PRs have been failing because of this
error, sample traces are below:
*
https://app.circleci.com/pipelines/github/chatwoot/chatwoot/114064/workflows/bdb6eca9-3b65-4c38-b8cf-f2f8564476f8/jobs/158777
*
https://app.circleci.com/pipelines/github/chatwoot/chatwoot/114064/workflows/bdb6eca9-3b65-4c38-b8cf-f2f8564476f8/jobs/158777

Fixes # N/A

## Type of change

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

## How Has This Been Tested?

- `/Users/sonymathew/.rbenv/shims/bundle exec rspec
spec/enterprise/services/onboarding/help_center_article_builder_spec.rb`
- `/Users/sonymathew/.rbenv/shims/bundle exec rubocop
spec/enterprise/services/onboarding/help_center_article_builder_spec.rb`
- `git diff --check`

## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] My changes generate no new warnings
- [x] New and existing unit tests pass locally with my changes
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] Any dependent changes have been merged and published in downstream
modules
2026-06-13 19:57:42 +05:30
Vishnu NarayananandGitHub 92a1fb8ab7 feat: manage active user sessions from profile (CW-7169) (#14556)
## Description

First PR of user_sessions feature - enforcement, impersonation and mfa will be handled separately.

Adds an Active Sessions section under Profile where users can see every
device currently logged in and revoke any session they don't recognize.
Helps users lock down stale or unrecognized logins on their own without
needing support.

**Behavior at the limit, by client:**
- **Browser:** returns 409 with a picker overlay; user picks a session
to revoke or chooses "End all sessions" to clear them.

- **Mobile / API client:** silently evicts the oldest session and
proceeds with login (no picker UI to render).
- **Pre-tracking users** (token rows without `user_sessions`, i.e.
anyone already logged in before this ships): silent-evict any untracked
token first, so freshly tracked sessions are never killed in favor of
legacy ones.

Sessions are stored in a new `user_sessions` table keyed on `(user_id,
client_id)` with browser, platform, IP, last activity and (when
configured) geo. Kept in sync with `user.tokens` via an after_save
callback so revoking a token from any path cleans up the row.

Fixes https://linear.app/chatwoot/issue/CW-7169

## Type of change

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

## How Has This Been Tested?

- Added specs.
- Manual local testing: browser picker fires at limit; pre-tracking user
silent-evicts; mixed tracked/untracked correctly drops the untracked one
first; profile page revoke succeeds; current session cannot be revoked
from profile.

## 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-06-12 16:11:07 +05:30
e055cead35 fix: only subscribe calls webhook field when voice calling enabled (#14718)
## Description

Solves issue https://github.com/chatwoot/chatwoot/issues/14690

## Type of change

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

## How has this been tested?

- 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-06-12 12:25:25 +05:30
c041fde3a2 fix: Preserve original filenames for WhatsApp cloud attachments (#14168)
## Summary

Preserves the original filename provided in the WhatsApp Cloud
attachment payload when downloading media files.

## Problem

Files containing accented or special characters could be saved with
malformed names or incorrect extensions due to relying on remote
download metadata.

## Changes Made

- Uses attachment_payload[:filename] when present
- Creates a tempfile using the original filename and extension
- Preserves content type metadata
- Falls back to existing behavior when no filename is provided

## Notes

This should improve attachment downloads for filenames containing
accented characters.
Related to #10973

---------

Co-authored-by: Rorrick Smith <rorrick@bulksms.com>
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>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-12 10:21:42 +04:00
c6a38e2fc6 fix: Keep Instagram scopes out of new Messenger OAuth flows (#14695)
Removes Instagram permissions from the Facebook Messenger inbox setup
flow to avoid blocking Meta App Review for Messenger-only apps.

Meta rejects or stalls Messenger-only app reviews when the OAuth flow
requests unrelated Instagram permissions such as `instagram_basic` and
`instagram_manage_messages`. This blocks approval for Messenger
permissions like `human_agent`, even when the user only wants to
configure a Facebook Messenger inbox.

New Facebook Messenger inbox setup now requests only the Page and
Messenger permissions required for Messenger. Existing Facebook
Messenger inboxes that already have Instagram details continue to
request Instagram permissions during reauthorization, preserving support
for the legacy combined Facebook/Instagram inbox flow.

Going forward, new Instagram setups should use the dedicated Instagram
channel inbox instead of being bundled into the Messenger setup flow.

Closes #13860

## How to test

1. Go to Settings → Inboxes → Add Inbox → Facebook Messenger.
2. Click Login with Facebook.
3. Verify the OAuth scope does not include `instagram_basic` or
`instagram_manage_messages`.
4. Reauthorize an existing Facebook Messenger inbox with `instagram_id`
present.
5. Verify the reauth scope still includes `instagram_basic` and
`instagram_manage_messages`.

---------

Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
2026-06-12 09:39:26 +04:00
Sugar Ray LedesmaandGitHub 2663a8495d refactor(security): rename path_without_extensions in Rack::Attack (#14216)
Fix misspelling 'extentions' and clarify comments. Behavior unchanged:
same path-stripping logic for throttle matching (e.g. /auth and
/auth.json). Also correct 'You may' in remote_ip comment.
2026-06-11 18:36:34 +05:30
Sivin VargheseandGitHub d0d275d962 fix: cannot resize pasted images (#14709) 2026-06-11 16:47:11 +05:30
95d6aecb51 chore: Add security flags to session cookie configuration (#14248)
## Summary
- Adds `httponly` and `secure` flags to session cookie configuration
- Adds RSpec tests for session configuration

## Changes
- `config/initializers/session_store.rb`: add `httponly: true`, `secure:
FORCE_SSL`
- `spec/config/session_store_spec.rb`: tests for all session store
options

Ref #2683 (hardens the session cookie but does not remove it, sessions
are still needed for super_admin dashboard)

---------

Co-authored-by: Adam-Relay <adam@userelay.ai>
Co-authored-by: Adam Berger <adam@adam-berger.com>
Co-authored-by: Vishnu Narayanan <iamwishnu@gmail.com>
2026-06-11 16:13:57 +05:30
a901c87ab4 perf: merge 3 conversation COUNT queries in /meta into single query (#13536)
# Pull Request Template

## Description


- Merges 3 separate COUNT queries in
`ConversationFinder#set_count_for_all_conversations` into a single query
using PostgreSQL `COUNT(*) FILTER (WHERE ...)`
- Every call to `/meta` and conversation index fires 3 COUNT queries
against the conversations table — one for "mine", one for "unassigned",
one for "all". These share the same base query and scan the same rows,
but execute as 3 independent round-trips.

from pg_stats

| Query | Calls | Total DB Time | Avg Latency |
|-------|-------|--------------|-------------|
| COUNT unassigned | 4.1M | 141,081 sec | 34ms |
| COUNT all | 4.1M | 132,302 sec | 32ms |
| COUNT mine | 3.7M | 37,637 sec | 10ms |
| **Total** | **~12M** | **311,020 sec** | |

`/meta` alone runs at 2.3K RPM (NewRelic), triggered on every WebSocket
event (conversation created/updated/status changed/assignee changed).



Fixes
https://linear.app/chatwoot/issue/INF-43/make-single-query-to-count-the-assigned-unassigned-and-all-instead-of

## Type of change

- [x] Performance fix

## How Has This Been Tested?

- [x] Verify conversation list shows correct
mine/unassigned/assigned/all counts
- [x] Verify /meta endpoint returns correct counts
- [x] Verify counts update correctly when assigning/unassigning
conversations
- [x] Verify counts with team filter applied
- [x] Monitor DB query count reduction in NewRelic after deploy

## 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: Sony Mathew <ynos1234@gmail.com>
2026-06-11 16:10:48 +05:30
Sony MathewandGitHub 6d26e7930a fix(contacts): truncate inbound contact names (IMAP sender display names) (#14631)
## Description

Truncates long contact names in `ContactInboxWithContactBuilder` before
persisting them, so inbound IMAP sender display names over the
`contacts.name` 255-character limit no longer raise
`ActiveRecord::RecordInvalid` from `Inboxes::FetchImapEmailsJob`. This
keeps email ingestion moving while preserving the existing contact name
length constraint.

Linear ticket:
[https://linear.app/chatwoot/issue/CW-7263/sentry-active-record-invalid-contact-create-from-imap-fetch-job](https://linear.app/chatwoot/issue/CW-7263/sentry-active-record-invalid-contact-create-from-imap-fetch-job)

## Type of change

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

## How Has This Been Tested?

- [x] `bundle exec rspec
spec/builders/contact_inbox_with_contact_builder_spec.rb`
- [x] `bundle exec rspec spec/mailboxes/imap/imap_mailbox_spec.rb`
- [x] `bundle exec rubocop
app/builders/contact_inbox_with_contact_builder.rb
spec/builders/contact_inbox_with_contact_builder_spec.rb`

## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [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
2026-06-11 15:15:14 +05:30
Tanmay Deep SharmaandGitHub 8d5d02ea97 feat: add per-inbox toggle to disable incoming calls (#14645)
## Description

Adds a per-inbox "Allow incoming calls" toggle for voice-enabled
WhatsApp and Twilio inboxes. When turned off, the setting is persisted
on the channel; actually rejecting inbound calls is handled in a
follow-up PR.

## Type of change

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

## Screenshot
<img width="804" height="384" alt="Screenshot 2026-06-04 at 11 44 07 AM"
src="https://github.com/user-attachments/assets/df8bb026-0387-4031-bcba-6d9a56872eb7"
/>


## 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-06-11 14:22:44 +05:30
ce93ddec78 fix: re-fetch widget conversation status on reconnect (#14683)
When an inbox has **"Allow messages after conversation is resolved"**
disabled, the live-chat widget should hide the reply box once a
conversation is resolved — but users could still send a reply, silently
reopening it. This syncs the widget's conversation status on reconnect
so the reply box hides correctly.

## Closes
- CW-7272

## How to reproduce
1. Disable **Allow messages after conversation is resolved** on an
inbox.
2. Open the widget, then let its websocket drop (background tab / lose
network).
3. While disconnected, get the conversation resolved (e.g. auto-resolve
on inactivity).
4. Reconnect → reply box is still visible and a sent message reopens the
resolved conversation.

## Why
Reply-box hiding is gated on the widget's local status, updated via the
`conversation.status_changed` socket event. If the resolve happens while
disconnected, the event is missed — and on reconnect the widget only
re-synced messages, never the status, so it stayed stale at `open`.

## What changed
- `onReconnect` now also dispatches
`conversationAttributes/getAttributes` to refresh status after a missed
event.
- Added a spec covering the reconnect behavior.

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
2026-06-11 14:09:13 +05:30
Sivin VargheseandGitHub 940428c10e fix: preserve markdown links with unsafe href characters in help center content (#14686) 2026-06-11 13:47:08 +05:30
4d3196b02f feat: Show unread count for all conversations (#14627)
## Description

Adds the unread count for All Conversations to the left sidebar. The
unread counts API now returns an `all_count` aggregate based on the same
permission-scoped inbox counts already used for sidebar badges, and the
sidebar renders that backend-provided value on the All Conversations
item.

Fixes
[CW-7240](https://linear.app/chatwoot/issue/CW-7240/unread-count-for-all-conversations)

## Type of change

- [ ] 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?

- `bundle exec rspec
spec/services/conversations/unread_counts/counter_spec.rb
spec/enterprise/services/conversations/unread_counts/counter_spec.rb
spec/controllers/api/v1/accounts/conversations_controller_spec.rb`
- `pnpm test
app/javascript/dashboard/store/modules/specs/conversationUnreadCounts/actions.spec.js
app/javascript/dashboard/store/modules/specs/conversationUnreadCounts/getters.spec.js
app/javascript/dashboard/store/modules/specs/conversationUnreadCounts/mutations.spec.js`
- `pnpm exec eslint
app/javascript/dashboard/components-next/sidebar/Sidebar.vue
app/javascript/dashboard/store/modules/conversationUnreadCounts.js
app/javascript/dashboard/store/modules/specs/conversationUnreadCounts/actions.spec.js
app/javascript/dashboard/store/modules/specs/conversationUnreadCounts/getters.spec.js
app/javascript/dashboard/store/modules/specs/conversationUnreadCounts/mutations.spec.js`
- `bundle exec rubocop
app/services/conversations/unread_counts/counter.rb
spec/services/conversations/unread_counts/counter_spec.rb
spec/enterprise/services/conversations/unread_counts/counter_spec.rb
spec/controllers/api/v1/accounts/conversations_controller_spec.rb`

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

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2026-06-11 13:05:52 +05:30
Tanmay Deep SharmaandGitHub c6e5657ee5 fix(call): emit assignee activity message when agent answers a call (#14700)
## Linear Ticket
- https://linear.app/chatwoot/issue/CW-7249/debug-assignment-log

## Description

When an agent answers an inbound WhatsApp call on an unassigned
conversation, the conversation is claimed for that agent — but no
"assigned" activity message or assignee-changed event was emitted, so
the assignment was invisible in the timeline (and notifications/live
assignee panel didn't update).

The claim ran before `update_conversation_call_status`, so that later
`update!` on the same conversation clobbered
`saved_change_to_assignee_id?` before `after_commit` fired. Moving the
claim to be the conversation's final write in the transaction restores
the activity message, the `ASSIGNEE_CHANGED` event, and the live
assignee update.

## Type of change

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

## How Has This Been Tested?

1. Open an unassigned WhatsApp-call conversation.
2. As an agent, answer an inbound call.
3. Before: the conversation is assigned to you, but no "self-assigned"
activity message appears. After: the activity message is created and the
assignee updates live.

## 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-06-11 11:16:41 +05:30
Sony Mathew e9a92a6366 Merge branch 'release/4.14.2' into develop 2026-06-10 22:44:31 +05:30
Sony Mathew bec795f764 Bump version to 4.14.2 2026-06-10 22:43:22 +05:30
Shivam MishraandGitHub e6dfb91fcc refactor: use SafeFetch for website branding page fetch (#14693) 2026-06-10 16:26:24 +05:30
Shivam MishraandGitHub d9c07fe2e9 feat: expose onboarding help center generation status (#14671)
This PR adds a reload-safe onboarding Help Center generation status
path. Previously, generation progress was pushed through ActionCable,
which meant the onboarding UI could lose context after a page reload or
missed websocket event. The new endpoint exposes the current generation
id, raw Redis generation state, and Help Center article/category counts
so clients can recover state by fetching from the backend.

**What changed**

- Added an onboarding Help Center generation status endpoint.
- Persisted `help_center_generation_id` when generation is enqueued.
- Removed Help Center generation ActionCable broadcasts completely.
- Kept generation progress in Redis as the backend source of truth.
- Kept Help Center generation out of the onboarding hot path; the
existing onboarding controller does not start generation yet.

**How to test**

1. Start Help Center generation for an account.
2. Fetch the onboarding generation status endpoint and verify it returns
the generation id, Redis state, and article/category counts.
3. Reload the onboarding UI or client state and fetch again to confirm
progress can be recovered without relying on websocket events.
4. Verify skipped/completed generation states are reflected from Redis.

## Related PRs

- https://github.com/chatwoot/chatwoot/pull/14569
- https://github.com/chatwoot/chatwoot/pull/14568
- https://github.com/chatwoot/chatwoot/pull/14567
- https://github.com/chatwoot/chatwoot/pull/14619
- https://github.com/chatwoot/chatwoot/pull/14565 (Primary onboarding
PR)
2026-06-10 16:24:37 +05:30
f93f2067b6 fix(whatsapp): Drop obsolete WABA scope check broken by Meta embedded signup (#14697)
## Description

Fixes WhatsApp embedded signup failing with "No WABA scope found in
token." Meta recently changed which scopes embedded-signup tokens carry
(whatsapp_business_manage_events instead of
whatsapp_business_management), which tripped a brittle scope-string
check. That check was redundant anyway — PhoneInfoService already
verifies the token's access to the specific WABA by calling its
/phone_numbers endpoint right before it. This removes the obsolete
TokenValidationService and relies on the functional check.

## 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: Muhsin Keloth <muhsinkeramam@gmail.com>
2026-06-10 13:22:30 +04:00
Shivam MishraandGitHub 3dfb5061e1 refactor: route captain custom tool requests through SafeFetch (#14620)
Captain custom tools previously used their own hand-rolled `Net::HTTP`
request code. This moves them onto `SafeFetch`, the same shared
HTTP-fetching helper already used by webhooks, uploads, avatar imports,
and the Captain page crawler. Behavior for normal tools is unchanged —
they just now share one consistent path for host resolution, timeouts,
response size limits, and redirect handling.

**What changed**
- `HttpTool#execute_http_request` now delegates to `SafeFetch.fetch`
instead of building `Net::HTTP` requests by hand. Auth headers, basic
auth, metadata headers, JSON content-type, and the 1 MB response cap all
map onto `SafeFetch` options.
- Removed ~80 lines of bespoke request/validation plumbing from
`HttpTool`.
- The custom tools `test` endpoint now reads the response body string
directly (the executor returns the body rather than a response object).

**How to test**
1. Enable `custom_tools` (or `captain_integration_v2`) for an account.
2. Create a Captain custom tool pointing at a public HTTPS endpoint
(e.g. a test API).
3. Use the **Test** button in the tool form — you should get a success
result.
4. Run the tool from a Captain conversation and confirm the response is
returned/templated as before.

**Gotchas**
- **Local dev:** `SafeFetch` blocks requests to private/loopback
addresses by default. If you're testing a custom tool against a service
on `localhost` or a private IP during development, set
`SAFE_FETCH_ALLOW_PRIVATE_NETWORK=true` or the request will be rejected.
(This matches how the rest of `SafeFetch` already behaves locally.)
- **Test endpoint status field:** on success the `test` response now
reports `status: 200` rather than the exact 2xx code (201/204/etc.),
because `SafeFetch` signals success-vs-failure rather than exposing the
raw response. The UI only checks the 2xx range, so this is invisible
there — but worth knowing if anything consumes the API directly.
- **Non-2xx responses** still surface as an error (same as before), now
via `SafeFetch::HttpError`.
2026-06-10 14:01:30 +05:30
72a59e4795 fix: update oauth dependencies (#14691)
Updates the locked OAuth dependencies to patched versions so bundle
audit no longer reports the current OAuth advisories.

What changed
- Updated `oauth` from `1.1.0` to `1.1.6` for `GHSA-prq8-7wvh-44qh`.
- Updated `oauth2` from `2.0.9` to `2.0.22` for `GHSA-pp92-crg2-gfv9`.
- Accepted Bundler's required transitive lockfile updates for the
patched OAuth gems.

How to test
1. Run `bundle exec bundle audit update && bundle exec bundle audit
check -v` and confirm no vulnerabilities are reported.
2. Smoke test OAuth-based authentication and email integration flows.

Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
2026-06-10 12:33:46 +05:30
Sivin VargheseandGitHub cabe9bc733 fix: populate general settings form on hard reload (#14685) 2026-06-10 10:56:17 +05:30
33f7550525 fix(whatsapp): restrict OGG voice recording to WhatsApp Cloud inboxes (#14692)
Recording and sending an audio message from a **Twilio WhatsApp** inbox
failed silently — Twilio rejected the media with delivery error `63019`
("Media failed to download") and the voice note never reached the
customer. This restores audio sending for Twilio WhatsApp (and
360dialog) inboxes.

Closes Regression from #14606

## How to reproduce

1. Open a conversation in a **Twilio WhatsApp** inbox.
2. Record a voice message in the reply box and send it.
3. Before this fix: the message fails to deliver and a
`Webhooks::TwilioDeliveryStatusJob` is enqueued with `ErrorCode: 63019`,
`ErrorMessage: "Media failed to download"`.
4. After this fix: the audio is recorded as MP3 and delivers normally.

## What changed

PR #14606 added WhatsApp **Cloud** voice notes, which require OGG/Opus.
It changed `audioRecordFormat` in `ReplyBox.vue` to return OGG for
`isAWhatsAppChannel` — but that getter is also `true` for Twilio
WhatsApp inboxes. The OGG handling (content-type normalization + the
`voice: true` flag) lives only in `WhatsappCloudService`, so Twilio
could not download/process the remuxed OGG file.

This change scopes OGG to `isAWhatsAppCloudChannel`. Twilio WhatsApp,
360dialog, and Telegram fall back to MP3 exactly as they did before the
PR.

🤖 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.8 (1M context) <noreply@anthropic.com>
2026-06-10 08:52:34 +04:00
Sivin VargheseandGitHub 59d869d1ed feat: Ability to resize table column width (#14611) 2026-06-09 18:04:48 +05:30
8a3b129292 fix: Disable re-oauth flow for manual whatsapp (#13599)
Restrict the WhatsApp reauthorization flag to channels whose
provider_config source is 'embedded_signup'. Previously the view exposed
resource.channel.reauthorization_required? for all WhatsApp channels;
now it only returns true when (provider_config || {}).to_h['source'] ==
'embedded_signup' && resource.channel.reauthorization_required?. This
prevents showing reauthorization prompts for manual/API-key flows
(non-OAuth) and safely handles nil provider_config.

# Pull Request Template

## Description

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

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

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

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
2026-06-09 11:12:03 +04:00
af1dfc21f6 fix: include account data in webhook payloads (#12445)
Conversation and inbox webhook payloads now include the account object
(`{ id, name }`) so integrations can reliably identify the account
without depending on nested message data.

## Closes

Closes #11753
Closes #12442

## Why

Message, contact, and contact inbox webhook payloads already expose
`account`. Conversation and inbox webhook payloads were outliers, which
forced webhook consumers to infer account context from nested messages
or other fields that may not always be present.

## What changed

- Adds `account: { id, name }` to conversation webhook payloads.
- Adds webhook-specific inbox payload data with `account: { id, name }`
for inbox created/updated events.
- Keeps non-webhook conversation and inbox push payload behavior
unchanged.

## How to test

- Configure an account webhook for `conversation_created`, trigger a new
conversation, and confirm the webhook payload includes `account.id` and
`account.name`.
- Configure an account webhook for `inbox_created`, create a new inbox,
and confirm the webhook payload includes `account.id` and
`account.name`.
- Configure a webhook agent bot and update a conversation status, then
confirm the bot webhook payload includes the same account object.

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Sojan Jose <sojan@pepalo.com>
2026-06-09 12:12:46 +05:30
d1c482cb64 fix(email): strip null bytes from inbound mailbox messages (#14546)
Inbound emails containing null bytes could fail while Chatwoot persisted
the incoming message, causing IMAP sync to drop and repeatedly retry the
same malformed email. This strips null bytes at the inbound mailbox
message persistence boundary so the rest of the email can be saved
normally.

Fixes
https://linear.app/chatwoot/issue/CW-7165/argumenterror-string-contains-null-byte-argumenterror
Sentry: https://chatwoot-p3.sentry.io/issues/7405946944/

## Test
- [x] added specs
- [x] manually tested the logic on prod for the inbox(email) which was
throwing the error

## What changed

- Sanitizes null bytes only while building attributes for inbound
mailbox message persistence.
- Uses the sanitized source ID for both duplicate lookup and message
creation.
- Adds regression coverage for the mailbox message creation path.

---------

Co-authored-by: Sony Mathew <2040199+sony-mathew@users.noreply.github.com>
Co-authored-by: Sony Mathew <sony@chatwoot.com>
2026-06-08 17:12:25 +05:30
d8656edc61 fix(facebook): Stop force tagging Messenger replies with MESSAGE_TAG/ACCOUNT_UPDATE (#14329)
Outbound Facebook Messenger replies were always sent with
`messaging_type: MESSAGE_TAG` and a `tag` of either `HUMAN_AGENT` or
`ACCOUNT_UPDATE` (fallback). `ACCOUNT_UPDATE` is reserved by Meta policy
for non-recurring account notifications (password resets, suspicious
activity, account-status changes), so general agent replies were getting
rejected by Facebook with "Invalid parameter" and risked page-level
penalties. After this fix, Facebook treats default replies as standard
`RESPONSE` messages within the 24-hour window, and only attaches
`HUMAN_AGENT` tagging when the operator opts in via
`ENABLE_MESSENGER_CHANNEL_HUMAN_AGENT`.

## Closes

<!-- Add the relevant issue link, e.g. Closes #1234 -->

## How to reproduce

1. Connect a Facebook page inbox.
2. Leave `ENABLE_MESSENGER_CHANNEL_HUMAN_AGENT` disabled (default).
3. From the dashboard, reply to an active Messenger conversation with a
regular/promotional message.
4. Before this PR: the request to Graph API includes
`messaging_type=MESSAGE_TAG&tag=ACCOUNT_UPDATE`. Facebook returns
"Invalid parameter" for content that does not match the `ACCOUNT_UPDATE`
use case, and the message is marked failed in Chatwoot.
5. After this PR: the request omits `messaging_type` and `tag`, so
Facebook treats it as a standard `RESPONSE` and accepts it within the
24-hour customer service window.

## What changed

- `app/services/facebook/send_on_facebook_service.rb` now mirrors the
Instagram service pattern: text and attachment params are built without
`messaging_type`/`tag` by default, and a shared `merge_human_agent_tag`
helper attaches `MESSAGE_TAG`/`HUMAN_AGENT` only when
`ENABLE_MESSENGER_CHANNEL_HUMAN_AGENT` is enabled.
- Removed the unused `sent_first_outgoing_message_after_24_hours?`
helper (dead code with no callers).
- Updated `spec/services/facebook/send_on_facebook_service_spec.rb`:
dropped `ACCOUNT_UPDATE` expectations, added a spec asserting no
`messaging_type`/`tag` is sent by default, and tightened the HUMAN_AGENT
spec to verify both `messaging_type` and `tag`.

## Operator note

Operators who were relying on the prior `ACCOUNT_UPDATE` fallback to
bypass the 24-hour window were already in violation of Meta policy and
likely seeing send failures. They should enable
`ENABLE_MESSENGER_CHANNEL_HUMAN_AGENT` (and request the Human Agent
permission from Meta) for the 7-day extended window

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
2026-06-08 15:28:36 +04:00
e919a2cef5 feat: add contact filter for conversations (#14629)
# Pull Request Template

## Description

Adds a Contact condition to the conversation advanced filter so agents
can search for an existing contact and filter conversations by
`conversations.contact_id`.

Fixes
[CW-7239](https://linear.app/chatwoot/issue/CW-7239/contact-filter-for-conversations)

## Type of change

- [ ] 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?

- `eval "$(rbenv init -)" && bundle exec rspec
spec/services/conversations/filter_service_spec.rb`
- `pnpm exec vitest --no-watch --no-cache --no-coverage --logHeapUsage
app/javascript/dashboard/components-next/filter/helper/filterHelper.spec.js
app/javascript/dashboard/store/modules/conversations/helpers/specs/filterHelpers.spec.js
app/javascript/dashboard/helper/specs/customViewsHelper.spec.js
app/javascript/dashboard/helper/specs/filterQueryGenerator.spec.js`
- `pnpm eslint`
- `eval "$(rbenv init -)" && bundle exec rubocop
spec/services/conversations/filter_service_spec.rb`

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

---------

Co-authored-by: iamsivin <iamsivin@gmail.com>
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
2026-06-08 16:00:17 +05:30
Sojan JoseandGitHub 7718f2a62c fix: update puma for security advisories (#14670)
Updates Puma to the patched 7.2 line so bundle-audit no longer reports
the high-severity Puma advisories affecting the current lockfile.

Fixes: CVE-2026-47736, CVE-2026-47737
Closes: N/A

## Why

The refreshed ruby-advisory-db flagged Puma 6.4.3 for two high-severity
PROXY Protocol v1 parser advisories. The advisory-recommended fixed
versions are the Puma 7.2 patch line or Puma 8, so this keeps the update
to the smaller fixed line.

## What changed

- Constrains Puma to `~> 7.2`, `>= 7.2.1`
- Updates the lockfile from Puma 6.4.3 to 7.2.1
- Updates nio4r from 2.7.3 to 2.7.5 as the transitive Puma dependency

## Validation

- `bundle exec bundle-audit check` -> `No vulnerabilities found`
- `bundle exec rails runner 'puts "Rails #{Rails.version}; Puma
#{Puma::Const::PUMA_VERSION}; Rack #{Rack.release}"'` -> `Rails 7.1.5.2;
Puma 7.2.1; Rack 3.2.6`
- `bundle exec rails server -p 3051` booted Puma 7.2.1 successfully
- Smoke checked `/`, `/api`, unknown-route 404, and `/cable` WebSocket
upgrade
2026-06-08 15:40:43 +05:30
Aakash BakhleandGitHub 45f4b423ae fix: captain usage for BYOK OpenAI tasks (#14587)
# Pull Request Template

## Description

Fixes:
https://linear.app/chatwoot/issue/CW-7167/label-suggestions-bad-ux

## Type of change

Please delete options that are not relevant.

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


## 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-06-08 13:37:21 +05:30
Shivam MishraandGitHub 78a6b2457d refactor(onboarding): extract whatsapp and facebook channel connect into reusable composables (#14619)
Extracts the Meta JS SDK logic for the WhatsApp embedded-signup and
Facebook Page connect flows out of their settings components into two
reusable composables. No behavior change — the settings inbox-creation
flows work exactly as before; this just makes the SDK orchestration
reusable (an upcoming onboarding PR consumes them) and adds unit
coverage.

## What changed
- `useWhatsappEmbeddedSignup` — owns the embedded-signup popup (SDK
load, FB.login, and the auth-code / postMessage race).
`WhatsappEmbeddedSignup.vue` now consumes it.
- `useFacebookPageConnect` — owns FB.login (page scopes) + page fetch,
with SDK preload split from login so the popup opens within the click's
activation window. `Facebook.vue` now consumes it.
- Adds unit specs for both composables.

## Related PRs

- https://github.com/chatwoot/chatwoot/pull/14569
- https://github.com/chatwoot/chatwoot/pull/14568
- https://github.com/chatwoot/chatwoot/pull/14567
- https://github.com/chatwoot/chatwoot/pull/14649
- https://github.com/chatwoot/chatwoot/pull/14565 (Primary onboarding
PR)
2026-06-08 13:13:20 +05:30
Shivam MishraandGitHub 73ba0b26e5 fix(instagram): process webhook events after mutex retry exhaustion (#14647)
Instagram webhooks use a Redis mutex to protect the first-message path
for a contact/account pair. When multiple webhook events for the same
Instagram contact arrive at nearly the same time, they can all enter the
“find or create” flow together.

There are two pieces involved:

`ContactInbox` maps the external Instagram scoped user id to a Chatwoot
contact inside an inbox. That side already has a unique `(inbox_id,
source_id)` index and retry handling, so duplicate contact-inbox
creation is mostly protected at the database layer.

`Conversation` creation is more fragile. The message builder looks for
an existing active conversation for the contact/inbox and creates one
when none is found. If two first-message jobs run concurrently, both can
see “no active conversation yet” and both can create a conversation. The
mutex exists to bump those jobs apart long enough for the first one to
create the conversation, so the next one appends to it instead of
creating a duplicate.

In production, the old behavior could turn that race dampener into a
drop path. Once `Webhooks::InstagramEventsJob` exhausted its lock
retries, ActiveJob stopped retrying with
`MutexApplicationJob::LockAcquisitionError`. Since ActiveJob retries are
not FIFO, later jobs could still win the lock while older jobs kept
getting deferred until exhaustion.

## Mutex Application Job Changes

This adds `retry_on_lock_conflict` as a small wrapper around the
existing `retry_on LockAcquisitionError` pattern.

The new API keeps the default behavior intact, but lets jobs explicitly
define what should happen after lock retry exhaustion:

```ruby
retry_on_lock_conflict wait: 1.second,
                       attempts: 3,
                       on_exhaustion: :process_without_lock
```

The fallback receives the original job arguments, so job classes do not
need to reach into ActiveJob internals like `arguments.first`.

## Instagram Fallback

Instagram now processes the webhook payload even after the mutex retry
window is exhausted.

This matches what the mutex was meant to do in the first place: bump
concurrent events apart long enough to avoid duplicate conversation
creation, not permanently block or drop webhook events. If a job cannot
acquire the lock after a few retries, it continues through the normal
Instagram processing path without the mutex.

## Retry And Lock Tuning

The Instagram lock retry window now uses deterministic backoff:

```ruby
retry_on_lock_conflict wait: ->(executions) { executions.seconds },
                       attempts: 3,
                       on_exhaustion: :process_without_lock
```

The lock TTL is also set explicitly:

```ruby
with_lock(key, 3.seconds)
```

This matters because Rails treats `attempts` as total executions, not
retries after the first execution. With a fixed `wait: 1.second` and
`attempts: 3`, the exhaustion handler can run roughly two seconds after
the first lock conflict. That is earlier than a 3-second lock TTL, so
the fallback could process without the lock while the original mutex is
still valid. That would reopen the duplicate-conversation race the lock
is meant to dampen.

Using a proc wait makes the timing predictable and avoids Rails jitter
for this retry path. The first conflict waits about 1 second, the second
waits about 2 seconds, and the final attempt happens around the 3-second
mark. That lines the retry window up with the Redis lock TTL before
falling back to `process_without_lock`.

The protected race window is intentionally small. `ContactInbox`
creation already has database uniqueness protection, while conversation
creation is the softer `find active conversation || create` path. We
only need to give the first job enough time to create the conversation
state that later jobs should reuse.

The mutex still does not preserve message order, and ActiveJob retries
are not FIFO. A longer retry window would mostly add latency for hot
Instagram contacts without making ordering more correct. After the lock
TTL has elapsed, processing without the lock is the better tradeoff than
dead-lettering customer messages.
2026-06-05 14:06:45 +05:30
f9385a31fc fix(api): allow agent bots to read conversations and manage labels (#14655)
Agent bots assigned to a conversation can now fetch that conversation's
details and manage its labels using their bot token. Previously these
two operations returned `Access to this endpoint is not authorized for
bots`, even though the same token worked for sending messages, updating
the conversation, and setting custom attributes.

Fixes
https://linear.app/chatwoot/issue/PLA-174/agent-bot-tokens-cannot-fetch-conversation-details-or-manage-labels

Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 11:01:25 +04:00
9d808a18df fix(drops): Resolve first_name for single-word contact and agent names (#13488)
WhatsApp template variables like `{{contact.first_name}}` previously
resolved to an empty string whenever a contact or agent had a
single-word name (e.g. "Petterson"). Because the variable came back
blank, WhatsApp template sends would either fail validation or get stuck
indefinitely in the "Sending" state. With this fix, `first_name` falls
back to the full single-word name, so templates render correctly for
everyone regardless of how many words their name has.

## Closes

- Fixes #13222

## How to reproduce

1. Create a contact with a single-word name (e.g. `Petterson`).
2. Configure a WhatsApp template action that uses
`{{contact.first_name}}`.
3. Send the template message.
4. **Before:** the variable resolves to an empty string and the message
fails / stays in "Sending".
**After:** the variable resolves to `Petterson` and the message sends.

## What changed

- Removed the "name must have 2+ words" guard from `first_name` in
`ContactDrop` and `UserDrop`, so a single-word name is returned (still
capitalized).
- Capitalization behavior introduced in #6758 is preserved; only the
single-word edge case changes.
- `last_name` still returns `nil` for single-word names, which is the
expected behavior.
- Added specs covering single-word names for both `ContactDrop` and
`UserDrop`.

---------

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>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-05 10:03:24 +04:00
Sivin VargheseandGitHub ec43975f3f chore: use square avatars (#14656) 2026-06-05 11:25:32 +05:30
64c5aeebee feat(help-center): support per-locale portal title, name & header (#14642)
Portals can now override their name, page title and header text per
locale, with a fallback chain of locale override → default locale → base
value. Overrides are stored under portal.config.locale_translations and
validated with a JSON schema.

Editing is exposed as a "Localize content" action in each non-default
locale's menu on the Locale page, and all public-facing portal views
(classic + documentation layouts) render the localized values. The live
chat widget is also loaded in the active portal locale.


<img width="494" height="395" alt="Screenshot 2026-06-03 at 2 48 59 PM"
src="https://github.com/user-attachments/assets/e55a06e8-f20f-4d8a-9352-92fde28a9a19"
/>



https://github.com/user-attachments/assets/f5affbd2-7ad4-415a-b376-57c759fc2aaa

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 06:34:12 -07:00
cd9192f7d1 chore(captain): update Firecrawl to use the v2 API (#14624)
## Description

Migrates Firecrawl from the v1 to the v2 API.
`Captain::Tools::FirecrawlService` now targets `api.firecrawl.dev/v2`,
with the request body updated to match the v2 schema.

> Disclosure: I work at Firecrawl.

Fixes # (n/a)

## Type of change

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

## How Has This Been Tested?

Updated
`spec/enterprise/services/captain/tools/firecrawl_service_spec.rb` to
assert the v2 endpoint and request body.

## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [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

---------

Co-authored-by: Aakash Bakhle <48802744+aakashb95@users.noreply.github.com>
2026-06-03 23:47:49 +05:30
Aakash BakhleandGitHub eaffad12e7 feat(langfuse): propagate observation metadata for evals (#14634)
# Pull Request Template

## Description

We need to pass on trace level attributes down to the spans inside them
like tool calls, observations, etc.
This way, we can filter observations based on trace level attributes.


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

Attributes added to observation metadata for easy filtering
<img width="1327" height="708" alt="image"
src="https://github.com/user-attachments/assets/8f1d1bf8-cde4-481d-a2c2-7920ad2fc52e"
/>

added a `generation_stage` to differentiate llm_calls that call tools vs
those that generate a `final_response`
<img width="1806" height="968" alt="CleanShot 2026-06-03 at 15 11 09@2x"
src="https://github.com/user-attachments/assets/db1fa8e0-7f2d-404b-a719-27a16d400442"
/>


propagated attributes to tool calls for future use
<img width="903" height="517" alt="image"
src="https://github.com/user-attachments/assets/edc61ce8-93db-465c-a66e-043138e2dc15"
/>



## 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-06-03 16:45:19 +05:30
Sivin VargheseandGitHub 18ef019cd4 fix: improve article editor typography & nested lists (#14572) 2026-06-03 16:11:32 +05:30
Tanmay Deep SharmaandGitHub ea910227ac feat: add Voice Calls feature card to settings showcase (#14635)
## Linear Ticket 

https://linear.app/chatwoot/issue/CW-7250/add-voice-calls-to-self-hosted-super-admin-feature-list

## Description

Adds a **Voice Calls** card to the Super Admin → Settings → Features
showcase so self-hostedadmins can see at a glance whether voice calling
is available on the installation.

## Type of change

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

## Screenshots?

<img width="1512" height="843" alt="Screenshot 2026-06-03 at 3 15 22 PM"
src="https://github.com/user-attachments/assets/988db14b-fef1-4b72-a55e-c7a8884521dd"
/>


## 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-06-03 16:01:14 +05:30
Tanmay Deep SharmaandGitHub 94ddd98050 chore: update the voice call ringtone (#14636)
## Linear Ticket
https://linear.app/chatwoot/issue/CW-7260/update-the-voice-call-ringtone

## Description

Updates the incoming voice call ringtone. The dashboard call widget now
plays the new tone while an inbound call is unanswered, replacing the
previous bell sound.

## Type of change

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

## 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
2026-06-03 16:00:04 +05:30
Tanmay Deep SharmaandGitHub 0e87519ecd feat: add mute button for Twilio calls (#14637)
## Linear Ticket

https://linear.app/chatwoot/issue/CW-7264/add-mute-button-in-twilio-calls

## Description

Adds mute support for Twilio voice calls. Previously the mute button was
shown only for WhatsApp calls (which toggle the local mic track in the
browser), so Twilio calls had no way to mute. The call widget now routes
mute by provider: WhatsApp continues to toggle the local mic track,
while Twilio uses the Voice SDK connection's native `mute()`. The mute
button is now shown for any active call, and unmute works symmetrically.

## Type of change

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

## 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
- [ ] 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-06-03 15:59:23 +05:30
Tanmay Deep SharmaandGitHub de89391031 fix: use hang-up handset icon for reject/end call button (#14640)
## Linear Ticket
https://linear.app/chatwoot/issue/CW-7261/call-window-theme-alignment

## Description

Updates the call window's reject/end button to use the hang-up handset
icon (the same handset as the accept button, rotated) instead of the
phone-with-X, matching the design.

## Type of change

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

## Screenshots

<img width="346" height="135" alt="Screenshot 2026-06-03 at 3 41 10 PM"
src="https://github.com/user-attachments/assets/4d507141-332e-436a-a6ec-516964a31013"
/>


## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] My changes generate no new warnings
2026-06-03 15:59:12 +05:30
Sojan JoseandGitHub 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`
2026-06-03 15:56:54 +05:30
1beaa284c6 feat: inline images in website and email channels (#14516)
# Pull Request Template

## Description

This PR adds support for inline image uploads in the reply editor for
Email and Website (chat widget) channels.

Agents can now insert images inline between text and resize them
directly in the editor by dragging the bottom corner, similar to the
help center editor experience.

Image sizes are preserved through markdown using the `cw_image_width`
URL param and render correctly in both outgoing emails and chat widget
messages.

Agents can also paste copied images directly into Email or Website
replies using **Shift+Cmd+V** (Shift+Ctrl+V on Windows/Linux). The image
gets inserted inline at the cursor position and supports resizing just
like uploaded images. Regular **Cmd+V / Ctrl+V** behavior remains
unchanged and continues to add images as attachments, so both inline and
attachment flows are supported.


### Prosemirror repo PR:
https://github.com/chatwoot/prosemirror-schema/pull/48

Fixes
https://linear.app/chatwoot/issue/CW-7133/inline-images-in-live-chat-and-email

https://linear.app/chatwoot/issue/CW-7225/ghsa-8j9w-jppp-xcfc-html-attribute-injection-via-unvalidated-cw-image

## Type of change

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

## How Has This Been Tested?

### Screencast



https://github.com/user-attachments/assets/a928f852-ab15-413a-9d35-6ea69b718ecf

<img width="414" height="654" alt="image"
src="https://github.com/user-attachments/assets/205e0729-8f2d-4cc5-9c55-7696f032eca4"
/>



## 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
- [ ] 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-06-03 15:05:17 +05:30
Sivin VargheseandGitHub d028cc1984 fix: prevent list marker overflow in messages (#14618) 2026-06-03 14:18:48 +05:30
Sivin VargheseandGitHub 0d59fb4459 fix: validate portal color format (#14632) 2026-06-03 14:18:19 +05:30
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>
2026-06-03 12:50:21 +05:30
Vinícius FitznerandGitHub b791d75b30 fix(microsoft): prevent OAuth admin consent loop (#13962)
Fixes #9775

## Description

This fixes a repeated admin consent loop in the Microsoft OAuth flow
when connecting a Microsoft email inbox.

Chatwoot was always sending `prompt=consent` in the Microsoft
authorization URL. In the current code path, this parameter is only used
when building the authorization URL and is not required by the callback,
token exchange, token persistence, or refresh flow.

By removing the forced consent prompt, the OAuth flow can proceed
normally without repeatedly sending users back through the admin consent
screen.

## What changed

- removed `prompt: 'consent'` from the Microsoft authorization URL
- added a regression assertion to ensure `prompt` is not included in the
generated URL

## Why this is safe

- `redirect_uri`, `scope`, and `state` remain unchanged
- callback and token exchange flow remain unchanged
- refresh token flow remains unchanged
- no other part of the current Microsoft inbox flow depends on forcing a
consent screen

## Testing

- updated controller spec to assert that the generated authorization URL
does not include `prompt`
2026-06-03 12:05:25 +05:30
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>
2026-06-02 23:28:13 +05:30
0a181b0cea chore: Update translations (#14498)
Updates dashboard, widget, and backend locale files with the latest
translation sync from Crowdin.

## Closes

N/A

## What changed

- Refreshes translated dashboard JSON locale files across supported
languages.
- Adds the latest backend Help Center/public portal locale strings.
- Keeps the branch current with `develop` and resolves the `ar`, `fr`,
and `pt_BR` locale conflicts.

## Validation

- Parsed all changed JSON and YAML locale files successfully.
- Checked for leftover merge conflict markers.
- Ran `git diff --check`.

---------

Co-authored-by: Sojan Jose <sojan@pepalo.com>
2026-06-02 21:21:50 +05:30
Sony MathewandGitHub 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.
2026-06-02 21:11:48 +05:30
28f87d2fca fix(widget): translate zh_CN availability keys (#14288)
Translate missing Simplified Chinese live-chat widget availability and
reply-time strings so visitors using `zh_CN` no longer see English
fallbacks in offline and pre-chat states.

## Closes

N/A

## How to test

- Open the widget with `Widget Locale = 中文 (zh_CN)` and set the team
offline. The availability card should stay in Chinese.
- Configure working hours so the widget shows minute, hour, tomorrow,
and day-specific return states. The `{time}`, `{n}`, and `{day}`
placeholders should render correctly.

## What changed

- Translated missing `THUMBNAIL.AUTHOR.NOT_AVAILABLE`,
`TEAM_AVAILABILITY.BACK_AS_SOON_AS_POSSIBLE`, and `REPLY_TIME.*` strings
in `app/javascript/widget/i18n/locale/zh_CN.json`.
- Kept the scope limited to Simplified Chinese.

Co-authored-by: Sojan Jose <sojan@pepalo.com>
2026-06-02 21:10:44 +05:30
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>
2026-06-02 15:33:32 +04:00
170b64d1f1 chore: upgrade to vite 6 (#14363)
Upgrades the frontend toolchain to Vite 6 and tidies up the build config
along the way. Behavior is unchanged for end users; this is dev/build
infra.

## What changed
- `vite` 5.4 → 6.4, `@vitejs/plugin-vue` → 5.2, `vite-plugin-ruby` → 5.2
(with matching `vite_rails`/`vite_ruby` gem bumps).
- Dropped the `vite-node` 2.0.1 pnpm override — no longer needed now
that vitest 3 runs on Vite 6 directly.
- Split the single `vite.config.ts` into:
- `vite.config.ts` (app), `vite.lib.config.ts` (SDK), `vite.shared.ts`
(aliases / Vue options), `vitest.config.ts` (tests).
- `pnpm build:sdk` now selects the SDK config explicitly instead of
branching on `BUILD_MODE=library`. SDK output path is unchanged
(`public/packs/js/sdk.js`).

No changes needed to Docker images, deployment scripts, or CI — Node 24
and pnpm 10 are already past Vite 6's floor, and the rake
`assets:precompile` hook still drives the SDK build via `pnpm`.

## How to test
- `pnpm dev` and verify the dashboard, widget, and survey routes load
and HMR works.
- Load a Chatwoot site widget on a test page and confirm `sdk.js` is
served and the widget mounts.
- `RAILS_ENV=production bundle exec rake assets:precompile` and confirm
`public/packs/js/sdk.js` plus the rest of the manifest are produced.
- `pnpm test` for the JS suite.

---------

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: Sony Mathew <2040199+sony-mathew@users.noreply.github.com>
2026-06-02 17:01:37 +05:30
95cb3a7ad8 feat(onboarding): honor return_to hint in Instagram OAuth callback (#14568)
When connecting an Instagram inbox during onboarding, the OAuth flow
used to drop users in inbox settings, breaking onboarding. The OAuth
start endpoint now accepts an optional `return_to=onboarding` hint,
carried tamper-proof inside the signed state (a claim on the Instagram
JWT), and the callback uses it to return the user to the onboarding
inbox-setup screen. Without the hint, behavior is unchanged.

This is the backend half only; the frontend that sends
`return_to=onboarding` ships separately.

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2026-06-02 15:28:08 +05:30
ecd9c26c8c feat: Implemented search results page functionality (#11086)
# Pull Request Template

## Description
Implemented search results page functionality. Now you can press "Enter"
to search by term and display results in a results page. Also now you
can link to /hc/{account}/en/search?query=XXXXXX to view search results
for XXXXXX query.

fixes: https://github.com/chatwoot/chatwoot/issues/10945

## Screenshots

Classic layout search results:
<img width="3840" height="2160" alt="classic-results"
src="https://github.com/user-attachments/assets/3bbb3272-33ca-4eb4-b80a-76ed77442088"
/>

Classic layout pagination:
<img width="3840" height="2160" alt="classic-page-two"
src="https://github.com/user-attachments/assets/062b09d3-7c58-4d3b-8611-b94375e7db51"
/>

Classic layout empty search:
<img width="3840" height="2160" alt="no-results"
src="https://github.com/user-attachments/assets/c5e3f47a-cd9a-4e14-ae92-ccba00c89e98"
/>

Documentation layout search results:
<img width="3840" height="2160" alt="documentation-results"
src="https://github.com/user-attachments/assets/9e45d8d9-c975-4589-b6c6-3bc7bb3c588e"
/>

Documentation layout dark theme:
<img width="3840" height="2160" alt="documentation-dark"
src="https://github.com/user-attachments/assets/cdb6ed63-4241-4b32-9f79-7d92ed479fc8"
/>

Plain embedded dark layout:
<img width="3840" height="2160" alt="plain-embedded-dark"
src="https://github.com/user-attachments/assets/7deb02b9-9f24-48fb-8979-a2ecd7002c05"
/>

---------

Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
Co-authored-by: Sojan Jose <sojan@pepalo.com>
Co-authored-by: Pranav <pranav@chatwoot.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Vinay Keerthi <11478411+stonecharioteer@users.noreply.github.com>
2026-06-02 15:19:23 +05:30
Sony MathewandGitHub 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
2026-06-02 14:35:37 +05:30
Sojan JoseandGitHub 33dea83716 docs: document message attachment uploads (#14600)
Updates the Create New Message API documentation to explain how to send
file attachments with multipart form data.

Closes #10472
Closes #12672

## Why

The endpoint already accepts attachment uploads, but the published API
docs only described the JSON request body. That made it unclear that
clients need to use multipart form data and send files through the
`attachments[]` field.

## What changed

- Adds `multipart/form-data` as a documented request body option for
Create New Message.
- Documents the `attachments` binary array and form encoding used by
`attachments[]`.
- Adds a multipart cURL request example for a message with an
attachment.
- Regenerates the Swagger JSON artifacts.

## Screenshots

Create New Message API docs with the multipart cURL sample and
`multipart/form-data` request sample selected:

<img width="1702" height="1083"
alt="create-message-attachment-docs-multipart"
src="https://github.com/user-attachments/assets/5fef9082-6eb9-494d-91d1-8dc0b7880212"
/>

## How to test

1. Open `/swagger`.
2. Navigate to Application -> Messages -> Create New Message.
3. Confirm the endpoint documents both `application/json` and
`multipart/form-data`, including the attachment payload schema.
2026-06-02 14:33:02 +05:30
4c6a345d60 feat(onboarding): honor return hint in email OAuth callback (#14567)
When connecting a Gmail or Outlook inbox during onboarding, the OAuth
flow used to drop users in inbox settings, breaking onboarding. The
OAuth start endpoint now accepts an optional `return_to=onboarding`
hint, carried tamper-proof inside the signed `state`, and the callback
uses it to return the user to the onboarding inbox-setup screen. Without
the hint, behavior is unchanged.

This is the backend half only; the frontend that sends
`return_to=onboarding` ships separately.

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2026-06-02 14:21:11 +05:30
Shivam MishraandGitHub 04ac9d3780 fix: use UPN for imap_login on Microsoft OAuth callback (#14522)
Outgoing email on a Microsoft email inbox was failing with `535 5.7.3
Authentication unsuccessful` immediately after the user
re-authenticated, while incoming (IMAP) continued to work. The root
cause is in our OAuth callback: we persist the id_token's `email` claim
into `channel.imap_login`, but Microsoft's SMTP AUTH (XOAUTH2) validates
the username against the access token's **UPN**, not the mailbox's
primary SMTP address or aliases. When those diverge — common in tenants
that use one domain for sign-in identities and another for mailbox
addresses — SMTP rejects every send.

This PR makes `OauthCallbackController#update_channel` prefer
`preferred_username` (v2.0) / `upn` (v1.0) from the id_token over
`email`, with `email` as the fallback so Google flows are unchanged.

## What changed

- `app/controllers/oauth_callback_controller.rb` — extract
`imap_login_identity` (default: `users_data['email']`, same as before).
`update_channel` now calls it instead of inlining the email claim. No
behavioural change in the base controller.
- `app/controllers/microsoft/callbacks_controller.rb` — override
`imap_login_identity` to return `preferred_username || upn || super`.
Provider-specific knowledge stays in the provider subclass; Google's
flow is literally untouched.
- `spec/controllers/microsoft/callbacks_controller_spec.rb` — adds one
example that reproduces the divergent shape (`email: <alias>`,
`preferred_username: <upn>`) and asserts `imap_login` lands on the UPN
while `channel.email` stays on the mailbox alias.

`channel.email` and `find_channel_by_email` still key on the id_token's
`email` claim everywhere, so customer-facing From identity and
reconnect-by-email matching are unchanged.

Behavioural matrix:

| Provider / shape | `imap_login` before | `imap_login` after |

|-----------------------------------------------------------------|---------------------|--------------------|
| Microsoft, `upn == email` (common case) | email | UPN (= email, same
string) |
| Microsoft, `upn != email` (e.g. UPN on one domain, mailbox alias on
another) | alias (broken) | UPN (works) |
| Google | email | email (no change, base impl used) |

## Why this happens (Microsoft side, for context)

Two facts that together produce the bug — one is documented, the other
we verified empirically because Microsoft's docs don't address it:

1. **`email` and `upn` are different claims and can legitimately
diverge.** In Entra, the UPN is the sign-in identity; the id_token's
`email` claim is the user's mailbox property (which can be a proxy
address). For v2.0 tokens (which is what we use —
`/common/oauth2/v2.0/token` in `MicrosoftConcern`), the documented
"username to sign in as" claim is `preferred_username`. See [ID token
claims
reference](https://learn.microsoft.com/en-us/entra/identity-platform/id-token-claims-reference).
So `users_data['email']` was the wrong source for `imap_login`; tenants
where UPN == primary SMTP happened to mask the bug.

2. **Exchange's XOAUTH2 SMTP rejects aliases in the `user=` field, even
though IMAP accepts them.** This asymmetry is **not** in [Microsoft's
canonical XOAUTH2
doc](https://learn.microsoft.com/en-us/exchange/client-developer/legacy-protocols/how-to-authenticate-an-imap-pop-smtp-application-by-using-oauth)
— the doc treats the SASL `user=` field uniformly across IMAP, POP, and
SMTP, with only a shared-mailbox carve-out spelled out. We confirmed the
SMTP-strict behaviour empirically by running an XOAUTH2 AUTH probe with
the same access token: `user=<alias>` returned `535 5.7.3`, `user=<UPN>`
returned `235`. Same token, only the `user=` field differed. That's what
tied the symptom to claim-shape.

Hence the patch: read the documented "sign-in name" claim and persist
that, not the customer-facing mailbox address.

## How to reproduce (before the fix)

1. Set up a Microsoft email inbox in a tenant where the user's UPN
domain differs from the user's primary SMTP / mailbox domain (e.g. UPN
`user@tenant-a.example`, mailbox `User@tenant-b.example` where
`tenant-b.example` is a proxy address on the same mailbox).
2. Connect or re-authenticate the inbox through the standard flow.
3. Inspect the channel:
   ```ruby
ch.imap_login # => "User@tenant-b.example" (alias — wrong)
   token = ch.provider_config['access_token']
JSON.parse(Base64.urlsafe_decode64(token.split('.')[1].then { |s| s +
'=' * (-s.length % 4) }))['upn']
   # => "user@tenant-a.example"  (UPN — what SMTP actually needs)
   ```
4. Send a reply. SMTP returns `535 5.7.3 Authentication unsuccessful`.
Incoming IMAP continues to work fine.

After the fix, `imap_login` is set to the UPN at callback time and SMTP
succeeds.

## Notes for review

- The id_token decoding path (`users_data`) already exists and is
trusted by the rest of the callback — no new attack surface.
- Scoped to Microsoft on purpose. A provider-agnostic fallback chain in
the base controller would also have worked (Google id_tokens don't carry
`preferred_username` / `upn`, so it'd land on email anyway), but keeping
Google's code path identical to today removes any risk of an unintended
interaction with whatever a future Google update might add to its
id_token. Pattern matches the existing `find_channel_by_email` override
Google already has.
- The [id_token claims
reference](https://learn.microsoft.com/en-us/entra/identity-platform/id-token-claims-reference)
warns that `preferred_username` is mutable and "can't be used to make
authorization decisions." That warning targets apps using the claim as a
stable cross-session identifier for app-level authz — we're not. We use
it as the SASL `user=` string for SMTP AUTH, validated by Microsoft
against the same token it just issued. The claim's mutability matters
only if a tenant admin renames a UPN between re-auths, in which case the
stored `imap_login` goes stale and SMTP 535s — but the pre-patch code
has the identical mutability characteristic on the `email` claim (rename
a mailbox's primary SMTP and you get the same stale-then-535). The patch
doesn't enlarge that failure surface; it shrinks it, because
UPN-vs-alias divergence is now handled rather than always-broken.
- Related but out of scope: SMTP failures don't trigger
`channel.authorization_error!` today. `ExceptionList::SMTP_EXCEPTIONS`
(`lib/exception_list.rb`) only contains `Net::SMTPSyntaxError`;
`Net::SMTPAuthenticationError` bubbles unhandled, and
`ApplicationMailer#handle_smtp_exceptions` only logs. So a 535 — whether
from this bug or any other identity drift — never prompts re-auth in the
UI, unlike the IMAP path (`fetch_imap_emails_job` catches
`OAuth2::Error` and calls `authorization_error!`). Worth a separate PR;
flagging here so we don't pretend stale-identity SMTP errors are
self-healing.
- The alternative I considered — decoding the access token's `upn`
directly — is more correct in principle but relies on Microsoft access
tokens being JWTs, which is undocumented behaviour for the
`outlook.office.com` audience and contradicts the [docs
guidance](https://learn.microsoft.com/en-us/entra/identity-platform/access-tokens)
that access tokens are opaque to clients.
- **Not addressed here, flagging for follow-up:** existing channels that
were re-authenticated before this fix still hold the alias in
`imap_login` and will keep 535ing until the user re-auths again. A
one-shot rake task could decode the stored access tokens and reconcile,
but fixing forward via natural re-auth is lower-risk; let me know if you
want the backfill.
- Also out of scope: `app/mailers/conversation_reply_mailer_helper.rb`
reads `provider_config['access_token']` raw without going through
`Microsoft::RefreshOauthTokenService`, while the IMAP path refreshes.
That's a real asymmetry but unrelated to this bug (the token here was
minutes-old) and worth its own PR.
2026-06-02 13:26:30 +05:30
1f6203d558 feat(onboarding): honor return_to hint in TikTok OAuth callback (#14569)
When connecting a TikTok inbox during onboarding, the OAuth flow used to
drop users in inbox settings, breaking onboarding. The OAuth start
endpoint now accepts an optional `return_to=onboarding` hint, carried
tamper-proof inside the signed `state` (a claim on TikTok's signed JWT),
and the callback uses it to return the user to the onboarding
inbox-setup screen. Without the hint, behavior is unchanged.

This is the backend half only; the frontend that sends
`return_to=onboarding` ships separately.

## What changed
- `Tiktok::IntegrationHelper`: the signed JWT carries an optional
`return_to` claim, added only when present (a request without it is
byte-identical to before); added `tiktok_token_return_to` to read it;
`decode_token` now returns the full payload and `verify_tiktok_token`
derives the account id from it.
- `Tiktok::AuthorizationsController#create` passes `params[:return_to]`
into the token.
- `Tiktok::CallbacksController` redirects to the onboarding inbox-setup
screen when `return_to == 'onboarding'`, before the normal
settings/agents redirect.
- Added the `app_onboarding_inbox_setup` route (shared with the sibling
Gmail/Outlook and Instagram PRs — keep a single copy on merge to avoid a
duplicate route name).

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2026-06-02 13:24:46 +05:30
3eed8905cc fix(facebook): render shared links as fallback attachments (#14554)
Fixes Facebook fallback and shared-post attachments so they render as
clickable links in conversations.

Closes:
- https://github.com/chatwoot/chatwoot/issues/4767
- https://github.com/chatwoot/chatwoot/issues/5327

Why:
Facebook can send shared links as `fallback` attachments with a
top-level `url`, and shared posts as `share` attachments with the URL
under `payload.url`. The current flow either misses the nested URL or
treats `share` as downloadable media, so these messages do not render
correctly.

What changed:
- Store Facebook fallback URLs from either `attachment.url` or
`attachment.payload.url`.
- Treat Facebook `share` attachments as fallback link attachments
instead of downloading them as files.
- Render fallback attachments in the next message bubble UI as clickable
links.

How to test:
1. Connect a Facebook inbox.
2. Send a shared link to the page.
3. Send/share a Facebook post to the page.
4. Open the conversation in Chatwoot.
5. Confirm both messages appear as clickable link bubbles.

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2026-06-01 17:08:00 +05:30
Sojan JoseandGitHub 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.
2026-06-01 16:47:17 +05:30
ed3059c1aa docs: Document API inbox webhook URL (#14593)
Documents the existing API inbox webhook_url request field and expands
inbox create/update request docs with channel-specific schemas and
examples.

Closes #14591

## Why
API inboxes already accept channel.webhook_url through Channel::Api
editable attributes, but the OpenAPI request schemas did not document
the field. The previous mixed channel object also made it hard to see
which fields belong to each supported channel type.

## What changed
- Added named channel payload schemas for supported inbox create channel
types.
- Added channel-specific create request samples in this order: Website
inbox, API channel, Email channel, LINE channel, Telegram channel,
WhatsApp channel, SMS channel.
- Added common inbox fields such as greeting_enabled, greeting_message,
enable_auto_assignment, working_hours_enabled, and timezone to request
samples.
- Kept website-only flags such as allow_messages_after_resolved and
enable_email_collect only in website inbox samples.
- Annotated inbox request field descriptions with channel availability
where Swagger UI cannot dynamically filter fields.
- Added channel-specific update settings schemas and request samples for
editable channel attributes.
- Documented channel.webhook_url for API inbox create/update payloads.
- Rebuilt generated Swagger JSON and tag-group specs.

## Screenshots

**Website inbox request sample**

Shows the request sample selector set to Website inbox, including
website-only fields such as enable_email_collect and
allow_messages_after_resolved.

<img width="3840" height="2160" alt="Website inbox request sample"
src="https://github.com/user-attachments/assets/ad130f04-b336-4cc3-aba1-e934b646d447"
/>

**API channel request sample**

Shows the selector switched to API channel, with API-specific fields
such as webhook_url, hmac_mandatory, and additional_attributes.

<img width="3840" height="2160" alt="API channel request sample"
src="https://github.com/user-attachments/assets/09484816-1d47-490f-9ab4-195835ed5cd3"
/>

**Expanded channel schema**

Shows the request-body schema with channel expanded, including the type
selector and channel-specific fields under it.

<img width="3840" height="2160" alt="Expanded channel schema"
src="https://github.com/user-attachments/assets/f6c4878e-ac34-40fe-8be3-5913f27cbdd8"
/>

## Validation
- bundle exec rake swagger:build
- bundle exec rspec spec/swagger/openapi_spec.rb
- Rendered the local Swagger page and verified the request sample
dropdown order and API channel payload.

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2026-06-01 12:37:07 +04:00
Sojan JoseandGitHub 9d591b8f46 feat: enable companies for Business cloud plans (#14598)
Enable the Companies feature automatically for Chatwoot Cloud accounts
on the Business plan and higher.

## Closes
None.

## How to test
Upgrade or reconcile a Cloud account on Business or Enterprise and
verify Companies is available. Reconcile a Startups account and verify
Companies remains disabled.

## What changed
- Added companies to the Business plan feature set, which Enterprise
inherits through the existing hierarchy.
- Added billing specs that assert Companies is disabled for Startups and
enabled for Business and Enterprise.
2026-06-01 14:02:03 +05:30
Sojan JoseandGitHub 1afcd36dee fix(contacts): align contact export permissions (#14601)
Allows contact managers to export and import contacts from the Contacts
page while keeping plain agents blocked. The contacts action menu now
mirrors backend permissions for both export and import.

## Closes

- https://linear.app/chatwoot/issue/CW-4438/contact-export-is-broken

## What changed

- Allows Enterprise custom roles with `contact_manage` to pass
`ContactPolicy#export?` and `ContactPolicy#import?`.
- Shows Export and Import to admins and contact managers only.
- Adds Enterprise policy coverage for contact export and import.

## Screenshots

Admin: Export and Import are available.

<img width="3840" height="2160" alt="Admin contact actions with Export
and Import visible"
src="https://github.com/user-attachments/assets/2b2cdaf2-ca8f-470d-be34-31cba68b9dce"
/>

Contact manager: Export and Import are available.

<img width="3840" height="2160" alt="Contact manager contact actions
with Export and Import visible"
src="https://github.com/user-attachments/assets/48fc038b-2e78-4d0c-ba17-a5965641bd88"
/>

Regular agent: Export and Import are hidden.

<img width="3840" height="2160" alt="Regular agent contact actions with
Export and Import hidden"
src="https://github.com/user-attachments/assets/a63b5731-743a-4223-8dab-ce58383067fe"
/>

## How to test

- Sign in as an administrator and open Contacts; the action menu shows
Export and Import.
- Sign in as a custom-role user with `contact_manage`; the action menu
shows Export and Import.
- Sign in as a plain agent; Export and Import are not available and both
APIs remain unauthorized.
2026-06-01 13:58:57 +05:30
Shivam MishraandGitHub a3ffb48a47 refactor(onboarding): use separate onboarding controller (#14507)
Depends on: https://github.com/chatwoot/chatwoot/pull/14370

This PR creates a new onboarding controller, this allows more control
that the default account update API. Allowing us to spin tasks and
update details required specifically during the onboarding flow
2026-06-01 13:34:11 +05:30
Sony Mathew 4339e0ee0a Merge branch 'release/4.14.1' into develop 2026-05-29 17:28:40 +05:30
Sony Mathew e76650d6d0 Bump version to 4.14.1 2026-05-29 17:27:01 +05:30
Sivin VargheseandGitHub 3b7b94f8d1 fix: prevent code block overflow in message bubble (#14583) 2026-05-29 10:19:41 +05:30
Vishnu NarayananandGitHub 6c8741b314 fix: increase audit log page size (#14582)
Audit logs now return up to 25 records per page instead of 15. This
reduces page turns for admins and API consumers while keeping the page
size server-defined.

Fixes
https://linear.app/chatwoot/issue/CW-7172/allow-an-option-to-fetch-more-audit-logs
2026-05-28 12:35:44 +05:30
Tanmay Deep SharmaandGitHub 68e358d732 feat: voice-call UX fixes (#14579)
## Linear ticket
https://linear.app/chatwoot/issue/CW-7187/voice-calls-followup-tasks

## Description

Improvements to the WhatsApp voice-calling experience plus a cheaper,
more accurate audio-transcription model.
- First-time callers now get a real name. An inbound WhatsApp call
creates the contact from the caller's WhatsApp profile name instead of
the bare phone number.
- Clear, consistent call attribution. Call bubbles show a unified
"Handled by {agent}"
- Cleaner call widget. The dismiss (✕) button is shown only for incoming
calls
- WhatsApp calling for manual inboxes. voice_calling_supported? now
covers any whatsapp_cloud inbox
- Transcription: whisper-1 → gpt-4o-mini-transcribe.

## 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-05-27 16:16:16 +05:30
d20950c5b4 feat: scheduler fairness [AI-159] (#14425)
# Pull Request Template

## Description
Better scheduling and queueing mechanics for document auto-sync
- add jitter plan wise for document sync
- move auto-sync documents to purgeable queue

## Type of change

Please delete options that are not relevant.

- [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.
locally tested and with specs

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

---------

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Sony Mathew <sony@chatwoot.com>
Co-authored-by: Vishnu Narayanan <iamwishnu@gmail.com>
2026-05-27 16:01:51 +05:30
Shivam MishraandGitHub 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).
2026-05-27 15:59:19 +05:30
Silva Dev BRandGitHub 2628d7a7fd fix: prevent alt shortcuts while typing (#14526) 2026-05-27 15:11:49 +05:30
Shivam MishraandGitHub 94daf26ead chore: update jwt and faraday (#14577)
This PR updates two dependencies — `faraday` (2.14.1 → 2.14.2) and `jwt`
(2.10.1 → 2.10.3) — to pick up security patches flagged by
`bundle-audit`. Both are bumped to the minimal patched release within
their existing major lines to keep the blast radius small.

### Faraday

`Faraday::Connection#build_exclusive_url` still allowed a
protocol-relative host override when the request target was passed as a
`URI` object (rather than a `String`), bypassing the earlier fix for the
string-based variant (CVE-2026-25765 / GHSA-33mh-2634-fwr2). On a
fixed-base connection this could redirect a request to an
attacker-controlled host while still forwarding connection-scoped
headers such as `Authorization` — i.e. off-host request forgery
(CVE-2026-33637 / GHSA-5rv5-xj5j-3484).

The fix is a clean patch bump to `2.14.2`, within Faraday's existing
version range — no API changes and no other gems affected.

### JWT

`jwt` 2.10.1 accepts an empty/`nil` HMAC key during verification:
`JWT.decode(token, "", true, algorithm: 'HS256')` (and keyfinder paths
returning `""`/`nil`) verify a forged token, because the empty-key HMAC
digest is treated as valid and `enforce_hmac_key_length` defaults to
`false` (CVE-2026-45363, High).

The advisory offers two fixes — `~> 2.10.3` or `>= 3.2.0`. We chose
**2.10.3** deliberately: jumping to 3.x cascaded into upgrading
`oauth2`, `twilio-ruby`, `googleauth`, `web-push`, and `signet` (all
pinned `jwt < 3.0`), and `jwt` is used directly in 8+ places here (token
services, OAuth callbacks, integration helpers), so a major bump carries
real breakage risk for no extra security benefit. The Gemfile is pinned
`'~> 2.10', '>= 2.10.3'` to hold the 2.x line.

**Spec changes.** 2.10.3 tightens key handling: HMAC sign/verify now
raises on a `nil`, empty, or non-`String` key instead of silently
coercing it. A few specs relied on the old lax behaviour and needed
updating:

- `microsoft` / `google` callback specs built unsigned ID tokens via
`JWT.encode(payload, false)`. Replaced with the correct unsigned form,
`JWT.encode(payload, nil, 'none')`.
- `instagram` / `linear` / `shopify` helper specs have a "client secret
not configured" context where `client_secret` is `nil`. Their shared
`valid_token` `let` signed with that `nil` secret, which Ruby evaluates
before the helper runs — now raising. Since the helper short-circuits on
the blank secret and never decodes the token, those contexts now
override `valid_token` with a throwaway string.

**Production is unaffected.** Every production HMAC path uses a real,
non-empty key — `Rails.application.secret_key_base` (`BaseTokenService`,
`Widget::TokenService`) or a client secret guarded by `return if
client_secret.blank?` (Instagram/TikTok/Shopify/Linear helpers). The one
`nil`-key call, `JWT.decode(id_token, nil, false)` in
`OauthCallbackController`, runs with verification disabled, so the key
is never inspected. Twilio voice tokens use `Twilio::JWT::AccessToken`
from `twilio-ruby`, not this gem. The specs failed precisely because
they exercised the unsafe empty-key pattern the patch now blocks —
production never did.
2026-05-27 14:43:23 +05:30
Sivin VargheseandGitHub 7422b656cd fix: prevent status label overflow (#14580) 2026-05-27 14:17:57 +05:30
Vinícius FitznerandGitHub 39a93db007 fix(sidebar): prevent line wrap in availability selector (#13940) 2026-05-27 14:06:45 +05:30
Shivam MishraandGitHub b495aad1e6 feat(onboarding): allow full color channel icons (#14570) 2026-05-26 17:37:27 +05:30
Sivin VargheseandGitHub b33eecaff7 feat: bulk change category for articles (#14443) 2026-05-26 17:32:58 +05:30
Vishnu NarayananandGitHub 7c16071fc7 fix: Support allowlisted private API inbox webhooks (#14548)
Self-hosted installations can now opt SafeFetch into private-network
access after SSRF hardening. The default remains unchanged: private IP
destinations are blocked unless the instance owner explicitly enables
private-network requests with `SAFE_FETCH_ALLOW_PRIVATE_NETWORK=true`.

Fixes https://linear.app/chatwoot/issue/CW-7131
Fixes https://github.com/chatwoot/chatwoot/issues/14489
Fixes https://github.com/chatwoot/chatwoot/issues/14494

## How to use

For self-hosted installations that need API inbox webhooks, or other
SafeFetch-backed requests, to call trusted private services, enable
private-network access with a single environment variable:

```bash
SAFE_FETCH_ALLOW_PRIVATE_NETWORK=true
```

This is disabled by default. Enable it only when the instance owner
controls the deployment network and trusts the configured URLs.
2026-05-26 17:03:19 +05:30
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>
2026-05-26 15:23:51 +05:30
Aakash BakhleandGitHub 37c8e7e699 fix: firecrawl long external link (#14566)
# Pull Request Template

## Description

Fixes urls going past 255 chars, this is because of arabic urls, where
each character balloons to 8-9 characters and goes past the 255 limit

## Type of change

Please delete options that are not relevant.

- [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.
specs


## 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-05-26 14:07:07 +05:30
Muhsin KelothandGitHub 75c2f91019 chore: Enable Tiktok on paid plans Automatically (#13628)
This PR add the ability enable Tiktok integration on all paid plans.
2026-05-25 18:42:55 +05:30
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>
2026-05-25 18:13:59 +05:30
Vishnu NarayananandGitHub 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
2026-05-25 15:17:05 +05:30
Vishnu NarayananandGitHub 52da165cb7 feat: add timeout for imap email job and skip problematic emails (#11981)
# Pull Request Template

## Description

Large emails (2MB+ with multiple attachments) were causing IMAP email
processing jobs to timeout silently, blocking all subsequent emails from
being processed. This created an infinite loop where:
- Problematic emails were repeatedly fetched but never successfully
processed
- Other emails in the queue were never processed as we iterated
sequentially
  - silent failures


  ### Solution

Enhanced the FetchImapEmailsJob with individual email processing
isolation:

  ### Key Changes

1. Individual Email Processing: Changed from map to each for better
memory efficiency
2. Timeout Protection: Added configurable timeout per email (default: 60
seconds)
3. Failure Tracking: Track failed emails with 6-hour expiry for retry
opportunities
4. Skip Logic: Skip emails that have failed 3+ times to prevent infinite
loops
  5. Error Isolation: Each email is processed in its own error boundary

  ### Configuration

- Timeout: Configurable via EMAIL_PROCESSING_TIMEOUT_SECONDS using
GlobalConfigService
  - Default: 60 seconds per email
  - Failure Limit: 3 attempts before skipping
- Retry Window: 6 hours so that emails get 8 more chances in the 2 day
window

  ### Benefits

  - Prevents queue blocking: One problematic email cannot stop others
- Maintains email order: Older emails (customers waiting longer)
processed first
  - Automatic recovery: Failed emails get retry opportunities
  - Better monitoring: Clear logging when emails timeout or are skipped
- Configurable: Deployments can adjust the timeout based on their needs

This fix ensures email processing reliability while maintaining existing
functionality.

## Type of change

Please delete options that are not relevant.

- [x] 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:

- [ ] 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-05-25 15:16:52 +05:30
Shivam MishraandGitHub 03fb6591e0 chore: relax conversation meta polling for high-volume accounts (#14518)
On high-volume accounts, the dashboard sidebar's conversation count
badges fall behind because a meaningful share of
`/api/v1/accounts/:id/conversations/meta` requests get rate-limited
(per-user throttle, default 30 req/min).

Root cause is in `conversationStats.js`. The tiered debounce uses
`allCount` from the last response to pick a wait interval. `allCount`
reflects the user's *current filtered scope*, not the account's true
volume — so an agent viewing a small filter on a busy account falls into
the most aggressive tier (500ms wait / 1.5s maxWait → up to 40
calls/min/tab) and trips the throttle.

## What changed

`app/javascript/dashboard/store/modules/conversationStats.js`:

- Short-tier `maxWait`: `1500 → 2000` (caps short-tier at 30/min/tab
instead of 40)
- Super-long-tier threshold: `allCount > 5000 → > 2000` (more
high-volume accounts fall into the safe 3/min/tab tier)
- Middle-tier threshold unchanged (`> 100`)

| Tier (allCount) | wait / maxWait | Calls/min/tab |
|---|---|---|
| `> 2000` | 10s / 20s | 3 |
| `> 100` | 5s / 10s | 6 |
| else | 500ms / 2s | 30 |

## Trade-off

Badge updates (including those triggered by the agent's own action) may
lag by up to the tier's `maxWait` — worst case 20s for accounts with >
2000 open conversations in the active scope. The conversation list
itself and push notifications continue to update in real time; only the
numeric badge is debounced.

## Not in scope

- Sticky-max `allCount` to fix the underlying tier-selection signal —
defer until the simpler tuning is validated in production
- Optimistic count updates on local user actions — adds non-trivial
state management for a cosmetic lag
2026-05-25 14:21:14 +05:30
c4a6a19e9b feat(voice): WhatsApp Cloud Calling — UI [6] (#14346)
## 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>
2026-05-22 18:42:39 +05:30
b9757447a8 fix(openapi): document webhook secret in API schema (#14199)
Fixes #13862

Updates the webhook OpenAPI schema to match the current API behavior for
webhook secrets and supported subscription events.

## Why

Current source already creates per-webhook secrets, returns `secret`
from the account webhook API, and uses that secret to sign outbound
webhook requests with `X-Chatwoot-Signature`.

The OpenAPI schema was behind that contract:
- `components.schemas.webhook` did not include the returned `secret`
field.
- Webhook subscription enums did not include the typing events that are
already available in the dashboard webhook form and handled by
`WebhookListener`.

## What this change does

- Documents `secret` on the webhook response schema.
- Documents the outbound webhook signing headers associated with
`secret`: `X-Chatwoot-Timestamp`, `X-Chatwoot-Signature`, and
`X-Chatwoot-Delivery`.
- Adds `conversation_typing_on` and `conversation_typing_off` to webhook
subscription enums.
- Regenerates the main and tag-group swagger JSON files.

## Validation

- Ran `bundle exec rails swagger:build`.
- Ran `bundle exec rspec spec/swagger/openapi_spec.rb`.
- Verified generated swagger JSON includes `secret`,
`conversation_typing_on`, and `conversation_typing_off` in the webhook
schemas.

---------

Co-authored-by: Syed Muhammad Bilal <sdmhbilal@users.noreply.github.com>
Co-authored-by: Sojan Jose <sojan@pepalo.com>
2026-05-22 16:10:17 +05:30
Sony MathewandGitHub e5d66020fd chore: made the design for unread-counts more subtle [DESN-43] (#14542)
# Pull Request Template

## Description

Made the design for unread-counts more subtle
Fixes # DESN-43

## 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 manually. Here are the screenshots.
Light theme:

<img width="273" height="605" alt="Screenshot 2026-05-22 at 1 28 31 PM"
src="https://github.com/user-attachments/assets/cbeccf11-41c4-4899-bbb5-f870df530260"
/>


Dark theme:
<img width="280" height="606" alt="Screenshot 2026-05-22 at 1 27 59 PM"
src="https://github.com/user-attachments/assets/3740f57d-3392-435d-9d84-75caf42df610"
/>


## 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
2026-05-22 14:01:17 +05:30
6b1d8203c6 fix: remove unused working hours endpoint (#13839)
Fixes #13752

Removes the standalone `working_hours` API endpoint instead of fixing
only the callback typo in `WorkingHoursController`.

## Why

The route is not used by the dashboard. The supported product flow
already updates business hours through `PATCH
/api/v1/accounts/:account_id/inboxes/:id`.

The standalone endpoint was already unusable in practice:
- The controller callback pointed to the wrong method.
- Fixing that callback alone would still leave the endpoint blocked by
missing `WorkingHourPolicy` authorization.
- Keeping the route would preserve unsupported API surface without
making the product flow better.

## What this change does

- Removes `PATCH/PUT /api/v1/accounts/:account_id/working_hours/:id`.
- Deletes `Api::V1::Accounts::WorkingHoursController`.
- Leaves the inbox working-hours update path unchanged.

Compatibility note: this removes an undocumented endpoint that was
already unusable in practice. Working-hours updates should continue to
go through the supported inbox update API.

## Validation

- Ran `bin/rails routes -g working_hours` and confirmed the standalone
working-hours API route is no longer present.
- Searched for remaining `WorkingHoursController` and `resources
:working_hours` references.

---------

Co-authored-by: easonysliu <easonysliu@tencent.com>
Co-authored-by: Sojan Jose <sojan@pepalo.com>
2026-05-22 13:41:09 +05:30
Sony MathewandGitHub 4550c4130b fix: order labels, teams, channels in sidebar by unread count (CW-7151) (#14510)
# Pull Request Template

## Description

Ordered the conversation sidebar labels, teams and channels by the
unread count.

Fixes # CW-7151

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

Verified manually. Adding the screenshot below.
<img width="625" height="833" alt="Screenshot 2026-05-20 at 10 24 30 PM"
src="https://github.com/user-attachments/assets/ad04464d-0fc3-4ac7-b8cc-786e9647a299"
/>



## 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
2026-05-22 13:11:29 +05:30
e09496078b fix(widget): improve dark mode select options (#14538)
Fixes the web widget select dropdown styling in dark mode so native
select options remain readable against the widget's dark UI.

Closes
None

## Screenshots

Previous state:

<img width="426" height="764" alt="Previous dark mode dropdown issue"
src="https://github.com/user-attachments/assets/812fa88c-ae5a-4769-be14-748fbbaf7dfe"
/>

Current state:

<img width="1210" height="610" alt="Current dark mode dropdown styling"
src="https://github.com/user-attachments/assets/0ec9b6d7-025d-4b97-b43e-ef026857f9c4"
/>

## How to test

1. Enable the web widget pre-chat form with a list/select field.
2. Load the widget with dark mode enabled.
3. Open the select field and verify the select control and option text
remain readable in dark mode.

## What changed

- Adds light/dark color-scheme handling for widget native selects.
- Applies explicit option background and text colors so dark-mode select
options do not inherit unreadable colors from the browser popup.

---------

Co-authored-by: iamsivin <iamsivin@gmail.com>
2026-05-22 12:59:48 +05:30
Khoa NguyenandGitHub 4cdfe4168c chore: resolve sass and vue compiler deprecation warnings (#13794) 2026-05-22 12:16:43 +05:30
Sivin VargheseandGitHub 0722750a55 chore: Captain reply actions not showing correctly with content (#14160) 2026-05-22 12:16:19 +05:30
Aakash BakhleandGitHub bef25781de feat(attachments): add XML and PFX file support (#14539)
Update frontend allowed file types and FileIcon mapping, and backend
Attachment constants to accept .xml and .pfx files

# Pull Request Template

## Description
Customer also wanted XML support along with .pfx

Following up on #14456

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

<img width="864" height="512" alt="CleanShot 2026-05-22 at 11 43 20@2x"
src="https://github.com/user-attachments/assets/4cbf65d4-b919-4a4b-bf75-a4f2e8690586"
/>

<img width="870" height="1440" alt="CleanShot 2026-05-22 at 11 44 03@2x"
src="https://github.com/user-attachments/assets/e763b49d-4365-4c45-9b43-b0c39af87656"
/>


## 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-05-22 11:55:16 +05:30
Sojan JoseandGitHub 1d7a9093d2 fix: clarify agent availability swagger fields (#14533)
Clarifies the agent availability API documentation so request payloads
use the writable `availability` field, while `availability_status`
remains documented as a read-only response field.

## Closes

Closes #13873

## Why

The backend already supports updating an agent's configured availability
through `availability`, but the Swagger request payloads documented
`availability_status`. That made clients follow a read-only response
field and see successful requests without the intended availability
change.

## What changed

- Replaces `availability_status` with `availability` in agent
create/update request schemas.
- Updates the availability enum to `online`, `busy`, and `offline`.
- Marks response `availability_status` as read-only and explains that it
is derived from configured availability, auto-offline, and presence.
- Regenerates the combined and tag-group Swagger JSON files.

## Validation

- `bundle exec rails swagger:build`
- `bundle exec rspec spec/swagger/openapi_spec.rb`
- `git diff --check`
2026-05-22 11:33:19 +05:30
3c67c41544 chore: support PFX filetype in attachment uploads (#14456)
# Pull Request Template

## Description

This PR expands the default upload rules to support PFX certificate
files (`application/x-pkcs12`, `application/pkcs12`, `.pfx`) across
private notes, Website, Email, and Telegram channels.

Also adds `.xls` / `.xlsx` extension fallbacks for cases where browsers
upload Excel files with an empty or generic MIME type.

### Utils Repo PR: https://github.com/chatwoot/utils/pull/61

Fixes
https://linear.app/chatwoot/issue/CW-7085/support-more-file-types-in-private-notes-and-in-app

## Type of change

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

## How Has This Been Tested?

### Screenshots
<img width="330" height="218" alt="image"
src="https://github.com/user-attachments/assets/80823250-893e-4509-adb9-61f845359151"
/>



## 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: aakashb95 <aakashbakhle@gmail.com>
2026-05-22 11:09:27 +05:30
d0ecdc14d8 feat(webhooks): Emit inbox_updated when an inbox is disconnected (#14504)
Chatwoot now lets external apps know when an inbox loses its connection
and needs re-authentication. When a channel's authorization expires (for
example, an email inbox disconnects), Chatwoot fires an `inbox_updated`
webhook reflecting the new `reauthorization_required` status, and fires
it again once the inbox is re-authenticated. Integrators can keep their
own view of which inboxes are healthy without polling the API.

This is gated behind the `ENABLE_INBOX_EVENTS` installation flag — the
**Inbox updated** webhook subscription only appears in the dashboard
when that flag is enabled, so no event is offered that the backend
wouldn't dispatch.

Fixes
https://linear.app/chatwoot/issue/CW-7148/emit-inbox-webhook-when-an-inbox-is-disconnected

## How to test

1. Set `ENABLE_INBOX_EVENTS=true` and restart the app.
2. In **Settings → Integrations → Webhooks**, add a webhook and
subscribe to **Inbox updated**.
3. Disconnect an inbox — let an email/Instagram channel hit its
auth-error threshold, or run `inbox.channel.prompt_reauthorization!` in
a console.
4. The endpoint receives an `inbox_updated` event whose
`changed_attributes` shows `reauthorization_required` flipping to
`true`.
5. Re-authenticate the inbox (or run `inbox.channel.reauthorized!`) —
the endpoint receives the `true → false` transition.
6. Confirm the **Inbox updated** option is hidden when
`ENABLE_INBOX_EVENTS` is unset.

---------

Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
2026-05-22 09:00:18 +04:00
Tanmay Deep SharmaandGitHub b1db6c3e9b fix: make zadd function optimised to stay in rubocop limits (#14520)
## Description
Fixes rubocop for alfred.rb file on develop 

## Type of change

- [x] 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
2026-05-21 17:24:51 +05:30
Shivam MishraandGitHub 3d20a7b049 feat: generate Help Center for Onboarding (#14370)
## Manually triggering help center generation

Open a Rails console (`bundle exec rails console`):

```ruby
account = Account.find(<ACCOUNT_ID>)
user    = account.users.first

# Optional: refresh brand info from the customer's website
domain = 'example.com'
result = WebsiteBrandingService.new("noreply@#{domain}").perform
account.update!(
  name: result[:title].presence || account.name,
  custom_attributes: account.custom_attributes.merge('website' => domain, 'brand_info' => result)
)

# Optional: wipe existing portals so a fresh one is created
account.portals.destroy_all

Onboarding::HelpCenterCreationService.new(account, user).perform
```

Sidekiq must be running — articles are written by
`Onboarding::HelpCenterArticleGenerationJob`. Avoid running on
production; generation calls the LLM provider.


### Generation flow (Happy Path) 

```mermaid
sequenceDiagram
    autonumber

    participant Kickoff as HelpCenterCreationService
    participant DB as DB
    participant GenJob as HelpCenterArticleGenerationJob
    participant Curator as HelpCenterCurator
    participant Firecrawl as Firecrawl
    participant CuratorLLM as Curation LLM
    participant Redis as Redis Progress
    participant WriterJob as HelpCenterArticleWriterJob
    participant Builder as HelpCenterArticleBuilder
    participant WriterLLM as Writer LLM
    participant Cable as ActionCable

    Kickoff->>DB: Create portal for account<br/>homepage_link=https://chatwoot.com
    Kickoff->>DB: Attach brand logo if available
    Kickoff->>GenJob: Enqueue generation job<br/>account_id, portal_id, user_id, generation_id

    GenJob->>Curator: Curate help center plan
    Curator->>Firecrawl: map https://chatwoot.com<br/>search: docs help support faq
    Firecrawl-->>Curator: Return discovered links
    Curator->>CuratorLLM: Select categories + article plans<br/>from discovered links only
    CuratorLLM-->>Curator: Return categories, articles, allowed_urls

    GenJob->>DB: Create portal categories
    GenJob->>GenJob: Stamp articles with category_id
    GenJob->>GenJob: Filter article URLs against allowed_urls
    GenJob->>GenJob: Drop articles with no category<br/>or no approved source URLs

    GenJob->>Redis: Start progress<br/>status=generating, total=N, finished=0

    loop For each approved article
      GenJob->>WriterJob: Enqueue writer job<br/>title, category_id, approved URLs
    end

    par Writer jobs run independently
      WriterJob->>Builder: Build article from approved URLs
      Builder->>Firecrawl: batch_scrape approved URLs
      Firecrawl-->>Builder: Return Markdown source pages
      Builder->>WriterLLM: Rewrite sources into one article
      WriterLLM-->>Builder: Return title, description, Markdown content
      Builder->>DB: Create draft portal article<br/>meta.source_urls
      WriterJob->>Redis: Increment finished count
      WriterJob->>Cable: Broadcast help_center.article_generated
    end

    WriterJob->>Redis: If finished >= total<br/>mark status=completed
    WriterJob->>Cable: Broadcast help_center.generation_completed
```

### Redis State Management

```mermaid
 stateDiagram-v2
    [*] --> active_pointer_set
    active_pointer_set --> generating: generation job creates valid plan
    active_pointer_set --> skipped: curation skipped/failed

    generating --> generating: each writer job increments finished
    generating --> completed: finished == total
    generating --> ignored_completion: generation_id superseded

    skipped --> [*]
    completed --> [*]
    ignored_completion --> [*]
```
2026-05-21 16:25:01 +05:30
Tanmay Deep SharmaandGitHub 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
2026-05-21 16:14:28 +05:30
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>
2026-05-20 19:21:25 +05:30
27f2c2b392 feat: Unread Count: added api, store refresher, invalidation and events (2/3)[CW-6851] (#14369)
# 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>
2026-05-20 17:36:09 +05:30
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>
2026-05-20 13:36:43 +04:00
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>
2026-05-20 14:26:21 +05:30
1913ccadfa fix: [CW-7141] fix gem audit issue for sidekiq-cron and devise (#14497)
# Pull Request Template

## Description
* sidekiq-cron upgraded to 2.4.0
* Sidekiq constrained to stay on 7.3.x
* Devise advisory ignored in .bundler-audit.yml with the reason:
Chatwoot does not enable Timeoutable, so the timeout redirect path is
not reachable


### Details
The sidekiq-cron upgrade is from 1.12.0 to 2.4.0.

What changed that matters for us:

Fixes the reported Sidekiq Web UI reflected XSS in 2.4.0.
Adds namespace handling changes from the 2.x series. Chatwoot does not
use custom cron namespaces in config/schedule.yml, so the migration
guide says no action is needed for our usage.
Drops support for old Sidekiq/Redis versions. We are still on Sidekiq
7.3.1, which is supported.
Adds new dependencies: cronex and unicode.
Keeps the same APIs we use: Sidekiq::Cron::Job.load_from_hash!(schedule,
source: 'schedule'), Sidekiq::Cron::Job.destroy(name), and require
'sidekiq/cron/web' still exist.
Chance of breakage: low, based on the current Chatwoot usage.

The main thing I would watch after deploy is scheduled job registration
in Sidekiq. The one subtle area is namespace behavior: if production has
custom, manually-created cron jobs using non-default namespaces,
load_from_hash! cleanup behavior could matter. For the committed
config/schedule.yml jobs, which do not specify namespaces, they should
continue in the default namespace.

For concerns around Devise, it does not look exploitable in current
Chatwoot, because Chatwoot does not enable Devise :timeoutable.
I checked:
app/models/user.rb (line 59) lists the Devise modules, and :timeoutable
is not included.
config/initializers/devise.rb (line 164) has the timeoutable section,
but config.timeout_in is commented out.
SuperAdmin inherits from User, so it does not add a separate timeoutable
path either.
So from a practical security perspective: the vulnerable redirect path
requires warden_message == :timeout, which is only produced by Devise
Timeoutable. Since Chatwoot does not use Timeoutable, this warning is
not currently reachable.
Is the patch really needed? Strictly for current exploitability: no.

Fixes #CW-7141

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

Spec and lints and change-log checks with codex.


## 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: Vishnu Narayanan <iamwishnu@gmail.com>
2026-05-19 20:39:40 +05:30
Sivin VargheseandGitHub bca95efb82 feat: add image resize support in articles (#14293) 2026-05-19 19:34:43 +05:30
6560dbb68d feat: Add an option on the dashboard to allow switching help center layout (#14491)
<img width="633" height="431" alt="Screenshot 2026-05-18 at 12 32 55 PM"
src="https://github.com/user-attachments/assets/682d4c5f-4c76-465b-8d2f-92fbc2bb2a40"
/>

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
2026-05-19 06:42:48 -07:00
Sivin VargheseandGitHub 497d34c097 fix: render markdown in CSAT survey messages (#14468) 2026-05-19 10:27:14 +05:30
4371793741 fix: captain-v2 cannot see image attachments shared via email (#14449)
## Description

Inbound email attachments are stored with `file_type: 'file'` regardless
of their actual MIME type. As a result, image screenshots shared by
customers via email are not exposed to Captain V2's multimodal pipeline
— `Captain::OpenAiMessageBuilderService#attachment_parts` selects images
via `attachments.where(file_type: :image)` and emits a placeholder
`"User has shared an attachment"` text part instead of an `image_url`
part. The model never gets the image, so Captain keeps asking the
customer to retype information that is already visible in the
screenshot.

This PR makes the email mailbox derive `file_type` from the blob's
`content_type` using the existing shared `FileTypeHelper`, matching how
every other inbound channel (`twilio`, `sms`, `telegram`, `line`,
`tiktok`, `twitter`, `messenger`) and `MessageBuilder` already classify
attachments.

Fixes #14448

## Type of change

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

## How Has This Been Tested?

Reproduced and verified on a self-hosted production instance:

1. Real customer reply via email with a PNG screenshot of an in-app
error.

   Before:
   ```ruby
   a = Message.find(<id>).attachments.first
   a.file_type                # => "file"
   a.file.blob.content_type   # => "image/png"
Captain::OpenAiMessageBuilderService.new(message:
a.message).generate_content
   # => [{type: 'text', text: '...'},
# {type: 'text', text: 'User has shared an attachment'}]  no image_url
   ```
Captain reply: "Please copy and paste the full error text…" (model never
saw the image).

2. After the patch + force-recreate, same conversation:
   ```ruby
   a.file_type                # => "image"
Captain::OpenAiMessageBuilderService.new(message:
a.message).generate_content
   # => [{type: 'text',      text: '...'},
# {type: 'image_url', image_url: {url: 'https://.../<blob>.png'}}] 
   ```
Captain reply now correctly references the on-screen error text from the
screenshot via the multimodal vision path — no more deflection.

3. Regression sanity-check on non-image attachments (PDF / Office docs):
`file_type` falls through to `:file`, behavior unchanged.

## Notes for self-hosted operators

Existing email image attachments in the DB will still have `file_type:
'file'`. A one-shot backfill is straightforward and safe (no data loss,
only metadata):

```ruby
Attachment.joins(message: :conversation)
          .where(messages: { content_type: 'incoming_email' })
          .where(file_type: 'file')
          .find_each do |a|
  next unless a.file.attached?
  ct = a.file.blob.content_type.to_s
  next unless ct.start_with?('image/', 'audio/', 'video/')
  new_type = ct.start_with?('image/') ? :image : (ct.start_with?('video/') ? :video : :audio)
  a.update_columns(file_type: Attachment.file_types[new_type])
end
```

## Checklist

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective — happy to add a
`mailbox_helper_spec` example for `process_regular_attachments` if
maintainers prefer; existing specs in that file focus on inline-image
handling.

---------

Co-authored-by: Aakash Bakhle <48802744+aakashb95@users.noreply.github.com>
2026-05-19 10:11:32 +05:30
64585faff0 feat: Add a documentation layout design for public help center portal (#14403)
https://github.com/user-attachments/assets/fc4d15f9-2b54-4627-940f-94772ec739b1

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
2026-05-18 12:30:08 -07:00
Sony Mathew e05008e0a4 Merge branch 'release/4.14.0' into develop 2026-05-18 21:55:01 +05:30
Sony Mathew 92f8c13ce5 Bump version to 4.14.0 2026-05-18 21:53:43 +05:30
c4089f2226 fix(csat): Require confirmation before submitting rating (#14450)
Customers reported that the CSAT survey was recording their rating the
instant they tapped a star — leaving no chance to correct an accidental
pick. This change lets the customer freely change their selection until
they actually submit the comment/feedback. The rating still saves on
click (so we don't lose ratings when a customer never types a comment),
but it stays editable until the comment form is submitted. Once that
happens, the rating locks.

The flow on both surfaces:

- Customer taps a star/emoji → rating is saved.
- Customer taps a different star/emoji → previous save is overwritten
with the new value.
- Customer types a comment and submits → latest rating + comment are
saved together.
- After that submit, the rating is locked and can't be changed.

Two surfaces are updated:

- **Standalone survey page** (`/survey/responses/:uuid`) — the rating
buttons remain re-tappable until the Feedback form is submitted; once
submitted, both rating and feedback lock.
- **In-conversation widget CSAT** — same behavior, the inline
arrow-submit button on the feedback form is the locking action.

In-flight guards prevent a race where the customer changes their pick
mid-network-call (raised by the codex review on the earlier revision):
while a save is in flight, the rating controls are temporarily disabled
so the request and the displayed selection can't diverge.

## Closes

-
https://linear.app/chatwoot/issue/CW-7061/csat-star-rating-submits-on-first-click-needs-confirmation-step

## How to test

**Standalone survey page**
1. Enable CSAT on any inbox (Settings → Inboxes → Configuration → CSAT
survey).
2. Resolve a conversation in that inbox so a CSAT message is generated.
3. Open the survey URL:
`{FRONTEND_URL}/survey/responses/{conversation.uuid}` (easiest: `bundle
exec rails runner 'puts Conversation.joins(:messages).where(messages: {
content_type: "input_csat" }).last.csat_survey_link'`).
4. Tap a star/emoji — confirm the rating saves (Network panel shows a
PUT to `/public/api/v1/csat_survey/{uuid}`).
5. Tap a different star/emoji — confirm a second PUT goes out with the
new value; the latest selection is reflected.
6. Type a comment and hit Submit feedback — confirm rating + feedback
persist; both controls now lock.
7. Reload the page — the locked state is rehydrated correctly.

**Widget CSAT**
1. From an inbox with CSAT enabled, resolve a conversation that has an
active widget session.
2. In the widget, the CSAT card appears with stars/emojis + the inline
comment box.
3. Tap a star/emoji — confirm a PATCH goes out and the selection visibly
updates.
4. Tap different stars/emojis — confirm each overrides the previous
save.
5. Type a comment and click the arrow — rating + comment submit
together; stars lock.

Both display types (emoji and 5-star) should behave consistently.

## What changed

- `app/javascript/survey/views/Response.vue` — `selectRating()` saves on
every tap and short-circuits while a save is in flight (or after
feedback was submitted). Rating components are disabled by
`isFeedbackSubmitted || isUpdating` so the lock follows the feedback
submission, not the first rating tap.
- `app/javascript/survey/components/Rating.vue` — new `isDisabled` prop.
The disabled / hover styling and click guard key off it instead of the
presence of `selectedRating`, so emojis stay re-clickable until the
feedback step locks them.
- `app/javascript/shared/components/CustomerSatisfaction.vue` — same
shape for the widget: rating click auto-submits and re-clicks override
the previous save; controls disabled while a submit is in flight;
emoji-button styling and the inline `StarRating` lock on
`isFeedbackSubmitted || isUpdating`.

---------

Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
Co-authored-by: Sony Mathew <2040199+sony-mathew@users.noreply.github.com>
2026-05-18 21:46:30 +05:30
Sivin VargheseandGitHub bcb66cdcc0 chore: Support creating articles from category view (#14406)
# Pull Request Template

## Description

This PR adds support for creating articles directly from the category
view. Previously, articles could only be created from the main articles
page. With this change, users can now create a new article while
browsing a specific category, making the workflow faster and more
convenient.

Fixes
https://linear.app/chatwoot/issue/CW-7050/create-an-article-when-inside-a-category

## Type of change

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

## How Has This Been Tested?

### Screencast


https://github.com/user-attachments/assets/e5a72a85-676e-4747-948a-6b1a19d2089f




## 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-05-18 18:09:53 +05:30
7f0d5caca4 chore: Update translations (#14276)
Co-authored-by: Sojan Jose <sojan@pepalo.com>
2026-05-18 17:31:05 +05:30
1d2f3e86dd feat(companies): track company last activity (#14435)
Tracks company recency from linked contact activity so the Companies
list and detail page can show/sort by real customer engagement instead
of generic record updates.

## Closes

None.

## Why

Company recency should reflect activity from people associated with the
company. This keeps the signal tied to persisted contact activity,
without treating passive online presence or widget heartbeat pings as
company activity.

## What Changed

- Adds a company helper to record `last_activity_at` from linked contact
activity.
- Rolls up `Contact#last_activity_at` changes to the associated company.
- Initializes company activity when an already-active contact is
associated with a company, including the business-email auto-association
path.
- Throttles company activity rollups to once every 5 minutes per company
to avoid unnecessary writes during active conversations.
- Treats company activity as monotonic: unlinking, moving, or deleting
contacts does not move a company's activity timestamp backwards.
- Leaves historical backfill, online presence tracking, widget visit
tracking, and richer activity attribution out of scope.

## How to Test

1. Open an account with Companies enabled and visit the Companies list.
2. Trigger activity for a contact that belongs to a company, for example
by receiving or sending a message in that contact's conversation.
3. Confirm the linked company shows a recent activity timestamp in the
Companies list/detail page after the contact activity updates.
4. Associate an already-active contact with a company and confirm the
company receives that contact's existing activity timestamp.
5. Confirm repeated contact activity within a short window does not
continuously rewrite the company timestamp.

---------

Co-authored-by: Sony Mathew <2040199+sony-mathew@users.noreply.github.com>
2026-05-18 15:01:05 +05:30
3253e863ed fix: validate OpenAI hook credentials (#14068)
# Pull Request Template

## Description

- Validates openai key while configuring hooks
- added backfill logic

Fixes # (issue)

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

<img width="1710" height="1234" alt="CleanShot 2026-04-15 at 16 15
02@2x"
src="https://github.com/user-attachments/assets/3d319fe0-19f9-4fd0-9308-74987daac2e1"
/>

<img width="2884" height="1136" alt="CleanShot 2026-05-11 at 19 22
53@2x"
src="https://github.com/user-attachments/assets/5eae8650-985b-4c4a-af42-35f7175ff52d"
/>



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

---------

Co-authored-by: Vishnu Narayanan <iamwishnu@gmail.com>
2026-05-18 14:08:57 +05:30
Aakash BakhleandGitHub 059d840272 feat: Refresh llm settings when superadmin configs change [AI-151] (#14388)
# Pull Request Template

## Description

fixes:
https://linear.app/chatwoot/issue/AI-151/captains-super-admin-config-dont-get-applied-into-rails-without

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

To test locally: 
go to super admin -> settings -> captain -> Change endpoint to something
incorrect
go to local app -> captain -> playground -> try chatting (should fail
due to incorrect endpoint)

now in super admin captain settings, set the correct endpoint then chat
in playground. Now it should work.

Current develop code doesn't reflect the changes in installation config
for captain instantly, needs a server restart.

## 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-05-18 14:08:26 +05:30
b8deb89613 fix: make SAML callback session independent (#14467)
This PR makes SAML login independent of Rails session cookies

## Problem

The normal SAML login flow should be straightforward:

- User opens Chatwoot.
- Chatwoot creates `_chatwoot_session`.
- User starts SSO.
- Chatwoot redirects the browser to the SAML provider.
- The provider authenticates the user.
- The provider sends the browser back to Chatwoot's ACS URL.
- Chatwoot reads the SAML response, finds or creates the user, and logs
them in.

The fragile step is the ACS callback. Most SSO flows return to the app
through browser redirects where cookies usually pass through as
expected. **ADFS commonly returns the SAML response with a cross-site
POST**. With Chatwoot's session cookie using `SameSite=Lax`, browsers
may not send `_chatwoot_session` on that POST.

SAML validation itself does not need the old Rails session cookie. The
problem was our callback handoff after validation. DeviseTokenAuth
stores the verified OmniAuth payload in Rails session, then redirects to
a second callback route. If the browser does not preserve that session,
Chatwoot has already received a valid SAML response but can no longer
finish login.

## Solution

This PR removes the session-backed handoff for SAML only:

- The SAML callback completes login in the same request where OmniAuth
validates the SAML response.
- Chatwoot reads the verified auth payload directly from
`request.env['omniauth.auth']`.
- Account context and RelayState come from callback params or OmniAuth
env data, not Rails session.
- Other OmniAuth providers continue using the existing DeviseTokenAuth
flow.
- Mobile SAML still works when the IdP returns `RelayState=mobile`; the
callback redirects to the mobile deep link with the generated SSO token.

The previous SAML override used `303 See Other` to avoid replaying the
SAML POST into the second callback route. This change keeps that intent,
but removes the second callback route for SAML entirely.

## Screen recording

### SP Initiated

https://github.com/user-attachments/assets/b0735e93-3864-4cc3-b6fc-419fff4b549e

### IDP Initiated

https://github.com/user-attachments/assets/3ded0246-933c-4c85-9b7c-fa15fdc34883

## Testing

Manual validation:

- Complete a SAML login.
- In the browser network trace, find the IdP POST to
`/omniauth/saml/callback?account_id=<account-id>`.
- Confirm it redirects directly to `/app/login?...sso_auth_token=...`
for web login.
- For mobile, confirm `RelayState=mobile` redirects to the configured
mobile deep link.
- Confirm there is no intermediate `/auth/saml/callback` request.

Testing with mocksaml.com:

- Configure Chatwoot with a public `FRONTEND_URL`.
- Set the mocksaml ACS URL to:

```text
https://<chatwoot-host>/omniauth/saml/callback?account_id=<account-id>
```

- Set the mocksaml audience/SP entity ID to the value shown in Chatwoot
SAML settings, usually:

```text
https://<chatwoot-host>/saml/sp/<account-id>
```

- Use an email returned by mocksaml that exists in the SAML-enabled
account.
- Start login from Chatwoot's SSO login page.
- Confirm the callback redirects directly to the app login URL with an
SSO token.

---------

Co-authored-by: Sojan Jose <sojan@pepalo.com>
2026-05-18 12:52:45 +05:30
Shivam MishraandGitHub ef27e571f7 feat: enable quoted reply for everyone (#14469)
Quoted email replies is now available to every account by default.
Previously this was gated behind the `quoted_email_reply` account-level
feature flag, so accounts needed it toggled on (via Super Admin) before
agents saw the toggle in the reply box.

## How to test

1. Open any conversation on an email inbox.
2. Confirm the **Quote previous email** toggle is visible in the reply
box (and is **not** visible on private notes or non-email channels).
3. Toggle it on, type a reply, and send — the outbound email should
include the quoted prior email below your message.
4. Toggle it off and send another reply — the quoted block should not
appear.
5. The toggle preference should persist per channel type (UI setting),
as before.
6. Verify the toggle works on a brand new account with no feature flags
flipped on (previously it would have been hidden).

## What changed

- Removed all `isFeatureEnabledonAccount(..., QUOTED_EMAIL_REPLY)` gates
from `ReplyBox.vue`, so the toggle and quoted-content behavior are
unconditional on email channels.
- Removed the `QUOTED_EMAIL_REPLY` constant from
`dashboard/featureFlags.js`.
- Marked the flag as `deprecated: true` in `config/features.yml` (kept
the entry in place to preserve FlagShihTzu bit positions on existing
accounts; `deprecated: true` hides it from the Super Admin UI).
- Dropped the now-unnecessary
`account.enable_features('quoted_email_reply')` setup from the message
builder spec.
2026-05-15 10:59:48 -07:00
1528fcde0c fix: missing widget es translations (#14375)
# Pull Request Template

## Description

There were English strings in the Spanish i18n file for the widget. This
PR translates them.

## Type of change

Please delete options that are not relevant.

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

## How Has This Been Tested?


## 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
- [ ] 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: Sojan Jose <sojan@pepalo.com>
2026-05-15 12:16:49 +05:30
5f6bd951b9 fix: portals#create returns 500 when custom_domain is omitted (#14400)
## Description

`POST /api/v1/accounts/:account_id/portals` returns a generic 500
(`{"status":500,"error":"Internal Server Error"}`) whenever the request
body omits `custom_domain`. Root cause: `parsed_custom_domain` calls
`URI.parse(@portal.custom_domain)` and `URI.parse(nil)` raises
`URI::InvalidURIError`. Existing callers either had to know to pass
`"custom_domain": ""` as a workaround or hit a 500 with no useful
diagnostic.

This PR guards `parsed_custom_domain` against blank values so the
existing fall-through (`else @portal.custom_domain`) applies —
equivalent to passing an empty string.

It also moves the `process_attached_logo` guard from the helper into the
`create` call site so `create` mirrors `update` (`process_attached_logo
if params[:blob_id].present?`) and avoids an unnecessary signed-blob
lookup on every create that doesn't include a logo.

Fixes #14397

## Type of change

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

## How Has This Been Tested?

Two new request specs in
`spec/controllers/api/v1/accounts/portals_controller_spec.rb` covering
the regression:

- `creates portal when custom_domain is omitted from request body` — the
previously-broken case, now returns 200.
- `creates portal when custom_domain is blank` — verifies the existing
workaround (`"custom_domain": ""`) still works after the change.

Manually verified against `chatwoot/chatwoot:latest` Docker image before
the fix (500) and against this branch (200) using the curl repro from
the issue.

```bash
curl -X POST "https://<host>/api/v1/accounts/<account_id>/portals" \
  -H "Content-Type: application/json" \
  -H "api_access_token: <token>" \
  -d '{"name":"Test Portal","slug":"test-portal","color":"#3b82f6"}'
```

Before: `{"status":500,"error":"Internal Server Error"}`
After: `200 OK` with the portal payload.

## 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 (no doc
change needed — controller behaviour, fully backward-compatible)
- [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: Sojan Jose <sojan@pepalo.com>
2026-05-15 11:11:18 +05:30
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Pranav
cfc7699b7e chore(deps): bump net-imap from 0.4.20 to 0.4.24 (#14361)
Bumps [net-imap](https://github.com/ruby/net-imap) from 0.4.20 to
0.4.24.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/ruby/net-imap/releases">net-imap's
releases</a>.</em></p>
<blockquote>
<h2>v0.4.24</h2>
<blockquote>
<p>[!IMPORTANT]
<em>The <code>0.4.x</code> release branch will only receive critical
security fixes, and will be unsupported when ruby 3.3 is EOL.
Please upgrade to a newer version.</em></p>
</blockquote>
<h2>What's Changed</h2>
<h3>🔒 Security</h3>
<p>This release contains fixes for <strong>multiple
vulnerabilities</strong> concerning <em><strong><code>STARTTLS</code>
stripping</strong></em>, argument validation, and denial of service
attacks.</p>
<blockquote>
<p>[!WARNING]
<a
href="https://redirect.github.com/ruby/net-imap/pull/666">ruby/net-imap#666</a>
fixes a <code>STARTTLS</code> stripping vulnerability
(GHSA-vcgp-9326-pqcp).
Without this fix, a man-in-the-middle attacker can cause
<code>Net::IMAP#starttls</code> to return &quot;successfully&quot;,
<strong><em>without starting TLS</em></strong>.</p>
</blockquote>
<blockquote>
<p>[!IMPORTANT]
Argument validation is significantly improved. Several injection
vulnerabilities have been fixed:
<a
href="https://redirect.github.com/ruby/net-imap/pull/663">ruby/net-imap#663</a>
fixes CRLF/command/argument injection via Symbol arguments
(GHSA-75xq-5h9v-w6px).
<a
href="https://redirect.github.com/ruby/net-imap/pull/663">ruby/net-imap#663</a>
fixes CRLF/command/argument injection via the <code>attr</code> argument
to <code>#store</code>/<code>#uid_store</code> (GHSA-hm49-wcqc-g2xg)
<a
href="https://redirect.github.com/ruby/net-imap/pull/663">ruby/net-imap#663</a>
fixes CRLF/command/argument injection via the <code>storage_limit</code>
argument to <code>#setquota</code> (GHSA-hm49-wcqc-g2xg).
<a
href="https://redirect.github.com/ruby/net-imap/pull/663">ruby/net-imap#663</a>
fixes CRLF/command injection via <code>RawData</code>
(GHSA-hm49-wcqc-g2xg):</p>
<ul>
<li><code>#search</code> and <code>#uid_search</code> send
<code>criteria</code> as raw data, when it is a String</li>
<li><code>#fetch</code> and <code>#uid_fetch</code> send
<code>attr</code> as raw data, when it is a String.
When <code>attr</code> is an Array, its String members are sent as raw
data.</li>
</ul>
</blockquote>
<blockquote>
<p>[!CAUTION]
<code>RawData</code> does not defend against <em>other</em> forms of
argument injection! It is an intentionally low-level API.</p>
</blockquote>
<blockquote>
<p>[!NOTE]
Two denial of service vulnerabilities have been addressed.
These are generally only relevant when connecting to an <em>untrusted
hostile server</em> (or without TLS).</p>
<p><a
href="https://redirect.github.com/ruby/net-imap/pull/651">ruby/net-imap#651</a>
fixes quadratic time complexity when reading large responses containing
many string literals (GHSA-q2mw-fvj9-vvcw).
<a
href="https://redirect.github.com/ruby/net-imap/pull/655">ruby/net-imap#655</a>
adds a configurable <code>max_iterations</code> count for
<code>SCRAM-*</code> authentication (GHSA-87pf-fpwv-p7m7).</p>
<p>The default <code>ScramAuthenticator#max_iterations</code> is
<code>2**31 - 1</code> (max 32-bit signed int), which was already
OpenSSL's maximum value. <em>It provides no protection</em> against
hostile servers unless it is explicitly set to a lower value by the
user.</p>
</blockquote>
<h3>Added</h3>
<ul>
<li>🔒 Add <code>ScramAuthenticator#max_iterations</code> (backports <a
href="https://redirect.github.com/ruby/net-imap/issues/654">#654</a>) in
<a
href="https://redirect.github.com/ruby/net-imap/pull/655">ruby/net-imap#655</a>,
reported by <a
href="https://github.com/Masamuneee"><code>@​Masamuneee</code></a></li>
</ul>
<h3>Fixed</h3>
<ul>
<li>🔒 Fix STARTTLS stripping vulnerability (backports <a
href="https://redirect.github.com/ruby/net-imap/issues/664">#664</a>) in
<a
href="https://redirect.github.com/ruby/net-imap/pull/666">ruby/net-imap#666</a>,
reported by <a
href="https://github.com/Masamuneee"><code>@​Masamuneee</code></a></li>
<li>🔒 Fix CRLF injection vulnerabilities (backports <a
href="https://redirect.github.com/ruby/net-imap/issues/657">#657</a>, <a
href="https://redirect.github.com/ruby/net-imap/issues/658">#658</a>, <a
href="https://redirect.github.com/ruby/net-imap/issues/659">#659</a>, <a
href="https://redirect.github.com/ruby/net-imap/issues/660">#660</a>, <a
href="https://redirect.github.com/ruby/net-imap/issues/636">#636</a>, <a
href="https://redirect.github.com/ruby/net-imap/issues/661">#661</a>) in
<a
href="https://redirect.github.com/ruby/net-imap/pull/663">ruby/net-imap#663</a>,
reported by <a
href="https://github.com/manunio"><code>@​manunio</code></a></li>
<li> Much faster ResponseReader performance (backports <a
href="https://redirect.github.com/ruby/net-imap/issues/642">#642</a>) in
<a
href="https://redirect.github.com/ruby/net-imap/pull/651">ruby/net-imap#651</a>,
reported by <a
href="https://github.com/Masamuneee"><code>@​Masamuneee</code></a></li>
<li>🐛 Wait to continue RawData literals (backports <a
href="https://redirect.github.com/ruby/net-imap/issues/660">#660</a>) by
<a href="https://github.com/nevans"><code>@​nevans</code></a> in <a
href="https://redirect.github.com/ruby/net-imap/pull/663">ruby/net-imap#663</a></li>
</ul>
<h3>Other Changes</h3>
<ul>
<li>♻️ Improve internal literal sending (partially backports <a
href="https://redirect.github.com/ruby/net-imap/issues/358">#358</a>, <a
href="https://redirect.github.com/ruby/net-imap/issues/616">#616</a>, <a
href="https://redirect.github.com/ruby/net-imap/issues/649">#649</a>) by
<a href="https://github.com/nevans"><code>@​nevans</code></a> in <a
href="https://redirect.github.com/ruby/net-imap/pull/653">ruby/net-imap#653</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/ruby/net-imap/compare/v0.4.23...v0.4.24">https://github.com/ruby/net-imap/compare/v0.4.23...v0.4.24</a></p>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/ruby/net-imap/commit/24a4e770b43230286a05aa2a9746cdbb3eb8485e"><code>24a4e77</code></a>
🔀 Merge pull request <a
href="https://redirect.github.com/ruby/net-imap/issues/666">#666</a>
from ruby/backport/v0.4/STARTTLS-stripping</li>
<li><a
href="https://github.com/ruby/net-imap/commit/63f53ffdefa3be6c779090bdbe6b7257b632b36c"><code>63f53ff</code></a>
🔖 Bump version to 0.4.24</li>
<li><a
href="https://github.com/ruby/net-imap/commit/038ae35d5ecbb2b85f77f4fa35e46604154dc8c4"><code>038ae35</code></a>
🍒 pick 24d5c773d: 🔒🥅 Handle tagged &quot;OK&quot; to incomplete command
[backport <a
href="https://redirect.github.com/ruby/net-imap/issues/664">#664</a>]</li>
<li><a
href="https://github.com/ruby/net-imap/commit/705aa59faa28083f496dce50c0ee1eccca287506"><code>705aa59</code></a>
🍒 pick 62eea6ffe: 🔒🥅 Ensure STARTTLS tagged response was handled
[backport <a
href="https://redirect.github.com/ruby/net-imap/issues/664">#664</a>]</li>
<li><a
href="https://github.com/ruby/net-imap/commit/c9a6f28f8794cb991613d378b3920e5719062e29"><code>c9a6f28</code></a>
🍒 pick 46636cae8: 🔒 Add failing test for STARTTLS stripping [backport
<a
href="https://redirect.github.com/ruby/net-imap/issues/664">#664</a>]</li>
<li><a
href="https://github.com/ruby/net-imap/commit/aec06996eb87a7e1bbcef1f9f8926e8add2b8c71"><code>aec0699</code></a>
🔀 Merge pull request <a
href="https://redirect.github.com/ruby/net-imap/issues/663">#663</a>
from ruby/backport/v0.4/raw_data-warnings</li>
<li><a
href="https://github.com/ruby/net-imap/commit/fd245ddd1e220143c9b7c7861700590fbe9d35ef"><code>fd245dd</code></a>
🍒 pick be32e712e: 📚 Improve documentation of RawData arguments
[backports <a
href="https://redirect.github.com/ruby/net-imap/issues/661">#661</a>]</li>
<li><a
href="https://github.com/ruby/net-imap/commit/6dd110bfda6b47da9c294791ef16911bd1f49033"><code>6dd110b</code></a>
🍒 pick 47c72186d: 🐛 Validate RawData and wait to continue literals
[backports...</li>
<li><a
href="https://github.com/ruby/net-imap/commit/4e93149e65f6f1a6225ef770d0cd129acb5d63a9"><code>4e93149</code></a>
🔀 Merge branch 'backport/v0.4/QUOTA-argument-validation' into
backport/v0.4/s...</li>
<li><a
href="https://github.com/ruby/net-imap/commit/d2b23602e8617311900a9b3acf0c8d708b70bad4"><code>d2b2360</code></a>
🍒 pick 0ec4fd351: 🥅 Validate <code>#setquota</code> storage limit
argument [backports <a
href="https://redirect.github.com/ruby/net-imap/issues/659">#659</a>]</li>
<li>Additional commits viewable in <a
href="https://github.com/ruby/net-imap/compare/v0.4.20...v0.4.24">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=net-imap&package-manager=bundler&previous-version=0.4.20&new-version=0.4.24)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/chatwoot/chatwoot/network/alerts).

</details>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Pranav <pranav@chatwoot.com>
2026-05-14 19:51:01 -07:00
dc332dd93e feat: add attachments endpoint for contact media view (#14391)
# Pull Request Template

## Description

This PR adds an endpoint to fetch all attachments shared with or by a
contact across all of their conversations.

Results are scoped based on the access:
* Admins can access all attachments
* Agents can access attachments only from inboxes they belong to
* Custom role agents are further filtered based on their conversation
permissions

Each attachment payload includes `conversation_id`, allowing the UI to
deep-link back to the source conversation.

Added `GET
/api/v1/accounts/:account_id/contacts/:contact_id/attachments` under the
existing contacts scope.

Fixes
https://linear.app/chatwoot/issue/CW-7021/add-media-view-to-the-contact-details-page

## Type of change

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


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

---------

Co-authored-by: Sojan Jose <sojan@pepalo.com>
2026-05-14 21:34:39 +05:30
13f66e3a88 fix: incorrect scope across controllers (#14459)
Co-authored-by: Sojan Jose <sojan@pepalo.com>
2026-05-14 20:34:18 +05:30
fbcb89e955 fix(swagger): prevent path traversal in docs controller (#14458)
This hardens the development/test Swagger docs endpoint by ensuring
requested files are resolved only within the `swagger/` directory.

This did not affect production security because the Swagger controller
only renders files in development or test environments; production
already returns `404`. The change still closes the scanner finding and
prevents future automated reports from flagging the development-only
path.

## Closes

Addresses: GHSA-xhp7-ggjq-p2rg

## How to reproduce

1. Start Chatwoot locally in development.
2. Visit `/swagger/%2Fetc%2Fpasswd`.
3. Before this change, the endpoint could render files outside the
Swagger directory in development/test.

## What changed

- Resolve Swagger file requests relative to `Rails.root/swagger`.
- Return `404` when the resolved path is outside the Swagger directory
or does not point to a file.
- Strip leading slashes from derived request paths.
- Add a request spec for the encoded absolute-path case.

## How to test

1. Start the app locally.
2. Visit `/swagger` and confirm the ReDoc page loads.
3. Visit `/swagger/swagger.json` and confirm the Swagger JSON loads.
4. Visit `/swagger/%2Fetc%2Fpasswd` and confirm it returns `404` with no
file contents.

Note: `bundle exec rspec spec/controllers/swagger_controller_spec.rb`
was passing locally earlier during this fix. A final rerun before
opening the PR was blocked because local Postgres on `localhost:5432`
was not accepting connections.

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2026-05-14 18:52:14 +05:30
Shivam MishraandGitHub 8712879681 test: Stabilize SafeFetch spec against constant-identity flake (#14454)
`spec/lib/safe_fetch_spec.rb` has been flaking intermittently under
full-suite runs with errors like:

```
expected SafeFetch::FileTooLargeError, got #<SafeFetch::FileTooLargeError: exceeded 1048576 bytes>
```

The class name on both sides is identical — yet RSpec reports a
mismatch. This PR replaces the constant-identity assertions in this spec
with a name-based matcher so the comparison stops depending on the live
Class object's identity. We have made a similar fix earlier, but that
wasn't addressing the core of the issue in #14139.


**How to reproduce:** The flake only surfaces under load. Running the
spec in isolation almost always passes, here's a [run failing in
CI](https://github.com/chatwoot/chatwoot/actions/runs/25852294516/job/75961520248?pr=14370)

## What's actually going on

Three facts combine:

1. **Test env reloads classes.** `config/environments/test.rb` sets
`cache_classes = false` so Zeitwerk reloads autoloadable code on demand.
2. **`lib/` is on the reloadable autoload tree.**
`config/application.rb` adds `lib` to `eager_load_paths`, which (with
`eager_load = false` in test) makes it lazily loaded by Zeitwerk's
*main* (reloading) autoloader. `lib/safe_fetch/` lives under that
umbrella.
3. **RSpec's `raise_error(Klass)` snapshots the Class object.**
`raise_error` matcher captures `Klass` (a specific `Class` instance)
when the matcher is built. At raise time it compares with `Module#===`,
which is identity-based.

When the executor between examples triggers
`Rails.application.reloader.reload!`, Zeitwerk does
`remove_const(:SafeFetch)` and re-installs an autoload trigger. The next
access produces a **fresh** `Class` object for
`SafeFetch::FileTooLargeError` — same name, different identity. The
matcher's snapshot now points at the dead Class, and the live raise
produces the new one. Identity fails, even though the error is
semantically correct.

This bites SafeFetch specifically because:

- SafeFetch is a *namespace* with 7 nested error classes — one reload
invalidates all of them.
- The spec contains 14+ `raise_error(SafeFetch::Foo)` assertions — many
chances to land in a reload window.
- SafeFetch is exercised by request-driven code (webhook delivery,
avatar fetch). Earlier specs in the suite warm up the reloader
machinery, which then fires during this spec.

Other custom-error specs don't visibly flake because their consumers
`rescue ConstName => e` (dynamic class lookup at raise time, walks the
ancestor chain via `Module#===` *at the moment of raise*, with no
captured snapshot), rather than RSpec's snapshot-then-compare pattern.

## What this PR does

Adds a small local helper to the spec that matches by class name string,
not by Class identity:

```ruby
def safe_fetch_error(name, message_pattern = nil)
  satisfy("raise SafeFetch::#{name}#{" matching #{message_pattern.inspect}" if message_pattern}") do |error|
    error.class.name == "SafeFetch::#{name}" &&
      (message_pattern.nil? || message_pattern.match?(error.message))
  end
end
```

Every `raise_error(described_class::FooError)` becomes
`raise_error(safe_fetch_error('FooError'))`. Class names are strings;
string equality survives any number of reloads. The semantic assertion
("this raised the right kind of error") is preserved.

This is the pattern CLAUDE.md already endorses for this codebase:

> Specs in parallel/reloading environments: prefer comparing
`error.class.name` over constant class equality when asserting raised
errors.

## Why this approach (even though it's suboptimal)

To be honest with reviewers: **this is a workaround, not a root-cause
fix.** Future spec authors who use `raise_error(SafeFetch::Foo)` in
other files will hit the same flake. The "real" fix removes the failure
mode at the source rather than dodging it per-spec.

Here's the menu of options we considered and the tradeoffs:

### Option A — Spec-side name matcher (this PR)

- **What:** the helper above.
- **Pro:** one-file change, zero blast radius beyond the spec.
- **Pro:** matches CLAUDE.md's documented stance.
- **Con:** every new spec touching reloadable error classes needs to
remember this pattern. It's a discipline tax, not a structural fix.
- **Verdict:** chosen for this PR.

### Option B — Pin SafeFetch outside Zeitwerk

```ruby
# config/application.rb
Rails.autoloaders.main.ignore(
  Rails.root.join('lib/safe_fetch.rb'),
  Rails.root.join('lib/safe_fetch'),
)
require Rails.root.join('lib/safe_fetch')
```

- **Pro:** real root-cause fix. `SafeFetch::*` constants become
process-lifetime stable. The spec helper becomes unnecessary, every
assertion in any spec works correctly.
- **Pro:** preserves the public API exactly.
- **Con:** loses hot-reload for SafeFetch in dev (need server restart to
see edits).
- **Con:** modifies `application.rb`, which has cross-team review
weight.
- **Verdict:** rejected for scope reasons in this PR; a sensible
follow-up.

### Option C — Move error classes to a non-reloadable location

Define top-level error classes in
`config/initializers/safe_fetch_errors.rb`. Initializers run once at
boot, constants are never reloaded.

- **Pro:** identity-stable errors, no spec helper needed.
- **Con:** API change — `SafeFetch::FileTooLargeError` becomes
`SafeFetchFileTooLargeError` (or similar). Every consumer's `rescue`
clause has to update.
- **Con:** namespace pollution at top-level.
- **Verdict:** rejected. The constraint of touching initializers is what
motivated the simpler PR.

### Option D — Vendor SafeFetch as a path gem

Move `lib/safe_fetch/` → `vendor/gems/safe_fetch/` with a gemspec.
Bundler `require`s gems once; Zeitwerk has zero involvement.

- **Pro:** structurally the most correct fix. Same identity stability as
Option B, no Zeitwerk plumbing required.
- **Pro:** zero API change for consumers.
- **Con:** larger refactor (6+ files moved, gemspec authored,
Gemfile/Gemfile.lock updated).
- **Con:** SafeFetch becomes harder to iterate on in dev (gem-style
edit-then-restart loop).
- **Verdict:** rejected for scope in this PR; the cleanest long-term
home for a security primitive.

### Option E — Drop custom errors, return a `Result`

Refactor SafeFetch to yield a result hash (`{ ok: true, ... }` / `{ ok:
false, kind: :unsafe_url, ... }`) instead of raising for anticipated
outcomes. Failure kinds become symbols, which are interned for the
process lifetime.

- **Pro:** eliminates the failure mode at its true root — there are no
custom exception classes to reload, anywhere.
- **Pro:** forces explicit, exhaustive handling at every call site — a
feature for a security primitive.
- **Pro:** spec assertions become data assertions (`expect(result).to
match(ok: false, kind: :too_large)`), which are robust against any
reload, any reordering.
- **Con:** paradigm shift away from the exception-driven style used
everywhere else in the codebase.
- **Con:** every consumer rewrites their `rescue` block into
pattern-matched handling.
- **Verdict:** rejected for scope in this PR; the architecturally
cleanest answer if we ever revisit SafeFetch's API.

## Why we're shipping A despite knowing B–E are better

This flake has been chewing CI time intermittently and previously took a
partial fix (#14139). We need it stable *now*. Options B–E are real
refactors with broader review surface (`application.rb`, consumer code,
or the lib's public contract). Option A:

- Costs nothing — one helper, mechanical replacements.
- Doesn't preclude any of B/C/D/E later. The helper goes away cleanly
once a root-cause fix lands.
- Aligns with the project's documented guidance for this scenario.

When SafeFetch next gets a substantive change, that's the right moment
to fold in B or D. Until then, the spec is stable and CI gets its time
back.

## What changed

- `spec/lib/safe_fetch_spec.rb`: added `safe_fetch_error(name,
message_pattern = nil)` helper; converted all 14
`raise_error(described_class::FooError[, /regex/])` assertions to
`raise_error(safe_fetch_error('FooError'[, /regex/]))`.
2026-05-14 16:48:14 +05:30
ffbf40c720 fix: harden Active Storage direct uploads and proxy streaming (#14440)
Hardens Active Storage handling on Rails 7.1 by filtering internal
direct-upload metadata keys and limiting proxy range requests, while
keeping audio playback on redirect URLs so large recordings are not
routed through the proxy limiter.

Closes
- CVE-2026-33173
- CVE-2026-33174
- CVE-2026-33658

Why
Rails 7.1 does not currently have patched releases for these Active
Storage advisories, and Chatwoot exposes Active Storage direct-upload
endpoints and media URLs. This keeps the Rails dependency unchanged
while adding small local mitigations until Rails can be upgraded to
7.2.3.1+.

What changed
- Filters `identified`, `analyzed`, and `composed` from direct-upload
blob metadata.
- Limits Active Storage proxy range requests to one range under 100 MB.
- Uses redirect URLs for inline audio attachments so normal playback of
large recordings avoids the proxy streaming path.
- Adds scoped bundle-audit ignores for the locally mitigated Active
Storage advisories and the remaining Rails advisories that are not
reachable through current Chatwoot usage.

How to test
- Upload an attachment from the dashboard reply composer and confirm it
sends successfully.
- Upload an attachment from the website widget and confirm it appears in
the conversation.
- POST a direct-upload request with `blob.metadata.identified`,
`blob.metadata.analyzed`, and `blob.metadata.composed`; confirm those
keys are not persisted while custom metadata remains.
- Play an audio/call-recording attachment and confirm the audio URL
loads through Active Storage redirect rather than proxy.
- Run `bundle exec bundle audit check -v`.

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2026-05-14 14:50:29 +05:30
dd7f5c27e5 perf: eliminate N+1 queries on inboxes#index (#14451)
The `inboxes#index` API endpoint was firing 3 queries per inbox while
rendering the response — one each for the polymorphic channel, avatar
attachment, and working hours. For accounts with many inboxes this
turned a routine list call into a multi-second request — measured at 35
seconds on an account with 5,000 inboxes.

Two root causes:
1. `policy_scope` was silently discarding the controller's
`.includes(...)` chain because `InboxPolicy::Scope#resolve` returns a
fresh `user.assigned_inboxes` relation, ignoring whatever scope is
passed in. So the eager loading never actually applied.
2. `Inbox#weekly_schedule` called `working_hours.order(...).select(...)`
which fires a new query per inbox even when the association is
preloaded.

The fix chains the eager loads and ordering after `policy_scope` so they
apply to the relation the policy actually returns, adds `:portal` and
`:working_hours` to the preload list, and refactors `weekly_schedule` to
sort the preloaded collection in Ruby and build the hash directly.

Result: queries drop from O(n) to a constant ~15 regardless of inbox
count, and total request time drops 5–6× across every account size
tested. Response payload is byte-identical.

## How to test

1. Open the agent dashboard for an account with a large number of
inboxes (1000+). On a stock dev DB you can seed quickly with
`Seeders::AccountSeeder` or by creating a few hundred `Channel::Api`
inboxes via the rails console.
2. Hit any UI surface that lists inboxes (settings → inboxes, sidebar
inbox list, agent assignment dropdowns). Should feel near-instant where
it previously hung.
3. Confirm every inbox shows the same data as before — channel type,
working hours, avatar, portal/help-center link. No fields should be
missing or different.

## Benchmarks

Real HTTP requests via curl against a dev Rails server (median of 5
timed trials after warmup; "Server total" is the time reported in Rails'
`Completed 200 OK in <X>ms` log line).

| Account | Channel mix | OLD | NEW | Speedup |
|---|---|---|---|---|
| 1000 inboxes | 3 types, no portals/avatars | 5.87 s | 0.96 s |
**6.1×** |
| 5000 inboxes | 3 types, no portals/avatars | 33.07 s | 6.93 s |
**4.8×** |
| 5000 inboxes | 10 types, 25% portals, 10% avatars | 35.04 s | 6.55 s |
**5.4×** |

Detailed breakdown for the realistic 5000-inbox case:

|                 | OLD       | NEW      | Δ        |
|-----------------|-----------|----------|----------|
| Server total    | 35,042 ms | 6,550 ms | −81%     |
| Views           | 32,034 ms | 6,452 ms | −80%     |
| ActiveRecord    | 2,948 ms  | 92 ms    | −97%     |
| Allocations     | 42.3 M    | 11.3 M   | −73%     |
| Queries         | ~15,000   | 15       | −99.9%   |

## What changed

- `app/controllers/api/v1/accounts/inboxes_controller.rb` — chain
`.includes(:channel, :portal, :working_hours, avatar_attachment:
:blob).order_by_name` *after* `policy_scope(...)`. The previous code
passed them into `policy_scope` but the policy resolver dropped the
chain.
- `app/models/concerns/out_of_offisable.rb` — `weekly_schedule` now
sorts the preloaded `working_hours` collection in Ruby and constructs
the result hashes inline, avoiding both the per-inbox query from
`.order(...).select(...)` and the `as_json` reflection overhead.

Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
2026-05-14 14:20:52 +05:30
c2523c5d8b fix(editor): Refresh reply editor when reply window reopens in real-time (#14446)
On WhatsApp and any channel that disables the reply editor outside its
messaging window (WhatsApp Cloud, Twilio WhatsApp, API channels with
`agent_reply_time_window` set), when the window was already expired and
a new inbound message arrived in real-time, the "messaging restricted"
banner correctly hid but the editor itself stayed un-typeable until the
agent refreshed the page. This made the dashboard look like it accepted
replies even though typing did nothing.

Fixes
[CW-7087](https://linear.app/chatwoot/issue/CW-7087/reply-editor-stays-disabled-after-real-time-incoming-message-reopens)

#### How to reproduce

1. Open a WhatsApp conversation whose last incoming message is older
than 24h, so `can_reply` is `false` (banner shown, editor greyed out).
2. With the dashboard open on that conversation, have the customer send
a fresh inbound message (or simulate one via the channel's webhook).
3. Before the fix: banner disappears, editor wrapper loses its disabled
styling, but clicking into the editor and typing does nothing — refresh
required.
4. After the fix: banner disappears and the editor accepts input
immediately.

Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
2026-05-14 12:16:58 +04:00
Shivam MishraandGitHub 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.
2026-05-13 20:09:48 +05:30
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>
2026-05-13 18:43:23 +05:30
Aarav UniyalandGitHub c6dceb0e07 fix: contacts dropdown overlap (#14305) 2026-05-13 15:28:58 +05:30
Sivin VargheseandGitHub cd33cea69f chore: Update nl translation in widget (#14441) 2026-05-13 14:18:25 +05:30
71cc5168be feat(linear): Auto link Linear issues from private notes (#14405)
When an agent pastes a Linear issue URL into a private note on a
conversation, Chatwoot now links the issue to the conversation
automatically — no need to click "Link to Linear issue" first. The
standard activity message ("X linked Linear issue ABC-123") is posted
just like a manual link.

Fixes
[CW-7032](https://linear.app/chatwoot/issue/CW-7032/if-someone-post-a-linear-url-in-the-private-notes-automatically-link)

---------

Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
Co-authored-by: Sojan Jose <sojan@pepalo.com>
2026-05-12 13:03:40 +04:00
58fdd20625 test(voice): WhatsApp Cloud Calling specs [5] (#14357)
Backend test coverage for the WhatsApp Cloud Calling pipeline introduced
in #14356. Stacked on top of that PR so the controller and service under
test exist when CI runs.

## Closes
- Replaces #14348 (which was based on the abandoned \`feature/pla-150\`)

## What's covered

-
\`spec/enterprise/controllers/api/v1/accounts/whatsapp_calls_controller_spec.rb\`
(new, ~210 lines)
- \`show / accept / reject / terminate / initiate / upload_recording\`
happy paths
- 422 paths: missing sdp_offer, missing recording, calling_disabled
inbox, missing contact phone, ringing-state guards, AlreadyAccepted,
NotRinging, CallFailed
- 138006 (no permission) → throttled opt-in template send under
conversation lock; idempotency on retry
  - \`upload_recording\` idempotency guard (\`already_uploaded\`)

- \`spec/enterprise/services/whatsapp/call_service_spec.rb\` (new, ~135
lines)
- State machine: ringing → in_progress → completed; ringing → failed
(reject); ringing → no_answer (terminate)
- Lock contention: concurrent terminate during accept doesn't corrupt
the message/conversation broadcast
- Provider failure paths surface as \`Voice::CallErrors::CallFailed\`
(transport and business)

- \`spec/models/channel/whatsapp_spec.rb\` — extends existing file with
\`voice_enabled?\` matrix (provider × source × calling_enabled)

## Verification

- 77/77 examples pass locally on this branch (controller + service +
channel + incoming-call + permission-reply + open-ai message builder)
- RuboCop clean

## Stack

- Backend: #14356 (\`feat/whatsapp-call-meta-bridge\` — base of this PR)
- FE: #14346 (\`feat/whatsapp-call-ui\`)

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 12:40:13 +05:30
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>
2026-05-12 10:57:29 +04:00
Sivin VargheseandGitHub 3df827c931 chore: update Captain documents filter UI (#14429) 2026-05-12 11:27:30 +05:30
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>
2026-05-12 11:23:57 +05:30
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
79a7423f9f chore(deps): bump nokogiri from 1.19.1 to 1.19.3 (#14410)
Bumps [nokogiri](https://github.com/sparklemotion/nokogiri) from 1.19.1
to 1.19.3.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/sparklemotion/nokogiri/releases">nokogiri's
releases</a>.</em></p>
<blockquote>
<h2>v1.19.3 / 2026-04-27</h2>
<h3>Fixed / Security</h3>
<ul>
<li>Address exponential regex backtracking in CSS selector tokenizer.
See <a
href="https://github.com/sparklemotion/nokogiri/security/advisories/GHSA-c4rq-3m3g-8wgx">GHSA-c4rq-3m3g-8wgx</a>
for more information.</li>
<li>[CRuby] Address memory leak in
<code>XSLT::Stylesheet#transform</code>. See <a
href="https://github.com/sparklemotion/nokogiri/security/advisories/GHSA-v2fc-qm4h-8hqv">GHSA-v2fc-qm4h-8hqv</a>
for more information.</li>
</ul>
<!-- raw HTML omitted -->

<pre><code>46b89e5d7b9e844c2ee360794240c6ea2a4e6fa0c5892a4ed487db621224b639
nokogiri-1.19.3-aarch64-linux-gnu.gem
8392dfdcd21be7a94dbbe9ccc138dea01b97b24cb2dc02a114ca98bfb1d9a0b7
nokogiri-1.19.3-aarch64-linux-musl.gem
3919d5ffc334ad778a4a9eb88fda7dcb8b1fb58c8a52ac640c6dcd2f038e774f
nokogiri-1.19.3-arm-linux-gnu.gem
9ce1cb6346bb9c67b1550eb537aa183ead91e4b6eadb2f36ade02d8dd2a79fb6
nokogiri-1.19.3-arm-linux-musl.gem
71b9bd424b1b7abc18b05052a1a3cfd3627abdca62be280854cc411791357e42
nokogiri-1.19.3-arm64-darwin.gem
40ea6ebf5cf2005dae1dee26dd557d3afb41fb6de6c9764aca8cf06fdb841db1
nokogiri-1.19.3-java.gem
8bb7132cad356c879a1286eaabcb5e68326cb2490317984280fbc62f456d506a
nokogiri-1.19.3-x64-mingw-ucrt.gem
77f3fba57d46c53ab31e62fc6c28f705109d1bf6264356c76f132b2be5728d4d
nokogiri-1.19.3-x86_64-darwin.gem
2f5078620fe12e83669b5b17311b32532a8153d02eee7ad06948b926d6080976
nokogiri-1.19.3-x86_64-linux-gnu.gem
248c906d2166eca5efb56d52fdee5f9a1f51d69a72e2b64fdac647b4ce39ea3f
nokogiri-1.19.3-x86_64-linux-musl.gem
78312cbac32a40c812780d9678221b79d51288eec00054c1a8d15f7ce05960e8
nokogiri-1.19.3.gem
</code></pre>
<h2>v1.19.2 / 2026-03-19</h2>
<h3>Dependencies</h3>
<ul>
<li>[JRuby] Saxon-HE is updated to 12.7, from 9.6.0-4. Saxon-HE is a
transitive dependency of nu.validator:jing, and this update addresses
CVEs in Saxon-HE's own transitive dependencies JDOM and dom4j. We don't
think this warrants a security release, however we're cutting a patch
release to help users whose security scanners are flagging this. <a
href="https://redirect.github.com/sparklemotion/nokogiri/issues/3611">#3611</a>
<a
href="https://github.com/flavorjones"><code>@​flavorjones</code></a></li>
</ul>
<h3>SHA256 Checksums</h3>

<pre><code>c34d5c8208025587554608e98fd88ab125b29c80f9352b821964e9a5d5cfbd19
nokogiri-1.19.2-aarch64-linux-gnu.gem
7f6b4b0202d507326841a4f790294bf75098aef50c7173443812e3ac5cb06515
nokogiri-1.19.2-aarch64-linux-musl.gem
b7fa1139016f3dc850bda1260988f0d749934a939d04ef2da13bec060d7d5081
nokogiri-1.19.2-arm-linux-gnu.gem
61114d44f6742ff72194a1b3020967201e2eb982814778d130f6471c11f9828c
nokogiri-1.19.2-arm-linux-musl.gem
58d8ea2e31a967b843b70487a44c14c8ba1866daa1b9da9be9dbdf1b43dee205
nokogiri-1.19.2-arm64-darwin.gem
e9d67034bc80ca71043040beea8a91be5dc99b662daa38a2bfb361b7a2cc8717
nokogiri-1.19.2-java.gem
8ccf25eea3363a2c7b3f2e173a3400582c633cfead27f805df9a9c56d4852d1a
nokogiri-1.19.2-x64-mingw-ucrt.gem
7d9af11fda72dfaa2961d8c4d5380ca0b51bc389dc5f8d4b859b9644f195e7a4
nokogiri-1.19.2-x86_64-darwin.gem
fa8feca882b73e871a9845f3817a72e9734c8e974bdc4fbad6e4bc6e8076b94f
nokogiri-1.19.2-x86_64-linux-gnu.gem
93128448e61a9383a30baef041bf1f5817e22f297a1d400521e90294445069a8
nokogiri-1.19.2-x86_64-linux-musl.gem
38fdd8b59db3d5ea9e7dfb14702e882b9bf819198d5bf976f17ebce12c481756
nokogiri-1.19.2.gem
</code></pre>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/sparklemotion/nokogiri/compare/v1.19.1...v1.19.2">https://github.com/sparklemotion/nokogiri/compare/v1.19.1...v1.19.2</a></p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/sparklemotion/nokogiri/blob/main/CHANGELOG.md">nokogiri's
changelog</a>.</em></p>
<blockquote>
<h2>v1.19.3 / 2026-04-27</h2>
<h3>Fixed / Security</h3>
<ul>
<li>Address exponential regex backtracking in CSS selector tokenizer.
See <a
href="https://github.com/sparklemotion/nokogiri/security/advisories/GHSA-c4rq-3m3g-8wgx">GHSA-c4rq-3m3g-8wgx</a>
for more information.</li>
<li>[CRuby] Address memory leak in
<code>XSLT::Stylesheet#transform</code>. See <a
href="https://github.com/sparklemotion/nokogiri/security/advisories/GHSA-v2fc-qm4h-8hqv">GHSA-v2fc-qm4h-8hqv</a>
for more information.</li>
</ul>
<h2>v1.19.2 / 2026-03-19</h2>
<h3>Dependencies</h3>
<ul>
<li>[JRuby] Saxon-HE is updated to 12.7, from 9.6.0-4. Saxon-HE is a
transitive dependency of nu.validator:jing, and this update addresses
CVEs in Saxon-HE's own transitive dependencies JDOM and dom4j. We don't
think this warrants a security release, however we're cutting a patch
release to help users whose security scanners are flagging this. <a
href="https://redirect.github.com/sparklemotion/nokogiri/issues/3611">#3611</a>
<a
href="https://github.com/flavorjones"><code>@​flavorjones</code></a></li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/sparklemotion/nokogiri/commit/c139a3da0fe0cae7499a0bafa20f2875877c585b"><code>c139a3d</code></a>
version bump to v1.19.3</li>
<li><a
href="https://github.com/sparklemotion/nokogiri/commit/7501a63b9f4246d12516e35b91fed8be34f854c0"><code>7501a63</code></a>
fix: backtracking in CSS tokenizer rules (v1.19.x backport) (<a
href="https://redirect.github.com/sparklemotion/nokogiri/issues/3627">#3627</a>)</li>
<li><a
href="https://github.com/sparklemotion/nokogiri/commit/03e7968a730a6544ab56a8d6c3e82dd630ad4339"><code>03e7968</code></a>
test: skip CSS tokenizer benchmarks on JRuby</li>
<li><a
href="https://github.com/sparklemotion/nokogiri/commit/b984b7e47f622d1aa97d54c16d5cd596c3eb9538"><code>b984b7e</code></a>
fix: ReDoS in CSS tokenizer ident rule</li>
<li><a
href="https://github.com/sparklemotion/nokogiri/commit/00926231e28d5a20e5b4873efba36099aea0d5c6"><code>0092623</code></a>
fix: ReDoS in CSS tokenizer STRING rule</li>
<li><a
href="https://github.com/sparklemotion/nokogiri/commit/ee17d33aff3adb30c14e71d3d4c8163465acaccf"><code>ee17d33</code></a>
fix: memory leak in XSLT transform (backport to v1.19.x) (<a
href="https://redirect.github.com/sparklemotion/nokogiri/issues/3624">#3624</a>)</li>
<li><a
href="https://github.com/sparklemotion/nokogiri/commit/ce188a395192e3757d8701949afb643dc025084c"><code>ce188a3</code></a>
doc: update CHANGELOG</li>
<li><a
href="https://github.com/sparklemotion/nokogiri/commit/caeaac41f874f0944f9397c78bf6c1bfac2cb472"><code>caeaac4</code></a>
fix: memory leak in XSLT transform</li>
<li><a
href="https://github.com/sparklemotion/nokogiri/commit/25220bf268c9808e28415563ed7f8ea8d5c332bf"><code>25220bf</code></a>
dep(test): test against libxml-ruby v6 (<a
href="https://redirect.github.com/sparklemotion/nokogiri/issues/3618">#3618</a>)</li>
<li><a
href="https://github.com/sparklemotion/nokogiri/commit/0caeb21a5c5e9ff45bbede88fb53655f6753bb0e"><code>0caeb21</code></a>
doc: add security warnings for untrusted XSLT stylesheets</li>
<li>Additional commits viewable in <a
href="https://github.com/sparklemotion/nokogiri/compare/v1.19.1...v1.19.3">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=nokogiri&package-manager=bundler&previous-version=1.19.1&new-version=1.19.3)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/chatwoot/chatwoot/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-11 18:49:31 -07:00
Sivin VargheseandGitHub 85ddc68834 fix: prevent bulk action checkbox reset in team view (#14432) 2026-05-12 07:13:48 +05:30
Sojan JoseandGitHub 086aa36ffe feat(companies): add notes and history to company details (#14401) 2026-05-11 23:15:25 +05:30
f6be0d80ef feat: UI changes for document auto sync [AI-153] (#14258)
# Pull Request Template

## Description

FE code for document sync

Adds:
- UI to show counts (stats) of available web pages, stale and synced
documents and last synced at
- Bulk action and manual ways to sync web documents
- index to stats related columns

## Type of change

Please delete options that are not relevant.

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

https://linear.app/chatwoot/issue/AI-153/fe-document-auto-sync

Documents dashboard:
<img width="2160" height="986" alt="CleanShot 2026-05-11 at 17 57 09@2x"
src="https://github.com/user-attachments/assets/6d934764-964c-4656-b005-1b4f0329e553"
/>

Filters:
<img width="1138" height="564" alt="CleanShot 2026-05-11 at 17 58 13@2x"
src="https://github.com/user-attachments/assets/cee780e6-eb8f-4aed-8cc5-b674244a821b"
/>

Needs update:
<img width="2222" height="966" alt="CleanShot 2026-05-11 at 17 57 53@2x"
src="https://github.com/user-attachments/assets/70c85ddd-7eb1-4328-ba14-7929e67e7b36"
/>

pdfs:
<img width="2180" height="558" alt="CleanShot 2026-05-11 at 17 58 30@2x"
src="https://github.com/user-attachments/assets/975b5c9f-bd1c-4979-9870-8f926d7f6e11"
/>

bulk actions:
<img width="2244" height="992" alt="CleanShot 2026-05-11 at 17 58 57@2x"
src="https://github.com/user-attachments/assets/bdb3c63f-d2de-41dc-a6d5-8821d3303be0"
/>

single url sync:
<img width="2264" height="722" alt="CleanShot 2026-05-11 at 17 59 19@2x"
src="https://github.com/user-attachments/assets/7d7323a5-0fcb-4be9-8635-55e56964999b"
/>



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

---------

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Sony Mathew <sony@chatwoot.com>
Co-authored-by: Vishnu Narayanan <iamwishnu@gmail.com>
2026-05-11 20:13:29 +05:30
Shivam MishraandGitHub 3489298726 feat: add WidgetCreationService for onboarding web widget setup (#14314)
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.
2026-05-11 16:10:48 +05:30
Shivam MishraandGitHub 2e13f69fdf chore: log errors from context.dev (#14310)
This PR updates the way we log errors and results from context.dev to
have better visibility on the enrichment process for onboarding
2026-05-11 16:09:45 +05:30
Shivam MishraandGitHub bc768bf04f chore: verbosely log errors for leadsquare activity failure (#14407) 2026-05-11 10:58:23 +05:30
Shivam MishraandGitHub cc612e755b fix: SafeFetch dependency loading (#14408)
The SafeFetch spec suite was failing in CI with `NameError:
uninitialized constant SafeFetch::Fetcher` across every example that
exercised `SafeFetch.fetch`. From a product perspective, this made the
external-file fetch path look unreliable even though the failure
happened before any network validation, SSRF protection, content-type
checks, or tempfile handling could run.

The symptom pointed to a load-order issue rather than an actual fetch
behavior regression. `SafeFetch.fetch` referenced `Fetcher` from the
top-level module, but that nested class was not guaranteed to be loaded
in every test execution path before the method was invoked.

This change keeps the existing SafeFetch split between the public API
and the implementation classes, but makes the public entry point
responsible for loading the implementation it needs before use. That is
intentionally smaller than folding all of the fetcher logic into
`lib/safe_fetch.rb`; the separate files still keep the request option
parsing and streaming implementation readable, while the public API no
longer depends on Rails or the test runner having loaded nested
constants in a particular order.

The file also now uses a single `SafeFetch` module declaration. That
removes the awkward reopen pattern and makes the dependency boundary
easier to see: constants and errors are defined first, then the public
`fetch` method loads and delegates to the implementation classes.
2026-05-08 21:18:50 +05:30
Aakash BakhleandGitHub aa10d42237 chore: bump RubyLLM version [AI-152] (#14387)
# Pull Request Template

## Description

Bump RubyLLM version and update model registry

## Type of change

Version bump on package

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


## 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-05-08 18:44:15 +05:30
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>
2026-05-08 16:40:15 +05:30
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>
2026-05-08 13:40:36 +05:30
5c6ea78ce6 fix(security): Enforce admin authorization on custom attribute definitions API (#14392)
Custom attribute definitions can now only be created, edited, or deleted
by administrators, matching the existing settings UI restriction.
Previously, an agent could call the `custom_attribute_definitions` API
directly and modify account configuration that they couldn't reach
through the dashboard — a Broken Access Control vulnerability reported
externally.

Fixes
https://linear.app/chatwoot/issue/CW-7038/broken-access-control-on-custom-attribute-definitions-api

## How to test

1. Sign in as an agent.
2. Try to create a custom attribute by calling `POST
/api/v1/accounts/<id>/custom_attribute_definitions` directly (the
settings page is hidden for agents — use curl with the agent's
`api_access_token`).
3. Expect `401 Unauthorized` with body `{"error":"You are not authorized
to do this action"}`. Repeat for `PATCH` and `DELETE`.
4. Sign in as an administrator and confirm create/edit/delete still work
from Settings → Custom Attributes.
5. As either role, the listing endpoint (`GET
.../custom_attribute_definitions`) should still succeed — agents need
this to render attributes in conversation and contact panels.

Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
2026-05-08 11:42:23 +04:00
Tanmay Deep SharmaandGitHub e6b8f48b3b fix: settle captain credits on subscription cancellation (#14089)
## Linear Ticket
-
https://linear.app/chatwoot/issue/CW-6875/captain-credits-3-bugs-in-stripe-subscription-lifecycle-cancel-ratchet

## Description

Fixes Captain credit settlement on subscription cancellation. Previously
`limits['captain_responses']` and `captain_responses_usage` were left in
their pre-cancellation state, which caused incorrect credit totals when
a customer re-subscribed. Cancellation now settles the monthly allotment
(preserving any remaining topup) and resets the usage counter.

## Type of change

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

## How Has This Been Tested?

1. Set up an account subscribed to a paid plan (e.g. Startups) so
`limits['captain_responses']` reflects the plan allotment.
2. Fire `customer.subscription.deleted` for that account's Stripe
customer. Confirm the limits.
3. Fire `customer.subscription.updated` re-subscribing to the paid plan.
Confirm the limits.
4. Repeat cancel → re-subscribe several times;


## 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-05-08 12:18:53 +05:30
Sojan JoseandGitHub 10597863d7 feat(companies): add company creation flow (#14402) 2026-05-08 11:46:33 +05:30
53b2a517d7 fix: resolve SendReplyJob flaky specs (#14394)
`SendReplyJob` was caching reloadable service class objects in
`CHANNEL_SERVICES`. In test, a request spec can trigger Rails constant
reloading after `SendReplyJob` has already been loaded, leaving the job
with stale class objects while later specs stub the reloaded constants.
This resolves the channel service at perform time so the job follows the
current Rails constant table.

How to reproduce
Run the CircleCI shard that contains send_reply_job_spec, or the
minimized order-dependent reproduction:
```sh
bundle exec rspec --format progress spec/builders/v2/reports/label_summary_builder_spec.rb spec/controllers/api/v1/accounts/bulk_actions_controller_spec.rb spec/jobs/send_reply_job_spec.rb:32
```

What changed
- Store service class names in `SendReplyJob::CHANNEL_SERVICES` instead
of class objects.
- Resolve the service with constantize inside perform so reloads do not
leave stale cached classes.

Co-authored-by: Sony Mathew <2040199+sony-mathew@users.noreply.github.com>
2026-05-07 22:47:09 +05:30
7a7db22a43 fix: Implement resend confirmation feature for login page (#11970)
# Pull Request Template

## Description

This PR fixes the non-functional resend confirmation feature on the V3
login page where clicking "Resend confirmation" did nothing. The issue
was caused by the V3 store not having the `resendConfirmation` action
that the login page was trying to dispatch.

**Key improvements:**
- Fixed V3 store integration by importing `resendConfirmation` directly
from auth API
- Added comprehensive UX improvements with loading states and 60-second
cooldown timer
- Implemented environment-aware debug logging for development
- Added proper error handling and user feedback
- Enhanced backend test coverage

**Context:** Users with unconfirmed accounts were unable to resend
confirmation emails from the login page, creating a poor user experience
and potential support burden.

Fixes #3157

## Type of change

- [x] 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?

**Backend Testing:**
- All existing resend_confirmation tests passing (7/7)
- Added comprehensive new test suite in
`spec/requests/api/v1/resend_confirmation_spec.rb`
- API endpoint returns 200 OK responses in ~0.39 seconds
- Email delivery confirmed via SMTP with test user `info@airbonar.com`

**Frontend Testing:**
- All frontend tests passing 
- ESLint compliant code with automatic corrections applied
- Manual testing of login page functionality:
  - 60-second cooldown timer with countdown display
  - Error handling with user-friendly messages
  - Development logging works (console output in dev mode only)

**Test Configuration:**
- Ruby/Rails backend with RSpec test suite
- Vue.js frontend with Jest/testing-library
- Development environment with Gmail SMTP configured
- Test user: unconfirmed account `info@airbonar.com`

**Reproduction Steps:**
1. Navigate to login page with unconfirmed account
2. Click "Resend confirmation link"
3. Observe loading state, API call, and success feedback
4. Verify 60-second cooldown prevents spam
5. Check email delivery.

## Checklist:

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

---------

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: Sojan Jose <sojan@pepalo.com>
Co-authored-by: Sony Mathew <2040199+sony-mathew@users.noreply.github.com>
Co-authored-by: Sony Mathew <sony@chatwoot.com>
2026-05-07 15:13:04 +05:30
Sivin VargheseandGitHub 2435d7503c feat: Update conversation bulk action UI (#14118) 2026-05-07 14:58:38 +05:30
salmonumbrellaandGitHub fe44b07147 feat(companies): add company detail page (#14054) 2026-05-06 20:50:27 +05:30
42bba748cf fix(mailbox): render inline images without Content-Disposition (#11949)
## Description
This pull request addresses issue #11948, where inline images embedded
in emails (such as those sent from Outlook) are not rendered correctly
if the Content-Disposition header is missing.

The solution ensures that images referenced via cid: in the HTML body
are correctly identified and rewritten using url_for.

Fixes #11948

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

## How Has This Been Tested?
Added test: detects image inline attachment by cid reference when
Content-Disposition is missing

## 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
- [ ] My changes generate no new warnings
- [X] 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: Pranav <pranav@chatwoot.com>
Co-authored-by: Sojan Jose <sojan@pepalo.com>
Co-authored-by: Sony Mathew <sony@chatwoot.com>
Co-authored-by: Sony Mathew <2040199+sony-mathew@users.noreply.github.com>
2026-05-06 18:56:31 +05:30
d43a87c9dc feat: Add labels to contact import/export (#13313)
Adds label support to contact import and export so teams can carry
approved contact labels through CSV workflows. Imports accept a `labels`
column with labels that already exist in the account; multiple labels
should be entered as a quoted comma-separated CSV value, for example
`"customer,vip"`.

Imports are additive: they add labels to contacts and do not remove
labels already on a contact. Removing a label from the CSV row or
leaving the `labels` cell blank will not clear existing contact labels.
To remove a label, edit the contact directly.

## Closes

- Closes #8535

## How to test

1. Create a few contact labels in the account, such as `customer`,
`vip`, and `lead`.
2. Go to Contacts -> Import contacts and download the sample CSV.
3. Import contacts with a `labels` column. Use a single label like
`lead`, or quote multiple labels like `"customer,vip"`.
4. Confirm imported contacts are created with the expected labels.
5. Re-import an existing contact with a new label and confirm the new
label is added without removing existing labels.
6. Try a row with an unknown label, such as `"vip,unknown_label"`, and
confirm only that row is rejected in the failed records CSV while the
other valid rows are imported.
7. Export contacts and confirm the CSV includes a `labels` column with
comma-separated approved labels.

## What changed

- Contact exports include approved `labels` in the default CSV columns.
This adds a new default export column for CSV consumers.
- Contact imports parse `labels` as comma-separated values inside the
CSV cell.
- Imported labels are validated against labels that already exist in the
account.
- Rows with unknown labels are rejected with an `Unknown labels: ...`
error; valid rows in the same import continue to process.
- Imported labels are additive and do not remove existing contact
labels.
- Label application during import does not dispatch an additional
per-contact update event.
- The sample CSV includes an import-safe `labels` column. The modal
keeps the existing generic CSV import copy.

---------

Co-authored-by: Sojan Jose <sojan@pepalo.com>
2026-05-06 18:46:36 +05:30
Aakash BakhleandGitHub d7d1e4113c fix: captain auto sync scheduler resilience (#14379)
# Pull Request Template

## Description

skip documents that fail with ActiveRecord errors possibly due to
stale/corrupt data and not crash scheduler

How did we find out about this error?

before October 28th, 2025, we did not have url normalisation.
so we had document rows as: 
id: 123 `https://example.com` status: `in_progress` --> likely stuck
crawl
id 234: `https://example.com/` status: `available` 

When the schedule sync job ran, it ran an `document.update!(sync_status:
:syncing, last_sync_attempted_at: Time.current)` on the 234 one since it
was `available`

now `update!` runs `before_validation :normalize_external_link`
so `https://example.com/` became `https://example.com`

which invalidated:
`validates :external_link, uniqueness: { scope: :assistant_id }`

so the scheduler crashed.

This PR logs the skipped ones with their errors and continues to pick
other documents to scheduler doesn't crash

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

## 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-05-06 18:07:22 +05:30
Sivin VargheseandGitHub 815593eec9 feat: display conversation ID conversation view (#14381) 2026-05-06 17:33:23 +05:30
8d7e926e06 fix: [Snyk] Security upgrade video.js from 7.18.1 to 7.21.1 (#13973)
![snyk-top-banner](https://res.cloudinary.com/snyk/image/upload/r-d/scm-platform/snyk-pull-requests/pr-banner-default.svg)

### Snyk has created this PR to fix 1 vulnerabilities in the yarn
dependencies of this project.

#### Snyk changed the following file(s):

- `package.json`


#### Note for
[zero-installs](https://yarnpkg.com/features/zero-installs) users

If you are using the Yarn feature
[zero-installs](https://yarnpkg.com/features/zero-installs) that was
introduced in Yarn V2, note that this PR does not update the
`.yarn/cache/` directory meaning this code cannot be pulled and
immediately developed on as one would expect for a zero-install project
- you will need to run `yarn` to update the contents of the
`./yarn/cache` directory.
If you are not using zero-install you can ignore this as your flow
should likely be unchanged.



<details>
<summary>⚠️ <b>Warning</b></summary>

```
Failed to update the yarn.lock, please update manually before merging.
```

</details>



#### Vulnerabilities that will be fixed with an upgrade:

|  | Issue |  
:-------------------------:|:-------------------------
![high
severity](https://res.cloudinary.com/snyk/image/upload/w_20,h_20/v1561977819/icon/h.png
'high severity') | XML Injection
<br/>[SNYK-JS-XMLDOMXMLDOM-15869636](https://snyk.io/vuln/SNYK-JS-XMLDOMXMLDOM-15869636)




---

> [!IMPORTANT]
>
> - Check the changes in this PR to ensure they won't cause issues with
your project.
> - Max score is 1000. Note that the real score may have changed since
the PR was raised.
> - This PR was automatically created by Snyk using the credentials of a
real user.

---

**Note:** _You are seeing this because you or someone else with access
to this repository has authorized Snyk to open fix PRs._

For more information: <img
src="https://api.segment.io/v1/pixel/track?data=eyJ3cml0ZUtleSI6InJyWmxZcEdHY2RyTHZsb0lYd0dUcVg4WkFRTnNCOUEwIiwiYW5vbnltb3VzSWQiOiJhMzFhMWZiNS1hOWYwLTQ1MTMtOTMxNi1iZTg3OThhYmZkOWMiLCJldmVudCI6IlBSIHZpZXdlZCIsInByb3BlcnRpZXMiOnsicHJJZCI6ImEzMWExZmI1LWE5ZjAtNDUxMy05MzE2LWJlODc5OGFiZmQ5YyJ9fQ=="
width="0" height="0"/>
🧐 [View latest project
report](https://app.snyk.io/org/chatwoot/project/3ca3819e-26b5-4e23-ac65-184abd9a6f10?utm_source&#x3D;github&amp;utm_medium&#x3D;referral&amp;page&#x3D;fix-pr)
📜 [Customise PR
templates](https://docs.snyk.io/scan-using-snyk/pull-requests/snyk-fix-pull-or-merge-requests/customize-pr-templates?utm_source=github&utm_content=fix-pr-template)
🛠 [Adjust project
settings](https://app.snyk.io/org/chatwoot/project/3ca3819e-26b5-4e23-ac65-184abd9a6f10?utm_source&#x3D;github&amp;utm_medium&#x3D;referral&amp;page&#x3D;fix-pr/settings)
📚 [Read about Snyk's upgrade
logic](https://docs.snyk.io/scan-with-snyk/snyk-open-source/manage-vulnerabilities/upgrade-package-versions-to-fix-vulnerabilities?utm_source=github&utm_content=fix-pr-template)

---

**Learn how to fix vulnerabilities with free interactive lessons:**

🦉 [XML Injection](https://learn.snyk.io/lesson/xxe/?loc&#x3D;fix-pr)

[//]: #
'snyk:metadata:{"breakingChangeRiskLevel":null,"FF_showPullRequestBreakingChanges":false,"FF_showPullRequestBreakingChangesWebSearch":false,"customTemplate":{"variablesUsed":[],"fieldsUsed":[]},"dependencies":[{"name":"video.js","from":"7.18.1","to":"7.21.1"}],"env":"prod","issuesToFix":["SNYK-JS-XMLDOMXMLDOM-15869636","SNYK-JS-XMLDOMXMLDOM-15869636"],"prId":"a31a1fb5-a9f0-4513-9316-be8798abfd9c","prPublicId":"a31a1fb5-a9f0-4513-9316-be8798abfd9c","packageManager":"yarn","priorityScoreList":[null],"projectPublicId":"3ca3819e-26b5-4e23-ac65-184abd9a6f10","projectUrl":"https://app.snyk.io/org/chatwoot/project/3ca3819e-26b5-4e23-ac65-184abd9a6f10?utm_source=github&utm_medium=referral&page=fix-pr","prType":"fix","templateFieldSources":{"branchName":"default","commitMessage":"default","description":"default","title":"default"},"templateVariants":["updated-fix-title","pr-warning-shown"],"type":"auto","upgrade":["SNYK-JS-XMLDOMXMLDOM-15869636"],"vulns":["SNYK-JS-XMLDOMXMLDOM-15869636"],"patch":[],"isBreakingChange":false,"remediationStrategy":"vuln"}'

---------

Co-authored-by: snyk-bot <snyk-bot@snyk.io>
Co-authored-by: Sony Mathew <2040199+sony-mathew@users.noreply.github.com>
2026-05-06 16:33:16 +05:30
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Sony Mathew
dd52f1d32b chore(deps): bump rack-session from 2.1.1 to 2.1.2 (#14017)
Bumps [rack-session](https://github.com/rack/rack-session) from 2.1.1 to
2.1.2.
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/rack/rack-session/blob/main/releases.md">rack-session's
changelog</a>.</em></p>
<blockquote>
<h2>v2.1.2</h2>
<ul>
<li><a
href="https://github.com/advisories/GHSA-33qg-7wpp-89cq">CVE-2026-39324</a>
Don't fall back to unencrypted coder if encryptors are present.</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/rack/rack-session/commit/504367b59caf7ec78127785cc6351f46be14f8ca"><code>504367b</code></a>
Bump patch version.</li>
<li><a
href="https://github.com/rack/rack-session/commit/f43638cb3a4d15c3ecaf59e67a04b47fda08eeac"><code>f43638c</code></a>
Don't fall back to unencrypted coder if encryptors are present.</li>
<li><a
href="https://github.com/rack/rack-session/commit/dadcfe60f193e8d8540bec6b95ca75bed8e5fd7e"><code>dadcfe6</code></a>
Bump actions/checkout from 4 to 5 (<a
href="https://redirect.github.com/rack/rack-session/issues/54">#54</a>)</li>
<li><a
href="https://github.com/rack/rack-session/commit/4eb9ea83b372e319c65a8c2bcfe87e8be942cf9b"><code>4eb9ea8</code></a>
Add top level session spec to validate existing formats.</li>
<li><a
href="https://github.com/rack/rack-session/commit/8f94577c1d11b746692974f1417acff2856060cb"><code>8f94577</code></a>
Add rails to external tests.</li>
<li><a
href="https://github.com/rack/rack-session/commit/38ea47da9937afb4f2140b3c23866e3791a46eaf"><code>38ea47d</code></a>
Allow the v2 encryptor to serialize messages with <code>Marshal</code>
(<a
href="https://redirect.github.com/rack/rack-session/issues/44">#44</a>)</li>
<li><a
href="https://github.com/rack/rack-session/commit/43f2e3a46393b51473bb90f54e61189465ae759d"><code>43f2e3a</code></a>
Fix compatibility with older Rubies.</li>
<li><a
href="https://github.com/rack/rack-session/commit/6a060b806399bff4961eaf6bf89535395c95549c"><code>6a060b8</code></a>
Support UTF-8 data when using the JSON serializer (<a
href="https://redirect.github.com/rack/rack-session/issues/39">#39</a>)</li>
<li><a
href="https://github.com/rack/rack-session/commit/8ce0146a7079332d9c58a43e418acb1ecf904ef6"><code>8ce0146</code></a>
Fix <code>auth_tag</code> retrieval on JRuby (<a
href="https://redirect.github.com/rack/rack-session/issues/32">#32</a>)</li>
<li><a
href="https://github.com/rack/rack-session/commit/77271850efd977897d02903bfde8ed51e4137a68"><code>7727185</code></a>
Add AEAD encryption (<a
href="https://redirect.github.com/rack/rack-session/issues/23">#23</a>)</li>
<li>See full diff in <a
href="https://github.com/rack/rack-session/compare/v2.1.1...v2.1.2">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=rack-session&package-manager=bundler&previous-version=2.1.1&new-version=2.1.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/chatwoot/chatwoot/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Sony Mathew <sony@chatwoot.com>
2026-05-06 15:37:32 +05:30
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Sony Mathew
deb259c8d2 chore(deps): bump rack from 3.2.5 to 3.2.6 (#13987)
Bumps [rack](https://github.com/rack/rack) from 3.2.5 to 3.2.6.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/rack/rack/releases">rack's
releases</a>.</em></p>
<blockquote>
<h2>v3.2.6</h2>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/rack/rack/compare/v3.2.5...v3.2.6">https://github.com/rack/rack/compare/v3.2.5...v3.2.6</a></p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/rack/rack/blob/main/CHANGELOG.md">rack's
changelog</a>.</em></p>
<blockquote>
<h2>[3.2.6] - 2026-04-01</h2>
<h3>Security</h3>
<ul>
<li><a
href="https://github.com/advisories/GHSA-7mqq-6cf9-v2qp">CVE-2026-34763</a>
Root directory disclosure via unescaped regex interpolation in
<code>Rack::Directory</code>.</li>
<li><a
href="https://github.com/advisories/GHSA-v569-hp3g-36wr">CVE-2026-34230</a>
Avoid O(n^2) algorithm in <code>Rack::Utils.select_best_encoding</code>
which could lead to denial of service.</li>
<li><a
href="https://github.com/advisories/GHSA-qfgr-crr9-7r49">CVE-2026-32762</a>
Forwarded header semicolon injection enables Host and Scheme
spoofing.</li>
<li><a
href="https://github.com/advisories/GHSA-vgpv-f759-9wx3">CVE-2026-26961</a>
Raise error for multipart requests with multiple boundary
parameters.</li>
<li><a
href="https://github.com/advisories/GHSA-q4qf-9j86-f5mh">CVE-2026-34786</a>
<code>Rack::Static</code> <code>header_rules</code> bypass via
URL-encoded path mismatch.</li>
<li><a
href="https://github.com/advisories/GHSA-q2ww-5357-x388">CVE-2026-34831</a>
<code>Content-Length</code> mismatch in <code>Rack::Files</code> error
responses.</li>
<li><a
href="https://github.com/advisories/GHSA-x8cg-fq8g-mxfx">CVE-2026-34826</a>
Multipart byte range processing allows denial of service via excessive
overlapping ranges.</li>
<li><a
href="https://github.com/advisories/GHSA-g2pf-xv49-m2h5">CVE-2026-34835</a>
<code>Rack::Request</code> accepts invalid Host characters, enabling
host allowlist bypass.</li>
<li><a
href="https://github.com/advisories/GHSA-qv7j-4883-hwh7">CVE-2026-34830</a>
<code>Rack::Sendfile</code> header-based <code>X-Accel-Mapping</code>
regex injection enables unauthorized <code>X-Accel-Redirect</code>.</li>
<li><a
href="https://github.com/advisories/GHSA-h2jq-g4cq-5ppq">CVE-2026-34785</a>
<code>Rack::Static</code> prefix matching can expose unintended files
under the static root.</li>
<li><a
href="https://github.com/advisories/GHSA-8vqr-qjwx-82mw">CVE-2026-34829</a>
Multipart parsing without <code>Content-Length</code> header allows
unbounded chunked file uploads.</li>
<li><a
href="https://github.com/advisories/GHSA-v6x5-cg8r-vv6x">CVE-2026-34827</a>
Multipart header parsing allows denial of service via escape-heavy
quoted parameters.</li>
<li><a
href="https://github.com/advisories/GHSA-rx22-g9mx-qrhv">CVE-2026-26962</a>
Improper unfolding of folded multipart headers preserves CRLF in parsed
parameter values.</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/rack/rack/commit/e1f22fdbe99afd2126b6fbf05bb12399359574b7"><code>e1f22fd</code></a>
Bump patch version.</li>
<li><a
href="https://github.com/rack/rack/commit/31989fd7bb6f806fdb3cfa4e9aec1fe8434f47d1"><code>31989fd</code></a>
Fix typo in test.</li>
<li><a
href="https://github.com/rack/rack/commit/d268165e390e17b83573fec916dcdef6304a8b4b"><code>d268165</code></a>
Fix test expectation.</li>
<li><a
href="https://github.com/rack/rack/commit/8f425de0ee75a2f3cdfbfdd57858c1910b7645ff"><code>8f425de</code></a>
Add Ruby v4.0 to the test matrix.</li>
<li><a
href="https://github.com/rack/rack/commit/bf830426ce5b3daccb5a226b733703c86504ceba"><code>bf83042</code></a>
Drop EOL Rubies from external tests.</li>
<li><a
href="https://github.com/rack/rack/commit/d50c4d3dab62fa80b2a276271d0d4fb338cfa7df"><code>d50c4d3</code></a>
Implement OBS unfolding for multipart requests per RFC 5322 2.2.3</li>
<li><a
href="https://github.com/rack/rack/commit/bfb69142dbe2a1e3298ad52d12935938d1b58205"><code>bfb6914</code></a>
Limit the number of quoted escapes during multipart parsing</li>
<li><a
href="https://github.com/rack/rack/commit/b3e5945c648c5a5b6982e5072b26e51990991229"><code>b3e5945</code></a>
Add Content-Length size check in Rack::Multipart::Parser</li>
<li><a
href="https://github.com/rack/rack/commit/7a8f32696609b88e2c4c1f09d473a1d2d837ed4b"><code>7a8f326</code></a>
Fix root prefix bug in Rack::Static</li>
<li><a
href="https://github.com/rack/rack/commit/a57bc140247f904dc1e3302badedcb73645072c7"><code>a57bc14</code></a>
Only do a simple substitution on the x-accel-mapping paths</li>
<li>Additional commits viewable in <a
href="https://github.com/rack/rack/compare/v3.2.5...v3.2.6">compare
view</a></li>
</ul>
</details>
<br />

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Sony Mathew <sony@chatwoot.com>
2026-05-06 15:33:40 +05:30
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Sivin Varghese
9c8cfc40b6 chore(deps): bump dompurify from 3.3.2 to 3.4.0 (#14074)
Bumps [dompurify](https://github.com/cure53/DOMPurify) from 3.3.2 to
3.4.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/cure53/DOMPurify/releases">dompurify's
releases</a>.</em></p>
<blockquote>
<h2>DOMPurify 3.4.0</h2>
<p><strong>Most relevant changes:</strong></p>
<ul>
<li>Fixed a problem with <code>FORBID_TAGS</code> not winning over
<code>ADD_TAGS</code>, thanks <a
href="https://github.com/kodareef5"><code>@​kodareef5</code></a></li>
<li>Fixed several minor problems and typos regarding MathML attributes,
thanks <a
href="https://github.com/DavidOliver"><code>@​DavidOliver</code></a></li>
<li>Fixed <code>ADD_ATTR</code>/<code>ADD_TAGS</code> function leaking
into subsequent array-based calls, thanks <a
href="https://github.com/1Jesper1"><code>@​1Jesper1</code></a></li>
<li>Fixed a missing <code>SAFE_FOR_TEMPLATES</code> scrub in
<code>RETURN_DOM</code> path, thanks <a
href="https://github.com/bencalif"><code>@​bencalif</code></a></li>
<li>Fixed a prototype pollution via
<code>CUSTOM_ELEMENT_HANDLING</code>, thanks <a
href="https://github.com/trace37labs"><code>@​trace37labs</code></a></li>
<li>Fixed an issue with <code>ADD_TAGS</code> function form bypassing
<code>FORBID_TAGS</code>, thanks <a
href="https://github.com/eddieran"><code>@​eddieran</code></a></li>
<li>Fixed an issue with <code>ADD_ATTR</code> predicates skipping URI
validation, thanks <a
href="https://github.com/christos-eth"><code>@​christos-eth</code></a></li>
<li>Fixed an issue with <code>USE_PROFILES</code> prototype pollution,
thanks <a
href="https://github.com/christos-eth"><code>@​christos-eth</code></a></li>
<li>Fixed an issue leading to possible mXSS via Re-Contextualization,
thanks <a
href="https://github.com/researchatfluidattacks"><code>@​researchatfluidattacks</code></a>
and others</li>
<li>Fixed an issue with closing tags leading to possible mXSS, thanks <a
href="https://github.com/frevadiscor"><code>@​frevadiscor</code></a></li>
<li>Fixed a problem with the type dentition patcher after Node version
bump</li>
<li>Fixed freezing BS runs by reducing the tested browsers array</li>
<li>Bumped several dependencies where possible</li>
<li>Added needed files for OpenSSF scorecard checks</li>
</ul>
<p><strong>Published Advisories are here:</strong>
<a
href="https://github.com/cure53/DOMPurify/security/advisories?state=published">https://github.com/cure53/DOMPurify/security/advisories?state=published</a></p>
<h2>DOMPurify 3.3.3</h2>
<ul>
<li>Fixed an engine requirement for Node 20 which caused hiccups, thanks
<a href="https://github.com/Rotzbua"><code>@​Rotzbua</code></a></li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/cure53/DOMPurify/commit/5b16e0b892e82b1779d62b9928b43c4c4ff290b9"><code>5b16e0b</code></a>
Getting 3.x branch ready for 3.4.0 release (<a
href="https://redirect.github.com/cure53/DOMPurify/issues/1250">#1250</a>)</li>
<li><a
href="https://github.com/cure53/DOMPurify/commit/8bcbf73ae7eb56e7b4f1300b66cf543342c7ee27"><code>8bcbf73</code></a>
chore: Preparing 3.3.3 release</li>
<li><a
href="https://github.com/cure53/DOMPurify/commit/5faddd60af7b4d612f32a0c6b44432b77c8c490c"><code>5faddd6</code></a>
fix: engine requirement (<a
href="https://redirect.github.com/cure53/DOMPurify/issues/1210">#1210</a>)</li>
<li><a
href="https://github.com/cure53/DOMPurify/commit/0f91e3add5c028bc4110c513b0c2571b284c35af"><code>0f91e3a</code></a>
Update README.md</li>
<li><a
href="https://github.com/cure53/DOMPurify/commit/d5ff1a8c605df1df998c2e7df2c4c8ac762b0dea"><code>d5ff1a8</code></a>
Merge branch 'main' of github.com:cure53/DOMPurify</li>
<li><a
href="https://github.com/cure53/DOMPurify/commit/c3efd489010366e755de9d65fd741888fd8b7462"><code>c3efd48</code></a>
fix: moved back from jsdom 28 to jsdom 20</li>
<li><a
href="https://github.com/cure53/DOMPurify/commit/988b888108c8df911ef37e68d0e26c85ad90e885"><code>988b888</code></a>
fix: moved back from jsdom 28 to jsdom 20</li>
<li><a
href="https://github.com/cure53/DOMPurify/commit/2726c74e9c6a0645127d1630e5ca49f64bc9fe67"><code>2726c74</code></a>
chore: Preparing 3.3.2 release</li>
<li><a
href="https://github.com/cure53/DOMPurify/commit/6202c7e43e9df01ba606396aed60fbae5583f7a1"><code>6202c7e</code></a>
build(deps): bump <code>@​tootallnate/once</code> and jsdom (<a
href="https://redirect.github.com/cure53/DOMPurify/issues/1204">#1204</a>)</li>
<li><a
href="https://github.com/cure53/DOMPurify/commit/302b51de22535cc90235472c52e3401bedd46f80"><code>302b51d</code></a>
fix: Expanded the regex ever so slightly to also cover script</li>
<li>Additional commits viewable in <a
href="https://github.com/cure53/DOMPurify/compare/3.3.2...3.4.0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=dompurify&package-manager=npm_and_yarn&previous-version=3.3.2&new-version=3.4.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/chatwoot/chatwoot/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
2026-05-06 15:14:54 +05:30
Sivin VargheseandGitHub 8f532f45ff feat: add attachments section to conversation sidebar (#14371) 2026-05-06 15:13:51 +05:30
Sojan JoseandGitHub 00ba468486 fix(macros): disable public visibility for agents (#14349) 2026-05-06 15:10:11 +05:30
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>
2026-05-06 11:21:15 +04:00
cc5974da9b feat(inbox): Add beta badge for TikTok and Voice channels (#14378)
TikTok and Voice channels in the inbox creation flow now display a small
"Beta" badge next to their title, signaling that these integrations are
still being polished while keeping them available for users to try.
Fixes
https://linear.app/chatwoot/issue/CW-7026/add-beta-label-for-tiktok-and-voice-inboxes

---------

Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 09:54:00 +04:00
2192af80f4 fix: html-escape captured values in helpcenter article markdown embeds (#14140)
Embed templates interpolate regex captures from user-authored article
URLs into HTML attribute values. CommonMark's angle-bracket link
destination syntax allows characters that the capture regexes don't
filter, so the unescaped substitution could produce malformed attribute
output. Escaping at substitution time keeps the render deterministic
regardless of the URL.

### How was this tested?
Added specs.

Fixes [CW-6934](https://linear.app/chatwoot/issue/CW-6934/)

Co-authored-by: Sony Mathew <sony@chatwoot.com>
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
2026-05-05 17:46:21 +05:30
Vishnu NarayananandGitHub 941c8a86b4 fix: use a dedicated PAT for ghsa linear sync gh action (#14364)
The default `GITHUB_TOKEN` cannot read `security-advisories`; that endpoint requires the `repository_advisories` permission, which is not available to the GitHub Actions installation token.

Switched to a fine-grained PAT stored in `GHSA_READ_TOKEN`.

Tested locally: the same PAT returns the full triage list

Changes
----
- Switch to custom token
- Add a discord alert for new advisories
- Switch to python
2026-05-05 17:20:22 +05:30
a9ac1c633d fix: added HMAC validation for Whatsapp and Instagram webhooks (#14280)
## Description
* Added Meta webhook HMAC validation in meta_token_verify_concern.rb.
* Wired it into instagram_controller.rb and whatsapp_controller.rb.
* WhatsApp now verifies X-Hub-Signature-256 with WHATSAPP_APP_SECRET.
* Instagram now verifies with either FB_APP_SECRET or
INSTAGRAM_APP_SECRET.
* Updated request specs so missing/invalid signatures return 401 and
valid signatures still enqueue jobs.


Fixes # (issue):
[CW-6786](https://linear.app/chatwoot/issue/CW-6786/ghsa-7rw7-pc8v-mrr3-unauthenticated-message-injection-via-missing)

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

* Updated the controller specs and ran them successfully.
* The original issue is no longer reproducible.


## 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: Muhsin Keloth <muhsinkeramam@gmail.com>
2026-05-05 15:01:11 +05:30
Aakash BakhleandGitHub 70f799ab35 fix(captain): add v1 handoff classifier [AI-137] (#14337)
# Pull Request Template

## Description

Captain (v1) makes false promises by saying it will handoff but doesn't.
This happens due to an exact string match comparison and the prompt
gives the model a lot of responsibilities:
- identity
- what to respond
- obey custom instructions
- decide on tool calls

This PR decouples responsibility, the core prompt responds, and an
additional llm call evaluates if handoff was needed or not after that
message.

## 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-05-05 14:34:31 +05:30
Sivin VargheseandGitHub 8cc36e1938 feat: inline url embeds in article editor (#14284) 2026-05-05 14:16:24 +05:30
c1d167bd64 fix: prevent -- signature delimiter rendering as \ in bubble (#14134)
# Pull Request Template

## Description

Fixes
https://linear.app/chatwoot/issue/CW-6903/signature-delimiter-renders-as-h2-when-using-enter-line-before

**1**. Fixes an issue where the signature delimiter `--` gets parsed as
an H2 when using **Enter** (new paragraph) before or after it, causing
it to render as a bold `\` in the message bubble.

* Ensures `--` renders as plain text
* Aligns renderer with parser behavior (both disable `lheading`)
* Prevents stray `\` from appearing as heading text

**2**. Also fixes a related editor issue where toggling signature
**off** leaves behind a stray `\` or `-- \`.

* Strips blank paragraph markers (`\`) and dangling hard breaks
(`\<newline>`) from ProseMirror serializer
* Applied in both `appendSignature` and `removeSignature`
* Replaces `trimEnd()` with shared helpers (`trimTrailingBlanks` /
`stripTrailingBlankMarkers`)



## Type of change

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

## How Has This Been Tested?

#### Screenshots

**Before**
<img width="194" height="204" alt="image"
src="https://github.com/user-attachments/assets/b286ab50-7f89-4910-a552-1568902b93b3"
/>

**After**
<img width="194" height="220" alt="image"
src="https://github.com/user-attachments/assets/658cd543-bce2-46e2-a319-35e5374f1aef"
/>

**Editor**

https://linear.app/chatwoot/issue/CW-6903/signature-delimiter-renders-as-in-h2-when-using-enter-line#comment-5814b882

### Steps

#### Editor

1. Enable agent signature
2. Add and remove new lines around the signature using Enter/shift enter
3. Toggle signature off
4. Notice stray `\` or `-- \` remains

#### Bubble

1. Enable agent signature
2. Send a message using Enter between lines
3. Verify `--` renders correctly (no H2, no bold `\`)

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

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2026-05-05 13:00:27 +05:30
Sivin VargheseandGitHub 6386eec5e7 fix: regex validation not applied for custom text attributes in UI (#14110)
# Pull Request Template

## Description

This PR fixes multiple issues related to regex patterns and validation
for custom attributes.
1. Fixed regex patterns being double-escaped when saving from Add and
Edit flows
2. Fixed regex validation not being enforced in the widget pre-chat form
3. Minor UI improvements in the Add/Edit custom attribute dialog


Fixes
[CW-6625](https://linear.app/chatwoot/issue/CW-6625/bug-report-custom-attribute-regex-validation-not-working-in-ui),
https://github.com/chatwoot/chatwoot/issues/13771

## 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/14f1983a8bc84f9fabc3663afd83cd50

**After**
https://www.loom.com/share/867c0484741140c1944fcbd43914c9c0

## 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
- [ ] Any dependent changes have been merged and published in downstream
modules
2026-05-05 12:55:53 +05:30
Tanmay Deep SharmaandGitHub 21c0f4dc52 fix(transcription): guard Whisper 25MB limit and zero temperature for stable output (#14335)
Two production-grade fixes to the existing audio transcription service.
**Independent of the WhatsApp Calling work** — these affect every audio
attachment that goes through Whisper (voice notes, call recordings,
voicemails, etc.).

## Closes
- [PLA-151 — PR-5: Recording Upload + Transcription
Pipeline](https://linear.app/chatwoot/issue/PLA-151/pr-5-recording-upload-transcription-pipeline)

## Why this is needed

### 1. Whisper rejects payloads larger than 25 MB

OpenAI's [Whisper
API](https://platform.openai.com/docs/guides/speech-to-text) hard-caps
file uploads at 25 MB. Long audio recordings — voice notes from chatty
contacts, ~70+ min Opus call recordings — currently hit OpenAI with the
full payload and 413 (\`Payload Too Large\`). The job retries via the
existing \`Faraday::BadRequestError\` discard path, but the agent still
sees a transcription failure for an attachment we knew was too big up
front.

This PR adds a pre-flight \`audio_too_large?\` check via the blob's
\`byte_size\` and returns a controlled error without hitting OpenAI. The
audio attachment is preserved (agents can still listen), only the
transcription is skipped.

### 2. Whisper hallucinates on silence at non-zero temperature

At \`temperature: 0.4\` (the previous value), Whisper produces
well-documented hallucinated repeats on silence and near-silent segments
— e.g. \`Oh, dear. Oh, dear. Oh, dear.\` filling the transcript. This
shows up in real recordings whenever there's a hold or quiet moment.
\`temperature: 0.0\` matches OpenAI's recommended default for
transcription and eliminates the spirals.

Reference:
[openai/whisper#928](https://github.com/openai/whisper/discussions/928),
[openai-python#1010](https://github.com/openai/openai-python/issues/1010).

## Are WhatsApp call recordings already handled?

Yes — by the existing pipeline, **before this PR**:

\`\`\`
Browser MediaRecorder → upload_recording (PR-4)
  → @call.message.attachments.create!(file_type: :audio, ...)
→ Enterprise::Concerns::Attachment#enqueue_audio_transcription
(after_create_commit hook)
      → Messages::AudioTranscriptionJob.perform_later(attachment.id)
        → Messages::AudioTranscriptionService → Whisper
\`\`\`

The \`after_create_commit\` hook already fires for every audio
attachment regardless of source. PR-4's \`upload_recording\` endpoint
creates the attachment; the existing job/service take it from there. No
new wiring needed.

This PR just makes the existing service more robust:
- Calls longer than ~70 min (Opus 48 kbps) no longer 413 against OpenAI
- Quiet recordings no longer produce hallucinated transcripts

## How to test

\`\`\`ruby
# In rails console with a real audio attachment:
service = Messages::AudioTranscriptionService.new(Attachment.audio.last)

# Normal-sized audio: unchanged behaviour
service.perform # => { success: true, transcriptions: ... }

# Large audio: new guard returns error instead of 413-ing OpenAI
allow(attachment.file.blob).to
receive(:byte_size).and_return(30.megabytes)
service.perform # => { error: 'Audio too large for Whisper' }
\`\`\`

Existing transcription specs cover the happy path; one new spec
exercises the byte-limit guard.

## Risk

Low. Both changes are pre-flight guards or parameter values — they
reduce the surface of OpenAI calls that can fail. Failure to transcribe
is already non-fatal (the audio attachment is preserved either way).
2026-05-05 11:49:28 +07:00
Vishnu NarayananandGitHub 624c6c90fd chore: sync GitHub security advisories to Linear (#14359)
Sync new GHSA reports to Linear.

Fixes https://linear.app/chatwoot/issue/CW-7006
2026-05-04 23:37:14 +05:30
0bd0cab868 feat(voice): Attach call recordings + show call duration on the bubble (#14344)
When an inbound voice call ends, the conversation bubble now (1) renders
an inline audio player as soon as Twilio finishes the recording and (2)
shows the call duration alongside "Call ended" so the agent gets the
at-a-glance summary without opening the recording.

Fixes
https://linear.app/chatwoot/issue/PLA-118/feat-recordings-on-calls-should-be-attached-on-the-conversation
and
https://linear.app/chatwoot/issue/PLA-119/duration-of-the-call-is-not-visible-on-the-chat-bubble

## How to test

1. Set up a Twilio voice inbox and trigger an inbound call.
2. Answer the call from an agent, talk for a few seconds, then hang up.
3. As soon as the call ends, the bubble should read **"Call ended —
0:NN"** (where NN is the call duration in seconds).
4. Wait a few seconds for Twilio to finish processing the recording
(usually <30s after hangup).
5. The same bubble should now show an inline audio player below the
duration. Press play; the recording should be audible.
6. Refresh the page — both the duration and the player should still be
there.
7. End a second call on the same conversation — its bubble should get
its own duration + player, independent of the first.

---------

Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
2026-05-04 17:14:01 +04:00
Vishnu NarayananandGitHub 2dee7457cd fix: set minimal top-level permissions on workflows (#14358)
- Fix CodeQL alerts by declaring read-only GITHUB_TOKEN scope at the
workflow level. The codespace image publish workflow additionally needs
packages: write to push to ghcr.io.
2026-05-04 17:56:25 +05:30
ea87610999 feat(voice): Join active call from the conversation bubble (#14343)
Agents can now click **Join call** directly on the incoming call bubble
in the conversation timeline. If they refresh the page or miss the
floating widget while a call is still ringing, the bubble becomes the
recovery affordance — one click joins the conference, no need to wait
for the next event.

The button only appears when the call is still ringing, no other agent
has claimed it, and the conversation is unassigned or assigned to the
current agent (mirroring the floating widget's eligibility rules). It
disappears as soon as anyone joins the call or it ends.

Fixes
https://linear.app/chatwoot/issue/PLA-117/ability-to-join-the-call-by-clicking-on-call-bubble-in-a-conversation

## How to test

1. Set up a Twilio voice inbox and trigger an inbound call to it.
2. As an agent who is eligible to answer (unassigned conversation, or
assigned to you), open the conversation **without answering from the
floating widget**. The bubble should show a teal **Join call** link
under "Not answered yet".
3. Refresh the page mid-ring — the link should still be there.
4. Click **Join call** — you should be connected to the conference, the
bubble should flip to "Call in progress / You answered", and the link
should disappear.
5. As a second agent who is **not** eligible (conversation assigned to
someone else), open the same conversation — the link should not appear.
6. Wait for the call to end — the bubble should show "Call ended" with
no Join link.

---------

Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
2026-05-04 12:24:31 +04:00
Aakash BakhleandGitHub d00867d636 fix: captain auto sync scheduler config (#14336) 2026-05-04 13:37:25 +05:30
PranavandGitHub 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
2026-05-04 13:26:28 +05:30
Sivin VargheseandGitHub 2a30e7b082 fix: render agent variables in automation messages (#14338)
# Pull Request Template

## Description

This PR fixes an issue where agent variables like
`{{agent.name}}`,`{{agent.first_name}}`, `{{agent.last_name}}`, and
`{{agent.email}}` were not rendering in automation messages.
In automation, these either showed blank or returned `Liquid error:
internal`, while the same variables worked fine in macros.

**Cause**
Automation messages are created without a sender, so agent data was
missing during variable rendering. This also caused errors in name
handling, and `email` was not defined at all.

**Solution**
* Handle missing agent data safely to avoid errors
* Add support for `{{agent.email}}`
* Fallback to conversation assignee when sender is not present


Fixes
https://linear.app/chatwoot/issue/CW-6979/template-variables-not-working-in-automated-messages

## Type of change

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

## How Has This Been Tested?

### Screenshots

**Automation**
<img width="759" height="284" alt="image"
src="https://github.com/user-attachments/assets/61a877b7-4984-4a7f-bbef-b8c510dcbdfe"
/>

**Before**
<img width="404" height="105" alt="image"
src="https://github.com/user-attachments/assets/da665ce8-137d-4249-8ee5-a1acc11391db"
/>


**After**
<img width="564" height="132" alt="image"
src="https://github.com/user-attachments/assets/6a80d67c-49c8-4658-b782-ae4acbc77256"
/>



## 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-05-04 13:25:40 +05:30
Tanmay Deep SharmaandGitHub 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.
2026-05-04 12:44:19 +07:00
64790ea204 fix: Redirect to the conversation URL if custom_view is not available (#14340)
When an agent shares a conversation link copied from a custom view (e.g.
/custom_view/{id}/conversations/{id}), the link previously broke for
recipients who didn't have access to that custom view. The conversation
now loads regardless — if the custom view isn't available to the
recipient, they're redirected to the direct conversation URL.

### How to reproduce

1. As Agent A, open a conversation from inside a personal custom view
and copy the URL from the address bar.
2. Share the URL with Agent B who does not have access to that custom
view.
3. Before this fix, the link failed to load the conversation. After this
fix, Agent B lands on the conversation via the direct URL.

### What changed

- Added a beforeEnter guard on the conversations_through_folders route.
It checks the user's available conversation custom views (fetching them
on demand for deep links), and if the foldersId in the URL isn't among
them, redirects to the inbox_conversation route with the same
conversation_id.

---------

Co-authored-by: iamsivin <iamsivin@gmail.com>
2026-05-01 10:53:01 -07:00
517b67dfd6 feat: Add rich template preview for WhatsApp & Twilio Templates (#13206)
This PR introduces a comprehensive, display-only template preview system
for both WhatsApp and Twilio templates rendering of all supported
template types.

This lays the foundation for rich template previews across:
- Template selection modals
- Campaign creation flows
- Message composer
- Message screen.

<img width="1818" height="2058" alt="template-preview"
src="https://github.com/user-attachments/assets/9833b28c-f824-4568-8f74-6da09ac61f62"
/>

---------

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
2026-04-30 18:54:34 +04:00
353089473e feat(voice): Assignment aware visibility and join conflict for inbound calls (#14333)
### Description
Inbound voice calls now route ownership cleanly: the call widget is
hidden from agents who aren't the conversation assignee, the first agent
to pick up becomes the assignee, and any later join attempt by another
agent is rejected with a clear "<agent> is already handling the call."
alert.

Closes
https://linear.app/chatwoot/issue/PLA-98/inbound-voice-calls-assignment-aware-visibility-auto-assignment-on

### How to test

1. As Agent A and Agent B, open the dashboard for the same voice inbox
in two browsers.
2. Place an inbound call to the inbox with the conversation
**unassigned** — both agents should see the call widget.
3. Have Agent A click **Join**. Agent A's widget transitions to the
active call; Agent B's widget disappears (conversation is now assigned
to Agent A).
4. While the call is in progress, attempt to join from a third agent
(e.g., via the bubble in the conversation timeline) — the join is
rejected with the toast `Agent A is already handling the call.`
5. Resolve the conversation, then place a second call to a conversation
that is already manually assigned to Agent A — only Agent A sees the
widget; nobody else does.
6. Race test: trigger two near-simultaneous join attempts (two agents
click Join within a few hundred ms of each other) — exactly one wins;
the other gets the conflict alert.

---------

Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
2026-04-30 18:38:10 +04:00
1124c1b4c2 feat(voice): Wire Twilio voice flow through unified call model (#14091)
Twilio voice now uses first-class `Call` records as the source of truth
for call state, instead of storing it on
`conversation.additional_attributes` and `conversation.identifier`. Each
call gets its own record, its own `voice_call` bubble matched by
`call_sid`, and its own conference name keyed off `Call.id`. Multiple
calls on the same conversation (for `lock_to_single_conversation`
inboxes) now work correctly, and the conversation card stays in sync
with the real latest message.
Fixes https://linear.app/chatwoot/issue/PLA-121/lock-to-single-thread

---------

Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
2026-04-30 11:25:39 +04:00
TonyandGitHub 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.
2026-04-29 23:27:14 +05:30
80fccbc526 fix: render slack emoji shortcodes as unicode characters (#12928)
This PR fixes an issue where Slack emojis are rendered as text
shortcodes (e.g. 🚀) instead of the actual emoji characters in
Chatwoot messages.

It introduces a new EmojiFormatter class that uses the emoji-data
mapping to convert shortcodes to unicode characters.

---------

Co-authored-by: Sony Mathew <sony@chatwoot.com>
Co-authored-by: Sony Mathew <2040199+sony-mathew@users.noreply.github.com>
2026-04-29 23:19:52 +05:30
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Sony Mathew
bcdb73502e chore(deps): bump addressable from 2.8.7 to 2.9.0 (#14019)
Bumps [addressable](https://github.com/sporkmonger/addressable) from
2.8.7 to 2.9.0.
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/sporkmonger/addressable/blob/main/CHANGELOG.md">addressable's
changelog</a>.</em></p>
<blockquote>
<h2>Addressable 2.9.0 <!-- raw HTML omitted --></h2>
<ul>
<li>fixes ReDoS vulnerability in Addressable::Template#match (fixes
incomplete
remediation in 2.8.10)</li>
</ul>
<h2>Addressable 2.8.10 <!-- raw HTML omitted --></h2>
<ul>
<li>fixes ReDoS vulnerability in Addressable::Template#match</li>
</ul>
<h2>Addressable 2.8.9 <!-- raw HTML omitted --></h2>
<ul>
<li>Reduce gem size by excluding test files (<a
href="https://redirect.github.com/sporkmonger/addressable/issues/569">#569</a>)</li>
<li>No need for bundler as development dependency (<a
href="https://redirect.github.com/sporkmonger/addressable/issues/571">#571</a>,
<a
href="https://github.com/sporkmonger/addressable/commit/5fc1d93">5fc1d93</a>)</li>
<li>idna/pure: stop building the useless <code>COMPOSITION_TABLE</code>
(removes the <code>Addressable::IDNA::COMPOSITION_TABLE</code> constant)
(<a
href="https://redirect.github.com/sporkmonger/addressable/issues/564">#564</a>)</li>
</ul>
<p><a
href="https://redirect.github.com/sporkmonger/addressable/issues/569">#569</a>:
<a
href="https://redirect.github.com/sporkmonger/addressable/pull/569">sporkmonger/addressable#569</a>
<a
href="https://redirect.github.com/sporkmonger/addressable/issues/571">#571</a>:
<a
href="https://redirect.github.com/sporkmonger/addressable/pull/571">sporkmonger/addressable#571</a>
<a
href="https://redirect.github.com/sporkmonger/addressable/issues/564">#564</a>:
<a
href="https://redirect.github.com/sporkmonger/addressable/pull/564">sporkmonger/addressable#564</a></p>
<h2>Addressable 2.8.8 <!-- raw HTML omitted --></h2>
<ul>
<li>Replace the <code>unicode.data</code> blob by a ruby constant (<a
href="https://redirect.github.com/sporkmonger/addressable/issues/561">#561</a>)</li>
<li>Allow <code>public_suffix</code> 7 (<a
href="https://redirect.github.com/sporkmonger/addressable/issues/558">#558</a>)</li>
</ul>
<p><a
href="https://redirect.github.com/sporkmonger/addressable/issues/561">#561</a>:
<a
href="https://redirect.github.com/sporkmonger/addressable/pull/561">sporkmonger/addressable#561</a>
<a
href="https://redirect.github.com/sporkmonger/addressable/issues/558">#558</a>:
<a
href="https://redirect.github.com/sporkmonger/addressable/pull/558">sporkmonger/addressable#558</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/sporkmonger/addressable/commit/0c3e8589b23d4402903a9b4e1fdeba4e43c52ca4"><code>0c3e858</code></a>
Revving version and changelog</li>
<li><a
href="https://github.com/sporkmonger/addressable/commit/91915c1f7aafa3e2c9f42e2f4e21d948c7a861b8"><code>91915c1</code></a>
Fixing additional vulnerable paths</li>
<li><a
href="https://github.com/sporkmonger/addressable/commit/a091e39ff02fc321b21dea3a0df585bef2ba3744"><code>a091e39</code></a>
Add many more adversarial test cases to ensure we don't have any ReDoS
regres...</li>
<li><a
href="https://github.com/sporkmonger/addressable/commit/463a819665a3b85ce5ce894c90bd7bfa3b9d2e15"><code>463a819</code></a>
Regenerate gemspec on newer rubygems</li>
<li><a
href="https://github.com/sporkmonger/addressable/commit/0afcb0b9672bee301e5e96ed850fec05b2fcabb0"><code>0afcb0b</code></a>
Improve from O(n^2) to O(n)</li>
<li><a
href="https://github.com/sporkmonger/addressable/commit/c87f768f22ab00376ed2f8cb106f59c9d0652d3a"><code>c87f768</code></a>
Fix a ReDoS vulnerability in URI template matching</li>
<li><a
href="https://github.com/sporkmonger/addressable/commit/0d7e9b259fb0940d1a85064b04f678a7984409a5"><code>0d7e9b2</code></a>
Fix links for 2.8.9 in CHANGELOG (<a
href="https://redirect.github.com/sporkmonger/addressable/issues/573">#573</a>)</li>
<li><a
href="https://github.com/sporkmonger/addressable/commit/e2091200b31553f19248eb871f071852409796f8"><code>e209120</code></a>
Update version, gemspec, and CHANGELOG for 2.8.9 (<a
href="https://redirect.github.com/sporkmonger/addressable/issues/572">#572</a>)</li>
<li><a
href="https://github.com/sporkmonger/addressable/commit/387587492b6536748ed12a11c3fdb44a48885f28"><code>3875874</code></a>
Reduce gem size by excluding test files (<a
href="https://redirect.github.com/sporkmonger/addressable/issues/569">#569</a>)</li>
<li><a
href="https://github.com/sporkmonger/addressable/commit/3e57cc6018f94231aabb47fd341acd1b40f1e71a"><code>3e57cc6</code></a>
CI: back to <code>windows-2022</code> for MRI job</li>
<li>Additional commits viewable in <a
href="https://github.com/sporkmonger/addressable/compare/addressable-2.8.7...addressable-2.9.0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=addressable&package-manager=bundler&previous-version=2.8.7&new-version=2.9.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/chatwoot/chatwoot/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Sony Mathew <sony@chatwoot.com>
2026-04-29 22:15:48 +05:30
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Sony Mathew
d634ced4e9 chore(deps-dev): bump uuid from 13.0.0 to 14.0.0 in /tests/playwright (#14294)
Bumps [uuid](https://github.com/uuidjs/uuid) from 13.0.0 to 14.0.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/uuidjs/uuid/releases">uuid's
releases</a>.</em></p>
<blockquote>
<h2>v14.0.0</h2>
<h2><a
href="https://github.com/uuidjs/uuid/compare/v13.0.0...v14.0.0">14.0.0</a>
(2026-04-19)</h2>
<h3>⚠ BREAKING CHANGES</h3>
<ul>
<li>expect <code>crypto</code> to be global everywhere (requires
node@20+) (<a
href="https://redirect.github.com/uuidjs/uuid/issues/935">#935</a>)</li>
<li>drop node@18 support (<a
href="https://redirect.github.com/uuidjs/uuid/issues/934">#934</a>)</li>
</ul>
<h3>Features</h3>
<ul>
<li>drop node@18 support (<a
href="https://redirect.github.com/uuidjs/uuid/issues/934">#934</a>) (<a
href="https://github.com/uuidjs/uuid/commit/dc4ddb87272ed2843faccd130bcc41d492688bd3">dc4ddb8</a>)</li>
</ul>
<h3>Bug Fixes</h3>
<ul>
<li>expect <code>crypto</code> to be global everywhere (requires
node@20+) (<a
href="https://redirect.github.com/uuidjs/uuid/issues/935">#935</a>) (<a
href="https://github.com/uuidjs/uuid/commit/f2c235f93059325fa43e1106e624b5291bb523c4">f2c235f</a>)</li>
<li>Use GITHUB_TOKEN for release-please and enable npm provenance (<a
href="https://redirect.github.com/uuidjs/uuid/issues/925">#925</a>) (<a
href="https://github.com/uuidjs/uuid/commit/ffa31383e8e4e1f0b4e22e504561272041b8738c">ffa3138</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/uuidjs/uuid/blob/main/CHANGELOG.md">uuid's
changelog</a>.</em></p>
<blockquote>
<h2><a
href="https://github.com/uuidjs/uuid/compare/v13.0.0...v14.0.0">14.0.0</a>
(2026-04-19)</h2>
<h3>Security</h3>
<ul>
<li>Fixes <a
href="https://github.com/uuidjs/uuid/security/advisories/GHSA-w5hq-g745-h8pq">GHSA-w5hq-g745-h8pq</a>:
<code>v3()</code>, <code>v5()</code>, and <code>v6()</code> did not
validate that writes would remain within the bounds of a caller-supplied
buffer, allowing out-of-bounds writes when an invalid
<code>offset</code> was provided. A <code>RangeError</code> is now
thrown if <code>offset &lt; 0</code> or <code>offset + 16 &gt;
buf.length</code>.</li>
</ul>
<h3>⚠ BREAKING CHANGES</h3>
<ul>
<li><code>crypto</code> is now expected to be globally defined (requires
node@20+) (<a
href="https://redirect.github.com/uuidjs/uuid/issues/935">#935</a>)</li>
<li>drop node@18 support (<a
href="https://redirect.github.com/uuidjs/uuid/issues/934">#934</a>)</li>
<li>upgrade minimum supported TypeScript version to 5.4.3, in keeping
with the project's policy of supporting TypeScript versions released
within the last two years</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/uuidjs/uuid/commit/7c1ea087a8149b57380fc8bb7f68c3a215cb6e4b"><code>7c1ea08</code></a>
chore(main): release 14.0.0 (<a
href="https://redirect.github.com/uuidjs/uuid/issues/926">#926</a>)</li>
<li><a
href="https://github.com/uuidjs/uuid/commit/3d2c5b0342f0fcb52a5ac681c3d47c13e7444b34"><code>3d2c5b0</code></a>
Merge commit from fork</li>
<li><a
href="https://github.com/uuidjs/uuid/commit/f2c235f93059325fa43e1106e624b5291bb523c4"><code>f2c235f</code></a>
fix!: expect <code>crypto</code> to be global everywhere (requires
node@20+) (<a
href="https://redirect.github.com/uuidjs/uuid/issues/935">#935</a>)</li>
<li><a
href="https://github.com/uuidjs/uuid/commit/529ef0899f5dd503d2ee90d690585d63d78bc212"><code>529ef08</code></a>
chore: upgrade TypeScript and fixup types (<a
href="https://redirect.github.com/uuidjs/uuid/issues/927">#927</a>)</li>
<li><a
href="https://github.com/uuidjs/uuid/commit/086fd7976f11433edf9ac80be876b3ad243fe087"><code>086fd79</code></a>
chore: update dependencies (<a
href="https://redirect.github.com/uuidjs/uuid/issues/933">#933</a>)</li>
<li><a
href="https://github.com/uuidjs/uuid/commit/dc4ddb87272ed2843faccd130bcc41d492688bd3"><code>dc4ddb8</code></a>
feat!: drop node@18 support (<a
href="https://redirect.github.com/uuidjs/uuid/issues/934">#934</a>)</li>
<li><a
href="https://github.com/uuidjs/uuid/commit/0f1f9c9c9cedbae5a1d363d5406c5dfbabe81404"><code>0f1f9c9</code></a>
chore: switch to Biome for parsing and linting (<a
href="https://redirect.github.com/uuidjs/uuid/issues/932">#932</a>)</li>
<li><a
href="https://github.com/uuidjs/uuid/commit/e2879e64bf125add903c1eff6e0860542c605013"><code>e2879e6</code></a>
chore: use maintained version of npm-run-all (<a
href="https://redirect.github.com/uuidjs/uuid/issues/930">#930</a>)</li>
<li><a
href="https://github.com/uuidjs/uuid/commit/ffa31383e8e4e1f0b4e22e504561272041b8738c"><code>ffa3138</code></a>
fix: Use GITHUB_TOKEN for release-please and enable npm provenance (<a
href="https://redirect.github.com/uuidjs/uuid/issues/925">#925</a>)</li>
<li><a
href="https://github.com/uuidjs/uuid/commit/0423d49df2dc8efc300c804731d25f4d7e0fccc4"><code>0423d49</code></a>
docs: remove obsolete v1 option notes (<a
href="https://redirect.github.com/uuidjs/uuid/issues/915">#915</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/uuidjs/uuid/compare/v13.0.0...v14.0.0">compare
view</a></li>
</ul>
</details>
<details>
<summary>Maintainer changes</summary>
<p>This version was pushed to npm by <a
href="https://www.npmjs.com/~GitHub%20Actions">GitHub Actions</a>, a new
releaser for uuid since your current version.</p>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=uuid&package-manager=npm_and_yarn&previous-version=13.0.0&new-version=14.0.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/chatwoot/chatwoot/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Sony Mathew <sony@chatwoot.com>
2026-04-29 22:03:50 +05:30
e723c6b6f2 fix: Prevent platform banners cloud check from breaking app boot (#14321)
The `ChatwootApp.chatwoot_cloud?` gate on the platform banners route in
#13943 reads `InstallationConfig` from the database. Because `routes.rb`
is evaluated during `Rails.application.initialize!`, this ran before the
database existed on a fresh setup, breaking `bundle exec rake db:create`
in CI and first-time installs with `ActiveRecord::NoDatabaseError: We
could not find your database: chatwoot_test`.

The route is now always mounted, and the cloud check moved to where the
database is guaranteed to be available — the controller
(`before_action`) and the super admin sidebar partial.

Closes the CI failure introduced by #13943.

## How to test
1. Drop your local databases: `bundle exec rake db:drop`
2. Run `bundle exec rake db:create` — it should succeed (previously
failed with `NoDatabaseError`)
3. Bring the DB back: `bundle exec rake db:setup`
4. On a non-cloud install, visit `/super_admin/platform_banners` —
should 404, and the sidebar entry should be hidden
5. With `DEPLOYMENT_ENV=cloud` configured (cloud install), the page and
sidebar entry should work as before

## What changed
- `config/routes.rb` — always mount `resources :platform_banners` (no DB
call at boot)
- `app/controllers/super_admin/platform_banners_controller.rb` —
`before_action` raises `ActionController::RoutingError` (404) when not
on Chatwoot Cloud
- `app/views/super_admin/application/_navigation.html.erb` — hides the
sidebar entry on non-cloud installs

---------

Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
2026-04-29 19:30:06 +04:00
5325e05143 feat: Add platform-wide status banners for outage notifications (#13943)
Adds a platform-wide status banner system to notify all users about
external service outages. Super Admins can create, edit, and manage
banners via the Super Admin console. Banners support markdown for links
and are dismissible by users.


<img width="1099" height="236" alt="image"
src="https://github.com/user-attachments/assets/047a7994-d885-4a8a-b9c4-aeb32f15474a"
/>




## How to test
1. Set `ENABLE_PLATFORM_BANNERS=true` in your environment
2. Go to Super Admin → Platform Banners
3. Create a banner with a message like: `Elevated error rates from Meta
APIs. [Check status](https://metastatus.com)`
4. Select a banner type: `info` (blue), `warning` (amber), or `error`
(red)
5. Visit the dashboard — the banner should appear at the top
6. Click "Dismiss" — the banner hides and stays dismissed across page
reloads
7. Deactivate the banner in Super Admin — it disappears on next page
load

## What changed
- New `PlatformBanner` model with `banner_message`, `banner_type`
(info/warning/error), and `active` flag
- Super Admin CRUD via Administrate (controller, dashboard, routes,
sidebar icon)
- `DashboardController` serves active banners via `globalConfig`
- `StatusBanner.vue` component renders banners with markdown support and
per-banner localStorage dismiss
- Feature gated behind `ENABLE_PLATFORM_BANNERS` env var

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
2026-04-29 17:18:38 +04:00
Aakash BakhleandGitHub c09206c22e fix: flaky time based document sync spec (#14320)
fixes:
https://app.circleci.com/pipelines/github/chatwoot/chatwoot/111572/workflows/210e3618-301a-4a8f-a411-826214965441/jobs/146263
2026-04-29 17:36:14 +05:30
Aakash BakhleandGitHub 568aae875b feat: wire up auto-sync job backend [AI-150] (#14117)
# Pull Request Template

## Description

- Wires up Controllers to auto-sync job
- adds plan based sync schedule
- a scheduler that runs every hour to check syncable documents
- guards the whole feature behind feature flag by reclaiming
`twilio_content_templates`
- Adds a global and account level cap on how many documents to enqueue
to prevent sudden burst at first run
- some refactor to simplify code
- specs

Fixes # (issue)

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

specs and 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-04-29 14:47:14 +05:30
Sivin VargheseandGitHub 2324a344dc fix: keep agents action visible in teams edit/add view (#14304) 2026-04-29 14:42:37 +05:30
Sivin VargheseandGitHub 7c7d67fd06 fix: show all matches when filtering by multiple labels (#14303) 2026-04-29 14:15:37 +05:30
Sivin VargheseandGitHub b058d84034 fix: prevent Escape from opening formatting toolbar in editor (#14133) 2026-04-29 14:15:08 +05:30
0e122188e9 feat: Add voice calling as a capability on Twilio SMS channel(Enterprise) (#13963)
Voice calling is now a capability on the existing TwilioSms rather than
a separate Voice model. A single Twilio phone number handles both SMS
and voice calls through one inbox.
Fixes
https://linear.app/chatwoot/issue/CW-6683/add-voice-calling-as-a-capability-on-twilio-sms-channel
and https://linear.app/chatwoot/issue/PLA-120/add-the-support-for-sms

**What changed**
- Replaced Channel::Voice with voice_enabled flag on Channel::TwilioSms
- Added voice_enabled, twiml_app_sid, api_key_secret columns to
channel_twilio_sms table
  - Dropped channel_voice table (no production data)
- All voice logic lives in Enterprise layer via
prepend_mod_with('Channel::TwilioSms')
- Added Voice settings tab on Twilio SMS inbox settings to
enable/disable voice
  - Validates Twilio number voice capability before provisioning
- Teardown service cleans up TwiML app and credentials when voice is
disabled
- Frontend voice detection uses isVoiceCallEnabled() /
getVoiceCallProvider() helpers — extensible to future providers
  - Gated by channel_voice feature flag

**How to test**

1. Enable feature flag:
Account.find(<id>).enable_features('channel_voice')
2. Create voice inbox: Inboxes → Voice tile → enter Twilio credentials →
verify incoming/outgoing calls and SMS work
3. Enable voice on existing SMS inbox: Inboxes → select Twilio SMS inbox
→ Voice tab → toggle on → provide API key credentials → verify calls
work
4. Disable voice: Voice tab → toggle off → verify TwiML app is deleted,
credentials cleared, SMS still works
5. Re-enable voice: Toggle on again → must provide api_key_secret again
→ new TwiML app provisioned

---------

Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
2026-04-29 11:32:19 +04:00
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>
2026-04-28 21:57:30 +04:00
6aeda0ddf6 feat: add Playwright setup and login flow test (#13578)
## Description

Adds Playwright E2E testing infrastructure with project configuration
and a login flow test. This is
Phase 1 of the Playwright E2E suite, kept minimal with only the core
setup and login component.

  Ref: Discussion #13500, PR #13067

## Type of change

Please delete options that are not relevant.

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


## How Has This Been Tested?

- Verified all imports resolve correctly
(`login-flow-ui-validation.spec.ts` only imports `Login` from
  `@components/ui`)
  - Ran login flow test locally against a running Chatwoot instance


## Checklist:

  - [x] My code follows the style guidelines of this project
  - [x] I have performed a self-review of my code
  - [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

---------

Co-authored-by: Sony Mathew <sony@chatwoot.com>
Co-authored-by: Sony Mathew <2040199+sony-mathew@users.noreply.github.com>
2026-04-28 18:21:05 +05:30
fed32d5964 fix(perf): force better index for pending_reponses longest first sort (#14291)
## Description

Fix a Postgres planner trap on the "Pending Response: Longest first"
sort that causes the conversation list to hang on busy accounts.

The current `sort_on_waiting_since` generates query with `ORDER BY
waiting_since ASC NULLS LAST, created_at ASC`. That order-by is exactly
the shape of the single-column `index_conversations_on_waiting_since`
btree, so the planner picks a forward index walk thinking `LIMIT 25`
will stop early. In practice the per-account matches are spread along
the global waiting_since timeline, so the scan reads tens of millions of
rows from other accounts and discards them via the filter before
producing any results which in turn causes the requests to time out and
the conversation list spinner never resolves.

DESC direction and every other sort (`priority`, `created_at`,
`last_activity_at`) are unaffected. They fall through to
`conv_acid_inbid_stat_asgnid_idx` (account-scoped composite), which is
the right index for this access pattern.

This change leads the ORDER BY with the expression `(waiting_since IS
NULL)`, which no column-only btree can satisfy. The planner falls back
to the same account-scoped index used by every other sort, and sorts in
memory. Same logical NULLS LAST output for both directions; no behavior
change for users.

Fixes CW-6965

## Type of change

- [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?

- [x] Existing specs pass
- [x] Added new specs to cover NULL case
- [x] Verify results and order for old and new query in prod
- [x] Tested in prod since staging data was not sufficient

`EXPLAIN (ANALYZE, BUFFERS)` for the same query (status=0, ASC, LIMIT
25) on a representative production account, before vs after:

| Metric | Before | After |
| --- | --- | --- |
| Execution time | 34,679 ms | 0.71 ms |
| Rows discarded by filter | 36,906,962 | 0 |
| Shared buffer hits | 12,699,924 | 11 |
| Blocks read from disk | 851,226 | 112 |
| I/O read time | 17,785 ms | 0.3 ms |
| Pages dirtied / written | 98 / 31,043 | 0 / 0 |

Verified on two production accounts: identical row IDs in identical
order between the old and new ORDER BY for both ASC and DESC. A
NULL-bucket regression spec was added covering ASC/DESC tail ordering
when some conversations have a null `waiting_since`.

Roughly `49,000×` faster on this query (34,679 ms → 0.71 ms), and
trivially less I/O and buffer pressure on the cluster while it runs.

## 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: Tanmay Deep Sharma <32020192+tds-1@users.noreply.github.com>
2026-04-28 18:50:29 +07:00
735bc73c96 fix: Preserve single newlines in outgoing email messages (#14138)
# Pull Request Template

## Description

This PR fixes an issue where outgoing Email messages (via API) do not
preserve single line breaks in rendered HTML.

#### Cause

Messages are stored with `\n`, but rendering differs:

* **Other channel** (`markdown-it`, `breaks: true`) → `\n` → `<br>`
* **Email** (CommonMark) without `HARDBREAKS` → `\n` collapsed into
spaces

Result: multi-line messages appear as a single paragraph in Email.

#### Solution

* Added `hardbreaks:` option to `render_message` (default: false)
* Enabled `hardbreaks: true` in `EmailHelper#render_email_html`

This ensures `\n` renders as `<br />` in Email, matching web widget
behavior.


Fixes
https://linear.app/chatwoot/issue/CW-6941/outgoing-email-messages-strip-single-newlines-from-plain-text-content

## Type of change

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

## How Has This Been Tested?

#### Screenshots

**Before**
<img width="604" height="104" alt="image"
src="https://github.com/user-attachments/assets/f9086ffb-a5c7-4688-99aa-97ea5edcccde"
/>


**After**
<img width="604" height="210" alt="image"
src="https://github.com/user-attachments/assets/a8f21c76-bcb8-4058-937a-dd185fb6745c"
/>



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

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2026-04-28 12:47:03 +04:00
5e79dd699e fix: prevent country defaulting to Zimbabwe in search result contacts (#14098)
## Context

Same root-cause as #7822 / #14078, but in a second file that PR #14078
doesn't touch:

\`app/javascript/dashboard/modules/search/components/SearchResultContactItem.vue\`.

\`countries.json\` entries only have \`id\` (e.g. \`"AF"\`) — there is
no \`code\` field. So:

\`\`\`js
const countriesMap = countries.reduce((acc, country) => {
acc[country.code] = country; // country.code is undefined →
acc[undefined] = country
  acc[country.id] = country;
  return acc;
}, {});
\`\`\`

…overwrites \`acc[undefined]\` on every iteration, leaving whichever
country comes last in the list (Zimbabwe, \`id: "ZW"\`). Later, the
\`countryDetails\` lookup falls back to that value whenever the
contact's \`country\` / \`countryCode\` is missing or unknown, and
Zimbabwe is displayed incorrectly.

## Fix

One-line delete — drop the dead \`acc[country.code] = country\` write.
Lookups by ISO code continue to work via \`country.id\`.

## Scope

Only the search-results card. The Contacts card is already being fixed
in #14078 with the same one-line delete. It's worth patching this
surface too so the same symptom doesn't reappear when the same contact
is accessed via \`Ctrl+K\` search.

## Test plan

- [ ] Reproduce: search for a contact with \`country: null\` /
\`countryCode: null\` — before this patch the flag renders as Zimbabwe;
after, it renders as no country (current expected fallback).
- [ ] Search for a contact with a valid ISO \`countryCode\` (e.g.
\`"IN"\`) — country still resolves correctly.
- [ ] Contacts list page (\`ContactsCard.vue\`, fixed in #14078) — no
regression.

## Follow-up (not in this PR)

Both components keep rebuilding the same \`countriesMap\` per mount. A
small \`shared/constants/countries.js\` export (\`export const byId =
countries.reduce(...)\`, computed once at module load) would save the
per-mount cost and centralise the shape so this bug can't return.

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

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Sojan Jose <sojan@pepalo.com>
2026-04-28 12:08:46 +05:30
05dd31389e fix(whatsapp): Prevent duplicate conversations from concurrent uploads (#14060)
When a WhatsApp contact starts a new conversation by sending multiple
images at once (an album), each image arrives as a separate webhook.
Because no conversation exists yet, the concurrent workers each pass the
"does a conversation exist?" check and each create their own
conversation — producing one conversation per image instead of one
grouped conversation.

This fix serializes webhook processing per `(inbox, contact)` using a
Redis lock at the job level, so only one webhook at a time can create
the initial conversation for a given contact. Concurrent workers retry
with backoff and append to the same conversation once the lock is
released.

## Closes

- Closes #13261

## How to test

1. On a WhatsApp inbox, ensure there is no active (open) conversation
with a specific test contact — resolve or delete any existing one.
2. From a phone, select 6+ images in the WhatsApp gallery and send them
as a single album to the Chatwoot-connected number.
3. Open the Chatwoot dashboard and confirm exactly **one** new
conversation is created, with all images grouped under it.
4. Repeat the test with a mix of attachment types (XMLs, PDFs, images)
sent in rapid succession — still one conversation.

## What changed

- New Redis key `WHATSAPP_MESSAGE_CREATE_LOCK::<inbox_id>::<sender_id>`
in `lib/redis/redis_keys.rb`.
- `Webhooks::WhatsappEventsJob` now inherits from `MutexApplicationJob`
and wraps event processing in `with_lock(key)`, matching the pattern
already used by `FacebookEventsJob`, `InstagramEventsJob`, and
`TiktokEventsJob`.
- Uses `retry_on LockAcquisitionError, wait: 1.second, attempts: 8` so
concurrent webhooks retry until the lock is free instead of poll-waiting
inside the service.
- Sender ID is derived from the webhook payload (contact's `from`, or
`to` for SMB echo events); status-only webhooks bypass the lock.
- Issue 1 from the report (same `source_id` redelivery) was already
handled previously by `Whatsapp::MessageDedupLock` (atomic `SET NX EX`);
no changes needed there.

---------

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-28 10:30:27 +04:00
f7bbd40816 fix(slack): Sync bot interactive responses (#14076)
When a customer responds to a bot's interactive prompt (input_select,
input_csat, form, input_email) from the widget, the response shows up in
the Chatwoot agent UI but is not reflected in the linked Slack channel —
Slack only ever shows the original question. This happens because the
widget submits the answer as an UPDATE to the original message (writing
`content_attributes.submitted_values` or `submitted_email`), but the
Slack hook only listened to `message.created`, so updates were ignored.

Closes https://linear.app/chatwoot/issue/PLA-147

### Preview

<img width="1290" height="1106" alt="CleanShot 2026-04-21 at 13 19
19@2x"
src="https://github.com/user-attachments/assets/cd2a9d3f-89d3-4e81-9230-5b078e1b7b44"
/>

### How to test

  1. Connect a web widget inbox to a Slack channel.
2. Trigger each bot message type (input_select, form, input_csat,
input_email) in a conversation.
  3. Submit responses from the widget.
4. Verify each response now appears in the Slack thread, appended to the
original bot question.

---------

Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-28 10:29:03 +04:00
Konstantin VasilevandGitHub 7820739f3a fix: prevent Country from being set to Zimbabwe in the contacts list (#14078)
## Description

This PR resolves an issue that was causing Zimbabwe country being
selected in the Contacts list.
The root cause is objects in `countries.json` file are missing the
`code` property which was causing the countries map to always have the
last country in the list also map to an `undefined` key. In turn, this
was causing an error when contact's `country` was undefined.

Additional thing to keep in mind: I am not sure if there is a case when
contact's `country` property is used or if it can be removed as well,
but in my Chatwoot installation contacts only have a `countryCode`
property, and there is no way to set `country` from the widget sdk.

Fixes #7822

## Type of change

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

## How Has This Been Tested?

Simply running the vue application locally targeting my live chatwoot
API.

## 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
- [ ] 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-04-28 11:36:40 +05:30
224556fd1b feat: onboarding account details with enriched data [UPM-17][UPM-18] (#13979)
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
2026-04-28 10:35:51 +05:30
Tanmay Deep SharmaandGitHub 51eb626b88 feat: allow disabling 2FA with a backup code (#14102)
## Linear Ticket
-
https://linear.app/chatwoot/issue/CW-6883/allow-disabling-2fa-using-a-backup-code

## Description

When a user loses access to their authenticator app, they can now
disable 2FA using one of their saved backup codes (in addition to their
password), so they can re-enroll a new authenticator. The disable dialog
includes a toggle to switch between entering a verification code and a
backup code.


## Type of change

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

## How Has This Been Tested?

- Via UI flows
<img width="495" height="423" alt="Screenshot 2026-04-20 at 2 17 21 PM"
src="https://github.com/user-attachments/assets/cc6b3dc5-39e6-4104-b5b9-cdabdc46947e"
/>
<img width="475" height="409" alt="Screenshot 2026-04-20 at 2 17 36 PM"
src="https://github.com/user-attachments/assets/97c7304d-4adb-42ed-b7b4-50f5b38585a3"
/>


## 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-28 10:09:41 +07:00
b0aa844a32 fix(portals): handle integer blob_id in process_attached_logo without 500 (#14274)
Updating portal settings (name, header text, page title, homepage link)
on a portal that already has a logo attached returns 500. The error is
\`NoMethodError: undefined method 'valid_encoding?' for an instance of
Integer\`. The fix is a two-character change in
\`process_attached_logo\`.

Closes #13300

## Root cause

\`ActiveStorage::Blob.find_signed\` expects a signed ID string (e.g.
\`"eyJfcmFpbH..."\`). Internally it calls \`valid_encoding?\` on the
argument to validate the signature payload — a method that exists on
\`String\` but not \`Integer\`.

When a portal already has a logo, the frontend includes the blob's raw
database integer ID (e.g. \`blob_id: 170\`) in the update request
payload. The controller passes this integer directly to \`find_signed\`,
which immediately raises \`NoMethodError\` before any database query is
made.

\`\`\`ruby
# before, crashes when blob_id is an Integer
blob_id = params[:blob_id]
blob = ActiveStorage::Blob.find_signed(blob_id)  # NoMethodError here
@portal.logo.attach(blob)
\`\`\`

## What changed

\`\`\`ruby
# after, safe for any input type
blob = ActiveStorage::Blob.find_signed(params[:blob_id].to_s)
@portal.logo.attach(blob) if blob
\`\`\`

\`.to_s\` on an Integer produces a plain decimal string (\`"170"\`),
which is not a valid signed ID. \`find_signed\` returns \`nil\` for any
invalid signature rather than raising, so the nil guard prevents a
broken \`attach\` call. The existing logo remains attached and the
settings update succeeds.

## Trade-offs considered

| Option | Decision |
|---|---|
| \`find(blob_id)\` when input is an Integer | Bypasses signature
verification — any authenticated user knowing a blob ID could attach
arbitrary files to a portal. Security risk. Rejected. |
| Raise a 422 for non-string blob_id | Overly strict — the frontend
sending an integer is pre-existing behaviour this PR shouldn't break. |
| Silently no-op for invalid blob_id (chosen) | Correct product
behaviour: if no valid signed upload is provided, leave the logo
unchanged. The settings update still succeeds. |

## Known limitation

The correct long-term fix is also on the frontend: it should only send
\`blob_id\` when attaching a **new** upload (using the signed ID from
the direct-upload flow), not when re-submitting the existing logo's raw
database integer ID. This PR makes the server robust against the current
frontend behaviour without requiring a coordinated frontend change.

## How to reproduce

1. Create a Help Center portal and upload a logo
2. Update any text field via \`PUT /api/v1/accounts/:id/portals/:slug\`
while including \`blob_id: <integer>\` in the payload
3. Observe 500 with \`NoMethodError: undefined method 'valid_encoding?'
for an instance of Integer\`

After this fix, the request returns 200, settings are updated, and the
existing logo is preserved.

Co-authored-by: Ramalau Debeila <rdebeila@datacentrix.co.za>
2026-04-28 01:14:51 +05:30
Sony MathewandGitHub c8e551820b fix: [CW-6940] Fix SSRF issue for webhook trigger used by macros and automations (#14155)
This routes external downloads used by webhook fetch used by macros and
acutomations through SafeFetch. It closes the SSRF exposure from raw
Down.download paths, preserves provider-specific auth and header flows,
and adds regression coverage for blocked internal URLs plus
authenticated downloads.

Fixes # (issue):
[CW-6940](https://linear.app/chatwoot/issue/CW-6940/ssrf-via-webhooksautomationmacros-non-upload-non-avatar)
2026-04-27 20:30:59 +05:30
035d2858f5 fix(agent-bots): destroy permissibles on AgentBot deletion and skip orphans in index (#14273)
\`GET /platform/api/v1/agent_bots\` returns 500 when any \`AgentBot\`
that was previously registered with a Platform App has since been
deleted. The bug was introduced by a missing \`dependent: :destroy\` on
the \`AgentBot\` model — deleting a bot left orphaned rows in
\`platform_app_permissibles\`, which the index action later iterated
over and crashed rendering with a \`NoMethodError\` on \`nil\`.

Closes #13407

## Root cause

The index action loads all \`platform_app_permissibles\` for the
platform app and passes each \`resource.permissible\` (the associated
\`AgentBot\`) to a Jbuilder partial. When the \`AgentBot\` no longer
exists, \`resource.permissible\` returns \`nil\` and the partial crashes
calling \`.id\`, \`.name\`, etc. on it.

Every other \`AgentBot\` association (\`agent_bot_inboxes\`,
\`messages\`, \`assigned_conversations\`) had a \`dependent:\` option —
\`platform_app_permissibles\` was the only one missing it. There was
also an N+1 query: the index fired a separate SQL query per permissible
to load each bot.

## What changed

**1. Model — prevent orphans at deletion time**
\`\`\`ruby
has_many :platform_app_permissibles, as: :permissible, dependent:
:destroy
\`\`\`

**2. Controller — eager-load to eliminate N+1**
\`\`\`ruby
@resources = @platform_app.platform_app_permissibles
               .where(permissible_type: 'AgentBot')
               .includes(:permissible)
\`\`\`

**3. Jbuilder — defensive nil guard for pre-existing orphans**
\`\`\`ruby
bot = resource.permissible
next if bot.nil?
json.partial! '...', resource: bot
\`\`\`

## Trade-offs considered

| Option | Decision |
|---|---|
| Rescue \`NoMethodError\` in jbuilder | Hides the failure rather than
fixing it. Rejected. |
| Only add the nil guard, skip the model fix | Leaves the data integrity
gap open — future deletions continue creating orphans. Rejected. |
| Both layers (chosen) | Model fix prevents new orphans; nil guard is
defence-in-depth for any orphans that survived before deployment. |
| \`dependent: :nullify\` | Doesn't apply — a nullified permissible
would still cause the same nil dereference. Rejected. |

## How to reproduce

1. Create an AgentBot via the Platform API
2. Delete the AgentBot via any path (admin UI, API, or direct model
call)
3. Call \`GET /platform/api/v1/agent_bots\` with a Platform App token
4. Observe 500

After this fix, the endpoint returns 200 with an empty array.

Co-authored-by: Ramalau Debeila <rdebeila@datacentrix.co.za>
2026-04-27 19:17:32 +05:30
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>
2026-04-27 18:43:26 +05:30
Aakash BakhleandGitHub 279dd1876c fix: make captain datetime aware (#14069)
# Pull Request Template

## Description

Captain currently cannot discern today, tomorrow etc. This PR adds
datetime awareness to the system prompt

Fixes:
https://linear.app/chatwoot/issue/AI-148/captain-should-be-aware-of-datetime

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

<img width="696" height="247" alt="CleanShot 2026-04-27 at 14 47 47"
src="https://github.com/user-attachments/assets/6a73a8d9-f48e-46bb-a306-7b9a28a5fa9c"
/>


## 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-04-27 15:51:49 +05:30
2266eb493b fix: Add validation to the name attribute in user (#10805)
With this change, the form will start displaying a required field. While
validation is already enforced in APIs and other areas, the super_admin
console—being autogenerated—will throw an error since this requirement
isn’t explicitly defined in the model.

<img width="670" alt="Screenshot 2025-01-30 at 2 12 43 PM"
src="https://github.com/user-attachments/assets/e0ab3ace-3649-4ef2-bc94-8d4d80453dd1"
/>

Fixes https://github.com/chatwoot/chatwoot/issues/10754

---------

Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Sony Mathew <ynos1234@gmail.com>
Co-authored-by: Sony Mathew <2040199+sony-mathew@users.noreply.github.com>
Co-authored-by: Sony Mathew <sony@chatwoot.com>
2026-04-27 15:47:11 +05:30
Sojan JoseandGitHub 0920a01e66 fix(i18n): align pluralization with locale rules (#14266)
Loads Rails locale-specific pluralization rules so languages with an
`other`-only plural model can safely use Crowdin exports without
maintaining duplicate `one` keys.

## Closes

None

## Why

Crowdin exports Rails YAML pluralized strings using each target
language's plural categories. These categories come from Unicode CLDR
and represent grammatical forms, not a literal "number is 1" bucket.

Some languages need separate forms such as `one` and `other`, but
languages like Japanese, Korean, Indonesian, Thai, Vietnamese, and
Chinese use the same form for `1`, `2`, `5`, and larger counts in these
strings. For those locales, CLDR correctly models the plural category as
`other` only.

Before this change, Chatwoot still relied on Rails' default
English-style plural behavior for these locales. That meant a valid
Crowdin export containing only `other` could fail at runtime when Rails
received `count: 1` and looked for a missing `one` branch.

Keeping duplicate `one` keys would only fight Crowdin on every
translation sync. The runtime should instead follow the locale's plural
rules.

## What changed

- Added `rails-i18n` and enabled only its pluralization module.
- Added explicit `other`-only plural rules for Chatwoot's underscore
Chinese locale aliases, `zh_CN` and `zh_TW`.
- Removed redundant `one` keys from the affected Devise and `time_units`
translations.

## Validation

- Ran a Rails runner check across `id`, `ja`, `ko`, `ms`, `th`, `vi`,
`zh_CN`, and `zh_TW` to verify `errors.messages.not_saved` and
`time_units.days` resolve with only `other` for `count: 1`.
- Ran YAML parse validation for all edited locale files.
- Ran `bundle exec rubocop Gemfile config/application.rb
config/initializers/i18n_pluralization.rb`.
2026-04-27 15:40:00 +05:30
Sivin VargheseandGitHub 06467057be fix: oversized email signature images in Letter render (#14144)
# Pull Request Template

## Description

This PR fixes an issue where signature images (with
`?cw_image_height=...`) render at their original large size in the email
bubble.

### Cause

Renderer output:

```html
<img src="..." height="24px" width="auto" />
```

Email UI and clients (Gmail, Outlook) apply CSS like:
`img { max-width: 100%; height: auto; }`
This overrides `height="24px"`.
Other channels work because they use inline styles (`style="height:
24px;"`).


### Solution

Use inline style instead:

```html
<img src="..." style="height: 24px;" />
```


### Why backend fix

* Fixes root cause and aligns Ruby + JS renderers
* Works in both Chatwoot UI and recipient inboxes
* Covers all email-rendered content
* Minimal change


Fixes
https://linear.app/chatwoot/issue/CW-6948/email-signature-image-renders-oversized-in-chatwoot-ui

## Type of change

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

## How Has This Been Tested?

#### Screenshots

**Before**
<img width="1637" height="377" alt="image"
src="https://github.com/user-attachments/assets/0477f6fb-3b95-4fc3-9ea8-f59b71e27f47"
/>


**After**
<img width="1637" height="289" alt="image"
src="https://github.com/user-attachments/assets/de5ea4c1-8452-4c5f-aeb1-e1e11e0fe7d5"
/>



## 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
- [ ] Any dependent changes have been merged and published in downstream
modules
2026-04-27 13:31:43 +05:30
Sivin VargheseandGitHub 8faa5a74b1 fix: prevent focus jump to title after new article auto-creates (#14145)
# Pull Request Template

## Description

When creating a help center article, typing a title and navigating into
the content auto-creates the article and switches the route
(`/articles/new` → `/articles/.../edit/:slug`). During this transition,
focus was jumping back to the title, interrupting editing.

This happened because `ArticleEditor` always autofocuses the title. On
route change, the component remounts and re-triggers focus. Now, after
auto-create, focus stays in the body as expected.


Fixes
https://linear.app/chatwoot/issue/CW-6951/issue-with-the-cursor-position-on-the-help-center-article-when

## Type of change

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

## How Has This Been Tested?

**Screencast**


https://github.com/user-attachments/assets/dac3f7c6-08c4-4df2-afb0-7731ee76424b



## 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-27 13:30:51 +05:30
Aakash BakhleandGitHub a651949c33 fix: improve FAQ generation [AI-145] (#14062)
# Pull Request Template

## Description

- Fetch main content only from Firecrawl, exclude some tags to remove
boilerplate
- Prompt changes for FAQ generation

## Type of change

Please delete options that are not relevant.

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

tested 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-04-27 13:01:44 +05:30
2ada713f29 feat: Add bulk actions for help center articles (translate, status change, delete) (#14137)
Fixes
https://linear.app/chatwoot/issue/CW-6950/support-bulk-actions-for-publish-archive-move-to-draft-delete-in

How to test

1. Go to Help Center → Articles
2. Select articles using checkboxes → bulk bar appears
3. Click Publish/Draft/Archive → articles update, list refreshes
4. Click Delete → confirmation dialog → articles removed
5. Click Translate (requires Captain enabled) → select locale + category
→ translation starts
6. Try translating to a locale that already has translations → warning
with links to existing articles → "Overwrite and translate" proceeds
8. Single article: click three-dot menu → Translate → same dialog opens
for that article



https://github.com/user-attachments/assets/7c76495e-f89e-4456-92bd-a6639a9992f4

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 09:13:43 -07:00
PranavandGitHub 751c28d94d feat(ee): Add article translation via LLM in help center (#14136)
Adds the ability to translate help center articles to other languages using Captain's LLM infrastructure. Translated articles are created as drafts linked to the source article.

Fixes
https://linear.app/chatwoot/issue/CW-6901/translate-article-to-another-language

**How to test**

1. Navigate to Help Center → Articles for a portal with multiple locales
2. Click the three-dot menu on any article → "Translate"
3. Select a target language and category → click Translate
4. Switch to the target locale — the translated article appears as a
draft
5. Try translating the same article again — a warning shows the existing
translation with a link to open it in a new tab
6. Click "Overwrite and translate" to replace the existing translation


https://github.com/user-attachments/assets/1d2e991b-f0ac-403a-bcc1-2181b5731ea4
2026-04-24 08:51:26 -07:00
Sony MathewandGitHub 4959a1ff1e style: [CW-6876] Updated designs for invite email (#14090)
Improved the design for the invite emails
2026-04-24 19:56:01 +05:30
Sony MathewandGitHub 661608c0b1 fix: [CW-6931] Harden external downloads against SSRF [avatar from url job] (#14153)
This routes external downloads used by avatar sync through SafeFetch. It closes the SSRF exposure from raw Down.download paths, preserves provider-specific auth and header flows, and adds regression coverage
for blocked internal URLs plus authenticated downloads.
Fixes # (issue): [CW-6931](https://linear.app/chatwoot/issue/CW-6931/avatarwidget-url-ssrf-downdownload-unprotected-unauth)
2026-04-24 18:59:45 +05:30
Kenta IshizakiandGitHub c5fb8d73cc fix: Prepend UTF-8 BOM to contact CSV export for non-ASCII character support (#14123)
## Description

Spreadsheet applications such as Microsoft Excel do not auto-detect
UTF-8 encoding when opening CSV files. This causes non-ASCII characters
(Arabic, Japanese, Chinese, Korean, etc.) to appear garbled in the
exported contacts CSV.

This PR prepends the UTF-8 Byte Order Mark (`EF BB BF`) to the CSV
output in `Account::ContactsExportJob`, which signals to spreadsheet
applications that the file is UTF-8 encoded.

Fixes: #13998
2026-04-24 18:06:25 +05:30
Kenta IshizakiandGitHub 9a89e1f522 fix: Strip UTF-8 BOM in DataImportJob#csv_reader before parsing CSV (#14126)
## Description

`DataImportJob#csv_reader` reads CSV data with `force_encoding('UTF-8')`
but does not strip the UTF-8 Byte Order Mark (`EF BB BF`). If a CSV file
containing a BOM is imported, the first header key is prefixed with
`\uFEFF`, which causes key mismatches in `DataImport::ContactManager`
when the first column is one of the recognized keys (`:email`,
`:identifier`, `:phone_number`, `:name`).

This was identified during review of #14123 (see #14124 for the tracking
issue).

Fixes #14124

## Type of change

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

## How Has This Been Tested?

- Added a new fixture (`spec/fixtures/data_import/with_bom.csv`)
containing a UTF-8 BOM followed by valid contact data.
- Added a new spec (`will strip UTF-8 BOM and import contacts
correctly`) that imports the BOM fixture and verifies that `name`,
`email`, and `phone_number` are all correctly parsed.
- All existing examples in `spec/jobs/data_import_job_spec.rb` continue
to pass.

## 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
2026-04-24 17:03:03 +05:30
667cd1ba1f fix: widget et translation (#14119)
Added missing et translations for widget

---------

Co-authored-by: Sojan Jose <sojan@pepalo.com>
2026-04-24 16:41:30 +05:30
Vishnu NarayananandGitHub ecdeb891ff fix(spec): pin SafeFetch error classes to described_class to survive Zeitwerk reload (#14139) 2026-04-23 18:18:05 +04:00
Aakash BakhleandGitHub 2182165201 feat: sync documents job [AI-142] (#14057)
# Pull Request Template

## Description

Document auto-sync job pipeline without wiring to controllers or FE

Fixes # (issue)

## 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.
specs and 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-04-22 23:36:32 +05:30
Vishnu NarayananandGitHub ce34f93917 fix: index email subject from conversation for outbound messages (#14122)
## Summary

Outbound email messages never populate
`content_attributes.email.subject`. The subject lives only on the parent
conversation's `additional_attributes.mail_subject`. The search index
doc built by `Messages::SearchDataPresenter` pulls from the
message-level field only, so outbound email subjects are unsearchable
for accounts on the advanced_search (Elasticsearch) path.

This change makes the presenter fall back to
`conversation.additional_attributes.mail_subject` when the message-level
subject is blank. Inbound email messages keep their existing behavior
(message-level subject takes precedence).

Closes https://linear.app/chatwoot/issue/CW-6877

## What changes

- `Messages::SearchDataPresenter#content_attributes_data` now falls back
to the conversation's `mail_subject` when the message-level subject is
blank.
- Added specs covering the fallback, precedence, and the neither-set
case.

## What does not change

- No schema changes, no migrations, no backfill of existing OpenSearch
documents.
- Searchkick's `after_commit :reindex_for_search` on `Message` will pick
up the new field for all newly created or updated messages via the
existing indexing path.
- Postgres search path (free accounts, fallback) is untouched. Broader
subject search for those users is a separate follow-up.
2026-04-22 20:36:35 +05:30
Sivin VargheseandGitHub 475db85318 fix: prevent template picker dropdown cut-off in compose form (#14129) 2026-04-22 17:13:26 +05:30
f12118a3c0 fix: void topup invoice when card payment fails (#14104)
When a credit top-up's card charge fails, the finalized invoice was left
in an open state with no hook back into our fulfillment service. If that
invoice was later paid (manually via the hosted invoice page, or by a
future dunning flow), the account would never receive its credits —
silent revenue loss.

This change voids the invoice the moment the charge fails, so a failed
top-up cannot turn into a paid-but-unfulfilled invoice.

## Closes

<!-- add the relevant issue / Linear link -->

## How to test

1. On a Business-plan account with a Stripe customer, attach a test card
that will decline on charge (e.g. `4000 0000 0000 0002`).
2. From the dashboard, open the credit top-up flow and purchase a credit
pack.
3. Observe the API returns an error and the account's captain credits
are unchanged.
4. In the Stripe dashboard, confirm the corresponding invoice is in
`void` status (not `open`).
5. Repeat with a good card (`4242 4242 4242 4242`) and confirm the happy
path still fulfills credits.

## What changed

- `finalize_and_pay` in `Enterprise::Billing::TopupCheckoutService` now
rescues `Stripe::CardError`, voids the open invoice via
`Stripe::Invoice.void_invoice`, and re-raises so the controller surfaces
the original decline error to the client.
- Rescue is intentionally narrow to `Stripe::CardError` (declines,
insufficient funds, SCA `authentication_required`). Transient errors
like `APIConnectionError` / `RateLimitError` are left to propagate — the
charge may have actually succeeded and voiding could be wrong.
- Invoices are created with `auto_advance: false`, so Stripe's Smart
Retries won't collect on an open invoice; voiding is the correct
terminal state.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 11:21:26 +07:00
Muhsin KelothandGitHub ca66218cb9 Revert: Validate Twilio webhook signatures (X-Twilio-Signature) (#14125)
Reverts chatwoot/chatwoot#13638
2026-04-21 19:16:08 +04:00
6a9c44476e feat(super-admin): Add push diagnostics tool (#14105)
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>
2026-04-21 15:55:12 +04:00
6928f14a76 fix: Validate Twilio webhook signatures (X-Twilio-Signature) (#13638)
Fixes #13619

## Summary

- Add `TwilioSignatureVerifyConcern` that validates the
`X-Twilio-Signature` header using `Twilio::Security::RequestValidator`
(already bundled via `twilio-ruby` gem)
- Include the concern in `Twilio::CallbackController` and
`Twilio::DeliveryStatusController` — both endpoints were previously
accepting requests from any source with no authentication
- Channels using API key authentication (`api_key_sid` present) skip
validation with a warning log, since Twilio signs with the account auth
token which isn't stored for those channels

## How it works

1. `before_action` looks up the `Channel::TwilioSms` from request params
(`MessagingServiceSid` or `AccountSid` + phone number)
2. Validates the HMAC-SHA1 signature using the channel's auth token
3. Returns `403 Forbidden` if signature is invalid, missing, or channel
not found
4. Handles reverse proxy URL reconstruction via `X-Forwarded-Proto`
header

Follows the same pattern used by `Webhooks::ShopifyController` and
`Webhooks::TiktokController`.

## Test plan

- [x] Valid signature → 204 No Content, job enqueued
- [x] Invalid signature → 403 Forbidden, job not enqueued
- [x] Missing signature header → 403 Forbidden
- [x] Channel not found → 403 Forbidden
- [x] API key channel → skips validation, job enqueued (with warning
log)
- [x] MessagingServiceSid lookup → validates and enqueues
- [x] All existing Twilio service/job specs pass (99 examples, 0
failures)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Sojan Jose <sojan@pepalo.com>
2026-04-21 13:44:25 +04:00
Sivin VargheseandGitHub d2625d8544 fix: TypeError cannot read properties of null (reading 'name') (#14112) 2026-04-21 14:48:03 +05:30
Tanmay Deep SharmaandGitHub 7d42edd17f fix: sanitize parentheses from email From header to prevent SMTP 553 errors (#14075)
## Description

When an inbox name or business name contains parentheses — e.g. `Giro
Crédito - Soporte (Email` — the resulting From header becomes
unparseable by SMTP servers. The `(` is interpreted as an RFC 5322
comment start, swallowing the actual email address and causing a `553
Invalid email address` rejection.

Closes [CW-6323](https://linear.app/chatwoot/issue/CW-6323)


## Type of change

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

## How to reproduce?

1. Set an inbox's name or business name to include a parenthesis, e.g.
`Support (Email`
2. Send an outgoing email reply from that inbox
3. Observe `Net::SMTPFatalError: 553 ... Invalid email address`

## 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-21 11:30:20 +05:30
Sivin VargheseandGitHub 437dd9d38c fix: prevent Ctrl+Enter adding extra line break on send (Windows) (#14077)
# Pull Request Template

## Description

This PR includes,
On Windows, pressing **Ctrl+Enter** in the reply editor was inserting an
unintended line break before sending. This led to two issues:

* **Unexpected blank lines**
After adding a line break with Shift+Enter and removing it with
Backspace, the editor looked correct. However, sending with Ctrl+Enter
reintroduced a hidden break, resulting in an extra blank line in the
final message.

* **Selected text being replaced**
When text was selected and Ctrl+Enter was pressed, the selection was
replaced with a line break instead of being sent.

Fixes
https://linear.app/chatwoot/issue/CW-6840/newline-bug-in-the-editor


### **Cause**

Two keyboard handlers responded to **Ctrl+Enter** on Windows:

* ProseMirror (`Mod-Enter`) inserted a hard break
* ReplyBox (`$mod+Enter`) triggered send

The existing guard only checked `metaKey` (Cmd), so it never worked on
Windows. As a result, a line break was inserted just before sending.

### **Solution**

Make the modifier check platform-aware so the editor correctly
intercepts the send shortcut:

* Added `detectOS`, `isMac`, and `OS` constants
* Introduced `hasPressedMod` (uses `metaKey` on macOS, `ctrlKey`
elsewhere)

This ensures Ctrl+Enter sends the message without modifying content,
while keeping existing behavior unchanged.


**NB:** macOS behavior with Cmd+Enter remains unchanged



## Type of change

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

## How Has This Been Tested?

**Case 1: line break**

1. Type `hello`
2. Press Shift+Enter, then Backspace
3. Press Ctrl+Enter
   → Message contains an unexpected blank new line

**Case 2: Selection replaced**

1. Type two lines using Shift+Enter
2. Select text on the second line
3. Press Ctrl+Enter
   → Selected text is replaced and not sent

### Screencast

**Before**


https://github.com/user-attachments/assets/d6d285a9-260b-4711-8bbd-d0c8519e8d20

**After**



https://github.com/user-attachments/assets/c0ace1f7-5d22-44a2-8e08-22190ee21e61



## 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
- [ ] Any dependent changes have been merged and published in downstream
modules
2026-04-20 18:16:41 +05:30
50720f2395 feat: Gate conversation continuity toggle behind inbound_emails feature flag (#13838)
The "conversation continuity via email" toggle was visible to all
accounts regardless of whether they had `inbound_emails` enabled.
Without inbound email infrastructure, replies to those follow-up emails
land in the agent's personal inbox instead of routing back into
Chatwoot. The feature appears to work but silently breaks the reply
path.

The toggle is now gated on the `inbound_emails` feature flag. On
self-hosted without the feature, the toggle is hidden entirely. On
cloud, it remains visible but disabled with upgrade messaging.

On the backend, `inbound_emails` is added to the manually managed
features list in `InternalAttributesService` so that Stripe webhook plan
syncs don't override it when support enables it for an account.

---------

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
2026-04-20 17:47:24 +05:30
Sivin VargheseandGitHub 1de90ec136 fix: clean up conversation list rendering (#14107) 2026-04-20 15:48:06 +05:30
Sivin VargheseandGitHub 3a4738729d feat(v5): New expanded layout for chatlist (#13652) 2026-04-20 14:02:10 +05:30
Sivin VargheseandGitHub 787fcc4a95 chore: update conversation sidebar interactions (#13988) 2026-04-20 13:08:19 +05:30
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>
2026-04-20 11:15:48 +05:30
Sojan Jose 5e82f24be5 Merge branch 'release/4.13.0' into develop 2026-04-16 19:03:04 +05:30
Sojan Jose e123a4e500 Bump version to 4.13.0 2026-04-16 19:02:23 +05:30
Sojan JoseandGitHub 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
2026-04-16 18:54:35 +05:30
03c10ba147 chore: Update translations (#14080)
Co-authored-by: Sojan Jose <sojan@pepalo.com>
2026-04-16 18:12:33 +05:30
aa2e8f99e4 fix(i18n): correct zh/zh_CN conversation assignment message translations (#14033)
## Summary

The `assignee_name` and `user_name` variables are swapped in the Chinese
(zh/zh_CN) locale files for conversation assignment activity messages,
causing the rendered text to show the wrong person as the assignee.

### Before (incorrect)

| Template | English (correct) | Chinese (incorrect) |
|---|---|---|
| `assignee.assigned` | Assigned to **AgentA** by **Admin** | 由
**AgentA** 分配给 **Admin** |
| `team.assigned` | Assigned to **TeamX** by **Admin** | 由 **TeamX** 分配给
**Admin** |
| `team.assigned_with_assignee` | Assigned to **AgentA** via **TeamX**
by **Admin** | 由 **AgentA** 分配给 **TeamX** 团队的 **Admin** |

The Chinese text reads as if the conversation was assigned **to Admin**
(the API caller), when it was actually assigned **to AgentA**.

### After (correct)

| Template | Chinese (fixed) |
|---|---|
| `assignee.assigned` | 由 **Admin** 分配给 **AgentA** |
| `team.assigned` | 由 **Admin** 分配给 **TeamX** |
| `team.assigned_with_assignee` | 由 **Admin** 通过 **TeamX** 团队分配给
**AgentA** |

Now correctly matches the English template semantics.

## Files Changed

- `config/locales/zh_CN.yml` — 3 lines
- `config/locales/zh.yml` — 3 lines

## How to Verify

1. Set locale to `zh_CN`
2. Have an admin assign a conversation to an agent
3. Check the activity message in the conversation — the assignee name
should appear after "分配给", not before it

Co-authored-by: Sojan Jose <sojan@pepalo.com>
2026-04-16 16:34:20 +05:30
Sojan JoseandGitHub 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.
2026-04-16 15:57:41 +05:30
Vishnu NarayananandGitHub 72b8a31f2d fix: handle users being stuck on is_creating billing flow (#12750)
Fixes https://linear.app/chatwoot/issue/CW-5880/handle-customers-being-stuck-on-biling-page
2026-04-16 13:22:31 +05:30
Sivin VargheseandGitHub 48533e2a5d fix: strip markdown hard-break backslashes from webhook payloads (#13950) 2026-04-16 13:19:35 +05:30
b5264a2560 feat: Adds the ability to resize the editor (#13916)
# Pull Request Template

## Description

This PR adds support for resizing the reply editor up to nearly half the
screen height. It also deprecates the old modal-based pop-out reply box,
clicking the same button now expands the editor inline. Users can adjust
the height using the slider or the expand button.


## Type of change

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

## How Has This Been Tested?

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


## 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
- [ ] 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: Pranav <pranav@chatwoot.com>
2026-04-16 12:37:56 +05:30
98cf1ce9f6 fix(bulk-select): limit select-all to visible items; add secondary slot (#12891)
Update BulkSelectBar to compute selection state (indeterminate/all) from
visible item IDs and only toggle selection for visible items. Preserve
existing selection for off-screen items when toggling, and guard against
empty visibility. Add detection/rendering for an optional
secondary-actions slot and adjust layout/divider. Also fix
ContactsBulkActionBar selection logic to determine "all selected" by
verifying every visible ID is in the selection. These changes ensure
correct select-all behavior with filtered/visible lists and support
additional UI actions.



https://github.com/user-attachments/assets/d06b78d1-a64a-4c0c-a82a-f870140236c7


# Pull Request Template

## Description

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

## Type of change

Please delete options that are not relevant.

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

## How Has This Been Tested?

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


## Checklist:

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

---------

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
2026-04-16 12:22:53 +05:30
Sivin VargheseandGitHub 5eee331da3 feat: add slash command menu to article editor (#14035) 2026-04-16 11:27:59 +05:30
Sivin VargheseandGitHub edd0fc98db feat: Table support in article editor (#13974) 2026-04-16 11:23:10 +05:30
Gabriel JablonskiandGitHub cc008951db fix(sidebar): improve active child route matching logic (#13121) 2026-04-16 10:57:16 +05:30
97dae52841 fix: use committed model registry for RubyLLM (#14067)
RubyLLM bundles a static models.json that doesn't know about models
released after the gem was published. Self-hosted users configuring
newer models hit ModelNotFoundError.

Added a rake task that refreshes the registry from models.dev and saves
to disk. ~~Called during Docker image build so every deploy gets fresh
model data. Falls back silently to the bundled registry if models.dev is
unreachable.~~

Commit the models.json file to code so it is available across
deployments.

---------

Co-authored-by: Sojan Jose <sojan@pepalo.com>
2026-04-16 10:28:38 +05:30
Aakash BakhleandGitHub 5264de24b0 feat: migrations for document auto-sync [AI-141] (#14041)
# Pull Request Template

## Description

Add migrations for document auto-sync

Fixes # (issue)

## Type of change

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

## How Has This Been Tested?
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-04-15 17:56:10 +05:30
b96bf41234 chore: Enable Participating tab for conversations (#11714)
## Summary

This PR enables the **Participating** conversation view in the main
sidebar and keeps the behavior aligned with existing conversation views.

## What changed

- Added **Participating** under Conversations in the new sidebar.
- Added a guard in conversation realtime `addConversation` flow so
generic `conversation.created` events are not injected while the user is
on Participating view.
- Added participating route mapping in conversation-list redirect helper
so list redirects resolve correctly to `/participating/conversations`.

## Scope notes

- Kept changes minimal and consistent with current `develop` behavior.
- No additional update-event filtering was added beyond what existing
views already do.

---------


Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
2026-04-15 17:03:39 +05:30
3f9f054c43 fix: drop WhatsApp incoming messages from blocked contacts (#14061)
## 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>
2026-04-15 13:42:48 +07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Sivin Varghese
8e5d4f4d23 chore(deps): bump axios from 1.13.6 to 1.15.0 (#14051)
Bumps [axios](https://github.com/axios/axios) from 1.13.6 to 1.15.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/axios/axios/releases">axios's
releases</a>.</em></p>
<blockquote>
<h2>v1.15.0</h2>
<p>This release delivers two critical security patches, adds runtime
support for Deno and Bun, and includes significant CI hardening,
documentation improvements, and routine dependency updates.</p>
<h2>⚠️ Important Changes</h2>
<ul>
<li><strong>Deprecation:</strong> <code>url.parse()</code> usage has
been replaced to address Node.js deprecation warnings. If you are on a
recent version of Node.js, this resolves console warnings you may have
been seeing. (<strong><a
href="https://redirect.github.com/axios/axios/issues/10625">#10625</a></strong>)</li>
</ul>
<h2>🔒 Security Fixes</h2>
<ul>
<li><strong>Proxy Handling:</strong> Fixed a <code>no_proxy</code>
hostname normalisation bypass that could lead to Server-Side Request
Forgery (SSRF). (<strong><a
href="https://redirect.github.com/axios/axios/issues/10661">#10661</a></strong>)</li>
<li><strong>Header Injection:</strong> Fixed an unrestricted cloud
metadata exfiltration vulnerability via a header injection chain.
(<strong><a
href="https://redirect.github.com/axios/axios/issues/10660">#10660</a></strong>)</li>
</ul>
<h2>🚀 New Features</h2>
<ul>
<li><strong>Runtime Support:</strong> Added compatibility checks and
documentation for Deno and Bun environments. (<strong><a
href="https://redirect.github.com/axios/axios/issues/10652">#10652</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10653">#10653</a></strong>)</li>
</ul>
<h2>🔧 Maintenance &amp; Chores</h2>
<ul>
<li><strong>CI Security:</strong> Hardened workflow permissions to least
privilege, added the <code>zizmor</code> security scanner, pinned action
versions, and gated npm publishing with OIDC and environment protection.
(<strong><a
href="https://redirect.github.com/axios/axios/issues/10618">#10618</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10619">#10619</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10627">#10627</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10637">#10637</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10666">#10666</a></strong>)</li>
<li><strong>Dependencies:</strong> Bumped
<code>serialize-javascript</code>, <code>handlebars</code>,
<code>picomatch</code>, <code>vite</code>, and
<code>denoland/setup-deno</code> to latest versions. Added a 7-day
Dependabot cooldown period. (<strong><a
href="https://redirect.github.com/axios/axios/issues/10574">#10574</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10572">#10572</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10568">#10568</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10663">#10663</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10664">#10664</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10665">#10665</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10669">#10669</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10670">#10670</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10616">#10616</a></strong>)</li>
<li><strong>Documentation:</strong> Unified docs, improved
<code>beforeRedirect</code> credential leakage example, clarified
<code>withCredentials</code>/<code>withXSRFToken</code> behaviour,
HTTP/2 support notes, async/await timeout error handling, header case
preservation, and various typo fixes. (<strong><a
href="https://redirect.github.com/axios/axios/issues/10649">#10649</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10624">#10624</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/7452">#7452</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/7471">#7471</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10654">#10654</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10644">#10644</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10589">#10589</a></strong>)</li>
<li><strong>Housekeeping:</strong> Removed stale files, regenerated
lockfile, and updated sponsor scripts and blocks. (<strong><a
href="https://redirect.github.com/axios/axios/issues/10584">#10584</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10650">#10650</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10582">#10582</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10640">#10640</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10659">#10659</a></strong>,
<strong><a
href="https://redirect.github.com/axios/axios/issues/10668">#10668</a></strong>)</li>
<li><strong>Tests:</strong> Added regression coverage for urlencoded
<code>Content-Type</code> casing. (<strong><a
href="https://redirect.github.com/axios/axios/issues/10573">#10573</a></strong>)</li>
</ul>
<h2>🌟 New Contributors</h2>
<p>We are thrilled to welcome our new contributors. Thank you for
helping improve Axios:</p>
<ul>
<li><strong><a
href="https://github.com/raashish1601"><code>@​raashish1601</code></a></strong>
(<strong><a
href="https://redirect.github.com/axios/axios/issues/10573">#10573</a></strong>)</li>
<li><strong><a
href="https://github.com/Kilros0817"><code>@​Kilros0817</code></a></strong>
(<strong><a
href="https://redirect.github.com/axios/axios/issues/10625">#10625</a></strong>)</li>
<li><strong><a
href="https://github.com/ashstrc"><code>@​ashstrc</code></a></strong>
(<strong><a
href="https://redirect.github.com/axios/axios/issues/10624">#10624</a></strong>)</li>
<li><strong><a
href="https://github.com/Abhi3975"><code>@​Abhi3975</code></a></strong>
(<strong><a
href="https://redirect.github.com/axios/axios/issues/10589">#10589</a></strong>)</li>
<li><strong><a
href="https://github.com/theamodhshetty"><code>@​theamodhshetty</code></a></strong>
(<strong><a
href="https://redirect.github.com/axios/axios/issues/7452">#7452</a></strong>)</li>
</ul>
<h2>v1.14.0</h2>
<p>This release focuses on compatibility fixes, adapter stability
improvements, and test/tooling modernisation.</p>
<h2>⚠️ Important Changes</h2>
<ul>
<li><strong>Breaking Changes:</strong> None identified in this
release.</li>
<li><strong>Action Required:</strong> If you rely on env-based proxy
behaviour or CJS resolution edge-cases, validate your integration after
upgrade (notably <code>proxy-from-env</code> v2 alignment and
<code>main</code> entry compatibility fix).</li>
</ul>
<h2>🚀 New Features</h2>
<ul>
<li><strong>Runtime Features:</strong> No new end-user features were
introduced in this release.</li>
<li><strong>Test Coverage Expansion:</strong> Added broader smoke/module
test coverage for CJS and ESM package usage. (<a
href="https://redirect.github.com/axios/axios/pull/7510">#7510</a>)</li>
</ul>
<h2>🐛 Bug Fixes</h2>
<ul>
<li><strong>Headers:</strong> Trim trailing CRLF in normalised header
values. (<a
href="https://redirect.github.com/axios/axios/pull/7456">#7456</a>)</li>
<li><strong>HTTP/2:</strong> Close detached HTTP/2 sessions on timeout
to avoid lingering sessions. (<a
href="https://redirect.github.com/axios/axios/pull/7457">#7457</a>)</li>
<li><strong>Fetch Adapter:</strong> Cancel <code>ReadableStream</code>
created during request-stream capability probing to prevent async
resource leaks. (<a
href="https://redirect.github.com/axios/axios/pull/7515">#7515</a>)</li>
<li><strong>Proxy Handling:</strong> Fixed env proxy behavior with
<code>proxy-from-env</code> v2 usage. (<a
href="https://redirect.github.com/axios/axios/pull/7499">#7499</a>)</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/axios/axios/blob/v1.x/CHANGELOG.md">axios's
changelog</a>.</em></p>
<blockquote>
<h1>Changelog</h1>
<h2><a
href="https://github.com/axios/axios/compare/v1.13.2...v1.13.3">1.13.3</a>
(2026-01-20)</h2>
<h3>Bug Fixes</h3>
<ul>
<li><strong>http2:</strong> Use port 443 for HTTPS connections by
default. (<a
href="https://redirect.github.com/axios/axios/issues/7256">#7256</a>)
(<a
href="https://github.com/axios/axios/commit/d7e60653460480ffacecf85383012ca1baa6263e">d7e6065</a>)</li>
<li><strong>interceptor:</strong> handle the error in the same
interceptor (<a
href="https://redirect.github.com/axios/axios/issues/6269">#6269</a>)
(<a
href="https://github.com/axios/axios/commit/5945e40bb171d4ac4fc195df276cf952244f0f89">5945e40</a>)</li>
<li>main field in package.json should correspond to cjs artifacts (<a
href="https://redirect.github.com/axios/axios/issues/5756">#5756</a>)
(<a
href="https://github.com/axios/axios/commit/7373fbff24cd92ce650d99ff6f7fe08c2e2a0a04">7373fbf</a>)</li>
<li><strong>package.json:</strong> add 'bun' package.json 'exports'
condition. Load the Node.js build in Bun instead of the browser build
(<a
href="https://redirect.github.com/axios/axios/issues/5754">#5754</a>)
(<a
href="https://github.com/axios/axios/commit/b89217e3e91de17a3d55e2b8f39ceb0e9d8aeda8">b89217e</a>)</li>
<li>silentJSONParsing=false should throw on invalid JSON (<a
href="https://redirect.github.com/axios/axios/issues/7253">#7253</a>)
(<a
href="https://redirect.github.com/axios/axios/issues/7257">#7257</a>)
(<a
href="https://github.com/axios/axios/commit/7d19335e43d6754a1a9a66e424f7f7da259895bf">7d19335</a>)</li>
<li>turn AxiosError into a native error (<a
href="https://redirect.github.com/axios/axios/issues/5394">#5394</a>)
(<a
href="https://redirect.github.com/axios/axios/issues/5558">#5558</a>)
(<a
href="https://github.com/axios/axios/commit/1c6a86dd2c0623ee1af043a8491dbc96d40e883b">1c6a86d</a>)</li>
<li><strong>types:</strong> add handlers to AxiosInterceptorManager
interface (<a
href="https://redirect.github.com/axios/axios/issues/5551">#5551</a>)
(<a
href="https://github.com/axios/axios/commit/8d1271b49fc226ed7defd07cd577bd69a55bb13a">8d1271b</a>)</li>
<li><strong>types:</strong> restore AxiosError.cause type from unknown
to Error (<a
href="https://redirect.github.com/axios/axios/issues/7327">#7327</a>)
(<a
href="https://github.com/axios/axios/commit/d8233d9e8e9a64bfba9bbe01d475ba417510b82b">d8233d9</a>)</li>
<li>unclear error message is thrown when specifying an empty proxy
authorization (<a
href="https://redirect.github.com/axios/axios/issues/6314">#6314</a>)
(<a
href="https://github.com/axios/axios/commit/6ef867e684adf7fb2343e3b29a79078a3c76dc29">6ef867e</a>)</li>
</ul>
<h3>Features</h3>
<ul>
<li>add <code>undefined</code> as a value in AxiosRequestConfig (<a
href="https://redirect.github.com/axios/axios/issues/5560">#5560</a>)
(<a
href="https://github.com/axios/axios/commit/095033c626895ecdcda2288050b63dcf948db3bd">095033c</a>)</li>
<li>add automatic minor and patch upgrades to dependabot (<a
href="https://redirect.github.com/axios/axios/issues/6053">#6053</a>)
(<a
href="https://github.com/axios/axios/commit/65a7584eda6164980ddb8cf5372f0afa2a04c1ed">65a7584</a>)</li>
<li>add Node.js coverage script using c8 (closes <a
href="https://redirect.github.com/axios/axios/issues/7289">#7289</a>)
(<a
href="https://redirect.github.com/axios/axios/issues/7294">#7294</a>)
(<a
href="https://github.com/axios/axios/commit/ec9d94e9f88da13e9219acadf65061fb38ce080a">ec9d94e</a>)</li>
<li>added copilot instructions (<a
href="https://github.com/axios/axios/commit/3f83143bfe617eec17f9d7dcf8bafafeeae74c26">3f83143</a>)</li>
<li>compatibility with frozen prototypes (<a
href="https://redirect.github.com/axios/axios/issues/6265">#6265</a>)
(<a
href="https://github.com/axios/axios/commit/860e03396a536e9b926dacb6570732489c9d7012">860e033</a>)</li>
<li>enhance pipeFileToResponse with error handling (<a
href="https://redirect.github.com/axios/axios/issues/7169">#7169</a>)
(<a
href="https://github.com/axios/axios/commit/88d78842541610692a04282233933d078a8a2552">88d7884</a>)</li>
<li><strong>types:</strong> Intellisense for string literals in a
widened union (<a
href="https://redirect.github.com/axios/axios/issues/6134">#6134</a>)
(<a
href="https://github.com/axios/axios/commit/f73474d02c5aa957b2daeecee65508557fd3c6e5">f73474d</a>),
closes <a
href="https://redirect.github.com//redirect.github.com/microsoft/TypeScript/issues/33471/issues/issuecomment-1376364329">microsoft/TypeScript#33471</a></li>
</ul>
<h3>Reverts</h3>
<ul>
<li>Revert &quot;fix: silentJSONParsing=false should throw on invalid
JSON (<a
href="https://redirect.github.com/axios/axios/issues/7253">#7253</a>)
(<a
href="https://redirect.github.com/axios/axios/issues/7">#7</a>…&quot;
(<a
href="https://redirect.github.com/axios/axios/issues/7298">#7298</a>)
(<a
href="https://github.com/axios/axios/commit/a4230f5581b3f58b6ff531b6dbac377a4fd7942a">a4230f5</a>),
closes <a
href="https://redirect.github.com/axios/axios/issues/7253">#7253</a> <a
href="https://redirect.github.com/axios/axios/issues/7">#7</a> <a
href="https://redirect.github.com/axios/axios/issues/7298">#7298</a></li>
<li><strong>deps:</strong> bump peter-evans/create-pull-request from 7
to 8 in the github-actions group (<a
href="https://redirect.github.com/axios/axios/issues/7334">#7334</a>)
(<a
href="https://github.com/axios/axios/commit/2d6ad5e48bd29b0b2b5e7e95fb473df98301543a">2d6ad5e</a>)</li>
</ul>
<h3>Contributors to this release</h3>
<ul>
<li><!-- raw HTML omitted --> <a href="https://github.com/ashvin2005"
title="+1752/-4 ([#7218](https://github.com/axios/axios/issues/7218)
[#7218](https://github.com/axios/axios/issues/7218) )">Ashvin
Tiwari</a></li>
<li><!-- raw HTML omitted --> <a href="https://github.com/mochinikunj"
title="+940/-12 ([#7294](https://github.com/axios/axios/issues/7294)
[#7294](https://github.com/axios/axios/issues/7294) )">Nikunj
Mochi</a></li>
<li><!-- raw HTML omitted --> <a href="https://github.com/imanchalsingh"
title="+544/-102 ([#7169](https://github.com/axios/axios/issues/7169)
[#7185](https://github.com/axios/axios/issues/7185) )">Anchal
Singh</a></li>
<li><!-- raw HTML omitted --> <a href="https://github.com/jasonsaayman"
title="+317/-73 ([#7334](https://github.com/axios/axios/issues/7334)
[#7298](https://github.com/axios/axios/issues/7298)
)">jasonsaayman</a></li>
<li><!-- raw HTML omitted --> <a href="https://github.com/brodo"
title="+99/-120 ([#5558](https://github.com/axios/axios/issues/5558)
)">Julian Dax</a></li>
<li><!-- raw HTML omitted --> <a
href="https://github.com/AKASHDHARDUBEY" title="+167/-0
([#7287](https://github.com/axios/axios/issues/7287)
[#7288](https://github.com/axios/axios/issues/7288) )">Akash Dhar
Dubey</a></li>
<li><!-- raw HTML omitted --> <a href="https://github.com/madhumitaaa"
title="+20/-68 ([#7198](https://github.com/axios/axios/issues/7198)
)">Madhumita</a></li>
<li><!-- raw HTML omitted --> <a href="https://github.com/Tackoil"
title="+80/-2 ([#6269](https://github.com/axios/axios/issues/6269)
)">Tackoil</a></li>
<li><!-- raw HTML omitted --> <a href="https://github.com/justindhillon"
title="+41/-41 ([#6324](https://github.com/axios/axios/issues/6324)
[#6315](https://github.com/axios/axios/issues/6315) )">Justin
Dhillon</a></li>
<li><!-- raw HTML omitted --> <a href="https://github.com/Rudrxxx"
title="+71/-2 ([#7257](https://github.com/axios/axios/issues/7257)
)">Rudransh</a></li>
<li><!-- raw HTML omitted --> <a href="https://github.com/WuMingDao"
title="+36/-36 ([#7215](https://github.com/axios/axios/issues/7215)
)">WuMingDao</a></li>
<li><!-- raw HTML omitted --> <a href="https://github.com/codenomnom"
title="+70/-0 ([#7201](https://github.com/axios/axios/issues/7201)
[#7201](https://github.com/axios/axios/issues/7201)
)">codenomnom</a></li>
<li><!-- raw HTML omitted --> <a href="https://github.com/Nandann018-ux"
title="+60/-10 ([#7272](https://github.com/axios/axios/issues/7272)
)">Nandan Acharya</a></li>
<li><!-- raw HTML omitted --> <a href="https://github.com/KernelDeimos"
title="+22/-40 ([#7042](https://github.com/axios/axios/issues/7042)
)">Eric Dubé</a></li>
<li><!-- raw HTML omitted --> <a href="https://github.com/tiborpilz"
title="+40/-4 ([#5551](https://github.com/axios/axios/issues/5551)
)">Tibor Pilz</a></li>
<li><!-- raw HTML omitted --> <a href="https://github.com/joaoGabriel55"
title="+31/-4 ([#6314](https://github.com/axios/axios/issues/6314)
)">Gabriel Quaresma</a></li>
<li><!-- raw HTML omitted --> <a href="https://github.com/turadg"
title="+23/-6 ([#6265](https://github.com/axios/axios/issues/6265)
)">Turadg Aleahmad</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/axios/axios/commit/772a4e54ecc4cc2421e2b746daff0aca10f359d7"><code>772a4e5</code></a>
chore(release): prepare release 1.15.0 (<a
href="https://redirect.github.com/axios/axios/issues/10671">#10671</a>)</li>
<li><a
href="https://github.com/axios/axios/commit/4b071371be2f810b4bc7797a13838e0f806ebb22"><code>4b07137</code></a>
chore(deps-dev): bump vite from 8.0.0 to 8.0.5 in /tests/smoke/esm (<a
href="https://redirect.github.com/axios/axios/issues/10663">#10663</a>)</li>
<li><a
href="https://github.com/axios/axios/commit/51e57b39db251bfe3d34af5c943dfea18e06c8b6"><code>51e57b3</code></a>
chore(deps-dev): bump vite from 8.0.2 to 8.0.5 (<a
href="https://redirect.github.com/axios/axios/issues/10664">#10664</a>)</li>
<li><a
href="https://github.com/axios/axios/commit/fba1a77930f0c459677b729161627234b88c90aa"><code>fba1a77</code></a>
chore(deps-dev): bump vite from 8.0.2 to 8.0.5 in /tests/module/esm (<a
href="https://redirect.github.com/axios/axios/issues/10665">#10665</a>)</li>
<li><a
href="https://github.com/axios/axios/commit/0bf6e28eac86e87da2b60bbf5ea4237910e1a08e"><code>0bf6e28</code></a>
chore(deps): bump denoland/setup-deno in the github-actions group (<a
href="https://redirect.github.com/axios/axios/issues/10669">#10669</a>)</li>
<li><a
href="https://github.com/axios/axios/commit/8107157c572ee4a54cb28c01ab7f7f3d895ba661"><code>8107157</code></a>
chore(deps-dev): bump the development_dependencies group with 4 updates
(<a
href="https://redirect.github.com/axios/axios/issues/10670">#10670</a>)</li>
<li><a
href="https://github.com/axios/axios/commit/e66530e3302d56176befd0778155dafea2487542"><code>e66530e</code></a>
ci: require npm-publish environment for releases (<a
href="https://redirect.github.com/axios/axios/issues/10666">#10666</a>)</li>
<li><a
href="https://github.com/axios/axios/commit/49f23cbfe4d308a075281c5f798d4c68f648cbe2"><code>49f23cb</code></a>
chore(sponsor): update sponsor block (<a
href="https://redirect.github.com/axios/axios/issues/10668">#10668</a>)</li>
<li><a
href="https://github.com/axios/axios/commit/363185461b90b1b78845dc8a99a1f103d9b122a1"><code>3631854</code></a>
fix: unrestricted cloud metadata exfiltration via header injection chain
(<a
href="https://redirect.github.com/axios/axios/issues/10">#10</a>...</li>
<li><a
href="https://github.com/axios/axios/commit/fb3befb6daac6cad26b2e54094d0f2d9e47f24df"><code>fb3befb</code></a>
fix: no_proxy hostname normalization bypass leads to ssrf (<a
href="https://redirect.github.com/axios/axios/issues/10661">#10661</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/axios/axios/compare/v1.13.6...v1.15.0">compare
view</a></li>
</ul>
</details>
<details>
<summary>Install script changes</summary>
<p>This version modifies <code>prepare</code> script that runs during
installation. Review the package contents before updating.</p>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=axios&package-manager=npm_and_yarn&previous-version=1.13.6&new-version=1.15.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/chatwoot/chatwoot/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
2026-04-15 00:44:54 +05:30
64f6bfc811 feat: Inline edit support for contact info (#13976)
# Pull Request Template

## Description

This PR adds inline editing support for contact name, phone number,
email, and company fields in the conversation contact sidebar

## Type of change

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

## How Has This Been Tested?

**Screencast**


https://github.com/user-attachments/assets/e9f8e37d-145b-4736-b27a-eb9ea66847bd



## 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: Pranav <pranav@chatwoot.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2026-04-14 16:53:40 +04:00
72c9e1775b fix: Prevent article editor from resetting content while typing (#14014)
# Pull Request Template

## Description


### Description

This PR fixes an issue where the editor would reset content and move the
cursor while typing. The issue was caused by a dual debounce setup
(400ms + 2500ms) that saved content and then overwrote local state with
stale API responses while the user was still typing.

### What changed

* Editor now uses local state (`localTitle`, `localContent`) as the
source of truth while editing
* Vuex store is only used on initial load or navigation
* Replaced dual debounce with a single 500ms debounce (fewer API calls)
* `UPDATE_ARTICLE` now merges updates instead of replacing the article
  * Prevents status changes from wiping unsaved content
* Removed `updateAsync` for a simpler update flow

### How it works

User types
→ local ref updates immediately (editor reads from this)
→ 500ms debounce triggers
→ dispatches `articles/update`
→ API persists the change
→ on success: store merges the response (used by other components)
→ editor remains unaffected (continues using local state)



Fixes
https://linear.app/chatwoot/issue/CW-6727/better-syncing-of-content-the-editor-randomly-updates-the-content

## Type of change

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

## How Has This Been Tested?

1. Open any Help Center article for editing
2. Type continuously for a few seconds — content should not reset or
jump
3. Change article status (publish/archive/draft) while editing — content
should remain intact
4. Test on a slow network (use DevTools throttling) — typing should
remain smooth


## 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
- [ ] 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-14 16:48:38 +04:00
b7b6e67df7 fix(captain): localize AI summary to account language (#13790)
AI-generated summaries now respect the account's language setting.
Previously, summaries were always returned in English regardless of the
user's configured language, making section headings like "Customer
Intent" and "Action Items" appear in English even for non-English
accounts.

Previous behavior:
<img width="1336" height="790" alt="image"
src="https://github.com/user-attachments/assets/5df8b78b-1218-438d-9578-a806b5cb94ac"
/>


Current Behavior: 
<img width="1253" height="372" alt="image"
src="https://github.com/user-attachments/assets/ae932c97-06da-4baf-9f77-9719bc9162e8"
/>


## What changed
- Added explicit account locale to the AI system prompt in
`Captain::SummaryService`
- Updated the summary prompt template to instruct the model to translate
section headings

## How to test
1. Configure an account with a non-English language (e.g., Portuguese)
2. Open a conversation with messages
3. Use the Copilot "Summarize" feature
4. Verify that section headings ("Customer Intent", "Conversation
Summary", etc.) appear in the account's language

---------

Co-authored-by: Aakash Bakhle <48802744+aakashb95@users.noreply.github.com>
2026-04-14 17:36:10 +05:30
Sivin VargheseandGitHub 288c1cb757 fix: Respect app direction for incoming email content (#14011) 2026-04-14 13:45:34 +05:30
Sivin VargheseandGitHub a8c8b38f51 fix: create article on title blur instead of debounce (#14037) 2026-04-13 23:23:25 +05:30
f422c83c26 feat: Add unified Call model for voice calling (#14026)
Adds a Call model to track voice call state across providers (Twilio,
WhatsApp). This replaces storing call data in
conversation.additional_attributes and provides a foundation for call
analytics multi-call-per-conversation support, and future voice
providers.

---------

Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
2026-04-13 20:28:09 +04:00
722e68eecb fix: validate support_email format and handle parse errors in mailer (#13958)
## Description

ConversationReplyMailer#parse_email calls
Mail::Address.new(email_string).address without error handling. When an
account's support_email contains a non-email string (e.g., "Smith
Smith"), the mail gem raises Mail::Field::IncompleteParseError, crashing
conversation transcript emails.

This has caused 1,056 errors on Sentry (EXTERNAL-CHATINC-JX) since Feb
25, all from a single account that has a name stored in the
support_email field instead of a valid email address.

Closes
https://linear.app/chatwoot/issue/CW-6687/mailfieldincompleteparseerror-mailaddresslist-can-not-parse-orsmith

## Type of change

Please delete options that are not relevant.

- [ ] 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>
Co-authored-by: Vishnu Narayanan <iamwishnu@gmail.com>
2026-04-13 19:06:06 +07:00
Tanmay Deep SharmaandGitHub 0592cccca9 fix: prevent lost custom_attributes updates from concurrent jsonb writes (#14040)
## Linear ticket
https://linear.app/chatwoot/issue/CW-6834/billing-upgrade-didnt-work

## Description
A `customer.subscription.updated` Stripe webhook for account 76162
returned 200 OK but did not persist the new `subscribed_quantity`. Root
cause: a race condition between the webhook handler and
`increment_response_usage` (Captain usage counter), both doing
read-modify-write on the `custom_attributes` JSONB column. The webhook
wrote `quantity: 6`, then a concurrent `save` from
`increment_response_usage` overwrote the entire hash with stale data —
restoring `quantity: 5`.

Fix: use atomic `jsonb_set` so usage counter updates only touch the
single key they care about, instead of rewriting the whole
`custom_attributes` hash. `increment_custom_attribute` also performs the
increment in SQL, making concurrent increments correct as well.

## Type of change

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

## How Has This Been Tested?

- New regression spec in `handle_stripe_event_service_spec.rb` that
simulates concurrent webhook + `increment_response_usage` and asserts
both `subscribed_quantity` and `captain_responses_usage` survive
- Existing account, billing, captain, and topup specs all pass 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
- [ ] Any dependent changes have been merged and published in downstream
modules
2026-04-13 19:03:37 +07:00
Sojan JoseandGitHub 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
2026-04-13 10:40:46 +05:30
Vishnu NarayananandGitHub 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
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
PranavandGitHub 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
PranavandGitHub 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
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 SharmaandGitHub 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 VargheseandGitHub bd14e96ed9 chore: allow article to create without content (#14007) 2026-04-09 10:40:37 +05:30
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
YJack0000andGitHub 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
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 MishraandGitHub 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 MishraandGitHub 871f2f4d56 fix: harden fetching on upload endpoint (#14012) 2026-04-08 10:47:54 +05:30
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
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
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
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
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
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
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
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
Tanmay Deep SharmaandGitHub 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
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 MishraandGitHub b9b5a18767 revert: html background for widget (#13981)
Reverts chatwoot/chatwoot#13955
2026-04-02 16:02:22 +05:30
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 KelothandGitHub 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
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
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 MishraandGitHub 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 VargheseandGitHub 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
Vishnu NarayananandGitHub 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
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
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 VargheseandGitHub 8824efe0e1 fix(sentry): syntaxError: No error message (#13954) 2026-03-31 21:09:02 +05:30
Sivin VargheseandGitHub 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 BakhleandGitHub 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 SharmaandGitHub 1987ac3d97 fix: remove bulk_auto_assignment_job cron schedule (#13877) 2026-03-31 10:56:59 +05:30
Sivin VargheseandGitHub 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 BakhleandGitHub 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 VargheseandGitHub 42441dbd28 feat: add GuideJar embed support in HC (#13944) 2026-03-30 14:19:02 +05:30
Alok DangreandGitHub b9f824b43b fix(ui): resolve unreadable select options in dark mode (#13207) 2026-03-30 13:05:28 +05:30
Shivam MishraandGitHub 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"
/>
2026-03-30 11:32:03 +05:30
Tanmay Deep SharmaandGitHub 04acc16609 fix: skip pay call if invoice already paid after finalize (#13924)
## Description

When a customer downgrades from Enterprise to Business, they may retain
unused Stripe credit balance. During an AI credits topup,
Stripe::Invoice.finalize_invoice auto-applies that credit balance to the
invoice. If the credit balance fully covers the invoice amount, Stripe
marks it as paid immediately upon finalization. Calling
Stripe::Invoice.pay on an already-paid invoice throws an error, breaking
the topup flow.
This fix retrieves the invoice status after finalization and skips the
pay call if Stripe has already settled it via credits.

## Type of change

Please delete options that are not relevant.

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

## How Has This Been Tested?

Tested against Stripe test mode with the following scenarios:

- Full credit balance payment: Customer has enough Stripe credit balance
to cover the entire invoice. Invoice is marked paid after
finalize_invoice — pay is correctly skipped. Credits are fulfilled
successfully.
- Partial credit balance payment: Customer has some Stripe credit
balance but not enough to cover the full amount. Invoice remains open
after finalization — pay is called and charges the remaining amount to
the default payment method. Credits are fulfilled successfully.
- Zero credit balance (normal payment): Customer has no Stripe credit
balance. Invoice remains open after finalization — pay charges the full
amount. Credits are fulfilled successfully.


## 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-03-30 10:37:28 +05:30
44a7a13117 fix: Add Estonian to settings language options (#13936)
Adds Estonian to the settings language dropdown so accounts can select
the existing `et` translation from the UI.

Fixes: N/A
Closes: N/A

## Why
Estonian translation files already exist in the repo and in Crowdin, but
the settings dropdown is driven by `LANGUAGES_CONFIG`, where `et` was
missing.

## What this change does
- Adds `et` / `Eesti (et)` to `LANGUAGES_CONFIG`
- Makes Estonian available in the settings language selectors backed by
`enabledLanguages`

## Validation
- `ruby -c config/initializers/languages.rb`
- Opened the local UI at `/app/accounts/1/settings/general` and verified
`Eesti (et)` appears in the `Site language` dropdown

---------

Co-authored-by: Pranav <pranav@chatwoot.com>
2026-03-29 09:44:34 +05:30
Tanmay Deep SharmaandGitHub 9efd554693 fix: resolve V2 capacity bypass in team assignment (#13904)
## Description

When Assignment V2 is enabled, the V2 capacity policies
(AgentCapacityPolicy / InboxCapacityLimit) are not respected during
team-based assignment paths. The system falls back to the legacy V1
max_assignment_limit, and since V1 is deprecated and typically
unconfigured in V2 setups, agents receive unlimited assignments
regardless of their V2 capacity.

Root cause: Inbox class directly defined
member_ids_with_assignment_capacity, which shadowed the
Enterprise::InboxAgentAvailability module override in Ruby's method
resolution order (MRO). This made the V2 capacity check unreachable
(dead code) for any code path using member_ids_with_assignment_capacity.

## Type of change

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

## How Has This Been Tested?

⏺ Before the fix
1. Enable assignment_v2 + advanced_assignment on account
2. Create AgentCapacityPolicy with InboxCapacityLimit = 1 for an inbox
3. Assign the policy to an agent (e.g., John)
4. Create 1 open conversation assigned to John (now at capacity)
5. Create a new unassigned conversation in the same inbox
6. Assign a team (containing John) to that conversation
7. Result: John gets assigned despite being at capacity
⏺ After the fix
Same steps 1–6.
7. Result: John is NOT assigned — conversation stays unassigned (no
agents with capacity available)


## 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-03-27 15:38:17 +05:30
Sojan JoseandGitHub 2b296c06fb chore(security): ignore CVE-2026-33658 for Chatwoot storage defaults (#13922)
This ignores `CVE-2026-33658` in `bundler-audit` after validating that
Chatwoot's default and recommended storage setups do not use Active
Storage proxy mode.

Fixes: N/A
Closes: N/A

## Why
`CVE-2026-33658` is an Active Storage proxy-mode DoS issue triggered by
multi-range requests.

For Chatwoot, the default and recommended setups do not appear to route
file downloads through Rails proxy mode:
- `config/environments/production.rb` selects the Active Storage service
but does not opt into `rails_storage_proxy`
- `.env.example` defaults to `ACTIVE_STORAGE_SERVICE=local`
- Chatwoot's storage docs recommend local/cloud storage with optional
direct uploads to the storage provider
- existing specs expect redirect/disk-style Active Storage URLs rather
than proxy-mode URLs

Given that validation, ignoring this advisory is a smaller and more
accurate response than a framework-wide Rails upgrade.

## What this change does
- adds `.bundler-audit.yml`
- preserves the existing advisory ignore entries already used by
Chatwoot
- ignores `CVE-2026-33658`
- documents why the ignore is acceptable for Chatwoot's current defaults
- notes that this should be revisited if Chatwoot enables
`rails_storage_proxy` or other app-served Active Storage proxy routes

## Validation
- reviewed `config/environments/production.rb`
- reviewed `.env.example`
- reviewed Chatwoot storage docs:
https://developers.chatwoot.com/self-hosted/deployment/storage/s3-bucket
- reviewed Active Storage URL expectations in
`spec/controllers/slack_uploads_controller_spec.rb` and
`spec/services/line/send_on_line_service_spec.rb`
- ran `bundle exec bundle-audit check --no-update`
2026-03-27 13:06:17 +05:30
4381be5f3e feat: disable helpcenter on hacker plans (#12068)
This change blocks Help Center access for default/Hacker-plan accounts
and closes the downgrade gap that could leave `help_center` enabled
after a subscription falls back to the default cloud plan.

Fixes: none
Closes: none

## Why

Default-plan accounts should not be able to access the Help Center, but
the downgrade fallback path only reset the plan name and did not
reconcile premium feature flags. That meant some accounts could keep
`help_center` enabled even after landing back on the Hacker/default
plan.

## What this change does

- blocks Help Center portal and article access for default/Hacker-plan
accounts
- reconciles premium feature flags when a subscription falls back to the
default cloud plan, so `help_center` is disabled immediately instead of
waiting for a later webhook
- preserves existing account `custom_attributes` during Stripe customer
recreation instead of overwriting them
- adds Enterprise coverage for the default-plan access checks on hosted
and custom-domain Help Center routes
- fixes the public access check to use the resolved portal object so
blocked requests return the intended response instead of raising an
error

## Validation

1. Create or use an account on the default/Hacker cloud plan with an
active portal.
2. Visit the portal home page and a published article on both the
Chatwoot-hosted URL and a configured custom domain.
3. Confirm the Help Center is blocked for that account.
4. Downgrade a paid account back to the default/Hacker plan through the
Stripe webhook flow.
5. Confirm `help_center` is disabled right after the downgrade fallback
is processed and the account can no longer access the Help Center.

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Sojan Jose <sojan@pepalo.com>
2026-03-26 23:48:46 -07:00
Shivam MishraandGitHub 127ac0a6b2 fix: show backend error message on API channel creation failure (#13855) 2026-03-27 11:42:33 +05:30
Sivin VargheseandGitHub 5d9d754961 chore(editor): Auto-linkify URLs immediately on paste (#13900)
# Pull Request Template

## Description

This PR upgrades the ProseMirror editor and enables automatic URL
linkification on paste. Previously, URLs were only linkified after a
user input event (e.g., typing a space). With this change, URLs are now
linkified instantly when pasted.

Fixes
https://linear.app/chatwoot/issue/CW-6682/email-channel-links-are-not-working

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

## Type of change

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

## How Has This Been Tested?

**Screencast**

**Before**


https://github.com/user-attachments/assets/d38725c9-a152-4c2c-8c33-3ee717f1628f



**After**


https://github.com/user-attachments/assets/9a69a0b6-93ee-421e-896b-5a4e01a167ba


## 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-27 11:29:54 +05:30
cac7438fff fix: Email Channel links are not working (backend) (#13898)
# Pull Request Template

## Description

This PR fixes the link formatting issue on the backend by adding
`:autolink` to `ChatwootMarkdownRenderer#render_message`, ensuring all
URLs are converted to `<a>` tags.

Fixes
https://linear.app/chatwoot/issue/CW-6682/email-channel-links-are-not-working

## Type of change

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


## 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: Sojan Jose <sojan@pepalo.com>
2026-03-26 21:44:57 -07:00
Haruma HIRABAYASHIandGitHub 0b41d7f483 docs(swagger): fix operationId typo converation -> conversation (#13920)
issue: https://github.com/chatwoot/chatwoot/issues/13921

Fix a typo in the Messages API operationId where `converation` was used
instead of `conversation`. This causes cspell errors when generating
client code with tools like Orval.

## What changed

- `swagger/paths/public/inboxes/messages/index.yml`: fixed operationId
from `list-all-converation-messages` to `list-all-conversation-messages`
- `swagger/swagger.json` and `swagger/tag_groups/client_swagger.json`:
regenerated to reflect the fix


## Note

If you are using a code generator like Orval against this swagger spec,
the generated function name will change from
`listAllConverationMessages` to `listAllConversationMessages`.
2026-03-27 09:23:55 +05:30
Sivin VargheseandGitHub 4517c50227 feat: support bulk select and delete for documents (#13907) 2026-03-26 19:48:12 +05:30
Vishnu NarayananandGitHub 4c4b70da25 fix: Skip email rate limiting for self-hosted instances (#13915)
Self-hosted installations were incorrectly hitting the daily email rate
limit of 100, seeded from `installation_config`. Since self-hosted users
control their own infrastructure, email rate limiting should only apply
to Chatwoot Cloud.

Closes #13913
2026-03-26 18:06:10 +05:30
Tanmay Deep SharmaandGitHub 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)
2026-03-26 15:00:09 +05:30
Sivin VargheseandGitHub 23786bcb52 chore: mark conversation notifications as read on visit (#13906) 2026-03-26 14:01:26 +05:30
Shivam MishraandGitHub 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
2026-03-26 12:32:27 +05:30
742c5cc1f4 feat(dialogflow): make language_code configurable instead of hardcoded (#13221)
# Pull Request Template

## Description

Please include a summary of the change and issue(s) fixed. Also, mention
relevant motivation, context, and any dependencies that this change
requires.
- Add language_code setting to Dialogflow integration configuration
- Support 'auto' mode to detect language from contact's
additional_attributes
- Fallback to 'en-US' when no language is configured or detected
- Include comprehensive language options (22 languages)
- Add tests for language code configuration scenarios

Fixes #3071

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

<img width="815" height="506" alt="Screenshot 2026-01-10 220410"
src="https://github.com/user-attachments/assets/26d2619c-ed42-4c9a-a41d-9fb07ef91a30"
/>


## 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: Sojan Jose <sojan@pepalo.com>
2026-03-25 21:30:17 -07:00
d9e732c005 chore(v5): update priority icons (#13905)
# Pull Request Template

## Description

This PR updates the priority icons with a new set and makes them
consistent across the app.


## How Has This Been Tested?

**Screenshots**
<img width="420" height="550" alt="image"
src="https://github.com/user-attachments/assets/cb392934-6c4d-46b4-9fde-244461da62ef"
/>
<img width="358" height="340" alt="image"
src="https://github.com/user-attachments/assets/cb18df47-9a17-42f8-9367-e8b7c4e3958d"
/>
<img width="344" height="468" alt="image"
src="https://github.com/user-attachments/assets/9de92374-e732-48eb-a8a9-85c5b5100931"
/>
<img width="445" height="548" alt="image"
src="https://github.com/user-attachments/assets/ecc4ce51-165c-4593-a9a2-e70b08a29006"
/>


## 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: Pranav <pranav@chatwoot.com>
2026-03-26 09:20:36 +05:30
e0e321b8e2 fix: Annotaterb model annotation incomplete migration (#13132)
This pull request fixes the model annotation tooling due to previous
incomplete migration from `annotate` to `annotaterb` gem (#12600). It
also improves the handling of serialized values in the
`InstallationConfig` model by ensuring a default value is set,
simplifying the code, and removing a workaround for YAML
deserialization.

**Annotation tooling updates:**

* Added `.annotaterb.yml` to configure the `annotate_rb` gem with
project-specific options, centralizing annotation settings.
* Replaced the custom `auto_annotate_models.rake` task with the standard
rake task from `annotate_rb`, and added `lib/tasks/annotate_rb.rake` to
load annotation tasks in development environments.
[[1]](diffhunk://#diff-9450d2359e45f1db407b3871dde787a25d60bb721aed179a65ffd2692e95fb4bL1-L61)
[[2]](diffhunk://#diff-578cdfc7ad56637e42472ea891ea286dff8803d9a1750afdbfeafec164d9b8b2R1-R8)

**Model serialization improvements:**

* Updated the `InstallationConfig` model to set a default value for the
`serialized_value` attribute, ensuring it always has a hash with
indifferent access and removing the need for a deserialization
workaround in the `value` method.
[[1]](diffhunk://#diff-b4bdde42c1ad0f584073818bd43dbd865b1b3b50d4701b131979f900d7c68297L22-R22)
[[2]](diffhunk://#diff-b4bdde42c1ad0f584073818bd43dbd865b1b3b50d4701b131979f900d7c68297L36-L39)

---------

Co-authored-by: Sojan Jose <sojan@pepalo.com>
2026-03-25 17:51:06 -07:00
Sojan Jose ecc66e064d Merge branch 'release/4.12.1' into develop 2026-03-25 16:21:38 -07:00
Sojan Jose 7144d55334 Bump version to 4.12.1 2026-03-25 16:20:58 -07:00
250650dd7a feat(platform): Add email channel migration endpoint for bulk OAuth channel creation (#13902)
Adds a Platform API endpoint that allows migrating existing Google and
Microsoft email channels (with OAuth credentials) into Chatwoot without
requiring end-users to re-authenticate. This enables customers who lack
Rails console access to programmatically migrate email channels from
legacy systems.
    
 ### How to test

1. Create a Platform App and grant it permissible access to a target
account
2. `POST /platform/api/v1/accounts/:account_id/email_channel_migrations`
with a payload like:
  ```json
  {
    "migrations": [
      {
        "email": "support@example.com",
        "provider": "google",
        "provider_config": {
          "access_token": "...",
          "refresh_token": "...",
          "expires_on": "..."
        },
        "inbox_name": "Migrated Support"
      }
    ]
  }
```
  3. Verify channels are created with correct provider, provider_config, and IMAP defaults
  4. Verify partial failures (e.g. duplicate email) don't roll back other migrations in the batch
  5. Verify unauthenticated and non-permissible requests return 401

---------

Co-authored-by: Sojan Jose <sojan@pepalo.com>
2026-03-25 15:58:08 -07:00
608be1036b fix: Send raw content in webhook payloads instead of channel-rendered markdown (#13896)
Webhook payloads (`message_created`, `message_updated`) started sending
channel-rendered HTML in the `content` field instead of the original raw
message content after PR #12878. This broke downstream agent bots and
integrations that expected plain text or markdown. 
Closes https://linear.app/chatwoot/issue/PLA-109/webhook-payloads-send-channel-rendered-html-instead-of-raw-content

## How to reproduce

1. Connect an agent bot to a WebWidget inbox
2. Send a message with markdown formatting (e.g. `**bold**`) from the
widget
3. Observe the agent bot webhook payload — `content` field contains
`<p><strong>bold</strong></p>` instead of `**bold**`

## What changed

Split `MessageContentPresenter` into two public methods:
- `outgoing_content` — renders markdown for the target channel (used by
channel delivery services)
- `webhook_content` — returns raw content with CSAT survey URL when
applicable, no markdown rendering (used by `webhook_data`)

Updated `Message#webhook_data` to use `webhook_content` instead of
`outgoing_content`.

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

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
2026-03-25 16:56:22 +04:00
salmonumbrellaandGitHub 6ff643b045 fix(i18n): add zh_TW snooze parser locale (#13822) 2026-03-25 16:54:18 +05:30
Vishnu NarayananandGitHub 775b73d1f9 fix: raise open file descriptor limit to prevent EMFILE errors (#13895)
## Summary
- Adds `LimitNOFILE=65536` to both web and worker systemd service units
- Fixes recurring `Errno::EMFILE` (Too many open files) errors during
peak traffic after deploys

## Context
Puma workers idle at 720-770 open FDs against the default soft limit of
1024, leaving ~250 FDs of headroom. During deploy-triggered instance
refreshes at peak traffic, concurrent requests exhaust the remaining
FDs, causing EMFILE across all web instances.

3 incidents in March 2026 with escalating event counts. The hard limit
is already 524288, so this just raises the soft limit to a standard
production value.

Self-hosted instances pick this up automatically via `cwctl --upgrade`.

Fixes
https://linear.app/chatwoot/issue/CW-6685/errnoemfile-too-many-open-files-rb-sysopen
2026-03-24 17:37:07 -07:00
14df7b3bc1 fix: ai-assist 404 on CE (#13891)
# Pull Request Template

## Description

Relocate controller from enterprise/ to app/ and add
Api::V1::Accounts::Captain::TasksController.prepend_mod_with for EE
overrides.

Fixes: Ai assist giving 404 on CE

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

before:
<img width="482" height="130" alt="image"
src="https://github.com/user-attachments/assets/f51dc28a-ac54-45c4-9015-6f956fdf5057"
/>

after:
<img width="458" height="182" alt="image"
src="https://github.com/user-attachments/assets/eb86a679-5482-4157-9f4e-f3e9953d8649"
/>


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

Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
2026-03-24 16:58:11 +05:30
Sivin VargheseandGitHub 6946859ba4 fix: normalize "in less than a minute" to "now" in chat list timestamp (#13874)
# Pull Request Template

## Description

This PR fixes the conversation list showing raw "**in less than a
minute**" text instead of "**now**" for very recent conversations.

Fixes https://linear.app/chatwoot/issue/CW-6666/issue-with-timestamps

## Type of change

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


## 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-24 16:16:35 +05:30
Sivin VargheseandGitHub c129ab00ba fix: normalize "in less than a minute" to "now" in chat list timestamp (#13874) 2026-03-24 15:40:31 +05:30
7edae93ee8 fix(agent-bot): Include payload in webhook retry failure logs (#13879)
Webhook retry failure logs for agent-bot now include the event payload,
making it easier to identify which event failed when debugging transient
upstream errors (429/500).

Previously the log only showed:
`[AgentBots::WebhookJob] attempt 1 failed
RestClient::InternalServerError`

Now it includes the payload:
`[AgentBots::WebhookJob] attempt 1 failed
RestClient::InternalServerError payload={"event":"message_created",...}`

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 10:52:37 +04:00
4b315bc2ec chore: Update translations (#13884)
Co-authored-by: Sojan Jose <sojan@pepalo.com>
2026-03-23 20:06:17 -07:00
Sivin VargheseandGitHub 30c0479e9a fix: show agent name in unread bubble for Captain replies (#13876) 2026-03-23 20:03:31 +05:30
Aakash BakhleandGitHub 3c0d55f87a fix: handoff only if conversation pending (#13882)
# Pull Request Template

## Description

- Skip handoff when the conversation is no longer pending. 
- Fetch conversation status with Conversation.uncached to avoid stale
query cache when checking pending state.

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

Difficult to reproduce locally, but seen a couple of conversations where
bot hands off an extra time or Captain re-generates usually due to a
retry on FaradayTimeoutError but at perform time, the conversation
status is stale so it appears as if Captain responded in an open
conversation.

## 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-23 17:09:58 +05:30
Aakash BakhleandGitHub 4af3e830fc fix: conversation completion prompt to auto-resolve gibberish/no-intent messages after inactivity (#13875)
# Pull Request Template

## Description

For our account, the conversation completion evaluator was proving to be
too conservative. This resulted in queue noise.
This PR adds a line to handle gibberish/single worded messages with no
further context.

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

<img width="3012" height="978" alt="CleanShot 2026-03-23 at 11 44 55@2x"
src="https://github.com/user-attachments/assets/328195e8-6ea0-4c3a-9049-ee80196eecad"
/>

<img width="2202" height="866" alt="CleanShot 2026-03-23 at 11 47 15@2x"
src="https://github.com/user-attachments/assets/8d51cff4-b18f-4582-9455-8119ec7eff3a"
/>


## 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-23 12:18:23 +05:30
b974993886 chore: Update translations (#13845)
Co-authored-by: Sojan Jose <sojan@pepalo.com>
2026-03-21 02:20:27 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Sojan Jose
4b849cdd11 chore(deps): bump bcrypt from 3.1.20 to 3.1.22 (#13852)
Bumps [bcrypt](https://github.com/bcrypt-ruby/bcrypt-ruby) from 3.1.20
to 3.1.22.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/bcrypt-ruby/bcrypt-ruby/releases">bcrypt's
releases</a>.</em></p>
<blockquote>
<h2>v3.1.22</h2>
<h2>What's Changed</h2>
<ul>
<li>Move compilation after bundle install by <a
href="https://github.com/tenderlove"><code>@​tenderlove</code></a> in <a
href="https://redirect.github.com/bcrypt-ruby/bcrypt-ruby/pull/291">bcrypt-ruby/bcrypt-ruby#291</a></li>
<li>Add TruffleRuby in CI by <a
href="https://github.com/tjschuck"><code>@​tjschuck</code></a> in <a
href="https://redirect.github.com/bcrypt-ruby/bcrypt-ruby/pull/293">bcrypt-ruby/bcrypt-ruby#293</a></li>
<li>fix env url by <a
href="https://github.com/tenderlove"><code>@​tenderlove</code></a> in <a
href="https://redirect.github.com/bcrypt-ruby/bcrypt-ruby/pull/294">bcrypt-ruby/bcrypt-ruby#294</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/bcrypt-ruby/bcrypt-ruby/compare/v3.1.21...v3.1.22">https://github.com/bcrypt-ruby/bcrypt-ruby/compare/v3.1.21...v3.1.22</a></p>
<h2>v3.1.21</h2>
<h2>What's Changed</h2>
<ul>
<li>Provide a 'Changelog' link on rubygems.org/gems/bcrypt by <a
href="https://github.com/mark-young-atg"><code>@​mark-young-atg</code></a>
in <a
href="https://redirect.github.com/bcrypt-ruby/bcrypt-ruby/pull/274">bcrypt-ruby/bcrypt-ruby#274</a></li>
<li>Support ruby 3.3 and 3.4.0-preview1 by <a
href="https://github.com/m-nakamura145"><code>@​m-nakamura145</code></a>
in <a
href="https://redirect.github.com/bcrypt-ruby/bcrypt-ruby/pull/276">bcrypt-ruby/bcrypt-ruby#276</a></li>
<li>Mark as ractor-safe by <a
href="https://github.com/mohamedhafez"><code>@​mohamedhafez</code></a>
in <a
href="https://redirect.github.com/bcrypt-ruby/bcrypt-ruby/pull/280">bcrypt-ruby/bcrypt-ruby#280</a></li>
<li>Add == gotcha that can be unintuitive at first by <a
href="https://github.com/federicoaldunate"><code>@​federicoaldunate</code></a>
in <a
href="https://redirect.github.com/bcrypt-ruby/bcrypt-ruby/pull/279">bcrypt-ruby/bcrypt-ruby#279</a></li>
<li>Constant compare by <a
href="https://github.com/tenderlove"><code>@​tenderlove</code></a> in <a
href="https://redirect.github.com/bcrypt-ruby/bcrypt-ruby/pull/282">bcrypt-ruby/bcrypt-ruby#282</a></li>
<li>try to modernize CI by <a
href="https://github.com/tenderlove"><code>@​tenderlove</code></a> in <a
href="https://redirect.github.com/bcrypt-ruby/bcrypt-ruby/pull/287">bcrypt-ruby/bcrypt-ruby#287</a></li>
<li>Try to deal with flaky tests by <a
href="https://github.com/tenderlove"><code>@​tenderlove</code></a> in <a
href="https://redirect.github.com/bcrypt-ruby/bcrypt-ruby/pull/288">bcrypt-ruby/bcrypt-ruby#288</a></li>
<li>Configure trusted publishing by <a
href="https://github.com/tenderlove"><code>@​tenderlove</code></a> in <a
href="https://redirect.github.com/bcrypt-ruby/bcrypt-ruby/pull/289">bcrypt-ruby/bcrypt-ruby#289</a></li>
<li>bump version by <a
href="https://github.com/tenderlove"><code>@​tenderlove</code></a> in <a
href="https://redirect.github.com/bcrypt-ruby/bcrypt-ruby/pull/290">bcrypt-ruby/bcrypt-ruby#290</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/mark-young-atg"><code>@​mark-young-atg</code></a>
made their first contribution in <a
href="https://redirect.github.com/bcrypt-ruby/bcrypt-ruby/pull/274">bcrypt-ruby/bcrypt-ruby#274</a></li>
<li><a
href="https://github.com/m-nakamura145"><code>@​m-nakamura145</code></a>
made their first contribution in <a
href="https://redirect.github.com/bcrypt-ruby/bcrypt-ruby/pull/276">bcrypt-ruby/bcrypt-ruby#276</a></li>
<li><a
href="https://github.com/mohamedhafez"><code>@​mohamedhafez</code></a>
made their first contribution in <a
href="https://redirect.github.com/bcrypt-ruby/bcrypt-ruby/pull/280">bcrypt-ruby/bcrypt-ruby#280</a></li>
<li><a
href="https://github.com/federicoaldunate"><code>@​federicoaldunate</code></a>
made their first contribution in <a
href="https://redirect.github.com/bcrypt-ruby/bcrypt-ruby/pull/279">bcrypt-ruby/bcrypt-ruby#279</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/bcrypt-ruby/bcrypt-ruby/compare/v3.1.20...v3.1.21">https://github.com/bcrypt-ruby/bcrypt-ruby/compare/v3.1.20...v3.1.21</a></p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/bcrypt-ruby/bcrypt-ruby/blob/master/CHANGELOG">bcrypt's
changelog</a>.</em></p>
<blockquote>
<p>3.1.22 Mar 18 2026</p>
<ul>
<li>[CVE-2026-33306] Fix integer overflow in Java extension</li>
</ul>
<p>3.1.21 Dec 31 2025</p>
<ul>
<li>Use constant time comparisons</li>
<li>Mark as Ractor safe</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/bcrypt-ruby/bcrypt-ruby/commit/831ce64cb0a9502130fa93a28bfd9527a5fa45c4"><code>831ce64</code></a>
Merge commit from fork</li>
<li><a
href="https://github.com/bcrypt-ruby/bcrypt-ruby/commit/32e687ec5f62baad01a62e4634e41d97f8432a61"><code>32e687e</code></a>
bump version update changelog</li>
<li><a
href="https://github.com/bcrypt-ruby/bcrypt-ruby/commit/5faa2748331d3edc661c127ef2fbb3afcb6b02a4"><code>5faa274</code></a>
Fix integer overflow in JRuby BCrypt rounds calculation</li>
<li><a
href="https://github.com/bcrypt-ruby/bcrypt-ruby/commit/aafc0332ac1aa0d774f2c864439596436f92d18d"><code>aafc033</code></a>
Merge pull request <a
href="https://redirect.github.com/bcrypt-ruby/bcrypt-ruby/issues/294">#294</a>
from bcrypt-ruby/fix-publishing</li>
<li><a
href="https://github.com/bcrypt-ruby/bcrypt-ruby/commit/01f947a66ad8c5e20d8c89d9adbc7e3bd49afb70"><code>01f947a</code></a>
fix env url</li>
<li><a
href="https://github.com/bcrypt-ruby/bcrypt-ruby/commit/92ca1d67deeb8e64dbe779396c52b177e307bc43"><code>92ca1d6</code></a>
Merge pull request <a
href="https://redirect.github.com/bcrypt-ruby/bcrypt-ruby/issues/293">#293</a>
from bcrypt-ruby/truffleruby-ci-alt-implementation</li>
<li><a
href="https://github.com/bcrypt-ruby/bcrypt-ruby/commit/4d1d95b8ec624d0cf8ed1099402a7edd2f308da2"><code>4d1d95b</code></a>
Add TruffleRuby in CI</li>
<li><a
href="https://github.com/bcrypt-ruby/bcrypt-ruby/commit/36a04a2278fae3b38100912ff489b86cd0984b8a"><code>36a04a2</code></a>
Merge pull request <a
href="https://redirect.github.com/bcrypt-ruby/bcrypt-ruby/issues/291">#291</a>
from tenderlove/fix-publishing</li>
<li><a
href="https://github.com/bcrypt-ruby/bcrypt-ruby/commit/01cc68835f0bcdd7ef16de477471c112adb417da"><code>01cc688</code></a>
Move compilation after bundle install</li>
<li><a
href="https://github.com/bcrypt-ruby/bcrypt-ruby/commit/82e6c4c6cf81912768c68d721372e78330ff2c92"><code>82e6c4c</code></a>
Merge pull request <a
href="https://redirect.github.com/bcrypt-ruby/bcrypt-ruby/issues/290">#290</a>
from tenderlove/bump</li>
<li>Additional commits viewable in <a
href="https://github.com/bcrypt-ruby/bcrypt-ruby/compare/v3.1.20...v3.1.22">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=bcrypt&package-manager=bundler&previous-version=3.1.20&new-version=3.1.22)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/chatwoot/chatwoot/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Sojan Jose <sojan@pepalo.com>
2026-03-20 16:30:50 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Sojan Jose
310590cae3 chore(deps): bump json from 2.18.1 to 2.19.2 (#13849)
Bumps [json](https://github.com/ruby/json) from 2.18.1 to 2.19.2.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/ruby/json/releases">json's
releases</a>.</em></p>
<blockquote>
<h2>v2.19.2</h2>
<h2>What's Changed</h2>
<ul>
<li>Fix a format string injection vulnerability in <code>JSON.parse(doc,
allow_duplicate_key: false)</code>. <code>CVE-2026-33210</code></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/ruby/json/compare/v2.19.1...v2.19.2">https://github.com/ruby/json/compare/v2.19.1...v2.19.2</a></p>
<h2>v2.19.1</h2>
<h2>What's Changed</h2>
<ul>
<li>Fix a compiler dependent GC bug introduced in
<code>2.18.0</code>.</li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/ruby/json/compare/v2.19.0...v2.19.1">https://github.com/ruby/json/compare/v2.19.0...v2.19.1</a></p>
<h2>v2.19.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Fix <code>allow_blank</code> parsing option to no longer allow
invalid types (e.g. <code>load([], allow_blank: true)</code> now raise a
type error).</li>
<li>Add <code>allow_invalid_escape</code> parsing option to ignore
backslashes that aren't followed by one of the valid escape
characters.</li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/ruby/json/compare/v2.18.1...v2.19.0">https://github.com/ruby/json/compare/v2.18.1...v2.19.0</a></p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/ruby/json/blob/master/CHANGES.md">json's
changelog</a>.</em></p>
<blockquote>
<h3>2026-03-18 (2.19.2)</h3>
<ul>
<li>Fix a format string injection vulnerability in <code>JSON.parse(doc,
allow_duplicate_key: false)</code>. <code>CVE-2026-33210</code>.</li>
</ul>
<h3>2026-03-08 (2.19.1)</h3>
<ul>
<li>Fix a compiler dependent GC bug introduced in
<code>2.18.0</code>.</li>
</ul>
<h3>2026-03-06 (2.19.0)</h3>
<ul>
<li>Fix <code>allow_blank</code> parsing option to no longer allow
invalid types (e.g. <code>load([], allow_blank: true)</code> now raise a
type error).</li>
<li>Add <code>allow_invalid_escape</code> parsing option to ignore
backslashes that aren't followed by one of the valid escape
characters.</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/ruby/json/commit/54f8a878aebee090476a53c851c943128894be62"><code>54f8a87</code></a>
Release 2.19.2</li>
<li><a
href="https://github.com/ruby/json/commit/393b41c3e5f87491e1e34fa59fa78ff6fa179a74"><code>393b41c</code></a>
Fix a format string injection vulnerability</li>
<li><a
href="https://github.com/ruby/json/commit/dbf6bb12aac85db939df1180028aea06c8d3b762"><code>dbf6bb1</code></a>
Merge pull request <a
href="https://redirect.github.com/ruby/json/issues/953">#953</a> from
ruby/dependabot/github_actions/actions/create-gi...</li>
<li><a
href="https://github.com/ruby/json/commit/7187315b4571ade59d68a1fad84be2794cda744d"><code>7187315</code></a>
Bump actions/create-github-app-token from 2 to 3</li>
<li><a
href="https://github.com/ruby/json/commit/4a42a04280d96d8dd94558078c16f1c078c38e1b"><code>4a42a04</code></a>
Release 2.19.1</li>
<li><a
href="https://github.com/ruby/json/commit/13689c269970f18316952541f8544830ec2dc5c4"><code>13689c2</code></a>
Add missing GC_GUARD in <code>fbuffer_append_str</code></li>
<li><a
href="https://github.com/ruby/json/commit/a11acc1ff496627e5d72c71d6d1229e8c8ffeaa1"><code>a11acc1</code></a>
Release 2.19.0</li>
<li><a
href="https://github.com/ruby/json/commit/0a4fb79cd97f535701cc2240ac736d76b9af5025"><code>0a4fb79</code></a>
fbuffer.h: Use size_t over unsigned long</li>
<li><a
href="https://github.com/ruby/json/commit/a29fcdcb4a78164daa14f6af05812690dd3ac939"><code>a29fcdc</code></a>
Add depth validation to Jruby and TruffleRuby implementations</li>
<li><a
href="https://github.com/ruby/json/commit/de993aa76639078da891f46351a36f77d51ad3d3"><code>de993aa</code></a>
Reject negative depth; add overflow guards to prevent hang/crash</li>
<li>Additional commits viewable in <a
href="https://github.com/ruby/json/compare/v2.18.1...v2.19.2">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=json&package-manager=bundler&previous-version=2.18.1&new-version=2.19.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/chatwoot/chatwoot/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Sojan Jose <sojan@pepalo.com>
2026-03-20 16:30:35 -07:00
Sivin VargheseandGitHub 251e9980fd chore: Auto-focus editor when replying to a message (#13857)
# Pull Request Template

## Description

This PR adds support to auto-focus the editor when clicking reply to
this message, the editor now automatically receives focus so users can
start typing immediately.

Fixes
https://linear.app/chatwoot/issue/CW-6661/typing-box-not-focused-after-clicking-reply-to-message

## Type of change


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

## How Has This Been Tested?

### Screencast


https://github.com/user-attachments/assets/c5e77055-3f68-4ad8-934e-cfc465166e8a




## 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-20 16:59:27 +05:30
Tanmay Deep SharmaandGitHub 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
2026-03-20 16:28:05 +05:30
Aakash BakhleandGitHub 290dd3abf5 feat: allow captain to access contact attributes (#13850)
# Pull Request Template

## Description

Captain v1 does not have access to contact attributes. Added a toggle to
let user choose if they want contact information available to Captain.

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

Specs and locally
<img width="1924" height="740" alt="CleanShot 2026-03-19 at 18 48 19@2x"
src="https://github.com/user-attachments/assets/353cfeaa-cd58-40eb-89e7-d660a1dc1185"
/>

![Uploading CleanShot 2026-03-19 at 18.53.26@2x.png…]()


## 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-20 16:15:06 +05:30
a9123e7d66 chore(i18n): add missing pt_BR locale imports for companies, mfa, snooze, webhooks and more (#13844)
## Description

Add missing JSON imports and spread exports for `companies`,
`contentTemplates`, `mfa`, `snooze`, `webhooks`, and `yearInReview` so
these translations are properly loaded in the pt_BR locale. Without
these imports, those sections of the UI were falling back to English for
Brazilian Portuguese users.

## Type of change

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

## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] My changes generate no new warnings

Co-authored-by: Sojan Jose <sojan@pepalo.com>
2026-03-19 01:27:37 -07:00
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>
2026-03-19 13:12:36 +05:30
654fcd43f2 docs(swagger): fix public API schema definitions to match jbuilder responses (#13693)
## Description

This PR updates the OpenAPI schema definitions for Public API resources
(`public_conversation`, `public_message`, `public_contact`) so they
match the actual API responses produced by the jbuilder views.

These definitions were introduced in #2417 (2021-06) with a minimal set
of fields. The jbuilder views have since been updated (e.g. `uuid` in
#7255, `agent_last_seen_at` in #4377), but the Swagger definitions were
never updated. As a result, generated API clients get incorrect or
missing types. This change fixes that by aligning the schemas with the
implementation.

**Fixes #13692**

## 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)
- [x] This change requires a documentation update

## How Has This Been Tested?

- Compared each jbuilder view
(`app/views/public/api/v1/models/_conversation.json.jbuilder`,
`_message.json.jbuilder`, `_contact.json.jbuilder`) field-by-field
against the Swagger YAML definitions.
- Cross-referenced Ruby model enums (`Conversation.status`,
`Attachment.file_type`, `Message.message_type`) for enum values.
- Ran the swagger build (via the project’s `rake swagger:build` logic /
`json_refs` resolution) to regenerate `swagger.json` and tag group
files; confirmed the generated schemas contain the correct fields and
types.
- No runtime tests were run; this is a documentation/schema-only change.

## 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 *(N/A: schema definitions)*
- [x] 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 *(N/A: documentation/schema only)*
- [ ] New and existing unit tests pass locally with my changes *(N/A: no
code under test)*
- [x] Any dependent changes have been merged and published in downstream
modules *(N/A: none)*

---

## Change summary (for reference)

### `public_conversation`
- Added missing fields: `uuid`, `status`, `contact_last_seen_at`,
`agent_last_seen_at`
- Fixed `inbox_id` type from `string` to `integer`
- Fixed `messages` items `$ref` from `message` to `public_message`
- Added property details for embedded `contact` object (`id`, `name`,
`email`, `phone_number`)
- Added `status` enum: `open`, `resolved`, `pending`, `snoozed`

### `public_message`
- Fixed `id`, `message_type`, `created_at`, `conversation_id` types
(string → integer where applicable)
- Fixed `content_attributes` type from `string` to `object`
- Added property details for `attachments` items and `sender` object
- Added `file_type` and `sender.type` enums

### `public_contact`
- Added missing `phone_number` field

---------

Co-authored-by: Sojan Jose <sojan@pepalo.com>
2026-03-19 00:03:37 -07:00
Shivam MishraandGitHub 284977687c fix: patch Devise confirmable race condition vulnerability (#13843)
Devise 4.9.x has a race condition in the reconfirmable flow where
concurrent email change requests can desynchronize the confirmation
token from `unconfirmed_email`, letting an attacker confirm an email
they don't own. We use `:confirmable` with `reconfirmable = true`, so
we're directly exposed.

The upstream fix is in Devise 5.0.3, but we can't upgrade —
`devise-two-factor` only supports Devise 5 from v6.4.0, which also
raised its Rails minimum to 7.2+. No released version supports both
Devise 5 and Rails 7.1.

This PR ports the Devise 5.0.3 fix locally by overriding
`postpone_email_change_until_confirmation_and_regenerate_confirmation_token`
on the User model to persist the record before regenerating the token.
This is a stopgap — remove it once the dependency chain allows upgrading
to Devise 5.

### How to test

Sign in as a confirmed user and change your email. The app should send a
confirmation to the new address while keeping the current email
unchanged until confirmed.
2026-03-18 21:30:09 -07:00
Sojan Jose 18dc77aa56 Merge branch 'release/4.12.0' into develop 2026-03-17 16:23:16 -07:00
Sojan Jose 8aad8ad38e Bump version to 4.12.0 2026-03-17 16:19:39 -07:00
098f7a77b6 chore: Update translations (#13832)
Co-authored-by: Sojan Jose <sojan@pepalo.com>
2026-03-17 16:02:40 -07:00
msaleh-313andGitHub 9c22d791c4 fix: return correct outgoing_url in Platform agent bot API responses (#13827)
The Platform API was returning the bot's `name` value for the
`outgoing_url` field across all agent bot endpoints (index, show,
create, update). A typo in the `_agent_bot.json.jbuilder` partial used
`resource.name` instead of `resource.outgoing_url`.

Closes #13787

## What changed

- `app/views/platform/api/v1/models/_agent_bot.json.jbuilder`: corrected
`resource.name` → `resource.outgoing_url` on the `outgoing_url` field.

## How to reproduce

1. Create an agent bot via `POST /platform/api/v1/agent_bots` with a
distinct `outgoing_url`.
2. Call `GET /platform/api/v1/agent_bots/:id`.
3. Before this fix the `outgoing_url` in the response equals the bot's
`name`; after the fix it equals the value set at creation.

## Tests

Added `outgoing_url` assertions to the existing GET index and GET show
request specs so the regression is covered.
2026-03-17 13:40:45 -07:00
Tanmay Deep SharmaandGitHub 4d344a47dc chore(tds-1): rake task for assignment v2 migration (#13828) 2026-03-17 20:35:03 +05:30
Aakash BakhleandGitHub 38dbda9378 fix: reverse order of api_key for bg task (#13826)
# Pull Request Template

## Description

we were getting 403, 401 errors on `translate_query` on langfuse and
sentry
This happened because, we use the customer's openai key if they have
BYOK
But translation is something they never opt in so we should not use
their quota for it.
This PR addresses the issue.

## 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-17 17:36:40 +05:30
a4c3d3d8c0 feat(widget): Allow widget loading in mobile app WebViews when domain restrictions are set (#13763)
When `allowed_domains` is configured on a web widget inbox, the server
responds with Content-Security-Policy: frame-ancestors <domains>, which
blocks the widget iframe in mobile app WebViews. This happens because
WebViews load content from file:// or null origins, which cannot match
any domain in the frame-ancestors directive.

This adds a per-inbox toggle — "Enable widget in mobile apps" — that
skips the frame-ancestors header when the request has no valid Origin
(i.e., it comes from a mobile WebView). Web browsers with a real origin
still get domain restrictions enforced as usual.

<img width="2330" height="1490" alt="CleanShot 2026-03-11 at 10 13
01@2x"
src="https://github.com/user-attachments/assets/d9326fac-020d-4ce7-9ced-0c185468c8fc"
/>


Fixes
https://linear.app/chatwoot/issue/CW-6560/widget-is-not-loading-from-iosandroid-widgets

How to test

1. Go to Settings → Inboxes → (Web Widget) → Configuration
2. Set allowed_domains to a specific domain (e.g., *.example.com)
3. Try loading the widget in a mobile app WebView — it should be blocked
4. Enable "Enable widget in mobile apps" checkbox
5. Reload the widget in the WebView — it should now load successfully
6. Verify the widget on a website not in the allowed domains list is
still blocked

---------

Co-authored-by: iamsivin <iamsivin@gmail.com>
2026-03-17 14:29:41 +04:00
688218de0a feat: distributed scheduling for version check job (#13042)
This change spreads Chatwoot Hub version checks across the day by
scheduling each installation at a stable minute derived from its
installation identifier, instead of having all instances check at the
same fixed time.

Closes
-
https://linear.app/chatwoot/issue/CW-6107/handle-the-spike-at-12-utc-on-chatwoot-hub

What changed
- Added `Internal::TriggerDailyScheduledItemsJob` to act as the daily
trigger for deferred internal jobs.
- Updated the version check cron entry to run once daily at `00:00 UTC`
and enqueue the actual version check for that installation’s assigned
minute of the day.
- Used a deterministic minute-of-day derived from
`ChatwootHub.installation_identifier` so the check time stays stable
across deploys and restarts.
- Kept the existing cron schedule key while switching it to the new
orchestrator job.

How to test
- Run `bundle exec rspec
spec/jobs/internal/check_new_versions_job_spec.rb
spec/jobs/internal/trigger_daily_scheduled_items_job_spec.rb
spec/configs/schedule_spec.rb`
- In a Rails console, run
`Internal::TriggerDailyScheduledItemsJob.perform_now` and verify
`Internal::CheckNewVersionsJob` is enqueued with a `wait_until` later
the same UTC day.
- In Super Admin settings, use Refresh and verify the version check
still runs immediately.

---------

Co-authored-by: Sojan Jose <sojan@pepalo.com>
2026-03-17 02:27:49 -07:00
Muhsin KelothandGitHub a8d53a6df4 feat(linear): Support refresh tokens and migrate legacy OAuth tokens (#13721)
Linear is deprecating long-lived OAuth2 access tokens (valid for 10
years) in favor of short-lived access tokens with refresh tokens.
Starting October 1, 2025, all new OAuth2 apps will default to refresh
tokens. Linear will no longer issue long-lived access tokens. Please
read more details
[here](https://linear.app/developers/oauth-2-0-authentication#migrate-to-using-refresh-tokens)
We currently use long-lived tokens in our Linear integration (valid for
up to 10 years). To remain compatible, this PR ensures compatibility by
supporting refresh-token-based auth and migrating existing legacy
tokens.

Fixes
https://linear.app/chatwoot/issue/CW-5541/migrate-linear-oauth2-integration-to-support-refresh-tokens
2026-03-17 13:09:03 +04:00
2a90652f05 feat: Add draft status for help center locales (#13768)
This adds a draft status for Help Center locales so teams can prepare
localized content in the dashboard without exposing those locales in the
public portal switcher until they are ready to publish.

Fixes: https://github.com/chatwoot/chatwoot/issues/10412
Closes: https://github.com/chatwoot/chatwoot/issues/10412

## Why

Teams need a way to work on locale-specific Help Center content ahead of
launch. The public portal should only show ready locales, while the
admin dashboard should continue to expose every allowed locale for
ongoing article and category work.

## What this change does

- Adds `draft_locales` to portal config as a subset of `allowed_locales`
- Hides drafted locales from the public portal language switchers while
keeping direct locale URLs working
- Keeps drafted locales fully visible in the admin dashboard for article
and category management
- Adds locale actions to move an existing locale to draft, publish a
drafted locale, and keep the default locale protected from drafting
- Adds a status dropdown when creating a locale so new locales can be
created as `Published` or `Draft`
- Returns each admin locale with a `draft` flag so the locale UI can
reflect the public visibility state

## Validation

- Seed a portal with multiple locales, draft one locale, and confirm the
public portal switcher hides it while `/hc/:slug/:locale` still loads
directly
- In the admin dashboard, confirm drafted locales still appear in the
locale list and remain selectable for articles and categories
- Create a new locale with `Draft` status and confirm it stays out of
the public switcher until published
- Move an existing locale back and forth between `Published` and `Draft`
and confirm the public switcher updates accordingly


## Demo 



https://github.com/user-attachments/assets/ba22dc26-c2e7-463a-b1f5-adf1fda1f9be

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2026-03-17 12:45:54 +04:00
270f3c6a80 fix: slim help center search results (#13761)
Fixes help center public article search so query responses stay compact
and locale-scoped. Whitespace-only queries are now treated as empty in
both the portal UI and the server-side search path, and search
suggestions stay aligned with the trimmed query.

Fixes: https://github.com/chatwoot/chatwoot/issues/10402
Closes: https://github.com/chatwoot/chatwoot/issues/10402

## Why

The public help center search endpoint reused the full article
serializer for query responses, which returned much more data than the
search suggestions UI needed. That made responses heavier than necessary
and also surfaced nested portal and category data that made the results
look cross-locale.

Whitespace-only searches could also still reach the backend search path,
and in Enterprise that meant embedding search could be invoked for a
blank query.

## What changed

- return a compact search-specific payload for article query responses
- keep the existing full article serializer for normal article listing
responses
- preserve current-locale search behavior for the portal search flow
- trim whitespace-only search terms on the client so they do not open
suggestions or trigger a request
- reuse the normalized query on the backend so whitespace-only requests
are treated as empty searches in both OSS and Enterprise paths
- pass the trimmed search term into suggestions so highlighting matches
the actual query being sent
- add request and frontend regression coverage for compact payloads,
locale scoping, and whitespace-only search behavior

## Validation

1. Open `/hc/:portal/:locale` in the public help center.
2. Enter only spaces in the search box and confirm suggestions do not
open.
3. Search for a real term and confirm suggestions appear.
4. Verify the results are limited to the active locale.
5. Click a suggestion and confirm it opens the correct article page.
6. Inspect the query response and confirm it returns the compact search
payload instead of the full article serializer.

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2026-03-17 00:46:23 -07:00
ad1539c6cf fix(email): Allow inbox OAuth replies without global SMTP (#13820)
Email inbox replies now work for Google and Microsoft OAuth inboxes even
when the self-hosted instance does not have global SMTP configured. This
keeps agent replies working for email channels that already have valid
inbox-level delivery settings.

fixes: chatwoot/chatwoot#13118
closes: chatwoot/chatwoot#13118

## Why
Self-hosted email inbox replies were blocked by a global SMTP guard in
the `email_reply` path. For OAuth-backed email inboxes, outbound
delivery is configured at the inbox level, so the mailer returned early
and the reply flow failed before sending.

## What this change does
- Allows the `email_reply` path to proceed when the inbox has SMTP
configured
- Allows the `email_reply` path to proceed when the inbox has Google or
Microsoft OAuth delivery configured
- Renames the touched mailer helper predicates to `?` methods for
clarity

## Validation
- Configure a Google email inbox on a self-hosted instance without
global `SMTP_ADDRESS`
- Reply from Chatwoot to an existing email conversation
- Confirm the reply is sent through the inbox OAuth SMTP configuration
- Run `bundle exec rspec
spec/mailers/conversation_reply_mailer_spec.rb:595`

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2026-03-17 11:10:42 +04:00
349f55b558 fix: upgrade rollup to 4.59.0 to remediate CVE-2026-27606 (#13781)
https://linear.app/chatwoot/issue/CW-6595/vanta-remediate-high-vulnerabilities-identified-in-packages-are

## Description

- Added "rollup": ">=4.59.0" to pnpm.overrides in package.json
- This bumps rollup from 4.52.5 to 4.59.0 (the transitive dep via vite)

## Type of change

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

## How Has This Been Tested?

- Overall Sanity via UI.

## 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-03-16 23:12:04 -07:00
ef91b8bb42 fix(i18n): improve Ukrainian widget translation (#13819)
## Description

This PR improves the Ukrainian translation for the Chatwoot widget
(`app/javascript/widget/i18n/locale/uk.json`).

Key changes:
- Fixed typo: `Звантажити` → `Завантажити`
- Translated missing English strings
- Improved reply time messages
- Updated day names to match `{day}` usage in `BACK_ON_DAY`
- Improved UX wording in form placeholders
- Fixed typography in `ім’я`
- Improved consistency with other Chatwoot translations

These updates improve readability and correctness of the Ukrainian
widget interface.

## Type of change

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

## How Has This Been Tested?

Reviewed the updated translations and verified that:

- Ukrainian translations render correctly
- Reply time messages display properly
- `{day}` values work correctly with the `BACK_ON_DAY` message
- Form placeholders appear correctly
- No untranslated English strings remain

## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] My changes generate no new warnings

---------

Co-authored-by: Sojan Jose <sojan@pepalo.com>
2026-03-16 22:38:08 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Shivam MishraSojan Jose
de4c837885 chore(deps): bump dompurify from 3.2.4 to 3.3.2 (#13738)
Bumps [dompurify](https://github.com/cure53/DOMPurify) from 3.2.4 to
3.3.2.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/cure53/DOMPurify/releases">dompurify's
releases</a>.</em></p>
<blockquote>
<h2>DOMPurify 3.3.2</h2>
<ul>
<li>Fixed a possible bypass caused by jsdom's faulty raw-text tag
parsing, thanks multiple reporters</li>
<li>Fixed a prototype pollution issue when working with custom elements,
thanks <a
href="https://github.com/christos-eth"><code>@​christos-eth</code></a></li>
<li>Fixed a lenient config parsing in <code>_isValidAttribute</code>,
thanks <a
href="https://github.com/christos-eth"><code>@​christos-eth</code></a></li>
<li>Bumped and removed several dependencies, thanks <a
href="https://github.com/Rotzbua"><code>@​Rotzbua</code></a></li>
<li>Fixed the test suite after bumping dependencies, thanks <a
href="https://github.com/Rotzbua"><code>@​Rotzbua</code></a></li>
</ul>
<h2>DOMPurify 3.3.1</h2>
<ul>
<li>Updated <code>ADD_FORBID_CONTENTS</code> setting to extend default
list, thanks <a
href="https://github.com/MariusRumpf"><code>@​MariusRumpf</code></a></li>
<li>Updated the ESM import syntax to be more correct, thanks <a
href="https://github.com/binhpv"><code>@​binhpv</code></a></li>
</ul>
<h2>DOMPurify 3.3.0</h2>
<ul>
<li>Added the SVG <code>mask-type</code> attribute to default
allow-list, thanks <a
href="https://github.com/prasadrajandran"><code>@​prasadrajandran</code></a></li>
<li>Added support for <code>ADD_ATTR</code> and <code>ADD_TAGS</code> to
accept functions, thanks <a
href="https://github.com/nelstrom"><code>@​nelstrom</code></a></li>
<li>Fixed an issue with the <code>slot</code> element being in both SVG
and HTML allow-list, thanks <a
href="https://github.com/Wim-Valgaeren"><code>@​Wim-Valgaeren</code></a></li>
</ul>
<h2>DOMPurify 3.2.7</h2>
<ul>
<li>Added new attributes and elements to default allow-list, thanks <a
href="https://github.com/elrion018"><code>@​elrion018</code></a></li>
<li>Added <code>tagName</code> parameter to custom element
<code>attributeNameCheck</code>, thanks <a
href="https://github.com/nelstrom"><code>@​nelstrom</code></a></li>
<li>Added better check for animated <code>href</code> attributes, thanks
<a href="https://github.com/llamakko"><code>@​llamakko</code></a></li>
<li>Updated and improved the bundled types, thanks <a
href="https://github.com/ssi02014"><code>@​ssi02014</code></a></li>
<li>Updated several tests to better align with new browser encoding
behaviors</li>
<li>Improved the handling of potentially risky content inside CDATA
elements, thanks <a
href="https://github.com/securityMB"><code>@​securityMB</code></a> &amp;
<a href="https://github.com/terjanq"><code>@​terjanq</code></a></li>
<li>Improved the regular expression for raw-text elements to cover
textareas, thanks <a
href="https://github.com/securityMB"><code>@​securityMB</code></a> &amp;
<a href="https://github.com/terjanq"><code>@​terjanq</code></a></li>
</ul>
<h2>DOMPurify 3.2.6</h2>
<ul>
<li>Fixed several typos and removed clutter from our documentation,
thanks <a
href="https://github.com/Rotzbua"><code>@​Rotzbua</code></a></li>
<li>Added <code>matrix:</code> as an allowed URI scheme, thanks <a
href="https://github.com/kleinesfilmroellchen"><code>@​kleinesfilmroellchen</code></a></li>
<li>Added better config hardening against prototype pollution, thanks <a
href="https://github.com/EffectRenan"><code>@​EffectRenan</code></a></li>
<li>Added better handling of attribute removal, thanks <a
href="https://github.com/michalnieruchalski-tiugo"><code>@​michalnieruchalski-tiugo</code></a></li>
<li>Added better configuration for aggressive mXSS scrubbing behavior,
thanks <a
href="https://github.com/BryanValverdeU"><code>@​BryanValverdeU</code></a></li>
<li>Removed the script that caused the fake entry <a
href="https://security.snyk.io/vuln/SNYK-JS-DOMPURIFY-10176060">CVE-2025-48050</a></li>
</ul>
<h2>DOMPurify 3.2.5</h2>
<ul>
<li>Added a check to the mXSS detection regex to be more strict, thanks
<a
href="https://github.com/masatokinugawa"><code>@​masatokinugawa</code></a></li>
<li>Added ESM type imports in source, removes patch function, thanks <a
href="https://github.com/donmccurdy"><code>@​donmccurdy</code></a></li>
<li>Added script to verify various TypeScript configurations, thanks <a
href="https://github.com/reduckted"><code>@​reduckted</code></a></li>
<li>Added more modern browsers to the Karma launchers list</li>
<li>Added Node 23.x to tested runtimes, removed Node 17.x</li>
<li>Fixed the generation of source maps, thanks <a
href="https://github.com/reduckted"><code>@​reduckted</code></a></li>
<li>Fixed an unexpected behavior with <code>ALLOWED_URI_REGEXP</code>
using the 'g' flag, thanks <a
href="https://github.com/hhk-png"><code>@​hhk-png</code></a></li>
<li>Fixed a few typos in the README file</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/cure53/DOMPurify/commit/5e56114cb24079ce52dbc51f76e494b77afa5153"><code>5e56114</code></a>
Getting 3.x branch ready for 3.3.2 release (<a
href="https://redirect.github.com/cure53/DOMPurify/issues/1208">#1208</a>)</li>
<li><a
href="https://github.com/cure53/DOMPurify/commit/e8c95f4a27aa8b041f92b59ab7685a94f7be6208"><code>e8c95f4</code></a>
fix: Fixed the broken package-lock.json</li>
<li><a
href="https://github.com/cure53/DOMPurify/commit/9636037c145b769dad0b52da8313301cbf867f46"><code>9636037</code></a>
Update package-lock.json</li>
<li><a
href="https://github.com/cure53/DOMPurify/commit/5cad4cecf2e647ac66eed25bc02a2415f00dbc8b"><code>5cad4ce</code></a>
Getting 3.x branch ready for 3.3.2 releas (<a
href="https://redirect.github.com/cure53/DOMPurify/issues/1205">#1205</a>)</li>
<li><a
href="https://github.com/cure53/DOMPurify/commit/6fc446a589ab3d1d72ae2a5b71167ba38dbd3096"><code>6fc446a</code></a>
Merge pull request <a
href="https://redirect.github.com/cure53/DOMPurify/issues/1175">#1175</a>
from cure53/main</li>
<li><a
href="https://github.com/cure53/DOMPurify/commit/3b3bf917d2b39460de6d130acebdc9243cf3e6ae"><code>3b3bf91</code></a>
Merge branch 'main' of github.com:cure53/DOMPurify</li>
<li><a
href="https://github.com/cure53/DOMPurify/commit/9863f4195bae6048de9eb2802219218c6904066c"><code>9863f41</code></a>
chore: Preparing 3.3.1 release</li>
<li><a
href="https://github.com/cure53/DOMPurify/commit/b4e02954dc4172c3944a755f3e99fbb76be64f7b"><code>b4e0295</code></a>
chore: Preparing 3.3.0 release</li>
<li><a
href="https://github.com/cure53/DOMPurify/commit/077746bb2cfb77836dfb628dca7ffc7ced8a5356"><code>077746b</code></a>
build(deps-dev): bump js-yaml from 4.1.0 to 4.1.1 (<a
href="https://redirect.github.com/cure53/DOMPurify/issues/1170">#1170</a>)</li>
<li><a
href="https://github.com/cure53/DOMPurify/commit/4de68bba9aba43dc3bba9348df603b64fc06d591"><code>4de68bb</code></a>
build(deps): bump actions/checkout from 5 to 6 (<a
href="https://redirect.github.com/cure53/DOMPurify/issues/1171">#1171</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/cure53/DOMPurify/compare/3.2.4...3.3.2">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=dompurify&package-manager=npm_and_yarn&previous-version=3.2.4&new-version=3.3.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/chatwoot/chatwoot/network/alerts).

</details>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
Co-authored-by: Sojan Jose <sojan@pepalo.com>
2026-03-16 21:54:32 -07:00
a62beffeef fix(i18n): complete zh_TW locale coverage (#13792)
This updates the Traditional Chinese (`zh_TW`) locale coverage across
Chatwoot so the app no longer falls back to English for missing backend,
dashboard, widget, and survey strings.

## How to test

1. Start Chatwoot locally and switch the UI locale to Traditional
Chinese (`zh_TW`).
2. Walk through the main product areas: dashboard, settings, inbox
management, help center, automations, reports, widget, and survey flows.
3. Confirm the UI surfaces translated Traditional Chinese copy instead
of English fallbacks.
4. Spot-check newly added locale surfaces such as secure password
messaging and snooze UI copy.

---------

Co-authored-by: Sojan Jose <sojan@pepalo.com>
2026-03-16 18:47:22 -07:00
Denis PetrovandGitHub b8f6fe5bb7 feat: Bulgarian locale updates(#13635)
Bulgarian locale updates
2026-03-16 17:47:25 -07:00
b866886b55 feat(i18n): complete Korean (ko) translations to 100% coverage (#13583)
Translate all English strings to Korean across 41 frontend locale files
and 2 backend locale files. Add structurally missing keys and translate
existing keys that were left in English.

# Pull Request Template

## Description

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

## Type of change

Please delete options that are not relevant.

- [ ] Bug fix (non-breaking change which fixes an issue)
- [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
- [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: Sojan Jose <sojan@pepalo.com>
2026-03-16 16:54:26 -07:00
b88236e86e chore(i18n): update Russian translations (#13405)
# Pull Request Template

## Description

Updated few i18n files to:
1. fix typos / grammar / punctuation
2. translate strings that were still in english
3. add missing keys

## Type of change

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

## How Has This Been Tested?

i18n change, the format remained the same.

## 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: Sojan Jose <sojan@pepalo.com>
2026-03-16 16:10:16 -07:00
11ee741716 chore: Update translations (#13227)
Co-authored-by: Sojan Jose <sojan@pepalo.com>
2026-03-16 15:44:32 -07:00
Aakash BakhleandGitHub ac93290c9a fix: skip captain auto-open for templates (#13802)
# Pull Request Template

## Description

WhatsApp template messages are treated as outgoing human messages so
conversations are automatically marked Open, preventing Captain from
responding. #13673

This PR fixes the behaviour for template messages, so that captain can
respond to them.

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


## 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-16 18:54:12 +05:30
03719cede0 fix: Correct reversed message status indicators for API channel (#13594)
## Description

Fixes the reversed message delivery status indicators for the API
channel. The API inbox was grouped with the web widget inbox in the
`isDelivered` computed property, causing both to treat a `sent` status
as `delivered`. Since the API channel provides real
`sent`/`delivered`/`read` status values from external systems (unlike
the web widget which has no separate delivery confirmation), the API
inbox needs its own handling.

**Before this fix:**
- Status `sent` (0) → incorrectly showed delivered checkmarks
- Status `delivered` (1) → incorrectly showed "Sending" spinner

**After this fix:**
- Status `sent` → correctly shows sent indicator (single checkmark)
- Status `delivered` → correctly shows delivered indicator (double
checkmarks)
- Status `read` → unchanged (already worked correctly)

The web widget inbox behavior is unchanged — it still treats `sent` as
`delivered` since it lacks a separate delivery confirmation mechanism.

Fixes #13576

## Type of change

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

## How Has This Been Tested?

Verified by code review that the computed properties now correctly map
API channel message statuses:
- `isSent` returns `true` when `status === 'sent'` for API inbox
- `isDelivered` returns `true` when `status === 'delivered'` for API
inbox
- `isRead` unchanged — already checks `status === 'read'` for API inbox
- Web widget inbox logic is unchanged

## 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] My changes generate no new warnings

*This PR was created with the assistance of Claude Opus 4.6 by
Anthropic. Happy to make any adjustments! Reviewed and submitted by a
human.*

Co-authored-by: Your Name <your-email@example.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2026-03-16 13:21:18 +04:00
a5c50354fc feat: trigger assignment on resolve (#13780)
## Description

When an agent resolves or snoozes a conversation, their capacity frees
up — but under Assignment V2, no new conversation was assigned until the
next event triggered the assignment job. This meant agents could sit
idle despite having queued conversations waiting. This change triggers
the AssignmentJob immediately on resolve/snooze so freed capacity is
utilized right away.

## Type of change

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

## How Has This Been Tested?

- Enable auto assignment v2 on an inbox
- Create a conversation (gets auto-assigned to an available agent)
- Resolve the conversation
- Verify that another unassigned conversation in the inbox gets assigned
to the freed-up agent

## 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: tds-1 <tds-1@users.noreply.github.com>
2026-03-16 13:13:37 +05:30
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>
2026-03-16 12:48:16 +05:30
Tanmay Deep SharmaandGitHub 28bf9fa5f9 fix: upgrade markdown-it to 14.1.1 to remediate CVE-2026-2327 (#13782)
## Linear tickets
-
https://linear.app/chatwoot/issue/CW-6607/vanta-remediate-medium-vulnerabilities-identified-in-packages-are
-
https://linear.app/chatwoot/issue/CW-6612/vanta-remediate-medium-vulnerabilities-identified-in-packages-are

## Description

Upgrades markdown-it from 13.0.2 to 14.1.1 to remediate CVE-2026-2327

## Type of change

Please delete options that are not relevant.

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

## How Has This Been Tested?

- Sanity testing of golden flows via UI

## 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-03-16 11:11:19 +05:30
Shivam MishraandGitHub 73a90f2841 feat: update bunny video support in HC (#13815)
Bunny Video has added a new URL player.mediadelivery.net, this PR adds
support for the new URL
2026-03-16 11:04:27 +05:30
Aakash BakhleandGitHub a90ffe6264 feat: Add force legacy auto-resolve flag (#13804)
# Pull Request Template

## Description

Add account setting and store_accessor for
`captain_force_legacy_auto_resolve`.
Enterprise job now skips LLM evaluation when this flag is true and falls
back to legacy time-based resolution. Add spec to cover the fallback.


## Type of change

We recently rolled out Captain deciding if a conversation is resolved or
not. While it is an improvement for majority of customers, some still
prefer the old way of auto-resolving based on inactivity. This PR adds a
check.

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

legacy_auto_resolve = true

<img width="1282" height="848" alt="CleanShot 2026-03-13 at 19 55 55@2x"
src="https://github.com/user-attachments/assets/dfdcc5d5-6d21-462b-87a6-a5e1b1290a8b"
/>


legacy_auto_resolve = false
<img width="1268" height="864" alt="CleanShot 2026-03-13 at 20 00 50@2x"
src="https://github.com/user-attachments/assets/f4719ec6-922a-4c3b-bc45-7b29eaced565"
/>



## 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-13 15:04:58 -07:00
Alexander UdovichenkoandGitHub 412b72db7c fix: Delete double hmac check (#12464)
## Description

When hmac identity check is enabled according to
[this](https://www.chatwoot.com/hc/user-guide/articles/1677587479-how-to-enable-identity-validation-in-chatwoot)
I found out, that it checked twice.

If `should_verify_hmac? -> true` then hmac checked in `before_action`
and we don't need to do it again later.

This perfomance related and PR fixes this.
2026-03-13 02:30:17 -07:00
Aakash BakhleandGitHub 8aa49f69d2 fix: prefer system API key for completion service (#13799)
# Pull Request Template

## Description

Add an api_key override so internal conversation completions prefers
using the system API key and do not consume customer OpenAI credits.
## Type of change

Please delete options that are not relevant.

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

specs and 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-13 13:10:10 +05:30
Shivam MishraandGitHub 550b408656 fix: restrict existing user sign-in to account members (#13793)
SAML sign-in now only links an existing user when that user already
belongs to the account that initiated SSO. New users can still be
created for SAML-enabled accounts, and invited members can continue to
sign in through their IdP, but SAML will no longer auto-attach an
unrelated existing user record during login.

**What changed**
- Added an account-membership check before SAML reuses an existing user
by email.
- Kept first-time SAML user creation unchanged for valid new users.
- Added builder and request specs covering the allowed and rejected
login paths.
2026-03-13 12:22:25 +05:30
b103747584 fix: skip Enter key submission during IME composition in AI inputs (#13779)
# Pull Request Template

## Description

CJK language users (Chinese, Japanese, Korean, etc.) use IME where Enter
confirms character selection. AI input components were intercepting
Enter unconditionally, making them unusable for IME users.

Add `event.isComposing` check to CopilotEditor, CopilotInput, and
AssistantPlayground so Enter during active IME composition is ignored.


## Type of change

Please delete options that are not relevant.

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

Before:

Add a Japenese keyboard, then go to AI follow-ups, type some word,
selecting it with enter submits the follow up. So CJK users cannot use
follow-ups.



https://github.com/user-attachments/assets/53517432-d97b-47fc-a802-81675e31d5c9



After:

Type a word, press enter to choose it, press enter again to unselect it
and enter again to send


https://github.com/user-attachments/assets/6c2a420b-7ee6-4c71-82a6-d9f1d7bbf31a



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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 10:26:51 +05:30
d6d38cdd7d feat: captain decides if conversation should be resolved or kept open (#13336)
# Pull Request Template

## Description

captain decides if conversation should be resolved or open

Fixes
https://linear.app/chatwoot/issue/AI-91/make-captain-resolution-time-configurable

Update: Added 2 entries in reporting events:
`conversation_captain_handoff` and `conversation_captain_resolved`

## Type of change

Please delete options that are not relevant.

- [x] New feature (non-breaking change which adds functionality)
- [x] 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.

LLM call decides that conversation is resolved, drops a private note
<img width="1228" height="438" alt="image"
src="https://github.com/user-attachments/assets/fb2cf1e9-4b2b-458b-a1e2-45c53d6a0158"
/>

LLM call decides conversation is still open as query was not resolved
<img width="1215" height="573" alt="image"
src="https://github.com/user-attachments/assets/2d1d5322-f567-487e-954e-11ab0798d11c"
/>


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

---------

Co-authored-by: Sojan Jose <sojan@pepalo.com>
2026-03-13 10:03:58 +05:30
Sojan JoseandGitHub 199dcd382e fix: Skip redundant contact saves in ContactIdentifyAction (#13778)
When the SDK sends identify calls with identical payloads (common on
every page load), `save!` fires even though no attributes changed. While
Rails skips the actual UPDATE SQL, it still opens a transaction, runs
all callbacks (including validation queries like `Contact Exists?`), and
triggers `after_commit` hooks — all for a no-op.

This adds a `changed?` guard before `save!` to skip it entirely when no
attributes have actually changed.

**How to test**

- Trigger an identify call via the SDK with a contact's existing
attributes (same name, email, custom_attributes, etc.)
- The contact should not fire a save (no transaction, no callbacks)
- Trigger an identify call with a changed attribute — save should work
normally

**What changed**

- `ContactIdentifyAction#update_contact`: guard `save!` with `changed?`
check
- Added specs to verify `save!` is skipped for unchanged params and
avatar job still enqueues independently
2026-03-11 21:40:38 -07:00
PranavandGitHub c6f82783ba chore: Remove message touch:true, use combined update query (#13770)
Removes touch: true from the belongs_to :conversation association on
Message and consolidates the conversation timestamp update into the
existing set_conversation_activity callback.

Previously, every message save triggered two separate UPDATE queries on
the conversation — one from Rails' touch (updating updated_at) and
another from set_conversation_activity (updating last_activity_at). This
combines both into a single update_columns call, reducing write load on
the conversations table on every message creation.

### What changed
- Removed touch: true from belongs_to :conversation in Message
- Added updated_at: created_at to the existing update_columns call in
set_conversation_activity
2026-03-11 07:31:46 -07:00
Tanmay Deep SharmaandGitHub 9b3f0029a4 fix: override minimatch to patch ReDoS vulnerability (#13769)
## Description

Remediates high severity ReDoS vulnerability in minimatch
(CVE-2026-27903) flagged by Vanta/Dependabot.
minimatch is a transitive dev-only dependency (via eslint and
tailwindcss build tooling) — not shipped to production. Added pnpm
overrides to force patched versions:
- minimatch@<4 → 3.1.5
- minimatch@>=9.0.0 <9.0.7 → 9.0.9

Closes:
https://linear.app/chatwoot/issue/CW-6595/vanta-remediate-high-vulnerabilities-identified-in-packages-are

## Type of change

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

## How Has This Been Tested?

- No production impact — minimatch is only used in dev tooling, not at
runtime
- pnpm install completes successfully

## 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-03-11 16:48:48 +05:30
6e46be36c8 fix: Add fix to only allow confirmed agents to used in Agent Assingments at Macros/Automations (#13225)
# 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>
2026-03-11 02:01:53 -07:00
Aakash BakhleandGitHub 87f5af4caa fix: playground captain v2 scenarios (#13747)
# Pull Request Template

## Description

Playground now uses v2. It was only wired to use v1. Traces get `source:
playground` on langfuse when playground has been used.

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

locally and specs
<img width="1806" height="1276" alt="image"
src="https://github.com/user-attachments/assets/41ef4eb3-52b1-4b8e-9a4f-e8510c90cb39"
/>


## 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-11 14:05:16 +05:30
dbe35252bc fix: Use handoff_key for scenarios (#13755)
# Pull Request Template

## Description

Ensure agent function names stay within OpenAI's 64-char limit
(ai-agents prepends "handoff_to_").

Add HANDOFF_TITLE_SLUG_MAX_LENGTH and
handoff_key generation: persisted records use `scenario_{id}_agent`; new
records use a truncated title slug.
Assistant scenario keys and agent_name now reference the generated
handoff key.

fixes :

`Invalid 'messages[9].tool_calls[0].function.name': string too long.
Expected a string with maximum length 64, but got a string with length
95 instead.`

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

Tested locally
<img width="1806" height="1044" alt="image"
src="https://github.com/user-attachments/assets/40cd7a3d-3d97-43a8-bd56-d3f5d63abbda"
/>

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

---------

Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
2026-03-11 14:01:25 +05:30
Tanmay Deep SharmaandGitHub de8aa48b83 feat: make assignment_v2 feature available to all accounts (#13764)
## Description

Makes the assignment_v2 feature flag available to all installations by
removing the chatwoot_internal restriction. Previously this feature was
hidden from self-hosted installations; this change surfaces it in the
feature flags UI so any Chatwoot instance can enable it.

## 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-03-11 13:46:53 +05:30
Shivam MishraandGitHub 43977a1927 chore: upgrade packages to resolve dependency advisories (#13762)
This PR generally upgrades the available packages clearing existing
transient package vulnerabilities

### minimatch

This is mostly a dev-dependency, it was in the main dependency for
`@formkit/vue`, this PR clears it.
It still remains a dependency for tailwind and tailwind typograpghy, but
it's tolerable
_Fixes: CW-6594 CW-6592 CW-6598_


### Axios

Upgraded the dependency directly. Fixed it
_Fixes: CW-6591_

### dompurify

Upgraded the dependency, it's in the safe range now
_Fixes: CW-6611 CW-6610_

### ajv

Dev dependency, can be safely ignored
_Fixes: CW-6606 CW-6604_

### @tootallnate/once

Dev dependency, comes from Histoire, can be safely ignored
_Fixes: CW-6603_
2026-03-11 13:20:17 +05:30
Sivin VargheseandGitHub a9cabad529 chore: Hide reply-to when copilot is active (#13749) 2026-03-11 11:30:30 +05:30
Sojan JoseandGitHub fdc326094a docs(swagger): document account label endpoints (#13760)
Documents the missing account-level label CRUD endpoints in Chatwoot's
Swagger output so label management is discoverable alongside the
existing contact and conversation label APIs.

Fixes: none
Closes: none

Why
The account-level label API already exists in Chatwoot, but it was
missing from the published Swagger spec. That made label management
harder to discover even though contact and conversation label assignment
endpoints were already documented.

What this change does
- adds a `Labels` tag to the application Swagger docs
- adds the label resource and create/update payload schemas
- documents `GET/POST /api/v1/accounts/{account_id}/labels`
- documents `GET/PATCH/DELETE /api/v1/accounts/{account_id}/labels/{id}`
- regenerates the compiled Swagger JSON artifacts

Validation
- rebuilt the Swagger JSON from the source YAML
- verified the new label endpoints appear in `swagger/swagger.json`
- verified the new label endpoints appear in
`swagger/tag_groups/application_swagger.json`
- started the local Rails server and confirmed `/swagger` and
`/swagger/swagger.json` return `200 OK`
2026-03-10 22:24:16 -07:00
9a9398b386 feat: validate OpenAPI spec using Skooma (#13623)
Adds Skooma-based OpenAPI validation so SDK-facing request specs can
assert that documented request and response contracts match real Rails
behavior. This also upgrades the spec to OpenAPI 3.1 and fixes contract
drift uncovered while validating core application and platform
resources.

Closes
None

Why
We want CI to catch OpenAPI drift before it reaches SDK consumers. While
wiring validation in, this PR surfaced several mismatches between the
documented contract and what the Rails endpoints actually accept or
return.

What this change does
- Adds Skooma-backed OpenAPI validation to the request spec flow and a
dedicated OpenAPI validation spec.
- Migrates nullable schema definitions to OpenAPI 3.1-compatible unions.
- Updates core SDK-facing schemas and payloads across accounts,
contacts, conversations, inboxes, messages, teams, reporting events, and
platform account resources.
- Documents concrete runtime cases that were previously missing or
inaccurate, including nested `profile` update payloads, multipart avatar
uploads, required profile update bodies, nullable inbox feature flags,
and message sender types that include both `Captain::Assistant` and
senderless activity-style messages.
- Regenerates the committed Swagger JSON and tag-group artifacts used by
CI sync checks.

Validation
- `bundle exec rake swagger:build`
- `bundle exec rspec spec/swagger/openapi_spec.rb`

---------

Co-authored-by: Sojan Jose <sojan@pepalo.com>
2026-03-10 18:33:55 -07:00
dc0e5eb465 fix: optimize message query with account_id filter (#13759)
## Description

This PR optimizes message queries by explicitly filtering with
`account_id` so the database can use the existing indexes more
efficiently.

Changes:
- Add `account_id` to message query filters to improve index
utilization.
- Update `last_incoming_message` query to include `account_id`.
- Avoid unnecessary preloading of `contact_inboxes` where it is not
required.
- Update specs to ensure `account_id` is set correctly in
message-related tests.

These changes reduce query cost and improve performance for message
lookups, especially on large accounts.

---------

Co-authored-by: Pranav <pranav@chatwoot.com>
2026-03-10 16:46:20 -07:00
PranavandGitHub 79218be5c4 fix: Force account_id to use index on messages query on conversation push_event_data (#13757)
Add a where clause. where(account_id: account_id).
2026-03-11 01:03:18 +05:30
Shivam MishraandGitHub 8d9dd99012 fix: scenario label (#13746) 2026-03-10 18:32:44 +05:30
9f376c43b5 fix(signup): normalize account signup config checks (#13745)
This makes account signup enforcement consistent when signup is disabled
at the installation level. Email signup and Google signup now stay
blocked regardless of whether the config value is stored as a string or
a boolean.

This effectively covers the config-loader path, where `YAML.safe_load`
reads `value: false` from `installation_config.yml` as a native boolean
and persists it that way.

- Normalized the account signup check so disabled signup is handled
consistently across config value types.
- Reused the same check across API signup and Google signup entry
points.
- Added regression coverage for the disabled-signup cases in the
existing controller specs.

---------

Co-authored-by: Vishnu Narayanan <iamwishnu@gmail.com>
2026-03-10 16:35:09 +05:30
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>
2026-03-10 14:15:52 +05:30
8ea93ec73d chore(docs): Update documentation for messages API (#13744)
Update the documentation for messages API

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2026-03-10 14:15:10 +05:30
Vishnu NarayananandGitHub 28f58b3694 fix: make conversation transcript rate limit configurable (#13740)
## Summary
- The conversation transcript endpoint rate limit is hardcoded at 30
requests/hour per account with no way to override it
- Self-hosted users with active accounts hit this limit and get 429
errors across all channels
- Add `RATE_LIMIT_CONVERSATION_TRANSCRIPT` env var (default: `1000`) to
make it configurable, consistent with other throttles like
`RATE_LIMIT_CONTACT_SEARCH` and `RATE_LIMIT_REPORTS_API_ACCOUNT_LEVEL`
2026-03-10 14:11:36 +05:30
Sojan JoseandGitHub 52cd70dfa3 fix(super-admin): prefill confirmed_at in new user form (#13662)
On self-hosted instances without email configured, users created from
Super Admin can get stuck in an unconfirmed state. This PR implements
the default at the Super Admin frontend form layer, not in backend
creation logic.

What changed:
- Added a custom `ConfirmedAtField` for Super Admin user forms.
- Prefills `confirmed_at` with current time on the **New User** form
(`GET /super_admin/users/new`).
- Kept backend create behavior unchanged
(`resource_class.new(resource_params)`), so API/manual payloads still
behave normally.

Behavior:
- In Super Admin UI, `confirmed_at` is prefilled by default.
- If someone wants an unconfirmed user, they can clear the
`confirmed_at` field before saving.
- If `confirmed_at` is omitted from payload entirely, the created user
remains unconfirmed.

Scope note: external signup flows are intentionally unchanged in this PR
(`/api/v1/accounts`, `/api/v2/accounts`, and social/omniauth signup
behavior are not modified).

## Demo 





https://github.com/user-attachments/assets/436abbb0-d4cf-49a6-a1b8-4b6aa85aa09f
2026-03-10 12:14:58 +05:30
Shivam Mishra 19683fae74 Merge branch 'hotfix/4.11.2' into develop
Publish Chatwoot EE docker images / build (linux/amd64, ubuntu-latest) (push) Waiting to run
Publish Chatwoot EE docker images / build (linux/arm64, ubuntu-22.04-arm) (push) Waiting to run
Publish Chatwoot EE docker images / merge (push) Blocked by required conditions
Publish Chatwoot CE docker images / build (linux/arm64, ubuntu-22.04-arm) (push) Waiting to run
Publish Chatwoot CE docker images / merge (push) Blocked by required conditions
Publish Chatwoot CE docker images / build (linux/amd64, ubuntu-latest) (push) Waiting to run
2026-03-09 21:20:08 +05:30
Shivam Mishra 432462f967 feat: harden filter service 2026-03-09 21:19:20 +05:30
Sojan JoseandGitHub 9e40431d3a feat: show MFA status on Super Admin user page (#13724)
This PR adds an MFA row to the individual Super Admin user page and
shows the current state as Enabled or Disabled with a compact status
badge.

Fixes #13723

## Screens

<img width="1370" height="1043" alt="image"
src="https://github.com/user-attachments/assets/b9fee284-43b7-4bbb-9f60-b71ab34b96b7"
/>


<img width="1370" height="1043" alt="image"
src="https://github.com/user-attachments/assets/23c5e6d3-24b8-40d2-9134-0c2b1dc98b41"
/>
2026-03-09 08:04:36 -07:00
Vishnu NarayananandGitHub 4576e75a67 fix: bump redis-client to 0.26.4 to fix Sentinel resolution (#13689)
Description:
  ## Summary

- `redis-client` 0.22.2 uses `.call()` during Sentinel master
resolution, but `redis-rb` 5.x undefines `.call()` (only `.call_v()`
  exists), causing Sentinel connections to fail.
- Bumps `redis-client` from 0.22.2 to 0.26.4 which includes the upstream
fix (redis-rb/redis-client#283).
- Also bumps transitive dependency `connection_pool` from 2.5.3 to
2.5.5.

  Fixes #11665 https://github.com/chatwoot/chatwoot/issues/8368

  ## Test

  - `bundle exec rspec spec/lib/redis/config_spec.rb` passes
  - Full CI suite passes
2026-03-09 20:03:01 +05:30
Tanmay Deep SharmaandGitHub 11826e2a21 perf: reduce presence update frequency and fix background tab throttling (#13726)
## Description
Reduces the frequency of update_presence WebSocket calls from the live
chat widget and fixes agents appearing offline when the dashboard is in
a background tab.

## Fixes # (issue)
https://github.com/chatwoot/chatwoot/issues/13720

## 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
2026-03-09 18:23:44 +05:30
Sivin VargheseandGitHub f4e6aa1bd2 fix: ProseMirror prompt modal UI issue (#13722) 2026-03-09 16:51:49 +05:30
Muhsin KelothandGitHub 939471cb3b fix: Prevent duplicate conversations in conversation list (#13713)
Agents using API channel inboxes (e.g., WhatsApp Automate) reported
seeing the same conversation appear twice in their conversation list —
one showing the last message preview and the other showing "No
Messages". Backend investigation confirmed no duplicate conversations
exist in the database, making this purely a frontend issue.

The root cause is a race condition in WebSocket event delivery. When a
conversation is created via the API with auto-assignment, the backend
enqueues multiple ActionCable broadcast jobs (`conversation.created`,
`assignee.changed`, `team.changed`) within milliseconds of each other.
In production with multi-threaded Sidekiq workers, these events can
arrive at the frontend out of order. If `assignee.changed` arrives
before `conversation.created`, the `UPDATE_CONVERSATION` mutation pushes
the conversation into the store (since it doesn't exist yet), and then
`ADD_CONVERSATION` blindly pushes it again — resulting in a duplicate
entry.

The fix adds a uniqueness check in the `ADD_CONVERSATION` mutation to
skip the push if a conversation with the same ID already exists in the
store, matching the dedup pattern already used by
`SET_ALL_CONVERSATION`.
2026-03-06 14:07:02 +04:00
Sivin VargheseandGitHub 88587b1ccb feat: Add natural language date parser for snooze functionality (#13587)
# Pull Request Template

## Description

This PR introduces a custom, lightweight natural-language date parser
(dependency-free except for date-fns) to power snooze actions via the
command bar (e.g., “Remind me tomorrow at 6am”). It also adds support
for multi-language searches.



<details>
  <summary>Supported Formats</summary>

## Snooze Date Parser — Supported Input Formats


## 1. Durations

Specify an amount of time from now.

### Basic

- `5 minutes` · `2 hours` · `3 days` · `1 week` · `6 months` · `ten
year`
- `in 2 hours` · `in 30 minutes` · `in a week` · `in a month`
- `5 minutes from now` · `a week from now` · `two weeks from now`

### Half / fractional

- `half hour` · `half day` · `half week` · `half month`
- `in half a day` · `in half an hour` · `in half a week`
- `one and a half hours` · `in one and a half hours`
- `1.5 hours` · `2.5 days`

### Compound

- `1 hour and 30 minutes` · `2 hours and 15 minutes`
- `2 days at 3pm` · `1 week at 9am`

### Shorthand (no spaces)

- `2h` · `30m` · `1h30m` · `2h15m`
- `1h30minutes` · `2hr15min` · `1hour30min`

### Informal quantities

- `couple hours` · `a couple of days` · `in a couple hours`
- `a few minutes` · `in a few hours` · `in a few days`
- `fortnight` · `in a fortnight` _(= 2 weeks)_

### Trailing "later"

- `2 days later` · `a week later` · `month later`

### Prefix words (`after` / `within`)

- `after 2 hours` · `after 3 days` · `after ten year`
- `within a week` · `within 2 hours`

### Recognised word-numbers

`a` (1) · `an` (1) · `one` – `twenty` · `thirty` · `forty` · `fifty` ·
`sixty` · `ninety` · `half` (0.5) · `couple` (2) · `few` (3)

---

## 2. Relative Days

- `today` · `tonight` · `tomorrow`
- `tomorrow morning` · `tomorrow afternoon` · `tomorrow evening` ·
`tomorrow night`
- `tomorrow at 3pm` · `tomorrow 9` · `tonight at 8` · `tonight at 10pm`
- `tomorrow same time` · `same time tomorrow`
- `day after tomorrow` · `the day after tomorrow` · `day after tomorrow
at 2pm`
- `later today` · `later this afternoon` · `later this evening`

---

## 3. Weekdays

- `monday` · `friday` · `wed` · `thu`
- `friday at 3pm` · `monday 9am` · `wednesday 14:30`
- `monday morning` · `friday afternoon` · `wednesday evening`
- `monday morning 6` · `friday evening 7`
- `this friday` · `upcoming monday` · `coming friday`
- `same time friday` · `same time wednesday`

---

## 4. "Next" Patterns

- `next hour` · `next week` · `next month` · `next year`
- `next week at 2pm` · `next month at 9am`
- `next monday` · `next friday` · `next friday at 3pm`
- `next monday morning` · `next friday evening`
- `monday of next week` · `next week monday`
- `next january` · `next december`
- `next business day` · `next working day`

---

## 5. Time of Day

- `morning` · `afternoon` · `evening` · `night` · `noon` · `midnight`
- `this morning` · `this afternoon` · `this evening`
- `early morning` · `late evening` · `late night`
- `morning at 8am` · `evening 6pm` · `afternoon 2pm`
- `eod` · `end of day` · `end of the day`

---

## 6. Standalone Time

- **12-hour:** `3pm` · `9am` · `at 3pm` · `at 9:30am`
- **24-hour:** `14:30` · `at 14:30`

---

## 7. Named Dates (Month + Day)

- `jan 15` · `january 15` · `march 20` · `dec 25`
- `jan 1st` · `march 3rd` · `april 2nd` · `december 31st`
- `15 march` · `25 dec` _(reversed order)_
- `jan 15 2025` · `dec 25 2025` · `march 20 next year`
- `jan 15 at 2pm` · `march 5 at 2pm`
- `december 2025` · `january 2024` _(month + year only)_

---

## 8. Month + Ordinal Patterns

Target a specific week or day within a month.

### Week of month

- `april first week` · `july 2nd week` · `feb 3rd week`
- `first week of april` · `2nd week of july`

### Day of month

- `april first day` · `march second day` · `march 5th day`
- `third day of march` · `5th day of jan at 2pm`

### Supported ordinals

- **Digit:** `1st` `2nd` `3rd` `4th` `5th` … (up to 31 for days, 5 for
weeks)
- **Word:** `first` `second` `third` `fourth` `fifth` `sixth` `seventh`
`eighth` `ninth` `tenth`

---

## 9. Formal / Numeric Dates

- **ISO:** `2025-01-15`
- **Slash (M/D/Y):** `01/15/2025`
- **Dash (D-M-Y):** `15-01-2025`
- **Dot (D.M.Y):** `15.01.2025`
- Any of the above **+ time:** `2025-01-15 at 3pm`

---

## 10. Special Phrases

- `this weekend` · `weekend` · `next weekend`
- `end of week` · `end of month`
- `end of next week` · `end of next month`
- `beginning of next week` · `start of next week`
- `beginning of next month`

---

## 11. Noise / Filler Stripping

The parser silently removes conversational prefixes so all of these work
exactly the same as the bare expression:

```
snooze for 2 hours          →  2 hours
remind me tomorrow          →  tomorrow
please snooze until friday  →  friday
can you set a reminder for next week  →  next week
schedule this for jan 15    →  jan 15
postpone to next monday     →  next monday
defer for 2 days            →  2 days
delay it by 1 hour          →  1 hour
```

### Recognised filler verbs / prefixes

`snooze` · `remind` · `remind me` · `set a reminder` · `add a reminder`
·
`schedule` · `postpone` · `defer` · `delay` · `push`

### Recognised prepositions (stripped)

`on` · `to` · `for` · `at` · `until` · `till` · `by` · `from` · `after`
· `within`

### Typo corrections

`tommorow` / `tommorrow` → `tomorrow` · `nxt` → `next`

---

## 12. Multi-Language Support

The parser supports localised input via translations in `snooze.json`.

### Translatable token categories

- **Units:** minute, hour, day, week, month, year _(singular + plural)_
- **Relative days:** tomorrow, day after tomorrow, next week / month,
this / next weekend
- **Time of day:** morning, afternoon, evening, night, noon, midnight
- **Word numbers:** one through ten, twelve, fifteen, twenty, thirty
- **Ordinals:** first through fifth
- **Structural words:** at, in, of, after, week, day, from now, next
year
- **Meridiem:** am, pm

### Auto-detected from locale

Weekday names and month names are resolved automatically via
`Intl.DateTimeFormat` for the user's locale — no manual translation
needed.

</details>

## Type of change

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

## How Has This Been Tested?

**Screenshots**
<img width="974" height="530" alt="image"
src="https://github.com/user-attachments/assets/c690d328-a0df-41d2-b531-2b4e6ce6b5fd"
/>
<img width="974" height="530" alt="image"
src="https://github.com/user-attachments/assets/fa881acc-4fed-4ba3-9166-58bd953bcb26"
/>
<img width="974" height="530" alt="image"
src="https://github.com/user-attachments/assets/4d9a224b-641c-409c-a7ce-3dec2b5355e2"
/>
<img width="974" height="530" alt="image"
src="https://github.com/user-attachments/assets/465b9835-d82c-4bc7-a2ae-94976ada2d3b"
/>
<img width="974" height="530" alt="image"
src="https://github.com/user-attachments/assets/839fe8fc-8943-4b66-83ca-5c61c95f24d8"
/>
<img width="974" height="530" alt="image"
src="https://github.com/user-attachments/assets/3a9a54f2-7669-40f2-b098-a3f5c183526d"
/>
<img width="974" height="530" alt="image"
src="https://github.com/user-attachments/assets/7791ab2b-c763-49a9-90a0-e91b0d8f0a26"
/>
<img width="974" height="530" alt="image"
src="https://github.com/user-attachments/assets/4689390c-0e7f-48ae-acc7-d8e28695452f"
/>
<img width="974" height="530" alt="image"
src="https://github.com/user-attachments/assets/d0aa5217-d0e1-4f41-b663-72888d028a3a"
/>
<img width="974" height="530" alt="image"
src="https://github.com/user-attachments/assets/4fa9ff5b-a874-43d5-812f-6abe1a95a5ac"
/>
<img width="974" height="530" alt="image"
src="https://github.com/user-attachments/assets/2c8199a6-f872-46af-986f-bdf8597248f5"
/>
<img width="974" height="530" alt="image"
src="https://github.com/user-attachments/assets/5bd9effc-7518-4f96-b2f2-7c547f32f500"
/>




## 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
- [ ] Any dependent changes have been merged and published in downstream
modules
2026-03-06 12:20:22 +04:00
598ece9a2d fix: Handle Facebook reel attachment type (#13691)
Fixes https://linear.app/chatwoot/issue/PLA-96/argumenterror-reel-is-not-a-valid-file-type-argumenterror
Fixes a crash in `when a user shares a Facebook reel via Messenger.
Facebook sends these attachments with `type: "reel"`, which isn't a
valid `file_type` enum value on the Attachment model (only `ig_reel`
exists, matching Instagram's format).

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Sojan Jose <sojan@pepalo.com>
2026-03-06 08:49:41 +04:00
059506b1db feat: Add automatic favicon fetching for companies (#13013)
## Summary

This Enterprise-only feature automatically fetches a favicon for
companies created with a domain, and adds a batch task to backfill
missing avatars for existing companies. The flow only targets companies
that do not already have an attached avatar, so existing avatars are
left untouched.


## Demo 



https://github.com/user-attachments/assets/d050334e-769f-4e46-b6e7-f7423727a192



## What changed

- Added `Avatar::AvatarFromFaviconJob` to build a Google favicon URL
from the company domain and fetch it through `Avatar::AvatarFromUrlJob`
- Triggered favicon fetching from `Company` with `after_create_commit`
- Added `Companies::FetchAvatarsJob` to batch existing companies that
are missing avatars
- Added `companies:fetch_missing_avatars` under `enterprise/lib/tasks`
- Kept the company-specific implementation inside the Enterprise
boundary
- Stubbed the new favicon request in unrelated specs that now hit this
callback indirectly
- Updated a couple of CI-sensitive specs that were failing due to
callback side effects / reload-safe exception assertions

## How to verify

1. Create a company in Enterprise with a valid domain and no avatar.
2. Confirm that a favicon-based avatar gets attached shortly after
creation.
3. Create another company with a domain and an avatar already attached.
4. Confirm that the existing avatar is not replaced.
5. Run `companies:fetch_missing_avatars`.
6. Confirm that existing companies without avatars get one, while
companies that already have avatars remain unchanged.

## Notes

- This change does not refresh or overwrite existing company avatars
- Favicon fetching only runs for companies with a present domain
- The branch includes the latest `develop`

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Sojan Jose <sojan@pepalo.com>
2026-03-05 18:51:28 -08:00
Sojan JoseandGitHub 397b0bcc9d feat: allow agent bots to toggle typing status (#13705)
Agent bot conversations now feel more natural because AgentBot tokens
can toggle typing status, so end users see a live typing indicator in
the widget while the bot is preparing a reply. This keeps the
interaction responsive and human-like without weakening token
authorization boundaries.

## Closes
- https://github.com/chatwoot/chatwoot/issues/8928
- https://linear.app/chatwoot/issue/CW-5205

## How to test
1. Open the widget and start a conversation as a customer.
2. Connect an AgentBot to the same inbox.
3. Trigger `toggle_typing_status` with the AgentBot token
(`typing_status: on`).
4. Confirm the customer sees the typing indicator in the widget.
5. Trigger `toggle_typing_status` with `typing_status: off` and confirm
the indicator disappears.

## What changed
- Added `toggle_typing_status` to bot-accessible conversation endpoints.
- Restricted bot-accessible endpoint usage to `AgentBot` token owners
only (non-user tokens like `PlatformApp` remain unauthorized).
- Updated typing status flow to preserve AgentBot identity in
dispatch/broadcast paths.
- Added request coverage for AgentBot success and PlatformApp
unauthorized behavior.
- Added Swagger documentation for `POST
/api/v1/accounts/{account_id}/conversations/{conversation_id}/toggle_typing_status`
and regenerated swagger artifacts.
2026-03-05 08:13:52 -08:00
fd69b4c8f2 fix: captain json parsing (#13708)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
2026-03-05 15:43:21 +05:30
Sivin VargheseandGitHub 3ea5f258a4 fix: Use page_title with fallback to name for portal display titles (#13719) 2026-03-05 14:20:31 +05:30
Sojan JoseandGitHub 42a244369d feat(help-center): enable drag-and-drop category reordering (#13706) 2026-03-05 12:53:38 +05:30
3abe32a2c7 chore(dev): add cleanup flow to force_run in Makefile (#13093)
## Summary

Improve local dev restart reliability by enhancing `make force_run` to
run cleanup before starting Overmind.

## How To Reproduce

During local development, if `make run` is interrupted (for example with
Ctrl-C), stale state can remain (`.overmind.sock`, PID files, and
processes on ports `3000`/`3036`), which can block or complicate the
next restart.

## Changes

Updated `force_run` in `Makefile` to:
- print cleanup start/end messages
- kill processes on ports `3036` and `3000` (best-effort)
- remove `.overmind.sock`
- remove `tmp/pids/*.pid`
- then start `Procfile.dev` via Overmind

No other files are changed in this PR.

## Testing

- Verified branch diff against `develop` only touches `Makefile`.
- Ran `make -n force_run` to validate the command sequence and startup
flow.

---------

Co-authored-by: Sojan Jose <sojan@pepalo.com>
2026-03-04 17:56:55 -08:00
Sivin VargheseandGitHub f24e7eb231 fix: Missing required prop warning in account settings page (#13711)
# Pull Request Template

## Description

This PR fixes the console warning in development: `[Vue warn]: Missing
required prop: "name"` on the account settings page.


## Type of change

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

## How Has This Been Tested?

**Screenshot**
<img width="599" height="1036" alt="image"
src="https://github.com/user-attachments/assets/b0b45854-4cfb-4fe7-ab14-c42a65c523df"
/>



## 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-04 19:58:47 +04:00
8cfbb75128 fix: add missing V1 guardrails to V2 assistant prompt (#13701)
Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
2026-03-03 17:36:49 +05:30
Muhsin KelothandGitHub a1b98a253c fix(ui): Show delivered state for Instagram external echo messages (#13700)
Instagram external echo messages were being saved with status:
delivered, but the message meta UI did not treat Instagram as a channel
eligible for delivered-state rendering. As a result, these messages fell
back to progress and showed as “Sending”. This change updates the
message status mapping in the new message UI to include Instagram in the
delivered-state condition.
2026-03-03 15:16:53 +04:00
Aakash BakhleandGitHub 374d2258c7 fix: captain talking over support agent (#13673) 2026-03-03 16:13:34 +05:30
Sivin VargheseandGitHub 89da4a2292 feat: compose form improvements (#13668) 2026-03-02 18:27:51 +05:30
Muhsin KelothandGitHub 9aacc0335b feat(facebook): use HUMAN_AGENT tag for Messenger replies when human-agent config is enabled (#13690)
This PR updates Facebook Messenger outbound tagging in Chatwoot to
support Human Agent messaging when enabled.

Previously, Facebook outbound text and attachment messages were always
sent with:

```
messaging_type: MESSAGE_TAG
tag: ACCOUNT_UPDATE
```
With this change, the tag is selected dynamically:

```
HUMAN_AGENT when ENABLE_MESSENGER_CHANNEL_HUMAN_AGENT is enabled
ACCOUNT_UPDATE as fallback when the flag is disabled
```
2026-03-02 15:32:59 +04:00
ab93821d2b fix(agent-bot): stabilize webhook delivery for transient upstream failures (#13521)
This fixes the agent-bot webhook delivery path so transient upstream
failures follow the expected delivery lifecycle. Existing fallback
behavior is preserved, and fallback actions are applied only after
delivery attempts are exhausted.

To reproduce, configure an agent-bot webhook endpoint to return 429/500
for message events. Before this fix, failure handling could be applied
too early; after this fix, delivery attempts complete first and then
existing fallback handling runs.

Tested with:
- bundle exec rspec spec/jobs/agent_bots/webhook_job_spec.rb
spec/lib/webhooks/trigger_spec.rb
- bundle exec rubocop spec/jobs/agent_bots/webhook_job_spec.rb
spec/lib/webhooks/trigger_spec.rb

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2026-03-02 14:18:29 +04:00
Shivam MishraandGitHub 8d48e05283 feat: reclaim mobile_v2 flag for report_rollup (#13666) 2026-03-02 13:12:42 +05:30
c08fa631a9 feat: Add temporary account setting to disable Captain auto-resolve (#13680)
Add a temporary `captain_disable_auto_resolve` boolean setting on
accounts to prevent Captain from resolving conversations. Guards both
the scheduled resolution job and the assistant's resolve tool.

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 09:37:00 -08:00
14b4c83dc6 fix: Prevent AudioTranscriptionJob from crashing on OpenAI 401 error (#13653)
Describe the bug
In v4.8.0, when an audio message is received, the system enqueues
Messages::AudioTranscriptionJob even if OpenAI and Captain are disabled.
This causes a Faraday::UnauthorizedError (401) which crashes the Sidekiq
job and breaks the pipeline for that message.

To Reproduce
Disable OpenAI/Captain integrations.

Send an audio message to an inbox.

Check Sidekiq logs and observe the 401 crash in
AudioTranscriptionService.

What this PR does
Adds a rescue Faraday::UnauthorizedError block inside
AudioTranscriptionService#perform. Instead of crashing the worker, it
logs a warning and gracefully exits, allowing the job to complete
successfully.

Note: This fixes the backend crash. However, there is still a frontend
reactivity issue where the audio player UI requires an F5 to load the
media, which has been reported in Issue #11013.

---------

Co-authored-by: Eloi Junior Seganfredo <eloi@seganfredo.local>
Co-authored-by: Aakash Bakhle <48802744+aakashb95@users.noreply.github.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2026-02-27 14:12:03 +04:00
df92fd12cb fix: bot handoff should set waiting time (#13417)
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2026-02-27 15:31:49 +05:30
Sojan JoseandGitHub d84ae196d5 fix: call authorization_error! on IMAP auth failures (#13560) (revert) (#13671)
This reverts commit 7acd239c70 to further
debug upstream issues.
2026-02-26 18:45:18 -08:00
Muhsin KelothandGitHub bdcc62f1b0 feat(facebook): Mark Messenger native-app echoes as external echo message (#13665)
When agents send replies from the native Facebook Messenger app (not
Chatwoot), echo events were created without external_echo metadata and
could be misrepresented in the UI. This change updates Messenger echo
message creation to:

- set content_attributes.external_echo = true for outgoing_echo messages
- set echo message status to delivered
- keep sender as nil for echo messages (existing behavior)

<img width="2614" height="1264" alt="CleanShot 2026-02-26 at 16 32
04@2x"
src="https://github.com/user-attachments/assets/ba61c941-465d-4893-814e-855e6b6c79e8"
/>
2026-02-26 19:05:15 +04:00
Tanmay Deep SharmaandGitHub 7acd239c70 fix: call authorization_error! on IMAP auth failures (#13560)
## Notion document

https://www.notion.so/chatwoot/Email-IMAP-Issue-30aa5f274c928062aa6bddc2e5877a63?showMoveTo=true&saveParent=true

## Description

PLAIN IMAP channels (non-OAuth) were silently retrying failed
authentication every minute, forever. When credentials are
wrong/expired, Net::IMAP::NoResponseError was caught and logged but
channel.authorization_error! was never called — so the Redis error
counter never incremented, reauthorization_required? was never set, and
admins were never notified. OAuth channels already had this handled
correctly via the Reauthorizable concern.
Additionally, Net::IMAP::ResponseParseError (raised by non-RFC-compliant
IMAP servers) was falling through to the StandardError catch-all,
flooding
Estimated impact before fix: ~70–75 broken IMAP inboxes generating
~700k–750k wasted Sidekiq jobs/week.

## 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
2026-02-26 18:01:23 +05:30
Tanmay Deep SharmaandGitHub 9ca03c1af3 chore: make all the deprecated feature flag reclaimable (#13646)
## Docs

https://www.notion.so/chatwoot/Redeeming-a-depreciated-feature-flag-313a5f274c9280f381cdd811eab42019?source=copy_link

## Description
Marks 8 unused feature flags as deprecated: true in features.yml,
freeing their bit slots for future reuse.
Removes dead code references from JS constants, help URLs, and
enterprise billing config.

## Type of change

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

## How Has This Been Tested?

- Simulated the "claim a slot" workflow 

## 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-02-26 18:01:13 +05:30
Shivam MishraandGitHub c218eff5ec feat: add per-webhook secret with backfill migration (#13573) 2026-02-26 17:26:12 +05:30
7c60ad9e28 feat: include contact verified status with each tool call (#13663)
Co-authored-by: aakashb95 <aakashbakhle@gmail.com>
2026-02-26 16:16:33 +05:30
Muhsin KelothandGitHub 6b3f1114fd fix(slack): Show correct sender name and avatar for Slack replies (#13624) 2026-02-26 16:15:15 +05:30
Sivin VargheseandGitHub 109b43aadb chore: Disable API channel reply editor outside 24h window (#13664) 2026-02-26 16:05:05 +05:30
Vishnu NarayananandGitHub 3ddab3ab26 fix: show upgrade prompt when email transcript returns 402 (#13650)
- Show a specific upgrade prompt when free-plan users attempt to send an
email transcript and the API returns a 402 Payment Required error
- Previously, a generic "There was an error, please try again" message
was shown for all failures, including plan restrictions

Fixes
https://linear.app/chatwoot/issue/CW-6538/show-ui-feedback-for-email-transcript-402-plan-restriction
2026-02-26 12:54:40 +05:30
PranavandGitHub e2dd2ccb42 feat: Add a priority + created at sort for conversations (#13658)
- Add a new conversation sort option "Priority: Highest first, Created:
Oldest first" that sorts by priority descending (urgent > high > medium
> low > none) with created_at ascending as the tiebreaker
2026-02-25 18:22:41 -08:00
9fab70aebf fix: Use search API instead of filter in the filter in the endpoints (#13651)
- Replace `POST /contacts/filter` with `GET /contacts/search` for
contact lookup in compose new conversation
- Remove client-side input-type detection logic (`generateContactQuery`,
key filtering by email/phone/name) — the search API handles matching
across name, email, phone_number, and identifier server-side via a
single `ILIKE` query
- Filter the contacts with emails in cc and bcc fields.

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
2026-02-25 09:08:24 -08:00
Aakash BakhleandGitHub efe49f7da4 fix: captain liquid render file system (#13647) 2026-02-25 20:19:34 +05:30
Sivin VargheseandGitHub 5aef9d2dd0 fix: Conversation list overlay issue with Virtua virtualizer (#13648) 2026-02-25 20:18:34 +05:30
b98c614669 feat: add campaign context to Captain v2 prompts (#13644)
Co-authored-by: Aakash Bakhle <48802744+aakashb95@users.noreply.github.com>
2026-02-25 18:33:37 +05:30
Sivin VargheseandGitHub ba804e0f30 fix: Upgrade pico-search to 0.6.0 (#13645) 2026-02-25 16:52:45 +05:30
a44cb2c738 feat(inbox): Enable conversation continuity for social channels (#11079)
## Summary
This PR enables and surfaces **conversation workflow** for social-style
channels that should support either:
- `Create new conversations` after resolve, or
- `Reopen same conversation`

## What is included
- Adds the conversation workflow setting UI as card-based options in
Inbox Settings.
- Expands channel availability in settings to include channels like:
  - Telegram
  - TikTok
  - Instagram
  - Line
  - WhatsApp
  - Facebook
- Updates conversation selection behavior for Line incoming messages to
respect the workflow (reopen vs create-new-after-resolved).
- Updates TikTok conversation selection behavior to respect the workflow
(reopen vs create-new-after-resolved).
- Keeps email behavior unchanged (always starts a new thread).

Fixes: https://github.com/chatwoot/chatwoot/issues/8426

## Screenshot

<img width="1400" height="900" alt="pr11079-workflow-sender-clear-tight"
src="https://github.com/user-attachments/assets/9456821f-8d83-4924-8dcf-7503c811a7b1"
/>


## How To Reproduce
1. Open `Settings -> Inboxes ->
<Telegram/TikTok/Instagram/Line/Facebook/WhatsApp inbox> -> Settings`.
2. Verify **Conversation workflow** is visible with the two card
options.
3. Toggle between both options and save.
4. For Line and TikTok, verify resolved-conversation behavior follows
the selected workflow.

## Testing
- `RAILS_ENV=test bundle exec rspec
spec/builders/messages/instagram/message_builder_spec.rb:213
spec/builders/messages/instagram/message_builder_spec.rb:255
spec/builders/messages/instagram/messenger/message_builder_spec.rb:228
spec/builders/messages/instagram/messenger/message_builder_spec.rb:293
spec/services/tiktok/message_service_spec.rb`
- Result: `16 examples, 0 failures`

## Follow-up
- Migrate Website Live Chat workflow settings into this same
conversation-workflow settings model.
- Add Voice channel support for this workflow setting.

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
2026-02-25 13:56:51 +04:00
Sivin VargheseandGitHub 172ff87b5b feat: Replace vue-virtual-scroller with virtua for chat list virtualization (#13642)
# Pull Request Template

## Description

This PR replaces `vue-virtual-scroller` with
[`virtua`](https://github.com/inokawa/virtua/#benchmark) for the
conversation list virtualization.

### Changes
- Replace `vue-virtual-scroller`
(`DynamicScroller`/`DynamicScrollerItem`) with `virtua`'s `Virtualizer`
component
- Remove `IntersectionObserver`-based infinite scroll in favor of
`Virtualizer`'s `@scroll` event with offset-based bottom detection
- Remove `useEventListener` scroll binding and
`intersectionObserverOptions` computed
- Simplify item rendering — no more `DynamicScrollerItem` wrapper or
`size-dependencies` tracking; `virtua` measures items automatically


## Type of change

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


## 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-02-25 12:59:02 +04:00
Sojan JoseandGitHub 55f6257313 chore(hub): clean up legacy Captain hub flow (#13640)
## Summary
This PR cleans up legacy Hub/Captain integration paths and simplifies
hub URL behavior coverage in tests.

## Changes
- remove legacy Captain account endpoint flow from `ChatwootHub`
- remove obsolete spec coverage tied to that retired flow
- keep hub URL handling centralized in `base_url` with enterprise
overlay precedence
- simplify hub URL specs to assert explicit static URL expectations
where applicable

## Reproduce
Run the focused hub specs from this branch:
- `bundle exec rspec spec/lib/chatwoot_hub_spec.rb
spec/enterprise/lib/chatwoot_hub_spec.rb`

## Testing
Validated locally with:
- `bundle exec rspec spec/lib/chatwoot_hub_spec.rb
spec/enterprise/lib/chatwoot_hub_spec.rb`
- `bundle exec rubocop lib/chatwoot_hub.rb spec/lib/chatwoot_hub_spec.rb
enterprise/lib/enterprise/chatwoot_hub.rb
spec/enterprise/lib/chatwoot_hub_spec.rb`
2026-02-24 20:29:53 -08:00
Muhsin KelothandGitHub 76f129efaf feat(tiktok): Enable outgoing image attachments (#13620)
- Enabled the attachment button for TikTok conversations in the reply
box
- Auto-split messages when both text and an image are composed together,
since the TikTok API rejects mixed text+media in a single message.
Fixes
https://linear.app/chatwoot/issue/CW-6528/enable-outgoing-image-attachments
2026-02-24 20:13:58 +04:00
7cec4ebaae feat: support multimodal user messages in captain v2 (#13581)
Extract and pass image attachments from the latest user message to the
runner,
excluding the last user message from the context for processing.

Fixes #13588 

# Pull Request Template

## Description

Adds image support to captain v2

## Type of change

Please delete options that are not relevant.

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

specs and local testing

<img width="754" height="1008" alt="image"
src="https://github.com/user-attachments/assets/914cbc2c-9d30-42d0-87d4-9e5430845c87"
/>

langfuse also shows media correctly with the instrumentation code:
<img width="1800" height="1260" alt="image"
src="https://github.com/user-attachments/assets/ce0f5fa6-b1a5-42ec-a213-9a82b1751037"
/>


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

---------

Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 19:37:41 +05:30
Muhsin KelothandGitHub 6be95e79f8 feat(csat): Add WhatsApp utility template analyzer with rewrite guidance (#13575)
CSAT templates for WhatsApp are submitted as Utility, but Meta may
reclassify them as Marketing based on content, which can significantly
increase messaging costs.
This PR introduces a Captain-powered CSAT template analyzer for
WhatsApp/Twilio WhatsApp that predicts utility fit, explains likely
risks, and suggests safer rewrites before submission. The flow is manual
(button-triggered), Captain-gated, and applies rewrites only on explicit
user action. It also updates UX copy to clearly set expectations: the
system submits as Utility, Meta makes the final categorization decision.

Fixes
https://linear.app/chatwoot/issue/CW-6424/ai-powered-whatsapp-template-classifier-for-csat-submissions


https://github.com/user-attachments/assets/8fd1d6db-2f91-447c-9771-3de271b16fd9
2026-02-24 15:11:04 +04:00
Tanmay Deep SharmaandGitHub 2b85275e26 feat: show assignment policy name in auto-assignment activity messages (#13598) 2026-02-24 13:32:54 +05:30
5b167b5b5b fix(contacts): Show telegram id in contact details form (#13611)
## Summary
This change fixes a mismatch in contact details where Telegram data
could be shown in the contact profile/social icon area but was not
available in the editable contact form.

### What changed
- Added Telegram to the social links section of the next-gen contact
form so agents can view and edit it alongside Facebook, Instagram,
TikTok, Twitter, GitHub, and LinkedIn.
- Added Telegram support to the legacy conversation contact edit form
for parity between both contact editing experiences.
- Mapped social_telegram_user_name into the editable socialProfiles
payload when preparing contact form state, so Telegram usernames sourced
from channel attributes are visible in the form.
- Updated the conversation contact social profile merge logic so
Telegram display prefers an explicitly saved social profile value and
falls back to social_telegram_user_name when needed.
- Added the missing English i18n placeholder: Add Telegram.

### Why
Without this, users could see Telegram info in some contact views but
could not reliably edit it in contact details, creating inconsistent
behavior between display and edit states.

---------

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
2026-02-23 19:26:45 +04:00
Muhsin KelothandGitHub b220663785 fix: Skip notifications for private notes (#13617)
When agents or integrations create private notes on a conversation,
every note was sending a notification to the assigned agent and all
conversation participants. The fix ensures that private notes no longer
trigger new message notifications. If someone explicitly mentions a
teammate in a private note, that person will still get notified as
expected.
2026-02-23 15:40:54 +04:00
40da358dc2 feat: better errors for SMTP (#13401)
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2026-02-23 16:00:17 +05:30
957a1b17c9 perf: add default configs for assignment V2 (#13577)
## Description

AutoAssignment::RateLimiter#within_limit? returned true early for
inboxes without an AssignmentPolicy, bypassing fair distribution
entirely and allowing unlimited conversation assignment.

## Type of change

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

## Script

- Script to enable users with V2 assignment:
https://www.notion.so/chatwoot/Script-to-migrate-account-to-assignment-V2-30ca5f274c9280f5b8ecfd15e28eeb9c?source=copy_link

## 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: Shivam Mishra <scm.mymail@gmail.com>
2026-02-23 15:08:11 +05:30
Aakash BakhleandGitHub 9dd13b9a2b fix: topup checkout flaky test (#13616) 2026-02-23 14:54:52 +05:30
2441487a76 perf: skip conversation loading in /meta endpoint (#13564)
# Pull Request Template

## Summary
- Adds `perform_meta_only` method to `ConversationFinder` that runs
setup and counts without loading the paginated conversation list
- Updates `/api/v1/conversations/meta` to use `perform_meta_only`
instead of `perform`

## Problem
The `/meta` endpoint calls `ConversationFinder#perform` which:
1. Runs all filters and setup (`set_up`)
2. Computes 3 COUNT queries (`set_count_for_all_conversations`)
3. Filters by assignee type
4. **Builds the full paginated conversation list** with
`.includes(:taggings, :inbox, {assignee: {avatar_attachment: [:blob]}},
{contact: {avatar_attachment: [:blob]}}, :team, :contact_inbox)` +
sorting + pagination

The controller then **discards the conversations** and only uses the
counts:
```ruby
def meta
result = conversation_finder.perform
@conversations_count = result[:count]  # conversations thrown away
end
```

## Type of change

- [x] Performance fix

## How Has This Been Tested?

- [ ] Verify /meta returns correct mine/unassigned/assigned/all counts
- [ ] Verify counts update when switching inbox, team, or status filters
- [ ] Verify conversation list still loads correctly (uses perform, not
affected)
- [ ] Monitor response time reduction for /meta in NewRelic after deploy

## 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: Pranav <pranav@chatwoot.com>
2026-02-20 21:20:19 +05:30
Sivin VargheseandGitHub 418bd177f8 fix: Adjust inbox settings pages layout width (#13590)
# Pull Request Template

## Description

This PR includes,

1. Adjusting the inbox settings page layout width from 3xl to 4xl for
the collaborators, configuration, and bot configuration sections.
2. Adding a dynamic max-width for inbox settings banners based on the
selected tab.
3. Making the sender name preview layout responsive.
4. Reordering automation rule row buttons so Clone appears before
Delete.
5. Update the Gmail icon ratio.
6. Fix height issues with team/inbox pages
7. The delete button changes to red on hover
8. Add border to conversation header when no dashboard apps present


## Type of change

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



## 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-02-20 20:20:32 +05:30
Shivam Mishra 572f5b2709 Merge branch 'hotfix/4.11.1' into develop 2026-02-20 20:02:39 +05:30
Shivam Mishra a08125e283 Merge branch 'hotfix/4.11.1'
Publish Chatwoot EE docker images / merge (push) Blocked by required conditions
Publish Chatwoot EE docker images / build (linux/amd64, ubuntu-latest) (push) Waiting to run
Publish Chatwoot EE docker images / build (linux/arm64, ubuntu-22.04-arm) (push) Waiting to run
Publish Chatwoot CE docker images / build (linux/amd64, ubuntu-latest) (push) Waiting to run
Publish Chatwoot CE docker images / build (linux/arm64, ubuntu-22.04-arm) (push) Waiting to run
Publish Chatwoot CE docker images / merge (push) Blocked by required conditions
2026-02-20 20:02:18 +05:30
Shivam Mishra 15f25f019e chore: bump version 2026-02-20 20:02:09 +05:30
Shivam Mishra 280ca06e5b fix: url endpoint
fix: spec
2026-02-20 20:01:14 +05:30
d8f4bb940e feat: add resolve_conversation tool for Captain V2 scenarios (#13597)
# Pull Request Template

## Description

Adds a new built-in tool that allows Captain scenarios to resolve
conversations programmatically. This enables automated workflows like
the misdirected contact deflector to close conversations after handling
them, while still allowing human review via label filtering.


## Type of change

Please delete options that are not relevant.

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

tested by mentioning it to be used in captain v2 scenario

<img width="1180" height="828" alt="image"
src="https://github.com/user-attachments/assets/e70baf96-0c70-407e-af2c-328500ac5434"
/>



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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Tanmay Deep Sharma <32020192+tds-1@users.noreply.github.com>
2026-02-20 19:08:36 +05:30
db7e02b93b feat: captain channel type langfuse metadata (#13574)
# Pull Request Template

## Description

Adds channel type to Captain assistant traces in Langfuse

## 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="906" height="672" alt="image"
src="https://github.com/user-attachments/assets/224cee95-56aa-4672-8f74-0c0052251db9"
/>

<img width="908" height="611" alt="image"
src="https://github.com/user-attachments/assets/ddd8ef0d-47c1-450c-a09f-27e82a34d04d"
/>


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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:16:43 +05:30
NatãandGitHub dbab0fe8da fix: search header overlap with new conversation form (#13548) 2026-02-20 11:24:37 +05:30
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
26c38a90f2 chore(deps): bump nokogiri from 1.18.9 to 1.19.1 (#13586)
Bumps [nokogiri](https://github.com/sparklemotion/nokogiri) from 1.18.9
to 1.19.1.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/sparklemotion/nokogiri/releases">nokogiri's
releases</a>.</em></p>
<blockquote>
<h2>v1.19.1 / 2026-02-16</h2>
<h3>Security</h3>
<ul>
<li>[CRuby] Address unchecked return value from
<code>xmlC14NExecute</code> which was a contributing cause to ruby-saml
GHSA-x4h9-gwv3-r4m4. See <a
href="https://github.com/sparklemotion/nokogiri/security/advisories/GHSA-wx95-c6cv-8532">GHSA-wx95-c6cv-8532</a>
for more information.</li>
</ul>
<!-- raw HTML omitted -->

<pre><code>cfdb0eafd9a554a88f12ebcc688d2b9005f9fce42b00b970e3dc199587b27f32
nokogiri-1.19.1-aarch64-linux-gnu.gem
1e2150ab43c3b373aba76cd1190af7b9e92103564063e48c474f7600923620b5
nokogiri-1.19.1-aarch64-linux-musl.gem
0a39ed59abe3bf279fab9dd4c6db6fe8af01af0608f6e1f08b8ffa4e5d407fa3
nokogiri-1.19.1-arm-linux-gnu.gem
3a18e559ee499b064aac6562d98daab3d39ba6cbb4074a1542781b2f556db47d
nokogiri-1.19.1-arm-linux-musl.gem
dfe2d337e6700eac47290407c289d56bcf85805d128c1b5a6434ddb79731cb9e
nokogiri-1.19.1-arm64-darwin.gem
1e0bda88b1c6409f0edb9e0c25f1bf9ff4fa94c3958f492a10fcf50dda594365
nokogiri-1.19.1-java.gem
110d92ae57694ae7866670d298a5d04cd150fae5a6a7849957d66f171e6aec9b
nokogiri-1.19.1-x64-mingw-ucrt.gem
7093896778cc03efb74b85f915a775862730e887f2e58d6921e3fa3d981e68bf
nokogiri-1.19.1-x86_64-darwin.gem
1a4902842a186b4f901078e692d12257678e6133858d0566152fe29cdb98456a
nokogiri-1.19.1-x86_64-linux-gnu.gem
4267f38ad4fc7e52a2e7ee28ed494e8f9d8eb4f4b3320901d55981c7b995fc23
nokogiri-1.19.1-x86_64-linux-musl.gem
598b327f36df0b172abd57b68b18979a6e14219353bca87180c31a51a00d5ad3
nokogiri-1.19.1.gem
</code></pre>
<!-- raw HTML omitted -->
<h2>v1.19.0 / 2025-12-28</h2>
<h4>Ruby</h4>
<p>This release is focused on changes to Ruby version support, and is
otherwise functionally identical to v1.18.10.</p>
<ul>
<li>Introduce native gem support for Ruby 4.0. <a
href="https://redirect.github.com/sparklemotion/nokogiri/issues/3590">#3590</a></li>
<li>End support for Ruby 3.1, for which <a
href="https://www.ruby-lang.org/en/downloads/branches/">upstream support
ended 2025-03-26</a>.</li>
<li>End support for JRuby 9.4 (which targets Ruby 3.1
compatibility).</li>
</ul>
<!-- raw HTML omitted -->

<pre><code>11a97ecc3c0e7e5edcf395720b10860ef493b768f6aa80c539573530bc933767
nokogiri-1.19.0-aarch64-linux-gnu.gem
eb70507f5e01bc23dad9b8dbec2b36ad0e61d227b42d292835020ff754fb7ba9
nokogiri-1.19.0-aarch64-linux-musl.gem
572a259026b2c8b7c161fdb6469fa2d0edd2b61cd599db4bbda93289abefbfe5
nokogiri-1.19.0-arm-linux-gnu.gem
23ed90922f1a38aed555d3de4d058e90850c731c5b756d191b3dc8055948e73c
nokogiri-1.19.0-arm-linux-musl.gem
0811dfd936d5f6dd3f6d32ef790568bf29b2b7bead9ba68866847b33c9cf5810
nokogiri-1.19.0-arm64-darwin.gem
5f3a70e252be641d8a4099f7fb4cc25c81c632cb594eec9b4b8f2ca8be4374f3
nokogiri-1.19.0-java.gem
05d7ed2d95731edc9bef2811522dc396df3e476ef0d9c76793a9fca81cab056b
nokogiri-1.19.0-x64-mingw-ucrt.gem
1dad56220b603a8edb9750cd95798bffa2b8dd9dd9aa47f664009ee5b43e3067
nokogiri-1.19.0-x86_64-darwin.gem
f482b95c713d60031d48c44ce14562f8d2ce31e3a9e8dd0ccb131e9e5a68b58c
nokogiri-1.19.0-x86_64-linux-gnu.gem
1c4ca6b381622420073ce6043443af1d321e8ed93cc18b08e2666e5bd02ffae4
nokogiri-1.19.0-x86_64-linux-musl.gem
e304d21865f62518e04f2bf59f93bd3a97ca7b07e7f03952946d8e1c05f45695
nokogiri-1.19.0.gem
</code></pre>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/sparklemotion/nokogiri/blob/main/CHANGELOG.md">nokogiri's
changelog</a>.</em></p>
<blockquote>
<h2>v1.19.1 / 2026-02-16</h2>
<h3>Security</h3>
<ul>
<li>[CRuby] Address unchecked return value from
<code>xmlC14NExecute</code> which was a contributing cause to ruby-saml
GHSA-x4h9-gwv3-r4m4. See <a
href="https://github.com/sparklemotion/nokogiri/security/advisories/GHSA-wx95-c6cv-8532">GHSA-wx95-c6cv-8532</a>
for more information.</li>
</ul>
<h2>v1.19.0 / 2025-12-28</h2>
<h4>Ruby</h4>
<p>This release is focused on changes to Ruby version support, and is
otherwise functionally identical to v1.18.10.</p>
<ul>
<li>Introduce native gem support for Ruby 4.0. <a
href="https://redirect.github.com/sparklemotion/nokogiri/issues/3590">#3590</a></li>
<li>End support for Ruby 3.1, for which <a
href="https://www.ruby-lang.org/en/downloads/branches/">upstream support
ended 2025-03-26</a>.</li>
<li>End support for JRuby 9.4 (which targets Ruby 3.1
compatibility).</li>
</ul>
<h2>v1.18.10 / 2025-09-15</h2>
<h3>Dependencies</h3>
<ul>
<li>[CRuby] Vendored libxml2 is updated to <a
href="https://gitlab.gnome.org/GNOME/libxml2/-/releases/v2.13.9">v2.13.9</a>.
Note that the security fixes published in v2.13.9 were already present
in Nokogiri v1.18.9.</li>
<li>[CRuby] [Windows and MacOS] Vendored libiconv is updated to <a
href="https://savannah.gnu.org/news/?id=10703">v1.18</a></li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/sparklemotion/nokogiri/commit/d9130457369de8a6efcb764e6da2cb80d5d3b6dd"><code>d913045</code></a>
version bump to v1.19.1</li>
<li><a
href="https://github.com/sparklemotion/nokogiri/commit/b81cb9869e8ed7d1785da3363ef490f455da96eb"><code>b81cb98</code></a>
doc: update CHANGELOG for upcoming v1.19.1</li>
<li><a
href="https://github.com/sparklemotion/nokogiri/commit/8e668095c6147def4a3ec044df5f2a478c8161c3"><code>8e66809</code></a>
C14n raise on failure (<a
href="https://redirect.github.com/sparklemotion/nokogiri/issues/3600">#3600</a>)</li>
<li><a
href="https://github.com/sparklemotion/nokogiri/commit/5b77f3d1c48cc09c92d10046c448a0866380eb4a"><code>5b77f3d</code></a>
Raise RuntimeError when canonicalization fails</li>
<li><a
href="https://github.com/sparklemotion/nokogiri/commit/edc559565819459d92f6db609f068f50491a57f9"><code>edc5595</code></a>
Thank sponsors in the README</li>
<li><a
href="https://github.com/sparklemotion/nokogiri/commit/d4dc245dfafd7ba42538051b0979306c8e5dc6f2"><code>d4dc245</code></a>
dep: update rdoc to v7</li>
<li><a
href="https://github.com/sparklemotion/nokogiri/commit/d77bfb66302532b90c0f340ed6b4ae74f275dde8"><code>d77bfb6</code></a>
version bump to v1.19.0</li>
<li><a
href="https://github.com/sparklemotion/nokogiri/commit/1eb5c2c035b360fd1195de0b274e901b6e0c12dd"><code>1eb5c2c</code></a>
dev: convert scripts/test-gem-set to use mise</li>
<li><a
href="https://github.com/sparklemotion/nokogiri/commit/88a120fd8198cd49b7cbe6388c92cd92d776407d"><code>88a120f</code></a>
dep: Add native Ruby 4 support, drop Ruby 3.1 support (v1.19.x) (<a
href="https://redirect.github.com/sparklemotion/nokogiri/issues/3592">#3592</a>)</li>
<li><a
href="https://github.com/sparklemotion/nokogiri/commit/f8c8f74e846ea49d2cb221710cc08618842ba21e"><code>f8c8f74</code></a>
Skip the parser compression test for Windows system libs</li>
<li>Additional commits viewable in <a
href="https://github.com/sparklemotion/nokogiri/compare/v1.18.9...v1.19.1">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=nokogiri&package-manager=bundler&previous-version=1.18.9&new-version=1.19.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/chatwoot/chatwoot/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-19 17:55:08 -08:00
PranavandGitHub f826dc2d15 fix: Rate-limit meta endpoint calls to 30/min (#13596)
Meta endpoints are now rate limited to 1 call per every minute. This rate limit is done at the user level not the browser.
2026-02-19 17:48:06 -08:00
Sivin VargheseandGitHub 6902969a09 chore: Remove vue-multiselect package and styles from codebase (#13585) 2026-02-19 15:42:34 +05:30
Sivin VargheseandGitHub 7b2b3ac37d feat(V5): Update settings pages UI (#13396)
# Pull Request Template

## Description

This PR updates settings page UI


## Type of change

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


## 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
- [ ] 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-02-19 15:04:40 +05:30
c9619eaed2 chore: ignore .claude directory in gitignore (#13584)
# Pull Request Template
adds .claude to gitignore

Co-authored-by: Tanmay Deep Sharma <32020192+tds-1@users.noreply.github.com>
2026-02-19 13:55:15 +05:30
Sojan JoseandGitHub 2ab117e8eb feat(cloud-billing): cancel subscriptions at period end on deletion mark (#13580)
## How to reproduce
In Chatwoot Cloud, mark an account for deletion from account settings
while the account has an active Stripe subscription. Before this change,
deletion marking did not explicitly mark subscriptions to stop renewing
at period end.

## What changed
This PR adds `Enterprise::Billing::CancelCloudSubscriptionsService` and
calls it from the delete action path in
`Enterprise::Api::V1::AccountsController`. The service lists only active
Stripe subscriptions for the customer and sets `cancel_at_period_end:
true` when needed. The account deletion schedule remains unchanged
(existing static 7-day behavior), and Stripe deleted-event fallback
behavior remains unchanged.

## How this was tested
Added and updated specs:
-
`spec/enterprise/services/enterprise/billing/cancel_cloud_subscriptions_service_spec.rb`
-
`spec/enterprise/controllers/enterprise/api/v1/accounts_controller_spec.rb`

Executed:
- `bundle exec rspec
spec/enterprise/services/enterprise/billing/cancel_cloud_subscriptions_service_spec.rb`
- `bundle exec rspec
spec/enterprise/controllers/enterprise/api/v1/accounts_controller_spec.rb:363`
2026-02-19 11:40:06 +05:30
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
594333a183 chore(deps): bump rack from 3.2.3 to 3.2.5 (#13569)
Bumps [rack](https://github.com/rack/rack) from 3.2.3 to 3.2.5.
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/rack/rack/blob/main/CHANGELOG.md">rack's
changelog</a>.</em></p>
<blockquote>
<h1>Changelog</h1>
<p>All notable changes to this project will be documented in this file.
For info on how to format all future additions to this file please
reference <a href="https://keepachangelog.com/en/1.0.0/">Keep A
Changelog</a>.</p>
<h2>Unreleased</h2>
<h3>Security</h3>
<ul>
<li><a
href="https://github.com/advisories/GHSA-r657-rxjc-j557">CVE-2025-61780</a>
Improper handling of headers in <code>Rack::Sendfile</code> may allow
proxy bypass.</li>
<li><a
href="https://github.com/advisories/GHSA-6xw4-3v39-52mm">CVE-2025-61919</a>
Unbounded read in <code>Rack::Request</code> form parsing can lead to
memory exhaustion.</li>
<li><a
href="https://github.com/advisories/GHSA-whrj-4476-wvmp">CVE-2026-25500</a>
XSS injection via malicious filename in
<code>Rack::Directory</code>.</li>
<li><a
href="https://github.com/advisories/GHSA-mxw3-3hh2-x2mh">CVE-2026-22860</a>
Directory traversal via root prefix bypass in
<code>Rack::Directory</code>.</li>
</ul>
<h3>SPEC Changes</h3>
<ul>
<li>Define <code>rack.response_finished</code> callback arguments more
strictly. (<a
href="https://redirect.github.com/rack/rack/pull/2365">#2365</a>, <a
href="https://github.com/skipkayhil"><code>@​skipkayhil</code></a>)</li>
</ul>
<h3>Added</h3>
<ul>
<li>Add <code>Rack::Files#assign_headers</code> to allow overriding how
the configured file headers are set. (<a
href="https://redirect.github.com/rack/rack/pull/2377">#2377</a>, <a
href="https://github.com/codergeek121"><code>@​codergeek121</code></a>)</li>
<li>Add support for <code>rack.response_finished</code> to
<code>Rack::TempfileReaper</code>. (<a
href="https://redirect.github.com/rack/rack/pull/2363">#2363</a>, <a
href="https://github.com/skipkayhil"><code>@​skipkayhil</code></a>)</li>
<li>Add support for streaming bodies when using
<code>Rack::Events</code>. (<a
href="https://redirect.github.com/rack/rack/blob/main/redirect.github.com/rack/rack/pull/2375">#2375</a>,
<a href="https://github.com/unflxw"><code>@​unflxw</code></a>)</li>
<li>Add <code>deflaters</code> option to <code>Rack::Deflater</code> to
enable custom compression algorithms like zstd. (<a
href="https://redirect.github.com/rack/rack/issues/2168">#2168</a>, <a
href="https://github.com/alexanderadam"><code>@​alexanderadam</code></a>)</li>
<li>Add <code>Rack::Request#prefetch?</code> for identifying requests
with <code>Sec-Purpose: prefetch</code> header set. (<a
href="https://redirect.github.com/rack/rack/pull/2405">#2405</a>, <a
href="https://github.com/glaszig"><code>@​glaszig</code></a>)</li>
<li>Add <code>rack.request.trusted_proxy</code> environment key to
indicate whether the request is coming from a trusted proxy.</li>
</ul>
<h3>Changed</h3>
<ul>
<li>Raise before exceeding a part limit, not after. (<a
href="https://redirect.github.com/rack/rack/pull/2362">#2362</a>, <a
href="https://github.com/matthew-puku"><code>@​matthew-puku</code></a>)</li>
<li>Rack::Deflater now uses a fixed GZip mtime value. (<a
href="https://redirect.github.com/rack/rack/pull/2372">#2372</a>, <a
href="https://github.com/bensheldon"><code>@​bensheldon</code></a>)</li>
<li>Multipart parser drops support for RFC 2231 <code>filename*</code>
parameter (prohibited by RFC 7578) and now properly handles UTF-8
encoded filenames via percent-encoding and direct UTF-8 bytes. (<a
href="https://redirect.github.com/rack/rack/pull/2398">#2398</a>, <a
href="https://github.com/wtn"><code>@​wtn</code></a>)</li>
<li>The query parser now raises
<code>Rack::QueryParser::IncompatibleEncodingError</code> if we try to
parse params that are not ASCII compatible. (<a
href="https://redirect.github.com/rack/rack/pull/2416">#2416</a>, <a
href="https://github.com/bquorning"><code>@​bquorning</code></a>)</li>
</ul>
<h3>Fixed</h3>
<ul>
<li>Multipart parser: limit MIME header size check to the unread buffer
region to avoid false <code>multipart mime part header too large</code>
errors when previously read data accumulates in the scan buffer. (<a
href="https://redirect.github.com/rack/rack/pull/2392">#2392</a>, <a
href="https://github.com/alpaca-tc"><code>@​alpaca-tc</code></a>, <a
href="https://github.com/willnet"><code>@​willnet</code></a>, <a
href="https://github.com/krororo"><code>@​krororo</code></a>)</li>
<li>Fix <code>Rack::MockResponse#body</code> when the body is a Proc.
(<a href="https://redirect.github.com/rack/rack/pull/2420">#2420</a>, <a
href="https://redirect.github.com/rack/rack/pull/2423">#2423</a>, <a
href="https://github.com/tavianator"><code>@​tavianator</code></a>, [<a
href="https://github.com/ioquatix"><code>@​ioquatix</code></a>])</li>
</ul>
<h2>[3.2.4] - 2025-11-03</h2>
<h3>Fixed</h3>
<ul>
<li>Multipart parser: limit MIME header size check to the unread buffer
region to avoid false <code>multipart mime part header too large</code>
errors when previously read data accumulates in the scan buffer. (<a
href="https://redirect.github.com/rack/rack/pull/2392">#2392</a>, <a
href="https://github.com/alpaca-tc"><code>@​alpaca-tc</code></a>, <a
href="https://github.com/willnet"><code>@​willnet</code></a>, <a
href="https://github.com/krororo"><code>@​krororo</code></a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/rack/rack/commit/bb5f3555bd12b9065112353e829298b3b5623ceb"><code>bb5f355</code></a>
Bump patch version.</li>
<li><a
href="https://github.com/rack/rack/commit/f9bde3bc2dde2771185ac1a7b7602a4d9fa0a0d8"><code>f9bde3b</code></a>
Prevent directory traversal via root prefix bypass.</li>
<li><a
href="https://github.com/rack/rack/commit/93a68f58aa82aa48f09b751501f19f5e760dd406"><code>93a68f5</code></a>
XSS injection via malicious filename in
<code>Rack::Directory</code>.</li>
<li><a
href="https://github.com/rack/rack/commit/3b8b0d22d68a7fb30fdea40f838d0f95a05c134d"><code>3b8b0d2</code></a>
Fix MockResponse#body when the body is a Proc (<a
href="https://redirect.github.com/rack/rack/issues/2420">#2420</a>)</li>
<li><a
href="https://github.com/rack/rack/commit/4c24539777db8833d78f881680cd245878cfba31"><code>4c24539</code></a>
Bump patch version.</li>
<li><a
href="https://github.com/rack/rack/commit/3ba5e4f22f55abac21037bb137e56e5c8e36b673"><code>3ba5e4f</code></a>
Allow Multipart head to span read boundary. (<a
href="https://redirect.github.com/rack/rack/issues/2392">#2392</a>)</li>
<li>See full diff in <a
href="https://github.com/rack/rack/compare/v3.2.3...v3.2.5">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=rack&package-manager=bundler&previous-version=3.2.3&new-version=3.2.5)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/chatwoot/chatwoot/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-17 16:12:58 -08:00
Sojan Jose a238034153 Merge branch 'release/4.11.0' into develop 2026-02-17 15:40:49 -08:00
Sojan Jose 2bd1d88d50 Merge branch 'release/4.11.0'
Publish Chatwoot EE docker images / build (linux/amd64, ubuntu-latest) (push) Waiting to run
Publish Chatwoot EE docker images / build (linux/arm64, ubuntu-22.04-arm) (push) Waiting to run
Publish Chatwoot EE docker images / merge (push) Blocked by required conditions
Publish Chatwoot CE docker images / build (linux/amd64, ubuntu-latest) (push) Waiting to run
Publish Chatwoot CE docker images / build (linux/arm64, ubuntu-22.04-arm) (push) Waiting to run
Publish Chatwoot CE docker images / merge (push) Blocked by required conditions
2026-02-17 15:40:39 -08:00
Sojan Jose e8152642f2 Bump version to 4.11.0 2026-02-17 15:35:20 -08:00
dae4f3ee13 fix: move llm call of captain outside transaction (#13559)
# Pull Request Template

## Description

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

The LLM call was wrapped in a transaction. This is an anti-pattern and
caused idle-connections which PG eventually terminated with
`PQconsumeInput() FATAL: terminating connection due to
idle-in-transaction timeout`

This resulted in activity messages being missing in some conversations
on captain handoff, failures queueing up for retry and captain
responding long after conversation was marked open/snoozed.

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


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

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2026-02-17 18:12:14 +05:30
e75e8a77f6 feat(shopify): Add mandatory compliance webhooks with HMAC verification (#13549)
Fixes
https://linear.app/chatwoot/issue/CW-6494/add-shopify-mandatory-compliance-webhooks-for-app-store-listing

Shopify requires all public apps to handle three GDPR compliance
webhooks before they can be listed on the App Store. Their automated
review checks for these endpoints and verifies that apps validate HMAC
signatures on incoming requests. We were failing both checks.

This PR adds a single webhook endpoint at `POST /webhooks/shopify` that
receives all three compliance events. When Shopify sends a webhook, it
signs the payload with our app's client secret and includes the
signature in the `X-Shopify-Hmac-SHA256` header. Our controller reads
the raw body, computes the expected HMAC-SHA256 digest, and rejects
mismatched requests with a 401.

Shopify identifies the event type through the `X-Shopify-Topic` header.
For `customers/data_request` and `customers/redact`, we simply
acknowledge with a 200—Chatwoot doesn't persist any Shopify customer
data. All order lookups happen as live API calls at query time. For
`shop/redact`, which Shopify sends after a merchant uninstalls the app,
we delete the integration hook for that shop domain and remove the
stored access token and configuration.


### How to test via Rails console
```
secret = GlobalConfigService.load('SHOPIFY_CLIENT_SECRET', nil)
body = '{"shop_domain":"test.myshopify.com"}'
valid_hmac = Base64.strict_encode64(OpenSSL::HMAC.digest('SHA256', secret, body))
```

  #### Test 1: No HMAC → 401
```
app.post '/webhooks/shopify', params: body, headers: { 'Content-Type' => 'application/json', 'X-Shopify-Topic' => 'customers/data_request' }
app.response.code  # => "401"
```
  ####  Test 2: Invalid HMAC → 401
```
app.post '/webhooks/shopify', params: body, headers: { 'Content-Type' => 'application/json', 'X-Shopify-Hmac-SHA256' => 'invalid', 'X-Shopify-Topic' => 'customers/data_request' }
app.response.code  # => "401"
```
  ####  Test 3: Valid HMAC, customers/data_request → 200
```
app.post '/webhooks/shopify', params: body, headers: { 'Content-Type' => 'application/json', 'X-Shopify-Hmac-SHA256' => valid_hmac, 'X-Shopify-Topic' => 'customers/data_request' }
app.response.code  # => "200"
```

####  Test 4: Valid HMAC, customers/redact → 200
```
app.post '/webhooks/shopify', params: body, headers: { 'Content-Type' => 'application/json', 'X-Shopify-Hmac-SHA256' => valid_hmac, 'X-Shopify-Topic' => 'customers/redact' }
app.response.code  # => "200"
```

#### Test 5: Valid HMAC, shop/redact → 200 (deletes hook)
```  
# First check if a hook exists for this domain:
Integrations::Hook.where(app_id: 'shopify', reference_id: 'test.myshopify.com').count
app.post '/webhooks/shopify', params: body, headers: { 'Content-Type' => 'application/json', 'X-Shopify-Hmac-SHA256' => valid_hmac, 'X-Shopify-Topic' => 'shop/redact' }
app.response.code  # => "200"
```

---------

Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
2026-02-17 16:52:13 +05:30
229f56d6e3 chore: Remove vue-multiselect and migrate to next components (#13506)
# Pull Request Template

## Description

This PR includes:
1. Removes multiselect usage from the Merge Contact modal (Conversation
sidebar) and replaces it with the existing component used on the Contact
Details page.
2. Replaces legacy form and multiselect elements in Add and Edit
automations flows with next components.**(Also check Macros)**
3. Replace multiselect with ComboBox in contact form country field.
4. Replace multiselect with TagInput in create/edit attribute form.
5. Replace multiselect with TagInput for agent selection in inbox
creation.
6. Replace multiselect with ComboBox in Facebook channel page selection

## Type of change

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

## How Has This Been Tested?

**Screenshots**

1. **Merge modal**
<img width="741" height="449" alt="image"
src="https://github.com/user-attachments/assets/a05a96ec-0692-4d94-9e27-d3e85fd143e4"
/>
<img width="741" height="449" alt="image"
src="https://github.com/user-attachments/assets/fc1dc977-689d-4440-869d-2124e4ca9083"
/>

2. **Automations**
<img width="849" height="1089" alt="image"
src="https://github.com/user-attachments/assets/b0155f06-ab21-4f90-a2c8-5bfbd97b08f7"
/>
<img width="813" height="879" alt="image"
src="https://github.com/user-attachments/assets/0921ac4a-88f5-49ac-a776-cc02941b479c"
/>
<img width="849" height="826" alt="image"
src="https://github.com/user-attachments/assets/44358dae-a076-4e10-b7ba-a4e40ccd817f"
/>

3. **Country field**
<img width="462" height="483" alt="image"
src="https://github.com/user-attachments/assets/d5db9aa1-b859-4327-9960-957d7091678f"
/>

4. **Add/Edit attribute form**
<img width="619" height="646" alt="image"
src="https://github.com/user-attachments/assets/6ab2ea94-73e5-40b8-ac29-399c0543fa7b"
/>
<img width="619" height="646" alt="image"
src="https://github.com/user-attachments/assets/b4c5bb0e-baa0-4ef7-a6a2-adb0f0203243"
/>
<img width="635" height="731" alt="image"
src="https://github.com/user-attachments/assets/74890c80-b213-4567-bf5f-4789dda39d2d"
/>

5. **Agent selection in inbox creation**
<img width="635" height="534" alt="image"
src="https://github.com/user-attachments/assets/0003bad1-1a75-4f20-b014-587e1c19a620"
/>
<img width="809" height="602" alt="image"
src="https://github.com/user-attachments/assets/5e7ab635-7340-420a-a191-e6cd49c02704"
/>

7. **Facebook channel page selection**
<img width="597" height="444" alt="image"
src="https://github.com/user-attachments/assets/f7ec8d84-0a7d-4bc6-92a1-a1365178e319"
/>
<img width="597" height="444" alt="image"
src="https://github.com/user-attachments/assets/d0596c4d-94c1-4544-8b50-e7103ff207a6"
/>
<img width="597" height="444" alt="image"
src="https://github.com/user-attachments/assets/be097921-011b-4dbe-b5f1-5d1306e25349"
/>



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

---------

Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
2026-02-17 16:40:12 +05:30
Aakash BakhleandGitHub 138840a23f fix: typo in metadata key in captain v2 (#13558)
# Pull Request Template

## Description

## Type of change

typo fix

## 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
- [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-02-17 15:40:50 +05:30
Sivin VargheseandGitHub c5f6844877 fix: Disable reply editor outside WhatsApp reply window (#13454) 2026-02-17 14:07:36 +05:30
Shivam MishraandGitHub 39243b9e71 fix: duplicate message_created webhooks for WhatsApp messages (#13523)
Some customers using WhatsApp inboxes with account-level webhooks were
reporting receiving duplicate `message_created` webhook deliveries for
every incoming message. Upon inspection, here's what we found

- Both payloads are identical.
- No errors appear in the application logs
- Webhook URL is only configured in one place. 

This meant, the system was sending the webhooks twice. For some context,
there's a know related issue... Meta's WhatsApp Business API can deliver
the same webhook notification multiple times for a single message. The
codebase already acknowledges this — there's a comment in
`IncomingMessageBaseService#process_messages` noting that "multiple
webhook events can be received against the same message due to
misconfigurations in the Meta business manager account." A deduplication
guard exists, but it doesn't actually work under concurrency.

### Rationale

The existing dedup was a three-step sequence: check Redis (`GET`), check
the database, then set a Redis flag (`SETEX`). Two Sidekiq workers
processing duplicate Meta webhooks simultaneously would both complete
the `GET` before either executed the `SETEX`, so both would proceed to
create a message. The `source_id` column has a non-unique index, so the
database wouldn't catch the duplicate either. Each message then
independently fires `after_create_commit`, dispatching two
`message_created` webhook events to the customer.

```
             Worker A                          Worker B
                │                                 │
                ▼                                 ▼
        Redis GET key ──► nil               Redis GET key ──► nil
                │                                 │
                │    ◄── both pass guard ──►      │
                │                                 │
                ▼                                 ▼
        Redis SETEX key                    Redis SETEX key
                │                                 │
                ▼                                 ▼
        BEGIN transaction               BEGIN transaction
        INSERT message                   INSERT message
        DELETE Redis key ◄─┐                      │
        COMMIT             │             DELETE Redis key
                           │             COMMIT
                           │                      │
                           └── key gone before ───┘
                              B's commit lands

                ▼                                 ▼
        after_create_commit              after_create_commit
        dispatch MESSAGE_CREATED         dispatch MESSAGE_CREATED
                │                                 │
                ▼                                 ▼
        WebhookJob ──► n8n               WebhookJob ──► n8n
                    (duplicate!)
```

There was a second, subtler problem visible in the diagram: the Redis
key was cleared *inside* the database transaction, before the
transaction committed. This opened a window where neither the Redis
check nor the database check would see the in-flight message.

The fix collapses the check-and-set into a single `SET NX EX` call,
which is atomic in Redis. The key is no longer eagerly cleared — it
expires naturally after 24 hours. The database lookup
(`find_message_by_source_id`) remains as a fallback for messages that
were created before the lock expired.

```
             Worker A                          Worker B
                │                                 │
                ▼                                 ▼
        Redis SET NX ──► OK              Redis SET NX ──► nil
                │                                 │
                ▼                                 ▼
        proceeds to create              returns early
        message normally                (lock already held)
```

### Implementation Notes

The lock logic is extracted into `Whatsapp::MessageDedupLock`, a small
class that wraps a single `Redis SET NX EX` call. This makes the
concurrency guarantee testable in isolation — the spec uses a
`CyclicBarrier` to race two threads against the same key and asserts
exactly one wins, without needing database writes,
`use_transactional_tests = false`, or monkey-patching.

Because the Redis lock now persists (instead of being cleared
mid-transaction), existing WhatsApp specs needed an `after` hook to
clean up `MESSAGE_SOURCE_KEY::*` keys between examples. Transactional
fixtures only roll back the database, not Redis.
2026-02-17 14:01:10 +05:30
Sivin VargheseandGitHub fb2f5e1d42 fix: Persist compose form state on accidental outside click (#13529) 2026-02-17 13:57:44 +05:30
Sivin VargheseandGitHub cfe3061b5d feat: Allow removing labels via conversation context menu (#13525)
# Pull Request Template

## Description

This PR adds support for removing labels from the conversation card
context menu. Assigned labels now show a checkmark, and clicking an
already-selected label will remove it.

Fixes
https://linear.app/chatwoot/issue/CW-6400/allow-removing-labels-directly-from-the-right-click-menu
https://github.com/chatwoot/chatwoot/issues/13367
## Type of change

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

## How Has This Been Tested?

**Screencast**


https://github.com/user-attachments/assets/4e3a6080-a67d-4851-9d10-d8dbf3ceeb04




## 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
- [ ] 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-02-17 13:30:55 +05:30
Aakash BakhleandGitHub aa7e3c2d38 feat: langfuse logging improvements (#13534)
Langfuse logging improvements

## Description

Please include a summary of the change and issue(s) fixed. Also, mention
relevant motivation, context, and any dependencies that this change
requires.
Fixes # (issue)
For reply suggestion: the errors are being stored inside output field,
but observations should be marked as errors.
For assistant: add credit_used metadata to filter handoffs from
ai-replies
For langfuse tool call: add `observation_type=tool`

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

before:
<img width="1028" height="57" alt="image"
src="https://github.com/user-attachments/assets/70f6a36e-6c33-444c-a083-723c7c9e823a"
/>

after:
<img width="872" height="69" alt="image"
src="https://github.com/user-attachments/assets/1b6b6f5f-5384-4e9c-92ba-f56748fec6dd"
/>

`credit_used` to filter handoffs from AI replies that cause credit usage
<img width="1082" height="672" alt="image"
src="https://github.com/user-attachments/assets/90914227-553a-4c03-bc43-56b2018ac7c1"
/>


set `observation_type` to `tool`
<img width="726" height="1452" alt="image"
src="https://github.com/user-attachments/assets/e639cc9b-1c6c-4427-887e-23e5523bf64f"
/>


## 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
- [ ] 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-02-17 13:30:04 +05:30
3874383698 feat: insrument captain v2 (#13439)
# Pull Request Template

## Description

Instruments captain v2

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

Local testing:
<img width="864" height="510" alt="image"
src="https://github.com/user-attachments/assets/855ebce5-e8b8-4d22-b0bb-0d413769a6ab"
/>



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

---------

Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
2026-02-17 13:28:26 +05:30
Aakash BakhleandGitHub 101eca3003 feat: add captain editor events (#13524)
## Description

Adds missing analytics instrumentation for the editor AI funnel so we
can measure end-to-end usage and outcome quality.

### What was added

- Captain: Editor AI menu opened
- Captain: Generation failed
- Captain: AI-assisted message sent

### Behavior covered

- Tracks AI button click + menu open from both entry points:
    - top panel sparkle button
    - inline editor copilot button
- Tracks generation failures (initial + follow-up stages).
- Tracks whether accepted AI content was sent as-is or edited before
send.

### Notes

- Applies to editor Captain accept/send flow
(rewrite/summarize/reply_suggestion + follow-ups).
- Does not change Copilot sidebar flow instrumentation.

## Type of change

- [x] 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?

### Manual verification steps

<img width="1906" height="832" alt="image"
src="https://github.com/user-attachments/assets/f0ade43b-aa8d-41be-8ca2-20a091a81f60"
/>

<img width="828" height="280" alt="image"
src="https://github.com/user-attachments/assets/be76219e-fb61-4a6e-bff5-dc085b0a3cc9"
/>

<img width="415" height="147" alt="image"
src="https://github.com/user-attachments/assets/36802c5c-33a7-49ed-bf7e-f0b02d86dccc"
/>

<img width="2040" height="516" alt="image"
src="https://github.com/user-attachments/assets/74b95288-bc86-4312-a282-14211ae8f25c"
/>


1. Open a conversation with Captain tasks enabled.
2. Click AI button in top panel and inline editor.
3. Confirm analytics events fire for:
    - AI menu opened
4. Run an AI action and force a failure scenario (or empty response
path) and confirm generation-failed event.
5. Accept AI output, then:
    - send without changes -> editedBeforeSend: false
    - edit then send -> editedBeforeSend: 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
- [ ] 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-02-17 13:26:56 +05:30
Sojan JoseandGitHub 61eaa098ae fix(messages): reduce audio transcription 400 retry noise (#13487)
## Summary
This PR reduces duplicate failure noise for audio transcription jobs
that fail with permanent HTTP 400 responses, and fixes a file-format
edge case causing intermittent 400s.

Sentry issue: [CHATWOOT-99E /
6660541334](https://chatwoot-p3.sentry.io/issues/6660541334/)

## Confirmed root cause
For some attachments, the stored filename had no extension (example:
`speech`, content type `audio/mpeg`).
When the temporary transcription upload file was created without an
extension, OpenAI returned:
`Unrecognized file format` (HTTP 400).

## Scope of changes
1. `Messages::AudioTranscriptionJob`
- Keeps `discard_on Faraday::BadRequestError` to avoid retry storms on
permanent request errors.
- Adds explicit Rails warning logs for discarded jobs with
attachment/job/status context.

2. `Messages::AudioTranscriptionService`
- Keeps guaranteed temp file cleanup via `ensure`.
- Ensures temp upload files include an extension when the original
filename has none, derived from blob `content_type`.
- This addresses intermittent failures like extensionless `audio/mpeg`
files.

## Reproduction
Enable audio transcription for an account and process an audio
attachment whose stored filename has no extension (for example `speech`)
but valid audio content type (`audio/mpeg`).
Before this fix, OpenAI transcription could return HTTP 400
`Unrecognized file format` for that attachment while similar attachments
with extensions succeeded.

## Testing
Ran:
`bundle exec rubocop
enterprise/app/jobs/messages/audio_transcription_job.rb
enterprise/app/services/messages/audio_transcription_service.rb`

Result: both modified files pass lint with no offenses.
2026-02-17 13:25:13 +05:30
9cd7c4ef89 fix: Enhance notification emails with message details and handle failed messages (#13273)
## Description

Handle messages with null content properly in UI and email notifications

## Type of change

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

## Relevant Screenshots:
<img width="688" height="765" alt="Screenshot 2026-01-21 at 4 43 00 PM"
src="https://github.com/user-attachments/assets/6a27c22e-2ae6-4377-a05d-cfa44bf181fe"
/>


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

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Touches notification email templates and message rendering conditions;
mistakes could lead to missing content/attachments in emails or
incorrect UI visibility, but changes are localized and non-auth/security
related.
> 
> **Overview**
> Agent notification emails for *assigned* and *participating* new
messages now include the actual message details (sender name, rendered
text when present, and attachment links) and gracefully fall back when
content is unavailable.
> 
> To support this, the mailer now passes `@message` into Liquid via
`MessageDrop` (adding `attachments` URLs), and the dashboard message UI
now renders failed/external-error messages even when `content` is `null`
while tightening retry eligibility to require content or attachments
(and still within 1 day).
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
475c8cedda54eb5e806990f977faf8098d0b27d8. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2026-02-16 14:47:33 +05:30
Tanmay Deep SharmaandGitHub f4538ae2c5 fix: Enforce team boundaries to prevent cross-team assignments (#13353)
## Description

Fixes a critical bug where conversations assigned to a team could be
auto-assigned to agents outside that team when all team members were at
capacity.

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

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Changes core assignment selection for both legacy and v2 flows;
misconfiguration of `allow_auto_assign` or team membership could cause
conversations to remain unassigned.
> 
> **Overview**
> Prevents auto-assignment from crossing team boundaries by filtering
eligible agents to the conversation’s `team` members (and requiring
`team.allow_auto_assign`) in both the legacy `AutoAssignmentHandler`
path and the v2 `AutoAssignment::AssignmentService` (including the
Enterprise override).
> 
> Adds test coverage to ensure team-scoped conversations only assign to
team members, and are skipped when team auto-assign is disabled or no
team members are available; also updates the conversations controller
spec setup to include team membership.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
67ed2bda0cd8ffd56c7e0253b86369dead2e6155. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
2026-02-16 14:39:20 +05:30
Sojan JoseandGitHub fd5ac2a8a3 fix: apply installation branding replacement in tooltip copy (#13538)
## Summary
Fix hardcoded `Chatwoot` branding in two UI tooltips using the existing
`useBranding` flow so self-hosted/white-label deployments no longer show
the wrong brand text.

## Changes
- LabelSuggestion tooltip now uses:
  - `replaceInstallationName($t('LABEL_MGMT.SUGGESTIONS.POWERED_BY'))`
- Message avatar tooltip (native app/external echo) now uses:
  - `replaceInstallationName(t('CONVERSATION.NATIVE_APP_ADVISORY'))`

## Why
This follows the existing branding pattern already used in the product
and keeps behavior consistent across deployments.

## Notes
- No change to message logic or API behavior.
- `AGENTS.md` updated with a branding guidance note.

## Fixes
- Fixes https://github.com/chatwoot/chatwoot/issues/13306
- Fixes https://github.com/chatwoot/chatwoot/issues/13466

## Testing

<img width="195" height="155" alt="Screenshot 2026-02-13 at 3 55 39 PM"
src="https://github.com/user-attachments/assets/5b295cdd-6e5d-42c0-bbd7-23ba7052e1c3"
/>
<img width="721" height="152" alt="Screenshot 2026-02-13 at 3 55 48 PM"
src="https://github.com/user-attachments/assets/19cec2a0-451f-4fb3-bd61-7c2e591fc3c7"
/>
2026-02-13 16:47:25 -08:00
Sojan JoseandGitHub 6b7180d051 fix(twilio): prevent dead jobs on missing channel lookup (#13522)
## Why
We observed `Webhooks::TwilioEventsJob` failures ending up in Sidekiq
dead jobs when Twilio callback payloads could not be mapped to a
`Channel::TwilioSms` record. In this scenario, channel lookup raised
`ActiveRecord::RecordNotFound`, which caused retries and eventual dead
jobs instead of a graceful drop.

Related Sentry issue/search:
-
https://chatwoot-p3.sentry.io/issues/?project=6382945&query=Webhooks%3A%3ATwilioEventsJob%20ActiveRecord%3A%3ARecordNotFound

## What changed
This PR keeps the existing lookup flow but makes it non-raising:
- `app/services/twilio/incoming_message_service.rb`
  - `find_by!` -> `find_by` for account SID + phone lookup
  - Added warning log when channel lookup misses
- `app/services/twilio/delivery_status_service.rb`
  - `find_by!` -> `find_by` for account SID + phone lookup
  - Added warning log when channel lookup misses

## Reproduction
Configure a Twilio webhook callback that reaches Chatwoot but does not
match an existing Twilio channel lookup path. Before this change, the
job raises `RecordNotFound` and can end up in dead jobs after retries.
After this change, the job logs the miss and exits safely.

## Testing
- `bundle exec rspec
spec/services/twilio/incoming_message_service_spec.rb
spec/services/twilio/delivery_status_service_spec.rb`
- `bundle exec rubocop app/services/twilio/incoming_message_service.rb
app/services/twilio/delivery_status_service.rb`
2026-02-13 14:06:12 -08:00
João Pedro Baza Garcia RodriguesandGitHub 4d362da9f0 fix: Prevent user enumeration on password reset endpoint (#13528)
## Description

The current password reset endpoint returns different HTTP status codes
and messages depending on whether the email exists in the system (200
for existing emails, 404 for non-existing ones). This allows attackers
to enumerate valid email addresses via the password reset form.

## Changes

### `app/controllers/devise_overrides/passwords_controller.rb`
- Removed the `if/else` branch that returned different responses based
on email existence
- Now always returns a generic `200 OK` response with the same message
regardless of whether the email exists
- Uses safe navigation operator (`&.`) to send reset instructions only
if the user exists

### `config/locales/en.yml`
- Consolidated `reset_password_success` and `reset_password_failure`
into a single generic `reset_password` key
- New message does not reveal whether the email exists in the system

## Security Impact
- **Before**: An attacker could determine if an email was registered by
observing the HTTP status code (200 vs 404) and response message
- **After**: All requests receive the same 200 response with a generic
message, preventing user enumeration

This follows [OWASP guidelines for authentication error
messages](https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html#authentication-responses).

Fixes #13527
2026-02-13 13:45:40 +05:30
2c2f0547f7 fix: Captain not responding to campaign conversations (#13489)
Co-authored-by: Aakash Bakhle <48802744+aakashb95@users.noreply.github.com>
2026-02-12 10:07:56 +05:30
Sojan JoseandGitHub c7193c7917 fix(slack): handle archived channel errors in SendOnSlackJob (#13520)
When a Slack-integrated channel is archived, posting from Chatwoot
raises `Slack::Web::Api::Errors::IsArchived` in `SendOnSlackJob`, which
retries and can end up in dead jobs. This can be reproduced by archiving
the connected Slack channel for a valid hook and creating outgoing
messages. This change adds `IsArchived` to the existing handled Slack
API rescue path in
`Integrations::Slack::SendOnSlackService#send_message`, so
archived-channel failures now follow the same flow as related Slack
failures (`prompt_reauthorization!` + `disable`) instead of bubbling and
retrying repeatedly. I tested this by running `bundle exec rubocop
lib/integrations/slack/send_on_slack_service.rb` (with `rbenv`
initialized), and it passes with no offenses.

Sentry issue: https://chatwoot-p3.sentry.io/issues/7150427066/
2026-02-11 17:05:44 -08:00
Sojan JoseandGitHub d272a64ff7 fix(mailbox): handle malformed sender address headers (#13486)
## How to reproduce
When an inbound email has malformed sender headers (for example `From:
McDonald <info@example.com` without a closing `>`), mailbox
processing can raise `Mail::Field::IncompleteParseError` while resolving
sender data in `MailPresenter`.

## What changed
This PR hardens sender parsing in `MailPresenter` with a small, readable
implementation:
- Added/used a safe parser (`parse_mail_address`) that rescues
`Mail::Field::ParseError` and `Mail::Field::IncompleteParseError`.
- `sender_name` now uses the same safe parser path.
- `original_sender` now resolves candidates in order via a compact
`filter_map` flow:
  - `Reply-To`
  - `X-Original-Sender`
  - `From`
- All three candidates are parsed as email addresses before use
(including `X-Original-Sender`), and invalid values are ignored.
- `notification_email_from_chatwoot?` now compares sender addresses
case-insensitively (`casecmp?`) to avoid case-only mismatches.

## Test coverage
Added focused presenter specs for:
- malformed `From` header returns nil sender values and does not
classify as notification sender
- malformed `Reply-To` falls back to valid `From`
- valid `X-Original-Sender` is used when present
- invalid `X-Original-Sender` falls back to valid `From`
- mixed-case sender address still matches configured
`MAILER_SENDER_EMAIL`

## How this was tested
Ran:
- `bundle exec rspec spec/presenters/mail_presenter_spec.rb`
- `bundle exec rubocop app/presenters/mail_presenter.rb
spec/presenters/mail_presenter_spec.rb`

Sentry issue:
[CHATWOOT-B9Y](https://chatwoot-p3.sentry.io/issues/7005483640/)
2026-02-11 11:02:38 -08:00
Sojan JoseandGitHub b2cb3717e5 fix: Replace default Rails error pages with custom designs (#13514)
## Summary

- Replace generic Rails 404, 422, and 500 error pages with
custom-designed pages
- Each page features a prominent gradient error code, clean typography,
and a "Back to home" CTA
- Full dark mode support via `prefers-color-scheme` media query  
- Mobile responsive design  
- No external dependencies (self-contained static HTML, works even when
Rails is down)
- No logo/branding to ensure compatibility with custom-branded
self-hosted instances

---

## Before (default Rails 404)

<img width="2400" height="1998" alt="old-404"
src="https://github.com/user-attachments/assets/c5f044c4-aab6-40e9-aa11-6096ed9d2b42"
/>

---

## After

### Light mode

| 404 | 422 | 500 |
|-----|-----|-----|
| <img width="600" alt="new-404-light"
src="https://github.com/user-attachments/assets/1826c812-0eb9-4219-bdd2-026c54b53123"
/> | <img width="600" alt="new-422-light"
src="https://github.com/user-attachments/assets/d72ffdbf-b61e-4f53-a16b-4ee81103124f"
/> | <img width="600" alt="new-500-light"
src="https://github.com/user-attachments/assets/81bbdb24-bfe7-43a1-b584-f37f71e3bded"
/> |

---

### Dark mode

| 404 | 422 | 500 |
|-----|-----|-----|
| <img width="600" alt="new-404-dark"
src="https://github.com/user-attachments/assets/bd323915-bfb9-48c1-885d-96ff263b4ae0"
/> | <img width="600" alt="new-422-dark"
src="https://github.com/user-attachments/assets/dcb08eca-aee5-4e36-9690-f44da13e8d88"
/> | <img width="600" alt="new-500-dark"
src="https://github.com/user-attachments/assets/538c3c2c-b9dc-406c-9932-ff8897b64790"
/> |

---

## Test plan

Easier way to verify:

- [ ] Visit `/404.html` directly to verify the 404 page  
- [ ] Visit `/422.html` directly to verify the 422 page  
- [ ] Visit `/500.html` directly to verify the 500 page  

Full verification:

- [ ] Visit a non-existent route in production mode to verify the 404
page
- [ ] Trigger a 422 error (e.g., invalid CSRF token) to verify the 422
page
- [ ] Trigger a 500 error to verify the 500 page  
- [ ] Toggle system dark mode and verify all pages render correctly  
- [ ] Test on mobile viewport widths
2026-02-11 07:57:00 -08:00
Vishnu NarayananandGitHub 00ed074d72 fix: disable email transcript for free plans (#13509)
- Block email transcript functionality for accounts without a paid plan
to prevent SES abuse.
2026-02-11 21:21:36 +05:30
7b512bd00e fix: V2 Assignment service enhancements (#13036)
## Linear Ticket:
https://linear.app/chatwoot/issue/CW-6081/review-feedback

## Description

Assignment V2 Service Enhancements

- Enable Assignment V2 on plan upgrade
- Fix UI issue with fair distribution policy display
- Add advanced assignment feature flag and enhance Assignment V2
capabilities

## Type of change

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

## How Has This Been Tested?

This has been tested using the UI.

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

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Changes auto-assignment execution paths, rate limiting defaults, and
feature-flag gating (including premium plan behavior), which could
affect which conversations get assigned and when. UI rewires inbox
settings and policy flows, so regressions are possible around
navigation/linking and feature visibility.
> 
> **Overview**
> **Adds a new premium `advanced_assignment` feature flag** and uses it
to gate capacity/balanced assignment features in the UI (sidebar entry,
settings routes, assignment-policy landing cards) and backend
(Enterprise balanced selector + capacity filtering).
`advanced_assignment` is marked premium, included in Business plan
entitlements, and auto-synced in Enterprise accounts when
`assignment_v2` is toggled.
> 
> **Improves Assignment V2 policy UX** by adding an inbox-level
“Conversation Assignment” section (behind `assignment_v2`) that can
link/unlink an assignment policy, navigate to create/edit policy flows
with `inboxId` query context, and show an inbox-link prompt after
creating a policy. The policy form now defaults to enabled, disables the
`balanced` option with a premium badge/message when unavailable, and
inbox lists support click-to-navigate.
> 
> **Tightens/adjusts auto-assignment behavior**: bulk assignment now
requires `inbox.enable_auto_assignment?`, conversation ordering uses the
attached `assignment_policy` priority, and rate limiting uses
`assignment_policy` config with an infinite default limit while still
tracking assignments. Tests and i18n strings are updated accordingly.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
23bc03bf75ee4376071e4d7fc7cd564c601d33d7. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Pranav <pranav@chatwoot.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
2026-02-11 12:24:45 +05:30
PranavandGitHub 8f95fafff4 feat: Add a setting to keep conversations pending on bot failures (#13512)
Adds an account-level setting `keep_pending_on_bot_failure` to control
whether conversations should move from pending to open when agent bot
webhooks fail.

Some users experience occasional message drops and don't want
conversations to automatically reopen due to transient bot failures.
This setting gives accounts control over that behavior. This is a
temporary setting which will be removed in future once a proper fix for
it is done, so it is not added in the UI.
2026-02-10 17:27:42 -08:00
0ad47d87f4 fix: Use Faraday for Telegram document uploads to fix large file failures (#13397)
Fixes
https://linear.app/chatwoot/issue/CW-6415/sending-large-attachments-11mb-via-telegram-channels-fails-with-http

 #### Issue
Sending large attachments (~11MB) via Telegram channels fails with HTTP
502 (Bad Gateway) and 413 (Request Entity Too Large) errors. The issue
is caused by HTTParty's built-in multipart encoding, which reads the
entire file into an in-memory string before constructing the request
body. For large files, this produces a malformed multipart request that
Telegram's API proxy rejects.

#### Solution

Replace HTTParty with Faraday + multipart-post (both already available
in the project) for the sendDocument multipart upload. The
multipart-post gem streams file content directly from disk into the HTTP
request, producing a correctly formed multipart body that Telegram
accepts for large files.

---------

Co-authored-by: Sojan Jose <sojan@pepalo.com>
2026-02-10 14:25:25 -08:00
Sivin VargheseandGitHub e65ea24360 fix: Wrong assignee displayed after switching conversations (#13501) 2026-02-10 15:23:55 +05:30
b252656984 fix: Prevent race condition in conversation dataFetched flag (#13492)
Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
2026-02-10 15:23:14 +05:30
Aakash BakhleandGitHub 6e397c7571 fix: default model for captain assistant (#13496) 2026-02-10 14:53:53 +05:30
Sojan JoseandGitHub 4622560fac chore(dev): document codex worktree local setup (#13494)
This PR standardizes local Codex worktree usage with a simple
dynamic-port workflow and ensures local-only artifacts stay out of
version control.

To reproduce: create a Codex worktree, run the setup script from
`.codex/environments/environment.toml`, and verify that it generates
per-worktree DB and port values along with a `Procfile.worktree` for
Overmind.

Changes included:
- Add `.codex/` and `Procfile.worktree` to `.gitignore`
- Document the Codex Worktree Workflow in `AGENTS.md`, outlining
expected local setup conventions

Tested locally by running the setup script with `CODEX_SKIP_INSTALL=1`
and `CODEX_SKIP_DB_PREPARE=1`. Verified successful output, dynamic
`FRONTEND_URL` / Vite port generation, and that `git diff` contains only
the intended documentation and ignore updates.
2026-02-09 20:56:40 -08:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
6632610e78 chore(deps): bump faraday from 2.13.1 to 2.14.1 (#13503)
Bumps [faraday](https://github.com/lostisland/faraday) from 2.13.1 to
2.14.1.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/lostisland/faraday/releases">faraday's
releases</a>.</em></p>
<blockquote>
<h2>v2.14.1</h2>
<h2>Security Note</h2>
<p>This release contains a security fix, we recommend all users to
upgrade as soon as possible.
A Security Advisory with more details will be posted shortly.</p>
<h2>What's Changed</h2>
<ul>
<li>Add comprehensive AI agent guidelines for Claude, Cursor, and GitHub
Copilot by <a
href="https://github.com/Copilot"><code>@​Copilot</code></a> in <a
href="https://redirect.github.com/lostisland/faraday/pull/1642">lostisland/faraday#1642</a></li>
<li>Add RFC document for Options architecture refactoring plan by <a
href="https://github.com/Copilot"><code>@​Copilot</code></a> in <a
href="https://redirect.github.com/lostisland/faraday/pull/1644">lostisland/faraday#1644</a></li>
<li>Bump actions/checkout from 5 to 6 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/lostisland/faraday/pull/1655">lostisland/faraday#1655</a></li>
<li>Explicit top-level namespace reference by <a
href="https://github.com/c960657"><code>@​c960657</code></a> in <a
href="https://redirect.github.com/lostisland/faraday/pull/1657">lostisland/faraday#1657</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/Copilot"><code>@​Copilot</code></a> made
their first contribution in <a
href="https://redirect.github.com/lostisland/faraday/pull/1642">lostisland/faraday#1642</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/lostisland/faraday/compare/v2.14.0...v2.14.1">https://github.com/lostisland/faraday/compare/v2.14.0...v2.14.1</a></p>
<h2>v2.14.0</h2>
<h2>What's Changed</h2>
<h3>New features </h3>
<ul>
<li>Use newer <code>UnprocessableContent</code> naming for 422 by <a
href="https://github.com/tylerhunt"><code>@​tylerhunt</code></a> in <a
href="https://redirect.github.com/lostisland/faraday/pull/1638">lostisland/faraday#1638</a></li>
</ul>
<h3>Fixes 🐞</h3>
<ul>
<li>Convert strings to UTF-8 by <a
href="https://github.com/c960657"><code>@​c960657</code></a> in <a
href="https://redirect.github.com/lostisland/faraday/pull/1624">lostisland/faraday#1624</a></li>
<li>Fix <code>Response#to_hash</code> when response not finished yet by
<a href="https://github.com/yykamei"><code>@​yykamei</code></a> in <a
href="https://redirect.github.com/lostisland/faraday/pull/1639">lostisland/faraday#1639</a></li>
</ul>
<h3>Misc/Docs 📄</h3>
<ul>
<li>Lint: use <code>filter_map</code> by <a
href="https://github.com/olleolleolle"><code>@​olleolleolle</code></a>
in <a
href="https://redirect.github.com/lostisland/faraday/pull/1637">lostisland/faraday#1637</a></li>
<li>Bump <code>actions/checkout</code> from v4 to v5 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/lostisland/faraday/pull/1636">lostisland/faraday#1636</a></li>
<li>Fixes documentation by <a
href="https://github.com/dharamgollapudi"><code>@​dharamgollapudi</code></a>
in <a
href="https://redirect.github.com/lostisland/faraday/pull/1635">lostisland/faraday#1635</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/c960657"><code>@​c960657</code></a> made
their first contribution in <a
href="https://redirect.github.com/lostisland/faraday/pull/1624">lostisland/faraday#1624</a></li>
<li><a
href="https://github.com/dharamgollapudi"><code>@​dharamgollapudi</code></a>
made their first contribution in <a
href="https://redirect.github.com/lostisland/faraday/pull/1635">lostisland/faraday#1635</a></li>
<li><a href="https://github.com/tylerhunt"><code>@​tylerhunt</code></a>
made their first contribution in <a
href="https://redirect.github.com/lostisland/faraday/pull/1638">lostisland/faraday#1638</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/lostisland/faraday/compare/v2.13.4...v2.14.0">https://github.com/lostisland/faraday/compare/v2.13.4...v2.14.0</a></p>
<h2>v2.13.4</h2>
<h2>What's Changed</h2>
<ul>
<li>Improve error handling logic and add missing test coverage by <a
href="https://github.com/iMacTia"><code>@​iMacTia</code></a> in <a
href="https://redirect.github.com/lostisland/faraday/pull/1633">lostisland/faraday#1633</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/lostisland/faraday/compare/v2.13.3...v2.13.4">https://github.com/lostisland/faraday/compare/v2.13.3...v2.13.4</a></p>
<h2>v2.13.3</h2>
<h2>What's Changed</h2>
<ul>
<li>Fix type assumption in <code>Faraday::Error</code> by <a
href="https://github.com/iMacTia"><code>@​iMacTia</code></a> in <a
href="https://redirect.github.com/lostisland/faraday/pull/1630">lostisland/faraday#1630</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/lostisland/faraday/commit/16cbd38ef252d25dedf416a4d2510a2f3db10c87"><code>16cbd38</code></a>
Version bump to 2.14.1</li>
<li><a
href="https://github.com/lostisland/faraday/commit/a6d3a3a0bf59c2ab307d0abd91bc126aef5561bc"><code>a6d3a3a</code></a>
Merge commit from fork</li>
<li><a
href="https://github.com/lostisland/faraday/commit/b23f710d28c0dba169470f568df4017a1e8beea7"><code>b23f710</code></a>
Explicit top-level namespace reference (<a
href="https://redirect.github.com/lostisland/faraday/issues/1657">#1657</a>)</li>
<li><a
href="https://github.com/lostisland/faraday/commit/49ba4ac3a7359baed634c12a82386f6c8c717ea8"><code>49ba4ac</code></a>
Bump actions/checkout from 5 to 6 (<a
href="https://redirect.github.com/lostisland/faraday/issues/1655">#1655</a>)</li>
<li><a
href="https://github.com/lostisland/faraday/commit/51a49bc99d7df6f724d250d64771e1d710576df7"><code>51a49bc</code></a>
Ensure Claude reads the guidelines and allow to plan in a gitignored
.ai/PLAN...</li>
<li><a
href="https://github.com/lostisland/faraday/commit/894f65cab8f04bcf35e84a2dfd9fc0286dbce340"><code>894f65c</code></a>
Add RFC document for Options architecture refactoring plan (<a
href="https://redirect.github.com/lostisland/faraday/issues/1644">#1644</a>)</li>
<li><a
href="https://github.com/lostisland/faraday/commit/397e3ded0c5166313bb22f1c0221b36b6023fd0f"><code>397e3de</code></a>
Add comprehensive AI agent guidelines for Claude, Cursor, and GitHub
Copilot ...</li>
<li><a
href="https://github.com/lostisland/faraday/commit/d98c65cfc254ea2898386e4359428527122abec3"><code>d98c65c</code></a>
Update Faraday-specific AI agent guidelines</li>
<li><a
href="https://github.com/lostisland/faraday/commit/56c18ecb718e30c5a3a0dea9bd2361912af9013c"><code>56c18ec</code></a>
Add AI agent guidelines specific to Faraday repository</li>
<li><a
href="https://github.com/lostisland/faraday/commit/3201a42957d37efc968ee8834ba9b50ed5dde54a"><code>3201a42</code></a>
Version bump to 2.14.0</li>
<li>Additional commits viewable in <a
href="https://github.com/lostisland/faraday/compare/v2.13.1...v2.14.1">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=faraday&package-manager=bundler&previous-version=2.13.1&new-version=2.14.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/chatwoot/chatwoot/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-09 16:12:52 -08:00
Aakash BakhleandGitHub bd732f1fa9 fix: search faqs in account language (#13428)
# Pull Request Template

## Description

Reply suggestions uses `search_documentation`. While this is useful,
there is a subtle bug, a user's message may be in a different language
(say spanish) than the FAQs present (english).
This results in embedding search in spanish and compared against english
vectors, which results in poor retrieval and poor suggestions.


Fixes # (issue)
This PR fixes the above behaviour by making a small llm call translate
the query before searching in the search documentation tool


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

before:
<img width="894" height="157" alt="image"
src="https://github.com/user-attachments/assets/83871ee5-511e-4432-8b99-39e803759f63"
/>

after:
<img width="1149" height="294" alt="image"
src="https://github.com/user-attachments/assets/f9617d7a-6d48-4ca1-ad1c-2181e16c1f3d"
/>


test on rails console:
<img width="2094" height="380" alt="image"
src="https://github.com/user-attachments/assets/159fdaa5-8808-49d2-be5d-304d69fa97f7"
/>


## 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-02-09 17:25:11 +05:30
Shivam MishraandGitHub 67112647e8 fix: escape special characters in Linear GraphQL queries (#13490)
Creating a Linear issue from Chatwoot fails with a GraphQL parse error
when the title, description, or search term contains double quotes. For
example, a description like `the sender is "Bot"` produces this broken
query:

```graphql
issueCreate(input: { description: "the sender is "Bot"" })
```

Linear's API rejects this with `Syntax Error: Expected ":", found
String`. This affects issue creation, issue linking, and issue search —
any flow where user-provided text is interpolated into a GraphQL query.

The `graphql_value` helper was only escaping newlines (`\n`) but not
quotes, backslashes, or other characters that are meaningful inside a
GraphQL string literal. On top of that, `issue_link` and `search_issue`
bypassed `graphql_value` entirely, using raw string interpolation
instead.

The fix replaces the manual `gsub` escaping with Ruby's `to_json`, which
produces a properly escaped, double-quoted string that handles all
special characters. This is a minimal, well-understood substitution —
`to_json` on a Ruby string returns a valid JSON string literal, which is
also a valid GraphQL string literal since GraphQL uses the same escaping
rules. The `issue_link` mutation and `search_issue` query are updated to
route their parameters through `graphql_value` instead of raw
interpolation.

The `team_entities_query` and `linked_issues` methods in `queries.rb`
also use raw interpolation, but their inputs are system-generated IDs
and URLs rather than user-provided text, so they're left as-is to keep
this change focused.
2026-02-09 16:18:04 +05:30
Tanmay Deep SharmaandGitHub 04c456e0a3 fix: handle 404 errors gracefully in avatar download job (#13491)
## Description

Fixes `Avatar::AvatarFromUrlJob` logging 404 errors as ERROR when
avatars don't exist

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

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Small logging-only behavior change that doesn’t affect attachment flow
or persisted data beyond existing sync-attribute updates.
> 
> **Overview**
> Updates `Avatar::AvatarFromUrlJob` error handling to treat
`Down::NotFound` (404/missing avatar URL) as a non-error: it now logs an
INFO message instead of logging as ERROR.
> 
> Other `Down::Error` failures continue to be logged as ERROR, and the
job still runs `update_avatar_sync_attributes` in `ensure`.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
675f41041ae3dd4ead6e0dee5f1586dcad9750cd. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
2026-02-09 13:27:23 +05:30
Sojan JoseandGitHub 656ae41b24 fix(imap): handle IMAP parser/read errors without exception tracking (#13473)
When an IMAP server returns malformed or partial protocol responses,
Inboxes::FetchImapEmailsJob can raise Net::IMAP::ResponseParseError,
Net::IMAP::ResponseReadError, or Net::IMAP::ResponseTooLargeError.
Previously these errors fell through to the generic StandardError
handler and were captured repeatedly in Sentry.

This PR updates app/jobs/inboxes/fetch_imap_emails_job.rb to treat those
parser/read exceptions as handled IMAP failures by adding them to the
existing IMAP/network rescue block, so they are logged and retried on
the next scheduled run without exception tracking noise.

fixes:
https://chatwoot-p3.sentry.io/issues/7132037793/events/104fb9b4d80a4fb6ba3861c44c6c9b83/

How to reproduce:
- Use an IMAP server/inbox that intermittently returns malformed or
truncated protocol responses during search/fetch.
- Run Inboxes::FetchImapEmailsJob for that channel and observe the
raised parser/read exception.

How this was tested:
- bundle exec ruby -c app/jobs/inboxes/fetch_imap_emails_job.rb
- bundle exec rubocop app/jobs/inboxes/fetch_imap_emails_job.rb
2026-02-07 17:30:54 -08:00
Sojan JoseandGitHub f83415f299 fix(account-deletion): normalize deleted email suffix and handle collisions safely (#13472)
## Summary
This PR fixes account deletion failures by changing how orphaned user
emails are rewritten during `AccountDeletionService`.

Ref:
https://chatwoot-p3.sentry.io/issues/6715254765/events/e228a5d045ad47348d6c32448bc33b7a/

## Changes (develop -> this branch)
- Updated soft-delete email rewrite from:
  - `#{original_email}-deleted.com`
- To deterministic value:
  - `#{user.id}@chatwoot-deleted.invalid`
- Added reserved non-deliverable domain constant:
  - `@chatwoot-deleted.invalid`
- Replaced the "other accounts" check from `count.zero?` to `exists?`
(same behavior, cheaper query).
- Updated service spec expectation to match deterministic email value
and assert it differs from original email.

## Files changed
- `app/services/account_deletion_service.rb`
- `spec/services/account_deletion_service_spec.rb`

## How to verify
- Run: `bundle exec rspec
spec/services/account_deletion_service_spec.rb`
- Run: `bundle exec rubocop app/services/account_deletion_service.rb
spec/services/account_deletion_service_spec.rb`
2026-02-07 17:29:27 -08:00
Vishnu NarayananandGitHub 0a910c3763 fix: Add email rate limiting to automation rule actions (#13474) 2026-02-07 10:02:40 -08:00
PranavandGitHub 6a7cbcf5ba fix: Fixes reply-to in WhatsApp Cloud API (#13467)
This change https://github.com/chatwoot/chatwoot/pull/13371 broke the
functionality. When a user replies to a WhatsApp message, the reply
context wasn't being properly stored in Chatwoot due to #13371

WhatsApp sends reply messages with a `context` field containing the
original message ID:
```json
{
    "messages": [{
      "context": {
        "from": "phone_number",
        "id": "wamid.ORIGINAL_MESSAGE_ID"
      },
      "from": "phone_number",
      "id": "wamid.REPLY_MESSAGE_ID",
      "text": { "body": "This is a reply" }
    }]
  }
```
However, the in_reply_to_external_id was being overridden when building
the message because content_attributes was explicitly set to either {
external_echo: true } or {}, which discarded the reply-to information.
2026-02-06 14:01:01 -08:00
0e30e3c00a fix: add loading and silent retry to summary reports (#13455)
For large accounts, summary report queries can take several seconds to
complete, often times hitting the 15-second production request timeout.
The existing implementation silently swallows these failures and
provides no feedback during loading. Users see stale data with no
indication that a fetch is in progress, and if they interact with
filters while a request is in flight, they trigger race conditions that
can result in mismatched data being displayed.

This is a UX-level fix for what is fundamentally a performance problem.
While the underlying query performance is addressed separately, users
need proper feedback either way

## Approach

The PR adds three things: 

1. A loading overlay on the table, to provide feedback on loading state
2. Disabled filter inputs during loading so that the user does not
request new information that can cause race conditions in updating the
store
3. Silent retry before showing an error.

The retry exists because these queries often succeed on the second
attempt—likely due to database query caching. Rather than immediately
showing an error and forcing the user to manually retry, we do it
automatically. If the second attempt also fails, we show a toast so the
user knows something went wrong.

The store previously caught and discarded errors entirely. It now
rethrows them after resetting the loading flag, allowing components to
handle failures as they see fit.

### Previews

#### Double Retry and Error


https://github.com/user-attachments/assets/c189b173-8017-44b7-9493-417d65582c95

#### Loading State


https://github.com/user-attachments/assets/9f899c20-fbad-469b-93cc-f0d05d0853b0

---------

Co-authored-by: iamsivin <iamsivin@gmail.com>
2026-02-06 19:53:46 +05:30
Sivin VargheseandGitHub 0d3b59fd9c feat: Refactor reports filters (#13443) 2026-02-06 18:22:30 +05:30
Shivam MishraandGitHub 04e747cc02 chore: temporarily disable ProcessStaleContactsJob (#13462)
We've been seeing orphan conversations (conversations whose contact has
been deleted) on high-frequency accounts. These orphans cause 500 errors
when the API attempts to render conversation data, since the jbuilder
partials expect a valid contact association.

The `ProcessStaleContactsJob` triggers `RemoveStaleContactsService`,
which uses `delete_all` to remove contacts. While the query only selects
contacts without conversations, `delete_all` bypasses ActiveRecord
callbacks and does not re-verify at deletion time. We suspect this
creates a window where a new conversation can be created for a contact
between query evaluation and the actual delete, leaving the conversation
orphaned.

**This is unverified — disabling the job is the experiment to confirm or
rule out this theory.**

This PR disables the job for a monitoring period. If orphan
conversations stop appearing, we'll have confirmation and can work on a
proper fix for the service. If they continue, we'll investigate other
deletion paths.

Stale contacts will accumulate while the job is disabled, but this is a
controlled tradeoff — they are inert records with no user-facing impact,
and can be cleaned up in bulk once we re-enable or fix the job.
2026-02-06 13:27:51 +05:30
053b7774dd fix: Render all account limit fields (#13435)
## Bug Explanation
- The Super Admin limits form renders inputs by iterating the keys of
`account.limits`.
- When `account.limits` was present, `AccountLimitsField#to_s` returned
only that hash (no defaults).
- On save, `SuperAdmin::AccountsController` compacts the limits hash,
removing blank keys.
- Result: if only one key (e.g., `agents`) was saved, the other keys
were missing from the hash and their fields disappeared on the next
render.

## Fix
- Always start from a defaults hash of all expected limit keys and merge
in any saved overrides.
- This keeps the UI stable and ensures all limit inputs remain visible
even when the stored hash is partial.
- Upgraded meta_request to `0.8.5` to stop a dev‑only `SystemStackError`
caused by JSON‑encoding ActiveRecord::Transaction in Rails 7.2. No
production behavior changes.

## Reproduction Steps
1. In Super Admin, edit an account and set only `agents` in the limits;
leave other limit fields blank and save.
2. Re-open the same account in Super Admin.
3. Observe that only `agents` is rendered and other limit fields are
missing.

## Testing
- Tested on UI

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2026-02-04 20:21:07 +05:30
Muhsin KelothandGitHub 8eaea7c72e feat: Add standalone outgoing messages count API endpoint (#13419)
This PR adds a new standalone `GET
/api/v2/accounts/:id/reports/outgoing_messages_count` endpoint that
returns outgoing message counts grouped by agent, team, inbox, or label.
2026-02-04 19:36:50 +05:30
Sojan Jose 1345f67966 Merge branch 'release/4.10.1'
Publish Chatwoot EE docker images / build (linux/amd64, ubuntu-latest) (push) Waiting to run
Publish Chatwoot EE docker images / build (linux/arm64, ubuntu-22.04-arm) (push) Waiting to run
Publish Chatwoot EE docker images / merge (push) Blocked by required conditions
Publish Chatwoot CE docker images / build (linux/amd64, ubuntu-latest) (push) Waiting to run
Publish Chatwoot CE docker images / build (linux/arm64, ubuntu-22.04-arm) (push) Waiting to run
Publish Chatwoot CE docker images / merge (push) Blocked by required conditions
2026-01-20 08:44:14 -08:00
Sojan Jose 0f914fa2ab Merge branch 'release/4.10.0'
Publish Chatwoot EE docker images / build (linux/amd64, ubuntu-latest) (push) Waiting to run
Publish Chatwoot EE docker images / build (linux/arm64, ubuntu-22.04-arm) (push) Waiting to run
Publish Chatwoot EE docker images / merge (push) Blocked by required conditions
Publish Chatwoot CE docker images / build (linux/amd64, ubuntu-latest) (push) Waiting to run
Publish Chatwoot CE docker images / build (linux/arm64, ubuntu-22.04-arm) (push) Waiting to run
Publish Chatwoot CE docker images / merge (push) Blocked by required conditions
2026-01-15 22:20:22 -08:00
Sojan Jose 59663dd558 Merge branch 'hotfix/4.9.2'
Publish Chatwoot EE docker images / build (linux/amd64, ubuntu-latest) (push) Waiting to run
Publish Chatwoot EE docker images / build (linux/arm64, ubuntu-22.04-arm) (push) Waiting to run
Publish Chatwoot EE docker images / merge (push) Blocked by required conditions
Publish Chatwoot CE docker images / merge (push) Blocked by required conditions
Publish Chatwoot CE docker images / build (linux/amd64, ubuntu-latest) (push) Waiting to run
Publish Chatwoot CE docker images / build (linux/arm64, ubuntu-22.04-arm) (push) Waiting to run
2026-01-12 09:15:17 -08:00
3879 changed files with 282553 additions and 78341 deletions
+65
View File
@@ -0,0 +1,65 @@
---
:position: before
:position_in_additional_file_patterns: before
:position_in_class: before
:position_in_factory: before
:position_in_fixture: before
:position_in_routes: before
:position_in_serializer: before
:position_in_test: before
:classified_sort: true
:exclude_controllers: true
:exclude_factories: true
:exclude_fixtures: true
:exclude_helpers: true
:exclude_scaffolds: true
:exclude_serializers: true
:exclude_sti_subclasses: false
:exclude_tests: true
:force: false
:format_markdown: false
:format_rdoc: false
:format_yard: false
:frozen: false
:grouped_polymorphic: false
:ignore_model_sub_dir: false
:ignore_unknown_models: false
:include_version: false
:show_check_constraints: false
:show_complete_foreign_keys: false
:show_foreign_keys: true
:show_indexes: true
:show_indexes_include: false
:simple_indexes: false
:sort: false
:timestamp: false
:trace: false
:with_comment: true
:with_column_comments: true
:with_table_comments: true
:position_of_column_comment: :with_name
:active_admin: false
:command:
:debug: false
:hide_default_column_types: json,jsonb,hstore
:hide_limit_column_types: integer,bigint,boolean
:timestamp_columns:
- created_at
- updated_at
:ignore_columns:
:ignore_routes:
:models: true
:routes: false
:skip_on_db_migrate: false
:target_action: :do_annotations
:wrapper:
:wrapper_close:
:wrapper_open:
:classes_default_to_s: []
:additional_file_patterns: []
:model_dir:
- app/models
- enterprise/app/models
:require: []
:root_dir:
- ''
+20
View File
@@ -1,3 +1,23 @@
---
ignore:
- CVE-2021-41098 # https://github.com/chatwoot/chatwoot/issues/3097 (update once azure blob storage is updated)
- GHSA-57hq-95w6-v4fc # Devise confirmable race condition — patched locally in User model (remove once on Devise 5+)
# Devise 5 is currently blocked by devise-secure_password/devise_token_auth/devise-two-factor.
# Chatwoot does not enable Timeoutable, so the timeout redirect path is not reachable.
- GHSA-jp94-3292-c3xv
# Rails 7.1 has no patched release for the Active Storage proxy range
# advisories. Chatwoot limits proxy range requests locally.
- CVE-2026-33658
# Rails 7.1 has no patched release for this Active Storage direct-upload
# advisory. Chatwoot filters internal metadata keys locally.
- CVE-2026-33173
- CVE-2026-33174
# Rails 7.1 has no patched release for these Rails advisories. These are not
# reachable through Chatwoot's current usage patterns and should be removed
# once we upgrade to Rails 7.2.3.1+.
- CVE-2026-33168
- CVE-2026-33169
- CVE-2026-33170
- CVE-2026-33176
- CVE-2026-33195
- CVE-2026-33202
+9 -9
View File
@@ -93,8 +93,8 @@ jobs:
exit 1
fi
mkdir -p ~/tmp
curl -L https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/6.3.0/openapi-generator-cli-6.3.0.jar > ~/tmp/openapi-generator-cli-6.3.0.jar
java -jar ~/tmp/openapi-generator-cli-6.3.0.jar validate -i swagger/swagger.json
curl -L https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/7.19.0/openapi-generator-cli-7.19.0.jar > ~/tmp/openapi-generator-cli-7.19.0.jar
java -jar ~/tmp/openapi-generator-cli-7.19.0.jar validate -i swagger/swagger.json
# Bundle audit
- run:
@@ -144,7 +144,7 @@ jobs:
# Backend tests with parallelization
backend-tests:
<<: *defaults
parallelism: 20
parallelism: 18
steps:
- checkout
- node/install:
@@ -350,12 +350,12 @@ jobs:
destination: coverage
build:
<<: *defaults
steps:
- run:
name: Legacy build aggregator
command: |
echo "All main jobs passed; build job kept only for GitHub required check compatibility."
<<: *defaults
steps:
- run:
name: Legacy build aggregator
command: |
echo "All main jobs passed; build job kept only for GitHub required check compatibility."
workflows:
version: 2
+6
View File
@@ -98,6 +98,8 @@ SMTP_OPENSSL_VERIFY_MODE=peer
# Mail Incoming
# This is the domain set for the reply emails when conversation continuity is enabled
MAILER_INBOUND_EMAIL_DOMAIN=
# Maximum time in seconds to process a single IMAP email
# EMAIL_PROCESSING_TIMEOUT_SECONDS=60
# Set this to the appropriate ingress channel with regards to incoming emails
# Possible values are :
# relay for Exim, Postfix, Qmail
@@ -232,6 +234,10 @@ ANDROID_SHA256_CERT_FINGERPRINT=AC:73:8E:DE:EB:56:EA:CC:10:87:02:A7:65:37:7B:38:
# Comma-separated list of trusted IPs that bypass Rack Attack throttling rules
# RACK_ATTACK_ALLOWED_IPS=127.0.0.1,::1,192.168.0.10
## SafeFetch private network access
## Keep disabled by default. Self-hosted installations can enable this to allow SafeFetch requests to private network URLs.
# SAFE_FETCH_ALLOW_PRIVATE_NETWORK=false
## Running chatwoot as an API only server
## setting this value to true will disable the frontend dashboard endpoints
# CW_API_ONLY_SERVER=false
+195
View File
@@ -0,0 +1,195 @@
#!/usr/bin/env python3
"""Sync triage GitHub security advisories to Linear issues."""
from __future__ import annotations
import os
import sys
from typing import Any
import requests
GITHUB_API = "https://api.github.com"
LINEAR_API = "https://api.linear.app/graphql"
SEVERITY_PRIORITY = {"critical": 1, "high": 2, "medium": 3, "low": 4}
SEVERITY_COLOR = {
"critical": 15548997,
"high": 15105570,
"medium": 15844367,
"low": 3066993,
}
DEFAULT_COLOR = 9807270
def required_env(name: str) -> str:
value = os.environ.get(name)
if not value:
sys.exit(f"Missing required env var: {name}")
return value
def fetch_triage_advisories(repo: str, token: str) -> list[dict[str, Any]]:
url: str | None = f"{GITHUB_API}/repos/{repo}/security-advisories"
params: dict[str, Any] | None = {"state": "triage", "per_page": 100}
headers = {
"Accept": "application/vnd.github+json",
"Authorization": f"Bearer {token}",
"X-GitHub-Api-Version": "2022-11-28",
}
advisories: list[dict[str, Any]] = []
while url:
r = requests.get(url, headers=headers, params=params, timeout=30)
r.raise_for_status()
advisories.extend(r.json())
next_link = r.links.get("next")
url = next_link["url"] if next_link else None
params = None
return advisories
def linear_call(query: str, variables: dict[str, Any], api_key: str) -> dict[str, Any]:
r = requests.post(
LINEAR_API,
headers={"Authorization": api_key},
json={"query": query, "variables": variables},
timeout=30,
)
r.raise_for_status()
return r.json()
def linear_issue_exists(ghsa_id: str, api_key: str) -> bool:
query = (
"query($q: String!) { issues(filter: {title: {contains: $q}}, first: 1) "
"{ nodes { id } } }"
)
resp = linear_call(query, {"q": ghsa_id}, api_key)
return len(resp.get("data", {}).get("issues", {}).get("nodes", [])) > 0
def linear_create_issue(input_data: dict[str, Any], api_key: str) -> dict[str, str] | None:
query = (
"mutation($input: IssueCreateInput!) { issueCreate(input: $input) "
"{ success issue { identifier url } } }"
)
resp = linear_call(query, {"input": input_data}, api_key)
create = resp.get("data", {}).get("issueCreate") or {}
if not create.get("success"):
return None
return create.get("issue")
def reporter_login(advisory: dict[str, Any]) -> str:
for credit in advisory.get("credits") or []:
user = (credit or {}).get("user") or {}
if user.get("login"):
return user["login"]
return "unknown"
def cvss_score(advisory: dict[str, Any]) -> str:
score = (advisory.get("cvss") or {}).get("score")
return str(score) if score is not None else "n/a"
def build_description(adv: dict[str, Any]) -> str:
return (
f"**GHSA:** {adv['ghsa_id']}\n"
f"**CVE:** {adv.get('cve_id') or 'n/a'}\n"
f"**Severity:** {adv.get('severity') or 'unknown'} (CVSS {cvss_score(adv)})\n"
f"**Reporter:** {reporter_login(adv)}\n"
f"**Reported:** {(adv.get('created_at') or '').split('T')[0]}\n"
f"**Advisory:** {adv['html_url']}\n\n"
f"---\n\n"
f"{adv.get('description') or 'No description provided.'}"
)
def post_discord(adv: dict[str, Any], issue: dict[str, str], webhook_url: str) -> None:
severity = adv.get("severity") or "unknown"
title = f"[{adv['ghsa_id']}] {adv['summary']}"[:250]
payload = {
"username": "GHSA Sync",
"embeds": [
{
"title": title,
"url": issue["url"],
"color": SEVERITY_COLOR.get(severity, DEFAULT_COLOR),
"fields": [
{"name": "Linear", "value": issue["identifier"], "inline": True},
{
"name": "Severity",
"value": f"{severity} (CVSS {cvss_score(adv)})",
"inline": True,
},
{
"name": "Advisory",
"value": f"[GitHub]({adv['html_url']})",
"inline": True,
},
],
}
],
}
try:
requests.post(webhook_url, json=payload, timeout=10)
except requests.RequestException:
pass
def main() -> int:
repo = required_env("GITHUB_REPOSITORY")
gh_token = required_env("GHSA_READ_TOKEN")
linear_api_key = required_env("LINEAR_API_KEY")
team_id = required_env("LINEAR_TEAM_ID")
project_id = required_env("LINEAR_PROJECT_ID")
label_id = required_env("LINEAR_LABEL_ID")
discord_webhook = os.environ.get("DISCORD_WEBHOOK_URL") or None
advisories = fetch_triage_advisories(repo, gh_token)
print(f"Fetched {len(advisories)} triage advisories")
created = skipped = failed = 0
for adv in advisories:
ghsa_id = adv.get("ghsa_id")
if not ghsa_id:
failed += 1
continue
try:
if linear_issue_exists(ghsa_id, linear_api_key):
skipped += 1
continue
severity = adv.get("severity") or "unknown"
issue = linear_create_issue(
{
"title": f"[{ghsa_id}] {adv.get('summary', '')}",
"description": build_description(adv),
"teamId": team_id,
"projectId": project_id,
"labelIds": [label_id],
"priority": SEVERITY_PRIORITY.get(severity, 3),
},
linear_api_key,
)
except requests.RequestException:
failed += 1
continue
if not issue:
failed += 1
continue
created += 1
if discord_webhook:
post_discord(adv, issue, discord_webhook)
print(f"Created {created}, skipped {skipped}, failed {failed}")
return 1 if failed > 0 else 0
if __name__ == "__main__":
sys.exit(main())
+3
View File
@@ -11,6 +11,9 @@ concurrency:
group: pr-${{ github.workflow }}-${{ github.head_ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
deployment_check:
name: Check Deployment
+3
View File
@@ -8,6 +8,9 @@ on:
branches:
- develop
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-22.04
+29
View File
@@ -0,0 +1,29 @@
name: Sync GHSA advisories to Linear
on:
schedule:
- cron: '0 4 * * *' # daily at 09:30 IST
workflow_dispatch: {}
permissions:
contents: read
jobs:
sync:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install dependencies
run: pip install requests==2.32.3
- name: Sync advisories
env:
GHSA_READ_TOKEN: ${{ secrets.GHSA_READ_TOKEN }}
LINEAR_API_KEY: ${{ secrets.LINEAR_API_KEY }}
LINEAR_TEAM_ID: ${{ secrets.LINEAR_TEAM_ID }}
LINEAR_PROJECT_ID: ${{ secrets.LINEAR_PROJECT_ID }}
LINEAR_LABEL_ID: ${{ secrets.LINEAR_LABEL_ID }}
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
run: python3 .github/scripts/ghsa_linear_sync.py
@@ -10,6 +10,9 @@ concurrency:
group: pr-${{ github.workflow }}-${{ github.head_ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
log_lines_check:
runs-on: ubuntu-latest
+3
View File
@@ -14,6 +14,9 @@ on:
- cron: "0 0 * * *"
workflow_dispatch:
permissions:
contents: read
jobs:
nightly:
runs-on: ubuntu-24.04
@@ -3,6 +3,10 @@ name: Publish Codespace Base Image
on:
workflow_dispatch:
permissions:
contents: read
packages: write
jobs:
publish-code-space-image:
runs-on: ubuntu-latest
+3
View File
@@ -18,6 +18,9 @@ on:
env:
DOCKER_REPO: chatwoot/chatwoot
permissions:
contents: read
jobs:
build:
strategy:
@@ -18,6 +18,9 @@ on:
env:
DOCKER_REPO: chatwoot/chatwoot
permissions:
contents: read
jobs:
build:
strategy:
+3
View File
@@ -10,6 +10,9 @@ concurrency:
group: pr-${{ github.workflow }}-${{ github.head_ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-22.04
+3
View File
@@ -7,6 +7,9 @@ on:
- master
workflow_dispatch:
permissions:
contents: read
jobs:
test-build:
strategy:
+3
View File
@@ -94,6 +94,8 @@ yarn-debug.log*
.vscode
.claude/settings.local.json
.cursor
.codex/
.claude/
CLAUDE.local.md
# Histoire deployment
@@ -101,3 +103,4 @@ CLAUDE.local.md
.histoire
.pnpm-store/*
local/
Procfile.worktree
+33 -3
View File
@@ -4,6 +4,11 @@
- **Setup**: `bundle install && pnpm install`
- **Run Dev**: `pnpm dev` or `overmind start -f ./Procfile.dev`
- **Seed Local Test Data**: `bundle exec rails db:seed` (quickly populates minimal data for standard feature verification)
- **Seed Search Test Data**: `bundle exec rails search:setup_test_data` (bulk fixture generation for search/performance/manual load scenarios)
- **Seed Account Sample Data (richer test data)**: `Seeders::AccountSeeder` is available as an internal utility and is exposed through Super Admin `Accounts#seed`, but can be used directly in dev workflows too:
- UI path: Super Admin → Accounts → Seed (enqueues `Internal::SeedAccountJob`).
- CLI path: `bundle exec rails runner "Internal::SeedAccountJob.perform_now(Account.find(<id>))"` (or call `Seeders::AccountSeeder.new(account: Account.find(<id>)).perform!` directly).
- **Lint JS/Vue**: `pnpm eslint` / `pnpm eslint:fix`
- **Lint Ruby**: `bundle exec rubocop -a`
- **Test JS**: `pnpm test` or `pnpm test:watch`
@@ -38,24 +43,45 @@
## General Guidelines
- MVP focus: Least code change, happy-path only
- No unnecessary defensive programming
- Ship the happy path first: limit guards/fallbacks to what production has proven necessary, then iterate
- Prefer the smallest production-ready change that solves the current problem.
- Build for the expected production path first. Do not add speculative guards, fallbacks, retries, or edge-case handling unless the caller can actually hit that case or production has proven it necessary.
- When an impossible or misconfigured state would indicate a setup/deployment bug, let it fail loudly instead of silently skipping behavior.
- For locked/internal configs that must exist in production, prefer direct reads (`find`, `find_by!`, required hash keys) over silent fallbacks.
- Do not add validation or response checks unless the code uses the result or the check changes behavior meaningfully.
- Prefer existing repo dependencies/client libraries over hand-rolled protocol code for auth, signing, parsing, or API plumbing.
- Avoid one-use private helpers unless they hide real complexity or make the main flow meaningfully easier to read.
- Prefer minimal, readable code over elaborate abstractions; clarity beats cleverness
- Break down complex tasks into small, testable units
- Iterate after confirmation
- Avoid writing specs unless explicitly asked
- In specs, avoid custom helper methods for setup/data. Prefer `let` values and direct per-example setup; only add a helper when it removes meaningful repeated complexity.
- Remove dead/unreachable/unused code
- Dont write multiple versions or backups for the same logic — pick the best approach and implement it
- Prefer `with_modified_env` (from spec helpers) over stubbing `ENV` directly in specs
- Specs in parallel/reloading environments: prefer comparing `error.class.name` over constant class equality when asserting raised errors
## Codex Worktree Workflow
- Use a separate git worktree + branch per task to keep changes isolated.
- Keep Codex-specific local setup under `.codex/` and use `Procfile.worktree` for worktree process orchestration.
- The setup workflow in `.codex/environments/environment.toml` should dynamically generate per-worktree DB/port values (Rails, Vite, Redis DB index) to avoid collisions.
- Start each worktree with its own Overmind socket/title so multiple instances can run at the same time.
## Commit Messages
- Prefer Conventional Commits: `type(scope): subject` (scope optional)
- Example: `feat(auth): add user authentication`
- Don't reference Claude in commit messages
## PR Description Format
- Start with a short, user-facing paragraph describing the product change.
- Add a `Closes` section with relevant issue links (GitHub, Linear, etc.).
- For feature PRs, add `How to test` from a product/UX standpoint.
- For bugfix PRs, use `How to reproduce` when helpful.
- Optionally add a `What changed` section for implementation highlights.
- Do not add a `How this was tested` section listing specs/commands.
## Project-Specific
- **Translations**:
@@ -86,3 +112,7 @@ Practical checklist for any change impacting core logic or public APIs
- When renaming/moving shared code, mirror the change in `enterprise/` to prevent drift.
- Tests: Add Enterprise-specific specs under `spec/enterprise`, mirroring OSS spec layout where applicable.
- When modifying existing OSS features for Enterprise-only behavior, add an Enterprise module (via `prepend_mod_with`/`include_mod_with`) instead of editing OSS files directly—especially for policies, controllers, and services. For Enterprise-exclusive features, place code directly under `enterprise/`.
## Branding / White-labeling note
- For user-facing strings that currently contain "Chatwoot" but should adapt to branded/self-hosted installs, prefer applying `replaceInstallationName` from `shared/composables/useBranding` in the UI layer (for example tooltip and suggestion labels) instead of adding hardcoded brand-specific copy.
+15 -6
View File
@@ -22,6 +22,7 @@ gem 'time_diff'
gem 'tzinfo-data'
gem 'valid_email2'
gem 'email-provider-info'
gem 'gemoji'
# compress javascript config.assets.js_compressor
gem 'uglifier'
##-- used for single column multiple binary flags in notification settings/feature flagging --##
@@ -40,6 +41,8 @@ gem 'json_refs'
gem 'rack-attack', '>= 6.7.0'
# a utility tool for streaming, flexible and safe downloading of remote files
gem 'down'
# SSRF-safe URL fetching
gem 'ssrf_filter', '~> 1.5'
# authentication type to fetch and send mail over oauth2.0
gem 'gmail_xoauth'
# Lock net-smtp to 0.3.4 to avoid issues with gmail_xoauth2
@@ -73,7 +76,7 @@ gem 'faraday_middleware-aws-sigv4'
##--- gems for server & infra configuration ---##
gem 'dotenv-rails', '>= 3.0.0'
gem 'foreman'
gem 'puma'
gem 'puma', '~> 7.2', '>= 7.2.1'
gem 'vite_rails'
# metrics on heroku
gem 'barnes'
@@ -82,10 +85,11 @@ gem 'barnes'
gem 'devise', '>= 4.9.4'
gem 'devise-secure_password', git: 'https://github.com/chatwoot/devise-secure_password', branch: 'chatwoot'
gem 'devise_token_auth', '>= 1.2.3'
gem 'rails-i18n', '~> 7.0'
# two-factor authentication
gem 'devise-two-factor', '>= 5.0.0'
# authorization
gem 'jwt'
gem 'jwt', '~> 2.10', '>= 2.10.3'
gem 'pundit'
# super admin
@@ -129,9 +133,9 @@ gem 'sentry-ruby', require: false
gem 'sentry-sidekiq', '>= 5.19.0', require: false
##-- background job processing --##
gem 'sidekiq', '>= 7.3.1'
gem 'sidekiq', '~> 7.3', '>= 7.3.1'
# We want cron jobs
gem 'sidekiq-cron', '>= 1.12.0'
gem 'sidekiq-cron', '>= 2.4.0'
# for sidekiq healthcheck
gem 'sidekiq_alive'
@@ -191,18 +195,22 @@ gem 'reverse_markdown'
gem 'iso-639'
gem 'ruby-openai'
gem 'ai-agents', '>= 0.7.0'
gem 'ai-agents', '>= 0.12.0'
# TODO: Move this gem as a dependency of ai-agents
gem 'ruby_llm', '>= 1.8.2'
gem 'ruby_llm', '>= 1.14.1'
gem 'ruby_llm-schema'
gem 'cld3', '~> 3.7'
# OpenTelemetry for LLM observability
gem 'opentelemetry-sdk'
gem 'opentelemetry-exporter-otlp'
gem 'shopify_api'
gem 'firecrawl-sdk', '~> 1.0', require: 'firecrawl'
### Gems required only in specific deployment environments ###
##############################################################
@@ -266,6 +274,7 @@ group :development, :test do
gem 'seed_dump'
gem 'shoulda-matchers'
gem 'simplecov', '>= 0.21', require: false
gem 'skooma'
gem 'spring'
gem 'spring-watcher-listen'
end
+117 -76
View File
@@ -108,8 +108,8 @@ GEM
acts-as-taggable-on (12.0.0)
activerecord (>= 7.1, < 8.1)
zeitwerk (>= 2.4, < 3.0)
addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0)
addressable (2.9.0)
public_suffix (>= 2.0.2, < 8.0)
administrate (0.20.1)
actionpack (>= 6.0, < 8.0)
actionview (>= 6.0, < 8.0)
@@ -126,8 +126,8 @@ GEM
jbuilder (~> 2)
rails (>= 4.2, < 7.2)
selectize-rails (~> 0.6)
ai-agents (0.7.0)
ruby_llm (~> 1.8.2)
ai-agents (0.12.0)
ruby_llm (~> 1.14)
annotaterb (4.20.0)
activerecord (>= 6.0.0)
activesupport (>= 6.0.0)
@@ -136,6 +136,8 @@ GEM
audited (5.4.1)
activerecord (>= 5.0, < 7.7)
activesupport (>= 5.0, < 7.7)
auth-sanitizer (0.2.1)
version_gem (~> 1.1, >= 1.1.10)
aws-actionmailbox-ses (0.1.0)
actionmailbox (>= 7.1.0)
aws-sdk-s3 (~> 1, >= 1.123.0)
@@ -166,9 +168,9 @@ GEM
multi_json (~> 1)
statsd-ruby (~> 1.1)
base64 (0.3.0)
bcrypt (3.1.20)
bcrypt (3.1.22)
benchmark (0.4.1)
bigdecimal (3.2.2)
bigdecimal (4.1.2)
bindex (0.8.1)
bootsnap (1.16.0)
msgpack (~> 1.2)
@@ -184,17 +186,22 @@ GEM
bundler (>= 1.2.0, < 3)
thor (~> 1.0)
byebug (11.1.3)
cgi (0.5.1)
childprocess (5.1.0)
logger (~> 1.5)
cld3 (3.7.0)
climate_control (1.2.0)
coderay (1.1.3)
commonmarker (0.23.10)
concurrent-ruby (1.3.5)
connection_pool (2.5.3)
concurrent-ruby (1.3.7)
connection_pool (2.5.5)
crack (1.0.0)
bigdecimal
rexml
crass (1.0.6)
crass (1.0.7)
cronex (0.15.0)
tzinfo
unicode (>= 0.4.4.5)
csv (3.3.0)
csv-safe (3.3.1)
csv (~> 3.0)
@@ -211,7 +218,7 @@ GEM
logger
msgpack
datadog-ruby_core_source (3.4.1)
date (3.4.1)
date (3.5.1)
debug (1.8.0)
irb (>= 1.5.0)
reline (>= 0.3.1)
@@ -267,8 +274,8 @@ GEM
dry-logic (~> 1.5)
dry-types (~> 1.8)
zeitwerk (~> 2.6)
dry-types (1.8.3)
bigdecimal (~> 3.0)
dry-types (1.9.1)
bigdecimal (>= 3.0)
concurrent-ruby (~> 1.0)
dry-core (~> 1.0)
dry-inflector (~> 1.0)
@@ -297,7 +304,7 @@ GEM
railties (>= 5.0.0)
faker (3.2.0)
i18n (>= 1.8.11, < 2)
faraday (2.13.1)
faraday (2.14.3)
faraday-net_http (>= 2.0, < 3.5)
json
logger
@@ -306,14 +313,14 @@ GEM
faraday-mashify (1.0.0)
faraday (~> 2.0)
hashie
faraday-multipart (1.0.4)
multipart-post (~> 2)
faraday-net_http (3.4.0)
net-http (>= 0.5.0)
faraday-multipart (1.2.0)
multipart-post (~> 2.0)
faraday-net_http (3.4.4)
net-http (~> 0.5)
faraday-net_http_persistent (2.1.0)
faraday (~> 2.5)
net-http-persistent (~> 4.0)
faraday-retry (2.2.1)
faraday-retry (2.4.0)
faraday (~> 2.0)
faraday_middleware-aws-sigv4 (1.0.1)
aws-sigv4 (~> 1.0)
@@ -335,6 +342,7 @@ GEM
ffi-compiler (1.0.1)
ffi (>= 1.0.0)
rake
firecrawl-sdk (1.4.1)
flag_shih_tzu (0.3.23)
foreman (0.87.2)
fugit (1.11.1)
@@ -348,6 +356,7 @@ GEM
googleapis-common-protos-types (>= 1.3.1, < 2.a)
googleauth (~> 1.0)
grpc (~> 1.36)
gemoji (4.1.0)
geocoder (1.8.1)
gli (2.22.2)
ostruct
@@ -429,7 +438,8 @@ GEM
hana (1.3.7)
hash_diff (1.1.1)
hashdiff (1.1.0)
hashie (5.0.0)
hashie (5.1.0)
logger
html2text (0.4.0)
nokogiri (>= 1.0, < 2.0)
http (5.1.1)
@@ -464,7 +474,7 @@ GEM
rails-dom-testing (>= 1, < 3)
railties (>= 4.2.0)
thor (>= 0.14, < 2.0)
json (2.13.2)
json (2.19.9)
json_refs (0.1.8)
hana
json_schemer (0.2.24)
@@ -472,6 +482,12 @@ GEM
hana (~> 1.3)
regexp_parser (~> 2.0)
uri_template (~> 0.7)
json_skooma (0.2.5)
bigdecimal
hana (~> 1.3)
regexp_parser (~> 2.0)
uri-idna (~> 0.2)
zeitwerk (~> 2.6)
judoscale-rails (1.8.2)
judoscale-ruby (= 1.8.2)
railties
@@ -479,7 +495,7 @@ GEM
judoscale-sidekiq (1.8.2)
judoscale-ruby (= 1.8.2)
sidekiq (>= 5.0)
jwt (2.10.1)
jwt (2.10.3)
base64
kaminari (1.2.2)
activesupport (>= 4.1.0)
@@ -539,11 +555,11 @@ GEM
net-imap
net-pop
net-smtp
marcel (1.0.4)
marcel (1.1.0)
maxminddb (0.1.22)
meta_request (0.8.3)
meta_request (0.8.5)
rack-contrib (>= 1.1, < 3)
railties (>= 3.0.0, < 8)
railties (>= 3.0.0, < 9)
method_source (1.1.0)
mime-types (3.4.1)
mime-types-data (~> 3.2015)
@@ -554,19 +570,19 @@ GEM
minitest (5.25.5)
mock_redis (0.36.0)
ruby2_keywords
msgpack (1.8.0)
msgpack (1.8.3)
multi_json (1.15.0)
multi_xml (0.8.0)
multi_xml (0.9.1)
bigdecimal (>= 3.1, < 5)
multipart-post (2.3.0)
multipart-post (2.4.1)
mutex_m (0.3.0)
neighbor (0.2.3)
activerecord (>= 5.2)
net-http (0.6.0)
uri
net-http (0.9.1)
uri (>= 0.11.1)
net-http-persistent (4.0.2)
connection_pool (~> 2.2)
net-imap (0.4.20)
net-imap (0.6.4.1)
date
net-protocol
net-pop (0.1.2)
@@ -581,30 +597,37 @@ GEM
sidekiq
newrelic_rpm (9.6.0)
base64
nio4r (2.7.3)
nokogiri (1.18.9)
nio4r (2.7.5)
nokogiri (1.19.4)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
nokogiri (1.18.9-arm64-darwin)
nokogiri (1.19.4-arm64-darwin)
racc (~> 1.4)
nokogiri (1.18.9-x86_64-darwin)
nokogiri (1.19.4-x86_64-darwin)
racc (~> 1.4)
nokogiri (1.18.9-x86_64-linux-gnu)
nokogiri (1.19.4-x86_64-linux-gnu)
racc (~> 1.4)
oauth (1.1.0)
oauth-tty (~> 1.0, >= 1.0.1)
snaky_hash (~> 2.0)
version_gem (~> 1.1)
oauth-tty (1.0.5)
version_gem (~> 1.1, >= 1.1.1)
oauth2 (2.0.9)
faraday (>= 0.17.3, < 3.0)
jwt (>= 1.0, < 3.0)
oauth (1.1.6)
auth-sanitizer (~> 0.2, >= 0.2.1)
base64 (~> 0.1)
cgi
oauth-tty (~> 1.0, >= 1.0.8)
snaky_hash (~> 2.0, >= 2.0.5)
version_gem (~> 1.1, >= 1.1.11)
oauth-tty (1.0.8)
auth-sanitizer (~> 0.1, >= 0.1.3)
cgi
version_gem (~> 1.1, >= 1.1.9)
oauth2 (2.0.22)
auth-sanitizer (~> 0.2, >= 0.2.1)
faraday (>= 0.17.3, < 4.0)
jwt (>= 1.0, < 4.0)
logger (~> 1.2)
multi_xml (~> 0.5)
rack (>= 1.2, < 4)
snaky_hash (~> 2.0)
version_gem (~> 1.1)
oj (3.16.10)
snaky_hash (~> 2.0, >= 2.0.5)
version_gem (~> 1.1, >= 1.1.11)
oj (3.17.3)
bigdecimal (>= 3.0)
ostruct (>= 0.2)
omniauth (2.1.4)
@@ -651,7 +674,7 @@ GEM
opentelemetry-api (~> 1.0)
orm_adapter (0.5.0)
os (1.1.4)
ostruct (0.6.1)
ostruct (0.6.3)
parallel (1.27.0)
parser (3.3.8.0)
ast (~> 2.4.1)
@@ -669,14 +692,14 @@ GEM
method_source (~> 1.0)
pry-rails (0.3.9)
pry (>= 0.10.4)
public_suffix (6.0.2)
puma (6.4.3)
public_suffix (7.0.5)
puma (7.2.1)
nio4r (~> 2.0)
pundit (2.3.0)
activesupport (>= 3.0.0)
raabro (1.4.0)
racc (1.8.1)
rack (3.2.3)
rack (3.2.6)
rack-attack (6.7.0)
rack (>= 1.0, < 4)
rack-contrib (2.5.0)
@@ -691,7 +714,7 @@ GEM
rack (>= 3.0.0, < 4)
rack-proxy (0.7.7)
rack
rack-session (2.1.1)
rack-session (2.1.2)
base64 (>= 0.1.0)
rack (>= 3.0.0)
rack-test (2.1.0)
@@ -720,6 +743,9 @@ GEM
rails-html-sanitizer (1.6.1)
loofah (~> 2.21)
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
rails-i18n (7.0.10)
i18n (>= 0.7, < 2)
railties (>= 6.0.0, < 8)
railties (7.1.5.2)
actionpack (= 7.1.5.2)
activesupport (= 7.1.5.2)
@@ -735,7 +761,7 @@ GEM
ffi (~> 1.0)
redis (5.0.6)
redis-client (>= 0.9.0)
redis-client (0.22.2)
redis-client (0.26.4)
connection_pool
redis-namespace (1.10.0)
redis (>= 4)
@@ -824,17 +850,17 @@ GEM
ruby2ruby (2.5.0)
ruby_parser (~> 3.1)
sexp_processor (~> 4.6)
ruby_llm (1.8.2)
ruby_llm (1.15.0)
base64
event_stream_parser (~> 1)
faraday (>= 1.10.0)
faraday-multipart (>= 1)
faraday-net_http (>= 1)
faraday-retry (>= 1)
marcel (~> 1.0)
ruby_llm-schema (~> 0.2.1)
marcel (~> 1)
ruby_llm-schema (~> 0)
zeitwerk (~> 2)
ruby_llm-schema (0.2.5)
ruby_llm-schema (0.3.0)
ruby_parser (3.20.0)
sexp_processor (~> 4.16)
sass (3.7.4)
@@ -891,10 +917,11 @@ GEM
logger
rack (>= 2.2.4)
redis-client (>= 0.22.2)
sidekiq-cron (1.12.0)
fugit (~> 1.8)
sidekiq-cron (2.4.0)
cronex (>= 0.13.0)
fugit (~> 1.8, >= 1.11.1)
globalid (>= 1.0.1)
sidekiq (>= 6)
sidekiq (>= 6.5.0)
sidekiq_alive (2.5.0)
gserver (~> 0.0.1)
sidekiq (>= 5, < 9)
@@ -909,6 +936,9 @@ GEM
simplecov_json_formatter (~> 0.1)
simplecov-html (0.13.2)
simplecov_json_formatter (0.1.4)
skooma (0.3.7)
json_skooma (~> 0.2.5)
zeitwerk (~> 2.6)
slack-ruby-client (2.7.0)
faraday (>= 2.0.1)
faraday-mashify
@@ -916,9 +946,9 @@ GEM
gli
hashie
logger
snaky_hash (2.0.1)
hashie
version_gem (~> 1.1, >= 1.1.1)
snaky_hash (2.0.5)
hashie (>= 0.1.0, < 6)
version_gem (>= 1.1.8, < 3)
sorbet-runtime (0.5.11934)
spring (4.1.1)
spring-watcher-listen (2.1.0)
@@ -932,6 +962,7 @@ GEM
activesupport (>= 5.2)
sprockets (>= 3.0.0)
squasher (0.7.2)
ssrf_filter (1.5.0)
stackprof (0.2.25)
statsd-ruby (1.5.0)
stripe (18.0.1)
@@ -946,7 +977,7 @@ GEM
time_diff (0.3.0)
activesupport
i18n
timeout (0.4.3)
timeout (0.6.1)
trailblazer-option (0.1.2)
twilio-ruby (7.6.0)
faraday (>= 0.9, < 3.0)
@@ -964,21 +995,25 @@ GEM
unf (0.1.4)
unf_ext
unf_ext (0.0.8.2)
unicode (0.4.4.5)
unicode-display_width (3.1.4)
unicode-emoji (~> 4.0, >= 4.0.4)
unicode-emoji (4.0.4)
uniform_notifier (1.17.0)
uri (1.0.4)
uri (1.1.1)
uri-idna (0.3.1)
uri_template (0.7.0)
valid_email2 (5.2.6)
activemodel (>= 3.2)
mail (~> 2.5)
version_gem (1.1.4)
vite_rails (3.0.17)
railties (>= 5.1, < 8)
version_gem (1.1.11)
vite_rails (3.10.0)
railties (>= 5.1, < 9)
vite_ruby (~> 3.0, >= 3.2.2)
vite_ruby (3.8.0)
vite_ruby (3.10.2)
dry-cli (>= 0.7, < 2)
logger (~> 1.6)
mutex_m
rack-proxy (~> 0.6, >= 0.6.1)
zeitwerk (~> 2.2)
warden (1.2.9)
@@ -1003,7 +1038,7 @@ GEM
working_hours (1.4.1)
activesupport (>= 3.2)
tzinfo
zeitwerk (2.6.17)
zeitwerk (2.7.5)
PLATFORMS
arm64-darwin-20
@@ -1023,7 +1058,7 @@ DEPENDENCIES
administrate (>= 0.20.1)
administrate-field-active_storage (>= 1.0.3)
administrate-field-belongs_to_search (>= 0.9.0)
ai-agents (>= 0.7.0)
ai-agents (>= 0.12.0)
annotaterb
attr_extras
audited (~> 5.4, >= 5.4.1)
@@ -1037,6 +1072,7 @@ DEPENDENCIES
bullet
bundle-audit
byebug
cld3 (~> 3.7)
climate_control
commonmarker
csv-safe
@@ -1057,8 +1093,10 @@ DEPENDENCIES
faker
faraday_middleware-aws-sigv4
fcm
firecrawl-sdk (~> 1.0)
flag_shih_tzu
foreman
gemoji
geocoder
gmail_xoauth
google-cloud-dialogflow-v2 (>= 0.24.0)
@@ -1077,7 +1115,7 @@ DEPENDENCIES
json_schemer
judoscale-rails
judoscale-sidekiq
jwt
jwt (~> 2.10, >= 2.10.3)
kaminari
koala
letter_opener
@@ -1105,13 +1143,14 @@ DEPENDENCIES
pgvector
procore-sift
pry-rails
puma
puma (~> 7.2, >= 7.2.1)
pundit
rack-attack (>= 6.7.0)
rack-cors (= 2.0.0)
rack-mini-profiler (>= 3.2.0)
rack-timeout
rails (~> 7.1)
rails-i18n (~> 7.0)
redis
redis-namespace
responders (>= 3.1.1)
@@ -1125,7 +1164,7 @@ DEPENDENCIES
rubocop-rails
rubocop-rspec
ruby-openai
ruby_llm (>= 1.8.2)
ruby_llm (>= 1.14.1)
ruby_llm-schema
scout_apm
scss_lint
@@ -1136,15 +1175,17 @@ DEPENDENCIES
sentry-sidekiq (>= 5.19.0)
shopify_api
shoulda-matchers
sidekiq (>= 7.3.1)
sidekiq-cron (>= 1.12.0)
sidekiq (~> 7.3, >= 7.3.1)
sidekiq-cron (>= 2.4.0)
sidekiq_alive
simplecov (>= 0.21)
simplecov_json_formatter
skooma
slack-ruby-client (~> 2.7.0)
spring
spring-watcher-listen
squasher
ssrf_filter (~> 1.5)
stackprof
stripe (~> 18.0)
telephone_number
+6 -2
View File
@@ -40,8 +40,12 @@ run:
fi
force_run:
rm -f ./.overmind.sock
rm -f tmp/pids/*.pid
@echo "Cleaning up Overmind processes..."
@lsof -ti:3036 2>/dev/null | xargs kill -9 2>/dev/null || true
@lsof -ti:3000 2>/dev/null | xargs kill -9 2>/dev/null || true
@rm -f ./.overmind.sock
@rm -f tmp/pids/*.pid
@echo "Cleanup complete"
overmind start -f Procfile.dev
force_run_tunnel:
+1 -1
View File
@@ -1 +1 @@
4.10.1
4.15.1
+1 -1
View File
@@ -104,7 +104,7 @@ class ContactIdentifyAction
# blank identifier or email will throw unique index error
# TODO: replace reject { |_k, v| v.blank? } with compact_blank when rails is upgraded
@contact.discard_invalid_attrs if discard_invalid_attrs
@contact.save!
@contact.save! if @contact.changed?
enqueue_avatar_job
end
+5 -1
View File
@@ -44,7 +44,11 @@ class AccountBuilder
end
def create_account
@account = Account.create!(name: account_name, locale: I18n.locale)
@account = Account.create!(
name: account_name,
locale: I18n.locale,
custom_attributes: { 'onboarding_step' => 'account_details' }
)
Current.account = @account
end
+2 -1
View File
@@ -29,8 +29,9 @@ class AgentBuilder
user = User.from_email(email)
return user if user
@name = email.split('@').first if @name.blank?
temp_password = "1!aA#{SecureRandom.alphanumeric(12)}"
User.create!(email: email, name: name, password: temp_password, password_confirmation: temp_password)
User.create!(email: email, name: @name, password: temp_password, password_confirmation: temp_password)
end
# Checks if the user needs confirmation.
@@ -50,7 +50,7 @@ class ContactInboxWithContactBuilder
def create_contact
account.contacts.create!(
name: contact_attributes[:name] || ::Haikunator.haikunate(1000),
name: contact_name,
phone_number: contact_attributes[:phone_number],
email: contact_attributes[:email],
identifier: contact_attributes[:identifier],
@@ -59,6 +59,11 @@ class ContactInboxWithContactBuilder
)
end
def contact_name
name = contact_attributes[:name] || ::Haikunator.haikunate(1000)
name.truncate(ApplicationRecord::MAX_STRING_COLUMN_LENGTH, omission: '')
end
def find_contact
contact = find_contact_by_identifier(contact_attributes[:identifier])
contact ||= find_contact_by_email(contact_attributes[:email])
+3 -5
View File
@@ -1,4 +1,6 @@
class Email::BaseBuilder
include EmailAddressParseable
pattr_initialize [:inbox!]
private
@@ -39,7 +41,7 @@ class Email::BaseBuilder
end
def business_name
inbox.business_name || inbox.sanitized_name
inbox.sanitized_business_name
end
def account_support_email
@@ -47,8 +49,4 @@ class Email::BaseBuilder
# can save it in the format "Name <email@domain.com>"
parse_email(account.support_email)
end
def parse_email(email_string)
Mail::Address.new(email_string).address
end
end
@@ -91,11 +91,21 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
def fallback_params(attachment)
{
fallback_title: attachment['title'],
external_url: attachment['url']
fallback_title: attachment['title'] || attachment.dig('payload', 'title'),
external_url: attachment['url'] || attachment.dig('payload', 'url')
}
end
# Facebook shared posts point to page URLs, not downloadable media URLs.
# Both `share` and `post` attachment types carry a page URL rather than a media file,
# so map them to `fallback` (which keeps the title/link without attempting a download).
# Keep this Facebook-only so Messenger/Instagram share attachments still use the parent media handling.
def normalize_file_type(type)
return :fallback if [:share, :post].include?(type.to_sym)
super
end
def conversation_params
{
account_id: @inbox.account_id,
@@ -105,15 +115,19 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
end
def message_params
content_attributes = {
in_reply_to_external_id: response.in_reply_to_external_id
}
content_attributes[:external_echo] = true if @outgoing_echo
{
account_id: conversation.account_id,
inbox_id: conversation.inbox_id,
message_type: @message_type,
status: @outgoing_echo ? :delivered : :sent,
content: response.content,
source_id: response.identifier,
content_attributes: {
in_reply_to_external_id: response.in_reply_to_external_id
},
content_attributes: content_attributes,
sender: @outgoing_echo ? nil : @contact_inbox.contact
}
end
+17 -7
View File
@@ -13,6 +13,7 @@ class Messages::MessageBuilder
@account = conversation.account
@message_type = params[:message_type] || 'outgoing'
@attachments = params[:attachments]
@is_voice_message = ActiveModel::Type::Boolean.new.cast(params[:is_voice_message])
@automation_rule = content_attributes&.dig(:automation_rule_id)
return unless params.instance_of?(ActionController::Parameters)
@@ -56,16 +57,25 @@ class Messages::MessageBuilder
file: uploaded_attachment
)
attachment.file_type = if uploaded_attachment.is_a?(String)
file_type_by_signed_id(
uploaded_attachment
)
else
file_type(uploaded_attachment&.content_type)
end
attachment.file_type = attachment_file_type(uploaded_attachment)
tag_voice_message(attachment)
end
end
def attachment_file_type(uploaded_attachment)
if uploaded_attachment.is_a?(String)
file_type_by_signed_id(uploaded_attachment)
else
file_type(uploaded_attachment&.content_type)
end
end
def tag_voice_message(attachment)
return unless @is_voice_message && attachment.file_type == 'audio'
attachment.meta = (attachment.meta || {}).merge('is_voice_message' => true)
end
def process_emails
return unless @conversation.inbox&.inbox_type == 'Email'
@@ -2,16 +2,30 @@ class Messages::Messenger::MessageBuilder
include ::FileTypeHelper
def process_attachment(attachment)
# This check handles very rare case if there are multiple files to attach with only one usupported file
# This check handles very rare case if there are multiple files to attach with only one unsupported file
return if unsupported_file_type?(attachment['type'])
attachment_obj = @message.attachments.new(attachment_params(attachment).except(:remote_file_url))
params = attachment_params(attachment)
# During Meta's sticker webhook transition, a sticker message carries both an `image`
# and a `sticker` attachment pointing to the same URL. Skip the redundant sticker so it
# isn't attached twice, while still storing legitimate duplicate attachments of other types.
return if duplicate_sticker?(attachment, params[:external_url])
attachment_obj = @message.attachments.new(params.except(:remote_file_url))
attachment_obj.save!
attach_file(attachment_obj, attachment_params(attachment)[:remote_file_url]) if attachment_params(attachment)[:remote_file_url]
if facebook_reel?(attachment)
update_facebook_reel_content(attachment)
elsif params[:remote_file_url]
attach_file(attachment_obj, params[:remote_file_url])
end
fetch_attachment_links(attachment_obj)
update_attachment_file_type(attachment_obj)
end
def fetch_attachment_links(attachment_obj)
fetch_story_link(attachment_obj) if attachment_obj.file_type == 'story_mention'
fetch_ig_story_link(attachment_obj) if attachment_obj.file_type == 'ig_story'
fetch_ig_post_link(attachment_obj) if attachment_obj.file_type == 'ig_post'
update_attachment_file_type(attachment_obj)
end
def attach_file(attachment, file_url)
@@ -23,10 +37,14 @@ class Messages::Messenger::MessageBuilder
filename: attachment_file.original_filename,
content_type: attachment_file.content_type
)
# The Attachment row is saved before the blob is attached, so the
# after_create_commit broadcast bails on `file.attached?`. Re-fire here
# for audio so the bubble updates without waiting on transcription.
attachment.message&.reload&.send_update_event if attachment.file_type.to_sym == :audio
end
def attachment_params(attachment)
file_type = attachment['type'].to_sym
file_type = normalize_file_type(attachment['type'])
params = { file_type: file_type, account_id: @message.account_id }
if [:image, :file, :audio, :video, :share, :story_mention, :ig_reel, :ig_post, :ig_story].include? file_type
@@ -100,6 +118,35 @@ class Messages::Messenger::MessageBuilder
private
# Facebook may send attachment types that don't directly match our file_type enum.
# Map known aliases to their canonical enum values.
FACEBOOK_FILE_TYPE_MAP = { reel: :ig_reel, sticker: :image }.freeze
def normalize_file_type(type)
sym = type.to_sym
FACEBOOK_FILE_TYPE_MAP.fetch(sym, sym)
end
def duplicate_sticker?(attachment, url)
return false unless attachment['type'].to_sym == :sticker
return false if url.blank?
@message.attachments.any? { |existing| existing.external_url == url }
end
# Facebook sends reel URLs as webpage links (facebook.com/reel/...) rather than
# direct video URLs. Downloading these yields HTML, not video content.
def facebook_reel?(attachment)
attachment['type'].to_sym == :reel
end
def update_facebook_reel_content(attachment)
url = attachment.dig('payload', 'url')
return if url.blank?
@message.update!(content: url) if @message.content.blank?
end
def unsupported_file_type?(attachment_type)
[:template, :unsupported_type, :ephemeral].include? attachment_type.to_sym
end
+15
View File
@@ -27,6 +27,8 @@ class NotificationBuilder
return if notification_type == 'conversation_creation' && !user_subscribed_to_notification?
# skip notifications for blocked conversations except for user mentions
return if primary_actor.contact.blocked? && notification_type != 'conversation_mention'
# respect conversation access (inbox/team membership and custom-role permissions)
return unless user_can_access_conversation?
user.notifications.create!(
notification_type: notification_type,
@@ -36,4 +38,17 @@ class NotificationBuilder
secondary_actor: secondary_actor || current_user
)
end
def user_can_access_conversation?
conversation = primary_actor.is_a?(Conversation) ? primary_actor : primary_actor.try(:conversation)
return true if conversation.blank?
account_user = AccountUser.find_by(account_id: account.id, user_id: user.id)
return false if account_user.blank?
ConversationPolicy.new(
{ user: user, account: account, account_user: account_user },
conversation
).show?
end
end
+1
View File
@@ -1,6 +1,7 @@
class V2::ReportBuilder
include DateRangeHelper
include ReportHelper
attr_reader :account, :params
DEFAULT_GROUP_BY = 'day'.freeze
@@ -11,10 +11,6 @@ class V2::Reports::AgentSummaryBuilder < V2::Reports::BaseSummaryBuilder
attr_reader :conversations_count, :resolved_count,
:avg_resolution_time, :avg_first_response_time, :avg_reply_time
def fetch_conversations_count
account.conversations.where(created_at: range).group('assignee_id').count
end
def prepare_report
account.account_users.map do |account_user|
build_agent_stats(account_user)
+27 -32
View File
@@ -9,37 +9,13 @@ class V2::Reports::BaseSummaryBuilder
private
def load_data
@conversations_count = fetch_conversations_count
load_reporting_events_data
end
results = data_source.summary
def load_reporting_events_data
# Extract the column name for indexing (e.g., 'conversations.team_id' -> 'team_id')
index_key = group_by_key.to_s.split('.').last
results = reporting_events
.select(
"#{group_by_key} as #{index_key}",
"COUNT(CASE WHEN name = 'conversation_resolved' THEN 1 END) as resolved_count",
"AVG(CASE WHEN name = 'conversation_resolved' THEN #{average_value_key} END) as avg_resolution_time",
"AVG(CASE WHEN name = 'first_response' THEN #{average_value_key} END) as avg_first_response_time",
"AVG(CASE WHEN name = 'reply_time' THEN #{average_value_key} END) as avg_reply_time"
)
.group(group_by_key)
.index_by { |record| record.public_send(index_key) }
@resolved_count = results.transform_values(&:resolved_count)
@avg_resolution_time = results.transform_values(&:avg_resolution_time)
@avg_first_response_time = results.transform_values(&:avg_first_response_time)
@avg_reply_time = results.transform_values(&:avg_reply_time)
end
def reporting_events
@reporting_events ||= account.reporting_events.where(created_at: range)
end
def fetch_conversations_count
# Override this method
@conversations_count = results.transform_values { |data| data[:conversations_count] }
@resolved_count = results.transform_values { |data| data[:resolved_conversations_count] }
@avg_resolution_time = results.transform_values { |data| data[:avg_resolution_time] }
@avg_first_response_time = results.transform_values { |data| data[:avg_first_response_time] }
@avg_reply_time = results.transform_values { |data| data[:avg_reply_time] }
end
def group_by_key
@@ -50,7 +26,26 @@ class V2::Reports::BaseSummaryBuilder
# Override this method
end
def average_value_key
ActiveModel::Type::Boolean.new.cast(params[:business_hours]).present? ? :value_in_business_hours : :value
def data_source
@data_source ||= Reports::DataSource.for(
account: account,
metric: nil,
dimension_type: summary_dimension_type,
dimension_id: nil,
scope: nil,
range: range,
group_by: 'day',
timezone_offset: params[:timezone_offset],
business_hours: params[:business_hours]
)
end
def summary_dimension_type
{
'account_id' => 'account',
'user_id' => 'agent',
'inbox_id' => 'inbox',
'conversations.team_id' => 'team'
}.fetch(group_by_key.to_s)
end
end
+15 -4
View File
@@ -31,13 +31,24 @@ class V2::Reports::BotMetricsBuilder
end
def bot_resolutions_count
account.reporting_events.joins(:conversation).select(:conversation_id).where(account_id: account.id, name: :conversation_bot_resolved,
created_at: range).distinct.count
# Exclude conversations that also had a handoff in the same range — handoff wins
account.reporting_events.joins(:conversation).select(:conversation_id)
.where(account_id: account.id, name: :conversation_bot_resolved, created_at: range)
.where.not(conversation_id: bot_handoff_conversation_ids_subquery)
.distinct.count
end
def bot_handoffs_count
account.reporting_events.joins(:conversation).select(:conversation_id).where(account_id: account.id, name: :conversation_bot_handoff,
created_at: range).distinct.count
account.reporting_events.joins(:conversation).select(:conversation_id)
.where(account_id: account.id, name: :conversation_bot_handoff, created_at: range)
.distinct.count
end
def bot_handoff_conversation_ids_subquery
account.reporting_events
.where(name: :conversation_bot_handoff, created_at: range)
.where.not(conversation_id: nil)
.select(:conversation_id)
end
def bot_resolution_rate
@@ -3,23 +3,10 @@ class V2::Reports::Conversations::BaseReportBuilder
private
AVG_METRICS = %w[avg_first_response_time avg_resolution_time reply_time].freeze
COUNT_METRICS = %w[
conversations_count
incoming_messages_count
outgoing_messages_count
resolutions_count
bot_resolutions_count
bot_handoffs_count
].freeze
def builder_class(metric)
case metric
when *AVG_METRICS
V2::Reports::Timeseries::AverageReportBuilder
when *COUNT_METRICS
V2::Reports::Timeseries::CountReportBuilder
end
return unless Reports::ReportMetricRegistry.supported?(metric)
V2::Reports::Timeseries::ReportBuilder
end
def log_invalid_metric
@@ -11,15 +11,6 @@ class V2::Reports::InboxSummaryBuilder < V2::Reports::BaseSummaryBuilder
attr_reader :conversations_count, :resolved_count,
:avg_resolution_time, :avg_first_response_time, :avg_reply_time
def load_data
@conversations_count = fetch_conversations_count
load_reporting_events_data
end
def fetch_conversations_count
account.conversations.where(created_at: range).group(group_by_key).count
end
def prepare_report
account.inboxes.map do |inbox|
build_inbox_stats(inbox)
@@ -40,8 +31,4 @@ class V2::Reports::InboxSummaryBuilder < V2::Reports::BaseSummaryBuilder
def group_by_key
:inbox_id
end
def average_value_key
ActiveModel::Type::Boolean.new.cast(params[:business_hours]) ? :value_in_business_hours : :value
end
end
@@ -0,0 +1,79 @@
class V2::Reports::OutgoingMessagesCountBuilder
include DateRangeHelper
attr_reader :account, :params
def initialize(account, params)
@account = account
@params = params
end
def build
send("build_by_#{params[:group_by]}")
end
private
def base_messages
account.messages.outgoing.unscope(:order).where(created_at: range)
end
def build_by_agent
counts = base_messages
.where(sender_type: 'User')
.where.not(sender_id: nil)
.group(:sender_id)
.count
user_names = account.users.where(id: counts.keys).index_by(&:id)
counts.map do |user_id, count|
user = user_names[user_id]
{ id: user_id, name: user&.name, outgoing_messages_count: count }
end
end
def build_by_team
counts = base_messages
.joins('INNER JOIN conversations ON messages.conversation_id = conversations.id')
.where.not(conversations: { team_id: nil })
.group('conversations.team_id')
.count
team_names = account.teams.where(id: counts.keys).index_by(&:id)
counts.map do |team_id, count|
team = team_names[team_id]
{ id: team_id, name: team&.name, outgoing_messages_count: count }
end
end
def build_by_inbox
counts = base_messages
.group(:inbox_id)
.count
inbox_names = account.inboxes.where(id: counts.keys).index_by(&:id)
counts.map do |inbox_id, count|
inbox = inbox_names[inbox_id]
{ id: inbox_id, name: inbox&.name, outgoing_messages_count: count }
end
end
def build_by_label
counts = base_messages
.joins('INNER JOIN conversations ON messages.conversation_id = conversations.id')
.joins("INNER JOIN taggings ON taggings.taggable_id = conversations.id
AND taggings.taggable_type = 'Conversation' AND taggings.context = 'labels'")
.joins('INNER JOIN tags ON tags.id = taggings.tag_id')
.group('tags.name')
.count
label_ids = account.labels.where(title: counts.keys).index_by(&:title)
counts.map do |label_name, count|
label = label_ids[label_name]
{ id: label&.id, name: label_name, outgoing_messages_count: count }
end
end
end
@@ -6,14 +6,6 @@ class V2::Reports::TeamSummaryBuilder < V2::Reports::BaseSummaryBuilder
attr_reader :conversations_count, :resolved_count,
:avg_resolution_time, :avg_first_response_time, :avg_reply_time
def fetch_conversations_count
account.conversations.where(created_at: range).group(:team_id).count
end
def reporting_events
@reporting_events ||= account.reporting_events.where(created_at: range).joins(:conversation)
end
def prepare_report
account.teams.map do |team|
build_team_stats(team)
@@ -1,48 +0,0 @@
class V2::Reports::Timeseries::AverageReportBuilder < V2::Reports::Timeseries::BaseTimeseriesBuilder
def timeseries
grouped_average_time = reporting_events.average(average_value_key)
grouped_event_count = reporting_events.count
grouped_average_time.each_with_object([]) do |element, arr|
event_date, average_time = element
arr << {
value: average_time,
timestamp: event_date.in_time_zone(timezone).to_i,
count: grouped_event_count[event_date]
}
end
end
def aggregate_value
object_scope.average(average_value_key)
end
private
def event_name
metric_to_event_name = {
avg_first_response_time: :first_response,
avg_resolution_time: :conversation_resolved,
reply_time: :reply_time
}
metric_to_event_name[params[:metric].to_sym]
end
def object_scope
scope.reporting_events.where(name: event_name, created_at: range, account_id: account.id)
end
def reporting_events
@grouped_values = object_scope.group_by_period(
group_by,
:created_at,
default_value: 0,
range: range,
permit: %w[day week month year hour],
time_zone: timezone
)
end
def average_value_key
@average_value_key ||= params[:business_hours].present? ? :value_in_business_hours : :value
end
end
@@ -1,12 +1,13 @@
class V2::Reports::Timeseries::BaseTimeseriesBuilder
include TimezoneHelper
include DateRangeHelper
DEFAULT_GROUP_BY = 'day'.freeze
pattr_initialize :account, :params
def scope
case params[:type].to_sym
case dimension_type.to_sym
when :account
account
when :inbox
@@ -20,6 +21,20 @@ class V2::Reports::Timeseries::BaseTimeseriesBuilder
end
end
def data_source
@data_source ||= Reports::DataSource.for(
account: account,
metric: params[:metric],
dimension_type: dimension_type,
dimension_id: params[:id],
scope: scope,
range: range,
group_by: group_by,
timezone_offset: params[:timezone_offset],
business_hours: params[:business_hours]
)
end
def inbox
@inbox ||= account.inboxes.find(params[:id])
end
@@ -43,4 +58,10 @@ class V2::Reports::Timeseries::BaseTimeseriesBuilder
def timezone
@timezone ||= timezone_name_from_offset(params[:timezone_offset])
end
private
def dimension_type
(params[:type].presence || 'account').to_s
end
end
@@ -1,78 +0,0 @@
class V2::Reports::Timeseries::CountReportBuilder < V2::Reports::Timeseries::BaseTimeseriesBuilder
def timeseries
grouped_count.each_with_object([]) do |element, arr|
event_date, event_count = element
# The `event_date` is in Date format (without time), such as "Wed, 15 May 2024".
# We need a timestamp for the start of the day. However, we can't use `event_date.to_time.to_i`
# because it converts the date to 12:00 AM server timezone.
# The desired output should be 12:00 AM in the specified timezone.
arr << { value: event_count, timestamp: event_date.in_time_zone(timezone).to_i }
end
end
def aggregate_value
object_scope.count
end
private
def metric
@metric ||= params[:metric]
end
def object_scope
send("scope_for_#{metric}")
end
def scope_for_conversations_count
scope.conversations.where(account_id: account.id, created_at: range)
end
def scope_for_incoming_messages_count
scope.messages.where(account_id: account.id, created_at: range).incoming.unscope(:order)
end
def scope_for_outgoing_messages_count
scope.messages.where(account_id: account.id, created_at: range).outgoing.unscope(:order)
end
def scope_for_resolutions_count
scope.reporting_events.where(
name: :conversation_resolved,
account_id: account.id,
created_at: range
)
end
def scope_for_bot_resolutions_count
scope.reporting_events.where(
name: :conversation_bot_resolved,
account_id: account.id,
created_at: range
)
end
def scope_for_bot_handoffs_count
scope.reporting_events.joins(:conversation).select(:conversation_id).where(
name: :conversation_bot_handoff,
account_id: account.id,
created_at: range
).distinct
end
def grouped_count
# IMPORTANT: time_zone parameter affects both data grouping AND output timestamps
# It converts timestamps to the target timezone before grouping, which means
# the same event can fall into different day buckets depending on timezone
# Example: 2024-01-15 00:00 UTC becomes 2024-01-14 16:00 PST (falls on different day)
@grouped_values = object_scope.group_by_period(
group_by,
:created_at,
default_value: 0,
range: range,
permit: %w[day week month year hour],
time_zone: timezone
).count
end
end
@@ -0,0 +1,9 @@
class V2::Reports::Timeseries::ReportBuilder < V2::Reports::Timeseries::BaseTimeseriesBuilder
def timeseries
data_source.timeseries
end
def aggregate_value
data_source.aggregate
end
end
@@ -34,6 +34,10 @@ class Api::V1::Accounts::AgentBotsController < Api::V1::Accounts::BaseController
@agent_bot.reload
end
def reset_secret
@agent_bot.reset_secret!
end
private
def agent_bot
@@ -0,0 +1,59 @@
class Api::V1::Accounts::Articles::BulkActionsController < Api::V1::Accounts::BaseController
before_action :portal
before_action :check_authorization
before_action :set_articles, only: [:update_status, :update_category, :delete_articles]
def translate
head :not_implemented
end
def update_status
return render_could_not_create_error(I18n.t('portals.articles.no_articles_found')) if @articles.none?
return render_could_not_create_error(I18n.t('portals.articles.invalid_status')) unless Article.statuses.key?(params[:status])
ActiveRecord::Base.transaction do
@articles.find_each { |article| article.update!(status: params[:status]) }
end
head :ok
rescue ActiveRecord::RecordInvalid => e
render_could_not_create_error(e.message)
end
def update_category
return render_could_not_create_error(I18n.t('portals.articles.no_articles_found')) if @articles.none?
return render_could_not_create_error(I18n.t('portals.articles.category_not_found')) unless category_valid?
ActiveRecord::Base.transaction do
@articles.find_each { |article| article.update!(category_id: params[:category_id]) }
end
head :ok
rescue ActiveRecord::RecordInvalid => e
render_could_not_create_error(e.message)
end
def delete_articles
return render_could_not_create_error(I18n.t('portals.articles.no_articles_found')) if @articles.none?
@articles.destroy_all
head :ok
end
private
def portal
@portal ||= Current.account.portals.find_by!(slug: params[:portal_id])
end
def check_authorization
authorize(Article, :create?)
end
def set_articles
@articles = @portal.articles.where(id: params[:ids])
end
def category_valid?
@portal.categories.exists?(id: params[:category_id])
end
end
Api::V1::Accounts::Articles::BulkActionsController.prepend_mod_with('Api::V1::Accounts::Articles::BulkActionsController')
@@ -40,7 +40,7 @@ class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController
end
def reorder
Article.update_positions(params[:positions_hash])
Article.update_positions(portal: @portal, positions_hash: params[:positions_hash])
head :ok
end
@@ -30,7 +30,8 @@ class Api::V1::Accounts::AssignmentPoliciesController < Api::V1::Accounts::BaseC
def assignment_policy_params
params.require(:assignment_policy).permit(
:name, :description, :assignment_order, :conversation_priority,
:fair_distribution_limit, :fair_distribution_window, :enabled
:fair_distribution_limit, :fair_distribution_window, :enabled,
:exclude_older_than_hours
)
end
end
@@ -8,8 +8,8 @@ class Api::V1::Accounts::Captain::PreferencesController < Api::V1::Accounts::Bas
def update
params_to_update = captain_params
@current_account.captain_models = params_to_update[:captain_models] if params_to_update[:captain_models]
@current_account.captain_features = params_to_update[:captain_features] if params_to_update[:captain_features]
@current_account.captain_models = params_to_update[:captain_models] if params_to_update.key?(:captain_models)
@current_account.captain_features = params_to_update[:captain_features] if params_to_update.key?(:captain_features)
@current_account.save!
render json: preferences_payload
@@ -38,7 +38,7 @@ class Api::V1::Accounts::Captain::PreferencesController < Api::V1::Accounts::Bas
def merged_captain_models
existing_models = @current_account.captain_models || {}
existing_models.merge(permitted_captain_models)
existing_models.merge(permitted_captain_models).compact_blank.presence
end
def merged_captain_features
@@ -47,29 +47,30 @@ class Api::V1::Accounts::Captain::PreferencesController < Api::V1::Accounts::Bas
end
def permitted_captain_models
params.require(:captain_models).permit(
:editor, :assistant, :copilot, :label_suggestion,
:audio_transcription, :help_center_search
).to_h.stringify_keys
params.require(:captain_models).permit(*captain_feature_keys).to_h.stringify_keys
end
def permitted_captain_features
params.require(:captain_features).permit(
:editor, :assistant, :copilot, :label_suggestion,
:audio_transcription, :help_center_search
).to_h.stringify_keys
params.require(:captain_features).permit(*captain_feature_keys).to_h.stringify_keys
end
def captain_feature_keys
Llm::Models.feature_keys.map(&:to_sym)
end
def features_with_account_preferences
preferences = Current.account.captain_preferences
account_features = preferences[:features] || {}
account_models = preferences[:models] || {}
Llm::Models.feature_keys.index_with do |feature_key|
config = Llm::Models.feature_config(feature_key)
route = Llm::FeatureRouter.resolve(feature: feature_key, account: Current.account)
config.merge(
enabled: account_features[feature_key] == true,
selected: account_models[feature_key] || config[:default]
model: route[:model],
selected: route[:model],
provider: route[:provider],
source: route[:source]
)
end
end
@@ -57,7 +57,7 @@ class Api::V1::Accounts::Captain::TasksController < Api::V1::Accounts::BaseContr
if result.nil?
render json: { message: nil }
elsif result[:error]
render json: { error: result[:error] }, status: :unprocessable_entity
render json: { error: result[:error] }, status: :unprocessable_content
else
response_data = { message: result[:message] }
response_data[:follow_up_context] = result[:follow_up_context] if result[:follow_up_context]
@@ -69,3 +69,5 @@ class Api::V1::Accounts::Captain::TasksController < Api::V1::Accounts::BaseContr
authorize(:'captain/tasks')
end
end
Api::V1::Accounts::Captain::TasksController.prepend_mod_with('Api::V1::Accounts::Captain::TasksController')
@@ -1,7 +1,7 @@
class Api::V1::Accounts::CategoriesController < Api::V1::Accounts::BaseController
before_action :portal
before_action :check_authorization
before_action :fetch_category, except: [:index, :create]
before_action :fetch_category, except: [:index, :create, :reorder]
before_action :set_current_page, only: [:index]
def index
@@ -32,6 +32,11 @@ class Api::V1::Accounts::CategoriesController < Api::V1::Accounts::BaseControlle
head :ok
end
def reorder
Category.update_positions(portal: @portal, positions_hash: params[:positions_hash])
head :ok
end
private
def fetch_category
@@ -39,7 +44,7 @@ class Api::V1::Accounts::CategoriesController < Api::V1::Accounts::BaseControlle
end
def portal
@portal ||= Current.account.portals.find_by(slug: params[:portal_id])
@portal ||= Current.account.portals.find_by!(slug: params[:portal_id])
end
def related_categories_records
@@ -48,7 +53,7 @@ class Api::V1::Accounts::CategoriesController < Api::V1::Accounts::BaseControlle
def category_params
params.require(:category).permit(
:name, :description, :position, :slug, :locale, :icon, :parent_category_id, :associated_category_id
:name, :description, :position, :slug, :locale, :icon, :icon_color, :parent_category_id, :associated_category_id
)
end
@@ -0,0 +1,55 @@
module Api::V1::Accounts::Concerns::WhatsappHealthManagement
extend ActiveSupport::Concern
included do
skip_before_action :check_authorization, only: [:health, :register_webhook]
before_action :check_admin_authorization?, only: [:register_webhook]
before_action :validate_whatsapp_cloud_channel, only: [:health, :register_webhook]
end
def sync_templates
return render status: :unprocessable_entity, json: { error: 'Template sync is only available for WhatsApp channels' } unless whatsapp_channel?
trigger_template_sync
render status: :ok, json: { message: 'Template sync initiated successfully' }
rescue StandardError => e
render status: :internal_server_error, json: { error: e.message }
end
def health
health_data = Whatsapp::HealthService.new(@inbox.channel).fetch_health_status
render json: health_data
rescue StandardError => e
Rails.logger.error "[INBOX HEALTH] Error fetching health data: #{e.message}"
render json: { error: e.message }, status: :unprocessable_entity
end
def register_webhook
Whatsapp::WebhookSetupService.new(@inbox.channel).register_callback
render json: { message: 'Webhook registered successfully' }, status: :ok
rescue StandardError => e
Rails.logger.error "[INBOX WEBHOOK] Webhook registration failed: #{e.message}"
render json: { error: e.message }, status: :unprocessable_entity
end
private
def validate_whatsapp_cloud_channel
return if @inbox.channel.is_a?(Channel::Whatsapp) && @inbox.channel.provider == 'whatsapp_cloud'
render json: { error: 'Health data only available for WhatsApp Cloud API channels' }, status: :bad_request
end
def whatsapp_channel?
@inbox.whatsapp? || (@inbox.twilio? && @inbox.channel.whatsapp?)
end
def trigger_template_sync
if @inbox.whatsapp?
Channels::Whatsapp::TemplatesSyncJob.perform_later(@inbox.channel)
elsif @inbox.twilio? && @inbox.channel.whatsapp?
Channels::Twilio::TemplatesSyncJob.perform_later(@inbox.channel)
end
end
end
@@ -0,0 +1,18 @@
class Api::V1::Accounts::Contacts::AttachmentsController < Api::V1::Accounts::Contacts::BaseController
RESULTS_PER_PAGE = 100
def index
conversations = Conversations::PermissionFilterService.new(
Current.account.conversations.where(contact_id: @contact.id),
Current.user,
Current.account
).perform
@attachments = Attachment.where(message_id: Message.where(conversation_id: conversations).select(:id))
.includes({ file_attachment: :blob }, message: [:conversation, :inbox, { sender: { avatar_attachment: :blob } }])
.order(created_at: :desc)
.page(params[:page])
.per(RESULTS_PER_PAGE)
@attachments_count = @attachments.total_count
end
end
@@ -5,7 +5,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
sort_on :phone_number, type: :string
sort_on :last_activity_at, internal_name: :order_on_last_activity_at, type: :scope, scope_params: [:direction]
sort_on :created_at, internal_name: :order_on_created_at, type: :scope, scope_params: [:direction]
sort_on :company, internal_name: :order_on_company_name, type: :scope, scope_params: [:direction]
sort_on :company_name, internal_name: :order_on_company_name, type: :scope, scope_params: [:direction]
sort_on :city, internal_name: :order_on_city, type: :scope, scope_params: [:direction]
sort_on :country, internal_name: :order_on_country_name, type: :scope, scope_params: [:direction]
@@ -201,7 +201,9 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
end
def fetch_contact
@contact = Current.account.contacts.includes(contact_inboxes: [:inbox]).find(params[:id])
contact_scope = Current.account.contacts
contact_scope = contact_scope.includes(contact_inboxes: [:inbox]) if @include_contact_inboxes
@contact = contact_scope.find(params[:id])
end
def process_avatar_from_url
@@ -212,3 +214,5 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
render json: error, status: error_status
end
end
Api::V1::Accounts::ContactsController.prepend_mod_with('Api::V1::Accounts::ContactsController')
@@ -52,6 +52,9 @@ class Api::V1::Accounts::Conversations::MessagesController < Api::V1::Accounts::
end
render json: { content: translated_content }
rescue Google::Cloud::Error => e
# `details` carries the clean human message; `message` includes gRPC debug noise
render_could_not_create_error(e.details.presence || e.message)
end
private
@@ -0,0 +1,16 @@
class Api::V1::Accounts::Conversations::UnreadCountsController < Api::V1::Accounts::BaseController
before_action :ensure_unread_counts_enabled
def index
counts = ::Conversations::UnreadCounts::Counter.new(account: Current.account, user: Current.user).perform
render json: { payload: counts }
end
private
def ensure_unread_counts_enabled
return if Current.account.feature_enabled?('conversation_unread_counts')
render json: { error: I18n.t('errors.conversations.unread_counts.feature_not_enabled') }, status: :forbidden
end
end
@@ -15,7 +15,7 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
end
def meta
result = conversation_finder.perform
result = conversation_finder.perform_meta_only
@conversations_count = result[:count]
end
@@ -28,7 +28,7 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
def attachments
@attachments_count = @conversation.attachments.count
@attachments = @conversation.attachments
.includes(:message)
.includes({ file_attachment: :blob }, message: [:inbox, { sender: { avatar_attachment: :blob } }])
.order(created_at: :desc)
.page(attachment_params[:page])
.per(ATTACHMENT_RESULTS_PER_PAGE)
@@ -70,6 +70,7 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
def transcript
render json: { error: 'email param missing' }, status: :unprocessable_entity and return if params[:email].blank?
return render_payment_required('Email transcript is not available on your plan') unless @conversation.account.email_transcript_enabled?
return head :too_many_requests unless @conversation.account.within_email_rate_limit?
ConversationReplyMailer.with(account: @conversation.account).conversation_transcript(@conversation, params[:email])&.deliver_later
@@ -106,7 +107,7 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
end
def toggle_typing_status
typing_status_manager = ::Conversations::TypingStatusManager.new(@conversation, current_user, params)
typing_status_manager = ::Conversations::TypingStatusManager.new(@conversation, Current.user, params)
typing_status_manager.toggle_typing_status
head :ok
end
@@ -115,6 +116,8 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
# High-traffic accounts generate excessive DB writes when agents frequently switch between conversations.
# Throttle last_seen updates to once per hour when there are no unread messages to reduce DB load.
# Always update immediately if there are unread messages to maintain accurate read/unread state.
# Visiting a conversation should clear any unread inbox notifications for this conversation.
Notification::MarkConversationReadService.new(user: Current.user, account: Current.account, conversation: @conversation).perform
return update_last_seen_on_conversation(DateTime.now.utc, true) if assignee? && @conversation.assignee_unread_messages.any?
return update_last_seen_on_conversation(DateTime.now.utc, false) if !assignee? && @conversation.unread_messages.any?
@@ -137,7 +140,7 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
def destroy
authorize @conversation, :destroy?
::DeleteObjectJob.perform_later(@conversation, Current.user, request.ip)
::Conversations::DeleteService.new(conversation: @conversation, user: Current.user, ip: request.ip).perform
head :ok
end
@@ -159,6 +162,8 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
# rubocop:disable Rails/SkipsModelValidations
@conversation.update_columns(updates)
# rubocop:enable Rails/SkipsModelValidations
::Conversations::UnreadCounts::Notifier.new(@conversation).perform
end
def should_update_last_seen?
@@ -1,6 +1,7 @@
class Api::V1::Accounts::CustomAttributeDefinitionsController < Api::V1::Accounts::BaseController
before_action :fetch_custom_attributes_definitions, except: [:create]
before_action :fetch_custom_attribute_definition, only: [:show, :update, :destroy]
before_action :check_authorization
DEFAULT_ATTRIBUTE_MODEL = 'conversation_attribute'.freeze
def index; end
@@ -1,6 +1,7 @@
class Api::V1::Accounts::InboxCsatTemplatesController < Api::V1::Accounts::BaseController
before_action :fetch_inbox
before_action :validate_whatsapp_channel
before_action :validate_captain_enabled, only: [:analyze]
def show
service = CsatTemplateManagementService.new(@inbox)
@@ -24,6 +25,23 @@ class Api::V1::Accounts::InboxCsatTemplatesController < Api::V1::Accounts::BaseC
render json: { error: 'Template parameters are required' }, status: :unprocessable_entity
end
def analyze
template_params = extract_template_params
return render_missing_message_error if template_params[:message].blank?
result = CsatTemplateUtilityAnalysisService.new(
account: Current.account,
inbox: @inbox,
message: template_params[:message],
button_text: template_params[:button_text],
language: template_params[:language]
).perform
render json: result
rescue ActionController::ParameterMissing
render json: { error: 'Template parameters are required' }, status: :unprocessable_entity
end
private
def fetch_inbox
@@ -46,6 +64,12 @@ class Api::V1::Accounts::InboxCsatTemplatesController < Api::V1::Accounts::BaseC
render json: { error: 'Message is required' }, status: :unprocessable_entity
end
def validate_captain_enabled
return if Current.account.feature_enabled?('captain_integration')
render json: { error: 'Captain is required for template analysis' }, status: :forbidden
end
def render_template_creation_result(result)
if result[:success]
render_successful_template_creation(result)
@@ -4,11 +4,14 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
before_action :fetch_agent_bot, only: [:set_agent_bot]
before_action :validate_limit, only: [:create]
# we are already handling the authorization in fetch inbox
before_action :check_authorization, except: [:show, :health]
before_action :validate_whatsapp_cloud_channel, only: [:health]
before_action :check_authorization, except: [:show]
include Api::V1::Accounts::Concerns::WhatsappHealthManagement
def index
@inboxes = policy_scope(Current.account.inboxes.order_by_name.includes(:channel, { avatar_attachment: [:blob] }))
@inboxes = policy_scope(Current.account.inboxes)
.includes(:channel, :portal, :working_hours, { avatar_attachment: :blob })
.order_by_name
end
def show; end
@@ -65,28 +68,17 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
head :ok
end
def reset_secret
return head :not_found unless @inbox.api?
@inbox.channel.reset_secret!
end
def destroy
::DeleteObjectJob.perform_later(@inbox, Current.user, request.ip) if @inbox.present?
render status: :ok, json: { message: I18n.t('messages.inbox_deletetion_response') }
end
def sync_templates
return render status: :unprocessable_entity, json: { error: 'Template sync is only available for WhatsApp channels' } unless whatsapp_channel?
trigger_template_sync
render status: :ok, json: { message: 'Template sync initiated successfully' }
rescue StandardError => e
render status: :internal_server_error, json: { error: e.message }
end
def health
health_data = Whatsapp::HealthService.new(@inbox.channel).fetch_health_status
render json: health_data
rescue StandardError => e
Rails.logger.error "[INBOX HEALTH] Error fetching health data: #{e.message}"
render json: { error: e.message }, status: :unprocessable_entity
end
private
def fetch_inbox
@@ -95,13 +87,7 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
end
def fetch_agent_bot
@agent_bot = AgentBot.find(params[:agent_bot]) if params[:agent_bot]
end
def validate_whatsapp_cloud_channel
return if @inbox.channel.is_a?(Channel::Whatsapp) && @inbox.channel.provider == 'whatsapp_cloud'
render json: { error: 'Health data only available for WhatsApp Cloud API channels' }, status: :bad_request
@agent_bot = AgentBot.accessible_to(Current.account).find(params[:agent_bot]) if params[:agent_bot]
end
def create_channel
@@ -200,18 +186,6 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
def get_channel_attributes(channel_type)
channel_type.constantize.const_defined?(:EDITABLE_ATTRS) ? channel_type.constantize::EDITABLE_ATTRS.presence : []
end
def whatsapp_channel?
@inbox.whatsapp? || (@inbox.twilio? && @inbox.channel.whatsapp?)
end
def trigger_template_sync
if @inbox.whatsapp?
Channels::Whatsapp::TemplatesSyncJob.perform_later(@inbox.channel)
elsif @inbox.twilio? && @inbox.channel.whatsapp?
Channels::Twilio::TemplatesSyncJob.perform_later(@inbox.channel)
end
end
end
Api::V1::Accounts::InboxesController.prepend_mod_with('Api::V1::Accounts::InboxesController')
@@ -11,7 +11,7 @@ class Api::V1::Accounts::Instagram::AuthorizationsController < Api::V1::Accounts
enable_fb_login: '0',
force_authentication: '1',
response_type: 'code',
state: generate_instagram_token(Current.account.id)
state: generate_instagram_token(Current.account.id, params[:return_to])
}
)
if redirect_url
@@ -15,7 +15,7 @@ class Api::V1::Accounts::Integrations::DyteController < Api::V1::Accounts::BaseC
end
render_response(
dyte_processor_service.add_participant_to_meeting(@message.content_attributes['data']['meeting_id'], Current.user)
dyte_processor_service.add_participant_to_meeting(@message.content_attributes['data']['meeting_id'], Current.user, @message)
)
end
@@ -126,7 +126,7 @@ class Api::V1::Accounts::Integrations::LinearController < Api::V1::Accounts::Bas
return unless @hook&.access_token
begin
linear_client = Linear.new(@hook.access_token)
linear_client = Linear.new(@hook.access_token, refresh_token: @hook.settings&.[]('refresh_token'))
linear_client.revoke_token
rescue StandardError => e
Rails.logger.error "Failed to revoke Linear token: #{e.message}"
@@ -18,7 +18,16 @@ class Api::V1::Accounts::LabelsController < Api::V1::Accounts::BaseController
end
def destroy
label_title = @label.title
account_id = Current.account.id
label_deleted_at = Time.current
@label.destroy!
Labels::RemoveAssociationsJob.perform_later(
label_title: label_title,
account_id: account_id,
label_deleted_at: label_deleted_at
)
head :ok
end
@@ -6,8 +6,7 @@ class Api::V1::Accounts::Microsoft::AuthorizationsController < Api::V1::Accounts
{
redirect_uri: "#{base_url}/microsoft/callback",
scope: scope,
state: state,
prompt: 'consent'
state: state
}
)
if redirect_url
@@ -41,9 +41,9 @@ class Api::V1::Accounts::NotificationsController < Api::V1::Accounts::BaseContro
def destroy_all
if params[:type] == 'read'
::Notification::DeleteNotificationJob.perform_later(Current.user, type: :read)
::Notification::DeleteNotificationJob.perform_later(Current.user, Current.account, type: :read)
else
::Notification::DeleteNotificationJob.perform_later(Current.user, type: :all)
::Notification::DeleteNotificationJob.perform_later(Current.user, Current.account, type: :all)
end
head :ok
end
@@ -69,7 +69,7 @@ class Api::V1::Accounts::NotificationsController < Api::V1::Accounts::BaseContro
end
def fetch_notification
@notification = current_user.notifications.find(params[:id])
@notification = current_user.notifications.where(account_id: Current.account.id).find(params[:id])
end
def set_current_page
@@ -8,7 +8,15 @@ class Api::V1::Accounts::OauthAuthorizationController < Api::V1::Accounts::BaseC
end
def state
Current.account.to_sgid(expires_in: 15.minutes).to_s
# The sgid purpose doubles as a return hint: onboarding tags it so the callback
# can route the user back to inbox setup. The purpose is part of the signed
# payload (tamper-proof), and a non-onboarding request keeps the default
# purpose, leaving callers like Notion byte-identical.
Current.account.to_sgid(expires_in: 15.minutes, for: state_purpose).to_s
end
def state_purpose
params[:return_to] == 'onboarding' ? 'onboarding' : 'default'
end
def base_url
@@ -0,0 +1,89 @@
class Api::V1::Accounts::OnboardingsController < Api::V1::Accounts::BaseController
before_action :check_admin_authorization?
ONBOARDING_STEP_KEY = 'onboarding_step'.freeze
STEP_ACCOUNT_DETAILS = 'account_details'.freeze
STEP_INBOX_SETUP = 'inbox_setup'.freeze
ONBOARDING_STEPS = [STEP_ACCOUNT_DETAILS, STEP_INBOX_SETUP].freeze
def update
return render json: { error: 'Invalid onboarding step' }, status: :unprocessable_entity unless ONBOARDING_STEPS.include?(params[:onboarding_step])
@account = Current.account
# The client declares the step it is completing; `account_details` runs
# `complete_account_details`, and so on. The known-step guard above keeps the
# client value from `send`-ing an arbitrary method.
send("complete_#{params[:onboarding_step]}")
render 'api/v1/accounts/update', format: :json
end
def help_center_generation
render json: help_center_generation_status
end
private
def complete_account_details
# Only act while the cursor still points here, so a stale replay after
# onboarding finished can't re-enter it.
return unless current_step == STEP_ACCOUNT_DETAILS
@account.assign_attributes(account_params)
@account.custom_attributes.merge!(custom_attributes_params)
# inbox_setup is a cloud-only step (DEPLOYMENT_ENV config, not a hardcoded
# environment check); self-hosted finishes onboarding here.
if ChatwootApp.chatwoot_cloud?
move_to_step(STEP_INBOX_SETUP)
create_onboarding_inboxes
else
finish_onboarding
end
end
def complete_inbox_setup
# Only finalize while the cursor still points here, so a stale or out-of-order
# request can't end onboarding early. Replays are no-ops.
return unless current_step == STEP_INBOX_SETUP
finish_onboarding
end
def current_step
@account.custom_attributes[ONBOARDING_STEP_KEY]
end
def move_to_step(step)
@account.custom_attributes[ONBOARDING_STEP_KEY] = step
@account.save!
end
def finish_onboarding
@account.custom_attributes.delete(ONBOARDING_STEP_KEY)
@account.save!
end
def create_onboarding_inboxes
Onboarding::WebWidgetCreationService.new(@account, Current.user).perform
end
def account_params
params.permit(:name, :locale)
end
def custom_attributes_params
params.permit(:industry, :company_size, :timezone, :referral_source, :user_role, :website)
end
def help_center_generation_status
{
generation_id: nil,
state: nil,
articles_count: 0,
categories_count: 0
}
end
end
Api::V1::Accounts::OnboardingsController.prepend_mod_with('Api::V1::Accounts::OnboardingsController')
@@ -18,7 +18,7 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
@portal = Current.account.portals.build(portal_params.merge(live_chat_widget_params))
@portal.custom_domain = parsed_custom_domain
@portal.save!
process_attached_logo
process_attached_logo if params[:blob_id].present?
end
def update
@@ -61,9 +61,8 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
end
def process_attached_logo
blob_id = params[:blob_id]
blob = ActiveStorage::Blob.find_signed(blob_id)
@portal.logo.attach(blob)
blob = ActiveStorage::Blob.find_signed(params[:blob_id].to_s)
@portal.logo.attach(blob) if blob
end
private
@@ -79,16 +78,23 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
def portal_params
params.require(:portal).permit(
:id, :color, :custom_domain, :header_text, :homepage_link,
:name, :page_title, :slug, :archived, { config: [:default_locale, { allowed_locales: [] }] }
:name, :page_title, :slug, :archived,
{ config: [:default_locale, :layout, { allowed_locales: [] }, { draft_locales: [] },
{ social_profiles: %i[facebook x instagram linkedin youtube tiktok github whatsapp] },
{ locale_translations: locale_translation_keys.index_with { %i[name page_title header_text] } }] }
)
end
def locale_translation_keys
params.dig(:portal, :config, :locale_translations)&.keys || []
end
def live_chat_widget_params
permitted_params = params.permit(:inbox_id)
return {} unless permitted_params.key?(:inbox_id)
return { channel_web_widget_id: nil } if permitted_params[:inbox_id].blank?
inbox = Inbox.find(permitted_params[:inbox_id])
inbox = Current.account.inboxes.find(permitted_params[:inbox_id])
return {} unless inbox.web_widget?
{ channel_web_widget_id: inbox.channel.id }
@@ -99,6 +105,8 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
end
def parsed_custom_domain
return @portal.custom_domain if @portal.custom_domain.blank?
domain = URI.parse(@portal.custom_domain)
domain.is_a?(URI::HTTP) ? domain.host : @portal.custom_domain
end
@@ -29,6 +29,6 @@ class Api::V1::Accounts::TeamsController < Api::V1::Accounts::BaseController
end
def team_params
params.require(:team).permit(:name, :description, :allow_auto_assign)
params.require(:team).permit(:name, :description, :allow_auto_assign, :icon, :icon_color)
end
end
@@ -3,7 +3,7 @@ class Api::V1::Accounts::Tiktok::AuthorizationsController < Api::V1::Accounts::O
def create
redirect_url = Tiktok::AuthClient.authorize_url(
state: generate_tiktok_token(Current.account.id)
state: generate_tiktok_token(Current.account.id, params[:return_to])
)
if redirect_url
@@ -5,7 +5,7 @@ class Api::V1::Accounts::UploadController < Api::V1::Accounts::BaseController
elsif params[:external_url].present?
create_from_url
else
render_error('No file or URL provided', :unprocessable_entity)
render_error(I18n.t('errors.upload.missing_input'), :unprocessable_entity)
end
render_success(result) if result.is_a?(ActiveStorage::Blob)
@@ -19,35 +19,21 @@ class Api::V1::Accounts::UploadController < Api::V1::Accounts::BaseController
end
def create_from_url
uri = parse_uri(params[:external_url])
return if performed?
fetch_and_process_file_from_uri(uri)
end
def parse_uri(url)
uri = URI.parse(url)
validate_uri(uri)
uri
rescue URI::InvalidURIError, SocketError
render_error('Invalid URL provided', :unprocessable_entity)
nil
end
def validate_uri(uri)
raise URI::InvalidURIError unless uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS)
end
def fetch_and_process_file_from_uri(uri)
uri.open do |file|
create_and_save_blob(file, File.basename(uri.path), file.content_type)
SafeFetch.fetch(params[:external_url].to_s) do |result|
create_and_save_blob(result.tempfile, result.filename, result.content_type)
end
rescue OpenURI::HTTPError => e
render_error("Failed to fetch file from URL: #{e.message}", :unprocessable_entity)
rescue SocketError
render_error('Invalid URL provided', :unprocessable_entity)
rescue SafeFetch::HttpError => e
render_error(I18n.t('errors.upload.fetch_failed_with_message', message: e.message), :unprocessable_entity)
rescue SafeFetch::FetchError
render_error(I18n.t('errors.upload.fetch_failed'), :unprocessable_entity)
rescue SafeFetch::FileTooLargeError
render_error(I18n.t('errors.upload.file_too_large'), :unprocessable_entity)
rescue SafeFetch::UnsupportedContentTypeError
render_error(I18n.t('errors.upload.unsupported_content_type'), :unprocessable_entity)
rescue SafeFetch::Error
render_error(I18n.t('errors.upload.invalid_url'), :unprocessable_entity)
rescue StandardError
render_error('An unexpected error occurred', :internal_server_error)
render_error(I18n.t('errors.upload.unexpected'), :internal_server_error)
end
def create_and_save_blob(io, filename, content_type)
@@ -1,18 +0,0 @@
class Api::V1::Accounts::WorkingHoursController < Api::V1::Accounts::BaseController
before_action :check_authorization
before_action :fetch_webhook, only: [:update]
def update
@working_hour.update!(working_hour_params)
end
private
def working_hour_params
params.require(:working_hour).permit(:inbox_id, :open_hour, :open_minutes, :close_hour, :close_minutes, :closed_all_day)
end
def fetch_working_hour
@working_hour = Current.account.working_hours.find(params[:id])
end
end
+35 -4
View File
@@ -30,9 +30,20 @@ class Api::V1::AccountsController < Api::BaseController
locale: account_params[:locale],
user: current_user
).perform
enqueue_branding_enrichment
if @user
send_auth_headers(@user)
render 'api/v1/accounts/create', format: :json, locals: { resource: @user }
# Authenticated users (dashboard "add account") and api_only signups
# need the full response with account_id. API-only deployments have no
# frontend to handle the email confirmation flow, so they need auth
# tokens to proceed.
# Unauthenticated web signup returns only the email — no session is
# created until the user confirms via the email link.
if current_user || api_only_signup?
send_auth_headers(@user)
render 'api/v1/accounts/create', format: :json, locals: { resource: @user }
else
render json: { email: @user.email }
end
else
render_error_response(CustomExceptions::Account::SignupFailed.new({}))
end
@@ -59,6 +70,17 @@ class Api::V1::AccountsController < Api::BaseController
private
def enqueue_branding_enrichment
email = account_params[:email].presence || @user&.email
return if email.blank?
Account::BrandingEnrichmentJob.perform_later(@account.id, email)
Redis::Alfred.set(format(Redis::Alfred::ACCOUNT_ONBOARDING_ENRICHMENT, account_id: @account.id), '1', ex: 30)
rescue StandardError => e
# Enrichment is optional — never let queue/Redis failures abort signup
ChatwootExceptionTracker.new(e).capture_exception
end
def ensure_account_name
# ensure that account_name and user_full_name is present
# this is becuase the account builder and the models validations are not triggered
@@ -88,7 +110,7 @@ class Api::V1::AccountsController < Api::BaseController
end
def custom_attributes_params
params.permit(:industry, :company_size, :timezone)
params.permit(:industry, :company_size, :timezone, :referral_source, :user_role, :website)
end
def settings_params
@@ -100,7 +122,16 @@ class Api::V1::AccountsController < Api::BaseController
end
def check_signup_enabled
raise ActionController::RoutingError, 'Not Found' if GlobalConfigService.load('ENABLE_ACCOUNT_SIGNUP', 'false') == 'false'
raise ActionController::RoutingError, 'Not Found' unless GlobalConfigService.account_signup_enabled?
end
def api_only_signup?
# CW_API_ONLY_SERVER is the canonical flag for API-only deployments.
# ENABLE_ACCOUNT_SIGNUP='api_only' is a legacy sentinel for the same purpose.
# Read ENABLE_ACCOUNT_SIGNUP raw from InstallationConfig because GlobalConfig.get
# typecasts it to boolean, coercing 'api_only' to true.
ActiveModel::Type::Boolean.new.cast(ENV.fetch('CW_API_ONLY_SERVER', false)) ||
InstallationConfig.find_by(name: 'ENABLE_ACCOUNT_SIGNUP')&.value.to_s == 'api_only'
end
def validate_captcha
@@ -8,7 +8,8 @@ class Api::V1::NotificationSubscriptionsController < Api::BaseController
end
def destroy
notification_subscription = NotificationSubscription.where(["subscription_attributes->>'push_token' = ?", params[:push_token]]).first
notification_subscription = current_user.notification_subscriptions
.where(["subscription_attributes->>'push_token' = ?", params[:push_token]]).first
notification_subscription.destroy! if notification_subscription.present?
head :ok
end
@@ -2,8 +2,8 @@ class Api::V1::Profile::MfaController < Api::BaseController
before_action :check_mfa_feature_available
before_action :check_mfa_enabled, only: [:destroy, :backup_codes]
before_action :check_mfa_disabled, only: [:create, :verify]
before_action :validate_otp, only: [:verify, :backup_codes, :destroy]
before_action :validate_password, only: [:destroy]
before_action :validate_otp, only: [:verify, :backup_codes, :destroy]
def show; end
@@ -48,7 +48,8 @@ class Api::V1::Profile::MfaController < Api::BaseController
def validate_otp
authenticated = Mfa::AuthenticationService.new(
user: current_user,
otp_code: mfa_params[:otp_code]
otp_code: mfa_params[:otp_code],
backup_code: mfa_params[:backup_code]
).authenticate
return if authenticated
@@ -63,6 +64,6 @@ class Api::V1::Profile::MfaController < Api::BaseController
end
def mfa_params
params.permit(:otp_code, :password)
params.permit(:otp_code, :backup_code, :password)
end
end
@@ -0,0 +1,36 @@
class Api::V1::Profile::SessionsController < Api::BaseController
before_action :set_session, only: [:destroy]
def index
@sessions = current_user.user_sessions.where(client_id: active_token_client_ids).order(last_activity_at: :desc)
@current_client_id = request.headers['client']
end
def destroy
if @session.current?(request.headers['client'])
render json: { error: I18n.t('profile_settings.sessions.cannot_revoke_current') }, status: :unprocessable_entity
return
end
revoke_token!(@session.client_id)
@session.destroy!
head :ok
end
private
def set_session
@session = current_user.user_sessions.find(params[:id])
end
def revoke_token!(client_id)
tokens = current_user.tokens
tokens.delete(client_id)
current_user.update!(tokens: tokens)
end
def active_token_client_ids
now = Time.current.to_i
(current_user.tokens || {}).select { |_, v| v['expiry'].to_i > now }.keys
end
end
@@ -19,7 +19,7 @@ class Api::V1::Widget::ContactsController < Api::V1::Widget::BaseController
contact = @contact
end
@contact_inbox.update(hmac_verified: true) if should_verify_hmac? && valid_hmac?
@contact_inbox.update(hmac_verified: true) if should_verify_hmac?
identify_contact(contact)
end
@@ -35,7 +35,9 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
end
def transcript
return head :too_many_requests unless conversation.present? && conversation.account.within_email_rate_limit?
return head :too_many_requests if conversation.blank?
return head :payment_required unless conversation.account.email_transcript_enabled?
return head :too_many_requests unless conversation.account.within_email_rate_limit?
send_transcript_email
head :ok
@@ -10,7 +10,8 @@ class Api::V1::Widget::Integrations::DyteController < Api::V1::Widget::BaseContr
response = dyte_processor_service.add_participant_to_meeting(
@message.content_attributes['data']['meeting_id'],
@conversation.contact
@conversation.contact,
@message
)
render_response(response)
end
@@ -43,7 +43,15 @@ class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController
end
def set_conversation
@conversation = create_conversation if conversation.nil?
return unless conversation.nil?
@conversation = create_conversation
apply_labels if permitted_params[:labels].present?
end
def apply_labels
valid_labels = inbox.account.labels.where(title: permitted_params[:labels]).pluck(:title)
@conversation.update_labels(valid_labels) if valid_labels.present?
end
def message_finder_params
@@ -64,10 +72,21 @@ class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController
def permitted_params
# timestamp parameter is used in create conversation method
params.permit(:id, :before, :after, :website_token, contact: [:name, :email], message: [:content, :referer_url, :timestamp, :echo_id, :reply_to])
# custom_attributes and labels are applied when a new conversation is created alongside the first message
params.permit(
:id, :before, :after, :website_token,
contact: [:name, :email],
message: [:content, :referer_url, :timestamp, :echo_id, :reply_to],
custom_attributes: {},
labels: []
)
end
def set_message
@message = @web_widget.inbox.messages.find(permitted_params[:id])
# `conversation.messages.find` would be simpler, but `conversation` is `conversations.last`,
# which means a visitor with more than one open thread could not edit a message in any
# but their most recent one. Scoping across all of the visitor's conversations keeps the
# happy path correct for that future multi-conversation widget flow.
@message = Message.where(conversation_id: conversations.select(:id)).find(permitted_params[:id])
end
end
@@ -78,6 +78,15 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
render json: builder.build
end
OUTGOING_MESSAGES_ALLOWED_GROUP_BY = %w[agent team inbox label].freeze
def outgoing_messages_count
return head :unprocessable_entity unless OUTGOING_MESSAGES_ALLOWED_GROUP_BY.include?(params[:group_by])
builder = V2::Reports::OutgoingMessagesCountBuilder.new(Current.account, outgoing_messages_count_params)
render json: builder.build
end
private
def generate_csv(filename, template)
@@ -171,4 +180,12 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
until: params[:until]
}
end
def outgoing_messages_count_params
{
group_by: params[:group_by],
since: params[:since],
until: params[:until]
}
end
end
@@ -3,15 +3,15 @@ class Api::V2::Accounts::SummaryReportsController < Api::V1::Accounts::BaseContr
before_action :prepare_builder_params, only: [:agent, :team, :inbox, :label, :channel]
def agent
render_report_with(V2::Reports::AgentSummaryBuilder)
render_report_with(V2::Reports::AgentSummaryBuilder, type: :agent)
end
def team
render_report_with(V2::Reports::TeamSummaryBuilder)
render_report_with(V2::Reports::TeamSummaryBuilder, type: :team)
end
def inbox
render_report_with(V2::Reports::InboxSummaryBuilder)
render_report_with(V2::Reports::InboxSummaryBuilder, type: :inbox)
end
def label
@@ -38,8 +38,9 @@ class Api::V2::Accounts::SummaryReportsController < Api::V1::Accounts::BaseContr
}
end
def render_report_with(builder_class)
builder = builder_class.new(account: Current.account, params: @builder_params)
def render_report_with(builder_class, type: nil)
builder_params = type.present? ? @builder_params.merge(type: type) : @builder_params
builder = builder_class.new(account: Current.account, params: builder_params)
render json: builder.build
end
@@ -58,7 +58,7 @@ class Api::V2::AccountsController < Api::BaseController
end
def check_signup_enabled
raise ActionController::RoutingError, 'Not Found' if GlobalConfigService.load('ENABLE_ACCOUNT_SIGNUP', 'false') == 'false'
raise ActionController::RoutingError, 'Not Found' unless GlobalConfigService.account_signup_enabled?
end
def validate_captcha
@@ -3,6 +3,7 @@ class ApplicationController < ActionController::Base
include RequestExceptionHandler
include Pundit::Authorization
include SwitchLocale
include TrackSessionActivity
skip_before_action :verify_authenticity_token
@@ -0,0 +1,18 @@
# Unauthenticated endpoint for resending confirmation emails during signup.
# This is a standalone controller (not on DeviseOverrides::ConfirmationsController)
# because OmniAuth middleware intercepts all POST /auth/* routes as provider
# callbacks, and Devise controller filters cause 307 redirects for custom actions.
# Inherits from ActionController::API to avoid both issues entirely.
# Rate-limited by Rack::Attack (IP + email) and gated by hCaptcha.
class Auth::ResendConfirmationsController < ActionController::API
def create
return head(:ok) unless ChatwootCaptcha.new(params[:h_captcha_client_response]).valid?
email = params[:email]
return head(:ok) unless email.is_a?(String)
user = User.from_email(email.strip.downcase)
user&.send_confirmation_instructions unless user&.confirmed?
head :ok
end
end
@@ -1,8 +1,9 @@
module AccessTokenAuthHelper
BOT_ACCESSIBLE_ENDPOINTS = {
'api/v1/accounts/conversations' => %w[toggle_status toggle_priority create update custom_attributes],
'api/v1/accounts/conversations' => %w[show toggle_status toggle_typing_status toggle_priority create update custom_attributes],
'api/v1/accounts/conversations/messages' => ['create'],
'api/v1/accounts/conversations/assignments' => ['create']
'api/v1/accounts/conversations/assignments' => ['create'],
'api/v1/accounts/conversations/labels' => %w[index create]
}.freeze
def ensure_access_token
@@ -28,7 +29,7 @@ module AccessTokenAuthHelper
def validate_bot_access_token!
return if Current.user.is_a?(User)
return if agent_bot_accessible?
return if @resource.is_a?(AgentBot) && agent_bot_accessible?
render_unauthorized('Access to this endpoint is not authorized for bots')
end
@@ -2,6 +2,10 @@
# This concern handles the token verification step.
module MetaTokenVerifyConcern
CHANNEL_APP_SECRET_KEYS = %w[app_secret app_secret_key client_secret api_secret].freeze
META_SIGNATURE_HEADER = 'X-Hub-Signature-256'.freeze
META_SIGNATURE_PREFIX = 'sha256='.freeze
def verify
service = is_a?(Webhooks::WhatsappController) ? 'whatsapp' : 'instagram'
if valid_token?(params['hub.verify_token'])
@@ -14,6 +18,53 @@ module MetaTokenVerifyConcern
private
def verify_meta_signature!
return unless meta_signature_verification_required?
return if valid_meta_signature?
head :unauthorized
end
def valid_meta_signature?
signature = request.headers[META_SIGNATURE_HEADER]
return false unless signature&.start_with?(META_SIGNATURE_PREFIX)
meta_app_secrets.any? do |secret|
next false if secret.blank?
expected_signature = "#{META_SIGNATURE_PREFIX}#{OpenSSL::HMAC.hexdigest('SHA256', secret, meta_request_body)}"
ActiveSupport::SecurityUtils.secure_compare(expected_signature, signature)
end
end
def meta_request_body
@meta_request_body ||= request.raw_post
end
def meta_app_secrets
raise 'Overwrite this method in your controller'
end
def meta_signature_verification_required?
true
end
def channel_meta_app_secrets(channel)
return [] if channel.blank?
secrets = []
secrets << channel.app_secret if channel.respond_to?(:app_secret)
secrets.concat(provider_config_meta_app_secrets(channel))
secrets.compact_blank.uniq
end
def provider_config_meta_app_secrets(channel)
return [] unless channel.respond_to?(:provider_config)
provider_config = channel.provider_config.to_h.with_indifferent_access
CHANNEL_APP_SECRET_KEYS.filter_map { |key| provider_config[key].presence }
end
def valid_token?(_token)
raise 'Overwrite this method your controller'
end
@@ -0,0 +1,29 @@
module PortalHomeData
extend ActiveSupport::Concern
private
def load_home_data
base_articles = @portal.articles.published.where(locale: @locale).includes(:author, :category)
@visible_categories = @portal.categories
.where(locale: @locale)
.joins(:articles).where(articles: { status: :published })
.order(position: :asc)
.group('categories.id')
@popular_topics = @visible_categories.first(3)
@featured = base_articles.order_by_views.limit(6)
@category_contributors = build_category_contributors(@visible_categories)
end
def build_category_contributors(categories)
category_ids = categories.map(&:id)
return {} if category_ids.empty?
@portal.articles
.published
.where(locale: @locale, category_id: category_ids)
.includes(:author)
.group_by(&:category_id)
.transform_values { |articles| articles.filter_map(&:author).uniq.first(3) }
end
end
@@ -0,0 +1,22 @@
module TrackSessionActivity
extend ActiveSupport::Concern
included do
after_action :update_session_activity
end
private
def update_session_activity
return unless current_user
return if request.headers['client'].blank?
UserSessionTrackingService.new(
user: current_user,
request: request,
client_id: request.headers['client']
).update_activity!
rescue StandardError => e
Rails.logger.warn "Session activity update failed: #{e.message}"
end
end
+13 -1
View File
@@ -1,5 +1,6 @@
class DashboardController < ActionController::Base
include SwitchLocale
include PortalHomeData
GLOBAL_CONFIG_KEYS = %w[
LOGO
@@ -63,6 +64,10 @@ class DashboardController < ActionController::Base
return unless @portal
@locale = @portal.default_locale
if @portal.layout == 'documentation'
request.variant = :documentation
load_home_data
end
render 'public/api/v1/portals/show', layout: 'portal', portal: @portal and return
end
@@ -80,10 +85,17 @@ class DashboardController < ActionController::Base
IS_ENTERPRISE: ChatwootApp.enterprise?,
AZURE_APP_ID: GlobalConfigService.load('AZURE_APP_ID', ''),
GIT_SHA: GIT_HASH,
ALLOWED_LOGIN_METHODS: allowed_login_methods
ALLOWED_LOGIN_METHODS: allowed_login_methods,
ACTIVE_PLATFORM_BANNERS: active_platform_banners
}
end
def active_platform_banners
return [] unless ChatwootApp.chatwoot_cloud?
PlatformBanner.active.order(created_at: :desc).as_json(only: %i[id banner_message banner_type updated_at])
end
def allowed_login_methods
methods = ['email']
methods << 'google_oauth' if GlobalConfigService.load('ENABLE_GOOGLE_OAUTH_LOGIN', 'true').to_s != 'false'
@@ -10,7 +10,12 @@ class DeviseOverrides::OmniauthCallbacksController < DeviseTokenAuth::OmniauthCa
private
def sign_in_user
# Capture before skip_confirmation! sets confirmed_at, which would
# make oauth_user_needs_password_reset? return false and skip the
# password reset for persisted unconfirmed users.
needs_password_reset = oauth_user_needs_password_reset?
@resource.skip_confirmation! if confirmable_enabled?
set_random_password_if_oauth_user if needs_password_reset
# once the resource is found and verified
# we can just send them to the login page again with the SSO params
@@ -20,7 +25,10 @@ class DeviseOverrides::OmniauthCallbacksController < DeviseTokenAuth::OmniauthCa
end
def sign_in_user_on_mobile
# See comment in sign_in_user for why this is captured before skip_confirmation!
needs_password_reset = oauth_user_needs_password_reset?
@resource.skip_confirmation! if confirmable_enabled?
set_random_password_if_oauth_user if needs_password_reset
# once the resource is found and verified
# we can just send them to the login page again with the SSO params
@@ -37,6 +45,7 @@ class DeviseOverrides::OmniauthCallbacksController < DeviseTokenAuth::OmniauthCa
return redirect_to login_page_url(error: 'business-account-only') unless validate_signup_email_is_business_domain?
create_account_for_user
set_random_password_if_oauth_user
token = @resource.send(:set_reset_password_token)
frontend_url = ENV.fetch('FRONTEND_URL', nil)
redirect_to "#{frontend_url}/app/auth/password/edit?config=default&reset_password_token=#{token}"
@@ -51,8 +60,7 @@ class DeviseOverrides::OmniauthCallbacksController < DeviseTokenAuth::OmniauthCa
end
def account_signup_allowed?
# set it to true by default, this is the behaviour across the app
GlobalConfigService.load('ENABLE_ACCOUNT_SIGNUP', 'false') != 'false'
GlobalConfigService.account_signup_enabled?
end
def resource_class(_mapping = nil)
@@ -82,6 +90,15 @@ class DeviseOverrides::OmniauthCallbacksController < DeviseTokenAuth::OmniauthCa
Avatar::AvatarFromUrlJob.perform_later(@resource, auth_hash['info']['image'])
end
def oauth_user_needs_password_reset?
@resource.present? && (@resource.new_record? || !@resource.confirmed?)
end
def set_random_password_if_oauth_user
# Password must satisfy secure_password requirements (uppercase, lowercase, number, special char)
@resource.update(password: "#{SecureRandom.hex(16)}aA1!") if @resource.persisted?
end
def default_devise_mapping
'user'
end
@@ -6,12 +6,8 @@ class DeviseOverrides::PasswordsController < Devise::PasswordsController
def create
@user = User.from_email(params[:email])
if @user
@user.send_reset_password_instructions
build_response(I18n.t('messages.reset_password_success'), 200)
else
build_response(I18n.t('messages.reset_password_failure'), 404)
end
@user&.send_reset_password_instructions
build_response(I18n.t('messages.reset_password'), 200)
end
def update
@@ -1,4 +1,6 @@
class DeviseOverrides::SessionsController < DeviseTokenAuth::SessionsController
MAX_SESSIONS = ENV.fetch('MAX_USER_SESSIONS', 25).to_i
# Prevent session parameter from being passed
# Unpermitted parameter: session
wrap_parameters format: []
@@ -14,17 +16,27 @@ class DeviseOverrides::SessionsController < DeviseTokenAuth::SessionsController
user = find_user_for_authentication
return handle_mfa_required(user) if user&.mfa_enabled?
return if user && enforce_session_limit_for_password_login(user)
# Only proceed with standard authentication if no MFA is required
super
end
def render_create_success
track_user_session unless @impersonation
render partial: 'devise/auth', formats: [:json], locals: { resource: @resource }
end
private
def render_create_error_not_confirmed
render_error(
:unauthorized,
I18n.t('devise_token_auth.sessions.not_confirmed', email: @resource.email),
error_code: 'user_not_confirmed'
)
end
def find_user_for_authentication
return nil unless params[:email].present? && params[:password].present?
@@ -45,6 +57,8 @@ class DeviseOverrides::SessionsController < DeviseTokenAuth::SessionsController
end
def handle_sso_authentication
return if !@impersonation && enforce_session_limit_for_password_login(@resource)
authenticate_resource_with_sso_token
yield @resource if block_given?
render_create_success
@@ -57,7 +71,10 @@ class DeviseOverrides::SessionsController < DeviseTokenAuth::SessionsController
end
def authenticate_resource_with_sso_token
@token = @resource.create_token
# DTA evicts the earliest-expiring token after save when at max_number_of_devices.
# The short-lived impersonation token would always be that one, so pre-evict to make room.
make_room_for_impersonation_token if @impersonation
@token = @resource.create_token(lifespan: @impersonation ? 2.days.to_i : nil)
@resource.save!
sign_in(:user, @resource, store: false, bypass: false)
@@ -65,11 +82,21 @@ class DeviseOverrides::SessionsController < DeviseTokenAuth::SessionsController
@resource.invalidate_sso_auth_token(params[:sso_auth_token])
end
def make_room_for_impersonation_token
return if @resource.tokens.size < DeviseTokenAuth.max_number_of_devices
oldest_client_id = @resource.tokens.min_by { |_, v| v['expiry'].to_i }&.first
@resource.tokens.delete(oldest_client_id) if oldest_client_id
end
def process_sso_auth_token
return if params[:email].blank?
user = User.from_email(params[:email])
@resource = user if user&.valid_sso_auth_token?(params[:sso_auth_token])
return unless user&.valid_sso_auth_token?(params[:sso_auth_token])
@resource = user
@impersonation = user.sso_auth_token_impersonation?(params[:sso_auth_token])
end
def handle_mfa_required(user)
@@ -95,6 +122,7 @@ class DeviseOverrides::SessionsController < DeviseTokenAuth::SessionsController
end
def sign_in_mfa_user(user)
evict_oldest_session(user) if sessions_limit_reached?(user)
@resource = user
@token = @resource.create_token
@resource.save!
@@ -106,6 +134,103 @@ class DeviseOverrides::SessionsController < DeviseTokenAuth::SessionsController
def render_mfa_error(message_key, status = :bad_request)
render json: { error: I18n.t(message_key) }, status: status
end
def sessions_limit_reached?(user)
active_token_count(user) >= MAX_SESSIONS
end
def active_token_count(user)
now = Time.current.to_i
(user.tokens || {}).count { |_, v| v['expiry'].to_i > now }
end
# Returns true when a response has been rendered (e.g., 409 picker). Non-browser clients
# auto-evict instead of getting stuck on a UI they can't render.
def enforce_session_limit_for_password_login(user)
if revoking_sessions?
revoke_sessions_for_login(user)
return false
end
return false unless sessions_limit_reached?(user)
# Picker only when every token has a tracked session; partial tracking would
# show a misleading count, so fall through to silent eviction instead.
if browser_request? && user.user_sessions.count >= user.tokens.size
handle_sessions_limit_for_login(user)
true
else
evict_oldest_session(user)
false
end
end
def browser_request?
request.user_agent.to_s.include?('Mozilla')
end
def revoking_sessions?
params[:revoke_session_id].present? || params[:revoke_all_sessions].present?
end
def revoke_sessions_for_login(user)
if params[:revoke_all_sessions].present?
user.tokens = {}
user.save!
user.user_sessions.destroy_all
elsif params[:revoke_session_id].present?
session = user.user_sessions.find_by(id: params[:revoke_session_id])
return unless session
user.tokens.delete(session.client_id)
user.save!
session.destroy!
end
end
def evict_oldest_session(user)
# Drop pre-rollout untracked tokens first so freshly tracked logins aren't evicted.
return evict_oldest_token(user) if user.user_sessions.count < user.tokens.size
oldest_session = user.user_sessions.order(Arel.sql('COALESCE(last_activity_at, created_at) ASC')).first
return evict_oldest_token(user) unless oldest_session
user.tokens.delete(oldest_session.client_id)
user.save!
oldest_session.destroy!
end
# Fallback if a token exists without a UserSession row (e.g., legacy data before tracking shipped).
def evict_oldest_token(user)
return if user.tokens.blank?
oldest_client_id = user.tokens.min_by { |_, v| v['expiry'].to_i }&.first
return unless oldest_client_id
user.tokens.delete(oldest_client_id)
user.save!
end
PICKER_SESSION_FIELDS = %i[id browser_name browser_version device_name platform_name platform_version
ip_address city country last_activity_at created_at].freeze
def handle_sessions_limit_for_login(user)
sessions = user.user_sessions.order(last_activity_at: :desc).map { |s| s.slice(*PICKER_SESSION_FIELDS) }
render json: { sessions_limit_reached: true, sessions: sessions }, status: :conflict
end
def track_user_session
client_id = @token&.try(:client) || response.headers['client']
return unless client_id.present? && @resource.present?
UserSessionTrackingService.new(
user: @resource,
request: request,
client_id: client_id
).create_or_update!
rescue StandardError => e
Rails.logger.warn "Session tracking failed: #{e.message}"
end
end
DeviseOverrides::SessionsController.prepend_mod_with('DeviseOverrides::SessionsController')
@@ -28,6 +28,8 @@ class Instagram::CallbacksController < ApplicationController
@long_lived_token_response = exchange_for_long_lived_token(@response.token)
inbox, already_exists = find_or_create_inbox
return redirect_to app_onboarding_inbox_setup_url(account_id: account_id) if return_to == 'onboarding'
if already_exists
redirect_to app_instagram_inbox_settings_url(account_id: account_id, inbox_id: inbox.id)
else
@@ -149,6 +151,10 @@ class Instagram::CallbacksController < ApplicationController
verify_instagram_token(params[:state])
end
def return_to
instagram_token_return_to(params[:state])
end
def oauth_code
params[:code]
end
+39 -12
View File
@@ -2,6 +2,8 @@ class Linear::CallbacksController < ApplicationController
include Linear::IntegrationHelper
def show
return redirect_to(safe_linear_redirect_uri) if params[:code].blank? || account_id.blank?
@response = oauth_client.auth_code.get_token(
params[:code],
redirect_uri: "#{base_url}/linear/callback"
@@ -10,7 +12,7 @@ class Linear::CallbacksController < ApplicationController
handle_response
rescue StandardError => e
Rails.logger.error("Linear callback error: #{e.message}")
redirect_to linear_redirect_uri
redirect_to safe_linear_redirect_uri
end
private
@@ -31,22 +33,19 @@ class Linear::CallbacksController < ApplicationController
end
def handle_response
hook = account.hooks.new(
raise ArgumentError, 'Missing access token in Linear OAuth response' if parsed_body['access_token'].blank?
hook = account.hooks.find_or_initialize_by(app_id: 'linear')
hook.assign_attributes(
access_token: parsed_body['access_token'],
status: 'enabled',
app_id: 'linear',
settings: {
token_type: parsed_body['token_type'],
expires_in: parsed_body['expires_in'],
scope: parsed_body['scope']
}
settings: merged_integration_settings(hook.settings)
)
# You may wonder why we're not handling the refresh token update, since the token will expire only after 10 years, https://github.com/linear/linear/issues/251
hook.save!
redirect_to linear_redirect_uri
rescue StandardError => e
Rails.logger.error("Linear callback error: #{e.message}")
redirect_to linear_redirect_uri
redirect_to safe_linear_redirect_uri
end
def account
@@ -54,19 +53,47 @@ class Linear::CallbacksController < ApplicationController
end
def account_id
return unless params[:state]
return @account_id if instance_variable_defined?(:@account_id)
verify_linear_token(params[:state])
@account_id = params[:state].present? ? verify_linear_token(params[:state]) : nil
end
def linear_redirect_uri
"#{ENV.fetch('FRONTEND_URL', nil)}/app/accounts/#{account.id}/settings/integrations/linear"
end
def safe_linear_redirect_uri
return base_url if account_id.blank?
linear_redirect_uri
rescue StandardError
base_url
end
def parsed_body
@parsed_body ||= @response.response.parsed
end
def integration_settings
{
token_type: parsed_body['token_type'],
expires_in: parsed_body['expires_in'],
expires_on: expires_on,
scope: parsed_body['scope'],
refresh_token: parsed_body['refresh_token']
}.compact
end
def merged_integration_settings(existing_settings)
existing_settings.to_h.with_indifferent_access.merge(integration_settings)
end
def expires_on
return if parsed_body['expires_in'].blank?
(Time.current.utc + parsed_body['expires_in'].to_i.seconds).to_s
end
def base_url
ENV.fetch('FRONTEND_URL', 'http://localhost:3000')
end
@@ -14,4 +14,11 @@ class Microsoft::CallbacksController < OauthCallbackController
def imap_address
'outlook.office365.com'
end
# Exchange Online's SMTP AUTH (XOAUTH2) rejects proxy addresses in the SASL `user=` field;
# it must match the token's UPN. `preferred_username` is the documented v2.0 claim;
# `upn` is the v1.0 fallback.
def imap_login_identity
users_data['preferred_username'] || users_data['upn'] || super
end
end
+25 -2
View File
@@ -16,6 +16,8 @@ class OauthCallbackController < ApplicationController
def handle_response
inbox, already_exists = find_or_create_inbox
return redirect_to app_onboarding_inbox_setup_url(account_id: account.id) if return_to == 'onboarding'
if already_exists
redirect_to app_email_inbox_settings_url(account_id: account.id, inbox_id: inbox.id)
else
@@ -44,7 +46,7 @@ class OauthCallbackController < ApplicationController
def update_channel(channel_email)
channel_email.update!({
imap_login: users_data['email'], imap_address: imap_address,
imap_login: imap_login_identity, imap_address: imap_address,
imap_port: '993', imap_enabled: true,
provider: provider_name,
provider_config: {
@@ -55,6 +57,13 @@ class OauthCallbackController < ApplicationController
})
end
# Identity used as the IMAP/SMTP login (SASL XOAUTH2 `user=` field). Defaults to the
# id_token's email claim; providers override when their server requires a different
# claim (e.g. Microsoft SMTP requires UPN).
def imap_login_identity
users_data['email']
end
def provider_name
raise NotImplementedError
end
@@ -81,10 +90,19 @@ class OauthCallbackController < ApplicationController
decoded_token[0]
end
# The sgid purpose carries the onboarding return hint (see
# OauthAuthorizationController#state). Try the onboarding purpose first — a match
# both resolves the account and records the return target — then fall back to the
# default purpose used by every other caller.
def account_from_signed_id
raise ActionController::BadRequest, 'Missing state variable' if params[:state].blank?
account = GlobalID::Locator.locate_signed(params[:state])
if (account = GlobalID::Locator.locate_signed(params[:state], for: 'onboarding'))
@return_to = 'onboarding'
else
account = GlobalID::Locator.locate_signed(params[:state])
end
raise 'Invalid or expired state' if account.nil?
account
@@ -94,6 +112,11 @@ class OauthCallbackController < ApplicationController
@account ||= account_from_signed_id
end
def return_to
account # resolving the sgid records which purpose matched
@return_to
end
# Fallback name, for when name field is missing from users_data
def fallback_name
users_data['email'].split('@').first.parameterize.titleize
@@ -3,7 +3,7 @@ class Platform::Api::V1::AgentBotsController < PlatformController
before_action :validate_platform_app_permissible, except: [:index, :create]
def index
@resources = @platform_app.platform_app_permissibles.where(permissible_type: 'AgentBot').all
@resources = @platform_app.platform_app_permissibles.where(permissible_type: 'AgentBot').includes(:permissible)
end
def show; end
@@ -0,0 +1,101 @@
class Platform::Api::V1::EmailChannelMigrationsController < PlatformController
before_action :set_account
before_action :validate_account_permissible
before_action :validate_feature_flag
before_action :validate_params
def create
results = migrate_email_channels
render json: { results: results }, status: :ok
end
private
def set_account
@account = Account.find(params[:account_id])
end
def validate_account_permissible
return if @platform_app.platform_app_permissibles.find_by(permissible: @account)
render json: { error: 'Non permissible resource' }, status: :unauthorized
end
def validate_feature_flag
return if ActiveModel::Type::Boolean.new.cast(ENV.fetch('EMAIL_CHANNEL_MIGRATION', false))
render json: { error: 'Email channel migration is not enabled' }, status: :forbidden
end
def validate_params
return render json: { error: 'Missing migrations parameter' }, status: :unprocessable_entity if migration_params.blank?
return unless migration_params.size > MAX_MIGRATIONS
return render json: { error: "Too many migrations (max #{MAX_MIGRATIONS})" },
status: :unprocessable_entity
end
def migrate_email_channels
migration_params.map { |entry| migrate_single(entry) }
end
MAX_MIGRATIONS = 25
SUPPORTED_PROVIDERS = %w[google microsoft].freeze
def migrate_single(entry)
validate_provider!(entry[:provider])
ActiveRecord::Base.transaction do
channel = create_channel(entry)
inbox = create_inbox(channel, entry)
{ email: entry[:email], inbox_id: inbox.id, channel_id: channel.id, status: 'success' }
end
rescue StandardError => e
{ email: entry[:email], status: 'error', message: e.message }
end
def create_channel(entry)
Channel::Email.create!(
account_id: @account.id,
email: entry[:email],
provider: entry[:provider],
provider_config: entry[:provider_config]&.to_h,
imap_enabled: entry.fetch(:imap_enabled, true),
imap_address: entry[:imap_address] || default_imap_address(entry[:provider]),
imap_port: entry[:imap_port] || 993,
imap_login: entry[:imap_login] || entry[:email],
imap_enable_ssl: entry.fetch(:imap_enable_ssl, true)
)
end
def create_inbox(channel, entry)
@account.inboxes.create!(
name: entry[:inbox_name] || "Migrated #{entry[:provider]&.capitalize}: #{entry[:email]}",
channel: channel
)
end
def validate_provider!(provider)
return if SUPPORTED_PROVIDERS.include?(provider)
raise ArgumentError, "Unsupported provider '#{provider}'. Must be one of: #{SUPPORTED_PROVIDERS.join(', ')}"
end
def default_imap_address(provider)
case provider
when 'google' then 'imap.gmail.com'
when 'microsoft' then 'outlook.office365.com'
else ''
end
end
def migration_params
params.permit(migrations: [
:email, :provider, :inbox_name,
:imap_enabled, :imap_address, :imap_port, :imap_login, :imap_enable_ssl,
{ provider_config: {} }
])[:migrations]
end
end
@@ -24,6 +24,10 @@ class Public::Api::V1::InboxesController < PublicController
def set_conversation
return if params[:conversation_id].blank?
@conversation = @contact_inbox.contact.conversations.find_by!(display_id: params[:conversation_id])
@conversation = if @contact_inbox.hmac_verified?
@contact_inbox.contact.conversations.find_by!(display_id: params[:conversation_id])
else
@contact_inbox.conversations.find_by!(display_id: params[:conversation_id])
end
end
end
@@ -1,11 +1,15 @@
class Public::Api::V1::Portals::ArticlesController < Public::Api::V1::Portals::BaseController
before_action :ensure_custom_domain_request, only: [:show, :index]
before_action :ensure_custom_domain_request, only: [:show, :index, :show_markdown]
before_action :portal
before_action :set_portal_layout
before_action :set_view_variant
before_action :ensure_portal_feature_enabled
before_action :set_category, except: [:index, :show, :tracking_pixel]
before_action :set_article, only: [:show]
before_action :set_article, only: [:show, :show_markdown]
layout 'portal'
def index
@search_query = list_params[:query]
@articles = @portal.articles.published.includes(:category, :author)
@articles = @articles.where(locale: permitted_params[:locale]) if permitted_params[:locale].present?
@@ -19,6 +23,13 @@ class Public::Api::V1::Portals::ArticlesController < Public::Api::V1::Portals::B
def show
@og_image_url = helpers.set_og_image_url(@portal.name, @article.title)
@parsed_content = render_article_content(@article.content.to_s)
end
def show_markdown
return head :not_found unless @article&.published?
render plain: @article.content.to_s, content_type: 'text/markdown; charset=utf-8'
end
def tracking_pixel
@@ -60,7 +71,6 @@ class Public::Api::V1::Portals::ArticlesController < Public::Api::V1::Portals::B
def set_article
@article = @portal.articles.find_by(slug: permitted_params[:article_slug])
@parsed_content = render_article_content(@article.content)
end
def set_category
@@ -73,7 +83,9 @@ class Public::Api::V1::Portals::ArticlesController < Public::Api::V1::Portals::B
end
def list_params
params.permit(:query, :locale, :sort, :status, :page, :per_page)
@list_params ||= params.permit(:query, :locale, :sort, :status, :page, :per_page).tap do |permitted|
permitted[:query] = permitted[:query].to_s.strip.presence
end
end
def permitted_params

Some files were not shown because too many files have changed in this diff Show More