From b034cd6afd8eb2a49db5f3d027c15dde2c29cc63 Mon Sep 17 00:00:00 2001 From: claudiob Date: Fri, 28 Jul 2017 13:53:36 -0700 Subject: [PATCH] Add Fb::Page#posts Fetch all the posts of a page (not just the first 25 or 100) --- CHANGELOG.md | 1 + README.md | 13 ++++++++++++- lib/fb/core.rb | 3 +++ lib/fb/core/version.rb | 2 +- lib/fb/page.rb | 16 +++++++++++++++- lib/fb/paginated_request.rb | 22 ++++++++++++++++++++++ lib/fb/post.rb | 31 +++++++++++++++++++++++++++++++ lib/fb/resource.rb | 11 +++++++++++ lib/fb/user.rb | 12 ++---------- spec/page/posts_spec.rb | 24 ++++++++++++++++++++++++ spec/paginated_request_spec.rb | 16 ++++++++++++++++ spec/post/to_s_spec.rb | 9 +++++++++ 12 files changed, 147 insertions(+), 13 deletions(-) create mode 100644 lib/fb/paginated_request.rb create mode 100644 lib/fb/post.rb create mode 100644 lib/fb/resource.rb create mode 100644 spec/page/posts_spec.rb create mode 100644 spec/paginated_request_spec.rb create mode 100644 spec/post/to_s_spec.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 1206b1a..e1919d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,3 +10,4 @@ For more information about changelogs, check * [FEATURE] Added `Fb::User` * [FEATURE] Added `Fb::Page` +* [FEATURE] Added `Fb::Page#posts` diff --git a/README.md b/README.md index 7a7885d..8c2cc86 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ user.email # => 'john.smith@example.com' Fb::User#pages -------------- -Given an user access token with the `manage_pages` scope, you can get the list of Facebook pages managed by the user by calling: +Given an user access token with the `pages_show_list` scope, you can get the list of Facebook pages managed by the user by calling: ```ruby user = Fb::User.new access_token: '--valid-access-token--' @@ -41,6 +41,17 @@ user.pages # => [#, #] ``` +Fb::Page#posts +-------------- + +Given a page with posts, you can get the posts on the page since creation by calling: + +```ruby +page = Fb::Page.new access_token: '--valid-access-token--' +page.posts +# => [#, #] +``` + ## Development To run tests, obtain a long-term access token for a Facebook user who manages diff --git a/lib/fb/core.rb b/lib/fb/core.rb index 8e23751..42573f4 100644 --- a/lib/fb/core.rb +++ b/lib/fb/core.rb @@ -1,6 +1,9 @@ require 'fb/support' +require 'fb/paginated_request' +require 'fb/resource' require 'fb/page' require 'fb/user' +require 'fb/post' # An object-oriented Ruby client for the Facebook Graph API. # @see http://www.rubydoc.info/gems/fb-core/ diff --git a/lib/fb/core/version.rb b/lib/fb/core/version.rb index e562127..6b52a6a 100644 --- a/lib/fb/core/version.rb +++ b/lib/fb/core/version.rb @@ -3,6 +3,6 @@ module Fb class Core # @return [String] the SemVer-compatible gem version. # @see http://semver.org - VERSION = '1.0.0.alpha2' + VERSION = '1.0.0.alpha3' end end diff --git a/lib/fb/page.rb b/lib/fb/page.rb index 0a41e5a..b21ae64 100644 --- a/lib/fb/page.rb +++ b/lib/fb/page.rb @@ -1,7 +1,7 @@ module Fb # Provides methods to interact with Facebook pages through the Graph API. # @see https://developers.facebook.com/docs/graph-api/reference/page/ - class Page + class Page < Resource # @option [String] the page’s unique ID. attr_reader :id @@ -15,10 +15,24 @@ class Page # @option [String] :id The page’s unique ID. # @option [String] :name The page’s name. # @option [String] :category The page’s category. + # @option [String] :access_token an access token for the page. def initialize(options = {}) @id = options[:id] @name = options[:name] @category = options[:category] + @access_token = options[:access_token] + end + + # @return [Array] the posts published on the page. + def posts + @posts ||= begin + fields = %i(type created_time).join ',' + params = {access_token: @access_token, limit: 100, fields: fields} + request = PaginatedRequest.new path: "/v2.9/#{@id}/posts", params: params + request.run.body['data'].map do |post_data| + Post.new symbolize_keys post_data + end + end end # @return [String] the representation of the page. diff --git a/lib/fb/paginated_request.rb b/lib/fb/paginated_request.rb new file mode 100644 index 0000000..2bde34b --- /dev/null +++ b/lib/fb/paginated_request.rb @@ -0,0 +1,22 @@ +module Fb + # Provides a wrapper for HTTPRequest when the result has pagination links. + # @api private + class PaginatedRequest < HTTPRequest + # Sends the request and returns the response with the body parsed from JSON. + # If the response body contains a link to the next page, fetches that page + # as well and combines the data with the previous page. + # @return [Net::HTTPResponse] if the request succeeds. + # @raise [Fb::HTTPError] if the request fails. + def run + response = super + while after = response.body.dig('paging', 'cursors', 'after') + next_params = @params.merge after: after, limit: 100 + next_request = HTTPRequest.new path: @path, params: next_params + next_body = next_request.run.body + response.body['paging'] = next_body['paging'] + response.body['data'].concat next_body['data'] + end + response + end + end +end diff --git a/lib/fb/post.rb b/lib/fb/post.rb new file mode 100644 index 0000000..1d1fd92 --- /dev/null +++ b/lib/fb/post.rb @@ -0,0 +1,31 @@ +# Ruby client to authenticate a Facebook user. +# @see http://www.rubydoc.info/gems/Fb/ +module Fb + # Fb::Post reprensents a Facebook post. Post provides getters for: + # :id, :title, :url, :created_time, and :type. + class Post + # @option [String] the post’s unique ID. + attr_reader :id + + # @option [String] the post’s type. + attr_reader :type + + # @option [Time] the post’s creation time. + attr_reader :created_at + + # @param [Hash] options the options to initialize an instance of Fb::Post. + # @option [String] :id the post id. + # @option [String] :type the post’s type. + # @option [String] :created_time the post’s creation time in iso8601 format. + def initialize(options = {}) + @id = options[:id] + @type = options[:type] + @created_at = Time.parse(options[:created_time]) if options[:created_time] + end + + # @return [String] the representation of the post. + def to_s + %Q(#<#{self.class.name} #{@id} "#{@type}">) + end + end +end diff --git a/lib/fb/resource.rb b/lib/fb/resource.rb new file mode 100644 index 0000000..d43ea7b --- /dev/null +++ b/lib/fb/resource.rb @@ -0,0 +1,11 @@ +module Fb + # Provides a base class for Facebook resources (users, pages, ...) + class Resource + private + def symbolize_keys(hash) + {}.tap do |new_hash| + hash.each_key{|key| new_hash[key.to_sym] = hash[key]} + end + end + end +end diff --git a/lib/fb/user.rb b/lib/fb/user.rb index 364cdc8..042a865 100644 --- a/lib/fb/user.rb +++ b/lib/fb/user.rb @@ -1,7 +1,7 @@ module Fb # Provides methods to interact with Facebook users through the Graph API. # @see https://developers.facebook.com/docs/graph-api/reference/user/ - class User + class User < Resource # @param [Hash] options to initialize a User object. # @option [String] :access_token an access token for the user. def initialize(options = {}) @@ -23,17 +23,9 @@ def pages params = {access_token: @access_token} request = HTTPRequest.new path: '/me/accounts', params: params request.run.body['data'].map do |page_data| - Page.new symbolize_keys(page_data) + Page.new symbolize_keys(page_data.merge access_token: @access_token) end end end - - private - - def symbolize_keys(hash) - {}.tap do |new_hash| - hash.each_key{|key| new_hash[key.to_sym] = hash[key]} - end - end end end diff --git a/spec/page/posts_spec.rb b/spec/page/posts_spec.rb new file mode 100644 index 0000000..238f95e --- /dev/null +++ b/spec/page/posts_spec.rb @@ -0,0 +1,24 @@ +require 'spec_helper' + +RSpec.describe 'Fb::Page#posts' do + context 'given an invalid access token' do + let(:page) { Fb::Page.new access_token: 'invalid_token' } + + it 'raises Fb::HTTPError' do + expect{page.posts}.to raise_error Fb::HTTPError + end + end + + context 'given a valid access_token' do + let(:user) { Fb::User.new access_token: ENV['FB_TEST_ACCESS_TOKEN'] } + let(:page) { user.pages.first } + + it 'returns array of Post for any given page' do + expect(page.posts).to be_a(Array) + expect(page.posts).to all (be_a Fb::Post) + expect(page.posts.map &:id).to all(be_a String) + expect(page.posts.map &:type).to all(be_a String) + expect(page.posts.map &:created_at).to all(be_a Time) + end + end +end diff --git a/spec/paginated_request_spec.rb b/spec/paginated_request_spec.rb new file mode 100644 index 0000000..c4ada4a --- /dev/null +++ b/spec/paginated_request_spec.rb @@ -0,0 +1,16 @@ +require 'spec_helper' + +RSpec.describe 'Fb::PaginatedRequest#run' do + let(:params) { {access_token: ENV['FB_TEST_ACCESS_TOKEN']} } + let(:request) { Fb::PaginatedRequest.new path: path, params: params } + + context 'given a request which more than one page of results' do + let(:path) { '/v2.9/20531316728/posts' } # Posts by Facebook + + it 'returns all the results, not just the first page' do + response = request.run + expect(response).to be_a Net::HTTPOK + expect(response.body['data'].size).to be > 100 + end + end +end diff --git a/spec/post/to_s_spec.rb b/spec/post/to_s_spec.rb new file mode 100644 index 0000000..c5c35cc --- /dev/null +++ b/spec/post/to_s_spec.rb @@ -0,0 +1,9 @@ +require 'spec_helper' + +RSpec.describe 'Fb::Post#to_s' do + let(:post) { Fb::Post.new id: 123, type: 'video' } + + it 'returns a pretty string representation' do + expect(post.to_s).to eq '#' + end +end