slaskis-sinbook 0.1.1

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