ttapi 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,49 @@
1
+ # rcov generated
2
+ coverage
3
+ coverage.data
4
+
5
+ # rdoc generated
6
+ rdoc
7
+
8
+ # yard generated
9
+ doc
10
+ .yardoc
11
+
12
+ # bundler
13
+ .bundle
14
+
15
+ # jeweler generated
16
+ pkg
17
+
18
+ # Have editor/IDE/OS specific files you need to ignore? Consider using a global gitignore:
19
+ #
20
+ # * Create a file at ~/.gitignore
21
+ # * Include files you want ignored
22
+ # * Run: git config --global core.excludesfile ~/.gitignore
23
+ #
24
+ # After doing this, these files will be ignored in all your git projects,
25
+ # saving you from having to 'pollute' every project you touch with them
26
+ #
27
+ # Not sure what to needs to be ignored for particular editors/OSes? Here's some ideas to get you started. (Remember, remove the leading # of the line)
28
+ #
29
+ # For MacOS:
30
+ #
31
+ #.DS_Store
32
+
33
+ # For TextMate
34
+ #*.tmproj
35
+ #tmtags
36
+
37
+ # For emacs:
38
+ #*~
39
+ #\#*
40
+ #.\#*
41
+
42
+ # For vim:
43
+ #*.swp
44
+
45
+ # For redcar:
46
+ #.redcar
47
+
48
+ # For rubinius:
49
+ #*.rbc
data/Gemfile CHANGED
@@ -1,7 +1,7 @@
1
1
  source "http://rubygems.org"
2
2
  # Add dependencies required to use your gem here.
3
3
  # Example:
4
- # gem "activesupport", ">= 2.3.5"
4
+ gem "json", ">= 0.0.0"
5
5
 
6
6
  # Add dependencies to develop your gem here.
7
7
  # Include everything needed to run rake, tests, features, etc.
File without changes
data/Rakefile CHANGED
@@ -15,7 +15,7 @@ require 'jeweler'
15
15
  Jeweler::Tasks.new do |gem|
16
16
  # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
17
17
  gem.name = "ttapi"
18
- gem.homepage = "http://github.com/alaingilbert/ttapi"
18
+ gem.homepage = "http://github.com/alaingilbert/Turntable-API"
19
19
  gem.license = "MIT"
20
20
  gem.summary = %Q{Turntable-API}
