Merge branch 'release/1.2.1'

This commit is contained in:
Pranav Raj Sreepuram 2020-03-08 22:15:04 +05:30
commit 1273ea64d3
46 changed files with 367 additions and 145 deletions

4
.gitignore vendored
View File

@ -52,4 +52,6 @@ coverage
# ignore packages # ignore packages
node_modules node_modules
package-lock.json package-lock.json
*.dump

View File

@ -293,7 +293,7 @@ GEM
pry-rails (0.3.9) pry-rails (0.3.9)
pry (>= 0.10.4) pry (>= 0.10.4)
public_suffix (4.0.3) public_suffix (4.0.3)
puma (4.3.2) puma (4.3.3)
nio4r (~> 2.0) nio4r (~> 2.0)
pundit (2.1.0) pundit (2.1.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)

View File

@ -1,8 +1,18 @@
class Api::V1::Conversations::MessagesController < Api::BaseController 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 def create
mb = Messages::Outgoing::NormalBuilder.new(current_user, @conversation, params) mb = Messages::Outgoing::NormalBuilder.new(current_user, @conversation, params)
@message = mb.perform @message = mb.perform
end end
private
def message_finder
@message_finder ||= MessageFinder.new(@conversation, params)
end
end end

View File

@ -7,9 +7,7 @@ class Api::V1::ConversationsController < Api::BaseController
@conversations_count = result[:count] @conversations_count = result[:count]
end end
def show def show; end
@messages = messages_finder.perform
end
def toggle_status def toggle_status
@status = @conversation.toggle_status @status = @conversation.toggle_status
@ -34,8 +32,4 @@ class Api::V1::ConversationsController < Api::BaseController
def conversation_finder def conversation_finder
@conversation_finder ||= ConversationFinder.new(current_user, params) @conversation_finder ||= ConversationFinder.new(current_user, params)
end end
def messages_finder
@message_finder ||= MessageFinder.new(@conversation, params)
end
end end

View File

