slaskis-sinbook 0.1.1

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