sinbook 0.1.5

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,107 @@
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
+ === Local Development
78
+
79
+ To develop locally, use ssh to setup a reverse tunnel to your external server.
80
+ For example, set your callback url to http://myserver.com:4567/, and run:
81
+
82
+ ssh -gNR 4567:localhost:4567 me@myserver.com
83
+
84
+ Then, simply launch sinatra on your local machine. Facebook will make requests to
85
+ http://myserver.com:4567/ which will be forwarded to port 4567 on your local machine.
86
+
87
+
88
+
89
+ === Other Options
90
+
91
+ facebook do
92
+ symbolize_keys true
93
+ end
94
+
95
+ >> fb.groups.get :uid => 1234
96
+ => [{:name => 'Sinatra Users'}]
97
+
98
+
99
+ === TODO
100
+
101
+ * Split out facebook api client so it can be used outside sinatra
102
+ * Add a batch mode for api calls:
103
+
104
+ groups, pics = fb.batch do |b|
105
+ b.groups.get :uid => 123
106
+ b.users.getInfo :uids => 123, :fields => [:pic_square]
107
+ 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.valid? and 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
@@ -0,0 +1,355 @@
1
+ begin
2
+ require 'sinatra/base'
3
+ rescue LoadError
4
+ retry if require 'rubygems'
5
+ raise
6
+ end
7
+
8
+ module Sinatra
9
+ require 'digest/md5'
10
+ require 'yajl'
11
+
12
+ class FacebookObject
13
+ def initialize app
14
+ @app = app
15
+
16
+ @api_key = app.options.facebook_api_key
17
+ @secret = app.options.facebook_secret
18
+ @app_id = app.options.facebook_app_id
19
+ @url = app.options.facebook_url
20
+ @callback = app.options.facebook_callback
21
+ @symbolize_keys = app.options.facebook_symbolize_keys || false
22
+ end
23
+ attr_reader :app
24
+ attr_accessor :api_key, :secret
25
+ attr_writer :url, :callback, :app_id
26
+
27
+ def app_id
28
+ @app_id || self[:app_id]
29
+ end
30
+
31
+ def url postfix=nil
32
+ postfix ? "#{@url}#{postfix}" : @url
33
+ end
34
+
35
+ def callback postfix=nil
36
+ postfix ? "#{@callback}#{postfix}" : @callback
37
+ end
38
+
39
+ def addurl
40
+ "http://apps.facebook.com/add.php?api_key=#{self.api_key}"
41
+ end
42
+
43
+ def appurl
44
+ "http://www.facebook.com/apps/application.php?id=#{self.app_id}"
45
+ end
46
+
47
+ def require_login!
48
+ if valid?
49
+ redirect addurl unless params[:user]
50
+ else
51
+ app.redirect url
52
+ end
53
+ end
54
+
55
+ def redirect url
56
+ url = self.url + url unless url =~ /^http/
57
+ app.body "<fb:redirect url='#{url}'/>"
58
+ throw :halt
59
+ end
60
+
61
+ def params
62
+ return {} unless valid?
63
+ app.env['facebook.params'] ||= \
64
+ app.env['facebook.vars'].inject({}) do |h,(k,v)|
65
+ s = k.to_sym
66
+ case k
67
+ when 'friends'
68
+ h[s] = v.split(',').map{|e|e.to_i}
69
+ when /time$/
70
+ h[s] = Time.at(v.to_f)
71
+ when 'expires'
72
+ v = v.to_i
73
+ h[s] = v>0 ? Time.at(v) : v
74
+ when 'user', 'app_id', 'canvas_user'
75
+ h[s] = v.to_i
76
+ when /^(logged_out|position_|in_|is_|added)/
77
+ h[s] = v=='1'
78
+ else
79
+ h[s] = v
80
+ end
81
+ h
82
+ end
83
+ end
84
+
85
+ def [] key
86
+ params[key]
87
+ end
88
+
89
+ def valid?
90
+ if app.params['fb_sig'] # canvas/iframe mode
91
+ prefix = 'fb_sig'
92
+ vars = app.params
93
+ elsif app.request.cookies[api_key] # fbconnect mode
94
+ prefix = api_key
95
+ vars = app.request.cookies
96
+ else
97
+ return false
98
+ end
99
+
100
+ if app.env['facebook.valid?'].nil?
101
+ fbvars = {}
102
+ sig = Digest::MD5.hexdigest(vars.map{|k,v|
103
+ if k =~ /^#{prefix}_(.+)$/
104
+ fbvars[$1] = v
105
+ "#{$1}=#{v}"
106
+ end
107
+ }.compact.sort.join+self.secret)
108
+
109
+ if app.env['facebook.valid?'] = (vars[prefix] == sig)
110
+ app.env['facebook.vars'] = fbvars
111
+ end
112
+ end
113
+
114
+ app.env['facebook.valid?']
115
+ end
116
+
117
+ class APIProxy
118
+ Types = %w[
119
+ admin
120
+ application
121
+ auth
122
+ batch
123
+ comments
124
+ connect
125
+ data
126
+ events
127
+ fbml
128
+ feed
129
+ fql
130
+ friends
131
+ groups
132
+ links
133
+ liveMessage
134
+ notes
135
+ notifications
136
+ pages
137
+ photos
138
+ profile
139
+ sms
140
+ status
141
+ stream
142
+ users
143
+ video
144
+ ]
145
+
146
+ alias :__class__ :class
147
+ alias :__inspect__ :inspect
148
+ instance_methods.each { |m| undef_method m unless m =~ /^__/ }
149
+ alias :inspect :__inspect__
150
+
151
+ def initialize name, obj
152
+ @name, @obj = name, obj
153
+ end
154
+
155
+ def method_missing method, opts = {}
156
+ @obj.request "#{@name}.#{method}", opts
157
+ end
158
+ end
159
+
160
+ APIProxy::Types.each do |n|
161
+ class_eval %[
162
+ def #{n}
163
+ (@proxies||={})[:#{n}] ||= APIProxy.new(:#{n}, self)
164
+ end
165
+ ]
166
+ end
167
+
168
+ def request method, opts = {}
169
+ if method == 'photos.upload'
170
+ image = opts.delete :image
171
+ end
172
+
173
+ opts = { :api_key => self.api_key,
174
+ :call_id => Time.now.to_f,
175
+ :format => 'JSON',
176
+ :v => '1.0',
177
+ :session_key => %w[ photos.upload ].include?(method) ? nil : params[:session_key],
178
+ :method => method }.merge(opts)
179
+
180
+ args = opts.map{ |k,v|
181
+ next nil unless v
182
+
183
+ "#{k}=" + case v
184
+ when Hash
185
+ Yajl::Encoder.encode(v)
186
+ when Array
187
+ if k == :tags
188
+ Yajl::Encoder.encode(v)
189
+ else
190
+ v.join(',')
191
+ end
192
+ else
193
+ v.to_s
194
+ end
195
+ }.compact.sort
196
+
197
+ sig = Digest::MD5.hexdigest(args.join+self.secret)
198
+
199
+ if method == 'photos.upload'
200
+ data = MimeBoundary
201
+ data += opts.merge(:sig => sig).inject('') do |buf, (key, val)|
202
+ if val
203
+ buf << (MimePart % [key, val])
204
+ else
205
+ buf
206
+ end
207
+ end
208
+ data += MimeImage % ['upload.jpg', 'jpg', image.respond_to?(:read) ? image.read : image]
209
+ else
210
+ data = Array["sig=#{sig}", *args.map{|a| a.gsub('&','%26') }].join('&')
211
+ end
212
+
213
+ ret = self.class.request(data, method == 'photos.upload')
214
+
215
+ ret = if ['true', '1'].include? ret
216
+ true
217
+ elsif ['false', '0'].include? ret
218
+ false
219
+ elsif (n = Integer(ret) rescue nil)
220
+ n
221
+ else
222
+ Yajl::Parser.parse(ret, :symbolize_keys => @symbolize_keys)
223
+ end
224
+
225
+ raise Facebook::Error, ret['error_msg'] if ret.is_a? Hash and ret['error_code']
226
+
227
+ ret
228
+ end
229
+
230
+ MimeBoundary = "--SoMeTeXtWeWiLlNeVeRsEe\r\n"
231
+ MimePart = %[Content-Disposition: form-data; name="%s"\r\n\r\n%s\r\n] + MimeBoundary
232
+ MimeImage = %[Content-Disposition: form-data; filename="%s"\r\nContent-Type: image/%s\r\n\r\n%s\r\n] + MimeBoundary
233
+
234
+ require 'resolv'
235
+ API_SERVER = Resolv.getaddress('api.facebook.com')
236
+ @keepalive = false
237
+
238
+ def self.connect
239
+ sock = TCPSocket.new(API_SERVER, 80)
240
+ begin
241
+ timeout = [3,0].pack('l_2') # 3 seconds
242
+ sock.setsockopt Socket::SOL_SOCKET, Socket::SO_RCVTIMEO, timeout
243
+ sock.setsockopt Socket::SOL_SOCKET, Socket::SO_SNDTIMEO, timeout
244
+ rescue Exception => ex
245
+ # causes issues on solaris?
246
+ puts ex.inspect
247
+ end
248
+ sock
249
+ end
250
+
251
+ def self.request data, mime=false
252
+ if @keepalive
253
+ @socket ||= connect
254
+ else
255
+ @socket = connect
256
+ end
257
+
258
+ @socket.print "POST /restserver.php HTTP/1.1\r\n"
259
+ @socket.print "Host: api.facebook.com\r\n"
260
+ @socket.print "Connection: keep-alive\r\n" if @keepalive
261
+ if mime
262
+ @socket.print "Content-Type: multipart/form-data; boundary=#{MimeBoundary[2..-3]}\r\n"
263
+ @socket.print "MIME-version: 1.0\r\n"
264
+ else
265
+ @socket.print "Content-Type: application/x-www-form-urlencoded\r\n"
266
+ end
267
+ @socket.print "Content-Length: #{data.length}\r\n"
268
+ @socket.print "\r\n#{data}\r\n"
269
+ @socket.print "\r\n\r\n"
270
+
271
+ buf = ''
272
+ headers = ''
273
+ headers_done = false
274
+ chunked = true
275
+
276
+ while true
277
+ line = @socket.gets
278
+ headers << line unless headers_done
279
+ raise Errno::ECONNRESET unless line
280
+
281
+ if line == "\r\n" # end of headers/chunk
282
+ unless headers_done
283
+ headers_done = true
284
+ if headers =~ /Encoding: chunked/i
285
+ chunked = true
286
+ else
287
+ len = headers[/Content-Length: (\d+)/i,1].to_i
288
+ buf = @socket.read(len)
289
+ break # done!
290
+ end
291
+ end
292
+
293
+ line = @socket.gets # get size of next chunk
294
+ if line.strip! == '0' # 0 sized chunk
295
+ @socket.gets # read last crlf
296
+ break # done!
297
+ end
298
+
299
+ buf << @socket.read(line.to_i(16)) # read in chunk
300
+ end
301
+ end
302
+
303
+ buf
304
+ rescue Errno::EPIPE, Errno::ECONNRESET
305
+ @socket = nil
306
+ retry
307
+ ensure
308
+ @socket.close if @socket and !@keepalive
309
+ end
310
+ end
311
+
312
+ module FacebookHelper
313
+ def facebook
314
+ env['facebook.helper'] ||= FacebookObject.new(self)
315
+ end
316
+ alias fb facebook
317
+ end
318
+
319
+ class FacebookSettings
320
+ def initialize app, &blk
321
+ @app = app
322
+ @app.set :facebook_symbolize_keys, false
323
+ instance_eval &blk
324
+ end
325
+ %w[ api_key secret app_id url callback symbolize_keys ].each do |param|
326
+ class_eval %[
327
+ def #{param} val, &blk
328
+ @app.set :facebook_#{param}, val
329
+ end
330
+ ]
331
+ end
332
+ end
333
+
334
+ module Facebook
335
+ class Error < StandardError; end
336
+
337
+ def facebook &blk
338
+ FacebookSettings.new(self, &blk)
339
+ end
340
+
341
+ def self.registered app
342
+ app.helpers FacebookHelper
343
+ app.before(&method(:fix_request_method))
344
+ app.disable :sessions
345
+ end
346
+
347
+ def self.fix_request_method app
348
+ if method = app.request.params['fb_sig_request_method']
349
+ app.request.env['REQUEST_METHOD'] = method
350
+ end
351
+ end
352
+ end
353
+
354
+ Application.register Facebook
355
+ end
@@ -0,0 +1,24 @@
1
+ spec = Gem::Specification.new do |s|
2
+ s.name = 'sinbook'
3
+ s.version = '0.1.5'
4
+ s.date = '2009-11-25'
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,68 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sinbook
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.5
5
+ platform: ruby
6
+ authors:
7
+ - Aman Gupta
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-11-25 00:00:00 -05:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: yajl-ruby
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0"
24
+ version:
25
+ description: A full-featured facebook extension for the sinatra webapp framework
26
+ email: aman@tmm1.net
27
+ executables: []
28
+
29
+ extensions: []
30
+
31
+ extra_rdoc_files: []
32
+
33
+ files:
34
+ - README
35
+ - sinbook.gemspec
36
+ - lib/sinbook.rb
37
+ - examples/simple.rb
38
+ - examples/connect.rb
39
+ has_rdoc: true
40
+ homepage: http://github.com/tmm1/sinbook
41
+ licenses: []
42
+
43
+ post_install_message:
44
+ rdoc_options: []
45
+
46
+ require_paths:
47
+ - lib
48
+ required_ruby_version: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: "0"
53
+ version:
54
+ required_rubygems_version: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: "0"
59
+ version:
60
+ requirements: []
61
+
62
+ rubyforge_project:
63
+ rubygems_version: 1.3.4
64
+ signing_key:
65
+ specification_version: 3
66
+ summary: simple sinatra facebook extension in 300 lines of ruby
67
+ test_files: []
68
+