root/lang/ruby/net-irc/trunk/examples/tig.rb

Revision 34249, 54.7 kB (checked in by drry, 45 hours ago)
  • (tig.rb) Twitter の制限緩和に合わせて既定の API 利用比率を変更しました。(20090702T1025+09)
  • (tig.rb) 配置換え。大クラス、いろいろカオスってきた。
  • Property svn:executable set to *
  • Property svn:keywords set to Revision
Line 
1#!/usr/bin/env ruby
2# vim:fileencoding=UTF-8:
3$KCODE = "u" if RUBY_VERSION < "1.9" # json use this
4=begin
5
6# tig.rb
7
8Ruby version of TwitterIrcGateway
9<http://www.misuzilla.org/dist/net/twitterircgateway/>
10
11## Launch
12
13        $ ruby tig.rb
14
15If you want to help:
16
17        $ ruby tig.rb --help
18
19## Configuration
20
21Options specified by after IRC realname.
22
23Configuration example for Tiarra <http://coderepos.org/share/wiki/Tiarra>.
24
25        general {
26                server-in-encoding: utf8
27                server-out-encoding: utf8
28                client-in-encoding: utf8
29                client-out-encoding: utf8
30        }
31
32        networks {
33                name: tig
34        }
35
36        tig {
37                server: localhost 16668
38                password: password on Twitter
39                # Recommended
40                name: username mentions tid
41
42                # Same as TwitterIrcGateway.exe.config.sample
43                #   (90, 360 and 300 seconds)
44                #name: username dm ratio=4:1 maxlimit=50
45                #name: username dm ratio=20:5:6 maxlimit=62 mentions
46                #
47                # <http://cheebow.info/chemt/archives/2009/04/posttwit.html>
48                #   (60, 360 and 150 seconds)
49                #name: username dm ratio=30:5:12 maxlimit=94 mentions
50                #
51                # <http://cheebow.info/chemt/archives/2009/07/api150rhtwit.html>
52                #   (36, 360 and 150 seconds)
53                #name: username dm ratio=50:5:12 maxlimit=134 mentions
54                #
55                # for Jabber
56                #name: username jabber=username@example.com:jabberpasswd
57        }
58
59### athack
60
61If `athack` client option specified,
62all nick in join message is leading with @.
63
64So if you complemente nicks (e.g. Irssi),
65it's good for Twitter like reply command (@nick).
66
67In this case, you will see torrent of join messages after connected,
68because NAMES list can't send @ leading nick (it interpreted op.)
69
70### tid[=<color:10>[,<bgcolor>]]
71
72Apply ID to each message for make favorites by CTCP ACTION.
73
74        /me fav [ID...]
75
76<color> and <bgcolor> can be
77
78        0  => white
79        1  => black
80        2  => blue         navy
81        3  => green
82        4  => red
83        5  => brown        maroon
84        6  => purple
85        7  => orange       olive
86        8  => yellow
87        9  => lightgreen   lime
88        10 => teal
89        11 => lightcyan    cyan aqua
90        12 => lightblue    royal
91        13 => pink         lightpurple fuchsia
92        14 => grey
93        15 => lightgrey    silver
94
95### jabber=<jid>:<pass>
96
97If `jabber=<jid>:<pass>` option specified,
98use Jabber to get friends timeline.
99
100You must setup im notifing settings in the site and
101install "xmpp4r-simple" gem.
102
103        $ sudo gem install xmpp4r-simple
104
105Be careful for managing password.
106
107### alwaysim
108
109Use IM instead of any APIs (e.g. post)
110
111### ratio=<timeline>:<dm>[:<mentions>]
112
113"121:6:20" by default.
114
115        /me ratios
116
117           Ratio | Timeline |   DM  | Mentions |
118        ---------+----------+-------+----------|
119               1 |      24s |   N/A |      N/A |
120           141:6 |      26s |   10m OR N/A     |
121          135:12 |      27s |    5m OR N/A     |
122         135:6:6 |      27s |   10m |      10m |
123        ---------+----------+-------+----------|
124        121:6:20 |      30s |   10m |       3m |
125        ---------+----------+-------+----------|
126             4:1 |      31s |  2m1s |      N/A |
127         50:5:12 |      49s | 8m12s |    3m25s |
128          20:5:6 |      57s | 3m48s |    3m10s |
129         30:5:12 |      58s | 5m45s |    2m24s |
130           1:1:1 |    1m13s | 1m13s |    1m13s |
131        ---------------------------------------+
132                            (Hourly limit: 150)
133
134### dm[=<ratio>]
135
136### mentions[=<ratio>]
137
138### maxlimit=<hourly_limit>
139
140### clientspoofing
141
142### httpproxy=[<user>[:<password>]@]<address>[:<port>]
143
144### main_channel=<channel:#twitter>
145
146### api_source=<source>
147
148### max_params_count=<number:3>
149
150### check_friends_interval=<seconds:3600>
151
152### check_updates_interval=<seconds:86400>
153
154Set 0 to disable checking.
155
156### old_style_reply
157
158### tmap_size=<number:10404>
159
160### strftime=<format:%m-%d %H:%M>
161
162### untiny_whole_urls
163
164### bitlify=<username>:<apikey>:<minlength:20>
165
166### unuify
167
168### shuffled_tmap
169
170## Extended commands through the CTCP ACTION
171
172### list (ls)
173
174        /me list NICK [NUMBER]
175
176### fav (favorite, favourite, unfav, unfavorite, unfavourite)
177
178        /me fav [ID...]
179        /me unfav [ID...]
180        /me fav! [ID...]
181        /me fav NICK
182
183### link (ln)
184
185        /me link ID [ID...]
186
187### destroy (del, delete, miss, oops, remove, rm)
188
189        /me destroy [ID...]
190
191### in (location)
192
193        /me in Sugamo, Tokyo, Japan
194
195### reply (re, mention)
196
197        /me reply ID blah, blah...
198
199### utf7 (utf-7)
200
201        /me utf7
202
203### name
204
205        /me name My Name
206
207### description (desc)
208
209        /me description blah, blah...
210
211### spoof
212
213        /me spoof
214        /me spoo[o...]f
215        /me spoof tigrb twitterircgateway twitt web mobileweb
216
217### bot (drone)
218
219        /me bot NICK [NICK...]
220
221## Feed
222
223<http://coderepos.org/share/log/lang/ruby/net-irc/trunk/examples/tig.rb?limit=100&mode=stop_on_copy&format=rss>
224
225## License
226
227Ruby's by cho45
228
229=end
230
231if File.directory? "lib"
232        $LOAD_PATH << "lib"
233elsif File.directory? File.expand_path("lib", "..")
234        $LOAD_PATH << File.expand_path("lib", "..")
235end
236
237require "rubygems"
238require "net/irc"
239require "net/https"
240require "uri"
241require "time"
242require "logger"
243require "yaml"
244require "pathname"
245require "ostruct"
246require "json"
247
248begin
249        require "iconv"
250        require "punycode"
251rescue LoadError
252end
253
254module Net::IRC::Constants; RPL_WHOISBOT = "335"; RPL_CREATEONTIME = "329"; end
255
256class TwitterIrcGateway < Net::IRC::Server::Session
257        def server_name
258                "twittergw"
259        end
260
261        def server_version
262                rev = %q$Revision$.split[1]
263                rev &&= "+r#{rev}"
264                "0.0.0#{rev}"
265        end
266
267        def available_user_modes
268                "o"
269        end
270
271        def available_channel_modes
272                "mnti"
273        end
274
275        def main_channel
276                @opts.main_channel || "#twitter"
277        end
278
279        def api_base(secure = true)
280                URI("http#{"s" if secure}://twitter.com/")
281        end
282
283        def api_source
284                "#{@opts.api_source || "tigrb"}"
285        end
286
287        def jabber_bot_id
288                "twitter@twitter.com"
289        end
290
291        def hourly_limit
292                150
293        end
294
295        class APIFailed < StandardError; end
296
297        def initialize(*args)
298                super
299                @groups        = {}
300                @channels      = [] # joined channels (groups)
301                @nicknames     = {}
302                @drones        = []
303                @config        = Pathname.new(ENV["HOME"]) + ".tig"
304                @etags         = {}
305                @consums       = []
306                @limit         = hourly_limit
307                @friends       =
308                @sources       =
309                @rsuffix_regex =
310                @im            =
311                @im_thread     =
312                @utf7          =
313                @httpproxy     = nil
314                load_config
315        end
316
317        def on_user(m)
318                super
319
320                @real, *@opts = (@opts.name || @real).split(" ")
321                @opts = @opts.inject({}) do |r, i|
322                        key, value = i.split("=", 2)
323                        key = "mentions" if key == "replies" # backcompat
324                        r.update key => case value
325                                when nil                      then true
326                                when /\A\d+\z/                then value.to_i
327                                when /\A(?:\d+\.\d*|\.\d+)\z/ then value.to_f
328                                else                               value
329                        end
330                end
331                @opts = OpenStruct.new(@opts)
332                @opts.httpproxy.sub!(/\A(?:([^:@]+)(?::([^@]+))?@)?([^:]+)(?::(\d+))?\z/) do
333                        @httpproxy = OpenStruct.new({
334                                :user => $1, :password => $2, :address => $3, :port => $4.to_i,
335                        })
336                        $&.sub(/[^:@]+(?=@)/, "********")
337                end if @opts.httpproxy
338
339                retry_count = 0
340                begin
341                        @me = api("account/update_profile") #api("account/verify_credentials")
342                rescue APIFailed => e
343                        @log.error e.inspect
344                        sleep 1
345                        retry_count += 1
346                        retry if retry_count < 3
347                        log "Failed to access API 3 times." <<
348                            " Please check your username/email and password combination, " <<
349                            " Twitter Status <http://status.twitter.com/> and try again later."
350                        finish
351                end
352
353                @prefix = prefix(@me)
354                @user   = @prefix.user
355                @host   = @prefix.host
356
357                #post NICK, @me.screen_name if @nick != @me.screen_name
358                post server_name, MODE, @nick, "+o"
359                post @prefix, JOIN, main_channel
360                post server_name, MODE, main_channel, "+mto", @nick
361                if @me.status
362                        @me.status.user = @me
363                        post @prefix, TOPIC, main_channel, generate_status_message(@me.status.text)
364                end
365
366                if @opts.jabber
367                        jid, pass = @opts.jabber.split(":", 2)
368                        @opts.jabber.replace("jabber=#{jid}:********")
369                        if jabber_bot_id
370                                begin
371                                        require "xmpp4r-simple"
372                                        start_jabber(jid, pass)
373                                rescue LoadError
374                                        log "Failed to start Jabber."
375                                        log 'Installl "xmpp4r-simple" gem or check your ID/pass.'
376                                        finish
377                                end
378                        else
379                                @opts.delete_field :jabber
380                                log "This gateway does not support Jabber bot."
381                        end
382                end
383
384                log "Client options: #{@opts.marshal_dump.inspect}"
385                @log.info "Client options: #{@opts.inspect}"
386
387                @opts.tid = begin
388                        c = @opts.tid # expect: 0..15, true, "0,1"
389                        b = nil
390                        c, b = c.split(",", 2).map {|i| i.to_i } if c.respond_to? :split
391                        c = 10 unless (0 .. 15).include? c # 10: teal
392                        if (0 .. 15).include?(b)
393                                "\003%.2d,%.2d[%%s]\017" % [c, b]
394                        else
395                                "\003%.2d[%%s]\017"      % c
396                        end
397                end if @opts.tid
398
399                @ratio = (@opts.ratio || "121").split(":")
400                @ratio = Struct.new(:timeline, :dm, :mentions).new(*@ratio)
401                @ratio.dm       ||= @opts.dm == true ? @opts.mentions ?  6 : 26 : @opts.dm
402                @ratio.mentions ||= @opts.mentions == true ? @opts.dm ? 20 : 26 : @opts.mentions
403
404                @check_friends_thread = Thread.start do
405                        loop do
406                                begin
407                                        check_friends
408                                rescue APIFailed => e
409                                        @log.error e.inspect
410                                rescue Exception => e
411                                        @log.error e.inspect
412                                        e.backtrace.each do |l|
413                                                @log.error "\t#{l}"
414                                        end
415                                end
416                                sleep @opts.check_friends_interval || 3600
417                        end
418                end
419
420                return if @opts.jabber
421
422                @timeline = TypableMap.new(@opts.tmap_size     || 10_404,
423                                           @opts.shuffled_tmap || false)
424                @sources  = @opts.clientspoofing ? fetch_sources : [[api_source, "tig.rb"]]
425
426                update_redundant_suffix
427                @check_updates_thread = Thread.start do
428                        sleep @opts.check_updates_interval || 86400
429
430                        loop do
431                                begin
432                                        check_updates
433                                rescue Exception => e
434                                        @log.error e.inspect
435                                        e.backtrace.each do |l|
436                                                @log.error "\t#{l}"
437                                        end
438                                end
439                                sleep 0.01 * (90 + rand(21)) *
440                                      (@opts.check_updates_interval || 86400) # 0.9 ... 1.1 day
441                        end
442                end if @opts.check_updates_interval != 0
443
444                @check_timeline_thread = Thread.start do
445                        sleep 2 * (@me.friends_count / 100.0).ceil
446
447                        loop do
448                                begin
449                                        check_timeline
450                                rescue APIFailed => e
451                                        @log.error e.inspect
452                                rescue Exception => e
453                                        @log.error e.inspect
454                                        e.backtrace.each do |l|
455                                                @log.error "\t#{l}"
456                                        end
457                                end
458                                sleep interval(@ratio.timeline)
459                        end
460                end
461
462                @check_dms_thread = Thread.start do
463                        loop do
464                                begin
465                                        check_direct_messages
466                                rescue APIFailed => e
467                                        @log.error e.inspect
468                                rescue Exception => e
469                                        @log.error e.inspect
470                                        e.backtrace.each do |l|
471                                                @log.error "\t#{l}"
472                                        end
473                                end
474                                sleep interval(@ratio.dm)
475                        end
476                end if @opts.dm
477
478                @check_mentions_thread = Thread.start do
479                        sleep interval(@ratio.timeline) / 2
480
481                        loop do
482                                begin
483                                        check_mentions
484                                rescue APIFailed => e
485                                        @log.error e.inspect
486                                rescue Exception => e
487                                        @log.error e.inspect
488                                        e.backtrace.each do |l|
489                                                @log.error "\t#{l}"
490                                        end
491                                end
492                                sleep interval(@ratio.mentions)
493                        end
494                end if @opts.mentions
495        end
496
497        def on_disconnected
498                @check_friends_thread.kill  rescue nil
499                @check_timeline_thread.kill rescue nil
500                @check_mentions_thread.kill rescue nil
501                @check_dms_thread.kill      rescue nil
502                @check_updates_thread.kill  rescue nil
503                @im_thread.kill             rescue nil
504                @im.disconnect              rescue nil
505        end
506
507        def on_privmsg(m)
508                target, mesg = *m.params
509
510                m.ctcps.each {|ctcp| on_ctcp target, ctcp } if m.ctcp?
511
512                return if mesg.empty?
513                return on_ctcp_action(target, mesg) if mesg.sub!(/\A +/, "") #and @opts.direct_action
514
515                command, params = mesg.split(" ", 2)
516                case command.downcase # TODO: escape recursive
517                when "d", "dm"
518                        screen_name, mesg = params.split(" ", 2)
519                        unless screen_name or mesg
520                                log 'Send "d NICK message" to send a direct (private) message.' <<
521                                    " You may reply to a direct message the same way."
522                                return
523                        end
524                        m.params[0] = screen_name.sub(/\A@/, "")
525                        m.params[1] = mesg #.rstrip
526                        return on_privmsg(m)
527                # TODO
528                #when "f", "follow"
529                #when "on"
530                #when "off" # BUG if no args
531                #when "g", "get"
532                #when "w", "whois"
533                #when "n", "nudge" # BUG if no args
534                #when "*", "fav"
535                #when "delete"
536                #when "stats" # no args
537                #when "leave"
538                #when "invite"
539                end unless command.nil?
540
541                mesg = escape_http_urls(mesg)
542                mesg = @opts.unuify ? unuify(mesg) : bitlify(mesg)
543                mesg = Iconv.iconv("UTF-7", "UTF-8", mesg).join.encoding!("ASCII-8BIT") if @utf7
544
545                ret         = nil
546                retry_count = 3
547                begin
548                        case
549                        when target.ch?
550                                if @opts.alwaysim and @im and @im.connected? # in Jabber mode, using Jabber post
551                                        ret = @im.deliver(jabber_bot_id, mesg)
552                                        post @prefix, TOPIC, main_channel, mesg
553                                else
554                                        previous = @me.status
555                                        if previous and
556                                           ((Time.now - Time.parse(previous.created_at)).to_i < 60 rescue true) and
557                                           mesg.strip == previous.text
558                                                log "You can't submit the same status twice in a row."
559                                                return
560                                        end
561
562                                        in_reply_to = nil
563                                        if @opts.old_style_reply and mesg[/\A@(?>([A-Za-z0-9_]{1,15}))[^A-Za-z0-9_]/]
564                                                screen_name = $1
565                                                unless user = friend(screen_name)
566                                                        user = api("users/show/#{screen_name}")
567                                                end
568                                                if user and user.status
569                                                        in_reply_to = user.status.id
570                                                elsif user
571                                                        user = api("users/show/#{user.id}", {}, { :authenticate => user.protected })
572                                                        in_reply_to = user.status.id if user.status
573                                                end
574                                        end
575
576                                        q = { :status => mesg, :source => source }
577                                        q.update(:in_reply_to_status_id => in_reply_to) if in_reply_to
578                                        ret = api("statuses/update", q)
579                                        log oops(ret) if ret.truncated
580                                        ret.user.status = ret
581                                        @me = ret.user
582                                        log "Status updated"
583                                end
584                        when target.nick? # Direct message
585                                ret = api("direct_messages/new", { :screen_name => target, :text => mesg })
586                                post server_name, NOTICE, @nick, "Your direct message has been sent to #{target}."
587                        else
588                                post server_name, ERR_NOSUCHNICK, target, "No such nick/channel"
589                        end
590                rescue => e
591                        @log.error [retry_count, e.inspect].inspect
592                        if retry_count > 0
593                                retry_count -= 1
594                                @log.debug "Retry to setting status..."
595                                retry
596                        end
597                        log "Some Error Happened on Sending #{mesg}. #{e}"
598                end
599        end
600
601        def on_whois(m)
602                nick = m.params[0]
603                unless nick.nick?
604                        post server_name, ERR_NOSUCHNICK, nick, "No such nick/channel"
605                        return
606                end
607
608                unless user = user(nick)
609                        if api("users/username_available", { :username => nick }).valid
610                        # TODO: 404 suspended
611                                post server_name, ERR_NOSUCHNICK, nick, "No such nick/channel"
612                                return
613                        end
614                        user = api("users/show/#{nick}", {}, { :authenticate => false })
615                end
616
617                prefix    = prefix(user)
618                desc      = user.name
619                desc      = "#{desc} / #{user.description}".gsub(/\s+/, " ") if user.description and not user.description.empty?
620                signon_at = Time.parse(user.created_at).to_i rescue 0
621                idle_sec  = (Time.now - (user.status ? Time.parse(user.status.created_at) : signon_at)).to_i rescue 0
622                location  = user.location
623                location  = "SoMa neighborhood of San Francisco, CA" if location.nil? or location.empty?
624                post server_name, RPL_WHOISUSER,   @nick, nick, prefix.user, prefix.host, "*", desc
625                post server_name, RPL_WHOISSERVER, @nick, nick, api_base.host, location
626                post server_name, RPL_WHOISIDLE,   @nick, nick, "#{idle_sec}", "#{signon_at}", "seconds idle, signon time"
627                post server_name, RPL_ENDOFWHOIS,  @nick, nick, "End of WHOIS list"
628                if @drones.include?(user.id)
629                        post server_name, RPL_WHOISBOT, @nick, nick, "is a \002Bot\002 on #{server_name}"
630                end
631        end
632
633        def on_who(m)
634                channel = m.params[0]
635                case
636                when channel.casecmp(main_channel).zero?
637                        users = [@me]
638                        users.concat @friends.reverse if @friends
639                        users.each {|friend| whoreply channel, friend }
640                        post server_name, RPL_ENDOFWHO, @nick, channel
641                when (@groups.key?(channel) and @friends)
642                        @groups[channel].each do |nick|
643                                whoreply channel, friend(nick)
644                        end
645                        post server_name, RPL_ENDOFWHO, @nick, channel
646                else
647                        post server_name, ERR_NOSUCHNICK, @nick, "No such nick/channel"
648                end
649        end
650
651        def on_join(m)
652                channels = m.params[0].split(/ *, */)
653                channels.each do |channel|
654                        channel = channel.split(" ", 2).first
655                        next if channel.casecmp(main_channel).zero?
656
657                        @channels << channel
658                        @channels.uniq!
659                        post @prefix, JOIN, channel
660                        post server_name, MODE, channel, "+mtio", @nick
661                        save_config
662                end
663        end
664
665        def on_part(m)
666                channel = m.params[0]
667                return if channel.casecmp(main_channel).zero?
668
669                @channels.delete(channel)
670                post @prefix, PART, channel, "Ignore group #{channel}, but setting is alive yet."
671        end
672
673        def on_invite(m)
674                nick, channel = *m.params
675                if not nick.nick? or @nick.casecmp(nick).zero?
676                        post server_name, ERR_NOSUCHNICK, nick, "No such nick/channel" # or yourself
677                        return
678                end
679
680                friend = friend(nick)
681
682                case
683                when channel.casecmp(main_channel).zero?
684                        case
685                        when friend #TODO
686                        when api("users/username_available", { :username => nick }).valid
687                                post server_name, ERR_NOSUCHNICK, nick, "No such nick/channel"
688                        else
689                                user = api("friendships/create/#{nick}")
690                                join main_channel, [user]
691                                @friends << user if @friends
692                                @me.friends_count += 1
693                        end
694                when friend
695                        ((@groups[channel] ||= []) << friend.screen_name).uniq!
696                        join channel, [friend]
697                        save_config
698                else
699                        post server_name, ERR_NOSUCHNICK, nick, "No such nick/channel"
700                end
701        end
702
703        def on_kick(m)
704                channel, nick, msg = *m.params
705
706                if channel.casecmp(main_channel).zero?
707                        @friends.delete_if do |friend|
708                                if friend.screen_name.casecmp(nick).zero?
709                                        user = api("friendships/destroy/#{friend.id}")
710                                        if user.is_a? User
711                                                post prefix(user), PART, main_channel, "Removed: #{msg}"
712                                                @me.friends_count -= 1
713                                        end
714                                end
715                        end if @friends
716                else
717                        friend = friend(nick)
718                        if friend
719                                (@groups[channel] ||= []).delete(friend.screen_name)
720                                post prefix(friend), PART, channel, "Removed: #{msg}"
721                                save_config
722                        else
723                                post server_name, ERR_NOSUCHNICK, nick, "No such nick/channel"
724                        end
725                end
726        end
727
728        #def on_nick(m)
729        #       @nicknames[@nick] = m.params[0]
730        #end
731
732        def on_topic(m)
733                channel = m.params[0]
734                return if not channel.casecmp(main_channel).zero? or @me.status.nil?
735
736                begin
737                        require "levenshtein"
738                        topic    = m.params[1]
739                        previous = @me.status
740                        return unless previous
741
742                        distance = Levenshtein.normalized_distance(previous.text, topic)
743                        return if distance.zero?
744
745                        status = api("statuses/update", { :status => topic, :source => source })
746                        log oops(ret) if status.truncated
747                        status.user.status = status
748                        @me = status.user
749
750                        if distance < 0.5
751                                deleted = api("statuses/destroy/#{previous.id}")
752                                @timeline.delete_if {|tid, s| s.id == deleted.id }
753                                log "Fixed: #{status.text}"
754                        else
755                                log "Status updated"
756                        end
757                rescue LoadError
758                end
759        end
760
761        def on_mode(m)
762                channel = m.params[0]
763
764                unless m.params[1]
765                        if channel.ch?
766                                mode = "+mt"
767                                mode += "i" unless channel.casecmp(main_channel).zero?
768                                post server_name, RPL_CHANNELMODEIS, @nick, channel, mode
769                                #post server_name, RPL_CREATEONTIME, @nick, channel, 0
770                        elsif channel.casecmp(@nick).zero?
771                                post server_name, RPL_UMODEIS, @nick, @nick, "+o"
772                        end
773                end
774        end
775
776        private
777        def on_ctcp(target, mesg)
778                type, mesg = mesg.split(" ", 2)
779                method = "on_ctcp_#{type.downcase}".to_sym
780                send(method, target, mesg) if respond_to? method, true
781        end
782
783        def on_ctcp_action(target, mesg)
784                #return unless main_channel.casecmp(target).zero?
785                command, *args = mesg.split(" ")
786                case command.downcase
787                when "call"
788                        if args.size < 2
789                                log "/me call <Twitter_screen_name> as <IRC_nickname>"
790                                return
791                        end
792                        screen_name = args[0]
793                        nickname    = args[2] || args[1] # allow omitting "as"
794                        if nickname == "is" and
795                           deleted_nick = @nicknames.delete(screen_name)
796                                log %Q{Removed the nickname "#{deleted_nick}" for #{screen_name}}
797                        else
798                                @nicknames[screen_name] = nickname
799                                log "Call #{screen_name} as #{nickname}"
800                        end
801                        #save_config
802                when /\Autf-?7\z/
803                        unless defined? ::Iconv
804                                log "Can't load iconv."
805                                return
806                        end
807                        @utf7 = !@utf7
808                        log "UTF-7 mode: #{@utf7 ? 'on' : 'off'}"
809                when "list", "ls"
810                        if args.empty?
811                                log "/me list <NICK> [<NUM>]"
812                                return
813                        end
814                        nick = args.first
815                        if not nick.nick? or
816                           api("users/username_available", { :username => nick }).valid
817                                post server_name, ERR_NOSUCHNICK, nick, "No such nick/channel"
818                                return
819                        end
820                        id           = nick
821                        authenticate = false
822                        if user = friend(nick)
823                                id           = user.id
824                                nick         = user.screen_name
825                                authenticate = user.protected
826                        end
827                        unless (1..200).include?(count = args[1].to_i)
828                                count = 20
829                        end
830                        begin
831                                res = api("statuses/user_timeline/#{id}",
832                                          { :count => count }, { :authenticate => authenticate })
833                        rescue APIFailed
834                                #log "#{nick} has protected their updates."
835                                return
836                        end
837                        res.reverse_each do |s|
838                                message(s, target, nil, nil, NOTICE)
839                        end
840                when /\A(un)?fav(?:ou?rite)?(!)?\z/
841                # fav, unfav, favorite, unfavorite, favourite, unfavourite
842                        method   = $1.nil? ? "create" : "destroy"
843                        force    = !!$2
844                        entered  = $&.capitalize
845                        statuses = []
846                        if args.empty?
847                                if method == "create"
848                                        if status = @timeline.last
849                                                statuses << status
850                                        else
851                                                #log ""
852                                                return
853                                        end
854                                else
855                                        @favorites ||= api("favorites").reverse
856                                        if @favorites.empty?
857                                                log "You've never favorite yet. No favorites to unfavorite."
858                                                return
859                                        end
860                                        statuses.push @favorites.last
861                                end
862                        else
863                                args.each do |tid_or_nick|
864                                        case
865                                        when status = @timeline[tid_or_nick]
866                                                statuses.push status
867                                        when friend = friend(tid_or_nick)
868                                                if friend.status
869                                                        statuses.push friend.status
870                                                else
871                                                        log "#{tid_or_nick} has no status."
872                                                end
873                                        else
874                                                # PRIVMSG: fav nick
875                                                log "No such ID/NICK #{@opts.tid % tid_or_nick}"
876                                        end
877                                end
878                        end
879                        @favorites ||= []
880                        statuses.each do |s|
881                                if not force and method == "create" and
882                                   @favorites.find {|i| i.id == s.id }
883                                        log "The status is already favorited! <#{permalink(s)}>"
884                                        next
885                                end
886                                res = api("favorites/#{method}/#{s.id}")
887                                log "#{entered}: #{res.user.screen_name}: #{generate_status_message(res.text)}"
888                                if method == "create"
889                                        @favorites.push res
890                                else
891                                        @favorites.delete_if {|i| i.id == res.id }
892                                end
893                        end
894                when "link", "ln"
895                        args.each do |tid|
896                                if status = @timeline[tid]
897                                        log "#{@opts.tid % tid}: #{permalink(status)}"
898                                else
899                                        log "No such ID #{@opts.tid % tid}"
900                                end
901                        end
902                when /\Aratios?\z/
903                        unless args.empty?
904                                args = args.first.split(":") if args.size == 1
905                                if @opts.dm and @opts.mentions and args.size < 3
906                                        log "/me ratios <timeline> <dm> <mentions>"
907                                        return
908                                elsif @opts.dm and args.size < 2
909                                        log "/me ratios <timeline> <dm>"
910                                        return
911                                elsif @opts.mentions and args.size < 2
912                                        log "/me ratios <timeline> <mentions>"
913                                        return
914                                end
915                                ratios = args.map {|ratio| ratio.to_f }
916                                if ratios.any? {|ratio| ratio <= 0.0 }
917                                        log "Ratios must be greater than 0.0 and fractional values are permitted."
918                                        return
919                                end
920                                @ratio.timeline = ratios[0]
921                                if @opts.dm
922                                        @ratio.dm       = ratios[1]
923                                        @ratio.mentions = ratios[2] if @opts.mentions
924                                elsif @opts.mentions
925                                        @ratio.mentions = ratios[1]
926                                end
927                        end
928                        log "Intervals: #{@ratio.map {|ratio| interval ratio }.inspect}"
929                when /\A(?:de(?:stroy|l(?:ete)?)|miss|oops|r(?:emove|m))\z/
930                # destroy, delete, del, remove, rm, miss, oops
931                        statuses = []
932                        if args.empty? and @me.status
933                                statuses.push @me.status
934                        else
935                                args.each do |tid|
936                                        if status = @timeline[tid]
937                                                if status.user.id == @me.id
938                                                        statuses.push status
939                                                else
940                                                        log "The status you specified by the ID #{@opts.tid % tid} is not yours."
941                                                end
942                                        else
943                                                log "No such ID #{@opts.tid % tid}"
944                                        end
945                                end
946                        end
947                        b = false
948                        statuses.each do |st|
949                                res = api("statuses/destroy/#{st.id}")
950                                @timeline.delete_if {|tid, s| s.id == res.id }
951                                b = @me.status && @me.status.id == res.id
952                                log "Destroyed: #{res.text}"
953                        end
954                        Thread.start do
955                                sleep 2
956                                @me = api("account/update_profile") #api("account/verify_credentials")
957                                if @me.status
958                                        @me.status.user = @me
959                                        msg = generate_status_message(@me.status.text)
960                                        @timeline.any? do |tid, s|
961                                                if s.id == @me.status.id
962                                                        msg << " " << @opts.tid % tid
963                                                end
964                                        end
965                                        post @prefix, TOPIC, main_channel, msg
966                                end
967                        end if b
968                when "name"
969                        name = mesg.split(" ", 2)[1]
970                        unless name.nil?
971                                @me = api("account/update_profile", { :name => name })
972                                @me.status.user = @me if @me.status
973                                log "You are named #{@me.name}."
974                        end
975                when "email"
976                        # FIXME
977                        email = args.first
978                        unless email.nil?
979                                @me = api("account/update_profile", { :email => email })
980                                @me.status.user = @me if @me.status
981                        end
982                when "url"
983                        # FIXME
984                        url = args.first || ""
985                        @me = api("account/update_profile", { :url => url })
986                        @me.status.user = @me if @me.status
987                when "in", "location"
988                        location = mesg.split(" ", 2)[1] || ""
989                        @me = api("account/update_profile", { :location => location })
990                        @me.status.user = @me if @me.status
991                        location = (@me.location and @me.location.empty?) ? "nowhere" : "in #{@me.location}"
992                        log "You are #{location} now."
993                when /\Adesc(?:ription)?\z/
994                        # FIXME
995                        description = mesg.split(" ", 2)[1] || ""
996                        @me = api("account/update_profile", { :description => description })
997                        @me.status.user = @me if @me.status
998                #when /\Acolou?rs?\z/ # TODO
999                #       # bg, text, link, fill and border
1000                #when "image", "img" # TODO
1001                #       url = args.first
1002                #       # DCC SEND
1003                #when "follow"# TODO
1004                #when "leave" # TODO
1005                when /\A(?:mention|re(?:ply)?)\z/ # reply, re, mention
1006                        tid = args.first
1007                        if status = @timeline[tid]
1008                                text = mesg.split(" ", 3)[2]
1009                                screen_name = "@#{status.user.screen_name}"
1010                                if text.nil? or not text.include?(screen_name)
1011                                        text = "#{screen_name} #{text}"
1012                                end
1013                                ret = api("statuses/update", { :status => text, :source => source,
1014                                                               :in_reply_to_status_id => status.id })
1015                                log oops(ret) if ret.truncated
1016                                msg = generate_status_message(status.text)
1017                                url = permalink(status)
1018                                log "Status updated (In reply to #{@opts.tid % tid}: #{msg} <#{url}>)"
1019                                ret.user.status = ret
1020                                @me = ret.user
1021                        end
1022                when /\Aspoo(o+)?f\z/
1023                        Thread.start do
1024                                @sources = args.empty? \
1025                                         ? @sources.size == 1 || $1 ? fetch_sources($1 && $1.size) \
1026                                                                    : [[api_source, "tig.rb"]] \
1027                                         : args.map {|src| [src.upcase != "WEB" ? src : "", "=#{src}"] }
1028                                log @sources.map {|src| src[1] }.sort.join(", ")
1029                        end
1030                when "bot", "drone"
1031                        if args.empty?
1032                                log "/me bot <NICK> [<NICK>...]"
1033                                return
1034                        end
1035                        args.each do |bot|
1036                                user = friend(bot)
1037                                unless user
1038                                        post server_name, ERR_NOSUCHNICK, bot, "No such nick/channel"
1039                                        next
1040                                end
1041                                if @drones.delete(user.id)
1042                                        mode = "-#{mode}"
1043                                        log "#{bot} is no longer a bot."
1044                                else
1045                                        @drones << user.id
1046                                        mode = "+#{mode}"
1047                                        log "Marks #{bot} as a bot."
1048                                end
1049                        end
1050                        save_config
1051                when "home", "h"
1052                        if args.empty?
1053                                log "/me home <NICK>"
1054                                return
1055                        end
1056                        nick = args.first
1057                        if not nick.nick? or
1058                           api("users/username_available", { :username => nick }).valid
1059                                post server_name, ERR_NOSUCHNICK, nick, "No such nick/channel"
1060                                return
1061                        end
1062                        log "http://twitter.com/#{nick}"
1063                end unless command.nil?
1064        rescue APIFailed => e
1065                log e.inspect
1066        end
1067
1068        def on_ctcp_clientinfo(target, msg)
1069                if user = user(target)
1070                        post prefix(user), NOTICE, @nick, ctcp_encode("CLIENTINFO :CLIENTINFO USERINFO VERSION TIME")
1071                end
1072        end
1073
1074        def on_ctcp_userinfo(target, msg)
1075                user = user(target)
1076                if user and not user.description.empty?
1077                        post prefix(user), NOTICE, @nick, ctcp_encode("USERINFO :#{user.description}")
1078                end
1079        end
1080
1081        def on_ctcp_version(target, msg)
1082                user = user(target)
1083                if user and user.status
1084                        source = user.status.source
1085                        version = source.gsub(/<[^>]*>/, "").strip
1086                        version << " <#{$1}>" if / href="([^"]+)/ === source
1087                        post prefix(user), NOTICE, @nick, ctcp_encode("VERSION :#{version}")
1088                end
1089        end
1090
1091        def on_ctcp_time(target, msg)
1092                if user = user(target)
1093                        offset = user.utc_offset
1094                        post prefix(user), NOTICE, @nick, ctcp_encode("TIME :%s%s (%s)" % [
1095                                (Time.now + offset).utc.iso8601[0, 19],
1096                                "%+.2d:%.2d" % (offset/60).divmod(60),
1097                                user.time_zone,
1098                        ])
1099                end
1100        end
1101
1102        def check_friends
1103                if @friends.nil?
1104                        @friends = page("statuses/friends/#{@me.id}", @me.friends_count)
1105                        if @opts.athack
1106                                join main_channel, @friends
1107                        else
1108                                rest = @friends.map do |i|
1109                                        prefix = "+" #@drones.include?(i.id) ? "%" : "+" # FIXME ~&%
1110                                        "#{prefix}#{i.screen_name}"
1111                                end.reverse.inject("@#{@nick}") do |r, nick|
1112                                        if r.size < 400
1113                                                r << " " << nick
1114                                        else
1115                                                post server_name, RPL_NAMREPLY, @nick, "=", main_channel, r
1116                                                nick
1117                                        end
1118                                end
1119                                post server_name, RPL_NAMREPLY, @nick, "=", main_channel, rest
1120                                post server_name, RPL_ENDOFNAMES, @nick, main_channel, "End of NAMES list"
1121                        end
1122                else
1123                        new_ids    = page("friends/ids/#{@me.id}", @me.friends_count)
1124                        friend_ids = @friends.reverse.map {|friend| friend.id }
1125
1126                        (friend_ids - new_ids).each do |id|
1127                                @friends.delete_if do |friend|
1128                                        if friend.id == id
1129                                                post prefix(friend), PART, main_channel, ""
1130                                                @me.friends_count -= 1
1131                                        end
1132                                end
1133                        end
1134
1135                        new_ids -= friend_ids
1136                        unless new_ids.empty?
1137                                new_friends = page("statuses/friends/#{@me.id}", new_ids.size)
1138                                join main_channel, new_friends.delete_if {|friend|
1139                                        @friends.any? {|i| i.id == friend.id }
1140                                }.reverse
1141                                @friends.concat new_friends
1142                                @me.friends_count += new_friends.size
1143                        end
1144                end
1145        end
1146
1147        def check_timeline
1148                cmd = PRIVMSG
1149                q   = { :count => 200 }
1150                if @latest_id ||= nil
1151                        q.update(:since_id => @latest_id)
1152                elsif not @me.statuses_count.zero? and not @me.friends_count.zero?
1153                        cmd = NOTICE
1154                end
1155
1156                api("statuses/friends_timeline", q).reverse_each do |status|
1157                        id = @latest_id = status.id
1158                        next if @timeline.any? {|tid, s| s.id == id }
1159
1160                        status.user.status = status
1161                        user = status.user
1162                        tid  = @timeline.push(status)
1163                        tid  = nil unless @opts.tid
1164
1165                        @log.debug [id, user.screen_name, status.text].inspect
1166
1167                        if user.id == @me.id
1168                                mesg = generate_status_message(status.text)
1169                                mesg << " " << @opts.tid % tid if tid
1170                                post @prefix, TOPIC, main_channel, mesg
1171
1172                                @me = user
1173                        else
1174                                if @friends
1175                                        b = false
1176                                        @friends.each_with_index do |friend, i|
1177                                                if b = friend.id == user.id
1178                                                        @friends[i] = user
1179                                                        break
1180                                                end
1181                                        end
1182                                        unless b
1183                                                join main_channel, [user]
1184                                                @friends << user
1185                                                @me.friends_count += 1
1186                                        end
1187                                end
1188
1189                                message(status, main_channel, tid, nil, cmd)
1190                        end
1191                        @groups.each do |channel, members|
1192                                next unless members.include?(user.screen_name)
1193                                message(status, channel, tid, nil, cmd)
1194                        end
1195                end
1196        end
1197
1198        def check_direct_messages
1199                @prev_dm_id ||= nil
1200                q = @prev_dm_id ? { :count => 200, :since_id => @prev_dm_id } \
1201                                : { :count => 1 }
1202                api("direct_messages", q).reverse_each do |mesg|
1203                        unless @prev_dm_id &&= mesg.id
1204                                @prev_dm_id = mesg.id
1205                                next
1206                        end
1207
1208                        id   = mesg.id
1209                        user = mesg.sender
1210                        tid  = nil
1211                        text = mesg.text
1212                        @log.debug [id, user.screen_name, text].inspect
1213                        message(user, @nick, tid, text)
1214                end
1215        end
1216
1217        def check_mentions
1218                return if @timeline.empty?
1219                @prev_mention_id ||= @timeline.last.id
1220                api("statuses/mentions", {
1221                        :count    => 200,
1222                        :since_id => @prev_mention_id
1223                }).reverse_each do |mention|
1224                        id = @prev_mention_id = mention.id
1225                        next if @timeline.any? {|tid, s| s.id == id }
1226
1227                        mention.user.status = mention
1228                        user = mention.user
1229                        tid  = @timeline.push(mention)
1230                        tid  = nil unless @opts.tid
1231
1232                        @log.debug [id, user.screen_name, mention.text].inspect
1233                        message(mention, main_channel, tid)
1234
1235                        @friends.each_with_index do |friend, i|
1236                                if friend.id == user.id
1237                                        @friends[i] = user
1238                                        break
1239                                end
1240                        end if @friends
1241                end
1242        end
1243
1244        def check_updates
1245                update_redundant_suffix
1246
1247                return unless /\+r(\d+)\z/ === server_version
1248                rev = $1.to_i
1249                uri = URI("http://svn.coderepos.org/share/lang/ruby/net-irc/trunk/examples/tig.rb")
1250                @log.debug uri.inspect
1251                res = http(uri).request(http_req(:head, uri))
1252                @etags[uri.to_s] = res["ETag"]
1253                return unless not res.is_a?(Net::HTTPNotModified) and
1254                              /\A"(\d+)/ === res["ETag"] and rev < $1.to_i
1255                uri = URI("http://coderepos.org/share/log/lang/ruby/net-irc/trunk/examples/tig.rb")
1256                uri.query = { :rev => $1, :stop_rev => rev, :verbose => "on" }.to_query_str(";")
1257                log "\002New version is available.\017 <#{uri}>"
1258        rescue Errno::ECONNREFUSED, Timeout::Error => e
1259                @log.error "Failed to get the latest revision of tig.rb from #{uri.host}: #{e.inspect}"
1260        end
1261
1262        def interval(ratio)
1263                now   = Time.now
1264                max   = @opts.maxlimit
1265                limit = 0.98 * @limit # 98% of the rate limit
1266                i     = 3600.0        # an hour in seconds
1267                i *= @ratio.inject {|sum, r| sum.to_f + r.to_f } +
1268                     @consums.delete_if {|t| t < now }.size
1269                i /= ratio.to_f
1270                i /= (max and 0 < max and max < limit) ? max : limit
1271        rescue => e
1272                @log.error e.inspect
1273                100
1274        end
1275
1276        def join(channel, users)
1277                max_params_count = @opts.max_params_count || 3
1278                params = []
1279                users.each do |user|
1280                        prefix = prefix(user)
1281                        post prefix, JOIN, channel
1282                        params << prefix.nick
1283                        next if params.size < max_params_count
1284
1285                        post server_name, MODE, channel, "+#{"v" * params.size}", *params
1286                        params = []
1287                end
1288                post server_name, MODE, channel, "+#{"v" * params.size}", *params unless params.empty?
1289                users
1290        end
1291
1292        def start_jabber(jid, pass)
1293                @log.info "Logging-in with #{jid} -> jabber_bot_id: #{jabber_bot_id}"
1294                @im = Jabber::Simple.new(jid, pass)
1295                @im.add(jabber_bot_id)
1296                @im_thread = Thread.start do
1297                        require "cgi"
1298
1299                        loop do
1300                                begin
1301                                        @im.received_messages.each do |msg|
1302                                                @log.debug [msg.from, msg.body].inspect
1303                                                if msg.from.strip == jabber_bot_id
1304                                                        # Twitter -> 'id: msg'
1305                                                        body = msg.body.sub(/\A(.+?)(?:\(([^()]+)\))?: /, "")
1306                                                        body = decode_utf7(body)
1307
1308                                                        if Regexp.last_match
1309                                                                nick, id = Regexp.last_match.captures
1310                                                                body = untinyurl(CGI.unescapeHTML(body))
1311                                                                user = nick
1312                                                                nick = id || nick
1313                                                                nick = @nicknames[nick] || nick
1314                                                                post "#{nick}!#{user}@#{api_base.host}", PRIVMSG, main_channel, body
1315                                                        end
1316                                                end
1317                                        end
1318                                rescue Exception => e
1319                                        @log.error "Error on Jabber loop: #{e.inspect}"
1320                                        e.backtrace.each do |l|
1321                                                @log.error "\t#{l}"
1322                                        end
1323                                end
1324                                sleep 1
1325                        end
1326                end
1327        end
1328
1329        def whoreply(channel, user)
1330                #     "<channel> <user> <host> <server> <nick>
1331                #         ( "H" / "G" > ["*"] [ ( "@" / "+" ) ]
1332                #             :<hopcount> <real name>"
1333                prefix = prefix(user)
1334                server = api_base.host
1335                real   = user.name
1336                mode   = case prefix.nick
1337                        when @nick                     then "@"
1338                        #when @drones.include?(user.id) then "%" # FIXME
1339                        else                                "+"
1340                end
1341                post server_name, RPL_WHOREPLY, @nick, channel,
1342                     prefix.user, prefix.host, server, prefix.nick, "H*#{mode}", "0 #{real}"
1343        end
1344
1345        def save_config
1346                config = {
1347                        :groups    => @groups,
1348                        :channels  => @channels,
1349                        #:nicknames => @nicknames,
1350                        :drones    => @drones,
1351                }
1352                @config.open("w") {|f| YAML.dump(config, f) }
1353        end
1354
1355        def load_config
1356                @config.open do |f|
1357                        config     = YAML.load(f)
1358                        @groups    = config[:groups]    || {}
1359                        @channels  = config[:channels]  || []
1360                        #@nicknames = config[:nicknames] || {}
1361                        @drones    = config[:drones]    || []
1362                end
1363        rescue Errno::ENOENT
1364        end
1365
1366        def require_post?(path)
1367                %r{
1368                        \A
1369                        (?: status(?:es)?/update \z
1370                          | direct_messages/new \z
1371                          | friendships/create/
1372                          | account/(?: end_session \z | update_ )
1373                          | favou?ri(?: ing | tes )/create/
1374                          | notifications/
1375                          | blocks/create/ )
1376                }x === path
1377        end
1378
1379        def api(path, query = {}, opts = {})
1380                path.sub!(%r{\A/+}, "")
1381                query = query.to_query_str
1382
1383                authenticate = opts.fetch(:authenticate, true)
1384
1385                uri = api_base(authenticate)
1386                uri.path += path
1387                uri.path += ".json" if path != "users/username_available"
1388                uri.query = query unless query.empty?
1389                @log.debug uri.inspect
1390
1391                header      = {}
1392                credentials = authenticate ? [@real, @pass] : nil
1393                req         = case
1394                        when path.include?("/destroy/")
1395                                http_req :delete, uri, header, credentials
1396                        when require_post?(path)
1397                                http_req :post,   uri, header, credentials
1398                        else
1399                                http_req :get,    uri, header, credentials
1400                end
1401
1402                ret = http(uri, 30, 30).request req
1403
1404                #@etags[uri.to_s] = ret["ETag"]
1405
1406                if authenticate
1407                        hourly_limit = ret["X-RateLimit-Limit"].to_i
1408                        unless hourly_limit.zero?
1409                                if @limit != hourly_limit
1410                                        msg = "The rate limit per hour was changed: #{@limit} to #{hourly_limit}"
1411                                        log msg
1412                                        @log.info msg
1413                                        @limit = hourly_limit
1414                                end
1415
1416                                #if req.is_a?(Net::HTTP::Get) and not %w{
1417                                if not %w{
1418                                        statuses/friends_timeline
1419                                        direct_messages
1420                                        statuses/mentions
1421                                }.include?(path) and not ret.is_a?(Net::HTTPServerError)
1422                                        expired_on = Time.parse(ret["Date"]) rescue Time.now
1423                                        expired_on += 3636 # 1.01 hours in seconds later
1424                                        @consums << expired_on
1425                                end
1426                        end
1427                elsif ret["X-RateLimit-Remaining"]
1428                        @limit_remaining_for_ip = ret["X-RateLimit-Remaining"].to_i
1429                        @log.debug "IP based limit: #{@limit_remaining_for_ip}"
1430                end
1431
1432                case ret
1433                when Net::HTTPOK # 200
1434                        # Avoid Twitter's invalid JSON
1435                        json = ret.body.strip.sub(/\A(?:false|true)\z/, "[\\&]")
1436
1437                        res = JSON.parse json
1438                        if res.is_a?(Hash) and res["error"] # and not res["response"]
1439                                if @error != res["error"]
1440                                        @error = res["error"]
1441                                        log @error
1442                                end
1443                                raise APIFailed, res["error"]
1444                        end
1445                        res.to_tig_struct
1446                when Net::HTTPNoContent,  # 204
1447                     Net::HTTPNotModified # 304
1448                        []
1449                when Net::HTTPBadRequest # 400: exceeded the rate limitation
1450                        if ret.key?("X-RateLimit-Reset")
1451                                s = ret["X-RateLimit-Reset"].to_i - Time.now.to_i
1452                                log "RateLimit: #{(s / 60.0).ceil} min remaining to get timeline"
1453                                sleep s
1454                        end
1455                        raise APIFailed, "#{ret.code}: #{ret.message}"
1456                when Net::HTTPUnauthorized # 401
1457                        raise APIFailed, "#{ret.code}: #{ret.message}"
1458                else
1459                        raise APIFailed, "Server Returned #{ret.code} #{ret.message}"
1460                end
1461        rescue Errno::ETIMEDOUT, JSON::ParserError, IOError, Timeout::Error, Errno::ECONNRESET => e
1462                raise APIFailed, e.inspect
1463        end
1464
1465        def page(path, max_count, authenticate = false)
1466                @limit_remaining_for_ip ||= 52
1467                limit = 0.98 * @limit_remaining_for_ip # 98% of IP based rate limit
1468                r     = []
1469                cpp   = nil # counts per page
1470                1.upto(limit) do |num|
1471                        ret = api(path, { :page => num }, { :authenticate => authenticate })
1472                        cpp ||= ret.size
1473                        r.concat ret
1474                        break if ret.empty? or num >= max_count / cpp.to_f or
1475                                 ret.size != cpp or r.size >= max_count
1476                end
1477                r
1478        end
1479
1480        def generate_status_message(mesg)
1481                mesg = decode_utf7(mesg)
1482                mesg.delete!("\000\001")
1483                mesg.gsub!("&gt;", ">")
1484                mesg.gsub!("&lt;", "<")
1485                #mesg.gsub!(/\r\n|[\r\n\t\u00A0\u1680\u180E\u2002-\u200D\u202F\u205F\u2060\uFEFF]/, " ")
1486                mesg.gsub!(/\r\n|[\r\n\t]/, " ")
1487                mesg = untinyurl(mesg)
1488                mesg.sub!(@rsuffix_regex, "") if @rsuffix_regex
1489                mesg.strip
1490        end
1491
1492        def friend(id)
1493                return nil unless @friends
1494                if id.is_a? String
1495                        @friends.find {|i| i.screen_name.casecmp(id).zero? }
1496                else
1497                        @friends.find {|i| i.id == id }
1498                end
1499        end
1500
1501        def user(id)
1502                if id.is_a? String
1503                        @nick.casecmp(id).zero? ? @me : friend(id)
1504                else
1505                        @me.id == id ? @me : friend(id)
1506                end
1507        end
1508
1509        def prefix(u)
1510                nick = u.screen_name
1511                nick = "@#{nick}" if @opts.athack
1512                user = "id=%.9d" % u.id
1513                host = api_base.host
1514                host += "/protected" if u.protected
1515                host += "/bot"       if @drones.include?(u.id)
1516
1517                Prefix.new("#{nick}!#{user}@#{host}")
1518        end
1519
1520        def message(struct, target, tid = nil, str = nil, command = PRIVMSG)
1521                unless str
1522                        status = struct.is_a?(Status) ? struct : struct.status
1523                        str = status.text
1524                        if command != PRIVMSG
1525                                time = Time.parse(status.created_at) rescue Time.now
1526                                str  = "#{time.strftime(@opts.strftime || "%m-%d %H:%M")} #{str}" # TODO: color
1527                        end
1528                end
1529                user        = (struct.is_a?(User) ? struct : struct.user).dup
1530                screen_name = user.screen_name
1531
1532                user.screen_name = @nicknames[screen_name] || screen_name
1533                prefix = prefix(user)
1534                str    = generate_status_message(str)
1535                str    = "#{str} #{@opts.tid % tid}" if tid
1536
1537                post prefix, command, target, str
1538        end
1539
1540        def log(str)
1541                post server_name, NOTICE, main_channel, str.gsub(/\r\n|[\r\n]/, " ")
1542        end
1543
1544        def decode_utf7(str)
1545                return str unless defined?(::Iconv) and str.include?("+")
1546
1547                str.sub!(/\A(?:.+ > |.+\z)/) { Iconv.iconv("UTF-8", "UTF-7", $&).join }
1548                #FIXME str = "[utf7]: #{str}" if str =~ /[^a-z0-9\s]/i
1549                str
1550        rescue Iconv::IllegalSequence
1551                str
1552        rescue => e
1553                @log.error e
1554                str
1555        end
1556
1557        def untinyurl(text)
1558                text.gsub(@opts.untiny_whole_urls ? URI.regexp(%w[http https]) : %r{
1559                        http:// (?:
1560                                (?: bit\.ly | (?: tin | rub) yurl\.com
1561                                  | is\.gd | cli\.gs | tr\.im | u\.nu | airme\.us
1562                                  | ff\.im | twurl.nl | bkite\.com | tumblr\.com
1563                                  | pic\.gd | sn\.im | digg\.com )
1564                                / [0-9a-z=-]+ |
1565                                blip\.fm/~ (?> [0-9a-z]+) (?! /) |
1566                                flic\.kr/[a-z0-9/]+
1567                        )
1568                }ix) {|url| "#{resolve_http_redirect(URI(url)) || url}" }
1569        end
1570
1571        def bitlify(text)
1572                login, key, len = @opts.bitlify.split(":", 3) if @opts.bitlify
1573                len      = (len || 20).to_i
1574                longurls = URI.extract(text, %w[http https]).uniq.map do |url|
1575                        URI.rstrip_unpaired_paren(url)
1576                end.reject {|url| url.size < len }
1577                return text if longurls.empty?
1578
1579                bitly = URI("http://api.bit.ly/shorten")
1580                if login and key
1581                        bitly.path  = "/shorten"
1582                        bitly.query = {
1583                                :version => "2.0.1", :format => "json", :longUrl => longurls,
1584                        }.to_query_str(";")
1585                        @log.debug bitly
1586                        req = http_req(:get, bitly, {}, [login, key])
1587                        res = http(bitly, 5, 10).request(req)
1588                        res = JSON.parse(res.body)
1589                        res = res["results"]
1590
1591                        longurls.each do |longurl|
1592                                text.gsub!(longurl) do
1593                                        res[$&] && res[$&]["shortUrl"] || $&
1594                                end
1595                        end
1596                else
1597                        bitly.path = "/api"
1598                        longurls.each do |longurl|
1599                                bitly.query = { :url => longurl }.to_query_str
1600                                @log.debug bitly
1601                                req = http_req(:get, bitly)
1602                                res = http(bitly, 5, 5).request(req)
1603                                text.gsub!(longurl, res.body)
1604                        end
1605                end
1606
1607                text
1608        rescue => e
1609                @log.error e
1610                text
1611        end
1612
1613        def unuify(text)
1614                unu_url = "http://u.nu/"
1615                unu     = URI("#{unu_url}unu-api-simple")
1616                size    = unu_url.size
1617
1618                text.gsub(URI.regexp(%w[http https])) do |url|
1619                        url = URI.rstrip_unpaired_paren(url)
1620                        if url.size < size + 5 or url[0, size] == unu_url
1621                                return url
1622                        end
1623
1624                        unu.query = { :url => url }.to_query_str
1625                        @log.debug unu
1626
1627                        res = http(unu, 5, 5).request(http_req(:get, unu)).body
1628
1629                        if res[0, 12] == unu_url
1630                                res
1631                        else
1632                                raise res.split("|")
1633                        end
1634                end
1635        rescue => e
1636                @log.error e
1637                text
1638        end
1639
1640        def escape_http_urls(text)
1641                original_text = text.encoding!("UTF-8").dup
1642
1643                if defined? ::Punycode
1644                        # TODO: Nameprep
1645                        text.gsub!(%r{(https?://)([^\x00-\x2C\x2F\x3A-\x40\x5B-\x60\x7B-\x7F]+)}) do
1646                                domain = $2
1647                                # Dots:
1648                                #   * U+002E (full stop)           * U+3002 (ideographic full stop)
1649                                #   * U+FF0E (fullwidth full stop) * U+FF61 (halfwidth ideographic full stop)
1650                                # => /[.\u3002\uFF0E\uFF61] # Ruby 1.9 /x
1651                                $1 + domain.split(/\.|\343\200\202|\357\274\216|\357\275\241/).map do |label|
1652                                        break [domain] if /\A-|[\x00-\x2C\x2E\x2F\x3A-\x40\x5B-\x60\x7B-\x7F]|-\z/ === label
1653                                        next label unless /[^-A-Za-z0-9]/ === label
1654                                        punycode = Punycode.encode(label)
1655                                        break [domain] if punycode.size > 59
1656                                        "xn--#{punycode}"
1657                                end.join(".")
1658                        end
1659                        if text != original_text
1660                                log "Punycode encoded: #{text}"
1661                                original_text = text.dup
1662                        end
1663                end
1664
1665                urls = []
1666                (text.split(/[\s<>]+/) + [text]).each do |str|
1667                        next if /%[0-9A-Fa-f]{2}/ === str
1668                        # URI::UNSAFE + "#"
1669                        escaped_str = URI.escape(str, %r{[^-_.!~*'()a-zA-Z0-9;/?:@&=+$,\[\]#]})
1670                        URI.extract(escaped_str, %w[http https]).each do |url|
1671                                uri = URI(URI.rstrip_unpaired_paren(url))
1672                                if not urls.include?(uri.to_s) and exist_uri?(uri)
1673                                        urls << uri.to_s
1674                                end
1675                        end if escaped_str != str
1676                end
1677                urls.each do |url|
1678                        unescaped_url = URI.unescape(url).encoding!("UTF-8")
1679                        text.gsub!(unescaped_url, url)
1680                end
1681                log "Percent encoded: #{text}" if text != original_text
1682
1683                text.encoding!("ASCII-8BIT")
1684        rescue => e
1685                @log.error e
1686                text
1687        end
1688
1689        def exist_uri?(uri, limit = 1)
1690                ret = nil
1691                #raise "Not supported." unless uri.is_a?(URI::HTTP)
1692                return ret if limit.zero? or uri.nil? or not uri.is_a?(URI::HTTP)
1693                @log.debug uri.inspect
1694
1695                req = http_req :head, uri
1696                http(uri, 3, 2).request(req) do |res|
1697                        ret = case res
1698                                when Net::HTTPSuccess
1699                                        true
1700                                when Net::HTTPRedirection
1701                                        uri = resolve_http_redirect(uri)
1702                                        exist_uri?(uri, limit - 1)
1703                                when Net::HTTPClientError
1704                                        false
1705                                #when Net::HTTPServerError
1706                                #       nil
1707                                else
1708                                        nil
1709                        end
1710                end
1711
1712                ret
1713        rescue => e
1714                @log.error e.inspect
1715                ret
1716        end
1717
1718        def resolve_http_redirect(uri, limit = 3)
1719                return uri if limit.zero? or uri.nil?
1720                @log.debug uri.inspect
1721
1722                req = http_req :head, uri
1723                http(uri, 3, 2).request(req) do |res|
1724                        break if not res.is_a?(Net::HTTPRedirection) or
1725                                 not res.key?("Location")
1726                        begin
1727                                location = URI(res["Location"])
1728                        rescue URI::InvalidURIError
1729                        end
1730                        unless location.is_a? URI::HTTP
1731                                begin
1732                                        location = URI.join(uri.to_s, res["Location"])
1733                                rescue URI::InvalidURIError, URI::BadURIError
1734                                        # FIXME
1735                                end
1736                        end
1737                        uri = resolve_http_redirect(location, limit - 1)
1738                end
1739
1740                uri
1741        rescue => e
1742                @log.error e.inspect
1743                uri
1744        end
1745
1746        def fetch_sources(n = nil)
1747                n    = n.to_i
1748                uri  = URI("http://wedata.net/databases/TwitterSources/items.json")
1749                @log.debug uri.inspect
1750                json = http(uri).request(http_req(:get, uri)).body
1751                sources = JSON.parse json
1752                sources.map! {|item| [item["data"]["source"], item["name"]] }.push ["", "web"]
1753                if (1 ... sources.size).include?(n)
1754                        sources = Array.new(n) { sources.delete_at(rand(sources.size)) }.compact
1755                end
1756                sources
1757        rescue => e
1758                @log.error e.inspect
1759                log "An error occured while loading #{uri.host}."
1760                @sources || [[api_source, "tig.rb"]]
1761        end
1762
1763        def update_redundant_suffix
1764                uri = URI("http://svn.coderepos.org/share/platform/twitterircgateway/suffixesblacklist.txt")
1765                @log.debug uri.inspect
1766                res = http(uri).request(http_req(:get, uri))
1767                @etags[uri.to_s] = res["ETag"]
1768                return if res.is_a? Net::HTTPNotModified
1769                source = res.body
1770                source.encoding!("UTF-8") if source.respond_to?(:encoding) and source.encoding == Encoding::BINARY
1771                @rsuffix_regex = /#{Regexp.union(*source.split)}\z/
1772        rescue Errno::ECONNREFUSED, Timeout::Error => e
1773                @log.error "Failed to get the redundant suffix blacklist from #{uri.host}: #{e.inspect}"
1774        end
1775
1776        def http(uri, open_timeout = nil, read_timeout = 60)
1777                http = case
1778                        when @httpproxy
1779                                Net::HTTP.new(uri.host, uri.port, @httpproxy.address, @httpproxy.port,
1780                                                                  @httpproxy.user, @httpproxy.password)
1781                        when ENV["HTTP_PROXY"], ENV["http_proxy"]
1782                                proxy = URI(ENV["HTTP_PROXY"] || ENV["http_proxy"])
1783                                Net::HTTP.new(uri.host, uri.port, proxy.host, proxy.port,
1784                                                                  proxy.user, proxy.password)
1785                        else
1786                                Net::HTTP.new(uri.host, uri.port)
1787                end
1788                http.open_timeout = open_timeout if open_timeout # nil by default
1789                http.read_timeout = read_timeout if read_timeout # 60 by default
1790                if uri.is_a? URI::HTTPS
1791                        http.use_ssl     = true
1792                        http.verify_mode = OpenSSL::SSL::VERIFY_NONE
1793                end
1794                http
1795        rescue => e
1796                @log.error e
1797        end
1798
1799        def http_req(method, uri, header = {}, credentials = nil)
1800                accepts = ["*/*;q=0.1"]
1801                #require "mime/types"; accepts.unshift MIME::Types.of(uri.path).first.simplified
1802                types   = { "json" => "application/json", "txt" => "text/plain" }
1803                ext     = uri.path[/[^.]+\z/]
1804                accepts.unshift types[ext] if types.key?(ext)
1805                user_agent = "#{self.class}/#{server_version} (#{File.basename(__FILE__)}; net-irc) Ruby/#{RUBY_VERSION} (#{RUBY_PLATFORM})"
1806
1807                header["User-Agent"]      ||= user_agent
1808                header["Accept"]          ||= accepts.join(",")
1809                header["Accept-Charset"]  ||= "UTF-8,*;q=0.0" if ext != "json"
1810                #header["Accept-Language"] ||= @opts.lang # "en-us,en;q=0.9,ja;q=0.5"
1811                header["If-None-Match"]   ||= @etags[uri.to_s] if @etags[uri.to_s]
1812
1813                req = case method.to_s.downcase.to_sym
1814                when :get
1815                        Net::HTTP::Get.new    uri.request_uri, header
1816                when :head
1817                        Net::HTTP::Head.new   uri.request_uri, header
1818                when :post
1819                        Net::HTTP::Post.new   uri.path,        header
1820                when :put
1821                        Net::HTTP::Put.new    uri.path,        header
1822                when :delete
1823                        Net::HTTP::Delete.new uri.request_uri, header
1824                else # raise ""
1825                end
1826                if req.request_body_permitted?
1827                        req["Content-Type"] ||= "application/x-www-form-urlencoded"
1828                        req.body = uri.query
1829                end
1830                req.basic_auth(*credentials) if credentials
1831                req
1832        rescue => e
1833                @log.error e
1834        end
1835
1836        def oops(status)
1837                "Oops! Your update was over 140 characters. We sent the short version" <<
1838                " to your friends (they can view the entire update on the Web <" <<
1839                permalink(status) << ">)."
1840        end
1841
1842        def permalink(struct)
1843                path = struct.is_a?(Status) ? "#{struct.user.screen_name}/statuses/#{struct.id}" \
1844                                            : struct.screen_name
1845                "http://twitter.com/#{path}"
1846        end
1847
1848        def source
1849                @sources[rand(@sources.size)].first
1850        end
1851
1852        def initial_message
1853                super
1854                post server_name, RPL_ISUPPORT, @nick,
1855                     "NETWORK=Twitter",
1856                     "CHANTYPES=#", "CHANNELLEN=50", "CHANMODES=#{available_channel_modes}",
1857                     "NICKLEN=15", "TOPICLEN=420", "PREFIX=(hov)%@+",
1858                     "are supported by this server"
1859        end
1860
1861        User   = Struct.new(:id, :name, :screen_name, :location, :description, :url,
1862                            :following, :notifications, :protected, :time_zone,
1863                            :utc_offset, :created_at, :friends_count, :followers_count,
1864                            :statuses_count, :favourites_count, :verified,
1865                            :verified_profile,
1866                            :profile_image_url, :profile_background_color, :profile_text_color,
1867                            :profile_link_color, :profile_sidebar_fill_color,
1868                            :profile_sidebar_border_color, :profile_background_image_url,
1869                            :profile_background_tile, :status)
1870        Status = Struct.new(:id, :text, :source, :created_at, :truncated, :favorited,
1871                            :in_reply_to_status_id, :in_reply_to_user_id,
1872                            :in_reply_to_screen_name, :user)
1873        DM     = Struct.new(:id, :text, :created_at,
1874                            :sender_id, :sender_screen_name, :sender,
1875                            :recipient_id, :recipient_screen_name, :recipient)
1876
1877        class TypableMap < Hash
1878                #Roman = %w[
1879                #       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
1880                #].unshift("").map do |consonant|
1881                #       case consonant
1882                #       when "h", "q"  then %w|a i   e o|
1883                #       when /[hy]$/   then %w|a   u   o|
1884                #       else                %w|a i u e o|
1885                #       end.map {|vowel| "#{consonant}#{vowel}" }
1886                #end.flatten
1887                Roman = %w[
1888                          a   i   u   e   o  ka  ki  ku  ke  ko  sa shi  su  se  so
1889                         ta chi tsu  te  to  na  ni  nu  ne  no  ha  hi  fu  he  ho
1890                         ma  mi  mu  me  mo  ya      yu      yo  ra  ri  ru  re  ro
1891                         wa              wo   n
1892                         ga  gi  gu  ge  go  za  ji  zu  ze  zo  da          de  do
1893                         ba  bi  bu  be  bo  pa  pi  pu  pe  po
1894                        kya     kyu     kyo sha     shu     sho cha     chu     cho
1895                        nya     nyu     nyo hya     hyu     hyo mya     myu     myo
1896                        rya     ryu     ryo
1897                        gya     gyu     gyo  ja      ju      jo bya     byu     byo
1898                        pya     pyu     pyo
1899                ].freeze
1900
1901                def initialize(size = nil, shuffle = false)
1902                        if shuffle
1903                                @seq = Roman.dup
1904                                if @seq.respond_to?(:shuffle!)
1905                                        @seq.shuffle!
1906                                else
1907                                        seq = @seq.dup
1908                                        @seq.map! { seq.slice!(rand(seq.size)) }
1909                                end
1910                                @seq.freeze
1911                        else
1912                                @seq = Roman
1913                        end
1914                        @n    = 0
1915                        @size = size || @seq.size
1916                end
1917
1918                def generate(n)
1919                        ret = []
1920                        begin
1921                                n, r = n.divmod(@seq.size)
1922                                ret << @seq[r]
1923                        end while n > 0
1924                        ret.reverse.join #.gsub(/n(?=[bmp])/, "m")
1925                end
1926
1927                def push(obj)
1928                        id = generate(@n)
1929                        self[id] = obj
1930                        @n += 1
1931                        @n %= @size
1932                        id
1933                end
1934                alias :<< :push
1935
1936                def clear
1937                        @n = 0
1938                        super
1939                end
1940
1941                def first
1942                        @size.times do |i|
1943                                id = generate((@n + i) % @size)
1944                                return self[id] if key? id
1945                        end unless empty?
1946                        nil
1947                end
1948
1949                def last
1950                        @size.times do |i|
1951                                id = generate((@n - 1 - i) % @size)
1952                                return self[id] if key? id
1953                        end unless empty?
1954                        nil
1955                end
1956
1957                private :[]=
1958                undef update, merge, merge!, replace
1959        end
1960
1961
1962end
1963
1964class Array
1965        def to_tig_struct
1966                map do |v|
1967                        v.respond_to?(:to_tig_struct) ? v.to_tig_struct : v
1968                end
1969        end
1970end
1971
1972class Hash
1973        def to_tig_struct
1974                if empty?
1975                        #warn "" if $VERBOSE
1976                        #raise Error
1977                        return nil
1978                end
1979
1980                struct = case
1981                        when struct_of?(TwitterIrcGateway::User)
1982                                TwitterIrcGateway::User.new
1983                        when struct_of?(TwitterIrcGateway::Status)
1984                                TwitterIrcGateway::Status.new
1985                        when struct_of?(TwitterIrcGateway::DM)
1986                                TwitterIrcGateway::DM.new
1987                        else
1988                                members = keys
1989                                members.concat TwitterIrcGateway::User.members
1990                                members.concat TwitterIrcGateway::Status.members
1991                                members.concat TwitterIrcGateway::DM.members
1992                                members.map! {|m| m.to_sym }
1993                                members.uniq!
1994                                Struct.new(*members).new
1995                end
1996                each do |k, v|
1997                        struct[k.to_sym] = v.respond_to?(:to_tig_struct) ? v.to_tig_struct : v
1998                end
1999                struct
2000        end
2001
2002        # { :f => nil }     #=> "f"
2003        # { "f" => "" }     #=> "f="
2004        # { "f" => "v" }    #=> "f=v"
2005        # { "f" => [1, 2] } #=> "f=1&f=2"
2006        def to_query_str separator = "&"
2007                inject([]) do |r, (k, v)|
2008                        k = URI.encode_component k.to_s
2009                        (v.is_a?(Array) ? v : [v]).each do |i|
2010                                if i.nil?
2011                                        r << k
2012                                else
2013                                        r << "#{k}=#{URI.encode_component i.to_s}"
2014                                end
2015                        end
2016                        r
2017                end.join separator
2018        end
2019
2020        private
2021        def struct_of? struct
2022                (keys - struct.members.map {|m| m.to_s }).size.zero?
2023        end
2024end
2025
2026class String
2027        def ch?
2028                /\A[&#+!][^ \007,]{1,50}\z/ === self
2029        end
2030
2031        def nick? # Twitter screen_name (username)
2032                /\A[A-Za-z0-9_]{1,15}\z/ === self
2033        end
2034
2035        def encoding! enc
2036                return self unless respond_to? :force_encoding
2037                force_encoding enc
2038        end
2039end
2040
2041module URI::Escape
2042        # URI.escape("あ1") #=> "%E3%81%82\xEF\xBC\x91"
2043        # URI("file:///4")  #=> #<URI::Generic:0x9d09db0 URL:file:/4>
2044        #   "\\d" -> "0-9" for Ruby 1.9
2045        alias :_orig_escape :escape
2046        def escape str, unsafe = %r{[^-_.!~*'()a-zA-Z0-9;/?:@&=+$,\[\]]}
2047                _orig_escape(str, unsafe)
2048        end
2049        alias :encode :escape
2050
2051        def encode_component str, unsafe = /[^-_.!~*'()a-zA-Z0-9 ]/
2052                _orig_escape(str, unsafe).tr(" ", "+")
2053        end
2054
2055        def rstrip_unpaired_paren str
2056                str.sub(%r{(/[^/()]*(?:\([^/()]*\)[^/()]*)*)\)[^/()]*\z}, "\\1")
2057        end
2058end
2059
2060if __FILE__ == $0
2061        require "optparse"
2062
2063        opts = {
2064                :port  => 16668,
2065                :host  => "localhost",
2066                :log   => nil,
2067                :debug => false,
2068                :foreground => false,
2069        }
2070
2071        OptionParser.new do |parser|
2072                parser.instance_eval do
2073                        self.banner = <<-EOB.gsub(/^\t+/, "")
2074                                Usage: #{$0} [opts]
2075
2076                        EOB
2077
2078                        separator ""
2079
2080                        separator "Options:"
2081                        on("-p", "--port [PORT=#{opts[:port]}]", "port number to listen") do |port|
2082                                opts[:port] = port
2083                        end
2084
2085                        on("-h", "--host [HOST=#{opts[:host]}]", "host name or IP address to listen") do |host|
2086                                opts[:host] = host
2087                        end
2088
2089                        on("-l", "--log LOG", "log file") do |log|
2090                                opts[:log] = log
2091                        end
2092
2093                        on("--debug", "Enable debug mode") do |debug|
2094                                opts[:log]   = $stdout
2095                                opts[:debug] = true
2096                        end
2097
2098                        on("-f", "--foreground", "run foreground") do |foreground|
2099                                opts[:log]        = $stdout
2100                                opts[:foreground] = true
2101                        end
2102
2103                        on("-n", "--name [user name or email address]") do |name|
2104                                opts[:name] = name
2105                        end
2106
2107                        parse!(ARGV)
2108                end
2109        end
2110
2111        opts[:logger] = Logger.new(opts[:log], "daily")
2112        opts[:logger].level = opts[:debug] ? Logger::DEBUG : Logger::INFO
2113
2114        #def daemonize(foreground = false)
2115        #       [:INT, :TERM, :HUP].each do |sig|
2116        #               Signal.trap sig, "EXIT"
2117        #       end
2118        #       return yield if $DEBUG or foreground
2119        #       Process.fork do
2120        #               Process.setsid
2121        #               Dir.chdir "/"
2122        #               STDIN.reopen  "/dev/null"
2123        #               STDOUT.reopen "/dev/null", "a"
2124        #               STDERR.reopen STDOUT
2125        #               yield
2126        #       end
2127        #       exit! 0
2128        #end
2129
2130        #daemonize(opts[:debug] || opts[:foreground]) do
2131                Net::IRC::Server.new(opts[:host], opts[:port], TwitterIrcGateway, opts).start
2132        #end
2133end
Note: See TracBrowser for help on using the browser.