Skip to content

Commit 94930de

Browse files
committed
Add working example for Tumblr
1 parent 39c8a76 commit 94930de

1 file changed

Lines changed: 256 additions & 0 deletions

File tree

examples/tumblr_cli.rb

Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
#!/usr/bin/env ruby
2+
# frozen_string_literal: true
3+
4+
# Tumblr CLI
5+
#
6+
# This script is a command-line interface for interacting with the Tumblr API
7+
# using OAuth 1.0a. It performs OAuth bootstrap using an ephemeral local Sinatra
8+
# callback server and immediately transitions into an interactive CLI menu
9+
# without requiring the script to be re-run.
10+
#
11+
# Tumblr App Setup (Required):
12+
# 1. Create a Tumblr application at https://www.tumblr.com/oauth/apps
13+
# 2. Enable OAuth 1.0a for the application
14+
# 3. Set the "Default callback URL" to:
15+
# http://localhost:4567/callback
16+
# (Must match exactly: scheme, host, port, and path)
17+
# 4. OAuth2 redirect URLs are not used by this script and may be left empty
18+
#
19+
# Authentication model:
20+
# - Consumer key and secret are provided as CLI arguments.
21+
# - On startup, an ephemeral local Sinatra server is launched on localhost
22+
# to handle the OAuth 1.0a callback.
23+
# - The server exists only long enough to exchange the OAuth verifier for an
24+
# access token.
25+
# - Access tokens are stored in memory only (not written to disk).
26+
# - Once OAuth completes, the server shuts down and the CLI menu starts.
27+
#
28+
# Required gems:
29+
# gem install oauth sinatra puma launchy
30+
#
31+
# Usage:
32+
# ruby tumblr_cli.rb --consumer-key <KEY> --consumer-secret <SECRET>
33+
#
34+
# Notes:
35+
# - OAuth authorization is required once per execution.
36+
# - This script intentionally uses OAuth 1.0a to demonstrate legacy OAuth
37+
# handling and Tumblr-specific constraints.
38+
39+
require "optparse"
40+
41+
# CLI Arguments
42+
options = {}
43+
44+
OptionParser.new do |opts|
45+
opts.banner = "Usage: ruby tumblr_cli.rb --consumer-key KEY --consumer-secret SECRET"
46+
47+
opts.on("--consumer-key KEY", "Tumblr consumer key") do |v|
48+
options[:consumer_key] = v
49+
end
50+
51+
opts.on("--consumer-secret SECRET", "Tumblr consumer secret") do |v|
52+
options[:consumer_secret] = v
53+
end
54+
end.parse!
55+
56+
ARGV.clear
57+
58+
unless options[:consumer_key] && options[:consumer_secret]
59+
puts "Missing required arguments."
60+
exit 1
61+
end
62+
63+
# Immutable configuration
64+
CONSUMER_KEY = options[:consumer_key].freeze
65+
CONSUMER_SECRET = options[:consumer_secret].freeze
66+
67+
require "sinatra"
68+
require "oauth"
69+
require "launchy"
70+
require "json"
71+
require "uri"
72+
73+
API_BASE = "https://api.tumblr.com/v2"
74+
CALLBACK_URL = "http://localhost:4567/callback"
75+
76+
set :bind, "localhost"
77+
set :port, 4567
78+
set :server, :puma
79+
set :logging, false
80+
81+
# Mutable runtime state (Sinatra-managed)
82+
set :access_token, nil
83+
84+
# OAuth Consumer
85+
def consumer
86+
OAuth::Consumer.new(
87+
CONSUMER_KEY,
88+
CONSUMER_SECRET,
89+
site: "https://www.tumblr.com",
90+
request_token_path: "/oauth/request_token",
91+
authorize_path: "/oauth/authorize",
92+
access_token_path: "/oauth/access_token",
93+
http_method: :post,
94+
)
95+
end
96+
97+
# OAuth Bootstrap (Ephemeral Sinatra)
98+
REQUEST_TOKENS = {}
99+
100+
get "/" do
101+
request_token = consumer.get_request_token(
102+
oauth_callback: CALLBACK_URL,
103+
http_method: :post,
104+
)
105+
106+
REQUEST_TOKENS[request_token.token] = request_token.secret
107+
redirect request_token.authorize_url
108+
end
109+
110+
get "/callback" do
111+
token = params[:oauth_token]
112+
verifier = params[:oauth_verifier]
113+
114+
request_token = OAuth::RequestToken.new(
115+
consumer,
116+
token,
117+
REQUEST_TOKENS[token],
118+
)
119+
120+
settings.access_token = request_token.get_access_token(
121+
oauth_verifier: verifier,
122+
http_method: :post,
123+
)
124+
125+
puts "\nOAuth complete. Access token acquired.\n"
126+
127+
Thread.new {
128+
sleep 1
129+
Sinatra::Application.quit!
130+
}
131+
"Authorization complete. You can close this window."
132+
end
133+
134+
# HTTP Helper
135+
def api_get(path, params = {})
136+
url = "#{API_BASE}#{path}"
137+
url += "?#{URI.encode_www_form(params)}" unless params.empty?
138+
JSON.parse(settings.access_token.get(url).body)
139+
end
140+
141+
# Rendering Helpers
142+
def print_post(post)
143+
puts "-" * 60
144+
puts "Type : #{post["type"]}"
145+
puts "Date : #{post["date"]}"
146+
puts "URL : #{post["post_url"]}"
147+
148+
case post["type"]
149+
when "text"
150+
puts "\n#{post["title"]}" if post["title"]
151+
puts post["body"]
152+
when "photo"
153+
puts "\nCaption:"
154+
puts post["caption"] if post["caption"]
155+
post["photos"]&.each do |p|
156+
puts "Photo: #{p["original_size"]["url"]}"
157+
end
158+
else
159+
puts "\nSummary:"
160+
puts post["summary"]
161+
end
162+
end
163+
164+
# Lists blogs owned by the authenticated user that have published posts.
165+
#
166+
# This method queries the Tumblr API for the current user's account details
167+
# (`/v2/user/info`) and extracts the set of blogs associated with the user.
168+
# It then filters that list to include only blogs that:
169+
#
170+
# - Are owned by the authenticated user (`admin == true`)
171+
# - Have at least one published post (`posts > 0`)
172+
#
173+
# For each qualifying blog, a short summary is printed to STDOUT including:
174+
# - Blog name
175+
# - Blog URL
176+
# - Total number of posts
177+
#
178+
# API endpoint used:
179+
# GET /v2/user/info
180+
#
181+
# This method does not return a value.
182+
def list_my_blogs
183+
blogs = api_get("/user/info").dig("response", "user", "blogs") || []
184+
185+
blogs.select { |b| b["admin"] && b["posts"].to_i > 0 }.each do |b|
186+
puts "-" * 50
187+
puts "Name : #{b["name"]}"
188+
puts "URL : #{b["url"]}"
189+
puts "Posts: #{b["posts"]}"
190+
end
191+
end
192+
193+
# Lists blogs owned by the authenticated user and displays the latest full posts
194+
# for each blog.
195+
#
196+
# This method retrieves the current user's blogs via the Tumblr API
197+
# (`/v2/user/info`), filters the list to blogs owned by the authenticated user
198+
# that contain published posts, and then fetches the most recent posts for
199+
# each qualifying blog.
200+
#
201+
# For each blog:
202+
# - A header containing the blog name and total post count is printed
203+
# - The latest five posts are retrieved via the blog posts endpoint
204+
# - Each post is rendered in full using type-aware formatting suitable for
205+
# command-line output
206+
#
207+
# Post content is displayed directly to STDOUT and may include text bodies,
208+
# captions, media URLs, and summaries depending on post type.
209+
#
210+
# API endpoints used:
211+
# GET /v2/user/info
212+
# GET /v2/blog/{blog-identifier}/posts
213+
#
214+
# This method does not return a value.
215+
def list_my_blogs_with_latest_posts
216+
blogs = api_get("/user/info").dig("response", "user", "blogs") || []
217+
218+
blogs.select { |b| b["admin"] && b["posts"].to_i > 0 }.each do |b|
219+
puts "\n" + "=" * 70
220+
puts "Blog: #{b["name"]} (#{b["posts"]} posts)"
221+
puts "=" * 70
222+
223+
posts = api_get(
224+
"/blog/#{b["name"]}.tumblr.com/posts",
225+
limit: 5,
226+
).dig("response", "posts") || []
227+
228+
posts.each { |p| print_post(p) }
229+
end
230+
end
231+
232+
# Menu
233+
def menu
234+
loop do
235+
puts "\nTumblr CLI"
236+
puts "1) List my blogs (with posts)"
237+
puts "2) Show latest 5 full posts per blog"
238+
puts "3) Exit"
239+
print("> ")
240+
241+
case STDIN.gets&.strip
242+
when "1" then list_my_blogs
243+
when "2" then list_my_blogs_with_latest_posts
244+
when "3" then exit(0)
245+
else puts "Invalid option."
246+
end
247+
end
248+
end
249+
250+
# Entry Point
251+
puts "Starting Tumblr OAuth flow..."
252+
Launchy.open("http://localhost:4567")
253+
254+
Sinatra::Application.run!
255+
256+
menu

0 commit comments

Comments
 (0)