Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2295fc1ebd | ||
|
|
ea4477ccde | ||
|
|
a2857cac38 | ||
|
|
293a29ec98 | ||
|
|
ddada56753 | ||
|
|
578e2ee8db | ||
|
|
b683973e79 | ||
|
|
2cfca6008b | ||
|
|
d2f5311400 | ||
|
|
f6dbbf0d90 | ||
|
|
768fa9ab1b |
@@ -1,32 +1,23 @@
|
||||
class Api::V1::Accounts::Google::AuthorizationsController < Api::V1::Accounts::BaseController
|
||||
class Api::V1::Accounts::Google::AuthorizationsController < Api::V1::Accounts::OauthAuthorizationController
|
||||
include GoogleConcern
|
||||
before_action :check_authorization
|
||||
|
||||
def create
|
||||
email = params[:authorization][:email]
|
||||
redirect_url = google_client.auth_code.authorize_url(
|
||||
{
|
||||
redirect_uri: "#{base_url}/google/callback",
|
||||
scope: 'email profile https://mail.google.com/',
|
||||
scope: scope,
|
||||
response_type: 'code',
|
||||
prompt: 'consent', # the oauth flow does not return a refresh token, this is supposed to fix it
|
||||
access_type: 'offline', # the default is 'online'
|
||||
state: state,
|
||||
client_id: GlobalConfigService.load('GOOGLE_OAUTH_CLIENT_ID', nil)
|
||||
}
|
||||
)
|
||||
|
||||
if redirect_url
|
||||
cache_key = "google::#{email.downcase}"
|
||||
::Redis::Alfred.setex(cache_key, Current.account.id, 5.minutes)
|
||||
render json: { success: true, url: redirect_url }
|
||||
else
|
||||
render json: { success: false }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_authorization
|
||||
raise Pundit::NotAuthorizedError unless Current.account_user.administrator?
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
class Api::V1::Accounts::Instagram::AuthorizationsController < Api::V1::Accounts::BaseController
|
||||
class Api::V1::Accounts::Instagram::AuthorizationsController < Api::V1::Accounts::OauthAuthorizationController
|
||||
include InstagramConcern
|
||||
include Instagram::IntegrationHelper
|
||||
before_action :check_authorization
|
||||
|
||||
def create
|
||||
# https://developers.facebook.com/docs/instagram-platform/instagram-api-with-instagram-login/business-login#step-1--get-authorization
|
||||
@@ -21,10 +20,4 @@ class Api::V1::Accounts::Instagram::AuthorizationsController < Api::V1::Accounts
|
||||
render json: { success: false }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_authorization
|
||||
raise Pundit::NotAuthorizedError unless Current.account_user.administrator?
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,28 +1,19 @@
|
||||
class Api::V1::Accounts::Microsoft::AuthorizationsController < Api::V1::Accounts::BaseController
|
||||
class Api::V1::Accounts::Microsoft::AuthorizationsController < Api::V1::Accounts::OauthAuthorizationController
|
||||
include MicrosoftConcern
|
||||
before_action :check_authorization
|
||||
|
||||
def create
|
||||
email = params[:authorization][:email]
|
||||
redirect_url = microsoft_client.auth_code.authorize_url(
|
||||
{
|
||||
redirect_uri: "#{base_url}/microsoft/callback",
|
||||
scope: 'offline_access https://outlook.office.com/IMAP.AccessAsUser.All https://outlook.office.com/SMTP.Send openid profile',
|
||||
scope: scope,
|
||||
state: state,
|
||||
prompt: 'consent'
|
||||
}
|
||||
)
|
||||
if redirect_url
|
||||
cache_key = "microsoft::#{email.downcase}"
|
||||
::Redis::Alfred.setex(cache_key, Current.account.id, 5.minutes)
|
||||
render json: { success: true, url: redirect_url }
|
||||
else
|
||||
render json: { success: false }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_authorization
|
||||
raise Pundit::NotAuthorizedError unless Current.account_user.administrator?
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
class Api::V1::Accounts::OauthAuthorizationController < Api::V1::Accounts::BaseController
|
||||
before_action :check_authorization
|
||||
|
||||
protected
|
||||
|
||||
def scope
|
||||
''
|
||||
end
|
||||
|
||||
def state
|
||||
Current.account.to_sgid(expires_in: 15.minutes).to_s
|
||||
end
|
||||
|
||||
def base_url
|
||||
ENV.fetch('FRONTEND_URL', 'http://localhost:3000')
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_authorization
|
||||
raise Pundit::NotAuthorizedError unless Current.account_user.administrator?
|
||||
end
|
||||
end
|
||||
@@ -14,7 +14,7 @@ module GoogleConcern
|
||||
|
||||
private
|
||||
|
||||
def base_url
|
||||
ENV.fetch('FRONTEND_URL', 'http://localhost:3000')
|
||||
def scope
|
||||
'email profile https://mail.google.com/'
|
||||
end
|
||||
end
|
||||
|
||||
@@ -15,7 +15,7 @@ module MicrosoftConcern
|
||||
|
||||
private
|
||||
|
||||
def base_url
|
||||
ENV.fetch('FRONTEND_URL', 'http://localhost:3000')
|
||||
def scope
|
||||
'offline_access https://outlook.office.com/IMAP.AccessAsUser.All https://outlook.office.com/SMTP.Send openid profile email'
|
||||
end
|
||||
end
|
||||
|
||||
@@ -6,7 +6,6 @@ class OauthCallbackController < ApplicationController
|
||||
)
|
||||
|
||||
handle_response
|
||||
::Redis::Alfred.delete(cache_key)
|
||||
rescue StandardError => e
|
||||
ChatwootExceptionTracker.new(e).capture_exception
|
||||
redirect_to '/'
|
||||
@@ -64,10 +63,6 @@ class OauthCallbackController < ApplicationController
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
def cache_key
|
||||
"#{provider_name}::#{users_data['email'].downcase}"
|
||||
end
|
||||
|
||||
def create_channel_with_inbox
|
||||
ActiveRecord::Base.transaction do
|
||||
channel_email = Channel::Email.create!(email: users_data['email'], account: account)
|
||||
@@ -86,12 +81,17 @@ class OauthCallbackController < ApplicationController
|
||||
decoded_token[0]
|
||||
end
|
||||
|
||||
def account_id
|
||||
::Redis::Alfred.get(cache_key)
|
||||
def account_from_signed_id
|
||||
raise ActionController::BadRequest, 'Missing state variable' if params[:state].blank?
|
||||
|
||||
account = GlobalID::Locator.locate_signed(params[:state])
|
||||
raise 'Invalid or expired state' if account.nil?
|
||||
|
||||
account
|
||||
end
|
||||
|
||||
def account
|
||||
@account ||= Account.find(account_id)
|
||||
@account ||= account_from_signed_id
|
||||
end
|
||||
|
||||
# Fallback name, for when name field is missing from users_data
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
@apply max-w-full;
|
||||
|
||||
.multiselect__option {
|
||||
@apply text-sm font-normal;
|
||||
@apply text-sm font-normal flex justify-between items-center;
|
||||
|
||||
span {
|
||||
@apply inline-block overflow-hidden text-ellipsis whitespace-nowrap w-fit;
|
||||
@@ -58,7 +58,7 @@
|
||||
}
|
||||
|
||||
&::after {
|
||||
@apply bottom-0 flex items-center justify-center text-center;
|
||||
@apply bottom-0 flex items-center justify-center text-center relative px-1 leading-tight;
|
||||
}
|
||||
|
||||
&.multiselect__option--highlight {
|
||||
@@ -74,7 +74,7 @@
|
||||
}
|
||||
|
||||
&.multiselect__option--highlight::after {
|
||||
@apply bg-transparent;
|
||||
@apply bg-transparent text-n-slate-12;
|
||||
}
|
||||
|
||||
&.multiselect__option--selected {
|
||||
|
||||
@@ -379,7 +379,7 @@ const shouldRenderMessage = computed(() => {
|
||||
function openContextMenu(e) {
|
||||
const shouldSkipContextMenu =
|
||||
e.target?.classList.contains('skip-context-menu') ||
|
||||
e.target?.tagName.toLowerCase() === 'a';
|
||||
['a', 'img'].includes(e.target?.tagName.toLowerCase());
|
||||
if (shouldSkipContextMenu || getSelection().toString()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -756,6 +756,7 @@ function toggleSelectAll(check) {
|
||||
}
|
||||
|
||||
useEmitter('fetch_conversation_stats', () => {
|
||||
if (hasAppliedFiltersOrActiveFolders.value) return;
|
||||
store.dispatch('conversationStats/get', conversationFilters.value);
|
||||
});
|
||||
|
||||
|
||||
@@ -130,15 +130,15 @@ export default {
|
||||
</div>
|
||||
<div class="flex multiselect-wrap--medium">
|
||||
<div
|
||||
class="w-8 relative text-base text-slate-100 dark:text-slate-600 after:content-[''] after:h-12 after:w-0 after:left-4 after:absolute after:border-l after:border-solid after:border-slate-100 after:dark:border-slate-600 before:content-[''] before:h-0 before:w-4 before:left-4 before:top-12 before:absolute before:border-b before:border-solid before:border-slate-100 before:dark:border-slate-600"
|
||||
class="w-8 relative text-base text-slate-100 dark:text-slate-600 after:content-[''] after:h-12 after:w-0 ltr:after:left-4 rtl:after:right-4 after:absolute after:border-l after:border-solid after:border-slate-100 after:dark:border-slate-600 before:content-[''] before:h-0 before:w-4 ltr:before:left-4 rtl:before:right-4 before:top-12 before:absolute before:border-b before:border-solid before:border-slate-100 before:dark:border-slate-600"
|
||||
>
|
||||
<fluent-icon
|
||||
icon="arrow-up"
|
||||
class="absolute -top-1 left-2"
|
||||
class="absolute -top-1 ltr:left-2 rtl:right-2"
|
||||
size="17"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col w-full">
|
||||
<div class="flex flex-col w-full ltr:pl-8 rtl:pr-8">
|
||||
<label class="multiselect__label">
|
||||
{{ $t('MERGE_CONTACTS.PRIMARY.TITLE') }}
|
||||
<woot-label
|
||||
|
||||
@@ -61,7 +61,7 @@ const isDefaultScreen = computed(() => {
|
||||
</h2>
|
||||
<p
|
||||
v-dompurify-html="formatMessage(config.welcomeTagline)"
|
||||
class="text-sm break-words text-n-slate-11 [&_a]:!text-n-slate-11 [&_a]:underline"
|
||||
class="text-sm break-words text-n-slate-11 [&_a]:!text-n-slate-11 [&_a]:underline [&_a:has(>strong)]:no-underline"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
-1
@@ -12,7 +12,6 @@ defineOptions({
|
||||
provider="google"
|
||||
:title="$t('INBOX_MGMT.ADD.GOOGLE.TITLE')"
|
||||
:description="$t('INBOX_MGMT.ADD.GOOGLE.DESCRIPTION')"
|
||||
:input-placeholder="$t('INBOX_MGMT.ADD.GOOGLE.EMAIL_PLACEHOLDER')"
|
||||
:submit-button-text="$t('INBOX_MGMT.ADD.GOOGLE.SIGN_IN')"
|
||||
:error-message="$t('INBOX_MGMT.ADD.GOOGLE.ERROR_MESSAGE')"
|
||||
/>
|
||||
|
||||
-1
@@ -12,7 +12,6 @@ defineOptions({
|
||||
provider="microsoft"
|
||||
:title="$t('INBOX_MGMT.ADD.MICROSOFT.TITLE')"
|
||||
:description="$t('INBOX_MGMT.ADD.MICROSOFT.DESCRIPTION')"
|
||||
:input-placeholder="$t('INBOX_MGMT.ADD.MICROSOFT.EMAIL_PLACEHOLDER')"
|
||||
:submit-button-text="$t('INBOX_MGMT.ADD.MICROSOFT.SIGN_IN')"
|
||||
:error-message="$t('INBOX_MGMT.ADD.MICROSOFT.ERROR_MESSAGE')"
|
||||
/>
|
||||
|
||||
+1
-13
@@ -30,14 +30,9 @@ const props = defineProps({
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
inputPlaceholder: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const isRequestingAuthorization = ref(false);
|
||||
const email = ref('');
|
||||
|
||||
const client = computed(() => {
|
||||
if (props.provider === 'microsoft') {
|
||||
@@ -50,9 +45,7 @@ const client = computed(() => {
|
||||
async function requestAuthorization() {
|
||||
try {
|
||||
isRequestingAuthorization.value = true;
|
||||
const response = await client.value.generateAuthorization({
|
||||
email: email.value,
|
||||
});
|
||||
const response = await client.value.generateAuthorization();
|
||||
const {
|
||||
data: { url },
|
||||
} = response;
|
||||
@@ -75,11 +68,6 @@ async function requestAuthorization() {
|
||||
:header-content="description"
|
||||
/>
|
||||
<form class="mt-6" @submit.prevent="requestAuthorization">
|
||||
<woot-input
|
||||
v-model="email"
|
||||
type="email"
|
||||
:placeholder="inputPlaceholder"
|
||||
/>
|
||||
<NextButton
|
||||
:is-loading="isRequestingAuthorization"
|
||||
type="submit"
|
||||
|
||||
@@ -51,7 +51,7 @@ const containerClasses = computed(() => [
|
||||
/>
|
||||
<p
|
||||
v-dompurify-html="formatMessage(introBody)"
|
||||
class="text-lg leading-normal text-n-slate-11 [&_a]:underline"
|
||||
class="text-lg leading-normal text-n-slate-11 [&_a]:underline [&_a:has(>strong)]:no-underline"
|
||||
/>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
@@ -167,7 +167,7 @@ class Message < ApplicationRecord
|
||||
additional_attributes: additional_attributes,
|
||||
content_attributes: content_attributes,
|
||||
content_type: content_type,
|
||||
content: outgoing_content,
|
||||
content: content,
|
||||
conversation: conversation.webhook_data,
|
||||
created_at: created_at,
|
||||
id: id,
|
||||
|
||||
@@ -24,6 +24,15 @@ class Instagram::Messenger::MessageText < Instagram::BaseMessageText
|
||||
end
|
||||
|
||||
def handle_client_error(error)
|
||||
# Handle error code 230: User consent is required to access user profile
|
||||
# This typically occurs when the connected Instagram account attempts to send a message to a user
|
||||
# who has never messaged this Instagram account before.
|
||||
# We can safely ignore this error as per Facebook documentation.
|
||||
if error.message.include?('230')
|
||||
Rails.logger.warn error
|
||||
return
|
||||
end
|
||||
|
||||
Rails.logger.warn("[FacebookUserFetchClientError]: account_id #{@inbox.account_id} inbox_id #{@inbox.id}")
|
||||
Rails.logger.warn("[FacebookUserFetchClientError]: #{error.message}")
|
||||
ChatwootExceptionTracker.new(error, account: @inbox.account).capture_exception
|
||||
|
||||
@@ -11,6 +11,13 @@ class LlmFormatter::ConversationLlmFormatter < LlmFormatter::DefaultLlmFormatter
|
||||
end
|
||||
|
||||
sections << "Contact Details: #{@record.contact.to_llm_text}" if config[:include_contact_details]
|
||||
|
||||
attributes = build_attributes
|
||||
if attributes.present?
|
||||
sections << 'Conversation Attributes:'
|
||||
sections << attributes
|
||||
end
|
||||
|
||||
sections.join("\n")
|
||||
end
|
||||
|
||||
@@ -30,4 +37,11 @@ class LlmFormatter::ConversationLlmFormatter < LlmFormatter::DefaultLlmFormatter
|
||||
sender = message.message_type == 'incoming' ? 'User' : 'Support agent'
|
||||
"#{sender}: #{message.content}\n"
|
||||
end
|
||||
|
||||
def build_attributes
|
||||
attributes = @record.account.custom_attribute_definitions.with_attribute_model('conversation_attribute').map do |attribute|
|
||||
"#{attribute.attribute_display_name}: #{@record.custom_attributes[attribute.attribute_key]}"
|
||||
end
|
||||
attributes.join("\n")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -4,8 +4,10 @@ class MessageTemplates::Template::AutoResolve
|
||||
def perform
|
||||
return if conversation.account.auto_resolve_message.blank?
|
||||
|
||||
ActiveRecord::Base.transaction do
|
||||
if within_messaging_window?
|
||||
conversation.messages.create!(auto_resolve_message_params)
|
||||
else
|
||||
create_auto_resolve_not_sent_activity_message
|
||||
end
|
||||
end
|
||||
|
||||
@@ -14,6 +16,21 @@ class MessageTemplates::Template::AutoResolve
|
||||
delegate :contact, :account, to: :conversation
|
||||
delegate :inbox, to: :message
|
||||
|
||||
def within_messaging_window?
|
||||
conversation.can_reply?
|
||||
end
|
||||
|
||||
def create_auto_resolve_not_sent_activity_message
|
||||
content = I18n.t('conversations.activity.auto_resolve.not_sent_due_to_messaging_window')
|
||||
activity_message_params = {
|
||||
account_id: conversation.account_id,
|
||||
inbox_id: conversation.inbox_id,
|
||||
message_type: :activity,
|
||||
content: content
|
||||
}
|
||||
::Conversations::ActivityMessageJob.perform_later(conversation, activity_message_params) if content
|
||||
end
|
||||
|
||||
def auto_resolve_message_params
|
||||
{
|
||||
account_id: @conversation.account_id,
|
||||
|
||||
+1
-1
@@ -6,8 +6,8 @@
|
||||
<strong>Chatwoot Installation:</strong> {{ meta.instance_url }}<br>
|
||||
<strong>Account ID:</strong> {{ meta.account_id }}<br>
|
||||
<strong>Account Name:</strong> {{ meta.account_name }}<br>
|
||||
<strong>Deletion due at:</strong> {{ meta.marked_for_deletion_at }}<br>
|
||||
<strong>Deleted At:</strong> {{ meta.deleted_at }}<br>
|
||||
<strong>Marked for Deletion at:</strong> {{ meta.marked_for_deletion_at }}<br>
|
||||
<strong>Deletion Reason:</strong> {{ meta.deletion_reason }}
|
||||
</p>
|
||||
|
||||
|
||||
@@ -196,6 +196,8 @@ en:
|
||||
issue_unlinked: 'Linear issue %{issue_id} was unlinked by %{user_name}'
|
||||
csat:
|
||||
not_sent_due_to_messaging_window: 'CSAT survey not sent due to outgoing message restrictions'
|
||||
auto_resolve:
|
||||
not_sent_due_to_messaging_window: 'Auto-resolve message not sent due to outgoing message restrictions'
|
||||
muted: '%{user_name} has muted the conversation'
|
||||
unmuted: '%{user_name} has unmuted the conversation'
|
||||
auto_resolution_message: 'Resolving the conversation as it has been inactive for a while. Please start a new conversation if you need further assistance.'
|
||||
|
||||
@@ -13,7 +13,7 @@ class Captain::Conversation::ResponseBuilderJob < ApplicationJob
|
||||
generate_and_process_response
|
||||
end
|
||||
rescue StandardError => e
|
||||
raise e if e.is_a?(ActiveJob::FileNotFoundError)
|
||||
raise e if e.is_a?(ActiveStorage::FileNotFoundError)
|
||||
|
||||
handle_error(e)
|
||||
ensure
|
||||
|
||||
@@ -20,7 +20,7 @@ class Messages::AudioTranscriptionService < Llm::BaseOpenAiService
|
||||
private
|
||||
|
||||
def can_transcribe?
|
||||
return false if account.feature_enabled?('captain_integration')
|
||||
return false unless account.feature_enabled?('captain_integration')
|
||||
return false if account.audio_transcriptions.blank?
|
||||
|
||||
account.usage_limits[:captain][:responses][:current_available].positive?
|
||||
|
||||
@@ -32,19 +32,20 @@ RSpec.describe 'Google Authorization API', type: :request do
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
google_service = Class.new { extend GoogleConcern }
|
||||
response_url = google_service.google_client.auth_code.authorize_url(
|
||||
{
|
||||
redirect_uri: "#{ENV.fetch('FRONTEND_URL', 'http://localhost:3000')}/google/callback",
|
||||
scope: 'email profile https://mail.google.com/',
|
||||
response_type: 'code',
|
||||
prompt: 'consent',
|
||||
access_type: 'offline',
|
||||
client_id: GlobalConfigService.load('GOOGLE_OAUTH_CLIENT_ID', nil)
|
||||
}
|
||||
)
|
||||
expect(response.parsed_body['url']).to eq response_url
|
||||
expect(Redis::Alfred.get("google::#{administrator.email}")).to eq(account.id.to_s)
|
||||
|
||||
# Validate URL components
|
||||
url = response.parsed_body['url']
|
||||
uri = URI.parse(url)
|
||||
params = CGI.parse(uri.query)
|
||||
|
||||
expect(url).to start_with('https://accounts.google.com/o/oauth2/auth')
|
||||
expect(params['scope']).to eq(['email profile https://mail.google.com/'])
|
||||
expect(params['redirect_uri']).to eq(["#{ENV.fetch('FRONTEND_URL', 'http://localhost:3000')}/google/callback"])
|
||||
|
||||
# Validate state parameter exists and can be decoded back to the account
|
||||
expect(params['state']).to be_present
|
||||
decoded_account = GlobalID::Locator.locate_signed(params['state'].first, for: 'default')
|
||||
expect(decoded_account).to eq(account)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -19,7 +19,6 @@ RSpec.describe 'Microsoft Authorization API', type: :request do
|
||||
it 'returns unathorized for agent' do
|
||||
post "/api/v1/accounts/#{account.id}/microsoft/authorization",
|
||||
headers: agent.create_new_auth_token,
|
||||
params: { email: administrator.email },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
@@ -28,20 +27,27 @@ RSpec.describe 'Microsoft Authorization API', type: :request do
|
||||
it 'creates a new authorization and returns the redirect url' do
|
||||
post "/api/v1/accounts/#{account.id}/microsoft/authorization",
|
||||
headers: administrator.create_new_auth_token,
|
||||
params: { email: administrator.email },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
microsoft_service = Class.new { extend MicrosoftConcern }
|
||||
response_url = microsoft_service.microsoft_client.auth_code.authorize_url(
|
||||
{
|
||||
redirect_uri: "#{ENV.fetch('FRONTEND_URL', 'http://localhost:3000')}/microsoft/callback",
|
||||
scope: 'offline_access https://outlook.office.com/IMAP.AccessAsUser.All https://outlook.office.com/SMTP.Send openid profile',
|
||||
prompt: 'consent'
|
||||
}
|
||||
)
|
||||
expect(response.parsed_body['url']).to eq response_url
|
||||
expect(Redis::Alfred.get("microsoft::#{administrator.email}")).to eq(account.id.to_s)
|
||||
|
||||
# Validate URL components
|
||||
url = response.parsed_body['url']
|
||||
uri = URI.parse(url)
|
||||
params = CGI.parse(uri.query)
|
||||
|
||||
expect(url).to start_with('https://login.microsoftonline.com/common/oauth2/v2.0/authorize')
|
||||
expected_scope = [
|
||||
'offline_access https://outlook.office.com/IMAP.AccessAsUser.All ' \
|
||||
'https://outlook.office.com/SMTP.Send openid profile email'
|
||||
]
|
||||
expect(params['scope']).to eq(expected_scope)
|
||||
expect(params['redirect_uri']).to eq(["#{ENV.fetch('FRONTEND_URL', 'http://localhost:3000')}/microsoft/callback"])
|
||||
|
||||
# Validate state parameter exists and can be decoded back to the account
|
||||
expect(params['state']).to be_present
|
||||
decoded_account = GlobalID::Locator.locate_signed(params['state'].first, for: 'default')
|
||||
expect(decoded_account).to eq(account)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -4,11 +4,7 @@ RSpec.describe 'Google::CallbacksController', type: :request do
|
||||
let(:account) { create(:account) }
|
||||
let(:code) { SecureRandom.hex(10) }
|
||||
let(:email) { Faker::Internet.email }
|
||||
let(:cache_key) { "google::#{email.downcase}" }
|
||||
|
||||
before do
|
||||
Redis::Alfred.set(cache_key, account.id)
|
||||
end
|
||||
let(:state) { account.to_sgid(expires_in: 15.minutes).to_s }
|
||||
|
||||
describe 'GET /google/callback' do
|
||||
let(:response_body_success) do
|
||||
@@ -27,7 +23,7 @@ RSpec.describe 'Google::CallbacksController', type: :request do
|
||||
'redirect_uri' => "#{ENV.fetch('FRONTEND_URL', 'http://localhost:3000')}/google/callback" })
|
||||
.to_return(status: 200, body: response_body_success.to_json, headers: { 'Content-Type' => 'application/json' })
|
||||
|
||||
get google_callback_url, params: { code: code }
|
||||
get google_callback_url, params: { code: code, state: state }
|
||||
|
||||
expect(response).to redirect_to app_email_inbox_agents_url(account_id: account.id, inbox_id: account.inboxes.last.id)
|
||||
expect(account.inboxes.count).to be 1
|
||||
@@ -36,7 +32,6 @@ RSpec.describe 'Google::CallbacksController', type: :request do
|
||||
expect(inbox.channel.reload.provider_config.keys).to include('access_token', 'refresh_token', 'expires_on')
|
||||
expect(inbox.channel.reload.provider_config['access_token']).to eq response_body_success[:access_token]
|
||||
expect(inbox.channel.imap_address).to eq 'imap.gmail.com'
|
||||
expect(Redis::Alfred.get(cache_key)).to be_nil
|
||||
end
|
||||
|
||||
it 'updates inbox channel config if inbox exists with imap_login and authentication is successful' do
|
||||
@@ -49,14 +44,13 @@ RSpec.describe 'Google::CallbacksController', type: :request do
|
||||
'redirect_uri' => "#{ENV.fetch('FRONTEND_URL', 'http://localhost:3000')}/google/callback" })
|
||||
.to_return(status: 200, body: response_body_success.to_json, headers: { 'Content-Type' => 'application/json' })
|
||||
|
||||
get google_callback_url, params: { code: code }
|
||||
get google_callback_url, params: { code: code, state: state }
|
||||
|
||||
expect(response).to redirect_to app_email_inbox_settings_url(account_id: account.id, inbox_id: inbox.id)
|
||||
expect(account.inboxes.count).to be 1
|
||||
expect(inbox.channel.reload.provider_config.keys).to include('access_token', 'refresh_token', 'expires_on')
|
||||
expect(inbox.channel.reload.provider_config['access_token']).to eq response_body_success[:access_token]
|
||||
expect(inbox.channel.imap_address).to eq 'imap.gmail.com'
|
||||
expect(Redis::Alfred.get(cache_key)).to be_nil
|
||||
end
|
||||
|
||||
it 'creates inboxes with fallback_name when account name is not present in id_token' do
|
||||
@@ -65,7 +59,7 @@ RSpec.describe 'Google::CallbacksController', type: :request do
|
||||
'redirect_uri' => "#{ENV.fetch('FRONTEND_URL', 'http://localhost:3000')}/google/callback" })
|
||||
.to_return(status: 200, body: response_body_success_without_name.to_json, headers: { 'Content-Type' => 'application/json' })
|
||||
|
||||
get google_callback_url, params: { code: code }
|
||||
get google_callback_url, params: { code: code, state: state }
|
||||
|
||||
expect(response).to redirect_to app_email_inbox_agents_url(account_id: account.id, inbox_id: account.inboxes.last.id)
|
||||
expect(account.inboxes.count).to be 1
|
||||
@@ -79,10 +73,9 @@ RSpec.describe 'Google::CallbacksController', type: :request do
|
||||
'redirect_uri' => "#{ENV.fetch('FRONTEND_URL', 'http://localhost:3000')}/google/callback" })
|
||||
.to_return(status: 401)
|
||||
|
||||
get google_callback_url, params: { code: code }
|
||||
get google_callback_url, params: { code: code, state: state }
|
||||
|
||||
expect(response).to redirect_to '/'
|
||||
expect(Redis::Alfred.get(cache_key).to_i).to eq account.id
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -4,11 +4,7 @@ RSpec.describe 'Microsoft::CallbacksController', type: :request do
|
||||
let(:account) { create(:account) }
|
||||
let(:code) { SecureRandom.hex(10) }
|
||||
let(:email) { Faker::Internet.email }
|
||||
let(:cache_key) { "microsoft::#{email.downcase}" }
|
||||
|
||||
before do
|
||||
Redis::Alfred.set(cache_key, account.id)
|
||||
end
|
||||
let(:state) { account.to_sgid(expires_in: 15.minutes).to_s }
|
||||
|
||||
describe 'GET /microsoft/callback' do
|
||||
let(:response_body_success) do
|
||||
@@ -27,7 +23,7 @@ RSpec.describe 'Microsoft::CallbacksController', type: :request do
|
||||
'redirect_uri' => "#{ENV.fetch('FRONTEND_URL', 'http://localhost:3000')}/microsoft/callback" })
|
||||
.to_return(status: 200, body: response_body_success.to_json, headers: { 'Content-Type' => 'application/json' })
|
||||
|
||||
get microsoft_callback_url, params: { code: code }
|
||||
get microsoft_callback_url, params: { code: code, state: state }
|
||||
|
||||
expect(response).to redirect_to app_email_inbox_agents_url(account_id: account.id, inbox_id: account.inboxes.last.id)
|
||||
expect(account.inboxes.count).to be 1
|
||||
@@ -36,7 +32,6 @@ RSpec.describe 'Microsoft::CallbacksController', type: :request do
|
||||
expect(inbox.channel.reload.provider_config.keys).to include('access_token', 'refresh_token', 'expires_on')
|
||||
expect(inbox.channel.reload.provider_config['access_token']).to eq response_body_success[:access_token]
|
||||
expect(inbox.channel.imap_address).to eq 'outlook.office365.com'
|
||||
expect(Redis::Alfred.get(cache_key)).to be_nil
|
||||
end
|
||||
|
||||
it 'creates updates inbox channel config if inbox exists and authentication is successful' do
|
||||
@@ -48,14 +43,13 @@ RSpec.describe 'Microsoft::CallbacksController', type: :request do
|
||||
'redirect_uri' => "#{ENV.fetch('FRONTEND_URL', 'http://localhost:3000')}/microsoft/callback" })
|
||||
.to_return(status: 200, body: response_body_success.to_json, headers: { 'Content-Type' => 'application/json' })
|
||||
|
||||
get microsoft_callback_url, params: { code: code }
|
||||
get microsoft_callback_url, params: { code: code, state: state }
|
||||
|
||||
expect(response).to redirect_to app_email_inbox_settings_url(account_id: account.id, inbox_id: account.inboxes.last.id)
|
||||
expect(account.inboxes.count).to be 1
|
||||
expect(inbox.channel.reload.provider_config.keys).to include('access_token', 'refresh_token', 'expires_on')
|
||||
expect(inbox.channel.reload.provider_config['access_token']).to eq response_body_success[:access_token]
|
||||
expect(inbox.channel.imap_address).to eq 'outlook.office365.com'
|
||||
expect(Redis::Alfred.get(cache_key)).to be_nil
|
||||
end
|
||||
|
||||
it 'creates inboxes with fallback_name when account name is not present in id_token' do
|
||||
@@ -64,7 +58,7 @@ RSpec.describe 'Microsoft::CallbacksController', type: :request do
|
||||
'redirect_uri' => "#{ENV.fetch('FRONTEND_URL', 'http://localhost:3000')}/microsoft/callback" })
|
||||
.to_return(status: 200, body: response_body_success_without_name.to_json, headers: { 'Content-Type' => 'application/json' })
|
||||
|
||||
get microsoft_callback_url, params: { code: code }
|
||||
get microsoft_callback_url, params: { code: code, state: state }
|
||||
|
||||
expect(response).to redirect_to app_email_inbox_agents_url(account_id: account.id, inbox_id: account.inboxes.last.id)
|
||||
expect(account.inboxes.count).to be 1
|
||||
@@ -78,10 +72,9 @@ RSpec.describe 'Microsoft::CallbacksController', type: :request do
|
||||
'redirect_uri' => "#{ENV.fetch('FRONTEND_URL', 'http://localhost:3000')}/microsoft/callback" })
|
||||
.to_return(status: 401)
|
||||
|
||||
get microsoft_callback_url, params: { code: code }
|
||||
get microsoft_callback_url, params: { code: code, state: state }
|
||||
|
||||
expect(response).to redirect_to '/'
|
||||
expect(Redis::Alfred.get(cache_key).to_i).to eq account.id
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -18,6 +18,16 @@ RSpec.describe Messages::AudioTranscriptionService, type: :service do
|
||||
describe '#perform' do
|
||||
let(:service) { described_class.new(attachment) }
|
||||
|
||||
context 'when captain_integration feature is not enabled' do
|
||||
before do
|
||||
account.disable_features!('captain_integration')
|
||||
end
|
||||
|
||||
it 'returns transcription limit exceeded' do
|
||||
expect(service.perform).to eq({ error: 'Transcription limit exceeded' })
|
||||
end
|
||||
end
|
||||
|
||||
context 'when transcription is successful' do
|
||||
before do
|
||||
# Mock can_transcribe? to return true and transcribe_audio method
|
||||
|
||||
@@ -61,5 +61,30 @@ RSpec.describe LlmFormatter::ConversationLlmFormatter do
|
||||
expect(formatter.format(include_contact_details: true)).to eq(expected_output)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when conversation has custom attributes' do
|
||||
it 'includes formatted custom attributes in the output' do
|
||||
create(
|
||||
:custom_attribute_definition,
|
||||
account: account,
|
||||
attribute_display_name: 'Order ID',
|
||||
attribute_key: 'order_id',
|
||||
attribute_model: :conversation_attribute
|
||||
)
|
||||
|
||||
conversation.update(custom_attributes: { 'order_id' => '12345' })
|
||||
|
||||
expected_output = [
|
||||
"Conversation ID: ##{conversation.display_id}",
|
||||
"Channel: #{conversation.inbox.channel.name}",
|
||||
'Message History:',
|
||||
'No messages in this conversation',
|
||||
'Conversation Attributes:',
|
||||
'Order ID: 12345'
|
||||
].join("\n")
|
||||
|
||||
expect(formatter.format).to eq(expected_output)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user