├── VERSION ├── .gitignore ├── lib ├── google_reader_api.rb └── google-reader-api │ ├── rss_utils.rb │ ├── cache.rb │ ├── entry.rb │ ├── user.rb │ ├── subscription_list.rb │ ├── feed.rb │ ├── api.rb │ └── google_login.rb ├── Rakefile ├── License ├── GoogleReaderApi.gemspec └── README.mdown /VERSION: -------------------------------------------------------------------------------- 1 | 0.5.0 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | pkg 2 | -------------------------------------------------------------------------------- /lib/google_reader_api.rb: -------------------------------------------------------------------------------- 1 | require "google-reader-api/rss_utils" 2 | require "google-reader-api/api" 3 | require "google-reader-api/cache" 4 | require "google-reader-api/entry" 5 | require "google-reader-api/feed" 6 | require "google-reader-api/subscription_list" 7 | require "google-reader-api/user" 8 | require "google-reader-api/google_login" 9 | -------------------------------------------------------------------------------- /lib/google-reader-api/rss_utils.rb: -------------------------------------------------------------------------------- 1 | module GoogleReaderApi 2 | module RssUtils 3 | 4 | require "rss/parser" 5 | require "rss/atom" 6 | 7 | private 8 | 9 | def create_entries(atom_feed) 10 | RSS::Parser.parse(atom_feed.force_encoding('utf-8')).entries.map {|e| GoogleReaderApi::Entry.new(@api,e) } 11 | end 12 | 13 | end 14 | end -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | begin 2 | require 'jeweler' 3 | Jeweler::Tasks.new do |gemspec| 4 | gemspec.name = "GoogleReaderApi" 5 | gemspec.summary = "a google reader api (unofficial) written in ruby" 6 | gemspec.description = "a google reader api (unofficial) written in ruby" 7 | gemspec.email = "willemstoon@gmail.com" 8 | gemspec.homepage = "http://github.com/nudded/GoogleReaderAPI" 9 | gemspec.authors = ["Toon Willems"] 10 | end 11 | rescue LoadError 12 | puts "Jeweler not available. Install it with: gem install jeweler" 13 | end 14 | -------------------------------------------------------------------------------- /lib/google-reader-api/cache.rb: -------------------------------------------------------------------------------- 1 | module GoogleReaderApi 2 | class Cache 3 | 4 | def initialize(time) 5 | @time = time 6 | @hash = {} 7 | end 8 | 9 | def [](key) 10 | if cached?(key) 11 | @hash[key].first 12 | else 13 | @hash[key] = nil 14 | end 15 | end 16 | 17 | def []=(key,value) 18 | @hash[key] = [value,Time.now.to_i] 19 | end 20 | 21 | def cached?(key) 22 | if @hash[key] 23 | Time.now.to_i - @hash[key][1] < @time 24 | else 25 | false 26 | end 27 | end 28 | 29 | end 30 | end -------------------------------------------------------------------------------- /lib/google-reader-api/entry.rb: -------------------------------------------------------------------------------- 1 | module GoogleReaderApi 2 | class Entry 3 | 4 | attr_reader :entry 5 | 6 | def initialize(api,entry) 7 | @api, @entry = api, entry 8 | end 9 | 10 | def toggle_read 11 | edit_tag 'user/-/state/com.google/read' 12 | end 13 | 14 | def toggle_like 15 | edit_tag 'user/-/state/com.google/like' 16 | end 17 | 18 | def toggle_star 19 | edit_tag 'user/-/state/com.google/starred' 20 | end 21 | 22 | def to_s 23 | "<>" 24 | end 25 | 26 | private 27 | 28 | def edit_tag(tag_identifier) 29 | @api.post_link "api/0/edit-tag" , :a => tag_identifier , 30 | :s => entry.parent.id.content.to_s.scan(/feed\/.*/) , 31 | :i => entry.id.content.to_s 32 | end 33 | 34 | end 35 | end -------------------------------------------------------------------------------- /lib/google-reader-api/user.rb: -------------------------------------------------------------------------------- 1 | module GoogleReaderApi 2 | 3 | class User 4 | 5 | require "json" 6 | # maybe someone would like to access the api for a user 7 | attr_reader :api 8 | 9 | # specify either the :email and :password or the :auth token you got in the past 10 | # 11 | # [:email] the user's email address for login purposes 12 | # 13 | # [:password] the user's password for login purposes 14 | # 15 | # [:auth] the auth token you got from a previous authentication request 16 | # if you provide this you do not need to provide the email and password 17 | def initialize(options) 18 | @api = GoogleReaderApi::Api::new options 19 | end 20 | 21 | def info 22 | JSON[api.get_link "api/0/user-info"] 23 | end 24 | 25 | def subscriptions 26 | @subscriptions ||= GoogleReaderApi::SubscriptionList.new @api 27 | end 28 | 29 | def feeds 30 | subscriptions.feeds 31 | end 32 | 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /License: -------------------------------------------------------------------------------- 1 | the MIT License 2 | 3 | Copyright (c) 2010 Toon Willems 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. -------------------------------------------------------------------------------- /GoogleReaderApi.gemspec: -------------------------------------------------------------------------------- 1 | # Generated by jeweler 2 | # DO NOT EDIT THIS FILE DIRECTLY 3 | # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec' 4 | # -*- encoding: utf-8 -*- 5 | 6 | Gem::Specification.new do |s| 7 | s.name = "GoogleReaderApi" 8 | s.version = "0.5.0" 9 | 10 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 11 | s.authors = ["Toon Willems"] 12 | s.date = "2013-04-09" 13 | s.description = "a google reader api (unofficial) written in ruby" 14 | s.email = "willemstoon@gmail.com" 15 | s.extra_rdoc_files = [ 16 | "README.mdown" 17 | ] 18 | s.files = [ 19 | "GoogleReaderApi.gemspec", 20 | "License", 21 | "README.mdown", 22 | "Rakefile", 23 | "VERSION", 24 | "lib/google-reader-api/api.rb", 25 | "lib/google-reader-api/cache.rb", 26 | "lib/google-reader-api/entry.rb", 27 | "lib/google-reader-api/feed.rb", 28 | "lib/google-reader-api/google_login.rb", 29 | "lib/google-reader-api/rss_utils.rb", 30 | "lib/google-reader-api/subscription_list.rb", 31 | "lib/google-reader-api/user.rb", 32 | "lib/google_reader_api.rb" 33 | ] 34 | s.homepage = "http://github.com/nudded/GoogleReaderAPI" 35 | s.require_paths = ["lib"] 36 | s.rubygems_version = "2.0.3" 37 | s.summary = "a google reader api (unofficial) written in ruby" 38 | end 39 | 40 | -------------------------------------------------------------------------------- /lib/google-reader-api/subscription_list.rb: -------------------------------------------------------------------------------- 1 | module GoogleReaderApi 2 | 3 | class SubscriptionList 4 | 5 | require "cgi" 6 | 7 | include GoogleReaderApi::RssUtils 8 | include Enumerable 9 | 10 | def initialize(api) 11 | @api = api 12 | update 13 | end 14 | 15 | # returns the total unread count 16 | def total_unread 17 | inject(0) {|i,j| i+j.unread_count} 18 | end 19 | 20 | # returns a hash 21 | # with following pattern: 22 | # feed => unread_count 23 | def unread_count 24 | hash = {} 25 | each { |feed| hash[feed] = feed.unread_count } 26 | hash 27 | end 28 | 29 | # yield each feed, if you return true 30 | # the feed will be removed from your subscriptions 31 | def remove_if 32 | each { |feed| feed.unsubscribe if yield feed} 33 | update 34 | end 35 | 36 | # subscribe to the given url 37 | # google will set the title for you 38 | def add(url) 39 | @api.post_link 'api/0/subscription/edit', :s => "feed/#{url}" , :ac => :subscribe 40 | update 41 | end 42 | 43 | # return an array of unread items 44 | def unread_items 45 | feeds.map(&:all_unread_items) 46 | end 47 | 48 | # will return an array of entries with label 49 | def items_with_label(label) 50 | create_entries(@api.get_link "atom/user/-/label/#{CGI::escape label}") 51 | end 52 | 53 | def feeds 54 | @feeds 55 | end 56 | 57 | def each 58 | @feeds.each {|feed| yield feed} 59 | end 60 | 61 | def update 62 | fetch_list 63 | end 64 | 65 | private 66 | 67 | def fetch_list 68 | json = JSON[@api.get_link 'api/0/subscription/list', :output => :json]['subscriptions'] 69 | @feeds = json.map {|hash| GoogleReaderApi::Feed.new(hash,@api) } 70 | end 71 | 72 | end 73 | 74 | end -------------------------------------------------------------------------------- /lib/google-reader-api/feed.rb: -------------------------------------------------------------------------------- 1 | module GoogleReaderApi 2 | 3 | class Feed 4 | 5 | include GoogleReaderApi::RssUtils 6 | 7 | attr_reader :url, :title, :api, :sortid, :categories, :firstitemmsec 8 | 9 | def initialize(hash,api) 10 | # strip the first 5 characters of the url (they are 'feed/') 11 | @url = hash['id'][5..-1] 12 | @title = hash['title'] 13 | # no idea what this is used for 14 | @sortid = hash['sortid'] 15 | @categories = hash['categories'] 16 | @firstitemmsec = hash['firstitemmsec'] 17 | 18 | @api = api 19 | end 20 | 21 | def unsubscribe 22 | @api.post_link 'api/0/subscription/edit' , :s => "feed/#{url}", 23 | :ac => :unsubscribe 24 | end 25 | 26 | def unread_count 27 | entry = JSON[@api.cached_unread_count]['unreadcounts'].find {|h| h['id'] == "feed/#{url}"} 28 | entry ? entry['count'] : 0 29 | end 30 | 31 | # return count read items 32 | def read_items(count=20) 33 | create_entries get_user_items('read',:n => count) 34 | end 35 | 36 | def starred_items(count=20) 37 | create_entries get_user_items('starred',:n => count) 38 | end 39 | 40 | # return the number of specified items. (read or not) 41 | def items(count = 20) 42 | create_entries get_feed_items(:n => count) 43 | end 44 | 45 | # return all the unread items in an array 46 | def all_unread_items 47 | unread_count > 0 ? unread_items(unread_count) : [] 48 | end 49 | 50 | # will return an array of GoogleReader::Feed::Entry objects. 51 | # will try to return the amount of unread items you specify. unless there are no more. 52 | # will return 20 unread items by default. 53 | def unread_items(count = 20) 54 | create_entries get_feed_items(:n => count,:xt => 'user/-/state/com.google/read') 55 | end 56 | 57 | def inspect 58 | to_s 59 | end 60 | 61 | def to_s 62 | "<>" 63 | end 64 | 65 | private 66 | 67 | def get_user_items(state,args={}) 68 | @api.get_link "atom/user/-/state/com.google/#{state}" , args 69 | end 70 | 71 | def get_feed_items(args={}) 72 | @api.get_link "atom/feed/#{url}" , args 73 | end 74 | 75 | end 76 | end -------------------------------------------------------------------------------- /README.mdown: -------------------------------------------------------------------------------- 1 | GoogleReaderAPI 2 | =============== 3 | 4 | A Google Reader api. Programmed in ruby. This is an unofficial api. 5 | Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php 6 | 7 | Usage 8 | ----- 9 | 10 | If you would like to implement your own methods, you can quite easily use 11 | the api class provided. 12 | 13 | api = GoogleReaderApi::Api.new {:email => 'example@gmail.com', :password => 'the pass'} 14 | 15 | # OR 16 | 17 | api = GoogleReaderApi::Api.new {:auth => 'token'} 18 | 19 | # OR If your token was getted from oauth2 (bearer token) 20 | 21 | api = GoogleReaderApi::Api.new {:auth => 'token', :auth_type => :bearer} 22 | 23 | 24 | and then you can perform requests using the `get_link` and `post_link` methods. 25 | 26 | # this will give the user info. 27 | api.get_link "api/0/user-info" 28 | 29 | # this will add a feed 30 | api.post_link 'api/0/subscription/edit', :s => "feed/#{url}" , :ac => :subscribe 31 | 32 | The following methods all use the `Api` class, so there is no magic 33 | involved. 34 | 35 | # password should be asked by the app using the api 36 | # you probably don't want to type that in cleartext here 37 | user = GoogleReaderApi::User.new {:email => 'example@gmail.com', :password => 'the pass'} 38 | 39 | # OR 40 | 41 | user = GoogleReaderApi::User.new {:auth => 'token'} 42 | 43 | 44 | you can access your feeds from there 45 | 46 | user.feeds 47 | 48 | which will return an array of GoogleReader::Feed objects. 49 | then you can get the unread items, the read items, ... 50 | 51 | hn = user.feeds.find {|feed| feed.title =~ /hacker news/i } 52 | # return 3 unread items (ordered by date) 53 | hn.unread_items(3) 54 | # return all the read items 55 | hn.read_items 56 | # all the starred items 57 | hn.starred_items 58 | 59 | you get the idea. 60 | you can like items, star items, and mark them as read or unread. 61 | 62 | hn.all_unread_items.each {|item| item.toggle_read} 63 | 64 | subscribing to new feeds is also easy 65 | 66 | user.subscriptions.add "any feed url here" 67 | 68 | unsubscribing is best done via `remove_if` method 69 | 70 | user.subscriptions.remove_if {|feed| feed.title =~ /hacker news/i} 71 | 72 | Todo 73 | ---- 74 | 75 | * provide nicer convenience methods. (user.subscriptions.feeds is not a good way to access your feeds). 76 | * labels should be part of the api. -------------------------------------------------------------------------------- /lib/google-reader-api/api.rb: -------------------------------------------------------------------------------- 1 | module GoogleReaderApi 2 | 3 | class Api 4 | 5 | require "cgi" 6 | require "net/https" 7 | require "uri" 8 | 9 | BASE_URL = "http://www.google.com/reader/" 10 | 11 | 12 | # specify either the :email and :password or the :auth token you got in the past 13 | # 14 | # [:email] the user's email address for login purposes 15 | # 16 | # [:password] the user's password for login purposes 17 | # 18 | # [:auth] the auth token you got from a previous authentication request 19 | # if you provide this you do not need to provide the email and password 20 | def initialize(options) 21 | if options[:auth] 22 | @auth = options[:auth] 23 | @auth_type = options[:auth_type] || :client_login 24 | else 25 | request_auth(options[:email],options[:password]) 26 | end 27 | @cache = GoogleReaderApi::Cache.new(2) 28 | end 29 | 30 | # do a get request to the link 31 | # args is a hash of values that should be used in the request 32 | def get_link(link,args={}) 33 | link = BASE_URL + link 34 | get_request(link,args) 35 | end 36 | 37 | def post_link(link,args={}) 38 | link = BASE_URL + link 39 | post_request(link,args) 40 | end 41 | 42 | def cached_unread_count 43 | @cache['unread-count'] ||= get_link 'api/0/unread-count', :output => :json 44 | end 45 | 46 | # url as a string 47 | # the post data as a hash 48 | def post_request(url,args) 49 | uri = URI.parse(url) 50 | req = Net::HTTP::Post.new(uri.path) 51 | req.set_form_data(args) 52 | request(uri,req) 53 | end 54 | 55 | # the url as a string and the args as a hash 56 | # e.g. :allcomments => true etc... 57 | def get_request(url,args) 58 | uri = URI.parse url 59 | 60 | # ck is the current unix timestamp 61 | args[:ck] = Time.now.to_i unless args[:ck] 62 | 63 | req = Net::HTTP::Get.new("#{uri.path}?#{argument_string(args)}") 64 | request(uri,req) 65 | end 66 | 67 | def request(uri,request) 68 | # add the cookie to the http header 69 | if @auth_type == :bearer 70 | request.add_field('Authorization',"Bearer #{auth}") 71 | else 72 | request.add_field('Authorization',"GoogleLogin auth=#{auth}") 73 | end 74 | res = Net::HTTP.start(uri.host,uri.port) do |http| 75 | http.request(request) 76 | end 77 | # TODO: use better exception 78 | if res.code != '200' 79 | p res.body 80 | raise "something went wrong" 81 | end 82 | res.body 83 | end 84 | 85 | # returns the argumentstring based on the hash it is given 86 | def argument_string(args) 87 | args.to_a.map { |v| v.join '=' }.join('&') 88 | end 89 | 90 | def auth 91 | @auth 92 | end 93 | 94 | def request_auth(email,password) 95 | login = GoogleLogin::ClientLogin.new :service => 'reader', :source => 'nudded-greader-0.1' 96 | login.authenticate email, password 97 | @auth = login.auth 98 | end 99 | 100 | end 101 | 102 | end 103 | -------------------------------------------------------------------------------- /lib/google-reader-api/google_login.rb: -------------------------------------------------------------------------------- 1 | require "net/https" 2 | require "uri" 3 | 4 | module GoogleLogin 5 | 6 | # == ClientLogin 7 | # 8 | # Use this Class to get an auth-token 9 | class ClientLogin 10 | 11 | # Base Exception class 12 | LoginError = Class.new Exception 13 | 14 | # All the possible exceptions 15 | [ 16 | "BadAuthentication", 17 | "NotVerified", 18 | "TermsNotAgreed", 19 | "CaptchaRequired", 20 | "Unknown", 21 | "AccountDeleted", 22 | "AccountDisabled", 23 | "ServiceDisabled", 24 | "ServiceUnavailable", 25 | ].each do |const| 26 | const_set const, Class.new(LoginError) 27 | end 28 | 29 | DEFAULTS = { 30 | :accountType => 'HOSTED_OR_GOOGLE' , 31 | :source => 'companyName-applicationName-versionID', 32 | :service => 'service-identifier' 33 | } 34 | 35 | attr_reader :auth, :sid, :lsid, :captcha_url 36 | 37 | # specify the :service, :source and optionally :accountType 38 | # 39 | # [:service] the service identifier, check the google api documentation. 40 | # 41 | # [:source] the name of your application. String should be in the form 42 | # "companyName-applicationName-versionID". 43 | # 44 | # [:accountType] one of the following values: 45 | # "GOOGLE", "HOSTED", "HOSTED_OR_GOOGLE" (default if none 46 | # given) 47 | def initialize(arghash = {}) 48 | @options = DEFAULTS.merge arghash 49 | end 50 | 51 | # authenticate a user, which sets the auth, sid and lsid instance_variables 52 | # if you provide a block, it will be called with a captcha url if google 53 | # forces you to answer the captcha. Make sure you return the anwer in the block. 54 | # 55 | # if no block is given, this will raise a CaptchaRequired error. 56 | # you can rescue them and show the url via the captcha_url method. 57 | # 58 | # you can then call authenticate and as 3rd parameter you provide the 59 | # captcha answer. 60 | # 61 | # all Exceptions this raises are subclasses of ClientLogin::LoginError. 62 | # so make sure you handle them. 63 | # 64 | # This is a list of all the possible errors and their meaning 65 | # Error code:: Description 66 | # BadAuthentication:: The login request used a username or password that is not recognized. 67 | # NotVerified:: The account email address has not been verified. The user will need to access their Google account directly to resolve the issue before logging in using a non-Google application. 68 | # TermsNotAgreed:: The user has not agreed to terms. The user will need to access their Google account directly to resolve the issue before logging in using a non-Google application. 69 | # CaptchaRequired:: A CAPTCHA is required. (A response with this error code will also contain an image URL and a CAPTCHA token.) 70 | # Unknown:: The error is unknown or unspecified; the request contained invalid input or was malformed. 71 | # AccountDeleted:: The user account has been deleted. 72 | # AccountDisabled:: The user account has been disabled. 73 | # ServiceDisabled:: The user's access to the specified service has been disabled. (The user account may still be valid.) 74 | # ServiceUnavailable:: The service is not available; try again later. 75 | def authenticate(username, password, captcha_response = nil) 76 | @options[:Email], @options[:Passwd] = username, password 77 | # set logincaptcha, captchatoken will already be set 78 | @options[:logincaptcha] = captcha_response if captcha_response 79 | 80 | parse_response perform_request 81 | 82 | rescue CaptchaRequired 83 | if block_given? 84 | @options[:logincaptcha] = yield captcha_url 85 | retry 86 | else 87 | raise CaptchaRequired 88 | end 89 | end 90 | 91 | private 92 | 93 | def perform_request 94 | request = Net::HTTP::Post.new '/accounts/ClientLogin' 95 | request.form_data = @options 96 | 97 | https = Net::HTTP.new 'www.google.com', 443 98 | https.use_ssl = true 99 | 100 | https.request request 101 | end 102 | 103 | def parse_body(response_body) 104 | response_body.scan(/(\w+)=(.+)\n/).each do |key, value| 105 | instance_variable_set "@#{key.downcase}" , value 106 | end 107 | end 108 | 109 | def parse_response(response) 110 | if response.code_type == Net::HTTPOK 111 | parse_body response.body 112 | else 113 | handle_error response.body 114 | end 115 | end 116 | 117 | 118 | def handle_error(response_body) 119 | error_message = response_body.match(/Error=(\w+)\n/)[1].strip 120 | 121 | if error_message == "CaptchaRequired" 122 | @options[:logintoken] = response_body.match(/CaptchaToken=(.+)\n/)[1] 123 | self.captcha_url = response_body.match(/CaptchaUrl=(.+)\n/)[1] 124 | end 125 | 126 | raise_error_class error_message 127 | end 128 | 129 | def raise_error_class(error_message) 130 | raise self.class.const_get error_message 131 | end 132 | 133 | def captcha_url=(url) 134 | @captcha_url = "http://www.google.com/accounts/" << url 135 | end 136 | 137 | end 138 | 139 | end 140 | --------------------------------------------------------------------------------