diff --git a/src/applications/chatlog/conduit/ConduitAPI_chatlog_record_Method.php b/src/applications/chatlog/conduit/ConduitAPI_chatlog_record_Method.php
index 2b4ddb0b05..ca9f22c746 100644
--- a/src/applications/chatlog/conduit/ConduitAPI_chatlog_record_Method.php
+++ b/src/applications/chatlog/conduit/ConduitAPI_chatlog_record_Method.php
@@ -1,77 +1,78 @@
 <?php
 
 /**
  * @group conduit
  */
 final class ConduitAPI_chatlog_record_Method
   extends ConduitAPI_chatlog_Method {
 
   public function getMethodStatus() {
     return self::METHOD_STATUS_UNSTABLE;
   }
 
   public function getMethodDescription() {
     return "Record chatter.";
   }
 
   public function defineParamTypes() {
     return array(
       'logs' => 'required list<dict>',
     );
   }
 
   public function defineReturnType() {
     return 'list<id>';
   }
 
   public function defineErrorTypes() {
     return array(
     );
   }
 
   protected function execute(ConduitAPIRequest $request) {
     $logs = $request->getValue('logs');
     if (!is_array($logs)) {
       $logs = array();
     }
 
     $template = new PhabricatorChatLogEvent();
     $template->setLoggedByPHID($request->getUser()->getPHID());
 
     $objs = array();
     foreach ($logs as $log) {
       $channel_name = idx($log, 'channel');
       $service_name = idx($log, 'serviceName');
       $service_type = idx($log, 'serviceType');
 
-      $channel = id(new PhabricatorChatLogChannel())
-                 ->loadOneWhere(
-                   'channelName = %s AND serviceName = %s
-                    AND serviceType = %s', $channel_name,
-                    $service_name, $service_type
-                    );
+      $channel = id(new PhabricatorChatLogChannel())->loadOneWhere(
+        'channelName = %s AND serviceName = %s AND serviceType = %s',
+        $channel_name,
+        $service_name,
+        $service_type);
 
       if (!$channel) {
         $channel = id(new PhabricatorChatLogChannel())
-                   ->setChannelName($channel_name)
-                   ->setserviceName($service_name)
-                   ->setServiceType($service_type)
-                   ->save();
+          ->setChannelName($channel_name)
+          ->setserviceName($service_name)
+          ->setServiceType($service_type)
+          ->setViewPolicy(PhabricatorPolicies::POLICY_USER)
+          ->setEditPolicy(PhabricatorPolicies::POLICY_USER)
+          ->save();
       }
 
       $obj = clone $template;
       $obj->setChannel($channel_name);
       $obj->setChannelID($channel->getID());
       $obj->setType(idx($log, 'type'));
       $obj->setAuthor(idx($log, 'author'));
       $obj->setEpoch(idx($log, 'epoch'));
       $obj->setMessage(idx($log, 'message'));
       $obj->save();
 
       $objs[] = $obj;
     }
 
     return array_values(mpull($objs, 'getID'));
   }
 
 }
