tumblr4r 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
data/ChangeLog ADDED
@@ -0,0 +1,4 @@
1
+ == 0.0.1 / 2009-09-07
2
+
3
+ * initial release
4
+
data/README ADDED
@@ -0,0 +1,48 @@
1
+ = tumblr4r
2
+
3
+ * http://github.com/tmaeda/tumblr4r
4
+
5
+ == Description
6
+
7
+ Tumblr API wrapper for Ruby.
8
+
9
+ == Synopsis
10
+
11
+ Finding by conditions.
12
+ require 'rubygems'
13
+ require 'tumblr4r'
14
+
15
+ site_a = Tumblr4r::Site.new("site_a.tumblr.com")
16
+ posts = site_a.find(:all)
17
+ quote_posts = site_a.find(:all, :type => "quote")
18
+ posts_offset_and_limit = site_a.find(:all, :offset => 50, :limit => 20)
19
+ quote_search = site_a.find(:all, :type => "quote", :search => "foo")
20
+ quote_tagged = site_a.find(:all, :type => "quote", :tagged => "bar")
21
+
22
+ Finding by id.
23
+ post = site_a.find(12345678)
24
+
25
+ Posting.
26
+ site_b = Tumblr4r::Site.new("site_b.tumblr.com", "foo@example.com", "password")
27
+ site_b.save(post[0])
28
+
29
+ Deleting.
30
+ site_b.delete(post[0].post_id)
31
+
32
+ == Installation
33
+
34
+ gem install tumblr4r
35
+
36
+ == Problems
37
+
38
+ * Can't get private posts yet.
39
+ * Can't upload audio and video data yet.
40
+ * Can't handle feeds yet.
41
+
42
+
43
+ == Copyright
44
+
45
+ Author:: Tomoki MAEDA <http://twitter.com/tmaeda>
46
+ Copyright:: Copyright (c) 2009 Tomoki MAEDA
47
+ License:: Ruby's license
48
+
data/Rakefile ADDED
@@ -0,0 +1,145 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'rake/clean'
4
+ require 'rake/testtask'
5
+ require 'rake/packagetask'
6
+ require 'rake/gempackagetask'
7
+ require 'rake/rdoctask'
8
+ require 'rake/contrib/rubyforgepublisher'
9
+ require 'rake/contrib/sshpublisher'
10
+ require 'fileutils'
11
+ require 'lib/tumblr4r'
12
+ include FileUtils
13
+
14
+ NAME = "tumblr4r"
15
+ AUTHOR = "Tomoki MAEDA"
16
+ EMAIL = "tmaeda@ruby-sapporo.org"
17
+ DESCRIPTION = "Tumblr API Wrapper for Ruby"
18
+ RUBYFORGE_PROJECT = "tumblr4r"
19
+ HOMEPATH = "http://#{RUBYFORGE_PROJECT}.rubyforge.org"
20
+ BIN_FILES = %w( )
21
+
22
+ VERS = Tumblr4r::VERSION
23
+ REV = File.read(".svn/entries")[/committed-rev="(d+)"/, 1] rescue nil
24
+ CLEAN.include ['**/.*.sw?', '*.gem', '.config']
25
+ RDOC_OPTS = [
26
+ '--title', "#{NAME} documentation",
27
+ "--charset", "utf-8",
28
+ "--opname", "index.html",
29
+ "--line-numbers",
30
+ "--main", "README",
31
+ "--inline-source",
32
+ ]
33
+
34
+ task :default => [:test]
35
+ task :package => [:clean]
36
+
37
+ Rake::TestTask.new("test") do |t|
38
+ t.libs << "test"
39
+ t.pattern = "test/**/*_test.rb"
40
+ t.verbose = true
41
+ end
42
+
43
+ spec = Gem::Specification.new do |s|
44
+ s.name = NAME
45
+ s.version = VERS
46
+ s.platform = Gem::Platform::RUBY
47
+ s.has_rdoc = true
48
+ s.extra_rdoc_files = ["README", "ChangeLog"]
49
+ s.rdoc_options += RDOC_OPTS + ['--exclude', '^(examples|extras)/']
50
+ s.summary = DESCRIPTION
51
+ s.description = DESCRIPTION
52
+ s.author = AUTHOR
53
+ s.email = EMAIL
54
+ s.homepage = HOMEPATH
55
+ s.executables = BIN_FILES
56
+ s.rubyforge_project = RUBYFORGE_PROJECT
57
+ s.bindir = "bin"
58
+ s.require_path = "lib"
59
+ #s.autorequire = ""
60
+ s.test_files = Dir["test/*_test.rb"]
61
+
62
+ s.add_runtime_dependency('activesupport', '>=2.3.2')
63
+ s.add_development_dependency('pit', '>=0.0.6')
64
+ s.required_ruby_version = '>= 1.8.6'
65
+
66
+ s.files = %w(README ChangeLog Rakefile) +
67
+ Dir.glob("{bin,doc,test,lib,templates,generator,extras,website,script}/**/*") +
68
+ Dir.glob("ext/**/*.{h,c,rb}") +
69
+ Dir.glob("examples/**/*.rb") +
70
+ Dir.glob("tools/*.rb") +
71
+ Dir.glob("rails/*.rb")
72
+
73
+ s.extensions = FileList["ext/**/extconf.rb"].to_a
74
+ end
75
+
76
+ Rake::GemPackageTask.new(spec) do |p|
77
+ p.need_tar = true
78
+ p.gem_spec = spec
79
+ end
80
+
81
+ task :install do
82
+ name = "#{NAME}-#{VERS}.gem"
83
+ sh %{rake package}
84
+ sh %{sudo gem install pkg/#{name}}
85
+ end
86
+
87
+ task :uninstall => [:clean] do
88
+ sh %{sudo gem uninstall #{NAME}}
89
+ end
90
+
91
+
92
+ Rake::RDocTask.new do |rdoc|
93
+ rdoc.rdoc_dir = 'html'
94
+ rdoc.options += RDOC_OPTS
95
+ rdoc.template = "resh"
96
+ #rdoc.template = "#{ENV['template']}.rb" if ENV['template']
97
+ if ENV['DOC_FILES']
98
+ rdoc.rdoc_files.include(ENV['DOC_FILES'].split(/,\s*/))
99
+ else
100
+ rdoc.rdoc_files.include('README', 'ChangeLog')
101
+ rdoc.rdoc_files.include('lib/**/*.rb')
102
+ rdoc.rdoc_files.include('ext/**/*.c')
103
+ end
104
+ end
105
+
106
+ desc "Publish to RubyForge"
107
+ task :rubyforge => [:rdoc, :package] do
108
+ require 'rubyforge'
109
+ Rake::RubyForgePublisher.new(RUBYFORGE_PROJECT, 'tmaeda').upload
110
+ end
111
+
112
+ desc 'Package and upload the release to rubyforge.'
113
+ task :release => [:clean, :package] do |t|
114
+ v = ENV["VERSION"] or abort "Must supply VERSION=x.y.z"
115
+ abort "Versions don't match #{v} vs #{VERS}" unless v == VERS
116
+ pkg = "pkg/#{NAME}-#{VERS}"
117
+
118
+ require 'rubyforge'
119
+ rf = RubyForge.new.configure
120
+ puts "Logging in"
121
+ rf.login
122
+
123
+ c = rf.userconfig
124
+ # c["release_notes"] = description if description
125
+ # c["release_changes"] = changes if changes
126
+ c["preformatted"] = true
127
+
128
+ files = [
129
+ "#{pkg}.tgz",
130
+ "#{pkg}.gem"
131
+ ].compact
132
+
133
+ puts "Releasing #{NAME} v. #{VERS}"
134
+ rf.add_release RUBYFORGE_PROJECT, NAME, VERS, *files
135
+ end
136
+
137
+ desc 'Show information about the gem.'
138
+ task :debug_gem do
139
+ puts spec.to_ruby
140
+ end
141
+
142
+ desc 'Update gem spec'
143
+ task :gemspec do
144
+ open("#{NAME}.gemspec", 'w').write spec.to_ruby
145
+ end
data/lib/tumblr4r.rb ADDED
@@ -0,0 +1,510 @@
1
+ require 'net/http'
2
+ require 'rubygems'
3
+ require 'rexml/document'
4
+ require 'active_support'
5
+ require 'logger'
6
+ require 'cgi'
7
+ module Tumblr4r
8
+ VERSION = '0.7.0'
9
+ class TumblrError < StandardError
10
+ end
11
+
12
+ module POST_TYPE
13
+ REGULAR = "regular"
14
+ PHOTO = "photo"
15
+ QUOTE = "quote"
16
+ LINK = "link"
17
+ CHAT = "conversation"
18
+ AUDIO = "audio"
19
+ VIDEO = "video"
20
+ end
21
+
22
+ # ConnectionオブジェクトとParserオブジェクトを組み合わせて、
23
+ # TumblrAPIとRubyオブジェクトの相互変換を行う
24
+ # TODO: private な post だけを取得する API が無いのだなぁ
25
+ # * Webから更新したものがAPIで取得できるデータに反映されるには少しタイムラグがあるようだ
26
+ # * Webから更新しちゃうと、POST日時の秒が丸められてしまう
27
+ class Site
28
+ attr_accessor :hostname, :email, :password, :name, :timezone, :title, :cname,
29
+ :description, :feeds
30
+ attr_accessor :logger
31
+ # TODO: 変数名もうちょっと考える
32
+ API_READ_LIMIT = 50
33
+ @@default_log_level = Logger::INFO
34
+ cattr_accessor :default_log_level
35
+
36
+ class << self
37
+ # TODO: unit test
38
+ def find(hostname, email=nil, password=nil, http=nil, &block)
39
+ site = self.new(hostname, email, password, http)
40
+ result = site.find(:all)
41
+ if block_given?
42
+ result.each do |post|
43
+ yield post
44
+ end
45
+ else
46
+ return result
47
+ end
48
+ end
49
+ end
50
+
51
+ def initialize(hostname, email=nil, password=nil, http = nil, logger = nil)
52
+ @hostname = hostname
53
+ @email = email
54
+ @password = password
55
+ @logger = logger || Logger.new(STDERR)
56
+ @logger.level = @@default_log_level
57
+ @conn = XMLConnection.new(http || @hostname, email, password, @logger)
58
+ @parser = XMLParser.new
59
+ self.site_info
60
+ end
61
+
62
+ # TODO: ここの再帰取得ロジックはTumblrAPIとは独立してるので
63
+ # TumblrAPIとは独立した形に切り出したり、TumblrAPIとは切り離してテストを書きたいものだ
64
+ # @param [Symbol|Integer] id_or_type :all, id
65
+ # @return [Array<Post>|Post]
66
+ def find(id_or_type, options = { })
67
+ params = { }
68
+ return result if options[:offset] && options[:offset].to_i < 0
69
+ [:type, :filter, :tagged, :search].each do |option|
70
+ params[option] = options[option] if options[option]
71
+ end
72
+
73
+ if id_or_type == :all
74
+ result = []
75
+ # 取得開始位置の初期化
76
+ params[:start] = options[:offset] || 0
77
+ # goal の設定
78
+ total = self.count(options)
79
+ if options[:limit]
80
+ goal = [total - params[:start],
81
+ options[:limit] - params[:start]].min
82
+ else
83
+ goal = total - params[:start]
84
+ end
85
+ # 取得件数の初期化
86
+ if goal < 0
87
+ return result
88
+ elsif goal < API_READ_LIMIT
89
+ params[:num] = goal
90
+ else
91
+ params[:num] = API_READ_LIMIT # :num を指定しないとデフォルトでは20件しかとれない
92
+ end
93
+
94
+ loop do
95
+ xml = @conn.get(params)
96
+ posts, start, total = @parser.posts(xml)
97
+ @logger.info("size: #{posts.size}")
98
+ @logger.info("start: #{start}")
99
+ @logger.info("total: #{total}")
100
+ result += posts
101
+ if result.size >= goal || posts.size == 0
102
+ # Tumblr API の total で得られる値は全く信用ならない。
103
+ # 検索条件を考慮した件数を返してくれない。
104
+ # (つまり、goalは信用ならない)ので、posts.sizeも終了判定に利用する。
105
+ # TODO: もしくは:numの値を足し合わせていって、それとgoalを比較する?
106
+ break
107
+ end
108
+ # 取得開始位置の調整
109
+ params[:start] += params[:num]
110
+ # 取得件数の調整
111
+ if (goal - result.size) >= API_READ_LIMIT
112
+ params[:num] = API_READ_LIMIT
113
+ else
114
+ params[:num] = goal - result.size
115
+ end
116
+ end
117
+ return result
118
+ elsif id_or_type.kind_of?(Integer)
119
+ xml = @conn.get({:id => id_or_type})
120
+ posts, start, total = @parser.posts(xml)
121
+ @logger.info("size: #{posts.size}")
122
+ @logger.info("start: #{start}")
123
+ @logger.info("total: #{total}")
124
+ return posts[0]
125
+ else
126
+ raise ArgumentError
127
+ end
128
+ end
129
+
130
+ def count(options = { })
131
+ params = { }
132
+ [:id, :type, :filter, :tagged, :search].each do |option|
133
+ params[option] = options[option] if options[option]
134
+ end
135
+ params[:num] = 1
136
+ params[:start] = 0
137
+ xml = @conn.get(params)
138
+ posts, start, total = @parser.posts(xml)
139
+ return total
140
+ end
141
+
142
+ def site_info
143
+ xml = @conn.get(:num => 1)
144
+ @parser.siteinfo(self, xml)
145
+ end
146
+
147
+ def save(post)
148
+ post_id = @conn.write(post.params)
149
+ new_post = self.find(post_id)
150
+ return new_post
151
+ end
152
+
153
+ end
154
+
155
+ # Postおよびその子クラスは原則として単なるData Transfer Objectとし、
156
+ # 何かのロジックをこの中に実装はしない。
157
+ class Post
158
+ attr_accessor :post_id, # Integer
159
+ :url, # String
160
+ :url_with_slug, # String
161
+ :type, # String
162
+ :date_gmt,
163
+ :date,
164
+ :unix_timestamp, # Integer
165
+ :format, # String("html"|"markdown")
166
+ :tags, # Array<String>
167
+ :bookmarklet, # true|false
168
+ :private, # Integer(0|1)
169
+ :generator # String
170
+
171
+ @@default_generator = nil
172
+ cattr_accessor :default_generator
173
+
174
+ def initialize
175
+ @generator = @@default_generator || "Tumblr4R"
176
+ @tags = []
177
+ end
178
+
179
+ def params
180
+ {"type" => @type,
181
+ "generator" => @generator,
182
+ "date" => @date,
183
+ "private" => @private,
184
+ "tags" => @tags.join(","),
185
+ "format" => @format
186
+ }
187
+ end
188
+ end
189
+
190
+ class Regular < Post
191
+ attr_accessor :regular_title, :regular_body
192
+
193
+ def params
194
+ super.merge!({"title" => @regular_title, "body" => @regular_body })
195
+ end
196
+ end
197
+
198
+ # TODO: Feed の扱いをどうするか
199
+ class Feed < Post
200
+ attr_accessor :regular_body, :feed_item, :from_feed_id
201
+ # TODO: titleのあるfeed itemってあるのか?
202
+ end
203
+
204
+ class Photo < Post
205
+ attr_accessor :photo_caption, :photo_link_url, :photo_url
206
+ #TODO: photo_url の max-width って何?
207
+ attr_accessor :data
208
+
209
+ # TODO: data をどうやってPOSTするか考える
210
+ # 生のデータを持たせるんじゃなく、TumblrPostDataみたいな
211
+ # クラスにラップして、それを各POSTのivarに保持させる?
212
+ def params
213
+ super.merge!(
214
+ {"source" => @photo_url,
215
+ "caption" => @photo_caption,
216
+ "click-through-url" => @photo_link_url,
217
+ "data" => @data})
218
+ end
219
+ end
220
+
221
+ class Quote < Post
222
+ attr_accessor :quote_text, :quote_source
223
+
224
+ def params
225
+ super.merge!(
226
+ {"quote" => @quote_text,
227
+ "source" => @quote_source})
228
+ end
229
+ end
230
+
231
+ class Link < Post
232
+ attr_accessor :link_text, :link_url, :link_description
233
+ def params
234
+ super.merge!(
235
+ {"name" => @link_text,
236
+ "url" => @link_url,
237
+ "description" => @link_description})
238
+ end
239
+ end
240
+
241
+ class Chat < Post
242
+ attr_accessor :conversation_title, :conversation_text
243
+ # <conversation><line name="..." label="...">text</line>のリスト</conversation>
244
+
245
+ def params
246
+ super.merge!(
247
+ {"title" => @conversation_title,
248
+ "conversation" => @conversation_text})
249
+ end
250
+ end
251
+
252
+ class Audio < Post
253
+ attr_accessor :audio_plays, :audio_caption, :audio_player
254
+ attr_accessor :data
255
+
256
+ def params
257
+ super.merge!(
258
+ {"data" => @data,
259
+ "caption" => @audio_caption})
260
+ end
261
+ end
262
+
263
+ class Video < Post
264
+ attr_accessor :video_caption, :video_source, :video_player
265
+ attr_accessor :data, :title
266
+ # TODO: title は vimeo へのアップロードのときのみ有効らしい
267
+ # TODO: embed を使うか、アップロードしたdataを使うかってのは
268
+ # Tumblr側で勝手に判断されるのかなぁ?
269
+ def params
270
+ super.merge!(
271
+ {"embed" => @video_source,
272
+ "data" => @data,
273
+ "title" => @title,
274
+ "caption" => @video_caption})
275
+ end
276
+ end
277
+
278
+ # Tumblr XML API への薄いラッパー。
279
+ # Rubyオブジェクトからの変換やRubyオブジェクトへの変換などは
280
+ # Parserクラスで行う。Parserクラスへの依存関係は一切持たない。
281
+ class XMLConnection
282
+ attr_accessor :logger, :group, :authenticated
283
+ def initialize(http_or_hostname, email=nil, password=nil, logger = nil)
284
+ case http_or_hostname
285
+ when String
286
+ @conn = Net::HTTP.new(http_or_hostname)
287
+ when Net::HTTP
288
+ @conn = http_or_hostname
289
+ else
290
+ raise ArgumentError.new("http_or_hostname must be String or Net::HTTP")
291
+ end
292
+ @email= email
293
+ @password = password
294
+ if @email && @password
295
+ begin
296
+ @authenticated = authenticate
297
+ rescue TumblrError
298
+ @authenticated = false
299
+ end
300
+ end
301
+ @group = @conn.address
302
+ @logger = logger || Logger.new(STDERR)
303
+ end
304
+
305
+ # @param [Hash] options :id, :type, :filter, :tagged, :search, :start, :num
306
+ def get(options = { })
307
+ params = options.map{|k, v|
308
+ "#{k}=#{v}"
309
+ }.join("&")
310
+ req = "/api/read?#{params}"
311
+ logger.info(req)
312
+ res = @conn.get(req)
313
+ logger.debug(res.body)
314
+ case res
315
+ when Net::HTTPOK
316
+ return res.body
317
+ when Net::HTTPNotFound
318
+ raise TumblrError.new("no such site(#{@hostname})")
319
+ else
320
+ raise TumblrError.new("unexpected response #{res.inspect}")
321
+ end
322
+ end
323
+
324
+ # @return true if email and password are valid
325
+ # @raise TumblrError if email or password is invalid
326
+ def authenticate
327
+ response = nil
328
+ http = Net::HTTP.new("www.tumblr.com")
329
+ response = http.post('/api/authenticate',
330
+ "email=#{CGI.escape(@email)}&password=#{CGI.escape(@password)}")
331
+
332
+ case response
333
+ when Net::HTTPOK
334
+ return true
335
+ else
336
+ raise TumblrError.new(response.inspect + "\n" + response.body)
337
+ end
338
+ end
339
+
340
+ # @return [Integer] post_id if success
341
+ # @raise [TumblrError] if fail
342
+ def write(options)
343
+ raise TumblrError.new("email or password is invalid") unless authenticated
344
+
345
+ response = nil
346
+ http = Net::HTTP.new("www.tumblr.com")
347
+ params = options.merge({"email" => @email, "password" => @password, "group" => @group})
348
+ query_string = params.delete_if{|k,v| v == nil }.map{|k,v| "#{k}=#{CGI.escape(v.to_s)}" unless v.nil?}.join("&")
349
+ logger.debug("#### query_string: #{query_string}")
350
+ response = http.post('/api/write', query_string)
351
+ case response
352
+ when Net::HTTPSuccess
353
+ return response.body.to_i
354
+ else
355
+ msg = response.inspect + "\n"
356
+ response.each{|k,v| msg += "#{k}: #{v}\n"}
357
+ msg += response.body
358
+ raise TumblrError.new(msg)
359
+ end
360
+ end
361
+
362
+ # @params [Integer] post_id
363
+ def delete(post_id)
364
+ raise TumblrError.new("email or password is invalid") unless authenticated
365
+ response = nil
366
+ http = Net::HTTP.new("www.tumblr.com")
367
+ params = {"post-id" => post_id, "email" => @email, "password" => @password, "group" => @group}
368
+ query_string = params.delete_if{|k,v| v == nil }.map{|k,v| "#{k}=#{CGI.escape(v.to_s)}" unless v.nil?}.join("&")
369
+ logger.debug("#### query_string: #{query_string}")
370
+ response = http.post('/api/delete', query_string)
371
+ case response
372
+ when Net::HTTPSuccess
373
+ logger.debug("#### response: #{response.code}: #{response.body}")
374
+ return true
375
+ else
376
+ msg = response.inspect + "\n"
377
+ response.each{|k,v| msg += "#{k}: #{v}\n"}
378
+ msg += response.body
379
+ raise TumblrError.new(msg)
380
+ end
381
+ end
382
+ end
383
+
384
+ # Tumblr XML API
385
+ class XMLParser
386
+ # @param [Site] site xmlをパースした結果を埋める入れ物
387
+ # @param [String] xml TumblrAPIのレスポンスのXMLそのまま
388
+ def siteinfo(site, xml)
389
+ xml_doc = REXML::Document.new(xml)
390
+ tumblelog = REXML::XPath.first(xml_doc, "//tumblr/tumblelog")
391
+ site.name = tumblelog.attributes["name"]
392
+ site.timezone = tumblelog.attributes["timezone"]
393
+ site.title = tumblelog.attributes["title"]
394
+ site.cname = tumblelog.attributes["cname"]
395
+ site.description = tumblelog.text
396
+ # tumblelog.elements["/feeds"]}
397
+ # TODO: feeds は後回し
398
+ return site
399
+ end
400
+
401
+ # XMLをパースしてオブジェクトのArrayを作る
402
+ # @param [String] xml APIからのレスポンス全体
403
+ # @return [Array<Post>, Integer, Integer] 各種Postの子クラスのArray, start, total
404
+ def posts(xml)
405
+ rexml_doc = REXML::Document.new(xml)
406
+ rexml_posts = REXML::XPath.first(rexml_doc, "//tumblr/posts")
407
+ start = rexml_posts.attributes["start"]
408
+ total = rexml_posts.attributes["total"]
409
+ posts = []
410
+ rexml_posts.elements.each("//posts/post") do |rexml_post|
411
+ post_type = rexml_post.attributes["type"]
412
+ post = nil
413
+ case post_type
414
+ when POST_TYPE::REGULAR
415
+ post = self.regular(Regular.new, rexml_post)
416
+ when POST_TYPE::PHOTO
417
+ post = self.photo(Photo.new, rexml_post)
418
+ when POST_TYPE::QUOTE
419
+ post = self.quote(Quote.new, rexml_post)
420
+ when POST_TYPE::LINK
421
+ post = self.link(Link.new, rexml_post)
422
+ when POST_TYPE::CHAT
423
+ post = self.chat(Chat.new, rexml_post)
424
+ when POST_TYPE::AUDIO
425
+ post = self.audio(Audio.new, rexml_post)
426
+ when POST_TYPE::VIDEO
427
+ post = self.video(Video.new, rexml_post)
428
+ else
429
+ raise TumblrError.new("unknown post type #{post_type}")
430
+ end
431
+ posts << post
432
+ end
433
+ return posts, start.to_i, total.to_i
434
+ end
435
+
436
+ # TODO: この辺りの設計についてはもう少し考慮の余地がある?
437
+ # みんな同じような構造(まずはpost(post, rexml_post)呼んでその後独自処理)してるし、
438
+ # 引数にpostとrexml_postをもらってくるってのもなんかイケてない気がする。
439
+ def post(post, rexml_post)
440
+ post.post_id = rexml_post.attributes["id"].to_i
441
+ post.url = rexml_post.attributes["url"]
442
+ post.url_with_slug = rexml_post.attributes["url-with-slug"]
443
+ post.type = rexml_post.attributes["type"]
444
+ # TODO: time 関係の型をStringじゃなくTimeとかにする?
445
+ post.date_gmt = rexml_post.attributes["date-gmt"]
446
+ post.date = rexml_post.attributes["date"]
447
+ post.unix_timestamp = rexml_post.attributes["unix-timestamp"].to_i
448
+ post.format = rexml_post.attributes["format"]
449
+ post.tags = rexml_post.get_elements("tag").map(&:text)
450
+ post.bookmarklet = (rexml_post.attributes["bookmarklet"] == "true")
451
+ post
452
+ end
453
+
454
+ def regular(post, rexml_post)
455
+ post = self.post(post, rexml_post)
456
+ post.regular_title = rexml_post.elements["regular-title"].try(:text) || ""
457
+ post.regular_body = rexml_post.elements["regular-body"].try(:text) || ""
458
+ post
459
+ end
460
+
461
+ def photo(post, rexml_post)
462
+ post = self.post(post, rexml_post)
463
+ post.type
464
+ post.photo_caption = rexml_post.elements["photo-caption"].try(:text) || ""
465
+ post.photo_link_url = rexml_post.elements["photo-link-url"].try(:text) || ""
466
+ post.photo_url = rexml_post.elements["photo-url"].try(:text) || ""
467
+ post
468
+ end
469
+
470
+ def quote(post, rexml_post)
471
+ post = self.post(post, rexml_post)
472
+ post.quote_text = rexml_post.elements["quote-text"].try(:text) || ""
473
+ post.quote_source = rexml_post.elements["quote-source"].try(:text) || ""
474
+ post
475
+ end
476
+
477
+ def link(post, rexml_post)
478
+ post = self.post(post, rexml_post)
479
+ post.link_text = rexml_post.elements["link-text"].try(:text) || ""
480
+ post.link_url = rexml_post.elements["link-url"].try(:text) || ""
481
+ post.link_description = rexml_post.elements["link-description"].try(:text) || ""
482
+ post
483
+ end
484
+
485
+ def chat(post, rexml_post)
486
+ post = self.post(post, rexml_post)
487
+ post.conversation_title = rexml_post.elements["conversation-title"].try(:text) || ""
488
+ post.conversation_text = rexml_post.elements["conversation-text"].try(:text) || ""
489
+ post
490
+ end
491
+
492
+ def audio(post, rexml_post)
493
+ post = self.post(post, rexml_post)
494
+ post.audio_plays = (rexml_post.attributes["audio-plays"] == "1")
495
+ post.audio_caption = rexml_post.elements["audio-caption"].try(:text) || ""
496
+ post.audio_player = rexml_post.elements["audio-player"].try(:text) || ""
497
+ post
498
+ end
499
+
500
+ def video(post, rexml_post)
501
+ post = self.post(post, rexml_post)
502
+ post.video_caption = rexml_post.elements["video-caption"].try(:text) || ""
503
+ post.video_source = rexml_post.elements["video-source"].try(:text) || ""
504
+ post.video_player = rexml_post.elements["video-player"].try(:text) || ""
505
+ post
506
+ end
507
+
508
+ end
509
+
510
+ end # module
@@ -0,0 +1,3 @@
1
+ require 'test/unit'
2
+ require File.dirname(__FILE__) + '/../lib/tumblr4r'
3
+
@@ -0,0 +1,379 @@
1
+ require File.dirname(__FILE__) + '/test_helper.rb'
2
+ require "test/unit"
3
+ require 'pit'
4
+ class Tumblr4rTest < Test::Unit::TestCase
5
+ include Tumblr4r
6
+ READ_TEST_HOST = "tumblr4rtest.tumblr.com"
7
+ WRITE_TEST_HOST = "tumblr4rwritetest.tumblr.com"
8
+ TOTAL_COUNT = 114
9
+ QUOTE_COUNT = 102
10
+
11
+ def setup
12
+ @site = Site.new(READ_TEST_HOST)
13
+ writetest_conf = Pit.get("tumblr4rwritetest",
14
+ :require => {"email" => "required email",
15
+ "password" => "required password"})
16
+ @write_site = Site.new(WRITE_TEST_HOST,
17
+ writetest_conf["email"],
18
+ writetest_conf["password"])
19
+ @large_site = Site.new("tumblr4rfindtest.tumblr.com")
20
+ end
21
+
22
+ def teardown
23
+ end
24
+
25
+
26
+ def test_initialize
27
+ assert_equal READ_TEST_HOST, @site.hostname
28
+ assert_nil @site.email
29
+ assert_nil @site.password
30
+
31
+ assert_equal "tumblr4rtest", @site.name
32
+ assert_equal "Asia/Tokyo", @site.timezone
33
+ assert_equal "tumblr4rテスト", @site.title
34
+ assert_nil @site.cname
35
+ assert_equal "tumblr4rのテスト用サイトです。\r\ntumblrサイコー", @site.description
36
+ # TODO: feeds は後回し
37
+ end
38
+
39
+ def test_count
40
+ total = @large_site.count
41
+ assert_equal TOTAL_COUNT, total
42
+
43
+ total = @large_site.count(:type => "quote")
44
+ assert_equal QUOTE_COUNT, total
45
+
46
+ total = @large_site.count(:filter => "text")
47
+ assert_equal TOTAL_COUNT, total
48
+
49
+ total = @large_site.count(:tagged => "test")
50
+ assert_equal TOTAL_COUNT, total
51
+
52
+ total = @large_site.count(:search => "test")
53
+ assert_equal TOTAL_COUNT, total
54
+ end
55
+
56
+ def test_find
57
+ posts = @site.find(:all)
58
+ assert_equal 8, posts.size
59
+ assert_equal Video, posts[0].class
60
+ end
61
+
62
+ def test_find_all
63
+ posts = @large_site.find(:all)
64
+ assert_equal TOTAL_COUNT, posts.size
65
+ end
66
+
67
+ def test_find_all_quote
68
+ posts = @large_site.find(:all, :type => "quote")
69
+ assert_equal QUOTE_COUNT, posts.size
70
+ end
71
+
72
+ # 実際に存在する件数より少なく指定した場合
73
+ def test_find_all_with_num
74
+ posts = @large_site.find(:all, :limit => 74)
75
+ assert_equal 74, posts.size
76
+ end
77
+
78
+ # 実際に存在する件数より多く指定した場合
79
+ def test_find_all_with_over_num
80
+ posts = @large_site.find(:all, :limit => 765)
81
+ assert_equal TOTAL_COUNT, posts.size
82
+ end
83
+
84
+ def test_find_all_with_offset
85
+ posts = @large_site.find(:all, :offset => 12)
86
+ assert_equal TOTAL_COUNT-12, posts.size
87
+ end
88
+
89
+ # 実際に存在するよりも多いoffsetを指定した場合
90
+ def test_find_all_over_offset
91
+ posts = @large_site.find(:all, :type => "quote", :offset => TOTAL_COUNT + 1)
92
+ assert_equal 0, posts.size
93
+ end
94
+
95
+ def test_find_with_type_regular
96
+ posts = @site.find(:all, :type => "regular")
97
+ assert_equal 2, posts.size
98
+
99
+ assert_equal Regular, posts[0].class
100
+ assert_equal 123459291, posts[0].post_id
101
+ assert_equal "http://tumblr4rtest.tumblr.com/post/123459291", posts[0].url
102
+ assert_equal "http://tumblr4rtest.tumblr.com/post/123459291/regular-test", posts[0].url_with_slug
103
+ assert_equal "regular", posts[0].type
104
+ assert_equal "2009-06-14 16:30:44 GMT", posts[0].date_gmt
105
+ assert_equal "Mon, 15 Jun 2009 01:30:44", posts[0].date
106
+ assert_equal 1244997044, posts[0].unix_timestamp
107
+ assert_equal "html", posts[0].format
108
+ assert_equal ["test", "regular"], posts[0].tags
109
+ assert_equal false, posts[0].bookmarklet
110
+
111
+ assert_equal "Text Postのテストです", posts[0].regular_title
112
+ assert_equal <<EOF.chomp, posts[0].regular_body
113
+ <p>テキストです。</p>
114
+ <p><b>ボールドです。</b></p>
115
+ <p><i>イタリックです。</i></p>
116
+ <p><strike>取り消し線です。</strike></p>
117
+ <ul>
118
+ <li>unordered 1</li>
119
+ <li>unordered 2</li>
120
+ </ul>
121
+ <ol>
122
+ <li>ordered 1</li>
123
+ <li>ordered 2</li>
124
+ </ol>
125
+ <p>ここからインデント</p>
126
+ <blockquote style="margin: 0 0 0 40px; border: none; padding: 0px;">インデント開始<br/>ああああ<br/>インデント終了</blockquote>
127
+ <p>ここまでインデント</p>
128
+ EOF
129
+
130
+ assert_equal Regular, posts[1].class
131
+ assert_equal 122871637, posts[1].post_id
132
+ assert_equal "http://tumblr4rtest.tumblr.com/post/122871637", posts[1].url
133
+ assert_equal "http://tumblr4rtest.tumblr.com/post/122871637/tumblr4r", posts[1].url_with_slug
134
+ assert_equal "regular", posts[1].type
135
+ assert_equal "2009-06-13 12:46:23 GMT", posts[1].date_gmt
136
+ assert_equal "Sat, 13 Jun 2009 21:46:23", posts[1].date
137
+ assert_equal 1244897183, posts[1].unix_timestamp
138
+ assert_equal "html", posts[1].format
139
+ assert_equal [], posts[1].tags
140
+ assert_equal false, posts[1].bookmarklet
141
+
142
+ assert_equal "", posts[1].regular_title
143
+ assert_equal "Tumblr4rのテストです", posts[1].regular_body
144
+ end
145
+
146
+ def test_find_with_type_photo
147
+ posts = @site.find(:all, :type => "photo")
148
+ assert_equal 1, posts.size
149
+ assert_equal Photo, posts[0].class
150
+ assert_equal 123461063, posts[0].post_id
151
+ assert_equal "http://tumblr4rtest.tumblr.com/post/123461063", posts[0].url
152
+ assert_equal "http://tumblr4rtest.tumblr.com/post/123461063/photo", posts[0].url_with_slug
153
+ assert_equal "photo", posts[0].type
154
+ assert_equal "2009-06-14 16:34:50 GMT", posts[0].date_gmt
155
+ assert_equal "Mon, 15 Jun 2009 01:34:50", posts[0].date
156
+ assert_equal 1244997290, posts[0].unix_timestamp
157
+ assert_equal "html", posts[0].format
158
+ assert_equal ["test", "photo"], posts[0].tags
159
+ assert_equal false, posts[0].bookmarklet
160
+
161
+ assert_equal "<p>Photoのテストです。</p>\n\n<p>ギコです。</p>", posts[0].photo_caption
162
+ assert_equal "http://www.google.co.jp/", posts[0].photo_link_url
163
+ assert_equal "http://5.media.tumblr.com/GyEYZujUYopiula4XKmXhCgmo1_250.jpg", posts[0].photo_url
164
+ end
165
+
166
+ def test_find_with_type_quote
167
+ posts = @site.find(:all, :type => "quote")
168
+ assert_equal 1, posts.size
169
+ assert_equal Quote, posts[0].class
170
+ assert_equal 123470309, posts[0].post_id
171
+ assert_equal "http://tumblr4rtest.tumblr.com/post/123470309", posts[0].url
172
+ assert_equal "http://tumblr4rtest.tumblr.com/post/123470309/wikipedia-tumblr", posts[0].url_with_slug
173
+ assert_equal "quote", posts[0].type
174
+ assert_equal "2009-06-14 16:58:00 GMT", posts[0].date_gmt
175
+ assert_equal "Mon, 15 Jun 2009 01:58:00", posts[0].date
176
+ assert_equal 1244998680, posts[0].unix_timestamp
177
+ assert_equal "html", posts[0].format
178
+ assert_equal ["quote", "test"], posts[0].tags
179
+ assert_equal true, posts[0].bookmarklet
180
+
181
+ assert_equal <<EOF.chomp, posts[0].quote_text
182
+ Tumblelog/Tumblr(タンブルログ/タンブラー)は、メディアミックスウェブログサービス。米国 Davidville.incにより2007年3月1日にサービスが開始された。
183
+ EOF
184
+ assert_equal "<a href=\"http://ja.wikipedia.org/wiki/Tumblr\">Tumblelog - Wikipedia</a>", posts[0].quote_source
185
+ end
186
+
187
+ def test_find_with_type_link
188
+ posts = @site.find(:all, :type => "link")
189
+ assert_equal 1, posts.size
190
+ assert_equal Link, posts[0].class
191
+ assert_equal 123470990, posts[0].post_id
192
+ assert_equal "http://tumblr4rtest.tumblr.com/post/123470990", posts[0].url
193
+ assert_equal "http://tumblr4rtest.tumblr.com/post/123470990/tumblr-link", posts[0].url_with_slug
194
+ assert_equal "link", posts[0].type
195
+ assert_equal "2009-06-14 17:00:00 GMT", posts[0].date_gmt
196
+ assert_equal "Mon, 15 Jun 2009 02:00:00", posts[0].date
197
+ assert_equal 1244998800, posts[0].unix_timestamp
198
+ assert_equal "html", posts[0].format
199
+ assert_equal ["test", "link"], posts[0].tags
200
+ assert_equal false, posts[0].bookmarklet
201
+
202
+ assert_equal "たんぶらー", posts[0].link_text
203
+ assert_equal "http://www.tumblr.com/", posts[0].link_url
204
+ assert_equal "ですくりぷしょん", posts[0].link_description
205
+ end
206
+
207
+ def test_find_with_type_conversation
208
+ posts = @site.find(:all, :type => "conversation")
209
+ assert_equal 1, posts.size
210
+ assert_equal Chat, posts[0].class
211
+ assert_equal 123471808, posts[0].post_id
212
+ assert_equal "http://tumblr4rtest.tumblr.com/post/123471808", posts[0].url
213
+ assert_equal "http://tumblr4rtest.tumblr.com/post/123471808/pointcard-chat", posts[0].url_with_slug
214
+ assert_equal "conversation", posts[0].type
215
+ assert_equal "2009-06-14 17:01:54 GMT", posts[0].date_gmt
216
+ assert_equal "Mon, 15 Jun 2009 02:01:54", posts[0].date
217
+ assert_equal 1244998914, posts[0].unix_timestamp
218
+ assert_equal "html", posts[0].format
219
+ assert_equal ["test", "chat"], posts[0].tags
220
+ assert_equal false, posts[0].bookmarklet
221
+
222
+ assert_equal "なにそれこわい", posts[0].conversation_title
223
+ assert_equal <<EOF.chomp, posts[0].conversation_text
224
+ 店員: 当店のポイントカードはお餅でしょうか\r
225
+ ぼく: えっ\r
226
+ 店員: 当店のポイントカードはお餅ですか \r
227
+ ぼく: いえしりません\r
228
+ 店員: えっ\r
229
+ ぼく: えっ\r
230
+ EOF
231
+ end
232
+
233
+ def test_find_with_type_audio
234
+ posts = @site.find(:all, :type => "audio")
235
+ assert_equal 1, posts.size
236
+ assert_equal Audio, posts[0].class
237
+ assert_equal 131705561, posts[0].post_id
238
+ assert_equal "http://tumblr4rtest.tumblr.com/post/131705561", posts[0].url
239
+ assert_equal "http://tumblr4rtest.tumblr.com/post/131705561/tumblr4r-miku", posts[0].url_with_slug
240
+ assert_equal "audio", posts[0].type
241
+ assert_equal "2009-06-28 13:58:13 GMT", posts[0].date_gmt
242
+ assert_equal "Sun, 28 Jun 2009 22:58:13", posts[0].date
243
+ assert_equal 1246197493, posts[0].unix_timestamp
244
+ assert_equal "html", posts[0].format
245
+ assert_equal [], posts[0].tags
246
+ assert_equal false, posts[0].bookmarklet
247
+
248
+ assert_equal true, posts[0].audio_plays
249
+ assert_equal "tumblr4r miku", posts[0].audio_caption
250
+ assert_equal "<embed type=\"application/x-shockwave-flash\" src=\"http://tumblr4rtest.tumblr.com/swf/audio_player.swf?audio_file=http://www.tumblr.com/audio_file/131705561/GyEYZujUYp9df3nv1WMefTH8&color=FFFFFF\" height=\"27\" width=\"207\" quality=\"best\"></embed>", posts[0].audio_player
251
+ end
252
+
253
+ def test_find_with_type_video
254
+ posts = @site.find(:all, :type => "video")
255
+ assert_equal 1, posts.size
256
+ assert_equal Video, posts[0].class
257
+ assert_equal 131714219, posts[0].post_id
258
+ assert_equal "http://tumblr4rtest.tumblr.com/post/131714219", posts[0].url
259
+ assert_equal "http://tumblr4rtest.tumblr.com/post/131714219/matrix-sappoloaded", posts[0].url_with_slug
260
+ assert_equal "video", posts[0].type
261
+ assert_equal "2009-06-28 14:22:56 GMT", posts[0].date_gmt
262
+ assert_equal "Sun, 28 Jun 2009 23:22:56", posts[0].date
263
+ assert_equal 1246198976, posts[0].unix_timestamp
264
+ assert_equal "html", posts[0].format
265
+ assert_equal [], posts[0].tags
266
+ assert_equal false, posts[0].bookmarklet
267
+
268
+ assert_equal "matrix sappoloaded", posts[0].video_caption
269
+ assert_equal "http://www.youtube.com/watch?v=FavWH5RhYpw", posts[0].video_source
270
+ assert_equal "<object width=\"400\" height=\"336\"><param name=\"movie\" value=\"http://www.youtube.com/v/FavWH5RhYpw&amp;rel=0&amp;egm=0&amp;showinfo=0&amp;fs=1\"></param><param name=\"wmode\" value=\"transparent\"></param><param name=\"allowFullScreen\" value=\"true\"></param><embed src=\"http://www.youtube.com/v/FavWH5RhYpw&amp;rel=0&amp;egm=0&amp;showinfo=0&amp;fs=1\" type=\"application/x-shockwave-flash\" width=\"400\" height=\"336\" allowFullScreen=\"true\" wmode=\"transparent\"></embed></object>", posts[0].video_player
271
+ end
272
+
273
+ def test_find_with_tagged
274
+ posts = @site.find(:all, :tagged => "test")
275
+ assert_equal 5, posts.size
276
+ end
277
+
278
+ def test_find_with_search
279
+ posts = @site.find(:all, :search => "matrix")
280
+ assert_equal 1, posts.size
281
+ end
282
+
283
+ def test_generator_default
284
+ post = Post.new
285
+ assert_equal "Tumblr4R", post.generator
286
+
287
+ Post.default_generator = "foo"
288
+ post2 = Post.new
289
+ assert_equal "foo", post2.generator
290
+ assert_equal "Tumblr4R", post.generator
291
+
292
+
293
+ Post.default_generator = nil
294
+ post3 = Post.new
295
+ assert_equal "Tumblr4R", post3.generator
296
+ end
297
+
298
+ def test_find_by_id
299
+ post = @site.find(123459291)
300
+
301
+ assert_equal Regular, post.class
302
+ assert_equal 123459291, post.post_id
303
+ assert_equal "http://tumblr4rtest.tumblr.com/post/123459291", post.url
304
+ assert_equal "http://tumblr4rtest.tumblr.com/post/123459291/regular-test", post.url_with_slug
305
+ assert_equal "regular", post.type
306
+ assert_equal "2009-06-14 16:30:44 GMT", post.date_gmt
307
+ assert_equal "Mon, 15 Jun 2009 01:30:44", post.date
308
+ assert_equal 1244997044, post.unix_timestamp
309
+ assert_equal "html", post.format
310
+ assert_equal ["test", "regular"], post.tags
311
+ assert_equal false, post.bookmarklet
312
+
313
+ assert_equal "Text Postのテストです", post.regular_title
314
+ assert_equal <<EOF.chomp, post.regular_body
315
+ <p>テキストです。</p>
316
+ <p><b>ボールドです。</b></p>
317
+ <p><i>イタリックです。</i></p>
318
+ <p><strike>取り消し線です。</strike></p>
319
+ <ul>
320
+ <li>unordered 1</li>
321
+ <li>unordered 2</li>
322
+ </ul>
323
+ <ol>
324
+ <li>ordered 1</li>
325
+ <li>ordered 2</li>
326
+ </ol>
327
+ <p>ここからインデント</p>
328
+ <blockquote style="margin: 0 0 0 40px; border: none; padding: 0px;">インデント開始<br/>ああああ<br/>インデント終了</blockquote>
329
+ <p>ここまでインデント</p>
330
+ EOF
331
+ end
332
+
333
+ def test_quote_create
334
+ quotes = @site.find(:all, :type => Tumblr4r::POST_TYPE::QUOTE)
335
+ post = @write_site.save(quotes[0])
336
+ assert_not_equal quotes[0].post_id, post.post_id
337
+ assert_equal "http://#{WRITE_TEST_HOST}/post/#{post.post_id}", post.url
338
+ assert_equal "http://#{WRITE_TEST_HOST}/post/#{post.post_id}/tumblelog-tumblr", post.url_with_slug
339
+ assert_equal Tumblr4r::POST_TYPE::QUOTE, post.type
340
+ assert_equal quotes[0].date_gmt, post.date_gmt
341
+ assert_equal quotes[0].date, post.date
342
+ # TODO
343
+ # assert_equal 1245045480, post.unix_timestamp
344
+ assert_equal quotes[0].format, post.format
345
+ assert_equal quotes[0].tags, post.tags
346
+ assert_equal false, post.bookmarklet
347
+
348
+ assert_equal quotes[0].quote_text, post.quote_text
349
+ assert_equal quotes[0].quote_source, post.quote_source
350
+ end
351
+
352
+ def test_regular_create
353
+ regulars = @site.find(:all, :type => Tumblr4r::POST_TYPE::REGULAR)
354
+ post = @write_site.save(regulars[0])
355
+ end
356
+
357
+ def test_link_create
358
+ links = @site.find(:all, :type => Tumblr4r::POST_TYPE::LINK)
359
+ post = @write_site.save(links[0])
360
+ end
361
+
362
+ def test_photo_create
363
+ photos = @site.find(:all, :type => Tumblr4r::POST_TYPE::PHOTO)
364
+ post = @write_site.save(photos[0])
365
+ end
366
+
367
+
368
+ # def test_audio_create
369
+ # audios = @site.find(:all, :type => Tumblr4r::POST_TYPE::AUDIO)
370
+ # post = @write_site.save(audios[0])
371
+ # end
372
+
373
+ def test_video_create
374
+ videos = @site.find(:all, :type => Tumblr4r::POST_TYPE::VIDEO)
375
+ post = @write_site.save(videos[0])
376
+ end
377
+
378
+ end
379
+
metadata ADDED
@@ -0,0 +1,89 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: tumblr4r
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.7.0
5
+ platform: ruby
6
+ authors:
7
+ - Tomoki MAEDA
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-09-09 00:00:00 +09:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: activesupport
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 2.3.2
24
+ version:
25
+ - !ruby/object:Gem::Dependency
26
+ name: pit
27
+ type: :development
28
+ version_requirement:
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 0.0.6
34
+ version:
35
+ description: Tumblr API Wrapper for Ruby
36
+ email: tmaeda@ruby-sapporo.org
37
+ executables: []
38
+
39
+ extensions: []
40
+
41
+ extra_rdoc_files:
42
+ - README
43
+ - ChangeLog
44
+ files:
45
+ - README
46
+ - ChangeLog
47
+ - Rakefile
48
+ - test/test_helper.rb
49
+ - test/tumblr4r_test.rb
50
+ - lib/tumblr4r.rb
51
+ has_rdoc: true
52
+ homepage: http://tumblr4r.rubyforge.org
53
+ post_install_message:
54
+ rdoc_options:
55
+ - --title
56
+ - tumblr4r documentation
57
+ - --charset
58
+ - utf-8
59
+ - --opname
60
+ - index.html
61
+ - --line-numbers
62
+ - --main
63
+ - README
64
+ - --inline-source
65
+ - --exclude
66
+ - ^(examples|extras)/
67
+ require_paths:
68
+ - lib
69
+ required_ruby_version: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: 1.8.6
74
+ version:
75
+ required_rubygems_version: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ version: "0"
80
+ version:
81
+ requirements: []
82
+
83
+ rubyforge_project: tumblr4r
84
+ rubygems_version: 1.3.1
85
+ signing_key:
86
+ specification_version: 2
87
+ summary: Tumblr API Wrapper for Ruby
88
+ test_files:
89
+ - test/tumblr4r_test.rb