arikui1911-hatenadiary 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/lib/hatenadiary.rb +166 -0
- data/test/test_hatenadiary.rb +230 -0
- metadata +4 -4
data/lib/hatenadiary.rb
ADDED
@@ -0,0 +1,166 @@
|
|
1
|
+
#
|
2
|
+
# Distributes under The modified BSD license.
|
3
|
+
#
|
4
|
+
# Copyright (c) 2009 arikui <http://d.hatena.ne.jp/arikui/>
|
5
|
+
# All rights reserved.
|
6
|
+
#
|
7
|
+
|
8
|
+
require 'rubygems'
|
9
|
+
require 'www/mechanize'
|
10
|
+
require 'www/mechanize/util'
|
11
|
+
require 'nkf'
|
12
|
+
|
13
|
+
class << WWW::Mechanize::Util
|
14
|
+
org = instance_method(:html_unescape)
|
15
|
+
define_method(:html_unescape) do |s|
|
16
|
+
m = org.bind(self)
|
17
|
+
begin
|
18
|
+
m.call s
|
19
|
+
rescue ArgumentError
|
20
|
+
m.call s.force_encoding(NKF.guess(s))
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
module HatenaDiary
|
26
|
+
#
|
27
|
+
# Allocates Client object and makes it login, execute a received block,
|
28
|
+
# and then logout.
|
29
|
+
#
|
30
|
+
# :call-seq:
|
31
|
+
# login(username, password, proxy = nil){|client| ... }
|
32
|
+
#
|
33
|
+
def login(*args, &block)
|
34
|
+
Client.login(*args, &block)
|
35
|
+
end
|
36
|
+
module_function :login
|
37
|
+
|
38
|
+
class LoginError < RuntimeError
|
39
|
+
def set_account(username, password)
|
40
|
+
@username = username
|
41
|
+
@password = password
|
42
|
+
self
|
43
|
+
end
|
44
|
+
|
45
|
+
attr_reader :username
|
46
|
+
attr_reader :password
|
47
|
+
end
|
48
|
+
|
49
|
+
class Client
|
50
|
+
def self.mechanizer
|
51
|
+
@mechanizer ||= WWW::Mechanize
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.mechanizer=(klass)
|
55
|
+
@mechanizer = klass
|
56
|
+
end
|
57
|
+
|
58
|
+
# Allocates Client object.
|
59
|
+
#
|
60
|
+
# If block given, login and execute a received block, and then logout ensurely.
|
61
|
+
#
|
62
|
+
# [username] Hatena ID
|
63
|
+
# [password] Password for _username_
|
64
|
+
# [proxy] Proxy configuration; [proxy_url, port_no] | nil
|
65
|
+
#
|
66
|
+
def self.login(username, password, proxy = nil, &block)
|
67
|
+
client = new(username, password)
|
68
|
+
client.set_proxy(*proxy) if proxy
|
69
|
+
return client unless block_given?
|
70
|
+
client.transaction(&block)
|
71
|
+
end
|
72
|
+
|
73
|
+
# Allocates Client object.
|
74
|
+
#
|
75
|
+
# [username] Hatena ID
|
76
|
+
# [password] Password for _username_
|
77
|
+
def initialize(username, password)
|
78
|
+
@agent = self.class.mechanizer.new
|
79
|
+
@username = username
|
80
|
+
@password = password
|
81
|
+
@current_account = nil
|
82
|
+
end
|
83
|
+
|
84
|
+
# Configure proxy.
|
85
|
+
def set_proxy(url, port)
|
86
|
+
@agent.set_proxy(url, port)
|
87
|
+
end
|
88
|
+
|
89
|
+
# Login and execute a received block, and then logout ensurely.
|
90
|
+
def transaction(username = nil, password = nil)
|
91
|
+
raise LocalJumpError, "no block given" unless block_given?
|
92
|
+
login(username, password)
|
93
|
+
begin
|
94
|
+
yield(self)
|
95
|
+
ensure
|
96
|
+
logout
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# Returns a client itself was logined or not.
|
101
|
+
#
|
102
|
+
# -> true | false
|
103
|
+
def login?
|
104
|
+
@current_account ? true : false
|
105
|
+
end
|
106
|
+
|
107
|
+
# Does login.
|
108
|
+
#
|
109
|
+
# If _username_ or _password_ are invalid, raises HatenaDiary::LoginError .
|
110
|
+
def login(username = nil, password = nil)
|
111
|
+
username ||= @username
|
112
|
+
password ||= @password
|
113
|
+
form = @agent.get("https://www.hatena.ne.jp/login").forms.first
|
114
|
+
form["name"] = username
|
115
|
+
form["password"] = password
|
116
|
+
form["persistent"] = "true"
|
117
|
+
response = form.submit
|
118
|
+
@current_account = [username, password]
|
119
|
+
case response.title
|
120
|
+
when "Hatena" then response
|
121
|
+
when "Login - Hatena" then raise LoginError.new("login failure").set_account(username, password)
|
122
|
+
else raise Exception, '[BUG] must not happen (maybe cannot follow hatena spec)'
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# Does logout if already logined.
|
127
|
+
def logout
|
128
|
+
return unless login?
|
129
|
+
@agent.get("https://www.hatena.ne.jp/logout")
|
130
|
+
account = @current_account
|
131
|
+
@current_account = nil
|
132
|
+
account
|
133
|
+
end
|
134
|
+
|
135
|
+
# Posts an entry to Hatena diary service.
|
136
|
+
#
|
137
|
+
# Raises HatenaDiary::LoginError unless logined.
|
138
|
+
def post(yyyy, mm, dd, title, body, trivial_p = false)
|
139
|
+
edit_page(yyyy, mm, dd, 0) do |form|
|
140
|
+
form["title"] = title
|
141
|
+
form["body"] = body
|
142
|
+
form["trivial"] = "true" if trivial_p
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
# Deletes an entry from Hatena diary service.
|
147
|
+
#
|
148
|
+
# Raises HatenaDiary::LoginError unless logined.
|
149
|
+
def delete(yyyy, mm, dd)
|
150
|
+
edit_page(yyyy, mm, dd, -1)
|
151
|
+
end
|
152
|
+
|
153
|
+
private
|
154
|
+
|
155
|
+
def edit_page(yyyy, mm, dd, form_index)
|
156
|
+
raise LoginError, "not login yet" unless login?
|
157
|
+
response = @agent.get("http://d.hatena.ne.jp/#{@current_account[0]}/edit?date=#{yyyy}#{mm}#{dd}")
|
158
|
+
form = response.forms.fetch(form_index)
|
159
|
+
form["year"] = "%04d" % yyyy
|
160
|
+
form["month"] = "%02d" % mm
|
161
|
+
form["day"] = "%02d" % dd
|
162
|
+
yield(form) if block_given?
|
163
|
+
form.submit
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
@@ -0,0 +1,230 @@
|
|
1
|
+
$LOAD_PATH.unshift File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib'))
|
2
|
+
require 'test/unit'
|
3
|
+
require 'hatenadiary'
|
4
|
+
|
5
|
+
module TU_CommonSetup
|
6
|
+
def common_setup
|
7
|
+
@username = 'HATENA_ID'
|
8
|
+
@password = 'PASSWORD'
|
9
|
+
@agent = TU_MechanizeMock.new
|
10
|
+
TU_MechanizeMock.queue.push @agent
|
11
|
+
TU_MechanizeMock::DUMMY_ACCOUNTS[@username] = @password
|
12
|
+
HatenaDiary::Client.mechanizer = TU_MechanizeMock
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class TC_HatenaDiary < Test::Unit::TestCase
|
17
|
+
include TU_CommonSetup
|
18
|
+
|
19
|
+
def setup
|
20
|
+
common_setup
|
21
|
+
end
|
22
|
+
|
23
|
+
def test_login
|
24
|
+
HatenaDiary.login(@username, @password) do |client|
|
25
|
+
assert client.login?
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class TC_HatenaDiary_Client_class < Test::Unit::TestCase
|
31
|
+
include TU_CommonSetup
|
32
|
+
|
33
|
+
def setup
|
34
|
+
common_setup
|
35
|
+
end
|
36
|
+
|
37
|
+
def test_login
|
38
|
+
HatenaDiary::Client.login(@username, @password) do |client|
|
39
|
+
assert_kind_of HatenaDiary::Client, client
|
40
|
+
assert client.login?
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def test_login_without_block
|
45
|
+
client = HatenaDiary::Client.login(@username, @password)
|
46
|
+
assert_kind_of HatenaDiary::Client, client
|
47
|
+
assert !@agent.proxy
|
48
|
+
end
|
49
|
+
|
50
|
+
def test_login_without_block_with_proxy
|
51
|
+
client = HatenaDiary::Client.login(@username, @password, ['URL', 666])
|
52
|
+
assert_equal ['URL', 666], @agent.proxy
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
class TC_HatenaDiary_Client < Test::Unit::TestCase
|
57
|
+
include TU_CommonSetup
|
58
|
+
|
59
|
+
def setup
|
60
|
+
common_setup
|
61
|
+
@client = HatenaDiary::Client.new(@username, @password)
|
62
|
+
end
|
63
|
+
|
64
|
+
def test_set_proxy
|
65
|
+
@client.set_proxy('URL', 666)
|
66
|
+
assert_equal @agent.proxy, ['URL', 666]
|
67
|
+
end
|
68
|
+
|
69
|
+
def test_login_and_logout
|
70
|
+
assert !@client.login?
|
71
|
+
@client.login
|
72
|
+
assert @client.login?
|
73
|
+
@client.logout
|
74
|
+
assert !@client.login?
|
75
|
+
end
|
76
|
+
|
77
|
+
def test_login_failure
|
78
|
+
TU_MechanizeMock::DUMMY_ACCOUNTS.delete('no_one')
|
79
|
+
begin
|
80
|
+
@client.login 'no_one', 'password?'
|
81
|
+
flunk
|
82
|
+
rescue HatenaDiary::LoginError => ex
|
83
|
+
assert_kind_of HatenaDiary::LoginError, ex
|
84
|
+
assert_equal 'no_one', ex.username
|
85
|
+
assert_equal 'password?', ex.password
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def test_login_if_hatena_changed
|
90
|
+
@agent.with_login_page_result_title 'jumbled page title :-)' do
|
91
|
+
@client.login
|
92
|
+
end
|
93
|
+
flunk
|
94
|
+
rescue Exception => ex
|
95
|
+
assert /must not happen/ =~ ex.message
|
96
|
+
end
|
97
|
+
|
98
|
+
def test_transaction
|
99
|
+
assert !@client.login?
|
100
|
+
@client.transaction do |client|
|
101
|
+
assert_same @client, client
|
102
|
+
assert @client.login?
|
103
|
+
end
|
104
|
+
assert !@client.login?
|
105
|
+
end
|
106
|
+
|
107
|
+
def test_transaction_without_block
|
108
|
+
assert !@client.login?
|
109
|
+
assert_raises LocalJumpError do
|
110
|
+
@client.transaction
|
111
|
+
end
|
112
|
+
assert !@client.login?
|
113
|
+
end
|
114
|
+
|
115
|
+
def test_post
|
116
|
+
@client.transaction do |client|
|
117
|
+
client.post 1234, 5, 6, "TITLE", "BODY\n"
|
118
|
+
end
|
119
|
+
h = @agent.latest_post_form
|
120
|
+
assert_equal "1234", h["year"]
|
121
|
+
assert_equal "05", h["month"]
|
122
|
+
assert_equal "06", h["day"]
|
123
|
+
assert_equal "TITLE", h["title"]
|
124
|
+
assert_equal "BODY\n", h["body"]
|
125
|
+
end
|
126
|
+
|
127
|
+
def test_post_trivial
|
128
|
+
@client.transaction do |client|
|
129
|
+
client.post 2000, 7, 8, "TITLE", "BODY\n", true
|
130
|
+
end
|
131
|
+
h = @agent.latest_post_form
|
132
|
+
assert_equal "edit", h[:form_id]
|
133
|
+
assert_equal "2000", h["year"]
|
134
|
+
assert_equal "07", h["month"]
|
135
|
+
assert_equal "08", h["day"]
|
136
|
+
assert_equal "TITLE", h["title"]
|
137
|
+
assert_equal "BODY\n", h["body"]
|
138
|
+
assert_equal "true", h["trivial"]
|
139
|
+
end
|
140
|
+
|
141
|
+
def test_post_without_login
|
142
|
+
assert_raises HatenaDiary::LoginError do
|
143
|
+
@client.post 1999, 5, 26, "TITLE", "BODY\n"
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
def test_delete
|
148
|
+
@client.transaction do |client|
|
149
|
+
client.delete 1234, 5, 6
|
150
|
+
end
|
151
|
+
h = @agent.latest_post_form
|
152
|
+
assert_equal "delete", h[:form_id]
|
153
|
+
assert_equal "1234", h["year"]
|
154
|
+
assert_equal "05", h["month"]
|
155
|
+
assert_equal "06", h["day"]
|
156
|
+
end
|
157
|
+
|
158
|
+
def test_delete_without_login
|
159
|
+
assert_raises HatenaDiary::LoginError do
|
160
|
+
@client.delete 2009, 8, 30
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
class TU_MechanizeMock
|
166
|
+
def self.queue
|
167
|
+
@queue ||= []
|
168
|
+
end
|
169
|
+
|
170
|
+
def self.new
|
171
|
+
queue.shift or super
|
172
|
+
end
|
173
|
+
|
174
|
+
def set_proxy(url, port)
|
175
|
+
@proxy = [url, port]
|
176
|
+
end
|
177
|
+
|
178
|
+
attr_reader :proxy
|
179
|
+
attr_reader :latest_post_form
|
180
|
+
|
181
|
+
def with_login_page_result_title(title, &block)
|
182
|
+
@preset_title = title
|
183
|
+
yield()
|
184
|
+
ensure
|
185
|
+
@preset_title = nil
|
186
|
+
end
|
187
|
+
|
188
|
+
DUMMY_ACCOUNTS = {}
|
189
|
+
|
190
|
+
def get(url)
|
191
|
+
case url
|
192
|
+
when "https://www.hatena.ne.jp/login"
|
193
|
+
forms = []
|
194
|
+
forms << create_form{|form|
|
195
|
+
form["persistent"] == "true" or throw :test_hatenadiary_mechanizemock_login_form_not_persistent
|
196
|
+
if @preset_title
|
197
|
+
Page.new(@preset_title)
|
198
|
+
else
|
199
|
+
Page.new(DUMMY_ACCOUNTS[form["name"]] == form["password"] ? "Hatena" : "Login - Hatena")
|
200
|
+
end
|
201
|
+
}
|
202
|
+
Page.new(nil, forms)
|
203
|
+
when "https://www.hatena.ne.jp/logout"
|
204
|
+
Page.new
|
205
|
+
when %r[\Ahttp://d.hatena.ne.jp/]
|
206
|
+
id, rest = $'.split('/', 2)
|
207
|
+
addr, query = rest.split('?', 2)
|
208
|
+
forms = [nil, nil, nil]
|
209
|
+
forms.unshift create_form{|form|
|
210
|
+
form[:form_id] = "edit"
|
211
|
+
@latest_post_form = form
|
212
|
+
}
|
213
|
+
forms.push create_form{|form|
|
214
|
+
form[:form_id] = "delete"
|
215
|
+
@latest_post_form = form
|
216
|
+
}
|
217
|
+
Page.new(nil, forms)
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
Page = Struct.new(:title, :forms)
|
222
|
+
|
223
|
+
def create_form(&block)
|
224
|
+
form = { :__submitter__ => block }
|
225
|
+
def form.submit
|
226
|
+
fetch(:__submitter__).call(self)
|
227
|
+
end
|
228
|
+
form
|
229
|
+
end
|
230
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: arikui1911-hatenadiary
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- arikui
|
@@ -35,8 +35,8 @@ files:
|
|
35
35
|
- README
|
36
36
|
- LICENSE
|
37
37
|
- Rakefile
|
38
|
-
- test
|
39
|
-
- lib
|
38
|
+
- test/test_hatenadiary.rb
|
39
|
+
- lib/hatenadiary.rb
|
40
40
|
has_rdoc: false
|
41
41
|
homepage: http://wiki.github.com/arikui1911/hatenadiary
|
42
42
|
post_install_message:
|
@@ -73,4 +73,4 @@ signing_key:
|
|
73
73
|
specification_version: 3
|
74
74
|
summary: It is a library provides a client for Hatena Diary to post and delete blog entries.
|
75
75
|
test_files:
|
76
|
-
- test
|
76
|
+
- test/test_hatenadiary.rb
|