Compare commits

..
Author SHA1 Message Date
Shivam Mishra 648b9c9507 chore: add logs for debug 2025-05-22 10:08:19 +05:30
611fc82847 feat: Add components to show steps in the copilot thinking process (#11530)
This PR adds the components for new Copilot UI
- Added a Header component
- Added a thinking block.
- Update the outline on copilot input

---------

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
2025-05-22 10:08:19 +05:30
ffad8458cb feat: Delete a contact from the contacts page (#11529)
# Pull Request Template

## Description


**This PR includes:**

1. Adds the ability to delete a contact from the contacts list accordion
section.
2. Improves the expand/collapse transition for the accordion.


Fixes
[CW-4375](https://linear.app/chatwoot/issue/CW-4375/allow-users-to-delete-a-contact-from-the-contacts-page)

## Type of change

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

## How Has This Been Tested?

### Loom video

https://www.loom.com/share/8c897d24737f40f6b8b29fef76ba18e2?sid=70910b9d-f3db-4d54-8bfc-820db680e537


## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules

---------

Co-authored-by: Pranav <pranavrajs@gmail.com>
2025-05-22 10:08:19 +05:30
5869a98e6e fix: account email validation during signup (#11307)
- Refactor email validation logic to be a service
- Use the service for both email/pass signup and Google SSO
- fix account email validation during signup
- Use `blocked_domain` setting for both email/pass signup and Google
Sign In [`BLOCKED_DOMAIN` via GlobalConfig]
- add specs for `account_builder`
- add specs for the new service

---------

Co-authored-by: Sojan Jose <sojan@pepalo.com>
2025-05-22 10:08:19 +05:30
dde08b0bd9 feat: Add support for search_conversations in copilot (#11520)
Earlier, we were manually checking if a user was an agent and filtering
their conversations based on inboxes. This logic should have been part
of the conversation permissions service.

This PR moves the check to the right place and updates the logic
accordingly.

Other updates:
- Add support for search_conversations service for copilot.
- Use PermissionFilterService in contacts/conversations, conversations,
copilot search_conversations.

---------

Co-authored-by: Sojan <sojan@pepalo.com>
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2025-05-22 10:08:19 +05:30
Sivin VargheseandShivam Mishra 5a4201ae58 chore: Display Agent Bot token after creation (#11488)
This PR includes:

- Displaying the Agent Bot token after creation
- Updating the avatar icon when an avatar image is not present

Fixes: https://linear.app/chatwoot/issue/CW-4337/agent-bot-token-not-visible
2025-05-22 10:08:19 +05:30
Sivin VargheseandShivam Mishra fc8fe9a8d1 feat: Prevent saving preferences and status when impersonating (#11164)
This PR will prevent saving user preferences and online status when impersonating. Previously, these settings could be updated during impersonation, causing the user to see a different view or UI settings.

Fixes https://linear.app/chatwoot/issue/CW-4163/impersonation-improvements
2025-05-22 10:08:19 +05:30
Muhsin KelothandShivam Mishra 2fe0761fd5 fix: Display message content for CSAT messages in non-widget inboxes (#11528)
We made so many improvements for CSAT via https://github.com/chatwoot/chatwoot/pull/11485. However, we missed showing message content in the dashboard for CSAT URLs created in non-widget inboxes. This PR fixes the issue by ensuring that CSAT-configured messages are passed along with CSAT responses, otherwise defaulting to the translation.
2025-05-22 10:08:19 +05:30
Sivin VargheseandShivam Mishra 9dd2d14655 fix: Status not updating when creating a Linear issue (#11523) 2025-05-22 10:08:19 +05:30
3491 changed files with 32419 additions and 222762 deletions
+25 -17
View File
@@ -1,7 +1,6 @@
version: 2.1 version: 2.1
orbs: orbs:
node: circleci/node@6.1.0 node: circleci/node@6.1.0
qlty-orb: qltysh/qlty-orb@0.0
defaults: &defaults defaults: &defaults
working_directory: ~/build working_directory: ~/build
@@ -74,15 +73,15 @@ jobs:
libvips libvips
- run: - run:
name: Install RVM and Ruby 3.4.4 name: Install RVM and Ruby 3.3.3
command: | command: |
sudo apt-get install -y gpg sudo apt-get install -y gpg
gpg --keyserver hkp://keyserver.ubuntu.com --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB gpg --keyserver hkp://keyserver.ubuntu.com --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB
\curl -sSL https://get.rvm.io | bash -s stable \curl -sSL https://get.rvm.io | bash -s stable
echo 'source ~/.rvm/scripts/rvm' >> $BASH_ENV echo 'source ~/.rvm/scripts/rvm' >> $BASH_ENV
source ~/.rvm/scripts/rvm source ~/.rvm/scripts/rvm
rvm install "3.4.4" rvm install "3.3.3"
rvm use 3.4.4 --default rvm use 3.3.3 --default
gem install bundler -v 2.5.16 gem install bundler -v 2.5.16
- run: - run:
@@ -90,6 +89,14 @@ jobs:
command: | command: |
source ~/.rvm/scripts/rvm source ~/.rvm/scripts/rvm
bundle install bundle install
# pnpm install
- run:
name: Download cc-test-reporter
command: |
mkdir -p ~/tmp
curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ~/tmp/cc-test-reporter
chmod +x ~/tmp/cc-test-reporter
# Swagger verification # Swagger verification
- run: - run:
@@ -101,11 +108,10 @@ jobs:
echo "ERROR: The swagger.json file is not in sync with the yaml specification. Run 'rake swagger:build' and commit 'swagger/swagger.json'." echo "ERROR: The swagger.json file is not in sync with the yaml specification. Run 'rake swagger:build' and commit 'swagger/swagger.json'."
exit 1 exit 1
fi fi
mkdir -p ~/tmp
curl -L https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/6.3.0/openapi-generator-cli-6.3.0.jar > ~/tmp/openapi-generator-cli-6.3.0.jar curl -L https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/6.3.0/openapi-generator-cli-6.3.0.jar > ~/tmp/openapi-generator-cli-6.3.0.jar
java -jar ~/tmp/openapi-generator-cli-6.3.0.jar validate -i swagger/swagger.json java -jar ~/tmp/openapi-generator-cli-6.3.0.jar validate -i swagger/swagger.json
# Configure environment and database # we remove the FRONTED_URL from the .env before running the tests
- run: - run:
name: Database Setup and Configure Environment Variables name: Database Setup and Configure Environment Variables
command: | command: |
@@ -143,11 +149,17 @@ jobs:
command: pnpm run eslint command: pnpm run eslint
- run: - run:
name: Run frontend tests (with coverage) name: Run frontend tests
command: | command: |
mkdir -p ~/build/coverage/frontend mkdir -p ~/build/coverage/frontend
~/tmp/cc-test-reporter before-build
pnpm run test:coverage pnpm run test:coverage
- run:
name: Code Climate Test Coverage (Frontend)
command: |
~/tmp/cc-test-reporter format-coverage -t lcov -o "~/build/coverage/frontend/codeclimate.frontend_$CIRCLE_NODE_INDEX.json"
# Run backend tests # Run backend tests
- run: - run:
name: Run backend tests name: Run backend tests
@@ -155,18 +167,18 @@ jobs:
mkdir -p ~/tmp/test-results/rspec mkdir -p ~/tmp/test-results/rspec
mkdir -p ~/tmp/test-artifacts mkdir -p ~/tmp/test-artifacts
mkdir -p ~/build/coverage/backend mkdir -p ~/build/coverage/backend
~/tmp/cc-test-reporter before-build
TESTFILES=$(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings) TESTFILES=$(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings)
bundle exec rspec -I ./spec --require coverage_helper --require spec_helper --format progress \ bundle exec rspec --format progress \
--format RspecJunitFormatter \ --format RspecJunitFormatter \
--out ~/tmp/test-results/rspec.xml \ --out ~/tmp/test-results/rspec.xml \
-- ${TESTFILES} -- ${TESTFILES}
no_output_timeout: 30m no_output_timeout: 30m
# Qlty coverage publish - run:
- qlty-orb/coverage_publish: name: Code Climate Test Coverage (Backend)
files: | command: |
coverage/coverage.json ~/tmp/cc-test-reporter format-coverage -t simplecov -o "~/build/coverage/backend/codeclimate.$CIRCLE_NODE_INDEX.json"
coverage/lcov.info
- run: - run:
name: List coverage directory contents name: List coverage directory contents
@@ -177,7 +189,3 @@ jobs:
root: ~/build root: ~/build
paths: paths:
- coverage - coverage
- store_artifacts:
path: coverage
destination: coverage
+62
View File
@@ -0,0 +1,62 @@
version: '2'
plugins:
rubocop:
enabled: false
channel: rubocop-0-73
eslint:
enabled: false
csslint:
enabled: true
scss-lint:
enabled: true
brakeman:
enabled: false
checks:
similar-code:
enabled: false
method-count:
enabled: true
config:
threshold: 32
file-lines:
enabled: true
config:
threshold: 300
method-lines:
config:
threshold: 50
exclude_patterns:
- 'spec/'
- '**/specs/**/**'
- '**/spec/**/**'
- 'db/*'
- 'bin/**/*'
- 'db/**/*'
- 'config/**/*'
- 'public/**/*'
- 'vendor/**/*'
- 'node_modules/**/*'
- 'lib/tasks/auto_annotate_models.rake'
- 'app/test-matchers.js'
- 'docs/*'
- '**/*.md'
- '**/*.yml'
- 'app/javascript/dashboard/i18n/locale'
- '**/*.stories.js'
- 'stories/'
- 'app/javascript/dashboard/components/widgets/conversation/advancedFilterItems/index.js'
- 'app/javascript/shared/constants/countries.js'
- 'app/javascript/dashboard/components/widgets/conversation/advancedFilterItems/languages.js'
- 'app/javascript/dashboard/routes/dashboard/contacts/contactFilterItems/index.js'
- 'app/javascript/dashboard/routes/dashboard/settings/automation/constants.js'
- 'app/javascript/dashboard/components/widgets/FilterInput/FilterOperatorTypes.js'
- 'app/javascript/dashboard/routes/dashboard/settings/reports/constants.js'
- 'app/javascript/dashboard/store/captain/storeFactory.js'
- 'app/javascript/dashboard/i18n/index.js'
- 'app/javascript/widget/i18n/index.js'
- 'app/javascript/survey/i18n/index.js'
- 'app/javascript/shared/constants/locales.js'
- 'app/javascript/dashboard/helper/specs/macrosFixtures.js'
- 'app/javascript/dashboard/routes/dashboard/settings/macros/constants.js'
- '**/fixtures/**'
- '**/*/fixtures.js'
+1 -11
View File
@@ -4,15 +4,5 @@ FROM ghcr.io/chatwoot/chatwoot_codespace:latest
# Do the set up required for chatwoot app # Do the set up required for chatwoot app
WORKDIR /workspace WORKDIR /workspace
# Copy dependency files first for better caching
COPY package.json pnpm-lock.yaml ./
COPY Gemfile Gemfile.lock ./
# Install dependencies (will be cached if files don't change)
RUN pnpm install --frozen-lockfile && \
gem install bundler && \
bundle install --jobs=$(nproc)
# Copy source code after dependencies are installed
COPY . /workspace COPY . /workspace
RUN yarn && gem install bundler && bundle install
+42 -65
View File
@@ -1,16 +1,12 @@
ARG VARIANT="ubuntu-22.04"
ARG VARIANT
FROM mcr.microsoft.com/vscode/devcontainers/base:0-${VARIANT} FROM mcr.microsoft.com/vscode/devcontainers/base:0-${VARIANT}
ENV DEBIAN_FRONTEND=noninteractive
ARG NODE_VERSION ARG NODE_VERSION
ARG RUBY_VERSION ARG RUBY_VERSION
ARG USER_UID ARG USER_UID
ARG USER_GID ARG USER_GID
ARG PNPM_VERSION="10.2.0"
ENV PNPM_VERSION ${PNPM_VERSION}
ENV RUBY_CONFIGURE_OPTS=--disable-install-doc
# Update args in docker-compose.yaml to set the UID/GID of the "vscode" user. # Update args in docker-compose.yaml to set the UID/GID of the "vscode" user.
RUN if [ "$USER_GID" != "1000" ] || [ "$USER_UID" != "1000" ]; then \ RUN if [ "$USER_GID" != "1000" ] || [ "$USER_UID" != "1000" ]; then \
@@ -19,80 +15,61 @@ RUN if [ "$USER_GID" != "1000" ] || [ "$USER_UID" != "1000" ]; then \
&& chmod -R $USER_UID:$USER_GID /home/vscode; \ && chmod -R $USER_UID:$USER_GID /home/vscode; \
fi fi
RUN NODE_MAJOR=$(echo $NODE_VERSION | cut -d. -f1) \ RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
&& curl -fsSL https://deb.nodesource.com/setup_${NODE_MAJOR}.x | bash - \ && apt-get -y install --no-install-recommends \
&& apt-get update \ build-essential \
&& apt-get -y install --no-install-recommends \ libssl-dev \
build-essential \ zlib1g-dev \
libssl-dev \ gnupg2 \
zlib1g-dev \ tar \
gnupg \ tzdata \
tar \ postgresql-client \
tzdata \ libpq-dev \
postgresql-client \ yarn \
libpq-dev \ git \
git \ imagemagick \
imagemagick \ tmux \
libyaml-dev \ zsh \
curl \ git-flow \
ca-certificates \ npm \
tmux \ libyaml-dev
nodejs \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
# Install rbenv and ruby for root user first # Install rbenv and ruby
RUN git clone --depth 1 https://github.com/rbenv/rbenv.git ~/.rbenv \ RUN git clone https://github.com/rbenv/rbenv.git ~/.rbenv \
&& echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc \ && echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc \
&& echo 'eval "$(rbenv init -)"' >> ~/.bashrc && echo 'eval "$(rbenv init -)"' >> ~/.bashrc
ENV PATH "/root/.rbenv/bin/:/root/.rbenv/shims/:$PATH" ENV PATH "/root/.rbenv/bin/:/root/.rbenv/shims/:$PATH"
RUN git clone --depth 1 https://github.com/rbenv/ruby-build.git && \ RUN git clone https://github.com/rbenv/ruby-build.git && \
PREFIX=/usr/local ./ruby-build/install.sh PREFIX=/usr/local ./ruby-build/install.sh
RUN rbenv install $RUBY_VERSION && \ RUN rbenv install $RUBY_VERSION && \
rbenv global $RUBY_VERSION && \ rbenv global $RUBY_VERSION && \
rbenv versions rbenv versions
# Set up rbenv for vscode user # Install overmind
RUN su - vscode -c "git clone --depth 1 https://github.com/rbenv/rbenv.git ~/.rbenv" \
&& su - vscode -c "echo 'export PATH=\"\$HOME/.rbenv/bin:\$PATH\"' >> ~/.bashrc" \
&& su - vscode -c "echo 'eval \"\$(rbenv init -)\"' >> ~/.bashrc" \
&& su - vscode -c "PATH=\"/home/vscode/.rbenv/bin:\$PATH\" rbenv install $RUBY_VERSION" \
&& su - vscode -c "PATH=\"/home/vscode/.rbenv/bin:\$PATH\" rbenv global $RUBY_VERSION"
# Install overmind and gh in single layer
RUN curl -L https://github.com/DarthSim/overmind/releases/download/v2.1.0/overmind-v2.1.0-linux-amd64.gz > overmind.gz \ RUN curl -L https://github.com/DarthSim/overmind/releases/download/v2.1.0/overmind-v2.1.0-linux-amd64.gz > overmind.gz \
&& gunzip overmind.gz \ && gunzip overmind.gz \
&& mv overmind /usr/local/bin \ && sudo mv overmind /usr/local/bin \
&& chmod +x /usr/local/bin/overmind \ && chmod +x /usr/local/bin/overmind
&& curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg \
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | tee /etc/apt/sources.list.d/github-cli.list > /dev/null \
&& apt-get update \ # Install gh
&& apt-get install -y --no-install-recommends gh \ RUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg \
&& apt-get clean \ && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* && sudo apt update \
&& sudo apt install gh
# Do the set up required for chatwoot app # Do the set up required for chatwoot app
WORKDIR /workspace WORKDIR /workspace
RUN chown vscode:vscode /workspace COPY . /workspace
# set up node js, pnpm and claude code in single layer # set up ruby
RUN npm install -g pnpm@${PNPM_VERSION} @anthropic-ai/claude-code \ COPY Gemfile Gemfile.lock ./
&& npm cache clean --force RUN gem install bundler && bundle install
# Switch to vscode user # set up node js
USER vscode RUN npm install n -g && \
ENV PATH="/home/vscode/.rbenv/bin:/home/vscode/.rbenv/shims:$PATH" n $NODE_VERSION
RUN npm install --global yarn
# Copy dependency files first for better caching RUN yarn
COPY --chown=vscode:vscode Gemfile Gemfile.lock package.json pnpm-lock.yaml ./
# Install dependencies as vscode user
RUN eval "$(rbenv init -)" \
&& gem install bundler -N \
&& bundle install --jobs=$(nproc) \
&& pnpm install --frozen-lockfile
# Copy source code after dependencies are installed
COPY --chown=vscode:vscode . /workspace
+8 -17
View File
@@ -4,26 +4,17 @@
"dockerComposeFile": "docker-compose.yml", "dockerComposeFile": "docker-compose.yml",
"settings": { "settings": {
"terminal.integrated.shell.linux": "/bin/zsh", "terminal.integrated.shell.linux": "/bin/zsh"
"extensions.showRecommendationsOnlyOnDemand": true,
"editor.formatOnSave": true,
"files.trimTrailingWhitespace": true,
"files.insertFinalNewline": true,
"search.exclude": {
"**/node_modules": true,
"**/tmp": true,
"**/log": true,
"**/coverage": true,
"**/public/packs": true
}
}, },
// Add the IDs of extensions you want installed when the container is created. // Add the IDs of extensions you want installed when the container is created.
"extensions": [ "extensions": [
"Shopify.ruby-lsp", "rebornix.Ruby",
"misogi.ruby-rubocop", "misogi.ruby-rubocop",
"wingrunr21.vscode-ruby",
"davidpallinder.rails-test-runner", "davidpallinder.rails-test-runner",
"eamodio.gitlens",
"github.copilot", "github.copilot",
"mrmlnc.vscode-duplicate" "mrmlnc.vscode-duplicate"
], ],
@@ -32,15 +23,15 @@
// 5432 postgres // 5432 postgres
// 6379 redis // 6379 redis
// 1025,8025 mailhog // 1025,8025 mailhog
"forwardPorts": [8025, 3000, 3036], "forwardPorts": [8025, 3000, 3035],
"postCreateCommand": ".devcontainer/scripts/setup.sh && POSTGRES_STATEMENT_TIMEOUT=600s bundle exec rake db:chatwoot_prepare && pnpm install", "postCreateCommand": ".devcontainer/scripts/setup.sh && POSTGRES_STATEMENT_TIMEOUT=600s bundle exec rake db:chatwoot_prepare && yarn",
"portsAttributes": { "portsAttributes": {
"3000": { "3000": {
"label": "Rails Server" "label": "Rails Server"
}, },
"3036": { "3035": {
"label": "Vite Dev Server" "label": "Webpack Dev Server"
}, },
"8025": { "8025": {
"label": "Mailhog UI" "label": "Mailhog UI"
-18
View File
@@ -1,18 +0,0 @@
# Docker Compose file for building the base image in GitHub Actions
# Usage: docker-compose -f .devcontainer/docker-compose.base.yml build base
version: '3'
services:
base:
build:
context: ..
dockerfile: .devcontainer/Dockerfile.base
args:
VARIANT: 'ubuntu-22.04'
NODE_VERSION: '23.7.0'
RUBY_VERSION: '3.4.4'
# On Linux, you may need to update USER_UID and USER_GID below if not your local UID is not 1000.
USER_UID: '1000'
USER_GID: '1000'
image: ghcr.io/chatwoot/chatwoot_codespace:latest
+14 -1
View File
@@ -5,6 +5,19 @@
version: '3' version: '3'
services: services:
base:
build:
context: ..
dockerfile: .devcontainer/Dockerfile.base
args:
VARIANT: 'ubuntu-22.04'
NODE_VERSION: '23.7.0'
RUBY_VERSION: '3.3.3'
# On Linux, you may need to update USER_UID and USER_GID below if not your local UID is not 1000.
USER_UID: '1000'
USER_GID: '1000'
image: base:latest
app: app:
build: build:
context: .. context: ..
@@ -12,7 +25,7 @@ services:
args: args:
VARIANT: 'ubuntu-22.04' VARIANT: 'ubuntu-22.04'
NODE_VERSION: '23.7.0' NODE_VERSION: '23.7.0'
RUBY_VERSION: '3.4.4' RUBY_VERSION: '3.3.3'
# On Linux, you may need to update USER_UID and USER_GID below if not your local UID is not 1000. # On Linux, you may need to update USER_UID and USER_GID below if not your local UID is not 1000.
USER_UID: '1000' USER_UID: '1000'
USER_GID: '1000' USER_GID: '1000'
+7 -10
View File
@@ -2,15 +2,12 @@ cp .env.example .env
sed -i -e '/REDIS_URL/ s/=.*/=redis:\/\/localhost:6379/' .env sed -i -e '/REDIS_URL/ s/=.*/=redis:\/\/localhost:6379/' .env
sed -i -e '/POSTGRES_HOST/ s/=.*/=localhost/' .env sed -i -e '/POSTGRES_HOST/ s/=.*/=localhost/' .env
sed -i -e '/SMTP_ADDRESS/ s/=.*/=localhost/' .env sed -i -e '/SMTP_ADDRESS/ s/=.*/=localhost/' .env
sed -i -e "/FRONTEND_URL/ s/=.*/=https:\/\/$CODESPACE_NAME-3000.app.github.dev/" .env sed -i -e "/FRONTEND_URL/ s/=.*/=https:\/\/$CODESPACE_NAME-3000.githubpreview.dev/" .env
sed -i -e "/WEBPACKER_DEV_SERVER_PUBLIC/ s/=.*/=https:\/\/$CODESPACE_NAME-3035.githubpreview.dev/" .env
# Setup Claude Code API key if available # uncomment the webpacker env variable
if [ -n "$CLAUDE_CODE_API_KEY" ]; then sed -i -e '/WEBPACKER_DEV_SERVER_PUBLIC/s/^# //' .env
mkdir -p ~/.claude # fix the error with webpacker
echo '{"apiKeyHelper": "~/.claude/anthropic_key.sh"}' > ~/.claude/settings.json echo 'export NODE_OPTIONS=--openssl-legacy-provider' >> ~/.zshrc
echo "echo \"$CLAUDE_CODE_API_KEY\"" > ~/.claude/anthropic_key.sh
chmod +x ~/.claude/anthropic_key.sh
fi
# codespaces make the ports public # codespaces make the ports public
gh codespace ports visibility 3000:public 3036:public 8025:public -c $CODESPACE_NAME gh codespace ports visibility 3000:public 3035:public 8025:public -c $CODESPACE_NAME
-7
View File
@@ -6,13 +6,6 @@
# Use `rake secret` to generate this variable # Use `rake secret` to generate this variable
SECRET_KEY_BASE=replace_with_lengthy_secure_hex SECRET_KEY_BASE=replace_with_lengthy_secure_hex
# Active Record Encryption keys (required for MFA/2FA functionality)
# Generate these keys by running: rails db:encryption:init
# IMPORTANT: Use different keys for each environment (development, staging, production)
# ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY=
# ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY=
# ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT=
# Replace with the URL you are planning to use for your app # Replace with the URL you are planning to use for your app
FRONTEND_URL=http://0.0.0.0:3000 FRONTEND_URL=http://0.0.0.0:3000
# To use a dedicated URL for help center pages # To use a dedicated URL for help center pages
-1
View File
@@ -103,7 +103,6 @@ module.exports = {
'⌘', '⌘',
'📄', '📄',
'🎉', '🎉',
'🚀',
'💬', '💬',
'👥', '👥',
'📥', '📥',
-28
View File
@@ -1,28 +0,0 @@
name: Auto-assign PR to Author
on:
pull_request:
types: [opened]
jobs:
auto-assign:
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- name: Auto-assign PR to author
uses: actions/github-script@v7
with:
script: |
const { owner, repo } = context.repo;
const pull_number = context.payload.pull_request.number;
const author = context.payload.pull_request.user.login;
await github.rest.issues.addAssignees({
owner,
repo,
issue_number: pull_number,
assignees: [author]
});
console.log(`Assigned PR #${pull_number} to ${author}`);
-5
View File
@@ -6,11 +6,6 @@ name: Deploy Check
on: on:
pull_request: pull_request:
# If two pushes happen within a short time in the same PR, cancel the run of the oldest push
concurrency:
group: pr-${{ github.workflow }}-${{ github.head_ref }}
cancel-in-progress: true
jobs: jobs:
deployment_check: deployment_check:
name: Check Deployment name: Check Deployment
@@ -5,11 +5,6 @@ on:
branches: branches:
- develop - develop
# If two pushes happen within a short time in the same PR, cancel the run of the oldest push
concurrency:
group: pr-${{ github.workflow }}-${{ github.head_ref }}
cancel-in-progress: true
jobs: jobs:
log_lines_check: log_lines_check:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -19,5 +19,6 @@ jobs:
- name: Build the Codespace Base Image - name: Build the Codespace Base Image
run: | run: |
docker compose -f .devcontainer/docker-compose.base.yml build base docker-compose -f .devcontainer/docker-compose.yml build base
docker tag base:latest ghcr.io/chatwoot/chatwoot_codespace:latest
docker push ghcr.io/chatwoot/chatwoot_codespace:latest docker push ghcr.io/chatwoot/chatwoot_codespace:latest
-100
View File
@@ -1,100 +0,0 @@
name: Run MFA Tests
permissions:
contents: read
on:
pull_request:
# If two pushes happen within a short time in the same PR, cancel the run of the oldest push
concurrency:
group: pr-${{ github.workflow }}-${{ github.head_ref }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-22.04
# Only run if MFA test keys are available
if: github.event_name == 'workflow_dispatch' || (github.repository == 'chatwoot/chatwoot' && github.actor != 'dependabot[bot]')
services:
postgres:
image: pgvector/pgvector:pg15
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: ''
POSTGRES_DB: postgres
POSTGRES_HOST_AUTH_METHOD: trust
ports:
- 5432:5432
options: >-
--mount type=tmpfs,destination=/var/lib/postgresql/data
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis
ports:
- 6379:6379
options: --entrypoint redis-server
env:
RAILS_ENV: test
POSTGRES_HOST: localhost
# Active Record encryption keys required for MFA - test keys only, not for production use
ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY: 'test_key_a6cde8f7b9c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7'
ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY: 'test_key_b7def9a8c0d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d8'
ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT: 'test_salt_c8efa0b9d1e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d9'
steps:
- uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- name: Create database
run: bundle exec rake db:create
- name: Install pgvector extension
run: |
PGPASSWORD="" psql -h localhost -U postgres -d chatwoot_test -c "CREATE EXTENSION IF NOT EXISTS vector;"
- name: Seed database
run: bundle exec rake db:schema:load
- name: Run MFA-related backend tests
run: |
bundle exec rspec \
spec/services/mfa/token_service_spec.rb \
spec/services/mfa/authentication_service_spec.rb \
spec/requests/api/v1/profile/mfa_controller_spec.rb \
spec/controllers/devise_overrides/sessions_controller_spec.rb \
spec/models/application_record_external_credentials_encryption_spec.rb \
--profile=10 \
--format documentation
env:
NODE_OPTIONS: --openssl-legacy-provider
- name: Run MFA-related tests in user_spec
run: |
# Run specific MFA-related tests from user_spec
bundle exec rspec spec/models/user_spec.rb \
-e "two factor" \
-e "2FA" \
-e "MFA" \
-e "otp" \
-e "backup code" \
--profile=10 \
--format documentation
env:
NODE_OPTIONS: --openssl-legacy-provider
- name: Upload test logs
uses: actions/upload-artifact@v4
if: failure()
with:
name: mfa-test-logs
path: |
log/test.log
tmp/screenshots/
-5
View File
@@ -5,11 +5,6 @@ on:
branches: branches:
- develop - develop
# If two pushes happen within a short time in the same PR, cancel the run of the oldest push
concurrency:
group: pr-${{ github.workflow }}-${{ github.head_ref }}
cancel-in-progress: true
jobs: jobs:
test: test:
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
-5
View File
@@ -94,8 +94,3 @@ yarn-debug.log*
.vscode .vscode
.claude/settings.local.json .claude/settings.local.json
.cursor .cursor
CLAUDE.local.md
# Histoire deployment
.netlify
.histoire
-7
View File
@@ -1,7 +0,0 @@
*
!configs
!configs/**
!hooks
!hooks/**
!qlty.toml
!.gitignore
-2
View File
@@ -1,2 +0,0 @@
ignored:
- DL3008
-1
View File
@@ -1 +0,0 @@
source-path=SCRIPTDIR
-8
View File
@@ -1,8 +0,0 @@
rules:
document-start: disable
quoted-strings:
required: only-when-needed
extra-allowed: ["{|}"]
key-duplicates: {}
octal-values:
forbid-implicit-octal: true
-84
View File
@@ -1,84 +0,0 @@
# This file was automatically generated by `qlty init`.
# You can modify it to suit your needs.
# We recommend you to commit this file to your repository.
#
# This configuration is used by both Qlty CLI and Qlty Cloud.
#
# Qlty CLI -- Code quality toolkit for developers
# Qlty Cloud -- Fully automated Code Health Platform
#
# Try Qlty Cloud: https://qlty.sh
#
# For a guide to configuration, visit https://qlty.sh/d/config
# Or for a full reference, visit https://qlty.sh/d/qlty-toml
config_version = "0"
exclude_patterns = [
"*_min.*",
"*-min.*",
"*.min.*",
"**/.yarn/**",
"**/*.d.ts",
"**/assets/**",
"**/bower_components/**",
"**/build/**",
"**/cache/**",
"**/config/**",
"**/db/**",
"**/deps/**",
"**/dist/**",
"**/extern/**",
"**/external/**",
"**/generated/**",
"**/Godeps/**",
"**/gradlew/**",
"**/mvnw/**",
"**/node_modules/**",
"**/protos/**",
"**/seed/**",
"**/target/**",
"**/templates/**",
"**/testdata/**",
"**/vendor/**", "spec/", "**/specs/**/**", "**/spec/**/**", "db/*", "bin/**/*", "db/**/*", "config/**/*", "public/**/*", "vendor/**/*", "node_modules/**/*", "lib/tasks/auto_annotate_models.rake", "app/test-matchers.js", "docs/*", "**/*.md", "**/*.yml", "app/javascript/dashboard/i18n/locale", "**/*.stories.js", "stories/", "app/javascript/dashboard/components/widgets/conversation/advancedFilterItems/index.js", "app/javascript/shared/constants/countries.js", "app/javascript/dashboard/components/widgets/conversation/advancedFilterItems/languages.js", "app/javascript/dashboard/routes/dashboard/contacts/contactFilterItems/index.js", "app/javascript/dashboard/routes/dashboard/settings/automation/constants.js", "app/javascript/dashboard/components/widgets/FilterInput/FilterOperatorTypes.js", "app/javascript/dashboard/routes/dashboard/settings/reports/constants.js", "app/javascript/dashboard/store/captain/storeFactory.js", "app/javascript/dashboard/i18n/index.js", "app/javascript/widget/i18n/index.js", "app/javascript/survey/i18n/index.js", "app/javascript/shared/constants/locales.js", "app/javascript/dashboard/helper/specs/macrosFixtures.js", "app/javascript/dashboard/routes/dashboard/settings/macros/constants.js", "**/fixtures/**", "**/*/fixtures.js",
]
test_patterns = [
"**/test/**",
"**/spec/**",
"**/*.test.*",
"**/*.spec.*",
"**/*_test.*",
"**/*_spec.*",
"**/test_*.*",
"**/spec_*.*",
]
[smells]
mode = "comment"
[smells.boolean_logic]
threshold = 4
[smells.file_complexity]
threshold = 66
enabled = true
[smells.return_statements]
threshold = 4
[smells.nested_control_flow]
threshold = 4
[smells.function_parameters]
threshold = 4
[smells.function_complexity]
threshold = 5
[smells.duplication]
enabled = true
threshold = 20
[[source]]
name = "default"
default = true
+9 -180
View File
@@ -1,10 +1,7 @@
plugins: require:
- rubocop-performance - rubocop-performance
- rubocop-rails - rubocop-rails
- rubocop-rspec - rubocop-rspec
- rubocop-factory_bot
require:
- ./rubocop/use_from_email.rb - ./rubocop/use_from_email.rb
- ./rubocop/custom_cop_location.rb - ./rubocop/custom_cop_location.rb
@@ -16,61 +13,44 @@ Metrics/ClassLength:
Exclude: Exclude:
- 'app/models/message.rb' - 'app/models/message.rb'
- 'app/models/conversation.rb' - 'app/models/conversation.rb'
Metrics/MethodLength: Metrics/MethodLength:
Max: 19 Max: 19
Exclude:
- 'enterprise/lib/captain/agent.rb'
RSpec/ExampleLength: RSpec/ExampleLength:
Max: 50 Max: 25
Style/Documentation: Style/Documentation:
Enabled: false Enabled: false
Style/ExponentialNotation: Style/ExponentialNotation:
Enabled: false Enabled: false
Style/FrozenStringLiteralComment: Style/FrozenStringLiteralComment:
Enabled: false Enabled: false
Style/SymbolArray: Style/SymbolArray:
Enabled: false Enabled: false
Style/OpenStructUse: Style/OpenStructUse:
Enabled: false Enabled: false
Style/OptionalBooleanParameter: Style/OptionalBooleanParameter:
Exclude: Exclude:
- 'app/services/email_templates/db_resolver_service.rb' - 'app/services/email_templates/db_resolver_service.rb'
- 'app/dispatchers/dispatcher.rb' - 'app/dispatchers/dispatcher.rb'
Style/GlobalVars: Style/GlobalVars:
Exclude: Exclude:
- 'config/initializers/01_redis.rb' - 'config/initializers/01_redis.rb'
- 'config/initializers/rack_attack.rb' - 'config/initializers/rack_attack.rb'
- 'lib/redis/alfred.rb' - 'lib/redis/alfred.rb'
- 'lib/global_config.rb' - 'lib/global_config.rb'
Style/ClassVars: Style/ClassVars:
Exclude: Exclude:
- 'app/services/email_templates/db_resolver_service.rb' - 'app/services/email_templates/db_resolver_service.rb'
Lint/MissingSuper: Lint/MissingSuper:
Exclude: Exclude:
- 'app/drops/base_drop.rb' - 'app/drops/base_drop.rb'
Lint/SymbolConversion: Lint/SymbolConversion:
Enabled: false Enabled: false
Lint/EmptyBlock: Lint/EmptyBlock:
Exclude: Exclude:
- 'app/views/api/v1/accounts/conversations/toggle_status.json.jbuilder' - 'app/views/api/v1/accounts/conversations/toggle_status.json.jbuilder'
Lint/OrAssignmentToConstant: Lint/OrAssignmentToConstant:
Exclude: Exclude:
- 'lib/redis/config.rb' - 'lib/redis/config.rb'
Metrics/BlockLength: Metrics/BlockLength:
Max: 30 Max: 30
Exclude: Exclude:
@@ -78,16 +58,10 @@ Metrics/BlockLength:
- '**/routes.rb' - '**/routes.rb'
- 'config/environments/*' - 'config/environments/*'
- db/schema.rb - db/schema.rb
Metrics/ModuleLength: Metrics/ModuleLength:
Exclude: Exclude:
- lib/seeders/message_seeder.rb - lib/seeders/message_seeder.rb
- spec/support/slack_stubs.rb - spec/support/slack_stubs.rb
Rails/HelperInstanceVariable:
Exclude:
- enterprise/app/helpers/captain/chat_helper.rb
Rails/ApplicationController: Rails/ApplicationController:
Exclude: Exclude:
- 'app/controllers/api/v1/widget/messages_controller.rb' - 'app/controllers/api/v1/widget/messages_controller.rb'
@@ -97,101 +71,74 @@ Rails/ApplicationController:
- 'app/controllers/platform_controller.rb' - 'app/controllers/platform_controller.rb'
- 'app/controllers/public_controller.rb' - 'app/controllers/public_controller.rb'
- 'app/controllers/survey/responses_controller.rb' - 'app/controllers/survey/responses_controller.rb'
Rails/FindEach: Rails/FindEach:
Enabled: true Enabled: true
Include: Include:
- 'app/**/*.rb' - 'app/**/*.rb'
Rails/CompactBlank: Rails/CompactBlank:
Enabled: false Enabled: false
Rails/EnvironmentVariableAccess: Rails/EnvironmentVariableAccess:
Enabled: false Enabled: false
Rails/TimeZoneAssignment: Rails/TimeZoneAssignment:
Enabled: false Enabled: false
Rails/RedundantPresenceValidationOnBelongsTo: Rails/RedundantPresenceValidationOnBelongsTo:
Enabled: false Enabled: false
Rails/InverseOf:
Exclude:
- enterprise/app/models/captain/assistant.rb
Rails/UniqueValidationWithoutIndex:
Exclude:
- app/models/canned_response.rb
- app/models/telegram_bot.rb
- enterprise/app/models/captain_inbox.rb
- 'app/models/channel/twitter_profile.rb'
- 'app/models/webhook.rb'
- 'app/models/contact.rb'
Style/ClassAndModuleChildren: Style/ClassAndModuleChildren:
EnforcedStyle: compact EnforcedStyle: compact
Exclude: Exclude:
- 'config/application.rb' - 'config/application.rb'
- 'config/initializers/monkey_patches/*' - 'config/initializers/monkey_patches/*'
Style/MapToHash: Style/MapToHash:
Enabled: false Enabled: false
Style/HashSyntax: Style/HashSyntax:
Enabled: true Enabled: true
EnforcedStyle: no_mixed_keys EnforcedStyle: no_mixed_keys
EnforcedShorthandSyntax: never EnforcedShorthandSyntax: never
RSpec/NestedGroups: RSpec/NestedGroups:
Enabled: true Enabled: true
Max: 4 Max: 4
RSpec/MessageSpies: RSpec/MessageSpies:
Enabled: false Enabled: false
RSpec/StubbedMock: RSpec/StubbedMock:
Enabled: false Enabled: false
RSpec/FactoryBot/SyntaxMethods:
Enabled: false
Naming/VariableNumber: Naming/VariableNumber:
Enabled: false Enabled: false
Naming/MemoizedInstanceVariableName: Naming/MemoizedInstanceVariableName:
Exclude: Exclude:
- 'app/models/message.rb' - 'app/models/message.rb'
Style/GuardClause: Style/GuardClause:
Exclude: Exclude:
- 'app/builders/account_builder.rb' - 'app/builders/account_builder.rb'
- 'app/models/attachment.rb' - 'app/models/attachment.rb'
- 'app/models/message.rb' - 'app/models/message.rb'
Metrics/AbcSize: Metrics/AbcSize:
Max: 26 Max: 26
Exclude: Exclude:
- 'app/controllers/concerns/auth_helper.rb' - 'app/controllers/concerns/auth_helper.rb'
Rails/UniqueValidationWithoutIndex:
Exclude:
- 'app/models/channel/twitter_profile.rb'
- 'app/models/webhook.rb'
- 'app/models/contact.rb'
- 'app/models/integrations/hook.rb' - 'app/models/integrations/hook.rb'
- 'app/models/canned_response.rb' - 'app/models/canned_response.rb'
- 'app/models/telegram_bot.rb' - 'app/models/telegram_bot.rb'
Rails/RenderInline: Rails/RenderInline:
Exclude: Exclude:
- 'app/controllers/swagger_controller.rb' - 'app/controllers/swagger_controller.rb'
Rails/ThreeStateBooleanColumn: Rails/ThreeStateBooleanColumn:
Exclude: Exclude:
- 'db/migrate/20230503101201_create_sla_policies.rb' - 'db/migrate/20230503101201_create_sla_policies.rb'
RSpec/IndexedLet: RSpec/IndexedLet:
Enabled: false Enabled: false
RSpec/NamedSubject: RSpec/NamedSubject:
Enabled: false Enabled: false
# we should bring this down # we should bring this down
RSpec/MultipleExpectations: RSpec/MultipleExpectations:
Max: 7 Max: 7
RSpec/MultipleMemoizedHelpers: RSpec/MultipleMemoizedHelpers:
Max: 14 Max: 14
@@ -219,121 +166,3 @@ AllCops:
- 'tmp/**/*' - 'tmp/**/*'
- 'storage/**/*' - 'storage/**/*'
- 'db/migrate/20230426130150_init_schema.rb' - 'db/migrate/20230426130150_init_schema.rb'
FactoryBot/SyntaxMethods:
Enabled: false
# Disable new rules causing errors
Layout/LeadingCommentSpace:
Enabled: false
Style/ReturnNilInPredicateMethodDefinition:
Enabled: false
Style/RedundantParentheses:
Enabled: false
Performance/StringIdentifierArgument:
Enabled: false
Layout/EmptyLinesAroundExceptionHandlingKeywords:
Enabled: false
Lint/LiteralAsCondition:
Enabled: false
Style/RedundantReturn:
Enabled: false
Layout/SpaceAroundOperators:
Enabled: false
Rails/EnvLocal:
Enabled: false
Rails/WhereRange:
Enabled: false
Lint/UselessConstantScoping:
Enabled: false
Style/MultipleComparison:
Enabled: false
Bundler/OrderedGems:
Enabled: false
RSpec/ExampleWording:
Enabled: false
RSpec/ReceiveMessages:
Enabled: false
FactoryBot/AssociationStyle:
Enabled: false
Rails/EnumSyntax:
Enabled: false
Lint/RedundantTypeConversion:
Enabled: false
# Additional rules to disable
Rails/RedundantActiveRecordAllMethod:
Enabled: false
Layout/TrailingEmptyLines:
Enabled: true
Style/SafeNavigationChainLength:
Enabled: false
Lint/SafeNavigationConsistency:
Enabled: false
Lint/CopDirectiveSyntax:
Enabled: false
# Final set of rules to disable
FactoryBot/ExcessiveCreateList:
Enabled: false
RSpec/MissingExpectationTargetMethod:
Enabled: false
Performance/InefficientHashSearch:
Enabled: false
Style/RedundantSelfAssignmentBranch:
Enabled: false
Style/YAMLFileRead:
Enabled: false
Layout/ExtraSpacing:
Enabled: false
Style/RedundantFilterChain:
Enabled: false
Performance/MapMethodChain:
Enabled: false
Rails/RootPathnameMethods:
Enabled: false
Style/SuperArguments:
Enabled: false
# Final remaining rules to disable
Rails/Delegate:
Enabled: false
Style/CaseLikeIf:
Enabled: false
FactoryBot/RedundantFactoryOption:
Enabled: false
FactoryBot/FactoryAssociationWithStrategy:
Enabled: false
+1 -1
View File
@@ -1 +1 @@
3.4.4 3.3.3
+1 -18
View File
@@ -55,21 +55,4 @@
## Ruby Best Practices ## Ruby Best Practices
- Use compact `module/class` definitions; avoid nested styles - Use compact `module/class` definitions; avoid nested styles
## Enterprise Edition Notes
- Chatwoot has an Enterprise overlay under `enterprise/` that extends/overrides OSS code.
- When you add or modify core functionality, always check for corresponding files in `enterprise/` and keep behavior compatible.
- Follow the Enterprise development practices documented here:
- https://chatwoot.help/hc/handbook/articles/developing-enterprise-edition-features-38
Practical checklist for any change impacting core logic or public APIs
- Search for related files in both trees before editing (e.g., `rg -n "FooService|ControllerName|ModelName" app enterprise`).
- If adding new endpoints, services, or models, consider whether Enterprise needs:
- An override (e.g., `enterprise/app/...`), or
- An extension point (e.g., `prepend_mod_with`, hooks, configuration) to avoid hard forks.
- Avoid hardcoding instance- or plan-specific behavior in OSS; prefer configuration, feature flags, or extension points consumed by Enterprise.
- Keep request/response contracts stable across OSS and Enterprise; update both sets of routes/controllers when introducing new APIs.
- When renaming/moving shared code, mirror the change in `enterprise/` to prevent drift.
- Tests: Add Enterprise-specific specs under `spec/enterprise`, mirroring OSS spec layout where applicable.
+9 -27
View File
@@ -1,10 +1,10 @@
source 'https://rubygems.org' source 'https://rubygems.org'
ruby '3.4.4' ruby '3.3.3'
##-- base gems for rails --## ##-- base gems for rails --##
gem 'rack-cors', '2.0.0', require: 'rack/cors' gem 'rack-cors', '2.0.0', require: 'rack/cors'
gem 'rails', '~> 7.1' gem 'rails', '~> 7.0.8.4'
# Reduces boot times through caching; required in config/boot.rb # Reduces boot times through caching; required in config/boot.rb
gem 'bootsnap', require: false gem 'bootsnap', require: false
@@ -21,7 +21,6 @@ gem 'telephone_number'
gem 'time_diff' gem 'time_diff'
gem 'tzinfo-data' gem 'tzinfo-data'
gem 'valid_email2' gem 'valid_email2'
gem 'octokit'
# compress javascript config.assets.js_compressor # compress javascript config.assets.js_compressor
gem 'uglifier' gem 'uglifier'
##-- used for single column multiple binary flags in notification settings/feature flagging --## ##-- used for single column multiple binary flags in notification settings/feature flagging --##
@@ -34,8 +33,6 @@ gem 'liquid'
gem 'commonmarker' gem 'commonmarker'
# Validate Data against JSON Schema # Validate Data against JSON Schema
gem 'json_schemer' gem 'json_schemer'
# used in swagger build
gem 'json_refs'
# Rack middleware for blocking & throttling abusive requests # Rack middleware for blocking & throttling abusive requests
gem 'rack-attack', '>= 6.7.0' gem 'rack-attack', '>= 6.7.0'
# a utility tool for streaming, flexible and safe downloading of remote files # a utility tool for streaming, flexible and safe downloading of remote files
@@ -63,10 +60,6 @@ gem 'redis-namespace'
# super fast record imports in bulk # super fast record imports in bulk
gem 'activerecord-import' gem 'activerecord-import'
gem 'searchkick'
gem 'opensearch-ruby'
gem 'faraday_middleware-aws-sigv4'
##--- gems for server & infra configuration ---## ##--- gems for server & infra configuration ---##
gem 'dotenv-rails', '>= 3.0.0' gem 'dotenv-rails', '>= 3.0.0'
gem 'foreman' gem 'foreman'
@@ -79,12 +72,9 @@ gem 'barnes'
gem 'devise', '>= 4.9.4' gem 'devise', '>= 4.9.4'
gem 'devise-secure_password', git: 'https://github.com/chatwoot/devise-secure_password', branch: 'chatwoot' gem 'devise-secure_password', git: 'https://github.com/chatwoot/devise-secure_password', branch: 'chatwoot'
gem 'devise_token_auth', '>= 1.2.3' gem 'devise_token_auth', '>= 1.2.3'
# two-factor authentication
gem 'devise-two-factor', '>= 5.0.0'
# authorization # authorization
gem 'jwt' gem 'jwt'
gem 'pundit' gem 'pundit'
# super admin # super admin
gem 'administrate', '>= 0.20.1' gem 'administrate', '>= 0.20.1'
gem 'administrate-field-active_storage', '>= 1.0.3' gem 'administrate-field-active_storage', '>= 1.0.3'
@@ -97,14 +87,14 @@ gem 'wisper', '2.0.0'
##--- gems for channels ---## ##--- gems for channels ---##
gem 'facebook-messenger' gem 'facebook-messenger'
gem 'line-bot-api' gem 'line-bot-api'
gem 'twilio-ruby' gem 'twilio-ruby', '~> 5.66'
# twitty will handle subscription of twitter account events # twitty will handle subscription of twitter account events
# gem 'twitty', git: 'https://github.com/chatwoot/twitty' # gem 'twitty', git: 'https://github.com/chatwoot/twitty'
gem 'twitty', '~> 0.1.5' gem 'twitty', '~> 0.1.5'
# facebook client # facebook client
gem 'koala' gem 'koala'
# slack client # slack client
gem 'slack-ruby-client', '~> 2.7.0' gem 'slack-ruby-client', '~> 2.5.2'
# for dialogflow integrations # for dialogflow integrations
gem 'google-cloud-dialogflow-v2', '>= 0.24.0' gem 'google-cloud-dialogflow-v2', '>= 0.24.0'
gem 'grpc' gem 'grpc'
@@ -116,7 +106,7 @@ gem 'google-cloud-translate-v3', '>= 0.7.0'
##-- apm and error monitoring ---# ##-- apm and error monitoring ---#
# loaded only when environment variables are set. # loaded only when environment variables are set.
# ref application.rb # ref application.rb
gem 'datadog', '~> 2.0', require: false gem 'ddtrace', require: false
gem 'elastic-apm', require: false gem 'elastic-apm', require: false
gem 'newrelic_rpm', require: false gem 'newrelic_rpm', require: false
gem 'newrelic-sidekiq-metrics', '>= 1.6.2', require: false gem 'newrelic-sidekiq-metrics', '>= 1.6.2', require: false
@@ -129,8 +119,6 @@ gem 'sentry-sidekiq', '>= 5.19.0', require: false
gem 'sidekiq', '>= 7.3.1' gem 'sidekiq', '>= 7.3.1'
# We want cron jobs # We want cron jobs
gem 'sidekiq-cron', '>= 1.12.0' gem 'sidekiq-cron', '>= 1.12.0'
# for sidekiq healthcheck
gem 'sidekiq_alive'
##-- Push notification service --## ##-- Push notification service --##
gem 'fcm' gem 'fcm'
@@ -175,7 +163,6 @@ gem 'audited', '~> 5.4', '>= 5.4.1'
# need for google auth # need for google auth
gem 'omniauth', '>= 2.1.2' gem 'omniauth', '>= 2.1.2'
gem 'omniauth-saml'
gem 'omniauth-google-oauth2', '>= 1.1.3' gem 'omniauth-google-oauth2', '>= 1.1.3'
gem 'omniauth-rails_csrf_protection', '~> 1.0', '>= 1.0.2' gem 'omniauth-rails_csrf_protection', '~> 1.0', '>= 1.0.2'
@@ -188,10 +175,6 @@ gem 'reverse_markdown'
gem 'iso-639' gem 'iso-639'
gem 'ruby-openai' gem 'ruby-openai'
gem 'ai-agents', '>= 0.4.3'
# TODO: Move this gem as a dependency of ai-agents
gem 'ruby_llm-schema'
gem 'shopify_api' gem 'shopify_api'
@@ -213,6 +196,9 @@ group :development do
gem 'scss_lint', require: false gem 'scss_lint', require: false
gem 'web-console', '>= 4.2.1' gem 'web-console', '>= 4.2.1'
# used in swagger build
gem 'json_refs'
# When we want to squash migrations # When we want to squash migrations
gem 'squasher' gem 'squasher'
@@ -221,8 +207,6 @@ group :development do
gem 'stackprof' gem 'stackprof'
# Should install the associated chrome extension to view query logs # Should install the associated chrome extension to view query logs
gem 'meta_request', '>= 0.8.3' gem 'meta_request', '>= 0.8.3'
gem 'tidewave'
end end
group :test do group :test do
@@ -232,7 +216,6 @@ group :test do
gem 'webmock' gem 'webmock'
# test profiling # test profiling
gem 'test-prof' gem 'test-prof'
gem 'simplecov_json_formatter', require: false
end end
group :development, :test do group :development, :test do
@@ -254,10 +237,9 @@ group :development, :test do
gem 'rubocop-performance', require: false gem 'rubocop-performance', require: false
gem 'rubocop-rails', require: false gem 'rubocop-rails', require: false
gem 'rubocop-rspec', require: false gem 'rubocop-rspec', require: false
gem 'rubocop-factory_bot', require: false
gem 'seed_dump' gem 'seed_dump'
gem 'shoulda-matchers' gem 'shoulda-matchers'
gem 'simplecov', '>= 0.21', require: false gem 'simplecov', '0.17.1', require: false
gem 'spring' gem 'spring'
gem 'spring-watcher-listen' gem 'spring-watcher-listen'
end end
+170 -306
View File
@@ -25,89 +25,76 @@ GIT
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
actioncable (7.1.5.2) actioncable (7.0.8.7)
actionpack (= 7.1.5.2) actionpack (= 7.0.8.7)
activesupport (= 7.1.5.2) activesupport (= 7.0.8.7)
nio4r (~> 2.0) nio4r (~> 2.0)
websocket-driver (>= 0.6.1) websocket-driver (>= 0.6.1)
zeitwerk (~> 2.6) actionmailbox (7.0.8.7)
actionmailbox (7.1.5.2) actionpack (= 7.0.8.7)
actionpack (= 7.1.5.2) activejob (= 7.0.8.7)
activejob (= 7.1.5.2) activerecord (= 7.0.8.7)
activerecord (= 7.1.5.2) activestorage (= 7.0.8.7)
activestorage (= 7.1.5.2) activesupport (= 7.0.8.7)
activesupport (= 7.1.5.2)
mail (>= 2.7.1) mail (>= 2.7.1)
net-imap net-imap
net-pop net-pop
net-smtp net-smtp
actionmailer (7.1.5.2) actionmailer (7.0.8.7)
actionpack (= 7.1.5.2) actionpack (= 7.0.8.7)
actionview (= 7.1.5.2) actionview (= 7.0.8.7)
activejob (= 7.1.5.2) activejob (= 7.0.8.7)
activesupport (= 7.1.5.2) activesupport (= 7.0.8.7)
mail (~> 2.5, >= 2.5.4) mail (~> 2.5, >= 2.5.4)
net-imap net-imap
net-pop net-pop
net-smtp net-smtp
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.0)
actionpack (7.1.5.2) actionpack (7.0.8.7)
actionview (= 7.1.5.2) actionview (= 7.0.8.7)
activesupport (= 7.1.5.2) activesupport (= 7.0.8.7)
nokogiri (>= 1.8.5) rack (~> 2.0, >= 2.2.4)
racc
rack (>= 2.2.4)
rack-session (>= 1.0.1)
rack-test (>= 0.6.3) rack-test (>= 0.6.3)
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.6) rails-html-sanitizer (~> 1.0, >= 1.2.0)
actiontext (7.1.5.2) actiontext (7.0.8.7)
actionpack (= 7.1.5.2) actionpack (= 7.0.8.7)
activerecord (= 7.1.5.2) activerecord (= 7.0.8.7)
activestorage (= 7.1.5.2) activestorage (= 7.0.8.7)
activesupport (= 7.1.5.2) activesupport (= 7.0.8.7)
globalid (>= 0.6.0) globalid (>= 0.6.0)
nokogiri (>= 1.8.5) nokogiri (>= 1.8.5)
actionview (7.1.5.2) actionview (7.0.8.7)
activesupport (= 7.1.5.2) activesupport (= 7.0.8.7)
builder (~> 3.1) builder (~> 3.1)
erubi (~> 1.11) erubi (~> 1.4)
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.6) rails-html-sanitizer (~> 1.1, >= 1.2.0)
active_record_query_trace (1.8) active_record_query_trace (1.8)
activejob (7.1.5.2) activejob (7.0.8.7)
activesupport (= 7.1.5.2) activesupport (= 7.0.8.7)
globalid (>= 0.3.6) globalid (>= 0.3.6)
activemodel (7.1.5.2) activemodel (7.0.8.7)
activesupport (= 7.1.5.2) activesupport (= 7.0.8.7)
activerecord (7.1.5.2) activerecord (7.0.8.7)
activemodel (= 7.1.5.2) activemodel (= 7.0.8.7)
activesupport (= 7.1.5.2) activesupport (= 7.0.8.7)
timeout (>= 0.4.0) activerecord-import (1.4.1)
activerecord-import (2.1.0)
activerecord (>= 4.2) activerecord (>= 4.2)
activestorage (7.1.5.2) activestorage (7.0.8.7)
actionpack (= 7.1.5.2) actionpack (= 7.0.8.7)
activejob (= 7.1.5.2) activejob (= 7.0.8.7)
activerecord (= 7.1.5.2) activerecord (= 7.0.8.7)
activesupport (= 7.1.5.2) activesupport (= 7.0.8.7)
marcel (~> 1.0) marcel (~> 1.0)
activesupport (7.1.5.2) mini_mime (>= 1.1.0)
base64 activesupport (7.0.8.7)
benchmark (>= 0.3)
bigdecimal
concurrent-ruby (~> 1.0, >= 1.0.2) concurrent-ruby (~> 1.0, >= 1.0.2)
connection_pool (>= 2.2.5)
drb
i18n (>= 1.6, < 2) i18n (>= 1.6, < 2)
logger (>= 1.4.2)
minitest (>= 5.1) minitest (>= 5.1)
mutex_m
securerandom (>= 0.3)
tzinfo (~> 2.0) tzinfo (~> 2.0)
acts-as-taggable-on (12.0.0) acts-as-taggable-on (9.0.1)
activerecord (>= 7.1, < 8.1) activerecord (>= 6.0, < 7.1)
zeitwerk (>= 2.4, < 3.0)
addressable (2.8.7) addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0) public_suffix (>= 2.0.2, < 7.0)
administrate (0.20.1) administrate (0.20.1)
@@ -126,12 +113,10 @@ GEM
jbuilder (~> 2) jbuilder (~> 2)
rails (>= 4.2, < 7.2) rails (>= 4.2, < 7.2)
selectize-rails (~> 0.6) selectize-rails (~> 0.6)
ai-agents (0.4.3)
ruby_llm (~> 1.3)
annotate (3.2.0) annotate (3.2.0)
activerecord (>= 3.2, < 8.0) activerecord (>= 3.2, < 8.0)
rake (>= 10.4, < 14.0) rake (>= 10.4, < 14.0)
ast (2.4.3) ast (2.4.2)
attr_extras (7.1.0) attr_extras (7.1.0)
audited (5.4.1) audited (5.4.1)
activerecord (>= 5.0, < 7.7) activerecord (>= 5.0, < 7.7)
@@ -155,17 +140,16 @@ GEM
barnes (0.0.9) barnes (0.0.9)
multi_json (~> 1) multi_json (~> 1)
statsd-ruby (~> 1.1) statsd-ruby (~> 1.1)
base64 (0.3.0) base64 (0.2.0)
bcrypt (3.1.20) bcrypt (3.1.20)
benchmark (0.4.1) bigdecimal (3.1.8)
bigdecimal (3.2.2)
bindex (0.8.1) bindex (0.8.1)
bootsnap (1.16.0) bootsnap (1.16.0)
msgpack (~> 1.2) msgpack (~> 1.2)
brakeman (5.4.1) brakeman (5.4.1)
browser (5.3.1) browser (5.3.1)
builder (3.3.0) builder (3.3.0)
bullet (8.0.7) bullet (7.0.7)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
uniform_notifier (~> 1.11) uniform_notifier (~> 1.11)
bundle-audit (0.1.0) bundle-audit (0.1.0)
@@ -174,13 +158,11 @@ GEM
bundler (>= 1.2.0, < 3) bundler (>= 1.2.0, < 3)
thor (~> 1.0) thor (~> 1.0)
byebug (11.1.3) byebug (11.1.3)
childprocess (5.1.0)
logger (~> 1.5)
climate_control (1.2.0) climate_control (1.2.0)
coderay (1.1.3) coderay (1.1.3)
commonmarker (0.23.10) commonmarker (0.23.10)
concurrent-ruby (1.3.5) concurrent-ruby (1.3.4)
connection_pool (2.5.3) connection_pool (2.4.1)
crack (1.0.0) crack (1.0.0)
bigdecimal bigdecimal
rexml rexml
@@ -194,14 +176,16 @@ GEM
activerecord (>= 5.a) activerecord (>= 5.a)
database_cleaner-core (~> 2.0.0) database_cleaner-core (~> 2.0.0)
database_cleaner-core (2.0.1) database_cleaner-core (2.0.1)
datadog (2.19.0) datadog-ci (0.8.3)
datadog-ruby_core_source (~> 3.4, >= 3.4.1)
libdatadog (~> 18.1.0.1.0)
libddwaf (~> 1.24.1.0.3)
logger
msgpack msgpack
datadog-ruby_core_source (3.4.1)
date (3.4.1) date (3.4.1)
ddtrace (1.23.2)
datadog-ci (~> 0.8.1)
debase-ruby_core_source (= 3.3.1)
libdatadog (~> 7.0.0.1.0)
libddwaf (~> 1.14.0.0.0)
msgpack
debase-ruby_core_source (3.3.1)
debug (1.8.0) debug (1.8.0)
irb (>= 1.5.0) irb (>= 1.5.0)
reline (>= 0.3.1) reline (>= 0.3.1)
@@ -212,19 +196,14 @@ GEM
railties (>= 4.1.0) railties (>= 4.1.0)
responders responders
warden (~> 1.2.3) warden (~> 1.2.3)
devise-two-factor (6.1.0) devise_token_auth (1.2.3)
activesupport (>= 7.0, < 8.1)
devise (~> 4.0)
railties (>= 7.0, < 8.1)
rotp (~> 6.0)
devise_token_auth (1.2.5)
bcrypt (~> 3.0) bcrypt (~> 3.0)
devise (> 3.5.2, < 5) devise (> 3.5.2, < 5)
rails (>= 4.2.0, < 8.1) rails (>= 4.2.0, < 7.2)
diff-lcs (1.5.1) diff-lcs (1.5.1)
digest-crc (0.6.5) digest-crc (0.6.5)
rake (>= 12.0.0, < 14.0.0) rake (>= 12.0.0, < 14.0.0)
docile (1.4.1) docile (1.4.0)
domain_name (0.5.20190701) domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0) unf (>= 0.0.5, < 1.0.0)
dotenv (3.1.2) dotenv (3.1.2)
@@ -233,37 +212,7 @@ GEM
railties (>= 6.1) railties (>= 6.1)
down (5.4.0) down (5.4.0)
addressable (~> 2.8) addressable (~> 2.8)
drb (2.2.3)
dry-cli (1.1.0) dry-cli (1.1.0)
dry-configurable (1.3.0)
dry-core (~> 1.1)
zeitwerk (~> 2.6)
dry-core (1.1.0)
concurrent-ruby (~> 1.0)
logger
zeitwerk (~> 2.6)
dry-inflector (1.2.0)
dry-initializer (3.2.0)
dry-logic (1.6.0)
bigdecimal
concurrent-ruby (~> 1.0)
dry-core (~> 1.1)
zeitwerk (~> 2.6)
dry-schema (1.14.1)
concurrent-ruby (~> 1.0)
dry-configurable (~> 1.0, >= 1.0.1)
dry-core (~> 1.1)
dry-initializer (~> 3.2)
dry-logic (~> 1.5)
dry-types (~> 1.8)
zeitwerk (~> 2.6)
dry-types (1.8.3)
bigdecimal (~> 3.0)
concurrent-ruby (~> 1.0)
dry-core (~> 1.0)
dry-inflector (~> 1.0)
dry-logic (~> 1.4)
zeitwerk (~> 2.6)
ecma-re-validator (0.4.0) ecma-re-validator (0.4.0)
regexp_parser (~> 2.2) regexp_parser (~> 2.2)
elastic-apm (4.6.2) elastic-apm (4.6.2)
@@ -286,41 +235,26 @@ GEM
railties (>= 5.0.0) railties (>= 5.0.0)
faker (3.2.0) faker (3.2.0)
i18n (>= 1.8.11, < 2) i18n (>= 1.8.11, < 2)
faraday (2.13.1) faraday (2.9.0)
faraday-net_http (>= 2.0, < 3.5) faraday-net_http (>= 2.0, < 3.2)
json
logger
faraday-follow_redirects (0.3.0) faraday-follow_redirects (0.3.0)
faraday (>= 1, < 3) faraday (>= 1, < 3)
faraday-mashify (1.0.0) faraday-mashify (0.1.1)
faraday (~> 2.0) faraday (~> 2.0)
hashie hashie
faraday-multipart (1.0.4) faraday-multipart (1.0.4)
multipart-post (~> 2) multipart-post (~> 2)
faraday-net_http (3.4.0) faraday-net_http (3.1.0)
net-http (>= 0.5.0) net-http
faraday-net_http_persistent (2.1.0) faraday-net_http_persistent (2.1.0)
faraday (~> 2.5) faraday (~> 2.5)
net-http-persistent (~> 4.0) net-http-persistent (~> 4.0)
faraday-retry (2.2.1) faraday-retry (2.2.1)
faraday (~> 2.0) faraday (~> 2.0)
faraday_middleware-aws-sigv4 (1.0.1)
aws-sigv4 (~> 1.0)
faraday (>= 2.0, < 3)
fast-mcp (1.5.0)
addressable (~> 2.8)
base64
dry-schema (~> 1.14)
json (~> 2.0)
mime-types (~> 3.4)
rack (~> 3.1)
fcm (1.0.8) fcm (1.0.8)
faraday (>= 1.0.0, < 3.0) faraday (>= 1.0.0, < 3.0)
googleauth (~> 1) googleauth (~> 1)
ffi (1.17.2) ffi (1.16.3)
ffi (1.17.2-arm64-darwin)
ffi (1.17.2-x86_64-darwin)
ffi (1.17.2-x86_64-linux-gnu)
ffi-compiler (1.0.1) ffi-compiler (1.0.1)
ffi (>= 1.0.0) ffi (>= 1.0.0)
rake rake
@@ -381,13 +315,16 @@ GEM
google-cloud-translate-v3 (0.10.0) google-cloud-translate-v3 (0.10.0)
gapic-common (>= 0.20.0, < 2.a) gapic-common (>= 0.20.0, < 2.a)
google-cloud-errors (~> 1.0) google-cloud-errors (~> 1.0)
google-protobuf (3.25.7) google-protobuf (3.25.5)
google-protobuf (3.25.5-arm64-darwin)
google-protobuf (3.25.5-x86_64-darwin)
google-protobuf (3.25.5-x86_64-linux)
googleapis-common-protos (1.6.0) googleapis-common-protos (1.6.0)
google-protobuf (>= 3.18, < 5.a) google-protobuf (>= 3.18, < 5.a)
googleapis-common-protos-types (~> 1.7) googleapis-common-protos-types (~> 1.7)
grpc (~> 1.41) grpc (~> 1.41)
googleapis-common-protos-types (1.20.0) googleapis-common-protos-types (1.14.0)
google-protobuf (>= 3.18, < 5.a) google-protobuf (~> 3.18)
googleauth (1.11.2) googleauth (1.11.2)
faraday (>= 1.0, < 3.a) faraday (>= 1.0, < 3.a)
google-cloud-env (~> 2.1) google-cloud-env (~> 2.1)
@@ -397,19 +334,18 @@ GEM
signet (>= 0.16, < 2.a) signet (>= 0.16, < 2.a)
groupdate (6.2.1) groupdate (6.2.1)
activesupport (>= 5.2) activesupport (>= 5.2)
grpc (1.72.0) grpc (1.62.0)
google-protobuf (>= 3.25, < 5.0) google-protobuf (~> 3.25)
googleapis-common-protos-types (~> 1.0) googleapis-common-protos-types (~> 1.0)
grpc (1.72.0-arm64-darwin) grpc (1.62.0-arm64-darwin)
google-protobuf (>= 3.25, < 5.0) google-protobuf (~> 3.25)
googleapis-common-protos-types (~> 1.0) googleapis-common-protos-types (~> 1.0)
grpc (1.72.0-x86_64-darwin) grpc (1.62.0-x86_64-darwin)
google-protobuf (>= 3.25, < 5.0) google-protobuf (~> 3.25)
googleapis-common-protos-types (~> 1.0) googleapis-common-protos-types (~> 1.0)
grpc (1.72.0-x86_64-linux) grpc (1.62.0-x86_64-linux)
google-protobuf (>= 3.25, < 5.0) google-protobuf (~> 3.25)
googleapis-common-protos-types (~> 1.0) googleapis-common-protos-types (~> 1.0)
gserver (0.0.1)
haikunator (1.1.1) haikunator (1.1.1)
hairtrigger (1.0.0) hairtrigger (1.0.0)
activerecord (>= 6.0, < 8) activerecord (>= 6.0, < 8)
@@ -434,7 +370,7 @@ GEM
mini_mime (>= 1.0.0) mini_mime (>= 1.0.0)
multi_xml (>= 0.5.2) multi_xml (>= 0.5.2)
httpclient (2.8.3) httpclient (2.8.3)
i18n (1.14.7) i18n (1.14.6)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
image_processing (1.12.2) image_processing (1.12.2)
mini_magick (>= 4.9.5, < 5) mini_magick (>= 4.9.5, < 5)
@@ -452,7 +388,7 @@ GEM
rails-dom-testing (>= 1, < 3) rails-dom-testing (>= 1, < 3)
railties (>= 4.2.0) railties (>= 4.2.0)
thor (>= 0.14, < 2.0) thor (>= 0.14, < 2.0)
json (2.13.2) json (2.6.3)
json_refs (0.1.8) json_refs (0.1.8)
hana hana
json_schemer (0.2.24) json_schemer (0.2.24)
@@ -467,7 +403,7 @@ GEM
judoscale-sidekiq (1.8.2) judoscale-sidekiq (1.8.2)
judoscale-ruby (= 1.8.2) judoscale-ruby (= 1.8.2)
sidekiq (>= 5.0) sidekiq (>= 5.0)
jwt (2.10.1) jwt (2.8.1)
base64 base64
kaminari (1.2.2) kaminari (1.2.2)
activesupport (>= 4.1.0) activesupport (>= 4.1.0)
@@ -487,25 +423,21 @@ GEM
faraday-multipart faraday-multipart
json (>= 1.8) json (>= 1.8)
rexml rexml
language_server-protocol (3.17.0.5) launchy (2.5.2)
launchy (3.1.1)
addressable (~> 2.8) addressable (~> 2.8)
childprocess (~> 5.0) letter_opener (1.8.1)
logger (~> 1.6) launchy (>= 2.2, < 3)
letter_opener (1.10.0) libdatadog (7.0.0.1.0)
launchy (>= 2.2, < 4) libdatadog (7.0.0.1.0-x86_64-linux)
libdatadog (18.1.0.1.0) libddwaf (1.14.0.0.0)
libdatadog (18.1.0.1.0-x86_64-linux)
libddwaf (1.24.1.0.3)
ffi (~> 1.0) ffi (~> 1.0)
libddwaf (1.24.1.0.3-arm64-darwin) libddwaf (1.14.0.0.0-arm64-darwin)
ffi (~> 1.0) ffi (~> 1.0)
libddwaf (1.24.1.0.3-x86_64-darwin) libddwaf (1.14.0.0.0-x86_64-darwin)
ffi (~> 1.0) ffi (~> 1.0)
libddwaf (1.24.1.0.3-x86_64-linux) libddwaf (1.14.0.0.0-x86_64-linux)
ffi (~> 1.0) ffi (~> 1.0)
line-bot-api (1.28.0) line-bot-api (1.28.0)
lint_roller (1.1.0)
liquid (5.4.0) liquid (5.4.0)
listen (3.8.0) listen (3.8.0)
rb-fsevent (~> 0.10, >= 0.10.3) rb-fsevent (~> 0.10, >= 0.10.3)
@@ -513,7 +445,7 @@ GEM
llhttp-ffi (0.4.0) llhttp-ffi (0.4.0)
ffi-compiler (~> 1.0) ffi-compiler (~> 1.0)
rake (~> 13.0) rake (~> 13.0)
logger (1.7.0) logger (1.6.0)
lograge (0.14.0) lograge (0.14.0)
actionpack (>= 4) actionpack (>= 4)
activesupport (>= 4) activesupport (>= 4)
@@ -538,18 +470,18 @@ GEM
mime-types-data (3.2023.0218.1) mime-types-data (3.2023.0218.1)
mini_magick (4.12.0) mini_magick (4.12.0)
mini_mime (1.1.5) mini_mime (1.1.5)
mini_portile2 (2.8.9) mini_portile2 (2.8.8)
minitest (5.25.5) minitest (5.25.4)
mock_redis (0.36.0) mock_redis (0.36.0)
ruby2_keywords ruby2_keywords
msgpack (1.8.0) msgpack (1.7.0)
multi_json (1.15.0) multi_json (1.15.0)
multi_xml (0.6.0) multi_xml (0.6.0)
multipart-post (2.3.0) multipart-post (2.3.0)
mutex_m (0.3.0) mutex_m (0.3.0)
neighbor (0.2.3) neighbor (0.2.3)
activerecord (>= 5.2) activerecord (>= 5.2)
net-http (0.6.0) net-http (0.4.1)
uri uri
net-http-persistent (4.0.2) net-http-persistent (4.0.2)
connection_pool (~> 2.2) connection_pool (~> 2.2)
@@ -569,14 +501,14 @@ GEM
newrelic_rpm (9.6.0) newrelic_rpm (9.6.0)
base64 base64
nio4r (2.7.3) nio4r (2.7.3)
nokogiri (1.18.9) nokogiri (1.18.8)
mini_portile2 (~> 2.8.2) mini_portile2 (~> 2.8.2)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.18.9-arm64-darwin) nokogiri (1.18.8-arm64-darwin)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.18.9-x86_64-darwin) nokogiri (1.18.8-x86_64-darwin)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.18.9-x86_64-linux-gnu) nokogiri (1.18.8-x86_64-linux-gnu)
racc (~> 1.4) racc (~> 1.4)
oauth (1.1.0) oauth (1.1.0)
oauth-tty (~> 1.0, >= 1.0.1) oauth-tty (~> 1.0, >= 1.0.1)
@@ -591,15 +523,11 @@ GEM
rack (>= 1.2, < 4) rack (>= 1.2, < 4)
snaky_hash (~> 2.0) snaky_hash (~> 2.0)
version_gem (~> 1.1) version_gem (~> 1.1)
octokit (10.0.0)
faraday (>= 1, < 3)
sawyer (~> 0.9)
oj (3.16.10) oj (3.16.10)
bigdecimal (>= 3.0) bigdecimal (>= 3.0)
ostruct (>= 0.2) ostruct (>= 0.2)
omniauth (2.1.3) omniauth (2.1.2)
hashie (>= 3.4.6) hashie (>= 3.4.6)
logger
rack (>= 2.2.3) rack (>= 2.2.3)
rack-protection rack-protection
omniauth-google-oauth2 (1.1.3) omniauth-google-oauth2 (1.1.3)
@@ -613,26 +541,18 @@ GEM
omniauth-rails_csrf_protection (1.0.2) omniauth-rails_csrf_protection (1.0.2)
actionpack (>= 4.2) actionpack (>= 4.2)
omniauth (~> 2.0) omniauth (~> 2.0)
omniauth-saml (2.2.4)
omniauth (~> 2.1)
ruby-saml (~> 1.18)
opensearch-ruby (3.4.0)
faraday (>= 1.0, < 3)
multi_json (>= 1.0)
openssl (3.2.0) openssl (3.2.0)
orm_adapter (0.5.0) orm_adapter (0.5.0)
os (1.1.4) os (1.1.4)
ostruct (0.6.1) ostruct (0.6.1)
parallel (1.27.0) parallel (1.23.0)
parser (3.3.8.0) parser (3.2.2.1)
ast (~> 2.4.1) ast (~> 2.4.1)
racc
pg (1.5.3) pg (1.5.3)
pg_search (2.3.6) pg_search (2.3.6)
activerecord (>= 5.2) activerecord (>= 5.2)
activesupport (>= 5.2) activesupport (>= 5.2)
pgvector (0.1.1) pgvector (0.1.1)
prism (1.4.0)
procore-sift (1.0.0) procore-sift (1.0.0)
activerecord (>= 6.1) activerecord (>= 6.1)
pry (0.14.2) pry (0.14.2)
@@ -640,14 +560,14 @@ GEM
method_source (~> 1.0) method_source (~> 1.0)
pry-rails (0.3.9) pry-rails (0.3.9)
pry (>= 0.10.4) pry (>= 0.10.4)
public_suffix (6.0.2) public_suffix (6.0.0)
puma (6.4.3) puma (6.4.3)
nio4r (~> 2.0) nio4r (~> 2.0)
pundit (2.3.0) pundit (2.3.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
raabro (1.4.0) raabro (1.4.0)
racc (1.8.1) racc (1.8.1)
rack (3.2.3) rack (2.2.14)
rack-attack (6.7.0) rack-attack (6.7.0)
rack (>= 1.0, < 4) rack (>= 1.0, < 4)
rack-contrib (2.5.0) rack-contrib (2.5.0)
@@ -656,34 +576,28 @@ GEM
rack (>= 2.0.0) rack (>= 2.0.0)
rack-mini-profiler (3.2.0) rack-mini-profiler (3.2.0)
rack (>= 1.2.0) rack (>= 1.2.0)
rack-protection (4.1.1) rack-protection (3.2.0)
base64 (>= 0.1.0) base64 (>= 0.1.0)
logger (>= 1.6.0) rack (~> 2.2, >= 2.2.4)
rack (>= 3.0.0, < 4)
rack-proxy (0.7.7) rack-proxy (0.7.7)
rack rack
rack-session (2.1.1)
base64 (>= 0.1.0)
rack (>= 3.0.0)
rack-test (2.1.0) rack-test (2.1.0)
rack (>= 1.3) rack (>= 1.3)
rack-timeout (0.6.3) rack-timeout (0.6.3)
rackup (2.2.1) rails (7.0.8.7)
rack (>= 3) actioncable (= 7.0.8.7)
rails (7.1.5.2) actionmailbox (= 7.0.8.7)
actioncable (= 7.1.5.2) actionmailer (= 7.0.8.7)
actionmailbox (= 7.1.5.2) actionpack (= 7.0.8.7)
actionmailer (= 7.1.5.2) actiontext (= 7.0.8.7)
actionpack (= 7.1.5.2) actionview (= 7.0.8.7)
actiontext (= 7.1.5.2) activejob (= 7.0.8.7)
actionview (= 7.1.5.2) activemodel (= 7.0.8.7)
activejob (= 7.1.5.2) activerecord (= 7.0.8.7)
activemodel (= 7.1.5.2) activestorage (= 7.0.8.7)
activerecord (= 7.1.5.2) activesupport (= 7.0.8.7)
activestorage (= 7.1.5.2)
activesupport (= 7.1.5.2)
bundler (>= 1.15.0) bundler (>= 1.15.0)
railties (= 7.1.5.2) railties (= 7.0.8.7)
rails-dom-testing (2.2.0) rails-dom-testing (2.2.0)
activesupport (>= 5.0.0) activesupport (>= 5.0.0)
minitest minitest
@@ -691,14 +605,13 @@ GEM
rails-html-sanitizer (1.6.1) rails-html-sanitizer (1.6.1)
loofah (~> 2.21) loofah (~> 2.21)
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
railties (7.1.5.2) railties (7.0.8.7)
actionpack (= 7.1.5.2) actionpack (= 7.0.8.7)
activesupport (= 7.1.5.2) activesupport (= 7.0.8.7)
irb method_source
rackup (>= 1.0.0)
rake (>= 12.2) rake (>= 12.2)
thor (~> 1.0, >= 1.2.2) thor (~> 1.0)
zeitwerk (~> 2.6) zeitwerk (~> 2.5)
rainbow (3.1.1) rainbow (3.1.1)
rake (13.2.1) rake (13.2.1)
rb-fsevent (0.11.2) rb-fsevent (0.11.2)
@@ -710,7 +623,7 @@ GEM
connection_pool connection_pool
redis-namespace (1.10.0) redis-namespace (1.10.0)
redis (>= 4) redis (>= 4)
regexp_parser (2.10.0) regexp_parser (2.8.0)
reline (0.3.6) reline (0.3.6)
io-console (~> 0.5) io-console (~> 0.5)
representable (3.2.0) representable (3.2.0)
@@ -730,8 +643,7 @@ GEM
retriable (3.1.2) retriable (3.1.2)
reverse_markdown (2.1.1) reverse_markdown (2.1.1)
nokogiri nokogiri
rexml (3.4.4) rexml (3.3.9)
rotp (6.3.0)
rspec-core (3.13.0) rspec-core (3.13.0)
rspec-support (~> 3.13.0) rspec-support (~> 3.13.0)
rspec-expectations (3.13.2) rspec-expectations (3.13.2)
@@ -751,60 +663,41 @@ GEM
rspec-support (3.13.1) rspec-support (3.13.1)
rspec_junit_formatter (0.6.0) rspec_junit_formatter (0.6.0)
rspec-core (>= 2, < 4, != 2.12.0) rspec-core (>= 2, < 4, != 2.12.0)
rubocop (1.75.6) rubocop (1.50.2)
json (~> 2.3) json (~> 2.3)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0)
parallel (~> 1.10) parallel (~> 1.10)
parser (>= 3.3.0.2) parser (>= 3.2.0.0)
rainbow (>= 2.2.2, < 4.0) rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 2.9.3, < 3.0) regexp_parser (>= 1.8, < 3.0)
rubocop-ast (>= 1.44.0, < 2.0) rexml (>= 3.2.5, < 4.0)
rubocop-ast (>= 1.28.0, < 2.0)
ruby-progressbar (~> 1.7) ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 4.0) unicode-display_width (>= 2.4.0, < 3.0)
rubocop-ast (1.44.1) rubocop-ast (1.28.1)
parser (>= 3.3.7.2) parser (>= 3.2.1.0)
prism (~> 1.4) rubocop-capybara (2.18.0)
rubocop-factory_bot (2.27.1) rubocop (~> 1.41)
lint_roller (~> 1.1) rubocop-performance (1.17.1)
rubocop (~> 1.72, >= 1.72.1) rubocop (>= 1.7.0, < 2.0)
rubocop-performance (1.25.0) rubocop-ast (>= 0.4.0)
lint_roller (~> 1.1) rubocop-rails (2.19.1)
rubocop (>= 1.75.0, < 2.0)
rubocop-ast (>= 1.38.0, < 2.0)
rubocop-rails (2.32.0)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
lint_roller (~> 1.1)
rack (>= 1.1) rack (>= 1.1)
rubocop (>= 1.75.0, < 2.0) rubocop (>= 1.33.0, < 2.0)
rubocop-ast (>= 1.44.0, < 2.0) rubocop-rspec (2.21.0)
rubocop-rspec (3.6.0) rubocop (~> 1.33)
lint_roller (~> 1.1) rubocop-capybara (~> 2.17)
rubocop (~> 1.72, >= 1.72.1)
ruby-openai (7.3.1) ruby-openai (7.3.1)
event_stream_parser (>= 0.3.0, < 2.0.0) event_stream_parser (>= 0.3.0, < 2.0.0)
faraday (>= 1) faraday (>= 1)
faraday-multipart (>= 1) faraday-multipart (>= 1)
ruby-progressbar (1.13.0) ruby-progressbar (1.13.0)
ruby-saml (1.18.1)
nokogiri (>= 1.13.10)
rexml
ruby-vips (2.1.4) ruby-vips (2.1.4)
ffi (~> 1.12) ffi (~> 1.12)
ruby2_keywords (0.0.5) ruby2_keywords (0.0.5)
ruby2ruby (2.5.0) ruby2ruby (2.5.0)
ruby_parser (~> 3.1) ruby_parser (~> 3.1)
sexp_processor (~> 4.6) sexp_processor (~> 4.6)
ruby_llm (1.5.1)
base64
event_stream_parser (~> 1)
faraday (>= 1.10.0)
faraday-multipart (>= 1)
faraday-net_http (>= 1)
faraday-retry (>= 1)
marcel (~> 1.0)
zeitwerk (~> 2)
ruby_llm-schema (0.1.0)
ruby_parser (3.20.0) ruby_parser (3.20.0)
sexp_processor (~> 4.16) sexp_processor (~> 4.16)
sass (3.7.4) sass (3.7.4)
@@ -820,16 +713,10 @@ GEM
sprockets (> 3.0) sprockets (> 3.0)
sprockets-rails sprockets-rails
tilt tilt
sawyer (0.9.2)
addressable (>= 2.3.5)
faraday (>= 0.17.3, < 3)
scout_apm (5.3.3) scout_apm (5.3.3)
parser parser
scss_lint (0.60.0) scss_lint (0.60.0)
sass (~> 3.5, >= 3.5.5) sass (~> 3.5, >= 3.5.5)
searchkick (5.5.2)
activemodel (>= 7.1)
hashie
securerandom (0.4.1) securerandom (0.4.1)
seed_dump (3.3.1) seed_dump (3.3.1)
activerecord (>= 4) activerecord (>= 4)
@@ -868,22 +755,18 @@ GEM
fugit (~> 1.8) fugit (~> 1.8)
globalid (>= 1.0.1) globalid (>= 1.0.1)
sidekiq (>= 6) sidekiq (>= 6)
sidekiq_alive (2.5.0)
gserver (~> 0.0.1)
sidekiq (>= 5, < 9)
signet (0.17.0) signet (0.17.0)
addressable (~> 2.8) addressable (~> 2.8)
faraday (>= 0.17.5, < 3.a) faraday (>= 0.17.5, < 3.a)
jwt (>= 1.5, < 3.0) jwt (>= 1.5, < 3.0)
multi_json (~> 1.10) multi_json (~> 1.10)
simplecov (0.22.0) simplecov (0.17.1)
docile (~> 1.1) docile (~> 1.1)
simplecov-html (~> 0.11) json (>= 1.8, < 3)
simplecov_json_formatter (~> 0.1) simplecov-html (~> 0.10.0)
simplecov-html (0.13.2) simplecov-html (0.10.2)
simplecov_json_formatter (0.1.4) slack-ruby-client (2.5.2)
slack-ruby-client (2.7.0) faraday (>= 2.0)
faraday (>= 2.0.1)
faraday-mashify faraday-mashify
faraday-multipart faraday-multipart
gli gli
@@ -910,18 +793,14 @@ GEM
stripe (8.5.0) stripe (8.5.0)
telephone_number (1.4.20) telephone_number (1.4.20)
test-prof (1.2.1) test-prof (1.2.1)
thor (1.4.0) thor (1.3.1)
tidewave (0.2.0)
fast-mcp (~> 1.5.0)
rack (>= 2.0)
rails (>= 7.1.0)
tilt (2.3.0) tilt (2.3.0)
time_diff (0.3.0) time_diff (0.3.0)
activesupport activesupport
i18n i18n
timeout (0.4.3) timeout (0.4.3)
trailblazer-option (0.1.2) trailblazer-option (0.1.2)
twilio-ruby (7.6.0) twilio-ruby (5.77.0)
faraday (>= 0.9, < 3.0) faraday (>= 0.9, < 3.0)
jwt (>= 1.5, < 3.0) jwt (>= 1.5, < 3.0)
nokogiri (>= 1.6, < 2.0) nokogiri (>= 1.6, < 2.0)
@@ -937,11 +816,9 @@ GEM
unf (0.1.4) unf (0.1.4)
unf_ext unf_ext
unf_ext (0.0.8.2) unf_ext (0.0.8.2)
unicode-display_width (3.1.4) unicode-display_width (2.4.2)
unicode-emoji (~> 4.0, >= 4.0.4) uniform_notifier (1.16.0)
unicode-emoji (4.0.4) uri (1.0.3)
uniform_notifier (1.17.0)
uri (1.0.4)
uri_template (0.7.0) uri_template (0.7.0)
valid_email2 (5.2.6) valid_email2 (5.2.6)
activemodel (>= 3.2) activemodel (>= 3.2)
@@ -968,8 +845,7 @@ GEM
addressable (>= 2.8.0) addressable (>= 2.8.0)
crack (>= 0.3.2) crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0) hashdiff (>= 0.4.0, < 2.0.0)
websocket-driver (0.7.7) websocket-driver (0.7.6)
base64
websocket-extensions (>= 0.1.0) websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5) websocket-extensions (0.1.5)
wisper (2.0.0) wisper (2.0.0)
@@ -996,7 +872,6 @@ DEPENDENCIES
administrate (>= 0.20.1) administrate (>= 0.20.1)
administrate-field-active_storage (>= 1.0.3) administrate-field-active_storage (>= 1.0.3)
administrate-field-belongs_to_search (>= 0.9.0) administrate-field-belongs_to_search (>= 0.9.0)
ai-agents (>= 0.4.3)
annotate annotate
attr_extras attr_extras
audited (~> 5.4, >= 5.4.1) audited (~> 5.4, >= 5.4.1)
@@ -1013,11 +888,10 @@ DEPENDENCIES
commonmarker commonmarker
csv-safe csv-safe
database_cleaner database_cleaner
datadog (~> 2.0) ddtrace
debug (~> 1.8) debug (~> 1.8)
devise (>= 4.9.4) devise (>= 4.9.4)
devise-secure_password! devise-secure_password!
devise-two-factor (>= 5.0.0)
devise_token_auth (>= 1.2.3) devise_token_auth (>= 1.2.3)
dotenv-rails (>= 3.0.0) dotenv-rails (>= 3.0.0)
down down
@@ -1026,7 +900,6 @@ DEPENDENCIES
facebook-messenger facebook-messenger
factory_bot_rails (>= 6.4.3) factory_bot_rails (>= 6.4.3)
faker faker
faraday_middleware-aws-sigv4
fcm fcm
flag_shih_tzu flag_shih_tzu
foreman foreman
@@ -1063,13 +936,10 @@ DEPENDENCIES
net-smtp (~> 0.3.4) net-smtp (~> 0.3.4)
newrelic-sidekiq-metrics (>= 1.6.2) newrelic-sidekiq-metrics (>= 1.6.2)
newrelic_rpm newrelic_rpm
octokit
omniauth (>= 2.1.2) omniauth (>= 2.1.2)
omniauth-google-oauth2 (>= 1.1.3) omniauth-google-oauth2 (>= 1.1.3)
omniauth-oauth2 omniauth-oauth2
omniauth-rails_csrf_protection (~> 1.0, >= 1.0.2) omniauth-rails_csrf_protection (~> 1.0, >= 1.0.2)
omniauth-saml
opensearch-ruby
pg pg
pg_search pg_search
pgvector pgvector
@@ -1081,7 +951,7 @@ DEPENDENCIES
rack-cors (= 2.0.0) rack-cors (= 2.0.0)
rack-mini-profiler (>= 3.2.0) rack-mini-profiler (>= 3.2.0)
rack-timeout rack-timeout
rails (~> 7.1) rails (~> 7.0.8.4)
redis redis
redis-namespace redis-namespace
responders (>= 3.1.1) responders (>= 3.1.1)
@@ -1090,15 +960,12 @@ DEPENDENCIES
rspec-rails (>= 6.1.5) rspec-rails (>= 6.1.5)
rspec_junit_formatter rspec_junit_formatter
rubocop rubocop
rubocop-factory_bot
rubocop-performance rubocop-performance
rubocop-rails rubocop-rails
rubocop-rspec rubocop-rspec
ruby-openai ruby-openai
ruby_llm-schema
scout_apm scout_apm
scss_lint scss_lint
searchkick
seed_dump seed_dump
sentry-rails (>= 5.19.0) sentry-rails (>= 5.19.0)
sentry-ruby sentry-ruby
@@ -1107,10 +974,8 @@ DEPENDENCIES
shoulda-matchers shoulda-matchers
sidekiq (>= 7.3.1) sidekiq (>= 7.3.1)
sidekiq-cron (>= 1.12.0) sidekiq-cron (>= 1.12.0)
sidekiq_alive simplecov (= 0.17.1)
simplecov (>= 0.21) slack-ruby-client (~> 2.5.2)
simplecov_json_formatter
slack-ruby-client (~> 2.7.0)
spring spring
spring-watcher-listen spring-watcher-listen
squasher squasher
@@ -1118,9 +983,8 @@ DEPENDENCIES
stripe stripe
telephone_number telephone_number
test-prof test-prof
tidewave
time_diff time_diff
twilio-ruby twilio-ruby (~> 5.66)
twitty (~> 0.1.5) twitty (~> 0.1.5)
tzinfo-data tzinfo-data
uglifier uglifier
@@ -1133,7 +997,7 @@ DEPENDENCIES
working_hours working_hours
RUBY VERSION RUBY VERSION
ruby 3.4.4p34 ruby 3.3.3p89
BUNDLED WITH BUNDLED WITH
2.5.16 2.5.16
+1 -8
View File
@@ -41,15 +41,8 @@ run:
force_run: force_run:
rm -f ./.overmind.sock rm -f ./.overmind.sock
rm -f tmp/pids/*.pid
overmind start -f Procfile.dev overmind start -f Procfile.dev
force_run_tunnel:
lsof -ti:3000 | xargs kill -9 2>/dev/null || true
rm -f ./.overmind.sock
rm -f tmp/pids/*.pid
overmind start -f Procfile.tunnel
debug: debug:
overmind connect backend overmind connect backend
@@ -59,4 +52,4 @@ debug_worker:
docker: docker:
docker build -t $(APP_NAME) -f ./docker/Dockerfile . docker build -t $(APP_NAME) -f ./docker/Dockerfile .
.PHONY: setup db_create db_migrate db_seed db_reset db console server burn docker run force_run force_run_tunnel debug debug_worker .PHONY: setup db_create db_migrate db_seed db_reset db console server burn docker run force_run debug debug_worker
-4
View File
@@ -1,4 +0,0 @@
backend: DISABLE_MINI_PROFILER=true bin/rails s -p 3000
# https://github.com/mperham/sidekiq/issues/3090#issuecomment-389748695
worker: dotenv bundle exec sidekiq -C config/sidekiq.yml
vite: bin/vite build --watch
-3
View File
@@ -2,8 +2,5 @@
# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
require_relative 'config/application' require_relative 'config/application'
# Load Enterprise Edition rake tasks if they exist
enterprise_tasks_path = Rails.root.join('enterprise/tasks_railtie.rb').to_s
require enterprise_tasks_path if File.exist?(enterprise_tasks_path)
Rails.application.load_tasks Rails.application.load_tasks
+1 -1
View File
@@ -1 +1 @@
4.4.0 3.13.0
+1 -1
View File
@@ -1 +1 @@
3.4.3 3.2.0
+1 -9
View File
@@ -6,7 +6,6 @@
# We don't want to update the name of the identified original contact. # We don't want to update the name of the identified original contact.
class ContactIdentifyAction class ContactIdentifyAction
include UrlHelper
pattr_initialize [:contact!, :params!, { retain_original_contact_name: false, discard_invalid_attrs: false }] pattr_initialize [:contact!, :params!, { retain_original_contact_name: false, discard_invalid_attrs: false }]
def perform def perform
@@ -105,14 +104,7 @@ class ContactIdentifyAction
# TODO: replace reject { |_k, v| v.blank? } with compact_blank when rails is upgraded # TODO: replace reject { |_k, v| v.blank? } with compact_blank when rails is upgraded
@contact.discard_invalid_attrs if discard_invalid_attrs @contact.discard_invalid_attrs if discard_invalid_attrs
@contact.save! @contact.save!
enqueue_avatar_job Avatar::AvatarFromUrlJob.perform_later(@contact, params[:avatar_url]) if params[:avatar_url].present? && !@contact.avatar.attached?
end
def enqueue_avatar_job
return unless params[:avatar_url].present? && !@contact.avatar.attached?
return unless url_valid?(params[:avatar_url])
Avatar::AvatarFromUrlJob.perform_later(@contact, params[:avatar_url])
end end
def merge_contact(base_contact, merge_contact) def merge_contact(base_contact, merge_contact)
+1 -12
View File
@@ -10,8 +10,7 @@ function toggleSecretField(e) {
if (!textElement) return; if (!textElement) return;
if (textElement.dataset.secretMasked === 'false') { if (textElement.dataset.secretMasked === 'false') {
const maskedLength = secretField.dataset.secretText?.length || 10; textElement.textContent = '•'.repeat(10);
textElement.textContent = '•'.repeat(maskedLength);
textElement.dataset.secretMasked = 'true'; textElement.dataset.secretMasked = 'true';
toggler.querySelector('svg use').setAttribute('xlink:href', '#eye-show'); toggler.querySelector('svg use').setAttribute('xlink:href', '#eye-show');
@@ -33,13 +32,3 @@ function copySecretField(e) {
navigator.clipboard.writeText(secretField.dataset.secretText); navigator.clipboard.writeText(secretField.dataset.secretText);
} }
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('.cell-data__secret-field').forEach(field => {
const span = field.querySelector('[data-secret-masked]');
if (span && span.dataset.secretMasked === 'true') {
const len = field.dataset.secretText?.length || 10;
span.textContent = '•'.repeat(len);
}
});
});
@@ -46,25 +46,17 @@
.cell-data__secret-field { .cell-data__secret-field {
align-items: center; align-items: center;
color: $hint-grey;
display: flex; display: flex;
span { span {
flex: 0 0 auto; flex: 1;
} }
[data-secret-toggler], button {
[data-secret-copier] { margin-left: 5px;
background: transparent;
border: 0;
color: inherit;
margin-left: 0.5rem;
padding: 0;
svg { svg {
fill: currentColor; fill: currentColor;
height: 1.25rem;
width: 1.25rem;
} }
} }
} }
-2
View File
@@ -52,5 +52,3 @@ class AgentBuilder
}.compact)) }.compact))
end end
end end
AgentBuilder.prepend_mod_with('AgentBuilder')
+2 -4
View File
@@ -59,13 +59,11 @@ class ContactInboxBuilder
end end
def create_contact_inbox def create_contact_inbox
attrs = { ::ContactInbox.create_with(hmac_verified: hmac_verified || false).find_or_create_by!(
contact_id: @contact.id, contact_id: @contact.id,
inbox_id: @inbox.id, inbox_id: @inbox.id,
source_id: @source_id source_id: @source_id
} )
::ContactInbox.where(attrs).first_or_create!(hmac_verified: hmac_verified || false)
rescue ActiveRecord::RecordNotUnique rescue ActiveRecord::RecordNotUnique
Rails.logger.info("[ContactInboxBuilder] RecordNotUnique #{@source_id} #{@contact.id} #{@inbox.id}") Rails.logger.info("[ContactInboxBuilder] RecordNotUnique #{@source_id} #{@contact.id} #{@inbox.id}")
update_old_contact_inbox update_old_contact_inbox
-54
View File
@@ -1,54 +0,0 @@
class Email::BaseBuilder
pattr_initialize [:inbox!]
private
def channel
@channel ||= inbox.channel
end
def account
@account ||= inbox.account
end
def conversation
@conversation ||= message.conversation
end
def custom_sender_name
message&.sender&.available_name || I18n.t('conversations.reply.email.header.notifications')
end
def sender_name(sender_email)
# Friendly: <agent_name> from <business_name>
# Professional: <business_name>
if inbox.friendly?
I18n.t(
'conversations.reply.email.header.friendly_name',
sender_name: custom_sender_name,
business_name: business_name,
from_email: sender_email
)
else
I18n.t(
'conversations.reply.email.header.professional_name',
business_name: business_name,
from_email: sender_email
)
end
end
def business_name
inbox.business_name || inbox.sanitized_name
end
def account_support_email
# Parse the email to ensure it's in the correct format, the user
# can save it in the format "Name <email@domain.com>"
parse_email(account.support_email)
end
def parse_email(email_string)
Mail::Address.new(email_string).address
end
end
-51
View File
@@ -1,51 +0,0 @@
class Email::FromBuilder < Email::BaseBuilder
pattr_initialize [:inbox!, :message!]
def build
return sender_name(account_support_email) unless inbox.email?
from_email = case email_channel_type
when :standard_imap_smtp,
:google_oauth,
:microsoft_oauth,
:forwarding_own_smtp
channel.email
when :imap_chatwoot_smtp,
:forwarding_chatwoot_smtp
channel.verified_for_sending ? channel.email : account_support_email
else
account_support_email
end
sender_name(from_email)
end
private
def email_channel_type
return :google_oauth if channel.google?
return :microsoft_oauth if channel.microsoft?
return :standard_imap_smtp if imap_and_smtp_enabled?
return :imap_chatwoot_smtp if imap_enabled_without_smtp?
return :forwarding_own_smtp if forwarding_with_own_smtp?
return :forwarding_chatwoot_smtp if forwarding_without_smtp?
:unknown
end
def imap_and_smtp_enabled?
channel.imap_enabled && channel.smtp_enabled
end
def imap_enabled_without_smtp?
channel.imap_enabled && !channel.smtp_enabled
end
def forwarding_with_own_smtp?
!channel.imap_enabled && channel.smtp_enabled
end
def forwarding_without_smtp?
!channel.imap_enabled && !channel.smtp_enabled
end
end
-21
View File
@@ -1,21 +0,0 @@
class Email::ReplyToBuilder < Email::BaseBuilder
pattr_initialize [:inbox!, :message!]
def build
reply_to = if inbox.email?
channel.email
elsif inbound_email_enabled?
"reply+#{conversation.uuid}@#{account.inbound_email_domain}"
else
account_support_email
end
sender_name(reply_to)
end
private
def inbound_email_enabled?
account.feature_enabled?('inbound_emails') && account.inbound_email_domain.present?
end
end
@@ -152,13 +152,11 @@ class Messages::Instagram::BaseMessageBuilder < Messages::Messenger::MessageBuil
end end
def message_already_exists? def message_already_exists?
find_message_by_source_id(@messaging[:message][:mid]).present? cw_message = conversation.messages.where(
end source_id: @messaging[:message][:mid]
).first
def find_message_by_source_id(source_id) cw_message.present?
return unless source_id
@message = Message.find_by(source_id: source_id)
end end
def all_unsupported_files? def all_unsupported_files?
-79
View File
@@ -7,7 +7,6 @@ class Messages::MessageBuilder
@private = params[:private] || false @private = params[:private] || false
@conversation = conversation @conversation = conversation
@user = user @user = user
@account = conversation.account
@message_type = params[:message_type] || 'outgoing' @message_type = params[:message_type] || 'outgoing'
@attachments = params[:attachments] @attachments = params[:attachments]
@automation_rule = content_attributes&.dig(:automation_rule_id) @automation_rule = content_attributes&.dig(:automation_rule_id)
@@ -21,9 +20,6 @@ class Messages::MessageBuilder
@message = @conversation.messages.build(message_params) @message = @conversation.messages.build(message_params)
process_attachments process_attachments
process_emails process_emails
# When the message has no quoted content, it will just be rendered as a regular message
# The frontend is equipped to handle this case
process_email_content if @account.feature_enabled?(:quoted_email_reply)
@message.save! @message.save!
@message @message
end end
@@ -96,14 +92,6 @@ class Messages::MessageBuilder
@message.content_attributes[:to_emails] = to_emails @message.content_attributes[:to_emails] = to_emails
end end
def process_email_content
return unless should_process_email_content?
@message.content_attributes ||= {}
email_attributes = build_email_attributes
@message.content_attributes[:email] = email_attributes
end
def process_email_string(email_string) def process_email_string(email_string)
return [] if email_string.blank? return [] if email_string.blank?
@@ -165,71 +153,4 @@ class Messages::MessageBuilder
source_id: @params[:source_id] source_id: @params[:source_id]
}.merge(external_created_at).merge(automation_rule_id).merge(campaign_id).merge(template_params) }.merge(external_created_at).merge(automation_rule_id).merge(campaign_id).merge(template_params)
end end
def email_inbox?
@conversation.inbox&.inbox_type == 'Email'
end
def should_process_email_content?
email_inbox? && !@private && @message.content.present?
end
def build_email_attributes
email_attributes = ensure_indifferent_access(@message.content_attributes[:email] || {})
normalized_content = normalize_email_body(@message.content)
# Use custom HTML content if provided, otherwise generate from message content
email_attributes[:html_content] = if custom_email_content_provided?
build_custom_html_content
else
build_html_content(normalized_content)
end
email_attributes[:text_content] = build_text_content(normalized_content)
email_attributes
end
def build_html_content(normalized_content)
html_content = ensure_indifferent_access(@message.content_attributes.dig(:email, :html_content) || {})
rendered_html = render_email_html(normalized_content)
html_content[:full] = rendered_html
html_content[:reply] = rendered_html
html_content
end
def build_text_content(normalized_content)
text_content = ensure_indifferent_access(@message.content_attributes.dig(:email, :text_content) || {})
text_content[:full] = normalized_content
text_content[:reply] = normalized_content
text_content
end
def ensure_indifferent_access(hash)
return {} if hash.blank?
hash.respond_to?(:with_indifferent_access) ? hash.with_indifferent_access : hash
end
def normalize_email_body(content)
content.to_s.gsub("\r\n", "\n")
end
def render_email_html(content)
return '' if content.blank?
ChatwootMarkdownRenderer.new(content).render_message.to_s
end
def custom_email_content_provided?
@params[:email_html_content].present?
end
def build_custom_html_content
html_content = ensure_indifferent_access(@message.content_attributes.dig(:email, :html_content) || {})
html_content[:full] = @params[:email_html_content]
html_content[:reply] = @params[:email_html_content]
html_content
end
end end
+16 -22
View File
@@ -10,28 +10,10 @@ class V2::Reports::BaseSummaryBuilder
def load_data def load_data
@conversations_count = fetch_conversations_count @conversations_count = fetch_conversations_count
load_reporting_events_data @resolved_count = fetch_resolved_count
end @avg_resolution_time = fetch_average_time('conversation_resolved')
@avg_first_response_time = fetch_average_time('first_response')
def load_reporting_events_data @avg_reply_time = fetch_average_time('reply_time')
# Extract the column name for indexing (e.g., 'conversations.team_id' -> 'team_id')
index_key = group_by_key.to_s.split('.').last
results = reporting_events
.select(
"#{group_by_key} as #{index_key}",
"COUNT(CASE WHEN name = 'conversation_resolved' THEN 1 END) as resolved_count",
"AVG(CASE WHEN name = 'conversation_resolved' THEN #{average_value_key} END) as avg_resolution_time",
"AVG(CASE WHEN name = 'first_response' THEN #{average_value_key} END) as avg_first_response_time",
"AVG(CASE WHEN name = 'reply_time' THEN #{average_value_key} END) as avg_reply_time"
)
.group(group_by_key)
.index_by { |record| record.public_send(index_key) }
@resolved_count = results.transform_values(&:resolved_count)
@avg_resolution_time = results.transform_values(&:avg_resolution_time)
@avg_first_response_time = results.transform_values(&:avg_first_response_time)
@avg_reply_time = results.transform_values(&:avg_reply_time)
end end
def reporting_events def reporting_events
@@ -42,6 +24,14 @@ class V2::Reports::BaseSummaryBuilder
# Override this method # Override this method
end end
def fetch_average_time(event_name)
get_grouped_average(reporting_events.where(name: event_name))
end
def fetch_resolved_count
reporting_events.where(name: 'conversation_resolved').group(group_by_key).count
end
def group_by_key def group_by_key
# Override this method # Override this method
end end
@@ -50,6 +40,10 @@ class V2::Reports::BaseSummaryBuilder
# Override this method # Override this method
end end
def get_grouped_average(events)
events.group(group_by_key).average(average_value_key)
end
def average_value_key def average_value_key
ActiveModel::Type::Boolean.new.cast(params[:business_hours]).present? ? :value_in_business_hours : :value ActiveModel::Type::Boolean.new.cast(params[:business_hours]).present? ? :value_in_business_hours : :value
end end
@@ -13,7 +13,10 @@ class V2::Reports::InboxSummaryBuilder < V2::Reports::BaseSummaryBuilder
def load_data def load_data
@conversations_count = fetch_conversations_count @conversations_count = fetch_conversations_count
load_reporting_events_data @resolved_count = fetch_resolved_count
@avg_resolution_time = fetch_average_time('conversation_resolved')
@avg_first_response_time = fetch_average_time('first_response')
@avg_reply_time = fetch_average_time('reply_time')
end end
def fetch_conversations_count def fetch_conversations_count
@@ -1,112 +0,0 @@
class V2::Reports::LabelSummaryBuilder < V2::Reports::BaseSummaryBuilder
attr_reader :account, :params
# rubocop:disable Lint/MissingSuper
# the parent class has no initialize
def initialize(account:, params:)
@account = account
@params = params
timezone_offset = (params[:timezone_offset] || 0).to_f
@timezone = ActiveSupport::TimeZone[timezone_offset]&.name
end
# rubocop:enable Lint/MissingSuper
def build
labels = account.labels.to_a
return [] if labels.empty?
report_data = collect_report_data
labels.map { |label| build_label_report(label, report_data) }
end
private
def collect_report_data
conversation_filter = build_conversation_filter
use_business_hours = use_business_hours?
{
conversation_counts: fetch_conversation_counts(conversation_filter),
resolved_counts: fetch_resolved_counts,
resolution_metrics: fetch_metrics(conversation_filter, 'conversation_resolved', use_business_hours),
first_response_metrics: fetch_metrics(conversation_filter, 'first_response', use_business_hours),
reply_metrics: fetch_metrics(conversation_filter, 'reply_time', use_business_hours)
}
end
def build_label_report(label, report_data)
{
id: label.id,
name: label.title,
conversations_count: report_data[:conversation_counts][label.title] || 0,
avg_resolution_time: report_data[:resolution_metrics][label.title] || 0,
avg_first_response_time: report_data[:first_response_metrics][label.title] || 0,
avg_reply_time: report_data[:reply_metrics][label.title] || 0,
resolved_conversations_count: report_data[:resolved_counts][label.title] || 0
}
end
def use_business_hours?
ActiveModel::Type::Boolean.new.cast(params[:business_hours])
end
def build_conversation_filter
conversation_filter = { account_id: account.id }
conversation_filter[:created_at] = range if range.present?
conversation_filter
end
def fetch_conversation_counts(conversation_filter)
fetch_counts(conversation_filter)
end
def fetch_resolved_counts
# Count resolution events, not conversations currently in resolved status
# Filter by reporting_event.created_at, not conversation.created_at
reporting_event_filter = { name: 'conversation_resolved', account_id: account.id }
reporting_event_filter[:created_at] = range if range.present?
ReportingEvent
.joins(conversation: { taggings: :tag })
.where(
reporting_event_filter.merge(
taggings: { taggable_type: 'Conversation', context: 'labels' }
)
)
.group('tags.name')
.count
end
def fetch_counts(conversation_filter)
ActsAsTaggableOn::Tagging
.joins('INNER JOIN conversations ON taggings.taggable_id = conversations.id')
.joins('INNER JOIN tags ON taggings.tag_id = tags.id')
.where(
taggable_type: 'Conversation',
context: 'labels',
conversations: conversation_filter
)
.select('tags.name, COUNT(taggings.*) AS count')
.group('tags.name')
.each_with_object({}) { |record, hash| hash[record.name] = record.count }
end
def fetch_metrics(conversation_filter, event_name, use_business_hours)
ReportingEvent
.joins(conversation: { taggings: :tag })
.where(
conversations: conversation_filter,
name: event_name,
taggings: { taggable_type: 'Conversation', context: 'labels' }
)
.group('tags.name')
.order('tags.name')
.select(
'tags.name',
use_business_hours ? 'AVG(reporting_events.value_in_business_hours) as avg_value' : 'AVG(reporting_events.value) as avg_value'
)
.each_with_object({}) { |record, hash| hash[record.name] = record.avg_value.to_f }
end
end
@@ -38,34 +38,27 @@ class V2::Reports::Timeseries::CountReportBuilder < V2::Reports::Timeseries::Bas
end end
def scope_for_resolutions_count def scope_for_resolutions_count
scope.reporting_events.where( scope.reporting_events.joins(:conversation).select(:conversation_id).where(
name: :conversation_resolved, name: :conversation_resolved,
account_id: account.id, conversations: { status: :resolved }, created_at: range
created_at: range ).distinct
)
end end
def scope_for_bot_resolutions_count def scope_for_bot_resolutions_count
scope.reporting_events.where( scope.reporting_events.joins(:conversation).select(:conversation_id).where(
name: :conversation_bot_resolved, name: :conversation_bot_resolved,
account_id: account.id, conversations: { status: :resolved }, created_at: range
created_at: range ).distinct
)
end end
def scope_for_bot_handoffs_count def scope_for_bot_handoffs_count
scope.reporting_events.joins(:conversation).select(:conversation_id).where( scope.reporting_events.joins(:conversation).select(:conversation_id).where(
name: :conversation_bot_handoff, name: :conversation_bot_handoff,
account_id: account.id,
created_at: range created_at: range
).distinct ).distinct
end end
def grouped_count def grouped_count
# IMPORTANT: time_zone parameter affects both data grouping AND output timestamps
# It converts timestamps to the target timezone before grouping, which means
# the same event can fall into different day buckets depending on timezone
# Example: 2024-01-15 00:00 UTC becomes 2024-01-14 16:00 PST (falls on different day)
@grouped_values = object_scope.group_by_period( @grouped_values = object_scope.group_by_period(
group_by, group_by,
:created_at, :created_at,
@@ -29,11 +29,6 @@ class Api::V1::Accounts::AgentBotsController < Api::V1::Accounts::BaseController
head :ok head :ok
end end
def reset_access_token
@agent_bot.access_token.regenerate_token
@agent_bot.reload
end
private private
def agent_bot def agent_bot
@@ -68,7 +68,7 @@ class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController
def article_params def article_params
params.require(:article).permit( 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, :locale, meta: [:title,
:description, :description,
{ tags: [] }] { tags: [] }]
@@ -1,20 +0,0 @@
class Api::V1::Accounts::AssignmentPolicies::InboxesController < Api::V1::Accounts::BaseController
before_action :fetch_assignment_policy
before_action -> { check_authorization(AssignmentPolicy) }
def index
@inboxes = @assignment_policy.inboxes
end
private
def fetch_assignment_policy
@assignment_policy = Current.account.assignment_policies.find(
params[:assignment_policy_id]
)
end
def permitted_params
params.permit(:assignment_policy_id)
end
end
@@ -1,36 +0,0 @@
class Api::V1::Accounts::AssignmentPoliciesController < Api::V1::Accounts::BaseController
before_action :fetch_assignment_policy, only: [:show, :update, :destroy]
before_action :check_authorization
def index
@assignment_policies = Current.account.assignment_policies
end
def show; end
def create
@assignment_policy = Current.account.assignment_policies.create!(assignment_policy_params)
end
def update
@assignment_policy.update!(assignment_policy_params)
end
def destroy
@assignment_policy.destroy!
head :ok
end
private
def fetch_assignment_policy
@assignment_policy = Current.account.assignment_policies.find(params[:id])
end
def assignment_policy_params
params.require(:assignment_policy).permit(
:name, :description, :assignment_order, :conversation_priority,
:fair_distribution_limit, :fair_distribution_window, :enabled
)
end
end
@@ -30,14 +30,7 @@ class Api::V1::Accounts::CallbacksController < Api::V1::Accounts::BaseController
end end
def facebook_pages def facebook_pages
pages = [] @page_details = mark_already_existing_facebook_pages(fb_object.get_connections('me', 'accounts'))
fb_pages = fb_object.get_connections('me', 'accounts')
pages.concat(fb_pages)
while fb_pages.respond_to?(:next_page) && (next_page = fb_pages.next_page)
fb_pages = next_page
pages.concat(fb_pages)
end
@page_details = mark_already_existing_facebook_pages(pages)
end end
def set_instagram_id(page_access_token, facebook_channel) def set_instagram_id(page_access_token, facebook_channel)
@@ -29,6 +29,6 @@ class Api::V1::Accounts::CampaignsController < Api::V1::Accounts::BaseController
def campaign_params def campaign_params
params.require(:campaign).permit(:title, :description, :message, :enabled, :trigger_only_during_business_hours, :inbox_id, :sender_id, params.require(:campaign).permit(:title, :description, :message, :enabled, :trigger_only_during_business_hours, :inbox_id, :sender_id,
:scheduled_at, audience: [:type, :id], trigger_rules: {}, template_params: {}) :scheduled_at, audience: [:type, :id], trigger_rules: {})
end end
end end
@@ -14,11 +14,11 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
before_action :check_authorization before_action :check_authorization
before_action :set_current_page, only: [:index, :active, :search, :filter] before_action :set_current_page, only: [:index, :active, :search, :filter]
before_action :fetch_contact, only: [:show, :update, :destroy, :avatar, :contactable_inboxes, :destroy_custom_attributes] before_action :fetch_contact, only: [:show, :update, :destroy, :avatar, :contactable_inboxes, :destroy_custom_attributes]
before_action :set_include_contact_inboxes, only: [:index, :active, :search, :filter, :show, :update] before_action :set_include_contact_inboxes, only: [:index, :search, :filter, :show, :update]
def index def index
@contacts_count = resolved_contacts.count
@contacts = fetch_contacts(resolved_contacts) @contacts = fetch_contacts(resolved_contacts)
@contacts_count = @contacts.total_count
end end
def search def search
@@ -29,8 +29,8 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
OR contacts.additional_attributes->>\'company_name\' ILIKE :search', OR contacts.additional_attributes->>\'company_name\' ILIKE :search',
search: "%#{params[:q].strip}%" search: "%#{params[:q].strip}%"
) )
@contacts_count = contacts.count
@contacts = fetch_contacts(contacts) @contacts = fetch_contacts(contacts)
@contacts_count = @contacts.total_count
end end
def import def import
@@ -55,8 +55,8 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
def active def active
contacts = Current.account.contacts.where(id: ::OnlineStatusTracker contacts = Current.account.contacts.where(id: ::OnlineStatusTracker
.get_available_contact_ids(Current.account.id)) .get_available_contact_ids(Current.account.id))
@contacts = fetch_contacts(contacts) @contacts_count = contacts.count
@contacts_count = @contacts.total_count @contacts = contacts.page(@current_page)
end end
def show; end def show; end
@@ -122,7 +122,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
def resolved_contacts def resolved_contacts
return @resolved_contacts if @resolved_contacts return @resolved_contacts if @resolved_contacts
@resolved_contacts = Current.account.contacts.resolved_contacts(use_crm_v2: Current.account.feature_enabled?('crm_v2')) @resolved_contacts = Current.account.contacts.resolved_contacts
@resolved_contacts = @resolved_contacts.tagged_with(params[:labels], any: true) if params[:labels].present? @resolved_contacts = @resolved_contacts.tagged_with(params[:labels], any: true) if params[:labels].present?
@resolved_contacts @resolved_contacts
@@ -133,14 +133,13 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
end end
def fetch_contacts(contacts) def fetch_contacts(contacts)
# Build includes hash to avoid separate query when contact_inboxes are needed contacts_with_avatar = filtrate(contacts)
includes_hash = { avatar_attachment: [:blob] } .includes([{ avatar_attachment: [:blob] }])
includes_hash[:contact_inboxes] = { inbox: :channel } if @include_contact_inboxes .page(@current_page).per(RESULTS_PER_PAGE)
filtrate(contacts) return contacts_with_avatar.includes([{ contact_inboxes: [:inbox] }]) if @include_contact_inboxes
.includes(includes_hash)
.page(@current_page) contacts_with_avatar
.per(RESULTS_PER_PAGE)
end end
def build_contact_inbox def build_contact_inbox
@@ -5,6 +5,6 @@ class Api::V1::Accounts::Conversations::BaseController < Api::V1::Accounts::Base
def conversation def conversation
@conversation ||= Current.account.conversations.find_by!(display_id: params[:conversation_id]) @conversation ||= Current.account.conversations.find_by!(display_id: params[:conversation_id])
authorize @conversation, :show? authorize @conversation.inbox, :show?
end end
end end
@@ -124,12 +124,6 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
@conversation.save! @conversation.save!
end end
def destroy
authorize @conversation, :destroy?
::DeleteObjectJob.perform_later(@conversation, Current.user, request.ip)
head :ok
end
private private
def permitted_update_params def permitted_update_params
@@ -160,7 +154,7 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
def conversation def conversation
@conversation ||= Current.account.conversations.find_by!(display_id: params[:id]) @conversation ||= Current.account.conversations.find_by!(display_id: params[:id])
authorize @conversation, :show? authorize @conversation.inbox, :show?
end end
def inbox def inbox
@@ -1,23 +1,32 @@
class Api::V1::Accounts::Google::AuthorizationsController < Api::V1::Accounts::OauthAuthorizationController class Api::V1::Accounts::Google::AuthorizationsController < Api::V1::Accounts::BaseController
include GoogleConcern include GoogleConcern
before_action :check_authorization
def create def create
email = params[:authorization][:email]
redirect_url = google_client.auth_code.authorize_url( redirect_url = google_client.auth_code.authorize_url(
{ {
redirect_uri: "#{base_url}/google/callback", redirect_uri: "#{base_url}/google/callback",
scope: scope, scope: 'email profile https://mail.google.com/',
response_type: 'code', response_type: 'code',
prompt: 'consent', # the oauth flow does not return a refresh token, this is supposed to fix it prompt: 'consent', # the oauth flow does not return a refresh token, this is supposed to fix it
access_type: 'offline', # the default is 'online' access_type: 'offline', # the default is 'online'
state: state,
client_id: GlobalConfigService.load('GOOGLE_OAUTH_CLIENT_ID', nil) client_id: GlobalConfigService.load('GOOGLE_OAUTH_CLIENT_ID', nil)
} }
) )
if redirect_url if redirect_url
cache_key = "google::#{email.downcase}"
::Redis::Alfred.setex(cache_key, Current.account.id, 5.minutes)
render json: { success: true, url: redirect_url } render json: { success: true, url: redirect_url }
else else
render json: { success: false }, status: :unprocessable_entity render json: { success: false }, status: :unprocessable_entity
end end
end end
private
def check_authorization
raise Pundit::NotAuthorizedError unless Current.account_user.administrator?
end
end end
@@ -1,46 +0,0 @@
class Api::V1::Accounts::Inboxes::AssignmentPoliciesController < Api::V1::Accounts::BaseController
before_action :fetch_inbox
before_action :fetch_assignment_policy, only: [:create]
before_action -> { check_authorization(AssignmentPolicy) }
before_action :validate_assignment_policy, only: [:show, :destroy]
def show
@assignment_policy = @inbox.assignment_policy
end
def create
# There should be only one assignment policy for an inbox.
# If there is a new request to add an assignment policy, we will
# delete the old one and attach the new policy
remove_inbox_assignment_policy
@inbox_assignment_policy = @inbox.create_inbox_assignment_policy!(assignment_policy: @assignment_policy)
@assignment_policy = @inbox.assignment_policy
end
def destroy
remove_inbox_assignment_policy
head :ok
end
private
def remove_inbox_assignment_policy
@inbox.inbox_assignment_policy&.destroy
end
def fetch_inbox
@inbox = Current.account.inboxes.find(permitted_params[:inbox_id])
end
def fetch_assignment_policy
@assignment_policy = Current.account.assignment_policies.find(permitted_params[:assignment_policy_id])
end
def permitted_params
params.permit(:assignment_policy_id, :inbox_id)
end
def validate_assignment_policy
return render_not_found_error(I18n.t('errors.assignment_policy.not_found')) unless @inbox.assignment_policy
end
end
@@ -4,8 +4,7 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
before_action :fetch_agent_bot, only: [:set_agent_bot] before_action :fetch_agent_bot, only: [:set_agent_bot]
before_action :validate_limit, only: [:create] before_action :validate_limit, only: [:create]
# we are already handling the authorization in fetch inbox # we are already handling the authorization in fetch inbox
before_action :check_authorization, except: [:show, :health] before_action :check_authorization, except: [:show]
before_action :validate_whatsapp_cloud_channel, only: [:health]
def index def index
@inboxes = policy_scope(Current.account.inboxes.order_by_name.includes(:channel, { avatar_attachment: [:blob] })) @inboxes = policy_scope(Current.account.inboxes.order_by_name.includes(:channel, { avatar_attachment: [:blob] }))
@@ -70,23 +69,6 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
render status: :ok, json: { message: I18n.t('messages.inbox_deletetion_response') } render status: :ok, json: { message: I18n.t('messages.inbox_deletetion_response') }
end end
def sync_templates
return render status: :unprocessable_entity, json: { error: 'Template sync is only available for WhatsApp channels' } unless whatsapp_channel?
trigger_template_sync
render status: :ok, json: { message: 'Template sync initiated successfully' }
rescue StandardError => e
render status: :internal_server_error, json: { error: e.message }
end
def health
health_data = Whatsapp::HealthService.new(@inbox.channel).fetch_health_status
render json: health_data
rescue StandardError => e
Rails.logger.error "[INBOX HEALTH] Error fetching health data: #{e.message}"
render json: { error: e.message }, status: :unprocessable_entity
end
private private
def fetch_inbox def fetch_inbox
@@ -98,22 +80,12 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
@agent_bot = AgentBot.find(params[:agent_bot]) if params[:agent_bot] @agent_bot = AgentBot.find(params[:agent_bot]) if params[:agent_bot]
end end
def validate_whatsapp_cloud_channel
return if @inbox.channel.is_a?(Channel::Whatsapp) && @inbox.channel.provider == 'whatsapp_cloud'
render json: { error: 'Health data only available for WhatsApp Cloud API channels' }, status: :bad_request
end
def create_channel def create_channel
return unless allowed_channel_types.include?(permitted_params[:channel][:type]) return unless %w[web_widget api email line telegram whatsapp sms].include?(permitted_params[:channel][:type])
account_channels_method.create!(permitted_params(channel_type_from_params::EDITABLE_ATTRS)[:channel].except(:type)) account_channels_method.create!(permitted_params(channel_type_from_params::EDITABLE_ATTRS)[:channel].except(:type))
end end
def allowed_channel_types
%w[web_widget api email line telegram whatsapp sms]
end
def update_inbox_working_hours def update_inbox_working_hours
@inbox.update_working_hours(params.permit(working_hours: Inbox::OFFISABLE_ATTRS)[:working_hours]) if params[:working_hours] @inbox.update_working_hours(params.permit(working_hours: Inbox::OFFISABLE_ATTRS)[:working_hours]) if params[:working_hours]
end end
@@ -198,18 +170,6 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
[] []
end end
end end
def whatsapp_channel?
@inbox.whatsapp? || (@inbox.twilio? && @inbox.channel.whatsapp?)
end
def trigger_template_sync
if @inbox.whatsapp?
Channels::Whatsapp::TemplatesSyncJob.perform_later(@inbox.channel)
elsif @inbox.twilio? && @inbox.channel.whatsapp?
Channels::Twilio::TemplatesSyncJob.perform_later(@inbox.channel)
end
end
end end
Api::V1::Accounts::InboxesController.prepend_mod_with('Api::V1::Accounts::InboxesController') Api::V1::Accounts::InboxesController.prepend_mod_with('Api::V1::Accounts::InboxesController')
@@ -1,6 +1,7 @@
class Api::V1::Accounts::Instagram::AuthorizationsController < Api::V1::Accounts::OauthAuthorizationController class Api::V1::Accounts::Instagram::AuthorizationsController < Api::V1::Accounts::BaseController
include InstagramConcern include InstagramConcern
include Instagram::IntegrationHelper include Instagram::IntegrationHelper
before_action :check_authorization
def create def create
# https://developers.facebook.com/docs/instagram-platform/instagram-api-with-instagram-login/business-login#step-1--get-authorization # https://developers.facebook.com/docs/instagram-platform/instagram-api-with-instagram-login/business-login#step-1--get-authorization
@@ -20,4 +21,10 @@ class Api::V1::Accounts::Instagram::AuthorizationsController < Api::V1::Accounts
render json: { success: false }, status: :unprocessable_entity render json: { success: false }, status: :unprocessable_entity
end end
end end
private
def check_authorization
raise Pundit::NotAuthorizedError unless Current.account_user.administrator?
end
end end
@@ -22,7 +22,7 @@ class Api::V1::Accounts::Integrations::DyteController < Api::V1::Accounts::BaseC
private private
def authorize_request def authorize_request
authorize @conversation, :show? authorize @conversation.inbox, :show?
end end
def render_response(response) def render_response(response)
@@ -1,155 +0,0 @@
class Api::V1::Accounts::Integrations::GithubController < Api::V1::Accounts::BaseController
before_action :fetch_hook
before_action :ensure_hook_exists, except: [:destroy]
def destroy
@hook.destroy!
head :ok
end
def repositories
repositories = github_service.repositories
filtered_repos = repositories.map do |repo|
{
full_name: repo[:full_name] || repo.full_name,
private: repo[:private] || repo.private
}
end
render json: filtered_repos
end
def search_repositories
repositories = github_service.search_repositories(params[:q])
filtered_repos = repositories.map do |repo|
{
full_name: repo[:full_name] || repo.full_name,
private: repo[:private] || repo.private
}
end
render json: filtered_repos
end
def assignees
assignees = github_service.assignees(repo_full_name)
filtered_assignees = assignees.map do |assignee|
{
login: assignee.login,
avatar_url: assignee.avatar_url
}
end
render json: filtered_assignees
end
def labels
labels = github_service.labels(repo_full_name)
filtered_labels = labels.map do |label|
{
name: label.name,
color: label.color
}
end
render json: filtered_labels
end
def search_issues
issues = github_service.search_issues(repo_full_name, params[:q])
filtered_issues = issues.map do |issue|
{
number: issue.number,
title: issue.title,
html_url: issue.html_url
}
end
render json: filtered_issues
end
def create_issue
issue_data = github_service.create_issue(
repo_full_name,
params[:title],
params[:body],
issue_options
)
github_issue = create_linked_issue(issue_data)
render json: issue_response(github_issue), status: :created
end
def link_issue
issue_data = github_service.issue(repo_full_name, params[:issue_number])
github_issue = create_linked_issue(issue_data)
render json: issue_response(github_issue), status: :created
end
def linked_issues
issues = GithubIssue.where(conversation_id: params[:conversation_id])
render json: issues.map do |issue|
{
id: issue.id,
issue_number: issue.issue_number,
title: issue.issue_title,
html_url: issue.html_url,
repo_full_name: issue.repo_full_name,
linked_by: {
id: issue.linked_by.id,
name: issue.linked_by.name
},
created_at: issue.created_at
}
end
end
def unlink_issue
github_issue = GithubIssue.find(params[:id])
github_issue.destroy!
head :ok
end
private
def fetch_hook
@hook = Integrations::Hook.where(account: Current.account).find_by(app_id: 'github')
end
def ensure_hook_exists
render json: { error: 'GitHub integration not configured' }, status: :unauthorized unless @hook
end
def github_service
@github_service ||= Github::GithubService.new(hook: @hook)
end
def repo_full_name
params[:repo_full_name] || "#{params[:owner]}/#{params[:repo]}"
end
def issue_options
options = {}
options[:assignees] = params[:assignees] if params[:assignees].present?
options[:labels] = params[:labels] if params[:labels].present?
options
end
def create_linked_issue(issue_data)
GithubIssue.create!(
conversation_id: params[:conversation_id],
account: Current.account,
repo_full_name: repo_full_name,
issue_number: issue_data.number,
issue_title: issue_data.title,
html_url: issue_data.html_url,
linked_by: Current.user
)
end
def issue_response(github_issue)
{
id: github_issue.id,
issue_number: github_issue.issue_number,
title: github_issue.issue_title,
html_url: github_issue.html_url,
repo_full_name: github_issue.repo_full_name
}
end
end
@@ -1,9 +1,8 @@
class Api::V1::Accounts::Integrations::LinearController < Api::V1::Accounts::BaseController class Api::V1::Accounts::Integrations::LinearController < Api::V1::Accounts::BaseController
before_action :fetch_conversation, only: [:create_issue, :link_issue, :unlink_issue, :linked_issues] before_action :fetch_conversation, only: [:link_issue, :linked_issues]
before_action :fetch_hook, only: [:destroy] before_action :fetch_hook, only: [:destroy]
def destroy def destroy
revoke_linear_token
@hook.destroy! @hook.destroy!
head :ok head :ok
end end
@@ -28,16 +27,10 @@ class Api::V1::Accounts::Integrations::LinearController < Api::V1::Accounts::Bas
end end
def create_issue def create_issue
issue = linear_processor_service.create_issue(permitted_params, Current.user) issue = linear_processor_service.create_issue(permitted_params)
if issue[:error] if issue[:error]
render json: { error: issue[:error] }, status: :unprocessable_entity render json: { error: issue[:error] }, status: :unprocessable_entity
else else
Linear::ActivityMessageService.new(
conversation: @conversation,
action_type: :issue_created,
issue_data: { id: issue[:data][:identifier] },
user: Current.user
).perform
render json: issue[:data], status: :ok render json: issue[:data], status: :ok
end end
end end
@@ -45,34 +38,21 @@ class Api::V1::Accounts::Integrations::LinearController < Api::V1::Accounts::Bas
def link_issue def link_issue
issue_id = permitted_params[:issue_id] issue_id = permitted_params[:issue_id]
title = permitted_params[:title] title = permitted_params[:title]
issue = linear_processor_service.link_issue(conversation_link, issue_id, title, Current.user) issue = linear_processor_service.link_issue(conversation_link, issue_id, title)
if issue[:error] if issue[:error]
render json: { error: issue[:error] }, status: :unprocessable_entity render json: { error: issue[:error] }, status: :unprocessable_entity
else else
Linear::ActivityMessageService.new(
conversation: @conversation,
action_type: :issue_linked,
issue_data: { id: issue_id },
user: Current.user
).perform
render json: issue[:data], status: :ok render json: issue[:data], status: :ok
end end
end end
def unlink_issue def unlink_issue
link_id = permitted_params[:link_id] link_id = permitted_params[:link_id]
issue_id = permitted_params[:issue_id]
issue = linear_processor_service.unlink_issue(link_id) issue = linear_processor_service.unlink_issue(link_id)
if issue[:error] if issue[:error]
render json: { error: issue[:error] }, status: :unprocessable_entity render json: { error: issue[:error] }, status: :unprocessable_entity
else else
Linear::ActivityMessageService.new(
conversation: @conversation,
action_type: :issue_unlinked,
issue_data: { id: issue_id },
user: Current.user
).perform
render json: issue[:data], status: :ok render json: issue[:data], status: :ok
end end
end end
@@ -121,15 +101,4 @@ class Api::V1::Accounts::Integrations::LinearController < Api::V1::Accounts::Bas
def fetch_hook def fetch_hook
@hook = Integrations::Hook.where(account: Current.account).find_by(app_id: 'linear') @hook = Integrations::Hook.where(account: Current.account).find_by(app_id: 'linear')
end end
def revoke_linear_token
return unless @hook&.access_token
begin
linear_client = Linear.new(@hook.access_token)
linear_client.revoke_token
rescue StandardError => e
Rails.logger.error "Failed to revoke Linear token: #{e.message}"
end
end
end end
@@ -1,14 +0,0 @@
class Api::V1::Accounts::Integrations::NotionController < Api::V1::Accounts::BaseController
before_action :fetch_hook, only: [:destroy]
def destroy
@hook.destroy!
head :ok
end
private
def fetch_hook
@hook = Integrations::Hook.where(account: Current.account).find_by(app_id: 'notion')
end
end
@@ -1,19 +1,28 @@
class Api::V1::Accounts::Microsoft::AuthorizationsController < Api::V1::Accounts::OauthAuthorizationController class Api::V1::Accounts::Microsoft::AuthorizationsController < Api::V1::Accounts::BaseController
include MicrosoftConcern include MicrosoftConcern
before_action :check_authorization
def create def create
email = params[:authorization][:email]
redirect_url = microsoft_client.auth_code.authorize_url( redirect_url = microsoft_client.auth_code.authorize_url(
{ {
redirect_uri: "#{base_url}/microsoft/callback", redirect_uri: "#{base_url}/microsoft/callback",
scope: scope, scope: 'offline_access https://outlook.office.com/IMAP.AccessAsUser.All https://outlook.office.com/SMTP.Send openid profile',
state: state,
prompt: 'consent' prompt: 'consent'
} }
) )
if redirect_url if redirect_url
cache_key = "microsoft::#{email.downcase}"
::Redis::Alfred.setex(cache_key, Current.account.id, 5.minutes)
render json: { success: true, url: redirect_url } render json: { success: true, url: redirect_url }
else else
render json: { success: false }, status: :unprocessable_entity render json: { success: false }, status: :unprocessable_entity
end end
end end
private
def check_authorization
raise Pundit::NotAuthorizedError unless Current.account_user.administrator?
end
end end
@@ -1,21 +0,0 @@
class Api::V1::Accounts::Notion::AuthorizationsController < Api::V1::Accounts::OauthAuthorizationController
include NotionConcern
def create
redirect_url = notion_client.auth_code.authorize_url(
{
redirect_uri: "#{base_url}/notion/callback",
response_type: 'code',
owner: 'user',
state: state,
client_id: GlobalConfigService.load('NOTION_CLIENT_ID', nil)
}
)
if redirect_url
render json: { success: true, url: redirect_url }
else
render json: { success: false }, status: :unprocessable_entity
end
end
end
@@ -1,23 +0,0 @@
class Api::V1::Accounts::OauthAuthorizationController < Api::V1::Accounts::BaseController
before_action :check_authorization
protected
def scope
''
end
def state
Current.account.to_sgid(expires_in: 15.minutes).to_s
end
def base_url
ENV.fetch('FRONTEND_URL', 'http://localhost:3000')
end
private
def check_authorization
raise Pundit::NotAuthorizedError unless Current.account_user.administrator?
end
end
@@ -26,8 +26,9 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
@portal.update!(portal_params.merge(live_chat_widget_params)) if params[:portal].present? @portal.update!(portal_params.merge(live_chat_widget_params)) if params[:portal].present?
# @portal.custom_domain = parsed_custom_domain # @portal.custom_domain = parsed_custom_domain
process_attached_logo if params[:blob_id].present? process_attached_logo if params[:blob_id].present?
rescue ActiveRecord::RecordInvalid => e rescue StandardError => e
render_record_invalid(e) Rails.logger.error e
render json: { error: @portal.errors.messages }.to_json, status: :unprocessable_entity
end end
end end
@@ -46,20 +47,6 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
head :ok head :ok
end end
def send_instructions
email = permitted_params[:email]
return render_could_not_create_error(I18n.t('portals.send_instructions.email_required')) if email.blank?
return render_could_not_create_error(I18n.t('portals.send_instructions.invalid_email_format')) unless valid_email?(email)
return render_could_not_create_error(I18n.t('portals.send_instructions.custom_domain_not_configured')) if @portal.custom_domain.blank?
PortalInstructionsMailer.send_cname_instructions(
portal: @portal,
recipient_email: email
).deliver_later
render json: { message: I18n.t('portals.send_instructions.instructions_sent_successfully') }, status: :ok
end
def process_attached_logo def process_attached_logo
blob_id = params[:blob_id] blob_id = params[:blob_id]
blob = ActiveStorage::Blob.find_by(id: blob_id) blob = ActiveStorage::Blob.find_by(id: blob_id)
@@ -73,20 +60,19 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
end end
def permitted_params def permitted_params
params.permit(:id, :email) params.permit(:id)
end end
def portal_params def portal_params
params.require(:portal).permit( params.require(:portal).permit(
:id, :account_id, :color, :custom_domain, :header_text, :homepage_link, :account_id, :color, :custom_domain, :header_text, :homepage_link,
:name, :page_title, :slug, :archived, { config: [:default_locale, { allowed_locales: [] }] } :name, :page_title, :slug, :archived, { config: [:default_locale, { allowed_locales: [] }] }
) )
end end
def live_chat_widget_params def live_chat_widget_params
permitted_params = params.permit(:inbox_id) permitted_params = params.permit(:inbox_id)
return {} unless permitted_params.key?(:inbox_id) return {} if permitted_params[:inbox_id].blank?
return { channel_web_widget_id: nil } if permitted_params[:inbox_id].blank?
inbox = Inbox.find(permitted_params[:inbox_id]) inbox = Inbox.find(permitted_params[:inbox_id])
return {} unless inbox.web_widget? return {} unless inbox.web_widget?
@@ -102,10 +88,4 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
domain = URI.parse(@portal.custom_domain) domain = URI.parse(@portal.custom_domain)
domain.is_a?(URI::HTTP) ? domain.host : @portal.custom_domain domain.is_a?(URI::HTTP) ? domain.host : @portal.custom_domain
end end
def valid_email?(email)
ValidEmail2::Address.new(email).valid?
end
end end
Api::V1::Accounts::PortalsController.prepend_mod_with('Api::V1::Accounts::PortalsController')
@@ -15,10 +15,6 @@ class Api::V1::Accounts::SearchController < Api::V1::Accounts::BaseController
@result = search('Message') @result = search('Message')
end end
def articles
@result = search('Article')
end
private private
def search(search_type) def search(search_type)
@@ -1,77 +0,0 @@
class Api::V1::Accounts::Whatsapp::AuthorizationsController < Api::V1::Accounts::BaseController
before_action :fetch_and_validate_inbox, if: -> { params[:inbox_id].present? }
# POST /api/v1/accounts/:account_id/whatsapp/authorization
# Handles both initial authorization and reauthorization
# If inbox_id is present in params, it performs reauthorization
def create
validate_embedded_signup_params!
channel = process_embedded_signup
render_success_response(channel.inbox)
rescue StandardError => e
render_error_response(e)
end
private
def process_embedded_signup
service = Whatsapp::EmbeddedSignupService.new(
account: Current.account,
params: params.permit(:code, :business_id, :waba_id, :phone_number_id).to_h.symbolize_keys,
inbox_id: params[:inbox_id]
)
service.perform
end
def fetch_and_validate_inbox
@inbox = Current.account.inboxes.find(params[:inbox_id])
validate_reauthorization_required
end
def validate_reauthorization_required
return if @inbox.channel.reauthorization_required? || can_upgrade_to_embedded_signup?
render json: {
success: false,
message: I18n.t('inbox.reauthorization.not_required')
}, status: :unprocessable_entity
end
def can_upgrade_to_embedded_signup?
channel = @inbox.channel
return false unless channel.provider == 'whatsapp_cloud'
true
end
def render_success_response(inbox)
response = {
success: true,
id: inbox.id,
name: inbox.name,
channel_type: 'whatsapp'
}
response[:message] = I18n.t('inbox.reauthorization.success') if params[:inbox_id].present?
render json: response
end
def render_error_response(error)
Rails.logger.error "[WHATSAPP AUTHORIZATION] Embedded signup error: #{error.message}"
Rails.logger.error error.backtrace.join("\n")
render json: {
success: false,
error: error.message
}, status: :unprocessable_entity
end
def validate_embedded_signup_params!
missing_params = []
missing_params << 'code' if params[:code].blank?
missing_params << 'business_id' if params[:business_id].blank?
missing_params << 'waba_id' if params[:waba_id].blank?
return if missing_params.empty?
raise ArgumentError, "Required parameters are missing: #{missing_params.join(', ')}"
end
end
@@ -92,7 +92,7 @@ class Api::V1::AccountsController < Api::BaseController
end end
def settings_params def settings_params
params.permit(:auto_resolve_after, :auto_resolve_message, :auto_resolve_ignore_waiting, :audio_transcriptions, :auto_resolve_label) params.permit(:auto_resolve_after, :auto_resolve_message, :auto_resolve_ignore_waiting)
end end
def check_signup_enabled def check_signup_enabled
@@ -1,68 +0,0 @@
class Api::V1::Profile::MfaController < Api::BaseController
before_action :check_mfa_feature_available
before_action :check_mfa_enabled, only: [:destroy, :backup_codes]
before_action :check_mfa_disabled, only: [:create, :verify]
before_action :validate_otp, only: [:verify, :backup_codes, :destroy]
before_action :validate_password, only: [:destroy]
def show; end
def create
mfa_service.enable_two_factor!
end
def verify
@backup_codes = mfa_service.verify_and_activate!
end
def destroy
mfa_service.disable_two_factor!
end
def backup_codes
@backup_codes = mfa_service.generate_backup_codes!
end
private
def mfa_service
@mfa_service ||= Mfa::ManagementService.new(user: current_user)
end
def check_mfa_enabled
render_could_not_create_error(I18n.t('errors.mfa.not_enabled')) unless current_user.mfa_enabled?
end
def check_mfa_feature_available
return if Chatwoot.mfa_enabled?
render json: {
error: I18n.t('errors.mfa.feature_unavailable')
}, status: :forbidden
end
def check_mfa_disabled
render_could_not_create_error(I18n.t('errors.mfa.already_enabled')) if current_user.mfa_enabled?
end
def validate_otp
authenticated = Mfa::AuthenticationService.new(
user: current_user,
otp_code: mfa_params[:otp_code]
).authenticate
return if authenticated
render_could_not_create_error(I18n.t('errors.mfa.invalid_code'))
end
def validate_password
return if current_user.valid_password?(mfa_params[:password])
render_could_not_create_error(I18n.t('errors.mfa.invalid_credentials'))
end
def mfa_params
params.permit(:otp_code, :password)
end
end
@@ -38,11 +38,6 @@ class Api::V1::ProfilesController < Api::BaseController
head :ok head :ok
end end
def reset_access_token
@user.access_token.regenerate_token
@user.reload
end
private private
def set_user def set_user
@@ -9,7 +9,7 @@ class Api::V1::Widget::ConfigsController < Api::V1::Widget::BaseController
private private
def set_global_config def set_global_config
@global_config = GlobalConfig.get('LOGO_THUMBNAIL', 'BRAND_NAME', 'WIDGET_BRAND_URL', 'INSTALLATION_NAME') @global_config = GlobalConfig.get('LOGO_THUMBNAIL', 'BRAND_NAME', 'WIDGET_BRAND_URL')
end end
def set_contact def set_contact
@@ -1,6 +1,6 @@
class Api::V2::Accounts::SummaryReportsController < Api::V1::Accounts::BaseController class Api::V2::Accounts::SummaryReportsController < Api::V1::Accounts::BaseController
before_action :check_authorization before_action :check_authorization
before_action :prepare_builder_params, only: [:agent, :team, :inbox, :label] before_action :prepare_builder_params, only: [:agent, :team, :inbox]
def agent def agent
render_report_with(V2::Reports::AgentSummaryBuilder) render_report_with(V2::Reports::AgentSummaryBuilder)
@@ -14,10 +14,6 @@ class Api::V2::Accounts::SummaryReportsController < Api::V1::Accounts::BaseContr
render_report_with(V2::Reports::InboxSummaryBuilder) render_report_with(V2::Reports::InboxSummaryBuilder)
end end
def label
render_report_with(V2::Reports::LabelSummaryBuilder)
end
private private
def check_authorization def check_authorization
@@ -14,7 +14,6 @@ module AccessTokenAuthHelper
ensure_access_token ensure_access_token
render_unauthorized('Invalid Access Token') && return if @access_token.blank? render_unauthorized('Invalid Access Token') && return if @access_token.blank?
# NOTE: This ensures that current_user is set and available for the rest of the controller actions
@resource = @access_token.owner @resource = @access_token.owner
Current.user = @resource if allowed_current_user_type?(@resource) Current.user = @resource if allowed_current_user_type?(@resource)
end end
+2 -2
View File
@@ -14,7 +14,7 @@ module GoogleConcern
private private
def scope def base_url
'email profile https://mail.google.com/' ENV.fetch('FRONTEND_URL', 'http://localhost:3000')
end end
end end
@@ -15,7 +15,7 @@ module MicrosoftConcern
private private
def scope def base_url
'offline_access https://outlook.office.com/IMAP.AccessAsUser.All https://outlook.office.com/SMTP.Send openid profile email' ENV.fetch('FRONTEND_URL', 'http://localhost:3000')
end end
end end
@@ -1,21 +0,0 @@
module NotionConcern
extend ActiveSupport::Concern
def notion_client
app_id = GlobalConfigService.load('NOTION_CLIENT_ID', nil)
app_secret = GlobalConfigService.load('NOTION_CLIENT_SECRET', nil)
::OAuth2::Client.new(app_id, app_secret, {
site: 'https://api.notion.com',
authorize_url: 'https://api.notion.com/v1/oauth/authorize',
token_url: 'https://api.notion.com/v1/oauth/token',
auth_scheme: :basic_auth
})
end
private
def scope
''
end
end
+2 -19
View File
@@ -4,28 +4,17 @@ module SwitchLocale
private private
def switch_locale(&) def switch_locale(&)
# Priority is for locale set in query string (mostly for widget/from js sdk) # priority is for locale set in query string (mostly for widget/from js sdk)
locale ||= params[:locale] locale ||= params[:locale]
# Use the user's locale if available
locale ||= locale_from_user
# Use the locale from a custom domain if applicable
locale ||= locale_from_custom_domain locale ||= locale_from_custom_domain
# if locale is not set in account, let's use DEFAULT_LOCALE env variable # if locale is not set in account, let's use DEFAULT_LOCALE env variable
locale ||= ENV.fetch('DEFAULT_LOCALE', nil) locale ||= ENV.fetch('DEFAULT_LOCALE', nil)
set_locale(locale, &) set_locale(locale, &)
end end
def switch_locale_using_account_locale(&) def switch_locale_using_account_locale(&)
# Get the locale from the user first locale = locale_from_account(@current_account)
locale = locale_from_user
# Fallback to the account's locale if the user's locale is not set
locale ||= locale_from_account(@current_account)
set_locale(locale, &) set_locale(locale, &)
end end
@@ -43,12 +32,6 @@ module SwitchLocale
@portal.default_locale @portal.default_locale
end end
def locale_from_user
return unless @user
@user.ui_settings&.dig('locale')
end
def set_locale(locale, &) def set_locale(locale, &)
safe_locale = validate_and_get_locale(locale) safe_locale = validate_and_get_locale(locale)
# Ensure locale won't bleed into other requests # Ensure locale won't bleed into other requests
+2 -4
View File
@@ -15,7 +15,7 @@ class DashboardController < ActionController::Base
private private
def ensure_html_format def ensure_html_format
render json: { error: 'Please use API routes instead of dashboard routes for JSON requests' }, status: :not_acceptable if request.format.json? head :not_acceptable unless request.format.html?
end end
def set_global_config def set_global_config
@@ -66,9 +66,7 @@ class DashboardController < ActionController::Base
ENABLE_ACCOUNT_SIGNUP: GlobalConfigService.load('ENABLE_ACCOUNT_SIGNUP', 'false'), ENABLE_ACCOUNT_SIGNUP: GlobalConfigService.load('ENABLE_ACCOUNT_SIGNUP', 'false'),
FB_APP_ID: GlobalConfigService.load('FB_APP_ID', ''), FB_APP_ID: GlobalConfigService.load('FB_APP_ID', ''),
INSTAGRAM_APP_ID: GlobalConfigService.load('INSTAGRAM_APP_ID', ''), INSTAGRAM_APP_ID: GlobalConfigService.load('INSTAGRAM_APP_ID', ''),
FACEBOOK_API_VERSION: GlobalConfigService.load('FACEBOOK_API_VERSION', 'v18.0'), FACEBOOK_API_VERSION: GlobalConfigService.load('FACEBOOK_API_VERSION', 'v17.0'),
WHATSAPP_APP_ID: GlobalConfigService.load('WHATSAPP_APP_ID', ''),
WHATSAPP_CONFIGURATION_ID: GlobalConfigService.load('WHATSAPP_CONFIGURATION_ID', ''),
IS_ENTERPRISE: ChatwootApp.enterprise?, IS_ENTERPRISE: ChatwootApp.enterprise?,
AZURE_APP_ID: GlobalConfigService.load('AZURE_APP_ID', ''), AZURE_APP_ID: GlobalConfigService.load('AZURE_APP_ID', ''),
GIT_SHA: GIT_HASH GIT_SHA: GIT_HASH
@@ -19,19 +19,6 @@ class DeviseOverrides::OmniauthCallbacksController < DeviseTokenAuth::OmniauthCa
redirect_to login_page_url(email: encoded_email, sso_auth_token: @resource.generate_sso_auth_token) redirect_to login_page_url(email: encoded_email, sso_auth_token: @resource.generate_sso_auth_token)
end end
def sign_in_user_on_mobile
@resource.skip_confirmation! if confirmable_enabled?
# once the resource is found and verified
# we can just send them to the login page again with the SSO params
# that will log them in
encoded_email = ERB::Util.url_encode(@resource.email)
params = { email: encoded_email, sso_auth_token: @resource.generate_sso_auth_token }.to_query
mobile_deep_link_base = GlobalConfigService.load('MOBILE_DEEP_LINK_BASE', 'chatwootapp')
redirect_to "#{mobile_deep_link_base}://auth/saml?#{params}", allow_other_host: true
end
def sign_up_user def sign_up_user
return redirect_to login_page_url(error: 'no-account-found') unless account_signup_allowed? return redirect_to login_page_url(error: 'no-account-found') unless account_signup_allowed?
return redirect_to login_page_url(error: 'business-account-only') unless validate_signup_email_is_business_domain? return redirect_to login_page_url(error: 'business-account-only') unless validate_signup_email_is_business_domain?
@@ -60,8 +47,10 @@ class DeviseOverrides::OmniauthCallbacksController < DeviseTokenAuth::OmniauthCa
end end
def get_resource_from_auth_hash # rubocop:disable Naming/AccessorMethodName def get_resource_from_auth_hash # rubocop:disable Naming/AccessorMethodName
email = auth_hash.dig('info', 'email') # find the user with their email instead of UID and token
@resource = resource_class.from_email(email) @resource = resource_class.where(
email: auth_hash['info']['email']
).first
end end
def validate_signup_email_is_business_domain? def validate_signup_email_is_business_domain?
@@ -86,5 +75,3 @@ class DeviseOverrides::OmniauthCallbacksController < DeviseTokenAuth::OmniauthCa
'user' 'user'
end end
end end
DeviseOverrides::OmniauthCallbacksController.prepend_mod_with('DeviseOverrides::OmniauthCallbacksController')
@@ -44,5 +44,3 @@ class DeviseOverrides::PasswordsController < Devise::PasswordsController
}, status: status }, status: status
end end
end end
DeviseOverrides::PasswordsController.prepend_mod_with('DeviseOverrides::PasswordsController')
@@ -9,14 +9,14 @@ class DeviseOverrides::SessionsController < DeviseTokenAuth::SessionsController
end end
def create def create
return handle_mfa_verification if mfa_verification_request? # Authenticate user via the temporary sso auth token
return handle_sso_authentication if sso_authentication_request? if params[:sso_auth_token].present? && @resource.present?
authenticate_resource_with_sso_token
user = find_user_for_authentication yield @resource if block_given?
return handle_mfa_required(user) if user&.mfa_enabled? render_create_success
else
# Only proceed with standard authentication if no MFA is required super
super end
end end
def render_create_success def render_create_success
@@ -25,31 +25,6 @@ class DeviseOverrides::SessionsController < DeviseTokenAuth::SessionsController
private private
def find_user_for_authentication
return nil unless params[:email].present? && params[:password].present?
normalized_email = params[:email].strip.downcase
user = User.from_email(normalized_email)
return nil unless user&.valid_password?(params[:password])
return nil unless user.active_for_authentication?
user
end
def mfa_verification_request?
params[:mfa_token].present?
end
def sso_authentication_request?
params[:sso_auth_token].present? && @resource.present?
end
def handle_sso_authentication
authenticate_resource_with_sso_token
yield @resource if block_given?
render_create_success
end
def login_page_url(error: nil) def login_page_url(error: nil)
frontend_url = ENV.fetch('FRONTEND_URL', nil) frontend_url = ENV.fetch('FRONTEND_URL', nil)
@@ -71,41 +46,6 @@ class DeviseOverrides::SessionsController < DeviseTokenAuth::SessionsController
user = User.from_email(params[:email]) user = User.from_email(params[:email])
@resource = user if user&.valid_sso_auth_token?(params[:sso_auth_token]) @resource = user if user&.valid_sso_auth_token?(params[:sso_auth_token])
end end
def handle_mfa_required(user)
render json: {
mfa_required: true,
mfa_token: Mfa::TokenService.new(user: user).generate_token
}, status: :partial_content
end
def handle_mfa_verification
user = Mfa::TokenService.new(token: params[:mfa_token]).verify_token
return render_mfa_error('errors.mfa.invalid_token', :unauthorized) unless user
authenticated = Mfa::AuthenticationService.new(
user: user,
otp_code: params[:otp_code],
backup_code: params[:backup_code]
).authenticate
return render_mfa_error('errors.mfa.invalid_code') unless authenticated
sign_in_mfa_user(user)
end
def sign_in_mfa_user(user)
@resource = user
@token = @resource.create_token
@resource.save!
sign_in(:user, @resource, store: false, bypass: false)
render_create_success
end
def render_mfa_error(message_key, status = :bad_request)
render json: { error: I18n.t(message_key) }, status: status
end
end end
DeviseOverrides::SessionsController.prepend_mod_with('DeviseOverrides::SessionsController') DeviseOverrides::SessionsController.prepend_mod_with('DeviseOverrides::SessionsController')
@@ -1,160 +0,0 @@
class Github::CallbacksController < ApplicationController
include Github::IntegrationHelper
def show
# Validate account context early for all flows that require it
account if params[:code].present?
if params[:installation_id].present? && params[:code].present?
# Both installation and OAuth code present - handle both
handle_installation_with_oauth
elsif params[:installation_id].present?
# Only installation_id present - redirect to OAuth
handle_installation
else
# Only OAuth code present - handle authorization
handle_authorization
end
rescue StandardError => e
Rails.logger.error("Github callback error: #{e.message}")
redirect_to fallback_redirect_uri
end
private
def handle_installation_with_oauth
# Handle both installation and OAuth in one go
installation_id = params[:installation_id]
@response = oauth_client.auth_code.get_token(
params[:code],
redirect_uri: "#{base_url}/github/callback"
)
handle_response(installation_id)
end
def handle_installation
if params[:setup_action] == 'install'
installation_id = params[:installation_id]
redirect_to build_oauth_url(installation_id)
else
Rails.logger.error("Unknown setup_action: #{params[:setup_action]}")
redirect_to github_integration_settings_url
end
end
def handle_authorization
@response = oauth_client.auth_code.get_token(
params[:code],
redirect_uri: "#{base_url}/github/callback"
)
handle_response
end
def build_oauth_url(installation_id)
GlobalConfigService.load('GITHUB_CLIENT_ID', nil)
# Store installation_id in session for later use
session[:github_installation_id] = installation_id
# For now, redirect to a page that will initiate OAuth with proper account context
# This is a temporary solution until we have a proper account-agnostic setup
"#{ENV.fetch('FRONTEND_URL', nil)}/app/accounts/1/settings/integrations/github?setup_action=install&installation_id=#{installation_id}"
end
def oauth_client
app_id = GlobalConfigService.load('GITHUB_CLIENT_ID', nil)
app_secret = GlobalConfigService.load('GITHUB_CLIENT_SECRET', nil)
OAuth2::Client.new(
app_id,
app_secret,
{
site: 'https://github.com',
token_url: '/login/oauth/access_token',
authorize_url: '/login/oauth/authorize'
}
)
end
def handle_response(installation_id = nil)
settings = build_hook_settings(installation_id)
hook = create_integration_hook(settings)
hook.save!
cleanup_session_data
redirect_to github_redirect_uri
rescue StandardError => e
Rails.logger.error("Github callback error: #{e.message}")
redirect_to fallback_redirect_uri
end
def build_hook_settings(installation_id)
settings = {
token_type: parsed_body['token_type'],
scope: parsed_body['scope']
}
settings[:installation_id] = installation_id || session[:github_installation_id]
settings.compact
end
def create_integration_hook(settings)
account.hooks.new(
access_token: parsed_body['access_token'],
status: 'enabled',
app_id: 'github',
settings: settings
)
end
def cleanup_session_data
session.delete(:github_installation_id)
end
def account
@account ||= account_from_state
end
def account_from_state
raise ActionController::BadRequest, 'Missing state variable' if params[:state].blank?
# Try signed GlobalID first (installation flow)
account = GlobalID::Locator.locate_signed(params[:state])
return account if account
# Fallback to JWT token (direct OAuth flow)
account_id = verify_github_token(params[:state])
return Account.find(account_id) if account_id
raise 'Invalid or expired state'
rescue StandardError
raise ActionController::BadRequest, 'Invalid account context'
end
def github_redirect_uri
"#{ENV.fetch('FRONTEND_URL', nil)}/app/accounts/#{account.id}/settings/integrations/github"
end
def github_integration_settings_url
"#{ENV.fetch('FRONTEND_URL', nil)}/app/accounts/1/settings/integrations/github"
end
def fallback_redirect_uri
github_redirect_uri
rescue StandardError
# Fallback if no account context available
"#{ENV.fetch('FRONTEND_URL', nil)}/app/settings/integrations"
end
def parsed_body
@parsed_body ||= @response.response.parsed
end
def base_url
ENV.fetch('FRONTEND_URL', 'http://localhost:3000')
end
end
+2 -2
View File
@@ -2,7 +2,7 @@ class MicrosoftController < ApplicationController
after_action :set_version_header after_action :set_version_header
def identity_association def identity_association
microsoft_identity microsoft_indentity
end end
private private
@@ -11,7 +11,7 @@ class MicrosoftController < ApplicationController
response.headers['Content-Length'] = { associatedApplications: [{ applicationId: @identity_json }] }.to_json.length response.headers['Content-Length'] = { associatedApplications: [{ applicationId: @identity_json }] }.to_json.length
end end
def microsoft_identity def microsoft_indentity
@identity_json = GlobalConfigService.load('AZURE_APP_ID', nil) @identity_json = GlobalConfigService.load('AZURE_APP_ID', nil)
end end
end end
@@ -1,36 +0,0 @@
class Notion::CallbacksController < OauthCallbackController
include NotionConcern
private
def provider_name
'notion'
end
def oauth_client
notion_client
end
def handle_response
hook = account.hooks.new(
access_token: parsed_body['access_token'],
status: 'enabled',
app_id: 'notion',
settings: {
token_type: parsed_body['token_type'],
workspace_name: parsed_body['workspace_name'],
workspace_id: parsed_body['workspace_id'],
workspace_icon: parsed_body['workspace_icon'],
bot_id: parsed_body['bot_id'],
owner: parsed_body['owner']
}
)
hook.save!
redirect_to notion_redirect_uri
end
def notion_redirect_uri
"#{ENV.fetch('FRONTEND_URL', nil)}/app/accounts/#{account.id}/settings/integrations/notion"
end
end
+8 -9
View File
@@ -6,6 +6,7 @@ class OauthCallbackController < ApplicationController
) )
handle_response handle_response
::Redis::Alfred.delete(cache_key)
rescue StandardError => e rescue StandardError => e
ChatwootExceptionTracker.new(e).capture_exception ChatwootExceptionTracker.new(e).capture_exception
redirect_to '/' redirect_to '/'
@@ -63,10 +64,13 @@ class OauthCallbackController < ApplicationController
raise NotImplementedError raise NotImplementedError
end end
def cache_key
"#{provider_name}::#{users_data['email'].downcase}"
end
def create_channel_with_inbox def create_channel_with_inbox
ActiveRecord::Base.transaction do ActiveRecord::Base.transaction do
channel_email = Channel::Email.create!(email: users_data['email'], account: account) channel_email = Channel::Email.create!(email: users_data['email'], account: account)
account.inboxes.create!( account.inboxes.create!(
account: account, account: account,
channel: channel_email, channel: channel_email,
@@ -81,17 +85,12 @@ class OauthCallbackController < ApplicationController
decoded_token[0] decoded_token[0]
end end
def account_from_signed_id def account_id
raise ActionController::BadRequest, 'Missing state variable' if params[:state].blank? ::Redis::Alfred.get(cache_key)
account = GlobalID::Locator.locate_signed(params[:state])
raise 'Invalid or expired state' if account.nil?
account
end end
def account def account
@account ||= account_from_signed_id @account ||= Account.find(account_id)
end end
# Fallback name, for when name field is missing from users_data # Fallback name, for when name field is missing from users_data
@@ -1,11 +1,4 @@
class Platform::Api::V1::AccountsController < PlatformController class Platform::Api::V1::AccountsController < PlatformController
def index
@resources = @platform_app.platform_app_permissibles
.where(permissible_type: 'Account')
.includes(:permissible)
.map(&:permissible)
end
def show; end def show; end
def create def create
@@ -1,9 +1,9 @@
class Platform::Api::V1::UsersController < PlatformController class Platform::Api::V1::UsersController < PlatformController
# ref: https://stackoverflow.com/a/45190318/939299 # ref: https://stackoverflow.com/a/45190318/939299
# set resource is called for other actions already in platform controller # set resource is called for other actions already in platform controller
# we want to add login and token to that chain as well # we want to add login to that chain as well
before_action(only: [:login, :token]) { set_resource } before_action(only: [:login]) { set_resource }
before_action(only: [:login, :token]) { validate_platform_app_permissible } before_action(only: [:login]) { validate_platform_app_permissible }
def show; end def show; end
@@ -18,8 +18,6 @@ class Platform::Api::V1::UsersController < PlatformController
render json: { url: @resource.generate_sso_link } render json: { url: @resource.generate_sso_link }
end end
def token; end
def update def update
@resource.assign_attributes(user_update_params) @resource.assign_attributes(user_update_params)
@@ -3,7 +3,7 @@ class Public::Api::V1::Inboxes::ConversationsController < Public::Api::V1::Inbox
before_action :set_conversation, only: [:toggle_typing, :update_last_seen, :show, :toggle_status] before_action :set_conversation, only: [:toggle_typing, :update_last_seen, :show, :toggle_status]
def index def index
@conversations = @contact_inbox.hmac_verified? ? @contact_inbox.contact.conversations : @contact_inbox.conversations @conversations = @contact_inbox.hmac_verified? ? @contact.conversations : @contact_inbox.conversations
end end
def show; end def show; end
@@ -1,40 +1,19 @@
class Public::Api::V1::Portals::ArticlesController < Public::Api::V1::Portals::BaseController class Public::Api::V1::Portals::ArticlesController < Public::Api::V1::Portals::BaseController
before_action :ensure_custom_domain_request, only: [:show, :index] before_action :ensure_custom_domain_request, only: [:show, :index]
before_action :portal before_action :portal
before_action :set_category, except: [:index, :show, :tracking_pixel] before_action :set_category, except: [:index, :show]
before_action :set_article, only: [:show] before_action :set_article, only: [:show]
layout 'portal' layout 'portal'
def index def index
@articles = @portal.articles.published.includes(:category, :author) @articles = @portal.articles.published.includes(:category, :author)
@articles = @articles.where(locale: permitted_params[:locale]) if permitted_params[:locale].present?
@articles_count = @articles.count @articles_count = @articles.count
search_articles search_articles
order_by_sort_param order_by_sort_param
limit_results limit_results
end end
def show def show; end
@og_image_url = helpers.set_og_image_url(@portal.name, @article.title)
end
def tracking_pixel
@article = @portal.articles.find_by(slug: permitted_params[:article_slug])
return head :not_found unless @article
@article.increment_view_count if @article.published?
# Serve the 1x1 tracking pixel with 24-hour private cache
# Private cache bypasses CDN but allows browser caching to prevent duplicate views from same user
expires_in 24.hours, public: false
response.headers['Content-Type'] = 'image/png'
pixel_path = Rails.public_path.join('assets/images/tracking-pixel.png')
send_file pixel_path, type: 'image/png', disposition: 'inline'
end
private private
@@ -60,6 +39,7 @@ class Public::Api::V1::Portals::ArticlesController < Public::Api::V1::Portals::B
def set_article def set_article
@article = @portal.articles.find_by(slug: permitted_params[:article_slug]) @article = @portal.articles.find_by(slug: permitted_params[:article_slug])
@article.increment_view_count if @article.published?
@parsed_content = render_article_content(@article.content) @parsed_content = render_article_content(@article.content)
end end
@@ -58,6 +58,6 @@ class Public::Api::V1::Portals::BaseController < PublicController
end end
def set_global_config def set_global_config
@global_config = GlobalConfig.get('LOGO_THUMBNAIL', 'BRAND_NAME', 'BRAND_URL', 'INSTALLATION_NAME') @global_config = GlobalConfig.get('LOGO_THUMBNAIL', 'BRAND_NAME', 'BRAND_URL')
end end
end end
@@ -8,9 +8,7 @@ class Public::Api::V1::Portals::CategoriesController < Public::Api::V1::Portals:
@categories = @portal.categories.order(position: :asc) @categories = @portal.categories.order(position: :asc)
end end
def show def show; end
@og_image_url = helpers.set_og_image_url(@portal.name, @category.name)
end
private private
@@ -4,9 +4,7 @@ class Public::Api::V1::PortalsController < Public::Api::V1::Portals::BaseControl
before_action :redirect_to_portal_with_locale, only: [:show] before_action :redirect_to_portal_with_locale, only: [:show]
layout 'portal' layout 'portal'
def show def show; end
@og_image_url = helpers.set_og_image_url('', @portal.header_text)
end
def sitemap def sitemap
@help_center_url = @portal.custom_domain || ChatwootApp.help_center_root @help_center_url = @portal.custom_domain || ChatwootApp.help_center_root
+1 -6
View File
@@ -17,12 +17,7 @@ class SlackUploadsController < ApplicationController
end end
def blob_url def blob_url
# Only generate representations for images url_for(@blob.representation(resize_to_fill: [250, nil]))
if @blob.content_type.start_with?('image/')
url_for(@blob.representation(resize_to_fill: [250, nil]))
else
url_for(@blob)
end
end end
def avatar_url def avatar_url
@@ -2,13 +2,6 @@ class SuperAdmin::AccountUsersController < SuperAdmin::ApplicationController
# Overwrite any of the RESTful controller actions to implement custom behavior # Overwrite any of the RESTful controller actions to implement custom behavior
# For example, you may want to send an email after a foo is updated. # For example, you may want to send an email after a foo is updated.
# #
# Since account/user page - account user role attribute links to the show page
# Handle with a redirect to the user show page
def show
redirect_to super_admin_user_path(requested_resource.user)
end
def create def create
resource = resource_class.new(resource_params) resource = resource_class.new(resource_params)
authorize_resource(resource) authorize_resource(resource)
@@ -32,21 +32,22 @@ class SuperAdmin::AppConfigsController < SuperAdmin::ApplicationController
end end
def allowed_configs def allowed_configs
mapping = { @allowed_configs = case @config
'facebook' => %w[FB_APP_ID FB_VERIFY_TOKEN FB_APP_SECRET IG_VERIFY_TOKEN FACEBOOK_API_VERSION ENABLE_MESSENGER_CHANNEL_HUMAN_AGENT], when 'facebook'
'shopify' => %w[SHOPIFY_CLIENT_ID SHOPIFY_CLIENT_SECRET], %w[FB_APP_ID FB_VERIFY_TOKEN FB_APP_SECRET IG_VERIFY_TOKEN FACEBOOK_API_VERSION ENABLE_MESSENGER_CHANNEL_HUMAN_AGENT]
'microsoft' => %w[AZURE_APP_ID AZURE_APP_SECRET], when 'shopify'
'email' => ['MAILER_INBOUND_EMAIL_DOMAIN'], %w[SHOPIFY_CLIENT_ID SHOPIFY_CLIENT_SECRET]
'linear' => %w[LINEAR_CLIENT_ID LINEAR_CLIENT_SECRET], when 'microsoft'
'slack' => %w[SLACK_CLIENT_ID SLACK_CLIENT_SECRET], %w[AZURE_APP_ID AZURE_APP_SECRET]
'instagram' => %w[INSTAGRAM_APP_ID INSTAGRAM_APP_SECRET INSTAGRAM_VERIFY_TOKEN INSTAGRAM_API_VERSION ENABLE_INSTAGRAM_CHANNEL_HUMAN_AGENT], when 'email'
'whatsapp_embedded' => %w[WHATSAPP_APP_ID WHATSAPP_APP_SECRET WHATSAPP_CONFIGURATION_ID WHATSAPP_API_VERSION], ['MAILER_INBOUND_EMAIL_DOMAIN']
'notion' => %w[NOTION_CLIENT_ID NOTION_CLIENT_SECRET], when 'linear'
'google' => %w[GOOGLE_OAUTH_CLIENT_ID GOOGLE_OAUTH_CLIENT_SECRET GOOGLE_OAUTH_REDIRECT_URI], %w[LINEAR_CLIENT_ID LINEAR_CLIENT_SECRET]
'github' => %w[GITHUB_CLIENT_ID GITHUB_CLIENT_SECRET] when 'instagram'
} %w[INSTAGRAM_APP_ID INSTAGRAM_APP_SECRET INSTAGRAM_VERIFY_TOKEN INSTAGRAM_API_VERSION ENABLE_INSTAGRAM_CHANNEL_HUMAN_AGENT]
else
@allowed_configs = mapping.fetch(@config, %w[ENABLE_ACCOUNT_SIGNUP FIREBASE_PROJECT_ID FIREBASE_CREDENTIALS]) %w[ENABLE_ACCOUNT_SIGNUP FIREBASE_PROJECT_ID FIREBASE_CREDENTIALS]
end
end end
end end
@@ -7,9 +7,8 @@
class SuperAdmin::ApplicationController < Administrate::ApplicationController class SuperAdmin::ApplicationController < Administrate::ApplicationController
include ActionView::Helpers::TagHelper include ActionView::Helpers::TagHelper
include ActionView::Context include ActionView::Context
include SuperAdmin::NavigationHelper
helper_method :render_vue_component, :settings_open?, :settings_pages helper_method :render_vue_component
# authenticiation done via devise : SuperAdmin Model # authenticiation done via devise : SuperAdmin Model
before_action :authenticate_super_admin! before_action :authenticate_super_admin!
@@ -13,11 +13,11 @@ class SuperAdmin::UsersController < SuperAdmin::ApplicationController
redirect_to new_super_admin_user_path, notice: notice redirect_to new_super_admin_user_path, notice: notice
end end
end end
#
def update # def update
requested_resource.skip_reconfirmation! if resource_params[:confirmed_at].present? # super
super # send_foo_updated_email(requested_resource)
end # end
# Override this method to specify custom lookup behavior. # Override this method to specify custom lookup behavior.
# This will be used to set the resource for the `show`, `edit`, and `update` # This will be used to set the resource for the `show`, `edit`, and `update`
@@ -5,6 +5,6 @@ class Survey::ResponsesController < ActionController::Base
private private
def set_global_config def set_global_config
@global_config = GlobalConfig.get('LOGO_THUMBNAIL', 'BRAND_NAME', 'WIDGET_BRAND_URL', 'INSTALLATION_NAME') @global_config = GlobalConfig.get('LOGO_THUMBNAIL', 'BRAND_NAME', 'WIDGET_BRAND_URL')
end end
end end

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