Merge branch 'release/3.5.2'

This commit is contained in:
Sojan
2024-01-19 15:15:27 +04:00
34 changed files with 346 additions and 102 deletions

View File

@@ -46,6 +46,14 @@ class V2::ReportBuilder
}
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

View File

@@ -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

View File

@@ -14,7 +14,14 @@ module AccessTokenAuthHelper
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!

View File

@@ -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
@@ -60,4 +61,8 @@ class Public::Api::V1::Portals::BaseController < PublicController
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

View File

@@ -53,7 +53,7 @@ module Api::V1::InboxesHelper
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)

View File

@@ -37,7 +37,7 @@ module Api::V2::Accounts::ReportsHelper
business_hours: ActiveModel::Type::Boolean.new.cast(params[:business_hours])
}
)
).summary
).short_summary
end
private

View File

@@ -519,6 +519,9 @@ export default {
},
},
watch: {
teamId() {
this.updateVirtualListProps('teamId', this.teamId);
},
activeTeam() {
this.resetAndFetchData();
},

View File

@@ -126,6 +126,6 @@
"NO_CONTENT": "Содержимое отсутствует",
"HIDE_QUOTED_TEXT": "Скрыть цитируемый текст",
"SHOW_QUOTED_TEXT": "Показать цитируемый текст",
"MESSAGE_READ": "Читать"
"MESSAGE_READ": "Прочитано"
}
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -11,6 +11,7 @@
<table-footer
:current-page="Number(meta.currentPage)"
:total-count="meta.count"
:page-size="15"
@page-change="onPageChange"
/>
</div>

View File

@@ -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

View File

@@ -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
@@ -83,7 +87,16 @@ class DataImportJob < ApplicationJob
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

View File

@@ -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
@@ -23,7 +27,6 @@ class Inboxes::FetchImapEmailsJob < ApplicationJob
end
def process_email_for_channel(channel)
# fetching email for microsoft provider
if channel.microsoft?
fetch_mail_for_ms_provider(channel)
else
@@ -34,32 +37,66 @@ class Inboxes::FetchImapEmailsJob < ApplicationJob
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']
mail_info_logger(channel, inbound_mail, message_id)
next if email_already_present?(channel, inbound_mail, last_email_time)
process_mail(inbound_mail, channel)
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
def email_already_present?(channel, inbound_mail, _last_email_time)
channel.inbox.messages.find_by(source_id: inbound_mail.message_id).present?
def process_message_id(channel, imap_client, message_id_with_seq)
seq_no, message_id = message_id_with_seq
return if email_already_present?(channel, message_id)
# 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
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])
# 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}."
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)]')
# .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
def processed_email?(current_email, last_email_time)
return current_email.date < last_email_time if current_email.date.present?
false
# 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)
@@ -69,16 +106,15 @@ class Inboxes::FetchImapEmailsJob < ApplicationJob
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?
@@ -86,38 +122,26 @@ class Inboxes::FetchImapEmailsJob < ApplicationJob
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)
def email_already_present?(channel, message_id)
channel.inbox.messages.find_by(source_id: message_id).present?
end
def yesterday
(Time.zone.today - 1).strftime('%d-%b-%Y')
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)
@@ -132,4 +156,12 @@ class Inboxes::FetchImapEmailsJob < ApplicationJob
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

View File

@@ -18,17 +18,15 @@ class MutexApplicationJob < ApplicationJob
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

View File

@@ -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

View File

@@ -19,5 +19,8 @@ class TriggerScheduledItemsJob < ApplicationJob
# Job to sync whatsapp templates
Channels::Whatsapp::TemplatesSyncSchedulerJob.perform_later
# Job to clear notifications which are older than 1 month
Notification::RemoveOldNotificationJob.perform_later
end
end

View File

@@ -51,6 +51,15 @@ class AdministratorNotifications::ChannelNotificationsMailer < ApplicationMailer
send_mail_with_liquid(to: admin_emails, subject: subject) and return
end
def contact_import_failed
return unless smtp_config_set_or_development?
subject = 'Contact Import Failed'
@meta = {}
send_mail_with_liquid(to: admin_emails, subject: subject) and return
end
def contact_export_complete(file_url)
return unless smtp_config_set_or_development?

