lpetre-sinbook 0.1.10.pre

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