From 1ef8d03e186c4df2e9ffb717634abfbc790fd110 Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Mon, 22 Jun 2020 13:19:26 +0530 Subject: [PATCH] Feature: Slack - receive messages, create threads, send replies (#974) Co-authored-by: Pranav Raj S --- .circleci/config.yml | 28 +++-- .env.example | 10 +- .../accounts/integrations/apps_controller.rb | 2 +- .../accounts/integrations/slack_controller.rb | 20 +-- app/javascript/dashboard/api/ApiClient.js | 7 +- app/javascript/dashboard/api/integrations.js | 21 ++++ .../scss/views/settings/integrations.scss | 7 +- .../dashboard/components/layout/Sidebar.vue | 1 - .../components/widgets/BackButton.vue | 18 ++- .../dashboard/i18n/default-sidebar.js | 1 + .../i18n/locale/en/generalSettings.json | 1 + .../i18n/locale/en/integrations.json | 11 +- .../dashboard/settings/SettingsHeader.vue | 6 +- .../routes/dashboard/settings/Wrapper.vue | 5 + .../dashboard/settings/integrations/Index.vue | 60 +++------ .../settings/integrations/Integration.vue | 114 ++++++++++++++++++ .../settings/integrations/ShowIntegration.vue | 70 +++++++++++ .../settings/integrations/Webhook.vue | 7 +- .../integrations/integrations.routes.js | 18 +++ app/javascript/dashboard/store/index.js | 2 + .../dashboard/store/modules/auth.js | 16 --- .../dashboard/store/modules/integrations.js | 83 +++++++++++++ .../specs/integrations/actions.spec.js | 72 +++++++++++ .../modules/specs/integrations/fixtures.js | 16 +++ .../specs/integrations/getters.spec.js | 51 ++++++++ .../specs/integrations/mutations.spec.js | 26 ++++ .../dashboard/store/mutation-types.js | 6 + .../shared/helpers/vuex/mutationHelpers.js | 9 ++ app/models/integrations/app.rb | 33 ++++- app/models/integrations/hook.rb | 2 +- .../integrations/apps/index.json.jbuilder | 15 ++- .../integrations/apps/show.json.jbuilder | 2 +- .../integrations/slack/create.json.jbuilder | 2 + config/environments/test.rb | 1 + config/integration/apps.yml | 15 ++- config/routes.rb | 2 +- db/seeds.rb | 1 + .../project-setup/slack-integration-setup.md | 39 ++++++ lib/integrations/slack/channel_builder.rb | 9 +- lib/integrations/slack/hook_builder.rb | 11 +- .../slack/incoming_message_builder.rb | 29 ++++- .../slack/outgoing_message_builder.rb | 28 ++++- public/admin/avatar_square.png | Bin 0 -> 4514 bytes .../assets/dashboard}/integrations/cable.svg | 0 .../assets/dashboard/integrations/slack.png | Bin 0 -> 1320 bytes .../integrations/apps_controller_spec.rb | 12 +- spec/factories/integrations/hooks.rb | 2 +- .../integrations/slack/hook_builder_spec.rb | 2 +- .../slack/incoming_message_builder_spec.rb | 4 +- .../slack/outgoing_message_builder_spec.rb | 11 +- .../integrations/slack_request_spec.rb | 27 +++-- .../v1/integrations/webhooks_request_spec.rb | 2 +- spec/support/slack_stubs.rb | 66 +++++----- 53 files changed, 815 insertions(+), 188 deletions(-) create mode 100644 app/javascript/dashboard/api/integrations.js create mode 100644 app/javascript/dashboard/routes/dashboard/settings/integrations/Integration.vue create mode 100644 app/javascript/dashboard/routes/dashboard/settings/integrations/ShowIntegration.vue create mode 100644 app/javascript/dashboard/store/modules/integrations.js create mode 100644 app/javascript/dashboard/store/modules/specs/integrations/actions.spec.js create mode 100644 app/javascript/dashboard/store/modules/specs/integrations/fixtures.js create mode 100644 app/javascript/dashboard/store/modules/specs/integrations/getters.spec.js create mode 100644 app/javascript/dashboard/store/modules/specs/integrations/mutations.spec.js create mode 100644 app/views/api/v1/accounts/integrations/slack/create.json.jbuilder create mode 100644 docs/development/project-setup/slack-integration-setup.md create mode 100644 public/admin/avatar_square.png rename {app/javascript/dashboard/assets/images => public/assets/dashboard}/integrations/cable.svg (100%) create mode 100644 public/assets/dashboard/integrations/slack.png diff --git a/.circleci/config.yml b/.circleci/config.yml index 9579c23a8..07f4a7991 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -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 diff --git a/.env.example b/.env.example index 06a39d17f..5adb4a848 100644 --- a/.env.example +++ b/.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 diff --git a/app/controllers/api/v1/accounts/integrations/apps_controller.rb b/app/controllers/api/v1/accounts/integrations/apps_controller.rb index 35b6f7c4d..f88cc7c5f 100644 --- a/app/controllers/api/v1/accounts/integrations/apps_controller.rb +++ b/app/controllers/api/v1/accounts/integrations/apps_controller.rb @@ -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 diff --git a/app/controllers/api/v1/accounts/integrations/slack_controller.rb b/app/controllers/api/v1/accounts/integrations/slack_controller.rb index 023deaa97..77cf05ade 100644 --- a/app/controllers/api/v1/accounts/integrations/slack_controller.rb +++ b/app/controllers/api/v1/accounts/integrations/slack_controller.rb @@ -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 diff --git a/app/javascript/dashboard/api/ApiClient.js b/app/javascript/dashboard/api/ApiClient.js index 9199109a1..097caa906 100644 --- a/app/javascript/dashboard/api/ApiClient.js +++ b/app/javascript/dashboard/api/ApiClient.js @@ -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() { diff --git a/app/javascript/dashboard/api/integrations.js b/app/javascript/dashboard/api/integrations.js new file mode 100644 index 000000000..587c94d43 --- /dev/null +++ b/app/javascript/dashboard/api/integrations.js @@ -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(); diff --git a/app/javascript/dashboard/assets/scss/views/settings/integrations.scss b/app/javascript/dashboard/assets/scss/views/settings/integrations.scss index e791f4cd7..b43bbb4bc 100644 --- a/app/javascript/dashboard/assets/scss/views/settings/integrations.scss +++ b/app/javascript/dashboard/assets/scss/views/settings/integrations.scss @@ -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; } } diff --git a/app/javascript/dashboard/components/layout/Sidebar.vue b/app/javascript/dashboard/components/layout/Sidebar.vue index b2ccf1697..88578e635 100644 --- a/app/javascript/dashboard/components/layout/Sidebar.vue +++ b/app/javascript/dashboard/components/layout/Sidebar.vue @@ -121,7 +121,6 @@ export default { computed: { ...mapGetters({ currentUser: 'getCurrentUser', - daysLeft: 'getTrialLeft', globalConfig: 'globalConfig/get', inboxes: 'inboxes/getInboxes', accountId: 'getCurrentAccountId', diff --git a/app/javascript/dashboard/components/widgets/BackButton.vue b/app/javascript/dashboard/components/widgets/BackButton.vue index 9b5085983..9bf87d131 100644 --- a/app/javascript/dashboard/components/widgets/BackButton.vue +++ b/app/javascript/dashboard/components/widgets/BackButton.vue @@ -1,14 +1,26 @@ \ No newline at end of file + diff --git a/app/javascript/dashboard/i18n/default-sidebar.js b/app/javascript/dashboard/i18n/default-sidebar.js index 5ada97da4..4b479f5f6 100644 --- a/app/javascript/dashboard/i18n/default-sidebar.js +++ b/app/javascript/dashboard/i18n/default-sidebar.js @@ -52,6 +52,7 @@ export const getSidebarItems = accountId => ({ 'settings_inbox_finish', 'settings_integrations', 'settings_integrations_webhook', + 'settings_integrations_integration', 'general_settings', 'general_settings_index', ], diff --git a/app/javascript/dashboard/i18n/locale/en/generalSettings.json b/app/javascript/dashboard/i18n/locale/en/generalSettings.json index 702680b92..31b5f9fe2 100644 --- a/app/javascript/dashboard/i18n/locale/en/generalSettings.json +++ b/app/javascript/dashboard/i18n/locale/en/generalSettings.json @@ -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" diff --git a/app/javascript/dashboard/i18n/locale/en/integrations.json b/app/javascript/dashboard/i18n/locale/en/integrations.json index 7259c038e..21df315bc 100644 --- a/app/javascript/dashboard/i18n/locale/en/integrations.json +++ b/app/javascript/dashboard/i18n/locale/en/integrations.json @@ -49,6 +49,15 @@ "NO": "No, Keep it" } } - } + }, + "DELETE": { + "BUTTON_TEXT": "Delete", + "API": { + "SUCCESS_MESSAGE": "Integration deleted successfully" + } + }, + "CONNECT": { + "BUTTON_TEXT": "Connect" + } } } diff --git a/app/javascript/dashboard/routes/dashboard/settings/SettingsHeader.vue b/app/javascript/dashboard/routes/dashboard/settings/SettingsHeader.vue index 442d14360..37374f03b 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/SettingsHeader.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/SettingsHeader.vue @@ -2,7 +2,7 @@

- + {{ headerTitle }}

@@ -45,6 +45,10 @@ export default { }, showBackButton: { type: Boolean, default: false }, showNewButton: { type: Boolean, default: false }, + backUrl: { + type: [String, Object], + default: '', + }, }, computed: { ...mapGetters({ diff --git a/app/javascript/dashboard/routes/dashboard/settings/Wrapper.vue b/app/javascript/dashboard/routes/dashboard/settings/Wrapper.vue index 04138cea1..c0aa1eae9 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/Wrapper.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/Wrapper.vue @@ -6,6 +6,7 @@ :header-title="$t(headerTitle)" :button-text="$t(headerButtonText)" :show-back-button="showBackButton" + :back-url="backUrl" :show-new-button="showNewButton" /> @@ -34,6 +35,10 @@ export default { type: Boolean, default: false, }, + backUrl: { + type: [String, Object], + default: '', + }, }, data() { return {}; diff --git a/app/javascript/dashboard/routes/dashboard/settings/integrations/Index.vue b/app/javascript/dashboard/routes/dashboard/settings/integrations/Index.vue index 5652677cd..6d8ca1895 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/integrations/Index.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/integrations/Index.vue @@ -3,38 +3,19 @@
-
-
-
- -
-
-

- {{ $t('INTEGRATION_SETTINGS.WEBHOOK.TITLE') }} -

-

- {{ - useInstallationName( - $t('INTEGRATION_SETTINGS.WEBHOOK.INTEGRATION_TXT'), - globalConfig.installationName - ) - }} -

-
-
- - - -
-
+
+
@@ -43,20 +24,19 @@ diff --git a/app/javascript/dashboard/routes/dashboard/settings/integrations/Integration.vue b/app/javascript/dashboard/routes/dashboard/settings/integrations/Integration.vue new file mode 100644 index 000000000..cdab7ceee --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/integrations/Integration.vue @@ -0,0 +1,114 @@ + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/integrations/ShowIntegration.vue b/app/javascript/dashboard/routes/dashboard/settings/integrations/ShowIntegration.vue new file mode 100644 index 000000000..3185f17b0 --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/integrations/ShowIntegration.vue @@ -0,0 +1,70 @@ + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/integrations/Webhook.vue b/app/javascript/dashboard/routes/dashboard/settings/integrations/Webhook.vue index 980f16b05..6f2ce7f40 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/integrations/Webhook.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/integrations/Webhook.vue @@ -82,16 +82,16 @@