Compare commits

...
Author SHA1 Message Date
Shivam Mishra b545653a93 feat: update enums 2025-03-06 20:05:59 +05:30
Shivam Mishra 0f02455224 feat: remove redact contact job 2025-03-06 20:05:59 +05:30
Shivam Mishra 1c2c8f26fc feat: update controllers to create delete request 2025-03-06 20:05:59 +05:30
Shivam Mishra 82408a8e03 feat: add delete requests table 2025-03-06 20:05:59 +05:30
Shivam MishraandGitHub c17c9b10d9 Merge branch 'develop' into feat/meta-delete-request 2025-03-06 13:02:34 +05:30
Sivin VargheseandGitHub 7e1458fd32 fix: Issue when saving hotkeys (#11026)
# Pull Request Template

## Description

This PR fixes an issue when saving send message button hotkeys, where a
TypeError occurs: `this.updateUISettings is not a function`.

This issue arises after merging this PR
https://github.com/chatwoot/chatwoot/pull/10974

Fixes
https://chatwoot-p3.sentry.io/issues/6339976939/events/ca9946f92cb74428a72f1f74976a56a3/

## Type of change

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

## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
2025-03-06 10:53:02 +05:30
PranavandGitHub d017156f32 fix: Disable syncing IMAP if the account is suspended (#11031)
This PR disables the IMAP syncing if the account is suspended.
2025-03-05 17:10:24 -08:00
Muhsin KelothandGitHub e5d41f4b03 Merge branch 'develop' into feat/meta-delete-request 2025-03-04 21:20:12 +05:30
Shivam Mishra 2be2c9aa04 feat: throw malformed request for standard error 2025-03-04 16:32:18 +05:30
Shivam Mishra d9b86182f2 fix: default value 2025-03-04 16:30:31 +05:30
Shivam MishraandGitHub 79e46d1758 Merge branch 'develop' into feat/meta-delete-request 2025-03-04 15:21:49 +05:30
Shivam Mishra 5895d7b2bd test: redact contact job 2025-03-04 15:21:28 +05:30
Shivam Mishra bf5220e881 fix: fn names 2025-03-04 15:14:58 +05:30
Shivam Mishra 6bc29bfef2 fix: fn name 2025-03-04 15:10:25 +05:30
Shivam Mishra 9dddd1b020 refactor: move facebook redact job 2025-03-04 15:09:21 +05:30
Shivam Mishra bc294d8829 chore: add comment for response format 2025-03-04 15:02:09 +05:30
Shivam Mishra 4b243a44ee test: delete controller 2025-03-04 15:01:20 +05:30
Shivam Mishra f2961c4f8e fix: align to facebook spec 2025-03-04 15:01:07 +05:30
Shivam Mishra c4ea779759 test: confirm controller 2025-03-04 14:54:18 +05:30
Shivam Mishra cfb74b12f9 fix: function call in concern 2025-03-04 14:54:09 +05:30
Shivam Mishra 63bea5219b feat: use a shared concern 2025-03-04 14:45:15 +05:30
Shivam Mishra 74c611325f feat: don't remove inbox 2025-03-04 14:32:57 +05:30
Shivam Mishra 71ebacc46e refactor: rename meta to facebook 2025-03-03 17:53:31 +05:30
Shivam Mishra 770e395f17 feat: remove webhook route 2025-03-03 17:52:45 +05:30
Shivam Mishra 4a925619a1 refactor: render plain text 2025-03-03 17:51:09 +05:30
Shivam Mishra db537b5c86 feat: update confirmation url 2025-03-03 17:50:08 +05:30
Shivam Mishra cd1dbb67eb feat: update confirmation url 2025-03-03 17:43:48 +05:30
Shivam Mishra 114596db19 feat: move controller to facebook 2025-03-03 17:43:10 +05:30
Shivam Mishra 96a3999c8b feat: add meta deletion callback endpoint 2025-03-03 17:36:10 +05:30
14 changed files with 294 additions and 9 deletions
+1 -1
View File
@@ -561,7 +561,7 @@ GEM
activesupport (>= 3.0.0)
raabro (1.4.0)
racc (1.8.1)
rack (2.2.11)
rack (2.2.12)
rack-attack (6.7.0)
rack (>= 1.0, < 4)
rack-contrib (2.5.0)
@@ -0,0 +1,35 @@
module FacebookConcern
extend ActiveSupport::Concern
class InvalidDigestError < StandardError; end
private
def deletion_processed?(code)
request = DeleteRequest.find_by!(confirmation_code: code)
request.complete?
rescue ActiveRecord::RecordNotFound
false
end
def parse_fb_signed_request(signed_request)
encoded_signature, payload = signed_request.split('.', 2)
decoded_signature = Base64.urlsafe_decode64(encoded_signature)
decoded_payload = JSON.parse(Base64.urlsafe_decode64(payload))
expected_signature = OpenSSL::HMAC.digest('sha256', app_secret, payload)
raise InvalidDigestError if decoded_signature != expected_signature
decoded_payload
end
def app_url_base
ENV.fetch('FRONTEND_URL', nil)
end
def app_secret
GlobalConfigService.load('FB_APP_SECRET', '')
end
end
@@ -0,0 +1,11 @@
class Facebook::ConfirmController < ApplicationController
include FacebookConcern
def show
if deletion_processed?(params[:id])
render plain: 'Data Deleted Successfully', status: :ok
else
render plain: 'Processing. If there is an issue, please contact support', status: :ok
end
end
end
@@ -0,0 +1,23 @@
class Facebook::DeleteController < ApplicationController
include FacebookConcern
def create
signed_request = params['signed_request']
payload = parse_fb_signed_request(signed_request)
id_to_process = payload['user_id']
delete_request = DeleteRequest.create(fb_id: id_to_process)
status_url = "#{app_url_base}/facebook/confirm/#{delete_request.confirmation_code}"
# IMPORTANT: Do not change the response format below.
# Facebook's Data Deletion Request system specifically expects responses in this format
# with a 'url' for status confirmation and a 'confirmation_code' field.
# See: https://developers.facebook.com/docs/development/create-an-app/app-dashboard/data-deletion-callback/#implementing
render json: { url: status_url, confirmation_code: delete_request.confirmation_code }, status: :ok
rescue InvalidDigestError
render json: { error: 'Invalid signature' }, status: :unprocessable_entity
rescue StandardError => e
ChatwootExceptionTracker.new(e).capture_exception
render json: { error: e.message }, status: :malformed_request
end
end
@@ -39,13 +39,14 @@ export default {
},
mixins: [globalConfigMixin],
setup() {
const { isEditorHotKeyEnabled } = useUISettings();
const { isEditorHotKeyEnabled, updateUISettings } = useUISettings();
const { currentFontSize, updateFontSize } = useFontSize();
return {
currentFontSize,
updateFontSize,
isEditorHotKeyEnabled,
updateUISettings,
};
},
data() {
@@ -2,8 +2,15 @@ class Inboxes::FetchImapEmailInboxesJob < ApplicationJob
queue_as :scheduled_jobs
def perform
Inbox.where(channel_type: 'Channel::Email').all.find_each(batch_size: 100) do |inbox|
::Inboxes::FetchImapEmailsJob.perform_later(inbox.channel) if inbox.channel.imap_enabled
email_inboxes = Inbox.where(channel_type: 'Channel::Email')
email_inboxes.find_each(batch_size: 100) do |inbox|
::Inboxes::FetchImapEmailsJob.perform_later(inbox.channel) if should_fetch_emails?(inbox)
end
end
private
def should_fetch_emails?(inbox)
inbox.channel.imap_enabled && !inbox.account.suspended?
end
end
+50
View File
@@ -0,0 +1,50 @@
# == Schema Information
#
# Table name: delete_requests
#
# id :bigint not null, primary key
# completed_at :datetime
# confirmation_code :string not null
# deleted_type :string
# status :string not null
# created_at :datetime not null
# updated_at :datetime not null
# account_id :bigint
# deleted_id :integer
# fb_id :string not null
#
# Indexes
#
# index_delete_requests_on_account_id (account_id)
# index_delete_requests_on_confirmation_code (confirmation_code) UNIQUE
#
class DeleteRequest < ApplicationRecord
belongs_to :account, optional: true
enum :status, %w[pending complete].index_by(&:itself), default: :pending
before_create :ensure_unqiue_confirmation_code
private
def ensure_unqiue_confirmation_code
max_retries = 3
retry_count = 0
begin
self.confirmation_code = generate_confirmation_code
raise ActiveRecord::RecordNotUnique if self.class.exists?(confirmation_code: confirmation_code)
rescue ActiveRecord::RecordNotUnique
retry_count += 1
if retry_count > max_retries
# it's really really unlikely that we'll ever ever hit this case, but just in case
self.confirmation_code = generate_confirmation_code + SecureRandom.alphanumeric(3)
else
retry
end
end
end
def generate_confirmation_code
SecureRandom.uuid.delete('-')
end
end
+5
View File
@@ -453,6 +453,11 @@ Rails.application.routes.draw do
resource :callback, only: [:show]
end
namespace :facebook do
resources :delete, only: [:create]
resources :confirm, only: [:show]
end
namespace :linear do
resource :callback, only: [:show]
end
@@ -0,0 +1,17 @@
class CreateDeleteRequests < ActiveRecord::Migration[7.0]
def change
create_table :delete_requests do |t|
t.string :fb_id, null: false
t.string :status, null: false
t.string :confirmation_code, null: false
t.datetime :completed_at
t.string :deleted_type
t.integer :deleted_id
t.references :account, index: true, null: true
t.timestamps
end
add_index :delete_requests, :confirmation_code, unique: true
end
end
+15 -1
View File
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.0].define(version: 2025_02_28_185548) do
ActiveRecord::Schema[7.0].define(version: 2025_03_06_081627) do
# These extensions should be enabled to support this database
enable_extension "pg_stat_statements"
enable_extension "pg_trgm"
@@ -639,6 +639,20 @@ ActiveRecord::Schema[7.0].define(version: 2025_02_28_185548) do
t.index ["account_id"], name: "index_data_imports_on_account_id"
end
create_table "delete_requests", force: :cascade do |t|
t.string "fb_id", null: false
t.string "status", null: false
t.string "confirmation_code", null: false
t.datetime "completed_at"
t.string "deleted_type"
t.integer "deleted_id"
t.bigint "account_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["account_id"], name: "index_delete_requests_on_account_id"
t.index ["confirmation_code"], name: "index_delete_requests_on_confirmation_code", unique: true
end
create_table "email_templates", force: :cascade do |t|
t.string "name", null: false
t.text "body", null: false
+3
View File
@@ -41,4 +41,7 @@ module Redis::RedisKeys
IG_MESSAGE_MUTEX = 'IG_MESSAGE_CREATE_LOCK::%<sender_id>s::%<ig_account_id>s'.freeze
SLACK_MESSAGE_MUTEX = 'SLACK_MESSAGE_LOCK::%<conversation_id>s::%<reference_id>s'.freeze
EMAIL_MESSAGE_MUTEX = 'EMAIL_CHANNEL_LOCK::%<inbox_id>s'.freeze
## Meta deletion flags
META_DELETE_PROCESSING = 'META_DELETE_PROCESSING::%<id>s'.freeze
end
@@ -0,0 +1,35 @@
require 'rails_helper'
RSpec.describe Facebook::ConfirmController do
describe 'GET #show' do
let(:user_id) { '12345' }
context 'when deletion is in progress' do
before do
redis_key = format(Redis::Alfred::META_DELETE_PROCESSING, id: user_id)
allow(Redis::Alfred).to receive(:get).with(redis_key).and_return('true')
end
it 'returns processing status' do
get :show, params: { id: user_id }
expect(response).to have_http_status(:ok)
expect(response.body).to eq('Processing')
end
end
context 'when deletion is completed' do
before do
redis_key = format(Redis::Alfred::META_DELETE_PROCESSING, id: user_id)
allow(Redis::Alfred).to receive(:get).with(redis_key).and_return(nil)
end
it 'returns success message' do
get :show, params: { id: user_id }
expect(response).to have_http_status(:ok)
expect(response.body).to eq('Data Deleted Successfully')
end
end
end
end
@@ -0,0 +1,59 @@
require 'rails_helper'
RSpec.describe Facebook::DeleteController do
describe 'POST #create' do
let(:user_id) { '12345' }
let(:app_secret) { 'test_app_secret' }
let(:frontend_url) { ENV.fetch('FRONTEND_URL', nil) }
let(:redis_key) { format(Redis::Alfred::META_DELETE_PROCESSING, id: user_id) }
before do
allow(GlobalConfigService).to receive(:load).with('FB_APP_SECRET', '').and_return(app_secret)
end
context 'with valid signed request' do
let(:payload) { { 'user_id' => user_id }.to_json }
let(:encoded_payload) { Base64.urlsafe_encode64(payload) }
let(:signature) { OpenSSL::HMAC.digest('sha256', app_secret, encoded_payload) }
let(:encoded_signature) { Base64.urlsafe_encode64(signature) }
let(:signed_request) { "#{encoded_signature}.#{encoded_payload}" }
before do
allow(Redis::Alfred).to receive(:set)
allow(Channels::Facebook::RedactContactDataJob).to receive(:perform_later)
end
it 'processes the delete request and returns status URL' do
post :create, params: { signed_request: signed_request }
expect(Redis::Alfred).to have_received(:set).with(redis_key, true)
expect(Channels::Facebook::RedactContactDataJob).to have_received(:perform_later).with(user_id)
expect(response).to have_http_status(:ok)
response_json = response.parsed_body
# NOTE: this is an important condition since this is exactly the format facebook expects
expect(response_json['url']).to eq("#{frontend_url}/facebook/confirm/#{user_id}")
expect(response_json['confirmation_code']).to eq(user_id)
end
end
context 'with invalid signed request' do
let(:invalid_signed_request) { 'invalid_signature.invalid_payload' }
before do
# Mock the Base64.urlsafe_decode64 to return valid values for testing
allow(Base64).to receive(:urlsafe_decode64).with('invalid_signature').and_return('decoded_signature')
allow(Base64).to receive(:urlsafe_decode64).with('invalid_payload').and_return('{"user_id":"12345"}')
allow(JSON).to receive(:parse).with('{"user_id":"12345"}').and_return({ 'user_id' => user_id })
allow(OpenSSL::HMAC).to receive(:digest).and_return('different_signature')
end
it 'returns an error for invalid signature' do
post :create, params: { signed_request: invalid_signed_request }
expect(response).to have_http_status(:unprocessable_entity)
end
end
end
end
@@ -2,11 +2,19 @@ require 'rails_helper'
RSpec.describe Inboxes::FetchImapEmailInboxesJob do
let(:account) { create(:account) }
let(:suspended_account) { create(:account, status: 'suspended') }
let(:imap_email_channel) do
create(:channel_email, imap_enabled: true, imap_address: 'imap.gmail.com', imap_port: 993, imap_login: 'imap@gmail.com',
imap_password: 'password', account: account)
create(:channel_email, imap_enabled: true, account: account)
end
let(:imap_email_channel_suspended) do
create(:channel_email, imap_enabled: true, account: suspended_account)
end
let(:disabled_imap_channel) do
create(:channel_email, imap_enabled: false, account: account)
end
let(:email_inbox) { create(:inbox, channel: imap_email_channel, account: account) }
it 'enqueues the job' do
expect { described_class.perform_later }.to have_enqueued_job(described_class)
@@ -14,9 +22,26 @@ RSpec.describe Inboxes::FetchImapEmailInboxesJob do
end
context 'when called' do
it 'fetch all the email channels' do
it 'fetches emails only for active accounts with imap enabled' do
# Should call perform_later only once for the active, imap-enabled inbox
expect(Inboxes::FetchImapEmailsJob).to receive(:perform_later).with(imap_email_channel).once
# Should not call for suspended account or disabled IMAP channels
expect(Inboxes::FetchImapEmailsJob).not_to receive(:perform_later).with(imap_email_channel_suspended)
expect(Inboxes::FetchImapEmailsJob).not_to receive(:perform_later).with(disabled_imap_channel)
described_class.perform_now
end
it 'skips suspended accounts' do
expect(Inboxes::FetchImapEmailsJob).not_to receive(:perform_later).with(imap_email_channel_suspended)
described_class.perform_now
end
it 'skips disabled imap channels' do
expect(Inboxes::FetchImapEmailsJob).not_to receive(:perform_later).with(disabled_imap_channel)
described_class.perform_now
end
end