Compare commits

..
Author SHA1 Message Date
Tanmay Sharma 97847e2cd4 add migration file for contact inboxes 2025-07-22 18:10:24 +04:00
94b7154926 chore: add audit-logs swagger (#12001)
Co-authored-by: Tanmay Sharma <tanmaydeepsharma21@gmail.com>
2025-07-22 13:42:31 +04:00
Vishnu NarayananandGitHub 60951b45fd fix: cwctl web/worker conversion (#12006) 2025-07-22 13:38:51 +04:00
Shivam MishraandGitHub 69ad953ae6 fix: bubble color for outgoing email (#12003) 2025-07-22 14:26:55 +05:30
Vishnu NarayananandGitHub c4b076dbb2 feat: allow setting up only web/worker deployments for linux (#12004)
## cwctl
-  allow setting up only web/worker deployments for linux
- allow upgrades to a custom branch - experimental

```
Usage Examples:
  # Convert existing full deployment to web-only (for web ASG)
  cwctl --convert web

  # Convert existing full deployment to worker-only (for worker ASG)
   cwctl --convert worker

  # Convert specialized deployment back to full
  cwctl --convert full

  # Fresh installs still work as before
    cwctl -i --web-only    # New web-only install
    cwctl -i --worker-only # New worker-only instal
```

- Upgrading to custom branches is risky, as other dependencies like node
or db might have changed

```
  cwctl --upgrade        # Upgrade to master branch - default
  cwctl -U v4.3.0         # Upgrade to specific release branch 
  cwctl --upgrade feat/new-feature  # Upgrade to feature branch
```
2025-07-22 12:54:25 +04:00
25 changed files with 1091 additions and 134 deletions
+1 -1
View File
@@ -1 +1 @@
3.13.0
4.4.0
+1 -1
View File
@@ -1 +1 @@
3.2.0
3.4.0
-16
View File
@@ -65,8 +65,6 @@ class ContactInboxBuilder
source_id: @source_id
}
update_contact_verification_status
::ContactInbox.where(attrs).first_or_create!(hmac_verified: hmac_verified || false)
rescue ActiveRecord::RecordNotUnique
Rails.logger.info("[ContactInboxBuilder] RecordNotUnique #{@source_id} #{@contact.id} #{@inbox.id}")
@@ -104,18 +102,4 @@ class ContactInboxBuilder
def allowed_channels?
@inbox.email? || @inbox.sms? || @inbox.twilio? || @inbox.whatsapp?
end
def update_contact_verification_status
return if @contact.is_verified
return unless should_verify_contact?
@contact.update!(is_verified: true)
end
def should_verify_contact?
# Social channels where contacts should be verified by default
@inbox.facebook? || @inbox.instagram? || @inbox.instagram_direct? ||
@inbox.twitter? || @inbox.line? || @inbox.whatsapp? ||
@inbox.telegram?
end
end
@@ -17,8 +17,8 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
before_action :set_include_contact_inboxes, only: [:index, :active, :search, :filter, :show, :update]
def index
@contacts_count = resolved_contacts.count
@contacts = fetch_contacts(resolved_contacts)
@contacts_count = @contacts.total_count
end
def search
@@ -29,8 +29,8 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
OR contacts.additional_attributes->>\'company_name\' ILIKE :search',
search: "%#{params[:q].strip}%"
)
@contacts_count = contacts.count
@contacts = fetch_contacts(contacts)
@contacts_count = @contacts.total_count
end
def import
@@ -55,8 +55,8 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
def active
contacts = Current.account.contacts.where(id: ::OnlineStatusTracker
.get_available_contact_ids(Current.account.id))
@contacts_count = contacts.count
@contacts = fetch_contacts(contacts)
@contacts_count = @contacts.total_count
end
def show; end
@@ -66,7 +66,6 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
contacts = result[:contacts]
@contacts_count = result[:count]
@contacts = fetch_contacts(contacts)
rescue CustomExceptions::CustomFilter::InvalidAttribute,
CustomExceptions::CustomFilter::InvalidOperator,
CustomExceptions::CustomFilter::InvalidQueryOperator,
@@ -134,14 +133,13 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
end
def fetch_contacts(contacts)
# Build includes hash to avoid separate query when contact_inboxes are needed
includes_hash = { avatar_attachment: [:blob] }
includes_hash[:contact_inboxes] = { inbox: :channel } if @include_contact_inboxes
contacts_with_avatar = filtrate(contacts)
.includes([{ avatar_attachment: [:blob] }])
.page(@current_page).per(RESULTS_PER_PAGE)
filtrate(contacts)
.includes(includes_hash)
.page(@current_page)
.per(RESULTS_PER_PAGE)
return contacts_with_avatar.includes([{ contact_inboxes: [:inbox] }]) if @include_contact_inboxes
contacts_with_avatar
end
def build_contact_inbox
@@ -119,7 +119,13 @@ const handleSeeOriginal = () => {
>
<div
v-if="isExpandable && !isExpanded"
class="absolute left-0 right-0 bottom-0 h-40 px-8 flex items-end bg-gradient-to-t from-n-slate-4 via-n-slate-4 via-20% to-transparent"
class="absolute left-0 right-0 bottom-0 h-40 px-8 flex items-end"
:class="{
'bg-gradient-to-t from-n-slate-4 via-n-slate-4 via-20% to-transparent':
isIncoming,
'bg-gradient-to-t from-n-solid-blue via-n-solid-blue via-20% to-transparent':
isOutgoing,
}"
>
<button
class="text-n-slate-12 py-2 px-8 mx-auto text-center flex items-center gap-2"
@@ -1,41 +0,0 @@
# Delete migration and spec after 2 consecutive releases.
class Migration::PopulateContactVerificationJob < ApplicationJob
include Sidekiq::Worker
sidekiq_options queue: :scheduled_jobs, timeout: 3_600 # 1 hour
def perform
updated_count = 0
Contact.find_each(batch_size: 1000) do |contact|
next if contact.is_verified
next unless should_verify_contact?(contact)
contact.update_columns(is_verified: true) # rubocop:disable Rails/SkipsModelValidations
updated_count += 1
end
Rails.logger.info "[PopulateContactVerificationJob] Updated #{updated_count} contacts to verified"
end
private
def should_verify_contact?(contact)
verification_attributes?(contact) || social_channel_inbox?(contact)
end
def verification_attributes?(contact)
contact.email.present? ||
contact.phone_number.present? ||
contact.identifier.present?
end
def social_channel_inbox?(contact)
contact.inboxes.any? { |inbox| social_channel?(inbox) }
end
def social_channel?(inbox)
inbox.facebook? || inbox.instagram? || inbox.instagram_direct? ||
inbox.twitter? || inbox.line? || inbox.whatsapp? ||
inbox.telegram?
end
end
+4 -14
View File
@@ -12,7 +12,7 @@
# custom_attributes :jsonb
# email :string
# identifier :string
# is_verified :boolean default(FALSE), not null
# verified :boolean default(FALSE), not null
# last_activity_at :datetime
# last_name :string default("")
# location :string default("")
@@ -26,10 +26,9 @@
# Indexes
#
# index_contacts_on_account_id (account_id)
# index_contacts_on_account_id_and_is_verified (account_id,is_verified)
# index_contacts_on_account_id_and_verified (account_id,verified)
# index_contacts_on_account_id_and_last_activity_at (account_id,last_activity_at DESC NULLS LAST)
# index_contacts_on_blocked (blocked)
# index_contacts_on_is_verified (is_verified)
# index_contacts_on_lower_email_account_id (lower((email)::text), account_id)
# index_contacts_on_name_email_phone_number_identifier (name,email,phone_number,identifier) USING gin
# index_contacts_on_nonempty_fields (account_id,email,phone_number,identifier) WHERE (((email)::text <> ''::text) OR ((phone_number)::text <> ''::text) OR ((identifier)::text <> ''::text))
@@ -66,7 +65,7 @@ class Contact < ApplicationRecord
after_create_commit :dispatch_create_event, :ip_lookup
after_update_commit :dispatch_update_event
after_destroy_commit :dispatch_destroy_event
before_save :sync_contact_attributes, :update_is_verified
before_save :sync_contact_attributes
enum contact_type: { visitor: 0, lead: 1, customer: 2 }
@@ -179,7 +178,7 @@ class Contact < ApplicationRecord
end
def self.resolved_contacts
where(is_verified: true)
where("contacts.email <> '' OR contacts.phone_number <> '' OR contacts.identifier <> ''")
end
def discard_invalid_attrs
@@ -241,13 +240,4 @@ class Contact < ApplicationRecord
def dispatch_destroy_event
Rails.configuration.dispatcher.dispatch(CONTACT_DELETED, Time.zone.now, contact: self)
end
def update_is_verified
# Once verified, always verified - don't change back to false
return if is_verified
self.is_verified = email.present? ||
phone_number.present? ||
identifier.present?
end
end
-8
View File
@@ -135,14 +135,6 @@ class Inbox < ApplicationRecord
channel_type == 'Channel::Email'
end
def line?
channel_type == 'Channel::Line'
end
def telegram?
channel_type == 'Channel::Telegram'
end
def twilio?
channel_type == 'Channel::TwilioSms'
end
@@ -5,6 +5,6 @@ end
json.payload do
json.array! @contacts do |contact|
json.partial! 'api/v1/models/contact', formats: [:json], resource: contact, with_contact_inboxes: @include_contact_inboxes
json.partial! 'api/v1/models/contact', formats: [:json], resource: contact, with_contact_inboxes: true
end
end
@@ -1,7 +1,7 @@
class AddIsVerifiedToContacts < ActiveRecord::Migration[7.1]
def change
add_column :contacts, :is_verified, :boolean, default: false, null: false
add_index :contacts, :is_verified
add_index :contacts, [:account_id, :is_verified], name: 'index_contacts_on_account_id_and_is_verified'
add_column :contacts, :verified, :boolean, default: false, null: false
add_index :contacts, [:account_id, :verified], name: 'index_contacts_on_account_id_and_verified'
end
end
@@ -1,5 +0,0 @@
class PopulateContactVerificationForSocialChannels < ActiveRecord::Migration[7.0]
def change
Migration::PopulateContactVerificationJob.perform_later
end
end
+3 -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_07_22_153206) do
ActiveRecord::Schema[7.1].define(version: 2025_07_22_093235) do
# These extensions should be enabled to support this database
enable_extension "pg_stat_statements"
enable_extension "pg_trgm"
@@ -538,17 +538,16 @@ ActiveRecord::Schema[7.1].define(version: 2025_07_22_153206) do
t.string "location", default: ""
t.string "country_code", default: ""
t.boolean "blocked", default: false, null: false
t.boolean "is_verified", default: false, null: false
t.boolean "verified", default: false, null: false
t.index "lower((email)::text), account_id", name: "index_contacts_on_lower_email_account_id"
t.index ["account_id", "email", "phone_number", "identifier"], name: "index_contacts_on_nonempty_fields", where: "(((email)::text <> ''::text) OR ((phone_number)::text <> ''::text) OR ((identifier)::text <> ''::text))"
t.index ["account_id", "is_verified"], name: "index_contacts_on_account_id_and_is_verified"
t.index ["account_id", "verified"], name: "index_contacts_on_account_id_and_verified"
t.index ["account_id", "last_activity_at"], name: "index_contacts_on_account_id_and_last_activity_at", order: { last_activity_at: "DESC NULLS LAST" }
t.index ["account_id"], name: "index_contacts_on_account_id"
t.index ["account_id"], name: "index_resolved_contact_account_id", where: "(((email)::text <> ''::text) OR ((phone_number)::text <> ''::text) OR ((identifier)::text <> ''::text))"
t.index ["blocked"], name: "index_contacts_on_blocked"
t.index ["email", "account_id"], name: "uniq_email_per_account_contact", unique: true
t.index ["identifier", "account_id"], name: "uniq_identifier_per_account_contact", unique: true
t.index ["is_verified"], name: "index_contacts_on_is_verified"
t.index ["name", "email", "phone_number", "identifier"], name: "index_contacts_on_name_email_phone_number_identifier", opclass: :gin_trgm_ops, using: :gin
t.index ["phone_number", "account_id"], name: "index_contacts_on_phone_number_and_account_id"
end
+6
View File
@@ -0,0 +1,6 @@
[Unit]
Description=Chatwoot Web Server
Wants=chatwoot-web.1.service
[Install]
WantedBy=multi-user.target
+6
View File
@@ -0,0 +1,6 @@
[Unit]
Description=Chatwoot Background Worker
Wants=chatwoot-worker.1.service
[Install]
WantedBy=multi-user.target
+236 -28
View File
@@ -2,7 +2,7 @@
# Description: Install and manage a Chatwoot installation.
# OS: Ubuntu 20.04 LTS, 22.04 LTS, 24.04 LTS
# Script Version: 3.2.0
# Script Version: 3.4.0
# Run this script as root
set -eu -o errexit -o pipefail -o noclobber -o nounset
@@ -17,9 +17,9 @@ fi
# Global variables
# option --output/-o requires 1 argument
LONGOPTS=console,debug,help,install,Install:,logs:,restart,ssl,upgrade,webserver,version
OPTIONS=cdhiI:l:rsuwv
CWCTL_VERSION="3.2.0"
LONGOPTS=console,debug,help,install,Install:,logs:,restart,ssl,upgrade,Upgrade:,webserver,version,web-only,worker-only,convert:
OPTIONS=cdhiI:l:rsuU:wvWK
CWCTL_VERSION="3.3.0"
pg_pass=$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 15 ; echo '')
CHATWOOT_HUB_URL="https://hub.2.chatwoot.com/events"
@@ -42,7 +42,7 @@ fi
# read getopt’s output this way to handle the quoting right:
eval set -- "$PARSED"
c=n d=n h=n i=n I=n l=n r=n s=n u=n w=n v=n BRANCH=master SERVICE=web
c=n d=n h=n i=n I=n l=n r=n s=n u=n U=n w=n v=n W=n K=n C=n BRANCH=master SERVICE=web DEPLOYMENT_TYPE=full CONVERT_TO=""
# Iterate options in order and nicely split until we see --
while true; do
case "$1" in
@@ -83,6 +83,12 @@ while true; do
;;
-u|--upgrade)
u=y
BRANCH="master"
break
;;
-U|--Upgrade)
U=y
BRANCH="$2"
break
;;
-w|--webserver)
@@ -93,6 +99,36 @@ while true; do
v=y
shift
;;
-W|--web-only)
W=y
DEPLOYMENT_TYPE=web
shift
;;
-K|--worker-only)
K=y
DEPLOYMENT_TYPE=worker
shift
;;
--convert)
C=y
CONVERT_TO="$2"
case "$CONVERT_TO" in
web)
DEPLOYMENT_TYPE=web
;;
worker)
DEPLOYMENT_TYPE=worker
;;
full)
DEPLOYMENT_TYPE=full
;;
*)
echo "Invalid conversion type. Use: web, worker, or full"
exit 3
;;
esac
shift 2
;;
--)
shift
break
@@ -107,7 +143,7 @@ done
# log if debug flag set
if [ "$d" == "y" ]; then
echo "console: $c, debug: $d, help: $h, install: $i, Install: $I, BRANCH: $BRANCH, \
logs: $l, SERVICE: $SERVICE, ssl: $s, upgrade: $u, webserver: $w"
logs: $l, SERVICE: $SERVICE, ssl: $s, upgrade: $u, Upgrade: $U, webserver: $w, web-only: $W, worker-only: $K, convert: $C, convert-to: $CONVERT_TO, deployment-type: $DEPLOYMENT_TYPE"
fi
# exit if script is not run as root
@@ -379,23 +415,98 @@ EOF
##############################################################################
# Setup Chatwoot systemd services and cwctl CLI
# Globals:
# None
# DEPLOYMENT_TYPE
# Arguments:
# None
# Outputs:
# None
##############################################################################
function configure_systemd_services() {
cp /home/chatwoot/chatwoot/deployment/chatwoot-web.1.service /etc/systemd/system/chatwoot-web.1.service
cp /home/chatwoot/chatwoot/deployment/chatwoot-worker.1.service /etc/systemd/system/chatwoot-worker.1.service
cp /home/chatwoot/chatwoot/deployment/chatwoot.target /etc/systemd/system/chatwoot.target
# Check if this is a conversion from existing deployment
local existing_full_deployment=false
if [ -f "/etc/systemd/system/chatwoot.target" ]; then
existing_full_deployment=true
fi
if [ "$DEPLOYMENT_TYPE" == "web" ]; then
echo "Setting up web-only deployment"
# Stop and disable existing services if converting
if [ "$existing_full_deployment" = true ]; then
echo "Converting from full deployment to web-only"
systemctl stop chatwoot.target || true
systemctl disable chatwoot.target || true
systemctl stop chatwoot-worker.1.service || true
systemctl disable chatwoot-worker.1.service || true
fi
# Stop and disable worker target if converting from worker-only
if [ -f "/etc/systemd/system/chatwoot-worker.target" ]; then
echo "Converting from worker-only to web-only"
systemctl stop chatwoot-worker.target || true
systemctl disable chatwoot-worker.target || true
fi
cp /home/chatwoot/chatwoot/deployment/chatwoot-web.1.service /etc/systemd/system/chatwoot-web.1.service
cp /home/chatwoot/chatwoot/deployment/chatwoot-web.target /etc/systemd/system/chatwoot-web.target
systemctl daemon-reload
systemctl enable chatwoot-web.target
systemctl start chatwoot-web.target
elif [ "$DEPLOYMENT_TYPE" == "worker" ]; then
echo "Setting up worker-only deployment"
# Stop and disable existing services if converting
if [ "$existing_full_deployment" = true ]; then
echo "Converting from full deployment to worker-only"
systemctl stop chatwoot.target || true
systemctl disable chatwoot.target || true
systemctl stop chatwoot-web.1.service || true
systemctl disable chatwoot-web.1.service || true
fi
# Stop and disable web target if converting from web-only
if [ -f "/etc/systemd/system/chatwoot-web.target" ]; then
echo "Converting from web-only to worker-only"
systemctl stop chatwoot-web.target || true
systemctl disable chatwoot-web.target || true
fi
cp /home/chatwoot/chatwoot/deployment/chatwoot-worker.1.service /etc/systemd/system/chatwoot-worker.1.service
cp /home/chatwoot/chatwoot/deployment/chatwoot-worker.target /etc/systemd/system/chatwoot-worker.target
systemctl daemon-reload
systemctl enable chatwoot-worker.target
systemctl start chatwoot-worker.target
else
echo "Setting up full deployment (web + worker)"
# Stop existing specialized deployments if converting back to full
if [ -f "/etc/systemd/system/chatwoot-web.target" ]; then
echo "Converting from web-only to full deployment"
systemctl stop chatwoot-web.target || true
systemctl disable chatwoot-web.target || true
fi
if [ -f "/etc/systemd/system/chatwoot-worker.target" ]; then
echo "Converting from worker-only to full deployment"
systemctl stop chatwoot-worker.target || true
systemctl disable chatwoot-worker.target || true
fi
cp /home/chatwoot/chatwoot/deployment/chatwoot-web.1.service /etc/systemd/system/chatwoot-web.1.service
cp /home/chatwoot/chatwoot/deployment/chatwoot-worker.1.service /etc/systemd/system/chatwoot-worker.1.service
cp /home/chatwoot/chatwoot/deployment/chatwoot.target /etc/systemd/system/chatwoot.target
systemctl daemon-reload
systemctl enable chatwoot.target
systemctl start chatwoot.target
fi
cp /home/chatwoot/chatwoot/deployment/chatwoot /etc/sudoers.d/chatwoot
cp /home/chatwoot/chatwoot/deployment/setup_20.04.sh /usr/local/bin/cwctl
chmod +x /usr/local/bin/cwctl
systemctl enable chatwoot.target
systemctl start chatwoot.target
}
##############################################################################
@@ -427,7 +538,15 @@ function setup_ssl() {
cd chatwoot
sed -i "s/http:\/\/0.0.0.0:3000/https:\/\/$domain_name/g" .env
EOF
systemctl restart chatwoot.target
# Restart the appropriate chatwoot target
if [ -f "/etc/systemd/system/chatwoot-web.target" ]; then
systemctl restart chatwoot-web.target
elif [ -f "/etc/systemd/system/chatwoot-worker.target" ]; then
systemctl restart chatwoot-worker.target
else
systemctl restart chatwoot.target
fi
}
##############################################################################
@@ -624,17 +743,27 @@ Usage: cwctl [OPTION]...
Install and manage your Chatwoot installation.
Example: cwctl -i master
Example: cwctl -i --web-only (for web server ASG)
Example: cwctl -i --worker-only (for worker ASG)
Example: cwctl --convert web (convert existing to web-only)
Example: cwctl --convert worker (convert existing to worker-only)
Example: cwctl --convert full (convert back to full deployment)
Example: cwctl --upgrade (upgrade to latest master)
Example: cwctl -U develop (upgrade to develop branch)
Example: cwctl -l web
Example: cwctl --logs worker
Example: cwctl --upgrade
Example: cwctl -c
Installation/Upgrade:
-i, --install Install the latest stable version of Chatwoot
-I Install Chatwoot from a git branch
-I BRANCH Install Chatwoot from a git branch
-u, --upgrade Upgrade Chatwoot to the latest stable version
-U BRANCH Upgrade Chatwoot from a git branch (EXPERIMENTAL)
-s, --ssl Fetch and install SSL certificates using LetsEncrypt
-w, --webserver Install and configure Nginx webserver with SSL
-W, --web-only Install only the web server (for ASG deployment)
-K, --worker-only Install only the background worker (for ASG deployment)
--convert TYPE Convert existing deployment (TYPE: web, worker, full)
Management:
-c, --console Open ruby console
@@ -831,7 +960,31 @@ EOF
function upgrade() {
cwctl_upgrade_check
get_cw_version
echo "Upgrading Chatwoot to v$CW_VERSION"
echo "Upgrading Chatwoot to v$CW_VERSION (branch: $BRANCH)"
# Warning for non-master branch upgrades
if [ "$BRANCH" != "master" ]; then
cat << EOF
⚠️ WARNING: Branch-specific upgrades are EXPERIMENTAL
⚠️ Switching between different versions/branches may cause:
- Database migration conflicts
- Asset compilation errors
- Configuration incompatibilities
- Data corruption or loss
⚠️ This is NOT recommended for production environments.
⚠️ Always backup your database before proceeding.
EOF
read -p "Do you understand the risks and want to continue? [y/N]: " user_input
user_input=${user_input:-N}
if [[ ! "$user_input" =~ ^([yY][eE][sS]|[yY])$ ]]; then
echo "Upgrade cancelled."
exit 1
fi
fi
sleep 3
# Check if CW_VERSION is 4.0 or above
@@ -857,8 +1010,9 @@ function upgrade() {
# Navigate to the Chatwoot directory
cd chatwoot
# Pull the latest version of the master branch
git checkout master && git pull
# Pull the latest version of the specified branch
git fetch
git checkout "$BRANCH" && git pull
# Ensure the ruby version is upto date
# Parse the latest ruby version
@@ -878,18 +1032,35 @@ function upgrade() {
EOF
# Copy the updated targets
cp /home/chatwoot/chatwoot/deployment/chatwoot-web.1.service /etc/systemd/system/chatwoot-web.1.service
cp /home/chatwoot/chatwoot/deployment/chatwoot-worker.1.service /etc/systemd/system/chatwoot-worker.1.service
cp /home/chatwoot/chatwoot/deployment/chatwoot.target /etc/systemd/system/chatwoot.target
# Copy the updated services and targets based on existing deployment
if [ -f "/etc/systemd/system/chatwoot-web.target" ]; then
echo "Updating web-only deployment"
cp /home/chatwoot/chatwoot/deployment/chatwoot-web.1.service /etc/systemd/system/chatwoot-web.1.service
cp /home/chatwoot/chatwoot/deployment/chatwoot-web.target /etc/systemd/system/chatwoot-web.target
elif [ -f "/etc/systemd/system/chatwoot-worker.target" ]; then
echo "Updating worker-only deployment"
cp /home/chatwoot/chatwoot/deployment/chatwoot-worker.1.service /etc/systemd/system/chatwoot-worker.1.service
cp /home/chatwoot/chatwoot/deployment/chatwoot-worker.target /etc/systemd/system/chatwoot-worker.target
else
echo "Updating full deployment"
cp /home/chatwoot/chatwoot/deployment/chatwoot-web.1.service /etc/systemd/system/chatwoot-web.1.service
cp /home/chatwoot/chatwoot/deployment/chatwoot-worker.1.service /etc/systemd/system/chatwoot-worker.1.service
cp /home/chatwoot/chatwoot/deployment/chatwoot.target /etc/systemd/system/chatwoot.target
fi
cp /home/chatwoot/chatwoot/deployment/chatwoot /etc/sudoers.d/chatwoot
# TODO:(@vn) handle cwctl updates
systemctl daemon-reload
# Restart the chatwoot server
systemctl restart chatwoot.target
# Restart the appropriate chatwoot target
if [ -f "/etc/systemd/system/chatwoot-web.target" ]; then
systemctl restart chatwoot-web.target
elif [ -f "/etc/systemd/system/chatwoot-worker.target" ]; then
systemctl restart chatwoot-worker.target
else
systemctl restart chatwoot.target
fi
}
@@ -903,8 +1074,40 @@ EOF
# None
##############################################################################
function restart() {
systemctl restart chatwoot.target
systemctl status chatwoot.target
if [ -f "/etc/systemd/system/chatwoot-web.target" ]; then
systemctl restart chatwoot-web.target
systemctl status chatwoot-web.target
elif [ -f "/etc/systemd/system/chatwoot-worker.target" ]; then
systemctl restart chatwoot-worker.target
systemctl status chatwoot-worker.target
else
systemctl restart chatwoot.target
systemctl status chatwoot.target
fi
}
##############################################################################
# Convert existing Chatwoot deployment to different type (--convert)
# Globals:
# DEPLOYMENT_TYPE
# Arguments:
# None
# Outputs:
# None
##############################################################################
function convert_deployment() {
echo "Converting Chatwoot deployment to: $DEPLOYMENT_TYPE"
# Check if Chatwoot is installed
if [ ! -d "/home/chatwoot/chatwoot" ]; then
echo "Chatwoot installation not found. Use --install first."
exit 1
fi
# Run the systemd service configuration which handles conversion logic
configure_systemd_services
echo "Deployment converted successfully to: $DEPLOYMENT_TYPE"
}
##############################################################################
@@ -1107,7 +1310,7 @@ function main() {
ssl
fi
if [ "$u" == "y" ]; then
if [ "$u" == "y" ] || [ "$U" == "y" ]; then
report_event "cwctl" "upgrade" > /dev/null 2>&1
upgrade
fi
@@ -1122,6 +1325,11 @@ function main() {
version
fi
if [ "$C" == "y" ]; then
report_event "cwctl" "convert" > /dev/null 2>&1
convert_deployment
fi
}
main "$@"
+2
View File
@@ -74,6 +74,8 @@ integrations_app:
$ref: ./resource/integrations/app.yml
integrations_hook:
$ref: ./resource/integrations/hook.yml
audit_log:
$ref: ./resource/audit_log.yml
## public resources
public_contact:
@@ -0,0 +1,53 @@
type: object
properties:
id:
type: integer
description: Unique identifier for the audit log entry
auditable_id:
type: integer
description: The ID of the audited object
auditable_type:
type: string
description: The type of the audited object (e.g., Conversation, Contact, User)
auditable:
type: object
description: The audited object data
associated_id:
type: integer
description: The ID of the associated object (typically the account ID)
associated_type:
type: string
description: The type of the associated object
user_id:
type: integer
description: The ID of the user who performed the action
user_type:
type: string
description: The type of user who performed the action
username:
type: string
description: The email/username of the user who performed the action
action:
type: string
enum: ['create', 'update', 'destroy']
description: The action performed on the object
audited_changes:
type: object
description: JSON object containing the changes made to the audited object
version:
type: integer
description: Version number of the audit log entry
comment:
type: string
nullable: true
description: Optional comment associated with the audit log entry
request_uuid:
type: string
description: UUID to identify the request that generated this audit log
created_at:
type: integer
description: Unix timestamp when the audit log entry was created
remote_address:
type: string
nullable: true
description: IP address from which the action was performed
+2
View File
@@ -99,7 +99,9 @@ x-tagGroups:
- name: Application
tags:
- Account AgentBots
- Account
- Agents
- Audit Logs
- Canned Responses
- Contacts
- Contact Labels
@@ -0,0 +1,52 @@
tags:
- Audit Logs
operationId: get-account-audit-logs
summary: List Audit Logs in Account
description: Get Details of Audit Log entries for an Account. This endpoint is only available in Enterprise editions and requires the audit_logs feature to be enabled.
security:
- userApiKey: []
parameters:
- name: page
in: query
description: Page number for pagination
required: false
schema:
type: integer
default: 1
responses:
200:
description: Success
content:
application/json:
schema:
type: object
properties:
per_page:
type: integer
description: Number of items per page
example: 15
total_entries:
type: integer
description: Total number of audit log entries
example: 150
current_page:
type: integer
description: Current page number
example: 1
audit_logs:
type: array
description: Array of audit log entries
items:
$ref: '#/components/schemas/audit_log'
403:
description: Access denied
content:
application/json:
schema:
$ref: '#/components/schemas/bad_request_error'
422:
description: Feature not enabled or not available in current plan
content:
application/json:
schema:
$ref: '#/components/schemas/bad_request_error'
+7
View File
@@ -175,6 +175,13 @@
patch:
$ref: ./application/accounts/update.yml
# Audit Logs
/api/v1/accounts/{account_id}/audit_logs:
parameters:
- $ref: '#/components/parameters/account_id'
get:
$ref: ./application/audit_logs/index.yml
# AgentBots
/api/v1/accounts/{account_id}/agent_bots:
parameters:
+166
View File
@@ -1608,6 +1608,94 @@
}
}
},
"/api/v1/accounts/{account_id}/audit_logs": {
"parameters": [
{
"$ref": "#/components/parameters/account_id"
}
],
"get": {
"tags": [
"Audit Logs"
],
"operationId": "get-account-audit-logs",
"summary": "List Audit Logs in Account",
"description": "Get Details of Audit Log entries for an Account. This endpoint is only available in Enterprise editions and requires the audit_logs feature to be enabled.",
"security": [
{
"userApiKey": []
}
],
"parameters": [
{
"name": "page",
"in": "query",
"description": "Page number for pagination",
"required": false,
"schema": {
"type": "integer",
"default": 1
}
}
],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"per_page": {
"type": "integer",
"description": "Number of items per page",
"example": 15
},
"total_entries": {
"type": "integer",
"description": "Total number of audit log entries",
"example": 150
},
"current_page": {
"type": "integer",
"description": "Current page number",
"example": 1
},
"audit_logs": {
"type": "array",
"description": "Array of audit log entries",
"items": {
"$ref": "#/components/schemas/audit_log"
}
}
}
}
}
}
},
"403": {
"description": "Access denied",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/bad_request_error"
}
}
}
},
"422": {
"description": "Feature not enabled or not available in current plan",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/bad_request_error"
}
}
}
}
}
}
},
"/api/v1/accounts/{account_id}/agent_bots": {
"parameters": [
{
@@ -9177,6 +9265,82 @@
}
}
},
"audit_log": {
"type": "object",
"properties": {
"id": {
"type": "integer",
"description": "Unique identifier for the audit log entry"
},
"auditable_id": {
"type": "integer",
"description": "The ID of the audited object"
},
"auditable_type": {
"type": "string",
"description": "The type of the audited object (e.g., Conversation, Contact, User)"
},
"auditable": {
"type": "object",
"description": "The audited object data"
},
"associated_id": {
"type": "integer",
"description": "The ID of the associated object (typically the account ID)"
},
"associated_type": {
"type": "string",
"description": "The type of the associated object"
},
"user_id": {
"type": "integer",
"description": "The ID of the user who performed the action"
},
"user_type": {
"type": "string",
"description": "The type of user who performed the action"
},
"username": {
"type": "string",
"description": "The email/username of the user who performed the action"
},
"action": {
"type": "string",
"enum": [
"create",
"update",
"destroy"
],
"description": "The action performed on the object"
},
"audited_changes": {
"type": "object",
"description": "JSON object containing the changes made to the audited object"
},
"version": {
"type": "integer",
"description": "Version number of the audit log entry"
},
"comment": {
"type": "string",
"nullable": true,
"description": "Optional comment associated with the audit log entry"
},
"request_uuid": {
"type": "string",
"description": "UUID to identify the request that generated this audit log"
},
"created_at": {
"type": "integer",
"description": "Unix timestamp when the audit log entry was created"
},
"remote_address": {
"type": "string",
"nullable": true,
"description": "IP address from which the action was performed"
}
}
},
"public_contact": {
"type": "object",
"properties": {
@@ -12148,7 +12312,9 @@
"name": "Application",
"tags": [
"Account AgentBots",
"Account",
"Agents",
"Audit Logs",
"Canned Responses",
"Contacts",
"Contact Labels",
+298
View File
@@ -19,6 +19,226 @@
}
],
"paths": {
"/api/v1/accounts/{id}": {
"parameters": [
{
"$ref": "#/components/parameters/account_id"
}
],
"get": {
"tags": [
"Account"
],
"operationId": "get-account-details",
"summary": "Get account details",
"description": "Get the details of the current account",
"security": [
{
"userApiKey": []
}
],
"parameters": [
{
"$ref": "#/components/parameters/account_id"
}
],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/account_show_response"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/bad_request_error"
}
}
}
},
"404": {
"description": "Account not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/bad_request_error"
}
}
}
}
}
},
"patch": {
"tags": [
"Account"
],
"operationId": "update-account",
"summary": "Update account",
"description": "Update account details, settings, and custom attributes",
"security": [
{
"userApiKey": []
}
],
"parameters": [
{
"$ref": "#/components/parameters/account_id"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/account_update_payload"
}
},
"application/x-www-form-urlencoded": {
"schema": {
"$ref": "#/components/schemas/account_update_payload"
}
}
}
},
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/account_detail"
}
}
}
},
"401": {
"description": "Unauthorized (requires administrator role)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/bad_request_error"
}
}
}
},
"404": {
"description": "Account not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/bad_request_error"
}
}
}
},
"422": {
"description": "Validation error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/bad_request_error"
}
}
}
}
}
}
},
"/api/v1/accounts/{account_id}/audit_logs": {
"parameters": [
{
"$ref": "#/components/parameters/account_id"
}
],
"get": {
"tags": [
"Audit Logs"
],
"operationId": "get-account-audit-logs",
"summary": "List Audit Logs in Account",
"description": "Get Details of Audit Log entries for an Account. This endpoint is only available in Enterprise editions and requires the audit_logs feature to be enabled.",
"security": [
{
"userApiKey": []
}
],
"parameters": [
{
"name": "page",
"in": "query",
"description": "Page number for pagination",
"required": false,
"schema": {
"type": "integer",
"default": 1
}
}
],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"per_page": {
"type": "integer",
"description": "Number of items per page",
"example": 15
},
"total_entries": {
"type": "integer",
"description": "Total number of audit log entries",
"example": 150
},
"current_page": {
"type": "integer",
"description": "Current page number",
"example": 1
},
"audit_logs": {
"type": "array",
"description": "Array of audit log entries",
"items": {
"$ref": "#/components/schemas/audit_log"
}
}
}
}
}
}
},
"403": {
"description": "Access denied",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/bad_request_error"
}
}
}
},
"422": {
"description": "Feature not enabled or not available in current plan",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/bad_request_error"
}
}
}
}
}
}
},
"/api/v1/accounts/{account_id}/agent_bots": {
"parameters": [
{
@@ -7406,6 +7626,82 @@
}
}
},
"audit_log": {
"type": "object",
"properties": {
"id": {
"type": "integer",
"description": "Unique identifier for the audit log entry"
},
"auditable_id": {
"type": "integer",
"description": "The ID of the audited object"
},
"auditable_type": {
"type": "string",
"description": "The type of the audited object (e.g., Conversation, Contact, User)"
},
"auditable": {
"type": "object",
"description": "The audited object data"
},
"associated_id": {
"type": "integer",
"description": "The ID of the associated object (typically the account ID)"
},
"associated_type": {
"type": "string",
"description": "The type of the associated object"
},
"user_id": {
"type": "integer",
"description": "The ID of the user who performed the action"
},
"user_type": {
"type": "string",
"description": "The type of user who performed the action"
},
"username": {
"type": "string",
"description": "The email/username of the user who performed the action"
},
"action": {
"type": "string",
"enum": [
"create",
"update",
"destroy"
],
"description": "The action performed on the object"
},
"audited_changes": {
"type": "object",
"description": "JSON object containing the changes made to the audited object"
},
"version": {
"type": "integer",
"description": "Version number of the audit log entry"
},
"comment": {
"type": "string",
"nullable": true,
"description": "Optional comment associated with the audit log entry"
},
"request_uuid": {
"type": "string",
"description": "UUID to identify the request that generated this audit log"
},
"created_at": {
"type": "integer",
"description": "Unix timestamp when the audit log entry was created"
},
"remote_address": {
"type": "string",
"nullable": true,
"description": "IP address from which the action was performed"
}
}
},
"public_contact": {
"type": "object",
"properties": {
@@ -10345,7 +10641,9 @@
"name": "Application",
"tags": [
"Account AgentBots",
"Account",
"Agents",
"Audit Logs",
"Canned Responses",
"Contacts",
"Contact Labels",
+78
View File
@@ -2249,6 +2249,82 @@
}
}
},
"audit_log": {
"type": "object",
"properties": {
"id": {
"type": "integer",
"description": "Unique identifier for the audit log entry"
},
"auditable_id": {
"type": "integer",
"description": "The ID of the audited object"
},
"auditable_type": {
"type": "string",
"description": "The type of the audited object (e.g., Conversation, Contact, User)"
},
"auditable": {
"type": "object",
"description": "The audited object data"
},
"associated_id": {
"type": "integer",
"description": "The ID of the associated object (typically the account ID)"
},
"associated_type": {
"type": "string",
"description": "The type of the associated object"
},
"user_id": {
"type": "integer",
"description": "The ID of the user who performed the action"
},
"user_type": {
"type": "string",
"description": "The type of user who performed the action"
},
"username": {
"type": "string",
"description": "The email/username of the user who performed the action"
},
"action": {
"type": "string",
"enum": [
"create",
"update",
"destroy"
],
"description": "The action performed on the object"
},
"audited_changes": {
"type": "object",
"description": "JSON object containing the changes made to the audited object"
},
"version": {
"type": "integer",
"description": "Version number of the audit log entry"
},
"comment": {
"type": "string",
"nullable": true,
"description": "Optional comment associated with the audit log entry"
},
"request_uuid": {
"type": "string",
"description": "UUID to identify the request that generated this audit log"
},
"created_at": {
"type": "integer",
"description": "Unix timestamp when the audit log entry was created"
},
"remote_address": {
"type": "string",
"nullable": true,
"description": "IP address from which the action was performed"
}
}
},
"public_contact": {
"type": "object",
"properties": {
@@ -5124,7 +5200,9 @@
"name": "Application",
"tags": [
"Account AgentBots",
"Account",
"Agents",
"Audit Logs",
"Canned Responses",
"Contacts",
"Contact Labels",
+78
View File
@@ -1664,6 +1664,82 @@
}
}
},
"audit_log": {
"type": "object",
"properties": {
"id": {
"type": "integer",
"description": "Unique identifier for the audit log entry"
},
"auditable_id": {
"type": "integer",
"description": "The ID of the audited object"
},
"auditable_type": {
"type": "string",
"description": "The type of the audited object (e.g., Conversation, Contact, User)"
},
"auditable": {
"type": "object",
"description": "The audited object data"
},
"associated_id": {
"type": "integer",
"description": "The ID of the associated object (typically the account ID)"
},
"associated_type": {
"type": "string",
"description": "The type of the associated object"
},
"user_id": {
"type": "integer",
"description": "The ID of the user who performed the action"
},
"user_type": {
"type": "string",
"description": "The type of user who performed the action"
},
"username": {
"type": "string",
"description": "The email/username of the user who performed the action"
},
"action": {
"type": "string",
"enum": [
"create",
"update",
"destroy"
],
"description": "The action performed on the object"
},
"audited_changes": {
"type": "object",
"description": "JSON object containing the changes made to the audited object"
},
"version": {
"type": "integer",
"description": "Version number of the audit log entry"
},
"comment": {
"type": "string",
"nullable": true,
"description": "Optional comment associated with the audit log entry"
},
"request_uuid": {
"type": "string",
"description": "UUID to identify the request that generated this audit log"
},
"created_at": {
"type": "integer",
"description": "Unix timestamp when the audit log entry was created"
},
"remote_address": {
"type": "string",
"nullable": true,
"description": "IP address from which the action was performed"
}
}
},
"public_contact": {
"type": "object",
"properties": {
@@ -4531,7 +4607,9 @@
"name": "Application",
"tags": [
"Account AgentBots",
"Account",
"Agents",
"Audit Logs",
"Canned Responses",
"Contacts",
"Contact Labels",
+78
View File
@@ -2425,6 +2425,82 @@
}
}
},
"audit_log": {
"type": "object",
"properties": {
"id": {
"type": "integer",
"description": "Unique identifier for the audit log entry"
},
"auditable_id": {
"type": "integer",
"description": "The ID of the audited object"
},
"auditable_type": {
"type": "string",
"description": "The type of the audited object (e.g., Conversation, Contact, User)"
},
"auditable": {
"type": "object",
"description": "The audited object data"
},
"associated_id": {
"type": "integer",
"description": "The ID of the associated object (typically the account ID)"
},
"associated_type": {
"type": "string",
"description": "The type of the associated object"
},
"user_id": {
"type": "integer",
"description": "The ID of the user who performed the action"
},
"user_type": {
"type": "string",
"description": "The type of user who performed the action"
},
"username": {
"type": "string",
"description": "The email/username of the user who performed the action"
},
"action": {
"type": "string",
"enum": [
"create",
"update",
"destroy"
],
"description": "The action performed on the object"
},
"audited_changes": {
"type": "object",
"description": "JSON object containing the changes made to the audited object"
},
"version": {
"type": "integer",
"description": "Version number of the audit log entry"
},
"comment": {
"type": "string",
"nullable": true,
"description": "Optional comment associated with the audit log entry"
},
"request_uuid": {
"type": "string",
"description": "UUID to identify the request that generated this audit log"
},
"created_at": {
"type": "integer",
"description": "Unix timestamp when the audit log entry was created"
},
"remote_address": {
"type": "string",
"nullable": true,
"description": "IP address from which the action was performed"
}
}
},
"public_contact": {
"type": "object",
"properties": {
@@ -5304,7 +5380,9 @@
"name": "Application",
"tags": [
"Account AgentBots",
"Account",
"Agents",
"Audit Logs",
"Canned Responses",
"Contacts",
"Contact Labels",