@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Bridge GitHub users into Phabricator and attribute actions to them

Summary:
Ref T10538. Ref T10537. This creates PHIDs which represent GitHub users, and uses them as the actors for synchronized comments.

I've just made them Doorkeeper objects. There are three major kinds of objects they //could// possibly be:

- Nuance requestor objects.
- External account objects.
- Doorkeeper objects.

I don't think we actually need distinct nuance requestor objects. These don't really do anything right now, and were originally created before Doorkeeper. I think Doorkeeper is a superset of nuance requestor functionality, and better developed and more flexible.

Likewise, doorkeeper objects are much more flexible than external account objects, and it's nice to imagine that we can import from Twootfeed or whatever without needing to build full OAuth for it. I also like less stuff touching auth code, when possible.

Making these separate from external accounts does make it a bit harder to reconcile external users with internal users, but I think that's OK, and that it's generally desirable to show the real source of a piece of content. That is, if I wrote a comment on GitHub but also have a Phabricator account, I think it's good to show "epriestley (GitHub)" (the GitHub user) as the author, not "epriestley" (the Phabricator user). I think this is generally less confusing overall, and we can add more linkage later to make it clearer.

Test Plan:
{F1194104}

{F1194105}

Reviewers: chad

Reviewed By: chad

Maniphest Tasks: T10537, T10538

Differential Revision: https://secure.phabricator.com/D15541

