net-irc 0.0.5 → 0.0.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: