Trouble connecting to Action Cable with remote origin - ruby-on-rails

I've setup a websocket on our Rails app, and have been able to connect and receive data on it from the same server.
The way I've done it now is to create a socket like this:
class UsersChannel < ApplicationCable::Channel
def subscribed
stream_from "users_1"
end
def unsubscribed
end
end
and then I use javascript to open the connection with
new WebSocket('wss://domain.com/cable/users_1');
I then broadcast and send JSON from a page in this format:
ActionCable.server.broadcast "users_1", {
store: {
name: store.name,
address: {
full_address: location.address,
latitude: location.latitude,
longitude: location.longitude
}
}
When that's triggered I can see in my console that it appears. I've added this channel in JS:
App.cable.subscriptions.create "UsersChannel",
received: (data) ->
console.log data
Now we're working building an app with React Native and when we add this code to our app:
var ws = new WebSocket('wss://domain.com/cable/users_1');
ws.onmessage = (e) => {
console.log(e.data);
};
we see that it pings, but receive nothing when we trigger the broadcast. I've also added this to the config file:
config.action_cable.url = 'wss://domain.com/cable'
config.action_cable.disable_request_forgery_protection = true
Anybody know why this is happening?

I finally figured out how to solve my issue.
Solving your issue
First thing first, in your case, you are missing the step to subscribe to a channel. You have to send a JSON onopen of your WebSocket like that :
JSON.stringify({
command: 'subscribe',
identifier: JSON.stringify({
channel: 'MyNotificationChannel',
user_id: userId
})
})
This will trigger the MyNotificationChannel class subscribed method passing the user_id as a param (params[:user_id]).
The Origins
Then comes the issue of Origins, which was my issue.
The WebSocket opening fails on Rails side with the message :
Request origin not allowed: https://loc.mydom.com:28080
My app is running on https://loc.mydom.com:3000/ and I have separated running server for cable which listen on port 28080.
Anyways the point here is that the message shows the origin which isn't accepted.
In the environment file (config/environments/development.rb) you just need to add ActionCable.server.config.allowed_request_origins :
Rails.application.configure do
# ...
ActionCable.server.config.allowed_request_origins = [
'https://loc.mydom.com:3000/', # My web front app
'https://loc.mydom.com:28080' # The phone app
]
# ...
end
After this, my WebSocket connects and I'm receiving the updates.

Related

Using Actioncable and Rails 5 API mode together

I am creating a very basic chat app. My goal is to have a Rails API backend and then build an IOS, Android, web, and desktop client. This is purely to explore Websockets and mobile development.
I've never used Actioncable, and my knowledge of Websockets is very limited. What I'd like to know is if I can set up Actioncable on my Rails API and have it communicate with Node (for instance).
Does Actioncable act like any other Websocket? Could I connect to it from my Node app through ws://<host>/cable and have a functional pub-sub system between whatever client and Rails?
I'm sorry if that doesn't make sense, I'm having a hard time wording it :)
Thank you!
Indeed you can!
Just like you create any api app, use generator
rails new my_app --api
Create your_channel
rails generate channel your_channel
Add mount path in routes.rb
mount ActionCable.server => '/cable'
Allow stream on subscribe method in /app/channels/your_channel.rb
class YourChannel < ApplicationCable::Channel
def subscribed
stream_from 'messages' # <----- Stream Name Here
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
end
Call ActionCable.server.broadcast from any other part of your app to stream
ActionCable.server.broadcast 'messages', message: 'ping'
Now use your front-end to test it. Since you told you want iOS Android and also mentioned about node, I assume you are using (or would choose to use) react-native
import ActionCable from 'react-native-actioncable';
const cable = ActionCable.createConsumer("ws://localhost:3000/cable");
class YourReactClass extends React.Component {
# add or update the following
componentDidMount = () => {
console.log("componentDidMount executed");
this.subscription = cable.subscriptions.create("OrderChannel", {
connected: function() { console.log("connected: action cable") },
disconnected: function() { console.log("disconnected: action cable") },
received: function (data) { console.log(data) }
}
)
};
componentWillUnmount () {
this.subscription &&
cable.subscriptions.remove(this.subscription)
}
}
And you're good to go, build your logic on top of this... If you got any problems, let me know.

How to receive data in ActionCable Channel without JS?

I'm writing a Rails application that uses WebSockets to communicate with other machines (no browser and client side logic in this process). I have a channel:
class MachinesChannel < ApplicationCable::Channel
def subscribed
...
end
def unsubscribed
...
end
def handle_messages
...
end
end
To receive the data the only way I know about is the JavaScript client:
ActionCable.createConsumer('/cable').subscriptions.create 'MachinesChannel',
received: (message) ->
#perform('handle_messages')
I can call server side methods from JS via #perform() method.
Is there any way to omit the JS part and somehow directly handle the incoming data in MachinesChannel?
The ideal situation would be to have the handle_messages method accept a data argument and have this metod called on incoming data.
After looking into ActionCable source code I got the following solution. You just have to create a method in MachinesChannel that you want to be called, e.g. handle_messages(data). Then, in the client that connects to your websocket, you need to send a message in the following format (example in ruby):
id = { channel: 'MachinesChannel' }
ws = WebSocket::Client::Simple.connect(url)
ws.send(JSON.generate(command: 'message', identifier: JSON.generate(id), data: JSON.generate(action: 'handle_messages', foo: 'bar', biz: 'baz')))
action has to be the name of the method you want to be called in MachinesChannel. The rest of key-values are whatever you want. This the date you can receive in the ActionCable channel.
Recently a gem action_cable_client has been release which seems exactly perfect for this kind of usage. I haven't used it, so I don't know how it really works.
Instead of:
def handle_messages
...
end
This works for me:
def receive(data)
puts data
...
end

ActionCable Not Receiving Data

I created the following using ActionCable but not able to receive any data that is being broadcasted.
Comments Channel:
class CommentsChannel < ApplicationCable::Channel
def subscribed
comment = Comment.find(params[:id])
stream_for comment
end
end
JavaScript:
var cable = Cable.createConsumer('ws://localhost:3000/cable');
var subscription = cable.subscriptions.create({
channel: "CommentsChannel",
id: 1
},{
received: function(data) {
console.log("Received data")
}
});
It connects fine and I can see the following in the logs:
CommentsChannel is streaming from comments:Z2lkOi8vdHJhZGUtc2hvdy9FdmVudC8x
I then broadcast to that stream:
ActionCable.server.broadcast "comments:Z2lkOi8vdHJhZGUtc2hvdy9FdmVudC8x", { test: '123' }
The issue is that the received function is never called. Am I doing something wrong?
Note: I'm using the actioncable npm package to connect from a BackboneJS application.
Changing the cable adapter from async to redis in config/cable.yml fixed it for me.
Update
As Erem pointed out below, the server and console are isolated processes so you need to use a centralized queue manager.

The stratigy of build a talk-to-talk system using em-websocket in rails?

Maybe it is a good example for server push system. There are many users in the system, and users can talk with each other. It can be accomplished like this: one user sends message(through websocket) to the server, then the server forward the message to the other user. The key is to find the binding between the ws(websocket object) and the user. The example code like below:
EM.run {
EM::WebSocket.run(:host => "0.0.0.0", :port => 8080, :debug => false) do |ws|
ws.onopen { |handshake|
# extract the user id from handshake and store the binding between user and ws
}
ws.onmessage { |msg|
# extract the text and receiver id from msg
# extract the ws_receiver from the binding
ws_receiver.send(text)
}
end
}
I want to figure out following issues:
The ws object can be serialized so it can be stored into disk or database? Otherwise I can only store the binding into memory.
What the differences between em-websocket and websocket-rails?
Which gem do you recommend for websocket?
You're approaching a use case that websockets are pretty good for, so you're on the right track.
You could serialize the ws object with Marshal, but think of websocket objects as being a bit like http request objects in that they are abstractions for a type of communication. You are probably best off marshaling/storing the data.
em-websocket is a lower(ish) lever websocket library built more or less directly on web-machine. websocket-rails is a higher level abstraction on websockets, with a lot of nice tools built in and pretty ok docs. It is built on top of faye-websocket-rails which is itself built on web machine. *Note, action cable which is the new websocket library for Rails 5 is built on faye.
I've use websocket-rails in the past and rather like it. It will take care of a lot for you. However, if you can use Rails 5 and Action Cable, do that, its the future.
The following is in addition to Chase Gilliam's succinct answer which included references to em-websocket, websocket-rails (which hadn't been maintained in a long while), faye-websocket-rails and ActionCable.
I would recommend the Plezi framework. It works both as an independent application framework as well as a Rails Websocket enhancement.
I would consider the following points as well:
do you need the message to persist between connections (i.e. if the other user if offline, should the message wait in a "message box"? for how long should the message wait?)...?
Do you wish to preserve message history?
These points would help yo decide if to use a persistent storage (i.e. a database) for the messages or not.
i.e., to use Plezi with Rails, create an init_plezi.rb in your application's config/initializers folder. use (as an example) the following code:
class ChatDemo
# use JSON events instead of raw websockets
#auto_dispatch = true
protected #protected functions are hidden from regular Http requests
def auth msg
#user = User.auth_token(msg['token'])
return close unless #user
# creates a websocket "mailbox" that will remain open for 9 hours.
register_as #user.id, lifetime: 60*60*9, max_connections: 5
end
def chat msg, received = false
unless #user # require authentication first
close
return false
end
if received
# this is only true when we sent the message
# using the `broadcast` or `notify` methods
write msg # writes to the client websocket
end
msg['from'] = #user.id
msg['time'] = Plezi.time # an existing time object
unless msg['to'] && registered?(msg['to'])
# send an error message event
return {event: :err, data: 'No recipient or recipient invalid'}.to_json
end
# everything was good, let's send the message and inform
# this will invoke the `chat` event on the other websocket
# notice the `true` is setting the `received` flag.
notify msg['to'], :chat, msg, true
# returning a String will send it to the client
# when using the auto-dispatch feature
{event: 'message_sent', msg: msg}.to_json
end
end
# remember our route for websocket connections.
route '/ws_chat', ChatDemo
# a route to the Javascript client (optional)
route '/ws/client.js', :client
Plezi sets up it's own server (Iodine, a Ruby server), so remember to remove from your application any references to puma, thin or any other custom server.
On the client side you might want to use the Javascript helper provided by Plezi (it's optional)... add:
<script src='/es/client.js' />
<script>
TOKEN = <%= #user.token %>;
c = new PleziClient(PleziClient.origin + "/ws_chat") // the client helper
c.log_events = true // debug
c.chat = function(event) {
// do what you need to print a received message to the screen
// `event` is the JSON data. i.e.: event.event == 'chat'
}
c.error = function(event) {
// do what you need to print a received message to the screen
alert(event.data);
}
c.message_sent = function(event) {
// invoked after the message was sent
}
// authenticate once connection is established
c.onopen = function(event) {
c.emit({event: 'auth', token: TOKEN});
}
// // to send a chat message:
// c.emit{event: 'chat', to: 8, data: "my chat message"}
</script>
I didn't test the actual message code because it's just a skeleton and also it requires a Rails app with a User model and a token that I didn't want to edit just to answer a question (no offense).

How to know when a user disconnects from a Faye channel?

I'm trying to use Faye to build a simple chat room with Rails, and host it on heroku. So far I was able to make the Faye server run, and get instant messaging to work. The crucial lines of code that I'm using are:
Javascript file launched when the page loads:
$(function() {
var faye = new Faye.Client(<< My Feye server on Heoku here >>);
faye.subscribe("/messages/new", function(data) {
eval(data);
});
});
create.js.erb, triggered when the user sends a message
<% broadcast "/messages/new" do %>
$("#chat").append("<%= j render(#message) %>");
<% end %>
Everything is working fine, but now I would like to notify when a user disconnects from the chat. How should I do this?
I already looked in the Faye's website about monitoring, but it's not clear where should I put that code.
Event monitoring goes in your rackup file. Here is an example I'm using in production:
Faye::WebSocket.load_adapter('thin')
server = Faye::RackAdapter.new(mount: '/faye', timeout: 25)
server.bind(:disconnect) do |client_id|
puts "Client #{client_id} disconnected"
end
run server
Of course you can do whatever you like in the block you pass to #bind.
You may want to bind to the subscribe and unsubscribe events instead of the disconnect event. Read the word of warning on the bottom of the faye monitoring docs.
This has worked well for me:
server.bind(:subscribe) do |client_id|
# code to execute
# puts "Client #{client_id} connected"
end
server.bind(:unsubscribe) do |client_id|
# code to execute
# puts "Client #{client_id} disconnected"
end
I also recommend using the private pub gem - this will help secure your faye app.

Resources