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); } } } }