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.
Files changed (2) hide show
  1. data/lib/masuda.rb +342 -0
  2. 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
+