Skip to content

Commit

Permalink
feat: Baileys-like option for setting up a WhatsApp inbox (#2)
Browse files Browse the repository at this point in the history
* draft: scaffold evo channel as api channel
* feat: add evolution to whatsapp channel
  • Loading branch information
milesibastos committed Aug 21, 2024
1 parent d349a2a commit 59fd3d9
Show file tree
Hide file tree
Showing 10 changed files with 304 additions and 2 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
class Api::V1::Accounts::Channels::EvolutionChannelsController < Api::V1::Accounts::BaseController
include Api::V1::InboxesHelper
before_action :authorize_request
before_action :set_user

def create
ActiveRecord::Base.transaction do
channel = create_channel
@inbox = Current.account.inboxes.build(
{
name: inbox_name(channel),
channel: channel
}.merge(
permitted_params.except(:channel)
)
)

params = permitted_params(channel_type_from_params::EDITABLE_ATTRS)[:channel].except(:type)
Evolution::ManagerService.new.create(@inbox.account_id, permitted_params[:name], params[:webhook_url],
params[:api_key], @user.access_token.token)
@inbox.save!
end

render json: @inbox, status: :created
rescue StandardError => e
render json: { error: e.message }, status: :unprocessable_entity
end

private

def authorize_request
authorize ::Inbox
end

def set_user
@user = current_user
end

def create_channel
return unless %w[api whatsapp].include?(permitted_params[:channel][:type])

params = permitted_params(channel_type_from_params::EDITABLE_ATTRS)[:channel].except(:type, :api_key)
params[:webhook_url] = "#{params[:webhook_url]}/chatwoot/webhook/#{permitted_params[:name]}"
account_channels_method.create!(params)
end

def inbox_attributes
[:name]
end

def permitted_params(channel_attributes = [])
# We will remove this line after fixing https://linear.app/chatwoot/issue/CW-1567/null-value-passed-as-null-string-to-backend
params.each { |k, v| params[k] = params[k] == 'null' ? nil : v }

params.permit(
*inbox_attributes,
channel: [:type, :api_key, *channel_attributes]
)
end

def channel_type_from_params
{
'api' => Channel::Api,
'whatsapp' => Channel::Whatsapp
}[permitted_params[:channel][:type]]
end
end
9 changes: 9 additions & 0 deletions app/javascript/dashboard/api/channel/evolutionChannel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import ApiClient from '../ApiClient';

class EvolutionChannel extends ApiClient {
constructor() {
super('channels/evolution_channel', { accountScoped: true });
}
}

export default new EvolutionChannel();
20 changes: 19 additions & 1 deletion app/javascript/dashboard/i18n/locale/en/inboxMgmt.json
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,8 @@
"LABEL": "API Provider",
"TWILIO": "Twilio",
"WHATSAPP_CLOUD": "WhatsApp Cloud",
"360_DIALOG": "360Dialog"
"360_DIALOG": "360Dialog",
"EVOLUTION": "Evolution API"
},
"INBOX_NAME": {
"LABEL": "Inbox Name",
Expand Down Expand Up @@ -279,6 +280,23 @@
"ERROR_MESSAGE": "We were not able to save the api channel"
}
},
"EVOLUTION": {
"WEBHOOK_URL": {
"LABEL": "Evolution API URL",
"SUBTITLE": "Configure the URL where you want to receive callbacks on events.",
"PLACEHOLDER": "https://evolution.startup.chatwoot.app.br",
"ERROR": "Please enter a valid value."
},
"API_KEY": {
"LABEL": "API key",
"SUBTITLE": "Configure the Evolution API key.",
"PLACEHOLDER": "API key",
"ERROR": "Please enter a valid value."
},
"API": {
"ERROR_MESSAGE": "We were not able to save the api channel"
}
},
"EMAIL_CHANNEL": {
"TITLE": "Email Channel",
"DESC": "Integrate your email inbox.",
Expand Down
20 changes: 19 additions & 1 deletion app/javascript/dashboard/i18n/locale/pt_BR/inboxMgmt.json
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,8 @@
"LABEL": "Provedor de API",
"TWILIO": "Twilio",
"WHATSAPP_CLOUD": "Cloud do WhatsApp",
"360_DIALOG": "360Dialog"
"360_DIALOG": "360Dialog",
"EVOLUTION": "Evolution API"
},
"INBOX_NAME": {
"LABEL": "Nome da Caixa de Entrada",
Expand Down Expand Up @@ -279,6 +280,23 @@
"ERROR_MESSAGE": "Não foi possível salvar o canal de API"
}
},
"EVOLUTION": {
"WEBHOOK_URL": {
"LABEL": "URL do Webhook",
"SUBTITLE": "Configure a URL onde você deseja receber retornos de chamada em eventos.",
"PLACEHOLDER": "URL do Webhook",
"ERROR": "Por favor, insira um valor válido."
},
"API_KEY": {
"LABEL": "Chave da API",
"SUBTITLE": "Configure a chave API da Evolution.",
"PLACEHOLDER": "Chave da API",
"ERROR": "Por favor, insira um valor válido."
},
"API": {
"ERROR_MESSAGE": "Não foi possível salvar o canal de API"
}
},
"EMAIL_CHANNEL": {
"TITLE": "Canal de e-mail",
"DESC": "Integre sua caixa de entrada de e-mail.",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
<template>
<form class="mx-0 flex flex-wrap" @submit.prevent="createChannel()">
<div class="w-[65%] flex-shrink-0 flex-grow-0 max-w-[65%]">
<label :class="{ error: $v.channelName.$error }">
{{ $t('INBOX_MGMT.ADD.WHATSAPP.INBOX_NAME.LABEL') }}
<input
v-model.trim="channelName"
type="text"
:placeholder="$t('INBOX_MGMT.ADD.WHATSAPP.INBOX_NAME.PLACEHOLDER')"
@blur="$v.channelName.$touch"
/>
<span v-if="$v.channelName.$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.webhookUrl.$error }">
{{ $t('INBOX_MGMT.ADD.EVOLUTION.WEBHOOK_URL.LABEL') }}
<input
v-model.trim="webhookUrl"
type="text"
:placeholder="$t('INBOX_MGMT.ADD.EVOLUTION.WEBHOOK_URL.PLACEHOLDER')"
@blur="$v.webhookUrl.$touch"
/>
<span v-if="$v.webhookUrl.$error" class="message">{{
$t('INBOX_MGMT.ADD.EVOLUTION.WEBHOOK_URL.ERROR')
}}</span>
</label>
<p class="help-text">
{{ $t('INBOX_MGMT.ADD.EVOLUTION.WEBHOOK_URL.SUBTITLE') }}
</p>
</div>

<div class="w-[65%] flex-shrink-0 flex-grow-0 max-w-[65%]">
<label :class="{ error: $v.apiKey.$error }">
{{ $t('INBOX_MGMT.ADD.EVOLUTION.API_KEY.LABEL') }}
<input
v-model.trim="apiKey"
type="text"
:placeholder="$t('INBOX_MGMT.ADD.EVOLUTION.API_KEY.PLACEHOLDER')"
@blur="$v.apiKey.$touch"
/>
<span v-if="$v.apiKey.$error" class="message">{{
$t('INBOX_MGMT.ADD.WHATSAPP.API_KEY.ERROR')
}}</span>
</label>
<p class="help-text">
{{ $t('INBOX_MGMT.ADD.EVOLUTION.API_KEY.SUBTITLE') }}
</p>
</div>

<div class="w-full">
<woot-submit-button
:loading="uiFlags.isCreating"
:button-text="$t('INBOX_MGMT.ADD.WHATSAPP.SUBMIT_BUTTON')"
/>
</div>
</form>
</template>

<script>
import { mapGetters } from 'vuex';
import alertMixin from 'shared/mixins/alertMixin';
import { required } from 'vuelidate/lib/validators';
import router from '../../../../index';
const shouldBeWebhookUrl = (value = '') =>
value ? value.startsWith('http') : true;
const shouldContainOnlyValidChars = (value = '') =>
value ? /^[a-zA-Z0-9_]+$/.test(value) : true;
export default {
mixins: [alertMixin],
data() {
return {
channelName: '',
webhookUrl: '',
apiKey: '',
};
},
computed: {
...mapGetters({
uiFlags: 'inboxes/getUIFlags',
}),
},
validations: {
channelName: { shouldContainOnlyValidChars },
webhookUrl: { shouldBeWebhookUrl },
apiKey: { required },
},
methods: {
async createChannel() {
this.$v.$touch();
if (this.$v.$invalid) {
return;
}
try {
const apiChannel = await this.$store.dispatch(
'inboxes/createEvolutionChannel',
{
name: this.channelName,
channel: {
type: 'api',
webhook_url: this.webhookUrl,
api_key: this.apiKey,
},
});
router.replace({
name: 'settings_inboxes_add_agents',
params: {
page: 'new',
inbox_id: apiChannel.id,
},
});
} catch (error) {
this.showAlert(this.$t('INBOX_MGMT.ADD.WHATSAPP.API.ERROR_MESSAGE'));
}
},
},
};
</script>
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ import PageHeader from '../../SettingsSubPageHeader.vue';
import Twilio from './Twilio.vue';
import ThreeSixtyDialogWhatsapp from './360DialogWhatsapp.vue';
import CloudWhatsapp from './CloudWhatsapp.vue';
import Evolution from './Evolution.vue';
export default {
components: {
PageHeader,
Twilio,
ThreeSixtyDialogWhatsapp,
CloudWhatsapp,
Evolution,
},
data() {
return {
Expand Down Expand Up @@ -37,12 +39,16 @@ export default {
<option value="twilio">
{{ $t('INBOX_MGMT.ADD.WHATSAPP.PROVIDERS.TWILIO') }}
</option>
<option value="evolution">
{{ $t('INBOX_MGMT.ADD.WHATSAPP.PROVIDERS.EVOLUTION') }}
</option>
</select>
</label>
</div>

<Twilio v-if="provider === 'twilio'" type="whatsapp" />
<ThreeSixtyDialogWhatsapp v-else-if="provider === '360dialog'" />
<Evolution v-else-if="provider === 'evolution'" />
<CloudWhatsapp v-else />
</div>
</template>
14 changes: 14 additions & 0 deletions app/javascript/dashboard/store/modules/inboxes.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import InboxesAPI from '../../api/inboxes';
import WebChannel from '../../api/channel/webChannel';
import FBChannel from '../../api/channel/fbChannel';
import TwilioChannel from '../../api/channel/twilioChannel';
import EvolutionChannel from '../../api/channel/evolutionChannel';
import { throwErrorMessage } from '../utils/api';
import AnalyticsHelper from '../../helper/AnalyticsHelper';
import { ACCOUNT_EVENTS } from '../../helper/AnalyticsHelper/events';
Expand Down Expand Up @@ -173,6 +174,19 @@ export const actions = {
return throwErrorMessage(error);
}
},
createEvolutionChannel: async ({ commit }, params) => {
try {
commit(types.default.SET_INBOXES_UI_FLAG, { isCreating: true });
const response = await EvolutionChannel.create(params);
commit(types.default.ADD_INBOXES, response.data);
commit(types.default.SET_INBOXES_UI_FLAG, { isCreating: false });
sendAnalyticsEvent('evolution');
return response.data;
} catch (error) {
commit(types.default.SET_INBOXES_UI_FLAG, { isCreating: false });
throw new Error(error);
}
},
createTwilioChannel: async ({ commit }, params) => {
try {
commit(types.default.SET_INBOXES_UI_FLAG, { isCreating: true });
Expand Down
41 changes: 41 additions & 0 deletions app/services/evolution/manager_service.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
class Evolution::ManagerService
def api_headers(api_key)
{ 'apikey' => api_key, 'Content-Type' => 'application/json' }
end

def create(account_id, name, webhook_url, api_key, access_token)
response = HTTParty.post(
"#{webhook_url}/instance/create",
headers: api_headers(api_key),
body: {
instanceName: name,
token: SecureRandom.uuid,
qrcode: true,
# number: phone_number,
auto_create: false,
chatwoot_name_inbox: name,
chatwoot_account_id: account_id,
chatwoot_token: access_token,
chatwoot_url: ENV.fetch('FRONTEND_URL', 'http://localhost:3000'),
chatwoot_sign_msg: true,
chatwoot_reopen_conversation: true,
chatwoot_conversation_pending: false,
chatwoot_import_messages: false,
chatwoot_import_contacts: false,
chatwoot_merge_brazil_contacts: true,
chatwoot_days_limit_import_messages: 0
}.to_json
)

process_response(response)
end

def process_response(response)
if response.success?
Rails.logger.info("HTTP Request Successful: #{response}")
else
Rails.logger.error response.body
nil
end
end
end
2 changes: 2 additions & 0 deletions config/environments/development.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
Rails.application.configure do
# Settings specified here will take precedence over those in config/application.rb.

config.web_console.whitelisted_ips = '192.168.0.0/16'

# In the development environment your application's code is reloaded on
# every request. This slows down response time but is perfect for development
# since you don't have to restart the web server when you make code changes.
Expand Down
1 change: 1 addition & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
resources :dashboard_apps, only: [:index, :show, :create, :update, :destroy]
namespace :channels do
resource :twilio_channel, only: [:create]
resource :evolution_channel, only: [:create]
end
resources :conversations, only: [:index, :create, :show, :update] do
collection do
Expand Down

0 comments on commit 59fd3d9

Please sign in to comment.