net-irc 0.0.5 → 0.0.6

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