From 3f896f946505a303db52d6779b9a6958744b9f97 Mon Sep 17 00:00:00 2001 From: Mynacol Date: Sat, 4 Jun 2022 23:59:10 +0200 Subject: [PATCH] [GitlabIssueBridge] Add bridge (#2760) * [GitlabIssueBridge] new bridge This tracks issue comments on arbitrary gitlab projects. * [GitlabIssueBridge] Prepare for Merge Request support + fixes - Proper UIDs - Default bridge name fixed - Fix cache identifiers - Add TODOs * [GitlabIssueBridge] creation timestamp preferred And prefer original author over editor. * [GitlabIssueBridge] Do not add date to item title Prettier without it. * [GitlabIssueBridge] Support Merge Requests This bridge can now generate feeds for Merge Requests. * [GitlabIssueBridge] typo * [GitlabIssueBridge] Fix Img src attr in comments * [GitlabIssueBridge] Fix function call * [GitlabIssueBridge] Fix test Use gitlab.com if no h parameter was given. Fixes a phpunit test. * [GitlabIssueBridge] linting * [GitlabIssueBridge] Add MR support to description * [GitlabIssueBridge] Move function collectData * [GitlabIssueBridge] rm single-use class constants * [GitlabIssueBridge] Remove manual caching Just depend on rss-bridges built-in caching. --- bridges/GitlabIssueBridge.php | 209 ++++++++++++++++++++++++++++++++++ 1 file changed, 209 insertions(+) create mode 100644 bridges/GitlabIssueBridge.php diff --git a/bridges/GitlabIssueBridge.php b/bridges/GitlabIssueBridge.php new file mode 100644 index 00000000..caf94568 --- /dev/null +++ b/bridges/GitlabIssueBridge.php @@ -0,0 +1,209 @@ + array( + 'h' => array( + 'name' => 'Gitlab instance host name', + 'exampleValue' => 'gitlab.com', + 'defaultValue' => 'gitlab.com', + 'required' => true + ), + 'u' => array( + 'name' => 'User/Organization name', + 'exampleValue' => 'fdroid', + 'required' => true + ), + 'p' => array( + 'name' => 'Project name', + 'exampleValue' => 'fdroidclient', + 'required' => true + ) + + ), + 'Issue comments' => array( + 'i' => array( + 'name' => 'Issue number', + 'type' => 'number', + 'exampleValue' => '2099', + 'required' => true + ) + ), + 'Merge Request comments' => array( + 'i' => array( + 'name' => 'Merge Request number', + 'type' => 'number', + 'exampleValue' => '2099', + 'required' => true + ) + ) + ); + + public function getName(){ + $name = $this->getInput('h') . '/' . $this->getInput('u') . '/' . $this->getInput('p'); + switch ($this->queriedContext) { + case 'Issue comments': + $name .= ' Issue #' . $this->getInput('i'); + break; + case 'Merge Request comments': + $name .= ' MR !' . $this->getInput('i'); + break; + default: + return parent::getName(); + } + return $name; + } + + private function getProjectURI() { + $host = $this->getInput('h') ?? 'gitlab.com'; + return 'https://' . $host . '/' . $this->getInput('u') . '/' + . $this->getInput('p') . '/'; + } + + public function getURI() { + $uri = $this->getProjectURI(); + switch ($this->queriedContext) { + case 'Issue comments': + $uri .= '-/issues'; + break; + case 'Merge Request comments': + $uri .= '-/merge_requests'; + break; + default: + return $uri; + } + $uri .= '/' . $this->getInput('i'); + return $uri; + } + + public function getIcon() { + return 'https://' . $this->getInput('h') . '/favicon.ico'; + } + + public function collectData() { + switch ($this->queriedContext) { + case 'Issue comments': + $this->items[] = $this->parseIssueDescription(); + break; + case 'Merge Request comments': + $this->items[] = $this->parseMRDescription(); + break; + default: + break; + } + + /* parse issue/MR comments */ + $comments_uri = $this->getURI() . '/discussions.json'; + $comments = getContents($comments_uri); + $comments = json_decode($comments, false); + + foreach ($comments as $value) { + foreach ($value->notes as $comment) { + $item = array(); + $item['uri'] = $comment->noteable_note_url; + $item['uid'] = $item['uri']; + + // TODO fix invalid timestamps (fdroid bot) + $item['timestamp'] = $comment->created_at ?? $comment->updated_at ?? $comment->last_edited_at; + $author = $comment->author ?? $comment->last_edited_by; + $item['author'] = ' ' . $author->name . ' @' . $author->username . ''; + + $content = ''; + if ($comment->system) { + $content = $comment->note_html; + if ($comment->type === 'StateNote') { + $content .= ' the issue'; + } elseif ($comment->type === null) { + // e.g. "added 900 commits\n800 from master\n175h4d - commit message\n..." + $content = str_get_html($comment->note_html)->find('p', 0); + } + } else { + // no switch-case to do strict comparison + if ($comment->type === null || $comment->type === 'DiscussionNote') { + $content = 'commented'; + } elseif ($comment->type === 'DiffNote') { + $content = 'commented on a thread'; + } else { + $content = $comment->note_html; + } + } + $item['title'] = $author->name . " $content"; + + $content = $this->fixImgSrc($comment->note_html); + $item['content'] = defaultLinkTo($content, 'https://' . $this->getInput('h') . '/'); + + $this->items[] = $item; + } + } + } + + private function parseIssueDescription() { + $description_uri = $this->getURI() . '.json'; + $description = getContents($description_uri); + $description = json_decode($description, false); + $description_html = getSimpleHtmlDomCached($this->getURI()); + + $item = array(); + $item['uri'] = $this->getURI(); + $item['uid'] = $item['uri']; + + $item['timestamp'] = $description->created_at ?? $description->updated_at; + + $item['author'] = $this->parseAuthor($description_html); + + $item['title'] = $description->title; + $item['content'] = markdownToHtml($description->description); + + return $item; + } + + private function fixImgSrc($html) { + if (is_string($html)) { + $html = str_get_html($html); + } + + foreach ($html->find('img') as $img) { + $img->src = $img->getAttribute('data-src'); + } + return $html; + } + + private function parseAuthor($description_html) { + $description_html = $this->fixImgSrc($description_html); + + $authors = $description_html->find('.issuable-meta a.author-link, .merge-request a.author-link'); + $editors = $description_html->find('.edited-text a.author-link'); + $author_str = implode(' ', $authors); + if ($editors) { + $author_str .= ', ' . implode(' ', $editors); + } + return defaultLinkTo($author_str, 'https://' . $this->getInput('h') . '/'); + } + + private function parseMRDescription() { + $description_uri = $this->getURI() . '/cached_widget.json'; + $description = getContents($description_uri); + $description = json_decode($description, false); + $description_html = getSimpleHtmlDomCached($this->getURI()); + + $item = array(); + $item['uri'] = $this->getURI(); + $item['uid'] = $item['uri']; + + $item['timestamp'] = $description_html->find('.merge-request-details time', 0)->datetime; + + $item['author'] = $this->parseAuthor($description_html); + + $item['title'] = 'Merge Request ' . $description->title; + $item['content'] = markdownToHtml($description->description); + + return $item; + } +}