Replace pusher with action cable (#178)

closes #43
This commit is contained in:
Pranav Raj S 2019-10-25 01:37:01 +05:30 committed by Sojan Jose
parent c0354364ff
commit f4358d9993
28 changed files with 144 additions and 165 deletions

View file

@ -1,4 +1 @@
pusher_cluster=
pusher_key=
fb_app_id=

View file

@ -43,7 +43,6 @@ module.exports = {
},
globals: {
__WEBPACK_ENV__: true,
__PUSHER__: true,
__FB_APP_ID__: true,
},
};

View file

@ -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'

View file

@ -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

View file

@ -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

View file

@ -0,0 +1,2 @@
class ApplicationCable::Channel < ActionCable::Channel::Base
end

View file

@ -0,0 +1,2 @@
class ApplicationCable::Connection < ActionCable::Connection::Base
end

View file

@ -0,0 +1,5 @@
class RoomChannel < ApplicationCable::Channel
def subscribed
stream_from params[:pubsub_token]
end
end

View file

@ -5,6 +5,6 @@ class SyncDispatcher < BaseDispatcher
end
def listeners
[PusherListener.instance]
[ActionCableListener.instance]
end
end

View file

@ -1,6 +1,5 @@
export default {
APP_BASE_URL: '/',
PUSHER: __PUSHER__,
get apiURL() {
return `${this.APP_BASE_URL}/`;
},

View file

@ -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;
},
};

View file

@ -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;
},
};

View file

@ -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}/`;
},

View file

@ -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();
})

View file

@ -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;

View file

@ -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: '<App/>',
}).$mount('#app');
window.pusher = vuePusher.init();
vueActionCable.init();
};
if ('serviceWorker' in navigator) {

View file

@ -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

View file

@ -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

View file

@ -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

13
config/cable.yml Normal file
View file

@ -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') %>

View file

@ -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']

View file

@ -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}"`,
})
);

View file

@ -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)

View file

@ -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",

View file

@ -1,10 +1,3 @@
#pusher
pusher_app_id: ''
pusher_key: ''
pusher_secret: ''
pusher_cluster: ''
#fb app
fb_verify_token: ''
fb_app_secret: ''

View file

@ -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

View file

@ -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

View file

@ -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"