hatenadiary 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (6) hide show
  1. data/ChangeLog +26 -0
  2. data/LICENSE +28 -0
  3. data/README +25 -0
  4. data/lib/hatenadiary.rb +191 -0
  5. data/test/test_hatenadiary.rb +238 -0
  6. metadata +87 -0
data/ChangeLog ADDED
@@ -0,0 +1,26 @@
1
+ 2009-12-04 arikui <arikui.ruby@gmail.com>
2
+
3
+ * Rakefile: define common settings as constant.
4
+ include gemspec.
5
+ (task 'gemspec') added. this task updates `hatenadiary.gemspec' along to spec written in Rakefile.
6
+ It means, don't have to correct `hatenadiary.gemspec' by hand.
7
+
8
+ * hatenadiary.gemspec: version up to 0.0.4
9
+
10
+ 2009-11-29 arikui <arikui.ruby@gmail.com>
11
+
12
+ * test/test_hatenadiary.rb: replace test by mocha used version.
13
+
14
+ * lib/hatenadiary.rb: use hpricot -> nokogiri 1.3.3
15
+ respond to hatena group diary.
16
+ (HatenaDiary::Client#post) change interface.
17
+ (HatenaDiary::Client#initialize): interface extended for test.
18
+
19
+ 2009-07-03 arikui <arikui.ruby@gmail.com>
20
+
21
+ * lib/hatenadiary.rb: use nokogiri -> hpricot
22
+
23
+ 2009-06-22 arikui <arikui.ruby@gmail.com>
24
+
25
+ * lib/hatenadiary.rb: first version.
26
+
data/LICENSE ADDED
@@ -0,0 +1,28 @@
1
+ Copyright (c) 2009 arikui <arikui.ruby@gmail.com>
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without
5
+ modification, are permitted provided that the following conditions are
6
+ met:
7
+
8
+ * Redistributions of source code must retain the above copyright
9
+ notice, this list of conditions and the following disclaimer.
10
+ * Redistributions in binary form must reproduce the above
11
+ copyright notice, this list of conditions and the following
12
+ disclaimer in the documentation and/or other materials provided
13
+ with the distribution.
14
+ * Neither the name of the arikui-hatenadiary nor the names of its
15
+ contributors may be used to endorse or promote products derived
16
+ from this software without specific prior written permission.
17
+
18
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19
+ "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20
+ LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21
+ A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22
+ OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23
+ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24
+ LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25
+ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26
+ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
data/README ADDED
@@ -0,0 +1,25 @@
1
+ = hatenadiary
2
+
3
+ It is a library provides
4
+ a client for Hatena Diary to post and delete blog entries.
5
+
6
+ Developed on:
7
+ - ruby 1.9.1p243 (2009-07-16 revision 24175) [i386-mswin32]
8
+ - ruby 1.8.7 (2009-06-12 patchlevel 174) [i386-mswin32]
9
+
10
+ - Gem mechanize 0.9.3
11
+ - Gem nokogiri 1.3.3
12
+
13
+ This list means these environments are enable to make all of /test pass.
14
+
15
+
16
+ == Dependences
17
+
18
+ - www/mechanize
19
+ - nokogiri
20
+
21
+
22
+ == Signature
23
+
24
+ arikui (arikui.ruby@gmail.com)
25
+
@@ -0,0 +1,191 @@
1
+ #
2
+ # Distributes under The modified BSD license.
3
+ #
4
+ # Copyright (c) 2009 arikui <http://d.hatena.ne.jp/arikui1911/>
5
+ # All rights reserved.
6
+ #
7
+
8
+ require 'rubygems'
9
+
10
+ # Gem Version
11
+ gem 'nokogiri', '1.3.3'
12
+
13
+ require 'nokogiri'
14
+ require 'mechanize'
15
+
16
+ # depend on Ruby version
17
+ module HatenaDiary
18
+ module Util
19
+ if RUBY_VERSION >= '1.9'
20
+ def encode_to_utf8(str)
21
+ str.encode(Encoding::UTF_8)
22
+ end
23
+ else
24
+ require 'kconv'
25
+
26
+ def encode_to_utf8(str)
27
+ Kconv.toutf8(str)
28
+ end
29
+ end
30
+
31
+ module_function :encode_to_utf8
32
+ end
33
+ end
34
+
35
+
36
+ module HatenaDiary
37
+ #
38
+ # Allocates Client object and makes it login, execute a received block,
39
+ # and then logout.
40
+ #
41
+ # :call-seq:
42
+ # login(username, password, proxy = nil){|client| ... }
43
+ #
44
+ def login(*args, &block)
45
+ Client.login(*args, &block)
46
+ end
47
+ module_function :login
48
+
49
+ class LoginError < RuntimeError
50
+ def set_account(username, password)
51
+ @username = username
52
+ @password = password
53
+ self
54
+ end
55
+
56
+ attr_reader :username
57
+ attr_reader :password
58
+ end
59
+
60
+ class Client
61
+ def self.mechanizer
62
+ @mechanizer ||= WWW::Mechanize
63
+ end
64
+
65
+ def self.mechanizer=(klass)
66
+ @mechanizer = klass
67
+ end
68
+
69
+ # Allocates Client object.
70
+ #
71
+ # If block given, login and execute a received block, and then logout ensurely.
72
+ #
73
+ # [username] Hatena ID
74
+ # [password] Password for _username_
75
+ # [proxy] Proxy configuration; [proxy_url, port_no] | nil
76
+ #
77
+ def self.login(username, password, proxy = nil, &block)
78
+ client = new(username, password)
79
+ client.set_proxy(*proxy) if proxy
80
+ return client unless block_given?
81
+ client.transaction(&block)
82
+ end
83
+
84
+ # Allocates Client object.
85
+ #
86
+ # [username] Hatena ID
87
+ # [password] Password for _username_
88
+ def initialize(username, password, agent = self.class.mechanizer.new)
89
+ @agent = agent
90
+ @username = username
91
+ @password = password
92
+ @current_account = nil
93
+ end
94
+
95
+ # Configure proxy.
96
+ def set_proxy(url, port)
97
+ @agent.set_proxy(url, port)
98
+ end
99
+
100
+ # Login and execute a received block, and then logout ensurely.
101
+ def transaction(username = nil, password = nil)
102
+ raise LocalJumpError, "no block given" unless block_given?
103
+ login(username, password)
104
+ begin
105
+ yield(self)
106
+ ensure
107
+ logout
108
+ end
109
+ end
110
+
111
+ # Returns a client itself was logined or not.
112
+ #
113
+ # -> true | false
114
+ def login?
115
+ @current_account ? true : false
116
+ end
117
+
118
+ # Does login.
119
+ #
120
+ # If _username_ or _password_ are invalid, raises HatenaDiary::LoginError .
121
+ def login(username = nil, password = nil)
122
+ username ||= @username
123
+ password ||= @password
124
+ form = @agent.get("https://www.hatena.ne.jp/login").forms.first
125
+ form["name"] = username
126
+ form["password"] = password
127
+ form["persistent"] = "true"
128
+ response = form.submit
129
+ @current_account = [username, password]
130
+ case response.title
131
+ when "Hatena" then response
132
+ when "Login - Hatena" then raise LoginError.new("login failure").set_account(username, password)
133
+ else raise Exception, '[BUG] must not happen (maybe cannot follow hatena spec)'
134
+ end
135
+ end
136
+
137
+ # Does logout if already logined.
138
+ def logout
139
+ return unless login?
140
+ @agent.get("https://www.hatena.ne.jp/logout")
141
+ account = @current_account
142
+ @current_account = nil
143
+ account
144
+ end
145
+
146
+ # Posts an entry to Hatena diary service.
147
+ #
148
+ # Raises HatenaDiary::LoginError unless logined.
149
+ #
150
+ # options
151
+ # [:trivial] check a checkbox of trivial updating.
152
+ # [:group] assign hatena-group name. edit group diary.
153
+ #
154
+ # Invalid options were ignored.
155
+ def post(yyyy, mm, dd, title, body, options = {})
156
+ title = Util.encode_to_utf8(title)
157
+ body = Util.encode_to_utf8(body)
158
+ form = get_form(yyyy, mm, dd, options[:group]){|r| r.form_with(:name => 'edit') }
159
+ form["year"] = "%04d" % yyyy
160
+ form["month"] = "%02d" % mm
161
+ form["day"] = "%02d" % dd
162
+ form["title"] = title
163
+ form["body"] = body
164
+ form["trivial"] = "true" if options[:trivial]
165
+ @agent.submit form, form.button_with(:name => 'edit')
166
+ end
167
+
168
+ # Deletes an entry from Hatena diary service.
169
+ #
170
+ # Raises HatenaDiary::LoginError unless logined.
171
+ #
172
+ # options
173
+ # [:group] assign hatena-group name. edit group diary.
174
+ #
175
+ # Invalid options were ignored.
176
+ def delete(yyyy, mm, dd, options = {})
177
+ get_form(yyyy, mm, dd, options[:group]){|r| r.forms.last }.submit
178
+ end
179
+
180
+ private
181
+
182
+ def get_form(yyyy, mm, dd, group = nil)
183
+ raise LoginError, "not login yet" unless login?
184
+ vals = [group ? "#{group}.g" : "d",
185
+ @current_account[0],
186
+ yyyy, mm, dd]
187
+ yield @agent.get("http://%s.hatena.ne.jp/%s/edit?date=%04d%02d%02d" % vals)
188
+ end
189
+ end
190
+ end
191
+
@@ -0,0 +1,238 @@
1
+ $LOAD_PATH.unshift File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ require 'test/unit'
3
+ require 'mocha'
4
+ require 'hatenadiary'
5
+
6
+
7
+ class TestHatenaDiaryAPI < Test::Unit::TestCase
8
+ def setup
9
+ @username = 'USERNAME'
10
+ @password = 'PASSWORD'
11
+ @proxy_url = "PROXY_URL"
12
+ @proxy_port = "PROXY_PORT"
13
+ end
14
+
15
+ def test_api_login
16
+ HatenaDiary::Client.expects(:login)
17
+ HatenaDiary.login
18
+ end
19
+
20
+ def test_login
21
+ client = mock()
22
+ HatenaDiary::Client.expects(:new).with(@username, @password).returns(client)
23
+ client.expects(:transaction).yields('block delegate check')
24
+ HatenaDiary::Client.login(@username, @password) do |str|
25
+ assert_equal 'block delegate check', str
26
+ end
27
+ end
28
+
29
+ def test_login_with_proxy
30
+ client = mock()
31
+ HatenaDiary::Client.expects(:new).with(@username, @password).returns(client)
32
+ client.expects(:set_proxy).with(@proxy_url, @proxy_port)
33
+ client.expects(:transaction).yields('block delegate check')
34
+ HatenaDiary::Client.login(@username, @password, [@proxy_url, @proxy_port]) do |str|
35
+ assert_equal 'block delegate check', str
36
+ end
37
+ end
38
+
39
+ def test_login_without_block
40
+ client = Object.new
41
+ HatenaDiary::Client.expects(:new).with(@username, @password).returns(client)
42
+ assert_equal client, HatenaDiary::Client.login(@username, @password)
43
+ end
44
+
45
+ def test_login_without_block_with_proxy
46
+ client = mock()
47
+ HatenaDiary::Client.expects(:new).with(@username, @password).returns(client)
48
+ client.expects(:set_proxy).with(@proxy_url, @proxy_port)
49
+ assert_equal client, HatenaDiary::Client.login(@username, @password, [@proxy_url, @proxy_port])
50
+ end
51
+ end
52
+
53
+
54
+ class TestHatenaDiary < Test::Unit::TestCase
55
+ def setup
56
+ @username = 'USERNAME'
57
+ @password = 'PASSWORD'
58
+ @agent = mock()
59
+ @client = HatenaDiary::Client.new(@username, @password, @agent)
60
+ end
61
+
62
+ def test_set_proxy
63
+ proxy_url = 'PROXY_URL'
64
+ proxy_port = 'PROXY_PORT'
65
+ @agent.expects(:set_proxy).with(proxy_url, proxy_port)
66
+ @client.set_proxy(proxy_url, proxy_port)
67
+ end
68
+
69
+ def test_logout_without_login
70
+ @client.logout
71
+ end
72
+
73
+ def login_mocking(submit_response_page_title)
74
+ login_page = mock()
75
+ form = {}
76
+ forms = [form]
77
+ response = mock()
78
+ @agent.expects(:get).with("https://www.hatena.ne.jp/login").returns(login_page)
79
+ login_page.expects(:forms).returns(forms)
80
+ form.expects(:submit).returns(response)
81
+ response.expects(:title).returns(submit_response_page_title)
82
+ form
83
+ end
84
+
85
+ def logout_mocking
86
+ @agent.expects(:get).with("https://www.hatena.ne.jp/logout")
87
+ end
88
+
89
+ def test_login_and_logout
90
+ # before login
91
+ assert !@client.login?
92
+ # login
93
+ form = login_mocking("Hatena")
94
+ @client.login
95
+ assert @client.login?
96
+ assert_equal form["name"], @username
97
+ assert_equal form["password"], @password
98
+ assert_equal form["persistent"], "true"
99
+ # logout
100
+ logout_mocking
101
+ @client.logout
102
+ assert !@client.login?
103
+ end
104
+
105
+ def test_login_failure
106
+ login_mocking "Login - Hatena"
107
+ begin
108
+ @client.login
109
+ rescue HatenaDiary::LoginError => ex
110
+ assert_equal @username, ex.username
111
+ assert_equal @password, ex.password
112
+ else
113
+ flunk "login error must be raised."
114
+ end
115
+ end
116
+
117
+ def test_login_if_hatena_changed
118
+ login_mocking "*jumbled pagetitle*"
119
+ begin
120
+ @client.login
121
+ rescue Exception => ex
122
+ assert /must not happen/ =~ ex.message
123
+ else
124
+ flunk "exception must be raised"
125
+ end
126
+ end
127
+
128
+ def test_transaction
129
+ assert !@client.login?
130
+ login_mocking "Hatena"
131
+ logout_mocking
132
+ @client.transaction do |client|
133
+ assert_same @client, client
134
+ assert @client.login?
135
+ end
136
+ assert !@client.login?
137
+ end
138
+
139
+ def test_transaction_without_block
140
+ assert !@client.login?
141
+ assert_raises LocalJumpError do
142
+ @client.transaction
143
+ end
144
+ assert !@client.login?
145
+ end
146
+
147
+ def post_mocking(host, date_str)
148
+ edit_page = mock()
149
+ form = {}
150
+ button = Object.new
151
+ login_mocking "Hatena"
152
+ logout_mocking
153
+ @agent.expects(:get).with("http://#{host}.hatena.ne.jp/#{@username}/edit?date=#{date_str}").returns(edit_page)
154
+ edit_page.expects(:form_with).with(:name => 'edit').returns(form)
155
+ form.expects(:button_with).with(:name => 'edit').returns(button)
156
+ @agent.expects(:submit).with(form, button)
157
+ form
158
+ end
159
+
160
+ def test_post
161
+ form = post_mocking("d", "12340506")
162
+ @client.transaction do |client|
163
+ client.post 1234, 5, 6, 'TITLE', 'BODY'
164
+ end
165
+ expected = {
166
+ "year" => "1234",
167
+ "month" => "05",
168
+ "day" => "06",
169
+ "title" => "TITLE",
170
+ "body" => "BODY",
171
+ }
172
+ assert_equal expected, form
173
+ assert !form["trivial"]
174
+ end
175
+
176
+ def test_post_trivial
177
+ form = post_mocking("d", "20071108")
178
+ @client.transaction do |client|
179
+ client.post 2007, 11, 8, 'TITLE', 'BODY', :trivial => true
180
+ end
181
+ assert_equal "true", form["trivial"]
182
+ end
183
+
184
+ def test_post_group
185
+ post_mocking "hoge.g", "12340506"
186
+ @client.transaction do |client|
187
+ client.post 1234, 5, 6, 'TITLE', 'BODY', :group => 'hoge'
188
+ end
189
+ end
190
+
191
+ def test_post_group_trivial
192
+ form = post_mocking("hoge.g", "12340506")
193
+ @client.transaction do |client|
194
+ client.post 1234, 5, 6, 'TITLE', 'BODY', :group => 'hoge', :trivial => true
195
+ end
196
+ assert_equal "true", form["trivial"]
197
+ end
198
+
199
+ def test_post_without_login
200
+ assert_raises HatenaDiary::LoginError do
201
+ @client.post 1999, 5, 26, "TITLE", "BODY\n"
202
+ end
203
+ end
204
+
205
+ def delete_mocking(host, date_str)
206
+ edit_page = mock()
207
+ form = {}
208
+ forms = [form]
209
+ button = Object.new
210
+ login_mocking "Hatena"
211
+ logout_mocking
212
+ @agent.expects(:get).with("http://#{host}.hatena.ne.jp/#{@username}/edit?date=#{date_str}").returns(edit_page)
213
+ edit_page.expects(:forms).returns(forms)
214
+ form.expects(:submit)
215
+ form
216
+ end
217
+
218
+ def test_delete
219
+ delete_mocking "d", "12340506"
220
+ @client.transaction do |client|
221
+ client.delete 1234, 5, 6
222
+ end
223
+ end
224
+
225
+ def test_delete_group
226
+ delete_mocking "piyo.g", "12340506"
227
+ @client.transaction do |client|
228
+ client.delete 1234, 5, 6, :group => 'piyo'
229
+ end
230
+ end
231
+
232
+ def test_delete_without_login
233
+ assert_raises HatenaDiary::LoginError do
234
+ @client.delete 2009, 8, 30
235
+ end
236
+ end
237
+ end
238
+
metadata ADDED
@@ -0,0 +1,87 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: hatenadiary
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.4
5
+ platform: ruby
6
+ authors:
7
+ - arikui
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-12-04 00:00:00 +09:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: mechanize
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0"
24
+ version:
25
+ - !ruby/object:Gem::Dependency
26
+ name: nokogiri
27
+ type: :runtime
28
+ version_requirement:
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "="
32
+ - !ruby/object:Gem::Version
33
+ version: 1.3.3
34
+ version:
35
+ description: A client for Hatena Diary to post and delete blog entries.
36
+ email: arikui.ruby@gmail.com
37
+ executables: []
38
+
39
+ extensions: []
40
+
41
+ extra_rdoc_files:
42
+ - README
43
+ - LICENSE
44
+ - ChangeLog
45
+ files:
46
+ - lib/hatenadiary.rb
47
+ - test/test_hatenadiary.rb
48
+ - README
49
+ - LICENSE
50
+ - ChangeLog
51
+ has_rdoc: true
52
+ homepage: http://wiki.github.com/arikui1911/hatenadiary
53
+ licenses: []
54
+
55
+ post_install_message:
56
+ rdoc_options:
57
+ - --title
58
+ - hatenadiary documentation
59
+ - --opname
60
+ - index.html
61
+ - --line-numbers
62
+ - --main
63
+ - README
64
+ - --inline-source
65
+ require_paths:
66
+ - lib
67
+ required_ruby_version: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: "0"
72
+ version:
73
+ required_rubygems_version: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ version: "0"
78
+ version:
79
+ requirements: []
80
+
81
+ rubyforge_project: hatenadiary
82
+ rubygems_version: 1.3.5
83
+ signing_key:
84
+ specification_version: 3
85
+ summary: It is a library provides a client for Hatena Diary to post and delete blog entries.
86
+ test_files:
87
+ - test/test_hatenadiary.rb