diff --git a/resources/ircbot/example_config.json b/resources/ircbot/example_config.json index 0e246ce536..6dadec2952 100644 --- a/resources/ircbot/example_config.json +++ b/resources/ircbot/example_config.json @@ -7,12 +7,12 @@ ], "handlers" : [ "PhabricatorIRCProtocolHandler", - "PhabricatorIRCObjectNameHandler", - "PhabricatorIRCSymbolHandler", - "PhabricatorIRCLogHandler", - "PhabricatorIRCWhatsNewHandler", - "PhabricatorIRCDifferentialNotificationHandler", - "PhabricatorIRCMacroHandler" + "PhabricatorBotObjectNameHandler", + "PhabricatorBotSymbolHandler", + "PhabricatorBotLogHandler", + "PhabricatorBotWhatsNewHandler", + "PhabricatorBotDifferentialNotificationHandler", + "PhabricatorBotMacroHandler" ], "conduit.uri" : null, diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 9e5aa560b7..4f11a0fc87 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -702,6 +702,18 @@ phutil_register_library_map(array( 'PhabricatorBarePageExample' => 'applications/uiexample/examples/PhabricatorBarePageExample.php', 'PhabricatorBarePageView' => 'view/page/PhabricatorBarePageView.php', 'PhabricatorBaseEnglishTranslation' => 'infrastructure/internationalization/PhabricatorBaseEnglishTranslation.php', + 'PhabricatorBaseProtocolAdapter' => 'infrastructure/daemon/bot/adapter/PhabricatorBaseProtocolAdapter.php', + 'PhabricatorBot' => 'infrastructure/daemon/bot/PhabricatorBot.php', + 'PhabricatorBotDebugLogHandler' => 'infrastructure/daemon/bot/handler/PhabricatorBotDebugLogHandler.php', + 'PhabricatorBotDifferentialNotificationHandler' => 'infrastructure/daemon/bot/handler/PhabricatorBotDifferentialNotificationHandler.php', + 'PhabricatorBotFeedNotificationHandler' => 'infrastructure/daemon/bot/handler/PhabricatorBotFeedNotificationHandler.php', + 'PhabricatorBotHandler' => 'infrastructure/daemon/bot/handler/PhabricatorBotHandler.php', + 'PhabricatorBotLogHandler' => 'infrastructure/daemon/bot/handler/PhabricatorBotLogHandler.php', + 'PhabricatorBotMacroHandler' => 'infrastructure/daemon/bot/handler/PhabricatorBotMacroHandler.php', + 'PhabricatorBotMessage' => 'infrastructure/daemon/bot/PhabricatorBotMessage.php', + 'PhabricatorBotObjectNameHandler' => 'infrastructure/daemon/bot/handler/PhabricatorBotObjectNameHandler.php', + 'PhabricatorBotSymbolHandler' => 'infrastructure/daemon/bot/handler/PhabricatorBotSymbolHandler.php', + 'PhabricatorBotWhatsNewHandler' => 'infrastructure/daemon/bot/handler/PhabricatorBotWhatsNewHandler.php', 'PhabricatorBuiltinPatchList' => 'infrastructure/storage/patch/PhabricatorBuiltinPatchList.php', 'PhabricatorButtonsExample' => 'applications/uiexample/examples/PhabricatorButtonsExample.php', 'PhabricatorCacheDAO' => 'applications/cache/storage/PhabricatorCacheDAO.php', @@ -917,17 +929,9 @@ phutil_register_library_map(array( 'PhabricatorHeaderView' => 'view/layout/PhabricatorHeaderView.php', 'PhabricatorHelpController' => 'applications/help/controller/PhabricatorHelpController.php', 'PhabricatorHelpKeyboardShortcutController' => 'applications/help/controller/PhabricatorHelpKeyboardShortcutController.php', - 'PhabricatorIRCBot' => 'infrastructure/daemon/irc/PhabricatorIRCBot.php', - 'PhabricatorIRCDifferentialNotificationHandler' => 'infrastructure/daemon/irc/handler/PhabricatorIRCDifferentialNotificationHandler.php', - 'PhabricatorIRCFeedNotificationHandler' => 'infrastructure/daemon/irc/handler/PhabricatorIRCFeedNotificationHandler.php', - 'PhabricatorIRCHandler' => 'infrastructure/daemon/irc/handler/PhabricatorIRCHandler.php', - 'PhabricatorIRCLogHandler' => 'infrastructure/daemon/irc/handler/PhabricatorIRCLogHandler.php', - 'PhabricatorIRCMacroHandler' => 'infrastructure/daemon/irc/handler/PhabricatorIRCMacroHandler.php', - 'PhabricatorIRCMessage' => 'infrastructure/daemon/irc/PhabricatorIRCMessage.php', - 'PhabricatorIRCObjectNameHandler' => 'infrastructure/daemon/irc/handler/PhabricatorIRCObjectNameHandler.php', - 'PhabricatorIRCProtocolHandler' => 'infrastructure/daemon/irc/handler/PhabricatorIRCProtocolHandler.php', - 'PhabricatorIRCSymbolHandler' => 'infrastructure/daemon/irc/handler/PhabricatorIRCSymbolHandler.php', - 'PhabricatorIRCWhatsNewHandler' => 'infrastructure/daemon/irc/handler/PhabricatorIRCWhatsNewHandler.php', + 'PhabricatorIRCBot' => 'infrastructure/daemon/bot/PhabricatorIRCBot.php', + 'PhabricatorIRCProtocolAdapter' => 'infrastructure/daemon/bot/adapter/PhabricatorIRCProtocolAdapter.php', + 'PhabricatorIRCProtocolHandler' => 'infrastructure/daemon/bot/handler/PhabricatorIRCProtocolHandler.php', 'PhabricatorImageTransformer' => 'applications/files/PhabricatorImageTransformer.php', 'PhabricatorInfrastructureTestCase' => 'infrastructure/__tests__/PhabricatorInfrastructureTestCase.php', 'PhabricatorInlineCommentController' => 'infrastructure/diff/PhabricatorInlineCommentController.php', @@ -2144,6 +2148,15 @@ phutil_register_library_map(array( 'PhabricatorBarePageExample' => 'PhabricatorUIExample', 'PhabricatorBarePageView' => 'AphrontPageView', 'PhabricatorBaseEnglishTranslation' => 'PhabricatorTranslation', + 'PhabricatorBot' => 'PhabricatorDaemon', + 'PhabricatorBotDebugLogHandler' => 'PhabricatorBotHandler', + 'PhabricatorBotDifferentialNotificationHandler' => 'PhabricatorBotHandler', + 'PhabricatorBotFeedNotificationHandler' => 'PhabricatorBotHandler', + 'PhabricatorBotLogHandler' => 'PhabricatorBotHandler', + 'PhabricatorBotMacroHandler' => 'PhabricatorBotHandler', + 'PhabricatorBotObjectNameHandler' => 'PhabricatorBotHandler', + 'PhabricatorBotSymbolHandler' => 'PhabricatorBotHandler', + 'PhabricatorBotWhatsNewHandler' => 'PhabricatorBotHandler', 'PhabricatorBuiltinPatchList' => 'PhabricatorSQLPatchList', 'PhabricatorButtonsExample' => 'PhabricatorUIExample', 'PhabricatorCacheDAO' => 'PhabricatorLiskDAO', @@ -2353,14 +2366,8 @@ phutil_register_library_map(array( 'PhabricatorHelpController' => 'PhabricatorController', 'PhabricatorHelpKeyboardShortcutController' => 'PhabricatorHelpController', 'PhabricatorIRCBot' => 'PhabricatorDaemon', - 'PhabricatorIRCDifferentialNotificationHandler' => 'PhabricatorIRCHandler', - 'PhabricatorIRCFeedNotificationHandler' => 'PhabricatorIRCHandler', - 'PhabricatorIRCLogHandler' => 'PhabricatorIRCHandler', - 'PhabricatorIRCMacroHandler' => 'PhabricatorIRCHandler', - 'PhabricatorIRCObjectNameHandler' => 'PhabricatorIRCHandler', - 'PhabricatorIRCProtocolHandler' => 'PhabricatorIRCHandler', - 'PhabricatorIRCSymbolHandler' => 'PhabricatorIRCHandler', - 'PhabricatorIRCWhatsNewHandler' => 'PhabricatorIRCHandler', + 'PhabricatorIRCProtocolAdapter' => 'PhabricatorBaseProtocolAdapter', + 'PhabricatorIRCProtocolHandler' => 'PhabricatorBotHandler', 'PhabricatorInfrastructureTestCase' => 'PhabricatorTestCase', 'PhabricatorInlineCommentController' => 'PhabricatorController', 'PhabricatorInlineCommentInterface' => 'PhabricatorMarkupInterface', diff --git a/src/infrastructure/daemon/bot/PhabricatorBot.php b/src/infrastructure/daemon/bot/PhabricatorBot.php new file mode 100644 index 0000000000..7f83307c64 --- /dev/null +++ b/src/infrastructure/daemon/bot/PhabricatorBot.php @@ -0,0 +1,132 @@ +getArgv(); + if (count($argv) !== 1) { + throw new Exception("usage: PhabricatorBot "); + } + + $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 writeCommand($command, $message) { + return $this->protocolAdapter->writeCommand($command, $message); + } + + private function routeMessage(PhabricatorBotMessage $message) { + $ignore = $this->getConfig('ignore'); + if ($ignore && in_array($message->getSenderNickName(), $ignore)) { + return; + } + + foreach ($this->handlers as $handler) { + try { + $handler->receiveMessage($message); + } catch (Exception $ex) { + phlog($ex); + } + } + } + + 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/irc/PhabricatorIRCMessage.php b/src/infrastructure/daemon/bot/PhabricatorBotMessage.php similarity index 97% rename from src/infrastructure/daemon/irc/PhabricatorIRCMessage.php rename to src/infrastructure/daemon/bot/PhabricatorBotMessage.php index 8d3bb14153..95e7f42bc0 100644 --- a/src/infrastructure/daemon/irc/PhabricatorIRCMessage.php +++ b/src/infrastructure/daemon/bot/PhabricatorBotMessage.php @@ -1,6 +1,6 @@ config = $config; + return $this; + } + + /** + * 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 String $command The command for the message + * @param String $message The contents of the message itself + */ + abstract public function writeCommand($command, $message); +} diff --git a/src/infrastructure/daemon/bot/adapter/PhabricatorIRCProtocolAdapter.php b/src/infrastructure/daemon/bot/adapter/PhabricatorIRCProtocolAdapter.php new file mode 100644 index 0000000000..ccd96fc0ea --- /dev/null +++ b/src/infrastructure/daemon/bot/adapter/PhabricatorIRCProtocolAdapter.php @@ -0,0 +1,156 @@ +config, 'nick', 'phabot'); + $server = idx($this->config, 'server'); + $port = idx($this->config, 'port', 6667); + $pass = idx($this->config, 'pass'); + $ssl = idx($this->config, 'ssl', false); + $user = idx($this->config, '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->writeCommand('USER', "{$user} 0 * :{$user}"); + if ($pass) { + $this->writeCommand('PASS', "{$pass}"); + } + + $this->writeCommand('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[] = new PhabricatorBotMessage( + null, + "LOG", + "<<< ".$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[] = new PhabricatorBotMessage( + null, + "LOG", + ">>> ".substr($this->writeBuffer, 0, $len)); + $this->writeBuffer = substr($this->writeBuffer, $len); + } + } while (strlen($this->writeBuffer)); + } + + while ($m = $this->processReadBuffer()) { + $messages[] = $m; + } + + return $messages; + } + + private function write($message) { + $this->writeBuffer .= $message; + return $this; + } + + public function writeCommand($command, $message) { + return $this->write($command.' '.$message."\r\n"); + } + + 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:(\S+)) )?'. // This may not be present. + '(?P[A-Z0-9]+) '. + '(?P.*)'. + '$/'; + + $matches = null; + if (!preg_match($pattern, $message, $matches)) { + throw new Exception("Unexpected message from server: {$message}"); + } + + $irc_message = new PhabricatorBotMessage( + idx($matches, 'sender'), + $matches['command'], + $matches['data']); + + return $irc_message; + } + + public function __destruct() { + $this->write("QUIT Goodbye.\r\n"); + fclose($this->socket); + } + +} diff --git a/src/infrastructure/daemon/bot/handler/PhabricatorBotDebugLogHandler.php b/src/infrastructure/daemon/bot/handler/PhabricatorBotDebugLogHandler.php new file mode 100644 index 0000000000..19ff95c562 --- /dev/null +++ b/src/infrastructure/daemon/bot/handler/PhabricatorBotDebugLogHandler.php @@ -0,0 +1,17 @@ +getCommand()) { + case 'LOG': + echo addcslashes( + $message->getRawData(), + "\0..\37\177..\377"); + echo "\n"; + break; + } + } +} diff --git a/src/infrastructure/daemon/irc/handler/PhabricatorIRCDifferentialNotificationHandler.php b/src/infrastructure/daemon/bot/handler/PhabricatorBotDifferentialNotificationHandler.php similarity index 89% rename from src/infrastructure/daemon/irc/handler/PhabricatorIRCDifferentialNotificationHandler.php rename to src/infrastructure/daemon/bot/handler/PhabricatorBotDifferentialNotificationHandler.php index c0a1f9fc88..1f801d472e 100644 --- a/src/infrastructure/daemon/irc/handler/PhabricatorIRCDifferentialNotificationHandler.php +++ b/src/infrastructure/daemon/bot/handler/PhabricatorBotDifferentialNotificationHandler.php @@ -3,12 +3,12 @@ /** * @group irc */ -final class PhabricatorIRCDifferentialNotificationHandler - extends PhabricatorIRCHandler { +final class PhabricatorBotDifferentialNotificationHandler + extends PhabricatorBotHandler { private $skippedOldEvents; - public function receiveMessage(PhabricatorIRCMessage $message) { + public function receiveMessage(PhabricatorBotMessage $message) { return; } diff --git a/src/infrastructure/daemon/irc/handler/PhabricatorIRCFeedNotificationHandler.php b/src/infrastructure/daemon/bot/handler/PhabricatorBotFeedNotificationHandler.php similarity index 96% rename from src/infrastructure/daemon/irc/handler/PhabricatorIRCFeedNotificationHandler.php rename to src/infrastructure/daemon/bot/handler/PhabricatorBotFeedNotificationHandler.php index 6f45ed936f..bd8b7e5375 100644 --- a/src/infrastructure/daemon/irc/handler/PhabricatorIRCFeedNotificationHandler.php +++ b/src/infrastructure/daemon/bot/handler/PhabricatorBotFeedNotificationHandler.php @@ -5,8 +5,8 @@ * * @group irc */ -final class PhabricatorIRCFeedNotificationHandler - extends PhabricatorIRCHandler { +final class PhabricatorBotFeedNotificationHandler + extends PhabricatorBotHandler { private $startupDelay = 30; private $lastSeenChronoKey = 0; @@ -82,7 +82,7 @@ final class PhabricatorIRCFeedNotificationHandler return false; } - public function receiveMessage(PhabricatorIRCMessage $message) { + public function receiveMessage(PhabricatorBotMessage $message) { return; } diff --git a/src/infrastructure/daemon/irc/handler/PhabricatorIRCHandler.php b/src/infrastructure/daemon/bot/handler/PhabricatorBotHandler.php similarity index 78% rename from src/infrastructure/daemon/irc/handler/PhabricatorIRCHandler.php rename to src/infrastructure/daemon/bot/handler/PhabricatorBotHandler.php index 8f059bed67..ec1dbc117b 100644 --- a/src/infrastructure/daemon/irc/handler/PhabricatorIRCHandler.php +++ b/src/infrastructure/daemon/bot/handler/PhabricatorBotHandler.php @@ -2,15 +2,15 @@ /** * Responds to IRC messages. You plug a bunch of these into a - * @{class:PhabricatorIRCBot} to give it special behavior. + * @{class:PhabricatorBot} to give it special behavior. * * @group irc */ -abstract class PhabricatorIRCHandler { +abstract class PhabricatorBotHandler { private $bot; - final public function __construct(PhabricatorIRCBot $irc_bot) { + final public function __construct(PhabricatorBot $irc_bot) { $this->bot = $irc_bot; } @@ -37,7 +37,7 @@ abstract class PhabricatorIRCHandler { return (strncmp($name, '#', 1) === 0); } - abstract public function receiveMessage(PhabricatorIRCMessage $message); + abstract public function receiveMessage(PhabricatorBotMessage $message); public function runBackgroundTasks() { return; diff --git a/src/infrastructure/daemon/irc/handler/PhabricatorIRCLogHandler.php b/src/infrastructure/daemon/bot/handler/PhabricatorBotLogHandler.php similarity index 92% rename from src/infrastructure/daemon/irc/handler/PhabricatorIRCLogHandler.php rename to src/infrastructure/daemon/bot/handler/PhabricatorBotLogHandler.php index 5576aa8bb3..3e1bd620a8 100644 --- a/src/infrastructure/daemon/irc/handler/PhabricatorIRCLogHandler.php +++ b/src/infrastructure/daemon/bot/handler/PhabricatorBotLogHandler.php @@ -5,11 +5,11 @@ * * @group irc */ -final class PhabricatorIRCLogHandler extends PhabricatorIRCHandler { +final class PhabricatorBotLogHandler extends PhabricatorBotHandler { private $futures = array(); - public function receiveMessage(PhabricatorIRCMessage $message) { + public function receiveMessage(PhabricatorBotMessage $message) { switch ($message->getCommand()) { case 'PRIVMSG': diff --git a/src/infrastructure/daemon/irc/handler/PhabricatorIRCMacroHandler.php b/src/infrastructure/daemon/bot/handler/PhabricatorBotMacroHandler.php similarity index 96% rename from src/infrastructure/daemon/irc/handler/PhabricatorIRCMacroHandler.php rename to src/infrastructure/daemon/bot/handler/PhabricatorBotMacroHandler.php index 078d77ac31..59e698a04d 100644 --- a/src/infrastructure/daemon/irc/handler/PhabricatorIRCMacroHandler.php +++ b/src/infrastructure/daemon/bot/handler/PhabricatorBotMacroHandler.php @@ -3,7 +3,7 @@ /** * @group irc */ -final class PhabricatorIRCMacroHandler extends PhabricatorIRCHandler { +final class PhabricatorBotMacroHandler extends PhabricatorBotHandler { private $macros; private $regexp; @@ -40,7 +40,7 @@ final class PhabricatorIRCMacroHandler extends PhabricatorIRCHandler { return true; } - public function receiveMessage(PhabricatorIRCMessage $message) { + public function receiveMessage(PhabricatorBotMessage $message) { if (!$this->init()) { return; } diff --git a/src/infrastructure/daemon/irc/handler/PhabricatorIRCObjectNameHandler.php b/src/infrastructure/daemon/bot/handler/PhabricatorBotObjectNameHandler.php similarity index 97% rename from src/infrastructure/daemon/irc/handler/PhabricatorIRCObjectNameHandler.php rename to src/infrastructure/daemon/bot/handler/PhabricatorBotObjectNameHandler.php index ba551d1d33..7832880bb7 100644 --- a/src/infrastructure/daemon/irc/handler/PhabricatorIRCObjectNameHandler.php +++ b/src/infrastructure/daemon/bot/handler/PhabricatorBotObjectNameHandler.php @@ -5,7 +5,7 @@ * * @group irc */ -final class PhabricatorIRCObjectNameHandler extends PhabricatorIRCHandler { +final class PhabricatorBotObjectNameHandler extends PhabricatorBotHandler { /** * Map of PHIDs to the last mention of them (as an epoch timestamp); prevents @@ -13,7 +13,7 @@ final class PhabricatorIRCObjectNameHandler extends PhabricatorIRCHandler { */ private $recentlyMentioned = array(); - public function receiveMessage(PhabricatorIRCMessage $message) { + public function receiveMessage(PhabricatorBotMessage $message) { switch ($message->getCommand()) { case 'PRIVMSG': diff --git a/src/infrastructure/daemon/irc/handler/PhabricatorIRCSymbolHandler.php b/src/infrastructure/daemon/bot/handler/PhabricatorBotSymbolHandler.php similarity index 90% rename from src/infrastructure/daemon/irc/handler/PhabricatorIRCSymbolHandler.php rename to src/infrastructure/daemon/bot/handler/PhabricatorBotSymbolHandler.php index 8b4343d3ed..2e57de3418 100644 --- a/src/infrastructure/daemon/irc/handler/PhabricatorIRCSymbolHandler.php +++ b/src/infrastructure/daemon/bot/handler/PhabricatorBotSymbolHandler.php @@ -5,9 +5,9 @@ * * @group irc */ -final class PhabricatorIRCSymbolHandler extends PhabricatorIRCHandler { +final class PhabricatorBotSymbolHandler extends PhabricatorBotHandler { - public function receiveMessage(PhabricatorIRCMessage $message) { + public function receiveMessage(PhabricatorBotMessage $message) { switch ($message->getCommand()) { case 'PRIVMSG': diff --git a/src/infrastructure/daemon/irc/handler/PhabricatorIRCWhatsNewHandler.php b/src/infrastructure/daemon/bot/handler/PhabricatorBotWhatsNewHandler.php similarity index 96% rename from src/infrastructure/daemon/irc/handler/PhabricatorIRCWhatsNewHandler.php rename to src/infrastructure/daemon/bot/handler/PhabricatorBotWhatsNewHandler.php index d921dca89c..1bd53ef194 100644 --- a/src/infrastructure/daemon/irc/handler/PhabricatorIRCWhatsNewHandler.php +++ b/src/infrastructure/daemon/bot/handler/PhabricatorBotWhatsNewHandler.php @@ -5,11 +5,11 @@ * * @group irc */ -final class PhabricatorIRCWhatsNewHandler extends PhabricatorIRCHandler { +final class PhabricatorBotWhatsNewHandler extends PhabricatorBotHandler { private $floodblock = 0; - public function receiveMessage(PhabricatorIRCMessage $message) { + public function receiveMessage(PhabricatorBotMessage $message) { switch ($message->getCommand()) { case 'PRIVMSG': diff --git a/src/infrastructure/daemon/irc/handler/PhabricatorIRCProtocolHandler.php b/src/infrastructure/daemon/bot/handler/PhabricatorIRCProtocolHandler.php similarity index 85% rename from src/infrastructure/daemon/irc/handler/PhabricatorIRCProtocolHandler.php rename to src/infrastructure/daemon/bot/handler/PhabricatorIRCProtocolHandler.php index 1347d2bbdc..80b76077b1 100644 --- a/src/infrastructure/daemon/irc/handler/PhabricatorIRCProtocolHandler.php +++ b/src/infrastructure/daemon/bot/handler/PhabricatorIRCProtocolHandler.php @@ -5,9 +5,9 @@ * * @group irc */ -final class PhabricatorIRCProtocolHandler extends PhabricatorIRCHandler { +final class PhabricatorIRCProtocolHandler extends PhabricatorBotHandler { - public function receiveMessage(PhabricatorIRCMessage $message) { + public function receiveMessage(PhabricatorBotMessage $message) { switch ($message->getCommand()) { case '422': // Error - no MOTD case '376': // End of MOTD diff --git a/src/infrastructure/daemon/irc/PhabricatorIRCBot.php b/src/infrastructure/daemon/irc/PhabricatorIRCBot.php deleted file mode 100644 index a75352d589..0000000000 --- a/src/infrastructure/daemon/irc/PhabricatorIRCBot.php +++ /dev/null @@ -1,250 +0,0 @@ -getArgv(); - if (count($argv) !== 1) { - throw new Exception("usage: PhabricatorIRCBot "); - } - - $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!"); - } - - $server = idx($config, 'server'); - $port = idx($config, 'port', 6667); - $handlers = idx($config, 'handlers', array()); - $pass = idx($config, 'pass'); - $nick = idx($config, 'nick', 'phabot'); - $user = idx($config, 'user', $nick); - $ssl = idx($config, 'ssl', false); - $nickpass = idx($config, 'nickpass'); - - $this->config = $config; - - if (!preg_match('/^[A-Za-z0-9_`[{}^|\]\\-]+$/', $nick)) { - throw new Exception( - "Nickname '{$nick}' is invalid!"); - } - - 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' => 'PhabricatorIRCBot', - 'clientVersion' => '1.0', - 'clientDescription' => php_uname('n').':'.$nick, - 'user' => $conduit_user, - 'certificate' => $conduit_cert, - )); - - $this->conduit = $conduit; - } - - $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->writeCommand('USER', "{$user} 0 * :{$user}"); - if ($pass) { - $this->writeCommand('PASS', "{$pass}"); - } - - $this->writeCommand('NICK', "{$nick}"); - $this->runSelectLoop(); - } - - public function getConfig($key, $default = null) { - return idx($this->config, $key, $default); - } - - private function runSelectLoop() { - do { - $this->stillWorking(); - - $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 { - $this->debugLog(true, $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 { - $this->debugLog(false, substr($this->writeBuffer, 0, $len)); - $this->writeBuffer = substr($this->writeBuffer, $len); - } - } while (strlen($this->writeBuffer)); - } - - do { - $routed_message = $this->processReadBuffer(); - } while ($routed_message); - - foreach ($this->handlers as $handler) { - $handler->runBackgroundTasks(); - } - - } while (true); - } - - private function write($message) { - $this->writeBuffer .= $message; - return $this; - } - - public function writeCommand($command, $message) { - return $this->write($command.' '.$message."\r\n"); - } - - 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:(\S+)) )?'. // This may not be present. - '(?P[A-Z0-9]+) '. - '(?P.*)'. - '$/'; - - $matches = null; - if (!preg_match($pattern, $message, $matches)) { - throw new Exception("Unexpected message from server: {$message}"); - } - - $irc_message = new PhabricatorIRCMessage( - idx($matches, 'sender'), - $matches['command'], - $matches['data']); - - $this->routeMessage($irc_message); - - return true; - } - - private function routeMessage(PhabricatorIRCMessage $message) { - $ignore = $this->getConfig('ignore'); - if ($ignore && in_array($message->getSenderNickName(), $ignore)) { - return; - } - - foreach ($this->handlers as $handler) { - try { - $handler->receiveMessage($message); - } catch (Exception $ex) { - phlog($ex); - } - } - } - - public function __destruct() { - $this->write("QUIT Goodbye.\r\n"); - fclose($this->socket); - } - - private function debugLog($is_read, $message) { - if ($this->getTraceMode()) { - echo $is_read ? '<<< ' : '>>> '; - echo addcslashes($message, "\0..\37\177..\377"); - echo "\n"; - } - } - - 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; - } - -}