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

Revision 5839, 10.4 kB (checked in by drry, 5 years ago)

lang/ruby/net-irc/trunk/examples/tig.rb: api_source を追加しました。User-Agent HTTP ヘッダを追加しました。split() の区切り正規表現を修正しました。api() の処理を変更しました。オプションの重複を除去しまいた。ほか。

  • Property svn:executable set to *
Line 
1#!/usr/bin/env ruby -Ku
2=begin
3
4# tig.rb
5
6Ruby version of TwitterIrcGateway
7( http://www.misuzilla.org/dist/net/twitterircgateway/ )
8
9
10## Client opts
11
12Options specified by after irc realname.
13
14Configuration example for Tiarra ( http://coderepos.org/share/wiki/Tiarra ).
15
16        twitter {
17                host: localhost
18                port: 16668
19                name: username@example.com athack
20                password: password on Twitter
21                in-encoding: utf8
22                out-encoding: utf8
23        }
24
25### athack
26
27If `athack` client options specified,
28all nick in join message is leading with @.
29
30So if you complemente nicks (ex. irssi),
31it's good for Twitter like reply command (@nick).
32
33In this case, you will see torrent of join messages after connected,
34because NAMES list can't send @ leading nick (it interpreted op.)
35
36## Licence
37
38Ruby's by cho45
39
40=end
41
42$LOAD_PATH << "lib"
43$LOAD_PATH << "../lib"
44
45$KCODE = "u" # json use this
46
47require "rubygems"
48require "net/http"
49require "net/irc"
50require "uri"
51require "json"
52require "socket"
53require "time"
54require "logger"
55require "yaml"
56require "pathname"
57require "digest/md5"
58
59Net::HTTP.version_1_2
60
61class TwitterIrcGateway < Net::IRC::Server::Session
62        def server_name
63                "twittergw"
64        end
65
66        def server_version
67                "0.0.0"
68        end
69
70        def main_channel
71                "#twitter"
72        end
73
74        def api_base
75                @api_base ||= URI("http://twitter.com/")
76        end
77
78        def api_source
79                @api_source ||= "tigrb"
80        end
81
82        class ApiFailed < StandardError; end
83
84        def initialize(*args)
85                super
86                @groups = {}
87                @channels = [] # join channels (groups)
88                @user_agent = "#{self.class}/#{server_version} (tig.rb)"
89                @config = Pathname.new(ENV["HOME"]) + ".tig"
90                load_config
91        end
92
93        def on_user(m)
94                super
95                post @mask, JOIN, main_channel
96                @real, @opts = @real.split(/\s+/)
97                @opts ||= []
98                @log.info "Client Options: #{@opts.inspect}"
99
100                @timeline = []
101                Thread.start do
102                        loop do
103                                begin
104                                        check_friends
105                                rescue ApiFailed => e
106                                        @log.error e.inspect
107                                rescue Exception => e
108                                        puts e
109                                        puts e.backtrace
110                                end
111                                sleep 10 * 60
112                        end
113                end
114                sleep 3
115                Thread.start do
116                        loop do
117                                begin
118                                        check_timeline
119                                        # check_direct_messages
120                                rescue ApiFailed => e
121                                        @log.error e.inspect
122                                rescue Exception => e
123                                        puts e
124                                        puts e.backtrace
125                                end
126                                sleep 90
127                        end
128                end
129        end
130
131        def on_privmsg(m)
132                retry_count = 3
133                ret = nil
134                target, message = *m.params
135                begin
136                        if target =~ /^#/
137                                ret = api("statuses/update", {"status" => message})
138                        else
139                                # direct message
140                                ret = api("direct_messages/new", {"user" => target, "text" => message})
141                        end
142                        raise ApiFailed, "api failed" unless ret
143                        log "Status Updated"
144                rescue => e
145                        @log.error [retry_count, e.inspect].inspect
146                        if retry_count > 0
147                                retry_count -= 1
148                                @log.debug "Retry to setting status..."
149                                retry
150                        else
151                                log "Some Error Happened on Sending #{message}. #{e}"
152                        end
153                end
154        end
155
156        def on_whois(m)
157                nick = m.params[0]
158                f = (@friends || []).find {|i| i["screen_name"] == nick }
159                if f
160                        post nil, RPL_WHOISUSER,   nick, nick, nick, api_base.host, "*", NKF.nkf("-j", "#{f["name"]} / #{f["description"]}")
161                        post nil, RPL_WHOISSERVER, nick, api_base.host, api_base.to_s
162                        post nil, RPL_WHOISIDLE,   nick, "0", "seconds idle"
163                        post nil, RPL_ENDOFWHOIS,  nick, "End of WHOIS list"
164                else
165                        post nil, ERR_NOSUCHNICK, nick, "No such nick/channel"
166                end
167        end
168
169        def on_who(m)
170                channel = m.params[0]
171                case
172                when channel == main_channel
173                        #     "<channel> <user> <host> <server> <nick>
174                        #         ( "H" / "G" > ["*"] [ ( "@" / "+" ) ]
175                        #             :<hopcount> <real name>"
176                        @friends.each do |f|
177                                user = nick = f["screen_name"]
178                                host = serv = api_base.host
179                                real = f["name"]
180                                post nil, RPL_WHOREPLY, channel, user, host, serv, nick, "H", "0 #{real}"
181                        end
182                        post nil, RPL_ENDOFWHO, channel
183                when @groups.key?(channel)
184                        @groups[channel].each do |name|
185                                f = @friends.find {|i| i["screen_name"] == name }
186                                user = nick = f["screen_name"]
187                                host = serv = api_base.host
188                                real = f["name"]
189                                post nil, RPL_WHOREPLY, channel, user, host, serv, nick, "H", "0 #{real}"
190                        end
191                        post nil, RPL_ENDOFWHO, channel
192                else
193                        post nil, ERR_NOSUCHNICK, nick, "No such nick/channel"
194                end
195        end
196
197        def on_join(m)
198                channels = m.params[0].split(/\s*,\s*/)
199                channels.each do |channel|
200                        next if channel == main_channel
201
202                        @channels << channel
203                        @channels.uniq!
204                        post "#{@nick}!#{@nick}@#{api_base.host}", JOIN, channel
205                        save_config
206                end
207        end
208
209        def on_part(m)
210                channel = m.params[0]
211                return if channel == main_channel
212
213                @channels.delete(channel)
214                post @nick, PART, channel, "Ignore group #{channel}, but setting is alive yet."
215        end
216
217        def on_invite(m)
218                nick, channel = *m.params
219                return if channel == main_channel
220
221                if (@friends || []).find {|i| i["screen_name"] == nick }
222                        ((@groups[channel] ||= []) << nick).uniq!
223                        post "#{nick}!#{nick}@#{api_base.host}", JOIN, channel
224                        save_config
225                else
226                        post ERR_NOSUCHNICK, nil, nick, "No such nick/channel"
227                end
228        end
229
230        def on_kick(m)
231                channel, nick, mes = *m.params
232                return if channel == main_channel
233
234                if (@friends || []).find {|i| i["screen_name"] == nick }
235                        (@groups[channel] ||= []).delete(nick)
236                        post nick, PART, channel
237                        save_config
238                else
239                        post ERR_NOSUCHNICK, nil, nick, "No such nick/channel"
240                end
241        end
242
243        private
244        def check_timeline
245                first = true unless @prev_time
246                @prev_time = Time.at(0) if first
247                api("statuses/friends_timeline", {"since" => [@prev_time.httpdate]}).reverse_each do |s|
248                        nick = s["user"]["screen_name"]
249                        mesg = s["text"]
250                        time = Time.parse(s["created_at"]) rescue Time.now
251                        m = { "&quot;" => "\"", "&lt;"=> "<", "&gt;"=> ">", "&amp;"=> "&", "\n" => " "}
252                        mesg.gsub!(/(#{m.keys.join("|")})/) { m[$1] }
253
254                        digest = Digest::MD5.hexdigest("#{nick}::#{mesg}")
255                        unless @timeline.include?(digest)
256                                @timeline << digest
257                                @log.debug [nick, mesg, time].inspect
258                                if nick == @nick # 自分のときは topic に
259                                        post nick, TOPIC, main_channel, mesg
260                                else
261                                        message(nick, main_channel, mesg)
262                                end
263                                @groups.each do |channel,members|
264                                        if members.include?(nick)
265                                                message(nick, channel, mesg)
266                                        end
267                                end
268                        end
269                end
270                @timeline  = @timeline.last(100)
271                @prev_time = Time.now
272        end
273
274        def check_direct_messages
275                first = true unless @prev_time_d
276                @prev_time_d = Time.now if first
277                api("direct_messages", {"since" => [@prev_time_d.httpdate] }).reverse_each do |s|
278                        nick = s["sender_screen_name"]
279                        mesg = s["text"]
280                        time = Time.parse(s["created_at"])
281                        @log.debug [nick, mesg, time].inspect
282                        message(nick, @nick, mesg)
283                end
284                @prev_time_d = Time.now
285        end
286
287        def check_friends
288                first = true unless @friends
289                @friends ||= []
290                friends = api("statuses/friends")
291                if first && !@opts.include?("athack")
292                        @friends = friends
293                        post nil, RPL_NAMREPLY,   server_name, @nick, "=", main_channel, @friends.map{|i| i["screen_name"] }.join(" ")
294                        post nil, RPL_ENDOFNAMES, server_name, @nick, main_channel, "End of NAMES list"
295                else
296                        prv_friends = @friends.map {|i| i["screen_name"] }
297                        now_friends = friends.map {|i| i["screen_name"] }
298                        (now_friends - prv_friends).each do |join|
299                                join = "@#{join}" if @opts.include?("athack")
300                                post "#{join}!#{join}@#{api_base.host}", JOIN, main_channel
301                        end
302                        (prv_friends - now_friends).each do |part|
303                                part = "@#{part}" if @opts.include?("athack")
304                                post "#{part}!#{part}@#{api_base.host}", PART, main_channel, ""
305                        end
306                        @friends = friends
307                end
308        end
309
310        def save_config
311                config = {
312                        :channels => @channels,
313                        :groups => @groups,
314                }
315                @config.open("w") do |f|
316                        YAML.dump(config, f)
317                end
318        end
319
320        def load_config
321                @config.open do |f|
322                        config = YAML.load(f)
323                        @channels = config[:channels]
324                        @groups   = config[:groups]
325                end
326        rescue Errno::ENOENT
327        end
328
329        def api(path, q={})
330                ret = {}
331                path = path.sub(%r{^/*}, '/') << '.json'
332                q["source"] = api_source
333                q = q.inject([]) {|r,(k,v)| v.inject(r) {|r,i| r << "#{k}=#{URI.escape(i, /[^-.!~*'()\w]/n)}" } }.join("&")
334                uri = api_base.dup
335                uri.path  = path
336                uri.query = q
337                @log.debug uri.inspect
338                Net::HTTP.start(uri.host, uri.port) do |http|
339                        header = {
340                                'Authorization' => "Basic " + ["#{@real}:#{@pass}"].pack("m"),
341                                'User-Agent'    => @user_agent,
342                        }
343                        case path
344                        when "statuses/update.json", "direct_messages/new.json"
345                                ret = http.post(uri.request_uri, q, header)
346                        else
347                                ret = http.get(uri.request_uri, header)
348                        end
349                end
350                @log.debug ret.inspect
351                case ret.code
352                when "200"
353                        JSON.parse(ret.body)
354                when "304"
355                        []
356                else
357                        raise ApiFailed, "Server Returned #{ret.code}"
358                end
359        rescue Errno::ETIMEDOUT, JSON::ParserError, IOError, Timeout::Error, Errno::ECONNRESET => e
360                raise ApiFailed, e.inspect
361        end
362
363        def message(sender, target, str)
364#                       str.gsub!(/&#(x)?([0-9a-f]+);/i) do |m|
365#                               [$1 ? $2.hex : $2.to_i].pack("U")
366#                       end
367                str = untinyurl(str)
368                sender =  "#{sender}!#{sender}@#{api_base.host}"
369                post sender, PRIVMSG, target, str
370        end
371
372        def log(str)
373                str.gsub!(/\n/, " ")
374                post server_name, NOTICE, main_channel, str
375        end
376
377        def untinyurl(text)
378                text.gsub(%r|http://(preview\.)?tinyurl\.com/[0-9a-z=]+|i) {|m|
379                        uri = URI(m)
380                        uri.host = uri.host.sub($1, '') if $1
381                        Net::HTTP.start(uri.host, uri.port) {|http|
382                                http.open_timeout = 3
383                                begin
384                                        http.head(uri.request_uri, { 'User-Agent' => @user_agent })["Location"]
385                                rescue Timeout::Error
386                                        m
387                                end
388                        }
389                }
390        end
391end
392
393if __FILE__ == $0
394        require "optparse"
395
396        opts = {
397                :port  => 16668,
398                :host  => "localhost",
399                :debug => false,
400                :log   => nil,
401        }
402
403        OptionParser.new do |parser|
404                parser.instance_eval do
405                        self.banner  = <<-EOB.gsub(/^\t+/, "")
406                                Usage: #{$0} [opts]
407
408                        EOB
409
410                        separator ""
411
412                        separator "Options:"
413                        on("-p", "--port [PORT=#{opts[:port]}]", "port number to listen") do |port|
414                                opts[:port] = port
415                        end
416
417                        on("-h", "--host [HOST=#{opts[:host]}]", "host name or IP address to listen") do |host|
418                                opts[:host] = host
419                        end
420
421                        on("-l", "--log LOG", "log file") do |log|
422                                opts[:log] = log
423                        end
424
425                        on("--debug", "Enable debug mode") do |debug|
426                                opts[:log]   = $stdout
427                                opts[:debug] = true
428                        end
429
430                        parse!(ARGV)
431                end
432        end
433
434        opts[:logger] = Logger.new(opts[:log], "daily")
435        opts[:logger].level = opts[:debug] ? Logger::DEBUG : Logger::INFO
436
437        def daemonize(debug=false)
438                return yield if $DEBUG || debug
439                Process.fork do
440                        Process.setsid
441                        Dir.chdir "/"
442                        trap("SIGINT")  { exit! 0 }
443                        trap("SIGTERM") { exit! 0 }
444                        trap("SIGHUP")  { exit! 0 }
445                        File.open("/dev/null") {|f|
446                                STDIN.reopen  f
447                                STDOUT.reopen f
448                                STDERR.reopen f
449                        }
450                        yield
451                end
452                exit! 0
453        end
454
455        daemonize(opts[:debug]) do
456                Net::IRC::Server.new(opts[:host], opts[:port], TwitterIrcGateway, opts).start
457        end
458end
459
Note: See TracBrowser for help on using the browser.