1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2025-02-17 01:08:41 +01:00

First (rough) pass at campfire protocol adapter for bot.

Summary:
Decided the best approach for refactoring the message/command stuff would be to actually start implementing the campfire adapter to get a better idea of what the abstractions should look like. It feels awkward and unwieldy trying to maintain the irc command interface (notice the message instantiation in the `processReadBuffer()` method. However, i'm still not clear what the best approach is without requiring a re-write of nearly all the existing handlers and defining essentially a custom dsl on top of irc's.

I suppose given that alternative, implementing to irc's dsl doesn't sound all that bad. Just feels like poor coupling.

Also, I know that there is some http stuff in libphutil's futures library, but the https future is shit and I need to do some custom curlopt stuff I wasn't sure how to do with that. But if you think this should be refactored, let me know.

I tested this with the ObjectHandler (messages with DXXX initiate the bot to respond with the title/link just as with irc), but beyond that, I haven't tried any of the other handlers, so if there are complications you think i'm going to run into, just let me know (this is one of the reasons for requesting review early on).

Also, this diff is against my last one, even though that hasn't been merged down yet. It was starting to get large and I'd prefer to keep to two conversations separate.

Fixing some lint issues.

Test Plan: Ran the bot with the Object Handler in campfire and observed it behaving properly.

Reviewers: epriestley

Reviewed By: epriestley

CC: aran, Korvin

Maniphest Tasks: T2462

Differential Revision: https://secure.phabricator.com/D4830
This commit is contained in:
indiefan 2013-02-07 06:31:29 -08:00 committed by epriestley
parent 4004621d76
commit 431e2bee6e
16 changed files with 632 additions and 328 deletions

View file

@ -729,6 +729,7 @@ phutil_register_library_map(array(
'PhabricatorCalendarHoliday' => 'applications/calendar/storage/PhabricatorCalendarHoliday.php',
'PhabricatorCalendarHolidayTestCase' => 'applications/calendar/storage/__tests__/PhabricatorCalendarHolidayTestCase.php',
'PhabricatorCalendarViewStatusController' => 'applications/calendar/controller/PhabricatorCalendarViewStatusController.php',
'PhabricatorCampfireProtocolAdapter' => 'infrastructure/daemon/bot/adapter/PhabricatorCampfireProtocolAdapter.php',
'PhabricatorChangesetResponse' => 'infrastructure/diff/PhabricatorChangesetResponse.php',
'PhabricatorChatLogChannelListController' => 'applications/chatlog/controller/PhabricatorChatLogChannelListController.php',
'PhabricatorChatLogChannelLogController' => 'applications/chatlog/controller/PhabricatorChatLogChannelLogController.php',
@ -2177,6 +2178,7 @@ phutil_register_library_map(array(
'PhabricatorCalendarHoliday' => 'PhabricatorCalendarDAO',
'PhabricatorCalendarHolidayTestCase' => 'PhabricatorTestCase',
'PhabricatorCalendarViewStatusController' => 'PhabricatorCalendarController',
'PhabricatorCampfireProtocolAdapter' => 'PhabricatorBaseProtocolAdapter',
'PhabricatorChangesetResponse' => 'AphrontProxyResponse',
'PhabricatorChatLogChannelListController' => 'PhabricatorChatLogController',
'PhabricatorChatLogChannelLogController' => 'PhabricatorChatLogController',

View file

@ -101,8 +101,8 @@ final class PhabricatorBot extends PhabricatorDaemon {
} while (true);
}
public function writeCommand($command, $message) {
return $this->protocolAdapter->writeCommand($command, $message);
public function writeMessage(PhabricatorBotMessage $message) {
return $this->protocolAdapter->writeMessage($message);
}
private function routeMessage(PhabricatorBotMessage $message) {

View file

@ -4,69 +4,65 @@ final class PhabricatorBotMessage {
private $sender;
private $command;
private $data;
private $body;
private $target;
private $public;
public function __construct($sender, $command, $data) {
$this->sender = $sender;
$this->command = $command;
$this->data = $data;
public function __construct() {
// By default messages are public
$this->public = true;
}
public function getRawSender() {
public function setSender($sender) {
$this->sender = $sender;
return $this;
}
public function getSender() {
return $this->sender;
}
public function getRawData() {
return $this->data;
public function setCommand($command) {
$this->command = $command;
return $this;
}
public function getCommand() {
return $this->command;
}
public function getReplyTo() {
switch ($this->getCommand()) {
case 'PRIVMSG':
$target = $this->getTarget();
if ($target[0] == '#') {
return $target;
}
break;
}
return null;
public function setBody($body) {
$this->body = $body;
return $this;
}
public function getSenderNickname() {
$nick = $this->getRawSender();
$nick = ltrim($nick, ':');
$nick = head(explode('!', $nick));
return $nick;
public function getBody() {
return $this->body;
}
public function setTarget($target) {
$this->target = $target;
return $this;
}
public function getTarget() {
switch ($this->getCommand()) {
case 'PRIVMSG':
$matches = null;
$raw = $this->getRawData();
if (preg_match('/^(\S+)\s/', $raw, $matches)) {
return $matches[1];
}
break;
}
return null;
return $this->target;
}
public function getMessageText() {
switch ($this->getCommand()) {
case 'PRIVMSG':
$matches = null;
$raw = $this->getRawData();
if (preg_match('/^\S+\s+:?(.*)$/', $raw, $matches)) {
return rtrim($matches[1], "\r\n");
}
break;
}
return null;
public function isPublic() {
return $this->public;
}
public function setPublic($is_public) {
$this->public = $is_public;
return $this;
}
public function getReplyTo() {
if ($this->public) {
return $this->target;
} else {
return $this->sender;
}
}
}

View file

@ -30,8 +30,7 @@ abstract class PhabricatorBaseProtocolAdapter {
/**
* This is the output mechanism for the protocol.
*
* @param String $command The command for the message
* @param String $message The contents of the message itself
* @param PhabricatorBotMessage $message The message to write
*/
abstract public function writeCommand($command, $message);
abstract public function writeMessage(PhabricatorBotMessage $message);
}

View file

@ -0,0 +1,201 @@
<?php
final class PhabricatorCampfireProtocolAdapter
extends PhabricatorBaseProtocolAdapter {
private $readBuffers;
private $authtoken;
private $server;
private $readHandles;
private $multiHandle;
private $active;
private $rooms;
public function connect() {
$this->server = idx($this->config, 'server');
$this->authtoken = idx($this->config, 'authtoken');
$ssl = idx($this->config, 'ssl', false);
$this->rooms = idx($this->config, 'join');
// First, join the room
if (!$this->rooms) {
throw new Exception("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 ($this->rooms as $room_id) {
$this->joinRoom($room_id);
// Set up the curl stream for reading
$url = ($ssl) ? "https://" : "http://";
$url .= "streaming.campfirenow.com/room/{$room_id}/live.json";
$this->readHandle[$url] = curl_init();
curl_setopt($this->readHandle[$url], CURLOPT_URL, $url);
curl_setopt($this->readHandle[$url], CURLOPT_RETURNTRANSFER, true);
curl_setopt($this->readHandle[$url], CURLOPT_FOLLOWLOCATION, 1);
curl_setopt(
$this->readHandle[$url],
CURLOPT_USERPWD,
$this->authtoken.':x');
curl_setopt(
$this->readHandle[$url],
CURLOPT_HTTPHEADER,
array("Content-type: application/json"));
curl_setopt(
$this->readHandle[$url],
CURLOPT_WRITEFUNCTION,
array($this, 'read'));
curl_setopt($this->readHandle[$url], CURLOPT_BUFFERSIZE, 128);
curl_setopt($this->readHandle[$url], CURLOPT_TIMEOUT, 0);
curl_multi_add_handle($this->multiHandle, $this->readHandle[$url]);
// Initialize read buffer
$this->readBuffers[$url] = '';
}
$this->active = null;
$this->blockingMultiExec();
}
// 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(
"Phabricator Bot had a problem reading from campfire.");
}
}
public function getNextMessages($poll_frequency) {
$messages = array();
if (!$this->active) {
throw new Exception("Phabricator Bot stopped reading from campfire.");
}
// 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 = json_decode($message, true);
return id(new PhabricatorBotMessage())
->setCommand('MESSAGE')
->setTarget($m_obj['room_id'])
->setBody($m_obj['body']);
}
// If we're here, there's nothing to process
return false;
}
public function writeMessage(PhabricatorBotMessage $message) {
switch ($message->getCommand()) {
case 'MESSAGE':
$this->speak(
$message->getBody(),
$message->getTarget());
break;
}
}
private function joinRoom($room_id) {
$this->performPost("/room/{$room_id}/join.json");
}
private function leaveRoom($room_id) {
$this->performPost("/room/{$room_id}/leave.json");
}
private function speak($message, $room_id) {
$this->performPost(
"/room/{$room_id}/speak.json",
array(
'message' => array(
'type' => 'TextMessage',
'body' => $message)));
}
private function performPost($endpoint, $data = Null) {
$url = $this->server.$endpoint;
$payload = json_encode($data);
// cURL init & config
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, 1);
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_POSTFIELDS, $payload);
$output = curl_exec($ch);
curl_close($ch);
$output = trim($output);
if (strlen($output)) {
return json_decode($output);
}
return true;
}
public function __destruct() {
if ($this->rooms) {
foreach ($this->rooms as $room_id) {
$this->leaveRoom($room_id);
}
}
if ($this->readHandles) {
foreach ($this->readHandles as $read_handle) {
curl_multi_remove_handle($this->multiHandle, $read_handle);
curl_close($read_handle);
}
}
curl_multi_close($this->multiHandle);
}
}

View file

@ -1,14 +1,17 @@
<?php
// TODO: Write PhabricatorBaseSocketProtocolAdapter
final class PhabricatorIRCProtocolAdapter
extends PhabricatorBaseProtocolAdapter {
extends PhabricatorBaseProtocolAdapter {
private $socket;
private $writeBuffer;
private $readBuffer;
// Hash map of command translations
public static $commandTranslations = array(
'PRIVMSG' => 'MESSAGE');
public function connect() {
$nick = idx($this->config, 'nick', 'phabot');
$server = idx($this->config, 'server');
@ -38,12 +41,21 @@ final class PhabricatorIRCProtocolAdapter
}
$this->socket = $socket;
$this->writeCommand('USER', "{$user} 0 * :{$user}");
$this->writeMessage(
id(new PhabricatorBotMessage())
->setCommand('USER')
->setBody("{$user} 0 * :{$user}"));
if ($pass) {
$this->writeCommand('PASS', "{$pass}");
$this->writeMessage(
id(new PhabricatorBotMessage())
->setCommand('PASS')
->setBody("{$pass}"));
}
$this->writeCommand('NICK', "{$nick}");
$this->writeMessage(
id(new PhabricatorBotMessage())
->setCommand('NICK')
->setBody("{$nick}"));
}
public function getNextMessages($poll_frequency) {
@ -77,12 +89,9 @@ final class PhabricatorIRCProtocolAdapter
if ($data === false) {
throw new Exception("fread() failed!");
} else {
$messages[] = new PhabricatorBotMessage(
null,
"LOG",
"<<< ".$data
);
$messages[] = id(new PhabricatorBotMessage())
->setCommand("LOG")
->setBody(">>> ".$data);
$this->readBuffer .= $data;
}
} while (strlen($data));
@ -94,10 +103,9 @@ final class PhabricatorIRCProtocolAdapter
if ($len === false) {
throw new Exception("fwrite() failed!");
} else {
$messages[] = new PhabricatorBotMessage(
null,
"LOG",
">>> ".substr($this->writeBuffer, 0, $len));
$messages[] = id(new PhabricatorBotMessage())
->setCommand("LOG")
->setBody(">>> ".substr($this->writeBuffer, 0, $len));
$this->writeBuffer = substr($this->writeBuffer, $len);
}
} while (strlen($this->writeBuffer));
@ -115,8 +123,21 @@ final class PhabricatorIRCProtocolAdapter
return $this;
}
public function writeCommand($command, $message) {
return $this->write($command.' '.$message."\r\n");
public function writeMessage(PhabricatorBotMessage $message) {
$irc_command = $this->getIRCCommand($message->getCommand());
switch ($message->getCommand()) {
case 'MESSAGE':
$data = $irc_command.' '.
$message->getTarget().' :'.
$message->getBody()."\r\n";
break;
default:
$data = $irc_command.' '.
$message->getBody()."\r\n";
break;
}
return $this->write($data);
}
private function processReadBuffer() {
@ -130,7 +151,7 @@ final class PhabricatorIRCProtocolAdapter
$pattern =
'/^'.
'(?:(?P<sender>:(\S+)) )?'. // This may not be present.
'(?::(?P<sender>(\S+?))(?:!\S*)? )?'. // This may not be present.
'(?P<command>[A-Z0-9]+) '.
'(?P<data>.*)'.
'$/';
@ -140,17 +161,61 @@ final class PhabricatorIRCProtocolAdapter
throw new Exception("Unexpected message from server: {$message}");
}
$irc_message = new PhabricatorBotMessage(
idx($matches, 'sender'),
$matches['command'],
$matches['data']);
$command = $this->getBotCommand($matches['command']);
list($target, $body) = $this->parseMessageData($command, $matches['data']);
return $irc_message;
$bot_message = id(new PhabricatorBotMessage())
->setSender(idx($matches, 'sender'))
->setCommand($command)
->setTarget($target)
->setBody($body);
if (!empty($target) && strncmp($target, '#', 1) !== 0) {
$bot_message->setPublic(false);
}
return $bot_message;
}
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 getIRCCommand($original_bot_command) {
foreach (self::$commandTranslations as $irc_command=>$bot_command) {
if ($bot_command === $original_bot_command) {
return $irc_command;
}
}
return $original_bot_command;
}
private function parseMessageData($command, $data) {
switch ($command) {
case 'MESSAGE':
$matches = null;
if (preg_match('/^(\S+)\s+:?(.*)$/', $data, $matches)) {
return array(
$matches[1],
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.\r\n");
fclose($this->socket);
}
}

View file

@ -8,7 +8,7 @@ final class PhabricatorBotDebugLogHandler extends PhabricatorBotHandler {
switch ($message->getCommand()) {
case 'LOG':
echo addcslashes(
$message->getRawData(),
$message->getBody(),
"\0..\37\177..\377");
echo "\n";
break;

View file

@ -4,7 +4,7 @@
* @group irc
*/
final class PhabricatorBotDifferentialNotificationHandler
extends PhabricatorBotHandler {
extends PhabricatorBotHandler {
private $skippedOldEvents;
@ -39,11 +39,16 @@ final class PhabricatorBotDifferentialNotificationHandler
$verb = DifferentialAction::getActionPastTenseVerb($data['action']);
$actor_name = $handles[$actor_phid]->getName();
$message = "{$actor_name} {$verb} revision D".$data['revision_id'].".";
$message_body =
"{$actor_name} {$verb} revision D".$data['revision_id'].".";
$channels = $this->getConfig('notification.channels', array());
foreach ($channels as $channel) {
$this->write('PRIVMSG', "{$channel} :{$message}");
$this->writeMessage(
id(new PhabricatorBotMessage())
->setCommand('MESSAGE')
->setTarget($channel)
->setBody($message_body));
}
}
}

View file

@ -150,7 +150,11 @@ final class PhabricatorBotFeedNotificationHandler
$channels = $this->getConfig('join');
foreach ($channels as $channel) {
$this->write('PRIVMSG', "{$channel} :{$story['text']}");
$this->writeMessage(
id(new PhabricatorBotMessage())
->setCommand('MESSAGE')
->setTarget($channel)
->setBody($story['text']));
}
}
}

View file

@ -14,8 +14,8 @@ abstract class PhabricatorBotHandler {
$this->bot = $irc_bot;
}
final protected function write($command, $message) {
$this->bot->writeCommand($command, $message);
final protected function writeMessage(PhabricatorBotMessage $message) {
$this->bot->writeMessage($message);
return $this;
}
@ -33,14 +33,34 @@ abstract class PhabricatorBotHandler {
return (string)$base_uri;
}
final protected function isChannelName($name) {
return (strncmp($name, '#', 1) === 0);
}
abstract public function receiveMessage(PhabricatorBotMessage $message);
public function runBackgroundTasks() {
return;
}
public function replyTo($original_message, $body) {
if ($original_message->getCommand() != 'MESSAGE') {
throw new Exception(
"Handler is trying to reply to something which is not a message!");
}
$reply = id(new PhabricatorBotMessage())
->setCommand('MESSAGE');
if ($original_message->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())
->setPublic(false);
}
$reply->setBody($body);
return $this->writeMessage($reply);
}
}

View file

@ -12,12 +12,12 @@ final class PhabricatorBotLogHandler extends PhabricatorBotHandler {
public function receiveMessage(PhabricatorBotMessage $message) {
switch ($message->getCommand()) {
case 'PRIVMSG':
case 'MESSAGE':
$reply_to = $message->getReplyTo();
if (!$reply_to) {
break;
}
if (!$this->isChannelName($reply_to)) {
if (!$message->isPublic()) {
// Don't log private messages, although maybe we should for debugging?
break;
}
@ -27,8 +27,8 @@ final class PhabricatorBotLogHandler extends PhabricatorBotHandler {
'channel' => $reply_to,
'type' => 'mesg',
'epoch' => time(),
'author' => $message->getSenderNickname(),
'message' => $message->getMessageText(),
'author' => $message->getSender(),
'message' => $message->getBody(),
),
);
@ -46,7 +46,7 @@ final class PhabricatorBotLogHandler extends PhabricatorBotHandler {
$tell = false;
foreach ($prompts as $prompt) {
if (preg_match($prompt, $message->getMessageText())) {
if (preg_match($prompt, $message->getBody())) {
$tell = true;
break;
}
@ -55,7 +55,8 @@ final class PhabricatorBotLogHandler extends PhabricatorBotHandler {
if ($tell) {
$response = $this->getURI(
'/chatlog/channel/'.phutil_escape_uri($reply_to).'/');
$this->write('PRIVMSG', "{$reply_to} :{$response}");
$this->replyTo($message, $response);
}
break;

View file

@ -46,38 +46,38 @@ final class PhabricatorBotMacroHandler extends PhabricatorBotHandler {
}
switch ($message->getCommand()) {
case 'PRIVMSG':
$reply_to = $message->getReplyTo();
if (!$reply_to) {
break;
}
$message = $message->getMessageText();
$matches = null;
if (!preg_match($this->regexp, $message, $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'];
}
foreach ($ascii as $line) {
$this->buffer[$reply_to][] = $line;
}
case 'MESSAGE':
$reply_to = $message->getReplyTo();
if (!$reply_to) {
break;
}
$message_body = $message->getBody();
$matches = null;
if (!preg_match($this->regexp, $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'];
}
foreach ($ascii as $line) {
$this->buffer[$reply_to][] = $line;
}
break;
}
}
@ -92,7 +92,11 @@ final class PhabricatorBotMacroHandler extends PhabricatorBotHandler {
continue;
}
foreach ($lines as $key => $line) {
$this->write('PRIVMSG', "{$channel} :{$line}");
$this->writeMessage(
id(new PhabricatorBotMessage())
->setCommand('MESSAGE')
->setTarget($channel)
->setBody($line));
unset($this->buffer[$channel][$key]);
break 2;
}

View file

@ -13,186 +13,182 @@ final class PhabricatorBotObjectNameHandler extends PhabricatorBotHandler {
*/
private $recentlyMentioned = array();
public function receiveMessage(PhabricatorBotMessage $message) {
public function receiveMessage(PhabricatorBotMessage $original_message) {
switch ($message->getCommand()) {
case 'PRIVMSG':
$reply_to = $message->getReplyTo();
if (!$reply_to) {
break;
}
switch ($original_message->getCommand()) {
case 'MESSAGE':
$message = $original_message->getBody();
$matches = null;
$message = $message->getMessageText();
$matches = null;
$pattern =
'@'.
'(?<!/)(?:^|\b)'. // Negative lookbehind prevent matching "/D123".
'(D|T|P|V|F)(\d+)'.
'(?:\b|$)'.
'@';
$pattern =
'@'.
'(?<!/)(?:^|\b)'. // Negative lookbehind prevent matching "/D123".
'(D|T|P|V|F)(\d+)'.
'(?:\b|$)'.
'@';
$revision_ids = array();
$task_ids = array();
$paste_ids = array();
$commit_names = array();
$vote_ids = array();
$file_ids = array();
$revision_ids = array();
$task_ids = array();
$paste_ids = array();
$commit_names = array();
$vote_ids = array();
$file_ids = array();
if (preg_match_all($pattern, $message, $matches, PREG_SET_ORDER)) {
foreach ($matches as $match) {
switch ($match[1]) {
case 'D':
$revision_ids[] = $match[2];
break;
case 'T':
$task_ids[] = $match[2];
break;
case 'P':
$paste_ids[] = $match[2];
break;
case 'V':
$vote_ids[] = $match[2];
break;
case 'F':
$file_ids[] = $match[2];
break;
}
if (preg_match_all($pattern, $message, $matches, PREG_SET_ORDER)) {
foreach ($matches as $match) {
switch ($match[1]) {
case 'D':
$revision_ids[] = $match[2];
break;
case 'T':
$task_ids[] = $match[2];
break;
case 'P':
$paste_ids[] = $match[2];
break;
case 'V':
$vote_ids[] = $match[2];
break;
case 'F':
$file_ids[] = $match[2];
break;
}
}
}
$pattern =
'@'.
'(?<!/)(?:^|\b)'.
'(r[A-Z]+[0-9a-z]{1,40})'.
'(?:\b|$)'.
'@';
if (preg_match_all($pattern, $message, $matches, PREG_SET_ORDER)) {
foreach ($matches as $match) {
$commit_names[] = $match[1];
}
$pattern =
'@'.
'(?<!/)(?:^|\b)'.
'(r[A-Z]+[0-9a-z]{1,40})'.
'(?:\b|$)'.
'@';
if (preg_match_all($pattern, $message, $matches, PREG_SET_ORDER)) {
foreach ($matches as $match) {
$commit_names[] = $match[1];
}
}
$output = array();
$output = array();
if ($revision_ids) {
$revisions = $this->getConduit()->callMethodSynchronous(
'differential.query',
array(
'ids' => $revision_ids,
));
$revisions = array_select_keys(
ipull($revisions, null, 'id'),
$revision_ids
);
foreach ($revisions as $revision) {
$output[$revision['phid']] =
'D'.$revision['id'].' '.$revision['title'].' - '.
$revision['uri'];
}
if ($revision_ids) {
$revisions = $this->getConduit()->callMethodSynchronous(
'differential.query',
array(
'ids' => $revision_ids,
));
$revisions = array_select_keys(
ipull($revisions, null, 'id'),
$revision_ids
);
foreach ($revisions as $revision) {
$output[$revision['phid']] =
'D'.$revision['id'].' '.$revision['title'].' - '.
$revision['uri'];
}
}
if ($task_ids) {
foreach ($task_ids as $task_id) {
if ($task_id == 1000) {
$output[1000] = 'T1000: A nanomorph mimetic poly-alloy'
.'(liquid metal) assassin controlled by Skynet: '
.'http://en.wikipedia.org/wiki/T-1000';
continue;
}
$task = $this->getConduit()->callMethodSynchronous(
'maniphest.info',
array(
'task_id' => $task_id,
));
$output[$task['phid']] = 'T'.$task['id'].': '.$task['title'].
' (Priority: '.$task['priority'].') - '.$task['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'].
' 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.info',
array(
'paste_id' => $paste_id,
));
// Eventually I'd like to show the username of the paster as well,
// however that will need something like a user.username_from_phid
// since we (ideally) want to keep the bot to Conduit calls...and
// not call to Phabricator-specific stuff (like actually loading
// the User object and fetching his/her username.)
$output[$paste['phid']] = 'P'.$paste['id'].': '.$paste['uri'].' - '.
$paste['title'];
if ($paste['language']) {
$output[$paste['phid']] .= ' ('.$paste['language'].')';
}
}
}
if ($commit_names) {
$commits = $this->getConduit()->callMethodSynchronous(
'diffusion.getcommits',
array(
'commits' => $commit_names,
));
foreach ($commits as $commit) {
if (isset($commit['error'])) {
continue;
}
$output[$commit['commitPHID']] = $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 discsussions of a specific revision, for example.
if (empty($this->recentlyMentioned[$reply_to])) {
$this->recentlyMentioned[$reply_to] = array();
}
$quiet_until = idx(
$this->recentlyMentioned[$reply_to],
$phid,
0) + (60 * 10);
if (time() < $quiet_until) {
// Remain quiet on this channel.
if ($task_ids) {
foreach ($task_ids as $task_id) {
if ($task_id == 1000) {
$output[1000] = 'T1000: A nanomorph mimetic poly-alloy'
.'(liquid metal) assassin controlled by Skynet: '
.'http://en.wikipedia.org/wiki/T-1000';
continue;
}
$this->recentlyMentioned[$reply_to][$phid] = time();
$this->write('PRIVMSG', "{$reply_to} :{$description}");
$task = $this->getConduit()->callMethodSynchronous(
'maniphest.info',
array(
'task_id' => $task_id,
));
$output[$task['phid']] = 'T'.$task['id'].': '.$task['title'].
' (Priority: '.$task['priority'].') - '.$task['uri'];
}
break;
}
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'].
' 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.info',
array(
'paste_id' => $paste_id,
));
// Eventually I'd like to show the username of the paster as well,
// however that will need something like a user.username_from_phid
// since we (ideally) want to keep the bot to Conduit calls...and
// not call to Phabricator-specific stuff (like actually loading
// the User object and fetching his/her username.)
$output[$paste['phid']] = 'P'.$paste['id'].': '.$paste['uri'].' - '.
$paste['title'];
if ($paste['language']) {
$output[$paste['phid']] .= ' ('.$paste['language'].')';
}
}
}
if ($commit_names) {
$commits = $this->getConduit()->callMethodSynchronous(
'diffusion.getcommits',
array(
'commits' => $commit_names,
));
foreach ($commits as $commit) {
if (isset($commit['error'])) {
continue;
}
$output[$commit['commitPHID']] = $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 discsussions of a specific revision, for example.
$reply_to = $original_message->getReplyTo();
if (empty($this->recentlyMentioned[$reply_to])) {
$this->recentlyMentioned[$reply_to] = array();
}
$quiet_until = idx(
$this->recentlyMentioned[$reply_to],
$phid,
0) + (60 * 10);
if (time() < $quiet_until) {
// Remain quiet on this channel.
continue;
}
$this->recentlyMentioned[$reply_to][$phid] = time();
$this->replyTo($original_message, $description);
}
break;
}
}

View file

@ -10,45 +10,40 @@ final class PhabricatorBotSymbolHandler extends PhabricatorBotHandler {
public function receiveMessage(PhabricatorBotMessage $message) {
switch ($message->getCommand()) {
case 'PRIVMSG':
$reply_to = $message->getReplyTo();
if (!$reply_to) {
case 'MESSAGE':
$text = $message->getBody();
$matches = null;
if (!preg_match('/where(?: in the world)? is (\S+?)\?/i',
$text, $matches)) {
break;
}
$text = $message->getMessageText();
$symbol = $matches[1];
$results = $this->getConduit()->callMethodSynchronous(
'diffusion.findsymbols',
array(
'name' => $symbol,
));
$matches = null;
if (!preg_match('/where(?: in the world)? is (\S+?)\?/i',
$text, $matches)) {
break;
}
$default_uri = $this->getURI('/diffusion/symbol/'.$symbol.'/');
$symbol = $matches[1];
$results = $this->getConduit()->callMethodSynchronous(
'diffusion.findsymbols',
array(
'name' => $symbol,
));
if (count($results) > 1) {
$response = "Multiple symbols named '{$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 = "No symbol '{$symbol}' found anywhere.";
}
$default_uri = $this->getURI('/diffusion/symbol/'.$symbol.'/');
$this->replyTo($message, $response);
if (count($results) > 1) {
$response = "Multiple symbols named '{$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 = "No symbol '{$symbol}' found anywhere.";
}
$this->write('PRIVMSG', "{$reply_to} :{$response}");
break;
break;
}
}

View file

@ -12,16 +12,16 @@ final class PhabricatorBotWhatsNewHandler extends PhabricatorBotHandler {
public function receiveMessage(PhabricatorBotMessage $message) {
switch ($message->getCommand()) {
case 'PRIVMSG':
case 'MESSAGE':
$reply_to = $message->getReplyTo();
if (!$reply_to) {
break;
}
$message = $message->getMessageText();
$message_body = $message->getBody();
$prompt = '~what( i|\')?s new\?~i';
if (preg_match($prompt, $message)) {
if (preg_match($prompt, $message_body)) {
if (time() < $this->floodblock) {
return;
}
@ -108,9 +108,15 @@ final class PhabricatorBotWhatsNewHandler extends PhabricatorBotHandler {
$gray = $color.'15';
$bold = chr(2);
$reset = chr(15);
$content = "{$bold}{$user}{$reset} {$gray}{$action} {$blue}{$bold}".
"{$title}{$reset} - {$gray}{$uri}{$reset}";
$this->write('PRIVMSG',"{$reply_to} :{$content}");
// Disabling irc-specific styling, at least for now
// $content = "{$bold}{$user}{$reset} {$gray}{$action} {$blue}{$bold}".
// "{$title}{$reset} - {$gray}{$uri}{$reset}";
$content = "{$user} {$action} {$title} - {$uri}";
$this->writeMessage(
id(new PhabricatorBotMessage())
->setCommand('MESSAGE')
->setTarget($reply_to)
->setBody($content));
}
return;
}

View file

@ -13,18 +13,28 @@ final class PhabricatorIRCProtocolHandler extends PhabricatorBotHandler {
case '376': // End of MOTD
$nickpass = $this->getConfig('nickpass');
if ($nickpass) {
$this->write('PRIVMSG', "nickserv :IDENTIFY {$nickpass}");
$this->writeMessage(
id(new PhabricatorBotMessage())
->setCommand('MESSAGE')
->setTarget('nickserv')
->setBody("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);
$this->writeMessage(
id(new PhabricatorBotMessage())
->setCommand('JOIN')
->setBody($channel));
}
break;
case 'PING':
$this->write('PONG', $message->getRawData());
$this->writeMessage(
id(new PhabricatorBotMessage())
->setCommand('PONG')
->setBody($message->getBody()));
break;
}
}