lpetre-sinbook 0.1.10.pre

Sign up to get free protection for your applications and to get access to all the features.
data/README ADDED
@@ -0,0 +1,130 @@
1
+ sinbook: simple sinatra facebook extension in 300 lines of ruby
2
+ (c) 2009 Aman Gupta (tmm1)
3
+
4
+ === Usage
5
+
6
+ require 'sinbook'
7
+ require 'sinatra'
8
+
9
+ facebook do
10
+ api_key '4579...cbb0'
11
+ secret '5106...2342'
12
+ app_id 81747826609
13
+ url 'http://apps.facebook.com/myappname'
14
+ callback 'http://myappserver.com'
15
+ end
16
+
17
+ get '/' do
18
+ fb.require_login!
19
+ "Hi <fb:name uid=#{fb[:user]} useyou=false />!"
20
+ end
21
+
22
+
23
+ === Features
24
+
25
+ sinbook provides a simple `facebook` helper (also aliased to `fb`).
26
+
27
+ >> fb.valid?
28
+ => true # valid (authenticated) request from facebook's servers
29
+
30
+ >> pp fb.params
31
+ {
32
+ :logged_out_facebook => false, # request came from a user logged into facebook
33
+ :added => true, # user is logged into our app
34
+ :user => 1234, # user's facebook uid
35
+ :friends => [1,2,3], # list of user's friends
36
+ ...
37
+ }
38
+
39
+ >> fb[:user] # [] is aliased to params[]
40
+ => 1234
41
+
42
+ >> fb.callback
43
+ => 'http://apps.facebook.com/myappname'
44
+
45
+ >> fb.callback('/homepage')
46
+ => 'http://apps.facebook.com/myappname/homepage'
47
+
48
+ >> fb.url('/images/static.gif')
49
+ => 'http://myappserver.com/images/static.gif'
50
+
51
+ >> fb.appurl
52
+ => 'http://apps.facebook.com/add.php?api_key=4579...cbb0'
53
+
54
+ >> fb.addurl
55
+ => 'http://www.facebook.com/apps/application.php?id=81747826609'
56
+
57
+ >> fb.redirect('/welcome') # redirect using an fb:redirect tag
58
+
59
+ >> fb.require_login! # redirect to addurl page unless logged in
60
+
61
+
62
+ The helper can also be used to make API calls
63
+
64
+ >> fb.users.getInfo :uid => 1234, :fields => [:name]
65
+ => [{'uid' => 1234, 'name' => 'Frank Sinatra'}]
66
+
67
+ >> fb.groups.get :uid => 1234
68
+ => [{'name' => 'Sinatra Users'}]
69
+
70
+ >> fb.profile.setFBML :profile => 'hello world'
71
+ => true
72
+
73
+ >> fb.profile.getFBML
74
+ => 'hello world'
75
+
76
+
77
+ === Other Options
78
+
79
+ facebook do
80
+ symbolize_keys true
81
+ end
82
+
83
+ >> fb.groups.get :uid => 1234
84
+ => [{:name => 'Sinatra Users'}]
85
+
86
+
87
+ === Standalone Usage
88
+
89
+ require 'sinbook'
90
+ fb = Sinbook.new(
91
+ :api_key => '4579...cbb0',
92
+ :secret => '5106...2342',
93
+ :app_id => 81747826609
94
+ )
95
+
96
+ >> fb.friends.get :uid => 1234, :session_key => 'user-session-key'
97
+ => [4321,4567,9876]
98
+
99
+
100
+ === Local Development
101
+
102
+ To develop locally, use ssh to setup a reverse tunnel to your external server.
103
+ For example, set your callback url to http://myserver.com:4567/, and run:
104
+
105
+ ssh -gNR 4567:localhost:4567 me@myserver.com
106
+
107
+ Then, simply launch sinatra on your local machine. Facebook will make requests to
108
+ http://myserver.com:4567/ which will be forwarded to port 4567 on your local machine.
109
+
110
+ For facebook connect, I generally add a CNAME on my domain for fb.myserver.com, and setup
111
+ nginx on my server with the following:
112
+
113
+ server {
114
+ server_name fb.myserver.com;
115
+ location / {
116
+ proxy_pass http://localhost:4567;
117
+ }
118
+ }
119
+
120
+ Make sure your connect url is set to 'http://myserver.com' and the base domain is set to 'myserver.com',
121
+ so that the fb.myserver.com subdomain works.
122
+
123
+ === TODO
124
+
125
+ * Add a batch mode for api calls:
126
+
127
+ groups, pics = fb.batch do |b|
128
+ b.groups.get :uid => 123
129
+ b.users.getInfo :uids => 123, :fields => [:pic_square]
130
+ end
@@ -0,0 +1,51 @@
1
+ require 'rubygems'
2
+ require 'sinbook'
3
+ require 'sinatra'
4
+
5
+ facebook do
6
+ api_key '858593842e5a3cefe59b72ddc7ffdd56'
7
+ secret '56ce1b26bf48ac8bac927c7d280b18f8'
8
+ app_id 185945096655
9
+ url 'http://tmm1.net:4568/'
10
+ callback 'http://tmm1.net:4568/'
11
+ end
12
+
13
+ set :port, 4568
14
+
15
+ get '/' do
16
+ haml :main
17
+ end
18
+
19
+ get '/receiver' do
20
+ %[<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
21
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
22
+ <html xmlns="http://www.w3.org/1999/xhtml" >
23
+ <body>
24
+ <script src="http://static.ak.connect.facebook.com/js/api_lib/v0.4/XdCommReceiver.js" type="text/javascript"></script>
25
+ </body>
26
+ </html>]
27
+ end
28
+
29
+ __END__
30
+
31
+ @@ layout
32
+ %html{:xmlns=>"http://www.w3.org/1999/xhtml", :'xmlns:fb'=>"http://www.facebook.com/2008/fbml"}
33
+ %head
34
+ %title Welcome to my Facebook Connect website!
35
+ %script{:type => 'text/javascript', :src => 'http://static.ak.connect.facebook.com/js/api_lib/v0.4/FeatureLoader.js.php/en_US'}
36
+ %body
37
+ = yield
38
+ :javascript
39
+ FB.init("#{fb.api_key}", "/receiver")
40
+
41
+ @@ main
42
+ - if fb[:user]
43
+ Hi,
44
+ %fb:profile-pic{:uid => fb[:user]}
45
+ %fb:name{:uid => fb[:user], :useyou => 'false', :firstnameonly => 'true'}
46
+ !
47
+ %br/
48
+ Do you want to <a href="javascript:FB.Connect.logoutAndRedirect('/')">logout</a>?
49
+ - else
50
+ Please login:
51
+ %fb:login-button{:onlogin => 'document.location.reload(true)'}
@@ -0,0 +1,60 @@
1
+ require 'rubygems'
2
+ require 'sinbook'
3
+ require 'sinatra'
4
+
5
+ facebook do
6
+ api_key '45796747415d12227f52146b4444cbb0'
7
+ secret '5106c7409f18d7618dd03433a2f72342'
8
+ app_id 81747826609
9
+ url 'http://apps.facebook.com/sinatrafacebook'
10
+ callback 'http://tmm1.net:4567'
11
+ end
12
+
13
+ get '/' do
14
+ if not facebook.valid?
15
+ # not accessed via facebook, redirect to the facebook app url
16
+ redirect fb.url
17
+
18
+ elsif fb[:logged_out_facebook]
19
+ # user is not logged into facebook
20
+ "Hey there! This is an awesome facebook app, but you must login to facebook to see it."
21
+
22
+ elsif not fb[:added]
23
+ # user is logged into facebook, but not our app
24
+
25
+ if not fb[:canvas_user]
26
+ # user navigated to the app directly, we know nothing about them
27
+ "
28
+ Hey there, you should add this <b>awesome</b> app! <br>
29
+ Go <a href='#{fb.addurl}'>here</a> or just click <a href='#' requirelogin=true>here</a>! <br>
30
+ Or, if you don't want to add the app, click <a href='#{fb.url('/')}'>here</a> so I know who you are.
31
+ "
32
+
33
+ else
34
+ # user came via a feed/notification or clicked on a link in the app, so we know who they are
35
+ "
36
+ Hey <fb:name uid=#{fb[:canvas_user]} useyou=false />. <br>
37
+ All I know about you is that you have #{fb[:friends].size} friends. <br>
38
+ Maybe you'll <a href='#{fb.addurl}'>add this app</a> so I can tell you more?
39
+ "
40
+ end
41
+
42
+ elsif fb[:user]
43
+ # logged into facebook and our app, we can get all their info
44
+ "
45
+ Hey <fb:name uid=#{fb[:user]} useyou=false />! <br>
46
+ Check out the special <a href='#{fb.url('/members')}'>members-only area</a>. <br>
47
+ And don't forget to tell your #{fb[:friends].size} friends to add this app too!
48
+ "
49
+ end
50
+ end
51
+
52
+ get '/members' do
53
+ fb.require_login!
54
+
55
+ groups = fb.groups.get :uid => fb[:user]
56
+ "
57
+ Hey there, now that you're a member I can tell what groups you're in on Facebook: <br>
58
+ #{groups.map{|g| g['name'] }.join('<br>')}
59
+ "
60
+ end
data/lib/sinbook.rb ADDED
@@ -0,0 +1,381 @@
1
+ begin
2
+ require 'sinatra/base'
3
+ rescue LoadError
4
+ retry if require 'rubygems'
5
+ raise
6
+ end
7
+
8
+ class FacebookError < StandardError
9
+ attr_accessor :data
10
+ end
11
+
12
+ module Sinatra
13
+ require 'digest/md5'
14
+ require 'yajl'
15
+
16
+ class FacebookObject
17
+ def initialize app
18
+ if app.respond_to?(:options)
19
+ @app = app
20
+
21
+ [ :api_key, :secret, :app_id, :url, :callback, :symbolize_keys ].each do |var|
22
+ instance_variable_set("@#{var}", app.options.send("facebook_#{var}"))
23
+ end
24
+ else
25
+ [ :api_key, :secret, :app_id ].each do |var|
26
+ raise ArgumentError, "missing option #{var}" unless app[var]
27
+ instance_variable_set("@#{var}", app[var])
28
+ end
29
+ [:url, :callback, :symbolize_keys ].each do |var|
30
+ instance_variable_set("@#{var}", app[var]) if app.has_key?(var)
31
+ end
32
+ end
33
+ end
34
+
35
+ attr_reader :app
36
+ attr_accessor :api_key, :secret
37
+ attr_writer :url, :callback, :app_id
38
+
39
+ def app_id
40
+ @app_id || self[:app_id]
41
+ end
42
+
43
+ def url postfix=nil
44
+ postfix ? "#{@url}#{postfix}" : @url
45
+ end
46
+
47
+ def callback postfix=nil
48
+ postfix ? "#{@callback}#{postfix}" : @callback
49
+ end
50
+
51
+ def addurl
52
+ "http://apps.facebook.com/add.php?api_key=#{self.api_key}"
53
+ end
54
+
55
+ def appurl
56
+ "http://www.facebook.com/apps/application.php?id=#{self.app_id}"
57
+ end
58
+
59
+ def require_login!
60
+ if valid?
61
+ redirect addurl unless params[:user]
62
+ else
63
+ app.redirect url
64
+ end
65
+ end
66
+
67
+ def redirect url
68
+ url = self.url + url unless url =~ /^http/
69
+ if params[:in_iframe]
70
+ app.body "<script type=\"text/javascript\">top.location.href=\"#{url}\"</script>"
71
+ else
72
+ app.body "<fb:redirect url='#{url}'/>"
73
+ end
74
+ throw :halt
75
+ end
76
+
77
+ def params
78
+ return {} unless valid?
79
+ app.env['facebook.params'] ||= \
80
+ app.env['facebook.vars'].inject({}) do |h,(k,v)|
81
+ s = k.to_sym
82
+ case k
83
+ when 'friends'
84
+ h[s] = v.split(',').map{|e|e.to_i}
85
+ when /time$/
86
+ h[s] = Time.at(v.to_f)
87
+ when 'expires'
88
+ v = v.to_i
89
+ h[s] = v>0 ? Time.at(v) : v
90
+ when 'user', 'app_id', 'canvas_user'
91
+ h[s] = v.to_i
92
+ when /^(logged_out|position_|in_|is_|added)/
93
+ h[s] = v=='1'
94
+ else
95
+ h[s] = v
96
+ end
97
+ h
98
+ end
99
+ end
100
+
101
+ def [] key
102
+ params[key]
103
+ end
104
+
105
+ def valid?
106
+ if app.nil?
107
+ return false
108
+ elsif app.params['fb_sig'] # canvas/iframe mode
109
+ prefix = 'fb_sig'
110
+ vars = app.request.POST[prefix] ? app.request.POST : app.request.GET
111
+ elsif app.request.cookies[api_key] # fbconnect mode
112
+ prefix = api_key
113
+ vars = app.request.cookies
114
+ else
115
+ return false
116
+ end
117
+
118
+ if app.env['facebook.valid?'].nil?
119
+ fbvars = {}
120
+ sig = Digest::MD5.hexdigest(vars.map{|k,v|
121
+ if k =~ /^#{prefix}_(.+)$/
122
+ fbvars[$1] = v
123
+ "#{$1}=#{v}"
124
+ end
125
+ }.compact.sort.join+self.secret)
126
+
127
+ if app.env['facebook.valid?'] = (vars[prefix] == sig)
128
+ app.env['facebook.vars'] = fbvars
129
+ end
130
+ end
131
+
132
+ app.env['facebook.valid?']
133
+ end
134
+
135
+ class APIProxy
136
+ Types = %w[
137
+ admin
138
+ application
139
+ auth
140
+ batch
141
+ comments
142
+ connect
143
+ dashboard
144
+ data
145
+ events
146
+ fbml
147
+ feed
148
+ fql
149
+ friends
150
+ groups
151
+ links
152
+ liveMessage
153
+ notes
154
+ notifications
155
+ pages
156
+ photos
157
+ profile
158
+ sms
159
+ status
160
+ stream
161
+ users
162
+ video
163
+ ]
164
+
165
+ alias :__class__ :class
166
+ alias :__inspect__ :inspect
167
+ instance_methods.each { |m| undef_method m unless m =~ /^(__|object_id)/ }
168
+ alias :inspect :__inspect__
169
+
170
+ def initialize name, obj
171
+ @name, @obj = name, obj
172
+ end
173
+
174
+ def method_missing method, opts = {}
175
+ @obj.request "#{@name}.#{method}", opts
176
+ end
177
+ end
178
+
179
+ APIProxy::Types.each do |n|
180
+ class_eval %[
181
+ def #{n}
182
+ (@proxies||={})[:#{n}] ||= APIProxy.new(:#{n}, self)
183
+ end
184
+ ]
185
+ end
186
+
187
+ def request method, opts = {}
188
+ if method == 'photos.upload'
189
+ image = opts.delete :image
190
+ end
191
+
192
+ opts = { :api_key => self.api_key,
193
+ :call_id => Time.now.to_f,
194
+ :format => 'JSON',
195
+ :v => '1.0',
196
+ :session_key => %w[ photos.upload ].include?(method) ? nil : params[:session_key],
197
+ :method => method }.merge(opts)
198
+
199
+ encode_array = false
200
+ [/^dashboard\./, /CustomTags$/].each do |pattern|
201
+ encode_array = true if pattern.match method
202
+ end
203
+
204
+ args = opts.map{ |k,v|
205
+ next nil unless v
206
+
207
+ "#{k}=" + case v
208
+ when Hash
209
+ Yajl::Encoder.encode(v)
210
+ when Array
211
+ if encode_array
212
+ Yajl::Encoder.encode(v)
213
+ else
214
+ v.join(',')
215
+ end
216
+ else
217
+ v.to_s
218
+ end
219
+ }.compact.sort
220
+
221
+ sig = Digest::MD5.hexdigest(args.join+self.secret)
222
+
223
+ if method == 'photos.upload'
224
+ data = MimeBoundary
225
+ data += opts.merge(:sig => sig).inject('') do |buf, (key, val)|
226
+ if val
227
+ buf << (MimePart % [key, val])
228
+ else
229
+ buf
230
+ end
231
+ end
232
+ data += MimeImage % ['upload.jpg', 'jpg', image.respond_to?(:read) ? image.read : image]
233
+ else
234
+ data = Array["sig=#{sig}", *args.map{|a| a.gsub('&','%26') }].join('&')
235
+ end
236
+
237
+ ret = self.class.request(data, method == 'photos.upload')
238
+
239
+ ret = if ['true', '1'].include? ret
240
+ true
241
+ elsif ['false', '0'].include? ret
242
+ false
243
+ elsif (n = Integer(ret) rescue nil)
244
+ n
245
+ else
246
+ Yajl::Parser.parse(ret, :symbolize_keys => @symbolize_keys)
247
+ end
248
+
249
+ if ret.is_a?(Hash) and (ret['error_code'] or ret[:error_code])
250
+ err = FacebookError.new(ret['error_msg'] || ret[:error_msg])
251
+ err.data = ret
252
+ raise err
253
+ end
254
+
255
+ ret
256
+ end
257
+
258
+ MimeBoundary = "--SoMeTeXtWeWiLlNeVeRsEe\r\n"
259
+ MimePart = %[Content-Disposition: form-data; name="%s"\r\n\r\n%s\r\n] + MimeBoundary
260
+ MimeImage = %[Content-Disposition: form-data; filename="%s"\r\nContent-Type: image/%s\r\n\r\n%s\r\n] + MimeBoundary
261
+
262
+ require 'resolv'
263
+ @keepalive = false
264
+
265
+ def self.connect
266
+ sock = TCPSocket.new(@api_server_ip ||= Resolv.getaddress('api.facebook.com'), 80)
267
+ begin
268
+ timeout = [3,0].pack('l_2') # 3 seconds
269
+ sock.setsockopt Socket::SOL_SOCKET, Socket::SO_RCVTIMEO, timeout
270
+ sock.setsockopt Socket::SOL_SOCKET, Socket::SO_SNDTIMEO, timeout
271
+ rescue Exception => ex
272
+ # causes issues on solaris?
273
+ puts ex.inspect
274
+ end
275
+ sock
276
+ end
277
+
278
+ def self.request data, mime=false
279
+ if @keepalive
280
+ @socket ||= connect
281
+ else
282
+ @socket = connect
283
+ end
284
+
285
+ @socket.print "POST /restserver.php HTTP/1.1\r\n"
286
+ @socket.print "Host: api.facebook.com\r\n"
287
+ @socket.print "Connection: keep-alive\r\n" if @keepalive
288
+ if mime
289
+ @socket.print "Content-Type: multipart/form-data; boundary=#{MimeBoundary[2..-3]}\r\n"
290
+ @socket.print "MIME-version: 1.0\r\n"
291
+ else
292
+ @socket.print "Content-Type: application/x-www-form-urlencoded\r\n"
293
+ end
294
+ @socket.print "Content-Length: #{data.length}\r\n"
295
+ @socket.print "\r\n#{data}\r\n"
296
+ @socket.print "\r\n\r\n"
297
+
298
+ buf = ''
299
+ headers = ''
300
+ headers_done = false
301
+ chunked = true
302
+
303
+ while true
304
+ line = @socket.gets
305
+ headers << line unless headers_done
306
+ raise Errno::ECONNRESET unless line
307
+
308
+ if line == "\r\n" # end of headers/chunk
309
+ unless headers_done
310
+ headers_done = true
311
+ if headers =~ /Encoding: chunked/i
312
+ chunked = true
313
+ else
314
+ len = headers[/Content-Length: (\d+)/i,1].to_i
315
+ buf = @socket.read(len)
316
+ break # done!
317
+ end
318
+ end
319
+
320
+ line = @socket.gets # get size of next chunk
321
+ if line.strip! == '0' # 0 sized chunk
322
+ @socket.gets # read last crlf
323
+ break # done!
324
+ end
325
+
326
+ buf << @socket.read(line.to_i(16)) # read in chunk
327
+ end
328
+ end
329
+
330
+ buf
331
+ rescue Errno::EPIPE, Errno::ECONNRESET
332
+ @socket = nil
333
+ retry
334
+ ensure
335
+ @socket.close if @socket and !@keepalive
336
+ end
337
+ end
338
+
339
+ module FacebookHelper
340
+ def facebook
341
+ env['facebook.helper'] ||= FacebookObject.new(self)
342
+ end
343
+ alias fb facebook
344
+ end
345
+
346
+ class FacebookSettings
347
+ def initialize app, &blk
348
+ @app = app
349
+ @app.set :facebook_symbolize_keys, false
350
+ instance_eval &blk
351
+ end
352
+ %w[ api_key secret app_id url callback symbolize_keys ].each do |param|
353
+ class_eval %[
354
+ def #{param} val, &blk
355
+ @app.set :facebook_#{param}, val
356
+ end
357
+ ]
358
+ end
359
+ end
360
+
361
+ module Facebook
362
+ def facebook &blk
363
+ FacebookSettings.new(self, &blk)
364
+ end
365
+
366
+ FixRequestMethod = proc{
367
+ if method = request.params['fb_sig_request_method']
368
+ request.env['REQUEST_METHOD'] = method
369
+ end
370
+ }
371
+
372
+ def self.registered app
373
+ app.helpers FacebookHelper
374
+ app.before(&FixRequestMethod)
375
+ end
376
+ end
377
+
378
+ Application.register Facebook
379
+ end
380
+
381
+ Sinbook = Sinatra::FacebookObject
data/sinbook.gemspec ADDED
@@ -0,0 +1,24 @@
1
+ spec = Gem::Specification.new do |s|
2
+ s.name = 'lpetre-sinbook'
3
+ s.version = '0.1.10.pre'
4
+ s.date = '2010-02-04'
5
+ s.summary = 'simple sinatra facebook extension in 300 lines of ruby'
6
+ s.description = 'A full-featured facebook extension for the sinatra webapp framework'
7
+
8
+ s.homepage = "http://github.com/tmm1/sinbook"
9
+
10
+ s.authors = ["Aman Gupta"]
11
+ s.email = "aman@tmm1.net"
12
+
13
+ s.add_dependency('yajl-ruby')
14
+ s.has_rdoc = false
15
+
16
+ # ruby -rpp -e' pp `git ls-files | grep -v examples`.split("\n") '
17
+ s.files = [
18
+ "README",
19
+ "sinbook.gemspec",
20
+ "lib/sinbook.rb",
21
+ "examples/simple.rb",
22
+ "examples/connect.rb"
23
+ ]
24
+ end
metadata ADDED
@@ -0,0 +1,80 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lpetre-sinbook
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: true
5
+ segments:
6
+ - 0
7
+ - 1
8
+ - 10
9
+ - pre
10
+ version: 0.1.10.pre
11
+ platform: ruby
12
+ authors:
13
+ - Aman Gupta
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2010-02-04 00:00:00 +00:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: yajl-ruby
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ segments:
29
+ - 0
30
+ version: "0"
31
+ type: :runtime
32
+ version_requirements: *id001
33
+ description: A full-featured facebook extension for the sinatra webapp framework
34
+ email: aman@tmm1.net
35
+ executables: []
36
+
37
+ extensions: []
38
+
39
+ extra_rdoc_files: []
40
+
41
+ files:
42
+ - README
43
+ - sinbook.gemspec
44
+ - lib/sinbook.rb
45
+ - examples/simple.rb
46
+ - examples/connect.rb
47
+ has_rdoc: true
48
+ homepage: http://github.com/tmm1/sinbook
49
+ licenses: []
50
+
51
+ post_install_message:
52
+ rdoc_options: []
53
+
54
+ require_paths:
55
+ - lib
56
+ required_ruby_version: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ segments:
61
+ - 0
62
+ version: "0"
63
+ required_rubygems_version: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">"
66
+ - !ruby/object:Gem::Version
67
+ segments:
68
+ - 1
69
+ - 3
70
+ - 1
71
+ version: 1.3.1
72
+ requirements: []
73
+
74
+ rubyforge_project:
75
+ rubygems_version: 1.3.6
76
+ signing_key:
77
+ specification_version: 3
78
+ summary: simple sinatra facebook extension in 300 lines of ruby
79
+ test_files: []
80
+