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.
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'