Merge branch 'master' into kt_bridge

This commit is contained in:
Knah Tsaeb 2021-03-05 16:04:43 +01:00
commit ca73a32545
19 changed files with 964 additions and 47 deletions

View file

@ -1,6 +1,6 @@
![RSS-Bridge](static/logo_600px.png) ![RSS-Bridge](static/logo_600px.png)
=== ===
[![LICENSE](https://img.shields.io/badge/license-UNLICENSE-blue.svg)](UNLICENSE) [![GitHub release](https://img.shields.io/github/release/rss-bridge/rss-bridge.svg?logo=github)](https://github.com/rss-bridge/rss-bridge/releases/latest) [![Debian Release](https://img.shields.io/badge/dynamic/json.svg?logo=debian&label=debian%20release&url=https%3A%2F%2Fsources.debian.org%2Fapi%2Fsrc%2Frss-bridge%2F&query=%24.versions%5B0%5D.version&colorB=blue)](https://tracker.debian.org/pkg/rss-bridge) [![Guix Release](https://img.shields.io/badge/guix%20release-unknown-blue.svg)](https://www.gnu.org/software/guix/packages/R/) [![Build Status](https://travis-ci.org/RSS-Bridge/rss-bridge.svg?branch=master)](https://travis-ci.org/RSS-Bridge/rss-bridge) [![Docker Build Status](https://img.shields.io/docker/build/rssbridge/rss-bridge.svg?logo=docker)](https://hub.docker.com/r/rssbridge/rss-bridge/) [![LICENSE](https://img.shields.io/badge/license-UNLICENSE-blue.svg)](UNLICENSE) [![GitHub release](https://img.shields.io/github/release/rss-bridge/rss-bridge.svg?logo=github)](https://github.com/rss-bridge/rss-bridge/releases/latest) [![Debian Release](https://img.shields.io/badge/dynamic/json.svg?logo=debian&label=debian%20release&url=https%3A%2F%2Fsources.debian.org%2Fapi%2Fsrc%2Frss-bridge%2F&query=%24.versions%5B0%5D.version&colorB=blue)](https://tracker.debian.org/pkg/rss-bridge) [![Guix Release](https://img.shields.io/badge/guix%20release-unknown-blue.svg)](https://www.gnu.org/software/guix/packages/R/) [![Actions Status](https://img.shields.io/github/workflow/status/RSS-Bridge/rss-bridge/Tests/master?label=GitHub%20Actions&logo=github)](https://github.com/RSS-Bridge/rss-bridge/actions) [![Docker Build Status](https://img.shields.io/docker/cloud/build/rssbridge/rss-bridge?logo=docker)](https://hub.docker.com/r/rssbridge/rss-bridge/)
RSS-Bridge is a PHP project capable of generating RSS and Atom feeds for websites that don't have one. It can be used on webservers or as a stand-alone application in CLI mode. RSS-Bridge is a PHP project capable of generating RSS and Atom feeds for websites that don't have one. It can be used on webservers or as a stand-alone application in CLI mode.

219
bridges/BukowskisBridge.php Executable file
View file

@ -0,0 +1,219 @@
<?php
class BukowskisBridge extends BridgeAbstract
{
const NAME = 'Bukowskis';
const URI = 'https://www.bukowskis.com';
const DESCRIPTION = 'Fetches info about auction objects from Bukowskis auction house';
const MAINTAINER = 'Qluxzz';
const PARAMETERS = array(array(
'category' => array(
'name' => 'Category',
'type' => 'list',
'values' => array(
'All categories' => '',
'Art' => array(
'All' => 'art',
'Classic Art' => 'art.classic-art',
'Classic Finnish Art' => 'art.classic-finnish-art',
'Classic Swedish Art' => 'art.classic-swedish-art',
'Contemporary' => 'art.contemporary',
'Modern Finnish Art' => 'art.modern-finnish-art',
'Modern International Art' => 'art.modern-international-art',
'Modern Swedish Art' => 'art.modern-swedish-art',
'Old Masters' => 'art.old-masters',
'Other' => 'art.other',
'Photographs' => 'art.photographs',
'Prints' => 'art.prints',
'Sculpture' => 'art.sculpture',
'Swedish Old Masters' => 'art.swedish-old-masters',
),
'Asian Ceramics & Works of Art' => array(
'All' => 'asian-ceramics-works-of-art',
'Other' => 'asian-ceramics-works-of-art.other',
'Porcelain' => 'asian-ceramics-works-of-art.porcelain',
),
'Books & Manuscripts' => array(
'All' => 'books-manuscripts',
'Books' => 'books-manuscripts.books',
),
'Carpets, rugs & textiles' => array(
'All' => 'carpets-rugs-textiles',
'European' => 'carpets-rugs-textiles.european',
'Oriental' => 'carpets-rugs-textiles.oriental',
'Rest of the world' => 'carpets-rugs-textiles.rest-of-the-world',
'Scandinavian' => 'carpets-rugs-textiles.scandinavian',
),
'Ceramics & porcelain' => array(
'All' => 'ceramics-porcelain',
'Ceramic ware' => 'ceramics-porcelain.ceramic-ware',
'European' => 'ceramics-porcelain.european',
'Rest of the world' => 'ceramics-porcelain.rest-of-the-world',
'Scandinavian' => 'ceramics-porcelain.scandinavian',
),
'Collectibles' => array(
'All' => 'collectibles',
'Advertising & Retail' => 'collectibles.advertising-retail',
'Memorabilia' => 'collectibles.memorabilia',
'Movies & music' => 'collectibles.movies-music',
'Other' => 'collectibles.other',
'Retro & Popular Culture' => 'collectibles.retro-popular-culture',
'Technica & Nautica' => 'collectibles.technica-nautica',
'Toys' => 'collectibles.toys',
),
'Design' => array(
'All' => 'design',
'Art glass' => 'design.art-glass',
'Furniture' => 'design.furniture',
'Other' => 'design.other',
),
'Folk art' => array(
'All' => 'folk-art',
'All categories' => 'lots',
),
'Furniture' => array(
'All' => 'furniture',
'Armchairs & Sofas' => 'furniture.armchairs-sofas',
'Cabinets & Bureaus' => 'furniture.cabinets-bureaus',
'Chairs' => 'furniture.chairs',
'Garden furniture' => 'furniture.garden-furniture',
'Mirrors' => 'furniture.mirrors',
'Other' => 'furniture.other',
'Shelves & Book cases' => 'furniture.shelves-book-cases',
'Tables' => 'furniture.tables',
),
'Glassware' => array(
'All' => 'glassware',
'Glassware' => 'glassware.glassware',
'Other' => 'glassware.other',
),
'Jewellery' => array(
'All' => 'jewellery',
'Bracelets' => 'jewellery.bracelets',
'Brooches' => 'jewellery.brooches',
'Earrings' => 'jewellery.earrings',
'Necklaces & Pendants' => 'jewellery.necklaces-pendants',
'Other' => 'jewellery.other',
'Rings' => 'jewellery.rings',
),
'Lighting' => array(
'All' => 'lighting',
'Candle sticks & Candelabras' => 'lighting.candle-sticks-candelabras',
'Ceiling lights' => 'lighting.ceiling-lights',
'Chandeliers' => 'lighting.chandeliers',
'Floor lights' => 'lighting.floor-lights',
'Other' => 'lighting.other',
'Table lights' => 'lighting.table-lights',
'Wall lights' => 'lighting.wall-lights',
),
'Militaria' => array(
'All' => 'militaria',
'Honors & Medals' => 'militaria.honors-medals',
'Other militaria' => 'militaria.other-militaria',
'Weaponry' => 'militaria.weaponry',
),
'Miscellaneous' => array(
'All' => 'miscellaneous',
'Brass, Copper & Pewter' => 'miscellaneous.brass-copper-pewter',
'Nickel silver' => 'miscellaneous.nickel-silver',
'Oriental' => 'miscellaneous.oriental',
'Other' => 'miscellaneous.other',
),
'Silver' => array(
'All' => 'silver',
'Candle sticks' => 'silver.candle-sticks',
'Cups & Bowls' => 'silver.cups-bowls',
'Cutlery' => 'silver.cutlery',
'Other' => 'silver.other',
),
'Timepieces' => array(
'All' => 'timepieces',
'Other' => 'timepieces.other',
'Pocket watches' => 'timepieces.pocket-watches',
'Table clocks' => 'timepieces.table-clocks',
'Wrist watches' => 'timepieces.wrist-watches',
),
'Vintage & Fashion' => array(
'All' => 'vintage-fashion',
'Accessories' => 'vintage-fashion.accessories',
'Bags & Trunks' => 'vintage-fashion.bags-trunks',
'Clothes' => 'vintage-fashion.clothes',
),
)
),
'sort_order' => array(
'name' => 'Sort order',
'type' => 'list',
'values' => array(
'Ending soon' => 'ending',
'Most recent' => 'recent',
'Most bids' => 'most',
'Fewest bids' => 'fewest',
'Lowest price' => 'lowest',
'Highest price' => 'highest',
'Lowest estimate' => 'low',
'Highest estimate' => 'high',
'Alphabetical' => 'alphabetical',
),
),
'language' => array(
'name' => 'Language',
'type' => 'list',
'values' => array(
'English' => 'en',
'Swedish' => 'sv',
'Finnish' => 'fi'
),
),
));
const CACHE_TIMEOUT = 3600; // 1 hour
private $title;
public function collectData()
{
$baseUrl = 'https://www.bukowskis.com';
$category = $this->getInput('category');
$language = $this->getInput('language');
$sort_order = $this->getInput('sort_order');
$url = $baseUrl . '/' . $language . '/lots';
if ($category)
$url = $url . '/category/' . $category;
if ($sort_order)
$url = $url . '/sort/' . $sort_order;
$html = getSimpleHTMLDOM($url)
or returnServerError('Could not request: ' . $url);
$this->title = htmlspecialchars_decode($html->find('title', 0)->innertext);
foreach ($html->find('div.c-lot-index-lot') as $lot) {
$title = $lot->find('a.c-lot-index-lot__title', 0)->plaintext;
$relative_url = $lot->find('a.c-lot-index-lot__link', 0)->href;
$images = json_decode(
htmlspecialchars_decode(
$lot
->find('img.o-aspect-ratio__image', 0)
->getAttribute('data-thumbnails')
)
);
$this->items[] = array(
'title' => $title,
'uri' => $baseUrl . $relative_url,
'uid' => $lot->getAttribute('data-lot-id'),
'content' => count($images) > 0 ? "<img src='$images[0]'/><br/>$title" : $title,
'enclosures' => array_slice($images, 1),
);
}
}
public function getName()
{
return $this->title ?: parent::getName();
}
}

143
bridges/DockerHubBridge.php Normal file
View file

@ -0,0 +1,143 @@
<?php
class DockerHubBridge extends BridgeAbstract {
const NAME = 'Docker Hub Bridge';
const URI = 'https://hub.docker.com';
const DESCRIPTION = 'Returns new images for a container';
const MAINTAINER = 'VerifiedJoseph';
const PARAMETERS = array(
'User Submitted Image' => array(
'user' => array(
'name' => 'User',
'type' => 'text',
'required' => true,
'exampleValue' => 'rssbridge',
),
'repo' => array(
'name' => 'Repository',
'type' => 'text',
'required' => true,
'exampleValue' => 'rss-bridge',
)
),
'Official Image' => array(
'repo' => array(
'name' => 'Repository',
'type' => 'text',
'required' => true,
'exampleValue' => 'postgres',
)
),
);
const CACHE_TIMEOUT = 3600; // 1 hour
private $apiURL = 'https://hub.docker.com/v2/repositories/';
public function collectData() {
$json = getContents($this->getApiUrl())
or returnServerError('Could not request: ' . $this->getURI());
$data = json_decode($json, false);
foreach ($data->results as $result) {
$item = array();
$lastPushed = date('Y-m-d H:i:s', strtotime($result->tag_last_pushed));
$item['title'] = $result->name;
$item['uid'] = $result->id;
$item['uri'] = $this->getTagUrl($result->name);
$item['author'] = $result->last_updater_username;
$item['timestamp'] = $result->tag_last_pushed;
$item['content'] = <<<EOD
<Strong>Tag</strong><br>
<p>{$result->name}</p>
<Strong>Last pushed</strong><br>
<p>{$lastPushed}</p>
<Strong>Images</strong><br>
{$this->getImages($result)}
EOD;
$this->items[] = $item;
}
}
public function getURI() {
if ($this->queriedContext === 'Official Image') {
return self::URI . '/_/' . $this->getRepo();
}
if ($this->getInput('repo')) {
return self::URI . '/r/' . $this->getRepo();
}
return parent::getURI();
}
public function getName() {
if ($this->getInput('repo')) {
return $this->getRepo() . ' - Docker Hub';
}
return parent::getName();
}
private function getRepo() {
if ($this->queriedContext === 'Official Image') {
return $this->getInput('repo');
}
return $this->getInput('user') . '/' . $this->getInput('repo');
}
private function getApiUrl() {
if ($this->queriedContext === 'Official Image') {
return $this->apiURL . 'library/' . $this->getRepo() . '/tags/?page_size=25&page=1';
}
return $this->apiURL . $this->getRepo() . '/tags/?page_size=25&page=1';
}
private function getLayerUrl($name, $digest) {
if ($this->queriedContext === 'Official Image') {
return self::URI . '/layers/' . $this->getRepo() . '/library/' .
$this->getRepo() . '/' . $name . '/images/' . $digest;
}
return self::URI . '/layers/' . $this->getRepo() . '/' . $name . '/images/' . $digest;
}
private function getTagUrl($name) {
if ($this->queriedContext === 'Official Image') {
return self::URI . '/_/' . $this->getRepo() . '?tab=tags&name=' . $name;
}
return self::URI . '/r/' . $this->getRepo() . '/tags?name=' . $name;
}
private function getImages($result) {
$html = <<<EOD
<table style="width:300px;"><thead><tr><th>Digest</th><th>OS/architecture</th></tr></thead></tbody>
EOD;
foreach ($result->images as $image) {
$layersUrl = $this->getLayerUrl($result->name, $image->digest);
$id = $this->getShortDigestId($image->digest);
$html .= <<<EOD
<tr>
<td><a href="{$layersUrl}">{$id}</a></td>
<td>{$image->os}/{$image->architecture}</td>
</tr>
EOD;
}
return $html . '</tbody></table>';
}
private function getShortDigestId($digest) {
$parts = explode(':', $digest);
return substr($parts[1], 0, 12);
}
}

View file

@ -0,0 +1,115 @@
<?php
class FSecureBlogBridge extends BridgeAbstract {
const NAME = 'F-Secure Blog';
const URI = 'https://blog.f-secure.com';
const DESCRIPTION = 'F-Secure Blog';
const MAINTAINER = 'simon816';
const PARAMETERS = array(
'' => array(
'categories' => array(
'name' => 'Blog categories',
'exampleValue' => 'home-security',
),
'language' => array(
'name' => 'Language',
'defaultValue' => 'en',
),
'oldest_date' => array(
'name' => 'Oldest article date',
'exampleValue' => '-2 months',
),
)
);
public function getURI() {
$lang = $this->getInput('language') or 'en';
if ($lang === 'en') {
return self::URI;
}
return self::URI . "/$lang";
}
public function collectData() {
$this->items = array();
$this->seen = array();
$this->oldest = strtotime($this->getInput('oldest_date')) ?: 0;
$categories = $this->getInput('categories');
if (!empty($categories)) {
foreach (explode(',', $categories) as $cat) {
if (!empty($cat)) {
$this->collectCategory($cat);
}
}
return;
}
$html = getSimpleHTMLDOMCached($this->getURI() . '/');
foreach ($html->find('ul.c-header-menu-desktop__list li a') as $link) {
$url = parse_url($link->href);
if (($pos = strpos($url['path'], '/category/')) !== false) {
$cat = substr($url['path'], $pos + strlen('/category/'), -1);
$this->collectCategory($cat);
}
}
}
private function collectCategory($category) {
$url = $this->getURI() . "/category/$category/";
while ($url) {
$url = $this->collectListing($url);
}
}
// n.b. this relies on articles to be ordered by date so the cutoff works
private function collectListing($url) {
$html = getSimpleHTMLDOMCached($url, 60 * 60);
$items = $html->find('section.b-blog .l-blog__content__listing div.c-listing-item');
$catName = trim($html->find('section.b-blog .c-blog-header__title', 0)->plaintext);
foreach ($items as $item) {
$url = $item->getAttribute('data-url');
if (!$this->collectArticle($url)) {
return null; // Too old, stop collecting
}
}
// Point's to 404 for non-english blog
// $next = $html->find('link[rel=next]', 0);
$next = $html->find('ul.page-numbers a.next', 0);
return $next ? $next->href : null;
}
// Returns a boolean whether to continue collecting articles
// i.e. date is after oldest cutoff
private function collectArticle($url) {
if (array_key_exists($url, $this->seen)) {
return true;
}
$html = getSimpleHTMLDOMCached($url);
$rssItem = array( 'uri' => $url, 'uid' => $url );
$rssItem['title'] = $html->find('meta[property=og:title]', 0)->content;
$dt = $html->find('meta[property=article:published_time]', 0)->content;
// Exit if too old
if (strtotime($dt) < $this->oldest) {
return false;
}
$rssItem['timestamp'] = $dt;
$img = $html->find('meta[property=og:image]', 0);
$rssItem['enclosures'] = $img ? array($img->content) : array();
$rssItem['author'] = trim($html->find('.c-blog-author__text a', 0)->plaintext);
$rssItem['categories'] = array_map(function ($link) {
return trim($link->plaintext);
}, $html->find('.b-single-header__categories .c-category-list a'));
$rssItem['content'] = trim($html->find('article', 0)->innertext);
$this->items[] = $rssItem;
$this->seen[$url] = 1;
return true;
}
}

View file

@ -0,0 +1,90 @@
<?php
class FirefoxAddonsBridge extends BridgeAbstract {
const NAME = 'Firefox Add-ons Bridge';
const URI = 'https://addons.mozilla.org/';
const DESCRIPTION = 'Returns version history for a Firefox Add-on.';
const MAINTAINER = 'VerifiedJoseph';
const PARAMETERS = array(array(
'id' => array(
'name' => 'Add-on ID',
'type' => 'text',
'required' => true,
'exampleValue' => 'save-to-the-wayback-machine',
)
)
);
const CACHE_TIMEOUT = 3600;
private $feedName = '';
private $releaseDateRegex = '/Released ([\w, ]+) - ([\w. ]+)/';
private $xpiFileRegex = '/([A-Za-z0-9_.-]+)\.xpi$/';
private $outgoingRegex = '/https:\/\/outgoing.prod.mozaws.net\/v1\/(?:[A-z0-9]+)\//';
public function collectData() {
$html = getSimpleHTMLDOM($this->getURI())
or returnServerError('Could not request: ' . $this->getURI());
$this->feedName = $html->find('h1[class="AddonTitle"] > a', 0)->innertext;
$author = $html->find('span.AddonTitle-author > a', 0)->plaintext;
foreach ($html->find('div.AddonVersionCard-content') as $div) {
$item = array();
$item['title'] = $div->find('h2.AddonVersionCard-version', 0)->plaintext;
$item['uri'] = $this->getURI();
$item['author'] = $author;
if (preg_match($this->releaseDateRegex, $div->find('div.AddonVersionCard-fileInfo', 0)->plaintext, $match)) {
$item['timestamp'] = $match[1];
$size = $match[2];
}
$compatibility = $div->find('div.AddonVersionCard-compatibility', 0)->plaintext;
$license = $div->find('p.AddonVersionCard-license', 0)->innertext;
$downloadlink = $div->find('a.InstallButtonWrapper-download-link', 0)->href;
$releaseNotes = $this->removeOutgoinglink($div->find('div.AddonVersionCard-releaseNotes', 0));
if (preg_match($this->xpiFileRegex, $downloadlink, $match)) {
$xpiFilename = $match[0];
}
$item['content'] = <<<EOD
<strong>Release Notes</strong>
<p>{$releaseNotes}</p>
<strong>Compatibility</strong>
<p>{$compatibility}</p>
<strong>License</strong>
<p>{$license}</p>
<strong>Download</strong>
<p><a href="{$downloadlink}">{$xpiFilename}</a> ($size)</p>
EOD;
$this->items[] = $item;
}
}
public function getURI() {
if (!is_null($this->getInput('id'))) {
return self::URI . 'en-US/firefox/addon/' . $this->getInput('id') . '/versions/';
}
return parent::getURI();
}
public function getName() {
if (!empty($this->feedName)) {
return $this->feedName . ' - Firefox Add-on';
}
return parent::getName();
}
private function removeOutgoinglink($html) {
foreach ($html->find('a') as $a) {
$a->href = urldecode(preg_replace($this->outgoingRegex, '', $a->href));
}
return $html->innertext;
}
}

View file

@ -352,12 +352,14 @@ class LeBonCoinBridge extends BridgeAbstract {
public function collectData(){ public function collectData(){
$url = 'https://api.leboncoin.fr/finder/search/'; $url = 'https://api.leboncoin.fr/api/adfinder/v1/search';
$data = $this->buildRequestJson(); $data = $this->buildRequestJson();
$header = array( $header = array(
'User-Agent: LBC;Android;Null;Null;Null;Null;Null;Null;Null;Null', 'User-Agent: LBC;Android;10;SAMSUNG;phone;0aaaaaaaaaaaaaaa;wifi;8.24.3.8;152437;0',
'Content-Type: application/json', 'Content-Type: application/json',
'X-LBC-CC: 7',
'Accept: application/json,application/hal+json',
'Content-Length: ' . strlen($data), 'Content-Length: ' . strlen($data),
'api_key: ' . self::$LBC_API_KEY 'api_key: ' . self::$LBC_API_KEY
); );

View file

@ -26,7 +26,7 @@ class NordbayernBridge extends BridgeAbstract {
'Gunzenhausen' => 'gunzenhausen', 'Gunzenhausen' => 'gunzenhausen',
'Hersbruck' => 'hersbruck', 'Hersbruck' => 'hersbruck',
'Herzogenaurach' => 'herzogenaurach', 'Herzogenaurach' => 'herzogenaurach',
'Hilpolstein' => 'holpolstein', 'Hilpoltstein' => 'hilpoltstein',
'Höchstadt' => 'hoechstadt', 'Höchstadt' => 'hoechstadt',
'Lauf' => 'lauf', 'Lauf' => 'lauf',
'Neumarkt' => 'neumarkt', 'Neumarkt' => 'neumarkt',

View file

@ -25,7 +25,7 @@ class RadioMelodieBridge extends BridgeAbstract {
$picture = array(); $picture = array();
// Get the Main picture URL // Get the Main picture URL
$picture[] = $this->rewriteImage($article->find('div[id=pictureTitleSupport]', 0)->find('img', 0)->src); $picture[] = self::URI . $article->find('div[id=pictureTitleSupport]', 0)->find('img', 0)->src;
$audioHTML = $article->find('audio'); $audioHTML = $article->find('audio');
// Add the audio element to the enclosure // Add the audio element to the enclosure

View file

@ -23,6 +23,19 @@ class RedditBridge extends BridgeAbstract {
'exampleValue' => 'selfhosted, php', 'exampleValue' => 'selfhosted, php',
'title' => 'SubReddit names, separated by commas' 'title' => 'SubReddit names, separated by commas'
) )
),
'user' => array(
'u' => array(
'name' => 'User',
'required' => true,
'title' => 'User name'
),
'comments' => array(
'type' => 'checkbox',
'name' => 'Comments',
'title' => 'Whether to return comments',
'defaultValue' => false
)
) )
); );
@ -33,12 +46,18 @@ class RedditBridge extends BridgeAbstract {
public function getName() { public function getName() {
if ($this->queriedContext == 'single') { if ($this->queriedContext == 'single') {
return 'Reddit r/' . $this->getInput('r'); return 'Reddit r/' . $this->getInput('r');
} elseif ($this->queriedContext == 'user') {
return 'Reddit u/' . $this->getInput('u');
} else { } else {
return self::NAME; return self::NAME;
} }
} }
public function collectData() { public function collectData() {
$user = false;
$comments = false;
switch ($this->queriedContext) { switch ($this->queriedContext) {
case 'single': case 'single':
$subreddits[] = $this->getInput('r'); $subreddits[] = $this->getInput('r');
@ -46,33 +65,55 @@ class RedditBridge extends BridgeAbstract {
case 'multi': case 'multi':
$subreddits = explode(',', $this->getInput('rs')); $subreddits = explode(',', $this->getInput('rs'));
break; break;
case 'user':
$subreddits[] = $this->getInput('u');
$user = true;
$comments = $this->getInput('comments');
break;
} }
foreach ($subreddits as $subreddit) { foreach ($subreddits as $subreddit) {
$name = trim($subreddit); $name = trim($subreddit);
$values = getContents(self::URI . '/r/' . $name . '.json') $values = getContents(self::URI . ($user ? '/user/' : '/r/') . $name . '.json')
or returnServerError('Unable to fetch posts!'); or returnServerError('Unable to fetch posts!');
$decodedValues = json_decode($values); $decodedValues = json_decode($values);
foreach ($decodedValues->data->children as $post) { foreach ($decodedValues->data->children as $post) {
if ($post->kind == 't1' && !$comments) {
continue;
}
$data = $post->data; $data = $post->data;
$item = array(); $item = array();
$item['author'] = $data->author; $item['author'] = $data->author;
$item['title'] = $data->title;
$item['uid'] = $data->id; $item['uid'] = $data->id;
$item['timestamp'] = $data->created_utc; $item['timestamp'] = $data->created_utc;
$item['uri'] = $this->encodePermalink($data->permalink); $item['uri'] = $this->encodePermalink($data->permalink);
$item['categories'] = array(); $item['categories'] = array();
if ($post->kind == 't1') {
$item['title'] = 'Comment: ' . $data->link_title;
} else {
$item['title'] = $data->title;
$item['categories'][] = $data->link_flair_text; $item['categories'][] = $data->link_flair_text;
$item['categories'][] = $data->pinned ? 'Pinned' : null; $item['categories'][] = $data->pinned ? 'Pinned' : null;
$item['categories'][] = $data->over_18 ? 'NSFW' : null;
$item['categories'][] = $data->spoiler ? 'Spoiler' : null; $item['categories'][] = $data->spoiler ? 'Spoiler' : null;
}
$item['categories'][] = $data->over_18 ? 'NSFW' : null;
$item['categories'] = array_filter($item['categories']); $item['categories'] = array_filter($item['categories']);
if ($data->is_self) { if ($post->kind == 't1') {
// Comment
$item['content']
= htmlspecialchars_decode($data->body_html);
} elseif ($data->is_self) {
// Text post // Text post
$item['content'] $item['content']
@ -112,7 +153,7 @@ class RedditBridge extends BridgeAbstract {
$id = $media->media_id; $id = $media->media_id;
$type = $data->media_metadata->$id->m == 'image/gif' ? 'gif' : 'u'; $type = $data->media_metadata->$id->m == 'image/gif' ? 'gif' : 'u';
$src = $data->media_metadata->$id->s->$type; $src = $data->media_metadata->$id->s->$type;
$images[] = '<img src="' . $src . '"/>'; $images[] = '<figure><img src="' . $src . '"/></figure>';
} }
$item['content'] = implode('', $images); $item['content'] = implode('', $images);

246
bridges/ReutersBridge.php Normal file
View file

@ -0,0 +1,246 @@
<?php
class ReutersBridge extends BridgeAbstract
{
const MAINTAINER = 'hollowleviathan, spraynard, csisoap';
const NAME = 'Reuters Bridge';
const URI = 'https://reuters.com/';
const CACHE_TIMEOUT = 1800; // 30min
const DESCRIPTION = 'Returns news from Reuters';
private $feedName = self::NAME;
/**
* Wireitem types allowed in the final story output
*/
const ALLOWED_WIREITEM_TYPES = array(
'story',
'headlines'
);
/**
* Wireitem template types allowed in the final story output
*/
const ALLOWED_TEMPLATE_TYPES = array(
'story'
);
const PARAMETERS = array(
array(
'feed' => array(
'name' => 'News Feed',
'type' => 'list',
'title' => 'Feeds from Reuters U.S/International edition',
'values' => array(
'Aerospace and Defense' => 'aerospace',
'Business' => 'business',
'China' => 'china',
'Energy' => 'energy',
'Entertainment' => 'chan:8ym8q8dl',
'Environment' => 'chan:6u4f0jgs',
'Health' => 'chan:8hw7807a',
'Lifestyle' => 'life',
'Markets' => 'markets',
'Politics' => 'politics',
'Science' => 'science',
'Special Reports' => 'special-reports',
'Sports' => 'sports',
'Tech' => 'tech',
'Top News' => 'home/topnews',
'UK' => 'chan:61leiu7j',
'USA News' => 'us',
'Wire' => 'wire',
'World' => 'world',
)
)
)
);
/**
* Performs an HTTP request to the Reuters API and returns decoded JSON
* in the form of an associative array
* @param string $feed_uri Parameter string to the Reuters API
* @return array
*/
private function getJson($feed_uri)
{
$uri = "https://wireapi.reuters.com/v8$feed_uri";
$returned_data = getContents($uri);
return json_decode($returned_data, true);
}
/**
* Takes in data from Reuters Wire API and
* creates structured data in the form of a list
* of story information.
* @param array $data JSON collected from the Reuters Wire API
*/
private function processData($data)
{
/**
* Gets a list of wire items which are groups of templates
*/
$reuters_allowed_wireitems = array_filter(
$data, function ($wireitem) {
return in_array(
$wireitem['wireitem_type'],
self::ALLOWED_WIREITEM_TYPES
);
}
);
/*
* Gets a list of "Templates", which is data containing a story
*/
$reuters_wireitem_templates = array_reduce(
$reuters_allowed_wireitems,
function (array $carry, array $wireitem) {
$wireitem_templates = $wireitem['templates'];
return array_merge(
$carry,
array_filter(
$wireitem_templates, function (
array $template_data
) {
return in_array(
$template_data['type'],
self::ALLOWED_TEMPLATE_TYPES
);
}
)
);
},
array()
);
return $reuters_wireitem_templates;
}
private function getArticle($feed_uri)
{
// This will make another request to API to get full detail of article and author's name.
$rawData = $this->getJson($feed_uri);
$reuters_wireitems = $rawData['wireitems'];
$processedData = $this->processData($reuters_wireitems);
$first = reset($processedData);
$article_content = $first['story']['body_items'];
$authorlist = $first['story']['authors'];
$category = $first['story']['channel']['name'];
$image_list = $first['story']['images'];
$img_placeholder = '';
foreach($image_list as $image) { // Add more image to article.
$image_url = $image['url'];
$image_caption = $image['caption'];
$img = "<img src=\"$image_url\">";
$img_caption = "<figcaption style=\"text-align: center;\"><i>$image_caption</i></figcaption>";
$figure = "<figure>$img \t $img_caption</figure>";
$img_placeholder = $img_placeholder . $figure;
}
$author = '';
$counter = 0;
foreach ($authorlist as $data) {
//Formatting author's name.
$counter++;
$name = $data['name'];
if ($counter == count($authorlist)) {
$author = $author . $name;
} else {
$author = $author . "$name, ";
}
}
$description = '';
foreach ($article_content as $content) {
$data;
if(isset($content['content'])) {
$data = $content['content'];
}
switch($content['type']) {
case 'paragraph':
$description = $description . "<p>$data</p>";
break;
case 'heading':
$description = $description . "<h3>$data</h3>";
break;
case 'infographics':
$description = $description . "<img src=\"$data\">";
break;
case 'inline_items':
$item_list = $content['items'];
$description = $description . '<p>';
foreach ($item_list as $item) {
if($item['type'] == 'text') {
$description = $description . $item['content'];
} else {
$description = $description . $item['symbol'];
}
}
$description = $description . '</p>';
break;
case 'p_table':
$description = $description . $content['content'];
break;
}
}
$content_detail = array(
'content' => $description,
'author' => $author,
'category' => $category,
'images' => $img_placeholder,
);
return $content_detail;
}
public function getName() {
return $this->feedName;
}
public function collectData()
{
$reuters_feed_name = $this->getInput('feed');
if(strpos($reuters_feed_name, 'chan:') !== false) {
// Now checking whether that feed has unique ID or not.
$feed_uri = "/feed/rapp/us/wirefeed/$reuters_feed_name";
} else {
$feed_uri = "/feed/rapp/us/tabbar/feeds/$reuters_feed_name";
}
$data = $this->getJson($feed_uri);
$reuters_wireitems = $data['wireitems'];
$this->feedName = $data['wire_name'] . ' | Reuters';
$processedData = $this->processData($reuters_wireitems);
// Merge all articles from Editor's Highlight section into existing array of templates.
$top_section = reset($reuters_wireitems);
if ($top_section['wireitem_type'] == 'headlines') {
$top_articles = $top_section['templates'][1]['headlines'];
$processedData = array_merge($top_articles, $processedData);
}
foreach ($processedData as $story) {
$item['uid'] = $story['story']['usn'];
$article_uri = $story['template_action']['api_path'];
$content_detail = $this->getArticle($article_uri);
$description = $content_detail['content'];
$author = $content_detail['author'];
$images = $content_detail['images'];
$item['categories'] = array($content_detail['category']);
$item['author'] = $author;
if (!(bool) $description) {
$description = $story['story']['lede']; // Just in case the content doesn't have anything.
} else {
$item['content'] = "$description $images";
}
$item['title'] = $story['story']['hed'];
$item['timestamp'] = $story['story']['updated_at'];
$item['uri'] = $story['template_action']['url'];
$this->items[] = $item;
}
}
}

View file

@ -27,6 +27,9 @@ class SoundCloudBridge extends BridgeAbstract {
private $feedIcon = null; private $feedIcon = null;
private $clientIDCache = null; private $clientIDCache = null;
private $clientIdRegex = '/client_id.*?"(.+?)"/';
private $widgetRegex = '/widget-.+?\.js/';
public function collectData(){ public function collectData(){
$res = $this->apiGet('resolve', array( $res = $this->apiGet('resolve', array(
'url' => 'https://soundcloud.com/' . $this->getInput('u') 'url' => 'https://soundcloud.com/' . $this->getInput('u')
@ -113,21 +116,32 @@ class SoundCloudBridge extends BridgeAbstract {
// Without url=http, this returns a 404 // Without url=http, this returns a 404
$playerHTML = getContents('https://w.soundcloud.com/player/?url=http') $playerHTML = getContents('https://w.soundcloud.com/player/?url=http')
or returnServerError('Unable to get player page.'); or returnServerError('Unable to get player page.');
$regex = '/widget-.+?\.js/';
if(preg_match($regex, $playerHTML, $matches) == false) // Extract widget JS filenames from player page
if(preg_match_all($this->widgetRegex, $playerHTML, $matches) == false)
returnServerError('Unable to find widget JS URL.'); returnServerError('Unable to find widget JS URL.');
$widgetURL = 'https://widget.sndcdn.com/' . $matches[0];
$clientID = '';
// Loop widget js files and extract client ID
foreach ($matches[0] as $widgetFile) {
$widgetURL = 'https://widget.sndcdn.com/' . $widgetFile;
$widgetJS = getContents($widgetURL) $widgetJS = getContents($widgetURL)
or returnServerError('Unable to get widget JS page.'); or returnServerError('Unable to get widget JS page.');
$regex = '/client_id.*?"(.+?)"/';
if(preg_match($regex, $widgetJS, $matches) == false)
returnServerError('Unable to find client ID.');
$clientID = $matches[1];
if(preg_match($this->clientIdRegex, $widgetJS, $matches)) {
$clientID = $matches[1];
$this->clientIDCache->saveData($clientID); $this->clientIDCache->saveData($clientID);
return $clientID; return $clientID;
} }
}
if (empty($clientID)) {
returnServerError('Unable to find client ID.');
}
}
private function buildAPIURL($endpoint, $parameters){ private function buildAPIURL($endpoint, $parameters){
return 'https://api-v2.soundcloud.com/' return 'https://api-v2.soundcloud.com/'

View file

@ -0,0 +1,34 @@
<?php
class SymfonyCastsBridge extends BridgeAbstract {
const NAME = 'SymfonyCasts Bridge';
const URI = 'https://symfonycasts.com/';
const DESCRIPTION = 'Follow new updates on symfonycasts.com';
const MAINTAINER = 'Park0';
const CACHE_TIMEOUT = 3600;
public function collectData() {
$html = getSimpleHTMLDOM('https://symfonycasts.com/updates/find')
or returnServerError('Unable to get page.');
$dives = $html->find('div');
/* @var simple_html_dom $div */
foreach ($dives as $div) {
$id = $div->getAttribute('data-mark-update-id-value');
$type = $div->find('h5', 0);
$title = $div->find('span', 0);
$dateString = $div->find('h5.font-gray', 0);
$href = $div->find('a', 0);
$url = 'https://symfonycasts.com' . $href->getAttribute('href');
$item = array(); // Create an empty item
$item['uid'] = $id;
$item['title'] = $title->innertext;
$item['timestamp'] = $dateString->innertext;
$item['content'] = $type->plaintext . '<a href="' . $url . '">' . $title . '</a>';
$item['uri'] = $url;
$this->items[] = $item; // Add item to the list
}
}
}

View file

@ -21,6 +21,18 @@ class TelegramBridge extends BridgeAbstract {
private $itemTitle = ''; private $itemTitle = '';
private $backgroundImageRegex = "/background-image:url\('(.*)'\)/"; private $backgroundImageRegex = "/background-image:url\('(.*)'\)/";
private $detectParamsRegex = '/^https?:\/\/t.me\/(?:s\/)?([\w]+)$/';
public function detectParameters($url) {
$params = array();
if(preg_match($this->detectParamsRegex, $url, $matches) > 0) {
$params['username'] = $matches[1];
return $params;
}
return null;
}
public function collectData() { public function collectData() {

View file

@ -12,7 +12,7 @@ class TheYeteeBridge extends BridgeAbstract {
$html = getSimpleHTMLDOM(self::URI) $html = getSimpleHTMLDOM(self::URI)
or returnServerError('Could not request The Yetee.'); or returnServerError('Could not request The Yetee.');
$div = $html->find('.hero-col'); $div = $html->find('.module_timed-item.is--full');
foreach($div as $element) { foreach($div as $element) {
$item = array(); $item = array();
@ -21,16 +21,15 @@ class TheYeteeBridge extends BridgeAbstract {
$title = $element->find('h2', 0)->plaintext; $title = $element->find('h2', 0)->plaintext;
$item['title'] = $title; $item['title'] = $title;
$author = trim($element->find('div[class=credit]', 0)->plaintext); $author = trim($element->find('.module_timed-item--artist a', 0)->plaintext);
$item['author'] = $author; $item['author'] = $author;
$uri = $element->find('div[class=controls] a', 0)->href; $item['uri'] = static::URI;
$item['uri'] = static::URI . $uri;
$content = '<p>' . $element->find('section[class=product-listing-info] p', -1)->plaintext . '</p>'; $content = '<p>' . $title . ' by ' . $author . '</p>';
$photos = $element->find('a[class=js-modaal-gallery] img'); $photos = $element->find('a.img');
foreach($photos as $photo) { foreach($photos as $photo) {
$content = $content . "<br /><img src='$photo->src' />"; $content = $content . "<br /><img src='$photo->href' />";
$item['enclosures'][] = $photo->src; $item['enclosures'][] = $photo->src;
} }
$item['content'] = $content; $item['content'] = $content;

View file

@ -379,6 +379,7 @@ class VkBridge extends BridgeAbstract
return $time; return $time;
} else { } else {
$strdate = $post->find('span.rel_date', 0)->plaintext; $strdate = $post->find('span.rel_date', 0)->plaintext;
$strdate = preg_replace('/[\x00-\x1F\x7F-\xFF]/', ' ', $strdate);
$date = date_parse($strdate); $date = date_parse($strdate);
if (!$date['year']) { if (!$date['year']) {

View file

@ -34,7 +34,7 @@ class ZoneTelechargementBridge extends BridgeAbstract {
); );
// This is an URL that is not protected by robot protection for Direct Download // This is an URL that is not protected by robot protection for Direct Download
const UNPROTECTED_URI = 'https://www.zone-annuaire.com/'; const UNPROTECTED_URI = 'https://www.zone-telechargement.net/';
// This is an URL that is not protected by robot protection for Streaming Links // This is an URL that is not protected by robot protection for Streaming Links
const UNPROTECTED_URI_STREAMING = 'https://zone-telechargement.stream/'; const UNPROTECTED_URI_STREAMING = 'https://zone-telechargement.stream/';

View file

@ -347,11 +347,13 @@ This bridge is not fetching its content through a secure connection</div>';
CARD; CARD;
// If we don't have any parameter for the bridge, we print a generic form to load it. // If we don't have any parameter for the bridge, we print a generic form to load it.
if(count($parameters) === 0 if (count($parameters) === 0) {
|| count($parameters) === 1 && array_key_exists('global', $parameters)) {
$card .= self::getForm($bridgeName, $formats, $isActive, $isHttps); $card .= self::getForm($bridgeName, $formats, $isActive, $isHttps);
// Display form with cache timeout and/or noproxy options (if enabled) when bridge has no parameters
} else if (count($parameters) === 1 && array_key_exists('global', $parameters)) {
$card .= self::getForm($bridgeName, $formats, $isActive, $isHttps, '', $parameters['global']);
} else { } else {
foreach($parameters as $parameterName => $parameter) { foreach($parameters as $parameterName => $parameter) {

View file

@ -46,7 +46,13 @@ class FormatFactory extends FactoryAbstract {
throw new \InvalidArgumentException('Format name invalid!'); throw new \InvalidArgumentException('Format name invalid!');
} }
$name = $this->sanitizeFormatName($name) . 'Format'; $name = $this->sanitizeFormatName($name);
if (is_null($name)) {
throw new \InvalidArgumentException('Unknown format given!');
}
$name .= 'Format';
$pathFormat = $this->getWorkingDir() . $name . '.php'; $pathFormat = $this->getWorkingDir() . $name . '.php';
if(!file_exists($pathFormat)) { if(!file_exists($pathFormat)) {
@ -72,7 +78,7 @@ class FormatFactory extends FactoryAbstract {
* @return bool true if the name is a valid format name, false otherwise. * @return bool true if the name is a valid format name, false otherwise.
*/ */
public function isFormatName($name){ public function isFormatName($name){
return is_string($name) && preg_match('/^[A-Z][a-zA-Z0-9-]*$/', $name) === 1; return is_string($name) && preg_match('/^[a-zA-Z0-9-]*$/', $name) === 1;
} }
/** /**
@ -108,8 +114,6 @@ class FormatFactory extends FactoryAbstract {
* * The PHP file name without file extension (i.e. `AtomFormat`) * * The PHP file name without file extension (i.e. `AtomFormat`)
* * The format name (i.e. `Atom`) * * The format name (i.e. `Atom`)
* *
* Casing is ignored (i.e. `ATOM` and `atom` are the same).
*
* A format file matching the given format name must exist in the working * A format file matching the given format name must exist in the working
* directory! * directory!
* *
@ -118,6 +122,7 @@ class FormatFactory extends FactoryAbstract {
* valid, null otherwise. * valid, null otherwise.
*/ */
protected function sanitizeFormatName($name) { protected function sanitizeFormatName($name) {
$name = ucfirst(strtolower($name));
if(is_string($name)) { if(is_string($name)) {
@ -131,18 +136,12 @@ class FormatFactory extends FactoryAbstract {
$name = $matches[1]; $name = $matches[1];
} }
// Improve performance for correctly written format names // The name is valid if a corresponding format file is found on disk
if(in_array($name, $this->getFormatNames())) { if(in_array($name, $this->getFormatNames())) {
$index = array_search($name, $this->getFormatNames()); $index = array_search($name, $this->getFormatNames());
return $this->getFormatNames()[$index]; return $this->getFormatNames()[$index];
} }
// The name is valid if a corresponding format file is found on disk
if(in_array(strtolower($name), array_map('strtolower', $this->getFormatNames()))) {
$index = array_search(strtolower($name), array_map('strtolower', $this->getFormatNames()));
return $this->getFormatNames()[$index];
}
Debug::log('Invalid format name: "' . $name . '"!'); Debug::log('Invalid format name: "' . $name . '"!');
} }

View file

@ -311,7 +311,7 @@ function getSimpleHTMLDOMCached($url,
$time = $cache->getTime(); $time = $cache->getTime();
if($time !== false if($time !== false
&& (time() - $duration < $time) && (time() - $duration < $time)
&& Debug::isEnabled()) { // Contents within duration && !Debug::isEnabled()) { // Contents within duration
$content = $cache->loadData(); $content = $cache->loadData();
} else { // Content not within duration } else { // Content not within duration
$content = getContents($url, $header, $opts); $content = getContents($url, $header, $opts);