From 574bc3ba31cca2767bafe7844d7f854d90d6be1c Mon Sep 17 00:00:00 2001 From: indiefan Date: Tue, 5 Feb 2013 18:45:20 -0800 Subject: [PATCH] First pass at decoupling Phabricator bot behavior from the protocol it's running on, this pulls the connection, reading, and writing functionalities out of the bot itself and into the adapter. Summary: Ugh, just wrote out a huge message, only to lose it with a fat-fingered ctrl-c. Le sigh. First pass at decoupling the bot from the protocol. Noticeably absent is the command/message coupling. After this design pass I'll give that a go. Could use some advice, thinking that handlers should only create messages (which can be public or private) and not open ended, undefined 'commands'. The problem being that there needs to be some consistant api if we want handlers to be protocol agnostic. Perhaps that's a pipedream, what are your thoughts? Secondly, a few notes, design review requests on the changes i did make: # Config. For now i'm passing config through to the adapter. This was mainly to remain backwards compatible on the config. I was thinking it should probably be namespaced into it's own subobject though to distinguish the adapter config from the bot config. # Adapter selection. This flavor is the one-bot-daemon, config specified protocol version. The upside is that in the future they won't have to run different daemons for this stuff, just have different config, and the door is open for multiple protocol adapters down the road if need be. The downside is that I had to rename the daemon (non-backwards compatible change) and there will need to be some sort of runtime evaluation for instatiation of the adapter. For now I just have a crude switch, but I was thinking of just taking the string they supply as the class name (ala `try { new $clasName(); } catch...`) so as to allow for homegrown adapters, but I wasn't sure how such runtime magic would go over. Also, an alternative would be to make the PhabricatorBot class a non-abstract non-final base class and have the adapters be accompanied by a bot class that just defines their adapter as a property. The upside of which is backwards compatibility (welcome back PhabricatorIRCBot) and perhaps a little bit clearer plugin path for homegrowners. # Logging. You'll notice I commented out two very important logging lines in the irc adapter. This isn't intended to remain commented out, but I'm not sure what the best way is to get logging at this layer. I'm wary of just composing the daemon back down into the adapter (bi-directional object composition makes my skin crawl), but something needs to happen, obviously. Advice? That's it. After the feedback on the above, you can either merge down, or wait until i finish the command/message refactor if you don't think the diff will grow too large. Up to you, this all functions as is. Test Plan: Ran an irc bot, connected, read input, and wrote output including handler integration. Reviewers: epriestley Reviewed By: epriestley CC: aran, Korvin Maniphest Tasks: T2462 Differential Revision: https://secure.phabricator.com/D4757 --- resources/ircbot/example_config.json | 12 +- src/__phutil_library_map__.php | 45 ++-- .../daemon/bot/PhabricatorBot.php | 132 +++++++++ .../PhabricatorBotMessage.php} | 2 +- .../daemon/bot/PhabricatorIRCBot.php | 11 + .../PhabricatorBaseProtocolAdapter.php | 37 +++ .../adapter/PhabricatorIRCProtocolAdapter.php | 156 +++++++++++ .../handler/PhabricatorBotDebugLogHandler.php | 17 ++ ...torBotDifferentialNotificationHandler.php} | 6 +- ...PhabricatorBotFeedNotificationHandler.php} | 6 +- .../handler/PhabricatorBotHandler.php} | 8 +- .../handler/PhabricatorBotLogHandler.php} | 4 +- .../handler/PhabricatorBotMacroHandler.php} | 4 +- .../PhabricatorBotObjectNameHandler.php} | 4 +- .../handler/PhabricatorBotSymbolHandler.php} | 4 +- .../PhabricatorBotWhatsNewHandler.php} | 4 +- .../handler/PhabricatorIRCProtocolHandler.php | 4 +- .../daemon/irc/PhabricatorIRCBot.php | 250 ------------------ 18 files changed, 408 insertions(+), 298 deletions(-) create mode 100644 src/infrastructure/daemon/bot/PhabricatorBot.php rename src/infrastructure/daemon/{irc/PhabricatorIRCMessage.php => bot/PhabricatorBotMessage.php} (97%) create mode 100644 src/infrastructure/daemon/bot/PhabricatorIRCBot.php create mode 100644 src/infrastructure/daemon/bot/adapter/PhabricatorBaseProtocolAdapter.php create mode 100644 src/infrastructure/daemon/bot/adapter/PhabricatorIRCProtocolAdapter.php create mode 100644 src/infrastructure/daemon/bot/handler/PhabricatorBotDebugLogHandler.php rename src/infrastructure/daemon/{irc/handler/PhabricatorIRCDifferentialNotificationHandler.php => bot/handler/PhabricatorBotDifferentialNotificationHandler.php} (89%) rename src/infrastructure/daemon/{irc/handler/PhabricatorIRCFeedNotificationHandler.php => bot/handler/PhabricatorBotFeedNotificationHandler.php} (96%) rename src/infrastructure/daemon/{irc/handler/PhabricatorIRCHandler.php => bot/handler/PhabricatorBotHandler.php} (78%) rename src/infrastructure/daemon/{irc/handler/PhabricatorIRCLogHandler.php => bot/handler/PhabricatorBotLogHandler.php} (92%) rename src/infrastructure/daemon/{irc/handler/PhabricatorIRCMacroHandler.php => bot/handler/PhabricatorBotMacroHandler.php} (96%) rename src/infrastructure/daemon/{irc/handler/PhabricatorIRCObjectNameHandler.php => bot/handler/PhabricatorBotObjectNameHandler.php} (97%) rename src/infrastructure/daemon/{irc/handler/PhabricatorIRCSymbolHandler.php => bot/handler/PhabricatorBotSymbolHandler.php} (90%) rename src/infrastructure/daemon/{irc/handler/PhabricatorIRCWhatsNewHandler.php => bot/handler/PhabricatorBotWhatsNewHandler.php} (96%) rename src/infrastructure/daemon/{irc => bot}/handler/PhabricatorIRCProtocolHandler.php (85%) delete mode 100644 src/infrastructure/daemon/irc/PhabricatorIRCBot.php 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; - } - -}