diff --git a/.codeclimate.yml b/.codeclimate.yml index 0bf089f4b..feb368e23 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -26,3 +26,6 @@ exclude_patterns: - "node_modules/**/*" - "lib/tasks/auto_annotate_models.rake" - "app/test-matchers.js" + - "docs/*" + - "**/*.md" + - "**/*.yml" \ No newline at end of file diff --git a/.env.example b/.env.example index 566ed8dff..7240829b3 100644 --- a/.env.example +++ b/.env.example @@ -22,17 +22,6 @@ POSTGRES_PASSWORD= RAILS_ENV=development RAILS_MAX_THREADS=5 -#fb app -FB_VERIFY_TOKEN= -FB_APP_SECRET= -FB_APP_ID= - -#twitter app -TWITTER_APP_ID= -TWITTER_CONSUMER_KEY= -TWITTER_CONSUMER_SECRET= -TWITTER_ENVIRONMENT= - #mail MAILER_SENDER_EMAIL=accounts@chatwoot.com SMTP_PORT=1025 @@ -59,13 +48,25 @@ AWS_REGION= SENTRY_DSN= #Log settings -LOG_LEVEL= -LOG_SIZE= +LOG_LEVEL=info +LOG_SIZE=500 # Credentials to access sidekiq dashboard in production SIDEKIQ_AUTH_USERNAME= SIDEKIQ_AUTH_PASSWORD= +### This environment variables are only required if you are setting up social media channels +#facebook +FB_VERIFY_TOKEN= +FB_APP_SECRET= +FB_APP_ID= + +#twitter +TWITTER_APP_ID= +TWITTER_CONSUMER_KEY= +TWITTER_CONSUMER_SECRET= +TWITTER_ENVIRONMENT= + #### This environment variables are only required in hosted version which has billing ENABLE_BILLING= diff --git a/app/controllers/api/v1/accounts/callbacks_controller.rb b/app/controllers/api/v1/accounts/callbacks_controller.rb index bb0e0ca0b..ab276ba29 100644 --- a/app/controllers/api/v1/accounts/callbacks_controller.rb +++ b/app/controllers/api/v1/accounts/callbacks_controller.rb @@ -59,6 +59,8 @@ class Api::V1::Accounts::CallbacksController < Api::BaseController def long_lived_token(omniauth_token) koala = Koala::Facebook::OAuth.new(ENV['FB_APP_ID'], ENV['FB_APP_SECRET']) koala.exchange_access_token_info(omniauth_token)['access_token'] + rescue StandardError => e + Rails.logger e end def mark_already_existing_facebook_pages(data) diff --git a/app/javascript/dashboard/api/channel/fbChannel.js b/app/javascript/dashboard/api/channel/fbChannel.js index f9781097c..e53885b4a 100644 --- a/app/javascript/dashboard/api/channel/fbChannel.js +++ b/app/javascript/dashboard/api/channel/fbChannel.js @@ -3,7 +3,7 @@ import ApiClient from '../ApiClient'; class FBChannel extends ApiClient { constructor() { - super('facebook_indicators'); + super('facebook_indicators', { accountScoped: true }); } markSeen({ inboxId, contactId }) { @@ -22,7 +22,7 @@ class FBChannel extends ApiClient { create(params) { return axios.post( - `${this.apiVersion}/callbacks/register_facebook_page`, + `${this.url.replace(this.resource, '')}callbacks/register_facebook_page`, params ); } diff --git a/app/javascript/dashboard/api/channels.js b/app/javascript/dashboard/api/channels.js index f7db9afbc..25998b1a2 100644 --- a/app/javascript/dashboard/api/channels.js +++ b/app/javascript/dashboard/api/channels.js @@ -5,9 +5,9 @@ import endPoints from './endPoints'; export default { - fetchFacebookPages(token) { + fetchFacebookPages(token, accountId) { const urlData = endPoints('fetchFacebookPages'); urlData.params.omniauth_token = token; - return axios.post(urlData.url, urlData.params); + return axios.post(urlData.url(accountId), urlData.params); }, }; diff --git a/app/javascript/dashboard/api/endPoints.js b/app/javascript/dashboard/api/endPoints.js index e5640af2b..53c669eb6 100644 --- a/app/javascript/dashboard/api/endPoints.js +++ b/app/javascript/dashboard/api/endPoints.js @@ -28,7 +28,9 @@ const endPoints = { }, fetchFacebookPages: { - url: 'api/v1/callbacks/facebook_pages.json', + url(accountId) { + return `api/v1/accounts/${accountId}/callbacks/facebook_pages.json`; + }, params: { omniauth_token: '' }, }, diff --git a/app/javascript/dashboard/api/specs/channel/fbChannel.spec.js b/app/javascript/dashboard/api/specs/channel/fbChannel.spec.js new file mode 100644 index 000000000..63ae1492d --- /dev/null +++ b/app/javascript/dashboard/api/specs/channel/fbChannel.spec.js @@ -0,0 +1,15 @@ +import fbChannel from '../../channel/fbChannel'; +import ApiClient from '../../ApiClient'; + +describe('#FBChannel', () => { + it('creates correct instance', () => { + expect(fbChannel).toBeInstanceOf(ApiClient); + expect(fbChannel).toHaveProperty('get'); + expect(fbChannel).toHaveProperty('show'); + expect(fbChannel).toHaveProperty('create'); + expect(fbChannel).toHaveProperty('update'); + expect(fbChannel).toHaveProperty('delete'); + expect(fbChannel).toHaveProperty('markSeen'); + expect(fbChannel).toHaveProperty('toggleTyping'); + }); +}); diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Facebook.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Facebook.vue index 15199c08d..5a838814d 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Facebook.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Facebook.vue @@ -68,6 +68,7 @@ /* global FB */ import { required } from 'vuelidate/lib/validators'; import LoadingState from 'dashboard/components/widgets/LoadingState'; +import { mapGetters } from 'vuex'; import ChannelApi from '../../../../../api/channels'; import PageHeader from '../../SettingsSubPageHeader'; import router from '../../../../index'; @@ -111,6 +112,12 @@ export default { getSelectablePages() { return this.pageList.filter(item => !item.exists); }, + ...mapGetters({ + currentUser: 'getCurrentUser', + }), + accountId() { + return this.currentUser.account_id; + }, }, created() { @@ -194,13 +201,20 @@ export default { ); }, - fetchPages(_token) { - ChannelApi.fetchFacebookPages(_token) - .then(response => { - this.pageList = response.data.data.page_details; - this.user_access_token = response.data.data.user_access_token; - }) - .catch(); + async fetchPages(_token) { + try { + const response = await ChannelApi.fetchFacebookPages( + _token, + this.accountId + ); + const { + data: { data }, + } = response; + this.pageList = data.page_details; + this.user_access_token = data.user_access_token; + } catch (error) { + // Ignore error + } }, channelParams() { diff --git a/app/models/attachment.rb b/app/models/attachment.rb index 79ec890cf..697de2767 100644 --- a/app/models/attachment.rb +++ b/app/models/attachment.rb @@ -30,6 +30,18 @@ class Attachment < ApplicationRecord base_data.merge(file_metadata) end + def file_url + file.attached? ? url_for(file) : '' + end + + def thumb_url + if file.attached? && file.representable? + url_for(file.representation(resize: '250x250')) + else + '' + end + end + private def file_metadata @@ -64,16 +76,4 @@ class Attachment < ApplicationRecord account_id: account_id } end - - def file_url - file.attached? ? url_for(file) : '' - end - - def thumb_url - if file.attached? && file.representable? - url_for(file.representation(resize: '250x250')) - else - '' - end - end end diff --git a/app/services/facebook/send_reply_service.rb b/app/services/facebook/send_reply_service.rb index ebc469149..87e07fe8c 100644 --- a/app/services/facebook/send_reply_service.rb +++ b/app/services/facebook/send_reply_service.rb @@ -32,13 +32,35 @@ class Facebook::SendReplyService # end # end - def fb_message_params + def fb_text_message_params { recipient: { id: contact.get_source_id(inbox.id) }, message: { text: message.content } } end + def fb_attachment_message_params + { + recipient: { id: contact.get_source_id(inbox.id) }, + message: { + attachment: { + type: 'image', + payload: { + url: message.attachment.file_url + } + } + } + } + end + + def fb_message_params + if message.attachment.blank? + fb_text_message_params + else + fb_attachment_message_params + end + end + def delivery_params if twenty_four_hour_window_over? fb_message_params.merge(tag: 'ISSUE_RESOLUTION') diff --git a/docs/channels/facebook-channel.md b/docs/channels/facebook-channel.md new file mode 100644 index 000000000..9e6865c84 --- /dev/null +++ b/docs/channels/facebook-channel.md @@ -0,0 +1,40 @@ +--- +path: "/docs/channels/facebook" +title: "How to create Facebook channel?" +--- + +If you are using self-hosted Chatwoot installation, please configure the Facebook app as described in the [guide to setup Facebook app](/docs/facebook-setup) + +**Step 1**. Click on "Add Inbox" button from Settings > Inboxes page. + +![fb_create](./images/facebook/inbox_create.png) + +**Step 2**. Click on "Facebook" icon. + +![list_of_channels](./images/facebook/list_of_channels.png) + +**Step 3**. Click on Facebook login button. It will open a new window for you to login. + +![create_fb](./images/facebook/login_with_facebook.png) + +**Step 4**. Authenticate with Facebook and select the page you want connect, enable all permissions shown in the list, otherwise the app might not work. + +![link_account](./images/facebook/link_account.png) + +![list_of_pages](./images/facebook/list_of_pages.png) + +![list_of_pages](./images/facebook/permissions.png) + +![select_page](./images/facebook/select_page.png) + +**Step 5**. "Add agents" to your Facebook inbox. + +![add_agents](./images/add_agents.png) + +**Step 6**. Hooray! You have sucessfully created a Facebook inbox. Whenever a customer sends a message to your Facebook page, you will be able to see it here and manage it. + +![finish_inbox](./images/facebook/finish_inbox.png) + +**Step 7**. If you want to update the agents who have access to the inbox, you can go to Settings > Inboxes. + +![inbox_settings](./images/inbox_settings.png) diff --git a/docs/channels/images/facebook/add_agents.png b/docs/channels/images/facebook/add_agents.png new file mode 100644 index 000000000..49523946e Binary files /dev/null and b/docs/channels/images/facebook/add_agents.png differ diff --git a/docs/channels/images/facebook/finish_inbox.png b/docs/channels/images/facebook/finish_inbox.png new file mode 100644 index 000000000..cc40da4f2 Binary files /dev/null and b/docs/channels/images/facebook/finish_inbox.png differ diff --git a/docs/channels/images/facebook/inbox_create.png b/docs/channels/images/facebook/inbox_create.png new file mode 100644 index 000000000..17c1533e9 Binary files /dev/null and b/docs/channels/images/facebook/inbox_create.png differ diff --git a/docs/channels/images/facebook/inbox_settings.png b/docs/channels/images/facebook/inbox_settings.png new file mode 100644 index 000000000..adf3a84cf Binary files /dev/null and b/docs/channels/images/facebook/inbox_settings.png differ diff --git a/docs/channels/images/facebook/link_account.png b/docs/channels/images/facebook/link_account.png new file mode 100644 index 000000000..24e5351e9 Binary files /dev/null and b/docs/channels/images/facebook/link_account.png differ diff --git a/docs/channels/images/facebook/list_of_channels.png b/docs/channels/images/facebook/list_of_channels.png new file mode 100644 index 000000000..58329a4fb Binary files /dev/null and b/docs/channels/images/facebook/list_of_channels.png differ diff --git a/docs/channels/images/facebook/list_of_pages.png b/docs/channels/images/facebook/list_of_pages.png new file mode 100644 index 000000000..b49d11600 Binary files /dev/null and b/docs/channels/images/facebook/list_of_pages.png differ diff --git a/docs/channels/images/facebook/login_with_facebook.png b/docs/channels/images/facebook/login_with_facebook.png new file mode 100644 index 000000000..4543adba1 Binary files /dev/null and b/docs/channels/images/facebook/login_with_facebook.png differ diff --git a/docs/channels/images/facebook/permissions.png b/docs/channels/images/facebook/permissions.png new file mode 100644 index 000000000..bb5cfaa1a Binary files /dev/null and b/docs/channels/images/facebook/permissions.png differ diff --git a/docs/channels/images/facebook/select_page.png b/docs/channels/images/facebook/select_page.png new file mode 100644 index 000000000..b8f5c1a89 Binary files /dev/null and b/docs/channels/images/facebook/select_page.png differ diff --git a/docs/development/project-setup/facebook-channel-setup.md b/docs/development/project-setup/facebook-channel-setup.md new file mode 100644 index 000000000..b42a77eee --- /dev/null +++ b/docs/development/project-setup/facebook-channel-setup.md @@ -0,0 +1,65 @@ +--- +path: "/docs/facebook-setup" +title: "Setting Up Facebook" +--- + +### Register A Facebook App + +To use Facebook Channel, you have to create an Facebook app in developer portal. You can find more details about creating Facebook channels [here](https://developers.facebook.com/docs/apps/#register) + +Once you register your Facebook App, you will have to obtain the `App Id` and `App Secret` . These values will be available in the app settings and will be required while setting up Chatwoot environment variables. + +### Configure the Facebook App + +1) In the app settings add your `Chatwoot installation url` as your app domain. +2) In the products section in your app settings page, Add Messenger +3) Go to the Messenger settings and configure the call Back URL with `{your_chatwoot_url}/bot` +4) Configure a `verify token`, you will need this value for configuring the chatwoot environment variables +5) You might have to add a Facebook page to your `Access Tokens` section in your Messenger settings page if your app is still in development. +6) You will also have to add your Facebook page to webhooks sections in your messenger settings with all the webhook events checked. + +### Configuring the Environment Variables in Chatwoot + +Configure the following Chatwoot environment variables with the values you have obtained during the facebook app setup. + +```bash +FB_VERIFY_TOKEN= +FB_APP_SECRET= +FB_APP_ID= +``` + +### Things to note before going into production. + +Before you can start using your Facebook app in production, you will have to get it verified by Facebook. Refer the [docs](https://developers.facebook.com/docs/apps/review/) on getting your app verified. + +### Developing or Testing Facebook Integration in You Local + +Install [ngrok](https://ngrok.com/docs) on your machine. This will be required since Facebook Messenger API's will only communicate via https. + +```bash +brew cask install ngrok +``` + +Configure ngrok to route to your Rails server port. + +```bash +ngrok http 3000 +``` + +Go to Facebook developers page and navigate into your app settings. In the app settings, add `localhost` as your app domain. +In the Messenger settings page, configure the callback url with the following value. + +```bash +{your_ngrok_url}/bot +``` + +Update verify token in your Chatwoot environment variables. + +You will also have to add a Facebook page to your `Access Tokens` section in your Messenger settings page. +Restart the Chatwoot local server. Your Chatwoot setup will be ready to receive Facebook messages. + +### Test your local Setup + +1. After finishing the set up above, [create a Facebook inbox](/docs/channels/facebook) after logging in to your Chatwoot Installation. +2. Send a message to your page from Facebook. +3. Wait and confirm incoming requests to `/bot` endpoint in your ngrok screen. diff --git a/spec/services/facebook/send_reply_service_spec.rb b/spec/services/facebook/send_reply_service_spec.rb index 44f028e8b..11aea9d58 100644 --- a/spec/services/facebook/send_reply_service_spec.rb +++ b/spec/services/facebook/send_reply_service_spec.rb @@ -46,6 +46,15 @@ describe Facebook::SendReplyService do create(:message, message_type: 'outgoing', inbox: facebook_inbox, account: account, conversation: conversation) expect(bot).to have_received(:deliver) end + + it 'if message with attachment is sent from chatwoot and is outgoing' do + create(:message, message_type: :incoming, inbox: facebook_inbox, account: account, conversation: conversation) + message = build(:message, message_type: 'outgoing', inbox: facebook_inbox, account: account, conversation: conversation) + message.attachment = Attachment.new(account_id: message.account_id, file_type: :image) + message.attachment.file.attach(io: File.open(Rails.root.join('spec/assets/avatar.png')), filename: 'avatar.png', content_type: 'image/png') + message.save! + expect(bot).to have_received(:deliver) + end end end end