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

Revision 5773, 10.1 kB (checked in by cho45, 6 years ago)

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

Change logger opts.
Add daemonize feature.

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