diff --git a/src/infrastructure/daemon/bot/PhabricatorBot.php b/src/infrastructure/daemon/bot/PhabricatorBot.php
index 6a803b22d6..78ccab5140 100644
--- a/src/infrastructure/daemon/bot/PhabricatorBot.php
+++ b/src/infrastructure/daemon/bot/PhabricatorBot.php
@@ -1,132 +1,136 @@
 <?php
 
 /**
  * Simple IRC bot which runs as a Phabricator daemon. Although this bot is
  * somewhat useful, it is also intended to serve as a demo of how to write
  * "system agents" which communicate with Phabricator over Conduit, so you can
  * script system interactions and integrate with other systems.
  *
  * NOTE: This is super janky and experimental right now.
  *
  * @group irc
  */
 final class PhabricatorBot extends PhabricatorDaemon {
 
   private $handlers;
 
   private $conduit;
   private $config;
   private $pollFrequency;
 
   public function run() {
     $argv = $this->getArgv();
     if (count($argv) !== 1) {
       throw new Exception("usage: PhabricatorBot <json_config_file>");
     }
 
     $json_raw = Filesystem::readFile($argv[0]);
     $config = json_decode($json_raw, true);
     if (!is_array($config)) {
       throw new Exception("File '{$argv[0]}' is not valid JSON!");
     }
 
     $nick                   = idx($config, 'nick', 'phabot');
     $handlers               = idx($config, 'handlers', array());
     $protocol_adapter_class = idx(
       $config,
       'protocol-adapter',
       'PhabricatorIRCProtocolAdapter');
     $this->pollFrequency = idx($config, 'poll-frequency', 1);
 
     $this->config = $config;
 
     foreach ($handlers as $handler) {
       $obj = newv($handler, array($this));
       $this->handlers[] = $obj;
     }
 
     $conduit_uri = idx($config, 'conduit.uri');
     if ($conduit_uri) {
       $conduit_user = idx($config, 'conduit.user');
       $conduit_cert = idx($config, 'conduit.cert');
 
       // Normalize the path component of the URI so users can enter the
       // domain without the "/api/" part.
       $conduit_uri = new PhutilURI($conduit_uri);
       $conduit_uri->setPath('/api/');
       $conduit_uri = (string)$conduit_uri;
 
       $conduit = new ConduitClient($conduit_uri);
       $response = $conduit->callMethodSynchronous(
         'conduit.connect',
         array(
           'client'            => 'PhabricatorBot',
           'clientVersion'     => '1.0',
           'clientDescription' => php_uname('n').':'.$nick,
           'user'              => $conduit_user,
           'certificate'       => $conduit_cert,
         ));
 
       $this->conduit = $conduit;
     }
 
     // Instantiate Protocol Adapter, for now follow same technique as
     // handler instantiation
     $this->protocolAdapter = newv($protocol_adapter_class, array());
     $this->protocolAdapter
       ->setConfig($this->config)
       ->connect();
 
     $this->runLoop();
   }
 
   public function getConfig($key, $default = null) {
     return idx($this->config, $key, $default);
   }
 
   private function runLoop() {
     do {
       $this->stillWorking();
 
       $messages = $this->protocolAdapter->getNextMessages($this->pollFrequency);
       if (count($messages) > 0) {
         foreach ($messages as $message) {
           $this->routeMessage($message);
         }
       }
 
       foreach ($this->handlers as $handler) {
         $handler->runBackgroundTasks();
       }
     } while (true);
   }
 
   public function writeMessage(PhabricatorBotMessage $message) {
     return $this->protocolAdapter->writeMessage($message);
   }
 
   private function routeMessage(PhabricatorBotMessage $message) {
     $ignore = $this->getConfig('ignore');
     if ($ignore && in_array($message->getSender(), $ignore)) {
       return;
     }
 
     foreach ($this->handlers as $handler) {
       try {
         $handler->receiveMessage($message);
       } catch (Exception $ex) {
         phlog($ex);
       }
     }
   }
 
+  public function getAdapter() {
+    return $this->protocolAdapter;
+  }
+
   public function getConduit() {
     if (empty($this->conduit)) {
       throw new Exception(
         "This bot is not configured with a Conduit uplink. Set 'conduit.uri', ".
         "'conduit.user' and 'conduit.cert' in the configuration to connect.");
     }
     return $this->conduit;
   }
 
 }
