faye-online 0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1 @@
1
+ Gemfile.lock
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ # encoding: UTF-8
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
@@ -0,0 +1,123 @@
1
+ Faye Online user list and time count
2
+ ===========================================
3
+
4
+ Usage
5
+ -------------------------------------------
6
+ 1. Add it to Gemfile
7
+
8
+ ```ruby
9
+ gem 'faye-online'
10
+ ```
11
+
12
+ 2. add faye related table
13
+
14
+ ```zsh
15
+ bundle exec rake db:migrate
16
+ ```
17
+
18
+ 3. faye server
19
+ Create a faye.ru at your rails app root, configure it,
20
+
21
+ ```ruby
22
+ $faye_server = FayeOnline.get_server(:host=>"localhost",
23
+ :port=>6379,
24
+ :database=>1,
25
+ :namespace=>"faye",
26
+ :gc=>5)
27
+ run $faye_server
28
+ ```
29
+
30
+ and start faye server
31
+
32
+ ```sh
33
+ bundle exec rake faye:start
34
+ DEBUG_FAYE=true DEBUG=true bundle exec rackup faye.ru -s thin -E production -p 9292
35
+ ```
36
+
37
+ 4. faye client
38
+
39
+ ```javascript
40
+ eoe.faye = Faye.init_online_client({
41
+ faye_url: faye_url,
42
+ client_opts: {},
43
+ auth_opts: {
44
+ room_channel: eoe.class_channel,
45
+ time_channel: eoe.lesson_channel,
46
+ current_user: eoe.current_user
47
+ }
48
+ });
49
+ ```
50
+
51
+ Date Storage Stucture
52
+ -------------------------------------------
53
+ * mysql.table.faye_channels channel name to id, use for key value cache.
54
+ * mysql.table.faye_channel_online_lists online user list, store as Set data structure, add and delete.
55
+ * mysql.table.faye_user_online_times user online time, store every start time and spend time.
56
+ * mysql.table.faye_user_login_logs verbose user login log, used for caculate online times alternately.
57
+ * .
58
+ * redis.hashes.ns/uid_to_clientids/room_channel all clientids of one uid in a room_channel
59
+ * redis.hashes.ns/uid_to_clientids/time_channel all clientids of one uid in a time_channel
60
+ * redis.keys.ns/clientid_auth/clientid map clientid to user info
61
+ * redis.keys.ns/clientid_status/clientid current clientid is login or logout
62
+
63
+
64
+ Glossary of Terms
65
+ -------------------------------------------
66
+ ### clientId
67
+ 客户端(一般是浏览器里的javascript)连到服务器后自动分配一个clientId。
68
+ 示例数据为:
69
+
70
+ ```json
71
+ {
72
+ "channel"=>"/meta/connect",
73
+ "clientId"=>"sqq4oxlwhj84zw92n0e592j8iq989yy",
74
+ "id"=>"7",
75
+ "auth"=>{
76
+ "room_channel"=>"/classes/4",
77
+ "time_channel"=>"/courses/5/lessons/1",
78
+ "current_user"=>{"uid"=>470700, "uname"=>"mvj3"}
79
+ }
80
+ }
81
+ ```
82
+
83
+ ### User Login
84
+ 有很多个clientId连到server。其中可能多个clientId关联的是一个user,这个在 Faye.init_online_client 可以配置。
85
+
86
+ ### User Logout
87
+ 一个user关联的所有clientId失去连接后才算离线。
88
+ server判断一个user离开房间的两种机制:
89
+
90
+ 1. client端主动触发disconnect发消息,对于的实际操作是关闭所有相关页面。
91
+
92
+ 2. 网络掉线。在 `FayeOnline.get_server` 设置gc参数让server端定时ping所有的clientId,具体方法在faye-redis-ruby.gem里的 `EventMachine.add_periodic_timer(gc, &method(:gc))` 。如果检测是失去连接,那么server就给自己发个伪装的disconnect消息,从而清理失去网络连接的clientId。
93
+
94
+
95
+ 改进的客户端autodisconnect
96
+ -------------------------------------------
97
+ ### 原来的情况
98
+ 在用户关闭浏览器前触发一个"/meta/disconnect"请求。这样如果用户否定关闭后,client对象却还是被销毁了。
99
+
100
+ ### 改进的方案
101
+ 在浏览器关闭前,发送给server一个过几秒后检测当前clientId是否失去连接的事件。
102
+
103
+ 1, 这样如果浏览器真的关掉了,那就和原来的autodisconnect发送的"/meta/disconnect"消息一样。
104
+
105
+ 2, 如果浏览器被用户选择否定关掉,那浏览器里的client对象还是没被销毁,而继续存活。
106
+
107
+
108
+ TODO
109
+ -------------------------------------------
110
+ 1. FayeOnline.faye_online_status 通用化
111
+ 2. Cluster front end https://github.com/alexkazeko/faye_shards
112
+ 3. use rainbows server, see faye-websocket README。一些尝试见rainbows.conf, https://groups.google.com/forum/#!msg/faye-users/cMPhvIpk-OU/MgRcFhmz8koJ
113
+ 4. 英文化
114
+
115
+
116
+
117
+ Related Resources
118
+ -------------------------------------------
119
+ 1. https://github.com/ryanb/private_pub Private Pub is a Ruby gem for use with Rails to publish and subscribe to messages through Faye. It allows you to easily provide real-time updates through an open socket without tying up a Rails process. All channels are private so users can only listen to events you subscribe them to.
120
+
121
+ 2. http://blog.edweng.com/2012/06/02/faye-extensions-tracking-users-in-a-chat-room/ only track clientId
122
+
123
+ 3. http://faye.jcoglan.com/ruby/monitoring.html tech detail about track connect and disconnect in faye
@@ -0,0 +1,28 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'rake'
4
+ require 'rake/testtask'
5
+
6
+ namespace :faye do
7
+ desc "Start faye server"
8
+ task :start => :environment do
9
+ # Rack::Builder.new.run @server # TODO 脱离父进程
10
+ # Rack::Server.start
11
+ #
12
+ # 自动启动faye服务器
13
+ # from http://stackoverflow.com/questions/6430437/autorun-the-faye-server-when-i-start-the-rails-server
14
+ @faye_ru = File.join(`bundle show faye-online`.strip, 'faye.ru')
15
+ Thread.new do
16
+ # system("bundle exec rackup faye.ru -s thin -E production")
17
+ system("bundle exec rackup #{@faye_ru} -s thin -E production")
18
+ end
19
+ end
20
+
21
+ end
22
+
23
+ Rake::TestTask.new do |t|
24
+ t.libs << 'test'
25
+ end
26
+
27
+ desc "Run tests"
28
+ task :default => :test
@@ -0,0 +1,52 @@
1
+ if (!window.jQuery) { throw("please require jquery.js first!"); }
2
+ if (!window.Faye) { throw("please require faye-browser.js first!"); }
3
+
4
+ // Faye 连接
5
+ Faye.init_online_client = function(opts) {
6
+ opts = opts || {};
7
+
8
+ // validate params
9
+ if (!opts.faye_url) { throw("faye_url is not defined!"); }
10
+ opts.client_opts = $.extend({
11
+ timeout: 10, // 只用于client连server时的主动检测timeout。如果client自行disconnect,那就没办法了。
12
+ interval: 5, // handshake every 5 seconds
13
+ retry: 5
14
+ }, opts.client_opts);
15
+ var faye = new Faye.Client(opts.faye_url, opts.client_opts);
16
+ // faye.getState() // "DISCONNECTED"
17
+ // faye.disable('websocket'); // cause loop connect error
18
+ faye.disable('autodisconnect'); // 就会关了页面还是保持连接
19
+
20
+ // copied from https://github.com/cloudfuji/kandan/
21
+ // Note that these events do not reflect the status of the client’s session, http://faye.jcoglan.com/browser/transport.html
22
+ // The transport is only considered down if a request or WebSocket connection explicitly fails, or times out, indicating the server is unreachable.
23
+ faye.bind("transport:down", function() { console.log("Comm link to Cybertron is down!"); });
24
+ faye.bind("transport:up", function() { console.log("Comm link is up!"); });
25
+
26
+ // 参考 https://github.com/cloudfuji/kandan/ 在线聊天系统
27
+ // http://faye.jcoglan.com/ruby/extensions.html
28
+ opts.auth_opts = opts.auth_opts || {};
29
+ opts.auth_opts.current_user = opts.auth_opts.current_user || {};
30
+ if (!opts.auth_opts.room_channel || !opts.auth_opts.time_channel || !opts.auth_opts.current_user.uid || !opts.auth_opts.current_user.uname) {
31
+ console.log("auth_opts is ", opts.auth_opts);
32
+ throw("auth_opts is not valid. see valided format in README");
33
+ }
34
+ var AuthExtension = {
35
+ outgoing: function (message, callback) {
36
+ message.auth = (message.auth || {});
37
+ $.extend(message.auth, opts.auth_opts);
38
+
39
+ callback(message);
40
+ }
41
+ };
42
+ faye.addExtension(AuthExtension);
43
+
44
+ // notice server to check my clientId after a few seconds, if there's
45
+ // no connection, and disconnect my clientId.
46
+ $(window).bind('beforeunload', function(event) {
47
+ faye.publish("/faye_online/before_leave");
48
+ });
49
+
50
+
51
+ return faye;
52
+ };
@@ -0,0 +1,6 @@
1
+ # encoding: UTF-8
2
+
3
+ class FayeChannel < ActiveRecord::Base
4
+ include IdNameCache; set_key_value :id, :name
5
+ attr_accessible :id, :name
6
+ end
@@ -0,0 +1,27 @@
1
+ # encoding: UTF-8
2
+
3
+ class FayeChannelOnlineList < ActiveRecord::Base
4
+ attr_accessible :channel_id, :user_info_list
5
+
6
+ def add uid
7
+ uid = uid.to_i; return if uid.zero?
8
+ online_list.update_attributes! :user_info_list => online_list.data.add(uid).to_json
9
+ end
10
+
11
+ def delete uid
12
+ uid = uid.to_i; return if uid.zero?
13
+ online_list.update_attributes! :user_info_list => online_list.data.delete(uid).to_json
14
+ end
15
+
16
+ def data
17
+ @data ||= begin
18
+ _a = JSON.parse(online_list.user_info_list) rescue []
19
+ Set.new(_a)
20
+ end
21
+ end
22
+
23
+ def user_list; online_list.data end
24
+ def user_count; online_list.data.count end
25
+
26
+ def online_list; self end
27
+ end
@@ -0,0 +1,6 @@
1
+ # encoding: UTF-8
2
+
3
+ class FayeUserLoginLog < ActiveRecord::Base
4
+ attr_accessible(*self.column_names)
5
+
6
+ end
@@ -0,0 +1,60 @@
1
+ # encoding: UTF-8
2
+
3
+ class FayeUserOnlineTime < ActiveRecord::Base
4
+ attr_accessible :user_id, :online_times
5
+ TIME2013 = Time.parse("20130101")
6
+ TimeFormat = "%Y%m%d%H%M%S"
7
+
8
+ def spend_time channel_name; @channel_name = channel_name; data[spend_key] end
9
+ def start_time channel_name; @channel_name = channel_name; data[start_key] end
10
+
11
+ # 只有两种状态,不管一个用户有多少个连接
12
+ # 1, 没开始: 设置started_at为当前值
13
+ # 2, 关闭所有连接: 去除started_at,更新spended
14
+ # 再添加一个连接: 在调用接口方实现
15
+ def start? channel_name
16
+ @channel_name = channel_name
17
+ !!data[start_key]
18
+ end
19
+ def start channel_name
20
+ @channel_name = channel_name
21
+ data[start_key] = Time.now.strftime(TimeFormat)
22
+ resave!
23
+ end
24
+ def start_key; "#{@channel_name}_started_at"; end
25
+ def spend_key; "#{@channel_name}_spended"; end
26
+
27
+ def spend_time_in_realtime channel_name
28
+ _old_spend_time = self.spend_time(channel_name).to_i
29
+
30
+ # 计时为0。 用户打开多个浏览器,在完成课时时其他页面没有关掉,导致faye_online_time不能关闭计时,所以无法计算spend_time
31
+ _new_spend_time = 0
32
+ if _start_time = self.start_time(channel_name)
33
+ # 兼容 没访问页面前 计时的start_time还没有开始
34
+ _new_spend_time = _start_time.blank? ? 0 : (Time.now - Time.parse(_start_time))
35
+ end
36
+
37
+ (_old_spend_time + _new_spend_time).round(0)
38
+ end
39
+
40
+ def stop channel_name
41
+ @channel_name = channel_name
42
+ if data[start_key]
43
+ data[spend_key] ||= 0
44
+ data[spend_key] += (Time.now - Time.parse(data[start_key])).to_i
45
+ data.delete start_key
46
+ resave!
47
+ end
48
+ end
49
+
50
+ def data
51
+ @data ||= (JSON.parse(online_time.online_times) rescue {})
52
+ end
53
+ def resave!
54
+ online_time.update_attributes! :online_times => data.to_json
55
+ end
56
+
57
+ def online_time; self end
58
+ def uid; self.user_id; end
59
+
60
+ end
@@ -0,0 +1,26 @@
1
+ class FayeCreateUserList < ActiveRecord::Migration
2
+ def change
3
+ create_table :faye_channels, :options => 'ENGINE=MyISAM DEFAULT CHARSET=utf8' do |t|
4
+ t.string :name
5
+ t.timestamps
6
+ end
7
+ add_index :faye_channels, [:name], :unique => true
8
+
9
+ create_table :faye_channel_online_lists, :options => 'ENGINE=Innodb DEFAULT CHARSET=utf8' do |t|
10
+ t.integer :channel_id, :default => 0
11
+ t.text :user_info_list
12
+ t.integer :lock_version, :default => 0
13
+ t.timestamps
14
+ end
15
+ add_index :faye_channel_online_lists, [:channel_id]
16
+
17
+ create_table :faye_user_online_times, :options => 'ENGINE=Innodb DEFAULT CHARSET=utf8' do |t|
18
+ t.integer :user_id, :default => 0
19
+ t.text :online_times
20
+ t.integer :lock_version, :default => 0
21
+ t.timestamps
22
+ end
23
+ add_index :faye_user_online_times, [:user_id], :unique => true
24
+
25
+ end
26
+ end
@@ -0,0 +1,15 @@
1
+ class AddFayeUserLoginLogs < ActiveRecord::Migration
2
+ def up
3
+ create_table :faye_user_login_logs, :options => 'ENGINE=Innodb DEFAULT CHARSET=utf8', :id => false do |t|
4
+ t.integer :status, :limit => 2
5
+ t.integer :uid
6
+ t.datetime :t
7
+ t.integer :channel_id
8
+ t.string :clientId
9
+ end
10
+ add_index :faye_user_login_logs, [:t, :channel_id, :uid], :name => 'idx_all'
11
+ end
12
+
13
+ def down
14
+ end
15
+ end
@@ -0,0 +1,29 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'faye-online'
3
+ s.version = '0.1'
4
+ s.date = '2013-03-26'
5
+ s.summary = File.read("README.markdown").split(/===+/)[0].strip
6
+ s.description = s.summary
7
+ s.authors = ["David Chen"]
8
+ s.email = 'mvjome@gmail.com'
9
+ s.homepage = 'https://github.com/eoecn/faye-online'
10
+
11
+ s.add_dependency "json"
12
+ s.add_dependency "rails"
13
+ s.add_dependency "activesupport"
14
+ s.add_dependency "activerecord_idnamecache"
15
+ s.add_dependency "thin"
16
+ s.add_dependency "rainbows"
17
+ s.add_dependency "redis"
18
+ s.add_dependency "faye"
19
+ s.add_dependency "faye-redis"
20
+ s.add_dependency "cross_time_calculation"
21
+
22
+ s.add_development_dependency 'sqlite3'
23
+ s.add_development_dependency 'combustion'
24
+ s.add_development_dependency 'pry-debugger'
25
+ s.add_development_dependency 'fakeredis'
26
+
27
+ s.files = `git ls-files`.split("\n")
28
+
29
+ end
@@ -0,0 +1,85 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'yaml'
4
+ require 'thin'
5
+ require 'json'
6
+ require 'rails'
7
+ require 'active_record'
8
+ require 'activerecord_idnamecache'
9
+ require 'redis'
10
+ require "faye"
11
+ require 'faye/redis'
12
+
13
+ (proc do
14
+ Faye::WebSocket.load_adapter('thin')
15
+ # Faye::WebSocket.load_adapter('rainbows')
16
+ if ENV['DEBUG_FAYE']
17
+ Faye::Logging.log_level = :debug
18
+ require 'logger'
19
+ _logger = Logger.new("log/faye.log")
20
+ Faye.logger = lambda {|m| _logger.info m }
21
+ end
22
+
23
+ # connect to database
24
+ database_yml = ENV['database_yml'] || File.join(ENV['RAILS_PATH'] || `pwd`.strip, 'config/database.yml')
25
+ ActiveRecord::Base.establish_connection YAML.load_file(database_yml).inject({}) {|h, kv| h[kv[0].to_sym] = kv[1]; h }[:production]
26
+ end).call
27
+
28
+ class FayeOnline
29
+ cattr_accessor :engine_proxy, :redis
30
+
31
+ ValidChannel = proc {|channel| !!channel.to_s.match(/\A[0-9a-z\/]+\Z/i) } # 只支持数字字母和斜杠
32
+ MONITORED_CHANNELS = ['/meta/connect', '/meta/disconnect'] # '/meta/subscribe', '/connect', '/close' are ignored
33
+ LOCAL_IP = Socket.ip_address_list.detect{|intf| intf.ipv4_private?}.ip_address # http://stackoverflow.com/questions/5029427/ruby-get-local-ip-nix
34
+ LOCAL_FAYE_URI = URI.parse("http://#{LOCAL_IP}:#{ENV['FAYE_PORT'] || 9292}/faye")
35
+ cattr_accessor :redis_opts, :faye_client, :valid_message_proc
36
+
37
+ def initialize redis_opts, valid_message_proc = nil
38
+
39
+ raise "Please run `$faye_server = FayeOnline.get_server` first, cause we have to bind disconnect event." if not $faye_server.is_a?(Faye::RackAdapter)
40
+ FayeOnline.redis_opts = redis_opts
41
+ FayeOnline.valid_message_proc = valid_message_proc || (proc {|message| true })
42
+ FayeOnline.redis = Redis.new(FayeOnline.redis_opts)
43
+ FayeOnline.redis.select FayeOnline.redis_opts[:database]
44
+
45
+ FayeOnline.faye_client ||= Faye::Client.new(LOCAL_FAYE_URI.to_s)
46
+
47
+ # 配置ActiveRecord
48
+ if Rails.root.nil?
49
+ Dir[File.expand_path('../../app/models/*.rb', __FILE__)].each {|f| require f }
50
+ end
51
+
52
+ return self
53
+ end
54
+
55
+ def incoming(message, callback)
56
+ Message.new(message).process
57
+ callback.call(message)
58
+ end
59
+
60
+ def self.channel_clientIds_array
61
+ array = []
62
+ FayeOnline.redis.keys("/#{FayeOnline.redis_opts[:namespace]}/uid_to_clientIds*").sort.each do |k|
63
+ _data = FayeOnline.redis.hgetall(k).values.map {|i| JSON.parse(i) rescue i }.flatten
64
+ array << [k, _data]
65
+ end
66
+ array
67
+ end
68
+ def self.uniq_clientIds
69
+ self.channel_clientIds_array.map(&:last).flatten.uniq
70
+ end
71
+
72
+ def self.disconnect clientId
73
+ message = {'channel' => '/meta/disconnect', 'clientId' => clientId}
74
+
75
+ # fake a client to disconnect, 仅仅接受message.auth为空,即网络失去连接的情况
76
+ FayeOnline::Message.new(message.merge('fake' => 'true')).process if not message['auth']
77
+ end
78
+
79
+ end
80
+
81
+
82
+ require File.expand_path("../faye-online/message.rb", __FILE__)
83
+ require File.expand_path("../faye-online/rails_engine.rb", __FILE__)
84
+ require File.expand_path("../faye-online/status.rb", __FILE__)
85
+ require File.expand_path("../faye-online/server.rb", __FILE__)
@@ -0,0 +1,183 @@
1
+ # encoding: UTF-8
2
+
3
+ # 管理用户在线文档
4
+ # 一个用户在单个房间单个多个页面,就有了多个clientId。
5
+ # 1. 用户在线状态规则
6
+ # 在线: 超过一个clientId
7
+ # 离线: 零个clientId
8
+ # 2. 清理过期clientId,以防止存储在redis Hashes里的数组无法删除个别元素。因为怀疑为过期clientId之后就没有连接,所以也不会进行任何操作。
9
+ # 采用策略是放到一个clientId和开始时间对应的redis Hashes里,在用户'/meta/disconnect'时检测各个clientId有效性
10
+
11
+ class FayeOnline
12
+ # To allow multiple messages process in their own Message instance.
13
+ class Message
14
+
15
+ attr_accessor :message
16
+ def initialize _msg
17
+ self.message = _msg
18
+ self.current_channel # load message.auth
19
+ self
20
+ end
21
+
22
+ def process
23
+ @time_begin = Time.now
24
+
25
+ # 主动检测是否离开,以减少GC的消耗
26
+ # TODO re-implement autodisonnect, pull request to faye.gem
27
+ if message['channel'] == "/faye_online/before_leave"
28
+ EM.run do
29
+ EM.add_timer(3) do
30
+ FayeOnline.disconnect(current_clientId) if not FayeOnline.engine_proxy.has_connection?(current_clientId)
31
+ end
32
+ end
33
+ return false;
34
+ end
35
+
36
+ # 验证是否用FayeOnline处理
37
+ step_idx = MONITORED_CHANNELS.index(message['channel'])
38
+ return false if step_idx.nil?
39
+
40
+ # 验证配置参数是否正确
41
+ return false if !(message['auth'] && (room_channel && time_channel && current_user))
42
+ raise "#{current_user.inspect} is invalid, the correct data struct is, .e.g. {uid: 470700, uname: 'mvj3'}" if !(current_user["uid"] && current_user["uname"])
43
+
44
+ # 验证渠道名字是否合法
45
+ (puts "invalid channel => #{message.inspect}" if ENV['DEBUG']; return false) if !(ValidChannel.call(message['auth']['room_channel']) && ValidChannel.call(message['auth']['time_channel']))
46
+ # 验证message是否合法
47
+ (puts "invalid message => #{message.inspect}" if ENV['DEBUG']; return false) if not FayeOnline.valid_message_proc.call(message)
48
+
49
+ begin
50
+ case step_idx
51
+
52
+ # A. 处理*开启*一个客户端
53
+ when 0
54
+ ### 处理 room_channel 在线人数
55
+ # *开始1* add 当前用户的clientIds数组
56
+ current_user_in_current_room__clientIds.add(current_clientId)
57
+ online_list.add current_user['uid']
58
+
59
+ ### 处理 time_chanel 在线时长
60
+ current_user_in_current_time__clientIds.add(current_clientId)
61
+ online_time.start time_channel if not online_time.start? time_channel
62
+ logger_online_info "连上"
63
+
64
+ # 绑定用户数据到clientId,以供服务器在主动disconnect时使用
65
+
66
+ # B. 处理*关闭*一个客户端(,但是这个用户可能还有其他客户端在连着)
67
+ when 1
68
+ # 解除 因意外原因导致的 clientId 没有过期
69
+ current_user_current_clientIds.each do |_clientId|
70
+ str = if FayeOnline.engine_proxy.has_connection?(_clientId)
71
+ "clientId[#{_clientId}] 还有连接"
72
+ else
73
+ [current_user_current_clientIds_arrays, current_user_in_current_time__clientIds].each {|a| a.delete _clientId }
74
+ "clientId[#{_clientId}] 没有连接的无效 已被删除"
75
+ end
76
+ puts str if ENV['DEBUG']
77
+ end
78
+ puts if ENV['DEBUG']
79
+
80
+ ### 处理 room_channel 在线人数
81
+ # *开始2* delete 当前用户的clientIds数组
82
+ current_user_in_current_room__clientIds.delete(current_clientId)
83
+ online_list.delete current_user['uid'] if current_user_in_current_room__clientIds.blank? # 一个uid的全部clientId都退出了
84
+
85
+ ### 处理 time_chanel 在线时长
86
+ # 关闭定时在线时间
87
+ current_user_in_current_time__clientIds.delete(current_clientId)
88
+ online_time.stop time_channel if current_user_in_current_time__clientIds.size.zero?
89
+ logger_online_info "离开"
90
+ end
91
+
92
+ rescue => e # 确保每次都正常存储current_user_in_current_room__clientIds
93
+ puts [e, e.backtrace].flatten.join("\n")
94
+ end
95
+
96
+ # *结束* save 当前用户的clientIds数组
97
+ FayeOnline.redis.hset redis_key__room, current_user['uid'], current_user_in_current_room__clientIds.to_json
98
+ FayeOnline.redis.hset redis_key__time, current_user['uid'], current_user_in_current_time__clientIds.to_json
99
+
100
+ # 发布在线用户列表
101
+ FayeOnline.faye_client.publish("#{room_channel}/user_list", {'count' => online_list.user_count, 'user_list' => online_list.user_list}) if FayeOnline.faye_client
102
+
103
+ puts "本次处理处理时间 #{((Time.now - @time_begin) * 1000).round(2)}ms" if ENV['DEBUG']
104
+ puts message.inspect
105
+ puts
106
+ return true
107
+ end
108
+
109
+ def logger_online_info status
110
+ _t = online_time.start_time(time_channel)
111
+ _start_time_str_ = _t ? Time.parse(_t).strftime("%Y-%m-%d %H:%M:%S") : nil
112
+ puts "*#{status}* 用户#{current_user['uname']}[#{current_user['uid']}] 的clientId #{current_clientId}。"
113
+ puts "当前用户在 #{redis_key__room} 的clientIds列表为 #{current_user_in_current_room__clientIds.inspect}。在线用户有#{online_list.user_list.count}个,他们是 #{online_list.user_list.inspect}"
114
+ puts "当前用户在 #{redis_key__time} 的clientIds列表为 #{current_user_in_current_time__clientIds.inspect}。开始时间为#{_start_time_str_}, 渡过时间为 #{online_time.spend_time(time_channel) || '空'}。"
115
+
116
+ # 记录用户登陆登出情况,方便之后追踪
117
+ _s = {"连上" => 1, "离开" => 0}[status]
118
+ # 用可以过期的Redis键值对来检测单个clientId上次是否为 "连上" 或 "离开"
119
+ _k = "/#{FayeOnline.redis_opts[:namespace]}/clientId_status/#{current_clientId}"
120
+ _s_old = FayeOnline.redis.get(_k).to_s
121
+ # *连上*和*离开* 只能操作一次
122
+ if _s_old.blank? || # 没登陆的
123
+ (_s.to_s != _s_old) # 已登陆的
124
+
125
+ # 把不*连上*和*离开*把放一张表,写时不阻塞
126
+ FayeUserLoginLog.create! :channel_id => FayeChannel[time_channel], :uid => current_user['uid'], :t => DateTime.now, :status => _s, :clientId => current_clientId
127
+
128
+ FayeOnline.redis.multi do
129
+ FayeOnline.redis.set(_k, _s)
130
+ FayeOnline.redis.expire(_k, 2.weeks)
131
+ end
132
+ end
133
+ end
134
+ def online_list; FayeChannelOnlineList.find_or_create_by_channel_id(FayeChannel[room_channel]) end
135
+ def online_time; FayeUserOnlineTime.find_or_create_by_user_id(current_user['uid']) end
136
+
137
+ # 渠道信息
138
+ def current_channel
139
+ # 从clientId反设置auth信息,并只设置一次
140
+ if message['auth'] && !message['_is_auth_load']
141
+ FayeOnline.redis.multi do
142
+ FayeOnline.redis.set(redis_key__auth, message['auth'].to_json)
143
+ FayeOnline.redis.expire(redis_key__auth, 2.weeks)
144
+ end
145
+ message['_is_auth_load'] = true
146
+ else
147
+ message['auth'] ||= JSON.parse(FayeOnline.redis.get(redis_key__auth)) rescue {}
148
+ end
149
+ message['channel']
150
+ end
151
+ def time_channel; message['auth']['time_channel'] end
152
+ def room_channel; message['auth']['room_channel'] end
153
+
154
+ # 用户信息
155
+ def current_user; message['auth']['current_user'] end
156
+ def current_clientId; message['clientId'] end
157
+
158
+ # room和time 分别对应 clientId 的关系
159
+ def current_user_in_current_room__clientIds
160
+ @_current_user_in_current_room__clientIds ||= begin
161
+ _a = JSON.parse(FayeOnline.redis.hget(redis_key__room, current_user['uid'])) rescue []
162
+ Set.new(_a)
163
+ end
164
+ end
165
+ def current_user_in_current_time__clientIds
166
+ @_current_user_in_current_time__clientIds ||= begin
167
+ _a = JSON.parse(FayeOnline.redis.hget(redis_key__time, current_user['uid'])) rescue []
168
+ Set.new(_a)
169
+ end
170
+ end
171
+ def redis_key__room; "/#{FayeOnline.redis_opts[:namespace]}/uid_to_clientIds#{room_channel}" end
172
+ def redis_key__time; "/#{FayeOnline.redis_opts[:namespace]}/uid_to_clientIds#{time_channel}" end
173
+ def redis_key__auth; "/#{FayeOnline.redis_opts[:namespace]}/clientId_auth/#{current_clientId}" end
174
+ def current_user_current_clientIds_arrays
175
+ [current_user_in_current_room__clientIds, current_user_in_current_time__clientIds]
176
+ end
177
+ def current_user_current_clientIds
178
+ current_user_current_clientIds_arrays.map(&:to_a).flatten.uniq
179
+ end
180
+
181
+ end
182
+
183
+ end
@@ -0,0 +1,20 @@
1
+ # encoding: UTF-8
2
+
3
+ module FayeOnline_Rails
4
+ class Engine < Rails::Engine
5
+ initializer "faye_online_rails.load_app_instance_data" do |app|
6
+ app.class.configure do
7
+ ['db/migrate', 'app/assets', 'app/models', 'app/controllers', 'app/views'].each do |path|
8
+ config.paths[path] ||= []
9
+ config.paths[path] += QA_Rails::Engine.paths[path].existent
10
+ end
11
+ end
12
+ end
13
+ initializer "faye_online_rails.load_static_assets" do |app|
14
+ app.middleware.use ::ActionDispatch::Static, "#{root}/public"
15
+ end
16
+ rake_tasks do
17
+ load File.expand_path('../../../Rakefile', __FILE__)
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,60 @@
1
+ # encoding: UTF-8
2
+
3
+ def FayeOnline.get_server redis_opts, valid_message_proc = nil
4
+ $faye_server = Faye::RackAdapter.new(
5
+ :mount => '/faye',
6
+
7
+ # the maximum time to hold a connection open before returning the response. This is given in seconds and must be smaller than the timeout on your frontend webserver(thin). Faye uses Thin as its webserver, whose default timeout is 30 seconds.
8
+ # https://groups.google.com/forum/?fromgroups#!topic/faye-users/DvFrPGOinKw
9
+ :timeout => 10,
10
+
11
+ # [https://groups.google.com/forum/#!searchin/faye-users/disconnect/faye-users/2bn8xUHF5-E/A4a3Sk7RgW4J] It's expected. The browser will not always be able to deliver an explicit disconnect message, which is why there is server-side idle client detection.
12
+ # garbag collect disconnected clientIds in EventMachine.add_periodic_timer
13
+ :engine => {:gc => 60}.merge(redis_opts.merge(:type => Faye::Redis)),
14
+
15
+ :ping => 30 # (optional) how often, in seconds, to send keep-alive ping messages over WebSocket and EventSource connections. Use this if your Faye server will be accessed through a proxy that kills idle connections.
16
+ )
17
+
18
+ $faye_server.bind(:handshake) do |clientId|
19
+ end
20
+ $faye_server.bind(:subscribe) do |clientId, channel|
21
+ end
22
+ $faye_server.bind(:unsubscribe) do |clientId, channel|
23
+ end
24
+ $faye_server.bind(:publish) do |clientId, channel, data|
25
+ end
26
+ $faye_server.bind(:disconnect) do |clientId|
27
+ FayeOnline.disconnect clientId
28
+
29
+ # dynamic compute interval seconds
30
+ tmp = FayeOnline.channel_clientIds_array.reject {|i| i[1].blank? }
31
+ # delete below, cause map data is valid
32
+ if tmp.any?
33
+ puts "开始有 #{FayeOnline.uniq_clientIds.count}个"
34
+ tmp.map(&:last).flatten.uniq.shuffle[0..19].each do |_clientId|
35
+ if not FayeOnline.engine_proxy.has_connection? _clientId
36
+ puts "开始处理无效 #{_clientId}"
37
+ # 1. 先伪装去disconnect clientId
38
+ # 2. 如果失败,就直接操作redis修改
39
+ if not FayeOnline.disconnect(_clientId)
40
+ # 没删除成功,因为之前没有设置auth
41
+ k = (tmp.detect {|a, b| b.index(_clientId) } || [])[0]
42
+ # 直接从redis清除无效_clientId
43
+ FayeOnline.redis.hgetall(k).each do |k2, v2|
44
+ v3 = JSON.parse(v2) rescue []
45
+ v3.delete _clientId
46
+ FayeOnline.redis.hset(k, k2, v3.to_json)
47
+ end if k
48
+ end
49
+ end
50
+ end
51
+ puts "结束有 #{FayeOnline.uniq_clientIds.count}个"
52
+ end
53
+ end
54
+
55
+ $faye_server.add_extension FayeOnline.new(redis_opts, valid_message_proc)
56
+
57
+ FayeOnline.engine_proxy = $faye_server.instance_variable_get("@server").engine
58
+
59
+ return $faye_server
60
+ end
@@ -0,0 +1,79 @@
1
+ # encoding: UTF-8
2
+
3
+ def FayeOnline.status
4
+ array = []
5
+ clientIds = Set.new
6
+ channel_clientIds_array = FayeOnline.channel_clientIds_array
7
+
8
+ clientId_to_users = channel_clientIds_array.map(&:last).flatten.uniq.inject({}) do |h, _clientId|
9
+ _data = JSON.parse(Redis.current.get("/#{FayeOnline.redis_opts[:namespace]}/clientId_auth/#{_clientId}")) rescue {}
10
+ h[_clientId] = (_data['current_user'] || {}).except('uhash').values
11
+ h
12
+ end
13
+
14
+ channel_clientIds_array.each do |_channel, _clientIds|
15
+ _a = _clientIds.map {|i| [i, clientId_to_users[i]] }
16
+ _c = _a.map {|i| i[1][0] }.uniq.count
17
+ array << "#{_channel}: #{_c}个用户: #{_a}"
18
+ _clientIds.each {|i| clientIds.add i }
19
+ end
20
+
21
+ array.unshift ""
22
+ users = clientIds.map {|i| clientId_to_users[i] }.uniq {|i| i[0] }
23
+ array.unshift "/classes/[0-9]+ 用于班级讨论的消息通讯, /courses/[0-9]+/lessons/[0-9]+ 用于课时的计时"
24
+ array.unshift ""
25
+ array.unshift "#{users.count}个用户分别是: #{users}"
26
+ array.unshift ""
27
+ array.unshift "实时在线clientIds总共有#{clientIds.count}个: #{clientIds.to_a}"
28
+ array.unshift ""
29
+ array.unshift "一个clientId表示用户打开了一个页面。一个用户在同一课时可能打开多个页面,那就是一个user,多个clientId"
30
+
31
+ # 删除意外没有退出的在线用户列表
32
+ uids = users.map(&:first)
33
+ FayeChannelOnlineList.all.reject {|i| i.data.blank? }.each do |online_list|
34
+ online_list.user_list.each do |uid|
35
+ online_list.delete uid if not uids.include? uid
36
+ end
37
+ end
38
+
39
+ array
40
+ end
41
+
42
+
43
+ def FayeOnline.log params, user_name_proc = proc {|uid| uid }
44
+ scope = FayeUserLoginLog.order("t ASC")
45
+
46
+ # 个人整理后列出
47
+ if params[:user]
48
+ scope = scope.where(:uid => user_name_proc.call(params[:user]))
49
+
50
+ channel_to_logs = scope.inject({}) {|h, log| i = FayeChannel[log.channel_id]; h[i] ||= []; h[i] << log; h }
51
+
52
+ array = ["用户 #{params[:user]}[#{user_name_proc.call(params[:user])}] 的登陆日志详情"]
53
+ channel_to_logs.each do |channel, logs|
54
+ array << ''
55
+ array << channel
56
+ logs2 = logs.inject({}) {|h, log| h[log.clientId] ||= []; h[log.clientId] << log; h }
57
+
58
+ # 合并交叉的时间
59
+ ctc = CrossTimeCalculation.new
60
+ logs2.each do |clientId, _logs|
61
+ # logs = logs.sort {|a, b| (a && a.t) <=> (b && b.t) }
62
+ if _logs.size > 0
63
+ # binding.pry if _logs[1].nil?
64
+ te = _logs[1] ? _logs[1].t : nil
65
+ ctc.add(_logs[0].t, te)
66
+ _time_passed = CrossTimeCalculation.new.add(_logs[0].t, te).total_seconds.to_i
67
+ end
68
+ array << [clientId, _logs.map {|_log| "#{_log.status}: #{_log.t.strftime("%Y-%m-%d %H:%M:%S")}" }, "#{_time_passed || '未知'}秒"].flatten.compact.inspect
69
+ end
70
+ array << "共用时 #{ctc.total_seconds.to_i}秒"
71
+ end
72
+ array
73
+ # 群体直接列出日志
74
+ else
75
+ scope.limit(500).map do |log|
76
+ [%w[离开 连上][log.status], log.uid, user_name_proc.call(log.uid), log.t.strftime("%Y-%m-%d %H:%M:%S"), FayeChannel[log.channel_id], log.clientId].inspect
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,34 @@
1
+ # encoding: UTF-8
2
+
3
+ # DEBUG_FAYE=true DEBUG=true bundle exec rainbows faye.ru -c /Users/mvj3/github/eoecn/faye-online/rainbows.conf -E production -p 9292
4
+
5
+ Rainbows! do
6
+ ####################
7
+ ### concurrency model to use
8
+ #
9
+ # WebSocket connection to 'ws://10.0.0.65:9292/faye' failed: No response code found: HTTP/1.1 -1 http://10.0.0.65:8003/courses/5/lessons/1
10
+ # WebSocket connection to 'ws://10.0.0.65:9292/faye' failed: WebSocket is closed before the connection is established.
11
+ # disconnect directly
12
+ # use :ThreadSpawn
13
+ #
14
+ # 错误: XMLHttpRequest cannot load http://10.0.0.65:9292/faye. Origin http://10.0.0.65:8003 is not allowed by Access-Control-Allow-Origin.
15
+ # use :FiberSpawn
16
+ #
17
+ # the client connect to server looply by Faye.Transport.CORS in faye.client.js
18
+ # {"channel"=>"/meta/connect", "clientId"=>"ekf8100ih4z04jiy2oo7l46qcud6wai", "connectionType"=>"cross-origin-long-polling", "id"=>"bi", "auth"=>{"room_channel"=>"/classes/7", "time_channel"=>"/courses/5/lessons/1", "current_user"=>{"uid"=>557558, "uname"=>"yiyou99storm", "uhash"=>""}}}
19
+ use :EventMachine
20
+ ####################
21
+
22
+
23
+ worker_connections 500
24
+ keepalive_timeout 5 # zero disables keepalives entirely
25
+ client_max_body_size 5*1024*1024 # 5 megabytes
26
+ keepalive_requests 666 # default:100
27
+ client_header_buffer_size 2 * 1024 # 2 kilobytes
28
+
29
+ end
30
+
31
+ # the rest of the Unicorn configuration...
32
+ worker_processes 2 # CPU cores
33
+ # stderr_path "/path/to/error.log"
34
+ # stdout_path "/path/to/output.log"
@@ -0,0 +1,6 @@
1
+ production:
2
+ host: localhost
3
+ adapter: sqlite3
4
+ encoding: utf8
5
+ collation: utf8_general_ci
6
+ database: ":memory:"
@@ -0,0 +1,110 @@
1
+ # encoding: UTF-8
2
+
3
+ ENV['database_yml'] = File.expand_path('../database.yml', __FILE__)
4
+
5
+ require 'test/unit'
6
+ require 'pry-debugger'
7
+ require 'active_support/core_ext'
8
+
9
+ require 'faye-online'
10
+ require 'fake_redis'
11
+
12
+
13
+ class TestFayeOnline < Test::Unit::TestCase
14
+ # setup database
15
+ Dir['db/migrate/*'].map {|i| eval File.read(i).gsub(/ENGINE=(MyISAM|Innodb) DEFAULT CHARSET=utf8/i, "") } # support sqlite
16
+ # migrate
17
+ FayeCreateUserList.new.change
18
+ AddFayeUserLoginLogs.new.up
19
+
20
+ # setup redis & faye server
21
+ redis_opts = {:host=>"localhost", :port=>50000, :database=>1, :namespace=>"faye"}
22
+ $faye_server = FayeOnline.get_server redis_opts
23
+ FayeOnline.faye_client = nil # dont publish from server to client
24
+
25
+ def setup
26
+ @message_connect = {"channel"=>"/meta/connect", "clientId"=>"sqq4oxlwhj84zw92n0e592j8iq989yy", "id"=>"7", "auth"=>{"room_channel"=>"/classes/4", "time_channel"=>"/courses/5/lessons/1", "current_user"=>{"uid"=>470700, "uname"=>"mvj3"}}}
27
+ @message_connect_1 = {"channel"=>"/meta/connect", "clientId"=>"qqq4oxlwhj84zw92n0e592j8iq989yy", "id"=>"8", "auth"=>{"room_channel"=>"/classes/4", "time_channel"=>"/courses/5/lessons/1", "current_user"=>{"uid"=>1, "uname"=>"admin"}}}
28
+ @message_connect_2 = {"channel"=>"/meta/connect", "clientId"=>"pqq4oxlwhj84zw92n0e592j8iq989yy", "id"=>"9", "auth"=>{"room_channel"=>"/classes/4", "time_channel"=>"/courses/5/lessons/1", "current_user"=>{"uid"=>2, "uname"=>"iceskysl"}}}
29
+
30
+ @room_channel_id = FayeChannel[@message_connect['auth']['room_channel']]
31
+ end
32
+
33
+ def test_validate_message
34
+ msg = @message_connect.dup
35
+ msg['auth']['current_user'].delete 'uid'
36
+ assert_raise RuntimeError do
37
+ FayeOnline::Message.new(msg).process
38
+ end
39
+
40
+ msg = @message_connect.dup
41
+ msg['auth'].delete('room_channel')
42
+ assert_not_equal FayeOnline::Message.new(msg).process, true
43
+ end
44
+
45
+ def test_one_user
46
+ # login
47
+ FayeOnline::Message.new(@message_connect).process
48
+ assert_equal FayeOnline.channel_clientIds_array.flatten.count("sqq4oxlwhj84zw92n0e592j8iq989yy"), 2, "Login clientId should have two `sqq4oxlwhj84zw92n0e592j8iq989yy`"
49
+
50
+ # relogin the same clientId
51
+ FayeOnline::Message.new(@message_connect).process
52
+ assert_equal FayeOnline.channel_clientIds_array.flatten.count("sqq4oxlwhj84zw92n0e592j8iq989yy"), 2, "Login clientId should have two `sqq4oxlwhj84zw92n0e592j8iq989yy`"
53
+ assert_equal FayeUserLoginLog.where(:clientId => "sqq4oxlwhj84zw92n0e592j8iq989yy").count, 1, "Login at second time, but there should only one log"
54
+
55
+ assert_equal FayeChannelOnlineList.where(:channel_id => @room_channel_id).first.user_list, Set.new([470700]), "There should be only 470700 user"
56
+
57
+ # logout
58
+ msg = @message_connect.dup
59
+ msg["channel"] = "/meta/disconnect"
60
+ FayeOnline::Message.new(msg).process
61
+ assert_equal FayeOnline.channel_clientIds_array.flatten.count("sqq4oxlwhj84zw92n0e592j8iq989yy"), 0, "Login clientId should have none `sqq4oxlwhj84zw92n0e592j8iq989yy`"
62
+ assert_equal FayeUserLoginLog.where(:clientId => "sqq4oxlwhj84zw92n0e592j8iq989yy").count, 2, "Login twice, but there should only two log"
63
+
64
+ FayeUserLoginLog.delete_all
65
+ end
66
+
67
+ def test_more_user
68
+ # login id#1, id#470700
69
+ [@message_connect, @message_connect_1].each {|msg| FayeOnline::Message.new(msg).process }
70
+ assert_equal FayeUserLoginLog.where(:clientId => [@message_connect['clientId'], @message_connect_1['clientId']]).count, 2, "Two user login, and there should have two logs"
71
+ assert_equal FayeChannelOnlineList.where(:channel_id => @room_channel_id).first.user_list, Set.new([1, 470700]), "There should be two user"
72
+
73
+ # logout id#1
74
+ FayeOnline::Message.new(@message_connect_1.merge("channel" => "/meta/disconnect")).process
75
+ assert_equal FayeUserLoginLog.where(:clientId => [@message_connect['clientId'], @message_connect_1['clientId']]).count, 3, "two user logins, one of them logout, and there should have three logs"
76
+ assert_equal FayeChannelOnlineList.where(:channel_id => @room_channel_id).first.user_list, Set.new([470700]), "There should be one user"
77
+
78
+ # login id#1, id#2
79
+ [@message_connect_1, @message_connect_2].each {|msg| FayeOnline::Message.new(msg).process }
80
+ assert_equal FayeUserLoginLog.where(:clientId => [@message_connect['clientId'], @message_connect_1['clientId'], @message_connect_2['clientId']]).count, 5, "five logs"
81
+ assert_equal FayeChannelOnlineList.where(:channel_id => @room_channel_id).first.user_list, Set.new([1, 2, 470700]), "There should be three user"
82
+
83
+ # logout id#470700
84
+ FayeOnline::Message.new(@message_connect.merge("channel" => "/meta/disconnect")).process
85
+ assert_equal FayeUserLoginLog.where(:clientId => [@message_connect['clientId'], @message_connect_1['clientId'], @message_connect_2['clientId']]).count, 6, "six logs"
86
+ assert_equal FayeChannelOnlineList.where(:channel_id => @room_channel_id).first.user_list, Set.new([1, 2]), "There should be two user"
87
+
88
+ # logout id#1, #id2
89
+ FayeOnline::Message.new(@message_connect_1.merge("channel" => "/meta/disconnect")).process
90
+ FayeOnline::Message.new(@message_connect_2.merge("channel" => "/meta/disconnect")).process
91
+ assert_equal FayeUserLoginLog.where(:clientId => [@message_connect['clientId'], @message_connect_1['clientId'], @message_connect_2['clientId']]).count, 8, "eight logs"
92
+ assert_equal FayeChannelOnlineList.where(:channel_id => @room_channel_id).first.user_list, Set.new, "There should be none user"
93
+
94
+ FayeUserLoginLog.delete_all
95
+ end
96
+
97
+ def test_online_time
98
+ channel_name = 'test'
99
+ online_time = FayeUserOnlineTime.find_or_create_by_user_id(@message_connect['auth']['current_user']['uid'])
100
+ assert_equal online_time.start?(channel_name), false
101
+
102
+ online_time.start channel_name
103
+ assert_equal online_time.start?(channel_name), true
104
+ sleep 1
105
+ online_time.stop channel_name
106
+ assert_equal online_time.spend_time_in_realtime(channel_name).zero?, false
107
+ assert_equal online_time.spend_time(channel_name).zero?, false
108
+ end
109
+
110
+ end
metadata ADDED
@@ -0,0 +1,288 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: faye-online
3
+ version: !ruby/object:Gem::Version
4
+ version: '0.1'
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - David Chen
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-03-26 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: json
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: rails
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: activesupport
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ - !ruby/object:Gem::Dependency
63
+ name: activerecord_idnamecache
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :runtime
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: thin
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ type: :runtime
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ - !ruby/object:Gem::Dependency
95
+ name: rainbows
96
+ requirement: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ! '>='
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ type: :runtime
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ! '>='
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ - !ruby/object:Gem::Dependency
111
+ name: redis
112
+ requirement: !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - ! '>='
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ none: false
122
+ requirements:
123
+ - - ! '>='
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ - !ruby/object:Gem::Dependency
127
+ name: faye
128
+ requirement: !ruby/object:Gem::Requirement
129
+ none: false
130
+ requirements:
131
+ - - ! '>='
132
+ - !ruby/object:Gem::Version
133
+ version: '0'
134
+ type: :runtime
135
+ prerelease: false
136
+ version_requirements: !ruby/object:Gem::Requirement
137
+ none: false
138
+ requirements:
139
+ - - ! '>='
140
+ - !ruby/object:Gem::Version
141
+ version: '0'
142
+ - !ruby/object:Gem::Dependency
143
+ name: faye-redis
144
+ requirement: !ruby/object:Gem::Requirement
145
+ none: false
146
+ requirements:
147
+ - - ! '>='
148
+ - !ruby/object:Gem::Version
149
+ version: '0'
150
+ type: :runtime
151
+ prerelease: false
152
+ version_requirements: !ruby/object:Gem::Requirement
153
+ none: false
154
+ requirements:
155
+ - - ! '>='
156
+ - !ruby/object:Gem::Version
157
+ version: '0'
158
+ - !ruby/object:Gem::Dependency
159
+ name: cross_time_calculation
160
+ requirement: !ruby/object:Gem::Requirement
161
+ none: false
162
+ requirements:
163
+ - - ! '>='
164
+ - !ruby/object:Gem::Version
165
+ version: '0'
166
+ type: :runtime
167
+ prerelease: false
168
+ version_requirements: !ruby/object:Gem::Requirement
169
+ none: false
170
+ requirements:
171
+ - - ! '>='
172
+ - !ruby/object:Gem::Version
173
+ version: '0'
174
+ - !ruby/object:Gem::Dependency
175
+ name: sqlite3
176
+ requirement: !ruby/object:Gem::Requirement
177
+ none: false
178
+ requirements:
179
+ - - ! '>='
180
+ - !ruby/object:Gem::Version
181
+ version: '0'
182
+ type: :development
183
+ prerelease: false
184
+ version_requirements: !ruby/object:Gem::Requirement
185
+ none: false
186
+ requirements:
187
+ - - ! '>='
188
+ - !ruby/object:Gem::Version
189
+ version: '0'
190
+ - !ruby/object:Gem::Dependency
191
+ name: combustion
192
+ requirement: !ruby/object:Gem::Requirement
193
+ none: false
194
+ requirements:
195
+ - - ! '>='
196
+ - !ruby/object:Gem::Version
197
+ version: '0'
198
+ type: :development
199
+ prerelease: false
200
+ version_requirements: !ruby/object:Gem::Requirement
201
+ none: false
202
+ requirements:
203
+ - - ! '>='
204
+ - !ruby/object:Gem::Version
205
+ version: '0'
206
+ - !ruby/object:Gem::Dependency
207
+ name: pry-debugger
208
+ requirement: !ruby/object:Gem::Requirement
209
+ none: false
210
+ requirements:
211
+ - - ! '>='
212
+ - !ruby/object:Gem::Version
213
+ version: '0'
214
+ type: :development
215
+ prerelease: false
216
+ version_requirements: !ruby/object:Gem::Requirement
217
+ none: false
218
+ requirements:
219
+ - - ! '>='
220
+ - !ruby/object:Gem::Version
221
+ version: '0'
222
+ - !ruby/object:Gem::Dependency
223
+ name: fakeredis
224
+ requirement: !ruby/object:Gem::Requirement
225
+ none: false
226
+ requirements:
227
+ - - ! '>='
228
+ - !ruby/object:Gem::Version
229
+ version: '0'
230
+ type: :development
231
+ prerelease: false
232
+ version_requirements: !ruby/object:Gem::Requirement
233
+ none: false
234
+ requirements:
235
+ - - ! '>='
236
+ - !ruby/object:Gem::Version
237
+ version: '0'
238
+ description: Faye Online user list and time count
239
+ email: mvjome@gmail.com
240
+ executables: []
241
+ extensions: []
242
+ extra_rdoc_files: []
243
+ files:
244
+ - .gitignore
245
+ - Gemfile
246
+ - README.markdown
247
+ - Rakefile
248
+ - app/assets/javascripts/faye-online.js
249
+ - app/models/faye_channel.rb
250
+ - app/models/faye_channel_online_list.rb
251
+ - app/models/faye_user_login_log.rb
252
+ - app/models/faye_user_online_time.rb
253
+ - db/migrate/20130326113504_faye_create_user_list.rb
254
+ - db/migrate/20130531033325_add_faye_user_login_logs.rb
255
+ - faye-online.gemspec
256
+ - lib/faye-online.rb
257
+ - lib/faye-online/message.rb
258
+ - lib/faye-online/rails_engine.rb
259
+ - lib/faye-online/server.rb
260
+ - lib/faye-online/status.rb
261
+ - rainbows.conf
262
+ - test/database.yml
263
+ - test/test_faye-online.rb
264
+ homepage: https://github.com/eoecn/faye-online
265
+ licenses: []
266
+ post_install_message:
267
+ rdoc_options: []
268
+ require_paths:
269
+ - lib
270
+ required_ruby_version: !ruby/object:Gem::Requirement
271
+ none: false
272
+ requirements:
273
+ - - ! '>='
274
+ - !ruby/object:Gem::Version
275
+ version: '0'
276
+ required_rubygems_version: !ruby/object:Gem::Requirement
277
+ none: false
278
+ requirements:
279
+ - - ! '>='
280
+ - !ruby/object:Gem::Version
281
+ version: '0'
282
+ requirements: []
283
+ rubyforge_project:
284
+ rubygems_version: 1.8.25
285
+ signing_key:
286
+ specification_version: 3
287
+ summary: Faye Online user list and time count
288
+ test_files: []