View File

@@ -149,24 +149,32 @@ class Channel::Telegram < ApplicationRecord
})
end
def convert_markdown_to_telegram(text)
## supported characters : https://core.telegram.org/bots/api#markdown-style
## To implement MarkdownV2, we will need to do a lot of escaping
def convert_markdown_to_telegram_html(text)
# ref: https://core.telegram.org/bots/api#html-style
# Convert bold - double asterisks to single asterisk in Telegram
# Chatwoot uses double asterisks for bold, while telegram used single asterisk
text.gsub!(/\*\*(.*?)\*\*/, '*\1*')
text
# escape html tags in text. We are subbing \n to <br> since commonmark will strip exta '\n'
text = CGI.escapeHTML(text.gsub("\n", '<br>'))
# convert markdown to html
html = CommonMarker.render_html(text).strip
# remove all html tags except b, strong, i, em, u, ins, s, strike, del, a, code, pre, blockquote
stripped_html = Rails::HTML5::SafeListSanitizer.new.sanitize(html, tags: %w[b strong i em u ins s strike del a code pre blockquote],
attributes: %w[href])
# converted escaped br tags to \n
stripped_html.gsub('&lt;br&gt;', "\n")
end
def message_request(chat_id, text, reply_markup = nil, reply_to_message_id = nil)
text_to_md = convert_markdown_to_telegram(text)
text_payload = convert_markdown_to_telegram_html(text)
HTTParty.post("#{telegram_api_url}/sendMessage",
body: {
chat_id: chat_id,
text: text_to_md,
text: text_payload,
reply_markup: reply_markup,
parse_mode: 'Markdown',
parse_mode: 'HTML',
reply_to_message_id: reply_to_message_id
})
end

View File

@@ -33,9 +33,12 @@ class DashboardApp < ApplicationRecord
'required' => %w[url type],
'properties' => {
'type' => { 'enum': ['frame'] },
'url' => { :type => 'string', 'format' => 'uri' }
'url' => { '$ref' => '#/definitions/saneUrl' }
}
},
'definitions' => {
'saneUrl' => { 'format' => 'uri', 'pattern' => '^https?://' }
},
'additionalProperties' => false,
'minItems' => 1
}

View File

@@ -19,6 +19,7 @@ json.data do
json.secondary_actor notification.secondary_actor&.push_event_data
json.user notification.user.push_event_data
json.created_at notification.created_at.to_i
json.last_activity_at notification.last_activity_at.to_i
end
end
end

View File

@@ -1,11 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16" height="16" viewBox="0 0 512 512" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>woot-log</title>
<desc>Created with Sketch.</desc>
<g id="Logo" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="woot-log" fill-rule="nonzero">
<circle id="Oval" fill="#47A7F6" cx="256" cy="256" r="256"></circle>
<path d="M362.807947,368.807947 L244.122956,368.807947 C178.699407,368.807947 125.456954,315.561812 125.456954,250.12177 C125.456954,184.703089 178.699407,131.456954 244.124143,131.456954 C309.565494,131.456954 362.807947,184.703089 362.807947,250.12177 L362.807947,368.807947 Z" id="Fill-1" stroke="#FFFFFF" stroke-width="6" fill="#FFFFFF"></path>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 846 B

View File

@@ -0,0 +1,3 @@
<p>Hello,</p>
<p>Your contact import has failed. It appears that the CSV file you uploaded may not be valid. We kindly request that you review the file and ensure it complies with the required format.<p/>

View File

@@ -2,9 +2,17 @@
<footer class="pt-16 pb-8 flex flex-col items-center justify-center">
<div class="mx-auto max-w-2xl text-center py-2">
<div class="flex items-center gap-2">
<%= render partial: 'icons/chatwoot-logo' %>
<img
class="w-4 h-4"
alt="<%= @global_config['BRAND_NAME'] %>"
src="<%= @global_config['LOGO_THUMBNAIL'] %>"
/>
<p class="text-slate-700 dark:text-slate-300 text-sm font-medium text-center">
<%= I18n.t('public_portal.footer.made_with') %> <a class="hover:underline" href="https://www.chatwoot.com" target="_blank" rel="noopener noreferrer nofoll/ow">Chatwoot</a>
<%= I18n.t('public_portal.footer.made_with') %>
<a class="hover:underline" href="<%= @global_config['BRAND_URL'] %>" target="_blank" rel="noopener noreferrer nofoll/ow"><%= @global_config['BRAND_NAME'] %></a>
</p>
</div>
</div>