diff --git a/src/infrastructure/daemon/bot/adapter/PhabricatorBaseProtocolAdapter.php b/src/infrastructure/daemon/bot/adapter/PhabricatorBaseProtocolAdapter.php
index 3d7c980f26..4a8a62d092 100644
--- a/src/infrastructure/daemon/bot/adapter/PhabricatorBaseProtocolAdapter.php
+++ b/src/infrastructure/daemon/bot/adapter/PhabricatorBaseProtocolAdapter.php
@@ -1,41 +1,55 @@
 <?php
 
 /**
  * Defines the api for protocol adapters for @{class:PhabricatorBot}
  */
 abstract class PhabricatorBaseProtocolAdapter {
 
   private $config;
 
   public function setConfig($config) {
     $this->config = $config;
     return $this;
   }
 
   public function getConfig($key, $default = null) {
     return idx($this->config, $key, $default);
   }
 
   /**
    * Performs any connection logic necessary for the protocol
    */
   abstract public function connect();
 
   /**
    * This is the spout for messages coming in from the protocol.
    * This will be called in the main event loop of the bot daemon
    * So if if doesn't implement some sort of blocking timeout
    * (e.g. select-based socket polling), it should at least sleep
    * for some period of time in order to not overwhelm the processor.
    *
    * @param Int $poll_frequency The number of seconds between polls
    */
   abstract public function getNextMessages($poll_frequency);
 
   /**
    * This is the output mechanism for the protocol.
    *
    * @param PhabricatorBotMessage $message The message to write
    */
   abstract public function writeMessage(PhabricatorBotMessage $message);
+
+  /**
+   * String identifying the service type the adapter provides access to, like
+   * "irc", "campfire", "flowdock", "hipchat", etc.
+   */
+  abstract public function getServiceType();
+
+  /**
+   * String identifying the service name the adapter is connecting to. This is
+   * used to distinguish between instances of a service. For example, for IRC,
+   * this should return the IRC network the client is connecting to.
+   */
+  abstract public function getServiceName();
+
 }
diff --git a/src/infrastructure/daemon/bot/adapter/PhabricatorBotBaseStreamingProtocolAdapter.php b/src/infrastructure/daemon/bot/adapter/PhabricatorBotBaseStreamingProtocolAdapter.php
index 31dd8bcbad..6c4d7f1627 100644
--- a/src/infrastructure/daemon/bot/adapter/PhabricatorBotBaseStreamingProtocolAdapter.php
+++ b/src/infrastructure/daemon/bot/adapter/PhabricatorBotBaseStreamingProtocolAdapter.php
@@ -1,158 +1,163 @@
 <?php
 
 abstract class PhabricatorBotBaseStreamingProtocolAdapter
   extends PhabricatorBaseProtocolAdapter {
 
   private $readBuffers;
   private $authtoken;
   private $server;
   private $readHandles;
   private $multiHandle;
   private $active;
   private $inRooms = array();
 
+  public function getServiceName() {
+    $uri = new PhutilURI($this->server);
+    return $uri->getDomain();
+  }
+
   public function connect() {
     $this->server = $this->getConfig('server');
     $this->authtoken = $this->getConfig('authtoken');
     $rooms = $this->getConfig('join');
 
     // First, join the room
     if (!$rooms) {
       throw new Exception("Not configured to join any rooms!");
     }
 
     $this->readBuffers = array();
 
     // Set up our long poll in a curl multi request so we can
     // continue running while it executes in the background
     $this->multiHandle = curl_multi_init();
     $this->readHandles = array();
 
     foreach ($rooms as $room_id) {
       $this->joinRoom($room_id);
 
       // Set up the curl stream for reading
       $url = $this->buildStreamingUrl($room_id);
       $this->readHandle[$url] = curl_init();
       curl_setopt($this->readHandle[$url], CURLOPT_URL, $url);
       curl_setopt($this->readHandle[$url], CURLOPT_RETURNTRANSFER, true);
       curl_setopt($this->readHandle[$url], CURLOPT_FOLLOWLOCATION, 1);
       curl_setopt(
         $this->readHandle[$url],
         CURLOPT_USERPWD,
         $this->authtoken.':x');
       curl_setopt(
         $this->readHandle[$url],
         CURLOPT_HTTPHEADER,
         array("Content-type: application/json"));
       curl_setopt(
         $this->readHandle[$url],
         CURLOPT_WRITEFUNCTION,
         array($this, 'read'));
       curl_setopt($this->readHandle[$url], CURLOPT_BUFFERSIZE, 128);
       curl_setopt($this->readHandle[$url], CURLOPT_TIMEOUT, 0);
 
       curl_multi_add_handle($this->multiHandle, $this->readHandle[$url]);
 
       // Initialize read buffer
       $this->readBuffers[$url] = '';
     }
 
     $this->active = null;
     $this->blockingMultiExec();
   }
 
   protected function joinRoom($room_id) {
     // Optional hook, by default, do nothing
   }
 
   // This is our callback for the background curl multi-request.
   // Puts the data read in on the readBuffer for processing.
   private function read($ch, $data) {
     $info = curl_getinfo($ch);
     $length = strlen($data);
     $this->readBuffers[$info['url']] .= $data;
     return $length;
   }
 
   private function blockingMultiExec() {
     do {
       $status = curl_multi_exec($this->multiHandle, $this->active);
     } while ($status == CURLM_CALL_MULTI_PERFORM);
 
     // Check for errors
     if ($status != CURLM_OK) {
       throw new Exception(
         "Phabricator Bot had a problem reading from stream.");
     }
   }
 
   public function getNextMessages($poll_frequency) {
     $messages = array();
 
     if (!$this->active) {
       throw new Exception("Phabricator Bot stopped reading from stream.");
     }
 
     // Prod our http request
     curl_multi_select($this->multiHandle, $poll_frequency);
     $this->blockingMultiExec();
 
     // Process anything waiting on the read buffer
     while ($m = $this->processReadBuffer()) {
       $messages[] = $m;
     }
 
     return $messages;
   }
 
   private function processReadBuffer() {
     foreach ($this->readBuffers as $url => &$buffer) {
       $until = strpos($buffer, "}\r");
       if ($until == false) {
         continue;
       }
 
       $message = substr($buffer, 0, $until + 1);
       $buffer = substr($buffer, $until + 2);
 
       $m_obj = json_decode($message, true);
       if ($message = $this->processMessage($m_obj)) {
         return $message;
       }
     }
 
     // If we're here, there's nothing to process
     return false;
   }
 
   protected function performPost($endpoint, $data = Null) {
     $uri = new PhutilURI($this->server);
     $uri->setPath($endpoint);
 
     $payload = json_encode($data);
 
     list($output) = id(new HTTPSFuture($uri))
       ->setMethod('POST')
       ->addHeader('Content-Type', 'application/json')
       ->addHeader('Authorization', $this->getAuthorizationHeader())
       ->setData($payload)
       ->resolvex();
 
     $output = trim($output);
     if (strlen($output)) {
       return json_decode($output, true);
     }
 
     return true;
   }
 
   protected function getAuthorizationHeader() {
     return 'Basic '.base64_encode($this->authtoken.':x');
   }
 
   abstract protected function buildStreamingUrl($channel);
 
   abstract protected function processMessage($raw_object);
 }
 
