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