@ -1,5 +1,7 @@
class Twitter::CallbacksController < Twitter::BaseController class Twitter::CallbacksController < Twitter::BaseController
def show def show
return redirect_to app_new_twitter_inbox_url if permitted_params[:denied]
@response = twitter_client.access_token( @response = twitter_client.access_token(
oauth_token: permitted_params[:oauth_token], oauth_token: permitted_params[:oauth_token],
oauth_verifier: permitted_params[:oauth_verifier] oauth_verifier: permitted_params[:oauth_verifier]
@ -46,6 +48,6 @@ class Twitter::CallbacksController < Twitter::BaseController
end end
def permitted_params def permitted_params
params.permit(:oauth_token, :oauth_verifier) params.permit(:oauth_token, :oauth_verifier, :denied)
end end
end end

View File

@ -15,7 +15,7 @@ class MessageApi extends ApiClient {
} }
getPreviousMessages({ conversationId, before }) { getPreviousMessages({ conversationId, before }) {
return axios.get(`${this.url}/${conversationId}`, { return axios.get(`${this.url}/${conversationId}/messages`, {
params: { before }, params: { before },
}); });
} }

View File

@ -33,14 +33,18 @@
<div class="reply-box__bottom"> <div class="reply-box__bottom">
<ul class="tabs"> <ul class="tabs">
<li class="tabs-title" :class="{ 'is-active': !isPrivate }"> <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>
<li class="tabs-title is-private" :class="{ 'is-active': isPrivate }"> <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>
<li v-if="message.length" class="tabs-title message-length"> <li v-if="message.length" class="tabs-title message-length">
<a :class="{ 'message-error': message.length > 620 }"> <a :class="{ 'message-error': message.length > maxLength - 40 }">
{{ message.length }} / 640 {{ message.length }} / {{ maxLength }}
</a> </a>
</li> </li>
</ul> </ul>
@ -49,16 +53,12 @@
class="button send-button" class="button send-button"
:disabled="disableButton()" :disabled="disableButton()"
:class="{ :class="{
disabled: message.length === 0 || message.length > 640, disabled: message.length === 0 || message.length > maxLength,
warning: isPrivate, warning: isPrivate,
}" }"
@click="sendMessage" @click="sendMessage"
> >
{{ {{ replyButtonLabel }}
isPrivate
? $t('CONVERSATION.REPLYBOX.CREATE')
: $t('CONVERSATION.REPLYBOX.SEND')
}}
<i <i
class="icon" class="icon"
:class="{ :class="{
@ -82,6 +82,10 @@ import EmojiInput from '../emoji/EmojiInput';
import CannedResponse from './CannedResponse'; import CannedResponse from './CannedResponse';
export default { export default {
components: {
EmojiInput,
CannedResponse,
},
mixins: [clickaway], mixins: [clickaway],
data() { data() {
return { return {
@ -103,10 +107,32 @@ export default {
} = this.currentChat; } = this.currentChat;
return channel; return channel;
}, },
}, conversationType() {
components: { const {
EmojiInput, additional_attributes: additionalAttributes = {},
CannedResponse, } = 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: { watch: {
message(val) { message(val) {

View File

@ -21,8 +21,11 @@
"PRIVATE_MSG_INPUT": "Shift + enter for new line. This will be visible only to Agents" "PRIVATE_MSG_INPUT": "Shift + enter for new line. This will be visible only to Agents"
}, },
"REPLYBOX": { "REPLYBOX": {
"REPLY": "Reply",
"PRIVATE_NOTE": "Private Note",
"SEND": "Send", "SEND": "Send",
"CREATE": "Add Note" "CREATE": "Add Note",
"TWEET": "Tweet"
}, },
"VISIBLE_TO_AGENTS": "Private Note: Only visible to you and your team", "VISIBLE_TO_AGENTS": "Private Note: Only visible to you and your team",
"CHANGE_STATUS": "Conversation status changed", "CHANGE_STATUS": "Conversation status changed",

View File

@ -58,6 +58,7 @@ export default {
this.initialize(); this.initialize();
this.$watch('$store.state.route', () => this.initialize()); this.$watch('$store.state.route', () => this.initialize());
this.$watch('chatList.length', () => { this.$watch('chatList.length', () => {
this.fetchConversation();
this.setActiveChat(); this.setActiveChat();
}); });
}, },
@ -81,13 +82,28 @@ export default {
break; break;
default: default:
this.$store.dispatch('setActiveInbox', null); this.$store.dispatch('setActiveInbox', null);
this.$store.dispatch('clearSelectedState');
break; 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 conversationId = parseInt(this.conversationId, 10);
const [chat] = this.chatList.filter(c => c.id === conversationId); const [chat] = this.chatList.filter(c => c.id === conversationId);
return chat;
},
setActiveChat() {
const chat = this.findConversation();
if (!chat) return; if (!chat) return;
this.$store.dispatch('setActiveChat', chat).then(() => { this.$store.dispatch('setActiveChat', chat).then(() => {
bus.$emit('scrollToMessage'); bus.$emit('scrollToMessage');

View File

@ -28,7 +28,7 @@ export default {
roles: ['administrator', 'agent'], roles: ['administrator', 'agent'],
component: ConversationView, component: ConversationView,
props: route => { props: route => {
return { conversationId: route.params.conversation_id }; return { inboxId: 0, conversationId: route.params.conversation_id };
}, },
}, },
{ {

View File

@ -39,6 +39,9 @@
<span v-if="item.channel_type === 'Channel::WebWidget'"> <span v-if="item.channel_type === 'Channel::WebWidget'">
Website Website
</span> </span>
<span v-if="item.channel_type === 'Channel::TwitterProfile'">
Twitter
</span>
</td> </td>
<!-- Action Buttons --> <!-- Action Buttons -->

View File

@ -1,9 +1,9 @@
<template> <template>
<div class="profile--settings--row row"> <div class="profile--settings--row row">
<div class="columns small-3 "> <div class="columns small-3 ">
<p class="section--title"> <h4 class="block-title">
{{ $t('PROFILE_SETTINGS.FORM.EMAIL_NOTIFICATIONS_SECTION.TITLE') }} {{ $t('PROFILE_SETTINGS.FORM.EMAIL_NOTIFICATIONS_SECTION.TITLE') }}
</p> </h4>
<p>{{ $t('PROFILE_SETTINGS.FORM.EMAIL_NOTIFICATIONS_SECTION.NOTE') }}</p> <p>{{ $t('PROFILE_SETTINGS.FORM.EMAIL_NOTIFICATIONS_SECTION.NOTE') }}</p>
</div> </div>
<div class="columns small-9"> <div class="columns small-9">

View File

@ -3,12 +3,12 @@
<form @submit.prevent="updateUser"> <form @submit.prevent="updateUser">
<div class="small-12 row profile--settings--row"> <div class="small-12 row profile--settings--row">
<div class="columns small-3 "> <div class="columns small-3 ">
<p class="section--title"> <h4 class="block-title">
{{ $t('PROFILE_SETTINGS.FORM.PROFILE_SECTION.TITLE') }} {{ $t('PROFILE_SETTINGS.FORM.PROFILE_SECTION.TITLE') }}
</p> </h4>
<p>{{ $t('PROFILE_SETTINGS.FORM.PROFILE_SECTION.NOTE') }}</p> <p>{{ $t('PROFILE_SETTINGS.FORM.PROFILE_SECTION.NOTE') }}</p>
</div> </div>
<div class="columns small-9"> <div class="columns small-9 medium-5">
<label> <label>
{{ $t('PROFILE_SETTINGS.FORM.PROFILE_IMAGE.LABEL') }} {{ $t('PROFILE_SETTINGS.FORM.PROFILE_IMAGE.LABEL') }}
<thumbnail size="80px" :src="avatarUrl"></thumbnail> <thumbnail size="80px" :src="avatarUrl"></thumbnail>
@ -48,12 +48,12 @@
</div> </div>
<div class="profile--settings--row row"> <div class="profile--settings--row row">
<div class="columns small-3 "> <div class="columns small-3 ">
<p class="section--title"> <h4 class="block-title">
{{ $t('PROFILE_SETTINGS.FORM.PASSWORD_SECTION.TITLE') }} {{ $t('PROFILE_SETTINGS.FORM.PASSWORD_SECTION.TITLE') }}
</p> </h4>
<p>{{ $t('PROFILE_SETTINGS.FORM.PASSWORD_SECTION.NOTE') }}</p> <p>{{ $t('PROFILE_SETTINGS.FORM.PASSWORD_SECTION.NOTE') }}</p>
</div> </div>
<div class="columns small-9"> <div class="columns small-9 medium-5">
<label :class="{ error: $v.password.$error }"> <label :class="{ error: $v.password.$error }">
{{ $t('PROFILE_SETTINGS.FORM.PASSWORD.LABEL') }} {{ $t('PROFILE_SETTINGS.FORM.PASSWORD.LABEL') }}
<input <input
@ -211,16 +211,12 @@ export default {
.profile--settings--row { .profile--settings--row {
@include border-normal-bottom; @include border-normal-bottom;
padding: 16px; padding: $space-normal;
.small-3 { .small-3 {
padding: 16px 16px 16px 0; padding: $space-normal $space-medium $space-normal 0;
} }
.small-9 { .small-9 {
padding: 16px; padding: $space-normal;
} }
} }
.section--title {
color: $color-woot;
}
</style> </style>

View File

@ -7,6 +7,15 @@ import FBChannel from '../../../api/channel/fbChannel';
// actions // actions
const 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) => { fetchAllConversations: async ({ commit, dispatch }, params) => {
commit(types.default.SET_LIST_LOADING_STATUS); commit(types.default.SET_LIST_LOADING_STATUS);
try { try {

View File

@ -33,12 +33,13 @@ const getters = {
getChatListLoadingStatus: ({ listLoadingStatus }) => listLoadingStatus, getChatListLoadingStatus: ({ listLoadingStatus }) => listLoadingStatus,
getAllMessagesLoaded(_state) { getAllMessagesLoaded(_state) {
const [chat] = getSelectedChatConversation(_state); const [chat] = getSelectedChatConversation(_state);
return chat.allMessagesLoaded === undefined return !chat || chat.allMessagesLoaded === undefined
? false ? false
: chat.allMessagesLoaded; : chat.allMessagesLoaded;
}, },
getUnreadCount(_state) { getUnreadCount(_state) {
const [chat] = getSelectedChatConversation(_state); const [chat] = getSelectedChatConversation(_state);
if (!chat) return [];
return chat.messages.filter( return chat.messages.filter(
chatMessage => chatMessage =>
chatMessage.created_at * 1000 > chat.agent_last_seen_at * 1000 && chatMessage.created_at * 1000 > chat.agent_last_seen_at * 1000 &&

View File

@ -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([]);
});
});
});

View File

@ -6,6 +6,7 @@ class Conversations::EventDataPresenter < SimpleDelegator
def push_data def push_data
{ {
id: display_id, id: display_id,
additional_attributes: additional_attributes,
inbox_id: inbox_id, inbox_id: inbox_id,
messages: push_messages, messages: push_messages,
meta: push_meta, meta: push_meta,

View File

@ -11,7 +11,9 @@ class Twitter::DirectMessageParserService < Twitter::WebhooksBaseService
content: message_create_data['message_data']['text'], content: message_create_data['message_data']['text'],
account_id: @inbox.account_id, account_id: @inbox.account_id,
inbox_id: @inbox.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 end

View File

@ -39,10 +39,16 @@ class Twitter::SendReplyService
end end
def send_tweet_reply def send_tweet_reply
twitter_client.send_tweet_reply( response = twitter_client.send_tweet_reply(
reply_to_tweet_id: conversation.additional_attributes['tweet_id'], reply_to_tweet_id: conversation.additional_attributes['tweet_id'],
tweet: screen_name + message.content 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 end
def send_reply def send_reply

View File

@ -3,16 +3,9 @@ class Twitter::TweetParserService < Twitter::WebhooksBaseService
def perform def perform
set_inbox set_inbox
find_or_create_contact(user) return if message_already_exist?
set_conversation
@conversation.messages.create( create_message
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
)
end end
private private
@ -37,6 +30,10 @@ class Twitter::TweetParserService < Twitter::WebhooksBaseService
tweet_data['user'] tweet_data['user']
end end
def tweet_id
tweet_data['id'].to_s
end
def parent_tweet_id 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'] tweet_data['in_reply_to_status_id_str'].nil? ? tweet_data['id'].to_s : tweet_data['in_reply_to_status_id_str']
end end
@ -66,4 +63,21 @@ class Twitter::TweetParserService < Twitter::WebhooksBaseService
@conversation = ::Conversation.create!(conversation_params) @conversation = ::Conversation.create!(conversation_params)
end 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 end

View File

@ -1,26 +1,5 @@
json.payload do json.payload do
json.array! @conversations do |conversation| json.array! @conversations do |conversation|
json.meta do json.partial! 'api/v1/conversations/partials/conversation.json.jbuilder', conversation: conversation
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
end end
end end

View File

@ -4,31 +4,9 @@ json.data do
json.unassigned_count @conversations_count[:unassigned_count] json.unassigned_count @conversations_count[:unassigned_count]
json.all_count @conversations_count[:all_count] json.all_count @conversations_count[:all_count]
end end
json.payload do json.payload do
json.array! @conversations do |conversation| json.array! @conversations do |conversation|
json.meta do json.partial! 'api/v1/conversations/partials/conversation.json.jbuilder', conversation: conversation
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
end end
end end
end end

View 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

View File

@ -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

View File

@ -1,20 +1 @@
json.meta do json.partial! 'api/v1/conversations/partials/conversation.json.jbuilder', conversation: @conversation
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

View File

@ -73,8 +73,8 @@ Rails.application.routes.draw do
end end
resources :conversations, only: [:index, :show] do resources :conversations, only: [:index, :show] do
scope module: :conversations do # for nested controller scope module: :conversations do
resources :messages, only: [:create] resources :messages, only: [:index, :create]
resources :assignments, only: [:create] resources :assignments, only: [:create]
resources :labels, only: [:create, :index] resources :labels, only: [:create, :index]
end end

View 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",
}
}
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 284 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 255 KiB

View File

@ -31,4 +31,29 @@ RSpec.describe 'Conversation Messages API', type: :request do
end end
end 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 end

View File

@ -51,7 +51,7 @@ RSpec.describe 'Conversations API', type: :request do
as: :json as: :json
expect(response).to have_http_status(:success) 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 end
end end

View File

@ -226,6 +226,7 @@ RSpec.describe Conversation, type: :model do
let(:conversation) { create(:conversation) } let(:conversation) { create(:conversation) }
let(:expected_data) do let(:expected_data) do
{ {
additional_attributes: nil,
meta: { meta: {
sender: conversation.contact.push_event_data, sender: conversation.contact.push_event_data,
assignee: conversation.assignee assignee: conversation.assignee

View File

@ -13,6 +13,7 @@ RSpec.describe Conversations::EventDataPresenter do
describe '#push_data' do describe '#push_data' do
let(:expected_data) do let(:expected_data) do
{ {
additional_attributes: nil,
meta: { meta: {
sender: conversation.contact.push_event_data, sender: conversation.contact.push_event_data,
assignee: conversation.assignee assignee: conversation.assignee

View File

@ -4,6 +4,7 @@ describe Twitter::SendReplyService do
subject(:send_reply_service) { described_class.new(message: message) } subject(:send_reply_service) { described_class.new(message: message) }
let(:twitter_client) { instance_double(::Twitty::Facade) } let(:twitter_client) { instance_double(::Twitty::Facade) }
let(:twitter_response) { instance_double(::Twitty::Response) }
let(:account) { create(:account) } let(:account) { create(:account) }
let(:widget_inbox) { create(:inbox, account: account) } let(:widget_inbox) { create(:inbox, account: account) }
let(:twitter_channel) { create(:channel_twitter_profile, account: account) } let(:twitter_channel) { create(:channel_twitter_profile, account: account) }
@ -32,7 +33,9 @@ describe Twitter::SendReplyService do
before do before do
allow(::Twitty::Facade).to receive(:new).and_return(twitter_client) 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_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 end
describe '#perform' do describe '#perform' do
@ -61,14 +64,15 @@ describe Twitter::SendReplyService do
context 'with reply' do context 'with reply' do
it 'if conversation is a direct message' 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: :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) expect(twitter_client).to have_received(:send_direct_message)
end end
it 'if conversation is a tweet' do it 'if conversation is a tweet' do
create(:message, message_type: :incoming, inbox: twitter_inbox, account: account, conversation: tweet_conversation) 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(twitter_client).to have_received(:send_tweet_reply)
expect(tweet.reload.source_id).to eq '12345'
end end
end end
end end

View File

@ -1,5 +1,7 @@
get: get:
tags: [Contact] tags:
- Contact
operationId: contactConversations
summary: Conversations summary: Conversations
parameters: parameters:
- name: id - name: id

View File

@ -1,5 +1,7 @@
get: get:
tags: [Contact] tags:
- Contact
operationId: contactDetails
summary: Show Contact summary: Show Contact
parameters: parameters:
- name: id - name: id
@ -18,7 +20,9 @@ get:
description: Access denied description: Access denied
put: put:
tags: [Contact] tags:
- Contact
operationId: contactUpdate
summary: Update Contact summary: Update Contact
parameters: parameters:
- name: id - name: id

View File

@ -1,5 +1,7 @@
get: get:
tags: [Contact] tags:
- Contact
operationId: contactList
description: Listing all the contacts with pagination description: Listing all the contacts with pagination
summary: List Contacts summary: List Contacts
parameters: parameters:
@ -17,7 +19,9 @@ get:
$ref: '#/definitions/bad_request_error' $ref: '#/definitions/bad_request_error'
post: post:
tags: [Contact] tags:
- Contact
operationId: contactCreate
description: Create New Contact description: Create New Contact
parameters: parameters:
- name: data - name: data

View File

@ -1,5 +1,7 @@
post: post:
tags: [Conversation] tags:
- Conversation
operationId: conversationAssignment
summary: Assign Conversation summary: Assign Conversation
description: Assign a conversation to an agent description: Assign a conversation to an agent
parameters: parameters:

View File

@ -1,5 +1,7 @@
get: get:
tags: [Conversation] tags:
- Conversation
operationId: conversationDetails
summary: Conversation Details summary: Conversation Details
description: Get all details regarding a conversation with all messages in the conversation description: Get all details regarding a conversation with all messages in the conversation
parameters: parameters:

View File

@ -1,5 +1,7 @@
get: get:
tags: [Conversation] tags:
- Conversation
operationId: conversationLabelsList
summary: List Labels summary: List Labels
description: Lists all the labels of a conversation description: Lists all the labels of a conversation
parameters: parameters:
@ -19,9 +21,11 @@ get:
description: Access denied description: Access denied
post: post:
tags: [Conversation] tags:
summary: Add Label - Conversation
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: parameters:
- name: id - name: id
in: path in: path

View File

@ -1,5 +1,7 @@
get: get:
tags: [Conversation] tags:
- Conversation
operationId: conversationList
description: List all the conversations with pagination description: List all the conversations with pagination
summary: Conversations List summary: Conversations List
parameters: parameters:

View File

@ -1,5 +1,7 @@
post: post:
tags: [Conversation] tags:
- Conversation
operationId: conversationNewMessage
summary: Create New Message summary: Create New Message
description: All the agent replies are created as new messages through this endpoint description: All the agent replies are created as new messages through this endpoint
parameters: parameters:

View File

@ -1,5 +1,7 @@
post: post:
tags: [Conversation] tags:
- Conversation
operationId: conversationToggleStatus
summary: Toggle Status summary: Toggle Status
description: Toggles the status of the conversation between open and resolved description: Toggles the status of the conversation between open and resolved
parameters: parameters:

View File

@ -1,5 +1,7 @@
post: post:
tags: [Conversation] tags:
- Conversation
operationId: conversationUpdateLastSeen
summary: Update Last Seen summary: Update Last Seen
description: Updates the last seen of the conversation so that conversations will have the bubbles in the agents screen description: Updates the last seen of the conversation so that conversations will have the bubbles in the agents screen
parameters: parameters:

View File

@ -21,6 +21,7 @@
"tags": [ "tags": [
"Contact" "Contact"
], ],
"operationId": "contactList",
"description": "Listing all the contacts with pagination", "description": "Listing all the contacts with pagination",
"summary": "List Contacts", "summary": "List Contacts",
"parameters": [ "parameters": [
@ -49,6 +50,7 @@
"tags": [ "tags": [
"Contact" "Contact"
], ],
"operationId": "contactCreate",
"description": "Create New Contact", "description": "Create New Contact",
"parameters": [ "parameters": [
{ {
@ -81,6 +83,7 @@
"tags": [ "tags": [
"Contact" "Contact"
], ],
"operationId": "contactDetails",
"summary": "Show Contact", "summary": "Show Contact",
"parameters": [ "parameters": [
{ {
@ -110,6 +113,7 @@
"tags": [ "tags": [
"Contact" "Contact"
], ],
"operationId": "contactUpdate",
"summary": "Update Contact", "summary": "Update Contact",
"parameters": [ "parameters": [
{ {
@ -149,6 +153,7 @@
"tags": [ "tags": [
"Contact" "Contact"
], ],
"operationId": "contactConversations",
"summary": "Conversations", "summary": "Conversations",
"parameters": [ "parameters": [
{ {
@ -180,6 +185,7 @@
"tags": [ "tags": [
"Conversation" "Conversation"
], ],
"operationId": "conversationList",
"description": "List all the conversations with pagination", "description": "List all the conversations with pagination",
"summary": "Conversations List", "summary": "Conversations List",
"parameters": [ "parameters": [
@ -210,6 +216,7 @@
"tags": [ "tags": [
"Conversation" "Conversation"
], ],
"operationId": "conversationDetails",
"summary": "Conversation Details", "summary": "Conversation Details",
"description": "Get all details regarding a conversation with all messages in the conversation", "description": "Get all details regarding a conversation with all messages in the conversation",
"parameters": [ "parameters": [
@ -242,6 +249,7 @@
"tags": [ "tags": [
"Conversation" "Conversation"
], ],
"operationId": "conversationToggleStatus",
"summary": "Toggle Status", "summary": "Toggle Status",
"description": "Toggles the status of the conversation between open and resolved", "description": "Toggles the status of the conversation between open and resolved",
"parameters": [ "parameters": [
@ -274,6 +282,7 @@
"tags": [ "tags": [
"Conversation" "Conversation"
], ],
"operationId": "conversationUpdateLastSeen",
"summary": "Update Last Seen", "summary": "Update Last Seen",
"description": "Updates the last seen of the conversation so that conversations will have the bubbles in the agents screen", "description": "Updates the last seen of the conversation so that conversations will have the bubbles in the agents screen",
"parameters": [ "parameters": [
@ -303,6 +312,7 @@
"tags": [ "tags": [
"Conversation" "Conversation"
], ],
"operationId": "conversationLabelsList",
"summary": "List Labels", "summary": "List Labels",
"description": "Lists all the labels of a conversation", "description": "Lists all the labels of a conversation",
"parameters": [ "parameters": [
@ -333,8 +343,9 @@
"tags": [ "tags": [
"Conversation" "Conversation"
], ],
"summary": "Add Label", "operationId": "conversationAddLabels",
"description": "Creates a new label and associates it with the conversation", "summary": "Add Labels",
"description": "Creates new labels and associates it with the conversation",
"parameters": [ "parameters": [
{ {
"name": "id", "name": "id",
@ -382,6 +393,7 @@
"tags": [ "tags": [
"Conversation" "Conversation"
], ],
"operationId": "conversationAssignment",
"summary": "Assign Conversation", "summary": "Assign Conversation",
"description": "Assign a conversation to an agent", "description": "Assign a conversation to an agent",
"parameters": [ "parameters": [
@ -427,6 +439,7 @@
"tags": [ "tags": [
"Conversation" "Conversation"
], ],
"operationId": "conversationNewMessage",
"summary": "Create New Message", "summary": "Create New Message",
"description": "All the agent replies are created as new messages through this endpoint", "description": "All the agent replies are created as new messages through this endpoint",
"parameters": [ "parameters": [