View File

@@ -1,5 +1,5 @@
shared: &shared
version: '3.5.1'
version: '3.5.2'
development:
<<: *shared

View File

@@ -38,4 +38,5 @@ module Redis::RedisKeys
FACEBOOK_MESSAGE_MUTEX = 'FB_MESSAGE_CREATE_LOCK::%<sender_id>s::%<recipient_id>s'.freeze
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
end

View File

@@ -1,6 +1,6 @@
{
"name": "@chatwoot/chatwoot",
"version": "3.5.1",
"version": "3.5.2",
"license": "MIT",
"scripts": {
"eslint": "eslint app/**/*.{js,vue}",

View File

@@ -29,6 +29,25 @@ RSpec.describe 'API Base', type: :request do
end
end
describe 'request with api_access_token for a super admin' do
before do
user.update!(type: 'SuperAdmin')
end
context 'when its a valid api_access_token' do
it 'returns current user information' do
get '/api/v1/profile',
headers: { api_access_token: user.access_token.token },
as: :json
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response['id']).to eq(user.id)
expect(json_response['email']).to eq(user.email)
end
end
end
describe 'request with api_access_token for bot' do
let!(:agent_bot) { create(:agent_bot) }
let!(:inbox) { create(:inbox, account: account) }

View File

@@ -55,8 +55,12 @@ RSpec.describe 'DashboardAppsController', type: :request do
describe 'POST /api/v1/accounts/{account.id}/dashboard_apps' do
let(:payload) { { dashboard_app: { title: 'CRM Dashboard', content: [{ type: 'frame', url: 'https://link.com' }] } } }
let(:no_ssl_payload) { { dashboard_app: { title: 'CRM Dashboard', content: [{ type: 'frame', url: 'http://link.com' }] } } }
let(:invalid_type_payload) { { dashboard_app: { title: 'CRM Dashboard', content: [{ type: 'dda', url: 'https://link.com' }] } } }
let(:invalid_url_payload) { { dashboard_app: { title: 'CRM Dashboard', content: [{ type: 'frame', url: 'com' }] } } }
let(:non_http_url_payload) do
{ dashboard_app: { title: 'CRM Dashboard', content: [{ type: 'frame', url: 'ftp://wontwork.chatwoot.com/hello-world' }] } }
end
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
@@ -82,6 +86,19 @@ RSpec.describe 'DashboardAppsController', type: :request do
expect(json_response['content'][0]['type']).to eq payload[:dashboard_app][:content][0][:type]
end
it 'creates the dashboard app even if the URL does not have SSL' do
expect do
post "/api/v1/accounts/#{account.id}/dashboard_apps", headers: user.create_new_auth_token,
params: no_ssl_payload
end.to change(DashboardApp, :count).by(1)
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response['title']).to eq 'CRM Dashboard'
expect(json_response['content'][0]['link']).to eq payload[:dashboard_app][:content][0][:link]
expect(json_response['content'][0]['type']).to eq payload[:dashboard_app][:content][0][:type]
end
it 'does not create the dashboard app if invalid URL' do
expect do
post "/api/v1/accounts/#{account.id}/dashboard_apps", headers: user.create_new_auth_token,
@@ -93,6 +110,17 @@ RSpec.describe 'DashboardAppsController', type: :request do
expect(json_response['message']).to eq 'Content : Invalid data'
end
it 'does not create the dashboard app if non HTTP URL' do
expect do
post "/api/v1/accounts/#{account.id}/dashboard_apps", headers: user.create_new_auth_token,
params: non_http_url_payload
end.not_to change(DashboardApp, :count)
expect(response).to have_http_status(:unprocessable_entity)
json_response = response.parsed_body
expect(json_response['message']).to eq 'Content : Invalid data'
end
it 'does not create the dashboard app if invalid type' do
expect do
post "/api/v1/accounts/#{account.id}/dashboard_apps", headers: user.create_new_auth_token,

