www-delicious 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,5 @@
1
+ = Changelog
2
+
3
+ == Release 0.1.0 (2008-05-11)
4
+
5
+ * Initial public release.
@@ -0,0 +1,21 @@
1
+ Copyright (c) 2008 Simone Carletti <weppos@weppos.net>
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+
data/README ADDED
@@ -0,0 +1,210 @@
1
+ = WWW::Delicious
2
+
3
+ WWW::Delicious is a Ruby client for http://del.icio.us XML API.
4
+
5
+ It provides both read and write functionalities. You can read user Posts, Tags
6
+ and Bundles but you can create new Posts, Tags and Bundles as well.
7
+
8
+
9
+ == Overview
10
+
11
+ WWW::Delicious maps all the original del.icio.us API calls and provides some
12
+ additional convenient methods to perform common tasks.
13
+ Please read the official documentation (http://del.icio.us/help/api/)
14
+ to learn more about del.icio.us API.
15
+
16
+ WWW::Delicious is 100% compatible with all del.icio.us API constraints,
17
+ including the requirement to set a valid user agent or wait at least
18
+ one second between queries.
19
+ Basically, the main benefit from using this library is that you don't need
20
+ to take care of all these low level details, if you don't want:
21
+ WWW::Delicious will try to give you the most with less efforts.
22
+
23
+
24
+ == Dependencies
25
+
26
+ * Ruby 1.8.6
27
+
28
+
29
+ == Source
30
+
31
+ WWW::Delicious source code is managed via GIT and hosted at GitHub: http://github.com/weppos/www-delicious/.
32
+
33
+
34
+ == Download and Installation
35
+
36
+ Installing WWW::Delicious as a GEM is probably the best and easiest way.
37
+ You must have RubyGems[http://rubyforge.org/projects/rubygems/] installed
38
+ for the following instruction to work:
39
+
40
+ $ sudo gem install www-delicious
41
+
42
+ To install the library manually grab the source code from the website,
43
+ navigate to the root library directory and enter:
44
+
45
+ $ sudo ruby setup.rb
46
+
47
+ If you need the latest development version you can download the source code
48
+ from the GIT repositories listed above.
49
+ Beware that the code might not as stable as the official release.
50
+
51
+
52
+ == Usage
53
+
54
+ In order to use this library you need a valid del.icio.us account.
55
+ Go to http://del.icio.us/ and register for a new account if you don't
56
+ already have one.
57
+
58
+ Then create a valid instance of WWW::Delicious with the account credentials.
59
+
60
+ require 'www/delicious'
61
+
62
+ # create a new instance with given username and password
63
+ d = WWW::Delicious.new('username', 'password')
64
+
65
+ Now you can use your delicious instance to call on of the API methods available.
66
+
67
+
68
+ === Last account update
69
+
70
+ The following example show you how to get the last account update Time.
71
+
72
+ require 'www/delicious'
73
+ d = WWW::Delicious.new('username', 'password')
74
+
75
+ time = d.update # => Fri May 02 18:02:48 UTC 2008
76
+
77
+
78
+ === Reading Posts
79
+
80
+ You can fetch your posts in 3 different ways:
81
+
82
+ require 'www/delicious'
83
+ d = WWW::Delicious.new('username', 'password')
84
+
85
+ # 1. get all posts
86
+ posts = d.posts_all
87
+
88
+ # 2. get recent posts
89
+ posts = d.posts_recent
90
+
91
+ # 3. get a single post (the latest one if no criteria is given)
92
+ posts = d.posts_get(:tag => 'ruby')
93
+
94
+ Each post call accepts some options to refine your search.
95
+ For example, you can always search for posts matching a specific tag.
96
+
97
+ posts = d.posts_all(:tag => 'ruby')
98
+ posts = d.posts_recent(:tag => 'ruby')
99
+ posts = d.posts_get(:tag => 'ruby')
100
+
101
+
102
+ === Creating a new Post
103
+
104
+ require 'www/delicious'
105
+ d = WWW::Delicious.new('username', 'password')
106
+
107
+ # add a post from options
108
+ d.posts_add(:url => 'http://www.simonecarletti.com/', :title => 'Cool site!')
109
+
110
+ # add a post from WWW::Delicious::Post
111
+ d.posts_add(WWW::Delicious::Post.new(:url => 'http://www.simonecarletti.com/', :title => 'Cool site!'))
112
+
113
+
114
+ === Deleting a Posts
115
+
116
+ require 'www/delicious'
117
+ d = WWW::Delicious.new('username', 'password')
118
+
119
+ # delete given post (the URL can be either a string or an URI)
120
+ d.posts_delete('http://www.foobar.com/')
121
+
122
+ Note. Actually you cannot delete a post from a WWW::Delicious::Post instance.
123
+ It means, the following example doesn't work as some ActiveRecord user might expect.
124
+
125
+ post = WWW::Delicious::Post.new(:url => 'http://www.foobar.com/')
126
+ post.delete
127
+
128
+ This feature is already in the TODO list. For now, use the following workaround
129
+ to delete a given Post.
130
+
131
+ # delete a post from an existing post = WWW::Delicious::Post
132
+ d.posts_delete(post.url)
133
+
134
+
135
+ === Tags
136
+
137
+ Working with tags it's really easy. You can get all your tags or rename an existing tag.
138
+
139
+ require 'www/delicious'
140
+ d = WWW::Delicious.new('username', 'password')
141
+
142
+ # get all tags
143
+ tags = d.tags_get
144
+
145
+ # print all tag names
146
+ tags.each { |t| puts t.name }
147
+
148
+ # rename the tag gems to gem
149
+ d.tags_rename('gems', 'gem')
150
+
151
+
152
+ === Bundles
153
+
154
+ WWW::Delicious enables you to get all bundles from given account.
155
+
156
+ require 'www/delicious'
157
+ d = WWW::Delicious.new('username', 'password')
158
+
159
+ # get all bundles
160
+ bundles = d.bundles_all
161
+
162
+ # print all bundle names
163
+ bundles.each { |b| puts b.name }
164
+
165
+ You can also create new bundles or delete existing ones.
166
+
167
+ require 'www/delicious'
168
+ d = WWW::Delicious.new('username', 'password')
169
+
170
+ # set a new bundle for tags ruby, rails and gem
171
+ d.bundles_set('MyBundle', %w(ruby rails gem))
172
+
173
+ # delete the old bundle
174
+ d.bundles_delete('OldBundle')
175
+
176
+
177
+ == Documentation
178
+
179
+ Visit the website[http://code.simonecarletti.com/] for the full documentation
180
+ and more examples.
181
+
182
+
183
+ == Website and Project Home
184
+
185
+ * {Project Homepage}[http://code.simonecarletti.com/]
186
+ * {At GitHub}[http://github.com/weppos/www-delicious/]
187
+ * {At RubyForge}[http://rubyforge.org/projects/www-delicious/]
188
+
189
+
190
+ == FeedBack and Bug reports
191
+
192
+ Feel free to email {Simone Carletti}[mailto:weppos@weppos.net]
193
+ with any questions or feedback.
194
+
195
+ Please submit your bug reports to the Redmine installation for WWW::Delicious
196
+ available at http://code.simonecarletti.com/.
197
+
198
+
199
+ == TODO
200
+
201
+ * allow Tags and Bundles to be passed as params for API calls
202
+ * allow Tags, Bundles and Posts to be used for API call (no longer readonly)
203
+ * improve the way new posts are created and updated
204
+ * more (see issue tracker on the website)
205
+
206
+
207
+ == Changelog
208
+
209
+ See CHANGELOG file.
210
+
@@ -0,0 +1,137 @@
1
+ require 'rubygems'
2
+ require 'rake/gempackagetask'
3
+ require 'rake/testtask'
4
+ require 'rake/rdoctask'
5
+
6
+ $LOAD_PATH.unshift(File.dirname(__FILE__) + "/lib")
7
+ require 'www/delicious'
8
+
9
+
10
+ # Common package properties
11
+ PKG_NAME = ENV['PKG_NAME'] || WWW::Delicious::GEM
12
+ PKG_VERSION = ENV['PKG_VERSION'] || WWW::Delicious::VERSION
13
+ PKG_SUMMARY = "Ruby client for del.icio.us API."
14
+ PKG_FILES = FileList.new("{lib,test}/**/*.rb") do |fl|
15
+ fl.exclude 'TODO'
16
+ fl.include %w(README CHANGELOG MIT-LICENSE)
17
+ fl.include %w(Rakefile setup.rb)
18
+ end
19
+ RUBYFORGE_PROJECT = 'www-delicious'
20
+
21
+
22
+ #
23
+ # task::
24
+ # :test
25
+ # desc::
26
+ # Run all the tests.
27
+ #
28
+ desc "Run all the tests"
29
+ Rake::TestTask.new(:test) do |t|
30
+ t.test_files = FileList["test/unit/*.rb"]
31
+ t.verbose = true
32
+ end
33
+
34
+
35
+ #
36
+ # task::
37
+ # :rcov
38
+ # desc::
39
+ # Create code coverage report.
40
+ #
41
+ begin
42
+ require 'rcov/rcovtask'
43
+
44
+ desc "Create code coverage report"
45
+ Rcov::RcovTask.new(:rcov) do |t|
46
+ t.rcov_opts = ["-xRakefile"]
47
+ t.test_files = FileList["test/unit/*.rb"]
48
+ t.output_dir = "coverage"
49
+ t.verbose = true
50
+ end
51
+ rescue LoadError
52
+ puts "RCov is not available"
53
+ end
54
+
55
+
56
+ #
57
+ # task::
58
+ # :rdoc
59
+ # desc::
60
+ # Generate RDoc documentation.
61
+ #
62
+ desc "Generate RDoc documentation"
63
+ Rake::RDocTask.new(:rdoc) do |rdoc|
64
+ rdoc.rdoc_dir = 'doc'
65
+ rdoc.title = "#{PKG_NAME} -- #{PKG_SUMMARY}"
66
+ rdoc.main = "README"
67
+ rdoc.options << "--inline-source" << "--line-numbers"
68
+ rdoc.options << '--charset' << 'utf-8'
69
+ rdoc.rdoc_files.include("README", "CHANGELOG", "MIT-LICENSE")
70
+ rdoc.rdoc_files.include("lib/**/*.rb")
71
+ end
72
+
73
+
74
+ unless defined?(Gem)
75
+ puts "Package Target requires RubyGEMs"
76
+ else
77
+
78
+ # Package requirements
79
+ GEM_SPEC = Gem::Specification.new do |s|
80
+
81
+ s.name = PKG_NAME
82
+ s.version = PKG_VERSION
83
+ s.summary = PKG_SUMMARY
84
+ s.description = <<-EOF
85
+ WWW::Delicious is a del.icio.us API client implemented in Ruby. \
86
+ It provides access to all available del.icio.us API queries \
87
+ and returns the original XML response as a friendly Ruby object.
88
+ EOF
89
+ s.platform = Gem::Platform::RUBY
90
+ s.rubyforge_project = RUBYFORGE_PROJECT
91
+
92
+ s.required_ruby_version = '>= 1.8.6'
93
+ s.add_dependency('rake', '>= 0.7.3')
94
+
95
+ s.files = PKG_FILES.to_a()
96
+
97
+ s.has_rdoc = true
98
+ s.rdoc_options << "--title" << "#{s.name} -- #{s.summary}"
99
+ s.rdoc_options << "--inline-source" << "--line-numbers"
100
+ s.rdoc_options << "--main" << "README"
101
+ s.rdoc_options << '--charset' << 'utf-8'
102
+ s.extra_rdoc_files = %w(README CHANGELOG MIT-LICENSE)
103
+
104
+ s.test_files = FileList["test/unit/*.rb"]
105
+
106
+ s.author = "Simone Carletti"
107
+ s.email = "weppos@weppos.net"
108
+ s.homepage = "http://code.simonecarletti.com/www-delicious"
109
+
110
+ end
111
+
112
+ #
113
+ # task::
114
+ # :gem
115
+ # desc::
116
+ # Generate the GEM package and all stuff.
117
+ #
118
+ Rake::GemPackageTask.new(GEM_SPEC) do |p|
119
+ p.gem_spec = GEM_SPEC
120
+ p.need_tar = true
121
+ p.need_zip = true
122
+ end
123
+ end
124
+
125
+
126
+ #
127
+ # task::
128
+ # :clean
129
+ # desc::
130
+ # Clean up generated directories and files.
131
+ #
132
+ desc "Clean up generated directories and files"
133
+ task :clean do
134
+ rm_rf "pkg"
135
+ rm_rf "doc"
136
+ rm_rf "coverage"
137
+ end
@@ -0,0 +1,868 @@
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
+ #
11
+ #--
12
+ # SVN: $Id$
13
+ #++
14
+
15
+
16
+ require 'net/https'
17
+ require 'rexml/document'
18
+ require 'time'
19
+ require File.dirname(__FILE__) + '/delicious/bundle'
20
+ require File.dirname(__FILE__) + '/delicious/post'
21
+ require File.dirname(__FILE__) + '/delicious/tag'
22
+ require File.dirname(__FILE__) + '/delicious/errors'
23
+ require File.dirname(__FILE__) + '/delicious/version'
24
+
25
+
26
+ module WWW #:nodoc:
27
+
28
+
29
+ #
30
+ # = WWW::Delicious
31
+ #
32
+ # WWW::Delicious is a Ruby client for http://del.icio.us XML API.
33
+ #
34
+ # It provides both read and write functionalities.
35
+ # You can read user Posts, Tags and Bundles
36
+ # but you can create new Posts, Tags and Bundles as well.
37
+ #
38
+ #
39
+ # == Basic Usage
40
+ #
41
+ # The following is just a basic demonstration of the main features.
42
+ # See the README file for a deeper explanation about how to get the best
43
+ # from WWW::Delicious library.
44
+ #
45
+ # The examples in this page make the following assumptions
46
+ # * you have a valid del.icio.us account
47
+ # * +username+ is your account username
48
+ # * +password+ is your account password
49
+ #
50
+ # In order to make a query you first need to create
51
+ # a new WWW::Delicious instance as follows:
52
+ #
53
+ # require 'www/delicious'
54
+ #
55
+ # username = 'my delicious username'
56
+ # password = 'my delicious password'
57
+ #
58
+ # d = WWW::Delicious.new(username, password)
59
+ #
60
+ # The constructor accepts some additional options.
61
+ # For instance, if you want to customize the user agent:
62
+ #
63
+ # d = WWW::Delicious.new(username, password, :user_agent => 'FooAgent')
64
+ #
65
+ # Now you can use any of the API methods available.
66
+ #
67
+ # For example, you may want to know when your account was last updated
68
+ # to check whether someone else made some changes on behalf of you:
69
+ #
70
+ # datetime = d.update # => Wed Mar 12 08:41:20 UTC 2008
71
+ #
72
+ # Because the answer is a valid +Time+ instance, you can format it with +strftime+.
73
+ #
74
+ # datetime = d.update # => Wed Mar 12 08:41:20 UTC 2008
75
+ # datetime.strftime('%Y') # => 2008
76
+ #
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
+
99
+ # API Base URL
100
+ API_BASE_URI = 'https://api.del.icio.us'
101
+
102
+ # API Path Update
103
+ API_PATH_UPDATE = '/v1/posts/update';
104
+
105
+ # API Path All Bundles
106
+ API_PATH_BUNDLES_ALL = '/v1/tags/bundles/all';
107
+ # API Path Set Bundle
108
+ API_PATH_BUNDLES_SET = '/v1/tags/bundles/set';
109
+ # API Path Delete Bundle
110
+ API_PATH_BUNDLES_DELETE = '/v1/tags/bundles/delete';
111
+
112
+ # API Path Get Tags
113
+ API_PATH_TAGS_GET = '/v1/tags/get';
114
+ # API Path Rename Tag
115
+ API_PATH_TAGS_RENAME = '/v1/tags/rename';
116
+
117
+ # API Path Get Posts
118
+ API_PATH_POSTS_GET = '/v1/posts/get';
119
+ # API Path Recent Posts
120
+ API_PATH_POSTS_RECENT = '/v1/posts/recent';
121
+ # API Path All Posts
122
+ API_PATH_POSTS_ALL = '/v1/posts/all';
123
+ # API Path Posts by Dates
124
+ API_PATH_POSTS_DATES = '/v1/posts/dates';
125
+ # API Path Add Post
126
+ API_PATH_POSTS_ADD = '/v1/posts/add';
127
+ # API Path Delete Post
128
+ API_PATH_POSTS_DELETE = '/v1/posts/delete';
129
+
130
+ # Time to wait before sending a new request, in seconds
131
+ SECONDS_BEFORE_NEW_REQUEST = 1
132
+
133
+ # Time converter converts a Time instance into the format
134
+ # requested by Delicious API
135
+ TIME_CONVERTER = lambda { |time| time.iso8601() }
136
+
137
+
138
+ public
139
+ #
140
+ # Constructs a new <tt>WWW::Delicious</tt> object
141
+ # with given +username+ and +password+.
142
+ #
143
+ # # create a new object with username 'user' and password 'psw
144
+ # obj = WWW::Delicious('user', 'psw')
145
+ # # => self
146
+ #
147
+ # If a block is given, the instance is passed to the block
148
+ # but this method always returns the instance itself.
149
+ #
150
+ # WWW::Delicious('user', 'psw') do |d|
151
+ # d.update() # => Fri May 02 18:02:48 UTC 2008
152
+ # end
153
+ # # => self
154
+ #
155
+ # === Options
156
+ # This class accepts an Hash with additional options.
157
+ # Here's the list of valid keys:
158
+ #
159
+ # <tt>:user_agent</tt>:: User agent to display in HTTP requests.
160
+ #
161
+ def initialize(username, password, options = {}, &block) # :yields: delicious
162
+ @username, @password = username.to_s, password.to_s
163
+
164
+ # set API base URI
165
+ @base_uri = URI.parse(API_BASE_URI)
166
+
167
+ init_user_agent(options)
168
+ init_http_client(options)
169
+
170
+ yield self if block_given?
171
+ self # ensure to always return self even if block is given
172
+ end
173
+
174
+
175
+ public
176
+ #
177
+ # Returns the reference to current <tt>@http_client</tt>.
178
+ # The http is always valid unless it has been previously set to +nil+.
179
+ #
180
+ # # nil client
181
+ # obj.http_client # => nil
182
+ #
183
+ # # valid client
184
+ # obj.http_client # => Net::HTTP
185
+ #
186
+ def http_client()
187
+ return @http_client
188
+ end
189
+
190
+ public
191
+ #
192
+ # Sets the internal <tt>@http_client</tt> to +client+.
193
+ #
194
+ # # nil client
195
+ # obj.http_client = nil
196
+ #
197
+ # # http client
198
+ # obj.http_client = Net::HTTP.new()
199
+ #
200
+ # # invalid client
201
+ # obj.http_client = 'foo' # => ArgumentError
202
+ #
203
+ def http_client=(client)
204
+ unless client.kind_of?(Net::HTTP) or client.nil?
205
+ raise ArgumentError, "`client` expected to be a kind of `Net::HTTP`, `#{client.class}` given"
206
+ end
207
+ @http_client = client
208
+ end
209
+
210
+ public
211
+ #
212
+ # Returns current user agent string.
213
+ #
214
+ def user_agent()
215
+ return @headers['User-Agent']
216
+ end
217
+
218
+
219
+ public
220
+ #
221
+ # Returns true if given account credentials are valid.
222
+ #
223
+ # d = WWW::Delicious.new('username', 'password')
224
+ # d.valid_account? # => true
225
+ #
226
+ # d = WWW::Delicious.new('username', 'invalid_password')
227
+ # d.valid_account? # => false
228
+ #
229
+ # This method is not "exception safe".
230
+ # It doesn't return false if an HTTP error or any kind of other error occurs,
231
+ # it raises back the exception to the caller instead.
232
+ #
233
+ # Raises:: WWW::Delicious::Error
234
+ # Raises:: WWW::Delicious::HTTPError
235
+ # Raises:: WWW::Delicious::ResponseError
236
+ #
237
+ def valid_account?
238
+ update()
239
+ return true
240
+ rescue HTTPError => e
241
+ return false if e.message =~ /invalid username or password/i
242
+ raise
243
+ end
244
+
245
+ public
246
+ #
247
+ # Checks to see when a user last posted an item
248
+ # and returns the last update +Time+ for the user.
249
+ #
250
+ # d.update() # => Fri May 02 18:02:48 UTC 2008
251
+ #
252
+ #
253
+ # Raises:: WWW::Delicious::Error
254
+ # Raises:: WWW::Delicious::HTTPError
255
+ # Raises:: WWW::Delicious::ResponseError
256
+ #
257
+ def update()
258
+ response = request(API_PATH_UPDATE)
259
+ return parse_update_response(response.body)
260
+ end
261
+
262
+ public
263
+ #
264
+ # Retrieves all of a user's bundles
265
+ # and returns an array of <tt>WWW::Delicious::Bundle</tt>.
266
+ #
267
+ # d.bundles_all() # => [#<WWW::Delicious::Bundle>, #<WWW::Delicious::Bundle>, ...]
268
+ # d.bundles_all() # => []
269
+ #
270
+ #
271
+ # Raises:: WWW::Delicious::Error
272
+ # Raises:: WWW::Delicious::HTTPError
273
+ # Raises:: WWW::Delicious::ResponseError
274
+ #
275
+ def bundles_all()
276
+ response = request(API_PATH_BUNDLES_ALL)
277
+ return parse_bundles_all_response(response.body)
278
+ end
279
+
280
+ public
281
+ #
282
+ # Assignes a set of tags to a single bundle,
283
+ # wipes away previous settings for bundle.
284
+ #
285
+ # # create from a bundle
286
+ # d.bundles_set(WWW::Delicious::Bundle.new('MyBundle'), %w(foo bar))
287
+ #
288
+ # # create from a string
289
+ # d.bundles_set('MyBundle', %w(foo bar))
290
+ #
291
+ #
292
+ # Raises:: WWW::Delicious::Error
293
+ # Raises:: WWW::Delicious::HTTPError
294
+ # Raises:: WWW::Delicious::ResponseError
295
+ #
296
+ def bundles_set(bundle_or_name, tags = [])
297
+ params = prepare_bundles_set_params(bundle_or_name, tags)
298
+ response = request(API_PATH_BUNDLES_SET, params)
299
+ return parse_and_eval_execution_response(response.body)
300
+ end
301
+
302
+ public
303
+ #
304
+ # Deletes a bundle.
305
+ #
306
+ # # delete from a bundle
307
+ # d.bundles_delete(WWW::Delicious::Bundle.new('MyBundle'))
308
+ #
309
+ # # delete from a string
310
+ # d.bundles_delete('MyBundle', %w(foo bar))
311
+ #
312
+ #
313
+ # Raises:: WWW::Delicious::Error
314
+ # Raises:: WWW::Delicious::HTTPError
315
+ # Raises:: WWW::Delicious::ResponseError
316
+ #
317
+ def bundles_delete(bundle_or_name)
318
+ params = prepare_bundles_delete_params(bundle_or_name)
319
+ response = request(API_PATH_BUNDLES_DELETE, params)
320
+ return parse_and_eval_execution_response(response.body)
321
+ end
322
+
323
+ public
324
+ #
325
+ # Retrieves the list of tags and number of times used by the user
326
+ # and returns an array of <tt>WWW::Delicious::Tag</tt>.
327
+ #
328
+ # Raises:: WWW::Delicious::Error
329
+ # Raises:: WWW::Delicious::HTTPError
330
+ # Raises:: WWW::Delicious::ResponseError
331
+ #
332
+ def tags_get()
333
+ response = request(API_PATH_TAGS_GET)
334
+ return parse_tags_get_response(response.body)
335
+ end
336
+
337
+ public
338
+ #
339
+ # Renames an existing tag with a new tag name.
340
+ #
341
+ # Raises:: WWW::Delicious::Error
342
+ # Raises:: WWW::Delicious::HTTPError
343
+ # Raises:: WWW::Delicious::ResponseError
344
+ #
345
+ def tags_rename(from_name_or_tag, to_name_or_tag)
346
+ params = prepare_tags_rename_params(from_name_or_tag, to_name_or_tag)
347
+ response = request(API_PATH_TAGS_RENAME, params)
348
+ return parse_and_eval_execution_response(response.body)
349
+ end
350
+
351
+ public
352
+ #
353
+ # Returns an array of <tt>WWW::Delicious::Post</tt> matching +options+.
354
+ # If no option is given, the last post is returned.
355
+ # If no date or url is given, most recent date will be used.
356
+ #
357
+ # === Options
358
+ # <tt>:tag</tt>:: a tag to filter by. It can be either a <tt>WWW::Delicious::Tag</tt> or a +String+.
359
+ # <tt>:dt</tt>:: a +Time+ with a date to filter by.
360
+ # <tt>:url</tt>:: a valid URI to filter by. It can be either an instance of +URI+ or a +String+.
361
+ #
362
+ # Raises:: WWW::Delicious::Error
363
+ # Raises:: WWW::Delicious::HTTPError
364
+ # Raises:: WWW::Delicious::ResponseError
365
+ #
366
+ def posts_get(options = {})
367
+ params = prepare_posts_params(options.clone, [:dt, :tag, :url])
368
+ response = request(API_PATH_POSTS_GET, params)
369
+ return parse_posts_response(response.body)
370
+ end
371
+
372
+ public
373
+ #
374
+ # Returns a list of the most recent posts, filtered by argument.
375
+ #
376
+ # === Options
377
+ # <tt>:tag</tt>:: a tag to filter by. It can be either a <tt>WWW::Delicious::Tag</tt> or a +String+.
378
+ # <tt>:count</tt>:: number of items to retrieve. (default: 15, maximum: 100).
379
+ #
380
+ def posts_recent(options = {})
381
+ params = prepare_posts_params(options.clone, [:count, :tag])
382
+ response = request(API_PATH_POSTS_RECENT, params)
383
+ return parse_posts_response(response.body)
384
+ end
385
+
386
+ public
387
+ #
388
+ # Returns a list of the most recent posts, filtered by argument.
389
+ #
390
+ # === Options
391
+ # <tt>:tag</tt>:: a tag to filter by. It can be either a <tt>WWW::Delicious::Tag</tt> or a +String+.
392
+ #
393
+ def posts_all(options = {})
394
+ params = prepare_posts_params(options.clone, [:tag])
395
+ response = request(API_PATH_POSTS_ALL, params)
396
+ return parse_posts_response(response.body)
397
+ end
398
+
399
+ public
400
+ #
401
+ # Returns a list of dates with the number of posts at each date.
402
+ #
403
+ # === Options
404
+ # <tt>:tag</tt>:: a tag to filter by. It can be either a <tt>WWW::Delicious::Tag</tt> or a +String+.
405
+ #
406
+ def posts_dates(options = {})
407
+ params = prepare_posts_params(options.clone, [:tag])
408
+ response = request(API_PATH_POSTS_DATES, params)
409
+ return parse_posts_dates_response(response.body)
410
+ end
411
+
412
+ public
413
+ #
414
+ # Add a post to del.icio.us.
415
+ #
416
+ def posts_add(post_or_values)
417
+ params = prepare_posts_add_params(post_or_values.clone)
418
+ response = request(API_PATH_POSTS_ADD, params)
419
+ return parse_and_eval_execution_response(response.body)
420
+ end
421
+
422
+ public
423
+ #
424
+ # Deletes a post from del.icio.us.
425
+ #
426
+ # === Params
427
+ # url::
428
+ # the url of the item.
429
+ # It can be either an +URI+ or a +String+.
430
+ #
431
+ def posts_delete(url)
432
+ params = prepare_posts_params({:url => url}, [:url])
433
+ response = request(API_PATH_POSTS_DELETE, params)
434
+ return parse_and_eval_execution_response(response.body)
435
+ end
436
+
437
+
438
+ protected
439
+ #
440
+ # Initializes HTTP client.
441
+ #
442
+ def init_http_client(options)
443
+ http = Net::HTTP.new(@base_uri.host, 443)
444
+ http.use_ssl = true if @base_uri.scheme == "https"
445
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE # FIXME: not 100% supported
446
+ self.http_client = http
447
+ end
448
+
449
+ protected
450
+ #
451
+ # Initializes user agent value for HTTP requests.
452
+ #
453
+ def init_user_agent(options)
454
+ user_agent = options[:user_agent] || default_user_agent()
455
+ @headers ||= {}
456
+ @headers['User-Agent'] = user_agent
457
+ end
458
+
459
+ protected
460
+ #
461
+ # Creates and returns the default user agent string.
462
+ #
463
+ # By default, the user agent is composed by the following schema:
464
+ # <tt>NAME/VERSION (Ruby/RUBY_VERSION)</tt>
465
+ #
466
+ # * +NAME+ is the constant representing this library name
467
+ # * +VERSION+ is the constant representing current library version
468
+ # * +RUBY_VERSION+ is the version of Ruby interpreter the library is interpreted by
469
+ #
470
+ def default_user_agent()
471
+ return "#{NAME}/#{VERSION} (Ruby/#{RUBY_VERSION})"
472
+ end
473
+
474
+
475
+ protected
476
+ #
477
+ # Composes an HTTP query string from an hash of +options+.
478
+ #
479
+ def http_build_query(params = {})
480
+ return params.collect do |k,v|
481
+ "#{URI.encode(k.to_s)}=#{URI.encode(v.to_s)}" unless v.nil?
482
+ end.compact.join('&')
483
+ end
484
+
485
+ protected
486
+ #
487
+ # Sends an HTTP GET request to +path+ and appends given +params+.
488
+ #
489
+ # This method is 100% compliant with Delicious API reference.
490
+ # It waits at least 1 second between each HTTP request and
491
+ # provides an identifiable user agent by default,
492
+ # or the custom user agent set by +user_agent+ option
493
+ # when this istance has been created.
494
+ #
495
+ def request(path, params = {})
496
+ raise Error, 'Invalid HTTP Client' unless http_client
497
+ wait_before_new_request
498
+
499
+ uri = @base_uri.merge(path)
500
+ uri.query = http_build_query(params) unless params.empty?
501
+
502
+ begin
503
+ @last_request = Time.now # see #wait_before_new_request
504
+ @last_request_uri = uri # useful for debug
505
+ response = http_client.start do |http|
506
+ req = Net::HTTP::Get.new(uri.request_uri, @headers)
507
+ req.basic_auth(@username, @password)
508
+ http.request(req)
509
+ end
510
+ rescue => e # catch EOFError, SocketError and more
511
+ raise HTTPError, e.message
512
+ end
513
+
514
+ case response
515
+ when Net::HTTPSuccess
516
+ return response
517
+ when Net::HTTPUnauthorized # 401
518
+ raise HTTPError, 'Invalid username or password'
519
+ when Net::HTTPServiceUnavailable # 503
520
+ raise HTTPError, 'You have been throttled.' +
521
+ 'Please ensure you are waiting at least one second before each request.'
522
+ else
523
+ raise HTTPError, "HTTP #{response.code}: #{response.message}"
524
+ end
525
+ end
526
+
527
+ protected
528
+ #
529
+ # Delicious API reference requests to wait AT LEAST ONE SECOND
530
+ # between queries or the client is likely to get automatically throttled.
531
+ #
532
+ # This method calculates the difference between current time
533
+ # and the last request time and wait for the necessary time to meet
534
+ # SECONDS_BEFORE_NEW_REQUEST requirement.
535
+ #
536
+ # The difference is not rounded. If you only have to wait for 0.034 seconds
537
+ # then your don't have to wait 0 or 1 seconds, but 0.034 seconds!
538
+ #
539
+ def wait_before_new_request()
540
+ return unless @last_request # this is the first request
541
+ # puts "Last request at #{TIME_CONVERTER.call(@last_request)}" if debug?
542
+ diff = Time.now - @last_request
543
+ if diff < SECONDS_BEFORE_NEW_REQUEST
544
+ # puts "Sleeping for #{diff} before new request..." if debug?
545
+ sleep(SECONDS_BEFORE_NEW_REQUEST - diff)
546
+ end
547
+ end
548
+
549
+
550
+ protected
551
+ #
552
+ # Parses the response +body+ and runs a common set of validators.
553
+ #
554
+ def parse_and_validate_response(body, options = {})
555
+ dom = REXML::Document.new(body)
556
+
557
+ if (value = options[:root_name]) && dom.root.name != value
558
+ raise ResponseError, "Invalid response, root node is not `#{value}`"
559
+ end
560
+ if (value = options[:root_text]) && dom.root.text != value
561
+ raise ResponseError, value
562
+ end
563
+
564
+ return dom
565
+ end
566
+
567
+ protected
568
+ #
569
+ # Parses and evaluates the response returned by an execution,
570
+ # usually an update/delete/insert operation.
571
+ #
572
+ def parse_and_eval_execution_response(body)
573
+ dom = parse_and_validate_response(body, :root_name => 'result')
574
+
575
+ rsp = dom.root.attribute_value(:code)
576
+ rsp = dom.root.text if rsp.nil?
577
+ raise Error, "Invalid response, #{rsp}" unless %w(done ok).include?(rsp)
578
+ end
579
+
580
+ protected
581
+ #
582
+ # Parses the response of an 'update' request.
583
+ #
584
+ def parse_update_response(body)
585
+ dom = parse_and_validate_response(body, :root_name => 'update')
586
+ return dom.root.attribute_value(:time) { |v| Time.parse(v) }
587
+ end
588
+
589
+ protected
590
+ #
591
+ # Parses the response of a 'bundles_all' request
592
+ # and returns an array of <tt>WWW::Delicious::Bundle</tt>.
593
+ #
594
+ def parse_bundles_all_response(body)
595
+ dom = parse_and_validate_response(body, :root_name => 'bundles')
596
+ bundles = []
597
+
598
+ dom.root.elements.each('bundle') { |xml| bundles << Bundle.from_rexml(xml) }
599
+ return bundles
600
+ end
601
+
602
+ protected
603
+ #
604
+ # Parses the response of a 'tags_get' request
605
+ # and returns an array of <tt>WWW::Delicious::Tag</tt>.
606
+ #
607
+ def parse_tags_get_response(body)
608
+ dom = parse_and_validate_response(body, :root_name => 'tags')
609
+ tags = []
610
+
611
+ dom.root.elements.each('tag') { |xml| tags << Tag.new(xml) }
612
+ return tags
613
+ end
614
+
615
+ protected
616
+ #
617
+ # Parses a response containing a list of Posts
618
+ # and returns an array of <tt>WWW::Delicious::Post</tt>.
619
+ #
620
+ def parse_posts_response(body)
621
+ dom = parse_and_validate_response(body, :root_name => 'posts')
622
+ posts = []
623
+
624
+ dom.root.elements.each('post') { |xml| posts << Post.new(xml) }
625
+ return posts
626
+ end
627
+
628
+ protected
629
+ #
630
+ # Parses the response of a 'posts_dates' request
631
+ # and returns an +Hash+ of date => count.
632
+ #
633
+ def parse_posts_dates_response(body)
634
+ dom = parse_and_validate_response(body, :root_name => 'dates')
635
+ results = {}
636
+
637
+ dom.root.elements.each('date') do |xml|
638
+ date = xml.attribute_value(:date)
639
+ count = xml.attribute_value(:count).to_i()
640
+ results[date] = count
641
+ end
642
+ return results
643
+ end
644
+
645
+
646
+ protected
647
+ #
648
+ # Prepares the params for a `bundles_set` request.
649
+ #
650
+ # === Returns
651
+ # An +Hash+ with params to supply to the HTTP request.
652
+ #
653
+ # Raises::
654
+ #
655
+ def prepare_bundles_set_params(name_or_bundle, tags = [])
656
+ bundle = prepare_param_bundle(name_or_bundle, tags) do |b|
657
+ raise Error, "Bundle name is empty" if b.name.empty?
658
+ raise Error, "Bundle must contain at least one tag" if b.tags.empty?
659
+ end
660
+
661
+ return {
662
+ :bundle => bundle.name,
663
+ :tags => bundle.tags.join(' '),
664
+ }
665
+ end
666
+
667
+ protected
668
+ #
669
+ # Prepares the params for a `bundles_set` request.
670
+ #
671
+ # === Returns
672
+ # An +Hash+ with params to supply to the HTTP request.
673
+ #
674
+ # Raises::
675
+ #
676
+ def prepare_bundles_delete_params(name_or_bundle)
677
+ bundle = prepare_param_bundle(name_or_bundle) do |b|
678
+ raise Error, "Bundle name is empty" if b.name.empty?
679
+ end
680
+ return { :bundle => bundle.name }
681
+ end
682
+
683
+ protected
684
+ #
685
+ # Prepares the params for a `tags_rename` request.
686
+ #
687
+ # === Returns
688
+ # An +Hash+ with params to supply to the HTTP request.
689
+ #
690
+ # Raises::
691
+ #
692
+ def prepare_tags_rename_params(from_name_or_tag, to_name_or_tag)
693
+ from, to = [from_name_or_tag, to_name_or_tag].collect do |v|
694
+ prepare_param_tag(v)
695
+ end
696
+ return { :old => from, :new => to }
697
+ end
698
+
699
+ protected
700
+ #
701
+ # Prepares the params for a `post_*` request.
702
+ #
703
+ # === Returns
704
+ # An +Hash+ with params to supply to the HTTP request.
705
+ #
706
+ # Raises::
707
+ #
708
+ def prepare_posts_params(params, allowed_params = [])
709
+ compare_params(params, allowed_params)
710
+
711
+ # we don't need to check whether the following parameters
712
+ # are valid for this request because compare_params
713
+ # would raise if an invalid param is supplied
714
+
715
+ params[:tag] = prepare_param_tag(params[:tag]) if params[:tag]
716
+ params[:dt] = TIME_CONVERTER.call(params[:dt]) if params[:dt]
717
+ params[:url] = URI.parse(params[:url]) if params[:url]
718
+ params[:count] = if value = params[:count]
719
+ raise Error, 'Expected `count` <= 100' if value.to_i() > 100 # requirement
720
+ value.to_i()
721
+ else
722
+ 15 # default value
723
+ end
724
+
725
+ return params
726
+ end
727
+
728
+ protected
729
+ #
730
+ # Prepares the params for a `post_add` request.
731
+ #
732
+ # === Returns
733
+ # An +Hash+ with params to supply to the HTTP request.
734
+ #
735
+ # Raises::
736
+ #
737
+ def prepare_posts_add_params(post_or_values)
738
+ post = case post_or_values
739
+ when WWW::Delicious::Post
740
+ post_or_values
741
+ when Hash
742
+ value = Post.new(post_or_values)
743
+ raise ArgumentError, 'Both `url` and `title` are required' unless value.api_valid?
744
+ value
745
+ else
746
+ raise ArgumentError, 'Expected `args` to be `WWW::Delicious::Post` or `Hash`'
747
+ end
748
+ return post.to_params()
749
+ end
750
+
751
+ protected
752
+ #
753
+ # Prepares the +bundle+ params.
754
+ #
755
+ # If +name_or_bundle+ is a string,
756
+ # creates a new <tt>WWW::Delicious::Bundle</tt> with
757
+ # +name_or_bundle+ as name and a collection of +tags+.
758
+ # If +name_or_bundle+, +tags+ is ignored.
759
+ #
760
+ def prepare_param_bundle(name_or_bundle, tags = [], &block) # :yields: bundle
761
+ bundle = case name_or_bundle
762
+ when WWW::Delicious::Bundle
763
+ name_or_bundle
764
+ else
765
+ Bundle.new(name_or_bundle.to_s(), tags)
766
+ end
767
+ yield(bundle) if block_given?
768
+ return bundle
769
+ end
770
+
771
+ protected
772
+ #
773
+ # Prepares the +tag+ params.
774
+ #
775
+ # If +name_or_tag+ is a string,
776
+ # it creates a new <tt>WWW::Delicious::Tag</tt> with
777
+ # +name_or_tag+ as name.
778
+ #
779
+ def prepare_param_tag(name_or_tag, &block) # :yields: tag
780
+ tag = case name_or_tag
781
+ when WWW::Delicious::Tag
782
+ name_or_tag
783
+ else
784
+ Tag.new(:name => name_or_tag.to_s())
785
+ end
786
+
787
+ yield(tag) if block_given?
788
+ raise "Invalid `tag` value supplied" unless tag.api_valid?
789
+
790
+ return tag
791
+ end
792
+
793
+ protected
794
+ #
795
+ # Checks whether user given params are valid against valid params.
796
+ #
797
+ # === Params
798
+ # params::
799
+ # an +Hash+ with user given params to validate
800
+ # valid_params::
801
+ # an +Array+ of valid params keys to check against
802
+ #
803
+ # === Examples
804
+ #
805
+ # params = {:foo => 1, :bar => 2}
806
+ #
807
+ # compare_params(params, [:foo, :bar])
808
+ # # => valid
809
+ #
810
+ # compare_params(params, [:foo, :bar, :baz])
811
+ # # => raises
812
+ #
813
+ # compare_params(params, [:foo])
814
+ # # => raises
815
+ #
816
+ # Raises:: WWW::Delicious::Error
817
+ #
818
+ def compare_params(params, valid_params)
819
+ raise ArgumentError, "Expected `params` to be a kind of `Hash`" unless params.kind_of?(Hash)
820
+ raise ArgumentError, "Expected `valid_params` to be a kind of `Array`" unless valid_params.kind_of?(Array)
821
+
822
+ # compute options difference
823
+ difference = params.keys - valid_params
824
+ raise Error,
825
+ "Invalid params: `#{difference.join('`, `')}`" unless difference.empty?
826
+ end
827
+
828
+
829
+ module XMLUtils #:nodoc:
830
+
831
+ public
832
+ #
833
+ # Returns the +xmlattr+ attribute value for given +node+.
834
+ #
835
+ # If block is given and attrivute value is not nil
836
+ # the content of the block is executed.
837
+ #
838
+ # === Params
839
+ # node::
840
+ # The REXML::Element node context
841
+ # xmlattr::
842
+ # A String corresponding to the name of the XML attribute to search for
843
+ #
844
+ # === Return
845
+ # The value of the +xmlattr+ if the attribute exists for given +node+,
846
+ # +nil+ otherwise.
847
+ #
848
+ def attribute_value(xmlattr, &block) #:nodoc:
849
+ value = if attr = self.attribute(xmlattr.to_s())
850
+ attr.value()
851
+ else
852
+ nil
853
+ end
854
+ value = yield value if !value.nil? and block_given?
855
+ return value
856
+ end
857
+
858
+ end
859
+
860
+ end
861
+ end
862
+
863
+
864
+ module REXML # :nodoc:
865
+ class Element < Parent # :nodoc:
866
+ include WWW::Delicious::XMLUtils
867
+ end
868
+ end