├── Gemfile ├── lib ├── tumblr │ ├── version.rb │ ├── tagged.rb │ ├── config.rb │ ├── helpers.rb │ ├── connection.rb │ ├── client.rb │ ├── user.rb │ ├── request.rb │ ├── blog.rb │ └── post.rb └── tumblr_client.rb ├── .gitignore ├── spec ├── spec_helper.rb └── examples │ ├── tagged_spec.rb │ ├── client_spec.rb │ ├── request_spec.rb │ ├── user_spec.rb │ ├── post_spec.rb │ └── blog_spec.rb ├── .github └── workflows │ ├── validate_yaml.yaml │ └── ci.yaml ├── Rakefile ├── CONTRIBUTING.md ├── tumblr_client.gemspec ├── bin └── tumblr ├── README.md └── LICENSE /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /lib/tumblr/version.rb: -------------------------------------------------------------------------------- 1 | module Tumblr 2 | 3 | VERSION = '0.8.6' 4 | 5 | end 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | .DS_Store 3 | 4 | *.swp 5 | *.swo 6 | 7 | tmp 8 | coverage 9 | .rspec 10 | 11 | Gemfile.lock 12 | 13 | .bundle 14 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | if ENV['COV'] == '1' 2 | require 'simplecov' 3 | SimpleCov.start 4 | end 5 | 6 | require 'ostruct' 7 | require_relative '../lib/tumblr_client' 8 | -------------------------------------------------------------------------------- /lib/tumblr/tagged.rb: -------------------------------------------------------------------------------- 1 | module Tumblr 2 | module Tagged 3 | 4 | def tagged(tag, options={}) 5 | validate_options([:before, :limit, :filter], options) 6 | 7 | params = { :tag => tag, :api_key => @consumer_key } 8 | params.merge!(options) 9 | get("v2/tagged", params) 10 | end 11 | 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/tumblr_client.rb: -------------------------------------------------------------------------------- 1 | require 'tumblr/client' 2 | require 'tumblr/config' 3 | 4 | module Tumblr 5 | 6 | autoload :VERSION, File.join(File.dirname(__FILE__), 'tumblr/version') 7 | 8 | extend Config 9 | 10 | class << self 11 | def new(options={}) 12 | Tumblr::Client.new(options) 13 | end 14 | end 15 | 16 | end 17 | -------------------------------------------------------------------------------- /.github/workflows/validate_yaml.yaml: -------------------------------------------------------------------------------- 1 | # yamllint disable rule:line-length 2 | 3 | name: YAML Validation 4 | 5 | on: 6 | pull_request: 7 | push: 8 | 9 | jobs: 10 | validate-yaml: 11 | name: Validate YAML 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout code 16 | uses: actions/checkout@v2 17 | 18 | - name: Run YAML linter 19 | run: | 20 | find . -path \*/vendor -prune -false -o -name \*.y*ml | xargs yamllint -d relaxed 21 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | 3 | require 'rspec/core/rake_task' 4 | require File.dirname(__FILE__) + '/lib/tumblr/version' 5 | 6 | task :default => :test 7 | 8 | task :build => :test do 9 | system 'gem build tumblr_client.gemspec' 10 | end 11 | 12 | task :release => :build do 13 | # tag and push 14 | system "git tag v#{Tumblr::VERSION}" 15 | system "git push origin --tags" 16 | # push the gem 17 | system "gem push tumblr_client-#{Tumblr::VERSION}.gem" 18 | end 19 | 20 | RSpec::Core::RakeTask.new(:test) do |t| 21 | t.pattern = 'spec/**/*_spec.rb' 22 | fail_on_error = true # be explicit 23 | end 24 | -------------------------------------------------------------------------------- /spec/examples/tagged_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Tumblr::Tagged do 4 | 5 | let(:client) { Tumblr::Client.new } 6 | let(:consumer_key) { 'consumer' } 7 | let(:tag) { 'helloworld' } 8 | 9 | before do 10 | Tumblr.configure do |c| 11 | c.consumer_key = consumer_key 12 | end 13 | end 14 | 15 | describe :tagged do 16 | 17 | before do 18 | expect(client).to receive(:get).once.with('v2/tagged', { 19 | :tag => tag, 20 | :api_key => consumer_key 21 | }).and_return('response') 22 | end 23 | 24 | it 'should setup the request properly' do 25 | r = client.tagged tag 26 | expect(r).to eq('response') 27 | end 28 | 29 | end 30 | 31 | end 32 | -------------------------------------------------------------------------------- /lib/tumblr/config.rb: -------------------------------------------------------------------------------- 1 | module Tumblr 2 | module Config 3 | 4 | VALID_OPTIONS_KEYS = [ 5 | :consumer_key, 6 | :consumer_secret, 7 | :oauth_token, 8 | :oauth_token_secret, 9 | :client, 10 | :api_scheme 11 | ] 12 | 13 | attr_accessor *VALID_OPTIONS_KEYS 14 | 15 | def configure 16 | yield self 17 | self 18 | end 19 | 20 | def options 21 | options = {} 22 | VALID_OPTIONS_KEYS.each{ |pname| options[pname] = send(pname) } 23 | options 24 | end 25 | 26 | def credentials 27 | { 28 | :consumer_key => consumer_key, 29 | :consumer_secret => consumer_secret, 30 | :token => oauth_token, 31 | :token_secret => oauth_token_secret 32 | } 33 | end 34 | 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | # yamllint disable rule:line-length 2 | # yamllint disable rule:braces 3 | name: CI 4 | 5 | on: 6 | pull_request: 7 | push: 8 | branches: 9 | - main 10 | - master 11 | 12 | jobs: 13 | tests: 14 | name: Testing with Ruby ${{ matrix.ruby-version }} 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | ruby-version: 20 | - '2.6' 21 | - '2.7' 22 | - '3.0' 23 | 24 | steps: 25 | - name: Checkout code 26 | uses: actions/checkout@v2 27 | 28 | - name: Set up Ruby 29 | uses: ruby/setup-ruby@v1 30 | with: 31 | ruby-version: ${{ matrix.ruby-version }} 32 | bundler-cache: true 33 | 34 | - name: Run tests 35 | run: | 36 | bundle exec rspec spec 37 | -------------------------------------------------------------------------------- /lib/tumblr/helpers.rb: -------------------------------------------------------------------------------- 1 | module Tumblr 2 | module Helper 3 | 4 | private 5 | 6 | def blog_path(blog_name, ext) 7 | "v2/blog/#{full_blog_name(blog_name)}/#{ext}" 8 | end 9 | 10 | def full_blog_name(blog_name) 11 | blog_name.include?('.') ? blog_name : "#{blog_name}.tumblr.com" 12 | end 13 | 14 | def validate_options(valid_opts, opts) 15 | bad_opts = opts.select { |val| !valid_opts.include?(val) } 16 | if bad_opts.any? 17 | raise ArgumentError.new "Invalid options (#{bad_opts.keys.join(', ')}) passed, only #{valid_opts} allowed." 18 | end 19 | end 20 | 21 | def validate_no_collision(options, attributes) 22 | count = attributes.count { |attr| options.has_key?(attr) } 23 | if count > 1 24 | raise ArgumentError.new "Can only use one of: #{attributes.join(', ')} (Found #{count})" 25 | end 26 | end 27 | 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/tumblr/connection.rb: -------------------------------------------------------------------------------- 1 | require 'faraday' 2 | require 'faraday_middleware' 3 | 4 | module Tumblr 5 | module Connection 6 | 7 | def connection(options={}) 8 | options = options.clone 9 | 10 | default_options = { 11 | :headers => { 12 | :accept => 'application/json', 13 | :user_agent => "tumblr_client/#{Tumblr::VERSION}" 14 | }, 15 | :url => "#{api_scheme}://#{api_host}/" 16 | } 17 | 18 | client = Faraday.default_adapter 19 | 20 | Faraday.new(default_options.merge(options)) do |conn| 21 | data = { :api_host => api_host, :ignore_extra_keys => true}.merge(credentials) 22 | unless credentials.empty? 23 | conn.request :oauth, data 24 | end 25 | conn.request :multipart 26 | conn.request :url_encoded 27 | conn.response :json, :content_type => /\bjson$/ 28 | conn.adapter client 29 | end 30 | end 31 | 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We want to make contributing to tumblr_client as easy and transparent as possible. If you run into problems, please open an issue. We also actively welcome pull requests. 4 | 5 | ## Pull Requests 6 | 7 | 1. Fork the repo and create your branch from `master`. 8 | 2. If you've added code that should be tested, add tests. 9 | 3. If you've changed APIs, update the documentation. 10 | 4. Ensure the test suite passes. 11 | 5. If you haven't already, complete the Contributor License Agreement ("CLA"). 12 | 13 | ## Contributor License Agreement ("CLA") 14 | 15 | In order to accept your contribution, we need you to submit a CLA. If you open 16 | a pull request, a bot will automatically check if you have already submitted 17 | one. If not it will ask you to do so by visiting a link and signing in with 18 | GitHub. 19 | 20 | The CLA, contact information, and GitHub sign-in can be found here: 21 | [https://yahoocla.herokuapp.com](https://yahoocla.herokuapp.com). 22 | 23 | ## License 24 | 25 | By contributing to tumblr_client you agree that your contributions will be licensed under its Apache 2.0 license. 26 | 27 | -------------------------------------------------------------------------------- /tumblr_client.gemspec: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require File.join(File.dirname(__FILE__), 'lib/tumblr/version') 3 | 4 | Gem::Specification.new do |gem| 5 | gem.add_dependency 'faraday', '~> 1.0' 6 | gem.add_dependency 'faraday_middleware', '~> 1.0' 7 | gem.add_dependency 'json' 8 | gem.add_dependency 'simple_oauth' 9 | gem.add_dependency 'oauth' 10 | gem.add_dependency 'mime-types' 11 | gem.add_development_dependency 'rake' 12 | gem.add_development_dependency 'rspec' 13 | gem.add_development_dependency 'webmock' 14 | gem.add_development_dependency 'simplecov' 15 | gem.authors = ['John Bunting', 'John Crepezzi'] 16 | gem.description = %q{A Ruby wrapper for the Tumblr v2 API} 17 | gem.email = ['codingjester@gmail.com', 'john@crepezzi.com'] 18 | gem.executables = `git ls-files -- bin/*`.split("\n").map{|f| File.basename(f)} 19 | gem.files = `git ls-files`.split("\n") 20 | gem.homepage = "http://github.com/tumblr/tumblr_client" 21 | gem.license = "Apache" 22 | gem.name = "tumblr_client" 23 | gem.require_paths = ["lib"] 24 | gem.summary = %q{Tumblr API wrapper} 25 | gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 26 | gem.version = Tumblr::VERSION 27 | end 28 | -------------------------------------------------------------------------------- /lib/tumblr/client.rb: -------------------------------------------------------------------------------- 1 | require 'tumblr/blog' 2 | require 'tumblr/user' 3 | require 'tumblr/request' 4 | require 'tumblr/connection' 5 | require 'tumblr/post' 6 | require 'tumblr/tagged' 7 | require 'tumblr/helpers' 8 | 9 | module Tumblr 10 | class Client 11 | 12 | class << self 13 | def default_api_host 14 | ENV['TUMBLR_API_HOST'] || 'api.tumblr.com' 15 | end 16 | end 17 | 18 | include Tumblr::Request 19 | include Tumblr::Blog 20 | include Tumblr::User 21 | include Tumblr::Post 22 | include Tumblr::Tagged 23 | include Tumblr::Helper 24 | include Tumblr::Connection 25 | 26 | def initialize(attrs= {}) 27 | attrs = Tumblr.options.merge(attrs) 28 | Config::VALID_OPTIONS_KEYS.each do |key| 29 | instance_variable_set("@#{key}".to_sym, attrs[key]) 30 | end 31 | end 32 | 33 | def api_host 34 | self.class.default_api_host 35 | end 36 | 37 | def api_scheme 38 | @api_scheme || 'https' 39 | end 40 | 41 | def credentials 42 | { 43 | :consumer_key => @consumer_key, 44 | :consumer_secret => @consumer_secret, 45 | :token => @oauth_token, 46 | :token_secret => @oauth_token_secret 47 | } 48 | end 49 | 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/tumblr/user.rb: -------------------------------------------------------------------------------- 1 | module Tumblr 2 | module User 3 | 4 | def info 5 | get('v2/user/info') 6 | end 7 | 8 | def dashboard(options = {}) 9 | valid_opts = [:limit, :offset, :type, :since_id, :reblog_info, :notes_info] 10 | validate_options(valid_opts, options) 11 | get('v2/user/dashboard', options) 12 | end 13 | 14 | def likes(options = {}) 15 | validate_options([:limit, :offset, :before, :after], options) 16 | get('v2/user/likes', options) 17 | end 18 | 19 | def following(options = {}) 20 | validate_options([:limit, :offset], options) 21 | get('v2/user/following', options) 22 | end 23 | 24 | def follow(url) 25 | post('v2/user/follow', :url => url) 26 | end 27 | 28 | def unfollow(url) 29 | post('v2/user/unfollow', :url => url) 30 | end 31 | 32 | def like(id, reblog_key) 33 | post('v2/user/like', :id => id, :reblog_key => reblog_key) 34 | end 35 | 36 | def unlike(id, reblog_key) 37 | post('v2/user/unlike', :id => id, :reblog_key => reblog_key) 38 | end 39 | 40 | def filtered_content 41 | get('v2/user/filtered_content') 42 | end 43 | alias_method :get_filtered_content, :filtered_content 44 | 45 | def add_filtered_content(filtered_strings=nil, options={}) 46 | validate_options([:filtered_content], options) 47 | options[:filtered_content] ||= filtered_strings 48 | post('v2/user/filtered_content', options) 49 | end 50 | 51 | def delete_filtered_content(filtered_strings, options={}) 52 | validate_options([:filtered_content], options) 53 | options[:filtered_content] ||= filtered_strings 54 | delete('v2/user/filtered_content', options) 55 | end 56 | 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /spec/examples/client_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Tumblr::Client do 4 | 5 | context 'when using the generic copy' do 6 | 7 | before do 8 | @key = 'thekey' 9 | Tumblr.configure do |c| 10 | c.consumer_key = @key 11 | end 12 | end 13 | 14 | it 'should give new clients those credentials' do 15 | client = Tumblr::Client.new 16 | expect(client.credentials[:consumer_key]).to eq(@key) 17 | end 18 | 19 | it 'should have it\'s own credentials' do 20 | expect(Tumblr.credentials[:consumer_key]).to eq(@key) 21 | end 22 | 23 | it 'should be able to make a new client (using these credentials)' do 24 | expect(Tumblr.new).to be_a(Tumblr::Client) 25 | expect(Tumblr.new.credentials[:consumer_key]).to eq(@key) 26 | end 27 | 28 | end 29 | 30 | context 'when using custom copies of the client' do 31 | 32 | before do 33 | @client1 = Tumblr::Client.new(:consumer_key => 'key1') 34 | @client2 = Tumblr::Client.new(:consumer_key => 'key2') 35 | end 36 | 37 | it 'should keep them separate' do 38 | expect([ 39 | @client1.credentials[:consumer_key], 40 | @client2.credentials[:consumer_key] 41 | ].uniq.count).to eq(2) 42 | end 43 | 44 | end 45 | 46 | describe :api_scheme do 47 | 48 | it 'defaults to https' do 49 | expect(Tumblr::Client.new.api_scheme).to eq('https') 50 | end 51 | 52 | it 'can be set by the initializer' do 53 | client = Tumblr::Client.new(:api_scheme => 'http') 54 | expect(client.api_scheme).to eq('http') 55 | end 56 | 57 | it 'can be set globally' do 58 | Tumblr.configure do |c| 59 | c.api_scheme = 'http' 60 | end 61 | expect(Tumblr::Client.new.api_scheme).to eq('http') 62 | end 63 | 64 | end 65 | 66 | end 67 | -------------------------------------------------------------------------------- /lib/tumblr/request.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | 3 | module Tumblr 4 | module Request 5 | 6 | # Perform a get request and return the raw response 7 | def get_response(path, params = {}) 8 | connection.get do |req| 9 | req.url path 10 | req.params = params 11 | end 12 | end 13 | 14 | # get a redirect url 15 | def get_redirect_url(path, params = {}) 16 | response = get_response path, params 17 | if response.status == 301 18 | response.headers['Location'] 19 | else 20 | response.body['meta'] 21 | end 22 | end 23 | 24 | # Performs a get request 25 | def get(path, params={}) 26 | respond get_response(path, params) 27 | end 28 | 29 | # Performs post request 30 | def post(path, params={}) 31 | if Array === params[:tags] 32 | params[:tags] = params[:tags].join(',') 33 | end 34 | response = connection.post do |req| 35 | req.url path 36 | req.body = params unless params.empty? 37 | end 38 | #Check for errors and encapsulate 39 | respond(response) 40 | end 41 | 42 | # Performs put request 43 | def put(path, params={}) 44 | if Array === params[:tags] 45 | params[:tags] = params[:tags].join(',') 46 | end 47 | response = connection.put do |req| 48 | req.url path 49 | req.body = params unless params.empty? 50 | end 51 | respond(response) 52 | end 53 | 54 | # Performs delete request 55 | def delete(path, params={}) 56 | response = connection.delete do |req| 57 | req.url path 58 | req.body = params unless params.empty? 59 | end 60 | respond(response) 61 | end 62 | 63 | def respond(response) 64 | if [201, 200].include?(response.status) 65 | response.body['response'] 66 | else 67 | # surface the meta alongside response 68 | res = response.body['meta'] || {} 69 | res.merge! response.body['response'] if response.body['response'].is_a?(Hash) 70 | res 71 | end 72 | end 73 | 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /bin/tumblr: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | $:.unshift File.join File.dirname(__FILE__), '..', 'lib' 3 | require 'rubygems' 4 | require 'tumblr_client' 5 | require 'oauth' 6 | require 'yaml' 7 | require 'irb' 8 | require 'irb/completion' 9 | 10 | path = File.join ENV['HOME'], '.tumblr' 11 | 12 | host = Tumblr::Client.default_api_host.dup 13 | host.gsub! 'api.', 'www.' 14 | 15 | if File.exist?(path) 16 | 17 | # Load configuration from data 18 | configuration = YAML.load_file path 19 | Tumblr.configure do |config| 20 | Tumblr::Config::VALID_OPTIONS_KEYS.each do |key| 21 | config.send(:"#{key}=", configuration[key.to_s]) 22 | end 23 | end 24 | 25 | else 26 | 27 | Tumblr.configure do |config| 28 | 29 | puts "Register an application at: http://#{host}/oauth/apps" 30 | print 'OAuth Consumer key: ' 31 | config.consumer_key = gets.chomp 32 | 33 | print 'OAuth Consumer secret: ' 34 | config.consumer_secret = gets.chomp 35 | 36 | site = "http://#{host}" 37 | consumer = OAuth::Consumer.new(config.consumer_key, config.consumer_secret, :site => site) 38 | request_token = consumer.get_request_token :exclude_callback => true 39 | 40 | puts 41 | 42 | puts request_token.authorize_url 43 | puts "Post-redirect, copy the oauth_verifier" 44 | print 'OAuth Verifier: ' 45 | verifier = gets.chomp 46 | 47 | access_token = request_token.get_access_token :oauth_verifier => verifier 48 | config.oauth_token = access_token.token 49 | config.oauth_token_secret = access_token.secret 50 | 51 | end 52 | 53 | # Save credentials 54 | File.open(path, 'w') do |f| 55 | configuration = {} 56 | Tumblr::Config::VALID_OPTIONS_KEYS.each do |key| 57 | configuration[key.to_s] = Tumblr.send(key) 58 | end 59 | f.write YAML.dump configuration 60 | end 61 | 62 | end 63 | 64 | 65 | ENV['IRBRC'] = '.irbrc' if File.exists? '.irbrc' 66 | 67 | puts %q[ 68 | . .o8 oooo 69 | .o8 "888 `888 70 | .o888oo oooo oooo ooo. .oo. .oo. 888oooo. 888 oooo d8b 71 | 888 `888 `888 `888P"Y88bP"Y88b d88' `88b 888 `888""8P 72 | 888 888 888 888 888 888 888 888 888 888 73 | 888 . 888 888 888 888 888 888 888 888 888 .o. 74 | "888" `V88V"V8P' o888o o888o o888o `Y8bod8P' o888o d888b Y8P 75 | 76 | ] 77 | 78 | ARGV.clear 79 | IRB.start 80 | exit! 81 | -------------------------------------------------------------------------------- /spec/examples/request_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Tumblr::Request do 4 | 5 | let(:client) { Tumblr::Client.new } 6 | 7 | describe :respond do 8 | 9 | [200, 201].each do |rcode| 10 | 11 | context "with a #{rcode} response" do 12 | 13 | it 'should return the meta object' do 14 | data = { :message => 'ohyes' } 15 | response = OpenStruct.new(:status => rcode, :body => { 'response' => data }) 16 | expect(client.respond(response)).to eq(data) 17 | end 18 | 19 | end 20 | 21 | end 22 | 23 | context 'with an error response' do 24 | 25 | it 'should return the meta object (merged with response)' do 26 | meta = { :message => 'ohno' } 27 | response = OpenStruct.new(:status => 401, :body => { 'meta' => meta, 'response' => { :also => 'hi' } }) 28 | expect(client.respond(response)).to eq({ :message => 'ohno', :also => 'hi' }) 29 | end 30 | 31 | it 'should return the meta object even when response is nil' do 32 | meta = { :message => 'ohno' } 33 | response = OpenStruct.new(:status => 401, :body => { 'meta' => meta, 'response' => nil }) 34 | expect(client.respond(response)).to eq(meta) 35 | end 36 | 37 | end 38 | 39 | end 40 | 41 | describe :get do 42 | 43 | before do 44 | @path = '/the/path' 45 | @params = { :hello => 'world' } 46 | expect(client).to receive(:get_response).once.with(@path, @params). 47 | and_return(OpenStruct.new({ 48 | :status => 200, 49 | :body => { 'response' => 'result' } 50 | })) 51 | end 52 | 53 | it 'should get the response directly' do 54 | expect(client.get(@path, @params)).to eq('result') 55 | end 56 | 57 | end 58 | 59 | describe :get_redirect_url do 60 | 61 | context 'when redirect is found' do 62 | 63 | before do 64 | @path = '/the/path' 65 | @params = { :hello => 'world' } 66 | @redirect_url = 'redirect-to-here' 67 | expect(client).to receive(:get_response).once.with(@path, @params). 68 | and_return(OpenStruct.new({ 69 | :status => 301, 70 | :headers => { 'Location' => @redirect_url } 71 | })) 72 | end 73 | 74 | it 'should return the redirect url' do 75 | expect(client.get_redirect_url(@path, @params)).to eq(@redirect_url) 76 | end 77 | 78 | end 79 | 80 | context 'when error is encountered' do 81 | 82 | before do 83 | @path = '/the/path' 84 | @params = { :hello => 'world' } 85 | @meta = { :message => 'ohno' } 86 | expect(client).to receive(:get_response).once.with(@path, @params). 87 | and_return(OpenStruct.new({ 88 | :status => 401, 89 | :body => { 'meta' => @meta } 90 | })) 91 | end 92 | 93 | it 'should return the error meta' do 94 | expect(client.get_redirect_url(@path, @params)).to eq(@meta) 95 | end 96 | 97 | end 98 | 99 | end 100 | 101 | end 102 | -------------------------------------------------------------------------------- /spec/examples/user_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Tumblr::User do 4 | 5 | let(:client) { Tumblr::Client.new } 6 | 7 | describe :info do 8 | 9 | it 'should make the request properly' do 10 | expect(client).to receive(:get).with('v2/user/info').and_return('response') 11 | r = client.info 12 | expect(r).to eq('response') 13 | end 14 | 15 | end 16 | 17 | describe :dashboard do 18 | 19 | context 'when using options that are not allowed' do 20 | 21 | it 'should raise an error' do 22 | expect(lambda { 23 | client.dashboard :not => 'an option' 24 | }).to raise_error ArgumentError 25 | end 26 | 27 | end 28 | 29 | context 'when using valid options' do 30 | 31 | it 'should make the correct call' do 32 | expect(client).to receive(:get).with('v2/user/dashboard', { 33 | :limit => 25 34 | }).and_return('response') 35 | r = client.dashboard :limit => 25 36 | expect(r).to eq('response') 37 | end 38 | 39 | end 40 | 41 | end 42 | 43 | # These two are very similar 44 | [:following, :likes].each do |type| 45 | 46 | describe type do 47 | 48 | context 'with defaults' do 49 | 50 | it 'should make the reqest properly' do 51 | expect(client).to receive(:get).with("v2/user/#{type}", {}). 52 | and_return('response') 53 | r = client.send type 54 | expect(r).to eq('response') 55 | end 56 | 57 | end 58 | 59 | context 'with custom limit & offset' do 60 | 61 | it 'should make the reqest properly' do 62 | expect(client).to receive(:get).with("v2/user/#{type}", { 63 | :limit => 10, 64 | :offset => 5 65 | }).and_return('response') 66 | r = client.send type, :offset => 5, :limit => 10 67 | expect(r).to eq('response') 68 | end 69 | 70 | end 71 | 72 | end 73 | 74 | end 75 | 76 | # Like and unlike are similar 77 | [:like, :unlike].each do |type| 78 | 79 | describe type do 80 | 81 | it 'should make the request properly' do 82 | id = 123 83 | reblog_key = 'hello' 84 | expect(client).to receive(:post).with("v2/user/#{type}", { 85 | :id => id, 86 | :reblog_key => reblog_key 87 | }).and_return('response') 88 | r = client.send type, id, reblog_key 89 | expect(r).to eq('response') 90 | end 91 | 92 | end 93 | 94 | end 95 | 96 | # Follow and unfollow are similar 97 | [:follow, :unfollow].each do |type| 98 | 99 | describe type do 100 | 101 | it 'should make the request properly' do 102 | url = 'some url' 103 | expect(client).to receive(:post).with("v2/user/#{type}", { 104 | :url => url 105 | }).and_return('response') 106 | r = client.send type, url 107 | expect(r).to eq('response') 108 | end 109 | 110 | end 111 | 112 | end 113 | 114 | describe :filtered_content do 115 | it 'should make the reqest properly' do 116 | expect(client).to receive(:get).with("v2/user/filtered_content").and_return('response') 117 | r = client.filtered_content 118 | expect(r).to eq('response') 119 | end 120 | end 121 | 122 | describe :add_filtered_content do 123 | it 'should make the reqest properly' do 124 | expect(client).to receive(:post).with("v2/user/filtered_content", filtered_content: ['str']).and_return('response') 125 | r = client.add_filtered_content ['str'] 126 | expect(r).to eq('response') 127 | end 128 | end 129 | 130 | describe :delete_filtered_content do 131 | it 'should make the reqest properly' do 132 | expect(client).to receive(:delete).with("v2/user/filtered_content", filtered_content: ['str']).and_return('response') 133 | r = client.delete_filtered_content ['str'] 134 | expect(r).to eq('response') 135 | end 136 | end 137 | 138 | end 139 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tumblr Ruby Gem 2 | 3 | [![Gem Version](https://badge.fury.io/rb/tumblr_client.png)](https://badge.fury.io/rb/tumblr_client) [![Build Status](https://secure.travis-ci.org/tumblr/tumblr_client.png)](https://travis-ci.org/tumblr/tumblr_client) 4 | 5 | This is the official Ruby wrapper for the Tumblr v2 API. It supports all endpoints currently available on the [Tumblr API](https://www.tumblr.com/docs/en/api/v2). 6 | 7 | ## Installation 8 | 9 | ``` 10 | gem install tumblr_client 11 | ``` 12 | 13 | ## Usage 14 | 15 | First and foremost, this gem will *not* do a three legged oauth request for you. It is just a wrapper to help make your life easier when using the v2 api. If you need to do the full oauth workflow, then please check out the [Ruby OAuth Gem](http://oauth.rubyforge.org/). 16 | 17 | ### Configuration 18 | 19 | Configuration for the gem is actually pretty easy: 20 | 21 | ```ruby 22 | Tumblr.configure do |config| 23 | config.consumer_key = "consumer_key" 24 | config.consumer_secret = "consumer_secret" 25 | config.oauth_token = "access_token" 26 | config.oauth_token_secret = "access_token_secret" 27 | end 28 | ``` 29 | 30 | Once you have your configuration squared away it's time to make some requests! 31 | 32 | ```ruby 33 | >> client = Tumblr::Client.new 34 | ``` 35 | 36 | That's it! You now have a client that can make any request to the Tumblr API. 37 | 38 | Also since the client is created with the amazing library [Faraday](https://github.com/lostisland/faraday), you can configure it to use any HTTP Client it supports. 39 | 40 | ```ruby 41 | >> client = Tumblr::Client.new(:client => :httpclient) 42 | ``` 43 | 44 | ### Some quick examples 45 | 46 | Getting user information: 47 | 48 | ```ruby 49 | >> client.info 50 | ``` 51 | 52 | Getting a specific blog's posts and type: 53 | 54 | ```ruby 55 | # Grabbing a specific blogs posts 56 | >> client.posts("codingjester.tumblr.com") 57 | 58 | # Grabbing only the last 10 photos off the blog 59 | >> client.posts("codingjester.tumblr.com", :type => "photo", :limit => 10) 60 | ``` 61 | 62 | Posting some photos to Tumblr: 63 | 64 | ```ruby 65 | # Uploads a great photoset 66 | >> client.photo("codingjester.tumblr.com", {:data => ['/path/to/pic.jpg', '/path/to/pic.jpg']}) 67 | ``` 68 | 69 | ### The irb Console 70 | 71 | Finally, there is an irb console packaged with the gem that should help you test any calls you want to make. The magic here is that you have a `.tumblr` file in your home directory. Inside this file it's just a basic YAML layout with four lines: 72 | 73 | ```yaml 74 | consumer_key: "your_consumer_key" 75 | consumer_secret: "your_consumer_secret" 76 | oauth_token: "your_access_token" 77 | oauth_token_secret: "your_access_token_secret" 78 | ``` 79 | 80 | From there, you should be able to run any of the above commands, with no problem! Just fire off the command `tumblr` from the terminal and you should be dropped into a console. 81 | 82 | --- 83 | 84 | The first time that you go to use the irb console, if you have no `.tumblr` file, it will walk you through the process of generating one. You will be prompted for your `consumer_key` and `consumer_secret` (which you can get here: https://www.tumblr.com/oauth/register) and then sent out to the site to verify your account. Once you verify, you will be redirected to your redirect URL (localhost by default) and copy the `oauth_verifier` back into the console. Then you're all set! 85 | 86 | ### Contributions and Pull Requests 87 | 88 | No request is too small and I encourage everyone to get involved. As you can see, we're sorely lacking in tests! So please if you would like to contribute, let me know and throw me a pull request! 89 | 90 | ### Requirements 91 | 92 | * Ruby 1.9.x to 3.x.x 93 | 94 | --- 95 | 96 | Copyright 2013 Tumblr, Inc. 97 | 98 | Licensed under the Apache License, Version 2.0 (the "License"); you may not 99 | use this work except in compliance with the License. You may obtain a copy of 100 | the License in the LICENSE file, or at: 101 | 102 | http://www.apache.org/licenses/LICENSE-2.0 103 | 104 | Unless required by applicable law or agreed to in writing, software 105 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 106 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 107 | License for the specific language governing permissions and limitations. 108 | -------------------------------------------------------------------------------- /lib/tumblr/blog.rb: -------------------------------------------------------------------------------- 1 | module Tumblr 2 | module Blog 3 | 4 | # Gets the info about the blog 5 | def blog_info(blog_name) 6 | get(blog_path(blog_name, 'info'), :api_key => @consumer_key) 7 | end 8 | 9 | # Gets the avatar URL of specified size 10 | def avatar(blog_name, size = nil) 11 | url = blog_path(blog_name, 'avatar') 12 | url = "#{url}/#{size}" if size 13 | get_redirect_url(url) 14 | end 15 | 16 | # Gets the list of followers for the blog 17 | def followers(blog_name, options = {}) 18 | validate_options([:limit, :offset], options) 19 | get(blog_path(blog_name, 'followers'), options) 20 | end 21 | 22 | # Gets the list of blogs the user is following 23 | def blog_following(blog_name, options = {}) 24 | validate_options([:limit, :offset], options) 25 | get(blog_path(blog_name, 'following'), options) 26 | end 27 | 28 | # Determines whether own blog (followee_blog) is followed by follower_blog 29 | # (if authorized) 30 | def followed_by(followee_blog, follower_blog=nil, **options) 31 | validate_options([:query], options) 32 | options[:query] ||= follower_blog 33 | get(blog_path(followee_blog, 'followed_by'), options) 34 | end 35 | alias_method :blog_followed_by, :followed_by 36 | 37 | # Gets the list of likes for the blog 38 | def blog_likes(blog_name, options = {}) 39 | validate_options([:limit, :offset, :before, :after], options) 40 | url = blog_path(blog_name, 'likes') 41 | 42 | params = { :api_key => @consumer_key } 43 | params.merge! options 44 | get(url, params) 45 | end 46 | 47 | # Get public posts from blog 48 | def posts(blog_name, options = {}) 49 | url = blog_path(blog_name, 'posts') 50 | if options.has_key?(:type) 51 | url = "#{url}/#{options[:type]}" 52 | end 53 | 54 | params = { :api_key => @consumer_key } 55 | params.merge! options 56 | get(url, params) 57 | end 58 | 59 | # Get post of given ID from blog 60 | def get_post(blog_name, post_id, **options) 61 | validate_options([:post_format], options) 62 | get(blog_path(blog_name, "posts/#{post_id}"), options) 63 | end 64 | 65 | # Get notes for post of given ID 66 | def notes(blog_name, post_id=nil, options = {}) 67 | validate_options([:id, :before_timestamp, :mode], options) 68 | options[:id] ||= post_id 69 | get(blog_path(blog_name, 'notes'), options) 70 | end 71 | 72 | # Get queued posts from blog (if authorized) 73 | def queue(blog_name, options = {}) 74 | validate_options([:limit, :offset], options) 75 | get(blog_path(blog_name, 'posts/queue'), options) 76 | end 77 | 78 | # Reorder blog's queue (if authorized) 79 | def reorder_queue(blog_name, options = {}) 80 | validate_options([:post_id, :insert_after], options) 81 | post(blog_path(blog_name, 'posts/queue/reorder'), options) 82 | end 83 | 84 | # Shuffle blog's queue (if authorized) 85 | def shuffle_queue(blog_name) 86 | post(blog_path(blog_name, 'posts/queue/shuffle')) 87 | end 88 | 89 | # Get drafts posts from blog (if authorized) 90 | def draft(blog_name, options = {}) 91 | validate_options([:limit, :before_id], options) 92 | get(blog_path(blog_name, 'posts/draft'), options) 93 | end 94 | alias_method :drafts, :draft 95 | 96 | # Get pending submissions posts from blog (if authorized) 97 | def submissions(blog_name, options = {}) 98 | validate_options([:limit, :offset], options) 99 | get(blog_path(blog_name, 'posts/submission'), options) 100 | end 101 | alias_method :submission, :submissions 102 | 103 | # Get notifications for blog (if authorized) 104 | def notifications(blog_name, options = {}) 105 | validate_options([:before, :types], options) 106 | get(blog_path(blog_name, 'notifications'), options) 107 | end 108 | 109 | # Get blogs blocked by blog (if authorized) 110 | def blocks(blog_name, options = {}) 111 | validate_options([:limit, :offset], options) 112 | get(blog_path(blog_name, 'blocks'), options) 113 | end 114 | alias_method :blocked, :blocks 115 | 116 | # Block a blog (blockee_blog) from blocker_blog (if authorized) 117 | def block(blocker_blog, blockee_blog=nil, **options) 118 | validate_options([:blocked_tumblelog, :post_id], options) 119 | options[:blocked_tumblelog] ||= blockee_blog 120 | post(blog_path(blocker_blog, 'blocks'), options) 121 | end 122 | 123 | # Unblock a blog (blockee_blog) from blocker_blog (if authorized) 124 | def unblock(blocker_blog, blockee_blog=nil, **options) 125 | validate_options([:blocked_tumblelog, :anonymous_only], options) 126 | options[:blocked_tumblelog] ||= blockee_blog 127 | delete(blog_path(blocker_blog, 'blocks'), options) 128 | end 129 | 130 | end 131 | end 132 | -------------------------------------------------------------------------------- /lib/tumblr/post.rb: -------------------------------------------------------------------------------- 1 | require 'mime/types' 2 | 3 | module Tumblr 4 | module Post 5 | 6 | STANDARD_POST_OPTIONS = [:state, :tags, :tweet, :date, :markdown, :slug, :format] 7 | DATA_POST_TYPES = [:audio, :video, :photo] 8 | VALID_POST_TYPES = DATA_POST_TYPES + [:quote, :text, :link, :chat] 9 | 10 | def edit(blog_name, options = {}) 11 | convert_source_array :source, options 12 | extract_data!(options) if DATA_POST_TYPES.include?(options[:type]) 13 | 14 | post(blog_path(blog_name, 'post/edit'), options) 15 | end 16 | 17 | def reblog(blog_name, options = {}) 18 | post(blog_path(blog_name, 'post/reblog'), options) 19 | end 20 | 21 | def delete(blog_name, id) 22 | post(blog_path(blog_name, 'post/delete'), :id => id) 23 | end 24 | 25 | def photo(blog_name, options = {}) 26 | valid_opts = STANDARD_POST_OPTIONS + [:caption, :link, :data, :source, :photoset_layout] 27 | validate_options(valid_opts, options) 28 | validate_no_collision options, [:data, :source] 29 | convert_source_array :source, options 30 | 31 | options[:type] = 'photo' 32 | extract_data!(options) 33 | post(post_path(blog_name), options) 34 | end 35 | 36 | def quote(blog_name, options = {}) 37 | valid_opts = STANDARD_POST_OPTIONS + [:quote, :source] 38 | validate_options(valid_opts, options) 39 | 40 | options[:type] = 'quote' 41 | post(post_path(blog_name), options) 42 | end 43 | 44 | def text(blog_name, options = {}) 45 | valid_opts = STANDARD_POST_OPTIONS + [:title, :body] 46 | validate_options(valid_opts, options) 47 | 48 | options[:type] = 'text' 49 | post(post_path(blog_name), options) 50 | end 51 | 52 | def link(blog_name, options = {}) 53 | valid_opts = STANDARD_POST_OPTIONS + [:title, :url, :description] 54 | validate_options(valid_opts, options) 55 | 56 | options[:type] = 'link' 57 | post(post_path(blog_name), options) 58 | end 59 | 60 | def chat(blog_name, options = {}) 61 | valid_opts = STANDARD_POST_OPTIONS + [:title, :conversation] 62 | validate_options(valid_opts, options) 63 | 64 | options[:type] = 'chat' 65 | post(post_path(blog_name), options) 66 | end 67 | 68 | def audio(blog_name, options = {}) 69 | valid_opts = STANDARD_POST_OPTIONS + [:data, :caption, :external_url] 70 | validate_options(valid_opts, options) 71 | validate_no_collision options, [:data, :external_url] 72 | 73 | options[:type] = 'audio' 74 | extract_data!(options) 75 | post(post_path(blog_name), options) 76 | end 77 | 78 | def video(blog_name, options = {}) 79 | valid_opts = STANDARD_POST_OPTIONS + [:data, :embed, :caption] 80 | validate_options(valid_opts, options) 81 | validate_no_collision options, [:data, :embed] 82 | 83 | options[:type] = 'video' 84 | extract_data!(options) 85 | post(post_path(blog_name), options) 86 | end 87 | 88 | def create_post(type, blog_name, options = {}) 89 | if VALID_POST_TYPES.include?(type) 90 | send(type, blog_name, options) 91 | else 92 | raise ArgumentError.new "\"#{type}\" is not a valid post type" 93 | end 94 | end 95 | 96 | private 97 | 98 | def post_path(blog_name) 99 | blog_path(blog_name, 'post') 100 | end 101 | 102 | # Allow source to be passed as an Array 103 | def convert_source_array(key, options) 104 | if options.has_key?(key) && options[key].kind_of?(Array) 105 | options[key].each.with_index do |src, idx| 106 | options["#{key.to_s}[#{idx}]"] = src 107 | end 108 | options.delete(key) 109 | end 110 | end 111 | 112 | # Look for the various ways that data can be passed, and normalize 113 | # the result in this hash 114 | def extract_data!(options) 115 | if options.has_key?(:data) 116 | data = options.delete :data 117 | 118 | if Array === data 119 | data.each.with_index do |filepath, idx| 120 | if filepath.is_a?(Faraday::UploadIO) 121 | options["data[#{idx}]"] = filepath 122 | else 123 | mime_type = extract_mimetype(filepath) 124 | options["data[#{idx}]"] = Faraday::UploadIO.new(filepath, mime_type) 125 | end 126 | end 127 | elsif data.is_a?(Faraday::UploadIO) 128 | options["data"] = data 129 | else 130 | mime_type = extract_mimetype(data) 131 | options["data"] = Faraday::UploadIO.new(data, mime_type) 132 | end 133 | end 134 | end 135 | 136 | def extract_mimetype(filepath) 137 | mime = MIME::Types.type_for(filepath) 138 | if (mime.empty?) 139 | mime_type = "application/octet-stream" 140 | else 141 | mime_type = MIME::Types.type_for(filepath)[0].content_type 142 | end 143 | mime_type 144 | end 145 | 146 | end 147 | end 148 | -------------------------------------------------------------------------------- /spec/examples/post_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Tumblr::Post do 4 | 5 | let(:client) { Tumblr::Client.new } 6 | let(:blog_name) { 'blog.name' } 7 | let(:file_path) { '/path/to/the/file' } 8 | let(:file_data) { 'lol cats' } 9 | let(:source) { 'the source' } 10 | let(:post_id) { 42 } 11 | 12 | describe :delete do 13 | 14 | context 'when deleting a post' do 15 | 16 | before do 17 | expect(client).to receive(:post).once.with("v2/blog/#{blog_name}/post/delete", { 18 | :id => post_id 19 | }) 20 | end 21 | 22 | it 'should setup a delete properly' do 23 | client.delete blog_name, post_id 24 | end 25 | 26 | end 27 | 28 | end 29 | 30 | describe :edit do 31 | [:photo, :audio, :video].each do |type| 32 | describe type do 33 | context 'when passing data as an array of filepaths' do 34 | before do 35 | fakefile = OpenStruct.new :read => file_data 36 | allow(File).to receive(:open).with(file_path + '.jpg').and_return(fakefile) 37 | expect(client).to receive(:post).once.with("v2/blog/#{blog_name}/post/edit", { 38 | 'data[0]' => kind_of(Faraday::UploadIO), 39 | :id => 123, 40 | :type => type 41 | }).and_return('response') 42 | end 43 | 44 | it 'should be able to pass data as an array of filepaths' do 45 | r = client.edit blog_name, :data => [file_path + ".jpg"], :id => 123, :type => type 46 | expect(r).to eq('response') 47 | end 48 | 49 | it 'should be able to pass data as an array of uploadios' do 50 | r = client.edit blog_name, :data => [Faraday::UploadIO.new(StringIO.new, 'image/jpeg')], :id => 123, :type => type 51 | expect(r).to eq('response') 52 | end 53 | 54 | end 55 | 56 | context 'when passing data different ways' do 57 | 58 | before do 59 | fakefile = OpenStruct.new :read => file_data 60 | allow(File).to receive(:open).with(file_path + '.jpg').and_return(fakefile) 61 | expect(client).to receive(:post).once.with("v2/blog/#{blog_name}/post/edit", { 62 | 'data' => kind_of(Faraday::UploadIO), 63 | :id => 123, 64 | :type => type 65 | }).and_return('response') 66 | end 67 | 68 | it 'should be able to pass data as a single filepath' do 69 | r = client.edit blog_name, :data => file_path + ".jpg", :id => 123, :type => type 70 | expect(r).to eq('response') 71 | end 72 | 73 | it 'should be able to pass data as a single uploadio' do 74 | r = client.edit blog_name, :data => Faraday::UploadIO.new(StringIO.new, 'image/jpeg'), :id => 123, :type => type 75 | expect(r).to eq('response') 76 | end 77 | 78 | end 79 | end 80 | end 81 | 82 | it 'should make the correct call' do 83 | expect(client).to receive(:post).once.with("v2/blog/#{blog_name}/post/edit", { 84 | :id => 123 85 | }).and_return('response') 86 | r = client.edit blog_name, :id => 123 87 | expect(r).to eq('response') 88 | end 89 | end 90 | 91 | describe :reblog do 92 | 93 | it 'should make the correct call' do 94 | expect(client).to receive(:post).once.with("v2/blog/#{blog_name}/post/reblog", { 95 | :id => 123 96 | }).and_return('response') 97 | r = client.reblog blog_name, :id => 123 98 | expect(r).to eq('response') 99 | end 100 | 101 | end 102 | 103 | # Simple post types 104 | [:quote, :text, :link, :chat].each do |type| 105 | 106 | field = type == :quote ? 'quote' : 'title' # uglay 107 | 108 | describe type do 109 | 110 | context 'when passing an option which is not allowed' do 111 | 112 | it 'should raise an error' do 113 | expect(lambda { 114 | client.send type, blog_name, :not => 'an option' 115 | }).to raise_error ArgumentError 116 | end 117 | 118 | end 119 | 120 | context 'when passing valid data' do 121 | 122 | before do 123 | @val = 'hello world' 124 | expect(client).to receive(:post).once.with("v2/blog/#{blog_name}/post", { 125 | field.to_sym => @val, 126 | :type => type.to_s 127 | }).and_return('response') 128 | end 129 | 130 | it 'should set up the call properly' do 131 | r = client.send type, blog_name, field.to_sym => @val 132 | expect(r).to eq('response') 133 | end 134 | 135 | end 136 | 137 | end 138 | 139 | end 140 | 141 | describe :create_post do 142 | 143 | let(:blog_name) { 'seejohnrun' } 144 | let(:args) { { :source => 'somesource' } } 145 | 146 | context 'with a valid post type' do 147 | 148 | before do 149 | expect(client).to receive(:photo).with(blog_name, args).and_return 'hi' 150 | end 151 | 152 | it 'should call the right method and grab the return' do 153 | expect(client.create_post(:photo, blog_name, args)).to eq('hi') 154 | end 155 | 156 | end 157 | 158 | context 'with an invalid post type' do 159 | 160 | it 'should raise an error' do 161 | expect(lambda do 162 | client.create_post(:fake, blog_name, args) 163 | end).to raise_error ArgumentError, '"fake" is not a valid post type' 164 | end 165 | 166 | end 167 | 168 | end 169 | 170 | # Complex post types 171 | [:photo, :audio, :video].each do |type| 172 | 173 | describe type do 174 | 175 | context 'when passing an option which is not allowed' do 176 | 177 | it 'should raise an error' do 178 | expect(lambda { 179 | client.send type, blog_name, :not => 'an option' 180 | }).to raise_error ArgumentError 181 | end 182 | 183 | end 184 | 185 | context 'when passing data as an array of filepaths' do 186 | before do 187 | fakefile = OpenStruct.new :read => file_data 188 | allow(File).to receive(:open).with(file_path + '.jpg').and_return(fakefile) 189 | expect(client).to receive(:post).once.with("v2/blog/#{blog_name}/post", { 190 | 'data[0]' => kind_of(Faraday::UploadIO), 191 | :type => type.to_s 192 | }).and_return('post') 193 | end 194 | 195 | it 'should be able to pass data as an array of filepaths' do 196 | r = client.send type, blog_name, :data => [file_path + ".jpg"] 197 | expect(r).to eq('post') 198 | end 199 | 200 | it 'should be able to pass data as an array of uploadios' do 201 | r = client.send type, blog_name, :data => [Faraday::UploadIO.new(StringIO.new, 'image/jpeg')] 202 | expect(r).to eq('post') 203 | end 204 | 205 | end 206 | 207 | context 'when passing data different ways' do 208 | 209 | before do 210 | fakefile = OpenStruct.new :read => file_data 211 | allow(File).to receive(:open).with(file_path + '.jpg').and_return(fakefile) 212 | expect(client).to receive(:post).once.with("v2/blog/#{blog_name}/post", { 213 | 'data' => kind_of(Faraday::UploadIO), 214 | :type => type.to_s 215 | }).and_return('post') 216 | end 217 | 218 | it 'should be able to pass data as a single filepath' do 219 | r = client.send type, blog_name, :data => file_path + ".jpg" 220 | expect(r).to eq('post') 221 | end 222 | 223 | it 'should be able to pass data as a single uploadio' do 224 | r = client.send type, blog_name, :data => Faraday::UploadIO.new(StringIO.new, 'image/jpeg') 225 | expect(r).to eq('post') 226 | end 227 | 228 | end 229 | 230 | # Only photos have source 231 | if type == :photo 232 | 233 | context 'when passing source different ways' do 234 | 235 | it 'should be able to be passed as a string' do 236 | expect(client).to receive(:post).once.with("v2/blog/#{blog_name}/post", { 237 | :source => source, 238 | :type => type.to_s 239 | }) 240 | client.send type, blog_name, :source => source 241 | end 242 | 243 | it 'should be able to be passed as an array' do 244 | expect(client).to receive(:post).once.with("v2/blog/#{blog_name}/post", { 245 | 'source[0]' => source, 246 | 'source[1]' => source, 247 | :type => type.to_s 248 | }) 249 | client.send type, blog_name, :source => [source, source] 250 | end 251 | 252 | it 'should be able to be passed as an array on edit' do 253 | expect(client).to receive(:post).once.with("v2/blog/#{blog_name}/post/edit", { 254 | :id => post_id, 255 | 'source[0]' => source, 256 | 'source[1]' => source 257 | }) 258 | client.edit blog_name, :id => post_id, :source => [source, source] 259 | end 260 | 261 | end 262 | 263 | end 264 | 265 | context 'when passing colliding options' do 266 | 267 | it 'should get an error when passing data & source' do 268 | expect(lambda { 269 | client.send type, blog_name, :data => 'hi', :source => 'bye' 270 | }).to raise_error ArgumentError 271 | end 272 | 273 | end 274 | 275 | end 276 | 277 | end 278 | 279 | end 280 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2013 Tumblr, Inc. 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | l 203 | -------------------------------------------------------------------------------- /spec/examples/blog_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Tumblr::Blog do 4 | 5 | let(:blog_name) { 'seejohnrun.tumblr.com' } 6 | let(:post_id) { 45693025 } 7 | let(:other_blog_name) { 'staff' } 8 | let(:consumer_key) { 'ckey' } 9 | let(:client) do 10 | Tumblr::Client.new :consumer_key => consumer_key 11 | end 12 | 13 | describe :blog_info do 14 | 15 | it 'should make the proper request' do 16 | expect(client).to receive(:get).once.with("v2/blog/#{blog_name}/info", { 17 | :api_key => consumer_key 18 | }).and_return 'response' 19 | r = client.blog_info blog_name 20 | expect(r).to eq('response') 21 | end 22 | 23 | it 'should make the proper request with a short blog name' do 24 | expect(client).to receive(:get).once.with("v2/blog/b.tumblr.com/info", { 25 | :api_key => consumer_key 26 | }).and_return 'response' 27 | r = client.blog_info 'b' 28 | expect(r).to eq('response') 29 | end 30 | 31 | end 32 | 33 | describe :avatar do 34 | 35 | context 'when supplying a size' do 36 | 37 | before do 38 | expect(client).to receive(:get_redirect_url).once.with("v2/blog/#{blog_name}/avatar/128"). 39 | and_return('url') 40 | end 41 | 42 | it 'should construct the request properly' do 43 | r = client.avatar blog_name, 128 44 | expect(r).to eq('url') 45 | end 46 | 47 | end 48 | 49 | context 'when no size is specified' do 50 | 51 | before do 52 | expect(client).to receive(:get_redirect_url).once.with("v2/blog/#{blog_name}/avatar"). 53 | and_return('url') 54 | end 55 | 56 | it 'should construct the request properly' do 57 | r = client.avatar blog_name 58 | expect(r).to eq('url') 59 | end 60 | 61 | end 62 | 63 | end 64 | 65 | describe :followers do 66 | 67 | context 'with invalid parameters' do 68 | 69 | it 'should raise an error' do 70 | expect(lambda { 71 | client.followers blog_name, :not => 'an option' 72 | }).to raise_error ArgumentError 73 | end 74 | 75 | end 76 | 77 | context 'with valid parameters' do 78 | 79 | before do 80 | expect(client).to receive(:get).once.with("v2/blog/#{blog_name}/followers", { 81 | :limit => 1 82 | }).and_return('response') 83 | end 84 | 85 | it 'should construct the request properly' do 86 | r = client.followers blog_name, :limit => 1 87 | expect(r).to eq'response' 88 | end 89 | 90 | end 91 | 92 | end 93 | 94 | describe :blow_following do 95 | context 'with invalid parameters' do 96 | it 'should raise an error' do 97 | expect(lambda { 98 | client.blog_following blog_name, :not => 'an option' 99 | }).to raise_error ArgumentError 100 | end 101 | end 102 | 103 | context 'with valid parameters' do 104 | before do 105 | expect(client).to receive(:get).once.with("v2/blog/#{blog_name}/following", limit: 1).and_return('response') 106 | end 107 | it 'should construct the request properly' do 108 | r = client.blog_following blog_name, limit: 1 109 | expect(r).to eq 'response' 110 | end 111 | end 112 | end # describe :blog_following 113 | 114 | describe :followed_by do 115 | context 'with invalid parameters' do 116 | it 'should raise an error' do 117 | expect(lambda { 118 | client.followed_by blog_name, other_blog_name, :not => 'an option' 119 | }).to raise_error ArgumentError 120 | end 121 | end 122 | 123 | context 'with valid parameters' do 124 | before do 125 | expect(client).to receive(:get).once.with("v2/blog/#{blog_name}/followed_by", query: other_blog_name).and_return('response') 126 | end 127 | it 'should construct the request properly' do 128 | r = client.followed_by blog_name, other_blog_name 129 | expect(r).to eq 'response' 130 | end 131 | end 132 | end # describe :followed_by 133 | 134 | describe :blog_likes do 135 | 136 | context 'with invalid parameters' do 137 | 138 | it 'should raise an error' do 139 | expect(lambda { 140 | client.blog_likes blog_name, :not => 'an option' 141 | }).to raise_error ArgumentError 142 | end 143 | 144 | end 145 | 146 | context 'with valid parameters' do 147 | 148 | before do 149 | expect(client).to receive(:get).once.with("v2/blog/#{blog_name}/likes", { 150 | :limit => 1, 151 | :api_key => consumer_key 152 | }).and_return('response') 153 | end 154 | 155 | it 'should construct the request properly' do 156 | r = client.blog_likes blog_name, :limit => 1 157 | expect(r).to eq('response') 158 | end 159 | 160 | end 161 | 162 | end 163 | 164 | describe :posts do 165 | 166 | context 'without a type supplied' do 167 | 168 | before do 169 | expect(client).to receive(:get).once.with("v2/blog/#{blog_name}/posts", { 170 | :limit => 1, 171 | :api_key => consumer_key 172 | }).and_return('response') 173 | end 174 | 175 | it 'should construct the request properly' do 176 | r = client.posts blog_name, :limit => 1 177 | expect(r).to eq('response') 178 | end 179 | 180 | end 181 | 182 | context 'when supplying a type' do 183 | 184 | before do 185 | expect(client).to receive(:get).once.with("v2/blog/#{blog_name}/posts/audio", { 186 | :limit => 1, 187 | :api_key => consumer_key, 188 | :type => 'audio' 189 | }).and_return('response') 190 | end 191 | 192 | it 'should construct the request properly' do 193 | r = client.posts blog_name, :limit => 1, :type => 'audio' 194 | expect(r).to eq('response') 195 | end 196 | 197 | end 198 | 199 | end 200 | 201 | describe :get_post do 202 | context 'with invalid parameters' do 203 | it 'should raise an error' do 204 | expect(lambda { 205 | client.get_post blog_name, post_id, not: 'an option' 206 | }).to raise_error ArgumentError 207 | end 208 | end 209 | 210 | context 'with valid parameters' do 211 | before do 212 | expect(client).to receive(:get).once.with("v2/blog/#{blog_name}/posts/#{post_id}", {}).and_return('response') 213 | end 214 | it 'should construct the request properly' do 215 | r = client.get_post blog_name, post_id 216 | expect(r).to eq('response') 217 | end 218 | end 219 | end # describe :get_post 220 | 221 | describe :notes do 222 | context 'with invalid parameters' do 223 | it 'should raise an error' do 224 | expect(lambda { 225 | client.notes blog_name, post_id, not: 'an option' 226 | }).to raise_error ArgumentError 227 | end 228 | end 229 | 230 | context 'with valid parameters' do 231 | before do 232 | expect(client).to receive(:get).once.with("v2/blog/#{blog_name}/notes", id: post_id).and_return('response') 233 | end 234 | it 'should construct the request properly' do 235 | r = client.notes blog_name, post_id 236 | expect(r).to eq('response') 237 | end 238 | end 239 | end # describe :notes 240 | 241 | # These are all just lists of posts with pagination 242 | [:queue, :draft, :submissions].each do |type| 243 | 244 | ext = type == :submissions ? 'submission' : type.to_s # annoying 245 | 246 | describe type do 247 | 248 | context 'when using parameters other than limit & offset' do 249 | 250 | it 'should raise an error' do 251 | expect(lambda { 252 | client.send type, blog_name, :not => 'an option' 253 | }).to raise_error ArgumentError 254 | end 255 | 256 | end 257 | 258 | context 'with valid options' do 259 | 260 | it 'should construct the call properly' do 261 | limit = 5 262 | expect(client).to receive(:get).once.with("v2/blog/#{blog_name}/posts/#{ext}", { 263 | :limit => limit 264 | }).and_return('response') 265 | r = client.send type, blog_name, :limit => limit 266 | expect(r).to eq('response') 267 | end 268 | 269 | end 270 | 271 | end 272 | 273 | end # [:queue, :draft, :submissions].each 274 | 275 | describe :reorder_queue do 276 | context 'with invalid parameters' do 277 | it 'should raise an error' do 278 | expect(lambda { 279 | client.reorder_queue blog_name, not: 'an option' 280 | }).to raise_error ArgumentError 281 | end 282 | end 283 | 284 | context 'with valid parameters' do 285 | before do 286 | expect(client).to receive(:post).once.with("v2/blog/#{blog_name}/posts/queue/reorder", post_id: 1, insert_after: 2).and_return('response') 287 | end 288 | it 'should construct the request properly' do 289 | r = client.reorder_queue blog_name, post_id: 1, insert_after: 2 290 | expect(r).to eq('response') 291 | end 292 | end 293 | end # describe :reorder_queue 294 | 295 | describe :shuffle_queue do 296 | it 'should construct the request properly' do 297 | expect(client).to receive(:post).once.with("v2/blog/#{blog_name}/posts/queue/shuffle").and_return('response') 298 | r = client.shuffle_queue blog_name 299 | expect(r).to eq('response') 300 | end 301 | end # describe :shuffle_queue 302 | 303 | describe :notifications do 304 | context 'with invalid parameters' do 305 | it 'should raise an error' do 306 | expect(lambda { 307 | client.notifications blog_name, not: 'an option' 308 | }).to raise_error ArgumentError 309 | end 310 | end 311 | 312 | context 'with valid parameters' do 313 | it 'should construct the request properly' do 314 | timestamp = Time.now.to_i 315 | expect(client).to receive(:get).once.with("v2/blog/#{blog_name}/notifications", before: timestamp).and_return('response') 316 | r = client.notifications blog_name, before: timestamp 317 | expect(r).to eq('response') 318 | end 319 | end 320 | end # describe :notifications 321 | 322 | describe :blocks do 323 | context 'with invalid parameters' do 324 | it 'should raise an error' do 325 | expect(lambda { 326 | client.blocks blog_name, not: 'an option' 327 | }).to raise_error ArgumentError 328 | end 329 | end 330 | 331 | context 'with valid parameters' do 332 | before do 333 | expect(client).to receive(:get).once.with("v2/blog/#{blog_name}/blocks", limit: 1).and_return('response') 334 | end 335 | it 'should construct the request properly' do 336 | r = client.blocks blog_name, limit: 1 337 | expect(r).to eq('response') 338 | end 339 | end 340 | end # describe :blocks 341 | 342 | describe :block do 343 | context 'with invalid parameters' do 344 | it 'should raise an error' do 345 | expect(lambda { 346 | client.block blog_name, other_blog_name, not: 'an option' 347 | }).to raise_error ArgumentError 348 | end 349 | end 350 | 351 | context 'with valid parameters' do 352 | before do 353 | expect(client).to receive(:post).once.with("v2/blog/#{blog_name}/blocks", blocked_tumblelog: other_blog_name).and_return('response') 354 | end 355 | it 'should construct the request properly' do 356 | r = client.block blog_name, other_blog_name 357 | expect(r).to eq('response') 358 | end 359 | end 360 | end # describe :block 361 | 362 | describe :unblock do 363 | context 'with invalid parameters' do 364 | it 'should raise an error' do 365 | expect(lambda { 366 | client.unblock blog_name, other_blog_name, not: 'an option' 367 | }).to raise_error ArgumentError 368 | end 369 | end 370 | 371 | context 'with valid parameters' do 372 | before do 373 | expect(client).to receive(:delete).once.with("v2/blog/#{blog_name}/blocks", blocked_tumblelog: other_blog_name).and_return('response') 374 | end 375 | it 'should construct the request properly' do 376 | r = client.unblock blog_name, other_blog_name 377 | expect(r).to eq('response') 378 | end 379 | end 380 | end # describe :unblock 381 | 382 | end 383 | --------------------------------------------------------------------------------