Merge branch 'release/1.2.1'
This commit is contained in:
commit
1273ea64d3
46 changed files with 367 additions and 145 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -52,4 +52,6 @@ coverage
|
|||
|
||||
# ignore packages
|
||||
node_modules
|
||||
package-lock.json
|
||||
package-lock.json
|
||||
|
||||
*.dump
|
||||
|
|
|
@ -293,7 +293,7 @@ GEM
|
|||
pry-rails (0.3.9)
|
||||
pry (>= 0.10.4)
|
||||
public_suffix (4.0.3)
|
||||
puma (4.3.2)
|
||||
puma (4.3.3)
|
||||
nio4r (~> 2.0)
|
||||
pundit (2.1.0)
|
||||
activesupport (>= 3.0.0)
|
||||
|
|
|
@ -1,8 +1,18 @@
|
|||
class Api::V1::Conversations::MessagesController < Api::BaseController
|
||||
before_action :set_conversation, only: [:create]
|
||||
before_action :set_conversation, only: [:index, :create]
|
||||
|
||||
def index
|
||||
@messages = message_finder.perform
|
||||
end
|
||||
|
||||
def create
|
||||
mb = Messages::Outgoing::NormalBuilder.new(current_user, @conversation, params)
|
||||
@message = mb.perform
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def message_finder
|
||||
@message_finder ||= MessageFinder.new(@conversation, params)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -7,9 +7,7 @@ class Api::V1::ConversationsController < Api::BaseController
|
|||
@conversations_count = result[:count]
|
||||
end
|
||||
|
||||
def show
|
||||
@messages = messages_finder.perform
|
||||
end
|
||||
def show; end
|
||||
|
||||
def toggle_status
|
||||
@status = @conversation.toggle_status
|
||||
|
@ -34,8 +32,4 @@ class Api::V1::ConversationsController < Api::BaseController
|
|||
def conversation_finder
|
||||
@conversation_finder ||= ConversationFinder.new(current_user, params)
|
||||
end
|
||||
|
||||
def messages_finder
|
||||
@message_finder ||= MessageFinder.new(@conversation, params)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
class Twitter::CallbacksController < Twitter::BaseController
|
||||
def show
|
||||
return redirect_to app_new_twitter_inbox_url if permitted_params[:denied]
|
||||
|
||||
@response = twitter_client.access_token(
|
||||
oauth_token: permitted_params[:oauth_token],
|
||||
oauth_verifier: permitted_params[:oauth_verifier]
|
||||
|
@ -46,6 +48,6 @@ class Twitter::CallbacksController < Twitter::BaseController
|
|||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(:oauth_token, :oauth_verifier)
|
||||
params.permit(:oauth_token, :oauth_verifier, :denied)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -15,7 +15,7 @@ class MessageApi extends ApiClient {
|
|||
}
|
||||
|
||||
getPreviousMessages({ conversationId, before }) {
|
||||
return axios.get(`${this.url}/${conversationId}`, {
|
||||
return axios.get(`${this.url}/${conversationId}/messages`, {
|
||||
params: { before },
|
||||
});
|
||||
}
|
||||
|
|
|
@ -33,14 +33,18 @@
|
|||
<div class="reply-box__bottom">
|
||||
<ul class="tabs">
|
||||
<li class="tabs-title" :class="{ 'is-active': !isPrivate }">
|
||||
<a href="#" @click="makeReply">Reply</a>
|
||||
<a href="#" @click="makeReply">{{
|
||||
$t('CONVERSATION.REPLYBOX.REPLY')
|
||||
}}</a>
|
||||
</li>
|
||||
<li class="tabs-title is-private" :class="{ 'is-active': isPrivate }">
|
||||
<a href="#" @click="makePrivate">Private Note</a>
|
||||
<a href="#" @click="makePrivate">{{
|
||||
$t('CONVERSATION.REPLYBOX.PRIVATE_NOTE')
|
||||
}}</a>
|
||||
</li>
|
||||
<li v-if="message.length" class="tabs-title message-length">
|
||||
<a :class="{ 'message-error': message.length > 620 }">
|
||||
{{ message.length }} / 640
|
||||
<a :class="{ 'message-error': message.length > maxLength - 40 }">
|
||||
{{ message.length }} / {{ maxLength }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
@ -49,16 +53,12 @@
|
|||
class="button send-button"
|
||||
:disabled="disableButton()"
|
||||
:class="{
|
||||
disabled: message.length === 0 || message.length > 640,
|
||||
disabled: message.length === 0 || message.length > maxLength,
|
||||
warning: isPrivate,
|
||||
}"
|
||||
@click="sendMessage"
|
||||
>
|
||||
{{
|
||||
isPrivate
|
||||
? $t('CONVERSATION.REPLYBOX.CREATE')
|
||||
: $t('CONVERSATION.REPLYBOX.SEND')
|
||||
}}
|
||||
{{ replyButtonLabel }}
|
||||
<i
|
||||
class="icon"
|
||||
:class="{
|
||||
|
@ -82,6 +82,10 @@ import EmojiInput from '../emoji/EmojiInput';
|
|||
import CannedResponse from './CannedResponse';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
EmojiInput,
|
||||
CannedResponse,
|
||||
},
|
||||
mixins: [clickaway],
|
||||
data() {
|
||||
return {
|
||||
|
@ -103,10 +107,32 @@ export default {
|
|||
} = this.currentChat;
|
||||
return channel;
|
||||
},
|
||||
},
|
||||
components: {
|
||||
EmojiInput,
|
||||
CannedResponse,
|
||||
conversationType() {
|
||||
const {
|
||||
additional_attributes: additionalAttributes = {},
|
||||
} = this.currentChat;
|
||||
return additionalAttributes.type || '';
|
||||
},
|
||||
maxLength() {
|
||||
if (this.channelType === 'Channel::FacebookPage') {
|
||||
return 640;
|
||||
}
|
||||
if (this.channelType === 'Channel::TwitterProfile') {
|
||||
if (this.conversationType === 'tweet') {
|
||||
return 280;
|
||||
}
|
||||
}
|
||||
return 10000;
|
||||
},
|
||||
replyButtonLabel() {
|
||||
if (this.isPrivate) {
|
||||
return this.$t('CONVERSATION.REPLYBOX.CREATE');
|
||||
}
|
||||
if (this.conversationType === 'tweet') {
|
||||
return this.$t('CONVERSATION.REPLYBOX.TWEET');
|
||||
}
|
||||
return this.$t('CONVERSATION.REPLYBOX.SEND');
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
message(val) {
|
||||
|
|
|
@ -21,8 +21,11 @@
|
|||
"PRIVATE_MSG_INPUT": "Shift + enter for new line. This will be visible only to Agents"
|
||||
},
|
||||
"REPLYBOX": {
|
||||
"REPLY": "Reply",
|
||||
"PRIVATE_NOTE": "Private Note",
|
||||
"SEND": "Send",
|
||||
"CREATE": "Add Note"
|
||||
"CREATE": "Add Note",
|
||||
"TWEET": "Tweet"
|
||||
},
|
||||
"VISIBLE_TO_AGENTS": "Private Note: Only visible to you and your team",
|
||||
"CHANGE_STATUS": "Conversation status changed",
|
||||
|
|
|
@ -58,6 +58,7 @@ export default {
|
|||
this.initialize();
|
||||
this.$watch('$store.state.route', () => this.initialize());
|
||||
this.$watch('chatList.length', () => {
|
||||
this.fetchConversation();
|
||||
this.setActiveChat();
|
||||
});
|
||||
},
|
||||
|
@ -81,13 +82,28 @@ export default {
|
|||
break;
|
||||
default:
|
||||
this.$store.dispatch('setActiveInbox', null);
|
||||
this.$store.dispatch('clearSelectedState');
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
setActiveChat() {
|
||||
fetchConversation() {
|
||||
if (!this.conversationId) {
|
||||
return;
|
||||
}
|
||||
const chat = this.findConversation();
|
||||
if (!chat) {
|
||||
this.$store.dispatch('getConversation', this.conversationId);
|
||||
}
|
||||
},
|
||||
findConversation() {
|
||||
const conversationId = parseInt(this.conversationId, 10);
|
||||
const [chat] = this.chatList.filter(c => c.id === conversationId);
|
||||
return chat;
|
||||
},
|
||||
|
||||
setActiveChat() {
|
||||
const chat = this.findConversation();
|
||||
if (!chat) return;
|
||||
this.$store.dispatch('setActiveChat', chat).then(() => {
|
||||
bus.$emit('scrollToMessage');
|
||||
|
|
|
@ -28,7 +28,7 @@ export default {
|
|||
roles: ['administrator', 'agent'],
|
||||
component: ConversationView,
|
||||
props: route => {
|
||||
return { conversationId: route.params.conversation_id };
|
||||
return { inboxId: 0, conversationId: route.params.conversation_id };
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
@ -39,6 +39,9 @@
|
|||
<span v-if="item.channel_type === 'Channel::WebWidget'">
|
||||
Website
|
||||
</span>
|
||||
<span v-if="item.channel_type === 'Channel::TwitterProfile'">
|
||||
Twitter
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
<template>
|
||||
<div class="profile--settings--row row">
|
||||
<div class="columns small-3 ">
|
||||
<p class="section--title">
|
||||
<h4 class="block-title">
|
||||
{{ $t('PROFILE_SETTINGS.FORM.EMAIL_NOTIFICATIONS_SECTION.TITLE') }}
|
||||
</p>
|
||||
</h4>
|
||||
<p>{{ $t('PROFILE_SETTINGS.FORM.EMAIL_NOTIFICATIONS_SECTION.NOTE') }}</p>
|
||||
</div>
|
||||
<div class="columns small-9">
|
||||
|
|
|
@ -3,12 +3,12 @@
|
|||
<form @submit.prevent="updateUser">
|
||||
<div class="small-12 row profile--settings--row">
|
||||
<div class="columns small-3 ">
|
||||
<p class="section--title">
|
||||
<h4 class="block-title">
|
||||
{{ $t('PROFILE_SETTINGS.FORM.PROFILE_SECTION.TITLE') }}
|
||||
</p>
|
||||
</h4>
|
||||
<p>{{ $t('PROFILE_SETTINGS.FORM.PROFILE_SECTION.NOTE') }}</p>
|
||||
</div>
|
||||
<div class="columns small-9">
|
||||
<div class="columns small-9 medium-5">
|
||||
<label>
|
||||
{{ $t('PROFILE_SETTINGS.FORM.PROFILE_IMAGE.LABEL') }}
|
||||
<thumbnail size="80px" :src="avatarUrl"></thumbnail>
|
||||
|
@ -48,12 +48,12 @@
|
|||
</div>
|
||||
<div class="profile--settings--row row">
|
||||
<div class="columns small-3 ">
|
||||
<p class="section--title">
|
||||
<h4 class="block-title">
|
||||
{{ $t('PROFILE_SETTINGS.FORM.PASSWORD_SECTION.TITLE') }}
|
||||
</p>
|
||||
</h4>
|
||||
<p>{{ $t('PROFILE_SETTINGS.FORM.PASSWORD_SECTION.NOTE') }}</p>
|
||||
</div>
|
||||
<div class="columns small-9">
|
||||
<div class="columns small-9 medium-5">
|
||||
<label :class="{ error: $v.password.$error }">
|
||||
{{ $t('PROFILE_SETTINGS.FORM.PASSWORD.LABEL') }}
|
||||
<input
|
||||
|
@ -211,16 +211,12 @@ export default {
|
|||
|
||||
.profile--settings--row {
|
||||
@include border-normal-bottom;
|
||||
padding: 16px;
|
||||
padding: $space-normal;
|
||||
.small-3 {
|
||||
padding: 16px 16px 16px 0;
|
||||
padding: $space-normal $space-medium $space-normal 0;
|
||||
}
|
||||
.small-9 {
|
||||
padding: 16px;
|
||||
padding: $space-normal;
|
||||
}
|
||||
}
|
||||
|
||||
.section--title {
|
||||
color: $color-woot;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -7,6 +7,15 @@ import FBChannel from '../../../api/channel/fbChannel';
|
|||
|
||||
// actions
|
||||
const actions = {
|
||||
getConversation: async ({ commit }, conversationId) => {
|
||||
try {
|
||||
const response = await ConversationApi.show(conversationId);
|
||||
commit(types.default.ADD_CONVERSATION, response.data);
|
||||
} catch (error) {
|
||||
// Ignore error
|
||||
}
|
||||
},
|
||||
|
||||
fetchAllConversations: async ({ commit, dispatch }, params) => {
|
||||
commit(types.default.SET_LIST_LOADING_STATUS);
|
||||
try {
|
||||
|
|
|
@ -33,12 +33,13 @@ const getters = {
|
|||
getChatListLoadingStatus: ({ listLoadingStatus }) => listLoadingStatus,
|
||||
getAllMessagesLoaded(_state) {
|
||||
const [chat] = getSelectedChatConversation(_state);
|
||||
return chat.allMessagesLoaded === undefined
|
||||
return !chat || chat.allMessagesLoaded === undefined
|
||||
? false
|
||||
: chat.allMessagesLoaded;
|
||||
},
|
||||
getUnreadCount(_state) {
|
||||
const [chat] = getSelectedChatConversation(_state);
|
||||
if (!chat) return [];
|
||||
return chat.messages.filter(
|
||||
chatMessage =>
|
||||
chatMessage.created_at * 1000 > chat.agent_last_seen_at * 1000 &&
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
import axios from 'axios';
|
||||
import actions from '../../conversations/actions';
|
||||
import * as types from '../../../mutation-types';
|
||||
|
||||
const commit = jest.fn();
|
||||
global.axios = axios;
|
||||
jest.mock('axios');
|
||||
|
||||
describe('#actions', () => {
|
||||
describe('#getConversation', () => {
|
||||
it('sends correct actions if API is success', async () => {
|
||||
axios.get.mockResolvedValue({ data: { id: 1, meta: {} } });
|
||||
await actions.getConversation({ commit }, 1);
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.default.ADD_CONVERSATION, { id: 1, meta: {} }],
|
||||
]);
|
||||
});
|
||||
it('sends correct actions if API is error', async () => {
|
||||
axios.get.mockRejectedValue({ message: 'Incorrect header' });
|
||||
await actions.getConversation({ commit });
|
||||
expect(commit.mock.calls).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -6,6 +6,7 @@ class Conversations::EventDataPresenter < SimpleDelegator
|
|||
def push_data
|
||||
{
|
||||
id: display_id,
|
||||
additional_attributes: additional_attributes,
|
||||
inbox_id: inbox_id,
|
||||
messages: push_messages,
|
||||
meta: push_meta,
|
||||
|
|
|
@ -11,7 +11,9 @@ class Twitter::DirectMessageParserService < Twitter::WebhooksBaseService
|
|||
content: message_create_data['message_data']['text'],
|
||||
account_id: @inbox.account_id,
|
||||
inbox_id: @inbox.id,
|
||||
message_type: outgoing_message? ? :outgoing : :incoming
|
||||
message_type: outgoing_message? ? :outgoing : :incoming,
|
||||
contact_id: @contact.id,
|
||||
source_id: direct_message_data['id']
|
||||
)
|
||||
end
|
||||
|
||||
|
|
|
@ -39,10 +39,16 @@ class Twitter::SendReplyService
|
|||
end
|
||||
|
||||
def send_tweet_reply
|
||||
twitter_client.send_tweet_reply(
|
||||
response = twitter_client.send_tweet_reply(
|
||||
reply_to_tweet_id: conversation.additional_attributes['tweet_id'],
|
||||
tweet: screen_name + message.content
|
||||
)
|
||||
if response.status == '200'
|
||||
tweet_data = response.body
|
||||
message.update!(source_id: tweet_data['id_str'])
|
||||
else
|
||||
Rails.logger 'TWITTER_TWEET_REPLY_ERROR' + response.body
|
||||
end
|
||||
end
|
||||
|
||||
def send_reply
|
||||
|
|
|
@ -3,16 +3,9 @@ class Twitter::TweetParserService < Twitter::WebhooksBaseService
|
|||
|
||||
def perform
|
||||
set_inbox
|
||||
find_or_create_contact(user)
|
||||
set_conversation
|
||||
@conversation.messages.create(
|
||||
account_id: @inbox.account_id,
|
||||
contact_id: @contact.id,
|
||||
content: tweet_text,
|
||||
inbox_id: @inbox.id,
|
||||
message_type: message_type,
|
||||
source_id: tweet_data['id'].to_s
|
||||
)
|
||||
return if message_already_exist?
|
||||
|
||||
create_message
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -37,6 +30,10 @@ class Twitter::TweetParserService < Twitter::WebhooksBaseService
|
|||
tweet_data['user']
|
||||
end
|
||||
|
||||
def tweet_id
|
||||
tweet_data['id'].to_s
|
||||
end
|
||||
|
||||
def parent_tweet_id
|
||||
tweet_data['in_reply_to_status_id_str'].nil? ? tweet_data['id'].to_s : tweet_data['in_reply_to_status_id_str']
|
||||
end
|
||||
|
@ -66,4 +63,21 @@ class Twitter::TweetParserService < Twitter::WebhooksBaseService
|
|||
|
||||
@conversation = ::Conversation.create!(conversation_params)
|
||||
end
|
||||
|
||||
def message_already_exist?
|
||||
@inbox.messages.find_by(source_id: tweet_id)
|
||||
end
|
||||
|
||||
def create_message
|
||||
find_or_create_contact(user)
|
||||
set_conversation
|
||||
@conversation.messages.create(
|
||||
account_id: @inbox.account_id,
|
||||
contact_id: @contact.id,
|
||||
content: tweet_text,
|
||||
inbox_id: @inbox.id,
|
||||
message_type: message_type,
|
||||
source_id: tweet_id
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,26 +1,5 @@
|
|||
json.payload do
|
||||
json.array! @conversations do |conversation|
|
||||
json.meta do
|
||||
json.sender do
|
||||
json.id conversation.contact.id
|
||||
json.name conversation.contact.name
|
||||
json.thumbnail conversation.contact.avatar_url
|
||||
json.channel conversation.inbox.try(:channel_type)
|
||||
end
|
||||
json.assignee conversation.assignee
|
||||
end
|
||||
|
||||
json.id conversation.display_id
|
||||
if conversation.unread_incoming_messages.count.zero?
|
||||
json.messages [conversation.messages.last.try(:push_event_data)]
|
||||
else
|
||||
json.messages conversation.unread_messages.map(&:push_event_data)
|
||||
end
|
||||
json.inbox_id conversation.inbox_id
|
||||
json.status conversation.status
|
||||
json.timestamp conversation.messages.last.try(:created_at).try(:to_i)
|
||||
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.partial! 'api/v1/conversations/partials/conversation.json.jbuilder', conversation: conversation
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,31 +4,9 @@ json.data do
|
|||
json.unassigned_count @conversations_count[:unassigned_count]
|
||||
json.all_count @conversations_count[:all_count]
|
||||
end
|
||||
|
||||
json.payload do
|
||||
json.array! @conversations do |conversation|
|
||||
json.meta do
|
||||
json.sender do
|
||||
json.id conversation.contact.id
|
||||
json.name conversation.contact.name
|
||||
json.thumbnail conversation.contact.avatar_url
|
||||
json.channel conversation.inbox.try(:channel_type)
|
||||
end
|
||||
json.assignee conversation.assignee
|
||||
end
|
||||
|
||||
json.id conversation.display_id
|
||||
if conversation.unread_incoming_messages.count.zero?
|
||||
json.messages [conversation.messages.last.try(:push_event_data)]
|
||||
else
|
||||
json.messages conversation.unread_messages.map(&:push_event_data)
|
||||
end
|
||||
json.inbox_id conversation.inbox_id
|
||||
json.status conversation.status
|
||||
json.timestamp conversation.messages.last.try(:created_at).try(:to_i)
|
||||
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.partial! 'api/v1/conversations/partials/conversation.json.jbuilder', conversation: conversation
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
20
app/views/api/v1/conversations/messages/index.json.jbuilder
Normal file
20
app/views/api/v1/conversations/messages/index.json.jbuilder
Normal file
|
@ -0,0 +1,20 @@
|
|||
json.meta do
|
||||
json.labels @conversation.label_list
|
||||
json.additional_attributes @conversation.additional_attributes
|
||||
json.contact_id @conversation.contact_id
|
||||
end
|
||||
|
||||
json.payload do
|
||||
json.array! @messages do |message|
|
||||
json.id message.id
|
||||
json.content message.content
|
||||
json.inbox_id message.inbox_id
|
||||
json.conversation_id message.conversation.display_id
|
||||
json.message_type message.message_type_before_type_cast
|
||||
json.created_at message.created_at.to_i
|
||||
json.private message.private
|
||||
json.source_id message.source_id
|
||||
json.attachment message.attachment.push_event_data if message.attachment
|
||||
json.sender message.user.push_event_data if message.user
|
||||
end
|
||||
end
|
|
@ -0,0 +1,24 @@
|
|||
json.meta do
|
||||
json.sender do
|
||||
json.id conversation.contact.id
|
||||
json.name conversation.contact.name
|
||||
json.thumbnail conversation.contact.avatar_url
|
||||
json.channel conversation.inbox.try(:channel_type)
|
||||
end
|
||||
json.assignee conversation.assignee
|
||||
end
|
||||
|
||||
json.id conversation.display_id
|
||||
if conversation.unread_incoming_messages.count.zero?
|
||||
json.messages [conversation.messages.last.try(:push_event_data)]
|
||||
else
|
||||
json.messages conversation.unread_messages.map(&:push_event_data)
|
||||
end
|
||||
|
||||
json.inbox_id conversation.inbox_id
|
||||
json.status conversation.status
|
||||
json.timestamp conversation.messages.last.try(:created_at).try(:to_i)
|
||||
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
|
|
@ -1,20 +1 @@
|
|||
json.meta do
|
||||
json.labels @conversation.label_list
|
||||
json.additional_attributes @conversation.additional_attributes
|
||||
json.contact_id @conversation.contact_id
|
||||
end
|
||||
|
||||
json.payload do
|
||||
json.array! @messages do |message|
|
||||
json.id message.id
|
||||
json.content message.content
|
||||
json.inbox_id message.inbox_id
|
||||
json.conversation_id message.conversation.display_id
|
||||
json.message_type message.message_type_before_type_cast
|
||||
json.created_at message.created_at.to_i
|
||||
json.private message.private
|
||||
json.source_id message.source_id
|
||||
json.attachment message.attachment.push_event_data if message.attachment
|
||||
json.sender message.user.push_event_data if message.user
|
||||
end
|
||||
end
|
||||
json.partial! 'api/v1/conversations/partials/conversation.json.jbuilder', conversation: @conversation
|
||||
|
|
|
@ -73,8 +73,8 @@ Rails.application.routes.draw do
|
|||
end
|
||||
|
||||
resources :conversations, only: [:index, :show] do
|
||||
scope module: :conversations do # for nested controller
|
||||
resources :messages, only: [:create]
|
||||
scope module: :conversations do
|
||||
resources :messages, only: [:index, :create]
|
||||
resources :assignments, only: [:create]
|
||||
resources :labels, only: [:create, :index]
|
||||
end
|
||||
|
|
61
docs/webhooks/add-webhooks-to-chatwoot.md
Normal file
61
docs/webhooks/add-webhooks-to-chatwoot.md
Normal file
|
@ -0,0 +1,61 @@
|
|||
---
|
||||
path: "/docs/webhooks"
|
||||
title: "Webhooks"
|
||||
---
|
||||
|
||||
Webhooks are HTTP callbacks which can be defined for every account. They are triggered by events like message creation in Chatwoot. You can create more than one webhook for an account. Currently webhooks support only `message_created` event
|
||||
|
||||
**Step 1**. Click on Integrations link is settings sidebar. Click on "Configure" button.
|
||||
|
||||
![integrations](./images/integrations.png)
|
||||
|
||||
**Step 2**. You will see the list of webhooks you have already added to the account.
|
||||
|
||||
![configure](./images/configure.png)
|
||||
|
||||
**Step 3**. Click on the "Add new webhook", it will display a modal where you can input the URL to which the POST request should be sent.
|
||||
|
||||
![add-a-webhook](./images/add-a-webhook.png)
|
||||
|
||||
Chatwoot currently supports webhooks for message creation only. Once a new message is created in the any of the inboxes of the account, it will send a POST request with the following payload to the configured URLs.
|
||||
|
||||
### A sample webhook payload
|
||||
|
||||
```json
|
||||
{
|
||||
|
||||
"event": "message_created", // The name of the event
|
||||
"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.
|
||||
"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",
|
||||
"name": "Agent",
|
||||
"email": "agent@example.com"
|
||||
},
|
||||
"contact": { // This would provide the details of the user who sent this message
|
||||
"id": "1",
|
||||
"name": "contact-name"
|
||||
},
|
||||
"conversation": { // This would provide the details of the conversation
|
||||
"display_id": "1", // This is the ID of the conversation which you can see in the dashboard.
|
||||
"additional_attributes": {
|
||||
"browser": {
|
||||
"device_name": "Macbook",
|
||||
"browser_name": "Chrome",
|
||||
"platform_name": "Macintosh",
|
||||
"browser_version": "80.0.3987.122",
|
||||
"platform_version": "10.15.2"
|
||||
},
|
||||
"referer": "http://www.chatwoot.com",
|
||||
"initiated_at": "Tue Mar 03 2020 18:37:38 GMT-0700 (Mountain Standard Time)"
|
||||
}
|
||||
},
|
||||
"account": { // This would provide the details of the account
|
||||
"id": "1",
|
||||
"name": "Chatwoot",
|
||||
}
|
||||
}
|
||||
```
|
BIN
docs/webhooks/images/add-a-webhook.png
Normal file
BIN
docs/webhooks/images/add-a-webhook.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 163 KiB |
BIN
docs/webhooks/images/configure.png
Normal file
BIN
docs/webhooks/images/configure.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 284 KiB |
BIN
docs/webhooks/images/integrations.png
Normal file
BIN
docs/webhooks/images/integrations.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 255 KiB |
|
@ -31,4 +31,29 @@ RSpec.describe 'Conversation Messages API', type: :request do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /api/v1/conversations/:id/messages' do
|
||||
let(:conversation) { create(:conversation, account: account) }
|
||||
|
||||
context 'when it is an unauthenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
get "/api/v1/conversations/#{conversation.display_id}/messages"
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
let(:agent) { create(:user, account: account, role: :agent) }
|
||||
|
||||
it 'shows the conversation' do
|
||||
get "/api/v1/conversations/#{conversation.display_id}/messages",
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(JSON.parse(response.body, symbolize_names: true)[:meta][:contact_id]).to eq(conversation.contact_id)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -51,7 +51,7 @@ RSpec.describe 'Conversations API', type: :request do
|
|||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(JSON.parse(response.body, symbolize_names: true)[:meta][:contact_id]).to eq(conversation.contact_id)
|
||||
expect(JSON.parse(response.body, symbolize_names: true)[:id]).to eq(conversation.display_id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -226,6 +226,7 @@ RSpec.describe Conversation, type: :model do
|
|||
let(:conversation) { create(:conversation) }
|
||||
let(:expected_data) do
|
||||
{
|
||||
additional_attributes: nil,
|
||||
meta: {
|
||||
sender: conversation.contact.push_event_data,
|
||||
assignee: conversation.assignee
|
||||
|
|
|
@ -13,6 +13,7 @@ RSpec.describe Conversations::EventDataPresenter do
|
|||
describe '#push_data' do
|
||||
let(:expected_data) do
|
||||
{
|
||||
additional_attributes: nil,
|
||||
meta: {
|
||||
sender: conversation.contact.push_event_data,
|
||||
assignee: conversation.assignee
|
||||
|
|
|
@ -4,6 +4,7 @@ describe Twitter::SendReplyService do
|
|||
subject(:send_reply_service) { described_class.new(message: message) }
|
||||
|
||||
let(:twitter_client) { instance_double(::Twitty::Facade) }
|
||||
let(:twitter_response) { instance_double(::Twitty::Response) }
|
||||
let(:account) { create(:account) }
|
||||
let(:widget_inbox) { create(:inbox, account: account) }
|
||||
let(:twitter_channel) { create(:channel_twitter_profile, account: account) }
|
||||
|
@ -32,7 +33,9 @@ describe Twitter::SendReplyService do
|
|||
before do
|
||||
allow(::Twitty::Facade).to receive(:new).and_return(twitter_client)
|
||||
allow(twitter_client).to receive(:send_direct_message).and_return(true)
|
||||
allow(twitter_client).to receive(:send_tweet_reply).and_return(true)
|
||||
allow(twitter_client).to receive(:send_tweet_reply).and_return(twitter_response)
|
||||
allow(twitter_response).to receive(:status).and_return('200')
|
||||
allow(twitter_response).to receive(:body).and_return(JSON.parse({ id_str: '12345' }.to_json))
|
||||
end
|
||||
|
||||
describe '#perform' do
|
||||
|
@ -61,14 +64,15 @@ describe Twitter::SendReplyService do
|
|||
context 'with reply' do
|
||||
it 'if conversation is a direct message' do
|
||||
create(:message, message_type: :incoming, inbox: twitter_inbox, account: account, conversation: dm_conversation)
|
||||
create(:message, message_type: 'outgoing', inbox: twitter_inbox, account: account, conversation: dm_conversation)
|
||||
create(:message, message_type: :outgoing, inbox: twitter_inbox, account: account, conversation: dm_conversation)
|
||||
expect(twitter_client).to have_received(:send_direct_message)
|
||||
end
|
||||
|
||||
it 'if conversation is a tweet' do
|
||||
create(:message, message_type: :incoming, inbox: twitter_inbox, account: account, conversation: tweet_conversation)
|
||||
create(:message, message_type: 'outgoing', inbox: twitter_inbox, account: account, conversation: tweet_conversation)
|
||||
tweet = create(:message, message_type: :outgoing, inbox: twitter_inbox, account: account, conversation: tweet_conversation)
|
||||
expect(twitter_client).to have_received(:send_tweet_reply)
|
||||
expect(tweet.reload.source_id).to eq '12345'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
get:
|
||||
tags: [Contact]
|
||||
tags:
|
||||
- Contact
|
||||
operationId: contactConversations
|
||||
summary: Conversations
|
||||
parameters:
|
||||
- name: id
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
get:
|
||||
tags: [Contact]
|
||||
tags:
|
||||
- Contact
|
||||
operationId: contactDetails
|
||||
summary: Show Contact
|
||||
parameters:
|
||||
- name: id
|
||||
|
@ -18,7 +20,9 @@ get:
|
|||
description: Access denied
|
||||
|
||||
put:
|
||||
tags: [Contact]
|
||||
tags:
|
||||
- Contact
|
||||
operationId: contactUpdate
|
||||
summary: Update Contact
|
||||
parameters:
|
||||
- name: id
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
get:
|
||||
tags: [Contact]
|
||||
tags:
|
||||
- Contact
|
||||
operationId: contactList
|
||||
description: Listing all the contacts with pagination
|
||||
summary: List Contacts
|
||||
parameters:
|
||||
|
@ -17,7 +19,9 @@ get:
|
|||
$ref: '#/definitions/bad_request_error'
|
||||
|
||||
post:
|
||||
tags: [Contact]
|
||||
tags:
|
||||
- Contact
|
||||
operationId: contactCreate
|
||||
description: Create New Contact
|
||||
parameters:
|
||||
- name: data
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
post:
|
||||
tags: [Conversation]
|
||||
tags:
|
||||
- Conversation
|
||||
operationId: conversationAssignment
|
||||
summary: Assign Conversation
|
||||
description: Assign a conversation to an agent
|
||||
parameters:
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
get:
|
||||
tags: [Conversation]
|
||||
tags:
|
||||
- Conversation
|
||||
operationId: conversationDetails
|
||||
summary: Conversation Details
|
||||
description: Get all details regarding a conversation with all messages in the conversation
|
||||
parameters:
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
get:
|
||||
tags: [Conversation]
|
||||
tags:
|
||||
- Conversation
|
||||
operationId: conversationLabelsList
|
||||
summary: List Labels
|
||||
description: Lists all the labels of a conversation
|
||||
parameters:
|
||||
|
@ -19,9 +21,11 @@ get:
|
|||
description: Access denied
|
||||
|
||||
post:
|
||||
tags: [Conversation]
|
||||
summary: Add Label
|
||||
description: Creates a new label and associates it with the conversation
|
||||
tags:
|
||||
- Conversation
|
||||
operationId: conversationAddLabels
|
||||
summary: Add Labels
|
||||
description: Creates new labels and associates it with the conversation
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
get:
|
||||
tags: [Conversation]
|
||||
tags:
|
||||
- Conversation
|
||||
operationId: conversationList
|
||||
description: List all the conversations with pagination
|
||||
summary: Conversations List
|
||||
parameters:
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
post:
|
||||
tags: [Conversation]
|
||||
tags:
|
||||
- Conversation
|
||||
operationId: conversationNewMessage
|
||||
summary: Create New Message
|
||||
description: All the agent replies are created as new messages through this endpoint
|
||||
parameters:
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
post:
|
||||
tags: [Conversation]
|
||||
tags:
|
||||
- Conversation
|
||||
operationId: conversationToggleStatus
|
||||
summary: Toggle Status
|
||||
description: Toggles the status of the conversation between open and resolved
|
||||
parameters:
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
post:
|
||||
tags: [Conversation]
|
||||
tags:
|
||||
- Conversation
|
||||
operationId: conversationUpdateLastSeen
|
||||
summary: Update Last Seen
|
||||
description: Updates the last seen of the conversation so that conversations will have the bubbles in the agents screen
|
||||
parameters:
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
"tags": [
|
||||
"Contact"
|
||||
],
|
||||
"operationId": "contactList",
|
||||
"description": "Listing all the contacts with pagination",
|
||||
"summary": "List Contacts",
|
||||
"parameters": [
|
||||
|
@ -49,6 +50,7 @@
|
|||
"tags": [
|
||||
"Contact"
|
||||
],
|
||||
"operationId": "contactCreate",
|
||||
"description": "Create New Contact",
|
||||
"parameters": [
|
||||
{
|
||||
|
@ -81,6 +83,7 @@
|
|||
"tags": [
|
||||
"Contact"
|
||||
],
|
||||
"operationId": "contactDetails",
|
||||
"summary": "Show Contact",
|
||||
"parameters": [
|
||||
{
|
||||
|
@ -110,6 +113,7 @@
|
|||
"tags": [
|
||||
"Contact"
|
||||
],
|
||||
"operationId": "contactUpdate",
|
||||
"summary": "Update Contact",
|
||||
"parameters": [
|
||||
{
|
||||
|
@ -149,6 +153,7 @@
|
|||
"tags": [
|
||||
"Contact"
|
||||
],
|
||||
"operationId": "contactConversations",
|
||||
"summary": "Conversations",
|
||||
"parameters": [
|
||||
{
|
||||
|
@ -180,6 +185,7 @@
|
|||
"tags": [
|
||||
"Conversation"
|
||||
],
|
||||
"operationId": "conversationList",
|
||||
"description": "List all the conversations with pagination",
|
||||
"summary": "Conversations List",
|
||||
"parameters": [
|
||||
|
@ -210,6 +216,7 @@
|
|||
"tags": [
|
||||
"Conversation"
|
||||
],
|
||||
"operationId": "conversationDetails",
|
||||
"summary": "Conversation Details",
|
||||
"description": "Get all details regarding a conversation with all messages in the conversation",
|
||||
"parameters": [
|
||||
|
@ -242,6 +249,7 @@
|
|||
"tags": [
|
||||
"Conversation"
|
||||
],
|
||||
"operationId": "conversationToggleStatus",
|
||||
"summary": "Toggle Status",
|
||||
"description": "Toggles the status of the conversation between open and resolved",
|
||||
"parameters": [
|
||||
|
@ -274,6 +282,7 @@
|
|||
"tags": [
|
||||
"Conversation"
|
||||
],
|
||||
"operationId": "conversationUpdateLastSeen",
|
||||
"summary": "Update Last Seen",
|
||||
"description": "Updates the last seen of the conversation so that conversations will have the bubbles in the agents screen",
|
||||
"parameters": [
|
||||
|
@ -303,6 +312,7 @@
|
|||
"tags": [
|
||||
"Conversation"
|
||||
],
|
||||
"operationId": "conversationLabelsList",
|
||||
"summary": "List Labels",
|
||||
"description": "Lists all the labels of a conversation",
|
||||
"parameters": [
|
||||
|
@ -333,8 +343,9 @@
|
|||
"tags": [
|
||||
"Conversation"
|
||||
],
|
||||
"summary": "Add Label",
|
||||
"description": "Creates a new label and associates it with the conversation",
|
||||
"operationId": "conversationAddLabels",
|
||||
"summary": "Add Labels",
|
||||
"description": "Creates new labels and associates it with the conversation",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
|
@ -382,6 +393,7 @@
|
|||
"tags": [
|
||||
"Conversation"
|
||||
],
|
||||
"operationId": "conversationAssignment",
|
||||
"summary": "Assign Conversation",
|
||||
"description": "Assign a conversation to an agent",
|
||||
"parameters": [
|
||||
|
@ -427,6 +439,7 @@
|
|||
"tags": [
|
||||
"Conversation"
|
||||
],
|
||||
"operationId": "conversationNewMessage",
|
||||
"summary": "Create New Message",
|
||||
"description": "All the agent replies are created as new messages through this endpoint",
|
||||
"parameters": [
|
||||
|
|
Loading…
Reference in a new issue