How to know when a user disconnects from a Faye channel? - ruby-on-rails

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.

Related

Action Cable Broadcast message fron sidekiq shows up only after refresh, works instantly from console

I followed this tutorial to create an action cable broadcast but it's not quite working as expected. The channel streams and the web app subscribes successfully, but messages broadcasted from the sidekiq background job are only displayed after refreshing the page. Using the same command on the console does result in an immediate update to the page.
When looking at the frames in chrome's developer mode, I cannot see the broadcasted messages from the background job but can immediately see the ones sent by the console. However, I can confirm that the sidekiq background job is broadcasting those messages somewhere since they do show up upon refresh; however, I don't know where they are being queued.
Are there any additional configuration changes needed to keep the messages from the background job from being queued somewhere? Are there any typos or errors in my code that could be causing this?
Action Cable Broadcast message:
ActionCable.server.broadcast "worker_channel", {html:
"<div class='alert alert-success alert-block text-center'>
Market data retrieval complete.
</div>"
}
smart_worker.rb: -- This is called as perform_async from the controller's action
class SmartWorker
include Sidekiq::Worker
include ApplicationHelper
sidekiq_options retry: false
def perform
ActionCable.server.broadcast "worker_channel", {html:
"<div class='alert alert-success alert-block text-center'>
Market data retrieval complete.
</div>"
}
end
connection.rb:
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user
def connect
self.current_user = current_user #find_verified_user ignored until method implemented correctly and does not always return unauthorized
end
private
def find_verified_user
if current_user = User.find_by(id: cookies.signed[:user_id])
current_user
else
reject_unauthorized_connection
end
end
end
end
worker_channel:
class WorkerChannel < ApplicationCable::Channel
def subscribed
stream_from "worker_channel"
end
def unsubscribed
end
end
worker.js:
App.notifications = App.cable.subscriptions.create('WorkerChannel', {
connected: function() {
console.log('message connected');
},
disconnected: function() {},
received: function(data) {
console.log('message recieved');
$('#notifications').html(data.html);
}
});
cable.yml
development:
adapter: redis
url: redis://localhost:6379/1
test:
adapter: async
production:
adapter: redis
url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
channel_prefix: smarthost_production
Also added
to the view but that didn't make a difference.
I'm not sure this is the entire explanation but this is what I have observed through further testing:
After multiple server restarts, the broadcast started working and would log as expected in the development logger. Console messages where still hit or miss, so I added some additional identifiers to the broadcasted messages and identified that they were being broadcasted before the loading of the next page was completed. This caused two things:
1) A quick flashing of flash messages triggered by the broadcast (in what was perceived to be the old page - i.e. only works after a refresh)
2) A lack of or inconsistent behavior in the browser console: Because the sidekiq worker job finished so quick, sometimes even before the browser started rendering the new page, I believe the console messages are being reset by the page loading actions and are therefore not visible when you check the logs (or even if you stare at it for a while).
It seems as though this is working as expected, and is simply working to quickly in the local environment which makes it seem as though it's not working as intended.
ActionChannel normally does not queue messages and those broadcasted when there's no subscriber should be lost. Observed behaviour can happen if notification actually comes later than you expect.
I'd check:
Run entire job in console, not just notification, and see if it's running slow
Check sidekiq queues latency
Add logging before/after notification in job and check logs if the job is actually run successfully

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

Rails push EM-Socket

I'm writing rails app and i want user to receive notification as soon as new message is saved to DB. For websocket i'm using em-websocket gem. After connection, i store client id and socket instance in array.
Question: How to push data to the client from controller\model action (before_save for example)? Is it able to do so with em-websocket?
chat.rb
EVENTCHAT_CONFIG = YAML.load_file("#{Rails.root}/config/eventchat.yml")[Rails.env].symbolize_keys
#escape html/xss
include ERB::Util
Thread.abort_on_exception = true
Thread.new {
EventMachine.run {
#sockets = Array.new
EventMachine::WebSocket.start(EVENTCHAT_CONFIG) do |ws|
ws.onopen do
end
ws.onclose do
index = #sockets.index {|i| i[:socket] == ws}
client = #sockets.delete_at index
#sockets.each {|s| s[:socket].send h("#{client[:id]} has disconnected!")}
end
ws.onmessage do |msg|
client = JSON.parse(msg).symbolize_keys
case client[:action]
when 'connect'
#sockets.push({:id=>client[:id], :socket=>ws})
#sockets.each {|s| s[:socket].send h("#{client[:id]} has connected!")}
when 'say'
#sockets.each {|s| s[:socket].send h("#{client[:id]} says : #{client[:data]}")}
end
end
end
}
}
When deploying - you may have problems related to having a separate EM reactor in each rails worker (for example errors about some failing to bind to socket or not all users receiving all messages)
Rails controllers in relation to websocket reactor are just another type of client, easiest way is to open a connection with special client id, push some data and close it afterwards. More efficient way - is to keep a open connection from each rails worker
If a EM-based single-process server is used(for example - thin), you can use EM::Queue to deliver messages to websocket worker in-process or even write to websocket directly from controller