21
21
  gem.description = %Q{A simple ruby wrapper for the turntable API}
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.1
1
+ 0.0.2
@@ -0,0 +1,16 @@
1
+ require "ttapi"
2
+
3
+ $b = Bot.new("auth+live+XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", "XXXXXXXXXXXXXXXXXXXXXXXX", "XXXXXXXXXXXXXXXXXXXXXXXX")
4
+
5
+ def speak(data)
6
+ name = data["name"]
7
+ text = data["text"]
8
+
9
+ if /\/hello/.match(text)
10
+ $b.speak("Hello %s" % name)
11
+ end
12
+ end
13
+
14
+ $b.on("speak", method(:speak))
15
+
16
+ $b.start
data/lib/bot.rb ADDED
@@ -0,0 +1,459 @@
1
+ require "websocket"
2
+ require "set"
3
+ require "uri"
4
+ require "net/http"
5
+ require "rubygems"
6
+ require "json"
7
+
8
+ class Bot
9
+ attr_accessor :debug, :speak
10
+
11
+ def initialize(auth, userId, roomId)
12
+ @auth = auth
13
+ @userId = userId
14
+ @roomId = roomId
15
+ @debug = false
16
+ @callback = nil
17
+ @currentDjId = nil
18
+ @currentSongId = nil
19
+ @lastHeartbeat = Time.now
20
+ @lastActivity = Time.now
21
+ @clientId = '%s-0.59633534294921572' % Time.now.to_i
22
+ @_msgId = 0
23
+ @_cmds = []
24
+ @_isConnected = false
25
+ @fanOf = Set.new
26
+ @currentStatus = "available"
27
+ @signals = {}
28
+
29
+ connect(@roomId)
30
+ end
31
+
32
+
33
+ def connect(roomId)
34
+ uri = URI.parse("http://turntable.fm:80/api/room.which_chatserver?roomid=%s" % @roomId)
35
+ response = Net::HTTP.get_response(uri)
36
+ data = JSON.parse(response.body)
37
+ host, port = data[1]["chatserver"][0], data[1]["chatserver"][1]
38
+ url = "ws://%s:%s/socket.io/websocket" % [host, port]
39
+ @ws = WebSocket.new(url)
40
+ if @roomId
41
+ def clb
42
+ rq = { "api" => "room.register", "roomid" => @roomId }
43
+ _send(rq, nil)
44
+ end
45
+ @callback = method(:clb)
46
+ end
47
+ end
48
+
49
+
50
+ def setTmpSong(data)
51
+ tmpSong = { "command" => "endsong", "room" => data["room"], "success" => true }
52
+ end
53
+
54
+
55
+ def on_message(msg)
56
+ heartbeat_rgx = /~m~[0-9]+~m~(~h~[0-9]+)/
57
+ if heartbeat_rgx.match(msg)
58
+ _heartbeat(heartbeat_rgx.match(msg)[1])
59
+ @lastHeartbeat = Time.now
60
+ updatePresence()
61
+ return
62
+ end
63
+
64
+ if @debug
65
+ puts "> %s" % msg
66
+ end
67
+
68
+ if msg == "~m~10~m~no_session"
69
+ def clb(obj)
70
+ if not @isConnected
71
+ def fanof(data)
72
+ @fanOf |= Set.new(data["fanof"])
73
+ updatePresence()
74
+ # TODO: setInterval ????
75
+ emit("ready")
76
+ end
77
+ getFanOf(method(:fanof))
78
+ end
79
+ @callback.call()
80
+ @isConnected = true
81
+ end
82
+ userAuthenticate(method(:clb))
83
+ return
84
+ end
85
+
86
+ @lastActivity = Time.now
87
+ len_rgx = /~m~([0-9]+)~m~/
88
+ len = len_rgx.match(msg)[1]
89
+ obj = JSON.parse(msg[msg.index("{"), msg.length])
90
+ for id, rq, clb in @_cmds
91
+ if id == obj["msgid"]
92
+ if rq["api"] == "room.info"
93
+ if obj["success"]
94
+ currentDj = obj["room"]["metadata"]["current_dj"]
95
+ currentSong = obj["room"]["metadata"]["current_song"]
96
+ if currentDj
97
+ @currentDj = currentDj
98
+ end
99
+ if currentSong
100
+ @currentSongId = currentSong["_id"]
101
+ end
102
+ end
103
+
104
+ elsif rq["api"] == "room.register"
105
+ if obj["success"]
106
+ @roomId = rq["roomid"]
107
+ def info_clb(data)
108
+ setTmpSong(data)
109
+ emit("roomChanged", data)
110
+ end
111
+ roomInfo(method(:info_clb))
112
+ else
113
+ emit("roomChanged", obj)
114
+ end
115
+ clb = nil
116
+
117
+ elsif rq["api"] == "room.deregister"
118
+ if obj["success"]
119
+ @roomId = nil
120
+ end
121
+ end
122
+
123
+ if clb
124
+ clb.call(obj)
125
+ end
126
+
127
+ @_cmds.delete([id, rq, clb])
128
+ break
129
+ end
130
+ end
131
+
132
+ if obj["command"] == "registered"
133
+ emit("registered", obj)
134
+ elsif obj["command"] == "deregistered"
135
+ emit("deregistered", obj)
136
+ elsif obj["command"] == "speak"
137
+ emit("speak", obj)
138
+ elsif obj["command"] == "pmmed"
139
+ emit("pmmed", obj)
140
+ elsif obj["command"] == "nosong"
141
+ @currentDjId = nil
142
+ @currentSongId = nil
143
+ emit("endsong", @tmpSong)
144
+ emit("nosong", obj)
145
+ elsif obj["command"] == "newsong"
146
+ if @currentSongId
147
+ emit("endsong", @tmpSong)
148
+ end
149
+ @currentDjId = obj["room"]["metadata"]["current_dj"]
150
+ @currentSongId = obj["room"]["metadata"]["current_song"]["_id"]
151
+ setTmpSong(obj)
152
+ emit("newsong", obj)
153
+ elsif obj["command"] == "update_votes"
154
+ if @tmpSong
155
+ @tmpSong["room"]['metadata']['upvotes'] = obj['room']['metadata']['upvotes']
156
+ @tmpSong['room']['metadata']['downvotes'] = obj['room']['metadata']['downvotes']
157
+ @tmpSong['room']['metadata']['listeners'] = obj['room']['metadata']['listeners']
158
+ end
159
+ emit("update_votes", obj)
160
+ elsif obj["command"] == "booted_user"
161
+ emit('booted_user', obj)
162
+ elsif obj["command"] == "update_user"
163
+ emit('update_user', obj)
164
+ elsif obj["command"] == "add_dj"
165
+ emit('add_dj', obj)
166
+ elsif obj["command"] == "rem_dj"
167
+ emit('rem_dj', obj)
168
+ elsif obj["command"] == "new_moderator"
169
+ emit('new_moderator', obj)
170
+ elsif obj["command"] == "rem_moderator"
171
+ emit('rem_moderator', obj)
172
+ elsif obj["command"] == "snagged"
173
+ emit('snagged', obj)
174
+ end
175
+ end
176
+
177
+
178
+ def _heartbeat(msg)
179
+ @ws.send('~m~%s~m~%s' % [msg.length, msg])
180
+ @_msgId += 1
181
+ end
182
+
183
+
184
+ def _send(rq, callback=nil)
185
+ rq["msgid"] = @_msgId
186
+ rq["clientid"] = @clientId
187
+ if not rq["userid"]
188
+ rq["userid"] = @userId
189
+ end
190
+ rq["userauth"] = @auth
191
+
192
+ msg = JSON.generate(rq)
193
+
194
+ if @debug
195
+ puts "< %s" % msg
196
+ end
197
+
198
+ @ws.send('~m~%s~m~%s' % [msg.length, msg])
199
+ @_cmds.push([@_msgId, rq, callback])
200
+ @_msgId += 1
201
+ end
202
+
203
+
204
+ def roomNow(callback=nil)
205
+ rq = { "api" => "room.now" }
206
+ _send(rq, callback)
207
+ end
208
+
209
+
210
+ def updatePresence(callback=nil)
211
+ rq = { "api": "presence.update", "status": @currentStatus }
212
+ _send(rq, callback)
213
+ end
214
+
215
+
216
+ def listRooms(skip=nil, callback=nil)
217
+ if not skip
218
+ skip = 0
219
+ end
220
+ rq = { "api" => "room.list_rooms", "skip" => skip }
221
+ _sned(rq, callback)
222
+ end
223
+
224
+
225
+ def directoryGraph(callback=nil)
226
+ rq = { "api" => "room.directory_graph" }
227
+ _send(rq, callback)
228
+ end
229
+
230
+
231
+ # TODO
232
+ def stalk(*args)
233
+ end
234
+
235
+
236
+ def getFavorites(callback=nil)
237
+ rq = { "api" => "room.get_favorites" }
238
+ _send(rq, callback)
239
+ end
240
+
241
+
242
+ def addFavorite(roomId, callback=nil)
243
+ rq = { "api" => "room.add_favorite", "roomid" => roomId }
244
+ _send(rq, callback)
245
+ end
246
+
247
+
248
+ def remFavorite(roomId, callback=nil)
249
+ rq = { "api" => "room.rem_favorite", "roomid" => roomId }
250
+ _send(rq, callback)
251
+ end
252
+
253
+
254
+ # TODO
255
+ def roomRegister(callback=nil)
256
+ end
257
+
258
+
259
+ def roomDeregister(callback=nil)
260
+ rq = { "api" => "room.deregister", "roomid" => @roomId }
261
+ _send(rq, callback)
262
+ end
263
+
264
+
265
+ def roomInfo(*args)
266
+ rq = { "api" => "room.info", "roomid" => @roomId }
267
+ callback = args[0]
268
+ _send(rq, callback)
269
+ end
270
+
271
+
272
+ def speak(msg, callback=nil)
273
+ rq = { "api" => "room.speak", "roomid" => @roomId, "text" => msg.to_s }
274
+ _send(rq, callback)
275
+ end
276
+
277
+
278
+ def pm(msg, userid, callback=nil)
279
+ rq = { "api" => "pm.send", "receiverid" => userid, "text" => msg.to_s }
280
+ _send(rq, callback)
281
+ end
282
+
283
+
284
+ def pmHistory(userid, callback=nil)
285
+ rq = { "api" => "pm.history", "receiverid" => userid }
286
+ _send(rq, callback)
287
+ end
288
+
289
+
290
+ def bootUser(userId, reason="", callback=nil)
291
+ rq = { "api" => "room.boot_user", "roomid" => @roomId, "target_userid" => userId, "reason" => reason }
292
+ _send(rq, callback)
293
+ end
294
+
295
+
296
+ def boot(userId, reason="", callback=nil)
297
+ bootUser(userId, reason, callback)
298
+ end
299
+
300
+
301
+ def addModerator(userId, callback=nil)
302
+ rq = { "api" => "room.add_moderator", "roomid" => @roomId, "target_userid" => userId }
303
+ _send(rq, callback)
304
+ end
305
+
306
+
307
+ def remModerator(userId, callback=nil)
308
+ rq = { "api" => "room.rem_moderator", "roomid" => @roomId, "target_userid" => userId }
309
+ _send(rq, callback)
310
+ end
311
+
312
+
313
+ def addDj(callback=nil)
314
+ rq = { "api" => "room.add_dj", "roomid" => @roomId }
315
+ _send(rq, callback)
316
+ end
317
+
318
+
319
+ # TODO
320
+ def remDj(*args)
321
+ end
322
+
323
+
324
+ def stopSong(callback=nil)
325
+ rq = { "api" => "room.stop_song", "roomid" => @roomId }
326
+ _send(rq, callback)
327
+ end
328
+
329
+
330
+ def skip(callback=nil)
331
+ stopSong(callback)
332
+ end
333
+
334
+
335
+ # TODO
336
+ def snag(callback=nil)
337
+ end
338
+
339
+
340
+ # TODO
341
+ def vote
342
+ end
343
+
344
+
345
+ # TODO
346
+ def bop
347
+ end
348
+
349
+
350
+ def userAuthenticate(callback)
351
+ rq = { "api" => "user.authenticate" }
352
+ _send(rq, callback)
353
+ end
354
+
355
+
356
+ def userInfo(callback=nil)
357
+ rq = { "api" => "user.info" }
358
+ _send(rq, callback)
359
+ end
360
+
361
+
362
+ def getFanOf(callback=nil)
363
+ rq = { "api" => "user.get_fan_of" }
364
+ _send(rq, callback)
365
+ end
366
+
367
+
368
+ # TODO
369
+ def getProfile
370
+ end
371
+
372
+
373
+ # TODO
374
+ def modifyProfile
375
+ end
376
+
377
+
378
+ def modifyLaptop(laptop="linux", callback=nil)
379
+ rq = { "api" => "user.modify", "laptop" => laptop }
380
+ _send(rq, callback)
381
+ end
382
+
383
+
384
+ def modifyName(name, callback=nil)
385
+ rq = { "api" => "user.modify", "name" => name }
386
+ _send(rq, callback)
387
+ end
388
+
389
+
390
+ def setAvatar(avatarId, callback=nil)
391
+ rq = { "api" => "user.set_avatar", "avatarid" => avatarId }
392
+ _send(rq, callback)
393
+ end
394
+
395
+
396
+ def becomeFan(userId, callback=nil)
397
+ rq = { "api" => "user.become_fan", "djid" => userId }
398
+ _send(rq, callback)
399
+ end
400
+
401
+
402
+ def removeFan(userId, callback=nil)
403
+ rq = { "api" => "user.remove_fan", "djid" => userId }
404
+ _send(rq, callback)
405
+ end
406
+
407
+
408
+ # TODO
409
+ def playlistAll
410
+ end
411
+
412
+
413
+ # TODO
414
+ def playlistAdd
415
+ end
416
+
417
+
418
+ # TODO
419
+ def playlistRemove
420
+ end
421
+
422
+
423
+ # TODO
424
+ def playlistReorder
425
+ end
426
+
427
+
428
+ def setStatus(st, callback)
429
+ @currentStatus = st
430
+ updatePresence()
431
+ if callback
432
+ callback({ "success" => true })
433
+ end
434
+ end
435
+
436
+
437
+ def emit(signal, data=nil)
438
+ callbacks = @signals[signal]
439
+ callbacks = [] if not callbacks
440
+ for clb in callbacks
441
+ clb.call(data)
442
+ end
443
+ end
444
+
445
+
446
+ def on(signal, callback)
447
+ if not @signals[signal]
448
+ @signals[signal] = []
449
+ end
450
+ @signals[signal].push(callback)
451
+ end
452
+
453
+
454
+ def start
455
+ while data = @ws.receive()
456
+ on_message(data)
457
+ end
458
+ end
459
+ end
data/lib/websocket.rb ADDED
@@ -0,0 +1,584 @@
1
+ # Copyright: Hiroshi Ichikawa <http://gimite.net/en/>
2
+ # Lincense: New BSD Lincense
3
+ # Reference: http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-75
4
+ # Reference: http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76
5
+ # Reference: http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-07
6
+ # Reference: http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-10
7
+
8
+ require "base64"
9
+ require "socket"
10
+ require "uri"
11
+ require "digest/md5"
12
+ require "digest/sha1"
13
+ require "openssl"
14
+ require "stringio"
15
+
16
+
17
+ class WebSocket
18
+
19
+ class << self
20
+
21
+ attr_accessor(:debug)
22
+
23
+ end
24
+
25
+ class Error < RuntimeError
26
+
27
+ end
28
+
29
+ WEB_SOCKET_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
30
+ OPCODE_CONTINUATION = 0x00
31
+ OPCODE_TEXT = 0x01
32
+ OPCODE_BINARY = 0x02
33
+ OPCODE_CLOSE = 0x08
34
+ OPCODE_PING = 0x09
35
+ OPCODE_PONG = 0x0a
36
+
37
+ def initialize(arg, params = {})
38
+ if params[:server] # server
39
+
40
+ @server = params[:server]
41
+ @socket = arg
42
+ line = gets().chomp()
43
+ if !(line =~ /\AGET (\S+) HTTP\/1.1\z/n)
44
+ raise(WebSocket::Error, "invalid request: #{line}")
45
+ end
46
+ @path = $1
47
+ read_header()
48
+ if @header["sec-websocket-version"]
49
+ @web_socket_version = @header["sec-websocket-version"]
50
+ @key3 = nil
51
+ elsif @header["sec-websocket-key1"] && @header["sec-websocket-key2"]
52
+ @web_socket_version = "hixie-76"
53
+ @key3 = read(8)
54
+ else
55
+ @web_socket_version = "hixie-75"
56
+ @key3 = nil
57
+ end
58
+ if !@server.accepted_origin?(self.origin)
59
+ raise(WebSocket::Error,
60
+ ("Unaccepted origin: %s (server.accepted_domains = %p)\n\n" +
61
+ "To accept this origin, write e.g. \n" +
62
+ " WebSocketServer.new(..., :accepted_domains => [%p]), or\n" +
63
+ " WebSocketServer.new(..., :accepted_domains => [\"*\"])\n") %
64
+ [self.origin, @server.accepted_domains, @server.origin_to_domain(self.origin)])
65
+ end
66
+ @handshaked = false
67
+
68
+ else # client
69
+
70
+ @web_socket_version = "hixie-76"
71
+ uri = arg.is_a?(String) ? URI.parse(arg) : arg
72
+
73
+ if uri.scheme == "ws"
74
+ default_port = 80
75
+ elsif uri.scheme = "wss"
76
+ default_port = 443
77
+ else
78
+ raise(WebSocket::Error, "unsupported scheme: #{uri.scheme}")
79
+ end
80
+
81
+ @path = (uri.path.empty? ? "/" : uri.path) + (uri.query ? "?" + uri.query : "")
82
+ host = uri.host + ((!uri.port || uri.port == default_port) ? "" : ":#{uri.port}")
83
+ origin = params[:origin] || "http://#{uri.host}"
84
+ key1 = generate_key()
85
+ key2 = generate_key()
86
+ key3 = generate_key3()
87
+
88
+ socket = TCPSocket.new(uri.host, uri.port || default_port)
89
+
90
+ if uri.scheme == "ws"
91
+ @socket = socket
92
+ else
93
+ @socket = ssl_handshake(socket)
94
+ end
95
+
96
+ write(
97
+ "GET #{@path} HTTP/1.1\r\n" +
98
+ "Upgrade: WebSocket\r\n" +
99
+ "Connection: Upgrade\r\n" +
100
+ "Host: #{host}\r\n" +
101
+ "Origin: #{origin}\r\n" +
102
+ "Sec-WebSocket-Key1: #{key1}\r\n" +
103
+ "Sec-WebSocket-Key2: #{key2}\r\n" +
104
+ "\r\n" +
105
+ "#{key3}")
106
+ flush()
107
+
108
+ line = gets().chomp()
109
+ raise(WebSocket::Error, "bad response: #{line}") if !(line =~ /\AHTTP\/1.1 101 /n)
110
+ read_header()
111
+ if (@header["sec-websocket-origin"] || "").downcase() != origin.downcase()
112
+ raise(WebSocket::Error,
113
+ "origin doesn't match: '#{@header["sec-websocket-origin"]}' != '#{origin}'")
114
+ end
115
+ reply_digest = read(16)
116
+ expected_digest = hixie_76_security_digest(key1, key2, key3)
117
+ if reply_digest != expected_digest
118
+ raise(WebSocket::Error,
119
+ "security digest doesn't match: %p != %p" % [reply_digest, expected_digest])
120
+ end
121
+ @handshaked = true
122
+
123
+ end
124
+ @received = []
125
+ @buffer = ""
126
+ @closing_started = false
127
+ end
128
+
129
+ attr_reader(:server, :header, :path)
130
+
131
+ def handshake(status = nil, header = {})
132
+ if @handshaked
133
+ raise(WebSocket::Error, "handshake has already been done")
134
+ end
135
+ status ||= "101 Switching Protocols"
136
+ def_header = {}
137
+ case @web_socket_version
138
+ when "hixie-75"
139
+ def_header["WebSocket-Origin"] = self.origin
140
+ def_header["WebSocket-Location"] = self.location
141
+ extra_bytes = ""
142
+ when "hixie-76"
143
+ def_header["Sec-WebSocket-Origin"] = self.origin
144
+ def_header["Sec-WebSocket-Location"] = self.location
145
+ extra_bytes = hixie_76_security_digest(
146
+ @header["Sec-WebSocket-Key1"], @header["Sec-WebSocket-Key2"], @key3)
147
+ else
148
+ def_header["Sec-WebSocket-Accept"] = security_digest(@header["sec-websocket-key"])
149
+ extra_bytes = ""
150
+ end
151
+ header = def_header.merge(header)
152
+ header_str = header.map(){ |k, v| "#{k}: #{v}\r\n" }.join("")
153
+ # Note that Upgrade and Connection must appear in this order.
154
+ write(
155
+ "HTTP/1.1 #{status}\r\n" +
156
+ "Upgrade: websocket\r\n" +
157
+ "Connection: Upgrade\r\n" +
158
+ "#{header_str}\r\n#{extra_bytes}")
159
+ flush()
160
+ @handshaked = true
161
+ end
162
+
163
+ def send(data)
164
+ if !@handshaked
165
+ raise(WebSocket::Error, "call WebSocket\#handshake first")
166
+ end
167
+ case @web_socket_version
168
+ when "hixie-75", "hixie-76"
169
+ data = force_encoding(data.dup(), "ASCII-8BIT")
170
+ write("\x00#{data}\xff")
171
+ flush()
172
+ else
173
+ send_frame(OPCODE_TEXT, data, !@server)
174
+ end
175
+ end
176
+
177
+ def receive()
178
+ if !@handshaked
179
+ raise(WebSocket::Error, "call WebSocket\#handshake first")
180
+ end
181
+ case @web_socket_version
182
+
183
+ when "hixie-75", "hixie-76"
184
+ packet = gets("\xff")
185
+ return nil if !packet
186
+ if packet =~ /\A\x00(.*)\xff\z/nm
187
+ return force_encoding($1, "UTF-8")
188
+ elsif packet == "\xff" && read(1) == "\x00" # closing
189
+ close(1005, "", :peer)
190
+ return nil
191
+ else
192
+ raise(WebSocket::Error, "input must be either '\\x00...\\xff' or '\\xff\\x00'")
193
+ end
194
+
195
+ else
196
+ begin
197
+ bytes = read(2).unpack("C*")
198
+ fin = (bytes[0] & 0x80) != 0
199
+ opcode = bytes[0] & 0x0f
200
+ mask = (bytes[1] & 0x80) != 0
201
+ plength = bytes[1] & 0x7f
202
+ if plength == 126
203
+ bytes = read(2)
204
+ plength = bytes.unpack("n")[0]
205
+ elsif plength == 127
206
+ bytes = read(8)
207
+ (high, low) = bytes.unpack("NN")
208
+ plength = high * (2 ** 32) + low
209
+ end
210
+ if @server && !mask
211
+ # Masking is required.
212
+ @socket.close()
213
+ raise(WebSocket::Error, "received unmasked data")
214
+ end
215
+ mask_key = mask ? read(4).unpack("C*") : nil
216
+ payload = read(plength)
217
+ payload = apply_mask(payload, mask_key) if mask
218
+ case opcode
219
+ when OPCODE_TEXT
220
+ return force_encoding(payload, "UTF-8")
221
+ when OPCODE_BINARY
222
+ raise(WebSocket::Error, "received binary data, which is not supported")
223
+ when OPCODE_CLOSE
224
+ close(1005, "", :peer)
225
+ return nil
226
+ when OPCODE_PING
227
+ raise(WebSocket::Error, "received ping, which is not supported")
228
+ when OPCODE_PONG
229
+ else
230
+ raise(WebSocket::Error, "received unknown opcode: %d" % opcode)
231
+ end
232
+ rescue EOFError
233
+ return nil
234
+ end
235
+
236
+ end
237
+ end
238
+
239
+ def tcp_socket
240
+ return @socket
241
+ end
242
+
243
+ def host
244
+ return @header["host"]
245
+ end
246
+
247
+ def origin
248
+ case @web_socket_version
249
+ when "7", "8"
250
+ name = "sec-websocket-origin"
251
+ else
252
+ name = "origin"
253
+ end
254
+ if @header[name]
255
+ return @header[name]
256
+ else
257
+ raise(WebSocket::Error, "%s header is missing" % name)
258
+ end
259
+ end
260
+
261
+ def location
262
+ return "ws://#{self.host}#{@path}"
263
+ end
264
+
265
+ # Does closing handshake.
266
+ def close(code = 1005, reason = "", origin = :self)
267
+ if !@closing_started
268
+ case @web_socket_version
269
+ when "hixie-75", "hixie-76"
270
+ write("\xff\x00")
271
+ else
272
+ if code == 1005
273
+ payload = ""
274
+ else
275
+ payload = [code].pack("n") + force_encoding(reason.dup(), "ASCII-8BIT")
276
+ end
277
+ send_frame(OPCODE_CLOSE, payload, false)
278
+ end
279
+ end
280
+ @socket.close() if origin == :peer
281
+ @closing_started = true
282
+ end
283
+
284
+ def close_socket()
285
+ @socket.close()
286
+ end
287
+
288
+ private
289
+
290
+ NOISE_CHARS = ("\x21".."\x2f").to_a() + ("\x3a".."\x7e").to_a()
291
+
292
+ def read_header()
293
+ @header = {}
294
+ while line = gets()
295
+ line = line.chomp()
296
+ break if line.empty?
297
+ if !(line =~ /\A(\S+): (.*)\z/n)
298
+ raise(WebSocket::Error, "invalid request: #{line}")
299
+ end
300
+ @header[$1] = $2
301
+ @header[$1.downcase()] = $2
302
+ end
303
+ if !@header["upgrade"]
304
+ raise(WebSocket::Error, "Upgrade header is missing")
305
+ end
306
+ if !(@header["upgrade"] =~ /\AWebSocket\z/i)
307
+ raise(WebSocket::Error, "invalid Upgrade: " + @header["upgrade"])
308
+ end
309
+ if !@header["connection"]
310
+ raise(WebSocket::Error, "Connection header is missing")
311
+ end
312
+ if @header["connection"].split(/,/).grep(/\A\s*Upgrade\s*\z/i).empty?
313
+ raise(WebSocket::Error, "invalid Connection: " + @header["connection"])
314
+ end
315
+ end
316
+
317
+ def send_frame(opcode, payload, mask)
318
+ payload = force_encoding(payload.dup(), "ASCII-8BIT")
319
+ # Setting StringIO's encoding to ASCII-8BIT.
320
+ buffer = StringIO.new(force_encoding("", "ASCII-8BIT"))
321
+ write_byte(buffer, 0x80 | opcode)
322
+ masked_byte = mask ? 0x80 : 0x00
323
+ if payload.bytesize <= 125
324
+ write_byte(buffer, masked_byte | payload.bytesize)
325
+ elsif payload.bytesize < 2 ** 16
326
+ write_byte(buffer, masked_byte | 126)
327
+ buffer.write([payload.bytesize].pack("n"))
328
+ else
329
+ write_byte(buffer, masked_byte | 127)
330
+ buffer.write([payload.bytesize / (2 ** 32), payload.bytesize % (2 ** 32)].pack("NN"))
331
+ end
332
+ if mask
333
+ mask_key = Array.new(4){ rand(256) }
334
+ buffer.write(mask_key.pack("C*"))
335
+ payload = apply_mask(payload, mask_key)
336
+ end
337
+ buffer.write(payload)
338
+ write(buffer.string)
339
+ end
340
+
341
+ def gets(rs = $/)
342
+ line = @socket.gets(rs)
343
+ $stderr.printf("recv> %p\n", line) if WebSocket.debug
344
+ return line
345
+ end
346
+
347
+ def read(num_bytes)
348
+ str = @socket.read(num_bytes)
349
+ $stderr.printf("recv> %p\n", str) if WebSocket.debug
350
+ if str && str.bytesize == num_bytes
351
+ return str
352
+ else
353
+ raise(EOFError)
354
+ end
355
+ end
356
+
357
+ def write(data)
358
+ if WebSocket.debug
359
+ data.scan(/\G(.*?(\n|\z))/n) do
360
+ $stderr.printf("send> %p\n", $&) if !$&.empty?
361
+ end
362
+ end
363
+ @socket.write(data)
364
+ end
365
+
366
+ def flush()
367
+ @socket.flush()
368
+ end
369
+
370
+ def write_byte(buffer, byte)
371
+ buffer.write([byte].pack("C"))
372
+ end
373
+
374
+ def security_digest(key)
375
+ return Base64.encode64(Digest::SHA1.digest(key + WEB_SOCKET_GUID)).gsub(/\n/, "")
376
+ end
377
+
378
+ def hixie_76_security_digest(key1, key2, key3)
379
+ bytes1 = websocket_key_to_bytes(key1)
380
+ bytes2 = websocket_key_to_bytes(key2)
381
+ return Digest::MD5.digest(bytes1 + bytes2 + key3)
382
+ end
383
+
384
+ def apply_mask(payload, mask_key)
385
+ orig_bytes = payload.unpack("C*")
386
+ new_bytes = []
387
+ orig_bytes.each_with_index() do |b, i|
388
+ new_bytes.push(b ^ mask_key[i % 4])
389
+ end
390
+ return new_bytes.pack("C*")
391
+ end
392
+
393
+ def generate_key()
394
+ spaces = 1 + rand(12)
395
+ max = 0xffffffff / spaces
396
+ number = rand(max + 1)
397
+ key = (number * spaces).to_s()
398
+ (1 + rand(12)).times() do
399
+ char = NOISE_CHARS[rand(NOISE_CHARS.size)]
400
+ pos = rand(key.size + 1)
401
+ key[pos...pos] = char
402
+ end
403
+ spaces.times() do
404
+ pos = 1 + rand(key.size - 1)
405
+ key[pos...pos] = " "
406
+ end
407
+ return key
408
+ end
409
+
410
+ def generate_key3()
411
+ return [rand(0x100000000)].pack("N") + [rand(0x100000000)].pack("N")
412
+ end
413
+
414
+ def websocket_key_to_bytes(key)
415
+ num = key.gsub(/[^\d]/n, "").to_i() / key.scan(/ /).size
416
+ return [num].pack("N")
417
+ end
418
+
419
+ def force_encoding(str, encoding)
420
+ if str.respond_to?(:force_encoding)
421
+ return str.force_encoding(encoding)
422
+ else
423
+ return str
424
+ end
425
+ end
426
+
427
+ def ssl_handshake(socket)
428
+ ssl_context = OpenSSL::SSL::SSLContext.new()
429
+ ssl_socket = OpenSSL::SSL::SSLSocket.new(socket, ssl_context)
430
+ ssl_socket.sync_close = true
431
+ ssl_socket.connect()
432
+ return ssl_socket
433
+ end
434
+
435
+ end
436
+
437
+
438
+ class WebSocketServer
439
+
440
+ def initialize(params_or_uri, params = nil)
441
+ if params
442
+ uri = params_or_uri.is_a?(String) ? URI.parse(params_or_uri) : params_or_uri
443
+ params[:port] ||= uri.port
444
+ params[:accepted_domains] ||= [uri.host]
445
+ else
446
+ params = params_or_uri
447
+ end
448
+ @port = params[:port] || 80
449
+ @accepted_domains = params[:accepted_domains]
450
+ if !@accepted_domains
451
+ raise(ArgumentError, "params[:accepted_domains] is required")
452
+ end
453
+ if params[:host]
454
+ @tcp_server = TCPServer.open(params[:host], @port)
455
+ else
456
+ @tcp_server = TCPServer.open(@port)
457
+ end
458
+ end
459
+
460
+ attr_reader(:tcp_server, :port, :accepted_domains)
461
+
462
+ def run(&block)
463
+ while true
464
+ Thread.start(accept()) do |s|
465
+ begin
466
+ ws = create_web_socket(s)
467
+ yield(ws) if ws
468
+ rescue => ex
469
+ print_backtrace(ex)
470
+ ensure
471
+ begin
472
+ ws.close_socket() if ws
473
+ rescue
474
+ end
475
+ end
476
+ end
477
+ end
478
+ end
479
+
480
+ def accept()
481
+ return @tcp_server.accept()
482
+ end
483
+
484
+ def accepted_origin?(origin)
485
+ domain = origin_to_domain(origin)
486
+ return @accepted_domains.any?(){ |d| File.fnmatch(d, domain) }
487
+ end
488
+
489
+ def origin_to_domain(origin)
490
+ if origin == "null" || origin == "file://" # local file
491
+ return "null"
492
+ else
493
+ return URI.parse(origin).host
494
+ end
495
+ end
496
+
497
+ def create_web_socket(socket)
498
+ ch = socket.getc()
499
+ if ch == ?<
500
+ # This is Flash socket policy file request, not an actual Web Socket connection.
501
+ send_flash_socket_policy_file(socket)
502
+ return nil
503
+ else
504
+ socket.ungetc(ch)
505
+ return WebSocket.new(socket, :server => self)
506
+ end
507
+ end
508
+
509
+ private
510
+
511
+ def print_backtrace(ex)
512
+ $stderr.printf("%s: %s (%p)\n", ex.backtrace[0], ex.message, ex.class)
513
+ for s in ex.backtrace[1..-1]
514
+ $stderr.printf(" %s\n", s)
515
+ end
516
+ end
517
+
518
+ # Handles Flash socket policy file request sent when web-socket-js is used:
519
+ # http://github.com/gimite/web-socket-js/tree/master
520
+ def send_flash_socket_policy_file(socket)
521
+ socket.puts('<?xml version="1.0"?>')
522
+ socket.puts('<!DOCTYPE cross-domain-policy SYSTEM ' +
523
+ '"http://www.macromedia.com/xml/dtds/cross-domain-policy.dtd">')
524
+ socket.puts('<cross-domain-policy>')
525
+ for domain in @accepted_domains
526
+ next if domain == "file://"
527
+ socket.puts("<allow-access-from domain=\"#{domain}\" to-ports=\"#{@port}\"/>")
528
+ end
529
+ socket.puts('</cross-domain-policy>')
530
+ socket.close()
531
+ end
532
+
533
+ end
534
+
535
+
536
+ if __FILE__ == $0
537
+ Thread.abort_on_exception = true
538
+
539
+ if ARGV[0] == "server" && ARGV.size == 3
540
+
541
+ server = WebSocketServer.new(
542
+ :accepted_domains => [ARGV[1]],
543
+ :port => ARGV[2].to_i())
544
+ puts("Server is running at port %d" % server.port)
545
+ server.run() do |ws|
546
+ puts("Connection accepted")
547
+ puts("Path: #{ws.path}, Origin: #{ws.origin}")
548
+ if ws.path == "/"
549
+ ws.handshake()
550
+ while data = ws.receive()
551
+ printf("Received: %p\n", data)
552
+ ws.send(data)
553
+ printf("Sent: %p\n", data)
554
+ end
555
+ else
556
+ ws.handshake("404 Not Found")
557
+ end
558
+ puts("Connection closed")
559
+ end
560
+
561
+ elsif ARGV[0] == "client" && ARGV.size == 2
562
+
563
+ client = WebSocket.new(ARGV[1])
564
+ puts("Connected")
565
+ Thread.new() do
566
+ while data = client.receive()
567
+ printf("Received: %p\n", data)
568
+ end
569
+ end
570
+ $stdin.each_line() do |line|
571
+ data = line.chomp()
572
+ client.send(data)
573
+ printf("Sent: %p\n", data)
574
+ end
575
+
576
+ else
577
+
578
+ $stderr.puts("Usage:")
579
+ $stderr.puts(" ruby web_socket.rb server ACCEPTED_DOMAIN PORT")
580
+ $stderr.puts(" ruby web_socket.rb client ws://HOST:PORT/")
581
+ exit(1)
582
+
583
+ end
584
+ end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ttapi
3
3
  version: !ruby/object:Gem::Version