+293 -38
+4
src/__phutil_library_map__.php
··· 843 843 'DoorkeeperBridgeAsana' => 'applications/doorkeeper/bridge/DoorkeeperBridgeAsana.php', 844 844 'DoorkeeperBridgeGitHub' => 'applications/doorkeeper/bridge/DoorkeeperBridgeGitHub.php', 845 845 'DoorkeeperBridgeGitHubIssue' => 'applications/doorkeeper/bridge/DoorkeeperBridgeGitHubIssue.php', 846 + 'DoorkeeperBridgeGitHubUser' => 'applications/doorkeeper/bridge/DoorkeeperBridgeGitHubUser.php', 846 847 'DoorkeeperBridgeJIRA' => 'applications/doorkeeper/bridge/DoorkeeperBridgeJIRA.php', 847 848 'DoorkeeperBridgeJIRATestCase' => 'applications/doorkeeper/bridge/__tests__/DoorkeeperBridgeJIRATestCase.php', 848 849 'DoorkeeperBridgedObjectCurtainExtension' => 'applications/doorkeeper/engineextension/DoorkeeperBridgedObjectCurtainExtension.php', 849 850 'DoorkeeperBridgedObjectInterface' => 'applications/doorkeeper/bridge/DoorkeeperBridgedObjectInterface.php', 850 851 'DoorkeeperDAO' => 'applications/doorkeeper/storage/DoorkeeperDAO.php', 851 852 'DoorkeeperExternalObject' => 'applications/doorkeeper/storage/DoorkeeperExternalObject.php', 853 + 'DoorkeeperExternalObjectPHIDType' => 'applications/doorkeeper/phid/DoorkeeperExternalObjectPHIDType.php', 852 854 'DoorkeeperExternalObjectQuery' => 'applications/doorkeeper/query/DoorkeeperExternalObjectQuery.php', 853 855 'DoorkeeperFeedStoryPublisher' => 'applications/doorkeeper/engine/DoorkeeperFeedStoryPublisher.php', 854 856 'DoorkeeperFeedWorker' => 'applications/doorkeeper/worker/DoorkeeperFeedWorker.php', ··· 5017 5019 'DoorkeeperBridgeAsana' => 'DoorkeeperBridge', 5018 5020 'DoorkeeperBridgeGitHub' => 'DoorkeeperBridge', 5019 5021 'DoorkeeperBridgeGitHubIssue' => 'DoorkeeperBridgeGitHub', 5022 + 'DoorkeeperBridgeGitHubUser' => 'DoorkeeperBridgeGitHub', 5020 5023 'DoorkeeperBridgeJIRA' => 'DoorkeeperBridge', 5021 5024 'DoorkeeperBridgeJIRATestCase' => 'PhabricatorTestCase', 5022 5025 'DoorkeeperBridgedObjectCurtainExtension' => 'PHUICurtainExtension', ··· 5025 5028 'DoorkeeperDAO', 5026 5029 'PhabricatorPolicyInterface', 5027 5030 ), 5031 + 'DoorkeeperExternalObjectPHIDType' => 'PhabricatorPHIDType', 5028 5032 'DoorkeeperExternalObjectQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 5029 5033 'DoorkeeperFeedStoryPublisher' => 'Phobject', 5030 5034 'DoorkeeperFeedWorker' => 'FeedPushWorker',
+120
src/applications/doorkeeper/bridge/DoorkeeperBridgeGitHubUser.php
··· 1 + <?php 2 + 3 + final class DoorkeeperBridgeGitHubUser 4 + extends DoorkeeperBridgeGitHub { 5 + 6 + const OBJTYPE_GITHUB_USER = 'github.user'; 7 + 8 + public function canPullRef(DoorkeeperObjectRef $ref) { 9 + if (!parent::canPullRef($ref)) { 10 + return false; 11 + } 12 + 13 + if ($ref->getObjectType() !== self::OBJTYPE_GITHUB_USER) { 14 + return false; 15 + } 16 + 17 + return true; 18 + } 19 + 20 + public function pullRefs(array $refs) { 21 + $token = $this->getGitHubAccessToken(); 22 + if (!strlen($token)) { 23 + return null; 24 + } 25 + 26 + $template = id(new PhutilGitHubFuture()) 27 + ->setAccessToken($token); 28 + 29 + $futures = array(); 30 + $id_map = mpull($refs, 'getObjectID', 'getObjectKey'); 31 + foreach ($id_map as $key => $id) { 32 + // GitHub doesn't provide a way to query for users by ID directly, but we 33 + // can list all users, ordered by ID, starting at some particular ID, 34 + // with a page size of one, which will achieve the desired effect. 35 + $one_less = ($id - 1); 36 + $uri = "/users?since={$one_less}&per_page=1"; 37 + 38 + $data = array(); 39 + $futures[$key] = id(clone $template) 40 + ->setRawGitHubQuery($uri, $data); 41 + } 42 + 43 + $results = array(); 44 + $failed = array(); 45 + foreach (new FutureIterator($futures) as $key => $future) { 46 + try { 47 + $results[$key] = $future->resolve(); 48 + } catch (Exception $ex) { 49 + if (($ex instanceof HTTPFutureResponseStatus) && 50 + ($ex->getStatusCode() == 404)) { 51 + // TODO: Do we end up here for deleted objects and invisible 52 + // objects? 53 + } else { 54 + phlog($ex); 55 + $failed[$key] = $ex; 56 + } 57 + } 58 + } 59 + 60 + $viewer = $this->getViewer(); 61 + 62 + foreach ($refs as $ref) { 63 + $ref->setAttribute('name', pht('GitHub User %s', $ref->getObjectID())); 64 + 65 + $did_fail = idx($failed, $ref->getObjectKey()); 66 + if ($did_fail) { 67 + $ref->setSyncFailed(true); 68 + continue; 69 + } 70 + 71 + $result = idx($results, $ref->getObjectKey()); 72 + if (!$result) { 73 + continue; 74 + } 75 + 76 + $body = $result->getBody(); 77 + if (!is_array($body) || !count($body)) { 78 + $ref->setSyncFailed(true); 79 + continue; 80 + } 81 + 82 + $spec = head($body); 83 + if (!is_array($spec)) { 84 + $ref->setSyncFailed(true); 85 + continue; 86 + } 87 + 88 + // Because we're using a paging query to load each user, if a user (say, 89 + // user ID 123) does not exist for some reason, we might get the next 90 + // user (say, user ID 124) back. Make sure the user we got back is really 91 + // the user we expect. 92 + $id = idx($spec, 'id'); 93 + if ($id !== $ref->getObjectID()) { 94 + $ref->setSyncFailed(true); 95 + continue; 96 + } 97 + 98 + $ref->setIsVisible(true); 99 + $ref->setAttribute('api.raw', $spec); 100 + $ref->setAttribute('name', $spec['login']); 101 + 102 + $obj = $ref->getExternalObject(); 103 + $this->fillObjectFromData($obj, $spec); 104 + 105 + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); 106 + $obj->save(); 107 + unset($unguarded); 108 + } 109 + } 110 + 111 + public function fillObjectFromData(DoorkeeperExternalObject $obj, $spec) { 112 + $uri = $spec['html_url']; 113 + $obj->setObjectURI($uri); 114 + 115 + $login = $spec['login']; 116 + 117 + $obj->setDisplayName(pht('%s <%s>', $login, pht('GitHub'))); 118 + } 119 + 120 + }
+47
src/applications/doorkeeper/phid/DoorkeeperExternalObjectPHIDType.php
··· 1 + <?php 2 + 3 + final class DoorkeeperExternalObjectPHIDType 4 + extends PhabricatorPHIDType { 5 + 6 + const TYPECONST = 'XOBJ'; 7 + 8 + public function getTypeName() { 9 + return pht('External Object'); 10 + } 11 + 12 + public function newObject() { 13 + return new DoorkeeperExternalObject(); 14 + } 15 + 16 + public function getPHIDTypeApplicationClass() { 17 + return 'PhabricatorDoorkeeperApplication'; 18 + } 19 + 20 + protected function buildQueryForObjects( 21 + PhabricatorObjectQuery $query, 22 + array $phids) { 23 + 24 + return id(new DoorkeeperExternalObjectQuery()) 25 + ->withPHIDs($phids); 26 + } 27 + 28 + public function loadHandles( 29 + PhabricatorHandleQuery $query, 30 + array $handles, 31 + array $objects) { 32 + 33 + foreach ($handles as $phid => $handle) { 34 + $xobj = $objects[$phid]; 35 + 36 + $uri = $xobj->getObjectURI(); 37 + $name = $xobj->getDisplayName(); 38 + $full_name = $xobj->getDisplayFullName(); 39 + 40 + $handle 41 + ->setURI($uri) 42 + ->setName($name) 43 + ->setFullName($full_name); 44 + } 45 + } 46 + 47 + }
+12 -20
src/applications/doorkeeper/query/DoorkeeperExternalObjectQuery.php
··· 16 16 return $this; 17 17 } 18 18 19 - protected function loadPage() { 20 - $table = new DoorkeeperExternalObject(); 21 - $conn_r = $table->establishConnection('r'); 22 - 23 - $data = queryfx_all( 24 - $conn_r, 25 - 'SELECT * FROM %T %Q %Q %Q', 26 - $table->getTableName(), 27 - $this->buildWhereClause($conn_r), 28 - $this->buildOrderClause($conn_r), 29 - $this->buildLimitClause($conn_r)); 19 + public function newResultObject() { 20 + return new DoorkeeperExternalObject(); 21 + } 30 22 31 - return $table->loadAllFromArray($data); 23 + protected function loadPage() { 24 + return $this->loadStandardPage($this->newResultObject()); 32 25 } 33 26 34 - protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { 35 - $where = array(); 27 + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { 28 + $where = parent::buildWhereClauseParts($conn); 36 29 37 - if ($this->phids) { 30 + if ($this->phids !== null) { 38 31 $where[] = qsprintf( 39 - $conn_r, 32 + $conn, 40 33 'phid IN (%Ls)', 41 34 $this->phids); 42 35 } 43 36 44 - if ($this->objectKeys) { 37 + if ($this->objectKeys !== null) { 45 38 $where[] = qsprintf( 46 - $conn_r, 39 + $conn, 47 40 'objectKey IN (%Ls)', 48 41 $this->objectKeys); 49 42 } 50 43 51 - $where[] = $this->buildPagingClause($conn_r); 52 - return $this->formatWhereClause($where); 44 + return $where; 53 45 } 54 46 55 47 public function getQueryApplicationClass() {
+22 -1
src/applications/doorkeeper/storage/DoorkeeperExternalObject.php
··· 47 47 48 48 public function generatePHID() { 49 49 return PhabricatorPHID::generateNewPHID( 50 - PhabricatorPHIDConstants::PHID_TYPE_XOBJ); 50 + DoorkeeperExternalObjectPHIDType::TYPECONST); 51 51 } 52 52 53 53 public function getProperty($key, $default = null) { ··· 83 83 return parent::save(); 84 84 } 85 85 86 + public function setDisplayName($display_name) { 87 + return $this->setProperty('xobj.name.display', $display_name); 88 + } 89 + 90 + public function getDisplayName() { 91 + return $this->getProperty('xobj.name.display', pht('External Object')); 92 + } 93 + 94 + public function setDisplayFullName($full_name) { 95 + return $this->setProperty('xobj.name.display-full', $full_name); 96 + } 97 + 98 + public function getDisplayFullName() { 99 + $full_name = $this->getProperty('xobj.name.display-full'); 100 + 101 + if ($full_name !== null) { 102 + return $full_name; 103 + } 104 + 105 + return $this->getDisplayName(); 106 + } 86 107 87 108 /* -( PhabricatorPolicyInterface )----------------------------------------- */ 88 109
+88 -14
src/applications/nuance/item/NuanceGitHubEventItemType.php
··· 6 6 const ITEMTYPE = 'github.event'; 7 7 8 8 private $externalObject; 9 + private $externalActor; 9 10 10 11 public function getItemTypeDisplayName() { 11 12 return pht('GitHub Event'); ··· 27 28 $viewer = $this->getViewer(); 28 29 $is_dirty = false; 29 30 30 - // TODO: Link up the requestor, etc. 31 - 32 - $is_dirty = false; 33 - 34 31 $xobj = $this->reloadExternalObject($item); 35 - 36 32 if ($xobj) { 37 33 $item->setItemProperty('doorkeeper.xobj.phid', $xobj->getPHID()); 38 34 $is_dirty = true; 39 35 } 40 36 37 + $actor = $this->reloadExternalActor($item); 38 + if ($actor) { 39 + $item->setRequestorPHID($actor->getPHID()); 40 + $is_dirty = true; 41 + } 42 + 41 43 if ($item->getStatus() == NuanceItem::STATUS_IMPORTING) { 42 44 $item->setStatus(NuanceItem::STATUS_ROUTING); 43 45 $is_dirty = true; ··· 48 50 } 49 51 } 50 52 53 + private function getDoorkeeperActorRef(NuanceItem $item) { 54 + $raw = $this->newRawEvent($item); 55 + 56 + $user_id = $raw->getActorGitHubUserID(); 57 + if (!$user_id) { 58 + return null; 59 + } 60 + 61 + $ref_type = DoorkeeperBridgeGitHubUser::OBJTYPE_GITHUB_USER; 62 + 63 + return $this->newDoorkeeperRef() 64 + ->setObjectType($ref_type) 65 + ->setObjectID($user_id); 66 + } 67 + 51 68 private function getDoorkeeperRef(NuanceItem $item) { 52 69 $raw = $this->newRawEvent($item); 53 70 ··· 64 81 return null; 65 82 } 66 83 84 + return $this->newDoorkeeperRef() 85 + ->setObjectType($ref_type) 86 + ->setObjectID($full_ref); 87 + } 88 + 89 + private function newDoorkeeperRef() { 67 90 return id(new DoorkeeperObjectRef()) 68 91 ->setApplicationType(DoorkeeperBridgeGitHub::APPTYPE_GITHUB) 69 - ->setApplicationDomain(DoorkeeperBridgeGitHub::APPDOMAIN_GITHUB) 70 - ->setObjectType($ref_type) 71 - ->setObjectID($full_ref); 92 + ->setApplicationDomain(DoorkeeperBridgeGitHub::APPDOMAIN_GITHUB); 72 93 } 73 94 74 95 private function reloadExternalObject(NuanceItem $item, $local = false) { ··· 77 98 return null; 78 99 } 79 100 101 + $xobj = $this->reloadExternalRef($item, $ref, $local); 102 + 103 + if ($xobj) { 104 + $this->externalObject = $xobj; 105 + } 106 + 107 + return $xobj; 108 + } 109 + 110 + private function reloadExternalActor(NuanceItem $item, $local = false) { 111 + $ref = $this->getDoorkeeperActorRef($item); 112 + if (!$ref) { 113 + return null; 114 + } 115 + 116 + $xobj = $this->reloadExternalRef($item, $ref, $local); 117 + 118 + if ($xobj) { 119 + $this->externalActor = $xobj; 120 + } 121 + 122 + return $xobj; 123 + } 124 + 125 + private function reloadExternalRef( 126 + NuanceItem $item, 127 + DoorkeeperObjectRef $ref, 128 + $local) { 129 + 80 130 $source = $item->getSource(); 81 131 $token = $source->getSourceProperty('github.token'); 82 132 $token = new PhutilOpaqueEnvelope($token); ··· 97 147 $xobj = $ref->getExternalObject(); 98 148 } 99 149 100 - if ($xobj) { 101 - $this->externalObject = $xobj; 102 - } 103 - 104 150 return $xobj; 105 151 } 106 152 ··· 121 167 return null; 122 168 } 123 169 170 + private function getExternalActor(NuanceItem $item) { 171 + if ($this->externalActor === null) { 172 + $xobj = $this->reloadExternalActor($item, $local = true); 173 + if ($xobj) { 174 + $this->externalActor = $xobj; 175 + } else { 176 + $this->externalActor = false; 177 + } 178 + } 179 + 180 + if ($this->externalActor) { 181 + return $this->externalActor; 182 + } 183 + 184 + return null; 185 + } 186 + 124 187 private function newRawEvent(NuanceItem $item) { 125 188 $type = $item->getItemProperty('api.type'); 126 189 $raw = $item->getItemProperty('api.raw', array()); ··· 172 235 ->setHeaderText(pht('Imported As')) 173 236 ->appendChild($viewer->renderHandle($task->getPHID())); 174 237 } 238 + } 239 + 240 + $xactor = $this->getExternalActor($item); 241 + if ($xactor) { 242 + $panels[] = $this->newCurtainPanel($item) 243 + ->setHeaderText(pht('GitHub Actor')) 244 + ->appendChild($viewer->renderHandle($xactor->getPHID())); 175 245 } 176 246 177 247 return $panels; ··· 363 433 } 364 434 365 435 protected function getActingAsPHID(NuanceItem $item) { 366 - // TODO: This should be an external account PHID representing the original 367 - // GitHub user. 436 + $actor_phid = $item->getRequestorPHID(); 437 + 438 + if ($actor_phid) { 439 + return $actor_phid; 440 + } 441 + 368 442 return parent::getActingAsPHID($item); 369 443 } 370 444
-2
src/applications/phid/PhabricatorPHIDConstants.php
··· 10 10 11 11 const PHID_TYPE_XCMT = 'XCMT'; 12 12 13 - const PHID_TYPE_XOBJ = 'XOBJ'; 14 - 15 13 const PHID_TYPE_VOID = 'VOID'; 16 14 const PHID_VOID = 'PHID-VOID-00000000000000000000'; 17 15
-1
src/infrastructure/edges/constants/PhabricatorEdgeConfig.php
··· 17 17 18 18 static $class_map = array( 19 19 PhabricatorPHIDConstants::PHID_TYPE_TOBJ => 'HarbormasterObject', 20 - PhabricatorPHIDConstants::PHID_TYPE_XOBJ => 'DoorkeeperExternalObject', 21 20 ); 22 21 23 22 $class = idx($class_map, $phid_type);