Ruby on Rails -- Faye Framework -- private_pub

I'm using private_pub to implement a one-to-one chat-like application.
Here is my story: as a user, I would like to receive a message when my partner leaves the chat – closes the window, etc.
Looking through the Faye Monitoring docs here is my attempt at binding on unsubscribe:
# Run with: rackup private_pub.ru -s thin -E production
require "bundler/setup"
require "yaml"
require "faye"
require "private_pub"
require "active_support/core_ext"
Faye::WebSocket.load_adapter('thin')
PrivatePub.load_config(File.expand_path("../config/private_pub.yml", __FILE__), ENV["RAILS_ENV"] || "development")
wts_pubsub = PrivatePub.faye_app
wts_pubsub.bind(:subscribe) do |client_id, channel|
puts "[#{Time.now}] Client #{client_id} joined #{channel}"
end
wts_pubsub.bind(:unsubscribe) do |client_id, channel|
puts "[#{Time.now}] Client #{client_id} disconnected from #{channel}"
PrivatePub.publish_to channel, { marius_says: 'quitter' }
end
run wts_pubsub
but I keep getting timeouts: [ERROR] [Faye::RackAdapter] Timeout::Error
Prying into PrivatePub#publish_to, data holds what I expect both when I'm publishing from the Rails or the private_pub app, but the private_pub app keeps hanging.
How can I get publishing from private_pub to work?
Your second bind should be to disconnect event instead of unsubscribe.
Also, remember to fire off a Faye/PrivatePub disconnect event in your client side code when a browser window is closed.
Note: You might need to do this for all open sessions with the Faye server or just on a channel by channel basis based on chat application's design
In plain JS this might be something like:
window.onbeforeunload = functionThatTriggersFayeDisconnectEvent;
Sorry for not using proper markup, posting from mobile.
After hours of research and numerous attempts, this is the solution I found:
Replace PrivatePub.publish_to channel, { marius_says: 'quitter' } with:
system "curl http://localhost:9292/faye -d 'message={\"channel\":\"#{channel}\", \"data\":{\"channel\":\"#{channel}\",\"data\":{\"message\":{\"content\":\"#{client_id} disconnected from this channel.\"}}}, \"ext\":{\"private_pub_token\":\"ADD_APPROPRIATE_SECRET_HERE\"}}' &"
This will trigger an asynchronous request (curl + &) which will bypass the problem. Not the best fix, but it works.

Playing around with mails in Rails

I`m trying to create the following feature: You register and receive an email like vouldjeff+ewr#myapp.com and when you send something to this email it automatically appears in something like your wall... So my problem is how to realize the creation of the email and the receiving of the mail itself.
Any ideas?
Ruby provides Net/IMAP and Net/POP3 you can use to login into your email account.
Here's a small tutorial.
POP3
pop = Net::POP3.new("pop.gmail.com", port)
pop.enable_ssl
pop.start('YourAccount', 'YourPassword')
if pop.mails.empty?
puts 'No mail.'
else
i = 0
pop.each_mail do |m|
File.open("inbox/#{i}", 'w') do |f|
f.write m.pop
end
m.delete
i += 1
end
puts "#{pop.mails.size} mails popped."
end
pop.finish
IMAP
imap = Net::IMAP.new('imap.gmail.com')
imap.authenticate('LOGIN', 'username', 'password')
imap.select('INBOX')
imap.search(['ALL']).each do |message_id|
msg = imap.fetch(message_id,'RFC822')[0].attr['RFC822']
MailReader.receive(msg)
imap.store(message_id, "+FLAGS", [:Deleted])
end
imap.expunge()
There might be other options but that's how we do it:
Postfix
Rails Cron Job
Postfix allows you to specify a MySQL table/view to check whether an email address exists or not. You can also define Mail Forwardings.
Create a DB View to match the requirements on Postfix
This View should contain all the email addresses and forward them to a different mail account, like mailparser.
Now your Rails can either
use a POP3/IMAP frontend to the mailserver (you should install Dovecot or Courier then) to fetch the mails and process them
or go to the place on the disk where all the mails are located (check Postfix config for that) and parse the files as TMail objects and process them.
A different option is to make Postfix call script/runner with the Mail data, but rails boot-up can take long and a lot of memory, so I prefer having a Cronjob/Backgroundjob/Worker to do this.
P.S. The Creation of the E-Mail will be done by creating a Model for your Rails app which the View will use as a basis.
Sending E-Mails is simple as pie. Simply have a look at the ActionMailer Basics. If you also want to receive E-Mail, you should write a daemon that fetches Mails from the mailserver continuously in the background.
Here a snippet that fetches Mails via POP:
require 'net/pop'
config = {
:host => "mail.example.com",
:user => "foobar#example.com",
:password => "…",
:port => 110,
:timeout => 10
}
pop = Net::POP3.new(config[:host])
pop.start(config[:user], config[:password])
if pop.mails.empty?
puts "No mails…"
else
pop.mails.each do |mail|
# do stuff with mail
end
end
This is pure Ruby-Code, Rails is not needed for this snippet.

Resources