goshrine_bot 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/History.txt +0 -0
- data/Manifest.txt +0 -0
- data/README.rdoc +56 -0
- data/Rakefile +22 -0
- data/TODO.txt +2 -0
- data/bin/goshrine_bot +10 -0
- data/goshrine_bot.gemspec +33 -0
- data/goshrine_bot.yml +5 -0
- data/lib/goshrine_bot/client.rb +180 -0
- data/lib/goshrine_bot/core_ext/hash.rb +53 -0
- data/lib/goshrine_bot/faye/channel.rb +143 -0
- data/lib/goshrine_bot/faye/client.rb +283 -0
- data/lib/goshrine_bot/faye/connection.rb +122 -0
- data/lib/goshrine_bot/faye/error.rb +44 -0
- data/lib/goshrine_bot/faye/grammar.rb +58 -0
- data/lib/goshrine_bot/faye/namespace.rb +20 -0
- data/lib/goshrine_bot/faye/rack_adapter.rb +115 -0
- data/lib/goshrine_bot/faye/server.rb +266 -0
- data/lib/goshrine_bot/faye/timeouts.rb +21 -0
- data/lib/goshrine_bot/faye/transport.rb +123 -0
- data/lib/goshrine_bot/faye.rb +36 -0
- data/lib/goshrine_bot/game.rb +252 -0
- data/lib/goshrine_bot/gtp_stdio_client.rb +130 -0
- data/lib/goshrine_bot/httpclient.rb +288 -0
- data/lib/goshrine_bot/runner.rb +91 -0
- data/lib/goshrine_bot.rb +25 -0
- metadata +104 -0
    
        data/History.txt
    ADDED
    
    | 
            File without changes
         | 
    
        data/Manifest.txt
    ADDED
    
    | 
            File without changes
         | 
    
        data/README.rdoc
    ADDED
    
    | @@ -0,0 +1,56 @@ | |
| 1 | 
            +
            = GoShrine Bot Client
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            * http://goshrine.com
         | 
| 4 | 
            +
            * http://github.com/ps2/goshrine_bot
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            The GoShrine bot client is a library that allows you connect a local Go playing program that speaks GTP (like gnugo) to goshrine.com
         | 
| 7 | 
            +
             | 
| 8 | 
            +
            The GTP protocol is documented here: http://www.lysator.liu.se/~gunnar/gtp/
         | 
| 9 | 
            +
             | 
| 10 | 
            +
            == INSTALL:
         | 
| 11 | 
            +
             | 
| 12 | 
            +
            * gem install goshrine_bot
         | 
| 13 | 
            +
             | 
| 14 | 
            +
            == USE:
         | 
| 15 | 
            +
             | 
| 16 | 
            +
            * Create a normal account on goshrine for your bot.
         | 
| 17 | 
            +
            * Email feedback@goshrine.com with the account name, and request that it be changed to a bot account.
         | 
| 18 | 
            +
            * Edit goshrine_bot.yml with your account info.
         | 
| 19 | 
            +
            * Create a database.yml file in the following format:
         | 
| 20 | 
            +
             | 
| 21 | 
            +
            botname:
         | 
| 22 | 
            +
              :login: yourbotlogin
         | 
| 23 | 
            +
              :password: yourbotpassword
         | 
| 24 | 
            +
              :gtp_cmd_line: gnugo --mode gtp --level 1
         | 
| 25 | 
            +
             | 
| 26 | 
            +
            * Run goshrine_bot botname
         | 
| 27 | 
            +
             | 
| 28 | 
            +
            == REQUIREMENTS:
         | 
| 29 | 
            +
             | 
| 30 | 
            +
            * json
         | 
| 31 | 
            +
            * eventmachine
         | 
| 32 | 
            +
             | 
| 33 | 
            +
            == LICENSE:
         | 
| 34 | 
            +
             | 
| 35 | 
            +
            (The MIT License)
         | 
| 36 | 
            +
             | 
| 37 | 
            +
            Copyright (c) 2010 Pete Schwamb
         | 
| 38 | 
            +
             | 
