commit ecdabe959057281f9350bbcd0735c9639206244f
Author: Marc Dequènes (Duck) <Duck@DuckCorp.org>
Date:   Sat Aug 14 02:35:17 2010 +0200

    [evol] uncomplete MapMaker bot implementation (global cyborg classes, use of EventMachine with UNIX socket backend, configuration adjustments, ...) (refs #31)

diff --git a/README b/README
index c10cfcf..9fd990a 100644
--- a/README
+++ b/README
@@ -7,12 +7,17 @@ libgettext-ruby-util
 
 Depends:
 --------
-liblog4r-ruby
-libgettext-ruby1.8
-libtmail-ruby1.8
-rails
-libgpgme-ruby
-libactiveldap-ruby
-libxmpp4r-ruby
-libdaemons-ruby
+common:
+  - liblog4r-ruby
+  - libgettext-ruby1.8
+  - libdaemons-ruby
+  - libeventmachine-ruby
+  - libxmpp4r-ruby (if using XMPP backend)
+Postman:
+  - libtmail-ruby1.8
+  - rails
+  - libgpgme-ruby
+MapMaker:
+Librarian:
+  - libactiveldap-ruby
 
diff --git a/bin/mapmaker b/bin/mapmaker
new file mode 100755
index 0000000..2309070
--- /dev/null
+++ b/bin/mapmaker
@@ -0,0 +1,60 @@
+#!/usr/bin/ruby -Ku
+
+#--
+# CyborgHood, a distributed system management software.
+# Copyright (c) 2009-2010 Marc Dequènes (Duck) <Duck@DuckCorp.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#++
+
+# to allow in-place run for test
+$: << File.join(File.dirname(__FILE__), "..", "lib")
+
+require 'cyborghood/cyborg'
+
+
+module CyborgHood
+  class Config
+    protected
+    class MapMakerValidator < CyborgHoodValidator
+      def validate_hook_in(value, rule, path, msg_list)
+        case rule.name
+        when 'MasterZonePattern'
+          if value.gsub('#ZONE#', 'test') == value
+            msg_list << "pattern is constant"
+          end
+        end
+      end
+    end
+  end
+
+  module MapMakerHome
+    include I18nTranslation
+    bindtextdomain("cyborghood_mapmaker", {:path => Config::L10N_DIR, :charset => "UTF-8"})
+
+    class MapMaker < Cyborg
+    end
+  end
+end
+
+bot = CyborgHood::MapMakerHome::MapMaker.new
+
+trap('INT') do
+  bot.ask_to_stop
+end
+trap('TERM') do
+  bot.ask_to_stop
+end
+
+bot.run
diff --git a/bin/mapmaker_daemon b/bin/mapmaker_daemon
new file mode 100755
index 0000000..cb35a24
--- /dev/null
+++ b/bin/mapmaker_daemon
@@ -0,0 +1,37 @@
+#!/usr/bin/ruby
+
+#--
+# CyborgHood, a distributed system management software.
+# Copyright (c) 2009-2010 Marc Dequènes (Duck) <Duck@DuckCorp.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#++
+
+# to allow in-place run for test
+$: << File.join(File.dirname(__FILE__), "..", "lib")
+
+require 'daemons'
+require 'cyborghood/base/config_setup'
+
+options = {
+  :app_name     => "CyborgHood_MapMaker",
+  :dir_mode     => :normal,
+  :dir          => CyborgHood::Config::RUN_DIR,
+  :multiple     => false,
+  :mode         => :exec,
+  :monitor      => true
+}
+# deactivated: monitor process not stopped when program is asked to stop
+#  :backtrace   => true,
+Daemons.run(File.join(CyborgHood::Config::BIN_DIR, 'mapmaker'), options)
diff --git a/data/cyborghood/default_config/global.yaml b/data/cyborghood/default_config/global.yaml
index f465656..1436c79 100755
--- a/data/cyborghood/default_config/global.yaml
+++ b/data/cyborghood/default_config/global.yaml
@@ -5,6 +5,9 @@ debug:
   flags: []
 contact: {}
 i18n: {}
+botnet:
+  connection_type: unix_socket
+  params: {}
 ldap:
   host: localhost
   port: 389
diff --git a/data/cyborghood/default_config/mapmaker.yaml b/data/cyborghood/default_config/mapmaker.yaml
new file mode 100644
index 0000000..189f152
--- /dev/null
+++ b/data/cyborghood/default_config/mapmaker.yaml
@@ -0,0 +1,2 @@
+---
+dns: {}
diff --git a/data/cyborghood/default_config/postman.yaml b/data/cyborghood/default_config/postman.yaml
index bb7e83a..ffed5bc 100755
--- a/data/cyborghood/default_config/postman.yaml
+++ b/data/cyborghood/default_config/postman.yaml
@@ -6,4 +6,3 @@ mail:
   smtp_server: localhost
   smtp_port: 25
   max_drift_time: 3600
-dns: {}
diff --git a/data/cyborghood/schema/global.yaml b/data/cyborghood/schema/global.yaml
index a65d5b3..cdb0559 100644
--- a/data/cyborghood/schema/global.yaml
+++ b/data/cyborghood/schema/global.yaml
@@ -26,6 +26,15 @@ mapping:
         type: seq
         sequence:
           - type: str
+  "botnet":
+    type: map
+    mapping:
+      "connection_type": {type: str, enum: [unix_socket]}
+      "params":
+        type: map
+        mapping:
+          =:
+            type: any
   # temporary location
   "ldap":
     type: map
diff --git a/data/cyborghood/schema/mapmaker.yaml b/data/cyborghood/schema/mapmaker.yaml
new file mode 100644
index 0000000..dd3a1bf
--- /dev/null
+++ b/data/cyborghood/schema/mapmaker.yaml
@@ -0,0 +1,7 @@
+---
+type: map
+mapping:
+  "dns":
+    type: map
+    mapping:
+      "master_zone_pattern": {type: str, required: yes, name: MasterZonePattern}
diff --git a/data/cyborghood/schema/postman.yaml b/data/cyborghood/schema/postman.yaml
index 430bcab..bdb7e2e 100755
--- a/data/cyborghood/schema/postman.yaml
+++ b/data/cyborghood/schema/postman.yaml
@@ -19,7 +19,3 @@ mapping:
       "key_id": {type: str, required: yes}
       "key_passphrase": {type: str, required: yes}
       "max_drift_time": {type: int}
-  "dns":
-    type: map
-    mapping:
-      "master_zone_pattern": {type: str, required: yes}
diff --git a/lib/cyborghood/base/config.rb b/lib/cyborghood/base/config.rb
index 460589e..e3df5c1 100644
--- a/lib/cyborghood/base/config.rb
+++ b/lib/cyborghood/base/config.rb
@@ -44,6 +44,8 @@ module CyborgHood
         config = merge_into_default_config(config, spc_config)
       end
 
+      config['bot_name'] = name
+
       # create config object for easier access
       @@config = config.to_ostruct.freeze
     end
@@ -77,7 +79,7 @@ module CyborgHood
         if name.nil?
           filename = CONFFILE_GLOBAL
         else
-          filename = CONFFILE_BOT % name
+          filename = CONFFILE_BOT % name.downcase
         end
         filepath = File.join(File.expand_path(CFG_DIR), filename)
         File.read(filepath)
@@ -95,7 +97,7 @@ module CyborgHood
         when 'global'
           GlobalValidator
         else
-          klass_name = type.capitalize + "Validator"
+          klass_name = type + "Validator"
           if self.const_defined? klass_name
              self.const_get klass_name
           else
@@ -111,7 +113,7 @@ module CyborgHood
       # validate config with schema
       parser = Kwalify::Yaml::Parser.new(validator)
       def_data = parser.parse(def_data_raw, :filename => def_name, :untabify => true)
-      raise_if_validation_errors("#{type.capitalize} '#{def_name}'", parser.errors)
+      raise_if_validation_errors("#{type} '#{def_name}'", parser.errors)
 
       def_data
     rescue
@@ -120,7 +122,7 @@ module CyborgHood
     end
 
     def self.load_default_config(type = "global")
-      filename = File.join(DATA_DIR, "default_config", type + ".yaml")
+      filename = File.join(DATA_DIR, "default_config", type.downcase + ".yaml")
       default_config_raw = File.read(filename)
       default_config = parse(':defaults:', type, default_config_raw, false)
       default_config
@@ -130,7 +132,7 @@ module CyborgHood
     end
 
     def self.load_schema(type = "global")
-      schema_file = File.join(DATA_DIR, "schema", type + ".yaml")
+      schema_file = File.join(DATA_DIR, "schema", type.downcase + ".yaml")
       schema = YAML.load_file(schema_file)
 
       # validate schema
diff --git a/lib/cyborghood/cyborg.rb b/lib/cyborghood/cyborg.rb
index 93feae8..e2b6786 100644
--- a/lib/cyborghood/cyborg.rb
+++ b/lib/cyborghood/cyborg.rb
@@ -16,231 +16,46 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 #++
 
-require 'xmpp4r'
-require 'xmpp4r/presence'
-require 'xmpp4r/version/iq/version'
-require 'xmpp4r/version/helper/simpleresponder'
-require 'xmpp4r/caps'
-require 'xmpp4r/discovery'
-#require 'xmpp4r/vcard/helper/vcard'
-require 'xmpp4r/muc/helper/mucbrowser'
-require 'xmpp4r/muc/helper/simplemucclient'
+require 'cyborghood'
+require 'cyborghood/cyborg/server'
 
 
 module CyborgHood
   class Cyborg
-    attr_reader :jid, :room_jid, :chat_jid
+    include I18nTranslation
+    bindtextdomain("cyborghood", {:path => Config::L10N_DIR, :charset => "UTF-8"})
 
-    FEATURE_NS = 'http://duckcorp.org/protocol/cyborghood'
-
-    def initialize()
+    def initialize
       # load config
-      Config.load(self.human_name.downcase)
+      Config.load(self.human_name)
       @config = Config.instance
 
       # setup logs
       unless @config.log.nil?
         logger.output_level(@config.log.console_level) unless @config.log.console_level.nil?
-        logger.log_to_file(@config.log.file) unless @config.log.file.nil?
-      end
-
-      # prepare
-      clear
-      @jid = Jabber::JID.new(@config.xmpp.jid)
-      @room_jid = Jabber::JID.new(@config.xmpp.muc).bare
-
-      yield if block_given?
-
-      logger.info "Bot '#{self.human_name}' loaded"
-    end
-
-    def run
-      logger.info "Initializing bot..."
-
-      begin
-        @cl = Jabber::Client.new(@jid)
-        @cl.allow_tls = true
-        @cl.ssl_capath = @config.xmpp.ssl_capath unless @config.xmpp.ssl_capath.nil?
-        reconnect
-      rescue Jabber::ServerError => e
-        if e.error.type == :cancel and e.error.code == 409
-          if @nick_index < 100
-            retry
+        unless @config.log.path.nil?
+          if File.directory? @config.log.path
+            logger.log_to_file(File.join(@config.log.path, "ch_#{self.class.human_name}.log"))
           else
-            logger.fatal "Cannot connect to CyborgHood HeadQuarters because my Nickname range is full (maybe a security problem)"
-            quit(true)
+            logger.fatal "Log path does not exist or is not a directory, exiting"
+            exit 1
           end
-        else
-          logger.fatal "Cannot connect to CyborgHood HeadQuarters: " + e.error.text
-          quit(true)
         end
       end
 
-      @cl.on_exception { sleep 5; reconnect }
-
-      logger.info "Bot ready for work"
-
-      @run_thread = Thread.new { yield }
-      @run_thread.join
-
-      logger.info "Bot work is finished, going to sleep..."
-      quit
-    end
-
-    def ask_to_stop
-      @should_stop = true
-      @run_thread.wakeup if @run_thread.stop?
-    end
-
-    protected
-
-    def quit(fatal = false)
-      if @cl
-        @muc.exit if @muc and @muc.active?
-        @cl.close
-      end
-      clear
-      exit 1 if fatal
-    end
-
-    def advertise_presence(available)
-      show = (available ? :chat : :dnd)
-      @cl.send(Jabber::Presence.new.set_show(show))
-    end
-
-    def advertise_presence_in_room(available)
-      show = (available ? :chat : :dnd)
-      stanza = Jabber::Presence.new.set_show(show)
-      stanza.from = @jid.to_s
-      stanza.to = @chat_jid.to_s
-      @cl.send(stanza)
-    end
-
-    def discover_features(jid)
-      result = discover(jid)
-      result.nil? ? [] : result.features
-    end
-
-    def room_members
-      @muc.roster.keys.sort
-    end
-
-    def unnormalize_message(msg)
-      msg2 = msg.dup
-      REXML::DocType::DEFAULT_ENTITIES.each_value do |entity|
-        msg2.gsub!("&#{entity.name};", entity.value)
-      end
-      msg2.gsub!( '&amp;', '&' )
-      return msg2
-    end
-
-    private
-
-    def reconnect
-      logger.debug "Connecting to the XMPP server..."
-      @cl.connect
-      @cl.auth(@config.xmpp.passwd)
-
-      unless @cl.is_tls?
-        logger.fatal "Connection to CyborgHood HeadQuarters is not encrypted; please configure TLS support !"
-        quit(true)
-      end
-
-      # initial presence before being ready
-      logger.debug "Advertising unavailable status"
-      advertise_presence(false)
-
-      # version
-      logger.debug "Starting version responder"
-      Jabber::Version::SimpleResponder.new(@cl, PRODUCT, VERSION, "")
-
-      # advertise discovery info
-      logger.debug "Starting capabilities responder"
-      Jabber::Caps::Helper.new(@cl,
-        [Jabber::Discovery::Identity.new('client', self.human_name, 'bot')],
-        [Jabber::Discovery::Feature.new(FEATURE_NS)]
-      )
-
-      # enter MUC room
-      knock_at_room_door unless @in_room
-      enter_room
-
-      # presence
-      logger.debug "Advertising available status"
-      advertise_presence(true)
-    end
-
-    def knock_at_room_door
-    end
-
-    def enter_room
-      logger.debug "Discovering room component features"
-      unless discover_features(@room_jid).include?(Jabber::MUC::XMUC.new.namespace)
-        logger.fatal "CyborgHood HeadQuarters has no MUC support"
-        quit(true)
-      end
-
-      logger.debug "Fetching room list"
-      @muc_br = Jabber::MUC::MUCBrowser.new(@cl)
-      room_list = @muc_br.muc_rooms(@room_jid.domain)
-
-      @chat_jid = @room_jid.dup
-      @chat_jid.resource = generate_available_nick()
+      @server = BotServer.build
 
-      logger.debug "Joining room '#{@room_jid}' as '#{@chat_jid.resource}'..."
-      @muc = Jabber::MUC::SimpleMUCClient.new(@cl)
-      register_room_callbacks
-      @muc.join(@chat_jid)
-
-      if room_list.keys.include?(@room_jid.to_s)
-        on_existing_room()
-      else
-        on_missing_room()
-      end
-
-      # presence
-      logger.debug "Advertising available status in room"
-      advertise_presence_in_room(true)
-
-      @in_room = true
-    end
-
-    def on_missing_room
-      logger.fatal "CyborgHood HeadQuarters has no meeting room"
-      quit(true)
-    end
-
-    def on_existing_room
-    end
-
-    def register_room_callbacks
-    end
-
-    def discover(jid, node = nil)
-        iq = Jabber::Iq.new(:get, jid.domain)
-        iq.query = Jabber::Discovery::IqQueryDiscoInfo.new
-        iq.query.node = node unless node.nil?
-
-        result = nil
-        @cl.send_with_id(iq) {|reply|
-          result = reply.query
-        }
-        return result
+      logger.info "Bot '#{self.human_name}' loaded"
     end
 
-    def generate_available_nick
-      @nick_index += 1
-      self.human_name + "-" + @nick_index.to_s.rjust(2, "0")
+    def run
+      logger.info "Bot starting"
+      @server.run
     end
 
-    def clear
-      @cl = nil
-      @muc_br = nil
-      @muc = nil
-      @chat_jid = nil
-      @nick_index = 0
-      @in_room = false
-      @should_stop = false
+    def ask_to_stop
+      @server.stop
+      logger.info "Bot was asked to stop..."
     end
   end
 end
diff --git a/lib/cyborghood/cyborg.rb.old b/lib/cyborghood/cyborg.rb.old
new file mode 100644
index 0000000..93feae8
--- /dev/null
+++ b/lib/cyborghood/cyborg.rb.old
@@ -0,0 +1,246 @@
+#--
+# CyborgHood, a distributed system management software.
+# Copyright (c) 2009-2010 Marc Dequènes (Duck) <Duck@DuckCorp.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#++
+
+require 'xmpp4r'
+require 'xmpp4r/presence'
+require 'xmpp4r/version/iq/version'
+require 'xmpp4r/version/helper/simpleresponder'
+require 'xmpp4r/caps'
+require 'xmpp4r/discovery'
+#require 'xmpp4r/vcard/helper/vcard'
+require 'xmpp4r/muc/helper/mucbrowser'
+require 'xmpp4r/muc/helper/simplemucclient'
+
+
+module CyborgHood
+  class Cyborg
+    attr_reader :jid, :room_jid, :chat_jid
+
+    FEATURE_NS = 'http://duckcorp.org/protocol/cyborghood'
+
+    def initialize()
+      # load config
+      Config.load(self.human_name.downcase)
+      @config = Config.instance
+
+      # setup logs
+      unless @config.log.nil?
+        logger.output_level(@config.log.console_level) unless @config.log.console_level.nil?
+        logger.log_to_file(@config.log.file) unless @config.log.file.nil?
+      end
+
+      # prepare
+      clear
+      @jid = Jabber::JID.new(@config.xmpp.jid)
+      @room_jid = Jabber::JID.new(@config.xmpp.muc).bare
+
+      yield if block_given?
+
+      logger.info "Bot '#{self.human_name}' loaded"
+    end
+
+    def run
+      logger.info "Initializing bot..."
+
+      begin
+        @cl = Jabber::Client.new(@jid)
+        @cl.allow_tls = true
+        @cl.ssl_capath = @config.xmpp.ssl_capath unless @config.xmpp.ssl_capath.nil?
+        reconnect
+      rescue Jabber::ServerError => e
+        if e.error.type == :cancel and e.error.code == 409
+          if @nick_index < 100
+            retry
+          else
+            logger.fatal "Cannot connect to CyborgHood HeadQuarters because my Nickname range is full (maybe a security problem)"
+            quit(true)
+          end
+        else
+          logger.fatal "Cannot connect to CyborgHood HeadQuarters: " + e.error.text
+          quit(true)
+        end
+      end
+
+      @cl.on_exception { sleep 5; reconnect }
+
+      logger.info "Bot ready for work"
+
+      @run_thread = Thread.new { yield }
+      @run_thread.join
+
+      logger.info "Bot work is finished, going to sleep..."
+      quit
+    end
+
+    def ask_to_stop
+      @should_stop = true
+      @run_thread.wakeup if @run_thread.stop?
+    end
+
+    protected
+
+    def quit(fatal = false)
+      if @cl
+        @muc.exit if @muc and @muc.active?
+        @cl.close
+      end
+      clear
+      exit 1 if fatal
+    end
+
+    def advertise_presence(available)
+      show = (available ? :chat : :dnd)
+      @cl.send(Jabber::Presence.new.set_show(show))
+    end
+
+    def advertise_presence_in_room(available)
+      show = (available ? :chat : :dnd)
+      stanza = Jabber::Presence.new.set_show(show)
+      stanza.from = @jid.to_s
+      stanza.to = @chat_jid.to_s
+      @cl.send(stanza)
+    end
+
+    def discover_features(jid)
+      result = discover(jid)
+      result.nil? ? [] : result.features
+    end
+
+    def room_members
+      @muc.roster.keys.sort
+    end
+
+    def unnormalize_message(msg)
+      msg2 = msg.dup
+      REXML::DocType::DEFAULT_ENTITIES.each_value do |entity|
+        msg2.gsub!("&#{entity.name};", entity.value)
+      end
+      msg2.gsub!( '&amp;', '&' )
+      return msg2
+    end
+
+    private
+
+    def reconnect
+      logger.debug "Connecting to the XMPP server..."
+      @cl.connect
+      @cl.auth(@config.xmpp.passwd)
+
+      unless @cl.is_tls?
+        logger.fatal "Connection to CyborgHood HeadQuarters is not encrypted; please configure TLS support !"
+        quit(true)
+      end
+
+      # initial presence before being ready
+      logger.debug "Advertising unavailable status"
+      advertise_presence(false)
+
+      # version
+      logger.debug "Starting version responder"
+      Jabber::Version::SimpleResponder.new(@cl, PRODUCT, VERSION, "")
+
+      # advertise discovery info
+      logger.debug "Starting capabilities responder"
+      Jabber::Caps::Helper.new(@cl,
+        [Jabber::Discovery::Identity.new('client', self.human_name, 'bot')],
+        [Jabber::Discovery::Feature.new(FEATURE_NS)]
+      )
+
+      # enter MUC room
+      knock_at_room_door unless @in_room
+      enter_room
+
+      # presence
+      logger.debug "Advertising available status"
+      advertise_presence(true)
+    end
+
+    def knock_at_room_door
+    end
+
+    def enter_room
+      logger.debug "Discovering room component features"
+      unless discover_features(@room_jid).include?(Jabber::MUC::XMUC.new.namespace)
+        logger.fatal "CyborgHood HeadQuarters has no MUC support"
+        quit(true)
+      end
+
+      logger.debug "Fetching room list"
+      @muc_br = Jabber::MUC::MUCBrowser.new(@cl)
+      room_list = @muc_br.muc_rooms(@room_jid.domain)
+
+      @chat_jid = @room_jid.dup
+      @chat_jid.resource = generate_available_nick()
+
+      logger.debug "Joining room '#{@room_jid}' as '#{@chat_jid.resource}'..."
+      @muc = Jabber::MUC::SimpleMUCClient.new(@cl)
+      register_room_callbacks
+      @muc.join(@chat_jid)
+
+      if room_list.keys.include?(@room_jid.to_s)
+        on_existing_room()
+      else
+        on_missing_room()
+      end
+
+      # presence
+      logger.debug "Advertising available status in room"
+      advertise_presence_in_room(true)
+
+      @in_room = true
+    end
+
+    def on_missing_room
+      logger.fatal "CyborgHood HeadQuarters has no meeting room"
+      quit(true)
+    end
+
+    def on_existing_room
+    end
+
+    def register_room_callbacks
+    end
+
+    def discover(jid, node = nil)
+        iq = Jabber::Iq.new(:get, jid.domain)
+        iq.query = Jabber::Discovery::IqQueryDiscoInfo.new
+        iq.query.node = node unless node.nil?
+
+        result = nil
+        @cl.send_with_id(iq) {|reply|
+          result = reply.query
+        }
+        return result
+    end
+
+    def generate_available_nick
+      @nick_index += 1
+      self.human_name + "-" + @nick_index.to_s.rjust(2, "0")
+    end
+
+    def clear
+      @cl = nil
+      @muc_br = nil
+      @muc = nil
+      @chat_jid = nil
+      @nick_index = 0
+      @in_room = false
+      @should_stop = false
+    end
+  end
+end
diff --git a/lib/cyborghood/cyborg/server.rb b/lib/cyborghood/cyborg/server.rb
new file mode 100644
index 0000000..5c02e43
--- /dev/null
+++ b/lib/cyborghood/cyborg/server.rb
@@ -0,0 +1,111 @@
+#--
+# CyborgHood, a distributed system management software.
+# Copyright (c) 2009-2010 Marc Dequènes (Duck) <Duck@DuckCorp.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#++
+
+require 'eventmachine'
+
+
+module CyborgHood
+  class BotServer
+    private_class_method :new
+
+    def self.build
+      case Config.instance.botnet.connection_type
+      when 'unix_socket'
+        return BotServerUNIXSocket.new
+      else
+        raise CyberError.new(:unrecoverable, "config", "Unknown botnet connection type")
+      end
+    end
+
+    def initialize
+      @config = Config.instance
+    end
+
+    def run
+      EventMachine.run do
+        yield
+      end
+    end
+
+    def stop
+      EventMachine.stop
+    end
+  end
+
+  class BotServerUNIXSocket < BotServer
+    public_class_method :new
+
+    def initialize
+      super
+
+      @socket = File.join(Config::RUN_DIR, @config.bot_name.downcase + ".sock")
+      at_exit { remove_socket_file }
+    end
+
+    def run
+      super do
+        EventMachine.start_unix_domain_server(@socket, ConversationUNIXSocket)
+      end
+    end
+
+    private
+
+    def remove_socket_file
+      File.delete(@socket) if @socket && File.exist?(@socket)
+    end
+  end
+
+  class Conversation < EventMachine::Protocols::LineAndTextProtocol
+    private_class_method :new
+
+    def initialize(*args)
+      super
+
+      @config = Config.instance
+    end
+
+    def send_line(msg)
+      send_data "#{msg}\r\n"
+    end
+
+    def post_init
+      logger.debug "New conversation with #{identifier}"
+      send_line "220 #{PRODUCT} #{VERSION} - #{@config.bot_name}"
+    end
+
+    def receive_line(data)
+      logger.debug "Received [#{identifier}]: #{data}"
+    end
+
+    def receive_error(msg)
+      logger.error "Error [#{identifier}]: #{msg}"
+    end
+
+    def unbind
+      logger.debug "Conversation finished with #{identifier}"
+    end
+  end
+
+  class ConversationUNIXSocket < Conversation
+    public_class_method :new
+
+    def identifier
+      "unix_socket/#{@signature}"
+    end
+  end
+end
diff --git a/lib/cyborghood/services/dns.rb b/lib/cyborghood/services/dns.rb
index 66516cc..a32738a 100644
--- a/lib/cyborghood/services/dns.rb
+++ b/lib/cyborghood/services/dns.rb
@@ -31,8 +31,6 @@ module CyborgHood
         @content = nil
 
         @filename = @config.dns.master_zone_pattern.gsub("#ZONE#", @zone)
-        # TODO: should be checked at startup time
-        raise CyberError.new(:unrecoverable, "services/dns", "erroneous configuration: pattern is constant") if @filename == @config.dns.master_zone_pattern
 
         # ensure we can find the needed programs (should be handled somewhere else)
         ENV['PATH'] = (ENV['PATH'].split(":") + ["/sbin", "/usr/sbin", "/usr/local/sbin"]).uniq.join(":")
