1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-25 16:22:43 +01:00

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
This commit is contained in:
epriestley 2017-04-21 12:42:50 -07:00
parent ede23efcc7
commit 5c1e4488de
20 changed files with 0 additions and 1933 deletions

View file

@ -2089,20 +2089,6 @@ phutil_register_library_map(array(
'PhabricatorBoardRenderingEngine' => 'applications/project/engine/PhabricatorBoardRenderingEngine.php',
'PhabricatorBoardResponseEngine' => 'applications/project/engine/PhabricatorBoardResponseEngine.php',
'PhabricatorBoolEditField' => 'applications/transactions/editfield/PhabricatorBoolEditField.php',
'PhabricatorBot' => 'infrastructure/daemon/bot/PhabricatorBot.php',
'PhabricatorBotChannel' => 'infrastructure/daemon/bot/target/PhabricatorBotChannel.php',
'PhabricatorBotDebugLogHandler' => 'infrastructure/daemon/bot/handler/PhabricatorBotDebugLogHandler.php',
'PhabricatorBotFeedNotificationHandler' => 'infrastructure/daemon/bot/handler/PhabricatorBotFeedNotificationHandler.php',
'PhabricatorBotFlowdockProtocolAdapter' => 'infrastructure/daemon/bot/adapter/PhabricatorBotFlowdockProtocolAdapter.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',
'PhabricatorBotTarget' => 'infrastructure/daemon/bot/target/PhabricatorBotTarget.php',
'PhabricatorBotUser' => 'infrastructure/daemon/bot/target/PhabricatorBotUser.php',
'PhabricatorBotWhatsNewHandler' => 'infrastructure/daemon/bot/handler/PhabricatorBotWhatsNewHandler.php',
'PhabricatorBritishEnglishTranslation' => 'infrastructure/internationalization/translation/PhabricatorBritishEnglishTranslation.php',
'PhabricatorBuiltinDraftEngine' => 'applications/transactions/draft/PhabricatorBuiltinDraftEngine.php',
'PhabricatorBuiltinPatchList' => 'infrastructure/storage/patch/PhabricatorBuiltinPatchList.php',
@ -2261,7 +2247,6 @@ phutil_register_library_map(array(
'PhabricatorCalendarRemarkupRule' => 'applications/calendar/remarkup/PhabricatorCalendarRemarkupRule.php',
'PhabricatorCalendarReplyHandler' => 'applications/calendar/mail/PhabricatorCalendarReplyHandler.php',
'PhabricatorCalendarSchemaSpec' => 'applications/calendar/storage/PhabricatorCalendarSchemaSpec.php',
'PhabricatorCampfireProtocolAdapter' => 'infrastructure/daemon/bot/adapter/PhabricatorCampfireProtocolAdapter.php',
'PhabricatorCelerityApplication' => 'applications/celerity/application/PhabricatorCelerityApplication.php',
'PhabricatorCelerityTestCase' => '__tests__/PhabricatorCelerityTestCase.php',
'PhabricatorChangeParserTestCase' => 'applications/repository/worker/__tests__/PhabricatorChangeParserTestCase.php',
@ -2921,7 +2906,6 @@ phutil_register_library_map(array(
'PhabricatorHovercardEngineExtensionModule' => 'applications/search/engineextension/PhabricatorHovercardEngineExtensionModule.php',
'PhabricatorIDsSearchEngineExtension' => 'applications/search/engineextension/PhabricatorIDsSearchEngineExtension.php',
'PhabricatorIDsSearchField' => 'applications/search/field/PhabricatorIDsSearchField.php',
'PhabricatorIRCProtocolAdapter' => 'infrastructure/daemon/bot/adapter/PhabricatorIRCProtocolAdapter.php',
'PhabricatorIconDatasource' => 'applications/files/typeahead/PhabricatorIconDatasource.php',
'PhabricatorIconRemarkupRule' => 'applications/macro/markup/PhabricatorIconRemarkupRule.php',
'PhabricatorIconSet' => 'applications/files/iconset/PhabricatorIconSet.php',
@ -3654,7 +3638,6 @@ phutil_register_library_map(array(
'PhabricatorProjectsSearchEngineExtension' => 'applications/project/engineextension/PhabricatorProjectsSearchEngineExtension.php',
'PhabricatorProjectsWatchersSearchEngineAttachment' => 'applications/project/engineextension/PhabricatorProjectsWatchersSearchEngineAttachment.php',
'PhabricatorPronounSetting' => 'applications/settings/setting/PhabricatorPronounSetting.php',
'PhabricatorProtocolAdapter' => 'infrastructure/daemon/bot/adapter/PhabricatorProtocolAdapter.php',
'PhabricatorPygmentSetupCheck' => 'applications/config/check/PhabricatorPygmentSetupCheck.php',
'PhabricatorQuery' => 'infrastructure/query/PhabricatorQuery.php',
'PhabricatorQueryConstraint' => 'infrastructure/query/constraint/PhabricatorQueryConstraint.php',
@ -3978,7 +3961,6 @@ phutil_register_library_map(array(
'PhabricatorStoragePatch' => 'infrastructure/storage/management/PhabricatorStoragePatch.php',
'PhabricatorStorageSchemaSpec' => 'infrastructure/storage/schema/PhabricatorStorageSchemaSpec.php',
'PhabricatorStorageSetupCheck' => 'applications/config/check/PhabricatorStorageSetupCheck.php',
'PhabricatorStreamingProtocolAdapter' => 'infrastructure/daemon/bot/adapter/PhabricatorStreamingProtocolAdapter.php',
'PhabricatorStringListEditField' => 'applications/transactions/editfield/PhabricatorStringListEditField.php',
'PhabricatorStringSetting' => 'applications/settings/setting/PhabricatorStringSetting.php',
'PhabricatorSubmitEditField' => 'applications/transactions/editfield/PhabricatorSubmitEditField.php',
@ -7150,20 +7132,6 @@ phutil_register_library_map(array(
'PhabricatorBoardRenderingEngine' => 'Phobject',
'PhabricatorBoardResponseEngine' => 'Phobject',
'PhabricatorBoolEditField' => 'PhabricatorEditField',
'PhabricatorBot' => 'PhabricatorDaemon',
'PhabricatorBotChannel' => 'PhabricatorBotTarget',
'PhabricatorBotDebugLogHandler' => 'PhabricatorBotHandler',
'PhabricatorBotFeedNotificationHandler' => 'PhabricatorBotHandler',
'PhabricatorBotFlowdockProtocolAdapter' => 'PhabricatorStreamingProtocolAdapter',
'PhabricatorBotHandler' => 'Phobject',
'PhabricatorBotLogHandler' => 'PhabricatorBotHandler',
'PhabricatorBotMacroHandler' => 'PhabricatorBotHandler',
'PhabricatorBotMessage' => 'Phobject',
'PhabricatorBotObjectNameHandler' => 'PhabricatorBotHandler',
'PhabricatorBotSymbolHandler' => 'PhabricatorBotHandler',
'PhabricatorBotTarget' => 'Phobject',
'PhabricatorBotUser' => 'PhabricatorBotTarget',
'PhabricatorBotWhatsNewHandler' => 'PhabricatorBotHandler',
'PhabricatorBritishEnglishTranslation' => 'PhutilTranslation',
'PhabricatorBuiltinDraftEngine' => 'PhabricatorDraftEngine',
'PhabricatorBuiltinPatchList' => 'PhabricatorSQLPatchList',
@ -7358,7 +7326,6 @@ phutil_register_library_map(array(
'PhabricatorCalendarRemarkupRule' => 'PhabricatorObjectRemarkupRule',
'PhabricatorCalendarReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
'PhabricatorCalendarSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhabricatorCampfireProtocolAdapter' => 'PhabricatorStreamingProtocolAdapter',
'PhabricatorCelerityApplication' => 'PhabricatorApplication',
'PhabricatorCelerityTestCase' => 'PhabricatorTestCase',
'PhabricatorChangeParserTestCase' => 'PhabricatorWorkingCopyTestCase',
@ -8115,7 +8082,6 @@ phutil_register_library_map(array(
'PhabricatorHovercardEngineExtensionModule' => 'PhabricatorConfigModule',
'PhabricatorIDsSearchEngineExtension' => 'PhabricatorSearchEngineExtension',
'PhabricatorIDsSearchField' => 'PhabricatorSearchField',
'PhabricatorIRCProtocolAdapter' => 'PhabricatorProtocolAdapter',
'PhabricatorIconDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorIconRemarkupRule' => 'PhutilRemarkupRule',
'PhabricatorIconSet' => 'Phobject',
@ -8973,7 +8939,6 @@ phutil_register_library_map(array(
'PhabricatorProjectsSearchEngineExtension' => 'PhabricatorSearchEngineExtension',
'PhabricatorProjectsWatchersSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment',
'PhabricatorPronounSetting' => 'PhabricatorSelectSetting',
'PhabricatorProtocolAdapter' => 'Phobject',
'PhabricatorPygmentSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorQuery' => 'Phobject',
'PhabricatorQueryConstraint' => 'Phobject',
@ -9377,7 +9342,6 @@ phutil_register_library_map(array(
'PhabricatorStoragePatch' => 'Phobject',
'PhabricatorStorageSchemaSpec' => 'PhabricatorConfigSchemaSpec',
'PhabricatorStorageSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorStreamingProtocolAdapter' => 'PhabricatorProtocolAdapter',
'PhabricatorStringListEditField' => 'PhabricatorEditField',
'PhabricatorStringSetting' => 'PhabricatorSetting',
'PhabricatorSubmitEditField' => 'PhabricatorEditField',

View file

@ -1,87 +0,0 @@
@title Chat Bot Technical Documentation
@group bot
Configuring and extending the chat bot.
= Overview =
Phabricator includes a simple chat bot daemon, which is primarily intended as
an example of how you can write an external script that interfaces with
Phabricator over Conduit and does some kind of useful work. If you use IRC or
another supported chat protocol, you can also have the bot hang out in your
channel.
NOTE: The chat bot is somewhat experimental and not very mature.
= Configuring the Bot =
The bot reads a JSON configuration file. You can find an example in:
resources/chatbot/example_config.json
These are the configuration values it reads:
- `server` String, required, the server to connect to.
- `port` Int, optional, the port to connect to (defaults to 6667).
- `ssl` Bool, optional, whether to connect via SSL or not (defaults to
false).
- `nick` String, nickname to use.
- `user` String, optional, username to use (defaults to `nick`).
- `pass` String, optional, password for server.
- `nickpass` String, optional, password for NickServ.
- `join` Array, list of channels to join.
- `handlers` Array, list of handlers to run. These are like plugins for the
bot.
- `conduit.uri`, `conduit.token` Conduit configuration,
see below.
- `notification.channels` Notification configuration, see below.
= Handlers =
You specify a list of "handlers", which are basically plugins or modules for
the bot. These are the default handlers available:
- @{class:PhabricatorBotObjectNameHandler} This handler looks for users
mentioning Phabricator objects like "T123" and "D345" in chat, looks them
up, and says their name with a link to the object. Requires conduit.
- @{class:PhabricatorBotFeedNotificationHandler} This handler posts
notifications about changes to revisions to the channels listed in
`notification.channels`.
- @{class:PhabricatorBotLogHandler} This handler records chatlogs which can
be browsed in the Phabricator web interface.
- @{class:PhabricatorBotSymbolHandler} This handler posts responses to lookups
for symbols in Diffusion
- @{class:PhabricatorBotMacroHandler} This handler looks for users mentioning
macros, if found will convert image to ASCII and output in chat. Configure
with `macro.size` and `macro.aspect`
You can also write your own handlers, by extending
@{class:PhabricatorBotHandler}.
= Conduit =
Some handlers (e.g., @{class:PhabricatorBotObjectNameHandler}) need to read data
from Phabricator over Conduit, Phabricator's HTTP API. You can use this method
to allow other scripts or programs to access Phabricator's data from different
servers and in different languages.
To allow the bot to access Conduit, you need to create a user that it can login
with. To do this, login to Phabricator as an administrator and go to
`People -> Create New Account`. Create a new account and flag them as a
"Bot/Script". Then in your configuration file, set these parameters:
- `conduit.uri` The URI for your Phabricator install, like
`http://phabricator.example.com/`
- `conduit.token` The user's conduit API token, from the "Conduit API Tokens"
tab in the user's administrative view.
Now the bot should be able to connect to Phabricator via Conduit.
= Starting the Bot =
The bot is a Phabricator daemon, so start it with `phd`:
./bin/phd launch phabricatorbot <absolute_path_to_config_file>
If you have issues you can try `debug` instead of `launch`, see
@{article:Managing Daemons with phd} for more information.

View file

@ -1,170 +0,0 @@
<?php
/**
* Simple IRC bot which runs as a Phabricator daemon. Although this bot is
* somewhat useful, it is also intended to serve as a demo of how to write
* "system agents" which communicate with Phabricator over Conduit, so you can
* script system interactions and integrate with other systems.
*
* NOTE: This is super janky and experimental right now.
*/
final class PhabricatorBot extends PhabricatorDaemon {
private $handlers;
private $conduit;
private $config;
private $pollFrequency;
private $protocolAdapter;
protected function run() {
$argv = $this->getArgv();
if (count($argv) !== 1) {
throw new Exception(
pht(
'Usage: %s %s',
__CLASS__,
'<json_config_file>'));
}
$json_raw = Filesystem::readFile($argv[0]);
try {
$config = phutil_json_decode($json_raw);
} catch (PhutilJSONParserException $ex) {
throw new PhutilProxyException(
pht("File '%s' is not valid JSON!", $argv[0]),
$ex);
}
$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;
}
$ca_bundle = idx($config, 'https.cabundle');
if ($ca_bundle) {
HTTPSFuture::setGlobalCABundleFromPath($ca_bundle);
}
$conduit_uri = idx($config, 'conduit.uri');
if ($conduit_uri) {
$conduit_token = idx($config, 'conduit.token');
// Normalize the path component of the URI so users can enter the
// domain without the "/api/" part.
$conduit_uri = new PhutilURI($conduit_uri);
$conduit_host = (string)$conduit_uri->setPath('/');
$conduit_uri = (string)$conduit_uri->setPath('/api/');
$conduit = new ConduitClient($conduit_uri);
if ($conduit_token) {
$conduit->setConduitToken($conduit_token);
} else {
$conduit_user = idx($config, 'conduit.user');
$conduit_cert = idx($config, 'conduit.cert');
$response = $conduit->callMethodSynchronous(
'conduit.connect',
array(
'client' => __CLASS__,
'clientVersion' => '1.0',
'clientDescription' => php_uname('n').':'.$nick,
'host' => $conduit_host,
'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();
$this->protocolAdapter->disconnect();
}
public function getConfig($key, $default = null) {
return idx($this->config, $key, $default);
}
private function runLoop() {
do {
PhabricatorCaches::destroyRequestCache();
$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 (!$this->shouldExit());
}
public function writeMessage(PhabricatorBotMessage $message) {
return $this->protocolAdapter->writeMessage($message);
}
private function routeMessage(PhabricatorBotMessage $message) {
$ignore = $this->getConfig('ignore');
if ($ignore) {
$sender = $message->getSender();
if ($sender && in_array($sender->getName(), $ignore)) {
return;
}
}
if ($message->getCommand() == 'LOG') {
$this->log('[LOG] '.$message->getBody());
}
foreach ($this->handlers as $handler) {
try {
$handler->receiveMessage($message);
} catch (Exception $ex) {
phlog($ex);
}
}
}
public function getAdapter() {
return $this->protocolAdapter;
}
public function getConduit() {
if (empty($this->conduit)) {
throw new Exception(
pht(
"This bot is not configured with a Conduit uplink. Set '%s' and ".
"'%s' in the configuration to connect.",
'conduit.uri',
'conduit.token'));
}
return $this->conduit;
}
}

View file

@ -1,52 +0,0 @@
<?php
final class PhabricatorBotMessage extends Phobject {
private $sender;
private $command;
private $body;
private $target;
private $public;
public function __construct() {
// By default messages are public
$this->public = true;
}
public function setSender(PhabricatorBotTarget $sender = null) {
$this->sender = $sender;
return $this;
}
public function getSender() {
return $this->sender;
}
public function setCommand($command) {
$this->command = $command;
return $this;
}
public function getCommand() {
return $this->command;
}
public function setBody($body) {
$this->body = $body;
return $this;
}
public function getBody() {
return $this->body;
}
public function setTarget(PhabricatorBotTarget $target = null) {
$this->target = $target;
return $this;
}
public function getTarget() {
return $this->target;
}
}

View file

@ -1,93 +0,0 @@
<?php
final class PhabricatorBotFlowdockProtocolAdapter
extends PhabricatorStreamingProtocolAdapter {
public function getServiceType() {
return 'Flowdock';
}
protected function buildStreamingUrl($channel) {
$organization = $this->getConfig('flowdock.organization');
if (empty($organization)) {
$this->getConfig('organization');
}
if (empty($organization)) {
throw new Exception(
'"flowdock.organization" configuration variable not set');
}
$ssl = $this->getConfig('ssl');
$url = ($ssl) ? 'https://' : 'http://';
$url .= "{$this->authtoken}@stream.flowdock.com";
$url .= "/flows/{$organization}/{$channel}";
return $url;
}
protected function processMessage(array $m_obj) {
$command = null;
switch ($m_obj['event']) {
case 'message':
$command = 'MESSAGE';
break;
default:
// For now, ignore anything which we don't otherwise know about.
break;
}
if ($command === null) {
return false;
}
// TODO: These should be usernames, not user IDs.
$sender = id(new PhabricatorBotUser())
->setName($m_obj['user']);
$target = id(new PhabricatorBotChannel())
->setName($m_obj['flow']);
return id(new PhabricatorBotMessage())
->setCommand($command)
->setSender($sender)
->setTarget($target)
->setBody($m_obj['content']);
}
public function writeMessage(PhabricatorBotMessage $message) {
switch ($message->getCommand()) {
case 'MESSAGE':
$this->speak(
$message->getBody(),
$message->getTarget());
break;
}
}
private function speak(
$body,
PhabricatorBotTarget $flow) {
// The $flow->getName() returns the flow's UUID,
// as such, the Flowdock API does not require the organization
// to be specified in the URI
$this->performPost(
'/messages',
array(
'flow' => $flow->getName(),
'event' => 'message',
'content' => $body,
));
}
public function __destruct() {
if ($this->readHandles) {
foreach ($this->readHandles as $read_handle) {
curl_multi_remove_handle($this->multiHandle, $read_handle);
curl_close($read_handle);
}
}
curl_multi_close($this->multiHandle);
}
}

View file

@ -1,114 +0,0 @@
<?php
final class PhabricatorCampfireProtocolAdapter
extends PhabricatorStreamingProtocolAdapter {
public function getServiceType() {
return 'Campfire';
}
protected function buildStreamingUrl($channel) {
$ssl = $this->getConfig('ssl');
$url = ($ssl) ? 'https://' : 'http://';
$url .= "streaming.campfirenow.com/room/{$channel}/live.json";
return $url;
}
protected function processMessage(array $m_obj) {
$command = null;
switch ($m_obj['type']) {
case 'TextMessage':
$command = 'MESSAGE';
break;
case 'PasteMessage':
$command = 'PASTE';
break;
default:
// For now, ignore anything which we don't otherwise know about.
break;
}
if ($command === null) {
return false;
}
// TODO: These should be usernames, not user IDs.
$sender = id(new PhabricatorBotUser())
->setName($m_obj['user_id']);
$target = id(new PhabricatorBotChannel())
->setName($m_obj['room_id']);
return id(new PhabricatorBotMessage())
->setCommand($command)
->setSender($sender)
->setTarget($target)
->setBody($m_obj['body']);
}
public function writeMessage(PhabricatorBotMessage $message) {
switch ($message->getCommand()) {
case 'MESSAGE':
$this->speak(
$message->getBody(),
$message->getTarget());
break;
case 'SOUND':
$this->speak(
$message->getBody(),
$message->getTarget(),
'SoundMessage');
break;
case 'PASTE':
$this->speak(
$message->getBody(),
$message->getTarget(),
'PasteMessage');
break;
}
}
protected function joinRoom($room_id) {
$this->performPost("/room/{$room_id}/join.json");
$this->inRooms[$room_id] = true;
}
private function leaveRoom($room_id) {
$this->performPost("/room/{$room_id}/leave.json");
unset($this->inRooms[$room_id]);
}
private function speak(
$message,
PhabricatorBotTarget $channel,
$type = 'TextMessage') {
$room_id = $channel->getName();
$this->performPost(
"/room/{$room_id}/speak.json",
array(
'message' => array(
'type' => $type,
'body' => $message,
),
));
}
public function __destruct() {
foreach ($this->inRooms as $room_id => $ignored) {
$this->leaveRoom($room_id);
}
if ($this->readHandles) {
foreach ($this->readHandles as $read_handle) {
curl_multi_remove_handle($this->multiHandle, $read_handle);
curl_close($read_handle);
}
}
curl_multi_close($this->multiHandle);
}
}

View file

@ -1,282 +0,0 @@
<?php
final class PhabricatorIRCProtocolAdapter extends PhabricatorProtocolAdapter {
private $socket;
private $writeBuffer;
private $readBuffer;
private $nickIncrement = 0;
public function getServiceType() {
return 'IRC';
}
public function getServiceName() {
return $this->getConfig('network', $this->getConfig('server'));
}
// Hash map of command translations
public static $commandTranslations = array(
'PRIVMSG' => 'MESSAGE',
);
public function connect() {
$nick = $this->getConfig('nick', 'phabot');
$server = $this->getConfig('server');
$port = $this->getConfig('port', 6667);
$pass = $this->getConfig('pass');
$ssl = $this->getConfig('ssl', false);
$user = $this->getConfig('user', $nick);
if (!preg_match('/^[A-Za-z0-9_`[{}^|\]\\-]+$/', $nick)) {
throw new Exception(
pht(
"Nickname '%s' is invalid!",
$nick));
}
$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(pht('Failed to connect, #%d: %s', $errno, $error));
}
$ok = stream_set_blocking($socket, false);
if (!$ok) {
throw new Exception(pht('Failed to set stream nonblocking.'));
}
$this->socket = $socket;
if ($pass) {
$this->write("PASS {$pass}");
}
$this->write("NICK {$nick}");
$this->write("USER {$user} 0 * :{$user}");
}
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) {
// We may have been interrupted by a signal, like a SIGINT. Try
// selecting again. If the second select works, conclude that the failure
// was most likely because we were signaled.
$ok = @stream_select($read, $write, $except, $timeout_sec = 0);
if ($ok === false) {
throw new Exception(pht('%s failed!', 'stream_select()'));
}
}
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(pht('Remote host closed connection.'));
}
do {
$data = fread($this->socket, 4096);
if ($data === false) {
throw new Exception(pht('%s failed!', 'fread()'));
} else {
$messages[] = id(new PhabricatorBotMessage())
->setCommand('LOG')
->setBody('>>> '.$data);
$this->readBuffer .= $data;
}
} while (strlen($data));
}
if ($write) {
do {
$len = fwrite($this->socket, $this->writeBuffer);
if ($len === false) {
throw new Exception(pht('%s failed!', 'fwrite()'));
} else if ($len === 0) {
break;
} else {
$messages[] = id(new PhabricatorBotMessage())
->setCommand('LOG')
->setBody('>>> '.substr($this->writeBuffer, 0, $len));
$this->writeBuffer = substr($this->writeBuffer, $len);
}
} while (strlen($this->writeBuffer));
}
while (($m = $this->processReadBuffer()) !== false) {
if ($m !== null) {
$messages[] = $m;
}
}
return $messages;
}
private function write($message) {
$this->writeBuffer .= $message."\r\n";
return $this;
}
public function writeMessage(PhabricatorBotMessage $message) {
switch ($message->getCommand()) {
case 'MESSAGE':
case 'PASTE':
$name = $message->getTarget()->getName();
$body = $message->getBody();
$this->write("PRIVMSG {$name} :{$body}");
return true;
default:
return false;
}
}
private function processReadBuffer() {
$until = strpos($this->readBuffer, "\r\n");
if ($until === false) {
return false;
}
$message = substr($this->readBuffer, 0, $until);
$this->readBuffer = substr($this->readBuffer, $until + 2);
$pattern =
'/^'.
'(?::(?P<sender>(\S+?))(?:!\S*)? )?'. // This may not be present.
'(?P<command>[A-Z0-9]+) '.
'(?P<data>.*)'.
'$/';
$matches = null;
if (!preg_match($pattern, $message, $matches)) {
throw new Exception("Unexpected message from server: {$message}");
}
if ($this->handleIRCProtocol($matches)) {
return null;
}
$command = $this->getBotCommand($matches['command']);
list($target, $body) = $this->parseMessageData($command, $matches['data']);
if (!strlen($matches['sender'])) {
$sender = null;
} else {
$sender = id(new PhabricatorBotUser())
->setName($matches['sender']);
}
$bot_message = id(new PhabricatorBotMessage())
->setSender($sender)
->setCommand($command)
->setTarget($target)
->setBody($body);
return $bot_message;
}
private function handleIRCProtocol(array $matches) {
$data = $matches['data'];
switch ($matches['command']) {
case '433': // Nickname already in use
// If we receive this error, try appending "-1", "-2", etc. to the nick
$this->nickIncrement++;
$nick = $this->getConfig('nick', 'phabot').'-'.$this->nickIncrement;
$this->write("NICK {$nick}");
return true;
case '422': // Error - no MOTD
case '376': // End of MOTD
$nickpass = $this->getConfig('nickpass');
if ($nickpass) {
$this->write("PRIVMSG nickserv :IDENTIFY {$nickpass}");
}
$join = $this->getConfig('join');
if (!$join) {
throw new Exception(pht('Not configured to join any channels!'));
}
foreach ($join as $channel) {
$this->write("JOIN {$channel}");
}
return true;
case 'PING':
$this->write("PONG {$data}");
return true;
}
return false;
}
private function getBotCommand($irc_command) {
if (isset(self::$commandTranslations[$irc_command])) {
return self::$commandTranslations[$irc_command];
}
// We have no translation for this command, use as-is
return $irc_command;
}
private function parseMessageData($command, $data) {
switch ($command) {
case 'MESSAGE':
$matches = null;
if (preg_match('/^(\S+)\s+:?(.*)$/', $data, $matches)) {
$target_name = $matches[1];
if (strncmp($target_name, '#', 1) === 0) {
$target = id(new PhabricatorBotChannel())
->setName($target_name);
} else {
$target = id(new PhabricatorBotUser())
->setName($target_name);
}
return array(
$target,
rtrim($matches[2], "\r\n"),
);
}
break;
}
// By default we assume there is no target, only a body
return array(
null,
$data,
);
}
public function disconnect() {
// NOTE: FreeNode doesn't show quit messages if you've recently joined a
// channel, presumably to prevent some kind of abuse. If you're testing
// this, you may need to stay connected to the network for a few minutes
// before it works. If you disconnect too quickly, the server will replace
// your message with a "Client Quit" message.
$quit = $this->getConfig('quit', pht('Shutting down.'));
$this->write("QUIT :{$quit}");
// Flush the write buffer.
while (strlen($this->writeBuffer)) {
$this->getNextMessages(0);
}
@fclose($this->socket);
$this->socket = null;
}
}

View file

@ -1,62 +0,0 @@
<?php
/**
* Defines the api for protocol adapters for @{class:PhabricatorBot}
*/
abstract class PhabricatorProtocolAdapter extends Phobject {
private $config;
public function setConfig($config) {
$this->config = $config;
return $this;
}
public function getConfig($key, $default = null) {
return idx($this->config, $key, $default);
}
/**
* Performs any connection logic necessary for the protocol
*/
abstract public function connect();
/**
* Disconnect from the service.
*/
public function disconnect() {
return;
}
/**
* This is the spout for messages coming in from the protocol.
* This will be called in the main event loop of the bot daemon
* So if if doesn't implement some sort of blocking timeout
* (e.g. select-based socket polling), it should at least sleep
* for some period of time in order to not overwhelm the processor.
*
* @param Int $poll_frequency The number of seconds between polls
*/
abstract public function getNextMessages($poll_frequency);
/**
* This is the output mechanism for the protocol.
*
* @param PhabricatorBotMessage $message The message to write
*/
abstract public function writeMessage(PhabricatorBotMessage $message);
/**
* String identifying the service type the adapter provides access to, like
* "irc", "campfire", "flowdock", "hipchat", etc.
*/
abstract public function getServiceType();
/**
* String identifying the service name the adapter is connecting to. This is
* used to distinguish between instances of a service. For example, for IRC,
* this should return the IRC network the client is connecting to.
*/
abstract public function getServiceName();
}

View file

@ -1,170 +0,0 @@
<?php
abstract class PhabricatorStreamingProtocolAdapter
extends PhabricatorProtocolAdapter {
protected $readHandles;
protected $multiHandle;
protected $authtoken;
protected $inRooms = array();
private $readBuffers;
private $server;
private $active;
public function getServiceName() {
$uri = new PhutilURI($this->server);
return $uri->getDomain();
}
public function connect() {
$this->server = $this->getConfig('server');
$this->authtoken = $this->getConfig('authtoken');
$rooms = $this->getConfig('join');
// First, join the room
if (!$rooms) {
throw new Exception(pht('Not configured to join any rooms!'));
}
$this->readBuffers = array();
// Set up our long poll in a curl multi request so we can
// continue running while it executes in the background
$this->multiHandle = curl_multi_init();
$this->readHandles = array();
foreach ($rooms as $room_id) {
$this->joinRoom($room_id);
// Set up the curl stream for reading
$url = $this->buildStreamingUrl($room_id);
$ch = $this->readHandles[$url] = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
curl_setopt(
$ch,
CURLOPT_USERPWD,
$this->authtoken.':x');
curl_setopt(
$ch,
CURLOPT_HTTPHEADER,
array('Content-type: application/json'));
curl_setopt(
$ch,
CURLOPT_WRITEFUNCTION,
array($this, 'read'));
curl_setopt($ch, CURLOPT_BUFFERSIZE, 128);
curl_setopt($ch, CURLOPT_TIMEOUT, 0);
curl_multi_add_handle($this->multiHandle, $ch);
// Initialize read buffer
$this->readBuffers[$url] = '';
}
$this->active = null;
$this->blockingMultiExec();
}
protected function joinRoom($room_id) {
// Optional hook, by default, do nothing
}
// This is our callback for the background curl multi-request.
// Puts the data read in on the readBuffer for processing.
private function read($ch, $data) {
$info = curl_getinfo($ch);
$length = strlen($data);
$this->readBuffers[$info['url']] .= $data;
return $length;
}
private function blockingMultiExec() {
do {
$status = curl_multi_exec($this->multiHandle, $this->active);
} while ($status == CURLM_CALL_MULTI_PERFORM);
// Check for errors
if ($status != CURLM_OK) {
throw new Exception(
pht('Phabricator Bot had a problem reading from stream.'));
}
}
public function getNextMessages($poll_frequency) {
$messages = array();
if (!$this->active) {
throw new Exception(pht('Phabricator Bot stopped reading from stream.'));
}
// Prod our http request
curl_multi_select($this->multiHandle, $poll_frequency);
$this->blockingMultiExec();
// Process anything waiting on the read buffer
while ($m = $this->processReadBuffer()) {
$messages[] = $m;
}
return $messages;
}
private function processReadBuffer() {
foreach ($this->readBuffers as $url => &$buffer) {
$until = strpos($buffer, "}\r");
if ($until == false) {
continue;
}
$message = substr($buffer, 0, $until + 1);
$buffer = substr($buffer, $until + 2);
$m_obj = phutil_json_decode($message);
if ($message = $this->processMessage($m_obj)) {
return $message;
}
}
// If we're here, there's nothing to process
return false;
}
protected function performPost($endpoint, $data = null) {
$uri = new PhutilURI($this->server);
$uri->setPath($endpoint);
$payload = json_encode($data);
list($output) = id(new HTTPSFuture($uri))
->setMethod('POST')
->addHeader('Content-Type', 'application/json')
->addHeader('Authorization', $this->getAuthorizationHeader())
->setData($payload)
->resolvex();
$output = trim($output);
if (strlen($output)) {
return phutil_json_decode($output);
}
return true;
}
protected function getAuthorizationHeader() {
return 'Basic '.$this->getEncodedAuthToken();
}
protected function getEncodedAuthToken() {
return base64_encode($this->authtoken.':x');
}
abstract protected function buildStreamingUrl($channel);
abstract protected function processMessage(array $raw_object);
}

View file

@ -1,17 +0,0 @@
<?php
/**
* Logs messages to stdout.
*/
final class PhabricatorBotDebugLogHandler extends PhabricatorBotHandler {
public function receiveMessage(PhabricatorBotMessage $message) {
switch ($message->getCommand()) {
case 'LOG':
echo addcslashes(
$message->getBody(),
"\0..\37\177..\377");
echo "\n";
break;
}
}
}

View file

@ -1,180 +0,0 @@
<?php
/**
* Watches the feed and puts notifications into channel(s) of choice.
*/
final class PhabricatorBotFeedNotificationHandler
extends PhabricatorBotHandler {
private $startupDelay = 30;
private $lastSeenChronoKey = 0;
private $typesNeedURI = array('DREV', 'TASK');
private function shouldShowStory($story) {
$story_objectphid = $story['objectPHID'];
$story_text = $story['text'];
$show = $this->getConfig('notification.types');
if ($show) {
$obj_type = phid_get_type($story_objectphid);
if (!in_array(strtolower($obj_type), $show)) {
return false;
}
}
$verbosity = $this->getConfig('notification.verbosity', 3);
$verbs = array();
switch ($verbosity) {
case 2:
$verbs[] = array(
'commented',
'added',
'changed',
'resigned',
'explained',
'modified',
'attached',
'edited',
'joined',
'left',
'removed',
);
// fallthrough
case 1:
$verbs[] = array(
'updated',
'accepted',
'requested',
'planned',
'claimed',
'summarized',
'commandeered',
'assigned',
);
// fallthrough
case 0:
$verbs[] = array(
'created',
'closed',
'raised',
'committed',
'abandoned',
'reclaimed',
'reopened',
'deleted',
);
break;
case 3:
default:
return true;
}
$verbs = '/('.implode('|', array_mergev($verbs)).')/';
if (preg_match($verbs, $story_text)) {
return true;
}
return false;
}
public function receiveMessage(PhabricatorBotMessage $message) {
return;
}
public function runBackgroundTasks() {
if ($this->startupDelay > 0) {
// the event loop runs every 1s so delay enough to fully conenct
$this->startupDelay--;
return;
}
if ($this->lastSeenChronoKey == 0) {
// Since we only want to post notifications about new stories, skip
// everything that's happened in the past when we start up so we'll
// only process real-time stories.
$latest = $this->getConduit()->callMethodSynchronous(
'feed.query',
array(
'limit' => 1,
));
foreach ($latest as $story) {
if ($story['chronologicalKey'] > $this->lastSeenChronoKey) {
$this->lastSeenChronoKey = $story['chronologicalKey'];
}
}
return;
}
$config_max_pages = $this->getConfig('notification.max_pages', 5);
$config_page_size = $this->getConfig('notification.page_size', 10);
$last_seen_chrono_key = $this->lastSeenChronoKey;
$chrono_key_cursor = 0;
// Not efficient but works due to feed.query API
for ($max_pages = $config_max_pages; $max_pages > 0; $max_pages--) {
$stories = $this->getConduit()->callMethodSynchronous(
'feed.query',
array(
'limit' => $config_page_size,
'after' => $chrono_key_cursor,
'view' => 'text',
));
foreach ($stories as $story) {
if ($story['chronologicalKey'] == $last_seen_chrono_key) {
// Caught up on feed
return;
}
if ($story['chronologicalKey'] > $this->lastSeenChronoKey) {
// Keep track of newest seen story
$this->lastSeenChronoKey = $story['chronologicalKey'];
}
if (!$chrono_key_cursor ||
$story['chronologicalKey'] < $chrono_key_cursor) {
// Keep track of oldest story on this page
$chrono_key_cursor = $story['chronologicalKey'];
}
if (!$story['text'] ||
!$this->shouldShowStory($story)) {
continue;
}
$message = $story['text'];
$story_object_type = phid_get_type($story['objectPHID']);
if (in_array($story_object_type, $this->typesNeedURI)) {
$objects = $this->getConduit()->callMethodSynchronous(
'phid.lookup',
array(
'names' => array($story['objectPHID']),
));
$message .= ' '.$objects[$story['objectPHID']]['uri'];
}
$channels = $this->getConfig('join');
foreach ($channels as $channel_name) {
$channel = id(new PhabricatorBotChannel())
->setName($channel_name);
$this->writeMessage(
id(new PhabricatorBotMessage())
->setCommand('MESSAGE')
->setTarget($channel)
->setBody($message));
}
}
}
}
}

View file

@ -1,72 +0,0 @@
<?php
/**
* Responds to IRC messages. You plug a bunch of these into a
* @{class:PhabricatorBot} to give it special behavior.
*/
abstract class PhabricatorBotHandler extends Phobject {
private $bot;
final public function __construct(PhabricatorBot $irc_bot) {
$this->bot = $irc_bot;
}
final protected function writeMessage(PhabricatorBotMessage $message) {
$this->bot->writeMessage($message);
return $this;
}
final protected function getConduit() {
return $this->bot->getConduit();
}
final protected function getConfig($key, $default = null) {
return $this->bot->getConfig($key, $default);
}
final protected function getURI($path) {
$base_uri = new PhutilURI($this->bot->getConfig('conduit.uri'));
$base_uri->setPath($path);
return (string)$base_uri;
}
final protected function getServiceName() {
return $this->bot->getAdapter()->getServiceName();
}
final protected function getServiceType() {
return $this->bot->getAdapter()->getServiceType();
}
abstract public function receiveMessage(PhabricatorBotMessage $message);
public function runBackgroundTasks() {
return;
}
public function replyTo(PhabricatorBotMessage $original_message, $body) {
if ($original_message->getCommand() != 'MESSAGE') {
throw new Exception(
pht('Handler is trying to reply to something which is not a message!'));
}
$reply = id(new PhabricatorBotMessage())
->setCommand('MESSAGE');
if ($original_message->getTarget()->isPublic()) {
// This is a public target, like a chatroom. Send the response to the
// chatroom.
$reply->setTarget($original_message->getTarget());
} else {
// This is a private target, like a private message. Send the response
// back to the sender (presumably, we are the target).
$reply->setTarget($original_message->getSender());
}
$reply->setBody($body);
return $this->writeMessage($reply);
}
}

View file

@ -1,77 +0,0 @@
<?php
/**
* Logs chatter.
*/
final class PhabricatorBotLogHandler extends PhabricatorBotHandler {
private $futures = array();
public function receiveMessage(PhabricatorBotMessage $message) {
switch ($message->getCommand()) {
case 'MESSAGE':
$target = $message->getTarget();
if (!$target->isPublic()) {
// Don't log private messages, although maybe we should for debugging?
break;
}
$target_name = $target->getName();
$logs = array(
array(
'channel' => $target_name,
'type' => 'mesg',
'epoch' => time(),
'author' => $message->getSender()->getName(),
'message' => $message->getBody(),
'serviceName' => $this->getServiceName(),
'serviceType' => $this->getServiceType(),
),
);
$this->futures[] = $this->getConduit()->callMethod(
'chatlog.record',
array(
'logs' => $logs,
));
$prompts = array(
'/where is the (chat)?log\?/i',
'/where am i\?/i',
'/what year is (this|it)\?/i',
);
$tell = false;
foreach ($prompts as $prompt) {
if (preg_match($prompt, $message->getBody())) {
$tell = true;
break;
}
}
if ($tell) {
$response = $this->getURI(
'/chatlog/channel/'.phutil_escape_uri($target_name).'/');
$this->replyTo($message, $response);
}
break;
}
}
public function runBackgroundTasks() {
foreach ($this->futures as $key => $future) {
try {
if ($future->isReady()) {
unset($this->futures[$key]);
}
} catch (Exception $ex) {
unset($this->futures[$key]);
phlog($ex);
}
}
}
}

View file

@ -1,176 +0,0 @@
<?php
final class PhabricatorBotMacroHandler extends PhabricatorBotHandler {
private $macros;
private $regexp;
private $next = 0;
private function init() {
if ($this->macros === false) {
return false;
}
if ($this->macros !== null) {
return true;
}
$macros = $this->getConduit()->callMethodSynchronous(
'macro.query',
array());
// If we have no macros, cache `false` (meaning "no macros") and return
// immediately.
if (!$macros) {
$this->macros = false;
return false;
}
$regexp = array();
foreach ($macros as $macro_name => $macro) {
$regexp[] = preg_quote($macro_name, '/');
}
$regexp = '/^('.implode('|', $regexp).')\z/';
$this->macros = $macros;
$this->regexp = $regexp;
return true;
}
public function receiveMessage(PhabricatorBotMessage $message) {
if (!$this->init()) {
return;
}
switch ($message->getCommand()) {
case 'MESSAGE':
$message_body = $message->getBody();
$matches = null;
if (!preg_match($this->regexp, trim($message_body), $matches)) {
return;
}
$macro = $matches[1];
$ascii = idx($this->macros[$macro], 'ascii');
if ($ascii === false) {
return;
}
if (!$ascii) {
$this->macros[$macro]['ascii'] = $this->rasterize(
$this->macros[$macro],
$this->getConfig('macro.size', 48),
$this->getConfig('macro.aspect', 0.66));
$ascii = $this->macros[$macro]['ascii'];
}
if ($ascii === false) {
// If we failed to rasterize the macro, bail out.
return;
}
$target_name = $message->getTarget()->getName();
foreach ($ascii as $line) {
$this->replyTo($message, $line);
}
break;
}
}
public function rasterize($macro, $size, $aspect) {
try {
$image = $this->getConduit()->callMethodSynchronous(
'file.download',
array(
'phid' => $macro['filePHID'],
));
$image = base64_decode($image);
} catch (Exception $ex) {
return false;
}
if (!$image) {
return false;
}
$img = @imagecreatefromstring($image);
if (!$img) {
return false;
}
$sx = imagesx($img);
$sy = imagesy($img);
if ($sx > $size || $sy > $size) {
$scale = max($sx, $sy) / $size;
$dx = floor($sx / $scale);
$dy = floor($sy / $scale);
} else {
$dx = $sx;
$dy = $sy;
}
$dy = floor($dy * $aspect);
$dst = imagecreatetruecolor($dx, $dy);
if (!$dst) {
return false;
}
imagealphablending($dst, false);
$ok = imagecopyresampled(
$dst, $img,
0, 0,
0, 0,
$dx, $dy,
$sx, $sy);
if (!$ok) {
return false;
}
$map = array(
' ',
'.',
',',
':',
';',
'!',
'|',
'*',
'=',
'@',
'$',
'#',
);
$lines = array();
for ($ii = 0; $ii < $dy; $ii++) {
$buf = '';
for ($jj = 0; $jj < $dx; $jj++) {
$c = imagecolorat($dst, $jj, $ii);
$a = ($c >> 24) & 0xFF;
$r = ($c >> 16) & 0xFF;
$g = ($c >> 8) & 0xFF;
$b = ($c) & 0xFF;
$luma = (255 - ((0.30 * $r) + (0.59 * $g) + (0.11 * $b))) / 256;
$luma *= ((127 - $a) / 127);
$char = $map[max(0, floor($luma * count($map)))];
$buf .= $char;
}
$lines[] = $buf;
}
return $lines;
}
}

View file

@ -1,206 +0,0 @@
<?php
/**
* Looks for Dxxxx, Txxxx and links to them.
*/
final class PhabricatorBotObjectNameHandler extends PhabricatorBotHandler {
/**
* Map of PHIDs to the last mention of them (as an epoch timestamp); prevents
* us from spamming chat when a single object is discussed.
*/
private $recentlyMentioned = array();
public function receiveMessage(PhabricatorBotMessage $original_message) {
switch ($original_message->getCommand()) {
case 'MESSAGE':
$message = $original_message->getBody();
$matches = null;
$paste_ids = array();
$commit_names = array();
$vote_ids = array();
$file_ids = array();
$object_names = array();
$output = array();
$pattern =
'@'.
'(?<![/:#-])(?:^|\b)'.
'(R2D2)'.
'(?:\b|$)'.
'@';
if (preg_match_all($pattern, $message, $matches, PREG_SET_ORDER)) {
foreach ($matches as $match) {
switch ($match[1]) {
case 'R2D2':
$output[$match[1]] = pht('beep boop bop');
break;
}
}
}
// Use a negative lookbehind to prevent matching "/D123", "#D123",
// ":D123", etc.
$pattern =
'@'.
'(?<![/:#-])(?:^|\b)'.
'([A-Z])(\d+)'.
'(?:\b|$)'.
'@';
$regex = trim(
PhabricatorEnv::getEnvConfig('remarkup.ignored-object-names'));
if (preg_match_all($pattern, $message, $matches, PREG_SET_ORDER)) {
foreach ($matches as $match) {
if ($regex && preg_match($regex, $match[0])) {
continue;
}
switch ($match[1]) {
case 'P':
$paste_ids[] = $match[2];
break;
case 'V':
$vote_ids[] = $match[2];
break;
case 'F':
$file_ids[] = $match[2];
break;
default:
$name = $match[1].$match[2];
switch ($name) {
case 'T1000':
$output[$name] = pht(
'T1000: A mimetic poly-alloy assassin controlled by '.
'Skynet');
break;
default:
$object_names[] = $name;
break;
}
break;
}
}
}
$pattern =
'@'.
'(?<!/)(?:^|\b)'.
'(r[A-Z]+)([0-9a-z]{0,40})'.
'(?:\b|$)'.
'@';
if (preg_match_all($pattern, $message, $matches, PREG_SET_ORDER)) {
foreach ($matches as $match) {
if ($match[2]) {
$commit_names[] = $match[1].$match[2];
} else {
$object_names[] = $match[1];
}
}
}
if ($object_names) {
$objects = $this->getConduit()->callMethodSynchronous(
'phid.lookup',
array(
'names' => $object_names,
));
foreach ($objects as $object) {
$output[$object['phid']] = $object['fullName'].' - '.$object['uri'];
}
}
if ($vote_ids) {
foreach ($vote_ids as $vote_id) {
$vote = $this->getConduit()->callMethodSynchronous(
'slowvote.info',
array(
'poll_id' => $vote_id,
));
$output[$vote['phid']] = 'V'.$vote['id'].': '.$vote['question'].
' '.pht('Come Vote').' '.$vote['uri'];
}
}
if ($file_ids) {
foreach ($file_ids as $file_id) {
$file = $this->getConduit()->callMethodSynchronous(
'file.info',
array(
'id' => $file_id,
));
$output[$file['phid']] = $file['objectName'].': '.
$file['uri'].' - '.$file['name'];
}
}
if ($paste_ids) {
foreach ($paste_ids as $paste_id) {
$paste = $this->getConduit()->callMethodSynchronous(
'paste.query',
array(
'ids' => array($paste_id),
));
$paste = head($paste);
$output[$paste['phid']] = 'P'.$paste['id'].': '.$paste['uri'].' - '.
$paste['title'];
if ($paste['language']) {
$output[$paste['phid']] .= ' ('.$paste['language'].')';
}
$user = $this->getConduit()->callMethodSynchronous(
'user.query',
array(
'phids' => array($paste['authorPHID']),
));
$user = head($user);
if ($user) {
$output[$paste['phid']] .= ' by '.$user['userName'];
}
}
}
if ($commit_names) {
$commits = $this->getConduit()->callMethodSynchronous(
'diffusion.querycommits',
array(
'names' => $commit_names,
));
foreach ($commits['data'] as $commit) {
$output[$commit['phid']] = $commit['uri'];
}
}
foreach ($output as $phid => $description) {
// Don't mention the same object more than once every 10 minutes
// in public channels, so we avoid spamming the chat over and over
// again for discussions of a specific revision, for example.
$target_name = $original_message->getTarget()->getName();
if (empty($this->recentlyMentioned[$target_name])) {
$this->recentlyMentioned[$target_name] = array();
}
$quiet_until = idx(
$this->recentlyMentioned[$target_name],
$phid,
0) + (60 * 10);
if (time() < $quiet_until) {
// Remain quiet on this channel.
continue;
}
$this->recentlyMentioned[$target_name][$phid] = time();
$this->replyTo($original_message, $description);
}
break;
}
}
}

View file

@ -1,50 +0,0 @@
<?php
/**
* Watches for "where is <symbol>?"
*/
final class PhabricatorBotSymbolHandler extends PhabricatorBotHandler {
public function receiveMessage(PhabricatorBotMessage $message) {
switch ($message->getCommand()) {
case 'MESSAGE':
$text = $message->getBody();
$matches = null;
if (!preg_match('/where(?: in the world)? is (\S+?)\?/i',
$text, $matches)) {
break;
}
$symbol = $matches[1];
$results = $this->getConduit()->callMethodSynchronous(
'diffusion.findsymbols',
array(
'name' => $symbol,
));
$default_uri = $this->getURI('/diffusion/symbol/'.$symbol.'/');
if (count($results) > 1) {
$response = pht(
"Multiple symbols named '%s': %s",
$symbol,
$default_uri);
} else if (count($results) == 1) {
$result = head($results);
$response =
$result['type'].' '.
$result['name'].' '.
'('.$result['language'].'): '.
nonempty($result['uri'], $default_uri);
} else {
$response = pht("No symbol '%s' found anywhere.", $symbol);
}
$this->replyTo($message, $response);
break;
}
}
}

View file

@ -1,43 +0,0 @@
<?php
/**
* Responds to "Whats new?" with some recent feed content.
*/
final class PhabricatorBotWhatsNewHandler extends PhabricatorBotHandler {
private $floodblock = 0;
public function receiveMessage(PhabricatorBotMessage $message) {
switch ($message->getCommand()) {
case 'MESSAGE':
$message_body = $message->getBody();
$now = time();
$prompt = '~what( i|\')?s new\?~i';
if (preg_match($prompt, $message_body)) {
if ($now < $this->floodblock) {
return;
}
$this->floodblock = $now + 60;
$this->reportNew($message);
}
break;
}
}
public function reportNew(PhabricatorBotMessage $message) {
$latest = $this->getConduit()->callMethodSynchronous(
'feed.query',
array(
'limit' => 5,
'view' => 'text',
));
foreach ($latest as $feed_item) {
if (isset($feed_item['text'])) {
$this->replyTo($message, html_entity_decode($feed_item['text']));
}
}
}
}

View file

@ -1,12 +0,0 @@
<?php
/**
* Represents a group/public space, like an IRC channel or a Campfire room.
*/
final class PhabricatorBotChannel extends PhabricatorBotTarget {
public function isPublic() {
return true;
}
}

View file

@ -1,22 +0,0 @@
<?php
/**
* Represents something which can be the target of messages, like a user or
* channel.
*/
abstract class PhabricatorBotTarget extends Phobject {
private $name;
public function setName($name) {
$this->name = $name;
return $this;
}
public function getName() {
return $this->name;
}
abstract public function isPublic();
}

View file

@ -1,12 +0,0 @@
<?php
/**
* Represents an individual user.
*/
final class PhabricatorBotUser extends PhabricatorBotTarget {
public function isPublic() {
return false;
}
}