@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Remove all "Phabricator Bot" code

Summary:
Closes T7829 as wontfix. Closes T7965 as wontfix. Closes T7800 as wontfix. Closes T2731 as wontfix. Closes T1271 as wontfix.

We aren't maintaining this at all (see, e.g., T7829) and a user reported a technically accurate security issue via HackerOne: <https://hackerone.com/reports/222870>

Just throw it away until we get to the eventual Conphernece bot/API update and can do this stuff correctly.

Test Plan: Grepped for `phabricatorbot`.

Reviewers: chad

Reviewed By: chad

Maniphest Tasks: T7965, T7829, T7800, T2731, T1271

Differential Revision: https://secure.phabricator.com/D17756

-1933
-36
src/__phutil_library_map__.php
··· 2089 2089 'PhabricatorBoardRenderingEngine' => 'applications/project/engine/PhabricatorBoardRenderingEngine.php', 2090 2090 'PhabricatorBoardResponseEngine' => 'applications/project/engine/PhabricatorBoardResponseEngine.php', 2091 2091 'PhabricatorBoolEditField' => 'applications/transactions/editfield/PhabricatorBoolEditField.php', 2092 - 'PhabricatorBot' => 'infrastructure/daemon/bot/PhabricatorBot.php', 2093 - 'PhabricatorBotChannel' => 'infrastructure/daemon/bot/target/PhabricatorBotChannel.php', 2094 - 'PhabricatorBotDebugLogHandler' => 'infrastructure/daemon/bot/handler/PhabricatorBotDebugLogHandler.php', 2095 - 'PhabricatorBotFeedNotificationHandler' => 'infrastructure/daemon/bot/handler/PhabricatorBotFeedNotificationHandler.php', 2096 - 'PhabricatorBotFlowdockProtocolAdapter' => 'infrastructure/daemon/bot/adapter/PhabricatorBotFlowdockProtocolAdapter.php', 2097 - 'PhabricatorBotHandler' => 'infrastructure/daemon/bot/handler/PhabricatorBotHandler.php', 2098 - 'PhabricatorBotLogHandler' => 'infrastructure/daemon/bot/handler/PhabricatorBotLogHandler.php', 2099 - 'PhabricatorBotMacroHandler' => 'infrastructure/daemon/bot/handler/PhabricatorBotMacroHandler.php', 2100 - 'PhabricatorBotMessage' => 'infrastructure/daemon/bot/PhabricatorBotMessage.php', 2101 - 'PhabricatorBotObjectNameHandler' => 'infrastructure/daemon/bot/handler/PhabricatorBotObjectNameHandler.php', 2102 - 'PhabricatorBotSymbolHandler' => 'infrastructure/daemon/bot/handler/PhabricatorBotSymbolHandler.php', 2103 - 'PhabricatorBotTarget' => 'infrastructure/daemon/bot/target/PhabricatorBotTarget.php', 2104 - 'PhabricatorBotUser' => 'infrastructure/daemon/bot/target/PhabricatorBotUser.php', 2105 - 'PhabricatorBotWhatsNewHandler' => 'infrastructure/daemon/bot/handler/PhabricatorBotWhatsNewHandler.php', 2106 2092 'PhabricatorBritishEnglishTranslation' => 'infrastructure/internationalization/translation/PhabricatorBritishEnglishTranslation.php', 2107 2093 'PhabricatorBuiltinDraftEngine' => 'applications/transactions/draft/PhabricatorBuiltinDraftEngine.php', 2108 2094 'PhabricatorBuiltinPatchList' => 'infrastructure/storage/patch/PhabricatorBuiltinPatchList.php', ··· 2261 2247 'PhabricatorCalendarRemarkupRule' => 'applications/calendar/remarkup/PhabricatorCalendarRemarkupRule.php', 2262 2248 'PhabricatorCalendarReplyHandler' => 'applications/calendar/mail/PhabricatorCalendarReplyHandler.php', 2263 2249 'PhabricatorCalendarSchemaSpec' => 'applications/calendar/storage/PhabricatorCalendarSchemaSpec.php', 2264 - 'PhabricatorCampfireProtocolAdapter' => 'infrastructure/daemon/bot/adapter/PhabricatorCampfireProtocolAdapter.php', 2265 2250 'PhabricatorCelerityApplication' => 'applications/celerity/application/PhabricatorCelerityApplication.php', 2266 2251 'PhabricatorCelerityTestCase' => '__tests__/PhabricatorCelerityTestCase.php', 2267 2252 'PhabricatorChangeParserTestCase' => 'applications/repository/worker/__tests__/PhabricatorChangeParserTestCase.php', ··· 2921 2906 'PhabricatorHovercardEngineExtensionModule' => 'applications/search/engineextension/PhabricatorHovercardEngineExtensionModule.php', 2922 2907 'PhabricatorIDsSearchEngineExtension' => 'applications/search/engineextension/PhabricatorIDsSearchEngineExtension.php', 2923 2908 'PhabricatorIDsSearchField' => 'applications/search/field/PhabricatorIDsSearchField.php', 2924 - 'PhabricatorIRCProtocolAdapter' => 'infrastructure/daemon/bot/adapter/PhabricatorIRCProtocolAdapter.php', 2925 2909 'PhabricatorIconDatasource' => 'applications/files/typeahead/PhabricatorIconDatasource.php', 2926 2910 'PhabricatorIconRemarkupRule' => 'applications/macro/markup/PhabricatorIconRemarkupRule.php', 2927 2911 'PhabricatorIconSet' => 'applications/files/iconset/PhabricatorIconSet.php', ··· 3654 3638 'PhabricatorProjectsSearchEngineExtension' => 'applications/project/engineextension/PhabricatorProjectsSearchEngineExtension.php', 3655 3639 'PhabricatorProjectsWatchersSearchEngineAttachment' => 'applications/project/engineextension/PhabricatorProjectsWatchersSearchEngineAttachment.php', 3656 3640 'PhabricatorPronounSetting' => 'applications/settings/setting/PhabricatorPronounSetting.php', 3657 - 'PhabricatorProtocolAdapter' => 'infrastructure/daemon/bot/adapter/PhabricatorProtocolAdapter.php', 3658 3641 'PhabricatorPygmentSetupCheck' => 'applications/config/check/PhabricatorPygmentSetupCheck.php', 3659 3642 'PhabricatorQuery' => 'infrastructure/query/PhabricatorQuery.php', 3660 3643 'PhabricatorQueryConstraint' => 'infrastructure/query/constraint/PhabricatorQueryConstraint.php', ··· 3978 3961 'PhabricatorStoragePatch' => 'infrastructure/storage/management/PhabricatorStoragePatch.php', 3979 3962 'PhabricatorStorageSchemaSpec' => 'infrastructure/storage/schema/PhabricatorStorageSchemaSpec.php', 3980 3963 'PhabricatorStorageSetupCheck' => 'applications/config/check/PhabricatorStorageSetupCheck.php', 3981 - 'PhabricatorStreamingProtocolAdapter' => 'infrastructure/daemon/bot/adapter/PhabricatorStreamingProtocolAdapter.php', 3982 3964 'PhabricatorStringListEditField' => 'applications/transactions/editfield/PhabricatorStringListEditField.php', 3983 3965 'PhabricatorStringSetting' => 'applications/settings/setting/PhabricatorStringSetting.php', 3984 3966 'PhabricatorSubmitEditField' => 'applications/transactions/editfield/PhabricatorSubmitEditField.php', ··· 7150 7132 'PhabricatorBoardRenderingEngine' => 'Phobject', 7151 7133 'PhabricatorBoardResponseEngine' => 'Phobject', 7152 7134 'PhabricatorBoolEditField' => 'PhabricatorEditField', 7153 - 'PhabricatorBot' => 'PhabricatorDaemon', 7154 - 'PhabricatorBotChannel' => 'PhabricatorBotTarget', 7155 - 'PhabricatorBotDebugLogHandler' => 'PhabricatorBotHandler', 7156 - 'PhabricatorBotFeedNotificationHandler' => 'PhabricatorBotHandler', 7157 - 'PhabricatorBotFlowdockProtocolAdapter' => 'PhabricatorStreamingProtocolAdapter', 7158 - 'PhabricatorBotHandler' => 'Phobject', 7159 - 'PhabricatorBotLogHandler' => 'PhabricatorBotHandler', 7160 - 'PhabricatorBotMacroHandler' => 'PhabricatorBotHandler', 7161 - 'PhabricatorBotMessage' => 'Phobject', 7162 - 'PhabricatorBotObjectNameHandler' => 'PhabricatorBotHandler', 7163 - 'PhabricatorBotSymbolHandler' => 'PhabricatorBotHandler', 7164 - 'PhabricatorBotTarget' => 'Phobject', 7165 - 'PhabricatorBotUser' => 'PhabricatorBotTarget', 7166 - 'PhabricatorBotWhatsNewHandler' => 'PhabricatorBotHandler', 7167 7135 'PhabricatorBritishEnglishTranslation' => 'PhutilTranslation', 7168 7136 'PhabricatorBuiltinDraftEngine' => 'PhabricatorDraftEngine', 7169 7137 'PhabricatorBuiltinPatchList' => 'PhabricatorSQLPatchList', ··· 7358 7326 'PhabricatorCalendarRemarkupRule' => 'PhabricatorObjectRemarkupRule', 7359 7327 'PhabricatorCalendarReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler', 7360 7328 'PhabricatorCalendarSchemaSpec' => 'PhabricatorConfigSchemaSpec', 7361 - 'PhabricatorCampfireProtocolAdapter' => 'PhabricatorStreamingProtocolAdapter', 7362 7329 'PhabricatorCelerityApplication' => 'PhabricatorApplication', 7363 7330 'PhabricatorCelerityTestCase' => 'PhabricatorTestCase', 7364 7331 'PhabricatorChangeParserTestCase' => 'PhabricatorWorkingCopyTestCase', ··· 8115 8082 'PhabricatorHovercardEngineExtensionModule' => 'PhabricatorConfigModule', 8116 8083 'PhabricatorIDsSearchEngineExtension' => 'PhabricatorSearchEngineExtension', 8117 8084 'PhabricatorIDsSearchField' => 'PhabricatorSearchField', 8118 - 'PhabricatorIRCProtocolAdapter' => 'PhabricatorProtocolAdapter', 8119 8085 'PhabricatorIconDatasource' => 'PhabricatorTypeaheadDatasource', 8120 8086 'PhabricatorIconRemarkupRule' => 'PhutilRemarkupRule', 8121 8087 'PhabricatorIconSet' => 'Phobject', ··· 8973 8939 'PhabricatorProjectsSearchEngineExtension' => 'PhabricatorSearchEngineExtension', 8974 8940 'PhabricatorProjectsWatchersSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment', 8975 8941 'PhabricatorPronounSetting' => 'PhabricatorSelectSetting', 8976 - 'PhabricatorProtocolAdapter' => 'Phobject', 8977 8942 'PhabricatorPygmentSetupCheck' => 'PhabricatorSetupCheck', 8978 8943 'PhabricatorQuery' => 'Phobject', 8979 8944 'PhabricatorQueryConstraint' => 'Phobject', ··· 9377 9342 'PhabricatorStoragePatch' => 'Phobject', 9378 9343 'PhabricatorStorageSchemaSpec' => 'PhabricatorConfigSchemaSpec', 9379 9344 'PhabricatorStorageSetupCheck' => 'PhabricatorSetupCheck', 9380 - 'PhabricatorStreamingProtocolAdapter' => 'PhabricatorProtocolAdapter', 9381 9345 'PhabricatorStringListEditField' => 'PhabricatorEditField', 9382 9346 'PhabricatorStringSetting' => 'PhabricatorSetting', 9383 9347 'PhabricatorSubmitEditField' => 'PhabricatorEditField',
-87
src/docs/tech/chatbot.diviner
··· 1 - @title Chat Bot Technical Documentation 2 - @group bot 3 - 4 - Configuring and extending the chat bot. 5 - 6 - = Overview = 7 - 8 - Phabricator includes a simple chat bot daemon, which is primarily intended as 9 - an example of how you can write an external script that interfaces with 10 - Phabricator over Conduit and does some kind of useful work. If you use IRC or 11 - another supported chat protocol, you can also have the bot hang out in your 12 - channel. 13 - 14 - NOTE: The chat bot is somewhat experimental and not very mature. 15 - 16 - = Configuring the Bot = 17 - 18 - The bot reads a JSON configuration file. You can find an example in: 19 - 20 - resources/chatbot/example_config.json 21 - 22 - These are the configuration values it reads: 23 - 24 - - `server` String, required, the server to connect to. 25 - - `port` Int, optional, the port to connect to (defaults to 6667). 26 - - `ssl` Bool, optional, whether to connect via SSL or not (defaults to 27 - false). 28 - - `nick` String, nickname to use. 29 - - `user` String, optional, username to use (defaults to `nick`). 30 - - `pass` String, optional, password for server. 31 - - `nickpass` String, optional, password for NickServ. 32 - - `join` Array, list of channels to join. 33 - - `handlers` Array, list of handlers to run. These are like plugins for the 34 - bot. 35 - - `conduit.uri`, `conduit.token` Conduit configuration, 36 - see below. 37 - - `notification.channels` Notification configuration, see below. 38 - 39 - = Handlers = 40 - 41 - You specify a list of "handlers", which are basically plugins or modules for 42 - the bot. These are the default handlers available: 43 - 44 - - @{class:PhabricatorBotObjectNameHandler} This handler looks for users 45 - mentioning Phabricator objects like "T123" and "D345" in chat, looks them 46 - up, and says their name with a link to the object. Requires conduit. 47 - - @{class:PhabricatorBotFeedNotificationHandler} This handler posts 48 - notifications about changes to revisions to the channels listed in 49 - `notification.channels`. 50 - - @{class:PhabricatorBotLogHandler} This handler records chatlogs which can 51 - be browsed in the Phabricator web interface. 52 - - @{class:PhabricatorBotSymbolHandler} This handler posts responses to lookups 53 - for symbols in Diffusion 54 - - @{class:PhabricatorBotMacroHandler} This handler looks for users mentioning 55 - macros, if found will convert image to ASCII and output in chat. Configure 56 - with `macro.size` and `macro.aspect` 57 - 58 - You can also write your own handlers, by extending 59 - @{class:PhabricatorBotHandler}. 60 - 61 - = Conduit = 62 - 63 - Some handlers (e.g., @{class:PhabricatorBotObjectNameHandler}) need to read data 64 - from Phabricator over Conduit, Phabricator's HTTP API. You can use this method 65 - to allow other scripts or programs to access Phabricator's data from different 66 - servers and in different languages. 67 - 68 - To allow the bot to access Conduit, you need to create a user that it can login 69 - with. To do this, login to Phabricator as an administrator and go to 70 - `People -> Create New Account`. Create a new account and flag them as a 71 - "Bot/Script". Then in your configuration file, set these parameters: 72 - 73 - - `conduit.uri` The URI for your Phabricator install, like 74 - `http://phabricator.example.com/` 75 - - `conduit.token` The user's conduit API token, from the "Conduit API Tokens" 76 - tab in the user's administrative view. 77 - 78 - Now the bot should be able to connect to Phabricator via Conduit. 79 - 80 - = Starting the Bot = 81 - 82 - The bot is a Phabricator daemon, so start it with `phd`: 83 - 84 - ./bin/phd launch phabricatorbot <absolute_path_to_config_file> 85 - 86 - If you have issues you can try `debug` instead of `launch`, see 87 - @{article:Managing Daemons with phd} for more information.
-170
src/infrastructure/daemon/bot/PhabricatorBot.php
··· 1 - <?php 2 - 3 - /** 4 - * Simple IRC bot which runs as a Phabricator daemon. Although this bot is 5 - * somewhat useful, it is also intended to serve as a demo of how to write 6 - * "system agents" which communicate with Phabricator over Conduit, so you can 7 - * script system interactions and integrate with other systems. 8 - * 9 - * NOTE: This is super janky and experimental right now. 10 - */ 11 - final class PhabricatorBot extends PhabricatorDaemon { 12 - 13 - private $handlers; 14 - 15 - private $conduit; 16 - private $config; 17 - private $pollFrequency; 18 - private $protocolAdapter; 19 - 20 - protected function run() { 21 - $argv = $this->getArgv(); 22 - if (count($argv) !== 1) { 23 - throw new Exception( 24 - pht( 25 - 'Usage: %s %s', 26 - __CLASS__, 27 - '<json_config_file>')); 28 - } 29 - 30 - $json_raw = Filesystem::readFile($argv[0]); 31 - try { 32 - $config = phutil_json_decode($json_raw); 33 - } catch (PhutilJSONParserException $ex) { 34 - throw new PhutilProxyException( 35 - pht("File '%s' is not valid JSON!", $argv[0]), 36 - $ex); 37 - } 38 - 39 - $nick = idx($config, 'nick', 'phabot'); 40 - $handlers = idx($config, 'handlers', array()); 41 - $protocol_adapter_class = idx( 42 - $config, 43 - 'protocol-adapter', 44 - 'PhabricatorIRCProtocolAdapter'); 45 - $this->pollFrequency = idx($config, 'poll-frequency', 1); 46 - 47 - $this->config = $config; 48 - 49 - foreach ($handlers as $handler) { 50 - $obj = newv($handler, array($this)); 51 - $this->handlers[] = $obj; 52 - } 53 - 54 - $ca_bundle = idx($config, 'https.cabundle'); 55 - if ($ca_bundle) { 56 - HTTPSFuture::setGlobalCABundleFromPath($ca_bundle); 57 - } 58 - 59 - $conduit_uri = idx($config, 'conduit.uri'); 60 - if ($conduit_uri) { 61 - $conduit_token = idx($config, 'conduit.token'); 62 - 63 - // Normalize the path component of the URI so users can enter the 64 - // domain without the "/api/" part. 65 - $conduit_uri = new PhutilURI($conduit_uri); 66 - 67 - $conduit_host = (string)$conduit_uri->setPath('/'); 68 - $conduit_uri = (string)$conduit_uri->setPath('/api/'); 69 - 70 - $conduit = new ConduitClient($conduit_uri); 71 - if ($conduit_token) { 72 - $conduit->setConduitToken($conduit_token); 73 - } else { 74 - $conduit_user = idx($config, 'conduit.user'); 75 - $conduit_cert = idx($config, 'conduit.cert'); 76 - 77 - $response = $conduit->callMethodSynchronous( 78 - 'conduit.connect', 79 - array( 80 - 'client' => __CLASS__, 81 - 'clientVersion' => '1.0', 82 - 'clientDescription' => php_uname('n').':'.$nick, 83 - 'host' => $conduit_host, 84 - 'user' => $conduit_user, 85 - 'certificate' => $conduit_cert, 86 - )); 87 - } 88 - 89 - $this->conduit = $conduit; 90 - } 91 - 92 - // Instantiate Protocol Adapter, for now follow same technique as 93 - // handler instantiation 94 - $this->protocolAdapter = newv($protocol_adapter_class, array()); 95 - $this->protocolAdapter 96 - ->setConfig($this->config) 97 - ->connect(); 98 - 99 - $this->runLoop(); 100 - 101 - $this->protocolAdapter->disconnect(); 102 - } 103 - 104 - public function getConfig($key, $default = null) { 105 - return idx($this->config, $key, $default); 106 - } 107 - 108 - private function runLoop() { 109 - do { 110 - PhabricatorCaches::destroyRequestCache(); 111 - 112 - $this->stillWorking(); 113 - 114 - $messages = $this->protocolAdapter->getNextMessages($this->pollFrequency); 115 - if (count($messages) > 0) { 116 - foreach ($messages as $message) { 117 - $this->routeMessage($message); 118 - } 119 - } 120 - 121 - foreach ($this->handlers as $handler) { 122 - $handler->runBackgroundTasks(); 123 - } 124 - } while (!$this->shouldExit()); 125 - 126 - } 127 - 128 - public function writeMessage(PhabricatorBotMessage $message) { 129 - return $this->protocolAdapter->writeMessage($message); 130 - } 131 - 132 - private function routeMessage(PhabricatorBotMessage $message) { 133 - $ignore = $this->getConfig('ignore'); 134 - if ($ignore) { 135 - $sender = $message->getSender(); 136 - if ($sender && in_array($sender->getName(), $ignore)) { 137 - return; 138 - } 139 - } 140 - 141 - if ($message->getCommand() == 'LOG') { 142 - $this->log('[LOG] '.$message->getBody()); 143 - } 144 - 145 - foreach ($this->handlers as $handler) { 146 - try { 147 - $handler->receiveMessage($message); 148 - } catch (Exception $ex) { 149 - phlog($ex); 150 - } 151 - } 152 - } 153 - 154 - public function getAdapter() { 155 - return $this->protocolAdapter; 156 - } 157 - 158 - public function getConduit() { 159 - if (empty($this->conduit)) { 160 - throw new Exception( 161 - pht( 162 - "This bot is not configured with a Conduit uplink. Set '%s' and ". 163 - "'%s' in the configuration to connect.", 164 - 'conduit.uri', 165 - 'conduit.token')); 166 - } 167 - return $this->conduit; 168 - } 169 - 170 - }
-52
src/infrastructure/daemon/bot/PhabricatorBotMessage.php
··· 1 - <?php 2 - 3 - final class PhabricatorBotMessage extends Phobject { 4 - 5 - private $sender; 6 - private $command; 7 - private $body; 8 - private $target; 9 - private $public; 10 - 11 - public function __construct() { 12 - // By default messages are public 13 - $this->public = true; 14 - } 15 - 16 - public function setSender(PhabricatorBotTarget $sender = null) { 17 - $this->sender = $sender; 18 - return $this; 19 - } 20 - 21 - public function getSender() { 22 - return $this->sender; 23 - } 24 - 25 - public function setCommand($command) { 26 - $this->command = $command; 27 - return $this; 28 - } 29 - 30 - public function getCommand() { 31 - return $this->command; 32 - } 33 - 34 - public function setBody($body) { 35 - $this->body = $body; 36 - return $this; 37 - } 38 - 39 - public function getBody() { 40 - return $this->body; 41 - } 42 - 43 - public function setTarget(PhabricatorBotTarget $target = null) { 44 - $this->target = $target; 45 - return $this; 46 - } 47 - 48 - public function getTarget() { 49 - return $this->target; 50 - } 51 - 52 - }
-93
src/infrastructure/daemon/bot/adapter/PhabricatorBotFlowdockProtocolAdapter.php
··· 1 - <?php 2 - 3 - final class PhabricatorBotFlowdockProtocolAdapter 4 - extends PhabricatorStreamingProtocolAdapter { 5 - 6 - public function getServiceType() { 7 - return 'Flowdock'; 8 - } 9 - 10 - protected function buildStreamingUrl($channel) { 11 - $organization = $this->getConfig('flowdock.organization'); 12 - if (empty($organization)) { 13 - $this->getConfig('organization'); 14 - } 15 - if (empty($organization)) { 16 - throw new Exception( 17 - '"flowdock.organization" configuration variable not set'); 18 - } 19 - 20 - 21 - $ssl = $this->getConfig('ssl'); 22 - 23 - $url = ($ssl) ? 'https://' : 'http://'; 24 - $url .= "{$this->authtoken}@stream.flowdock.com"; 25 - $url .= "/flows/{$organization}/{$channel}"; 26 - return $url; 27 - } 28 - 29 - protected function processMessage(array $m_obj) { 30 - $command = null; 31 - switch ($m_obj['event']) { 32 - case 'message': 33 - $command = 'MESSAGE'; 34 - break; 35 - default: 36 - // For now, ignore anything which we don't otherwise know about. 37 - break; 38 - } 39 - 40 - if ($command === null) { 41 - return false; 42 - } 43 - 44 - // TODO: These should be usernames, not user IDs. 45 - $sender = id(new PhabricatorBotUser()) 46 - ->setName($m_obj['user']); 47 - 48 - $target = id(new PhabricatorBotChannel()) 49 - ->setName($m_obj['flow']); 50 - 51 - return id(new PhabricatorBotMessage()) 52 - ->setCommand($command) 53 - ->setSender($sender) 54 - ->setTarget($target) 55 - ->setBody($m_obj['content']); 56 - } 57 - 58 - public function writeMessage(PhabricatorBotMessage $message) { 59 - switch ($message->getCommand()) { 60 - case 'MESSAGE': 61 - $this->speak( 62 - $message->getBody(), 63 - $message->getTarget()); 64 - break; 65 - } 66 - } 67 - 68 - private function speak( 69 - $body, 70 - PhabricatorBotTarget $flow) { 71 - // The $flow->getName() returns the flow's UUID, 72 - // as such, the Flowdock API does not require the organization 73 - // to be specified in the URI 74 - $this->performPost( 75 - '/messages', 76 - array( 77 - 'flow' => $flow->getName(), 78 - 'event' => 'message', 79 - 'content' => $body, 80 - )); 81 - } 82 - 83 - public function __destruct() { 84 - if ($this->readHandles) { 85 - foreach ($this->readHandles as $read_handle) { 86 - curl_multi_remove_handle($this->multiHandle, $read_handle); 87 - curl_close($read_handle); 88 - } 89 - } 90 - 91 - curl_multi_close($this->multiHandle); 92 - } 93 - }
-114
src/infrastructure/daemon/bot/adapter/PhabricatorCampfireProtocolAdapter.php
··· 1 - <?php 2 - 3 - final class PhabricatorCampfireProtocolAdapter 4 - extends PhabricatorStreamingProtocolAdapter { 5 - 6 - public function getServiceType() { 7 - return 'Campfire'; 8 - } 9 - 10 - protected function buildStreamingUrl($channel) { 11 - $ssl = $this->getConfig('ssl'); 12 - 13 - $url = ($ssl) ? 'https://' : 'http://'; 14 - $url .= "streaming.campfirenow.com/room/{$channel}/live.json"; 15 - 16 - return $url; 17 - } 18 - 19 - protected function processMessage(array $m_obj) { 20 - $command = null; 21 - switch ($m_obj['type']) { 22 - case 'TextMessage': 23 - $command = 'MESSAGE'; 24 - break; 25 - case 'PasteMessage': 26 - $command = 'PASTE'; 27 - break; 28 - default: 29 - // For now, ignore anything which we don't otherwise know about. 30 - break; 31 - } 32 - 33 - if ($command === null) { 34 - return false; 35 - } 36 - 37 - // TODO: These should be usernames, not user IDs. 38 - $sender = id(new PhabricatorBotUser()) 39 - ->setName($m_obj['user_id']); 40 - 41 - $target = id(new PhabricatorBotChannel()) 42 - ->setName($m_obj['room_id']); 43 - 44 - return id(new PhabricatorBotMessage()) 45 - ->setCommand($command) 46 - ->setSender($sender) 47 - ->setTarget($target) 48 - ->setBody($m_obj['body']); 49 - } 50 - 51 - public function writeMessage(PhabricatorBotMessage $message) { 52 - switch ($message->getCommand()) { 53 - case 'MESSAGE': 54 - $this->speak( 55 - $message->getBody(), 56 - $message->getTarget()); 57 - break; 58 - case 'SOUND': 59 - $this->speak( 60 - $message->getBody(), 61 - $message->getTarget(), 62 - 'SoundMessage'); 63 - break; 64 - case 'PASTE': 65 - $this->speak( 66 - $message->getBody(), 67 - $message->getTarget(), 68 - 'PasteMessage'); 69 - break; 70 - } 71 - } 72 - 73 - protected function joinRoom($room_id) { 74 - $this->performPost("/room/{$room_id}/join.json"); 75 - $this->inRooms[$room_id] = true; 76 - } 77 - 78 - private function leaveRoom($room_id) { 79 - $this->performPost("/room/{$room_id}/leave.json"); 80 - unset($this->inRooms[$room_id]); 81 - } 82 - 83 - private function speak( 84 - $message, 85 - PhabricatorBotTarget $channel, 86 - $type = 'TextMessage') { 87 - 88 - $room_id = $channel->getName(); 89 - 90 - $this->performPost( 91 - "/room/{$room_id}/speak.json", 92 - array( 93 - 'message' => array( 94 - 'type' => $type, 95 - 'body' => $message, 96 - ), 97 - )); 98 - } 99 - 100 - public function __destruct() { 101 - foreach ($this->inRooms as $room_id => $ignored) { 102 - $this->leaveRoom($room_id); 103 - } 104 - 105 - if ($this->readHandles) { 106 - foreach ($this->readHandles as $read_handle) { 107 - curl_multi_remove_handle($this->multiHandle, $read_handle); 108 - curl_close($read_handle); 109 - } 110 - } 111 - 112 - curl_multi_close($this->multiHandle); 113 - } 114 - }
-282
src/infrastructure/daemon/bot/adapter/PhabricatorIRCProtocolAdapter.php
··· 1 - <?php 2 - 3 - final class PhabricatorIRCProtocolAdapter extends PhabricatorProtocolAdapter { 4 - 5 - private $socket; 6 - 7 - private $writeBuffer; 8 - private $readBuffer; 9 - 10 - private $nickIncrement = 0; 11 - 12 - public function getServiceType() { 13 - return 'IRC'; 14 - } 15 - 16 - public function getServiceName() { 17 - return $this->getConfig('network', $this->getConfig('server')); 18 - } 19 - 20 - // Hash map of command translations 21 - public static $commandTranslations = array( 22 - 'PRIVMSG' => 'MESSAGE', 23 - ); 24 - 25 - public function connect() { 26 - $nick = $this->getConfig('nick', 'phabot'); 27 - $server = $this->getConfig('server'); 28 - $port = $this->getConfig('port', 6667); 29 - $pass = $this->getConfig('pass'); 30 - $ssl = $this->getConfig('ssl', false); 31 - $user = $this->getConfig('user', $nick); 32 - 33 - if (!preg_match('/^[A-Za-z0-9_`[{}^|\]\\-]+$/', $nick)) { 34 - throw new Exception( 35 - pht( 36 - "Nickname '%s' is invalid!", 37 - $nick)); 38 - } 39 - 40 - $errno = null; 41 - $error = null; 42 - if (!$ssl) { 43 - $socket = fsockopen($server, $port, $errno, $error); 44 - } else { 45 - $socket = fsockopen('ssl://'.$server, $port, $errno, $error); 46 - } 47 - if (!$socket) { 48 - throw new Exception(pht('Failed to connect, #%d: %s', $errno, $error)); 49 - } 50 - $ok = stream_set_blocking($socket, false); 51 - if (!$ok) { 52 - throw new Exception(pht('Failed to set stream nonblocking.')); 53 - } 54 - 55 - $this->socket = $socket; 56 - if ($pass) { 57 - $this->write("PASS {$pass}"); 58 - } 59 - $this->write("NICK {$nick}"); 60 - $this->write("USER {$user} 0 * :{$user}"); 61 - } 62 - 63 - public function getNextMessages($poll_frequency) { 64 - $messages = array(); 65 - 66 - $read = array($this->socket); 67 - if (strlen($this->writeBuffer)) { 68 - $write = array($this->socket); 69 - } else { 70 - $write = array(); 71 - } 72 - $except = array(); 73 - 74 - $ok = @stream_select($read, $write, $except, $timeout_sec = 1); 75 - if ($ok === false) { 76 - // We may have been interrupted by a signal, like a SIGINT. Try 77 - // selecting again. If the second select works, conclude that the failure 78 - // was most likely because we were signaled. 79 - $ok = @stream_select($read, $write, $except, $timeout_sec = 0); 80 - if ($ok === false) { 81 - throw new Exception(pht('%s failed!', 'stream_select()')); 82 - } 83 - } 84 - 85 - if ($read) { 86 - // Test for connection termination; in PHP, fread() off a nonblocking, 87 - // closed socket is empty string. 88 - if (feof($this->socket)) { 89 - // This indicates the connection was terminated on the other side, 90 - // just exit via exception and let the overseer restart us after a 91 - // delay so we can reconnect. 92 - throw new Exception(pht('Remote host closed connection.')); 93 - } 94 - do { 95 - $data = fread($this->socket, 4096); 96 - if ($data === false) { 97 - throw new Exception(pht('%s failed!', 'fread()')); 98 - } else { 99 - $messages[] = id(new PhabricatorBotMessage()) 100 - ->setCommand('LOG') 101 - ->setBody('>>> '.$data); 102 - $this->readBuffer .= $data; 103 - } 104 - } while (strlen($data)); 105 - } 106 - 107 - if ($write) { 108 - do { 109 - $len = fwrite($this->socket, $this->writeBuffer); 110 - if ($len === false) { 111 - throw new Exception(pht('%s failed!', 'fwrite()')); 112 - } else if ($len === 0) { 113 - break; 114 - } else { 115 - $messages[] = id(new PhabricatorBotMessage()) 116 - ->setCommand('LOG') 117 - ->setBody('>>> '.substr($this->writeBuffer, 0, $len)); 118 - $this->writeBuffer = substr($this->writeBuffer, $len); 119 - } 120 - } while (strlen($this->writeBuffer)); 121 - } 122 - 123 - while (($m = $this->processReadBuffer()) !== false) { 124 - if ($m !== null) { 125 - $messages[] = $m; 126 - } 127 - } 128 - 129 - return $messages; 130 - } 131 - 132 - private function write($message) { 133 - $this->writeBuffer .= $message."\r\n"; 134 - return $this; 135 - } 136 - 137 - public function writeMessage(PhabricatorBotMessage $message) { 138 - switch ($message->getCommand()) { 139 - case 'MESSAGE': 140 - case 'PASTE': 141 - $name = $message->getTarget()->getName(); 142 - $body = $message->getBody(); 143 - $this->write("PRIVMSG {$name} :{$body}"); 144 - return true; 145 - default: 146 - return false; 147 - } 148 - } 149 - 150 - private function processReadBuffer() { 151 - $until = strpos($this->readBuffer, "\r\n"); 152 - if ($until === false) { 153 - return false; 154 - } 155 - 156 - $message = substr($this->readBuffer, 0, $until); 157 - $this->readBuffer = substr($this->readBuffer, $until + 2); 158 - 159 - $pattern = 160 - '/^'. 161 - '(?::(?P<sender>(\S+?))(?:!\S*)? )?'. // This may not be present. 162 - '(?P<command>[A-Z0-9]+) '. 163 - '(?P<data>.*)'. 164 - '$/'; 165 - 166 - $matches = null; 167 - if (!preg_match($pattern, $message, $matches)) { 168 - throw new Exception("Unexpected message from server: {$message}"); 169 - } 170 - 171 - if ($this->handleIRCProtocol($matches)) { 172 - return null; 173 - } 174 - 175 - $command = $this->getBotCommand($matches['command']); 176 - list($target, $body) = $this->parseMessageData($command, $matches['data']); 177 - 178 - if (!strlen($matches['sender'])) { 179 - $sender = null; 180 - } else { 181 - $sender = id(new PhabricatorBotUser()) 182 - ->setName($matches['sender']); 183 - } 184 - 185 - $bot_message = id(new PhabricatorBotMessage()) 186 - ->setSender($sender) 187 - ->setCommand($command) 188 - ->setTarget($target) 189 - ->setBody($body); 190 - 191 - return $bot_message; 192 - } 193 - 194 - private function handleIRCProtocol(array $matches) { 195 - $data = $matches['data']; 196 - switch ($matches['command']) { 197 - case '433': // Nickname already in use 198 - // If we receive this error, try appending "-1", "-2", etc. to the nick 199 - $this->nickIncrement++; 200 - $nick = $this->getConfig('nick', 'phabot').'-'.$this->nickIncrement; 201 - $this->write("NICK {$nick}"); 202 - return true; 203 - case '422': // Error - no MOTD 204 - case '376': // End of MOTD 205 - $nickpass = $this->getConfig('nickpass'); 206 - if ($nickpass) { 207 - $this->write("PRIVMSG nickserv :IDENTIFY {$nickpass}"); 208 - } 209 - $join = $this->getConfig('join'); 210 - if (!$join) { 211 - throw new Exception(pht('Not configured to join any channels!')); 212 - } 213 - foreach ($join as $channel) { 214 - $this->write("JOIN {$channel}"); 215 - } 216 - return true; 217 - case 'PING': 218 - $this->write("PONG {$data}"); 219 - return true; 220 - } 221 - 222 - return false; 223 - } 224 - 225 - private function getBotCommand($irc_command) { 226 - if (isset(self::$commandTranslations[$irc_command])) { 227 - return self::$commandTranslations[$irc_command]; 228 - } 229 - 230 - // We have no translation for this command, use as-is 231 - return $irc_command; 232 - } 233 - 234 - private function parseMessageData($command, $data) { 235 - switch ($command) { 236 - case 'MESSAGE': 237 - $matches = null; 238 - if (preg_match('/^(\S+)\s+:?(.*)$/', $data, $matches)) { 239 - 240 - $target_name = $matches[1]; 241 - if (strncmp($target_name, '#', 1) === 0) { 242 - $target = id(new PhabricatorBotChannel()) 243 - ->setName($target_name); 244 - } else { 245 - $target = id(new PhabricatorBotUser()) 246 - ->setName($target_name); 247 - } 248 - 249 - return array( 250 - $target, 251 - rtrim($matches[2], "\r\n"), 252 - ); 253 - } 254 - break; 255 - } 256 - 257 - // By default we assume there is no target, only a body 258 - return array( 259 - null, 260 - $data, 261 - ); 262 - } 263 - 264 - public function disconnect() { 265 - // NOTE: FreeNode doesn't show quit messages if you've recently joined a 266 - // channel, presumably to prevent some kind of abuse. If you're testing 267 - // this, you may need to stay connected to the network for a few minutes 268 - // before it works. If you disconnect too quickly, the server will replace 269 - // your message with a "Client Quit" message. 270 - 271 - $quit = $this->getConfig('quit', pht('Shutting down.')); 272 - $this->write("QUIT :{$quit}"); 273 - 274 - // Flush the write buffer. 275 - while (strlen($this->writeBuffer)) { 276 - $this->getNextMessages(0); 277 - } 278 - 279 - @fclose($this->socket); 280 - $this->socket = null; 281 - } 282 - }
-62
src/infrastructure/daemon/bot/adapter/PhabricatorProtocolAdapter.php
··· 1 - <?php 2 - 3 - /** 4 - * Defines the api for protocol adapters for @{class:PhabricatorBot} 5 - */ 6 - abstract class PhabricatorProtocolAdapter extends Phobject { 7 - 8 - private $config; 9 - 10 - public function setConfig($config) { 11 - $this->config = $config; 12 - return $this; 13 - } 14 - 15 - public function getConfig($key, $default = null) { 16 - return idx($this->config, $key, $default); 17 - } 18 - 19 - /** 20 - * Performs any connection logic necessary for the protocol 21 - */ 22 - abstract public function connect(); 23 - 24 - /** 25 - * Disconnect from the service. 26 - */ 27 - public function disconnect() { 28 - return; 29 - } 30 - 31 - /** 32 - * This is the spout for messages coming in from the protocol. 33 - * This will be called in the main event loop of the bot daemon 34 - * So if if doesn't implement some sort of blocking timeout 35 - * (e.g. select-based socket polling), it should at least sleep 36 - * for some period of time in order to not overwhelm the processor. 37 - * 38 - * @param Int $poll_frequency The number of seconds between polls 39 - */ 40 - abstract public function getNextMessages($poll_frequency); 41 - 42 - /** 43 - * This is the output mechanism for the protocol. 44 - * 45 - * @param PhabricatorBotMessage $message The message to write 46 - */ 47 - abstract public function writeMessage(PhabricatorBotMessage $message); 48 - 49 - /** 50 - * String identifying the service type the adapter provides access to, like 51 - * "irc", "campfire", "flowdock", "hipchat", etc. 52 - */ 53 - abstract public function getServiceType(); 54 - 55 - /** 56 - * String identifying the service name the adapter is connecting to. This is 57 - * used to distinguish between instances of a service. For example, for IRC, 58 - * this should return the IRC network the client is connecting to. 59 - */ 60 - abstract public function getServiceName(); 61 - 62 - }
-170
src/infrastructure/daemon/bot/adapter/PhabricatorStreamingProtocolAdapter.php
··· 1 - <?php 2 - 3 - abstract class PhabricatorStreamingProtocolAdapter 4 - extends PhabricatorProtocolAdapter { 5 - 6 - protected $readHandles; 7 - protected $multiHandle; 8 - protected $authtoken; 9 - protected $inRooms = array(); 10 - 11 - private $readBuffers; 12 - private $server; 13 - private $active; 14 - 15 - public function getServiceName() { 16 - $uri = new PhutilURI($this->server); 17 - return $uri->getDomain(); 18 - } 19 - 20 - public function connect() { 21 - $this->server = $this->getConfig('server'); 22 - $this->authtoken = $this->getConfig('authtoken'); 23 - $rooms = $this->getConfig('join'); 24 - 25 - // First, join the room 26 - if (!$rooms) { 27 - throw new Exception(pht('Not configured to join any rooms!')); 28 - } 29 - 30 - $this->readBuffers = array(); 31 - 32 - // Set up our long poll in a curl multi request so we can 33 - // continue running while it executes in the background 34 - $this->multiHandle = curl_multi_init(); 35 - $this->readHandles = array(); 36 - 37 - foreach ($rooms as $room_id) { 38 - $this->joinRoom($room_id); 39 - 40 - // Set up the curl stream for reading 41 - $url = $this->buildStreamingUrl($room_id); 42 - $ch = $this->readHandles[$url] = curl_init(); 43 - 44 - curl_setopt($ch, CURLOPT_URL, $url); 45 - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 46 - curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1); 47 - curl_setopt( 48 - $ch, 49 - CURLOPT_USERPWD, 50 - $this->authtoken.':x'); 51 - 52 - curl_setopt( 53 - $ch, 54 - CURLOPT_HTTPHEADER, 55 - array('Content-type: application/json')); 56 - curl_setopt( 57 - $ch, 58 - CURLOPT_WRITEFUNCTION, 59 - array($this, 'read')); 60 - curl_setopt($ch, CURLOPT_BUFFERSIZE, 128); 61 - curl_setopt($ch, CURLOPT_TIMEOUT, 0); 62 - 63 - curl_multi_add_handle($this->multiHandle, $ch); 64 - 65 - // Initialize read buffer 66 - $this->readBuffers[$url] = ''; 67 - } 68 - 69 - $this->active = null; 70 - $this->blockingMultiExec(); 71 - } 72 - 73 - protected function joinRoom($room_id) { 74 - // Optional hook, by default, do nothing 75 - } 76 - 77 - // This is our callback for the background curl multi-request. 78 - // Puts the data read in on the readBuffer for processing. 79 - private function read($ch, $data) { 80 - $info = curl_getinfo($ch); 81 - $length = strlen($data); 82 - $this->readBuffers[$info['url']] .= $data; 83 - return $length; 84 - } 85 - 86 - private function blockingMultiExec() { 87 - do { 88 - $status = curl_multi_exec($this->multiHandle, $this->active); 89 - } while ($status == CURLM_CALL_MULTI_PERFORM); 90 - 91 - // Check for errors 92 - if ($status != CURLM_OK) { 93 - throw new Exception( 94 - pht('Phabricator Bot had a problem reading from stream.')); 95 - } 96 - } 97 - 98 - public function getNextMessages($poll_frequency) { 99 - $messages = array(); 100 - 101 - if (!$this->active) { 102 - throw new Exception(pht('Phabricator Bot stopped reading from stream.')); 103 - } 104 - 105 - // Prod our http request 106 - curl_multi_select($this->multiHandle, $poll_frequency); 107 - $this->blockingMultiExec(); 108 - 109 - // Process anything waiting on the read buffer 110 - while ($m = $this->processReadBuffer()) { 111 - $messages[] = $m; 112 - } 113 - 114 - return $messages; 115 - } 116 - 117 - private function processReadBuffer() { 118 - foreach ($this->readBuffers as $url => &$buffer) { 119 - $until = strpos($buffer, "}\r"); 120 - if ($until == false) { 121 - continue; 122 - } 123 - 124 - $message = substr($buffer, 0, $until + 1); 125 - $buffer = substr($buffer, $until + 2); 126 - 127 - $m_obj = phutil_json_decode($message); 128 - if ($message = $this->processMessage($m_obj)) { 129 - return $message; 130 - } 131 - } 132 - 133 - // If we're here, there's nothing to process 134 - return false; 135 - } 136 - 137 - protected function performPost($endpoint, $data = null) { 138 - $uri = new PhutilURI($this->server); 139 - $uri->setPath($endpoint); 140 - 141 - $payload = json_encode($data); 142 - 143 - list($output) = id(new HTTPSFuture($uri)) 144 - ->setMethod('POST') 145 - ->addHeader('Content-Type', 'application/json') 146 - ->addHeader('Authorization', $this->getAuthorizationHeader()) 147 - ->setData($payload) 148 - ->resolvex(); 149 - 150 - $output = trim($output); 151 - if (strlen($output)) { 152 - return phutil_json_decode($output); 153 - } 154 - 155 - return true; 156 - } 157 - 158 - protected function getAuthorizationHeader() { 159 - return 'Basic '.$this->getEncodedAuthToken(); 160 - } 161 - 162 - protected function getEncodedAuthToken() { 163 - return base64_encode($this->authtoken.':x'); 164 - } 165 - 166 - abstract protected function buildStreamingUrl($channel); 167 - 168 - abstract protected function processMessage(array $raw_object); 169 - 170 - }
-17
src/infrastructure/daemon/bot/handler/PhabricatorBotDebugLogHandler.php
··· 1 - <?php 2 - 3 - /** 4 - * Logs messages to stdout. 5 - */ 6 - final class PhabricatorBotDebugLogHandler extends PhabricatorBotHandler { 7 - public function receiveMessage(PhabricatorBotMessage $message) { 8 - switch ($message->getCommand()) { 9 - case 'LOG': 10 - echo addcslashes( 11 - $message->getBody(), 12 - "\0..\37\177..\377"); 13 - echo "\n"; 14 - break; 15 - } 16 - } 17 - }
-180
src/infrastructure/daemon/bot/handler/PhabricatorBotFeedNotificationHandler.php
··· 1 - <?php 2 - 3 - /** 4 - * Watches the feed and puts notifications into channel(s) of choice. 5 - */ 6 - final class PhabricatorBotFeedNotificationHandler 7 - extends PhabricatorBotHandler { 8 - 9 - private $startupDelay = 30; 10 - private $lastSeenChronoKey = 0; 11 - 12 - private $typesNeedURI = array('DREV', 'TASK'); 13 - 14 - private function shouldShowStory($story) { 15 - $story_objectphid = $story['objectPHID']; 16 - $story_text = $story['text']; 17 - 18 - $show = $this->getConfig('notification.types'); 19 - 20 - if ($show) { 21 - $obj_type = phid_get_type($story_objectphid); 22 - if (!in_array(strtolower($obj_type), $show)) { 23 - return false; 24 - } 25 - } 26 - 27 - $verbosity = $this->getConfig('notification.verbosity', 3); 28 - 29 - $verbs = array(); 30 - 31 - switch ($verbosity) { 32 - case 2: 33 - $verbs[] = array( 34 - 'commented', 35 - 'added', 36 - 'changed', 37 - 'resigned', 38 - 'explained', 39 - 'modified', 40 - 'attached', 41 - 'edited', 42 - 'joined', 43 - 'left', 44 - 'removed', 45 - ); 46 - // fallthrough 47 - case 1: 48 - $verbs[] = array( 49 - 'updated', 50 - 'accepted', 51 - 'requested', 52 - 'planned', 53 - 'claimed', 54 - 'summarized', 55 - 'commandeered', 56 - 'assigned', 57 - ); 58 - // fallthrough 59 - case 0: 60 - $verbs[] = array( 61 - 'created', 62 - 'closed', 63 - 'raised', 64 - 'committed', 65 - 'abandoned', 66 - 'reclaimed', 67 - 'reopened', 68 - 'deleted', 69 - ); 70 - break; 71 - 72 - case 3: 73 - default: 74 - return true; 75 - } 76 - 77 - $verbs = '/('.implode('|', array_mergev($verbs)).')/'; 78 - 79 - if (preg_match($verbs, $story_text)) { 80 - return true; 81 - } 82 - 83 - return false; 84 - } 85 - 86 - public function receiveMessage(PhabricatorBotMessage $message) { 87 - return; 88 - } 89 - 90 - public function runBackgroundTasks() { 91 - if ($this->startupDelay > 0) { 92 - // the event loop runs every 1s so delay enough to fully conenct 93 - $this->startupDelay--; 94 - 95 - return; 96 - } 97 - if ($this->lastSeenChronoKey == 0) { 98 - // Since we only want to post notifications about new stories, skip 99 - // everything that's happened in the past when we start up so we'll 100 - // only process real-time stories. 101 - $latest = $this->getConduit()->callMethodSynchronous( 102 - 'feed.query', 103 - array( 104 - 'limit' => 1, 105 - )); 106 - 107 - foreach ($latest as $story) { 108 - if ($story['chronologicalKey'] > $this->lastSeenChronoKey) { 109 - $this->lastSeenChronoKey = $story['chronologicalKey']; 110 - } 111 - } 112 - 113 - return; 114 - } 115 - 116 - $config_max_pages = $this->getConfig('notification.max_pages', 5); 117 - $config_page_size = $this->getConfig('notification.page_size', 10); 118 - 119 - $last_seen_chrono_key = $this->lastSeenChronoKey; 120 - $chrono_key_cursor = 0; 121 - 122 - // Not efficient but works due to feed.query API 123 - for ($max_pages = $config_max_pages; $max_pages > 0; $max_pages--) { 124 - $stories = $this->getConduit()->callMethodSynchronous( 125 - 'feed.query', 126 - array( 127 - 'limit' => $config_page_size, 128 - 'after' => $chrono_key_cursor, 129 - 'view' => 'text', 130 - )); 131 - 132 - foreach ($stories as $story) { 133 - if ($story['chronologicalKey'] == $last_seen_chrono_key) { 134 - // Caught up on feed 135 - return; 136 - } 137 - if ($story['chronologicalKey'] > $this->lastSeenChronoKey) { 138 - // Keep track of newest seen story 139 - $this->lastSeenChronoKey = $story['chronologicalKey']; 140 - } 141 - if (!$chrono_key_cursor || 142 - $story['chronologicalKey'] < $chrono_key_cursor) { 143 - // Keep track of oldest story on this page 144 - $chrono_key_cursor = $story['chronologicalKey']; 145 - } 146 - 147 - if (!$story['text'] || 148 - !$this->shouldShowStory($story)) { 149 - continue; 150 - } 151 - 152 - $message = $story['text']; 153 - 154 - $story_object_type = phid_get_type($story['objectPHID']); 155 - if (in_array($story_object_type, $this->typesNeedURI)) { 156 - $objects = $this->getConduit()->callMethodSynchronous( 157 - 'phid.lookup', 158 - array( 159 - 'names' => array($story['objectPHID']), 160 - )); 161 - $message .= ' '.$objects[$story['objectPHID']]['uri']; 162 - } 163 - 164 - $channels = $this->getConfig('join'); 165 - foreach ($channels as $channel_name) { 166 - 167 - $channel = id(new PhabricatorBotChannel()) 168 - ->setName($channel_name); 169 - 170 - $this->writeMessage( 171 - id(new PhabricatorBotMessage()) 172 - ->setCommand('MESSAGE') 173 - ->setTarget($channel) 174 - ->setBody($message)); 175 - } 176 - } 177 - } 178 - } 179 - 180 - }
-72
src/infrastructure/daemon/bot/handler/PhabricatorBotHandler.php
··· 1 - <?php 2 - 3 - /** 4 - * Responds to IRC messages. You plug a bunch of these into a 5 - * @{class:PhabricatorBot} to give it special behavior. 6 - */ 7 - abstract class PhabricatorBotHandler extends Phobject { 8 - 9 - private $bot; 10 - 11 - final public function __construct(PhabricatorBot $irc_bot) { 12 - $this->bot = $irc_bot; 13 - } 14 - 15 - final protected function writeMessage(PhabricatorBotMessage $message) { 16 - $this->bot->writeMessage($message); 17 - return $this; 18 - } 19 - 20 - final protected function getConduit() { 21 - return $this->bot->getConduit(); 22 - } 23 - 24 - final protected function getConfig($key, $default = null) { 25 - return $this->bot->getConfig($key, $default); 26 - } 27 - 28 - final protected function getURI($path) { 29 - $base_uri = new PhutilURI($this->bot->getConfig('conduit.uri')); 30 - $base_uri->setPath($path); 31 - return (string)$base_uri; 32 - } 33 - 34 - final protected function getServiceName() { 35 - return $this->bot->getAdapter()->getServiceName(); 36 - } 37 - 38 - final protected function getServiceType() { 39 - return $this->bot->getAdapter()->getServiceType(); 40 - } 41 - 42 - abstract public function receiveMessage(PhabricatorBotMessage $message); 43 - 44 - public function runBackgroundTasks() { 45 - return; 46 - } 47 - 48 - public function replyTo(PhabricatorBotMessage $original_message, $body) { 49 - if ($original_message->getCommand() != 'MESSAGE') { 50 - throw new Exception( 51 - pht('Handler is trying to reply to something which is not a message!')); 52 - } 53 - 54 - $reply = id(new PhabricatorBotMessage()) 55 - ->setCommand('MESSAGE'); 56 - 57 - if ($original_message->getTarget()->isPublic()) { 58 - // This is a public target, like a chatroom. Send the response to the 59 - // chatroom. 60 - $reply->setTarget($original_message->getTarget()); 61 - } else { 62 - // This is a private target, like a private message. Send the response 63 - // back to the sender (presumably, we are the target). 64 - $reply->setTarget($original_message->getSender()); 65 - } 66 - 67 - $reply->setBody($body); 68 - 69 - return $this->writeMessage($reply); 70 - } 71 - 72 - }
-77
src/infrastructure/daemon/bot/handler/PhabricatorBotLogHandler.php
··· 1 - <?php 2 - 3 - /** 4 - * Logs chatter. 5 - */ 6 - final class PhabricatorBotLogHandler extends PhabricatorBotHandler { 7 - 8 - private $futures = array(); 9 - 10 - public function receiveMessage(PhabricatorBotMessage $message) { 11 - switch ($message->getCommand()) { 12 - case 'MESSAGE': 13 - $target = $message->getTarget(); 14 - if (!$target->isPublic()) { 15 - // Don't log private messages, although maybe we should for debugging? 16 - break; 17 - } 18 - 19 - $target_name = $target->getName(); 20 - 21 - $logs = array( 22 - array( 23 - 'channel' => $target_name, 24 - 'type' => 'mesg', 25 - 'epoch' => time(), 26 - 'author' => $message->getSender()->getName(), 27 - 'message' => $message->getBody(), 28 - 'serviceName' => $this->getServiceName(), 29 - 'serviceType' => $this->getServiceType(), 30 - ), 31 - ); 32 - 33 - $this->futures[] = $this->getConduit()->callMethod( 34 - 'chatlog.record', 35 - array( 36 - 'logs' => $logs, 37 - )); 38 - 39 - $prompts = array( 40 - '/where is the (chat)?log\?/i', 41 - '/where am i\?/i', 42 - '/what year is (this|it)\?/i', 43 - ); 44 - 45 - $tell = false; 46 - foreach ($prompts as $prompt) { 47 - if (preg_match($prompt, $message->getBody())) { 48 - $tell = true; 49 - break; 50 - } 51 - } 52 - 53 - if ($tell) { 54 - $response = $this->getURI( 55 - '/chatlog/channel/'.phutil_escape_uri($target_name).'/'); 56 - 57 - $this->replyTo($message, $response); 58 - } 59 - 60 - break; 61 - } 62 - } 63 - 64 - public function runBackgroundTasks() { 65 - foreach ($this->futures as $key => $future) { 66 - try { 67 - if ($future->isReady()) { 68 - unset($this->futures[$key]); 69 - } 70 - } catch (Exception $ex) { 71 - unset($this->futures[$key]); 72 - phlog($ex); 73 - } 74 - } 75 - } 76 - 77 - }
-176
src/infrastructure/daemon/bot/handler/PhabricatorBotMacroHandler.php
··· 1 - <?php 2 - 3 - final class PhabricatorBotMacroHandler extends PhabricatorBotHandler { 4 - 5 - private $macros; 6 - private $regexp; 7 - 8 - private $next = 0; 9 - 10 - private function init() { 11 - if ($this->macros === false) { 12 - return false; 13 - } 14 - 15 - if ($this->macros !== null) { 16 - return true; 17 - } 18 - 19 - $macros = $this->getConduit()->callMethodSynchronous( 20 - 'macro.query', 21 - array()); 22 - 23 - // If we have no macros, cache `false` (meaning "no macros") and return 24 - // immediately. 25 - if (!$macros) { 26 - $this->macros = false; 27 - return false; 28 - } 29 - 30 - $regexp = array(); 31 - foreach ($macros as $macro_name => $macro) { 32 - $regexp[] = preg_quote($macro_name, '/'); 33 - } 34 - $regexp = '/^('.implode('|', $regexp).')\z/'; 35 - 36 - $this->macros = $macros; 37 - $this->regexp = $regexp; 38 - 39 - return true; 40 - } 41 - 42 - public function receiveMessage(PhabricatorBotMessage $message) { 43 - if (!$this->init()) { 44 - return; 45 - } 46 - 47 - switch ($message->getCommand()) { 48 - case 'MESSAGE': 49 - $message_body = $message->getBody(); 50 - 51 - $matches = null; 52 - if (!preg_match($this->regexp, trim($message_body), $matches)) { 53 - return; 54 - } 55 - 56 - $macro = $matches[1]; 57 - 58 - $ascii = idx($this->macros[$macro], 'ascii'); 59 - if ($ascii === false) { 60 - return; 61 - } 62 - 63 - if (!$ascii) { 64 - $this->macros[$macro]['ascii'] = $this->rasterize( 65 - $this->macros[$macro], 66 - $this->getConfig('macro.size', 48), 67 - $this->getConfig('macro.aspect', 0.66)); 68 - $ascii = $this->macros[$macro]['ascii']; 69 - } 70 - 71 - if ($ascii === false) { 72 - // If we failed to rasterize the macro, bail out. 73 - return; 74 - } 75 - 76 - $target_name = $message->getTarget()->getName(); 77 - foreach ($ascii as $line) { 78 - $this->replyTo($message, $line); 79 - } 80 - break; 81 - } 82 - } 83 - 84 - public function rasterize($macro, $size, $aspect) { 85 - try { 86 - $image = $this->getConduit()->callMethodSynchronous( 87 - 'file.download', 88 - array( 89 - 'phid' => $macro['filePHID'], 90 - )); 91 - $image = base64_decode($image); 92 - } catch (Exception $ex) { 93 - return false; 94 - } 95 - 96 - if (!$image) { 97 - return false; 98 - } 99 - 100 - $img = @imagecreatefromstring($image); 101 - if (!$img) { 102 - return false; 103 - } 104 - 105 - $sx = imagesx($img); 106 - $sy = imagesy($img); 107 - 108 - if ($sx > $size || $sy > $size) { 109 - $scale = max($sx, $sy) / $size; 110 - $dx = floor($sx / $scale); 111 - $dy = floor($sy / $scale); 112 - } else { 113 - $dx = $sx; 114 - $dy = $sy; 115 - } 116 - 117 - $dy = floor($dy * $aspect); 118 - 119 - $dst = imagecreatetruecolor($dx, $dy); 120 - if (!$dst) { 121 - return false; 122 - } 123 - imagealphablending($dst, false); 124 - 125 - $ok = imagecopyresampled( 126 - $dst, $img, 127 - 0, 0, 128 - 0, 0, 129 - $dx, $dy, 130 - $sx, $sy); 131 - 132 - if (!$ok) { 133 - return false; 134 - } 135 - 136 - $map = array( 137 - ' ', 138 - '.', 139 - ',', 140 - ':', 141 - ';', 142 - '!', 143 - '|', 144 - '*', 145 - '=', 146 - '@', 147 - '$', 148 - '#', 149 - ); 150 - 151 - $lines = array(); 152 - 153 - for ($ii = 0; $ii < $dy; $ii++) { 154 - $buf = ''; 155 - for ($jj = 0; $jj < $dx; $jj++) { 156 - $c = imagecolorat($dst, $jj, $ii); 157 - 158 - $a = ($c >> 24) & 0xFF; 159 - $r = ($c >> 16) & 0xFF; 160 - $g = ($c >> 8) & 0xFF; 161 - $b = ($c) & 0xFF; 162 - 163 - $luma = (255 - ((0.30 * $r) + (0.59 * $g) + (0.11 * $b))) / 256; 164 - $luma *= ((127 - $a) / 127); 165 - 166 - $char = $map[max(0, floor($luma * count($map)))]; 167 - $buf .= $char; 168 - } 169 - 170 - $lines[] = $buf; 171 - } 172 - 173 - return $lines; 174 - } 175 - 176 - }
-206
src/infrastructure/daemon/bot/handler/PhabricatorBotObjectNameHandler.php
··· 1 - <?php 2 - 3 - /** 4 - * Looks for Dxxxx, Txxxx and links to them. 5 - */ 6 - final class PhabricatorBotObjectNameHandler extends PhabricatorBotHandler { 7 - 8 - /** 9 - * Map of PHIDs to the last mention of them (as an epoch timestamp); prevents 10 - * us from spamming chat when a single object is discussed. 11 - */ 12 - private $recentlyMentioned = array(); 13 - 14 - public function receiveMessage(PhabricatorBotMessage $original_message) { 15 - switch ($original_message->getCommand()) { 16 - case 'MESSAGE': 17 - $message = $original_message->getBody(); 18 - $matches = null; 19 - 20 - $paste_ids = array(); 21 - $commit_names = array(); 22 - $vote_ids = array(); 23 - $file_ids = array(); 24 - $object_names = array(); 25 - $output = array(); 26 - 27 - $pattern = 28 - '@'. 29 - '(?<![/:#-])(?:^|\b)'. 30 - '(R2D2)'. 31 - '(?:\b|$)'. 32 - '@'; 33 - 34 - if (preg_match_all($pattern, $message, $matches, PREG_SET_ORDER)) { 35 - foreach ($matches as $match) { 36 - switch ($match[1]) { 37 - case 'R2D2': 38 - $output[$match[1]] = pht('beep boop bop'); 39 - break; 40 - } 41 - } 42 - } 43 - 44 - // Use a negative lookbehind to prevent matching "/D123", "#D123", 45 - // ":D123", etc. 46 - $pattern = 47 - '@'. 48 - '(?<![/:#-])(?:^|\b)'. 49 - '([A-Z])(\d+)'. 50 - '(?:\b|$)'. 51 - '@'; 52 - 53 - $regex = trim( 54 - PhabricatorEnv::getEnvConfig('remarkup.ignored-object-names')); 55 - 56 - if (preg_match_all($pattern, $message, $matches, PREG_SET_ORDER)) { 57 - foreach ($matches as $match) { 58 - if ($regex && preg_match($regex, $match[0])) { 59 - continue; 60 - } 61 - switch ($match[1]) { 62 - case 'P': 63 - $paste_ids[] = $match[2]; 64 - break; 65 - case 'V': 66 - $vote_ids[] = $match[2]; 67 - break; 68 - case 'F': 69 - $file_ids[] = $match[2]; 70 - break; 71 - default: 72 - $name = $match[1].$match[2]; 73 - switch ($name) { 74 - case 'T1000': 75 - $output[$name] = pht( 76 - 'T1000: A mimetic poly-alloy assassin controlled by '. 77 - 'Skynet'); 78 - break; 79 - default: 80 - $object_names[] = $name; 81 - break; 82 - } 83 - break; 84 - } 85 - } 86 - } 87 - 88 - $pattern = 89 - '@'. 90 - '(?<!/)(?:^|\b)'. 91 - '(r[A-Z]+)([0-9a-z]{0,40})'. 92 - '(?:\b|$)'. 93 - '@'; 94 - if (preg_match_all($pattern, $message, $matches, PREG_SET_ORDER)) { 95 - foreach ($matches as $match) { 96 - if ($match[2]) { 97 - $commit_names[] = $match[1].$match[2]; 98 - } else { 99 - $object_names[] = $match[1]; 100 - } 101 - } 102 - } 103 - 104 - if ($object_names) { 105 - $objects = $this->getConduit()->callMethodSynchronous( 106 - 'phid.lookup', 107 - array( 108 - 'names' => $object_names, 109 - )); 110 - foreach ($objects as $object) { 111 - $output[$object['phid']] = $object['fullName'].' - '.$object['uri']; 112 - } 113 - } 114 - 115 - if ($vote_ids) { 116 - foreach ($vote_ids as $vote_id) { 117 - $vote = $this->getConduit()->callMethodSynchronous( 118 - 'slowvote.info', 119 - array( 120 - 'poll_id' => $vote_id, 121 - )); 122 - $output[$vote['phid']] = 'V'.$vote['id'].': '.$vote['question']. 123 - ' '.pht('Come Vote').' '.$vote['uri']; 124 - } 125 - } 126 - 127 - if ($file_ids) { 128 - foreach ($file_ids as $file_id) { 129 - $file = $this->getConduit()->callMethodSynchronous( 130 - 'file.info', 131 - array( 132 - 'id' => $file_id, 133 - )); 134 - $output[$file['phid']] = $file['objectName'].': '. 135 - $file['uri'].' - '.$file['name']; 136 - } 137 - } 138 - 139 - if ($paste_ids) { 140 - foreach ($paste_ids as $paste_id) { 141 - $paste = $this->getConduit()->callMethodSynchronous( 142 - 'paste.query', 143 - array( 144 - 'ids' => array($paste_id), 145 - )); 146 - $paste = head($paste); 147 - 148 - $output[$paste['phid']] = 'P'.$paste['id'].': '.$paste['uri'].' - '. 149 - $paste['title']; 150 - 151 - if ($paste['language']) { 152 - $output[$paste['phid']] .= ' ('.$paste['language'].')'; 153 - } 154 - 155 - $user = $this->getConduit()->callMethodSynchronous( 156 - 'user.query', 157 - array( 158 - 'phids' => array($paste['authorPHID']), 159 - )); 160 - $user = head($user); 161 - if ($user) { 162 - $output[$paste['phid']] .= ' by '.$user['userName']; 163 - } 164 - } 165 - } 166 - 167 - if ($commit_names) { 168 - $commits = $this->getConduit()->callMethodSynchronous( 169 - 'diffusion.querycommits', 170 - array( 171 - 'names' => $commit_names, 172 - )); 173 - foreach ($commits['data'] as $commit) { 174 - $output[$commit['phid']] = $commit['uri']; 175 - } 176 - } 177 - 178 - foreach ($output as $phid => $description) { 179 - 180 - // Don't mention the same object more than once every 10 minutes 181 - // in public channels, so we avoid spamming the chat over and over 182 - // again for discussions of a specific revision, for example. 183 - 184 - $target_name = $original_message->getTarget()->getName(); 185 - if (empty($this->recentlyMentioned[$target_name])) { 186 - $this->recentlyMentioned[$target_name] = array(); 187 - } 188 - 189 - $quiet_until = idx( 190 - $this->recentlyMentioned[$target_name], 191 - $phid, 192 - 0) + (60 * 10); 193 - 194 - if (time() < $quiet_until) { 195 - // Remain quiet on this channel. 196 - continue; 197 - } 198 - 199 - $this->recentlyMentioned[$target_name][$phid] = time(); 200 - $this->replyTo($original_message, $description); 201 - } 202 - break; 203 - } 204 - } 205 - 206 - }
-50
src/infrastructure/daemon/bot/handler/PhabricatorBotSymbolHandler.php
··· 1 - <?php 2 - 3 - /** 4 - * Watches for "where is <symbol>?" 5 - */ 6 - final class PhabricatorBotSymbolHandler extends PhabricatorBotHandler { 7 - 8 - public function receiveMessage(PhabricatorBotMessage $message) { 9 - switch ($message->getCommand()) { 10 - case 'MESSAGE': 11 - $text = $message->getBody(); 12 - 13 - $matches = null; 14 - if (!preg_match('/where(?: in the world)? is (\S+?)\?/i', 15 - $text, $matches)) { 16 - break; 17 - } 18 - 19 - $symbol = $matches[1]; 20 - $results = $this->getConduit()->callMethodSynchronous( 21 - 'diffusion.findsymbols', 22 - array( 23 - 'name' => $symbol, 24 - )); 25 - 26 - $default_uri = $this->getURI('/diffusion/symbol/'.$symbol.'/'); 27 - 28 - if (count($results) > 1) { 29 - $response = pht( 30 - "Multiple symbols named '%s': %s", 31 - $symbol, 32 - $default_uri); 33 - } else if (count($results) == 1) { 34 - $result = head($results); 35 - $response = 36 - $result['type'].' '. 37 - $result['name'].' '. 38 - '('.$result['language'].'): '. 39 - nonempty($result['uri'], $default_uri); 40 - } else { 41 - $response = pht("No symbol '%s' found anywhere.", $symbol); 42 - } 43 - 44 - $this->replyTo($message, $response); 45 - 46 - break; 47 - } 48 - } 49 - 50 - }
-43
src/infrastructure/daemon/bot/handler/PhabricatorBotWhatsNewHandler.php
··· 1 - <?php 2 - 3 - /** 4 - * Responds to "Whats new?" with some recent feed content. 5 - */ 6 - final class PhabricatorBotWhatsNewHandler extends PhabricatorBotHandler { 7 - 8 - private $floodblock = 0; 9 - 10 - public function receiveMessage(PhabricatorBotMessage $message) { 11 - switch ($message->getCommand()) { 12 - case 'MESSAGE': 13 - $message_body = $message->getBody(); 14 - $now = time(); 15 - 16 - $prompt = '~what( i|\')?s new\?~i'; 17 - if (preg_match($prompt, $message_body)) { 18 - if ($now < $this->floodblock) { 19 - return; 20 - } 21 - $this->floodblock = $now + 60; 22 - $this->reportNew($message); 23 - } 24 - break; 25 - } 26 - } 27 - 28 - public function reportNew(PhabricatorBotMessage $message) { 29 - $latest = $this->getConduit()->callMethodSynchronous( 30 - 'feed.query', 31 - array( 32 - 'limit' => 5, 33 - 'view' => 'text', 34 - )); 35 - 36 - foreach ($latest as $feed_item) { 37 - if (isset($feed_item['text'])) { 38 - $this->replyTo($message, html_entity_decode($feed_item['text'])); 39 - } 40 - } 41 - } 42 - 43 - }
-12
src/infrastructure/daemon/bot/target/PhabricatorBotChannel.php
··· 1 - <?php 2 - 3 - /** 4 - * Represents a group/public space, like an IRC channel or a Campfire room. 5 - */ 6 - final class PhabricatorBotChannel extends PhabricatorBotTarget { 7 - 8 - public function isPublic() { 9 - return true; 10 - } 11 - 12 - }
-22
src/infrastructure/daemon/bot/target/PhabricatorBotTarget.php
··· 1 - <?php 2 - 3 - /** 4 - * Represents something which can be the target of messages, like a user or 5 - * channel. 6 - */ 7 - abstract class PhabricatorBotTarget extends Phobject { 8 - 9 - private $name; 10 - 11 - public function setName($name) { 12 - $this->name = $name; 13 - return $this; 14 - } 15 - 16 - public function getName() { 17 - return $this->name; 18 - } 19 - 20 - abstract public function isPublic(); 21 - 22 - }
-12
src/infrastructure/daemon/bot/target/PhabricatorBotUser.php
··· 1 - <?php 2 - 3 - /** 4 - * Represents an individual user. 5 - */ 6 - final class PhabricatorBotUser extends PhabricatorBotTarget { 7 - 8 - public function isPublic() { 9 - return false; 10 - } 11 - 12 - }