ttapi 0.0.1 → 0.0.2

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/.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