Feature: API Channel (#1052)

This commit is contained in:
Sojan Jose 2020-07-21 12:15:24 +05:30 committed by GitHub
parent fa04098c20
commit 8079bf50a0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
40 changed files with 735 additions and 246 deletions

View file

@ -0,0 +1,139 @@
# This class creates both outgoing messages from chatwoot and echo outgoing messages based on the flag `outgoing_echo`
# Assumptions
# 1. Incase of an outgoing message which is echo, source_id will NOT be nil,
# based on this we are showing "not sent from chatwoot" message in frontend
# Hence there is no need to set user_id in message for outgoing echo messages.
class Messages::Facebook::MessageBuilder
attr_reader :response
def initialize(response, inbox, outgoing_echo = false)
@response = response
@inbox = inbox
@sender_id = (outgoing_echo ? @response.recipient_id : @response.sender_id)
@message_type = (outgoing_echo ? :outgoing : :incoming)
end
def perform
ActiveRecord::Base.transaction do
build_contact
build_message
end
rescue StandardError => e
Raven.capture_exception(e)
true
end
private
def contact
@contact ||= @inbox.contact_inboxes.find_by(source_id: @sender_id)&.contact
end
def build_contact
return if contact.present?
@contact = Contact.create!(contact_params.except(:remote_avatar_url))
ContactAvatarJob.perform_later(@contact, contact_params[:remote_avatar_url]) if contact_params[:remote_avatar_url]
@contact_inbox = ContactInbox.create(contact: contact, inbox: @inbox, source_id: @sender_id)
end
def build_message
@message = conversation.messages.create!(message_params)
(response.attachments || []).each do |attachment|
attachment_obj = @message.attachments.new(attachment_params(attachment).except(:remote_file_url))
attachment_obj.save!
attach_file(attachment_obj, attachment_params(attachment)[:remote_file_url]) if attachment_params(attachment)[:remote_file_url]
end
end
def attach_file(attachment, file_url)
file_resource = LocalResource.new(file_url)
attachment.file.attach(io: file_resource.file, filename: file_resource.tmp_filename, content_type: file_resource.encoding)
end
def conversation
@conversation ||= Conversation.find_by(conversation_params) || build_conversation
end
def build_conversation
@contact_inbox ||= contact.contact_inboxes.find_by!(source_id: @sender_id)
Conversation.create!(conversation_params.merge(
contact_inbox_id: @contact_inbox.id
))
end
def attachment_params(attachment)
file_type = attachment['type'].to_sym
params = { file_type: file_type, account_id: @message.account_id }
if [:image, :file, :audio, :video].include? file_type
params.merge!(file_type_params(attachment))
elsif file_type == :location
params.merge!(location_params(attachment))
elsif file_type == :fallback
params.merge!(fallback_params(attachment))
end
params
end
def file_type_params(attachment)
{
external_url: attachment['payload']['url'],
remote_file_url: attachment['payload']['url']
}
end
def location_params(attachment)
lat = attachment['payload']['coordinates']['lat']
long = attachment['payload']['coordinates']['long']
{
external_url: attachment['url'],
coordinates_lat: lat,
coordinates_long: long,
fallback_title: attachment['title']
}
end
def fallback_params(attachment)
{
fallback_title: attachment['title'],
external_url: attachment['url']
}
end
def conversation_params
{
account_id: @inbox.account_id,
inbox_id: @inbox.id,
contact_id: contact.id
}
end
def message_params
{
account_id: conversation.account_id,
inbox_id: conversation.inbox_id,
message_type: @message_type,
content: response.content,
source_id: response.identifier,
sender: contact
}
end
def contact_params
begin
k = Koala::Facebook::API.new(@inbox.channel.page_access_token) if @inbox.facebook?
result = k.get_object(@sender_id) || {}
rescue StandardError => e
result = {}
Raven.capture_exception(e)
end
{
name: "#{result['first_name'] || 'John'} #{result['last_name'] || 'Doe'}",
account_id: @inbox.account_id,
remote_avatar_url: result['profile_pic'] || ''
}
end
end

View file

@ -1,2 +0,0 @@
class Messages::IncomingMessageBuilder < Messages::MessageBuilder
end

View file

@ -1,139 +1,57 @@
# This class creates both outgoing messages from chatwoot and echo outgoing messages based on the flag `outgoing_echo`
# Assumptions
# 1. Incase of an outgoing message which is echo, source_id will NOT be nil,
# based on this we are showing "not sent from chatwoot" message in frontend
# Hence there is no need to set user_id in message for outgoing echo messages.
class Messages::MessageBuilder
attr_reader :response
include ::FileTypeHelper
attr_reader :message
def initialize(response, inbox, outgoing_echo = false)
@response = response
@inbox = inbox
@sender_id = (outgoing_echo ? @response.recipient_id : @response.sender_id)
@message_type = (outgoing_echo ? :outgoing : :incoming)
def initialize(user, conversation, params)
@content = params[:content]
@private = params[:private] || false
@conversation = conversation
@user = user
@message_type = params[:message_type] || 'outgoing'
@content_type = params[:content_type]
@items = params.to_unsafe_h&.dig(:content_attributes, :items)
@attachments = params[:attachments]
end
def perform
ActiveRecord::Base.transaction do
build_contact
build_message
@message = @conversation.messages.build(message_params)
if @attachments.present?
@attachments.each do |uploaded_attachment|
attachment = @message.attachments.new(
account_id: @message.account_id,
file_type: file_type(uploaded_attachment&.content_type)
)
attachment.file.attach(uploaded_attachment)
end
end
rescue StandardError => e
Raven.capture_exception(e)
true
@message.save
@message
end
private
def contact
@contact ||= @inbox.contact_inboxes.find_by(source_id: @sender_id)&.contact
end
def build_contact
return if contact.present?
@contact = Contact.create!(contact_params.except(:remote_avatar_url))
ContactAvatarJob.perform_later(@contact, contact_params[:remote_avatar_url]) if contact_params[:remote_avatar_url]
@contact_inbox = ContactInbox.create(contact: contact, inbox: @inbox, source_id: @sender_id)
end
def build_message
@message = conversation.messages.create!(message_params)
(response.attachments || []).each do |attachment|
attachment_obj = @message.attachments.new(attachment_params(attachment).except(:remote_file_url))
attachment_obj.save!
attach_file(attachment_obj, attachment_params(attachment)[:remote_file_url]) if attachment_params(attachment)[:remote_file_url]
end
end
def attach_file(attachment, file_url)
file_resource = LocalResource.new(file_url)
attachment.file.attach(io: file_resource.file, filename: file_resource.tmp_filename, content_type: file_resource.encoding)
end
def conversation
@conversation ||= Conversation.find_by(conversation_params) || build_conversation
end
def build_conversation
@contact_inbox ||= contact.contact_inboxes.find_by!(source_id: @sender_id)
Conversation.create!(conversation_params.merge(
contact_inbox_id: @contact_inbox.id
))
end
def attachment_params(attachment)
file_type = attachment['type'].to_sym
params = { file_type: file_type, account_id: @message.account_id }
if [:image, :file, :audio, :video].include? file_type
params.merge!(file_type_params(attachment))
elsif file_type == :location
params.merge!(location_params(attachment))
elsif file_type == :fallback
params.merge!(fallback_params(attachment))
def message_type
if @conversation.inbox.channel.class != Channel::Api && @message_type == 'incoming'
raise StandardError, 'Incoming messages are only allowed in Api inboxes'
end
params
@message_type
end
def file_type_params(attachment)
{
external_url: attachment['payload']['url'],
remote_file_url: attachment['payload']['url']
}
end
def location_params(attachment)
lat = attachment['payload']['coordinates']['lat']
long = attachment['payload']['coordinates']['long']
{
external_url: attachment['url'],
coordinates_lat: lat,
coordinates_long: long,
fallback_title: attachment['title']
}
end
def fallback_params(attachment)
{
fallback_title: attachment['title'],
external_url: attachment['url']
}
end
def conversation_params
{
account_id: @inbox.account_id,
inbox_id: @inbox.id,
contact_id: contact.id
}
def sender
message_type == 'outgoing' ? @user : @conversation.contact
end
def message_params
{
account_id: conversation.account_id,
inbox_id: conversation.inbox_id,
message_type: @message_type,
content: response.content,
source_id: response.identifier,
sender: contact
}
end
def contact_params
begin
k = Koala::Facebook::API.new(@inbox.channel.page_access_token) if @inbox.facebook?
result = k.get_object(@sender_id) || {}
rescue Exception => e
result = {}
Raven.capture_exception(e)
end
{
name: "#{result['first_name'] || 'John'} #{result['last_name'] || 'Doe'}",
account_id: @inbox.account_id,
remote_avatar_url: result['profile_pic'] || ''
account_id: @conversation.account_id,
inbox_id: @conversation.inbox_id,
message_type: message_type,
content: @content,
private: @private,
sender: sender,
content_type: @content_type,
items: @items
}
end
end

View file

@ -1,2 +0,0 @@
class Messages::Outgoing::EchoBuilder < ::Messages::MessageBuilder
end

View file

@ -1,46 +0,0 @@
class Messages::Outgoing::NormalBuilder
include ::FileTypeHelper
attr_reader :message
def initialize(user, conversation, params)
@content = params[:content]
@private = params[:private] || false
@conversation = conversation
@user = user
@fb_id = params[:fb_id]
@content_type = params[:content_type]
@items = params.to_unsafe_h&.dig(:content_attributes, :items)
@attachments = params[:attachments]
end
def perform
@message = @conversation.messages.build(message_params)
if @attachments.present?
@attachments.each do |uploaded_attachment|
attachment = @message.attachments.new(
account_id: @message.account_id,
file_type: file_type(uploaded_attachment&.content_type)
)
attachment.file.attach(uploaded_attachment)
end
end
@message.save
@message
end
private
def message_params
{
account_id: @conversation.account_id,
inbox_id: @conversation.inbox_id,
message_type: :outgoing,
content: @content,
private: @private,
sender: @user,
source_id: @fb_id,
content_type: @content_type,
items: @items
}
end
end

View file

@ -11,9 +11,11 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
def show; end
def create
@contact = Current.account.contacts.new(contact_create_params)
@contact.save!
render json: @contact
ActiveRecord::Base.transaction do
@contact = Current.account.contacts.new(contact_create_params)
@contact.save!
@contact_inbox = build_contact_inbox
end
end
def update
@ -26,6 +28,14 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
authorize(Contact)
end
def build_contact_inbox
return if params[:inbox_id].blank?
inbox = Inbox.find(params[:inbox_id])
source_id = params[:source_id] || SecureRandom.uuid
ContactInbox.create(contact: @contact, inbox: inbox, source_id: source_id)
end
def contact_params
params.require(:contact).permit(:name, :email, :phone_number)
end

View file

@ -5,8 +5,10 @@ class Api::V1::Accounts::Conversations::MessagesController < Api::V1::Accounts::
def create
user = current_user || @resource
mb = Messages::Outgoing::NormalBuilder.new(user, @conversation, params)
mb = Messages::MessageBuilder.new(user, @conversation, params)
@message = mb.perform
rescue StandardError => e
render_could_not_create_error(e.message)
end
private

View file

@ -4,12 +4,12 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
before_action :check_authorization
def index
@inboxes = policy_scope(Current.account.inboxes)
@inboxes = policy_scope(Current.account.inboxes.includes(:channel, :avatar_attachment))
end
def create
ActiveRecord::Base.transaction do
channel = web_widgets.create!(permitted_params[:channel].except(:type)) if permitted_params[:channel][:type] == 'web_widget'
channel = create_channel
@inbox = Current.account.inboxes.build(
name: permitted_params[:name],
greeting_message: permitted_params[:greeting_message],
@ -52,21 +52,28 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
@agent_bot = AgentBot.find(params[:agent_bot]) if params[:agent_bot]
end
def web_widgets
Current.account.web_widgets
end
def check_authorization
authorize(Inbox)
end
def create_channel
case permitted_params[:channel][:type]
when 'web_widget'
Current.account.web_widgets.create!(permitted_params[:channel].except(:type))
when 'api'
Current.account.api_channels.create!(permitted_params[:channel].except(:type))
when 'email'
Current.account.email_channels.create!(permitted_params[:channel].except(:type))
end
end
def permitted_params
params.permit(:id, :avatar, :name, :greeting_message, :greeting_enabled, channel:
[:type, :website_url, :widget_color, :welcome_title, :welcome_tagline])
[:type, :website_url, :widget_color, :welcome_title, :welcome_tagline, :webhook_url, :email])
end
def inbox_update_params
params.permit(:enable_auto_assignment, :name, :avatar, :greeting_message, :greeting_enabled,
channel: [:website_url, :widget_color, :welcome_title, :welcome_tagline])
channel: [:website_url, :widget_color, :welcome_title, :welcome_tagline, :webhook_url, :email])
end
end

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

