net-irc2 0.0.10

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/examples/hig.rb ADDED
@@ -0,0 +1,771 @@
1
+ #!/usr/bin/env ruby
2
+ # vim:encoding=UTF-8:
3
+ =begin
4
+ # hig.rb
5
+
6
+ ## Launch
7
+
8
+ $ ruby hig.rb
9
+
10
+ If you want to help:
11
+
12
+ $ ruby hig.rb --help
13
+
14
+ ## Configuration
15
+
16
+ Options specified by after irc realname.
17
+
18
+ Configuration example for Tiarra ( http://coderepos.org/share/wiki/Tiarra ).
19
+
20
+ haiku {
21
+ host: localhost
22
+ port: 16679
23
+ name: username@example.com athack jabber=username@example.com:jabberpasswd tid=10 ratio=10:3:5
24
+ password: password on Haiku
25
+ in-encoding: utf8
26
+ out-encoding: utf8
27
+ }
28
+
29
+ ### athack
30
+
31
+ If `athack` client option specified,
32
+ all nick in join message is leading with @.
33
+
34
+ So if you complemente nicks (e.g. Irssi),
35
+ it's good for Twitter like reply command (@nick).
36
+
37
+ In this case, you will see torrent of join messages after connected,
38
+ because NAMES list can't send @ leading nick (it interpreted op.)
39
+
40
+ ### tid=<color>
41
+
42
+ Apply id to each message for make favorites by CTCP ACTION.
43
+
44
+ /me fav id
45
+
46
+ <color> can be
47
+
48
+ 0 => white
49
+ 1 => black
50
+ 2 => blue navy
51
+ 3 => green
52
+ 4 => red
53
+ 5 => brown maroon
54
+ 6 => purple
55
+ 7 => orange olive
56
+ 8 => yellow
57
+ 9 => lightgreen lime
58
+ 10 => teal
59
+ 11 => lightcyan cyan aqua
60
+ 12 => lightblue royal
61
+ 13 => pink lightpurple fuchsia
62
+ 14 => grey
63
+ 15 => lightgrey silver
64
+
65
+
66
+ ### jabber=<jid>:<pass>
67
+
68
+ If `jabber=<jid>:<pass>` option specified,
69
+ use jabber to get friends timeline.
70
+
71
+ You must setup im notifing settings in the site and
72
+ install "xmpp4r-simple" gem.
73
+
74
+ $ sudo gem install xmpp4r-simple
75
+
76
+ Be careful for managing password.
77
+
78
+ ### alwaysim
79
+
80
+ Use IM instead of any APIs (e.g. post)
81
+
82
+ ### ratio=<timeline>:<friends>:<channel>
83
+
84
+ ## License
85
+
86
+ Ruby's by cho45
87
+
88
+ =end
89
+
90
+ $LOAD_PATH << "lib"
91
+ $LOAD_PATH << "../lib"
92
+
93
+ $KCODE = "u" unless defined? ::Encoding # json use this
94
+
95
+ require "rubygems"
96
+ require "net/irc"
97
+ require "net/http"
98
+ require "uri"
99
+ require "json"
100
+ require "socket"
101
+ require "time"
102
+ require "logger"
103
+ require "yaml"
104
+ require "pathname"
105
+ require "cgi"
106
+ require "digest/md5"
107
+
108
+ Net::HTTP.version_1_2
109
+
110
+ class HaikuIrcGateway < Net::IRC::Server::Session
111
+ def server_name
112
+ "haikugw"
113
+ end
114
+
115
+ def server_version
116
+ "0.0.0"
117
+ end
118
+
119
+ def main_channel
120
+ "#haiku"
121
+ end
122
+
123
+ def api_base
124
+ URI(ENV["HAIKU_BASE"] || "http://h.hatena.ne.jp/api/")
125
+ end
126
+
127
+ def api_source
128
+ "hig.rb"
129
+ end
130
+
131
+ def jabber_bot_id
132
+ nil
133
+ end
134
+
135
+ def hourly_limit
136
+ 60
137
+ end
138
+
139
+ class ApiFailed < StandardError; end
140
+
141
+ def initialize(*args)
142
+ super
143
+ @channels = {}
144
+ @user_agent = "#{self.class}/#{server_version} (hig.rb)"
145
+ @counters = {} # for jabber fav
146
+ end
147
+
148
+ def on_user(m)
149
+ super
150
+ post @prefix, JOIN, main_channel
151
+ post server_name, MODE, main_channel, "+o", @prefix.nick
152
+
153
+ @real, *@opts = @opts.name || @real.split(/\s+/)
154
+ @opts = @opts.inject({}) {|r,i|
155
+ key, value = i.split("=")
156
+ r.update(key => value)
157
+ }
158
+ @tmap = TypableMap.new
159
+
160
+ if @opts["jabber"]
161
+ jid, pass = @opts["jabber"].split(":", 2)
162
+ @opts["jabber"].replace("jabber=#{jid}:********")
163
+ if jabber_bot_id
164
+ begin
165
+ require "xmpp4r-simple"
166
+ start_jabber(jid, pass)
167
+ rescue LoadError
168
+ log "Failed to start Jabber."
169
+ log 'Installl "xmpp4r-simple" gem or check your id/pass.'
170
+ finish
171
+ end
172
+ else
173
+ @opts.delete("jabber")
174
+ log "This gateway does not support Jabber bot."
175
+ end
176
+ end
177
+
178
+ log "Client Options: #{@opts.inspect}"
179
+ @log.info "Client Options: #{@opts.inspect}"
180
+
181
+ timeline_ratio, friends_ratio, channel_ratio = (@opts["ratio"] || "10:3:5").split(":").map {|ratio| ratio.to_i }
182
+ footing = (timeline_ratio + friends_ratio + channel_ratio).to_f
183
+
184
+ @timeline = []
185
+ @check_follows_thread = Thread.start do
186
+ loop do
187
+ begin
188
+ check_friends
189
+ check_keywords
190
+ rescue ApiFailed => e
191
+ @log.error e.inspect
192
+ rescue Exception => e
193
+ @log.error e.inspect
194
+ e.backtrace.each do |l|
195
+ @log.error "\t#{l}"
196
+ end
197
+ end
198
+ sleep freq(friends_ratio / footing)
199
+ end
200
+ end
201
+
202
+ return if @opts["jabber"]
203
+
204
+ @check_timeline_thread = Thread.start do
205
+ sleep 10
206
+ loop do
207
+ begin
208
+ check_timeline
209
+ rescue ApiFailed => e
210
+ @log.error e.inspect
211
+ rescue Exception => e
212
+ @log.error e.inspect
213
+ e.backtrace.each do |l|
214
+ @log.error "\t#{l}"
215
+ end
216
+ end
217
+ sleep freq(timeline_ratio / footing)
218
+ end
219
+ end
220
+ end
221
+
222
+ def on_disconnected
223
+ @check_follows_thread.kill rescue nil
224
+ @check_timeline_thread.kill rescue nil
225
+ @im_thread.kill rescue nil
226
+ @im.disconnect rescue nil
227
+ end
228
+
229
+ def on_privmsg(m)
230
+ return m.ctcps.each {|ctcp| on_ctcp(m[0], ctcp) } if m.ctcp?
231
+ retry_count = 3
232
+ ret = nil
233
+ target, message = *m.params
234
+ begin
235
+ channel = target.sub(/^#/, "")
236
+ reply = message[/\s+>(.+)$/, 1]
237
+ if !reply && @opts.key?("alwaysim") && @im && @im.connected? # in jabber mode, using jabber post
238
+ message = "##{channel} #{message}" unless "##{channel}" == main_channel
239
+ ret = @im.deliver(jabber_bot_id, message)
240
+ post "#{nick}!#{nick}@#{api_base.host}", TOPIC, channel, message
241
+ else
242
+ channel = "" if "##{channel}" == main_channel
243
+ rid = rid_for(reply) if reply
244
+ if @typo
245
+ log "typo mode. requesting..."
246
+ message.gsub!(/\\n/, "\n")
247
+ file = Net::HTTP.get(URI("http://lab.lowreal.net/test/haiku.rb/?text=" + URI.escape(message)))
248
+ ret = api("statuses/update", {"file" => file, "in_reply_to_status_id" => rid, "keyword" => channel})
249
+ else
250
+ ret = api("statuses/update", {"status" => "id:#{@real}=#{message}", "in_reply_to_status_id" => rid, "keyword" => channel})
251
+ end
252
+ log "Status Updated via API"
253
+ end
254
+ raise ApiFailed, "API failed" unless ret
255
+ check_timeline
256
+ rescue => e
257
+ @log.error [retry_count, e.message, e.inspect, e.backtrace].inspect
258
+ if retry_count > 0
259
+ retry_count -= 1
260
+ @log.debug "Retry to setting status..."
261
+ # retry
262
+ else
263
+ log "Some Error Happened on Sending #{message}. #{e}"
264
+ end
265
+ end
266
+ end
267
+
268
+ def on_ctcp(target, message)
269
+ _, command, *args = message.split(/\s+/)
270
+ case command
271
+ when "list"
272
+ nick = args[0]
273
+ @log.debug([ nick, message ])
274
+ res = api("statuses/user_timeline", { "id" => nick }).reverse_each do |s|
275
+ @log.debug(s)
276
+ post nick, NOTICE, main_channel, s
277
+ end
278
+
279
+ unless res
280
+ post nil, ERR_NOSUCHNICK, nick, "No such nick/channel"
281
+ end
282
+ when "fav"
283
+ target = args[0]
284
+ st = @tmap[target]
285
+ id = rid_for(target)
286
+ if st || id
287
+ unless id
288
+ if @im && @im.connected?
289
+ # IM のときはいろいろめんどうなことする
290
+ nick, count = *st
291
+ pos = @counters[nick] - count
292
+ @log.debug "%p %s %d/%d => %d" % [
293
+ st,
294
+ nick,
295
+ count,
296
+ @counters[nick],
297
+ pos
298
+ ]
299
+ res = api("statuses/user_timeline", { "id" => nick })
300
+ raise ApiFailed, "#{nick} may be private mode" if res.empty?
301
+ if res[pos]
302
+ id = res[pos]["id"]
303
+ else
304
+ raise ApiFailed, "#{pos} of #{nick} is not found."
305
+ end
306
+ else
307
+ id = st["id"]
308
+ end
309
+ end
310
+ res = api("favorites/create/#{id}", {})
311
+ post nil, NOTICE, main_channel, "Fav: #{res["screen_name"]}: #{res["text"].gsub(URI.regexp(%w|http https|), "http...")}"
312
+ else
313
+ post nil, NOTICE, main_channel, "No such id or status #{target}"
314
+ end
315
+ when "link"
316
+ tid = args[0]
317
+ st = @tmap[tid]
318
+ if st
319
+ st["link"] = (api_base + "/#{st["user"]["screen_name"]}/#{st["id"]}").to_s unless st["link"]
320
+ post nil, NOTICE, main_channel, st["link"]
321
+ else
322
+ post nil, NOTICE, main_channel, "No such id #{tid}"
323
+ end
324
+ when "typo"
325
+ @typo = !@typo
326
+ post nil, NOTICE, main_channel, "typo mode: #{@typo}"
327
+ end
328
+ rescue ApiFailed => e
329
+ log e.inspect
330
+ end; private :on_ctcp
331
+
332
+ def on_whois(m)
333
+ nick = m.params[0]
334
+ f = (@friends || []).find {|i| i["screen_name"] == nick }
335
+ if f
336
+ post nil, RPL_WHOISUSER, @nick, nick, nick, api_base.host, "*", "#{f["name"]} / #{f["description"]}"
337
+ post nil, RPL_WHOISSERVER, @nick, nick, api_base.host, api_base.to_s
338
+ post nil, RPL_WHOISIDLE, @nick, nick, "0", "seconds idle"
339
+ post nil, RPL_ENDOFWHOIS, @nick, nick, "End of WHOIS list"
340
+ else
341
+ post nil, ERR_NOSUCHNICK, nick, "No such nick/channel"
342
+ end
343
+ end
344
+
345
+ def on_join(m)
346
+ return ### なんかしらんけど何度も入ってしまってうざいので……
347
+ channels = m.params[0].split(/\s*,\s*/)
348
+ channels.each do |channel|
349
+ next if channel == main_channel
350
+ begin
351
+ api("keywords/create/#{URI.escape(channel.sub(/^#/, ""))}")
352
+ @channels[channel] = {
353
+ :read => []
354
+ }
355
+ post "#{@nick}!#{@nick}@#{api_base.host}", JOIN, channel
356
+ rescue => e
357
+ @log.debug e.inspect
358
+ post nil, ERR_NOSUCHNICK, nick, "No such nick/channel"
359
+ end
360
+ end
361
+ end
362
+
363
+ def on_part(m)
364
+ channel = m.params[0]
365
+ return if channel == main_channel
366
+ @channels.delete(channel)
367
+ api("keywords/destroy/#{URI.escape(channel.sub(/^#/, ""))}")
368
+ post "#{@nick}!#{@nick}@#{api_base.host}", PART, channel
369
+ end
370
+
371
+ def on_who(m)
372
+ channel = m.params[0]
373
+ case
374
+ when channel == main_channel
375
+ # "<channel> <user> <host> <server> <nick>
376
+ # ( "H" / "G" > ["*"] [ ( "@" / "+" ) ]
377
+ # :<hopcount> <real name>"
378
+ @friends.each do |f|
379
+ user = nick = f["screen_name"]
380
+ host = serv = api_base.host
381
+ real = f["name"]
382
+ post nil, RPL_WHOREPLY, @nick, channel, user, host, serv, nick, "H*@", "0 #{real}"
383
+ end
384
+ post nil, RPL_ENDOFWHO, @nick, channel
385
+ when @groups.key?(channel)
386
+ @groups[channel].each do |name|
387
+ f = @friends.find {|i| i["screen_name"] == name }
388
+ user = nick = f["screen_name"]
389
+ host = serv = api_base.host
390
+ real = f["name"]
391
+ post nil, RPL_WHOREPLY, @nick, channel, user, host, serv, nick, "H*@", "0 #{real}"
392
+ end
393
+ post nil, RPL_ENDOFWHO, @nick, channel
394
+ else
395
+ post nil, ERR_NOSUCHNICK, @nick, nick, "No such nick/channel"
396
+ end
397
+ end
398
+
399
+ private
400
+ def check_timeline
401
+ api("statuses/friends_timeline").reverse_each do |s|
402
+ begin
403
+ id = s["id"]
404
+ next if id.nil? || @timeline.include?(id)
405
+ @timeline << id
406
+ nick = s["user"]["id"]
407
+ mesg = generate_status_message(s)
408
+
409
+ tid = @tmap.push(s)
410
+
411
+ @log.debug [id, nick, mesg]
412
+
413
+ channel = "##{s["keyword"]}"
414
+ case
415
+ when s["keyword"].match(/^id:/)
416
+ channel = main_channel
417
+ when !@channels.keys.include?(channel)
418
+ channel = main_channel
419
+ mesg = "%s = %s" % [s["keyword"], mesg]
420
+ end
421
+
422
+ if nick == @nick # 自分のときは topic に
423
+ post "#{nick}!#{nick}@#{api_base.host}", TOPIC, channel, mesg
424
+ else
425
+ if @opts["tid"]
426
+ message(nick, channel, "%s \x03%s [%s]" % [mesg, @opts["tid"], tid])
427
+ else
428
+ message(nick, channel, "%s" % [mesg, tid])
429
+ end
430
+
431
+ if @opts.key?("metadata")
432
+ post "metadata", NOTICE, channel, JSON.generate({ "uri" => (api_base + "/#{s["user"]["screen_name"]}/#{s["id"]}").to_s })
433
+ end
434
+ end
435
+ rescue => e
436
+ @log.debug "Error: %p" % e
437
+ end
438
+ end
439
+ @log.debug "@timeline.size = #{@timeline.size}"
440
+ @timeline = @timeline.last(100)
441
+ end
442
+
443
+ def generate_status_message(s)
444
+ mesg = s["text"]
445
+ mesg.sub!("#{s["keyword"]}=", "") unless s["keyword"] =~ /^id:/
446
+ mesg << " > #{s["in_reply_to_user_id"]}" unless s["in_reply_to_user_id"].empty?
447
+
448
+ @log.debug(mesg)
449
+ mesg
450
+ end
451
+
452
+ def check_friends
453
+ first = true unless @friends
454
+ @friends ||= []
455
+ friends = api("statuses/friends")
456
+ if first && !@opts.key?("athack")
457
+ @friends = friends
458
+ post nil, RPL_NAMREPLY, @nick, "=", main_channel, @friends.map{|i| "@#{i["screen_name"]}" }.join(" ")
459
+ post nil, RPL_ENDOFNAMES, @nick, main_channel, "End of NAMES list"
460
+ else
461
+ prv_friends = @friends.map {|i| i["screen_name"] }
462
+ now_friends = friends.map {|i| i["screen_name"] }
463
+
464
+ # Twitter API bug?
465
+ return if !first && (now_friends.length - prv_friends.length).abs > 10
466
+
467
+ (now_friends - prv_friends).each do |join|
468
+ join = "@#{join}" if @opts.key?("athack")
469
+ post "#{join}!#{join}@#{api_base.host}", JOIN, main_channel
470
+ end
471
+ (prv_friends - now_friends).each do |part|
472
+ part = "@#{part}" if @opts.key?("athack")
473
+ post "#{part}!#{part}@#{api_base.host}", PART, main_channel, ""
474
+ end
475
+ @friends = friends
476
+ end
477
+ end
478
+
479
+ def check_keywords
480
+ keywords = api("statuses/keywords").map {|i| "##{i["title"]}" }
481
+ current = @channels.keys
482
+ current.delete main_channel
483
+
484
+ (current - keywords).each do |part|
485
+ @channels.delete(part)
486
+ post "#{@nick}!#{@nick}@#{api_base.host}", PART, part
487
+ end
488
+
489
+ (keywords - current).each do |join|
490
+ @channels[join] = {
491
+ :read => []
492
+ }
493
+ post "#{@nick}!#{@nick}@#{api_base.host}", JOIN, join
494
+ end
495
+ end
496
+
497
+ def freq(ratio)
498
+ ret = 3600 / (hourly_limit * ratio).round
499
+ @log.debug "Frequency: #{ret}"
500
+ ret
501
+ end
502
+
503
+ def start_jabber(jid, pass)
504
+ @log.info "Logging-in with #{jid} -> jabber_bot_id: #{jabber_bot_id}"
505
+ @im = Jabber::Simple.new(jid, pass)
506
+ @im.add(jabber_bot_id)
507
+ @im_thread = Thread.start do
508
+ loop do
509
+ begin
510
+ @im.received_messages.each do |msg|
511
+ @log.debug [msg.from, msg.body]
512
+ if msg.from.strip == jabber_bot_id
513
+ # Haiku -> 'nick(id): msg'
514
+ body = msg.body.sub(/^(.+?)(?:\((.+?)\))?: /, "")
515
+ if Regexp.last_match
516
+ nick, id = Regexp.last_match.captures
517
+ body = CGI.unescapeHTML(body)
518
+
519
+ case
520
+ when nick == "投稿完了"
521
+ log "#{nick}: #{body}"
522
+ when nick == "チャンネル投稿完了"
523
+ log "#{nick}: #{body}"
524
+ when body =~ /^#([a-z_]+)\s+(.+)$/i
525
+ # channel message or not
526
+ message(id || nick, "##{Regexp.last_match[1]}", Regexp.last_match[2])
527
+ when nick == "photo" && body =~ %r|^http://haiku\.jp/user/([^/]+)/|
528
+ nick = Regexp.last_match[1]
529
+ message(nick, main_channel, body)
530
+ else
531
+ @counters[nick] ||= 0
532
+ @counters[nick] += 1
533
+ tid = @tmap.push([nick, @counters[nick]])
534
+ message(nick, main_channel, "%s \x03%s [%s]" % [body, @opts["tid"], tid])
535
+ end
536
+ end
537
+ end
538
+ end
539
+ rescue Exception => e
540
+ @log.error "Error on Jabber loop: #{e.inspect}"
541
+ e.backtrace.each do |l|
542
+ @log.error "\t#{l}"
543
+ end
544
+ end
545
+ sleep 1
546
+ end
547
+ end
548
+ end
549
+
550
+ def require_post?(path)
551
+ [
552
+ %r|/update|,
553
+ %r|/create|,
554
+ %r|/destroy|,
555
+ ].any? {|i| i === path }
556
+ end
557
+
558
+ def api(path, q={})
559
+ ret = {}
560
+ q["source"] ||= api_source
561
+
562
+ uri = api_base.dup
563
+ uri.path = "/api/#{path}.json"
564
+ uri.query = q.inject([]) {|r,(k,v)| v ? r << "#{k}=#{URI.escape(v, /[^:,-.!~*'()\w]/n)}" : r }.join("&")
565
+
566
+
567
+ req = nil
568
+ if require_post?(path)
569
+ req = Net::HTTP::Post.new(uri.path)
570
+ if q["file"]
571
+ boundary = (rand(0x1_00_00_00_00_00) + 0x1_00_00_00_00_00).to_s(16)
572
+ @log.info boundary
573
+ req["content-type"] = "multipart/form-data; boundary=#{boundary}"
574
+
575
+ body = ""
576
+ q.each do |k, v|
577
+ body << "--#{boundary}\r\n"
578
+ if k == "file"
579
+ body << "Content-Disposition: form-data; name=\"#{k}\"; filename=\"temp.png\";\r\n"
580
+ body << "Content-Transfer-Encoding: binary\r\n"
581
+ body << "Content-Type: image/png\r\n"
582
+ else
583
+ body << "Content-Disposition: form-data; name=\"#{k}\";\r\n"
584
+ end
585
+ body << "\r\n"
586
+ body << v.to_s
587
+ body << "\r\n"
588
+ end
589
+ body << "--#{boundary}--\r\n"
590
+
591
+ req.body = body
592
+ uri.query = ""
593
+ else
594
+ req.body = uri.query
595
+ end
596
+ else
597
+ req = Net::HTTP::Get.new(uri.request_uri)
598
+ end
599
+ req.basic_auth(@real, @pass)
600
+ req["User-Agent"] = @user_agent
601
+ req["If-Modified-Since"] = q["since"] if q.key?("since")
602
+
603
+ @log.debug uri.inspect
604
+ ret = Net::HTTP.start(uri.host, uri.port) { |http| http.request(req) }
605
+
606
+ case ret
607
+ when Net::HTTPOK # 200
608
+ ret = JSON.parse(ret.body.gsub(/:'/, ':"').gsub(/',/, '",').gsub(/'(y(?:es)?|no?|true|false|null)'/, '"\1"'))
609
+ raise ApiFailed, "Server Returned Error: #{ret["error"]}" if ret.kind_of?(Hash) && ret["error"]
610
+ ret
611
+ when Net::HTTPNotModified # 304
612
+ []
613
+ when Net::HTTPBadRequest # 400
614
+ # exceeded the rate limitation
615
+ raise ApiFailed, "#{ret.code}: #{ret.message}"
616
+ else
617
+ raise ApiFailed, "Server Returned #{ret.code} #{ret.message}"
618
+ end
619
+ rescue Errno::ETIMEDOUT, JSON::ParserError, IOError, Timeout::Error, Errno::ECONNRESET => e
620
+ raise ApiFailed, e.inspect
621
+ end
622
+
623
+ def message(sender, target, str)
624
+ sender = "#{sender}!#{sender}@#{api_base.host}"
625
+ post sender, PRIVMSG, target, str.gsub(/\s+/, " ")
626
+ end
627
+
628
+ def log(str)
629
+ str.gsub!(/\n/, " ")
630
+ post server_name, NOTICE, main_channel, str
631
+ end
632
+
633
+ # return rid of most recent matched status with text
634
+ def rid_for(text)
635
+ target = Regexp.new(Regexp.quote(text.strip), "i")
636
+ status = api("statuses/friends_timeline").find {|i|
637
+ next false if i["user"]["name"] == @nick # 自分は除外
638
+ i["text"] =~ target
639
+ }
640
+
641
+ @log.debug "Looking up status contains #{text.inspect} -> #{status.inspect}"
642
+ status ? status["id"] : nil
643
+ end
644
+
645
+ class TypableMap < Hash
646
+ Roman = %w[
647
+ k g ky gy s z sh j t d ch n ny h b p hy by py m my y r ry w v q
648
+ ].unshift("").map do |consonant|
649
+ case consonant
650
+ when "y", /\A.{2}/ then %w|a u o|
651
+ when "q" then %w|a i e o|
652
+ else %w|a i u e o|
653
+ end.map {|vowel| "#{consonant}#{vowel}" }
654
+ end.flatten
655
+
656
+ def initialize(size = 1)
657
+ @seq = Roman
658
+ @n = 0
659
+ @size = size
660
+ end
661
+
662
+ def generate(n)
663
+ ret = []
664
+ begin
665
+ n, r = n.divmod(@seq.size)
666
+ ret << @seq[r]
667
+ end while n > 0
668
+ ret.reverse.join
669
+ end
670
+
671
+ def push(obj)
672
+ id = generate(@n)
673
+ self[id] = obj
674
+ @n += 1
675
+ @n %= @seq.size ** @size
676
+ id
677
+ end
678
+ alias << push
679
+
680
+ def clear
681
+ @n = 0
682
+ super
683
+ end
684
+
685
+ private :[]=
686
+ undef update, merge, merge!, replace
687
+ end
688
+
689
+
690
+ end
691
+
692
+ if __FILE__ == $0
693
+ require "optparse"
694
+
695
+ opts = {
696
+ :port => 16679,
697
+ :host => "localhost",
698
+ :log => nil,
699
+ :debug => false,
700
+ :foreground => false,
701
+ }
702
+
703
+ OptionParser.new do |parser|
704
+ parser.instance_eval do
705
+ self.banner = <<-EOB.gsub(/^\t+/, "")
706
+ Usage: #{$0} [opts]
707
+
708
+ EOB
709
+
710
+ separator ""
711
+
712
+ separator "Options:"
713
+ on("-p", "--port [PORT=#{opts[:port]}]", "port number to listen") do |port|
714
+ opts[:port] = port
715
+ end
716
+
717
+ on("-h", "--host [HOST=#{opts[:host]}]", "host name or IP address to listen") do |host|
718
+ opts[:host] = host
719
+ end
720
+
721
+ on("-l", "--log LOG", "log file") do |log|
722
+ opts[:log] = log
723
+ end
724
+
725
+ on("--debug", "Enable debug mode") do |debug|
726
+ opts[:log] = $stdout
727
+ opts[:debug] = true
728
+ end
729
+
730
+ on("-f", "--foreground", "run foreground") do |foreground|
731
+ opts[:log] = $stdout
732
+ opts[:foreground] = true
733
+ end
734
+
735
+ on("-n", "--name [user name or email address]") do |name|
736
+ opts[:name] = name
737
+ end
738
+
739
+ parse!(ARGV)
740
+ end
741
+ end
742
+
743
+ opts[:logger] = Logger.new(opts[:log], "daily")
744
+ opts[:logger].level = opts[:debug] ? Logger::DEBUG : Logger::INFO
745
+
746
+ # def daemonize(foreground=false)
747
+ # trap("SIGINT") { exit! 0 }
748
+ # trap("SIGTERM") { exit! 0 }
749
+ # trap("SIGHUP") { exit! 0 }
750
+ # return yield if $DEBUG || foreground
751
+ # Process.fork do
752
+ # Process.setsid
753
+ # Dir.chdir "/"
754
+ # File.open("/dev/null") {|f|
755
+ # STDIN.reopen f
756
+ # STDOUT.reopen f
757
+ # STDERR.reopen f
758
+ # }
759
+ # yield
760
+ # end
761
+ # exit! 0
762
+ # end
763
+
764
+ # daemonize(opts[:debug] || opts[:foreground]) do
765
+ Net::IRC::Server.new(opts[:host], opts[:port], HaikuIrcGateway, opts).start
766
+ # end
767
+ end
768
+
769
+ # Local Variables:
770
+ # coding: utf-8
771
+ # End: