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.
Files changed (62) hide show
  1. data/.travis.yml +7 -0
  2. data/Gemfile +15 -8
  3. data/Gemfile.lock +65 -65
  4. data/LICENSE +4 -2
  5. data/README.md +31 -84
  6. data/Rakefile +8 -68
  7. data/bin/tumblr +3 -133
  8. data/lib/tumblr.rb +7 -184
  9. data/lib/tumblr/authentication.rb +71 -0
  10. data/lib/tumblr/client.rb +148 -0
  11. data/lib/tumblr/command_line_interface.rb +222 -0
  12. data/lib/tumblr/credentials.rb +31 -0
  13. data/lib/tumblr/post.rb +253 -171
  14. data/lib/tumblr/post/answer.rb +17 -0
  15. data/lib/tumblr/post/audio.rb +22 -10
  16. data/lib/tumblr/post/chat.rb +25 -0
  17. data/lib/tumblr/post/link.rb +22 -9
  18. data/lib/tumblr/post/photo.rb +29 -10
  19. data/lib/tumblr/post/quote.rb +17 -10
  20. data/lib/tumblr/post/text.rb +18 -0
  21. data/lib/tumblr/post/video.rb +26 -11
  22. data/lib/tumblr/version.rb +3 -0
  23. data/lib/tumblr/views/error.erb +6 -0
  24. data/lib/tumblr/views/form.erb +11 -0
  25. data/lib/tumblr/views/layout.erb +41 -0
  26. data/lib/tumblr/views/success.erb +6 -0
  27. data/man/tumblr.1 +67 -65
  28. data/man/tumblr.1.html +131 -108
  29. data/man/tumblr.1.ronn +76 -57
  30. data/man/tumblr.5 +48 -68
  31. data/man/tumblr.5.html +106 -114
  32. data/man/tumblr.5.ronn +38 -51
  33. data/spec/fixtures/posts.json +10 -0
  34. data/spec/fixtures/typical_animated_gif.gif +0 -0
  35. data/spec/spec_helper.rb +12 -0
  36. data/spec/tumblr/authentication_spec.rb +57 -0
  37. data/spec/tumblr/client_spec.rb +223 -0
  38. data/spec/tumblr/credentials_spec.rb +63 -0
  39. data/spec/tumblr/post_spec.rb +125 -0
  40. data/tumblr-rb.gemspec +16 -89
  41. metadata +101 -102
  42. data/lib/tumblr/authenticator.rb +0 -18
  43. data/lib/tumblr/post/conversation.rb +0 -15
  44. data/lib/tumblr/post/regular.rb +0 -14
  45. data/lib/tumblr/reader.rb +0 -191
  46. data/lib/tumblr/writer.rb +0 -39
  47. data/test/fixtures/vcr_cassettes/authenticate/authenticate.yml +0 -39
  48. data/test/fixtures/vcr_cassettes/read/all_pages.yml +0 -34
  49. data/test/fixtures/vcr_cassettes/read/authenticated.yml +0 -40
  50. data/test/fixtures/vcr_cassettes/read/authentication_failure.yml +0 -33
  51. data/test/fixtures/vcr_cassettes/read/like.yml +0 -31
  52. data/test/fixtures/vcr_cassettes/read/mwunsch.yml +0 -101
  53. data/test/fixtures/vcr_cassettes/read/optional.yml +0 -48
  54. data/test/fixtures/vcr_cassettes/read/pages.yml +0 -36
  55. data/test/fixtures/vcr_cassettes/read/tumblrgemtest.yml +0 -42
  56. data/test/fixtures/vcr_cassettes/read/unlike.yml +0 -31
  57. data/test/fixtures/vcr_cassettes/write/delete.yml +0 -31
  58. data/test/fixtures/vcr_cassettes/write/edit.yml +0 -31
  59. data/test/fixtures/vcr_cassettes/write/reblog.yml +0 -31
  60. data/test/fixtures/vcr_cassettes/write/write.yml +0 -31
  61. data/test/helper.rb +0 -44
  62. 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
@@ -1,190 +1,272 @@
1
- # An object that represents a post. From:
2
- # http://www.tumblr.com/docs/en/api#api_write
3
- class Tumblr
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
- BASIC_PARAMS = [:date,:tags,:format,:group,:generator,:private,
6
- :slug,:state,:'send-to-twitter',:'publish-on',:'reblog-key']
7
- POST_PARAMS = [:title,:body,:source,:caption,:'click-through-url',
8
- :quote,:name,:url,:description,:conversation,
9
- :embed,:'externally-hosted-url']
10
- REBLOG_PARAMS = [:comment, :as]
11
-
12
- def self.parameters(*attributes)
13
- if !attributes.blank?
14
- @parameters = attributes
15
- attr_accessor *@parameters
16
- end
17
- @parameters
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
- attr_reader :type, :state, :post_id, :format
21
- attr_accessor :slug, :date, :group, :generator, :reblog_key
22
-
23
- def initialize(post_id = nil)
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
- def private=(bool)
28
- @private = bool ? true : false
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
- def private?
32
- @private
33
- end
34
-
35
- def tags(*post_tags)
36
- @tags = post_tags.join(',') if !post_tags.blank?
37
- @tags
38
- end
39
-
40
- def state=(published_state)
41
- allowed_states = [:published, :draft, :submission, :queue]
42
- if !allowed_states.include?(published_state.to_sym)
43
- raise "Not a recognized published state. Must be one of #{allowed_states.inspect}"
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 format=(markup)
49
- markup_format = markup.to_sym
50
- if markup_format.eql?(:html) || markup_format.eql?(:markdown)
51
- @format = markup_format
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
- def send_to_twitter(status=false)
56
- if status
57
- if status.to_sym.eql?(:no)
58
- @send_to_twitter = false
59
- else
60
- @send_to_twitter = status
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
- @send_to_twitter
64
- end
65
-
66
- def publish_on(pubdate=nil)
67
- @publish_on = pubdate if state.eql?(:queue) && pubdate
68
- @publish_on
69
- end
70
-
71
- # Convert to a hash to be used in post writing/editing
72
- def to_h
73
- post_hash = {}
74
- basics = [:post_id, :type, :date, :tags, :format, :group, :generator,
75
- :slug, :state, :send_to_twitter, :publish_on, :reblog_key]
76
- params = basics.select {|opt| respond_to?(opt) && send(opt) }
77
- params |= self.class.parameters.select {|opt| send(opt) } unless self.class.parameters.blank?
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 unlike(email,password)
107
- if (post_id && reblog_key)
108
- Reader.new(email,password).unlike(:'post-id' => post_id, :'reblog-key' => reblog_key)
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
- # Write to Tumblr and set state to Publish
113
- def publish_now(email, password)
114
- self.state = :published
115
- return edit(email,password) if post_id
116
- write(email,password)
117
- end
118
-
119
- # Save as a draft
120
- def save_as_draft(email, password)
121
- self.state = :draft
122
- return edit(email,password) if post_id
123
- write(email,password)
124
- end
125
-
126
- # Adds to Queue. Pass an additional date to publish at a specific date.
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
- def post_body
162
- case type
163
- when :regular
164
- :body
165
- when :photo
166
- :source
167
- when :quote
168
- :quote
169
- when :link
170
- :url
171
- when :conversation
172
- :conversation
173
- when :video
174
- :embed
175
- when :audio
176
- :'externally-hosted-url'
177
- else
178
- raise "#{type} is not a recognized Tumblr post type."
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'