diff --git a/bridges/FDroidRepoBridge.php b/bridges/FDroidRepoBridge.php new file mode 100644 index 00000000..48f3fe59 --- /dev/null +++ b/bridges/FDroidRepoBridge.php @@ -0,0 +1,197 @@ + array( + 'url' => array( + 'name' => 'Repository URL', + 'title' => 'Usually ends with /repo/', + 'required' => true, + 'exampleValue' => 'https://srv.tt-rss.org/fdroid/repo' + ) + ), + 'Latest Updates' => array( + 'sorting' => array( + 'name' => 'Sort By', + 'type' => 'list', + 'values' => array( + 'Latest added apps' => 'added', + 'Latest updated apps' => 'lastUpdated' + ) + ), + 'locale' => array( + 'name' => 'Locale', + 'defaultValue' => 'en-US' + ) + ), + 'Follow Package' => array( + 'package' => array( + 'name' => 'Package Identifier', + 'required' => true, + 'exampleValue' => 'org.fox.ttrss' + ) + ) + ); + + // Stores repo information + private $repo; + + public function getURI() { + if (empty($this->queriedContext)) + return parent::getURI(); + + $url = rtrim($this->GetInput('url'), '/'); + return strstr($url, '?', true) ?: $url; + } + + public function getName() { + if (empty($this->queriedContext)) + return parent::getName(); + + $name = $this->repo['repo']['name']; + switch($this->queriedContext) { + case 'Latest Updates': + return $name; + case 'Follow Package': + return $this->getInput('package') . ' - ' . $name; + default: + returnServerError('Unimplemented Context (getName)'); + } + } + + public function collectData() { + $this->repo = $this->getRepo(); + switch($this->queriedContext) { + case 'Latest Updates': + $this->getAllUpdates(); + break; + case 'Follow Package': + $this->getPackage($this->getInput('package')); + break; + default: + returnServerError('Unimplemented Context (collectData)'); + } + } + + private function getRepo() { + $url = $this->getURI(); + + // Get repo information (only available as JAR) + $jar = getContents($url . '/index-v1.jar'); + $jar_loc = tempnam(sys_get_temp_dir(), ''); + file_put_contents($jar_loc, $jar); + + // JAR files are specially formatted ZIP files + $jar = new ZipArchive; + if ($jar->open($jar_loc) !== true) { + returnServerError('Failed to extract archive'); + } + + // Get file pointer to the relevant JSON inside + $fp = $jar->getStream('index-v1.json'); + if (!$fp) { + returnServerError('Failed to get file pointer'); + } + + $data = json_decode(stream_get_contents($fp), true); + fclose($fp); + $jar->close(); + return $data; + } + + private function getAllUpdates() { + $apps = $this->repo['apps']; + usort($apps, function($a, $b) { + return $b[$this->getInput('sorting')] <=> $a[$this->getInput('sorting')]; + }); + $apps = array_slice($apps, 0, self::ITEM_LIMIT); + foreach($apps as $app) { + $latest = reset($this->repo['packages'][$app['packageName']]); + + if (isset($app['localized'])) { + // Try provided locale, then en-US, then any + $lang = $app['localized']; + $lang = $lang[$this->getInput('locale')] ?? $lang['en-US'] ?? reset($lang); + } else + $lang = array(); + + $item = array(); + $item['uri'] = $this->getURI() . '/' . $latest['apkName']; + $item['title'] = $lang['name'] ?? $app['packageName']; + $item['title'] .= ' ' . $latest['versionName']; + $item['timestamp'] = date(DateTime::ISO8601, (int) ($app['lastUpdated'] / 1000)); + if (isset($app['authorName'])) + $item['author'] = $app['authorName']; + if (isset($app['categories'])) + $item['categories'] = $app['categories']; + + // Adding Content + $icon = $app['icon'] ?? ''; + if (!empty($icon)) { + $icon = $this->getURI() . '/icons-320/' . $icon; + $item['enclosures'] = array($icon); + $icon = ''; + } + $summary = $lang['summary'] ?? $app['summary'] ?? ''; + $description = markdownToHtml(trim($lang['description'] ?? $app['description'] ?? 'None')); + $whatsNew = markdownToHtml(trim($lang['whatsNew'] ?? 'None')); + $website = $this->link($lang['webSite'] ?? $app['webSite'] ?? $app['authorWebSite'] ?? null); + $source = $this->link($app['sourceCode'] ?? null); + $issueTracker = $this->link($app['issueTracker'] ?? null); + $license = $app['license'] ?? 'None'; + $item['content'] = <<{$summary}

+

Description

+{$description} +

What's New

+{$whatsNew} +

Information

+

Website: {$website}

+

Source Code: {$source}

+

Issue Tracker: {$issueTracker}

+

license: {$app['license']}

+EOD; + $this->items[] = $item; + } + } + + private function getPackage($package) { + if (!isset($this->repo['packages'][$package])) { + returnClientError('Invalid Package Name'); + } + $package = $this->repo['packages'][$package]; + + $count = self::ITEM_LIMIT; + foreach($package as $version) { + $item = array(); + $item['uri'] = $this->getURI() . '/' . $version['apkName']; + $item['title'] = $version['versionName']; + $item['timestamp'] = date(DateTime::ISO8601, (int) ($version['added'] / 1000)); + $item['uid'] = $version['versionCode']; + $size = round($version['size'] / 1048576, 1); // Bytes -> MB + $sdk_link = 'https://developer.android.com/studio/releases/platforms'; + $item['content'] = <<size: {$size}MB

+

Minimum SDK: {$version['minSdkVersion']} +(SDK to Android Version List)

+

hash ({$version['hashType']}): {$version['hash']}

+EOD; + $this->items[] = $item; + if (--$count <= 0) + break; + } + } + + private function link($url) { + if (empty($url)) + return null; + return '' . $url . ''; + } +}