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

Revision 35371, 58.4 kB (checked in by cho45, 5 months ago)

hig.rb に metadata モードを追加。tig.rb に /me debug <code> で instance_eval させるコマンドを追加

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