-
-
-
-
+
+
+
-
- {{ agentName }}
-
+
+
+
+
+
+
+
+ {{ agentName }}
+
+
+
+
-
-
diff --git a/app/javascript/widget/components/AgentMessageBubble.vue b/app/javascript/widget/components/AgentMessageBubble.vue
index 8017a8854..5266d1f43 100755
--- a/app/javascript/widget/components/AgentMessageBubble.vue
+++ b/app/javascript/widget/components/AgentMessageBubble.vue
@@ -1,21 +1,64 @@
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/widget/components/template/EmailInput.vue b/app/javascript/widget/components/template/EmailInput.vue
index 1951e8db4..9d4d4a57c 100644
--- a/app/javascript/widget/components/template/EmailInput.vue
+++ b/app/javascript/widget/components/template/EmailInput.vue
@@ -21,9 +21,6 @@
-
- {{ messageContentAttributes.submitted_email }}
-
@@ -71,7 +68,7 @@ export default {
},
methods: {
onSubmit() {
- this.$store.dispatch('message/updateContactAttributes', {
+ this.$store.dispatch('message/update', {
email: this.email,
messageId: this.messageId,
});
diff --git a/app/javascript/widget/helpers/actionCable.js b/app/javascript/widget/helpers/actionCable.js
index 1e4a12523..a7706bbe5 100644
--- a/app/javascript/widget/helpers/actionCable.js
+++ b/app/javascript/widget/helpers/actionCable.js
@@ -14,6 +14,9 @@ class ActionCableConnector extends BaseActionCableConnector {
}
export const refreshActionCableConnector = pubsubToken => {
+ if (!pubsubToken) {
+ return;
+ }
window.chatwootPubsubToken = pubsubToken;
window.actionCable.disconnect();
window.actionCable = new ActionCableConnector(
diff --git a/app/javascript/widget/i18n/en.js b/app/javascript/widget/i18n/en.js
index c8aadd8da..4cac34d13 100644
--- a/app/javascript/widget/i18n/en.js
+++ b/app/javascript/widget/i18n/en.js
@@ -4,5 +4,8 @@ export default {
DOWNLOAD: 'Download',
UPLOADING: 'Uploading...',
},
+ FORM_BUBBLE: {
+ SUBMIT: 'Submit',
+ },
},
};
diff --git a/app/javascript/widget/store/index.js b/app/javascript/widget/store/index.js
index 076951c8b..d487ac991 100755
--- a/app/javascript/widget/store/index.js
+++ b/app/javascript/widget/store/index.js
@@ -5,6 +5,7 @@ import appConfig from 'widget/store/modules/appConfig';
import contacts from 'widget/store/modules/contacts';
import conversation from 'widget/store/modules/conversation';
import conversationLabels from 'widget/store/modules/conversationLabels';
+import events from 'widget/store/modules/events';
import message from 'widget/store/modules/message';
Vue.use(Vuex);
@@ -13,9 +14,10 @@ export default new Vuex.Store({
modules: {
agent,
appConfig,
- message,
contacts,
conversation,
conversationLabels,
+ events,
+ message,
},
});
diff --git a/app/javascript/widget/store/modules/conversation.js b/app/javascript/widget/store/modules/conversation.js
index 4388f6052..3a7762a42 100755
--- a/app/javascript/widget/store/modules/conversation.js
+++ b/app/javascript/widget/store/modules/conversation.js
@@ -188,7 +188,10 @@ export const mutations = {
updateMessage($state, { id, content_attributes }) {
$state.conversations[id] = {
...$state.conversations[id],
- content_attributes,
+ content_attributes: {
+ ...($state.conversations[id].content_attributes || {}),
+ ...content_attributes,
+ },
};
},
};
diff --git a/app/javascript/widget/store/modules/events.js b/app/javascript/widget/store/modules/events.js
new file mode 100644
index 000000000..3fa38147d
--- /dev/null
+++ b/app/javascript/widget/store/modules/events.js
@@ -0,0 +1,19 @@
+import events from 'widget/api/events';
+
+const actions = {
+ create: async (_, { name }) => {
+ try {
+ await events.create(name);
+ } catch (error) {
+ // Ignore error
+ }
+ },
+};
+
+export default {
+ namespaced: true,
+ state: {},
+ getters: {},
+ actions,
+ mutations: {},
+};
diff --git a/app/javascript/widget/store/modules/message.js b/app/javascript/widget/store/modules/message.js
index 4243023a7..839305d03 100644
--- a/app/javascript/widget/store/modules/message.js
+++ b/app/javascript/widget/store/modules/message.js
@@ -1,4 +1,4 @@
-import MessageAPI from 'widget/api/message';
+import MessageAPI from '../../api/message';
import { refreshActionCableConnector } from '../../helpers/actionCable';
const state = {
@@ -12,19 +12,24 @@ const getters = {
};
const actions = {
- updateContactAttributes: async ({ commit }, { email, messageId }) => {
+ update: async ({ commit }, { email, messageId, submittedValues }) => {
commit('toggleUpdateStatus', true);
try {
const {
- data: {
- contact: { pubsub_token: pubsubToken },
- },
- } = await MessageAPI.update({ email, messageId });
+ data: { contact: { pubsub_token: pubsubToken } = {} },
+ } = await MessageAPI.update({
+ email,
+ messageId,
+ values: submittedValues,
+ });
commit(
'conversation/updateMessage',
{
id: messageId,
- content_attributes: { submitted_email: email },
+ content_attributes: {
+ submitted_email: email,
+ submitted_values: email ? null : submittedValues,
+ },
},
{ root: true }
);
diff --git a/app/listeners/agent_bot_listener.rb b/app/listeners/agent_bot_listener.rb
index 848c09cb7..7155357c8 100644
--- a/app/listeners/agent_bot_listener.rb
+++ b/app/listeners/agent_bot_listener.rb
@@ -10,4 +10,28 @@ class AgentBotListener < BaseListener
payload = message.webhook_data.merge(event: __method__.to_s)
AgentBotJob.perform_later(agent_bot.outgoing_url, payload)
end
+
+ def message_updated(event)
+ message = extract_message_and_account(event)[0]
+ inbox = message.inbox
+ return unless message.reportable? && inbox.agent_bot_inbox.present?
+ return unless inbox.agent_bot_inbox.active?
+
+ agent_bot = inbox.agent_bot_inbox.agent_bot
+
+ payload = message.webhook_data.merge(event: __method__.to_s)
+ AgentBotJob.perform_later(agent_bot.outgoing_url, payload)
+ end
+
+ def webwidget_triggered(event)
+ contact_inbox = event.data[:contact_inbox]
+ inbox = contact_inbox.inbox
+ return if inbox.agent_bot_inbox.blank?
+ return unless inbox.agent_bot_inbox.active?
+
+ agent_bot = inbox.agent_bot_inbox.agent_bot
+
+ payload = contact_inbox.webhook_data.merge(event: __method__.to_s)
+ AgentBotJob.perform_later(agent_bot.outgoing_url, payload)
+ end
end
diff --git a/app/listeners/webhook_listener.rb b/app/listeners/webhook_listener.rb
index 0da0fdad4..3597a402e 100644
--- a/app/listeners/webhook_listener.rb
+++ b/app/listeners/webhook_listener.rb
@@ -6,6 +6,30 @@ class WebhookListener < BaseListener
return unless message.reportable?
payload = message.webhook_data.merge(event: __method__.to_s)
+ deliver_webhook_payloads(payload, inbox)
+ end
+
+ def message_updated(event)
+ message = extract_message_and_account(event)[0]
+ inbox = message.inbox
+
+ return unless message.reportable?
+
+ payload = message.webhook_data.merge(event: __method__.to_s)
+ deliver_webhook_payloads(payload, inbox)
+ end
+
+ def webwidget_triggered(event)
+ contact_inbox = event.data[:contact_inbox]
+ inbox = contact_inbox.inbox
+
+ payload = contact_inbox.webhook_data.merge(event: __method__.to_s)
+ deliver_webhook_payloads(payload, inbox)
+ end
+
+ private
+
+ def deliver_webhook_payloads(payload, inbox)
# Account webhooks
inbox.account.webhooks.account.each do |webhook|
WebhookJob.perform_later(webhook.url, payload)
diff --git a/app/models/channel/web_widget.rb b/app/models/channel/web_widget.rb
index 68fe67ae8..33a743f64 100644
--- a/app/models/channel/web_widget.rb
+++ b/app/models/channel/web_widget.rb
@@ -51,11 +51,12 @@ class Channel::WebWidget < ApplicationRecord
def create_contact_inbox
ActiveRecord::Base.transaction do
contact = inbox.account.contacts.create!(name: ::Haikunator.haikunate(1000))
- ::ContactInbox.create!(
+ contact_inbox = ::ContactInbox.create!(
contact_id: contact.id,
inbox_id: inbox.id,
source_id: SecureRandom.uuid
)
+ contact_inbox
rescue StandardError => e
Rails.logger e
end
diff --git a/app/models/concerns/content_attribute_validator.rb b/app/models/concerns/content_attribute_validator.rb
new file mode 100644
index 000000000..bbeac3a7f
--- /dev/null
+++ b/app/models/concerns/content_attribute_validator.rb
@@ -0,0 +1,52 @@
+class ContentAttributeValidator < ActiveModel::Validator
+ ALLOWED_SELECT_ITEM_KEYS = [:title, :value].freeze
+ ALLOWED_CARD_ITEM_KEYS = [:title, :description, :media_url, :actions].freeze
+ ALLOWED_CARD_ITEM_ACTION_KEYS = [:text, :type, :payload, :uri].freeze
+ ALLOWED_FORM_ITEM_KEYS = [:type, :placeholder, :label, :name, :options].freeze
+ ALLOWED_ARTICLE_KEYS = [:title, :description, :link].freeze
+
+ def validate(record)
+ case record.content_type
+ when 'input_select'
+ validate_items!(record)
+ validate_item_attributes!(record, ALLOWED_SELECT_ITEM_KEYS)
+ when 'cards'
+ validate_items!(record)
+ validate_item_attributes!(record, ALLOWED_CARD_ITEM_KEYS)
+ validate_item_actions!(record)
+ when 'form'
+ validate_items!(record)
+ validate_item_attributes!(record, ALLOWED_FORM_ITEM_KEYS)
+ when 'article'
+ validate_items!(record)
+ validate_item_attributes!(record, ALLOWED_ARTICLE_KEYS)
+ end
+ end
+
+ private
+
+ def validate_items!(record)
+ record.errors.add(:content_attributes, 'At least one item is required.') if record.items.blank?
+ record.errors.add(:content_attributes, 'Items should be a hash.') if record.items.reject { |item| item.is_a?(Hash) }.present?
+ end
+
+ def validate_item_attributes!(record, valid_keys)
+ item_keys = record.items.collect(&:keys).flatten.map(&:to_sym).compact
+ invalid_keys = item_keys - valid_keys
+ record.errors.add(:content_attributes, "contains invalid keys for items : #{invalid_keys}") if invalid_keys.present?
+ end
+
+ def validate_item_actions!(record)
+ if record.items.select { |item| item[:actions].blank? }.present?
+ record.errors.add(:content_attributes, 'contains items missing actions') && return
+ end
+
+ validate_item_action_attributes!(record)
+ end
+
+ def validate_item_action_attributes!(record)
+ item_action_keys = record.items.collect { |item| item[:actions].collect(&:keys) }
+ invalid_keys = item_action_keys.flatten.compact.map(&:to_sym) - ALLOWED_CARD_ITEM_ACTION_KEYS
+ record.errors.add(:content_attributes, "contains invalid keys for actions: #{invalid_keys}") if invalid_keys.present?
+ end
+end
diff --git a/app/models/contact_inbox.rb b/app/models/contact_inbox.rb
index c79786e1e..c528a9af0 100644
--- a/app/models/contact_inbox.rb
+++ b/app/models/contact_inbox.rb
@@ -31,4 +31,19 @@ class ContactInbox < ApplicationRecord
belongs_to :inbox
has_many :conversations, dependent: :destroy
+
+ def webhook_data
+ {
+ id: id,
+ contact: contact.try(:webhook_data),
+ inbox: inbox.webhook_data,
+ account: inbox.account.webhook_data,
+ current_conversation: current_conversation.try(:webhook_data),
+ source_id: source_id
+ }
+ end
+
+ def current_conversation
+ conversations.last
+ end
end
diff --git a/app/models/message.rb b/app/models/message.rb
index 1e434ffa2..cc323ebd0 100644
--- a/app/models/message.rb
+++ b/app/models/message.rb
@@ -38,11 +38,21 @@ class Message < ApplicationRecord
validates :account_id, presence: true
validates :inbox_id, presence: true
validates :conversation_id, presence: true
+ validates_with ContentAttributeValidator
enum message_type: { incoming: 0, outgoing: 1, activity: 2, template: 3 }
- enum content_type: { text: 0, input: 1, input_textarea: 2, input_email: 3 }
+ enum content_type: {
+ text: 0,
+ input_text: 1,
+ input_textarea: 2,
+ input_email: 3,
+ input_select: 4,
+ cards: 5,
+ form: 6,
+ article: 7
+ }
enum status: { sent: 0, delivered: 1, read: 2, failed: 3 }
- store :content_attributes, accessors: [:submitted_email], coder: JSON, prefix: :input
+ store :content_attributes, accessors: [:submitted_email, :items, :submitted_values], coder: JSON
# .succ is a hack to avoid https://makandracards.com/makandra/1057-why-two-ruby-time-objects-are-not-equal-although-they-appear-to-be
scope :unread_since, ->(datetime) { where('EXTRACT(EPOCH FROM created_at) > (?)', datetime.to_i.succ) }
@@ -63,6 +73,8 @@ class Message < ApplicationRecord
:execute_message_template_hooks,
:notify_via_mail
+ after_update :dispatch_update_event
+
def channel_token
@token ||= inbox.channel.try(:page_access_token)
end
@@ -88,6 +100,8 @@ class Message < ApplicationRecord
content: content,
created_at: created_at,
message_type: message_type,
+ content_type: content_type,
+ content_attributes: content_attributes,
source_id: source_id,
sender: user.try(:webhook_data),
contact: contact.try(:webhook_data),
@@ -107,6 +121,10 @@ class Message < ApplicationRecord
end
end
+ def dispatch_update_event
+ Rails.configuration.dispatcher.dispatch(MESSAGE_UPDATED, Time.zone.now, message: self)
+ end
+
def send_reply
channel_name = conversation.inbox.channel.class.to_s
if channel_name == 'Channel::FacebookPage'
diff --git a/app/views/api/v1/accounts/conversations/create.json.jbuilder b/app/views/api/v1/accounts/conversations/create.json.jbuilder
new file mode 100644
index 000000000..2d39b121c
--- /dev/null
+++ b/app/views/api/v1/accounts/conversations/create.json.jbuilder
@@ -0,0 +1 @@
+json.partial! 'api/v1/conversations/partials/conversation.json.jbuilder', conversation: @conversation
diff --git a/app/views/api/v1/accounts/conversations/messages/create.json.jbuilder b/app/views/api/v1/accounts/conversations/messages/create.json.jbuilder
index f47f86587..e01408e43 100644
--- a/app/views/api/v1/accounts/conversations/messages/create.json.jbuilder
+++ b/app/views/api/v1/accounts/conversations/messages/create.json.jbuilder
@@ -1,8 +1,10 @@
json.id @message.id
json.content @message.content
json.inbox_id @message.inbox_id
-json.conversation_id @message.conversation_id
+json.conversation_id @message.conversation.display_id
json.message_type @message.message_type_before_type_cast
+json.content_type @message.content_type
+json.content_attributes @message.content_attributes
json.created_at @message.created_at.to_i
json.private @message.private
json.attachment @message.attachment.push_event_data if @message.attachment
diff --git a/app/views/api/v1/conversations/partials/_conversation.json.jbuilder b/app/views/api/v1/conversations/partials/_conversation.json.jbuilder
index c4e416d81..4f511d3ba 100644
--- a/app/views/api/v1/conversations/partials/_conversation.json.jbuilder
+++ b/app/views/api/v1/conversations/partials/_conversation.json.jbuilder
@@ -22,3 +22,4 @@ json.user_last_seen_at conversation.user_last_seen_at.to_i
json.agent_last_seen_at conversation.agent_last_seen_at.to_i
json.unread_count conversation.unread_incoming_messages.count
json.additional_attributes conversation.additional_attributes
+json.account_id conversation.account_id
diff --git a/app/views/api/v1/widget/messages/update.json.jbuilder b/app/views/api/v1/widget/messages/update.json.jbuilder
index da1e28d00..e5abbd295 100644
--- a/app/views/api/v1/widget/messages/update.json.jbuilder
+++ b/app/views/api/v1/widget/messages/update.json.jbuilder
@@ -1 +1 @@
-json.contact @contact
+json.contact @contact if @contact
diff --git a/config/routes.rb b/config/routes.rb
index be63d95e9..8a9f0e568 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -39,7 +39,7 @@ Rails.application.routes.draw do
namespace :channels do
resource :twilio_channel, only: [:create]
end
- resources :conversations, only: [:index, :show] do
+ resources :conversations, only: [:index, :create, :show] do
scope module: :conversations do
resources :messages, only: [:index, :create]
resources :assignments, only: [:create]
@@ -109,10 +109,11 @@ Rails.application.routes.draw do
resources :agent_bots, only: [:index]
namespace :widget do
+ resources :events, only: [:create]
+ resources :messages, only: [:index, :create, :update]
resource :contact, only: [:update]
resources :inbox_members, only: [:index]
resources :labels, only: [:create, :destroy]
- resources :messages, only: [:index, :create, :update]
end
resources :webhooks, only: [] do
diff --git a/db/schema.rb b/db/schema.rb
index 4ce01e6ff..b64a28634 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -287,9 +287,11 @@ ActiveRecord::Schema.define(version: 2020_04_04_135009) do
t.index ["taggable_id", "taggable_type", "context"], name: "index_taggings_on_taggable_id_and_taggable_type_and_context"
t.index ["taggable_id", "taggable_type", "tagger_id", "context"], name: "taggings_idy"
t.index ["taggable_id"], name: "index_taggings_on_taggable_id"
+ t.index ["taggable_type", "taggable_id"], name: "index_taggings_on_taggable_type_and_taggable_id"
t.index ["taggable_type"], name: "index_taggings_on_taggable_type"
t.index ["tagger_id", "tagger_type"], name: "index_taggings_on_tagger_id_and_tagger_type"
t.index ["tagger_id"], name: "index_taggings_on_tagger_id"
+ t.index ["tagger_type", "tagger_id"], name: "index_taggings_on_tagger_type_and_tagger_id"
end
create_table "tags", id: :serial, force: :cascade do |t|
diff --git a/docs/webhooks/add-webhooks-to-chatwoot.md b/docs/webhooks/add-webhooks-to-chatwoot.md
index 6e3576707..ddf45a031 100644
--- a/docs/webhooks/add-webhooks-to-chatwoot.md
+++ b/docs/webhooks/add-webhooks-to-chatwoot.md
@@ -28,7 +28,9 @@ Chatwoot currently supports webhooks for message creation only. Once a new messa
"id": "1", // Message ID
"content": "Hi", // Content of the message
"created_at": "2020-03-03 13:05:57 UTC", // Time at which the message was sent
- "message_type": "incoming", // This will have a type incoming or outgoing. Incoming messages are sent by the user from the widget, Outgoing messages are sent by the agent to the user.
+ "message_type": "incoming", // This will have a type incoming, outgoing or template. Incoming messages are sent by the user from the widget, Outgoing messages are sent by the agent to the user.
+ "content_type": "enum", // This is an enum, it can be input_select, cards, form or text. The message_type will be template if content_type is one og these. Default value is text
+ "content_attributes": {} // This will an object, different values are defined below
"source_id": "", // This would the external id if the inbox is a Twitter or Facebook integration.
"sender": { // This would provide the details of the agent who sent this message
"id": "1",
@@ -59,3 +61,70 @@ Chatwoot currently supports webhooks for message creation only. Once a new messa
}
}
```
+
+### Content Attributes
+
+#### 1. Options
+
+```json
+{
+ "items": [
+ {
+ "title": "Option 1",
+ "value": "option_1"
+ },
+ {
+ "title": "Option 2",
+ "value": "option_2"
+ }
+ ],
+ "submitted_values": [
+ {
+ "title": "Option 1",
+ "value": "option_1"
+ }
+ ]
+}
+```
+
+#### 2. Form
+
+```json
+{
+ "items": [
+ {
+ "type": "text/text_area/email",
+ "placeholder": "Placeholder",
+ "name": "unique_name_of_the_field",
+ "label": "Label"
+ },
+ ],
+ "submitted_values": [
+ {
+ "name": "unique_name_of_the_field 1",
+ "value": "sample_value"
+ }
+ ]
+}
+```
+
+#### 3. Cards
+
+```json
+{
+ "items": [
+ {
+ "media_url": "", // Url of the image to be displayed
+ "title": "", // Title of the card
+ "description": "", // Description of the card
+ "actions": [
+ {
+ "type": "link",
+ "text": "View More",
+ "uri": "" // Link to the website
+ }
+ ]
+ },
+ ],
+}
+```
diff --git a/lib/events/types.rb b/lib/events/types.rb
index 158312ee9..4e34509df 100644
--- a/lib/events/types.rb
+++ b/lib/events/types.rb
@@ -4,9 +4,11 @@ module Events::Types
CONVERSATION_CREATED = 'conversation.created'
CONVERSATION_RESOLVED = 'conversation.resolved'
CONVERSATION_READ = 'conversation.read'
+ WEBWIDGET_TRIGGERED = 'webwidget.triggered'
MESSAGE_CREATED = 'message.created'
FIRST_REPLY_CREATED = 'first.reply.created'
+ MESSAGE_UPDATED = 'message.updated'
CONVERSATION_REOPENED = 'conversation.reopened'
CONVERSATION_LOCK_TOGGLE = 'conversation.lock_toggle'
ASSIGNEE_CHANGED = 'assignee.changed'
diff --git a/spec/controllers/api/v1/accounts/conversations/messages_controller_spec.rb b/spec/controllers/api/v1/accounts/conversations/messages_controller_spec.rb
index 9f21f1fae..588a129ab 100644
--- a/spec/controllers/api/v1/accounts/conversations/messages_controller_spec.rb
+++ b/spec/controllers/api/v1/accounts/conversations/messages_controller_spec.rb
@@ -19,7 +19,7 @@ RSpec.describe 'Conversation Messages API', type: :request do
let(:agent) { create(:user, account: account, role: :agent) }
it 'creates a new outgoing message' do
- params = { message: 'test-message', private: true }
+ params = { content: 'test-message', private: true }
post api_v1_account_conversation_messages_url(account_id: account.id, conversation_id: conversation.display_id),
params: params,
@@ -28,12 +28,12 @@ RSpec.describe 'Conversation Messages API', type: :request do
expect(response).to have_http_status(:success)
expect(conversation.messages.count).to eq(1)
- expect(conversation.messages.first.content).to eq(params[:message])
+ expect(conversation.messages.first.content).to eq(params[:content])
end
it 'creates a new outgoing message with attachment' do
file = fixture_file_upload(Rails.root.join('spec/assets/avatar.png'), 'image/png')
- params = { message: 'test-message', attachment: { file: file } }
+ params = { content: 'test-message', attachment: { file: file } }
post api_v1_account_conversation_messages_url(account_id: account.id, conversation_id: conversation.display_id),
params: params,
@@ -50,7 +50,7 @@ RSpec.describe 'Conversation Messages API', type: :request do
it 'creates a new outgoing message' do
create(:agent_bot_inbox, inbox: inbox, agent_bot: agent_bot)
- params = { message: 'test-message' }
+ params = { content: 'test-message' }
post api_v1_account_conversation_messages_url(account_id: account.id, conversation_id: conversation.display_id),
params: params,
@@ -59,7 +59,39 @@ RSpec.describe 'Conversation Messages API', type: :request do
expect(response).to have_http_status(:success)
expect(conversation.messages.count).to eq(1)
- expect(conversation.messages.first.content).to eq(params[:message])
+ expect(conversation.messages.first.content).to eq(params[:content])
+ end
+
+ it 'creates a new outgoing input select message' do
+ create(:agent_bot_inbox, inbox: inbox, agent_bot: agent_bot)
+ select_item1 = build(:bot_message_select)
+ select_item2 = build(:bot_message_select)
+ params = { content_type: 'input_select', content_attributes: { items: [select_item1, select_item2] } }
+
+ post api_v1_account_conversation_messages_url(account_id: account.id, conversation_id: conversation.display_id),
+ params: params,
+ headers: { api_access_token: agent_bot.access_token.token },
+ as: :json
+
+ expect(response).to have_http_status(:success)
+ expect(conversation.messages.count).to eq(1)
+ expect(conversation.messages.first.content_type).to eq(params[:content_type])
+ expect(conversation.messages.first.content).to eq nil
+ end
+
+ it 'creates a new outgoing cards message' do
+ create(:agent_bot_inbox, inbox: inbox, agent_bot: agent_bot)
+ card = build(:bot_message_card)
+ params = { content_type: 'cards', content_attributes: { items: [card] } }
+
+ post api_v1_account_conversation_messages_url(account_id: account.id, conversation_id: conversation.display_id),
+ params: params,
+ headers: { api_access_token: agent_bot.access_token.token },
+ as: :json
+
+ expect(response).to have_http_status(:success)
+ expect(conversation.messages.count).to eq(1)
+ expect(conversation.messages.first.content_type).to eq(params[:content_type])
end
end
end
diff --git a/spec/controllers/api/v1/widget/events_controller_spec.rb b/spec/controllers/api/v1/widget/events_controller_spec.rb
new file mode 100644
index 000000000..e99686830
--- /dev/null
+++ b/spec/controllers/api/v1/widget/events_controller_spec.rb
@@ -0,0 +1,37 @@
+require 'rails_helper'
+
+RSpec.describe '/api/v1/widget/events', type: :request do
+ let(:account) { create(:account) }
+ let(:web_widget) { create(:channel_widget, account: account) }
+ let(:contact) { create(:contact, account: account) }
+ let(:contact_inbox) { create(:contact_inbox, contact: contact, inbox: web_widget.inbox) }
+ let(:payload) { { source_id: contact_inbox.source_id, inbox_id: web_widget.inbox.id } }
+ let(:token) { ::Widget::TokenService.new(payload: payload).generate_token }
+
+ describe 'POST /api/v1/widget/events' do
+ let(:params) { { website_token: web_widget.website_token, name: 'webwidget.triggered' } }
+
+ context 'with invalid website token' do
+ it 'returns unauthorized' do
+ post '/api/v1/widget/events', params: { website_token: '' }
+ expect(response).to have_http_status(:not_found)
+ end
+ end
+
+ context 'with correct website token' do
+ before do
+ allow(Rails.configuration.dispatcher).to receive(:dispatch)
+ end
+
+ it 'dispatches the webwidget event' do
+ post '/api/v1/widget/events',
+ params: params,
+ headers: { 'X-Auth-Token' => token },
+ as: :json
+
+ expect(response).to have_http_status(:success)
+ expect(Rails.configuration.dispatcher).to have_received(:dispatch).with(params[:name], anything, contact_inbox: contact_inbox)
+ end
+ end
+ end
+end
diff --git a/spec/controllers/api/v1/widget/messages_controller_spec.rb b/spec/controllers/api/v1/widget/messages_controller_spec.rb
index 9a0b73d02..8ce37e17a 100644
--- a/spec/controllers/api/v1/widget/messages_controller_spec.rb
+++ b/spec/controllers/api/v1/widget/messages_controller_spec.rb
@@ -65,7 +65,7 @@ RSpec.describe '/api/v1/widget/messages', type: :request do
describe 'PUT /api/v1/widget/messages' do
context 'when put request is made with non existing email' do
it 'updates message in conversation and creates a new contact' do
- message = create(:message, account: account, inbox: web_widget.inbox, conversation: conversation)
+ message = create(:message, content_type: 'input_email', account: account, inbox: web_widget.inbox, conversation: conversation)
email = Faker::Internet.email
contact_params = { email: email }
put api_v1_widget_message_url(message.id),
@@ -75,14 +75,14 @@ RSpec.describe '/api/v1/widget/messages', type: :request do
expect(response).to have_http_status(:success)
message.reload
- expect(message.input_submitted_email).to eq(email)
+ expect(message.submitted_email).to eq(email)
expect(message.conversation.contact.email).to eq(email)
end
end
context 'when put request is made with invalid email' do
it 'rescues the error' do
- message = create(:message, account: account, inbox: web_widget.inbox, conversation: conversation)
+ message = create(:message, account: account, content_type: 'input_email', inbox: web_widget.inbox, conversation: conversation)
contact_params = { email: nil }
put api_v1_widget_message_url(message.id),
params: { website_token: web_widget.website_token, contact: contact_params },
@@ -95,7 +95,7 @@ RSpec.describe '/api/v1/widget/messages', type: :request do
context 'when put request is made with existing email' do
it 'updates message in conversation and deletes the current contact' do
- message = create(:message, account: account, inbox: web_widget.inbox, conversation: conversation)
+ message = create(:message, account: account, content_type: 'input_email', inbox: web_widget.inbox, conversation: conversation)
email = Faker::Internet.email
create(:contact, account: account, email: email)
contact_params = { email: email }
@@ -110,7 +110,7 @@ RSpec.describe '/api/v1/widget/messages', type: :request do
end
it 'ignores the casing of email, updates message in conversation and deletes the current contact' do
- message = create(:message, account: account, inbox: web_widget.inbox, conversation: conversation)
+ message = create(:message, content_type: 'input_email', account: account, inbox: web_widget.inbox, conversation: conversation)
email = Faker::Internet.email
create(:contact, account: account, email: email)
contact_params = { email: email.upcase }
diff --git a/spec/factories/bot_message/bot_message_card.rb b/spec/factories/bot_message/bot_message_card.rb
new file mode 100644
index 000000000..f2f77bb34
--- /dev/null
+++ b/spec/factories/bot_message/bot_message_card.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :bot_message_card, class: Hash do
+ title { Faker::Book.name }
+ description { Faker::Movie.quote }
+ media_url { 'https://via.placeholder.com/250x250.png' }
+ actions do
+ [{
+ text: 'Select',
+ type: 'postback',
+ payload: 'TACOS'
+ }, {
+ text: 'More info',
+ type: 'link',
+ uri: 'http://example.org'
+ }]
+ end
+
+ initialize_with { attributes }
+ end
+end
diff --git a/spec/factories/bot_message/bot_message_select.rb b/spec/factories/bot_message/bot_message_select.rb
new file mode 100644
index 000000000..c50f3d31b
--- /dev/null
+++ b/spec/factories/bot_message/bot_message_select.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :bot_message_select, class: Hash do
+ title { Faker::Book.name }
+ value { Faker::Book.name }
+
+ initialize_with { attributes }
+ end
+end
diff --git a/swagger/definitions/request/conversation/create_message.yml b/swagger/definitions/request/conversation/create_message.yml
index 02a5564cb..caa5dcd36 100644
--- a/swagger/definitions/request/conversation/create_message.yml
+++ b/swagger/definitions/request/conversation/create_message.yml
@@ -4,10 +4,16 @@ properties:
type: number
description: ID of the conversation
required: true
- message:
+ content:
type: string
description: The content of the message
required: true
private:
type: boolean
description: Flag to identify if it is a private note
+ content_type:
+ type: string
+ enum: ['input_select', 'form', 'cards']
+ content_attributes:
+ type: object
+ description: options/form object
diff --git a/swagger/definitions/resource/conversation.yml b/swagger/definitions/resource/conversation.yml
index 204810534..05126e1ab 100644
--- a/swagger/definitions/resource/conversation.yml
+++ b/swagger/definitions/resource/conversation.yml
@@ -5,6 +5,9 @@ properties:
description: ID of the conversation
messages:
type: array
+ account_id:
+ type: number
+ description: Account Id
inbox_id:
type: number
description: ID of the inbox
diff --git a/swagger/definitions/resource/message.yml b/swagger/definitions/resource/message.yml
index a27a88d76..78c8fd640 100644
--- a/swagger/definitions/resource/message.yml
+++ b/swagger/definitions/resource/message.yml
@@ -3,6 +3,13 @@ properties:
content:
type: string
description: The text content of the message
+ content_type:
+ type: string
+ enum: ["text", "input_select", "cards", "form"]
+ description: The type of the template message
+ content_attributes:
+ type: object
+ description: The content attributes for each content_type
message_type:
type: string
enum: ["incoming", "outgoing", "activity", "template"]
diff --git a/swagger/paths/conversation/list.yml b/swagger/paths/conversation/index_or_create.yml
similarity index 51%
rename from swagger/paths/conversation/list.yml
rename to swagger/paths/conversation/index_or_create.yml
index 1583bdfd5..b1a2dbdd4 100644
--- a/swagger/paths/conversation/list.yml
+++ b/swagger/paths/conversation/index_or_create.yml
@@ -37,3 +37,39 @@ get:
description: Bad Request Error
schema:
$ref: '#/definitions/bad_request_error'
+ description: Access denied
+
+post:
+ tags:
+ - Conversation
+ operationId: newConversation
+ summary: Create New Conversation
+ description: Create conversation
+ parameters:
+ - name: data
+ in: body
+ required: true
+ schema:
+ type: object
+ properties:
+ source_id:
+ type: string
+ description: Contact Source Id
+
+ responses:
+ 200:
+ description: Success
+ schema:
+ type: object
+ properties:
+ id:
+ type: number
+ description: ID of the conversation
+ account_id:
+ type: number
+ description: Account Id
+ inbox_id:
+ type: number
+ description: ID of the inbox
+ 403:
+ description: Access denied
diff --git a/swagger/paths/index.yml b/swagger/paths/index.yml
index 67069bdc9..e4944868d 100644
--- a/swagger/paths/index.yml
+++ b/swagger/paths/index.yml
@@ -10,15 +10,14 @@
/accounts/{account_id}/inboxes/{id}:
$ref: ./inboxes/update.yml
/accounts/{account_id}/inboxes/{id}/set_agent_bot:
- $ref: ./inboxes/update.yml
+ $ref: ./inboxes/set_agent_bot.yml
/agent_bots:
$ref: ./agent_bots/index.yml
-
# Conversations
/accounts/{account_id}/conversations:
- $ref: ./conversation/list.yml
+ $ref: ./conversation/index_or_create.yml
/accounts/{account_id}/conversations/{id}:
$ref: ./conversation/crud.yml
/accounts/{account_id}/conversations/{id}/toggle_status:
diff --git a/swagger/swagger.json b/swagger/swagger.json
index 67e3302c2..31ab3a226 100644
--- a/swagger/swagger.json
+++ b/swagger/swagger.json
@@ -201,13 +201,13 @@
}
},
"/accounts/{account_id}/inboxes/{id}/set_agent_bot": {
- "patch": {
+ "post": {
"tags": [
"Inbox"
],
- "operationId": "updateInbox",
- "summary": "Update Inbox",
- "description": "Add avatar and disable auto assignment for an inbox",
+ "operationId": "updateAgentBot",
+ "summary": "Add or remove agent bot",
+ "description": "To add an agent bot pass agent_bot id, to remove agent bot from an inbox pass null",
"parameters": [
{
"name": "id",
@@ -223,29 +223,21 @@
"schema": {
"type": "object",
"properties": {
- "enable_auto_assignment": {
- "type": "boolean",
+ "agent_bot": {
+ "type": "number",
"required": true,
- "description": "Enable Auto Assignment"
- },
- "avatar": {
- "type": "file",
- "required": false,
- "description": "Image file for avatar"
+ "description": "Agent bot ID"
}
}
}
}
],
"responses": {
- "200": {
- "description": "Success",
- "schema": {
- "$ref": "#/definitions/inbox"
- }
+ "204": {
+ "description": "Success"
},
"404": {
- "description": "Inbox not found"
+ "description": "Inbox not found, Agent bot not found"
},
"403": {
"description": "Access denied"
@@ -340,6 +332,56 @@
"schema": {
"$ref": "#/definitions/bad_request_error"
}
+ },
+ "description": "Access denied"
+ }
+ },
+ "post": {
+ "tags": [
+ "Conversation"
+ ],
+ "operationId": "newConversation",
+ "summary": "Create New Conversation",
+ "description": "Create conversation",
+ "parameters": [
+ {
+ "name": "data",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "type": "object",
+ "properties": {
+ "source_id": {
+ "type": "string",
+ "description": "Contact Source Id"
+ }
+ }
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Success",
+ "schema": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "number",
+ "description": "ID of the conversation"
+ },
+ "account_id": {
+ "type": "number",
+ "description": "Account Id"
+ },
+ "inbox_id": {
+ "type": "number",
+ "description": "ID of the inbox"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Access denied"
}
}
}
@@ -881,6 +923,10 @@
"messages": {
"type": "array"
},
+ "account_id": {
+ "type": "number",
+ "description": "Account Id"
+ },
"inbox_id": {
"type": "number",
"description": "ID of the inbox"
@@ -921,6 +967,20 @@
"type": "string",
"description": "The text content of the message"
},
+ "content_type": {
+ "type": "string",
+ "enum": [
+ "text",
+ "input_select",
+ "cards",
+ "form"
+ ],
+ "description": "The type of the template message"
+ },
+ "content_attributes": {
+ "type": "object",
+ "description": "The content attributes for each content_type"
+ },
"message_type": {
"type": "string",
"enum": [
@@ -1350,7 +1410,7 @@
"description": "ID of the conversation",
"required": true
},
- "message": {
+ "content": {
"type": "string",
"description": "The content of the message",
"required": true
@@ -1358,6 +1418,18 @@
"private": {
"type": "boolean",
"description": "Flag to identify if it is a private note"
+ },
+ "content_type": {
+ "type": "string",
+ "enum": [
+ "input_select",
+ "form",
+ "cards"
+ ]
+ },
+ "content_attributes": {
+ "type": "object",
+ "description": "options/form object"
}
}
}