Page MenuHomestyx hydra

No OneTemporary

diff --git a/src/infrastructure/daemon/bot/PhabricatorBot.php b/src/infrastructure/daemon/bot/PhabricatorBot.php
index bdc2092eaf..667c90f4f0 100644
--- a/src/infrastructure/daemon/bot/PhabricatorBot.php
+++ b/src/infrastructure/daemon/bot/PhabricatorBot.php
@@ -1,148 +1,151 @@
<?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;
public function run() {
$argv = $this->getArgv();
if (count($argv) !== 1) {
throw new Exception('usage: PhabricatorBot <json_config_file>');
}
$json_raw = Filesystem::readFile($argv[0]);
$config = json_decode($json_raw, true);
if (!is_array($config)) {
throw new Exception("File '{$argv[0]}' is not valid JSON!");
}
$nick = idx($config, 'nick', 'phabot');
$handlers = idx($config, 'handlers', array());
$protocol_adapter_class = idx(
$config,
'protocol-adapter',
'PhabricatorIRCProtocolAdapter');
$this->pollFrequency = idx($config, 'poll-frequency', 1);
$this->config = $config;
foreach ($handlers as $handler) {
$obj = newv($handler, array($this));
$this->handlers[] = $obj;
}
$ca_bundle = idx($config, 'https.cabundle');
if ($ca_bundle) {
HTTPSFuture::setGlobalCABundleFromPath($ca_bundle);
}
$conduit_uri = idx($config, 'conduit.uri');
if ($conduit_uri) {
$conduit_user = idx($config, 'conduit.user');
$conduit_cert = idx($config, 'conduit.cert');
// Normalize the path component of the URI so users can enter the
// domain without the "/api/" part.
$conduit_uri = new PhutilURI($conduit_uri);
$conduit_host = (string)$conduit_uri->setPath('/');
$conduit_uri = (string)$conduit_uri->setPath('/api/');
$conduit = new ConduitClient($conduit_uri);
$response = $conduit->callMethodSynchronous(
'conduit.connect',
array(
'client' => 'PhabricatorBot',
'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 {
$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(
"This bot is not configured with a Conduit uplink. Set 'conduit.uri', ".
"'conduit.user' and 'conduit.cert' in the configuration to connect.");
}
return $this->conduit;
}
}
diff --git a/src/infrastructure/daemon/bot/adapter/PhabricatorBaseProtocolAdapter.php b/src/infrastructure/daemon/bot/adapter/PhabricatorBaseProtocolAdapter.php
index 4a8a62d092..e50edea7be 100644
--- a/src/infrastructure/daemon/bot/adapter/PhabricatorBaseProtocolAdapter.php
+++ b/src/infrastructure/daemon/bot/adapter/PhabricatorBaseProtocolAdapter.php
@@ -1,55 +1,62 @@
<?php
/**
* Defines the api for protocol adapters for @{class:PhabricatorBot}
*/
abstract class PhabricatorBaseProtocolAdapter {
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();
}
diff --git a/src/infrastructure/daemon/bot/adapter/PhabricatorIRCProtocolAdapter.php b/src/infrastructure/daemon/bot/adapter/PhabricatorIRCProtocolAdapter.php
index d2740209b0..504605982c 100644
--- a/src/infrastructure/daemon/bot/adapter/PhabricatorIRCProtocolAdapter.php
+++ b/src/infrastructure/daemon/bot/adapter/PhabricatorIRCProtocolAdapter.php
@@ -1,257 +1,278 @@
<?php
final class PhabricatorIRCProtocolAdapter
extends PhabricatorBaseProtocolAdapter {
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(
"Nickname '{$nick}' is invalid!");
}
$errno = null;
$error = null;
if (!$ssl) {
$socket = fsockopen($server, $port, $errno, $error);
} else {
$socket = fsockopen('ssl://'.$server, $port, $errno, $error);
}
if (!$socket) {
throw new Exception("Failed to connect, #{$errno}: {$error}");
}
$ok = stream_set_blocking($socket, false);
if (!$ok) {
throw new Exception('Failed to set stream nonblocking.');
}
$this->socket = $socket;
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) {
- throw new Exception(
- 'socket_select() failed: '.socket_strerror(socket_last_error()));
+ // 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('stream_select() failed!');
+ }
}
if ($read) {
// Test for connection termination; in PHP, fread() off a nonblocking,
// closed socket is empty string.
if (feof($this->socket)) {
// This indicates the connection was terminated on the other side,
// just exit via exception and let the overseer restart us after a
// delay so we can reconnect.
throw new Exception('Remote host closed connection.');
}
do {
$data = fread($this->socket, 4096);
if ($data === false) {
throw new Exception('fread() failed!');
} else {
$messages[] = 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('fwrite() failed!');
+ } 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('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 __destruct() {
- $this->write('QUIT Goodbye.');
- fclose($this->socket);
+ 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;
}
}

File Metadata

Mime Type
text/x-diff
Expires
Tue, Jun 10, 4:12 AM (17 h, 33 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
140237
Default Alt Text
(15 KB)

Event Timeline