diff --git a/src/infrastructure/daemon/bot/adapter/PhabricatorBotFlowdockProtocolAdapter.php b/src/infrastructure/daemon/bot/adapter/PhabricatorBotFlowdockProtocolAdapter.php
index 8124aca6e8..86da1f5541 100644
--- a/src/infrastructure/daemon/bot/adapter/PhabricatorBotFlowdockProtocolAdapter.php
+++ b/src/infrastructure/daemon/bot/adapter/PhabricatorBotFlowdockProtocolAdapter.php
@@ -1,78 +1,82 @@
 <?php
 
 final class PhabricatorBotFlowdockProtocolAdapter
   extends PhabricatorBotBaseStreamingProtocolAdapter {
 
+  public function getServiceType() {
+    return 'Flowdock';
+  }
+
   protected function buildStreamingUrl($channel) {
     $organization = $this->getConfig('organization');
     $ssl = $this->getConfig('ssl');
 
     $url = ($ssl) ? "https://" : "http://";
     $url .= "stream.flowdock.com/flows/{$organization}/{$channel}";
 
     return $url;
   }
 
   protected function processMessage($m_obj) {
       $command = null;
       switch ($m_obj['event']) {
         case 'message':
           $command = 'MESSAGE';
           break;
         default:
           // For now, ignore anything which we don't otherwise know about.
           break;
       }
 
       if ($command === null) {
         return false;
       }
 
       // TODO: These should be usernames, not user IDs.
       $sender = id(new PhabricatorBotUser())
         ->setName($m_obj['user']);
 
       $target = id(new PhabricatorBotChannel())
         ->setName($m_obj['flow']);
 
       return id(new PhabricatorBotMessage())
         ->setCommand($command)
         ->setSender($sender)
         ->setTarget($target)
         ->setBody($m_obj['content']);
   }
 
   public function writeMessage(PhabricatorBotMessage $message) {
     switch ($message->getCommand()) {
       case 'MESSAGE':
         $this->speak(
           $message->getBody(),
           $message->getTarget());
         break;
     }
   }
 
   private function speak(
     $body,
     PhabricatorBotTarget $flow) {
 
     list($organization, $room_id) = explode(":", $flow->getName());
 
     $this->performPost(
       "/flows/{$organization}/{$room_id}/messages",
       array(
         'event' => 'message',
         'content' => $body));
   }
 
   public function __destruct() {
     if ($this->readHandles) {
       foreach ($this->readHandles as $read_handle) {
         curl_multi_remove_handle($this->multiHandle, $read_handle);
         curl_close($read_handle);
       }
     }
 
     curl_multi_close($this->multiHandle);
   }
 }
