net-irc2 0.0.10

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