commit cdd6154d6bc7e5034408799b673c6766f806c293
Author: Marc Dequènes (Duck) <Duck@DuckCorp.org>
Date:   Mon Nov 1 01:15:45 2010 +0100

    [evol] conversation protocol complete rework + work on action protocol §1 (refs #30)

diff --git a/lib/cyborghood/base/config.rb b/lib/cyborghood/base/config.rb
index e3df5c1..cab17b2 100644
--- a/lib/cyborghood/base/config.rb
+++ b/lib/cyborghood/base/config.rb
@@ -28,6 +28,8 @@ module CyborgHood
     CONFFILE_GLOBAL = "cyborghood.conf"
     CONFFILE_BOT = "cyborghood_%s.conf"
 
+    attr_accessor :bot_id
+
     def self.load(name = nil)
       # load all config parts
       g_config_raw = load_config_raw()
@@ -58,9 +60,7 @@ module CyborgHood
       end
     end
 
-    protected
-
-    def self.merge_into_default_config(conf_default, conf)
+    protected    def self.merge_into_default_config(conf_default, conf)
       new_conf = conf_default.dup
       conf.each_pair do |k, v|
         if conf_default.has_key?(k) and conf_default[k].is_a?(Hash) and v.is_a?(Hash)
diff --git a/lib/cyborghood/cyborg.rb b/lib/cyborghood/cyborg.rb
index 745aeb4..b7d4ebc 100644
--- a/lib/cyborghood/cyborg.rb
+++ b/lib/cyborghood/cyborg.rb
@@ -28,6 +28,7 @@ module CyborgHood
       # load config
       Config.load(self.human_name)
       @config = Config.instance
+      @config.bot_id = self.class.name
 
       # setup logs
       unless @config.log.nil?
diff --git a/lib/cyborghood/cyborg/conversation.rb b/lib/cyborghood/cyborg/conversation.rb
index 2563bc1..9bdd594 100644
--- a/lib/cyborghood/cyborg/conversation.rb
+++ b/lib/cyborghood/cyborg/conversation.rb
@@ -16,10 +16,40 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 #++
 
+require 'yaml'
 require 'cyborghood/cyborg/session'
 
 
 module CyborgHood
+
+  class Protocol
+    VERSION = "0.1"
+
+    def initialize(conversation)
+      @conversation = conversation
+    end
+
+    def receive_announce_helo(parameters)
+      unless parameters[:bot_name] =~ Conversation::BOT_ID_PATTERN
+        return send_error_protocol "bad bot name"
+      end
+      unless parameters[:protocol_version] == VERSION
+        @conversation.set_fatal
+        return send_error_protocol "protocol version does not match"
+      end
+      @conversation.set_peer_info(parameters[:bot_name], parameters[:capabilities])
+      send_reply_ok
+    end
+
+    def send_error_protocol(msg = nil)
+      @conversation.set_reply("ERROR PROTO", { :error => msg || "" })
+    end
+
+    def send_reply_ack
+      @conversation.set_reply("REPLY ACK")
+    end
+  end
+
   class Conversation < EventMachine::Protocols::LineAndTextProtocol
     private_class_method :new
 
@@ -27,8 +57,12 @@ module CyborgHood
     MaxLineLength = 16*1024
 
     EOD = "\033[0J"
-    COMMAND_PATTERN = "^#{CyborgServerInterfaceBase::NODE_PATTERN}(?: ([+?]+))?$"
+    BOT_ID_PATTERN = "[a-zA-Z0-9]+"
+    ACTION_PATTERN = "^(#{BOT_ID_PATTERN})(\d{4})-(\d{4})([+]?) ([a-zA-Z0-9 ]+)$"
     MAXIMUM_ERROR_COUNT = 3
+    MAXIMUM_LINES = 1024
+
+    attr_reader :interface
 
     def initialize(interface)
       @interface = interface
@@ -37,10 +71,17 @@ module CyborgHood
 
       @config = Config.instance
       @split_data_mode = false
-      @split_data_cmd = nil
+      @split_data_action = nil
       @split_data = []
-      @session = Session.new
+      # one session for each conversation thread
+      @sessions = {}
       @error_count = 0
+      @fatal_error = false
+      @actions_wip = {}
+      @peer_id = nil
+      @peer_capabilities = []
+
+      @protocol = Protocol.new(this)
     end
 
     def send_line(msg)
@@ -50,13 +91,14 @@ module CyborgHood
 
     def post_init
       logger.debug "New conversation with #{identifier}"
-      send_line "220 #{PRODUCT} #{VERSION} - #{@config.bot_name}"
     end
 
     def receive_line(data)
-
       return if data.empty?
 
+      @reply_action = "Internal error"
+      @reply_parameters = {}
+
       if data == EOD
         logger.debug "Received EOD [#{identifier}]"
         exit_split_mode
@@ -64,44 +106,59 @@ module CyborgHood
         if @split_data_mode
           logger.debug "Received data (split mode) [#{identifier}]: #{data}"
 
-          @split_data << data
+          if @split_data.size > MAXIMUM_LINES
+            reply_fatal_error "overflow"
+          else
+            @split_data << data
+          end
         else
           logger.debug "Received data [#{identifier}]: #{data}"
 
-          unless data =~ Regexp.new(COMMAND_PATTERN)
+          if data =~ Regexp.new(ACTION_PATTERN)
+            @thread_id = $1
+            @action_id = $2
+            flags = $3 || ""
+            action = $4
+
+            if @actions_wip[@thread_id].nil? or not @actions_wip[@thread_id].include?(@action_id)
+              @sessions[@thread_id] ||= Session.new
+              if flags.index '+'
+                enter_split_mode(action)
+              else
+                receive_action(action)
+              end
+            else
+              reply_fatal_error "wip action id reused"
+            end
+          else
             logger.error "Error [#{identifier}]: syntax error"
             @error_count += 1
 
             if @error_count >= MAXIMUM_ERROR_COUNT
-              send_line "503 too much errors, terminating"
-              close_connection_after_writing
+              reply_fatal_error "too much errors, terminating"
             else
-              send_line "552 syntax error in command"
+              reply_syntax_error "bad action"
             end
-            return
-          end
-          cmd = $1
-          flags = $2 || ""
-          @error_count = 0
-
-          if flags.index '?'
-            send_line "250+ ok"
-            send_line({'exists' => @interface.has_node?(cmd)}.to_yaml)
-            return
-          end
-
-          if flags.index '+'
-            enter_split_mode(cmd)
-          else
-            receive_command(cmd)
           end
         end
       end
+
+      send_reply
+      close_connection_after_writing if @fatal_error
     end
 
-    def receive_command(cmd, data = nil)
-      logger.debug "Executing command '#{cmd}' [#{identifier}]"
-      send_line @interface.call(@session, cmd, data)
+    def receive_action(action, data = nil)
+      logger.debug "Executing action '#{action}' [#{identifier}]"
+
+      unless data.nil?
+        parameters = YAML.parse(data)
+        reply_syntax_error "bad parameters"
+        return
+      end
+
+      # use a Protocol object and convert back result to the conversation level protocol
+      # we need to use EM.defer or EM.spawn+Deferrable, preferably after parsing, to avoid spawning threads for nothing
+      #send_line @interface.call(@session, action, data)
     end
 
     def receive_error(msg)
@@ -110,35 +167,72 @@ module CyborgHood
 
     def unbind
       logger.debug "Conversation finished with #{identifier}"
-      @session.clear
+      @sessions.each_values {|s| s.clear }
+    end
+
+    def set_peer_info(id, capabilities)
+      @peer_id = id
+      @peer_capabilities = capabilities || []
+    end
+
+    def set_reply(action, parameters = nil)
+      @reply_action =  action
+      @reply_parameters = parameters
+    end
+
+    def set_fatal
+      @fatal_error = true
+    end
+
+    def current_session
+      @sessions[@thread_id]
     end
 
     protected
 
-    def enter_split_mode(cmd)
+    def reply_syntax_error(msg = nil)
+      msg = "syntax error" + (msg ? ": " + msg : "")
+      @protocol.send_error_protocol(msg)
+    end
+
+    def reply_fatal_error(msg = nil)
+      msg = "fatal error" + (msg ? ": " + msg : "")
+      @protocol.send_error_protocol(msg)
+      set_fatal
+    end
+
+    def send_reply
+      send_line "#{@config.bot_id}-#{@thread_id}-#{@action_id}" + (@reply_parameters.nil? : "" : "+") + " #{@reply_action}"
+      unless @reply_parameters.nil?
+        @reply_parameters.to_yaml.split.each {|l| send_line l }
+        send_line EOD
+      end
+    end
+
+    def enter_split_mode(action)
       if @split_data_mode
         logger.error "Error [#{identifier}]: already in split mode"
         send_line "551 protocol error"
         @split_data_mode = false
-        @split_data_cmd = nil
+        @split_data_action = nil
       else
-        logger.debug "Entered split mode for command '#{cmd}' [#{identifier}]"
+        logger.debug "Entered split mode for action '#{action}' [#{identifier}]"
         @split_data_mode = true
-        @split_data_cmd = cmd
+        @split_data_action = action
       end
       @split_data = []
     end
 
     def exit_split_mode
       if @split_data_mode
-        logger.debug "Quit split mode for command '#{@split_data_cmd}' [#{identifier}]"
-        receive_command(@split_data_cmd, @split_data.join("\n"))
+        logger.debug "Quit split mode for action '#{@split_data_action}' [#{identifier}]"
+        receive_action(@split_data_action, @split_data.join("\n"))
       else
         logger.error "Error [#{identifier}]: not in split mode"
         send_line "551 protocol error"
       end
       @split_data_mode = false
-      @split_data_cmd = nil
+      @split_data_action = nil
       @split_data = []
     end
   end
