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

Revision 6138, 10.9 kB (checked in by cho45, 5 years ago)

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

Update doc

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