4
- hash: 29
4
+ hash: 27
5
5
  prerelease: false
6
6
  segments:
7
7
  - 0
8
8
  - 0
9
- - 1
10
- version: 0.0.1
9
+ - 2
10
+ version: 0.0.2
11
11
  platform: ruby
12
12
  authors:
13
13
  - alaingilbert
@@ -17,82 +17,8 @@ cert_chain: []
17
17
 
18
18
  date: 2012-04-25 00:00:00 -04:00
19
19
  default_executable:
20
- dependencies:
21
- - !ruby/object:Gem::Dependency
22
- requirement: &id001 !ruby/object:Gem::Requirement
23
- none: false
24
- requirements:
25
- - - ">="
26
- - !ruby/object:Gem::Version
27
- hash: 3
28
- segments:
29
- - 0
30
- version: "0"
31
- type: :development
32
- name: shoulda
33
- prerelease: false
34
- version_requirements: *id001
35
- - !ruby/object:Gem::Dependency
36
- requirement: &id002 !ruby/object:Gem::Requirement
37
- none: false
38
- requirements:
39
- - - ">="
40
- - !ruby/object:Gem::Version
41
- hash: 31
42
- segments:
43
- - 3
44
- - 12
45
- version: "3.12"
46
- type: :development
47
- name: rdoc
48
- prerelease: false
49
- version_requirements: *id002
50
- - !ruby/object:Gem::Dependency
51
- requirement: &id003 !ruby/object:Gem::Requirement
52
- none: false
53
- requirements:
54
- - - ">="
55
- - !ruby/object:Gem::Version
56
- hash: 23
57
- segments:
58
- - 1
59
- - 0
60
- - 0
61
- version: 1.0.0
62
- type: :development
63
- name: bundler
64
- prerelease: false
65
- version_requirements: *id003
66
- - !ruby/object:Gem::Dependency
67
- requirement: &id004 !ruby/object:Gem::Requirement
68
- none: false
69
- requirements:
70
- - - ">="
71
- - !ruby/object:Gem::Version
72
- hash: 49
73
- segments:
74
- - 1
75
- - 8
76
- - 3
77
- version: 1.8.3
78
- type: :development
79
- name: jeweler
80
- prerelease: false
81
- version_requirements: *id004
82
- - !ruby/object:Gem::Dependency
83
- requirement: &id005 !ruby/object:Gem::Requirement
84
- none: false
85
- requirements:
86
- - - ">="
87
- - !ruby/object:Gem::Version
88
- hash: 3
89
- segments:
90
- - 0
91
- version: "0"
92
- type: :development
93
- name: rcov
94
- prerelease: false
95
- version_requirements: *id005
20
+ dependencies: []
21
+
96
22
  description: A simple ruby wrapper for the turntable API
