Merge branch 'develop' into fix/show-error-on-clipboard-errors
This commit is contained in:
@@ -85,6 +85,8 @@ SMTP_OPENSSL_VERIFY_MODE=peer
|
||||
# Comment out the following environment variables if required by your SMTP server
|
||||
# SMTP_TLS=
|
||||
# SMTP_SSL=
|
||||
# SMTP_OPEN_TIMEOUT
|
||||
# SMTP_READ_TIMEOUT
|
||||
|
||||
# Mail Incoming
|
||||
# This is the domain set for the reply emails when conversation continuity is enabled
|
||||
|
||||
@@ -111,9 +111,9 @@ gem 'elastic-apm', require: false
|
||||
gem 'newrelic_rpm', require: false
|
||||
gem 'newrelic-sidekiq-metrics', '>= 1.6.2', require: false
|
||||
gem 'scout_apm', require: false
|
||||
gem 'sentry-rails', '>= 5.14.0', require: false
|
||||
gem 'sentry-rails', '>= 5.18.0', require: false
|
||||
gem 'sentry-ruby', require: false
|
||||
gem 'sentry-sidekiq', '>= 5.15.0', require: false
|
||||
gem 'sentry-sidekiq', '>= 5.18.0', require: false
|
||||
|
||||
##-- background job processing --##
|
||||
gem 'sidekiq', '>= 7.2.4'
|
||||
@@ -228,7 +228,7 @@ group :development, :test do
|
||||
gem 'mock_redis'
|
||||
gem 'pry-rails'
|
||||
gem 'rspec_junit_formatter'
|
||||
gem 'rspec-rails', '>= 6.0.3'
|
||||
gem 'rspec-rails', '>= 6.1.3'
|
||||
gem 'rubocop', require: false
|
||||
gem 'rubocop-performance', require: false
|
||||
gem 'rubocop-rails', require: false
|
||||
|
||||
+19
-19
@@ -150,7 +150,7 @@ GEM
|
||||
statsd-ruby (~> 1.1)
|
||||
base64 (0.2.0)
|
||||
bcrypt (3.1.20)
|
||||
bigdecimal (3.1.7)
|
||||
bigdecimal (3.1.8)
|
||||
bindex (0.8.1)
|
||||
bootsnap (1.16.0)
|
||||
msgpack (~> 1.2)
|
||||
@@ -223,7 +223,7 @@ GEM
|
||||
http (>= 3.0)
|
||||
ruby2_keywords
|
||||
email_reply_trimmer (0.1.13)
|
||||
erubi (1.12.0)
|
||||
erubi (1.13.0)
|
||||
et-orbi (1.2.7)
|
||||
tzinfo
|
||||
execjs (2.8.1)
|
||||
@@ -460,7 +460,7 @@ GEM
|
||||
mini_magick (4.12.0)
|
||||
mini_mime (1.1.5)
|
||||
mini_portile2 (2.8.7)
|
||||
minitest (5.23.1)
|
||||
minitest (5.24.0)
|
||||
mock_redis (0.36.0)
|
||||
ruby2_keywords
|
||||
msgpack (1.7.0)
|
||||
@@ -489,14 +489,14 @@ GEM
|
||||
newrelic_rpm (9.6.0)
|
||||
base64
|
||||
nio4r (2.7.3)
|
||||
nokogiri (1.16.5)
|
||||
nokogiri (1.16.6)
|
||||
mini_portile2 (~> 2.8.2)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.16.5-arm64-darwin)
|
||||
nokogiri (1.16.6-arm64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.16.5-x86_64-darwin)
|
||||
nokogiri (1.16.6-x86_64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.16.5-x86_64-linux)
|
||||
nokogiri (1.16.6-x86_64-linux)
|
||||
racc (~> 1.4)
|
||||
oauth (1.1.0)
|
||||
oauth-tty (~> 1.0, >= 1.0.1)
|
||||
@@ -603,7 +603,7 @@ GEM
|
||||
ffi (~> 1.0)
|
||||
redis (5.0.6)
|
||||
redis-client (>= 0.9.0)
|
||||
redis-client (0.22.1)
|
||||
redis-client (0.22.2)
|
||||
connection_pool
|
||||
redis-namespace (1.10.0)
|
||||
redis (>= 4)
|
||||
@@ -631,13 +631,13 @@ GEM
|
||||
strscan (>= 3.0.9)
|
||||
rspec-core (3.13.0)
|
||||
rspec-support (~> 3.13.0)
|
||||
rspec-expectations (3.13.0)
|
||||
rspec-expectations (3.13.1)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.13.0)
|
||||
rspec-mocks (3.13.1)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.13.0)
|
||||
rspec-rails (6.1.2)
|
||||
rspec-rails (6.1.3)
|
||||
actionpack (>= 6.1)
|
||||
activesupport (>= 6.1)
|
||||
railties (>= 6.1)
|
||||
@@ -703,14 +703,14 @@ GEM
|
||||
activesupport (>= 4)
|
||||
selectize-rails (0.12.6)
|
||||
semantic_range (3.0.0)
|
||||
sentry-rails (5.17.3)
|
||||
sentry-rails (5.18.0)
|
||||
railties (>= 5.0)
|
||||
sentry-ruby (~> 5.17.3)
|
||||
sentry-ruby (5.17.3)
|
||||
sentry-ruby (~> 5.18.0)
|
||||
sentry-ruby (5.18.0)
|
||||
bigdecimal
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
sentry-sidekiq (5.17.3)
|
||||
sentry-ruby (~> 5.17.3)
|
||||
sentry-sidekiq (5.18.0)
|
||||
sentry-ruby (~> 5.18.0)
|
||||
sidekiq (>= 3.0)
|
||||
sexp_processor (4.17.0)
|
||||
shoulda-matchers (5.3.0)
|
||||
@@ -819,7 +819,7 @@ GEM
|
||||
working_hours (1.4.1)
|
||||
activesupport (>= 3.2)
|
||||
tzinfo
|
||||
zeitwerk (2.6.15)
|
||||
zeitwerk (2.6.16)
|
||||
|
||||
PLATFORMS
|
||||
arm64-darwin-20
|
||||
@@ -922,7 +922,7 @@ DEPENDENCIES
|
||||
responders (>= 3.1.1)
|
||||
rest-client
|
||||
reverse_markdown
|
||||
rspec-rails (>= 6.0.3)
|
||||
rspec-rails (>= 6.1.3)
|
||||
rspec_junit_formatter
|
||||
rubocop
|
||||
rubocop-performance
|
||||
@@ -931,9 +931,9 @@ DEPENDENCIES
|
||||
scout_apm
|
||||
scss_lint
|
||||
seed_dump
|
||||
sentry-rails (>= 5.14.0)
|
||||
sentry-rails (>= 5.18.0)
|
||||
sentry-ruby
|
||||
sentry-sidekiq (>= 5.15.0)
|
||||
sentry-sidekiq (>= 5.18.0)
|
||||
shoulda-matchers
|
||||
sidekiq (>= 7.2.4)
|
||||
sidekiq-cron (>= 1.12.0)
|
||||
|
||||
@@ -37,7 +37,7 @@ class DashboardController < ActionController::Base
|
||||
end
|
||||
|
||||
def set_dashboard_scripts
|
||||
@dashboard_scripts = GlobalConfig.get_value('DASHBOARD_SCRIPTS')
|
||||
@dashboard_scripts = sensitive_path? ? nil : GlobalConfig.get_value('DASHBOARD_SCRIPTS')
|
||||
end
|
||||
|
||||
def ensure_installation_onboarding
|
||||
@@ -75,4 +75,14 @@ class DashboardController < ActionController::Base
|
||||
'application'
|
||||
end
|
||||
end
|
||||
|
||||
def sensitive_path?
|
||||
# dont load dashboard scripts on sensitive paths like password reset
|
||||
sensitive_paths = [edit_user_password_path].freeze
|
||||
|
||||
# remove app prefix
|
||||
current_path = request.path.gsub(%r{^/app}, '')
|
||||
|
||||
sensitive_paths.include?(current_path)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -40,7 +40,7 @@ class SuperAdmin::AppConfigsController < SuperAdmin::ApplicationController
|
||||
when 'email'
|
||||
['MAILER_INBOUND_EMAIL_DOMAIN']
|
||||
else
|
||||
%w[ENABLE_ACCOUNT_SIGNUP]
|
||||
%w[ENABLE_ACCOUNT_SIGNUP FIREBASE_PROJECT_ID FIREBASE_CREDENTIALS]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -257,7 +257,16 @@ export default {
|
||||
html_content: { full: fullHTMLContent } = {},
|
||||
text_content: { full: fullTextContent } = {},
|
||||
} = this.contentAttributes.email || {};
|
||||
return fullHTMLContent || fullTextContent || '';
|
||||
|
||||
if (fullHTMLContent) {
|
||||
return fullHTMLContent;
|
||||
}
|
||||
|
||||
if (fullTextContent) {
|
||||
return fullTextContent.replace(/\n/g, '<br>');
|
||||
}
|
||||
|
||||
return '';
|
||||
},
|
||||
displayQuotedButton() {
|
||||
if (this.emailMessageContent.includes('<blockquote')) {
|
||||
|
||||
@@ -121,7 +121,7 @@ export const generateConditionOptions = (options, key = 'id') => {
|
||||
};
|
||||
|
||||
// Add the "None" option to the agent list
|
||||
export const agentList = agents => [
|
||||
export const addNoneToList = agents => [
|
||||
{
|
||||
id: 'nil',
|
||||
name: 'None',
|
||||
@@ -137,8 +137,8 @@ export const getActionOptions = ({
|
||||
type,
|
||||
}) => {
|
||||
const actionsMap = {
|
||||
assign_agent: agentList(agents),
|
||||
assign_team: teams,
|
||||
assign_agent: addNoneToList(agents),
|
||||
assign_team: addNoneToList(teams),
|
||||
send_email_to_team: teams,
|
||||
add_label: generateConditionOptions(labels, 'title'),
|
||||
remove_label: generateConditionOptions(labels, 'title'),
|
||||
|
||||
@@ -6,12 +6,16 @@ import { validateRouteAccess } from '../helpers/RouteHelper';
|
||||
|
||||
export const router = new VueRouter({ mode: 'history', routes });
|
||||
|
||||
const sensitiveRouteNames = ['auth_password_edit'];
|
||||
|
||||
export const initalizeRouter = () => {
|
||||
router.beforeEach((to, _, next) => {
|
||||
AnalyticsHelper.page(to.name || '', {
|
||||
path: to.path,
|
||||
name: to.name,
|
||||
});
|
||||
if (!sensitiveRouteNames.includes(to.name)) {
|
||||
AnalyticsHelper.page(to.name || '', {
|
||||
path: to.path,
|
||||
name: to.name,
|
||||
});
|
||||
}
|
||||
|
||||
return validateRouteAccess(to, next, window.chatwootConfig);
|
||||
});
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
/>
|
||||
<div
|
||||
v-if="hasAttachments"
|
||||
class="chat-bubble has-attachment agent"
|
||||
class="chat-bubble has-attachment space-y-2 agent"
|
||||
:class="(wrapClass, $dm('bg-white', 'dark:bg-slate-700'))"
|
||||
>
|
||||
<div
|
||||
@@ -44,6 +44,14 @@
|
||||
:readable-time="readableTime"
|
||||
@error="onImageLoadError"
|
||||
/>
|
||||
|
||||
<video-bubble
|
||||
v-if="attachment.file_type === 'video' && !hasVideoError"
|
||||
:url="attachment.data_url"
|
||||
:readable-time="readableTime"
|
||||
@error="onVideoLoadError"
|
||||
/>
|
||||
|
||||
<audio v-else-if="attachment.file_type === 'audio'" controls>
|
||||
<source :src="attachment.data_url" />
|
||||
</audio>
|
||||
@@ -84,6 +92,7 @@ import AgentMessageBubble from 'widget/components/AgentMessageBubble.vue';
|
||||
import MessageReplyButton from 'widget/components/MessageReplyButton.vue';
|
||||
import timeMixin from 'dashboard/mixins/time';
|
||||
import ImageBubble from 'widget/components/ImageBubble.vue';
|
||||
import VideoBubble from 'widget/components/VideoBubble.vue';
|
||||
import FileBubble from 'widget/components/FileBubble.vue';
|
||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||
import { MESSAGE_TYPE } from 'widget/helpers/constants';
|
||||
@@ -100,6 +109,7 @@ export default {
|
||||
components: {
|
||||
AgentMessageBubble,
|
||||
ImageBubble,
|
||||
VideoBubble,
|
||||
Thumbnail,
|
||||
UserMessage,
|
||||
FileBubble,
|
||||
@@ -120,6 +130,7 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
hasImageError: false,
|
||||
hasVideoError: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -215,15 +226,20 @@ export default {
|
||||
watch: {
|
||||
message() {
|
||||
this.hasImageError = false;
|
||||
this.hasVideoError = false;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.hasImageError = false;
|
||||
this.hasVideoError = false;
|
||||
},
|
||||
methods: {
|
||||
onImageLoadError() {
|
||||
this.hasImageError = true;
|
||||
},
|
||||
onVideoLoadError() {
|
||||
this.hasVideoError = true;
|
||||
},
|
||||
toggleReply() {
|
||||
emitter.emit(BUS_EVENTS.TOGGLE_REPLY_TO_MESSAGE, this.message);
|
||||
},
|
||||
|
||||
@@ -39,6 +39,14 @@
|
||||
:readable-time="readableTime"
|
||||
@error="onImageLoadError"
|
||||
/>
|
||||
|
||||
<video-bubble
|
||||
v-if="attachment.file_type === 'video' && !hasVideoError"
|
||||
:url="attachment.data_url"
|
||||
:readable-time="readableTime"
|
||||
@error="onVideoLoadError"
|
||||
/>
|
||||
|
||||
<file-bubble
|
||||
v-else
|
||||
:url="attachment.data_url"
|
||||
@@ -72,6 +80,7 @@
|
||||
import UserMessageBubble from 'widget/components/UserMessageBubble.vue';
|
||||
import MessageReplyButton from 'widget/components/MessageReplyButton.vue';
|
||||
import ImageBubble from 'widget/components/ImageBubble.vue';
|
||||
import VideoBubble from 'widget/components/VideoBubble.vue';
|
||||
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
|
||||
import FileBubble from 'widget/components/FileBubble.vue';
|
||||
import timeMixin from 'dashboard/mixins/time';
|
||||
@@ -88,6 +97,7 @@ export default {
|
||||
UserMessageBubble,
|
||||
MessageReplyButton,
|
||||
ImageBubble,
|
||||
VideoBubble,
|
||||
FileBubble,
|
||||
FluentIcon,
|
||||
ReplyToChip,
|
||||
@@ -107,6 +117,7 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
hasImageError: false,
|
||||
hasVideoError: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -143,10 +154,12 @@ export default {
|
||||
watch: {
|
||||
message() {
|
||||
this.hasImageError = false;
|
||||
this.hasVideoError = false;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.hasImageError = false;
|
||||
this.hasVideoError = false;
|
||||
},
|
||||
methods: {
|
||||
async retrySendMessage() {
|
||||
@@ -158,6 +171,9 @@ export default {
|
||||
onImageLoadError() {
|
||||
this.hasImageError = true;
|
||||
},
|
||||
onVideoLoadError() {
|
||||
this.hasVideoError = true;
|
||||
},
|
||||
toggleReply() {
|
||||
this.$emitter.emit(BUS_EVENTS.TOGGLE_REPLY_TO_MESSAGE, this.message);
|
||||
},
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
url: { type: String, default: '' },
|
||||
readableTime: { type: String, default: '' },
|
||||
});
|
||||
|
||||
const emits = defineEmits(['error']);
|
||||
|
||||
const onVideoError = () => {
|
||||
emits('error');
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div class="block relative max-w-full">
|
||||
<video
|
||||
class="w-full max-w-[250px] h-auto"
|
||||
:src="url"
|
||||
controls
|
||||
@error="onVideoError"
|
||||
/>
|
||||
<span
|
||||
class="text-xs absolute text-white dark:text-white right-3 bottom-1 whitespace-nowrap"
|
||||
>
|
||||
{{ readableTime }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
@@ -49,6 +49,10 @@ class AccountUser < ApplicationRecord
|
||||
::Agents::DestroyJob.perform_later(account, user)
|
||||
end
|
||||
|
||||
def permissions
|
||||
administrator? ? ['administrator'] : ['agent']
|
||||
end
|
||||
|
||||
def push_event_data
|
||||
{
|
||||
id: id,
|
||||
|
||||
@@ -48,7 +48,6 @@ class JsonSchemaValidator < ActiveModel::Validator
|
||||
|
||||
# Add validation errors to the record with a formatted statement
|
||||
validation_errors.each do |error|
|
||||
# byebug
|
||||
format_and_append_error(error, record)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -50,7 +50,11 @@ class ActionService
|
||||
end
|
||||
|
||||
def assign_team(team_ids = [])
|
||||
return unassign_team if team_ids[0]&.zero?
|
||||
# FIXME: The explicit checks for zero or nil (string) is bad. Move
|
||||
# this to a separate unassign action.
|
||||
should_unassign = team_ids.blank? || %w[nil 0].include?(team_ids[0].to_s)
|
||||
return @conversation.update!(team_id: nil) if should_unassign
|
||||
|
||||
# check if team belongs to account only if team_id is present
|
||||
# if team_id is nil, then it means that the team is being unassigned
|
||||
return unless !team_ids[0].nil? && team_belongs_to_account?(team_ids)
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
class Notification::FcmService
|
||||
SCOPES = ['https://www.googleapis.com/auth/firebase.messaging'].freeze
|
||||
|
||||
def initialize(project_id, credentials)
|
||||
@project_id = project_id
|
||||
@credentials = credentials
|
||||
@token_info = nil
|
||||
end
|
||||
|
||||
def fcm_client
|
||||
FCM.new(current_token, credentials_path, @project_id)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def current_token
|
||||
@token_info = generate_token if @token_info.nil? || token_expired?
|
||||
@token_info[:token]
|
||||
end
|
||||
|
||||
def token_expired?
|
||||
Time.zone.now >= @token_info[:expires_at]
|
||||
end
|
||||
|
||||
def generate_token
|
||||
authorizer = Google::Auth::ServiceAccountCredentials.make_creds(
|
||||
json_key_io: credentials_path,
|
||||
scope: SCOPES
|
||||
)
|
||||
token = authorizer.fetch_access_token!
|
||||
{
|
||||
token: token['access_token'],
|
||||
expires_at: Time.zone.now + token['expires_in'].to_i
|
||||
}
|
||||
end
|
||||
|
||||
def credentials_path
|
||||
StringIO.new(@credentials)
|
||||
end
|
||||
end
|
||||
@@ -42,14 +42,12 @@ class Notification::PushNotificationService
|
||||
app_account_conversation_url(account_id: conversation.account_id, id: conversation.display_id)
|
||||
end
|
||||
|
||||
def send_browser_push?(subscription)
|
||||
def can_send_browser_push?(subscription)
|
||||
VapidService.public_key && subscription.browser_push?
|
||||
end
|
||||
|
||||
def send_browser_push(subscription)
|
||||
return unless send_browser_push?(subscription)
|
||||
|
||||
WebPush.payload_send(
|
||||
def browser_push_payload(subscription)
|
||||
{
|
||||
message: JSON.generate(push_message),
|
||||
endpoint: subscription.subscription_attributes['endpoint'],
|
||||
p256dh: subscription.subscription_attributes['p256dh'],
|
||||
@@ -62,44 +60,104 @@ class Notification::PushNotificationService
|
||||
ssl_timeout: 5,
|
||||
open_timeout: 5,
|
||||
read_timeout: 5
|
||||
)
|
||||
rescue WebPush::ExpiredSubscription
|
||||
}
|
||||
end
|
||||
|
||||
def send_browser_push(subscription)
|
||||
return unless can_send_browser_push?(subscription)
|
||||
|
||||
WebPush.payload_send(**browser_push_payload(subscription))
|
||||
Rails.logger.info("Browser push sent to #{user.email} with title #{push_message[:title]}")
|
||||
rescue WebPush::ExpiredSubscription, WebPush::InvalidSubscription, WebPush::Unauthorized => e
|
||||
Rails.logger.info "WebPush subscription expired: #{e.message}"
|
||||
subscription.destroy!
|
||||
rescue Errno::ECONNRESET, Net::OpenTimeout, Net::ReadTimeout => e
|
||||
Rails.logger.error "WebPush operation error: #{e.message}"
|
||||
rescue StandardError => e
|
||||
ChatwootExceptionTracker.new(e, account: notification.account).capture_exception
|
||||
true
|
||||
end
|
||||
|
||||
def send_fcm_push(subscription)
|
||||
return unless ENV['FCM_SERVER_KEY']
|
||||
return unless firebase_credentials_present?
|
||||
return unless subscription.fcm?
|
||||
|
||||
fcm = FCM.new(ENV.fetch('FCM_SERVER_KEY', nil))
|
||||
response = fcm.send([subscription.subscription_attributes['push_token']], fcm_options)
|
||||
fcm_service = Notification::FcmService.new(
|
||||
GlobalConfigService.load('FIREBASE_PROJECT_ID', nil), GlobalConfigService.load('FIREBASE_CREDENTIALS', nil)
|
||||
)
|
||||
fcm = fcm_service.fcm_client
|
||||
response = fcm.send_v1(fcm_options(subscription))
|
||||
remove_subscription_if_error(subscription, response)
|
||||
end
|
||||
|
||||
def send_push_via_chatwoot_hub(subscription)
|
||||
return if ENV['FCM_SERVER_KEY']
|
||||
return unless ActiveModel::Type::Boolean.new.cast(ENV.fetch('ENABLE_PUSH_RELAY_SERVER', true))
|
||||
return if firebase_credentials_present?
|
||||
return unless chatwoot_hub_enabled?
|
||||
return unless subscription.fcm?
|
||||
|
||||
ChatwootHub.send_browser_push([subscription.subscription_attributes['push_token']], fcm_options)
|
||||
ChatwootHub.send_push(fcm_options(subscription))
|
||||
end
|
||||
|
||||
def firebase_credentials_present?
|
||||
GlobalConfigService.load('FIREBASE_PROJECT_ID', nil) && GlobalConfigService.load('FIREBASE_CREDENTIALS', nil)
|
||||
end
|
||||
|
||||
def chatwoot_hub_enabled?
|
||||
ActiveModel::Type::Boolean.new.cast(ENV.fetch('ENABLE_PUSH_RELAY_SERVER', true))
|
||||
end
|
||||
|
||||
def remove_subscription_if_error(subscription, response)
|
||||
subscription.destroy! if JSON.parse(response[:body])['results']&.first&.keys&.include?('error')
|
||||
if JSON.parse(response[:body])['results']&.first&.keys&.include?('error')
|
||||
subscription.destroy!
|
||||
else
|
||||
Rails.logger.info("FCM push sent to #{user.email} with title #{push_message[:title]}")
|
||||
end
|
||||
end
|
||||
|
||||
def fcm_options
|
||||
def fcm_options(subscription)
|
||||
{
|
||||
notification: {
|
||||
title: notification.push_message_title,
|
||||
body: notification.push_message_body,
|
||||
sound: 'default'
|
||||
},
|
||||
android: { priority: 'high' },
|
||||
data: { notification: notification.fcm_push_data.to_json },
|
||||
collapse_key: "chatwoot_#{notification.primary_actor_type.downcase}_#{notification.primary_actor_id}"
|
||||
'token': subscription.subscription_attributes['push_token'],
|
||||
'data': fcm_data,
|
||||
'notification': fcm_notification,
|
||||
'android': fcm_android_options,
|
||||
'apns': fcm_apns_options,
|
||||
'fcm_options': {
|
||||
analytics_label: 'Label'
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def fcm_data
|
||||
{
|
||||
payload: {
|
||||
data: {
|
||||
notification: notification.fcm_push_data
|
||||
}
|
||||
}.to_json
|
||||
}
|
||||
end
|
||||
|
||||
def fcm_notification
|
||||
{
|
||||
title: notification.push_message_title,
|
||||
body: notification.push_message_body
|
||||
}
|
||||
end
|
||||
|
||||
def fcm_android_options
|
||||
{
|
||||
priority: 'high'
|
||||
}
|
||||
end
|
||||
|
||||
def fcm_apns_options
|
||||
{
|
||||
payload: {
|
||||
aps: {
|
||||
sound: 'default',
|
||||
category: Time.zone.now.to_i.to_s
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -14,6 +14,7 @@ json.provider resource.provider
|
||||
json.pubsub_token resource.pubsub_token
|
||||
json.custom_attributes resource.custom_attributes if resource.custom_attributes.present?
|
||||
json.role resource.active_account_user&.role
|
||||
json.permissions resource.active_account_user&.permissions
|
||||
json.ui_settings resource.ui_settings
|
||||
json.uid resource.uid
|
||||
json.type resource.type
|
||||
@@ -24,6 +25,7 @@ json.accounts do
|
||||
json.status account_user.account.status
|
||||
json.active_at account_user.active_at
|
||||
json.role account_user.role
|
||||
json.permissions account_user.permissions
|
||||
# the actual availability user has configured
|
||||
json.availability account_user.availability
|
||||
# availability derived from presence
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
shared: &shared
|
||||
version: '3.9.0'
|
||||
version: '3.10.2'
|
||||
|
||||
development:
|
||||
<<: *shared
|
||||
|
||||
@@ -24,6 +24,8 @@ Rails.application.configure do
|
||||
smtp_settings[:openssl_verify_mode] = ENV['SMTP_OPENSSL_VERIFY_MODE'] if ENV['SMTP_OPENSSL_VERIFY_MODE'].present?
|
||||
smtp_settings[:ssl] = ActiveModel::Type::Boolean.new.cast(ENV.fetch('SMTP_SSL', true)) if ENV['SMTP_SSL']
|
||||
smtp_settings[:tls] = ActiveModel::Type::Boolean.new.cast(ENV.fetch('SMTP_TLS', true)) if ENV['SMTP_TLS']
|
||||
smtp_settings[:open_timeout] = ENV['SMTP_OPEN_TIMEOUT'].to_i if ENV['SMTP_OPEN_TIMEOUT'].present?
|
||||
smtp_settings[:read_timeout] = ENV['SMTP_READ_TIMEOUT'].to_i if ENV['SMTP_READ_TIMEOUT'].present?
|
||||
|
||||
config.action_mailer.delivery_method = :smtp unless Rails.env.test?
|
||||
config.action_mailer.smtp_settings = smtp_settings
|
||||
|
||||
@@ -204,3 +204,16 @@
|
||||
locked: false
|
||||
description: 'Disable rendering profile update page for users'
|
||||
## ------ End of Configs added for enterprise clients ------ ##
|
||||
|
||||
## ------ Configs added for FCM v1 notifications ------ ##
|
||||
- name: FIREBASE_PROJECT_ID
|
||||
display_title: 'Firebase Project ID'
|
||||
value:
|
||||
locked: false
|
||||
description: 'Firebase project ID'
|
||||
- name: FIREBASE_CREDENTIALS
|
||||
display_title: 'Firebase Credentials'
|
||||
value:
|
||||
locked: false
|
||||
description: 'Contents on your firebase credentials json file'
|
||||
## ------ End of Configs added for FCM v1 notifications ------ ##
|
||||
|
||||
+40
-40
@@ -31,7 +31,7 @@ vi:
|
||||
disposable_email: Chúng tôi không cho phép các email dùng một lần
|
||||
invalid_email: Bạn đã nhập một email không hợp lệ
|
||||
email_already_exists: "Bạn đã đăng ký một tài khoản với %{email}"
|
||||
invalid_params: 'Invalid, please check the signup paramters and try again'
|
||||
invalid_params: 'Không hợp lệ, vui lòng kiểm tra thông số đăng ký và thử lại'
|
||||
failed: Đăng ký thât bại
|
||||
data_import:
|
||||
data_type:
|
||||
@@ -51,7 +51,7 @@ vi:
|
||||
dyte:
|
||||
invalid_message_type: "Loại tin nhắn không hợp lệ. Hành động không được phép"
|
||||
slack:
|
||||
invalid_channel_id: "Invalid slack channel. Please try again"
|
||||
invalid_channel_id: "Kênh chùng không hợp lệ. Vui lòng thử lại"
|
||||
inboxes:
|
||||
imap:
|
||||
socket_error: Vui lòng kiểm tra kết nối mạng, địa chỉ IMAP và thử lại.
|
||||
@@ -63,43 +63,43 @@ vi:
|
||||
name: không nên bắt đầu hoặc kết thúc bằng các ký hiệu và không nên có kí tự < > / \ @.
|
||||
custom_filters:
|
||||
number_of_records: Đã đạt giới hạn. Số lượng tuỳ chọn lọc tối đa cho mỗi mỗi người dùng mỗi tài khoản là 50.
|
||||
invalid_attribute: Invalid attribute key - [%{key}]. The key should be one of [%{allowed_keys}] or a custom attribute defined in the account.
|
||||
invalid_operator: Invalid operator. The allowed operators for %{attribute_name} are [%{allowed_keys}].
|
||||
invalid_value: Invalid value. The values provided for %{attribute_name} are invalid
|
||||
invalid_attribute: Khóa thuộc tính không hợp lệ - [%{key}]. Chìa khóa phải là một trong [%{allowed_keys}] hoặc thuộc tính tùy chỉnh được xác định trong tài khoản.
|
||||
invalid_operator: Toán tử không hợp lệ. Các toán tử được phép cho %{attribute_name} là [%{allowed_keys}].
|
||||
invalid_value: Giá trị không hợp lệ. Các giá trị được cung cấp cho %{attribute_name} không hợp lệ
|
||||
reports:
|
||||
period: Thời gian báo cáo từ %{since} đến %{until}
|
||||
utc_warning: Báo cáo đã được tạo với múi giờ UTC
|
||||
agent_csv:
|
||||
agent_name: Tên tổng đài viên
|
||||
conversations_count: Assigned conversations
|
||||
avg_first_response_time: Avg first response time
|
||||
conversations_count: Cuộc trò chuyện được chỉ định
|
||||
avg_first_response_time: Thời gian phản hồi đầu tiên trung bình
|
||||
avg_resolution_time: Avg resolution time
|
||||
resolution_count: Số lượng giải quyết
|
||||
avg_customer_waiting_time: Avg customer waiting time
|
||||
avg_customer_waiting_time: Thời gian chờ đợi trung bình của khách hàng
|
||||
inbox_csv:
|
||||
inbox_name: Tên kênh
|
||||
inbox_type: Kiểu kênh
|
||||
conversations_count: Số hội thoại
|
||||
avg_first_response_time: Avg first response time
|
||||
avg_resolution_time: Avg resolution time
|
||||
avg_first_response_time: Thời gian phản hồi đầu tiên trung bình
|
||||
avg_resolution_time: Thời gian giải quyết trung bình
|
||||
label_csv:
|
||||
label_title: Nhãn
|
||||
conversations_count: Số hội thoại
|
||||
avg_first_response_time: Avg first response time
|
||||
avg_resolution_time: Avg resolution time
|
||||
avg_first_response_time: Thời gian phản hồi đầu tiên trung bình
|
||||
avg_resolution_time: Thời gian giải quyết trung bình
|
||||
team_csv:
|
||||
team_name: Tên nhóm
|
||||
conversations_count: Số hội thoại
|
||||
avg_first_response_time: Avg first response time
|
||||
avg_resolution_time: Avg resolution time
|
||||
avg_first_response_time: Thời gian phản hồi đầu tiên trung bình
|
||||
avg_resolution_time: Thời gian giải quyết trung bình
|
||||
resolution_count: Số lượng giải quyết
|
||||
avg_customer_waiting_time: Avg customer waiting time
|
||||
avg_customer_waiting_time: Thời gian chờ đợi trung bình của khách hàng
|
||||
conversation_traffic_csv:
|
||||
timezone: Múi giờ
|
||||
sla_csv:
|
||||
conversation_id: Conversation ID
|
||||
conversation_id: ID hội thoại
|
||||
sla_policy_breached: SLA Policy
|
||||
assignee: Assignee
|
||||
assignee: Đại lý được chỉ định
|
||||
team: Nhóm
|
||||
inbox: Hộp thư đến
|
||||
labels: Nhãn
|
||||
@@ -118,22 +118,22 @@ vi:
|
||||
recorded_at: Ngày nghi
|
||||
notifications:
|
||||
notification_title:
|
||||
conversation_creation: "A conversation (#%{display_id}) has been created in %{inbox_name}"
|
||||
conversation_assignment: "A conversation (#%{display_id}) has been assigned to you"
|
||||
assigned_conversation_new_message: "A new message is created in conversation (#%{display_id})"
|
||||
conversation_mention: "You have been mentioned in conversation (#%{display_id})"
|
||||
sla_missed_first_response: "SLA target first response missed for conversation (#%{display_id})"
|
||||
sla_missed_next_response: "SLA target next response missed for conversation (#%{display_id})"
|
||||
sla_missed_resolution: "SLA target resolution missed for conversation (#%{display_id})"
|
||||
attachment: "Attachment"
|
||||
no_content: "No content"
|
||||
conversation_creation: "Một cuộc trò chuyện (#%{display_id}) đã được tạo trong %{inbox_name}"
|
||||
conversation_assignment: "Một cuộc trò chuyện (#%{display_id}) đã được chỉ định cho bạn"
|
||||
assigned_conversation_new_message: "Một tin nhắn mới được tạo trong cuộc trò chuyện (#%{display_id})"
|
||||
conversation_mention: "Bạn đã được nhắc đến trong cuộc trò chuyện (#%{display_id})"
|
||||
sla_missed_first_response: "Mục tiêu SLA phản hồi đầu tiên bị bỏ lỡ cho cuộc trò chuyện (#%{display_id})"
|
||||
sla_missed_next_response: "Mục tiêu SLA phản hồi tiếp theo bị bỏ lỡ cho cuộc trò chuyện (#%{display_id})"
|
||||
sla_missed_resolution: "Độ phân giải mục tiêu SLA bị bỏ lỡ cho cuộc trò chuyện (#%{display_id})"
|
||||
attachment: "Tập tin đính kèm"
|
||||
no_content: "Không có nội dung"
|
||||
conversations:
|
||||
messages:
|
||||
instagram_story_content: "%{story_sender} đã đề cập đến bạn trong hội thoại: "
|
||||
instagram_deleted_story_content: Hội thoại này không còn nữa.
|
||||
deleted: Tin nhắn đã bị xoá
|
||||
delivery_status:
|
||||
error_code: "Error code: %{error_code}"
|
||||
error_code: "Mã lỗi: %{error_code}"
|
||||
activity:
|
||||
status:
|
||||
resolved: "Cuộc trò chuyện được đánh dấu là đã giải quyết bởi %{user_name}"
|
||||
@@ -221,7 +221,7 @@ vi:
|
||||
common:
|
||||
home: Trang Chủ
|
||||
last_updated_on: 'Cập nhật lần cuối: %{last_updated_on}'
|
||||
view_all_articles: View all
|
||||
view_all_articles: Xem tất cả
|
||||
article: bài viết
|
||||
articles: bài viết
|
||||
author: tác giả
|
||||
@@ -233,17 +233,17 @@ vi:
|
||||
footer:
|
||||
made_with: Tạo bởi
|
||||
header:
|
||||
go_to_homepage: Website
|
||||
go_to_homepage: Trang web
|
||||
appearance:
|
||||
system: System
|
||||
light: Light
|
||||
dark: Dark
|
||||
featured_articles: Featured Articles
|
||||
system: Hệ thống
|
||||
light: Sáng
|
||||
dark: Tối
|
||||
featured_articles: Bài viết nổi bật
|
||||
uncategorized: Chưa được phân loại
|
||||
404:
|
||||
title: Page not found
|
||||
description: We couldn't find the page you were looking for.
|
||||
back_to_home: Go to home page
|
||||
title: Không tìm thấy trang
|
||||
description: Chúng tôi không thể tìm thấy trang bạn đang tìm kiếm.
|
||||
back_to_home: Tới trang chủ
|
||||
slack_unfurl:
|
||||
fields:
|
||||
name: Tên
|
||||
@@ -255,10 +255,10 @@ vi:
|
||||
button: Mở cuộc trò chuyện
|
||||
time_units:
|
||||
days:
|
||||
other: "%{count} days"
|
||||
other: "%{count} ngày"
|
||||
hours:
|
||||
other: "%{count} hours"
|
||||
other: "%{count} giờ"
|
||||
minutes:
|
||||
other: "%{count} minutes"
|
||||
other: "%{count} phút"
|
||||
seconds:
|
||||
other: "%{count} seconds"
|
||||
other: "%{count} giây"
|
||||
|
||||
+2
-2
@@ -77,8 +77,8 @@ class ChatwootHub
|
||||
ChatwootExceptionTracker.new(e).capture_exception
|
||||
end
|
||||
|
||||
def self.send_browser_push(fcm_token_list, fcm_options)
|
||||
info = { fcm_token_list: fcm_token_list, fcm_options: fcm_options }
|
||||
def self.send_push(fcm_options)
|
||||
info = { fcm_options: fcm_options }
|
||||
RestClient.post(PUSH_NOTIFICATION_URL, info.merge(instance_config).to_json, { content_type: :json, accept: :json })
|
||||
rescue *ExceptionList::REST_CLIENT_EXCEPTIONS => e
|
||||
Rails.logger.error "Exception: #{e.message}"
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@chatwoot/chatwoot",
|
||||
"version": "3.9.0",
|
||||
"version": "3.10.2",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"eslint": "eslint app/**/*.{js,vue}",
|
||||
|
||||
@@ -41,6 +41,17 @@ RSpec.describe 'Session', type: :request do
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(response.body).to include(user_with_new_pwd.email)
|
||||
end
|
||||
|
||||
it 'returns the permission of the user' do
|
||||
params = { email: user.email, password: 'Password1!' }
|
||||
|
||||
post new_user_session_url,
|
||||
params: params,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(response.parsed_body['data']['permissions']).to eq(['agent'])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is invalid sso auth token' do
|
||||
|
||||
@@ -16,5 +16,17 @@ FactoryBot.define do
|
||||
conversation.contact ||= create(:contact, :with_email, account: conversation.account)
|
||||
conversation.contact_inbox ||= create(:contact_inbox, contact: conversation.contact, inbox: conversation.inbox)
|
||||
end
|
||||
|
||||
trait :with_team do
|
||||
after(:build) do |conversation|
|
||||
conversation.team ||= create(:team, account: conversation.account)
|
||||
end
|
||||
end
|
||||
|
||||
trait :with_assignee do
|
||||
after(:build) do |conversation|
|
||||
conversation.assignee ||= create(:user, account: conversation.account, role: :agent)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -6,5 +6,13 @@ FactoryBot.define do
|
||||
identifier { 'test' }
|
||||
subscription_type { 'browser_push' }
|
||||
subscription_attributes { { endpoint: 'test', auth: 'test' } }
|
||||
|
||||
trait :browser_push do
|
||||
subscription_type { 'browser_push' }
|
||||
end
|
||||
|
||||
trait :fcm do
|
||||
subscription_type { 'fcm' }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -17,6 +17,17 @@ RSpec.describe AccountUser do
|
||||
end
|
||||
end
|
||||
|
||||
describe 'permissions' do
|
||||
it 'returns the right permissions' do
|
||||
expect(account_user.permissions).to eq(['agent'])
|
||||
end
|
||||
|
||||
it 'returns the right permissions for administrator' do
|
||||
account_user.administrator!
|
||||
expect(account_user.permissions).to eq(['administrator'])
|
||||
end
|
||||
end
|
||||
|
||||
describe 'destroy call agent::destroy service' do
|
||||
it 'gets created with the right default settings' do
|
||||
create(:conversation, account: account_user.account, assignee: account_user.user, inbox: inbox)
|
||||
|
||||
@@ -31,18 +31,54 @@ describe ActionService do
|
||||
|
||||
describe '#assign_agent' do
|
||||
let(:agent) { create(:user, account: account, role: :agent) }
|
||||
let(:conversation) { create(:conversation, account: account) }
|
||||
let(:inbox_member) { create(:inbox_member, inbox: conversation.inbox, user: agent) }
|
||||
let(:conversation) { create(:conversation, :with_assignee, account: account) }
|
||||
let(:action_service) { described_class.new(conversation) }
|
||||
|
||||
it 'unassigns the conversation if agent id is nil' do
|
||||
action_service.assign_agent(['nil'])
|
||||
expect(conversation.reload.assignee).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
it 'unassigns the team if team_id is nil' do
|
||||
action_service.assign_team([nil])
|
||||
expect(conversation.reload.team).to be_nil
|
||||
describe '#assign_team' do
|
||||
let(:agent) { create(:user, account: account, role: :agent) }
|
||||
let(:inbox_member) { create(:inbox_member, inbox: conversation.inbox, user: agent) }
|
||||
let(:team) { create(:team, name: 'ConversationTeam', account: account) }
|
||||
let(:conversation) { create(:conversation, :with_team, account: account) }
|
||||
let(:action_service) { described_class.new(conversation) }
|
||||
|
||||
context 'when team_id is not present' do
|
||||
it 'unassign the if team_id is "nil"' do
|
||||
expect do
|
||||
action_service.assign_team(['nil'])
|
||||
end.not_to raise_error
|
||||
expect(conversation.reload.team).to be_nil
|
||||
end
|
||||
|
||||
it 'unassign the if team_id is 0' do
|
||||
expect do
|
||||
action_service.assign_team([0])
|
||||
end.not_to raise_error
|
||||
expect(conversation.reload.team).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when team_id is present' do
|
||||
it 'assign the team if the team is part of the account' do
|
||||
original_team = conversation.team
|
||||
expect do
|
||||
action_service.assign_team([team.id])
|
||||
end.to change { conversation.reload.team }.from(original_team)
|
||||
end
|
||||
|
||||
it 'does not assign the team if the team is part of the account' do
|
||||
original_team = conversation.team
|
||||
invalid_team_id = 999_999_999
|
||||
expect do
|
||||
action_service.assign_team([invalid_team_id])
|
||||
end.not_to change { conversation.reload.team }.from(original_team)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe Notification::FcmService do
|
||||
let(:project_id) { 'test_project_id' }
|
||||
let(:credentials) { '{ "type": "service_account", "project_id": "test_project_id" }' }
|
||||
let(:fcm_service) { described_class.new(project_id, credentials) }
|
||||
let(:fcm_double) { instance_double(FCM) }
|
||||
let(:token_info) { { token: 'test_token', expires_at: 1.hour.from_now } }
|
||||
let(:creds_double) do
|
||||
instance_double(Google::Auth::ServiceAccountCredentials, fetch_access_token!: { 'access_token' => 'test_token', 'expires_in' => 3600 })
|
||||
end
|
||||
|
||||
before do
|
||||
allow(FCM).to receive(:new).and_return(fcm_double)
|
||||
allow(fcm_service).to receive(:generate_token).and_return(token_info)
|
||||
allow(Google::Auth::ServiceAccountCredentials).to receive(:make_creds).and_return(creds_double)
|
||||
end
|
||||
|
||||
describe '#fcm_client' do
|
||||
it 'returns an FCM client' do
|
||||
expect(fcm_service.fcm_client).to eq(fcm_double)
|
||||
expect(FCM).to have_received(:new).with('test_token', anything, project_id)
|
||||
end
|
||||
|
||||
it 'generates a new token if expired' do
|
||||
allow(fcm_service).to receive(:generate_token).and_return(token_info)
|
||||
allow(fcm_service).to receive(:token_expired?).and_return(true)
|
||||
|
||||
expect(fcm_service.fcm_client).to eq(fcm_double)
|
||||
expect(FCM).to have_received(:new).with('test_token', anything, project_id)
|
||||
expect(fcm_service).to have_received(:generate_token)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'private methods' do
|
||||
describe '#current_token' do
|
||||
it 'returns the current token if not expired' do
|
||||
fcm_service.instance_variable_set(:@token_info, token_info)
|
||||
expect(fcm_service.send(:current_token)).to eq('test_token')
|
||||
end
|
||||
|
||||
it 'generates a new token if expired' do
|
||||
expired_token_info = { token: 'expired_token', expires_at: 1.hour.ago }
|
||||
fcm_service.instance_variable_set(:@token_info, expired_token_info)
|
||||
allow(fcm_service).to receive(:generate_token).and_return(token_info)
|
||||
|
||||
expect(fcm_service.send(:current_token)).to eq('test_token')
|
||||
expect(fcm_service).to have_received(:generate_token)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#generate_token' do
|
||||
it 'generates a new token' do
|
||||
allow(Google::Auth::ServiceAccountCredentials).to receive(:make_creds).and_return(creds_double)
|
||||
|
||||
token = fcm_service.send(:generate_token)
|
||||
expect(token[:token]).to eq('test_token')
|
||||
expect(token[:expires_at]).to be_within(1.second).of(Time.zone.now + 3600)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#credentials_path' do
|
||||
it 'creates a StringIO with credentials' do
|
||||
string_io = fcm_service.send(:credentials_path)
|
||||
expect(string_io).to be_a(StringIO)
|
||||
expect(string_io.read).to eq(credentials)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -4,32 +4,61 @@ describe Notification::PushNotificationService do
|
||||
let!(:account) { create(:account) }
|
||||
let!(:user) { create(:user, account: account) }
|
||||
let!(:notification) { create(:notification, user: user, account: user.accounts.first) }
|
||||
let(:fcm_double) { double }
|
||||
|
||||
before do
|
||||
allow(WebPush).to receive(:payload_send).and_return(true)
|
||||
allow(FCM).to receive(:new).and_return(fcm_double)
|
||||
allow(fcm_double).to receive(:send).and_return({ body: { 'results': [] }.to_json })
|
||||
end
|
||||
let(:fcm_double) { instance_double(FCM) }
|
||||
let(:fcm_service_double) { instance_double(Notification::FcmService, fcm_client: fcm_double) }
|
||||
|
||||
describe '#perform' do
|
||||
it 'sends webpush notifications for webpush subscription' do
|
||||
with_modified_env VAPID_PUBLIC_KEY: 'test' do
|
||||
create(:notification_subscription, user: notification.user)
|
||||
context 'when the push server returns success' do
|
||||
before do
|
||||
allow(WebPush).to receive(:payload_send).and_return(true)
|
||||
allow(Rails.logger).to receive(:info)
|
||||
allow(Notification::FcmService).to receive(:new).and_return(fcm_service_double)
|
||||
allow(fcm_double).to receive(:send_v1).and_return({ body: { 'results': [] }.to_json })
|
||||
allow(GlobalConfigService).to receive(:load).with('FIREBASE_PROJECT_ID', nil).and_return('test_project_id')
|
||||
allow(GlobalConfigService).to receive(:load).with('FIREBASE_CREDENTIALS', nil).and_return('test_credentials')
|
||||
end
|
||||
|
||||
described_class.new(notification: notification).perform
|
||||
expect(WebPush).to have_received(:payload_send)
|
||||
expect(FCM).not_to have_received(:new)
|
||||
it 'sends webpush notifications for webpush subscription' do
|
||||
with_modified_env VAPID_PUBLIC_KEY: 'test' do
|
||||
create(:notification_subscription, user: notification.user)
|
||||
|
||||
described_class.new(notification: notification).perform
|
||||
expect(WebPush).to have_received(:payload_send)
|
||||
expect(Notification::FcmService).not_to have_received(:new)
|
||||
expect(Rails.logger).to have_received(:info).with("Browser push sent to #{user.email} with title #{notification.push_message_title}")
|
||||
end
|
||||
end
|
||||
|
||||
it 'sends a fcm notification for firebase subscription' do
|
||||
with_modified_env ENABLE_PUSH_RELAY_SERVER: 'false' do
|
||||
create(:notification_subscription, user: notification.user, subscription_type: 'fcm')
|
||||
|
||||
described_class.new(notification: notification).perform
|
||||
expect(Notification::FcmService).to have_received(:new)
|
||||
expect(fcm_double).to have_received(:send_v1)
|
||||
expect(WebPush).not_to have_received(:payload_send)
|
||||
expect(Rails.logger).to have_received(:info).with("FCM push sent to #{user.email} with title #{notification.push_message_title}")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'sends a fcm notification for firebase subscription' do
|
||||
with_modified_env FCM_SERVER_KEY: 'test', ENABLE_PUSH_RELAY_SERVER: 'false' do
|
||||
create(:notification_subscription, user: notification.user, subscription_type: 'fcm')
|
||||
context 'when the push server returns error' do
|
||||
it 'sends webpush notifications for webpush subscription' do
|
||||
with_modified_env VAPID_PUBLIC_KEY: 'test' do
|
||||
mock_response = instance_double(Net::HTTPResponse, body: 'Subscription is invalid')
|
||||
mock_host = 'fcm.googleapis.com'
|
||||
|
||||
allow(WebPush).to receive(:payload_send).and_raise(WebPush::InvalidSubscription.new(mock_response, mock_host))
|
||||
allow(Rails.logger).to receive(:info)
|
||||
|
||||
create(:notification_subscription, :browser_push, user: notification.user)
|
||||
|
||||
expect(Rails.logger).to receive(:info) do |message|
|
||||
expect(message).to include('WebPush subscription expired:')
|
||||
end
|
||||
|
||||
described_class.new(notification: notification).perform
|
||||
expect(FCM).to have_received(:new)
|
||||
expect(WebPush).not_to have_received(:payload_send)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -21315,21 +21315,16 @@ write-file-atomic@^4.0.2:
|
||||
signal-exit "^3.0.7"
|
||||
|
||||
ws@^6.2.1:
|
||||
version "6.2.2"
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.2.tgz#dd5cdbd57a9979916097652d78f1cc5faea0c32e"
|
||||
integrity sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw==
|
||||
version "6.2.3"
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.3.tgz#ccc96e4add5fd6fedbc491903075c85c5a11d9ee"
|
||||
integrity sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA==
|
||||
dependencies:
|
||||
async-limiter "~1.0.0"
|
||||
|
||||
ws@^8.11.0:
|
||||
version "8.14.2"
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-8.14.2.tgz#6c249a806eb2db7a20d26d51e7709eab7b2e6c7f"
|
||||
integrity sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==
|
||||
|
||||
ws@^8.2.3:
|
||||
version "8.13.0"
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0"
|
||||
integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==
|
||||
ws@^8.11.0, ws@^8.2.3:
|
||||
version "8.17.1"
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.1.tgz#9293da530bb548febc95371d90f9c878727d919b"
|
||||
integrity sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==
|
||||
|
||||
x-default-browser@^0.4.0:
|
||||
version "0.4.0"
|
||||
|
||||
Reference in New Issue
Block a user