Compare commits

..
Author SHA1 Message Date
Muhsin Keloth e2d9bd65eb chore: enable CORS for APIs 2025-05-22 09:16:43 +05:30
293 changed files with 6120 additions and 39378 deletions
+3 -4
View File
@@ -7,8 +7,6 @@ gem 'rack-cors', '2.0.0', require: 'rack/cors'
gem 'rails', '~> 7.1'
# Reduces boot times through caching; required in config/boot.rb
gem 'bootsnap', require: false
# Turbo for SPA-like page acceleration
gem 'turbo-rails'
##-- rails application helper gems --##
gem 'acts-as-taggable-on'
@@ -35,8 +33,6 @@ gem 'liquid'
gem 'commonmarker'
# Validate Data against JSON Schema
gem 'json_schemer'
# used in swagger build
gem 'json_refs'
# Rack middleware for blocking & throttling abusive requests
gem 'rack-attack', '>= 6.7.0'
# a utility tool for streaming, flexible and safe downloading of remote files
@@ -200,6 +196,9 @@ group :development do
gem 'scss_lint', require: false
gem 'web-console', '>= 4.2.1'
# used in swagger build
gem 'json_refs'
# When we want to squash migrations
gem 'squasher'
-4
View File
@@ -815,9 +815,6 @@ GEM
i18n
timeout (0.4.3)
trailblazer-option (0.1.2)
turbo-rails (2.0.13)
actionpack (>= 7.1.0)
railties (>= 7.1.0)
twilio-ruby (5.77.0)
faraday (>= 0.9, < 3.0)
jwt (>= 1.5, < 3.0)
@@ -1007,7 +1004,6 @@ DEPENDENCIES
telephone_number
test-prof
time_diff
turbo-rails
twilio-ruby (~> 5.66)
twitty (~> 0.1.5)
tzinfo-data
@@ -152,13 +152,11 @@ class Messages::Instagram::BaseMessageBuilder < Messages::Messenger::MessageBuil
end
def message_already_exists?
find_message_by_source_id(@messaging[:message][:mid]).present?
end
cw_message = conversation.messages.where(
source_id: @messaging[:message][:mid]
).first
def find_message_by_source_id(source_id)
return unless source_id
@message = Message.find_by(source_id: source_id)
cw_message.present?
end
def all_unsupported_files?
@@ -68,7 +68,7 @@ class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController
def article_params
params.require(:article).permit(
:title, :slug, :position, :content, :description, :category_id, :author_id, :associated_article_id, :status,
:title, :slug, :position, :content, :description, :position, :category_id, :author_id, :associated_article_id, :status,
:locale, meta: [:title,
:description,
{ tags: [] }]
@@ -1,9 +1,9 @@
class Platform::Api::V1::UsersController < PlatformController
# ref: https://stackoverflow.com/a/45190318/939299
# set resource is called for other actions already in platform controller
# we want to add login and token to that chain as well
before_action(only: [:login, :token]) { set_resource }
before_action(only: [:login, :token]) { validate_platform_app_permissible }
# we want to add login to that chain as well
before_action(only: [:login]) { set_resource }
before_action(only: [:login]) { validate_platform_app_permissible }
def show; end
@@ -18,8 +18,6 @@ class Platform::Api::V1::UsersController < PlatformController
render json: { url: @resource.generate_sso_link }
end
def token; end
def update
@resource.assign_attributes(user_update_params)
@@ -1,19 +0,0 @@
class Public::Api::V1::Portals::SearchController < Public::Api::V1::Portals::BaseController
before_action :portal
layout 'portal'
def index
@query = params[:query]
@articles = @portal.articles.published
@articles = @articles.search(search_params) if @query.present?
@articles = @articles.page(params[:page]).per(10) # Add pagination
end
private
def search_params
params.permit(:query, :locale, :sort, :status, :page)
end
end
@@ -42,7 +42,6 @@ const initialState = {
conversationFaqs: false,
memories: false,
},
temperature: 1,
};
const state = reactive({ ...initialState });
@@ -88,7 +87,6 @@ const updateStateFromAssistant = assistant => {
conversationFaqs: config.feature_faq || false,
memories: config.feature_memory || false,
};
state.temperature = config.temperature || 1;
};
const handleBasicInfoUpdate = async () => {
@@ -138,7 +136,6 @@ const handleInstructionsUpdate = async () => {
const payload = {
config: {
...props.assistant.config,
temperature: state.temperature || 1,
instructions: state.instructions,
},
};
@@ -215,7 +212,7 @@ watch(
<!-- Instructions Section -->
<Accordion :title="t('CAPTAIN.ASSISTANTS.FORM.SECTIONS.INSTRUCTIONS')">
<div class="flex flex-col gap-4">
<div class="flex flex-col gap-4 pt-4">
<Editor
v-model="state.instructions"
:placeholder="t('CAPTAIN.ASSISTANTS.FORM.INSTRUCTIONS.PLACEHOLDER')"
@@ -224,25 +221,6 @@ watch(
:message-type="formErrors.instructions ? 'error' : 'info'"
/>
<div class="flex flex-col gap-2 mt-4">
<label class="text-sm font-medium text-n-slate-12">
{{ t('CAPTAIN.ASSISTANTS.FORM.TEMPERATURE.LABEL') }}
</label>
<div class="flex items-center gap-4">
<input
v-model="state.temperature"
type="range"
min="0"
max="1"
step="0.1"
class="w-full"
/>
<span class="text-sm text-n-slate-12">{{ state.temperature }}</span>
</div>
<p class="text-sm text-n-slate-11 italic">
{{ t('CAPTAIN.ASSISTANTS.FORM.TEMPERATURE.DESCRIPTION') }}
</p>
</div>
<div class="flex justify-end">
<Button
size="small"
@@ -3,9 +3,6 @@ import BaseActionCableConnector from '../../shared/helpers/BaseActionCableConnec
import DashboardAudioNotificationHelper from './AudioAlerts/DashboardAudioNotificationHelper';
import { BUS_EVENTS } from 'shared/constants/busEvents';
import { emitter } from 'shared/helpers/mitt';
import { useImpersonation } from 'dashboard/composables/useImpersonation';
const { isImpersonating } = useImpersonation();
class ActionCableConnector extends BaseActionCableConnector {
constructor(app, pubsubToken) {
@@ -55,7 +52,6 @@ class ActionCableConnector extends BaseActionCableConnector {
};
onPresenceUpdate = data => {
if (isImpersonating.value) return;
this.app.$store.dispatch('contacts/updatePresence', data.contacts);
this.app.$store.dispatch('agents/updatePresence', data.users);
this.app.$store.dispatch('setCurrentUserAvailability', data.users);
@@ -367,6 +367,7 @@
"CANCEL_ANYTIME": "You can change or cancel your plan anytime"
},
"ENTERPRISE_PAYWALL": {
"AVAILABLE_ON": "Captain AI feature is only available in a paid plan.",
"UPGRADE_PROMPT": "Upgrade your plan to get access to our assistants, copilot and more.",
"ASK_ADMIN": "Please reach out to your administrator for the upgrade."
},
@@ -409,10 +410,6 @@
"PLACEHOLDER": "Enter assistant name",
"ERROR": "The name is required"
},
"TEMPERATURE": {
"LABEL": "Response Temperature",
"DESCRIPTION": "Adjust how creative or restrictive the assistant's responses should be. Lower values produce more focused and deterministic responses, while higher values allow for more creative and varied outputs."
},
"DESCRIPTION": {
"LABEL": "Description",
"PLACEHOLDER": "Enter assistant description",
+3 -3
View File
@@ -1,9 +1,9 @@
import Rails from '@rails/ujs';
import { Turbo } from '@hotwired/turbo-rails';
import Turbolinks from 'turbolinks';
import '../portal/application.scss';
import { InitializationHelpers } from '../portal/portalHelpers';
Rails.start();
Turbo.start();
Turbolinks.start();
document.addEventListener('turbo:load', InitializationHelpers.onLoad);
document.addEventListener('turbolinks:load', InitializationHelpers.onLoad);
@@ -90,29 +90,18 @@ export default {
this.isLoading = false;
}
},
handleSubmit() {
if (this.searchTerm.trim()) {
const url = `/hc/${this.portalSlug}/${this.localeCode}/search?query=${encodeURIComponent(this.searchTerm)}`;
window.location.href = url;
}
},
},
};
</script>
<template>
<div v-on-clickaway="closeSearch" class="relative w-full max-w-5xl my-4">
<form @submit.prevent="handleSubmit">
<PublicSearchInput
:search-term="searchTerm"
:search-placeholder="searchTranslations.searchPlaceholder"
@update:search-term="onUpdateSearchTerm"
@focus="openSearch"
/>
<button type="submit" class="sr-only">
{{ searchTranslations.submit || 'Search' }}
</button>
</form>
<PublicSearchInput
:search-term="searchTerm"
:search-placeholder="searchTranslations.searchPlaceholder"
@update:search-term="onUpdateSearchTerm"
@focus="openSearch"
/>
<div
v-if="shouldShowSearchBox"
class="absolute w-full top-14"
@@ -110,7 +110,7 @@ export default {
<p class="py-1 px-3" :class="getClassName(element)">
<a
:href="`#${element.slug}`"
data-turbo="false"
data-turbolinks="false"
class="font-medium text-sm tracking-[0.28px] cursor-pointer"
:class="elementTextStyles(element)"
>
+2 -2
View File
@@ -15,7 +15,7 @@ export const getHeadingsfromTheArticle = () => {
const slug = slugifyWithCounter(element.innerText);
element.id = slug;
element.className = 'scroll-mt-24 heading';
element.innerHTML += `<a class="permalink text-slate-600 ml-3" href="#${slug}" title="${element.innerText}" data-turbo="false">#</a>`;
element.innerHTML += `<a class="permalink text-slate-600 ml-3" href="#${slug}" title="${element.innerText}" data-turbolinks="false">#</a>`;
rows.push({
slug,
title: element.innerText,
@@ -144,7 +144,7 @@ export const InitializationHelpers = {
const a = document.createElement('a');
a.href = window.location.hash;
a['data-turbo'] = false;
a['data-turbolinks'] = false;
a.click();
}
},
-2
View File
@@ -204,5 +204,3 @@ class ActionCableListener < BaseListener
::ActionCableBroadcastJob.perform_later(tokens.uniq, event_name, payload)
end
end
ActionCableListener.prepend_mod_with('ActionCableListener')
@@ -31,11 +31,7 @@ class Crm::Leadsquared::LeadFinderService
def find_by_phone_number(contact)
return if contact.phone_number.blank?
lead_data = Crm::Leadsquared::Mappers::ContactMapper.map(contact)
return if lead_data.blank? || lead_data['Mobile'].nil?
search_by_field(lead_data['Mobile'])
search_by_field(contact.phone_number)
end
def search_by_field(value)
@@ -20,29 +20,11 @@ class Crm::Leadsquared::Mappers::ContactMapper
'FirstName' => contact.name.presence,
'LastName' => contact.last_name.presence,
'EmailAddress' => contact.email.presence,
'Mobile' => formatted_phone_number,
'Mobile' => contact.phone_number.presence,
'Source' => brand_name
}.compact
end
def formatted_phone_number
# it seems like leadsquared needs a different phone number format
# it's not documented anywhere, so don't bother trying to look up online
# After some trial and error, I figured out the format, its +<country_code>-<national_number>
return nil if contact.phone_number.blank?
parsed = TelephoneNumber.parse(contact.phone_number)
return contact.phone_number unless parsed.valid?
country_code = parsed.country.country_code
e164 = parsed.e164_number
e164 = e164.sub(/^\+/, '')
national_number = e164.sub(/^#{Regexp.escape(country_code)}/, '')
"+#{country_code}-#{national_number}"
end
def brand_name
::GlobalConfig.get('BRAND_NAME')['BRAND_NAME'] || 'Chatwoot'
end
@@ -27,7 +27,7 @@ class MessageTemplates::HookExecutionService
return false unless message.incoming?
# prevents sending out-of-office message if an agent has sent a message in last 5 minutes
# ensures better UX by not interrupting active conversations at the end of business hours
return false if conversation.messages.outgoing.where(private: false).exists?(['created_at > ?', 5.minutes.ago])
return false if conversation.messages.outgoing.exists?(['created_at > ?', 5.minutes.ago])
inbox.out_of_office? && conversation.messages.today.template.empty? && inbox.out_of_office_message.present?
end
+2 -2
View File
@@ -18,7 +18,7 @@ By default, it renders:
<head>
<meta charset="utf-8">
<meta name="viewport" content="initial-scale=1">
<meta name= "turbo-cache-control" content= "no-cache">
<meta name= "turbolinks-cache-control" content= "no-cache">
<meta name="viewport" content="width=device-width, initial-scale=1" />
<%= vite_client_tag %>
@@ -39,7 +39,7 @@ By default, it renders:
<% else %>
<title><%= @portal.page_title%></title>
<% end %>
<% if @portal.logo.present? %>
<link rel="icon" href="<%= url_for(@portal.logo) %>">
<% end %>
@@ -1,9 +0,0 @@
json.access_token @resource.access_token.token
json.expiry nil
json.user do
json.id @resource.id
json.name @resource.name
json.display_name @resource.display_name
json.email @resource.email
json.pubsub_token @resource.pubsub_token
end
@@ -1,184 +0,0 @@
<% content_for :head do %>
<title><%= I18n.t('public_portal.search.results_for', query: @query) %> | <%= @portal.name %></title>
<% end %>
<% if !@is_plain_layout_enabled %>
<div id="portal-bg" class="bg-white dark:bg-slate-900 shadow-inner">
<div id="portal-bg-gradient" class="pt-8 pb-8 md:pt-14 md:pb-6">
<div class="max-w-5xl px-4 md:px-8 mx-auto flex flex-col">
<div class="flex flex-row items-center gap-px mb-6">
<a class="text-slate-500 dark:text-slate-200 text-sm gap-1 hover:cursor-pointer hover:underline leading-8 font-semibold"
href="<%= generate_home_link(@portal.slug, params[:locale], @theme_from_params, @is_plain_layout_enabled) %>">
<%= I18n.t('public_portal.common.home') %>
</a>
<span class="w-4 h-4 [&>svg]:w-3 [&>svg]:h-3 flex items-center justify-center text-xs text-slate-500 dark:text-slate-300">
<%= render partial: 'icons/chevron-right' %>
</span>
<span class="text-sm font-semibold text-slate-800 dark:text-slate-100">
<%= I18n.t('public_portal.search.results') %>
</span>
</div>
<h1 class="text-3xl font-semibold leading-normal md:tracking-normal md:text-4xl text-slate-900 dark:text-white">
<%= I18n.t('public_portal.search.results_for', query: @query) %>
</h1>
<!-- Search form for the results page -->
<form class="w-full my-4" action="<%= request.path %>" method="GET" data-search-form>
<input type="hidden" name="locale" value="<%= params[:locale] %>">
<input type="text"
name="query"
value="<%= @query %>"
placeholder="<%= I18n.t('public_portal.search.search_placeholder') %>"
data-search-input
autofocus
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">
<button type="submit" class="sr-only">
<%= I18n.t('public_portal.search.submit') %>
</button>
</form>
</div>
</div>
</div>
<% else %>
<div class="max-w-5xl mx-auto space-y-4 w-full px-4 md:px-8 py-4">
<div class="flex items-center flex-row">
<a class="text-slate-500 dark:text-slate-200 text-sm gap-1 hover:underline hover:cursor-pointer leading-8 font-semibold"
href="<%= generate_home_link(@portal.slug, params[:locale], @theme_from_params, @is_plain_layout_enabled) %>">
<%= I18n.t('public_portal.common.home') %>
</a>
<span class="w-4 h-4 [&>svg]:w-3 [&>svg]:h-3 flex items-center justify-center text-xs text-slate-500 dark:text-slate-300">
<%= render partial: 'icons/chevron-right' %>
</span>
<span class="text-sm font-semibold text-slate-800 dark:text-slate-100">
<%= I18n.t('public_portal.search.results') %>
</span>
</div>
<h1 class="text-3xl font-semibold leading-normal md:tracking-normal md:text-4xl text-slate-900 dark:text-white">
<%= I18n.t('public_portal.search.results_for', query: @query) %>
</h1>
<!-- Search form for the results page -->
<form class="w-full my-4" action="<%= request.path %>" method="GET" data-search-form>
<input type="hidden" name="locale" value="<%= params[:locale] %>">
<input type="text"
name="query"
value="<%= @query %>"
placeholder="<%= I18n.t('public_portal.search.search_placeholder') %>"
data-search-input
autofocus
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">
<button type="submit" class="sr-only">
<%= I18n.t('public_portal.search.submit') %>
</button>
</form>
</div>
<% end %>
<script>
document.addEventListener('turbo:load', function() {
const searchInputs = document.querySelectorAll('[data-search-input]');
searchInputs.forEach(function(input) {
let debounceTimer;
// Move cursor to end of input text after autofocus
if (input.value.length > 0) {
setTimeout(function() {
input.setSelectionRange(input.value.length, input.value.length);
}, 0);
}
input.addEventListener('input', function() {
const form = input.closest('[data-search-form]');
const query = input.value.trim();
// Clear existing timer
clearTimeout(debounceTimer);
// Only search if there's a query and it's different from current
if (query.length > 0) {
debounceTimer = setTimeout(function() {
// Check if the query has actually changed
const currentQuery = new URLSearchParams(window.location.search).get('query') || '';
if (query !== currentQuery) {
// Use Turbo.visit for SPA-like navigation
const formData = new FormData(form);
const searchParams = new URLSearchParams(formData);
const url = form.action + '?' + searchParams.toString();
Turbo.visit(url);
}
}, 500); // 500ms debounce
}
});
// Handle manual form submission
input.closest('[data-search-form]').addEventListener('submit', function(e) {
clearTimeout(debounceTimer);
});
});
});
</script>
<div class="max-w-5xl w-full mx-auto px-4 md:px-8 py-6 flex flex-col items-center justify-center flex-grow">
<div class="w-full flex flex-col gap-6 flex-grow">
<% if @articles.empty? %>
<div class="h-full flex items-center justify-center bg-slate-50 dark:bg-slate-800 rounded-xl py-6">
<p class="text-sm text-slate-500"><%= I18n.t('public_portal.search.no_results', query: @query) %></p>
</div>
<% else %>
<p class="text-sm text-slate-600 dark:text-slate-400">
<%= I18n.t('public_portal.search.found_results', count: @articles.size) %>
</p>
<% @articles.each do |article| %>
<div class="border border-solid border-slate-100 dark:border-slate-800 rounded-lg">
<a class="p-4 text-slate-800 dark:text-slate-50 flex justify-between content-center hover:cursor-pointer"
href="<%= generate_article_link(@portal.slug, article.slug, @theme_from_params, @is_plain_layout_enabled) %>">
<div class="flex flex-col gap-5">
<div class="flex flex-col gap-1">
<h3 class="text-lg text-slate-900 tracking-[0.28px] dark:text-slate-50 font-semibold">
<%= article.title %>
</h3>
<p class="text-base font-normal text-slate-600 dark:text-slate-200 line-clamp-2 break-all">
<%= render_category_content(article.content) %>
</p>
</div>
<div class="flex flex-row items-center gap-2">
<% if article.category.present? %>
<span class="text-sm text-slate-600 dark:text-slate-400 font-medium">
<%= article.category.name %>
</span>
<span class="text-slate-600 dark:text-slate-400"></span>
<% end %>
<span class="text-sm text-slate-600 dark:text-slate-400 font-medium">
<%= I18n.t('public_portal.common.last_updated_on', last_updated_on: article.updated_at.strftime("%b %d, %Y")) %>
</span>
</div>
</div>
</a>
</div>
<% end %>
<!-- Pagination -->
<% if @articles.respond_to?(:total_pages) && @articles.total_pages > 1 %>
<div class="flex justify-center mt-6">
<nav class="inline-flex">
<% if @articles.prev_page %>
<a href="<%= url_for(page: @articles.prev_page) %>" class="px-3 py-2 border border-slate-200 dark:border-slate-700 rounded-l-md text-sm font-medium text-slate-700 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-800">
<%= I18n.t('public_portal.common.previous') %>
</a>
<% end %>
<% if @articles.next_page %>
<a href="<%= url_for(page: @articles.next_page) %>" class="px-3 py-2 border border-slate-200 dark:border-slate-700 rounded-r-md text-sm font-medium text-slate-700 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-800">
<%= I18n.t('public_portal.common.next') %>
</a>
<% end %>
</nav>
</div>
<% end %>
<% end %>
</div>
</div>
+1
View File
@@ -15,6 +15,7 @@ Rails.application.config.middleware.insert_before 0, Rack::Cors do
resource '*', headers: :any, methods: :any, expose: %w[access-token client uid expiry]
end
# This is temporary fix for the issue the loading APIs from the frontend, we will remove this once we have a proper solution
if ActiveModel::Type::Boolean.new.cast(ENV.fetch('ENABLE_API_CORS', false))
resource '/api/*', headers: :any, methods: :any, expose: %w[access-token client uid expiry]
end
-7
View File
@@ -254,13 +254,6 @@ 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'
hero:
sub_title: Search for the articles here or browse the categories below.
-7
View File
@@ -240,13 +240,6 @@ es:
empty_placeholder: No se encontraron resultados.
loading_placeholder: Buscando...
results_title: Buscar resultados
results: Resultados de búsqueda
results_for: "Resultados de búsqueda para '%{query}'"
no_results: "No se encontraron resultados para '%{query}'"
found_results:
one: Se encontró 1 resultado
other: "Se encontraron %{count} resultados"
submit: Buscar
toc_header: 'En esta página'
hero:
sub_title: Busque aquí los artículos o busque las categorías de abajo.
+2 -4
View File
@@ -60,8 +60,8 @@ Rails.application.routes.draw do
end
resources :assistant_responses
resources :bulk_actions, only: [:create]
resources :copilot_threads, only: [:index, :create] do
resources :copilot_messages, only: [:index, :create]
resources :copilot_threads, only: [:index] do
resources :copilot_messages, only: [:index]
end
resources :documents, only: [:index, :show, :create, :destroy]
end
@@ -396,7 +396,6 @@ Rails.application.routes.draw do
resources :users, only: [:create, :show, :update, :destroy] do
member do
get :login
post :token
end
end
resources :agent_bots, only: [:index, :create, :show, :update, :destroy] do
@@ -442,7 +441,6 @@ 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'
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'
@@ -1,8 +0,0 @@
class RemoveUuidFromCopilotThreads < ActiveRecord::Migration[7.1]
def change
remove_column :copilot_threads, :uuid, :string
add_column :copilot_threads, :assistant_id, :integer
add_index :copilot_threads, :assistant_id
end
end
@@ -1,5 +0,0 @@
class RemoveUserIdFromCopilotMessages < ActiveRecord::Migration[7.1]
def change
remove_reference :copilot_messages, :user, index: true
end
end
@@ -1,11 +0,0 @@
class ChangeMessageTypeToIntegerInCopilotMessages < ActiveRecord::Migration[7.1]
def up
remove_column :copilot_messages, :message_type
add_column :copilot_messages, :message_type, :integer, default: 0
end
def down
remove_column :copilot_messages, :message_type
add_column :copilot_messages, :message_type, :string, default: 'user'
end
end
+6 -4
View File
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.1].define(version: 2025_05_23_031839) do
ActiveRecord::Schema[7.0].define(version: 2025_05_14_045638) do
# These extensions should be enabled to support this database
enable_extension "pg_stat_statements"
enable_extension "pg_trgm"
@@ -577,25 +577,27 @@ ActiveRecord::Schema[7.1].define(version: 2025_05_23_031839) do
create_table "copilot_messages", force: :cascade do |t|
t.bigint "copilot_thread_id", null: false
t.bigint "user_id", null: false
t.bigint "account_id", null: false
t.string "message_type", null: false
t.jsonb "message", default: {}, null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "message_type", default: 0
t.index ["account_id"], name: "index_copilot_messages_on_account_id"
t.index ["copilot_thread_id"], name: "index_copilot_messages_on_copilot_thread_id"
t.index ["user_id"], name: "index_copilot_messages_on_user_id"
end
create_table "copilot_threads", force: :cascade do |t|
t.string "title", null: false
t.bigint "user_id", null: false
t.bigint "account_id", null: false
t.uuid "uuid", default: -> { "gen_random_uuid()" }, null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "assistant_id"
t.index ["account_id"], name: "index_copilot_threads_on_account_id"
t.index ["assistant_id"], name: "index_copilot_threads_on_assistant_id"
t.index ["user_id"], name: "index_copilot_threads_on_user_id"
t.index ["uuid"], name: "index_copilot_threads_on_uuid", unique: true
end
create_table "csat_survey_responses", force: :cascade do |t|
-28
View File
@@ -1,28 +0,0 @@
## Chatwoot Developer Documentation
Welcome to the official Chatwoot developer documentation. This guide contains everything you need to know about Chatwoot APIs and build custom flows on top of Chatwoot APIs.
### 👩‍💻 Development
Install the [Mintlify CLI](https://www.npmjs.com/package/mintlify) to preview the documentation changes locally. To install, use the following command
```
npm i -g mintlify
```
Run the following command at the root of your documentation (where mint.json is)
```
mintlify dev
```
### 😎 Publishing Changes
Changes will be deployed to production automatically after pushing to the default branch.
You can also preview changes using PRs, which generates a preview link of the docs.
#### Troubleshooting
- Mintlify dev isn't running - Run `mintlify install` it'll re-install dependencies.
- Page loads as a 404 - Make sure you are running in a folder with `mint.json`
-100
View File
@@ -1,100 +0,0 @@
{
"$schema": "https://mintlify.com/docs.json",
"name": "Chatwoot Developer Docs",
"description": "Official developer documentation for Chatwoot - the open-source customer support platform. Learn about our APIs, integrations, and development guidelines.",
"logo": {
"dark": "/logo/dark.png",
"light": "/logo/light.png"
},
"favicon": "/favicon.png",
"colors": {
"primary": "#0069ED",
"light": "#4D9CFF",
"dark": "#0050B4"
},
"fonts": {
"heading": {
"family": "Haskoy",
"weight": 500,
"source": "https://d1j5ayohogpcd2.cloudfront.net/fonts/haskoy/Haskoy-Medium.woff2",
"format": "woff2"
},
"body": {
"family": "Haskoy",
"weight": 400,
"source": "https://d1j5ayohogpcd2.cloudfront.net/fonts/haskoy/Haskoy-Regular.woff2",
"format": "woff2"
}
},
"theme": "maple",
"navigation": {
"groups": [
{
"group": "API Reference",
"pages": [
"introduction"
]
},
{
"group": "Application API",
"description": "APIs for managing application aspects of Chatwoot",
"includeTags": [
"Agents",
"Automation Rule",
"Account AgentBots",
"Canned Responses",
"Contact Labels",
"Contacts",
"Conversation Assignments",
"Conversation Labels",
"Conversations",
"Custom Attributes",
"Custom Filters",
"Help Center",
"Inboxes",
"Integrations",
"Messages",
"Profile",
"Reports",
"Teams",
"Webhooks"
],
"openapi": "https://raw.githubusercontent.com/chatwoot/chatwoot/develop/swagger/tag_groups/application_swagger.json"
},
{
"group": "Platform API",
"description": "APIs for managing platform aspects of Chatwoot",
"includeTags": [
"Accounts",
"Account Users",
"AgentBots",
"Users"
],
"openapi": "https://raw.githubusercontent.com/chatwoot/chatwoot/develop/swagger/tag_groups/platform_swagger.json"
},
{
"group": "Client API",
"description": "APIs for client applications",
"includeTags": [
"Contacts API",
"Conversations API",
"Messages API"
],
"openapi": "https://raw.githubusercontent.com/chatwoot/chatwoot/develop/swagger/tag_groups/client_swagger.json"
},
{
"group": "Other APIs",
"description": "Other Chatwoot APIs",
"includeTags": [
"CSAT Survey Page"
],
"openapi": "https://raw.githubusercontent.com/chatwoot/chatwoot/develop/swagger/tag_groups/other_swagger.json"
}
]
},
"footerSocials": {
"twitter": "https://twitter.com/chatwootapp",
"github": "https://github.com/chatwoot",
"linkedin": "https://www.linkedin.com/company/chatwoot"
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

-49
View File
@@ -1,49 +0,0 @@
---
title: Introduction to Chatwoot APIs
description: Learn how to use Chatwoot APIs to build integrations, customize chat experiences, and manage your installation.
sidebarTitle: Introduction
---
Welcome to the Chatwoot API documentation. Whether you're building custom workflows for your support team, integrating Chatwoot into your product, or managing users across installations, our APIs provide the flexibility and power to help you do more with Chatwoot.
Chatwoot provides three categories of APIs, each designed with a specific use case in mind:
- **Application APIs** For account-level automation and agent-facing integrations.
- **Client APIs** For building custom chat interfaces for end-users
- **Platform APIs** For managing and administering installations at scale
---
## Application APIs
Application APIs are designed for interacting with a Chatwoot account from an agent/admin perspective. Use them to build internal tools, automate workflows, or perform bulk operations like data import/export.
- **Authentication**: Requires a user `access_token`, which can be generated from **Profile Settings** after logging into your Chatwoot account.
- **Availability**: Supported on both **Cloud** and **Self-hosted** Chatwoot installations.
- **Example**: [Google Cloud Functions Demo](https://github.com/chatwoot/google-cloud-functions-demo)
---
## Client APIs
Client APIs are intended for building custom messaging experiences over Chatwoot. If you're not using the native website widget or want to embed chat in your mobile app, these APIs are the way to go.
- **Authentication**: Uses `inbox_identifier` (from **Settings → Configuration** in API inboxes) and `contact_identifier` (returned when creating a contact).
- **Availability**: Supported on both **Cloud** and **Self-hosted** Chatwoot installations.
- **Examples**:
- [Client API Demo](https://github.com/chatwoot/client-api-demo)
- [Flutter SDK](https://github.com/chatwoot/chatwoot-flutter-sdk)
---
## Platform APIs
Platform APIs are used to manage Chatwoot installations at the admin level. These APIs allow you to control users, roles, and accounts, or sync data from external authentication systems.
- **Authentication**: Requires an `access_token` generated by a **Platform App**, which can be created in the **Super Admin Console**.
- **Availability**: Available on **Self-hosted** / **Managed Hosting** Chatwoot installations only.
---
Use the right API for your use case, and you'll be able to extend, customize, and integrate Chatwoot into your stack with ease.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 168 KiB

@@ -47,7 +47,7 @@ class Api::V1::Accounts::Captain::AssistantsController < Api::V1::Accounts::Base
config: [
:product_name, :feature_faq, :feature_memory,
:welcome_message, :handoff_message, :resolution_message,
:instructions, :temperature
:instructions
])
end
@@ -1,28 +1,21 @@
class Api::V1::Accounts::Captain::CopilotMessagesController < Api::V1::Accounts::BaseController
before_action :current_account
before_action -> { check_authorization(Captain::Assistant) }
before_action :set_copilot_thread
def index
@copilot_messages = @copilot_thread
.copilot_messages
.includes(:copilot_thread)
.order(created_at: :asc)
.page(permitted_params[:page] || 1)
.per(1000)
end
def create
@copilot_message = @copilot_thread.copilot_messages.create!(
message: params[:message],
message_type: :user
)
end
private
def set_copilot_thread
@copilot_thread = Current.account.copilot_threads.find_by!(
id: params[:copilot_thread_id],
user: Current.user
uuid: params[:copilot_thread_id], user_id: Current.user.id
)
end
@@ -1,41 +1,18 @@
class Api::V1::Accounts::Captain::CopilotThreadsController < Api::V1::Accounts::BaseController
before_action :ensure_message, only: :create
before_action :current_account
before_action -> { check_authorization(Captain::Assistant) }
def index
@copilot_threads = Current.account.copilot_threads
.where(user_id: Current.user.id)
.includes(:user, :assistant)
.includes(:user)
.order(created_at: :desc)
.page(permitted_params[:page] || 1)
.per(5)
end
def create
ActiveRecord::Base.transaction do
@copilot_thread = Current.account.copilot_threads.create!(
title: copilot_thread_params[:message],
user: Current.user,
assistant: assistant
)
@copilot_thread.copilot_messages.create!(message_type: :user, message: copilot_thread_params[:message])
end
end
private
def ensure_message
return render_could_not_create_error('Message is required') if copilot_thread_params[:message].blank?
end
def assistant
Current.account.captain_assistants.find(copilot_thread_params[:assistant_id])
end
def copilot_thread_params
params.permit(:message, :assistant_id)
end
def permitted_params
params.permit(:page)
end
@@ -7,8 +7,7 @@ module Captain::ChatHelper
model: @model,
messages: @messages,
tools: @tool_registry&.registered_tools || [],
response_format: { type: 'json_object' },
temperature: @assistant&.config&.[]('temperature').to_f || 1
response_format: { type: 'json_object' }
}
)
@@ -1,11 +0,0 @@
module Enterprise::ActionCableListener
include Events::Types
def copilot_message_created(event)
copilot_message = event.data[:copilot_message]
copilot_thread = copilot_message.copilot_thread
account = copilot_thread.account
user = copilot_thread.user
broadcast(account, [user.pubsub_token], COPILOT_MESSAGE_CREATED, copilot_message.push_event_data)
end
end
@@ -29,7 +29,6 @@ class Captain::Assistant < ApplicationRecord
has_many :inboxes,
through: :captain_inboxes
has_many :messages, as: :sender, dependent: :nullify
has_many :copilot_threads, dependent: :destroy_async
validates :name, presence: true
validates :description, presence: true
+5 -28
View File
@@ -4,47 +4,24 @@
#
# id :bigint not null, primary key
# message :jsonb not null
# message_type :integer default("user")
# message_type :string not null
# created_at :datetime not null
# updated_at :datetime not null
# account_id :bigint not null
# copilot_thread_id :bigint not null
# user_id :bigint not null
#
# Indexes
#
# index_copilot_messages_on_account_id (account_id)
# index_copilot_messages_on_copilot_thread_id (copilot_thread_id)
# index_copilot_messages_on_user_id (user_id)
#
class CopilotMessage < ApplicationRecord
belongs_to :copilot_thread
belongs_to :user
belongs_to :account
before_validation :ensure_account
enum message_type: { user: 0, assistant: 1, assistant_thinking: 2 }
validates :message_type, presence: true, inclusion: { in: message_types.keys }
validates :message_type, presence: true, inclusion: { in: %w[user assistant assistant_thinking] }
validates :message, presence: true
after_create_commit :broadcast_message
def push_event_data
{
id: id,
message: message,
message_type: message_type,
created_at: created_at.to_i,
copilot_thread: copilot_thread.push_event_data
}
end
private
def ensure_account
self.account = copilot_thread.account
end
def broadcast_message
Rails.configuration.dispatcher.dispatch(COPILOT_MESSAGE_CREATED, Time.zone.now, copilot_message: self)
end
end
+12 -34
View File
@@ -2,47 +2,25 @@
#
# Table name: copilot_threads
#
# id :bigint not null, primary key
# title :string not null
# created_at :datetime not null
# updated_at :datetime not null
# account_id :bigint not null
# assistant_id :integer
# user_id :bigint not null
# id :bigint not null, primary key
# title :string not null
# uuid :uuid not null
# created_at :datetime not null
# updated_at :datetime not null
# account_id :bigint not null
# user_id :bigint not null
#
# Indexes
#
# index_copilot_threads_on_account_id (account_id)
# index_copilot_threads_on_assistant_id (assistant_id)
# index_copilot_threads_on_user_id (user_id)
# index_copilot_threads_on_account_id (account_id)
# index_copilot_threads_on_user_id (user_id)
# index_copilot_threads_on_uuid (uuid) UNIQUE
#
class CopilotThread < ApplicationRecord
belongs_to :user
belongs_to :account
belongs_to :assistant, class_name: 'Captain::Assistant'
has_many :copilot_messages, dependent: :destroy_async
has_many :copilot_messages, dependent: :destroy
validates :title, presence: true
def push_event_data
{
id: id,
title: title,
created_at: created_at.to_i,
user: user.push_event_data,
account_id: account_id
}
end
def previous_history
copilot_messages
.where(message_type: %w[user assistant])
.order(created_at: :asc)
.map do |copilot_message|
{
content: copilot_message.message,
role: copilot_message.message_type
}
end
end
validates :uuid, presence: true, uniqueness: true
end
@@ -1 +0,0 @@
json.partial! 'api/v1/models/captain/copilot_message', formats: [:json], resource: @copilot_message
@@ -1,5 +1,8 @@
json.payload do
json.array! @copilot_messages do |message|
json.partial! 'api/v1/models/captain/copilot_message', formats: [:json], resource: message
json.id message.id
json.message message.message
json.message_type message.message_type
json.created_at message.created_at.to_i
end
end
@@ -1 +0,0 @@
json.partial! 'api/v1/models/captain/copilot_thread', formats: [:json], resource: @copilot_thread
@@ -1,5 +1,12 @@
json.payload do
json.array! @copilot_threads do |thread|
json.partial! 'api/v1/models/captain/copilot_thread', resource: thread
json.id thread.id
json.title thread.title
json.uuid thread.uuid
json.created_at thread.created_at.to_i
json.user do
json.id thread.user.id
json.name thread.user.name
end
end
end
@@ -1,6 +0,0 @@
json.id resource.id
json.message resource.message
json.message_type resource.message_type
json.created_at resource.created_at.to_i
json.copilot_thread resource.copilot_thread.push_event_data
json.account_id resource.account_id
@@ -1,6 +0,0 @@
json.id resource.id
json.title resource.title
json.created_at resource.created_at.to_i
json.user resource.user.push_event_data
json.assistant resource.assistant.push_event_data
json.account_id resource.account_id
@@ -1,10 +1,7 @@
class CaptainListener < BaseListener
include ::Events::Types
def conversation_resolved(event)
conversation = extract_conversation_and_account(event)[0]
assistant = conversation.inbox.captain_assistant
return unless conversation.inbox.captain_active?
Captain::Llm::ContactNotesService.new(assistant, conversation).generate_and_update_notes if assistant.config['feature_memory'].present?
+48 -13
View File
@@ -6,7 +6,6 @@ class CustomMarkdownRenderer < CommonMarker::HtmlRenderer
VIMEO_REGEX = %r{https?://(?:www\.)?vimeo\.com/(\d+)}
MP4_REGEX = %r{https?://(?:www\.)?.+\.(mp4)}
ARCADE_REGEX = %r{https?://(?:www\.)?app\.arcade\.software/share/([^&/]+)}
WISTIA_REGEX = %r{https?://(?:www\.)?([^/]+)\.wistia\.com/medias/([^&/]+)}
def text(node)
content = node.string_content
@@ -51,8 +50,7 @@ class CustomMarkdownRenderer < CommonMarker::HtmlRenderer
VIMEO_REGEX => :make_vimeo_embed,
MP4_REGEX => :make_video_embed,
LOOM_REGEX => :make_loom_embed,
ARCADE_REGEX => :make_arcade_embed,
WISTIA_REGEX => :make_wistia_embed
ARCADE_REGEX => :make_arcade_embed
}
embedding_methods.each do |regex, method|
@@ -78,30 +76,67 @@ class CustomMarkdownRenderer < CommonMarker::HtmlRenderer
def make_youtube_embed(youtube_match)
video_id = youtube_match[1]
EmbedRenderer.youtube(video_id)
%(
<div style="position: relative; padding-bottom: 62.5%; height: 0;">
<iframe
src="https://www.youtube-nocookie.com/embed/#{video_id}"
frameborder="0"
style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen></iframe>
</div>
)
end
def make_loom_embed(loom_match)
video_id = loom_match[1]
EmbedRenderer.loom(video_id)
%(
<div style="position: relative; padding-bottom: 62.5%; height: 0;">
<iframe
src="https://www.loom.com/embed/#{video_id}"
frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen
style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;"></iframe>
</div>
)
end
def make_vimeo_embed(vimeo_match)
video_id = vimeo_match[1]
EmbedRenderer.vimeo(video_id)
%(
<div style="position: relative; padding-bottom: 62.5%; height: 0;">
<iframe
src="https://player.vimeo.com/video/#{video_id}?dnt=true"
frameborder="0"
allow="autoplay; fullscreen; picture-in-picture"
allowfullscreen
style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;"></iframe>
</div>
)
end
def make_video_embed(link_url)
EmbedRenderer.video(link_url)
end
def make_wistia_embed(wistia_match)
video_id = wistia_match[2]
EmbedRenderer.wistia(video_id)
%(
<video width="640" height="360" controls>
<source src="#{link_url}" type="video/mp4">
Your browser does not support the video tag.
</video>
)
end
def make_arcade_embed(arcade_match)
video_id = arcade_match[1]
EmbedRenderer.arcade(video_id)
%(
<div style="position: relative; padding-bottom: 62.5%; height: 0;">
<iframe
src="https://app.arcade.software/embed/#{video_id}"
frameborder="0"
webkitallowfullscreen
mozallowfullscreen
allowfullscreen
allow="fullscreen"
style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;">
</iframe>
</div>
)
end
end
-87
View File
@@ -1,87 +0,0 @@
module EmbedRenderer
def self.youtube(video_id)
%(
<div style="position: relative; padding-bottom: 62.5%; height: 0;">
<iframe
src="https://www.youtube-nocookie.com/embed/#{video_id}"
frameborder="0"
style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen></iframe>
</div>
)
end
def self.loom(video_id)
%(
<div style="position: relative; padding-bottom: 62.5%; height: 0;">
<iframe
src="https://www.loom.com/embed/#{video_id}"
frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen
style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;"></iframe>
</div>
)
end
def self.vimeo(video_id)
%(
<div style="position: relative; padding-bottom: 62.5%; height: 0;">
<iframe
src="https://player.vimeo.com/video/#{video_id}?dnt=true"
frameborder="0"
allow="autoplay; fullscreen; picture-in-picture"
allowfullscreen
style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;"></iframe>
</div>
)
end
def self.video(link_url)
%(
<video width="640" height="360" controls>
<source src="#{link_url}" type="video/mp4">
Your browser does not support the video tag.
</video>
)
end
# Generates an HTML embed for a Wistia video.
# @param wistia_match [MatchData] A match object from the WISTIA_REGEX regex, where wistia_match[2] contains the video ID.
def self.wistia(video_id)
%(
<div style="position: relative; padding-bottom: 56.25%; height: 0;">
<script src="https://fast.wistia.com/player.js" async></script>
<script src="https://fast.wistia.com/embed/#{video_id}.js" async type="module"></script>
<style>
wistia-player[media-id='#{video_id}']:not(:defined) {
background: center / contain no-repeat url('https://fast.wistia.com/embed/medias/#{video_id}/swatch');
display: block;
filter: blur(5px);
padding-top:56.25%;
}
</style>
<wistia-player
media-id="#{video_id}"
aspect="1.7777777777777777"
style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;">
</wistia-player>
</div>
)
end
def self.arcade(video_id)
%(
<div style="position: relative; padding-bottom: 62.5%; height: 0;">
<iframe
src="https://app.arcade.software/embed/#{video_id}"
frameborder="0"
webkitallowfullscreen
mozallowfullscreen
allowfullscreen
allow="fullscreen"
style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;">
</iframe>
</div>
)
end
end
-3
View File
@@ -54,7 +54,4 @@ module Events::Types
# agent events
AGENT_ADDED = 'agent.added'
AGENT_REMOVED = 'agent.removed'
# copilot events
COPILOT_MESSAGE_CREATED = 'copilot.message.created'
end
+10 -144
View File
@@ -1,156 +1,22 @@
require 'json_refs'
require 'fileutils'
require 'pathname'
require 'yaml'
require 'json'
namespace :swagger do
desc 'build combined swagger.json file from all the fragmented definitions and paths inside swagger folder'
task build: :environment do
require 'json_refs'
module SwaggerTaskActions
def self.execute_build
swagger_dir = Rails.root.join('swagger')
# Paths relative to swagger_dir for use within Dir.chdir
index_yml_relative_path = 'index.yml'
swagger_json_relative_path = 'swagger.json'
Dir.chdir(swagger_dir) do
# Operations within this block are relative to swagger_dir
swagger_index_content = File.read(index_yml_relative_path)
swagger_index = YAML.safe_load(swagger_index_content)
base_path = Rails.root.join('swagger')
Dir.chdir(base_path) do
swagger_index = YAML.safe_load(File.open('index.yml'))
final_build = JsonRefs.call(
swagger_index,
resolve_local_ref: false,
resolve_file_ref: true, # Uses CWD (swagger_dir) for resolving file refs
resolve_file_ref: true,
logging: true
)
File.write(swagger_json_relative_path, JSON.pretty_generate(final_build))
# For user messages, provide the absolute path
absolute_swagger_json_path = swagger_dir.join(swagger_json_relative_path)
File.write('swagger.json', JSON.pretty_generate(final_build))
puts 'Swagger build was successful.'
puts "Generated #{absolute_swagger_json_path}"
puts "Generated #{base_path}/swagger.json"
puts 'Go to http://localhost:3000/swagger see the changes.'
# Trigger dependent task
Rake::Task['swagger:build_tag_groups'].invoke
end
end
def self.execute_build_tag_groups
base_swagger_path = Rails.root.join('swagger')
tag_groups_output_dir = base_swagger_path.join('tag_groups')
full_spec_path = base_swagger_path.join('swagger.json')
index_yml_path = base_swagger_path.join('index.yml')
full_spec = JSON.parse(File.read(full_spec_path))
swagger_index = YAML.safe_load(File.read(index_yml_path))
tag_groups = swagger_index['x-tagGroups']
FileUtils.mkdir_p(tag_groups_output_dir)
tag_groups.each do |tag_group|
_process_tag_group(tag_group, full_spec, tag_groups_output_dir)
end
puts 'Tag-specific swagger files generated successfully.'
end
def self.execute_build_for_docs
Rake::Task['swagger:build'].invoke # Ensure all swagger files are built first
developer_docs_public_path = Rails.root.join('developer-docs/public')
tag_groups_in_dev_docs_path = developer_docs_public_path.join('swagger/tag_groups')
source_tag_groups_path = Rails.root.join('swagger/tag_groups')
FileUtils.mkdir_p(tag_groups_in_dev_docs_path)
puts 'Creating symlinks for developer-docs...'
symlink_files = %w[platform_swagger.json application_swagger.json client_swagger.json other_swagger.json]
symlink_files.each do |file|
_create_symlink(source_tag_groups_path.join(file), tag_groups_in_dev_docs_path.join(file))
end
puts 'Symlinks created successfully.'
puts 'You can now run the Mintlify dev server to preview the documentation.'
end
# Private helper methods
class << self
private
def _process_tag_group(tag_group, full_spec, output_dir)
group_name = tag_group['name']
tags_in_current_group = tag_group['tags']
tag_spec = JSON.parse(JSON.generate(full_spec)) # Deep clone
tag_spec['paths'] = _filter_paths_for_tag_group(tag_spec['paths'], tags_in_current_group)
tag_spec['tags'] = _filter_tags_for_tag_group(tag_spec['tags'], tags_in_current_group)
output_filename = _determine_output_filename(group_name)
File.write(output_dir.join(output_filename), JSON.pretty_generate(tag_spec))
end
def _operation_has_matching_tags?(operation, tags_in_group)
return false unless operation.is_a?(Hash)
operation_tags = operation['tags']
return false unless operation_tags.is_a?(Array)
operation_tags.intersect?(tags_in_group)
end
def _filter_paths_for_tag_group(paths_spec, tags_in_group)
(paths_spec || {}).filter_map do |path, path_item|
next unless path_item.is_a?(Hash)
operations_with_group_tags = path_item.any? do |_method, operation|
_operation_has_matching_tags?(operation, tags_in_group)
end
[path, path_item] if operations_with_group_tags
end.to_h
end
def _filter_tags_for_tag_group(tags_spec, tags_in_group)
if tags_spec.is_a?(Array)
tags_spec.select { |tag_definition| tags_in_group.include?(tag_definition['name']) }
else
[]
end
end
def _determine_output_filename(group_name)
return 'other_swagger.json' if group_name.casecmp('others').zero?
sanitized_group_name = group_name.downcase.tr(' ', '_').gsub(/[^a-z0-9_]+/, '')
"#{sanitized_group_name}_swagger.json"
end
def _create_symlink(source_file_path, target_file_path)
FileUtils.rm_f(target_file_path) # Remove existing to avoid errors
if File.exist?(source_file_path)
relative_source_path = Pathname.new(source_file_path).relative_path_from(target_file_path.dirname)
FileUtils.ln_sf(relative_source_path, target_file_path)
else
puts "Warning: Source file #{source_file_path} not found. Skipping symlink for #{File.basename(target_file_path)}."
end
end
end
end
namespace :swagger do
desc 'build combined swagger.json file from all the fragmented definitions and paths inside swagger folder'
task build: :environment do
SwaggerTaskActions.execute_build
end
desc 'build separate swagger files for each tag group'
task build_tag_groups: :environment do
SwaggerTaskActions.execute_build_tag_groups
end
desc 'build swagger files and create symlinks in developer-docs'
task build_for_docs: :environment do
SwaggerTaskActions.execute_build_for_docs
end
end
+1 -1
View File
@@ -39,7 +39,6 @@
"@formkit/vue": "^1.6.7",
"@hcaptcha/vue3-hcaptcha": "^1.3.0",
"@highlightjs/vue-plugin": "^2.1.0",
"@hotwired/turbo-rails": "^8.0.13",
"@iconify-json/material-symbols": "^1.2.10",
"@june-so/analytics-next": "^2.0.0",
"@lk77/vue3-color": "^3.0.6",
@@ -85,6 +84,7 @@
"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.18.1",
"videojs-record": "4.5.0",
+8 -22
View File
@@ -37,9 +37,6 @@ 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/material-symbols':
specifier: ^1.2.10
version: 1.2.10
@@ -175,6 +172,9 @@ 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
@@ -865,13 +865,6 @@ packages:
'@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'}
@@ -1043,9 +1036,6 @@ 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==}
@@ -4720,6 +4710,9 @@ 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'}
@@ -5730,13 +5723,6 @@ snapshots:
'@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
@@ -5956,8 +5942,6 @@ snapshots:
'@rails/actioncable@6.1.3': {}
'@rails/actioncable@7.2.201': {}
'@rails/ujs@7.1.400': {}
'@rollup/rollup-android-arm-eabi@4.40.2':
@@ -10216,6 +10200,8 @@ snapshots:
tslib@2.8.1: {}
turbolinks@5.2.0: {}
type-check@0.4.0:
dependencies:
prelude-ls: 1.2.1
@@ -79,18 +79,6 @@ describe Messages::Instagram::MessageBuilder do
expect(instagram_inbox.messages.count).to be 1
end
it 'discards duplicate messages from webhook events with the same message_id' do
messaging = dm_params[:entry][0]['messaging'][0]
described_class.new(messaging, instagram_inbox).perform
initial_message_count = instagram_inbox.messages.count
expect(initial_message_count).to be 1
described_class.new(messaging, instagram_inbox).perform
expect(instagram_inbox.messages.count).to eq initial_message_count
end
it 'creates message for shared reel' do
messaging = shared_reel_params[:entry][0]['messaging'][0]
described_class.new(messaging, instagram_inbox).perform
@@ -163,15 +151,11 @@ describe Messages::Instagram::MessageBuilder do
end
it 'does not create message for unsupported file type' do
conversation
# try to create a message with unsupported file type
story_mention_params[:entry][0][:messaging][0]['message']['attachments'][0]['type'] = 'unsupported_type'
messaging = story_mention_params[:entry][0][:messaging][0]
described_class.new(messaging, instagram_inbox, outgoing_echo: false).perform
# Conversation should exist but no new message should be created
expect(instagram_inbox.conversations.count).to be 1
expect(instagram_inbox.messages.count).to be 0
end
@@ -189,22 +189,19 @@ describe Messages::Instagram::Messenger::MessageBuilder do
profile_pic: 'https://chatwoot-assets.local/sample.png'
}.with_indifferent_access
)
conversation
# create a message with unsupported file type
story_mention_params[:entry][0][:messaging][0]['message']['attachments'][0]['type'] = 'unsupported_type'
messaging = story_mention_params[:entry][0][:messaging][0]
contact_inbox
described_class.new(messaging, instagram_messenger_inbox, outgoing_echo: false).perform
instagram_messenger_inbox.reload
# Conversation should exist but no new message should be created
# we would have contact created but message and attachments won't be created
expect(instagram_messenger_inbox.conversations.count).to be 1
expect(instagram_messenger_inbox.messages.count).to be 0
contact = instagram_messenger_channel.inbox.contacts.first
expect(contact.name).to eq('Jane Dae')
end
end
@@ -76,54 +76,6 @@ RSpec.describe 'Platform Users API', type: :request do
end
end
describe 'POST /platform/api/v1/users/{user_id}/token' do
context 'when it is an unauthenticated platform app' do
it 'returns unauthorized' do
post "/platform/api/v1/users/#{user.id}/token"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an invalid platform app token' do
it 'returns unauthorized' do
post "/platform/api/v1/users/#{user.id}/token", headers: { api_access_token: 'invalid' }, as: :json
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated platform app' do
let(:platform_app) { create(:platform_app) }
it 'returns unauthorized when its not a permissible object' do
post "/platform/api/v1/users/#{user.id}/token", headers: { api_access_token: platform_app.access_token.token }, as: :json
expect(response).to have_http_status(:unauthorized)
end
it 'returns access token for the user with expiry and user info' do
create(:platform_app_permissible, platform_app: platform_app, permissible: user)
post "/platform/api/v1/users/#{user.id}/token",
headers: { api_access_token: platform_app.access_token.token }, as: :json
expect(response).to have_http_status(:success)
data = response.parsed_body
# Check access token and expiry
expect(data['access_token']).to eq(user.access_token.token)
expect(data['expiry']).to be_nil
# Check user info
expect(data['user']).to include(
'id' => user.id,
'name' => user.name,
'display_name' => user.display_name,
'email' => user.email,
'pubsub_token' => user.pubsub_token
)
end
end
end
describe 'POST /platform/api/v1/users/' do
context 'when it is an unauthenticated platform app' do
it 'returns unauthorized' do
@@ -4,12 +4,12 @@ RSpec.describe 'Api::V1::Accounts::Captain::CopilotMessagesController', type: :r
let(:account) { create(:account) }
let(:user) { create(:user, account: account, role: :administrator) }
let(:copilot_thread) { create(:captain_copilot_thread, account: account, user: user) }
let!(:copilot_message) { create(:captain_copilot_message, copilot_thread: copilot_thread, account: account) }
let!(:copilot_message) { create(:captain_copilot_message, copilot_thread: copilot_thread, user: user, account: account) }
describe 'GET /api/v1/accounts/{account.id}/captain/copilot_threads/{thread.id}/copilot_messages' do
describe 'GET /api/v1/accounts/{account.id}/captain/copilot_threads/{thread.uuid}/copilot_messages' do
context 'when it is an authenticated user' do
it 'returns all messages' do
get "/api/v1/accounts/#{account.id}/captain/copilot_threads/#{copilot_thread.id}/copilot_messages",
get "/api/v1/accounts/#{account.id}/captain/copilot_threads/#{copilot_thread.uuid}/copilot_messages",
headers: user.create_new_auth_token,
as: :json
@@ -20,9 +20,9 @@ RSpec.describe 'Api::V1::Accounts::Captain::CopilotMessagesController', type: :r
end
end
context 'when thread id is invalid' do
context 'when thread uuid is invalid' do
it 'returns not found error' do
get "/api/v1/accounts/#{account.id}/captain/copilot_threads/999999999/copilot_messages",
get "/api/v1/accounts/#{account.id}/captain/copilot_threads/invalid-uuid/copilot_messages",
headers: user.create_new_auth_token,
as: :json
@@ -30,49 +30,4 @@ RSpec.describe 'Api::V1::Accounts::Captain::CopilotMessagesController', type: :r
end
end
end
describe 'POST /api/v1/accounts/{account.id}/captain/copilot_threads/{thread.id}/copilot_messages' do
context 'when it is an authenticated user' do
it 'creates a new message' do
message_content = { 'content' => 'This is a test message' }
expect do
post "/api/v1/accounts/#{account.id}/captain/copilot_threads/#{copilot_thread.id}/copilot_messages",
params: { message: message_content },
headers: user.create_new_auth_token,
as: :json
end.to change(CopilotMessage, :count).by(1)
expect(response).to have_http_status(:success)
expect(CopilotMessage.last.message).to eq(message_content)
expect(CopilotMessage.last.message_type).to eq('user')
expect(CopilotMessage.last.copilot_thread_id).to eq(copilot_thread.id)
end
end
context 'when thread does not exist' do
it 'returns not found error' do
post "/api/v1/accounts/#{account.id}/captain/copilot_threads/999999999/copilot_messages",
params: { message: { text: 'Test message' } },
headers: user.create_new_auth_token,
as: :json
expect(response).to have_http_status(:not_found)
end
end
context 'when thread belongs to another user' do
let(:another_user) { create(:user, account: account) }
let(:another_thread) { create(:captain_copilot_thread, account: account, user: another_user) }
it 'returns not found error' do
post "/api/v1/accounts/#{account.id}/captain/copilot_threads/#{another_thread.id}/copilot_messages",
params: { message: { text: 'Test message' } },
headers: user.create_new_auth_token,
as: :json
expect(response).to have_http_status(:not_found)
end
end
end
end
@@ -18,7 +18,7 @@ RSpec.describe 'Api::V1::Accounts::Captain::CopilotThreads', type: :request do
end
end
context 'when it is an authenticated user' do
context 'when it is an agent' do
it 'fetches copilot threads for the current user' do
# Create threads for the current agent
create_list(:captain_copilot_thread, 3, account: account, user: agent)
@@ -47,65 +47,4 @@ RSpec.describe 'Api::V1::Accounts::Captain::CopilotThreads', type: :request do
end
end
end
describe 'POST /api/v1/accounts/{account.id}/captain/copilot_threads' do
let(:assistant) { create(:captain_assistant, account: account) }
let(:valid_params) { { message: 'Hello, how can you help me?', assistant_id: assistant.id } }
context 'when it is an un-authenticated user' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/captain/copilot_threads",
params: valid_params,
as: :json
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
context 'with invalid params' do
it 'returns error when message is blank' do
post "/api/v1/accounts/#{account.id}/captain/copilot_threads",
params: { message: '', assistant_id: assistant.id },
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unprocessable_entity)
expect(json_response[:error]).to eq('Message is required')
end
it 'returns error when assistant_id is invalid' do
post "/api/v1/accounts/#{account.id}/captain/copilot_threads",
params: { message: 'Hello', assistant_id: 0 },
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:not_found)
end
end
context 'with valid params' do
it 'creates a new copilot thread with initial message' do
expect do
post "/api/v1/accounts/#{account.id}/captain/copilot_threads",
params: valid_params,
headers: agent.create_new_auth_token,
as: :json
end.to change(CopilotThread, :count).by(1)
.and change(CopilotMessage, :count).by(1)
expect(response).to have_http_status(:success)
thread = CopilotThread.last
expect(thread.title).to eq(valid_params[:message])
expect(thread.user_id).to eq(agent.id)
expect(thread.assistant_id).to eq(assistant.id)
message = thread.copilot_messages.last
expect(message.message_type).to eq('user')
expect(message.message).to eq(valid_params[:message])
end
end
end
end
end
@@ -1,24 +0,0 @@
require 'rails_helper'
describe ActionCableListener do
describe '#copilot_message_created' do
let(:event_name) { :copilot_message_created }
let(:account) { create(:account) }
let(:user) { create(:user, account: account) }
let(:assistant) { create(:captain_assistant, account: account) }
let(:copilot_thread) { create(:captain_copilot_thread, account: account, user: user, assistant: assistant) }
let(:copilot_message) { create(:captain_copilot_message, copilot_thread: copilot_thread) }
let(:event) { Events::Base.new(event_name, Time.zone.now, copilot_message: copilot_message) }
let(:listener) { described_class.instance }
it 'broadcasts message to the user' do
expect(ActionCableBroadcastJob).to receive(:perform_later).with(
[user.pubsub_token],
'copilot.message.created',
copilot_message.push_event_data.merge(account_id: account.id)
)
listener.copilot_message_created(event)
end
end
end
@@ -1,57 +0,0 @@
require 'rails_helper'
describe CaptainListener do
let(:listener) { described_class.instance }
let(:account) { create(:account) }
let(:inbox) { create(:inbox, account: account) }
let(:user) { create(:user, account: account) }
let(:assistant) { create(:captain_assistant, account: account, config: { feature_memory: true, feature_faq: true }) }
describe '#conversation_resolved' do
let(:agent) { create(:user, account: account) }
let(:conversation) { create(:conversation, account: account, inbox: inbox, assignee: agent) }
let(:event_name) { :conversation_resolved }
let(:event) { Events::Base.new(event_name, Time.zone.now, conversation: conversation) }
before do
create(:captain_inbox, captain_assistant: assistant, inbox: inbox)
end
context 'when feature_memory is enabled' do
before do
assistant.config['feature_memory'] = true
assistant.config['feature_faq'] = false
assistant.save!
end
it 'generates and updates notes' do
expect(Captain::Llm::ContactNotesService)
.to receive(:new)
.with(assistant, conversation)
.and_return(instance_double(Captain::Llm::ContactNotesService, generate_and_update_notes: nil))
expect(Captain::Llm::ConversationFaqService).not_to receive(:new)
listener.conversation_resolved(event)
end
end
context 'when feature_faq is enabled' do
before do
assistant.config['feature_faq'] = true
assistant.config['feature_memory'] = false
assistant.save!
end
it 'generates and deduplicates FAQs' do
expect(Captain::Llm::ConversationFaqService)
.to receive(:new)
.with(assistant, conversation)
.and_return(instance_double(Captain::Llm::ConversationFaqService, generate_and_deduplicate: false))
expect(Captain::Llm::ContactNotesService).not_to receive(:new)
listener.conversation_resolved(event)
end
end
end
end
@@ -1,64 +0,0 @@
require 'rails_helper'
RSpec.describe CopilotMessage, type: :model do
describe 'associations' do
it { is_expected.to belong_to(:copilot_thread) }
it { is_expected.to belong_to(:account) }
end
describe 'validations' do
it { is_expected.to validate_presence_of(:message_type) }
it { is_expected.to validate_presence_of(:message) }
it { is_expected.to validate_inclusion_of(:message_type).in_array(described_class.message_types.keys) }
end
describe 'callbacks' do
let(:account) { create(:account) }
let(:user) { create(:user, account: account) }
let(:assistant) { create(:captain_assistant, account: account) }
let(:copilot_thread) { create(:captain_copilot_thread, account: account, user: user, assistant: assistant) }
describe '#ensure_account' do
it 'sets the account from the copilot thread before validation' do
message = build(:captain_copilot_message, copilot_thread: copilot_thread, account: nil)
message.valid?
expect(message.account).to eq(copilot_thread.account)
end
end
describe '#broadcast_message' do
it 'dispatches COPILOT_MESSAGE_CREATED event after create' do
message = build(:captain_copilot_message, copilot_thread: copilot_thread)
expect(Rails.configuration.dispatcher).to receive(:dispatch)
.with(COPILOT_MESSAGE_CREATED, anything, copilot_message: message)
message.save!
end
end
end
describe '#push_event_data' do
let(:account) { create(:account) }
let(:user) { create(:user, account: account) }
let(:assistant) { create(:captain_assistant, account: account) }
let(:copilot_thread) { create(:captain_copilot_thread, account: account, user: user, assistant: assistant) }
let(:message_content) { { 'content' => 'Test message' } }
let(:copilot_message) do
create(:captain_copilot_message,
copilot_thread: copilot_thread,
message_type: 'user',
message: message_content)
end
it 'returns the correct event data' do
event_data = copilot_message.push_event_data
expect(event_data[:id]).to eq(copilot_message.id)
expect(event_data[:message]).to eq(message_content)
expect(event_data[:message_type]).to eq('user')
expect(event_data[:created_at]).to eq(copilot_message.created_at.to_i)
expect(event_data[:copilot_thread]).to eq(copilot_thread.push_event_data)
end
end
end
@@ -1,62 +0,0 @@
require 'rails_helper'
RSpec.describe CopilotThread, type: :model do
describe 'associations' do
it { is_expected.to belong_to(:user) }
it { is_expected.to belong_to(:account) }
it { is_expected.to belong_to(:assistant).class_name('Captain::Assistant') }
it { is_expected.to have_many(:copilot_messages).dependent(:destroy_async) }
end
describe 'validations' do
it { is_expected.to validate_presence_of(:title) }
end
describe '#push_event_data' do
let(:account) { create(:account) }
let(:user) { create(:user, account: account) }
let(:assistant) { create(:captain_assistant, account: account) }
let(:copilot_thread) { create(:captain_copilot_thread, account: account, user: user, assistant: assistant, title: 'Test Thread') }
it 'returns the correct event data' do
event_data = copilot_thread.push_event_data
expect(event_data[:id]).to eq(copilot_thread.id)
expect(event_data[:title]).to eq('Test Thread')
expect(event_data[:created_at]).to eq(copilot_thread.created_at.to_i)
expect(event_data[:user]).to eq(user.push_event_data)
expect(event_data[:account_id]).to eq(account.id)
end
end
describe '#previous_history' do
let(:account) { create(:account) }
let(:user) { create(:user, account: account) }
let(:assistant) { create(:captain_assistant, account: account) }
let(:copilot_thread) { create(:captain_copilot_thread, account: account, user: user, assistant: assistant) }
context 'when there are messages in the thread' do
before do
create(:captain_copilot_message, copilot_thread: copilot_thread, message_type: 'user', message: { 'content' => 'User message' })
create(:captain_copilot_message, copilot_thread: copilot_thread, message_type: 'assistant_thinking', message: { 'content' => 'Thinking...' })
create(:captain_copilot_message, copilot_thread: copilot_thread, message_type: 'assistant', message: { 'content' => 'Assistant message' })
end
it 'returns only user and assistant messages in chronological order' do
history = copilot_thread.previous_history
expect(history.length).to eq(2)
expect(history[0][:role]).to eq('user')
expect(history[0][:content]).to eq({ 'content' => 'User message' })
expect(history[1][:role]).to eq('assistant')
expect(history[1][:content]).to eq({ 'content' => 'Assistant message' })
end
end
context 'when there are no messages in the thread' do
it 'returns an empty array' do
expect(copilot_thread.previous_history).to eq([])
end
end
end
end
+2 -1
View File
@@ -1,8 +1,9 @@
FactoryBot.define do
factory :captain_copilot_message, class: 'CopilotMessage' do
account
user
copilot_thread { association :captain_copilot_thread }
message { { content: 'This is a test message' } }
message_type { 0 }
message_type { 'user' }
end
end
+1 -1
View File
@@ -3,6 +3,6 @@ FactoryBot.define do
account
user
title { Faker::Lorem.sentence }
assistant { create(:captain_assistant, account: account) }
uuid { SecureRandom.uuid }
end
end
-11
View File
@@ -143,17 +143,6 @@ describe CustomMarkdownRenderer do
end
end
context 'when link is a wistia URL' do
let(:wistia_url) { 'https://chatwoot.wistia.com/medias/kjwjeq6f9i' }
it 'renders a custom element with Wistia embed code' do
output = render_markdown_link(wistia_url)
expect(output).to include('<script src="https://fast.wistia.com/player.js" async></script>')
expect(output).to include('<wistia-player')
expect(output).to include('media-id="kjwjeq6f9i"')
end
end
context 'when multiple links including Arcade are present' do
it 'renders Arcade embed along with other content types' do
markdown = "\n[arcade](https://app.arcade.software/share/ARCADE_ID)\n\n[youtube](https://www.youtube.com/watch?v=VIDEO_ID)\n"
@@ -203,24 +203,4 @@ describe ActionCableListener do
listener.conversation_updated(event)
end
end
describe '#copilot_message_created' do
let(:event_name) { :copilot_message_created }
let(:account) { create(:account) }
let(:user) { create(:user, account: account) }
let(:assistant) { create(:captain_assistant, account: account) }
let(:copilot_thread) { create(:captain_copilot_thread, account: account, user: user, assistant: assistant) }
let(:copilot_message) { create(:captain_copilot_message, copilot_thread: copilot_thread) }
let(:event) { Events::Base.new(event_name, Time.zone.now, copilot_message: copilot_message) }
it 'broadcasts message to the user' do
expect(ActionCableBroadcastJob).to receive(:perform_later).with(
[user.pubsub_token],
'copilot.message.created',
copilot_message.push_event_data
)
listener.copilot_message_created(event)
end
end
end
@@ -16,7 +16,6 @@ RSpec.describe Crm::Leadsquared::Mappers::ContactMapper do
name: 'John',
last_name: 'Doe',
email: 'john@example.com',
# the phone number is intentionally wrong
phone_number: '+1234567890'
)
@@ -30,19 +29,6 @@ RSpec.describe Crm::Leadsquared::Mappers::ContactMapper do
'Source' => 'Test Brand'
)
end
it 'represents the phone number correctly' do
contact.update!(
name: 'John',
last_name: 'Doe',
email: 'john@example.com',
phone_number: '+917507684392'
)
mapped_data = described_class.map(contact)
expect(mapped_data).to include('Mobile' => '+91-7507684392')
end
end
end
end
@@ -194,44 +194,23 @@ describe MessageTemplates::HookExecutionService do
expect(out_of_office_service).to have_received(:perform)
end
context 'with recent outgoing messages' do
it 'does not call ::MessageTemplates::Template::OutOfOffice when there are recent outgoing messages' do
contact = create(:contact)
conversation = create(:conversation, contact: contact)
it 'does not call ::MessageTemplates::Template::OutOfOffice when there are recent outgoing messages' do
contact = create(:contact)
conversation = create(:conversation, contact: contact)
conversation.inbox.update(working_hours_enabled: true, out_of_office_message: 'We are out of office')
conversation.inbox.working_hours.today.update!(closed_all_day: true)
conversation.inbox.update(working_hours_enabled: true, out_of_office_message: 'We are out of office')
conversation.inbox.working_hours.today.update!(closed_all_day: true)
create(:message, conversation: conversation, message_type: :outgoing, created_at: 2.minutes.ago)
create(:message, conversation: conversation, message_type: :outgoing, created_at: 2.minutes.ago)
out_of_office_service = double
allow(MessageTemplates::Template::OutOfOffice).to receive(:new).and_return(out_of_office_service)
allow(out_of_office_service).to receive(:perform).and_return(true)
out_of_office_service = double
allow(MessageTemplates::Template::OutOfOffice).to receive(:new).and_return(out_of_office_service)
allow(out_of_office_service).to receive(:perform).and_return(true)
create(:message, conversation: conversation)
create(:message, conversation: conversation)
expect(MessageTemplates::Template::OutOfOffice).not_to have_received(:new)
expect(out_of_office_service).not_to have_received(:perform)
end
it 'ignores private note and calls ::MessageTemplates::Template::OutOfOffice' do
contact = create(:contact)
conversation = create(:conversation, contact: contact)
conversation.inbox.update(working_hours_enabled: true, out_of_office_message: 'We are out of office')
conversation.inbox.working_hours.today.update!(closed_all_day: true)
create(:message, conversation: conversation, private: true, message_type: :outgoing, created_at: 2.minutes.ago)
out_of_office_service = double
allow(MessageTemplates::Template::OutOfOffice).to receive(:new).and_return(out_of_office_service)
allow(out_of_office_service).to receive(:perform).and_return(true)
create(:message, conversation: conversation)
expect(MessageTemplates::Template::OutOfOffice).to have_received(:new).with(conversation: conversation)
expect(out_of_office_service).to have_received(:perform)
end
expect(MessageTemplates::Template::OutOfOffice).not_to have_received(:new)
expect(out_of_office_service).not_to have_received(:perform)
end
it 'will not calls ::MessageTemplates::Template::OutOfOffice when outgoing message' do
+1 -1
View File
@@ -6,4 +6,4 @@ properties:
errors:
type: array
items:
$ref: '#/components/schemas/request_error'
$ref: '#/definitions/request_error'
+32 -84
View File
@@ -1,11 +1,13 @@
## ---------- ERRORS -------------- ##
## ---------- ERRORS ------------- ##
## -------------------------------- ##
bad_request_error:
$ref: ./error/bad_request.yml
request_error:
$ref: ./error/request.yml
## ---------- RESOURCE ------------ ##
## ---------- RESOURCE ------------- ##
## -------------------------------- ##
generic_id:
@@ -16,20 +18,12 @@ custom_attribute:
$ref: ./resource/custom_attribute.yml
automation_rule:
$ref: ./resource/automation_rule.yml
automation_rule_item:
$ref: ./resource/automation_rule_item.yml
portal:
$ref: ./resource/portal.yml
portal_single:
$ref: ./resource/portal_single.yml
portal_config:
$ref: ./resource/portal_config.yml
portal_logo:
$ref: ./resource/portal_logo.yml
portal_meta:
$ref: ./resource/portal_meta.yml
portal_item:
$ref: ./resource/portal_item.yml
category:
$ref: ./resource/category.yml
article:
$ref: ./resource/article.yml
category:
$ref: ./resource/category.yml
article:
@@ -46,8 +40,6 @@ agent:
$ref: ./resource/agent.yml
inbox:
$ref: ./resource/inbox.yml
inbox_contact:
$ref: ./resource/inbox_contact.yml
agent_bot:
$ref: ./resource/agent_bot.yml
contact_inboxes:
@@ -60,8 +52,6 @@ webhook:
$ref: ./resource/webhook.yml
account:
$ref: ./resource/account.yml
account_user:
$ref: ./resource/account_user.yml
platform_account:
$ref: ./resource/platform_account.yml
team:
@@ -81,18 +71,13 @@ public_message:
public_inbox:
$ref: ./resource/public/inbox.yml
## ---------- REQUEST ------------- ##
## ---------- REQUEST------------- ##
## -------------------------------- ##
account_create_update_payload:
$ref: ./request/account/create_update_payload.yml
account_user_create_update_payload:
$ref: ./request/account_user/create_update_payload.yml
platform_agent_bot_create_update_payload:
$ref: ./request/platform/agent_bot/create_update_payload.yml
agent_bot_create_update_payload:
$ref: ./request/agent_bot/create_update_payload.yml
@@ -104,36 +89,21 @@ canned_response_create_update_payload:
custom_attribute_create_update_payload:
$ref: ./request/custom_attribute/create_update_payload.yml
## contact
contact_create:
$ref: ./request/contact/create.yml
contact_update:
$ref: ./request/contact/update.yml
## Agent
agent_create_payload:
$ref: ./request/agent/create_payload.yml
agent_update_payload:
$ref: ./request/agent/update_payload.yml
## conversation
conversation_message_create:
$ref: ./request/conversation/create_message.yml
## Contact
contact_create_payload:
$ref: ./request/contact/create_payload.yml
contact_update_payload:
$ref: ./request/contact/update_payload.yml
## Conversation
conversation_create_payload:
$ref: ./request/conversation/create_payload.yml
conversation_message_create_payload:
$ref: ./request/conversation/create_message_payload.yml
# Inbox
inbox_create_payload:
$ref: ./request/inbox/create_payload.yml
inbox_update_payload:
$ref: ./request/inbox/update_payload.yml
# Team
# Team request Payload
team_create_update_payload:
$ref: ./request/team/create_update_payload.yml
# Custom Filter
# Custom Filter request Payload
custom_filter_create_update_payload:
$ref: ./request/custom_filter/create_update_payload.yml
@@ -158,6 +128,7 @@ category_create_update_payload:
article_create_update_payload:
$ref: ./request/portal/article_create_update_payload.yml
## public requests
public_contact_create_update_payload:
$ref: ./request/public/contact/create_update_payload.yml
@@ -170,36 +141,37 @@ public_message_update_payload:
public_conversation_create_payload:
$ref: ./request/public/conversation/create_payload.yml
## ---------- RESPONSE ------------ ##
## ---------- RESPONSE ------------- ##
## -------------------------------- ##
## Contact
## contact
extended_contact:
allOf:
- $ref: '#/components/schemas/contact'
- $ref: '#/definitions/contact'
- $ref: ./resource/extension/contact/show.yml
contact_base:
allOf:
- $ref: '#/components/schemas/generic_id'
- $ref: '#/components/schemas/contact'
- $ref: '#/definitions/generic_id'
- $ref: '#/definitions/contact'
contact_list:
type: array
description: 'array of contacts'
items:
allOf:
- $ref: '#/components/schemas/contact'
- $ref: '#/definitions/generic_id'
- $ref: '#/definitions/contact'
contact_conversations:
type: array
description: 'array of conversations'
items:
allOf:
- $ref: '#/components/schemas/conversation'
- $ref: '#/definitions/conversation'
- $ref: ./resource/extension/contact/conversation.yml
- $ref: ./resource/extension/conversation/with_display_id.yml
contact_labels:
$ref: ./resource/extension/contact/labels.yml
## Conversation
## conversation
conversation_list:
$ref: ./resource/extension/conversation/list.yml
conversation_show:
@@ -209,33 +181,9 @@ conversation_status_toggle:
conversation_labels:
$ref: ./resource/extension/conversation/labels.yml
## Report
## report
account_summary:
$ref: './resource/reports/summary.yml'
agent_conversation_metrics:
$ref: './resource/reports/conversation/agent.yml'
contact_detail:
$ref: ./resource/contact_detail.yml
message_detailed:
$ref: ./resource/message_detailed.yml
conversation_meta:
$ref: ./resource/conversation_meta.yml
conversation_messages:
$ref: ./resource/conversation_messages.yml
contact_meta:
$ref: ./resource/contact_meta.yml
contact_inbox:
$ref: ./resource/contact_inbox.yml
contact_list_item:
$ref: ./resource/contact_list_item.yml
contacts_list_response:
$ref: ./resource/contacts_list_response.yml
contact_show_response:
$ref: ./resource/contact_show_response.yml
contact_conversation_message:
$ref: ./resource/contact_conversation_message.yml
contact_conversations_response:
$ref: ./resource/contact_conversations_response.yml
contactable_inboxes_response:
$ref: ./resource/contactable_inboxes_response.yml
@@ -3,29 +3,3 @@ properties:
name:
type: string
description: Name of the account
example: 'My Account'
locale:
type: string
description: The locale of the account
example: 'en'
domain:
type: string
description: The domain of the account
example: 'example.com'
support_email:
type: string
description: The support email of the account
example: 'support@example.com'
status:
type: string
enum: ['active', 'suspended']
description: The status of the account
example: 'active'
limits:
type: object
description: The limits of the account
example: {}
custom_attributes:
type: object
description: The custom attributes of the account
example: {}
@@ -1,13 +0,0 @@
type: object
required:
- user_id
- role
properties:
user_id:
type: integer
description: The ID of the user
example: 1
role:
type: string
description: whether user is an administrator or agent
example: administrator
@@ -1,28 +0,0 @@
type: object
required:
- name
- email
- role
properties:
name:
type: string
description: Full Name of the agent
example: 'John Doe'
email:
type: string
description: Email of the Agent
example: 'john.doe@acme.inc'
role:
type: string
enum: ['agent', 'administrator']
description: Whether its administrator or agent
example: 'agent'
availability_status:
type: string
enum: ['available', 'busy', 'offline']
description: The availability setting of the agent.
example: 'available'
auto_offline:
type: boolean
description: Whether the availability status of agent is configured to go offline automatically when away.
example: true
@@ -1,18 +0,0 @@
type: object
required:
- role
properties:
role:
type: string
enum: ['agent', 'administrator']
description: Whether its administrator or agent
example: 'agent'
availability_status:
type: string
enum: ['available', 'busy', 'offline']
description: The availability status of the agent.
example: 'available'
auto_offline:
type: boolean
description: Whether the availability status of agent is configured to go offline automatically when away.
example: true
@@ -3,28 +3,9 @@ properties:
name:
type: string
description: The name of the agent bot
example: 'My Agent Bot'
description:
type: string
description: The description of the agent bot
example: 'This is a sample agent bot'
description: The description about the agent bot
outgoing_url:
type: string
description: The webhook URL for the bot
example: 'https://example.com/webhook'
avatar:
type: string
format: binary
description: Send the form data with the avatar image binary or use the avatar_url
avatar_url:
type: string
description: The url to a jpeg, png file for the agent bot avatar
example: https://example.com/avatar.png
bot_type:
type: integer
description: The type of the bot (0 for webhook)
example: 0
bot_config:
type: object
description: The configuration for the bot
example: {}
@@ -36,6 +36,6 @@ properties:
example:
attribute_key: content
filter_operator: contains
query_operator: OR
query_operator: nil
values:
- help
@@ -3,8 +3,6 @@ properties:
content:
type: string
description: Message content for canned response
example: 'Hello, {{contact.name}}! Welcome to our service.'
short_code:
type: string
description: Short Code for quick access of the canned response
example: 'welcome'
@@ -1,21 +1,18 @@
type: object
required:
- inbox_id
properties:
inbox_id:
type: number
name:
type: string
description: name of the contact
example: Alice
email:
type: string
description: email of the contact
example: alice@acme.inc
blocked:
type: boolean
description: whether the contact is blocked or not
example: false
phone_number:
type: string
description: phone number of the contact
example: '+123456789'
avatar:
type: string
format: binary
@@ -23,16 +20,9 @@ properties:
avatar_url:
type: string
description: The url to a jpeg, png file for the contact avatar
example: https://example.com/avatar.png
identifier:
type: string
description: A unique identifier for the contact in external system
example: '1234567890'
additional_attributes:
type: object
description: An object where you can store additional attributes for contact. example {"type":"customer", "age":30}
example: { 'type': 'customer', 'age': 30 }
custom_attributes:
type: object
description: An object where you can store custom attributes for contact. example {"type":"customer", "age":30}, this should have a valid custom attribute definition.
example: {}
description: An object where you can store custom attributes for contact. example {"type":"customer", "age":30}
@@ -1,44 +0,0 @@
type: object
required:
- inbox_id
properties:
inbox_id:
type: number
description: ID of the inbox to which the contact belongs
example: 1
name:
type: string
description: name of the contact
example: Alice
email:
type: string
description: email of the contact
example: alice@acme.inc
blocked:
type: boolean
description: whether the contact is blocked or not
example: false
phone_number:
type: string
description: phone number of the contact
example: '+123456789'
avatar:
type: string
format: binary
description: Send the form data with the avatar image binary or use the avatar_url
avatar_url:
type: string
description: The url to a jpeg, png file for the contact avatar
example: https://example.com/avatar.png
identifier:
type: string
description: A unique identifier for the contact in external system
example: '1234567890'
additional_attributes:
type: object
description: An object where you can store additional attributes for contact. example {"type":"customer", "age":30}
example: { 'type': 'customer', 'age': 30 }
custom_attributes:
type: object
description: An object where you can store custom attributes for contact. example {"type":"customer", "age":30}, this should have a valid custom attribute definition.
example: {}
@@ -0,0 +1,25 @@
type: object
properties:
name:
type: string
description: name of the contact
email:
type: string
description: email of the contact
phone_number:
type: string
description: phone number of the contact
avatar:
type: string
format: binary
description: Send the form data with the avatar image binary or use the avatar_url
avatar_url:
type: string
description: The url to a jpeg, png file for the contact avatar
identifier:
type: string
description: A unique identifier for the contact in external system
custom_attributes:
type: object
description: An object where you can store custom attributes for contact. example {"type":"customer", "age":30}
@@ -5,29 +5,20 @@ properties:
content:
type: string
description: The content of the message
example: 'Hello, how can I help you?'
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'
enum: ['text', 'input_email', 'cards', 'input_select', 'form' , 'article']
example: 'cards'
description: 'if you want to create custom message types'
content_attributes:
type: object
description: Attributes based on the content type
example: {}
campaign_id:
type: integer
description: The campaign id to which the message belongs
example: 1
description: attributes based on your content type
template_params:
type: object
description: The template params for the message in case of whatsapp Channel
@@ -44,8 +35,8 @@ properties:
type: string
description: Language of the template
example: en_US
processed_params:
processed_params:
type: object
description: The processed param values for template variables in template
example:
1: 'Chatwoot'
example:
1: "Chatwoot"
@@ -1,79 +0,0 @@
type: object
required:
- source_id
- inbox_id
properties:
source_id:
type: string
description: Conversation source id
example: '1234567890'
inbox_id:
type: integer
description: 'Id of inbox in which the conversation is created <br/> Allowed Inbox Types: Website, Phone, Api, Email'
example: 1
contact_id:
type: integer
description: Contact Id for which conversation is created
example: 1
additional_attributes:
type: object
description: Lets you specify attributes like browser information
example:
{
browser: 'Chrome',
browser_version: '89.0.4389.82',
os: 'Windows',
os_version: '10',
}
custom_attributes:
type: object
description: The object to save custom attributes for conversation, accepts custom attributes key and value
example: { attribute_key: attribute_value, priority_conversation_number: 3 }
status:
type: string
enum: ['open', 'resolved', 'pending']
description: Specify the conversation whether it's pending, open, closed
example: open
assignee_id:
type: integer
description: Agent Id for assigning a conversation to an agent
example: 1
team_id:
type: integer
description: Team Id for assigning a conversation to a team\
example: 1
snoozed_until:
type: string
format: date-time
description: Snoozed until date time
example: '2030-07-21T17:32:28Z'
message:
type: object
description: The initial message to be sent to the conversation
required: ['content']
properties:
content:
type: string
description: The content of the message
example: 'Hello, how can I help you?'
template_params:
type: object
description: The template params for the message in case of whatsapp Channel
properties:
name:
type: string
description: Name of the template
example: 'sample_issue_resolution'
category:
type: string
description: Category of the template
example: UTILITY
language:
type: string
description: Language of the template
example: en_US
processed_params:
type: object
description: The processed param values for template variables in template
example:
1: 'Chatwoot'
@@ -3,34 +3,20 @@ properties:
attribute_display_name:
type: string
description: Attribute display name
example: 'Custom Attribute'
attribute_display_type:
type: integer
description: Attribute display type (text- 0, number- 1, currency- 2, percent- 3, link- 4, date- 5, list- 6, checkbox- 7)
example: 0
attribute_description:
type: string
description: Attribute description
example: 'This is a custom attribute'
attribute_key:
type: string
description: Attribute unique key value
example: 'custom_attribute'
attribute_values:
type: array
description: Attribute values
items:
type: string
example: ['value1', 'value2']
attribute_model:
type: integer
description: Attribute type(conversation_attribute- 0, contact_attribute- 1)
example: 0
regex_pattern:
type: string
description: Regex pattern (Only applicable for type- text). The regex pattern is used to validate the attribute value(s).
example: '^[a-zA-Z0-9]+$'
regex_cue:
type: string
description: Regex cue message (Only applicable for type- text). The cue message is shown when the regex pattern is not matched.
example: 'Please enter a valid value'
@@ -3,13 +3,10 @@ properties:
name:
type: string
description: The name of the custom filter
example: 'My Custom Filter'
type:
type: string
enum: ['conversation', 'contact', 'report']
enum: ["conversation", "contact", "report"]
description: The description about the custom filter
example: 'conversation'
query:
type: object
description: A query that needs to be saved as a custom filter
example: {}
@@ -1,88 +0,0 @@
type: object
properties:
name:
type: string
description: The name of the inbox
example: 'Support'
avatar:
type: string
format: binary
description: Image file for avatar
greeting_enabled:
type: boolean
description: Enable greeting message
example: true
greeting_message:
type: string
description: Greeting message to be displayed on the widget
example: Hello, how can I help you?
enable_email_collect:
type: boolean
description: Enable email collection
example: true
csat_survey_enabled:
type: boolean
description: Enable CSAT survey
example: true
enable_auto_assignment:
type: boolean
description: Enable Auto Assignment
example: true
working_hours_enabled:
type: boolean
description: Enable working hours
example: true
out_of_office_message:
type: string
description: Out of office message to be displayed on the widget
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
example: 'America/New_York'
allow_messages_after_resolved:
type: boolean
description: Allow messages after conversation is resolved
example: true
lock_to_single_conversation:
type: boolean
description: Lock to single conversation
example: true
portal_id:
type: integer
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
enum: ['friendly', 'professional']
example: 'friendly'
business_name:
type: string
description: Business name for the inbox
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'
@@ -1,82 +0,0 @@
type: object
properties:
name:
type: string
description: The name of the inbox
example: 'Support'
avatar:
type: string
format: binary
description: Image file for avatar
greeting_enabled:
type: boolean
description: Enable greeting message
example: true
greeting_message:
type: string
description: Greeting message to be displayed on the widget
example: Hello, how can I help you?
enable_email_collect:
type: boolean
description: Enable email collection
example: true
csat_survey_enabled:
type: boolean
description: Enable CSAT survey
example: true
enable_auto_assignment:
type: boolean
description: Enable Auto Assignment
example: true
working_hours_enabled:
type: boolean
description: Enable working hours
example: true
out_of_office_message:
type: string
description: Out of office message to be displayed on the widget
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
example: 'America/New_York'
allow_messages_after_resolved:
type: boolean
description: Allow messages after conversation is resolved
example: true
lock_to_single_conversation:
type: boolean
description: Lock to single conversation
example: true
portal_id:
type: integer
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
enum: ['friendly', 'professional']
example: 'friendly'
business_name:
type: string
description: Business name for the inbox
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'
@@ -1,18 +1,11 @@
type: object
properties:
app_id:
type: integer
type: string
description: The ID of app for which integration hook is being created
example: 1
inbox_id:
type: integer
type: string
description: The inbox ID, if the hook is an inbox hook
example: 1
status:
type: integer
description: The status of the integration (0 for inactive, 1 for active)
example: 1
settings:
type: object
description: The settings required by the integration
example: {}
@@ -1,10 +1,5 @@
type: object
properties:
status:
type: integer
description: The status of the integration (0 for inactive, 1 for active)
example: 1
settings:
type: object
description: The settings required by the integration
example: {}
@@ -1,26 +0,0 @@
type: object
properties:
name:
type: string
description: The name of the agent bot
example: 'My Agent Bot'
description:
type: string
description: The description of the agent bot
example: 'This is a sample agent bot'
outgoing_url:
type: string
description: The webhook URL for the bot
example: 'https://example.com/webhook'
account_id:
type: integer
description: The account ID to associate the agent bot with
example: 1
avatar:
type: string
format: binary
description: Send the form data with the avatar image binary or use the avatar_url
avatar_url:
type: string
description: The url to a jpeg, png file for the agent bot avatar
example: https://example.com/avatar.png
@@ -1,51 +1,34 @@
type: object
properties:
title:
type: string
description: The title of the article
example: 'Article Title'
slug:
type: string
description: The slug of the article
example: 'article-title'
position:
type: integer
description: article position in category
example: 1
content:
type: string
description: The text content.
example: 'This is the content of the article'
description:
type: string
description: The description of the article
example: 'This is the description of the article'
category_id:
type: integer
description: The category id of the article
example: 1
author_id:
type: integer
description: The author agent id of the article
example: 1
associated_article_id:
type: integer
description: To associate similar articles to each other, e.g to provide the link for the reference.
example: 2
status:
type: integer
description: The status of the article. 0 for draft, 1 for published, 2 for archived
example: 1
locale:
type: string
description: The locale of the article
example: 'en'
meta:
type: object
description: Use for search
example:
{
tags: ['article_name'],
title: 'article title',
description: 'article description',
}
example: { tags: ['article_name'], title: 'article title', description: 'article description' }
position:
type: integer
description: article position in category
status:
type: integer
example: ['draft', 'published', 'archived']
title:
type: string
slug:
type: string
views:
type: integer
portal_id:
type: integer
account_id:
type: integer
author_id:
type: integer
category_id:
type: integer
folder_id:
type: integer
associated_article_id:
type: integer
description: To associate similar articles to each other, e.g to provide the link for the reference.
@@ -1,34 +1,28 @@
type: object
properties:
name:
type: string
description: The name of the category
example: 'Category Name'
description:
type: string
description: A description for the category
example: 'Category description'
description: Category description
locale:
type: string
description: Category locale
example: en/es
name:
type: string
description: Category name
slug:
type: string
description: Category slug
position:
type: integer
description: Category position in the portal list to sort
example: 1
slug:
type: string
description: The category slug used in the URL
example: 'category-name'
locale:
type: string
description: The locale of the category
example: en
icon:
type: string
description: The icon of the category as a string (emoji)
example: '📚'
parent_category_id:
portal_id:
type: integer
account_id:
type: integer
description: To define parent category, e.g product documentation has multiple level features in sales category or in engineering category.
example: 1
associated_category_id:
type: integer
description: To associate similar categories to each other, e.g same category of product documentation in different languages
example: 2
parent_category_id:
type: integer
description: To define parent category, e.g product documentation has multiple level features in sales category or in engineering category.
@@ -1,13 +1,20 @@
type: object
properties:
archived:
type: boolean
description: Status to check if portal is live
color:
type: string
description: Header color for help-center in hex format
example: '#FFFFFF'
description: Header color for help-center
example: add color HEX string, "#fffff"
config:
type: object
description: Configuration about supporting locales
example: { allowed_locales: ['en', 'es'], default_locale: 'en' }
custom_domain:
type: string
description: Custom domain to display help center.
example: chatwoot.help
description: Custom domain to display help center.
example: https://chatwoot.help/.
header_text:
type: string
description: Help center header
@@ -19,20 +26,11 @@ properties:
name:
type: string
description: Name for the portal
example: Handbook
page_title:
type: string
description: Page title for the portal
example: Handbook
slug:
type: string
description: Slug for the portal to display in link
example: handbook
archived:
type: boolean
description: Status to check if portal is live
example: false
config:
type: object
description: Configuration about supporting locales
example: { allowed_locales: ['en', 'es'], default_locale: 'en' }
page_title:
type: string
description: Page title for the portal
account_id:
type: integer
@@ -1,30 +1,24 @@
type: object
properties:
identifier:
type: string
description: External identifier of the contact
example: '1234567890'
identifier_hash:
type: string
description: Identifier hash prepared for HMAC authentication
example: 'e93275d4eba0e5679ad55f5360af00444e2a888df9b0afa3e8b691c3173725f9'
email:
type: string
description: Email of the contact
example: alice@acme.inc
name:
type: string
description: Name of the contact
example: Alice
phone_number:
type: string
description: Phone number of the contact
example: '+123456789'
avatar:
avatar_url:
type: string
format: binary
description: Send the form data with the avatar image binary or use the avatar_url
description: The url to a jpeg, png file for the user avatar
custom_attributes:
type: object
description: Custom attributes of the customer
example: {}
description: Custom attributes of the customer
@@ -3,4 +3,3 @@ properties:
custom_attributes:
type: object
description: Custom attributes of the conversation
example: {}
@@ -1,10 +1,10 @@
type: object
properties:
content:
type: string
description: Content for the message
example: 'Hello, how can I help you?'
echo_id:
type: string
description: Temporary identifier which will be passed back via websockets
example: '1234567890'
@@ -1,30 +1,6 @@
type: object
properties:
submitted_values:
type: object
description: Replies to the Bot Message Types
properties:
name:
type: string
description: The name of the submiitted value
example: 'My Name'
title:
type: string
description: The title of the submitted value
example: 'My Title'
value:
type: string
description: The value of the submitted value
example: 'value'
csat_survey_response:
type: object
description: The CSAT survey response
properties:
feedback_message:
type: string
description: The feedback message of the CSAT survey response
example: 'Great service!'
rating:
type: integer
description: The rating of the CSAT survey response
example: 5
description: Replies to the Bot Message Types
@@ -3,12 +3,9 @@ properties:
name:
type: string
description: The name of the team
example: Support Team
description:
type: string
description: The description of the team
example: This is a team of support agents
allow_auto_assign:
type: boolean
description: If this setting is turned on, the system would automatically assign the conversation to an agent in the team while assigning the conversation to a team
example: true
@@ -3,20 +3,13 @@ properties:
name:
type: string
description: Name of the user
example: 'Daniel'
display_name:
type: string
description: Display name of the user
example: 'Dan'
email:
type: string
description: Email of the user
example: 'daniel@acme.inc'
password:
type: string
description: Password must contain uppercase, lowercase letters, number and a special character
example: 'Password2!'
custom_attributes:
type: object
description: Custom attributes you want to associate with the user
example: {}

Some files were not shown because too many files have changed in this diff Show More