fbauth 0.9.9.9 → 1.0.0.0
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.mdown +216 -31
- data/app/controllers/facebook_auth_functions.rb +4 -3
- data/lib/facebook_auth.rb +1 -1
- data/lib/facebook_graph.rb +16 -47
- data/lib/facebook_http.rb +87 -0
- data/lib/facebook_query.rb +16 -0
- data/lib/fbauth.rb +2 -0
- metadata +8 -6
data/README.mdown
CHANGED
@@ -1,54 +1,239 @@
|
|
1
|
-
FBAuth
|
2
|
-
======
|
1
|
+
# FBAuth #
|
3
2
|
|
4
|
-
This gem provides authentication and basic Facebook functions for your
|
3
|
+
This gem provides authentication and basic Facebook functions for your
|
4
|
+
Rails application.
|
5
5
|
|
6
|
-
The Authentication Challenge
|
7
|
-
----------------------------
|
6
|
+
## The Authentication Challenge ##
|
8
7
|
|
9
|
-
Facebook is an evolving platform, over the past couple years we've seen
|
10
|
-
third-party
|
8
|
+
Facebook is an evolving platform, over the past couple years we've seen
|
9
|
+
a lot of change in how it authenticates users of third-party
|
10
|
+
applications.
|
11
11
|
|
12
|
-
|
13
|
-
|
14
|
-
|
12
|
+
The documentation for Facebook is minimal, without historic reference or
|
13
|
+
in-depth discussion of the different implementation scenarios that our
|
14
|
+
applications may take.
|
15
15
|
|
16
|
-
|
17
|
-
|
18
|
-
the
|
16
|
+
For example, the preferred method of doing "Canvas" applications, those
|
17
|
+
that live within the Facebook "wrapper" is now to use an iFrame. If you
|
18
|
+
choose to utilize the Javascript Connect SDK and rely on its
|
19
|
+
authentication mechanisms this will fail under any browsers that block
|
20
|
+
cross-domain cookies - which is more prominent as time goes on due to
|
21
|
+
the security risks involved in allowing cross-domain cookies.
|
22
|
+
|
23
|
+
Therefore, as of this writing, applications using the prescribed iFrame
|
24
|
+
style and using the JS SDK will fail on mobile Safari, Safari on
|
25
|
+
Windows' default settings, and various mobile implementations of the
|
26
|
+
Android browser.
|
27
|
+
|
28
|
+
This plugin uses a few techniques to locate your "access token" and
|
29
|
+
prefers to use the OAuth API to get what you need as a Facebook app to
|
30
|
+
ensure your users have correctly added your app, authenticated in
|
31
|
+
Facebook, and to communicate with the Graph and Query APIs.
|
19
32
|
|
20
33
|
Here are the scenarios we currently handle:
|
21
34
|
|
22
|
-
|
23
|
-
-----------
|
35
|
+
__iFrame Apps__
|
24
36
|
|
25
|
-
- first page load as an iFrame app inside Facebook, where authentication
|
26
|
-
|
27
|
-
-
|
37
|
+
- first page load as an iFrame app inside Facebook, where authentication
|
38
|
+
params are sent in the URL used for your iFrame
|
39
|
+
- this is particularly required for mobile Safari and other browsers
|
40
|
+
blocking cross-domain cookies by default
|
41
|
+
- handles the old session parameter, as well as the new signed_request
|
42
|
+
parameter
|
28
43
|
|
29
44
|
- loading from the cookie initialized by the JavaScript API
|
30
45
|
- works great for browsers supporting cross-domain cookies by default
|
31
46
|
|
32
|
-
- the access token you get is time-limited, if it has expired you need
|
47
|
+
- the access token you get is time-limited, if it has expired you need
|
48
|
+
to be re-authenticated
|
49
|
+
|
50
|
+
__External (Connect) Apps__
|
51
|
+
|
52
|
+
- handling an OAuth exchange back & forth with Facebook to handle
|
53
|
+
authentication and capture URL parameters back for token
|
54
|
+
|
55
|
+
## Integration with Your Rails App ##
|
56
|
+
|
57
|
+
Add fbauth to your Gemfile
|
58
|
+
|
59
|
+
gem 'fbauth', '~> 1.0'
|
60
|
+
|
61
|
+
Create `config/facebook.yml`
|
62
|
+
|
63
|
+
development:
|
64
|
+
app_id: 'xxxxxxxxxxxxx'
|
65
|
+
app_context: 'my-app-dev'
|
66
|
+
auth_path: '/login'
|
67
|
+
canvas_url: 'http://dev.myapp.com/facebook'
|
68
|
+
app_secret: 'xxxxxxxxxxxx'
|
69
|
+
|
70
|
+
test:
|
71
|
+
app_id: 'fake_id'
|
72
|
+
app_context: 'my-app'
|
73
|
+
auth_path: '/login'
|
74
|
+
canvas_url: 'http://myapp.com/facebook'
|
75
|
+
app_secret: 'fake_secret'
|
76
|
+
|
77
|
+
production:
|
78
|
+
app_id: 'xxxxxxxxxxxxx'
|
79
|
+
app_context: 'my-app'
|
80
|
+
auth_path: '/login'
|
81
|
+
canvas_url: 'http://myapp.com/facebook'
|
82
|
+
app_secret: 'xxxxxxxxxxxx'
|
83
|
+
|
84
|
+
- `app_id` - this is your Facebook App ID
|
85
|
+
- `app_context` - this is your Canvas Page path, ie.
|
86
|
+
`http://apps.facebook.com/my-app-dev`
|
87
|
+
- `auth_path` - the path in your application to your login page (must be
|
88
|
+
a string, not a logical route name)
|
89
|
+
- `canvas_url` - this is the Facebook Canvas URL, the base URL that gets
|
90
|
+
to the Facebook iFrame pages for your Canvas app
|
91
|
+
- `app_secret` - the Facebook App Secret code for your application
|
92
|
+
|
93
|
+
Note: _We are assuming that you will be registering **two** facebook
|
94
|
+
applications, one that you will be using to actively develop your
|
95
|
+
application and the other for production use for your users._
|
96
|
+
|
97
|
+
Include the fbauth modules in your application controllers
|
98
|
+
|
99
|
+
include FacebookAuthFunctions
|
100
|
+
include FbauthHelper
|
101
|
+
|
102
|
+
In the controllers you want restricted to authorized users, or in your
|
103
|
+
`Application` controller, add the filters
|
104
|
+
|
105
|
+
before_filter :require_facebook_auth
|
106
|
+
|
107
|
+
Create the controller and method that will live at your `auth_path`
|
108
|
+
specified in your facebook.yml file.
|
109
|
+
|
110
|
+
class AuthController < ApplicationController
|
111
|
+
|
112
|
+
# Use this if you've included FacebookAuthFunctions in your
|
113
|
+
# Application controller...
|
114
|
+
# skip_before_filter :require_facebook_auth
|
115
|
+
|
116
|
+
def login
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
And then create your login template (we use HAML, but you can use ERB if
|
121
|
+
you want.
|
122
|
+
|
123
|
+
%div.fblogin
|
124
|
+
You are not logged in to Facebook, please click here:
|
125
|
+
%fb:login-button{:perms => 'publish_stream'} Log In to Facebook
|
126
|
+
|
127
|
+
%div.fbadd
|
128
|
+
Please add this Application - it's great!
|
129
|
+
%fb:login-button{:perms => 'publish_stream'} Add This Application
|
130
|
+
|
131
|
+
%div.fbready
|
132
|
+
Please wait...
|
33
133
|
|
34
|
-
|
35
|
-
-----------------------
|
134
|
+
= fbauth_login_javascript(:login => '.fblogin', :add => '.fbadd', :ready => '.fbready')
|
36
135
|
|
37
|
-
|
38
|
-
token
|
136
|
+
The three areas noted above are exposed in the following scenarios:
|
39
137
|
|
40
|
-
|
41
|
-
|
138
|
+
- `:login` - this element is exposed when the user has not logged into
|
139
|
+
Facebook, they may or may not have added your application but we don't
|
140
|
+
know that yet.
|
141
|
+
- `:add` - this element is exposed when the user is logged in to
|
142
|
+
Facebook but has not added / authorized your application. Note we are
|
143
|
+
requesting certain permissions when the app is added:
|
144
|
+
`<fb:login-button perms="publish_stream"></fb:login-button>`
|
145
|
+
- `:ready` - once the user has satisfied the pop-ups that appear when
|
146
|
+
logging in and/or adding the application, or if the user loads this
|
147
|
+
screen and is already fully authenticated, this element appears and
|
148
|
+
a redirect is done to your application root_path
|
42
149
|
|
43
|
-
|
44
|
-
to these questions yet:
|
150
|
+
## Using the Facebook APIs ##
|
45
151
|
|
46
|
-
|
152
|
+
### Graph API ###
|
153
|
+
|
154
|
+
The Graph API gives you the ability to request a great deal of
|
155
|
+
information about individual social relationships and perform basic
|
156
|
+
updates to that graph (ie. posting news-feed events).
|
157
|
+
|
158
|
+
Certain actions (posting changes, reading certain restricted attributes)
|
159
|
+
are only available if you provide an access token and make an
|
160
|
+
authenticated call.
|
161
|
+
|
162
|
+
__Unauthenticated Call__
|
163
|
+
|
164
|
+
graph = FacebookGraph.new
|
165
|
+
graph.call('svetzal')
|
166
|
+
|
167
|
+
This will retrieve publicly available information on the provided user's
|
168
|
+
Facebook UID or handle in a Ruby hash structure.
|
169
|
+
|
170
|
+
{"name"=>"Steven Vetzal", "gender"=>"male", "id"=>"849395216", "last_name"=>"Vetzal",
|
171
|
+
"locale"=>"en_US", "link"=>"http://www.facebook.com/svetzal", "first_name"=>"Steven"}
|
172
|
+
|
173
|
+
__Authenticated Call__
|
174
|
+
|
175
|
+
graph = FacebookGraph.new(fbauth.access_token)
|
176
|
+
graph.call('svetzal', { :scope => "birthday" })
|
177
|
+
|
178
|
+
This will make a similar call but as an authenticated caller we can
|
179
|
+
request a restricted scope, in this case information about the user's
|
180
|
+
birthday (if our app has requested the birthday permission from the
|
181
|
+
user).
|
182
|
+
|
183
|
+
### Query API (FQL) ###
|
184
|
+
|
185
|
+
__Unauthenticated Call__
|
186
|
+
|
187
|
+
query = FacebookQuery.new
|
188
|
+
query.fql('SELECT name, first_name, last_name FROM user WHERE uid in ("849395216","58001611","1018515154")')
|
189
|
+
|
190
|
+
FQL is handy for performing multiple lookups at once, and can save you a
|
191
|
+
lot of latency.
|
192
|
+
|
193
|
+
[
|
194
|
+
{"name"=>"Steven Vetzal", "last_name"=>"Vetzal", "first_name"=>"Steven"},
|
195
|
+
{"name"=>"Nate Smith", "last_name"=>"Smith", "first_name"=>"Nate"},
|
196
|
+
{"name"=>"Craig Savolainen", "last_name"=>"Savolainen", "first_name"=>"Craig"}
|
197
|
+
]
|
198
|
+
|
199
|
+
__Authenticated Call__
|
200
|
+
|
201
|
+
query = FacebookQuery.new(fbauth.access_token)
|
202
|
+
|
203
|
+
__Manually Building an Access Token__
|
204
|
+
|
205
|
+
If you scan your logs, you'll see the `signed_request` OAuth GET
|
206
|
+
parameter being sent to your application. You can manually build
|
207
|
+
yourself an authentication token from this using the following code.
|
208
|
+
|
209
|
+
data = FacebookDecoder.decode('{"signed_request"=>"oXWWpLi2tX7QW3cjX...(removed)"}')
|
210
|
+
fbauth = FacebookAuth.create(data)
|
211
|
+
fbauth.validate
|
212
|
+
|
213
|
+
The `.validate` step is optional, but pre-warns you if the token you are
|
214
|
+
about to use is good. If it is not, you can find out why by looking at
|
215
|
+
`fbauth.validation_error`.
|
216
|
+
|
217
|
+
# Things Remaining Unclear #
|
218
|
+
|
219
|
+
Documentation for the Facebook platform is a little fragmented, so we
|
220
|
+
haven't (that we recall) come across the answers to these questions yet:
|
221
|
+
|
222
|
+
- what timezone is the OAuth token expiry value in? (we get it in Epoch,
|
223
|
+
no TZ data, currently assuming San Francisco)
|
47
224
|
- what happens when time approaches the OAuth token expiry?
|
48
225
|
- do we get a new one?
|
49
226
|
- are we expected to stop functioning and redirect to a FB login?
|
50
227
|
|
51
|
-
Change Log
|
52
|
-
|
228
|
+
# Change Log #
|
229
|
+
|
230
|
+
v1.0.0.0
|
231
|
+
|
232
|
+
- Changed call semantics for FacebookGraph and FacebookQuery,
|
233
|
+
use objects instead of class methods
|
234
|
+
- Preparing for public release
|
53
235
|
|
54
|
-
v0.9.9.6
|
236
|
+
v0.9.9.6
|
237
|
+
|
238
|
+
- Raising info-rich exceptions when errors returned from
|
239
|
+
Facebook on graph calls
|
@@ -5,9 +5,6 @@ module FacebookAuthFunctions
|
|
5
5
|
end
|
6
6
|
|
7
7
|
def require_facebook_auth
|
8
|
-
# Prep IE so it will take our cookies in a Facebook iFrame
|
9
|
-
response.headers['P3P'] = 'CP="IDC DSP COR ADM DEVi TAIi PSA PSD IVAi IVDi CONi HIS OUR IND CNT"'
|
10
|
-
|
11
8
|
setup_facebook_auth
|
12
9
|
if @facebook_auth.nil?
|
13
10
|
redirect_to build_auth_url
|
@@ -21,6 +18,9 @@ private
|
|
21
18
|
end
|
22
19
|
|
23
20
|
def facebook_auth
|
21
|
+
# Prep IE so it will take our cookies in a Facebook iFrame
|
22
|
+
response.headers['P3P'] = 'CP="IDC DSP COR ADM DEVi TAIi PSA PSD IVAi IVDi CONi HIS OUR IND CNT"'
|
23
|
+
|
24
24
|
# If we have valid auth in session, use it
|
25
25
|
data = parse_session
|
26
26
|
auth = validate_and_save(data) unless data.nil?
|
@@ -70,6 +70,7 @@ private
|
|
70
70
|
parms = JSON.parse(params[:session])
|
71
71
|
logger.warn("Parsed facebook params from session parameter (deprecated)")
|
72
72
|
elsif params[:signed_request].present?
|
73
|
+
logger.warn("Found signed_request param")
|
73
74
|
begin
|
74
75
|
parms = FacebookDecoder.decode(params[:signed_request])
|
75
76
|
logger.warn("Parsed facebook params from signed_request parameter")
|
data/lib/facebook_auth.rb
CHANGED
@@ -36,7 +36,7 @@ class FacebookAuth
|
|
36
36
|
msgs = []
|
37
37
|
unless self.uid.nil? || self.access_token.nil?
|
38
38
|
begin
|
39
|
-
self.user_data = FacebookGraph.call(self.uid
|
39
|
+
self.user_data = FacebookGraph.new(self.access_token).call(self.uid)
|
40
40
|
rescue => e
|
41
41
|
msgs << "Error calling FacebookGraph - #{e}"
|
42
42
|
end
|
data/lib/facebook_graph.rb
CHANGED
@@ -2,56 +2,25 @@ require 'net/http'
|
|
2
2
|
require 'uri'
|
3
3
|
|
4
4
|
class FacebookGraph
|
5
|
+
include FacebookHttp
|
5
6
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
http = Net::HTTP.new uri.host, uri.port
|
16
|
-
begin
|
17
|
-
http.use_ssl = (uri.scheme == "https")
|
18
|
-
req = Net::HTTP::Get.new(uri.path)
|
19
|
-
response = http.request(req)
|
20
|
-
raise "Facebook error response #{response.code} - #{response.body}" unless response.code == '200'
|
21
|
-
begin
|
22
|
-
json = JSON.parse(response.body)
|
23
|
-
rescue => e
|
24
|
-
raise "Error parsing Facebook response: #{response.body}"
|
25
|
-
end
|
26
|
-
ensure
|
27
|
-
http.finish if http.started?
|
28
|
-
end
|
29
|
-
json
|
7
|
+
FB_GRAPH_URL = "https://graph.facebook.com"
|
8
|
+
|
9
|
+
def initialize(access_token = nil, options = {})
|
10
|
+
@options = options.merge({ :access_token => access_token })
|
11
|
+
end
|
12
|
+
|
13
|
+
# Generic Graph call to lookup data for any path
|
14
|
+
def call(path, options = {})
|
15
|
+
get "#{FB_GRAPH_URL}/#{path}", merged_options(options)
|
30
16
|
end
|
31
17
|
|
32
|
-
#
|
33
|
-
# message, picture, link, name, caption, description
|
34
|
-
def
|
35
|
-
|
36
|
-
if %w{staging production}.include?
|
37
|
-
|
38
|
-
uri = URI.parse(url)
|
39
|
-
http = Net::HTTP.new uri.host, uri.port
|
40
|
-
begin
|
41
|
-
http.use_ssl = (uri.scheme == "https")
|
42
|
-
req = Net::HTTP::Post.new(uri.path)
|
43
|
-
req.set_form_data(options)
|
44
|
-
response = http.request(req)
|
45
|
-
raise "Facebook error response #{response.code} - #{response.body}" unless response.code == '200'
|
46
|
-
begin
|
47
|
-
json = JSON.parse(response.body)
|
48
|
-
rescue => e
|
49
|
-
raise "Error parsing Facebook response: #{response.body}"
|
50
|
-
end
|
51
|
-
ensure
|
52
|
-
http.finish if http.started?
|
53
|
-
end
|
54
|
-
json
|
18
|
+
# Post item to member's wall
|
19
|
+
# Available options: message, picture, link, name, caption, description
|
20
|
+
def publish_to_member_feed(uid, options)
|
21
|
+
raise "access_token required" unless has_access_token?(options)
|
22
|
+
if %w{staging production}.include? Rails.env
|
23
|
+
post "#{FB_GRAPH_URL}/#{uid}/feed", merged_options(options)
|
55
24
|
end
|
56
25
|
end
|
57
26
|
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
require 'benchmark'
|
2
|
+
require 'net/http'
|
3
|
+
require 'uri'
|
4
|
+
require 'cgi'
|
5
|
+
|
6
|
+
module FacebookHttp
|
7
|
+
|
8
|
+
def build_get_url(url, params = {})
|
9
|
+
q = build_query_string(params)
|
10
|
+
if q
|
11
|
+
url + q
|
12
|
+
else
|
13
|
+
url
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def get(url, params = {})
|
18
|
+
json = nil
|
19
|
+
uri = URI.parse(build_get_url(url, params))
|
20
|
+
bench = Benchmark.measure do
|
21
|
+
http = Net::HTTP.new uri.host, uri.port
|
22
|
+
begin
|
23
|
+
http.use_ssl = (uri.scheme == "https")
|
24
|
+
req = Net::HTTP::Get.new(uri.request_uri)
|
25
|
+
response = http.request(req)
|
26
|
+
raise "Facebook error response #{response.code} - #{response.body}" unless response.code == '200'
|
27
|
+
begin
|
28
|
+
json = JSON.parse(response.body)
|
29
|
+
rescue => e
|
30
|
+
raise "Error parsing facebook response: #{response.body}"
|
31
|
+
end
|
32
|
+
ensure
|
33
|
+
http.finish if http.started?
|
34
|
+
end
|
35
|
+
end
|
36
|
+
logger.warn("Facebook GET call to #{uri.to_s} completed in #{bench.total} seconds")
|
37
|
+
json
|
38
|
+
end
|
39
|
+
|
40
|
+
def post(url, params = {})
|
41
|
+
json = nil
|
42
|
+
uri = URI.parse(url)
|
43
|
+
bench = Benchmark.measure do
|
44
|
+
http = Net::HTTP.new uri.host, uri.port
|
45
|
+
begin
|
46
|
+
http.use_ssl = (uri.scheme == "https")
|
47
|
+
req = Net::HTTP::Post.new(uri.path)
|
48
|
+
req.set_form_data(params)
|
49
|
+
response = http.request(req)
|
50
|
+
raise "Facebook error response #{response.code} - #{response.body}" unless response.code == '200'
|
51
|
+
begin
|
52
|
+
json = JSON.parse(response.body)
|
53
|
+
rescue => e
|
54
|
+
raise "Error parsing Facebook response: #{response.body}"
|
55
|
+
end
|
56
|
+
ensure
|
57
|
+
http.finish if http.started?
|
58
|
+
end
|
59
|
+
end
|
60
|
+
logger.warn("Facebook POST call to #{uri.to_s} completed in #{bench.total} seconds")
|
61
|
+
json
|
62
|
+
end
|
63
|
+
|
64
|
+
def build_query_string options={}
|
65
|
+
params = []
|
66
|
+
str_keys = options.keys.collect{ |k| k.to_s }
|
67
|
+
str_keys.sort.each do |str_key|
|
68
|
+
key = str_key.to_sym
|
69
|
+
value = options[key]
|
70
|
+
params << "#{key.to_s}=#{URI.escape(value.to_s)}" unless value.nil?
|
71
|
+
end
|
72
|
+
"?#{params.join('&')}" unless params.empty?
|
73
|
+
end
|
74
|
+
|
75
|
+
def merged_options(options = {})
|
76
|
+
options.merge!(@options) if @options
|
77
|
+
options
|
78
|
+
end
|
79
|
+
|
80
|
+
def has_access_token?(options = {})
|
81
|
+
merged_options.has_key? :access_token
|
82
|
+
end
|
83
|
+
|
84
|
+
def logger
|
85
|
+
Rails.logger
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'uri'
|
2
|
+
|
3
|
+
class FacebookQuery
|
4
|
+
include FacebookHttp
|
5
|
+
|
6
|
+
FB_API_URL = "https://api.facebook.com/method/fql.query"
|
7
|
+
|
8
|
+
def initialize(access_token = nil, options = {})
|
9
|
+
@options = options.merge({ :access_token => access_token })
|
10
|
+
@options.merge!({ :format => "JSON" }) unless @options.has_key?(:format)
|
11
|
+
end
|
12
|
+
|
13
|
+
def fql(query, options = {})
|
14
|
+
get FB_API_URL, merged_options(options.merge({ :query => query }))
|
15
|
+
end
|
16
|
+
end
|
data/lib/fbauth.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: fbauth
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 95
|
5
5
|
prerelease: false
|
6
6
|
segments:
|
7
|
+
- 1
|
7
8
|
- 0
|
8
|
-
-
|
9
|
-
-
|
10
|
-
|
11
|
-
version: 0.9.9.9
|
9
|
+
- 0
|
10
|
+
- 0
|
11
|
+
version: 1.0.0.0
|
12
12
|
platform: ruby
|
13
13
|
authors:
|
14
14
|
- Three Wise Men Inc.
|
@@ -16,7 +16,7 @@ autorequire:
|
|
16
16
|
bindir: bin
|
17
17
|
cert_chain: []
|
18
18
|
|
19
|
-
date: 2011-
|
19
|
+
date: 2011-03-03 00:00:00 -05:00
|
20
20
|
default_executable:
|
21
21
|
dependencies: []
|
22
22
|
|
@@ -33,6 +33,8 @@ files:
|
|
33
33
|
- lib/facebook_config.rb
|
34
34
|
- lib/facebook_decoder.rb
|
35
35
|
- lib/facebook_graph.rb
|
36
|
+
- lib/facebook_http.rb
|
37
|
+
- lib/facebook_query.rb
|
36
38
|
- lib/fbauth.rb
|
37
39
|
- app/controllers/facebook_auth_functions.rb
|
38
40
|
- app/helpers/fbauth_helper.rb
|