View File

@@ -151,5 +151,20 @@ RSpec.describe DataImportJob do
end
end
end
context 'when the CSV file is invalid' do
let(:invalid_csv_content) do
"id,name,email,phone_number,company\n1,\"Clarice Uzzell,\"missing_quote,918080808080,Acmecorp\n2,Marieann Creegan,,+918080808081,Acmecorp"
end
before do
allow(data_import.import_file).to receive(:download).and_return(invalid_csv_content)
end
it 'does not import any data and handles the MalformedCSVError' do
expect { described_class.perform_now(data_import) }
.to change { data_import.reload.status }.from('pending').to('failed')
end
end
end
end

View File

@@ -16,14 +16,12 @@ RSpec.describe MutexApplicationJob do
before do
allow(Redis::LockManager).to receive(:new).and_return(lock_manager)
allow(lock_manager).to receive(:locked?).and_return(false)
allow(lock_manager).to receive(:lock).and_return(true)
allow(lock_manager).to receive(:unlock).and_return(true)
end
describe '#with_lock' do
it 'acquires the lock and yields the block if lock is not acquired' do
expect(lock_manager).to receive(:locked?).with(lock_key).and_return(false)
expect(lock_manager).to receive(:lock).with(lock_key).and_return(true)
expect(lock_manager).to receive(:unlock).with(lock_key).and_return(true)
@@ -31,7 +29,7 @@ RSpec.describe MutexApplicationJob do
end
it 'raises LockAcquisitionError if it cannot acquire the lock' do
allow(lock_manager).to receive(:locked?).with(lock_key).and_return(true)
allow(lock_manager).to receive(:lock).with(lock_key).and_return(false)
expect do
described_class.new.send(:with_lock, lock_key) do
@@ -41,7 +39,6 @@ RSpec.describe MutexApplicationJob do
end
it 'ensures that the lock is released even if there is an error during block execution' do
expect(lock_manager).to receive(:locked?).with(lock_key).and_return(false)
expect(lock_manager).to receive(:lock).with(lock_key).and_return(true)
expect(lock_manager).to receive(:unlock).with(lock_key).and_return(true)

View File

@@ -0,0 +1,24 @@
require 'rails_helper'
RSpec.describe Notification::RemoveOldNotificationJob do
let(:user) { create(:user) }
let(:conversation) { create(:conversation) }
it 'enqueues the job' do
notification = create(:notification, user: user, notification_type: 'conversation_creation', primary_actor: conversation)
expect do
described_class.perform_later(notification)
end.to have_enqueued_job(described_class)
.on_queue('low')
end
it 'removes old notifications which are older than 1 month' do
create(:notification, user: user, notification_type: 'conversation_creation', primary_actor: conversation, created_at: 2.months.ago)
create(:notification, user: user, notification_type: 'conversation_creation', primary_actor: conversation, created_at: 1.month.ago)
create(:notification, user: user, notification_type: 'conversation_creation', primary_actor: conversation, created_at: 1.day.ago)
create(:notification, user: user, notification_type: 'conversation_creation', primary_actor: conversation, created_at: 1.hour.ago)
described_class.perform_now
expect(Notification.count).to eq(2)
end
end

View File

@@ -40,5 +40,10 @@ RSpec.describe TriggerScheduledItemsJob do
expect(Channels::Whatsapp::TemplatesSyncSchedulerJob).to receive(:perform_later).once
described_class.perform_now
end
it 'triggers Notification::RemoveOldNotificationJob' do
expect(Notification::RemoveOldNotificationJob).to receive(:perform_later).once
described_class.perform_now
end
end
end

View File