View file

@ -63,6 +63,8 @@ const INBOX_TYPES = {
FB: 'Channel::FacebookPage',
TWITTER: 'Channel::TwitterProfile',
TWILIO: 'Channel::TwilioSms',
API: 'Channel::Api',
EMAIL: 'Channel::Email',
};
const getInboxClassByType = type => {
switch (type) {
@ -78,6 +80,12 @@ const getInboxClassByType = type => {
case INBOX_TYPES.TWILIO:
return 'ion-android-textsms';
case INBOX_TYPES.API:
return 'ion-cloud';
case INBOX_TYPES.EMAIL:
return 'ion-email';
default:
return '';
}

View file

@ -16,6 +16,14 @@
v-if="channel === 'telegram'"
src="~dashboard/assets/images/channels/telegram.png"
/>
<img
v-if="channel === 'api'"
src="~dashboard/assets/images/channels/api.png"
/>
<img
v-if="channel === 'email'"
src="~dashboard/assets/images/channels/email.png"
/>
<img
v-if="channel === 'line'"
src="~dashboard/assets/images/channels/line.png"
@ -56,7 +64,10 @@ export default {
if (channel === 'twitter') {
return this.enabledFeatures.channel_twitter;
}
return ['website', 'twilio'].includes(channel);
if (channel === 'email') {
return this.enabledFeatures.channel_email;
}
return ['website', 'twilio', 'api'].includes(channel);
},
onItemClick() {
if (this.isActive(this.channel)) {

View file

@ -115,6 +115,43 @@
"ERROR_MESSAGE": "We were not able to authenticate Twilio credentials, please try again"
}
},
"API_CHANNEL": {
"TITLE": "API Channel",
"DESC": "Integrate with API channel and start supporting your customers via chatwoot.",
"CHANNEL_NAME": {
"LABEL": "Channel Name",
"PLACEHOLDER": "Please enter a channel name",
"ERROR": "This field is required"
},
"WEBHOOK_URL": {
"LABEL": "Webhook Url",
"SUBTITLE": "Configure the url where you want to recieve callbacks from chatwoot on events.",
"PLACEHOLDER": "Webhook Url"
},
"SUBMIT_BUTTON": "Create API Channel",
"API": {
"ERROR_MESSAGE": "We were not able to save the api channel"
}
},
"EMAIL_CHANNEL": {
"TITLE": "Email Channel",
"DESC": "Integrate you email inbox with chatwoot.",
"CHANNEL_NAME": {
"LABEL": "Channel Name",
"PLACEHOLDER": "Please enter a channel name",
"ERROR": "This field is required"
},
"EMAIL": {
"LABEL": "Email",
"SUBTITLE": "Email where your customers sends you support tickets",
"PLACEHOLDER": "Email"
},
"SUBMIT_BUTTON": "Create Email Channel",
"API": {
"ERROR_MESSAGE": "We were not able to save the email channel"
},
"FINISH_MESSAGE" : "Start forwarding your emails to the following email address."
},
"AUTH": {
"TITLE": "Channels",
"DESC": "Currently we support Website live chat widgets, Facebook Pages and Twitter profiles as platforms. We have more platforms like Whatsapp, Email, Telegram and Line in the works, which will be out soon."

View file

@ -34,6 +34,8 @@ export default {
'facebook',
'twitter',
'twilio',
'email',
'api',
'telegram',
'line',
],

View file

@ -21,6 +21,14 @@
>
</woot-code>
</div>
<div class="medium-6 small-offset-3">
<woot-code
v-if="isAEmailInbox"
lang="html"
:script="currentInbox.forward_to_address"
>
</woot-code>
</div>
<router-link
class="button success nice"
:to="{
@ -53,6 +61,9 @@ export default {
isATwilioInbox() {
return this.currentInbox.channel_type === 'Channel::TwilioSms';
},
isAEmailInbox() {
return this.currentInbox.channel_type === 'Channel::Email';
},
message() {
if (this.isATwilioInbox) {
return `${this.$t('INBOX_MGMT.FINISH.MESSAGE')}. ${this.$t(
@ -60,6 +71,10 @@ export default {
)}`;
}
if (this.isAEmailInbox) {
return this.$t('INBOX_MGMT.ADD.EMAIL_CHANNEL.FINISH_MESSAGE');
}
if (!this.currentInbox.website_token) {
return this.$t('INBOX_MGMT.FINISH.MESSAGE');
}

View file

@ -45,6 +45,12 @@
<span v-if="item.channel_type === 'Channel::TwilioSms'">
Twilio SMS
</span>
<span v-if="item.channel_type === 'Channel::Email'">
Email
</span>
<span v-if="item.channel_type === 'Channel::Api'">
Api
</span>
</td>
<!-- Action Buttons -->

View file

@ -2,12 +2,16 @@ import Facebook from './channels/Facebook';
import Website from './channels/Website';
import Twitter from './channels/Twitter';
import Twilio from './channels/Twilio';
import Api from './channels/Api';
import Email from './channels/Email';
const channelViewList = {
facebook: Facebook,
website: Website,
twitter: Twitter,
twilio: Twilio,
api: Api,
email: Email,
};
export default {

View file

@ -0,0 +1,110 @@
<template>
<div class="wizard-body small-9 columns">
<page-header
:header-title="$t('INBOX_MGMT.ADD.API_CHANNEL.TITLE')"
:header-content="$t('INBOX_MGMT.ADD.API_CHANNEL.DESC')"
/>
<form class="row" @submit.prevent="createChannel()">
<div class="medium-8 columns">
<label :class="{ error: $v.channelName.$error }">
{{ $t('INBOX_MGMT.ADD.API_CHANNEL.CHANNEL_NAME.LABEL') }}
<input
v-model.trim="channelName"
type="text"
:placeholder="
$t('INBOX_MGMT.ADD.API_CHANNEL.CHANNEL_NAME.PLACEHOLDER')
"
@blur="$v.channelName.$touch"
/>
<span v-if="$v.channelName.$error" class="message">{{
$t('INBOX_MGMT.ADD.API_CHANNEL.CHANNEL_NAME.ERROR')
}}</span>
</label>
</div>
<div class="medium-8 columns">
<label :class="{ error: $v.webhookUrl.$error }">
{{ $t('INBOX_MGMT.ADD.API_CHANNEL.WEBHOOK_URL.LABEL') }}
<input
v-model.trim="webhookUrl"
type="text"
:placeholder="
$t('INBOX_MGMT.ADD.API_CHANNEL.WEBHOOK_URL.PLACEHOLDER')
"
@blur="$v.webhookUrl.$touch"
/>
</label>
<p class="help-text">
{{ $t('INBOX_MGMT.ADD.API_CHANNEL.WEBHOOK_URL.SUBTITLE') }}
</p>
</div>
<div class="medium-12 columns">
<woot-submit-button
:loading="uiFlags.isCreating"
:button-text="$t('INBOX_MGMT.ADD.API_CHANNEL.SUBMIT_BUTTON')"
/>
</div>
</form>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import alertMixin from 'shared/mixins/alertMixin';
import { required } from 'vuelidate/lib/validators';
import router from '../../../../index';
import PageHeader from '../../SettingsSubPageHeader';
const shouldBeWebhookUrl = (value = '') => value.startsWith('http');
export default {
components: {
PageHeader,
},
mixins: [alertMixin],
data() {
return {
channelName: '',
webhookUrl: '',
};
},
computed: {
...mapGetters({
uiFlags: 'inboxes/getUIFlags',
}),
},
validations: {
channelName: { required },
webhookUrl: { required, shouldBeWebhookUrl },
},
methods: {
async createChannel() {
this.$v.$touch();
if (this.$v.$invalid) {
return;
}
try {
const apiChannel = await this.$store.dispatch('inboxes/createChannel', {
name: this.channelName,
channel: {
type: 'api',
webhook_url: this.webhookUrl,
},
});
router.replace({
name: 'settings_inboxes_add_agents',
params: {
page: 'new',
inbox_id: apiChannel.id,
},
});
} catch (error) {
this.showAlert(this.$t('INBOX_MGMT.ADD.API_CHANNEL.API.ERROR_MESSAGE'));
}
},
},
};
</script>

View file

@ -0,0 +1,113 @@
<template>
<div class="wizard-body small-9 columns">
<page-header
:header-title="$t('INBOX_MGMT.ADD.EMAIL_CHANNEL.TITLE')"
:header-content="$t('INBOX_MGMT.ADD.EMAIL_CHANNEL.DESC')"
/>
<form class="row" @submit.prevent="createChannel()">
<div class="medium-8 columns">
<label :class="{ error: $v.channelName.$error }">
{{ $t('INBOX_MGMT.ADD.EMAIL_CHANNEL.CHANNEL_NAME.LABEL') }}
<input
v-model.trim="channelName"
type="text"
:placeholder="
$t('INBOX_MGMT.ADD.EMAIL_CHANNEL.CHANNEL_NAME.PLACEHOLDER')
"
@blur="$v.channelName.$touch"
/>
<span v-if="$v.channelName.$error" class="message">{{
$t('INBOX_MGMT.ADD.EMAIL_CHANNEL.CHANNEL_NAME.ERROR')
}}</span>
</label>
</div>
<div class="medium-8 columns">
<label :class="{ error: $v.email.$error }">
{{ $t('INBOX_MGMT.ADD.EMAIL_CHANNEL.EMAIL.LABEL') }}
<input
v-model.trim="email"
type="text"
:placeholder="$t('INBOX_MGMT.ADD.EMAIL_CHANNEL.EMAIL.PLACEHOLDER')"
@blur="$v.email.$touch"
/>
</label>
<p class="help-text">
{{ $t('INBOX_MGMT.ADD.EMAIL_CHANNEL.EMAIL.SUBTITLE') }}
</p>
</div>
<div class="medium-12 columns">
<woot-submit-button
:loading="uiFlags.isCreating"
:button-text="$t('INBOX_MGMT.ADD.EMAIL_CHANNEL.SUBMIT_BUTTON')"
/>
</div>
</form>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import alertMixin from 'shared/mixins/alertMixin';
import { required } from 'vuelidate/lib/validators';
import router from '../../../../index';
import PageHeader from '../../SettingsSubPageHeader';
const validEmail = (value = '') => value.includes('@');
export default {
components: {
PageHeader,
},
mixins: [alertMixin],
data() {
return {
channelName: '',
email: '',
};
},
computed: {
...mapGetters({
uiFlags: 'inboxes/getUIFlags',
}),
},
validations: {
channelName: { required },
email: { required, validEmail },
},
methods: {
async createChannel() {
this.$v.$touch();
if (this.$v.$invalid) {
return;
}
try {
const emailChannel = await this.$store.dispatch(
'inboxes/createChannel',
{
name: this.channelName,
channel: {
type: 'email',
email: this.email,
},
}
);
router.replace({
name: 'settings_inboxes_add_agents',
params: {
page: 'new',
inbox_id: emailChannel.id,
},
});
} catch (error) {
this.showAlert(
this.$t('INBOX_MGMT.ADD.EMAIL_CHANNEL.API.ERROR_MESSAGE')
);
}
},
},
};
</script>

View file

@ -55,6 +55,18 @@ export const actions = {
commit(types.default.SET_INBOXES_UI_FLAG, { isFetching: false });
}
},
createChannel: async ({ commit }, params) => {
try {
commit(types.default.SET_INBOXES_UI_FLAG, { isCreating: true });
const response = await WebChannel.create(params);
commit(types.default.ADD_INBOXES, response.data);
commit(types.default.SET_INBOXES_UI_FLAG, { isCreating: false });
return response.data;
} catch (error) {
commit(types.default.SET_INBOXES_UI_FLAG, { isCreating: false });
throw new Error(error);
}
},
createWebsiteChannel: async ({ commit }, params) => {
try {
commit(types.default.SET_INBOXES_UI_FLAG, { isCreating: true });

View file

@ -50,9 +50,7 @@ class WebhookListener < BaseListener
WebhookJob.perform_later(webhook.url, payload)
end
# Inbox webhooks
inbox.webhooks.inbox.each do |webhook|
WebhookJob.perform_later(webhook.url, payload)
end
# Deliver for API Inbox
WebhookJob.perform_later(inbox.channel.webhook_url, payload) if inbox.channel_type == 'Channel::Api'
end
end

View file

@ -43,6 +43,8 @@ class Account < ApplicationRecord
has_many :twilio_sms, dependent: :destroy, class_name: '::Channel::TwilioSms'
has_many :twitter_profiles, dependent: :destroy, class_name: '::Channel::TwitterProfile'
has_many :web_widgets, dependent: :destroy, class_name: '::Channel::WebWidget'
has_many :email_channels, dependent: :destroy, class_name: '::Channel::Email'
has_many :api_channels, dependent: :destroy, class_name: '::Channel::Api'
has_many :canned_responses, dependent: :destroy
has_many :webhooks, dependent: :destroy
has_many :labels, dependent: :destroy

19
app/models/channel/api.rb Normal file
View file

@ -0,0 +1,19 @@
# == Schema Information
#
# Table name: channel_api
#
# id :bigint not null, primary key
# webhook_url :string not null
# created_at :datetime not null
# updated_at :datetime not null
# account_id :integer not null
#
class Channel::Api < ApplicationRecord
self.table_name = 'channel_api'
validates :account_id, presence: true
belongs_to :account
has_one :inbox, as: :channel, dependent: :destroy
end

View file

@ -0,0 +1,35 @@
# == Schema Information
#
# Table name: channel_email
#
# id :bigint not null, primary key
# email :string not null
# forward_to_address :string not null
# created_at :datetime not null
# updated_at :datetime not null
# account_id :integer not null
#
# Indexes
#
# index_channel_email_on_email (email) UNIQUE
# index_channel_email_on_forward_to_address (forward_to_address) UNIQUE
#
class Channel::Email < ApplicationRecord
self.table_name = 'channel_email'
validates :account_id, presence: true
belongs_to :account
validates :email, uniqueness: true
validates :forward_to_address, uniqueness: true
has_one :inbox, as: :channel, dependent: :destroy
before_validation :ensure_forward_to_address, on: :create
private
def ensure_forward_to_address
# TODO : implement better logic here
self.forward_to_address ||= "#{SecureRandom.hex}@xyc.com"
end
end

View file

@ -17,9 +17,6 @@
#
class Channel::FacebookPage < ApplicationRecord
# FIXME: this should be removed post 1.4 release. we moved avatars to inbox
include Avatarable
self.table_name = 'channel_facebook_pages'
validates :account_id, presence: true

View file

@ -0,0 +1,9 @@
json.payload do
json.contact do
json.partial! 'api/v1/models/contact.json.jbuilder', resource: @contact
end
json.contact_inbox do
json.inbox @contact_inbox&.inbox
json.source_id @contact_inbox&.source_id
end
end

View file

@ -1,14 +1 @@
json.id @inbox.id
json.channel_id @inbox.channel_id
json.name @inbox.name
json.channel_type @inbox.channel_type
json.greeting_enabled @inbox.greeting_enabled
json.greeting_message @inbox.greeting_message
json.avatar_url @inbox.try(:avatar_url)
json.website_token @inbox.channel.try(:website_token)
json.widget_color @inbox.channel.try(:widget_color)
json.website_url @inbox.channel.try(:website_url)
json.welcome_title @inbox.channel.try(:welcome_title)
json.welcome_tagline @inbox.channel.try(:welcome_tagline)
json.web_widget_script @inbox.channel.try(:web_widget_script)
json.enable_auto_assignment @inbox.enable_auto_assignment
json.partial! 'api/v1/models/inbox.json.jbuilder', resource: @inbox

View file

@ -1,19 +1,5 @@
json.payload do
json.array! @inboxes do |inbox|
json.id inbox.id
json.channel_id inbox.channel_id
json.name inbox.name
json.channel_type inbox.channel_type
json.greeting_enabled inbox.greeting_enabled
json.greeting_message inbox.greeting_message
json.avatar_url inbox.try(:avatar_url)
json.page_id inbox.channel.try(:page_id)
json.widget_color inbox.channel.try(:widget_color)
json.website_url inbox.channel.try(:website_url)
json.welcome_title inbox.channel.try(:welcome_title)
json.welcome_tagline inbox.channel.try(:welcome_tagline)
json.enable_auto_assignment inbox.enable_auto_assignment
json.web_widget_script inbox.channel.try(:web_widget_script)
json.phone_number inbox.channel.try(:phone_number)
json.partial! 'api/v1/models/inbox.json.jbuilder', resource: inbox
end
end

View file

@ -1,14 +1 @@
json.id @inbox.id
json.channel_id @inbox.channel_id
json.name @inbox.name
json.channel_type @inbox.channel_type
json.greeting_enabled @inbox.greeting_enabled
json.greeting_message @inbox.greeting_message
json.avatar_url @inbox.try(:avatar_url)
json.website_token @inbox.channel.try(:website_token)
json.widget_color @inbox.channel.try(:widget_color)
json.website_url @inbox.channel.try(:website_url)
json.welcome_title @inbox.channel.try(:welcome_title)
json.welcome_tagline @inbox.channel.try(:welcome_tagline)
json.web_widget_script @inbox.channel.try(:web_widget_script)
json.enable_auto_assignment @inbox.enable_auto_assignment
json.partial! 'api/v1/models/inbox.json.jbuilder', resource: @inbox

View file

@ -0,0 +1,16 @@
json.id resource.id
json.channel_id resource.channel_id
json.name resource.name
json.channel_type resource.channel_type
json.greeting_enabled resource.greeting_enabled
json.greeting_message resource.greeting_message
json.avatar_url resource.try(:avatar_url)
json.page_id resource.channel.try(:page_id)
json.widget_color resource.channel.try(:widget_color)
json.website_url resource.channel.try(:website_url)
json.welcome_title resource.channel.try(:welcome_title)
json.welcome_tagline resource.channel.try(:welcome_tagline)
json.enable_auto_assignment resource.enable_auto_assignment
json.web_widget_script resource.channel.try(:web_widget_script)
json.forward_to_address resource.channel.try(:forward_to_address)
json.phone_number resource.channel.try(:phone_number)

View file

@ -0,0 +1,9 @@
class CreateApiChannel < ActiveRecord::Migration[6.0]
def change
create_table :channel_api do |t|
t.integer :account_id, null: false
t.string :webhook_url, null: false
t.timestamps
end
end
end

View file

@ -0,0 +1,10 @@
class CreateEmailChannel < ActiveRecord::Migration[6.0]
def change
create_table :channel_email do |t|
t.integer :account_id, null: false
t.string :email, null: false, index: { unique: true }
t.string :forward_to_address, null: false, index: { unique: true }
t.timestamps
end
end
end

View file

@ -120,6 +120,23 @@ ActiveRecord::Schema.define(version: 2020_07_19_171437) do
t.datetime "updated_at", null: false
end
create_table "channel_api", force: :cascade do |t|
t.integer "account_id", null: false
t.string "webhook_url", null: false
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
create_table "channel_email", force: :cascade do |t|
t.integer "account_id", null: false
t.string "email", null: false
t.string "forward_to_address", null: false
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["email"], name: "index_channel_email_on_email", unique: true
t.index ["forward_to_address"], name: "index_channel_email_on_forward_to_address", unique: true
end
create_table "channel_facebook_pages", id: :serial, force: :cascade do |t|
t.string "page_id", null: false
t.string "user_access_token", null: false

View file

@ -9,10 +9,10 @@ class Integrations::Facebook::MessageCreator
def perform
# begin
if outgoing_message_via_echo?
create_outgoing_message
if agent_message_via_echo?
create_agent_message
else
create_incoming_message
create_contact_message
end
# rescue => e
# Raven.capture_exception(e)
@ -21,22 +21,22 @@ class Integrations::Facebook::MessageCreator
private
def outgoing_message_via_echo?
def agent_message_via_echo?
response.echo? && !response.sent_from_chatwoot_app?
# this means that it is an outgoing message from page, but not sent from chatwoot.
# User can send from fb page directly on mobile messenger, so this case should be handled as outgoing message
# this means that it is an agent message from page, but not sent from chatwoot.
# User can send from fb page directly on mobile / web messenger, so this case should be handled as agent message
end
def create_outgoing_message
def create_agent_message
Channel::FacebookPage.where(page_id: response.sender_id).each do |page|
mb = Messages::Outgoing::EchoBuilder.new(response, page.inbox, true)
mb = Messages::Facebook::MessageBuilder.new(response, page.inbox, true)
mb.perform
end
end
def create_incoming_message
def create_contact_message
Channel::FacebookPage.where(page_id: response.recipient_id).each do |page|
mb = Messages::IncomingMessageBuilder.new(response, page.inbox)
mb = Messages::Facebook::MessageBuilder.new(response, page.inbox)
mb.perform
end
end

View file

@ -1,6 +1,6 @@
class Webhooks::Trigger
def self.execute(url, payload)
RestClient.post(url, payload)
RestClient.post(url, payload.to_json, { content_type: :json, accept: :json })
rescue StandardError => e
Raven.capture_exception(e)
end

View file

@ -1,6 +1,6 @@
require 'rails_helper'
describe ::Messages::IncomingMessageBuilder do
describe ::Messages::Facebook::MessageBuilder do
subject(:message_builder) { described_class.new(incoming_fb_text_message, facebook_channel.inbox).perform }
let!(:facebook_channel) { create(:channel_facebook_page) }

View file

@ -0,0 +1,54 @@
require 'rails_helper'
describe ::Messages::MessageBuilder do
subject(:message_builder) { described_class.new(user, conversation, params).perform }
let(:account) { create(:account) }
let(:user) { create(:user, account: account) }
let(:inbox) { create(:inbox, account: account) }
let(:inbox_member) { create(:inbox_member, inbox: inbox, account: account) }
let(:conversation) { create(:conversation, inbox: inbox, account: account) }
let(:params) do
ActionController::Parameters.new({
content: 'test'
})
end
describe '#perform' do
it 'creates a message' do
message = message_builder
expect(message.content).to eq params[:content]
end
end
describe '#perform when message_type is incoming' do
context 'when channel is not api' do
let(:params) do
ActionController::Parameters.new({
content: 'test',
message_type: 'incoming'
})
end
it 'creates throws error when channel is not api' do
expect { message_builder }.to raise_error 'Incoming messages are only allowed in Api inboxes'
end
end
context 'when channel is api' do
let(:channel_api) { create(:channel_api, account: account) }
let(:conversation) { create(:conversation, inbox: channel_api.inbox, account: account) }
let(:params) do
ActionController::Parameters.new({
content: 'test',
message_type: 'incoming'
})
end
it 'creates message when channel is api' do
message = message_builder
expect(message.message_type).to eq params[:message_type]
end
end
end
end

View file

@ -65,6 +65,7 @@ RSpec.describe 'Contacts API', type: :request do
context 'when it is an authenticated user' do
let(:admin) { create(:user, account: account, role: :administrator) }
let(:inbox) { create(:inbox, account: account) }
it 'creates the contact' do
expect do
@ -74,6 +75,15 @@ RSpec.describe 'Contacts API', type: :request do
expect(response).to have_http_status(:success)
end
it 'creates the contact identifier when inbox id is passed' do
expect do
post "/api/v1/accounts/#{account.id}/contacts", headers: admin.create_new_auth_token,
params: valid_params.merge({ inbox_id: inbox.id })
end.to change(ContactInbox, :count).by(1)
expect(response).to have_http_status(:success)
end
end
end

View file

@ -0,0 +1,9 @@
FactoryBot.define do
factory :channel_api, class: 'Channel::Api' do
webhook_url { 'http://example.com' }
account
after(:create) do |channel_api|
create(:inbox, channel: channel_api, account: channel_api.account)
end
end
end

View file

@ -5,10 +5,10 @@ describe Webhooks::Trigger do
describe '#execute' do
it 'triggers webhook' do
params = { hello: 'hello' }
url = 'htpps://test.com'
params = { hello: :hello }
url = 'https://test.com'
expect(RestClient).to receive(:post).with(url, params).once
expect(RestClient).to receive(:post).with(url, params.to_json, { accept: :json, content_type: :json }).once
trigger.execute(url, params)
end
end