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

Revision 9422, 13.3 kB (checked in by hirose31, 5 years ago)

added --foreground option and catch SIGTERM,INT,HUP.

  • 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## 備考
73
74このクライアントで 1000speakers への応募はできません。lingr.com から行ってください。
75
76=end
77
78$LOAD_PATH << File.dirname(__FILE__)
79$LOAD_PATH << "lib"
80$LOAD_PATH << "../lib"
81
82require "rubygems"
83require "lingr"
84require "net/irc"
85require "pit"
86
87
88class LingrIrcGateway < Net::IRC::Server::Session
89        def server_name
90                "lingrgw"
91        end
92
93        def server_version
94                "0.0.0"
95        end
96
97        def initialize(*args)
98                super
99                @channels = {}
100        end
101
102        def on_user(m)
103                super
104                @real, *@copts = @real.split(/\s+/)
105                @copts ||= []
106
107                # Tiarra sends prev nick when reconnects.
108                @nick.sub!(/\|.+$/, "")
109
110                log "Hello #{@nick}, this is Lingr IRC Gateway."
111                log "Client Option: #{@copts.join(", ")}"
112                @log.info "Client Option: #{@copts.join(", ")}"
113                @log.info "Client initialization is completed."
114
115                @lingr = Lingr::Client.new(@opts.api_key)
116                @lingr.create_session('human')
117                @lingr.login(@real, @pass)
118                @user_info = @lingr.get_user_info
119
120                prefix = make_ids(@user_info)
121                @user_info["prefix"] = prefix
122                post @prefix, NICK, prefix.nick
123        rescue Lingr::Client::APIError => e
124                case e.code
125                when 105
126                        post nil, ERR_PASSWDMISMATCH, @nick, "Password incorrect"
127                else
128                        log "Error: #{e.code}: #{e.message}"
129                end
130                finish
131        end
132
133        def on_privmsg(m)
134                target, message = *m.params
135                if @channels.key?(target.downcase)
136                        @lingr.say(@channels[target.downcase][:ticket], message)
137                else
138                        post nil, ERR_NOSUCHNICK, @user_info["prefix"].nick, target, "No such nick/channel"
139                end
140        rescue Lingr::Client::APIError => e
141                log "Error: #{e.code}: #{e.message}"
142                log "Coundn't say to #{channel}."
143                on_join(Message.new(nil, "JOIN", target)) if e.code == 102 # invalid session
144        end
145
146        def on_notice(m)
147                on_privmsg(m)
148        end
149
150        def on_whois(m)
151                nick = m.params[0]
152                chan = nil
153                info = nil
154
155                @channels.each do |k, v|
156                        if v[:users].key?(nick)
157                                chan = k
158                                info = v[:users][nick]
159                                break
160                        end
161                end
162
163                if chan
164                        prefix      = info["prefix"]
165                        real_name   = info["description"].to_s
166                        server_info = "Lingr: type:#{info["client_type"]} source:#{info["source"]}"
167                        channels    = [info["client_type"] == "human" ? "@#{chan}" : chan]
168                        me          = @user_info["prefix"]
169
170                        post nil, RPL_WHOISUSER,     me.nick, prefix.nick, prefix.user, prefix.host, "*", real_name
171                        post nil, RPL_WHOISSERVER,   me.nick, prefix.nick, prefix.host, server_info
172                        # post nil, RPL_WHOISOPERATOR, me.nick, prefix.nick, "is an IRC operator"
173                        # post nil, RPL_WHOISIDLE,     me.nick, prefix.nick, idle, "seconds idle"
174                        post nil, RPL_WHOISCHANNELS, me.nick, prefix.nick, channels.join(" ")
175                        post nil, RPL_ENDOFWHOIS,    me.nick, prefix.nick, "End of WHOIS list"
176                else
177                        post nil, ERR_NOSUCHNICK, me.nick, nick, "No such nick/channel"
178                end
179        rescue Exception => e
180                @log.error e.inspect
181                e.backtrace.each do |l|
182                        @log.error "\t#{l}"
183                end
184        end
185
186        def on_who(m)
187                channel = m.params[0]
188                return unless channel
189
190                info = @channels[channel.downcase]
191                me   = @user_info["prefix"]
192                res  = @lingr.get_room_info(info[:chan_id], nil, info[:password])
193                res["occupants"].each do |o|
194                        next unless o["nickname"]
195                        u_id, o_id, prefix = *make_ids(o, true)
196                        op = (o["client_type"] == "human") ? "@" : ""
197                        post nil, RPL_WHOREPLY, me.nick, channel, o_id, "lingr.com", "lingr.com", prefix.nick, "H*#{op}", "0 #{o["description"].to_s.gsub(/\s+/, " ")}"
198                end
199                post nil, RPL_ENDOFWHO, me.nick, channel
200        rescue Lingr::Client::APIError => e
201                log "Maybe gateway don't know password for channel #{channel}. Please part and join."
202        end
203
204        def on_join(m)
205                channels = m.params[0].split(/\s*,\s*/)
206                password = m.params[1]
207                channels.each do |channel|
208                        next if @channels.key? channel.downcase
209                        begin
210                                @log.debug "Enter room -> #{channel}"
211                                res = @lingr.enter_room(channel.sub(/^#/, ""), @nick, password)
212                                res["password"] = password
213
214                                create_observer(channel, res)
215                        rescue Lingr::Client::APIError => e
216                                log "Error: #{e.code}: #{e.message}"
217                                log "Coundn't join to #{channel}."
218                                if e.code == 102
219                                        log "Invalid session... prompt the client to reconnect"
220                                        finish
221                                end
222                        rescue Exception => e
223                                @log.error e.inspect
224                                e.backtrace.each do |l|
225                                        @log.error "\t#{l}"
226                                end
227                        end
228                end
229        end
230
231        def on_part(m)
232                channel = m.params[0]
233                info    = @channels[channel.downcase]
234                prefix  = @user_info["prefix"]
235
236                if info
237                        info[:observer].kill
238                        @lingr.exit_room(info[:ticket])
239                        @channels.delete(channel.downcase)
240
241                        post prefix, PART, channel, "Parted"
242                else
243                        post nil, ERR_NOSUCHCHANNEL, prefix.nick, channel, "No such channel"
244                end
245        end
246
247        def on_disconnected
248                @channels.each do |k, info|
249                        info[:observer].kill
250                end
251                begin
252                        @lingr.destroy_session
253                rescue
254                end
255        end
256
257        private
258
259        def create_observer(channel, response)
260                Thread.start(channel, response) do |chan, res|
261                        myprefix = @user_info["prefix"]
262                        post server_name, TOPIC, chan, "#{res["room"]["url"]} #{res["room"]["description"]}"
263                        @channels[chan.downcase] = {
264                                :ticket   => res["ticket"],
265                                :counter  => res["room"]["counter"],
266                                :o_id     => res["occupant_id"],
267                                :chan_id  => res["room"]["id"],
268                                :password => res["password"],
269                                :users    => res["occupants"].reject {|i| i["nickname"].nil? }.inject({}) {|r,i|
270                                        i["prefix"] = make_ids(i)
271                                        r.update(i["prefix"].nick => i)
272                                },
273                                :hcounter => 0,
274                                :observer => Thread.current,
275                        }
276                        post myprefix, JOIN, channel
277                        post server_name, MODE, channel, "+o", myprefix.nick
278                        post nil, RPL_NAMREPLY,   myprefix.nick, "=", chan, @channels[chan.downcase][:users].map{|k,v|
279                                v["client_type"] == "human" ? "@#{k}" : k
280                        }.join(" ")
281                        post nil, RPL_ENDOFNAMES, myprefix.nick, chan, "End of NAMES list"
282
283                        info = @channels[chan.downcase]
284                        while true
285                                begin
286                                        @log.debug "observe_room<#{info[:counter]}><#{chan}> start <- #{myprefix}"
287                                        res = @lingr.observe_room info[:ticket], info[:counter]
288
289                                        info[:counter] = res["counter"] if res["counter"]
290
291                                        (res["messages"] || []).each do |m|
292                                                next if m["id"].to_i <= info[:hcounter]
293
294                                                u_id, o_id, prefix = *make_ids(m, true)
295
296                                                case m["type"]
297                                                when "user"
298                                                        # Don't send my messages.
299                                                        unless info[:o_id] == o_id
300                                                                post prefix, PRIVMSG, chan, m["text"]
301                                                        end
302                                                when "private"
303                                                        # TODO not sent from lingr?
304                                                        post prefix, PRIVMSG, chan, ctcp_encoding("ACTION Sent private: #{m["text"]}")
305
306                                                # system:{enter,leave,nickname_changed} should not be used for nick management.
307#                                               when "system:enter"
308#                                                       post prefix, PRIVMSG, chan, ctcp_encoding("ACTION #{m["text"]}")
309#                                               when "system:leave"
310#                                                       post prefix, PRIVMSG, chan, ctcp_encoding("ACTION #{m["text"]}")
311#                                               when "system:nickname_change"
312#                                                       post prefix, PRIVMSG, chan, ctcp_encoding("ACTION #{m["text"]}")
313                                                when "system:broadcast"
314                                                        post "system.broadcast",  NOTICE, chan, m["text"]
315                                                end
316
317                                                info[:hcounter] = m["id"].to_i if m["id"]
318                                        end
319
320                                        if res["occupants"]
321                                                enter = [], leave = []
322                                                newusers = res["occupants"].reject {|i| i["nickname"].nil? }.inject({}) {|r,i|
323                                                        i["prefix"] = make_ids(i)
324                                                        r.update(i["prefix"].nick => i)
325                                                }
326
327
328                                                nickchange = newusers.inject({:new => [], :old => []}) {|r,(k,new)|
329                                                        old = info[:users].find {|l,old|
330                                                                # same occupant_id and different nickname
331                                                                # when nickname was changed and when un-authed user promoted to authed user.
332                                                                new["prefix"] != old["prefix"] && new["id"] == old["id"]
333                                                        }
334                                                        if old
335                                                                old = old[1]
336                                                                post old["prefix"], NICK, new["prefix"].nick
337                                                                r[:old] << old["prefix"].nick
338                                                                r[:new] << new["prefix"].nick
339                                                        end
340                                                        r
341                                                }
342
343                                                entered = newusers.keys - info[:users].keys - nickchange[:new]
344                                                leaved  = info[:users].keys - newusers.keys - entered - nickchange[:old]
345
346                                                leaved.each do |leave|
347                                                        leave = info[:users][leave]
348                                                        post leave["prefix"], PART, chan, ""
349                                                end
350
351                                                entered.each do |enter|
352                                                        enter  = newusers[enter]
353                                                        prefix = enter["prefix"]
354                                                        post prefix, JOIN, chan
355                                                        if enter["client_type"] == "human"
356                                                                post server_name, MODE, chan, "+o", prefix.nick
357                                                        end
358                                                end
359
360                                                info[:users] = newusers
361                                        end
362
363
364                                rescue Lingr::Client::APIError => e
365                                        case e.code
366                                        when 100
367                                                @log.fatal "BUG: API returns invalid HTTP method"
368                                                exit 1
369                                        when 102
370                                                @log.error "BUG: API returns invalid session. Prompt the client to reconnect."
371                                                finish
372                                        when 104
373                                                @log.fatal "BUG: API returns invalid response format. JSON is unsupported?"
374                                                exit 1
375                                        when 109
376                                                @log.error "BUG: API returns invalid ticket. Part this channel..."
377                                                on_part(Message.new("", PART, [chan, res["error"]["message"]]))
378                                        when 114
379                                                @log.fatal "BUG: API returns no counter parameter."
380                                                exit 1
381                                        when 120
382                                                @log.error "Error: API returns invalid encoding. But continues."
383                                        when 122
384                                                @log.error "Error: API returns repeated counter. But continues."
385                                                info[:counter] += 10
386                                                log "Error: repeated counter. Some message may be ignored..."
387                                        else
388                                                # may be socket error?
389                                                @log.debug "observe failed : #{res.inspect}"
390                                                log "Error: #{e.code}: #{e.message}"
391                                        end
392                                rescue JSON::ParserError => e
393                                        @log.error e
394                                        info[:counter] += 10
395                                        log "Error: JSON::ParserError Some message may be ignored..."
396                                rescue Exception => e
397                                        @log.error e.inspect
398                                        e.backtrace.each do |l|
399                                                @log.error "\t#{l}"
400                                        end
401                                end
402                                sleep 1
403                        end
404                end
405        end
406
407        def log(str)
408                str.gsub!(/\s/, " ")
409                begin
410                        post nil, NOTICE, @user_info["prefix"].nick, str
411                rescue
412                        post nil, NOTICE, @nick, str
413                end
414        end
415
416        def make_ids(o, ext=false)
417                u_id  = o["user_id"] || "anon"
418                o_id  = o["occupant_id"] || o["id"]
419                nick  = (o["default_nickname"] || o["nickname"]).gsub(/\s+/, "")
420                if o["user_id"] == @user_info["user_id"]
421                        nick << "|#{o["user_id"]}"
422                else
423                        nick << "|#{o["user_id"] ? o_id : "_"+o_id}"
424                end
425                pref = Prefix.new("#{nick}!#{u_id}@lingr.com")
426                ext ? [u_id, o_id, pref] : pref
427        end
428end
429
430
431if __FILE__ == $0
432        require "rubygems"
433        require "optparse"
434        require "pit"
435
436        opts = {
437                :port  => 16669,
438                :host  => "localhost",
439                :log   => nil,
440                :debug => false,
441                :foreground => false,
442        }
443
444        OptionParser.new do |parser|
445                parser.instance_eval do
446                        self.banner  = <<-EOB.gsub(/^\t+/, "")
447                                Usage: #{$0} [opts]
448
449                        EOB
450
451                        separator ""
452
453                        separator "Options:"
454                        on("-p", "--port [PORT=#{opts[:port]}]", "port number to listen") do |port|
455                                opts[:port] = port
456                        end
457
458                        on("-h", "--host [HOST=#{opts[:host]}]", "host name or IP address to listen") do |host|
459                                opts[:host] = host
460                        end
461
462                        on("-l", "--log LOG", "log file") do |log|
463                                opts[:log] = log
464                        end
465
466                        on("-a", "--api_key API_KEY", "Your api key on Lingr") do |key|
467                                opts[:api_key] = key
468                        end
469
470                        on("--debug", "Enable debug mode") do |debug|
471                                opts[:log]   = $stdout
472                                opts[:debug] = true
473                        end
474
475                        on("-f", "--foreground", "run foreground") do |foreground|
476                                opts[:log]        = $stdout
477                                opts[:foreground] = true
478                        end
479
480                        parse!(ARGV)
481                end
482        end
483
484        opts[:logger] = Logger.new(opts[:log], "daily")
485        opts[:logger].level = opts[:debug] ? Logger::DEBUG : Logger::INFO
486
487        def daemonize(foreground=false)
488                trap("SIGINT")  { exit! 0 }
489                trap("SIGTERM") { exit! 0 }
490                trap("SIGHUP")  { exit! 0 }
491                return yield if $DEBUG || foreground
492                Process.fork do
493                        Process.setsid
494                        Dir.chdir "/"
495                        File.open("/dev/null") {|f|
496                                STDIN.reopen  f
497                                STDOUT.reopen f
498                                STDERR.reopen f
499                        }
500                        yield
501                end
502                exit! 0
503        end
504
505        opts[:api_key] = Pit.get("lig.rb", :require => {
506                "api_key" => "API key of Lingr"
507        })["api_key"] unless opts[:api_key]
508
509        daemonize(opts[:debug] || opts[:foreground]) do
510                Net::IRC::Server.new(opts[:host], opts[:port], LingrIrcGateway, opts).start
511        end
512
513end
514
515
Note: See TracBrowser for help on using the browser.