Chat
Activity Streams
Notifications
Collaboration
Multiplayer games
Realtime data
Dashboards
2nd Screen experiences
# file: faye.ru - rackup config file
require File.expand_path('../config/environment', __FILE__)
bayeux = Faye::RackAdapter.new(:mount => '/faye', :timeout => 25)
bayeux.add_extension(PushOnlyServer.new)
bayeux.add_extension(ServerAuth.new)
bayeux.bind(:subscribe) do |client_id, channel|
Rails.logger.info "[#{Time.now.to_s(:db)} Faye #{client_id}] Subscribed channel #{channel}"
end
run bayeux
# lib/faye/push_only_server.rb
class PushOnlyServer
def incoming(message, callback)
unless message['channel'] =~ /^\/meta\//
password = message['ext'] && message['ext']['password']
if password != AppConfig.faye.push_secret
message['error'] = '403::Password required'
Rails.logger.warn "[#{Time.now.to_s(:db)} Faye #{message['clientId']}] Failed to authenticate as message sender: invalid password"
end
end
callback.call(message)
end
# ...
end
# lib/faye/push_only_server.rb
class PushOnlyServer
# ...
def outgoing(message, callback)
unless message['ext'].nil?
message['ext'].delete('password')
end
callback.call(message)
end
end
class LiveUpdate
def self.send_to_faye(projects, project_parts, ...)
# ...
channel = FayeUtils.js_app_channel_name(project_part, project.id)
message = {
:channel => channel,
:data => {
:event => msg,
:faye_socket_id => socket_id
},
:ext => {
:password => AppConfig.faye.push_secret
}
}
uri = URI.parse(AppConfig.faye.internal_url)
Net::HTTP.post_form(uri, :message => message.to_json) # POST message
# ...
end
end
class ServerAuth
def incoming(message, callback)
# Let non-subscribe messages through
return callback.call(message) unless message['channel'] == '/meta/subscribe'
if message['ext'].nil?
message['error'] = 'Missing ext key in the message'
else
user_id = message['ext']['user_id']
# ... check if user can subscribe the channel
end
# Call the server back now we're done
callback.call(message)
end
end
# ...
user_id = message['ext']['user_id']
faye_token = message['ext']['faye_token']
public_board_keys = message['ext']['public_board_keys']
project_id = message['ext']['project_id']
project = Project.get(project_id)
if project.present?
# ... check if user has access to project etc
user = User.get(user_id)
if user && user.valid_faye_token?(faye_token)
client_id = message['clientId'] # faye client id
project_membership = project.project_memberships.first(user_id: user_id)
$semaphore.synchronize do
$user_activities[client_id] = project_membership.id
end
end
elsif user
message['error'] = "Invalid subscription faye token"
else
message['error'] = "User doesn't exist"
end
end
else
message['error'] = "Project doesn't exist"
end
end
# ...
require File.expand_path('../config/environment', __FILE__)
# key: faye client id
# value: list of project memberships ids
$user_activities = {}
$semaphore = Mutex.new
$update_thread = Thread.new do
loop do
sleep 60
$semaphore.synchronize do
ids = $user_activities.values.uniq
Rails.logger.info "[#{Time.now.to_s(:db)} Faye] Updating user activity timestamps for #{ids.size} membership(s): #{ids.join(", ")}"
ProjectMembership.touch_timestamps_and_last_activity($user_activities.values.uniq)
end
end
end
bayeux = Faye::RackAdapter.new(:mount => '/faye', :timeout => 25)
# ...
class K2.FayeConnection
# ...
connectAndBindEvents: ->
# ...
@faye_client = new Faye.Client '<%= AppConfig.faye.public_url %>',
timeout: timeout
# client-side extension
@faye_client.addExtension outgoing: (message, callback) ->
message.ext = message.ext or {}
message.ext.user_id = K2.faye.currentUserId
message.ext.faye_token = K2.faye.fayeToken
message.ext.public_board_keys = K2.faye.publicBoardKeys
message.ext.project_id = K2.faye.projectId
callback(message)
# ...
class K2.FayeConnection
# ...
@faye_client.bind 'transport:up', ->
# the client is online
console.log "TRANSPORT_UP"
console.log "New socket ID: " + faye_connection.faye_client._0
faye_connection.polling.stop()
faye_connection.polling.fullRefresh()
@faye_client.bind 'transport:down', ->
# the client is offline
# update data with interval (kanbanery stuff - not related to faye)
faye_connection.polling.start()
faye_connected = false
class K2.FayeConnection
subscribeToChannelAndBindEvents: ->
# ...
subscription = @faye_client.subscribe @fayeChannelName(), (message) ->
# handle message
console.log "GOT MESSAGE", message
# ignore message sent by client
if message.faye_socket_id != faye_connection.faye_client._0
faye_connection.store.refresh message.event
# subscription has been set up and acknowledged by the server
subscription.callback ->
# uniq faye hash for connection
console.log "SUBSCRIBED"
console.log "New socket ID: " + faye_connection.faye_client._0
faye_connection.polling.stop()
# error while creating subscription
subscription.errback (error) ->
console.log(error.message)
faye_connection.polling.start()
Alternatives for Faye
https://github.com/maccman/juggernaut (deprecated)
http://socket.io
Alternative hosted API for pusher.com
http://www.pubnub.com
Other:
http://railscasts.com/episodes/260-messaging-with-faye
http://net.tutsplus.com/tutorials/ruby/how-to-use-faye-as-a-real-time-push-server-in-rails/
https://blog.jcoglan.com/2012/06/09/why-you-should-never-use-hash-functions-for-message-authentication/