diff --git a/src/infrastructure/daemon/bot/adapter/PhabricatorCampfireProtocolAdapter.php b/src/infrastructure/daemon/bot/adapter/PhabricatorCampfireProtocolAdapter.php
index 8f8d62f196..b3afaa8a06 100644
--- a/src/infrastructure/daemon/bot/adapter/PhabricatorCampfireProtocolAdapter.php
+++ b/src/infrastructure/daemon/bot/adapter/PhabricatorCampfireProtocolAdapter.php
@@ -1,108 +1,112 @@
 <?php
 
 final class PhabricatorCampfireProtocolAdapter
   extends PhabricatorBotBaseStreamingProtocolAdapter {
 
+  public function getServiceType() {
+    return 'Campfire';
+  }
+
   protected function buildStreamingUrl($channel) {
     $ssl = $this->getConfig('ssl');
 
     $url = ($ssl) ? "https://" : "http://";
     $url .= "streaming.campfirenow.com/room/{$channel}/live.json";
 
     return $url;
   }
 
   protected function processMessage($m_obj) {
       $command = null;
       switch ($m_obj['type']) {
         case 'TextMessage':
           $command = 'MESSAGE';
           break;
         case 'PasteMessage':
           $command = 'PASTE';
           break;
         default:
           // For now, ignore anything which we don't otherwise know about.
           break;
       }
 
       if ($command === null) {
         return false;
       }
 
       // TODO: These should be usernames, not user IDs.
       $sender = id(new PhabricatorBotUser())
         ->setName($m_obj['user_id']);
 
       $target = id(new PhabricatorBotChannel())
         ->setName($m_obj['room_id']);
 
       return id(new PhabricatorBotMessage())
         ->setCommand($command)
         ->setSender($sender)
         ->setTarget($target)
         ->setBody($m_obj['body']);
   }
 
   public function writeMessage(PhabricatorBotMessage $message) {
     switch ($message->getCommand()) {
       case 'MESSAGE':
         $this->speak(
           $message->getBody(),
           $message->getTarget());
         break;
       case 'SOUND':
         $this->speak(
           $message->getBody(),
           $message->getTarget(),
           'SoundMessage');
         break;
       case 'PASTE':
         $this->speak(
           $message->getBody(),
           $message->getTarget(),
           'PasteMessage');
         break;
     }
   }
 
   protected function joinRoom($room_id) {
     $this->performPost("/room/{$room_id}/join.json");
     $this->inRooms[$room_id] = true;
   }
 
   private function leaveRoom($room_id) {
     $this->performPost("/room/{$room_id}/leave.json");
     unset($this->inRooms[$room_id]);
   }
 
   private function speak(
     $message,
     PhabricatorBotTarget $channel,
     $type = 'TextMessage') {
 
     $room_id = $channel->getName();
 
     $this->performPost(
       "/room/{$room_id}/speak.json",
       array(
         'message' => array(
           'type' => $type,
           'body' => $message)));
   }
 
   public function __destruct() {
     foreach ($this->inRooms as $room_id => $ignored) {
       $this->leaveRoom($room_id);
     }
 
     if ($this->readHandles) {
       foreach ($this->readHandles as $read_handle) {
         curl_multi_remove_handle($this->multiHandle, $read_handle);
         curl_close($read_handle);
       }
     }
 
     curl_multi_close($this->multiHandle);
   }
 }
