fogli 0.1.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.
Files changed (49) hide show
  1. data/.gitignore +5 -0
  2. data/Gemfile +14 -0
  3. data/LICENSE +21 -0
  4. data/README.md +168 -0
  5. data/Rakefile +37 -0
  6. data/VERSION +1 -0
  7. data/examples/README.md +6 -0
  8. data/examples/me/README.md +18 -0
  9. data/examples/me/me.rb +30 -0
  10. data/lib/fogli.rb +46 -0
  11. data/lib/fogli/album.rb +8 -0
  12. data/lib/fogli/categorized_object.rb +9 -0
  13. data/lib/fogli/comment.rb +5 -0
  14. data/lib/fogli/event.rb +9 -0
  15. data/lib/fogli/exception.rb +15 -0
  16. data/lib/fogli/facebook_graph.rb +125 -0
  17. data/lib/fogli/facebook_object.rb +225 -0
  18. data/lib/fogli/facebook_object/connection_proxy.rb +80 -0
  19. data/lib/fogli/facebook_object/connection_scope.rb +123 -0
  20. data/lib/fogli/facebook_object/connections.rb +56 -0
  21. data/lib/fogli/facebook_object/properties.rb +81 -0
  22. data/lib/fogli/facebook_object/scope_methods.rb +49 -0
  23. data/lib/fogli/group.rb +8 -0
  24. data/lib/fogli/link.rb +8 -0
  25. data/lib/fogli/named_object.rb +8 -0
  26. data/lib/fogli/note.rb +7 -0
  27. data/lib/fogli/oauth.rb +167 -0
  28. data/lib/fogli/page.rb +15 -0
  29. data/lib/fogli/photo.rb +8 -0
  30. data/lib/fogli/post.rb +18 -0
  31. data/lib/fogli/status.rb +7 -0
  32. data/lib/fogli/user.rb +36 -0
  33. data/lib/fogli/util/module_attributes.rb +61 -0
  34. data/lib/fogli/util/options.rb +32 -0
  35. data/lib/fogli/video.rb +7 -0
  36. data/test/fogli/facebook_graph_test.rb +124 -0
  37. data/test/fogli/facebook_object/connection_proxy_test.rb +42 -0
  38. data/test/fogli/facebook_object/connection_scope_test.rb +91 -0
  39. data/test/fogli/facebook_object/connections_test.rb +50 -0
  40. data/test/fogli/facebook_object/properties_test.rb +79 -0
  41. data/test/fogli/facebook_object/scope_methods_test.rb +69 -0
  42. data/test/fogli/facebook_object_test.rb +178 -0
  43. data/test/fogli/oauth_test.rb +56 -0
  44. data/test/fogli/post_test.rb +18 -0
  45. data/test/fogli/user_test.rb +25 -0
  46. data/test/fogli/util/options_test.rb +38 -0
  47. data/test/fogli_test.rb +16 -0
  48. data/test/test_helper.rb +14 -0
  49. metadata +150 -0
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ .bundle
2
+ *.lock
3
+ test.rb
4
+ .yardoc/
5
+ doc/
data/Gemfile ADDED
@@ -0,0 +1,14 @@
1
+ source "http://rubygems.org"
2
+
3
+ gem "rest-client", "~> 1.5.1"
4
+ gem "json_pure", "~> 1.2.0"
5
+
6
+ group :development do
7
+ gem "sinatra" # for examples
8
+
9
+ gem "yard"
10
+ gem "bluecloth"
11
+ gem "contest", ">= 0.1.2"
12
+ gem "mocha"
13
+ gem "jeweler", "~> 1.4.0"
14
+ end
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License
2
+
3
+ Copyright (c) 2010 Mitchell Hashimoto
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,168 @@
1
+ # Fogli
2
+
3
+ Fogli is a **F**acebook **O**pen **G**raph **Li**brary.
4
+
5
+ ## Why?
6
+
7
+ Facebook introduced the Graph API in April, 2010. This API is intended
8
+ to replace their existing "RESTful API" in the future. Mature Facebook
9
+ libraries such as [Facebooker](http://github.com/mmangino/facebooker) are
10
+ heavily tied to the RESTful API and have not started supporting the Open
11
+ Graph.
12
+
13
+ At the time of Fogli's creation, there were a handful of other libraries
14
+ claiming to be Facebook Graph libraries, but I found them unsatisfactory
15
+ since they did not meet any or all of the following requirements:
16
+
17
+ * Minimizes query calls
18
+ * Strong feature support
19
+ * Intuitive object model
20
+ * Independent library; not tied to any web framework
21
+
22
+ Fogli aims to expose the objects of the Facebook Graph through an intuitive
23
+ object oriented interface while minimizing the number of queries needed
24
+ to access data through lazy loading and caching. Fogli is not tied to any
25
+ web framework and can run standalone perfectly fine.
26
+
27
+ ## Supported Features
28
+
29
+ Below is a list of supported Facebook Graph features:
30
+
31
+ * Authorization via OAuth
32
+ * Reading objects (`User`, `Page`, etc.)
33
+ * Reading object connections
34
+ * Deleting objects
35
+ * Scoping top level objects by `fields`
36
+ * Scoping connections by `limit`, `offset`, `since`, `until`, and `fields`
37
+ * Liking/unliking posts
38
+
39
+ The list below is a list of unsupported features, but development is
40
+ planned in the near future:
41
+
42
+ * More support for publishing (posting comments, attending events, etc.)
43
+ * Search
44
+ * Real time updates
45
+ * Analytics
46
+
47
+ Have a feature request? Submit it on the [GitHub Issues](https://github.com/mitchellh/fogli/issues)
48
+ page, or fork and send me a patch!
49
+
50
+ ## Getting Started
51
+
52
+ Install Fogli:
53
+
54
+ gem install fogli
55
+
56
+ Next, you can do a few things:
57
+
58
+ * Open an IRB session and begin playing. Fogli isn't tied to any framework,
59
+ so you can see it work right away:
60
+
61
+ >> require "fogli"
62
+ >> m = Fogli::User["mitchellh"]
63
+ >> m.name
64
+ => "Mitchell Hashimoto"
65
+
66
+ * Run the examples in the `examples/` directory to see how to access basic
67
+ connections and also use {Fogli::OAuth OAuth} to access authorized data.
68
+
69
+ * Read the documentation here! For more specific, class by class documentation,
70
+ please read [the source-generated documentation](http://mitchellh.github.com/fogli/).
71
+
72
+ ## Reading Data and Accessing Connections
73
+
74
+ Reading public information is straightforward and requires no configuration:
75
+
76
+ u = Fogli::User["mitchellh"]
77
+ p u.first_name # "Mitchell"
78
+ p u.last_name # "Hashimoto"
79
+
80
+ Accessing connections (relationships to data) is also intuitive and
81
+ easy:
82
+
83
+ u.feed.each do |item|
84
+ p item
85
+ end
86
+
87
+ ## Authentication, Authorization, and Accessing Private Data
88
+
89
+ To access private data, Facebook requires that the you gain an `access_token`
90
+ by asking the user to authorize your application. This is a two-step process.
91
+ The example before uses [Sinatra](http://www.sinatrarb.com/) with Fogli to
92
+ authorize a user and print his or her email:
93
+
94
+ # Set these configuration values. The keys are retrieved from
95
+ # registering an application with Facebook. These can all be
96
+ # set on the actual method calls later as well.
97
+ Fogli.client_id = "..."
98
+ Fogli.client_secret = "..."
99
+ Fogli.redirect_uri = "http://my-website.com/verify"
100
+
101
+ get "/" do
102
+ # Redirect the user to authorized, asking for email permissions
103
+ redirect Fogli::OAuth.authorize(:scope => "email")
104
+ end
105
+
106
+ get "/verify"
107
+ # Facebook redirects back to this page with a GET parameter `code`
108
+ # which is used to get the access token. This token should also be
109
+ # stored in a cookie or some session storage to be set on subsequent pages.
110
+ Fogli.access_token = Fogli::OAuth.access_token(:code => params[:code])
111
+
112
+ # Print authorized data to prove we're allowed!
113
+ Fogli::User[:me].email
114
+ end
115
+
116
+ Checking if a user is already authenciated is easy as well:
117
+
118
+ # Set from cookies, perhaps
119
+ Fogli.access_token = cookies[:access_token]
120
+
121
+ # Verify its valid
122
+ if Fogli::User.authorized?
123
+ # valid!
124
+ else
125
+ # reauthenticate
126
+ end
127
+
128
+ ## Accessing Connections with Scopes
129
+
130
+ Facebook data connections are typically relationships to massive amounts
131
+ of data, most of which you're not interested in. By scoping the data, you
132
+ can limit the data to only what you want, and optimize the queries made by
133
+ Fogli in the process:
134
+
135
+ # Assuming we're authenticated (above)
136
+ u = Fogli::User[:me]
137
+
138
+ # We just want feed times since yesterday, and we're only interested
139
+ # in 10 of them:
140
+ u.feed.since("yesterday").limit(10).each do |item|
141
+ # Do something with the item
142
+ end
143
+
144
+ # Another example: We want to access all the user's friends, but only
145
+ # want their first and last name:
146
+ u.friends.fields(:first_name, :last_name).each do |friend|
147
+ p "Friend: #{friend.first_name} #{friend.last_name}"
148
+ end
149
+
150
+ ## Lazy Loading and Caching
151
+
152
+ Everything in Fogli is lazy-loaded. This means that data is loaded on
153
+ demand and no queries are made until you need the data. So, how many
154
+ HTTP queries do you think `User[:me].feed.since("yesterday")` takes?
155
+ Since no property was ever access on the user, only 1 query is used
156
+ (to get the `feed` items since yesterday).
157
+
158
+ Additionally, once the data is loaded, it is cached to minimize the
159
+ number of queries needed:
160
+
161
+ # This is also only one query, the entire script, even though the
162
+ # items are accessed 4 times.
163
+ items = Fogli::User[:me].feed.since("yesterday")
164
+ items.each {}
165
+ items.each {}
166
+ items.each {}
167
+ items.each {}
168
+
data/Rakefile ADDED
@@ -0,0 +1,37 @@
1
+ require 'rake/testtask'
2
+
3
+ begin
4
+ require 'jeweler'
5
+ Jeweler::Tasks.new do |gem|
6
+ gem.name = "fogli"
7
+ gem.summary = "An efficient, simple, and intuitive Facebook Open Graph library."
8
+ gem.description = "An efficient, simple, and intuitive Facebook Open Graph library."
9
+ gem.email = "mitchell.hashimoto@gmail.com"
10
+ gem.homepage = "http://github.com/mitchellh/fogli"
11
+ gem.authors = ["Mitchell Hashimoto"]
12
+
13
+ gem.add_dependency('rest-client', "~> 1.5.1")
14
+ gem.add_dependency('json_pure', "~> 1.2.0")
15
+ end
16
+ Jeweler::GemcutterTasks.new
17
+ rescue LoadError
18
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
19
+ end
20
+
21
+ task :default => :test
22
+
23
+ Rake::TestTask.new do |t|
24
+ t.libs << "libs"
25
+ t.libs << "test"
26
+ t.pattern = 'test/**/*_test.rb'
27
+ end
28
+
29
+ begin
30
+ require 'yard'
31
+ YARD::Rake::YardocTask.new do |t|
32
+ t.options = ['--main', 'README.md', '--markup', 'markdown']
33
+ t.options += ['--title', 'Fogli Documentation']
34
+ end
35
+ rescue LoadError
36
+ puts "Yard not available. Install it with: gem install yard bluecloth"
37
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
@@ -0,0 +1,6 @@
1
+ # Fogli Examples
2
+
3
+ The examples are organized into directories by their name. Please
4
+ view the `README` files in an example's respective directory for
5
+ more information on that example.
6
+
@@ -0,0 +1,18 @@
1
+ # Fogli "Me" Example
2
+
3
+ The "Me" example shows how to use Fogli to access data about the
4
+ `me` pseudo-user on Facebook which represents the currently authorized
5
+ user.
6
+
7
+ ## Highlights
8
+
9
+ * OAuth authentication
10
+ * Accessing authorized data
11
+
12
+ ## Running
13
+
14
+ To run the example, make sure you have [Sinatra](http://www.sinatrarb.com)
15
+ installed, then enter this directory and run the main file:
16
+
17
+ $ ruby me.rb
18
+
data/examples/me/me.rb ADDED
@@ -0,0 +1,30 @@
1
+ require 'rubygems'
2
+ require 'sinatra'
3
+
4
+ # Make sure we load the most recent fogli that this is packaged with
5
+ # if its available.
6
+ $:.unshift(File.join(File.dirname(__FILE__), *%W[.. .. lib]))
7
+ require 'fogli'
8
+
9
+ #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!#
10
+ # IMPORTANT: Set your app configuration details here:
11
+ #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!#
12
+ Fogli.client_id = "132862110062471"
13
+ Fogli.client_secret = "3f2f371b6f01c449eb3bcfc841b1b9c1"
14
+
15
+ # No need to edit this:
16
+ Fogli.redirect_uri = "http://localhost:4567/verify"
17
+
18
+ get "/" do
19
+ # Authorize the user so we can get an access token
20
+ redirect Fogli::OAuth.authorize(:scope => "email")
21
+ end
22
+
23
+ get "/verify" do
24
+ # Get and store the access token, then print out their user
25
+ # information to verify it worked
26
+ Fogli.access_token = Fogli::OAuth.access_token(:code => params[:code])
27
+
28
+ # Output authorized information to verify it worked
29
+ Fogli::User[:me].email
30
+ end
data/lib/fogli.rb ADDED
@@ -0,0 +1,46 @@
1
+ # Try to load the Rails ActiveSupport module accessors
2
+ # But fall back to our own if it doesn't exist.
3
+ begin
4
+ require 'active_support/core_ext/module/attribute_accessors'
5
+ rescue LoadError
6
+ require 'fogli/util/module_attributes'
7
+ end
8
+
9
+ module Fogli
10
+ # The configuration options below only need to be set if you
11
+ # wish to retrieve authorized information via the Graph API.
12
+ # The configuration keys should be self explanatory.
13
+ mattr_accessor :client_id
14
+ mattr_accessor :client_secret
15
+ mattr_accessor :redirect_uri
16
+ mattr_accessor :access_token
17
+
18
+ # Logger. Set this to anything which has the same API as the Ruby
19
+ # standard library logger, or `nil` to disable logging (default)
20
+ mattr_accessor :logger
21
+
22
+ autoload :Exception, 'fogli/exception'
23
+ autoload :FacebookGraph, 'fogli/facebook_graph'
24
+ autoload :FacebookObject, 'fogli/facebook_object'
25
+
26
+ # User-friendly models
27
+ autoload :Album, 'fogli/album'
28
+ autoload :CategorizedObject, 'fogli/categorized_object'
29
+ autoload :Comment, 'fogli/comment'
30
+ autoload :Event, 'fogli/event'
31
+ autoload :Group, 'fogli/group'
32
+ autoload :Link, 'fogli/link'
33
+ autoload :NamedObject, 'fogli/named_object'
34
+ autoload :Note, 'fogli/note'
35
+ autoload :OAuth, 'fogli/oauth'
36
+ autoload :Page, 'fogli/page'
37
+ autoload :Photo, 'fogli/photo'
38
+ autoload :Post, 'fogli/post'
39
+ autoload :Status, 'fogli/status'
40
+ autoload :User, 'fogli/user'
41
+ autoload :Video, 'fogli/video'
42
+
43
+ module Util
44
+ autoload :Options, 'fogli/util/options'
45
+ end
46
+ end
@@ -0,0 +1,8 @@
1
+ module Fogli
2
+ class Album < FacebookObject
3
+ property :from, :name, :description, :location, :link, :count, :created_time
4
+
5
+ connection :photos, :class => :Photo
6
+ connection :comments, :class => :Comment
7
+ end
8
+ end
@@ -0,0 +1,9 @@
1
+ module Fogli
2
+ # A categorized object. In addition to simple objects such as
3
+ # {NamedObject}s, Facebook also has simple objects which only have a
4
+ # name and category in addition to an ID. This object represents
5
+ # such an object.
6
+ class CategorizedObject < NamedObject
7
+ property :category
8
+ end
9
+ end
@@ -0,0 +1,5 @@
1
+ module Fogli
2
+ class Comment < FacebookObject
3
+ property :from, :message, :created_time
4
+ end
5
+ end
@@ -0,0 +1,9 @@
1
+ module Fogli
2
+ class Event < FacebookObject
3
+ property :owner, :name, :description, :start_time, :end_time, :location,
4
+ :venue, :privacy
5
+
6
+ connection :feed, :class => :Post
7
+ conncetion :noreply, :maybe, :invited, :attending, :declined, :class => :User
8
+ end
9
+ end
@@ -0,0 +1,15 @@
1
+ module Fogli
2
+ # An exception from a call to the Facebook Graph API. This will
3
+ # always contain the exception type and the message given.
4
+ class Exception < ::Exception
5
+ attr_reader :type
6
+ attr_reader :message
7
+
8
+ def initialize(type, message)
9
+ @type = type
10
+ @message = "[#{type}] #{message}"
11
+
12
+ super()
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,125 @@
1
+ require 'cgi'
2
+ require 'restclient'
3
+ require 'json'
4
+
5
+ module Fogli
6
+ # The most basic level access to the Facebook Graph. This class only
7
+ # has access to the `get`, `post`, etc. methods to query the
8
+ # Facebook Graph directly. This class is used by every other class
9
+ # to access the graph. This class also will automatically insert the
10
+ # Facebook `access_token` if it has been specified, and handles
11
+ # changing the HTTP method to HTTPS in that case.
12
+ #
13
+ # **Note:** Most users should never have to use this class
14
+ # directly. Instead, use one of the models, such as {User}, to
15
+ # access your data.
16
+ #
17
+ # # Requesting Data
18
+ #
19
+ # Getting data from the Facebook graph is simple:
20
+ #
21
+ # Fogli::FacebookGraph.get("/mitchellh")
22
+ #
23
+ # If you have an access token, {FacebookGraph} will add it to the
24
+ # query string without you having to do anything:
25
+ #
26
+ # Fogli.access_token = "..."
27
+ # Fogli::FacebookGraph.get("/me")
28
+ #
29
+ class FacebookGraph
30
+ GRAPH_DOMAIN = "graph.facebook.com"
31
+
32
+ class << self
33
+ # Override default HTTParty behavior to go through our request
34
+ # method to make sure that the access token is properly set if
35
+ # needed.
36
+ [:get, :post, :delete, :head].each do |method|
37
+ # A requester, where the arguments go through the {request}
38
+ # method, which handles automatically adding the
39
+ # {Fogli.access_token} if it is specified, and also handles
40
+ # the URL relative to the given path.
41
+ define_method(method) do |*args|
42
+ Fogli.logger.debug("Fogli #{method}: #{args.inspect}") if Fogli.logger
43
+ request(method, *args)
44
+ end
45
+
46
+ # A raw requester, which just directly requests with the given
47
+ # arguments.
48
+ define_method("raw_#{method}".to_sym) do |*args|
49
+ Fogli.logger.debug("Fogli raw #{method}: #{args.inspect}") if Fogli.logger
50
+ error_check { RestClient.send(method, *args) }
51
+ end
52
+ end
53
+
54
+ # Issues the specified request type with the given URL and
55
+ # options. This method will inject the Facebook `access_token`
56
+ # if it is available, and will properly change the method to
57
+ # "https" if needed.
58
+ def request(type, url, params=nil)
59
+ params ||= {}
60
+
61
+ if Fogli.access_token
62
+ params ||= {}
63
+ params.merge!(:access_token => Fogli.access_token)
64
+ end
65
+
66
+ method = "http"
67
+ method = "https" if params[:access_token]
68
+
69
+ url = "#{method}://#{GRAPH_DOMAIN}#{url}"
70
+ if !params.empty? && [:get, :delete].include?(type)
71
+ # For GET and DELETE, we're responsible for appending any
72
+ # query parameters onto the end of the URL
73
+ params = params.inject([]) do |acc, data|
74
+ k, v = data
75
+ acc << "#{k}=#{CGI.escape(v.to_s)}"
76
+ acc
77
+ end
78
+
79
+ url += "?#{params.join("&")}"
80
+ params = {}
81
+ end
82
+
83
+ error_check { RestClient.send(type, url, params) }
84
+ end
85
+
86
+ # Yields a block, checking for any errors in a request. If no
87
+ # errors are detected, decodes JSON and returns the result.
88
+ def error_check
89
+ response = begin
90
+ yield
91
+ rescue RestClient::Exception => e
92
+ e.response
93
+ end
94
+
95
+ data = parse_response(response)
96
+ if data.is_a?(Hash) && data["error"]
97
+ data = data["error"]
98
+ raise Exception.new(data["type"], data["message"])
99
+ elsif response.code.to_i == 500
100
+ raise Exception.new("Unknown", "Internal server error")
101
+ end
102
+
103
+ data
104
+ rescue NoMethodError
105
+ data
106
+ end
107
+
108
+ # Parses a response depending on the content type it sent back.
109
+ def parse_response(response)
110
+ type = response.headers[:content_type]
111
+ return response.body if type.include?("text/plain")
112
+ return JSON.parse(response.body)
113
+ rescue JSON::ParserError
114
+ response.body
115
+ end
116
+ end
117
+
118
+ # Shortcut methods to access the class-level methods.
119
+ [:get, :post, :delete, :head].each do |method|
120
+ define_method(method) do |*args|
121
+ self.class.send(method, *args)
122
+ end
123
+ end
124
+ end
125
+ end