@@ -3,6 +3,58 @@ require 'rails_helper'
RSpec.describe Channel::Telegram do
let(:telegram_channel) { create(:channel_telegram) }
describe '#convert_markdown_to_telegram_html' do
subject { telegram_channel.send(:convert_markdown_to_telegram_html, text) }
context 'when text contains multiple newline characters' do
let(:text) { "Line one\nLine two\n\nLine four" }
it 'preserves multiple newline characters' do
expect(subject).to eq("Line one\nLine two\n\nLine four")
end
end
context 'when text contains broken markdown' do
let(:text) { 'This is a **broken markdown with <b>HTML</b> tags.' }
it 'does not break and properly converts to Telegram HTML format and escapes html tags' do
expect(subject).to eq('This is a **broken markdown with &lt;b&gt;HTML&lt;/b&gt; tags.')
end
end
context 'when text contains markdown and HTML elements' do
let(:text) { "Hello *world*! This is <b>bold</b> and this is <i>italic</i>.\nThis is a new line." }
it 'converts markdown to Telegram HTML format and escapes other html' do
expect(subject).to eq("Hello <em>world</em>! This is &lt;b&gt;bold&lt;/b&gt; and this is &lt;i&gt;italic&lt;/i&gt;.\nThis is a new line.")
end
end
context 'when text contains unsupported HTML tags' do
let(:text) { 'This is a <span>test</span> with unsupported tags.' }
it 'removes unsupported HTML tags' do
expect(subject).to eq('This is a &lt;span&gt;test&lt;/span&gt; with unsupported tags.')
end
end
context 'when text contains special characters' do
let(:text) { 'Special characters: & < >' }
it 'escapes special characters' do
expect(subject).to eq('Special characters: &amp; &lt; &gt;')
end
end
context 'when text contains markdown links' do
let(:text) { 'Check this [link](http://example.com) out!' }
it 'converts markdown links to Telegram HTML format' do
expect(subject).to eq('Check this <a href="http://example.com">link</a> out!')
end
end
end
context 'when a valid message and empty attachments' do
it 'send message' do
message = create(:message, message_type: :outgoing, content: 'test',
@@ -10,7 +62,7 @@ RSpec.describe Channel::Telegram do
stub_request(:post, "https://api.telegram.org/bot#{telegram_channel.bot_token}/sendMessage")
.with(
body: 'chat_id=123&text=test&reply_markup=&parse_mode=Markdown&reply_to_message_id='
body: 'chat_id=123&text=test&reply_markup=&parse_mode=HTML&reply_to_message_id='
)
.to_return(
status: 200,
@@ -21,13 +73,15 @@ RSpec.describe Channel::Telegram do
expect(telegram_channel.send_message_on_telegram(message)).to eq('telegram_123')
end
it 'send message with markdown converted to telegram markdown' do
it 'send message with markdown converted to telegram HTML' do
message = create(:message, message_type: :outgoing, content: '**test** *test* ~test~',
conversation: create(:conversation, inbox: telegram_channel.inbox, additional_attributes: { 'chat_id' => '123' }))
stub_request(:post, "https://api.telegram.org/bot#{telegram_channel.bot_token}/sendMessage")
.with(
body: "chat_id=123&text=#{ERB::Util.url_encode('*test* *test* ~test~')}&reply_markup=&parse_mode=Markdown&reply_to_message_id="
body: "chat_id=123&text=#{
ERB::Util.url_encode('<strong>test</strong> <em>test</em> ~test~')
}&reply_markup=&parse_mode=HTML&reply_to_message_id="
)
.to_return(
status: 200,
@@ -49,7 +103,7 @@ RSpec.describe Channel::Telegram do
.with(
body: 'chat_id=123&text=test' \
'&reply_markup=%7B%22one_time_keyboard%22%3Atrue%2C%22inline_keyboard%22%3A%5B%5B%7B%22text%22%3A%22test%22%2C%22' \
'callback_data%22%3A%22test%22%7D%5D%5D%7D&parse_mode=Markdown&reply_to_message_id='
'callback_data%22%3A%22test%22%7D%5D%5D%7D&parse_mode=HTML&reply_to_message_id='
)
.to_return(
status: 200,
@@ -66,7 +120,7 @@ RSpec.describe Channel::Telegram do
stub_request(:post, "https://api.telegram.org/bot#{telegram_channel.bot_token}/sendMessage")
.with(
body: 'chat_id=123&text=test&reply_markup=&parse_mode=Markdown&reply_to_message_id='
body: 'chat_id=123&text=test&reply_markup=&parse_mode=HTML&reply_to_message_id='
)
.to_return(
status: 403,