Page MenuHomePhabricator

D7723.diff

diff --git a/src/__celerity_resource_map__.php b/src/__celerity_resource_map__.php
--- a/src/__celerity_resource_map__.php
+++ b/src/__celerity_resource_map__.php
@@ -1969,6 +1969,19 @@
),
'disk' => '/rsrc/js/application/maniphest/behavior-transaction-preview.js',
),
+ 'javelin-behavior-nuance-source-editor' =>
+ array(
+ 'uri' => '/res/93e5bcd8/rsrc/js/application/nuance/nuance-source-editor.js',
+ 'type' => 'js',
+ 'requires' =>
+ array(
+ 0 => 'javelin-dom',
+ 1 => 'javelin-behavior',
+ 2 => 'javelin-stratcom',
+ 3 => 'javelin-uri',
+ ),
+ 'disk' => '/rsrc/js/application/nuance/nuance-source-editor.js',
+ ),
'javelin-behavior-owners-path-editor' =>
array(
'uri' => '/res/9cf78ffc/rsrc/js/application/owners/owners-path-editor.js',
diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -886,7 +886,7 @@
'NuancePHIDTypeQueue' => 'applications/nuance/phid/NuancePHIDTypeQueue.php',
'NuancePHIDTypeRequestor' => 'applications/nuance/phid/NuancePHIDTypeRequestor.php',
'NuancePHIDTypeSource' => 'applications/nuance/phid/NuancePHIDTypeSource.php',
- 'NuancePhabricatorFormSourceDefinition' => 'applications/nuance/source/NuancePhabricatorFormSourceDefinition.php',
+ 'NuancePhabricatorFormSourceDefinition' => 'applications/nuance/source/definition/NuancePhabricatorFormSourceDefinition.php',
'NuanceQuery' => 'applications/nuance/query/NuanceQuery.php',
'NuanceQueue' => 'applications/nuance/storage/NuanceQueue.php',
'NuanceQueueEditController' => 'applications/nuance/controller/NuanceQueueEditController.php',
@@ -907,15 +907,27 @@
'NuanceRequestorTransactionQuery' => 'applications/nuance/query/NuanceRequestorTransactionQuery.php',
'NuanceRequestorViewController' => 'applications/nuance/controller/NuanceRequestorViewController.php',
'NuanceSource' => 'applications/nuance/storage/NuanceSource.php',
- 'NuanceSourceDefinition' => 'applications/nuance/source/NuanceSourceDefinition.php',
+ 'NuanceSourceController' => 'applications/nuance/controller/NuanceSourceController.php',
+ 'NuanceSourceDefinition' => 'applications/nuance/source/definition/NuanceSourceDefinition.php',
+ 'NuanceSourceDeleteController' => 'applications/nuance/controller/NuanceSourceDeleteController.php',
'NuanceSourceEditController' => 'applications/nuance/controller/NuanceSourceEditController.php',
'NuanceSourceEditor' => 'applications/nuance/editor/NuanceSourceEditor.php',
+ 'NuanceSourceListController' => 'applications/nuance/controller/NuanceSourceListController.php',
'NuanceSourceQuery' => 'applications/nuance/query/NuanceSourceQuery.php',
+ 'NuanceSourceSearchEngine' => 'applications/nuance/query/NuanceSourceSearchEngine.php',
'NuanceSourceTransaction' => 'applications/nuance/storage/NuanceSourceTransaction.php',
'NuanceSourceTransactionComment' => 'applications/nuance/storage/NuanceSourceTransactionComment.php',
'NuanceSourceTransactionQuery' => 'applications/nuance/query/NuanceSourceTransactionQuery.php',
'NuanceSourceViewController' => 'applications/nuance/controller/NuanceSourceViewController.php',
+ 'NuanceSourceWorker' => 'applications/nuance/source/worker/NuanceSourceWorker.php',
'NuanceTransaction' => 'applications/nuance/storage/NuanceTransaction.php',
+ 'NuanceTwitterProtocolBuffer' => 'applications/nuance/source/bot/buffer/NuanceTwitterProtocolBuffer.php',
+ 'NuanceTwitterProtocolBufferException' => 'applications/nuance/source/bot/buffer/NuanceTwitterProtocolBufferException.php',
+ 'NuanceTwitterProtocolBufferTestCase' => 'applications/nuance/source/bot/buffer/__tests__/NuanceTwitterProtocolBufferTestCase.php',
+ 'NuanceTwitterPublicStreamSourceDefinition' => 'applications/nuance/source/definition/NuanceTwitterPublicStreamSourceDefinition.php',
+ 'NuanceTwitterSourceBot' => 'applications/nuance/source/bot/NuanceTwitterSourceBot.php',
+ 'NuanceTwitterSourceDefinition' => 'applications/nuance/source/definition/NuanceTwitterSourceDefinition.php',
+ 'NuanceTwitterUserStreamSourceDefinition' => 'applications/nuance/source/definition/NuanceTwitterUserStreamSourceDefinition.php',
'OwnersPackageReplyHandler' => 'applications/owners/mail/OwnersPackageReplyHandler.php',
'PHUI' => 'view/phui/PHUI.php',
'PHUIBoxExample' => 'applications/uiexample/examples/PHUIBoxExample.php',
@@ -3331,15 +3343,30 @@
0 => 'NuanceDAO',
1 => 'PhabricatorPolicyInterface',
),
+ 'NuanceSourceController' => 'NuanceController',
'NuanceSourceDefinition' => 'Phobject',
- 'NuanceSourceEditController' => 'NuanceController',
+ 'NuanceSourceDeleteController' => 'NuanceController',
+ 'NuanceSourceEditController' => 'NuanceSourceController',
'NuanceSourceEditor' => 'PhabricatorApplicationTransactionEditor',
+ 'NuanceSourceListController' =>
+ array(
+ 0 => 'NuanceSourceController',
+ 1 => 'PhabricatorApplicationSearchResultsControllerInterface',
+ ),
'NuanceSourceQuery' => 'NuanceQuery',
+ 'NuanceSourceSearchEngine' => 'PhabricatorApplicationSearchEngine',
'NuanceSourceTransaction' => 'NuanceTransaction',
'NuanceSourceTransactionComment' => 'PhabricatorApplicationTransactionComment',
'NuanceSourceTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
- 'NuanceSourceViewController' => 'NuanceController',
+ 'NuanceSourceViewController' => 'NuanceSourceController',
+ 'NuanceSourceWorker' => 'PhabricatorWorker',
'NuanceTransaction' => 'PhabricatorApplicationTransaction',
+ 'NuanceTwitterProtocolBufferException' => 'Exception',
+ 'NuanceTwitterProtocolBufferTestCase' => 'PhabricatorTestCase',
+ 'NuanceTwitterPublicStreamSourceDefinition' => 'NuanceTwitterSourceDefinition',
+ 'NuanceTwitterSourceBot' => 'PhabricatorDaemon',
+ 'NuanceTwitterSourceDefinition' => 'NuanceSourceDefinition',
+ 'NuanceTwitterUserStreamSourceDefinition' => 'NuanceTwitterSourceDefinition',
'OwnersPackageReplyHandler' => 'PhabricatorMailReplyHandler',
'PHUIBoxExample' => 'PhabricatorUIExample',
'PHUIBoxView' => 'AphrontTagView',
diff --git a/src/applications/nuance/application/PhabricatorApplicationNuance.php b/src/applications/nuance/application/PhabricatorApplicationNuance.php
--- a/src/applications/nuance/application/PhabricatorApplicationNuance.php
+++ b/src/applications/nuance/application/PhabricatorApplicationNuance.php
@@ -31,24 +31,27 @@
return array(
'/nuance/' => array(
'item/' => array(
- 'view/(?P<id>[1-9]\d*)/' => 'NuanceItemViewController',
- 'edit/(?P<id>[1-9]\d*)/' => 'NuanceItemEditController',
- 'new/' => 'NuanceItemEditController',
+ 'view/(?P<id>[1-9]\d*)/' => 'NuanceItemViewController',
+ 'edit/(?P<id>[1-9]\d*)/' => 'NuanceItemEditController',
+ 'new/' => 'NuanceItemEditController',
),
'source/' => array(
- 'view/(?P<id>[1-9]\d*)/' => 'NuanceSourceViewController',
- 'edit/(?P<id>[1-9]\d*)/' => 'NuanceSourceEditController',
- 'new/' => 'NuanceSourceEditController',
+ '' => 'NuanceSourceListController',
+ '(query/(?P<queryKey>[^/]+)/)?' => 'NuanceSourceListController',
+ 'view/(?P<id>[1-9]\d*)/' => 'NuanceSourceViewController',
+ 'edit/(?P<id>[1-9]\d*)/' => 'NuanceSourceEditController',
+ 'new/' => 'NuanceSourceEditController',
+ 'delete/(?P<id>[1-9]\d*)/' => 'NuanceSourceDeleteController',
),
'queue/' => array(
- 'view/(?P<id>[1-9]\d*)/' => 'NuanceQueueViewController',
- 'edit/(?P<id>[1-9]\d*)/' => 'NuanceQueueEditController',
- 'new/' => 'NuanceQueueEditController',
+ 'view/(?P<id>[1-9]\d*)/' => 'NuanceQueueViewController',
+ 'edit/(?P<id>[1-9]\d*)/' => 'NuanceQueueEditController',
+ 'new/' => 'NuanceQueueEditController',
),
'requestor/' => array(
- 'view/(?P<id>[1-9]\d*)/' => 'NuanceRequestorViewController',
- 'edit/(?P<id>[1-9]\d*)/' => 'NuanceRequestorEditController',
- 'new/' => 'NuanceRequestorEditController',
+ 'view/(?P<id>[1-9]\d*)/' => 'NuanceRequestorViewController',
+ 'edit/(?P<id>[1-9]\d*)/' => 'NuanceRequestorEditController',
+ 'new/' => 'NuanceRequestorEditController',
),
),
);
diff --git a/src/applications/nuance/controller/NuanceController.php b/src/applications/nuance/controller/NuanceController.php
--- a/src/applications/nuance/controller/NuanceController.php
+++ b/src/applications/nuance/controller/NuanceController.php
@@ -2,4 +2,7 @@
abstract class NuanceController extends PhabricatorController {
+ protected function buildApplicationCrumbs() {
+ return parent::buildApplicationCrumbs();
+ }
}
diff --git a/src/applications/nuance/controller/NuanceSourceController.php b/src/applications/nuance/controller/NuanceSourceController.php
new file mode 100644
--- /dev/null
+++ b/src/applications/nuance/controller/NuanceSourceController.php
@@ -0,0 +1,15 @@
+<?php
+
+abstract class NuanceSourceController extends NuanceController {
+
+ protected function buildApplicationCrumbs() {
+ $crumbs = parent::buildApplicationCrumbs();
+ $crumbs->addAction(
+ id(new PHUIListItemView())
+ ->setName(pht('Create Source'))
+ ->setHref(id(new NuanceSource())->getEditURI())
+ ->setIcon('create'));
+
+ return $crumbs;
+ }
+}
diff --git a/src/applications/nuance/controller/NuanceSourceDeleteController.php b/src/applications/nuance/controller/NuanceSourceDeleteController.php
new file mode 100644
--- /dev/null
+++ b/src/applications/nuance/controller/NuanceSourceDeleteController.php
@@ -0,0 +1,46 @@
+<?php
+
+final class NuanceSourceDeleteController extends NuanceController {
+
+ private $sourceID;
+
+ public function willProcessRequest(array $data) {
+ $this->sourceID = $data['id'];
+ }
+
+ public function processRequest() {
+ $can_edit = $this->requireApplicationCapability(
+ NuanceCapabilitySourceManage::CAPABILITY);
+ $request = $this->getRequest();
+ $user = $request->getUser();
+ $source = id(new NuanceSourceQuery())
+ ->setViewer($user)
+ ->withIDs(array($this->sourceID))
+ ->requireCapabilities(
+ array(
+ PhabricatorPolicyCapability::CAN_VIEW,
+ PhabricatorPolicyCapability::CAN_EDIT,
+ ))
+ ->executeOne();
+
+ if (!$source) {
+ return new Aphront404Response();
+ }
+
+ if ($request->isFormPost()) {
+ $source->delete();
+ return id(new AphrontReloadResponse())->setURI('/nuance/source/');
+ }
+
+ $dialog = id(new AphrontDialogView())
+ ->setUser($user)
+ ->setTitle(pht('Delete Source?'))
+ ->appendChild(
+ pht('Really delete this source? You can not recover it.'))
+ ->addSubmitButton(pht('Delete'))
+ ->addCancelButton($source->getURI());
+
+ return id(new AphrontDialogResponse())->setDialog($dialog);
+ }
+
+}
diff --git a/src/applications/nuance/controller/NuanceSourceEditController.php b/src/applications/nuance/controller/NuanceSourceEditController.php
--- a/src/applications/nuance/controller/NuanceSourceEditController.php
+++ b/src/applications/nuance/controller/NuanceSourceEditController.php
@@ -1,6 +1,6 @@
<?php
-final class NuanceSourceEditController extends NuanceController {
+final class NuanceSourceEditController extends NuanceSourceController {
private $sourceID;
@@ -19,31 +19,26 @@
public function processRequest() {
$can_edit = $this->requireApplicationCapability(
NuanceCapabilitySourceManage::CAPABILITY);
-
$request = $this->getRequest();
$user = $request->getUser();
-
- $source_id = $this->getSourceID();
- $is_new = !$source_id;
-
- if ($is_new) {
- $source = NuanceSource::initializeNewSource($user);
- } else {
- $source = id(new NuanceSourceQuery())
- ->setViewer($user)
- ->withIDs(array($source_id))
- ->requireCapabilities(
- array(
- PhabricatorPolicyCapability::CAN_VIEW,
- PhabricatorPolicyCapability::CAN_EDIT,
- ))
- ->executeOne();
- }
-
+ $source = $this->loadOrCreateSourceObject($user);
if (!$source) {
return new Aphront404Response();
}
+ return $this->processEditRequest($source);
+ }
+
+ private function processEditRequest(NuanceSource $source) {
+ $request = $this->getRequest();
+ $user = $request->getUser();
+
+ // this handles if the user just changes the source type, which does a
+ // quick re-direct and preserves the new type and name
+ if (!$request->isFormPost() && $request->getExists('redraw')) {
+ $source->setType($request->getStr('type'));
+ $source->setName($request->getStr('name'));
+ }
$definition = NuanceSourceDefinition::getDefinitionForSource($source);
$definition->setActor($user);
@@ -63,4 +58,26 @@
'title' => $definition->getEditTitle(),
'device' => true));
}
+
+ private function loadOrCreateSourceObject(PhabricatorUser $user) {
+ $source_id = $this->getSourceID();
+ $is_new = !$source_id;
+
+ if (!$source_id) {
+ $source = NuanceSource::initializeNewSource($user);
+ } else {
+ $source = id(new NuanceSourceQuery())
+ ->setViewer($user)
+ ->withIDs(array($source_id))
+ ->requireCapabilities(
+ array(
+ PhabricatorPolicyCapability::CAN_VIEW,
+ PhabricatorPolicyCapability::CAN_EDIT,
+ ))
+ ->executeOne();
+ }
+
+ return $source;
+ }
+
}
diff --git a/src/applications/nuance/controller/NuanceSourceListController.php b/src/applications/nuance/controller/NuanceSourceListController.php
new file mode 100644
--- /dev/null
+++ b/src/applications/nuance/controller/NuanceSourceListController.php
@@ -0,0 +1,83 @@
+<?php
+
+final class NuanceSourceListController extends NuanceSourceController
+ implements PhabricatorApplicationSearchResultsControllerInterface {
+
+ private $queryKey;
+
+ public function shouldAllowPublic() {
+ return true;
+ }
+
+ public function willProcessRequest(array $data) {
+ $this->queryKey = idx($data, 'queryKey');
+ }
+
+ public function processRequest() {
+ $request = $this->getRequest();
+ $viewer = $request->getUser();
+
+ $controller = id(new PhabricatorApplicationSearchController($request))
+ ->setQueryKey($this->queryKey)
+ ->setSearchEngine(new NuanceSourceSearchEngine())
+ ->setNavigation($this->buildSideNavView());
+
+ return $this->delegateToController($controller);
+ }
+
+ private function buildSideNavView() {
+ $user = $this->getRequest()->getUser();
+
+ $nav = new AphrontSideNavFilterView();
+ $nav->setBaseURI(new PhutilURI($this->getApplicationURI()));
+
+ id(new NuanceSourceSearchEngine())
+ ->setViewer($user)
+ ->addNavigationItems($nav->getMenu());
+
+ $nav->selectFilter(null);
+
+ return $nav;
+ }
+
+ public function renderResultsList(
+ array $sources,
+ PhabricatorSavedQuery $query) {
+ assert_instances_of($sources, 'NuanceSource');
+
+ $user = $this->getRequest()->getUser();
+
+ $list = new PHUIObjectItemListView();
+ $list->setUser($user);
+ foreach ($sources as $source) {
+ $updated = phabricator_datetime($source->getDateModified(), $user);
+
+ $title = $source->getName();
+
+ $definition = NuanceSourceDefinition::getDefinitionForSource($source);
+ $item = id(new PHUIObjectItemView())
+ ->setHeader($title)
+ ->setHref($source->getURI())
+ ->setObject($source)
+ ->addIcon('none', $updated)
+ ->addAttribute($definition->getName())
+ ->addAction(
+ id(new PHUIListItemView())
+ ->setHref($source->getEditURI())
+ ->setName(pht('Edit'))
+ ->setIcon('edit'))
+ ->addAction(
+ id(new PHUIListItemView())
+ ->setHref($source->getDeleteURI())
+ ->setName(pht('Delete'))
+ ->setIcon('delete')
+ ->setWorkflow(true));
+
+ $list->addItem($item);
+ }
+
+ return $list;
+ }
+
+
+}
diff --git a/src/applications/nuance/controller/NuanceSourceViewController.php b/src/applications/nuance/controller/NuanceSourceViewController.php
--- a/src/applications/nuance/controller/NuanceSourceViewController.php
+++ b/src/applications/nuance/controller/NuanceSourceViewController.php
@@ -1,6 +1,6 @@
<?php
-final class NuanceSourceViewController extends NuanceController {
+final class NuanceSourceViewController extends NuanceSourceController {
private $sourceID;
@@ -101,10 +101,18 @@
id(new PhabricatorActionView())
->setName(pht('Edit Source'))
->setIcon('edit')
- ->setHref($this->getApplicationURI("source/edit/{$id}/"))
+ ->setHref($source->getEditURI())
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit));
+ $actions->addAction(
+ id(new PhabricatorActionView())
+ ->setName(pht('Delete Source'))
+ ->setIcon('delete')
+ ->setHref($source->getDeleteURI())
+ ->setDisabled(!$can_edit)
+ ->setWorkflow(true));
+
return $actions;
}
diff --git a/src/applications/nuance/editor/NuanceSourceEditor.php b/src/applications/nuance/editor/NuanceSourceEditor.php
--- a/src/applications/nuance/editor/NuanceSourceEditor.php
+++ b/src/applications/nuance/editor/NuanceSourceEditor.php
@@ -7,6 +7,8 @@
$types = parent::getTransactionTypes();
$types[] = NuanceSourceTransaction::TYPE_NAME;
+ $types[] = NuanceSourceTransaction::TYPE_SOURCE_TYPE;
+ $types[] = NuanceSourceTransaction::TYPE_METADATA;
$types[] = PhabricatorTransactions::TYPE_EDGE;
$types[] = PhabricatorTransactions::TYPE_COMMENT;
@@ -23,6 +25,16 @@
switch ($xaction->getTransactionType()) {
case NuanceSourceTransaction::TYPE_NAME:
return $object->getName();
+ case NuanceSourceTransaction::TYPE_SOURCE_TYPE:
+ if ($this->getIsNewObject()) {
+ return null;
+ }
+ return $object->getType();
+ case NuanceSourceTransaction::TYPE_METADATA:
+ if ($this->getIsNewObject()) {
+ return null;
+ }
+ return $object->getData();
}
return parent::getCustomTransactionOldValue($object, $xaction);
@@ -34,6 +46,8 @@
switch ($xaction->getTransactionType()) {
case NuanceSourceTransaction::TYPE_NAME:
+ case NuanceSourceTransaction::TYPE_SOURCE_TYPE:
+ case NuanceSourceTransaction::TYPE_METADATA:
return $xaction->getNewValue();
}
@@ -48,6 +62,12 @@
case NuanceSourceTransaction::TYPE_NAME:
$object->setName($xaction->getNewValue());
break;
+ case NuanceSourceTransaction::TYPE_SOURCE_TYPE:
+ $object->setType($xaction->getNewValue());
+ break;
+ case NuanceSourceTransaction::TYPE_METADATA:
+ $object->setData($xaction->getNewValue());
+ break;
}
}
@@ -57,6 +77,9 @@
switch ($xaction->getTransactionType()) {
case NuanceSourceTransaction::TYPE_NAME:
+ case NuanceSourceTransaction::TYPE_SOURCE_TYPE:
+ return;
+ case NuanceSourceTransaction::TYPE_METADATA:
return;
}
@@ -87,6 +110,13 @@
$errors[] = $error;
}
break;
+ case NuanceSourceTransaction::TYPE_METADATA:
+ $definition = NuanceSourceDefinition::getDefinitionForSource($object);
+ $error = $definition->validateTransaction($type, $xactions);
+ if ($error) {
+ $errors[] = $error;
+ }
+ break;
}
return $errors;
diff --git a/src/applications/nuance/query/NuanceSourceQuery.php b/src/applications/nuance/query/NuanceSourceQuery.php
--- a/src/applications/nuance/query/NuanceSourceQuery.php
+++ b/src/applications/nuance/query/NuanceSourceQuery.php
@@ -28,7 +28,6 @@
return $this;
}
-
public function loadPage() {
$table = new NuanceSource();
$conn_r = $table->establishConnection('r');
@@ -59,7 +58,7 @@
if ($this->types) {
$where[] = qsprintf(
$conn_r,
- 'type IN (%Ld)',
+ 'type IN (%Ls)',
$this->types);
}
diff --git a/src/applications/nuance/query/NuanceSourceSearchEngine.php b/src/applications/nuance/query/NuanceSourceSearchEngine.php
new file mode 100644
--- /dev/null
+++ b/src/applications/nuance/query/NuanceSourceSearchEngine.php
@@ -0,0 +1,62 @@
+<?php
+
+final class NuanceSourceSearchEngine
+ extends PhabricatorApplicationSearchEngine {
+
+ public function buildSavedQueryFromRequest(AphrontRequest $request) {
+ $saved = new PhabricatorSavedQuery();
+ $saved->setParameter('type', $request->getStr('type'));
+
+ return $saved;
+ }
+
+ public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) {
+ $query = id(new NuanceSourceQuery());
+
+ if ($saved->getParameter('type')) {
+ $query->withTypes(array($saved->getParameter('type')));
+ }
+
+ return $query;
+ }
+
+ public function buildSearchForm(
+ AphrontFormView $form,
+ PhabricatorSavedQuery $saved_query) {
+
+ $v_type = $saved_query->getParameter('type', null);
+ $form
+ ->appendChild(
+ id(new AphrontFormSelectControl())
+ ->setLabel(pht('Type'))
+ ->setName('type')
+ ->setOptions(NuanceSourceDefinition::getSelectOptions())
+ ->setValue($v_type));
+ }
+
+ protected function getURI($path) {
+ return '/nuance/source/'.$path;
+ }
+
+ public function getBuiltinQueryNames() {
+ $names = array(
+ 'all' => pht('All Sources'),
+ );
+
+ return $names;
+ }
+
+ public function buildSavedQueryFromBuiltin($query_key) {
+
+ $query = $this->newSavedQuery();
+ $query->setQueryKey($query_key);
+
+ switch ($query_key) {
+ case 'all':
+ return $query;
+ }
+
+ return parent::buildSavedQueryFromBuiltin($query_key);
+ }
+
+}
diff --git a/src/applications/nuance/source/bot/NuanceTwitterSourceBot.php b/src/applications/nuance/source/bot/NuanceTwitterSourceBot.php
new file mode 100644
--- /dev/null
+++ b/src/applications/nuance/source/bot/NuanceTwitterSourceBot.php
@@ -0,0 +1,214 @@
+<?php
+
+/**
+ * Continuously loads all known @{class:NuanceSource} objects of
+ * @{class:NuanceTwitterSourceDefinition} type, calling each objects
+ * @{method:updateItems} method.
+ */
+final class NuanceTwitterSourceBot extends PhabricatorDaemon {
+
+ const PUBLIC_STREAM_URI =
+ 'https://stream.twitter.com/1.1/statuses/filter.json';
+ const USER_STREAM_URI =
+ 'https://userstream.twitter.com/1.1/user.json';
+
+ public function run() {
+ $argv = $this->getArgv();
+ if (count($argv) !== 0) {
+ throw new Exception("usage: NuanceTwitterSourceBot");
+ }
+ $this->runLoop();
+ }
+
+ private function runLoop() {
+ do {
+ $this->stillWorking();
+
+ $twitter_provider = $this->getTwitterProvider();
+
+ $user_source_map = array();
+ $sources = $this->reloadSources();
+ foreach ($sources as $source) {
+ $source_data = $source->getData();
+ $user_phid = $source_data['user_phid'];
+ if (!isset($user_source_map[$user_phid])) {
+ $user_source_map[$user_phid] = array();
+ }
+ $user_source_map[$user_phid][] = $source;
+ }
+
+ $user_phids = array_keys($user_source_map);
+ $users = id(new PhabricatorPeopleQuery())
+ ->setViewer($this->getViewer())
+ ->withPHIDs(array_keys($user_phids))
+ ->execute();
+ $users = mpull($users, null, 'getPHID');
+
+ $external_accounts = id(new PhabricatorExternalAccountQuery())
+ ->setViewer($this->getViewer())
+ ->withAccountTypes(array($twitter_provider->getProviderType()))
+ ->withAccountDomains(array($twitter_provider->getProviderDomain()))
+ ->withUserPHIDs($user_phids)
+ ->execute();
+ $external_accounts = mpull($external_accounts, null, 'getUserPHID');
+
+ $futures = array();
+ $buffers = array();
+ $public_type = id(new NuanceTwitterPublicStreamSourceDefinition())
+ ->getSourceTypeConstant();
+ $user_type = id(new NuanceTwitterUserStreamSourceDefinition())
+ ->getSourceTypeConstant();
+ foreach ($user_source_map as $user_phid => $sources) {
+ if (!isset($external_accounts[$user_phid])) {
+ $user = $users[$user_phid];
+ // all sources are misconfigured, but just take the first
+ $source = reset($sources);
+ throw new Exception(sprintf(
+ 'No external twitter account for user "%s". This means the '.
+ 'source named "%s" is misconfigured, or user "%s" should (re-)'.
+ 'authorize twitter in "External Accounts" on their account '.
+ 'settings.',
+ $user->getUserName(),
+ $source->getName(),
+ $user->getUserName()));
+ }
+ $external_account = $external_accounts[$user_phid];
+ $token = $external_account->getProperty('oauth1.token');
+ $secret = $external_account->getProperty('oauth1.token.secret');
+ foreach ($sources as $source) {
+ $adapter = $twitter_provider->getAdapter();
+ $adapter->setToken($token);
+ $adapter->setTokenSecret($secret);
+ $uri = null;
+ $source_data = $source->getData();
+ switch ($source->getType()) {
+ case $public_type:
+ $uri = new PhutilURI(self::PUBLIC_STREAM_URI);
+ $track = implode(' ', $source_data['keywords']);
+ $uri->setQueryParam('track', $track);
+ break;
+ case $user_type:
+ $uri = new PhutilURI(self::USER_STREAM_URI);
+ // restrict to data just about this user (as opposed to including
+ // their followed folks in this stream)
+ $uri->setQueryParam('with', 'user');
+ break;
+ default:
+ throw new Exception(sprintf(
+ 'Unknown source type "%s".',
+ $source->getType()));
+ break;
+ }
+ $futures[$source->getPHID()] = $adapter->newOAuth1Future($uri);
+ $buffers[$source->getPHID()] = new NuanceTwitterProtocolBuffer();
+ }
+ }
+
+ $wait = 1.0;
+ foreach (Futures($futures)->setUpdateInterval($wait) as $key => $future) {
+
+ // This might be a periodic update, in which case $future will be null.
+ // It might also be a future exiting, in which case $future will not be
+ // null. In either case, let's read any data we can first.
+ foreach ($futures as $key => $oauth_future) {
+ $buffer = $buffers[$key];
+ $future = $oauth_future->getProxiedFuture();
+ $new_data = $future->read();
+ // Throw the buffered data away after we read it.
+ $future->discardBuffers();
+
+ if (strlen($new_data)) {
+ try {
+ $messages = $buffer->write($new_data);
+ } catch (NuanceTwitterProtocolBufferException $ex) {
+ throw new Exception($this->getMessageForHTTPCode(
+ $ex->getHTTPCode()));
+ }
+ foreach ($messages as $message) {
+ // TODO - queue up a task
+ var_dump($message);
+ }
+ }
+ }
+
+ // Now, check if a future exited.
+ if ($future !== null) {
+ // This future has exited.
+ // We should sleep for a bit and then restart it,
+ // or whatever.
+ echo "Future {$key} exited!\n";
+ }
+ }
+ } while (true);
+ }
+
+ private function reloadSources() {
+ $types = array(
+ id(new NuanceTwitterPublicStreamSourceDefinition())
+ ->getSourceTypeConstant(),
+ id(new NuanceTwitterUserStreamSourceDefinition())
+ ->getSourceTypeConstant(),
+ );
+ return id(new NuanceSourceQuery())
+ ->withTypes($types)
+ ->setViewer($this->getViewer())
+ ->execute();
+ }
+
+ private function getTwitterProvider() {
+ $twitter = null;
+ $providers = PhabricatorAuthProvider::getAllEnabledProviders();
+ foreach ($providers as $provider) {
+ if ($provider instanceof PhabricatorAuthProviderOAuth1Twitter) {
+ $twitter = $provider;
+ break;
+ }
+ }
+
+ if (!$twitter) {
+ throw new Exception("No Twitter OAuth on this install!");
+ }
+
+ return $twitter;
+ }
+
+ /**
+ * See https://dev.twitter.com/docs/streaming-apis/connecting#HTTP_Error_Codes
+ * for details on what codes Twitter may send, what they mean, and how the
+ * application should react to these codes.
+ */
+ private function getMessageForHTTPCode($code, NuanceSource $source) {
+ $exception = null;
+ switch ($code) {
+ case 200:
+ // success
+ break;
+ case 401:
+ // unauthorized
+ break;
+ case 403:
+ // forbidden
+ break;
+ case 404:
+ // unknown
+ break;
+ case 406:
+ // not acceptable
+ break;
+ case 413:
+ // too long
+ break;
+ case 416:
+ // range unacceptable
+ break;
+ case 420:
+ // rate limited
+ break;
+ case 503:
+ // service unavailable
+ break;
+ }
+
+ return $exception;
+ }
+}
diff --git a/src/applications/nuance/source/bot/buffer/NuanceTwitterProtocolBuffer.php b/src/applications/nuance/source/bot/buffer/NuanceTwitterProtocolBuffer.php
new file mode 100644
--- /dev/null
+++ b/src/applications/nuance/source/bot/buffer/NuanceTwitterProtocolBuffer.php
@@ -0,0 +1,98 @@
+<?php
+
+final class NuanceTwitterProtocolBuffer {
+
+ /* modes for parsing the buffer */
+ const START_MODE = 'start-mode';
+ const READ_MODE = 'read-mode';
+ const DELIMITER_MODE = 'delimiter-mode';
+ const MESSAGE_MODE = 'message-mode';
+
+ /* magic string */
+ const DELIMITER = "\r\n";
+ const DELIMITER_LEN = 2;
+
+ private $buffer = '';
+ private $mode = self::START_MODE;
+
+ public function write($data) {
+ $objects = array();
+ $this->buffer .= $data;
+
+ while (strlen($this->buffer)) {
+ switch ($this->mode) {
+ case self::START_MODE:
+ $base_regex = "@^(?P<head>.*?)\r?\n\r?\n(?P<body>.*)$@s";
+ $matches = null;
+ if (!preg_match($base_regex, $this->buffer, $matches)) {
+ // this means we only have part of the headers or just the
+ // headers and no body yet
+ break;
+ }
+ $header = $matches['head'];
+ // fast-forward through the headers
+ $this->buffer = substr($this->buffer, strlen($header));
+ $header_regex =
+ "@^HTTP\/\S+ (?P<code>\d+) (?P<status>.*?)".
+ "(?:\r?\n(?P<headers>.*))?$@s";
+ if (!preg_match($header_regex, $header, $matches)) {
+ throw new Exception('Malformed response from twitter...!');
+ }
+ $code = $matches['code'];
+ if ($code != 200) {
+ throw id(new NuanceTwitterProtocolBufferException())
+ ->setHTTPCode($code);
+ }
+ $this->mode = self::READ_MODE;
+ break;
+ case self::READ_MODE:
+ if (strncmp(
+ $this->buffer, self::DELIMITER, self::DELIMITER_LEN) == 0) {
+ $this->mode = self::DELIMITER_MODE;
+ } else {
+ $this->mode = self::MESSAGE_MODE;
+ }
+ break;
+ case self::DELIMITER_MODE:
+ // consume the delimiter
+ $this->buffer = substr($this->buffer, self::DELIMITER_LEN);
+ // there could be multiple delimiters in a row
+ if ($this->buffer) {
+ if (strncmp(
+ $this->buffer, self::DELIMITER, self::DELIMITER_LEN) == 0) {
+ $this->mode = self::DELIMITER_MODE;
+ } else {
+ $this->mode = self::MESSAGE_MODE;
+ }
+ } else {
+ $this->mode = self::READ_MODE;
+ }
+ break;
+ case self::MESSAGE_MODE:
+ $another_delimiter = strpos($this->buffer, self::DELIMITER);
+ if ($another_delimiter) {
+ $object_str = substr($this->buffer, 0, $another_delimiter);
+ } else {
+ $object_str = $this->buffer;
+ }
+ $object = json_decode($object_str, true);
+ if ($object) {
+ $objects[] = $object;
+ $this->buffer = substr($this->buffer, strlen($object_str));
+ if ($another_delimiter) {
+ $this->mode = self::DELIMITER_MODE;
+ } else {
+ $this->mode = self::READ_MODE;
+ }
+ } else {
+ // we don't have enough data for an object yet, so read some more
+ $this->mode = self::READ_MODE;
+ }
+ break;
+ }
+ }
+
+ return $objects;
+ }
+
+}
diff --git a/src/applications/nuance/source/bot/buffer/NuanceTwitterProtocolBufferException.php b/src/applications/nuance/source/bot/buffer/NuanceTwitterProtocolBufferException.php
new file mode 100644
--- /dev/null
+++ b/src/applications/nuance/source/bot/buffer/NuanceTwitterProtocolBufferException.php
@@ -0,0 +1,15 @@
+<?php
+
+final class NuanceTwitterProtocolBufferException extends Exception {
+
+ private $httpCode;
+
+ public function setHTTPCode($code) {
+ $this->httpCode = $code;
+ return $this;
+ }
+ public function getHTTPCode() {
+ return $this->httpCode;
+ }
+
+}
diff --git a/src/applications/nuance/source/bot/buffer/__tests__/NuanceTwitterProtocolBufferTestCase.php b/src/applications/nuance/source/bot/buffer/__tests__/NuanceTwitterProtocolBufferTestCase.php
new file mode 100644
--- /dev/null
+++ b/src/applications/nuance/source/bot/buffer/__tests__/NuanceTwitterProtocolBufferTestCase.php
@@ -0,0 +1,57 @@
+<?php
+
+final class NuanceTwitterProtocolBufferTestCase
+ extends PhabricatorTestCase {
+
+ public function testBufferOneTweet() {
+ $test = $this->getSampleTweetResponse(1);
+ $buffer = new NuanceTwitterProtocolBuffer();
+ $tweets = $buffer->write($test);
+ $this->assertEqual(1, count($tweets));
+ }
+
+ public function testBufferTwoTweets() {
+ $test = $this->getSampleTweetResponse(2);
+ $buffer = new NuanceTwitterProtocolBuffer();
+ $tweets = $buffer->write($test);
+ $this->assertEqual(2, count($tweets));
+ }
+
+ public function testBufferTenTweets() {
+ $test = $this->getSampleTweetResponse(10);
+ $buffer = new NuanceTwitterProtocolBuffer();
+ $tweets = $buffer->write($test);
+ $this->assertEqual(10, count($tweets));
+ }
+
+ private function get200Headers() {
+ $headers = <<<EOTEXT
+HTTP/1.1 200 OK
+Content-Type: application/json
+Transfer-Encoding: chunked
+EOTEXT;
+
+ return $headers;
+ }
+
+ private function getSampleTweet() {
+ $tweet = <<<EOTEXT
+{"created_at":"Thu Dec 12 00:08:59 +0000 2013","id":410924204716879872,"id_str":"410924204716879872","text":"VIDEO: George W. Bush paints you a Merry Christmas http:\/\/t.co\/bPOVlT9FBF","source":"\u003ca href=\"http:\/\/publicize.wp.com\/\" rel=\"nofollow\"\u003eWordPress.com\u003c\/a\u003e","truncated":false,"in_reply_to_status_id":null,"in_reply_to_status_id_str":null,"in_reply_to_user_id":null,"in_reply_to_user_id_str":null,"in_reply_to_screen_name":null,"user":{"id":1255863102,"id_str":"1255863102","name":"talazo dotcom","screen_name":"talazodotcom1","location":"","url":null,"description":null,"protected":false,"followers_count":1,"friends_count":10,"listed_count":0,"created_at":"Sun Mar 10 02:18:08 +0000 2013","favourites_count":0,"utc_offset":null,"time_zone":null,"geo_enabled":false,"verified":false,"statuses_count":23180,"lang":"en","contributors_enabled":false,"is_translator":false,"profile_background_color":"C0DEED","profile_background_image_url":"http:\/\/abs.twimg.com\/images\/themes\/theme1\/bg.png","profile_background_image_url_https":"https:\/\/abs.twimg.com\/images\/themes\/theme1\/bg.png","profile_background_tile":false,"profile_image_url":"http:\/\/abs.twimg.com\/sticky\/default_profile_images\/default_profile_4_normal.png","profile_image_url_https":"https:\/\/abs.twimg.com\/sticky\/default_profile_images\/default_profile_4_normal.png","profile_link_color":"0084B4","profile_sidebar_border_color":"C0DEED","profile_sidebar_fill_color":"DDEEF6","profile_text_color":"333333","profile_use_background_image":true,"default_profile":true,"default_profile_image":true,"following":null,"follow_request_sent":null,"notifications":null},"geo":null,"coordinates":null,"place":null,"contributors":null,"retweet_count":0,"favorite_count":0,"entities":{"hashtags":[],"symbols":[],"urls":[{"url":"http:\/\/t.co\/bPOVlT9FBF","expanded_url":"http:\/\/wp.me\/p3guDH-cil","display_url":"wp.me\/p3guDH-cil","indices":[51,73]}],"user_mentions":[]},"favorited":false,"retweeted":false,"possibly_sensitive":false,"filter_level":"medium","lang":"en"}
+EOTEXT;
+ return $tweet;
+ }
+
+ private function getSampleTweetResponse($number_of_tweets = 1) {
+ $headers = $this->get200Headers();
+ $tweet = $this->getSampleTweet();
+ $delimiter = "\r\n";
+
+ $response = $headers . $delimiter . $delimiter;
+ for ($i = $number_of_tweets; $i > 0; $i--) {
+ $response .= $tweet;
+ $response .= $delimiter;
+ }
+
+ return $response;
+ }
+}
diff --git a/src/applications/nuance/source/NuancePhabricatorFormSourceDefinition.php b/src/applications/nuance/source/definition/NuancePhabricatorFormSourceDefinition.php
rename from src/applications/nuance/source/NuancePhabricatorFormSourceDefinition.php
rename to src/applications/nuance/source/definition/NuancePhabricatorFormSourceDefinition.php
--- a/src/applications/nuance/source/NuancePhabricatorFormSourceDefinition.php
+++ b/src/applications/nuance/source/definition/NuancePhabricatorFormSourceDefinition.php
@@ -15,7 +15,14 @@
return null;
}
+ protected function getEditFormDescription() {
+ return pht(
+ 'This source type creates a form, accessible to Phabricator users, that '.
+ 'creates Nuance issues.');
+ }
+
protected function augmentEditForm(
+ AphrontRequest $request,
AphrontFormView $form,
PhabricatorApplicationTransactionValidationException $ex = null) {
diff --git a/src/applications/nuance/source/NuanceSourceDefinition.php b/src/applications/nuance/source/definition/NuanceSourceDefinition.php
rename from src/applications/nuance/source/NuanceSourceDefinition.php
rename to src/applications/nuance/source/definition/NuanceSourceDefinition.php
--- a/src/applications/nuance/source/NuanceSourceDefinition.php
+++ b/src/applications/nuance/source/definition/NuanceSourceDefinition.php
@@ -56,13 +56,25 @@
*/
public static function getDefinitionForSource(NuanceSource $source) {
$definitions = self::getAllDefinitions();
- $map = mpull($definitions, null, 'getSourceTypeConstant');
- $definition = $map[$source->getType()];
+ if (!$source->getType()) {
+ $definition = reset($definitions);
+ } else {
+ $map = mpull($definitions, null, 'getSourceTypeConstant');
+ $definition = $map[$source->getType()];
+ }
$definition->setSourceObject($source);
return $definition;
}
+ public static function getDefinitionForSourceType($type) {
+ $definitions = self::getAllDefinitions();
+ $map = mpull($definitions, null, 'getSourceTypeConstant');
+ $definition = $map[$type];
+
+ return $definition;
+ }
+
public static function getAllDefinitions() {
static $definitions;
@@ -128,7 +140,7 @@
if ($source->getPHID()) {
$title = pht('Edit "%s" source.', $source->getName());
} else {
- $title = pht('Create a new "%s" source.', $this->getName());
+ $title = pht('Create a new source.');
}
return $title;
@@ -157,10 +169,15 @@
} catch (PhabricatorApplicationTransactionValidationException $ex) {
$validation_exception = $ex;
}
-
}
- $form = $this->renderEditForm($validation_exception);
+ $form = $this->renderEditForm($request, $validation_exception);
+ Javelin::initBehavior('nuance-source-editor',
+ array(
+ 'redrawURI' => $source->getEditURI(),
+ 'isNew' => $source->getPHID() ? false : true,
+ 'baseValueKeys' => array('name', 'type'),
+ ));
$layout = id(new PHUIObjectBoxView())
->setHeaderText($this->getEditTitle())
->setValidationException($validation_exception)
@@ -172,12 +189,15 @@
return $layout;
}
+ abstract protected function getEditFormDescription();
+
/**
* Code to create a form to edit the @{class:NuanceItem} you are defining.
*
* return @{class:AphrontFormView}
*/
- private function renderEditForm(
+ public function renderEditForm(
+ AphrontRequest $request,
PhabricatorApplicationTransactionValidationException $ex = null) {
$user = $this->requireActor();
$source = $this->requireSourceObject();
@@ -187,22 +207,54 @@
$e_name = $ex->getShortMessage(NuanceSourceTransaction::TYPE_NAME);
}
+ $is_new = !(bool) $source->getPHID();
+
+ $v_name = $request->getStr('name', $source->getName());
+ $v_type = $request->getStr('type', $source->getType());
+ if ($request->isFormPost()) {
+ $source->setViewPolicy($request->getStr('viewPolicy'));
+ $source->setEditPolicy($request->getStr('editPolicy'));
+ }
+
+ if ($is_new) {
+ $form_id = celerity_generate_unique_node_id();
+ $type_control =
+ id(new AphrontFormSelectControl())
+ ->setLabel(pht('Type'))
+ ->setName('type')
+ ->setOptions(self::getSelectOptions())
+ ->setValue($v_type)
+ ->setMetadata(array(
+ 'form_id' => $form_id,
+ 'selected' => $v_type))
+ ->addSigil('source-type-select');
+ $cancel_uri = '/nuance/source/';
+ } else {
+ $form_id = null;
+ $type_control =
+ id(new AphrontFormStaticControl())
+ ->setLabel(pht('Type'))
+ ->setValue($this->getName());
+ $cancel_uri = $source->getURI();
+ }
+
$form = id(new AphrontFormView())
->setUser($user)
+ ->setAction($source->getEditURI())
+ ->setID($form_id)
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Name'))
->setName('name')
->setError($e_name)
- ->setValue($source->getName()))
+ ->setValue($v_name))
+ ->appendChild($type_control)
->appendChild(
- id(new AphrontFormSelectControl())
- ->setLabel(pht('Type'))
- ->setName('type')
- ->setOptions(self::getSelectOptions())
- ->setValue($source->getType()));
+ id(new AphrontFormStaticControl())
+ ->setLabel(pht('Type Description'))
+ ->setValue($this->getEditFormDescription()));
- $form = $this->augmentEditForm($form, $ex);
+ $form = $this->augmentEditForm($request, $form, $ex);
$form
->appendChild(
@@ -221,7 +273,7 @@
->setName('editPolicy'))
->appendChild(
id(new AphrontFormSubmitControl())
- ->addCancelButton($source->getURI())
+ ->addCancelButton($cancel_uri)
->setValue(pht('Save')));
return $form;
@@ -231,6 +283,7 @@
* return @{class:AphrontFormView}
*/
protected function augmentEditForm(
+ AphrontRequest $request,
AphrontFormView $form,
PhabricatorApplicationTransactionValidationException $ex = null) {
@@ -260,13 +313,27 @@
$transactions[] = id(new NuanceSourceTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY)
->setNewValue($request->getStr('viewPolicy'));
- $transactions[] = id(new NuanceSourceTransaction())
+ $transactions[] = id(new NuanceSourceTransaction())
->setTransactionType(NuanceSourceTransaction::TYPE_NAME)
->setNewvalue($request->getStr('name'));
+ if ($request->getStr('type')) {
+ $transactions[] = id(new NuanceSourceTransaction())
+ ->setTransactionType(NuanceSourceTransaction::TYPE_SOURCE_TYPE)
+ ->setNewvalue($request->getStr('type'));
+ }
return $transactions;
}
+ /**
+ * Hook to validate @{class:PhabricatorTransactions} on a per-definition
+ * basis. Useful for validating metadata, which tends to vary from definition
+ * to definition.
+ */
+ public function validateTransaction($type, array $type_xactions) {
+ return null;
+ }
+
abstract public function renderView();
abstract public function renderListView();
diff --git a/src/applications/nuance/source/definition/NuanceTwitterPublicStreamSourceDefinition.php b/src/applications/nuance/source/definition/NuanceTwitterPublicStreamSourceDefinition.php
new file mode 100644
--- /dev/null
+++ b/src/applications/nuance/source/definition/NuanceTwitterPublicStreamSourceDefinition.php
@@ -0,0 +1,111 @@
+<?php
+
+final class NuanceTwitterPublicStreamSourceDefinition
+ extends NuanceTwitterSourceDefinition {
+
+ public function getName() {
+ return pht('Twitter Public Stream');
+ }
+
+ public function getSourceTypeConstant() {
+ return 'twitter-public-stream';
+ }
+
+ protected function getEditFormDescription() {
+ return pht(
+ 'This source type uses the Twitter Public Steam API to create items '.
+ 'from the stream of public tweets that match the specified keywords. '.
+ 'The Twitter account of the last user to edit this source is used to '.
+ 'connect to twitter.');
+ }
+
+ protected function augmentEditForm(
+ AphrontRequest $request,
+ AphrontFormView $form,
+ PhabricatorApplicationTransactionValidationException $ex = null) {
+
+ $source = $this->requireSourceObject();
+ $data = $source->getData();
+ $v_keywords = null;
+ if ($request->isFormPost()) {
+ $v_keywords = $request->getStr('keywords');
+ } else if ($data) {
+ $v_keywords = implode(' ', idx($data, 'keywords', array()));
+ }
+ $e_keywords = null;
+ if ($ex) {
+ $e_keywords =
+ $ex->getShortMessage(NuanceSourceTransaction::TYPE_METADATA);
+ }
+ $form
+ ->appendChild(
+ id(new AphrontFormTextAreaControl())
+ ->setLabel(pht('Keywords'))
+ ->setName('keywords')
+ ->setValue($v_keywords)
+ ->setError($e_keywords)
+ ->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_SHORT)
+ ->setCaption(
+ pht(
+ 'Space-delimited keywords this source should track. '.
+ 'Keywords are "OR\'d" and match regardless of capitalization.')));
+
+ // See https://dev.twitter.com/docs/streaming-apis/parameters#track for
+ // more details on keywords.
+
+ return $form;
+ }
+
+ protected function buildTransactions(AphrontRequest $request) {
+ $transactions = parent::buildTransactions($request);
+
+ $keyword_string = trim($request->getStr('keywords'));
+ if ($keyword_string) {
+ $keywords = explode(' ', $keyword_string);
+ } else {
+ $keywords = array();
+ }
+ $metadata = array(
+ 'keywords' => $keywords,
+ 'user_phid' => $request->getUser()->getPHID());
+
+ $transactions[] = id(new NuanceSourceTransaction())
+ ->setTransactionType(NuanceSourceTransaction::TYPE_METADATA)
+ ->setNewValue($metadata);
+
+ return $transactions;
+ }
+
+ public function validateTransaction($type, array $type_xactions) {
+ $error = null;
+ switch ($type) {
+ case NuanceSourceTransaction::TYPE_METADATA:
+ $xaction = last($type_xactions);
+ $data = $xaction->getNewValue();
+ $keywords = $data['keywords'];
+ if (empty($keywords)) {
+ $error = new PhabricatorApplicationTransactionValidationError(
+ $type,
+ pht('Required'),
+ pht('At least one keyword is required.'),
+ $xaction);
+ $error->setIsMissingFieldError(true);
+ } else {
+ foreach ($keywords as $keyword) {
+ if (phutil_utf8_strlen($keyword) > 60) {
+ $error = new PhabricatorApplicationTransactionValidationError(
+ $type,
+ pht('Too long'),
+ pht(
+ 'At least one keyword ("%s") is too long; each keyword must '.
+ 'be 60 characters or less.', $keyword));
+ break;
+ }
+ }
+ }
+ break;
+ }
+ return $error;
+ }
+
+}
diff --git a/src/applications/nuance/source/definition/NuanceTwitterSourceDefinition.php b/src/applications/nuance/source/definition/NuanceTwitterSourceDefinition.php
new file mode 100644
--- /dev/null
+++ b/src/applications/nuance/source/definition/NuanceTwitterSourceDefinition.php
@@ -0,0 +1,16 @@
+<?php
+
+abstract class NuanceTwitterSourceDefinition
+ extends NuanceSourceDefinition {
+
+ public function updateItems() {
+ return null;
+ }
+
+ public function renderView() {
+ }
+
+ public function renderListView() {
+ }
+
+}
diff --git a/src/applications/nuance/source/definition/NuanceTwitterUserStreamSourceDefinition.php b/src/applications/nuance/source/definition/NuanceTwitterUserStreamSourceDefinition.php
new file mode 100644
--- /dev/null
+++ b/src/applications/nuance/source/definition/NuanceTwitterUserStreamSourceDefinition.php
@@ -0,0 +1,115 @@
+<?php
+
+final class NuanceTwitterUserStreamSourceDefinition
+ extends NuanceTwitterSourceDefinition {
+
+ public function getName() {
+ return pht('Twitter User Stream');
+ }
+
+ public function getSourceTypeConstant() {
+ return 'twitter-user-stream';
+ }
+
+ protected function getEditFormDescription() {
+ return pht(
+ 'This source type uses the Twitter User Stream API to create items from '.
+ 'the stream of twitter events specific to the authenticated user. '.
+ 'The Twitter account of the last user to edit this source is used to '.
+ 'connect to twitter.');
+ }
+
+ protected function augmentEditForm(
+ AphrontRequest $request,
+ AphrontFormView $form,
+ PhabricatorApplicationTransactionValidationException $ex = null) {
+
+ $source = $this->requireSourceObject();
+ $data = $source->getData();
+ $v_tweeted = false;
+ $v_mentioned = true;
+ $v_direct_messaged = true;
+ $v_followed = true;
+ if ($request->isFormPost()) {
+ $v_tweeted = $request->getExists('tweeted');
+ $v_mentioned = $request->getExists('mentioned');
+ $v_direct_messaged = $request->getExists('direct_messaged');
+ $v_followed = $request->getExists('followed');
+ } else if ($data) {
+ $v_tweeted = idx($data, 'tweeted', false);
+ $v_mentioned = idx($data, 'mentioned', true);
+ $v_direct_messaged = idx($data, 'direct_messaged', true);
+ $v_followed = idx($data, 'followed', true);
+ }
+ $e_checkboxes = null;
+ if ($ex) {
+ $e_checkboxes =
+ $ex->getShortMessage(NuanceSourceTransaction::TYPE_METADATA);
+ }
+
+ $form
+ ->appendChild(
+ id(new AphrontFormCheckboxControl())
+ ->setError($e_checkboxes)
+ ->setLabel(pht('Events'))
+ ->setName('events')
+ ->addCheckbox('tweeted',
+ 'tweeted',
+ 'User tweeted',
+ $v_tweeted)
+ ->addCheckbox('mentioned',
+ 'mentioned',
+ 'User is mentioned',
+ $v_mentioned)
+ ->addCheckbox('direct_messaged',
+ 'direct_messaged',
+ 'User is direct messaged',
+ $v_direct_messaged)
+ ->addCheckbox('followed',
+ 'followed',
+ 'User is followed',
+ $v_followed));
+
+ return $form;
+ }
+
+ protected function buildTransactions(AphrontRequest $request) {
+
+ $transactions = parent::buildTransactions($request);
+
+ $metadata = array(
+ 'tweeted' => $request->getExists('tweeted'),
+ 'mentioned' => $request->getExists('mentioned'),
+ 'direct_messaged' => $request->getExists('direct_messaged'),
+ 'followed' => $request->getExists('followed'),
+ 'user_phid' => $request->getUser()->getPHID());
+
+ $transactions[] = id(new NuanceSourceTransaction())
+ ->setTransactionType(NuanceSourceTransaction::TYPE_METADATA)
+ ->setNewValue($metadata);
+
+ return $transactions;
+ }
+
+ public function validateTransaction($type, array $type_xactions) {
+ $error = null;
+ switch ($type) {
+ case NuanceSourceTransaction::TYPE_METADATA:
+ $xaction = last($type_xactions);
+ $data = $xaction->getNewValue();
+ if (!$data['tweeted'] &&
+ !$data['mentioned'] &&
+ !$data['direct_messaged'] &&
+ !$data['followed']) {
+ $error = new PhabricatorApplicationTransactionValidationError(
+ $type,
+ pht('Required'),
+ pht('At least one event is required.'),
+ $xaction);
+ $error->setIsMissingFieldError(true);
+ }
+ break;
+ }
+ return $error;
+ }
+}
diff --git a/src/applications/nuance/source/worker/NuanceSourceWorker.php b/src/applications/nuance/source/worker/NuanceSourceWorker.php
new file mode 100644
--- /dev/null
+++ b/src/applications/nuance/source/worker/NuanceSourceWorker.php
@@ -0,0 +1,6 @@
+<?php
+
+final class NuanceSourceWorker
+ extends PhabricatorWorker {
+
+}
diff --git a/src/applications/nuance/storage/NuanceSource.php b/src/applications/nuance/storage/NuanceSource.php
--- a/src/applications/nuance/storage/NuanceSource.php
+++ b/src/applications/nuance/storage/NuanceSource.php
@@ -6,7 +6,7 @@
protected $name;
protected $type;
- protected $data;
+ protected $data = array();
protected $mailKey;
protected $viewPolicy;
protected $editPolicy;
@@ -36,6 +36,20 @@
return '/nuance/source/view/'.$this->getID().'/';
}
+ public function getEditURI() {
+ if ($this->getID()) {
+ $uri = '/nuance/source/edit/'.$this->getID().'/';
+ } else {
+ $uri = '/nuance/source/new/';
+ }
+
+ return $uri;
+ }
+
+ public function getDeleteURI() {
+ return '/nuance/source/delete/'.$this->getID().'/';
+ }
+
public static function initializeNewSource(PhabricatorUser $actor) {
$app = id(new PhabricatorApplicationQuery())
->setViewer($actor)
@@ -47,13 +61,9 @@
$edit_policy = $app->getPolicy(
NuanceCapabilitySourceDefaultEdit::CAPABILITY);
- $definitions = NuanceSourceDefinition::getAllDefinitions();
- $lucky_definition = head($definitions);
-
return id(new NuanceSource())
->setViewPolicy($view_policy)
- ->setEditPolicy($edit_policy)
- ->setType($lucky_definition->getSourceTypeConstant());
+ ->setEditPolicy($edit_policy);
}
diff --git a/src/applications/nuance/storage/NuanceSourceTransaction.php b/src/applications/nuance/storage/NuanceSourceTransaction.php
--- a/src/applications/nuance/storage/NuanceSourceTransaction.php
+++ b/src/applications/nuance/storage/NuanceSourceTransaction.php
@@ -3,7 +3,9 @@
final class NuanceSourceTransaction
extends NuanceTransaction {
- const TYPE_NAME = 'name-source';
+ const TYPE_NAME = 'name-source';
+ const TYPE_SOURCE_TYPE = 'source-type-source';
+ const TYPE_METADATA = 'source-metadata';
public function getApplicationTransactionType() {
return NuancePHIDTypeSource::TYPECONST;
@@ -32,8 +34,67 @@
$new);
}
break;
+ case self::TYPE_SOURCE_TYPE:
+ if ($old === null) {
+ return null;
+ } else {
+ $old_definition = NuanceSourceDefinition::getDefinitionForSourceType(
+ $old);
+ $new_definition = NuanceSourceDefinition::getDefinitionForSourceType(
+ $new);
+ return pht(
+ '%s changed the source type from "%s" to "%s".',
+ $this->renderHandleLink($author_phid),
+ $old_definition->getName(),
+ $new_definition->getName());
+ break;
+ }
+ case self::TYPE_METADATA:
+ if ($old === null) {
+ return null;
+ } else {
+ return pht(
+ '%s updated the metadata.',
+ $this->renderHandleLink($author_phid));
+ break;
+ }
}
}
+ public function shouldHide() {
+ switch ($this->getTransactionType()) {
+ case self::TYPE_SOURCE_TYPE:
+ case self::TYPE_METADATA:
+ if ($this->getOldValue() === null) {
+ return true;
+ } else {
+ return false;
+ }
+ break;
+ }
+
+ return false;
+ }
+
+ public function hasChangeDetails() {
+ switch ($this->getTransactionType()) {
+ case self::TYPE_METADATA:
+ return true;
+ }
+ return parent::hasChangeDetails();
+ }
+
+ public function renderChangeDetails(PhabricatorUser $viewer) {
+ $old = $this->getOldValue();
+ $new = $this->getNewValue();
+
+ $view = id(new PhabricatorApplicationTransactionTextDiffDetailView())
+ ->setUser($viewer)
+ ->setOldText(json_encode($old))
+ ->setNewText(json_encode($new));
+
+ return $view->render();
+ }
+
}
diff --git a/src/view/form/control/AphrontFormSelectControl.php b/src/view/form/control/AphrontFormSelectControl.php
--- a/src/view/form/control/AphrontFormSelectControl.php
+++ b/src/view/form/control/AphrontFormSelectControl.php
@@ -7,6 +7,18 @@
}
private $options;
+ private $sigils = array();
+ private $metadata;
+
+ public function addSigil($sigil) {
+ $this->sigils[] = $sigil;
+ return $this;
+ }
+
+ public function setMetadata(array $data) {
+ $this->metadata = $data;
+ return $this;
+ }
public function setOptions(array $options) {
$this->options = $options;
@@ -25,6 +37,8 @@
'name' => $this->getName(),
'disabled' => $this->getDisabled() ? 'disabled' : null,
'id' => $this->getID(),
+ 'sigil' => $this->sigils ? implode(' ', $this->sigils) : null,
+ 'meta' => $this->metadata,
));
}
diff --git a/webroot/rsrc/js/application/nuance/nuance-source-editor.js b/webroot/rsrc/js/application/nuance/nuance-source-editor.js
new file mode 100644
--- /dev/null
+++ b/webroot/rsrc/js/application/nuance/nuance-source-editor.js
@@ -0,0 +1,35 @@
+/**
+ * @requires javelin-dom
+ * javelin-behavior
+ * javelin-stratcom
+ * javelin-uri
+ * @provides javelin-behavior-nuance-source-editor
+ * @javelin
+ */
+
+JX.behavior('nuance-source-editor', function(config) {
+
+ JX.Stratcom.listen(
+ 'change',
+ 'source-type-select',
+ redrawSourceEditForm);
+
+ function redrawSourceEditForm(e) {
+ var form_id = e.getNodeData('source-type-select').form_id;
+ var form = JX.$(form_id);
+ var form_data = JX.DOM.convertFormToDictionary(form);
+ var uri = JX.$U(config.redrawURI);
+ var params = {};
+ var key = null;
+ for (var i = 0; i < config.baseValueKeys.length; i++) {
+ key = config.baseValueKeys[i];
+ if (form_data[key]) {
+ params[key] = form_data[key];
+ }
+ }
+ params.redraw = true;
+ uri.addQueryParams(params);
+ uri.go();
+ }
+
+});

File Metadata

Mime Type
text/x-diff
Storage Engine
amazon-s3
Storage Format
Raw Data
Storage Handle
phabricator/tv/dv/qbzclltynkhpgfpy
Default Alt Text
D7723.diff (58 KB)

Event Timeline