diff --git a/.env.sample b/.env.sample index 3cebf7270..08c9853b0 100644 --- a/.env.sample +++ b/.env.sample @@ -1,4 +1 @@ -pusher_cluster= -pusher_key= - fb_app_id= diff --git a/.eslintrc.js b/.eslintrc.js index 52f9f9e02..50f5ee562 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -43,7 +43,6 @@ module.exports = { }, globals: { __WEBPACK_ENV__: true, - __PUSHER__: true, __FB_APP_ID__: true, }, }; diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 970b60ed9..17dfb84f9 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -40,7 +40,7 @@ Lint/UselessAssignment: Exclude: - 'app/controllers/api/v1/callbacks_controller.rb' - 'app/controllers/api/v1/facebook_indicators_controller.rb' - - 'app/listeners/pusher_listener.rb' + - 'app/listeners/action_cable_listener.rb' - 'app/listeners/reporting_listener.rb' - 'app/models/channel/facebook_page.rb' - 'app/models/facebook_page.rb' diff --git a/Gemfile b/Gemfile index 71f16c0b2..893be6119 100644 --- a/Gemfile +++ b/Gemfile @@ -39,7 +39,6 @@ gem 'devise_token_auth', git: 'https://github.com/lynndylanhurley/devise_token_a gem 'pundit' ##--- gems for pubsub service ---## -gem 'pusher' gem 'wisper', '2.0.0' ##--- gems for reporting ---## @@ -72,6 +71,7 @@ group :development do end group :test do + gem 'action-cable-testing' gem 'mock_redis' gem 'shoulda-matchers' end diff --git a/Gemfile.lock b/Gemfile.lock index d759699e5..205747213 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -113,6 +113,8 @@ GIT GEM remote: https://rubygems.org/ specs: + action-cable-testing (0.6.0) + actioncable (>= 5.0) addressable (2.7.0) public_suffix (>= 2.0.2, < 5.0) ast (2.4.0) @@ -161,13 +163,6 @@ GEM coderay (1.1.2) coercible (1.0.0) descendants_tracker (~> 0.0.1) - coffee-rails (5.0.0) - coffee-script (>= 2.2.0) - railties (>= 5.2.0) - coffee-script (2.4.1) - coffee-script-source - execjs - coffee-script-source (1.12.2) concurrent-ruby (1.1.5) connection_pool (2.2.2) crass (1.0.5) @@ -211,7 +206,6 @@ GEM httparty (0.17.1) mime-types (~> 3.0) multi_xml (>= 0.5.2) - httpclient (2.8.3) i18n (1.7.0) concurrent-ruby (~> 1.0) ice_nine (0.11.2) @@ -269,7 +263,6 @@ GEM minitest (5.12.2) mock_redis (0.22.0) msgpack (1.3.1) - multi_json (1.14.1) multi_xml (0.6.0) multipart-post (2.1.1) naught (1.1.0) @@ -292,11 +285,6 @@ GEM puma (3.12.1) pundit (2.1.0) activesupport (>= 3.0.0) - pusher (1.3.3) - httpclient (~> 2.7) - multi_json (~> 1.0) - pusher-signature (~> 0.1.8) - pusher-signature (0.1.8) rack (2.0.7) rack-cache (1.9.0) rack (>= 0.4) @@ -452,6 +440,7 @@ PLATFORMS ruby DEPENDENCIES + action-cable-testing acts-as-taggable-on! attr_extras bootsnap @@ -459,7 +448,6 @@ DEPENDENCIES byebug carrierwave-aws chargebee (~> 2) - coffee-rails devise! devise_token_auth! facebook-messenger @@ -480,7 +468,6 @@ DEPENDENCIES pry-rails puma (~> 3.0) pundit - pusher rack-cors rails (~> 6)! redis diff --git a/app/channels/application_cable/channel.rb b/app/channels/application_cable/channel.rb new file mode 100644 index 000000000..1a38744ce --- /dev/null +++ b/app/channels/application_cable/channel.rb @@ -0,0 +1,2 @@ +class ApplicationCable::Channel < ActionCable::Channel::Base +end diff --git a/app/channels/application_cable/connection.rb b/app/channels/application_cable/connection.rb new file mode 100644 index 000000000..12d37bb49 --- /dev/null +++ b/app/channels/application_cable/connection.rb @@ -0,0 +1,2 @@ +class ApplicationCable::Connection < ActionCable::Connection::Base +end diff --git a/app/channels/room_channel.rb b/app/channels/room_channel.rb new file mode 100644 index 000000000..c04b72877 --- /dev/null +++ b/app/channels/room_channel.rb @@ -0,0 +1,5 @@ +class RoomChannel < ApplicationCable::Channel + def subscribed + stream_from params[:pubsub_token] + end +end diff --git a/app/dispatchers/sync_dispatcher.rb b/app/dispatchers/sync_dispatcher.rb index 11f3b5414..e3bad28a1 100644 --- a/app/dispatchers/sync_dispatcher.rb +++ b/app/dispatchers/sync_dispatcher.rb @@ -5,6 +5,6 @@ class SyncDispatcher < BaseDispatcher end def listeners - [PusherListener.instance] + [ActionCableListener.instance] end end diff --git a/app/javascript/dashboard/constants.js b/app/javascript/dashboard/constants.js index 38a52274e..5b42a9166 100644 --- a/app/javascript/dashboard/constants.js +++ b/app/javascript/dashboard/constants.js @@ -1,6 +1,5 @@ export default { APP_BASE_URL: '/', - PUSHER: __PUSHER__, get apiURL() { return `${this.APP_BASE_URL}/`; }, diff --git a/app/javascript/dashboard/helper/actionCable.js b/app/javascript/dashboard/helper/actionCable.js new file mode 100644 index 000000000..4706f6b8f --- /dev/null +++ b/app/javascript/dashboard/helper/actionCable.js @@ -0,0 +1,70 @@ +import { createConsumer } from '@rails/actioncable'; + +import AuthAPI from '../api/auth'; + +class ActionCableConnector { + constructor(app, pubsubToken) { + const consumer = createConsumer(); + consumer.subscriptions.create( + { + channel: 'RoomChannel', + pubsub_token: pubsubToken, + }, + { + received: this.onReceived, + } + ); + this.app = app; + this.events = { + 'message.created': this.onMessageCreated, + 'conversation.created': this.onConversationCreated, + 'status_change:conversation': this.onStatusChange, + 'user:logout': this.onLogout, + 'page:reload': this.onReload, + 'assignee.changed': this.onAssigneeChanged, + }; + } + + onAssigneeChanged = payload => { + const { meta = {}, id } = payload; + const { assignee } = meta || {}; + if (id) { + this.app.$store.dispatch('updateAssignee', { id, assignee }); + } + }; + + onConversationCreated = data => { + this.app.$store.dispatch('addConversation', data); + }; + + onLogout = () => AuthAPI.logout(); + + onMessageCreated = data => { + this.app.$store.dispatch('addMessage', data); + }; + + onReceived = ({ event, data } = {}) => { + if (this.events[event]) { + this.events[event](data); + } + }; + + onReload = () => window.location.reload(); + + onStatusChange = data => { + this.app.$store.dispatch('addConversation', data); + }; +} + +export default { + init() { + if (AuthAPI.isLoggedIn()) { + const actionCable = new ActionCableConnector( + window.WOOT, + AuthAPI.getPubSubToken() + ); + return actionCable; + } + return null; + }, +}; diff --git a/app/javascript/dashboard/helper/pusher.js b/app/javascript/dashboard/helper/pusher.js deleted file mode 100644 index 934de92ed..000000000 --- a/app/javascript/dashboard/helper/pusher.js +++ /dev/null @@ -1,71 +0,0 @@ -/* eslint-env browser */ -/* eslint no-console: 0 */ -import Pusher from 'pusher-js'; -import AuthAPI from '../api/auth'; -import CONSTANTS from '../constants'; - -class VuePusher { - constructor(apiKey, options) { - this.app = options.app; - this.pusher = new Pusher(apiKey, options); - this.channels = []; - } - - subscribe(channelName) { - const channel = this.pusher.subscribe(channelName); - if (!this.channels.includes(channel)) { - this.channels.push(channelName); - } - this.bindEvent(channel); - } - - unsubscribe(channelName) { - this.pusher.unsubscribe(channelName); - } - - bindEvent(channel) { - channel.bind('message.created', data => { - this.app.$store.dispatch('addMessage', data); - }); - - channel.bind('conversation.created', data => { - this.app.$store.dispatch('addConversation', data); - }); - - channel.bind('status_change:conversation', data => { - this.app.$store.dispatch('addConversation', data); - }); - - channel.bind('assignee.changed', payload => { - const { meta = {}, id } = payload; - const { assignee } = meta || {}; - if (id) { - this.app.$store.dispatch('updateAssignee', { id, assignee }); - } - }); - - channel.bind('user:logout', () => AuthAPI.logout()); - channel.bind('page:reload', () => window.location.reload()); - } -} - -/* eslint no-param-reassign: ["error", { "props": false }] */ -export default { - init() { - // Log only if env is testing or development. - Pusher.logToConsole = CONSTANTS.PUSHER.logToConsole || true; - // Init Pusher - const options = { - encrypted: true, - app: window.WOOT, - cluster: CONSTANTS.PUSHER.cluster, - }; - const pusher = new VuePusher(CONSTANTS.PUSHER.token, options); - // Add to global Obj - if (AuthAPI.isLoggedIn()) { - pusher.subscribe(AuthAPI.getPubSubToken()); - return pusher.pusher; - } - return null; - }, -}; diff --git a/app/javascript/dashboard/routes/index.spec.js b/app/javascript/dashboard/routes/index.spec.js index 8cdfcba43..8d23a6927 100644 --- a/app/javascript/dashboard/routes/index.spec.js +++ b/app/javascript/dashboard/routes/index.spec.js @@ -14,7 +14,6 @@ jest.mock('./login/login.routes', () => ({ jest.mock('../constants', () => { return { APP_BASE_URL: '/', - PUSHER: false, get apiUrl() { return `${this.APP_BASE_URL}/`; }, diff --git a/app/javascript/dashboard/store/modules/auth.js b/app/javascript/dashboard/store/modules/auth.js index 26fa99066..a0d1f692e 100644 --- a/app/javascript/dashboard/store/modules/auth.js +++ b/app/javascript/dashboard/store/modules/auth.js @@ -8,7 +8,7 @@ import * as types from '../mutation-types'; import router from '../../routes'; import authAPI from '../../api/auth'; import createAxios from '../../helper/APIHelper'; -import vuePusher from '../../helper/pusher'; +import actionCable from '../../helper/actionCable'; // initial state const state = { currentUser: { @@ -61,7 +61,7 @@ const actions = { .then(() => { commit(types.default.SET_CURRENT_USER); window.axios = createAxios(axios); - window.pusher = vuePusher.init(Vue); + actionCable.init(Vue); router.replace({ name: 'home' }); resolve(); }) diff --git a/app/javascript/dashboard/store/modules/conversations/index.js b/app/javascript/dashboard/store/modules/conversations/index.js index a0cd43c7b..da48a3c7b 100644 --- a/app/javascript/dashboard/store/modules/conversations/index.js +++ b/app/javascript/dashboard/store/modules/conversations/index.js @@ -168,7 +168,7 @@ const mutations = { _state.chatStatusFilter = data; }, - // Update assignee on pusher message + // Update assignee on action cable message [types.default.UPDATE_ASSIGNEE](_state, payload) { const [chat] = _state.allConversations.filter(c => c.id === payload.id); chat.meta.assignee = payload.assignee; diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js index 7bae422ee..714713133 100644 --- a/app/javascript/packs/application.js +++ b/app/javascript/packs/application.js @@ -24,7 +24,7 @@ import createAxios from '../dashboard/helper/APIHelper'; import commonHelpers from '../dashboard/helper/commons'; import router from '../dashboard/routes'; import store from '../dashboard/store'; -import vuePusher from '../dashboard/helper/pusher'; +import vueActionCable from '../dashboard/helper/actionCable'; import constants from '../dashboard/constants'; Vue.config.env = process.env; @@ -58,7 +58,7 @@ window.onload = () => { components: { App }, template: '', }).$mount('#app'); - window.pusher = vuePusher.init(); + vueActionCable.init(); }; if ('serviceWorker' in navigator) { diff --git a/app/listeners/pusher_listener.rb b/app/listeners/action_cable_listener.rb similarity index 63% rename from app/listeners/pusher_listener.rb rename to app/listeners/action_cable_listener.rb index 1cea7b144..17f137237 100644 --- a/app/listeners/pusher_listener.rb +++ b/app/listeners/action_cable_listener.rb @@ -1,46 +1,53 @@ -class PusherListener < BaseListener +class ActionCableListener < BaseListener include Events::Types def conversation_created(event) conversation, account, timestamp = extract_conversation_and_account(event) members = conversation.inbox.members.pluck(:pubsub_token) - Pusher.trigger(members, CONVERSATION_CREATED, conversation.push_event_data) if members.present? + send_to_members(members, CONVERSATION_CREATED, conversation.push_event_data) end def conversation_read(event) conversation, account, timestamp = extract_conversation_and_account(event) members = conversation.inbox.members.pluck(:pubsub_token) - Pusher.trigger(members, CONVERSATION_READ, conversation.push_event_data) if members.present? + send_to_members(members, CONVERSATION_READ, conversation.push_event_data) end def message_created(event) message, account, timestamp = extract_message_and_account(event) conversation = message.conversation members = conversation.inbox.members.pluck(:pubsub_token) - - Pusher.trigger(members, MESSAGE_CREATED, message.push_event_data) if members.present? + send_to_members(members, MESSAGE_CREATED, message.push_event_data) end def conversation_reopened(event) conversation, account, timestamp = extract_conversation_and_account(event) members = conversation.inbox.members.pluck(:pubsub_token) - Pusher.trigger(members, CONVERSATION_REOPENED, conversation.push_event_data) if members.present? + send_to_members(members, CONVERSATION_REOPENED, conversation.push_event_data) end def conversation_lock_toggle(event) conversation, account, timestamp = extract_conversation_and_account(event) members = conversation.inbox.members.pluck(:pubsub_token) - Pusher.trigger(members, CONVERSATION_LOCK_TOGGLE, conversation.lock_event_data) if members.present? + send_to_members(members, CONVERSATION_LOCK_TOGGLE, conversation.lock_event_data) end def assignee_changed(event) conversation, account, timestamp = extract_conversation_and_account(event) members = conversation.inbox.members.pluck(:pubsub_token) - Pusher.trigger(members, ASSIGNEE_CHANGED, conversation.push_event_data) if members.present? + send_to_members(members, ASSIGNEE_CHANGED, conversation.push_event_data) end private + def send_to_members(members, event_name, data) + return if members.blank? + + members.each do |member| + ActionCable.server.broadcast(member, event: event_name, data: data) + end + end + def push(pubsub_token, data) # Enqueue sidekiq job to push event to corresponding channel end diff --git a/app/models/concerns/pubsubable.rb b/app/models/concerns/pubsubable.rb index 83ba07480..2e8abb9da 100644 --- a/app/models/concerns/pubsubable.rb +++ b/app/models/concerns/pubsubable.rb @@ -4,7 +4,7 @@ module Pubsubable extend ActiveSupport::Concern included do - # Used by the pusher/PubSub Service we use for real time communications + # Used by the actionCable/PubSub Service we use for real time communications has_secure_token :pubsub_token end end diff --git a/app/models/user.rb b/app/models/user.rb index 065d85c59..7e88d666e 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -12,7 +12,7 @@ class User < ApplicationRecord :validatable, :confirmable - # Used by the pusher/PubSub Service we use for real time communications + # Used by the actionCable/PubSub Service we use for real time communications has_secure_token :pubsub_token validates_uniqueness_of :email, scope: :account_id diff --git a/config/cable.yml b/config/cable.yml new file mode 100644 index 000000000..cfe40a8a0 --- /dev/null +++ b/config/cable.yml @@ -0,0 +1,13 @@ +development: + adapter: async + +test: + adapter: test + +staging: + adapter: redis + url: <%= ENV.fetch('REDIS_URL', 'redis://127.0.0.1:6379') %> + +production: + adapter: redis + url: <%= ENV.fetch('REDIS_URL', 'redis://127.0.0.1:6379') %> diff --git a/config/initializers/pusher.rb b/config/initializers/pusher.rb deleted file mode 100644 index 5f2e6d48b..000000000 --- a/config/initializers/pusher.rb +++ /dev/null @@ -1,6 +0,0 @@ -Pusher.app_id = ENV['pusher_app_id'] -Pusher.key = ENV['pusher_key'] -Pusher.secret = ENV['pusher_secret'] -Pusher.encrypted = true -Pusher.logger = Rails.logger -Pusher.cluster = ENV['pusher_cluster'] diff --git a/config/webpack/environment.js b/config/webpack/environment.js index 6fd087a1f..3dbc714dc 100644 --- a/config/webpack/environment.js +++ b/config/webpack/environment.js @@ -17,19 +17,11 @@ environment.loaders.append('audio', { environment.config.merge({ resolve }); -const { - pusher_cluster: cluster, - pusher_key: token, - fb_app_id: fbAppID, -} = process.env; +const { fb_app_id: fbAppID } = process.env; environment.plugins.prepend( 'DefinePlugin', new webpack.DefinePlugin({ - __PUSHER__: { - token: `"${token}"`, - cluster: `"${cluster}"`, - }, __FB_ID__: `"${fbAppID}"`, }) ); diff --git a/docs/development/project-setup/environment-variables.md b/docs/development/project-setup/environment-variables.md index d8627e474..3e6e77d0c 100644 --- a/docs/development/project-setup/environment-variables.md +++ b/docs/development/project-setup/environment-variables.md @@ -26,17 +26,6 @@ development: Following changes has to be in `config/application.yml` -### Configure Pusher - -Chatwoot uses [Pusher](https://pusher.com/) to handle realtime messages. Create a free account on Pusher and fill the following environment values. - -```yml -pusher_app_id: '' -pusher_key: '' -pusher_secret: '' -pusher_cluster: '' -``` - ### Configure FB Channel To use FB Channel, you have to create an Facebook app in developer portal. You can find more details about creating FB channels [here](https://developers.facebook.com/docs/apps/#register) diff --git a/package.json b/package.json index eff9cc94b..5eea8cae0 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ }, "dependencies": { "@babel/polyfill": "^7.6.0", + "@rails/actioncable": "^6.0.0", "@rails/webpacker": "^4.0.7", "axios": "^0.19.0", "babel-helper-vue-jsx-merge-props": "^2.0.3", @@ -27,7 +28,6 @@ "js-cookie": "~2.1.3", "md5": "~2.2.1", "moment": "~2.19.3", - "pusher-js": "~4.0.0", "query-string": "5", "spinkit": "~1.2.5", "tween.js": "~16.6.0", diff --git a/shared/config/application.yml b/shared/config/application.yml index 3581104e1..ba1ce019c 100644 --- a/shared/config/application.yml +++ b/shared/config/application.yml @@ -1,10 +1,3 @@ -#pusher - -pusher_app_id: '' -pusher_key: '' -pusher_secret: '' -pusher_cluster: '' - #fb app fb_verify_token: '' fb_app_secret: '' diff --git a/spec/controllers/room_channel_spec.rb b/spec/controllers/room_channel_spec.rb new file mode 100644 index 000000000..e50aaebe4 --- /dev/null +++ b/spec/controllers/room_channel_spec.rb @@ -0,0 +1,15 @@ +require 'rails_helper' + +RSpec.describe RoomChannel, type: :channel do + let!(:user) { create(:user) } + + before do + stub_connection + end + + it 'subscribes to a stream when pubsub_token is provided' do + subscribe(pubsub_token: user.uid) + expect(subscription).to be_confirmed + expect(subscription).to have_stream_for(user.uid) + end +end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 4aee32e6c..940b0702a 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -4,6 +4,8 @@ require File.expand_path('../config/environment', __dir__) # Prevent database truncation if the environment is production abort('The Rails environment is running in production mode!') if Rails.env.production? require 'rspec/rails' +require 'action_cable/testing/rspec' + # Add additional requires below this line. Rails is not loaded until this point! # Requires supporting ruby files with custom matchers and macros, etc, in diff --git a/yarn.lock b/yarn.lock index 6e133f45d..bd74b17a4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -966,6 +966,11 @@ "@nodelib/fs.scandir" "2.1.3" fastq "^1.6.0" +"@rails/actioncable@^6.0.0": + version "6.0.0" + resolved "https://registry.yarnpkg.com/@rails/actioncable/-/actioncable-6.0.0.tgz#8bff9c902be1531ef7a9e191562e9771efcfdfe1" + integrity sha512-DieouotYHpI6k2EGTCnh1eMvD3W8p3zqjWXEYj4z0khJ+A0qQ5tHxihjTEkio54MVwqHt1DcpUm2woh2n/alCA== + "@rails/webpacker@^4.0.7": version "4.0.7" resolved "https://registry.yarnpkg.com/@rails/webpacker/-/webpacker-4.0.7.tgz#268571bf974e78ce57eca9fa478f5bd97fd5182c" @@ -4431,13 +4436,6 @@ fastq@^1.6.0: dependencies: reusify "^1.0.0" -faye-websocket@0.9.4: - version "0.9.4" - resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.9.4.tgz#885934c79effb0409549e0c0a3801ed17a40cdad" - integrity sha1-iFk0x57/sECVSeDAo4Ae0XpAza0= - dependencies: - websocket-driver ">=0.5.1" - faye-websocket@^0.10.0: version "0.10.0" resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.10.0.tgz#4e492f8d04dfb6f89003507f6edbf2d501e7c6f4" @@ -8730,14 +8728,6 @@ punycode@^2.1.0, punycode@^2.1.1: resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== -pusher-js@~4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/pusher-js/-/pusher-js-4.0.0.tgz#3f53f9a8e2cb55b89b7724881615f891f200ab8e" - integrity sha1-P1P5qOLLVbibdySIFhX4kfIAq44= - dependencies: - faye-websocket "0.9.4" - xmlhttprequest "^1.8.0" - q@^1.1.2: version "1.5.1" resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" @@ -10975,11 +10965,6 @@ xml-name-validator@^3.0.0: resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw== -xmlhttprequest@^1.8.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz#67fe075c5c24fef39f9d65f5f7b7fe75171968fc" - integrity sha1-Z/4HXFwk/vOfnWX197f+dRcZaPw= - xtend@^4.0.0, xtend@~4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"