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

Revision 18654, 22.3 kB (checked in by znz, 5 years ago)

@hatebu が使ってるRubyURL対応

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