tumblr-rb 1.3.0 → 2.0.0.alpha
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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'
|