1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-10 08:52:39 +01:00

Added Flowdock protocol adapter for the bot. Refactored campfire bot into a base streaming protocol adapter for common functionality.

Summary: First pass. Flowdock supports interesting message types (like replies to messages), but for now implementing a standard messaging interface.

Test Plan: Ran both a Flowdock bot and a Campfire bot. Made sure both still connected and responded properly to the Object Handler.

Reviewers: epriestley

Reviewed By: epriestley

CC: aran, Korvin

Differential Revision: https://secure.phabricator.com/D4983
This commit is contained in:
indiefan 2013-02-15 20:24:23 -08:00 committed by epriestley
parent 0e7382b102
commit 5fb56f859c
4 changed files with 249 additions and 140 deletions

View file

@ -713,10 +713,12 @@ phutil_register_library_map(array(
'PhabricatorBaseEnglishTranslation' => 'infrastructure/internationalization/PhabricatorBaseEnglishTranslation.php',
'PhabricatorBaseProtocolAdapter' => 'infrastructure/daemon/bot/adapter/PhabricatorBaseProtocolAdapter.php',
'PhabricatorBot' => 'infrastructure/daemon/bot/PhabricatorBot.php',
'PhabricatorBotBaseStreamingProtocolAdapter' => 'infrastructure/daemon/bot/adapter/PhabricatorBotBaseStreamingProtocolAdapter.php',
'PhabricatorBotChannel' => 'infrastructure/daemon/bot/target/PhabricatorBotChannel.php',
'PhabricatorBotDebugLogHandler' => 'infrastructure/daemon/bot/handler/PhabricatorBotDebugLogHandler.php',
'PhabricatorBotDifferentialNotificationHandler' => 'infrastructure/daemon/bot/handler/PhabricatorBotDifferentialNotificationHandler.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',
@ -2197,10 +2199,12 @@ phutil_register_library_map(array(
'PhabricatorBarePageView' => 'AphrontPageView',
'PhabricatorBaseEnglishTranslation' => 'PhabricatorTranslation',
'PhabricatorBot' => 'PhabricatorDaemon',
'PhabricatorBotBaseStreamingProtocolAdapter' => 'PhabricatorBaseProtocolAdapter',
'PhabricatorBotChannel' => 'PhabricatorBotTarget',
'PhabricatorBotDebugLogHandler' => 'PhabricatorBotHandler',
'PhabricatorBotDifferentialNotificationHandler' => 'PhabricatorBotHandler',
'PhabricatorBotFeedNotificationHandler' => 'PhabricatorBotHandler',
'PhabricatorBotFlowdockProtocolAdapter' => 'PhabricatorBotBaseStreamingProtocolAdapter',
'PhabricatorBotLogHandler' => 'PhabricatorBotHandler',
'PhabricatorBotMacroHandler' => 'PhabricatorBotHandler',
'PhabricatorBotObjectNameHandler' => 'PhabricatorBotHandler',

View file

@ -0,0 +1,158 @@
<?php
abstract class PhabricatorBotBaseStreamingProtocolAdapter
extends PhabricatorBaseProtocolAdapter {
private $readBuffers;
private $authtoken;
private $server;
private $readHandles;
private $multiHandle;
private $active;
private $inRooms = array();
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("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);
$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();
}
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(
"Phabricator Bot had a problem reading from stream.");
}
}
public function getNextMessages($poll_frequency) {
$messages = array();
if (!$this->active) {
throw new Exception("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 = json_decode($message, true);
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 json_decode($output, true);
}
return true;
}
protected function getAuthorizationHeader() {
return 'Basic '.base64_encode($this->authtoken.':x');
}
abstract protected function buildStreamingUrl($channel);
abstract protected function processMessage($raw_object);
}

View file

@ -0,0 +1,78 @@
<?php
final class PhabricatorBotFlowdockProtocolAdapter
extends PhabricatorBotBaseStreamingProtocolAdapter {
protected function buildStreamingUrl($channel) {
$organization = $this->getConfig('organization');
$ssl = $this->getConfig('ssl');
$url = ($ssl) ? "https://" : "http://";
$url .= "stream.flowdock.com/flows/{$organization}/{$channel}";
return $url;
}
protected function processMessage($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) {
list($organization, $room_id) = explode(":", $flow->getName());
$this->performPost(
"/flows/{$organization}/{$room_id}/messages",
array(
'event' => 'message',
'content' => $body));
}
public function __destruct() {
if ($this->readHandles) {
foreach ($this->readHandles as $read_handle) {
curl_multi_remove_handle($this->multiHandle, $read_handle);
curl_close($read_handle);
}
}
curl_multi_close($this->multiHandle);
}
}

View file

@ -1,120 +1,18 @@
<?php
final class PhabricatorCampfireProtocolAdapter
extends PhabricatorBaseProtocolAdapter {
extends PhabricatorBotBaseStreamingProtocolAdapter {
private $readBuffers;
private $authtoken;
private $server;
private $readHandles;
private $multiHandle;
private $active;
private $inRooms = array();
protected function buildStreamingUrl($channel) {
$ssl = $this->getConfig('ssl');
public function connect() {
$this->server = $this->getConfig('server');
$this->authtoken = $this->getConfig('authtoken');
$ssl = $this->getConfig('ssl', false);
$rooms = $this->getConfig('join');
$url = ($ssl) ? "https://" : "http://";
$url .= "streaming.campfirenow.com/room/{$channel}/live.json";
// First, join the room
if (!$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 ($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();
return $url;
}
// 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);
protected function processMessage($m_obj) {
$command = null;
switch ($m_obj['type']) {
case 'TextMessage':
@ -129,7 +27,7 @@ final class PhabricatorCampfireProtocolAdapter
}
if ($command === null) {
continue;
return false;
}
// TODO: These should be usernames, not user IDs.
@ -144,10 +42,6 @@ final class PhabricatorCampfireProtocolAdapter
->setSender($sender)
->setTarget($target)
->setBody($m_obj['body']);
}
// If we're here, there's nothing to process
return false;
}
public function writeMessage(PhabricatorBotMessage $message) {
@ -172,7 +66,7 @@ final class PhabricatorCampfireProtocolAdapter
}
}
private function joinRoom($room_id) {
protected function joinRoom($room_id) {
$this->performPost("/room/{$room_id}/join.json");
$this->inRooms[$room_id] = true;
}
@ -197,27 +91,6 @@ final class PhabricatorCampfireProtocolAdapter
'body' => $message)));
}
private 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 json_decode($output, true);
}
return true;
}
public function __destruct() {
foreach ($this->inRooms as $room_id => $ignored) {
$this->leaveRoom($room_id);
@ -232,8 +105,4 @@ final class PhabricatorCampfireProtocolAdapter
curl_multi_close($this->multiHandle);
}
private function getAuthorizationHeader() {
return 'Basic '.base64_encode($this->authtoken.':x');
}
}