Skip to content

Commit

Permalink
Merge branch 'release/3.5.2'
Browse files Browse the repository at this point in the history
  • Loading branch information
sojan-official committed Jan 19, 2024
2 parents ab1f803 + 48e638c commit 608592d
Show file tree
Hide file tree
Showing 34 changed files with 344 additions and 100 deletions.
8 changes: 8 additions & 0 deletions app/builders/v2/report_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,14 @@ def summary
}
end

def short_summary
{
conversations_count: conversations.count,
avg_first_response_time: avg_first_response_time_summary,
avg_resolution_time: avg_resolution_time_summary
}
end

def conversation_metrics
if params[:type].equal?(:account)
live_conversations
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class Api::V1::Accounts::NotificationsController < Api::V1::Accounts::BaseContro
def index
@unread_count = notification_finder.unread_count
@notifications = notification_finder.perform
@count = @notifications.count
@count = notification_finder.count
end

def read_all
Expand Down
9 changes: 8 additions & 1 deletion app/controllers/concerns/access_token_auth_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,14 @@ def authenticate_access_token!
render_unauthorized('Invalid Access Token') && return if @access_token.blank?

@resource = @access_token.owner
Current.user = @resource if [User, AgentBot].include?(@resource.class)
Current.user = @resource if allowed_current_user_type?(@resource)
end

def allowed_current_user_type?(resource)
return true if resource.is_a?(User)
return true if resource.is_a?(AgentBot)

false
end

def validate_bot_access_token!
Expand Down
5 changes: 5 additions & 0 deletions app/controllers/public/api/v1/portals/base_controller.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
class Public::Api::V1::Portals::BaseController < PublicController
before_action :show_plain_layout
before_action :set_color_scheme
before_action :set_global_config
around_action :set_locale
after_action :allow_iframe_requests

Expand Down Expand Up @@ -60,4 +61,8 @@ def render_404
portal
render 'public/api/v1/portals/error/404', status: :not_found
end

def set_global_config
@global_config = GlobalConfig.get('LOGO_THUMBNAIL', 'BRAND_NAME', 'BRAND_URL')
end
end
2 changes: 1 addition & 1 deletion app/helpers/api/v1/inboxes_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ def check_imap_connection(channel_data)
rescue StandardError => e
raise StandardError, e.message
ensure
ChatwootExceptionTracker.new(e).capture_exception if e.present?
Rails.logger.error "[Api::V1::InboxesHelper] check_imap_connection failed with #{e.message}" if e.present?
end

def check_smtp_connection(channel_data, smtp)
Expand Down
2 changes: 1 addition & 1 deletion app/helpers/api/v2/accounts/reports_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ def generate_report(report_params)
business_hours: ActiveModel::Type::Boolean.new.cast(params[:business_hours])
}
)
).summary
).short_summary
end

