tumblr-rb 1.3.0 → 2.0.0.alpha
Sign up to get free protection for your applications and to get access to all the features.
- data/.travis.yml +7 -0
- data/Gemfile +15 -8
- data/Gemfile.lock +65 -65
- data/LICENSE +4 -2
- data/README.md +31 -84
- data/Rakefile +8 -68
- data/bin/tumblr +3 -133
- data/lib/tumblr.rb +7 -184
- data/lib/tumblr/authentication.rb +71 -0
- data/lib/tumblr/client.rb +148 -0
- data/lib/tumblr/command_line_interface.rb +222 -0
- data/lib/tumblr/credentials.rb +31 -0
- data/lib/tumblr/post.rb +253 -171
- data/lib/tumblr/post/answer.rb +17 -0
- data/lib/tumblr/post/audio.rb +22 -10
- data/lib/tumblr/post/chat.rb +25 -0
- data/lib/tumblr/post/link.rb +22 -9
- data/lib/tumblr/post/photo.rb +29 -10
- data/lib/tumblr/post/quote.rb +17 -10
- data/lib/tumblr/post/text.rb +18 -0
- data/lib/tumblr/post/video.rb +26 -11
- data/lib/tumblr/version.rb +3 -0
- data/lib/tumblr/views/error.erb +6 -0
- data/lib/tumblr/views/form.erb +11 -0
- data/lib/tumblr/views/layout.erb +41 -0
- data/lib/tumblr/views/success.erb +6 -0
- data/man/tumblr.1 +67 -65
- data/man/tumblr.1.html +131 -108
- data/man/tumblr.1.ronn +76 -57
- data/man/tumblr.5 +48 -68
- data/man/tumblr.5.html +106 -114
- data/man/tumblr.5.ronn +38 -51
- data/spec/fixtures/posts.json +10 -0
- data/spec/fixtures/typical_animated_gif.gif +0 -0
- data/spec/spec_helper.rb +12 -0
- data/spec/tumblr/authentication_spec.rb +57 -0
- data/spec/tumblr/client_spec.rb +223 -0
- data/spec/tumblr/credentials_spec.rb +63 -0
- data/spec/tumblr/post_spec.rb +125 -0
- data/tumblr-rb.gemspec +16 -89
- metadata +101 -102
- data/lib/tumblr/authenticator.rb +0 -18
- data/lib/tumblr/post/conversation.rb +0 -15
- data/lib/tumblr/post/regular.rb +0 -14
- data/lib/tumblr/reader.rb +0 -191
- data/lib/tumblr/writer.rb +0 -39
- data/test/fixtures/vcr_cassettes/authenticate/authenticate.yml +0 -39
- data/test/fixtures/vcr_cassettes/read/all_pages.yml +0 -34
- data/test/fixtures/vcr_cassettes/read/authenticated.yml +0 -40
- data/test/fixtures/vcr_cassettes/read/authentication_failure.yml +0 -33
- data/test/fixtures/vcr_cassettes/read/like.yml +0 -31
- data/test/fixtures/vcr_cassettes/read/mwunsch.yml +0 -101
- data/test/fixtures/vcr_cassettes/read/optional.yml +0 -48
- data/test/fixtures/vcr_cassettes/read/pages.yml +0 -36
- data/test/fixtures/vcr_cassettes/read/tumblrgemtest.yml +0 -42
- data/test/fixtures/vcr_cassettes/read/unlike.yml +0 -31
- data/test/fixtures/vcr_cassettes/write/delete.yml +0 -31
- data/test/fixtures/vcr_cassettes/write/edit.yml +0 -31
- data/test/fixtures/vcr_cassettes/write/reblog.yml +0 -31
- data/test/fixtures/vcr_cassettes/write/write.yml +0 -31
- data/test/helper.rb +0 -44
- data/test/test_tumblr.rb +0 -710
@@ -0,0 +1,31 @@
|
|
1
|
+
require "yaml"
|
2
|
+
|
3
|
+
module Tumblr
|
4
|
+
class Credentials
|
5
|
+
FILE_NAME = ".tumblr"
|
6
|
+
|
7
|
+
attr_reader :path
|
8
|
+
|
9
|
+
def initialize(path = nil)
|
10
|
+
@path = !path.nil? ? File.expand_path(path) : File.join(File.expand_path("~"), FILE_NAME)
|
11
|
+
end
|
12
|
+
|
13
|
+
def write(consumer_key, consumer_secret, token, token_secret)
|
14
|
+
File.open(path, "w") do |io|
|
15
|
+
YAML.dump({
|
16
|
+
"consumer_key" => consumer_key,
|
17
|
+
"consumer_secret" => consumer_secret,
|
18
|
+
"token" => token,
|
19
|
+
"token_secret" => token_secret
|
20
|
+
}, io)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def read
|
25
|
+
YAML.load_file path
|
26
|
+
rescue Errno::ENOENT
|
27
|
+
{}
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
data/lib/tumblr/post.rb
CHANGED
@@ -1,190 +1,272 @@
|
|
1
|
-
|
2
|
-
#
|
3
|
-
|
1
|
+
module Tumblr
|
2
|
+
# A Tumblr::Post object can be serialized into a YAML front-matter formatted string,
|
3
|
+
# and provides convenient ways to publish, edit, and delete to the API.
|
4
|
+
# Don't call #new directly, instead use Post::create to instantiate a subclass.
|
4
5
|
class Post
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
6
|
+
|
7
|
+
autoload :Text, 'tumblr/post/text'
|
8
|
+
autoload :Quote, 'tumblr/post/quote'
|
9
|
+
autoload :Link, 'tumblr/post/link'
|
10
|
+
autoload :Answer, 'tumblr/post/answer'
|
11
|
+
autoload :Video, 'tumblr/post/video'
|
12
|
+
autoload :Audio, 'tumblr/post/audio'
|
13
|
+
autoload :Photo, 'tumblr/post/photo'
|
14
|
+
autoload :Chat, 'tumblr/post/chat'
|
15
|
+
|
16
|
+
FIELDS = [
|
17
|
+
:blog_name, :id, :post_url, :type, :timestamp, :date, :format,
|
18
|
+
:reblog_key, :tags, :bookmarklet, :mobile, :source_url, :source_title,
|
19
|
+
:total_posts,
|
20
|
+
:photos, :dialogue, :player # Post-specific response fields
|
21
|
+
]
|
22
|
+
|
23
|
+
# Some post types have several "body keys", which allow the YAML front-matter
|
24
|
+
# serialization to seem a bit more human. This separator separates those keys.
|
25
|
+
POST_BODY_SEPARATOR = "\n\n"
|
26
|
+
|
27
|
+
# Given a Request, perform it and transform the response into a list of Post objects.
|
28
|
+
def self.perform(request)
|
29
|
+
response = request.perform
|
30
|
+
posts = response.parse["response"]["posts"]
|
31
|
+
|
32
|
+
(posts || []).map{|post| self.create(post) }
|
18
33
|
end
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
@post_id = post_id if post_id
|
34
|
+
|
35
|
+
# Insantiate a subclass of Tumblr::Post, corresponding to the post's type.
|
36
|
+
def self.create(post_response)
|
37
|
+
type = post_response["type"].to_s.capitalize.to_sym
|
38
|
+
get_post_type(post_response["type"]).new(post_response)
|
25
39
|
end
|
26
|
-
|
27
|
-
|
28
|
-
|
40
|
+
|
41
|
+
# Get a subclass of Tumblr::Post based on a type token.
|
42
|
+
def self.get_post_type(type)
|
43
|
+
const_get type.to_s.capitalize.to_sym
|
29
44
|
end
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
45
|
+
|
46
|
+
# Transform a yaml front matter formatted String into a subclass of Tumblr::Post
|
47
|
+
def self.load(doc)
|
48
|
+
create parse(doc)
|
49
|
+
end
|
50
|
+
|
51
|
+
# Load a document and transform into a post via file path
|
52
|
+
def self.load_from_path(path)
|
53
|
+
raise ArgumentError, "Given path: #{path} is not a file" unless File.file? File.expand_path(path)
|
54
|
+
post_type = infer_post_type_from_extname File.extname(path)
|
55
|
+
if post_type == :text
|
56
|
+
load File.read(File.expand_path(path))
|
57
|
+
else
|
58
|
+
load_from_binary File.new(File.expand_path(path), "rb"), post_type
|
44
59
|
end
|
45
|
-
@state = published_state.to_sym
|
46
60
|
end
|
47
|
-
|
48
|
-
def
|
49
|
-
|
50
|
-
|
51
|
-
|
61
|
+
|
62
|
+
def self.load_from_binary(file, post_type = nil)
|
63
|
+
file_size_in_mb = File.size(file.path).to_f / 2**20
|
64
|
+
raise ArgumentError, "File size is greater than 5 MB (Tumblr's limit)" if file_size_in_mb > 5
|
65
|
+
post_type ||= infer_post_type_from_extname File.extname(file.path)
|
66
|
+
get_post_type(post_type).new "data" => file.read
|
67
|
+
end
|
68
|
+
|
69
|
+
# Transform a yaml front matter formatted String into a set of parameters to create a post.
|
70
|
+
def self.parse(doc)
|
71
|
+
doc =~ /^(\s*---(.*?)---\s*)/m
|
72
|
+
|
73
|
+
if Regexp.last_match
|
74
|
+
meta_data = YAML.load(Regexp.last_match[2].strip)
|
75
|
+
doc_body = doc.sub(Regexp.last_match[1],'').strip
|
76
|
+
else
|
77
|
+
meta_data = {}
|
78
|
+
doc_body = doc
|
52
79
|
end
|
80
|
+
meta_data["type"] ||= infer_post_type_from_string(doc_body)
|
81
|
+
meta_data["format"] ||= "markdown"
|
82
|
+
|
83
|
+
post_type = get_post_type(meta_data["type"])
|
84
|
+
post_body_parts = doc_body.split(POST_BODY_SEPARATOR)
|
85
|
+
|
86
|
+
pairs = pair_post_body_types(post_type.post_body_keys, post_body_parts.dup)
|
87
|
+
Hash[pairs].merge(meta_data)
|
53
88
|
end
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
end
|
89
|
+
|
90
|
+
# Pair the post body keys for a particular post type with a list of values.
|
91
|
+
# If the length list of values is greater than the list of keys, the last key
|
92
|
+
# should be paired with the remaining values joined together.
|
93
|
+
def self.pair_post_body_types(keys, values)
|
94
|
+
values.fill(keys.length - 1) do |i|
|
95
|
+
values[keys.length - 1, values.length].join(POST_BODY_SEPARATOR)
|
62
96
|
end
|
63
|
-
|
64
|
-
end
|
65
|
-
|
66
|
-
def
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
params.each { |key| post_hash[key.to_s.gsub('_','-').to_sym] = send(key) } unless params.empty?
|
79
|
-
post_hash[:private] = 1 if private?
|
80
|
-
post_hash
|
81
|
-
end
|
82
|
-
|
83
|
-
# Publish this post to Tumblr
|
84
|
-
def write(email, password)
|
85
|
-
Writer.new(email,password).write(to_h)
|
86
|
-
end
|
87
|
-
|
88
|
-
def edit(email, password)
|
89
|
-
Writer.new(email,password).edit(to_h)
|
90
|
-
end
|
91
|
-
|
92
|
-
def reblog(email, password)
|
93
|
-
Writer.new(email,password).reblog(to_h)
|
94
|
-
end
|
95
|
-
|
96
|
-
def delete(email, password)
|
97
|
-
Writer.new(email,password).delete(to_h)
|
98
|
-
end
|
99
|
-
|
100
|
-
def like(email,password)
|
101
|
-
if (post_id && reblog_key)
|
102
|
-
Reader.new(email,password).like(:'post-id' => post_id, :'reblog-key' => reblog_key)
|
97
|
+
keys.map(&:to_s).zip values
|
98
|
+
end
|
99
|
+
|
100
|
+
def self.infer_post_type_from_extname(extname)
|
101
|
+
require 'rack'
|
102
|
+
mime_type = Rack::Mime.mime_type extname
|
103
|
+
case mime_type.split("/").first
|
104
|
+
when "image"
|
105
|
+
:photo
|
106
|
+
when "video"
|
107
|
+
:video
|
108
|
+
when "audio"
|
109
|
+
:audio
|
110
|
+
else
|
111
|
+
:text
|
103
112
|
end
|
104
113
|
end
|
105
|
-
|
106
|
-
def
|
107
|
-
|
108
|
-
|
114
|
+
|
115
|
+
def self.infer_post_type_from_string(str)
|
116
|
+
require 'uri'
|
117
|
+
video_hosts = ["youtube.com", "vimeo.com", "youtu.be"]
|
118
|
+
audio_hosts = ["open.spotify.com", "soundcloud.com", "snd.sc"]
|
119
|
+
url = URI.parse(str)
|
120
|
+
if url.is_a?(URI::HTTP)
|
121
|
+
return :video if video_hosts.find {|h| url.host.include?(h) }
|
122
|
+
return :audio if audio_hosts.find {|h| url.host.include?(h) }
|
123
|
+
:link
|
124
|
+
elsif url.scheme.eql?("spotify")
|
125
|
+
:audio
|
126
|
+
else
|
127
|
+
:text
|
109
128
|
end
|
129
|
+
rescue URI::InvalidURIError
|
130
|
+
:text
|
110
131
|
end
|
111
|
-
|
112
|
-
#
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
def add_to_queue(email, password, pubdate = nil)
|
128
|
-
self.state = :queue
|
129
|
-
self.publish_on(pubdate) if pubdate
|
130
|
-
return edit(email,password) if post_id
|
131
|
-
write(email,password)
|
132
|
-
end
|
133
|
-
|
134
|
-
# Convert post to a YAML representation
|
135
|
-
def to_yaml
|
136
|
-
post = {}
|
137
|
-
post['data'] = post_data
|
138
|
-
post['body'] = to_h[post_body].to_s
|
139
|
-
YAML.dump(post)
|
140
|
-
end
|
141
|
-
|
142
|
-
# Convert post to a string for writing to a file
|
143
|
-
def to_s
|
144
|
-
post_string = YAML.dump(post_data)
|
145
|
-
post_string += "---\x0D\x0A"
|
146
|
-
post_string += YAML.load(to_yaml)['body']
|
147
|
-
post_string
|
148
|
-
end
|
149
|
-
|
150
|
-
private
|
151
|
-
|
152
|
-
def post_data
|
153
|
-
data = {}
|
154
|
-
to_h.each_pair do |key,value|
|
155
|
-
data[key.to_s] = value.to_s
|
132
|
+
|
133
|
+
# A post_body_key determines what parts of the serialization map to certain
|
134
|
+
# fields in the post request.
|
135
|
+
def self.post_body_keys
|
136
|
+
[:body]
|
137
|
+
end
|
138
|
+
|
139
|
+
# Serialize a post.
|
140
|
+
def self.dump(post)
|
141
|
+
post.serialize
|
142
|
+
end
|
143
|
+
|
144
|
+
def initialize(post_response = {})
|
145
|
+
post_response.delete_if {|k,v| !(FIELDS | Tumblr::Client::POST_OPTIONS).map(&:to_s).include? k.to_s }
|
146
|
+
post_response.each_pair do |k,v|
|
147
|
+
instance_variable_set "@#{k}".to_sym, v
|
156
148
|
end
|
157
|
-
data.reject! {|key,value| key.eql?(post_body.to_s) }
|
158
|
-
data
|
159
149
|
end
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
150
|
+
|
151
|
+
# Transform this post into it's YAML front-matter post form.
|
152
|
+
def serialize
|
153
|
+
buffer = YAML.dump(meta_data)
|
154
|
+
buffer << "---\x0D\x0A"
|
155
|
+
buffer << post_body
|
156
|
+
buffer
|
157
|
+
end
|
158
|
+
|
159
|
+
# Given a client, publish this post to tumblr.
|
160
|
+
def post(client)
|
161
|
+
client.post(request_parameters)
|
162
|
+
end
|
163
|
+
|
164
|
+
# Given a client, edit this post.
|
165
|
+
def edit(client)
|
166
|
+
raise "Must have an id to edit a post" unless id
|
167
|
+
client.edit(request_parameters)
|
168
|
+
end
|
169
|
+
|
170
|
+
# Given a client, delete this post.
|
171
|
+
def delete(client)
|
172
|
+
raise "Must have an id to delete a post" unless id
|
173
|
+
client.delete(:id => id)
|
174
|
+
end
|
175
|
+
|
176
|
+
# Transform this Post into a hash ready to be serialized and posted to the API.
|
177
|
+
# This looks for the fields of Tumblr::Client::POST_OPTIONS as methods on the object.
|
178
|
+
def request_parameters
|
179
|
+
Hash[(Tumblr::Client::POST_OPTIONS | [:id, :type]).map {|key|
|
180
|
+
[key.to_s, send(key)] if respond_to?(key) && send(key)
|
181
|
+
}]
|
182
|
+
end
|
183
|
+
|
184
|
+
# Which parts of this post represent it's meta data (eg. they're not part of the body).
|
185
|
+
def meta_data
|
186
|
+
request_parameters.reject {|k,v| self.class.post_body_keys.include?(k.to_sym) }
|
187
|
+
end
|
188
|
+
|
189
|
+
# Below this line are public methods that are used to transform this post into an API request.
|
190
|
+
|
191
|
+
def id
|
192
|
+
@id.to_i unless @id.nil?
|
193
|
+
end
|
194
|
+
|
195
|
+
def type
|
196
|
+
@type.to_s
|
197
|
+
end
|
198
|
+
|
199
|
+
def reblog_key
|
200
|
+
@reblog_key
|
201
|
+
end
|
202
|
+
|
203
|
+
def state
|
204
|
+
@state
|
205
|
+
end
|
206
|
+
|
207
|
+
def tags
|
208
|
+
if @tags.respond_to? :join
|
209
|
+
@tags.join(",")
|
210
|
+
else
|
211
|
+
@tags
|
179
212
|
end
|
180
213
|
end
|
214
|
+
|
215
|
+
def tweet
|
216
|
+
@tweet
|
217
|
+
end
|
218
|
+
|
219
|
+
def date
|
220
|
+
@date
|
221
|
+
end
|
222
|
+
|
223
|
+
def format
|
224
|
+
@format
|
225
|
+
end
|
226
|
+
|
227
|
+
def slug
|
228
|
+
@slug
|
229
|
+
end
|
230
|
+
|
231
|
+
# These are handy convenience methods.
|
232
|
+
|
233
|
+
def markdown?
|
234
|
+
@format.to_s == "markdown"
|
235
|
+
end
|
236
|
+
|
237
|
+
def published?
|
238
|
+
@state.to_s == "published"
|
239
|
+
end
|
240
|
+
|
241
|
+
def draft?
|
242
|
+
@state.to_s == "draft"
|
243
|
+
end
|
244
|
+
|
245
|
+
def queued?
|
246
|
+
@state.to_s == "queued" or @state.to_s == "queue"
|
247
|
+
end
|
248
|
+
|
249
|
+
def private?
|
250
|
+
@state.to_s == "private"
|
251
|
+
end
|
252
|
+
|
253
|
+
def publish!
|
254
|
+
@state = "published"
|
255
|
+
end
|
256
|
+
|
257
|
+
def queue!
|
258
|
+
@state = "queue"
|
259
|
+
end
|
260
|
+
|
261
|
+
def draft!
|
262
|
+
@state ="draft"
|
263
|
+
end
|
264
|
+
|
265
|
+
private
|
266
|
+
|
267
|
+
def post_body
|
268
|
+
self.class.post_body_keys.map{|key| self.send(key) }.join(POST_BODY_SEPARATOR)
|
269
|
+
end
|
270
|
+
|
181
271
|
end
|
182
272
|
end
|
183
|
-
|
184
|
-
require 'tumblr/post/regular'
|
185
|
-
require 'tumblr/post/photo'
|
186
|
-
require 'tumblr/post/quote'
|
187
|
-
require 'tumblr/post/link'
|
188
|
-
require 'tumblr/post/conversation'
|
189
|
-
require 'tumblr/post/video'
|
190
|
-
require 'tumblr/post/audio'
|