masuda 0.5.0
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/masuda.rb +342 -0
- metadata +47 -0
data/lib/masuda.rb
ADDED
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
#
|
|
2
|
+
# = masuda.rb
|
|
3
|
+
#
|
|
4
|
+
# Copyright (c) 2007 SUGAWARA Genki <sgwr_dts@yahoo.co.jp>
|
|
5
|
+
#
|
|
6
|
+
# == Example
|
|
7
|
+
#
|
|
8
|
+
# require 'masuda'
|
|
9
|
+
#
|
|
10
|
+
# diary = Masuda::Diary.new
|
|
11
|
+
# diary.entries.each {|entry| puts entry.content }
|
|
12
|
+
# entry = diary.entry('20070712231804')
|
|
13
|
+
# puts <<EOS
|
|
14
|
+
# #{entry.title}
|
|
15
|
+
# #{entry.content}
|
|
16
|
+
# EOS
|
|
17
|
+
# entry.trackbacks.each {|trackback| puts trackback.snippet }
|
|
18
|
+
#
|
|
19
|
+
# diary.login('my_id', 'my_pass')
|
|
20
|
+
# diary.my_entries.each {|entry| puts entry.content }
|
|
21
|
+
# diary.post('Ruby is ...', <<EOS)
|
|
22
|
+
# A dynamic, open source
|
|
23
|
+
# programming language with a
|
|
24
|
+
# ...
|
|
25
|
+
# EOS
|
|
26
|
+
#
|
|
27
|
+
# session[:diary] = diary.raw
|
|
28
|
+
# ...
|
|
29
|
+
# diary = Masuda::Diary.restore(session[:diary])
|
|
30
|
+
#
|
|
31
|
+
|
|
32
|
+
require 'base64'
|
|
33
|
+
require 'cgi'
|
|
34
|
+
require 'digest/md5'
|
|
35
|
+
require 'net/http'
|
|
36
|
+
require 'stringio'
|
|
37
|
+
require 'time'
|
|
38
|
+
|
|
39
|
+
module Masuda
|
|
40
|
+
class Diary
|
|
41
|
+
@@host = 'anond.hatelabo.jp'
|
|
42
|
+
@@login_host = 'www.hatelabo.jp'
|
|
43
|
+
@@user_agent = "RubyMasudaLibrary/#{VERSION}"
|
|
44
|
+
|
|
45
|
+
def initialize
|
|
46
|
+
@cookies = {}
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def self.restore(raw_diary)
|
|
50
|
+
diary = Diary.new
|
|
51
|
+
diary.update(raw_diary)
|
|
52
|
+
diary
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def login(user, pass)
|
|
56
|
+
logout if logined?
|
|
57
|
+
res = request('/login', {:mode => 'enter', :key => user, :password => pass, :autologin => 1}, @@login_host)
|
|
58
|
+
cookies = res.get_fields('set-cookie')
|
|
59
|
+
set_cookie(cookies) if cookies
|
|
60
|
+
@user = user if (retval = logined?)
|
|
61
|
+
retval
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def logout
|
|
65
|
+
@cookies.clear
|
|
66
|
+
@user = nil
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def logined?
|
|
70
|
+
%w(rk b).all? {|k| @cookies.has_key?(k) and @cookies[k].valid? }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def entry(id)
|
|
74
|
+
res = request("/#{id}/")
|
|
75
|
+
return nil unless ('200' == res.code)
|
|
76
|
+
source = res.body.to_a
|
|
77
|
+
et= to_entry(source)
|
|
78
|
+
load_trackbacks(source, self, et)
|
|
79
|
+
et
|
|
80
|
end
|
|
81
|
+
|
|
82
|
+
def trackbacks(id)
|
|
83
|
+
entry(id).trackbacks
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def entries(page = nil)
|
|
87
|
+
path = page ? "/?page=#{page}" : '/'
|
|
88
|
+
res = request(path)
|
|
89
|
+
ets = nil
|
|
90
|
+
|
|
91
|
+
if '200' == res.code
|
|
92
|
+
ets, pages = to_entries(res.body)
|
|
93
|
+
ets.instance_variable_set(:@pages, pages)
|
|
94
|
+
def ets.pages; @pages; end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
ets
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def my_entries(page = nil)
|
|
101
|
+
return nil unless logined?
|
|
102
|
+
path = page ? "/#{@user}/?page=#{page}" : "/#{@user}/"
|
|
103
|
+
res = request(path)
|
|
104
|
+
ets = nil
|
|
105
|
+
|
|
106
|
+
if '200' == res.code
|
|
107
|
+
ets, pages = to_entries(res.body)
|
|
108
|
+
ets.instance_variable_set(:@pages, pages)
|
|
109
|
+
def ets.pages; @pages; end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
ets
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def post(title, content)
|
|
116
|
+
return false unless logined?
|
|
117
|
+
res = request("/#{@user}/edit", {:mode => 'confirm', :rkm => rkm, :id => '', :title => title, :body => content, :edit => "\343\201\223\343\201\256\345\206\205\345\256\271\343\202\222\347\231\273\351\214\262\343\201\231\343\202\213"})
|
|
118
|
+
('302' == res.code)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def raw
|
|
122
|
+
Marshal.dump('user' => @user, 'cookies' => @cookies.values.map {|cookie| cookie.raw })
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def update(raw_diary)
|
|
126
|
+
attrs = Marshal.restore(raw_diary)
|
|
127
|
+
@user = attrs['user']
|
|
128
|
+
set_cookie(attrs['cookies'])
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
private
|
|
132
|
+
def request(path, params = {}, host = @@host)
|
|
133
|
+
Net::HTTP.version_1_2
|
|
134
|
+
|
|
135
|
+
Net::HTTP.start(host, 80) {|http|
|
|
136
|
+
req = Net::HTTP::Post.new(path)
|
|
137
|
+
req['Host'] = host
|
|
138
|
+
req['User-Agent'] = @@user_agent
|
|
139
|
+
req['Cookie'] = @cookies.values.select {|cookie| cookie.apply?(host, path) }.map {|cookie| cookie.header_string }.join('; ')
|
|
140
|
+
|
|
141
|
+
req.body = params.map {|k, v| "#{CGI::escape(k.to_s)}=#{CGI::escape(v.to_s)}" }.join('&')
|
|
142
|
+
http.request(req)
|
|
143
|
+
}
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def set_cookie(cookies)
|
|
147
|
+
cookies.each {|raw_cookie|
|
|
148
|
+
cookie = Cookie.parse(raw_cookie)
|
|
149
|
+
@cookies[cookie.name] = cookie
|
|
150
|
+
}
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def rkm
|
|
154
|
+
return nil unless @cookies.has_key?('rk')
|
|
155
|
+
rk = @cookies['rk'].value
|
|
156
|
+
Base64.encode64(Digest::MD5.digest(rk)).strip.sub(/=+\Z/o, '')
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def to_entry(source, date = '')
|
|
160
|
+
source = source.to_a if source.kind_of?(String)
|
|
161
|
+
|
|
162
|
+
while line = source.shift
|
|
163
|
+
yield(line) if block_given?
|
|
164
|
+
if line['<span class="date">']
|
|
165
|
+
date.replace(%r|<span class="date">(.+)</span>|o.match(line)[1])
|
|
166
|
+
elsif line['<div class="section">']
|
|
167
|
+
header = source.shift
|
|
168
|
+
line = source.shift # skip
|
|
169
|
+
content = StringIO.new
|
|
170
|
+
|
|
171
|
+
until line['<p class="sectionfooter">']
|
|
172
|
+
content << line unless /<a href="[^"]+" class="edit">/o =~ line
|
|
173
|
+
line = source.shift
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
footer = line
|
|
177
|
+
|
|
178
|
+
id, title = %r|<h3><a href="/([0-9]+)"><span class="sanchor">.+</span></a>(.*)?\Z|o.match(header).captures
|
|
179
|
+
title.gsub!('</h3>', '')
|
|
180
|
+
tb_num = %r|トラックバック\((\d+)\)|uo.match(footer)[1].to_i
|
|
181
|
+
time = %r|(\d{2}:\d{2})|.match(footer)[1]
|
|
182
|
+
|
|
183
|
+
return Entry.new(self, id, title, content.string, Time.parse("#{date} #{time}"), tb_num)
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
nil
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def to_entries(source)
|
|
191
|
+
source = source.to_a if source.kind_of?(String)
|
|
192
|
+
ets = []
|
|
193
|
+
date = ''
|
|
194
|
+
pages = nil
|
|
195
|
+
|
|
196
|
+
until source.empty?
|
|
197
|
+
et = to_entry(source, date) {|line|
|
|
198
|
+
if line['<div class="pager-r">'] and line['</div>']
|
|
199
|
+
pages = %r|<div class="pager-r">(\d+)[^<]+</div>|o.match(line)[1]
|
|
200
|
+
end
|
|
201
|
+
}
|
|
202
|
+
ets << et if et
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
[ets, pages]
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def load_trackbacks(source, diary, parent)
|
|
209
|
+
source = source.to_a if source.kind_of?(String)
|
|
210
|
+
tbs = []
|
|
211
|
+
|
|
212
|
+
while (line = source.shift) and not line['</ul>']
|
|
213
|
+
if line['<li>']
|
|
214
|
+
header = source.shift
|
|
215
|
+
line = source.shift until source.shift['<div class="box-curve">']
|
|
216
|
+
line = source.shift # skip
|
|
217
|
+
snippet = StringIO.new
|
|
218
|
+
|
|
219
|
+
until line['<span class="curve-bottom">']
|
|
220
|
+
snippet << line
|
|
221
|
+
line = source.shift
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
m = %r|<a href="http://anond.hatelabo.jp/([0-9]+)"|o.match(header)
|
|
225
|
+
m = %r|<a href="http://d.hatena.ne.jp/([^/]+)/.*"|o.match(header) unless m
|
|
226
|
+
id = m ? m[1] : nil # TODO
|
|
227
|
+
tbs << (tb = Trackback.new(diary, parent, id, snippet.string))
|
|
228
|
+
|
|
229
|
+
until line['</li>']
|
|
230
|
+
load_trackbacks(source, diary, tb) if line['<ul>']
|
|
231
|
+
line = source.shift
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
parent.trackbacks = tbs
|
|
237
|
+
end
|
|
238
|
+
end # Diary
|
|
239
|
+
|
|
240
|
+
class Entry
|
|
241
|
+
attr_reader :diary, :id, :title, :content, :time
|
|
242
|
+
attr_writer :trackbacks
|
|
243
|
+
attr_accessor :tb_num
|
|
244
|
+
|
|
245
|
+
def initialize(diary, id, title, content, time, tb_num)
|
|
246
|
+
@diary = diary
|
|
247
|
+
@id = id
|
|
248
|
+
@title = title
|
|
249
|
+
@content = content
|
|
250
|
+
@time = time
|
|
251
|
+
@tb_num = tb_num
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def trackbacks
|
|
255
|
+
@trackbacks or @trackbacks = diary.trackbacks(@id)
|
|
256
|
+
end
|
|
257
|
+
end # Entry
|
|
258
|
+
|
|
259
|
+
class Trackback
|
|
260
|
+
attr_reader :diary, :parent, :id, :snippet
|
|
261
|
+
attr_accessor :trackbacks
|
|
262
|
+
|
|
263
|
+
def initialize(diary, parent, id, snippet)
|
|
264
|
+
@diary = diary
|
|
265
|
+
@parent = parent
|
|
266
|
+
@id = id
|
|
267
|
+
@snippet = snippet
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def entry
|
|
271
|
+
@entry or @entry = @diary.entry(@id)
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def tb_num
|
|
275
|
+
@trackbacks ? @trackbacks.length : 0
|
|
276
|
+
end
|
|
277
|
+
end # Trackback
|
|
278
|
+
|
|
279
|
+
class Cookie
|
|
280
|
+
attr_reader :name, :value, :expires, :domain, :path, :secure
|
|
281
|
+
|
|
282
|
+
def initialize(source = {})
|
|
283
|
+
%w(name value expires domain path secure).each {|k|
|
|
284
|
+
next unless source.has_key?(k)
|
|
285
|
+
instance_variable_set("@#{k}", source[k])
|
|
286
|
+
}
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def valid?(now = Time.now)
|
|
290
|
+
not @expires or (now <= @expires)
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def apply?(domain, path, secure = true, now = Time.now)
|
|
294
|
+
valid?(now) and
|
|
295
|
+
(domain.slice(-@domain.length, @domain.length) == @domain) and
|
|
296
|
+
(path.slice(0, @path.length) == @path) and
|
|
297
|
+
(not @secure or secure)
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def header_string
|
|
301
|
+
"#{CGI::escape(@name)}=#{CGI::escape(@value)}"
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def raw
|
|
305
|
+
attrs = []
|
|
306
|
+
attrs << "#{@name}=#{@value}"
|
|
307
|
+
attrs << "expires=#{@expires.rfc2822}" if @expires
|
|
308
|
+
attrs << "domain=#{@domain}" if @domain
|
|
309
|
+
attrs << "path=#{@path}" if @path
|
|
310
|
+
attrs << "secure=#{@secure}" if @secure
|
|
311
|
+
attrs.join('; ')
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def self.parse(raw_cookie)
|
|
315
|
+
source = {}
|
|
316
|
+
|
|
317
|
+
raw_cookie.split(/;\s?/o).each {|pair|
|
|
318
|
+
key, value = pair.split('=', 2)
|
|
319
|
+
next unless key and value
|
|
320
|
+
key = CGI::unescape(key)
|
|
321
|
+
value = CGI::unescape(value)
|
|
322
|
+
|
|
323
|
+
case key
|
|
324
|
+
when 'expires'
|
|
325
|
+
source['expires'] = Time.parse(value)
|
|
326
|
+
when 'domain'
|
|
327
|
+
source['domain'] = value
|
|
328
|
+
when 'path'
|
|
329
|
+
source['path'] = value
|
|
330
|
+
when 'secure'
|
|
331
|
+
source['secure'] = ('true' == value.downcase)
|
|
332
|
+
else
|
|
333
|
+
unless source.has_key?('name')
|
|
334
|
+
source['name'] = key
|
|
335
|
+
source['value'] = value
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
Cookie.new(source)
|
|
341
|
+
end
|
|
342
|
+
end # Cookie
|
|
343
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
rubygems_version: 0.9.4
|
|
3
|
+
specification_version: 1
|
|
4
|
+
name: masuda
|
|
5
|
+
version: !ruby/object:Gem::Version
|
|
6
|
+
version: 0.5.0
|
|
7
|
+
date: 2007-10-18 00:00:00 +09:00
|
|
8
|
+
summary: Hatena AnonymouseDiary Reader/Writer
|
|
9
|
+
require_paths:
|
|
10
|
+
- lib
|
|
11
|
+
email: sgwr_dts@yahoo.co.jp
|
|
12
|
+
homepage: http://masuda.rubyforge.org
|
|
13
|
+
rubyforge_project: masuda
|
|
14
|
+
description: Hatena AnonymouseDiary Reader/Writer
|
|
15
|
+
autorequire:
|
|
16
|
+
default_executable:
|
|
17
|
+
bindir: bin
|
|
18
|
+
has_rdoc: true
|
|
19
|
+
required_ruby_version: !ruby/object:Gem::Version::Requirement
|
|
20
|
+
requirements:
|
|
21
|
+
- - ">"
|
|
22
|
+
- !ruby/object:Gem::Version
|
|
23
|
+
version: 0.0.0
|
|
24
|
+
version:
|
|
25
|
+
platform: ruby
|
|
26
|
+
signing_key:
|
|
27
|
+
cert_chain:
|
|
28
|
+
post_install_message:
|
|
29
|
+
authors:
|
|
30
|
+
- winebarrel
|
|
31
|
+
files:
|
|
32
|
+
- lib/masuda.rb
|
|
33
|
+
test_files: []
|
|
34
|
+
|
|
35
|
+
rdoc_options:
|
|
36
|
+
- --main
|
|
37
|
+
- README.txt
|
|
38
|
+
extra_rdoc_files: []
|
|
39
|
+
|
|
40
|
+
executables: []
|
|
41
|
+
|
|
42
|
+
extensions: []
|
|
43
|
+
|
|
44
|
+
requirements: []
|
|
45
|
+
|
|
46
|
+
dependencies: []
|
|
47
|
+
|