Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Initial commit

  • Loading branch information...
commit 76eafcd88f16d9acb6c3651b9aef22c57a07fe2d 0 parents
Gustavo Machado Campagnani Gama authored
2  .gitignore
@@ -0,0 +1,2 @@
+*.swp
+google-reader-*.gem
2  LICENCE
@@ -0,0 +1,2 @@
+Copyright (c) 2010 Gustavo Machado C. Gama (gustavo.gama@gmail.com) and
+Igenesis Ltda. (http://www.igenesis.com.br)
4 TODO
@@ -0,0 +1,4 @@
+* Implement write methods (only read-only API is currently available)
+* Make Google::Reader::ItemList implement Enumerable (or inherit from Array)
+* (?) Ditch the Hashie::Mash interface and use instance_eval to define the accessors
+* Tests
111 bin/google-reader-console
@@ -0,0 +1,111 @@
+#!/usr/bin/env ruby
+
+############################################################
+# Copyright (c) 2010, iGenesis Ltda. #
+# Author: Gustavo Machado C. Gama <gustavo.gama@gmail.com> #
+############################################################
+
+require 'optparse'
+require 'irb'
+require 'rack'
+require 'json'
+require 'google-reader/auth'
+require 'google-reader/client'
+require 'google-reader/user'
+
+module Google; module Reader
+
+class Console
+ include Google::Reader::Auth
+ include Google::Reader::Client
+
+ WEB_SERVER_PORT = 8088
+
+ def initialize(request_token_file, access_token_file)
+ @request_token_file = request_token_file
+ @access_token_file = access_token_file
+
+ if File.readable?(@request_token_file)
+ token, secret = IO.read(@request_token_file).split(/\r?\n/)
+ @request_token = ::OAuth::RequestToken.new(consumer, token, secret) if token && secret
+ end
+ if File.readable?(@access_token_file)
+ token, secret = IO.read(@access_token_file).split(/\r?\n/)
+ @access_token = ::OAuth::AccessToken.new(consumer, token, secret) if token && secret
+ end
+
+ # use the net::http of the AccessToken as a request proxy
+ self.request_proxy = access_token
+ end
+
+ def request_token
+ @request_token ||= begin
+ token = super
+ File.open(@request_token_file, 'w') do |file|
+ file.write "#{token.token}\n#{token.secret}\n"
+ end
+ token
+ end
+ end
+
+ def access_token
+ @access_token ||= begin
+ puts "please access #{request_token.authorize_url} using your web browser."
+ Rack::Handler.default.run(self, :Port => WEB_SERVER_PORT)
+
+ token = super
+ File.open(@access_token_file, 'w') do |file|
+ file.write "#{token.token}\n#{token.secret}\n"
+ end
+ token
+ end
+ end
+
+ def google_reader_authorize_callback
+ "http://localhost:#{WEB_SERVER_PORT}/authorize"
+ end
+
+ #############################################################
+ # Rack Handler (automatically handles authorization callback)
+ #############################################################
+
+ # implement the Rack interface to handle the 'authorize callback'
+ def call(env)
+ if env['REQUEST_URI'].start_with?(self.class.google_reader_authorize_callback)
+ extract_verifier(env['REQUEST_URI'])
+ Process.kill('INT', 0) # stop the rack server
+ [200, {'Content-Type' => 'text/plain'}, ["Request token accepted (#{@oauth_token}/#{@oauth_verifier}). Proceeding.\n"]]
+ else
+ [404, {'Content-Type' => 'text/plain'}, ['']]
+ end
+ end
+end
+
+end; end # module Google::Reader
+
+
+############################ MAIN ###############################
+
+# default token files
+request_token_file = File.join(ENV['HOME'], '.cache', 'google', 'reader', 'oauth-request-token')
+access_token_file = File.join(ENV['HOME'], '.cache', 'google', 'reader', 'oauth-access-token')
+
+OptionParser.new do |opts|
+ opts.banner = "Usage: #$0 [-r <request-token-file>][-a <access-token-file>]\n"
+ opts.on '-r', '--request-token <request-token-file>', 'file with oauth request token' do |f| request_token_file = f; end
+ opts.on '-a', '--access-token <access-token-file> ', 'file with oauth access token' do |f| access_token_file = f; end
+end.parse!
+
+@console = Google::Reader::Console.new(request_token_file, access_token_file)
+@user = @console.user
+puts <<EOS
+
+--------------- Google Reader interactive console --------------
+The base client is available through the instance variable '@console'.
+The authenticated user may be retrieved though '@console.user':
+
+ irb> puts @console.user.subscriptions.inspect
+
+EOS
+require 'irb'
+IRB.start
22 google-reader.gemspec
@@ -0,0 +1,22 @@
+# -*- encoding: utf-8 -*-
+require File.expand_path('../lib/google-reader/version', __FILE__)
+
+Gem::Specification.new do |s|
+ s.add_runtime_dependency('rack', '~> 1.2')
+ s.add_runtime_dependency('json', '~> 1.4')
+ s.add_runtime_dependency('hashie', '~> 0.4.0')
+ s.add_runtime_dependency('oauth', '~> 0.4.0')
+ s.authors = ['Gustavo Machado C. Gama']
+ s.description = %q{A Ruby wrapper for the 'Google Reader' unofficial API}
+ s.email = ['gustavo.gama@gmail.com']
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
+ s.files = `git ls-files`.split("\n")
+ s.homepage = 'http://github.com/gama/google-reader'
+ s.name = 'google-reader'
+ s.platform = Gem::Platform::RUBY
+ s.require_paths = ['lib']
+ s.required_rubygems_version = Gem::Requirement.new(">= 1.3.6") if s.respond_to? :required_rubygems_version=
+ s.summary = %q{Ruby wrapper for the 'Google Reader' API}
+ s.test_files = `git ls-files -- test/*`.split("\n")
+ s.version = Google::Reader::VERSION
+end
7 lib/google-reader.rb
@@ -0,0 +1,7 @@
+##########################################################
+# Copyright 2010, iGenesis Ltda. #
+# Author: Gustavo Machado C. Gama <gama@igenesis.com.br> #
+##########################################################
+
+require 'google-reader/client'
+require 'google-reader/auth'
80 lib/google-reader/auth.rb
@@ -0,0 +1,80 @@
+############################################################
+# Copyright (c) 2010, iGenesis Ltda. #
+# Author: Gustavo Machado C. Gama <gustavo.gama@gmail.com> #
+############################################################
+
+require 'oauth'
+require 'oauth/consumer'
+
+module Google; module Reader;
+
+module Auth
+ DEFAULT_OAUTH_CONSUMER_KEY = 'anonymous'
+ DEFAULT_OAUTH_CONSUMER_SECRET = 'anonymous'
+ DEFAULT_OAUTH_AUTHORIZE_CALLBACK = 'oob'
+ OAUTH_SCOPE =
+
+ def self.included(base)
+ base.instance_eval do
+ def google_reader_consumer_key(key = nil)
+ key ? (@@google_reader_consumer_key = key) : (@@google_reader_consumer_key)
+ end
+
+ def google_reader_consumer_secret(secret = nil)
+ secret ? (@@google_reader_consumer_secret = secret) : (@@google_reader_consumer_secret)
+ end
+
+ google_reader_consumer_key DEFAULT_OAUTH_CONSUMER_KEY
+ google_reader_consumer_secret DEFAULT_OAUTH_CONSUMER_SECRET
+ end
+
+ base.class_eval do
+ def google_reader_authorize_callback
+ DEFAULT_OAUTH_AUTHORIZE_CALLBACK
+ end
+ end
+ end
+
+ def consumer
+ @consumer ||= ::OAuth::Consumer.new(@@google_reader_consumer_key, @@google_reader_consumer_secret, {
+ :site => 'https://www.google.com',
+ :scheme => :header,
+ :http_method => :post,
+ :request_token_path => '/accounts/OAuthGetRequestToken',
+ :access_token_path => '/accounts/OAuthGetAccessToken',
+ :authorize_path => '/accounts/OAuthAuthorizeToken',
+ })
+ end
+
+ def request_token
+ @request_token ||= get_request_token
+ end
+
+ def access_token
+ @access_token ||= get_access_token
+ end
+
+ def verifier=(verifier)
+ @oauth_verifier = verifier
+ end
+
+ def extract_verifier(request_uri)
+ @oauth_token = CGI.unescape(request_uri[/oauth_token=(.*?)(&|$)/, 1])
+ @oauth_verifier = CGI.unescape(request_uri[/oauth_verifier=(.*?)(&|$)/, 1])
+ end
+
+ protected
+
+ def get_request_token
+ consumer.get_request_token({:oauth_callback => google_reader_authorize_callback},
+ {:scope => 'https://www.google.com/reader/api/', :xoauth_display_name => 'Igenesis Priority Feedzz'})
+ end
+
+ def get_access_token
+ #throw "no 'request token' or 'verifier' received" unless request_token && @oauth_verifier
+ return nil unless request_token && @oauth_verifier
+ token = request_token.get_access_token({:oauth_verifier => @oauth_verifier})
+ end
+end
+
+end; end # module Google::Reader
48 lib/google-reader/client.rb
@@ -0,0 +1,48 @@
+############################################################
+# Copyright (c) 2010, iGenesis Ltda. #
+# Author: Gustavo Machado C. Gama <gustavo.gama@gmail.com> #
+############################################################
+
+require 'google-reader/user'
+
+module Google; module Reader;
+
+# implement the base functionality of the Google Reader API
+module Client
+ attr_accessor :request_proxy
+
+ # update requests (i.e., not read-only) need an additional google-reader
+ # specific token
+ def token
+ resp = request_proxy.get('/reader/api/0/token')
+ raise "unable to retrieve token" unless resp.code_type == Net::HTTPOK
+ @google_reader_token = resp.body
+ end
+
+ # build a profile of the logged in Google Reader user/profile
+ def user
+ @user ||= begin
+ # we rely on the 'friends' request to fetch the user's profile; this was the only
+ # reliable way I could find that did it
+ _friends = friends
+ _current_user = _friends.slice!(_friends.find_index{|f| f.is_a?(CurrentUser)})
+ _current_user.friends = _friends
+ _current_user
+ end
+ end
+ alias :profile :user
+
+ protected
+
+ # fetch the list of profiles of the logged user's friends/followers
+ # (which includes the logged user him/herself)
+ def friends
+ resp = request_proxy.get('/reader/api/0/friend/list?output=json')
+ raise "unable to retrieve the list of friends" unless resp.code_type == Net::HTTPOK
+ JSON.parse(resp.body)['friends'].collect do |friend|
+ ((friend['flags'] & (1 << User::IS_ME)) > 0) ? CurrentUser.new(request_proxy, friend) : User.new(request_proxy, friend)
+ end
+ end
+end
+
+end; end # module Google::Reader
93 lib/google-reader/item-list.rb
@@ -0,0 +1,93 @@
+############################################################
+# Copyright (c) 2010, iGenesis Ltda. #
+# Author: Gustavo Machado C. Gama <gustavo.gama@gmail.com> #
+############################################################
+
+require 'json'
+require 'google-reader/item'
+
+module Google; module Reader;
+
+class ItemList
+ attr_reader :items
+
+ def initialize(req_proxy, json_str = nil)
+ @request_proxy = req_proxy
+ if json_str
+ json = JSON.parse(json_str)
+ %w(author title updated direction continuation).each do |key|
+ instance_variable_set(('@'+key).to_sym, json[key])
+ end
+ @url = json['self'].first['href']
+ @items = json['items'].collect{|i| Google::Reader::Item.new(i)}
+ else
+ @items = Array.new
+ end
+ end
+
+ def size
+ @items.size
+ end
+
+ # check whether there are more items available (using the 'continuation'
+ # attribute')
+ def next?
+ !@continuation.nil?
+ end
+ alias :has_next? :next?
+ alias :more? :next?
+
+ # fetch next batch of items, if available
+ def next(params = {})
+ next? or return nil
+ resp = @request_proxy.get(merge_query_string(@url, {:continuation => @continuation}))
+ self.class.new(@request_proxy, resp.body)
+ end
+ alias :more :next
+
+ # fetch all available items
+ def all
+ all_items = Array.new
+ cur_items = self
+ begin
+ all_items += cur_items.items
+ cur_items = cur_items.next
+ end until cur_items.nil?
+ all_items
+ end
+
+ def self.merge_query_string(url, new_params)
+ url = URI.parse(url) if url.is_a?(String)
+ url.is_a?(URI) or return nil
+
+ query_hash = url.query.split(/\&/).collect{|i|i.split(/=/)}
+
+ query_hash = convert_key_aliases(query_hash).merge(convert_key_aliases(new_params))
+ url.query = query_hash.collect{|k, v| "#{k}=#{v}"}.join('&')
+ url.to_s
+ end
+ def merge_query_string(*args); self.class.merge_query_string(*args); end
+
+ protected
+
+ def self.convert_key_aliases(query_hash)
+ new_hash = {}
+ query_hash.each do |key, value|
+ new_key = case key.to_s
+ when 'n', 'count', 'n_items': 'n'
+ when 'r', 'order': 'r'
+ when 'ot', 'start_time': 'ot'
+ when 'ck', 'timestamp': 'ck'
+ when 'xt', 'exclude': 'xt'
+ when 'c', 'continuation': 'c'
+ when 'client': 'client'
+ when 'output': 'output'
+ else raise "invalid key: #{key}"
+ end
+ new_hash[new_key] = value
+ end
+ new_hash
+ end
+end
+
+end; end # module Google::Reader
13 lib/google-reader/item.rb
@@ -0,0 +1,13 @@
+############################################################
+# Copyright (c) 2010, iGenesis Ltda. #
+# Author: Gustavo Machado C. Gama <gustavo.gama@gmail.com> #
+############################################################
+
+require 'google-reader/underscore-mash'
+
+module Google; module Reader;
+
+class Item < UnderscoreMash
+end
+
+end; end # module Google::Reader
8 lib/google-reader/state.rb
@@ -0,0 +1,8 @@
+############################################################
+# Copyright (c) 2010, iGenesis Ltda. #
+# Author: Gustavo Machado C. Gama <gustavo.gama@gmail.com> #
+############################################################
+
+module Google; module Reader;
+
+end; end # module Google::Reader
13 lib/google-reader/subscription.rb
@@ -0,0 +1,13 @@
+############################################################
+# Copyright (c) 2010, iGenesis Ltda. #
+# Author: Gustavo Machado C. Gama <gustavo.gama@gmail.com> #
+############################################################
+
+require 'google-reader/underscore-mash'
+
+module Google; module Reader;
+
+class Subscription < UnderscoreMash
+end
+
+end; end # module Google::Reader
33 lib/google-reader/underscore-mash.rb
@@ -0,0 +1,33 @@
+############################################################
+# Copyright (c) 2010, iGenesis Ltda. #
+# Author: Gustavo Machado C. Gama <gustavo.gama@gmail.com> #
+############################################################
+
+require 'hashie'
+
+module Google; module Reader;
+
+class UnderscoreMash < Hashie::Mash
+
+ def initialize(hash = nil, default = nil, &blk)
+ super(hash, default, &blk)
+ end
+
+ protected
+
+ def convert_key(key)
+ underscore(key.to_s)
+ end
+
+ private
+
+ # stolen/adapted from ActiveSuport::Inflector
+ def underscore(camel_cased_word)
+ camel_cased_word.to_s.gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
+ gsub(/([a-z\d])([A-Z])/,'\1_\2').
+ tr("-", "_").
+ downcase
+ end
+end
+
+end; end # module Google::Reader
168 lib/google-reader/user.rb
@@ -0,0 +1,168 @@
+############################################################
+# Copyright (c) 2010, iGenesis Ltda. #
+# Author: Gustavo Machado C. Gama <gustavo.gama@gmail.com> #
+############################################################
+
+require 'google-reader/underscore-mash'
+require 'google-reader/subscription'
+require 'google-reader/item-list'
+
+module Google; module Reader;
+
+class User < UnderscoreMash
+ # user types & flags (taken from
+ # http://groups.google.com/group/fougrapi/browse_thread/thread/ba43dcaabe896bd3/a8527d8aab29e60d?lnk=gst&q=friends#a8527d8aab29e60d)
+ @@types = [
+ :FOLLOWER, # 0: this person is following the user
+ :FOLLOWING, # 1: the user is following this person
+ :NOTYPE2, # 2: n/a
+ :CONTACT, # 3: this person is in the user's contacts list
+ :PENDING_FOLLOWING, # 4: the user is attempting to follow this person
+ :PENDING_FOLLOWER, # 5: this person is attempting to follow this user
+ :ALLOWED_FOLLOWING, # 6: the user is allowed to follow this person
+ :ALLOWED_COMMENTING # 7: the user is allowed to comment on this person's shared items
+ ]
+ @@flags = [
+ :IS_ME, # 0: represents the current user
+ :IS_HIDDEN, # 1: current user has hidden this person from the list of people with shared items that show up
+ :IS_NEW, # 2: this person is a recent addition to the user's list of people that they follow
+ :USER_READER, # 3: this person uses reader
+ :IS_BLOCKED, # 4: the user has blocked this person
+ :HAS_PROFILE, # 5: this person has created a Google Profile
+ :IS_IGNORED, # 6: this person has requested to follow the user, but the use has ignored the request
+ :IS_NEW_FOLLOWER, # 7: this person has just begun to follow the user
+ :IS_ANONYMOUS, # 8: this person doesn't have a display name set
+ :HAS_SHARED_ITEMS # 9: this person has shared items in reader
+ ]
+
+ # create constantized version of each flag & type
+ @@types.each_with_index do |flag, index|
+ const_set(flag, index)
+ end
+ @@flags.each_with_index do |flag, index|
+ const_set(flag, index)
+ end
+
+ def initialize(req_proxy, hash = nil)
+ @request_proxy = req_proxy
+ super(hash, default, &blk)
+ friends = Array.new
+ end
+
+ def has_flag?(flag)
+ ((self['flags'] & (1 << flag)) > 0)
+ end
+
+ def flags
+ bitmap_flags = self['flags']
+ (0 .. @@flags.size).collect do |i|
+ @@flags[i] if ((bitmap_flags & (1 << i)) > 0)
+ end.compact
+ end
+
+ def has_type?(type)
+ self['types'].include?(type)
+ end
+
+ def types
+ self['types'].collect{|t| @@types[t]}
+ end
+
+ def user_id
+ user_ids.first
+ end
+
+ # returns the list of broadcast, or shared items
+ def broadcast_items
+ @broadcast_items ||= filtered_items_list('broadcast')
+ end
+ alias :shared_items :broadcast_items
+
+ protected
+
+ # retrieve the a list of items from a given user, using a given filter
+ def filtered_items_list(filter, params = {})
+ begin
+ resp = @request_proxy.get(ItemList.merge_query_string("/reader/api/0/stream/contents/user/#{user_id}/state/com.google/#{filter}?output=json", params))
+
+ raise "unable to retrieve the list of #{filter} items for user #{user_id}" unless resp.code_type == Net::HTTPOK
+ Google::Reader::ItemList.new(@request_proxy, resp.body)
+ end
+ end
+end
+
+class CurrentUser < User
+ attr_accessor :friends
+
+ # retrieve the list of subscriptions/feeds
+ def subscriptions
+ @subscriptions ||= begin
+ resp = @request_proxy.get('/reader/api/0/subscription/list?output=json')
+ raise "unable to retrieve the list of subscription for user #{user_id}" unless resp.code_type == Net::HTTPOK
+ JSON.parse(resp.body)['subscriptions'].collect do |f|
+ Google::Reader::Subscription.new(f)
+ end
+ end
+ end
+
+ # returns the list with all items
+ def all_items(params = {})
+ @all_items ||= filtered_items_list('reading-list', params)
+ end
+ alias :reading_list :all_items
+
+ # returns the list of 'unread' items
+ def unread_items(params = {})
+ @unread_items ||= filtered_items_list('unread', params.merge(:exclude => 'user/-/state/com.google/read'))
+ end
+
+ # returns the list of 'read' items
+ def read_items(params = {})
+ @read_items ||= filtered_items_list('read', params)
+ end
+
+ # returns the list of items that the user's friends have shared
+ def broadcast_friends_items(params = {})
+ @broadcast_friends_items ||= filtered_items_list('broadcast-friends', params)
+ end
+ alias :shared_friends_items :broadcast_friends_items
+
+ # returns the list of items that the user has 'starred'
+ def starred_items(params = {})
+ @starred_items ||= filtered_items_list('starred', params)
+ end
+
+ # returns the list of items that are currently marked as 'keep-unread'
+ # by the user
+ def kept_unread_items(params = {})
+ @kept_unread_items ||= filtered_items_list('kept-unread', params)
+ end
+
+ # returns the list of items which the user has ever clicked on a link
+ # inside its body
+ def tracking_body_link_items(params = {})
+ @tracking_body_link_items ||= filtered_items_list('tracking-body-link-used', params)
+ end
+ alias :followed_body_items :tracking_body_link_items
+
+ # returns the list of items which the user has ever emailed to someone
+ def tracking_emailed_items(params = {})
+ @tracking_emailed_items ||= filtered_items_list('tracking-emailed', params)
+ end
+ alias :emailed_items :tracking_emailed_items
+
+ # returns the list of items which the user has ever clicked on a link
+ # inside its description
+ def tracking_link_items(params = {})
+ @tracking_link_items ||= filtered_items_list('tracking-item-link-used', params)
+ end
+ alias :followed_items :tracking_link_items
+
+ # returns the list of items which the user has ever marked as 'keep-unread'
+ def tracking_kept_unread_items(params = {})
+ @tracking_kept_unread_items ||= filtered_items_list('tracking-kept-unread', params)
+ end
+ alias :kept_unread_ever_items :tracking_kept_unread_items
+end
+
+end; end # module Google::Reader
8 lib/google-reader/version.rb
@@ -0,0 +1,8 @@
+############################################################
+# Copyright (c) 2010, iGenesis Ltda. #
+# Author: Gustavo Machado C. Gama <gustavo.gama@gmail.com> #
+############################################################
+
+module Google; module Reader
+ VERSION = '0.1' unless defined?(Google::Reader::VERSION)
+end; end
Please sign in to comment.
Something went wrong with that request. Please try again.