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

Revision 5985, 10.7 kB (checked in by cho45, 7 years ago)

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

More sweet error messages.

  • 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 @prefix, JOIN, main_channel
96                post server_name, MODE, main_channel, "+o", @prefix.nick
97
98                @real, @opts = @real.split(/\s+/)
99                @opts ||= []
100                @log.info "Client Options: #{@opts.inspect}"
101
102                @timeline = []
103                Thread.start do
104                        loop do
105                                begin
106                                        check_friends
107                                rescue ApiFailed => e
108                                        @log.error e.inspect
109                                rescue Exception => e
110                                        @log.error e.inspect
111                                        e.backtrace.each do |l|
112                                                @log.error "\t#{l}"
113                                        end
114                                end
115                                sleep 10 * 60
116                        end
117                end
118                sleep 3
119                Thread.start do
120                        loop do
121                                begin
122                                        check_timeline
123                                        # check_direct_messages
124                                rescue ApiFailed => e
125                                        @log.error e.inspect
126                                rescue Exception => e
127                                        @log.error e.inspect
128                                        e.backtrace.each do |l|
129                                                @log.error "\t#{l}"
130                                        end
131                                end
132                                sleep 90
133                        end
134                end
135        end
136
137        def on_privmsg(m)
138                retry_count = 3
139                ret = nil
140                target, message = *m.params
141                begin
142                        if target =~ /^#/
143                                ret = api("statuses/update", {"status" => message})
144                        else
145                                # direct message
146                                ret = api("direct_messages/new", {"user" => target, "text" => message})
147                        end
148                        raise ApiFailed, "api failed" unless ret
149                        log "Status Updated"
150                rescue => e
151                        @log.error [retry_count, e.inspect].inspect
152                        if retry_count > 0
153                                retry_count -= 1
154                                @log.debug "Retry to setting status..."
155                                retry
156                        else
157                                log "Some Error Happened on Sending #{message}. #{e}"
158                        end
159                end
160        end
161
162        def on_whois(m)
163                nick = m.params[0]
164                f = (@friends || []).find {|i| i["screen_name"] == nick }
165                if f
166                        post nil, RPL_WHOISUSER,   nick, nick, nick, api_base.host, "*", NKF.nkf("-j", "#{f["name"]} / #{f["description"]}")
167                        post nil, RPL_WHOISSERVER, nick, api_base.host, api_base.to_s
168                        post nil, RPL_WHOISIDLE,   nick, "0", "seconds idle"
169                        post nil, RPL_ENDOFWHOIS,  nick, "End of WHOIS list"
170                else
171                        post nil, ERR_NOSUCHNICK, nick, "No such nick/channel"
172                end
173        end
174
175        def on_who(m)
176                channel = m.params[0]
177                case
178                when channel == main_channel
179                        #     "<channel> <user> <host> <server> <nick>
180                        #         ( "H" / "G" > ["*"] [ ( "@" / "+" ) ]
181                        #             :<hopcount> <real name>"
182                        @friends.each do |f|
183                                user = nick = f["screen_name"]
184                                host = serv = api_base.host
185                                real = f["name"]
186                                post nil, RPL_WHOREPLY, channel, user, host, serv, nick, "H", "0 #{real}"
187                        end
188                        post nil, RPL_ENDOFWHO, channel
189                when @groups.key?(channel)
190                        @groups[channel].each do |name|
191                                f = @friends.find {|i| i["screen_name"] == name }
192                                user = nick = f["screen_name"]
193                                host = serv = api_base.host
194                                real = f["name"]
195                                post nil, RPL_WHOREPLY, channel, user, host, serv, nick, "H", "0 #{real}"
196                        end
197                        post nil, RPL_ENDOFWHO, channel
198                else
199                        post nil, ERR_NOSUCHNICK, nick, "No such nick/channel"
200                end
201        end
202
203        def on_join(m)
204                channels = m.params[0].split(/\s*,\s*/)
205                channels.each do |channel|
206                        next if channel == main_channel
207
208                        @channels << channel
209                        @channels.uniq!
210                        post "#{@nick}!#{@nick}@#{api_base.host}", JOIN, channel
211                        post server_name, MODE, channel, "+o", @nick
212                        save_config
213                end
214        end
215
216        def on_part(m)
217                channel = m.params[0]
218                return if channel == main_channel
219
220                @channels.delete(channel)
221                post @nick, PART, channel, "Ignore group #{channel}, but setting is alive yet."
222        end
223
224        def on_invite(m)
225                nick, channel = *m.params
226                return if channel == main_channel
227
228                if (@friends || []).find {|i| i["screen_name"] == nick }
229                        ((@groups[channel] ||= []) << nick).uniq!
230                        post "#{nick}!#{nick}@#{api_base.host}", JOIN, channel
231                        post server_name, MODE, channel, "+o", nick
232                        save_config
233                else
234                        post ERR_NOSUCHNICK, nil, nick, "No such nick/channel"
235                end
236        end
237
238        def on_kick(m)
239                channel, nick, mes = *m.params
240                return if channel == main_channel
241
242                if (@friends || []).find {|i| i["screen_name"] == nick }
243                        (@groups[channel] ||= []).delete(nick)
244                        post nick, PART, channel
245                        save_config
246                else
247                        post ERR_NOSUCHNICK, nil, nick, "No such nick/channel"
248                end
249        end
250
251        private
252        def check_timeline
253                first = true unless @prev_time
254                @prev_time = Time.at(0) if first
255                api("statuses/friends_timeline", {"since" => [@prev_time.httpdate]}).reverse_each do |s|
256                        nick = s["user"]["screen_name"]
257                        mesg = s["text"]
258                        # time = Time.parse(s["created_at"]) rescue Time.now
259                        m = { "&quot;" => "\"", "&lt;"=> "<", "&gt;"=> ">", "&amp;"=> "&", "\n" => " "}
260                        mesg.gsub!(/(#{m.keys.join("|")})/) { m[$1] }
261
262                        digest = Digest::MD5.hexdigest("#{nick}::#{mesg}")
263                        unless @timeline.include?(digest)
264                                @timeline << digest
265                                @log.debug [nick, mesg]
266                                if nick == @nick # 自分のときは topic に
267                                        post nick, TOPIC, main_channel, mesg
268                                else
269                                        message(nick, main_channel, mesg)
270                                end
271                                @groups.each do |channel,members|
272                                        if members.include?(nick)
273                                                message(nick, channel, mesg)
274                                        end
275                                end
276                        end
277                end
278                @log.debug "@timeline.size = #{@timeline.size}"
279                @timeline  = @timeline.last(100)
280                @prev_time = Time.now
281        end
282
283        def check_direct_messages
284                first = true unless @prev_time_d
285                @prev_time_d = Time.now if first
286                api("direct_messages", {"since" => [@prev_time_d.httpdate] }).reverse_each do |s|
287                        nick = s["sender_screen_name"]
288                        mesg = s["text"]
289                        time = Time.parse(s["created_at"])
290                        @log.debug [nick, mesg, time].inspect
291                        message(nick, @nick, mesg)
292                end
293                @prev_time_d = Time.now
294        end
295
296        def check_friends
297                first = true unless @friends
298                @friends ||= []
299                friends = api("statuses/friends")
300                if first && !@opts.include?("athack")
301                        @friends = friends
302                        post nil, RPL_NAMREPLY,   @nick, "=", main_channel, @friends.map{|i| "@#{i["screen_name"]}" }.join(" ")
303                        post nil, RPL_ENDOFNAMES, @nick, main_channel, "End of NAMES list"
304                else
305                        prv_friends = @friends.map {|i| i["screen_name"] }
306                        now_friends = friends.map {|i| i["screen_name"] }
307                        (now_friends - prv_friends).each do |join|
308                                join = "@#{join}" if @opts.include?("athack")
309                                post "#{join}!#{join}@#{api_base.host}", JOIN, main_channel
310                        end
311                        (prv_friends - now_friends).each do |part|
312                                part = "@#{part}" if @opts.include?("athack")
313                                post "#{part}!#{part}@#{api_base.host}", PART, main_channel, ""
314                        end
315                        @friends = friends
316                end
317        end
318
319        def save_config
320                config = {
321                        :channels => @channels,
322                        :groups => @groups,
323                }
324                @config.open("w") do |f|
325                        YAML.dump(config, f)
326                end
327        end
328
329        def load_config
330                @config.open do |f|
331                        config = YAML.load(f)
332                        @channels = config[:channels]
333                        @groups   = config[:groups]
334                end
335        rescue Errno::ENOENT
336        end
337
338        def api(path, q={})
339                ret = {}
340                path = path.sub(%r{^/*}, '/') << '.json'
341                q["source"] = api_source
342                q = q.inject([]) {|r,(k,v)| v.inject(r) {|r,i| r << "#{k}=#{URI.escape(i, /[^-.!~*'()\w]/n)}" } }.join("&")
343                uri = api_base.dup
344                uri.path  = path
345                uri.query = q
346                @log.debug uri.inspect
347                Net::HTTP.start(uri.host, uri.port) do |http|
348                        header = {
349                                'Authorization' => "Basic " + ["#{@real}:#{@pass}"].pack("m"),
350                                'User-Agent'    => @user_agent,
351                        }
352                        case path
353                        when "/statuses/update.json", "/direct_messages/new.json"
354                                ret = http.post(uri.request_uri, q, header)
355                        else
356                                ret = http.get(uri.request_uri, header)
357                        end
358                end
359                @log.debug ret.inspect
360                case ret.code
361                when "200"
362                        JSON.parse(ret.body.gsub(/'(y(?:es)?|n(?:o)?|true|false|null)'/, '"\1"'))
363                when "304"
364                        []
365                else
366                        raise ApiFailed, "Server Returned #{ret.code}"
367                end
368        rescue Errno::ETIMEDOUT, JSON::ParserError, IOError, Timeout::Error, Errno::ECONNRESET => e
369                raise ApiFailed, e.inspect
370        end
371
372        def message(sender, target, str)
373#                       str.gsub!(/&#(x)?([0-9a-f]+);/i) do |m|
374#                               [$1 ? $2.hex : $2.to_i].pack("U")
375#                       end
376                str = untinyurl(str)
377                sender =  "#{sender}!#{sender}@#{api_base.host}"
378                post sender, PRIVMSG, target, str
379        end
380
381        def log(str)
382                str.gsub!(/\n/, " ")
383                post server_name, NOTICE, main_channel, str
384        end
385
386        def untinyurl(text)
387                text.gsub(%r|http://(preview\.)?tinyurl\.com/[0-9a-z=]+|i) {|m|
388                        uri = URI(m)
389                        uri.host = uri.host.sub($1, '') if $1
390                        Net::HTTP.start(uri.host, uri.port) {|http|
391                                http.open_timeout = 3
392                                begin
393                                        http.head(uri.request_uri, { 'User-Agent' => @user_agent })["Location"]
394                                rescue Timeout::Error
395                                        m
396                                end
397                        }
398                }
399        end
400end
401
402if __FILE__ == $0
403        require "optparse"
404
405        opts = {
406                :port  => 16668,
407                :host  => "localhost",
408                :debug => false,
409                :log   => nil,
410        }
411
412        OptionParser.new do |parser|
413                parser.instance_eval do
414                        self.banner  = <<-EOB.gsub(/^\t+/, "")
415                                Usage: #{$0} [opts]
416
417                        EOB
418
419                        separator ""
420
421                        separator "Options:"
422                        on("-p", "--port [PORT=#{opts[:port]}]", "port number to listen") do |port|
423                                opts[:port] = port
424                        end
425
426                        on("-h", "--host [HOST=#{opts[:host]}]", "host name or IP address to listen") do |host|
427                                opts[:host] = host
428                        end
429
430                        on("-l", "--log LOG", "log file") do |log|
431                                opts[:log] = log
432                        end
433
434                        on("--debug", "Enable debug mode") do |debug|
435                                opts[:log]   = $stdout
436                                opts[:debug] = true
437                        end
438
439                        parse!(ARGV)
440                end
441        end
442
443        opts[:logger] = Logger.new(opts[:log], "daily")
444        opts[:logger].level = opts[:debug] ? Logger::DEBUG : Logger::INFO
445
446        def daemonize(debug=false)
447                return yield if $DEBUG || debug
448                Process.fork do
449                        Process.setsid
450                        Dir.chdir "/"
451                        trap("SIGINT")  { exit! 0 }
452                        trap("SIGTERM") { exit! 0 }
453                        trap("SIGHUP")  { exit! 0 }
454                        File.open("/dev/null") {|f|
455                                STDIN.reopen  f
456                                STDOUT.reopen f
457                                STDERR.reopen f
458                        }
459                        yield
460                end
461                exit! 0
462        end
463
464        daemonize(opts[:debug]) do
465                Net::IRC::Server.new(opts[:host], opts[:port], TwitterIrcGateway, opts).start
466        end
467end
468
Note: See TracBrowser for help on using the browser.