Feature: Slack - receive messages, create threads, send replies (#974)
Co-authored-by: Pranav Raj S <pranav@thoughtwoot.com>
This commit is contained in:
parent
aa8a85b8bd
commit
1ef8d03e18
53 changed files with 815 additions and 188 deletions
|
@ -16,7 +16,7 @@ defaults: &defaults
|
|||
- image: circleci/redis:alpine
|
||||
environment:
|
||||
- CC_TEST_REPORTER_ID: b1b5c4447bf93f6f0b06a64756e35afd0810ea83649f03971cbf303b4449456f
|
||||
|
||||
- RAILS_LOG_TO_STDOUT: false
|
||||
jobs:
|
||||
build:
|
||||
<<: *defaults
|
||||
|
@ -69,11 +69,11 @@ jobs:
|
|||
- run:
|
||||
name: Download cc-test-reporter
|
||||
command: |
|
||||
mkdir -p tmp/
|
||||
curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./tmp/cc-test-reporter
|
||||
chmod +x ./tmp/cc-test-reporter
|
||||
mkdir -p ~/tmp
|
||||
curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ~/tmp/cc-test-reporter
|
||||
chmod +x ~/tmp/cc-test-reporter
|
||||
- persist_to_workspace:
|
||||
root: tmp
|
||||
root: ~/tmp
|
||||
paths:
|
||||
- cc-test-reporter
|
||||
|
||||
|
@ -99,9 +99,9 @@ jobs:
|
|||
name: Run backend tests
|
||||
command: |
|
||||
bundle exec rspec $(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings)
|
||||
./tmp/cc-test-reporter format-coverage -t simplecov -o tmp/codeclimate.backend.json coverage/backend/.resultset.json
|
||||
~/tmp/cc-test-reporter format-coverage -t simplecov -o ~/tmp/codeclimate.backend.json coverage/backend/.resultset.json
|
||||
- persist_to_workspace:
|
||||
root: tmp
|
||||
root: ~/tmp
|
||||
paths:
|
||||
- codeclimate.backend.json
|
||||
|
||||
|
@ -109,21 +109,23 @@ jobs:
|
|||
name: Run frontend tests
|
||||
command: |
|
||||
yarn test:coverage
|
||||
./tmp/cc-test-reporter format-coverage -t lcov -o tmp/codeclimate.frontend.json buildreports/lcov.info
|
||||
~/tmp/cc-test-reporter format-coverage -t lcov -o ~/tmp/codeclimate.frontend.json buildreports/lcov.info
|
||||
- persist_to_workspace:
|
||||
root: tmp
|
||||
root: ~/tmp
|
||||
paths:
|
||||
- codeclimate.frontend.json
|
||||
|
||||
# collect reports
|
||||
- store_test_results:
|
||||
path: /tmp/test-results
|
||||
path: ~/tmp/test-results
|
||||
- store_artifacts:
|
||||
path: /tmp/test-results
|
||||
path: ~/tmp/test-results
|
||||
destination: test-results
|
||||
- store_artifacts:
|
||||
path: log
|
||||
|
||||
- run:
|
||||
name: Upload coverage results to Code Climate
|
||||
command: |
|
||||
./tmp/cc-test-reporter sum-coverage tmp/codeclimate.*.json -p 2 -o tmp/codeclimate.total.json
|
||||
./tmp/cc-test-reporter upload-coverage -i tmp/codeclimate.total.json
|
||||
~/tmp/cc-test-reporter sum-coverage ~/tmp/codeclimate.*.json -p 2 -o ~/tmp/codeclimate.total.json
|
||||
~/tmp/cc-test-reporter upload-coverage -i ~/tmp/codeclimate.total.json
|
||||
|
|
10
.env.example
10
.env.example
|
@ -59,6 +59,7 @@ MANDRILL_INGRESS_API_KEY=
|
|||
ACTIVE_STORAGE_SERVICE=local
|
||||
|
||||
# Amazon S3
|
||||
# documentation: https://www.chatwoot.com/docs/configuring-s3-bucket-as-cloud-storage
|
||||
S3_BUCKET_NAME=
|
||||
AWS_ACCESS_KEY_ID=
|
||||
AWS_SECRET_ACCESS_KEY=
|
||||
|
@ -74,20 +75,23 @@ LOG_LEVEL=info
|
|||
LOG_SIZE=500
|
||||
|
||||
### This environment variables are only required if you are setting up social media channels
|
||||
#facebook
|
||||
|
||||
# Facebook
|
||||
# documentation: https://www.chatwoot.com/docs/facebook-setup
|
||||
FB_VERIFY_TOKEN=
|
||||
FB_APP_SECRET=
|
||||
FB_APP_ID=
|
||||
|
||||
# Twitter
|
||||
# documentation: https://www.chatwoot.com/docs/twitter-app-setup
|
||||
TWITTER_APP_ID=
|
||||
TWITTER_CONSUMER_KEY=
|
||||
TWITTER_CONSUMER_SECRET=
|
||||
TWITTER_ENVIRONMENT=
|
||||
|
||||
#slack
|
||||
#slack integration
|
||||
SLACK_CLIENT_ID=
|
||||
SLACK_CLIENT_SECRET
|
||||
SLACK_CLIENT_SECRET=
|
||||
|
||||
### Change this env variable only if you are using a custom build mobile app
|
||||
## Mobile app env variables
|
||||
|
|
|
@ -9,7 +9,7 @@ class Api::V1::Accounts::Integrations::AppsController < Api::V1::Accounts::BaseC
|
|||
private
|
||||
|
||||
def fetch_apps
|
||||
@apps = Integrations::App.all
|
||||
@apps = Integrations::App.all.select(&:active?)
|
||||
end
|
||||
|
||||
def fetch_app
|
||||
|
|
|
@ -7,18 +7,12 @@ class Api::V1::Accounts::Integrations::SlackController < Api::V1::Accounts::Base
|
|||
code: params[:code],
|
||||
inbox_id: params[:inbox_id]
|
||||
)
|
||||
|
||||
@hook = builder.perform
|
||||
|
||||
render json: @hook
|
||||
create_chatwoot_slack_channel
|
||||
end
|
||||
|
||||
def update
|
||||
builder = Integrations::Slack::ChannelBuilder.new(
|
||||
hook: @hook, channel: params[:channel]
|
||||
)
|
||||
builder.perform
|
||||
|
||||
create_chatwoot_slack_channel
|
||||
render json: @hook
|
||||
end
|
||||
|
||||
|
@ -31,6 +25,14 @@ class Api::V1::Accounts::Integrations::SlackController < Api::V1::Accounts::Base
|
|||
private
|
||||
|
||||
def fetch_hook
|
||||
@hook = Integrations::Hook.find(params[:id])
|
||||
@hook = Integrations::Hook.find_by(app_id: 'slack')
|
||||
end
|
||||
|
||||
def create_chatwoot_slack_channel
|
||||
channel = params[:channel] || 'customer-conversations'
|
||||
builder = Integrations::Slack::ChannelBuilder.new(
|
||||
hook: @hook, channel: channel
|
||||
)
|
||||
builder.perform
|
||||
end
|
||||
end
|
||||
|
|
|
@ -10,6 +10,10 @@ class ApiClient {
|
|||
}
|
||||
|
||||
get url() {
|
||||
return `${this.baseUrl()}/${this.resource}`;
|
||||
}
|
||||
|
||||
baseUrl() {
|
||||
let url = this.apiVersion;
|
||||
if (this.options.accountScoped) {
|
||||
const isInsideAccountScopedURLs = window.location.pathname.includes(
|
||||
|
@ -21,7 +25,8 @@ class ApiClient {
|
|||
url = `${url}/accounts/${accountId}`;
|
||||
}
|
||||
}
|
||||
return `${url}/${this.resource}`;
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
get() {
|
||||
|
|
21
app/javascript/dashboard/api/integrations.js
Normal file
21
app/javascript/dashboard/api/integrations.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
/* global axios */
|
||||
|
||||
import ApiClient from './ApiClient';
|
||||
|
||||
class IntegrationsAPI extends ApiClient {
|
||||
constructor() {
|
||||
super('integrations/apps', { accountScoped: true });
|
||||
}
|
||||
|
||||
connectSlack(code) {
|
||||
return axios.post(`${this.baseUrl()}/integrations/slack`, {
|
||||
code: code,
|
||||
});
|
||||
}
|
||||
|
||||
delete(integrationId) {
|
||||
return axios.delete(`${this.baseUrl()}/integrations/${integrationId}`);
|
||||
}
|
||||
}
|
||||
|
||||
export default new IntegrationsAPI();
|
|
@ -3,16 +3,17 @@
|
|||
background: $color-white;
|
||||
border: 1px solid $color-border;
|
||||
border-radius: $space-smaller;
|
||||
margin-bottom: $space-normal;
|
||||
padding: $space-normal;
|
||||
|
||||
.integration--image {
|
||||
display: flex;
|
||||
margin-right: $space-normal;
|
||||
width: 8rem;
|
||||
width: 10rem;
|
||||
|
||||
img {
|
||||
max-width: 8rem;
|
||||
padding: $space-small;
|
||||
max-width: 100%;
|
||||
padding: $space-medium;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -121,7 +121,6 @@ export default {
|
|||
computed: {
|
||||
...mapGetters({
|
||||
currentUser: 'getCurrentUser',
|
||||
daysLeft: 'getTrialLeft',
|
||||
globalConfig: 'globalConfig/get',
|
||||
inboxes: 'inboxes/getInboxes',
|
||||
accountId: 'getCurrentAccountId',
|
||||
|
|
|
@ -1,14 +1,26 @@
|
|||
<template>
|
||||
<span class="back-button ion-ios-arrow-left" @click.capture="goBack">Back</span>
|
||||
<span class="back-button ion-ios-arrow-left" @click.capture="goBack">
|
||||
{{ $t('GENERAL_SETTINGS.BACK') }}
|
||||
</span>
|
||||
</template>
|
||||
<script>
|
||||
import router from '../../routes/index';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
backUrl: {
|
||||
type: [String, Object],
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
goBack() {
|
||||
router.go(-1);
|
||||
if (this.backUrl !== '') {
|
||||
router.push(this.backUrl);
|
||||
} else {
|
||||
router.go(-1);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
</script>
|
||||
|
|
|
@ -52,6 +52,7 @@ export const getSidebarItems = accountId => ({
|
|||
'settings_inbox_finish',
|
||||
'settings_integrations',
|
||||
'settings_integrations_webhook',
|
||||
'settings_integrations_integration',
|
||||
'general_settings',
|
||||
'general_settings_index',
|
||||
],
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
"GENERAL_SETTINGS": {
|
||||
"TITLE": "Account settings",
|
||||
"SUBMIT": "Update settings",
|
||||
"BACK": "Back",
|
||||
"UPDATE": {
|
||||
"ERROR": "Could not update settings, try again!",
|
||||
"SUCCESS": "Successfully updated account settings"
|
||||
|
|
|
@ -49,6 +49,15 @@
|
|||
"NO": "No, Keep it"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"DELETE": {
|
||||
"BUTTON_TEXT": "Delete",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Integration deleted successfully"
|
||||
}
|
||||
},
|
||||
"CONNECT": {
|
||||
"BUTTON_TEXT": "Connect"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<div class="settings-header">
|
||||
<h1 class="page-title">
|
||||
<woot-sidemenu-icon></woot-sidemenu-icon>
|
||||
<back-button v-if="showBackButton"></back-button>
|
||||
<back-button v-if="showBackButton" :back-url="backUrl"></back-button>
|
||||
<i :class="iconClass"></i>
|
||||
<span>{{ headerTitle }}</span>
|
||||
</h1>
|
||||
|
@ -45,6 +45,10 @@ export default {
|
|||
},
|
||||
showBackButton: { type: Boolean, default: false },
|
||||
showNewButton: { type: Boolean, default: false },
|
||||
backUrl: {
|
||||
type: [String, Object],
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
:header-title="$t(headerTitle)"
|
||||
:button-text="$t(headerButtonText)"
|
||||
:show-back-button="showBackButton"
|
||||
:back-url="backUrl"
|
||||
:show-new-button="showNewButton"
|
||||
/>
|
||||
<keep-alive>
|
||||
|
@ -34,6 +35,10 @@ export default {
|
|||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
backUrl: {
|
||||
type: [String, Object],
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {};
|
||||
|
|
|
@ -3,38 +3,19 @@
|
|||
<div class="row">
|
||||
<div class="small-8 columns integrations-wrap">
|
||||
<div class="row integrations">
|
||||
<div class="small-12 columns integration">
|
||||
<div class="row">
|
||||
<div class="integration--image">
|
||||
<img src="~dashboard/assets/images/integrations/cable.svg" />
|
||||
</div>
|
||||
<div class="column">
|
||||
<h3 class="integration--title">
|
||||
{{ $t('INTEGRATION_SETTINGS.WEBHOOK.TITLE') }}
|
||||
</h3>
|
||||
<p class="integration--description">
|
||||
{{
|
||||
useInstallationName(
|
||||
$t('INTEGRATION_SETTINGS.WEBHOOK.INTEGRATION_TXT'),
|
||||
globalConfig.installationName
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
<div class="small-2 column button-wrap">
|
||||
<router-link
|
||||
:to="
|
||||
frontendURL(
|
||||
`accounts/${accountId}/settings/integrations/webhook`
|
||||
)
|
||||
"
|
||||
>
|
||||
<button class="button success nice">
|
||||
{{ $t('INTEGRATION_SETTINGS.WEBHOOK.CONFIGURE') }}
|
||||
</button>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-for="item in integrationsList"
|
||||
:key="item.id"
|
||||
class="small-12 columns integration"
|
||||
>
|
||||
<integration
|
||||
:integration-id="item.id"
|
||||
:integration-logo="item.logo"
|
||||
:integration-name="item.name"
|
||||
:integration-description="item.description"
|
||||
:integration-enabled="item.enabled"
|
||||
:integration-action="item.action"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -43,20 +24,19 @@
|
|||
</template>
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { frontendURL } from '../../../../helper/URLHelper';
|
||||
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
|
||||
import Integration from './Integration';
|
||||
|
||||
export default {
|
||||
mixins: [globalConfigMixin],
|
||||
components: {
|
||||
Integration,
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
currentUser: 'getCurrentUser',
|
||||
globalConfig: 'globalConfig/get',
|
||||
accountId: 'getCurrentAccountId',
|
||||
integrationsList: 'integrations/getIntegrations',
|
||||
}),
|
||||
},
|
||||
methods: {
|
||||
frontendURL,
|
||||
mounted() {
|
||||
this.$store.dispatch('integrations/get');
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -0,0 +1,114 @@
|
|||
<template>
|
||||
<div class="row">
|
||||
<div class="integration--image">
|
||||
<img :src="'/assets/dashboard/integrations/' + integrationLogo" />
|
||||
</div>
|
||||
<div class="column">
|
||||
<h3 class="integration--title">
|
||||
{{ integrationName }}
|
||||
</h3>
|
||||
<p class="integration--description">
|
||||
{{ integrationDescription }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="small-2 column button-wrap">
|
||||
<router-link
|
||||
:to="
|
||||
frontendURL(
|
||||
`accounts/${accountId}/settings/integrations/` + integrationId
|
||||
)
|
||||
"
|
||||
>
|
||||
<div v-if="integrationEnabled">
|
||||
<div v-if="integrationAction === 'disconnect'">
|
||||
<div @click="openDeletePopup()">
|
||||
<woot-submit-button
|
||||
:button-text="
|
||||
$t('INTEGRATION_SETTINGS.WEBHOOK.DELETE.BUTTON_TEXT')
|
||||
"
|
||||
icon-class="ion-close-circled"
|
||||
button-class="nice alert"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<button class="button nice">
|
||||
{{ $t('INTEGRATION_SETTINGS.WEBHOOK.CONFIGURE') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</router-link>
|
||||
<div v-if="!integrationEnabled">
|
||||
<a :href="integrationAction" class="button success nice">
|
||||
{{ $t('INTEGRATION_SETTINGS.CONNECT.BUTTON_TEXT') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<woot-delete-modal
|
||||
:show.sync="showDeleteConfirmationPopup"
|
||||
:on-close="closeDeletePopup"
|
||||
:on-confirm="confirmDeletion"
|
||||
:title="$t('INTEGRATION_SETTINGS.WEBHOOK.DELETE.CONFIRM.TITLE')"
|
||||
:message="$t('INTEGRATION_SETTINGS.WEBHOOK.DELETE.CONFIRM.MESSAGE')"
|
||||
:confirm-text="$t('INTEGRATION_SETTINGS.WEBHOOK.DELETE.CONFIRM.YES')"
|
||||
:reject-text="$t('INTEGRATION_SETTINGS.WEBHOOK.DELETE.CONFIRM.NO')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { frontendURL } from '../../../../helper/URLHelper';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
|
||||
export default {
|
||||
mixins: [alertMixin],
|
||||
props: [
|
||||
'integrationId',
|
||||
'integrationLogo',
|
||||
'integrationName',
|
||||
'integrationDescription',
|
||||
'integrationEnabled',
|
||||
'integrationAction',
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
showDeleteConfirmationPopup: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
currentUser: 'getCurrentUser',
|
||||
accountId: 'getCurrentAccountId',
|
||||
}),
|
||||
},
|
||||
methods: {
|
||||
frontendURL,
|
||||
openDeletePopup() {
|
||||
this.showDeleteConfirmationPopup = true;
|
||||
},
|
||||
closeDeletePopup() {
|
||||
this.showDeleteConfirmationPopup = false;
|
||||
},
|
||||
confirmDeletion() {
|
||||
this.closeDeletePopup();
|
||||
this.deleteIntegration(this.deleteIntegration);
|
||||
this.$router.push({ name: 'settings_integrations' });
|
||||
},
|
||||
async deleteIntegration() {
|
||||
try {
|
||||
await this.$store.dispatch(
|
||||
'integrations/deleteIntegration',
|
||||
this.integrationId
|
||||
);
|
||||
this.showAlert(
|
||||
this.$t('INTEGRATION_SETTINGS.DELETE.API.SUCCESS_MESSAGE')
|
||||
);
|
||||
} catch (error) {
|
||||
this.showAlert(
|
||||
this.$t('INTEGRATION_SETTINGS.WEBHOOK.DELETE.API.ERROR_MESSAGE')
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -0,0 +1,70 @@
|
|||
<template>
|
||||
<div class="column content-box">
|
||||
<div class="row">
|
||||
<div class="small-8 columns integrations-wrap">
|
||||
<div class="row integrations">
|
||||
<div v-if="integrationLoaded" class="small-12 columns integration">
|
||||
<integration
|
||||
:integration-id="integration.id"
|
||||
:integration-logo="integration.logo"
|
||||
:integration-name="integration.name"
|
||||
:integration-description="integration.description"
|
||||
:integration-enabled="integration.enabled"
|
||||
:integration-action="integrationAction()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
|
||||
import Integration from './Integration';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Integration,
|
||||
},
|
||||
mixins: [globalConfigMixin],
|
||||
props: ['integrationId', 'code'],
|
||||
data() {
|
||||
return {
|
||||
integrationLoaded: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
integration() {
|
||||
return this.$store.getters['integrations/getIntegration'](
|
||||
this.integrationId
|
||||
);
|
||||
},
|
||||
...mapGetters({
|
||||
currentUser: 'getCurrentUser',
|
||||
globalConfig: 'globalConfig/get',
|
||||
accountId: 'getCurrentAccountId',
|
||||
}),
|
||||
},
|
||||
mounted() {
|
||||
this.intializeSlackIntegration();
|
||||
},
|
||||
methods: {
|
||||
integrationAction() {
|
||||
if (this.integration.enabled) {
|
||||
return 'disconnect';
|
||||
}
|
||||
return this.integration.action;
|
||||
},
|
||||
async intializeSlackIntegration() {
|
||||
await this.$store.dispatch('integrations/get', this.integrationId);
|
||||
if (this.code) {
|
||||
await this.$store.dispatch('integrations/connectSlack', this.code);
|
||||
// we are clearing code from the path as subsequent request would throw error
|
||||
this.$router.replace(this.$route.path);
|
||||
}
|
||||
this.integrationLoaded = true;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -82,16 +82,16 @@
|
|||
</div>
|
||||
</template>
|
||||
<script>
|
||||
/* global bus */
|
||||
import { mapGetters } from 'vuex';
|
||||
import NewWebhook from './New';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
NewWebhook,
|
||||
},
|
||||
mixins: [globalConfigMixin],
|
||||
mixins: [alertMixin, globalConfigMixin],
|
||||
data() {
|
||||
return {
|
||||
loading: {},
|
||||
|
@ -111,9 +111,6 @@ export default {
|
|||
this.$store.dispatch('webhooks/get');
|
||||
},
|
||||
methods: {
|
||||
showAlert(message) {
|
||||
bus.$emit('newToastMessage', message);
|
||||
},
|
||||
openAddPopup() {
|
||||
this.showAddPopup = true;
|
||||
},
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import Index from './Index';
|
||||
import SettingsContent from '../Wrapper';
|
||||
import Webhook from './Webhook';
|
||||
import ShowIntegration from './ShowIntegration';
|
||||
import { frontendURL } from '../../../../helper/URLHelper';
|
||||
|
||||
export default {
|
||||
|
@ -10,10 +11,15 @@ export default {
|
|||
component: SettingsContent,
|
||||
props: params => {
|
||||
const showBackButton = params.name !== 'settings_integrations';
|
||||
const backUrl =
|
||||
params.name === 'settings_integrations_integration'
|
||||
? { name: 'settings_integrations' }
|
||||
: '';
|
||||
return {
|
||||
headerTitle: 'INTEGRATION_SETTINGS.HEADER',
|
||||
icon: 'ion-flash',
|
||||
showBackButton,
|
||||
backUrl,
|
||||
};
|
||||
},
|
||||
children: [
|
||||
|
@ -29,6 +35,18 @@ export default {
|
|||
name: 'settings_integrations_webhook',
|
||||
roles: ['administrator'],
|
||||
},
|
||||
{
|
||||
path: ':integration_id',
|
||||
name: 'settings_integrations_integration',
|
||||
component: ShowIntegration,
|
||||
roles: ['administrator'],
|
||||
props: route => {
|
||||
return {
|
||||
integrationId: route.params.integration_id,
|
||||
code: route.query.code,
|
||||
};
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
|
|
@ -17,6 +17,7 @@ import conversationTypingStatus from './modules/conversationTypingStatus';
|
|||
import globalConfig from 'shared/store/globalConfig';
|
||||
import inboxes from './modules/inboxes';
|
||||
import inboxMembers from './modules/inboxMembers';
|
||||
import integrations from './modules/integrations';
|
||||
import reports from './modules/reports';
|
||||
import userNotificationSettings from './modules/userNotificationSettings';
|
||||
import webhooks from './modules/webhooks';
|
||||
|
@ -40,6 +41,7 @@ export default new Vuex.Store({
|
|||
globalConfig,
|
||||
inboxes,
|
||||
inboxMembers,
|
||||
integrations,
|
||||
reports,
|
||||
userNotificationSettings,
|
||||
webhooks,
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
/* eslint no-param-reassign: 0 */
|
||||
import axios from 'axios';
|
||||
import moment from 'moment';
|
||||
import Vue from 'vue';
|
||||
import * as types from '../mutation-types';
|
||||
import authAPI from '../../api/auth';
|
||||
|
@ -50,21 +49,6 @@ export const getters = {
|
|||
getCurrentUser(_state) {
|
||||
return _state.currentUser;
|
||||
},
|
||||
|
||||
getSubscription(_state) {
|
||||
return _state.currentUser.subscription === undefined
|
||||
? null
|
||||
: _state.currentUser.subscription;
|
||||
},
|
||||
|
||||
getTrialLeft(_state) {
|
||||
const createdAt =
|
||||
_state.currentUser.subscription === undefined
|
||||
? moment()
|
||||
: _state.currentUser.subscription.expiry * 1000;
|
||||
const daysLeft = moment(createdAt).diff(moment(), 'days');
|
||||
return daysLeft < 0 ? 0 : daysLeft;
|
||||
},
|
||||
};
|
||||
|
||||
// actions
|
||||
|
|
83
app/javascript/dashboard/store/modules/integrations.js
Normal file
83
app/javascript/dashboard/store/modules/integrations.js
Normal file
|
@ -0,0 +1,83 @@
|
|||
/* eslint no-param-reassign: 0 */
|
||||
import * as MutationHelpers from 'shared/helpers/vuex/mutationHelpers';
|
||||
import * as types from '../mutation-types';
|
||||
import IntegrationsAPI from '../../api/integrations';
|
||||
|
||||
const state = {
|
||||
records: [],
|
||||
uiFlags: {
|
||||
isFetching: false,
|
||||
isFetchingItem: false,
|
||||
isUpdating: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const getters = {
|
||||
getIntegrations($state) {
|
||||
return $state.records;
|
||||
},
|
||||
getIntegration: $state => integrationId => {
|
||||
const [integration] = $state.records.filter(
|
||||
record => record.id === integrationId
|
||||
);
|
||||
return integration || {};
|
||||
},
|
||||
getUIFlags($state) {
|
||||
return $state.uiFlags;
|
||||
},
|
||||
};
|
||||
|
||||
export const actions = {
|
||||
get: async ({ commit }) => {
|
||||
commit(types.default.SET_INTEGRATIONS_UI_FLAG, { isFetching: true });
|
||||
try {
|
||||
const response = await IntegrationsAPI.get();
|
||||
commit(types.default.SET_INTEGRATIONS, response.data.payload);
|
||||
commit(types.default.SET_INTEGRATIONS_UI_FLAG, { isFetching: false });
|
||||
} catch (error) {
|
||||
commit(types.default.SET_INTEGRATIONS_UI_FLAG, { isFetching: false });
|
||||
}
|
||||
},
|
||||
|
||||
connectSlack: async ({ commit }, code) => {
|
||||
commit(types.default.SET_INTEGRATIONS_UI_FLAG, { isUpdating: true });
|
||||
try {
|
||||
const response = await IntegrationsAPI.connectSlack(code);
|
||||
commit(types.default.ADD_INTEGRATION, response.data);
|
||||
commit(types.default.SET_INTEGRATIONS_UI_FLAG, { isUpdating: false });
|
||||
} catch (error) {
|
||||
commit(types.default.SET_INTEGRATIONS_UI_FLAG, { isUpdating: false });
|
||||
}
|
||||
},
|
||||
|
||||
deleteIntegration: async ({ commit }, integrationId) => {
|
||||
commit(types.default.SET_INTEGRATIONS_UI_FLAG, { isDeleting: true });
|
||||
try {
|
||||
await IntegrationsAPI.delete(integrationId);
|
||||
commit(types.default.DELETE_INTEGRATION, {
|
||||
id: integrationId,
|
||||
enabled: false,
|
||||
});
|
||||
commit(types.default.SET_INTEGRATIONS_UI_FLAG, { isDeleting: false });
|
||||
} catch (error) {
|
||||
commit(types.default.SET_INTEGRATIONS_UI_FLAG, { isDeleting: false });
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export const mutations = {
|
||||
[types.default.SET_INTEGRATIONS_UI_FLAG]($state, uiFlag) {
|
||||
$state.uiFlags = { ...$state.uiFlags, ...uiFlag };
|
||||
},
|
||||
[types.default.SET_INTEGRATIONS]: MutationHelpers.set,
|
||||
[types.default.ADD_INTEGRATION]: MutationHelpers.updateAttributes,
|
||||
[types.default.DELETE_INTEGRATION]: MutationHelpers.updateAttributes,
|
||||
};
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state,
|
||||
getters,
|
||||
actions,
|
||||
mutations,
|
||||
};
|
|
@ -0,0 +1,72 @@
|
|||
import axios from 'axios';
|
||||
import { actions } from '../../integrations';
|
||||
import * as types from '../../../mutation-types';
|
||||
import integrationsList from './fixtures';
|
||||
|
||||
const commit = jest.fn();
|
||||
global.axios = axios;
|
||||
jest.mock('axios');
|
||||
|
||||
describe('#actions', () => {
|
||||
describe('#get', () => {
|
||||
it('sends correct actions if API is success', async () => {
|
||||
axios.get.mockResolvedValue({ data: integrationsList });
|
||||
await actions.get({ commit });
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.default.SET_INTEGRATIONS_UI_FLAG, { isFetching: true }],
|
||||
[types.default.SET_INTEGRATIONS, integrationsList.payload],
|
||||
[types.default.SET_INTEGRATIONS_UI_FLAG, { isFetching: false }],
|
||||
]);
|
||||
});
|
||||
it('sends correct actions if API is error', async () => {
|
||||
axios.get.mockRejectedValue({ message: 'Incorrect header' });
|
||||
await actions.get({ commit });
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.default.SET_INTEGRATIONS_UI_FLAG, { isFetching: true }],
|
||||
[types.default.SET_INTEGRATIONS_UI_FLAG, { isFetching: false }],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#connectSlack:', () => {
|
||||
it('sends correct actions if API is success', async () => {
|
||||
let data = { id: 'slack', enabled: true };
|
||||
axios.post.mockResolvedValue({ data: data });
|
||||
await actions.connectSlack({ commit });
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.default.SET_INTEGRATIONS_UI_FLAG, { isUpdating: true }],
|
||||
[types.default.ADD_INTEGRATION, data],
|
||||
[types.default.SET_INTEGRATIONS_UI_FLAG, { isUpdating: false }],
|
||||
]);
|
||||
});
|
||||
it('sends correct actions if API is error', async () => {
|
||||
axios.post.mockRejectedValue({ message: 'Incorrect header' });
|
||||
await actions.connectSlack({ commit });
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.default.SET_INTEGRATIONS_UI_FLAG, { isUpdating: true }],
|
||||
[types.default.SET_INTEGRATIONS_UI_FLAG, { isUpdating: false }],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#deleteIntegration:', () => {
|
||||
it('sends correct actions if API is success', async () => {
|
||||
let data = { id: 'slack', enabled: false };
|
||||
axios.delete.mockResolvedValue({ data: data });
|
||||
await actions.deleteIntegration({ commit }, data.id);
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.default.SET_INTEGRATIONS_UI_FLAG, { isDeleting: true }],
|
||||
[types.default.DELETE_INTEGRATION, data],
|
||||
[types.default.SET_INTEGRATIONS_UI_FLAG, { isDeleting: false }],
|
||||
]);
|
||||
});
|
||||
it('sends correct actions if API is error', async () => {
|
||||
axios.delete.mockRejectedValue({ message: 'Incorrect header' });
|
||||
await actions.deleteIntegration({ commit });
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.default.SET_INTEGRATIONS_UI_FLAG, { isDeleting: true }],
|
||||
[types.default.SET_INTEGRATIONS_UI_FLAG, { isDeleting: false }],
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,16 @@
|
|||
export default {
|
||||
payload: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'test1',
|
||||
logo: 'test',
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'test2',
|
||||
logo: 'test',
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
};
|
|
@ -0,0 +1,51 @@
|
|||
import { getters } from '../../integrations';
|
||||
|
||||
describe('#getters', () => {
|
||||
it('getIntegrations', () => {
|
||||
const state = {
|
||||
records: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'test1',
|
||||
logo: 'test',
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'test2',
|
||||
logo: 'test',
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(getters.getIntegrations(state)).toEqual([
|
||||
{
|
||||
id: 1,
|
||||
name: 'test1',
|
||||
logo: 'test',
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'test2',
|
||||
logo: 'test',
|
||||
enabled: true,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('getUIFlags', () => {
|
||||
const state = {
|
||||
uiFlags: {
|
||||
isFetching: true,
|
||||
isFetchingItem: false,
|
||||
isUpdating: false,
|
||||
},
|
||||
};
|
||||
expect(getters.getUIFlags(state)).toEqual({
|
||||
isFetching: true,
|
||||
isFetchingItem: false,
|
||||
isUpdating: false,
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,26 @@
|
|||
import * as types from '../../../mutation-types';
|
||||
import { mutations } from '../../integrations';
|
||||
|
||||
describe('#mutations', () => {
|
||||
describe('#GET_INTEGRATIONS', () => {
|
||||
it('set integrations records', () => {
|
||||
const state = { records: [] };
|
||||
mutations[types.default.SET_INTEGRATIONS](state, [
|
||||
{
|
||||
id: 1,
|
||||
name: 'test1',
|
||||
logo: 'test',
|
||||
enabled: true,
|
||||
},
|
||||
]);
|
||||
expect(state.records).toEqual([
|
||||
{
|
||||
id: 1,
|
||||
name: 'test1',
|
||||
logo: 'test',
|
||||
enabled: true,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -67,6 +67,12 @@ export default {
|
|||
EDIT_CANNED: 'EDIT_CANNED',
|
||||
DELETE_CANNED: 'DELETE_CANNED',
|
||||
|
||||
// Integrations
|
||||
SET_INTEGRATIONS_UI_FLAG: 'SET_INTEGRATIONS_UI_FLAG',
|
||||
SET_INTEGRATIONS: 'SET_INTEGRATIONS',
|
||||
ADD_INTEGRATION: 'ADD_INTEGRATION',
|
||||
DELETE_INTEGRATION: 'DELETE_INTEGRATION',
|
||||
|
||||
// WebHook
|
||||
SET_WEBHOOK_UI_FLAG: 'SET_WEBHOOK_UI_FLAG',
|
||||
SET_WEBHOOK: 'SET_WEBHOOK',
|
||||
|
|
|
@ -25,6 +25,15 @@ export const update = (state, data) => {
|
|||
});
|
||||
};
|
||||
|
||||
/* when you don't want to overwrite the whole object */
|
||||
export const updateAttributes = (state, data) => {
|
||||
state.records.forEach((element, index) => {
|
||||
if (element.id === data.id) {
|
||||
Vue.set(state.records, index, { ...state.records[index], ...data });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const destroy = (state, id) => {
|
||||
state.records = state.records.filter(record => record.id !== id);
|
||||
};
|
||||
|
|
|
@ -25,12 +25,39 @@ class Integrations::App
|
|||
params[:fields]
|
||||
end
|
||||
|
||||
def button
|
||||
params[:button]
|
||||
def action
|
||||
case params[:id]
|
||||
when 'slack'
|
||||
"#{params[:action]}&client_id=#{ENV['SLACK_CLIENT_ID']}&redirect_uri=#{self.class.slack_integration_url}"
|
||||
else
|
||||
params[:action]
|
||||
end
|
||||
end
|
||||
|
||||
def active?
|
||||
case params[:id]
|
||||
when 'slack'
|
||||
ENV['SLACK_CLIENT_SECRET'].present?
|
||||
else
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
def enabled?(account)
|
||||
account.hooks.where(app_id: id).exists?
|
||||
case params[:id]
|
||||
when 'slack'
|
||||
account.hooks.where(app_id: id).exists?
|
||||
else
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
def hooks
|
||||
Current.account.hooks.where(app_id: id)
|
||||
end
|
||||
|
||||
def self.slack_integration_url
|
||||
"#{ENV['FRONTEND_URL']}/app/accounts/#{Current.account.id}/settings/integrations/slack"
|
||||
end
|
||||
|
||||
class << self
|
||||
|
|
|
@ -31,6 +31,6 @@ class Integrations::Hook < ApplicationRecord
|
|||
end
|
||||
|
||||
def slack?
|
||||
app_id == 'cw_slack'
|
||||
app_id == 'slack'
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
json.array! @apps do |app|
|
||||
json.id app.id
|
||||
json.name app.name
|
||||
json.logo app.logo
|
||||
json.enabled app.enabled?(@current_account)
|
||||
json.button app.button
|
||||
json.payload do
|
||||
json.array! @apps do |app|
|
||||
json.id app.id
|
||||
json.name app.name
|
||||
json.description app.description
|
||||
json.logo app.logo
|
||||
json.enabled app.enabled?(@current_account)
|
||||
json.action app.action
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,4 +4,4 @@ json.logo @app.logo
|
|||
json.description @app.description
|
||||
json.fields @app.fields
|
||||
json.enabled @app.enabled?(@current_account)
|
||||
json.button @app.button
|
||||
json.button @app.action
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
json.id @hook.app_id
|
||||
json.enabled true
|
|
@ -50,4 +50,5 @@ Rails.application.configure do
|
|||
|
||||
# Raises error for missing translations.
|
||||
# config.action_view.raise_on_missing_translations = true
|
||||
config.log_level = ENV.fetch('LOG_LEVEL', 'debug').to_sym
|
||||
end
|
||||
|
|
|
@ -1,6 +1,13 @@
|
|||
slack:
|
||||
id: cw_slack
|
||||
id: slack
|
||||
name: Slack
|
||||
logo: https://a.slack-edge.com/80588/marketing/img/media-kit/img-logos@2x.png
|
||||
description: "'Be less busy' - Slack is the chat tool that brings all your communication together in one place. By integrating Slack with SupportBee, you can get notified in your Slack channels for important events in your support desk"
|
||||
button: <a href="https://slack.com/oauth/v2/authorize?scope=incoming-webhook,commands,chat:write&client_id=706921004289.1094198503990"><img alt=""Add to Slack"" height="40" width="139" src="https://platform.slack-edge.com/img/add_to_slack.png" srcset="https://platform.slack-edge.com/img/add_to_slack.png 1x, https://platform.slack-edge.com/img/add_to_slack@2x.png 2x" /></a>
|
||||
logo: slack.png
|
||||
description: "Slack is a chat tool that brings all your communication together in one place. By integrating Slack, you can get notified of all the new conversations in your account right inside your Slack."
|
||||
action: https://slack.com/oauth/v2/authorize?scope=commands,chat:write,channels:manage,channels:join,groups:write,im:write,mpim:write,users:read,users:read.email,chat:write.customize
|
||||
webhooks:
|
||||
id: webhook
|
||||
name: Webhooks
|
||||
logo: cable.svg
|
||||
description: Webhook events provide you the realtime information about what's happening in your account. You can make use of the webhooks to communicate the events to your favourite apps like Slack or Github. Click on Configure to set up your webhooks.
|
||||
action: /webhook
|
||||
|
|
@ -90,7 +90,7 @@ Rails.application.routes.draw do
|
|||
resources :webhooks, except: [:show]
|
||||
namespace :integrations do
|
||||
resources :apps, only: [:index, :show]
|
||||
resources :slack, only: [:create, :update, :destroy]
|
||||
resource :slack, only: [:create, :update, :destroy], controller: 'slack'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
# loading installation configs
|
||||
GlobalConfig.clear_cache
|
||||
ConfigLoader.new.process
|
||||
|
||||
account = Account.create!(
|
||||
|
|
39
docs/development/project-setup/slack-integration-setup.md
Normal file
39
docs/development/project-setup/slack-integration-setup.md
Normal file
|
@ -0,0 +1,39 @@
|
|||
---
|
||||
path: "/docs/slack-integration-setup"
|
||||
title: "Setting Up Slack Intergration"
|
||||
---
|
||||
|
||||
### Register A Facebook App
|
||||
|
||||
To use Slack Integration, you have to create an Slack app in developer portal. You can find more details about creating Slack apps [here](https://api.slack.com/)
|
||||
|
||||
Once you register your Slack App, you will have to obtain the `Client Id` and `Client Secret` . These values will be available in the app settings and will be required while setting up Chatwoot environment variables.
|
||||
|
||||
### Configure the Slack App
|
||||
|
||||
1) Create a slack app and add it to your development workspace.
|
||||
2) Obtain the `Client Id` and `Client Secret` for the app and configure it in your Chatwoot environment variables.
|
||||
3) Head over to the `oauth & permissions` section under `features` tab.
|
||||
4) In the redirect urls, Add your Chatwoot installation base url.
|
||||
5) In the scopes section configure the given scopes for bot token scopes.
|
||||
`commands,chat:write,channels:manage,channels:join,groups:write,im:write,mpim:write,users:read,users:read.email,chat:write.customize`
|
||||
6) Head over to the `events subscriptions` section under `features` tab.
|
||||
7) Enable events and configure the the given request url `{chatwoot installation url}/api/v1/integrations/webhooks`
|
||||
8) Subscribe to the following bot events `message.channels` , `message.groups`, `message.im`, `message.mpim`
|
||||
9) Connect slack integration on Chatwoot app and get productive.
|
||||
|
||||
### Configuring the Environment Variables in Chatwoot
|
||||
|
||||
Configure the following Chatwoot environment variables with the values you have obtained during the slack app setup.
|
||||
|
||||
```bash
|
||||
SLACK_CLIENT_ID=
|
||||
SLACK_CLIENT_SECRET=
|
||||
```
|
||||
|
||||
### Test your Setup
|
||||
|
||||
1. Ensure that you are recieving the Chatwoot messages in `customer-conversations` channel
|
||||
2. Add a message to that thread and ensure its coming back on to Chatwoot
|
||||
3. Add `note:` or `private:` in front on the slack message see if its coming out as private notes
|
||||
4. If your slack member email matches their email on Chatwoot, the messages will be associated with their Chatwoot user account.
|
|
@ -6,7 +6,7 @@ class Integrations::Slack::ChannelBuilder
|
|||
end
|
||||
|
||||
def perform
|
||||
create_channel
|
||||
find_or_create_channel
|
||||
update_reference_id
|
||||
end
|
||||
|
||||
|
@ -23,11 +23,12 @@ class Integrations::Slack::ChannelBuilder
|
|||
Slack::Web::Client.new
|
||||
end
|
||||
|
||||
def create_channel
|
||||
@channel = slack_client.conversations_create(name: params[:channel])
|
||||
def find_or_create_channel
|
||||
exisiting_channel = slack_client.conversations_list.channels.find { |channel| channel['name'] == params[:channel] }
|
||||
@channel = exisiting_channel || slack_client.conversations_create(name: params[:channel])['channel']
|
||||
end
|
||||
|
||||
def update_reference_id
|
||||
@hook.reference_id = channel['channel']['id']
|
||||
@hook.update(reference_id: channel['id'])
|
||||
end
|
||||
end
|
||||
|
|
|
@ -13,7 +13,7 @@ class Integrations::Slack::HookBuilder
|
|||
status: 'enabled',
|
||||
inbox_id: params[:inbox_id],
|
||||
hook_type: hook_type,
|
||||
app_id: 'cw_slack'
|
||||
app_id: 'slack'
|
||||
)
|
||||
|
||||
hook.save!
|
||||
|
@ -32,11 +32,12 @@ class Integrations::Slack::HookBuilder
|
|||
|
||||
def fetch_access_token
|
||||
client = Slack::Web::Client.new
|
||||
|
||||
client.oauth_access(
|
||||
slack_access = client.oauth_v2_access(
|
||||
client_id: ENV.fetch('SLACK_CLIENT_ID', 'TEST_CLIENT_ID'),
|
||||
client_secret: ENV.fetch('SLACK_CLIENT_SECRET', 'TEST_CLIENT_SECRET'),
|
||||
code: params[:code]
|
||||
)['bot']['bot_access_token']
|
||||
code: params[:code],
|
||||
redirect_uri: Integrations::App.slack_integration_url
|
||||
)
|
||||
slack_access['access_token']
|
||||
end
|
||||
end
|
||||
|
|
|
@ -34,7 +34,7 @@ class Integrations::Slack::IncomingMessageBuilder
|
|||
end
|
||||
|
||||
def supported_message?
|
||||
SUPPORTED_MESSAGE_TYPES.include?(message[:type])
|
||||
SUPPORTED_MESSAGE_TYPES.include?(message[:type]) if message.present?
|
||||
end
|
||||
|
||||
def hook_verification?
|
||||
|
@ -46,7 +46,7 @@ class Integrations::Slack::IncomingMessageBuilder
|
|||
end
|
||||
|
||||
def message
|
||||
params[:event][:blocks].first
|
||||
params[:event][:blocks]&.first
|
||||
end
|
||||
|
||||
def verify_hook
|
||||
|
@ -56,23 +56,42 @@ class Integrations::Slack::IncomingMessageBuilder
|
|||
end
|
||||
|
||||
def integration_hook
|
||||
@integration_hook ||= Integrations::Hook.where(reference_id: params[:event][:channel])
|
||||
@integration_hook ||= Integrations::Hook.find_by(reference_id: params[:event][:channel])
|
||||
end
|
||||
|
||||
def conversation
|
||||
@conversation ||= Conversation.where(identifier: params[:event][:thread_ts]).first
|
||||
end
|
||||
|
||||
def sender
|
||||
user_email = slack_client.users_info(user: params[:event][:user])[:user][:profile][:email]
|
||||
conversation.account.users.find_by(email: user_email)
|
||||
end
|
||||
|
||||
def private_note?
|
||||
params[:event][:text].strip.starts_with?('note:', 'private:')
|
||||
end
|
||||
|
||||
def create_message
|
||||
return unless conversation
|
||||
|
||||
conversation.messages.create(
|
||||
message_type: 0,
|
||||
message_type: :outgoing,
|
||||
account_id: conversation.account_id,
|
||||
inbox_id: conversation.inbox_id,
|
||||
content: message[:elements].first[:elements].first[:text]
|
||||
content: params[:event][:text],
|
||||
source_id: "slack_#{params[:event][:ts]}",
|
||||
private: private_note?,
|
||||
user: sender
|
||||
)
|
||||
|
||||
{ status: 'success' }
|
||||
end
|
||||
|
||||
def slack_client
|
||||
Slack.configure do |config|
|
||||
config.token = integration_hook.access_token
|
||||
end
|
||||
Slack::Web::Client.new
|
||||
end
|
||||
end
|
||||
|
|
|
@ -11,6 +11,8 @@ class Integrations::Slack::OutgoingMessageBuilder
|
|||
end
|
||||
|
||||
def perform
|
||||
return if message.source_id.present?
|
||||
|
||||
send_message
|
||||
update_reference_id
|
||||
end
|
||||
|
@ -25,12 +27,32 @@ class Integrations::Slack::OutgoingMessageBuilder
|
|||
@contact ||= conversation.contact
|
||||
end
|
||||
|
||||
def agent
|
||||
@agent ||= message.user
|
||||
end
|
||||
|
||||
def message_content
|
||||
if conversation.identifier.present?
|
||||
message.content
|
||||
else
|
||||
"*Inbox: #{message.inbox.name}* \n\n #{message.content}"
|
||||
end
|
||||
end
|
||||
|
||||
def avatar_url(sender)
|
||||
sender.try(:avatar_url) || "#{ENV['FRONTEND_URL']}/admin/avatar_square.png"
|
||||
end
|
||||
|
||||
def send_message
|
||||
sender = message.outgoing? ? agent : contact
|
||||
sender_type = sender.class == Contact ? 'Contact' : 'Agent'
|
||||
|
||||
@slack_message = slack_client.chat_postMessage(
|
||||
channel: hook.reference_id,
|
||||
text: message.content,
|
||||
username: contact.try(:name),
|
||||
thread_ts: conversation.identifier
|
||||
text: message_content,
|
||||
username: "#{sender_type}: #{sender.try(:name)}",
|
||||
thread_ts: conversation.identifier,
|
||||
icon_url: avatar_url(sender)
|
||||
)
|
||||
end
|
||||
|
||||
|
|
BIN
public/admin/avatar_square.png
Normal file
BIN
public/admin/avatar_square.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.4 KiB |
Before Width: | Height: | Size: 3 KiB After Width: | Height: | Size: 3 KiB |
BIN
public/assets/dashboard/integrations/slack.png
Normal file
BIN
public/assets/dashboard/integrations/slack.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.3 KiB |
|
@ -20,9 +20,9 @@ RSpec.describe 'Integration Apps API', type: :request do
|
|||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
app = JSON.parse(response.body).first
|
||||
expect(app['id']).to eql('cw_slack')
|
||||
expect(app['name']).to eql('Slack')
|
||||
app = JSON.parse(response.body)['payload'].first
|
||||
expect(app['id']).to eql('webhook')
|
||||
expect(app['name']).to eql('Webhooks')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -30,7 +30,7 @@ RSpec.describe 'Integration Apps API', type: :request do
|
|||
describe 'GET /api/v1/integrations/apps/:id' do
|
||||
context 'when it is an unauthenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
get api_v1_account_integrations_app_url(account_id: account.id, id: 'cw_slack')
|
||||
get api_v1_account_integrations_app_url(account_id: account.id, id: 'slack')
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
@ -39,13 +39,13 @@ RSpec.describe 'Integration Apps API', type: :request do
|
|||
let(:agent) { create(:user, account: account, role: :agent) }
|
||||
|
||||
it 'returns details of the app' do
|
||||
get api_v1_account_integrations_app_url(account_id: account.id, id: 'cw_slack'),
|
||||
get api_v1_account_integrations_app_url(account_id: account.id, id: 'slack'),
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
app = JSON.parse(response.body)
|
||||
expect(app['id']).to eql('cw_slack')
|
||||
expect(app['id']).to eql('slack')
|
||||
expect(app['name']).to eql('Slack')
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,7 +3,7 @@ FactoryBot.define do
|
|||
status { 1 }
|
||||
inbox_id { 1 }
|
||||
account_id { 1 }
|
||||
app_id { 'cw_slack' }
|
||||
app_id { 'slack' }
|
||||
settings { 'MyText' }
|
||||
hook_type { 1 }
|
||||
access_token { SecureRandom.hex }
|
||||
|
|
|
@ -10,7 +10,7 @@ describe Integrations::Slack::HookBuilder do
|
|||
hooks_count = account.hooks.count
|
||||
|
||||
builder = described_class.new(account: account, code: code)
|
||||
builder.stub(:fetch_access_token) { token }
|
||||
allow(builder).to receive(:fetch_access_token).and_return(token)
|
||||
|
||||
builder.perform
|
||||
expect(account.hooks.count).to eql(hooks_count + 1)
|
||||
|
|
|
@ -5,7 +5,7 @@ describe Integrations::Slack::IncomingMessageBuilder do
|
|||
let(:message_params) { slack_message_stub }
|
||||
let(:verification_params) { slack_url_verification_stub }
|
||||
|
||||
let(:hook) { create(:integrations_hook, account: account, reference_id: message_params[:event][:channel]) }
|
||||
let!(:hook) { create(:integrations_hook, account: account, reference_id: message_params[:event][:channel]) }
|
||||
let!(:conversation) { create(:conversation, identifier: message_params[:event][:thread_ts]) }
|
||||
|
||||
describe '#perform' do
|
||||
|
@ -19,8 +19,10 @@ describe Integrations::Slack::IncomingMessageBuilder do
|
|||
|
||||
context 'when message creation' do
|
||||
it 'creates message' do
|
||||
expect(hook).not_to eq nil
|
||||
messages_count = conversation.messages.count
|
||||
builder = described_class.new(message_params)
|
||||
allow(builder).to receive(:sender).and_return(nil)
|
||||
builder.perform
|
||||
expect(conversation.messages.count).to eql(messages_count + 1)
|
||||
end
|
||||
|
|
|
@ -14,15 +14,16 @@ describe Integrations::Slack::OutgoingMessageBuilder do
|
|||
builder = described_class.new(hook, message)
|
||||
stub_request(:post, 'https://slack.com/api/chat.postMessage')
|
||||
.to_return(status: 200, body: '', headers: {})
|
||||
slack_client = double
|
||||
expect(builder).to receive(:slack_client).and_return(slack_client)
|
||||
|
||||
# rubocop:disable RSpec/AnyInstance
|
||||
allow_any_instance_of(Slack::Web::Client).to receive(:chat_postMessage).with(
|
||||
expect(slack_client).to receive(:chat_postMessage).with(
|
||||
channel: hook.reference_id,
|
||||
text: message.content,
|
||||
username: contact.name,
|
||||
thread_ts: conversation.identifier
|
||||
username: "Contact: #{contact.name}",
|
||||
thread_ts: conversation.identifier,
|
||||
icon_url: anything
|
||||
)
|
||||
# rubocop:enable RSpec/AnyInstance
|
||||
|
||||
builder.perform
|
||||
end
|
||||
|
|
|
@ -3,7 +3,7 @@ require 'rails_helper'
|
|||
RSpec.describe 'Api::V1::Accounts::Integrations::Slacks', type: :request do
|
||||
let(:account) { create(:account) }
|
||||
let(:agent) { create(:user, account: account, role: :agent) }
|
||||
let(:hook) { create(:integrations_hook, account: account) }
|
||||
let!(:hook) { create(:integrations_hook, account: account) }
|
||||
|
||||
describe 'POST /api/v1/accounts/{account.id}/integrations/slack' do
|
||||
context 'when it is an unauthenticated user' do
|
||||
|
@ -16,24 +16,27 @@ RSpec.describe 'Api::V1::Accounts::Integrations::Slacks', type: :request do
|
|||
context 'when it is an authenticated user' do
|
||||
it 'creates hook' do
|
||||
hook_builder = Integrations::Slack::HookBuilder.new(account: account, code: SecureRandom.hex)
|
||||
hook_builder.stub(:fetch_access_token) { SecureRandom.hex }
|
||||
|
||||
expect(hook_builder).to receive(:fetch_access_token).and_return(SecureRandom.hex)
|
||||
expect(Integrations::Slack::HookBuilder).to receive(:new).and_return(hook_builder)
|
||||
|
||||
channel_builder = Integrations::Slack::ChannelBuilder.new(hook: hook, channel: 'channel')
|
||||
expect(channel_builder).to receive(:perform)
|
||||
expect(Integrations::Slack::ChannelBuilder).to receive(:new).and_return(channel_builder)
|
||||
|
||||
post "/api/v1/accounts/#{account.id}/integrations/slack",
|
||||
params: { code: SecureRandom.hex },
|
||||
headers: agent.create_new_auth_token
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
json_response = JSON.parse(response.body)
|
||||
expect(json_response['app_id']).to eql('cw_slack')
|
||||
expect(json_response['id']).to eql('slack')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'PUT /api/v1/accounts/{account.id}/integrations/slack/{id}' do
|
||||
describe 'PUT /api/v1/accounts/{account.id}/integrations/slack/' do
|
||||
context 'when it is an unauthenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
put "/api/v1/accounts/#{account.id}/integrations/slack/#{hook.id}", params: {}
|
||||
put "/api/v1/accounts/#{account.id}/integrations/slack/", params: {}
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
@ -41,32 +44,32 @@ RSpec.describe 'Api::V1::Accounts::Integrations::Slacks', type: :request do
|
|||
context 'when it is an authenticated user' do
|
||||
it 'updates hook' do
|
||||
channel_builder = Integrations::Slack::ChannelBuilder.new(hook: hook, channel: 'channel')
|
||||
channel_builder.stub(:perform)
|
||||
expect(channel_builder).to receive(:perform)
|
||||
|
||||
expect(Integrations::Slack::ChannelBuilder).to receive(:new).and_return(channel_builder)
|
||||
|
||||
put "/api/v1/accounts/#{account.id}/integrations/slack/#{hook.id}",
|
||||
put "/api/v1/accounts/#{account.id}/integrations/slack",
|
||||
params: { channel: SecureRandom.hex },
|
||||
headers: agent.create_new_auth_token
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
json_response = JSON.parse(response.body)
|
||||
expect(json_response['app_id']).to eql('cw_slack')
|
||||
expect(json_response['app_id']).to eql('slack')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'DELETE /api/v1/accounts/{account.id}/integrations/slack/{id}' do
|
||||
describe 'DELETE /api/v1/accounts/{account.id}/integrations/slack' do
|
||||
context 'when it is an unauthenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
delete "/api/v1/accounts/#{account.id}/integrations/slack/#{hook.id}", params: {}
|
||||
delete "/api/v1/accounts/#{account.id}/integrations/slack", params: {}
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
it 'deletes hook' do
|
||||
delete "/api/v1/accounts/#{account.id}/integrations/slack/#{hook.id}",
|
||||
delete "/api/v1/accounts/#{account.id}/integrations/slack",
|
||||
headers: agent.create_new_auth_token
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(Integrations::Hook.find_by(id: hook.id)).to be nil
|
||||
|
|
|
@ -4,7 +4,7 @@ RSpec.describe 'Api::V1::Integrations::Webhooks', type: :request do
|
|||
describe 'POST /api/v1/integrations/webhooks' do
|
||||
it 'consumes webhook' do
|
||||
builder = Integrations::Slack::IncomingMessageBuilder.new({})
|
||||
builder.stub(:perform) { true }
|
||||
expect(builder).to receive(:perform).and_return(true)
|
||||
|
||||
expect(Integrations::Slack::IncomingMessageBuilder).to receive(:new).and_return(builder)
|
||||
|
||||
|
|
|
@ -7,41 +7,12 @@ module SlackStubs
|
|||
}
|
||||
end
|
||||
|
||||
# rubocop:disable Metrics/MethodLength
|
||||
def slack_message_stub
|
||||
{
|
||||
"token": '[FILTERED]',
|
||||
"team_id": 'TLST3048H',
|
||||
"api_app_id": 'A012S5UETV4',
|
||||
"event": {
|
||||
"client_msg_id": 'ffc6e64e-6f0c-4a3d-b594-faa6b44e48ab',
|
||||
"type": 'message',
|
||||
"text": 'this is test',
|
||||
"user": 'ULYPAKE5S',
|
||||
"ts": '1588623033.006000',
|
||||
"team": 'TLST3048H',
|
||||
"blocks": [
|
||||
{
|
||||
"type": 'rich_text',
|
||||
"block_id": 'jaIv3',
|
||||
"elements": [
|
||||
{
|
||||
"type": 'rich_text_section',
|
||||
"elements": [
|
||||
{
|
||||
"type": 'text',
|
||||
"text": 'this is test'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"thread_ts": '1588623023.005900',
|
||||
"channel": 'G01354F6A6Q',
|
||||
"event_ts": '1588623033.006000',
|
||||
"channel_type": 'group'
|
||||
},
|
||||
"event": message_event,
|
||||
"type": 'event_callback',
|
||||
"event_id": 'Ev013QUX3WV6',
|
||||
"event_time": 1_588_623_033,
|
||||
|
@ -49,5 +20,38 @@ module SlackStubs
|
|||
"webhook": {}
|
||||
}
|
||||
end
|
||||
# rubocop:enable Metrics/MethodLength
|
||||
|
||||
def message_event
|
||||
{ "client_msg_id": 'ffc6e64e-6f0c-4a3d-b594-faa6b44e48ab',
|
||||
"type": 'message',
|
||||
"text": 'this is test',
|
||||
"user": 'ULYPAKE5S',
|
||||
"ts": '1588623033.006000',
|
||||
"team": 'TLST3048H',
|
||||
"blocks": message_blocks,
|
||||
"thread_ts": '1588623023.005900',
|
||||
"channel": 'G01354F6A6Q',
|
||||
"event_ts": '1588623033.006000',
|
||||
"channel_type": 'group' }
|
||||
end
|
||||
|
||||
def message_blocks
|
||||
[
|
||||
{
|
||||
"type": 'rich_text',
|
||||
"block_id": 'jaIv3',
|
||||
"elements": [
|
||||
{
|
||||
"type": 'rich_text_section',
|
||||
"elements": [
|
||||
{
|
||||
"type": 'text',
|
||||
"text": 'this is test'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue