mirror of
https://we.phorge.it/source/phorge.git
synced 2024-11-29 10:12:41 +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:
parent
ede23efcc7
commit
5c1e4488de
20 changed files with 0 additions and 1933 deletions
|
@ -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',
|
||||
|
|
|
@ -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.
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
||||
}
|
|
@ -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);
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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']));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Represents an individual user.
|
||||
*/
|
||||
final class PhabricatorBotUser extends PhabricatorBotTarget {
|
||||
|
||||
public function isPublic() {
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in a new issue