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

Revision 5534, 8.7 kB (checked in by cho45, 5 years ago)

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

Pass opts passed to Server also to Session.
Fix bug.

  • Property svn:executable set to *
Line 
1#!/usr/bin/env ruby
2=begin
3
4# tig.rb
5
6Ruby version of Twitter IRC Gateway
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
45require "rubygems"
46require "net/http"
47require "net/irc"
48require "uri"
49require "json"
50require "socket"
51require "time"
52require "logger"
53require "yaml"
54require "pathname"
55require "digest/md5"
56
57Net::HTTP.version_1_2
58
59class TwitterIrcGateway < Net::IRC::Server::Session
60        @@name     = "twittergw"
61        @@version  = "0.0.0"
62        @@channel  = "#twitter"
63        @@api_base = URI("http://twitter.com/")
64
65        class ApiFailed < StandardError; end
66
67        def initialize(*args)
68                super
69                @groups = {}
70                @channels = [] # join channels (groups)
71                @config = Pathname.new(ENV["HOME"]) + ".tig"
72                load_config
73        end
74
75        def on_user(m)
76                super
77                post @mask, JOIN, @@channel
78                @real, @opts = @real.split(/\s/)
79                @opts ||= []
80                @log.info "Client Options: #{@opts.inspect}"
81
82                @timeline = []
83                Thread.start do
84                        loop do
85                                begin
86                                        check_friends
87                                rescue ApiFailed => e
88                                        @log.error e.inspect
89                                rescue Exception => e
90                                        puts e
91                                        puts e.backtrace
92                                end
93                                sleep 10 * 60
94                        end
95                end
96                sleep 3
97                Thread.start do
98                        loop do
99                                begin
100                                        check_timeline
101                                        # check_direct_messages
102                                rescue ApiFailed => e
103                                        @log.error e.inspect
104                                rescue Exception => e
105                                        puts e
106                                        puts e.backtrace
107                                end
108                                sleep 90
109                        end
110                end
111        end
112
113        def on_privmsg(m)
114                retry_count = 3
115                ret = nil
116                target, message = *m.params
117                begin
118                        if target =~ /^#/
119                                ret = api("statuses/update.json", {"status" => message})
120                        else
121                                # direct message
122                                ret = api("direct_messages/new.json", {"user" => target, "text" => message})
123                        end
124                        raise ApiFailed, "api failed" unless ret
125                        log "Status Updated"
126                rescue => e
127                        @log.error [retry_count, e.inspect].inspect
128                        if retry_count > 0
129                                retry_count -= 1
130                                @log.debug "Retry to setting status..."
131                                retry
132                        else
133                                log "Some Error Happened on Sending #{message}. #{e}"
134                        end
135                end
136        end
137
138        def on_whois(m)
139                nick = m.params[0]
140                f = (@friends || []).find {|i| i["screen_name"] == nick }
141                if f
142                        post nil, RPL_WHOISUSER,   nick, nick, nick, @@api_base.host, "*", NKF.nkf("-j", "#{f["name"]} / #{f["description"]}")
143                        post nil, RPL_WHOISSERVER, nick, @@api_base.host, @@api_base.to_s
144                        post nil, RPL_WHOISIDLE,   nick, "0", "seconds idle"
145                        post nil, RPL_ENDOFWHOIS,  nick, "End of WHOIS list"
146                else
147                        post nil, ERR_NOSUCHNICK, nick, "No such nick/channel"
148                end
149        end
150
151        def on_who(m)
152                channel = m.params[0]
153                case
154                when channel == @@channel
155                        #     "<channel> <user> <host> <server> <nick>
156                        #         ( "H" / "G" > ["*"] [ ( "@" / "+" ) ]
157                        #             :<hopcount> <real name>"
158                        @friends.each do |f|
159                                user = nick = f["screen_name"]
160                                host = serv = @@api_base.host
161                                real = f["name"]
162                                post nil, RPL_WHOREPLY, channel, user, host, serv, nick, "H", "0 #{real}"
163                        end
164                        post nil, RPL_ENDOFWHO, channel
165                when @groups.key?(channel)
166                        @groups[channel].each do |name|
167                                f = @friends.find {|i| i["screen_name"] == name }
168                                user = nick = f["screen_name"]
169                                host = serv = @@api_base.host
170                                real = f["name"]
171                                post nil, RPL_WHOREPLY, channel, user, host, serv, nick, "H", "0 #{real}"
172                        end
173                        post nil, RPL_ENDOFWHO, channel
174                else
175                        post nil, ERR_NOSUCHNICK, nick, "No such nick/channel"
176                end
177        end
178
179        def on_join(m)
180                channels = m.params[0].split(/\s*,\s*/)
181                channels.each do |channel|
182                        next if channel == @@channel
183
184                        @channels << channel
185                        @channels.uniq!
186                        post "#{@nick}!#{@nick}@#{@@api_base.host}", JOIN, channel
187                        save_config
188                end
189        end
190
191        def on_part(m)
192                channel = m.params[0]
193                return if channel == @@channel
194
195                @channels.delete(channel)
196                post @nick, PART, channel, "Ignore group #{channel}, but setting is alive yet."
197        end
198
199        def on_invite(m)
200                nick, channel = *m.params
201                return if channel == @@channel
202
203                if (@friends || []).find {|i| i["screen_name"] == nick }
204                        ((@groups[channel] ||= []) << nick).uniq!
205                        post "#{nick}!#{nick}@#{@@api_base.host}", JOIN, channel
206                        save_config
207                else
208                        post ERR_NOSUCHNICK, nil, nick, "No such nick/channel"
209                end
210        end
211
212        def on_kick(m)
213                channel, nick, mes = *m.params
214                return if channel == @@channel
215
216                if (@friends || []).find {|i| i["screen_name"] == nick }
217                        (@groups[channel] ||= []).delete(nick)
218                        post nick, PART, channel
219                        save_config
220                else
221                        post ERR_NOSUCHNICK, nil, nick, "No such nick/channel"
222                end
223        end
224
225        private
226        def check_timeline
227                first = true unless @prev_time
228                @prev_time = Time.at(0) if first
229                api("statuses/friends_timeline.json", {"since" => [@prev_time.httpdate] }).reverse_each do |s|
230                        nick = s["user"]["screen_name"]
231                        mesg = s["text"]
232                        time = Time.parse(s["created_at"]) rescue Time.now
233                        m = { "&quot;" => "\"", "&lt;"=> "<", "&gt;"=> ">", "&amp;"=> "&", "\n" => " "}
234                        mesg.gsub!(/(#{m.keys.join("|")})/) { m[$1] }
235
236                        digest = Digest::MD5.hexdigest("#{nick}::#{mesg}")
237                        unless @timeline.include?(digest)
238                                @timeline << digest
239                                @log.debug [nick, mesg, time].inspect
240                                if nick == @nick # 自分のときは topic に
241                                        post nick, TOPIC, @@channel, mesg
242                                else
243                                        message(nick, @@channel, mesg)
244                                end
245                                @groups.each do |channel,members|
246                                        if members.include?(nick)
247                                                message(nick, channel, mesg)
248                                        end
249                                end
250                        end
251                end
252                @timeline  = @timeline.last(100)
253                @prev_time = Time.now
254        end
255
256        def check_direct_messages
257                first = true unless @prev_time_d
258                @prev_time_d = Time.now if first
259                api("direct_messages.json", {"since" => [@prev_time_d.httpdate] }).reverse_each do |s|
260                        nick = s["sender_screen_name"]
261                        mesg = s["text"]
262                        time = Time.parse(s["created_at"])
263                        @log.debug [nick, mesg, time].inspect
264                        message(nick, @nick, mesg)
265                end
266                @prev_time_d = Time.now
267        end
268
269        def check_friends
270                first = true unless @friends
271                @friends ||= []
272                friends = api("statuses/friends.json")
273                if first && !@opts.include?("athack")
274                        @friends = friends
275                        post nil, RPL_NAMREPLY,   @@name, @nick, "=", @@channel, @friends.map{|i| i["screen_name"] }.join(" ")
276                        post nil, RPL_ENDOFNAMES, @@name, @nick, @@channel, "End of NAMES list"
277                else
278                        prv_friends = @friends.map {|i| i["screen_name"] }
279                        now_friends = friends.map {|i| i["screen_name"] }
280                        (now_friends - prv_friends).each do |join|
281                                join = "@#{join}" if @opts.include?("athack")
282                                post "#{join}!#{join}@#{@@api_base.host}", JOIN, @@channel
283                        end
284                        (prv_friends - now_friends).each do |part|
285                                part = "@#{part}" if @opts.include?("athack")
286                                post "#{part}!#{part}@#{@@api_base.host}", PART, @@channel, ""
287                        end
288                        @friends = friends
289                end
290        end
291
292        def save_config
293                config = {
294                        :channels => @channels,
295                        :groups => @groups,
296                }
297                @config.open("w") do |f|
298                        YAML.dump(config, f)
299                end
300        end
301
302        def load_config
303                @config.open do |f|
304                        config = YAML.load(f)
305                        @channels = config[:channels]
306                        @groups   = config[:groups]
307                end
308        rescue Errno::ENOENT
309        end
310
311        def api(path, q={})
312                ret = {}
313                q["source"] = "tigrb"
314                q = q.inject([]) {|r,(k,v)| v.inject(r) {|r,i| r << "#{k}=#{URI.escape(i, /./)}" } }.join("&")
315                uri = @@api_base + "/#{path}?#{q}"
316                @log.debug uri.inspect
317                Net::HTTP.start(uri.host, uri.port) do |http|
318                        header = {
319                                'Authorization' => "Basic " + ["#{@real}:#{@pass}"].pack("m"),
320                        }
321                        case path
322                        when "statuses/update.json", "direct_messages/new.json"
323                                ret = http.post(uri.request_uri, q, header)
324                        else
325                                ret = http.get(uri.request_uri, header)
326                        end
327                end
328                @log.debug ret.inspect
329                case ret.code
330                when "200"
331                        JSON.parse(ret.body)
332                when "304"
333                        []
334                else
335                        raise ApiFailed, "Server Returned #{ret.code}"
336                end
337        rescue Errno::ETIMEDOUT, JSON::ParserError, IOError, Timeout::Error, Errno::ECONNRESET => e
338                raise ApiFailed, e.inspect
339        end
340
341        def message(sender, target, str)
342#                       str.gsub!(/&#(x)?([0-9a-f]+);/i) do |m|
343#                               [$1 ? $2.hex : $2.to_i].pack("U")
344#                       end
345                str = untinyurl(str)
346                sender =  "#{sender}!#{sender}@#{@@api_base.host}"
347                post sender, PRIVMSG, target, str
348        end
349
350        def log(str)
351                str.gsub!(/\n/, " ")
352                post @@name, NOTICE, @@channel, str
353        end
354
355        def untinyurl(text)
356                text.gsub(%r|http://tinyurl.com/[0-9a-z=]+|i) {|m|
357                        uri = URI(m)
358                        Net::HTTP.start(uri.host, uri.port) {|http|
359                                http.head(uri.request_uri)["Location"]
360                        }
361                }
362        end
363end
364
365if __FILE__ == $0
366        Net::IRC::Server.new("localhost", 16668, TwitterIrcGateway).start
367end
368
369
370
Note: See TracBrowser for help on using the browser.