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

Revision 13136, 14.2 kB (checked in by cho45, 5 years ago)

Fixed lingr gateway to avoid invalid ticket error etc.

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