slaskis-sinbook 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README +86 -0
- data/examples/simple.rb +60 -0
- data/lib/sinbook.rb +314 -0
- data/sinbook.gemspec +22 -0
- 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
|
data/examples/simple.rb
ADDED
@@ -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
|
+
|