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