97
23
  email: alain.gilbert.15@gmail.com
98
24
  executables: []
@@ -100,20 +26,20 @@ executables: []
100
26
  extensions: []
101
27
 
102
28
  extra_rdoc_files:
103
- - LICENSE.txt
104
- - README.rdoc
29
+ - README.md
105
30
  files:
106
- - .document
31
+ - .gitignore
107
32
  - Gemfile
108
- - LICENSE.txt
109
- - README.rdoc
33
+ - README.md
110
34
  - Rakefile
111
35
  - VERSION
112
- - lib/ttapi.rb
36
+ - examples/chat_bot.rb
37
+ - lib/bot.rb
38
+ - lib/websocket.rb
113
39
  - test/helper.rb
114
40
  - test/test_ttapi.rb
115
41
  has_rdoc: true
116
- homepage: http://github.com/alaingilbert/ttapi
42
+ homepage: http://github.com/alaingilbert/Turntable-API
117
43
  licenses:
118
44
  - MIT
119
45
  post_install_message:
data/.document DELETED
@@ -1,5 +0,0 @@
1
- lib/**/*.rb
2
- bin/*
3
- -
4
- features/**/*.feature
5
- LICENSE.txt
data/LICENSE.txt DELETED
@@ -1,20 +0,0 @@
1
- Copyright (c) 2012 alaingilbert
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.
data/lib/ttapi.rb DELETED
File without changes