| 39 | 
            +
            Permission is hereby granted, free of charge, to any person obtaining
         | 
| 40 | 
            +
            a copy of this software and associated documentation files (the
         | 
| 41 | 
            +
            'Software'), to deal in the Software without restriction, including
         | 
| 42 | 
            +
            without limitation the rights to use, copy, modify, merge, publish,
         | 
| 43 | 
            +
            distribute, sublicense, and/or sell copies of the Software, and to
         | 
| 44 | 
            +
            permit persons to whom the Software is furnished to do so, subject to
         | 
| 45 | 
            +
            the following conditions:
         | 
| 46 | 
            +
             | 
| 47 | 
            +
            The above copyright notice and this permission notice shall be
         | 
| 48 | 
            +
            included in all copies or substantial portions of the Software.
         | 
| 49 | 
            +
             | 
| 50 | 
            +
            THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
         | 
| 51 | 
            +
            EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
         | 
| 52 | 
            +
            MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
         | 
| 53 | 
            +
            IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
         | 
| 54 | 
            +
            CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
         | 
| 55 | 
            +
            TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
         | 
| 56 | 
            +
            SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
         | 
    
        data/Rakefile
    ADDED
    
    | @@ -0,0 +1,22 @@ | |
| 1 | 
            +
            # Available options:
         | 
| 2 | 
            +
            #
         | 
| 3 | 
            +
            # rake test - Runs all test cases.
         | 
| 4 | 
            +
            # rake package - Runs test cases and builds packages for distribution.
         | 
| 5 | 
            +
            # rake rdoc - Builds API documentation in doc dir.
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            require 'rake'
         | 
| 8 | 
            +
            require 'rspec/core/rake_task'
         | 
| 9 | 
            +
            require 'rake/gempackagetask'
         | 
| 10 | 
            +
             | 
| 11 | 
            +
            task :default => :spec
         | 
| 12 | 
            +
             | 
| 13 | 
            +
            RSpec::Core::RakeTask.new do |rspec|
         | 
| 14 | 
            +
              #rspec.ruby_opts="-w"
         | 
| 15 | 
            +
            end
         | 
| 16 | 
            +
             | 
| 17 | 
            +
            load(File.join(File.dirname(__FILE__), "goshrine_bot.gemspec"))
         | 
| 18 | 
            +
             | 
| 19 | 
            +
            Rake::GemPackageTask.new(SPEC) do |package|
         | 
| 20 | 
            +
              # do nothing: I just need a gem but this block is required
         | 
| 21 | 
            +
            end
         | 
| 22 | 
            +
             | 
    
        data/TODO.txt
    ADDED
    
    
    
        data/bin/goshrine_bot
    ADDED
    
    
| @@ -0,0 +1,33 @@ | |
| 1 | 
            +
            DIR = File.dirname(__FILE__)
         | 
| 2 | 
            +
            LIB = File.join(DIR, *%w[lib goshrine_bot.rb])
         | 
