diff --git a/.env.example b/.env.example
index bc7380a29..69b1b9cde 100644
--- a/.env.example
+++ b/.env.example
@@ -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
diff --git a/Gemfile b/Gemfile
index e10984f53..680a0738b 100644
--- a/Gemfile
+++ b/Gemfile
@@ -89,7 +89,7 @@ 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
diff --git a/Gemfile.lock b/Gemfile.lock
index 4da0e5847..7151d0ff1 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -301,7 +301,7 @@ GEM
railties (>= 5.0.0)
faker (3.2.0)
i18n (>= 1.8.11, < 2)
- faraday (2.14.1)
+ faraday (2.14.2)
faraday-net_http (>= 2.0, < 3.5)
json
logger
@@ -491,7 +491,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)
@@ -996,11 +996,13 @@ GEM
activemodel (>= 3.2)
mail (~> 2.5)
version_gem (1.1.4)
- vite_rails (3.0.17)
- railties (>= 5.1, < 8)
+ 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)
@@ -1102,7 +1104,7 @@ DEPENDENCIES
json_schemer
judoscale-rails
judoscale-sidekiq
- jwt
+ jwt (~> 2.10, >= 2.10.3)
kaminari
koala
letter_opener
diff --git a/VERSION_CW b/VERSION_CW
index c412a4e2e..d2b9909a9 100644
--- a/VERSION_CW
+++ b/VERSION_CW
@@ -1 +1 @@
-4.14.0
+4.14.1
diff --git a/app/builders/messages/facebook/message_builder.rb b/app/builders/messages/facebook/message_builder.rb
index 1f59deadb..24b6d9e70 100644
--- a/app/builders/messages/facebook/message_builder.rb
+++ b/app/builders/messages/facebook/message_builder.rb
@@ -92,10 +92,18 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
def fallback_params(attachment)
{
fallback_title: attachment['title'],
- external_url: attachment['url']
+ external_url: attachment['url'] || attachment.dig('payload', 'url')
}
end
+ # Facebook shared posts point to page URLs, not downloadable media URLs.
+ # Keep this Facebook-only so Messenger/Instagram share attachments still use the parent media handling.
+ def normalize_file_type(type)
+ return :fallback if type.to_sym == :share
+
+ super
+ end
+
def conversation_params
{
account_id: @inbox.account_id,
diff --git a/app/builders/messages/message_builder.rb b/app/builders/messages/message_builder.rb
index 7df72e14a..297e77fcc 100644
--- a/app/builders/messages/message_builder.rb
+++ b/app/builders/messages/message_builder.rb
@@ -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'
diff --git a/app/controllers/api/v1/accounts/articles/bulk_actions_controller.rb b/app/controllers/api/v1/accounts/articles/bulk_actions_controller.rb
index b45c16828..7ea9cab5d 100644
--- a/app/controllers/api/v1/accounts/articles/bulk_actions_controller.rb
+++ b/app/controllers/api/v1/accounts/articles/bulk_actions_controller.rb
@@ -1,7 +1,7 @@
class Api::V1::Accounts::Articles::BulkActionsController < Api::V1::Accounts::BaseController
before_action :portal
before_action :check_authorization
- before_action :set_articles, only: [:update_status, :delete_articles]
+ before_action :set_articles, only: [:update_status, :update_category, :delete_articles]
def translate
head :not_implemented
@@ -19,6 +19,18 @@ class Api::V1::Accounts::Articles::BulkActionsController < Api::V1::Accounts::Ba
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?
@@ -39,5 +51,9 @@ class Api::V1::Accounts::Articles::BulkActionsController < Api::V1::Accounts::Ba
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')
diff --git a/app/controllers/api/v1/accounts/instagram/authorizations_controller.rb b/app/controllers/api/v1/accounts/instagram/authorizations_controller.rb
index 053c29731..116a31e85 100644
--- a/app/controllers/api/v1/accounts/instagram/authorizations_controller.rb
+++ b/app/controllers/api/v1/accounts/instagram/authorizations_controller.rb
@@ -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
diff --git a/app/controllers/api/v1/accounts/oauth_authorization_controller.rb b/app/controllers/api/v1/accounts/oauth_authorization_controller.rb
index feb218b59..7fbca86da 100644
--- a/app/controllers/api/v1/accounts/oauth_authorization_controller.rb
+++ b/app/controllers/api/v1/accounts/oauth_authorization_controller.rb
@@ -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
diff --git a/app/controllers/api/v1/accounts/onboardings_controller.rb b/app/controllers/api/v1/accounts/onboardings_controller.rb
new file mode 100644
index 000000000..77c6245ea
--- /dev/null
+++ b/app/controllers/api/v1/accounts/onboardings_controller.rb
@@ -0,0 +1,36 @@
+class Api::V1::Accounts::OnboardingsController < Api::V1::Accounts::BaseController
+ before_action :check_admin_authorization?
+
+ def update
+ @account = Current.account
+ finalize = finalizing_account_details?
+
+ @account.assign_attributes(account_params)
+ @account.custom_attributes.merge!(custom_attributes_params)
+ @account.custom_attributes.delete('onboarding_step') if finalize
+ @account.save!
+
+ # TODO: re-enable when the help center generation UI is ready to surface progress
+ # Onboarding::HelpCenterCreationService.new(@account, Current.user).perform if finalize && website.present?
+
+ render 'api/v1/accounts/update', format: :json
+ end
+
+ private
+
+ def finalizing_account_details?
+ @account.custom_attributes['onboarding_step'] == 'account_details'
+ end
+
+ def website
+ custom_attributes_params[:website]
+ 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
+end
diff --git a/app/controllers/api/v1/accounts/tiktok/authorizations_controller.rb b/app/controllers/api/v1/accounts/tiktok/authorizations_controller.rb
index 7c7320393..64bf38775 100644
--- a/app/controllers/api/v1/accounts/tiktok/authorizations_controller.rb
+++ b/app/controllers/api/v1/accounts/tiktok/authorizations_controller.rb
@@ -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
diff --git a/app/controllers/api/v1/accounts/working_hours_controller.rb b/app/controllers/api/v1/accounts/working_hours_controller.rb
deleted file mode 100644
index 96d98293a..000000000
--- a/app/controllers/api/v1/accounts/working_hours_controller.rb
+++ /dev/null
@@ -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
diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb
index 865b387b9..fb991949a 100644
--- a/app/controllers/api/v1/accounts_controller.rb
+++ b/app/controllers/api/v1/accounts_controller.rb
@@ -58,7 +58,6 @@ class Api::V1::AccountsController < Api::BaseController
@account.assign_attributes(account_params.slice(:name, :locale, :domain, :support_email))
@account.custom_attributes.merge!(custom_attributes_params)
@account.settings.merge!(settings_params)
- @account.custom_attributes.delete('onboarding_step') if @account.custom_attributes['onboarding_step'] == 'account_details'
@account.custom_attributes['onboarding_step'] = 'invite_team' if @account.custom_attributes['onboarding_step'] == 'account_update'
@account.save!
end
diff --git a/app/controllers/instagram/callbacks_controller.rb b/app/controllers/instagram/callbacks_controller.rb
index 4dc8ece1c..cd317363c 100644
--- a/app/controllers/instagram/callbacks_controller.rb
+++ b/app/controllers/instagram/callbacks_controller.rb
@@ -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
diff --git a/app/controllers/microsoft/callbacks_controller.rb b/app/controllers/microsoft/callbacks_controller.rb
index 2f07505fc..045789f75 100644
--- a/app/controllers/microsoft/callbacks_controller.rb
+++ b/app/controllers/microsoft/callbacks_controller.rb
@@ -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
diff --git a/app/controllers/oauth_callback_controller.rb b/app/controllers/oauth_callback_controller.rb
index be0fa5008..4a3d049a6 100644
--- a/app/controllers/oauth_callback_controller.rb
+++ b/app/controllers/oauth_callback_controller.rb
@@ -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
diff --git a/app/controllers/public/api/v1/portals/search_controller.rb b/app/controllers/public/api/v1/portals/search_controller.rb
new file mode 100644
index 000000000..104741773
--- /dev/null
+++ b/app/controllers/public/api/v1/portals/search_controller.rb
@@ -0,0 +1,31 @@
+class Public::Api::V1::Portals::SearchController < Public::Api::V1::Portals::BaseController
+ before_action :ensure_custom_domain_request, only: [:index]
+ before_action :portal
+ before_action :set_portal_layout
+ before_action :set_view_variant
+ before_action :ensure_portal_feature_enabled
+ layout 'portal'
+
+ def index
+ @query = params[:query].to_s.strip
+ @articles = @portal.articles.published.includes(:category).where(locale: params[:locale])
+
+ search_articles
+
+ @articles = @articles.page(params[:page]).per(10)
+ end
+
+ private
+
+ def search_articles
+ @articles = @query.present? ? @articles.search(search_params) : @articles.none
+ end
+
+ def search_params
+ params.permit(:query, :locale, :sort, :status, :page).tap do |permitted|
+ permitted[:query] = @query
+ end
+ end
+end
+
+Public::Api::V1::Portals::SearchController.prepend_mod_with('Public::Api::V1::Portals::SearchController')
diff --git a/app/controllers/tiktok/callbacks_controller.rb b/app/controllers/tiktok/callbacks_controller.rb
index e484905c3..20c0ee9c0 100644
--- a/app/controllers/tiktok/callbacks_controller.rb
+++ b/app/controllers/tiktok/callbacks_controller.rb
@@ -20,6 +20,8 @@ class Tiktok::CallbacksController < ApplicationController
def process_successful_authorization
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_tiktok_inbox_settings_url(account_id: account_id, inbox_id: inbox.id)
else
@@ -127,6 +129,10 @@ class Tiktok::CallbacksController < ApplicationController
@account_id ||= verify_tiktok_token(params[:state])
end
+ def return_to
+ tiktok_token_return_to(params[:state])
+ end
+
def account
@account ||= Account.find(account_id)
end
diff --git a/app/helpers/instagram/integration_helper.rb b/app/helpers/instagram/integration_helper.rb
index 8ba57bf95..2f91eb6b0 100644
--- a/app/helpers/instagram/integration_helper.rb
+++ b/app/helpers/instagram/integration_helper.rb
@@ -4,21 +4,21 @@ module Instagram::IntegrationHelper
# Generates a signed JWT token for Instagram integration
#
# @param account_id [Integer] The account ID to encode in the token
+ # @param return_to [String, nil] Optional onboarding return hint
# @return [String, nil] The encoded JWT token or nil if client secret is missing
- def generate_instagram_token(account_id)
+ def generate_instagram_token(account_id, return_to = nil)
return if client_secret.blank?
- JWT.encode(token_payload(account_id), client_secret, 'HS256')
+ JWT.encode(token_payload(account_id, return_to), client_secret, 'HS256')
rescue StandardError => e
Rails.logger.error("Failed to generate Instagram token: #{e.message}")
nil
end
- def token_payload(account_id)
- {
- sub: account_id,
- iat: Time.current.to_i
- }
+ def token_payload(account_id, return_to = nil)
+ payload = { sub: account_id, iat: Time.current.to_i }
+ payload[:return_to] = return_to if return_to.present?
+ payload
end
# Verifies and decodes a Instagram JWT token
@@ -28,7 +28,14 @@ module Instagram::IntegrationHelper
def verify_instagram_token(token)
return if token.blank? || client_secret.blank?
- decode_token(token, client_secret)
+ decode_token(token, client_secret)&.dig('sub')
+ end
+
+ # Reads the onboarding return hint from a Instagram JWT token, if present.
+ def instagram_token_return_to(token)
+ return if token.blank? || client_secret.blank?
+
+ decode_token(token, client_secret)&.dig('return_to')
end
private
@@ -41,7 +48,7 @@ module Instagram::IntegrationHelper
JWT.decode(token, secret, true, {
algorithm: 'HS256',
verify_expiration: true
- }).first['sub']
+ }).first
rescue StandardError => e
Rails.logger.error("Unexpected error verifying Instagram token: #{e.message}")
nil
diff --git a/app/helpers/portal_helper.rb b/app/helpers/portal_helper.rb
index 65166145c..0c993ec59 100644
--- a/app/helpers/portal_helper.rb
+++ b/app/helpers/portal_helper.rb
@@ -45,9 +45,16 @@ module PortalHelper
theme.present? && theme != 'system' ? "?theme=#{theme}" : ''
end
+ def portal_query_string(theme, is_plain_layout_enabled)
+ query_params = {}
+ query_params[:theme] = theme if theme.present? && theme != 'system'
+ query_params[:show_plain_layout] = true if is_plain_layout_enabled
+ query_params.present? ? "?#{query_params.to_query}" : ''
+ end
+
def generate_home_link(portal_slug, portal_locale, theme, is_plain_layout_enabled)
if is_plain_layout_enabled
- "/hc/#{portal_slug}/#{portal_locale}#{theme_query_string(theme)}"
+ "/hc/#{portal_slug}/#{portal_locale}#{portal_query_string(theme, is_plain_layout_enabled)}"
else
"/hc/#{portal_slug}/#{portal_locale}"
end
@@ -61,7 +68,7 @@ module PortalHelper
is_plain_layout_enabled = params[:is_plain_layout_enabled]
if is_plain_layout_enabled
- "/hc/#{portal_slug}/#{category_locale}/categories/#{category_slug}#{theme_query_string(theme)}"
+ "/hc/#{portal_slug}/#{category_locale}/categories/#{category_slug}#{portal_query_string(theme, is_plain_layout_enabled)}"
else
"/hc/#{portal_slug}/#{category_locale}/categories/#{category_slug}"
end
@@ -69,7 +76,7 @@ module PortalHelper
def generate_article_link(portal_slug, article_slug, theme, is_plain_layout_enabled)
if is_plain_layout_enabled
- "/hc/#{portal_slug}/articles/#{article_slug}#{theme_query_string(theme)}"
+ "/hc/#{portal_slug}/articles/#{article_slug}#{portal_query_string(theme, is_plain_layout_enabled)}"
else
"/hc/#{portal_slug}/articles/#{article_slug}"
end
diff --git a/app/helpers/tiktok/integration_helper.rb b/app/helpers/tiktok/integration_helper.rb
index b2de4a092..7bc8bc4ab 100644
--- a/app/helpers/tiktok/integration_helper.rb
+++ b/app/helpers/tiktok/integration_helper.rb
@@ -2,11 +2,12 @@ module Tiktok::IntegrationHelper
# Generates a signed JWT token for Tiktok integration
#
# @param account_id [Integer] The account ID to encode in the token
+ # @param return_to [String, nil] Optional onboarding return hint
# @return [String, nil] The encoded JWT token or nil if client secret is missing
- def generate_tiktok_token(account_id)
+ def generate_tiktok_token(account_id, return_to = nil)
return if client_secret.blank?
- JWT.encode(token_payload(account_id), client_secret, 'HS256')
+ JWT.encode(token_payload(account_id, return_to), client_secret, 'HS256')
rescue StandardError => e
Rails.logger.error("Failed to generate TikTok token: #{e.message}")
nil
@@ -19,7 +20,14 @@ module Tiktok::IntegrationHelper
def verify_tiktok_token(token)
return if token.blank? || client_secret.blank?
- decode_token(token, client_secret)
+ decode_token(token, client_secret)&.dig('sub')
+ end
+
+ # Reads the onboarding return hint from a Tiktok JWT token, if present.
+ def tiktok_token_return_to(token)
+ return if token.blank? || client_secret.blank?
+
+ decode_token(token, client_secret)&.dig('return_to')
end
private
@@ -28,18 +36,17 @@ module Tiktok::IntegrationHelper
@client_secret ||= GlobalConfigService.load('TIKTOK_APP_SECRET', nil)
end
- def token_payload(account_id)
- {
- sub: account_id,
- iat: Time.current.to_i
- }
+ def token_payload(account_id, return_to = nil)
+ payload = { sub: account_id, iat: Time.current.to_i }
+ payload[:return_to] = return_to if return_to.present?
+ payload
end
def decode_token(token, secret)
JWT.decode(token, secret, true, {
algorithm: 'HS256',
verify_expiration: true
- }).first['sub']
+ }).first
rescue StandardError => e
Rails.logger.error("Unexpected error verifying Tiktok token: #{e.message}")
nil
diff --git a/app/javascript/dashboard/api/channel/whatsapp/whatsappCallsAPI.js b/app/javascript/dashboard/api/channel/whatsapp/whatsappCallsAPI.js
new file mode 100644
index 000000000..ec24aae34
--- /dev/null
+++ b/app/javascript/dashboard/api/channel/whatsapp/whatsappCallsAPI.js
@@ -0,0 +1,45 @@
+/* global axios */
+import ApiClient from '../../ApiClient';
+
+class WhatsappCallsAPI extends ApiClient {
+ constructor() {
+ super('whatsapp_calls', { accountScoped: true });
+ }
+
+ show(callId) {
+ return axios.get(`${this.url}/${callId}`).then(r => r.data);
+ }
+
+ initiate(conversationId, sdpOffer) {
+ return axios
+ .post(`${this.url}/initiate`, {
+ conversation_id: conversationId,
+ sdp_offer: sdpOffer,
+ })
+ .then(r => r.data);
+ }
+
+ accept(callId, sdpAnswer) {
+ return axios
+ .post(`${this.url}/${callId}/accept`, { sdp_answer: sdpAnswer })
+ .then(r => r.data);
+ }
+
+ reject(callId) {
+ return axios.post(`${this.url}/${callId}/reject`).then(r => r.data);
+ }
+
+ terminate(callId) {
+ return axios.post(`${this.url}/${callId}/terminate`).then(r => r.data);
+ }
+
+ uploadRecording(callId, blob, filename = 'call-recording.webm') {
+ const formData = new FormData();
+ formData.append('recording', blob, filename);
+ return axios
+ .post(`${this.url}/${callId}/upload_recording`, formData)
+ .then(r => r.data);
+ }
+}
+
+export default new WhatsappCallsAPI();
diff --git a/app/javascript/dashboard/api/contacts.js b/app/javascript/dashboard/api/contacts.js
index bae5623a7..c39a4cf9d 100644
--- a/app/javascript/dashboard/api/contacts.js
+++ b/app/javascript/dashboard/api/contacts.js
@@ -35,8 +35,9 @@ class ContactAPI extends ApiClient {
return axios.patch(`${this.url}/${id}?include_contact_inboxes=false`, data);
}
- getConversations(contactId) {
- return axios.get(`${this.url}/${contactId}/conversations`);
+ getConversations(contactId, { inboxId } = {}) {
+ const params = inboxId ? { inbox_id: inboxId } : {};
+ return axios.get(`${this.url}/${contactId}/conversations`, { params });
}
getContactableInboxes(contactId) {
@@ -47,9 +48,10 @@ class ContactAPI extends ApiClient {
return axios.get(`${this.url}/${contactId}/labels`);
}
- initiateCall(contactId, inboxId) {
+ initiateCall(contactId, inboxId, conversationId = null) {
return axios.post(`${this.url}/${contactId}/call`, {
inbox_id: inboxId,
+ conversation_id: conversationId,
});
}
diff --git a/app/javascript/dashboard/api/helpCenter/articles.js b/app/javascript/dashboard/api/helpCenter/articles.js
index c79aa5da7..bab45bcb5 100644
--- a/app/javascript/dashboard/api/helpCenter/articles.js
+++ b/app/javascript/dashboard/api/helpCenter/articles.js
@@ -87,6 +87,13 @@ class ArticlesAPI extends PortalsAPI {
);
}
+ bulkUpdateCategory({ portalSlug, articleIds, categoryId }) {
+ return axios.patch(
+ `${this.url}/${portalSlug}/articles/bulk_actions/update_category`,
+ { ids: articleIds, category_id: categoryId }
+ );
+ }
+
bulkDelete({ portalSlug, articleIds }) {
return axios.delete(
`${this.url}/${portalSlug}/articles/bulk_actions/delete_articles`,
diff --git a/app/javascript/dashboard/api/inbox/message.js b/app/javascript/dashboard/api/inbox/message.js
index 8f294a0ee..06b85078e 100644
--- a/app/javascript/dashboard/api/inbox/message.js
+++ b/app/javascript/dashboard/api/inbox/message.js
@@ -12,6 +12,7 @@ export const buildCreatePayload = ({
bccEmails = '',
toEmails = '',
templateParams,
+ isVoiceMessage = false,
}) => {
let payload;
if (files && files.length !== 0) {
@@ -33,6 +34,9 @@ export const buildCreatePayload = ({
if (contentAttributes) {
payload.append('content_attributes', JSON.stringify(contentAttributes));
}
+ if (isVoiceMessage) {
+ payload.append('is_voice_message', true);
+ }
} else {
payload = {
content: message,
@@ -64,6 +68,7 @@ class MessageApi extends ApiClient {
bccEmails = '',
toEmails = '',
templateParams,
+ isVoiceMessage = false,
}) {
return axios({
method: 'post',
@@ -78,6 +83,7 @@ class MessageApi extends ApiClient {
bccEmails,
toEmails,
templateParams,
+ isVoiceMessage,
}),
});
}
diff --git a/app/javascript/dashboard/api/inboxes.js b/app/javascript/dashboard/api/inboxes.js
index cc564fe96..114dbb6f4 100644
--- a/app/javascript/dashboard/api/inboxes.js
+++ b/app/javascript/dashboard/api/inboxes.js
@@ -52,6 +52,14 @@ class Inboxes extends CacheEnabledApiClient {
resetSecret(inboxId) {
return axios.post(`${this.url}/${inboxId}/reset_secret`);
}
+
+ enableWhatsappCalling(inboxId) {
+ return axios.post(`${this.url}/${inboxId}/enable_whatsapp_calling`);
+ }
+
+ disableWhatsappCalling(inboxId) {
+ return axios.post(`${this.url}/${inboxId}/disable_whatsapp_calling`);
+ }
}
export default new Inboxes();
diff --git a/app/javascript/dashboard/api/onboarding.js b/app/javascript/dashboard/api/onboarding.js
new file mode 100644
index 000000000..2bc098eef
--- /dev/null
+++ b/app/javascript/dashboard/api/onboarding.js
@@ -0,0 +1,14 @@
+/* global axios */
+import ApiClient from './ApiClient';
+
+class OnboardingAPI extends ApiClient {
+ constructor() {
+ super('onboarding', { accountScoped: true });
+ }
+
+ update(data) {
+ return axios.patch(this.url, data);
+ }
+}
+
+export default new OnboardingAPI();
diff --git a/app/javascript/dashboard/api/specs/article.spec.js b/app/javascript/dashboard/api/specs/article.spec.js
index 71128682c..b40613739 100644
--- a/app/javascript/dashboard/api/specs/article.spec.js
+++ b/app/javascript/dashboard/api/specs/article.spec.js
@@ -153,4 +153,33 @@ describe('#PortalAPI', () => {
);
});
});
+ describe('API calls', () => {
+ const originalAxios = window.axios;
+ const axiosMock = {
+ post: vi.fn(() => Promise.resolve()),
+ get: vi.fn(() => Promise.resolve()),
+ patch: vi.fn(() => Promise.resolve()),
+ delete: vi.fn(() => Promise.resolve()),
+ };
+
+ beforeEach(() => {
+ window.axios = axiosMock;
+ });
+
+ afterEach(() => {
+ window.axios = originalAxios;
+ });
+
+ it('#bulkUpdateCategory', () => {
+ articlesAPI.bulkUpdateCategory({
+ portalSlug: 'room-rental',
+ articleIds: [1, 2, 3],
+ categoryId: 7,
+ });
+ expect(axiosMock.patch).toHaveBeenCalledWith(
+ '/api/v1/portals/room-rental/articles/bulk_actions/update_category',
+ { ids: [1, 2, 3], category_id: 7 }
+ );
+ });
+ });
});
diff --git a/app/javascript/dashboard/api/specs/contacts.spec.js b/app/javascript/dashboard/api/specs/contacts.spec.js
index b21aeb102..f55ecdfaa 100644
--- a/app/javascript/dashboard/api/specs/contacts.spec.js
+++ b/app/javascript/dashboard/api/specs/contacts.spec.js
@@ -41,7 +41,8 @@ describe('#ContactsAPI', () => {
it('#getConversations', () => {
contactAPI.getConversations(1);
expect(axiosMock.get).toHaveBeenCalledWith(
- '/api/v1/contacts/1/conversations'
+ '/api/v1/contacts/1/conversations',
+ { params: {} }
);
});
diff --git a/app/javascript/dashboard/api/specs/inbox/message.spec.js b/app/javascript/dashboard/api/specs/inbox/message.spec.js
index 941f5c99c..84c0b9cf2 100644
--- a/app/javascript/dashboard/api/specs/inbox/message.spec.js
+++ b/app/javascript/dashboard/api/specs/inbox/message.spec.js
@@ -83,5 +83,29 @@ describe('#ConversationAPI', () => {
template_params: undefined,
});
});
+
+ it('appends is_voice_message when isVoiceMessage is true', () => {
+ const formPayload = buildCreatePayload({
+ message: 'voice message',
+ echoId: 42,
+ isPrivate: false,
+ files: [new Blob(['audio-data'], { type: 'audio/ogg' })],
+ isVoiceMessage: true,
+ });
+ expect(formPayload).toBeInstanceOf(FormData);
+ expect(formPayload.get('is_voice_message')).toEqual('true');
+ });
+
+ it('does not append is_voice_message when isVoiceMessage is false', () => {
+ const formPayload = buildCreatePayload({
+ message: 'regular audio',
+ echoId: 43,
+ isPrivate: false,
+ files: [new Blob(['audio-data'], { type: 'audio/ogg' })],
+ isVoiceMessage: false,
+ });
+ expect(formPayload).toBeInstanceOf(FormData);
+ expect(formPayload.get('is_voice_message')).toBeNull();
+ });
});
});
diff --git a/app/javascript/dashboard/components-next/Campaigns/CampaignCard/CampaignCard.vue b/app/javascript/dashboard/components-next/Campaigns/CampaignCard/CampaignCard.vue
index 05507fc89..437933e5f 100644
--- a/app/javascript/dashboard/components-next/Campaigns/CampaignCard/CampaignCard.vue
+++ b/app/javascript/dashboard/components-next/Campaigns/CampaignCard/CampaignCard.vue
@@ -49,6 +49,7 @@ const emit = defineEmits(['edit', 'delete']);
const { t } = useI18n();
const STATUS_COMPLETED = 'completed';
+const STATUS_PROCESSING = 'processing';
const { formatMessage } = useMessageFormatter();
@@ -68,9 +69,15 @@ const campaignStatus = computed(() => {
: t('CAMPAIGN.LIVE_CHAT.CARD.STATUS.DISABLED');
}
- return props.status === STATUS_COMPLETED
- ? t('CAMPAIGN.SMS.CARD.STATUS.COMPLETED')
- : t('CAMPAIGN.SMS.CARD.STATUS.SCHEDULED');
+ if (props.status === STATUS_COMPLETED) {
+ return t('CAMPAIGN.SMS.CARD.STATUS.COMPLETED');
+ }
+
+ if (props.status === STATUS_PROCESSING) {
+ return t('CAMPAIGN.SMS.CARD.STATUS.PROCESSING');
+ }
+
+ return t('CAMPAIGN.SMS.CARD.STATUS.SCHEDULED');
});
const inboxName = computed(() => props.inbox?.name || '');
diff --git a/app/javascript/dashboard/components-next/Contacts/ContactsHeader/components/ContactMoreActions.vue b/app/javascript/dashboard/components-next/Contacts/ContactsHeader/components/ContactMoreActions.vue
index d5932535c..9deaa84f2 100644
--- a/app/javascript/dashboard/components-next/Contacts/ContactsHeader/components/ContactMoreActions.vue
+++ b/app/javascript/dashboard/components-next/Contacts/ContactsHeader/components/ContactMoreActions.vue
@@ -1,34 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/call/CallCard.vue b/app/javascript/dashboard/components-next/call/CallCard.vue
new file mode 100644
index 000000000..207129282
--- /dev/null
+++ b/app/javascript/dashboard/components-next/call/CallCard.vue
@@ -0,0 +1,242 @@
+
+
+
+
+
+
+
+
+
+
+
+ {{ statusLabel }}
+
+
+
+
+
+
+ {{ callInfo.countryFlag }}
+
+
+
+ {{ callInfo.location }}
+
+
+
+
+
+ {{ duration }}
+
+
+
+
+
+ {{ statusLabel }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ callInfo.contactName }}
+
+
+ {{ callInfo.phoneNumber }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ #{{ call.conversationId }}
+
+
+
+
+
+ {{ $t('CONVERSATION.VOICE_WIDGET.GO_TO_CONVERSATION') }}
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/call/FloatingCallWidget.vue b/app/javascript/dashboard/components-next/call/FloatingCallWidget.vue
new file mode 100644
index 000000000..86d9e979c
--- /dev/null
+++ b/app/javascript/dashboard/components-next/call/FloatingCallWidget.vue
@@ -0,0 +1,268 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/assistant/DocumentBulkActions.vue b/app/javascript/dashboard/components-next/captain/assistant/DocumentBulkActions.vue
index 378860b3e..fa4888576 100644
--- a/app/javascript/dashboard/components-next/captain/assistant/DocumentBulkActions.vue
+++ b/app/javascript/dashboard/components-next/captain/assistant/DocumentBulkActions.vue
@@ -25,7 +25,7 @@ const store = useStore();
const bulkDeleteDialog = ref(null);
const isSyncableDocument = doc =>
- !doc.pdf_document && doc.status === 'available' && !doc.sync_in_progress;
+ !doc.pdf_document && doc.status === 'available';
const syncableSelectedIds = computed(() => {
if (!props.selectedIds.size) return [];
diff --git a/app/javascript/dashboard/components-next/captain/assistant/DocumentCard.vue b/app/javascript/dashboard/components-next/captain/assistant/DocumentCard.vue
index c8a011b9a..9d6d574ec 100644
--- a/app/javascript/dashboard/components-next/captain/assistant/DocumentCard.vue
+++ b/app/javascript/dashboard/components-next/captain/assistant/DocumentCard.vue
@@ -127,7 +127,6 @@ const menuItems = computed(() => {
value: 'sync',
action: 'sync',
icon: 'i-lucide-refresh-cw',
- disabled: props.syncInProgress,
});
}
diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/customTool/AuthConfig.vue b/app/javascript/dashboard/components-next/captain/pageComponents/customTool/AuthConfig.vue
index 208a94dba..c1d54eeb6 100644
--- a/app/javascript/dashboard/components-next/captain/pageComponents/customTool/AuthConfig.vue
+++ b/app/javascript/dashboard/components-next/captain/pageComponents/customTool/AuthConfig.vue
@@ -1,5 +1,5 @@
+
+
+
+
+
+
+
+ {{ inbox.name }}
+
+
+
+
+
+
+
+ {{ inbox.name }}
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/icon/ChannelIcon.vue b/app/javascript/dashboard/components-next/icon/ChannelIcon.vue
index 68102dbd3..f680d3f03 100644
--- a/app/javascript/dashboard/components-next/icon/ChannelIcon.vue
+++ b/app/javascript/dashboard/components-next/icon/ChannelIcon.vue
@@ -1,6 +1,7 @@
-
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/icon/FileIcon.vue b/app/javascript/dashboard/components-next/icon/FileIcon.vue
index 8dd9e7ce1..66a971bc6 100644
--- a/app/javascript/dashboard/components-next/icon/FileIcon.vue
+++ b/app/javascript/dashboard/components-next/icon/FileIcon.vue
@@ -18,6 +18,7 @@ const fileTypeIcon = computed(() => {
json: 'i-woot-file-txt',
odt: 'i-woot-file-doc',
pdf: 'i-woot-file-pdf',
+ pfx: 'i-woot-file-pfx',
ppt: 'i-woot-file-ppt',
pptx: 'i-woot-file-ppt',
rar: 'i-woot-file-zip',
@@ -26,6 +27,7 @@ const fileTypeIcon = computed(() => {
txt: 'i-woot-file-txt',
xls: 'i-woot-file-xls',
xlsx: 'i-woot-file-xls',
+ xml: 'i-woot-file-txt',
zip: 'i-woot-file-zip',
};
diff --git a/app/javascript/dashboard/components-next/icon/provider.js b/app/javascript/dashboard/components-next/icon/provider.js
index d47d600d7..e86581a15 100644
--- a/app/javascript/dashboard/components-next/icon/provider.js
+++ b/app/javascript/dashboard/components-next/icon/provider.js
@@ -1,46 +1,76 @@
+import { INBOX_TYPES, TWILIO_CHANNEL_MEDIUM } from 'dashboard/helper/inbox';
import { computed } from 'vue';
-import { isVoiceCallEnabled } from 'dashboard/helper/inbox';
+
+const channelTypeIconMap = {
+ 'Channel::Api': 'i-woot-api',
+ 'Channel::Email': 'i-woot-mail',
+ 'Channel::FacebookPage': 'i-woot-messenger',
+ 'Channel::Line': 'i-woot-line',
+ 'Channel::Sms': 'i-woot-sms',
+ 'Channel::Telegram': 'i-woot-telegram',
+ 'Channel::TwilioSms': 'i-woot-sms',
+ 'Channel::TwitterProfile': 'i-woot-x',
+ 'Channel::WebWidget': 'i-woot-website',
+ 'Channel::Whatsapp': 'i-woot-whatsapp',
+ 'Channel::Instagram': 'i-woot-instagram',
+ 'Channel::Tiktok': 'i-woot-tiktok',
+ 'Channel::GooglePlay': 'i-ri-google-play-line'
+};
+
+const providerIconMap = {
+ microsoft: 'i-woot-outlook',
+ google: 'i-woot-gmail',
+};
+
+// Full-color brand icons. Most come from the `logos` Iconify set; Instagram,
+// Outlook and Line use custom `woot` glyphs since the `logos` versions are
+// monochrome or missing. Channels not listed here have no brand variant and
+// callers should fall back to the monochrome glyph via useChannelIcon.
+const channelTypeBrandIconMap = {
+ 'Channel::FacebookPage': 'i-logos-messenger',
+ 'Channel::Line': 'i-woot-line-color',
+ 'Channel::Telegram': 'i-logos-telegram',
+ 'Channel::Whatsapp': 'i-logos-whatsapp-icon',
+ 'Channel::Instagram': 'i-woot-instagram-color',
+ 'Channel::Tiktok': 'i-logos-tiktok-icon',
+};
+
+const providerBrandIconMap = {
+ microsoft: 'i-woot-outlook-color',
+ google: 'i-logos-google-gmail',
+};
+
+const resolveInbox = inbox => inbox?.value ?? inbox;
export function useChannelIcon(inbox) {
- const channelTypeIconMap = {
- 'Channel::Api': 'i-woot-api',
- 'Channel::Email': 'i-woot-mail',
- 'Channel::FacebookPage': 'i-woot-messenger',
- 'Channel::Line': 'i-woot-line',
- 'Channel::Sms': 'i-woot-sms',
- 'Channel::Telegram': 'i-woot-telegram',
- 'Channel::TwilioSms': 'i-woot-sms',
- 'Channel::TwitterProfile': 'i-woot-x',
- 'Channel::WebWidget': 'i-woot-website',
- 'Channel::Whatsapp': 'i-woot-whatsapp',
- 'Channel::Instagram': 'i-woot-instagram',
- 'Channel::Tiktok': 'i-woot-tiktok',
- 'Channel::GooglePlay': 'i-ri-google-play-line',
- };
-
- const providerIconMap = {
- microsoft: 'i-woot-outlook',
- google: 'i-woot-gmail',
- };
-
const channelIcon = computed(() => {
- const inboxDetails = inbox.value || inbox;
+ const inboxDetails = resolveInbox(inbox);
const type = inboxDetails.channel_type;
let icon = channelTypeIconMap[type];
- if (type === 'Channel::Email' && inboxDetails.provider) {
+ if (type === INBOX_TYPES.EMAIL && inboxDetails.provider) {
if (Object.keys(providerIconMap).includes(inboxDetails.provider)) {
icon = providerIconMap[inboxDetails.provider];
}
}
// Special case for Twilio whatsapp
- if (type === 'Channel::TwilioSms' && inboxDetails.medium === 'whatsapp') {
+ if (
+ type === INBOX_TYPES.TWILIO &&
+ inboxDetails.medium === TWILIO_CHANNEL_MEDIUM.WHATSAPP
+ ) {
icon = 'i-woot-whatsapp';
}
- // Special case for voice-enabled inboxes (Twilio, WhatsApp, etc.)
- if (isVoiceCallEnabled(inboxDetails)) {
+ // Native Twilio voice inbox: a TwilioSms with voice enabled (and no WhatsApp medium)
+ // is presented as a Voice channel, so show the phone icon.
+ const voiceEnabled =
+ inboxDetails.voice_enabled || inboxDetails.voiceEnabled;
+ if (
+ type === INBOX_TYPES.TWILIO &&
+ voiceEnabled &&
+ inboxDetails.medium !== TWILIO_CHANNEL_MEDIUM.WHATSAPP
+ ) {
icon = 'i-woot-voice';
}
@@ -49,3 +79,26 @@ export function useChannelIcon(inbox) {
return channelIcon;
}
+
+export function useChannelBrandIcon(inbox) {
+ return computed(() => {
+ const inboxDetails = resolveInbox(inbox);
+ const type = inboxDetails.channel_type;
+ let icon = channelTypeBrandIconMap[type];
+
+ if (type === INBOX_TYPES.EMAIL && inboxDetails.provider) {
+ if (Object.keys(providerBrandIconMap).includes(inboxDetails.provider)) {
+ icon = providerBrandIconMap[inboxDetails.provider];
+ }
+ }
+
+ if (
+ type === INBOX_TYPES.TWILIO &&
+ inboxDetails.medium === TWILIO_CHANNEL_MEDIUM.WHATSAPP
+ ) {
+ icon = channelTypeBrandIconMap['Channel::Whatsapp'];
+ }
+
+ return icon ?? null;
+ });
+}
diff --git a/app/javascript/dashboard/components-next/message/Message.vue b/app/javascript/dashboard/components-next/message/Message.vue
index d738bfcc6..0ef64eedf 100644
--- a/app/javascript/dashboard/components-next/message/Message.vue
+++ b/app/javascript/dashboard/components-next/message/Message.vue
@@ -31,6 +31,7 @@ import FileBubble from './bubbles/File.vue';
import AudioBubble from './bubbles/Audio.vue';
import VideoBubble from './bubbles/Video.vue';
import EmbedBubble from './bubbles/Embed.vue';
+import FallbackBubble from './bubbles/Fallback.vue';
import InstagramStoryBubble from './bubbles/InstagramStory.vue';
import EmailBubble from './bubbles/Email/Index.vue';
import UnsupportedBubble from './bubbles/Unsupported.vue';
@@ -328,6 +329,8 @@ const componentToRender = computed(() => {
if (Array.isArray(props.attachments) && props.attachments.length === 1) {
const fileType = props.attachments[0].fileType;
+ if (fileType === ATTACHMENT_TYPES.FALLBACK) return FallbackBubble;
+
if (!props.content) {
if (fileType === ATTACHMENT_TYPES.IMAGE) return ImageBubble;
if (fileType === ATTACHMENT_TYPES.FILE) return FileBubble;
@@ -554,11 +557,10 @@ provideMessageContext({
diff --git a/app/javascript/dashboard/components-next/message/MessageList.vue b/app/javascript/dashboard/components-next/message/MessageList.vue
index 7691ae246..a579208e8 100644
--- a/app/javascript/dashboard/components-next/message/MessageList.vue
+++ b/app/javascript/dashboard/components-next/message/MessageList.vue
@@ -1,5 +1,5 @@
+
+
+
+
+
+ {{ title }}
+
+
+ {{ title }}
+
+
+
diff --git a/app/javascript/dashboard/components-next/message/bubbles/Unsupported.vue b/app/javascript/dashboard/components-next/message/bubbles/Unsupported.vue
index be67f85ca..5f6544738 100644
--- a/app/javascript/dashboard/components-next/message/bubbles/Unsupported.vue
+++ b/app/javascript/dashboard/components-next/message/bubbles/Unsupported.vue
@@ -6,9 +6,12 @@ import BaseBubble from './Base.vue';
const { inboxId } = useMessageContext();
-const { isAFacebookInbox, isAnInstagramChannel, isATiktokChannel } = useInbox(
- inboxId.value
-);
+const {
+ isAFacebookInbox,
+ isAnInstagramChannel,
+ isATiktokChannel,
+ isAWhatsAppChannel,
+} = useInbox(inboxId.value);
const unsupportedMessageKey = computed(() => {
if (isAFacebookInbox.value)
@@ -16,6 +19,8 @@ const unsupportedMessageKey = computed(() => {
if (isAnInstagramChannel.value)
return 'CONVERSATION.UNSUPPORTED_MESSAGE_INSTAGRAM';
if (isATiktokChannel.value) return 'CONVERSATION.UNSUPPORTED_MESSAGE_TIKTOK';
+ if (isAWhatsAppChannel.value)
+ return 'CONVERSATION.UNSUPPORTED_MESSAGE_WHATSAPP';
return 'CONVERSATION.UNSUPPORTED_MESSAGE';
});
diff --git a/app/javascript/dashboard/components-next/message/bubbles/VoiceCall.vue b/app/javascript/dashboard/components-next/message/bubbles/VoiceCall.vue
index a0f950ad4..37a80e8fc 100644
--- a/app/javascript/dashboard/components-next/message/bubbles/VoiceCall.vue
+++ b/app/javascript/dashboard/components-next/message/bubbles/VoiceCall.vue
@@ -2,14 +2,27 @@
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useStore } from 'vuex';
+import { useMapGetter } from 'dashboard/composables/store';
import { useMessageContext } from '../provider.js';
-import { VOICE_CALL_STATUS } from '../constants';
-import { useCallSession } from 'dashboard/composables/useCallSession';
+import {
+ VOICE_CALL_STATUS,
+ VOICE_CALL_DIRECTION,
+ VOICE_CALL_OUTBOUND_INIT_STATUS,
+ VOICE_CALL_END_REASON,
+ MESSAGE_TYPES,
+ ATTACHMENT_TYPES,
+} from '../constants';
+import { useCallActions } from 'dashboard/composables/useCallSession';
+import { useWhatsappCallSession } from 'dashboard/composables/useWhatsappCallSession';
+import { useCallsStore } from 'dashboard/stores/calls';
+import { VOICE_CALL_PROVIDERS } from 'dashboard/helper/inbox';
import { formatDuration } from 'shared/helpers/timeHelper';
+import { useAlert } from 'dashboard/composables';
import Icon from 'dashboard/components-next/icon/Icon.vue';
import BaseBubble from 'next/message/bubbles/Base.vue';
import AudioChip from 'next/message/chips/Audio.vue';
+import NextButton from 'dashboard/components-next/button/Button.vue';
const LABEL_MAP = {
[VOICE_CALL_STATUS.IN_PROGRESS]: 'CONVERSATION.VOICE_CALL.CALL_IN_PROGRESS',
@@ -17,39 +30,67 @@ const LABEL_MAP = {
};
const ICON_MAP = {
- [VOICE_CALL_STATUS.IN_PROGRESS]: 'i-ph-phone-call',
- [VOICE_CALL_STATUS.NO_ANSWER]: 'i-ph-phone-x',
- [VOICE_CALL_STATUS.FAILED]: 'i-ph-phone-x',
-};
-
-const BG_COLOR_MAP = {
- [VOICE_CALL_STATUS.IN_PROGRESS]: 'bg-n-teal-9',
- [VOICE_CALL_STATUS.RINGING]: 'bg-n-teal-9 animate-pulse',
- [VOICE_CALL_STATUS.COMPLETED]: 'bg-n-slate-11',
- [VOICE_CALL_STATUS.NO_ANSWER]: 'bg-n-ruby-9',
- [VOICE_CALL_STATUS.FAILED]: 'bg-n-ruby-9',
+ [VOICE_CALL_STATUS.IN_PROGRESS]: 'i-ph-phone-call-bold',
+ [VOICE_CALL_STATUS.COMPLETED]: 'i-ph-phone-bold',
+ [VOICE_CALL_STATUS.NO_ANSWER]: 'i-ph-phone-x-bold',
+ [VOICE_CALL_STATUS.FAILED]: 'i-ph-phone-x-bold',
};
const { t } = useI18n();
const store = useStore();
-const { call, conversationId, currentUserId, inboxId } = useMessageContext();
+const {
+ call,
+ attachments,
+ contentAttributes,
+ conversationId,
+ currentUserId,
+ inboxId,
+ sender,
+ messageType,
+} = useMessageContext();
const { joinCall, endCall, activeCall, hasActiveCall, isJoining } =
- useCallSession();
+ useCallActions();
+const whatsappCallSession = useWhatsappCallSession();
+const callsStore = useCallsStore();
+const contactsUiFlags = useMapGetter('contacts/getUIFlags');
+const isInitiatingCall = computed(
+ () => contactsUiFlags.value?.isInitiatingCall || false
+);
const status = computed(() => call.value?.status);
-const isOutbound = computed(() => call.value?.direction === 'outgoing');
+// Server-side call records use `outgoing`/`incoming`, while the Pinia store
+// and a few API hops normalise to `outbound`/`inbound`. Accept either so the
+// bubble label matches the message orientation no matter the source.
+const isOutbound = computed(() => {
+ const dir = call.value?.direction;
+ if (
+ dir === VOICE_CALL_DIRECTION.OUTGOING ||
+ dir === VOICE_CALL_DIRECTION.OUTBOUND
+ )
+ return true;
+ if (
+ dir === VOICE_CALL_DIRECTION.INCOMING ||
+ dir === VOICE_CALL_DIRECTION.INBOUND
+ )
+ return false;
+ // Fall back to the message orientation: agent-authored messages sit on the
+ // right (outbound) and contact-authored ones on the left.
+ return messageType.value === MESSAGE_TYPES.OUTGOING;
+});
+const isWhatsapp = computed(
+ () => call.value?.provider === VOICE_CALL_PROVIDERS.WHATSAPP
+);
const isFailed = computed(() =>
[VOICE_CALL_STATUS.NO_ANSWER, VOICE_CALL_STATUS.FAILED].includes(status.value)
);
-const acceptedByAgentId = computed(() => call.value?.acceptedByAgentId);
-const didCurrentUserAnswer = computed(
+const isMissedInbound = computed(() => isFailed.value && !isOutbound.value);
+const endReason = computed(() => call.value?.endReason);
+const wasDeclinedByAgent = computed(
() =>
- !!acceptedByAgentId.value && acceptedByAgentId.value === currentUserId.value
+ isMissedInbound.value &&
+ endReason.value === VOICE_CALL_END_REASON.AGENT_REJECTED
);
-// Pickup auto-assigns the conversation, so the assignee is a safe display proxy
-// for the answerer when the Call payload lacks accepted_by_agent_id (e.g.,
-// Twilio's call-status webhook flipped the call to in-progress before the
-// participant-join webhook claimed it).
+const acceptedByAgentId = computed(() => call.value?.acceptedByAgentId);
const conversationAssignee = computed(() => {
const conversation = store.getters.getConversationById?.(
conversationId?.value
@@ -66,67 +107,107 @@ const displayAgentName = computed(() => {
return conversationAssignee.value?.name || null;
});
+const audioAttachment = computed(() =>
+ (attachments?.value || []).find(a => a.fileType === ATTACHMENT_TYPES.AUDIO)
+);
+
+const durationSeconds = computed(() => {
+ const fromCall = call.value?.durationSeconds || call.value?.duration_seconds;
+ if (fromCall != null) return fromCall;
+ const data = contentAttributes?.value?.data;
+ return data?.durationSeconds || data?.duration_seconds;
+});
+
+const formattedDuration = computed(() => formatDuration(durationSeconds.value));
+
+// Agent who handled the call (initiator on outbound, answerer on inbound), taken
+// strictly from the persisted accept fields — never the conversation's current
+// assignee, which would mis-attribute a historical call after a reassignment.
+const handlerName = computed(() => {
+ if (call.value?.acceptedByAgentName) return call.value.acceptedByAgentName;
+ if (!acceptedByAgentId.value) return null;
+ const agent = store.getters['agents/getAgentById'](acceptedByAgentId.value);
+ return agent?.available_name || agent?.name || null;
+});
+
+const handledBy = computed(() =>
+ handlerName.value
+ ? t('CONVERSATION.VOICE_CALL.HANDLED_BY', { agentName: handlerName.value })
+ : null
+);
+
const labelKey = computed(() => {
if (LABEL_MAP[status.value]) return LABEL_MAP[status.value];
- if (status.value === VOICE_CALL_STATUS.RINGING) {
+ if (isFailed.value) {
return isOutbound.value
- ? 'CONVERSATION.VOICE_CALL.OUTGOING_CALL'
- : 'CONVERSATION.VOICE_CALL.INCOMING_CALL';
+ ? 'CONVERSATION.VOICE_CALL.NO_ANSWER_OUTBOUND_LABEL'
+ : 'CONVERSATION.VOICE_CALL.MISSED_CALL';
}
- return isFailed.value
- ? 'CONVERSATION.VOICE_CALL.MISSED_CALL'
+ // RINGING or an as-yet-unknown/initial status: orient purely by direction so an
+ // outbound call never falls through to the "Incoming call" label.
+ return isOutbound.value
+ ? 'CONVERSATION.VOICE_CALL.OUTGOING_CALL'
: 'CONVERSATION.VOICE_CALL.INCOMING_CALL';
});
-const formattedDuration = computed(() =>
- formatDuration(call.value?.durationSeconds)
-);
-
const subtext = computed(() => {
- if (status.value === VOICE_CALL_STATUS.RINGING) {
- return t('CONVERSATION.VOICE_CALL.NOT_ANSWERED_YET');
- }
+ // Completed: "Handled by {agent} · 0:42" (drops either part when absent).
if (status.value === VOICE_CALL_STATUS.COMPLETED) {
- return formattedDuration.value;
+ return [handledBy.value, formattedDuration.value]
+ .filter(Boolean)
+ .join(' · ');
}
if (status.value === VOICE_CALL_STATUS.IN_PROGRESS) {
- if (isOutbound.value) return t('CONVERSATION.VOICE_CALL.THEY_ANSWERED');
- if (didCurrentUserAnswer.value) {
- return t('CONVERSATION.VOICE_CALL.YOU_ANSWERED');
+ return handledBy.value;
+ }
+ if (isFailed.value) {
+ // Missed/failed calls have no handler, so keep the reason rather than "Handled by".
+ if (isOutbound.value) {
+ return t('CONVERSATION.VOICE_CALL.NO_ANSWER_OUTBOUND_SUBTEXT');
}
- if (displayAgentName.value) {
- return t('CONVERSATION.VOICE_CALL.AGENT_ANSWERED', {
+ if (wasDeclinedByAgent.value && displayAgentName.value) {
+ return t('CONVERSATION.VOICE_CALL.MISSED_CALL_DECLINED_BY', {
agentName: displayAgentName.value,
});
}
- return t('CONVERSATION.VOICE_CALL.THEY_ANSWERED');
+ return t('CONVERSATION.VOICE_CALL.MISSED_CALL_INBOUND_SUBTEXT');
}
- return isFailed.value
- ? t('CONVERSATION.VOICE_CALL.NO_ANSWER')
- : t('CONVERSATION.VOICE_CALL.NOT_ANSWERED_YET');
+ // RINGING or an as-yet-unknown/initial status.
+ if (isOutbound.value) {
+ return handledBy.value || t('CONVERSATION.VOICE_CALL.CALLING');
+ }
+ return t('CONVERSATION.VOICE_CALL.NOT_ANSWERED_YET');
});
const iconName = computed(() => {
if (ICON_MAP[status.value]) return ICON_MAP[status.value];
- return isOutbound.value ? 'i-ph-phone-outgoing' : 'i-ph-phone-incoming';
+ return isOutbound.value
+ ? 'i-ph-phone-outgoing-bold'
+ : 'i-ph-phone-incoming-bold';
});
-const bgColor = computed(() => BG_COLOR_MAP[status.value] || 'bg-n-teal-9');
+// Subtle icon container — matches the design's tonal swatch over the bubble bg.
+// Status drives the accent: teal for live, ruby for missed, neutral otherwise.
+const iconContainerClass = computed(() => {
+ if (status.value === VOICE_CALL_STATUS.IN_PROGRESS) {
+ return 'bg-n-teal-3 text-n-teal-11';
+ }
+ if (status.value === VOICE_CALL_STATUS.RINGING) {
+ return 'bg-n-teal-3 text-n-teal-11';
+ }
+ if (isMissedInbound.value) {
+ return 'bg-n-alpha-2 text-n-ruby-9';
+ }
+ return 'bg-n-alpha-2 text-n-slate-12';
+});
const callSid = computed(() => call.value?.providerCallId);
-// Show "Join call" when the call is still ringing, no agent has claimed it,
-// and the conversation is unassigned or assigned to the current user. Mirrors
-// the eligibility used by FloatingCallWidget so the bubble can act as a
-// recovery affordance after a refresh or missed widget.
const canJoinCall = computed(() => {
if (status.value !== VOICE_CALL_STATUS.RINGING) return false;
if (isOutbound.value) return false;
if (acceptedByAgentId.value) return false;
if (!callSid.value || !inboxId.value || !conversationId.value) return false;
- // Suppress the button once this call is the local active session — the
- // message status webhook may lag behind, so we can't rely on `status` alone
- // to hide it after a successful join from this client.
if (hasActiveCall.value && activeCall.value?.callSid === callSid.value)
return false;
const assignee = conversationAssignee.value;
@@ -135,11 +216,12 @@ const canJoinCall = computed(() => {
});
const recordingAttachment = computed(() => {
+ if (audioAttachment.value) return audioAttachment.value;
const url = call.value?.recordingUrl;
if (!url) return null;
return {
dataUrl: url,
- fileType: 'audio',
+ fileType: ATTACHMENT_TYPES.AUDIO,
extension: 'wav',
transcribedText: call.value?.transcript || '',
};
@@ -162,48 +244,117 @@ const handleJoinCall = async () => {
callSid: callSid.value,
});
};
+
+const canCallBack = computed(
+ () =>
+ isMissedInbound.value &&
+ !!inboxId.value &&
+ !!conversationId.value &&
+ !hasActiveCall.value &&
+ !callsStore.hasIncomingCall
+);
+
+const handleCallBack = async () => {
+ if (!canCallBack.value || isInitiatingCall.value) return;
+ try {
+ if (isWhatsapp.value) {
+ const response = await whatsappCallSession.initiateOutboundCall(
+ conversationId.value
+ );
+ if (response?.status === VOICE_CALL_OUTBOUND_INIT_STATUS.LOCKED) return;
+ // Permission template path returns no call id — show banner, no widget yet.
+ if (!response?.id) {
+ useAlert(
+ response?.status ===
+ VOICE_CALL_OUTBOUND_INIT_STATUS.PERMISSION_PENDING
+ ? t('CONVERSATION.HEADER.WHATSAPP_CALL_PERMISSION_PENDING')
+ : t('CONVERSATION.HEADER.WHATSAPP_CALL_PERMISSION_REQUESTED')
+ );
+ return;
+ }
+ callsStore.addCall({
+ callSid: response.call_id,
+ callId: response.id,
+ conversationId: conversationId.value,
+ inboxId: inboxId.value,
+ callDirection: VOICE_CALL_DIRECTION.OUTBOUND,
+ provider: VOICE_CALL_PROVIDERS.WHATSAPP,
+ });
+ return;
+ }
+ const response = await store.dispatch('contacts/initiateCall', {
+ contactId: sender.value?.id,
+ inboxId: inboxId.value,
+ conversationId: conversationId.value,
+ });
+ callsStore.addCall({
+ callSid: response?.call_sid,
+ conversationId: response?.conversation_id ?? conversationId.value,
+ inboxId: inboxId.value,
+ callDirection: VOICE_CALL_DIRECTION.OUTBOUND,
+ });
+ } catch (error) {
+ useAlert(error?.message || t('CONTACT_PANEL.CALL_FAILED'));
+ }
+};
-
-
-
+
+
+
+
-
+
-
-
-
+
+
{{ $t(labelKey) }}
-
+
{{ subtext }}
-
-
+
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/message/chips/AttachmentChips.vue b/app/javascript/dashboard/components-next/message/chips/AttachmentChips.vue
index 4fd1d6428..0a3299d46 100644
--- a/app/javascript/dashboard/components-next/message/chips/AttachmentChips.vue
+++ b/app/javascript/dashboard/components-next/message/chips/AttachmentChips.vue
@@ -1,5 +1,5 @@
-
-
-
-
-
-
-
-
- {{ getCallInfo(call).contactName }}
-
-
- {{ getCallInfo(call).inboxName }}
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ getCallInfo(activeCall || incomingCalls[0]).contactName }}
-
-
- {{ formattedCallDuration }}
-
-
- {{
- incomingCalls[0]?.callDirection === 'outbound'
- ? $t('CONVERSATION.VOICE_WIDGET.OUTGOING_CALL')
- : $t('CONVERSATION.VOICE_WIDGET.INCOMING_CALL')
- }}
-
-
-
-
-
-
-
-
-
diff --git a/app/javascript/dashboard/components/widgets/WootWriter/AudioRecorder.vue b/app/javascript/dashboard/components/widgets/WootWriter/AudioRecorder.vue
index 924e72c53..50bbc6ba4 100644
--- a/app/javascript/dashboard/components/widgets/WootWriter/AudioRecorder.vue
+++ b/app/javascript/dashboard/components/widgets/WootWriter/AudioRecorder.vue
@@ -1,10 +1,10 @@
+
+
+
+
+
diff --git a/app/javascript/dashboard/components/widgets/conversation/ConversationHeader.vue b/app/javascript/dashboard/components/widgets/conversation/ConversationHeader.vue
index 4a46afe73..b48930401 100644
--- a/app/javascript/dashboard/components/widgets/conversation/ConversationHeader.vue
+++ b/app/javascript/dashboard/components/widgets/conversation/ConversationHeader.vue
@@ -8,13 +8,14 @@ import InboxName from '../InboxName.vue';
import MoreActions from './MoreActions.vue';
import Avatar from 'next/avatar/Avatar.vue';
import SLACardLabel from './components/SLACardLabel.vue';
+import ConversationCallButton from './ConversationCallButton.vue';
import wootConstants from 'dashboard/constants/globals';
import { conversationListPageURL } from 'dashboard/helper/URLHelper';
import { snoozedReopenTime } from 'dashboard/helper/snoozeHelpers';
import { useInbox } from 'dashboard/composables/useInbox';
+import { useAlert } from 'dashboard/composables';
import { useI18n } from 'vue-i18n';
import { copyTextToClipboard } from 'shared/helpers/clipboard';
-import { useAlert } from 'dashboard/composables';
const props = defineProps({
chat: {
@@ -172,6 +173,7 @@ const copyConversationId = async () => {
:parent-width="width"
class="hidden md:flex"
/>
+
diff --git a/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue b/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue
index ee53a0858..951d53d34 100644
--- a/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue
+++ b/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue
@@ -179,6 +179,21 @@ export default {
}
return true;
},
+ hasMeaningfulEditorContent() {
+ const body = this.message || '';
+ // Only strip the signature when it's actually being auto-appended.
+ // If the toggle is off, the agent's text might happen to match their
+ // saved signature and we'd incorrectly treat it as empty.
+ const shouldStripSignature =
+ !this.isPrivate && this.sendWithSignature && !!this.messageSignature;
+ if (!shouldStripSignature) return !!body.trim();
+ const stripped = removeSignature(
+ body,
+ this.messageSignature,
+ getEffectiveChannelType(this.channelType, this.inbox?.medium || '')
+ );
+ return !!stripped.trim();
+ },
isReplyRestricted() {
return (
!this.currentChat?.can_reply &&
@@ -363,7 +378,10 @@ export default {
return `draft-${this.conversationIdByRoute}-${this.replyType}`;
},
audioRecordFormat() {
- if (this.isAWhatsAppChannel || this.isATelegramChannel) {
+ if (this.isAWhatsAppChannel) {
+ return AUDIO_FORMATS.OGG;
+ }
+ if (this.isATelegramChannel) {
return AUDIO_FORMATS.MP3;
}
if (this.isAPIInbox) {
@@ -996,14 +1014,18 @@ export default {
onFinishRecorder(file) {
this.recordingAudioState = 'stopped';
this.hasRecordedAudio = true;
- // Added a new key isRecordedAudio to the file to find it's and recorded audio
+ // Added a new key isVoiceMessage to the file to identify recorded audio
// Because to filter and show only non recorded audio and other attachments
const autoRecordedFile = {
...file,
- isRecordedAudio: true,
+ isVoiceMessage: true,
};
return file && this.onFileUpload(autoRecordedFile);
},
+ onRecordError() {
+ this.toggleAudioRecorder();
+ useAlert(this.$t('CONVERSATION.REPLYBOX.AUDIO_CONVERSION_FAILED'));
+ },
toggleTyping(status) {
const conversationId = this.currentChat.id;
const isPrivate = this.isPrivate;
@@ -1030,7 +1052,7 @@ export default {
isPrivate: this.isPrivate,
thumb: reader.result,
blobSignedId: blob ? blob.signed_id : undefined,
- isRecordedAudio: file?.isRecordedAudio || false,
+ isVoiceMessage: file?.isVoiceMessage || false,
});
};
},
@@ -1066,6 +1088,7 @@ export default {
private: false,
message: caption,
sender: this.sender,
+ isVoiceMessage: attachment.isVoiceMessage || false,
};
attachmentPayload = this.setReplyToInPayload(attachmentPayload);
@@ -1115,6 +1138,9 @@ export default {
this.attachedFiles.forEach(attachment => {
if (this.globalConfig.directUploadsEnabled) {
messagePayload.files.push(attachment.blobSignedId);
+ if (attachment.isVoiceMessage) {
+ messagePayload.isVoiceMessage = true;
+ }
} else {
messagePayload.files.push(attachment.resource.file);
}
@@ -1203,7 +1229,7 @@ export default {
this.hasRecordedAudio = false;
// Only clear the recorded audio when we click toggle button.
this.attachedFiles = this.attachedFiles.filter(
- file => !file?.isRecordedAudio
+ file => !file?.isVoiceMessage
);
},
toggleEditorSize() {
@@ -1234,6 +1260,7 @@ export default {
:is-message-length-reaching-threshold="isMessageLengthReachingThreshold"
:characters-remaining="charactersRemaining"
:editor-content="message"
+ :has-content="hasMeaningfulEditorContent"
@set-reply-mode="setReplyMode"
@toggle-editor-size="toggleEditorSize"
@toggle-copilot="copilot.toggleEditor"
@@ -1280,6 +1307,7 @@ export default {
:audio-record-format="audioRecordFormat"
@recorder-progress-changed="onRecordProgressChanged"
@finish-record="onFinishRecorder"
+ @record-error="onRecordError"
@play="recordingAudioState = 'playing'"
@pause="recordingAudioState = 'paused'"
/>
diff --git a/app/javascript/dashboard/components/widgets/conversation/conversationBulkActions/BulkLabelActions.vue b/app/javascript/dashboard/components/widgets/conversation/conversationBulkActions/BulkLabelActions.vue
index bbbf30090..e46f45da5 100644
--- a/app/javascript/dashboard/components/widgets/conversation/conversationBulkActions/BulkLabelActions.vue
+++ b/app/javascript/dashboard/components/widgets/conversation/conversationBulkActions/BulkLabelActions.vue
@@ -14,6 +14,11 @@ const props = defineProps({
type: String,
default: 'conversation',
},
+ action: {
+ type: String,
+ default: 'assign',
+ validator: value => ['assign', 'remove'].includes(value),
+ },
isLoading: {
type: Boolean,
default: false,
@@ -22,9 +27,13 @@ const props = defineProps({
type: Boolean,
default: false,
},
+ appliedLabels: {
+ type: Array,
+ default: null,
+ },
});
-const emit = defineEmits(['assign']);
+const emit = defineEmits(['assign', 'remove']);
const { t } = useI18n();
@@ -35,17 +44,43 @@ const [showDropdown, toggleDropdown] = useToggle(false);
const selectedLabels = ref([]);
const isTypeContact = computed(() => props.type === 'contact');
+const isRemoveAction = computed(() => props.action === 'remove');
-const buttonLabel = computed(() =>
- props.type === 'contact' ? t('CONTACTS_BULK_ACTIONS.ASSIGN_LABELS') : ''
+const buttonLabel = computed(() => {
+ if (!isTypeContact.value) return '';
+
+ return isRemoveAction.value
+ ? t('CONTACTS_BULK_ACTIONS.REMOVE_LABELS')
+ : t('CONTACTS_BULK_ACTIONS.ASSIGN_LABELS');
+});
+
+const tooltipLabel = computed(() =>
+ isRemoveAction.value
+ ? t('BULK_ACTION.LABELS.REMOVE_LABELS')
+ : t('BULK_ACTION.LABELS.ASSIGN_LABELS')
+);
+
+const confirmLabel = computed(() =>
+ isRemoveAction.value
+ ? t('BULK_ACTION.LABELS.REMOVE_SELECTED_LABELS')
+ : t('BULK_ACTION.LABELS.ASSIGN_SELECTED_LABELS')
);
const isLabelSelected = labelTitle => {
return selectedLabels.value.includes(labelTitle);
};
+const visibleLabels = computed(() => {
+ if (!isRemoveAction.value || props.appliedLabels === null) {
+ return labels.value;
+ }
+
+ const applied = new Set(props.appliedLabels);
+ return labels.value.filter(label => applied.has(label.title));
+});
+
const labelMenuItems = computed(() => {
- return labels.value.map(label => ({
+ return visibleLabels.value.map(label => ({
action: 'select',
value: label.title,
label: label.title,
@@ -64,9 +99,13 @@ const toggleLabelSelection = labelTitle => {
}
};
-const handleAssign = () => {
+const handleApply = () => {
if (selectedLabels.value.length > 0) {
- emit('assign', selectedLabels.value);
+ if (isRemoveAction.value) {
+ emit('remove', selectedLabels.value);
+ } else {
+ emit('assign', selectedLabels.value);
+ }
toggleDropdown(false);
selectedLabels.value = [];
}
@@ -81,9 +120,9 @@ const handleDismiss = () => {
{
diff --git a/app/javascript/dashboard/components/widgets/conversation/conversationBulkActions/Index.vue b/app/javascript/dashboard/components/widgets/conversation/conversationBulkActions/Index.vue
index 145e89bde..eef70cf02 100644
--- a/app/javascript/dashboard/components/widgets/conversation/conversationBulkActions/Index.vue
+++ b/app/javascript/dashboard/components/widgets/conversation/conversationBulkActions/Index.vue
@@ -1,5 +1,6 @@
@@ -103,6 +108,13 @@ const handleAssignLabels = labels => {
:disabled="!selectedCount"
@assign="handleAssignLabels"
/>
+
diff --git a/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactInfo.vue b/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactInfo.vue
index 41c5854e0..a27b308b0 100644
--- a/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactInfo.vue
+++ b/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactInfo.vue
@@ -56,7 +56,10 @@ export default {
};
},
computed: {
- ...mapGetters({ uiFlags: 'contacts/getUIFlags' }),
+ ...mapGetters({
+ uiFlags: 'contacts/getUIFlags',
+ currentChat: 'getSelectedChat',
+ }),
contactProfileLink() {
return `/app/accounts/${this.$route.params.accountId}/contacts/${this.contact.id}`;
},
@@ -305,11 +308,12 @@ export default {
{
websiteInput.value?.blur();
};
+const normalizeWebsiteUrl = raw => {
+ const trimmed = (raw || '').trim();
+ if (!trimmed) return '';
+ return /^https?:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`;
+};
+
const handleSubmit = async () => {
// Block submit while enrichment is still running so users can't bypass
// the form with empty values — the controller would otherwise clear
@@ -211,9 +217,27 @@ const handleSubmit = async () => {
return;
}
+ // Detect which enrichable fields the user actually edited *before*
+ // normalizing — otherwise an untouched auto-filled domain
+ // (acme.com -> https://acme.com) compares unequal against the raw snapshot
+ // and gets falsely reported as changed, skewing onboarding telemetry.
+ const init = initialValues.value;
+ const enrichableFields = {
+ website: website.value,
+ company_size: companySize.value,
+ industry: industry.value,
+ };
+ const fieldsChanged = Object.entries(enrichableFields)
+ .filter(([key, val]) => val !== init[key])
+ .map(([key]) => key);
+
+ // Persist with a scheme so downstream consumers (Firecrawl, portal
+ // homepage_link) get a fully-qualified URL regardless of what the user typed.
+ website.value = normalizeWebsiteUrl(website.value);
+
isSubmitting.value = true;
try {
- await updateAccount({
+ await finishOnboarding({
name: accountName.value,
locale: locale.value,
website: website.value,
@@ -224,20 +248,11 @@ const handleSubmit = async () => {
user_role: userRole.value,
});
- const init = initialValues.value;
- const enrichableFields = {
- website: website.value,
- company_size: companySize.value,
- industry: industry.value,
- };
-
useTrack(ONBOARDING_EVENTS.ACCOUNT_DETAILS_COMPLETED, {
has_enriched_data: Boolean(
currentAccount.value?.custom_attributes?.brand_info
),
- fields_changed: Object.entries(enrichableFields)
- .filter(([key, val]) => val !== init[key])
- .map(([key]) => key),
+ fields_changed: fieldsChanged,
user_role: userRole.value,
company_size: companySize.value,
industry: industry.value,
diff --git a/app/javascript/dashboard/routes/dashboard/settings/canned/AddCanned.vue b/app/javascript/dashboard/routes/dashboard/settings/canned/AddCanned.vue
index 56caa2558..7fd23ebb1 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/canned/AddCanned.vue
+++ b/app/javascript/dashboard/routes/dashboard/settings/canned/AddCanned.vue
@@ -143,17 +143,15 @@ export default {
diff --git a/app/javascript/dashboard/routes/dashboard/settings/canned/EditCanned.vue b/app/javascript/dashboard/routes/dashboard/settings/canned/EditCanned.vue
index d2c906511..7a570a300 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/canned/EditCanned.vue
+++ b/app/javascript/dashboard/routes/dashboard/settings/canned/EditCanned.vue
@@ -147,17 +147,15 @@ export default {
diff --git a/app/javascript/dashboard/routes/dashboard/settings/canned/Index.vue b/app/javascript/dashboard/routes/dashboard/settings/canned/Index.vue
index 5c1810b63..1bc1ae392 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/canned/Index.vue
+++ b/app/javascript/dashboard/routes/dashboard/settings/canned/Index.vue
@@ -4,7 +4,7 @@ import AddCanned from './AddCanned.vue';
import EditCanned from './EditCanned.vue';
import SettingsLayout from '../SettingsLayout.vue';
import BaseSettingsHeader from '../components/BaseSettingsHeader.vue';
-import { computed, onMounted, ref, defineOptions } from 'vue';
+import { computed, onMounted, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useStoreGetters, useStore } from 'dashboard/composables/store';
import { picoSearch } from '@scmmishra/pico-search';
diff --git a/app/javascript/dashboard/routes/dashboard/settings/customRoles/component/CustomRolePaywall.vue b/app/javascript/dashboard/routes/dashboard/settings/customRoles/component/CustomRolePaywall.vue
index 90636dbde..4a3387fa2 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/customRoles/component/CustomRolePaywall.vue
+++ b/app/javascript/dashboard/routes/dashboard/settings/customRoles/component/CustomRolePaywall.vue
@@ -74,23 +74,25 @@ const tableHeaders = computed(() => {
-
-
-
-
- {{ thHeader }}
-
- |
-
-
+
+
+
+
+ |
+
+ {{ thHeader }}
+
+ |
+
+
+
+
+
+
{
icon: 'i-woot-voice',
});
+ channels.push({
+ key: 'whatsapp_call',
+ title: t('INBOX_MGMT.ADD.AUTH.CHANNEL.WHATSAPP_CALL.TITLE'),
+ description: t('INBOX_MGMT.ADD.AUTH.CHANNEL.WHATSAPP_CALL.DESCRIPTION'),
+ icon: 'i-woot-whatsapp',
+ });
+
return channels;
});
diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/PreChatForm/Settings.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/PreChatForm/Settings.vue
index 7f6ac7fe1..626971081 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/inbox/PreChatForm/Settings.vue
+++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/PreChatForm/Settings.vue
@@ -156,10 +156,8 @@ onMounted(() => {
.message-editor {
@apply px-3;
- ::v-deep {
- .ProseMirror-menubar {
- @apply rounded-tl-[4px];
- }
+ :deep(.ProseMirror-menubar) {
+ @apply rounded-tl-[4px];
}
}
diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/Settings.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/Settings.vue
index 6abb4fc33..f0e3ab3d1 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/inbox/Settings.vue
+++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/Settings.vue
@@ -22,6 +22,7 @@ import WeeklyAvailability from './components/WeeklyAvailability.vue';
import GreetingsEditor from 'shared/components/GreetingsEditor.vue';
import ConfigurationPage from './settingsPage/ConfigurationPage.vue';
import VoiceConfigurationPage from './settingsPage/VoiceConfigurationPage.vue';
+import WhatsappCallingPage from './settingsPage/WhatsappCallingPage.vue';
import CustomerSatisfactionPage from './settingsPage/CustomerSatisfactionPage.vue';
import CollaboratorsPage from './settingsPage/CollaboratorsPage.vue';
import BotConfiguration from './components/BotConfiguration.vue';
@@ -48,6 +49,7 @@ export default {
CollaboratorsPage,
ConfigurationPage,
VoiceConfigurationPage,
+ WhatsappCallingPage,
CustomerSatisfactionPage,
FacebookReauthorize,
GreetingsEditor,
@@ -248,8 +250,6 @@ export default {
},
];
}
-
- // Google Play has no support for bots, business hours, or CSAT — strip those tabs
if (this.isAGooglePlayChannel) {
const unsupportedKeys = ['business-hours', 'csat', 'bot-configuration'];
visibleToAllChannelTabs = visibleToAllChannelTabs.filter(
@@ -257,6 +257,22 @@ export default {
);
}
+ if (
+ this.isAWhatsAppCloudChannel &&
+ this.isFeatureEnabledonAccount(
+ this.accountId,
+ FEATURE_FLAGS.CHANNEL_VOICE
+ )
+ ) {
+ visibleToAllChannelTabs = [
+ ...visibleToAllChannelTabs,
+ {
+ key: 'calls-configuration',
+ name: this.$t('INBOX_MGMT.TABS.CALLS'),
+ },
+ ];
+ }
+
return visibleToAllChannelTabs;
},
currentInboxId() {
@@ -1275,6 +1291,12 @@ export default {
>
+
+
+
diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/WhatsappCall.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/WhatsappCall.vue
new file mode 100644
index 000000000..c27cd7d1f
--- /dev/null
+++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/WhatsappCall.vue
@@ -0,0 +1,11 @@
+
+
+
+
+
diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/WhatsappEmbeddedSignup.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/WhatsappEmbeddedSignup.vue
index cf5c1310e..668e0709a 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/WhatsappEmbeddedSignup.vue
+++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/WhatsappEmbeddedSignup.vue
@@ -7,6 +7,7 @@ import { useAlert } from 'dashboard/composables';
import Icon from 'next/icon/Icon.vue';
import NextButton from 'next/button/Button.vue';
import LoadingState from 'dashboard/components/widgets/LoadingState.vue';
+import InboxesAPI from 'dashboard/api/inboxes';
import { parseAPIErrorResponse } from 'dashboard/store/utils/api';
import globalConstants from 'dashboard/constants/globals.js';
import {
@@ -16,6 +17,13 @@ import {
isValidBusinessData,
} from './whatsapp/utils';
+const props = defineProps({
+ enableCallingOnComplete: {
+ type: Boolean,
+ default: false,
+ },
+});
+
const store = useStore();
const router = useRouter();
const { t } = useI18n();
@@ -65,11 +73,27 @@ const handleSignupCancellation = () => {
isAuthenticating.value = false;
};
-const handleSignupSuccess = inboxData => {
- isProcessing.value = false;
- isAuthenticating.value = false;
+const enableCallingForInbox = async inboxId => {
+ try {
+ await InboxesAPI.enableWhatsappCalling(inboxId);
+ } catch (_) {
+ useAlert(
+ t('INBOX_MGMT.ADD.WHATSAPP.EMBEDDED_SIGNUP.CALLING_ENABLE_FAILED')
+ );
+ }
+};
+const handleSignupSuccess = async inboxData => {
if (inboxData && inboxData.id) {
+ if (props.enableCallingOnComplete) {
+ isProcessing.value = true;
+ processingMessage.value = t(
+ 'INBOX_MGMT.ADD.WHATSAPP.EMBEDDED_SIGNUP.ENABLING_CALLING'
+ );
+ await enableCallingForInbox(inboxData.id);
+ }
+ isProcessing.value = false;
+ isAuthenticating.value = false;
useAlert(t('INBOX_MGMT.FINISH.MESSAGE'));
router.replace({
name: 'settings_inboxes_add_agents',
@@ -79,6 +103,8 @@ const handleSignupSuccess = inboxData => {
},
});
} else {
+ isProcessing.value = false;
+ isAuthenticating.value = false;
useAlert(t('INBOX_MGMT.ADD.WHATSAPP.EMBEDDED_SIGNUP.SUCCESS_FALLBACK'));
router.replace({
name: 'settings_inbox_list',
diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/emailChannels/Google.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/emailChannels/Google.vue
index a5c16fa57..e49d98423 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/emailChannels/Google.vue
+++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/emailChannels/Google.vue
@@ -1,6 +1,5 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/routes/dashboard/settings/integrations/SingleIntegrationHooks.vue b/app/javascript/dashboard/routes/dashboard/settings/integrations/SingleIntegrationHooks.vue
index 405656542..14fec4996 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/integrations/SingleIntegrationHooks.vue
+++ b/app/javascript/dashboard/routes/dashboard/settings/integrations/SingleIntegrationHooks.vue
@@ -1,5 +1,4 @@
-
+
{{ element.title }}
diff --git a/app/javascript/portal/portalHelpers.js b/app/javascript/portal/portalHelpers.js
index d654f9054..1d2ae2e42 100644
--- a/app/javascript/portal/portalHelpers.js
+++ b/app/javascript/portal/portalHelpers.js
@@ -24,10 +24,9 @@ export const getHeadingsfromTheArticle = () => {
permalink.className = 'permalink text-slate-600 ml-3';
permalink.href = `#${slug}`;
permalink.title = headingText;
- permalink.dataset.turbolinks = 'false';
+ permalink.dataset.turbo = 'false';
permalink.textContent = '#';
element.appendChild(permalink);
-
rows.push({
slug,
title: headingText,
@@ -188,7 +187,7 @@ export const InitializationHelpers = {
const a = document.createElement('a');
a.href = window.location.hash;
- a['data-turbolinks'] = false;
+ a['data-turbo'] = false;
a.click();
}
},
diff --git a/app/javascript/shared/components/StarRating.vue b/app/javascript/shared/components/StarRating.vue
index a7139638e..b3b11d4fb 100644
--- a/app/javascript/shared/components/StarRating.vue
+++ b/app/javascript/shared/components/StarRating.vue
@@ -1,5 +1,5 @@
diff --git a/app/views/public/api/v1/portals/search/index.html+documentation.erb b/app/views/public/api/v1/portals/search/index.html+documentation.erb
new file mode 100644
index 000000000..8577a5f4e
--- /dev/null
+++ b/app/views/public/api/v1/portals/search/index.html+documentation.erb
@@ -0,0 +1,64 @@
+<% content_for :head do %>
+ <%= I18n.t('public_portal.search.results_for', query: @query) %> | <%= @portal.name %>
+<% end %>
+
+
+ <% pagination_params = params.permit(:query, :locale, :theme, :show_plain_layout) %>
+
+
+
+
+
+ <%= I18n.t('public_portal.search.results_for', query: @query) %>
+
+ <%= render 'public/api/v1/portals/search/form',
+ input_class: 'w-full px-4 py-3 border border-n-weak rounded-lg bg-n-alpha-1 text-n-slate-12 placeholder-n-slate-10 focus:outline-none focus:ring-2 focus:ring-n-portal focus:border-transparent' %>
+
+
+ <%= render 'public/api/v1/portals/search/search_handler' %>
+
+ <% if @articles.empty? %>
+ <%= render 'public/api/v1/portals/documentation_layout/empty_state',
+ message: I18n.t('public_portal.search.no_results', query: @query) %>
+ <% else %>
+
+ <%= I18n.t('public_portal.search.found_results', count: @articles.total_count) %>
+
+
+
+ <% @articles.each do |article| %>
+ <%= render 'public/api/v1/portals/documentation_layout/article_card',
+ portal: @portal,
+ article: article %>
+ <% end %>
+
+
+ <% if @articles.respond_to?(:total_pages) && @articles.total_pages > 1 %>
+
+ <% end %>
+ <% end %>
+
diff --git a/app/views/public/api/v1/portals/search/index.html.erb b/app/views/public/api/v1/portals/search/index.html.erb
new file mode 100644
index 000000000..82c29775f
--- /dev/null
+++ b/app/views/public/api/v1/portals/search/index.html.erb
@@ -0,0 +1,118 @@
+<% content_for :head do %>
+ <%= I18n.t('public_portal.search.results_for', query: @query) %> | <%= @portal.name %>
+<% end %>
+
+<% search_input_class = 'w-full px-4 py-3 border border-slate-200 dark:border-slate-700 rounded-lg bg-white dark:bg-slate-800 text-slate-900 dark:text-slate-100 placeholder-slate-500 dark:placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent' %>
+
+<% if !@is_plain_layout_enabled %>
+
+
+
+
+
+
+ <%= I18n.t('public_portal.search.results_for', query: @query) %>
+
+
+ <%= render 'public/api/v1/portals/search/form', input_class: search_input_class %>
+
+
+
+<% else %>
+
+
+
+
+ <%= I18n.t('public_portal.search.results_for', query: @query) %>
+
+
+ <%= render 'public/api/v1/portals/search/form', input_class: search_input_class %>
+
+<% end %>
+
+<% pagination_params = params.permit(:query, :locale, :theme, :show_plain_layout) %>
+
+<%= render 'public/api/v1/portals/search/search_handler' %>
+
+
+
+ <% if @articles.empty? %>
+
+
<%= I18n.t('public_portal.search.no_results', query: @query) %>
+
+ <% else %>
+
+ <%= I18n.t('public_portal.search.found_results', count: @articles.total_count) %>
+
+
+ <% @articles.each do |article| %>
+
+ <% end %>
+
+ <% if @articles.respond_to?(:total_pages) && @articles.total_pages > 1 %>
+
+ <% end %>
+ <% end %>
+
+
diff --git a/config/app.yml b/config/app.yml
index 1c93a1287..fec34cd07 100644
--- a/config/app.yml
+++ b/config/app.yml
@@ -1,5 +1,5 @@
shared: &shared
- version: '4.14.0'
+ version: '4.14.1'
development:
<<: *shared
diff --git a/config/installation_config.yml b/config/installation_config.yml
index e374d0948..673c6df4c 100644
--- a/config/installation_config.yml
+++ b/config/installation_config.yml
@@ -215,6 +215,18 @@
value:
locked: false
type: code
+- name: CAPTAIN_DOCUMENT_AUTO_SYNC_PER_ACCOUNT_BATCH_LIMIT
+ display_title: 'Captain Document Auto Sync Per Account Batch Limit'
+ description: 'Maximum syncable Captain documents to enqueue per account in one scheduler run. Defaults to 50.'
+ value: 50
+ locked: false
+ type: number
+- name: CAPTAIN_DOCUMENT_AUTO_SYNC_GLOBAL_BATCH_LIMIT
+ display_title: 'Captain Document Auto Sync Global Batch Limit'
+ description: 'Maximum syncable Captain documents to enqueue globally in one scheduler run. Defaults to 1000.'
+ value: 1000
+ locked: false
+ type: number
# End of Captain Config
# ------- Context.dev Config ------- #
diff --git a/config/locales/am.yml b/config/locales/am.yml
index a42e45ab3..70c1310af 100644
--- a/config/locales/am.yml
+++ b/config/locales/am.yml
@@ -241,6 +241,7 @@ am:
whatsapp:
list_button_label: 'Choose an item'
call_permission_request_body: 'We would like to call you regarding your conversation.'
+ unsupported_message: 'This message is unavailable.'
voice_call:
twilio: 'Voice Call'
whatsapp: 'WhatsApp Call'
@@ -412,7 +413,30 @@ am:
empty_placeholder: No results found.
loading_placeholder: Searching...
results_title: Search results
+ results: Search Results
+ results_for: "Search Results for '%{query}'"
+ no_results: "No results found for '%{query}'"
+ found_results:
+ one: Found 1 result
+ other: 'Found %{count} results'
+ submit: ፈልግ
toc_header: 'On this page'
+ sidebar:
+ help_center: የእርዳታ ማዕከል
+ categories: ምድቦች
+ language: Language
+ theme: Theme
+ open_sidebar: አጠገብ በር ክፈት
+ close_sidebar: አጠገብ በር ዝጋ
+ browse: Browse
+ back_to_category: Back to %{name}
+ browse_by_topic: Browse by topic
+ browse_by_topic_subtitle: Find guides, tutorials, and answers organised by category.
+ popular_articles: Popular articles
+ popular_articles_subtitle: What other people are reading right now.
+ popular_label: 'Popular topics:'
+ authors_others: "%{names} and %{count} others"
+ primary_nav: Primary
hero:
sub_title: Search for the articles here or browse the categories below.
common:
@@ -427,12 +451,21 @@ am:
others: others
by: By
no_articles: There are no articles here
+ previous: Previous
+ next: Next
+ article_actions:
+ label: Open in
+ view_markdown: View as Markdown
+ open_in_chatgpt: Open in ChatGPT
+ open_in_claude: Open in Claude
+ llm_prompt: "Read the following article and help me understand it: %{url}"
footer:
made_with: Made with
header:
go_to_homepage: Website
visit_website: Visit website
appearance:
+ title: Appearance
system: System
light: Light
dark: Dark
diff --git a/config/locales/ar.yml b/config/locales/ar.yml
index a7bf0234b..3882666eb 100644
--- a/config/locales/ar.yml
+++ b/config/locales/ar.yml
@@ -241,6 +241,7 @@ ar:
whatsapp:
list_button_label: 'اختر عنصر'
call_permission_request_body: 'We would like to call you regarding your conversation.'
+ unsupported_message: 'This message is unavailable.'
voice_call:
twilio: 'Voice Call'
whatsapp: 'WhatsApp Call'
@@ -412,6 +413,17 @@ ar:
empty_placeholder: لم يتم العثور على النتائج.
loading_placeholder: جاري البحث...
results_title: نتائج البحث
+ results: نتائج البحث
+ results_for: "Search Results for '%{query}'"
+ no_results: "No results found for '%{query}'"
+ found_results:
+ zero: 'Found %{count} results'
+ one: Found 1 result
+ two: 'Found %{count} results'
+ few: 'Found %{count} results'
+ many: 'Found %{count} results'
+ other: 'Found %{count} results'
+ submit: بحث
toc_header: 'في هذه الصفحة'
sidebar:
help_center: مركز المساعدة
@@ -443,6 +455,8 @@ ar:
others: الآخرين
by: بواسطة
no_articles: لا توجد مقالات
+ previous: السابق
+ next: التالي
article_actions:
label: فتح في
view_markdown: عرض كـ Markdown
@@ -455,6 +469,7 @@ ar:
go_to_homepage: الموقع الإلكتروني
visit_website: Visit website
appearance:
+ title: المظهر
system: النظام
light: فاتح
dark: مظلم
diff --git a/config/locales/az.yml b/config/locales/az.yml
index 5632d143e..fe51b907d 100644
--- a/config/locales/az.yml
+++ b/config/locales/az.yml
@@ -241,6 +241,7 @@ az:
whatsapp:
list_button_label: 'Choose an item'
call_permission_request_body: 'We would like to call you regarding your conversation.'
+ unsupported_message: 'This message is unavailable.'
voice_call:
twilio: 'Voice Call'
whatsapp: 'WhatsApp Call'
@@ -412,7 +413,30 @@ az:
empty_placeholder: No results found.
loading_placeholder: Searching...
results_title: Search results
+ results: Axtarış Nəticələri
+ results_for: "Search Results for '%{query}'"
+ no_results: "No results found for '%{query}'"
+ found_results:
+ one: Found 1 result
+ other: 'Found %{count} results'
+ submit: Axtar
toc_header: 'On this page'
+ sidebar:
+ help_center: Kömək Mərkəzi
+ categories: Kateqoriyalar
+ language: Language
+ theme: Theme
+ open_sidebar: Yan paneli aç
+ close_sidebar: Yan paneli bağla
+ browse: Browse
+ back_to_category: Back to %{name}
+ browse_by_topic: Browse by topic
+ browse_by_topic_subtitle: Find guides, tutorials, and answers organised by category.
+ popular_articles: Popular articles
+ popular_articles_subtitle: What other people are reading right now.
+ popular_label: 'Popular topics:'
+ authors_others: "%{names} and %{count} others"
+ primary_nav: Primary
hero:
sub_title: Search for the articles here or browse the categories below.
common:
@@ -427,12 +451,21 @@ az:
others: others
by: By
no_articles: There are no articles here
+ previous: Previous
+ next: Next
+ article_actions:
+ label: Open in
+ view_markdown: View as Markdown
+ open_in_chatgpt: Open in ChatGPT
+ open_in_claude: Open in Claude
+ llm_prompt: "Read the following article and help me understand it: %{url}"
footer:
made_with: Made with
header:
go_to_homepage: Website
visit_website: Visit website
appearance:
+ title: Appearance
system: System
light: Light
dark: Dark
diff --git a/config/locales/bg.yml b/config/locales/bg.yml
index ca2fe620a..e2674edda 100644
--- a/config/locales/bg.yml
+++ b/config/locales/bg.yml
@@ -241,6 +241,7 @@ bg:
whatsapp:
list_button_label: 'Choose an item'
call_permission_request_body: 'We would like to call you regarding your conversation.'
+ unsupported_message: 'This message is unavailable.'
voice_call:
twilio: 'Voice Call'
whatsapp: 'WhatsApp Call'
@@ -412,7 +413,30 @@ bg:
empty_placeholder: Няма намерени резултати.
loading_placeholder: Searching...
results_title: Search results
+ results: Search Results
+ results_for: "Search Results for '%{query}'"
+ no_results: "No results found for '%{query}'"
+ found_results:
+ one: Found 1 result
+ other: 'Found %{count} results'
+ submit: Търсене
toc_header: 'On this page'
+ sidebar:
+ help_center: Help Center
+ categories: Categories
+ language: Language
+ theme: Theme
+ open_sidebar: Open sidebar
+ close_sidebar: Close sidebar
+ browse: Browse
+ back_to_category: Back to %{name}
+ browse_by_topic: Browse by topic
+ browse_by_topic_subtitle: Find guides, tutorials, and answers organised by category.
+ popular_articles: Popular articles
+ popular_articles_subtitle: What other people are reading right now.
+ popular_label: 'Popular topics:'
+ authors_others: "%{names} and %{count} others"
+ primary_nav: Primary
hero:
sub_title: Search for the articles here or browse the categories below.
common:
@@ -427,12 +451,21 @@ bg:
others: others
by: By
no_articles: There are no articles here
+ previous: Previous
+ next: Next
+ article_actions:
+ label: Open in
+ view_markdown: View as Markdown
+ open_in_chatgpt: Open in ChatGPT
+ open_in_claude: Open in Claude
+ llm_prompt: "Read the following article and help me understand it: %{url}"
footer:
made_with: Made with
header:
go_to_homepage: Website
visit_website: Visit website
appearance:
+ title: Appearance
system: System
light: Light
dark: Dark
diff --git a/config/locales/bn.yml b/config/locales/bn.yml
index 1a7b02b1b..fb88cd27b 100644
--- a/config/locales/bn.yml
+++ b/config/locales/bn.yml
@@ -241,6 +241,7 @@ bn:
whatsapp:
list_button_label: 'একটি আইটেম নির্বাচন করুন'
call_permission_request_body: 'We would like to call you regarding your conversation.'
+ unsupported_message: 'This message is unavailable.'
voice_call:
twilio: 'Voice Call'
whatsapp: 'WhatsApp Call'
@@ -412,7 +413,30 @@ bn:
empty_placeholder: কোনো ফলাফল পাওয়া যায়নি।.
loading_placeholder: অনুসন্ধান চলছে...
results_title: অনুসন্ধানের ফলাফল
+ results: অনুসন্ধান ফলাফল
+ results_for: "Search Results for '%{query}'"
+ no_results: "No results found for '%{query}'"
+ found_results:
+ one: Found 1 result
+ other: 'Found %{count} results'
+ submit: অনুসন্ধান
toc_header: 'এই পাতায়'
+ sidebar:
+ help_center: সহায়তা কেন্দ্র
+ categories: বিভাগসমূহ
+ language: Language
+ theme: Theme
+ open_sidebar: সাইডবার খুলুন
+ close_sidebar: সাইডবার বন্ধ করুন
+ browse: Browse
+ back_to_category: Back to %{name}
+ browse_by_topic: Browse by topic
+ browse_by_topic_subtitle: Find guides, tutorials, and answers organised by category.
+ popular_articles: Popular articles
+ popular_articles_subtitle: What other people are reading right now.
+ popular_label: 'Popular topics:'
+ authors_others: "%{names} and %{count} others"
+ primary_nav: Primary
hero:
sub_title: এখানে প্রবন্ধগুলি খুঁজুন অথবা নিচের বিভাগগুলো ব্রাউজ করুন।.
common:
@@ -427,12 +451,21 @@ bn:
others: অন্যান্যরা
by: দ্বারা
no_articles: এখানে কোনো প্রবন্ধ নেই
+ previous: Previous
+ next: Next
+ article_actions:
+ label: Open in
+ view_markdown: View as Markdown
+ open_in_chatgpt: Open in ChatGPT
+ open_in_claude: Open in Claude
+ llm_prompt: "Read the following article and help me understand it: %{url}"
footer:
made_with: নির্মিত
header:
go_to_homepage: ওয়েবসাইট
visit_website: ওয়েবসাইট দেখুন
appearance:
+ title: Appearance
system: সিস্টেম
light: হালকা
dark: গাঢ়
diff --git a/config/locales/ca.yml b/config/locales/ca.yml
index 920ba2ec0..ebf894da2 100644
--- a/config/locales/ca.yml
+++ b/config/locales/ca.yml
@@ -241,6 +241,7 @@ ca:
whatsapp:
list_button_label: 'Choose an item'
call_permission_request_body: 'We would like to call you regarding your conversation.'
+ unsupported_message: 'This message is unavailable.'
voice_call:
twilio: 'Voice Call'
whatsapp: 'WhatsApp Call'
@@ -412,7 +413,30 @@ ca:
empty_placeholder: No s'ha trobat agents.
loading_placeholder: S'està cercant...
results_title: Resultats de la cerca
+ results: Resultats de la cerca
+ results_for: "Search Results for '%{query}'"
+ no_results: "No results found for '%{query}'"
+ found_results:
+ one: Found 1 result
+ other: 'Found %{count} results'
+ submit: Cercar
toc_header: 'En aquesta pàgina'
+ sidebar:
+ help_center: Centre d'ajuda
+ categories: Categories
+ language: Idioma
+ theme: Theme
+ open_sidebar: Obre la barra lateral
+ close_sidebar: Tanca la barra lateral
+ browse: Browse
+ back_to_category: Back to %{name}
+ browse_by_topic: Browse by topic
+ browse_by_topic_subtitle: Find guides, tutorials, and answers organised by category.
+ popular_articles: Popular articles
+ popular_articles_subtitle: What other people are reading right now.
+ popular_label: 'Popular topics:'
+ authors_others: "%{names} and %{count} others"
+ primary_nav: Primary
hero:
sub_title: Cerca els articles aquí o navega per les categories següents.
common:
@@ -427,12 +451,21 @@ ca:
others: altres
by: Per
no_articles: No hi ha articles aquí
+ previous: Previous
+ next: Next
+ article_actions:
+ label: Open in
+ view_markdown: View as Markdown
+ open_in_chatgpt: Open in ChatGPT
+ open_in_claude: Open in Claude
+ llm_prompt: "Read the following article and help me understand it: %{url}"
footer:
made_with: Fet amb
header:
go_to_homepage: Lloc web
visit_website: Visit website
appearance:
+ title: Aparença
system: Sistema
light: Clar
dark: Fosc
diff --git a/config/locales/cs.yml b/config/locales/cs.yml
index 2fe9bfe41..5e6315000 100644
--- a/config/locales/cs.yml
+++ b/config/locales/cs.yml
@@ -241,6 +241,7 @@ cs:
whatsapp:
list_button_label: 'Choose an item'
call_permission_request_body: 'We would like to call you regarding your conversation.'
+ unsupported_message: 'This message is unavailable.'
voice_call:
twilio: 'Voice Call'
whatsapp: 'WhatsApp Call'
@@ -412,7 +413,32 @@ cs:
empty_placeholder: Žádné výsledky.
loading_placeholder: Searching...
results_title: Search results
+ results: Výsledky hledání
+ results_for: "Search Results for '%{query}'"
+ no_results: "No results found for '%{query}'"
+ found_results:
+ one: Found 1 result
+ few: 'Found %{count} results'
+ many: 'Found %{count} results'
+ other: 'Found %{count} results'
+ submit: Hledat
toc_header: 'On this page'
+ sidebar:
+ help_center: Help Center
+ categories: Categories
+ language: Language
+ theme: Theme
+ open_sidebar: Otevřít postranní panel
+ close_sidebar: Zavřít postranní panel
+ browse: Browse
+ back_to_category: Back to %{name}
+ browse_by_topic: Browse by topic
+ browse_by_topic_subtitle: Find guides, tutorials, and answers organised by category.
+ popular_articles: Popular articles
+ popular_articles_subtitle: What other people are reading right now.
+ popular_label: 'Popular topics:'
+ authors_others: "%{names} and %{count} others"
+ primary_nav: Primary
hero:
sub_title: Search for the articles here or browse the categories below.
common:
@@ -427,12 +453,21 @@ cs:
others: others
by: By
no_articles: There are no articles here
+ previous: Previous
+ next: Next
+ article_actions:
+ label: Open in
+ view_markdown: View as Markdown
+ open_in_chatgpt: Open in ChatGPT
+ open_in_claude: Open in Claude
+ llm_prompt: "Read the following article and help me understand it: %{url}"
footer:
made_with: Made with
header:
go_to_homepage: Website
visit_website: Visit website
appearance:
+ title: Appearance
system: System
light: Light
dark: Dark
diff --git a/config/locales/da.yml b/config/locales/da.yml
index 5af6c9ccd..43d291bfc 100644
--- a/config/locales/da.yml
+++ b/config/locales/da.yml
@@ -241,6 +241,7 @@ da:
whatsapp:
list_button_label: 'Choose an item'
call_permission_request_body: 'We would like to call you regarding your conversation.'
+ unsupported_message: 'This message is unavailable.'
voice_call:
twilio: 'Voice Call'
whatsapp: 'WhatsApp Call'
@@ -412,7 +413,30 @@ da:
empty_placeholder: Ingen resultater fundet.
loading_placeholder: Søger...
results_title: Søgeresultater
+ results: Søgeresultater
+ results_for: "Search Results for '%{query}'"
+ no_results: "No results found for '%{query}'"
+ found_results:
+ one: Found 1 result
+ other: 'Found %{count} results'
+ submit: Søg
toc_header: 'On this page'
+ sidebar:
+ help_center: Help Center
+ categories: Kategorier
+ language: Sprog
+ theme: Theme
+ open_sidebar: Åbn sidepanel
+ close_sidebar: Luk sidepanel
+ browse: Browse
+ back_to_category: Back to %{name}
+ browse_by_topic: Browse by topic
+ browse_by_topic_subtitle: Find guides, tutorials, and answers organised by category.
+ popular_articles: Popular articles
+ popular_articles_subtitle: What other people are reading right now.
+ popular_label: 'Popular topics:'
+ authors_others: "%{names} and %{count} others"
+ primary_nav: Primary
hero:
sub_title: Søg efter artiklerne her eller gennemse kategorierne nedenfor.
common:
@@ -427,12 +451,21 @@ da:
others: others
by: By
no_articles: There are no articles here
+ previous: Previous
+ next: Next
+ article_actions:
+ label: Open in
+ view_markdown: View as Markdown
+ open_in_chatgpt: Open in ChatGPT
+ open_in_claude: Open in Claude
+ llm_prompt: "Read the following article and help me understand it: %{url}"
footer:
made_with: Made with
header:
go_to_homepage: Website
visit_website: Visit website
appearance:
+ title: Appearance
system: System
light: Light
dark: Dark
diff --git a/config/locales/de.yml b/config/locales/de.yml
index f9156ece1..ddf1ad052 100644
--- a/config/locales/de.yml
+++ b/config/locales/de.yml
@@ -241,6 +241,7 @@ de:
whatsapp:
list_button_label: 'Choose an item'
call_permission_request_body: 'We would like to call you regarding your conversation.'
+ unsupported_message: 'This message is unavailable.'
voice_call:
twilio: 'Voice Call'
whatsapp: 'WhatsApp Call'
@@ -412,7 +413,30 @@ de:
empty_placeholder: Keine Ergebnisse gefunden.
loading_placeholder: Suchen...
results_title: Suchergebnisse
+ results: Suchergebnisse
+ results_for: "Search Results for '%{query}'"
+ no_results: "No results found for '%{query}'"
+ found_results:
+ one: Found 1 result
+ other: 'Found %{count} results'
+ submit: Suchen
toc_header: 'Auf dieser Seite'
+ sidebar:
+ help_center: Hilfezentrum
+ categories: Kategorien
+ language: Sprache
+ theme: Theme
+ open_sidebar: Seitenleiste öffnen
+ close_sidebar: Seitenleiste schließen
+ browse: Browse
+ back_to_category: Back to %{name}
+ browse_by_topic: Browse by topic
+ browse_by_topic_subtitle: Find guides, tutorials, and answers organised by category.
+ popular_articles: Popular articles
+ popular_articles_subtitle: What other people are reading right now.
+ popular_label: 'Popular topics:'
+ authors_others: "%{names} and %{count} others"
+ primary_nav: Primary
hero:
sub_title: Suchen Sie hier nach den Artikeln oder stöbern Sie in den unten stehenden Kategorien.
common:
@@ -427,12 +451,21 @@ de:
others: andere
by: Von
no_articles: Keine Artikel vorhanden
+ previous: Previous
+ next: Next
+ article_actions:
+ label: Open in
+ view_markdown: View as Markdown
+ open_in_chatgpt: Open in ChatGPT
+ open_in_claude: Open in Claude
+ llm_prompt: "Read the following article and help me understand it: %{url}"
footer:
made_with: Erstellt mit
header:
go_to_homepage: Webseite
visit_website: Visit website
appearance:
+ title: Darstellung
system: System
light: Hell
dark: Dunkel
diff --git a/config/locales/el.yml b/config/locales/el.yml
index ae45f7518..153bb5c89 100644
--- a/config/locales/el.yml
+++ b/config/locales/el.yml
@@ -241,6 +241,7 @@ el:
whatsapp:
list_button_label: 'Choose an item'
call_permission_request_body: 'We would like to call you regarding your conversation.'
+ unsupported_message: 'This message is unavailable.'
voice_call:
twilio: 'Voice Call'
whatsapp: 'WhatsApp Call'
@@ -412,7 +413,30 @@ el:
empty_placeholder: Δεν βρέθηκαν αποτελέσματα.
loading_placeholder: Αναζήτηση...
results_title: Αποτελέσματα Αναζήτησης
+ results: Αποτελέσματα Αναζήτησης
+ results_for: "Search Results for '%{query}'"
+ no_results: "No results found for '%{query}'"
+ found_results:
+ one: Found 1 result
+ other: 'Found %{count} results'
+ submit: Αναζήτηση
toc_header: 'On this page'
+ sidebar:
+ help_center: Help Center
+ categories: Κατηγορίες
+ language: Γλώσσα
+ theme: Theme
+ open_sidebar: Άνοιγμα πλευρικής μπάρας
+ close_sidebar: Κλείσιμο πλευρικής μπάρας
+ browse: Browse
+ back_to_category: Back to %{name}
+ browse_by_topic: Browse by topic
+ browse_by_topic_subtitle: Find guides, tutorials, and answers organised by category.
+ popular_articles: Popular articles
+ popular_articles_subtitle: What other people are reading right now.
+ popular_label: 'Popular topics:'
+ authors_others: "%{names} and %{count} others"
+ primary_nav: Primary
hero:
sub_title: Αναζητήστε τα άρθρα εδώ ή περιηγηθείτε στις κατηγορίες παρακάτω.
common:
@@ -427,12 +451,21 @@ el:
others: others
by: By
no_articles: There are no articles here
+ previous: Previous
+ next: Next
+ article_actions:
+ label: Open in
+ view_markdown: View as Markdown
+ open_in_chatgpt: Open in ChatGPT
+ open_in_claude: Open in Claude
+ llm_prompt: "Read the following article and help me understand it: %{url}"
footer:
made_with: Made with
header:
go_to_homepage: Website
visit_website: Visit website
appearance:
+ title: Appearance
system: System
light: Light
dark: Dark
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 18b721caf..8b8202f1c 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -258,6 +258,7 @@ en:
whatsapp:
list_button_label: 'Choose an item'
call_permission_request_body: 'We would like to call you regarding your conversation.'
+ unsupported_message: 'This message is unavailable.'
voice_call:
twilio: 'Voice Call'
whatsapp: 'WhatsApp Call'
@@ -429,6 +430,13 @@ en:
empty_placeholder: No results found.
loading_placeholder: Searching...
results_title: Search results
+ results: Search Results
+ results_for: "Search Results for '%{query}'"
+ no_results: "No results found for '%{query}'"
+ found_results:
+ one: Found 1 result
+ other: 'Found %{count} results'
+ submit: Search
toc_header: 'On this page'
sidebar:
help_center: Help Center
@@ -460,6 +468,8 @@ en:
others: others
by: By
no_articles: There are no articles here
+ previous: Previous
+ next: Next
article_actions:
label: Open in
view_markdown: View as Markdown
diff --git a/config/locales/es.yml b/config/locales/es.yml
index 24166770a..c5a17fc79 100644
--- a/config/locales/es.yml
+++ b/config/locales/es.yml
@@ -241,6 +241,7 @@ es:
whatsapp:
list_button_label: 'Choose an item'
call_permission_request_body: 'We would like to call you regarding your conversation.'
+ unsupported_message: 'This message is unavailable.'
voice_call:
twilio: 'Voice Call'
whatsapp: 'WhatsApp Call'
@@ -412,7 +413,30 @@ es:
empty_placeholder: No se encontraron resultados.
loading_placeholder: Buscando...
results_title: Buscar resultados
+ results: Buscar resultados
+ results_for: "Search Results for '%{query}'"
+ no_results: "No results found for '%{query}'"
+ found_results:
+ one: Found 1 result
+ other: 'Found %{count} results'
+ submit: Buscar
toc_header: 'En esta página'
+ sidebar:
+ help_center: Centro de ayuda
+ categories: Categorías
+ language: Idioma
+ theme: Theme
+ open_sidebar: Abrir barra lateral
+ close_sidebar: Cerrar barra lateral
+ browse: Browse
+ back_to_category: Back to %{name}
+ browse_by_topic: Browse by topic
+ browse_by_topic_subtitle: Find guides, tutorials, and answers organised by category.
+ popular_articles: Popular articles
+ popular_articles_subtitle: What other people are reading right now.
+ popular_label: 'Popular topics:'
+ authors_others: "%{names} and %{count} others"
+ primary_nav: Primary
hero:
sub_title: Busque aquí los artículos o busque las categorías de abajo.
common:
@@ -427,12 +451,21 @@ es:
others: otros
by: Por
no_articles: No hay artículos aquí
+ previous: Anterior
+ next: Siguiente
+ article_actions:
+ label: Open in
+ view_markdown: View as Markdown
+ open_in_chatgpt: Open in ChatGPT
+ open_in_claude: Open in Claude
+ llm_prompt: "Read the following article and help me understand it: %{url}"
footer:
made_with: Hecho con
header:
go_to_homepage: Sitio web
visit_website: Visit website
appearance:
+ title: Apariencia
system: Sistema
light: Claro
dark: Oscuro
diff --git a/config/locales/et.yml b/config/locales/et.yml
index f5a0a6229..4a846374e 100644
--- a/config/locales/et.yml
+++ b/config/locales/et.yml
@@ -241,6 +241,7 @@ et:
whatsapp:
list_button_label: 'Choose an item'
call_permission_request_body: 'We would like to call you regarding your conversation.'
+ unsupported_message: 'This message is unavailable.'
voice_call:
twilio: 'Voice Call'
whatsapp: 'WhatsApp Call'
@@ -412,7 +413,30 @@ et:
empty_placeholder: Tulemusi ei leitud.
loading_placeholder: Otsin...
results_title: Otsingutulemused
+ results: Otsingutulemused
+ results_for: "Search Results for '%{query}'"
+ no_results: "No results found for '%{query}'"
+ found_results:
+ one: Found 1 result
+ other: 'Found %{count} results'
+ submit: Otsi
toc_header: 'On this page'
+ sidebar:
+ help_center: Abi keskus
+ categories: Kategooriad
+ language: Language
+ theme: Theme
+ open_sidebar: Ava külgriba
+ close_sidebar: Sulge külgriba
+ browse: Browse
+ back_to_category: Back to %{name}
+ browse_by_topic: Browse by topic
+ browse_by_topic_subtitle: Find guides, tutorials, and answers organised by category.
+ popular_articles: Popular articles
+ popular_articles_subtitle: What other people are reading right now.
+ popular_label: 'Popular topics:'
+ authors_others: "%{names} and %{count} others"
+ primary_nav: Primary
hero:
sub_title: Search for the articles here or browse the categories below.
common:
@@ -427,12 +451,21 @@ et:
others: others
by: By
no_articles: There are no articles here
+ previous: Previous
+ next: Next
+ article_actions:
+ label: Open in
+ view_markdown: View as Markdown
+ open_in_chatgpt: Open in ChatGPT
+ open_in_claude: Open in Claude
+ llm_prompt: "Read the following article and help me understand it: %{url}"
footer:
made_with: Made with
header:
go_to_homepage: Website
visit_website: Visit website
appearance:
+ title: Appearance
system: System
light: Light
dark: Dark
diff --git a/config/locales/fa.yml b/config/locales/fa.yml
index b48f5ffaa..4e6e2e492 100644
--- a/config/locales/fa.yml
+++ b/config/locales/fa.yml
@@ -241,6 +241,7 @@ fa:
whatsapp:
list_button_label: 'Choose an item'
call_permission_request_body: 'We would like to call you regarding your conversation.'
+ unsupported_message: 'This message is unavailable.'
voice_call:
twilio: 'Voice Call'
whatsapp: 'WhatsApp Call'
@@ -412,7 +413,30 @@ fa:
empty_placeholder: نتیجهای یافت نشد.
loading_placeholder: در حال جستجو...
results_title: نتایج جستجو
+ results: نتایج جستجو
+ results_for: "Search Results for '%{query}'"
+ no_results: "No results found for '%{query}'"
+ found_results:
+ one: Found 1 result
+ other: 'Found %{count} results'
+ submit: جستجو
toc_header: 'در این صفحه'
+ sidebar:
+ help_center: مرکز راهنما
+ categories: دستهبندیها
+ language: زبان
+ theme: Theme
+ open_sidebar: نوار کناری را باز کنید
+ close_sidebar: نوار کناری را ببندید
+ browse: Browse
+ back_to_category: Back to %{name}
+ browse_by_topic: Browse by topic
+ browse_by_topic_subtitle: Find guides, tutorials, and answers organised by category.
+ popular_articles: Popular articles
+ popular_articles_subtitle: What other people are reading right now.
+ popular_label: 'Popular topics:'
+ authors_others: "%{names} and %{count} others"
+ primary_nav: Primary
hero:
sub_title: مقالات را در اینجا جستجو کنید یا دستهبندیهای زیر را مرور کنید.
common:
@@ -427,12 +451,21 @@ fa:
others: دیگران
by: توسط
no_articles: هنوز هیچ مقالهای در اینجا وجود ندارد
+ previous: Previous
+ next: Next
+ article_actions:
+ label: Open in
+ view_markdown: View as Markdown
+ open_in_chatgpt: Open in ChatGPT
+ open_in_claude: Open in Claude
+ llm_prompt: "Read the following article and help me understand it: %{url}"
footer:
made_with: ساخته شده با
header:
go_to_homepage: وب سایت
visit_website: Visit website
appearance:
+ title: ظاهری
system: سیستم
light: روشن
dark: تیره
diff --git a/config/locales/fi.yml b/config/locales/fi.yml
index ed9925caf..3317f547e 100644
--- a/config/locales/fi.yml
+++ b/config/locales/fi.yml
@@ -241,6 +241,7 @@ fi:
whatsapp:
list_button_label: 'Choose an item'
call_permission_request_body: 'We would like to call you regarding your conversation.'
+ unsupported_message: 'This message is unavailable.'
voice_call:
twilio: 'Voice Call'
whatsapp: 'WhatsApp Call'
@@ -412,7 +413,30 @@ fi:
empty_placeholder: No results found.
loading_placeholder: Searching...
results_title: Search results
+ results: Search Results
+ results_for: "Search Results for '%{query}'"
+ no_results: "No results found for '%{query}'"
+ found_results:
+ one: Found 1 result
+ other: 'Found %{count} results'
+ submit: Etsi
toc_header: 'On this page'
+ sidebar:
+ help_center: Help Center
+ categories: Categories
+ language: Language
+ theme: Theme
+ open_sidebar: Open sidebar
+ close_sidebar: Close sidebar
+ browse: Browse
+ back_to_category: Back to %{name}
+ browse_by_topic: Browse by topic
+ browse_by_topic_subtitle: Find guides, tutorials, and answers organised by category.
+ popular_articles: Popular articles
+ popular_articles_subtitle: What other people are reading right now.
+ popular_label: 'Popular topics:'
+ authors_others: "%{names} and %{count} others"
+ primary_nav: Primary
hero:
sub_title: Search for the articles here or browse the categories below.
common:
@@ -427,12 +451,21 @@ fi:
others: others
by: By
no_articles: There are no articles here
+ previous: Previous
+ next: Next
+ article_actions:
+ label: Open in
+ view_markdown: View as Markdown
+ open_in_chatgpt: Open in ChatGPT
+ open_in_claude: Open in Claude
+ llm_prompt: "Read the following article and help me understand it: %{url}"
footer:
made_with: Made with
header:
go_to_homepage: Website
visit_website: Visit website
appearance:
+ title: Appearance
system: System
light: Light
dark: Dark
diff --git a/config/locales/fr.yml b/config/locales/fr.yml
index 8564a7154..b63c46cd6 100644
--- a/config/locales/fr.yml
+++ b/config/locales/fr.yml
@@ -51,7 +51,7 @@ fr:
invalid_params: 'Invalid, please check the signup paramters and try again'
failed: L'inscription a échoué
voice:
- call_already_accepted: '%{agent_name} is already handling the call.'
+ call_already_accepted: '%{agent_name} traite déjà l''appel.'
assignment_policy:
not_found: Assignment policy not found
attachments:
@@ -106,11 +106,11 @@ fr:
not_enabled: 'Calling is not enabled for this inbox'
no_recording: 'No recording file provided'
no_message: 'Call has no associated message'
- sdp_offer_required: 'sdp_offer is required'
- contact_phone_required: 'Contact phone number is required'
- permission_request_failed: 'Failed to send call permission request'
+ sdp_offer_required: 'sdp_offer est requis'
+ contact_phone_required: 'Le numéro de téléphone du contact est requis'
+ permission_request_failed: 'Échec de l''envoi de la demande d''autorisation d''appel'
openai:
- invalid_api_key: 'OpenAI API key is invalid or revoked. Please check your key in your OpenAI dashboard.'
+ invalid_api_key: 'La clé API OpenAI est invalide ou révoquée. Veuillez vérifier votre clé dans votre tableau de bord OpenAI.'
inboxes:
imap:
socket_error: Veuillez vérifier la connexion, l'adresse IMAP et réessayez.
@@ -241,9 +241,10 @@ fr:
whatsapp:
list_button_label: 'Choose an item'
call_permission_request_body: 'We would like to call you regarding your conversation.'
+ unsupported_message: 'This message is unavailable.'
voice_call:
- twilio: 'Voice Call'
- whatsapp: 'WhatsApp Call'
+ twilio: 'Appel vocal'
+ whatsapp: 'Appel WhatsApp'
delivery_status:
error_code: 'Code d''erreur : %{error_code}'
activity:
@@ -289,8 +290,8 @@ fr:
issue_linked: 'Linear issue %{issue_id} was linked by %{user_name}'
issue_unlinked: 'Linear issue %{issue_id} was unlinked by %{user_name}'
whatsapp_call:
- permission_requested: 'Sent a call permission request to %{contact_name}.'
- permission_granted: '%{contact_name} accepted the call permission request.'
+ permission_requested: 'A envoyé une demande d''autorisation d''appel à %{contact_name}.'
+ permission_granted: '%{contact_name} a accepté la demande d''autorisation d''appel.'
csat:
not_sent_due_to_messaging_window: 'CSAT survey not sent due to outgoing message restrictions'
auto_resolve:
@@ -355,7 +356,7 @@ fr:
name: 'Linear'
short_description: 'Create and link Linear issues directly from conversations.'
description: 'Create issues in Linear directly from your conversation window. Alternatively, link existing Linear issues for a more streamlined and efficient issue tracking process.'
- attachment_link_title: 'Conversation (#%{conversation_id}) with %{name}'
+ attachment_link_title: 'Conversation (#%{conversation_id}) avec %{name}'
notion:
name: 'Notion'
short_description: 'Integrate databases, documents and pages directly with Captain.'
@@ -386,7 +387,7 @@ fr:
pdf_size_error: 'must be less than 10MB'
sync_not_supported_for_pdf: 'Sync is not supported for PDF documents'
sync_only_available_documents: 'Sync is only available for processed documents'
- sync_already_in_progress: 'Document sync is already in progress'
+ sync_already_in_progress: 'La synchronisation du document est déjà en cours'
pdf_upload_failed: 'Failed to upload PDF to OpenAI'
pdf_upload_success: 'PDF uploaded successfully with file_id: %{file_id}'
pdf_processing_failed: 'Failed to process PDF document %{document_id}: %{error}'
@@ -412,9 +413,16 @@ fr:
empty_placeholder: Aucun résultat trouvé.
loading_placeholder: Recherche en cours...
results_title: Résultats de recherche
+ results: Résultats de recherche
+ results_for: "Search Results for '%{query}'"
+ no_results: "No results found for '%{query}'"
+ found_results:
+ one: Found 1 result
+ other: 'Found %{count} results'
+ submit: Rechercher
toc_header: 'Sur cette page'
sidebar:
- help_center: "Centre d'aide"
+ help_center: Centre d'aide
categories: Catégories
language: Langue
theme: Thème
@@ -443,6 +451,8 @@ fr:
others: autres
by: Par
no_articles: Il n'y a pas d'articles ici
+ previous: Précédent
+ next: Suivant
article_actions:
label: Ouvrir dans
view_markdown: Voir en Markdown
@@ -455,6 +465,7 @@ fr:
go_to_homepage: Site internet
visit_website: Visit website
appearance:
+ title: Apparence
system: Système
light: Clair
dark: Sombre
@@ -521,9 +532,9 @@ fr:
articles:
captain_not_available: 'Translation requires Captain to be enabled for this account'
locale_not_available: 'Locale not available in this portal'
- category_not_found: 'Category not found in this portal'
- no_articles_found: 'No articles found to process'
- invalid_status: 'Invalid status value'
+ category_not_found: 'Catégorie introuvable dans ce portail'
+ no_articles_found: 'Aucun article à traiter'
+ invalid_status: 'Valeur de statut invalide'
send_instructions:
email_required: 'L''e-mail est requis'
invalid_email_format: 'Invalid email format'
@@ -534,7 +545,7 @@ fr:
custom_domain_not_configured: 'Custom domain is not configured'
super_admin:
push_diagnostics:
- user_not_found: 'User not found.'
+ user_not_found: 'Utilisateur introuvable.'
no_subscriptions_to_test: 'Select at least one subscription to test.'
no_subscriptions_to_delete: 'Select at least one subscription to delete.'
subscriptions_deleted: "Deleted %{count} subscription(s). The user's device(s) will re-register on next app launch."
diff --git a/config/locales/he.yml b/config/locales/he.yml
index 7f6439e6d..39ad787fc 100644
--- a/config/locales/he.yml
+++ b/config/locales/he.yml
@@ -241,6 +241,7 @@ he:
whatsapp:
list_button_label: 'בחר פריט'
call_permission_request_body: 'We would like to call you regarding your conversation.'
+ unsupported_message: 'This message is unavailable.'
voice_call:
twilio: 'Voice Call'
whatsapp: 'WhatsApp Call'
@@ -412,7 +413,32 @@ he:
empty_placeholder: לא נמצאו תוצאות.
loading_placeholder: מחפש...
results_title: תוצאות חיפוש
+ results: תוצאות חיפוש
+ results_for: "Search Results for '%{query}'"
+ no_results: "No results found for '%{query}'"
+ found_results:
+ one: Found 1 result
+ two: 'Found %{count} results'
+ many: 'Found %{count} results'
+ other: 'Found %{count} results'
+ submit: חפש
toc_header: 'בעמוד זה'
+ sidebar:
+ help_center: מרכז עזרה
+ categories: קטגוריות
+ language: שפה
+ theme: Theme
+ open_sidebar: פתח סרגל צד
+ close_sidebar: סגור סרגל צד
+ browse: Browse
+ back_to_category: Back to %{name}
+ browse_by_topic: Browse by topic
+ browse_by_topic_subtitle: Find guides, tutorials, and answers organised by category.
+ popular_articles: Popular articles
+ popular_articles_subtitle: What other people are reading right now.
+ popular_label: 'Popular topics:'
+ authors_others: "%{names} and %{count} others"
+ primary_nav: Primary
hero:
sub_title: חפש את המאמרים כאן או עיין בקטגוריות למטה.
common:
@@ -427,12 +453,21 @@ he:
others: אחרים
by: על ידי
no_articles: אין כאן מאמרים
+ previous: Previous
+ next: Next
+ article_actions:
+ label: Open in
+ view_markdown: View as Markdown
+ open_in_chatgpt: Open in ChatGPT
+ open_in_claude: Open in Claude
+ llm_prompt: "Read the following article and help me understand it: %{url}"
footer:
made_with: נוצר עם
header:
go_to_homepage: אתר
visit_website: בקר באתר
appearance:
+ title: מראה
system: מערכת
light: בהיר
dark: כהה
diff --git a/config/locales/hi.yml b/config/locales/hi.yml
index 6f9a44e7c..06fabd7d5 100644
--- a/config/locales/hi.yml
+++ b/config/locales/hi.yml
@@ -241,6 +241,7 @@ hi:
whatsapp:
list_button_label: 'Choose an item'
call_permission_request_body: 'We would like to call you regarding your conversation.'
+ unsupported_message: 'This message is unavailable.'
voice_call:
twilio: 'Voice Call'
whatsapp: 'WhatsApp Call'
@@ -412,7 +413,30 @@ hi:
empty_placeholder: No results found.
loading_placeholder: Searching...
results_title: Search results
+ results: Search Results
+ results_for: "Search Results for '%{query}'"
+ no_results: "No results found for '%{query}'"
+ found_results:
+ one: Found 1 result
+ other: 'Found %{count} results'
+ submit: Search
toc_header: 'On this page'
+ sidebar:
+ help_center: Help Center
+ categories: Categories
+ language: Language
+ theme: Theme
+ open_sidebar: Open sidebar
+ close_sidebar: Close sidebar
+ browse: Browse
+ back_to_category: Back to %{name}
+ browse_by_topic: Browse by topic
+ browse_by_topic_subtitle: Find guides, tutorials, and answers organised by category.
+ popular_articles: Popular articles
+ popular_articles_subtitle: What other people are reading right now.
+ popular_label: 'Popular topics:'
+ authors_others: "%{names} and %{count} others"
+ primary_nav: Primary
hero:
sub_title: Search for the articles here or browse the categories below.
common:
@@ -427,12 +451,21 @@ hi:
others: others
by: By
no_articles: There are no articles here
+ previous: Previous
+ next: Next
+ article_actions:
+ label: Open in
+ view_markdown: View as Markdown
+ open_in_chatgpt: Open in ChatGPT
+ open_in_claude: Open in Claude
+ llm_prompt: "Read the following article and help me understand it: %{url}"
footer:
made_with: Made with
header:
go_to_homepage: Website
visit_website: Visit website
appearance:
+ title: Appearance
system: System
light: Light
dark: Dark
diff --git a/config/locales/hr.yml b/config/locales/hr.yml
index ea2a83880..2cbae5b61 100644
--- a/config/locales/hr.yml
+++ b/config/locales/hr.yml
@@ -241,6 +241,7 @@ hr:
whatsapp:
list_button_label: 'Choose an item'
call_permission_request_body: 'We would like to call you regarding your conversation.'
+ unsupported_message: 'This message is unavailable.'
voice_call:
twilio: 'Voice Call'
whatsapp: 'WhatsApp Call'
@@ -412,7 +413,31 @@ hr:
empty_placeholder: No results found.
loading_placeholder: Searching...
results_title: Search results
+ results: Search Results
+ results_for: "Search Results for '%{query}'"
+ no_results: "No results found for '%{query}'"
+ found_results:
+ one: Found 1 result
+ few: 'Found %{count} results'
+ other: 'Found %{count} results'
+ submit: Search
toc_header: 'On this page'
+ sidebar:
+ help_center: Centar za pomoć
+ categories: Categories
+ language: Jezik
+ theme: Theme
+ open_sidebar: Open sidebar
+ close_sidebar: Close sidebar
+ browse: Browse
+ back_to_category: Back to %{name}
+ browse_by_topic: Browse by topic
+ browse_by_topic_subtitle: Find guides, tutorials, and answers organised by category.
+ popular_articles: Popular articles
+ popular_articles_subtitle: What other people are reading right now.
+ popular_label: 'Popular topics:'
+ authors_others: "%{names} and %{count} others"
+ primary_nav: Primary
hero:
sub_title: Search for the articles here or browse the categories below.
common:
@@ -427,12 +452,21 @@ hr:
others: others
by: By
no_articles: There are no articles here
+ previous: Previous
+ next: Next
+ article_actions:
+ label: Open in
+ view_markdown: View as Markdown
+ open_in_chatgpt: Open in ChatGPT
+ open_in_claude: Open in Claude
+ llm_prompt: "Read the following article and help me understand it: %{url}"
footer:
made_with: Made with
header:
go_to_homepage: Website
visit_website: Visit website
appearance:
+ title: Appearance
system: System
light: Light
dark: Dark
diff --git a/config/locales/hu.yml b/config/locales/hu.yml
index 2de754354..64133fab1 100644
--- a/config/locales/hu.yml
+++ b/config/locales/hu.yml
@@ -241,6 +241,7 @@ hu:
whatsapp:
list_button_label: 'Choose an item'
call_permission_request_body: 'We would like to call you regarding your conversation.'
+ unsupported_message: 'This message is unavailable.'
voice_call:
twilio: 'Voice Call'
whatsapp: 'WhatsApp Call'
@@ -412,7 +413,30 @@ hu:
empty_placeholder: Nincs találat.
loading_placeholder: Keresés...
results_title: Keresés eredménye
+ results: Keresés eredménye
+ results_for: "Search Results for '%{query}'"
+ no_results: "No results found for '%{query}'"
+ found_results:
+ one: Found 1 result
+ other: 'Found %{count} results'
+ submit: Keresés
toc_header: 'Ezen az oldalon'
+ sidebar:
+ help_center: Súgóközpont
+ categories: Kategóriák
+ language: Nyelv
+ theme: Theme
+ open_sidebar: Oldalsáv megnyitása
+ close_sidebar: Oldalsáv becsukás
+ browse: Browse
+ back_to_category: Back to %{name}
+ browse_by_topic: Browse by topic
+ browse_by_topic_subtitle: Find guides, tutorials, and answers organised by category.
+ popular_articles: Popular articles
+ popular_articles_subtitle: What other people are reading right now.
+ popular_label: 'Popular topics:'
+ authors_others: "%{names} and %{count} others"
+ primary_nav: Primary
hero:
sub_title: Keress bejegyzéseket, vagy válassz a kategóriákból lejjebb.
common:
@@ -427,12 +451,21 @@ hu:
others: egyebek
by: Által
no_articles: Nincsenek bejegyzések
+ previous: Previous
+ next: Next
+ article_actions:
+ label: Open in
+ view_markdown: View as Markdown
+ open_in_chatgpt: Open in ChatGPT
+ open_in_claude: Open in Claude
+ llm_prompt: "Read the following article and help me understand it: %{url}"
footer:
made_with: 'Ezzel készítve: '
header:
go_to_homepage: Honlap
visit_website: Visit website
appearance:
+ title: Megjelenés
system: Rendszer
light: Világos mód
dark: Sötét mód
diff --git a/config/locales/hy.yml b/config/locales/hy.yml
index 28845eaf3..664459655 100644
--- a/config/locales/hy.yml
+++ b/config/locales/hy.yml
@@ -241,6 +241,7 @@ hy:
whatsapp:
list_button_label: 'Choose an item'
call_permission_request_body: 'We would like to call you regarding your conversation.'
+ unsupported_message: 'This message is unavailable.'
voice_call:
twilio: 'Voice Call'
whatsapp: 'WhatsApp Call'
@@ -412,7 +413,30 @@ hy:
empty_placeholder: Արդյունքներ չեն գտնվել։
loading_placeholder: Որոնում...
results_title: Որոնման արդյունքներ
+ results: Որոնման արդյունքներ
+ results_for: "Search Results for '%{query}'"
+ no_results: "No results found for '%{query}'"
+ found_results:
+ one: Found 1 result
+ other: 'Found %{count} results'
+ submit: Որոնել
toc_header: 'Այս էջում'
+ sidebar:
+ help_center: Օգնության կենտրոն
+ categories: Կատեգորիաներ
+ language: Language
+ theme: Theme
+ open_sidebar: Բացել կողային վահանակը
+ close_sidebar: Փակել կողային վահանակը
+ browse: Browse
+ back_to_category: Back to %{name}
+ browse_by_topic: Browse by topic
+ browse_by_topic_subtitle: Find guides, tutorials, and answers organised by category.
+ popular_articles: Popular articles
+ popular_articles_subtitle: What other people are reading right now.
+ popular_label: 'Popular topics:'
+ authors_others: "%{names} and %{count} others"
+ primary_nav: Primary
hero:
sub_title: Որոնեք հոդվածներ այստեղ կամ դիտեք ստորև ներկայացված կատեգորիաները։
common:
@@ -427,12 +451,21 @@ hy:
others: այլք
by: Ըստ
no_articles: Այստեղ հոդվածներ չկան
+ previous: Previous
+ next: Next
+ article_actions:
+ label: Open in
+ view_markdown: View as Markdown
+ open_in_chatgpt: Open in ChatGPT
+ open_in_claude: Open in Claude
+ llm_prompt: "Read the following article and help me understand it: %{url}"
footer:
made_with: Ստեղծված է
header:
go_to_homepage: Կայք
visit_website: Visit website
appearance:
+ title: Appearance
system: Համակարգ
light: Լուսավոր
dark: Մութ
diff --git a/config/locales/id.yml b/config/locales/id.yml
index 246c7e1c3..f465157a5 100644
--- a/config/locales/id.yml
+++ b/config/locales/id.yml
@@ -241,6 +241,7 @@ id:
whatsapp:
list_button_label: 'Choose an item'
call_permission_request_body: 'We would like to call you regarding your conversation.'
+ unsupported_message: 'This message is unavailable.'
voice_call:
twilio: 'Voice Call'
whatsapp: 'WhatsApp Call'
@@ -412,7 +413,29 @@ id:
empty_placeholder: Tidak ada hasil ditemukan.
loading_placeholder: Sedang mencari...
results_title: Hasil pencarian
+ results: Hasil Pencarian
+ results_for: "Search Results for '%{query}'"
+ no_results: "No results found for '%{query}'"
+ found_results:
+ other: 'Found %{count} results'
+ submit: Cari
toc_header: 'Di halaman ini'
+ sidebar:
+ help_center: Pusat Bantuan
+ categories: Kategori
+ language: Bahasa
+ theme: Theme
+ open_sidebar: Buka sidebar
+ close_sidebar: Tutup sidebar
+ browse: Browse
+ back_to_category: Back to %{name}
+ browse_by_topic: Browse by topic
+ browse_by_topic_subtitle: Find guides, tutorials, and answers organised by category.
+ popular_articles: Popular articles
+ popular_articles_subtitle: What other people are reading right now.
+ popular_label: 'Popular topics:'
+ authors_others: "%{names} and %{count} others"
+ primary_nav: Primary
hero:
sub_title: Cari artikel di sini atau jelajahi kategori di bawah ini.
common:
@@ -427,12 +450,21 @@ id:
others: others
by: By
no_articles: Tidak ada artikel di sini
+ previous: Previous
+ next: Next
+ article_actions:
+ label: Open in
+ view_markdown: View as Markdown
+ open_in_chatgpt: Open in ChatGPT
+ open_in_claude: Open in Claude
+ llm_prompt: "Read the following article and help me understand it: %{url}"
footer:
made_with: Dibuat oleh
header:
go_to_homepage: Website
visit_website: Visit website
appearance:
+ title: Appearance
system: Sistem
light: Light
dark: Dark
diff --git a/config/locales/is.yml b/config/locales/is.yml
index c20c24820..840ac4f4f 100644
--- a/config/locales/is.yml
+++ b/config/locales/is.yml
@@ -241,6 +241,7 @@ is:
whatsapp:
list_button_label: 'Choose an item'
call_permission_request_body: 'We would like to call you regarding your conversation.'
+ unsupported_message: 'This message is unavailable.'
voice_call:
twilio: 'Voice Call'
whatsapp: 'WhatsApp Call'
@@ -412,7 +413,30 @@ is:
empty_placeholder: Engar niðurstöður fundust.
loading_placeholder: Searching...
results_title: Search results
+ results: Leitarniðurstöður
+ results_for: "Search Results for '%{query}'"
+ no_results: "No results found for '%{query}'"
+ found_results:
+ one: Found 1 result
+ other: 'Found %{count} results'
+ submit: Leit
toc_header: 'On this page'
+ sidebar:
+ help_center: Help Center
+ categories: Categories
+ language: Language
+ theme: Theme
+ open_sidebar: Open sidebar
+ close_sidebar: Close sidebar
+ browse: Browse
+ back_to_category: Back to %{name}
+ browse_by_topic: Browse by topic
+ browse_by_topic_subtitle: Find guides, tutorials, and answers organised by category.
+ popular_articles: Popular articles
+ popular_articles_subtitle: What other people are reading right now.
+ popular_label: 'Popular topics:'
+ authors_others: "%{names} and %{count} others"
+ primary_nav: Primary
hero:
sub_title: Leitaðu að greinunum hér eða skoðaðu flokkana hér að neðan.
common:
@@ -427,12 +451,21 @@ is:
others: others
by: By
no_articles: There are no articles here
+ previous: Previous
+ next: Next
+ article_actions:
+ label: Open in
+ view_markdown: View as Markdown
+ open_in_chatgpt: Open in ChatGPT
+ open_in_claude: Open in Claude
+ llm_prompt: "Read the following article and help me understand it: %{url}"
footer:
made_with: Made with
header:
go_to_homepage: Website
visit_website: Visit website
appearance:
+ title: Appearance
system: System
light: Light
dark: Dark
diff --git a/config/locales/it.yml b/config/locales/it.yml
index 3b132a9d7..54e2c218f 100644
--- a/config/locales/it.yml
+++ b/config/locales/it.yml
@@ -241,6 +241,7 @@ it:
whatsapp:
list_button_label: 'Scegli un elemento'
call_permission_request_body: 'Vorremmo chiamarti riguardo alla tua conversazione.'
+ unsupported_message: 'This message is unavailable.'
voice_call:
twilio: 'Chiamata Vocale'
whatsapp: 'Chiamata WhatsApp'
@@ -412,7 +413,30 @@ it:
empty_placeholder: Nessun risultato trovato.
loading_placeholder: Ricerca...
results_title: Risultati di ricerca
+ results: Risultati di Ricerca
+ results_for: "Search Results for '%{query}'"
+ no_results: "No results found for '%{query}'"
+ found_results:
+ one: Found 1 result
+ other: 'Found %{count} results'
+ submit: Cerca
toc_header: 'Su questa pagina'
+ sidebar:
+ help_center: Help Center
+ categories: Categorie
+ language: Lingua
+ theme: Theme
+ open_sidebar: Apri barra laterale
+ close_sidebar: Chiudi barra laterale
+ browse: Browse
+ back_to_category: Back to %{name}
+ browse_by_topic: Browse by topic
+ browse_by_topic_subtitle: Find guides, tutorials, and answers organised by category.
+ popular_articles: Popular articles
+ popular_articles_subtitle: What other people are reading right now.
+ popular_label: 'Popular topics:'
+ authors_others: "%{names} and %{count} others"
+ primary_nav: Primary
hero:
sub_title: Cerca gli articoli qui oppure sfoglia le categorie qui sotto.
common:
@@ -427,12 +451,21 @@ it:
others: altri
by: Da
no_articles: Non ci sono articoli qui
+ previous: Precedente
+ next: Successivo
+ article_actions:
+ label: Open in
+ view_markdown: View as Markdown
+ open_in_chatgpt: Open in ChatGPT
+ open_in_claude: Open in Claude
+ llm_prompt: "Read the following article and help me understand it: %{url}"
footer:
made_with: Realizzato con
header:
go_to_homepage: Sito Web
visit_website: Visita sito
appearance:
+ title: Aspetto
system: Sistema
light: Chiaro
dark: Scuro
diff --git a/config/locales/ja.yml b/config/locales/ja.yml
index f1409a55d..e6a0fbaa2 100644
--- a/config/locales/ja.yml
+++ b/config/locales/ja.yml
@@ -241,6 +241,7 @@ ja:
whatsapp:
list_button_label: 'Choose an item'
call_permission_request_body: 'We would like to call you regarding your conversation.'
+ unsupported_message: 'This message is unavailable.'
voice_call:
twilio: 'Voice Call'
whatsapp: 'WhatsApp Call'
@@ -412,7 +413,29 @@ ja:
empty_placeholder: 該当結果が見つかりませんでした。
loading_placeholder: 検索中...
results_title: 検索結果
+ results: 検索結果
+ results_for: "Search Results for '%{query}'"
+ no_results: "No results found for '%{query}'"
+ found_results:
+ other: 'Found %{count} results'
+ submit: 検索
toc_header: 'このページで'
+ sidebar:
+ help_center: ヘルプセンター
+ categories: カテゴリー
+ language: 言語
+ theme: Theme
+ open_sidebar: サイドバーを開く
+ close_sidebar: サイドバーを閉じる
+ browse: Browse
+ back_to_category: Back to %{name}
+ browse_by_topic: Browse by topic
+ browse_by_topic_subtitle: Find guides, tutorials, and answers organised by category.
+ popular_articles: Popular articles
+ popular_articles_subtitle: What other people are reading right now.
+ popular_label: 'Popular topics:'
+ authors_others: "%{names} and %{count} others"
+ primary_nav: Primary
hero:
sub_title: ここで記事を検索するか、以下のカテゴリを参照してください。
common:
@@ -427,12 +450,21 @@ ja:
others: その他
by: 作成者
no_articles: ここには記事がありません
+ previous: Previous
+ next: Next
+ article_actions:
+ label: Open in
+ view_markdown: View as Markdown
+ open_in_chatgpt: Open in ChatGPT
+ open_in_claude: Open in Claude
+ llm_prompt: "Read the following article and help me understand it: %{url}"
footer:
made_with: 作成
header:
go_to_homepage: ウェブサイト
visit_website: Visit website
appearance:
+ title: 外観
system: システム
light: ライト
dark: ダーク
diff --git a/config/locales/ka.yml b/config/locales/ka.yml
index 8272e6eda..81f2921f7 100644
--- a/config/locales/ka.yml
+++ b/config/locales/ka.yml
@@ -241,6 +241,7 @@ ka:
whatsapp:
list_button_label: 'Choose an item'
call_permission_request_body: 'We would like to call you regarding your conversation.'
+ unsupported_message: 'This message is unavailable.'
voice_call:
twilio: 'Voice Call'
whatsapp: 'WhatsApp Call'
@@ -412,7 +413,30 @@ ka:
empty_placeholder: No results found.
loading_placeholder: Searching...
results_title: Search results
+ results: Search Results
+ results_for: "Search Results for '%{query}'"
+ no_results: "No results found for '%{query}'"
+ found_results:
+ one: Found 1 result
+ other: 'Found %{count} results'
+ submit: ძებნა
toc_header: 'On this page'
+ sidebar:
+ help_center: დახმარების ცენტრი
+ categories: კატეგორიები
+ language: Language
+ theme: Theme
+ open_sidebar: გვერდითი პანელის გახსნა
+ close_sidebar: გვერდითი პანელის დახურვა
+ browse: Browse
+ back_to_category: Back to %{name}
+ browse_by_topic: Browse by topic
+ browse_by_topic_subtitle: Find guides, tutorials, and answers organised by category.
+ popular_articles: Popular articles
+ popular_articles_subtitle: What other people are reading right now.
+ popular_label: 'Popular topics:'
+ authors_others: "%{names} and %{count} others"
+ primary_nav: Primary
hero:
sub_title: Search for the articles here or browse the categories below.
common:
@@ -427,12 +451,21 @@ ka:
others: others
by: By
no_articles: There are no articles here
+ previous: Previous
+ next: Next
+ article_actions:
+ label: Open in
+ view_markdown: View as Markdown
+ open_in_chatgpt: Open in ChatGPT
+ open_in_claude: Open in Claude
+ llm_prompt: "Read the following article and help me understand it: %{url}"
footer:
made_with: Made with
header:
go_to_homepage: Website
visit_website: Visit website
appearance:
+ title: Appearance
system: System
light: Light
dark: Dark
diff --git a/config/locales/ko.yml b/config/locales/ko.yml
index c52f3525b..e50dac85a 100644
--- a/config/locales/ko.yml
+++ b/config/locales/ko.yml
@@ -241,6 +241,7 @@ ko:
whatsapp:
list_button_label: '항목 선택'
call_permission_request_body: 'We would like to call you regarding your conversation.'
+ unsupported_message: 'This message is unavailable.'
voice_call:
twilio: 'Voice Call'
whatsapp: 'WhatsApp Call'
@@ -412,7 +413,29 @@ ko:
empty_placeholder: 검색 결과가 없습니다.
loading_placeholder: 검색중...
results_title: 검색 결과
+ results: 검색 결과
+ results_for: "Search Results for '%{query}'"
+ no_results: "No results found for '%{query}'"
+ found_results:
+ other: 'Found %{count} results'
+ submit: 검색
toc_header: '이 페이지에서'
+ sidebar:
+ help_center: 도움말 센터
+ categories: 카테고리
+ language: 언어
+ theme: Theme
+ open_sidebar: 사이드바 열기
+ close_sidebar: 사이드바 닫기
+ browse: Browse
+ back_to_category: Back to %{name}
+ browse_by_topic: Browse by topic
+ browse_by_topic_subtitle: Find guides, tutorials, and answers organised by category.
+ popular_articles: Popular articles
+ popular_articles_subtitle: What other people are reading right now.
+ popular_label: 'Popular topics:'
+ authors_others: "%{names} and %{count} others"
+ primary_nav: Primary
hero:
sub_title: 게시물을 여기서 검색하거나 아래에서 카테고리를 탐색해보세요.
common:
@@ -427,12 +450,21 @@ ko:
others: 기타
by: 작성자
no_articles: 게시물이 없습니다.
+ previous: 이전
+ next: 다음
+ article_actions:
+ label: Open in
+ view_markdown: View as Markdown
+ open_in_chatgpt: Open in ChatGPT
+ open_in_claude: Open in Claude
+ llm_prompt: "Read the following article and help me understand it: %{url}"
footer:
made_with: 제작 도구
header:
go_to_homepage: 홈페이지
visit_website: 웹사이트 방문
appearance:
+ title: 외관
system: 시스템
light: 밝게
dark: 어둡게
diff --git a/config/locales/lt.yml b/config/locales/lt.yml
index 8e78bccf8..eaf3ff8ff 100644
--- a/config/locales/lt.yml
+++ b/config/locales/lt.yml
@@ -241,6 +241,7 @@ lt:
whatsapp:
list_button_label: 'Choose an item'
call_permission_request_body: 'We would like to call you regarding your conversation.'
+ unsupported_message: 'This message is unavailable.'
voice_call:
twilio: 'Voice Call'
whatsapp: 'WhatsApp Call'
@@ -412,7 +413,32 @@ lt:
empty_placeholder: Nieko nerasta.
loading_placeholder: Ieškoma...
results_title: Paieškos rezultatai
+ results: Rezultatų Paieška
+ results_for: "Search Results for '%{query}'"
+ no_results: "No results found for '%{query}'"
+ found_results:
+ one: Found 1 result
+ few: 'Found %{count} results'
+ many: 'Found %{count} results'
+ other: 'Found %{count} results'
+ submit: Ieškoti
toc_header: 'Šitame puslapyje'
+ sidebar:
+ help_center: Palaikymo centras
+ categories: Kategorijos
+ language: Kalba
+ theme: Theme
+ open_sidebar: Atidaryti šoninę juostą
+ close_sidebar: Uždaryti šoninę juostą
+ browse: Browse
+ back_to_category: Back to %{name}
+ browse_by_topic: Browse by topic
+ browse_by_topic_subtitle: Find guides, tutorials, and answers organised by category.
+ popular_articles: Popular articles
+ popular_articles_subtitle: What other people are reading right now.
+ popular_label: 'Popular topics:'
+ authors_others: "%{names} and %{count} others"
+ primary_nav: Primary
hero:
sub_title: Ieškokite straipsnių čia arba naršykite toliau pateiktose kategorijose.
common:
@@ -427,12 +453,21 @@ lt:
others: kiti
by: Autorius
no_articles: Čia nėra straipsnių
+ previous: Previous
+ next: Next
+ article_actions:
+ label: Open in
+ view_markdown: View as Markdown
+ open_in_chatgpt: Open in ChatGPT
+ open_in_claude: Open in Claude
+ llm_prompt: "Read the following article and help me understand it: %{url}"
footer:
made_with: Padaryta su
header:
go_to_homepage: Internetinis puslapis
visit_website: Visit website
appearance:
+ title: Išvaizda
system: Sistema
light: Šviesus
dark: Tamsus
diff --git a/config/locales/lv.yml b/config/locales/lv.yml
index 7f28dbca0..c8e45b2a4 100644
--- a/config/locales/lv.yml
+++ b/config/locales/lv.yml
@@ -241,6 +241,7 @@ lv:
whatsapp:
list_button_label: 'Choose an item'
call_permission_request_body: 'We would like to call you regarding your conversation.'
+ unsupported_message: 'This message is unavailable.'
voice_call:
twilio: 'Voice Call'
whatsapp: 'WhatsApp Call'
@@ -412,7 +413,31 @@ lv:
empty_placeholder: Nav atrasts.
loading_placeholder: Meklēšana...
results_title: Meklēšanas rezultāti
+ results: Meklēšanas Rezultāti
+ results_for: "Search Results for '%{query}'"
+ no_results: "No results found for '%{query}'"
+ found_results:
+ zero: 'Found %{count} results'
+ one: Found 1 result
+ other: 'Found %{count} results'
+ submit: Meklēt
toc_header: 'Šajā lapā'
+ sidebar:
+ help_center: Palīdzības centrs
+ categories: Kategorijas
+ language: Valoda
+ theme: Theme
+ open_sidebar: Atvērt sānjoslu
+ close_sidebar: Aizvērt sānjoslu
+ browse: Browse
+ back_to_category: Back to %{name}
+ browse_by_topic: Browse by topic
+ browse_by_topic_subtitle: Find guides, tutorials, and answers organised by category.
+ popular_articles: Popular articles
+ popular_articles_subtitle: What other people are reading right now.
+ popular_label: 'Popular topics:'
+ authors_others: "%{names} and %{count} others"
+ primary_nav: Primary
hero:
sub_title: Meklējiet rakstus šeit, vai pārlūkojiet tālāk norādītās kategorijas.
common:
@@ -427,12 +452,21 @@ lv:
others: citi
by: autors
no_articles: Šeit nav neviena raksta
+ previous: Previous
+ next: Next
+ article_actions:
+ label: Open in
+ view_markdown: View as Markdown
+ open_in_chatgpt: Open in ChatGPT
+ open_in_claude: Open in Claude
+ llm_prompt: "Read the following article and help me understand it: %{url}"
footer:
made_with: Veidots ar
header:
go_to_homepage: Tīmekļa vietne
visit_website: Visit website
appearance:
+ title: Izskats
system: Sistēma
light: Gaišs
dark: Tumšs
diff --git a/config/locales/ml.yml b/config/locales/ml.yml
index 583e033b2..7dee9f525 100644
--- a/config/locales/ml.yml
+++ b/config/locales/ml.yml
@@ -241,6 +241,7 @@ ml:
whatsapp:
list_button_label: 'Choose an item'
call_permission_request_body: 'We would like to call you regarding your conversation.'
+ unsupported_message: 'This message is unavailable.'
voice_call:
twilio: 'Voice Call'
whatsapp: 'WhatsApp Call'
@@ -412,7 +413,30 @@ ml:
empty_placeholder: ഒരു ഫലവും കണ്ടെത്താനായില്ല.
loading_placeholder: Searching...
results_title: Search results
+ results: Search Results
+ results_for: "Search Results for '%{query}'"
+ no_results: "No results found for '%{query}'"
+ found_results:
+ one: Found 1 result
+ other: 'Found %{count} results'
+ submit: തിരയുക
toc_header: 'On this page'
+ sidebar:
+ help_center: സഹായ കേന്ദ്രം
+ categories: Categories
+ language: Language
+ theme: Theme
+ open_sidebar: Open sidebar
+ close_sidebar: Close sidebar
+ browse: Browse
+ back_to_category: Back to %{name}
+ browse_by_topic: Browse by topic
+ browse_by_topic_subtitle: Find guides, tutorials, and answers organised by category.
+ popular_articles: Popular articles
+ popular_articles_subtitle: What other people are reading right now.
+ popular_label: 'Popular topics:'
+ authors_others: "%{names} and %{count} others"
+ primary_nav: Primary
hero:
sub_title: Search for the articles here or browse the categories below.
common:
@@ -427,12 +451,21 @@ ml:
others: others
by: By
no_articles: There are no articles here
+ previous: Previous
+ next: Next
+ article_actions:
+ label: Open in
+ view_markdown: View as Markdown
+ open_in_chatgpt: Open in ChatGPT
+ open_in_claude: Open in Claude
+ llm_prompt: "Read the following article and help me understand it: %{url}"
footer:
made_with: Made with
header:
go_to_homepage: Website
visit_website: Visit website
appearance:
+ title: Appearance
system: System
light: Light
dark: Dark
diff --git a/config/locales/ms.yml b/config/locales/ms.yml
index e506a8825..095c8b9e3 100644
--- a/config/locales/ms.yml
+++ b/config/locales/ms.yml
@@ -241,6 +241,7 @@ ms:
whatsapp:
list_button_label: 'Choose an item'
call_permission_request_body: 'We would like to call you regarding your conversation.'
+ unsupported_message: 'This message is unavailable.'
voice_call:
twilio: 'Voice Call'
whatsapp: 'WhatsApp Call'
@@ -412,7 +413,29 @@ ms:
empty_placeholder: Tiada dijumpa.
loading_placeholder: Searching...
results_title: Search results
+ results: Keputusan Carian
+ results_for: "Search Results for '%{query}'"
+ no_results: "No results found for '%{query}'"
+ found_results:
+ other: 'Found %{count} results'
+ submit: Cari
toc_header: 'On this page'
+ sidebar:
+ help_center: Pusat Bantuan
+ categories: Kategori
+ language: Language
+ theme: Theme
+ open_sidebar: Buka bar sisi
+ close_sidebar: Tutup bar sisi
+ browse: Browse
+ back_to_category: Back to %{name}
+ browse_by_topic: Browse by topic
+ browse_by_topic_subtitle: Find guides, tutorials, and answers organised by category.
+ popular_articles: Popular articles
+ popular_articles_subtitle: What other people are reading right now.
+ popular_label: 'Popular topics:'
+ authors_others: "%{names} and %{count} others"
+ primary_nav: Primary
hero:
sub_title: Search for the articles here or browse the categories below.
common:
@@ -427,12 +450,21 @@ ms:
others: others
by: By
no_articles: There are no articles here
+ previous: Previous
+ next: Next
+ article_actions:
+ label: Open in
+ view_markdown: View as Markdown
+ open_in_chatgpt: Open in ChatGPT
+ open_in_claude: Open in Claude
+ llm_prompt: "Read the following article and help me understand it: %{url}"
footer:
made_with: Made with
header:
go_to_homepage: Website
visit_website: Visit website
appearance:
+ title: Appearance
system: System
light: Light
dark: Dark
diff --git a/config/locales/ne.yml b/config/locales/ne.yml
index 3dc7731ce..cbc3d624f 100644
--- a/config/locales/ne.yml
+++ b/config/locales/ne.yml
@@ -241,6 +241,7 @@ ne:
whatsapp:
list_button_label: 'Choose an item'
call_permission_request_body: 'We would like to call you regarding your conversation.'
+ unsupported_message: 'This message is unavailable.'
voice_call:
twilio: 'Voice Call'
whatsapp: 'WhatsApp Call'
@@ -412,7 +413,30 @@ ne:
empty_placeholder: कुनै परिणाम फेला परेन.
loading_placeholder: खोज्दै...
results_title: खोज परिणामहरू
+ results: खोज परिणामहरू
+ results_for: "Search Results for '%{query}'"
+ no_results: "No results found for '%{query}'"
+ found_results:
+ one: Found 1 result
+ other: 'Found %{count} results'
+ submit: खोज्नुहोस्
toc_header: 'यस पृष्ठमा'
+ sidebar:
+ help_center: मद्दत केन्द्र
+ categories: श्रेणीहरू
+ language: Language
+ theme: Theme
+ open_sidebar: साइडबार खोल्नु
+ close_sidebar: साइडबार बन्द गर्नु
+ browse: Browse
+ back_to_category: Back to %{name}
+ browse_by_topic: Browse by topic
+ browse_by_topic_subtitle: Find guides, tutorials, and answers organised by category.
+ popular_articles: Popular articles
+ popular_articles_subtitle: What other people are reading right now.
+ popular_label: 'Popular topics:'
+ authors_others: "%{names} and %{count} others"
+ primary_nav: Primary
hero:
sub_title: यहाँ लेखहरू खोज्नुहोस् वा तलका श्रेणीहरू हेर्नुहोस्।.
common:
@@ -427,12 +451,21 @@ ne:
others: others
by: द्वारा
no_articles: यहाँ कुनै लेखहरू छैनन्
+ previous: Previous
+ next: Next
+ article_actions:
+ label: Open in
+ view_markdown: View as Markdown
+ open_in_chatgpt: Open in ChatGPT
+ open_in_claude: Open in Claude
+ llm_prompt: "Read the following article and help me understand it: %{url}"
footer:
made_with: Made with
header:
go_to_homepage: वेबसाइट
visit_website: Visit website
appearance:
+ title: Appearance
system: प्रणाली
light: हल्का
dark: गाढा
diff --git a/config/locales/nl.yml b/config/locales/nl.yml
index 97a5ae9a4..7d872bcab 100644
--- a/config/locales/nl.yml
+++ b/config/locales/nl.yml
@@ -241,6 +241,7 @@ nl:
whatsapp:
list_button_label: 'Choose an item'
call_permission_request_body: 'We would like to call you regarding your conversation.'
+ unsupported_message: 'This message is unavailable.'
voice_call:
twilio: 'Voice Call'
whatsapp: 'WhatsApp Call'
@@ -412,7 +413,30 @@ nl:
empty_placeholder: Geen resultaten gevonden.
loading_placeholder: Zoeken...
results_title: Zoekresultaten
+ results: Zoekresultaten
+ results_for: "Search Results for '%{query}'"
+ no_results: "No results found for '%{query}'"
+ found_results:
+ one: Found 1 result
+ other: 'Found %{count} results'
+ submit: Zoeken
toc_header: 'Op deze pagina'
+ sidebar:
+ help_center: Help Center
+ categories: Categories
+ language: Taal
+ theme: Theme
+ open_sidebar: Open sidebar
+ close_sidebar: Close sidebar
+ browse: Browse
+ back_to_category: Back to %{name}
+ browse_by_topic: Browse by topic
+ browse_by_topic_subtitle: Find guides, tutorials, and answers organised by category.
+ popular_articles: Popular articles
+ popular_articles_subtitle: What other people are reading right now.
+ popular_label: 'Popular topics:'
+ authors_others: "%{names} and %{count} others"
+ primary_nav: Primary
hero:
sub_title: Zoek hier naar de artikelen of blader door de onderstaande categorieën.
common:
@@ -427,12 +451,21 @@ nl:
others: others
by: By
no_articles: There are no articles here
+ previous: Previous
+ next: Volgende
+ article_actions:
+ label: Open in
+ view_markdown: View as Markdown
+ open_in_chatgpt: Open in ChatGPT
+ open_in_claude: Open in Claude
+ llm_prompt: "Read the following article and help me understand it: %{url}"
footer:
made_with: Mogelijk gemaakt door
header:
go_to_homepage: Website
visit_website: Visit website
appearance:
+ title: Weergave
system: Systeem
light: Light
dark: Dark
diff --git a/config/locales/no.yml b/config/locales/no.yml
index c311ae85e..5a9caa60c 100644
--- a/config/locales/no.yml
+++ b/config/locales/no.yml
@@ -241,6 +241,7 @@
whatsapp:
list_button_label: 'Choose an item'
call_permission_request_body: 'We would like to call you regarding your conversation.'
+ unsupported_message: 'This message is unavailable.'
voice_call:
twilio: 'Voice Call'
whatsapp: 'WhatsApp Call'
@@ -412,7 +413,30 @@
empty_placeholder: No results found.
loading_placeholder: Searching...
results_title: Search results
+ results: Search Results
+ results_for: "Search Results for '%{query}'"
+ no_results: "No results found for '%{query}'"
+ found_results:
+ one: Found 1 result
+ other: 'Found %{count} results'
+ submit: Søk
toc_header: 'On this page'
+ sidebar:
+ help_center: Help Center
+ categories: Categories
+ language: Language
+ theme: Theme
+ open_sidebar: Open sidebar
+ close_sidebar: Close sidebar
+ browse: Browse
+ back_to_category: Back to %{name}
+ browse_by_topic: Browse by topic
+ browse_by_topic_subtitle: Find guides, tutorials, and answers organised by category.
+ popular_articles: Popular articles
+ popular_articles_subtitle: What other people are reading right now.
+ popular_label: 'Popular topics:'
+ authors_others: "%{names} and %{count} others"
+ primary_nav: Primary
hero:
sub_title: Search for the articles here or browse the categories below.
common:
@@ -427,12 +451,21 @@
others: others
by: By
no_articles: There are no articles here
+ previous: Previous
+ next: Next
+ article_actions:
+ label: Open in
+ view_markdown: View as Markdown
+ open_in_chatgpt: Open in ChatGPT
+ open_in_claude: Open in Claude
+ llm_prompt: "Read the following article and help me understand it: %{url}"
footer:
made_with: Made with
header:
go_to_homepage: Website
visit_website: Visit website
appearance:
+ title: Appearance
system: System
light: Light
dark: Dark
diff --git a/config/locales/pl.yml b/config/locales/pl.yml
index 3c3c0c1be..f4368c230 100644
--- a/config/locales/pl.yml
+++ b/config/locales/pl.yml
@@ -241,6 +241,7 @@ pl:
whatsapp:
list_button_label: 'Choose an item'
call_permission_request_body: 'We would like to call you regarding your conversation.'
+ unsupported_message: 'This message is unavailable.'
voice_call:
twilio: 'Voice Call'
whatsapp: 'WhatsApp Call'
@@ -412,7 +413,32 @@ pl:
empty_placeholder: Brak wyników.
loading_placeholder: Wyszukiwanie...
results_title: Wyniki wyszukiwania
+ results: Wyniki wyszukiwania
+ results_for: "Search Results for '%{query}'"
+ no_results: "No results found for '%{query}'"
+ found_results:
+ one: Found 1 result
+ few: 'Found %{count} results'
+ many: 'Found %{count} results'
+ other: 'Found %{count} results'
+ submit: Szukaj
toc_header: 'Na tej stronie'
+ sidebar:
+ help_center: Centrum pomocy
+ categories: Kategorie
+ language: Język
+ theme: Theme
+ open_sidebar: Otwórz panel boczny
+ close_sidebar: Zamknij panel boczny
+ browse: Browse
+ back_to_category: Back to %{name}
+ browse_by_topic: Browse by topic
+ browse_by_topic_subtitle: Find guides, tutorials, and answers organised by category.
+ popular_articles: Popular articles
+ popular_articles_subtitle: What other people are reading right now.
+ popular_label: 'Popular topics:'
+ authors_others: "%{names} and %{count} others"
+ primary_nav: Primary
hero:
sub_title: Szukaj artykułów tutaj lub przeglądaj kategorie poniżej.
common:
@@ -427,12 +453,21 @@ pl:
others: others
by: By
no_articles: Nie ma tu żadnych artykułów
+ previous: Previous
+ next: Next
+ article_actions:
+ label: Open in
+ view_markdown: View as Markdown
+ open_in_chatgpt: Open in ChatGPT
+ open_in_claude: Open in Claude
+ llm_prompt: "Read the following article and help me understand it: %{url}"
footer:
made_with: Wykonane z
header:
go_to_homepage: Website
visit_website: Visit website
appearance:
+ title: Appearance
system: System
light: Light
dark: Dark
diff --git a/config/locales/pt.yml b/config/locales/pt.yml
index 1cec41f9d..e5cf9e883 100644
--- a/config/locales/pt.yml
+++ b/config/locales/pt.yml
@@ -241,6 +241,7 @@ pt:
whatsapp:
list_button_label: 'Escolha um item'
call_permission_request_body: 'We would like to call you regarding your conversation.'
+ unsupported_message: 'This message is unavailable.'
voice_call:
twilio: 'Voice Call'
whatsapp: 'WhatsApp Call'
@@ -412,7 +413,30 @@ pt:
empty_placeholder: Nenhum resultado encontrado.
loading_placeholder: A pesquisar...
results_title: Resultados da pesquisa
+ results: Resultados da pesquisa
+ results_for: "Search Results for '%{query}'"
+ no_results: "No results found for '%{query}'"
+ found_results:
+ one: Found 1 result
+ other: 'Found %{count} results'
+ submit: Procurar
toc_header: 'Nesta página'
+ sidebar:
+ help_center: Centro de suporte
+ categories: Categorias
+ language: Idioma
+ theme: Theme
+ open_sidebar: Abrir barra lateral
+ close_sidebar: Fechar barra lateral
+ browse: Browse
+ back_to_category: Back to %{name}
+ browse_by_topic: Browse by topic
+ browse_by_topic_subtitle: Find guides, tutorials, and answers organised by category.
+ popular_articles: Popular articles
+ popular_articles_subtitle: What other people are reading right now.
+ popular_label: 'Popular topics:'
+ authors_others: "%{names} and %{count} others"
+ primary_nav: Primary
hero:
sub_title: Pesquise aqui os artigos ou procure as categorias abaixo.
common:
@@ -427,12 +451,21 @@ pt:
others: outros
by: Por
no_articles: Não há artigos aqui
+ previous: Previous
+ next: Next
+ article_actions:
+ label: Open in
+ view_markdown: View as Markdown
+ open_in_chatgpt: Open in ChatGPT
+ open_in_claude: Open in Claude
+ llm_prompt: "Read the following article and help me understand it: %{url}"
footer:
made_with: Feito com
header:
go_to_homepage: Website
visit_website: Visitar website
appearance:
+ title: Aparência
system: Sistema
light: Claro
dark: Escuro
diff --git a/config/locales/pt_BR.yml b/config/locales/pt_BR.yml
index 73dd356a7..f647c2c9a 100644
--- a/config/locales/pt_BR.yml
+++ b/config/locales/pt_BR.yml
@@ -110,7 +110,7 @@ pt_BR:
contact_phone_required: 'Número de telefone do contato é obrigatório'
permission_request_failed: 'Falha ao enviar solicitação de permissão de chamada'
openai:
- invalid_api_key: 'OpenAI API key is invalid or revoked. Please check your key in your OpenAI dashboard.'
+ invalid_api_key: 'A chave da API da OpenAI é inválida ou revogada. Por favor, verifique sua chave no seu painel da OpenAI.'
inboxes:
imap:
socket_error: Por favor, verifique a conexão de rede, endereço IMAP e tente novamente.
@@ -241,6 +241,7 @@ pt_BR:
whatsapp:
list_button_label: 'Escolha um item'
call_permission_request_body: 'Gostaríamos de ligar para você em relação à sua conversa.'
+ unsupported_message: 'Esta mensagem não está disponível.'
voice_call:
twilio: 'Chamada de Voz'
whatsapp: 'Chamada do WhatsApp'
@@ -412,6 +413,13 @@ pt_BR:
empty_placeholder: Nenhum resultado encontrado.
loading_placeholder: Procurando...
results_title: Resultados de pesquisa
+ results: Resultados da Pesquisa
+ results_for: "Search Results for '%{query}'"
+ no_results: "No results found for '%{query}'"
+ found_results:
+ one: Um resultado encontrado
+ other: 'Found %{count} results'
+ submit: Pesquisar
toc_header: 'Nesta página'
sidebar:
help_center: Central de Ajuda
@@ -427,8 +435,8 @@ pt_BR:
popular_articles: Artigos populares
popular_articles_subtitle: O que outras pessoas estão lendo agora.
popular_label: 'Tópicos populares:'
- authors_others: "%{names} e mais %{count}"
- primary_nav: Navegação principal
+ authors_others: "%{names} e %{count} outros"
+ primary_nav: Primário
hero:
sub_title: Pesquise os artigos aqui ou navegue pelas categorias abaixo.
common:
@@ -443,6 +451,8 @@ pt_BR:
others: outros
by: Por
no_articles: Não há artigos aqui
+ previous: Anterior
+ next: Próximo
article_actions:
label: Abrir em
view_markdown: Ver como Markdown
@@ -455,6 +465,7 @@ pt_BR:
go_to_homepage: Site
visit_website: Visite o site
appearance:
+ title: Tema
system: Sistema
light: Claro
dark: Escuro
diff --git a/config/locales/ro.yml b/config/locales/ro.yml
index 89f8ea497..5b5772b41 100644
--- a/config/locales/ro.yml
+++ b/config/locales/ro.yml
@@ -241,6 +241,7 @@ ro:
whatsapp:
list_button_label: 'Choose an item'
call_permission_request_body: 'We would like to call you regarding your conversation.'
+ unsupported_message: 'This message is unavailable.'
voice_call:
twilio: 'Voice Call'
whatsapp: 'WhatsApp Call'
@@ -412,7 +413,31 @@ ro:
empty_placeholder: Niciun rezultat găsit.
loading_placeholder: In căutare...
results_title: Rezultate căutare pentru
+ results: Rezultate căutare
+ results_for: "Search Results for '%{query}'"
+ no_results: "No results found for '%{query}'"
+ found_results:
+ one: Found 1 result
+ few: 'Found %{count} results'
+ other: 'Found %{count} results'
+ submit: Caută
toc_header: 'Pe această pagină'
+ sidebar:
+ help_center: Asistenta
+ categories: Categorii
+ language: Limbă
+ theme: Theme
+ open_sidebar: Deschideți bara laterală
+ close_sidebar: Închideți bara laterală
+ browse: Browse
+ back_to_category: Back to %{name}
+ browse_by_topic: Browse by topic
+ browse_by_topic_subtitle: Find guides, tutorials, and answers organised by category.
+ popular_articles: Popular articles
+ popular_articles_subtitle: What other people are reading right now.
+ popular_label: 'Popular topics:'
+ authors_others: "%{names} and %{count} others"
+ primary_nav: Primary
hero:
sub_title: Căutați articolele aici sau răsfoiți categoriile de mai jos.
common:
@@ -427,12 +452,21 @@ ro:
others: others
by: By
no_articles: Nu există articole aici
+ previous: Previous
+ next: Next
+ article_actions:
+ label: Open in
+ view_markdown: View as Markdown
+ open_in_chatgpt: Open in ChatGPT
+ open_in_claude: Open in Claude
+ llm_prompt: "Read the following article and help me understand it: %{url}"
footer:
made_with: Realizat cu
header:
go_to_homepage: Website
visit_website: Visit website
appearance:
+ title: Appearance
system: Sistem
light: Light
dark: Dark
diff --git a/config/locales/ru.yml b/config/locales/ru.yml
index c5884b521..ff7ac7980 100644
--- a/config/locales/ru.yml
+++ b/config/locales/ru.yml
@@ -241,6 +241,7 @@ ru:
whatsapp:
list_button_label: 'Выберите элемент'
call_permission_request_body: 'Мы хотели бы позвонить Вам по поводу Вашего разговора.'
+ unsupported_message: 'This message is unavailable.'
voice_call:
twilio: 'Голосовой звонок'
whatsapp: 'WhatsApp звонок'
@@ -412,7 +413,32 @@ ru:
empty_placeholder: Результаты не найдены.
loading_placeholder: Идёт поиск...
results_title: Результаты поиска
+ results: Результаты поиска
+ results_for: "Search Results for '%{query}'"
+ no_results: "No results found for '%{query}'"
+ found_results:
+ one: Found 1 result
+ few: 'Found %{count} results'
+ many: 'Found %{count} results'
+ other: 'Found %{count} results'
+ submit: Поиск
toc_header: 'На этой странице'
+ sidebar:
+ help_center: Центр поддержки
+ categories: Категории
+ language: Язык
+ theme: Theme
+ open_sidebar: Открыть боковую панель
+ close_sidebar: Закрыть боковую панель
+ browse: Browse
+ back_to_category: Back to %{name}
+ browse_by_topic: Browse by topic
+ browse_by_topic_subtitle: Find guides, tutorials, and answers organised by category.
+ popular_articles: Popular articles
+ popular_articles_subtitle: What other people are reading right now.
+ popular_label: 'Popular topics:'
+ authors_others: "%{names} and %{count} others"
+ primary_nav: Primary
hero:
sub_title: Ищите здесь статьи или выберите категории, указанные ниже.
common:
@@ -427,12 +453,21 @@ ru:
others: другие
by: От
no_articles: Здесь нет статей
+ previous: Previous
+ next: Next
+ article_actions:
+ label: Open in
+ view_markdown: View as Markdown
+ open_in_chatgpt: Open in ChatGPT
+ open_in_claude: Open in Claude
+ llm_prompt: "Read the following article and help me understand it: %{url}"
footer:
made_with: Сделано с
header:
go_to_homepage: Сайт
visit_website: Посетить сайт
appearance:
+ title: Образец
system: Система
light: Светлая
dark: Тёмная
diff --git a/config/locales/sh.yml b/config/locales/sh.yml
index 749490e19..37588e130 100644
--- a/config/locales/sh.yml
+++ b/config/locales/sh.yml
@@ -241,6 +241,7 @@ sh:
whatsapp:
list_button_label: 'Choose an item'
call_permission_request_body: 'We would like to call you regarding your conversation.'
+ unsupported_message: 'This message is unavailable.'
voice_call:
twilio: 'Voice Call'
whatsapp: 'WhatsApp Call'
@@ -412,7 +413,32 @@ sh:
empty_placeholder: Nema pronađenih rezultata.
loading_placeholder: Pretraga...
results_title: Rezultati pretrage
+ results: Rezultati pretrage
+ results_for: "Search Results for '%{query}'"
+ no_results: "No results found for '%{query}'"
+ found_results:
+ one: Found 1 result
+ few: 'Found %{count} results'
+ many: 'Found %{count} results'
+ other: 'Found %{count} results'
+ submit: Pretraži
toc_header: 'On this page'
+ sidebar:
+ help_center: Centar za pomoć
+ categories: Kategorije
+ language: Language
+ theme: Theme
+ open_sidebar: Otvori bočnu traku
+ close_sidebar: Zatvori bočnu traku
+ browse: Browse
+ back_to_category: Back to %{name}
+ browse_by_topic: Browse by topic
+ browse_by_topic_subtitle: Find guides, tutorials, and answers organised by category.
+ popular_articles: Popular articles
+ popular_articles_subtitle: What other people are reading right now.
+ popular_label: 'Popular topics:'
+ authors_others: "%{names} and %{count} others"
+ primary_nav: Primary
hero:
sub_title: Ovde pretražite članke ili pregledajte kategorije ispod.
common:
@@ -427,12 +453,21 @@ sh:
others: others
by: By
no_articles: There are no articles here
+ previous: Previous
+ next: Next
+ article_actions:
+ label: Open in
+ view_markdown: View as Markdown
+ open_in_chatgpt: Open in ChatGPT
+ open_in_claude: Open in Claude
+ llm_prompt: "Read the following article and help me understand it: %{url}"
footer:
made_with: Made with
header:
go_to_homepage: Website
visit_website: Visit website
appearance:
+ title: Appearance
system: System
light: Light
dark: Dark
diff --git a/config/locales/sk.yml b/config/locales/sk.yml
index 55256f38a..2a5c9d917 100644
--- a/config/locales/sk.yml
+++ b/config/locales/sk.yml
@@ -241,6 +241,7 @@ sk:
whatsapp:
list_button_label: 'Choose an item'
call_permission_request_body: 'We would like to call you regarding your conversation.'
+ unsupported_message: 'This message is unavailable.'
voice_call:
twilio: 'Voice Call'
whatsapp: 'WhatsApp Call'
@@ -412,7 +413,32 @@ sk:
empty_placeholder: Žiadne výsledky neboli nájdené.
loading_placeholder: Searching...
results_title: Search results
+ results: Search Results
+ results_for: "Search Results for '%{query}'"
+ no_results: "No results found for '%{query}'"
+ found_results:
+ one: Found 1 result
+ few: 'Found %{count} results'
+ many: 'Found %{count} results'
+ other: 'Found %{count} results'
+ submit: Hľadať
toc_header: 'On this page'
+ sidebar:
+ help_center: Help Center
+ categories: Categories
+ language: Language
+ theme: Theme
+ open_sidebar: Open sidebar
+ close_sidebar: Close sidebar
+ browse: Browse
+ back_to_category: Back to %{name}
+ browse_by_topic: Browse by topic
+ browse_by_topic_subtitle: Find guides, tutorials, and answers organised by category.
+ popular_articles: Popular articles
+ popular_articles_subtitle: What other people are reading right now.
+ popular_label: 'Popular topics:'
+ authors_others: "%{names} and %{count} others"
+ primary_nav: Primary
hero:
sub_title: Search for the articles here or browse the categories below.
common:
@@ -427,12 +453,21 @@ sk:
others: others
by: By
no_articles: There are no articles here
+ previous: Previous
+ next: Next
+ article_actions:
+ label: Open in
+ view_markdown: View as Markdown
+ open_in_chatgpt: Open in ChatGPT
+ open_in_claude: Open in Claude
+ llm_prompt: "Read the following article and help me understand it: %{url}"
footer:
made_with: Made with
header:
go_to_homepage: Website
visit_website: Visit website
appearance:
+ title: Appearance
system: System
light: Light
dark: Dark
diff --git a/config/locales/sl.yml b/config/locales/sl.yml
index 6182643c3..0cb87bfbb 100644
--- a/config/locales/sl.yml
+++ b/config/locales/sl.yml
@@ -241,6 +241,7 @@ sl:
whatsapp:
list_button_label: 'Choose an item'
call_permission_request_body: 'We would like to call you regarding your conversation.'
+ unsupported_message: 'This message is unavailable.'
voice_call:
twilio: 'Voice Call'
whatsapp: 'WhatsApp Call'
@@ -412,7 +413,32 @@ sl:
empty_placeholder: Ni rezultatov.
loading_placeholder: Iskanje ...
results_title: Rezultati iskanja
+ results: Search Results
+ results_for: "Search Results for '%{query}'"
+ no_results: "No results found for '%{query}'"
+ found_results:
+ one: Found 1 result
+ two: 'Found %{count} results'
+ few: 'Found %{count} results'
+ other: 'Found %{count} results'
+ submit: Išči
toc_header: 'Na tej strani'
+ sidebar:
+ help_center: Pomoč
+ categories: Kategorije
+ language: Jezik
+ theme: Theme
+ open_sidebar: Odpri stransko vrstico
+ close_sidebar: Zapri stransko vrstico
+ browse: Browse
+ back_to_category: Back to %{name}
+ browse_by_topic: Browse by topic
+ browse_by_topic_subtitle: Find guides, tutorials, and answers organised by category.
+ popular_articles: Popular articles
+ popular_articles_subtitle: What other people are reading right now.
+ popular_label: 'Popular topics:'
+ authors_others: "%{names} and %{count} others"
+ primary_nav: Primary
hero:
sub_title: Poiščite članke tukaj ali prebrskajte spodnje kategorije.
common:
@@ -427,12 +453,21 @@ sl:
others: ostali
by: Od
no_articles: Tukaj ni člankov
+ previous: Previous
+ next: Next
+ article_actions:
+ label: Open in
+ view_markdown: View as Markdown
+ open_in_chatgpt: Open in ChatGPT
+ open_in_claude: Open in Claude
+ llm_prompt: "Read the following article and help me understand it: %{url}"
footer:
made_with: Narejeno z
header:
go_to_homepage: Spletna stran
visit_website: Visit website
appearance:
+ title: Appearance
system: Sistem
light: Svetlo
dark: Temno
diff --git a/config/locales/sq.yml b/config/locales/sq.yml
index 35c5f6911..59e98e325 100644
--- a/config/locales/sq.yml
+++ b/config/locales/sq.yml
@@ -241,6 +241,7 @@ sq:
whatsapp:
list_button_label: 'Zgjidhni një element'
call_permission_request_body: 'We would like to call you regarding your conversation.'
+ unsupported_message: 'This message is unavailable.'
voice_call:
twilio: 'Voice Call'
whatsapp: 'WhatsApp Call'
@@ -412,7 +413,30 @@ sq:
empty_placeholder: No results found.
loading_placeholder: Searching...
results_title: Search results
+ results: Search Results
+ results_for: "Search Results for '%{query}'"
+ no_results: "No results found for '%{query}'"
+ found_results:
+ one: Found 1 result
+ other: 'Found %{count} results'
+ submit: Kërko
toc_header: 'On this page'
+ sidebar:
+ help_center: Qendra e Ndihmës
+ categories: Kategoritë
+ language: Language
+ theme: Theme
+ open_sidebar: Hap shiritin anësor
+ close_sidebar: Mbyll shiritin anësor
+ browse: Browse
+ back_to_category: Back to %{name}
+ browse_by_topic: Browse by topic
+ browse_by_topic_subtitle: Find guides, tutorials, and answers organised by category.
+ popular_articles: Popular articles
+ popular_articles_subtitle: What other people are reading right now.
+ popular_label: 'Popular topics:'
+ authors_others: "%{names} and %{count} others"
+ primary_nav: Primary
hero:
sub_title: Search for the articles here or browse the categories below.
common:
@@ -427,12 +451,21 @@ sq:
others: others
by: By
no_articles: There are no articles here
+ previous: Previous
+ next: Next
+ article_actions:
+ label: Open in
+ view_markdown: View as Markdown
+ open_in_chatgpt: Open in ChatGPT
+ open_in_claude: Open in Claude
+ llm_prompt: "Read the following article and help me understand it: %{url}"
footer:
made_with: Made with
header:
go_to_homepage: Website
visit_website: Vizitoni faqen e internetit
appearance:
+ title: Appearance
system: System
light: Light
dark: Dark
diff --git a/config/locales/sr.yml b/config/locales/sr.yml
index c220b5437..5120a1066 100644
--- a/config/locales/sr.yml
+++ b/config/locales/sr.yml
@@ -241,6 +241,7 @@ sr-Latn:
whatsapp:
list_button_label: 'Choose an item'
call_permission_request_body: 'We would like to call you regarding your conversation.'
+ unsupported_message: 'This message is unavailable.'
voice_call:
twilio: 'Voice Call'
whatsapp: 'WhatsApp Call'
@@ -412,7 +413,31 @@ sr-Latn:
empty_placeholder: Ništa nije pronađeno.
loading_placeholder: Searching...
results_title: Search results
+ results: Rezultat pretrage
+ results_for: "Search Results for '%{query}'"
+ no_results: "No results found for '%{query}'"
+ found_results:
+ one: Found 1 result
+ few: 'Found %{count} results'
+ other: 'Found %{count} results'
+ submit: Traži
toc_header: 'On this page'
+ sidebar:
+ help_center: Help Center
+ categories: Categories
+ language: Jezik
+ theme: Theme
+ open_sidebar: Otvorite bočnu traku
+ close_sidebar: Zatvorite bočnu traku
+ browse: Browse
+ back_to_category: Back to %{name}
+ browse_by_topic: Browse by topic
+ browse_by_topic_subtitle: Find guides, tutorials, and answers organised by category.
+ popular_articles: Popular articles
+ popular_articles_subtitle: What other people are reading right now.
+ popular_label: 'Popular topics:'
+ authors_others: "%{names} and %{count} others"
+ primary_nav: Primary
hero:
sub_title: Search for the articles here or browse the categories below.
common:
@@ -427,12 +452,21 @@ sr-Latn:
others: others
by: By
no_articles: There are no articles here
+ previous: Previous
+ next: Next
+ article_actions:
+ label: Open in
+ view_markdown: View as Markdown
+ open_in_chatgpt: Open in ChatGPT
+ open_in_claude: Open in Claude
+ llm_prompt: "Read the following article and help me understand it: %{url}"
footer:
made_with: Made with
header:
go_to_homepage: Website
visit_website: Visit website
appearance:
+ title: Appearance
system: System
light: Light
dark: Dark
diff --git a/config/locales/sv.yml b/config/locales/sv.yml
index ba1d82c77..94c0ba3cb 100644
--- a/config/locales/sv.yml
+++ b/config/locales/sv.yml
@@ -241,6 +241,7 @@ sv:
whatsapp:
list_button_label: 'Choose an item'
call_permission_request_body: 'We would like to call you regarding your conversation.'
+ unsupported_message: 'This message is unavailable.'
voice_call:
twilio: 'Voice Call'
whatsapp: 'WhatsApp Call'
@@ -412,7 +413,30 @@ sv:
empty_placeholder: Inga resultat hittades.
loading_placeholder: Searching...
results_title: Search results
+ results: Sökresultat
+ results_for: "Search Results for '%{query}'"
+ no_results: "No results found for '%{query}'"
+ found_results:
+ one: Found 1 result
+ other: 'Found %{count} results'
+ submit: Sök
toc_header: 'På denna sida'
+ sidebar:
+ help_center: Help Center
+ categories: Categories
+ language: Language
+ theme: Theme
+ open_sidebar: Open sidebar
+ close_sidebar: Close sidebar
+ browse: Browse
+ back_to_category: Back to %{name}
+ browse_by_topic: Browse by topic
+ browse_by_topic_subtitle: Find guides, tutorials, and answers organised by category.
+ popular_articles: Popular articles
+ popular_articles_subtitle: What other people are reading right now.
+ popular_label: 'Popular topics:'
+ authors_others: "%{names} and %{count} others"
+ primary_nav: Primary
hero:
sub_title: Sök efter artiklarna här eller bläddra i kategorierna nedan.
common:
@@ -427,12 +451,21 @@ sv:
others: andra
by: Av
no_articles: Det finns inga artiklar här
+ previous: Previous
+ next: Next
+ article_actions:
+ label: Open in
+ view_markdown: View as Markdown
+ open_in_chatgpt: Open in ChatGPT
+ open_in_claude: Open in Claude
+ llm_prompt: "Read the following article and help me understand it: %{url}"
footer:
made_with: Gjord med
header:
go_to_homepage: Hemsida
visit_website: Visit website
appearance:
+ title: Appearance
system: System
light: Ljus
dark: Mörk
diff --git a/config/locales/ta.yml b/config/locales/ta.yml
index c1faba784..0915cdabd 100644
--- a/config/locales/ta.yml
+++ b/config/locales/ta.yml
@@ -241,6 +241,7 @@ ta:
whatsapp:
list_button_label: 'Choose an item'
call_permission_request_body: 'We would like to call you regarding your conversation.'
+ unsupported_message: 'This message is unavailable.'
voice_call:
twilio: 'Voice Call'
whatsapp: 'WhatsApp Call'
@@ -412,7 +413,30 @@ ta:
empty_placeholder: No results found.
loading_placeholder: Searching...
results_title: Search results
+ results: Search Results
+ results_for: "Search Results for '%{query}'"
+ no_results: "No results found for '%{query}'"
+ found_results:
+ one: Found 1 result
+ other: 'Found %{count} results'
+ submit: Search
toc_header: 'On this page'
+ sidebar:
+ help_center: உதவி மையம்
+ categories: Categories
+ language: Language
+ theme: Theme
+ open_sidebar: Open sidebar
+ close_sidebar: Close sidebar
+ browse: Browse
+ back_to_category: Back to %{name}
+ browse_by_topic: Browse by topic
+ browse_by_topic_subtitle: Find guides, tutorials, and answers organised by category.
+ popular_articles: Popular articles
+ popular_articles_subtitle: What other people are reading right now.
+ popular_label: 'Popular topics:'
+ authors_others: "%{names} and %{count} others"
+ primary_nav: Primary
hero:
sub_title: Search for the articles here or browse the categories below.
common:
@@ -427,12 +451,21 @@ ta:
others: others
by: By
no_articles: There are no articles here
+ previous: Previous
+ next: அடுத்து
+ article_actions:
+ label: Open in
+ view_markdown: View as Markdown
+ open_in_chatgpt: Open in ChatGPT
+ open_in_claude: Open in Claude
+ llm_prompt: "Read the following article and help me understand it: %{url}"
footer:
made_with: Made with
header:
go_to_homepage: Website
visit_website: Visit website
appearance:
+ title: Appearance
system: System
light: Light
dark: Dark
diff --git a/config/locales/th.yml b/config/locales/th.yml
index 1f3420af2..3131e5918 100644
--- a/config/locales/th.yml
+++ b/config/locales/th.yml
@@ -241,6 +241,7 @@ th:
whatsapp:
list_button_label: 'Choose an item'
call_permission_request_body: 'We would like to call you regarding your conversation.'
+ unsupported_message: 'This message is unavailable.'
voice_call:
twilio: 'Voice Call'
whatsapp: 'WhatsApp Call'
@@ -412,7 +413,29 @@ th:
empty_placeholder: ไม่พบผลการค้นหา
loading_placeholder: Searching...
results_title: Search results
+ results: ผลการค้นหา
+ results_for: "Search Results for '%{query}'"
+ no_results: "No results found for '%{query}'"
+ found_results:
+ other: 'Found %{count} results'
+ submit: ค้นหา
toc_header: 'On this page'
+ sidebar:
+ help_center: Help Center
+ categories: Categories
+ language: Language
+ theme: Theme
+ open_sidebar: Open sidebar
+ close_sidebar: Close sidebar
+ browse: Browse
+ back_to_category: Back to %{name}
+ browse_by_topic: Browse by topic
+ browse_by_topic_subtitle: Find guides, tutorials, and answers organised by category.
+ popular_articles: Popular articles
+ popular_articles_subtitle: What other people are reading right now.
+ popular_label: 'Popular topics:'
+ authors_others: "%{names} and %{count} others"
+ primary_nav: Primary
hero:
sub_title: Search for the articles here or browse the categories below.
common:
@@ -427,12 +450,21 @@ th:
others: others
by: By
no_articles: There are no articles here
+ previous: Previous
+ next: Next
+ article_actions:
+ label: Open in
+ view_markdown: View as Markdown
+ open_in_chatgpt: Open in ChatGPT
+ open_in_claude: Open in Claude
+ llm_prompt: "Read the following article and help me understand it: %{url}"
footer:
made_with: Made with
header:
go_to_homepage: Website
visit_website: Visit website
appearance:
+ title: Appearance
system: System
light: Light
dark: Dark
diff --git a/config/locales/tl.yml b/config/locales/tl.yml
index 64ea7e0b9..e95be4794 100644
--- a/config/locales/tl.yml
+++ b/config/locales/tl.yml
@@ -241,6 +241,7 @@ tl:
whatsapp:
list_button_label: 'Pumili ng isang item'
call_permission_request_body: 'We would like to call you regarding your conversation.'
+ unsupported_message: 'This message is unavailable.'
voice_call:
twilio: 'Voice Call'
whatsapp: 'WhatsApp Call'
@@ -412,7 +413,30 @@ tl:
empty_placeholder: Walang nahanap na resulta.
loading_placeholder: Naghahanap...
results_title: Mga resulta ng paghahanap
+ results: Search Results
+ results_for: "Search Results for '%{query}'"
+ no_results: "No results found for '%{query}'"
+ found_results:
+ one: Found 1 result
+ other: 'Found %{count} results'
+ submit: Hanapin
toc_header: 'Sa pahinang ito'
+ sidebar:
+ help_center: Help Center
+ categories: Mga kategorya
+ language: Language
+ theme: Theme
+ open_sidebar: Buksan ang sidebar
+ close_sidebar: Isara ang sidebar
+ browse: Browse
+ back_to_category: Back to %{name}
+ browse_by_topic: Browse by topic
+ browse_by_topic_subtitle: Find guides, tutorials, and answers organised by category.
+ popular_articles: Popular articles
+ popular_articles_subtitle: What other people are reading right now.
+ popular_label: 'Popular topics:'
+ authors_others: "%{names} and %{count} others"
+ primary_nav: Primary
hero:
sub_title: Maghanap ng mga artikulo dito o tingnan ang mga kategorya sa ibaba.
common:
@@ -427,12 +451,21 @@ tl:
others: mga iba
by: Ni
no_articles: Walang mga artikulo dito
+ previous: Previous
+ next: Next
+ article_actions:
+ label: Open in
+ view_markdown: View as Markdown
+ open_in_chatgpt: Open in ChatGPT
+ open_in_claude: Open in Claude
+ llm_prompt: "Read the following article and help me understand it: %{url}"
footer:
made_with: Ginawa gamit ang
header:
go_to_homepage: Website
visit_website: Bisitahin ang website
appearance:
+ title: Appearance
system: Sistema
light: Maliwanag
dark: Madilim
diff --git a/config/locales/tr.yml b/config/locales/tr.yml
index f3e72a41e..fdeb50341 100644
--- a/config/locales/tr.yml
+++ b/config/locales/tr.yml
@@ -241,6 +241,7 @@ tr:
whatsapp:
list_button_label: 'Bir öğe seçin'
call_permission_request_body: 'We would like to call you regarding your conversation.'
+ unsupported_message: 'This message is unavailable.'
voice_call:
twilio: 'Voice Call'
whatsapp: 'WhatsApp Call'
@@ -412,7 +413,30 @@ tr:
empty_placeholder: Sonuç bulunamadı.
loading_placeholder: Aranıyor...
results_title: Arama sonuçları
+ results: Arama Sonucu
+ results_for: "Search Results for '%{query}'"
+ no_results: "No results found for '%{query}'"
+ found_results:
+ one: Found 1 result
+ other: 'Found %{count} results'
+ submit: Ara
toc_header: 'Bu sayfada'
+ sidebar:
+ help_center: Yardım Merkezi
+ categories: Kategoriler
+ language: Dil
+ theme: Theme
+ open_sidebar: Kenar Çubuğunu Aç
+ close_sidebar: Kenar Çubuğunu Kapat
+ browse: Browse
+ back_to_category: Back to %{name}
+ browse_by_topic: Browse by topic
+ browse_by_topic_subtitle: Find guides, tutorials, and answers organised by category.
+ popular_articles: Popular articles
+ popular_articles_subtitle: What other people are reading right now.
+ popular_label: 'Popular topics:'
+ authors_others: "%{names} and %{count} others"
+ primary_nav: Primary
hero:
sub_title: Makaleleri buradan arayın veya aşağıdaki kategorilere göz atın.
common:
@@ -427,12 +451,21 @@ tr:
others: diğerleri
by: Tarafından
no_articles: Burada makale bulunmuyor
+ previous: Önceki
+ next: Sonraki
+ article_actions:
+ label: Open in
+ view_markdown: View as Markdown
+ open_in_chatgpt: Open in ChatGPT
+ open_in_claude: Open in Claude
+ llm_prompt: "Read the following article and help me understand it: %{url}"
footer:
made_with: İle yapılmıştır
header:
go_to_homepage: Website
visit_website: Siteyi ziyaret et
appearance:
+ title: Görünüm
system: Sistem
light: Açık Mod
dark: Koyu Mod
diff --git a/config/locales/uk.yml b/config/locales/uk.yml
index 9ba280726..7cef7561a 100644
--- a/config/locales/uk.yml
+++ b/config/locales/uk.yml
@@ -241,6 +241,7 @@ uk:
whatsapp:
list_button_label: 'Choose an item'
call_permission_request_body: 'We would like to call you regarding your conversation.'
+ unsupported_message: 'This message is unavailable.'
voice_call:
twilio: 'Voice Call'
whatsapp: 'WhatsApp Call'
@@ -412,7 +413,32 @@ uk:
empty_placeholder: Результатів не знайдено.
loading_placeholder: Шукаємо...
results_title: Результати пошуку
+ results: Результати пошуку
+ results_for: "Search Results for '%{query}'"
+ no_results: "No results found for '%{query}'"
+ found_results:
+ one: Found 1 result
+ few: 'Found %{count} results'
+ many: 'Found %{count} results'
+ other: 'Found %{count} results'
+ submit: Пошук
toc_header: 'На цій сторінці'
+ sidebar:
+ help_center: Довідковий центр
+ categories: Категорії
+ language: Мова
+ theme: Theme
+ open_sidebar: Відкрити бічну панель
+ close_sidebar: Закрити бічну панель
+ browse: Browse
+ back_to_category: Back to %{name}
+ browse_by_topic: Browse by topic
+ browse_by_topic_subtitle: Find guides, tutorials, and answers organised by category.
+ popular_articles: Popular articles
+ popular_articles_subtitle: What other people are reading right now.
+ popular_label: 'Popular topics:'
+ authors_others: "%{names} and %{count} others"
+ primary_nav: Primary
hero:
sub_title: Шукайте тут або перегляньте категорії нижче.
common:
@@ -427,12 +453,21 @@ uk:
others: інші
by: Від
no_articles: Тут немає статей
+ previous: Previous
+ next: Next
+ article_actions:
+ label: Open in
+ view_markdown: View as Markdown
+ open_in_chatgpt: Open in ChatGPT
+ open_in_claude: Open in Claude
+ llm_prompt: "Read the following article and help me understand it: %{url}"
footer:
made_with: Зроблено з
header:
go_to_homepage: Вебсайт
visit_website: Visit website
appearance:
+ title: Оформлення
system: Системна
light: Світла
dark: Темна
diff --git a/config/locales/ur.yml b/config/locales/ur.yml
index 1ec17a447..0a3d9c705 100644
--- a/config/locales/ur.yml
+++ b/config/locales/ur.yml
@@ -241,6 +241,7 @@ ur:
whatsapp:
list_button_label: 'Choose an item'
call_permission_request_body: 'We would like to call you regarding your conversation.'
+ unsupported_message: 'This message is unavailable.'
voice_call:
twilio: 'Voice Call'
whatsapp: 'WhatsApp Call'
@@ -412,7 +413,30 @@ ur:
empty_placeholder: کوئی نتیجہ نہیں.
loading_placeholder: Searching...
results_title: Search results
+ results: تلاش کے نتائج
+ results_for: "Search Results for '%{query}'"
+ no_results: "No results found for '%{query}'"
+ found_results:
+ one: Found 1 result
+ other: 'Found %{count} results'
+ submit: تلاش کریں۔
toc_header: 'On this page'
+ sidebar:
+ help_center: Help Center
+ categories: Categories
+ language: Language
+ theme: Theme
+ open_sidebar: Open sidebar
+ close_sidebar: Close sidebar
+ browse: Browse
+ back_to_category: Back to %{name}
+ browse_by_topic: Browse by topic
+ browse_by_topic_subtitle: Find guides, tutorials, and answers organised by category.
+ popular_articles: Popular articles
+ popular_articles_subtitle: What other people are reading right now.
+ popular_label: 'Popular topics:'
+ authors_others: "%{names} and %{count} others"
+ primary_nav: Primary
hero:
sub_title: Search for the articles here or browse the categories below.
common:
@@ -427,12 +451,21 @@ ur:
others: others
by: By
no_articles: There are no articles here
+ previous: Previous
+ next: Next
+ article_actions:
+ label: Open in
+ view_markdown: View as Markdown
+ open_in_chatgpt: Open in ChatGPT
+ open_in_claude: Open in Claude
+ llm_prompt: "Read the following article and help me understand it: %{url}"
footer:
made_with: Made with
header:
go_to_homepage: Website
visit_website: Visit website
appearance:
+ title: Appearance
system: System
light: Light
dark: Dark
diff --git a/config/locales/ur_IN.yml b/config/locales/ur_IN.yml
index f3c26d1c8..f3cfab8b1 100644
--- a/config/locales/ur_IN.yml
+++ b/config/locales/ur_IN.yml
@@ -241,6 +241,7 @@ ur:
whatsapp:
list_button_label: 'Choose an item'
call_permission_request_body: 'We would like to call you regarding your conversation.'
+ unsupported_message: 'This message is unavailable.'
voice_call:
twilio: 'Voice Call'
whatsapp: 'WhatsApp Call'
@@ -412,7 +413,30 @@ ur:
empty_placeholder: No results found.
loading_placeholder: Searching...
results_title: Search results
+ results: Search Results
+ results_for: "Search Results for '%{query}'"
+ no_results: "No results found for '%{query}'"
+ found_results:
+ one: Found 1 result
+ other: 'Found %{count} results'
+ submit: تلاش کریں
toc_header: 'On this page'
+ sidebar:
+ help_center: مدد مرکز
+ categories: زمرہ جات
+ language: Language
+ theme: Theme
+ open_sidebar: سائڈبار کھولیں
+ close_sidebar: سائڈبار بند کریں
+ browse: Browse
+ back_to_category: Back to %{name}
+ browse_by_topic: Browse by topic
+ browse_by_topic_subtitle: Find guides, tutorials, and answers organised by category.
+ popular_articles: Popular articles
+ popular_articles_subtitle: What other people are reading right now.
+ popular_label: 'Popular topics:'
+ authors_others: "%{names} and %{count} others"
+ primary_nav: Primary
hero:
sub_title: Search for the articles here or browse the categories below.
common:
@@ -427,12 +451,21 @@ ur:
others: others
by: By
no_articles: There are no articles here
+ previous: Previous
+ next: Next
+ article_actions:
+ label: Open in
+ view_markdown: View as Markdown
+ open_in_chatgpt: Open in ChatGPT
+ open_in_claude: Open in Claude
+ llm_prompt: "Read the following article and help me understand it: %{url}"
footer:
made_with: Made with
header:
go_to_homepage: Website
visit_website: Visit website
appearance:
+ title: Appearance
system: System
light: Light
dark: Dark
diff --git a/config/locales/vi.yml b/config/locales/vi.yml
index fa827806f..b6430287e 100644
--- a/config/locales/vi.yml
+++ b/config/locales/vi.yml
@@ -241,6 +241,7 @@ vi:
whatsapp:
list_button_label: 'Choose an item'
call_permission_request_body: 'We would like to call you regarding your conversation.'
+ unsupported_message: 'This message is unavailable.'
voice_call:
twilio: 'Voice Call'
whatsapp: 'WhatsApp Call'
@@ -412,7 +413,29 @@ vi:
empty_placeholder: Không tìm thấy kết quả.
loading_placeholder: Đang tìm kiếm...
results_title: Các kết quả tìm kiếm
+ results: Các kết quả tìm kiếm
+ results_for: "Search Results for '%{query}'"
+ no_results: "No results found for '%{query}'"
+ found_results:
+ other: 'Found %{count} results'
+ submit: Tìm kiếm
toc_header: 'Trên trang này'
+ sidebar:
+ help_center: Help Center
+ categories: Danh mục
+ language: Ngôn ngữ
+ theme: Theme
+ open_sidebar: Mở thanh bên
+ close_sidebar: Đóng thanh bên
+ browse: Browse
+ back_to_category: Back to %{name}
+ browse_by_topic: Browse by topic
+ browse_by_topic_subtitle: Find guides, tutorials, and answers organised by category.
+ popular_articles: Popular articles
+ popular_articles_subtitle: What other people are reading right now.
+ popular_label: 'Popular topics:'
+ authors_others: "%{names} and %{count} others"
+ primary_nav: Primary
hero:
sub_title: Tìm trong bài viết hoặc xem danh mục dưới đây.
common:
@@ -427,12 +450,21 @@ vi:
others: others
by: By
no_articles: Không tìm thấy bài viết
+ previous: Previous
+ next: Next
+ article_actions:
+ label: Open in
+ view_markdown: View as Markdown
+ open_in_chatgpt: Open in ChatGPT
+ open_in_claude: Open in Claude
+ llm_prompt: "Read the following article and help me understand it: %{url}"
footer:
made_with: Tạo bởi
header:
go_to_homepage: Website
visit_website: Visit website
appearance:
+ title: Appearance
system: System
light: Light
dark: Dark
diff --git a/config/locales/zh_CN.yml b/config/locales/zh_CN.yml
index 89ebf755d..5d1bdef9f 100644
--- a/config/locales/zh_CN.yml
+++ b/config/locales/zh_CN.yml
@@ -241,6 +241,7 @@ zh_CN:
whatsapp:
list_button_label: 'Choose an item'
call_permission_request_body: 'We would like to call you regarding your conversation.'
+ unsupported_message: 'This message is unavailable.'
voice_call:
twilio: 'Voice Call'
whatsapp: 'WhatsApp Call'
@@ -412,7 +413,29 @@ zh_CN:
empty_placeholder: 未找到结果。
loading_placeholder: 搜索中...
results_title: 搜索结果
+ results: 搜索结果
+ results_for: "Search Results for '%{query}'"
+ no_results: "No results found for '%{query}'"
+ found_results:
+ other: 'Found %{count} results'
+ submit: 搜索
toc_header: '在此页面'
+ sidebar:
+ help_center: 帮助中心
+ categories: 类别
+ language: 语言
+ theme: Theme
+ open_sidebar: 打开侧边栏
+ close_sidebar: 关闭侧边栏
+ browse: Browse
+ back_to_category: Back to %{name}
+ browse_by_topic: Browse by topic
+ browse_by_topic_subtitle: Find guides, tutorials, and answers organised by category.
+ popular_articles: Popular articles
+ popular_articles_subtitle: What other people are reading right now.
+ popular_label: 'Popular topics:'
+ authors_others: "%{names} and %{count} others"
+ primary_nav: Primary
hero:
sub_title: 在这里搜索文章或浏览下面的分类
common:
@@ -427,12 +450,21 @@ zh_CN:
others: 其他
by: 作者:
no_articles: 没有文章在这里
+ previous: Previous
+ next: Next
+ article_actions:
+ label: Open in
+ view_markdown: View as Markdown
+ open_in_chatgpt: Open in ChatGPT
+ open_in_claude: Open in Claude
+ llm_prompt: "Read the following article and help me understand it: %{url}"
footer:
made_with: 制作于
header:
go_to_homepage: 网站
visit_website: Visit website
appearance:
+ title: 外观
system: 系统
light: 浅色
dark: 暗色
diff --git a/config/locales/zh_TW.yml b/config/locales/zh_TW.yml
index 6e8055c51..d51d86f29 100644
--- a/config/locales/zh_TW.yml
+++ b/config/locales/zh_TW.yml
@@ -241,6 +241,7 @@ zh_TW:
whatsapp:
list_button_label: '選擇一個項目'
call_permission_request_body: 'We would like to call you regarding your conversation.'
+ unsupported_message: 'This message is unavailable.'
voice_call:
twilio: 'Voice Call'
whatsapp: 'WhatsApp Call'
@@ -412,7 +413,29 @@ zh_TW:
empty_placeholder: 查無結果。
loading_placeholder: 搜尋中...
results_title: 搜尋結果
+ results: 搜尋結果
+ results_for: "Search Results for '%{query}'"
+ no_results: "No results found for '%{query}'"
+ found_results:
+ other: 'Found %{count} results'
+ submit: 搜尋
toc_header: '本頁內容'
+ sidebar:
+ help_center: 幫助中心
+ categories: 分類
+ language: 語言
+ theme: Theme
+ open_sidebar: 開啟側邊欄
+ close_sidebar: 關閉側邊欄
+ browse: Browse
+ back_to_category: Back to %{name}
+ browse_by_topic: Browse by topic
+ browse_by_topic_subtitle: Find guides, tutorials, and answers organised by category.
+ popular_articles: Popular articles
+ popular_articles_subtitle: What other people are reading right now.
+ popular_label: 'Popular topics:'
+ authors_others: "%{names} and %{count} others"
+ primary_nav: Primary
hero:
sub_title: 在此搜尋文章或瀏覽以下分類。
common:
@@ -423,16 +446,25 @@ zh_TW:
articles: 篇文章
author: 位作者
authors: 位作者
- other: 其他
+ other: other
others: 其他
by: 作者:
no_articles: 這裡還沒有文章
+ previous: 上一頁
+ next: 下一頁
+ article_actions:
+ label: Open in
+ view_markdown: View as Markdown
+ open_in_chatgpt: Open in ChatGPT
+ open_in_claude: Open in Claude
+ llm_prompt: "Read the following article and help me understand it: %{url}"
footer:
made_with: 由以下技術製作
header:
go_to_homepage: 網站首頁
visit_website: 前往網站
appearance:
+ title: 外觀
system: 系統
light: 淺色
dark: 深色
diff --git a/config/routes.rb b/config/routes.rb
index ca2f75479..766678235 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -31,6 +31,7 @@ Rails.application.routes.draw do
get '/app/accounts/:account_id/settings/inboxes/:inbox_id', to: 'dashboard#index', as: 'app_instagram_inbox_settings'
get '/app/accounts/:account_id/settings/inboxes/:inbox_id', to: 'dashboard#index', as: 'app_tiktok_inbox_settings'
get '/app/accounts/:account_id/settings/inboxes/:inbox_id', to: 'dashboard#index', as: 'app_email_inbox_settings'
+ get '/app/accounts/:account_id/onboarding/inbox-setup', to: 'dashboard#index', as: 'app_onboarding_inbox_setup'
resource :widget, only: [:show]
namespace :survey do
@@ -56,6 +57,7 @@ Rails.application.routes.draw do
resource :contact_merge, only: [:create]
end
resource :bulk_actions, only: [:create]
+ resource :onboarding, only: [:update]
resources :agents, only: [:index, :create, :update, :destroy] do
post :bulk_create, on: :collection
end
@@ -263,6 +265,8 @@ Rails.application.routes.draw do
resource :conference, only: %i[create destroy], controller: 'conference' do
get :token, on: :member
end
+ post :enable_whatsapp_calling, on: :member
+ post :disable_whatsapp_calling, on: :member
end
resource :csat_template, only: [:show, :create], controller: 'inbox_csat_templates' do
@@ -384,8 +388,6 @@ Rails.application.routes.draw do
end
end
end
- resources :working_hours, only: [:update]
-
resources :portals do
member do
patch :archive
@@ -400,6 +402,7 @@ Rails.application.routes.draw do
resource :bulk_actions, only: [] do
post :translate
patch :update_status
+ patch :update_category
delete :delete_articles
end
end
@@ -593,6 +596,7 @@ Rails.application.routes.draw do
get 'hc/:slug', to: 'public/api/v1/portals#show'
get 'hc/:slug/sitemap.xml', to: 'public/api/v1/portals#sitemap'
get 'hc/:slug/:locale', to: 'public/api/v1/portals#show', as: :public_portal_locale
+ get 'hc/:slug/:locale/search', to: 'public/api/v1/portals/search#index', as: :portal_search
get 'hc/:slug/:locale/articles', to: 'public/api/v1/portals/articles#index'
get 'hc/:slug/:locale/categories', to: 'public/api/v1/portals/categories#index'
get 'hc/:slug/:locale/categories/:category_slug', to: 'public/api/v1/portals/categories#show', as: :public_portal_category
diff --git a/db/migrate/20260525093000_change_captain_document_external_link_to_text.rb b/db/migrate/20260525093000_change_captain_document_external_link_to_text.rb
new file mode 100644
index 000000000..50cee2b0d
--- /dev/null
+++ b/db/migrate/20260525093000_change_captain_document_external_link_to_text.rb
@@ -0,0 +1,16 @@
+class ChangeCaptainDocumentExternalLinkToText < ActiveRecord::Migration[7.0]
+ OLD_INDEX_NAME = 'index_captain_documents_on_assistant_id_and_external_link'.freeze
+ NEW_INDEX_NAME = 'idx_captain_documents_on_assistant_id_and_external_link_md5'.freeze
+
+ def up
+ remove_index :captain_documents, name: OLD_INDEX_NAME, if_exists: true
+ change_column :captain_documents, :external_link, :text, null: false
+ add_index :captain_documents, 'assistant_id, md5(external_link)', unique: true, name: NEW_INDEX_NAME, if_not_exists: true
+ end
+
+ def down
+ remove_index :captain_documents, name: NEW_INDEX_NAME, if_exists: true
+ change_column :captain_documents, :external_link, :string, null: false
+ add_index :captain_documents, [:assistant_id, :external_link], unique: true, name: OLD_INDEX_NAME, if_not_exists: true
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index a309c6bb8..9bc189618 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema[7.1].define(version: 2026_05_19_160000) do
+ActiveRecord::Schema[7.1].define(version: 2026_05_25_093000) do
# These extensions should be enabled to support this database
enable_extension "pg_stat_statements"
enable_extension "pg_trgm"
@@ -370,7 +370,7 @@ ActiveRecord::Schema[7.1].define(version: 2026_05_19_160000) do
create_table "captain_documents", force: :cascade do |t|
t.string "name"
- t.string "external_link", null: false
+ t.text "external_link", null: false
t.text "content"
t.bigint "assistant_id", null: false
t.bigint "account_id", null: false
@@ -381,10 +381,10 @@ ActiveRecord::Schema[7.1].define(version: 2026_05_19_160000) do
t.integer "sync_status"
t.datetime "last_synced_at"
t.datetime "last_sync_attempted_at"
+ t.index "assistant_id, md5(external_link)", name: "idx_captain_documents_on_assistant_id_and_external_link_md5", unique: true
t.index ["account_id", "assistant_id", "sync_status", "last_synced_at"], name: "idx_captain_documents_on_account_assistant_sync_stats"
t.index ["account_id", "sync_status"], name: "index_captain_documents_on_account_id_and_sync_status"
t.index ["account_id"], name: "index_captain_documents_on_account_id"
- t.index ["assistant_id", "external_link"], name: "index_captain_documents_on_assistant_id_and_external_link", unique: true
t.index ["assistant_id"], name: "index_captain_documents_on_assistant_id"
t.index ["status"], name: "index_captain_documents_on_status"
end
diff --git a/enterprise/app/controllers/api/v1/accounts/audit_logs_controller.rb b/enterprise/app/controllers/api/v1/accounts/audit_logs_controller.rb
index 29e02c1dd..7c569b3f1 100644
--- a/enterprise/app/controllers/api/v1/accounts/audit_logs_controller.rb
+++ b/enterprise/app/controllers/api/v1/accounts/audit_logs_controller.rb
@@ -2,7 +2,7 @@ class Api::V1::Accounts::AuditLogsController < Api::V1::Accounts::EnterpriseAcco
before_action :check_admin_authorization?
before_action :fetch_audit
- RESULTS_PER_PAGE = 15
+ RESULTS_PER_PAGE = 25
def show
@audit_logs = @audit_logs.page(params[:page]).per(RESULTS_PER_PAGE)
diff --git a/enterprise/app/controllers/api/v1/accounts/captain/bulk_actions_controller.rb b/enterprise/app/controllers/api/v1/accounts/captain/bulk_actions_controller.rb
index b9a6bbcc5..7e2817f69 100644
--- a/enterprise/app/controllers/api/v1/accounts/captain/bulk_actions_controller.rb
+++ b/enterprise/app/controllers/api/v1/accounts/captain/bulk_actions_controller.rb
@@ -75,7 +75,6 @@ class Api::V1::Accounts::Captain::BulkActionsController < Api::V1::Accounts::Bas
Current.account.captain_documents.where(id: params[:ids]).find_each(batch_size: 100) do |document|
next unless document.syncable?
next unless document.available?
- next if document.sync_in_progress?
document.update!(
sync_status: :syncing,
diff --git a/enterprise/app/controllers/api/v1/accounts/captain/documents_controller.rb b/enterprise/app/controllers/api/v1/accounts/captain/documents_controller.rb
index 23f410499..273c082b1 100644
--- a/enterprise/app/controllers/api/v1/accounts/captain/documents_controller.rb
+++ b/enterprise/app/controllers/api/v1/accounts/captain/documents_controller.rb
@@ -37,7 +37,6 @@ class Api::V1::Accounts::Captain::DocumentsController < Api::V1::Accounts::BaseC
def sync
return render_could_not_create_error(I18n.t('captain.documents.sync_not_supported_for_pdf')) unless @document.syncable?
return render_could_not_create_error(I18n.t('captain.documents.sync_only_available_documents')) unless @document.available?
- return render_could_not_create_error(I18n.t('captain.documents.sync_already_in_progress')) if @document.sync_in_progress?
@document.update!(
sync_status: :syncing,
diff --git a/enterprise/app/controllers/api/v1/accounts/conference_controller.rb b/enterprise/app/controllers/api/v1/accounts/conference_controller.rb
index 1123699d8..0bea29843 100644
--- a/enterprise/app/controllers/api/v1/accounts/conference_controller.rb
+++ b/enterprise/app/controllers/api/v1/accounts/conference_controller.rb
@@ -27,7 +27,10 @@ class Api::V1::Accounts::ConferenceController < Api::V1::Accounts::BaseControlle
def destroy
call = resolve_call!
+ rejecting = agent_rejecting_before_pickup?(call)
+ # Tear down provider side first so a teardown failure leaves the call repairable.
Voice::Provider::Twilio::ConferenceService.new(call: call).end_conference
+ finalize_as_agent_reject!(call) if rejecting
render json: { status: 'success', id: call.conversation.display_id }
end
@@ -59,4 +62,21 @@ class Api::V1::Accounts::ConferenceController < Api::V1::Accounts::BaseControlle
def render_call_already_accepted(error)
render json: { error: error.message }, status: :conflict
end
+
+ # A hangup before pickup is treated as an agent rejection, matching WhatsApp.
+ def agent_rejecting_before_pickup?(call)
+ call.ringing? && call.accepted_by_agent_id.nil?
+ end
+
+ def finalize_as_agent_reject!(call)
+ # Re-check under a row lock: a webhook may have accepted/completed the call
+ # while end_conference was in flight, so don't force agent_rejected on stale state.
+ rejected = call.with_lock do
+ next false unless agent_rejecting_before_pickup?(call)
+
+ call.update!(status: 'failed', end_reason: 'agent_rejected', accepted_by_agent_id: Current.user.id)
+ true
+ end
+ Voice::CallMessageBuilder.new(call).update_status!(status: 'failed', agent: Current.user) if rejected
+ end
end
diff --git a/enterprise/app/controllers/api/v1/accounts/whatsapp_calls_controller.rb b/enterprise/app/controllers/api/v1/accounts/whatsapp_calls_controller.rb
index b54940586..0301d428b 100644
--- a/enterprise/app/controllers/api/v1/accounts/whatsapp_calls_controller.rb
+++ b/enterprise/app/controllers/api/v1/accounts/whatsapp_calls_controller.rb
@@ -35,8 +35,14 @@ class Api::V1::Accounts::WhatsappCallsController < Api::V1::Accounts::BaseContro
def initiate
@call = create_outbound_call
- @message = Voice::CallMessageBuilder.new(@call).perform!
- @call.update!(message_id: @message.id)
+ # Link the call to its message in one transaction so the message.created
+ # broadcast (an after_create_commit hook) fires only once call.message_id is
+ # set. Otherwise the live ringing bubble receives a message with no `call`
+ # payload (no direction/agent) and renders "Calling…" instead of "Handled by …".
+ ActiveRecord::Base.transaction do
+ @message = Voice::CallMessageBuilder.new(@call).perform!
+ @call.update!(message_id: @message.id)
+ end
end
private
diff --git a/enterprise/app/controllers/enterprise/api/v1/accounts/inboxes_controller.rb b/enterprise/app/controllers/enterprise/api/v1/accounts/inboxes_controller.rb
index f9d828806..76f578bf1 100644
--- a/enterprise/app/controllers/enterprise/api/v1/accounts/inboxes_controller.rb
+++ b/enterprise/app/controllers/enterprise/api/v1/accounts/inboxes_controller.rb
@@ -3,12 +3,38 @@ module Enterprise::Api::V1::Accounts::InboxesController
super + ee_inbox_attributes
end
+ def enable_whatsapp_calling
+ return unless ensure_whatsapp_calling_supported
+
+ @inbox.channel.enable_voice_calling!
+ head :ok
+ rescue StandardError => e
+ render_could_not_create_error(e.message)
+ end
+
+ def disable_whatsapp_calling
+ return unless ensure_whatsapp_calling_supported
+
+ @inbox.channel.disable_voice_calling!
+ head :ok
+ rescue StandardError => e
+ render_could_not_create_error(e.message)
+ end
+
def ee_inbox_attributes
[auto_assignment_config: [:max_assignment_limit]]
end
private
+ def ensure_whatsapp_calling_supported
+ channel = @inbox.channel
+ return true if channel.is_a?(Channel::Whatsapp) && channel.voice_calling_supported?
+
+ render_could_not_create_error('Inbox does not support WhatsApp calling')
+ false
+ end
+
def allowed_channel_types
super + ['voice']
end
diff --git a/enterprise/app/controllers/enterprise/public/api/v1/portals/search_controller.rb b/enterprise/app/controllers/enterprise/public/api/v1/portals/search_controller.rb
new file mode 100644
index 000000000..2bb4164fb
--- /dev/null
+++ b/enterprise/app/controllers/enterprise/public/api/v1/portals/search_controller.rb
@@ -0,0 +1,9 @@
+module Enterprise::Public::Api::V1::Portals::SearchController
+ private
+
+ def search_articles
+ return super if @query.blank? || !@portal.account.feature_enabled?('help_center_embedding_search')
+
+ @articles = @articles.vector_search(search_params.merge(account_id: @portal.account_id, limit: nil))
+ end
+end
diff --git a/enterprise/app/jobs/captain/documents/perform_sync_job.rb b/enterprise/app/jobs/captain/documents/perform_sync_job.rb
index eaef7d64d..2a33db3f3 100644
--- a/enterprise/app/jobs/captain/documents/perform_sync_job.rb
+++ b/enterprise/app/jobs/captain/documents/perform_sync_job.rb
@@ -48,9 +48,7 @@ class Captain::Documents::PerformSyncJob < MutexApplicationJob
return if document.pdf_document?
with_lock(lock_key(document), LOCK_TIMEOUT) do
- mark_sync_started(document)
- result = Captain::Documents::SyncService.new(document.reload).perform
- log_sync_outcome(document, result: result, duration_ms: duration_ms_since(start_time))
+ perform_sync(document, start_time)
end
rescue LockAcquisitionError
log_sync_outcome(document, result: :already_syncing)
@@ -64,6 +62,12 @@ class Captain::Documents::PerformSyncJob < MutexApplicationJob
private
+ def perform_sync(document, start_time)
+ mark_sync_started(document)
+ result = Captain::Documents::SyncService.new(document.reload).perform
+ log_sync_outcome(document, result: result, duration_ms: duration_ms_since(start_time))
+ end
+
def log_sync_outcome(document, **fields)
payload = {
document_id: document.id,
diff --git a/enterprise/app/jobs/captain/documents/schedule_syncs_job.rb b/enterprise/app/jobs/captain/documents/schedule_syncs_job.rb
index 393a307db..82693c0bb 100644
--- a/enterprise/app/jobs/captain/documents/schedule_syncs_job.rb
+++ b/enterprise/app/jobs/captain/documents/schedule_syncs_job.rb
@@ -1,21 +1,27 @@
class Captain::Documents::ScheduleSyncsJob < ApplicationJob
queue_as :scheduled_jobs
- PER_ACCOUNT_HOURLY_CAP = 50
- GLOBAL_HOURLY_CAP = 1000
- DUE_DOCUMENT_BATCH_SIZE = PER_ACCOUNT_HOURLY_CAP * 2 # Inspite of skipping, we should at least reach the hourly cap
+ DEFAULT_PER_ACCOUNT_BATCH_LIMIT = 50
+ DEFAULT_GLOBAL_BATCH_LIMIT = 1000
SYNC_STALE_TIMEOUT = Captain::Document::SYNC_STALE_TIMEOUT
+ DAILY_SYNC_JITTER = 4.hours
+ WEEKLY_SYNC_JITTER = 1.day
+ MONTHLY_SYNC_JITTER = 4.days
- def perform
- @remaining_global_capacity = GLOBAL_HOURLY_CAP
+ def perform(plan_name = nil)
+ @per_account_batch_limit = configured_sync_limit('CAPTAIN_DOCUMENT_AUTO_SYNC_PER_ACCOUNT_BATCH_LIMIT', DEFAULT_PER_ACCOUNT_BATCH_LIMIT)
+ @global_batch_limit = configured_sync_limit('CAPTAIN_DOCUMENT_AUTO_SYNC_GLOBAL_BATCH_LIMIT', DEFAULT_GLOBAL_BATCH_LIMIT)
+ @remaining_global_capacity = @global_batch_limit
+ @plan_name = plan_name.to_s.downcase.presence
sync_intervals = Enterprise::Account.captain_document_sync_intervals
- stats = { accounts_scanned: 0, accounts_enabled: 0, accounts_scheduled: 0, documents_enqueued: 0, documents_skipped: 0 }
+ stats = { accounts_scanned: 0, accounts_enabled: 0, accounts_scheduled: 0, documents_enqueued: 0 }
Account.joins(:captain_documents).distinct.find_each(batch_size: 100) do |account|
break if @remaining_global_capacity <= 0
stats[:accounts_scanned] += 1
next unless account.feature_enabled?('captain_document_auto_sync')
+ next unless account_in_selected_plan?(account)
stats[:accounts_enabled] += 1
interval = account.captain_document_sync_interval(sync_intervals)
@@ -24,7 +30,6 @@ class Captain::Documents::ScheduleSyncsJob < ApplicationJob
stats[:accounts_scheduled] += 1
result = enqueue_due_documents(account, interval)
stats[:documents_enqueued] += result[:enqueued]
- stats[:documents_skipped] += result[:skipped]
end
log_scheduler_summary(stats)
@@ -33,92 +38,85 @@ class Captain::Documents::ScheduleSyncsJob < ApplicationJob
private
def enqueue_due_documents(account, interval)
- per_account_limit = [PER_ACCOUNT_HOURLY_CAP, @remaining_global_capacity].min
- result = { enqueued: 0, skipped: 0 }
- skipped_document_ids = []
+ per_account_limit = [@per_account_batch_limit, @remaining_global_capacity].min
+ result = { enqueued: 0 }
- while result[:enqueued] < per_account_limit
-
- documents = due_documents(account, interval, skipped_document_ids).limit(DUE_DOCUMENT_BATCH_SIZE).to_a
- break if documents.empty?
-
- documents.each do |document|
- break if result[:enqueued] >= per_account_limit
-
- process_due_document(document, result, skipped_document_ids)
- end
+ due_documents(account, interval).limit(per_account_limit).to_a.each do |document|
+ process_due_document(document, interval, result)
end
result
end
- def process_due_document(document, result, skipped_document_ids)
- return unless document.syncable?
+ def process_due_document(document, interval, result)
+ sync_execution_delay = sync_jitter(interval)
- # Reserve the sync slot before enqueueing so later scheduler runs skip this document while the job is queued.
- unless reserve_sync_slot(document)
- result[:skipped] += 1
- skipped_document_ids << document.id
- return
- end
-
- Captain::Documents::PerformSyncJob.perform_later(document)
+ Captain::Documents::PerformSyncJob.set(queue: :purgable, wait: sync_execution_delay).perform_later(document)
@remaining_global_capacity -= 1
result[:enqueued] += 1
end
- def due_documents(account, interval, skipped_document_ids)
+ def due_documents(account, interval)
syncing = Captain::Document.sync_statuses[:syncing]
synced = Captain::Document.sync_statuses[:synced]
failed = Captain::Document.sync_statuses[:failed]
+ stale_cutoff = SYNC_STALE_TIMEOUT.ago
+ # The scheduler runs at predictable plan windows. Use a wider due window so
+ # jittered executions do not miss the next window just because they finished later.
+ sync_due_before = due_window(interval).ago
documents = account.captain_documents.syncable.where(status: :available).where(
'(sync_status = ? AND last_synced_at < ?) OR (sync_status = ? AND last_sync_attempted_at < ?) OR ' \
'(sync_status = ? AND last_sync_attempted_at < ?)',
- synced, interval.ago, failed, interval.ago, syncing, SYNC_STALE_TIMEOUT.ago
+ synced, sync_due_before, failed, sync_due_before, syncing, stale_cutoff
)
- documents = documents.where.not(id: skipped_document_ids) if skipped_document_ids.present?
documents.order(Arel.sql('last_sync_attempted_at ASC NULLS FIRST'), :id)
end
- def reserve_sync_slot(document)
- mark_sync_started(document)
- true
- rescue ActiveRecord::RecordInvalid => e
- log_document_skip(document, e)
- false
+ def configured_sync_limit(config_key, default)
+ configured_value = InstallationConfig.find_by(name: config_key)&.value
+ limit = configured_value.to_s.to_i
+ limit.positive? ? limit : default
end
- def log_document_skip(document, error)
- payload = {
- event: 'document_skipped',
- document_id: document.id,
- account_id: document.account_id,
- assistant_id: document.assistant_id,
- error_class: error.class.name,
- error_message: error.message,
- validation_errors: document.errors.full_messages
- }
+ def account_in_selected_plan?(account)
+ return true if @plan_name.blank?
- Rails.logger.warn("[Captain::Documents::ScheduleSyncsJob] #{payload.to_json}")
+ account_sync_plan(account) == @plan_name
+ end
+
+ def account_sync_plan(account)
+ plan = account.custom_attributes['plan_name']
+ plan = 'enterprise' if plan.blank? && ChatwootApp.self_hosted_enterprise?
+ plan.to_s.downcase.presence
+ end
+
+ def sync_jitter(interval)
+ jitter_window = if interval <= 1.day
+ DAILY_SYNC_JITTER
+ elsif interval <= 1.week
+ WEEKLY_SYNC_JITTER
+ else
+ MONTHLY_SYNC_JITTER
+ end
+
+ rand(0..jitter_window.to_i).seconds
+ end
+
+ def due_window(interval)
+ (interval.to_i / 2).seconds
end
def log_scheduler_summary(stats)
payload = {
event: 'completed',
+ plan_name: @plan_name,
global_cap_hit: @remaining_global_capacity <= 0,
+ per_account_batch_limit: @per_account_batch_limit,
+ global_batch_limit: @global_batch_limit,
remaining_global_capacity: @remaining_global_capacity
}.merge(stats)
Rails.logger.info("[Captain::Documents::ScheduleSyncsJob] #{payload.to_json}")
end
-
- def mark_sync_started(document)
- document.update!(
- sync_status: :syncing,
- sync_step: nil,
- last_sync_error_code: nil,
- last_sync_attempted_at: Time.current
- )
- end
end
diff --git a/enterprise/app/jobs/enterprise/internal/trigger_daily_scheduled_items_job.rb b/enterprise/app/jobs/enterprise/internal/trigger_daily_scheduled_items_job.rb
new file mode 100644
index 000000000..aa22310fb
--- /dev/null
+++ b/enterprise/app/jobs/enterprise/internal/trigger_daily_scheduled_items_job.rb
@@ -0,0 +1,19 @@
+module Enterprise::Internal::TriggerDailyScheduledItemsJob
+ def perform
+ super
+
+ Captain::Documents::ScheduleSyncsJob.perform_later('enterprise')
+ Captain::Documents::ScheduleSyncsJob.perform_later('business') if business_auto_sync_due?
+ Captain::Documents::ScheduleSyncsJob.perform_later('startups') if startup_auto_sync_due?
+ end
+
+ private
+
+ def business_auto_sync_due?
+ Time.current.utc.sunday?
+ end
+
+ def startup_auto_sync_due?
+ Time.current.utc.day == 1
+ end
+end
diff --git a/enterprise/app/jobs/enterprise/internal/trigger_hourly_scheduled_items_job.rb b/enterprise/app/jobs/enterprise/internal/trigger_hourly_scheduled_items_job.rb
deleted file mode 100644
index 9d3baa2d6..000000000
--- a/enterprise/app/jobs/enterprise/internal/trigger_hourly_scheduled_items_job.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-module Enterprise::Internal::TriggerHourlyScheduledItemsJob
- def perform
- super
-
- Captain::Documents::ScheduleSyncsJob.perform_later
- end
-end
diff --git a/enterprise/app/jobs/enterprise/webhooks/whatsapp_events_job.rb b/enterprise/app/jobs/enterprise/webhooks/whatsapp_events_job.rb
index b1e2bc053..db411f068 100644
--- a/enterprise/app/jobs/enterprise/webhooks/whatsapp_events_job.rb
+++ b/enterprise/app/jobs/enterprise/webhooks/whatsapp_events_job.rb
@@ -31,10 +31,11 @@ module Enterprise::Webhooks::WhatsappEventsJob
# and timer/recorder kick off before the contact actually answers.
def handle_call_events(channel, params)
value = params.dig(:entry, 0, :changes, 0, :value) || {}
+ contacts = value[:contacts]
Array(value[:calls]).each do |call_payload|
with_call_lock(channel, call_payload[:id]) do
- Whatsapp::IncomingCallService.new(inbox: channel.inbox, params: { calls: [call_payload] }).perform
+ Whatsapp::IncomingCallService.new(inbox: channel.inbox, params: { calls: [call_payload], contacts: contacts }).perform
end
end
diff --git a/enterprise/app/models/call.rb b/enterprise/app/models/call.rb
index f8c0580f8..e111cdd48 100644
--- a/enterprise/app/models/call.rb
+++ b/enterprise/app/models/call.rb
@@ -116,6 +116,7 @@ class Call < ApplicationRecord
direction: direction,
status: display_status,
duration_seconds: duration_seconds,
+ end_reason: end_reason,
conference_sid: conference_sid,
accepted_by_agent_id: accepted_by_agent_id,
accepted_by_agent_name: accepted_by_agent&.available_name,
diff --git a/enterprise/app/models/captain/document.rb b/enterprise/app/models/captain/document.rb
index d4fb400f5..e83df757e 100644
--- a/enterprise/app/models/captain/document.rb
+++ b/enterprise/app/models/captain/document.rb
@@ -4,7 +4,8 @@
#
# id :bigint not null, primary key
# content :text
-# external_link :string not null
+# content_fingerprint :string
+# external_link :text not null
# last_sync_attempted_at :datetime
# last_synced_at :datetime
# metadata :jsonb
@@ -18,12 +19,12 @@
#
# Indexes
#
-# idx_captain_documents_on_account_assistant_sync_stats (account_id,assistant_id,sync_status,last_synced_at)
-# index_captain_documents_on_account_id (account_id)
-# index_captain_documents_on_account_id_and_sync_status (account_id,sync_status)
-# index_captain_documents_on_assistant_id (assistant_id)
-# index_captain_documents_on_assistant_id_and_external_link (assistant_id,external_link) UNIQUE
-# index_captain_documents_on_status (status)
+# idx_captain_documents_on_account_assistant_sync_stats (account_id,assistant_id,sync_status,last_synced_at)
+# idx_captain_documents_on_assistant_id_and_external_link_md5 (assistant_id, md5(external_link)) UNIQUE
+# index_captain_documents_on_account_id (account_id)
+# index_captain_documents_on_account_id_and_sync_status (account_id,sync_status)
+# index_captain_documents_on_assistant_id (assistant_id)
+# index_captain_documents_on_status (status)
#
class Captain::Document < ApplicationRecord
class LimitExceededError < StandardError; end
diff --git a/enterprise/app/models/enterprise/concerns/article.rb b/enterprise/app/models/enterprise/concerns/article.rb
index 4527bfdf6..9482313fd 100644
--- a/enterprise/app/models/enterprise/concerns/article.rb
+++ b/enterprise/app/models/enterprise/concerns/article.rb
@@ -26,13 +26,15 @@ module Enterprise::Concerns::Article
# if using add the filter block to the below query
# .filter { |ae| ae.neighbor_distance <= distance_threshold }
- article_ids = ArticleEmbedding.where(article_id: filtered_article_ids)
- .nearest_neighbors(:embedding, embedding, distance: 'cosine')
- .limit(5)
- .pluck(:article_id)
+ limit = params.key?(:limit) ? params[:limit] : 5
+
+ article_embeddings = ArticleEmbedding.where(article_id: filtered_article_ids)
+ .nearest_neighbors(:embedding, embedding, distance: 'cosine')
+ article_embeddings = article_embeddings.limit(limit) if limit.present?
+ article_ids = article_embeddings.pluck(:article_id)
# Fetch the articles by the IDs obtained from the nearest neighbors search
- where(id: article_ids)
+ where(id: article_ids).in_order_of(:id, article_ids)
end
end
diff --git a/enterprise/app/policies/enterprise/contact_policy.rb b/enterprise/app/policies/enterprise/contact_policy.rb
new file mode 100644
index 000000000..a11898007
--- /dev/null
+++ b/enterprise/app/policies/enterprise/contact_policy.rb
@@ -0,0 +1,9 @@
+module Enterprise::ContactPolicy
+ def export?
+ @account_user.custom_role&.permissions&.include?('contact_manage') || super
+ end
+
+ def import?
+ @account_user.custom_role&.permissions&.include?('contact_manage') || super
+ end
+end
diff --git a/enterprise/app/services/enterprise/billing/reconcile_plan_features_service.rb b/enterprise/app/services/enterprise/billing/reconcile_plan_features_service.rb
index 932cee661..a6f76f6b5 100644
--- a/enterprise/app/services/enterprise/billing/reconcile_plan_features_service.rb
+++ b/enterprise/app/services/enterprise/billing/reconcile_plan_features_service.rb
@@ -11,13 +11,23 @@ class Enterprise::Billing::ReconcilePlanFeaturesService
channel_facebook
channel_email
channel_instagram
+ channel_tiktok
captain_integration
advanced_search_indexing
advanced_search
linear_integration
+ channel_voice
].freeze
- BUSINESS_PLAN_FEATURES = %w[sla custom_roles csat_review_notes conversation_required_attributes advanced_assignment custom_tools].freeze
+ BUSINESS_PLAN_FEATURES = %w[
+ sla
+ custom_roles
+ csat_review_notes
+ conversation_required_attributes
+ advanced_assignment
+ custom_tools
+ companies
+ ].freeze
ENTERPRISE_PLAN_FEATURES = %w[audit_logs disable_branding saml].freeze
PREMIUM_PLAN_FEATURES = (STARTUP_PLAN_FEATURES + BUSINESS_PLAN_FEATURES + ENTERPRISE_PLAN_FEATURES).freeze
diff --git a/enterprise/app/services/enterprise/website_branding_service.rb b/enterprise/app/services/enterprise/website_branding_service.rb
index 553317c43..c6f2fbdaa 100644
--- a/enterprise/app/services/enterprise/website_branding_service.rb
+++ b/enterprise/app/services/enterprise/website_branding_service.rb
@@ -58,6 +58,7 @@ module Enterprise::WebsiteBrandingService
socials: brand['socials'] || [],
links: brand['links'],
email: @email,
+ email_provider: detect_email_provider,
industries: brand.dig('industries', 'eic') || [],
stock: brand['stock'],
is_nsfw: brand['is_nsfw'] || false
diff --git a/enterprise/app/services/enterprise/whatsapp/providers/whatsapp_cloud_service.rb b/enterprise/app/services/enterprise/whatsapp/providers/whatsapp_cloud_service.rb
index ec29a5a38..2fcf4b5e7 100644
--- a/enterprise/app/services/enterprise/whatsapp/providers/whatsapp_cloud_service.rb
+++ b/enterprise/app/services/enterprise/whatsapp/providers/whatsapp_cloud_service.rb
@@ -40,6 +40,22 @@ module Enterprise::Whatsapp::Providers::WhatsappCloudService
process_initiate_call_response(response)
end
+ # Sets WABA calling status ('ENABLED'/'DISABLED'). Returns true, or raises with
+ # Meta's user-facing message on failure so the caller can surface it.
+ def update_calling_status(status)
+ response = HTTParty.post(
+ "#{calls_phone_id_path}/settings",
+ headers: api_headers,
+ body: { calling: { status: status } }.to_json
+ )
+ return true if response.success?
+
+ parsed = response.parsed_response.is_a?(Hash) ? response.parsed_response : {}
+ message = parsed.dig('error', 'error_user_msg') || parsed.dig('error', 'message') || 'Failed to update calling status'
+ Rails.logger.error "[WHATSAPP CALL] update_calling_status failed: status=#{response.code} body=#{response.body}"
+ raise message
+ end
+
private
def calls_phone_id_path
diff --git a/enterprise/app/services/messages/audio_transcription_service.rb b/enterprise/app/services/messages/audio_transcription_service.rb
index c502b717a..748bf1efa 100644
--- a/enterprise/app/services/messages/audio_transcription_service.rb
+++ b/enterprise/app/services/messages/audio_transcription_service.rb
@@ -1,11 +1,12 @@
class Messages::AudioTranscriptionService< Llm::LegacyBaseOpenAiService
include Integrations::LlmInstrumentation
- WHISPER_MODEL = 'whisper-1'.freeze
- # Whisper's hard limit is 25 MB *decimal* (25_000_000), not binary (25.megabytes
- # = 26_214_400) — using the binary form leaks the 25.0–26.2 MB range to the API
- # as 413s. Long audio (~70+ min Opus) keeps the attachment but skips transcription.
- WHISPER_BYTE_LIMIT = 25_000_000
+ TRANSCRIPTION_MODEL = 'gpt-4o-mini-transcribe'.freeze
+ # OpenAI's transcription endpoint hard limit is 25 MB *decimal* (25_000_000), not
+ # binary (25.megabytes = 26_214_400) — using the binary form leaks the 25.0–26.2 MB
+ # range to the API as 413s. Long audio (~70+ min Opus) keeps the attachment but skips
+ # transcription.
+ TRANSCRIPTION_BYTE_LIMIT = 25_000_000
attr_reader :attachment, :message, :account
@@ -42,7 +43,7 @@ class Messages::AudioTranscriptionService< Llm::LegacyBaseOpenAiService
blob = attachment.file&.blob
return false unless blob
- blob.byte_size > WHISPER_BYTE_LIMIT
+ blob.byte_size > TRANSCRIPTION_BYTE_LIMIT
end
def fetch_audio_file
@@ -75,12 +76,12 @@ class Messages::AudioTranscriptionService< Llm::LegacyBaseOpenAiService
transcribed_text = nil
File.open(temp_file_path, 'rb') do |file|
- # temperature: 0.0 minimises Whisper's hallucinations on silence /
- # near-silent audio; non-zero values trigger spiraling repeats like
- # "Oh, dear. Oh, dear. Oh, dear." — well-documented Whisper behaviour.
+ # temperature: 0.0 minimises hallucinations on silence / near-silent
+ # audio; non-zero values trigger spiraling repeats — well-documented
+ # behaviour across OpenAI transcription models.
response = @client.audio.transcribe(
parameters: {
- model: WHISPER_MODEL,
+ model: TRANSCRIPTION_MODEL,
file: file,
temperature: 0.0
}
@@ -97,7 +98,7 @@ class Messages::AudioTranscriptionService< Llm::LegacyBaseOpenAiService
def instrumentation_params(file_path)
{
span_name: 'llm.messages.audio_transcription',
- model: WHISPER_MODEL,
+ model: TRANSCRIPTION_MODEL,
account_id: account&.id,
feature_name: 'audio_transcription',
file_path: file_path
diff --git a/enterprise/app/services/voice/call_status/manager.rb b/enterprise/app/services/voice/call_status/manager.rb
index 73ace3a78..942ee0cc0 100644
--- a/enterprise/app/services/voice/call_status/manager.rb
+++ b/enterprise/app/services/voice/call_status/manager.rb
@@ -4,6 +4,9 @@ class Voice::CallStatus::Manager
def process_status_update(status, duration: nil, timestamp: nil)
return unless Call::STATUSES.include?(status)
return if call.status == status
+ # Don't overwrite a terminal status — Twilio's late `completed` events would
+ # otherwise clobber an agent-rejection reason.
+ return if Call::TERMINAL_STATUSES.include?(call.status)
apply_call_updates!(status, duration: duration, timestamp: timestamp)
call.conversation.update!(last_activity_at: Time.zone.now)
diff --git a/enterprise/app/services/voice/inbound_call_builder.rb b/enterprise/app/services/voice/inbound_call_builder.rb
index 7b8b0e684..eef70e76b 100644
--- a/enterprise/app/services/voice/inbound_call_builder.rb
+++ b/enterprise/app/services/voice/inbound_call_builder.rb
@@ -59,9 +59,17 @@ class Voice::InboundCallBuilder
end
def ensure_contact!
- account.contacts.find_or_create_by!(phone_number: from_number) do |record|
- record.name = from_number if record.name.blank?
+ contact = account.contacts.find_or_create_by!(phone_number: from_number) do |record|
+ record.name = contact_name.presence || from_number
end
+ contact.update!(name: contact_name) if contact_name.present? && contact.name == from_number
+ contact
+ end
+
+ # WhatsApp inbound calls carry the caller's profile name in extra_meta; Twilio
+ # calls don't, so contact naming falls back to the phone number.
+ def contact_name
+ extra_meta['contact_name'].presence
end
# WhatsApp ContactInbox.source_id must be digits-only (the wa_id); Twilio accepts the +.
@@ -74,15 +82,14 @@ class Voice::InboundCallBuilder
Whatsapp::PhoneNumberNormalizationService.new(inbox).normalize_and_find_contact_by_provider(digits, :cloud)
end
+ # Mirror incoming-message routing: reuse the open conversation (or the last one when locked), else create new.
def resolve_conversation!(contact, contact_inbox)
- if inbox.lock_to_single_conversation
- reusable = account.conversations
- .where(contact_id: contact.id, inbox_id: inbox.id)
- .where.not(status: :resolved)
- .order(last_activity_at: :desc)
- .first
- return reusable if reusable
- end
+ reusable = if inbox.lock_to_single_conversation
+ contact_inbox.conversations.last
+ else
+ contact_inbox.conversations.where.not(status: :resolved).last
+ end
+ return reusable if reusable
account.conversations.create!(
contact_inbox_id: contact_inbox.id,
diff --git a/enterprise/app/services/whatsapp/call_service.rb b/enterprise/app/services/whatsapp/call_service.rb
index 8a52ea6bc..93eba957c 100644
--- a/enterprise/app/services/whatsapp/call_service.rb
+++ b/enterprise/app/services/whatsapp/call_service.rb
@@ -20,7 +20,8 @@ class Whatsapp::CallService
next if call.terminal? || call.in_progress?
invoke_provider!(:reject_call)
- finalize_call('failed')
+ call.update!(accepted_by_agent_id: agent.id) if call.accepted_by_agent_id.nil?
+ finalize_call('failed', end_reason: 'agent_rejected')
end
call
end
diff --git a/enterprise/app/services/whatsapp/incoming_call_service.rb b/enterprise/app/services/whatsapp/incoming_call_service.rb
index d290e96c2..afca508a8 100644
--- a/enterprise/app/services/whatsapp/incoming_call_service.rb
+++ b/enterprise/app/services/whatsapp/incoming_call_service.rb
@@ -73,15 +73,27 @@ class Whatsapp::IncomingCallService
def create_inbound_call(payload)
sdp_offer = payload.dig(:session, :sdp)
+ extra_meta = { 'sdp_offer' => sdp_offer, 'ice_servers' => Call.default_ice_servers }
+ name = caller_profile_name(payload)
+ extra_meta['contact_name'] = name if name.present?
+
call = Voice::InboundCallBuilder.perform!(
inbox: inbox, from_number: "+#{payload[:from]}", call_sid: payload[:id],
- provider: :whatsapp,
- extra_meta: { 'sdp_offer' => sdp_offer, 'ice_servers' => Call.default_ice_servers }
+ provider: :whatsapp, extra_meta: extra_meta
)
update_conversation(call)
broadcast_incoming(call, sdp_offer)
end
+ # Match strictly on wa_id (== calls[].from): in a batched payload missing this
+ # call's contact entry, borrowing another caller's name would corrupt this
+ # contact, so fall back to the phone number (nil here) instead of contacts.first.
+ def caller_profile_name(payload)
+ contacts = Array(params[:contacts]).map(&:with_indifferent_access)
+ match = contacts.find { |c| c[:wa_id].to_s == payload[:from].to_s }
+ match&.dig(:profile, :name).presence
+ end
+
# `connect` is the WebRTC tunnel-ready signal, not the pickup signal. Apply
# Meta's SDP answer so the handshake completes during ringing; the call
# stays in `ringing` until status=ACCEPTED arrives. Don't gate on
@@ -159,17 +171,29 @@ class Whatsapp::IncomingCallService
)
end
- # Ring the assignee if assigned; otherwise account-wide so any agent can pick up.
+ # Ring the assignee if any, else online inbox agents, else fall back to the
+ # inbox's own agents and account admins. Never the whole-account stream, which
+ # would ring online agents from unrelated inboxes.
def broadcast_incoming(call, sdp_offer)
contact = call.contact
token = call.conversation.assignee&.pubsub_token
+ streams = token ? [token] : (online_agent_streams.presence || fallback_agent_streams)
broadcast(call, 'voice_call.incoming',
- streams: token ? [token] : account_streams,
+ streams: streams,
direction: call.direction_label, inbox_id: call.inbox_id,
sdp_offer: sdp_offer, ice_servers: Call.default_ice_servers,
caller: { name: contact.name, phone: contact.phone_number, avatar: contact.avatar_url })
end
+ def online_agent_streams
+ inbox.available_agents.pluck('users.pubsub_token').compact
+ end
+
+ def fallback_agent_streams
+ user_ids = inbox.member_ids | inbox.account.administrators.ids
+ User.where(id: user_ids).pluck(:pubsub_token).compact
+ end
+
def broadcast(call, event, streams: account_streams, **extra)
payload = { event: event, data: base_payload(call).merge(extra) }
streams.each { |s| ActionCable.server.broadcast(s, payload) }
diff --git a/lib/safe_fetch.rb b/lib/safe_fetch.rb
index 7f89c03c4..f635758af 100644
--- a/lib/safe_fetch.rb
+++ b/lib/safe_fetch.rb
@@ -34,4 +34,8 @@ module SafeFetch
rescue Net::OpenTimeout, Net::ReadTimeout, SocketError, OpenSSL::SSL::SSLError => e
raise FetchError, e.message
end
+
+ def self.allow_private_network?
+ ActiveModel::Type::Boolean.new.cast(ENV.fetch('SAFE_FETCH_ALLOW_PRIVATE_NETWORK', false))
+ end
end
diff --git a/lib/safe_fetch/fetcher.rb b/lib/safe_fetch/fetcher.rb
index fa3c01f55..ab8a9863c 100644
--- a/lib/safe_fetch/fetcher.rb
+++ b/lib/safe_fetch/fetcher.rb
@@ -29,18 +29,20 @@ class SafeFetch::Fetcher
end
def stream_response(tempfile)
- response = nil
bytes_written = 0
- SsrfFilter.public_send(options.method, options.url, **options.request_options) do |res|
- response = res
+ perform_request do |res|
next unless res.is_a?(Net::HTTPSuccess)
validate_content_type!(res['content-type'])
bytes_written = write_response_body(res, tempfile, bytes_written)
end
+ end
- response
+ def perform_request(&)
+ return SafeFetch::PrivateNetworkRequest.new(options).perform(&) if SafeFetch.allow_private_network?
+
+ SsrfFilter.public_send(options.method, options.url, **options.request_options, &)
end
def validate_content_type!(content_type)
diff --git a/lib/safe_fetch/private_network_request.rb b/lib/safe_fetch/private_network_request.rb
new file mode 100644
index 000000000..9e9740ef4
--- /dev/null
+++ b/lib/safe_fetch/private_network_request.rb
@@ -0,0 +1,102 @@
+class SafeFetch::PrivateNetworkRequest
+ def initialize(options)
+ @options = options
+ end
+
+ def perform(&)
+ url = options.url
+ original_url = url
+ original_uri = URI(url)
+
+ (SsrfFilter::DEFAULT_MAX_REDIRECTS + 1).times do
+ uri = URI(url)
+ validate_scheme!(uri)
+
+ response, next_url = fetch_once(uri, resolved_addresses(uri.hostname).sample.to_s, original_uri, &)
+ return response if next_url.nil?
+
+ url = next_url
+ end
+
+ raise SsrfFilter::TooManyRedirects, "Got #{SsrfFilter::DEFAULT_MAX_REDIRECTS} redirects fetching #{original_url}"
+ end
+
+ private
+
+ attr_reader :options
+
+ def validate_scheme!(uri)
+ return if SsrfFilter::DEFAULT_SCHEME_WHITELIST.include?(uri.scheme)
+
+ raise SsrfFilter::InvalidUriScheme, "URI scheme '#{uri.scheme}' not in whitelist: #{SsrfFilter::DEFAULT_SCHEME_WHITELIST}"
+ end
+
+ def resolved_addresses(hostname)
+ ip_addresses = options.resolver.call(hostname)
+ raise SsrfFilter::UnresolvedHostname, "Could not resolve hostname '#{hostname}'" if ip_addresses.empty?
+
+ ip_addresses
+ end
+
+ def fetch_once(uri, ip_address, original_uri, &)
+ request = build_request(uri)
+ strip_sensitive_headers!(request, original_uri, uri)
+ validate_request!(request)
+
+ Net::HTTP.start(uri.hostname, uri.port, **http_options(uri, ip_address)) do |http|
+ response = http.request(request, &)
+ return response, redirect_location(response, uri)
+ end
+ end
+
+ def build_request(uri)
+ request = SsrfFilter::VERB_MAP[options.method].new(uri)
+ request['host'] = normalized_hostname(uri)
+
+ Array(options.request_options[:headers]).each { |header, value| request[header] = value }
+ request.body = options.body if options.body
+ options.request_options[:request_proc].call(request) if options.request_options[:request_proc].respond_to?(:call)
+
+ request
+ end
+
+ def http_options(uri, ip_address)
+ options.request_options[:http_options].merge(
+ use_ssl: uri.scheme == 'https',
+ ipaddr: ip_address
+ )
+ end
+
+ def strip_sensitive_headers!(request, original_uri, uri)
+ return unless different_origin?(original_uri, uri)
+
+ options.request_options[:sensitive_headers].each { |header| request.delete(header) }
+ end
+
+ def validate_request!(request)
+ request.each do |header, value|
+ next if header.count("\r\n").zero? && value.count("\r\n").zero?
+
+ raise SsrfFilter::CRLFInjection, "CRLF injection in header #{header} with value #{value}"
+ end
+ end
+
+ def redirect_location(response, uri)
+ return unless response.is_a?(Net::HTTPRedirection)
+
+ location = response['location']
+ return "#{uri.scheme}://#{normalized_hostname(uri)}#{location}" if location&.start_with?('/')
+
+ location
+ end
+
+ def normalized_hostname(uri)
+ return uri.hostname if (uri.port == 80 && uri.scheme == 'http') || (uri.port == 443 && uri.scheme == 'https')
+
+ "#{uri.hostname}:#{uri.port}"
+ end
+
+ def different_origin?(uri, other_uri)
+ uri.scheme != other_uri.scheme || uri.hostname != other_uri.hostname || uri.port != other_uri.port
+ end
+end
diff --git a/lib/safe_fetch/request_options.rb b/lib/safe_fetch/request_options.rb
index 72969a76d..6d11ebd19 100644
--- a/lib/safe_fetch/request_options.rb
+++ b/lib/safe_fetch/request_options.rb
@@ -53,6 +53,10 @@ class SafeFetch::RequestOptions
@validate_content_type
end
+ def resolver
+ SsrfFilter::DEFAULT_RESOLVER
+ end
+
private
def default_max_bytes
diff --git a/package.json b/package.json
index ed8bbf00f..081c706c6 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@chatwoot/chatwoot",
- "version": "4.14.0",
+ "version": "4.14.1",
"license": "MIT",
"scripts": {
"eslint": "eslint app/**/*.{js,vue}",
@@ -12,7 +12,7 @@
"start:test": "RAILS_ENV=test foreman start -f ./Procfile.test",
"dev": "overmind start -f ./Procfile.dev",
"ruby:prettier": "bundle exec rubocop -a",
- "build:sdk": "BUILD_MODE=library vite build",
+ "build:sdk": "vite build --config vite.lib.config.ts",
"prepare": "husky install",
"size": "size-limit",
"story:dev": "histoire dev",
@@ -35,11 +35,12 @@
"@breezystack/lamejs": "^1.2.7",
"@chatwoot/ninja-keys": "1.2.3",
"@chatwoot/prosemirror-schema": "1.3.13",
- "@chatwoot/utils": "^0.0.52",
+ "@chatwoot/utils": "^0.0.55",
"@formkit/core": "^1.7.2",
"@formkit/vue": "^1.7.2",
"@hcaptcha/vue3-hcaptcha": "^1.3.0",
"@highlightjs/vue-plugin": "^2.1.0",
+ "@hotwired/turbo-rails": "^8.0.13",
"@iconify-json/fluent": "^1.2.32",
"@iconify-json/material-symbols": "^1.2.10",
"@lk77/vue3-color": "^3.0.6",
@@ -52,7 +53,7 @@
"@tailwindcss/typography": "^0.5.19",
"@tanstack/vue-table": "^8.20.5",
"@twilio/voice-sdk": "^2.12.4",
- "@vitejs/plugin-vue": "^5.1.4",
+ "@vitejs/plugin-vue": "^5.2.4",
"@vue/compiler-sfc": "^3.5.8",
"@vuelidate/core": "^2.0.3",
"@vuelidate/validators": "^2.0.4",
@@ -92,7 +93,6 @@
"snakecase-keys": "^8.0.1",
"timezone-phone-codes": "^0.0.2",
"tinykeys": "^3.0.0",
- "turbolinks": "^5.2.0",
"urlpattern-polyfill": "^10.0.0",
"video.js": "7.21.1",
"videojs-record": "4.5.0",
@@ -146,8 +146,8 @@
"prosemirror-model": "^1.22.3",
"size-limit": "^8.2.4",
"tailwindcss": "^3.4.19",
- "vite": "^5.4.21",
- "vite-plugin-ruby": "^5.0.0",
+ "vite": "6.4.2",
+ "vite-plugin-ruby": "^5.2.1",
"vitest": "3.0.5"
},
"engines": {
@@ -161,8 +161,7 @@
},
"pnpm": {
"overrides": {
- "vite-node": "2.0.1",
- "vite": "5.4.21",
+ "vite": "6.4.2",
"vitest": "3.0.5",
"minimatch@<4": "3.1.5",
"minimatch@>=9.0.0 <9.0.7": "9.0.9",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 3f1e9e2cf..afcf5b60f 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -5,8 +5,7 @@ settings:
excludeLinksFromLockfile: false
overrides:
- vite-node: 2.0.1
- vite: 5.4.21
+ vite: 6.4.2
vitest: 3.0.5
minimatch@<4: 3.1.5
minimatch@>=9.0.0 <9.0.7: 9.0.9
@@ -29,8 +28,8 @@ importers:
specifier: 1.3.13
version: 1.3.13
'@chatwoot/utils':
- specifier: ^0.0.52
- version: 0.0.52
+ specifier: ^0.0.55
+ version: 0.0.55
'@formkit/core':
specifier: ^1.7.2
version: 1.7.2
@@ -43,6 +42,9 @@ importers:
'@highlightjs/vue-plugin':
specifier: ^2.1.0
version: 2.1.0(highlight.js@11.10.0)(vue@3.5.12(typescript@5.6.2))
+ '@hotwired/turbo-rails':
+ specifier: ^8.0.13
+ version: 8.0.13
'@iconify-json/fluent':
specifier: ^1.2.32
version: 1.2.36
@@ -80,8 +82,8 @@ importers:
specifier: ^2.12.4
version: 2.17.0
'@vitejs/plugin-vue':
- specifier: ^5.1.4
- version: 5.1.4(vite@5.4.21(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0))(vue@3.5.12(typescript@5.6.2))
+ specifier: ^5.2.4
+ version: 5.2.4(vite@6.4.2(@types/node@22.7.0)(jiti@1.21.6)(lightningcss@1.32.0)(sass@1.79.3)(terser@5.33.0)(yaml@2.8.2))(vue@3.5.12(typescript@5.6.2))
'@vue/compiler-sfc':
specifier: ^3.5.8
version: 3.5.8
@@ -199,9 +201,6 @@ importers:
tinykeys:
specifier: ^3.0.0
version: 3.0.0
- turbolinks:
- specifier: ^5.2.0
- version: 5.2.0
urlpattern-polyfill:
specifier: ^10.0.0
version: 10.0.0
@@ -262,7 +261,7 @@ importers:
version: 1.9.2(tailwindcss@3.4.19)
'@histoire/plugin-vue':
specifier: 0.17.15
- version: 0.17.15(histoire@0.17.15(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0)(vite@5.4.21(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0)))(vite@5.4.21(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0))(vue@3.5.12(typescript@5.6.2))
+ version: 0.17.15(histoire@0.17.15(@types/node@22.7.0)(lightningcss@1.32.0)(sass@1.79.3)(terser@5.33.0)(vite@6.4.2(@types/node@22.7.0)(jiti@1.21.6)(lightningcss@1.32.0)(sass@1.79.3)(terser@5.33.0)(yaml@2.8.2))(yaml@2.8.2))(vite@6.4.2(@types/node@22.7.0)(jiti@1.21.6)(lightningcss@1.32.0)(sass@1.79.3)(terser@5.33.0)(yaml@2.8.2))(vue@3.5.12(typescript@5.6.2))
'@iconify-json/logos':
specifier: ^1.2.10
version: 1.2.10
@@ -289,7 +288,7 @@ importers:
version: 8.2.6(size-limit@8.2.6)
'@vitest/coverage-v8':
specifier: 3.0.5
- version: 3.0.5(vitest@3.0.5(@types/node@22.7.0)(jsdom@27.2.0)(sass@1.79.3)(terser@5.33.0))
+ version: 3.0.5(vitest@3.0.5(@types/node@22.7.0)(jiti@1.21.6)(jsdom@27.2.0)(lightningcss@1.32.0)(sass@1.79.3)(terser@5.33.0)(yaml@2.8.2))
'@vue/test-utils':
specifier: ^2.4.6
version: 2.4.6
@@ -328,7 +327,7 @@ importers:
version: 6.0.0
histoire:
specifier: 0.17.15
- version: 0.17.15(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0)(vite@5.4.21(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0))
+ version: 0.17.15(@types/node@22.7.0)(lightningcss@1.32.0)(sass@1.79.3)(terser@5.33.0)(vite@6.4.2(@types/node@22.7.0)(jiti@1.21.6)(lightningcss@1.32.0)(sass@1.79.3)(terser@5.33.0)(yaml@2.8.2))(yaml@2.8.2)
husky:
specifier: ^7.0.0
version: 7.0.4
@@ -357,14 +356,14 @@ importers:
specifier: ^3.4.19
version: 3.4.19
vite:
- specifier: 5.4.21
- version: 5.4.21(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0)
+ specifier: 6.4.2
+ version: 6.4.2(@types/node@22.7.0)(jiti@1.21.6)(lightningcss@1.32.0)(sass@1.79.3)(terser@5.33.0)(yaml@2.8.2)
vite-plugin-ruby:
- specifier: ^5.0.0
- version: 5.0.0(vite@5.4.21(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0))
+ specifier: ^5.2.1
+ version: 5.2.1(vite@6.4.2(@types/node@22.7.0)(jiti@1.21.6)(lightningcss@1.32.0)(sass@1.79.3)(terser@5.33.0)(yaml@2.8.2))
vitest:
specifier: 3.0.5
- version: 3.0.5(@types/node@22.7.0)(jsdom@27.2.0)(sass@1.79.3)(terser@5.33.0)
+ version: 3.0.5(@types/node@22.7.0)(jiti@1.21.6)(jsdom@27.2.0)(lightningcss@1.32.0)(sass@1.79.3)(terser@5.33.0)(yaml@2.8.2)
packages:
@@ -462,8 +461,8 @@ packages:
'@chatwoot/prosemirror-schema@1.3.13':
resolution: {integrity: sha512-T6FBUinMJbwDCD7975g8M/Tsn2+G3O2pTGIXdcLkMRpbAAC6mVdl4ZcZektlt5y/PVmPVqNHPsfee1XB/C3vAw==}
- '@chatwoot/utils@0.0.52':
- resolution: {integrity: sha512-e57uVqyVW4tj1gql4YJPNMykqMJPkETn5Y9AmHdhc6Y7oxDXfRXBq27fZrrDadLkZdn5RYVCZjfIhXOumyYv2Q==}
+ '@chatwoot/utils@0.0.55':
+ resolution: {integrity: sha512-8G6HYQe1ZEYfJEsSYfDVvE+uhf98JDRjtGlpB+bzMko+yltbrk4yACSo/ImC3jSaJ6K8yPTSjJToSRmsQbL2iQ==}
engines: {node: '>=10'}
'@codemirror/commands@6.7.0':
@@ -710,141 +709,159 @@ packages:
peerDependencies:
tailwindcss: '*'
- '@esbuild/aix-ppc64@0.21.5':
- resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==}
- engines: {node: '>=12'}
+ '@esbuild/aix-ppc64@0.25.12':
+ resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==}
+ engines: {node: '>=18'}
cpu: [ppc64]
os: [aix]
- '@esbuild/android-arm64@0.21.5':
- resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==}
- engines: {node: '>=12'}
+ '@esbuild/android-arm64@0.25.12':
+ resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==}
+ engines: {node: '>=18'}
cpu: [arm64]
os: [android]
- '@esbuild/android-arm@0.21.5':
- resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==}
- engines: {node: '>=12'}
+ '@esbuild/android-arm@0.25.12':
+ resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==}
+ engines: {node: '>=18'}
cpu: [arm]
os: [android]
- '@esbuild/android-x64@0.21.5':
- resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==}
- engines: {node: '>=12'}
+ '@esbuild/android-x64@0.25.12':
+ resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==}
+ engines: {node: '>=18'}
cpu: [x64]
os: [android]
- '@esbuild/darwin-arm64@0.21.5':
- resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==}
- engines: {node: '>=12'}
+ '@esbuild/darwin-arm64@0.25.12':
+ resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==}
+ engines: {node: '>=18'}
cpu: [arm64]
os: [darwin]
- '@esbuild/darwin-x64@0.21.5':
- resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==}
- engines: {node: '>=12'}
+ '@esbuild/darwin-x64@0.25.12':
+ resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==}
+ engines: {node: '>=18'}
cpu: [x64]
os: [darwin]
- '@esbuild/freebsd-arm64@0.21.5':
- resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==}
- engines: {node: '>=12'}
+ '@esbuild/freebsd-arm64@0.25.12':
+ resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==}
+ engines: {node: '>=18'}
cpu: [arm64]
os: [freebsd]
- '@esbuild/freebsd-x64@0.21.5':
- resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==}
- engines: {node: '>=12'}
+ '@esbuild/freebsd-x64@0.25.12':
+ resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==}
+ engines: {node: '>=18'}
cpu: [x64]
os: [freebsd]
- '@esbuild/linux-arm64@0.21.5':
- resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==}
- engines: {node: '>=12'}
+ '@esbuild/linux-arm64@0.25.12':
+ resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==}
+ engines: {node: '>=18'}
cpu: [arm64]
os: [linux]
- '@esbuild/linux-arm@0.21.5':
- resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==}
- engines: {node: '>=12'}
+ '@esbuild/linux-arm@0.25.12':
+ resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==}
+ engines: {node: '>=18'}
cpu: [arm]
os: [linux]
- '@esbuild/linux-ia32@0.21.5':
- resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==}
- engines: {node: '>=12'}
+ '@esbuild/linux-ia32@0.25.12':
+ resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==}
+ engines: {node: '>=18'}
cpu: [ia32]
os: [linux]
- '@esbuild/linux-loong64@0.21.5':
- resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==}
- engines: {node: '>=12'}
+ '@esbuild/linux-loong64@0.25.12':
+ resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==}
+ engines: {node: '>=18'}
cpu: [loong64]
os: [linux]
- '@esbuild/linux-mips64el@0.21.5':
- resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==}
- engines: {node: '>=12'}
+ '@esbuild/linux-mips64el@0.25.12':
+ resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==}
+ engines: {node: '>=18'}
cpu: [mips64el]
os: [linux]
- '@esbuild/linux-ppc64@0.21.5':
- resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==}
- engines: {node: '>=12'}
+ '@esbuild/linux-ppc64@0.25.12':
+ resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==}
+ engines: {node: '>=18'}
cpu: [ppc64]
os: [linux]
- '@esbuild/linux-riscv64@0.21.5':
- resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==}
- engines: {node: '>=12'}
+ '@esbuild/linux-riscv64@0.25.12':
+ resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==}
+ engines: {node: '>=18'}
cpu: [riscv64]
os: [linux]
- '@esbuild/linux-s390x@0.21.5':
- resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==}
- engines: {node: '>=12'}
+ '@esbuild/linux-s390x@0.25.12':
+ resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==}
+ engines: {node: '>=18'}
cpu: [s390x]
os: [linux]
- '@esbuild/linux-x64@0.21.5':
- resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==}
- engines: {node: '>=12'}
+ '@esbuild/linux-x64@0.25.12':
+ resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==}
+ engines: {node: '>=18'}
cpu: [x64]
os: [linux]
- '@esbuild/netbsd-x64@0.21.5':
- resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==}
- engines: {node: '>=12'}
+ '@esbuild/netbsd-arm64@0.25.12':
+ resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [netbsd]
+
+ '@esbuild/netbsd-x64@0.25.12':
+ resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==}
+ engines: {node: '>=18'}
cpu: [x64]
os: [netbsd]
- '@esbuild/openbsd-x64@0.21.5':
- resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==}
- engines: {node: '>=12'}
+ '@esbuild/openbsd-arm64@0.25.12':
+ resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [openbsd]
+
+ '@esbuild/openbsd-x64@0.25.12':
+ resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==}
+ engines: {node: '>=18'}
cpu: [x64]
os: [openbsd]
- '@esbuild/sunos-x64@0.21.5':
- resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==}
- engines: {node: '>=12'}
+ '@esbuild/openharmony-arm64@0.25.12':
+ resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [openharmony]
+
+ '@esbuild/sunos-x64@0.25.12':
+ resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==}
+ engines: {node: '>=18'}
cpu: [x64]
os: [sunos]
- '@esbuild/win32-arm64@0.21.5':
- resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==}
- engines: {node: '>=12'}
+ '@esbuild/win32-arm64@0.25.12':
+ resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==}
+ engines: {node: '>=18'}
cpu: [arm64]
os: [win32]
- '@esbuild/win32-ia32@0.21.5':
- resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==}
- engines: {node: '>=12'}
+ '@esbuild/win32-ia32@0.25.12':
+ resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==}
+ engines: {node: '>=18'}
cpu: [ia32]
os: [win32]
- '@esbuild/win32-x64@0.21.5':
- resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==}
- engines: {node: '>=12'}
+ '@esbuild/win32-x64@0.25.12':
+ resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==}
+ engines: {node: '>=18'}
cpu: [x64]
os: [win32]
@@ -937,11 +954,18 @@ packages:
'@histoire/shared@0.17.17':
resolution: {integrity: sha512-ueGtURysonT0MujCObPCR57+mgZluMEXCrbc2FBgKAD/DoAt38tNwSGsmLldk2O6nTr7lr6ClbVSgWrLwgY6Xw==}
peerDependencies:
- vite: 5.4.21
+ vite: 6.4.2
'@histoire/vendors@0.17.17':
resolution: {integrity: sha512-QZvmffdoJlLuYftPIkOU5Q2FPAdG2JjMuQ5jF7NmEl0n1XnmbMqtRkdYTZ4eF6CO1KLZ0Zyf6gBQvoT1uWNcjA==}
+ '@hotwired/turbo-rails@8.0.13':
+ resolution: {integrity: sha512-6SCnnOSzhtaJ0pNkAjncZxjtKsK3sP/vPEkCnTXBXSHkr+vF7DTZkOlwjhms1DbbQNTsjCsBoKvzSMbh/omSCQ==}
+
+ '@hotwired/turbo@8.0.13':
+ resolution: {integrity: sha512-M7qXUqcGab6G5PKOiwhgbByTtrPgKPFCTMNQ52QhzUEXEqmp0/ApEguUesh/FPiUjrmFec+3lq98KsWnYY2C7g==}
+ engines: {node: '>= 14'}
+
'@humanwhocodes/config-array@0.11.14':
resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==}
engines: {node: '>=10.10.0'}
@@ -1118,6 +1142,9 @@ packages:
'@rails/actioncable@6.1.3':
resolution: {integrity: sha512-m02524MR9cTnUNfGz39Lkx9jVvuL0tle4O7YgvouJ7H83FILxzG1nQ5jw8pAjLAr9XQGu+P1sY4SKE3zyhCNjw==}
+ '@rails/actioncable@7.2.201':
+ resolution: {integrity: sha512-wsTdWoZ5EfG5k3t7ORdyQF0ZmDEgN4aVPCanHAiNEwCROqibSZMXXmCbH7IDJUVri4FOeAVwwbPINI7HVHPKBw==}
+
'@rails/ujs@7.1.400':
resolution: {integrity: sha512-YwvXm3BR5tn+VCAKYGycLejMRVZE3Ionj5gFjEeGXCZnI0Rpi+7dKpmyu90kdUY7dRUFpHTdu9zZceEzFLl38w==}
@@ -1397,11 +1424,11 @@ packages:
'@videojs/xhr@2.6.0':
resolution: {integrity: sha512-7J361GiN1tXpm+gd0xz2QWr3xNWBE+rytvo8J3KuggFaLg+U37gZQ2BuPLcnkfGffy2e+ozY70RHC8jt7zjA6Q==}
- '@vitejs/plugin-vue@5.1.4':
- resolution: {integrity: sha512-N2XSI2n3sQqp5w7Y/AN/L2XDjBIRGqXko+eDp42sydYSBeJuSm5a1sLf8zakmo8u7tA8NmBgoDLA1HeOESjp9A==}
+ '@vitejs/plugin-vue@5.2.4':
+ resolution: {integrity: sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==}
engines: {node: ^18.0.0 || >=20.0.0}
peerDependencies:
- vite: 5.4.21
+ vite: 6.4.2
vue: ^3.2.25
'@vitest/coverage-v8@3.0.5':
@@ -1420,7 +1447,7 @@ packages:
resolution: {integrity: sha512-CLPNBFBIE7x6aEGbIjaQAX03ZZlBMaWwAjBdMkIf/cAn6xzLTiM3zYqO/WAbieEjsAZir6tO71mzeHZoodThvw==}
peerDependencies:
msw: ^2.4.9
- vite: 5.4.21
+ vite: 6.4.2
peerDependenciesMeta:
msw:
optional: true
@@ -2145,6 +2172,10 @@ packages:
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
engines: {node: '>=0.4.0'}
+ detect-libc@2.1.2:
+ resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
+ engines: {node: '>=8'}
+
diacritics@1.3.0:
resolution: {integrity: sha512-wlwEkqcsaxvPJML+rDh/2iS824jbREk6DUMUKkEaSlxdYHeS43cClJtsWglvw2RfeXGm6ohKDqsXteJ5sP5enA==}
@@ -2269,6 +2300,9 @@ packages:
resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
engines: {node: '>= 0.4'}
+ es-module-lexer@1.7.0:
+ resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==}
+
es-object-atoms@1.0.0:
resolution: {integrity: sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==}
engines: {node: '>= 0.4'}
@@ -2288,9 +2322,9 @@ packages:
resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==}
engines: {node: '>= 0.4'}
- esbuild@0.21.5:
- resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==}
- engines: {node: '>=12'}
+ esbuild@0.25.12:
+ resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==}
+ engines: {node: '>=18'}
hasBin: true
escalade@3.1.2:
@@ -2497,6 +2531,15 @@ packages:
fastq@1.15.0:
resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==}
+ fdir@6.5.0:
+ resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
+ engines: {node: '>=12.0.0'}
+ peerDependencies:
+ picomatch: ^3 || ^4
+ peerDependenciesMeta:
+ picomatch:
+ optional: true
+
file-entry-cache@6.0.1:
resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==}
engines: {node: ^10.12.0 || >=12.0.0}
@@ -2742,7 +2785,7 @@ packages:
resolution: {integrity: sha512-DiRMSIgj340z+zikqf0f3Pj0CTv2/xtdBMBIAO1EARat+QXxMwumbfK41Gi7f9IIBr+UVmomNcwFxVY2EM/vrw==}
hasBin: true
peerDependencies:
- vite: 5.4.21
+ vite: 6.4.2
hookable@5.5.3:
resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==}
@@ -3105,6 +3148,76 @@ packages:
libphonenumber-js@1.11.9:
resolution: {integrity: sha512-Zs5wf5HaWzW2/inlupe2tstl0I/Tbqo7lH20ZLr6Is58u7Dz2n+gRFGNlj9/gWxFvNfp9+YyDsiegjNhdixB9A==}
+ lightningcss-android-arm64@1.32.0:
+ resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [android]
+
+ lightningcss-darwin-arm64@1.32.0:
+ resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [darwin]
+
+ lightningcss-darwin-x64@1.32.0:
+ resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [darwin]
+
+ lightningcss-freebsd-x64@1.32.0:
+ resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [freebsd]
+
+ lightningcss-linux-arm-gnueabihf@1.32.0:
+ resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm]
+ os: [linux]
+
+ lightningcss-linux-arm64-gnu@1.32.0:
+ resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [linux]
+
+ lightningcss-linux-arm64-musl@1.32.0:
+ resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [linux]
+
+ lightningcss-linux-x64-gnu@1.32.0:
+ resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [linux]
+
+ lightningcss-linux-x64-musl@1.32.0:
+ resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [linux]
+
+ lightningcss-win32-arm64-msvc@1.32.0:
+ resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [win32]
+
+ lightningcss-win32-x64-msvc@1.32.0:
+ resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [win32]
+
+ lightningcss@1.32.0:
+ resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==}
+ engines: {node: '>= 12.0.0'}
+
lilconfig@2.1.0:
resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==}
engines: {node: '>=10'}
@@ -3426,6 +3539,9 @@ packages:
resolution: {integrity: sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==}
engines: {node: '>= 0.4'}
+ obug@2.1.1:
+ resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==}
+
on-finished@2.3.0:
resolution: {integrity: sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==}
engines: {node: '>= 0.8'}
@@ -3816,6 +3932,10 @@ packages:
resolution: {integrity: sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==}
engines: {node: ^10 || ^12 || >=14}
+ postcss@8.5.14:
+ resolution: {integrity: sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==}
+ engines: {node: ^10 || ^12 || >=14}
+
postcss@8.5.6:
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
engines: {node: ^10 || ^12 || >=14}
@@ -4306,6 +4426,10 @@ packages:
resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==}
engines: {node: '>=18'}
+ tinyglobby@0.2.16:
+ resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==}
+ engines: {node: '>=12.0.0'}
+
tinykeys@3.0.0:
resolution: {integrity: sha512-nazawuGv5zx6MuDfDY0rmfXjuOGhD5XU2z0GLURQ1nzl0RUe9OuCJq+0u8xxJZINHe+mr7nw8PWYYZ9WhMFujw==}
@@ -4368,9 +4492,6 @@ packages:
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
- turbolinks@5.2.0:
- resolution: {integrity: sha512-pMiez3tyBo6uRHFNNZoYMmrES/IaGgMhQQM+VFF36keryjb5ms0XkVpmKHkfW/4Vy96qiGW3K9bz0tF5sK9bBw==}
-
type-check@0.4.0:
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
engines: {node: '>= 0.8.0'}
@@ -4525,32 +4646,42 @@ packages:
vue:
optional: true
- vite-node@2.0.1:
- resolution: {integrity: sha512-nVd6kyhPAql0s+xIVJzuF+RSRH8ZimNrm6U8ZvTA4MXv8CHI17TFaQwRaFiK75YX6XeFqZD4IoAaAfi9OR1XvQ==}
- engines: {node: ^18.0.0 || >=20.0.0}
+ vite-node@0.34.7:
+ resolution: {integrity: sha512-0Yzb96QzHmqIKIs/x2q/sqG750V/EF6yDkS2p1WjJc1W2bgRSuQjf5vB9HY8h2nVb5j4pO5paS5Npcv3s69YUg==}
+ engines: {node: '>=v14.18.0'}
hasBin: true
- vite-plugin-ruby@5.0.0:
- resolution: {integrity: sha512-c8PjTp21Ah/ttgnNUyu0qvCXZI08Jr9I24oUKg3TRIRhF5GcOZ++6wtlTCrNFd9COEQbpXHxlRIXd/MEg0iZJw==}
- peerDependencies:
- vite: 5.4.21
+ vite-node@3.0.5:
+ resolution: {integrity: sha512-02JEJl7SbtwSDJdYS537nU6l+ktdvcREfLksk/NDAqtdKWGqHl+joXzEubHROmS3E6pip+Xgu2tFezMu75jH7A==}
+ engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
+ hasBin: true
- vite@5.4.21:
- resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==}
- engines: {node: ^18.0.0 || >=20.0.0}
+ vite-plugin-ruby@5.2.1:
+ resolution: {integrity: sha512-wI3F/Yr4e4mEwiMff/cvNwGu8nZok5wrwUjHxO8we+h3y9+qCluO3Y5dzvz6vHJDBya9fKXkltoMwoJhaB2SRg==}
+ peerDependencies:
+ vite: 6.4.2
+
+ vite@6.4.2:
+ resolution: {integrity: sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==}
+ engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
hasBin: true
peerDependencies:
- '@types/node': ^18.0.0 || >=20.0.0
+ '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0
+ jiti: '>=1.21.0'
less: '*'
lightningcss: ^1.21.0
sass: '*'
sass-embedded: '*'
stylus: '*'
sugarss: '*'
- terser: ^5.4.0
+ terser: ^5.16.0
+ tsx: ^4.8.1
+ yaml: ^2.4.2
peerDependenciesMeta:
'@types/node':
optional: true
+ jiti:
+ optional: true
less:
optional: true
lightningcss:
@@ -4565,6 +4696,10 @@ packages:
optional: true
terser:
optional: true
+ tsx:
+ optional: true
+ yaml:
+ optional: true
vitest@3.0.5:
resolution: {integrity: sha512-4dof+HvqONw9bvsYxtkfUp2uHsTN9bV2CZIi1pWgoFpL1Lld8LA1ka9q/ONSsoScAKG7NVGf2stJTI7XRkXb2Q==}
@@ -5011,7 +5146,7 @@ snapshots:
prosemirror-utils: 1.2.2(prosemirror-model@1.22.3)(prosemirror-state@1.4.3)
prosemirror-view: 1.34.1
- '@chatwoot/utils@0.0.52':
+ '@chatwoot/utils@0.0.55':
dependencies:
date-fns: 2.30.0
@@ -5261,73 +5396,82 @@ snapshots:
'@iconify/utils': 3.1.0
tailwindcss: 3.4.19
- '@esbuild/aix-ppc64@0.21.5':
+ '@esbuild/aix-ppc64@0.25.12':
optional: true
- '@esbuild/android-arm64@0.21.5':
+ '@esbuild/android-arm64@0.25.12':
optional: true
- '@esbuild/android-arm@0.21.5':
+ '@esbuild/android-arm@0.25.12':
optional: true
- '@esbuild/android-x64@0.21.5':
+ '@esbuild/android-x64@0.25.12':
optional: true
- '@esbuild/darwin-arm64@0.21.5':
+ '@esbuild/darwin-arm64@0.25.12':
optional: true
- '@esbuild/darwin-x64@0.21.5':
+ '@esbuild/darwin-x64@0.25.12':
optional: true
- '@esbuild/freebsd-arm64@0.21.5':
+ '@esbuild/freebsd-arm64@0.25.12':
optional: true
- '@esbuild/freebsd-x64@0.21.5':
+ '@esbuild/freebsd-x64@0.25.12':
optional: true
- '@esbuild/linux-arm64@0.21.5':
+ '@esbuild/linux-arm64@0.25.12':
optional: true
- '@esbuild/linux-arm@0.21.5':
+ '@esbuild/linux-arm@0.25.12':
optional: true
- '@esbuild/linux-ia32@0.21.5':
+ '@esbuild/linux-ia32@0.25.12':
optional: true
- '@esbuild/linux-loong64@0.21.5':
+ '@esbuild/linux-loong64@0.25.12':
optional: true
- '@esbuild/linux-mips64el@0.21.5':
+ '@esbuild/linux-mips64el@0.25.12':
optional: true
- '@esbuild/linux-ppc64@0.21.5':
+ '@esbuild/linux-ppc64@0.25.12':
optional: true
- '@esbuild/linux-riscv64@0.21.5':
+ '@esbuild/linux-riscv64@0.25.12':
optional: true
- '@esbuild/linux-s390x@0.21.5':
+ '@esbuild/linux-s390x@0.25.12':
optional: true
- '@esbuild/linux-x64@0.21.5':
+ '@esbuild/linux-x64@0.25.12':
optional: true
- '@esbuild/netbsd-x64@0.21.5':
+ '@esbuild/netbsd-arm64@0.25.12':
optional: true
- '@esbuild/openbsd-x64@0.21.5':
+ '@esbuild/netbsd-x64@0.25.12':
optional: true
- '@esbuild/sunos-x64@0.21.5':
+ '@esbuild/openbsd-arm64@0.25.12':
optional: true
- '@esbuild/win32-arm64@0.21.5':
+ '@esbuild/openbsd-x64@0.25.12':
optional: true
- '@esbuild/win32-ia32@0.21.5':
+ '@esbuild/openharmony-arm64@0.25.12':
optional: true
- '@esbuild/win32-x64@0.21.5':
+ '@esbuild/sunos-x64@0.25.12':
+ optional: true
+
+ '@esbuild/win32-arm64@0.25.12':
+ optional: true
+
+ '@esbuild/win32-ia32@0.25.12':
+ optional: true
+
+ '@esbuild/win32-x64@0.25.12':
optional: true
'@eslint-community/eslint-utils@4.4.0(eslint@8.57.0)':
@@ -5442,10 +5586,10 @@ snapshots:
highlight.js: 11.10.0
vue: 3.5.12(typescript@5.6.2)
- '@histoire/app@0.17.17(vite@5.4.21(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0))':
+ '@histoire/app@0.17.17(vite@6.4.2(@types/node@22.7.0)(jiti@1.21.6)(lightningcss@1.32.0)(sass@1.79.3)(terser@5.33.0)(yaml@2.8.2))':
dependencies:
- '@histoire/controls': 0.17.17(vite@5.4.21(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0))
- '@histoire/shared': 0.17.17(vite@5.4.21(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0))
+ '@histoire/controls': 0.17.17(vite@6.4.2(@types/node@22.7.0)(jiti@1.21.6)(lightningcss@1.32.0)(sass@1.79.3)(terser@5.33.0)(yaml@2.8.2))
+ '@histoire/shared': 0.17.17(vite@6.4.2(@types/node@22.7.0)(jiti@1.21.6)(lightningcss@1.32.0)(sass@1.79.3)(terser@5.33.0)(yaml@2.8.2))
'@histoire/vendors': 0.17.17
'@types/flexsearch': 0.7.6
flexsearch: 0.7.21
@@ -5453,7 +5597,7 @@ snapshots:
transitivePeerDependencies:
- vite
- '@histoire/controls@0.17.17(vite@5.4.21(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0))':
+ '@histoire/controls@0.17.17(vite@6.4.2(@types/node@22.7.0)(jiti@1.21.6)(lightningcss@1.32.0)(sass@1.79.3)(terser@5.33.0)(yaml@2.8.2))':
dependencies:
'@codemirror/commands': 6.7.0
'@codemirror/lang-json': 6.0.1
@@ -5462,26 +5606,26 @@ snapshots:
'@codemirror/state': 6.4.1
'@codemirror/theme-one-dark': 6.1.2
'@codemirror/view': 6.34.1
- '@histoire/shared': 0.17.17(vite@5.4.21(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0))
+ '@histoire/shared': 0.17.17(vite@6.4.2(@types/node@22.7.0)(jiti@1.21.6)(lightningcss@1.32.0)(sass@1.79.3)(terser@5.33.0)(yaml@2.8.2))
'@histoire/vendors': 0.17.17
transitivePeerDependencies:
- vite
- '@histoire/plugin-vue@0.17.15(histoire@0.17.15(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0)(vite@5.4.21(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0)))(vite@5.4.21(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0))(vue@3.5.12(typescript@5.6.2))':
+ '@histoire/plugin-vue@0.17.15(histoire@0.17.15(@types/node@22.7.0)(lightningcss@1.32.0)(sass@1.79.3)(terser@5.33.0)(vite@6.4.2(@types/node@22.7.0)(jiti@1.21.6)(lightningcss@1.32.0)(sass@1.79.3)(terser@5.33.0)(yaml@2.8.2))(yaml@2.8.2))(vite@6.4.2(@types/node@22.7.0)(jiti@1.21.6)(lightningcss@1.32.0)(sass@1.79.3)(terser@5.33.0)(yaml@2.8.2))(vue@3.5.12(typescript@5.6.2))':
dependencies:
- '@histoire/controls': 0.17.17(vite@5.4.21(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0))
- '@histoire/shared': 0.17.17(vite@5.4.21(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0))
+ '@histoire/controls': 0.17.17(vite@6.4.2(@types/node@22.7.0)(jiti@1.21.6)(lightningcss@1.32.0)(sass@1.79.3)(terser@5.33.0)(yaml@2.8.2))
+ '@histoire/shared': 0.17.17(vite@6.4.2(@types/node@22.7.0)(jiti@1.21.6)(lightningcss@1.32.0)(sass@1.79.3)(terser@5.33.0)(yaml@2.8.2))
'@histoire/vendors': 0.17.17
change-case: 4.1.2
globby: 13.2.2
- histoire: 0.17.15(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0)(vite@5.4.21(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0))
+ histoire: 0.17.15(@types/node@22.7.0)(lightningcss@1.32.0)(sass@1.79.3)(terser@5.33.0)(vite@6.4.2(@types/node@22.7.0)(jiti@1.21.6)(lightningcss@1.32.0)(sass@1.79.3)(terser@5.33.0)(yaml@2.8.2))(yaml@2.8.2)
launch-editor: 2.9.1
pathe: 1.1.2
vue: 3.5.12(typescript@5.6.2)
transitivePeerDependencies:
- vite
- '@histoire/shared@0.17.17(vite@5.4.21(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0))':
+ '@histoire/shared@0.17.17(vite@6.4.2(@types/node@22.7.0)(jiti@1.21.6)(lightningcss@1.32.0)(sass@1.79.3)(terser@5.33.0)(yaml@2.8.2))':
dependencies:
'@histoire/vendors': 0.17.17
'@types/fs-extra': 9.0.13
@@ -5489,10 +5633,17 @@ snapshots:
chokidar: 3.6.0
pathe: 1.1.2
picocolors: 1.1.0
- vite: 5.4.21(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0)
+ vite: 6.4.2(@types/node@22.7.0)(jiti@1.21.6)(lightningcss@1.32.0)(sass@1.79.3)(terser@5.33.0)(yaml@2.8.2)
'@histoire/vendors@0.17.17': {}
+ '@hotwired/turbo-rails@8.0.13':
+ dependencies:
+ '@hotwired/turbo': 8.0.13
+ '@rails/actioncable': 7.2.201
+
+ '@hotwired/turbo@8.0.13': {}
+
'@humanwhocodes/config-array@0.11.14':
dependencies:
'@humanwhocodes/object-schema': 2.0.3
@@ -5704,6 +5855,8 @@ snapshots:
'@rails/actioncable@6.1.3': {}
+ '@rails/actioncable@7.2.201': {}
+
'@rails/ujs@7.1.400': {}
'@rollup/plugin-yaml@4.1.2(rollup@4.59.0)':
@@ -5931,12 +6084,12 @@ snapshots:
global: 4.4.0
is-function: 1.0.2
- '@vitejs/plugin-vue@5.1.4(vite@5.4.21(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0))(vue@3.5.12(typescript@5.6.2))':
+ '@vitejs/plugin-vue@5.2.4(vite@6.4.2(@types/node@22.7.0)(jiti@1.21.6)(lightningcss@1.32.0)(sass@1.79.3)(terser@5.33.0)(yaml@2.8.2))(vue@3.5.12(typescript@5.6.2))':
dependencies:
- vite: 5.4.21(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0)
+ vite: 6.4.2(@types/node@22.7.0)(jiti@1.21.6)(lightningcss@1.32.0)(sass@1.79.3)(terser@5.33.0)(yaml@2.8.2)
vue: 3.5.12(typescript@5.6.2)
- '@vitest/coverage-v8@3.0.5(vitest@3.0.5(@types/node@22.7.0)(jsdom@27.2.0)(sass@1.79.3)(terser@5.33.0))':
+ '@vitest/coverage-v8@3.0.5(vitest@3.0.5(@types/node@22.7.0)(jiti@1.21.6)(jsdom@27.2.0)(lightningcss@1.32.0)(sass@1.79.3)(terser@5.33.0)(yaml@2.8.2))':
dependencies:
'@ampproject/remapping': 2.3.0
'@bcoe/v8-coverage': 1.0.2
@@ -5950,7 +6103,7 @@ snapshots:
std-env: 3.8.0
test-exclude: 7.0.1
tinyrainbow: 2.0.0
- vitest: 3.0.5(@types/node@22.7.0)(jsdom@27.2.0)(sass@1.79.3)(terser@5.33.0)
+ vitest: 3.0.5(@types/node@22.7.0)(jiti@1.21.6)(jsdom@27.2.0)(lightningcss@1.32.0)(sass@1.79.3)(terser@5.33.0)(yaml@2.8.2)
transitivePeerDependencies:
- supports-color
@@ -5961,13 +6114,13 @@ snapshots:
chai: 5.1.2
tinyrainbow: 2.0.0
- '@vitest/mocker@3.0.5(vite@5.4.21(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0))':
+ '@vitest/mocker@3.0.5(vite@6.4.2(@types/node@22.7.0)(jiti@1.21.6)(lightningcss@1.32.0)(sass@1.79.3)(terser@5.33.0)(yaml@2.8.2))':
dependencies:
'@vitest/spy': 3.0.5
estree-walker: 3.0.3
magic-string: 0.30.17
optionalDependencies:
- vite: 5.4.21(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0)
+ vite: 6.4.2(@types/node@22.7.0)(jiti@1.21.6)(lightningcss@1.32.0)(sass@1.79.3)(terser@5.33.0)(yaml@2.8.2)
'@vitest/pretty-format@3.0.5':
dependencies:
@@ -6054,7 +6207,7 @@ snapshots:
'@vue/shared': 3.5.13
estree-walker: 2.0.2
magic-string: 0.30.17
- postcss: 8.5.6
+ postcss: 8.5.14
source-map-js: 1.2.1
'@vue/compiler-sfc@3.5.8':
@@ -6240,7 +6393,7 @@ snapshots:
agent-base@6.0.2:
dependencies:
- debug: 4.4.0
+ debug: 4.4.3
transitivePeerDependencies:
- supports-color
@@ -6805,6 +6958,9 @@ snapshots:
delayed-stream@1.0.0: {}
+ detect-libc@2.1.2:
+ optional: true
+
diacritics@1.3.0: {}
didyoumean@1.2.2: {}
@@ -6997,6 +7153,8 @@ snapshots:
es-errors@1.3.0: {}
+ es-module-lexer@1.7.0: {}
+
es-object-atoms@1.0.0:
dependencies:
es-errors: 1.3.0
@@ -7022,31 +7180,34 @@ snapshots:
is-date-object: 1.0.5
is-symbol: 1.0.4
- esbuild@0.21.5:
+ esbuild@0.25.12:
optionalDependencies:
- '@esbuild/aix-ppc64': 0.21.5
- '@esbuild/android-arm': 0.21.5
- '@esbuild/android-arm64': 0.21.5
- '@esbuild/android-x64': 0.21.5
- '@esbuild/darwin-arm64': 0.21.5
- '@esbuild/darwin-x64': 0.21.5
- '@esbuild/freebsd-arm64': 0.21.5
- '@esbuild/freebsd-x64': 0.21.5
- '@esbuild/linux-arm': 0.21.5
- '@esbuild/linux-arm64': 0.21.5
- '@esbuild/linux-ia32': 0.21.5
- '@esbuild/linux-loong64': 0.21.5
- '@esbuild/linux-mips64el': 0.21.5
- '@esbuild/linux-ppc64': 0.21.5
- '@esbuild/linux-riscv64': 0.21.5
- '@esbuild/linux-s390x': 0.21.5
- '@esbuild/linux-x64': 0.21.5
- '@esbuild/netbsd-x64': 0.21.5
- '@esbuild/openbsd-x64': 0.21.5
- '@esbuild/sunos-x64': 0.21.5
- '@esbuild/win32-arm64': 0.21.5
- '@esbuild/win32-ia32': 0.21.5
- '@esbuild/win32-x64': 0.21.5
+ '@esbuild/aix-ppc64': 0.25.12
+ '@esbuild/android-arm': 0.25.12
+ '@esbuild/android-arm64': 0.25.12
+ '@esbuild/android-x64': 0.25.12
+ '@esbuild/darwin-arm64': 0.25.12
+ '@esbuild/darwin-x64': 0.25.12
+ '@esbuild/freebsd-arm64': 0.25.12
+ '@esbuild/freebsd-x64': 0.25.12
+ '@esbuild/linux-arm': 0.25.12
+ '@esbuild/linux-arm64': 0.25.12
+ '@esbuild/linux-ia32': 0.25.12
+ '@esbuild/linux-loong64': 0.25.12
+ '@esbuild/linux-mips64el': 0.25.12
+ '@esbuild/linux-ppc64': 0.25.12
+ '@esbuild/linux-riscv64': 0.25.12
+ '@esbuild/linux-s390x': 0.25.12
+ '@esbuild/linux-x64': 0.25.12
+ '@esbuild/netbsd-arm64': 0.25.12
+ '@esbuild/netbsd-x64': 0.25.12
+ '@esbuild/openbsd-arm64': 0.25.12
+ '@esbuild/openbsd-x64': 0.25.12
+ '@esbuild/openharmony-arm64': 0.25.12
+ '@esbuild/sunos-x64': 0.25.12
+ '@esbuild/win32-arm64': 0.25.12
+ '@esbuild/win32-ia32': 0.25.12
+ '@esbuild/win32-x64': 0.25.12
escalade@3.1.2: {}
@@ -7289,6 +7450,10 @@ snapshots:
dependencies:
reusify: 1.0.4
+ fdir@6.5.0(picomatch@4.0.4):
+ optionalDependencies:
+ picomatch: 4.0.4
+
file-entry-cache@6.0.1:
dependencies:
flat-cache: 3.1.0
@@ -7554,12 +7719,12 @@ snapshots:
highlight.js@11.10.0: {}
- histoire@0.17.15(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0)(vite@5.4.21(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0)):
+ histoire@0.17.15(@types/node@22.7.0)(lightningcss@1.32.0)(sass@1.79.3)(terser@5.33.0)(vite@6.4.2(@types/node@22.7.0)(jiti@1.21.6)(lightningcss@1.32.0)(sass@1.79.3)(terser@5.33.0)(yaml@2.8.2))(yaml@2.8.2):
dependencies:
'@akryum/tinypool': 0.3.1
- '@histoire/app': 0.17.17(vite@5.4.21(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0))
- '@histoire/controls': 0.17.17(vite@5.4.21(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0))
- '@histoire/shared': 0.17.17(vite@5.4.21(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0))
+ '@histoire/app': 0.17.17(vite@6.4.2(@types/node@22.7.0)(jiti@1.21.6)(lightningcss@1.32.0)(sass@1.79.3)(terser@5.33.0)(yaml@2.8.2))
+ '@histoire/controls': 0.17.17(vite@6.4.2(@types/node@22.7.0)(jiti@1.21.6)(lightningcss@1.32.0)(sass@1.79.3)(terser@5.33.0)(yaml@2.8.2))
+ '@histoire/shared': 0.17.17(vite@6.4.2(@types/node@22.7.0)(jiti@1.21.6)(lightningcss@1.32.0)(sass@1.79.3)(terser@5.33.0)(yaml@2.8.2))
'@histoire/vendors': 0.17.17
'@types/flexsearch': 0.7.6
'@types/markdown-it': 12.2.3
@@ -7586,8 +7751,8 @@ snapshots:
sade: 1.8.1
shiki-es: 0.2.0
sirv: 2.0.4
- vite: 5.4.21(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0)
- vite-node: 2.0.1(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0)
+ vite: 6.4.2(@types/node@22.7.0)(jiti@1.21.6)(lightningcss@1.32.0)(sass@1.79.3)(terser@5.33.0)(yaml@2.8.2)
+ vite-node: 0.34.7(@types/node@22.7.0)(jiti@1.21.6)(lightningcss@1.32.0)(sass@1.79.3)(terser@5.33.0)(yaml@2.8.2)
transitivePeerDependencies:
- '@types/node'
- bufferutil
@@ -7600,7 +7765,9 @@ snapshots:
- sugarss
- supports-color
- terser
+ - tsx
- utf-8-validate
+ - yaml
hookable@5.5.3: {}
@@ -7993,6 +8160,56 @@ snapshots:
libphonenumber-js@1.11.9: {}
+ lightningcss-android-arm64@1.32.0:
+ optional: true
+
+ lightningcss-darwin-arm64@1.32.0:
+ optional: true
+
+ lightningcss-darwin-x64@1.32.0:
+ optional: true
+
+ lightningcss-freebsd-x64@1.32.0:
+ optional: true
+
+ lightningcss-linux-arm-gnueabihf@1.32.0:
+ optional: true
+
+ lightningcss-linux-arm64-gnu@1.32.0:
+ optional: true
+
+ lightningcss-linux-arm64-musl@1.32.0:
+ optional: true
+
+ lightningcss-linux-x64-gnu@1.32.0:
+ optional: true
+
+ lightningcss-linux-x64-musl@1.32.0:
+ optional: true
+
+ lightningcss-win32-arm64-msvc@1.32.0:
+ optional: true
+
+ lightningcss-win32-x64-msvc@1.32.0:
+ optional: true
+
+ lightningcss@1.32.0:
+ dependencies:
+ detect-libc: 2.1.2
+ optionalDependencies:
+ lightningcss-android-arm64: 1.32.0
+ lightningcss-darwin-arm64: 1.32.0
+ lightningcss-darwin-x64: 1.32.0
+ lightningcss-freebsd-x64: 1.32.0
+ lightningcss-linux-arm-gnueabihf: 1.32.0
+ lightningcss-linux-arm64-gnu: 1.32.0
+ lightningcss-linux-arm64-musl: 1.32.0
+ lightningcss-linux-x64-gnu: 1.32.0
+ lightningcss-linux-x64-musl: 1.32.0
+ lightningcss-win32-arm64-msvc: 1.32.0
+ lightningcss-win32-x64-msvc: 1.32.0
+ optional: true
+
lilconfig@2.1.0: {}
lilconfig@3.1.3: {}
@@ -8317,6 +8534,8 @@ snapshots:
define-properties: 1.2.1
es-object-atoms: 1.0.0
+ obug@2.1.1: {}
+
on-finished@2.3.0:
dependencies:
ee-first: 1.1.1
@@ -8727,6 +8946,12 @@ snapshots:
picocolors: 1.1.0
source-map-js: 1.2.1
+ postcss@8.5.14:
+ dependencies:
+ nanoid: 3.3.11
+ picocolors: 1.1.1
+ source-map-js: 1.2.1
+
postcss@8.5.6:
dependencies:
nanoid: 3.3.11
@@ -9319,6 +9544,11 @@ snapshots:
tinyexec@1.0.2: {}
+ tinyglobby@0.2.16:
+ dependencies:
+ fdir: 6.5.0(picomatch@4.0.4)
+ picomatch: 4.0.4
+
tinykeys@3.0.0: {}
tinypool@1.0.2: {}
@@ -9373,8 +9603,6 @@ snapshots:
tslib@2.8.1: {}
- turbolinks@5.2.0: {}
-
type-check@0.4.0:
dependencies:
prelude-ls: 1.2.1
@@ -9547,15 +9775,17 @@ snapshots:
optionalDependencies:
vue: 3.5.12(typescript@5.6.2)
- vite-node@2.0.1(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0):
+ vite-node@0.34.7(@types/node@22.7.0)(jiti@1.21.6)(lightningcss@1.32.0)(sass@1.79.3)(terser@5.33.0)(yaml@2.8.2):
dependencies:
cac: 6.7.14
- debug: 4.4.0
+ debug: 4.4.3
+ mlly: 1.8.1
pathe: 1.1.2
picocolors: 1.1.1
- vite: 5.4.21(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0)
+ vite: 6.4.2(@types/node@22.7.0)(jiti@1.21.6)(lightningcss@1.32.0)(sass@1.79.3)(terser@5.33.0)(yaml@2.8.2)
transitivePeerDependencies:
- '@types/node'
+ - jiti
- less
- lightningcss
- sass
@@ -9564,30 +9794,57 @@ snapshots:
- sugarss
- supports-color
- terser
+ - tsx
+ - yaml
- vite-plugin-ruby@5.0.0(vite@5.4.21(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0)):
+ vite-node@3.0.5(@types/node@22.7.0)(jiti@1.21.6)(lightningcss@1.32.0)(sass@1.79.3)(terser@5.33.0)(yaml@2.8.2):
dependencies:
- debug: 4.3.5
- fast-glob: 3.3.2
- vite: 5.4.21(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0)
+ cac: 6.7.14
+ debug: 4.4.3
+ es-module-lexer: 1.7.0
+ pathe: 2.0.3
+ vite: 6.4.2(@types/node@22.7.0)(jiti@1.21.6)(lightningcss@1.32.0)(sass@1.79.3)(terser@5.33.0)(yaml@2.8.2)
transitivePeerDependencies:
+ - '@types/node'
+ - jiti
+ - less
+ - lightningcss
+ - sass
+ - sass-embedded
+ - stylus
+ - sugarss
- supports-color
+ - terser
+ - tsx
+ - yaml
- vite@5.4.21(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0):
+ vite-plugin-ruby@5.2.1(vite@6.4.2(@types/node@22.7.0)(jiti@1.21.6)(lightningcss@1.32.0)(sass@1.79.3)(terser@5.33.0)(yaml@2.8.2)):
dependencies:
- esbuild: 0.21.5
- postcss: 8.5.6
+ obug: 2.1.1
+ tinyglobby: 0.2.16
+ vite: 6.4.2(@types/node@22.7.0)(jiti@1.21.6)(lightningcss@1.32.0)(sass@1.79.3)(terser@5.33.0)(yaml@2.8.2)
+
+ vite@6.4.2(@types/node@22.7.0)(jiti@1.21.6)(lightningcss@1.32.0)(sass@1.79.3)(terser@5.33.0)(yaml@2.8.2):
+ dependencies:
+ esbuild: 0.25.12
+ fdir: 6.5.0(picomatch@4.0.4)
+ picomatch: 4.0.4
+ postcss: 8.5.14
rollup: 4.59.0
+ tinyglobby: 0.2.16
optionalDependencies:
'@types/node': 22.7.0
fsevents: 2.3.3
+ jiti: 1.21.6
+ lightningcss: 1.32.0
sass: 1.79.3
terser: 5.33.0
+ yaml: 2.8.2
- vitest@3.0.5(@types/node@22.7.0)(jsdom@27.2.0)(sass@1.79.3)(terser@5.33.0):
+ vitest@3.0.5(@types/node@22.7.0)(jiti@1.21.6)(jsdom@27.2.0)(lightningcss@1.32.0)(sass@1.79.3)(terser@5.33.0)(yaml@2.8.2):
dependencies:
'@vitest/expect': 3.0.5
- '@vitest/mocker': 3.0.5(vite@5.4.21(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0))
+ '@vitest/mocker': 3.0.5(vite@6.4.2(@types/node@22.7.0)(jiti@1.21.6)(lightningcss@1.32.0)(sass@1.79.3)(terser@5.33.0)(yaml@2.8.2))
'@vitest/pretty-format': 3.0.5
'@vitest/runner': 3.0.5
'@vitest/snapshot': 3.0.5
@@ -9603,13 +9860,14 @@ snapshots:
tinyexec: 0.3.2
tinypool: 1.0.2
tinyrainbow: 2.0.0
- vite: 5.4.21(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0)
- vite-node: 2.0.1(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0)
+ vite: 6.4.2(@types/node@22.7.0)(jiti@1.21.6)(lightningcss@1.32.0)(sass@1.79.3)(terser@5.33.0)(yaml@2.8.2)
+ vite-node: 3.0.5(@types/node@22.7.0)(jiti@1.21.6)(lightningcss@1.32.0)(sass@1.79.3)(terser@5.33.0)(yaml@2.8.2)
why-is-node-running: 2.3.0
optionalDependencies:
'@types/node': 22.7.0
jsdom: 27.2.0
transitivePeerDependencies:
+ - jiti
- less
- lightningcss
- msw
@@ -9619,6 +9877,8 @@ snapshots:
- sugarss
- supports-color
- terser
+ - tsx
+ - yaml
vue-chartjs@5.3.1(chart.js@4.4.4)(vue@3.5.12(typescript@5.6.2)):
dependencies:
diff --git a/spec/builders/messages/facebook/message_builder_spec.rb b/spec/builders/messages/facebook/message_builder_spec.rb
index f3244bc21..afa9d5f34 100644
--- a/spec/builders/messages/facebook/message_builder_spec.rb
+++ b/spec/builders/messages/facebook/message_builder_spec.rb
@@ -140,6 +140,45 @@ describe Messages::Facebook::MessageBuilder do
end
end
+ [
+ {
+ source_id: 'm_fallback_test',
+ attachment: { type: 'fallback', title: 'Shared link', url: 'https://www.example.com/shared-link' },
+ title: 'Shared link',
+ url: 'https://www.example.com/shared-link'
+ },
+ {
+ source_id: 'm_share_test',
+ attachment: { type: 'share', title: 'Shared Facebook post', payload: { url: 'https://www.facebook.com/example/posts/123' } },
+ title: 'Shared Facebook post',
+ url: 'https://www.facebook.com/example/posts/123'
+ }
+ ].each do |message_data|
+ it "stores #{message_data[:attachment][:type]} attachments as fallback links" do
+ allow(Koala::Facebook::API).to receive(:new).and_return(fb_object)
+ allow(fb_object).to receive(:get_object).and_return(
+ { first_name: 'Jane', last_name: 'Dae', profile_pic: 'https://chatwoot-assets.local/sample.png' }.with_indifferent_access
+ )
+ expect(Down).not_to receive(:download)
+
+ message_object = {
+ messaging: {
+ sender: { id: '3383290475046708' },
+ recipient: { id: facebook_channel.page_id },
+ message: { mid: message_data[:source_id], attachments: [message_data[:attachment]] }
+ }
+ }.to_json
+ message = Integrations::Facebook::MessageParser.new(message_object)
+
+ described_class.new(message, facebook_channel.inbox).perform
+
+ attachment = facebook_channel.inbox.messages.find_by(source_id: message_data[:source_id]).attachments.first
+ expect(attachment.file_type).to eq('fallback')
+ expect(attachment.fallback_title).to eq(message_data[:title])
+ expect(attachment.external_url).to eq(message_data[:url])
+ end
+ end
+
context 'when lock to single conversation' do
subject(:mocked_message_builder) do
described_class.new(mocked_incoming_fb_text_message, facebook_channel.inbox).perform
diff --git a/spec/builders/messages/message_builder_spec.rb b/spec/builders/messages/message_builder_spec.rb
index e61198b60..671604ad4 100644
--- a/spec/builders/messages/message_builder_spec.rb
+++ b/spec/builders/messages/message_builder_spec.rb
@@ -149,6 +149,35 @@ describe Messages::MessageBuilder do
end
end
+ context 'when is_voice_message is true' do
+ let(:params) do
+ ActionController::Parameters.new({
+ content: 'test',
+ attachments: [Rack::Test::UploadedFile.new('spec/assets/sample.ogg', 'audio/ogg')],
+ is_voice_message: true
+ })
+ end
+
+ it 'sets is_voice_message in attachment meta' do
+ message = message_builder
+ expect(message.attachments.first.meta).to include('is_voice_message' => true)
+ end
+ end
+
+ context 'when is_voice_message is not provided' do
+ let(:params) do
+ ActionController::Parameters.new({
+ content: 'test',
+ attachments: [Rack::Test::UploadedFile.new('spec/assets/avatar.png', 'image/png')]
+ })
+ end
+
+ it 'does not set is_voice_message in attachment meta' do
+ message = message_builder
+ expect(message.attachments.first.meta).not_to include('is_voice_message')
+ end
+ end
+
context 'when email channel messages' do
let!(:channel_email) { create(:channel_email, account: account) }
let(:inbox_member) { create(:inbox_member, inbox: channel_email.inbox) }
diff --git a/spec/controllers/api/v1/accounts/bulk_actions_controller_spec.rb b/spec/controllers/api/v1/accounts/bulk_actions_controller_spec.rb
index 7ee763dbc..1d378ab65 100644
--- a/spec/controllers/api/v1/accounts/bulk_actions_controller_spec.rb
+++ b/spec/controllers/api/v1/accounts/bulk_actions_controller_spec.rb
@@ -263,6 +263,31 @@ RSpec.describe 'Api::V1::Accounts::BulkActionsController', type: :request do
expect(response).to have_http_status(:success)
end
+ it 'permits contact label removal params' do
+ contact_one = create(:contact, account: account)
+ contact_two = create(:contact, account: account)
+
+ expect do
+ post "/api/v1/accounts/#{account.id}/bulk_actions",
+ headers: agent.create_new_auth_token,
+ params: {
+ type: 'Contact',
+ ids: [contact_one.id, contact_two.id],
+ labels: { remove: %w[vip support] },
+ extra: 'ignored'
+ }
+ end.to have_enqueued_job(Contacts::BulkActionJob).with(
+ account.id,
+ agent.id,
+ hash_including(
+ 'ids' => [contact_one.id.to_s, contact_two.id.to_s],
+ 'labels' => hash_including('remove' => %w[vip support])
+ )
+ )
+
+ expect(response).to have_http_status(:success)
+ end
+
it 'returns unauthorized for delete action when user is not admin' do
contact = create(:contact, account: account)
diff --git a/spec/controllers/api/v1/accounts/onboardings_controller_spec.rb b/spec/controllers/api/v1/accounts/onboardings_controller_spec.rb
new file mode 100644
index 000000000..ec2b4dcaa
--- /dev/null
+++ b/spec/controllers/api/v1/accounts/onboardings_controller_spec.rb
@@ -0,0 +1,114 @@
+require 'rails_helper'
+
+RSpec.describe 'Onboarding API', type: :request do
+ let(:account) { create(:account, domain: 'example.com') }
+ let(:admin) { create(:user, account: account, role: :administrator) }
+
+ describe 'PATCH /api/v1/accounts/{account.id}/onboarding' do
+ context 'when unauthenticated' do
+ it 'returns unauthorized' do
+ patch "/api/v1/accounts/#{account.id}/onboarding", params: { website: 'acme.com' }, as: :json
+ expect(response).to have_http_status(:unauthorized)
+ end
+ end
+
+ context 'when authenticated as an agent (non-admin)' do
+ let(:agent) { create(:user, account: account, role: :agent) }
+
+ it 'returns unauthorized and does not change the account' do
+ patch "/api/v1/accounts/#{account.id}/onboarding",
+ params: { name: 'Hijacked', website: 'attacker.com' },
+ headers: agent.create_new_auth_token, as: :json
+
+ expect(response).to have_http_status(:unauthorized)
+ expect(account.reload.name).not_to eq('Hijacked')
+ end
+
+ it 'does not create a help center portal' do
+ account.update!(custom_attributes: { 'onboarding_step' => 'account_details' })
+
+ expect do
+ patch "/api/v1/accounts/#{account.id}/onboarding",
+ params: { website: 'attacker.com' },
+ headers: agent.create_new_auth_token, as: :json
+ end.not_to change(account.portals, :count)
+ end
+ end
+
+ context 'when finalizing account_details' do
+ before { account.update!(custom_attributes: { 'onboarding_step' => 'account_details' }) }
+
+ it 'saves name and locale' do
+ patch "/api/v1/accounts/#{account.id}/onboarding",
+ params: { name: 'Acme Inc', locale: 'fr' },
+ headers: admin.create_new_auth_token, as: :json
+
+ expect(response).to have_http_status(:success)
+ expect(account.reload.name).to eq('Acme Inc')
+ expect(account.locale).to eq('fr')
+ end
+
+ it 'merges custom_attributes' do
+ patch "/api/v1/accounts/#{account.id}/onboarding",
+ params: { website: 'acme.com', industry: 'tech', company_size: '10-50' },
+ headers: admin.create_new_auth_token, as: :json
+
+ attrs = account.reload.custom_attributes
+ expect(attrs['website']).to eq('acme.com')
+ expect(attrs['industry']).to eq('tech')
+ expect(attrs['company_size']).to eq('10-50')
+ end
+
+ it 'clears onboarding_step' do
+ patch "/api/v1/accounts/#{account.id}/onboarding",
+ params: { website: 'acme.com' },
+ headers: admin.create_new_auth_token, as: :json
+
+ expect(account.reload.custom_attributes).not_to have_key('onboarding_step')
+ end
+
+ it 'invokes HelpCenterCreationService when website is present', skip: 'help center generation wiring disabled until UI is ready' do
+ service = instance_double(Onboarding::HelpCenterCreationService, perform: nil)
+ allow(Onboarding::HelpCenterCreationService).to receive(:new).and_return(service)
+
+ patch "/api/v1/accounts/#{account.id}/onboarding",
+ params: { website: 'acme.com' },
+ headers: admin.create_new_auth_token, as: :json
+
+ expect(Onboarding::HelpCenterCreationService).to have_received(:new) do |arg_account, arg_user|
+ expect(arg_account.id).to eq(account.id)
+ expect(arg_user.id).to eq(admin.id)
+ end
+ expect(service).to have_received(:perform)
+ end
+
+ it 'does not create a help center portal when website is blank' do
+ expect do
+ patch "/api/v1/accounts/#{account.id}/onboarding",
+ params: { name: 'Acme Inc' },
+ headers: admin.create_new_auth_token, as: :json
+ end.not_to change(account.portals, :count)
+ end
+ end
+
+ context 'when onboarding_step is not account_details' do
+ before { account.update!(custom_attributes: { 'onboarding_step' => 'invite_team' }) }
+
+ it 'does not clear onboarding_step' do
+ patch "/api/v1/accounts/#{account.id}/onboarding",
+ params: { website: 'acme.com' },
+ headers: admin.create_new_auth_token, as: :json
+
+ expect(account.reload.custom_attributes['onboarding_step']).to eq('invite_team')
+ end
+
+ it 'does not create a help center portal' do
+ expect do
+ patch "/api/v1/accounts/#{account.id}/onboarding",
+ params: { website: 'acme.com' },
+ headers: admin.create_new_auth_token, as: :json
+ end.not_to change(account.portals, :count)
+ end
+ end
+ end
+end
diff --git a/spec/controllers/api/v1/accounts_controller_spec.rb b/spec/controllers/api/v1/accounts_controller_spec.rb
index 340803a9b..d93503418 100644
--- a/spec/controllers/api/v1/accounts_controller_spec.rb
+++ b/spec/controllers/api/v1/accounts_controller_spec.rb
@@ -302,16 +302,6 @@ RSpec.describe 'Accounts API', type: :request do
expect(account.reload.custom_attributes['onboarding_step']).to eq('invite_team')
end
- it 'clears onboarding step when current value is account_details' do
- account.update(custom_attributes: { onboarding_step: 'account_details' })
- patch "/api/v1/accounts/#{account.id}",
- params: params,
- headers: admin.create_new_auth_token,
- as: :json
-
- expect(account.reload.custom_attributes).not_to have_key('onboarding_step')
- end
-
it 'will not update onboarding step if onboarding step is not present in account custom attributes' do
patch "/api/v1/accounts/#{account.id}",
params: params,
diff --git a/spec/controllers/google/callbacks_controller_spec.rb b/spec/controllers/google/callbacks_controller_spec.rb
index a898ab395..b38fcfb59 100644
--- a/spec/controllers/google/callbacks_controller_spec.rb
+++ b/spec/controllers/google/callbacks_controller_spec.rb
@@ -8,12 +8,12 @@ RSpec.describe 'Google::CallbacksController', type: :request do
describe 'GET /google/callback' do
let(:response_body_success) do
- { id_token: JWT.encode({ email: email, name: 'test' }, false), access_token: SecureRandom.hex(10), token_type: 'Bearer',
+ { id_token: JWT.encode({ email: email, name: 'test' }, nil, 'none'), access_token: SecureRandom.hex(10), token_type: 'Bearer',
refresh_token: SecureRandom.hex(10) }
end
let(:response_body_success_without_name) do
- { id_token: JWT.encode({ email: email }, false), access_token: SecureRandom.hex(10), token_type: 'Bearer',
+ { id_token: JWT.encode({ email: email }, nil, 'none'), access_token: SecureRandom.hex(10), token_type: 'Bearer',
refresh_token: SecureRandom.hex(10) }
end
diff --git a/spec/controllers/microsoft/callbacks_controller_spec.rb b/spec/controllers/microsoft/callbacks_controller_spec.rb
index 6bd9a0583..be13d5c84 100644
--- a/spec/controllers/microsoft/callbacks_controller_spec.rb
+++ b/spec/controllers/microsoft/callbacks_controller_spec.rb
@@ -8,12 +8,12 @@ RSpec.describe 'Microsoft::CallbacksController', type: :request do
describe 'GET /microsoft/callback' do
let(:response_body_success) do
- { id_token: JWT.encode({ email: email, name: 'test' }, false), access_token: SecureRandom.hex(10), token_type: 'Bearer',
+ { id_token: JWT.encode({ email: email, name: 'test' }, nil, 'none'), access_token: SecureRandom.hex(10), token_type: 'Bearer',
refresh_token: SecureRandom.hex(10) }
end
let(:response_body_success_without_name) do
- { id_token: JWT.encode({ email: email }, false), access_token: SecureRandom.hex(10), token_type: 'Bearer',
+ { id_token: JWT.encode({ email: email }, nil, 'none'), access_token: SecureRandom.hex(10), token_type: 'Bearer',
refresh_token: SecureRandom.hex(10) }
end
@@ -34,6 +34,25 @@ RSpec.describe 'Microsoft::CallbacksController', type: :request do
expect(inbox.channel.imap_address).to eq 'outlook.office365.com'
end
+ it 'sets imap_login from preferred_username when the id_token carries a UPN that differs from email' do
+ upn = 'testaccount@primary-domain.example'
+ mailbox = 'TestAccount@mailbox-domain.example'
+ response_body = {
+ id_token: JWT.encode({ email: mailbox, preferred_username: upn, name: 'test' }, nil, 'none'),
+ access_token: SecureRandom.hex(10), token_type: 'Bearer', refresh_token: SecureRandom.hex(10)
+ }
+ stub_request(:post, 'https://login.microsoftonline.com/common/oauth2/v2.0/token')
+ .with(body: { 'code' => code, 'grant_type' => 'authorization_code',
+ 'redirect_uri' => "#{ENV.fetch('FRONTEND_URL', 'http://localhost:3000')}/microsoft/callback" })
+ .to_return(status: 200, body: response_body.to_json, headers: { 'Content-Type' => 'application/json' })
+
+ get microsoft_callback_url, params: { code: code, state: state }
+
+ channel = account.inboxes.last.channel
+ expect(channel.imap_login).to eq upn
+ expect(channel.email).to eq mailbox
+ end
+
it 'creates updates inbox channel config if inbox exists and authentication is successful' do
inbox = create(:channel_email, account: account, email: email)&.inbox
expect(inbox.channel.provider_config).to eq({})
diff --git a/spec/enterprise/controllers/api/v1/accounts/audit_logs_controller_spec.rb b/spec/enterprise/controllers/api/v1/accounts/audit_logs_controller_spec.rb
index 8cf28f93a..7c6c69ec6 100644
--- a/spec/enterprise/controllers/api/v1/accounts/audit_logs_controller_spec.rb
+++ b/spec/enterprise/controllers/api/v1/accounts/audit_logs_controller_spec.rb
@@ -51,10 +51,13 @@ RSpec.describe 'Enterprise Audit API', type: :request do
expect(json_response['audit_logs'][1]['action']).to eql('create')
expect(json_response['audit_logs'][1]['audited_changes']['name']).to eql(inbox.name)
expect(json_response['audit_logs'][1]['associated_id']).to eql(account.id)
- expect(json_response['current_page']).to be(1)
# contains audit log for account user as well
# contains audit logs for account update(enable audit logs)
- expect(json_response['total_entries']).to be(3)
+ expect(json_response.slice('current_page', 'per_page', 'total_entries')).to eql(
+ 'current_page' => 1,
+ 'per_page' => 25,
+ 'total_entries' => 3
+ )
end
end
end
diff --git a/spec/enterprise/controllers/api/v1/accounts/captain/bulk_actions_controller_spec.rb b/spec/enterprise/controllers/api/v1/accounts/captain/bulk_actions_controller_spec.rb
index 744fa4ea8..31a0bc36f 100644
--- a/spec/enterprise/controllers/api/v1/accounts/captain/bulk_actions_controller_spec.rb
+++ b/spec/enterprise/controllers/api/v1/accounts/captain/bulk_actions_controller_spec.rb
@@ -147,7 +147,7 @@ RSpec.describe 'Api::V1::Accounts::Captain::BulkActions', type: :request do
params: sync_params,
headers: admin.create_new_auth_token,
as: :json
- end.to have_enqueued_job(Captain::Documents::PerformSyncJob).exactly(documents.size).times
+ end.to have_enqueued_job(Captain::Documents::PerformSyncJob).on_queue('low').exactly(documents.size).times
documents.each do |document|
expect(document.reload).to have_attributes(
@@ -190,7 +190,7 @@ RSpec.describe 'Api::V1::Accounts::Captain::BulkActions', type: :request do
expect(response).to have_http_status(:ok)
end
- it 'skips documents that already have a sync in progress' do
+ it 'queues documents that already have a sync in progress' do
syncing_document = create(:captain_document, assistant: assistant, account: account, status: :available)
syncing_document.update!(sync_status: :syncing, last_sync_attempted_at: 1.minute.ago)
@@ -199,9 +199,10 @@ RSpec.describe 'Api::V1::Accounts::Captain::BulkActions', type: :request do
params: sync_params.merge(ids: [syncing_document.id]),
headers: admin.create_new_auth_token,
as: :json
- end.not_to have_enqueued_job(Captain::Documents::PerformSyncJob)
+ end.to have_enqueued_job(Captain::Documents::PerformSyncJob).with(syncing_document).on_queue('low')
expect(response).to have_http_status(:ok)
+ expect(json_response).to eq({ ids: [syncing_document.id], count: 1 })
end
it 'queues stale syncing documents again' do
diff --git a/spec/enterprise/controllers/api/v1/accounts/captain/documents_controller_spec.rb b/spec/enterprise/controllers/api/v1/accounts/captain/documents_controller_spec.rb
index cb7731652..77cb25f49 100644
--- a/spec/enterprise/controllers/api/v1/accounts/captain/documents_controller_spec.rb
+++ b/spec/enterprise/controllers/api/v1/accounts/captain/documents_controller_spec.rb
@@ -243,7 +243,7 @@ RSpec.describe 'Api::V1::Accounts::Captain::Documents', type: :request do
before do
create_list(:captain_document, 5, assistant: assistant, account: account)
- create(:installation_config, name: 'CAPTAIN_CLOUD_PLAN_LIMITS', value: captain_limits.to_json)
+ InstallationConfig.find_or_initialize_by(name: 'CAPTAIN_CLOUD_PLAN_LIMITS').update!(value: captain_limits.to_json)
post "/api/v1/accounts/#{account.id}/captain/documents",
params: valid_attributes,
headers: admin.create_new_auth_token
@@ -281,7 +281,7 @@ RSpec.describe 'Api::V1::Accounts::Captain::Documents', type: :request do
expect do
post "/api/v1/accounts/#{account.id}/captain/documents/#{document.id}/sync",
headers: admin.create_new_auth_token, as: :json
- end.to have_enqueued_job(Captain::Documents::PerformSyncJob).with(document)
+ end.to have_enqueued_job(Captain::Documents::PerformSyncJob).with(document).on_queue('low')
expect(document.reload).to have_attributes(
sync_status: 'syncing',
@@ -292,15 +292,15 @@ RSpec.describe 'Api::V1::Accounts::Captain::Documents', type: :request do
expect(response).to have_http_status(:accepted)
end
- it 'rejects documents that already have a sync in progress' do
+ it 'queues documents that already have a sync in progress' do
document.update!(sync_status: :syncing, last_sync_attempted_at: 1.minute.ago)
expect do
post "/api/v1/accounts/#{account.id}/captain/documents/#{document.id}/sync",
headers: admin.create_new_auth_token, as: :json
- end.not_to have_enqueued_job(Captain::Documents::PerformSyncJob)
+ end.to have_enqueued_job(Captain::Documents::PerformSyncJob).with(document).on_queue('low')
- expect(response).to have_http_status(:unprocessable_entity)
+ expect(response).to have_http_status(:accepted)
end
it 'queues stale syncing documents again' do
diff --git a/spec/enterprise/jobs/captain/documents/perform_sync_job_spec.rb b/spec/enterprise/jobs/captain/documents/perform_sync_job_spec.rb
new file mode 100644
index 000000000..dfec1c9d7
--- /dev/null
+++ b/spec/enterprise/jobs/captain/documents/perform_sync_job_spec.rb
@@ -0,0 +1,60 @@
+require 'rails_helper'
+
+RSpec.describe Captain::Documents::PerformSyncJob, type: :job do
+ let(:account) { create(:account) }
+ let(:assistant) { create(:captain_assistant, account: account) }
+ let(:document) { create(:captain_document, assistant: assistant, account: account, status: :available) }
+
+ def stub_lock(job)
+ allow(job).to receive(:with_lock).and_yield
+ end
+
+ def stub_page_fetch(content: 'Updated content')
+ fetch_result = Captain::Documents::SinglePageFetcher::Result.new(
+ success: true,
+ title: 'Updated title',
+ content: content
+ )
+ fetcher = instance_double(Captain::Documents::SinglePageFetcher, fetch: fetch_result)
+ allow(Captain::Documents::SinglePageFetcher).to receive(:new).and_return(fetcher)
+ end
+
+ def stub_page_fetch_failure
+ fetcher = instance_double(Captain::Documents::SinglePageFetcher)
+ allow(fetcher).to receive(:fetch).and_raise(StandardError, 'boom')
+ allow(Captain::Documents::SinglePageFetcher).to receive(:new).and_return(fetcher)
+ end
+
+ it 'syncs the document content' do
+ travel_to Time.zone.local(2026, 5, 18, 10, 0, 0) do
+ job = described_class.new
+ stub_lock(job)
+ stub_page_fetch
+
+ job.perform(document)
+
+ expect(document.reload).to have_attributes(
+ sync_status: 'synced',
+ last_sync_attempted_at: Time.current,
+ last_synced_at: Time.current,
+ content: 'Updated content'
+ )
+ end
+ end
+
+ it 'marks unexpected failures as failed' do
+ travel_to Time.zone.local(2026, 5, 18, 10, 0, 0) do
+ job = described_class.new
+ stub_lock(job)
+ stub_page_fetch_failure
+
+ expect { job.perform(document) }.to raise_error(StandardError, 'boom')
+
+ expect(document.reload).to have_attributes(
+ sync_status: 'failed',
+ last_sync_error_code: 'sync_error',
+ last_sync_attempted_at: Time.current
+ )
+ end
+ end
+end
diff --git a/spec/enterprise/jobs/captain/documents/schedule_syncs_job_spec.rb b/spec/enterprise/jobs/captain/documents/schedule_syncs_job_spec.rb
index 76604a0b5..3a241d0a1 100644
--- a/spec/enterprise/jobs/captain/documents/schedule_syncs_job_spec.rb
+++ b/spec/enterprise/jobs/captain/documents/schedule_syncs_job_spec.rb
@@ -5,11 +5,28 @@ RSpec.describe Captain::Documents::ScheduleSyncsJob, type: :job do
let(:assistant) { create(:captain_assistant, account: account) }
before do
- create(:installation_config, name: 'CAPTAIN_DOCUMENT_AUTO_SYNC_INTERVALS', value: { business: 24, hacker: nil }.to_json)
+ set_installation_config('CAPTAIN_DOCUMENT_AUTO_SYNC_INTERVALS', { business: 168, enterprise: 24, startups: 720, hacker: nil }.to_json)
+ set_installation_config('CAPTAIN_DOCUMENT_AUTO_SYNC_PER_ACCOUNT_BATCH_LIMIT', 50)
+ set_installation_config('CAPTAIN_DOCUMENT_AUTO_SYNC_GLOBAL_BATCH_LIMIT', 1000)
account.enable_features!('captain_document_auto_sync')
clear_enqueued_jobs
end
+ def set_installation_config(name, value)
+ InstallationConfig.find_or_initialize_by(name: name).tap do |config|
+ config.value = value
+ config.save!
+ end
+ end
+
+ def update_sync_limit(name, value)
+ InstallationConfig.find_by!(name: name).update!(value: value)
+ end
+
+ def sync_job_for(document)
+ have_enqueued_job(Captain::Documents::PerformSyncJob).with(document)
+ end
+
context 'when the account has not enabled auto-sync' do
before { account.disable_features!('captain_document_auto_sync') }
@@ -32,6 +49,25 @@ RSpec.describe Captain::Documents::ScheduleSyncsJob, type: :job do
end
end
+ context 'when a plan name is passed' do
+ it 'queues due documents only for that plan' do
+ enterprise_account = create(:account, custom_attributes: { plan_name: 'Enterprise' })
+ enterprise_account.enable_features!('captain_document_auto_sync')
+ enterprise_assistant = create(:captain_assistant, account: enterprise_account)
+ business_document = create(:captain_document, assistant: assistant, account: account, status: :available)
+ enterprise_document = create(:captain_document, assistant: enterprise_assistant, account: enterprise_account, status: :available)
+
+ business_document.update!(sync_status: :synced, last_synced_at: 3.days.ago, last_sync_attempted_at: 3.days.ago)
+ enterprise_document.update!(sync_status: :synced, last_synced_at: 3.days.ago, last_sync_attempted_at: 3.days.ago)
+ clear_enqueued_jobs
+
+ described_class.new.perform('enterprise')
+
+ expect(Captain::Documents::PerformSyncJob).to have_been_enqueued.with(enterprise_document)
+ expect(Captain::Documents::PerformSyncJob).not_to have_been_enqueued.with(business_document)
+ end
+ end
+
context 'when an available document has backfilled sync metadata' do
it 'leaves it alone when last synced within the plan cadence' do
create(
@@ -54,53 +90,34 @@ RSpec.describe Captain::Documents::ScheduleSyncsJob, type: :job do
account: account,
status: :available,
sync_status: :synced,
- last_synced_at: 3.days.ago
+ last_synced_at: 8.days.ago
)
clear_enqueued_jobs
expect { described_class.new.perform }
- .to have_enqueued_job(Captain::Documents::PerformSyncJob).with(document)
+ .to sync_job_for(document).on_queue('purgable')
end
- it 'marks the due document as syncing before queueing' do
+ it 'delays only the queued sync job' do
travel_to Time.zone.local(2026, 4, 27, 10, 0, 0) do
+ job = described_class.new
+ allow(job).to receive(:rand).and_return(30.minutes.to_i)
document = create(
:captain_document,
assistant: assistant,
account: account,
status: :available,
sync_status: :synced,
- last_synced_at: 3.days.ago
+ last_synced_at: 8.days.ago
)
clear_enqueued_jobs
- described_class.new.perform
+ job.perform
- expect(document.reload).to have_attributes(
- sync_status: 'syncing',
- last_sync_attempted_at: Time.current
- )
+ expect(Captain::Documents::PerformSyncJob)
+ .to have_been_enqueued.with(document).at(30.minutes.from_now)
end
end
-
- it 'does not queue the same document again while the reserved sync is fresh' do
- document = create(
- :captain_document,
- assistant: assistant,
- account: account,
- status: :available,
- sync_status: :synced,
- last_synced_at: 2.days.ago
- )
- clear_enqueued_jobs
-
- expect { described_class.new.perform }
- .to have_enqueued_job(Captain::Documents::PerformSyncJob).with(document)
-
- clear_enqueued_jobs
-
- expect { described_class.new.perform }.not_to have_enqueued_job(Captain::Documents::PerformSyncJob)
- end
end
context 'when an available document was synced within the plan cadence' do
@@ -116,78 +133,59 @@ RSpec.describe Captain::Documents::ScheduleSyncsJob, type: :job do
context 'when an available document was last synced before the plan cadence' do
it 'queues a sync for that document' do
document = create(:captain_document, assistant: assistant, account: account, status: :available)
- document.update!(sync_status: :synced, last_synced_at: 2.days.ago, last_sync_attempted_at: 2.days.ago)
+ document.update!(sync_status: :synced, last_synced_at: 8.days.ago, last_sync_attempted_at: 8.days.ago)
clear_enqueued_jobs
expect { described_class.new.perform }
- .to have_enqueued_job(Captain::Documents::PerformSyncJob).with(document)
+ .to sync_job_for(document)
+ end
+ end
+
+ context 'when jitter spreads queued sync execution' do
+ it 'uses a widened due window so jittered syncs do not skip the next plan run' do
+ travel_to Time.zone.local(2026, 4, 27, 10, 0, 0) do
+ job = described_class.new
+ interval = 1.week
+ due_window = (interval.to_i / 2).seconds
+ allow(job).to receive(:rand).and_return(2.hours.to_i)
+ document = create(:captain_document, assistant: assistant, account: account, status: :available)
+
+ document.update!(sync_status: :synced, last_synced_at: (due_window - 1.minute).ago)
+ clear_enqueued_jobs
+
+ expect { job.perform }.not_to have_enqueued_job(Captain::Documents::PerformSyncJob)
+
+ document.update!(sync_status: :synced, last_synced_at: (due_window + 1.minute).ago)
+ clear_enqueued_jobs
+
+ expect { job.perform }
+ .to have_enqueued_job(Captain::Documents::PerformSyncJob)
+ .with(document)
+ .on_queue('purgable')
+ .at(2.hours.from_now)
+ end
end
- it 'skips invalid legacy documents without counting them against the account cap' do
- stub_const("#{described_class}::PER_ACCOUNT_HOURLY_CAP", 1)
- create(
- :captain_document,
- assistant: assistant,
- account: account,
- status: :in_progress,
- content: nil,
- external_link: 'https://example.com'
- )
- invalid_document = build(
- :captain_document,
- assistant: assistant,
- account: account,
- status: :available,
- sync_status: :synced,
- last_synced_at: 2.days.ago,
- last_sync_attempted_at: 2.days.ago,
- external_link: 'https://example.com/'
- )
- invalid_document.save!(validate: false)
- valid_document = create(:captain_document, assistant: assistant, account: account, status: :available)
- valid_document.update!(sync_status: :synced, last_synced_at: 2.days.ago, last_sync_attempted_at: 2.days.ago)
- clear_enqueued_jobs
+ it 'uses a random delay inside the cadence window' do
+ travel_to Time.zone.local(2026, 4, 27, 10, 0, 0) do
+ document = create(:captain_document, assistant: assistant, account: account, status: :available)
+ document.update!(sync_status: :synced, last_synced_at: 8.days.ago)
+ job = described_class.new
+ sync_execution_delay = 12_345.seconds
- expect { described_class.new.perform }.not_to raise_error
- expect(Captain::Documents::PerformSyncJob).not_to have_been_enqueued.with(invalid_document)
- expect(Captain::Documents::PerformSyncJob).to have_been_enqueued.with(valid_document)
- end
+ clear_enqueued_jobs
+ allow(job).to receive(:rand).with(0..described_class::WEEKLY_SYNC_JITTER.to_i).and_return(sync_execution_delay.to_i)
- it 'keeps paging due documents when invalid documents fill the first batch' do
- stub_const("#{described_class}::PER_ACCOUNT_HOURLY_CAP", 1)
- stub_const("#{described_class}::DUE_DOCUMENT_BATCH_SIZE", 1)
- create(
- :captain_document,
- assistant: assistant,
- account: account,
- status: :in_progress,
- content: nil,
- external_link: 'https://example.com'
- )
- invalid_document = build(
- :captain_document,
- assistant: assistant,
- account: account,
- status: :available,
- sync_status: :synced,
- last_synced_at: 2.days.ago,
- last_sync_attempted_at: 3.days.ago,
- external_link: 'https://example.com/'
- )
- invalid_document.save!(validate: false)
- valid_document = create(:captain_document, assistant: assistant, account: account, status: :available)
- valid_document.update!(sync_status: :synced, last_synced_at: 2.days.ago, last_sync_attempted_at: 2.days.ago)
- clear_enqueued_jobs
-
- described_class.new.perform
-
- expect(Captain::Documents::PerformSyncJob).to have_been_enqueued.with(valid_document)
+ expect { job.perform }
+ .to sync_job_for(document)
+ .at(sync_execution_delay.from_now)
+ end
end
end
context 'when more documents are due than the account cap allows' do
before do
- stub_const("#{described_class}::PER_ACCOUNT_HOURLY_CAP", 2)
+ update_sync_limit('CAPTAIN_DOCUMENT_AUTO_SYNC_PER_ACCOUNT_BATCH_LIMIT', 2)
end
it 'queues backfilled and oldest-attempted documents first' do
@@ -195,26 +193,47 @@ RSpec.describe Captain::Documents::ScheduleSyncsJob, type: :job do
oldest_document = create(:captain_document, assistant: assistant, account: account, status: :available)
backfilled_document = create(:captain_document, assistant: assistant, account: account, status: :available)
- newest_document.update!(sync_status: :synced, last_synced_at: 2.days.ago, last_sync_attempted_at: 2.days.ago)
- oldest_document.update!(sync_status: :synced, last_synced_at: 3.days.ago, last_sync_attempted_at: 3.days.ago)
- backfilled_document.update!(sync_status: :synced, last_synced_at: 4.days.ago, last_sync_attempted_at: nil)
+ newest_document.update!(sync_status: :synced, last_synced_at: 8.days.ago, last_sync_attempted_at: 8.days.ago)
+ oldest_document.update!(sync_status: :synced, last_synced_at: 9.days.ago, last_sync_attempted_at: 9.days.ago)
+ backfilled_document.update!(sync_status: :synced, last_synced_at: 10.days.ago, last_sync_attempted_at: nil)
clear_enqueued_jobs
expect { described_class.new.perform }
- .to have_enqueued_job(Captain::Documents::PerformSyncJob).with(backfilled_document)
- .and have_enqueued_job(Captain::Documents::PerformSyncJob).with(oldest_document)
+ .to sync_job_for(backfilled_document)
+ .and sync_job_for(oldest_document)
expect(Captain::Documents::PerformSyncJob).not_to have_been_enqueued.with(newest_document)
end
end
+ context 'when sync caps are configured' do
+ it 'uses installation config caps for per-account and global limits' do
+ update_sync_limit('CAPTAIN_DOCUMENT_AUTO_SYNC_PER_ACCOUNT_BATCH_LIMIT', 2)
+ update_sync_limit('CAPTAIN_DOCUMENT_AUTO_SYNC_GLOBAL_BATCH_LIMIT', 3)
+
+ second_account = create(:account, custom_attributes: { plan_name: 'business' })
+ second_account.enable_features!('captain_document_auto_sync')
+ second_assistant = create(:captain_assistant, account: second_account)
+
+ first_account_documents = create_list(:captain_document, 3, assistant: assistant, account: account, status: :available)
+ second_account_documents = create_list(:captain_document, 3, assistant: second_assistant, account: second_account, status: :available)
+ (first_account_documents + second_account_documents).each do |document|
+ document.update!(sync_status: :synced, last_synced_at: 8.days.ago, last_sync_attempted_at: 8.days.ago)
+ end
+ clear_enqueued_jobs
+
+ expect { described_class.new.perform }
+ .to have_enqueued_job(Captain::Documents::PerformSyncJob).exactly(3).times
+ end
+ end
+
context 'when an available document failed before the plan cadence' do
it 'queues a sync for that document' do
document = create(:captain_document, assistant: assistant, account: account, status: :available)
- document.update!(sync_status: :failed, last_sync_attempted_at: 2.days.ago)
+ document.update!(sync_status: :failed, last_sync_attempted_at: 8.days.ago)
clear_enqueued_jobs
expect { described_class.new.perform }
- .to have_enqueued_job(Captain::Documents::PerformSyncJob).with(document)
+ .to sync_job_for(document)
end
end
@@ -228,7 +247,7 @@ RSpec.describe Captain::Documents::ScheduleSyncsJob, type: :job do
clear_enqueued_jobs
expect { described_class.new.perform }
- .to have_enqueued_job(Captain::Documents::PerformSyncJob).with(document)
+ .to sync_job_for(document)
end
end
diff --git a/spec/enterprise/jobs/captain/tools/firecrawl_parser_job_spec.rb b/spec/enterprise/jobs/captain/tools/firecrawl_parser_job_spec.rb
index e12efc54b..6aed60385 100644
--- a/spec/enterprise/jobs/captain/tools/firecrawl_parser_job_spec.rb
+++ b/spec/enterprise/jobs/captain/tools/firecrawl_parser_job_spec.rb
@@ -61,6 +61,16 @@ RSpec.describe Captain::Tools::FirecrawlParserJob, type: :job do
end
end
+ it 'stores external links longer than 255 characters' do
+ long_url = "https://example.com/#{'arabic-product-slug-' * 300}"
+ payload[:metadata]['url'] = long_url
+
+ described_class.perform_now(assistant_id: assistant.id, payload: payload)
+
+ expect(assistant.documents.last.external_link).to eq(long_url)
+ expect(assistant.documents.last.external_link.length).to be > 255
+ end
+
context 'when an error occurs' do
it 'raises an error with a descriptive message' do
allow(Captain::Assistant).to receive(:find).and_raise(ActiveRecord::RecordNotFound)
diff --git a/spec/enterprise/jobs/enterprise/internal/trigger_daily_scheduled_items_job_spec.rb b/spec/enterprise/jobs/enterprise/internal/trigger_daily_scheduled_items_job_spec.rb
new file mode 100644
index 000000000..3afc20328
--- /dev/null
+++ b/spec/enterprise/jobs/enterprise/internal/trigger_daily_scheduled_items_job_spec.rb
@@ -0,0 +1,42 @@
+require 'rails_helper'
+
+RSpec.describe Internal::TriggerDailyScheduledItemsJob do
+ before do
+ allow(ChatwootHub).to receive(:installation_identifier).and_return('test-installation-id')
+ allow(Captain::Documents::ScheduleSyncsJob).to receive(:perform_later)
+ end
+
+ it 'enqueues enterprise Captain document auto-sync every day' do
+ travel_to Time.zone.parse('2026-05-26 00:00:00 UTC') do
+ described_class.perform_now
+ end
+
+ expect(Captain::Documents::ScheduleSyncsJob).to have_received(:perform_later).with('enterprise')
+ end
+
+ it 'enqueues business Captain document auto-sync weekly' do
+ travel_to Time.zone.parse('2026-05-24 00:00:00 UTC') do
+ described_class.perform_now
+ end
+
+ expect(Captain::Documents::ScheduleSyncsJob).to have_received(:perform_later).with('business')
+ end
+
+ it 'enqueues startup Captain document auto-sync monthly' do
+ travel_to Time.zone.parse('2026-06-01 00:00:00 UTC') do
+ described_class.perform_now
+ end
+
+ expect(Captain::Documents::ScheduleSyncsJob).to have_received(:perform_later).with('startups')
+ end
+
+ it 'does not enqueue business or startup Captain document auto-sync before their plan window' do
+ travel_to Time.zone.parse('2026-05-25 00:00:00 UTC') do
+ described_class.perform_now
+ end
+
+ expect(Captain::Documents::ScheduleSyncsJob).to have_received(:perform_later).with('enterprise')
+ expect(Captain::Documents::ScheduleSyncsJob).not_to have_received(:perform_later).with('business')
+ expect(Captain::Documents::ScheduleSyncsJob).not_to have_received(:perform_later).with('startups')
+ end
+end
diff --git a/spec/enterprise/policies/contact_policy_spec.rb b/spec/enterprise/policies/contact_policy_spec.rb
new file mode 100644
index 000000000..7dbc837a5
--- /dev/null
+++ b/spec/enterprise/policies/contact_policy_spec.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe 'Enterprise::ContactPolicy', type: :policy do
+ subject(:contact_policy) { ContactPolicy }
+
+ let(:account) { create(:account) }
+ let(:contact) { create(:contact, account: account) }
+ let(:custom_role) { create(:custom_role, account: account, permissions: ['contact_manage']) }
+ let(:agent) { create(:user) }
+ let(:account_user) { create(:account_user, user: agent, account: account, role: :agent, custom_role: custom_role) }
+ let(:agent_context) { { user: agent, account: account, account_user: account_user } }
+
+ permissions :export? do
+ context 'when agent has contact_manage permission' do
+ it { expect(contact_policy).to permit(agent_context, contact) }
+ end
+ end
+
+ permissions :import? do
+ context 'when agent has contact_manage permission' do
+ it { expect(contact_policy).to permit(agent_context, contact) }
+ end
+ end
+end
diff --git a/spec/enterprise/services/messages/audio_transcription_service_spec.rb b/spec/enterprise/services/messages/audio_transcription_service_spec.rb
index 212c0bf01..32752c2b2 100644
--- a/spec/enterprise/services/messages/audio_transcription_service_spec.rb
+++ b/spec/enterprise/services/messages/audio_transcription_service_spec.rb
@@ -72,7 +72,7 @@ RSpec.describe Messages::AudioTranscriptionService, type: :service do
content_type: 'audio/mpeg'
)
allow(service).to receive(:can_transcribe?).and_return(true)
- allow(attachment.file.blob).to receive(:byte_size).and_return(described_class::WHISPER_BYTE_LIMIT + 1)
+ allow(attachment.file.blob).to receive(:byte_size).and_return(described_class::TRANSCRIPTION_BYTE_LIMIT + 1)
end
it 'returns an error without calling Whisper' do
diff --git a/spec/enterprise/services/whatsapp/incoming_call_service_spec.rb b/spec/enterprise/services/whatsapp/incoming_call_service_spec.rb
index 53c7974f4..53b8ec52f 100644
--- a/spec/enterprise/services/whatsapp/incoming_call_service_spec.rb
+++ b/spec/enterprise/services/whatsapp/incoming_call_service_spec.rb
@@ -32,6 +32,9 @@ describe Whatsapp::IncomingCallService do
describe 'inbound connect' do
let(:sdp_offer) { "v=0\r\n...sdp..." }
+ let!(:agent) { create(:user, account: account) }
+
+ before { create(:inbox_member, inbox: inbox, user: agent) }
it 'creates the Call + Conversation + voice_call message and broadcasts voice_call.incoming' do
allow(ActionCable.server).to receive(:broadcast)
@@ -44,10 +47,16 @@ describe Whatsapp::IncomingCallService do
expect(call).to have_attributes(provider: 'whatsapp', direction: 'incoming', status: 'ringing',
provider_call_id: provider_call_id)
expect(call.meta['sdp_offer']).to eq(sdp_offer)
+ # No agent is online, so the call falls back to the inbox's agents (and
+ # account admins) — never the whole-account stream.
expect(ActionCable.server).to have_received(:broadcast).with(
- "account_#{account.id}",
+ agent.pubsub_token,
hash_including(event: 'voice_call.incoming', data: hash_including(sdp_offer: sdp_offer))
)
+ expect(ActionCable.server).not_to have_received(:broadcast).with(
+ "account_#{account.id}",
+ hash_including(event: 'voice_call.incoming')
+ )
end
end
diff --git a/spec/helpers/instagram/integration_helper_spec.rb b/spec/helpers/instagram/integration_helper_spec.rb
index 7a8bb30a4..25ec46b58 100644
--- a/spec/helpers/instagram/integration_helper_spec.rb
+++ b/spec/helpers/instagram/integration_helper_spec.rb
@@ -82,6 +82,7 @@ RSpec.describe Instagram::IntegrationHelper do
context 'when client secret is not configured' do
let(:client_secret) { nil }
+ let(:valid_token) { 'any-token' }
it 'returns nil' do
expect(verify_instagram_token(valid_token)).to be_nil
diff --git a/spec/helpers/linear/integration_helper_spec.rb b/spec/helpers/linear/integration_helper_spec.rb
index 4f0f65c31..958baa4bf 100644
--- a/spec/helpers/linear/integration_helper_spec.rb
+++ b/spec/helpers/linear/integration_helper_spec.rb
@@ -65,6 +65,7 @@ RSpec.describe Linear::IntegrationHelper do
context 'when client secret is not configured' do
let(:client_secret) { nil }
+ let(:valid_token) { 'any-token' }
it 'returns nil' do
expect(verify_linear_token(valid_token)).to be_nil
diff --git a/spec/helpers/portal_helper_spec.rb b/spec/helpers/portal_helper_spec.rb
index d55c83be7..993399e74 100644
--- a/spec/helpers/portal_helper_spec.rb
+++ b/spec/helpers/portal_helper_spec.rb
@@ -140,7 +140,7 @@ describe PortalHelper do
context 'when theme is not present' do
it 'returns the correct link' do
expect(helper.generate_home_link('portal_slug', 'en', nil, true)).to eq(
- '/hc/portal_slug/en'
+ '/hc/portal_slug/en?show_plain_layout=true'
)
end
end
@@ -148,7 +148,7 @@ describe PortalHelper do
context 'when theme is present and plain layout is enabled' do
it 'returns the correct link' do
expect(helper.generate_home_link('portal_slug', 'en', 'dark', true)).to eq(
- '/hc/portal_slug/en?theme=dark'
+ '/hc/portal_slug/en?show_plain_layout=true&theme=dark'
)
end
end
@@ -172,7 +172,7 @@ describe PortalHelper do
theme: nil,
is_plain_layout_enabled: true
)).to eq(
- '/hc/portal_slug/en/categories/category_slug'
+ '/hc/portal_slug/en/categories/category_slug?show_plain_layout=true'
)
end
end
@@ -186,7 +186,7 @@ describe PortalHelper do
theme: 'dark',
is_plain_layout_enabled: true
)).to eq(
- '/hc/portal_slug/en/categories/category_slug?theme=dark'
+ '/hc/portal_slug/en/categories/category_slug?show_plain_layout=true&theme=dark'
)
end
end
@@ -210,7 +210,7 @@ describe PortalHelper do
context 'when theme is not present' do
it 'returns the correct link' do
expect(helper.generate_article_link('portal_slug', 'article_slug', nil, true)).to eq(
- '/hc/portal_slug/articles/article_slug'
+ '/hc/portal_slug/articles/article_slug?show_plain_layout=true'
)
end
end
@@ -218,7 +218,7 @@ describe PortalHelper do
context 'when theme is present and plain layout is enabled' do
it 'returns the correct link' do
expect(helper.generate_article_link('portal_slug', 'article_slug', 'dark', true)).to eq(
- '/hc/portal_slug/articles/article_slug?theme=dark'
+ '/hc/portal_slug/articles/article_slug?show_plain_layout=true&theme=dark'
)
end
end
diff --git a/spec/helpers/shopify/integration_helper_spec.rb b/spec/helpers/shopify/integration_helper_spec.rb
index 15b7120d4..bfad66d9a 100644
--- a/spec/helpers/shopify/integration_helper_spec.rb
+++ b/spec/helpers/shopify/integration_helper_spec.rb
@@ -65,6 +65,7 @@ RSpec.describe Shopify::IntegrationHelper do
context 'when client secret is not configured' do
let(:client_secret) { nil }
+ let(:valid_token) { 'any-token' }
it 'returns nil' do
expect(verify_shopify_token(valid_token)).to be_nil
diff --git a/spec/jobs/inboxes/fetch_imap_emails_job_spec.rb b/spec/jobs/inboxes/fetch_imap_emails_job_spec.rb
index da4b95b15..04336c619 100644
--- a/spec/jobs/inboxes/fetch_imap_emails_job_spec.rb
+++ b/spec/jobs/inboxes/fetch_imap_emails_job_spec.rb
@@ -88,7 +88,10 @@ RSpec.describe Inboxes::FetchImapEmailsJob do
end
context 'when the fetch service returns the email objects' do
- let(:inbound_mail) { create_inbound_email_from_fixture('welcome.eml').mail }
+ let(:inbound_mail) { instance_double(Mail::Message, message_id: 'message-id') }
+ let(:failure_cache_key) { "email_failures:#{inbound_mail.message_id}" }
+ let(:second_inbound_mail) { instance_double(Mail::Message, message_id: 'second-message-id') }
+ let(:second_failure_cache_key) { "email_failures:#{second_inbound_mail.message_id}" }
let(:mailbox) { double }
let(:exception_tracker) { double }
let(:fetch_service) { double }
@@ -101,6 +104,11 @@ RSpec.describe Inboxes::FetchImapEmailsJob do
allow(fetch_service).to receive(:perform).and_return([inbound_mail])
end
+ after do
+ Rails.cache.delete(failure_cache_key)
+ Rails.cache.delete(second_failure_cache_key)
+ end
+
it 'calls the mailbox to create emails' do
allow(mailbox).to receive(:process)
@@ -111,6 +119,36 @@ RSpec.describe Inboxes::FetchImapEmailsJob do
described_class.perform_now(imap_email_channel)
end
+ it 'marks the email as failed when processing times out' do
+ allow(Timeout).to receive(:timeout).and_raise(Timeout::Error)
+ allow(Rails.cache).to receive(:read).and_call_original
+ allow(Rails.cache).to receive(:read).with(failure_cache_key).and_return(nil)
+
+ expect(Rails.cache).to receive(:write).with(failure_cache_key, 1, expires_in: 6.hours)
+
+ described_class.perform_now(imap_email_channel)
+ end
+
+ it 'continues processing remaining emails when one email fails' do
+ allow(fetch_service).to receive(:perform).and_return([inbound_mail, second_inbound_mail])
+ allow(mailbox).to receive(:process).with(inbound_mail, imap_email_channel).and_raise(StandardError)
+ allow(mailbox).to receive(:process).with(second_inbound_mail, imap_email_channel)
+ allow(exception_tracker).to receive(:capture_exception)
+
+ described_class.perform_now(imap_email_channel)
+
+ expect(mailbox).to have_received(:process).with(second_inbound_mail, imap_email_channel)
+ end
+
+ it 'skips emails that have failed multiple times recently' do
+ allow(Rails.cache).to receive(:read).and_call_original
+ allow(Rails.cache).to receive(:read).with(failure_cache_key).and_return(3)
+
+ expect(mailbox).not_to receive(:process)
+
+ described_class.perform_now(imap_email_channel)
+ end
+
it 'logs errors if mailbox returns errors' do
allow(mailbox).to receive(:process).and_raise(StandardError)
diff --git a/spec/jobs/trigger_scheduled_items_job_spec.rb b/spec/jobs/trigger_scheduled_items_job_spec.rb
index 6ccb41916..141abdcdd 100644
--- a/spec/jobs/trigger_scheduled_items_job_spec.rb
+++ b/spec/jobs/trigger_scheduled_items_job_spec.rb
@@ -40,5 +40,13 @@ RSpec.describe TriggerScheduledItemsJob do
expect(Campaigns::TriggerOneoffCampaignJob).to receive(:perform_later).with(campaign).once
described_class.perform_now
end
+
+ it 'does not trigger campaigns that are already processing' do
+ create(:campaign, inbox: twilio_inbox, account: account, campaign_status: :processing)
+
+ expect(Campaigns::TriggerOneoffCampaignJob).not_to receive(:perform_later)
+
+ described_class.perform_now
+ end
end
end
diff --git a/spec/lib/safe_fetch_spec.rb b/spec/lib/safe_fetch_spec.rb
index 8c54a092c..a124be774 100644
--- a/spec/lib/safe_fetch_spec.rb
+++ b/spec/lib/safe_fetch_spec.rb
@@ -205,6 +205,50 @@ RSpec.describe SafeFetch do
expect(error.class.name).to eq('SafeFetch::UnsafeUrlError')
end
end
+
+ it 'allows private IP literals when private network access is enabled' do
+ private_url = 'http://192.168.3.21/image.png'
+ allow(Resolv).to receive(:getaddresses).with('192.168.3.21').and_return(['192.168.3.21'])
+ stub_request(:get, private_url).to_return(
+ status: 200,
+ body: File.new(Rails.root.join('spec/assets/avatar.png')),
+ headers: { 'Content-Type' => 'image/png' }
+ )
+
+ with_modified_env('SAFE_FETCH_ALLOW_PRIVATE_NETWORK' => 'true') do
+ expect { described_class.fetch(private_url) { nil } }.not_to raise_error
+ end
+ end
+
+ it 'allows private hostnames when private network access is enabled' do
+ private_url = 'http://internal-webhook-service/image.png'
+ allow(Resolv).to receive(:getaddresses).with('internal-webhook-service').and_return(['10.0.0.5'])
+ stub_request(:get, private_url).to_return(
+ status: 200,
+ body: File.new(Rails.root.join('spec/assets/avatar.png')),
+ headers: { 'Content-Type' => 'image/png' }
+ )
+
+ with_modified_env('SAFE_FETCH_ALLOW_PRIVATE_NETWORK' => 'true') do
+ expect { described_class.fetch(private_url) { nil } }.not_to raise_error
+ end
+ end
+
+ it 'allows redirects to private hostnames when private network access is enabled' do
+ redirect_url = 'http://example.com/redirect.png'
+ private_url = 'http://private.example.com/image.png'
+ allow(Resolv).to receive(:getaddresses).with('private.example.com').and_return(['10.0.0.5'])
+ stub_request(:get, redirect_url).to_return(status: 302, headers: { 'Location' => private_url })
+ stub_request(:get, private_url).to_return(
+ status: 200,
+ body: File.new(Rails.root.join('spec/assets/avatar.png')),
+ headers: { 'Content-Type' => 'image/png' }
+ )
+
+ with_modified_env('SAFE_FETCH_ALLOW_PRIVATE_NETWORK' => 'true') do
+ expect { described_class.fetch(redirect_url) { nil } }.not_to raise_error
+ end
+ end
end
context 'with content-type allowlist' do
diff --git a/spec/models/article_spec.rb b/spec/models/article_spec.rb
index 5741b95f0..04466ccd1 100644
--- a/spec/models/article_spec.rb
+++ b/spec/models/article_spec.rb
@@ -20,6 +20,15 @@ RSpec.describe Article do
expect(article).not_to be_valid
expect(article.errors[:content]).to include("can't be blank")
end
+
+ it 'rejects reserved slugs that collide with help center routes' do
+ Article::RESERVED_SLUGS.each do |reserved_slug|
+ article = build(:article, portal_id: portal_1.id, author_id: user.id, category_id: category_1.id,
+ title: reserved_slug, slug: reserved_slug, content: 'content')
+ expect(article).not_to be_valid
+ expect(article.errors[:slug]).to include('is reserved')
+ end
+ end
end
describe 'associations' do
diff --git a/spec/models/campaign_spec.rb b/spec/models/campaign_spec.rb
index 480f187f3..2be6bd588 100644
--- a/spec/models/campaign_spec.rb
+++ b/spec/models/campaign_spec.rb
@@ -83,6 +83,38 @@ RSpec.describe Campaign do
campaign.save!
campaign.trigger!
end
+
+ it 'marks the campaign as processing before triggering the service' do
+ campaign.save!
+ sms_service = double
+
+ expect(Twilio::OneoffSmsCampaignService).to receive(:new).with(campaign: campaign).and_return(sms_service)
+ expect(sms_service).to receive(:perform) do
+ expect(campaign.reload.processing?).to be true
+ end
+
+ campaign.trigger!
+ end
+
+ it 'does not trigger a processing campaign again' do
+ campaign.save!
+ campaign.processing!
+
+ expect(Twilio::OneoffSmsCampaignService).not_to receive(:new)
+
+ campaign.trigger!
+ end
+
+ it 'keeps the campaign processing when triggering fails' do
+ campaign.save!
+ sms_service = double
+
+ expect(Twilio::OneoffSmsCampaignService).to receive(:new).with(campaign: campaign).and_return(sms_service)
+ expect(sms_service).to receive(:perform).and_raise(StandardError, 'provider error')
+
+ expect { campaign.trigger! }.to raise_error(StandardError, 'provider error')
+ expect(campaign.reload.processing?).to be true
+ end
end
context 'when SMS campaign' do
@@ -107,6 +139,22 @@ RSpec.describe Campaign do
end
end
+ context 'when WhatsApp campaign feature is disabled' do
+ let(:account) { create(:account) }
+ let(:whatsapp_channel) do
+ create(:channel_whatsapp, account: account, provider: 'whatsapp_cloud', validate_provider_config: false, sync_templates: false)
+ end
+ let(:campaign) { create(:campaign, account: account, inbox: whatsapp_channel.inbox) }
+
+ it 'does not mark the campaign as processing' do
+ expect(Whatsapp::OneoffCampaignService).not_to receive(:new)
+
+ campaign.trigger!
+
+ expect(campaign.reload.active?).to be true
+ end
+ end
+
context 'when Website campaign' do
let(:campaign) { build(:campaign) }
diff --git a/spec/models/channel/whatsapp_spec.rb b/spec/models/channel/whatsapp_spec.rb
index 9afc21f7c..95bc83932 100644
--- a/spec/models/channel/whatsapp_spec.rb
+++ b/spec/models/channel/whatsapp_spec.rb
@@ -223,12 +223,12 @@ RSpec.describe Channel::Whatsapp do
expect(channel.voice_enabled?).to be true
end
- it 'returns false for whatsapp_cloud channels without embedded_signup source' do
+ it 'returns true for manual whatsapp_cloud channels with calling_enabled' do
channel = create(:channel_whatsapp, account: account, provider: 'whatsapp_cloud',
validate_provider_config: false, sync_templates: false)
channel.update!(provider_config: channel.provider_config.merge('source' => 'manual', 'calling_enabled' => true))
- expect(channel.voice_enabled?).to be false
+ expect(channel.voice_enabled?).to be true
end
it 'returns false for default-provider channels (360dialog) even with calling_enabled' do
diff --git a/spec/services/auto_assignment/assignment_service_spec.rb b/spec/services/auto_assignment/assignment_service_spec.rb
index 36a8c7816..eb6ebf060 100644
--- a/spec/services/auto_assignment/assignment_service_spec.rb
+++ b/spec/services/auto_assignment/assignment_service_spec.rb
@@ -56,6 +56,19 @@ RSpec.describe AutoAssignment::AssignmentService do
expect(conversation.reload.assignee).to be_nil
end
+ it 'short-circuits without iterating conversations when no agents are online' do
+ 3.times do
+ conv = create(:conversation, inbox: inbox, status: 'open')
+ conv.update!(assignee_id: nil)
+ end
+ allow(OnlineStatusTracker).to receive(:get_available_users).and_return({})
+
+ expect(service).not_to receive(:perform_for_conversation)
+
+ assigned_count = service.perform_bulk_assignment(limit: 10)
+ expect(assigned_count).to eq(0)
+ end
+
it 'respects the limit parameter' do
3.times do
conv = create(:conversation, inbox: inbox, status: 'open')
diff --git a/spec/services/contacts/bulk_action_service_spec.rb b/spec/services/contacts/bulk_action_service_spec.rb
index 0411c8455..77eeacff1 100644
--- a/spec/services/contacts/bulk_action_service_spec.rb
+++ b/spec/services/contacts/bulk_action_service_spec.rb
@@ -34,5 +34,19 @@ RSpec.describe Contacts::BulkActionService do
service.perform
end
end
+
+ context 'when labels are removed' do
+ let(:params) { { ids: [10, 20], labels: { remove: %w[vip] }, extra: 'ignored' } }
+
+ it 'delegates to the bulk remove labels service with permitted params' do
+ bulk_remove_service = instance_double(Contacts::BulkRemoveLabelsService, perform: true)
+
+ expect(Contacts::BulkRemoveLabelsService).to receive(:new)
+ .with(account: account, contact_ids: [10, 20], labels: %w[vip])
+ .and_return(bulk_remove_service)
+
+ service.perform
+ end
+ end
end
end
diff --git a/spec/services/contacts/bulk_remove_labels_service_spec.rb b/spec/services/contacts/bulk_remove_labels_service_spec.rb
new file mode 100644
index 000000000..db5a7c530
--- /dev/null
+++ b/spec/services/contacts/bulk_remove_labels_service_spec.rb
@@ -0,0 +1,54 @@
+require 'rails_helper'
+
+RSpec.describe Contacts::BulkRemoveLabelsService do
+ subject(:service) do
+ described_class.new(
+ account: account,
+ contact_ids: [contact_one.id, contact_two.id, other_contact.id],
+ labels: labels
+ )
+ end
+
+ let(:account) { create(:account) }
+ let!(:contact_one) { create(:contact, account: account) }
+ let!(:contact_two) { create(:contact, account: account) }
+ let!(:other_contact) { create(:contact) }
+ let(:labels) { %w[vip] }
+
+ before do
+ contact_one.add_labels(%w[vip support])
+ contact_two.add_labels(%w[vip priority])
+ other_contact.add_labels(%w[vip support])
+ end
+
+ it 'removes labels from contacts that belong to the account' do
+ service.perform
+
+ expect(contact_one.reload.label_list).to contain_exactly('support')
+ expect(contact_two.reload.label_list).to contain_exactly('priority')
+ end
+
+ it 'does not remove labels from contacts outside the account' do
+ service.perform
+
+ expect(other_contact.reload.label_list).to contain_exactly('vip', 'support')
+ end
+
+ it 'returns ids of contacts that were updated' do
+ result = service.perform
+
+ expect(result[:success]).to be(true)
+ expect(result[:updated_contact_ids]).to contain_exactly(contact_one.id, contact_two.id)
+ end
+
+ it 'returns success with no updates when labels are blank' do
+ result = described_class.new(
+ account: account,
+ contact_ids: [contact_one.id],
+ labels: []
+ ).perform
+
+ expect(result).to eq(success: true, updated_contact_ids: [])
+ expect(contact_one.reload.label_list).to contain_exactly('vip', 'support')
+ end
+end
diff --git a/spec/services/sms/oneoff_sms_campaign_service_spec.rb b/spec/services/sms/oneoff_sms_campaign_service_spec.rb
index b1f48b0b9..4d3f4574a 100644
--- a/spec/services/sms/oneoff_sms_campaign_service_spec.rb
+++ b/spec/services/sms/oneoff_sms_campaign_service_spec.rb
@@ -45,6 +45,19 @@ describe Sms::OneoffSmsCampaignService do
expect(campaign.reload.completed?).to be true
end
+ it 'marks the campaign completed after processing the audience' do
+ contact = create(:contact, :with_phone_number, account: account)
+ contact.update_labels([label1.title])
+
+ expect(sms_channel).to receive(:send_text_message) do
+ expect(campaign.reload.completed?).to be false
+ end
+
+ sms_campaign_service.perform
+
+ expect(campaign.reload.completed?).to be true
+ end
+
it 'uses liquid template service to process campaign message' do
contact = create(:contact, :with_phone_number, account: account)
contact.update_labels([label1.title])
diff --git a/spec/services/twilio/oneoff_sms_campaign_service_spec.rb b/spec/services/twilio/oneoff_sms_campaign_service_spec.rb
index 90636a439..d5f4c3e9a 100644
--- a/spec/services/twilio/oneoff_sms_campaign_service_spec.rb
+++ b/spec/services/twilio/oneoff_sms_campaign_service_spec.rb
@@ -61,6 +61,24 @@ describe Twilio::OneoffSmsCampaignService do
expect(campaign.reload.completed?).to be true
end
+ it 'marks the campaign completed after processing the audience' do
+ contact = create(:contact, :with_phone_number, account: account)
+ contact.update_labels([label1.title])
+
+ expect(twilio_messages).to receive(:create).with(
+ body: campaign.message,
+ messaging_service_sid: twilio_sms.messaging_service_sid,
+ to: contact.phone_number,
+ status_callback: 'http://localhost:3000/twilio/delivery_status'
+ ) do
+ expect(campaign.reload.completed?).to be false
+ end
+
+ sms_campaign_service.perform
+
+ expect(campaign.reload.completed?).to be true
+ end
+
it 'uses liquid template service to process campaign message' do
contact = create(:contact, :with_phone_number, account: account)
contact.update_labels([label1.title])
diff --git a/spec/services/whatsapp/incoming_message_service_spec.rb b/spec/services/whatsapp/incoming_message_service_spec.rb
index dbaea621c..fe6b179c1 100644
--- a/spec/services/whatsapp/incoming_message_service_spec.rb
+++ b/spec/services/whatsapp/incoming_message_service_spec.rb
@@ -206,7 +206,7 @@ describe Whatsapp::IncomingMessageService do
expect(whatsapp_channel.inbox.messages.count).to eq(0)
end
- it 'ignores type unsupported and does not create ghost conversation' do
+ it 'stores type unsupported as a placeholder message so the conversation is not headless' do
params = {
'contacts' => [{ 'profile' => { 'name' => 'Sojan Jose' }, 'wa_id' => '2423423243' }],
'messages' => [{
@@ -217,9 +217,12 @@ describe Whatsapp::IncomingMessageService do
}.with_indifferent_access
described_class.new(inbox: whatsapp_channel.inbox, params: params).perform
- expect(whatsapp_channel.inbox.conversations.count).to eq(0)
- expect(Contact.count).to eq(0)
- expect(whatsapp_channel.inbox.messages.count).to eq(0)
+ expect(whatsapp_channel.inbox.conversations.count).to eq(1)
+ expect(Contact.count).to eq(1)
+ expect(whatsapp_channel.inbox.messages.count).to eq(1)
+ message = whatsapp_channel.inbox.messages.last
+ expect(message.content).to eq('This message is unavailable.')
+ expect(message.content_attributes['is_unsupported']).to be(true)
end
end
diff --git a/spec/services/whatsapp/oneoff_campaign_service_spec.rb b/spec/services/whatsapp/oneoff_campaign_service_spec.rb
index 00d7fdd3b..ccae8d52f 100644
--- a/spec/services/whatsapp/oneoff_campaign_service_spec.rb
+++ b/spec/services/whatsapp/oneoff_campaign_service_spec.rb
@@ -82,6 +82,19 @@ describe Whatsapp::OneoffCampaignService do
expect(campaign.reload.completed?).to be true
end
+ it 'marks the campaign completed after processing the audience' do
+ contact = create(:contact, :with_phone_number, account: account)
+ contact.update_labels([label1.title])
+
+ expect(whatsapp_channel).to receive(:send_template) do
+ expect(campaign.reload.completed?).to be false
+ end
+
+ described_class.new(campaign: campaign).perform
+
+ expect(campaign.reload.completed?).to be true
+ end
+
it 'processes contacts with matching labels' do
contact_with_label1, contact_with_label2, contact_with_both_labels =
create_list(:contact, 3, :with_phone_number, account: account)
diff --git a/spec/services/whatsapp/providers/whatsapp_cloud_service_spec.rb b/spec/services/whatsapp/providers/whatsapp_cloud_service_spec.rb
index 156e46349..a94ba2e44 100644
--- a/spec/services/whatsapp/providers/whatsapp_cloud_service_spec.rb
+++ b/spec/services/whatsapp/providers/whatsapp_cloud_service_spec.rb
@@ -60,7 +60,7 @@ describe Whatsapp::Providers::WhatsappCloudService do
attachment = message.attachments.new(account_id: message.account_id, file_type: :image)
attachment.file.attach(io: Rails.root.join('spec/assets/avatar.png').open, filename: 'avatar.png', content_type: 'image/png')
- stub_request(:post, 'https://graph.facebook.com/v13.0/123456789/messages')
+ stub_request(:post, 'https://graph.facebook.com/v24.0/123456789/messages')
.with(
body: hash_including({
messaging_product: 'whatsapp',
@@ -79,7 +79,7 @@ describe Whatsapp::Providers::WhatsappCloudService do
# ref: https://github.com/bblimke/webmock/issues/900
# reason for Webmock::API.hash_including
- stub_request(:post, 'https://graph.facebook.com/v13.0/123456789/messages')
+ stub_request(:post, 'https://graph.facebook.com/v24.0/123456789/messages')
.with(
body: hash_including({
messaging_product: 'whatsapp',
@@ -91,6 +91,41 @@ describe Whatsapp::Providers::WhatsappCloudService do
.to_return(status: 200, body: whatsapp_response.to_json, headers: response_headers)
expect(service.send_message('+123456789', message)).to eq 'message_id'
end
+
+ it 'calls message endpoints for audio voice message with voice flag' do
+ attachment = message.attachments.new(account_id: message.account_id, file_type: :audio, meta: { 'is_voice_message' => true })
+ attachment.file.attach(io: Rails.root.join('spec/assets/sample.ogg').open, filename: 'voice.ogg', content_type: 'audio/ogg')
+
+ stub_request(:post, 'https://graph.facebook.com/v24.0/123456789/messages')
+ .with(
+ body: hash_including({
+ messaging_product: 'whatsapp',
+ to: '+123456789',
+ type: 'audio',
+ audio: WebMock::API.hash_including({ link: anything, voice: true })
+ })
+ )
+ .to_return(status: 200, body: whatsapp_response.to_json, headers: response_headers)
+ expect(service.send_message('+123456789', message)).to eq 'message_id'
+ end
+
+ it 'calls message endpoints for regular audio attachment without voice flag' do
+ attachment = message.attachments.new(account_id: message.account_id, file_type: :audio)
+ attachment.file.attach(io: Rails.root.join('spec/assets/sample.ogg').open, filename: 'audio.ogg', content_type: 'audio/ogg')
+
+ stub_request(:post, 'https://graph.facebook.com/v24.0/123456789/messages')
+ .with(
+ body: hash_including({
+ messaging_product: 'whatsapp',
+ to: '+123456789',
+ type: 'audio'
+ })
+ )
+ .to_return(status: 200, body: whatsapp_response.to_json, headers: response_headers)
+
+ result = service.send_message('+123456789', message)
+ expect(result).to eq 'message_id'
+ end
end
end
diff --git a/swagger/definitions/index.yml b/swagger/definitions/index.yml
index edde31d68..53ff5553b 100644
--- a/swagger/definitions/index.yml
+++ b/swagger/definitions/index.yml
@@ -145,6 +145,34 @@ inbox_create_payload:
$ref: ./request/inbox/create_payload.yml
inbox_update_payload:
$ref: ./request/inbox/update_payload.yml
+inbox_create_web_widget_channel_payload:
+ $ref: ./request/inbox/channels/create_web_widget_channel_payload.yml
+inbox_create_api_channel_payload:
+ $ref: ./request/inbox/channels/create_api_channel_payload.yml
+inbox_create_email_channel_payload:
+ $ref: ./request/inbox/channels/create_email_channel_payload.yml
+inbox_create_line_channel_payload:
+ $ref: ./request/inbox/channels/create_line_channel_payload.yml
+inbox_create_telegram_channel_payload:
+ $ref: ./request/inbox/channels/create_telegram_channel_payload.yml
+inbox_create_whatsapp_channel_payload:
+ $ref: ./request/inbox/channels/create_whatsapp_channel_payload.yml
+inbox_create_sms_channel_payload:
+ $ref: ./request/inbox/channels/create_sms_channel_payload.yml
+inbox_update_web_widget_channel_payload:
+ $ref: ./request/inbox/channels/update_web_widget_channel_payload.yml
+inbox_update_api_channel_payload:
+ $ref: ./request/inbox/channels/update_api_channel_payload.yml
+inbox_update_email_channel_payload:
+ $ref: ./request/inbox/channels/update_email_channel_payload.yml
+inbox_update_line_channel_payload:
+ $ref: ./request/inbox/channels/update_line_channel_payload.yml
+inbox_update_telegram_channel_payload:
+ $ref: ./request/inbox/channels/update_telegram_channel_payload.yml
+inbox_update_whatsapp_channel_payload:
+ $ref: ./request/inbox/channels/update_whatsapp_channel_payload.yml
+inbox_update_sms_channel_payload:
+ $ref: ./request/inbox/channels/update_sms_channel_payload.yml
# Team
team_create_update_payload:
diff --git a/swagger/definitions/request/agent/create_payload.yml b/swagger/definitions/request/agent/create_payload.yml
index 1daeae83a..77180d282 100644
--- a/swagger/definitions/request/agent/create_payload.yml
+++ b/swagger/definitions/request/agent/create_payload.yml
@@ -17,12 +17,12 @@ properties:
enum: ['agent', 'administrator']
description: Whether its administrator or agent
example: 'agent'
- availability_status:
+ availability:
type: string
- enum: ['available', 'busy', 'offline']
- description: The availability setting of the agent.
- example: 'available'
+ enum: ['online', 'busy', 'offline']
+ description: The configured availability of the agent.
+ example: 'online'
auto_offline:
type: boolean
- description: Whether the availability status of agent is configured to go offline automatically when away.
+ description: Whether the agent is automatically marked offline when they are away.
example: true
diff --git a/swagger/definitions/request/agent/update_payload.yml b/swagger/definitions/request/agent/update_payload.yml
index fc8d1457d..168d46f49 100644
--- a/swagger/definitions/request/agent/update_payload.yml
+++ b/swagger/definitions/request/agent/update_payload.yml
@@ -7,12 +7,12 @@ properties:
enum: ['agent', 'administrator']
description: Whether its administrator or agent
example: 'agent'
- availability_status:
+ availability:
type: string
- enum: ['available', 'busy', 'offline']
- description: The availability status of the agent.
- example: 'available'
+ enum: ['online', 'busy', 'offline']
+ description: The configured availability of the agent.
+ example: 'online'
auto_offline:
type: boolean
- description: Whether the availability status of agent is configured to go offline automatically when away.
+ description: Whether the agent is automatically marked offline when they are away.
example: true
diff --git a/swagger/definitions/request/inbox/channels/create_api_channel_payload.yml b/swagger/definitions/request/inbox/channels/create_api_channel_payload.yml
new file mode 100644
index 000000000..46e1201fa
--- /dev/null
+++ b/swagger/definitions/request/inbox/channels/create_api_channel_payload.yml
@@ -0,0 +1,22 @@
+type: object
+title: API channel
+required:
+ - type
+properties:
+ type:
+ type: string
+ enum: ['api']
+ example: api
+ webhook_url:
+ type: string
+ description: Webhook URL for API channel inbox callbacks
+ example: 'https://example.com/webhook'
+ hmac_mandatory:
+ type: boolean
+ description: Require HMAC verification for incoming API channel messages
+ example: false
+ additional_attributes:
+ type: object
+ description: Additional attributes stored on contacts created through the API channel
+ example:
+ source: mobile_app
diff --git a/swagger/definitions/request/inbox/channels/create_email_channel_payload.yml b/swagger/definitions/request/inbox/channels/create_email_channel_payload.yml
new file mode 100644
index 000000000..b15bf8383
--- /dev/null
+++ b/swagger/definitions/request/inbox/channels/create_email_channel_payload.yml
@@ -0,0 +1,90 @@
+type: object
+title: Email channel
+required:
+ - type
+ - email
+properties:
+ type:
+ type: string
+ enum: ['email']
+ example: email
+ email:
+ type: string
+ description: Email address for the inbox
+ example: support@example.com
+ imap_enabled:
+ type: boolean
+ description: Enable IMAP for inbound emails
+ example: true
+ imap_login:
+ type: string
+ description: IMAP login username
+ example: support@example.com
+ imap_password:
+ type: string
+ description: IMAP login password
+ example: your-imap-password
+ imap_address:
+ type: string
+ description: IMAP server address
+ example: imap.example.com
+ imap_port:
+ type: integer
+ description: IMAP server port
+ example: 993
+ imap_enable_ssl:
+ type: boolean
+ description: Enable SSL for IMAP
+ example: true
+ imap_authentication:
+ type: string
+ description: IMAP authentication method
+ example: plain
+ smtp_enabled:
+ type: boolean
+ description: Enable SMTP for outbound emails
+ example: true
+ smtp_login:
+ type: string
+ description: SMTP login username
+ example: support@example.com
+ smtp_password:
+ type: string
+ description: SMTP login password
+ example: your-smtp-password
+ smtp_address:
+ type: string
+ description: SMTP server address
+ example: smtp.example.com
+ smtp_port:
+ type: integer
+ description: SMTP server port
+ example: 587
+ smtp_domain:
+ type: string
+ description: SMTP HELO domain
+ example: example.com
+ smtp_enable_starttls_auto:
+ type: boolean
+ description: Automatically enable STARTTLS for SMTP
+ example: true
+ smtp_enable_ssl_tls:
+ type: boolean
+ description: Enable SSL/TLS for SMTP
+ example: false
+ smtp_openssl_verify_mode:
+ type: string
+ description: OpenSSL certificate verification mode for SMTP
+ example: none
+ smtp_authentication:
+ type: string
+ description: SMTP authentication method
+ example: login
+ provider:
+ type: string
+ description: Email provider
+ example: google
+ verified_for_sending:
+ type: boolean
+ description: Whether the inbox is verified for sending emails
+ example: false
diff --git a/swagger/definitions/request/inbox/channels/create_line_channel_payload.yml b/swagger/definitions/request/inbox/channels/create_line_channel_payload.yml
new file mode 100644
index 000000000..7a796983b
--- /dev/null
+++ b/swagger/definitions/request/inbox/channels/create_line_channel_payload.yml
@@ -0,0 +1,24 @@
+type: object
+title: LINE channel
+required:
+ - type
+ - line_channel_id
+ - line_channel_secret
+ - line_channel_token
+properties:
+ type:
+ type: string
+ enum: ['line']
+ example: line
+ line_channel_id:
+ type: string
+ description: LINE channel ID
+ example: '1234567890'
+ line_channel_secret:
+ type: string
+ description: LINE channel secret
+ example: line-channel-secret
+ line_channel_token:
+ type: string
+ description: LINE channel access token
+ example: line-channel-token
diff --git a/swagger/definitions/request/inbox/channels/create_sms_channel_payload.yml b/swagger/definitions/request/inbox/channels/create_sms_channel_payload.yml
new file mode 100644
index 000000000..32f78640b
--- /dev/null
+++ b/swagger/definitions/request/inbox/channels/create_sms_channel_payload.yml
@@ -0,0 +1,20 @@
+type: object
+title: SMS channel
+required:
+ - type
+ - phone_number
+properties:
+ type:
+ type: string
+ enum: ['sms']
+ example: sms
+ phone_number:
+ type: string
+ description: SMS phone number
+ example: '+15551234567'
+ provider_config:
+ type: object
+ description: Provider-specific SMS configuration
+ example:
+ account_id: your-account-id
+ application_id: your-application-id
diff --git a/swagger/definitions/request/inbox/channels/create_telegram_channel_payload.yml b/swagger/definitions/request/inbox/channels/create_telegram_channel_payload.yml
new file mode 100644
index 000000000..8f14fb377
--- /dev/null
+++ b/swagger/definitions/request/inbox/channels/create_telegram_channel_payload.yml
@@ -0,0 +1,14 @@
+type: object
+title: Telegram channel
+required:
+ - type
+ - bot_token
+properties:
+ type:
+ type: string
+ enum: ['telegram']
+ example: telegram
+ bot_token:
+ type: string
+ description: Telegram bot token
+ example: 123456789:telegram-bot-token
diff --git a/swagger/definitions/request/inbox/channels/create_web_widget_channel_payload.yml b/swagger/definitions/request/inbox/channels/create_web_widget_channel_payload.yml
new file mode 100644
index 000000000..b4e215a63
--- /dev/null
+++ b/swagger/definitions/request/inbox/channels/create_web_widget_channel_payload.yml
@@ -0,0 +1,66 @@
+type: object
+title: Website channel
+required:
+ - type
+ - website_url
+properties:
+ type:
+ type: string
+ enum: ['web_widget']
+ example: web_widget
+ website_url:
+ type: string
+ description: URL at which the widget will be loaded
+ example: 'https://example.com'
+ welcome_title:
+ type: string
+ description: Welcome title to be displayed on the widget
+ example: 'Welcome to our support'
+ welcome_tagline:
+ type: string
+ description: Welcome tagline to be displayed on the widget
+ example: 'We are here to help you'
+ widget_color:
+ type: string
+ description: A Hex-color string used to customize the widget
+ example: '#FF5733'
+ reply_time:
+ type: string
+ description: Expected reply time shown on the widget
+ enum: ['in_a_few_minutes', 'in_a_few_hours', 'in_a_day']
+ example: in_a_few_minutes
+ pre_chat_form_enabled:
+ type: boolean
+ description: Enable the pre-chat form before starting a conversation
+ example: false
+ pre_chat_form_options:
+ type: object
+ description: Pre-chat form configuration
+ example:
+ pre_chat_message: Share your queries or comments here.
+ pre_chat_fields:
+ - field_type: standard
+ label: Email Id
+ name: emailAddress
+ type: email
+ required: true
+ enabled: true
+ continuity_via_email:
+ type: boolean
+ description: Continue conversations over email when the contact leaves the website
+ example: true
+ hmac_mandatory:
+ type: boolean
+ description: Require HMAC verification for contacts using the widget
+ example: false
+ allowed_domains:
+ type: string
+ description: Comma-separated list of domains where the widget is allowed to load
+ example: example.com
+ selected_feature_flags:
+ type: array
+ description: Enabled widget feature flags
+ items:
+ type: string
+ enum: ['attachments', 'emoji_picker', 'end_conversation', 'use_inbox_avatar_for_bot', 'allow_mobile_webview']
+ example: ['attachments', 'emoji_picker', 'end_conversation']
diff --git a/swagger/definitions/request/inbox/channels/create_whatsapp_channel_payload.yml b/swagger/definitions/request/inbox/channels/create_whatsapp_channel_payload.yml
new file mode 100644
index 000000000..cb395646a
--- /dev/null
+++ b/swagger/definitions/request/inbox/channels/create_whatsapp_channel_payload.yml
@@ -0,0 +1,80 @@
+oneOf:
+ - type: object
+ title: WhatsApp Cloud channel
+ required:
+ - type
+ - phone_number
+ - provider
+ - provider_config
+ properties:
+ type:
+ type: string
+ enum: ['whatsapp']
+ example: whatsapp
+ phone_number:
+ type: string
+ description: WhatsApp phone number
+ example: '+15551234567'
+ provider:
+ type: string
+ description: WhatsApp provider
+ enum: ['whatsapp_cloud']
+ example: whatsapp_cloud
+ provider_config:
+ type: object
+ description: WhatsApp Cloud provider configuration
+ required:
+ - api_key
+ - phone_number_id
+ - business_account_id
+ properties:
+ api_key:
+ type: string
+ description: WhatsApp Cloud API key
+ example: your-api-key
+ phone_number_id:
+ type: string
+ description: Phone number ID for WhatsApp Cloud
+ example: your-phone-number-id
+ business_account_id:
+ type: string
+ description: Business account ID for WhatsApp Cloud
+ example: your-business-account-id
+ example:
+ api_key: your-api-key
+ phone_number_id: your-phone-number-id
+ business_account_id: your-business-account-id
+ - type: object
+ title: Legacy 360dialog WhatsApp channel
+ deprecated: true
+ required:
+ - type
+ - phone_number
+ - provider_config
+ properties:
+ type:
+ type: string
+ enum: ['whatsapp']
+ example: whatsapp
+ phone_number:
+ type: string
+ description: WhatsApp phone number
+ example: '+15551234567'
+ provider:
+ type: string
+ description: Legacy 360dialog provider. Omit this field or use `default` only for existing deprecated 360dialog setups.
+ enum: ['default']
+ deprecated: true
+ example: default
+ provider_config:
+ type: object
+ description: Legacy 360dialog provider configuration
+ required:
+ - api_key
+ properties:
+ api_key:
+ type: string
+ description: 360dialog API key
+ example: your-api-key
+ example:
+ api_key: your-api-key
diff --git a/swagger/definitions/request/inbox/channels/update_api_channel_payload.yml b/swagger/definitions/request/inbox/channels/update_api_channel_payload.yml
new file mode 100644
index 000000000..3f831e7f6
--- /dev/null
+++ b/swagger/definitions/request/inbox/channels/update_api_channel_payload.yml
@@ -0,0 +1,16 @@
+type: object
+title: API channel settings
+properties:
+ webhook_url:
+ type: string
+ description: Webhook URL for API channel inbox callbacks
+ example: 'https://example.com/webhook'
+ hmac_mandatory:
+ type: boolean
+ description: Require HMAC verification for incoming API channel messages
+ example: false
+ additional_attributes:
+ type: object
+ description: Additional attributes stored on contacts created through the API channel
+ example:
+ source: mobile_app
diff --git a/swagger/definitions/request/inbox/channels/update_email_channel_payload.yml b/swagger/definitions/request/inbox/channels/update_email_channel_payload.yml
new file mode 100644
index 000000000..48a5514f5
--- /dev/null
+++ b/swagger/definitions/request/inbox/channels/update_email_channel_payload.yml
@@ -0,0 +1,83 @@
+type: object
+title: Email channel settings
+properties:
+ email:
+ type: string
+ description: Email address for the inbox
+ example: support@example.com
+ imap_enabled:
+ type: boolean
+ description: Enable IMAP for inbound emails
+ example: true
+ imap_login:
+ type: string
+ description: IMAP login username
+ example: support@example.com
+ imap_password:
+ type: string
+ description: IMAP login password
+ example: your-imap-password
+ imap_address:
+ type: string
+ description: IMAP server address
+ example: imap.example.com
+ imap_port:
+ type: integer
+ description: IMAP server port
+ example: 993
+ imap_enable_ssl:
+ type: boolean
+ description: Enable SSL for IMAP
+ example: true
+ imap_authentication:
+ type: string
+ description: IMAP authentication method
+ example: plain
+ smtp_enabled:
+ type: boolean
+ description: Enable SMTP for outbound emails
+ example: true
+ smtp_login:
+ type: string
+ description: SMTP login username
+ example: support@example.com
+ smtp_password:
+ type: string
+ description: SMTP login password
+ example: your-smtp-password
+ smtp_address:
+ type: string
+ description: SMTP server address
+ example: smtp.example.com
+ smtp_port:
+ type: integer
+ description: SMTP server port
+ example: 587
+ smtp_domain:
+ type: string
+ description: SMTP HELO domain
+ example: example.com
+ smtp_enable_starttls_auto:
+ type: boolean
+ description: Automatically enable STARTTLS for SMTP
+ example: true
+ smtp_enable_ssl_tls:
+ type: boolean
+ description: Enable SSL/TLS for SMTP
+ example: false
+ smtp_openssl_verify_mode:
+ type: string
+ description: OpenSSL certificate verification mode for SMTP
+ example: none
+ smtp_authentication:
+ type: string
+ description: SMTP authentication method
+ example: login
+ provider:
+ type: string
+ description: Email provider
+ example: google
+ verified_for_sending:
+ type: boolean
+ description: Whether the inbox is verified for sending emails
+ example: false
diff --git a/swagger/definitions/request/inbox/channels/update_line_channel_payload.yml b/swagger/definitions/request/inbox/channels/update_line_channel_payload.yml
new file mode 100644
index 000000000..3220b6200
--- /dev/null
+++ b/swagger/definitions/request/inbox/channels/update_line_channel_payload.yml
@@ -0,0 +1,15 @@
+type: object
+title: LINE channel settings
+properties:
+ line_channel_id:
+ type: string
+ description: LINE channel ID
+ example: '1234567890'
+ line_channel_secret:
+ type: string
+ description: LINE channel secret
+ example: line-channel-secret
+ line_channel_token:
+ type: string
+ description: LINE channel access token
+ example: line-channel-token
diff --git a/swagger/definitions/request/inbox/channels/update_sms_channel_payload.yml b/swagger/definitions/request/inbox/channels/update_sms_channel_payload.yml
new file mode 100644
index 000000000..97af3e1c8
--- /dev/null
+++ b/swagger/definitions/request/inbox/channels/update_sms_channel_payload.yml
@@ -0,0 +1,15 @@
+type: object
+title: SMS channel settings
+properties:
+ phone_number:
+ type: string
+ description: SMS phone number
+ example: '+15551234567'
+ provider_config:
+ type: object
+ description: Provider-specific SMS configuration
+ example:
+ api_key: your-api-key
+ api_secret: your-api-secret
+ application_id: your-application-id
+ account_id: your-account-id
diff --git a/swagger/definitions/request/inbox/channels/update_telegram_channel_payload.yml b/swagger/definitions/request/inbox/channels/update_telegram_channel_payload.yml
new file mode 100644
index 000000000..46a0c1cb7
--- /dev/null
+++ b/swagger/definitions/request/inbox/channels/update_telegram_channel_payload.yml
@@ -0,0 +1,7 @@
+type: object
+title: Telegram channel settings
+properties:
+ bot_token:
+ type: string
+ description: Telegram bot token
+ example: 123456789:telegram-bot-token
diff --git a/swagger/definitions/request/inbox/channels/update_web_widget_channel_payload.yml b/swagger/definitions/request/inbox/channels/update_web_widget_channel_payload.yml
new file mode 100644
index 000000000..f928a1782
--- /dev/null
+++ b/swagger/definitions/request/inbox/channels/update_web_widget_channel_payload.yml
@@ -0,0 +1,59 @@
+type: object
+title: Website channel settings
+properties:
+ website_url:
+ type: string
+ description: URL at which the widget will be loaded
+ example: 'https://example.com'
+ welcome_title:
+ type: string
+ description: Welcome title to be displayed on the widget
+ example: 'Welcome to our support'
+ welcome_tagline:
+ type: string
+ description: Welcome tagline to be displayed on the widget
+ example: 'We are here to help you'
+ widget_color:
+ type: string
+ description: A Hex-color string used to customize the widget
+ example: '#FF5733'
+ reply_time:
+ type: string
+ description: Expected reply time shown on the widget
+ enum: ['in_a_few_minutes', 'in_a_few_hours', 'in_a_day']
+ example: in_a_few_minutes
+ pre_chat_form_enabled:
+ type: boolean
+ description: Enable the pre-chat form before starting a conversation
+ example: false
+ pre_chat_form_options:
+ type: object
+ description: Pre-chat form configuration
+ example:
+ pre_chat_message: Share your queries or comments here.
+ pre_chat_fields:
+ - field_type: standard
+ label: Email Id
+ name: emailAddress
+ type: email
+ required: true
+ enabled: true
+ continuity_via_email:
+ type: boolean
+ description: Continue conversations over email when the contact leaves the website
+ example: true
+ hmac_mandatory:
+ type: boolean
+ description: Require HMAC verification for contacts using the widget
+ example: false
+ allowed_domains:
+ type: string
+ description: Comma-separated list of domains where the widget is allowed to load
+ example: example.com
+ selected_feature_flags:
+ type: array
+ description: Enabled widget feature flags
+ items:
+ type: string
+ enum: ['attachments', 'emoji_picker', 'end_conversation', 'use_inbox_avatar_for_bot', 'allow_mobile_webview']
+ example: ['attachments', 'emoji_picker', 'end_conversation']
diff --git a/swagger/definitions/request/inbox/channels/update_whatsapp_channel_payload.yml b/swagger/definitions/request/inbox/channels/update_whatsapp_channel_payload.yml
new file mode 100644
index 000000000..d4ee202a9
--- /dev/null
+++ b/swagger/definitions/request/inbox/channels/update_whatsapp_channel_payload.yml
@@ -0,0 +1,32 @@
+type: object
+title: WhatsApp channel settings
+properties:
+ phone_number:
+ type: string
+ description: WhatsApp phone number
+ example: '+15551234567'
+ provider:
+ type: string
+ description: WhatsApp provider. `default` is supported only for existing deprecated 360dialog setups.
+ enum: ['whatsapp_cloud', 'default']
+ example: whatsapp_cloud
+ provider_config:
+ type: object
+ description: WhatsApp provider configuration. Cloud channels use `api_key`, `phone_number_id`, and `business_account_id`; legacy 360dialog channels use `api_key`.
+ properties:
+ api_key:
+ type: string
+ description: Provider API key
+ example: your-api-key
+ phone_number_id:
+ type: string
+ description: Phone number ID for WhatsApp Cloud
+ example: your-phone-number-id
+ business_account_id:
+ type: string
+ description: Business account ID for WhatsApp Cloud
+ example: your-business-account-id
+ example:
+ api_key: your-api-key
+ phone_number_id: your-phone-number-id
+ business_account_id: your-business-account-id
diff --git a/swagger/definitions/request/inbox/create_payload.yml b/swagger/definitions/request/inbox/create_payload.yml
index 59584054c..8f6dfd4d3 100644
--- a/swagger/definitions/request/inbox/create_payload.yml
+++ b/swagger/definitions/request/inbox/create_payload.yml
@@ -2,87 +2,129 @@ type: object
properties:
name:
type: string
- description: The name of the inbox
+ description: The name of the inbox.
example: 'Support'
avatar:
type: string
format: binary
- description: Image file for avatar
+ description: Image file for avatar.
greeting_enabled:
type: boolean
- description: Enable greeting message
+ description: Enable greeting message.
example: true
greeting_message:
type: string
- description: Greeting message to be displayed on the widget
+ description: Greeting message to send when greeting messages are enabled.
example: Hello, how can I help you?
enable_email_collect:
type: boolean
- description: Enable email collection
+ description: |
+ Enable email collection.
+
+ Available for: `Website`
example: true
csat_survey_enabled:
type: boolean
- description: Enable CSAT survey
+ description: Enable CSAT survey.
example: true
+ csat_config:
+ type: object
+ description: CSAT survey configuration.
+ properties:
+ display_type:
+ type: string
+ description: Display style for the CSAT survey.
+ enum: ['emoji', 'star']
+ example: emoji
+ message:
+ type: string
+ description: Message shown with the CSAT survey.
+ example: Please rate your conversation
+ button_text:
+ type: string
+ description: Text shown on the CSAT survey button.
+ example: Please rate us
+ language:
+ type: string
+ description: Language code for the CSAT survey.
+ example: en
+ survey_rules:
+ type: object
+ description: Rules that decide when to show the CSAT survey.
+ properties:
+ operator:
+ type: string
+ example: contains
+ values:
+ type: array
+ items:
+ type: string
+ example: ['billing']
enable_auto_assignment:
type: boolean
- description: Enable Auto Assignment
+ description: Enable Auto Assignment.
example: true
working_hours_enabled:
type: boolean
- description: Enable working hours
+ description: Enable working hours.
example: true
out_of_office_message:
type: string
- description: Out of office message to be displayed on the widget
+ description: Out of office message to send outside working hours.
example: We are currently out of office. Please leave a message and we will get back to you.
timezone:
type: string
- description: Timezone of the inbox
+ description: Timezone of the inbox.
example: 'America/New_York'
allow_messages_after_resolved:
type: boolean
- description: Allow messages after conversation is resolved
+ description: |
+ Allow messages after conversation is resolved.
+
+ Available for: `Website`
example: true
lock_to_single_conversation:
type: boolean
- description: Lock to single conversation
+ description: |
+ Lock contact messages to a single active conversation.
+
+ Available for: `API` `LINE` `Telegram` `WhatsApp` `SMS`
example: true
portal_id:
type: integer
- description: Id of the help center portal to attach to the inbox
+ description: Id of the help center portal to attach to the inbox.
example: 1
sender_name_type:
type: string
- description: Sender name type for the inbox
+ description: |
+ Sender name type for outbound email replies.
+
+ Available for: `Website` `Email`
enum: ['friendly', 'professional']
example: 'friendly'
business_name:
type: string
- description: Business name for the inbox
+ description: |
+ Business name for outbound email replies.
+
+ Available for: `Website` `Email`
example: 'My Business'
channel:
- type: object
- properties:
- type:
- type: string
- description: Type of the channel
- enum:
- ['web_widget', 'api', 'email', 'line', 'telegram', 'whatsapp', 'sms']
- example: web_widget
- website_url:
- type: string
- description: URL at which the widget will be loaded
- example: 'https://example.com'
- welcome_title:
- type: string
- description: Welcome title to be displayed on the widget
- example: 'Welcome to our support'
- welcome_tagline:
- type: string
- description: Welcome tagline to be displayed on the widget
- example: 'We are here to help you'
- widget_color:
- type: string
- description: A Hex-color string used to customize the widget
- example: '#FF5733'
+ oneOf:
+ - $ref: '#/components/schemas/inbox_create_web_widget_channel_payload'
+ - $ref: '#/components/schemas/inbox_create_api_channel_payload'
+ - $ref: '#/components/schemas/inbox_create_email_channel_payload'
+ - $ref: '#/components/schemas/inbox_create_line_channel_payload'
+ - $ref: '#/components/schemas/inbox_create_telegram_channel_payload'
+ - $ref: '#/components/schemas/inbox_create_whatsapp_channel_payload'
+ - $ref: '#/components/schemas/inbox_create_sms_channel_payload'
+ discriminator:
+ propertyName: type
+ mapping:
+ web_widget: '#/components/schemas/inbox_create_web_widget_channel_payload'
+ api: '#/components/schemas/inbox_create_api_channel_payload'
+ email: '#/components/schemas/inbox_create_email_channel_payload'
+ line: '#/components/schemas/inbox_create_line_channel_payload'
+ telegram: '#/components/schemas/inbox_create_telegram_channel_payload'
+ whatsapp: '#/components/schemas/inbox_create_whatsapp_channel_payload'
+ sms: '#/components/schemas/inbox_create_sms_channel_payload'
diff --git a/swagger/definitions/request/inbox/update_payload.yml b/swagger/definitions/request/inbox/update_payload.yml
index f625fc5b8..2490f2101 100644
--- a/swagger/definitions/request/inbox/update_payload.yml
+++ b/swagger/definitions/request/inbox/update_payload.yml
@@ -2,81 +2,119 @@ type: object
properties:
name:
type: string
- description: The name of the inbox
+ description: The name of the inbox.
example: 'Support'
avatar:
type: string
format: binary
- description: Image file for avatar
+ description: Image file for avatar.
greeting_enabled:
type: boolean
- description: Enable greeting message
+ description: Enable greeting message.
example: true
greeting_message:
type: string
- description: Greeting message to be displayed on the widget
+ description: Greeting message to send when greeting messages are enabled.
example: Hello, how can I help you?
enable_email_collect:
type: boolean
- description: Enable email collection
+ description: |
+ Enable email collection.
+
+ Available for: `Website`
example: true
csat_survey_enabled:
type: boolean
- description: Enable CSAT survey
+ description: Enable CSAT survey.
example: true
+ csat_config:
+ type: object
+ description: CSAT survey configuration.
+ properties:
+ display_type:
+ type: string
+ description: Display style for the CSAT survey.
+ enum: ['emoji', 'star']
+ example: emoji
+ message:
+ type: string
+ description: Message shown with the CSAT survey.
+ example: Please rate your conversation
+ button_text:
+ type: string
+ description: Text shown on the CSAT survey button.
+ example: Please rate us
+ language:
+ type: string
+ description: Language code for the CSAT survey.
+ example: en
+ survey_rules:
+ type: object
+ description: Rules that decide when to show the CSAT survey.
+ properties:
+ operator:
+ type: string
+ example: contains
+ values:
+ type: array
+ items:
+ type: string
+ example: ['billing']
enable_auto_assignment:
type: boolean
- description: Enable Auto Assignment
+ description: Enable Auto Assignment.
example: true
working_hours_enabled:
type: boolean
- description: Enable working hours
+ description: Enable working hours.
example: true
out_of_office_message:
type: string
- description: Out of office message to be displayed on the widget
+ description: Out of office message to send outside working hours.
example: We are currently out of office. Please leave a message and we will get back to you.
timezone:
type: string
- description: Timezone of the inbox
+ description: Timezone of the inbox.
example: 'America/New_York'
allow_messages_after_resolved:
type: boolean
- description: Allow messages after conversation is resolved
+ description: |
+ Allow messages after conversation is resolved.
+
+ Available for: `Website`
example: true
lock_to_single_conversation:
type: boolean
- description: Lock to single conversation
+ description: |
+ Lock contact messages to a single active conversation.
+
+ Available for: `API` `LINE` `Telegram` `WhatsApp` `SMS`
example: true
portal_id:
type: integer
- description: Id of the help center portal to attach to the inbox
+ description: Id of the help center portal to attach to the inbox.
example: 1
sender_name_type:
type: string
- description: Sender name type for the inbox
+ description: |
+ Sender name type for outbound email replies.
+
+ Available for: `Website` `Email`
enum: ['friendly', 'professional']
example: 'friendly'
business_name:
type: string
- description: Business name for the inbox
+ description: |
+ Business name for outbound email replies.
+
+ Available for: `Website` `Email`
example: 'My Business'
channel:
- type: object
- properties:
- website_url:
- type: string
- description: URL at which the widget will be loaded
- example: 'https://example.com'
- welcome_title:
- type: string
- description: Welcome title to be displayed on the widget
- example: 'Welcome to our support'
- welcome_tagline:
- type: string
- description: Welcome tagline to be displayed on the widget
- example: 'We are here to help you'
- widget_color:
- type: string
- description: A Hex-color string used to customize the widget
- example: '#FF5733'
+ anyOf:
+ - $ref: '#/components/schemas/inbox_update_web_widget_channel_payload'
+ - $ref: '#/components/schemas/inbox_update_api_channel_payload'
+ - $ref: '#/components/schemas/inbox_update_email_channel_payload'
+ - $ref: '#/components/schemas/inbox_update_line_channel_payload'
+ - $ref: '#/components/schemas/inbox_update_telegram_channel_payload'
+ - $ref: '#/components/schemas/inbox_update_whatsapp_channel_payload'
+ - $ref: '#/components/schemas/inbox_update_sms_channel_payload'
diff --git a/swagger/definitions/request/webhooks/create_update_payload.yml b/swagger/definitions/request/webhooks/create_update_payload.yml
index 485d532e4..d3ad3560c 100644
--- a/swagger/definitions/request/webhooks/create_update_payload.yml
+++ b/swagger/definitions/request/webhooks/create_update_payload.yml
@@ -21,6 +21,8 @@ properties:
'contact_created',
'contact_updated',
'webwidget_triggered',
+ 'conversation_typing_on',
+ 'conversation_typing_off',
]
description: The events you want to subscribe to.
example:
diff --git a/swagger/definitions/resource/agent.yml b/swagger/definitions/resource/agent.yml
index 1d7b2b4c3..cabd1ee27 100644
--- a/swagger/definitions/resource/agent.yml
+++ b/swagger/definitions/resource/agent.yml
@@ -6,11 +6,15 @@ properties:
type: integer
availability_status:
type: string
- enum: ['available', 'busy', 'offline']
- description: The availability status of the agent computed by Chatwoot.
+ enum: ['online', 'busy', 'offline']
+ readOnly: true
+ description: >-
+ The effective availability status of the agent, derived from the configured availability,
+ auto-offline setting, and current presence. To update an agent's configured availability,
+ use the availability field in create or update requests.
auto_offline:
type: boolean
- description: Whether the availability status of agent is configured to go offline automatically when away.
+ description: Whether the agent is automatically marked offline when they are away.
confirmed:
type: boolean
description: Whether the agent has confirmed their email address.
diff --git a/swagger/definitions/resource/webhook.yml b/swagger/definitions/resource/webhook.yml
index da5cb112a..a0184afb9 100644
--- a/swagger/definitions/resource/webhook.yml
+++ b/swagger/definitions/resource/webhook.yml
@@ -21,9 +21,15 @@ properties:
"contact_updated",
"message_created",
"message_updated",
- "webwidget_triggered"
+ "webwidget_triggered",
+ "conversation_typing_on",
+ "conversation_typing_off"
]
description: The list of subscribed events
+ secret:
+ type: string
+ nullable: true
+ description: Secret used to sign webhook requests. Signed webhook deliveries include `X-Chatwoot-Timestamp` and `X-Chatwoot-Signature`; the signature is `sha256=` followed by the HMAC-SHA256 of `{timestamp}.{raw_request_body}` using this secret. Deliveries also include `X-Chatwoot-Delivery` when a delivery id is available.
account_id:
type: number
description: The id of the account which the webhook object belongs to
diff --git a/swagger/paths/application/audit_logs/index.yml b/swagger/paths/application/audit_logs/index.yml
index 8fcd22256..df7966c57 100644
--- a/swagger/paths/application/audit_logs/index.yml
+++ b/swagger/paths/application/audit_logs/index.yml
@@ -24,7 +24,7 @@ responses:
per_page:
type: integer
description: Number of items per page
- example: 15
+ example: 25
total_entries:
type: integer
description: Total number of audit log entries
@@ -49,4 +49,4 @@ responses:
content:
application/json:
schema:
- $ref: '#/components/schemas/bad_request_error'
\ No newline at end of file
+ $ref: '#/components/schemas/bad_request_error'
diff --git a/swagger/paths/application/conversation/messages/create.yml b/swagger/paths/application/conversation/messages/create.yml
index 1b8272585..09dece97f 100644
--- a/swagger/paths/application/conversation/messages/create.yml
+++ b/swagger/paths/application/conversation/messages/create.yml
@@ -4,6 +4,23 @@ operationId: create-a-new-message-in-a-conversation
summary: Create New Message
description: |
Create a new message in the conversation.
+
+ Use `application/json` for text messages and `multipart/form-data` when the
+ message includes file attachments.
+
+ ### Multipart attachment request
+
+ Send files with the `attachments[]` form field. `curl -F` sets the
+ `multipart/form-data` content type and boundary automatically.
+
+ ```bash
+ curl -X POST "https://app.chatwoot.com/api/v1/accounts/{account_id}/conversations/{conversation_id}/messages" \
+ -H "api_access_token: " \
+ -F "content=Here is the screenshot" \
+ -F "message_type=outgoing" \
+ -F "private=false" \
+ -F "attachments[]=@/path/to/screenshot.png"
+ ```
## WhatsApp Template Messages
@@ -62,6 +79,58 @@ requestBody:
application/json:
schema:
$ref: '#/components/schemas/conversation_message_create_payload'
+ multipart/form-data:
+ schema:
+ type: object
+ description: Form data payload for creating a message with file attachments.
+ example:
+ content: Here is the screenshot
+ message_type: outgoing
+ private: false
+ 'attachments[]':
+ - screenshot.png
+ properties:
+ content:
+ type: string
+ description: The content of the message
+ example: Here is the screenshot
+ message_type:
+ type: string
+ enum: ['outgoing', 'incoming']
+ description: The type of the message
+ example: outgoing
+ private:
+ type: boolean
+ description: Flag to identify if it is a private note
+ example: false
+ content_type:
+ type: string
+ enum: ['text', 'input_email', 'cards', 'input_select', 'form', 'article']
+ description: Content type of the message
+ example: text
+ content_attributes:
+ type: object
+ description: Attributes based on the content type
+ example: {}
+ 'attachments[]':
+ type: array
+ description: Files to attach to the message
+ items:
+ type: string
+ format: binary
+ encoding:
+ 'attachments[]':
+ style: form
+ explode: true
+ examples:
+ attachment_message:
+ summary: Message with an attachment
+ value:
+ content: Here is the screenshot
+ message_type: outgoing
+ private: false
+ 'attachments[]':
+ - screenshot.png
responses:
'200':
description: Success
diff --git a/swagger/paths/application/inboxes/create.yml b/swagger/paths/application/inboxes/create.yml
deleted file mode 100644
index 6f88b0d4a..000000000
--- a/swagger/paths/application/inboxes/create.yml
+++ /dev/null
@@ -1,35 +0,0 @@
-post:
- tags:
- - Inboxes
- operationId: inboxCreation
- summary: Create an inbox
- description: You can create more than one website inbox in each account
- security:
- - userApiKey: []
- parameters:
- - $ref: '#/components/parameters/account_id'
- requestBody:
- required: true
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/inbox_create_payload'
- responses:
- '200':
- description: Success
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/inbox'
- '404':
- description: Inbox not found
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/bad_request_error'
- '403':
- description: Access denied
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/bad_request_error'
diff --git a/swagger/paths/application/inboxes/index.yml b/swagger/paths/application/inboxes/index.yml
index 3d9f4b4e0..6e41cb7f8 100644
--- a/swagger/paths/application/inboxes/index.yml
+++ b/swagger/paths/application/inboxes/index.yml
@@ -49,6 +49,121 @@ post:
application/json:
schema:
$ref: '#/components/schemas/inbox_create_payload'
+ examples:
+ web_widget:
+ summary: Website inbox
+ value:
+ name: Support
+ greeting_enabled: true
+ greeting_message: Hello, how can I help you?
+ enable_email_collect: true
+ enable_auto_assignment: true
+ working_hours_enabled: true
+ timezone: America/New_York
+ allow_messages_after_resolved: true
+ channel:
+ type: web_widget
+ website_url: https://example.com
+ welcome_title: Welcome to our support
+ welcome_tagline: We are here to help you
+ widget_color: '#FF5733'
+ reply_time: in_a_few_minutes
+ pre_chat_form_enabled: false
+ continuity_via_email: true
+ hmac_mandatory: false
+ selected_feature_flags:
+ - attachments
+ - emoji_picker
+ - end_conversation
+ api:
+ summary: API channel
+ value:
+ name: API Inbox
+ greeting_enabled: true
+ greeting_message: Hello, how can I help you?
+ enable_auto_assignment: true
+ working_hours_enabled: true
+ timezone: America/New_York
+ channel:
+ type: api
+ webhook_url: https://example.com/webhook
+ hmac_mandatory: false
+ additional_attributes:
+ source: mobile_app
+ email:
+ summary: Email channel
+ value:
+ name: Email Inbox
+ greeting_enabled: true
+ greeting_message: Hello, how can I help you?
+ enable_auto_assignment: true
+ working_hours_enabled: true
+ timezone: America/New_York
+ channel:
+ type: email
+ email: support@example.com
+ imap_enabled: false
+ smtp_enabled: false
+ line:
+ summary: LINE channel
+ value:
+ name: LINE Inbox
+ greeting_enabled: true
+ greeting_message: Hello, how can I help you?
+ enable_auto_assignment: true
+ working_hours_enabled: true
+ timezone: America/New_York
+ channel:
+ type: line
+ line_channel_id: '1234567890'
+ line_channel_secret: line-channel-secret
+ line_channel_token: line-channel-token
+ telegram:
+ summary: Telegram channel
+ value:
+ name: Telegram Inbox
+ greeting_enabled: true
+ greeting_message: Hello, how can I help you?
+ enable_auto_assignment: true
+ working_hours_enabled: true
+ timezone: America/New_York
+ channel:
+ type: telegram
+ bot_token: 123456789:telegram-bot-token
+ whatsapp:
+ summary: WhatsApp channel
+ value:
+ name: WhatsApp Inbox
+ greeting_enabled: true
+ greeting_message: Hello, how can I help you?
+ enable_auto_assignment: true
+ working_hours_enabled: true
+ timezone: America/New_York
+ channel:
+ type: whatsapp
+ phone_number: '+15551234567'
+ provider: whatsapp_cloud
+ provider_config:
+ api_key: your-api-key
+ phone_number_id: your-phone-number-id
+ business_account_id: your-business-account-id
+ sms:
+ summary: SMS channel
+ value:
+ name: SMS Inbox
+ greeting_enabled: true
+ greeting_message: Hello, how can I help you?
+ enable_auto_assignment: true
+ working_hours_enabled: true
+ timezone: America/New_York
+ channel:
+ type: sms
+ phone_number: '+15551234567'
+ provider_config:
+ api_key: your-api-key
+ api_secret: your-api-secret
+ application_id: your-application-id
+ account_id: your-account-id
responses:
'200':
description: Success
diff --git a/swagger/paths/application/inboxes/update.yml b/swagger/paths/application/inboxes/update.yml
index 912335861..00f4fdff3 100644
--- a/swagger/paths/application/inboxes/update.yml
+++ b/swagger/paths/application/inboxes/update.yml
@@ -55,6 +55,114 @@ patch:
application/json:
schema:
$ref: '#/components/schemas/inbox_update_payload'
+ examples:
+ web_widget:
+ summary: Website inbox settings
+ value:
+ name: Support
+ greeting_enabled: true
+ greeting_message: Hello, how can I help you?
+ enable_email_collect: true
+ enable_auto_assignment: true
+ working_hours_enabled: true
+ timezone: America/New_York
+ allow_messages_after_resolved: true
+ channel:
+ website_url: https://example.com
+ welcome_title: Welcome to our support
+ welcome_tagline: We are here to help you
+ widget_color: '#FF5733'
+ reply_time: in_a_few_minutes
+ pre_chat_form_enabled: false
+ continuity_via_email: true
+ hmac_mandatory: false
+ selected_feature_flags:
+ - attachments
+ - emoji_picker
+ - end_conversation
+ api:
+ summary: API channel settings
+ value:
+ name: API Inbox
+ greeting_enabled: true
+ greeting_message: Hello, how can I help you?
+ enable_auto_assignment: true
+ working_hours_enabled: true
+ timezone: America/New_York
+ channel:
+ webhook_url: https://example.com/webhook
+ hmac_mandatory: false
+ additional_attributes:
+ source: mobile_app
+ email:
+ summary: Email channel settings
+ value:
+ name: Email Inbox
+ greeting_enabled: true
+ greeting_message: Hello, how can I help you?
+ enable_auto_assignment: true
+ working_hours_enabled: true
+ timezone: America/New_York
+ channel:
+ email: support@example.com
+ imap_enabled: false
+ smtp_enabled: false
+ line:
+ summary: LINE channel settings
+ value:
+ name: LINE Inbox
+ greeting_enabled: true
+ greeting_message: Hello, how can I help you?
+ enable_auto_assignment: true
+ working_hours_enabled: true
+ timezone: America/New_York
+ channel:
+ line_channel_id: '1234567890'
+ line_channel_secret: line-channel-secret
+ line_channel_token: line-channel-token
+ telegram:
+ summary: Telegram channel settings
+ value:
+ name: Telegram Inbox
+ greeting_enabled: true
+ greeting_message: Hello, how can I help you?
+ enable_auto_assignment: true
+ working_hours_enabled: true
+ timezone: America/New_York
+ channel:
+ bot_token: 123456789:telegram-bot-token
+ whatsapp:
+ summary: WhatsApp channel settings
+ value:
+ name: WhatsApp Inbox
+ greeting_enabled: true
+ greeting_message: Hello, how can I help you?
+ enable_auto_assignment: true
+ working_hours_enabled: true
+ timezone: America/New_York
+ channel:
+ phone_number: '+15551234567'
+ provider: whatsapp_cloud
+ provider_config:
+ api_key: your-api-key
+ phone_number_id: your-phone-number-id
+ business_account_id: your-business-account-id
+ sms:
+ summary: SMS channel settings
+ value:
+ name: SMS Inbox
+ greeting_enabled: true
+ greeting_message: Hello, how can I help you?
+ enable_auto_assignment: true
+ working_hours_enabled: true
+ timezone: America/New_York
+ channel:
+ phone_number: '+15551234567'
+ provider_config:
+ api_key: your-api-key
+ api_secret: your-api-secret
+ application_id: your-application-id
+ account_id: your-account-id
responses:
'200':
description: Success
diff --git a/swagger/swagger.json b/swagger/swagger.json
index 94d1f04d3..b21742b4e 100644
--- a/swagger/swagger.json
+++ b/swagger/swagger.json
@@ -1649,7 +1649,7 @@
"per_page": {
"type": "integer",
"description": "Number of items per page",
- "example": 15
+ "example": 25
},
"total_entries": {
"type": "integer",
@@ -5457,6 +5457,147 @@
"application/json": {
"schema": {
"$ref": "#/components/schemas/inbox_create_payload"
+ },
+ "examples": {
+ "web_widget": {
+ "summary": "Website inbox",
+ "value": {
+ "name": "Support",
+ "greeting_enabled": true,
+ "greeting_message": "Hello, how can I help you?",
+ "enable_email_collect": true,
+ "enable_auto_assignment": true,
+ "working_hours_enabled": true,
+ "timezone": "America/New_York",
+ "allow_messages_after_resolved": true,
+ "channel": {
+ "type": "web_widget",
+ "website_url": "https://example.com",
+ "welcome_title": "Welcome to our support",
+ "welcome_tagline": "We are here to help you",
+ "widget_color": "#FF5733",
+ "reply_time": "in_a_few_minutes",
+ "pre_chat_form_enabled": false,
+ "continuity_via_email": true,
+ "hmac_mandatory": false,
+ "selected_feature_flags": [
+ "attachments",
+ "emoji_picker",
+ "end_conversation"
+ ]
+ }
+ }
+ },
+ "api": {
+ "summary": "API channel",
+ "value": {
+ "name": "API Inbox",
+ "greeting_enabled": true,
+ "greeting_message": "Hello, how can I help you?",
+ "enable_auto_assignment": true,
+ "working_hours_enabled": true,
+ "timezone": "America/New_York",
+ "channel": {
+ "type": "api",
+ "webhook_url": "https://example.com/webhook",
+ "hmac_mandatory": false,
+ "additional_attributes": {
+ "source": "mobile_app"
+ }
+ }
+ }
+ },
+ "email": {
+ "summary": "Email channel",
+ "value": {
+ "name": "Email Inbox",
+ "greeting_enabled": true,
+ "greeting_message": "Hello, how can I help you?",
+ "enable_auto_assignment": true,
+ "working_hours_enabled": true,
+ "timezone": "America/New_York",
+ "channel": {
+ "type": "email",
+ "email": "support@example.com",
+ "imap_enabled": false,
+ "smtp_enabled": false
+ }
+ }
+ },
+ "line": {
+ "summary": "LINE channel",
+ "value": {
+ "name": "LINE Inbox",
+ "greeting_enabled": true,
+ "greeting_message": "Hello, how can I help you?",
+ "enable_auto_assignment": true,
+ "working_hours_enabled": true,
+ "timezone": "America/New_York",
+ "channel": {
+ "type": "line",
+ "line_channel_id": "1234567890",
+ "line_channel_secret": "line-channel-secret",
+ "line_channel_token": "line-channel-token"
+ }
+ }
+ },
+ "telegram": {
+ "summary": "Telegram channel",
+ "value": {
+ "name": "Telegram Inbox",
+ "greeting_enabled": true,
+ "greeting_message": "Hello, how can I help you?",
+ "enable_auto_assignment": true,
+ "working_hours_enabled": true,
+ "timezone": "America/New_York",
+ "channel": {
+ "type": "telegram",
+ "bot_token": "123456789:telegram-bot-token"
+ }
+ }
+ },
+ "whatsapp": {
+ "summary": "WhatsApp channel",
+ "value": {
+ "name": "WhatsApp Inbox",
+ "greeting_enabled": true,
+ "greeting_message": "Hello, how can I help you?",
+ "enable_auto_assignment": true,
+ "working_hours_enabled": true,
+ "timezone": "America/New_York",
+ "channel": {
+ "type": "whatsapp",
+ "phone_number": "+15551234567",
+ "provider": "whatsapp_cloud",
+ "provider_config": {
+ "api_key": "your-api-key",
+ "phone_number_id": "your-phone-number-id",
+ "business_account_id": "your-business-account-id"
+ }
+ }
+ }
+ },
+ "sms": {
+ "summary": "SMS channel",
+ "value": {
+ "name": "SMS Inbox",
+ "greeting_enabled": true,
+ "greeting_message": "Hello, how can I help you?",
+ "enable_auto_assignment": true,
+ "working_hours_enabled": true,
+ "timezone": "America/New_York",
+ "channel": {
+ "type": "sms",
+ "phone_number": "+15551234567",
+ "provider_config": {
+ "api_key": "your-api-key",
+ "api_secret": "your-api-secret",
+ "application_id": "your-application-id",
+ "account_id": "your-account-id"
+ }
+ }
+ }
+ }
}
}
}
@@ -5587,6 +5728,140 @@
"application/json": {
"schema": {
"$ref": "#/components/schemas/inbox_update_payload"
+ },
+ "examples": {
+ "web_widget": {
+ "summary": "Website inbox settings",
+ "value": {
+ "name": "Support",
+ "greeting_enabled": true,
+ "greeting_message": "Hello, how can I help you?",
+ "enable_email_collect": true,
+ "enable_auto_assignment": true,
+ "working_hours_enabled": true,
+ "timezone": "America/New_York",
+ "allow_messages_after_resolved": true,
+ "channel": {
+ "website_url": "https://example.com",
+ "welcome_title": "Welcome to our support",
+ "welcome_tagline": "We are here to help you",
+ "widget_color": "#FF5733",
+ "reply_time": "in_a_few_minutes",
+ "pre_chat_form_enabled": false,
+ "continuity_via_email": true,
+ "hmac_mandatory": false,
+ "selected_feature_flags": [
+ "attachments",
+ "emoji_picker",
+ "end_conversation"
+ ]
+ }
+ }
+ },
+ "api": {
+ "summary": "API channel settings",
+ "value": {
+ "name": "API Inbox",
+ "greeting_enabled": true,
+ "greeting_message": "Hello, how can I help you?",
+ "enable_auto_assignment": true,
+ "working_hours_enabled": true,
+ "timezone": "America/New_York",
+ "channel": {
+ "webhook_url": "https://example.com/webhook",
+ "hmac_mandatory": false,
+ "additional_attributes": {
+ "source": "mobile_app"
+ }
+ }
+ }
+ },
+ "email": {
+ "summary": "Email channel settings",
+ "value": {
+ "name": "Email Inbox",
+ "greeting_enabled": true,
+ "greeting_message": "Hello, how can I help you?",
+ "enable_auto_assignment": true,
+ "working_hours_enabled": true,
+ "timezone": "America/New_York",
+ "channel": {
+ "email": "support@example.com",
+ "imap_enabled": false,
+ "smtp_enabled": false
+ }
+ }
+ },
+ "line": {
+ "summary": "LINE channel settings",
+ "value": {
+ "name": "LINE Inbox",
+ "greeting_enabled": true,
+ "greeting_message": "Hello, how can I help you?",
+ "enable_auto_assignment": true,
+ "working_hours_enabled": true,
+ "timezone": "America/New_York",
+ "channel": {
+ "line_channel_id": "1234567890",
+ "line_channel_secret": "line-channel-secret",
+ "line_channel_token": "line-channel-token"
+ }
+ }
+ },
+ "telegram": {
+ "summary": "Telegram channel settings",
+ "value": {
+ "name": "Telegram Inbox",
+ "greeting_enabled": true,
+ "greeting_message": "Hello, how can I help you?",
+ "enable_auto_assignment": true,
+ "working_hours_enabled": true,
+ "timezone": "America/New_York",
+ "channel": {
+ "bot_token": "123456789:telegram-bot-token"
+ }
+ }
+ },
+ "whatsapp": {
+ "summary": "WhatsApp channel settings",
+ "value": {
+ "name": "WhatsApp Inbox",
+ "greeting_enabled": true,
+ "greeting_message": "Hello, how can I help you?",
+ "enable_auto_assignment": true,
+ "working_hours_enabled": true,
+ "timezone": "America/New_York",
+ "channel": {
+ "phone_number": "+15551234567",
+ "provider": "whatsapp_cloud",
+ "provider_config": {
+ "api_key": "your-api-key",
+ "phone_number_id": "your-phone-number-id",
+ "business_account_id": "your-business-account-id"
+ }
+ }
+ }
+ },
+ "sms": {
+ "summary": "SMS channel settings",
+ "value": {
+ "name": "SMS Inbox",
+ "greeting_enabled": true,
+ "greeting_message": "Hello, how can I help you?",
+ "enable_auto_assignment": true,
+ "working_hours_enabled": true,
+ "timezone": "America/New_York",
+ "channel": {
+ "phone_number": "+15551234567",
+ "provider_config": {
+ "api_key": "your-api-key",
+ "api_secret": "your-api-secret",
+ "application_id": "your-application-id",
+ "account_id": "your-account-id"
+ }
+ }
+ }
+ }
}
}
}
@@ -6462,7 +6737,7 @@
],
"operationId": "create-a-new-message-in-a-conversation",
"summary": "Create New Message",
- "description": "Create a new message in the conversation.\n\n## WhatsApp Template Messages\n\nFor WhatsApp channels, you can send structured template messages using the `template_params` field. \nTemplates must be pre-approved in WhatsApp Business Manager.\n\n### Example Templates\n\n**Text with Image Header:**\n```json\n{\n \"content\": \"Hi your order 121212 is confirmed. Please wait for further updates\",\n \"template_params\": {\n \"name\": \"order_confirmation\",\n \"category\": \"MARKETING\",\n \"language\": \"en\",\n \"processed_params\": {\n \"body\": {\n \"1\": \"121212\"\n },\n \"header\": {\n \"media_url\": \"https://picsum.photos/200/300\",\n \"media_type\": \"image\"\n }\n }\n }\n}\n```\n\n**Text with Copy Code Button:**\n```json\n{\n \"content\": \"Special offer! Get 30% off your next purchase. Use the code below\",\n \"template_params\": {\n \"name\": \"discount_coupon\",\n \"category\": \"MARKETING\",\n \"language\": \"en\",\n \"processed_params\": {\n \"body\": {\n \"discount_percentage\": \"30\"\n },\n \"buttons\": [{\n \"type\": \"copy_code\",\n \"parameter\": \"SAVE20\"\n }]\n }\n }\n}\n```\n",
+ "description": "Create a new message in the conversation.\n\nUse `application/json` for text messages and `multipart/form-data` when the\nmessage includes file attachments.\n\n### Multipart attachment request\n\nSend files with the `attachments[]` form field. `curl -F` sets the\n`multipart/form-data` content type and boundary automatically.\n\n```bash\ncurl -X POST \"https://app.chatwoot.com/api/v1/accounts/{account_id}/conversations/{conversation_id}/messages\" \\\n -H \"api_access_token: \" \\\n -F \"content=Here is the screenshot\" \\\n -F \"message_type=outgoing\" \\\n -F \"private=false\" \\\n -F \"attachments[]=@/path/to/screenshot.png\"\n```\n\n## WhatsApp Template Messages\n\nFor WhatsApp channels, you can send structured template messages using the `template_params` field. \nTemplates must be pre-approved in WhatsApp Business Manager.\n\n### Example Templates\n\n**Text with Image Header:**\n```json\n{\n \"content\": \"Hi your order 121212 is confirmed. Please wait for further updates\",\n \"template_params\": {\n \"name\": \"order_confirmation\",\n \"category\": \"MARKETING\",\n \"language\": \"en\",\n \"processed_params\": {\n \"body\": {\n \"1\": \"121212\"\n },\n \"header\": {\n \"media_url\": \"https://picsum.photos/200/300\",\n \"media_type\": \"image\"\n }\n }\n }\n}\n```\n\n**Text with Copy Code Button:**\n```json\n{\n \"content\": \"Special offer! Get 30% off your next purchase. Use the code below\",\n \"template_params\": {\n \"name\": \"discount_coupon\",\n \"category\": \"MARKETING\",\n \"language\": \"en\",\n \"processed_params\": {\n \"body\": {\n \"discount_percentage\": \"30\"\n },\n \"buttons\": [{\n \"type\": \"copy_code\",\n \"parameter\": \"SAVE20\"\n }]\n }\n }\n}\n```\n",
"security": [
{
"userApiKey": []
@@ -6478,6 +6753,86 @@
"schema": {
"$ref": "#/components/schemas/conversation_message_create_payload"
}
+ },
+ "multipart/form-data": {
+ "schema": {
+ "type": "object",
+ "description": "Form data payload for creating a message with file attachments.",
+ "example": {
+ "content": "Here is the screenshot",
+ "message_type": "outgoing",
+ "private": false,
+ "attachments[]": [
+ "screenshot.png"
+ ]
+ },
+ "properties": {
+ "content": {
+ "type": "string",
+ "description": "The content of the message",
+ "example": "Here is the screenshot"
+ },
+ "message_type": {
+ "type": "string",
+ "enum": [
+ "outgoing",
+ "incoming"
+ ],
+ "description": "The type of the message",
+ "example": "outgoing"
+ },
+ "private": {
+ "type": "boolean",
+ "description": "Flag to identify if it is a private note",
+ "example": false
+ },
+ "content_type": {
+ "type": "string",
+ "enum": [
+ "text",
+ "input_email",
+ "cards",
+ "input_select",
+ "form",
+ "article"
+ ],
+ "description": "Content type of the message",
+ "example": "text"
+ },
+ "content_attributes": {
+ "type": "object",
+ "description": "Attributes based on the content type",
+ "example": {}
+ },
+ "attachments[]": {
+ "type": "array",
+ "description": "Files to attach to the message",
+ "items": {
+ "type": "string",
+ "format": "binary"
+ }
+ }
+ }
+ },
+ "encoding": {
+ "attachments[]": {
+ "style": "form",
+ "explode": true
+ }
+ },
+ "examples": {
+ "attachment_message": {
+ "summary": "Message with an attachment",
+ "value": {
+ "content": "Here is the screenshot",
+ "message_type": "outgoing",
+ "private": false,
+ "attachments[]": [
+ "screenshot.png"
+ ]
+ }
+ }
+ }
}
}
},
@@ -9892,15 +10247,16 @@
"availability_status": {
"type": "string",
"enum": [
- "available",
+ "online",
"busy",
"offline"
],
- "description": "The availability status of the agent computed by Chatwoot."
+ "readOnly": true,
+ "description": "The effective availability status of the agent, derived from the configured availability, auto-offline setting, and current presence. To update an agent's configured availability, use the availability field in create or update requests."
},
"auto_offline": {
"type": "boolean",
- "description": "Whether the availability status of agent is configured to go offline automatically when away."
+ "description": "Whether the agent is automatically marked offline when they are away."
},
"confirmed": {
"type": "boolean",
@@ -10315,11 +10671,18 @@
"contact_updated",
"message_created",
"message_updated",
- "webwidget_triggered"
+ "webwidget_triggered",
+ "conversation_typing_on",
+ "conversation_typing_off"
]
},
"description": "The list of subscribed events"
},
+ "secret": {
+ "type": "string",
+ "nullable": true,
+ "description": "Secret used to sign webhook requests. Signed webhook deliveries include `X-Chatwoot-Timestamp` and `X-Chatwoot-Signature`; the signature is `sha256=` followed by the HMAC-SHA256 of `{timestamp}.{raw_request_body}` using this secret. Deliveries also include `X-Chatwoot-Delivery` when a delivery id is available."
+ },
"account_id": {
"type": "number",
"description": "The id of the account which the webhook object belongs to"
@@ -11595,19 +11958,19 @@
"description": "Whether its administrator or agent",
"example": "agent"
},
- "availability_status": {
+ "availability": {
"type": "string",
"enum": [
- "available",
+ "online",
"busy",
"offline"
],
- "description": "The availability setting of the agent.",
- "example": "available"
+ "description": "The configured availability of the agent.",
+ "example": "online"
},
"auto_offline": {
"type": "boolean",
- "description": "Whether the availability status of agent is configured to go offline automatically when away.",
+ "description": "Whether the agent is automatically marked offline when they are away.",
"example": true
}
}
@@ -11627,19 +11990,19 @@
"description": "Whether its administrator or agent",
"example": "agent"
},
- "availability_status": {
+ "availability": {
"type": "string",
"enum": [
- "available",
+ "online",
"busy",
"offline"
],
- "description": "The availability status of the agent.",
- "example": "available"
+ "description": "The configured availability of the agent.",
+ "example": "online"
},
"auto_offline": {
"type": "boolean",
- "description": "Whether the availability status of agent is configured to go offline automatically when away.",
+ "description": "Whether the agent is automatically marked offline when they are away.",
"example": true
}
}
@@ -12018,72 +12381,121 @@
"properties": {
"name": {
"type": "string",
- "description": "The name of the inbox",
+ "description": "The name of the inbox.",
"example": "Support"
},
"avatar": {
"type": "string",
"format": "binary",
- "description": "Image file for avatar"
+ "description": "Image file for avatar."
},
"greeting_enabled": {
"type": "boolean",
- "description": "Enable greeting message",
+ "description": "Enable greeting message.",
"example": true
},
"greeting_message": {
"type": "string",
- "description": "Greeting message to be displayed on the widget",
+ "description": "Greeting message to send when greeting messages are enabled.",
"example": "Hello, how can I help you?"
},
"enable_email_collect": {
"type": "boolean",
- "description": "Enable email collection",
+ "description": "Enable email collection.\n\nAvailable for: `Website`\n",
"example": true
},
"csat_survey_enabled": {
"type": "boolean",
- "description": "Enable CSAT survey",
+ "description": "Enable CSAT survey.",
"example": true
},
+ "csat_config": {
+ "type": "object",
+ "description": "CSAT survey configuration.",
+ "properties": {
+ "display_type": {
+ "type": "string",
+ "description": "Display style for the CSAT survey.",
+ "enum": [
+ "emoji",
+ "star"
+ ],
+ "example": "emoji"
+ },
+ "message": {
+ "type": "string",
+ "description": "Message shown with the CSAT survey.",
+ "example": "Please rate your conversation"
+ },
+ "button_text": {
+ "type": "string",
+ "description": "Text shown on the CSAT survey button.",
+ "example": "Please rate us"
+ },
+ "language": {
+ "type": "string",
+ "description": "Language code for the CSAT survey.",
+ "example": "en"
+ },
+ "survey_rules": {
+ "type": "object",
+ "description": "Rules that decide when to show the CSAT survey.",
+ "properties": {
+ "operator": {
+ "type": "string",
+ "example": "contains"
+ },
+ "values": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "example": [
+ "billing"
+ ]
+ }
+ }
+ }
+ }
+ },
"enable_auto_assignment": {
"type": "boolean",
- "description": "Enable Auto Assignment",
+ "description": "Enable Auto Assignment.",
"example": true
},
"working_hours_enabled": {
"type": "boolean",
- "description": "Enable working hours",
+ "description": "Enable working hours.",
"example": true
},
"out_of_office_message": {
"type": "string",
- "description": "Out of office message to be displayed on the widget",
+ "description": "Out of office message to send outside working hours.",
"example": "We are currently out of office. Please leave a message and we will get back to you."
},
"timezone": {
"type": "string",
- "description": "Timezone of the inbox",
+ "description": "Timezone of the inbox.",
"example": "America/New_York"
},
"allow_messages_after_resolved": {
"type": "boolean",
- "description": "Allow messages after conversation is resolved",
+ "description": "Allow messages after conversation is resolved.\n\nAvailable for: `Website`\n",
"example": true
},
"lock_to_single_conversation": {
"type": "boolean",
- "description": "Lock to single conversation",
+ "description": "Lock contact messages to a single active conversation.\n\nAvailable for: `API` `LINE` `Telegram` `WhatsApp` `SMS`\n",
"example": true
},
"portal_id": {
"type": "integer",
- "description": "Id of the help center portal to attach to the inbox",
+ "description": "Id of the help center portal to attach to the inbox.",
"example": 1
},
"sender_name_type": {
"type": "string",
- "description": "Sender name type for the inbox",
+ "description": "Sender name type for outbound email replies.\n\nAvailable for: `Website` `Email`\n",
"enum": [
"friendly",
"professional"
@@ -12092,45 +12504,43 @@
},
"business_name": {
"type": "string",
- "description": "Business name for the inbox",
+ "description": "Business name for outbound email replies.\n\nAvailable for: `Website` `Email`\n",
"example": "My Business"
},
"channel": {
- "type": "object",
- "properties": {
- "type": {
- "type": "string",
- "description": "Type of the channel",
- "enum": [
- "web_widget",
- "api",
- "email",
- "line",
- "telegram",
- "whatsapp",
- "sms"
- ],
- "example": "web_widget"
+ "oneOf": [
+ {
+ "$ref": "#/components/schemas/inbox_create_web_widget_channel_payload"
},
- "website_url": {
- "type": "string",
- "description": "URL at which the widget will be loaded",
- "example": "https://example.com"
+ {
+ "$ref": "#/components/schemas/inbox_create_api_channel_payload"
},
- "welcome_title": {
- "type": "string",
- "description": "Welcome title to be displayed on the widget",
- "example": "Welcome to our support"
+ {
+ "$ref": "#/components/schemas/inbox_create_email_channel_payload"
},
- "welcome_tagline": {
- "type": "string",
- "description": "Welcome tagline to be displayed on the widget",
- "example": "We are here to help you"
+ {
+ "$ref": "#/components/schemas/inbox_create_line_channel_payload"
},
- "widget_color": {
- "type": "string",
- "description": "A Hex-color string used to customize the widget",
- "example": "#FF5733"
+ {
+ "$ref": "#/components/schemas/inbox_create_telegram_channel_payload"
+ },
+ {
+ "$ref": "#/components/schemas/inbox_create_whatsapp_channel_payload"
+ },
+ {
+ "$ref": "#/components/schemas/inbox_create_sms_channel_payload"
+ }
+ ],
+ "discriminator": {
+ "propertyName": "type",
+ "mapping": {
+ "web_widget": "#/components/schemas/inbox_create_web_widget_channel_payload",
+ "api": "#/components/schemas/inbox_create_api_channel_payload",
+ "email": "#/components/schemas/inbox_create_email_channel_payload",
+ "line": "#/components/schemas/inbox_create_line_channel_payload",
+ "telegram": "#/components/schemas/inbox_create_telegram_channel_payload",
+ "whatsapp": "#/components/schemas/inbox_create_whatsapp_channel_payload",
+ "sms": "#/components/schemas/inbox_create_sms_channel_payload"
}
}
}
@@ -12141,72 +12551,121 @@
"properties": {
"name": {
"type": "string",
- "description": "The name of the inbox",
+ "description": "The name of the inbox.",
"example": "Support"
},
"avatar": {
"type": "string",
"format": "binary",
- "description": "Image file for avatar"
+ "description": "Image file for avatar."
},
"greeting_enabled": {
"type": "boolean",
- "description": "Enable greeting message",
+ "description": "Enable greeting message.",
"example": true
},
"greeting_message": {
"type": "string",
- "description": "Greeting message to be displayed on the widget",
+ "description": "Greeting message to send when greeting messages are enabled.",
"example": "Hello, how can I help you?"
},
"enable_email_collect": {
"type": "boolean",
- "description": "Enable email collection",
+ "description": "Enable email collection.\n\nAvailable for: `Website`\n",
"example": true
},
"csat_survey_enabled": {
"type": "boolean",
- "description": "Enable CSAT survey",
+ "description": "Enable CSAT survey.",
"example": true
},
+ "csat_config": {
+ "type": "object",
+ "description": "CSAT survey configuration.",
+ "properties": {
+ "display_type": {
+ "type": "string",
+ "description": "Display style for the CSAT survey.",
+ "enum": [
+ "emoji",
+ "star"
+ ],
+ "example": "emoji"
+ },
+ "message": {
+ "type": "string",
+ "description": "Message shown with the CSAT survey.",
+ "example": "Please rate your conversation"
+ },
+ "button_text": {
+ "type": "string",
+ "description": "Text shown on the CSAT survey button.",
+ "example": "Please rate us"
+ },
+ "language": {
+ "type": "string",
+ "description": "Language code for the CSAT survey.",
+ "example": "en"
+ },
+ "survey_rules": {
+ "type": "object",
+ "description": "Rules that decide when to show the CSAT survey.",
+ "properties": {
+ "operator": {
+ "type": "string",
+ "example": "contains"
+ },
+ "values": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "example": [
+ "billing"
+ ]
+ }
+ }
+ }
+ }
+ },
"enable_auto_assignment": {
"type": "boolean",
- "description": "Enable Auto Assignment",
+ "description": "Enable Auto Assignment.",
"example": true
},
"working_hours_enabled": {
"type": "boolean",
- "description": "Enable working hours",
+ "description": "Enable working hours.",
"example": true
},
"out_of_office_message": {
"type": "string",
- "description": "Out of office message to be displayed on the widget",
+ "description": "Out of office message to send outside working hours.",
"example": "We are currently out of office. Please leave a message and we will get back to you."
},
"timezone": {
"type": "string",
- "description": "Timezone of the inbox",
+ "description": "Timezone of the inbox.",
"example": "America/New_York"
},
"allow_messages_after_resolved": {
"type": "boolean",
- "description": "Allow messages after conversation is resolved",
+ "description": "Allow messages after conversation is resolved.\n\nAvailable for: `Website`\n",
"example": true
},
"lock_to_single_conversation": {
"type": "boolean",
- "description": "Lock to single conversation",
+ "description": "Lock contact messages to a single active conversation.\n\nAvailable for: `API` `LINE` `Telegram` `WhatsApp` `SMS`\n",
"example": true
},
"portal_id": {
"type": "integer",
- "description": "Id of the help center portal to attach to the inbox",
+ "description": "Id of the help center portal to attach to the inbox.",
"example": 1
},
"sender_name_type": {
"type": "string",
- "description": "Sender name type for the inbox",
+ "description": "Sender name type for outbound email replies.\n\nAvailable for: `Website` `Email`\n",
"enum": [
"friendly",
"professional"
@@ -12215,32 +12674,808 @@
},
"business_name": {
"type": "string",
- "description": "Business name for the inbox",
+ "description": "Business name for outbound email replies.\n\nAvailable for: `Website` `Email`\n",
"example": "My Business"
},
"channel": {
- "type": "object",
- "properties": {
- "website_url": {
- "type": "string",
- "description": "URL at which the widget will be loaded",
- "example": "https://example.com"
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/inbox_update_web_widget_channel_payload"
},
- "welcome_title": {
- "type": "string",
- "description": "Welcome title to be displayed on the widget",
- "example": "Welcome to our support"
+ {
+ "$ref": "#/components/schemas/inbox_update_api_channel_payload"
},
- "welcome_tagline": {
- "type": "string",
- "description": "Welcome tagline to be displayed on the widget",
- "example": "We are here to help you"
+ {
+ "$ref": "#/components/schemas/inbox_update_email_channel_payload"
},
- "widget_color": {
- "type": "string",
- "description": "A Hex-color string used to customize the widget",
- "example": "#FF5733"
+ {
+ "$ref": "#/components/schemas/inbox_update_line_channel_payload"
+ },
+ {
+ "$ref": "#/components/schemas/inbox_update_telegram_channel_payload"
+ },
+ {
+ "$ref": "#/components/schemas/inbox_update_whatsapp_channel_payload"
+ },
+ {
+ "$ref": "#/components/schemas/inbox_update_sms_channel_payload"
}
+ ]
+ }
+ }
+ },
+ "inbox_create_web_widget_channel_payload": {
+ "type": "object",
+ "title": "Website channel",
+ "required": [
+ "type",
+ "website_url"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": [
+ "web_widget"
+ ],
+ "example": "web_widget"
+ },
+ "website_url": {
+ "type": "string",
+ "description": "URL at which the widget will be loaded",
+ "example": "https://example.com"
+ },
+ "welcome_title": {
+ "type": "string",
+ "description": "Welcome title to be displayed on the widget",
+ "example": "Welcome to our support"
+ },
+ "welcome_tagline": {
+ "type": "string",
+ "description": "Welcome tagline to be displayed on the widget",
+ "example": "We are here to help you"
+ },
+ "widget_color": {
+ "type": "string",
+ "description": "A Hex-color string used to customize the widget",
+ "example": "#FF5733"
+ },
+ "reply_time": {
+ "type": "string",
+ "description": "Expected reply time shown on the widget",
+ "enum": [
+ "in_a_few_minutes",
+ "in_a_few_hours",
+ "in_a_day"
+ ],
+ "example": "in_a_few_minutes"
+ },
+ "pre_chat_form_enabled": {
+ "type": "boolean",
+ "description": "Enable the pre-chat form before starting a conversation",
+ "example": false
+ },
+ "pre_chat_form_options": {
+ "type": "object",
+ "description": "Pre-chat form configuration",
+ "example": {
+ "pre_chat_message": "Share your queries or comments here.",
+ "pre_chat_fields": [
+ {
+ "field_type": "standard",
+ "label": "Email Id",
+ "name": "emailAddress",
+ "type": "email",
+ "required": true,
+ "enabled": true
+ }
+ ]
+ }
+ },
+ "continuity_via_email": {
+ "type": "boolean",
+ "description": "Continue conversations over email when the contact leaves the website",
+ "example": true
+ },
+ "hmac_mandatory": {
+ "type": "boolean",
+ "description": "Require HMAC verification for contacts using the widget",
+ "example": false
+ },
+ "allowed_domains": {
+ "type": "string",
+ "description": "Comma-separated list of domains where the widget is allowed to load",
+ "example": "example.com"
+ },
+ "selected_feature_flags": {
+ "type": "array",
+ "description": "Enabled widget feature flags",
+ "items": {
+ "type": "string",
+ "enum": [
+ "attachments",
+ "emoji_picker",
+ "end_conversation",
+ "use_inbox_avatar_for_bot",
+ "allow_mobile_webview"
+ ]
+ },
+ "example": [
+ "attachments",
+ "emoji_picker",
+ "end_conversation"
+ ]
+ }
+ }
+ },
+ "inbox_create_api_channel_payload": {
+ "type": "object",
+ "title": "API channel",
+ "required": [
+ "type"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": [
+ "api"
+ ],
+ "example": "api"
+ },
+ "webhook_url": {
+ "type": "string",
+ "description": "Webhook URL for API channel inbox callbacks",
+ "example": "https://example.com/webhook"
+ },
+ "hmac_mandatory": {
+ "type": "boolean",
+ "description": "Require HMAC verification for incoming API channel messages",
+ "example": false
+ },
+ "additional_attributes": {
+ "type": "object",
+ "description": "Additional attributes stored on contacts created through the API channel",
+ "example": {
+ "source": "mobile_app"
+ }
+ }
+ }
+ },
+ "inbox_create_email_channel_payload": {
+ "type": "object",
+ "title": "Email channel",
+ "required": [
+ "type",
+ "email"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": [
+ "email"
+ ],
+ "example": "email"
+ },
+ "email": {
+ "type": "string",
+ "description": "Email address for the inbox",
+ "example": "support@example.com"
+ },
+ "imap_enabled": {
+ "type": "boolean",
+ "description": "Enable IMAP for inbound emails",
+ "example": true
+ },
+ "imap_login": {
+ "type": "string",
+ "description": "IMAP login username",
+ "example": "support@example.com"
+ },
+ "imap_password": {
+ "type": "string",
+ "description": "IMAP login password",
+ "example": "your-imap-password"
+ },
+ "imap_address": {
+ "type": "string",
+ "description": "IMAP server address",
+ "example": "imap.example.com"
+ },
+ "imap_port": {
+ "type": "integer",
+ "description": "IMAP server port",
+ "example": 993
+ },
+ "imap_enable_ssl": {
+ "type": "boolean",
+ "description": "Enable SSL for IMAP",
+ "example": true
+ },
+ "imap_authentication": {
+ "type": "string",
+ "description": "IMAP authentication method",
+ "example": "plain"
+ },
+ "smtp_enabled": {
+ "type": "boolean",
+ "description": "Enable SMTP for outbound emails",
+ "example": true
+ },
+ "smtp_login": {
+ "type": "string",
+ "description": "SMTP login username",
+ "example": "support@example.com"
+ },
+ "smtp_password": {
+ "type": "string",
+ "description": "SMTP login password",
+ "example": "your-smtp-password"
+ },
+ "smtp_address": {
+ "type": "string",
+ "description": "SMTP server address",
+ "example": "smtp.example.com"
+ },
+ "smtp_port": {
+ "type": "integer",
+ "description": "SMTP server port",
+ "example": 587
+ },
+ "smtp_domain": {
+ "type": "string",
+ "description": "SMTP HELO domain",
+ "example": "example.com"
+ },
+ "smtp_enable_starttls_auto": {
+ "type": "boolean",
+ "description": "Automatically enable STARTTLS for SMTP",
+ "example": true
+ },
+ "smtp_enable_ssl_tls": {
+ "type": "boolean",
+ "description": "Enable SSL/TLS for SMTP",
+ "example": false
+ },
+ "smtp_openssl_verify_mode": {
+ "type": "string",
+ "description": "OpenSSL certificate verification mode for SMTP",
+ "example": "none"
+ },
+ "smtp_authentication": {
+ "type": "string",
+ "description": "SMTP authentication method",
+ "example": "login"
+ },
+ "provider": {
+ "type": "string",
+ "description": "Email provider",
+ "example": "google"
+ },
+ "verified_for_sending": {
+ "type": "boolean",
+ "description": "Whether the inbox is verified for sending emails",
+ "example": false
+ }
+ }
+ },
+ "inbox_create_line_channel_payload": {
+ "type": "object",
+ "title": "LINE channel",
+ "required": [
+ "type",
+ "line_channel_id",
+ "line_channel_secret",
+ "line_channel_token"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": [
+ "line"
+ ],
+ "example": "line"
+ },
+ "line_channel_id": {
+ "type": "string",
+ "description": "LINE channel ID",
+ "example": "1234567890"
+ },
+ "line_channel_secret": {
+ "type": "string",
+ "description": "LINE channel secret",
+ "example": "line-channel-secret"
+ },
+ "line_channel_token": {
+ "type": "string",
+ "description": "LINE channel access token",
+ "example": "line-channel-token"
+ }
+ }
+ },
+ "inbox_create_telegram_channel_payload": {
+ "type": "object",
+ "title": "Telegram channel",
+ "required": [
+ "type",
+ "bot_token"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": [
+ "telegram"
+ ],
+ "example": "telegram"
+ },
+ "bot_token": {
+ "type": "string",
+ "description": "Telegram bot token",
+ "example": "123456789:telegram-bot-token"
+ }
+ }
+ },
+ "inbox_create_whatsapp_channel_payload": {
+ "oneOf": [
+ {
+ "type": "object",
+ "title": "WhatsApp Cloud channel",
+ "required": [
+ "type",
+ "phone_number",
+ "provider",
+ "provider_config"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": [
+ "whatsapp"
+ ],
+ "example": "whatsapp"
+ },
+ "phone_number": {
+ "type": "string",
+ "description": "WhatsApp phone number",
+ "example": "+15551234567"
+ },
+ "provider": {
+ "type": "string",
+ "description": "WhatsApp provider",
+ "enum": [
+ "whatsapp_cloud"
+ ],
+ "example": "whatsapp_cloud"
+ },
+ "provider_config": {
+ "type": "object",
+ "description": "WhatsApp Cloud provider configuration",
+ "required": [
+ "api_key",
+ "phone_number_id",
+ "business_account_id"
+ ],
+ "properties": {
+ "api_key": {
+ "type": "string",
+ "description": "WhatsApp Cloud API key",
+ "example": "your-api-key"
+ },
+ "phone_number_id": {
+ "type": "string",
+ "description": "Phone number ID for WhatsApp Cloud",
+ "example": "your-phone-number-id"
+ },
+ "business_account_id": {
+ "type": "string",
+ "description": "Business account ID for WhatsApp Cloud",
+ "example": "your-business-account-id"
+ }
+ },
+ "example": {
+ "api_key": "your-api-key",
+ "phone_number_id": "your-phone-number-id",
+ "business_account_id": "your-business-account-id"
+ }
+ }
+ }
+ },
+ {
+ "type": "object",
+ "title": "Legacy 360dialog WhatsApp channel",
+ "deprecated": true,
+ "required": [
+ "type",
+ "phone_number",
+ "provider_config"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": [
+ "whatsapp"
+ ],
+ "example": "whatsapp"
+ },
+ "phone_number": {
+ "type": "string",
+ "description": "WhatsApp phone number",
+ "example": "+15551234567"
+ },
+ "provider": {
+ "type": "string",
+ "description": "Legacy 360dialog provider. Omit this field or use `default` only for existing deprecated 360dialog setups.",
+ "enum": [
+ "default"
+ ],
+ "deprecated": true,
+ "example": "default"
+ },
+ "provider_config": {
+ "type": "object",
+ "description": "Legacy 360dialog provider configuration",
+ "required": [
+ "api_key"
+ ],
+ "properties": {
+ "api_key": {
+ "type": "string",
+ "description": "360dialog API key",
+ "example": "your-api-key"
+ }
+ },
+ "example": {
+ "api_key": "your-api-key"
+ }
+ }
+ }
+ }
+ ]
+ },
+ "inbox_create_sms_channel_payload": {
+ "type": "object",
+ "title": "SMS channel",
+ "required": [
+ "type",
+ "phone_number"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": [
+ "sms"
+ ],
+ "example": "sms"
+ },
+ "phone_number": {
+ "type": "string",
+ "description": "SMS phone number",
+ "example": "+15551234567"
+ },
+ "provider_config": {
+ "type": "object",
+ "description": "Provider-specific SMS configuration",
+ "example": {
+ "account_id": "your-account-id",
+ "application_id": "your-application-id"
+ }
+ }
+ }
+ },
+ "inbox_update_web_widget_channel_payload": {
+ "type": "object",
+ "title": "Website channel settings",
+ "properties": {
+ "website_url": {
+ "type": "string",
+ "description": "URL at which the widget will be loaded",
+ "example": "https://example.com"
+ },
+ "welcome_title": {
+ "type": "string",
+ "description": "Welcome title to be displayed on the widget",
+ "example": "Welcome to our support"
+ },
+ "welcome_tagline": {
+ "type": "string",
+ "description": "Welcome tagline to be displayed on the widget",
+ "example": "We are here to help you"
+ },
+ "widget_color": {
+ "type": "string",
+ "description": "A Hex-color string used to customize the widget",
+ "example": "#FF5733"
+ },
+ "reply_time": {
+ "type": "string",
+ "description": "Expected reply time shown on the widget",
+ "enum": [
+ "in_a_few_minutes",
+ "in_a_few_hours",
+ "in_a_day"
+ ],
+ "example": "in_a_few_minutes"
+ },
+ "pre_chat_form_enabled": {
+ "type": "boolean",
+ "description": "Enable the pre-chat form before starting a conversation",
+ "example": false
+ },
+ "pre_chat_form_options": {
+ "type": "object",
+ "description": "Pre-chat form configuration",
+ "example": {
+ "pre_chat_message": "Share your queries or comments here.",
+ "pre_chat_fields": [
+ {
+ "field_type": "standard",
+ "label": "Email Id",
+ "name": "emailAddress",
+ "type": "email",
+ "required": true,
+ "enabled": true
+ }
+ ]
+ }
+ },
+ "continuity_via_email": {
+ "type": "boolean",
+ "description": "Continue conversations over email when the contact leaves the website",
+ "example": true
+ },
+ "hmac_mandatory": {
+ "type": "boolean",
+ "description": "Require HMAC verification for contacts using the widget",
+ "example": false
+ },
+ "allowed_domains": {
+ "type": "string",
+ "description": "Comma-separated list of domains where the widget is allowed to load",
+ "example": "example.com"
+ },
+ "selected_feature_flags": {
+ "type": "array",
+ "description": "Enabled widget feature flags",
+ "items": {
+ "type": "string",
+ "enum": [
+ "attachments",
+ "emoji_picker",
+ "end_conversation",
+ "use_inbox_avatar_for_bot",
+ "allow_mobile_webview"
+ ]
+ },
+ "example": [
+ "attachments",
+ "emoji_picker",
+ "end_conversation"
+ ]
+ }
+ }
+ },
+ "inbox_update_api_channel_payload": {
+ "type": "object",
+ "title": "API channel settings",
+ "properties": {
+ "webhook_url": {
+ "type": "string",
+ "description": "Webhook URL for API channel inbox callbacks",
+ "example": "https://example.com/webhook"
+ },
+ "hmac_mandatory": {
+ "type": "boolean",
+ "description": "Require HMAC verification for incoming API channel messages",
+ "example": false
+ },
+ "additional_attributes": {
+ "type": "object",
+ "description": "Additional attributes stored on contacts created through the API channel",
+ "example": {
+ "source": "mobile_app"
+ }
+ }
+ }
+ },
+ "inbox_update_email_channel_payload": {
+ "type": "object",
+ "title": "Email channel settings",
+ "properties": {
+ "email": {
+ "type": "string",
+ "description": "Email address for the inbox",
+ "example": "support@example.com"
+ },
+ "imap_enabled": {
+ "type": "boolean",
+ "description": "Enable IMAP for inbound emails",
+ "example": true
+ },
+ "imap_login": {
+ "type": "string",
+ "description": "IMAP login username",
+ "example": "support@example.com"
+ },
+ "imap_password": {
+ "type": "string",
+ "description": "IMAP login password",
+ "example": "your-imap-password"
+ },
+ "imap_address": {
+ "type": "string",
+ "description": "IMAP server address",
+ "example": "imap.example.com"
+ },
+ "imap_port": {
+ "type": "integer",
+ "description": "IMAP server port",
+ "example": 993
+ },
+ "imap_enable_ssl": {
+ "type": "boolean",
+ "description": "Enable SSL for IMAP",
+ "example": true
+ },
+ "imap_authentication": {
+ "type": "string",
+ "description": "IMAP authentication method",
+ "example": "plain"
+ },
+ "smtp_enabled": {
+ "type": "boolean",
+ "description": "Enable SMTP for outbound emails",
+ "example": true
+ },
+ "smtp_login": {
+ "type": "string",
+ "description": "SMTP login username",
+ "example": "support@example.com"
+ },
+ "smtp_password": {
+ "type": "string",
+ "description": "SMTP login password",
+ "example": "your-smtp-password"
+ },
+ "smtp_address": {
+ "type": "string",
+ "description": "SMTP server address",
+ "example": "smtp.example.com"
+ },
+ "smtp_port": {
+ "type": "integer",
+ "description": "SMTP server port",
+ "example": 587
+ },
+ "smtp_domain": {
+ "type": "string",
+ "description": "SMTP HELO domain",
+ "example": "example.com"
+ },
+ "smtp_enable_starttls_auto": {
+ "type": "boolean",
+ "description": "Automatically enable STARTTLS for SMTP",
+ "example": true
+ },
+ "smtp_enable_ssl_tls": {
+ "type": "boolean",
+ "description": "Enable SSL/TLS for SMTP",
+ "example": false
+ },
+ "smtp_openssl_verify_mode": {
+ "type": "string",
+ "description": "OpenSSL certificate verification mode for SMTP",
+ "example": "none"
+ },
+ "smtp_authentication": {
+ "type": "string",
+ "description": "SMTP authentication method",
+ "example": "login"
+ },
+ "provider": {
+ "type": "string",
+ "description": "Email provider",
+ "example": "google"
+ },
+ "verified_for_sending": {
+ "type": "boolean",
+ "description": "Whether the inbox is verified for sending emails",
+ "example": false
+ }
+ }
+ },
+ "inbox_update_line_channel_payload": {
+ "type": "object",
+ "title": "LINE channel settings",
+ "properties": {
+ "line_channel_id": {
+ "type": "string",
+ "description": "LINE channel ID",
+ "example": "1234567890"
+ },
+ "line_channel_secret": {
+ "type": "string",
+ "description": "LINE channel secret",
+ "example": "line-channel-secret"
+ },
+ "line_channel_token": {
+ "type": "string",
+ "description": "LINE channel access token",
+ "example": "line-channel-token"
+ }
+ }
+ },
+ "inbox_update_telegram_channel_payload": {
+ "type": "object",
+ "title": "Telegram channel settings",
+ "properties": {
+ "bot_token": {
+ "type": "string",
+ "description": "Telegram bot token",
+ "example": "123456789:telegram-bot-token"
+ }
+ }
+ },
+ "inbox_update_whatsapp_channel_payload": {
+ "type": "object",
+ "title": "WhatsApp channel settings",
+ "properties": {
+ "phone_number": {
+ "type": "string",
+ "description": "WhatsApp phone number",
+ "example": "+15551234567"
+ },
+ "provider": {
+ "type": "string",
+ "description": "WhatsApp provider. `default` is supported only for existing deprecated 360dialog setups.",
+ "enum": [
+ "whatsapp_cloud",
+ "default"
+ ],
+ "example": "whatsapp_cloud"
+ },
+ "provider_config": {
+ "type": "object",
+ "description": "WhatsApp provider configuration. Cloud channels use `api_key`, `phone_number_id`, and `business_account_id`; legacy 360dialog channels use `api_key`.",
+ "properties": {
+ "api_key": {
+ "type": "string",
+ "description": "Provider API key",
+ "example": "your-api-key"
+ },
+ "phone_number_id": {
+ "type": "string",
+ "description": "Phone number ID for WhatsApp Cloud",
+ "example": "your-phone-number-id"
+ },
+ "business_account_id": {
+ "type": "string",
+ "description": "Business account ID for WhatsApp Cloud",
+ "example": "your-business-account-id"
+ }
+ },
+ "example": {
+ "api_key": "your-api-key",
+ "phone_number_id": "your-phone-number-id",
+ "business_account_id": "your-business-account-id"
+ }
+ }
+ }
+ },
+ "inbox_update_sms_channel_payload": {
+ "type": "object",
+ "title": "SMS channel settings",
+ "properties": {
+ "phone_number": {
+ "type": "string",
+ "description": "SMS phone number",
+ "example": "+15551234567"
+ },
+ "provider_config": {
+ "type": "object",
+ "description": "Provider-specific SMS configuration",
+ "example": {
+ "api_key": "your-api-key",
+ "api_secret": "your-api-secret",
+ "application_id": "your-application-id",
+ "account_id": "your-account-id"
}
}
}
@@ -12339,7 +13574,9 @@
"message_updated",
"contact_created",
"contact_updated",
- "webwidget_triggered"
+ "webwidget_triggered",
+ "conversation_typing_on",
+ "conversation_typing_off"
]
},
"description": "The events you want to subscribe to.",
diff --git a/swagger/tag_groups/application_swagger.json b/swagger/tag_groups/application_swagger.json
index a013b3694..f9ff31a7e 100644
--- a/swagger/tag_groups/application_swagger.json
+++ b/swagger/tag_groups/application_swagger.json
@@ -192,7 +192,7 @@
"per_page": {
"type": "integer",
"description": "Number of items per page",
- "example": 15
+ "example": 25
},
"total_entries": {
"type": "integer",
@@ -4000,6 +4000,147 @@
"application/json": {
"schema": {
"$ref": "#/components/schemas/inbox_create_payload"
+ },
+ "examples": {
+ "web_widget": {
+ "summary": "Website inbox",
+ "value": {
+ "name": "Support",
+ "greeting_enabled": true,
+ "greeting_message": "Hello, how can I help you?",
+ "enable_email_collect": true,
+ "enable_auto_assignment": true,
+ "working_hours_enabled": true,
+ "timezone": "America/New_York",
+ "allow_messages_after_resolved": true,
+ "channel": {
+ "type": "web_widget",
+ "website_url": "https://example.com",
+ "welcome_title": "Welcome to our support",
+ "welcome_tagline": "We are here to help you",
+ "widget_color": "#FF5733",
+ "reply_time": "in_a_few_minutes",
+ "pre_chat_form_enabled": false,
+ "continuity_via_email": true,
+ "hmac_mandatory": false,
+ "selected_feature_flags": [
+ "attachments",
+ "emoji_picker",
+ "end_conversation"
+ ]
+ }
+ }
+ },
+ "api": {
+ "summary": "API channel",
+ "value": {
+ "name": "API Inbox",
+ "greeting_enabled": true,
+ "greeting_message": "Hello, how can I help you?",
+ "enable_auto_assignment": true,
+ "working_hours_enabled": true,
+ "timezone": "America/New_York",
+ "channel": {
+ "type": "api",
+ "webhook_url": "https://example.com/webhook",
+ "hmac_mandatory": false,
+ "additional_attributes": {
+ "source": "mobile_app"
+ }
+ }
+ }
+ },
+ "email": {
+ "summary": "Email channel",
+ "value": {
+ "name": "Email Inbox",
+ "greeting_enabled": true,
+ "greeting_message": "Hello, how can I help you?",
+ "enable_auto_assignment": true,
+ "working_hours_enabled": true,
+ "timezone": "America/New_York",
+ "channel": {
+ "type": "email",
+ "email": "support@example.com",
+ "imap_enabled": false,
+ "smtp_enabled": false
+ }
+ }
+ },
+ "line": {
+ "summary": "LINE channel",
+ "value": {
+ "name": "LINE Inbox",
+ "greeting_enabled": true,
+ "greeting_message": "Hello, how can I help you?",
+ "enable_auto_assignment": true,
+ "working_hours_enabled": true,
+ "timezone": "America/New_York",
+ "channel": {
+ "type": "line",
+ "line_channel_id": "1234567890",
+ "line_channel_secret": "line-channel-secret",
+ "line_channel_token": "line-channel-token"
+ }
+ }
+ },
+ "telegram": {
+ "summary": "Telegram channel",
+ "value": {
+ "name": "Telegram Inbox",
+ "greeting_enabled": true,
+ "greeting_message": "Hello, how can I help you?",
+ "enable_auto_assignment": true,
+ "working_hours_enabled": true,
+ "timezone": "America/New_York",
+ "channel": {
+ "type": "telegram",
+ "bot_token": "123456789:telegram-bot-token"
+ }
+ }
+ },
+ "whatsapp": {
+ "summary": "WhatsApp channel",
+ "value": {
+ "name": "WhatsApp Inbox",
+ "greeting_enabled": true,
+ "greeting_message": "Hello, how can I help you?",
+ "enable_auto_assignment": true,
+ "working_hours_enabled": true,
+ "timezone": "America/New_York",
+ "channel": {
+ "type": "whatsapp",
+ "phone_number": "+15551234567",
+ "provider": "whatsapp_cloud",
+ "provider_config": {
+ "api_key": "your-api-key",
+ "phone_number_id": "your-phone-number-id",
+ "business_account_id": "your-business-account-id"
+ }
+ }
+ }
+ },
+ "sms": {
+ "summary": "SMS channel",
+ "value": {
+ "name": "SMS Inbox",
+ "greeting_enabled": true,
+ "greeting_message": "Hello, how can I help you?",
+ "enable_auto_assignment": true,
+ "working_hours_enabled": true,
+ "timezone": "America/New_York",
+ "channel": {
+ "type": "sms",
+ "phone_number": "+15551234567",
+ "provider_config": {
+ "api_key": "your-api-key",
+ "api_secret": "your-api-secret",
+ "application_id": "your-application-id",
+ "account_id": "your-account-id"
+ }
+ }
+ }
+ }
}
}
}
@@ -4130,6 +4271,140 @@
"application/json": {
"schema": {
"$ref": "#/components/schemas/inbox_update_payload"
+ },
+ "examples": {
+ "web_widget": {
+ "summary": "Website inbox settings",
+ "value": {
+ "name": "Support",
+ "greeting_enabled": true,
+ "greeting_message": "Hello, how can I help you?",
+ "enable_email_collect": true,
+ "enable_auto_assignment": true,
+ "working_hours_enabled": true,
+ "timezone": "America/New_York",
+ "allow_messages_after_resolved": true,
+ "channel": {
+ "website_url": "https://example.com",
+ "welcome_title": "Welcome to our support",
+ "welcome_tagline": "We are here to help you",
+ "widget_color": "#FF5733",
+ "reply_time": "in_a_few_minutes",
+ "pre_chat_form_enabled": false,
+ "continuity_via_email": true,
+ "hmac_mandatory": false,
+ "selected_feature_flags": [
+ "attachments",
+ "emoji_picker",
+ "end_conversation"
+ ]
+ }
+ }
+ },
+ "api": {
+ "summary": "API channel settings",
+ "value": {
+ "name": "API Inbox",
+ "greeting_enabled": true,
+ "greeting_message": "Hello, how can I help you?",
+ "enable_auto_assignment": true,
+ "working_hours_enabled": true,
+ "timezone": "America/New_York",
+ "channel": {
+ "webhook_url": "https://example.com/webhook",
+ "hmac_mandatory": false,
+ "additional_attributes": {
+ "source": "mobile_app"
+ }
+ }
+ }
+ },
+ "email": {
+ "summary": "Email channel settings",
+ "value": {
+ "name": "Email Inbox",
+ "greeting_enabled": true,
+ "greeting_message": "Hello, how can I help you?",
+ "enable_auto_assignment": true,
+ "working_hours_enabled": true,
+ "timezone": "America/New_York",
+ "channel": {
+ "email": "support@example.com",
+ "imap_enabled": false,
+ "smtp_enabled": false
+ }
+ }
+ },
+ "line": {
+ "summary": "LINE channel settings",
+ "value": {
+ "name": "LINE Inbox",
+ "greeting_enabled": true,
+ "greeting_message": "Hello, how can I help you?",
+ "enable_auto_assignment": true,
+ "working_hours_enabled": true,
+ "timezone": "America/New_York",
+ "channel": {
+ "line_channel_id": "1234567890",
+ "line_channel_secret": "line-channel-secret",
+ "line_channel_token": "line-channel-token"
+ }
+ }
+ },
+ "telegram": {
+ "summary": "Telegram channel settings",
+ "value": {
+ "name": "Telegram Inbox",
+ "greeting_enabled": true,
+ "greeting_message": "Hello, how can I help you?",
+ "enable_auto_assignment": true,
+ "working_hours_enabled": true,
+ "timezone": "America/New_York",
+ "channel": {
+ "bot_token": "123456789:telegram-bot-token"
+ }
+ }
+ },
+ "whatsapp": {
+ "summary": "WhatsApp channel settings",
+ "value": {
+ "name": "WhatsApp Inbox",
+ "greeting_enabled": true,
+ "greeting_message": "Hello, how can I help you?",
+ "enable_auto_assignment": true,
+ "working_hours_enabled": true,
+ "timezone": "America/New_York",
+ "channel": {
+ "phone_number": "+15551234567",
+ "provider": "whatsapp_cloud",
+ "provider_config": {
+ "api_key": "your-api-key",
+ "phone_number_id": "your-phone-number-id",
+ "business_account_id": "your-business-account-id"
+ }
+ }
+ }
+ },
+ "sms": {
+ "summary": "SMS channel settings",
+ "value": {
+ "name": "SMS Inbox",
+ "greeting_enabled": true,
+ "greeting_message": "Hello, how can I help you?",
+ "enable_auto_assignment": true,
+ "working_hours_enabled": true,
+ "timezone": "America/New_York",
+ "channel": {
+ "phone_number": "+15551234567",
+ "provider_config": {
+ "api_key": "your-api-key",
+ "api_secret": "your-api-secret",
+ "application_id": "your-application-id",
+ "account_id": "your-account-id"
+ }
+ }
+ }
+ }
}
}
}
@@ -5005,7 +5280,7 @@
],
"operationId": "create-a-new-message-in-a-conversation",
"summary": "Create New Message",
- "description": "Create a new message in the conversation.\n\n## WhatsApp Template Messages\n\nFor WhatsApp channels, you can send structured template messages using the `template_params` field. \nTemplates must be pre-approved in WhatsApp Business Manager.\n\n### Example Templates\n\n**Text with Image Header:**\n```json\n{\n \"content\": \"Hi your order 121212 is confirmed. Please wait for further updates\",\n \"template_params\": {\n \"name\": \"order_confirmation\",\n \"category\": \"MARKETING\",\n \"language\": \"en\",\n \"processed_params\": {\n \"body\": {\n \"1\": \"121212\"\n },\n \"header\": {\n \"media_url\": \"https://picsum.photos/200/300\",\n \"media_type\": \"image\"\n }\n }\n }\n}\n```\n\n**Text with Copy Code Button:**\n```json\n{\n \"content\": \"Special offer! Get 30% off your next purchase. Use the code below\",\n \"template_params\": {\n \"name\": \"discount_coupon\",\n \"category\": \"MARKETING\",\n \"language\": \"en\",\n \"processed_params\": {\n \"body\": {\n \"discount_percentage\": \"30\"\n },\n \"buttons\": [{\n \"type\": \"copy_code\",\n \"parameter\": \"SAVE20\"\n }]\n }\n }\n}\n```\n",
+ "description": "Create a new message in the conversation.\n\nUse `application/json` for text messages and `multipart/form-data` when the\nmessage includes file attachments.\n\n### Multipart attachment request\n\nSend files with the `attachments[]` form field. `curl -F` sets the\n`multipart/form-data` content type and boundary automatically.\n\n```bash\ncurl -X POST \"https://app.chatwoot.com/api/v1/accounts/{account_id}/conversations/{conversation_id}/messages\" \\\n -H \"api_access_token: \" \\\n -F \"content=Here is the screenshot\" \\\n -F \"message_type=outgoing\" \\\n -F \"private=false\" \\\n -F \"attachments[]=@/path/to/screenshot.png\"\n```\n\n## WhatsApp Template Messages\n\nFor WhatsApp channels, you can send structured template messages using the `template_params` field. \nTemplates must be pre-approved in WhatsApp Business Manager.\n\n### Example Templates\n\n**Text with Image Header:**\n```json\n{\n \"content\": \"Hi your order 121212 is confirmed. Please wait for further updates\",\n \"template_params\": {\n \"name\": \"order_confirmation\",\n \"category\": \"MARKETING\",\n \"language\": \"en\",\n \"processed_params\": {\n \"body\": {\n \"1\": \"121212\"\n },\n \"header\": {\n \"media_url\": \"https://picsum.photos/200/300\",\n \"media_type\": \"image\"\n }\n }\n }\n}\n```\n\n**Text with Copy Code Button:**\n```json\n{\n \"content\": \"Special offer! Get 30% off your next purchase. Use the code below\",\n \"template_params\": {\n \"name\": \"discount_coupon\",\n \"category\": \"MARKETING\",\n \"language\": \"en\",\n \"processed_params\": {\n \"body\": {\n \"discount_percentage\": \"30\"\n },\n \"buttons\": [{\n \"type\": \"copy_code\",\n \"parameter\": \"SAVE20\"\n }]\n }\n }\n}\n```\n",
"security": [
{
"userApiKey": []
@@ -5021,6 +5296,86 @@
"schema": {
"$ref": "#/components/schemas/conversation_message_create_payload"
}
+ },
+ "multipart/form-data": {
+ "schema": {
+ "type": "object",
+ "description": "Form data payload for creating a message with file attachments.",
+ "example": {
+ "content": "Here is the screenshot",
+ "message_type": "outgoing",
+ "private": false,
+ "attachments[]": [
+ "screenshot.png"
+ ]
+ },
+ "properties": {
+ "content": {
+ "type": "string",
+ "description": "The content of the message",
+ "example": "Here is the screenshot"
+ },
+ "message_type": {
+ "type": "string",
+ "enum": [
+ "outgoing",
+ "incoming"
+ ],
+ "description": "The type of the message",
+ "example": "outgoing"
+ },
+ "private": {
+ "type": "boolean",
+ "description": "Flag to identify if it is a private note",
+ "example": false
+ },
+ "content_type": {
+ "type": "string",
+ "enum": [
+ "text",
+ "input_email",
+ "cards",
+ "input_select",
+ "form",
+ "article"
+ ],
+ "description": "Content type of the message",
+ "example": "text"
+ },
+ "content_attributes": {
+ "type": "object",
+ "description": "Attributes based on the content type",
+ "example": {}
+ },
+ "attachments[]": {
+ "type": "array",
+ "description": "Files to attach to the message",
+ "items": {
+ "type": "string",
+ "format": "binary"
+ }
+ }
+ }
+ },
+ "encoding": {
+ "attachments[]": {
+ "style": "form",
+ "explode": true
+ }
+ },
+ "examples": {
+ "attachment_message": {
+ "summary": "Message with an attachment",
+ "value": {
+ "content": "Here is the screenshot",
+ "message_type": "outgoing",
+ "private": false,
+ "attachments[]": [
+ "screenshot.png"
+ ]
+ }
+ }
+ }
}
}
},
@@ -8399,15 +8754,16 @@
"availability_status": {
"type": "string",
"enum": [
- "available",
+ "online",
"busy",
"offline"
],
- "description": "The availability status of the agent computed by Chatwoot."
+ "readOnly": true,
+ "description": "The effective availability status of the agent, derived from the configured availability, auto-offline setting, and current presence. To update an agent's configured availability, use the availability field in create or update requests."
},
"auto_offline": {
"type": "boolean",
- "description": "Whether the availability status of agent is configured to go offline automatically when away."
+ "description": "Whether the agent is automatically marked offline when they are away."
},
"confirmed": {
"type": "boolean",
@@ -8822,11 +9178,18 @@
"contact_updated",
"message_created",
"message_updated",
- "webwidget_triggered"
+ "webwidget_triggered",
+ "conversation_typing_on",
+ "conversation_typing_off"
]
},
"description": "The list of subscribed events"
},
+ "secret": {
+ "type": "string",
+ "nullable": true,
+ "description": "Secret used to sign webhook requests. Signed webhook deliveries include `X-Chatwoot-Timestamp` and `X-Chatwoot-Signature`; the signature is `sha256=` followed by the HMAC-SHA256 of `{timestamp}.{raw_request_body}` using this secret. Deliveries also include `X-Chatwoot-Delivery` when a delivery id is available."
+ },
"account_id": {
"type": "number",
"description": "The id of the account which the webhook object belongs to"
@@ -10102,19 +10465,19 @@
"description": "Whether its administrator or agent",
"example": "agent"
},
- "availability_status": {
+ "availability": {
"type": "string",
"enum": [
- "available",
+ "online",
"busy",
"offline"
],
- "description": "The availability setting of the agent.",
- "example": "available"
+ "description": "The configured availability of the agent.",
+ "example": "online"
},
"auto_offline": {
"type": "boolean",
- "description": "Whether the availability status of agent is configured to go offline automatically when away.",
+ "description": "Whether the agent is automatically marked offline when they are away.",
"example": true
}
}
@@ -10134,19 +10497,19 @@
"description": "Whether its administrator or agent",
"example": "agent"
},
- "availability_status": {
+ "availability": {
"type": "string",
"enum": [
- "available",
+ "online",
"busy",
"offline"
],
- "description": "The availability status of the agent.",
- "example": "available"
+ "description": "The configured availability of the agent.",
+ "example": "online"
},
"auto_offline": {
"type": "boolean",
- "description": "Whether the availability status of agent is configured to go offline automatically when away.",
+ "description": "Whether the agent is automatically marked offline when they are away.",
"example": true
}
}
@@ -10525,72 +10888,121 @@
"properties": {
"name": {
"type": "string",
- "description": "The name of the inbox",
+ "description": "The name of the inbox.",
"example": "Support"
},
"avatar": {
"type": "string",
"format": "binary",
- "description": "Image file for avatar"
+ "description": "Image file for avatar."
},
"greeting_enabled": {
"type": "boolean",
- "description": "Enable greeting message",
+ "description": "Enable greeting message.",
"example": true
},
"greeting_message": {
"type": "string",
- "description": "Greeting message to be displayed on the widget",
+ "description": "Greeting message to send when greeting messages are enabled.",
"example": "Hello, how can I help you?"
},
"enable_email_collect": {
"type": "boolean",
- "description": "Enable email collection",
+ "description": "Enable email collection.\n\nAvailable for: `Website`\n",
"example": true
},
"csat_survey_enabled": {
"type": "boolean",
- "description": "Enable CSAT survey",
+ "description": "Enable CSAT survey.",
"example": true
},
+ "csat_config": {
+ "type": "object",
+ "description": "CSAT survey configuration.",
+ "properties": {
+ "display_type": {
+ "type": "string",
+ "description": "Display style for the CSAT survey.",
+ "enum": [
+ "emoji",
+ "star"
+ ],
+ "example": "emoji"
+ },
+ "message": {
+ "type": "string",
+ "description": "Message shown with the CSAT survey.",
+ "example": "Please rate your conversation"
+ },
+ "button_text": {
+ "type": "string",
+ "description": "Text shown on the CSAT survey button.",
+ "example": "Please rate us"
+ },
+ "language": {
+ "type": "string",
+ "description": "Language code for the CSAT survey.",
+ "example": "en"
+ },
+ "survey_rules": {
+ "type": "object",
+ "description": "Rules that decide when to show the CSAT survey.",
+ "properties": {
+ "operator": {
+ "type": "string",
+ "example": "contains"
+ },
+ "values": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "example": [
+ "billing"
+ ]
+ }
+ }
+ }
+ }
+ },
"enable_auto_assignment": {
"type": "boolean",
- "description": "Enable Auto Assignment",
+ "description": "Enable Auto Assignment.",
"example": true
},
"working_hours_enabled": {
"type": "boolean",
- "description": "Enable working hours",
+ "description": "Enable working hours.",
"example": true
},
"out_of_office_message": {
"type": "string",
- "description": "Out of office message to be displayed on the widget",
+ "description": "Out of office message to send outside working hours.",
"example": "We are currently out of office. Please leave a message and we will get back to you."
},
"timezone": {
"type": "string",
- "description": "Timezone of the inbox",
+ "description": "Timezone of the inbox.",
"example": "America/New_York"
},
"allow_messages_after_resolved": {
"type": "boolean",
- "description": "Allow messages after conversation is resolved",
+ "description": "Allow messages after conversation is resolved.\n\nAvailable for: `Website`\n",
"example": true
},
"lock_to_single_conversation": {
"type": "boolean",
- "description": "Lock to single conversation",
+ "description": "Lock contact messages to a single active conversation.\n\nAvailable for: `API` `LINE` `Telegram` `WhatsApp` `SMS`\n",
"example": true
},
"portal_id": {
"type": "integer",
- "description": "Id of the help center portal to attach to the inbox",
+ "description": "Id of the help center portal to attach to the inbox.",
"example": 1
},
"sender_name_type": {
"type": "string",
- "description": "Sender name type for the inbox",
+ "description": "Sender name type for outbound email replies.\n\nAvailable for: `Website` `Email`\n",
"enum": [
"friendly",
"professional"
@@ -10599,45 +11011,43 @@
},
"business_name": {
"type": "string",
- "description": "Business name for the inbox",
+ "description": "Business name for outbound email replies.\n\nAvailable for: `Website` `Email`\n",
"example": "My Business"
},
"channel": {
- "type": "object",
- "properties": {
- "type": {
- "type": "string",
- "description": "Type of the channel",
- "enum": [
- "web_widget",
- "api",
- "email",
- "line",
- "telegram",
- "whatsapp",
- "sms"
- ],
- "example": "web_widget"
+ "oneOf": [
+ {
+ "$ref": "#/components/schemas/inbox_create_web_widget_channel_payload"
},
- "website_url": {
- "type": "string",
- "description": "URL at which the widget will be loaded",
- "example": "https://example.com"
+ {
+ "$ref": "#/components/schemas/inbox_create_api_channel_payload"
},
- "welcome_title": {
- "type": "string",
- "description": "Welcome title to be displayed on the widget",
- "example": "Welcome to our support"
+ {
+ "$ref": "#/components/schemas/inbox_create_email_channel_payload"
},
- "welcome_tagline": {
- "type": "string",
- "description": "Welcome tagline to be displayed on the widget",
- "example": "We are here to help you"
+ {
+ "$ref": "#/components/schemas/inbox_create_line_channel_payload"
},
- "widget_color": {
- "type": "string",
- "description": "A Hex-color string used to customize the widget",
- "example": "#FF5733"
+ {
+ "$ref": "#/components/schemas/inbox_create_telegram_channel_payload"
+ },
+ {
+ "$ref": "#/components/schemas/inbox_create_whatsapp_channel_payload"
+ },
+ {
+ "$ref": "#/components/schemas/inbox_create_sms_channel_payload"
+ }
+ ],
+ "discriminator": {
+ "propertyName": "type",
+ "mapping": {
+ "web_widget": "#/components/schemas/inbox_create_web_widget_channel_payload",
+ "api": "#/components/schemas/inbox_create_api_channel_payload",
+ "email": "#/components/schemas/inbox_create_email_channel_payload",
+ "line": "#/components/schemas/inbox_create_line_channel_payload",
+ "telegram": "#/components/schemas/inbox_create_telegram_channel_payload",
+ "whatsapp": "#/components/schemas/inbox_create_whatsapp_channel_payload",
+ "sms": "#/components/schemas/inbox_create_sms_channel_payload"
}
}
}
@@ -10648,72 +11058,121 @@
"properties": {
"name": {
"type": "string",
- "description": "The name of the inbox",
+ "description": "The name of the inbox.",
"example": "Support"
},
"avatar": {
"type": "string",
"format": "binary",
- "description": "Image file for avatar"
+ "description": "Image file for avatar."
},
"greeting_enabled": {
"type": "boolean",
- "description": "Enable greeting message",
+ "description": "Enable greeting message.",
"example": true
},
"greeting_message": {
"type": "string",
- "description": "Greeting message to be displayed on the widget",
+ "description": "Greeting message to send when greeting messages are enabled.",
"example": "Hello, how can I help you?"
},
"enable_email_collect": {
"type": "boolean",
- "description": "Enable email collection",
+ "description": "Enable email collection.\n\nAvailable for: `Website`\n",
"example": true
},
"csat_survey_enabled": {
"type": "boolean",
- "description": "Enable CSAT survey",
+ "description": "Enable CSAT survey.",
"example": true
},
+ "csat_config": {
+ "type": "object",
+ "description": "CSAT survey configuration.",
+ "properties": {
+ "display_type": {
+ "type": "string",
+ "description": "Display style for the CSAT survey.",
+ "enum": [
+ "emoji",
+ "star"
+ ],
+ "example": "emoji"
+ },
+ "message": {
+ "type": "string",
+ "description": "Message shown with the CSAT survey.",
+ "example": "Please rate your conversation"
+ },
+ "button_text": {
+ "type": "string",
+ "description": "Text shown on the CSAT survey button.",
+ "example": "Please rate us"
+ },
+ "language": {
+ "type": "string",
+ "description": "Language code for the CSAT survey.",
+ "example": "en"
+ },
+ "survey_rules": {
+ "type": "object",
+ "description": "Rules that decide when to show the CSAT survey.",
+ "properties": {
+ "operator": {
+ "type": "string",
+ "example": "contains"
+ },
+ "values": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "example": [
+ "billing"
+ ]
+ }
+ }
+ }
+ }
+ },
"enable_auto_assignment": {
"type": "boolean",
- "description": "Enable Auto Assignment",
+ "description": "Enable Auto Assignment.",
"example": true
},
"working_hours_enabled": {
"type": "boolean",
- "description": "Enable working hours",
+ "description": "Enable working hours.",
"example": true
},
"out_of_office_message": {
"type": "string",
- "description": "Out of office message to be displayed on the widget",
+ "description": "Out of office message to send outside working hours.",
"example": "We are currently out of office. Please leave a message and we will get back to you."
},
"timezone": {
"type": "string",
- "description": "Timezone of the inbox",
+ "description": "Timezone of the inbox.",
"example": "America/New_York"
},
"allow_messages_after_resolved": {
"type": "boolean",
- "description": "Allow messages after conversation is resolved",
+ "description": "Allow messages after conversation is resolved.\n\nAvailable for: `Website`\n",
"example": true
},
"lock_to_single_conversation": {
"type": "boolean",
- "description": "Lock to single conversation",
+ "description": "Lock contact messages to a single active conversation.\n\nAvailable for: `API` `LINE` `Telegram` `WhatsApp` `SMS`\n",
"example": true
},
"portal_id": {
"type": "integer",
- "description": "Id of the help center portal to attach to the inbox",
+ "description": "Id of the help center portal to attach to the inbox.",
"example": 1
},
"sender_name_type": {
"type": "string",
- "description": "Sender name type for the inbox",
+ "description": "Sender name type for outbound email replies.\n\nAvailable for: `Website` `Email`\n",
"enum": [
"friendly",
"professional"
@@ -10722,32 +11181,808 @@
},
"business_name": {
"type": "string",
- "description": "Business name for the inbox",
+ "description": "Business name for outbound email replies.\n\nAvailable for: `Website` `Email`\n",
"example": "My Business"
},
"channel": {
- "type": "object",
- "properties": {
- "website_url": {
- "type": "string",
- "description": "URL at which the widget will be loaded",
- "example": "https://example.com"
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/inbox_update_web_widget_channel_payload"
},
- "welcome_title": {
- "type": "string",
- "description": "Welcome title to be displayed on the widget",
- "example": "Welcome to our support"
+ {
+ "$ref": "#/components/schemas/inbox_update_api_channel_payload"
},
- "welcome_tagline": {
- "type": "string",
- "description": "Welcome tagline to be displayed on the widget",
- "example": "We are here to help you"
+ {
+ "$ref": "#/components/schemas/inbox_update_email_channel_payload"
},
- "widget_color": {
- "type": "string",
- "description": "A Hex-color string used to customize the widget",
- "example": "#FF5733"
+ {
+ "$ref": "#/components/schemas/inbox_update_line_channel_payload"
+ },
+ {
+ "$ref": "#/components/schemas/inbox_update_telegram_channel_payload"
+ },
+ {
+ "$ref": "#/components/schemas/inbox_update_whatsapp_channel_payload"
+ },
+ {
+ "$ref": "#/components/schemas/inbox_update_sms_channel_payload"
}
+ ]
+ }
+ }
+ },
+ "inbox_create_web_widget_channel_payload": {
+ "type": "object",
+ "title": "Website channel",
+ "required": [
+ "type",
+ "website_url"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": [
+ "web_widget"
+ ],
+ "example": "web_widget"
+ },
+ "website_url": {
+ "type": "string",
+ "description": "URL at which the widget will be loaded",
+ "example": "https://example.com"
+ },
+ "welcome_title": {
+ "type": "string",
+ "description": "Welcome title to be displayed on the widget",
+ "example": "Welcome to our support"
+ },
+ "welcome_tagline": {
+ "type": "string",
+ "description": "Welcome tagline to be displayed on the widget",
+ "example": "We are here to help you"
+ },
+ "widget_color": {
+ "type": "string",
+ "description": "A Hex-color string used to customize the widget",
+ "example": "#FF5733"
+ },
+ "reply_time": {
+ "type": "string",
+ "description": "Expected reply time shown on the widget",
+ "enum": [
+ "in_a_few_minutes",
+ "in_a_few_hours",
+ "in_a_day"
+ ],
+ "example": "in_a_few_minutes"
+ },
+ "pre_chat_form_enabled": {
+ "type": "boolean",
+ "description": "Enable the pre-chat form before starting a conversation",
+ "example": false
+ },
+ "pre_chat_form_options": {
+ "type": "object",
+ "description": "Pre-chat form configuration",
+ "example": {
+ "pre_chat_message": "Share your queries or comments here.",
+ "pre_chat_fields": [
+ {
+ "field_type": "standard",
+ "label": "Email Id",
+ "name": "emailAddress",
+ "type": "email",
+ "required": true,
+ "enabled": true
+ }
+ ]
+ }
+ },
+ "continuity_via_email": {
+ "type": "boolean",
+ "description": "Continue conversations over email when the contact leaves the website",
+ "example": true
+ },
+ "hmac_mandatory": {
+ "type": "boolean",
+ "description": "Require HMAC verification for contacts using the widget",
+ "example": false
+ },
+ "allowed_domains": {
+ "type": "string",
+ "description": "Comma-separated list of domains where the widget is allowed to load",
+ "example": "example.com"
+ },
+ "selected_feature_flags": {
+ "type": "array",
+ "description": "Enabled widget feature flags",
+ "items": {
+ "type": "string",
+ "enum": [
+ "attachments",
+ "emoji_picker",
+ "end_conversation",
+ "use_inbox_avatar_for_bot",
+ "allow_mobile_webview"
+ ]
+ },
+ "example": [
+ "attachments",
+ "emoji_picker",
+ "end_conversation"
+ ]
+ }
+ }
+ },
+ "inbox_create_api_channel_payload": {
+ "type": "object",
+ "title": "API channel",
+ "required": [
+ "type"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": [
+ "api"
+ ],
+ "example": "api"
+ },
+ "webhook_url": {
+ "type": "string",
+ "description": "Webhook URL for API channel inbox callbacks",
+ "example": "https://example.com/webhook"
+ },
+ "hmac_mandatory": {
+ "type": "boolean",
+ "description": "Require HMAC verification for incoming API channel messages",
+ "example": false
+ },
+ "additional_attributes": {
+ "type": "object",
+ "description": "Additional attributes stored on contacts created through the API channel",
+ "example": {
+ "source": "mobile_app"
+ }
+ }
+ }
+ },
+ "inbox_create_email_channel_payload": {
+ "type": "object",
+ "title": "Email channel",
+ "required": [
+ "type",
+ "email"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": [
+ "email"
+ ],
+ "example": "email"
+ },
+ "email": {
+ "type": "string",
+ "description": "Email address for the inbox",
+ "example": "support@example.com"
+ },
+ "imap_enabled": {
+ "type": "boolean",
+ "description": "Enable IMAP for inbound emails",
+ "example": true
+ },
+ "imap_login": {
+ "type": "string",
+ "description": "IMAP login username",
+ "example": "support@example.com"
+ },
+ "imap_password": {
+ "type": "string",
+ "description": "IMAP login password",
+ "example": "your-imap-password"
+ },
+ "imap_address": {
+ "type": "string",
+ "description": "IMAP server address",
+ "example": "imap.example.com"
+ },
+ "imap_port": {
+ "type": "integer",
+ "description": "IMAP server port",
+ "example": 993
+ },
+ "imap_enable_ssl": {
+ "type": "boolean",
+ "description": "Enable SSL for IMAP",
+ "example": true
+ },
+ "imap_authentication": {
+ "type": "string",
+ "description": "IMAP authentication method",
+ "example": "plain"
+ },
+ "smtp_enabled": {
+ "type": "boolean",
+ "description": "Enable SMTP for outbound emails",
+ "example": true
+ },
+ "smtp_login": {
+ "type": "string",
+ "description": "SMTP login username",
+ "example": "support@example.com"
+ },
+ "smtp_password": {
+ "type": "string",
+ "description": "SMTP login password",
+ "example": "your-smtp-password"
+ },
+ "smtp_address": {
+ "type": "string",
+ "description": "SMTP server address",
+ "example": "smtp.example.com"
+ },
+ "smtp_port": {
+ "type": "integer",
+ "description": "SMTP server port",
+ "example": 587
+ },
+ "smtp_domain": {
+ "type": "string",
+ "description": "SMTP HELO domain",
+ "example": "example.com"
+ },
+ "smtp_enable_starttls_auto": {
+ "type": "boolean",
+ "description": "Automatically enable STARTTLS for SMTP",
+ "example": true
+ },
+ "smtp_enable_ssl_tls": {
+ "type": "boolean",
+ "description": "Enable SSL/TLS for SMTP",
+ "example": false
+ },
+ "smtp_openssl_verify_mode": {
+ "type": "string",
+ "description": "OpenSSL certificate verification mode for SMTP",
+ "example": "none"
+ },
+ "smtp_authentication": {
+ "type": "string",
+ "description": "SMTP authentication method",
+ "example": "login"
+ },
+ "provider": {
+ "type": "string",
+ "description": "Email provider",
+ "example": "google"
+ },
+ "verified_for_sending": {
+ "type": "boolean",
+ "description": "Whether the inbox is verified for sending emails",
+ "example": false
+ }
+ }
+ },
+ "inbox_create_line_channel_payload": {
+ "type": "object",
+ "title": "LINE channel",
+ "required": [
+ "type",
+ "line_channel_id",
+ "line_channel_secret",
+ "line_channel_token"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": [
+ "line"
+ ],
+ "example": "line"
+ },
+ "line_channel_id": {
+ "type": "string",
+ "description": "LINE channel ID",
+ "example": "1234567890"
+ },
+ "line_channel_secret": {
+ "type": "string",
+ "description": "LINE channel secret",
+ "example": "line-channel-secret"
+ },
+ "line_channel_token": {
+ "type": "string",
+ "description": "LINE channel access token",
+ "example": "line-channel-token"
+ }
+ }
+ },
+ "inbox_create_telegram_channel_payload": {
+ "type": "object",
+ "title": "Telegram channel",
+ "required": [
+ "type",
+ "bot_token"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": [
+ "telegram"
+ ],
+ "example": "telegram"
+ },
+ "bot_token": {
+ "type": "string",
+ "description": "Telegram bot token",
+ "example": "123456789:telegram-bot-token"
+ }
+ }
+ },
+ "inbox_create_whatsapp_channel_payload": {
+ "oneOf": [
+ {
+ "type": "object",
+ "title": "WhatsApp Cloud channel",
+ "required": [
+ "type",
+ "phone_number",
+ "provider",
+ "provider_config"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": [
+ "whatsapp"
+ ],
+ "example": "whatsapp"
+ },
+ "phone_number": {
+ "type": "string",
+ "description": "WhatsApp phone number",
+ "example": "+15551234567"
+ },
+ "provider": {
+ "type": "string",
+ "description": "WhatsApp provider",
+ "enum": [
+ "whatsapp_cloud"
+ ],
+ "example": "whatsapp_cloud"
+ },
+ "provider_config": {
+ "type": "object",
+ "description": "WhatsApp Cloud provider configuration",
+ "required": [
+ "api_key",
+ "phone_number_id",
+ "business_account_id"
+ ],
+ "properties": {
+ "api_key": {
+ "type": "string",
+ "description": "WhatsApp Cloud API key",
+ "example": "your-api-key"
+ },
+ "phone_number_id": {
+ "type": "string",
+ "description": "Phone number ID for WhatsApp Cloud",
+ "example": "your-phone-number-id"
+ },
+ "business_account_id": {
+ "type": "string",
+ "description": "Business account ID for WhatsApp Cloud",
+ "example": "your-business-account-id"
+ }
+ },
+ "example": {
+ "api_key": "your-api-key",
+ "phone_number_id": "your-phone-number-id",
+ "business_account_id": "your-business-account-id"
+ }
+ }
+ }
+ },
+ {
+ "type": "object",
+ "title": "Legacy 360dialog WhatsApp channel",
+ "deprecated": true,
+ "required": [
+ "type",
+ "phone_number",
+ "provider_config"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": [
+ "whatsapp"
+ ],
+ "example": "whatsapp"
+ },
+ "phone_number": {
+ "type": "string",
+ "description": "WhatsApp phone number",
+ "example": "+15551234567"
+ },
+ "provider": {
+ "type": "string",
+ "description": "Legacy 360dialog provider. Omit this field or use `default` only for existing deprecated 360dialog setups.",
+ "enum": [
+ "default"
+ ],
+ "deprecated": true,
+ "example": "default"
+ },
+ "provider_config": {
+ "type": "object",
+ "description": "Legacy 360dialog provider configuration",
+ "required": [
+ "api_key"
+ ],
+ "properties": {
+ "api_key": {
+ "type": "string",
+ "description": "360dialog API key",
+ "example": "your-api-key"
+ }
+ },
+ "example": {
+ "api_key": "your-api-key"
+ }
+ }
+ }
+ }
+ ]
+ },
+ "inbox_create_sms_channel_payload": {
+ "type": "object",
+ "title": "SMS channel",
+ "required": [
+ "type",
+ "phone_number"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": [
+ "sms"
+ ],
+ "example": "sms"
+ },
+ "phone_number": {
+ "type": "string",
+ "description": "SMS phone number",
+ "example": "+15551234567"
+ },
+ "provider_config": {
+ "type": "object",
+ "description": "Provider-specific SMS configuration",
+ "example": {
+ "account_id": "your-account-id",
+ "application_id": "your-application-id"
+ }
+ }
+ }
+ },
+ "inbox_update_web_widget_channel_payload": {
+ "type": "object",
+ "title": "Website channel settings",
+ "properties": {
+ "website_url": {
+ "type": "string",
+ "description": "URL at which the widget will be loaded",
+ "example": "https://example.com"
+ },
+ "welcome_title": {
+ "type": "string",
+ "description": "Welcome title to be displayed on the widget",
+ "example": "Welcome to our support"
+ },
+ "welcome_tagline": {
+ "type": "string",
+ "description": "Welcome tagline to be displayed on the widget",
+ "example": "We are here to help you"
+ },
+ "widget_color": {
+ "type": "string",
+ "description": "A Hex-color string used to customize the widget",
+ "example": "#FF5733"
+ },
+ "reply_time": {
+ "type": "string",
+ "description": "Expected reply time shown on the widget",
+ "enum": [
+ "in_a_few_minutes",
+ "in_a_few_hours",
+ "in_a_day"
+ ],
+ "example": "in_a_few_minutes"
+ },
+ "pre_chat_form_enabled": {
+ "type": "boolean",
+ "description": "Enable the pre-chat form before starting a conversation",
+ "example": false
+ },
+ "pre_chat_form_options": {
+ "type": "object",
+ "description": "Pre-chat form configuration",
+ "example": {
+ "pre_chat_message": "Share your queries or comments here.",
+ "pre_chat_fields": [
+ {
+ "field_type": "standard",
+ "label": "Email Id",
+ "name": "emailAddress",
+ "type": "email",
+ "required": true,
+ "enabled": true
+ }
+ ]
+ }
+ },
+ "continuity_via_email": {
+ "type": "boolean",
+ "description": "Continue conversations over email when the contact leaves the website",
+ "example": true
+ },
+ "hmac_mandatory": {
+ "type": "boolean",
+ "description": "Require HMAC verification for contacts using the widget",
+ "example": false
+ },
+ "allowed_domains": {
+ "type": "string",
+ "description": "Comma-separated list of domains where the widget is allowed to load",
+ "example": "example.com"
+ },
+ "selected_feature_flags": {
+ "type": "array",
+ "description": "Enabled widget feature flags",
+ "items": {
+ "type": "string",
+ "enum": [
+ "attachments",
+ "emoji_picker",
+ "end_conversation",
+ "use_inbox_avatar_for_bot",
+ "allow_mobile_webview"
+ ]
+ },
+ "example": [
+ "attachments",
+ "emoji_picker",
+ "end_conversation"
+ ]
+ }
+ }
+ },
+ "inbox_update_api_channel_payload": {
+ "type": "object",
+ "title": "API channel settings",
+ "properties": {
+ "webhook_url": {
+ "type": "string",
+ "description": "Webhook URL for API channel inbox callbacks",
+ "example": "https://example.com/webhook"
+ },
+ "hmac_mandatory": {
+ "type": "boolean",
+ "description": "Require HMAC verification for incoming API channel messages",
+ "example": false
+ },
+ "additional_attributes": {
+ "type": "object",
+ "description": "Additional attributes stored on contacts created through the API channel",
+ "example": {
+ "source": "mobile_app"
+ }
+ }
+ }
+ },
+ "inbox_update_email_channel_payload": {
+ "type": "object",
+ "title": "Email channel settings",
+ "properties": {
+ "email": {
+ "type": "string",
+ "description": "Email address for the inbox",
+ "example": "support@example.com"
+ },
+ "imap_enabled": {
+ "type": "boolean",
+ "description": "Enable IMAP for inbound emails",
+ "example": true
+ },
+ "imap_login": {
+ "type": "string",
+ "description": "IMAP login username",
+ "example": "support@example.com"
+ },
+ "imap_password": {
+ "type": "string",
+ "description": "IMAP login password",
+ "example": "your-imap-password"
+ },
+ "imap_address": {
+ "type": "string",
+ "description": "IMAP server address",
+ "example": "imap.example.com"
+ },
+ "imap_port": {
+ "type": "integer",
+ "description": "IMAP server port",
+ "example": 993
+ },
+ "imap_enable_ssl": {
+ "type": "boolean",
+ "description": "Enable SSL for IMAP",
+ "example": true
+ },
+ "imap_authentication": {
+ "type": "string",
+ "description": "IMAP authentication method",
+ "example": "plain"
+ },
+ "smtp_enabled": {
+ "type": "boolean",
+ "description": "Enable SMTP for outbound emails",
+ "example": true
+ },
+ "smtp_login": {
+ "type": "string",
+ "description": "SMTP login username",
+ "example": "support@example.com"
+ },
+ "smtp_password": {
+ "type": "string",
+ "description": "SMTP login password",
+ "example": "your-smtp-password"
+ },
+ "smtp_address": {
+ "type": "string",
+ "description": "SMTP server address",
+ "example": "smtp.example.com"
+ },
+ "smtp_port": {
+ "type": "integer",
+ "description": "SMTP server port",
+ "example": 587
+ },
+ "smtp_domain": {
+ "type": "string",
+ "description": "SMTP HELO domain",
+ "example": "example.com"
+ },
+ "smtp_enable_starttls_auto": {
+ "type": "boolean",
+ "description": "Automatically enable STARTTLS for SMTP",
+ "example": true
+ },
+ "smtp_enable_ssl_tls": {
+ "type": "boolean",
+ "description": "Enable SSL/TLS for SMTP",
+ "example": false
+ },
+ "smtp_openssl_verify_mode": {
+ "type": "string",
+ "description": "OpenSSL certificate verification mode for SMTP",
+ "example": "none"
+ },
+ "smtp_authentication": {
+ "type": "string",
+ "description": "SMTP authentication method",
+ "example": "login"
+ },
+ "provider": {
+ "type": "string",
+ "description": "Email provider",
+ "example": "google"
+ },
+ "verified_for_sending": {
+ "type": "boolean",
+ "description": "Whether the inbox is verified for sending emails",
+ "example": false
+ }
+ }
+ },
+ "inbox_update_line_channel_payload": {
+ "type": "object",
+ "title": "LINE channel settings",
+ "properties": {
+ "line_channel_id": {
+ "type": "string",
+ "description": "LINE channel ID",
+ "example": "1234567890"
+ },
+ "line_channel_secret": {
+ "type": "string",
+ "description": "LINE channel secret",
+ "example": "line-channel-secret"
+ },
+ "line_channel_token": {
+ "type": "string",
+ "description": "LINE channel access token",
+ "example": "line-channel-token"
+ }
+ }
+ },
+ "inbox_update_telegram_channel_payload": {
+ "type": "object",
+ "title": "Telegram channel settings",
+ "properties": {
+ "bot_token": {
+ "type": "string",
+ "description": "Telegram bot token",
+ "example": "123456789:telegram-bot-token"
+ }
+ }
+ },
+ "inbox_update_whatsapp_channel_payload": {
+ "type": "object",
+ "title": "WhatsApp channel settings",
+ "properties": {
+ "phone_number": {
+ "type": "string",
+ "description": "WhatsApp phone number",
+ "example": "+15551234567"
+ },
+ "provider": {
+ "type": "string",
+ "description": "WhatsApp provider. `default` is supported only for existing deprecated 360dialog setups.",
+ "enum": [
+ "whatsapp_cloud",
+ "default"
+ ],
+ "example": "whatsapp_cloud"
+ },
+ "provider_config": {
+ "type": "object",
+ "description": "WhatsApp provider configuration. Cloud channels use `api_key`, `phone_number_id`, and `business_account_id`; legacy 360dialog channels use `api_key`.",
+ "properties": {
+ "api_key": {
+ "type": "string",
+ "description": "Provider API key",
+ "example": "your-api-key"
+ },
+ "phone_number_id": {
+ "type": "string",
+ "description": "Phone number ID for WhatsApp Cloud",
+ "example": "your-phone-number-id"
+ },
+ "business_account_id": {
+ "type": "string",
+ "description": "Business account ID for WhatsApp Cloud",
+ "example": "your-business-account-id"
+ }
+ },
+ "example": {
+ "api_key": "your-api-key",
+ "phone_number_id": "your-phone-number-id",
+ "business_account_id": "your-business-account-id"
+ }
+ }
+ }
+ },
+ "inbox_update_sms_channel_payload": {
+ "type": "object",
+ "title": "SMS channel settings",
+ "properties": {
+ "phone_number": {
+ "type": "string",
+ "description": "SMS phone number",
+ "example": "+15551234567"
+ },
+ "provider_config": {
+ "type": "object",
+ "description": "Provider-specific SMS configuration",
+ "example": {
+ "api_key": "your-api-key",
+ "api_secret": "your-api-secret",
+ "application_id": "your-application-id",
+ "account_id": "your-account-id"
}
}
}
@@ -10846,7 +12081,9 @@
"message_updated",
"contact_created",
"contact_updated",
- "webwidget_triggered"
+ "webwidget_triggered",
+ "conversation_typing_on",
+ "conversation_typing_off"
]
},
"description": "The events you want to subscribe to.",
diff --git a/swagger/tag_groups/client_swagger.json b/swagger/tag_groups/client_swagger.json
index 763e090b1..b810a4308 100644
--- a/swagger/tag_groups/client_swagger.json
+++ b/swagger/tag_groups/client_swagger.json
@@ -1664,15 +1664,16 @@
"availability_status": {
"type": "string",
"enum": [
- "available",
+ "online",
"busy",
"offline"
],
- "description": "The availability status of the agent computed by Chatwoot."
+ "readOnly": true,
+ "description": "The effective availability status of the agent, derived from the configured availability, auto-offline setting, and current presence. To update an agent's configured availability, use the availability field in create or update requests."
},
"auto_offline": {
"type": "boolean",
- "description": "Whether the availability status of agent is configured to go offline automatically when away."
+ "description": "Whether the agent is automatically marked offline when they are away."
},
"confirmed": {
"type": "boolean",
@@ -2087,11 +2088,18 @@
"contact_updated",
"message_created",
"message_updated",
- "webwidget_triggered"
+ "webwidget_triggered",
+ "conversation_typing_on",
+ "conversation_typing_off"
]
},
"description": "The list of subscribed events"
},
+ "secret": {
+ "type": "string",
+ "nullable": true,
+ "description": "Secret used to sign webhook requests. Signed webhook deliveries include `X-Chatwoot-Timestamp` and `X-Chatwoot-Signature`; the signature is `sha256=` followed by the HMAC-SHA256 of `{timestamp}.{raw_request_body}` using this secret. Deliveries also include `X-Chatwoot-Delivery` when a delivery id is available."
+ },
"account_id": {
"type": "number",
"description": "The id of the account which the webhook object belongs to"
@@ -3367,19 +3375,19 @@
"description": "Whether its administrator or agent",
"example": "agent"
},
- "availability_status": {
+ "availability": {
"type": "string",
"enum": [
- "available",
+ "online",
"busy",
"offline"
],
- "description": "The availability setting of the agent.",
- "example": "available"
+ "description": "The configured availability of the agent.",
+ "example": "online"
},
"auto_offline": {
"type": "boolean",
- "description": "Whether the availability status of agent is configured to go offline automatically when away.",
+ "description": "Whether the agent is automatically marked offline when they are away.",
"example": true
}
}
@@ -3399,19 +3407,19 @@
"description": "Whether its administrator or agent",
"example": "agent"
},
- "availability_status": {
+ "availability": {
"type": "string",
"enum": [
- "available",
+ "online",
"busy",
"offline"
],
- "description": "The availability status of the agent.",
- "example": "available"
+ "description": "The configured availability of the agent.",
+ "example": "online"
},
"auto_offline": {
"type": "boolean",
- "description": "Whether the availability status of agent is configured to go offline automatically when away.",
+ "description": "Whether the agent is automatically marked offline when they are away.",
"example": true
}
}
@@ -3790,72 +3798,121 @@
"properties": {
"name": {
"type": "string",
- "description": "The name of the inbox",
+ "description": "The name of the inbox.",
"example": "Support"
},
"avatar": {
"type": "string",
"format": "binary",
- "description": "Image file for avatar"
+ "description": "Image file for avatar."
},
"greeting_enabled": {
"type": "boolean",
- "description": "Enable greeting message",
+ "description": "Enable greeting message.",
"example": true
},
"greeting_message": {
"type": "string",
- "description": "Greeting message to be displayed on the widget",
+ "description": "Greeting message to send when greeting messages are enabled.",
"example": "Hello, how can I help you?"
},
"enable_email_collect": {
"type": "boolean",
- "description": "Enable email collection",
+ "description": "Enable email collection.\n\nAvailable for: `Website`\n",
"example": true
},
"csat_survey_enabled": {
"type": "boolean",
- "description": "Enable CSAT survey",
+ "description": "Enable CSAT survey.",
"example": true
},
+ "csat_config": {
+ "type": "object",
+ "description": "CSAT survey configuration.",
+ "properties": {
+ "display_type": {
+ "type": "string",
+ "description": "Display style for the CSAT survey.",
+ "enum": [
+ "emoji",
+ "star"
+ ],
+ "example": "emoji"
+ },
+ "message": {
+ "type": "string",
+ "description": "Message shown with the CSAT survey.",
+ "example": "Please rate your conversation"
+ },
+ "button_text": {
+ "type": "string",
+ "description": "Text shown on the CSAT survey button.",
+ "example": "Please rate us"
+ },
+ "language": {
+ "type": "string",
+ "description": "Language code for the CSAT survey.",
+ "example": "en"
+ },
+ "survey_rules": {
+ "type": "object",
+ "description": "Rules that decide when to show the CSAT survey.",
+ "properties": {
+ "operator": {
+ "type": "string",
+ "example": "contains"
+ },
+ "values": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "example": [
+ "billing"
+ ]
+ }
+ }
+ }
+ }
+ },
"enable_auto_assignment": {
"type": "boolean",
- "description": "Enable Auto Assignment",
+ "description": "Enable Auto Assignment.",
"example": true
},
"working_hours_enabled": {
"type": "boolean",
- "description": "Enable working hours",
+ "description": "Enable working hours.",
"example": true
},
"out_of_office_message": {
"type": "string",
- "description": "Out of office message to be displayed on the widget",
+ "description": "Out of office message to send outside working hours.",
"example": "We are currently out of office. Please leave a message and we will get back to you."
},
"timezone": {
"type": "string",
- "description": "Timezone of the inbox",
+ "description": "Timezone of the inbox.",
"example": "America/New_York"
},
"allow_messages_after_resolved": {
"type": "boolean",
- "description": "Allow messages after conversation is resolved",
+ "description": "Allow messages after conversation is resolved.\n\nAvailable for: `Website`\n",
"example": true
},
"lock_to_single_conversation": {
"type": "boolean",
- "description": "Lock to single conversation",
+ "description": "Lock contact messages to a single active conversation.\n\nAvailable for: `API` `LINE` `Telegram` `WhatsApp` `SMS`\n",
"example": true
},
"portal_id": {
"type": "integer",
- "description": "Id of the help center portal to attach to the inbox",
+ "description": "Id of the help center portal to attach to the inbox.",
"example": 1
},
"sender_name_type": {
"type": "string",
- "description": "Sender name type for the inbox",
+ "description": "Sender name type for outbound email replies.\n\nAvailable for: `Website` `Email`\n",
"enum": [
"friendly",
"professional"
@@ -3864,45 +3921,43 @@
},
"business_name": {
"type": "string",
- "description": "Business name for the inbox",
+ "description": "Business name for outbound email replies.\n\nAvailable for: `Website` `Email`\n",
"example": "My Business"
},
"channel": {
- "type": "object",
- "properties": {
- "type": {
- "type": "string",
- "description": "Type of the channel",
- "enum": [
- "web_widget",
- "api",
- "email",
- "line",
- "telegram",
- "whatsapp",
- "sms"
- ],
- "example": "web_widget"
+ "oneOf": [
+ {
+ "$ref": "#/components/schemas/inbox_create_web_widget_channel_payload"
},
- "website_url": {
- "type": "string",
- "description": "URL at which the widget will be loaded",
- "example": "https://example.com"
+ {
+ "$ref": "#/components/schemas/inbox_create_api_channel_payload"
},
- "welcome_title": {
- "type": "string",
- "description": "Welcome title to be displayed on the widget",
- "example": "Welcome to our support"
+ {
+ "$ref": "#/components/schemas/inbox_create_email_channel_payload"
},
- "welcome_tagline": {
- "type": "string",
- "description": "Welcome tagline to be displayed on the widget",
- "example": "We are here to help you"
+ {
+ "$ref": "#/components/schemas/inbox_create_line_channel_payload"
},
- "widget_color": {
- "type": "string",
- "description": "A Hex-color string used to customize the widget",
- "example": "#FF5733"
+ {
+ "$ref": "#/components/schemas/inbox_create_telegram_channel_payload"
+ },
+ {
+ "$ref": "#/components/schemas/inbox_create_whatsapp_channel_payload"
+ },
+ {
+ "$ref": "#/components/schemas/inbox_create_sms_channel_payload"
+ }
+ ],
+ "discriminator": {
+ "propertyName": "type",
+ "mapping": {
+ "web_widget": "#/components/schemas/inbox_create_web_widget_channel_payload",
+ "api": "#/components/schemas/inbox_create_api_channel_payload",
+ "email": "#/components/schemas/inbox_create_email_channel_payload",
+ "line": "#/components/schemas/inbox_create_line_channel_payload",
+ "telegram": "#/components/schemas/inbox_create_telegram_channel_payload",
+ "whatsapp": "#/components/schemas/inbox_create_whatsapp_channel_payload",
+ "sms": "#/components/schemas/inbox_create_sms_channel_payload"
}
}
}
@@ -3913,72 +3968,121 @@
"properties": {
"name": {
"type": "string",
- "description": "The name of the inbox",
+ "description": "The name of the inbox.",
"example": "Support"
},
"avatar": {
"type": "string",
"format": "binary",
- "description": "Image file for avatar"
+ "description": "Image file for avatar."
},
"greeting_enabled": {
"type": "boolean",
- "description": "Enable greeting message",
+ "description": "Enable greeting message.",
"example": true
},
"greeting_message": {
"type": "string",
- "description": "Greeting message to be displayed on the widget",
+ "description": "Greeting message to send when greeting messages are enabled.",
"example": "Hello, how can I help you?"
},
"enable_email_collect": {
"type": "boolean",
- "description": "Enable email collection",
+ "description": "Enable email collection.\n\nAvailable for: `Website`\n",
"example": true
},
"csat_survey_enabled": {
"type": "boolean",
- "description": "Enable CSAT survey",
+ "description": "Enable CSAT survey.",
"example": true
},
+ "csat_config": {
+ "type": "object",
+ "description": "CSAT survey configuration.",
+ "properties": {
+ "display_type": {
+ "type": "string",
+ "description": "Display style for the CSAT survey.",
+ "enum": [
+ "emoji",
+ "star"
+ ],
+ "example": "emoji"
+ },
+ "message": {
+ "type": "string",
+ "description": "Message shown with the CSAT survey.",
+ "example": "Please rate your conversation"
+ },
+ "button_text": {
+ "type": "string",
+ "description": "Text shown on the CSAT survey button.",
+ "example": "Please rate us"
+ },
+ "language": {
+ "type": "string",
+ "description": "Language code for the CSAT survey.",
+ "example": "en"
+ },
+ "survey_rules": {
+ "type": "object",
+ "description": "Rules that decide when to show the CSAT survey.",
+ "properties": {
+ "operator": {
+ "type": "string",
+ "example": "contains"
+ },
+ "values": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "example": [
+ "billing"
+ ]
+ }
+ }
+ }
+ }
+ },
"enable_auto_assignment": {
"type": "boolean",
- "description": "Enable Auto Assignment",
+ "description": "Enable Auto Assignment.",
"example": true
},
"working_hours_enabled": {
"type": "boolean",
- "description": "Enable working hours",
+ "description": "Enable working hours.",
"example": true
},
"out_of_office_message": {
"type": "string",
- "description": "Out of office message to be displayed on the widget",
+ "description": "Out of office message to send outside working hours.",
"example": "We are currently out of office. Please leave a message and we will get back to you."
},
"timezone": {
"type": "string",
- "description": "Timezone of the inbox",
+ "description": "Timezone of the inbox.",
"example": "America/New_York"
},
"allow_messages_after_resolved": {
"type": "boolean",
- "description": "Allow messages after conversation is resolved",
+ "description": "Allow messages after conversation is resolved.\n\nAvailable for: `Website`\n",
"example": true
},
"lock_to_single_conversation": {
"type": "boolean",
- "description": "Lock to single conversation",
+ "description": "Lock contact messages to a single active conversation.\n\nAvailable for: `API` `LINE` `Telegram` `WhatsApp` `SMS`\n",
"example": true
},
"portal_id": {
"type": "integer",
- "description": "Id of the help center portal to attach to the inbox",
+ "description": "Id of the help center portal to attach to the inbox.",
"example": 1
},
"sender_name_type": {
"type": "string",
- "description": "Sender name type for the inbox",
+ "description": "Sender name type for outbound email replies.\n\nAvailable for: `Website` `Email`\n",
"enum": [
"friendly",
"professional"
@@ -3987,32 +4091,808 @@
},
"business_name": {
"type": "string",
- "description": "Business name for the inbox",
+ "description": "Business name for outbound email replies.\n\nAvailable for: `Website` `Email`\n",
"example": "My Business"
},
"channel": {
- "type": "object",
- "properties": {
- "website_url": {
- "type": "string",
- "description": "URL at which the widget will be loaded",
- "example": "https://example.com"
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/inbox_update_web_widget_channel_payload"
},
- "welcome_title": {
- "type": "string",
- "description": "Welcome title to be displayed on the widget",
- "example": "Welcome to our support"
+ {
+ "$ref": "#/components/schemas/inbox_update_api_channel_payload"
},
- "welcome_tagline": {
- "type": "string",
- "description": "Welcome tagline to be displayed on the widget",
- "example": "We are here to help you"
+ {
+ "$ref": "#/components/schemas/inbox_update_email_channel_payload"
},
- "widget_color": {
- "type": "string",
- "description": "A Hex-color string used to customize the widget",
- "example": "#FF5733"
+ {
+ "$ref": "#/components/schemas/inbox_update_line_channel_payload"
+ },
+ {
+ "$ref": "#/components/schemas/inbox_update_telegram_channel_payload"
+ },
+ {
+ "$ref": "#/components/schemas/inbox_update_whatsapp_channel_payload"
+ },
+ {
+ "$ref": "#/components/schemas/inbox_update_sms_channel_payload"
}
+ ]
+ }
+ }
+ },
+ "inbox_create_web_widget_channel_payload": {
+ "type": "object",
+ "title": "Website channel",
+ "required": [
+ "type",
+ "website_url"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": [
+ "web_widget"
+ ],
+ "example": "web_widget"
+ },
+ "website_url": {
+ "type": "string",
+ "description": "URL at which the widget will be loaded",
+ "example": "https://example.com"
+ },
+ "welcome_title": {
+ "type": "string",
+ "description": "Welcome title to be displayed on the widget",
+ "example": "Welcome to our support"
+ },
+ "welcome_tagline": {
+ "type": "string",
+ "description": "Welcome tagline to be displayed on the widget",
+ "example": "We are here to help you"
+ },
+ "widget_color": {
+ "type": "string",
+ "description": "A Hex-color string used to customize the widget",
+ "example": "#FF5733"
+ },
+ "reply_time": {
+ "type": "string",
+ "description": "Expected reply time shown on the widget",
+ "enum": [
+ "in_a_few_minutes",
+ "in_a_few_hours",
+ "in_a_day"
+ ],
+ "example": "in_a_few_minutes"
+ },
+ "pre_chat_form_enabled": {
+ "type": "boolean",
+ "description": "Enable the pre-chat form before starting a conversation",
+ "example": false
+ },
+ "pre_chat_form_options": {
+ "type": "object",
+ "description": "Pre-chat form configuration",
+ "example": {
+ "pre_chat_message": "Share your queries or comments here.",
+ "pre_chat_fields": [
+ {
+ "field_type": "standard",
+ "label": "Email Id",
+ "name": "emailAddress",
+ "type": "email",
+ "required": true,
+ "enabled": true
+ }
+ ]
+ }
+ },
+ "continuity_via_email": {
+ "type": "boolean",
+ "description": "Continue conversations over email when the contact leaves the website",
+ "example": true
+ },
+ "hmac_mandatory": {
+ "type": "boolean",
+ "description": "Require HMAC verification for contacts using the widget",
+ "example": false
+ },
+ "allowed_domains": {
+ "type": "string",
+ "description": "Comma-separated list of domains where the widget is allowed to load",
+ "example": "example.com"
+ },
+ "selected_feature_flags": {
+ "type": "array",
+ "description": "Enabled widget feature flags",
+ "items": {
+ "type": "string",
+ "enum": [
+ "attachments",
+ "emoji_picker",
+ "end_conversation",
+ "use_inbox_avatar_for_bot",
+ "allow_mobile_webview"
+ ]
+ },
+ "example": [
+ "attachments",
+ "emoji_picker",
+ "end_conversation"
+ ]
+ }
+ }
+ },
+ "inbox_create_api_channel_payload": {
+ "type": "object",
+ "title": "API channel",
+ "required": [
+ "type"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": [
+ "api"
+ ],
+ "example": "api"
+ },
+ "webhook_url": {
+ "type": "string",
+ "description": "Webhook URL for API channel inbox callbacks",
+ "example": "https://example.com/webhook"
+ },
+ "hmac_mandatory": {
+ "type": "boolean",
+ "description": "Require HMAC verification for incoming API channel messages",
+ "example": false
+ },
+ "additional_attributes": {
+ "type": "object",
+ "description": "Additional attributes stored on contacts created through the API channel",
+ "example": {
+ "source": "mobile_app"
+ }
+ }
+ }
+ },
+ "inbox_create_email_channel_payload": {
+ "type": "object",
+ "title": "Email channel",
+ "required": [
+ "type",
+ "email"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": [
+ "email"
+ ],
+ "example": "email"
+ },
+ "email": {
+ "type": "string",
+ "description": "Email address for the inbox",
+ "example": "support@example.com"
+ },
+ "imap_enabled": {
+ "type": "boolean",
+ "description": "Enable IMAP for inbound emails",
+ "example": true
+ },
+ "imap_login": {
+ "type": "string",
+ "description": "IMAP login username",
+ "example": "support@example.com"
+ },
+ "imap_password": {
+ "type": "string",
+ "description": "IMAP login password",
+ "example": "your-imap-password"
+ },
+ "imap_address": {
+ "type": "string",
+ "description": "IMAP server address",
+ "example": "imap.example.com"
+ },
+ "imap_port": {
+ "type": "integer",
+ "description": "IMAP server port",
+ "example": 993
+ },
+ "imap_enable_ssl": {
+ "type": "boolean",
+ "description": "Enable SSL for IMAP",
+ "example": true
+ },
+ "imap_authentication": {
+ "type": "string",
+ "description": "IMAP authentication method",
+ "example": "plain"
+ },
+ "smtp_enabled": {
+ "type": "boolean",
+ "description": "Enable SMTP for outbound emails",
+ "example": true
+ },
+ "smtp_login": {
+ "type": "string",
+ "description": "SMTP login username",
+ "example": "support@example.com"
+ },
+ "smtp_password": {
+ "type": "string",
+ "description": "SMTP login password",
+ "example": "your-smtp-password"
+ },
+ "smtp_address": {
+ "type": "string",
+ "description": "SMTP server address",
+ "example": "smtp.example.com"
+ },
+ "smtp_port": {
+ "type": "integer",
+ "description": "SMTP server port",
+ "example": 587
+ },
+ "smtp_domain": {
+ "type": "string",
+ "description": "SMTP HELO domain",
+ "example": "example.com"
+ },
+ "smtp_enable_starttls_auto": {
+ "type": "boolean",
+ "description": "Automatically enable STARTTLS for SMTP",
+ "example": true
+ },
+ "smtp_enable_ssl_tls": {
+ "type": "boolean",
+ "description": "Enable SSL/TLS for SMTP",
+ "example": false
+ },
+ "smtp_openssl_verify_mode": {
+ "type": "string",
+ "description": "OpenSSL certificate verification mode for SMTP",
+ "example": "none"
+ },
+ "smtp_authentication": {
+ "type": "string",
+ "description": "SMTP authentication method",
+ "example": "login"
+ },
+ "provider": {
+ "type": "string",
+ "description": "Email provider",
+ "example": "google"
+ },
+ "verified_for_sending": {
+ "type": "boolean",
+ "description": "Whether the inbox is verified for sending emails",
+ "example": false
+ }
+ }
+ },
+ "inbox_create_line_channel_payload": {
+ "type": "object",
+ "title": "LINE channel",
+ "required": [
+ "type",
+ "line_channel_id",
+ "line_channel_secret",
+ "line_channel_token"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": [
+ "line"
+ ],
+ "example": "line"
+ },
+ "line_channel_id": {
+ "type": "string",
+ "description": "LINE channel ID",
+ "example": "1234567890"
+ },
+ "line_channel_secret": {
+ "type": "string",
+ "description": "LINE channel secret",
+ "example": "line-channel-secret"
+ },
+ "line_channel_token": {
+ "type": "string",
+ "description": "LINE channel access token",
+ "example": "line-channel-token"
+ }
+ }
+ },
+ "inbox_create_telegram_channel_payload": {
+ "type": "object",
+ "title": "Telegram channel",
+ "required": [
+ "type",
+ "bot_token"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": [
+ "telegram"
+ ],
+ "example": "telegram"
+ },
+ "bot_token": {
+ "type": "string",
+ "description": "Telegram bot token",
+ "example": "123456789:telegram-bot-token"
+ }
+ }
+ },
+ "inbox_create_whatsapp_channel_payload": {
+ "oneOf": [
+ {
+ "type": "object",
+ "title": "WhatsApp Cloud channel",
+ "required": [
+ "type",
+ "phone_number",
+ "provider",
+ "provider_config"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": [
+ "whatsapp"
+ ],
+ "example": "whatsapp"
+ },
+ "phone_number": {
+ "type": "string",
+ "description": "WhatsApp phone number",
+ "example": "+15551234567"
+ },
+ "provider": {
+ "type": "string",
+ "description": "WhatsApp provider",
+ "enum": [
+ "whatsapp_cloud"
+ ],
+ "example": "whatsapp_cloud"
+ },
+ "provider_config": {
+ "type": "object",
+ "description": "WhatsApp Cloud provider configuration",
+ "required": [
+ "api_key",
+ "phone_number_id",
+ "business_account_id"
+ ],
+ "properties": {
+ "api_key": {
+ "type": "string",
+ "description": "WhatsApp Cloud API key",
+ "example": "your-api-key"
+ },
+ "phone_number_id": {
+ "type": "string",
+ "description": "Phone number ID for WhatsApp Cloud",
+ "example": "your-phone-number-id"
+ },
+ "business_account_id": {
+ "type": "string",
+ "description": "Business account ID for WhatsApp Cloud",
+ "example": "your-business-account-id"
+ }
+ },
+ "example": {
+ "api_key": "your-api-key",
+ "phone_number_id": "your-phone-number-id",
+ "business_account_id": "your-business-account-id"
+ }
+ }
+ }
+ },
+ {
+ "type": "object",
+ "title": "Legacy 360dialog WhatsApp channel",
+ "deprecated": true,
+ "required": [
+ "type",
+ "phone_number",
+ "provider_config"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": [
+ "whatsapp"
+ ],
+ "example": "whatsapp"
+ },
+ "phone_number": {
+ "type": "string",
+ "description": "WhatsApp phone number",
+ "example": "+15551234567"
+ },
+ "provider": {
+ "type": "string",
+ "description": "Legacy 360dialog provider. Omit this field or use `default` only for existing deprecated 360dialog setups.",
+ "enum": [
+ "default"
+ ],
+ "deprecated": true,
+ "example": "default"
+ },
+ "provider_config": {
+ "type": "object",
+ "description": "Legacy 360dialog provider configuration",
+ "required": [
+ "api_key"
+ ],
+ "properties": {
+ "api_key": {
+ "type": "string",
+ "description": "360dialog API key",
+ "example": "your-api-key"
+ }
+ },
+ "example": {
+ "api_key": "your-api-key"
+ }
+ }
+ }
+ }
+ ]
+ },
+ "inbox_create_sms_channel_payload": {
+ "type": "object",
+ "title": "SMS channel",
+ "required": [
+ "type",
+ "phone_number"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": [
+ "sms"
+ ],
+ "example": "sms"
+ },
+ "phone_number": {
+ "type": "string",
+ "description": "SMS phone number",
+ "example": "+15551234567"
+ },
+ "provider_config": {
+ "type": "object",
+ "description": "Provider-specific SMS configuration",
+ "example": {
+ "account_id": "your-account-id",
+ "application_id": "your-application-id"
+ }
+ }
+ }
+ },
+ "inbox_update_web_widget_channel_payload": {
+ "type": "object",
+ "title": "Website channel settings",
+ "properties": {
+ "website_url": {
+ "type": "string",
+ "description": "URL at which the widget will be loaded",
+ "example": "https://example.com"
+ },
+ "welcome_title": {
+ "type": "string",
+ "description": "Welcome title to be displayed on the widget",
+ "example": "Welcome to our support"
+ },
+ "welcome_tagline": {
+ "type": "string",
+ "description": "Welcome tagline to be displayed on the widget",
+ "example": "We are here to help you"
+ },
+ "widget_color": {
+ "type": "string",
+ "description": "A Hex-color string used to customize the widget",
+ "example": "#FF5733"
+ },
+ "reply_time": {
+ "type": "string",
+ "description": "Expected reply time shown on the widget",
+ "enum": [
+ "in_a_few_minutes",
+ "in_a_few_hours",
+ "in_a_day"
+ ],
+ "example": "in_a_few_minutes"
+ },
+ "pre_chat_form_enabled": {
+ "type": "boolean",
+ "description": "Enable the pre-chat form before starting a conversation",
+ "example": false
+ },
+ "pre_chat_form_options": {
+ "type": "object",
+ "description": "Pre-chat form configuration",
+ "example": {
+ "pre_chat_message": "Share your queries or comments here.",
+ "pre_chat_fields": [
+ {
+ "field_type": "standard",
+ "label": "Email Id",
+ "name": "emailAddress",
+ "type": "email",
+ "required": true,
+ "enabled": true
+ }
+ ]
+ }
+ },
+ "continuity_via_email": {
+ "type": "boolean",
+ "description": "Continue conversations over email when the contact leaves the website",
+ "example": true
+ },
+ "hmac_mandatory": {
+ "type": "boolean",
+ "description": "Require HMAC verification for contacts using the widget",
+ "example": false
+ },
+ "allowed_domains": {
+ "type": "string",
+ "description": "Comma-separated list of domains where the widget is allowed to load",
+ "example": "example.com"
+ },
+ "selected_feature_flags": {
+ "type": "array",
+ "description": "Enabled widget feature flags",
+ "items": {
+ "type": "string",
+ "enum": [
+ "attachments",
+ "emoji_picker",
+ "end_conversation",
+ "use_inbox_avatar_for_bot",
+ "allow_mobile_webview"
+ ]
+ },
+ "example": [
+ "attachments",
+ "emoji_picker",
+ "end_conversation"
+ ]
+ }
+ }
+ },
+ "inbox_update_api_channel_payload": {
+ "type": "object",
+ "title": "API channel settings",
+ "properties": {
+ "webhook_url": {
+ "type": "string",
+ "description": "Webhook URL for API channel inbox callbacks",
+ "example": "https://example.com/webhook"
+ },
+ "hmac_mandatory": {
+ "type": "boolean",
+ "description": "Require HMAC verification for incoming API channel messages",
+ "example": false
+ },
+ "additional_attributes": {
+ "type": "object",
+ "description": "Additional attributes stored on contacts created through the API channel",
+ "example": {
+ "source": "mobile_app"
+ }
+ }
+ }
+ },
+ "inbox_update_email_channel_payload": {
+ "type": "object",
+ "title": "Email channel settings",
+ "properties": {
+ "email": {
+ "type": "string",
+ "description": "Email address for the inbox",
+ "example": "support@example.com"
+ },
+ "imap_enabled": {
+ "type": "boolean",
+ "description": "Enable IMAP for inbound emails",
+ "example": true
+ },
+ "imap_login": {
+ "type": "string",
+ "description": "IMAP login username",
+ "example": "support@example.com"
+ },
+ "imap_password": {
+ "type": "string",
+ "description": "IMAP login password",
+ "example": "your-imap-password"
+ },
+ "imap_address": {
+ "type": "string",
+ "description": "IMAP server address",
+ "example": "imap.example.com"
+ },
+ "imap_port": {
+ "type": "integer",
+ "description": "IMAP server port",
+ "example": 993
+ },
+ "imap_enable_ssl": {
+ "type": "boolean",
+ "description": "Enable SSL for IMAP",
+ "example": true
+ },
+ "imap_authentication": {
+ "type": "string",
+ "description": "IMAP authentication method",
+ "example": "plain"
+ },
+ "smtp_enabled": {
+ "type": "boolean",
+ "description": "Enable SMTP for outbound emails",
+ "example": true
+ },
+ "smtp_login": {
+ "type": "string",
+ "description": "SMTP login username",
+ "example": "support@example.com"
+ },
+ "smtp_password": {
+ "type": "string",
+ "description": "SMTP login password",
+ "example": "your-smtp-password"
+ },
+ "smtp_address": {
+ "type": "string",
+ "description": "SMTP server address",
+ "example": "smtp.example.com"
+ },
+ "smtp_port": {
+ "type": "integer",
+ "description": "SMTP server port",
+ "example": 587
+ },
+ "smtp_domain": {
+ "type": "string",
+ "description": "SMTP HELO domain",
+ "example": "example.com"
+ },
+ "smtp_enable_starttls_auto": {
+ "type": "boolean",
+ "description": "Automatically enable STARTTLS for SMTP",
+ "example": true
+ },
+ "smtp_enable_ssl_tls": {
+ "type": "boolean",
+ "description": "Enable SSL/TLS for SMTP",
+ "example": false
+ },
+ "smtp_openssl_verify_mode": {
+ "type": "string",
+ "description": "OpenSSL certificate verification mode for SMTP",
+ "example": "none"
+ },
+ "smtp_authentication": {
+ "type": "string",
+ "description": "SMTP authentication method",
+ "example": "login"
+ },
+ "provider": {
+ "type": "string",
+ "description": "Email provider",
+ "example": "google"
+ },
+ "verified_for_sending": {
+ "type": "boolean",
+ "description": "Whether the inbox is verified for sending emails",
+ "example": false
+ }
+ }
+ },
+ "inbox_update_line_channel_payload": {
+ "type": "object",
+ "title": "LINE channel settings",
+ "properties": {
+ "line_channel_id": {
+ "type": "string",
+ "description": "LINE channel ID",
+ "example": "1234567890"
+ },
+ "line_channel_secret": {
+ "type": "string",
+ "description": "LINE channel secret",
+ "example": "line-channel-secret"
+ },
+ "line_channel_token": {
+ "type": "string",
+ "description": "LINE channel access token",
+ "example": "line-channel-token"
+ }
+ }
+ },
+ "inbox_update_telegram_channel_payload": {
+ "type": "object",
+ "title": "Telegram channel settings",
+ "properties": {
+ "bot_token": {
+ "type": "string",
+ "description": "Telegram bot token",
+ "example": "123456789:telegram-bot-token"
+ }
+ }
+ },
+ "inbox_update_whatsapp_channel_payload": {
+ "type": "object",
+ "title": "WhatsApp channel settings",
+ "properties": {
+ "phone_number": {
+ "type": "string",
+ "description": "WhatsApp phone number",
+ "example": "+15551234567"
+ },
+ "provider": {
+ "type": "string",
+ "description": "WhatsApp provider. `default` is supported only for existing deprecated 360dialog setups.",
+ "enum": [
+ "whatsapp_cloud",
+ "default"
+ ],
+ "example": "whatsapp_cloud"
+ },
+ "provider_config": {
+ "type": "object",
+ "description": "WhatsApp provider configuration. Cloud channels use `api_key`, `phone_number_id`, and `business_account_id`; legacy 360dialog channels use `api_key`.",
+ "properties": {
+ "api_key": {
+ "type": "string",
+ "description": "Provider API key",
+ "example": "your-api-key"
+ },
+ "phone_number_id": {
+ "type": "string",
+ "description": "Phone number ID for WhatsApp Cloud",
+ "example": "your-phone-number-id"
+ },
+ "business_account_id": {
+ "type": "string",
+ "description": "Business account ID for WhatsApp Cloud",
+ "example": "your-business-account-id"
+ }
+ },
+ "example": {
+ "api_key": "your-api-key",
+ "phone_number_id": "your-phone-number-id",
+ "business_account_id": "your-business-account-id"
+ }
+ }
+ }
+ },
+ "inbox_update_sms_channel_payload": {
+ "type": "object",
+ "title": "SMS channel settings",
+ "properties": {
+ "phone_number": {
+ "type": "string",
+ "description": "SMS phone number",
+ "example": "+15551234567"
+ },
+ "provider_config": {
+ "type": "object",
+ "description": "Provider-specific SMS configuration",
+ "example": {
+ "api_key": "your-api-key",
+ "api_secret": "your-api-secret",
+ "application_id": "your-application-id",
+ "account_id": "your-account-id"
}
}
}
@@ -4111,7 +4991,9 @@
"message_updated",
"contact_created",
"contact_updated",
- "webwidget_triggered"
+ "webwidget_triggered",
+ "conversation_typing_on",
+ "conversation_typing_off"
]
},
"description": "The events you want to subscribe to.",
diff --git a/swagger/tag_groups/other_swagger.json b/swagger/tag_groups/other_swagger.json
index 50bf2212b..f6e10e57c 100644
--- a/swagger/tag_groups/other_swagger.json
+++ b/swagger/tag_groups/other_swagger.json
@@ -1079,15 +1079,16 @@
"availability_status": {
"type": "string",
"enum": [
- "available",
+ "online",
"busy",
"offline"
],
- "description": "The availability status of the agent computed by Chatwoot."
+ "readOnly": true,
+ "description": "The effective availability status of the agent, derived from the configured availability, auto-offline setting, and current presence. To update an agent's configured availability, use the availability field in create or update requests."
},
"auto_offline": {
"type": "boolean",
- "description": "Whether the availability status of agent is configured to go offline automatically when away."
+ "description": "Whether the agent is automatically marked offline when they are away."
},
"confirmed": {
"type": "boolean",
@@ -1502,11 +1503,18 @@
"contact_updated",
"message_created",
"message_updated",
- "webwidget_triggered"
+ "webwidget_triggered",
+ "conversation_typing_on",
+ "conversation_typing_off"
]
},
"description": "The list of subscribed events"
},
+ "secret": {
+ "type": "string",
+ "nullable": true,
+ "description": "Secret used to sign webhook requests. Signed webhook deliveries include `X-Chatwoot-Timestamp` and `X-Chatwoot-Signature`; the signature is `sha256=` followed by the HMAC-SHA256 of `{timestamp}.{raw_request_body}` using this secret. Deliveries also include `X-Chatwoot-Delivery` when a delivery id is available."
+ },
"account_id": {
"type": "number",
"description": "The id of the account which the webhook object belongs to"
@@ -2782,19 +2790,19 @@
"description": "Whether its administrator or agent",
"example": "agent"
},
- "availability_status": {
+ "availability": {
"type": "string",
"enum": [
- "available",
+ "online",
"busy",
"offline"
],
- "description": "The availability setting of the agent.",
- "example": "available"
+ "description": "The configured availability of the agent.",
+ "example": "online"
},
"auto_offline": {
"type": "boolean",
- "description": "Whether the availability status of agent is configured to go offline automatically when away.",
+ "description": "Whether the agent is automatically marked offline when they are away.",
"example": true
}
}
@@ -2814,19 +2822,19 @@
"description": "Whether its administrator or agent",
"example": "agent"
},
- "availability_status": {
+ "availability": {
"type": "string",
"enum": [
- "available",
+ "online",
"busy",
"offline"
],
- "description": "The availability status of the agent.",
- "example": "available"
+ "description": "The configured availability of the agent.",
+ "example": "online"
},
"auto_offline": {
"type": "boolean",
- "description": "Whether the availability status of agent is configured to go offline automatically when away.",
+ "description": "Whether the agent is automatically marked offline when they are away.",
"example": true
}
}
@@ -3205,72 +3213,121 @@
"properties": {
"name": {
"type": "string",
- "description": "The name of the inbox",
+ "description": "The name of the inbox.",
"example": "Support"
},
"avatar": {
"type": "string",
"format": "binary",
- "description": "Image file for avatar"
+ "description": "Image file for avatar."
},
"greeting_enabled": {
"type": "boolean",
- "description": "Enable greeting message",
+ "description": "Enable greeting message.",
"example": true
},
"greeting_message": {
"type": "string",
- "description": "Greeting message to be displayed on the widget",
+ "description": "Greeting message to send when greeting messages are enabled.",
"example": "Hello, how can I help you?"
},
"enable_email_collect": {
"type": "boolean",
- "description": "Enable email collection",
+ "description": "Enable email collection.\n\nAvailable for: `Website`\n",
"example": true
},
"csat_survey_enabled": {
"type": "boolean",
- "description": "Enable CSAT survey",
+ "description": "Enable CSAT survey.",
"example": true
},
+ "csat_config": {
+ "type": "object",
+ "description": "CSAT survey configuration.",
+ "properties": {
+ "display_type": {
+ "type": "string",
+ "description": "Display style for the CSAT survey.",
+ "enum": [
+ "emoji",
+ "star"
+ ],
+ "example": "emoji"
+ },
+ "message": {
+ "type": "string",
+ "description": "Message shown with the CSAT survey.",
+ "example": "Please rate your conversation"
+ },
+ "button_text": {
+ "type": "string",
+ "description": "Text shown on the CSAT survey button.",
+ "example": "Please rate us"
+ },
+ "language": {
+ "type": "string",
+ "description": "Language code for the CSAT survey.",
+ "example": "en"
+ },
+ "survey_rules": {
+ "type": "object",
+ "description": "Rules that decide when to show the CSAT survey.",
+ "properties": {
+ "operator": {
+ "type": "string",
+ "example": "contains"
+ },
+ "values": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "example": [
+ "billing"
+ ]
+ }
+ }
+ }
+ }
+ },
"enable_auto_assignment": {
"type": "boolean",
- "description": "Enable Auto Assignment",
+ "description": "Enable Auto Assignment.",
"example": true
},
"working_hours_enabled": {
"type": "boolean",
- "description": "Enable working hours",
+ "description": "Enable working hours.",
"example": true
},
"out_of_office_message": {
"type": "string",
- "description": "Out of office message to be displayed on the widget",
+ "description": "Out of office message to send outside working hours.",
"example": "We are currently out of office. Please leave a message and we will get back to you."
},
"timezone": {
"type": "string",
- "description": "Timezone of the inbox",
+ "description": "Timezone of the inbox.",
"example": "America/New_York"
},
"allow_messages_after_resolved": {
"type": "boolean",
- "description": "Allow messages after conversation is resolved",
+ "description": "Allow messages after conversation is resolved.\n\nAvailable for: `Website`\n",
"example": true
},
"lock_to_single_conversation": {
"type": "boolean",
- "description": "Lock to single conversation",
+ "description": "Lock contact messages to a single active conversation.\n\nAvailable for: `API` `LINE` `Telegram` `WhatsApp` `SMS`\n",
"example": true
},
"portal_id": {
"type": "integer",
- "description": "Id of the help center portal to attach to the inbox",
+ "description": "Id of the help center portal to attach to the inbox.",
"example": 1
},
"sender_name_type": {
"type": "string",
- "description": "Sender name type for the inbox",
+ "description": "Sender name type for outbound email replies.\n\nAvailable for: `Website` `Email`\n",
"enum": [
"friendly",
"professional"
@@ -3279,45 +3336,43 @@
},
"business_name": {
"type": "string",
- "description": "Business name for the inbox",
+ "description": "Business name for outbound email replies.\n\nAvailable for: `Website` `Email`\n",
"example": "My Business"
},
"channel": {
- "type": "object",
- "properties": {
- "type": {
- "type": "string",
- "description": "Type of the channel",
- "enum": [
- "web_widget",
- "api",
- "email",
- "line",
- "telegram",
- "whatsapp",
- "sms"
- ],
- "example": "web_widget"
+ "oneOf": [
+ {
+ "$ref": "#/components/schemas/inbox_create_web_widget_channel_payload"
},
- "website_url": {
- "type": "string",
- "description": "URL at which the widget will be loaded",
- "example": "https://example.com"
+ {
+ "$ref": "#/components/schemas/inbox_create_api_channel_payload"
},
- "welcome_title": {
- "type": "string",
- "description": "Welcome title to be displayed on the widget",
- "example": "Welcome to our support"
+ {
+ "$ref": "#/components/schemas/inbox_create_email_channel_payload"
},
- "welcome_tagline": {
- "type": "string",
- "description": "Welcome tagline to be displayed on the widget",
- "example": "We are here to help you"
+ {
+ "$ref": "#/components/schemas/inbox_create_line_channel_payload"
},
- "widget_color": {
- "type": "string",
- "description": "A Hex-color string used to customize the widget",
- "example": "#FF5733"
+ {
+ "$ref": "#/components/schemas/inbox_create_telegram_channel_payload"
+ },
+ {
+ "$ref": "#/components/schemas/inbox_create_whatsapp_channel_payload"
+ },
+ {
+ "$ref": "#/components/schemas/inbox_create_sms_channel_payload"
+ }
+ ],
+ "discriminator": {
+ "propertyName": "type",
+ "mapping": {
+ "web_widget": "#/components/schemas/inbox_create_web_widget_channel_payload",
+ "api": "#/components/schemas/inbox_create_api_channel_payload",
+ "email": "#/components/schemas/inbox_create_email_channel_payload",
+ "line": "#/components/schemas/inbox_create_line_channel_payload",
+ "telegram": "#/components/schemas/inbox_create_telegram_channel_payload",
+ "whatsapp": "#/components/schemas/inbox_create_whatsapp_channel_payload",
+ "sms": "#/components/schemas/inbox_create_sms_channel_payload"
}
}
}
@@ -3328,72 +3383,121 @@
"properties": {
"name": {
"type": "string",
- "description": "The name of the inbox",
+ "description": "The name of the inbox.",
"example": "Support"
},
"avatar": {
"type": "string",
"format": "binary",
- "description": "Image file for avatar"
+ "description": "Image file for avatar."
},
"greeting_enabled": {
"type": "boolean",
- "description": "Enable greeting message",
+ "description": "Enable greeting message.",
"example": true
},
"greeting_message": {
"type": "string",
- "description": "Greeting message to be displayed on the widget",
+ "description": "Greeting message to send when greeting messages are enabled.",
"example": "Hello, how can I help you?"
},
"enable_email_collect": {
"type": "boolean",
- "description": "Enable email collection",
+ "description": "Enable email collection.\n\nAvailable for: `Website`\n",
"example": true
},
"csat_survey_enabled": {
"type": "boolean",
- "description": "Enable CSAT survey",
+ "description": "Enable CSAT survey.",
"example": true
},
+ "csat_config": {
+ "type": "object",
+ "description": "CSAT survey configuration.",
+ "properties": {
+ "display_type": {
+ "type": "string",
+ "description": "Display style for the CSAT survey.",
+ "enum": [
+ "emoji",
+ "star"
+ ],
+ "example": "emoji"
+ },
+ "message": {
+ "type": "string",
+ "description": "Message shown with the CSAT survey.",
+ "example": "Please rate your conversation"
+ },
+ "button_text": {
+ "type": "string",
+ "description": "Text shown on the CSAT survey button.",
+ "example": "Please rate us"
+ },
+ "language": {
+ "type": "string",
+ "description": "Language code for the CSAT survey.",
+ "example": "en"
+ },
+ "survey_rules": {
+ "type": "object",
+ "description": "Rules that decide when to show the CSAT survey.",
+ "properties": {
+ "operator": {
+ "type": "string",
+ "example": "contains"
+ },
+ "values": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "example": [
+ "billing"
+ ]
+ }
+ }
+ }
+ }
+ },
"enable_auto_assignment": {
"type": "boolean",
- "description": "Enable Auto Assignment",
+ "description": "Enable Auto Assignment.",
"example": true
},
"working_hours_enabled": {
"type": "boolean",
- "description": "Enable working hours",
+ "description": "Enable working hours.",
"example": true
},
"out_of_office_message": {
"type": "string",
- "description": "Out of office message to be displayed on the widget",
+ "description": "Out of office message to send outside working hours.",
"example": "We are currently out of office. Please leave a message and we will get back to you."
},
"timezone": {
"type": "string",
- "description": "Timezone of the inbox",
+ "description": "Timezone of the inbox.",
"example": "America/New_York"
},
"allow_messages_after_resolved": {
"type": "boolean",
- "description": "Allow messages after conversation is resolved",
+ "description": "Allow messages after conversation is resolved.\n\nAvailable for: `Website`\n",
"example": true
},
"lock_to_single_conversation": {
"type": "boolean",
- "description": "Lock to single conversation",
+ "description": "Lock contact messages to a single active conversation.\n\nAvailable for: `API` `LINE` `Telegram` `WhatsApp` `SMS`\n",
"example": true
},
"portal_id": {
"type": "integer",
- "description": "Id of the help center portal to attach to the inbox",
+ "description": "Id of the help center portal to attach to the inbox.",
"example": 1
},
"sender_name_type": {
"type": "string",
- "description": "Sender name type for the inbox",
+ "description": "Sender name type for outbound email replies.\n\nAvailable for: `Website` `Email`\n",
"enum": [
"friendly",
"professional"
@@ -3402,32 +3506,808 @@
},
"business_name": {
"type": "string",
- "description": "Business name for the inbox",
+ "description": "Business name for outbound email replies.\n\nAvailable for: `Website` `Email`\n",
"example": "My Business"
},
"channel": {
- "type": "object",
- "properties": {
- "website_url": {
- "type": "string",
- "description": "URL at which the widget will be loaded",
- "example": "https://example.com"
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/inbox_update_web_widget_channel_payload"
},
- "welcome_title": {
- "type": "string",
- "description": "Welcome title to be displayed on the widget",
- "example": "Welcome to our support"
+ {
+ "$ref": "#/components/schemas/inbox_update_api_channel_payload"
},
- "welcome_tagline": {
- "type": "string",
- "description": "Welcome tagline to be displayed on the widget",
- "example": "We are here to help you"
+ {
+ "$ref": "#/components/schemas/inbox_update_email_channel_payload"
},
- "widget_color": {
- "type": "string",
- "description": "A Hex-color string used to customize the widget",
- "example": "#FF5733"
+ {
+ "$ref": "#/components/schemas/inbox_update_line_channel_payload"
+ },
+ {
+ "$ref": "#/components/schemas/inbox_update_telegram_channel_payload"
+ },
+ {
+ "$ref": "#/components/schemas/inbox_update_whatsapp_channel_payload"
+ },
+ {
+ "$ref": "#/components/schemas/inbox_update_sms_channel_payload"
}
+ ]
+ }
+ }
+ },
+ "inbox_create_web_widget_channel_payload": {
+ "type": "object",
+ "title": "Website channel",
+ "required": [
+ "type",
+ "website_url"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": [
+ "web_widget"
+ ],
+ "example": "web_widget"
+ },
+ "website_url": {
+ "type": "string",
+ "description": "URL at which the widget will be loaded",
+ "example": "https://example.com"
+ },
+ "welcome_title": {
+ "type": "string",
+ "description": "Welcome title to be displayed on the widget",
+ "example": "Welcome to our support"
+ },
+ "welcome_tagline": {
+ "type": "string",
+ "description": "Welcome tagline to be displayed on the widget",
+ "example": "We are here to help you"
+ },
+ "widget_color": {
+ "type": "string",
+ "description": "A Hex-color string used to customize the widget",
+ "example": "#FF5733"
+ },
+ "reply_time": {
+ "type": "string",
+ "description": "Expected reply time shown on the widget",
+ "enum": [
+ "in_a_few_minutes",
+ "in_a_few_hours",
+ "in_a_day"
+ ],
+ "example": "in_a_few_minutes"
+ },
+ "pre_chat_form_enabled": {
+ "type": "boolean",
+ "description": "Enable the pre-chat form before starting a conversation",
+ "example": false
+ },
+ "pre_chat_form_options": {
+ "type": "object",
+ "description": "Pre-chat form configuration",
+ "example": {
+ "pre_chat_message": "Share your queries or comments here.",
+ "pre_chat_fields": [
+ {
+ "field_type": "standard",
+ "label": "Email Id",
+ "name": "emailAddress",
+ "type": "email",
+ "required": true,
+ "enabled": true
+ }
+ ]
+ }
+ },
+ "continuity_via_email": {
+ "type": "boolean",
+ "description": "Continue conversations over email when the contact leaves the website",
+ "example": true
+ },
+ "hmac_mandatory": {
+ "type": "boolean",
+ "description": "Require HMAC verification for contacts using the widget",
+ "example": false
+ },
+ "allowed_domains": {
+ "type": "string",
+ "description": "Comma-separated list of domains where the widget is allowed to load",
+ "example": "example.com"
+ },
+ "selected_feature_flags": {
+ "type": "array",
+ "description": "Enabled widget feature flags",
+ "items": {
+ "type": "string",
+ "enum": [
+ "attachments",
+ "emoji_picker",
+ "end_conversation",
+ "use_inbox_avatar_for_bot",
+ "allow_mobile_webview"
+ ]
+ },
+ "example": [
+ "attachments",
+ "emoji_picker",
+ "end_conversation"
+ ]
+ }
+ }
+ },
+ "inbox_create_api_channel_payload": {
+ "type": "object",
+ "title": "API channel",
+ "required": [
+ "type"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": [
+ "api"
+ ],
+ "example": "api"
+ },
+ "webhook_url": {
+ "type": "string",
+ "description": "Webhook URL for API channel inbox callbacks",
+ "example": "https://example.com/webhook"
+ },
+ "hmac_mandatory": {
+ "type": "boolean",
+ "description": "Require HMAC verification for incoming API channel messages",
+ "example": false
+ },
+ "additional_attributes": {
+ "type": "object",
+ "description": "Additional attributes stored on contacts created through the API channel",
+ "example": {
+ "source": "mobile_app"
+ }
+ }
+ }
+ },
+ "inbox_create_email_channel_payload": {
+ "type": "object",
+ "title": "Email channel",
+ "required": [
+ "type",
+ "email"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": [
+ "email"
+ ],
+ "example": "email"
+ },
+ "email": {
+ "type": "string",
+ "description": "Email address for the inbox",
+ "example": "support@example.com"
+ },
+ "imap_enabled": {
+ "type": "boolean",
+ "description": "Enable IMAP for inbound emails",
+ "example": true
+ },
+ "imap_login": {
+ "type": "string",
+ "description": "IMAP login username",
+ "example": "support@example.com"
+ },
+ "imap_password": {
+ "type": "string",
+ "description": "IMAP login password",
+ "example": "your-imap-password"
+ },
+ "imap_address": {
+ "type": "string",
+ "description": "IMAP server address",
+ "example": "imap.example.com"
+ },
+ "imap_port": {
+ "type": "integer",
+ "description": "IMAP server port",
+ "example": 993
+ },
+ "imap_enable_ssl": {
+ "type": "boolean",
+ "description": "Enable SSL for IMAP",
+ "example": true
+ },
+ "imap_authentication": {
+ "type": "string",
+ "description": "IMAP authentication method",
+ "example": "plain"
+ },
+ "smtp_enabled": {
+ "type": "boolean",
+ "description": "Enable SMTP for outbound emails",
+ "example": true
+ },
+ "smtp_login": {
+ "type": "string",
+ "description": "SMTP login username",
+ "example": "support@example.com"
+ },
+ "smtp_password": {
+ "type": "string",
+ "description": "SMTP login password",
+ "example": "your-smtp-password"
+ },
+ "smtp_address": {
+ "type": "string",
+ "description": "SMTP server address",
+ "example": "smtp.example.com"
+ },
+ "smtp_port": {
+ "type": "integer",
+ "description": "SMTP server port",
+ "example": 587
+ },
+ "smtp_domain": {
+ "type": "string",
+ "description": "SMTP HELO domain",
+ "example": "example.com"
+ },
+ "smtp_enable_starttls_auto": {
+ "type": "boolean",
+ "description": "Automatically enable STARTTLS for SMTP",
+ "example": true
+ },
+ "smtp_enable_ssl_tls": {
+ "type": "boolean",
+ "description": "Enable SSL/TLS for SMTP",
+ "example": false
+ },
+ "smtp_openssl_verify_mode": {
+ "type": "string",
+ "description": "OpenSSL certificate verification mode for SMTP",
+ "example": "none"
+ },
+ "smtp_authentication": {
+ "type": "string",
+ "description": "SMTP authentication method",
+ "example": "login"
+ },
+ "provider": {
+ "type": "string",
+ "description": "Email provider",
+ "example": "google"
+ },
+ "verified_for_sending": {
+ "type": "boolean",
+ "description": "Whether the inbox is verified for sending emails",
+ "example": false
+ }
+ }
+ },
+ "inbox_create_line_channel_payload": {
+ "type": "object",
+ "title": "LINE channel",
+ "required": [
+ "type",
+ "line_channel_id",
+ "line_channel_secret",
+ "line_channel_token"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": [
+ "line"
+ ],
+ "example": "line"
+ },
+ "line_channel_id": {
+ "type": "string",
+ "description": "LINE channel ID",
+ "example": "1234567890"
+ },
+ "line_channel_secret": {
+ "type": "string",
+ "description": "LINE channel secret",
+ "example": "line-channel-secret"
+ },
+ "line_channel_token": {
+ "type": "string",
+ "description": "LINE channel access token",
+ "example": "line-channel-token"
+ }
+ }
+ },
+ "inbox_create_telegram_channel_payload": {
+ "type": "object",
+ "title": "Telegram channel",
+ "required": [
+ "type",
+ "bot_token"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": [
+ "telegram"
+ ],
+ "example": "telegram"
+ },
+ "bot_token": {
+ "type": "string",
+ "description": "Telegram bot token",
+ "example": "123456789:telegram-bot-token"
+ }
+ }
+ },
+ "inbox_create_whatsapp_channel_payload": {
+ "oneOf": [
+ {
+ "type": "object",
+ "title": "WhatsApp Cloud channel",
+ "required": [
+ "type",
+ "phone_number",
+ "provider",
+ "provider_config"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": [
+ "whatsapp"
+ ],
+ "example": "whatsapp"
+ },
+ "phone_number": {
+ "type": "string",
+ "description": "WhatsApp phone number",
+ "example": "+15551234567"
+ },
+ "provider": {
+ "type": "string",
+ "description": "WhatsApp provider",
+ "enum": [
+ "whatsapp_cloud"
+ ],
+ "example": "whatsapp_cloud"
+ },
+ "provider_config": {
+ "type": "object",
+ "description": "WhatsApp Cloud provider configuration",
+ "required": [
+ "api_key",
+ "phone_number_id",
+ "business_account_id"
+ ],
+ "properties": {
+ "api_key": {
+ "type": "string",
+ "description": "WhatsApp Cloud API key",
+ "example": "your-api-key"
+ },
+ "phone_number_id": {
+ "type": "string",
+ "description": "Phone number ID for WhatsApp Cloud",
+ "example": "your-phone-number-id"
+ },
+ "business_account_id": {
+ "type": "string",
+ "description": "Business account ID for WhatsApp Cloud",
+ "example": "your-business-account-id"
+ }
+ },
+ "example": {
+ "api_key": "your-api-key",
+ "phone_number_id": "your-phone-number-id",
+ "business_account_id": "your-business-account-id"
+ }
+ }
+ }
+ },
+ {
+ "type": "object",
+ "title": "Legacy 360dialog WhatsApp channel",
+ "deprecated": true,
+ "required": [
+ "type",
+ "phone_number",
+ "provider_config"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": [
+ "whatsapp"
+ ],
+ "example": "whatsapp"
+ },
+ "phone_number": {
+ "type": "string",
+ "description": "WhatsApp phone number",
+ "example": "+15551234567"
+ },
+ "provider": {
+ "type": "string",
+ "description": "Legacy 360dialog provider. Omit this field or use `default` only for existing deprecated 360dialog setups.",
+ "enum": [
+ "default"
+ ],
+ "deprecated": true,
+ "example": "default"
+ },
+ "provider_config": {
+ "type": "object",
+ "description": "Legacy 360dialog provider configuration",
+ "required": [
+ "api_key"
+ ],
+ "properties": {
+ "api_key": {
+ "type": "string",
+ "description": "360dialog API key",
+ "example": "your-api-key"
+ }
+ },
+ "example": {
+ "api_key": "your-api-key"
+ }
+ }
+ }
+ }
+ ]
+ },
+ "inbox_create_sms_channel_payload": {
+ "type": "object",
+ "title": "SMS channel",
+ "required": [
+ "type",
+ "phone_number"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": [
+ "sms"
+ ],
+ "example": "sms"
+ },
+ "phone_number": {
+ "type": "string",
+ "description": "SMS phone number",
+ "example": "+15551234567"
+ },
+ "provider_config": {
+ "type": "object",
+ "description": "Provider-specific SMS configuration",
+ "example": {
+ "account_id": "your-account-id",
+ "application_id": "your-application-id"
+ }
+ }
+ }
+ },
+ "inbox_update_web_widget_channel_payload": {
+ "type": "object",
+ "title": "Website channel settings",
+ "properties": {
+ "website_url": {
+ "type": "string",
+ "description": "URL at which the widget will be loaded",
+ "example": "https://example.com"
+ },
+ "welcome_title": {
+ "type": "string",
+ "description": "Welcome title to be displayed on the widget",
+ "example": "Welcome to our support"
+ },
+ "welcome_tagline": {
+ "type": "string",
+ "description": "Welcome tagline to be displayed on the widget",
+ "example": "We are here to help you"
+ },
+ "widget_color": {
+ "type": "string",
+ "description": "A Hex-color string used to customize the widget",
+ "example": "#FF5733"
+ },
+ "reply_time": {
+ "type": "string",
+ "description": "Expected reply time shown on the widget",
+ "enum": [
+ "in_a_few_minutes",
+ "in_a_few_hours",
+ "in_a_day"
+ ],
+ "example": "in_a_few_minutes"
+ },
+ "pre_chat_form_enabled": {
+ "type": "boolean",
+ "description": "Enable the pre-chat form before starting a conversation",
+ "example": false
+ },
+ "pre_chat_form_options": {
+ "type": "object",
+ "description": "Pre-chat form configuration",
+ "example": {
+ "pre_chat_message": "Share your queries or comments here.",
+ "pre_chat_fields": [
+ {
+ "field_type": "standard",
+ "label": "Email Id",
+ "name": "emailAddress",
+ "type": "email",
+ "required": true,
+ "enabled": true
+ }
+ ]
+ }
+ },
+ "continuity_via_email": {
+ "type": "boolean",
+ "description": "Continue conversations over email when the contact leaves the website",
+ "example": true
+ },
+ "hmac_mandatory": {
+ "type": "boolean",
+ "description": "Require HMAC verification for contacts using the widget",
+ "example": false
+ },
+ "allowed_domains": {
+ "type": "string",
+ "description": "Comma-separated list of domains where the widget is allowed to load",
+ "example": "example.com"
+ },
+ "selected_feature_flags": {
+ "type": "array",
+ "description": "Enabled widget feature flags",
+ "items": {
+ "type": "string",
+ "enum": [
+ "attachments",
+ "emoji_picker",
+ "end_conversation",
+ "use_inbox_avatar_for_bot",
+ "allow_mobile_webview"
+ ]
+ },
+ "example": [
+ "attachments",
+ "emoji_picker",
+ "end_conversation"
+ ]
+ }
+ }
+ },
+ "inbox_update_api_channel_payload": {
+ "type": "object",
+ "title": "API channel settings",
+ "properties": {
+ "webhook_url": {
+ "type": "string",
+ "description": "Webhook URL for API channel inbox callbacks",
+ "example": "https://example.com/webhook"
+ },
+ "hmac_mandatory": {
+ "type": "boolean",
+ "description": "Require HMAC verification for incoming API channel messages",
+ "example": false
+ },
+ "additional_attributes": {
+ "type": "object",
+ "description": "Additional attributes stored on contacts created through the API channel",
+ "example": {
+ "source": "mobile_app"
+ }
+ }
+ }
+ },
+ "inbox_update_email_channel_payload": {
+ "type": "object",
+ "title": "Email channel settings",
+ "properties": {
+ "email": {
+ "type": "string",
+ "description": "Email address for the inbox",
+ "example": "support@example.com"
+ },
+ "imap_enabled": {
+ "type": "boolean",
+ "description": "Enable IMAP for inbound emails",
+ "example": true
+ },
+ "imap_login": {
+ "type": "string",
+ "description": "IMAP login username",
+ "example": "support@example.com"
+ },
+ "imap_password": {
+ "type": "string",
+ "description": "IMAP login password",
+ "example": "your-imap-password"
+ },
+ "imap_address": {
+ "type": "string",
+ "description": "IMAP server address",
+ "example": "imap.example.com"
+ },
+ "imap_port": {
+ "type": "integer",
+ "description": "IMAP server port",
+ "example": 993
+ },
+ "imap_enable_ssl": {
+ "type": "boolean",
+ "description": "Enable SSL for IMAP",
+ "example": true
+ },
+ "imap_authentication": {
+ "type": "string",
+ "description": "IMAP authentication method",
+ "example": "plain"
+ },
+ "smtp_enabled": {
+ "type": "boolean",
+ "description": "Enable SMTP for outbound emails",
+ "example": true
+ },
+ "smtp_login": {
+ "type": "string",
+ "description": "SMTP login username",
+ "example": "support@example.com"
+ },
+ "smtp_password": {
+ "type": "string",
+ "description": "SMTP login password",
+ "example": "your-smtp-password"
+ },
+ "smtp_address": {
+ "type": "string",
+ "description": "SMTP server address",
+ "example": "smtp.example.com"
+ },
+ "smtp_port": {
+ "type": "integer",
+ "description": "SMTP server port",
+ "example": 587
+ },
+ "smtp_domain": {
+ "type": "string",
+ "description": "SMTP HELO domain",
+ "example": "example.com"
+ },
+ "smtp_enable_starttls_auto": {
+ "type": "boolean",
+ "description": "Automatically enable STARTTLS for SMTP",
+ "example": true
+ },
+ "smtp_enable_ssl_tls": {
+ "type": "boolean",
+ "description": "Enable SSL/TLS for SMTP",
+ "example": false
+ },
+ "smtp_openssl_verify_mode": {
+ "type": "string",
+ "description": "OpenSSL certificate verification mode for SMTP",
+ "example": "none"
+ },
+ "smtp_authentication": {
+ "type": "string",
+ "description": "SMTP authentication method",
+ "example": "login"
+ },
+ "provider": {
+ "type": "string",
+ "description": "Email provider",
+ "example": "google"
+ },
+ "verified_for_sending": {
+ "type": "boolean",
+ "description": "Whether the inbox is verified for sending emails",
+ "example": false
+ }
+ }
+ },
+ "inbox_update_line_channel_payload": {
+ "type": "object",
+ "title": "LINE channel settings",
+ "properties": {
+ "line_channel_id": {
+ "type": "string",
+ "description": "LINE channel ID",
+ "example": "1234567890"
+ },
+ "line_channel_secret": {
+ "type": "string",
+ "description": "LINE channel secret",
+ "example": "line-channel-secret"
+ },
+ "line_channel_token": {
+ "type": "string",
+ "description": "LINE channel access token",
+ "example": "line-channel-token"
+ }
+ }
+ },
+ "inbox_update_telegram_channel_payload": {
+ "type": "object",
+ "title": "Telegram channel settings",
+ "properties": {
+ "bot_token": {
+ "type": "string",
+ "description": "Telegram bot token",
+ "example": "123456789:telegram-bot-token"
+ }
+ }
+ },
+ "inbox_update_whatsapp_channel_payload": {
+ "type": "object",
+ "title": "WhatsApp channel settings",
+ "properties": {
+ "phone_number": {
+ "type": "string",
+ "description": "WhatsApp phone number",
+ "example": "+15551234567"
+ },
+ "provider": {
+ "type": "string",
+ "description": "WhatsApp provider. `default` is supported only for existing deprecated 360dialog setups.",
+ "enum": [
+ "whatsapp_cloud",
+ "default"
+ ],
+ "example": "whatsapp_cloud"
+ },
+ "provider_config": {
+ "type": "object",
+ "description": "WhatsApp provider configuration. Cloud channels use `api_key`, `phone_number_id`, and `business_account_id`; legacy 360dialog channels use `api_key`.",
+ "properties": {
+ "api_key": {
+ "type": "string",
+ "description": "Provider API key",
+ "example": "your-api-key"
+ },
+ "phone_number_id": {
+ "type": "string",
+ "description": "Phone number ID for WhatsApp Cloud",
+ "example": "your-phone-number-id"
+ },
+ "business_account_id": {
+ "type": "string",
+ "description": "Business account ID for WhatsApp Cloud",
+ "example": "your-business-account-id"
+ }
+ },
+ "example": {
+ "api_key": "your-api-key",
+ "phone_number_id": "your-phone-number-id",
+ "business_account_id": "your-business-account-id"
+ }
+ }
+ }
+ },
+ "inbox_update_sms_channel_payload": {
+ "type": "object",
+ "title": "SMS channel settings",
+ "properties": {
+ "phone_number": {
+ "type": "string",
+ "description": "SMS phone number",
+ "example": "+15551234567"
+ },
+ "provider_config": {
+ "type": "object",
+ "description": "Provider-specific SMS configuration",
+ "example": {
+ "api_key": "your-api-key",
+ "api_secret": "your-api-secret",
+ "application_id": "your-application-id",
+ "account_id": "your-account-id"
}
}
}
@@ -3526,7 +4406,9 @@
"message_updated",
"contact_created",
"contact_updated",
- "webwidget_triggered"
+ "webwidget_triggered",
+ "conversation_typing_on",
+ "conversation_typing_off"
]
},
"description": "The events you want to subscribe to.",
diff --git a/swagger/tag_groups/platform_swagger.json b/swagger/tag_groups/platform_swagger.json
index f1e471e79..7e00568f2 100644
--- a/swagger/tag_groups/platform_swagger.json
+++ b/swagger/tag_groups/platform_swagger.json
@@ -1840,15 +1840,16 @@
"availability_status": {
"type": "string",
"enum": [
- "available",
+ "online",
"busy",
"offline"
],
- "description": "The availability status of the agent computed by Chatwoot."
+ "readOnly": true,
+ "description": "The effective availability status of the agent, derived from the configured availability, auto-offline setting, and current presence. To update an agent's configured availability, use the availability field in create or update requests."
},
"auto_offline": {
"type": "boolean",
- "description": "Whether the availability status of agent is configured to go offline automatically when away."
+ "description": "Whether the agent is automatically marked offline when they are away."
},
"confirmed": {
"type": "boolean",
@@ -2263,11 +2264,18 @@
"contact_updated",
"message_created",
"message_updated",
- "webwidget_triggered"
+ "webwidget_triggered",
+ "conversation_typing_on",
+ "conversation_typing_off"
]
},
"description": "The list of subscribed events"
},
+ "secret": {
+ "type": "string",
+ "nullable": true,
+ "description": "Secret used to sign webhook requests. Signed webhook deliveries include `X-Chatwoot-Timestamp` and `X-Chatwoot-Signature`; the signature is `sha256=` followed by the HMAC-SHA256 of `{timestamp}.{raw_request_body}` using this secret. Deliveries also include `X-Chatwoot-Delivery` when a delivery id is available."
+ },
"account_id": {
"type": "number",
"description": "The id of the account which the webhook object belongs to"
@@ -3543,19 +3551,19 @@
"description": "Whether its administrator or agent",
"example": "agent"
},
- "availability_status": {
+ "availability": {
"type": "string",
"enum": [
- "available",
+ "online",
"busy",
"offline"
],
- "description": "The availability setting of the agent.",
- "example": "available"
+ "description": "The configured availability of the agent.",
+ "example": "online"
},
"auto_offline": {
"type": "boolean",
- "description": "Whether the availability status of agent is configured to go offline automatically when away.",
+ "description": "Whether the agent is automatically marked offline when they are away.",
"example": true
}
}
@@ -3575,19 +3583,19 @@
"description": "Whether its administrator or agent",
"example": "agent"
},
- "availability_status": {
+ "availability": {
"type": "string",
"enum": [
- "available",
+ "online",
"busy",
"offline"
],
- "description": "The availability status of the agent.",
- "example": "available"
+ "description": "The configured availability of the agent.",
+ "example": "online"
},
"auto_offline": {
"type": "boolean",
- "description": "Whether the availability status of agent is configured to go offline automatically when away.",
+ "description": "Whether the agent is automatically marked offline when they are away.",
"example": true
}
}
@@ -3966,72 +3974,121 @@
"properties": {
"name": {
"type": "string",
- "description": "The name of the inbox",
+ "description": "The name of the inbox.",
"example": "Support"
},
"avatar": {
"type": "string",
"format": "binary",
- "description": "Image file for avatar"
+ "description": "Image file for avatar."
},
"greeting_enabled": {
"type": "boolean",
- "description": "Enable greeting message",
+ "description": "Enable greeting message.",
"example": true
},
"greeting_message": {
"type": "string",
- "description": "Greeting message to be displayed on the widget",
+ "description": "Greeting message to send when greeting messages are enabled.",
"example": "Hello, how can I help you?"
},
"enable_email_collect": {
"type": "boolean",
- "description": "Enable email collection",
+ "description": "Enable email collection.\n\nAvailable for: `Website`\n",
"example": true
},
"csat_survey_enabled": {
"type": "boolean",
- "description": "Enable CSAT survey",
+ "description": "Enable CSAT survey.",
"example": true
},
+ "csat_config": {
+ "type": "object",
+ "description": "CSAT survey configuration.",
+ "properties": {
+ "display_type": {
+ "type": "string",
+ "description": "Display style for the CSAT survey.",
+ "enum": [
+ "emoji",
+ "star"
+ ],
+ "example": "emoji"
+ },
+ "message": {
+ "type": "string",
+ "description": "Message shown with the CSAT survey.",
+ "example": "Please rate your conversation"
+ },
+ "button_text": {
+ "type": "string",
+ "description": "Text shown on the CSAT survey button.",
+ "example": "Please rate us"
+ },
+ "language": {
+ "type": "string",
+ "description": "Language code for the CSAT survey.",
+ "example": "en"
+ },
+ "survey_rules": {
+ "type": "object",
+ "description": "Rules that decide when to show the CSAT survey.",
+ "properties": {
+ "operator": {
+ "type": "string",
+ "example": "contains"
+ },
+ "values": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "example": [
+ "billing"
+ ]
+ }
+ }
+ }
+ }
+ },
"enable_auto_assignment": {
"type": "boolean",
- "description": "Enable Auto Assignment",
+ "description": "Enable Auto Assignment.",
"example": true
},
"working_hours_enabled": {
"type": "boolean",
- "description": "Enable working hours",
+ "description": "Enable working hours.",
"example": true
},
"out_of_office_message": {
"type": "string",
- "description": "Out of office message to be displayed on the widget",
+ "description": "Out of office message to send outside working hours.",
"example": "We are currently out of office. Please leave a message and we will get back to you."
},
"timezone": {
"type": "string",
- "description": "Timezone of the inbox",
+ "description": "Timezone of the inbox.",
"example": "America/New_York"
},
"allow_messages_after_resolved": {
"type": "boolean",
- "description": "Allow messages after conversation is resolved",
+ "description": "Allow messages after conversation is resolved.\n\nAvailable for: `Website`\n",
"example": true
},
"lock_to_single_conversation": {
"type": "boolean",
- "description": "Lock to single conversation",
+ "description": "Lock contact messages to a single active conversation.\n\nAvailable for: `API` `LINE` `Telegram` `WhatsApp` `SMS`\n",
"example": true
},
"portal_id": {
"type": "integer",
- "description": "Id of the help center portal to attach to the inbox",
+ "description": "Id of the help center portal to attach to the inbox.",
"example": 1
},
"sender_name_type": {
"type": "string",
- "description": "Sender name type for the inbox",
+ "description": "Sender name type for outbound email replies.\n\nAvailable for: `Website` `Email`\n",
"enum": [
"friendly",
"professional"
@@ -4040,45 +4097,43 @@
},
"business_name": {
"type": "string",
- "description": "Business name for the inbox",
+ "description": "Business name for outbound email replies.\n\nAvailable for: `Website` `Email`\n",
"example": "My Business"
},
"channel": {
- "type": "object",
- "properties": {
- "type": {
- "type": "string",
- "description": "Type of the channel",
- "enum": [
- "web_widget",
- "api",
- "email",
- "line",
- "telegram",
- "whatsapp",
- "sms"
- ],
- "example": "web_widget"
+ "oneOf": [
+ {
+ "$ref": "#/components/schemas/inbox_create_web_widget_channel_payload"
},
- "website_url": {
- "type": "string",
- "description": "URL at which the widget will be loaded",
- "example": "https://example.com"
+ {
+ "$ref": "#/components/schemas/inbox_create_api_channel_payload"
},
- "welcome_title": {
- "type": "string",
- "description": "Welcome title to be displayed on the widget",
- "example": "Welcome to our support"
+ {
+ "$ref": "#/components/schemas/inbox_create_email_channel_payload"
},
- "welcome_tagline": {
- "type": "string",
- "description": "Welcome tagline to be displayed on the widget",
- "example": "We are here to help you"
+ {
+ "$ref": "#/components/schemas/inbox_create_line_channel_payload"
},
- "widget_color": {
- "type": "string",
- "description": "A Hex-color string used to customize the widget",
- "example": "#FF5733"
+ {
+ "$ref": "#/components/schemas/inbox_create_telegram_channel_payload"
+ },
+ {
+ "$ref": "#/components/schemas/inbox_create_whatsapp_channel_payload"
+ },
+ {
+ "$ref": "#/components/schemas/inbox_create_sms_channel_payload"
+ }
+ ],
+ "discriminator": {
+ "propertyName": "type",
+ "mapping": {
+ "web_widget": "#/components/schemas/inbox_create_web_widget_channel_payload",
+ "api": "#/components/schemas/inbox_create_api_channel_payload",
+ "email": "#/components/schemas/inbox_create_email_channel_payload",
+ "line": "#/components/schemas/inbox_create_line_channel_payload",
+ "telegram": "#/components/schemas/inbox_create_telegram_channel_payload",
+ "whatsapp": "#/components/schemas/inbox_create_whatsapp_channel_payload",
+ "sms": "#/components/schemas/inbox_create_sms_channel_payload"
}
}
}
@@ -4089,72 +4144,121 @@
"properties": {
"name": {
"type": "string",
- "description": "The name of the inbox",
+ "description": "The name of the inbox.",
"example": "Support"
},
"avatar": {
"type": "string",
"format": "binary",
- "description": "Image file for avatar"
+ "description": "Image file for avatar."
},
"greeting_enabled": {
"type": "boolean",
- "description": "Enable greeting message",
+ "description": "Enable greeting message.",
"example": true
},
"greeting_message": {
"type": "string",
- "description": "Greeting message to be displayed on the widget",
+ "description": "Greeting message to send when greeting messages are enabled.",
"example": "Hello, how can I help you?"
},
"enable_email_collect": {
"type": "boolean",
- "description": "Enable email collection",
+ "description": "Enable email collection.\n\nAvailable for: `Website`\n",
"example": true
},
"csat_survey_enabled": {
"type": "boolean",
- "description": "Enable CSAT survey",
+ "description": "Enable CSAT survey.",
"example": true
},
+ "csat_config": {
+ "type": "object",
+ "description": "CSAT survey configuration.",
+ "properties": {
+ "display_type": {
+ "type": "string",
+ "description": "Display style for the CSAT survey.",
+ "enum": [
+ "emoji",
+ "star"
+ ],
+ "example": "emoji"
+ },
+ "message": {
+ "type": "string",
+ "description": "Message shown with the CSAT survey.",
+ "example": "Please rate your conversation"
+ },
+ "button_text": {
+ "type": "string",
+ "description": "Text shown on the CSAT survey button.",
+ "example": "Please rate us"
+ },
+ "language": {
+ "type": "string",
+ "description": "Language code for the CSAT survey.",
+ "example": "en"
+ },
+ "survey_rules": {
+ "type": "object",
+ "description": "Rules that decide when to show the CSAT survey.",
+ "properties": {
+ "operator": {
+ "type": "string",
+ "example": "contains"
+ },
+ "values": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "example": [
+ "billing"
+ ]
+ }
+ }
+ }
+ }
+ },
"enable_auto_assignment": {
"type": "boolean",
- "description": "Enable Auto Assignment",
+ "description": "Enable Auto Assignment.",
"example": true
},
"working_hours_enabled": {
"type": "boolean",
- "description": "Enable working hours",
+ "description": "Enable working hours.",
"example": true
},
"out_of_office_message": {
"type": "string",
- "description": "Out of office message to be displayed on the widget",
+ "description": "Out of office message to send outside working hours.",
"example": "We are currently out of office. Please leave a message and we will get back to you."
},
"timezone": {
"type": "string",
- "description": "Timezone of the inbox",
+ "description": "Timezone of the inbox.",
"example": "America/New_York"
},
"allow_messages_after_resolved": {
"type": "boolean",
- "description": "Allow messages after conversation is resolved",
+ "description": "Allow messages after conversation is resolved.\n\nAvailable for: `Website`\n",
"example": true
},
"lock_to_single_conversation": {
"type": "boolean",
- "description": "Lock to single conversation",
+ "description": "Lock contact messages to a single active conversation.\n\nAvailable for: `API` `LINE` `Telegram` `WhatsApp` `SMS`\n",
"example": true
},
"portal_id": {
"type": "integer",
- "description": "Id of the help center portal to attach to the inbox",
+ "description": "Id of the help center portal to attach to the inbox.",
"example": 1
},
"sender_name_type": {
"type": "string",
- "description": "Sender name type for the inbox",
+ "description": "Sender name type for outbound email replies.\n\nAvailable for: `Website` `Email`\n",
"enum": [
"friendly",
"professional"
@@ -4163,32 +4267,808 @@
},
"business_name": {
"type": "string",
- "description": "Business name for the inbox",
+ "description": "Business name for outbound email replies.\n\nAvailable for: `Website` `Email`\n",
"example": "My Business"
},
"channel": {
- "type": "object",
- "properties": {
- "website_url": {
- "type": "string",
- "description": "URL at which the widget will be loaded",
- "example": "https://example.com"
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/inbox_update_web_widget_channel_payload"
},
- "welcome_title": {
- "type": "string",
- "description": "Welcome title to be displayed on the widget",
- "example": "Welcome to our support"
+ {
+ "$ref": "#/components/schemas/inbox_update_api_channel_payload"
},
- "welcome_tagline": {
- "type": "string",
- "description": "Welcome tagline to be displayed on the widget",
- "example": "We are here to help you"
+ {
+ "$ref": "#/components/schemas/inbox_update_email_channel_payload"
},
- "widget_color": {
- "type": "string",
- "description": "A Hex-color string used to customize the widget",
- "example": "#FF5733"
+ {
+ "$ref": "#/components/schemas/inbox_update_line_channel_payload"
+ },
+ {
+ "$ref": "#/components/schemas/inbox_update_telegram_channel_payload"
+ },
+ {
+ "$ref": "#/components/schemas/inbox_update_whatsapp_channel_payload"
+ },
+ {
+ "$ref": "#/components/schemas/inbox_update_sms_channel_payload"
}
+ ]
+ }
+ }
+ },
+ "inbox_create_web_widget_channel_payload": {
+ "type": "object",
+ "title": "Website channel",
+ "required": [
+ "type",
+ "website_url"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": [
+ "web_widget"
+ ],
+ "example": "web_widget"
+ },
+ "website_url": {
+ "type": "string",
+ "description": "URL at which the widget will be loaded",
+ "example": "https://example.com"
+ },
+ "welcome_title": {
+ "type": "string",
+ "description": "Welcome title to be displayed on the widget",
+ "example": "Welcome to our support"
+ },
+ "welcome_tagline": {
+ "type": "string",
+ "description": "Welcome tagline to be displayed on the widget",
+ "example": "We are here to help you"
+ },
+ "widget_color": {
+ "type": "string",
+ "description": "A Hex-color string used to customize the widget",
+ "example": "#FF5733"
+ },
+ "reply_time": {
+ "type": "string",
+ "description": "Expected reply time shown on the widget",
+ "enum": [
+ "in_a_few_minutes",
+ "in_a_few_hours",
+ "in_a_day"
+ ],
+ "example": "in_a_few_minutes"
+ },
+ "pre_chat_form_enabled": {
+ "type": "boolean",
+ "description": "Enable the pre-chat form before starting a conversation",
+ "example": false
+ },
+ "pre_chat_form_options": {
+ "type": "object",
+ "description": "Pre-chat form configuration",
+ "example": {
+ "pre_chat_message": "Share your queries or comments here.",
+ "pre_chat_fields": [
+ {
+ "field_type": "standard",
+ "label": "Email Id",
+ "name": "emailAddress",
+ "type": "email",
+ "required": true,
+ "enabled": true
+ }
+ ]
+ }
+ },
+ "continuity_via_email": {
+ "type": "boolean",
+ "description": "Continue conversations over email when the contact leaves the website",
+ "example": true
+ },
+ "hmac_mandatory": {
+ "type": "boolean",
+ "description": "Require HMAC verification for contacts using the widget",
+ "example": false
+ },
+ "allowed_domains": {
+ "type": "string",
+ "description": "Comma-separated list of domains where the widget is allowed to load",
+ "example": "example.com"
+ },
+ "selected_feature_flags": {
+ "type": "array",
+ "description": "Enabled widget feature flags",
+ "items": {
+ "type": "string",
+ "enum": [
+ "attachments",
+ "emoji_picker",
+ "end_conversation",
+ "use_inbox_avatar_for_bot",
+ "allow_mobile_webview"
+ ]
+ },
+ "example": [
+ "attachments",
+ "emoji_picker",
+ "end_conversation"
+ ]
+ }
+ }
+ },
+ "inbox_create_api_channel_payload": {
+ "type": "object",
+ "title": "API channel",
+ "required": [
+ "type"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": [
+ "api"
+ ],
+ "example": "api"
+ },
+ "webhook_url": {
+ "type": "string",
+ "description": "Webhook URL for API channel inbox callbacks",
+ "example": "https://example.com/webhook"
+ },
+ "hmac_mandatory": {
+ "type": "boolean",
+ "description": "Require HMAC verification for incoming API channel messages",
+ "example": false
+ },
+ "additional_attributes": {
+ "type": "object",
+ "description": "Additional attributes stored on contacts created through the API channel",
+ "example": {
+ "source": "mobile_app"
+ }
+ }
+ }
+ },
+ "inbox_create_email_channel_payload": {
+ "type": "object",
+ "title": "Email channel",
+ "required": [
+ "type",
+ "email"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": [
+ "email"
+ ],
+ "example": "email"
+ },
+ "email": {
+ "type": "string",
+ "description": "Email address for the inbox",
+ "example": "support@example.com"
+ },
+ "imap_enabled": {
+ "type": "boolean",
+ "description": "Enable IMAP for inbound emails",
+ "example": true
+ },
+ "imap_login": {
+ "type": "string",
+ "description": "IMAP login username",
+ "example": "support@example.com"
+ },
+ "imap_password": {
+ "type": "string",
+ "description": "IMAP login password",
+ "example": "your-imap-password"
+ },
+ "imap_address": {
+ "type": "string",
+ "description": "IMAP server address",
+ "example": "imap.example.com"
+ },
+ "imap_port": {
+ "type": "integer",
+ "description": "IMAP server port",
+ "example": 993
+ },
+ "imap_enable_ssl": {
+ "type": "boolean",
+ "description": "Enable SSL for IMAP",
+ "example": true
+ },
+ "imap_authentication": {
+ "type": "string",
+ "description": "IMAP authentication method",
+ "example": "plain"
+ },
+ "smtp_enabled": {
+ "type": "boolean",
+ "description": "Enable SMTP for outbound emails",
+ "example": true
+ },
+ "smtp_login": {
+ "type": "string",
+ "description": "SMTP login username",
+ "example": "support@example.com"
+ },
+ "smtp_password": {
+ "type": "string",
+ "description": "SMTP login password",
+ "example": "your-smtp-password"
+ },
+ "smtp_address": {
+ "type": "string",
+ "description": "SMTP server address",
+ "example": "smtp.example.com"
+ },
+ "smtp_port": {
+ "type": "integer",
+ "description": "SMTP server port",
+ "example": 587
+ },
+ "smtp_domain": {
+ "type": "string",
+ "description": "SMTP HELO domain",
+ "example": "example.com"
+ },
+ "smtp_enable_starttls_auto": {
+ "type": "boolean",
+ "description": "Automatically enable STARTTLS for SMTP",
+ "example": true
+ },
+ "smtp_enable_ssl_tls": {
+ "type": "boolean",
+ "description": "Enable SSL/TLS for SMTP",
+ "example": false
+ },
+ "smtp_openssl_verify_mode": {
+ "type": "string",
+ "description": "OpenSSL certificate verification mode for SMTP",
+ "example": "none"
+ },
+ "smtp_authentication": {
+ "type": "string",
+ "description": "SMTP authentication method",
+ "example": "login"
+ },
+ "provider": {
+ "type": "string",
+ "description": "Email provider",
+ "example": "google"
+ },
+ "verified_for_sending": {
+ "type": "boolean",
+ "description": "Whether the inbox is verified for sending emails",
+ "example": false
+ }
+ }
+ },
+ "inbox_create_line_channel_payload": {
+ "type": "object",
+ "title": "LINE channel",
+ "required": [
+ "type",
+ "line_channel_id",
+ "line_channel_secret",
+ "line_channel_token"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": [
+ "line"
+ ],
+ "example": "line"
+ },
+ "line_channel_id": {
+ "type": "string",
+ "description": "LINE channel ID",
+ "example": "1234567890"
+ },
+ "line_channel_secret": {
+ "type": "string",
+ "description": "LINE channel secret",
+ "example": "line-channel-secret"
+ },
+ "line_channel_token": {
+ "type": "string",
+ "description": "LINE channel access token",
+ "example": "line-channel-token"
+ }
+ }
+ },
+ "inbox_create_telegram_channel_payload": {
+ "type": "object",
+ "title": "Telegram channel",
+ "required": [
+ "type",
+ "bot_token"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": [
+ "telegram"
+ ],
+ "example": "telegram"
+ },
+ "bot_token": {
+ "type": "string",
+ "description": "Telegram bot token",
+ "example": "123456789:telegram-bot-token"
+ }
+ }
+ },
+ "inbox_create_whatsapp_channel_payload": {
+ "oneOf": [
+ {
+ "type": "object",
+ "title": "WhatsApp Cloud channel",
+ "required": [
+ "type",
+ "phone_number",
+ "provider",
+ "provider_config"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": [
+ "whatsapp"
+ ],
+ "example": "whatsapp"
+ },
+ "phone_number": {
+ "type": "string",
+ "description": "WhatsApp phone number",
+ "example": "+15551234567"
+ },
+ "provider": {
+ "type": "string",
+ "description": "WhatsApp provider",
+ "enum": [
+ "whatsapp_cloud"
+ ],
+ "example": "whatsapp_cloud"
+ },
+ "provider_config": {
+ "type": "object",
+ "description": "WhatsApp Cloud provider configuration",
+ "required": [
+ "api_key",
+ "phone_number_id",
+ "business_account_id"
+ ],
+ "properties": {
+ "api_key": {
+ "type": "string",
+ "description": "WhatsApp Cloud API key",
+ "example": "your-api-key"
+ },
+ "phone_number_id": {
+ "type": "string",
+ "description": "Phone number ID for WhatsApp Cloud",
+ "example": "your-phone-number-id"
+ },
+ "business_account_id": {
+ "type": "string",
+ "description": "Business account ID for WhatsApp Cloud",
+ "example": "your-business-account-id"
+ }
+ },
+ "example": {
+ "api_key": "your-api-key",
+ "phone_number_id": "your-phone-number-id",
+ "business_account_id": "your-business-account-id"
+ }
+ }
+ }
+ },
+ {
+ "type": "object",
+ "title": "Legacy 360dialog WhatsApp channel",
+ "deprecated": true,
+ "required": [
+ "type",
+ "phone_number",
+ "provider_config"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": [
+ "whatsapp"
+ ],
+ "example": "whatsapp"
+ },
+ "phone_number": {
+ "type": "string",
+ "description": "WhatsApp phone number",
+ "example": "+15551234567"
+ },
+ "provider": {
+ "type": "string",
+ "description": "Legacy 360dialog provider. Omit this field or use `default` only for existing deprecated 360dialog setups.",
+ "enum": [
+ "default"
+ ],
+ "deprecated": true,
+ "example": "default"
+ },
+ "provider_config": {
+ "type": "object",
+ "description": "Legacy 360dialog provider configuration",
+ "required": [
+ "api_key"
+ ],
+ "properties": {
+ "api_key": {
+ "type": "string",
+ "description": "360dialog API key",
+ "example": "your-api-key"
+ }
+ },
+ "example": {
+ "api_key": "your-api-key"
+ }
+ }
+ }
+ }
+ ]
+ },
+ "inbox_create_sms_channel_payload": {
+ "type": "object",
+ "title": "SMS channel",
+ "required": [
+ "type",
+ "phone_number"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": [
+ "sms"
+ ],
+ "example": "sms"
+ },
+ "phone_number": {
+ "type": "string",
+ "description": "SMS phone number",
+ "example": "+15551234567"
+ },
+ "provider_config": {
+ "type": "object",
+ "description": "Provider-specific SMS configuration",
+ "example": {
+ "account_id": "your-account-id",
+ "application_id": "your-application-id"
+ }
+ }
+ }
+ },
+ "inbox_update_web_widget_channel_payload": {
+ "type": "object",
+ "title": "Website channel settings",
+ "properties": {
+ "website_url": {
+ "type": "string",
+ "description": "URL at which the widget will be loaded",
+ "example": "https://example.com"
+ },
+ "welcome_title": {
+ "type": "string",
+ "description": "Welcome title to be displayed on the widget",
+ "example": "Welcome to our support"
+ },
+ "welcome_tagline": {
+ "type": "string",
+ "description": "Welcome tagline to be displayed on the widget",
+ "example": "We are here to help you"
+ },
+ "widget_color": {
+ "type": "string",
+ "description": "A Hex-color string used to customize the widget",
+ "example": "#FF5733"
+ },
+ "reply_time": {
+ "type": "string",
+ "description": "Expected reply time shown on the widget",
+ "enum": [
+ "in_a_few_minutes",
+ "in_a_few_hours",
+ "in_a_day"
+ ],
+ "example": "in_a_few_minutes"
+ },
+ "pre_chat_form_enabled": {
+ "type": "boolean",
+ "description": "Enable the pre-chat form before starting a conversation",
+ "example": false
+ },
+ "pre_chat_form_options": {
+ "type": "object",
+ "description": "Pre-chat form configuration",
+ "example": {
+ "pre_chat_message": "Share your queries or comments here.",
+ "pre_chat_fields": [
+ {
+ "field_type": "standard",
+ "label": "Email Id",
+ "name": "emailAddress",
+ "type": "email",
+ "required": true,
+ "enabled": true
+ }
+ ]
+ }
+ },
+ "continuity_via_email": {
+ "type": "boolean",
+ "description": "Continue conversations over email when the contact leaves the website",
+ "example": true
+ },
+ "hmac_mandatory": {
+ "type": "boolean",
+ "description": "Require HMAC verification for contacts using the widget",
+ "example": false
+ },
+ "allowed_domains": {
+ "type": "string",
+ "description": "Comma-separated list of domains where the widget is allowed to load",
+ "example": "example.com"
+ },
+ "selected_feature_flags": {
+ "type": "array",
+ "description": "Enabled widget feature flags",
+ "items": {
+ "type": "string",
+ "enum": [
+ "attachments",
+ "emoji_picker",
+ "end_conversation",
+ "use_inbox_avatar_for_bot",
+ "allow_mobile_webview"
+ ]
+ },
+ "example": [
+ "attachments",
+ "emoji_picker",
+ "end_conversation"
+ ]
+ }
+ }
+ },
+ "inbox_update_api_channel_payload": {
+ "type": "object",
+ "title": "API channel settings",
+ "properties": {
+ "webhook_url": {
+ "type": "string",
+ "description": "Webhook URL for API channel inbox callbacks",
+ "example": "https://example.com/webhook"
+ },
+ "hmac_mandatory": {
+ "type": "boolean",
+ "description": "Require HMAC verification for incoming API channel messages",
+ "example": false
+ },
+ "additional_attributes": {
+ "type": "object",
+ "description": "Additional attributes stored on contacts created through the API channel",
+ "example": {
+ "source": "mobile_app"
+ }
+ }
+ }
+ },
+ "inbox_update_email_channel_payload": {
+ "type": "object",
+ "title": "Email channel settings",
+ "properties": {
+ "email": {
+ "type": "string",
+ "description": "Email address for the inbox",
+ "example": "support@example.com"
+ },
+ "imap_enabled": {
+ "type": "boolean",
+ "description": "Enable IMAP for inbound emails",
+ "example": true
+ },
+ "imap_login": {
+ "type": "string",
+ "description": "IMAP login username",
+ "example": "support@example.com"
+ },
+ "imap_password": {
+ "type": "string",
+ "description": "IMAP login password",
+ "example": "your-imap-password"
+ },
+ "imap_address": {
+ "type": "string",
+ "description": "IMAP server address",
+ "example": "imap.example.com"
+ },
+ "imap_port": {
+ "type": "integer",
+ "description": "IMAP server port",
+ "example": 993
+ },
+ "imap_enable_ssl": {
+ "type": "boolean",
+ "description": "Enable SSL for IMAP",
+ "example": true
+ },
+ "imap_authentication": {
+ "type": "string",
+ "description": "IMAP authentication method",
+ "example": "plain"
+ },
+ "smtp_enabled": {
+ "type": "boolean",
+ "description": "Enable SMTP for outbound emails",
+ "example": true
+ },
+ "smtp_login": {
+ "type": "string",
+ "description": "SMTP login username",
+ "example": "support@example.com"
+ },
+ "smtp_password": {
+ "type": "string",
+ "description": "SMTP login password",
+ "example": "your-smtp-password"
+ },
+ "smtp_address": {
+ "type": "string",
+ "description": "SMTP server address",
+ "example": "smtp.example.com"
+ },
+ "smtp_port": {
+ "type": "integer",
+ "description": "SMTP server port",
+ "example": 587
+ },
+ "smtp_domain": {
+ "type": "string",
+ "description": "SMTP HELO domain",
+ "example": "example.com"
+ },
+ "smtp_enable_starttls_auto": {
+ "type": "boolean",
+ "description": "Automatically enable STARTTLS for SMTP",
+ "example": true
+ },
+ "smtp_enable_ssl_tls": {
+ "type": "boolean",
+ "description": "Enable SSL/TLS for SMTP",
+ "example": false
+ },
+ "smtp_openssl_verify_mode": {
+ "type": "string",
+ "description": "OpenSSL certificate verification mode for SMTP",
+ "example": "none"
+ },
+ "smtp_authentication": {
+ "type": "string",
+ "description": "SMTP authentication method",
+ "example": "login"
+ },
+ "provider": {
+ "type": "string",
+ "description": "Email provider",
+ "example": "google"
+ },
+ "verified_for_sending": {
+ "type": "boolean",
+ "description": "Whether the inbox is verified for sending emails",
+ "example": false
+ }
+ }
+ },
+ "inbox_update_line_channel_payload": {
+ "type": "object",
+ "title": "LINE channel settings",
+ "properties": {
+ "line_channel_id": {
+ "type": "string",
+ "description": "LINE channel ID",
+ "example": "1234567890"
+ },
+ "line_channel_secret": {
+ "type": "string",
+ "description": "LINE channel secret",
+ "example": "line-channel-secret"
+ },
+ "line_channel_token": {
+ "type": "string",
+ "description": "LINE channel access token",
+ "example": "line-channel-token"
+ }
+ }
+ },
+ "inbox_update_telegram_channel_payload": {
+ "type": "object",
+ "title": "Telegram channel settings",
+ "properties": {
+ "bot_token": {
+ "type": "string",
+ "description": "Telegram bot token",
+ "example": "123456789:telegram-bot-token"
+ }
+ }
+ },
+ "inbox_update_whatsapp_channel_payload": {
+ "type": "object",
+ "title": "WhatsApp channel settings",
+ "properties": {
+ "phone_number": {
+ "type": "string",
+ "description": "WhatsApp phone number",
+ "example": "+15551234567"
+ },
+ "provider": {
+ "type": "string",
+ "description": "WhatsApp provider. `default` is supported only for existing deprecated 360dialog setups.",
+ "enum": [
+ "whatsapp_cloud",
+ "default"
+ ],
+ "example": "whatsapp_cloud"
+ },
+ "provider_config": {
+ "type": "object",
+ "description": "WhatsApp provider configuration. Cloud channels use `api_key`, `phone_number_id`, and `business_account_id`; legacy 360dialog channels use `api_key`.",
+ "properties": {
+ "api_key": {
+ "type": "string",
+ "description": "Provider API key",
+ "example": "your-api-key"
+ },
+ "phone_number_id": {
+ "type": "string",
+ "description": "Phone number ID for WhatsApp Cloud",
+ "example": "your-phone-number-id"
+ },
+ "business_account_id": {
+ "type": "string",
+ "description": "Business account ID for WhatsApp Cloud",
+ "example": "your-business-account-id"
+ }
+ },
+ "example": {
+ "api_key": "your-api-key",
+ "phone_number_id": "your-phone-number-id",
+ "business_account_id": "your-business-account-id"
+ }
+ }
+ }
+ },
+ "inbox_update_sms_channel_payload": {
+ "type": "object",
+ "title": "SMS channel settings",
+ "properties": {
+ "phone_number": {
+ "type": "string",
+ "description": "SMS phone number",
+ "example": "+15551234567"
+ },
+ "provider_config": {
+ "type": "object",
+ "description": "Provider-specific SMS configuration",
+ "example": {
+ "api_key": "your-api-key",
+ "api_secret": "your-api-secret",
+ "application_id": "your-application-id",
+ "account_id": "your-account-id"
}
}
}
@@ -4287,7 +5167,9 @@
"message_updated",
"contact_created",
"contact_updated",
- "webwidget_triggered"
+ "webwidget_triggered",
+ "conversation_typing_on",
+ "conversation_typing_off"
]
},
"description": "The events you want to subscribe to.",
diff --git a/theme/icons.js b/theme/icons.js
index 266c7ddfa..2bd218cb0 100644
--- a/theme/icons.js
+++ b/theme/icons.js
@@ -113,6 +113,23 @@ export const icons = {
width: 16,
height: 20,
},
+ 'file-pfx': {
+ body: `
+
+
+
+
+
+
+
+
+
+
+
+ `,
+ width: 16,
+ height: 20,
+ },
bin: {
body: ``,
width: 16,
@@ -297,6 +314,21 @@ export const icons = {
width: 24,
height: 24,
},
+ 'outlook-color': {
+ body: ``,
+ width: 1040.8409,
+ height: 742.6319,
+ },
+ 'instagram-color': {
+ body: ``,
+ width: 256,
+ height: 256,
+ },
+ 'line-color': {
+ body: ``,
+ width: 427,
+ height: 427,
+ },
voice: {
body: ``,
width: 24,
@@ -428,5 +460,10 @@ export const icons = {
width: 15,
height: 15,
},
+ 'tag-remove': {
+ body: ``,
+ width: 24,
+ height: 24,
+ },
/** Ends */
};
diff --git a/vite.config.ts b/vite.config.ts
index 17d47fc76..49f9af926 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -1,118 +1,17 @@
-///
-
-/**
-What's going on with library mode?
-
-Glad you asked, here's a quick rundown:
-
-1. vite-plugin-ruby will automatically bring all the entrypoints like dashbord and widget as input to vite.
-2. vite needs to be in library mode to build the SDK as a single file. (UMD) format and set `inlineDynamicImports` to true.
-3. But when setting `inlineDynamicImports` to true, vite will not be able to handle mutliple entrypoints.
-
-This puts us in a deadlock, now there are two ways around this, either add another separate build pipeline to
-the app using vanilla rollup or rspack or something. The second option is to remove sdk building from the main pipeline
-and build it separately using Vite itself, toggled by an ENV variable.
-
-`BUILD_MODE=library bin/vite build` should build only the SDK and save it to `public/packs/js/sdk.js`
-`bin/vite build` will build the rest of the app as usual. But exclude the SDK.
-
-We need to edit the `asset:precompile` rake task to include the SDK in the precompile list.
-*/
import { defineConfig } from 'vite';
import ruby from 'vite-plugin-ruby';
-import path from 'path';
import vue from '@vitejs/plugin-vue';
+import { aliases, vueOptions } from './vite.shared';
import yaml from '@rollup/plugin-yaml';
-const isLibraryMode = process.env.BUILD_MODE === 'library';
-const isTestMode = process.env.TEST === 'true';
-
-const vueOptions = {
- template: {
- compilerOptions: {
- isCustomElement: tag => ['ninja-keys'].includes(tag),
- },
- },
-};
-
-let plugins = [ruby(), vue(vueOptions), yaml()];
-
-if (isLibraryMode) {
- plugins = [];
-} else if (isTestMode) {
- plugins = [vue(vueOptions), yaml()];
-}
-
export default defineConfig({
- plugins: plugins,
- build: {
- rollupOptions: {
- output: {
- // [NOTE] when not in library mode, no new keys will be addedd or overwritten
- // setting dir: isLibraryMode ? 'public/packs' : undefined will not work
- ...(isLibraryMode
- ? {
- dir: 'public/packs',
- entryFileNames: chunkInfo => {
- if (chunkInfo.name === 'sdk') {
- return 'js/sdk.js';
- }
- return '[name].js';
- },
- }
- : {}),
- inlineDynamicImports: isLibraryMode, // Disable code-splitting for SDK
+ plugins: [ruby(), vue(vueOptions), yaml()],
+ css: {
+ preprocessorOptions: {
+ scss: {
+ api: 'modern-compiler',
},
},
- lib: isLibraryMode
- ? {
- entry: path.resolve(__dirname, './app/javascript/entrypoints/sdk.js'),
- formats: ['iife'], // IIFE format for single file
- name: 'sdk',
- }
- : undefined,
- },
- resolve: {
- alias: {
- vue: 'vue/dist/vue.esm-bundler.js',
- components: path.resolve('./app/javascript/dashboard/components'),
- next: path.resolve('./app/javascript/dashboard/components-next'),
- v3: path.resolve('./app/javascript/v3'),
- dashboard: path.resolve('./app/javascript/dashboard'),
- helpers: path.resolve('./app/javascript/shared/helpers'),
- shared: path.resolve('./app/javascript/shared'),
- survey: path.resolve('./app/javascript/survey'),
- widget: path.resolve('./app/javascript/widget'),
- assets: path.resolve('./app/javascript/dashboard/assets'),
- },
- },
- test: {
- environment: 'jsdom',
- include: ['app/**/*.{test,spec}.?(c|m)[jt]s?(x)'],
- coverage: {
- reporter: ['lcov', 'text'],
- include: ['app/**/*.js', 'app/**/*.vue'],
- exclude: [
- 'app/**/*.@(spec|stories|routes).js',
- '**/specs/**/*',
- '**/i18n/**/*',
- ],
- },
- globals: true,
- outputFile: 'coverage/sonar-report.xml',
- pool: 'threads',
- poolOptions: {
- threads: {
- singleThread: false,
- },
- },
- server: {
- deps: {
- inline: ['tinykeys', '@material/mwc-icon'],
- },
- },
- setupFiles: ['fake-indexeddb/auto', 'vitest.setup.js'],
- mockReset: true,
- clearMocks: true,
},
+ resolve: { alias: aliases },
});
diff --git a/vite.lib.config.ts b/vite.lib.config.ts
new file mode 100644
index 000000000..14d73f6ca
--- /dev/null
+++ b/vite.lib.config.ts
@@ -0,0 +1,33 @@
+/*
+ * SDK library build.
+ *
+ * vite-plugin-ruby pulls every entrypoint as input, but the SDK needs to ship
+ * as a single IIFE file (`inlineDynamicImports: true`), which is incompatible
+ * with multiple entrypoints. So the SDK gets its own pipeline:
+ *
+ * vite build --config vite.lib.config.ts → public/packs/js/sdk.js
+ *
+ * The `assets:precompile` rake task runs this alongside the main app build.
+ */
+import { defineConfig } from 'vite';
+import path from 'path';
+import { aliases } from './vite.shared';
+
+export default defineConfig({
+ build: {
+ rollupOptions: {
+ output: {
+ dir: 'public/packs',
+ entryFileNames: chunkInfo =>
+ chunkInfo.name === 'sdk' ? 'js/sdk.js' : '[name].js',
+ inlineDynamicImports: true,
+ },
+ },
+ lib: {
+ entry: path.resolve(__dirname, './app/javascript/entrypoints/sdk.js'),
+ formats: ['iife'],
+ name: 'sdk',
+ },
+ },
+ resolve: { alias: aliases },
+});
diff --git a/vite.shared.ts b/vite.shared.ts
new file mode 100644
index 000000000..d3892d571
--- /dev/null
+++ b/vite.shared.ts
@@ -0,0 +1,22 @@
+import path from 'path';
+
+export const aliases = {
+ vue: 'vue/dist/vue.esm-bundler.js',
+ components: path.resolve('./app/javascript/dashboard/components'),
+ next: path.resolve('./app/javascript/dashboard/components-next'),
+ v3: path.resolve('./app/javascript/v3'),
+ dashboard: path.resolve('./app/javascript/dashboard'),
+ helpers: path.resolve('./app/javascript/shared/helpers'),
+ shared: path.resolve('./app/javascript/shared'),
+ survey: path.resolve('./app/javascript/survey'),
+ widget: path.resolve('./app/javascript/widget'),
+ assets: path.resolve('./app/javascript/dashboard/assets'),
+};
+
+export const vueOptions = {
+ template: {
+ compilerOptions: {
+ isCustomElement: (tag: string) => ['ninja-keys'].includes(tag),
+ },
+ },
+};
diff --git a/vitest.config.ts b/vitest.config.ts
new file mode 100644
index 000000000..5533f3998
--- /dev/null
+++ b/vitest.config.ts
@@ -0,0 +1,39 @@
+///
+import { defineConfig } from 'vitest/config';
+import vue from '@vitejs/plugin-vue';
+import { aliases, vueOptions } from './vite.shared';
+import yaml from '@rollup/plugin-yaml';
+
+export default defineConfig({
+ plugins: [vue(vueOptions), yaml()],
+ resolve: { alias: aliases },
+ test: {
+ environment: 'jsdom',
+ include: ['app/**/*.{test,spec}.?(c|m)[jt]s?(x)'],
+ coverage: {
+ reporter: ['lcov', 'text'],
+ include: ['app/**/*.js', 'app/**/*.vue'],
+ exclude: [
+ 'app/**/*.@(spec|stories|routes).js',
+ '**/specs/**/*',
+ '**/i18n/**/*',
+ ],
+ },
+ globals: true,
+ outputFile: 'coverage/sonar-report.xml',
+ pool: 'threads',
+ poolOptions: {
+ threads: {
+ singleThread: false,
+ },
+ },
+ server: {
+ deps: {
+ inline: ['tinykeys', '@material/mwc-icon'],
+ },
+ },
+ setupFiles: ['fake-indexeddb/auto', 'vitest.setup.js'],
+ mockReset: true,
+ clearMocks: true,
+ },
+});