Merge branch 'release/1.20.0' into develop
This commit is contained in:
commit
928766f07b
@ -148,6 +148,14 @@ class Messages::Facebook::MessageBuilder
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def process_contact_params_result(result)
|
||||||
|
{
|
||||||
|
name: "#{result['first_name'] || 'John'} #{result['last_name'] || 'Doe'}",
|
||||||
|
account_id: @inbox.account_id,
|
||||||
|
remote_avatar_url: result['profile_pic'] || ''
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
def contact_params
|
def contact_params
|
||||||
begin
|
begin
|
||||||
k = Koala::Facebook::API.new(@inbox.channel.page_access_token) if @inbox.facebook?
|
k = Koala::Facebook::API.new(@inbox.channel.page_access_token) if @inbox.facebook?
|
||||||
@ -155,14 +163,15 @@ class Messages::Facebook::MessageBuilder
|
|||||||
rescue Koala::Facebook::AuthenticationError
|
rescue Koala::Facebook::AuthenticationError
|
||||||
@inbox.channel.authorization_error!
|
@inbox.channel.authorization_error!
|
||||||
raise
|
raise
|
||||||
|
rescue Koala::Facebook::ClientError => e
|
||||||
|
result = {}
|
||||||
|
# OAuthException, code: 100, error_subcode: 2018218, message: (#100) No profile available for this user
|
||||||
|
# We don't need to capture this error as we don't care about contact params in case of echo messages
|
||||||
|
Sentry.capture_exception(e) unless outgoing_echo?
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
result = {}
|
result = {}
|
||||||
Sentry.capture_exception(e)
|
Sentry.capture_exception(e)
|
||||||
end
|
end
|
||||||
{
|
process_contact_params_result(result)
|
||||||
name: "#{result['first_name'] || 'John'} #{result['last_name'] || 'Doe'}",
|
|
||||||
account_id: @inbox.account_id,
|
|
||||||
remote_avatar_url: result['profile_pic'] || ''
|
|
||||||
}
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -100,6 +100,7 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def update_channel_feature_flags
|
def update_channel_feature_flags
|
||||||
|
return unless @inbox.web_widget?
|
||||||
return unless permitted_params(Channel::WebWidget::EDITABLE_ATTRS)[:channel].key? :selected_feature_flags
|
return unless permitted_params(Channel::WebWidget::EDITABLE_ATTRS)[:channel].key? :selected_feature_flags
|
||||||
|
|
||||||
@inbox.channel.selected_feature_flags = permitted_params(Channel::WebWidget::EDITABLE_ATTRS)[:channel][:selected_feature_flags]
|
@inbox.channel.selected_feature_flags = permitted_params(Channel::WebWidget::EDITABLE_ATTRS)[:channel][:selected_feature_flags]
|
||||||
|
|||||||
@ -3,10 +3,9 @@ class SuperAdmin::DashboardController < SuperAdmin::ApplicationController
|
|||||||
|
|
||||||
def index
|
def index
|
||||||
@data = Conversation.unscoped.group_by_day(:created_at, range: 30.days.ago..2.seconds.ago).count.to_a
|
@data = Conversation.unscoped.group_by_day(:created_at, range: 30.days.ago..2.seconds.ago).count.to_a
|
||||||
@accounts_count = number_with_delimiter(Account.all.length)
|
@accounts_count = number_with_delimiter(Account.count)
|
||||||
@users_count = number_with_delimiter(User.all.length)
|
@users_count = number_with_delimiter(User.count)
|
||||||
@inboxes_count = number_with_delimiter(Inbox.all.length)
|
@inboxes_count = number_with_delimiter(Inbox.count)
|
||||||
@conversations_count = number_with_delimiter(Conversation.all.length)
|
@conversations_count = number_with_delimiter(Conversation.count)
|
||||||
@messages_count = number_with_delimiter(Message.all.length)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -194,7 +194,7 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
handleKeyEvents(e) {
|
getKeyboardListenerParams() {
|
||||||
const allConversations = this.$refs.activeConversation.querySelectorAll(
|
const allConversations = this.$refs.activeConversation.querySelectorAll(
|
||||||
'div.conversations-list div.conversation'
|
'div.conversations-list div.conversation'
|
||||||
);
|
);
|
||||||
@ -205,7 +205,19 @@ export default {
|
|||||||
activeConversation
|
activeConversation
|
||||||
);
|
);
|
||||||
const lastConversationIndex = allConversations.length - 1;
|
const lastConversationIndex = allConversations.length - 1;
|
||||||
|
return {
|
||||||
|
allConversations,
|
||||||
|
activeConversation,
|
||||||
|
activeConversationIndex,
|
||||||
|
lastConversationIndex,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
handleKeyEvents(e) {
|
||||||
if (hasPressedAltAndJKey(e)) {
|
if (hasPressedAltAndJKey(e)) {
|
||||||
|
const {
|
||||||
|
allConversations,
|
||||||
|
activeConversationIndex,
|
||||||
|
} = this.getKeyboardListenerParams();
|
||||||
if (activeConversationIndex === -1) {
|
if (activeConversationIndex === -1) {
|
||||||
allConversations[0].click();
|
allConversations[0].click();
|
||||||
}
|
}
|
||||||
@ -214,6 +226,11 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (hasPressedAltAndKKey(e)) {
|
if (hasPressedAltAndKKey(e)) {
|
||||||
|
const {
|
||||||
|
allConversations,
|
||||||
|
activeConversationIndex,
|
||||||
|
lastConversationIndex,
|
||||||
|
} = this.getKeyboardListenerParams();
|
||||||
if (activeConversationIndex === -1) {
|
if (activeConversationIndex === -1) {
|
||||||
allConversations[lastConversationIndex].click();
|
allConversations[lastConversationIndex].click();
|
||||||
} else if (activeConversationIndex < lastConversationIndex) {
|
} else if (activeConversationIndex < lastConversationIndex) {
|
||||||
|
|||||||
@ -176,7 +176,9 @@ export default {
|
|||||||
'.conversations-list .conversation'
|
'.conversations-list .conversation'
|
||||||
);
|
);
|
||||||
if (hasPressedAltAndMKey(e)) {
|
if (hasPressedAltAndMKey(e)) {
|
||||||
this.$refs.arrowDownButton.$el.click();
|
if (this.$refs.arrowDownButton) {
|
||||||
|
this.$refs.arrowDownButton.$el.click();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (hasPressedAltAndEKey(e)) {
|
if (hasPressedAltAndEKey(e)) {
|
||||||
const activeConversation = document.querySelector(
|
const activeConversation = document.querySelector(
|
||||||
|
|||||||
@ -95,10 +95,15 @@ import AddAccountModal from './sidebarComponents/AddAccountModal.vue';
|
|||||||
import AddLabelModal from '../../routes/dashboard/settings/labels/AddLabel';
|
import AddLabelModal from '../../routes/dashboard/settings/labels/AddLabel';
|
||||||
import WootKeyShortcutModal from 'components/widgets/modal/WootKeyShortcutModal';
|
import WootKeyShortcutModal from 'components/widgets/modal/WootKeyShortcutModal';
|
||||||
import {
|
import {
|
||||||
|
hasPressedAltAndCKey,
|
||||||
|
hasPressedAltAndRKey,
|
||||||
|
hasPressedAltAndSKey,
|
||||||
|
hasPressedAltAndVKey,
|
||||||
hasPressedCommandAndForwardSlash,
|
hasPressedCommandAndForwardSlash,
|
||||||
isEscape,
|
isEscape,
|
||||||
} from 'shared/helpers/KeyboardHelpers';
|
} from 'shared/helpers/KeyboardHelpers';
|
||||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||||
|
import router from '../../routes';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@ -276,6 +281,27 @@ export default {
|
|||||||
if (isEscape(e)) {
|
if (isEscape(e)) {
|
||||||
this.closeKeyShortcutModal();
|
this.closeKeyShortcutModal();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (hasPressedAltAndCKey(e)) {
|
||||||
|
if (!this.isCurrentRouteSameAsNavigation('home')) {
|
||||||
|
router.push({ name: 'home' });
|
||||||
|
}
|
||||||
|
} else if (hasPressedAltAndVKey(e)) {
|
||||||
|
if (!this.isCurrentRouteSameAsNavigation('contacts_dashboard')) {
|
||||||
|
router.push({ name: 'contacts_dashboard' });
|
||||||
|
}
|
||||||
|
} else if (hasPressedAltAndRKey(e)) {
|
||||||
|
if (!this.isCurrentRouteSameAsNavigation('settings_account_reports')) {
|
||||||
|
router.push({ name: 'settings_account_reports' });
|
||||||
|
}
|
||||||
|
} else if (hasPressedAltAndSKey(e)) {
|
||||||
|
if (!this.isCurrentRouteSameAsNavigation('agent_list')) {
|
||||||
|
router.push({ name: 'agent_list' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isCurrentRouteSameAsNavigation(routeName) {
|
||||||
|
return router.currentRoute && router.currentRoute.name === routeName;
|
||||||
},
|
},
|
||||||
toggleSupportChatWindow() {
|
toggleSupportChatWindow() {
|
||||||
window.$chatwoot.toggle();
|
window.$chatwoot.toggle();
|
||||||
|
|||||||
@ -33,7 +33,7 @@
|
|||||||
<a href="#" :class="computedChildClass(child)">
|
<a href="#" :class="computedChildClass(child)">
|
||||||
<div class="wrap">
|
<div class="wrap">
|
||||||
<i
|
<i
|
||||||
v-if="computedInboxClass(child)"
|
v-if="menuItem.key === 'inbox'"
|
||||||
class="inbox-icon"
|
class="inbox-icon"
|
||||||
:class="computedInboxClass(child)"
|
:class="computedInboxClass(child)"
|
||||||
/>
|
/>
|
||||||
@ -59,17 +59,10 @@
|
|||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
|
|
||||||
import router from '../../routes';
|
import router from '../../routes';
|
||||||
import {
|
|
||||||
hasPressedAltAndCKey,
|
|
||||||
hasPressedAltAndVKey,
|
|
||||||
hasPressedAltAndRKey,
|
|
||||||
hasPressedAltAndSKey,
|
|
||||||
} from 'shared/helpers/KeyboardHelpers';
|
|
||||||
import adminMixin from '../../mixins/isAdmin';
|
import adminMixin from '../../mixins/isAdmin';
|
||||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
|
||||||
import { getInboxClassByType } from 'dashboard/helper/inbox';
|
import { getInboxClassByType } from 'dashboard/helper/inbox';
|
||||||
export default {
|
export default {
|
||||||
mixins: [adminMixin, eventListenerMixins],
|
mixins: [adminMixin],
|
||||||
props: {
|
props: {
|
||||||
menuItem: {
|
menuItem: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@ -124,20 +117,6 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
handleKeyEvents(e) {
|
|
||||||
if (hasPressedAltAndCKey(e)) {
|
|
||||||
router.push({ name: 'home' });
|
|
||||||
}
|
|
||||||
if (hasPressedAltAndVKey(e)) {
|
|
||||||
router.push({ name: 'contacts_dashboard' });
|
|
||||||
}
|
|
||||||
if (hasPressedAltAndRKey(e)) {
|
|
||||||
router.push({ name: 'settings_account_reports' });
|
|
||||||
}
|
|
||||||
if (hasPressedAltAndSKey(e)) {
|
|
||||||
router.push({ name: 'settings_home' });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
showItem(item) {
|
showItem(item) {
|
||||||
return this.isAdmin && item.newLink !== undefined;
|
return this.isAdmin && item.newLink !== undefined;
|
||||||
},
|
},
|
||||||
|
|||||||
@ -76,7 +76,6 @@
|
|||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import { mixin as clickaway } from 'vue-clickaway';
|
import { mixin as clickaway } from 'vue-clickaway';
|
||||||
import alertMixin from 'shared/mixins/alertMixin';
|
import alertMixin from 'shared/mixins/alertMixin';
|
||||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
|
||||||
|
|
||||||
import EmojiInput from 'shared/components/emoji/EmojiInput';
|
import EmojiInput from 'shared/components/emoji/EmojiInput';
|
||||||
import CannedResponse from './CannedResponse';
|
import CannedResponse from './CannedResponse';
|
||||||
@ -108,13 +107,7 @@ export default {
|
|||||||
ReplyBottomPanel,
|
ReplyBottomPanel,
|
||||||
WootMessageEditor,
|
WootMessageEditor,
|
||||||
},
|
},
|
||||||
mixins: [
|
mixins: [clickaway, inboxMixin, uiSettingsMixin, alertMixin],
|
||||||
clickaway,
|
|
||||||
inboxMixin,
|
|
||||||
uiSettingsMixin,
|
|
||||||
alertMixin,
|
|
||||||
eventListenerMixins,
|
|
||||||
],
|
|
||||||
props: {
|
props: {
|
||||||
selectedTweet: {
|
selectedTweet: {
|
||||||
type: [Object, String],
|
type: [Object, String],
|
||||||
@ -304,6 +297,15 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
// Donot use the keyboard listener mixin here as the events here are supposed to be
|
||||||
|
// working even if input/textarea is focussed.
|
||||||
|
document.addEventListener('keydown', this.handleKeyEvents);
|
||||||
|
},
|
||||||
|
destroyed() {
|
||||||
|
document.removeEventListener('keydown', this.handleKeyEvents);
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
toggleUserMention(currentMentionState) {
|
toggleUserMention(currentMentionState) {
|
||||||
this.hasUserMention = currentMentionState;
|
this.hasUserMention = currentMentionState;
|
||||||
|
|||||||
@ -22,7 +22,10 @@ export const getInboxClassByType = (type, phoneNumber) => {
|
|||||||
case INBOX_TYPES.EMAIL:
|
case INBOX_TYPES.EMAIL:
|
||||||
return 'ion-ios-email';
|
return 'ion-ios-email';
|
||||||
|
|
||||||
|
case INBOX_TYPES.TELEGRAM:
|
||||||
|
return 'ion-ios-navigate';
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return '';
|
return 'ion-ios-chatbubble';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -195,6 +195,10 @@
|
|||||||
"SUBMIT_BUTTON": "Create LINE Channel",
|
"SUBMIT_BUTTON": "Create LINE Channel",
|
||||||
"API": {
|
"API": {
|
||||||
"ERROR_MESSAGE": "We were not able to save the LINE channel"
|
"ERROR_MESSAGE": "We were not able to save the LINE channel"
|
||||||
|
},
|
||||||
|
"API_CALLBACK": {
|
||||||
|
"TITLE": "Callback URL",
|
||||||
|
"SUBTITLE": "You have to configure the webhook URL in LINE application with the URL mentioned here."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"TELEGRAM_CHANNEL": {
|
"TELEGRAM_CHANNEL": {
|
||||||
|
|||||||
@ -13,18 +13,18 @@
|
|||||||
"STATUS_TABS": [
|
"STATUS_TABS": [
|
||||||
{
|
{
|
||||||
"NAME": "Apri",
|
"NAME": "Apri",
|
||||||
"KEY": "contaaperture"
|
"KEY": "openCount"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"NAME": "Risolti",
|
"NAME": "Risolti",
|
||||||
"KEY": "Conteggio"
|
"KEY": "allConvCount"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"ASSIGNEE_TYPE_TABS": [
|
"ASSIGNEE_TYPE_TABS": [
|
||||||
{
|
{
|
||||||
"NAME": "Miniera",
|
"NAME": "Miniera",
|
||||||
"KEY": "Io",
|
"KEY": "Io",
|
||||||
"COUNT_KEY": "contaMinore"
|
"COUNT_KEY": "mineCount"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"NAME": "Non assegnato",
|
"NAME": "Non assegnato",
|
||||||
@ -40,11 +40,11 @@
|
|||||||
"CHAT_STATUS_ITEMS": [
|
"CHAT_STATUS_ITEMS": [
|
||||||
{
|
{
|
||||||
"TEXT": "Apri",
|
"TEXT": "Apri",
|
||||||
"VALUE": "Aperto"
|
"VALUE": "open"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"TEXT": "Risolti",
|
"TEXT": "Risolti",
|
||||||
"VALUE": "risolto"
|
"VALUE": "resolved"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"TEXT": "Pending",
|
"TEXT": "Pending",
|
||||||
|
|||||||
@ -88,7 +88,7 @@ export default {
|
|||||||
const selectedAgents = this.selectedAgents.map(x => x.id);
|
const selectedAgents = this.selectedAgents.map(x => x.id);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await InboxMembersAPI.create({ inboxId, agentList: selectedAgents });
|
await InboxMembersAPI.update({ inboxId, agentList: selectedAgents });
|
||||||
router.replace({
|
router.replace({
|
||||||
name: 'settings_inbox_finish',
|
name: 'settings_inbox_finish',
|
||||||
params: {
|
params: {
|
||||||
|
|||||||
@ -17,7 +17,7 @@
|
|||||||
<woot-code
|
<woot-code
|
||||||
v-if="isATwilioInbox"
|
v-if="isATwilioInbox"
|
||||||
lang="html"
|
lang="html"
|
||||||
:script="currentInbox.webhook_url"
|
:script="currentInbox.callback_webhook_url"
|
||||||
>
|
>
|
||||||
</woot-code>
|
</woot-code>
|
||||||
</div>
|
</div>
|
||||||
@ -25,7 +25,7 @@
|
|||||||
<woot-code
|
<woot-code
|
||||||
v-if="isALineInbox"
|
v-if="isALineInbox"
|
||||||
lang="html"
|
lang="html"
|
||||||
:script="currentInbox.webhook_url"
|
:script="currentInbox.callback_webhook_url"
|
||||||
>
|
>
|
||||||
</woot-code>
|
</woot-code>
|
||||||
</div>
|
</div>
|
||||||
@ -93,6 +93,12 @@ export default {
|
|||||||
)}`;
|
)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.isALineInbox) {
|
||||||
|
return `${this.$t('INBOX_MGMT.FINISH.MESSAGE')}. ${this.$t(
|
||||||
|
'INBOX_MGMT.ADD.LINE_CHANNEL.API_CALLBACK.SUBTITLE'
|
||||||
|
)}`;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.isAEmailInbox) {
|
if (this.isAEmailInbox) {
|
||||||
return this.$t('INBOX_MGMT.ADD.EMAIL_CHANNEL.FINISH_MESSAGE');
|
return this.$t('INBOX_MGMT.ADD.EMAIL_CHANNEL.FINISH_MESSAGE');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -51,6 +51,9 @@
|
|||||||
<span v-if="item.channel_type === 'Channel::Telegram'">
|
<span v-if="item.channel_type === 'Channel::Telegram'">
|
||||||
Telegram
|
Telegram
|
||||||
</span>
|
</span>
|
||||||
|
<span v-if="item.channel_type === 'Channel::Line'">
|
||||||
|
Line
|
||||||
|
</span>
|
||||||
<span v-if="item.channel_type === 'Channel::Api'">
|
<span v-if="item.channel_type === 'Channel::Api'">
|
||||||
{{ globalConfig.apiChannelName || 'API' }}
|
{{ globalConfig.apiChannelName || 'API' }}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@ -259,7 +259,21 @@
|
|||||||
:title="$t('INBOX_MGMT.ADD.TWILIO.API_CALLBACK.TITLE')"
|
:title="$t('INBOX_MGMT.ADD.TWILIO.API_CALLBACK.TITLE')"
|
||||||
:sub-title="$t('INBOX_MGMT.ADD.TWILIO.API_CALLBACK.SUBTITLE')"
|
:sub-title="$t('INBOX_MGMT.ADD.TWILIO.API_CALLBACK.SUBTITLE')"
|
||||||
>
|
>
|
||||||
<woot-code :script="twilioCallbackURL" lang="html"></woot-code>
|
<woot-code
|
||||||
|
:script="inbox.callback_webhook_url"
|
||||||
|
lang="html"
|
||||||
|
></woot-code>
|
||||||
|
</settings-section>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="isALineChannel" class="settings--content">
|
||||||
|
<settings-section
|
||||||
|
:title="$t('INBOX_MGMT.ADD.LINE_CHANNEL.API_CALLBACK.TITLE')"
|
||||||
|
:sub-title="$t('INBOX_MGMT.ADD.LINE_CHANNEL.API_CALLBACK.SUBTITLE')"
|
||||||
|
>
|
||||||
|
<woot-code
|
||||||
|
:script="inbox.callback_webhook_url"
|
||||||
|
lang="html"
|
||||||
|
></woot-code>
|
||||||
</settings-section>
|
</settings-section>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="isAWebWidgetInbox">
|
<div v-else-if="isAWebWidgetInbox">
|
||||||
@ -398,7 +412,12 @@ export default {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isATwilioChannel || this.isAPIInbox || this.isAnEmailChannel) {
|
if (
|
||||||
|
this.isATwilioChannel ||
|
||||||
|
this.isALineChannel ||
|
||||||
|
this.isAPIInbox ||
|
||||||
|
this.isAnEmailChannel
|
||||||
|
) {
|
||||||
return [
|
return [
|
||||||
...visibleToAllChannelTabs,
|
...visibleToAllChannelTabs,
|
||||||
{
|
{
|
||||||
|
|||||||
@ -99,6 +99,7 @@ export const SDK_CSS = `.woot-widget-holder {
|
|||||||
.woot--close::before, .woot--close::after {
|
.woot--close::before, .woot--close::after {
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
content: ' ';
|
content: ' ';
|
||||||
|
display: inline;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
left: 32px;
|
left: 32px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|||||||
@ -1,8 +1,28 @@
|
|||||||
|
import { isEscape } from '../helpers/KeyboardHelpers';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
mounted() {
|
mounted() {
|
||||||
document.addEventListener('keydown', this.handleKeyEvents);
|
document.addEventListener('keydown', this.onKeyDownHandler);
|
||||||
},
|
},
|
||||||
destroyed() {
|
beforeDestroy() {
|
||||||
document.removeEventListener('keydown', this.handleKeyEvents);
|
document.removeEventListener('keydown', this.onKeyDownHandler);
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onKeyDownHandler(e) {
|
||||||
|
const isEventFromAnInputBox =
|
||||||
|
e.target?.tagName === 'INPUT' || e.target?.tagName === 'TEXTAREA';
|
||||||
|
const isEventFromProseMirror = e.target?.className?.includes(
|
||||||
|
'ProseMirror'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isEventFromAnInputBox || isEventFromProseMirror) {
|
||||||
|
if (isEscape(e)) {
|
||||||
|
e.target.blur();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.handleKeyEvents(e);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -5,6 +5,8 @@ export const INBOX_TYPES = {
|
|||||||
TWILIO: 'Channel::TwilioSms',
|
TWILIO: 'Channel::TwilioSms',
|
||||||
API: 'Channel::Api',
|
API: 'Channel::Api',
|
||||||
EMAIL: 'Channel::Email',
|
EMAIL: 'Channel::Email',
|
||||||
|
TELEGRAM: 'Channel::Telegram',
|
||||||
|
LINE: 'Channel::Line',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@ -27,6 +29,9 @@ export default {
|
|||||||
isATwilioChannel() {
|
isATwilioChannel() {
|
||||||
return this.channelType === INBOX_TYPES.TWILIO;
|
return this.channelType === INBOX_TYPES.TWILIO;
|
||||||
},
|
},
|
||||||
|
isALineChannel() {
|
||||||
|
return this.channelType === INBOX_TYPES.LINE;
|
||||||
|
},
|
||||||
isAnEmailChannel() {
|
isAnEmailChannel() {
|
||||||
return this.channelType === INBOX_TYPES.EMAIL;
|
return this.channelType === INBOX_TYPES.EMAIL;
|
||||||
},
|
},
|
||||||
|
|||||||
@ -27,6 +27,7 @@ class ContactIpLookupJob < ApplicationJob
|
|||||||
geocoder_result = Geocoder.search(ip).first
|
geocoder_result = Geocoder.search(ip).first
|
||||||
return unless geocoder_result
|
return unless geocoder_result
|
||||||
|
|
||||||
|
contact.additional_attributes ||= {}
|
||||||
contact.additional_attributes['city'] = geocoder_result.city
|
contact.additional_attributes['city'] = geocoder_result.city
|
||||||
contact.additional_attributes['country'] = geocoder_result.country
|
contact.additional_attributes['country'] = geocoder_result.country
|
||||||
contact.additional_attributes['country_code'] = geocoder_result.country_code
|
contact.additional_attributes['country_code'] = geocoder_result.country_code
|
||||||
@ -34,7 +35,7 @@ class ContactIpLookupJob < ApplicationJob
|
|||||||
end
|
end
|
||||||
|
|
||||||
def get_contact_ip(contact)
|
def get_contact_ip(contact)
|
||||||
contact.additional_attributes['updated_at_ip'] || contact.additional_attributes['created_at_ip']
|
contact.additional_attributes&.dig('updated_at_ip') || contact.additional_attributes&.dig('created_at_ip')
|
||||||
end
|
end
|
||||||
|
|
||||||
def ensure_look_up_db
|
def ensure_look_up_db
|
||||||
|
|||||||
@ -31,6 +31,7 @@ class Inbox < ApplicationRecord
|
|||||||
include Avatarable
|
include Avatarable
|
||||||
include OutOfOffisable
|
include OutOfOffisable
|
||||||
|
|
||||||
|
validates :name, presence: true
|
||||||
validates :account_id, presence: true
|
validates :account_id, presence: true
|
||||||
validates :timezone, inclusion: { in: TZInfo::Timezone.all_identifiers }
|
validates :timezone, inclusion: { in: TZInfo::Timezone.all_identifiers }
|
||||||
|
|
||||||
@ -93,9 +94,9 @@ class Inbox < ApplicationRecord
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
def webhook_url
|
def callback_webhook_url
|
||||||
case channel_type
|
case channel_type
|
||||||
when 'Channel::TwilioSMS'
|
when 'Channel::TwilioSms'
|
||||||
"#{ENV['FRONTEND_URL']}/twilio/callback"
|
"#{ENV['FRONTEND_URL']}/twilio/callback"
|
||||||
when 'Channel::Line'
|
when 'Channel::Line'
|
||||||
"#{ENV['FRONTEND_URL']}/webhooks/line/#{channel.line_channel_id}"
|
"#{ENV['FRONTEND_URL']}/webhooks/line/#{channel.line_channel_id}"
|
||||||
|
|||||||
@ -1,9 +1,17 @@
|
|||||||
|
# ref : https://developers.line.biz/en/docs/messaging-api/receiving-messages/#webhook-event-types
|
||||||
|
# https://developers.line.biz/en/reference/messaging-api/#message-event
|
||||||
|
|
||||||
class Line::IncomingMessageService
|
class Line::IncomingMessageService
|
||||||
include ::FileTypeHelper
|
include ::FileTypeHelper
|
||||||
pattr_initialize [:inbox!, :params!]
|
pattr_initialize [:inbox!, :params!]
|
||||||
|
|
||||||
def perform
|
def perform
|
||||||
|
# probably test events
|
||||||
|
return if params[:events].blank?
|
||||||
|
|
||||||
line_contact_info
|
line_contact_info
|
||||||
|
return if line_contact_info['userId'].blank?
|
||||||
|
|
||||||
set_contact
|
set_contact
|
||||||
set_conversation
|
set_conversation
|
||||||
# TODO: iterate over the events and handle the attachments in future
|
# TODO: iterate over the events and handle the attachments in future
|
||||||
|
|||||||
@ -1,8 +1,14 @@
|
|||||||
|
# Find the various telegram payload samples here: https://core.telegram.org/bots/webhooks#testing-your-bot-with-updates
|
||||||
|
# https://core.telegram.org/bots/api#available-types
|
||||||
|
|
||||||
class Telegram::IncomingMessageService
|
class Telegram::IncomingMessageService
|
||||||
include ::FileTypeHelper
|
include ::FileTypeHelper
|
||||||
pattr_initialize [:inbox!, :params!]
|
pattr_initialize [:inbox!, :params!]
|
||||||
|
|
||||||
def perform
|
def perform
|
||||||
|
# chatwoot doesn't support group conversations at the moment
|
||||||
|
return unless private_message?
|
||||||
|
|
||||||
set_contact
|
set_contact
|
||||||
update_contact_avatar
|
update_contact_avatar
|
||||||
set_conversation
|
set_conversation
|
||||||
@ -20,6 +26,10 @@ class Telegram::IncomingMessageService
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def private_message?
|
||||||
|
params.dig(:message, :chat, :type) == 'private'
|
||||||
|
end
|
||||||
|
|
||||||
def account
|
def account
|
||||||
@account ||= inbox.account
|
@account ||= inbox.account
|
||||||
end
|
end
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
json.id resource.id
|
json.id resource.id
|
||||||
|
json.avatar_url resource.try(:avatar_url)
|
||||||
json.channel_id resource.channel_id
|
json.channel_id resource.channel_id
|
||||||
json.name resource.name
|
json.name resource.name
|
||||||
json.channel_type resource.channel_type
|
json.channel_type resource.channel_type
|
||||||
@ -6,30 +7,42 @@ json.greeting_enabled resource.greeting_enabled
|
|||||||
json.greeting_message resource.greeting_message
|
json.greeting_message resource.greeting_message
|
||||||
json.working_hours_enabled resource.working_hours_enabled
|
json.working_hours_enabled resource.working_hours_enabled
|
||||||
json.enable_email_collect resource.enable_email_collect
|
json.enable_email_collect resource.enable_email_collect
|
||||||
json.out_of_office_message resource.out_of_office_message
|
|
||||||
json.csat_survey_enabled resource.csat_survey_enabled
|
json.csat_survey_enabled resource.csat_survey_enabled
|
||||||
|
json.enable_auto_assignment resource.enable_auto_assignment
|
||||||
|
json.out_of_office_message resource.out_of_office_message
|
||||||
json.working_hours resource.weekly_schedule
|
json.working_hours resource.weekly_schedule
|
||||||
json.timezone resource.timezone
|
json.timezone resource.timezone
|
||||||
json.webhook_url resource.webhook_url
|
json.callback_webhook_url resource.callback_webhook_url
|
||||||
json.avatar_url resource.try(:avatar_url)
|
|
||||||
json.page_id resource.channel.try(:page_id)
|
## Channel specific settings
|
||||||
|
## TODO : Clean up and move the attributes into channel sub section
|
||||||
|
|
||||||
|
## WebWidget Attributes
|
||||||
json.widget_color resource.channel.try(:widget_color)
|
json.widget_color resource.channel.try(:widget_color)
|
||||||
json.website_url resource.channel.try(:website_url)
|
json.website_url resource.channel.try(:website_url)
|
||||||
json.welcome_title resource.channel.try(:welcome_title)
|
json.welcome_title resource.channel.try(:welcome_title)
|
||||||
json.welcome_tagline resource.channel.try(:welcome_tagline)
|
json.welcome_tagline resource.channel.try(:welcome_tagline)
|
||||||
json.enable_auto_assignment resource.enable_auto_assignment
|
|
||||||
json.web_widget_script resource.channel.try(:web_widget_script)
|
json.web_widget_script resource.channel.try(:web_widget_script)
|
||||||
json.website_token resource.channel.try(:website_token)
|
json.website_token resource.channel.try(:website_token)
|
||||||
json.forward_to_email resource.channel.try(:forward_to_email)
|
|
||||||
json.phone_number resource.channel.try(:phone_number)
|
|
||||||
json.selected_feature_flags resource.channel.try(:selected_feature_flags)
|
json.selected_feature_flags resource.channel.try(:selected_feature_flags)
|
||||||
json.reply_time resource.channel.try(:reply_time)
|
json.reply_time resource.channel.try(:reply_time)
|
||||||
json.reauthorization_required resource.channel.try(:reauthorization_required?) if resource.facebook?
|
|
||||||
if resource.web_widget?
|
if resource.web_widget?
|
||||||
json.hmac_token resource.channel.try(:hmac_token)
|
json.hmac_token resource.channel.try(:hmac_token)
|
||||||
json.pre_chat_form_enabled resource.channel.try(:pre_chat_form_enabled)
|
json.pre_chat_form_enabled resource.channel.try(:pre_chat_form_enabled)
|
||||||
json.pre_chat_form_options resource.channel.try(:pre_chat_form_options)
|
json.pre_chat_form_options resource.channel.try(:pre_chat_form_options)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
## Facebook Attributes
|
||||||
|
json.page_id resource.channel.try(:page_id)
|
||||||
|
json.reauthorization_required resource.channel.try(:reauthorization_required?) if resource.facebook?
|
||||||
|
|
||||||
|
## Twilio Attributes
|
||||||
|
json.phone_number resource.channel.try(:phone_number)
|
||||||
|
|
||||||
|
## Email Channel Attributes
|
||||||
|
json.forward_to_email resource.channel.try(:forward_to_email)
|
||||||
json.email resource.channel.try(:email) if resource.email?
|
json.email resource.channel.try(:email) if resource.email?
|
||||||
|
|
||||||
|
## API Channel Attributes
|
||||||
json.webhook_url resource.channel.try(:webhook_url) if resource.api?
|
json.webhook_url resource.channel.try(:webhook_url) if resource.api?
|
||||||
json.inbox_identifier resource.channel.try(:identifier) if resource.api?
|
json.inbox_identifier resource.channel.try(:identifier) if resource.api?
|
||||||
|
|||||||
@ -53,10 +53,6 @@ It renders the `_table` partial to display details about the resources.
|
|||||||
<div class="metric"><%= @conversations_count %></div>
|
<div class="metric"><%= @conversations_count %></div>
|
||||||
<div>Conversations</div>
|
<div>Conversations</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="report-card">
|
|
||||||
<div class="metric"><%= @messages_count %></div>
|
|
||||||
<div>Messages</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="chart--container" style="height:500px">
|
<div class="chart--container" style="height:500px">
|
||||||
|
|||||||
@ -54,13 +54,19 @@ class Rack::Attack
|
|||||||
|
|
||||||
# ref: https://github.com/rack/rack-attack/issues/399
|
# ref: https://github.com/rack/rack-attack/issues/399
|
||||||
throttle('login/email', limit: 20, period: 5.minutes) do |req|
|
throttle('login/email', limit: 20, period: 5.minutes) do |req|
|
||||||
email = req.params['email'].presence || ActionDispatch::Request.new(req.env).params['email'].presence
|
if req.path == '/auth/sign_in' && req.post?
|
||||||
email.to_s.downcase.gsub(/\s+/, '') if req.path == '/auth/sign_in' && req.post?
|
# NOTE: This line used to throw ArgumentError /rails/action_mailbox/sendgrid/inbound_emails : invalid byte sequence in UTF-8
|
||||||
|
# Hence placed in the if block
|
||||||
|
email = req.params['email'].presence || ActionDispatch::Request.new(req.env).params['email'].presence
|
||||||
|
email.to_s.downcase.gsub(/\s+/, '')
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
throttle('reset_password/email', limit: 5, period: 1.hour) do |req|
|
throttle('reset_password/email', limit: 5, period: 1.hour) do |req|
|
||||||
email = req.params['email'].presence || ActionDispatch::Request.new(req.env).params['email'].presence
|
if req.path == '/auth/password' && req.post?
|
||||||
email.to_s.downcase.gsub(/\s+/, '') if req.path == '/auth/password' && req.post?
|
email = req.params['email'].presence || ActionDispatch::Request.new(req.env).params['email'].presence
|
||||||
|
email.to_s.downcase.gsub(/\s+/, '')
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@ -63,7 +63,7 @@ sed -i -e '/POSTGRES_HOST/ s/=.*/=localhost/' .env
|
|||||||
sed -i -e '/POSTGRES_USERNAME/ s/=.*/=chatwoot/' .env
|
sed -i -e '/POSTGRES_USERNAME/ s/=.*/=chatwoot/' .env
|
||||||
sed -i -e "/POSTGRES_PASSWORD/ s/=.*/=$pg_pass/" .env
|
sed -i -e "/POSTGRES_PASSWORD/ s/=.*/=$pg_pass/" .env
|
||||||
sed -i -e '/RAILS_ENV/ s/=.*/=$RAILS_ENV/' .env
|
sed -i -e '/RAILS_ENV/ s/=.*/=$RAILS_ENV/' .env
|
||||||
echo -en "\nINSTALLATION_ENV=LINUX_SCRIPT" >> ".env"
|
echo -en "\nINSTALLATION_ENV=linux_script" >> ".env"
|
||||||
|
|
||||||
RAILS_ENV=production bundle exec rake db:create
|
RAILS_ENV=production bundle exec rake db:create
|
||||||
RAILS_ENV=production bundle exec rake db:reset
|
RAILS_ENV=production bundle exec rake db:reset
|
||||||
|
|||||||
@ -70,7 +70,7 @@ sed -i -e '/POSTGRES_HOST/ s/=.*/=localhost/' .env
|
|||||||
sed -i -e '/POSTGRES_USERNAME/ s/=.*/=chatwoot/' .env
|
sed -i -e '/POSTGRES_USERNAME/ s/=.*/=chatwoot/' .env
|
||||||
sed -i -e "/POSTGRES_PASSWORD/ s/=.*/=$pg_pass/" .env
|
sed -i -e "/POSTGRES_PASSWORD/ s/=.*/=$pg_pass/" .env
|
||||||
sed -i -e '/RAILS_ENV/ s/=.*/=$RAILS_ENV/' .env
|
sed -i -e '/RAILS_ENV/ s/=.*/=$RAILS_ENV/' .env
|
||||||
echo -en "\nINSTALLATION_ENV=LINUX_SCRIPT" >> ".env"
|
echo -en "\nINSTALLATION_ENV=linux_script" >> ".env"
|
||||||
|
|
||||||
RAILS_ENV=production bundle exec rake db:create
|
RAILS_ENV=production bundle exec rake db:create
|
||||||
RAILS_ENV=production bundle exec rake db:reset
|
RAILS_ENV=production bundle exec rake db:reset
|
||||||
|
|||||||
@ -7,14 +7,18 @@ class Integrations::Dialogflow::ProcessorService
|
|||||||
return unless processable_message?(message)
|
return unless processable_message?(message)
|
||||||
return unless message.conversation.pending?
|
return unless message.conversation.pending?
|
||||||
|
|
||||||
response = get_dialogflow_response(message.conversation.contact_inbox.source_id, message_content(message))
|
content = message_content(message)
|
||||||
process_response(message, response)
|
response = get_dialogflow_response(message.conversation.contact_inbox.source_id, content) if content.present?
|
||||||
|
process_response(message, response) if response.present?
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def message_content(message)
|
def message_content(message)
|
||||||
return message.content_attributes['submitted_values'].first['value'] if event_name == 'message.updated'
|
# TODO: might needs to change this to a way that we fetch the updated value from event data instead
|
||||||
|
# cause the message.updated event could be that that the message was deleted
|
||||||
|
|
||||||
|
return message.content_attributes['submitted_values']&.dig 'value' if event_name == 'message.updated'
|
||||||
|
|
||||||
message.content
|
message.content
|
||||||
end
|
end
|
||||||
|
|||||||
@ -45,6 +45,8 @@ class Integrations::Slack::SendOnSlackService < Base::SendOnChannelService
|
|||||||
end
|
end
|
||||||
|
|
||||||
def send_message
|
def send_message
|
||||||
|
return if message_content.blank?
|
||||||
|
|
||||||
@slack_message = slack_client.chat_postMessage(
|
@slack_message = slack_client.chat_postMessage(
|
||||||
channel: hook.reference_id,
|
channel: hook.reference_id,
|
||||||
text: message_content,
|
text: message_content,
|
||||||
|
|||||||
@ -306,6 +306,7 @@ RSpec.describe 'Inboxes API', type: :request do
|
|||||||
|
|
||||||
expect(response).to have_http_status(:success)
|
expect(response).to have_http_status(:success)
|
||||||
expect(response.body).to include('Line Inbox')
|
expect(response.body).to include('Line Inbox')
|
||||||
|
expect(response.body).to include('callback_webhook_url')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -352,7 +353,7 @@ RSpec.describe 'Inboxes API', type: :request do
|
|||||||
|
|
||||||
patch "/api/v1/accounts/#{account.id}/inboxes/#{api_inbox.id}",
|
patch "/api/v1/accounts/#{account.id}/inboxes/#{api_inbox.id}",
|
||||||
headers: admin.create_new_auth_token,
|
headers: admin.create_new_auth_token,
|
||||||
params: { enable_auto_assignment: false, channel: { webhook_url: 'webhook.test' } },
|
params: { enable_auto_assignment: false, channel: { webhook_url: 'webhook.test', selected_feature_flags: [] } },
|
||||||
as: :json
|
as: :json
|
||||||
|
|
||||||
expect(response).to have_http_status(:success)
|
expect(response).to have_http_status(:success)
|
||||||
|
|||||||
@ -18,10 +18,28 @@ describe Telegram::IncomingMessageService do
|
|||||||
}
|
}
|
||||||
}.with_indifferent_access
|
}.with_indifferent_access
|
||||||
described_class.new(inbox: telegram_channel.inbox, params: params).perform
|
described_class.new(inbox: telegram_channel.inbox, params: params).perform
|
||||||
expect(telegram_channel.inbox.conversations).not_to eq(0)
|
expect(telegram_channel.inbox.conversations.count).not_to eq(0)
|
||||||
expect(Contact.all.first.name).to eq('Sojan Jose')
|
expect(Contact.all.first.name).to eq('Sojan Jose')
|
||||||
expect(telegram_channel.inbox.messages.first.content).to eq('test')
|
expect(telegram_channel.inbox.messages.first.content).to eq('test')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when group messages' do
|
||||||
|
it 'doesnot create conversations, message and contacts' do
|
||||||
|
params = {
|
||||||
|
'update_id' => 2_342_342_343_242,
|
||||||
|
'message' => {
|
||||||
|
'message_id' => 1,
|
||||||
|
'from' => {
|
||||||
|
'id' => 23, 'is_bot' => false, 'first_name' => 'Sojan', 'last_name' => 'Jose', 'username' => 'sojan', 'language_code' => 'en'
|
||||||
|
},
|
||||||
|
'chat' => { 'id' => 23, 'first_name' => 'Sojan', 'last_name' => 'Jose', 'username' => 'sojan', 'type' => 'group' },
|
||||||
|
'date' => 1_631_132_077, 'text' => 'test'
|
||||||
|
}
|
||||||
|
}.with_indifferent_access
|
||||||
|
described_class.new(inbox: telegram_channel.inbox, params: params).perform
|
||||||
|
expect(telegram_channel.inbox.conversations.count).to eq(0)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user