| 3 | 
            +
            VERSION = open(LIB) { |lib|
         | 
| 4 | 
            +
              lib.each { |line|
         | 
| 5 | 
            +
                if v = line[/^\s*VERSION\s*=\s*(['"])(\d\.\d\.\d)\1/, 2]
         | 
| 6 | 
            +
                  break v
         | 
| 7 | 
            +
                end
         | 
| 8 | 
            +
              }
         | 
| 9 | 
            +
            }
         | 
| 10 | 
            +
             | 
| 11 | 
            +
            SPEC = Gem::Specification.new do |s|
         | 
| 12 | 
            +
              s.name = "goshrine_bot"
         | 
| 13 | 
            +
              s.version = VERSION
         | 
| 14 | 
            +
              s.platform = Gem::Platform::RUBY
         | 
| 15 | 
            +
              s.authors = ["Pete Schwamb"]
         | 
| 16 | 
            +
              s.email = ["pete@schwamb.net"]
         | 
| 17 | 
            +
              s.homepage = "http://github.com/ps2/goshrine_bot"
         | 
| 18 | 
            +
              s.summary = "A client to connect GTP go programs to GoShrine"
         | 
| 19 | 
            +
              s.description = <<-END_DESCRIPTION.gsub(/\s+/, " ").strip
         | 
| 20 | 
            +
              The GoShrine bot client is a library that allows you connect a local Go playing program that speaks GTP (like gnugo) to http://goshrine.com.
         | 
| 21 | 
            +
              END_DESCRIPTION
         | 
| 22 | 
            +
             | 
| 23 | 
            +
              s.required_rubygems_version = "~> 1.9.2"
         | 
| 24 | 
            +
              s.required_rubygems_version = "~> 1.3.6"
         | 
| 25 | 
            +
             | 
| 26 | 
            +
              s.add_development_dependency "rspec"
         | 
| 27 | 
            +
             | 
| 28 | 
            +
              s.executables = ['goshrine_bot']
         | 
| 29 | 
            +
             | 
| 30 | 
            +
              s.files = `git ls-files`.split("\n")
         | 
| 31 | 
            +
              s.test_files = `git ls-files -- spec/*_spec.rb`.split("\n")
         | 
| 32 | 
            +
              s.require_paths = %w[lib]
         | 
| 33 | 
            +
            end
         | 
    
        data/goshrine_bot.yml
    ADDED
    
    
| @@ -0,0 +1,180 @@ | |
| 1 | 
            +
             | 
| 2 | 
            +
            module GoshrineBot
         | 
| 3 | 
            +
             | 
| 4 | 
            +
              class Client
         | 
| 5 | 
            +
             | 
| 6 | 
            +
                class << self
         | 
| 7 | 
            +
                end
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                def initialize(options)
         | 
| 10 | 
            +
                  @base_url = URI::parse(options[:server_url])
         | 
| 11 | 
            +
                  @options = options
         | 
| 12 | 
            +
                  @password = options[:password]
         | 
| 13 | 
            +
                  @login = options[:login]
         | 
| 14 | 
            +
                  @gtp_cmd_line = options[:gtp_cmd_line]
         | 
| 15 | 
            +
                  @connected = false
         | 
| 16 | 
            +
                  @cookie = ""
         | 
| 17 | 
            +
                  @games = {}
         | 
| 18 | 
            +
                end
         | 
| 19 | 
            +
                
         | 
| 20 | 
            +
                def gtp_cmd_line
         | 
| 21 | 
            +
                  @gtp_cmd_line
         | 
| 22 | 
            +
                end
         | 
| 23 | 
            +
                
         | 
| 24 | 
            +
                def my_user_id
         | 
| 25 | 
            +
                  @my_user_id
         | 
| 26 | 
            +
                end
         | 
| 27 | 
            +
                
         | 
| 28 | 
            +
                def parse_cookie(headers)
         | 
| 29 | 
            +
                  cookies = []
         | 
| 30 | 
            +
                  headers.each do |h|
         | 
| 31 | 
            +
                    if h.match(/^Set-Cookie: /)
         | 
| 32 | 
            +
                      pieces = h[12..-1].split("=")
         | 
| 33 | 
            +
                      cookies << "#{pieces[0]}=#{pieces[1].split(";").first}";
         | 
| 34 | 
            +
                    end
         | 
| 35 | 
            +
                  end
         | 
| 36 | 
            +
                  @cookie = cookies.join("; ") + ';'
         | 
| 37 | 
            +
                end
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                def login(&blk)
         | 
| 40 | 
            +
                  # login
         | 
| 41 | 
            +
                  http = http_post('/sessions/create', {'login' => @login, 'password' => @password})
         | 
| 42 | 
            +
                  http.callback {|response|
         | 
| 43 | 
            +
                    #puts "login returned: #{response[:status]}"
         | 
| 44 | 
            +
                    if response[:status].to_i == 401
         | 
| 45 | 
            +
                      puts "Invalid Login or Password"
         | 
| 46 | 
            +
                      EventMachine::stop
         | 
| 47 | 
            +
                    else
         | 
| 48 | 
            +
                      parse_cookie(response[:headers])
         | 
| 49 | 
            +
                      user = JSON.parse(response[:content])
         | 
| 50 | 
            +
                      @queue_id = user['queue_id']
         | 
| 51 | 
            +
                      @my_user_id = user['id']
         | 
| 52 | 
            +
                      if user['user_type'] != 'bot'
         | 
| 53 | 
            +
                        puts "Account #{user['login']} is not registered as a robot!"
         | 
| 54 | 
            +
                        EventMachine::stop
         | 
| 55 | 
            +
                      else
         | 
| 56 | 
            +
                        puts "Login successful"
         | 
| 57 | 
            +
                        blk.call
         | 
| 58 | 
            +
                      end
         | 
| 59 | 
            +
                    end
         | 
| 60 | 
            +
                  }
         | 
| 61 | 
            +
                  http.errback {|response|
         | 
| 62 | 
            +
                    puts "Login failed (network issue?)";
         | 
| 63 | 
            +
                  }
         | 
| 64 | 
            +
                end
         | 
| 65 | 
            +
                
         | 
| 66 | 
            +
                def http_post(url, data = nil)
         | 
| 67 | 
            +
                  headers = {'Accept' => 'application/json', :cookie => @cookie}
         | 
| 68 | 
            +
                  if data.is_a? Hash
         | 
| 69 | 
            +
                    data = data.to_params        
         | 
| 70 | 
            +
                    contenttype = "application/x-www-form-urlencoded"
         | 
| 71 | 
            +
                  else
         | 
| 72 | 
            +
                    contenttype = "application/octet-stream"
         | 
| 73 | 
            +
                  end
         | 
| 74 | 
            +
                  #puts "Posting #{data}"
         | 
| 75 | 
            +
                  GoshrineBot::HttpClient.request(
         | 
| 76 | 
            +
                    :host => @base_url.host,
         | 
| 77 | 
            +
                    :port => @base_url.port,
         | 
| 78 | 
            +
                    :contenttype => contenttype,
         | 
| 79 | 
            +
                    :request => url,
         | 
| 80 | 
            +
                    :verb => 'POST',
         | 
| 81 | 
            +
                    :content => data,
         | 
| 82 | 
            +
                    :custom_headers => headers)
         | 
| 83 | 
            +
                end
         | 
| 84 | 
            +
             | 
| 85 | 
            +
                def http_get(url)
         | 
| 86 | 
            +
                  headers = {'Accept' => 'application/json', :cookie => @cookie}
         | 
| 87 | 
            +
                  GoshrineBot::HttpClient.request(
         | 
| 88 | 
            +
                    :host => @base_url.host,
         | 
| 89 | 
            +
                    :port => @base_url.port,
         | 
| 90 | 
            +
                    :request => url,
         | 
| 91 | 
            +
                    :custom_headers => headers)
         | 
| 92 | 
            +
                end
         | 
| 93 | 
            +
                
         | 
| 94 | 
            +
                def subscribe
         | 
| 95 | 
            +
                  @client.subscribe("/user/private/" + @queue_id) do |m|
         | 
| 96 | 
            +
                    msg_type = m["type"]
         | 
| 97 | 
            +
                    case msg_type
         | 
| 98 | 
            +
                    when 'match_requested'
         | 
| 99 | 
            +
                      handle_match_request(m["match_request"])
         | 
| 100 | 
            +
                    when 'match_accepted'
         | 
| 101 | 
            +
                      handle_match_accept(m["game_token"])
         | 
| 102 | 
            +
                    else
         | 
| 103 | 
            +
                      puts "Unsupported private message type: #{msg_type}"
         | 
| 104 | 
            +
                    end
         | 
| 105 | 
            +
                    #@msg_queue_in.push(doc)
         | 
| 106 | 
            +
                  end
         | 
| 107 | 
            +
                  @client.subscribe("/room/1") do |m|
         | 
| 108 | 
            +
                    #@msg_queue_in.push(doc)
         | 
| 109 | 
            +
                  end
         | 
| 110 | 
            +
                end
         | 
| 111 | 
            +
             | 
| 112 | 
            +
                def run
         | 
| 113 | 
            +
                  login {
         | 
| 114 | 
            +
                    @client = Faye::Client.new((@base_url + '/events').to_s, :timeout => 120, :cookie => @cookie)
         | 
| 115 | 
            +
                    subscribe          
         | 
| 116 | 
            +
                    load_existing_games {
         | 
| 117 | 
            +
                      EM::add_periodic_timer( 60 ) {
         | 
| 118 | 
            +
                        @games.each do |token, game|
         | 
| 119 | 
            +
                          game.idle_check
         | 
| 120 | 
            +
                        end
         | 
| 121 | 
            +
                      }
         | 
| 122 | 
            +
                    }
         | 
| 123 | 
            +
                  }
         | 
| 124 | 
            +
                end
         | 
| 125 | 
            +
             | 
| 126 | 
            +
                def load_existing_games(&blk)
         | 
| 127 | 
            +
                  http = http_get('/game/active')
         | 
| 128 | 
            +
                  http.callback {|response|
         | 
| 129 | 
            +
                    #puts "Got #{response.inspect}"
         | 
| 130 | 
            +
                    parse_cookie(response[:headers])
         | 
| 131 | 
            +
                    games = JSON.parse(response[:content])
         | 
| 132 | 
            +
                    puts "#{games.count} game(s) in progress"
         | 
| 133 | 
            +
                    games.each do |game_attrs|
         | 
| 134 | 
            +
                      game = GameInProgress.new(self)
         | 
| 135 | 
            +
                      game.update_from_game_list(game_attrs)
         | 
| 136 | 
            +
                      if game.move_number != game.moves.size
         | 
| 137 | 
            +
                        puts "Only #{game.moves.size} available! Expected #{game.move_number} in game #{game.token}"
         | 
| 138 | 
            +
                      else
         | 
| 139 | 
            +
                        add_game(game)
         | 
| 140 | 
            +
                      end
         | 
| 141 | 
            +
                    end
         | 
| 142 | 
            +
                    blk.call
         | 
| 143 | 
            +
                  }
         | 
| 144 | 
            +
                end
         | 
| 145 | 
            +
                
         | 
| 146 | 
            +
                def add_game(game)
         | 
| 147 | 
            +
                  @games[game.game_id] = game
         | 
| 148 | 
            +
                  game.make_move # does nothing if its not our turn, or the game is not started
         | 
| 149 | 
            +
                  @client.subscribe("/game/private/" + game.token + '/' + @queue_id) do |m|
         | 
| 150 | 
            +
                    game.private_message(m)
         | 
| 151 | 
            +
                  end
         | 
| 152 | 
            +
                  @client.subscribe("/game/play/" + game.token) do |m|
         | 
| 153 | 
            +
                    game.play_message(m)
         | 
| 154 | 
            +
                  end
         | 
| 155 | 
            +
                end
         | 
| 156 | 
            +
             | 
| 157 | 
            +
                def handle_match_accept(token)
         | 
| 158 | 
            +
                  game = GameInProgress.new(self)
         | 
| 159 | 
            +
                  http = http_get("/g/#{token}")
         | 
| 160 | 
            +
                  http.callback {|response|
         | 
| 161 | 
            +
                    attrs = JSON.parse(response[:content])
         | 
| 162 | 
            +
                    game.update_from_game_list(attrs)
         | 
| 163 | 
            +
                    add_game(game)
         | 
| 164 | 
            +
                  }      
         | 
| 165 | 
            +
                end
         | 
| 166 | 
            +
                
         | 
| 167 | 
            +
                def handle_match_request(request)
         | 
| 168 | 
            +
                  game = GameInProgress.new(self)
         | 
| 169 | 
            +
                  game.update_from_match_request(request)
         | 
| 170 | 
            +
                  http = http_get("/match/accept?id=#{game.challenge_id}")
         | 
| 171 | 
            +
                  http.callback {|response|
         | 
| 172 | 
            +
                    attrs = JSON.parse(response[:content])
         | 
| 173 | 
            +
                    game.update_from_game_list(attrs)
         | 
| 174 | 
            +
                    add_game(game)
         | 
| 175 | 
            +
                  }
         | 
| 176 | 
            +
                end
         | 
| 177 | 
            +
                
         | 
| 178 | 
            +
              end
         | 
| 179 | 
            +
            end
         | 
| 180 | 
            +
             | 
| @@ -0,0 +1,53 @@ | |
| 1 | 
            +
            class Hash
         | 
| 2 | 
            +
              # Stolen partially from Merb : http://noobkit.com/show/ruby/gems/development/merb/hash/to_params.html
         | 
| 3 | 
            +
              # Convert this hash to a query string:
         | 
| 4 | 
            +
              #   
         | 
| 5 | 
            +
              #   { :name => "Bob",
         | 
| 6 | 
            +
              #     :address => {
         | 
| 7 | 
            +
              #       :street => '111 Ruby Ave.',
         | 
| 8 | 
            +
              #       :city => 'Ruby Central',
         | 
| 9 | 
            +
              #       :phones => ['111-111-1111', '222-222-2222']
         | 
| 10 | 
            +
              #     }
         | 
| 11 | 
            +
              #   }.to_params
         | 
| 12 | 
            +
              #   #=> "name=Bob&address[city]=Ruby Central&address[phones]=111-111-1111222-222-2222&address[street]=111 Ruby Ave."
         | 
| 13 | 
            +
              # 
         | 
| 14 | 
            +
              def to_params
         | 
| 15 | 
            +
                params = ''
         | 
| 16 | 
            +
                stack = []
         | 
| 17 | 
            +
                
         | 
| 18 | 
            +
                each do |k, v|
         | 
| 19 | 
            +
                  if v.is_a?(Hash)
         | 
| 20 | 
            +
                    stack << [k,v]
         | 
| 21 | 
            +
                  elsif v.is_a?(Array)
         | 
| 22 | 
            +
                    stack << [k,Hash.from_array(v)]
         | 
| 23 | 
            +
                  else
         | 
| 24 | 
            +
                    params << "#{k}=#{v}&"
         | 
| 25 | 
            +
                  end
         | 
| 26 | 
            +
                end
         | 
| 27 | 
            +
                
         | 
| 28 | 
            +
                stack.each do |parent, hash|
         | 
| 29 | 
            +
                  hash.each do |k, v|
         | 
| 30 | 
            +
                    if v.is_a?(Hash)
         | 
| 31 | 
            +
                      stack << ["#{parent}[#{k}]", v]
         | 
| 32 | 
            +
                    else
         | 
| 33 | 
            +
                      params << "#{parent}[#{k}]=#{v}&"
         | 
| 34 | 
            +
                    end
         | 
| 35 | 
            +
                  end
         | 
| 36 | 
            +
                end
         | 
| 37 | 
            +
                
         | 
| 38 | 
            +
                params.chop! # trailing &
         | 
| 39 | 
            +
                params
         | 
| 40 | 
            +
              end
         | 
| 41 | 
            +
              
         | 
| 42 | 
            +
              ##
         | 
| 43 | 
            +
              # Builds a hash from an array with keys as array indices.
         | 
| 44 | 
            +
              def self.from_array(array = [])
         | 
| 45 | 
            +
                h = Hash.new
         | 
| 46 | 
            +
                array.size.times do |t|
         | 
| 47 | 
            +
                  h[t] = array[t]
         | 
| 48 | 
            +
                end
         | 
| 49 | 
            +
                h
         | 
| 50 | 
            +
              end
         | 
| 51 | 
            +
             | 
| 52 | 
            +
            end
         | 
| 53 | 
            +
             | 
| @@ -0,0 +1,143 @@ | |
| 1 | 
            +
            module Faye
         | 
| 2 | 
            +
              class Channel
         | 
| 3 | 
            +
                
         | 
| 4 | 
            +
                include Observable
         | 
| 5 | 
            +
                attr_reader :name
         | 
| 6 | 
            +
                
         | 
| 7 | 
            +
                def initialize(name)
         | 
| 8 | 
            +
                  @name = name
         | 
| 9 | 
            +
                end
         | 
| 10 | 
            +
                
         | 
| 11 | 
            +
                def <<(message)
         | 
| 12 | 
            +
                  changed(true)
         | 
| 13 | 
            +
                  notify_observers(:message, message)
         | 
| 14 | 
            +
                end
         | 
| 15 | 
            +
                
         | 
| 16 | 
            +
                HANDSHAKE   = '/meta/handshake'
         | 
| 17 | 
            +
                CONNECT     = '/meta/connect'
         | 
| 18 | 
            +
                SUBSCRIBE   = '/meta/subscribe'
         | 
| 19 | 
            +
                UNSUBSCRIBE = '/meta/unsubscribe'
         | 
| 20 | 
            +
                DISCONNECT  = '/meta/disconnect'
         | 
| 21 | 
            +
                
         | 
| 22 | 
            +
                META        = :meta
         | 
| 23 | 
            +
                SERVICE     = :service
         | 
| 24 | 
            +
                
         | 
| 25 | 
            +
                class << self
         | 
| 26 | 
            +
                  def valid?(name)
         | 
| 27 | 
            +
                    Grammar::CHANNEL_NAME =~ name or
         | 
| 28 | 
            +
                    Grammar::CHANNEL_PATTERN =~ name
         | 
| 29 | 
            +
                  end
         | 
| 30 | 
            +
                  
         | 
| 31 | 
            +
                  def parse(name)
         | 
| 32 | 
            +
                    return nil unless valid?(name)
         | 
| 33 | 
            +
                    name.split('/')[1..-1].map { |s| s.to_sym }
         | 
| 34 | 
            +
                  end
         | 
| 35 | 
            +
                  
         | 
| 36 | 
            +
                  def meta?(name)
         | 
| 37 | 
            +
                    segments = parse(name)
         | 
| 38 | 
            +
                    segments ? (segments.first == META) : nil
         | 
| 39 | 
            +
                  end
         | 
| 40 | 
            +
                  
         | 
| 41 | 
            +
                  def service?(name)
         | 
| 42 | 
            +
                    segments = parse(name)
         | 
| 43 | 
            +
                    segments ? (segments.first == SERVICE) : nil
         | 
| 44 | 
            +
                  end
         | 
| 45 | 
            +
                  
         | 
| 46 | 
            +
                  def subscribable?(name)
         | 
| 47 | 
            +
                    return nil unless valid?(name)
         | 
| 48 | 
            +
                    not meta?(name) and not service?(name)
         | 
| 49 | 
            +
                  end
         | 
| 50 | 
            +
                end
         | 
| 51 | 
            +
                
         | 
| 52 | 
            +
                class Tree
         | 
| 53 | 
            +
                  include Enumerable
         | 
| 54 | 
            +
                  attr_accessor :value
         | 
| 55 | 
            +
                  
         | 
| 56 | 
            +
                  def initialize(value = nil)
         | 
| 57 | 
            +
                    @value = value
         | 
| 58 | 
            +
                    @children = {}
         | 
| 59 | 
            +
                  end
         | 
| 60 | 
            +
                  
         | 
| 61 | 
            +
                  # Remove channels that have no subscribers
         | 
| 62 | 
            +
                  def delete_unobserved_channels
         | 
| 63 | 
            +
                    total_observers = 0
         | 
| 64 | 
            +
                    if @value
         | 
| 65 | 
            +
                      total_observers += @value.count_observers
         | 
| 66 | 
            +
                      @value = nil if @value.count_observers == 0
         | 
| 67 | 
            +
                    end
         | 
| 68 | 
            +
                    
         | 
| 69 | 
            +
                    @children.delete_if { |key, subtree|
         | 
| 70 | 
            +
                      num_child_observers = subtree.delete_unobserved_channels
         | 
| 71 | 
            +
                      total_observers += num_child_observers
         | 
| 72 | 
            +
                      num_child_observers == 0
         | 
| 73 | 
            +
                    }
         | 
| 74 | 
            +
                    total_observers
         | 
| 75 | 
            +
                  end
         | 
| 76 | 
            +
                  
         | 
| 77 | 
            +
                  def each_child
         | 
| 78 | 
            +
                    @children.each { |key, subtree| yield(key, subtree) }
         | 
| 79 | 
            +
                  end
         | 
| 80 | 
            +
                  
         | 
| 81 | 
            +
                  def each(prefix = [], &block)
         | 
| 82 | 
            +
                    each_child { |path, subtree| subtree.each(prefix + [path], &block) }
         | 
| 83 | 
            +
                    yield(prefix, @value) unless @value.nil?
         | 
| 84 | 
            +
                  end
         | 
| 85 | 
            +
                  
         | 
| 86 | 
            +
                  def keys
         | 
| 87 | 
            +
                    map { |key, value| '/' + key * '/' }
         | 
| 88 | 
            +
                  end
         | 
| 89 | 
            +
                  
         | 
| 90 | 
            +
                  def [](name)
         | 
| 91 | 
            +
                    subtree = traverse(name)
         | 
| 92 | 
            +
                    subtree ? subtree.value : nil
         | 
| 93 | 
            +
                  end
         | 
| 94 | 
            +
                  
         | 
| 95 | 
            +
                  def []=(name, value)
         | 
| 96 | 
            +
                    subtree = traverse(name, true)
         | 
| 97 | 
            +
                    subtree.value = value unless subtree.nil?
         | 
| 98 | 
            +
                  end
         | 
| 99 | 
            +
                  
         | 
| 100 | 
            +
                  def traverse(path, create_if_absent = false)
         | 
| 101 | 
            +
                    path = Channel.parse(path) if String === path
         | 
| 102 | 
            +
                    
         | 
| 103 | 
            +
                    return nil if path.nil?
         | 
| 104 | 
            +
                    return self if path.empty?
         | 
| 105 | 
            +
                    
         | 
| 106 | 
            +
                    subtree = @children[path.first]
         | 
| 107 | 
            +
                    return nil if subtree.nil? and not create_if_absent
         | 
| 108 | 
            +
                    subtree = @children[path.first] = self.class.new if subtree.nil?
         | 
| 109 | 
            +
                    
         | 
| 110 | 
            +
                    subtree.traverse(path[1..-1], create_if_absent)
         | 
| 111 | 
            +
                  end
         | 
| 112 | 
            +
                  
         | 
| 113 | 
            +
                  def glob(path = [])
         | 
| 114 | 
            +
                    path = Channel.parse(path) if String === path
         | 
| 115 | 
            +
                    
         | 
| 116 | 
            +
                    return [] if path.nil?
         | 
| 117 | 
            +
                    return @value.nil? ? [] : [@value] if path.empty?
         | 
| 118 | 
            +
                    
         | 
| 119 | 
            +
                    if path == [:*]
         | 
| 120 | 
            +
                      return @children.inject([]) do |list, (key, subtree)|
         | 
| 121 | 
            +
                        list << subtree.value unless subtree.value.nil?
         | 
| 122 | 
            +
                        list
         | 
| 123 | 
            +
                      end
         | 
| 124 | 
            +
                    end
         | 
| 125 | 
            +
                    
         | 
| 126 | 
            +
                    if path == [:**]
         | 
| 127 | 
            +
                      list = map { |key, value| value }
         | 
| 128 | 
            +
                      list.pop unless @value.nil?
         | 
| 129 | 
            +
                      return list
         | 
| 130 | 
            +
                    end
         | 
| 131 | 
            +
                    
         | 
| 132 | 
            +
                    list = @children.values_at(path.first, :*).
         | 
| 133 | 
            +
                                     compact.
         | 
| 134 | 
            +
                                     map { |t| t.glob(path[1..-1]) }
         | 
| 135 | 
            +
                    
         | 
| 136 | 
            +
                    list << @children[:**].value if @children[:**]
         | 
| 137 | 
            +
                    list.flatten
         | 
| 138 | 
            +
                  end
         | 
| 139 | 
            +
                end
         | 
| 140 | 
            +
                
         | 
| 141 | 
            +
              end
         | 
| 142 | 
            +
            end
         | 
| 143 | 
            +
             |