diff --git a/src/infrastructure/daemon/bot/adapter/PhabricatorIRCProtocolAdapter.php b/src/infrastructure/daemon/bot/adapter/PhabricatorIRCProtocolAdapter.php
index a3d001cdbf..2d472e7bc3 100644
--- a/src/infrastructure/daemon/bot/adapter/PhabricatorIRCProtocolAdapter.php
+++ b/src/infrastructure/daemon/bot/adapter/PhabricatorIRCProtocolAdapter.php
@@ -1,241 +1,249 @@
 <?php
 
 final class PhabricatorIRCProtocolAdapter
   extends PhabricatorBaseProtocolAdapter {
 
   private $socket;
 
   private $writeBuffer;
   private $readBuffer;
 
+  public function getServiceType() {
+    return 'IRC';
+  }
+
+  public function getServiceName() {
+    return $this->getConfig('network', $this->getConfig('server'));
+  }
+
   // Hash map of command translations
   public static $commandTranslations = array(
     'PRIVMSG' => 'MESSAGE');
 
   public function connect() {
     $nick = $this->getConfig('nick', 'phabot');
     $server = $this->getConfig('server');
     $port = $this->getConfig('port', 6667);
     $pass = $this->getConfig('pass');
     $ssl = $this->getConfig('ssl', false);
     $user = $this->getConfig('user', $nick);
 
     if (!preg_match('/^[A-Za-z0-9_`[{}^|\]\\-]+$/', $nick)) {
       throw new Exception(
         "Nickname '{$nick}' is invalid!");
     }
 
     $errno = null;
     $error = null;
     if (!$ssl) {
       $socket = fsockopen($server, $port, $errno, $error);
     } else {
       $socket = fsockopen('ssl://'.$server, $port, $errno, $error);
     }
     if (!$socket) {
       throw new Exception("Failed to connect, #{$errno}: {$error}");
     }
     $ok = stream_set_blocking($socket, false);
     if (!$ok) {
       throw new Exception("Failed to set stream nonblocking.");
     }
 
     $this->socket = $socket;
     $this->write("USER {$user} 0 * :{$user}");
     if ($pass) {
       $this->write("PASS {$pass}");
     }
     $this->write("NICK {$nick}");
   }
 
   public function getNextMessages($poll_frequency) {
     $messages = array();
 
     $read = array($this->socket);
     if (strlen($this->writeBuffer)) {
       $write = array($this->socket);
     } else {
       $write = array();
     }
     $except = array();
 
     $ok = @stream_select($read, $write, $except, $timeout_sec = 1);
     if ($ok === false) {
       throw new Exception(
         "socket_select() failed: ".socket_strerror(socket_last_error()));
     }
 
     if ($read) {
       // Test for connection termination; in PHP, fread() off a nonblocking,
       // closed socket is empty string.
       if (feof($this->socket)) {
         // This indicates the connection was terminated on the other side,
         // just exit via exception and let the overseer restart us after a
         // delay so we can reconnect.
         throw new Exception("Remote host closed connection.");
       }
       do {
         $data = fread($this->socket, 4096);
         if ($data === false) {
           throw new Exception("fread() failed!");
         } else {
           $messages[] = id(new PhabricatorBotMessage())
             ->setCommand("LOG")
             ->setBody(">>> ".$data);
           $this->readBuffer .= $data;
         }
       } while (strlen($data));
     }
 
     if ($write) {
       do {
         $len = fwrite($this->socket, $this->writeBuffer);
         if ($len === false) {
           throw new Exception("fwrite() failed!");
         } else {
           $messages[] = id(new PhabricatorBotMessage())
             ->setCommand("LOG")
             ->setBody(">>> ".substr($this->writeBuffer, 0, $len));
           $this->writeBuffer = substr($this->writeBuffer, $len);
         }
       } while (strlen($this->writeBuffer));
     }
 
     while (($m = $this->processReadBuffer()) !== false) {
       if ($m !== null) {
         $messages[] = $m;
       }
     }
 
     return $messages;
   }
 
   private function write($message) {
     $this->writeBuffer .= $message."\r\n";
     return $this;
   }
 
   public function writeMessage(PhabricatorBotMessage $message) {
     switch ($message->getCommand()) {
       case 'MESSAGE':
       case 'PASTE':
         $name = $message->getTarget()->getName();
         $body = $message->getBody();
         $this->write("PRIVMSG {$name} :{$body}");
         return true;
       default:
         return false;
     }
   }
 
   private function processReadBuffer() {
     $until = strpos($this->readBuffer, "\r\n");
     if ($until === false) {
       return false;
     }
 
     $message = substr($this->readBuffer, 0, $until);
     $this->readBuffer = substr($this->readBuffer, $until + 2);
 
     $pattern =
       '/^'.
       '(?::(?P<sender>(\S+?))(?:!\S*)? )?'. // This may not be present.
       '(?P<command>[A-Z0-9]+) '.
       '(?P<data>.*)'.
       '$/';
 
     $matches = null;
     if (!preg_match($pattern, $message, $matches)) {
       throw new Exception("Unexpected message from server: {$message}");
     }
 
     if ($this->handleIRCProtocol($matches)) {
       return null;
     }
 
     $command = $this->getBotCommand($matches['command']);
     list($target, $body) = $this->parseMessageData($command, $matches['data']);
 
     if (!strlen($matches['sender'])) {
       $sender = null;
     } else {
       $sender = id(new PhabricatorBotUser())
        ->setName($matches['sender']);
     }
 
     $bot_message = id(new PhabricatorBotMessage())
       ->setSender($sender)
       ->setCommand($command)
       ->setTarget($target)
       ->setBody($body);
 
     return $bot_message;
   }
 
   private function handleIRCProtocol(array $matches) {
     $data = $matches['data'];
     switch ($matches['command']) {
       case '422': // Error - no MOTD
       case '376': // End of MOTD
         $nickpass = $this->getConfig('nickpass');
         if ($nickpass) {
           $this->write("PRIVMSG nickserv :IDENTIFY {$nickpass}");
         }
         $join = $this->getConfig('join');
         if (!$join) {
           throw new Exception("Not configured to join any channels!");
         }
         foreach ($join as $channel) {
           $this->write("JOIN {$channel}");
         }
         return true;
       case 'PING':
         $this->write("PONG {$data}");
         return true;
     }
 
     return false;
   }
 
   private function getBotCommand($irc_command) {
     if (isset(self::$commandTranslations[$irc_command])) {
       return self::$commandTranslations[$irc_command];
     }
 
     // We have no translation for this command, use as-is
     return $irc_command;
   }
 
   private function parseMessageData($command, $data) {
     switch ($command) {
       case 'MESSAGE':
         $matches = null;
         if (preg_match('/^(\S+)\s+:?(.*)$/', $data, $matches)) {
 
           $target_name = $matches[1];
           if (strncmp($target_name, '#', 1) === 0) {
             $target = id(new PhabricatorBotChannel())
               ->setName($target_name);
           } else {
             $target = id(new PhabricatorBotUser())
               ->setName($target_name);
           }
 
           return array(
             $target,
             rtrim($matches[2], "\r\n"));
         }
         break;
     }
 
     // By default we assume there is no target, only a body
     return array(
       null,
       $data);
   }
 
   public function __destruct() {
     $this->write("QUIT Goodbye.");
     fclose($this->socket);
   }
 }
