ideaoforder-www-delicious 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.rdoc +46 -0
- data/Manifest +49 -0
- data/README.rdoc +209 -0
- data/Rakefile +55 -0
- data/lib/www/delicious/bundle.rb +73 -0
- data/lib/www/delicious/element.rb +73 -0
- data/lib/www/delicious/errors.rb +46 -0
- data/lib/www/delicious/post.rb +123 -0
- data/lib/www/delicious/tag.rb +101 -0
- data/lib/www/delicious/version.rb +29 -0
- data/lib/www/delicious.rb +949 -0
- data/setup.rb +1585 -0
- data/test/fixtures/net_response_invalid_account.yml +25 -0
- data/test/fixtures/net_response_success.yml +23 -0
- data/test/helper.rb +49 -0
- data/test/test_all.rb +18 -0
- data/test/test_offline.rb +18 -0
- data/test/test_online.rb +20 -0
- data/test/testcases/element/bundle.xml +1 -0
- data/test/testcases/element/invalid_root.xml +2 -0
- data/test/testcases/element/post.xml +2 -0
- data/test/testcases/element/post_unshared.xml +2 -0
- data/test/testcases/element/tag.xml +1 -0
- data/test/testcases/response/bundles_all.xml +5 -0
- data/test/testcases/response/bundles_all_empty.xml +2 -0
- data/test/testcases/response/bundles_delete.xml +2 -0
- data/test/testcases/response/bundles_set.xml +2 -0
- data/test/testcases/response/bundles_set_error.xml +2 -0
- data/test/testcases/response/posts_add.xml +2 -0
- data/test/testcases/response/posts_all.xml +12 -0
- data/test/testcases/response/posts_dates.xml +14 -0
- data/test/testcases/response/posts_dates_with_tag.xml +14 -0
- data/test/testcases/response/posts_delete.xml +2 -0
- data/test/testcases/response/posts_get.xml +7 -0
- data/test/testcases/response/posts_get_with_tag.xml +6 -0
- data/test/testcases/response/posts_recent.xml +19 -0
- data/test/testcases/response/posts_recent_with_tag.xml +19 -0
- data/test/testcases/response/tags_get.xml +5 -0
- data/test/testcases/response/tags_get_empty.xml +2 -0
- data/test/testcases/response/tags_rename.xml +2 -0
- data/test/testcases/response/update.delicious1.xml +2 -0
- data/test/testcases/response/update.xml +3 -0
- data/test/unit/bundle_test.rb +63 -0
- data/test/unit/delicious_test.rb +369 -0
- data/test/unit/online/online_test.rb +148 -0
- data/test/unit/post_test.rb +68 -0
- data/test/unit/tag_test.rb +69 -0
- data/www-delicious.gemspec +146 -0
- metadata +143 -0
@@ -0,0 +1,949 @@
|
|
1
|
+
#
|
2
|
+
# = WWW::Delicious
|
3
|
+
#
|
4
|
+
# Ruby client for del.icio.us API.
|
5
|
+
#
|
6
|
+
#
|
7
|
+
# Category:: WWW
|
8
|
+
# Package:: WWW::Delicious
|
9
|
+
# Author:: Simone Carletti <weppos@weppos.net>
|
10
|
+
# License:: MIT License
|
11
|
+
#
|
12
|
+
#--
|
13
|
+
# SVN: $Id$
|
14
|
+
#++
|
15
|
+
|
16
|
+
|
17
|
+
require 'net/https'
|
18
|
+
require 'rexml/document'
|
19
|
+
require 'time'
|
20
|
+
require 'www/delicious/bundle'
|
21
|
+
require 'www/delicious/post'
|
22
|
+
require 'www/delicious/tag'
|
23
|
+
require 'www/delicious/errors'
|
24
|
+
require 'www/delicious/version'
|
25
|
+
|
26
|
+
|
27
|
+
module WWW #:nodoc:
|
28
|
+
|
29
|
+
|
30
|
+
#
|
31
|
+
# = WWW::Delicious
|
32
|
+
#
|
33
|
+
# WWW::Delicious is a Ruby client for http://del.icio.us XML API.
|
34
|
+
#
|
35
|
+
# It provides both read and write functionalities.
|
36
|
+
# You can read user Posts, Tags and Bundles
|
37
|
+
# but you can create new Posts, Tags and Bundles as well.
|
38
|
+
#
|
39
|
+
#
|
40
|
+
# == Basic Usage
|
41
|
+
#
|
42
|
+
# The following is just a basic demonstration of the main features.
|
43
|
+
# See the README file for a deeper explanation about how to get the best
|
44
|
+
# from WWW::Delicious library.
|
45
|
+
#
|
46
|
+
# The examples in this page make the following assumptions
|
47
|
+
# * you have a valid del.icio.us account
|
48
|
+
# * +username+ is your account username
|
49
|
+
# * +password+ is your account password
|
50
|
+
#
|
51
|
+
# In order to make a query you first need to create
|
52
|
+
# a new WWW::Delicious instance as follows:
|
53
|
+
#
|
54
|
+
# require 'www/delicious'
|
55
|
+
#
|
56
|
+
# username = 'my delicious username'
|
57
|
+
# password = 'my delicious password'
|
58
|
+
#
|
59
|
+
# d = WWW::Delicious.new(username, password)
|
60
|
+
#
|
61
|
+
# The constructor accepts some additional options.
|
62
|
+
# For instance, if you want to customize the user agent:
|
63
|
+
#
|
64
|
+
# d = WWW::Delicious.new(username, password, :user_agent => 'FooAgent')
|
65
|
+
#
|
66
|
+
# Now you can use any of the API methods available.
|
67
|
+
#
|
68
|
+
# For example, you may want to know when your account was last updated
|
69
|
+
# to check whether someone else made some changes on behalf of you:
|
70
|
+
#
|
71
|
+
# datetime = d.update # => Wed Mar 12 08:41:20 UTC 2008
|
72
|
+
#
|
73
|
+
# Because the answer is a valid +Time+ instance, you can format it with +strftime+.
|
74
|
+
#
|
75
|
+
# datetime = d.update # => Wed Mar 12 08:41:20 UTC 2008
|
76
|
+
# datetime.strftime('%Y') # => 2008
|
77
|
+
#
|
78
|
+
#
|
79
|
+
# Category:: WWW
|
80
|
+
# Package:: WWW::Delicious
|
81
|
+
# Author:: Simone Carletti <weppos@weppos.net>
|
82
|
+
#
|
83
|
+
class Delicious
|
84
|
+
|
85
|
+
NAME = 'WWW::Delicious'
|
86
|
+
GEM = 'www-delicious'
|
87
|
+
AUTHOR = 'Simone Carletti <weppos@weppos.net>'
|
88
|
+
VERSION = defined?(Version) ? Version::STRING : nil
|
89
|
+
STATUS = 'alpha'
|
90
|
+
BUILD = ''.match(/(\d+)/).to_a.first
|
91
|
+
|
92
|
+
# del.icio.us account username
|
93
|
+
attr_reader :username
|
94
|
+
|
95
|
+
# del.icio.us account password
|
96
|
+
attr_reader :password
|
97
|
+
|
98
|
+
# base URI for del.icio.us API
|
99
|
+
attr_reader :base_uri
|
100
|
+
|
101
|
+
|
102
|
+
# API Base URL
|
103
|
+
API_BASE_URI = 'https://api.del.icio.us'
|
104
|
+
|
105
|
+
# API Path Update
|
106
|
+
API_PATH_UPDATE = '/v1/posts/update';
|
107
|
+
|
108
|
+
# API Path All Bundles
|
109
|
+
API_PATH_BUNDLES_ALL = '/v1/tags/bundles/all';
|
110
|
+
# API Path Set Bundle
|
111
|
+
API_PATH_BUNDLES_SET = '/v1/tags/bundles/set';
|
112
|
+
# API Path Delete Bundle
|
113
|
+
API_PATH_BUNDLES_DELETE = '/v1/tags/bundles/delete';
|
114
|
+
|
115
|
+
# API Path Get Tags
|
116
|
+
API_PATH_TAGS_GET = '/v1/tags/get';
|
117
|
+
# API Path Rename Tag
|
118
|
+
API_PATH_TAGS_RENAME = '/v1/tags/rename';
|
119
|
+
|
120
|
+
# API Path Get Posts
|
121
|
+
API_PATH_POSTS_GET = '/v1/posts/get';
|
122
|
+
# API Path Recent Posts
|
123
|
+
API_PATH_POSTS_RECENT = '/v1/posts/recent';
|
124
|
+
# API Path All Posts
|
125
|
+
API_PATH_POSTS_ALL = '/v1/posts/all';
|
126
|
+
# API Path Posts by Dates
|
127
|
+
API_PATH_POSTS_DATES = '/v1/posts/dates';
|
128
|
+
# API Path Add Post
|
129
|
+
API_PATH_POSTS_ADD = '/v1/posts/add';
|
130
|
+
# API Path Delete Post
|
131
|
+
API_PATH_POSTS_DELETE = '/v1/posts/delete';
|
132
|
+
|
133
|
+
# Time to wait before sending a new request, in seconds
|
134
|
+
SECONDS_BEFORE_NEW_REQUEST = 1
|
135
|
+
|
136
|
+
# Time converter converts a Time instance into the format
|
137
|
+
# requested by Delicious API
|
138
|
+
TIME_CONVERTER = lambda { |time| time.iso8601() }
|
139
|
+
|
140
|
+
|
141
|
+
#
|
142
|
+
# Constructs a new <tt>WWW::Delicious</tt> object
|
143
|
+
# with given +username+ and +password+.
|
144
|
+
#
|
145
|
+
# # create a new object with username 'user' and password 'psw
|
146
|
+
# obj = WWW::Delicious('user', 'psw')
|
147
|
+
# # => self
|
148
|
+
#
|
149
|
+
# If a block is given, the instance is passed to the block
|
150
|
+
# but this method always returns the instance itself.
|
151
|
+
#
|
152
|
+
# WWW::Delicious('user', 'psw') do |d|
|
153
|
+
# d.update() # => Fri May 02 18:02:48 UTC 2008
|
154
|
+
# end
|
155
|
+
# # => self
|
156
|
+
#
|
157
|
+
# You can also specify some additional options, including a custom user agent
|
158
|
+
# or the base URI for del.icio.us API.
|
159
|
+
#
|
160
|
+
# WWW::Delicious('user', 'psw', :base_uri => 'https://ma.gnolia.com/api/mirrord') do |d|
|
161
|
+
# # the following call is mirrored by ma.gnolia
|
162
|
+
# d.update() # => Fri May 02 18:02:48 UTC 2008
|
163
|
+
# end
|
164
|
+
# # => self
|
165
|
+
#
|
166
|
+
# === Options
|
167
|
+
# This class accepts a Hash with additional options.
|
168
|
+
# Here's the list of valid keys:
|
169
|
+
#
|
170
|
+
# <tt>:user_agent</tt>:: User agent to display in HTTP requests.
|
171
|
+
# <tt>:base_uri</tt>:: The base URI to del.icio.us API.
|
172
|
+
#
|
173
|
+
def initialize(username, password, options = {}, &block) # :yields: delicious
|
174
|
+
@username, @password = username.to_s, password.to_s
|
175
|
+
|
176
|
+
# set API base URI
|
177
|
+
@base_uri = URI.parse(options[:base_uri] || API_BASE_URI)
|
178
|
+
|
179
|
+
init_user_agent(options)
|
180
|
+
init_http_client(options)
|
181
|
+
|
182
|
+
yield self if block_given?
|
183
|
+
self # ensure to always return self even if block is given
|
184
|
+
end
|
185
|
+
|
186
|
+
|
187
|
+
#
|
188
|
+
# Returns the reference to current <tt>@http_client</tt>.
|
189
|
+
# The http is always valid unless it has been previously set to +nil+.
|
190
|
+
#
|
191
|
+
# # nil client
|
192
|
+
# obj.http_client # => nil
|
193
|
+
#
|
194
|
+
# # valid client
|
195
|
+
# obj.http_client # => Net::HTTP
|
196
|
+
#
|
197
|
+
def http_client()
|
198
|
+
return @http_client
|
199
|
+
end
|
200
|
+
|
201
|
+
#
|
202
|
+
# Sets the internal <tt>@http_client</tt> to +client+.
|
203
|
+
#
|
204
|
+
# # nil client
|
205
|
+
# obj.http_client = nil
|
206
|
+
#
|
207
|
+
# # http client
|
208
|
+
# obj.http_client = Net::HTTP.new()
|
209
|
+
#
|
210
|
+
# # invalid client
|
211
|
+
# obj.http_client = 'foo' # => ArgumentError
|
212
|
+
#
|
213
|
+
def http_client=(client)
|
214
|
+
unless client.kind_of?(Net::HTTP) or client.nil?
|
215
|
+
raise ArgumentError, "`client` expected to be a kind of `Net::HTTP`, `#{client.class}` given"
|
216
|
+
end
|
217
|
+
@http_client = client
|
218
|
+
end
|
219
|
+
|
220
|
+
# Returns current user agent string.
|
221
|
+
def user_agent()
|
222
|
+
return @headers['User-Agent']
|
223
|
+
end
|
224
|
+
|
225
|
+
|
226
|
+
#
|
227
|
+
# Returns true if given account credentials are valid.
|
228
|
+
#
|
229
|
+
# d = WWW::Delicious.new('username', 'password')
|
230
|
+
# d.valid_account? # => true
|
231
|
+
#
|
232
|
+
# d = WWW::Delicious.new('username', 'invalid_password')
|
233
|
+
# d.valid_account? # => false
|
234
|
+
#
|
235
|
+
# This method is not "exception safe".
|
236
|
+
# It doesn't return false if an HTTP error or any kind of other error occurs,
|
237
|
+
# it raises back the exception to the caller instead.
|
238
|
+
#
|
239
|
+
#
|
240
|
+
# Raises:: WWW::Delicious::Error
|
241
|
+
# Raises:: WWW::Delicious::HTTPError
|
242
|
+
# Raises:: WWW::Delicious::ResponseError
|
243
|
+
#
|
244
|
+
def valid_account?
|
245
|
+
update()
|
246
|
+
return true
|
247
|
+
rescue HTTPError => e
|
248
|
+
return false if e.message =~ /invalid username or password/i
|
249
|
+
raise
|
250
|
+
end
|
251
|
+
|
252
|
+
#
|
253
|
+
# Checks to see when a user last posted an item
|
254
|
+
# and returns the last update +Time+ for the user.
|
255
|
+
#
|
256
|
+
# d.update() # => Fri May 02 18:02:48 UTC 2008
|
257
|
+
#
|
258
|
+
#
|
259
|
+
# Raises:: WWW::Delicious::Error
|
260
|
+
# Raises:: WWW::Delicious::HTTPError
|
261
|
+
# Raises:: WWW::Delicious::ResponseError
|
262
|
+
#
|
263
|
+
def update()
|
264
|
+
response = request(API_PATH_UPDATE)
|
265
|
+
return parse_update_response(response.body)
|
266
|
+
end
|
267
|
+
|
268
|
+
#
|
269
|
+
# Retrieves all of a user's bundles
|
270
|
+
# and returns an array of <tt>WWW::Delicious::Bundle</tt>.
|
271
|
+
#
|
272
|
+
# d.bundles_all() # => [#<WWW::Delicious::Bundle>, #<WWW::Delicious::Bundle>, ...]
|
273
|
+
# d.bundles_all() # => []
|
274
|
+
#
|
275
|
+
#
|
276
|
+
# Raises:: WWW::Delicious::Error
|
277
|
+
# Raises:: WWW::Delicious::HTTPError
|
278
|
+
# Raises:: WWW::Delicious::ResponseError
|
279
|
+
#
|
280
|
+
def bundles_all()
|
281
|
+
response = request(API_PATH_BUNDLES_ALL)
|
282
|
+
return parse_bundle_collection(response.body)
|
283
|
+
end
|
284
|
+
|
285
|
+
#
|
286
|
+
# Assignes a set of tags to a single bundle,
|
287
|
+
# wipes away previous settings for bundle.
|
288
|
+
#
|
289
|
+
# # create from a bundle
|
290
|
+
# d.bundles_set(WWW::Delicious::Bundle.new('MyBundle'), %w(foo bar))
|
291
|
+
#
|
292
|
+
# # create from a string
|
293
|
+
# d.bundles_set('MyBundle', %w(foo bar))
|
294
|
+
#
|
295
|
+
#
|
296
|
+
# Raises:: WWW::Delicious::Error
|
297
|
+
# Raises:: WWW::Delicious::HTTPError
|
298
|
+
# Raises:: WWW::Delicious::ResponseError
|
299
|
+
#
|
300
|
+
def bundles_set(bundle_or_name, tags = [])
|
301
|
+
params = prepare_bundles_set_params(bundle_or_name, tags)
|
302
|
+
response = request(API_PATH_BUNDLES_SET, params)
|
303
|
+
return parse_and_eval_execution_response(response.body)
|
304
|
+
end
|
305
|
+
|
306
|
+
#
|
307
|
+
# Deletes +bundle_or_name+ bundle from del.icio.us.
|
308
|
+
# +bundle_or_name+ can be either a WWW::Delicious::Bundle instance
|
309
|
+
# or a string with the name of the bundle.
|
310
|
+
#
|
311
|
+
# This method doesn't care whether the exists.
|
312
|
+
# If not, the execution will silently return without rising any error.
|
313
|
+
#
|
314
|
+
# # delete from a bundle
|
315
|
+
# d.bundles_delete(WWW::Delicious::Bundle.new('MyBundle'))
|
316
|
+
#
|
317
|
+
# # delete from a string
|
318
|
+
# d.bundles_delete('MyBundle', %w(foo bar))
|
319
|
+
#
|
320
|
+
#
|
321
|
+
# Raises:: WWW::Delicious::Error
|
322
|
+
# Raises:: WWW::Delicious::HTTPError
|
323
|
+
# Raises:: WWW::Delicious::ResponseError
|
324
|
+
#
|
325
|
+
def bundles_delete(bundle_or_name)
|
326
|
+
params = prepare_bundles_delete_params(bundle_or_name)
|
327
|
+
response = request(API_PATH_BUNDLES_DELETE, params)
|
328
|
+
return parse_and_eval_execution_response(response.body)
|
329
|
+
end
|
330
|
+
|
331
|
+
#
|
332
|
+
# Retrieves the list of tags and number of times used by the user
|
333
|
+
# and returns an array of <tt>WWW::Delicious::Tag</tt>.
|
334
|
+
#
|
335
|
+
# d.tags_get() # => [#<WWW::Delicious::Tag>, #<WWW::Delicious::Tag>, ...]
|
336
|
+
# d.tags_get() # => []
|
337
|
+
#
|
338
|
+
#
|
339
|
+
# Raises:: WWW::Delicious::Error
|
340
|
+
# Raises:: WWW::Delicious::HTTPError
|
341
|
+
# Raises:: WWW::Delicious::ResponseError
|
342
|
+
#
|
343
|
+
def tags_get()
|
344
|
+
response = request(API_PATH_TAGS_GET)
|
345
|
+
return parse_tag_collection(response.body)
|
346
|
+
end
|
347
|
+
|
348
|
+
#
|
349
|
+
# Renames an existing tag with a new tag name.
|
350
|
+
#
|
351
|
+
# # rename from a tag
|
352
|
+
# d.bundles_set(WWW::Delicious::Tag.new('old'), WWW::Delicious::Tag.new('new'))
|
353
|
+
#
|
354
|
+
# # rename from a string
|
355
|
+
# d.bundles_set('old', 'new')
|
356
|
+
#
|
357
|
+
#
|
358
|
+
# Raises:: WWW::Delicious::Error
|
359
|
+
# Raises:: WWW::Delicious::HTTPError
|
360
|
+
# Raises:: WWW::Delicious::ResponseError
|
361
|
+
#
|
362
|
+
def tags_rename(from_name_or_tag, to_name_or_tag)
|
363
|
+
params = prepare_tags_rename_params(from_name_or_tag, to_name_or_tag)
|
364
|
+
response = request(API_PATH_TAGS_RENAME, params)
|
365
|
+
return parse_and_eval_execution_response(response.body)
|
366
|
+
end
|
367
|
+
|
368
|
+
#
|
369
|
+
# Returns an array of <tt>WWW::Delicious::Post</tt> matching +options+.
|
370
|
+
# If no option is given, the last post is returned.
|
371
|
+
# If no date or url is given, most recent date will be used.
|
372
|
+
#
|
373
|
+
# d.posts_get() # => [#<WWW::Delicious::Post>, #<WWW::Delicious::Post>, ...]
|
374
|
+
# d.posts_get() # => []
|
375
|
+
#
|
376
|
+
# # get all posts tagged with ruby
|
377
|
+
# d.posts_get(:tag => WWW::Delicious::Tag.new('ruby))
|
378
|
+
#
|
379
|
+
# # get all posts matching URL 'http://www.simonecarletti.com'
|
380
|
+
# d.posts_get(:url => URI.parse('http://www.simonecarletti.com'))
|
381
|
+
#
|
382
|
+
# # get all posts tagged with ruby and matching URL 'http://www.simonecarletti.com'
|
383
|
+
# d.posts_get(:tag => WWW::Delicious::Tag.new('ruby),
|
384
|
+
# :url => URI.parse('http://www.simonecarletti.com'))
|
385
|
+
#
|
386
|
+
#
|
387
|
+
# === Options
|
388
|
+
# <tt>:tag</tt>:: a tag to filter by. It can be either a <tt>WWW::Delicious::Tag</tt> or a +String+.
|
389
|
+
# <tt>:dt</tt>:: a +Time+ with a date to filter by.
|
390
|
+
# <tt>:url</tt>:: a valid URI to filter by. It can be either an instance of +URI+ or a +String+.
|
391
|
+
#
|
392
|
+
# Raises:: WWW::Delicious::Error
|
393
|
+
# Raises:: WWW::Delicious::HTTPError
|
394
|
+
# Raises:: WWW::Delicious::ResponseError
|
395
|
+
#
|
396
|
+
def posts_get(options = {})
|
397
|
+
params = prepare_posts_params(options.clone, [:dt, :tag, :url])
|
398
|
+
response = request(API_PATH_POSTS_GET, params)
|
399
|
+
return parse_post_collection(response.body)
|
400
|
+
end
|
401
|
+
|
402
|
+
#
|
403
|
+
# Returns a list of the most recent posts, filtered by argument.
|
404
|
+
#
|
405
|
+
# # get the most recent posts
|
406
|
+
# d.posts_recent()
|
407
|
+
#
|
408
|
+
# # get the 10 most recent posts
|
409
|
+
# d.posts_recent(:count => 10)
|
410
|
+
#
|
411
|
+
#
|
412
|
+
# === Options
|
413
|
+
# <tt>:tag</tt>:: a tag to filter by. It can be either a <tt>WWW::Delicious::Tag</tt> or a +String+.
|
414
|
+
# <tt>:count</tt>:: number of items to retrieve. (default: 15, maximum: 100).
|
415
|
+
#
|
416
|
+
def posts_recent(options = {})
|
417
|
+
params = prepare_posts_params(options.clone, [:count, :tag])
|
418
|
+
response = request(API_PATH_POSTS_RECENT, params)
|
419
|
+
return parse_post_collection(response.body)
|
420
|
+
end
|
421
|
+
|
422
|
+
#
|
423
|
+
# Returns a list of all posts, filtered by argument.
|
424
|
+
#
|
425
|
+
# # get all (this is a very expensive query)
|
426
|
+
# d.posts_all
|
427
|
+
#
|
428
|
+
# # get all posts matching ruby
|
429
|
+
# d.posts_all(:tag => WWW::Delicious::Tag.new('ruby'))
|
430
|
+
#
|
431
|
+
#
|
432
|
+
# === Options
|
433
|
+
# <tt>:tag</tt>:: a tag to filter by. It can be either a <tt>WWW::Delicious::Tag</tt> or a +String+.
|
434
|
+
#
|
435
|
+
def posts_all(options = {})
|
436
|
+
params = prepare_posts_params(options.clone, [:tag])
|
437
|
+
response = request(API_PATH_POSTS_ALL, params)
|
438
|
+
return parse_post_collection(response.body)
|
439
|
+
end
|
440
|
+
|
441
|
+
#
|
442
|
+
# Returns a list of dates with the number of posts at each date.
|
443
|
+
#
|
444
|
+
# # get number of posts per date
|
445
|
+
# d.posts_dates
|
446
|
+
# # => { '2008-05-05' => 12, '2008-05-06' => 3, ... }
|
447
|
+
#
|
448
|
+
# # get number posts per date tagged as ruby
|
449
|
+
# d.posts_dates(:tag => WWW::Delicious::Tag.new('ruby'))
|
450
|
+
# # => { '2008-05-05' => 10, '2008-05-06' => 3, ... }
|
451
|
+
#
|
452
|
+
#
|
453
|
+
# === Options
|
454
|
+
# <tt>:tag</tt>:: a tag to filter by. It can be either a <tt>WWW::Delicious::Tag</tt> or a +String+.
|
455
|
+
#
|
456
|
+
def posts_dates(options = {})
|
457
|
+
params = prepare_posts_params(options.clone, [:tag])
|
458
|
+
response = request(API_PATH_POSTS_DATES, params)
|
459
|
+
return parse_posts_dates_response(response.body)
|
460
|
+
end
|
461
|
+
|
462
|
+
#
|
463
|
+
# Add a post to del.icio.us.
|
464
|
+
# +post_or_values+ can be either a +WWW::Delicious::Post+ instance
|
465
|
+
# or a Hash of params. This method accepts all params available
|
466
|
+
# to initialize a new +WWW::Delicious::Post+.
|
467
|
+
#
|
468
|
+
# # add a post from WWW::Delicious::Post
|
469
|
+
# d.posts_add(WWW::Delicious::Post.new(:url => 'http://www.foobar.com', :title => 'Hello world!'))
|
470
|
+
#
|
471
|
+
# # add a post from values
|
472
|
+
# d.posts_add(:url => 'http://www.foobar.com', :title => 'Hello world!')
|
473
|
+
#
|
474
|
+
#
|
475
|
+
def posts_add(post_or_values)
|
476
|
+
params = prepare_param_post(post_or_values).to_params
|
477
|
+
response = request(API_PATH_POSTS_ADD, params)
|
478
|
+
return parse_and_eval_execution_response(response.body)
|
479
|
+
end
|
480
|
+
|
481
|
+
#
|
482
|
+
# Deletes the post matching given +url+ from del.icio.us.
|
483
|
+
# +url+ can be either an URI instance or a string representation of a valid URL.
|
484
|
+
#
|
485
|
+
# This method doesn't care whether a post with given +url+ exists.
|
486
|
+
# If not, the execution will silently return without rising any error.
|
487
|
+
#
|
488
|
+
# # delete a post from URI
|
489
|
+
# d.post_delete(URI.parse('http://www.foobar.com/'))
|
490
|
+
#
|
491
|
+
# # delete a post from a string
|
492
|
+
# d.post_delete('http://www.foobar.com/')
|
493
|
+
#
|
494
|
+
#
|
495
|
+
def posts_delete(url)
|
496
|
+
params = prepare_posts_params({:url => url}, [:url])
|
497
|
+
response = request(API_PATH_POSTS_DELETE, params)
|
498
|
+
return parse_and_eval_execution_response(response.body)
|
499
|
+
end
|
500
|
+
|
501
|
+
|
502
|
+
protected
|
503
|
+
|
504
|
+
# Initializes the HTTP client.
|
505
|
+
# It automatically enable +use_ssl+ flag according to +@base_uri+ scheme.
|
506
|
+
def init_http_client(options)
|
507
|
+
http = Net::HTTP.new(@base_uri.host, 443)
|
508
|
+
http.use_ssl = true if @base_uri.scheme == "https"
|
509
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE # FIXME: not 100% supported
|
510
|
+
self.http_client = http
|
511
|
+
end
|
512
|
+
|
513
|
+
# Initializes user agent value for HTTP requests.
|
514
|
+
def init_user_agent(options)
|
515
|
+
user_agent = options[:user_agent] || default_user_agent()
|
516
|
+
@headers ||= {}
|
517
|
+
@headers['User-Agent'] = user_agent
|
518
|
+
end
|
519
|
+
|
520
|
+
#
|
521
|
+
# Creates and returns the default user agent string.
|
522
|
+
#
|
523
|
+
# By default, the user agent is composed by the following schema:
|
524
|
+
# <tt>NAME/VERSION (Ruby/RUBY_VERSION)</tt>
|
525
|
+
#
|
526
|
+
# * +NAME+ is the constant representing this library name
|
527
|
+
# * +VERSION+ is the constant representing current library version
|
528
|
+
# * +RUBY_VERSION+ is the version of Ruby interpreter the library is interpreted by
|
529
|
+
#
|
530
|
+
# default_user_agent
|
531
|
+
# # => WWW::Delicious/0.1.0 (Ruby/1.8.6)
|
532
|
+
#
|
533
|
+
def default_user_agent
|
534
|
+
return "#{NAME}/#{VERSION} (Ruby/#{RUBY_VERSION})"
|
535
|
+
end
|
536
|
+
|
537
|
+
|
538
|
+
#
|
539
|
+
# Composes an HTTP query string from an hash of +options+.
|
540
|
+
# The result is URI encoded.
|
541
|
+
#
|
542
|
+
# http_build_query(:foo => 'baa', :bar => 'boo')
|
543
|
+
# # => foo=baa&bar=boo
|
544
|
+
#
|
545
|
+
def http_build_query(params = {})
|
546
|
+
return params.collect do |k,v|
|
547
|
+
"#{URI.encode(k.to_s)}=#{URI.encode(v.to_s)}" unless v.nil?
|
548
|
+
end.compact.join('&')
|
549
|
+
end
|
550
|
+
|
551
|
+
#
|
552
|
+
# Sends an HTTP GET request to +path+ and appends given +params+.
|
553
|
+
#
|
554
|
+
# This method is 100% compliant with Delicious API reference.
|
555
|
+
# It waits at least 1 second between each HTTP request and
|
556
|
+
# provides an identifiable user agent by default,
|
557
|
+
# or the custom user agent set by +user_agent+ option
|
558
|
+
# when this istance has been created.
|
559
|
+
#
|
560
|
+
# request('/v1/api/path', :foo => 1, :bar => 2)
|
561
|
+
# # => sends a GET request to /v1/api/path?foo=1&bar=2
|
562
|
+
#
|
563
|
+
def request(path, params = {})
|
564
|
+
raise Error, 'Invalid HTTP Client' unless http_client
|
565
|
+
wait_before_new_request
|
566
|
+
|
567
|
+
uri = @base_uri.merge(path)
|
568
|
+
uri.query = http_build_query(params) unless params.empty?
|
569
|
+
|
570
|
+
begin
|
571
|
+
@last_request = Time.now # see #wait_before_new_request
|
572
|
+
@last_request_uri = uri # useful for debug
|
573
|
+
response = make_request(uri)
|
574
|
+
rescue => e # catch EOFError, SocketError and more
|
575
|
+
raise HTTPError, e.message
|
576
|
+
end
|
577
|
+
|
578
|
+
case response
|
579
|
+
when Net::HTTPSuccess
|
580
|
+
return response
|
581
|
+
when Net::HTTPUnauthorized # 401
|
582
|
+
raise HTTPError, 'Invalid username or password'
|
583
|
+
when Net::HTTPServiceUnavailable # 503
|
584
|
+
raise HTTPError, 'You have been throttled.' +
|
585
|
+
'Please ensure you are waiting at least one second before each request.'
|
586
|
+
else
|
587
|
+
raise HTTPError, "HTTP #{response.code}: #{response.message}"
|
588
|
+
end
|
589
|
+
end
|
590
|
+
|
591
|
+
# Makes the real HTTP request to given +uri+ and returns the +response+.
|
592
|
+
# This method exists basically to simplify unit testing with mocha.
|
593
|
+
def make_request(uri)
|
594
|
+
http_client.start do |http|
|
595
|
+
req = Net::HTTP::Get.new(uri.request_uri, @headers)
|
596
|
+
req.basic_auth(@username, @password)
|
597
|
+
http.request(req)
|
598
|
+
end
|
599
|
+
end
|
600
|
+
|
601
|
+
#
|
602
|
+
# Delicious API reference requests to wait AT LEAST ONE SECOND
|
603
|
+
# between queries or the client is likely to get automatically throttled.
|
604
|
+
#
|
605
|
+
# This method calculates the difference between current time
|
606
|
+
# and the last request time and wait for the necessary time to meet
|
607
|
+
# SECONDS_BEFORE_NEW_REQUEST requirement.
|
608
|
+
#
|
609
|
+
# The difference is not rounded. If you only have to wait for 0.034 seconds
|
610
|
+
# then your don't have to wait 0 or 1 seconds, but 0.034 seconds!
|
611
|
+
#
|
612
|
+
def wait_before_new_request
|
613
|
+
return unless @last_request # this is the first request
|
614
|
+
# puts "Last request at #{TIME_CONVERTER.call(@last_request)}" if debug?
|
615
|
+
diff = Time.now - @last_request
|
616
|
+
if diff < SECONDS_BEFORE_NEW_REQUEST
|
617
|
+
# puts "Sleeping for #{diff} before new request..." if debug?
|
618
|
+
sleep(SECONDS_BEFORE_NEW_REQUEST - diff)
|
619
|
+
end
|
620
|
+
end
|
621
|
+
|
622
|
+
|
623
|
+
#
|
624
|
+
# Parses the response <tt>body</tt> and runs a common set of validators.
|
625
|
+
# Returns <tt>body</tt> as parsed REXML::Document on success.
|
626
|
+
#
|
627
|
+
# Raises:: WWW::Delicious::ResponseError in case of invalid response.
|
628
|
+
#
|
629
|
+
def parse_and_validate_response(body, options = {})
|
630
|
+
dom = REXML::Document.new(body)
|
631
|
+
|
632
|
+
if (value = options[:root_name]) && dom.root.name != value
|
633
|
+
raise ResponseError, "Invalid response, root node is not `#{value}`"
|
634
|
+
end
|
635
|
+
if (value = options[:root_text]) && dom.root.text != value
|
636
|
+
raise ResponseError, value
|
637
|
+
end
|
638
|
+
|
639
|
+
return dom
|
640
|
+
end
|
641
|
+
|
642
|
+
#
|
643
|
+
# Parses and evaluates the response returned by an execution,
|
644
|
+
# usually an update/delete/insert operation.
|
645
|
+
#
|
646
|
+
# Raises:: WWW::Delicious::ResponseError in case of invalid response
|
647
|
+
# Raises:: WWW::Delicious::Error in case of execution error
|
648
|
+
#
|
649
|
+
def parse_and_eval_execution_response(body)
|
650
|
+
dom = parse_and_validate_response(body, :root_name => 'result')
|
651
|
+
response = dom.root.if_attribute_value(:code)
|
652
|
+
response = dom.root.text if response.nil?
|
653
|
+
raise Error, "Invalid response, #{response}" unless %w(done ok).include?(response)
|
654
|
+
true
|
655
|
+
end
|
656
|
+
|
657
|
+
# Parses the response of an Update request
|
658
|
+
# and returns the update Timestamp.
|
659
|
+
def parse_update_response(body)
|
660
|
+
dom = parse_and_validate_response(body, :root_name => 'update')
|
661
|
+
dom.root.if_attribute_value(:time) { |v| Time.parse(v) }
|
662
|
+
end
|
663
|
+
|
664
|
+
# Parses a response containing a collection of Bundles
|
665
|
+
# and returns an array of <tt>WWW::Delicious::Bundle</tt>.
|
666
|
+
def parse_bundle_collection(body)
|
667
|
+
dom = parse_and_validate_response(body, :root_name => 'bundles')
|
668
|
+
dom.root.elements.collect('bundle') { |xml| Bundle.from_rexml(xml) }
|
669
|
+
end
|
670
|
+
|
671
|
+
# Parses a response containing a collection of Tags
|
672
|
+
# and returns an array of <tt>WWW::Delicious::Tag</tt>.
|
673
|
+
def parse_tag_collection(body)
|
674
|
+
dom = parse_and_validate_response(body, :root_name => 'tags')
|
675
|
+
dom.root.elements.collect('tag') { |xml| Tag.from_rexml(xml) }
|
676
|
+
end
|
677
|
+
|
678
|
+
# Parses a response containing a collection of Posts
|
679
|
+
# and returns an array of <tt>WWW::Delicious::Post</tt>.
|
680
|
+
def parse_post_collection(body)
|
681
|
+
dom = parse_and_validate_response(body, :root_name => 'posts')
|
682
|
+
dom.root.elements.collect('post') { |xml| Post.from_rexml(xml) }
|
683
|
+
end
|
684
|
+
|
685
|
+
# Parses the response of a <tt>posts_dates</tt> request
|
686
|
+
# and returns a +Hash+ of date => count.
|
687
|
+
def parse_posts_dates_response(body)
|
688
|
+
dom = parse_and_validate_response(body, :root_name => 'dates')
|
689
|
+
return dom.root.get_elements('date').inject({}) do |collection, xml|
|
690
|
+
date = xml.if_attribute_value(:date)
|
691
|
+
count = xml.if_attribute_value(:count)
|
692
|
+
collection.merge({ date => count })
|
693
|
+
end
|
694
|
+
end
|
695
|
+
|
696
|
+
|
697
|
+
#
|
698
|
+
# Prepares the params for a `bundles_set` call
|
699
|
+
# and returns a Hash with the params ready for the HTTP request.
|
700
|
+
#
|
701
|
+
# Raises:: WWW::Delicious::Error
|
702
|
+
#
|
703
|
+
def prepare_bundles_set_params(name_or_bundle, tags = [])
|
704
|
+
bundle = prepare_param_bundle(name_or_bundle, tags) do |b|
|
705
|
+
raise Error, "Bundle name is empty" if b.name.empty?
|
706
|
+
raise Error, "Bundle must contain at least one tag" if b.tags.empty?
|
707
|
+
end
|
708
|
+
return { :bundle => bundle.name, :tags => bundle.tags.join(' ') }
|
709
|
+
end
|
710
|
+
|
711
|
+
#
|
712
|
+
# Prepares the params for a `bundles_set` call
|
713
|
+
# and returns a Hash with the params ready for the HTTP request.
|
714
|
+
#
|
715
|
+
# Raises:: WWW::Delicious::Error
|
716
|
+
#
|
717
|
+
def prepare_bundles_delete_params(name_or_bundle)
|
718
|
+
bundle = prepare_param_bundle(name_or_bundle) do |b|
|
719
|
+
raise Error, "Bundle name is empty" if b.name.empty?
|
720
|
+
end
|
721
|
+
return { :bundle => bundle.name }
|
722
|
+
end
|
723
|
+
|
724
|
+
#
|
725
|
+
# Prepares the params for a `tags_rename` call
|
726
|
+
# and returns a Hash with the params ready for the HTTP request.
|
727
|
+
#
|
728
|
+
# Raises:: WWW::Delicious::Error
|
729
|
+
#
|
730
|
+
def prepare_tags_rename_params(from_name_or_tag, to_name_or_tag)
|
731
|
+
from, to = [from_name_or_tag, to_name_or_tag].collect do |v|
|
732
|
+
prepare_param_tag(v)
|
733
|
+
end
|
734
|
+
return { :old => from, :new => to }
|
735
|
+
end
|
736
|
+
|
737
|
+
#
|
738
|
+
# Prepares the params for a `post_*` call
|
739
|
+
# and returns a Hash with the params ready for the HTTP request.
|
740
|
+
#
|
741
|
+
# Raises:: WWW::Delicious::Error
|
742
|
+
#
|
743
|
+
def prepare_posts_params(params, allowed_params = [])
|
744
|
+
compare_params(params, allowed_params)
|
745
|
+
|
746
|
+
# we don't need to check whether the following parameters
|
747
|
+
# are valid for this request because compare_params
|
748
|
+
# would raise if an invalid param is supplied
|
749
|
+
|
750
|
+
params[:tag] = prepare_param_tag(params[:tag]) if params[:tag]
|
751
|
+
params[:dt] = TIME_CONVERTER.call(params[:dt]) if params[:dt]
|
752
|
+
params[:url] = URI.parse(params[:url]) if params[:url]
|
753
|
+
params[:count] = if value = params[:count]
|
754
|
+
raise Error, 'Expected `count` <= 100' if value.to_i() > 100 # requirement
|
755
|
+
value.to_i
|
756
|
+
else
|
757
|
+
15 # default value
|
758
|
+
end
|
759
|
+
|
760
|
+
return params
|
761
|
+
end
|
762
|
+
|
763
|
+
|
764
|
+
#
|
765
|
+
# Prepares the +post+ param for an API request.
|
766
|
+
#
|
767
|
+
# Creates and returns a <tt>WWW::Delicious::Post</tt> instance from <tt>post_or_values</tt>.
|
768
|
+
# <tt>post_or_values</tt> can be either an Hash with post attributes
|
769
|
+
# or a <tt>WWW::Delicious::Post</tt> instance.
|
770
|
+
#
|
771
|
+
def prepare_param_post(post_or_values, &block)
|
772
|
+
post = case post_or_values
|
773
|
+
when WWW::Delicious::Post
|
774
|
+
post_or_values
|
775
|
+
when Hash
|
776
|
+
Post.new(post_or_values)
|
777
|
+
else
|
778
|
+
raise ArgumentError, 'Expected `args` to be `WWW::Delicious::Post` or `Hash`'
|
779
|
+
end
|
780
|
+
|
781
|
+
yield(post) if block_given?
|
782
|
+
# TODO: validate post with post.validate!
|
783
|
+
raise ArgumentError, 'Both `url` and `title` are required' unless post.api_valid?
|
784
|
+
post
|
785
|
+
end
|
786
|
+
|
787
|
+
#
|
788
|
+
# Prepares the +bundle+ param for an API request.
|
789
|
+
#
|
790
|
+
# Creates and returns a <tt>WWW::Delicious::Bundle</tt> instance from <tt>name_or_bundle</tt>.
|
791
|
+
# <tt>name_or_bundle</tt> can be either a string holding bundle name
|
792
|
+
# or a <tt>WWW::Delicious::Bundle</tt> instance.
|
793
|
+
#
|
794
|
+
def prepare_param_bundle(name_or_bundle, tags = [], &block) # :yields: bundle
|
795
|
+
bundle = case name_or_bundle
|
796
|
+
when WWW::Delicious::Bundle
|
797
|
+
name_or_bundle
|
798
|
+
else
|
799
|
+
Bundle.new(:name => name_or_bundle, :tags => tags)
|
800
|
+
end
|
801
|
+
|
802
|
+
yield(bundle) if block_given?
|
803
|
+
# TODO: validate bundle with bundle.validate!
|
804
|
+
bundle
|
805
|
+
end
|
806
|
+
|
807
|
+
#
|
808
|
+
# Prepares the +tag+ param for an API request.
|
809
|
+
#
|
810
|
+
# Creates and returns a <tt>WWW::Delicious::Tag</tt> instance from <tt>name_or_tag</tt>.
|
811
|
+
# <tt>name_or_tag</tt> can be either a string holding tag name
|
812
|
+
# or a <tt>WWW::Delicious::Tag</tt> instance.
|
813
|
+
#
|
814
|
+
def prepare_param_tag(name_or_tag, &block) # :yields: tag
|
815
|
+
tag = case name_or_tag
|
816
|
+
when WWW::Delicious::Tag
|
817
|
+
name_or_tag
|
818
|
+
else
|
819
|
+
Tag.new(:name => name_or_tag.to_s)
|
820
|
+
end
|
821
|
+
|
822
|
+
yield(tag) if block_given?
|
823
|
+
# TODO: validate tag with tag.validate!
|
824
|
+
raise "Invalid `tag` value supplied" unless tag.api_valid?
|
825
|
+
tag
|
826
|
+
end
|
827
|
+
|
828
|
+
#
|
829
|
+
# Checks whether user given +params+ are valid against a defined collection of +valid_params+.
|
830
|
+
#
|
831
|
+
# === Examples
|
832
|
+
#
|
833
|
+
# params = {:foo => 1, :bar => 2}
|
834
|
+
#
|
835
|
+
# compare_params(params, [:foo, :bar])
|
836
|
+
# # => valid
|
837
|
+
#
|
838
|
+
# compare_params(params, [:foo, :bar, :baz])
|
839
|
+
# # => raises
|
840
|
+
#
|
841
|
+
# compare_params(params, [:foo])
|
842
|
+
# # => raises
|
843
|
+
#
|
844
|
+
# Raises:: WWW::Delicious::Error
|
845
|
+
#
|
846
|
+
def compare_params(params, valid_params)
|
847
|
+
raise ArgumentError, "Expected `params` to be a kind of `Hash`" unless params.kind_of?(Hash)
|
848
|
+
raise ArgumentError, "Expected `valid_params` to be a kind of `Array`" unless valid_params.kind_of?(Array)
|
849
|
+
|
850
|
+
# compute options difference
|
851
|
+
difference = params.keys - valid_params
|
852
|
+
raise Error, "Invalid params: `#{difference.join('`, `')}`" unless difference.empty?
|
853
|
+
end
|
854
|
+
|
855
|
+
|
856
|
+
module XMLUtils #:nodoc:
|
857
|
+
|
858
|
+
#
|
859
|
+
# Returns the +xmlattr+ attribute value for current <tt>REXML::Element</tt>.
|
860
|
+
#
|
861
|
+
# If block is given and attribute value is not nil,
|
862
|
+
# the content of the block is executed.
|
863
|
+
#
|
864
|
+
# === Examples
|
865
|
+
#
|
866
|
+
# dom = REXML::Document.new('<a name="1"><b>foo</b><b>bar</b></a>')
|
867
|
+
#
|
868
|
+
# dom.root.if_attribute_value(:name)
|
869
|
+
# # => "1"
|
870
|
+
#
|
871
|
+
# dom.root.if_attribute_value(:name) { |v| v.to_i }
|
872
|
+
# # => 1
|
873
|
+
#
|
874
|
+
# dom.root.if_attribute_value(:foo)
|
875
|
+
# # => nil
|
876
|
+
#
|
877
|
+
# dom.root.if_attribute_value(:name) { |v| v.to_i }
|
878
|
+
# # => nil
|
879
|
+
#
|
880
|
+
def if_attribute_value(xmlattr, &block) #:nodoc:
|
881
|
+
value = if attr = self.attribute(xmlattr.to_s)
|
882
|
+
attr.value
|
883
|
+
else
|
884
|
+
nil
|
885
|
+
end
|
886
|
+
value = yield value if !value.nil? and block_given?
|
887
|
+
value
|
888
|
+
end
|
889
|
+
|
890
|
+
#
|
891
|
+
# Returns the value of +expression+ child of this element, if it exists.
|
892
|
+
# If blog is given, block is called on +expression+ element value
|
893
|
+
# and the result is returned.
|
894
|
+
#
|
895
|
+
def if_element_value(expression, &block)
|
896
|
+
if_element(expression) do |element|
|
897
|
+
value = element.text
|
898
|
+
value = yield value if block_given?
|
899
|
+
value
|
900
|
+
end
|
901
|
+
end
|
902
|
+
|
903
|
+
#
|
904
|
+
# Executes the content of +block+ on +expression+
|
905
|
+
# child of this element, if it exists.
|
906
|
+
# Returns the result or +nil+ if +xmlelement+ doesn't exist.
|
907
|
+
#
|
908
|
+
def if_element(expression, &block)
|
909
|
+
raise LocalJumpError, "no block given" unless block_given?
|
910
|
+
if element = self.elements[expression.to_s]
|
911
|
+
yield element
|
912
|
+
else
|
913
|
+
nil
|
914
|
+
end
|
915
|
+
end
|
916
|
+
|
917
|
+
end # XMLUtils
|
918
|
+
|
919
|
+
end
|
920
|
+
end
|
921
|
+
|
922
|
+
|
923
|
+
class Object
|
924
|
+
|
925
|
+
# An object is blank if it's false, empty, or a whitespace string.
|
926
|
+
# For example, "", " ", +nil+, [], and {} are blank.
|
927
|
+
#
|
928
|
+
# This simplifies
|
929
|
+
#
|
930
|
+
# if !address.nil? && !address.empty?
|
931
|
+
#
|
932
|
+
# to
|
933
|
+
#
|
934
|
+
# if !address.blank?
|
935
|
+
#
|
936
|
+
# Object#blank? comes from the GEM ActiveSupport 2.1.
|
937
|
+
#
|
938
|
+
def blank?
|
939
|
+
respond_to?(:empty?) ? empty? : !self
|
940
|
+
end unless Object.method_defined? :blanks?
|
941
|
+
|
942
|
+
end
|
943
|
+
|
944
|
+
|
945
|
+
module REXML # :nodoc:
|
946
|
+
class Element < Parent # :nodoc:
|
947
|
+
include WWW::Delicious::XMLUtils
|
948
|
+
end
|
949
|
+
end
|