cockatoo 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ Copyright (c) 2010 Yann Klis
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+
@@ -0,0 +1,11 @@
1
+ LICENSE
2
+ Manifest
3
+ README.rdoc
4
+ Rakefile
5
+ bin/cockatoo
6
+ cockatoo.gemspec
7
+ extra/_chat.html.haml
8
+ extra/_chat_initialization.html.haml
9
+ extra/chat.js
10
+ lib/cockatoo_daemon.rb
11
+ lib/cockatoo_http_server.rb
@@ -0,0 +1,51 @@
1
+ = Cockatoo
2
+
3
+ Cockatoo is a simple chat server. It's coded in Ruby with the help of the fantastic EventMachine.
4
+
5
+ There are several techniques to handle the real-time web, but we decided to use the Long Polling technique.
6
+ It works with plenty of browsers.
7
+
8
+ * homepage[http://github.com/novelys/cockatoo]
9
+
10
+ == Server part
11
+
12
+ The server part is coded in Ruby and EventMachine. The protocol is pretty straightforward.
13
+
14
+ == Client part
15
+
16
+ You can use any client part you want.
17
+ We provide 3 components so you can setup a simple chat system in any Ruby/Rails app you're working on :
18
+ * _chat_initialization[http://github.com/novelys/cockatoo/raw/master/extra/_chat_initialization.html.haml] HAML template
19
+ * _chat[http://github.com/novelys/cockatoo/raw/master/extra/_chat.html.haml] HAML template
20
+ * chat.js[http://github.com/novelys/cockatoo/raw/master/extra/chat.js] is the javascript client library (you will need jQuery[http://jquery.com])
21
+
22
+ Just include the 2 templates and the javascript lib in your layout and you'll be good to go.
23
+
24
+ Here are two screenshots of the client side in action :
25
+ * http://www.novelys.com/assets/Gallery/Lollidays/BigFrame5.png
26
+ * http://www.novelys.com/assets/Gallery/TheSphere/BigFrame3.png
27
+
28
+ == Installation
29
+
30
+ gem install cockatoo
31
+
32
+ == Usage
33
+
34
+ Beware that you'll need a "log" directory located where you will run cockatoo.
35
+
36
+ start the server (by default it will run on the 8000 port)
37
+ cockatoo start
38
+
39
+ start the server on the 9000 port
40
+ cockatoo start -- 9000
41
+
42
+ stop the server
43
+ cockatoo stop
44
+
45
+ restart the server
46
+ cockatoo restart
47
+
48
+ == Copyright
49
+
50
+ Copyright (c) 2010 Yann Klis. See LICENSE[http://github.com/novelys/cockatoo/raw/master/LICENSE] for details.
51
+
@@ -0,0 +1,12 @@
1
+ require 'echoe'
2
+ Echoe.new('cockatoo', '0.0.1') do |p|
3
+ p.summary = "Cockatoo is a simple chat server"
4
+ p.description = "Cockatoo is a simple chat server coded with EventMachine and using the Long Polling technique"
5
+ p.url = "http://github.com/novelys/cockatoo"
6
+ p.author = "Yann Klis"
7
+ p.email = "yann.klis @nospam@ novelys.com"
8
+ p.ignore_pattern = ["tmp/*", "log/*"]
9
+ p.development_dependencies = []
10
+ p.runtime_dependencies = ["daemons >=1.1.0", "log4r >=1.1.8", "eventmachine >=0.12.10", "yajl-ruby >=0.7.0", "htmlentities >=4.2.1"]
11
+ p.executable_pattern = ["bin/*"]
12
+ end
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env ruby
2
+ require 'daemons'
3
+
4
+ file = File.join(File.expand_path(__FILE__), '..', '..', 'lib', 'cockatoo_daemon.rb')
5
+ pwd = Dir.pwd
6
+ Daemons.run(file,
7
+ :app_name => "cockatoo",
8
+ :backtrace => true,
9
+ :log_output => true,
10
+ :dir_mode => :normal,
11
+ :dir => File.join(pwd, "log")
12
+ )
@@ -0,0 +1,47 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = %q{cockatoo}
5
+ s.version = "0.0.1"
6
+
7
+ s.required_rubygems_version = Gem::Requirement.new(">= 1.2") if s.respond_to? :required_rubygems_version=
8
+ s.authors = ["Yann Klis"]
9
+ s.date = %q{2010-10-27}
10
+ s.default_executable = %q{cockatoo}
11
+ s.description = %q{Cockatoo is a simple chat server coded with EventMachine and using the Long Polling technique}
12
+ s.email = %q{yann.klis @nospam@ novelys.com}
13
+ s.executables = ["cockatoo"]
14
+ s.extra_rdoc_files = ["LICENSE", "README.rdoc", "bin/cockatoo", "extra/_chat.html.haml", "extra/_chat_initialization.html.haml", "extra/chat.js", "lib/cockatoo_daemon.rb", "lib/cockatoo_http_server.rb"]
15
+ s.files = ["LICENSE", "Manifest", "README.rdoc", "Rakefile", "bin/cockatoo", "cockatoo.gemspec", "extra/_chat.html.haml", "extra/_chat_initialization.html.haml", "extra/chat.js", "lib/cockatoo_daemon.rb", "lib/cockatoo_http_server.rb"]
16
+ s.homepage = %q{http://github.com/novelys/cockatoo}
17
+ s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "Cockatoo", "--main", "README.rdoc"]
18
+ s.require_paths = ["lib"]
19
+ s.rubyforge_project = %q{cockatoo}
20
+ s.rubygems_version = %q{1.3.7}
21
+ s.summary = %q{Cockatoo is a simple chat server}
22
+
23
+ if s.respond_to? :specification_version then
24
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
25
+ s.specification_version = 3
26
+
27
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
28
+ s.add_runtime_dependency(%q<daemons>, [">= 1.1.0"])
29
+ s.add_runtime_dependency(%q<log4r>, [">= 1.1.8"])
30
+ s.add_runtime_dependency(%q<eventmachine>, [">= 0.12.10"])
31
+ s.add_runtime_dependency(%q<yajl-ruby>, [">= 0.7.0"])
32
+ s.add_runtime_dependency(%q<htmlentities>, [">= 4.2.1"])
33
+ else
34
+ s.add_dependency(%q<daemons>, [">= 1.1.0"])
35
+ s.add_dependency(%q<log4r>, [">= 1.1.8"])
36
+ s.add_dependency(%q<eventmachine>, [">= 0.12.10"])
37
+ s.add_dependency(%q<yajl-ruby>, [">= 0.7.0"])
38
+ s.add_dependency(%q<htmlentities>, [">= 4.2.1"])
39
+ end
40
+ else
41
+ s.add_dependency(%q<daemons>, [">= 1.1.0"])
42
+ s.add_dependency(%q<log4r>, [">= 1.1.8"])
43
+ s.add_dependency(%q<eventmachine>, [">= 0.12.10"])
44
+ s.add_dependency(%q<yajl-ruby>, [">= 0.7.0"])
45
+ s.add_dependency(%q<htmlentities>, [">= 4.2.1"])
46
+ end
47
+ end
@@ -0,0 +1,182 @@
1
+ #ChatApplication.ChatApplication.is-chat
2
+ .body.csb-body
3
+ .applets
4
+ .FriendsApplet.bar-container
5
+ .Bubble.decoration.TEMPLATE.hidden
6
+ .header.bbl-header
7
+ .title
8
+ %span.out-title
9
+ [TITLE]
10
+ %span.date.out-date
11
+ [DATE]
12
+ .do-close.action
13
+ x
14
+ .body.bbl-body
15
+ .message
16
+ .icon.left
17
+ %img.author.out-author.P{ :src => "/images/user-avatar-default.png", :align => "absmiddle", :height => "60", :width => "60" }
18
+ .text.out-text
19
+ [TEXT]
20
+ .clearer
21
+ \&nbsp;
22
+ .footer.bbl-footer
23
+ .operations
24
+ %button.do-ok.out-ok.action.T
25
+ reply
26
+ %span.hsep-5
27
+ %button.do-cancel.out-cancel.action.T
28
+ decline
29
+ .tip
30
+ \&nbsp;
31
+ .FriendsList.SlidingPane.hidden
32
+ .header.fl-header
33
+ .title.T
34
+ My friends
35
+ %img.do-collapse.action.P{ :src => "/images/icon-collapse.png", :align => "absmiddle", :height => "16", :width => "16" }
36
+ .clearer
37
+ \&nbsp;
38
+ .body.fl-body
39
+ %ul.friends
40
+ %li.when-empty.when-service_available
41
+ .message.when-empty
42
+ %span.text.T
43
+ No online friend
44
+ %li.when-not_service_available.hidden
45
+ .message
46
+ %span.text.T
47
+ No service
48
+ /%button.action.do-reconnect.T
49
+ / reconnect
50
+ .clearer
51
+ \&nbsp;
52
+ .clearer
53
+ \&nbsp;
54
+ .footer.TEMPLATES
55
+ %ul
56
+ %li.friend-element.when-service_available
57
+ .friend
58
+ .left
59
+ %img.icon.out-picture.P{ :src => "/images/user-avatar-default.png", :align => "absmiddle", :height => "30", :width => "30" }
60
+ .name.out-name
61
+ NAME
62
+ .status.out-status
63
+ STATUS
64
+ %span.when-not_in_discussion.hidden
65
+ %button.do-invite.action.T
66
+ invite
67
+ %span.when-in_discussion
68
+ %button.do-seeChat.action.T
69
+ chat
70
+ .clearer
71
+ \&nbsp;
72
+ .UserStatus.StatusBar
73
+ .body.us-body
74
+ .info
75
+ %img.P{ :src => "/images/icon-contact.png", :align => "absmiddle", :height => "16", :width => "16" }
76
+ %span
77
+ %span.hi
78
+ %span.out-onlineFriends
79
+ 0
80
+ %span.T.out-onlineFriendsLabel
81
+ friends
82
+ %span
83
+ %span.T
84
+ online
85
+ .toggle
86
+ %img.do-expand.action.when-not_expanded.P{ :src => "/images/icon-expand.png", :align => "absmiddle", :height => "16", :width => "16" }
87
+ %img.do-collapse.action.when-expanded.hidden.P{ :src => "/images/icon-collapse.png", :align => "absmiddle", :height => "16", :width => "16" }
88
+ .clearer
89
+ \&nbsp;
90
+ .footer.TEMPLATES
91
+ .DiscussionApplet.bar-container
92
+ .Bubble.decoration.TEMPLATE.hidden
93
+ .header.bbl-header
94
+ .title
95
+ %span.out-title
96
+ [TITLE]
97
+ %span.date.out-date
98
+ [DATE]
99
+ .do-close.action
100
+ x
101
+ .body.bbl-body
102
+ .message
103
+ .icon.left
104
+ %img.author.out-author.P{ :src => "/images/user-avatar-default.png", :align => "absmiddle", :height => "60", :width => "60" }
105
+ .text.out-text
106
+ [TEXT]
107
+ .clearer
108
+ \&nbsp;
109
+ .footer.bbl-footer
110
+ .operations
111
+ %button.do-ok.out-ok.action.T
112
+ reply
113
+ %span.hsep-5
114
+ %button.do-cancel.out-cancel.action.T
115
+ decline
116
+ .tip
117
+ \&nbsp;
118
+ .ChatLog.SlidingPane.decoration.hidden
119
+ .header.cl-header
120
+ .icon.left
121
+ %img.out-withPicture.P{ :src => "/images/user-avatar-default.png", :align => "absmiddle", :height => "45", :width => "45" }
122
+ .name.out-withName
123
+ NAME
124
+ .status.out-withStatus
125
+ STATUS
126
+ %img.do-collapse.action.P{ :src => "/images/icon-collapse.png", :align => "absmiddle", :height => "16", :width => "16" }
127
+ %img.do-close-discussion.action.P{ :src => "/images/icon-close.png", :align => "absmiddle", :height => "16", :width => "16" }
128
+ .clearer
129
+ \&nbsp;
130
+ .header.cl-header-2
131
+ .left
132
+ .discussionStatus
133
+ %span.out
134
+ // TODO : find something interesting to display here...
135
+ &nbsp;
136
+ .right
137
+ %span.on-state.when-initiated.when-accepted.when-active.when-inactive.hidden
138
+ %button.do-endDiscussion.action.T
139
+ end
140
+ %span.on-state.when-ended.when-cancelled.when-declined.hidden
141
+ %button.do-closeDiscussion.action.T
142
+ close
143
+ .clearer
144
+ \&nbsp;
145
+ .body.cl-body
146
+ %ul.messages.is-empty
147
+ %li.when-empty.hidden
148
+ .TEMPLATES
149
+ %ul
150
+ %li
151
+ .message
152
+ .header
153
+ .left.out.name
154
+ NAME
155
+ .right.out.date
156
+ HHhMM
157
+ .body.out.text
158
+ TEXT
159
+ .footer.cl-footer
160
+ .on-state.when-initiated.when-accepted.when-active.when-inactive
161
+ %input.in-message{ :type => "text" }
162
+ %button.do-writeMessage.action
163
+ %img.action.P{ :src => "/images/icon-bubble_and_shadow.png", :align => "absmiddle", :height => "16", :width => "16" }
164
+ .on-state.when-ended.when-cancelled.when-declined.hidden
165
+ %span.T
166
+ discussion ended
167
+ .ChatStatus.StatusBar
168
+ .body.us-body
169
+ .info
170
+ %img.P{ :src => "/images/icon-bubble.png", :align => "absmiddle", :height => "16", :width => "16" }
171
+ %span
172
+ %span.hi
173
+ %span.out-withName
174
+ NAME
175
+ \:
176
+ %span.out-messageCount
177
+ 0
178
+ %span.T
179
+ messages
180
+ .toggle.action
181
+ %img.do-expand.when-not_expanded.hidden.action.P{ :src => "/images/icon-expand.png", :align => "absmiddle", :height => "16", :width => "16" }
182
+ %img.do-collapse.when-expanded.action.P{ :src => "/images/icon-collapse.png", :align => "absmiddle", :height => "16", :width => "16" }
@@ -0,0 +1,38 @@
1
+ :javascript
2
+ var ui_chat_applet = null;
3
+ var ui_chat_applet_status = null;
4
+ var ui_chat_applet_sliding_pane = null;
5
+ var ui_chat_applet_expanded = null;
6
+ var ui_chat_applet_collapsed = null;
7
+ var ui_chat_applet_friends = null;
8
+ var ui_chat_applet_friend_template = null;
9
+
10
+ var ui_chat_applets = null;
11
+ var ui_chat_friends_applet = null;
12
+ var ui_chat_discussion_applet_template = null;
13
+
14
+ var ui_chat_messages = null;
15
+ var ui_chat_message_template = null;
16
+ var friends_json = null;
17
+ var chat_initialization = null;
18
+
19
+ $(document).ready(function() {
20
+ ui_chat_applet = $('#ChatApplication')
21
+ ui_chat_applet_status = $('.StatusBar', ui_chat_applet)
22
+ ui_chat_applet_sliding_pane = $('.SlidingPane', ui_chat_applet)
23
+ ui_chat_applet_expanded = $('.when-expanded', ui_chat_applet)
24
+ ui_chat_applet_collapsed = $('.when-not_expanded', ui_chat_applet)
25
+ ui_chat_applet_friends = $('.fl-body .friends', ui_chat_applet)
26
+ ui_chat_applet_friend_template = $('.TEMPLATES li.friend-element', ui_chat_applet)
27
+
28
+ ui_chat_applets = $('.csb-body > .applets', ui_chat_applet)
29
+ ui_chat_friends_applet = $('.csb-body .FriendsApplet', ui_chat_applet)
30
+ ui_chat_discussion_applet_template = $('.TEMPLATES .DiscussionApplet', ui_chat_applet)
31
+
32
+ ui_chat_messages = $('ul.messages')
33
+ ui_chat_message_template = $('.TEMPLATES ul li')
34
+
35
+ load_friends_json();
36
+
37
+ chat_init();
38
+ });
@@ -0,0 +1,376 @@
1
+ function getKeys(h) {
2
+ var keys = "";
3
+ for (var key in h)
4
+ keys += "," + key;
5
+ return keys.substr(1);
6
+ }
7
+
8
+ function load_friends_json() {
9
+ $.ajax({
10
+ cache: false,
11
+ type: "GET",
12
+ dataType: "json",
13
+ url: "/profile/friends.json",
14
+ async: false,
15
+ //error: chat_unavailable,
16
+ success: function(msg) {
17
+ friends_json = msg;
18
+ }
19
+ });
20
+ }
21
+
22
+ function chat_init() {
23
+ chat_initialization = true;
24
+ chat_available();
25
+ chat_register();
26
+
27
+ $('.header', ui_chat_applet).click(function(){
28
+ chat_applet_toggle();
29
+ return false;
30
+ });
31
+
32
+ $('.info', ui_chat_applet_status).click(function(){
33
+ chat_applet_toggle();
34
+ return false;
35
+ });
36
+ $(ui_chat_applet_collapsed).click(function() {
37
+ chat_applet_toggle();
38
+ return false;
39
+ });
40
+ $(ui_chat_applet_expanded).click(function() {
41
+ chat_applet_toggle();
42
+ return false;
43
+ });
44
+ // ui_chat_applet.click(function(){
45
+ // chat_applet_focus()
46
+ // });
47
+ }
48
+
49
+ function chat_applet_toggle() {
50
+ chat_all_discussions_collapse();
51
+
52
+ if ( ui_chat_applet_status.hasClass('is-expanded') ) {
53
+ chat_applet_collapse();
54
+ } else {
55
+ chat_applet_expand();
56
+ }
57
+ }
58
+
59
+ function chat_applet_collapse() {
60
+ ui_chat_applet_sliding_pane.addClass('hidden');
61
+ ui_chat_applet_collapsed.removeClass('hidden');
62
+ ui_chat_applet_expanded.addClass('hidden');
63
+ ui_chat_applet_status.removeClass('is-expanded');
64
+ chat_applet_unfocus();
65
+ }
66
+
67
+ function is_all_chat_discussions_collapsed() {
68
+ return $('.ChatLog.hidden').length == $('.ChatLog').length;
69
+ }
70
+
71
+ function chat_applet_expand() {
72
+ ui_chat_applet_expanded.removeClass('hidden');
73
+ ui_chat_applet_collapsed.addClass('hidden');
74
+ ui_chat_applet_sliding_pane.removeClass('hidden');
75
+ ui_chat_applet_status.addClass('is-expanded');
76
+ chat_applet_focus();
77
+ }
78
+
79
+ function chat_applet_focus(){
80
+ $('#ChatApplication .with-focus').removeClass('with-focus');
81
+ ui_chat_applet.addClass('with-focus');
82
+ }
83
+
84
+ function chat_applet_unfocus(){
85
+ ui_chat_applet.removeClass('with-focus');
86
+ }
87
+
88
+ function chat_discussion_toggle(nui) {
89
+ if ($('.toggle .do-collapse', nui).hasClass('hidden')) {
90
+ chat_discussion_expand(nui);
91
+ } else {
92
+ chat_discussion_collapse(nui);
93
+ }
94
+ }
95
+
96
+ function chat_all_discussions_collapse() {
97
+ $(".ChatLog").addClass('hidden');
98
+ $(".ChatStatus .toggle .do-collapse").addClass('hidden');
99
+ $(".ChatStatus .toggle .do-expand").removeClass('hidden');
100
+ }
101
+
102
+ function chat_discussion_expand(nui) {
103
+ chat_all_discussions_collapse();
104
+ $(".ChatLog", nui).removeClass('hidden');
105
+ $(".toggle .do-collapse", nui).removeClass('hidden');
106
+ $(".toggle .do-expand", nui).addClass('hidden');
107
+ chat_applet_collapse();
108
+ chat_scroll(nui);
109
+ focus_on_input(nui);
110
+ }
111
+
112
+ function focus_on_input(nui) {
113
+ $('.in-message', nui).focus();
114
+ }
115
+
116
+ function chat_discussion_collapse(nui) {
117
+ $(".ChatLog", nui).addClass('hidden');
118
+ $(".toggle .do-collapse", nui).addClass('hidden');
119
+ $(".toggle .do-expand", nui).removeClass('hidden');
120
+ chat_applet_collapse();
121
+ }
122
+
123
+ function chat_unavailable() {
124
+ $('.when-service_available', ui_chat_applet).addClass('hidden');
125
+ $('.DiscussionApplet', ui_chat_applets).addClass('hidden');
126
+ $('.when-not_service_available', ui_chat_applet).removeClass('hidden');
127
+ }
128
+
129
+ function chat_available() {
130
+ $('.when-service_available', ui_chat_applet).removeClass('hidden');
131
+ $('.DiscussionApplet', ui_chat_applets).removeClass('hidden');
132
+ $('.when-not_service_available', ui_chat_applet).addClass('hidden');
133
+ }
134
+
135
+ function chat_discussion_close(nui, fid) {
136
+ nui.remove();
137
+
138
+ $.ajax({
139
+ cache: false,
140
+ type: "GET",
141
+ dataType: "json",
142
+ url: "/chat/close_discussion",
143
+ error: chat_unavailable,
144
+ data: {uid: uid, did: fid}
145
+ });
146
+ }
147
+
148
+ function chat_register() {
149
+ url = "/chat/register";
150
+ $.ajax({
151
+ cache: false,
152
+ type: "GET",
153
+ dataType: "json",
154
+ url: url,
155
+ error: chat_unavailable,
156
+ success: chat_dispatch_messages,
157
+ data: {uid: uid, "friends_ids": getKeys(friends_json)}
158
+ });
159
+ }
160
+
161
+ function chat_poll() {
162
+ url = "/chat/poll";
163
+ $.ajax({
164
+ cache: false,
165
+ type: "GET",
166
+ dataType: "json",
167
+ url: url,
168
+ error: chat_unavailable,
169
+ success: chat_dispatch_messages,
170
+ data: {uid: uid}
171
+ });
172
+ }
173
+
174
+ function chat_say(fid, msg) {
175
+ url = "/chat/say";
176
+ $.ajax({
177
+ cache: false,
178
+ type: "GET",
179
+ dataType: "json",
180
+ url: url,
181
+ data: {sid: uid, did: fid, msg: msg}
182
+ });
183
+ }
184
+
185
+ function chat_leave() {
186
+ url = "/chat/leave";
187
+ $.ajax({
188
+ cache: false,
189
+ type: "GET",
190
+ dataType: "json",
191
+ url: url,
192
+ async: false,
193
+ data: {uid: uid}
194
+ });
195
+ }
196
+
197
+ function chat_dispatch_messages(messages) {
198
+ if (messages) {
199
+ for ( var i=0, len=messages.length; i<len; ++i ){
200
+ message = messages[i];
201
+ switch(message.type) {
202
+ case "status":
203
+ // user change his status : offline/online
204
+ chat_user_change_status(message);
205
+ break;
206
+ case "message":
207
+ // user send a message to another
208
+ chat_incoming_message(message);
209
+ break;
210
+ }
211
+ }
212
+ }
213
+ chat_initialization = false;
214
+
215
+ setTimeout(chat_poll, 0);
216
+ }
217
+
218
+ function chat_user_change_status(friend) {
219
+ not_existing = $('[fid='+friend.uid+']').length == 0;
220
+ if ( not_existing && (friend.state && (friend.state == 'ONLINE')) ) {
221
+ fui = ui_chat_applet_friend_template.clone();
222
+ fui.attr({'fid':('' + friend.uid), 'status':friend.state});
223
+ $('.out-name', fui).html(friends_json[friend.uid].login);
224
+ $('.out-status', fui).html(friend.state);
225
+ $('.out-picture', fui).attr({'title':friends_json[friend.uid].login, 'src':friends_json[friend.uid].avatar_url});
226
+ $('.do-seeChat', fui).click(function(){
227
+ chat_discuss_with_user(friend);
228
+ });
229
+ fui.removeClass('hidden');
230
+ ui_chat_applet_friends.append(fui);
231
+ chat_user_online(friend.uid);
232
+ }
233
+ if (friend.state && (friend.state == 'OFFLINE')) {
234
+ chat_user_offline(friend.uid);
235
+ }
236
+ chat_applet_update_counters();
237
+ }
238
+
239
+ function chat_user_offline(uid) {
240
+ $('[fid='+uid+']').remove();
241
+ nui = $('[discussion_with='+uid+']');
242
+ if (nui.length != 0) {
243
+ $('.out-withStatus', nui).html("OFFLINE");
244
+ $('.in-message', nui).attr("disabled", "disabled");
245
+ }
246
+ }
247
+
248
+ function chat_user_online(uid) {
249
+ nui = $('[discussion_with='+uid+']');
250
+ if (nui.length != 0) {
251
+ $('.out-withStatus', nui).html("ONLINE");
252
+ $('.in-message', nui).removeAttr("disabled");
253
+ }
254
+ }
255
+
256
+ function chat_scroll(nui) {
257
+ $('.cl-body', nui).scrollTo('100%');
258
+ }
259
+
260
+ function chat_incoming_message(message) {
261
+ var friend = null;
262
+ if (message.sid == uid) {
263
+ friend = {uid:message.did, state:"ONLINE"};
264
+ message_login = user_name;
265
+ } else {
266
+ friend = {uid:message.sid, state:"ONLINE"};
267
+ message_login = friends_json[friend.uid].login;
268
+ }
269
+ var nui = $('[discussion_with='+friend.uid+']');
270
+ if ( nui.length == 0 ) {
271
+ chat_discuss_with_user(friend);
272
+ nui = $('[discussion_with='+friend.uid+']');
273
+ }
274
+
275
+ var chat_log = $('.ChatLog', nui);
276
+
277
+ if (chat_initialization) {
278
+ chat_discussion_collapse(nui);
279
+ } else if (is_all_chat_discussions_collapsed()) {
280
+ chat_discussion_expand(nui);
281
+ }
282
+
283
+ var messages = $('ul.messages', chat_log);
284
+ chat_discussion_message_counter(nui, messages);
285
+
286
+ var message_template = $('.TEMPLATES ul li', chat_log).clone();
287
+ var date=new Date(message.date);
288
+ var m=date.getMinutes();
289
+ var h=date.getHours();
290
+ $('.out.name', message_template).html(message_login);
291
+ $('.out.text', message_template).html(message.value);
292
+ $('.out.date', message_template).html(sprintf('%02dh%02d', h, m));
293
+ if (message.sid == uid) {
294
+ message_template.addClass('inbound');
295
+ } else {
296
+ message_template.addClass('outbound');
297
+ }
298
+ messages.append(message_template).removeClass('is-empty');
299
+
300
+ chat_scroll(nui);
301
+
302
+ // Play a sound...
303
+ if (!chat_initialization && message.sid != uid) {
304
+ chatAlarmSound.play();
305
+ }
306
+ }
307
+
308
+ function chat_discuss_with_user(friend) {
309
+ var nui = $('[discussion_with='+friend.uid+']');
310
+ if ( nui.length == 0 ) {
311
+ var nui = ui_chat_discussion_applet_template.clone();
312
+ nui.attr({'discussion_with':friend.uid});
313
+ $('.out-withName', nui).html(friends_json[friend.uid].login);
314
+ $('.out-withStatus', nui).html(friend.state);
315
+ $('.out-withPicture', nui).attr({'title':friends_json[friend.uid].login, 'src':friends_json[friend.uid].avatar_url});
316
+
317
+
318
+ $('.ChatStatus.StatusBar', nui).click(function(){
319
+ chat_discussion_toggle(nui);
320
+ return false;
321
+ });
322
+
323
+ $('.ChatLog .header .do-collapse', nui).click(function() {
324
+ chat_discussion_toggle(nui);
325
+ return false;
326
+ });
327
+
328
+ $('.ChatLog .header .do-close-discussion', nui).click(function() {
329
+ chat_discussion_close(nui, friend.uid);
330
+ return false;
331
+ });
332
+
333
+ $(".in-message", nui).keypress(function (e) {
334
+ if (e.keyCode != 13 /* Return */) return;
335
+ chat_send_message(nui, friend.uid);
336
+ });
337
+
338
+ $(".do-writeMessage", nui).click(function() {
339
+ chat_send_message(nui, friend.uid);
340
+ });
341
+
342
+ ui_chat_applets.append(nui);
343
+ chat_applet_collapse();
344
+ focus_on_input(nui);
345
+ } else {
346
+ chat_discussion_expand(nui);
347
+ }
348
+ }
349
+
350
+ function chat_send_message(nui, friend_uid) {
351
+ /*var msg = $("#entry").attr("value").replace("\n", "");*/
352
+ var msg = $(".in-message", nui).attr("value");
353
+ if (msg != "") {
354
+ chat_say(friend_uid, msg);
355
+ $(".in-message", nui).attr("value", "");
356
+ }
357
+ }
358
+
359
+ function chat_discussion_message_counter(nui, messages) {
360
+ nbr = $('li', messages).length;
361
+ $('.out-messageCount', nui).html(nbr);
362
+ }
363
+
364
+ function chat_applet_update_counters() {
365
+ nbr = $('li.friend-element[STATUS=ONLINE]', ui_chat_applet_friends).length;
366
+ $('.out-onlineFriends', ui_chat_applet).html(nbr);
367
+ if (nbr > 0) {
368
+ $('.when-empty', ui_chat_applet_friends).addClass('hidden');
369
+ } else {
370
+ $('.when-empty', ui_chat_applet_friends).removeClass('hidden');
371
+ }
372
+ }
373
+
374
+ $(window).unload(function () {
375
+ chat_leave();
376
+ });
@@ -0,0 +1,26 @@
1
+ require File.join(File.expand_path(File.dirname(__FILE__) + "/cockatoo_http_server"))
2
+ require 'log4r'
3
+ include Log4r
4
+
5
+ $logger = Logger.new 'cockatoo'
6
+ $logger.outputters = Outputter.stdout
7
+
8
+
9
+ loop do
10
+ begin
11
+ GC.start
12
+ $chat_users = {}
13
+ $timers = {}
14
+ EM.epoll if EM.epoll?
15
+ EM.run do
16
+ $logger.info "#{Time.now} Starting server on port #{ARGV.first || 8000}"
17
+ EM.start_server '0.0.0.0', ARGV.first || 8000, ChatHttpServer
18
+ end
19
+ rescue Interrupt
20
+ $logger.info "#{Time.now} Shuting down..."
21
+ exit
22
+ rescue
23
+ $logger.error "#{Time.now} Error: " + $!.message
24
+ $logger.error "\t" + $!.backtrace.join("\n\t")
25
+ end
26
+ end
@@ -0,0 +1,205 @@
1
+ require 'eventmachine'
2
+ require 'cgi'
3
+ require 'yajl'
4
+ require "htmlentities"
5
+
6
+ class Object
7
+ def blank?
8
+ respond_to?(:empty?) ? empty? : !self
9
+ end
10
+
11
+ def present?
12
+ !blank?
13
+ end
14
+ end
15
+
16
+ class ChatUser
17
+ attr_accessor :uid, :discussion, :friends_ids
18
+ def self.new_from_query(query)
19
+ return nil unless query.present? && query['uid'].present?
20
+ new(:uid => query['uid'].first.to_i)
21
+ end
22
+
23
+ def initialize(attributes)
24
+ @uid = attributes[:uid]
25
+ @friends_ids = []
26
+ end
27
+
28
+ def log_with_user(other_chat_user)
29
+ self.discussion.present? ? self.discussion.log.select { |message| message["type"] == "message" && (message["sid"] == other_chat_user.uid || message["did"] == other_chat_user.uid) } : []
30
+ end
31
+
32
+ def clear_discussion(did)
33
+ if self.discussion.present?
34
+ self.discussion.log.delete_if { |message| message["sid"] == did || message["did"] == did }
35
+ end
36
+ end
37
+ end
38
+
39
+ class Discussion < EM::Channel
40
+ attr_accessor :log
41
+
42
+ def initialize
43
+ @log = []
44
+ super()
45
+ end
46
+
47
+ def say(ary)
48
+ ary = [ ary ].flatten
49
+ @log = (@log + ary)[0, 500]
50
+ json = Yajl::Encoder.encode(ary)
51
+ push json
52
+ end
53
+ end
54
+
55
+ module ChatHttpServer
56
+
57
+ def unbind
58
+ $chat_users[@user_id].discussion.unsubscribe(@sid) if @user_id && @sid && $chat_users[@user_id] && $chat_users[@user_id].discussion
59
+ end
60
+
61
+ def receive_data(data)
62
+ # puts "received data : #{data}"
63
+ lines = data.split(/[\r\n]+/)
64
+ method, request, version = lines.shift.split(' ', 3)
65
+ if request.nil?
66
+ $logger.error "#{Time.now} Warning: strange request #{[method, request, version].inspect}"
67
+ close_connection
68
+ return
69
+ else
70
+ path, query = request.split('?', 2)
71
+ $logger.info "#{Time.now} request on #{path} with #{query}"
72
+ query = CGI.parse(query) if not query.nil?
73
+ cookies = {}
74
+ lines.each do |line|
75
+ if line[0..6].downcase == 'cookie:'
76
+ cookies = CGI::Cookie.parse(line.split(':', 2).last.strip)
77
+ end
78
+ end
79
+ end
80
+
81
+ if query.present? && query['uid'].present?
82
+ uid = query['uid'].first.to_i
83
+ chat_user = ($chat_users[uid] ||= ChatUser.new_from_query(query))
84
+ end
85
+
86
+ case path
87
+
88
+ when '/chat/register'
89
+ if chat_user && query['friends_ids'].present? && query['friends_ids'].first.present?
90
+ remove_all_timers_for(chat_user)
91
+
92
+ chat_user.friends_ids = query["friends_ids"].first.split(',').map { |id| id.to_i }
93
+
94
+ message = { "type" => "status", "state" => "ONLINE", "uid" => chat_user.uid }
95
+ back_messages = []
96
+ chat_user.friends_ids.each do |fid|
97
+ chat_user_friend = $chat_users[fid]
98
+ if chat_user_friend && chat_user_friend.discussion
99
+ chat_user_friend.discussion.say(message)
100
+ back_messages << {"type" => "status", "state" => "ONLINE", "uid" => fid}
101
+
102
+ back_messages += chat_user.discussion ? chat_user.log_with_user(chat_user_friend) : chat_user_friend.log_with_user(chat_user)
103
+ end
104
+ end
105
+ respond Yajl::Encoder.encode(back_messages)
106
+ else
107
+ respond "no user id or no friends ids", 500
108
+ end
109
+
110
+ when '/chat/leave'
111
+ if chat_user
112
+ empty_response
113
+ unregister_user_after(chat_user, 15)
114
+ else
115
+ respond "no user id", 500
116
+ end
117
+
118
+ when '/chat/close_discussion'
119
+ empty_response
120
+
121
+ if chat_user && query['did'].present?
122
+ did = query['did'].first.to_i
123
+ $logger.info "User #{chat_user.uid} clears discussion for user #{did}"
124
+ chat_user.clear_discussion(did)
125
+ end
126
+
127
+ when '/chat/poll'
128
+ if chat_user
129
+ unregister_user_after(chat_user, 120)
130
+ timer = EventMachine::Timer.new(30) do
131
+ empty_response
132
+ end
133
+ @user_id = chat_user.uid
134
+ chat_user.discussion ||= Discussion.new
135
+ @sid = chat_user.discussion.subscribe do |code|
136
+ respond code
137
+ end
138
+ else
139
+ respond "no user id", 500
140
+ end
141
+
142
+ when '/chat/say'
143
+ if query['sid'].present? and query['msg'].present? and query['did'].present?
144
+ empty_response
145
+ sid = query['sid'].first.to_i
146
+ did = query['did'].first.to_i
147
+ msg = query['msg'].first
148
+
149
+ chat_user_sender = $chat_users[sid]
150
+ chat_user_recipient = $chat_users[did]
151
+ message = {"type" => "message", "value" => HTMLEntities.new.encode(msg.gsub('"', '\"')), "sid" => sid, "did" => did, "date" => (Time.now.to_i * 1000)}
152
+
153
+ if chat_user_sender && chat_user_recipient
154
+ chat_user_sender.discussion.say(message)
155
+ chat_user_recipient.discussion.say(message)
156
+ else
157
+ $logger.error "#{Time.now} ERROR : message could not be transmitted between sid #{sid} and did #{did}!"
158
+ end
159
+ else
160
+ respond "USER and MSG parameters are mandatory", 500
161
+ end
162
+
163
+ else
164
+ respond "not found", 404
165
+ end
166
+ end
167
+
168
+ RESPONSE = [
169
+ "HTTP/1.1 %d Cockatoo Alpha 1",
170
+ "Content-length: %d",
171
+ "Content-type: %s",
172
+ "Connection: close",
173
+ "",
174
+ "%s"].join("\r\n")
175
+
176
+ def empty_response
177
+ respond Yajl::Encoder.encode([])
178
+ end
179
+
180
+ def respond(body, status = 200, content_type = 'application/json; charset=utf-8')
181
+ send_data RESPONSE % [status, body.length, content_type, body]
182
+ close_connection_after_writing
183
+ end
184
+
185
+ def unregister_user_after(chat_user, seconds)
186
+ remove_all_timers_for(chat_user)
187
+ $logger.info "INFO : new timer for unregister user #{chat_user.uid} in #{seconds} sec."
188
+ $timers[chat_user.uid] = EventMachine::Timer.new(seconds) do
189
+ $logger.info "INFO : execute timer for user #{chat_user.uid}"
190
+ message = { "type" => "status", "state" => "OFFLINE", "uid" => chat_user.uid }
191
+ chat_user.friends_ids.each{|fid|
192
+ $chat_users[fid].discussion.say(message) if $chat_users[fid] && $chat_users[fid].discussion
193
+ }
194
+ $chat_users.delete(chat_user.uid)
195
+ end
196
+ end
197
+
198
+ def remove_all_timers_for(chat_user)
199
+ if $timers[chat_user.uid]
200
+ $logger.info "INFO : Removes timer for user #{chat_user.uid}"
201
+ $timers[chat_user.uid].cancel
202
+ $timers.delete(chat_user.uid)
203
+ end
204
+ end
205
+ end
metadata ADDED
@@ -0,0 +1,169 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cockatoo
3
+ version: !ruby/object:Gem::Version
4
+ hash: 29
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 1
10
+ version: 0.0.1
11
+ platform: ruby
12
+ authors:
13
+ - Yann Klis
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2010-10-27 00:00:00 +02:00
19
+ default_executable: cockatoo
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: daemons
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 19
30
+ segments:
31
+ - 1
32
+ - 1
33
+ - 0
34
+ version: 1.1.0
35
+ type: :runtime
36
+ version_requirements: *id001
37
+ - !ruby/object:Gem::Dependency
38
+ name: log4r
39
+ prerelease: false
40
+ requirement: &id002 !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ hash: 3
46
+ segments:
47
+ - 1
48
+ - 1
49
+ - 8
50
+ version: 1.1.8
51
+ type: :runtime
52
+ version_requirements: *id002
53
+ - !ruby/object:Gem::Dependency
54
+ name: eventmachine
55
+ prerelease: false
56
+ requirement: &id003 !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ hash: 59
62
+ segments:
63
+ - 0
64
+ - 12
65
+ - 10
66
+ version: 0.12.10
67
+ type: :runtime
68
+ version_requirements: *id003
69
+ - !ruby/object:Gem::Dependency
70
+ name: yajl-ruby
71
+ prerelease: false
72
+ requirement: &id004 !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ hash: 3
78
+ segments:
79
+ - 0
80
+ - 7
81
+ - 0
82
+ version: 0.7.0
83
+ type: :runtime
84
+ version_requirements: *id004
85
+ - !ruby/object:Gem::Dependency
86
+ name: htmlentities
87
+ prerelease: false
88
+ requirement: &id005 !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ">="
92
+ - !ruby/object:Gem::Version
93
+ hash: 53
94
+ segments:
95
+ - 4
96
+ - 2
97
+ - 1
98
+ version: 4.2.1
99
+ type: :runtime
100
+ version_requirements: *id005
101
+ description: Cockatoo is a simple chat server coded with EventMachine and using the Long Polling technique
102
+ email: yann.klis @nospam@ novelys.com
103
+ executables:
104
+ - cockatoo
105
+ extensions: []
106
+
107
+ extra_rdoc_files:
108
+ - LICENSE
109
+ - README.rdoc
110
+ - bin/cockatoo
111
+ - extra/_chat.html.haml
112
+ - extra/_chat_initialization.html.haml
113
+ - extra/chat.js
114
+ - lib/cockatoo_daemon.rb
115
+ - lib/cockatoo_http_server.rb
116
+ files:
117
+ - LICENSE
118
+ - Manifest
119
+ - README.rdoc
120
+ - Rakefile
121
+ - bin/cockatoo
122
+ - cockatoo.gemspec
123
+ - extra/_chat.html.haml
124
+ - extra/_chat_initialization.html.haml
125
+ - extra/chat.js
126
+ - lib/cockatoo_daemon.rb
127
+ - lib/cockatoo_http_server.rb
128
+ has_rdoc: true
129
+ homepage: http://github.com/novelys/cockatoo
130
+ licenses: []
131
+
132
+ post_install_message:
133
+ rdoc_options:
134
+ - --line-numbers
135
+ - --inline-source
136
+ - --title
137
+ - Cockatoo
138
+ - --main
139
+ - README.rdoc
140
+ require_paths:
141
+ - lib
142
+ required_ruby_version: !ruby/object:Gem::Requirement
143
+ none: false
144
+ requirements:
145
+ - - ">="
146
+ - !ruby/object:Gem::Version
147
+ hash: 3
148
+ segments:
149
+ - 0
150
+ version: "0"
151
+ required_rubygems_version: !ruby/object:Gem::Requirement
152
+ none: false
153
+ requirements:
154
+ - - ">="
155
+ - !ruby/object:Gem::Version
156
+ hash: 11
157
+ segments:
158
+ - 1
159
+ - 2
160
+ version: "1.2"
161
+ requirements: []
162
+
163
+ rubyforge_project: cockatoo
164
+ rubygems_version: 1.3.7
165
+ signing_key:
166
+ specification_version: 3
167
+ summary: Cockatoo is a simple chat server
168
+ test_files: []
169
+