feat: baileys provider for whatsapp (#7)
* feat: baileys provider and placeholder for link device modal * chore: drop qrcode.vue in favor of just img tag * chore: update modal props * feat: setup channel provider connection * chore: update .env.example with Baileys API default configuration * feat: add support for Baileys provider in WhatsApp events processing * chore: rename Baileys API default host variable to DEFAULT_BAILEYS_URL * feat: add setup and disconnect methods for Baileys channel provider in inboxes controller that will be implemented * feat: add CHANNEL_CONNECTION_UPDATE event and include it in broadcast data preparation * refactor: simplify channel retrieval logic in WhatsappEventsJob * refactor: revert CHANNEL_UPDATE_EVENTS constant from ActionCableBroadcastJob * feat: add 'baileys' as a provider option in Whatsapp channel model * feat: add provider_connection field to Whatsapp channel model and migration * refactor: remove unnecessary CHANNEL_CONNECTION_UPDATE event type * feat: implement channel provider connection with baileys API * feat: add inbox association to Whatsapp channel model and update webhook URL handling * feat: enhance Baileys service to handle webhook multiple event types * refactor: simplify webhook verification logic in Baileys service * feat: add setup channel provider call, and refactor some logic * chore: adapt logic to new API * refactor: fix typo * refactor: fix import * refactor: fix typo * chore: add fixme comment about race condition * fix: remove double disconnect call * feat: implement message processing for incoming WhatsApp messages * refactor: streamline message type determination and improve readability * chore: increase cache key granularity provider connection info might be updated multiple times within 1 second, so updates might be lost due to cache key not being updated. changing cache key to milliseconds solves this * feat: add `is-loading` to buttons * feat: update send_message method to use 'to' parameter and improve error handling * refactor: simplify test setup and update API key in specs * chore: add setup and disconnect channel provider specs * test: fix spec after increase cache key granularity * feat: handle reconnecting state on modal * style: centered error text * feat: advanced options on create inbox * feat: handle new reconnecting on backend * refactor: update inbox controller specs and leave a FIXME note * test: add specs for Whatsapp::IncomingMessageBaileysService * feat: add baileys configuration page * feat: link device button when disconnected on conversation * chore: refactor .env.example * feat: add TODO for unimplemented methods in IncomingMessageBaileysService * fix: correct method name and update environment variable references in WhatsappBaileysService * refactor: simplify channel lookup by removing redundant method and handling phone number check directly * chore: add TODO for unimplemented event processing methods in IncomingMessageBaileysService * fix: update environment variable references in WhatsappBaileysService tests * chore(webhook): add pt-BR translations * chore: add pt-br translations * chore: inboxname component margin * refactor: inboxname computed prop * feat: enhance WhatsApp provider connection handling and message processing * test: inbox controller * chore: improve baileys connection and messages handling * test: incoming message service baileys * refactor: update provider config validation and improve test setup for WhatsApp Baileys service * fix: ensure only text messages are sent and update message source ID * fix: create message * fix: only update message on success * test: fix broken specs * chore: raise error on unsupported message content type * feat: hide provider connection data from non-admins * fix: update advanced options * chore: move class definition * fix: issue with send_message not returning id --------- Co-authored-by: gabrieljablonski <contact@gabrieljablonski.com>
This commit is contained in:
parent
2c36aefecc
commit
acd1e56a28
@ -2,7 +2,7 @@
|
||||
# https://www.chatwoot.com/docs/self-hosted/configuration/environment-variables/#rails-production-variables
|
||||
|
||||
# Used to verify the integrity of signed cookies. so ensure a secure value is set
|
||||
# SECRET_KEY_BASE should be alphanumeric. Avoid special characters or symbols.
|
||||
# SECRET_KEY_BASE should be alphanumeric. Avoid special characters or symbols.
|
||||
# Use `rake secret` to generate this variable
|
||||
SECRET_KEY_BASE=replace_with_lengthy_secure_hex
|
||||
|
||||
@ -258,3 +258,7 @@ AZURE_APP_SECRET=
|
||||
# contact_inboxes with no conversation older than 90 days will be removed
|
||||
# REMOVE_STALE_CONTACT_INBOX_JOB_STATUS=false
|
||||
|
||||
# Baileys API Whatsapp provider
|
||||
BAILEYS_PROVIDER_DEFAULT_CLIENT_NAME=Chatwoot
|
||||
BAILEYS_PROVIDER_DEFAULT_URL=http://localhost:3025
|
||||
BAILEYS_PROVIDER_DEFAULT_API_KEY=
|
||||
|
||||
@ -62,6 +62,28 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
||||
head :ok
|
||||
end
|
||||
|
||||
def setup_channel_provider
|
||||
channel = @inbox.channel
|
||||
|
||||
unless channel.respond_to?(:setup_channel_provider)
|
||||
render json: { error: 'Channel does not support setup' }, status: :unprocessable_entity and return
|
||||
end
|
||||
|
||||
channel.setup_channel_provider
|
||||
head :ok
|
||||
end
|
||||
|
||||
def disconnect_channel_provider
|
||||
channel = @inbox.channel
|
||||
|
||||
unless channel.respond_to?(:disconnect_channel_provider)
|
||||
render json: { error: 'Channel does not support disconnect' }, status: :unprocessable_entity and return
|
||||
end
|
||||
|
||||
channel.disconnect_channel_provider
|
||||
head :ok
|
||||
end
|
||||
|
||||
def destroy
|
||||
::DeleteObjectJob.perform_later(@inbox, Current.user, request.ip) if @inbox.present?
|
||||
render status: :ok, json: { message: I18n.t('messages.inbox_deletetion_response') }
|
||||
|
||||
@ -10,6 +10,6 @@ module CacheKeysHelper
|
||||
return value_from_cache if value_from_cache.present?
|
||||
|
||||
# zero epoch time: 1970-01-01 00:00:00 UTC
|
||||
'0000000000'
|
||||
'0000000000000'
|
||||
end
|
||||
end
|
||||
|
||||
@ -28,6 +28,14 @@ class Inboxes extends CacheEnabledApiClient {
|
||||
agent_bot: botId,
|
||||
});
|
||||
}
|
||||
|
||||
setupChannelProvider(inboxId) {
|
||||
return axios.post(`${this.url}/${inboxId}/setup_channel_provider`);
|
||||
}
|
||||
|
||||
disconnectChannelProvider(inboxId) {
|
||||
return axios.post(`${this.url}/${inboxId}/disconnect_channel_provider`);
|
||||
}
|
||||
}
|
||||
|
||||
export default new Inboxes();
|
||||
|
||||
@ -7,6 +7,14 @@ export default {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
withPhoneNumber: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
withProviderConnectionStatus: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
computedInboxClass() {
|
||||
@ -14,6 +22,9 @@ export default {
|
||||
const classByType = getInboxClassByType(type, phoneNumber);
|
||||
return classByType;
|
||||
},
|
||||
providerConnection() {
|
||||
return this.inbox.provider_connection?.connection;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@ -28,5 +39,17 @@ export default {
|
||||
size="12"
|
||||
/>
|
||||
{{ inbox.name }}
|
||||
<span v-if="withPhoneNumber" class="ml-2 text-n-slate-12">{{
|
||||
inbox.phone_number
|
||||
}}</span>
|
||||
<span v-if="withProviderConnectionStatus" class="ml-2">
|
||||
<fluent-icon
|
||||
icon="circle"
|
||||
type="filled"
|
||||
:class="
|
||||
providerConnection === 'open' ? 'text-green-500' : 'text-n-slate-8'
|
||||
"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -5,6 +5,7 @@ import { useConfig } from 'dashboard/composables/useConfig';
|
||||
import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents';
|
||||
import { useAI } from 'dashboard/composables/useAI';
|
||||
import { useMapGetter } from 'dashboard/composables/store';
|
||||
import { useAdmin } from 'dashboard/composables/useAdmin';
|
||||
|
||||
// components
|
||||
import ReplyBox from './ReplyBox.vue';
|
||||
@ -36,6 +37,7 @@ import { REPLY_POLICY } from 'shared/constants/links';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
|
||||
import { FEATURE_FLAGS } from '../../../featureFlags';
|
||||
import WhatsappBaileysLinkDeviceModal from '../../../routes/dashboard/settings/inbox/components/WhatsappBaileysLinkDeviceModal.vue';
|
||||
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
@ -47,6 +49,7 @@ export default {
|
||||
Banner,
|
||||
ConversationLabelSuggestion,
|
||||
NextButton,
|
||||
WhatsappBaileysLinkDeviceModal,
|
||||
},
|
||||
mixins: [inboxMixin],
|
||||
props: {
|
||||
@ -61,6 +64,7 @@ export default {
|
||||
},
|
||||
emits: ['contactPanelToggle'],
|
||||
setup() {
|
||||
const { isAdmin } = useAdmin();
|
||||
const isPopOutReplyBox = ref(false);
|
||||
const { isEnterprise } = useConfig();
|
||||
|
||||
@ -107,6 +111,7 @@ export default {
|
||||
fetchIntegrationsIfRequired,
|
||||
fetchLabelSuggestions,
|
||||
showNextBubbles,
|
||||
isAdmin,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
@ -118,6 +123,7 @@ export default {
|
||||
isProgrammaticScroll: false,
|
||||
messageSentSinceOpened: false,
|
||||
labelSuggestions: [],
|
||||
showBaileysLinkDeviceModal: false,
|
||||
};
|
||||
},
|
||||
|
||||
@ -128,6 +134,9 @@ export default {
|
||||
listLoadingStatus: 'getAllMessagesLoaded',
|
||||
currentAccountId: 'getCurrentAccountId',
|
||||
}),
|
||||
currentInbox() {
|
||||
return this.$store.getters['inboxes/getInbox'](this.currentChat.inbox_id);
|
||||
},
|
||||
isOpen() {
|
||||
return this.currentChat?.status === wootConstants.STATUS_TYPE.OPEN;
|
||||
},
|
||||
@ -266,6 +275,9 @@ export default {
|
||||
|
||||
return { incoming, outgoing };
|
||||
},
|
||||
inboxProviderConnection() {
|
||||
return this.currentInbox.provider_connection?.connection;
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
@ -476,12 +488,48 @@ export default {
|
||||
return false;
|
||||
});
|
||||
},
|
||||
onOpenBaileysLinkDeviceModal() {
|
||||
this.showBaileysLinkDeviceModal = true;
|
||||
},
|
||||
onCloseBaileysLinkDeviceModal() {
|
||||
this.showBaileysLinkDeviceModal = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col justify-between flex-grow h-full min-w-0 m-0">
|
||||
<template v-if="isAWhatsAppBaileysChannel">
|
||||
<WhatsappBaileysLinkDeviceModal
|
||||
v-if="showBaileysLinkDeviceModal"
|
||||
:show="showBaileysLinkDeviceModal"
|
||||
:on-close="onCloseBaileysLinkDeviceModal"
|
||||
:inbox="currentInbox"
|
||||
/>
|
||||
<Banner
|
||||
v-if="inboxProviderConnection !== 'open'"
|
||||
color-scheme="alert"
|
||||
class="mt-2 mx-2 rounded-lg overflow-hidden"
|
||||
:banner-message="
|
||||
isAdmin
|
||||
? $t(
|
||||
'CONVERSATION.INBOX.WHATSAPP_BAILEYS_PROVIDER_CONNECTION.NOT_CONNECTED'
|
||||
)
|
||||
: $t(
|
||||
'CONVERSATION.INBOX.WHATSAPP_BAILEYS_PROVIDER_CONNECTION.NOT_CONNECTED_CONTACT_ADMIN'
|
||||
)
|
||||
"
|
||||
:has-action-button="isAdmin"
|
||||
:action-button-label="
|
||||
$t(
|
||||
'CONVERSATION.INBOX.WHATSAPP_BAILEYS_PROVIDER_CONNECTION.LINK_DEVICE'
|
||||
)
|
||||
"
|
||||
action-button-icon=""
|
||||
@primary-action="onOpenBaileysLinkDeviceModal"
|
||||
/>
|
||||
</template>
|
||||
<Banner
|
||||
v-if="!currentChat.can_reply"
|
||||
color-scheme="alert"
|
||||
|
||||
@ -241,7 +241,7 @@ export default {
|
||||
if (this.isAFacebookInbox) {
|
||||
return MESSAGE_MAX_LENGTH.FACEBOOK;
|
||||
}
|
||||
if (this.isAWhatsAppChannel) {
|
||||
if (this.isATwilioWhatsAppChannel) {
|
||||
return MESSAGE_MAX_LENGTH.TWILIO_WHATSAPP;
|
||||
}
|
||||
if (this.isASmsInbox) {
|
||||
|
||||
@ -114,6 +114,13 @@ export const useInbox = () => {
|
||||
);
|
||||
});
|
||||
|
||||
const isAWhatsAppBaileysChannel = computed(() => {
|
||||
return (
|
||||
channelType.value === INBOX_TYPES.WHATSAPP &&
|
||||
whatsAppAPIProvider.value === 'baileys'
|
||||
);
|
||||
});
|
||||
|
||||
const isAWhatsAppChannel = computed(() => {
|
||||
return (
|
||||
channelType.value === INBOX_TYPES.WHATSAPP ||
|
||||
@ -136,6 +143,7 @@ export const useInbox = () => {
|
||||
isATwilioWhatsAppChannel,
|
||||
isAWhatsAppCloudChannel,
|
||||
is360DialogWhatsAppChannel,
|
||||
isAWhatsAppBaileysChannel,
|
||||
isAnEmailChannel,
|
||||
};
|
||||
};
|
||||
|
||||
@ -234,6 +234,13 @@
|
||||
"SIDEBAR": {
|
||||
"CONTACT": "Contact",
|
||||
"COPILOT": "Copilot"
|
||||
},
|
||||
"INBOX": {
|
||||
"WHATSAPP_BAILEYS_PROVIDER_CONNECTION": {
|
||||
"NOT_CONNECTED": "WhatsApp is not connected. Please link your device again.",
|
||||
"NOT_CONNECTED_CONTACT_ADMIN": "WhatsApp is not connected. Please contact your administrator to link your device again.",
|
||||
"LINK_DEVICE": "Link device"
|
||||
}
|
||||
}
|
||||
},
|
||||
"EMAIL_TRANSCRIPT": {
|
||||
|
||||
@ -221,7 +221,8 @@
|
||||
"LABEL": "API Provider",
|
||||
"TWILIO": "Twilio",
|
||||
"WHATSAPP_CLOUD": "WhatsApp Cloud",
|
||||
"360_DIALOG": "360Dialog"
|
||||
"360_DIALOG": "360Dialog",
|
||||
"BAILEYS": "Baileys"
|
||||
},
|
||||
"INBOX_NAME": {
|
||||
"LABEL": "Inbox Name",
|
||||
@ -260,6 +261,25 @@
|
||||
"WEBHOOK_URL": "Webhook URL",
|
||||
"WEBHOOK_VERIFICATION_TOKEN": "Webhook Verification Token"
|
||||
},
|
||||
"PROVIDER_URL": {
|
||||
"LABEL": "Provider URL",
|
||||
"PLACEHOLDER": "If provider is not running locally, please provide the URL",
|
||||
"ERROR":"Please enter a valid URL"
|
||||
},
|
||||
"ADVANCED_OPTIONS": "Advanced options",
|
||||
"BAILEYS": {
|
||||
"SUBTITLE": "Click below to setup the WhatsApp channel using Baileys.",
|
||||
"LINK_BUTTON": "Link device",
|
||||
"LINK_DEVICE_MODAL": {
|
||||
"TITLE": "Link your device",
|
||||
"SUBTITLE": "Scan the QR code to link your device. Make sure the phone number is correct before scanning.",
|
||||
"LOADING_QRCODE": "Loading QR code...",
|
||||
"RECONNECTING": "Connecting...",
|
||||
"LINK_DEVICE": "Link device",
|
||||
"DISCONNECT": "Disconnect",
|
||||
"CONNECTED": "Your device has been connected successfully. You can now start sending and receiving messages."
|
||||
}
|
||||
},
|
||||
"SUBMIT_BUTTON": "Create WhatsApp Channel",
|
||||
"API": {
|
||||
"ERROR_MESSAGE": "We were not able to save the WhatsApp channel"
|
||||
@ -529,6 +549,13 @@
|
||||
"WHATSAPP_SECTION_UPDATE_BUTTON": "Update",
|
||||
"WHATSAPP_WEBHOOK_TITLE": "Webhook Verification Token",
|
||||
"WHATSAPP_WEBHOOK_SUBHEADER": "This token is used to verify the authenticity of the webhook endpoint.",
|
||||
"WHATSAPP_MANAGE_PROVIDER_CONNECTION_TITLE": "Manage Provider Connection",
|
||||
"WHATSAPP_MANAGE_PROVIDER_CONNECTION_SUBHEADER": "Link your device and manage the provider connection.",
|
||||
"WHATSAPP_MANAGE_PROVIDER_CONNECTION_BUTTON": "Manage connection",
|
||||
"WHATSAPP_PROVIDER_URL_TITLE": "Provider URL",
|
||||
"WHATSAPP_PROVIDER_URL_SUBHEADER": "If the provider is not running locally, please provide the URL.",
|
||||
"WHATSAPP_PROVIDER_URL_PLACEHOLDER": "Enter the provider URL",
|
||||
"WHATSAPP_PROVIDER_URL_ERROR": "Please enter a valid URL",
|
||||
"UPDATE_PRE_CHAT_FORM_SETTINGS": "Update Pre Chat Form Settings"
|
||||
},
|
||||
"HELP_CENTER": {
|
||||
|
||||
@ -234,6 +234,13 @@
|
||||
"SIDEBAR": {
|
||||
"CONTACT": "Contato",
|
||||
"COPILOT": "Copiloto"
|
||||
},
|
||||
"INBOX": {
|
||||
"WHATSAPP_BAILEYS_PROVIDER_CONNECTION": {
|
||||
"NOT_CONNECTED": "O WhatsApp não está conectado. Por favor conecte o seu dispositivo novamente.",
|
||||
"NOT_CONNECTED_CONTACT_ADMIN": "O WhatsApp não está conectado. Por favor contate o seu administrador para conectar o dispositivo novamente.",
|
||||
"LINK_DEVICE": "Conectar dispositivo"
|
||||
}
|
||||
}
|
||||
},
|
||||
"EMAIL_TRANSCRIPT": {
|
||||
|
||||
@ -214,7 +214,8 @@
|
||||
"LABEL": "Provedor de API",
|
||||
"TWILIO": "Twilio",
|
||||
"WHATSAPP_CLOUD": "Cloud do WhatsApp",
|
||||
"360_DIALOG": "360Dialog"
|
||||
"360_DIALOG": "360Dialog",
|
||||
"BAILEYS": "Baileys"
|
||||
},
|
||||
"INBOX_NAME": {
|
||||
"LABEL": "Nome da Caixa de Entrada",
|
||||
@ -253,6 +254,25 @@
|
||||
"WEBHOOK_URL": "URL do Webhook",
|
||||
"WEBHOOK_VERIFICATION_TOKEN": "Token de verificação Webhook"
|
||||
},
|
||||
"PROVIDER_URL": {
|
||||
"LABEL": "URL do provedor",
|
||||
"PLACEHOLDER": "Se o provedor não está rodando localmente, por favor, insira a URL do provedor",
|
||||
"ERROR":"Por favor, insira uma URL válida"
|
||||
},
|
||||
"ADVANCED_OPTIONS": "Opções avançadas",
|
||||
"BAILEYS": {
|
||||
"SUBTITLE": "Clique abaixo para configurar o canal do WhatsApp usando o Baileys.",
|
||||
"LINK_BUTTON": "Conectar dispositivo",
|
||||
"LINK_DEVICE_MODAL": {
|
||||
"TITLE": "Conecte o seu dispositivo",
|
||||
"SUBTITLE": "Escaneie o QR code para conectar seu dispositivo. Certifique-se de que o número de telefone esteja correto antes de escanear.",
|
||||
"LOADING_QRCODE": "Carregando QR code...",
|
||||
"RECONNECTING": "Conectando...",
|
||||
"LINK_DEVICE": "Conectar dispositivo",
|
||||
"DISCONNECT": "Desconectar",
|
||||
"CONNECTED": "Seu dispositivo foi conectado com sucesso. Agora você pode começar a enviar e receber mensagens."
|
||||
}
|
||||
},
|
||||
"SUBMIT_BUTTON": "Criar canal do WhatsApp",
|
||||
"API": {
|
||||
"ERROR_MESSAGE": "Não foi possível salvar o canal do WhatsApp"
|
||||
@ -522,6 +542,13 @@
|
||||
"WHATSAPP_SECTION_UPDATE_BUTTON": "Atualizar",
|
||||
"WHATSAPP_WEBHOOK_TITLE": "Token de verificação Webhook",
|
||||
"WHATSAPP_WEBHOOK_SUBHEADER": "Este token é usado para verificar a autenticidade do webhook endpoint.",
|
||||
"WHATSAPP_MANAGE_PROVIDER_CONNECTION_TITLE": "Gerenciar Conexão do Provedor",
|
||||
"WHATSAPP_MANAGE_PROVIDER_CONNECTION_SUBHEADER": "Conecte o seu dispositivo e gerencie a conexão do provedor.",
|
||||
"WHATSAPP_MANAGE_PROVIDER_CONNECTION_BUTTON": "Gerenciar conexão",
|
||||
"WHATSAPP_PROVIDER_URL_TITLE": "URL do provedor",
|
||||
"WHATSAPP_PROVIDER_URL_SUBHEADER": "Se o provedor não estiver rodando localmente, por favor, forneça a URL.",
|
||||
"WHATSAPP_PROVIDER_URL_PLACEHOLDER": "Digite a URL do provedor",
|
||||
"WHATSAPP_PROVIDER_URL_ERROR": "Por favor, insira uma URL válida",
|
||||
"UPDATE_PRE_CHAT_FORM_SETTINGS": "Atualizar configurações do Formulário Pre Chat"
|
||||
},
|
||||
"HELP_CENTER": {
|
||||
|
||||
@ -44,6 +44,17 @@
|
||||
"CONTACT_UPDATED": "Contato atualizado"
|
||||
}
|
||||
},
|
||||
"NAME": {
|
||||
"LABEL": "Nome do Webhook",
|
||||
"PLACEHOLDER": "Insira o nome do webhook"
|
||||
},
|
||||
"INBOX": {
|
||||
"LABEL": "Caixa de Entrada",
|
||||
"TITLE": "Selecione a caixa de entrada",
|
||||
"PLACEHOLDER": "Todas as caixas de entrada",
|
||||
"NO_RESULTS": "Nenhuma caixa de entrada encontrada",
|
||||
"INPUT_PLACEHOLDER": "Buscar caixa de entrada"
|
||||
},
|
||||
"END_POINT": {
|
||||
"LABEL": "URL do Webhook",
|
||||
"PLACEHOLDER": "Exemplo: {webhookExampleURL}",
|
||||
|
||||
@ -1,11 +1,18 @@
|
||||
<script>
|
||||
import EmptyState from '../../../../components/widgets/EmptyState.vue';
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
import WhatsappBaileysLinkDeviceModal from './components/WhatsappBaileysLinkDeviceModal.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
EmptyState,
|
||||
NextButton,
|
||||
WhatsappBaileysLinkDeviceModal,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showBaileysLinkDeviceModal: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
currentInbox() {
|
||||
@ -31,6 +38,12 @@ export default {
|
||||
this.currentInbox.provider === 'whatsapp_cloud'
|
||||
);
|
||||
},
|
||||
isWhatsAppBaileysInbox() {
|
||||
return (
|
||||
this.currentInbox.channel_type === 'Channel::Whatsapp' &&
|
||||
this.currentInbox.provider === 'baileys'
|
||||
);
|
||||
},
|
||||
message() {
|
||||
if (this.isATwilioInbox) {
|
||||
return `${this.$t('INBOX_MGMT.FINISH.MESSAGE')}. ${this.$t(
|
||||
@ -56,6 +69,12 @@ export default {
|
||||
)}`;
|
||||
}
|
||||
|
||||
if (this.isWhatsAppBaileysInbox) {
|
||||
return `${this.$t('INBOX_MGMT.FINISH.MESSAGE')}. ${this.$t(
|
||||
'INBOX_MGMT.ADD.WHATSAPP.BAILEYS.SUBTITLE'
|
||||
)}`;
|
||||
}
|
||||
|
||||
if (this.isAEmailInbox && !this.currentInbox.provider) {
|
||||
return this.$t('INBOX_MGMT.ADD.EMAIL_CHANNEL.FINISH_MESSAGE');
|
||||
}
|
||||
@ -67,6 +86,14 @@ export default {
|
||||
return this.$t('INBOX_MGMT.FINISH.MESSAGE');
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onOpenBaileysLinkDeviceModal() {
|
||||
this.showBaileysLinkDeviceModal = true;
|
||||
},
|
||||
onCloseBaileysLinkDeviceModal() {
|
||||
this.showBaileysLinkDeviceModal = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@ -110,6 +137,11 @@ export default {
|
||||
:script="currentInbox.provider_config.webhook_verify_token"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="isWhatsAppBaileysInbox" class="w-[50%] max-w-[50%] ml-[25%]">
|
||||
<woot-button @click="onOpenBaileysLinkDeviceModal">
|
||||
{{ $t('INBOX_MGMT.ADD.WHATSAPP.BAILEYS.LINK_BUTTON') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
<div class="w-[50%] max-w-[50%] ml-[25%]">
|
||||
<woot-code
|
||||
v-if="isALineInbox"
|
||||
@ -158,5 +190,12 @@ export default {
|
||||
</div>
|
||||
</div>
|
||||
</EmptyState>
|
||||
<WhatsappBaileysLinkDeviceModal
|
||||
v-if="showBaileysLinkDeviceModal"
|
||||
:show="showBaileysLinkDeviceModal"
|
||||
:on-close="onCloseBaileysLinkDeviceModal"
|
||||
:inbox="currentInbox"
|
||||
is-setup
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -88,6 +88,9 @@ export default {
|
||||
if (this.isATwilioWhatsAppChannel) {
|
||||
return this.$t('INBOX_MGMT.ADD.WHATSAPP.PROVIDERS.TWILIO');
|
||||
}
|
||||
if (this.isAWhatsAppBaileysChannel) {
|
||||
return this.$t('INBOX_MGMT.ADD.WHATSAPP.PROVIDERS.BAILEYS');
|
||||
}
|
||||
return '';
|
||||
},
|
||||
tabs() {
|
||||
|
||||
@ -0,0 +1,171 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { required, requiredIf } from '@vuelidate/validators';
|
||||
import router from '../../../../index';
|
||||
import { isPhoneE164OrEmpty } from 'shared/helpers/Validators';
|
||||
import { isValidURL } from '../../../../../helper/URLHelper';
|
||||
|
||||
export default {
|
||||
setup() {
|
||||
return { v$: useVuelidate() };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
inboxName: '',
|
||||
phoneNumber: '',
|
||||
apiKey: '',
|
||||
providerUrl: '',
|
||||
showAdvancedOptions: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({ uiFlags: 'inboxes/getUIFlags' }),
|
||||
},
|
||||
validations() {
|
||||
return {
|
||||
inboxName: { required },
|
||||
phoneNumber: { required, isPhoneE164OrEmpty },
|
||||
providerUrl: {
|
||||
isValidURL: value => !value || isValidURL(value),
|
||||
requiredIf: requiredIf(this.apiKey),
|
||||
},
|
||||
apiKey: { requiredIf: requiredIf(this.providerUrl) },
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
async createChannel() {
|
||||
this.v$.$touch();
|
||||
if (this.v$.$invalid) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const whatsappChannel = await this.$store.dispatch(
|
||||
'inboxes/createChannel',
|
||||
{
|
||||
name: this.inboxName,
|
||||
channel: {
|
||||
type: 'whatsapp',
|
||||
phone_number: this.phoneNumber,
|
||||
provider: 'baileys',
|
||||
provider_config:
|
||||
this.apiKey || this.providerUrl
|
||||
? {
|
||||
api_key: this.apiKey,
|
||||
url: this.providerUrl,
|
||||
}
|
||||
: {},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
router.replace({
|
||||
name: 'settings_inboxes_add_agents',
|
||||
params: {
|
||||
page: 'new',
|
||||
inbox_id: whatsappChannel.id,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
useAlert(
|
||||
error.message || this.$t('INBOX_MGMT.ADD.WHATSAPP.API.ERROR_MESSAGE')
|
||||
);
|
||||
}
|
||||
},
|
||||
setShowAdvancedOptions() {
|
||||
this.showAdvancedOptions = true;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form class="flex flex-wrap mx-0" @submit.prevent="createChannel()">
|
||||
<div class="w-[65%] flex-shrink-0 flex-grow-0 max-w-[65%]">
|
||||
<label :class="{ error: v$.inboxName.$error }">
|
||||
{{ $t('INBOX_MGMT.ADD.WHATSAPP.INBOX_NAME.LABEL') }}
|
||||
<input
|
||||
v-model="inboxName"
|
||||
type="text"
|
||||
:placeholder="$t('INBOX_MGMT.ADD.WHATSAPP.INBOX_NAME.PLACEHOLDER')"
|
||||
@blur="v$.inboxName.$touch"
|
||||
/>
|
||||
<span v-if="v$.inboxName.$error" class="message">
|
||||
{{ $t('INBOX_MGMT.ADD.WHATSAPP.INBOX_NAME.ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="w-[65%] flex-shrink-0 flex-grow-0 max-w-[65%]">
|
||||
<label :class="{ error: v$.phoneNumber.$error }">
|
||||
{{ $t('INBOX_MGMT.ADD.WHATSAPP.PHONE_NUMBER.LABEL') }}
|
||||
<input
|
||||
v-model="phoneNumber"
|
||||
type="text"
|
||||
:placeholder="$t('INBOX_MGMT.ADD.WHATSAPP.PHONE_NUMBER.PLACEHOLDER')"
|
||||
@blur="v$.phoneNumber.$touch"
|
||||
/>
|
||||
<span v-if="v$.phoneNumber.$error" class="message">
|
||||
{{ $t('INBOX_MGMT.ADD.WHATSAPP.PHONE_NUMBER.ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="!showAdvancedOptions"
|
||||
class="w-[65%] flex-shrink-0 flex-grow-0 max-w-[65%] mb-4"
|
||||
>
|
||||
<woot-button
|
||||
icon="add"
|
||||
size="small"
|
||||
variant="link"
|
||||
@click="setShowAdvancedOptions"
|
||||
>
|
||||
{{ $t('INBOX_MGMT.ADD.WHATSAPP.ADVANCED_OPTIONS') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
<template v-else>
|
||||
<div class="w-[65%] flex-shrink-0 flex-grow-0 max-w-[65%]">
|
||||
<span class="text-sm text-gray-600">
|
||||
{{ $t('INBOX_MGMT.ADD.WHATSAPP.ADVANCED_OPTIONS') }}
|
||||
</span>
|
||||
<label :class="{ error: v$.providerUrl.$error }">
|
||||
{{ $t('INBOX_MGMT.ADD.WHATSAPP.PROVIDER_URL.LABEL') }}
|
||||
<input
|
||||
v-model="providerUrl"
|
||||
type="text"
|
||||
:placeholder="
|
||||
$t('INBOX_MGMT.ADD.WHATSAPP.PROVIDER_URL.PLACEHOLDER')
|
||||
"
|
||||
/>
|
||||
<span v-if="v$.providerUrl.$error" class="message">
|
||||
{{ $t('INBOX_MGMT.ADD.WHATSAPP.PROVIDER_URL.ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="w-[65%] flex-shrink-0 flex-grow-0 max-w-[65%]">
|
||||
<label :class="{ error: v$.apiKey.$error }">
|
||||
{{ $t('INBOX_MGMT.ADD.WHATSAPP.API_KEY.LABEL') }}
|
||||
<input
|
||||
v-model="apiKey"
|
||||
type="text"
|
||||
:placeholder="$t('INBOX_MGMT.ADD.WHATSAPP.API_KEY.PLACEHOLDER')"
|
||||
/>
|
||||
<span v-if="v$.apiKey.$error" class="message">
|
||||
{{ $t('INBOX_MGMT.ADD.WHATSAPP.API_KEY.ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="w-full">
|
||||
<woot-submit-button
|
||||
:loading="uiFlags.isCreating"
|
||||
:button-text="$t('INBOX_MGMT.ADD.WHATSAPP.SUBMIT_BUTTON')"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
@ -3,6 +3,7 @@ import PageHeader from '../../SettingsSubPageHeader.vue';
|
||||
import Twilio from './Twilio.vue';
|
||||
import ThreeSixtyDialogWhatsapp from './360DialogWhatsapp.vue';
|
||||
import CloudWhatsapp from './CloudWhatsapp.vue';
|
||||
import BaileysWhatsapp from './BaileysWhatsapp.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@ -10,6 +11,7 @@ export default {
|
||||
Twilio,
|
||||
ThreeSixtyDialogWhatsapp,
|
||||
CloudWhatsapp,
|
||||
BaileysWhatsapp,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@ -37,12 +39,16 @@ export default {
|
||||
<option value="twilio">
|
||||
{{ $t('INBOX_MGMT.ADD.WHATSAPP.PROVIDERS.TWILIO') }}
|
||||
</option>
|
||||
<option value="baileys">
|
||||
{{ $t('INBOX_MGMT.ADD.WHATSAPP.PROVIDERS.BAILEYS') }}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<Twilio v-if="provider === 'twilio'" type="whatsapp" />
|
||||
<ThreeSixtyDialogWhatsapp v-else-if="provider === '360dialog'" />
|
||||
<CloudWhatsapp v-else />
|
||||
<CloudWhatsapp v-if="provider === 'whatsapp_cloud'" />
|
||||
<BaileysWhatsapp v-if="provider === 'baileys'" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -0,0 +1,165 @@
|
||||
<script setup>
|
||||
import { onMounted, computed, onUnmounted, ref, watchEffect } from 'vue';
|
||||
import { useStore } from 'vuex';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import InboxName from 'dashboard/components/widgets/InboxName.vue';
|
||||
import Spinner from 'shared/components/Spinner.vue';
|
||||
|
||||
const props = defineProps({
|
||||
show: { type: Boolean, required: true },
|
||||
onClose: { type: Function, required: true },
|
||||
isSetup: { type: Boolean, required: false },
|
||||
inbox: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const providerConnection = computed(() => props.inbox.provider_connection);
|
||||
const connection = computed(() => providerConnection.value?.connection);
|
||||
const qrDataUrl = computed(() => providerConnection.value?.qr_data_url);
|
||||
const error = computed(() => providerConnection.value?.error);
|
||||
|
||||
const loading = ref(false);
|
||||
|
||||
const handleError = e => {
|
||||
useAlert(e.message);
|
||||
loading.value = false;
|
||||
};
|
||||
const setup = () => {
|
||||
loading.value = true;
|
||||
store
|
||||
.dispatch('inboxes/setupChannelProvider', props.inbox.id)
|
||||
.catch(handleError);
|
||||
};
|
||||
const disconnect = () => {
|
||||
loading.value = true;
|
||||
store
|
||||
.dispatch('inboxes/disconnectChannelProvider', props.inbox.id)
|
||||
.catch(handleError);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
if (!connection.value || connection.value === 'close') {
|
||||
setup();
|
||||
}
|
||||
});
|
||||
onUnmounted(() => {
|
||||
if (connection.value === 'connecting') {
|
||||
disconnect();
|
||||
}
|
||||
});
|
||||
watchEffect(() => {
|
||||
if (connection.value) {
|
||||
loading.value = false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<woot-modal :show="show" size="small" @close="onClose">
|
||||
<div class="flex flex-col h-auto overflow-auto">
|
||||
<woot-modal-header
|
||||
:header-title="
|
||||
$t('INBOX_MGMT.ADD.WHATSAPP.BAILEYS.LINK_DEVICE_MODAL.TITLE')
|
||||
"
|
||||
:header-content="
|
||||
$t('INBOX_MGMT.ADD.WHATSAPP.BAILEYS.LINK_DEVICE_MODAL.SUBTITLE')
|
||||
"
|
||||
/>
|
||||
|
||||
<div class="flex flex-col gap-4 p-8 pt-4">
|
||||
<div class="flex flex-col gap-4 items-center">
|
||||
<InboxName
|
||||
:inbox="inbox"
|
||||
class="!text-lg"
|
||||
with-phone-number
|
||||
with-provider-connection-status
|
||||
/>
|
||||
|
||||
<template v-if="!connection || connection === 'close' || error">
|
||||
<p v-if="error" class="text-red-500 text-center">
|
||||
{{ error }}
|
||||
</p>
|
||||
<woot-button
|
||||
class="button clear w-fit"
|
||||
:is-loading="loading"
|
||||
@click="setup"
|
||||
>
|
||||
{{
|
||||
$t(
|
||||
'INBOX_MGMT.ADD.WHATSAPP.BAILEYS.LINK_DEVICE_MODAL.LINK_DEVICE'
|
||||
)
|
||||
}}
|
||||
</woot-button>
|
||||
</template>
|
||||
|
||||
<template v-else-if="connection === 'connecting'">
|
||||
<div v-if="!qrDataUrl" class="flex flex-col gap-4 items-center">
|
||||
<p>
|
||||
{{
|
||||
$t(
|
||||
'INBOX_MGMT.ADD.WHATSAPP.BAILEYS.LINK_DEVICE_MODAL.LOADING_QRCODE'
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
<Spinner />
|
||||
</div>
|
||||
<img
|
||||
v-else
|
||||
:src="qrDataUrl"
|
||||
alt="QR Code"
|
||||
class="w-[276px] h-[276px]"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template v-else-if="connection === 'reconnecting'">
|
||||
<p>
|
||||
{{
|
||||
$t(
|
||||
'INBOX_MGMT.ADD.WHATSAPP.BAILEYS.LINK_DEVICE_MODAL.RECONNECTING'
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
<Spinner />
|
||||
</template>
|
||||
|
||||
<template v-else-if="connection === 'open'">
|
||||
<p v-if="isSetup" class="text-center">
|
||||
{{
|
||||
$t(
|
||||
'INBOX_MGMT.ADD.WHATSAPP.BAILEYS.LINK_DEVICE_MODAL.CONNECTED'
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
<div class="flex gap-4">
|
||||
<woot-button
|
||||
class="button clear w-fit"
|
||||
:is-loading="loading"
|
||||
@click="disconnect"
|
||||
>
|
||||
{{
|
||||
$t(
|
||||
'INBOX_MGMT.ADD.WHATSAPP.BAILEYS.LINK_DEVICE_MODAL.DISCONNECT'
|
||||
)
|
||||
}}
|
||||
</woot-button>
|
||||
<router-link
|
||||
v-if="isSetup"
|
||||
class="rounded button success"
|
||||
:to="{
|
||||
name: 'inbox_dashboard',
|
||||
params: { inboxId: inbox.id },
|
||||
}"
|
||||
>
|
||||
{{ $t('INBOX_MGMT.FINISH.BUTTON_TEXT') }}
|
||||
</router-link>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</woot-modal>
|
||||
</template>
|
||||
@ -5,8 +5,11 @@ import SettingsSection from '../../../../../components/SettingsSection.vue';
|
||||
import ImapSettings from '../ImapSettings.vue';
|
||||
import SmtpSettings from '../SmtpSettings.vue';
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
import { required } from '@vuelidate/validators';
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
import { requiredIf } from '@vuelidate/validators';
|
||||
import { isValidURL } from '../../../../../helper/URLHelper';
|
||||
import WhatsappBaileysLinkDeviceModal from '../components/WhatsappBaileysLinkDeviceModal.vue';
|
||||
import InboxName from '../../../../../components/widgets/InboxName.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@ -14,6 +17,8 @@ export default {
|
||||
ImapSettings,
|
||||
SmtpSettings,
|
||||
NextButton,
|
||||
WhatsappBaileysLinkDeviceModal,
|
||||
InboxName,
|
||||
},
|
||||
mixins: [inboxMixin],
|
||||
props: {
|
||||
@ -29,10 +34,17 @@ export default {
|
||||
return {
|
||||
hmacMandatory: false,
|
||||
whatsAppInboxAPIKey: '',
|
||||
whatsAppProviderUrl: '',
|
||||
showBaileysLinkDeviceModal: false,
|
||||
};
|
||||
},
|
||||
validations: {
|
||||
whatsAppInboxAPIKey: { required },
|
||||
validations() {
|
||||
return {
|
||||
whatsAppInboxAPIKey: {
|
||||
requiredIf: requiredIf(!this.isAWhatsAppBaileysChannel),
|
||||
},
|
||||
whatsAppProviderUrl: { isValidURL: value => !value || isValidURL(value) },
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
inbox() {
|
||||
@ -83,6 +95,31 @@ export default {
|
||||
useAlert(this.$t('INBOX_MGMT.EDIT.API.ERROR_MESSAGE'));
|
||||
}
|
||||
},
|
||||
async updateWhatsAppProviderUrl() {
|
||||
try {
|
||||
const payload = {
|
||||
id: this.inbox.id,
|
||||
formData: false,
|
||||
channel: {
|
||||
provider_config: {
|
||||
...this.inbox.provider_config,
|
||||
provider_url: this.whatsAppProviderUrl,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
await this.$store.dispatch('inboxes/updateInbox', payload);
|
||||
useAlert(this.$t('INBOX_MGMT.EDIT.API.SUCCESS_MESSAGE'));
|
||||
} catch (error) {
|
||||
useAlert(this.$t('INBOX_MGMT.EDIT.API.ERROR_MESSAGE'));
|
||||
}
|
||||
},
|
||||
onOpenBaileysLinkDeviceModal() {
|
||||
this.showBaileysLinkDeviceModal = true;
|
||||
},
|
||||
onCloseBaileysLinkDeviceModal() {
|
||||
this.showBaileysLinkDeviceModal = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@ -194,8 +231,8 @@ export default {
|
||||
<ImapSettings :inbox="inbox" />
|
||||
<SmtpSettings v-if="inbox.imap_enabled" :inbox="inbox" />
|
||||
</div>
|
||||
<div v-else-if="isAWhatsAppChannel && !isATwilioChannel">
|
||||
<div v-if="inbox.provider_config" class="mx-8">
|
||||
<div v-else-if="isAWhatsAppCloudChannel && inbox.provider_config">
|
||||
<div class="mx-8">
|
||||
<SettingsSection
|
||||
:title="$t('INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_WEBHOOK_TITLE')"
|
||||
:sub-title="$t('INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_WEBHOOK_SUBHEADER')"
|
||||
@ -237,6 +274,116 @@ export default {
|
||||
</SettingsSection>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="isAWhatsAppBaileysChannel">
|
||||
<WhatsappBaileysLinkDeviceModal
|
||||
v-if="showBaileysLinkDeviceModal"
|
||||
:show="showBaileysLinkDeviceModal"
|
||||
:on-close="onCloseBaileysLinkDeviceModal"
|
||||
:inbox="inbox"
|
||||
/>
|
||||
<div class="mx-8">
|
||||
<SettingsSection
|
||||
:title="
|
||||
$t(
|
||||
'INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_MANAGE_PROVIDER_CONNECTION_TITLE'
|
||||
)
|
||||
"
|
||||
:sub-title="
|
||||
$t(
|
||||
'INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_MANAGE_PROVIDER_CONNECTION_SUBHEADER'
|
||||
)
|
||||
"
|
||||
>
|
||||
<div class="flex flex-col gap-2">
|
||||
<InboxName
|
||||
:inbox="inbox"
|
||||
class="!text-lg !m-0"
|
||||
with-phone-number
|
||||
with-provider-connection-status
|
||||
/>
|
||||
<woot-button class="w-fit" @click="onOpenBaileysLinkDeviceModal">
|
||||
{{
|
||||
$t(
|
||||
'INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_MANAGE_PROVIDER_CONNECTION_BUTTON'
|
||||
)
|
||||
}}
|
||||
</woot-button>
|
||||
</div>
|
||||
</SettingsSection>
|
||||
<SettingsSection
|
||||
:title="$t('INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_PROVIDER_URL_TITLE')"
|
||||
:sub-title="
|
||||
$t('INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_PROVIDER_URL_SUBHEADER')
|
||||
"
|
||||
>
|
||||
<div
|
||||
class="flex items-center justify-between flex-1 mt-2 whatsapp-settings--content"
|
||||
>
|
||||
<woot-input
|
||||
v-model="whatsAppProviderUrl"
|
||||
type="text"
|
||||
class="flex-1 mr-2"
|
||||
:placeholder="
|
||||
$t('INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_PROVIDER_URL_PLACEHOLDER')
|
||||
"
|
||||
@keydown="v$.whatsAppProviderUrl.$touch"
|
||||
/>
|
||||
<woot-button
|
||||
:disabled="
|
||||
v$.whatsAppProviderUrl.$invalid ||
|
||||
whatsAppProviderUrl === inbox.provider_config.provider_url
|
||||
"
|
||||
@click="updateWhatsAppProviderUrl"
|
||||
>
|
||||
{{ $t('INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_SECTION_UPDATE_BUTTON') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
<span v-if="v$.whatsAppProviderUrl.$error" class="text-red-400">
|
||||
{{ $t('INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_PROVIDER_URL_ERROR') }}
|
||||
</span>
|
||||
</SettingsSection>
|
||||
<template v-if="inbox.provider_config.api_key">
|
||||
<SettingsSection
|
||||
:title="$t('INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_SECTION_TITLE')"
|
||||
:sub-title="
|
||||
$t('INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_SECTION_SUBHEADER')
|
||||
"
|
||||
>
|
||||
<woot-code :script="inbox.provider_config.api_key" />
|
||||
</SettingsSection>
|
||||
</template>
|
||||
<SettingsSection
|
||||
:title="$t('INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_SECTION_UPDATE_TITLE')"
|
||||
:sub-title="
|
||||
$t('INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_SECTION_UPDATE_SUBHEADER')
|
||||
"
|
||||
>
|
||||
<div
|
||||
class="flex items-center justify-between flex-1 mt-2 whatsapp-settings--content"
|
||||
>
|
||||
<woot-input
|
||||
v-model="whatsAppInboxAPIKey"
|
||||
type="text"
|
||||
class="flex-1 mr-2"
|
||||
:placeholder="
|
||||
$t(
|
||||
'INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_SECTION_UPDATE_PLACEHOLDER'
|
||||
)
|
||||
"
|
||||
/>
|
||||
<woot-button
|
||||
:disabled="
|
||||
v$.whatsAppInboxAPIKey.$invalid ||
|
||||
whatsAppInboxAPIKey === inbox.provider_config.api_key
|
||||
"
|
||||
@click="updateWhatsAppInboxAPIKey"
|
||||
>
|
||||
{{ $t('INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_SECTION_UPDATE_BUTTON') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</SettingsSection>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@ -268,6 +268,20 @@ export const actions = {
|
||||
throw new Error(error);
|
||||
}
|
||||
},
|
||||
setupChannelProvider: async (_, inboxId) => {
|
||||
try {
|
||||
await InboxesAPI.setupChannelProvider(inboxId);
|
||||
} catch (error) {
|
||||
throwErrorMessage(error);
|
||||
}
|
||||
},
|
||||
disconnectChannelProvider: async (_, inboxId) => {
|
||||
try {
|
||||
await InboxesAPI.disconnectChannelProvider(inboxId);
|
||||
} catch (error) {
|
||||
throwErrorMessage(error);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export const mutations = {
|
||||
|
||||
@ -285,5 +285,7 @@
|
||||
"M9.60364 9.20645C9.60364 8.67008 10.0385 8.23523 10.5749 8.23523C11.1113 8.23523 11.5461 8.67008 11.5461 9.20645V11.4511C11.5461 11.9875 11.1113 12.4223 10.5749 12.4223C10.0385 12.4223 9.60364 11.9875 9.60364 11.4511V9.20645Z",
|
||||
"M17.1442 5.57049C13.5275 5.06019 10.5793 5.04007 6.88135 5.56825C5.9466 5.70176 5.32812 5.79197 4.85654 5.92976C4.41928 6.05757 4.17061 6.20994 3.96492 6.43984C3.539 6.91583 3.48286 7.45419 3.4248 9.33184C3.36775 11.1772 3.48076 12.831 3.69481 14.6918C3.80887 15.6834 3.88736 16.3526 4.01268 16.8613C4.13155 17.3439 4.27532 17.6034 4.47513 17.802C4.67654 18.0023 4.93467 18.1435 5.40841 18.2581C5.90952 18.3793 6.56702 18.4526 7.5442 18.5592C10.7045 18.904 13.0702 18.9022 16.2423 18.561C17.2313 18.4546 17.8995 18.3813 18.4081 18.2609C18.8913 18.1465 19.1511 18.0063 19.3497 17.8118C19.5442 17.6213 19.6928 17.3587 19.8217 16.852C19.9561 16.3234 20.0476 15.624 20.18 14.5966C20.4162 12.7633 20.5863 11.1533 20.5929 9.3896C20.5999 7.50391 20.5613 6.96737 20.1306 6.46971C19.9226 6.22932 19.6696 6.0713 19.2224 5.93968C18.7395 5.79754 18.1042 5.70594 17.1442 5.57049ZM6.65555 3.98715C10.5078 3.43695 13.6072 3.45849 17.3674 3.98902L17.4224 3.99678C18.3127 4.12235 19.0648 4.22844 19.6733 4.40753C20.33 4.60078 20.8792 4.89417 21.3382 5.4245C22.2041 6.42482 22.1984 7.6117 22.1909 9.18858C22.1905 9.25686 22.1902 9.32584 22.19 9.3956C22.183 11.2604 22.0026 12.949 21.764 14.8006L21.7577 14.8496C21.6332 15.8159 21.5307 16.6121 21.3695 17.2458C21.2 17.9121 20.9467 18.4833 20.4672 18.9529C19.9919 19.4183 19.4302 19.6602 18.776 19.8151C18.1582 19.9613 17.3895 20.044 16.4629 20.1436L16.4131 20.149C13.1283 20.5023 10.6472 20.5043 7.37097 20.1469L7.32043 20.1414C6.40679 20.0417 5.64604 19.9587 5.03292 19.8104C4.38112 19.6527 3.82317 19.406 3.34911 18.9347C2.87346 18.4618 2.62363 17.8999 2.46191 17.2433C2.30938 16.6241 2.22071 15.8531 2.11393 14.9246L2.10815 14.8743C1.88863 12.9659 1.76823 11.23 1.82845 9.28246C1.83063 9.2118 1.83272 9.14191 1.83479 9.07281C1.8816 7.50776 1.91671 6.33374 2.7747 5.37486C3.22992 4.86612 3.76798 4.58399 4.40853 4.39678C5.00257 4.22316 5.73505 4.11858 6.60207 3.99479C6.61981 3.99225 6.63764 3.9897 6.65555 3.98715Z"
|
||||
],
|
||||
"scan-person-outline": "M5.25 3.5A1.75 1.75 0 0 0 3.5 5.25v3a.75.75 0 0 1-1.5 0v-3A3.25 3.25 0 0 1 5.25 2h3a.75.75 0 0 1 0 1.5zm0 17a1.75 1.75 0 0 1-1.75-1.75v-3a.75.75 0 0 0-1.5 0v3A3.25 3.25 0 0 0 5.25 22h3a.75.75 0 0 0 .707-1l-.005-.015a.75.75 0 0 0-.702-.485zM20.5 5.25a1.75 1.75 0 0 0-1.75-1.75h-3a.75.75 0 0 1 0-1.5h3A3.25 3.25 0 0 1 22 5.25v3a.75.75 0 0 1-1.5 0zM18.75 20.5a1.75 1.75 0 0 0 1.75-1.75v-3a.75.75 0 0 1 1.5 0v3A3.25 3.25 0 0 1 18.75 22h-3a.75.75 0 0 1 0-1.5zM6.5 18.616q0 .465.258.884H5.25a1 1 0 0 1-.129-.011A3.1 3.1 0 0 1 5 18.616v-.366A2.25 2.25 0 0 1 7.25 16h9.5A2.25 2.25 0 0 1 19 18.25v.366c0 .31-.047.601-.132.875a1 1 0 0 1-.118.009h-1.543a1.56 1.56 0 0 0 .293-.884v-.366a.75.75 0 0 0-.75-.75h-9.5a.75.75 0 0 0-.75.75zm8.25-8.866a2.75 2.75 0 1 0-5.5 0a2.75 2.75 0 0 0 5.5 0m1.5 0a4.25 4.25 0 1 1-8.5 0a4.25 4.25 0 0 1 8.5 0"
|
||||
"scan-person-outline": "M5.25 3.5A1.75 1.75 0 0 0 3.5 5.25v3a.75.75 0 0 1-1.5 0v-3A3.25 3.25 0 0 1 5.25 2h3a.75.75 0 0 1 0 1.5zm0 17a1.75 1.75 0 0 1-1.75-1.75v-3a.75.75 0 0 0-1.5 0v3A3.25 3.25 0 0 0 5.25 22h3a.75.75 0 0 0 .707-1l-.005-.015a.75.75 0 0 0-.702-.485zM20.5 5.25a1.75 1.75 0 0 0-1.75-1.75h-3a.75.75 0 0 1 0-1.5h3A3.25 3.25 0 0 1 22 5.25v3a.75.75 0 0 1-1.5 0zM18.75 20.5a1.75 1.75 0 0 0 1.75-1.75v-3a.75.75 0 0 1 1.5 0v3A3.25 3.25 0 0 1 18.75 22h-3a.75.75 0 0 1 0-1.5zM6.5 18.616q0 .465.258.884H5.25a1 1 0 0 1-.129-.011A3.1 3.1 0 0 1 5 18.616v-.366A2.25 2.25 0 0 1 7.25 16h9.5A2.25 2.25 0 0 1 19 18.25v.366c0 .31-.047.601-.132.875a1 1 0 0 1-.118.009h-1.543a1.56 1.56 0 0 0 .293-.884v-.366a.75.75 0 0 0-.75-.75h-9.5a.75.75 0 0 0-.75.75zm8.25-8.866a2.75 2.75 0 1 0-5.5 0a2.75 2.75 0 0 0 5.5 0m1.5 0a4.25 4.25 0 1 1-8.5 0a4.25 4.25 0 0 1 8.5 0",
|
||||
"circle-outline": "M12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22ZM12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20Z",
|
||||
"circle-filled": "M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z"
|
||||
}
|
||||
|
||||
@ -86,6 +86,12 @@ export default {
|
||||
this.whatsAppAPIProvider === 'default'
|
||||
);
|
||||
},
|
||||
isAWhatsAppBaileysChannel() {
|
||||
return (
|
||||
this.channelType === INBOX_TYPES.WHATSAPP &&
|
||||
this.whatsAppAPIProvider === 'baileys'
|
||||
);
|
||||
},
|
||||
chatAdditionalAttributes() {
|
||||
const { additional_attributes: additionalAttributes } = this.chat || {};
|
||||
return additionalAttributes || {};
|
||||
|
||||
@ -269,6 +269,14 @@ describe('inboxMixin', () => {
|
||||
expect(wrapper.vm.is360DialogWhatsAppChannel).toBe(true);
|
||||
});
|
||||
|
||||
it('isAWhatsAppBaileysChannel returns true if channel type is WhatsApp and provider is baileys', () => {
|
||||
const Component = getComponentConfigForInbox('Channel::Whatsapp', {
|
||||
provider: 'baileys',
|
||||
});
|
||||
const wrapper = shallowMount(Component);
|
||||
expect(wrapper.vm.isAWhatsAppBaileysChannel).toBe(true);
|
||||
});
|
||||
|
||||
it('isAWhatsAppChannel returns true if channel type is WhatsApp', () => {
|
||||
const Component = getComponentConfigForInbox('Channel::Whatsapp');
|
||||
const wrapper = shallowMount(Component);
|
||||
|
||||
@ -2,8 +2,7 @@ class Webhooks::WhatsappEventsJob < ApplicationJob
|
||||
queue_as :low
|
||||
|
||||
def perform(params = {})
|
||||
channel = find_channel_from_whatsapp_business_payload(params)
|
||||
|
||||
channel = find_channel(params)
|
||||
if channel_is_inactive?(channel)
|
||||
Rails.logger.warn("Inactive WhatsApp channel: #{channel&.phone_number || "unknown - #{params[:phone_number]}"}")
|
||||
return
|
||||
@ -12,6 +11,8 @@ class Webhooks::WhatsappEventsJob < ApplicationJob
|
||||
case channel.provider
|
||||
when 'whatsapp_cloud'
|
||||
Whatsapp::IncomingMessageWhatsappCloudService.new(inbox: channel.inbox, params: params).perform
|
||||
when 'baileys'
|
||||
Whatsapp::IncomingMessageBaileysService.new(inbox: channel.inbox, params: params).perform
|
||||
else
|
||||
Whatsapp::IncomingMessageService.new(inbox: channel.inbox, params: params).perform
|
||||
end
|
||||
@ -19,6 +20,14 @@ class Webhooks::WhatsappEventsJob < ApplicationJob
|
||||
|
||||
private
|
||||
|
||||
def find_channel(params)
|
||||
return find_channel_from_whatsapp_business_payload(params) if params[:object] == 'whatsapp_business_account'
|
||||
|
||||
return unless params[:phone_number]
|
||||
|
||||
Channel::Whatsapp.find_by(phone_number: params[:phone_number])
|
||||
end
|
||||
|
||||
def channel_is_inactive?(channel)
|
||||
return true if channel.blank?
|
||||
return true if channel.reauthorization_required?
|
||||
@ -27,24 +36,12 @@ class Webhooks::WhatsappEventsJob < ApplicationJob
|
||||
false
|
||||
end
|
||||
|
||||
def find_channel_by_url_param(params)
|
||||
return unless params[:phone_number]
|
||||
|
||||
Channel::Whatsapp.find_by(phone_number: params[:phone_number])
|
||||
end
|
||||
|
||||
def find_channel_from_whatsapp_business_payload(params)
|
||||
# for the case where facebook cloud api support multiple numbers for a single app
|
||||
# https://github.com/chatwoot/chatwoot/issues/4712#issuecomment-1173838350
|
||||
# we will give priority to the phone_number in the payload
|
||||
return get_channel_from_wb_payload(params) if params[:object] == 'whatsapp_business_account'
|
||||
|
||||
find_channel_by_url_param(params)
|
||||
end
|
||||
|
||||
def get_channel_from_wb_payload(wb_params)
|
||||
phone_number = "+#{wb_params[:entry].first[:changes].first.dig(:value, :metadata, :display_phone_number)}"
|
||||
phone_number_id = wb_params[:entry].first[:changes].first.dig(:value, :metadata, :phone_number_id)
|
||||
phone_number = "+#{params[:entry].first[:changes].first.dig(:value, :metadata, :display_phone_number)}"
|
||||
phone_number_id = params[:entry].first[:changes].first.dig(:value, :metadata, :phone_number_id)
|
||||
channel = Channel::Whatsapp.find_by(phone_number: phone_number)
|
||||
# validate to ensure the phone number id matches the whatsapp channel
|
||||
channel if channel && channel.provider_config['phone_number_id'] == phone_number_id
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
# phone_number :string not null
|
||||
# provider :string default("default")
|
||||
# provider_config :jsonb
|
||||
# provider_connection :jsonb
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# account_id :integer not null
|
||||
@ -25,13 +26,15 @@ class Channel::Whatsapp < ApplicationRecord
|
||||
EDITABLE_ATTRS = [:phone_number, :provider, { provider_config: {} }].freeze
|
||||
|
||||
# default at the moment is 360dialog lets change later.
|
||||
PROVIDERS = %w[default whatsapp_cloud].freeze
|
||||
PROVIDERS = %w[default whatsapp_cloud baileys].freeze
|
||||
before_validation :ensure_webhook_verify_token
|
||||
|
||||
validates :provider, inclusion: { in: PROVIDERS }
|
||||
validates :phone_number, presence: true, uniqueness: true
|
||||
validate :validate_provider_config
|
||||
|
||||
has_one :inbox, as: :channel, dependent: :destroy
|
||||
|
||||
after_create :sync_templates
|
||||
|
||||
def name
|
||||
@ -39,15 +42,18 @@ class Channel::Whatsapp < ApplicationRecord
|
||||
end
|
||||
|
||||
def provider_service
|
||||
if provider == 'whatsapp_cloud'
|
||||
case provider
|
||||
when 'whatsapp_cloud'
|
||||
Whatsapp::Providers::WhatsappCloudService.new(whatsapp_channel: self)
|
||||
when 'baileys'
|
||||
Whatsapp::Providers::WhatsappBaileysService.new(whatsapp_channel: self)
|
||||
else
|
||||
Whatsapp::Providers::Whatsapp360DialogService.new(whatsapp_channel: self)
|
||||
end
|
||||
end
|
||||
|
||||
def messaging_window_enabled?
|
||||
true
|
||||
provider != 'baileys'
|
||||
end
|
||||
|
||||
def mark_message_templates_updated
|
||||
@ -56,6 +62,23 @@ class Channel::Whatsapp < ApplicationRecord
|
||||
# rubocop:enable Rails/SkipsModelValidations
|
||||
end
|
||||
|
||||
def update_provider_connection!(provider_connection)
|
||||
assign_attributes(provider_connection: provider_connection)
|
||||
# NOTE: Skip `validate_provider_config?` check
|
||||
save!(validate: false)
|
||||
end
|
||||
|
||||
def provider_connection_data
|
||||
data = { connection: provider_connection['connection'] }
|
||||
if Current.account_user&.administrator?
|
||||
data[:qr_data_url] = provider_connection['qr_data_url']
|
||||
data[:error] = provider_connection['error']
|
||||
end
|
||||
data
|
||||
end
|
||||
|
||||
delegate :setup_channel_provider, to: :provider_service
|
||||
delegate :disconnect_channel_provider, to: :provider_service
|
||||
delegate :send_message, to: :provider_service
|
||||
delegate :send_template, to: :provider_service
|
||||
delegate :sync_templates, to: :provider_service
|
||||
@ -65,7 +88,7 @@ class Channel::Whatsapp < ApplicationRecord
|
||||
private
|
||||
|
||||
def ensure_webhook_verify_token
|
||||
provider_config['webhook_verify_token'] ||= SecureRandom.hex(16) if provider == 'whatsapp_cloud'
|
||||
provider_config['webhook_verify_token'] ||= SecureRandom.hex(16) if provider.in?(%w[whatsapp_cloud baileys])
|
||||
end
|
||||
|
||||
def validate_provider_config
|
||||
|
||||
@ -37,7 +37,8 @@ module CacheKeys
|
||||
|
||||
def update_cache_key_for_account(account_id, key)
|
||||
prefixed_cache_key = get_prefixed_cache_key(account_id, key)
|
||||
Redis::Alfred.setex(prefixed_cache_key, Time.now.utc.to_i, CACHE_KEYS_EXPIRY)
|
||||
timestamp = (Time.now.utc.to_f * 1000).to_i
|
||||
Redis::Alfred.setex(prefixed_cache_key, timestamp, CACHE_KEYS_EXPIRY)
|
||||
end
|
||||
|
||||
def dispatch_cache_update_event
|
||||
|
||||
@ -57,4 +57,12 @@ class InboxPolicy < ApplicationPolicy
|
||||
def avatar?
|
||||
@account_user.administrator?
|
||||
end
|
||||
|
||||
def setup_channel_provider?
|
||||
@account_user.administrator?
|
||||
end
|
||||
|
||||
def disconnect_channel_provider?
|
||||
@account_user.administrator?
|
||||
end
|
||||
end
|
||||
|
||||
140
app/services/whatsapp/incoming_message_baileys_service.rb
Normal file
140
app/services/whatsapp/incoming_message_baileys_service.rb
Normal file
@ -0,0 +1,140 @@
|
||||
class Whatsapp::IncomingMessageBaileysService < Whatsapp::IncomingMessageBaseService
|
||||
class InvalidWebhookVerifyToken < StandardError; end
|
||||
|
||||
def perform
|
||||
raise InvalidWebhookVerifyToken if processed_params[:webhookVerifyToken] != inbox.channel.provider_config['webhook_verify_token']
|
||||
return if processed_params[:event].blank? || processed_params[:data].blank?
|
||||
|
||||
event_prefix = processed_params[:event].gsub(/[\.-]/, '_')
|
||||
method_name = "process_#{event_prefix}"
|
||||
if respond_to?(method_name, true)
|
||||
# TODO: Implement the methods for all expected events
|
||||
send(method_name)
|
||||
else
|
||||
Rails.logger.warn "Baileys unsupported event: #{processed_params[:event]}"
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def process_connection_update
|
||||
data = processed_params[:data]
|
||||
|
||||
# NOTE: `connection` values
|
||||
# - `close`: Never opened, or closed and no longer able to send/receive messages
|
||||
# - `connecting`: In the process of connecting, expecting QR code to be read
|
||||
# - `reconnecting`: Connection has been established, but not open (i.e. device is being linked for the first time, or Baileys server restart)
|
||||
# - `open`: Open and ready to send/receive messages
|
||||
inbox.channel.update_provider_connection!({
|
||||
connection: data[:connection] || inbox.channel.provider_connection['connection'],
|
||||
qr_data_url: data[:qrDataUrl] || nil,
|
||||
error: data[:error] ? I18n.t("errors.inboxes.channel.provider_connection.#{data[:error]}") : nil
|
||||
}.compact)
|
||||
|
||||
Rails.logger.error "Baileys connection error: #{data[:error]}" if data[:error].present?
|
||||
end
|
||||
|
||||
def process_messages_upsert
|
||||
messages = processed_params[:data][:messages]
|
||||
messages.each do |message|
|
||||
@message = nil
|
||||
@contact_inbox = nil
|
||||
@contact = nil
|
||||
@raw_message = message
|
||||
handle_message
|
||||
end
|
||||
end
|
||||
|
||||
def handle_message
|
||||
return if find_message_by_source_id(message_id) || message_under_process?
|
||||
|
||||
cache_message_source_id_in_redis
|
||||
set_contact
|
||||
|
||||
unless @contact
|
||||
Rails.logger.warn "Contact not found for message: #{message_id}"
|
||||
return
|
||||
end
|
||||
|
||||
set_conversation
|
||||
handle_create_message
|
||||
clear_message_source_id_from_redis
|
||||
end
|
||||
|
||||
def set_contact
|
||||
phone_number_from_jid = @raw_message[:key][:remoteJid].split('@').first.split(':').first
|
||||
|
||||
contact_inbox = ::ContactInboxWithContactBuilder.new(
|
||||
source_id: phone_number_from_jid,
|
||||
inbox: inbox,
|
||||
contact_attributes: { name: @raw_message[:pushName], phone_number: "+#{phone_number_from_jid}" }
|
||||
).perform
|
||||
|
||||
@contact_inbox = contact_inbox
|
||||
@contact = contact_inbox.contact
|
||||
end
|
||||
|
||||
def handle_create_message
|
||||
case message_type
|
||||
when 'text'
|
||||
create_text_message
|
||||
else
|
||||
Rails.logger.warn "Baileys unsupported message type: #{message_type}"
|
||||
end
|
||||
end
|
||||
|
||||
def message_type # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
|
||||
msg = @raw_message[:message]
|
||||
return 'text' if msg.key?(:conversation)
|
||||
return 'contacts' if msg.key?(:contactMessage)
|
||||
return 'image' if msg.key?(:imageMessage)
|
||||
return 'audio' if msg.key?(:audioMessage)
|
||||
return 'video' if msg.key?(:videoMessage)
|
||||
return 'video_note' if msg.key?(:ptvMessage)
|
||||
return 'location' if msg.key?(:locationMessage)
|
||||
return 'live_location' if msg.key?(:liveLocationMessage)
|
||||
return 'document' if msg.key?(:documentMessage)
|
||||
return 'poll' if msg.key?(:pollCreationMessageV3)
|
||||
return 'event' if msg.key?(:eventMessage)
|
||||
return 'sticker' if msg.key?(:stickerMessage)
|
||||
|
||||
'unsupported'
|
||||
end
|
||||
|
||||
def create_text_message
|
||||
is_outgoing = @raw_message[:key][:fromMe]
|
||||
sender = is_outgoing ? @inbox.account.account_users.first.user : @contact
|
||||
sender_type = is_outgoing ? 'User' : 'Contact'
|
||||
message_type = is_outgoing ? :outgoing : :incoming
|
||||
|
||||
@message = @conversation.messages.create!(
|
||||
content: @raw_message[:message][:conversation],
|
||||
account_id: @inbox.account_id,
|
||||
inbox_id: @inbox.id,
|
||||
source_id: message_id,
|
||||
sender: sender,
|
||||
sender_type: sender_type,
|
||||
message_type: message_type,
|
||||
in_reply_to_external_id: nil
|
||||
)
|
||||
end
|
||||
|
||||
def message_id
|
||||
@raw_message[:key][:id]
|
||||
end
|
||||
|
||||
def message_under_process?
|
||||
key = format(Redis::RedisKeys::MESSAGE_SOURCE_KEY, id: message_id)
|
||||
Redis::Alfred.get(key)
|
||||
end
|
||||
|
||||
def cache_message_source_id_in_redis
|
||||
key = format(Redis::RedisKeys::MESSAGE_SOURCE_KEY, id: message_id)
|
||||
::Redis::Alfred.setex(key, true)
|
||||
end
|
||||
|
||||
def clear_message_source_id_from_redis
|
||||
key = format(Redis::RedisKeys::MESSAGE_SOURCE_KEY, id: message_id)
|
||||
::Redis::Alfred.delete(key)
|
||||
end
|
||||
end
|
||||
101
app/services/whatsapp/providers/whatsapp_baileys_service.rb
Normal file
101
app/services/whatsapp/providers/whatsapp_baileys_service.rb
Normal file
@ -0,0 +1,101 @@
|
||||
class Whatsapp::Providers::WhatsappBaileysService < Whatsapp::Providers::BaseService
|
||||
class MessageContentTypeNotSupported < StandardError; end
|
||||
|
||||
DEFAULT_CLIENT_NAME = ENV.fetch('BAILEYS_PROVIDER_DEFAULT_CLIENT_NAME', nil)
|
||||
DEFAULT_URL = ENV.fetch('BAILEYS_PROVIDER_DEFAULT_URL', nil)
|
||||
DEFAULT_API_KEY = ENV.fetch('BAILEYS_PROVIDER_DEFAULT_API_KEY', nil)
|
||||
|
||||
def setup_channel_provider
|
||||
response = HTTParty.post(
|
||||
"#{provider_url}/connections/#{whatsapp_channel.phone_number}",
|
||||
headers: api_headers,
|
||||
body: {
|
||||
clientName: DEFAULT_CLIENT_NAME,
|
||||
webhookUrl: whatsapp_channel.inbox.callback_webhook_url,
|
||||
webhookVerifyToken: whatsapp_channel.provider_config['webhook_verify_token']
|
||||
}.to_json
|
||||
)
|
||||
|
||||
process_response(response)
|
||||
end
|
||||
|
||||
def disconnect_channel_provider
|
||||
response = HTTParty.delete(
|
||||
"#{provider_url}/connections/#{whatsapp_channel.phone_number}",
|
||||
headers: api_headers
|
||||
)
|
||||
|
||||
process_response(response)
|
||||
end
|
||||
|
||||
def send_message(phone_number, message)
|
||||
raise MessageContentTypeNotSupported unless message.content_type == 'text'
|
||||
|
||||
response = HTTParty.post(
|
||||
"#{provider_url}/connections/#{whatsapp_channel.phone_number}/send-message",
|
||||
headers: api_headers,
|
||||
body: {
|
||||
type: 'text',
|
||||
recipient: phone_number,
|
||||
message: message.content
|
||||
}.to_json
|
||||
)
|
||||
|
||||
return unless process_response(response)
|
||||
|
||||
response.parsed_response.dig('data', 'key', 'id')
|
||||
end
|
||||
|
||||
def send_template(phone_number, template_info); end
|
||||
|
||||
def sync_templates; end
|
||||
|
||||
def media_url(media_id); end
|
||||
|
||||
def api_headers
|
||||
{ 'x-api-key' => api_key, 'Content-Type' => 'application/json' }
|
||||
end
|
||||
|
||||
def validate_provider_config?
|
||||
response = HTTParty.get(
|
||||
"#{provider_url}/status",
|
||||
headers: api_headers
|
||||
)
|
||||
|
||||
process_response(response)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def provider_url
|
||||
whatsapp_channel.provider_config['provider_url'].presence || DEFAULT_URL
|
||||
end
|
||||
|
||||
def api_key
|
||||
whatsapp_channel.provider_config['api_key'].presence || DEFAULT_API_KEY
|
||||
end
|
||||
|
||||
def process_response(response)
|
||||
Rails.logger.error response.body unless response.success?
|
||||
response.success?
|
||||
end
|
||||
|
||||
private_class_method def self.with_error_handling(*method_names)
|
||||
method_names.each do |method_name|
|
||||
original_method = instance_method(method_name)
|
||||
|
||||
define_method(method_name) do |*args, &block|
|
||||
original_method.bind_call(self, *args, &block)
|
||||
rescue StandardError => e
|
||||
handle_channel_error
|
||||
raise e
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def handle_channel_error
|
||||
whatsapp_channel.update_provider_connection!(connection: 'close')
|
||||
end
|
||||
|
||||
with_error_handling :setup_channel_provider, :disconnect_channel_provider, :send_message
|
||||
end
|
||||
@ -110,4 +110,5 @@ json.provider resource.channel.try(:provider)
|
||||
if resource.whatsapp?
|
||||
json.message_templates resource.channel.try(:message_templates)
|
||||
json.provider_config resource.channel.try(:provider_config) if Current.account_user&.administrator?
|
||||
json.provider_connection resource.channel.try(:provider_connection_data)
|
||||
end
|
||||
|
||||
@ -76,6 +76,9 @@ en:
|
||||
connection_closed_error: Connection closed.
|
||||
validations:
|
||||
name: should not start or end with symbols, and it should not have < > / \ @ characters.
|
||||
channel:
|
||||
provider_connection:
|
||||
wrong_phone_number: The phone number you are trying to link is not the same as the one you have configured. Please make sure the phone number is correct before scanning the QR code.
|
||||
custom_filters:
|
||||
number_of_records: Limit reached. The maximum number of allowed custom filters for a user per account is 50.
|
||||
invalid_attribute: Invalid attribute key - [%{key}]. The key should be one of [%{allowed_keys}] or a custom attribute defined in the account.
|
||||
|
||||
@ -64,6 +64,9 @@ pt_BR:
|
||||
connection_closed_error: Conexão fechada.
|
||||
validations:
|
||||
name: 'não deve iniciar ou terminar com símbolos e não deve ter os caracteres: < > / \ @.'
|
||||
channel:
|
||||
provider_connection:
|
||||
wrong_phone_number: O número de telefone que você está tentando conectar não é o mesmo que o configurado. Certifique-se de que o número está correto antes de escanear o QR code.
|
||||
custom_filters:
|
||||
number_of_records: Limite atingido. O número máximo de filtros personalizados permitidos para um usuário por conta é de 50.
|
||||
invalid_attribute: Chave de atributo inválido - [%{key}]. A chave deve ser uma das [%{allowed_keys}] ou um atributo personalizado definido na conta.
|
||||
|
||||
@ -173,6 +173,8 @@ Rails.application.routes.draw do
|
||||
get :campaigns, on: :member
|
||||
get :agent_bot, on: :member
|
||||
post :set_agent_bot, on: :member
|
||||
post :setup_channel_provider, on: :member
|
||||
post :disconnect_channel_provider, on: :member
|
||||
delete :avatar, on: :member
|
||||
end
|
||||
resources :inbox_members, only: [:create, :show], param: :inbox_id do
|
||||
|
||||
@ -0,0 +1,5 @@
|
||||
class AddProviderConnectionToWhatsapp < ActiveRecord::Migration[7.0]
|
||||
def change
|
||||
add_column :channel_whatsapp, :provider_connection, :jsonb, default: {}
|
||||
end
|
||||
end
|
||||
@ -471,6 +471,7 @@ ActiveRecord::Schema[7.0].define(version: 2025_04_02_233933) do
|
||||
t.datetime "updated_at", null: false
|
||||
t.jsonb "message_templates", default: {}
|
||||
t.datetime "message_templates_last_updated", precision: nil
|
||||
t.jsonb "provider_connection", default: {}
|
||||
t.index ["phone_number"], name: "index_channel_whatsapp_on_phone_number", unique: true
|
||||
end
|
||||
|
||||
|
||||
@ -774,4 +774,82 @@ RSpec.describe 'Inboxes API', type: :request do
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST /api/v1/accounts/:account_id/inboxes/:id/setup_channel_provider' do
|
||||
let(:channel) { create(:channel_whatsapp, account: account, provider: 'baileys', validate_provider_config: false) }
|
||||
let(:inbox) { channel.inbox }
|
||||
|
||||
context 'when unauthenticated' do
|
||||
it 'returns unauthorized' do
|
||||
post "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}/setup_channel_provider"
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when authenticated' do
|
||||
it 'returns unprocessable entity when channel does not support setup' do
|
||||
inbox = create(:inbox, account: account)
|
||||
|
||||
post "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}/setup_channel_provider",
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response.parsed_body['error']).to eq('Channel does not support setup')
|
||||
end
|
||||
|
||||
it 'calls setup_channel_provider when supported and returns ok' do
|
||||
service_double = instance_double(Whatsapp::Providers::WhatsappBaileysService, setup_channel_provider: true)
|
||||
allow(Whatsapp::Providers::WhatsappBaileysService).to receive(:new)
|
||||
.with(whatsapp_channel: channel)
|
||||
.and_return(service_double)
|
||||
|
||||
post "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}/setup_channel_provider",
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST /api/v1/accounts/:account_id/inboxes/:id/disconnect_channel_provider' do
|
||||
let(:channel) { create(:channel_whatsapp, account: account, provider: 'baileys', validate_provider_config: false) }
|
||||
let(:inbox) { channel.inbox }
|
||||
|
||||
context 'when unauthenticated' do
|
||||
it 'returns unauthorized' do
|
||||
post "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}/disconnect_channel_provider"
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when authenticated' do
|
||||
it 'returns unprocessable entity when channel does not support disconnect' do
|
||||
inbox = create(:inbox, account: account)
|
||||
|
||||
post "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}/disconnect_channel_provider",
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response.parsed_body['error']).to eq('Channel does not support disconnect')
|
||||
end
|
||||
|
||||
it 'calls disconnect_channel_provider when supported and returns ok' do
|
||||
service_double = instance_double(Whatsapp::Providers::WhatsappBaileysService, disconnect_channel_provider: true)
|
||||
allow(Whatsapp::Providers::WhatsappBaileysService).to receive(:new)
|
||||
.with(whatsapp_channel: channel)
|
||||
.and_return(service_double)
|
||||
|
||||
post "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}/disconnect_channel_provider",
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@ -44,12 +44,12 @@ RSpec.describe 'Super Admin accounts API', type: :request do
|
||||
expect(account.cache_keys.keys).to contain_exactly(:inbox, :label, :team)
|
||||
sign_in(super_admin, scope: :super_admin)
|
||||
|
||||
now_timestamp = Time.now.utc.to_i
|
||||
now_timestamp = (Time.now.utc.to_f * 1000).to_i
|
||||
post "/super_admin/accounts/#{account.id}/reset_cache"
|
||||
expect(response).to have_http_status(:redirect)
|
||||
expect(flash[:notice]).to eq('Cache keys cleared')
|
||||
|
||||
range = now_timestamp..(now_timestamp + 10)
|
||||
range = now_timestamp..(now_timestamp + 10_000)
|
||||
expect(account.reload.cache_keys.values.all? { |v| range.cover?(v.to_i) }).to be(true)
|
||||
end
|
||||
end
|
||||
|
||||
@ -75,7 +75,10 @@ FactoryBot.define do
|
||||
# since factory already has the required message templates, we just need to bypass it getting updated
|
||||
channel_whatsapp.define_singleton_method(:sync_templates) { nil } unless options.sync_templates
|
||||
channel_whatsapp.define_singleton_method(:validate_provider_config) { nil } unless options.validate_provider_config
|
||||
if channel_whatsapp.provider == 'whatsapp_cloud'
|
||||
if channel_whatsapp.provider == 'baileys'
|
||||
channel_whatsapp.provider_config = channel_whatsapp.provider_config.merge({ 'api_key' => 'test_key', 'provider_url' => 'https://baileys.api',
|
||||
'phone_number_id' => '123456789' })
|
||||
elsif channel_whatsapp.provider == 'whatsapp_cloud'
|
||||
channel_whatsapp.provider_config = channel_whatsapp.provider_config.merge({ 'api_key' => 'test_key', 'phone_number_id' => '123456789',
|
||||
'business_account_id' => '123456789' })
|
||||
end
|
||||
|
||||
@ -17,7 +17,7 @@ RSpec.describe CacheKeysHelper do
|
||||
it 'returns the zero epoch time if no value is cached' do
|
||||
result = helper.fetch_value_for_key(account_id, 'another-key')
|
||||
|
||||
expect(result).to eq('0000000000')
|
||||
expect(result).to eq('0000000000000')
|
||||
end
|
||||
|
||||
it 'returns a cached value if it exists' do
|
||||
|
||||
268
spec/services/whatsapp/incoming_message_baileys_service_spec.rb
Normal file
268
spec/services/whatsapp/incoming_message_baileys_service_spec.rb
Normal file
@ -0,0 +1,268 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe Whatsapp::IncomingMessageBaileysService do
|
||||
describe '#perform' do
|
||||
let(:webhook_verify_token) { 'valid_token' }
|
||||
let!(:whatsapp_channel) do
|
||||
create(:channel_whatsapp,
|
||||
provider: 'baileys',
|
||||
provider_config: { webhook_verify_token: webhook_verify_token },
|
||||
validate_provider_config: false)
|
||||
end
|
||||
let(:inbox) { whatsapp_channel.inbox }
|
||||
|
||||
context 'when webhook verify token is invalid' do
|
||||
it 'raises an InvalidWebhookVerifyToken error' do
|
||||
params = { webhookVerifyToken: 'invalid_token' }
|
||||
|
||||
expect do
|
||||
described_class.new(inbox: inbox, params: params).perform
|
||||
end.to raise_error(Whatsapp::IncomingMessageBaileysService::InvalidWebhookVerifyToken)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when event is blank' do
|
||||
it 'returns early and does nothing' do
|
||||
params = {
|
||||
webhookVerifyToken: webhook_verify_token,
|
||||
event: '',
|
||||
data: { connection: 'open' }
|
||||
}
|
||||
service = described_class.new(inbox: inbox, params: params)
|
||||
allow(service).to receive(:respond_to?)
|
||||
|
||||
service.perform
|
||||
|
||||
expect(service).not_to have_received(:respond_to?)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when data is blank' do
|
||||
it 'returns early and does nothing' do
|
||||
params = {
|
||||
webhookVerifyToken: webhook_verify_token,
|
||||
event: 'connection.update',
|
||||
data: {}
|
||||
}
|
||||
service = described_class.new(inbox: inbox, params: params)
|
||||
allow(service).to receive(:respond_to?)
|
||||
|
||||
service.perform
|
||||
|
||||
expect(service).not_to have_received(:respond_to?)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when event is unsupported' do
|
||||
it 'logs a warning message' do
|
||||
params = {
|
||||
webhookVerifyToken: webhook_verify_token,
|
||||
event: 'unsupported.event',
|
||||
data: 'some-data'
|
||||
}
|
||||
allow(Rails.logger).to receive(:warn).with('Baileys unsupported event: unsupported.event')
|
||||
|
||||
described_class.new(inbox: inbox, params: params).perform
|
||||
|
||||
expect(Rails.logger).to have_received(:warn)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when processing connection.update event' do
|
||||
let(:base_params) { { webhookVerifyToken: webhook_verify_token, event: 'connection.update' } }
|
||||
|
||||
it 'updates the channel provider_connection' do
|
||||
params = base_params.merge(
|
||||
{
|
||||
data: {
|
||||
connection: 'open',
|
||||
qrDataUrl: 'data:image/jpeg;base64,',
|
||||
error: 'wrong_phone_number'
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
described_class.new(inbox: inbox, params: params).perform
|
||||
|
||||
expect(inbox.channel.provider_connection).to include(
|
||||
'connection' => 'open',
|
||||
'qr_data_url' => 'data:image/jpeg;base64,',
|
||||
'error' => I18n.t('errors.inboxes.channel.provider_connection.wrong_phone_number')
|
||||
)
|
||||
end
|
||||
|
||||
it "logs an error message if there's an error" do
|
||||
params = base_params.merge(
|
||||
{
|
||||
data: { error: 'wrong_phone_number' }
|
||||
}
|
||||
)
|
||||
allow(Rails.logger).to receive(:error).with('Baileys connection error: wrong_phone_number')
|
||||
|
||||
described_class.new(inbox: inbox, params: params).perform
|
||||
|
||||
expect(Rails.logger).to have_received(:error)
|
||||
end
|
||||
|
||||
it "keeps connection value if it's not present in the data" do
|
||||
inbox.channel.update_provider_connection!(connection: 'connecting')
|
||||
params = base_params.merge(
|
||||
{
|
||||
data: { qrDataUrl: 'data:image/jpeg;base64,' }
|
||||
}
|
||||
)
|
||||
|
||||
described_class.new(inbox: inbox, params: params).perform
|
||||
|
||||
expect(inbox.channel.provider_connection['connection']).to eq('connecting')
|
||||
end
|
||||
|
||||
it "removes qr_data_url value if it's not present in the data" do
|
||||
inbox.channel.update_provider_connection!(qr_data_url: 'data:image/jpeg;base64,')
|
||||
params = base_params.merge(
|
||||
{
|
||||
data: { connection: 'open' }
|
||||
}
|
||||
)
|
||||
|
||||
described_class.new(inbox: inbox, params: params).perform
|
||||
|
||||
expect(inbox.channel.provider_connection['qr_data_url']).to be_nil
|
||||
end
|
||||
|
||||
it "removes error value if it's not present in the data" do
|
||||
inbox.channel.update_provider_connection!(error: 'wrong_phone_number')
|
||||
params = base_params.merge(
|
||||
{
|
||||
data: { connection: 'open' }
|
||||
}
|
||||
)
|
||||
|
||||
described_class.new(inbox: inbox, params: params).perform
|
||||
|
||||
expect(inbox.channel.provider_connection['error']).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when processing messages.upsert event' do
|
||||
context 'when message type is unsupported' do
|
||||
let(:raw_message) do
|
||||
{
|
||||
key: { id: 'msg_123', remoteJid: '5511912345678@s.whatsapp.net', fromMe: false },
|
||||
message: { unsupported: 'message' },
|
||||
pushName: 'John Doe'
|
||||
}
|
||||
end
|
||||
let(:params) do
|
||||
{
|
||||
webhookVerifyToken: webhook_verify_token,
|
||||
event: 'messages.upsert',
|
||||
data: {
|
||||
type: 'notify',
|
||||
messages: [raw_message]
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
it 'logs a warning message' do
|
||||
allow(Rails.logger).to receive(:warn).with('Baileys unsupported message type: unsupported')
|
||||
|
||||
described_class.new(inbox: inbox, params: params).perform
|
||||
|
||||
expect(Rails.logger).to have_received(:warn)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when message type is text' do
|
||||
let(:raw_message) do
|
||||
{
|
||||
key: { id: 'msg_123', remoteJid: '5511912345678@s.whatsapp.net', fromMe: false },
|
||||
message: { conversation: 'Hello from Baileys' },
|
||||
pushName: 'John Doe'
|
||||
}
|
||||
end
|
||||
let(:params) do
|
||||
{
|
||||
webhookVerifyToken: webhook_verify_token,
|
||||
event: 'messages.upsert',
|
||||
data: {
|
||||
type: 'notify',
|
||||
messages: [raw_message]
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
it 'creates an incoming message' do
|
||||
described_class.new(inbox: inbox, params: params).perform
|
||||
|
||||
conversation = inbox.conversations.last
|
||||
message = conversation.messages.last
|
||||
expect(message).to be_present
|
||||
expect(message.content).to eq('Hello from Baileys')
|
||||
expect(message.message_type).to eq('incoming')
|
||||
expect(message.sender).to be_present
|
||||
expect(message.sender.name).to eq('John Doe')
|
||||
end
|
||||
|
||||
it 'creates an outgoing message' do
|
||||
raw_message_outgoing = raw_message.merge(
|
||||
key: { id: 'msg_123', remoteJid: '5511912345678@s.whatsapp.net', fromMe: true }
|
||||
)
|
||||
params_outgoing = params.merge(data: { type: 'notify', messages: [raw_message_outgoing] })
|
||||
create(:account_user, account: inbox.account)
|
||||
|
||||
described_class.new(inbox: inbox, params: params_outgoing).perform
|
||||
|
||||
conversation = inbox.conversations.last
|
||||
message = conversation.messages.last
|
||||
expect(message).to be_present
|
||||
expect(message.content).to eq('Hello from Baileys')
|
||||
expect(message.message_type).to eq('outgoing')
|
||||
end
|
||||
|
||||
it 'creates a message on an existing conversation' do
|
||||
contact = create(:contact, account: inbox.account, name: 'John Doe')
|
||||
contact_inbox = create(:contact_inbox, inbox: inbox, contact: contact, source_id: '5511912345678')
|
||||
existing_conversation = create(:conversation, inbox: inbox, contact_inbox: contact_inbox)
|
||||
|
||||
described_class.new(inbox: inbox, params: params).perform
|
||||
|
||||
message = existing_conversation.messages.last
|
||||
expect(message.sender).to eq(contact)
|
||||
end
|
||||
|
||||
it 'does not create a message if it already exists' do
|
||||
message = create(:message, inbox: inbox, source_id: 'msg_123')
|
||||
|
||||
described_class.new(inbox: inbox, params: params).perform
|
||||
|
||||
conversation = inbox.conversations.last
|
||||
messages = conversation.messages
|
||||
expect(messages).to eq([message])
|
||||
end
|
||||
|
||||
it 'does not create a message if it is already being processed' do
|
||||
allow(Redis::Alfred).to receive(:get).with(format_message_source_key('msg_123')).and_return(true)
|
||||
|
||||
described_class.new(inbox: inbox, params: params).perform
|
||||
|
||||
expect(inbox.conversations).to be_empty
|
||||
end
|
||||
|
||||
it 'caches the message source id in Redis and clears it' do
|
||||
allow(Redis::Alfred).to receive(:setex).with(format_message_source_key('msg_123'), true)
|
||||
allow(Redis::Alfred).to receive(:delete).with(format_message_source_key('msg_123'))
|
||||
|
||||
described_class.new(inbox: inbox, params: params).perform
|
||||
|
||||
expect(Redis::Alfred).to have_received(:setex)
|
||||
expect(Redis::Alfred).to have_received(:delete)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def format_message_source_key(message_id)
|
||||
format(Redis::RedisKeys::MESSAGE_SOURCE_KEY, id: message_id)
|
||||
end
|
||||
end
|
||||
@ -0,0 +1,232 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe Whatsapp::Providers::WhatsappBaileysService do
|
||||
subject(:service) { described_class.new(whatsapp_channel: whatsapp_channel) }
|
||||
|
||||
let(:whatsapp_channel) { create(:channel_whatsapp, provider: 'baileys', validate_provider_config: false) }
|
||||
let(:message) { create(:message) }
|
||||
|
||||
let(:test_send_phone_number) { '+5511987654321' }
|
||||
|
||||
before do
|
||||
stub_const('Whatsapp::Providers::WhatsappBaileysService::DEFAULT_CLIENT_NAME', 'chatwoot-test')
|
||||
end
|
||||
|
||||
describe '#setup_channel_provider' do
|
||||
context 'when response is successful' do
|
||||
it 'calls the connection endpoint' do
|
||||
stub_request(:post, "#{whatsapp_channel.provider_config['provider_url']}/connections/#{whatsapp_channel.phone_number}")
|
||||
.with(
|
||||
headers: stub_headers(whatsapp_channel),
|
||||
body: {
|
||||
clientName: 'chatwoot-test',
|
||||
webhookUrl: whatsapp_channel.inbox.callback_webhook_url,
|
||||
webhookVerifyToken: whatsapp_channel.provider_config['webhook_verify_token']
|
||||
}.to_json
|
||||
)
|
||||
.to_return(status: 200)
|
||||
|
||||
response = service.setup_channel_provider
|
||||
|
||||
expect(response).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'when response is unsuccessful' do
|
||||
it 'logs the error and returns false' do
|
||||
stub_request(:post, "#{whatsapp_channel.provider_config['provider_url']}/connections/#{whatsapp_channel.phone_number}")
|
||||
.with(
|
||||
headers: stub_headers(whatsapp_channel),
|
||||
body: {
|
||||
clientName: 'chatwoot-test',
|
||||
webhookUrl: whatsapp_channel.inbox.callback_webhook_url,
|
||||
webhookVerifyToken: whatsapp_channel.provider_config['webhook_verify_token']
|
||||
}.to_json
|
||||
)
|
||||
.to_return(
|
||||
status: 400,
|
||||
body: 'error message',
|
||||
headers: {}
|
||||
)
|
||||
allow(Rails.logger).to receive(:error).with('error message')
|
||||
|
||||
response = service.setup_channel_provider
|
||||
|
||||
expect(response).to be(false)
|
||||
expect(Rails.logger).to have_received(:error)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#disconnect_channel_provider' do
|
||||
context 'when response is successful' do
|
||||
it 'disconnects the whatsapp connection' do
|
||||
stub_request(:delete, "#{whatsapp_channel.provider_config['provider_url']}/connections/#{whatsapp_channel.phone_number}")
|
||||
.with(headers: stub_headers(whatsapp_channel))
|
||||
.to_return(status: 200)
|
||||
|
||||
response = service.disconnect_channel_provider
|
||||
|
||||
expect(response).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'when response is unsuccessful' do
|
||||
it 'logs the error and returns false' do
|
||||
stub_request(:delete, "#{whatsapp_channel.provider_config['provider_url']}/connections/#{whatsapp_channel.phone_number}")
|
||||
.with(headers: stub_headers(whatsapp_channel))
|
||||
.to_return(
|
||||
status: 400,
|
||||
body: 'error message',
|
||||
headers: {}
|
||||
)
|
||||
allow(Rails.logger).to receive(:error).with('error message')
|
||||
|
||||
response = service.disconnect_channel_provider
|
||||
|
||||
expect(response).to be(false)
|
||||
expect(Rails.logger).to have_received(:error)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#send_message' do
|
||||
context 'when response is successful' do
|
||||
it 'returns true' do
|
||||
stub_request(:post, "#{whatsapp_channel.provider_config['provider_url']}/connections/#{whatsapp_channel.phone_number}/send-message")
|
||||
.with(
|
||||
headers: stub_headers(whatsapp_channel),
|
||||
body: {
|
||||
type: 'text',
|
||||
recipient: test_send_phone_number,
|
||||
message: message.content
|
||||
}.to_json
|
||||
)
|
||||
.to_return(
|
||||
status: 200,
|
||||
headers: { 'Content-Type' => 'application/json' },
|
||||
body: { 'data' => { 'key' => { 'id' => message.id } } }.to_json
|
||||
)
|
||||
|
||||
result = service.send_message(test_send_phone_number, message)
|
||||
|
||||
expect(result).to be message.id
|
||||
end
|
||||
end
|
||||
|
||||
context 'when response is unsuccessful' do
|
||||
it 'logs the error and returns false' do
|
||||
with_modified_env BAILEYS_PROVIDER_DEFAULT_URL: 'http://test.com' do
|
||||
stub_request(:post, "#{whatsapp_channel.provider_config['provider_url']}/connections/#{whatsapp_channel.phone_number}/send-message")
|
||||
.with(
|
||||
headers: stub_headers(whatsapp_channel),
|
||||
body: {
|
||||
type: 'text',
|
||||
recipient: test_send_phone_number,
|
||||
message: message.content
|
||||
}.to_json
|
||||
)
|
||||
.to_return(
|
||||
status: 400,
|
||||
body: 'error message',
|
||||
headers: {}
|
||||
)
|
||||
allow(Rails.logger).to receive(:error).with('error message')
|
||||
|
||||
result = service.send_message(test_send_phone_number, message)
|
||||
|
||||
expect(result).to be_nil
|
||||
expect(Rails.logger).to have_received(:error)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when message content type is not supported' do
|
||||
it 'raises an error' do
|
||||
message.update!(content_type: 'sticker')
|
||||
|
||||
expect do
|
||||
service.send_message(test_send_phone_number, message)
|
||||
end.to raise_error(Whatsapp::Providers::WhatsappBaileysService::MessageContentTypeNotSupported)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#api_headers' do
|
||||
context 'when called' do
|
||||
it 'returns the headers' do
|
||||
expect(service.api_headers).to eq('x-api-key' => 'test_key', 'Content-Type' => 'application/json')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#validate_provider_config?' do
|
||||
context 'when response is successful' do
|
||||
it 'returns true' do
|
||||
stub_request(:get, "#{whatsapp_channel.provider_config['provider_url']}/status")
|
||||
.with(headers: { 'Content-Type' => 'application/json', 'x-api-key' => whatsapp_channel.provider_config['api_key'] })
|
||||
.to_return(status: 200, body: '', headers: {})
|
||||
|
||||
expect(service.validate_provider_config?).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'when response is unsuccessful' do
|
||||
it 'logs the error and returns false' do
|
||||
stub_request(:get, "#{whatsapp_channel.provider_config['provider_url']}/status")
|
||||
.with(headers: { 'Content-Type' => 'application/json', 'x-api-key' => whatsapp_channel.provider_config['api_key'] })
|
||||
.to_return(status: 400, body: 'error message', headers: {})
|
||||
allow(Rails.logger).to receive(:error).with('error message')
|
||||
|
||||
expect(service.validate_provider_config?).to be false
|
||||
expect(Rails.logger).to have_received(:error)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when provider responds with 5XX' do
|
||||
it 'updated provider connection to close' do
|
||||
whatsapp_channel.update!(provider_connection: { 'connection' => 'open' })
|
||||
allow(HTTParty).to receive(:post).with(
|
||||
"#{whatsapp_channel.provider_config['provider_url']}/connections/#{whatsapp_channel.phone_number}/send-message",
|
||||
headers: stub_headers(whatsapp_channel),
|
||||
body: {
|
||||
type: 'text',
|
||||
recipient: test_send_phone_number,
|
||||
message: message.content
|
||||
}.to_json
|
||||
).and_raise(HTTParty::ResponseError.new(OpenStruct.new(status_code: 500)))
|
||||
|
||||
expect do
|
||||
service.send_message(test_send_phone_number, message)
|
||||
end.to raise_error(HTTParty::ResponseError)
|
||||
|
||||
expect(whatsapp_channel.provider_connection['connection']).to eq('close')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when environment variable BAILEYS_PROVIDER_DEFAULT_URL is set' do
|
||||
it 'uses the base url from the environment variable' do
|
||||
stub_const('Whatsapp::Providers::WhatsappBaileysService::DEFAULT_URL', 'http://test.com')
|
||||
whatsapp_channel.update!(provider_config: {})
|
||||
|
||||
expect(service.send(:provider_url)).to eq('http://test.com')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when environment variable BAILEYS_PROVIDER_DEFAULT_API_KEY is set' do
|
||||
it 'uses the API key from the environment variable' do
|
||||
stub_const('Whatsapp::Providers::WhatsappBaileysService::DEFAULT_API_KEY', 'key')
|
||||
whatsapp_channel.update!(provider_config: {})
|
||||
|
||||
expect(service.send(:api_key)).to eq('key')
|
||||
end
|
||||
end
|
||||
|
||||
def stub_headers(channel)
|
||||
{
|
||||
'Content-Type' => 'application/json',
|
||||
'x-api-key' => channel.provider_config['api_key']
|
||||
}
|
||||
end
|
||||
end
|
||||
Loading…
Reference in New Issue
Block a user