private
Expand Down
3 changes: 3 additions & 0 deletions app/javascript/dashboard/components/ChatList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,9 @@ export default {
},
},
watch: {
teamId() {
this.updateVirtualListProps('teamId', this.teamId);
},
activeTeam() {
this.resetAndFetchData();
},
Expand Down
2 changes: 1 addition & 1 deletion app/javascript/dashboard/i18n/locale/ru/chatlist.json
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,6 @@
"NO_CONTENT": "小芯写械褉卸懈屑芯械 芯褌褋褍褌褋褌胁褍械褌",
"HIDE_QUOTED_TEXT": "小泻褉褘褌褜 褑懈褌懈褉褍械屑褘泄 褌械泻褋褌",
"SHOW_QUOTED_TEXT": "袩芯泻邪蟹邪褌褜 褑懈褌懈褉褍械屑褘泄 褌械泻褋褌",
"MESSAGE_READ": "效懈褌邪褌褜"
"MESSAGE_READ": "袩褉芯褔懈褌邪薪芯"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
<span
class="mt-1 text-slate-500 dark:text-slate-400 text-xxs font-semibold flex"
>
{{ dynamicTime(notificationItem.created_at) }}
{{ dynamicTime(notificationItem.last_activity_at) }}
</span>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@
<td>
<div class="text-right timestamp--column">
<span class="notification--created-at">
{{ dynamicTime(notificationItem.created_at) }}
{{ dynamicTime(notificationItem.last_activity_at) }}
</span>
</div>
</td>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<table-footer
:current-page="Number(meta.currentPage)"
:total-count="meta.count"
:page-size="15"
@page-change="onPageChange"
/>
</div>
Expand Down
6 changes: 4 additions & 2 deletions app/jobs/application_job.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
class ApplicationJob < ActiveJob::Base
# https://api.rubyonrails.org/v5.2.1/classes/ActiveJob/Exceptions/ClassMethods.html
discard_on ActiveJob::DeserializationError do |_job, error|
Rails.logger.error("Skipping job because of ActiveJob::DeserializationError (#{error.message})")
discard_on ActiveJob::DeserializationError do |job, error|
Rails.logger.info("Skipping #{job.class} with #{
job.instance_variable_get(:@serialized_arguments)
} because of ActiveJob::DeserializationError (#{error.message})")
end
end
17 changes: 15 additions & 2 deletions app/jobs/data_import_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,12 @@ class DataImportJob < ApplicationJob
def perform(data_import)
@data_import = data_import
@contact_manager = DataImport::ContactManager.new(@data_import.account)
process_import_file
send_import_notification_to_admin
begin
process_import_file
send_import_notification_to_admin
rescue CSV::MalformedCSVError => e
handle_csv_error(e)
end
end

private
Expand Down Expand Up @@ -83,7 +87,16 @@ def generate_csv_data(rejected_contacts)
end
end

def handle_csv_error(error) # rubocop:disable Lint/UnusedMethodArgument
@data_import.update!(status: :failed)
send_import_failed_notification_to_admin
end

def send_import_notification_to_admin
AdministratorNotifications::ChannelNotificationsMailer.with(account: @data_import.account).contact_import_complete(@data_import).deliver_later
end

def send_import_failed_notification_to_admin
AdministratorNotifications::ChannelNotificationsMailer.with(account: @data_import.account).contact_import_failed.deliver_later
end
end
122 changes: 77 additions & 45 deletions app/jobs/inboxes/fetch_imap_emails_job.rb
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
require 'net/imap'

class Inboxes::FetchImapEmailsJob < ApplicationJob
class Inboxes::FetchImapEmailsJob < MutexApplicationJob
queue_as :scheduled_jobs

def perform(channel)
return unless should_fetch_email?(channel)

process_email_for_channel(channel)
with_lock(::Redis::Alfred::EMAIL_MESSAGE_MUTEX, inbox_id: channel.inbox.id) do
process_email_for_channel(channel)
end
rescue *ExceptionList::IMAP_EXCEPTIONS => e
Rails.logger.error e
channel.authorization_error!
rescue EOFError, OpenSSL::SSL::SSLError, Net::IMAP::NoResponseError, Net::IMAP::BadResponseError => e
Rails.logger.error e
rescue LockAcquisitionError
Rails.logger.error "Lock failed for #{channel.inbox.id}"
rescue StandardError => e
ChatwootExceptionTracker.new(e, account: channel.account).capture_exception
end
Expand All @@ -23,7 +27,6 @@ def should_fetch_email?(channel)
end

def process_email_for_channel(channel)
# fetching email for microsoft provider
if channel.microsoft?
fetch_mail_for_ms_provider(channel)
else
Expand All @@ -34,32 +37,66 @@ def process_email_for_channel(channel)
end

def fetch_mail_for_channel(channel)
imap_inbox = authenticated_imap_inbox(channel, channel.imap_password, 'PLAIN')
last_email_time = DateTime.parse(Net::IMAP.format_datetime(last_email_time(channel)))
imap_client = build_imap_client(channel, channel.imap_password, 'PLAIN')

received_mails(imap_inbox).each do |message_id|
inbound_mail = Mail.read_from_string imap_inbox.fetch(message_id, 'RFC822')[0].attr['RFC822']
message_ids_with_seq = fetch_message_ids_with_sequence(imap_client, channel)
message_ids_with_seq.each do |message_id_with_seq|
process_message_id(channel, imap_client, message_id_with_seq)
end
end

mail_info_logger(channel, inbound_mail, message_id)
def process_message_id(channel, imap_client, message_id_with_seq)
seq_no, message_id = message_id_with_seq

next if email_already_present?(channel, inbound_mail, last_email_time)
return if email_already_present?(channel, message_id)

process_mail(inbound_mail, channel)
# Fetch the original mail content using the sequence no
mail_str = imap_client.fetch(seq_no, 'RFC822')[0].attr['RFC822']

if mail_str.blank?
Rails.logger.info "[IMAP::FETCH_EMAIL_SERVICE] Fetch failed for #{channel.email} with message-id <#{message_id}>."
return
end
end

def email_already_present?(channel, inbound_mail, _last_email_time)
channel.inbox.messages.find_by(source_id: inbound_mail.message_id).present?
inbound_mail = build_mail_from_string(mail_str)
mail_info_logger(channel, inbound_mail, seq_no)
process_mail(inbound_mail, channel)
end

def received_mails(imap_inbox)
imap_inbox.search(['BEFORE', tomorrow, 'SINCE', yesterday])
end
# Sends a FETCH command to retrieve data associated with a message in the mailbox.
# You can send batches of message sequence number in `.fetch` method.
def fetch_message_ids_with_sequence(imap_client, channel)
seq_nums = fetch_available_mail_sequence_numbers(imap_client)

Rails.logger.info "[IMAP::FETCH_EMAIL_SERVICE] Fetching mails from #{channel.email}, found #{seq_nums.length}."

def processed_email?(current_email, last_email_time)
return current_email.date < last_email_time if current_email.date.present?
message_ids_with_seq = []
seq_nums.each_slice(10).each do |batch|
# Fetch only message-id only without mail body or contents.
batch_message_ids = imap_client.fetch(batch, 'BODY.PEEK[HEADER.FIELDS (MESSAGE-ID)]')

false
# .fetch returns an array of Net::IMAP::FetchData or nil
# (instead of an empty array) if there is no matching message.
# Check
if batch_message_ids.blank?
Rails.logger.info "[IMAP::FETCH_EMAIL_SERVICE] Fetching the batch failed for #{channel.email}."
next
end

batch_message_ids.each do |data|
message_id = build_mail_from_string(data.attr['BODY[HEADER.FIELDS (MESSAGE-ID)]']).message_id
message_ids_with_seq.push([data.seqno, message_id])
end
end

message_ids_with_seq
end

# Sends a SEARCH command to search the mailbox for messages that were
# created between yesterday and today and returns message sequence numbers.
# Return <message set>
def fetch_available_mail_sequence_numbers(imap_client)
imap_client.search(['BEFORE', tomorrow, 'SINCE', yesterday])
end

def fetch_mail_for_ms_provider(channel)
Expand All @@ -69,55 +106,42 @@ def fetch_mail_for_ms_provider(channel)

return unless access_token

imap_inbox = authenticated_imap_inbox(channel, access_token, 'XOAUTH2')

process_mails(imap_inbox, channel)
imap_client = build_imap_client(channel, access_token, 'XOAUTH2')
process_mails(imap_client, channel)
end

def process_mails(imap_inbox, channel)
received_mails(imap_inbox).each do |message_id|
inbound_mail = Mail.read_from_string imap_inbox.fetch(message_id, 'RFC822')[0].attr['RFC822']
def process_mails(imap_client, channel)
fetch_available_mail_sequence_numbers(imap_client).each do |seq_no|
inbound_mail = Mail.read_from_string imap_client.fetch(seq_no, 'RFC822')[0].attr['RFC822']

mail_info_logger(channel, inbound_mail, message_id)
mail_info_logger(channel, inbound_mail, seq_no)

next if channel.inbox.messages.find_by(source_id: inbound_mail.message_id).present?

process_mail(inbound_mail, channel)
end
end

def mail_info_logger(channel, inbound_mail, message_id)
def mail_info_logger(channel, inbound_mail, uid)
return if Rails.env.test?

Rails.logger.info("
#{channel.provider} Email id: #{inbound_mail.from} and message_source_id: #{inbound_mail.message_id}, message_id: #{message_id}")
#{channel.provider} Email id: #{inbound_mail.from} - message_source_id: #{inbound_mail.message_id} - sequence id: #{uid}")
end

def authenticated_imap_inbox(channel, access_token, auth_method)
def build_imap_client(channel, access_token, auth_method)
imap = Net::IMAP.new(channel.imap_address, channel.imap_port, true)
imap.authenticate(auth_method, channel.imap_login, access_token)
imap.select('INBOX')
imap
end

def last_email_time(channel)
# we are only checking for emails in last 2 day
last_email_incoming_message = channel.inbox.messages.incoming.where('messages.created_at >= ?', 2.days.ago).last
if last_email_incoming_message.present?
time = last_email_incoming_message.content_attributes['email']['date']
time ||= last_email_incoming_message.created_at.to_s
end
time ||= 1.hour.ago.to_s

DateTime.parse(time)
end

def yesterday
(Time.zone.today - 1).strftime('%d-%b-%Y')
def email_already_present?(channel, message_id)
channel.inbox.messages.find_by(source_id: message_id).present?
end

def tomorrow
(Time.zone.today + 1).strftime('%d-%b-%Y')
def build_mail_from_string(raw_email_content)
Mail.read_from_string(raw_email_content)
end

def process_mail(inbound_mail, channel)
Expand All @@ -132,4 +156,12 @@ def process_mail(inbound_mail, channel)
def valid_access_token(channel)
Microsoft::RefreshOauthTokenService.new(channel: channel).access_token
end

def yesterday
(Time.zone.today - 1).strftime('%d-%b-%Y')
end

def tomorrow
(Time.zone.today + 1).strftime('%d-%b-%Y')
end
end
16 changes: 7 additions & 9 deletions app/jobs/mutex_application_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,15 @@ def with_lock(key_format, *args)
lock_key = format(key_format, *args)
lock_manager = Redis::LockManager.new

if lock_manager.locked?(lock_key)
Rails.logger.warn "[#{self.class.name}] Failed to acquire lock on attempt #{executions}: #{lock_key}"
raise LockAcquisitionError, "Failed to acquire lock for key: #{lock_key}"
end

begin
lock_manager.lock(lock_key)
Rails.logger.info "[#{self.class.name}] Acquired lock for: #{lock_key} on attempt #{executions}"
yield
if lock_manager.lock(lock_key)
Rails.logger.info "[#{self.class.name}] Acquired lock for: #{lock_key} on attempt #{executions}"
yield
else
Rails.logger.warn "[#{self.class.name}] Failed to acquire lock on attempt #{executions}: #{lock_key}"
raise LockAcquisitionError, "Failed to acquire lock for key: #{lock_key}"
end
ensure
# Ensure that the lock is released even if there's an error in processing
lock_manager.unlock(lock_key)
end
end
Expand Down
8 changes: 8 additions & 0 deletions app/jobs/notification/remove_old_notification_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
class Notification::RemoveOldNotificationJob < ApplicationJob
queue_as :low

def perform
Notification.where('created_at < ?', 1.month.ago)
.find_each(batch_size: 1000, &:delete)
end
end

0 comments on commit 608592d

Please sign in to comment.