cockatoo 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
+