diff --git a/src/infrastructure/daemon/bot/handler/PhabricatorBotHandler.php b/src/infrastructure/daemon/bot/handler/PhabricatorBotHandler.php
index a2543b319c..77ac1891e6 100644
--- a/src/infrastructure/daemon/bot/handler/PhabricatorBotHandler.php
+++ b/src/infrastructure/daemon/bot/handler/PhabricatorBotHandler.php
@@ -1,65 +1,73 @@
 <?php
 
 /**
  * Responds to IRC messages. You plug a bunch of these into a
  * @{class:PhabricatorBot} to give it special behavior.
  *
  * @group irc
  */
 abstract class PhabricatorBotHandler {
 
   private $bot;
 
   final public function __construct(PhabricatorBot $irc_bot) {
     $this->bot = $irc_bot;
   }
 
   final protected function writeMessage(PhabricatorBotMessage $message) {
     $this->bot->writeMessage($message);
     return $this;
   }
 
   final protected function getConduit() {
     return $this->bot->getConduit();
   }
 
   final protected function getConfig($key, $default = null) {
     return $this->bot->getConfig($key, $default);
   }
 
   final protected function getURI($path) {
     $base_uri = new PhutilURI($this->bot->getConfig('conduit.uri'));
     $base_uri->setPath($path);
     return (string)$base_uri;
   }
 
+  final protected function getServiceName() {
+    return $this->bot->getAdapter()->getServiceName();
+  }
+
+  final protected function getServiceType() {
+    return $this->bot->getAdapter()->getServiceType();
+  }
+
   abstract public function receiveMessage(PhabricatorBotMessage $message);
 
   public function runBackgroundTasks() {
     return;
   }
 
   public function replyTo(PhabricatorBotMessage $original_message, $body) {
     if ($original_message->getCommand() != 'MESSAGE') {
       throw new Exception(
         "Handler is trying to reply to something which is not a message!");
     }
 
     $reply = id(new PhabricatorBotMessage())
       ->setCommand('MESSAGE');
 
     if ($original_message->getTarget()->isPublic()) {
       // This is a public target, like a chatroom. Send the response to the
       // chatroom.
       $reply->setTarget($original_message->getTarget());
     } else {
       // This is a private target, like a private message. Send the response
       // back to the sender (presumably, we are the target).
       $reply->setTarget($original_message->getSender());
     }
 
     $reply->setBody($body);
 
     return $this->writeMessage($reply);
   }
 }
