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

Revision 32947, 28.7 kB (checked in by drry, 4 years ago)
  • (tig.rb) API 制限の確認を廃止しました。checkrls オプションも廃止しました。
  • (tig.rb) HTTP proxy に対応しました。
  • (tig.rb) API 呼び出しを回数を一部削減しました。
  • (tig.rb) 最新のつぶやきを destroy したら TOPIC をひとつ前のつぶやきに設定するようにしました。
  • (tig.rb) DELETE リクエストの際にボディを付けてしまった問題を修正しました。
  • (tig.rb) API 更新に伴い今では意味のないエラー対処を削除しました。
  • (tig.rb) API 制限超過時に残り待ち時間を流すようにしました。
  • Property svn:executable set to *
Line 
1#!/usr/bin/env ruby
2# vim:fileencoding=utf-8:
3# -*- coding: utf-8 -*-
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        twitter {
26                host: localhost
27                port: 16668
28                name: username@example.com athack jabber=username@example.com:jabberpasswd tid ratio=32:1 mentions=6 maxlimit=70
29                password: password on Twitter
30                in-encoding: utf8
31                out-encoding: utf8
32        }
33
34### athack
35
36If `athack` client option specified,
37all nick in join message is leading with @.
38
39So if you complemente nicks (e.g. Irssi),
40it's good for Twitter like reply command (@nick).
41
42In this case, you will see torrent of join messages after connected,
43because NAMES list can't send @ leading nick (it interpreted op.)
44
45### tid[=<color>]
46
47Apply ID to each message for make favorites by CTCP ACTION.
48
49        /me fav ID [ID...]
50
51<color> can be
52
53        0  => white
54        1  => black
55        2  => blue         navy
56        3  => green
57        4  => red
58        5  => brown        maroon
59        6  => purple
60        7  => orange       olive
61        8  => yellow
62        9  => lightgreen   lime
63        10 => teal
64        11 => lightcyan    cyan aqua
65        12 => lightblue    royal
66        13 => pink         lightpurple fuchsia
67        14 => grey
68        15 => lightgrey    silver
69
70
71### jabber=<jid>:<pass>
72
73If `jabber=<jid>:<pass>` option specified,
74use jabber to get friends timeline.
75
76You must setup im notifing settings in the site and
77install "xmpp4r-simple" gem.
78
79        $ sudo gem install xmpp4r-simple
80
81Be careful for managing password.
82
83### alwaysim
84
85Use IM instead of any APIs (e.g. post)
86
87### ratio=<timeline>:<friends>
88
89### mentions[=<ratio>]
90
91### maxlimit=<hourly limit>
92
93### secure
94
95### clientspoofing
96
97### httpproxy=[<user>[:<password>]@]<address>[:<port>]
98
99Force SSL for API.
100
101## Extended commands through the CTCP ACTION
102
103### list (ls)
104
105        /me list NICK [NUMBER]
106
107### fav (favorite, favourite, unfav, unfavorite, unfavourite)
108
109        /me fav [ID...]
110        /me unfav [ID...]
111
112### link (ln)
113
114        /me link ID [ID...]
115
116### destroy (del, delete, miss, oops, remove, rm)
117
118        /me destroy [ID...]
119
120### in (location)
121
122        /me in Sugamo, Tokyo, Japan
123
124### reply (re, mention)
125
126        /me reply ID blah, blah...
127
128### utf7
129
130        /me utf7
131
132### name
133
134        /me name My Name
135
136### description (desc)
137
138        /me description blah, blah...
139
140### spoof
141
142        /me spoof
143        /me spoo[o...]f
144        /me spoof tigrb twitterircgateway twitt web mobileweb
145
146## License
147
148Ruby's by cho45
149
150=end
151
152$LOAD_PATH << "lib"
153$LOAD_PATH << "../lib"
154
155$KCODE = "u" # json use this
156
157require "rubygems"
158require "net/irc"
159require "net/https"
160require "uri"
161require "json"
162require "socket"
163require "time"
164require "logger"
165require "yaml"
166require "pathname"
167require "cgi"
168
169class TwitterIrcGateway < Net::IRC::Server::Session
170        def server_name
171                "twittergw"
172        end
173
174        def server_version
175                "0.0.0"
176        end
177
178        def main_channel
179                "#twitter"
180        end
181
182        def api_base
183                URI("http://twitter.com/")
184        end
185
186        def api_source
187                "tigrb"
188        end
189
190        def jabber_bot_id
191                "twitter@twitter.com"
192        end
193
194        def hourly_limit
195                60
196        end
197
198        class APIFailed < StandardError; end
199
200        def initialize(*args)
201                super
202                @groups     = {}
203                @channels   = [] # joined channels (groups)
204                @nicknames  = {}
205                @user_agent = "#{self.class}/#{server_version} (#{File.basename(__FILE__)})"
206                @config     = Pathname.new(ENV["HOME"]) + ".tig"
207                load_config
208        end
209
210#       def on_nick(m)
211#               @nicknames[@nick] = m.params[0]
212#       end
213
214        def on_user(m)
215                super
216                @real, *@opts = (@opts.name || @real).split(/\s+/)
217                @opts = @opts.inject({}) do |r,i|
218                        key, value = i.split("=")
219                        key = "mentions" if key == "replies" # backcompat
220                        r.update(key => value || true)
221                end
222
223                @hourly_limit = hourly_limit
224
225                @me = api("account/verify_credentials")
226                if @me
227                        @user   = @me["id"].to_s
228                        @host   = hostname(@me)
229                        @prefix = Prefix.new("#{@me["screen_name"]}!#{@user}@#{@host}")
230                        #post NICK, @me["screen_name"] if @nick != @me["screen_name"]
231                end
232                post @prefix, JOIN, main_channel
233                post server_name, MODE, main_channel, "+o", @prefix.nick
234                post @prefix, TOPIC, main_channel, generate_status_message(@me["status"]) if @me
235
236                @tmap = TypableMap.new
237
238                if @opts["jabber"]
239                        jid, pass = @opts["jabber"].split(":", 2)
240                        @opts["jabber"].replace("jabber=#{jid}:********")
241                        if jabber_bot_id
242                                begin
243                                        require "xmpp4r-simple"
244                                        start_jabber(jid, pass)
245                                rescue LoadError
246                                        log "Failed to start Jabber."
247                                        log 'Installl "xmpp4r-simple" gem or check your ID/pass.'
248                                        finish
249                                end
250                        else
251                                @opts.delete("jabber")
252                                log "This gateway does not support Jabber bot."
253                        end
254                end
255
256                log "Client Options: #{@opts.inspect}"
257                @log.info "Client Options: #{@opts.inspect}"
258
259                @ratio = (@opts["ratio"] || "11:3").split(":").map {|r| r.to_f }
260                @ratio = Struct.new(:timeline, :friends, :mentions).new(*@ratio)
261                @ratio[:mentions] = (@opts["mentions"] == true ? 5 : @opts["mentions"]).to_f
262                footing = @ratio.inject {|sum, ratio| sum + ratio }
263                @ratio.each_pair {|m, v| @ratio[m] = v / footing }
264
265                @timeline = []
266                @check_friends_thread = Thread.start do
267                        loop do
268                                begin
269                                        check_friends
270                                rescue APIFailed => e
271                                        @log.error e.inspect
272                                rescue Exception => e
273                                        @log.error e.inspect
274                                        e.backtrace.each do |l|
275                                                @log.error "\t#{l}"
276                                        end
277                                end
278                                sleep interval(@ratio[:friends])
279                        end
280                end
281
282                return if @opts["jabber"]
283
284                @sources   = @opts["clientspoofing"] ? fetch_sources : [[api_source, "tig.rb"]]
285                @suffix_bl = fetch_suffix_bl
286
287                sleep 3
288                @check_timeline_thread = Thread.start do
289                        loop do
290                                begin
291                                        check_timeline
292                                        # check_direct_messages
293                                rescue APIFailed => e
294                                        @log.error e.inspect
295                                rescue Exception => e
296                                        @log.error e.inspect
297                                        e.backtrace.each do |l|
298                                                @log.error "\t#{l}"
299                                        end
300                                end
301                                sleep interval(@ratio[:timeline])
302                        end
303                end
304
305                return unless @opts["mentions"]
306
307                sleep 10
308                @check_mentions_thread = Thread.start do
309                        loop do
310                                begin
311                                        check_mentions
312                                rescue APIFailed => e
313                                        @log.error e.inspect
314                                rescue Exception => e
315                                        @log.error e.inspect
316                                        e.backtrace.each do |l|
317                                                @log.error "\t#{l}"
318                                        end
319                                end
320                                sleep interval(@ratio[:mentions])
321                        end
322                end
323        end
324
325        def on_disconnected
326                @check_friends_thread.kill  rescue nil
327                @check_mentions_thread.kill rescue nil
328                @check_timeline_thread.kill rescue nil
329                @im_thread.kill             rescue nil
330                @im.disconnect              rescue nil
331        end
332
333        def on_privmsg(m)
334                return on_ctcp(m[0], ctcp_decoding(m[1])) if m.ctcp?
335                retry_count = 3
336                ret = nil
337                target, mesg = *m.params
338                if @utf7
339                        mesg = Iconv.iconv("UTF-7", "UTF-8", mesg).join
340                        mesg = mesg.force_encoding("ASCII-8BIT") if mesg.respond_to?(:force_encoding)
341                end
342                begin
343                        if target =~ /^#/
344                                if @opts["alwaysim"] and @im and @im.connected? # in jabber mode, using jabber post
345                                        ret = @im.deliver(jabber_bot_id, mesg)
346                                        post @prefix, TOPIC, main_channel, mesg
347                                else
348                                        src = @sources[rand(@sources.size)].first
349                                        ret = api("statuses/update", { :status => mesg, :source => src })
350                                        if ret["truncated"]
351                                                log "Oops! Your update was over 140 characters. We sent the short version" <<
352                                                    " to your friends (they can view the entire update on the Web <" <<
353                                                    permalink(ret) << ">)."
354                                        end
355                                        ret.delete("user")
356                                        @me.update("status" => ret)
357                                end
358                        else
359                                # direct message
360                                ret = api("direct_messages/new", { :user => target, :text => mesg })
361                        end
362                        raise APIFailed, "API failed" unless ret
363                        log "Status Updated"
364                rescue => e
365                        @log.error [retry_count, e.inspect].inspect
366                        if retry_count > 0
367                                retry_count -= 1
368                                @log.debug "Retry to setting status..."
369                                retry
370                        else
371                                log "Some Error Happened on Sending #{mesg}. #{e}"
372                        end
373                end
374        end
375
376        def on_ctcp(target, mesg)
377                _, command, *args = mesg.split(/\s+/)
378                case command
379                when "call"
380                        if args.size < 2
381                                log "/me call <Twitter_screen_name> as <IRC_nickname>"
382                                return
383                        end
384                        screen_name = args[0]
385                        nickname    = args[2] || args[1] # allow omitting 'as'
386                        if nickname == "is"
387                                @nicknames.delete(screen_name)
388                                log "Removed the nickname for #{screen_name}"
389                        else
390                                @nicknames[screen_name] = nickname
391                                log "Call #{screen_name} as #{nickname}"
392                        end
393                when "utf7"
394                        begin
395                                require "iconv"
396                                @utf7 = !@utf7
397                                log "UTF-7 mode: #{@utf7 ? 'on' : 'off'}"
398                        rescue LoadError => e
399                                log "Can't load iconv."
400                        end
401                when "list", "ls"
402                        if args.empty?
403                                log "/me list <NICK> [<NUM>]"
404                                return
405                        end
406                        nick = args.first
407                        unless (1..200).include?(count = args[1].to_i)
408                                count = 20
409                        end
410                        @log.debug [nick, mesg]
411                        to = nick == @nick ? server_name : nick
412                        res = api("statuses/user_timeline/#{nick}", { :count => "#{count}" }).reverse_each do |s|
413                                @log.debug s
414                                time = Time.parse(s["created_at"]) rescue Time.now
415                                post to, NOTICE, main_channel,
416                                     "#{time.strftime "%m-%d %H:%M"} #{generate_status_message(s)}"
417                        end
418                        unless res
419                                post server_name, ERR_NOSUCHNICK, nick, "No such nick/channel"
420                        end
421                when /^(un)?fav(?:ou?rite)?$/
422                # fav, favorite, favourite, unfav, unfavorite, unfavourite
423                        method   = $1.nil? ? "create" : "destroy"
424                        entered  = $&.capitalize
425                        statuses = []
426                        if args.empty?
427                                if method == "create"
428                                        id = @timeline.last
429                                        @tmap.each_value do |v|
430                                                if v["id"] == id
431                                                        statuses.push v
432                                                        break
433                                                end
434                                        end
435                                else
436                                        @favorites ||= api("favorites").reverse
437                                        if @favorites.empty?
438                                                log "You've never favorite yet. No favorites to unfavorite."
439                                                return
440                                        end
441                                        statuses.push @favorites.last
442                                end
443                        else
444                                args.each do |tid|
445                                        if status = @tmap[tid]
446                                                statuses.push status
447                                        else
448                                                log "No such ID #{colored_tid(tid)}"
449                                        end
450                                end
451                        end
452                        @favorites ||= []
453                        statuses.each do |status|
454                                if method == "create" and @favorites.find {|i| i["id"] == status["id"] }
455                                        log "The status is already favorited! <#{permalink(status)}>"
456                                        next
457                                end
458                                res = api("favorites/#{method}/#{status["id"]}")
459                                log "#{entered}: #{res["user"]["screen_name"]}: #{res["text"]}"
460                                if method == "create"
461                                        @favorites.push res
462                                else
463                                        @favorites.delete_if {|i| i["id"] == res["id"] }
464                                end
465                                sleep 1
466                        end
467                when "link", "ln"
468                        args.each do |tid|
469                                if @tmap[tid]
470                                        log "#{colored_tid(tid)}: #{permalink(@tmap[tid])}"
471                                else
472                                        log "No such ID #{colored_tid(tid)}"
473                                end
474                        end
475#               when /^ratios?$/
476#                       unless args.empty?
477#                               if args.size < 2 or
478#                                  (@opts["mentions"] and args.size < 3)
479#                                       log "/me ratios <timeline> <frends>[ <mentions>]"
480#                                       return
481#                               end
482#                               ratios = args.map {|ratio| ratio.to_f }
483#                               if ratios.any? {|ratio| ratio <= 0.0 }
484#                                       log "Ratios must be greater than 0.0 and fractional values are permitted."
485#                                       return
486#                               end
487#                               footing = ratios.inject {|sum, ratio| sum + ratio }
488#                               @ratio[:timeline] = ratios[0]
489#                               @ratio[:friends]  = ratios[1]
490#                               @ratio[:mentions] = ratios[2] if @opts["mentions"]
491#                               @ratio.each_pair {|m, v| @ratio[m] = v / footing }
492#                       end
493#                       intervals = @ratio.map {|ratio| interval ratio }
494#                       log "Intervals: #{intervals.join(", ")}"
495                when /^(?:de(?:stroy|l(?:ete)?)|miss|oops|r(?:emove|m))$/
496                # destroy, delete, del, remove, rm, miss, oops
497                        statuses = []
498                        if args.empty?
499                                statuses.push @me["status"]
500                        else
501                                args.each do |tid|
502                                        if status = @tmap[tid]
503                                                if status["user"]["screen_name"] == @nick
504                                                        statuses.push status
505                                                else
506                                                        log "The status you specified by the ID #{colored_tid(tid)} is not yours."
507                                                end
508                                        else
509                                                log "No such ID #{colored_tid(tid)}"
510                                        end
511                                end
512                        end
513                        b = false
514                        statuses.each do |status|
515                                res = api("statuses/destroy/#{status["id"]}")
516                                @tmap.delete_if {|k, v| v["id"] == res["id"] }
517                                b = status["id"] == @me["status"]["id"]
518                                log "Destroyed: #{res["text"]}"
519                                sleep 1
520                        end
521                        if b
522                                @me = api("account/verify_credentials")
523                                post @prefix, TOPIC, main_channel, generate_status_message(@me["status"])
524                        end
525                when "name"
526                        name = mesg.split(/\s+/, 3)[2]
527                        unless name.nil?
528                                api("account/update_profile", { :name => name })
529                                log "You are named #{name}."
530                        end
531                when "email"
532                        # FIXME
533                        email = args.first
534                        unless email.nil?
535                                api("account/update_profile", { :email => email })
536                        end
537                when "url"
538                        # FIXME
539                        url = args.first || ""
540                        api("account/update_profile", { :url => url })
541                when "in", "location"
542                        location = mesg.split(/\s+/, 3)[2] || ""
543                        api("account/update_profile", { :location => location })
544                        location = location.empty? ? "nowhere" : "in #{location}"
545                        log "You are #{location} now."
546                when /^desc(?:ription)?$/
547                        # FIXME
548                        description = mesg.split(/\s+/, 3)[2] || ""
549                        api("account/update_profile", { :description => description })
550#               when /^colou?rs?$/
551#                       # FIXME
552#                       # bg, text, link, fill and border
553#               when "image", "img"
554#                       # FIXME
555#                       url = args.first
556#                       # TODO: DCC SEND
557#               when "follow"
558#                       # FIXME
559#               when "leave"
560#                       # FIXME
561                when /^(?:mention|re(?:ply)?)$/ # reply, re, mention
562                        tid = args.first
563                        if status = @tmap[tid]
564                                text = mesg.split(/\s+/, 4)[3]
565                                src  = @sources[rand(@sources.size)].first
566                                ret  = api("statuses/update", { :status => text, :source => src,
567                                                                :in_reply_to_status_id => "#{status["id"]}" })
568                                if ret["truncated"]
569                                        log "Oops! Your update was over 140 characters. We sent the short version" <<
570                                            " to your friends (they can view the entire update on the Web <" <<
571                                            permalink(ret) << ">)."
572                                end
573                                msg = generate_status_message(status)
574                                url = permalink(status)
575                                log "Status updated (In reply to #{colored_tid(tid)}: #{msg} <#{url}>)"
576                                ret.delete("user")
577                                @me.update("status" => ret)
578                        end
579                when /^spoo(o+)?f$/
580                        @sources = args.empty? \
581                                 ? @sources.size == 1 || $1 ? fetch_sources($1 && $1.size) \
582                                                            : [[api_source, "tig.rb"]] \
583                                 : args.map {|source| [source.upcase != "WEB" ? source : "", "=#{source}"] }
584                        log @sources.map {|source| source[1] }.sort.join(", ")
585                end
586        rescue APIFailed => e
587                log e.inspect
588        end
589
590        def on_whois(m)
591                nick  = m.params[0]
592                users = []
593                users.push @me if @me
594                users.concat @friends if @friends
595                f = users.find {|i| i["screen_name"].upcase == nick.upcase } ||
596                    api("users/show/#{nick}") rescue nil
597                if f
598                        host = hostname f
599                        desc = f["name"]
600                        desc << " / #{f["description"]}".gsub(/\s+/, " ") unless f["description"].empty?
601                        idle = (Time.now - Time.parse(f["status"]["created_at"])).to_i rescue 0
602                        sion = Time.parse(f["created_at"]).to_i rescue 0
603                        post server_name, RPL_WHOISUSER,   @nick, nick, "#{f["id"]}", host, "*", desc
604                        post server_name, RPL_WHOISSERVER, @nick, nick, api_base.host, "SoMa neighborhood of San Francisco, CA"
605                        post server_name, RPL_WHOISIDLE,   @nick, nick, "#{idle}", "#{sion}", "seconds idle, signon time"
606                        post server_name, RPL_ENDOFWHOIS,  @nick, nick, "End of WHOIS list"
607                else
608                        post server_name, ERR_NOSUCHNICK, nick, "No such nick/channel"
609                end
610        end
611
612        def on_who(m)
613                channel = m.params[0]
614                case
615                when channel.downcase == main_channel
616                        users = []
617                        users.push @me if @me
618                        users.concat @friends if @friends
619                        users.each {|friend| whoreply channel, friend }
620                        post server_name, RPL_ENDOFWHO, @nick, channel
621                when @groups.key?(channel)
622                        @groups[channel].each do |name|
623                                whoreply channel, @friends.find {|i| i["screen_name"] == name }
624                        end
625                        post server_name, RPL_ENDOFWHO, @nick, channel
626                else
627                        post server_name, ERR_NOSUCHNICK, @nick, "No such nick/channel"
628                end
629                def whoreply(channel, u)
630                        #     "<channel> <user> <host> <server> <nick>
631                        #         ( "H" / "G" > ["*"] [ ( "@" / "+" ) ]
632                        #             :<hopcount> <real name>"
633                        nick = u["screen_name"]
634                        user = u["id"].to_s
635                        host = hostname u
636                        serv = api_base.host
637                        real = u["name"]
638                        post server_name, RPL_WHOREPLY, @nick, channel, user, host, serv, nick, "H*@", "0 #{real}"
639                end
640        end
641
642        def on_join(m)
643                channels = m.params[0].split(/\s*,\s*/)
644                channels.each do |channel|
645                        next if channel.downcase == main_channel
646
647                        @channels << channel
648                        @channels.uniq!
649                        post @prefix, JOIN, channel
650                        post server_name, MODE, channel, "+o", @nick
651                        save_config
652                end
653        end
654
655        def on_part(m)
656                channel = m.params[0]
657                return if channel.downcase == main_channel
658
659                @channels.delete(channel)
660                post @nick, PART, channel, "Ignore group #{channel}, but setting is alive yet."
661        end
662
663        def on_invite(m)
664                nick, channel = *m.params
665                return if channel.downcase == main_channel
666
667                f = (@friends || []).find {|i| i["screen_name"].upcase == nick.upcase }
668                if f
669                        ((@groups[channel] ||= []) << f["screen_name"]).uniq!
670                        post generate_prefix(f), JOIN, channel
671                        post server_name, MODE, channel, "+v", nick
672                        save_config
673                else
674                        post server_name, ERR_NOSUCHNICK, nick, "No such nick/channel"
675                end
676        end
677
678        def on_kick(m)
679                channel, nick, mes = *m.params
680                return if channel == main_channel
681
682                f = (@friends || []).find {|i| i["screen_name"].upcase == nick.upcase }
683                if f
684                        (@groups[channel] ||= []).delete(f["screen_name"])
685                        post nick, PART, channel
686                        save_config
687                else
688                        post server_name, ERR_NOSUCHNICK, nick, "No such nick/channel"
689                end
690        end
691
692        private
693        def check_timeline
694                q = { :count => "200" }
695                q[:since_id] = @timeline.last.to_s if @timeline.last
696                api("statuses/friends_timeline", q).reverse_each do |status|
697                        id = status["id"]
698                        next if id.nil? or @timeline.include?(id)
699
700                        @timeline << id
701                        user = status["user"]
702                        nick = user["screen_name"]
703                        mesg = generate_status_message(status)
704                        tid  = @tmap.push(status)
705
706                        if @opts["tid"]
707                                mesg << " " << colored_tid(tid)
708                        end
709
710                        @log.debug [id, nick, mesg]
711                        if nick == @nick # 自分のときは TOPIC に
712                                post @prefix, TOPIC, main_channel, mesg
713                        else
714                                message(user, main_channel, mesg)
715                        end
716                        @groups.each do |channel, members|
717                                next unless members.include?(nick)
718                                message(user, channel, mesg)
719                        end
720                end
721                @log.debug "@timeline.size = #{@timeline.size}"
722                @timeline = @timeline.last(200)
723        end
724
725        def generate_status_message(status)
726                mesg = status["text"]
727                @log.debug mesg
728
729                mesg = decode_utf7(mesg)
730                # time = Time.parse(status["created_at"]) rescue Time.now
731                m = { "&quot;" => "\"", "&lt;" => "<", "&gt;" => ">", "&amp;" => "&", "\n" => " " }
732                mesg = mesg.gsub(Regexp.union(*m.keys)) { m[$&] }
733                mesg = mesg.sub(/\s*#{Regexp.union(*@suffix_bl)}\s*$/, "") if @suffix_bl
734                mesg = untinyurl(mesg)
735        end
736
737        def generate_prefix(u, athack = false)
738                nick = u["screen_name"]
739                nick = "@#{nick}" if athack
740                user = u["id"]
741                host = hostname u
742                "#{nick}!#{user}@#{host}"
743        end
744
745        def check_mentions
746                time = @prev_time_m || Time.now
747                @prev_time_m = Time.now
748                api("statuses/mentions").reverse_each do |mention|
749                        id = mention["id"]
750                        next if id.nil? or @timeline.include?(id)
751
752                        created_at = Time.parse(mention["created_at"]) rescue next
753                        next if created_at < time
754
755                        @timeline << id
756                        user = mention["user"]
757                        mesg = generate_status_message(mention)
758                        tid  = @tmap.push(mention)
759
760                        if @opts["tid"]
761                                mesg << " " << colored_tid(tid)
762                        end
763
764                        @log.debug [id, user["screen_name"], mesg]
765                        message(user, main_channel, mesg)
766                end
767        end
768
769        def check_direct_messages
770                time = @prev_time_d || Time.now
771                @prev_time_d = Time.now
772                api("direct_messages", { :since => time.httpdate }).reverse_each do |mesg|
773                        user = mesg["sender"]
774                        text = mesg["text"]
775                        time = Time.parse(mesg["created_at"])
776                        @log.debug [user["screen_name"], text, time].inspect
777                        message(user, @nick, text)
778                end
779        end
780
781        def check_friends
782                first = true unless @friends
783                athack = @opts["athack"]
784                @friends ||= []
785                friends = api("statuses/friends")
786                if first and not athack
787                        names_list = friends.map {|i| "+#{i["screen_name"]}" }.join(" ")
788                        post server_name, RPL_NAMREPLY,   @nick, "=", main_channel, names_list
789                        post server_name, RPL_ENDOFNAMES, @nick, main_channel, "End of NAMES list"
790                else
791                        prv_friends = @friends.map {|friend| generate_prefix friend, athack }
792                        now_friends = friends.map {|friend| generate_prefix friend, athack }
793
794                        # Twitter API bug?
795                        return if not first and (now_friends.length - prv_friends.length).abs > 10
796
797                        (prv_friends - now_friends).each {|part| post part, PART, main_channel, "" }
798                        sleep 1
799                        (now_friends - prv_friends).each {|join| post join, JOIN, main_channel }
800                end
801                @friends = friends
802        end
803
804        def interval(ratio)
805                max   = (@opts["maxlimit"] || 100).to_i
806                limit = @hourly_limit < max ? @hourly_limit : max
807                f     = 3600 / (limit * ratio).round rescue nil
808                @log.debug "Interval: #{f} seconds"
809                f
810        end
811
812        def start_jabber(jid, pass)
813                @log.info "Logging-in with #{jid} -> jabber_bot_id: #{jabber_bot_id}"
814                @im = Jabber::Simple.new(jid, pass)
815                @im.add(jabber_bot_id)
816                @im_thread = Thread.start do
817                        loop do
818                                begin
819                                        @im.received_messages.each do |msg|
820                                                @log.debug [msg.from, msg.body]
821                                                if msg.from.strip == jabber_bot_id
822                                                        # Twitter -> 'id: msg'
823                                                        body = msg.body.sub(/^(.+?)(?:\(([^()]+)\))?: /, "")
824                                                        body = decode_utf7(body)
825
826                                                        if Regexp.last_match
827                                                                nick, id = Regexp.last_match.captures
828                                                                body = untinyurl(CGI.unescapeHTML(body))
829                                                                user = nick
830                                                                nick = id || nick
831                                                                nick = @nicknames[nick] || nick
832                                                                post "#{nick}!#{user}@#{api_base.host}", PRIVMSG, main_channel, body
833                                                        end
834                                                end
835                                        end
836                                rescue Exception => e
837                                        @log.error "Error on Jabber loop: #{e.inspect}"
838                                        e.backtrace.each do |l|
839                                                @log.error "\t#{l}"
840                                        end
841                                end
842                                sleep 1
843                        end
844                end
845        end
846
847        def save_config
848                config = {
849                        :channels => @channels,
850                        :groups   => @groups,
851                }
852                @config.open("w") do |f|
853                        YAML.dump(config, f)
854                end
855        end
856
857        def load_config
858                @config.open do |f|
859                        config = YAML.load(f)
860                        @channels = config[:channels]
861                        @groups   = config[:groups]
862                end
863        rescue Errno::ENOENT
864        end
865
866        def require_post?(path)
867                %r{
868                        ^
869                        (?: status(?:es)?/update $
870                          | direct_messages/new $
871                          | friendships/create/
872                          | account/ (?: end_session $
873                                       | update_ )
874                          | favou?ri(?:ing|tes)/create/
875                          | notifications/
876                          | blocks/create/ )
877                }x === path
878        end
879
880        def api(path, q = {}, opt = {})
881                q["source"] ||= api_source
882
883                path     = path.sub(%r{^/+}, "")
884                uri      = api_base.dup
885                uri.port = 443 if @opts["secure"]
886                uri.path += "#{path}.json"
887                uri.query = q.inject([]) {|r,(k,v)| v ? r << "#{k}=#{URI.escape(v, /[^-.!~*'()\w]/n)}" : r }.join("&")
888                req = case
889                when path.include?("/destroy/") then Net::HTTP::Delete.new uri.request_uri
890                when require_post?(path)        then Net::HTTP::Post.new   uri.path
891                else                                 Net::HTTP::Get.new    uri.request_uri
892                end
893                req.body = uri.query if req.request_body_permitted?
894                req.add_field("User-Agent", @user_agent)
895                req.add_field("If-Modified-Since", q["since"]) if q.key?("since")
896                req.basic_auth(@real, @pass)
897                @log.debug uri.inspect
898
899                http = case
900                when /^(?:([^:@]+)(?::([^@]+))?@)?([^:]+)(?::(\d+))?$/ === @opts["httpproxy"]
901                        Net::HTTP.new(uri.host, uri.port, $3, $4.to_i, $1, $2)
902                when ENV["HTTP_PROXY"], ENV["http_proxy"]
903                        proxy = URI(ENV["HTTP_PROXY"] || ENV["http_proxy"])
904                        Net::HTTP.new(uri.host, uri.port, proxy.host, proxy.port, proxy.user, proxy.password)
905                else
906                        Net::HTTP.new(uri.host, uri.port)
907                end
908                http.use_ssl     = !@opts["secure"].nil?
909                http.verify_mode = OpenSSL::SSL::VERIFY_NONE if http.use_ssl?
910
911                ret = http.request req
912
913                if ret.key?("X-RateLimit-Limit")
914                        hourly_limit = ret["X-RateLimit-Limit"].to_i
915                        if @hourly_limit != hourly_limit
916                                msg = "The rate limit per hour was changed: #{@hourly_limit} to #{hourly_limit}"
917                                log msg
918                                @log.info msg
919                                @hourly_limit = hourly_limit
920                        end
921                end
922
923                case ret
924                when Net::HTTPOK # 200
925                        JSON.parse ret.body
926                when Net::HTTPNotModified # 304
927                        []
928                when Net::HTTPBadRequest # 400: exceeded the rate limitation
929                        if ret.key?("X-RateLimit-Reset")
930                                s = Time.now - Time.at(ret["X-RateLimit-Reset"].to_i)
931                                log "#{(s / 60.0).ceil} min remaining."
932                        end
933                        raise APIFailed, "#{ret.code}: #{ret.message}"
934                else
935                        raise APIFailed, "Server Returned #{ret.code} #{ret.message}"
936                end
937        rescue Errno::ETIMEDOUT, JSON::ParserError, IOError, Timeout::Error, Errno::ECONNRESET => e
938                raise APIFailed, e.inspect
939        end
940
941        def message(sender, target, str)
942#               str.gsub!(/&#(x)?([0-9a-f]+);/i) do
943#                       [$1 ? $2.hex : $2.to_i].pack("U")
944#               end
945                screen_name = sender["screen_name"]
946                sender["screen_name"] = @nicknames[screen_name] || screen_name
947                prefix = generate_prefix(sender)
948                post prefix, PRIVMSG, target, str
949        end
950
951        def log(str)
952                str.gsub!(/\r\n|[\r\n]/, " ")
953                post server_name, NOTICE, main_channel, str
954        end
955
956        def untinyurl(text)
957                text.gsub(%r{
958                        http://
959                        (?:
960                         (?: (preview\.)? tin | rub) yurl\.com
961                           | is\.gd | bit\.ly | ff\.im | twurl.nl | blip\.fm
962                        )
963                        /~?[0-9a-z=-]+
964                }ix) do |url|
965                        uri = URI(url)
966                        uri.host = uri.host.sub($1, "") if $1
967                        req = Net::HTTP::Head.new uri.request_uri
968                        req.add_field "User-Agent", @user_agent
969                        /^(?:([^:@]+)(?::([^@]+))?@)?([^:]+)(?::(\d+))?$/.match(@opts["httpproxy"])
970                        http = Net::HTTP.new uri.host, uri.port, $3, $4.to_i, $1, $2
971                        http.open_timeout = 3
972                        http.request(req) do |res|
973                                begin
974                                        if res.is_a?(Net::HTTPRedirection) and res.key?("Location")
975                                                url = res["Location"]
976                                        end
977                                rescue Timeout::Error
978                                end
979                        end
980                        url
981                end
982        end
983
984        def decode_utf7(str)
985                begin
986                        require "iconv"
987                        str = str.sub(/^.+ > |^.+/) {|m| Iconv.iconv("UTF-8", "UTF-7", m).join }
988                        #FIXME str = "[utf7]: #{str}" if str =~ /[^a-z0-9\s]/i
989                        str
990                rescue LoadError
991                rescue Iconv::IllegalSequence
992                end
993                str
994        end
995
996        def fetch_sources(n = nil)
997                json = http_get URI("http://wedata.net/databases/TwitterSources/items.json")
998                sources = JSON.parse json
999                sources.map! {|item| [item["data"]["source"], item["name"]] }.push ["", "web"]
1000                if n.is_a?(Integer) and n < sources.size
1001                        sources = Array.new(n) { sources.delete_at(rand(sources.size)) }.compact
1002                end
1003                sources
1004        rescue => e
1005                @log.error e.inspect
1006                log "An error occured while loading wedata.net."
1007                @sources
1008        end
1009
1010        def fetch_suffix_bl(r = [])
1011                source = http_get URI("http://svn.coderepos.org/share/platform/twitterircgateway/suffixesblacklist.txt")
1012                if source.respond_to?(:encoding) and source.encoding == Encoding::BINARY
1013                        source.force_encoding("UTF-8")
1014                end
1015                source.split
1016        rescue
1017                r
1018        end
1019
1020        def http_get(uri)
1021                req = Net::HTTP::Get.new uri.request_uri
1022                req.add_field("User-Agent", @user_agent)
1023                /^(?:([^:@]+)(?::([^@]+))?@)?([^:]+)(?::(\d+))?$/.match(@opts["httpproxy"])
1024                http = Net::HTTP.new(uri.host, uri.port, $3, $4.to_i, $1, $2)
1025                res = http.request req
1026                res.body
1027        end
1028
1029        def hostname(user)
1030                user["protected"] ? "protected.#{api_base.host}" : api_base.host
1031        end
1032
1033        def colored_tid(tid)
1034                "\x03%s[%s]\x0f" % [@opts["tid"] || 10, tid]
1035        end
1036
1037        def permalink(status)
1038                "#{api_base}#{status["user"]["screen_name"]}/statuses/#{status["id"]}"
1039        end
1040
1041        class TypableMap < Hash
1042                Roman = %w[
1043                        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
1044                ].unshift("").map do |consonant|
1045                        case consonant
1046                        when "y", /^.{2}/ then %w|a u o|
1047                        when "q"          then %w|a i e o|
1048                        else                   %w|a i u e o|
1049                        end.map {|vowel| "#{consonant}#{vowel}" }
1050                end.flatten
1051
1052                def initialize(size = 1)
1053                        @seq  = Roman
1054                        @n    = 0
1055                        @size = size
1056                end
1057
1058                def generate(n)
1059                        ret = []
1060                        begin
1061                                n, r = n.divmod(@seq.size)
1062                                ret << @seq[r]
1063                        end while n > 0
1064                        ret.reverse.join
1065                end
1066
1067                def push(obj)
1068                        id = generate(@n)
1069                        self[id] = obj
1070                        @n += 1
1071                        @n %= @seq.size ** @size
1072                        id
1073                end
1074                alias << push
1075
1076                def clear
1077                        @n = 0
1078                        super
1079                end
1080
1081                private :[]=
1082                undef update, merge, merge!, replace
1083        end
1084
1085
1086end
1087
1088if __FILE__ == $0
1089        require "optparse"
1090
1091        opts = {
1092                :port  => 16668,
1093                :host  => "localhost",
1094                :log   => nil,
1095                :debug => false,
1096                :foreground => false,
1097        }
1098
1099        OptionParser.new do |parser|
1100                parser.instance_eval do
1101                        self.banner = <<-EOB.gsub(/^\t+/, "")
1102                                Usage: #{$0} [opts]
1103
1104                        EOB
1105
1106                        separator ""
1107
1108                        separator "Options:"
1109                        on("-p", "--port [PORT=#{opts[:port]}]", "port number to listen") do |port|
1110                                opts[:port] = port
1111                        end
1112
1113                        on("-h", "--host [HOST=#{opts[:host]}]", "host name or IP address to listen") do |host|
1114                                opts[:host] = host
1115                        end
1116
1117                        on("-l", "--log LOG", "log file") do |log|
1118                                opts[:log] = log
1119                        end
1120
1121                        on("--debug", "Enable debug mode") do |debug|
1122                                opts[:log]   = $stdout
1123                                opts[:debug] = true
1124                        end
1125
1126                        on("-f", "--foreground", "run foreground") do |foreground|
1127                                opts[:log]        = $stdout
1128                                opts[:foreground] = true
1129                        end
1130
1131                        on("-n", "--name [user name or email address]") do |name|
1132                                opts[:name] = name
1133                        end
1134
1135                        parse!(ARGV)
1136                end
1137        end
1138
1139        opts[:logger] = Logger.new(opts[:log], "daily")
1140        opts[:logger].level = opts[:debug] ? Logger::DEBUG : Logger::INFO
1141
1142#       def daemonize(foreground = false)
1143#               [:INT, :TERM, :HUP].each do |sig|
1144#                       Signal.trap sig, "EXIT"
1145#               end
1146#               return yield if $DEBUG or foreground
1147#               Process.fork do
1148#                       Process.setsid
1149#                       Dir.chdir "/"
1150#                       STDIN.reopen  "/dev/null"
1151#                       STDOUT.reopen "/dev/null", "a"
1152#                       STDERR.reopen STDOUT
1153#                       yield
1154#               end
1155#               exit! 0
1156#       end
1157
1158#       daemonize(opts[:debug] || opts[:foreground]) do
1159                Net::IRC::Server.new(opts[:host], opts[:port], TwitterIrcGateway, opts).start
1160#       end
1161end
Note: See TracBrowser for help on using the browser.