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

Revision 5937, 10.6 kB (checked in by cho45, 7 years ago)

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

tig.rb も chanop を付与するように (普通は独りになったりしないはずだけれど)

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