Page MenuHomestyx hydra

No OneTemporary

diff --git a/src/applications/differential/doorkeeper/DifferentialDoorkeeperRevisionFeedStoryPublisher.php b/src/applications/differential/doorkeeper/DifferentialDoorkeeperRevisionFeedStoryPublisher.php
index 1b9750a100..d24b997b1f 100644
--- a/src/applications/differential/doorkeeper/DifferentialDoorkeeperRevisionFeedStoryPublisher.php
+++ b/src/applications/differential/doorkeeper/DifferentialDoorkeeperRevisionFeedStoryPublisher.php
@@ -1,107 +1,110 @@
<?php
final class DifferentialDoorkeeperRevisionFeedStoryPublisher
extends DoorkeeperFeedStoryPublisher {
public function canPublishStory(PhabricatorFeedStory $story, $object) {
return ($object instanceof DifferentialRevision);
}
public function isStoryAboutObjectCreation($object) {
$story = $this->getFeedStory();
+ $action = $story->getStoryData()->getValue('action');
+
+ return ($action == DifferentialAction::ACTION_CREATE);
+ }
+ public function isStoryAboutObjectClosure($object) {
+ $story = $this->getFeedStory();
$action = $story->getStoryData()->getValue('action');
- switch ($action) {
- case DifferentialAction::ACTION_CREATE:
- return true;
- default:
- return false;
- }
+
+ return ($action == DifferentialAction::ACTION_CLOSE) ||
+ ($action == DifferentialAction::ACTION_ABANDON);
}
public function willPublishStory($object) {
return id(new DifferentialRevisionQuery())
->setViewer($this->getViewer())
->withIDs(array($object->getID()))
->needRelationships(true)
->executeOne();
}
public function getOwnerPHID($object) {
return $object->getAuthorPHID();
}
public function getActiveUserPHIDs($object) {
$status = $object->getStatus();
if ($status == ArcanistDifferentialRevisionStatus::NEEDS_REVIEW) {
return $object->getReviewers();
} else {
return array();
}
}
public function getPassiveUserPHIDs($object) {
$status = $object->getStatus();
if ($status == ArcanistDifferentialRevisionStatus::NEEDS_REVIEW) {
return array();
} else {
return $object->getReviewers();
}
}
public function getCCUserPHIDs($object) {
return $object->getCCPHIDs();
}
public function getObjectTitle($object) {
$prefix = $this->getTitlePrefix($object);
$lines = new PhutilNumber($object->getLineCount());
$lines = pht('[Request, %d lines]', $lines);
$id = $object->getID();
$title = $object->getTitle();
return ltrim("{$prefix} {$lines} D{$id}: {$title}");
}
public function getObjectURI($object) {
return PhabricatorEnv::getProductionURI('/D'.$object->getID());
}
public function getObjectDescription($object) {
return $object->getSummary();
}
public function isObjectClosed($object) {
switch ($object->getStatus()) {
case ArcanistDifferentialRevisionStatus::CLOSED:
case ArcanistDifferentialRevisionStatus::ABANDONED:
return true;
default:
return false;
}
}
public function getResponsibilityTitle($object) {
$prefix = $this->getTitlePrefix($object);
return pht('%s Review Request', $prefix);
}
public function getStoryText($object) {
$story = $this->getFeedStory();
if ($story instanceof PhabricatorFeedStoryDifferential) {
$text = $story->renderForAsanaBridge();
} else {
$text = $story->renderText();
}
return $text;
}
private function getTitlePrefix(DifferentialRevision $revision) {
$prefix_key = 'metamta.differential.subject-prefix';
return PhabricatorEnv::getEnvConfig($prefix_key);
}
}
diff --git a/src/applications/diffusion/doorkeeper/DiffusionDoorkeeperCommitFeedStoryPublisher.php b/src/applications/diffusion/doorkeeper/DiffusionDoorkeeperCommitFeedStoryPublisher.php
index 6ded4ad2f8..feae1628ad 100644
--- a/src/applications/diffusion/doorkeeper/DiffusionDoorkeeperCommitFeedStoryPublisher.php
+++ b/src/applications/diffusion/doorkeeper/DiffusionDoorkeeperCommitFeedStoryPublisher.php
@@ -1,170 +1,193 @@
<?php
final class DiffusionDoorkeeperCommitFeedStoryPublisher
extends DoorkeeperFeedStoryPublisher {
private $auditRequests;
private $activePHIDs;
private $passivePHIDs;
private function getAuditRequests() {
return $this->auditRequests;
}
public function canPublishStory(PhabricatorFeedStory $story, $object) {
return ($object instanceof PhabricatorRepositoryCommit);
}
public function isStoryAboutObjectCreation($object) {
// TODO: Although creation stories exist, they currently don't have a
// primary object PHID set, so they'll never make it here because they
// won't pass `canPublishStory()`.
return false;
}
+ public function isStoryAboutObjectClosure($object) {
+ // TODO: This isn't quite accurate, but pretty close: check if this story
+ // is a close (which clearly is about object closure) or is an "Accept" and
+ // the commit is fully audited (which is almost certainly a closure).
+ // After ApplicationTransactions, we could annotate feed stories more
+ // explicitly.
+
+ $story = $this->getFeedStory();
+ $action = $story->getStoryData()->getValue('action');
+
+ if ($action == PhabricatorAuditActionConstants::CLOSE) {
+ return true;
+ }
+
+ $fully_audited = PhabricatorAuditCommitStatusConstants::FULLY_AUDITED;
+ if (($action == PhabricatorAuditActionConstants::ACCEPT) &&
+ $object->getAuditStatus() == $fully_audited) {
+ return true;
+ }
+
+ return false;
+ }
+
public function willPublishStory($commit) {
$requests = id(new PhabricatorAuditQuery())
->withCommitPHIDs(array($commit->getPHID()))
->execute();
// TODO: This is messy and should be generalized, but we don't have a good
// query for it yet. Since we run in the daemons, just do the easiest thing
// we can for the moment. Figure out who all of the "active" (need to
// audit) and "passive" (no action necessary) user are.
$auditor_phids = mpull($requests, 'getAuditorPHID');
$objects = id(new PhabricatorObjectHandleData($auditor_phids))
->setViewer($this->getViewer())
->loadObjects();
$active = array();
$passive = array();
foreach ($requests as $request) {
$status = $request->getAuditStatus();
if ($status == PhabricatorAuditStatusConstants::CC) {
// We handle these specially below.
continue;
}
$object = idx($objects, $request->getAuditorPHID());
if (!$object) {
continue;
}
$request_phids = array();
if ($object instanceof PhabricatorUser) {
$request_phids = array($object->getPHID());
} else if ($object instanceof PhabricatorOwnersPackage) {
$request_phids = PhabricatorOwnersOwner::loadAffiliatedUserPHIDs(
array($object->getID()));
} else if ($object instanceof PhabricatorProject) {
$request_phids = $object->loadMemberPHIDs();
} else {
// Dunno what this is.
$request_phids = array();
}
switch ($status) {
case PhabricatorAuditStatusConstants::AUDIT_REQUIRED:
case PhabricatorAuditStatusConstants::AUDIT_REQUESTED:
case PhabricatorAuditStatusConstants::CONCERNED:
$active += array_fuse($request_phids);
break;
default:
$passive += array_fuse($request_phids);
break;
}
}
// Remove "Active" users from the "Passive" list.
$passive = array_diff_key($passive, $active);
$this->activePHIDs = $active;
$this->passivePHIDs = $passive;
$this->auditRequests = $requests;
return $commit;
}
public function getOwnerPHID($object) {
return $object->getAuthorPHID();
}
public function getActiveUserPHIDs($object) {
return $this->activePHIDs;
}
public function getPassiveUserPHIDs($object) {
return $this->passivePHIDs;
}
public function getCCUserPHIDs($object) {
$ccs = array();
foreach ($this->getAuditRequests() as $request) {
if ($request->getAuditStatus() == PhabricatorAuditStatusConstants::CC) {
$ccs[] = $request->getAuditorPHID();
}
}
return $ccs;
}
public function getObjectTitle($object) {
$prefix = $this->getTitlePrefix($object);
$repository = $object->getRepository();
$name = $repository->formatCommitName($object->getCommitIdentifier());
$title = $object->getSummary();
return ltrim("{$prefix} {$name}: {$title}");
}
public function getObjectURI($object) {
$repository = $object->getRepository();
$name = $repository->formatCommitName($object->getCommitIdentifier());
return PhabricatorEnv::getProductionURI('/'.$name);
}
public function getObjectDescription($object) {
$data = $object->loadCommitData();
if ($data) {
return $data->getCommitMessage();
}
return null;
}
public function isObjectClosed($object) {
switch ($object->getAuditStatus()) {
case PhabricatorAuditCommitStatusConstants::NEEDS_AUDIT:
case PhabricatorAuditCommitStatusConstants::CONCERN_RAISED:
case PhabricatorAuditCommitStatusConstants::PARTIALLY_AUDITED:
return false;
default:
return true;
}
}
public function getResponsibilityTitle($object) {
$prefix = $this->getTitlePrefix($object);
return pht('%s Audit', $prefix);
}
public function getStoryText($object) {
$story = $this->getFeedStory();
if ($story instanceof PhabricatorFeedStoryAudit) {
$text = $story->renderForAsanaBridge();
} else {
$text = $story->renderText();
}
return $text;
}
private function getTitlePrefix(PhabricatorRepositoryCommit $commit) {
$prefix_key = 'metamta.diffusion.subject-prefix';
return PhabricatorEnv::getEnvConfig($prefix_key);
}
}
diff --git a/src/applications/doorkeeper/engine/DoorkeeperFeedStoryPublisher.php b/src/applications/doorkeeper/engine/DoorkeeperFeedStoryPublisher.php
index 428a22be67..bd6809d87f 100644
--- a/src/applications/doorkeeper/engine/DoorkeeperFeedStoryPublisher.php
+++ b/src/applications/doorkeeper/engine/DoorkeeperFeedStoryPublisher.php
@@ -1,50 +1,51 @@
<?php
abstract class DoorkeeperFeedStoryPublisher {
private $feedStory;
private $viewer;
public function setFeedStory(PhabricatorFeedStory $feed_story) {
$this->feedStory = $feed_story;
return $this;
}
public function getFeedStory() {
return $this->feedStory;
}
public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
public function getViewer() {
return $this->viewer;
}
abstract public function canPublishStory(
PhabricatorFeedStory $story,
$object);
/**
* Hook for publishers to mutate the story object, particularly by loading
* and attaching additional data.
*/
public function willPublishStory($object) {
return $object;
}
abstract public function isStoryAboutObjectCreation($object);
+ abstract public function isStoryAboutObjectClosure($object);
abstract public function getOwnerPHID($object);
abstract public function getActiveUserPHIDs($object);
abstract public function getPassiveUserPHIDs($object);
abstract public function getCCUserPHIDs($object);
abstract public function getObjectTitle($object);
abstract public function getObjectURI($object);
abstract public function getObjectDescription($object);
abstract public function isObjectClosed($object);
abstract public function getResponsibilityTitle($object);
abstract public function getStoryText($object);
}
diff --git a/src/applications/doorkeeper/worker/DoorkeeperFeedWorkerAsana.php b/src/applications/doorkeeper/worker/DoorkeeperFeedWorkerAsana.php
index f23653a744..3f3cf7e209 100644
--- a/src/applications/doorkeeper/worker/DoorkeeperFeedWorkerAsana.php
+++ b/src/applications/doorkeeper/worker/DoorkeeperFeedWorkerAsana.php
@@ -1,627 +1,629 @@
<?php
final class DoorkeeperFeedWorkerAsana extends FeedPushWorker {
private $provider;
private $publisher;
private $workspaceID;
private $feedStory;
private $storyObject;
private function getProvider() {
if (!$this->provider) {
$provider = PhabricatorAuthProviderOAuthAsana::getAsanaProvider();
if (!$provider) {
throw new PhabricatorWorkerPermanentFailureException(
'No Asana provider configured.');
}
$this->provider = $provider;
}
return $this->provider;
}
private function getWorkspaceID() {
if (!$this->workspaceID) {
$workspace_id = PhabricatorEnv::getEnvConfig('asana.workspace-id');
if (!$workspace_id) {
throw new PhabricatorWorkerPermanentFailureException(
'No workspace Asana ID configured.');
}
$this->workspaceID = $workspace_id;
}
return $this->workspaceID;
}
private function getFeedStory() {
if (!$this->feedStory) {
$story = $this->loadFeedStory();
$this->feedStory = $story;
}
return $this->feedStory;
}
private function getViewer() {
return PhabricatorUser::getOmnipotentUser();
}
private function getPublisher() {
return $this->publisher;
}
private function getStoryObject() {
if (!$this->storyObject) {
$story = $this->getFeedStory();
try {
$object = $story->getPrimaryObject();
} catch (Exception $ex) {
throw new PhabricatorWorkerPermanentFailureException(
$ex->getMessage());
}
$this->storyObject = $object;
}
return $this->storyObject;
}
private function getAsanaTaskData($object) {
$publisher = $this->getPublisher();
$title = $publisher->getObjectTitle($object);
$uri = $publisher->getObjectURI($object);
$description = $publisher->getObjectDescription($object);
$is_completed = $publisher->isObjectClosed($object);
$notes = array(
$description,
$uri,
$this->getSynchronizationWarning(),
);
$notes = implode("\n\n", $notes);
return array(
'name' => $title,
'notes' => $notes,
'completed' => $is_completed,
);
}
private function getAsanaSubtaskData($object) {
$publisher = $this->getPublisher();
$title = $publisher->getResponsibilityTitle($object);
$uri = $publisher->getObjectURI($object);
$description = $publisher->getObjectDescription($object);
$notes = array(
$description,
$uri,
$this->getSynchronizationWarning(),
);
$notes = implode("\n\n", $notes);
return array(
'name' => $title,
'notes' => $notes,
);
}
private function getSynchronizationWarning() {
return
"\xE2\x9A\xA0 DO NOT EDIT THIS TASK \xE2\x9A\xA0\n".
"\xE2\x98\xA0 Your changes will not be reflected in Phabricator.\n".
"\xE2\x98\xA0 Your changes will be destroyed the next time state ".
"is synchronized.";
}
protected function doWork() {
$story = $this->getFeedStory();
$data = $story->getStoryData();
$viewer = $this->getViewer();
$provider = $this->getProvider();
$workspace_id = $this->getWorkspaceID();
$object = $this->getStoryObject();
$src_phid = $object->getPHID();
$chronological_key = $story->getChronologicalKey();
$publishers = id(new PhutilSymbolLoader())
->setAncestorClass('DoorkeeperFeedStoryPublisher')
->loadObjects();
foreach ($publishers as $publisher) {
if ($publisher->canPublishStory($story, $object)) {
$publisher
->setViewer($viewer)
->setFeedStory($story);
$object = $publisher->willPublishStory($object);
$this->storyObject = $object;
$this->publisher = $publisher;
$this->log("Using publisher '%s'.\n", get_class($publisher));
break;
}
}
if (!$this->publisher) {
$this->log("Story is about an unsupported object type.\n");
return;
}
// Figure out all the users related to the object. Users go into one of
// four buckets:
//
// - Owner: the owner of the object. This user becomes the assigned owner
// of the parent task.
// - Active: users who are responsible for the object and need to act on
// it. For example, reviewers of a "needs review" revision.
// - Passive: users who are responsible for the object, but do not need
// to act on it right now. For example, reviewers of a "needs revision"
// revision.
// - Follow: users who are following the object; generally CCs.
$owner_phid = $publisher->getOwnerPHID($object);
$active_phids = $publisher->getActiveUserPHIDs($object);
$passive_phids = $publisher->getPassiveUserPHIDs($object);
$follow_phids = $publisher->getCCUserPHIDs($object);
$all_phids = array();
$all_phids = array_merge(
array($owner_phid),
$active_phids,
$passive_phids,
$follow_phids);
$all_phids = array_unique(array_filter($all_phids));
$phid_aid_map = $this->lookupAsanaUserIDs($all_phids);
if (!$phid_aid_map) {
throw new PhabricatorWorkerPermanentFailureException(
'No related users have linked Asana accounts.');
}
$owner_asana_id = idx($phid_aid_map, $owner_phid);
$all_asana_ids = array_select_keys($phid_aid_map, $all_phids);
$all_asana_ids = array_values($all_asana_ids);
// Even if the actor isn't a reviewer, etc., try to use their account so
// we can post in the correct voice. If we miss, we'll try all the other
// related users.
$try_users = array_merge(
array($data->getAuthorPHID()),
array_keys($phid_aid_map));
$try_users = array_filter($try_users);
list($possessed_user, $oauth_token) = $this->findAnyValidAsanaAccessToken(
$try_users);
if (!$oauth_token) {
throw new PhabricatorWorkerPermanentFailureException(
'Unable to find any Asana user with valid credentials to '.
'pull an OAuth token out of.');
}
$etype_main = PhabricatorEdgeConfig::TYPE_PHOB_HAS_ASANATASK;
$etype_sub = PhabricatorEdgeConfig::TYPE_PHOB_HAS_ASANASUBTASK;
$equery = id(new PhabricatorEdgeQuery())
->withSourcePHIDs(array($src_phid))
->withEdgeTypes(
array(
$etype_main,
$etype_sub,
))
->needEdgeData(true);
$edges = $equery->execute();
$main_edge = head($edges[$src_phid][$etype_main]);
$main_data = $this->getAsanaTaskData($object) + array(
'assignee' => $owner_asana_id,
'followers' => $all_asana_ids,
);
$extra_data = array();
if ($main_edge) {
$extra_data = $main_edge['data'];
$refs = id(new DoorkeeperImportEngine())
->setViewer($possessed_user)
->withPHIDs(array($main_edge['dst']))
->execute();
$parent_ref = head($refs);
if (!$parent_ref) {
throw new PhabricatorWorkerPermanentFailureException(
'DoorkeeperExternalObject could not be loaded.');
}
if ($parent_ref->getSyncFailed()) {
throw new Exception(
'Synchronization of parent task from Asana failed!');
} else if (!$parent_ref->getIsVisible()) {
$this->log("Skipping main task update, object is no longer visible.\n");
$extra_data['gone'] = true;
} else {
$edge_cursor = idx($main_edge['data'], 'cursor', 0);
// TODO: This probably breaks, very rarely, on 32-bit systems.
if ($edge_cursor <= $story->getChronologicalKey()) {
$this->log("Updating main task.\n");
// We need to synchronize follower data separately.
$put_data = $main_data;
unset($put_data['followers']);
$this->makeAsanaAPICall(
$oauth_token,
"tasks/".$parent_ref->getObjectID(),
'PUT',
$put_data);
// To synchronize follower data, just add all the followers. The task
// might have additional followers, but we can't really tell how they
// got there: were they CC'd and then unsubscribed, or did they
// manually follow the task? Assume the latter since it's easier and
// less destructive and the former is rare.
if ($main_data['followers']) {
$this->makeAsanaAPICall(
$oauth_token,
'tasks/'.$parent_ref->getObjectID().'/addFollowers',
'POST',
array(
'followers' => $main_data['followers'],
));
}
} else {
$this->log(
"Skipping main task update, cursor is ahead of the story.\n");
}
}
} else {
$parent = $this->makeAsanaAPICall(
$oauth_token,
'tasks',
'POST',
array(
'workspace' => $workspace_id,
// NOTE: We initially create parent tasks in the "Later" state but
// don't update it afterward, even if the corresponding object
// becomes actionable. The expectation is that users will prioritize
// tasks in responses to notifications of state changes, and that
// we should not overwrite their choices.
'assignee_status' => 'later',
) + $main_data);
$parent_ref = $this->newRefFromResult(
DoorkeeperBridgeAsana::OBJTYPE_TASK,
$parent);
$extra_data = array(
'workspace' => $workspace_id,
);
}
$dst_phid = $parent_ref->getExternalObject()->getPHID();
// Update the main edge.
$edge_data = array(
'cursor' => $story->getChronologicalKey(),
) + $extra_data;
$edge_options = array(
'data' => $edge_data,
);
id(new PhabricatorEdgeEditor())
->setActor($viewer)
->addEdge($src_phid, $etype_main, $dst_phid, $edge_options)
->save();
if (!$parent_ref->getIsVisible()) {
throw new PhabricatorWorkerPermanentFailureException(
'DoorkeeperExternalObject has no visible object on the other side; '.
'this likely indicates the Asana task has been deleted.');
}
// Now, handle the subtasks.
$sub_editor = id(new PhabricatorEdgeEditor())
->setActor($viewer);
// First, find all the object references in Phabricator for tasks that we
// know about and import their objects from Asana.
$sub_edges = $edges[$src_phid][$etype_sub];
$sub_refs = array();
$subtask_data = $this->getAsanaSubtaskData($object);
$have_phids = array();
if ($sub_edges) {
$refs = id(new DoorkeeperImportEngine())
->setViewer($possessed_user)
->withPHIDs(array_keys($sub_edges))
->execute();
foreach ($refs as $ref) {
if ($ref->getSyncFailed()) {
throw new Exception(
'Synchronization of child task from Asana failed!');
}
if (!$ref->getIsVisible()) {
$ref->getExternalObject()->delete();
continue;
}
$have_phids[$ref->getExternalObject()->getPHID()] = $ref;
}
}
// Remove any edges in Phabricator which don't have valid tasks in Asana.
// These are likely tasks which have been deleted. We're going to respawn
// them.
foreach ($sub_edges as $sub_phid => $sub_edge) {
if (isset($have_phids[$sub_phid])) {
continue;
}
$this->log(
"Removing subtask edge to %s, foreign object is not visible.\n",
$sub_phid);
$sub_editor->removeEdge($src_phid, $etype_sub, $sub_phid);
unset($sub_edges[$sub_phid]);
}
// For each active or passive user, we're looking for an existing, valid
// task. If we find one we're going to update it; if we don't, we'll
// create one. We ignore extra subtasks that we didn't create (we gain
// nothing by deleting them and might be nuking something important) and
// ignore subtasks which have been moved across workspaces or replanted
// under new parents (this stuff is too edge-casey to bother checking for
// and complicated to fix, as it needs extra API calls). However, we do
// clean up subtasks we created whose owners are no longer associated
// with the object.
$subtask_states = array_fill_keys($active_phids, false) +
array_fill_keys($passive_phids, true);
// Continue with only those users who have Asana credentials.
$subtask_states = array_select_keys(
$subtask_states,
array_keys($phid_aid_map));
$need_subtasks = $subtask_states;
$user_to_ref_map = array();
$nuke_refs = array();
foreach ($sub_edges as $sub_phid => $sub_edge) {
$user_phid = idx($sub_edge['data'], 'userPHID');
if (isset($need_subtasks[$user_phid])) {
unset($need_subtasks[$user_phid]);
$user_to_ref_map[$user_phid] = $have_phids[$sub_phid];
} else {
// This user isn't associated with the object anymore, so get rid
// of their task and edge.
$nuke_refs[$sub_phid] = $have_phids[$sub_phid];
}
}
// These are tasks we know about but which are no longer relevant -- for
// example, because a user has been removed as a reviewer. Remove them and
// their edges.
foreach ($nuke_refs as $sub_phid => $ref) {
$sub_editor->removeEdge($src_phid, $etype_sub, $sub_phid);
$this->makeAsanaAPICall(
$oauth_token,
'tasks/'.$ref->getObjectID(),
'DELETE',
array());
$ref->getExternalObject()->delete();
}
// For each user that we don't have a subtask for, create a new subtask.
foreach ($need_subtasks as $user_phid => $is_completed) {
$subtask = $this->makeAsanaAPICall(
$oauth_token,
'tasks',
'POST',
$subtask_data + array(
'assignee' => $phid_aid_map[$user_phid],
'completed' => $is_completed,
'parent' => $parent_ref->getObjectID(),
));
$subtask_ref = $this->newRefFromResult(
DoorkeeperBridgeAsana::OBJTYPE_TASK,
$subtask);
$user_to_ref_map[$user_phid] = $subtask_ref;
// We don't need to synchronize this subtask's state because we just
// set it when we created it.
unset($subtask_states[$user_phid]);
// Add an edge to track this subtask.
$sub_editor->addEdge(
$src_phid,
$etype_sub,
$subtask_ref->getExternalObject()->getPHID(),
array(
'data' => array(
'userPHID' => $user_phid,
),
));
}
// Synchronize all the previously-existing subtasks.
foreach ($subtask_states as $user_phid => $is_completed) {
$this->makeAsanaAPICall(
$oauth_token,
'tasks/'.$user_to_ref_map[$user_phid]->getObjectID(),
'PUT',
$subtask_data + array(
'assignee' => $phid_aid_map[$user_phid],
'completed' => $is_completed,
));
}
// Update edges on our side.
$sub_editor->save();
// Don't publish the "create" story, since pushing the object into Asana
// naturally generates a notification which effectively serves the same
- // purpose as the "create" story.
- if (!$publisher->isStoryAboutObjectCreation($object)) {
+ // purpose as the "create" story. Similarly, "close" stories generate a
+ // close notification.
+ if (!$publisher->isStoryAboutObjectCreation($object) &&
+ !$publisher->isStoryAboutObjectClosure($object)) {
// Post the feed story itself to the main Asana task. We do this last
// because everything else is idempotent, so this is the only effect we
// can't safely run more than once.
$text = $publisher->getStoryText($object);
$this->makeAsanaAPICall(
$oauth_token,
'tasks/'.$parent_ref->getObjectID().'/stories',
'POST',
array(
'text' => $text,
));
}
}
private function lookupAsanaUserIDs($all_phids) {
$phid_map = array();
$all_phids = array_unique(array_filter($all_phids));
if (!$all_phids) {
return $phid_map;
}
$provider = PhabricatorAuthProviderOAuthAsana::getAsanaProvider();
$accounts = id(new PhabricatorExternalAccountQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withUserPHIDs($all_phids)
->withAccountTypes(array($provider->getProviderType()))
->withAccountDomains(array($provider->getProviderDomain()))
->execute();
foreach ($accounts as $account) {
$phid_map[$account->getUserPHID()] = $account->getAccountID();
}
// Put this back in input order.
$phid_map = array_select_keys($phid_map, $all_phids);
return $phid_map;
}
private function findAnyValidAsanaAccessToken(array $user_phids) {
if (!$user_phids) {
return array(null, null);
}
$provider = $this->getProvider();
$viewer = $this->getViewer();
$accounts = id(new PhabricatorExternalAccountQuery())
->setViewer($viewer)
->withUserPHIDs($user_phids)
->withAccountTypes(array($provider->getProviderType()))
->withAccountDomains(array($provider->getProviderDomain()))
->execute();
// Reorder accounts in the original order.
// TODO: This needs to be adjusted if/when we allow you to link multiple
// accounts.
$accounts = mpull($accounts, null, 'getUserPHID');
$accounts = array_select_keys($accounts, $user_phids);
$workspace_id = $this->getWorkspaceID();
foreach ($accounts as $account) {
// Get a token if possible.
$token = $provider->getOAuthAccessToken($account);
if (!$token) {
continue;
}
// Verify we can actually make a call with the token, and that the user
// has access to the workspace in question.
try {
id(new PhutilAsanaFuture())
->setAccessToken($token)
->setRawAsanaQuery("workspaces/{$workspace_id}")
->resolve();
} catch (Exception $ex) {
// This token didn't make it through; try the next account.
continue;
}
$user = id(new PhabricatorPeopleQuery())
->setViewer($viewer)
->withPHIDs(array($account->getUserPHID()))
->executeOne();
if ($user) {
return array($user, $token);
}
}
return array(null, null);
}
private function makeAsanaAPICall($token, $action, $method, array $params) {
foreach ($params as $key => $value) {
if ($value === null) {
unset($params[$key]);
} else if (is_array($value)) {
unset($params[$key]);
foreach ($value as $skey => $svalue) {
$params[$key.'['.$skey.']'] = $svalue;
}
}
}
return id(new PhutilAsanaFuture())
->setAccessToken($token)
->setMethod($method)
->setRawAsanaQuery($action, $params)
->resolve();
}
private function newRefFromResult($type, $result) {
$ref = id(new DoorkeeperObjectRef())
->setApplicationType(DoorkeeperBridgeAsana::APPTYPE_ASANA)
->setApplicationDomain(DoorkeeperBridgeAsana::APPDOMAIN_ASANA)
->setObjectType($type)
->setObjectID($result['id'])
->setIsVisible(true);
$xobj = $ref->newExternalObject();
$ref->attachExternalObject($xobj);
$bridge = new DoorkeeperBridgeAsana();
$bridge->fillObjectFromData($xobj, $result);
$xobj->save();
return $ref;
}
public function getMaximumRetryCount() {
return 4;
}
public function getWaitBeforeRetry(PhabricatorWorkerTask $task) {
$count = $task->getFailureCount();
return (5 * 60) * pow(8, $count);
}
}

File Metadata

Mime Type
text/x-diff
Expires
Mon, Jul 28, 4:12 AM (1 w, 23 h ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
186411
Default Alt Text
(31 KB)

Event Timeline