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

Revision 6250, 12.4 kB (checked in by cho45, 5 years ago)

lang/ruby/net-irc/trunk/examples/nig.rb,
lang/ruby/net-irc/trunk/examples/tig.rb,
lang/ruby/net-irc/trunk/examples/wig.rb:

ドキュメント更新。デバッグメッセージの位置を変更

  • 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 # daemonized
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
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 (ex. 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### jabber=<jid>:<pass>
44
45If `jabber=<jid>:<pass>` option specified,
46use jabber to get friends timeline.
47
48You must setup im notifing settings in the site and
49install 'xmpp4r-simple' gem.
50
51        $ sudo gem install xmpp4r-simple
52
53Be careful for managing password.
54
55
56## Licence
57
58Ruby's by cho45
59
60=end
61
62$LOAD_PATH << "lib"
63$LOAD_PATH << "../lib"
64
65$KCODE = "u" # json use this
66
67require "rubygems"
68require "net/http"
69require "net/irc"
70require "uri"
71require "json"
72require "socket"
73require "time"
74require "logger"
75require "yaml"
76require "pathname"
77require "digest/md5"
78
79Net::HTTP.version_1_2
80
81class TwitterIrcGateway < Net::IRC::Server::Session
82        def server_name
83                "twittergw"
84        end
85
86        def server_version
87                "0.0.0"
88        end
89
90        def main_channel
91                "#twitter"
92        end
93
94        def api_base
95                URI("http://twitter.com/")
96        end
97
98        def api_source
99                "tigrb"
100        end
101
102        def jabber_bot_id
103                "twitter@twitter.com"
104        end
105
106        class ApiFailed < StandardError; end
107
108        def initialize(*args)
109                super
110                @groups = {}
111                @channels = [] # join channels (groups)
112                @user_agent = "#{self.class}/#{server_version} (tig.rb)"
113                @config = Pathname.new(ENV["HOME"]) + ".tig"
114                load_config
115        end
116
117        def on_user(m)
118                super
119                post @prefix, JOIN, main_channel
120                post server_name, MODE, main_channel, "+o", @prefix.nick
121
122                @real, *@opts = @real.split(/\s+/)
123                @opts ||= []
124
125                jabber = @opts.find {|i| i =~ /^jabber=(\S+?):(\S+)/ }
126                if jabber
127                        jid, pass = Regexp.last_match.captures
128                        jabber.replace("jabber=#{jid}:********")
129                        if jabber_bot_id
130                                begin
131                                        require "xmpp4r-simple"
132                                        start_jabber(jid, pass)
133                                rescue LoadError
134                                        log "Failed to start Jabber."
135                                        log "Installl 'xmpp4r-simple' gem or check your id/pass."
136                                        finish
137                                end
138                        else
139                                jabber = nil
140                                log "This gateway does not support Jabber bot."
141                        end
142                end
143
144                @log.info "Client Options: #{@opts.inspect}"
145
146                @timeline = []
147                Thread.start do
148                        loop do
149                                begin
150                                        check_friends
151                                rescue ApiFailed => e
152                                        @log.error e.inspect
153                                rescue Exception => e
154                                        @log.error e.inspect
155                                        e.backtrace.each do |l|
156                                                @log.error "\t#{l}"
157                                        end
158                                end
159                                sleep 10 * 60
160                        end
161                end
162                sleep 3
163
164                return if jabber
165
166                Thread.start do
167                        loop do
168                                begin
169                                        check_timeline
170                                        # check_direct_messages
171                                rescue ApiFailed => e
172                                        @log.error e.inspect
173                                rescue Exception => e
174                                        @log.error e.inspect
175                                        e.backtrace.each do |l|
176                                                @log.error "\t#{l}"
177                                        end
178                                end
179                                sleep 90
180                        end
181                end
182        end
183
184        def on_privmsg(m)
185                retry_count = 3
186                ret = nil
187                target, message = *m.params
188                begin
189                        if target =~ /^#/
190                                ret = api("statuses/update", {"status" => message})
191                        else
192                                # direct message
193                                ret = api("direct_messages/new", {"user" => target, "text" => message})
194                        end
195                        raise ApiFailed, "api failed" unless ret
196                        log "Status Updated"
197                rescue => e
198                        @log.error [retry_count, e.inspect].inspect
199                        if retry_count > 0
200                                retry_count -= 1
201                                @log.debug "Retry to setting status..."
202                                retry
203                        else
204                                log "Some Error Happened on Sending #{message}. #{e}"
205                        end
206                end
207        end
208
209        def on_whois(m)
210                nick = m.params[0]
211                f = (@friends || []).find {|i| i["screen_name"] == nick }
212                if f
213                        post nil, RPL_WHOISUSER,   @nick, nick, nick, api_base.host, "*", "#{f["name"]} / #{f["description"]}"
214                        post nil, RPL_WHOISSERVER, @nick, nick, api_base.host, api_base.to_s
215                        post nil, RPL_WHOISIDLE,   @nick, nick, "0", "seconds idle"
216                        post nil, RPL_ENDOFWHOIS,  @nick, nick, "End of WHOIS list"
217                else
218                        post nil, ERR_NOSUCHNICK, nick, "No such nick/channel"
219                end
220        end
221
222        def on_who(m)
223                channel = m.params[0]
224                case
225                when channel == main_channel
226                        #     "<channel> <user> <host> <server> <nick>
227                        #         ( "H" / "G" > ["*"] [ ( "@" / "+" ) ]
228                        #             :<hopcount> <real name>"
229                        @friends.each do |f|
230                                user = nick = f["screen_name"]
231                                host = serv = api_base.host
232                                real = f["name"]
233                                post nil, RPL_WHOREPLY, @nick, channel, user, host, serv, nick, "H*@", "0 #{real}"
234                        end
235                        post nil, RPL_ENDOFWHO, @nick, channel
236                when @groups.key?(channel)
237                        @groups[channel].each do |name|
238                                f = @friends.find {|i| i["screen_name"] == name }
239                                user = nick = f["screen_name"]
240                                host = serv = api_base.host
241                                real = f["name"]
242                                post nil, RPL_WHOREPLY, @nick, channel, user, host, serv, nick, "H*@", "0 #{real}"
243                        end
244                        post nil, RPL_ENDOFWHO, @nick, channel
245                else
246                        post nil, ERR_NOSUCHNICK, @nick, nick, "No such nick/channel"
247                end
248        end
249
250        def on_join(m)
251                channels = m.params[0].split(/\s*,\s*/)
252                channels.each do |channel|
253                        next if channel == main_channel
254
255                        @channels << channel
256                        @channels.uniq!
257                        post "#{@nick}!#{@nick}@#{api_base.host}", JOIN, channel
258                        post server_name, MODE, channel, "+o", @nick
259                        save_config
260                end
261        end
262
263        def on_part(m)
264                channel = m.params[0]
265                return if channel == main_channel
266
267                @channels.delete(channel)
268                post @nick, PART, channel, "Ignore group #{channel}, but setting is alive yet."
269        end
270
271        def on_invite(m)
272                nick, channel = *m.params
273                return if channel == main_channel
274
275                if (@friends || []).find {|i| i["screen_name"] == nick }
276                        ((@groups[channel] ||= []) << nick).uniq!
277                        post "#{nick}!#{nick}@#{api_base.host}", JOIN, channel
278                        post server_name, MODE, channel, "+o", nick
279                        save_config
280                else
281                        post ERR_NOSUCHNICK, nil, nick, "No such nick/channel"
282                end
283        end
284
285        def on_kick(m)
286                channel, nick, mes = *m.params
287                return if channel == main_channel
288
289                if (@friends || []).find {|i| i["screen_name"] == nick }
290                        (@groups[channel] ||= []).delete(nick)
291                        post nick, PART, channel
292                        save_config
293                else
294                        post ERR_NOSUCHNICK, nil, nick, "No such nick/channel"
295                end
296        end
297
298        private
299        def check_timeline
300                first = true unless @prev_time
301                @prev_time = Time.at(0) if first
302                api("statuses/friends_timeline", {"since" => [@prev_time.httpdate]}).reverse_each do |s|
303                        nick = s["user"]["screen_name"]
304                        mesg = s["text"]
305                        # display photo url(wassr only)
306                        if s.has_key?('photo_url')
307                                mesg += " #{s['photo_url']}"
308                        end
309                        # time = Time.parse(s["created_at"]) rescue Time.now
310                        m = { "&quot;" => "\"", "&lt;"=> "<", "&gt;"=> ">", "&amp;"=> "&", "\n" => " "}
311                        mesg.gsub!(/(#{m.keys.join("|")})/) { m[$1] }
312
313                        digest = Digest::MD5.hexdigest("#{nick}::#{mesg}")
314                        unless @timeline.include?(digest)
315                                @timeline << digest
316                                @log.debug [nick, mesg]
317                                if nick == @nick # 自分のときは topic に
318                                        post nick, TOPIC, main_channel, mesg
319                                else
320                                        message(nick, main_channel, mesg)
321                                end
322                                @groups.each do |channel,members|
323                                        if members.include?(nick)
324                                                message(nick, channel, mesg)
325                                        end
326                                end
327                        end
328                end
329                @log.debug "@timeline.size = #{@timeline.size}"
330                @timeline  = @timeline.last(100)
331                @prev_time = Time.now
332        end
333
334        def check_direct_messages
335                first = true unless @prev_time_d
336                @prev_time_d = Time.now if first
337                api("direct_messages", {"since" => [@prev_time_d.httpdate] }).reverse_each do |s|
338                        nick = s["sender_screen_name"]
339                        mesg = s["text"]
340                        time = Time.parse(s["created_at"])
341                        @log.debug [nick, mesg, time].inspect
342                        message(nick, @nick, mesg)
343                end
344                @prev_time_d = Time.now
345        end
346
347        def check_friends
348                first = true unless @friends
349                @friends ||= []
350                friends = api("statuses/friends")
351                if first && !@opts.include?("athack")
352                        @friends = friends
353                        post nil, RPL_NAMREPLY,   @nick, "=", main_channel, @friends.map{|i| "@#{i["screen_name"]}" }.join(" ")
354                        post nil, RPL_ENDOFNAMES, @nick, main_channel, "End of NAMES list"
355                else
356                        prv_friends = @friends.map {|i| i["screen_name"] }
357                        now_friends = friends.map {|i| i["screen_name"] }
358                        (now_friends - prv_friends).each do |join|
359                                join = "@#{join}" if @opts.include?("athack")
360                                post "#{join}!#{join}@#{api_base.host}", JOIN, main_channel
361                        end
362                        (prv_friends - now_friends).each do |part|
363                                part = "@#{part}" if @opts.include?("athack")
364                                post "#{part}!#{part}@#{api_base.host}", PART, main_channel, ""
365                        end
366                        @friends = friends
367                end
368        end
369
370        def start_jabber(jid, pass)
371                @log.info "Logging-in with #{jid} -> jabber_bot_id: #{jabber_bot_id}"
372                im = Jabber::Simple.new(jid, pass)
373                im.add(jabber_bot_id)
374                Thread.start do
375                        loop do
376                                begin
377                                        im.received_messages.each do |msg|
378                                                @log.debug msg.inspect
379                                                if msg.from.strip == jabber_bot_id
380                                                        body = msg.body.sub(/^(.+)(?:\((.+?)\))?: /, "")
381                                                        if Regexp.last_match
382                                                                nick, id = Regexp.last_match.captures
383                                                                message(nick, main_channel, body)
384                                                        end
385                                                end
386                                        end
387                                rescue Exception => e
388                                        @log.error "Error on Jabber loop: #{e.inspect}"
389                                        e.backtrace.each do |l|
390                                                @log.error "\t#{l}"
391                                        end
392                                end
393                                sleep 1
394                        end
395                end
396        end
397
398        def save_config
399                config = {
400                        :channels => @channels,
401                        :groups => @groups,
402                }
403                @config.open("w") do |f|
404                        YAML.dump(config, f)
405                end
406        end
407
408        def load_config
409                @config.open do |f|
410                        config = YAML.load(f)
411                        @channels = config[:channels]
412                        @groups   = config[:groups]
413                end
414        rescue Errno::ENOENT
415        end
416
417        def api(path, q={})
418                ret = {}
419                path = path.sub(%r{^/*}, '/') << '.json'
420                q["source"] = api_source
421                q = q.inject([]) {|r,(k,v)| v.inject(r) {|r,i| r << "#{k}=#{URI.escape(i, /[^-.!~*'()\w]/n)}" } }.join("&")
422                uri = api_base.dup
423                uri.path  = path
424                uri.query = q
425                @log.debug uri.inspect
426                Net::HTTP.start(uri.host, uri.port) do |http|
427                        header = {
428                                'Authorization' => "Basic " + ["#{@real}:#{@pass}"].pack("m"),
429                                'User-Agent'    => @user_agent,
430                        }
431                        case path
432                        when "/statuses/update.json", "/direct_messages/new.json"
433                                ret = http.post(uri.request_uri, q, header)
434                        else
435                                ret = http.get(uri.request_uri, header)
436                        end
437                end
438                @log.debug ret.inspect
439                case ret.code
440                when "200"
441                        JSON.parse(ret.body.gsub(/'(y(?:es)?|n(?:o)?|true|false|null)'/, '"\1"'))
442                when "304"
443                        []
444                else
445                        raise ApiFailed, "Server Returned #{ret.code}"
446                end
447        rescue Errno::ETIMEDOUT, JSON::ParserError, IOError, Timeout::Error, Errno::ECONNRESET => e
448                raise ApiFailed, e.inspect
449        end
450
451        def message(sender, target, str)
452#                       str.gsub!(/&#(x)?([0-9a-f]+);/i) do |m|
453#                               [$1 ? $2.hex : $2.to_i].pack("U")
454#                       end
455                str = untinyurl(str)
456                sender =  "#{sender}!#{sender}@#{api_base.host}"
457                post sender, PRIVMSG, target, str
458        end
459
460        def log(str)
461                str.gsub!(/\n/, " ")
462                post server_name, NOTICE, main_channel, str
463        end
464
465        def untinyurl(text)
466                text.gsub(%r|http://(preview\.)?tinyurl\.com/[0-9a-z=]+|i) {|m|
467                        uri = URI(m)
468                        uri.host = uri.host.sub($1, '') if $1
469                        Net::HTTP.start(uri.host, uri.port) {|http|
470                                http.open_timeout = 3
471                                begin
472                                        http.head(uri.request_uri, { 'User-Agent' => @user_agent })["Location"]
473                                rescue Timeout::Error
474                                        m
475                                end
476                        }
477                }
478        end
479end
480
481if __FILE__ == $0
482        require "optparse"
483
484        opts = {
485                :port  => 16668,
486                :host  => "localhost",
487                :log   => nil,
488                :debug => false,
489        }
490
491        OptionParser.new do |parser|
492                parser.instance_eval do
493                        self.banner  = <<-EOB.gsub(/^\t+/, "")
494                                Usage: #{$0} [opts]
495
496                        EOB
497
498                        separator ""
499
500                        separator "Options:"
501                        on("-p", "--port [PORT=#{opts[:port]}]", "port number to listen") do |port|
502                                opts[:port] = port
503                        end
504
505                        on("-h", "--host [HOST=#{opts[:host]}]", "host name or IP address to listen") do |host|
506                                opts[:host] = host
507                        end
508
509                        on("-l", "--log LOG", "log file") do |log|
510                                opts[:log] = log
511                        end
512
513                        on("--debug", "Enable debug mode") do |debug|
514                                opts[:log]   = $stdout
515                                opts[:debug] = true
516                        end
517
518                        parse!(ARGV)
519                end
520        end
521
522        opts[:logger] = Logger.new(opts[:log], "daily")
523        opts[:logger].level = opts[:debug] ? Logger::DEBUG : Logger::INFO
524
525        def daemonize(debug=false)
526                return yield if $DEBUG || debug
527                Process.fork do
528                        Process.setsid
529                        Dir.chdir "/"
530                        trap("SIGINT")  { exit! 0 }
531                        trap("SIGTERM") { exit! 0 }
532                        trap("SIGHUP")  { exit! 0 }
533                        File.open("/dev/null") {|f|
534                                STDIN.reopen  f
535                                STDOUT.reopen f
536                                STDERR.reopen f
537                        }
538                        yield
539                end
540                exit! 0
541        end
542
543        daemonize(opts[:debug]) do
544                Net::IRC::Server.new(opts[:host], opts[:port], TwitterIrcGateway, opts).start
545        end
546end
547
Note: See TracBrowser for help on using the browser.