root/lang/ruby/net-irc/trunk/examples/lig.rb @ 6152

Revision 6152, 12.4 kB (checked in by cho45, 5 years ago)

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

  • WHOIS のときの情報をマシに (server info として client_type, source を表示)
  • そもそも RPL_WHOISSERVER のメッセージが間違っていたのを修正

312 RPL_WHOISSERVER

"<nick> <server> :<server info>"

  • Property svn:executable set to *
Line 
1#!/usr/bin/env ruby
2=begin
3
4# lig.rb
5
6Lingr IRC Gateway - IRC Gateway to Lingr ( http://www.lingr.com/ )
7
8## Launch
9
10        $ ruby lig.rb # daemonized
11
12If you want to help:
13
14        $ ruby lig.rb --help
15        Usage: examples/lig.rb [opts]
16       
17       
18        Options:
19            -p, --port [PORT=16669]          port number to listen
20            -h, --host [HOST=localhost]      host name or IP address to listen
21            -l, --log LOG                    log file
22            -a, --api_key API_KEY            Your api key on Lingr
23                --debug                      Enable debug mode
24
25## Configuration
26
27Configuration example for Tiarra ( http://coderepos.org/share/wiki/Tiarra ).
28
29        lingr {
30                host: localhost
31                port: 16669
32                name: username@example.com (Email on Lingr)
33                password: password on Lingr
34                in-encoding: utf8
35                out-encoding: utf8
36        }
37
38Set your email as IRC 'real name' field, and password as server password.
39This does not allow anonymous connection to Lingr.
40You must create a account on Lingr and get API key (ask it first time).
41
42## Client
43
44This gateway sends multibyte nicknames at Lingr rooms as-is.
45So you should use a client which treats it correctly.
46
47Recommended:
48
49 * LimeChat for OSX ( http://limechat.sourceforge.net/ )
50 * irssi ( http://irssi.org/ )
51 * (gateway) Tiarra ( http://coderepos.org/share/wiki/Tiarra )
52
53## Nickname/Mask
54
55nick -> nickname in a room.
56o_id -> occupant_id (unique id in a room)
57u_id -> user_id (unique user id in Lingr)
58
59 * Anonymous User: <nick>|<o_id>!anon@lingr.com
60 * Logged-in User: <nick>|<o_id>!<u_id>@lingr.com
61 * Your:           <nick>|<u_id>!<u_id>@lingr.com
62
63So you can see some nicknames in same user, but it is needed for
64nickname management on client.
65
66(Lingr allows different nicknames between rooms in a same user, but IRC not)
67
68## Licence
69
70Ruby's by cho45
71
72=end
73
74$LOAD_PATH << File.dirname(__FILE__)
75$LOAD_PATH << "lib"
76$LOAD_PATH << "../lib"
77
78require "rubygems"
79require "lingr"
80require "net/irc"
81require "pit"
82
83
84class LingrIrcGateway < Net::IRC::Server::Session
85        def server_name
86                "lingrgw"
87        end
88
89        def server_version
90                "0.0.0"
91        end
92
93        def initialize(*args)
94                super
95                @channels = {}
96        end
97
98        def on_user(m)
99                super
100                @real, @copts = @real.split(/\s+/)
101                @copts ||= []
102
103                # Tiarra sends prev nick when reconnects.
104                @nick.sub!(/\|.+$/, "")
105
106                log "Hello #{@nick}, this is Lingr IRC Gateway."
107                log "Client Option: #{@copts.join(", ")}"
108                @log.info "Client Option: #{@copts.join(", ")}"
109                @log.info "Client initialization is completed."
110
111                @lingr = Lingr::Client.new(@opts.api_key)
112                @lingr.create_session('human')
113                @lingr.login(@real, @pass)
114                @user_info = @lingr.get_user_info
115
116                prefix = make_ids(@user_info)
117                @user_info["prefix"] = prefix
118                post @prefix, NICK, prefix.nick
119        end
120
121        def on_privmsg(m)
122                target, message = *m.params
123                @lingr.say(@channels[target.downcase][:ticket], message)
124        rescue Lingr::Client::APIError => e
125                log "Error: #{e.code}: #{e.message}"
126                log "Coundn't say to #{channel}."
127        end
128
129        def on_whois(m)
130                nick = m.params[0]
131                chan = nil
132                info = nil
133
134                @channels.each do |k, v|
135                        if v[:users].key?(nick)
136                                chan = k
137                                info = v[:users][nick]
138                                break
139                        end
140                end
141
142                if chan
143                        prefix      = info["prefix"]
144                        real_name   = info["description"].to_s
145                        server_info = "Lingr: type:#{info["client_type"]} source:#{info["source"]}"
146                        channels    = [info["client_type"] == "human" ? "@#{chan}" : chan]
147                        me          = make_ids(@user_info)
148
149                        post nil, RPL_WHOISUSER,     me.nick, prefix.nick, prefix.user, prefix.host, "*", real_name
150                        post nil, RPL_WHOISSERVER,   me.nick, prefix.nick, prefix.host, server_info
151                        # post nil, RPL_WHOISOPERATOR, me.nick, prefix.nick, "is an IRC operator"
152                        # post nil, RPL_WHOISIDLE,     me.nick, prefix.nick, idle, "seconds idle"
153                        post nil, RPL_WHOISCHANNELS, me.nick, prefix.nick, channels.join(" ")
154                        post nil, RPL_ENDOFWHOIS,    me.nick, prefix.nick, "End of WHOIS list"
155                else
156                        post nil, ERR_NOSUCHNICK, me.nick, nick, "No such nick/channel"
157                end
158        end
159
160        def on_who(m)
161                channel = m.params[0]
162                return unless channel
163
164                info = @channels[channel.downcase]
165                me   = make_ids(@user_info)
166                res  = @lingr.get_room_info(info[:chan_id], nil, info[:password])
167                res["occupants"].each do |o|
168                        next unless o["nickname"]
169                        u_id, o_id, prefix = *make_ids(o, true)
170                        op = (o["client_type"] == "human") ? "@" : ""
171                        post nil, RPL_WHOREPLY, me.nick, channel, o_id, "lingr.com", "lingr.com", prefix.nick, "H*#{op}", "0 #{o["description"].to_s.gsub(/\s+/, " ")}"
172                end
173                post nil, RPL_ENDOFWHO, me.nick, channel
174        rescue Lingr::Client::APIError => e
175                log "Maybe gateway don't know password for channel #{channel}. Please part and join."
176        end
177
178        def on_join(m)
179                channels = m.params[0].split(/\s*,\s*/)
180                password = m.params[1]
181                channels.each do |channel|
182                        next if @channels.key? channel.downcase
183                        begin
184                                @log.debug "Enter room -> #{channel}"
185                                res = @lingr.enter_room(channel.sub(/^#/, ""), @nick, password)
186                                res["password"] = password
187
188                                create_observer(channel, res)
189                        rescue Lingr::Client::APIError => e
190                                log "Error: #{e.code}: #{e.message}"
191                                log "Coundn't join to #{channel}."
192                        rescue Exception => e
193                                @log.error e.inspect
194                                e.backtrace.each do |l|
195                                        @log.error "\t#{l}"
196                                end
197                        end
198                end
199        end
200
201        def on_part(m)
202                channel = m.params[0]
203                info    = @channels[channel.downcase]
204                prefix  = make_ids(@user_info)
205
206                if info
207                        info[:observer].kill
208                        @lingr.exit_room(info[:ticket])
209                        @channels.delete(channel.downcase)
210
211                        post prefix, PART, channel, "Parted"
212                else
213                        post nil, ERR_NOSUCHCHANNEL, prefix.nick, channel, "No such channel"
214                end
215        end
216
217        private
218
219        def create_observer(channel, response)
220                Thread.start(channel, response) do |chan, res|
221                        myprefix = @user_info["prefix"]
222                        post server_name, TOPIC, chan, "#{res["room"]["url"]} #{res["room"]["description"]}"
223                        @channels[chan.downcase] = {
224                                :ticket   => res["ticket"],
225                                :counter  => res["counter"],
226                                :o_id     => res["occupant_id"],
227                                :chan_id  => res["room"]["id"],
228                                :password => res["password"],
229                                :users    => res["occupants"].reject {|i| i["nickname"].nil? }.inject({}) {|r,i|
230                                        i["prefix"] = make_ids(i)
231                                        r.update(i["prefix"].nick => i)
232                                },
233                                :hcounter => 0,
234                                :observer => Thread.current,
235                        }
236                        post myprefix, JOIN, channel
237                        post server_name, MODE, channel, "+o", myprefix.nick
238                        post nil, RPL_NAMREPLY,   myprefix.nick, "=", chan, @channels[chan.downcase][:users].map{|k,v|
239                                v["client_type"] == "human" ? "@#{k}" : k
240                        }.join(" ")
241                        post nil, RPL_ENDOFNAMES, myprefix.nick, chan, "End of NAMES list"
242
243                        first = true
244                        info = @channels[chan.downcase]
245                        while true
246                                begin
247                                        res = @lingr.observe_room info[:ticket], info[:counter]
248                                        @log.debug "observe_room<#{chan}> returned"
249
250                                        info[:counter] = res["counter"] if res["counter"]
251
252                                        (res["messages"] || []).each do |m|
253                                                next if m["id"].to_i <= info[:hcounter]
254
255                                                u_id, o_id, prefix = *make_ids(m, true)
256
257                                                case m["type"]
258                                                when "user"
259                                                        if first
260                                                                post prefix, NOTICE, chan, m["text"]
261                                                        else
262                                                                post prefix, PRIVMSG, chan, m["text"] unless info[:o_id] == o_id
263                                                        end
264                                                when "private"
265                                                        # TODO
266                                                        post prefix, PRIVMSG, chan, "\x01ACTION Sent private: #{m["text"]}\x01" unless info[:o_id] == o_id
267## process occupants list. should not use thease as nick management.
268#                                               when "system:enter"
269#                                                       unless prefix.nick == myprefix.nick
270#                                                               post prefix, JOIN, chan
271#                                                               if m["client_type"] == "human"
272#                                                                       post server_name, MODE, chan, "+o", prefix.nick
273#                                                               end
274#                                                               info[:users][prefix.nick] = m.merge("prefix" => prefix)
275#                                                       end
276#                                               when "system:leave"
277#                                                       unless prefix.nick == myprefix.nick
278#                                                               post prefix, PART, chan
279#                                                               info[:users].delete(prefix.nick)
280#                                                       end
281#                                               when "system:nickname_change"
282#                                                       m["nickname"] = m["new_nickname"]
283#                                                       newprefix = make_ids(m)
284#                                                       post prefix, NICK, newprefix.nick
285#                                                       info[:users].delete prefix.nick
286#                                                       info[:users][newprefix.nick] = m.merge("prefix" => newprefix)
287                                                when "system:broadcast"
288                                                        post "system.broadcast",  NOTICE, chan, m["text"]
289                                                end
290
291                                                info[:hcounter] = m["id"].to_i if m["id"]
292                                        end
293
294                                        if res["occupants"]
295                                                enter = [], leave = []
296                                                newusers = res["occupants"].reject {|i| i["nickname"].nil? }.inject({}) {|r,i|
297                                                        i["prefix"] = make_ids(i)
298                                                        r.update(i["prefix"].nick => i)
299                                                }
300
301
302                                                nickchange = newusers.inject({:new => [], :old => []}) {|r,(k,new)|
303                                                        old = info[:users].find {|l,old|
304                                                                # same occupant_id and different nickname
305                                                                # when nickname was changed and when un-authed user promoted to authed user.
306                                                                new["prefix"] != old["prefix"] && new["id"] == old["id"]
307                                                        }
308                                                        if old
309                                                                old = old[1]
310                                                                post old["prefix"], NICK, new["prefix"].nick
311                                                                r[:old] << old["prefix"].nick
312                                                                r[:new] << new["prefix"].nick
313                                                        end
314                                                        r
315                                                }
316
317                                                entered = newusers.keys - info[:users].keys - nickchange[:new]
318                                                leaved  = info[:users].keys - newusers.keys - entered - nickchange[:old]
319
320                                                leaved.each do |leave|
321                                                        leave = info[:users][leave]
322                                                        post leave["prefix"], PART, chan, ""
323                                                end
324
325                                                entered.each do |enter|
326                                                        enter  = newusers[enter]
327                                                        prefix = enter["prefix"]
328                                                        post prefix, JOIN, chan
329                                                        if enter["client_type"] == "human"
330                                                                post server_name, MODE, chan, "+o", prefix.nick
331                                                        end
332                                                end
333
334                                                info[:users] = newusers
335                                        end
336
337
338                                        first = false
339                                rescue Lingr::Client::APIError => e
340                                        case e.code
341                                        when 100
342                                                @log.fatal "BUG: API returns invalid HTTP method"
343                                                exit 1
344                                        when 102
345                                                @log.error "BUG: API returns invalid session. Prompt the client to reconnect."
346                                                finish
347                                        when 104
348                                                @log.fatal "BUG: API returns invalid response format. JSON is unsupported?"
349                                                exit 1
350                                        when 109
351                                                @log.error "BUG: API returns invalid ticket. Part this channel..."
352                                                on_part(Message.new("", PART, [chan, res["error"]["message"]]))
353                                        when 114
354                                                @log.fatal "BUG: API returns no counter parameter."
355                                                exit 1
356                                        when 120
357                                                @log.error "BUG: API returns invalid encoding. But continues."
358                                        when 122
359                                                @log.fatal "BUG: API returns repeated counter"
360                                                exit 1
361                                        else
362                                                # may be socket error?
363                                                @log.debug "observe failed : #{res.inspect}"
364                                                log "Error: #{e.code}: #{e.message}"
365                                        end
366                                rescue Exception => e
367                                        @log.error e.inspect
368                                        e.backtrace.each do |l|
369                                                @log.error "\t#{l}"
370                                        end
371                                end
372                                sleep 1
373                        end
374                end
375        end
376
377        def log(str)
378                str.gsub!(/\s/, " ")
379                begin
380                        myprefix = make_ids(@user_info)
381                        post nil, NOTICE, myprefix.nick, str
382                rescue
383                        post nil, NOTICE, @nick, str
384                end
385        end
386
387        def make_ids(o, ext=false)
388                u_id  = o["user_id"] || "anon"
389                o_id  = o["occupant_id"] || o["id"]
390                nick  = (o["default_nickname"] || o["nickname"]).gsub(/\s+/, "")
391                if o["user_id"] == @user_info["user_id"]
392                        nick << "|#{o["user_id"]}"
393                else
394                        nick << "|#{o["user_id"] ? o_id : "_"+o_id}"
395                end
396                pref = Prefix.new("#{nick}!#{u_id}@lingr.com")
397                ext ? [u_id, o_id, pref] : pref
398        end
399end
400
401
402if __FILE__ == $0
403        require "rubygems"
404        require "optparse"
405        require "pit"
406
407        opts = {
408                :port   => 16669,
409                :host   => "localhost",
410                :debug  => false,
411                :log    => nil,
412                :debug  => false,
413        }
414
415        OptionParser.new do |parser|
416                parser.instance_eval do
417                        self.banner  = <<-EOB.gsub(/^\t+/, "")
418                                Usage: #{$0} [opts]
419
420                        EOB
421
422                        separator ""
423
424                        separator "Options:"
425                        on("-p", "--port [PORT=#{opts[:port]}]", "port number to listen") do |port|
426                                opts[:port] = port
427                        end
428
429                        on("-h", "--host [HOST=#{opts[:host]}]", "host name or IP address to listen") do |host|
430                                opts[:host] = host
431                        end
432
433                        on("-l", "--log LOG", "log file") do |log|
434                                opts[:log] = log
435                        end
436
437                        on("-a", "--api_key API_KEY", "Your api key on Lingr") do |key|
438                                opts[:api_key] = key
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        opts[:api_key] = Pit.get("lig.rb", :require => {
472                "api_key" => "API key of Lingr"
473        })["api_key"] unless opts[:api_key]
474
475        daemonize(opts[:debug]) do
476                Net::IRC::Server.new(opts[:host], opts[:port], LingrIrcGateway, opts).start
477        end
478
479end
480
481
Note: See TracBrowser for help on using the browser.