diff --git a/src/infrastructure/daemon/bot/handler/PhabricatorBotLogHandler.php b/src/infrastructure/daemon/bot/handler/PhabricatorBotLogHandler.php
index f852efefb7..7f110c9487 100644
--- a/src/infrastructure/daemon/bot/handler/PhabricatorBotLogHandler.php
+++ b/src/infrastructure/daemon/bot/handler/PhabricatorBotLogHandler.php
@@ -1,78 +1,80 @@
 <?php
 
 /**
  * Logs chatter.
  *
  * @group irc
  */
 final class PhabricatorBotLogHandler extends PhabricatorBotHandler {
 
   private $futures = array();
 
   public function receiveMessage(PhabricatorBotMessage $message) {
 
     switch ($message->getCommand()) {
       case 'MESSAGE':
         $target = $message->getTarget();
         if (!$target->isPublic()) {
           // Don't log private messages, although maybe we should for debugging?
           break;
         }
 
         $target_name = $target->getName();
 
         $logs = array(
           array(
             'channel' => $target_name,
             'type'    => 'mesg',
             'epoch'   => time(),
             'author'  => $message->getSender()->getName(),
             'message' => $message->getBody(),
+            'serviceName' => $this->getServiceName(),
+            'serviceType' => $this->getServiceType(),
           ),
         );
 
         $this->futures[] = $this->getConduit()->callMethod(
           'chatlog.record',
           array(
             'logs' => $logs,
           ));
 
         $prompts = array(
           '/where is the (chat)?log\?/i',
           '/where am i\?/i',
           '/what year is (this|it)\?/i',
         );
 
         $tell = false;
         foreach ($prompts as $prompt) {
           if (preg_match($prompt, $message->getBody())) {
             $tell = true;
             break;
           }
         }
 
         if ($tell) {
           $response = $this->getURI(
             '/chatlog/channel/'.phutil_escape_uri($target_name).'/');
 
           $this->replyTo($message, $response);
         }
 
         break;
     }
   }
 
   public function runBackgroundTasks() {
     foreach ($this->futures as $key => $future) {
       try {
         if ($future->isReady()) {
           unset($this->futures[$key]);
         }
       } catch (Exception $ex) {
         unset($this->futures[$key]);
         phlog($ex);
       }
     }
   }
 
 }