Compare commits

...

23 Commits

Author SHA1 Message Date
9922787aa9
add cybermonit, feedproxy and OSV bridges
Some checks failed
Build Image on Commit and Release / bake (push) Failing after 23s
Lint / phpcs (7.4) (push) Failing after 1m2s
Lint / phpcompatibility (7.4) (push) Successful in 1m6s
Lint / executable_php_files_check (push) Failing after 4s
Tests / phpunit8 (7.4) (push) Failing after 55s
Tests / phpunit8 (8.0) (push) Failing after 56s
Tests / phpunit8 (8.1) (push) Failing after 57s
remove unnecessary salt
2025-03-23 06:25:04 +01:00
André Andersson
dee734d360
Add Auctionet bridge (#4452) 2025-03-05 19:41:24 +01:00
Latz
744f996224
Added bridge for Toms Touché (https://taz.de/#!tom=tomdestages) (#4438) 2025-03-05 19:39:18 +01:00
Pavel Korytov
f270cd35e7
[TldrTechBridge] Fix duplicate entries and empty sections (#4466) 2025-03-05 19:36:41 +01:00
Tomasz Molski
83c36a87e2
[ReutersBridge] Adjust Fact Check feed path (#4465) 2025-03-05 19:35:12 +01:00
Tomasz Molski
810e17b556
feat: added LeagueOfLegendsNewsBridge (#4462) 2025-03-05 19:34:35 +01:00
sysadminstory
97f07cf216
[InstagramBridge] Add a fallback to the "Username" mode (#4461)
- Added some header that could help Instagram to not block RSS Bridge
- Added a fallback function to use the "Embed profile" Instagram feature
  to get the content shared by one Instagram user
2025-03-05 19:32:03 +01:00
sysadminstory
62fafdc24b
[FreeTelechargerBridge] Update URL and some fix (#4459)
- Updated the URL to the new URL in the bridge Meta Data
- Use an other URL that seems to permit to bypass CF protection
  (sometimes)
2025-03-05 19:30:38 +01:00
sysadminstory
cd4cdcfd65
[RadioMelodieBridge] Fix media content (#4458)
- Fix the audio source with the absolute URL
- Fix the pictture enclosure URL (those are already absolute URL)
2025-03-05 19:30:09 +01:00
Tobias Alexander Franke
00a24e2f69
New bridge for the latest Shadertoy submissions (#4456)
* New bridge for the latest Shadertoy submissions

* [ShadertoyBridge] Linter fixes

* [ShadertoyBridge] More Linter fixes

* [ShadertoyBridge] Even more Linter fixes
2025-02-26 10:20:28 +01:00
André Andersson
92b5e7093f
Fix data-lot-id not being correctly set so use href instead (#4453) 2025-02-24 17:58:24 +01:00
Dag
b52f01505d
fix(github): semi-repair (#4449) 2025-02-14 02:42:23 +01:00
Dag
e4c32bb046
fix(vk): semi-disable broken bridge (#4448) 2025-02-14 02:00:07 +01:00
Christian Schabesberger
dd4dcfa59c
fix nn.de description and paywall filter (#4444) 2025-02-08 01:41:51 +01:00
Tostiman
4e678c955f
fix CarThrottleBridge (#4442) 2025-02-05 18:41:42 +01:00
July
549bed64d2
[YouTubeFeedExpanderBridge] Add bridge (#4430) 2025-02-04 20:11:43 +01:00
sysadminstory
94924d8e16
[PepperBridgeAbstract, DealabsBridge, HotUKDealsBridge, MydealsBridge] Fix parameters typo (#4439)
Fixed typo in DealabsBridge and HotUKDealsBridge parameters name
2025-02-03 23:24:42 +01:00
sysadminstory
920b21b1fd
[PepperBridgeAbstract, DealabsBridge, HotUKDealsBridge, MydealsBridge] Fixing bridge and add subcategories (#4436)
- Follow site change to get deal data (fix for #4432)
- Add Categories (sub categories in reality) support
2025-02-03 15:35:48 +01:00
Dag
935075072b
fix: set default cache ttl of 1d (#4434) 2025-01-30 21:05:17 +01:00
July
3ae7a10223
[GovTrackBridge] Rebase on top of official RSS feed (#4429) 2025-01-29 11:11:25 +01:00
Tone
bf431a6eae
[AnisearchBridge] changed id of div so trailers work again (#4428) 2025-01-27 21:55:34 +01:00
Dag
824ac5e373
docs (#4427)
* docs

* docs
2025-01-26 21:24:33 +01:00
Bartosz Sosna
ae8394d976
Fix lfc.pl bug with page content when comments exist (#4425)
* Add lfc.pl bridge

* Adjust bridge

* Add comments section

* Fix a bug with page content when comments exist

* Add brtsos to CONTRIBUTORS.md
2025-01-26 18:58:03 +01:00
32 changed files with 1122 additions and 119 deletions

View File

@ -49,9 +49,9 @@ Please describe what you expect from the bridge. Whenever possible provide sampl
- _Default limit_: 5
- [ ] Load full articles
- _Cache articles_ (articles are stored in a local cache on first request): yes
- _Cache timeout_ (max = 24 hours): 24 hours
- _Cache timeout_ : 24 hours
- [X] Balance requests (RSS-Bridge uses cached versions to reduce bandwith usage)
- _Timeout_ (default = 5 minutes, max = 24 hours): 5 minutes
- _Timeout_ (default = 5 minutes): 5 minutes
<!--Be aware that some options might not be available for your specific request due to technical limitations!-->

View File

@ -23,6 +23,7 @@
* [Binnette](https://github.com/Binnette)
* [BoboTiG](https://github.com/BoboTiG)
* [Bockiii](https://github.com/Bockiii)
* [brtsos](https://github.com/brtsos)
* [captn3m0](https://github.com/captn3m0)
* [chemel](https://github.com/chemel)
* [Chouchen](https://github.com/Chouchen)

View File

@ -150,11 +150,11 @@ listen = /run/php/rss-bridge.sock
listen.owner = www-data
listen.group = www-data
# Create 10 workers standing by to serve requests
; Create 10 workers standing by to serve requests
pm = static
pm.max_children = 10
# Respawn worker after 500 requests (workaround for memory leaks etc.)
; Respawn worker after 500 requests (workaround for memory leaks etc.)
pm.max_requests = 500
```
@ -460,7 +460,6 @@ See [CONTRIBUTORS.md](CONTRIBUTORS.md)
RSS-Bridge uses caching to prevent services from banning your server for repeatedly updating feeds.
The specific cache duration can be different between bridges.
Cached files are deleted automatically after 24 hours.
RSS-Bridge allows you to take full control over which bridges are displayed to the user.
That way you can host your own RSS-Bridge service with your favorite collection of bridges!

View File

@ -67,7 +67,7 @@ class AnisearchBridge extends BridgeAbstract
$trailerlink = $domarticle->find('section#trailers > div > div.swiper > ul.swiper-wrapper > li.swiper-slide > a', 0);
if (isset($trailerlink)) {
$trailersite = getSimpleHTMLDOM($baseurl . $trailerlink->href);
$trailer = $trailersite->find('div#player > iframe', 0);
$trailer = $trailersite->find('div#video > iframe', 0);
$trailer = $trailer->{'data-xsrc'};
$ytlink = <<<EOT
<br /><iframe width="560" height="315" src="$trailer" title="YouTube video player"

344
bridges/AuctionetBridge.php Normal file
View File

@ -0,0 +1,344 @@
<?php
class AuctionetBridge extends BridgeAbstract
{
const NAME = 'Auctionet';
const URI = 'https://www.auctionet.com';
const DESCRIPTION = 'Fetches info about auction objects from Auctionet (an auction platform for many European auction houses)';
const MAINTAINER = 'Qluxzz';
const PARAMETERS = [[
'category' => [
'name' => 'Category',
'type' => 'list',
'values' => [
'All categories' => '',
'Art' => [
'All' => '25-art',
'Drawings' => '119-drawings',
'Engravings & Prints' => '27-engravings-prints',
'Other' => '30-other',
'Paintings' => '28-paintings',
'Photography' => '26-photography',
'Sculptures & Bronzes' => '29-sculptures-bronzes',
],
'Asiatica' => [
'All' => '117-asiatica',
],
'Books, Maps & Manuscripts' => [
'All' => '50-books-maps-manuscripts',
'Autographs & Manuscripts' => '206-autographs-manuscripts',
'Books' => '204-books',
'Maps' => '205-maps',
'Other' => '207-other',
],
'Carpets & Textiles' => [
'All' => '35-carpets-textiles',
'Carpets' => '36-carpets',
'Textiles' => '37-textiles',
],
'Ceramics & Porcelain' => [
'All' => '9-ceramics-porcelain',
'European' => '10-european',
'Oriental' => '11-oriental',
'Rest of the world' => '12-rest-of-the-world',
'Tableware' => '210-tableware',
],
'Clocks & Watches' => [
'All' => '31-clocks-watches',
'Carriage & Miniature Clocks' => '258-carriage-miniature-clocks',
'Longcase clocks' => '32-longcase-clocks',
'Mantel clocks' => '33-mantel-clocks',
'Other clocks' => '34-other-clocks',
'Pocket & Stop Watches' => '110-pocket-stop-watches',
'Wall Clocks' => '127-wall-clocks',
'Wristwatches' => '15-wristwatches',
],
'Coins, Medals & Stamps' => [
'All' => '46-coins-medals-stamps',
'Coins' => '128-coins',
'Orders & Medals' => '135-orders-medals',
'Other' => '131-other',
'Stamps' => '136-stamps',
],
'Folk art' => [
'All' => '58-folk-art',
'Bowls & Boxes' => '121-bowls-boxes',
'Furniture' => '122-furniture',
'Other' => '123-other',
'Tools & Gears' => '120-tools-gears',
],
'Furniture' => [
'All' => '16-furniture',
'Armchairs & Chairs' => '18-armchairs-chairs',
'Chests of drawers' => '24-chests-of-drawers',
'Cupboards, Cabinets & Shelves' => '23-cupboards-cabinets-shelves',
'Dining room furniture' => '22-dining-room-furniture',
'Garden' => '21-garden',
'Other' => '17-other',
'Sofas & seatings' => '20-sofas-seatings',
'Tables' => '19-tables',
],
'Glass' => [
'All' => '6-glass',
'Art glass' => '208-art-glass',
'Other' => '8-other',
'Tableware' => '7-tableware',
'Utility glass' => '209-utility-glass',
],
'Jewellery & Gemstones' => [
'All' => '13-jewellery-gemstones',
'Alliance rings' => '113-alliance-rings',
'Bracelets' => '106-bracelets',
'Brooches & Pendants' => '107-brooches-pendants',
'Costume Jewellery' => '259-costume-jewellery',
'Cufflinks & Tie Pins' => '111-cufflinks-tie-pins',
'Ear studs' => '116-ear-studs',
'Earrings' => '115-earrings',
'Gemstones' => '48-gemstones',
'Jewellery' => '14-jewellery',
'Jewellery Suites' => '109-jewellery-suites',
'Necklace' => '104-necklace',
'Other' => '118-other',
'Rings' => '112-rings',
'Signet rings' => '105-signet-rings',
'Solitaire rings' => '114-solitaire-rings',
],
'Licence weapons' => [
'All' => '59-licence-weapons',
'Combi/Combo' => '63-combi-combo',
'Double express rifles' => '60-double-express-rifles',
'Rifles' => '61-rifles',
'Shotguns' => '62-shotguns',
],
'Lighting & Lamps' => [
'All' => '1-lighting-lamps',
'Candlesticks' => '4-candlesticks',
'Ceiling lights' => '3-ceiling-lights',
'Chandeliers' => '203-chandeliers',
'Floor lights' => '2-floor-lights',
'Other lighting' => '5-other-lighting',
'Table Lamps' => '125-table-lamps',
'Wall Lights' => '124-wall-lights',
],
'Mirrors' => [
'All' => '42-mirrors',
],
'Miscellaneous' => [
'All' => '43-miscellaneous',
'Fishing equipment' => '54-fishing-equipment',
'Miscellaneous' => '47-miscellaneous',
'Modern Tools' => '133-modern-tools',
'Modern consumer electronics' => '52-modern-consumer-electronics',
'Musical instruments' => '51-musical-instruments',
'Technica & Nautica' => '45-technica-nautica',
],
'Photo, Cameras & Lenses' => [
'All' => '57-photo-cameras-lenses',
'Cameras & accessories' => '71-cameras-accessories',
'Optics' => '66-optics',
'Other' => '72-other',
],
'Silver & Metals' => [
'All' => '38-silver-metals',
'Other metals' => '40-other-metals',
'Pewter, Brass & Copper' => '41-pewter-brass-copper',
'Silver' => '39-silver',
'Silver plated' => '213-silver-plated',
],
'Toys' => [
'All' => '44-toys',
'Comics' => '211-comics',
'Toys' => '212-toys',
],
'Tribal art' => [
'All' => '134-tribal-art',
],
'Vehicles, Boats & Parts' => [
'All' => '249-vehicles-boats-parts',
'Automobilia & Transport' => '255-automobilia-transport',
'Bicycles' => '132-bicycles',
'Boats & Accessories' => '250-boats-accessories',
'Car parts' => '253-car-parts',
'Cars' => '215-cars',
'Moped parts' => '254-moped-parts',
'Mopeds' => '216-mopeds',
'Motorcycle parts' => '252-motorcycle-parts',
'Motorcycles' => '251-motorcycles',
'Other' => '256-other',
],
'Vintage & Designer Fashion' => [
'All' => '49-vintage-designer-fashion',
],
'Weapons & Militaria' => [
'All' => '137-weapons-militaria',
'Airguns' => '257-airguns',
'Armour & Uniform' => '138-armour-uniform',
'Edged weapons' => '130-edged-weapons',
'Guns & Rifles' => '129-guns-rifles',
'Other' => '214-other',
],
'Wine, Port & Spirits' => [
'All' => '170-wine-port-spirits',
],
]
],
'sort_order' => [
'name' => 'Sort order',
'type' => 'list',
'values' => [
'Most bids' => 'bids_count_desc',
'Lowest bid' => 'bid_asc',
'Highest bid' => 'bid_desc',
'Last bid on' => 'bid_on',
'Ending soonest' => 'end_asc_active',
'Lowest estimate' => 'estimate_asc',
'Highest estimate' => 'estimate_desc',
'Recently added' => 'recent'
],
],
'country' => [
'name' => 'Country',
'type' => 'list',
'values' => [
'All' => '',
'Denmark' => 'DK',
'Finland' => 'FI',
'Germany' => 'DE',
'Spain' => 'ES',
'Sweden' => 'SE',
'United Kingdom' => 'GB'
]
],
'language' => [
'name' => 'Language',
'type' => 'list',
'values' => [
'English' => 'en',
'Español' => 'es',
'Deutsch' => 'de',
'Svenska' => 'sv',
'Dansk' => 'da',
'Suomi' => 'fi',
],
],
]];
const CACHE_TIMEOUT = 3600; // 1 hour
private $title;
public function collectData()
{
// Each page contains 48 auctions
// So we fetch 10 pages so we decrease the likelihood
// of missing auctions between feed refreshes
// Fetch first page and use that to get title
{
$url = $this->getUrl(1);
$data = getContents($url);
$title = $this->getDocumentTitle($data);
$this->items = array_merge($this->items, $this->parsePageData($data));
}
// Fetch remaining pages
for ($page = 2; $page <= 10; $page++) {
$url = $this->getUrl($page);
$data = getContents($url);
$this->items = array_merge($this->items, $this->parsePageData($data));
}
}
public function getName()
{
return $this->title ?: parent::getName();
}
/* HELPERS */
private function getUrl($page)
{
$category = $this->getInput('category');
$language = $this->getInput('language');
$sort_order = $this->getInput('sort_order');
$country = $this->getInput('country');
$url = self::URI . '/' . $language . '/search';
if ($category) {
$url = $url . '/' . $category;
}
$query = [];
$query['page'] = $page;
if ($sort_order) {
$query['order'] = $sort_order;
}
if ($country) {
$query['country_code'] = $country;
}
if (count($query) > 0) {
$url = $url . '?' . http_build_query($query);
}
return $url;
}
private function getDocumentTitle($data)
{
$title_elem = '<title>';
$title_elem_length = strlen($title_elem);
$title_start = strpos($data, $title_elem);
$title_end = strpos($data, '</title>', $title_start);
$title_length = $title_end - $title_start + strlen($title_elem);
$title = substr($data, $title_start + strlen($title_elem), $title_length);
return $title;
}
/**
* The auction items data is included in the HTML document
* as a HTML entities encoded JSON structure
* which is used to hydrate the React component for the list of auctions
*/
private function parsePageData($data)
{
$key = 'data-react-props="';
$keyLength = strlen($key);
$start = strpos($data, $key);
$end = strpos($data, '"', $start + strlen($key));
$length = $end - ($start + $keyLength);
$jsonString = substr($data, $start + $keyLength, $length);
$jsonData = json_decode(htmlspecialchars_decode($jsonString), false);
$items = [];
foreach ($jsonData->{'items'} as $item) {
$title = $item->{'longTitle'};
$relative_url = $item->{'url'};
$images = $item->{'imageUrls'};
$id = $item->{'auctionId'};
$items[] = [
'title' => $title,
'uri' => self::URI . $relative_url,
'uid' => $id,
'content' => count($images) > 0 ? "<img src='$images[0]'/><br/>$title" : $title,
'enclosures' => array_slice($images, 1),
];
}
return $items;
}
}

View File

@ -206,7 +206,7 @@ class BukowskisBridge extends BridgeAbstract
$this->items[] = [
'title' => $title,
'uri' => $baseUrl . $relative_url,
'uid' => $lot->getAttribute('data-lot-id'),
'uid' => $relative_url,
'content' => count($images) > 0 ? "<img src='$images[0]'/><br/>$title" : $title,
'enclosures' => array_slice($images, 1),
];

View File

@ -66,7 +66,7 @@ class CarThrottleBridge extends BridgeAbstract
foreach ($categoryPage->find('div.cmg-card') as $post) {
$item = [];
$titleElement = $post->find('div.title a')[0];
$titleElement = $post->find('a.title')[0];
$post_uri = self::URI . $titleElement->getAttribute('href');
if (!isset($post_uri) || $post_uri == '') {
@ -80,8 +80,8 @@ class CarThrottleBridge extends BridgeAbstract
$item['author'] = $this->parseAuthor($articlePage);
$articleImage = $articlePage->find('div.block-layout-field-image')[0];
$article = $articlePage->find('div.block-layout-body')[1];
$articleImage = $articlePage->find('figure')[0];
$article = $articlePage->find('div.first-column div.body')[0];
//remove ads
foreach ($article->find('aside') as $ad) {

152
bridges/CybermonitBridge.php Executable file
View File

@ -0,0 +1,152 @@
<?php
class CybermonitBridge extends BridgeAbstract
{
const SALT = "Mc!dks\\0#)as";
const NAME = 'Cybermonit';
const DESCRIPTION = 'Returns cybermonite CVEs, DDoS, Ransomware, Leaks, EOL, Releases';
const MAINTAINER = 'void';
const PARAMETERS = [[
'feed_type' => [
'name' => 'feed_type',
'type' => 'list',
'values' => [
'Leaks' => 'Leaks',
'CVEs' => 'CVEs',
'DDoS' => 'DDoS',
'Ransomware' => 'Ransomware',
'Releases' => 'Releases',
'EOL' => "EOL"
]
]
]];
public function getURI() {
switch ($this->getKey("feed_type")) {
case 'Leaks':
return 'https://pypbbsgyhtlerxdyfuvv.supabase.co/storage/v1/object/dane/leak.json';
break;
case 'CVEs':
return 'https://pypbbsgyhtlerxdyfuvv.supabase.co/storage/v1/object/dane/2025.json';
break;
case 'DDoS':
return 'https://pypbbsgyhtlerxdyfuvv.supabase.co/storage/v1/object/dane/ddos.json';
break;
case 'Ransomware':
return 'https://pypbbsgyhtlerxdyfuvv.supabase.co/storage/v1/object/dane/ransomware.json';
break;
case 'EOL':
return 'https://pypbbsgyhtlerxdyfuvv.supabase.co/storage/v1/object/dane/eol.json';
break;
case 'Releases':
return 'https://pypbbsgyhtlerxdyfuvv.supabase.co/storage/v1/object/dane/releases.json';
break;
default:
return 'https://cybermonit.com';
break;
}
}
public function collectData()
{
$opts = array(
'accept: */*',
'apikey: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InB5cGJic2d5aHRsZXJ4ZHlmdXZ2Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDAxMjUwNzAsImV4cCI6MjA1NTcwMTA3MH0.xmcCqJUgvRtVsp6XmaH1YmUeerqZRZNQ_XmCnpOEFAo',
'authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InB5cGJic2d5aHRsZXJ4ZHlmdXZ2Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDAxMjUwNzAsImV4cCI6MjA1NTcwMTA3MH0.xmcCqJUgvRtVsp6XmaH1YmUeerqZRZNQ_XmCnpOEFAo',
'origin: https://cybermonit.com',
'referer: https://cybermonit.com',
'user-agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) QtWebEngine/6.8.2 Chrome/122.0.6261.171 Safari/537.36',
'x-client-info: supabase-js-web/2.49.1',
);
$html = getContents($this->getURI(), $opts);
$data = json_decode($html, true);
array_walk_recursive($data, function(&$item, $key) {
$item = htmlspecialchars($item);
});
switch ($this->getKey("feed_type")) {
case 'Leaks':
foreach ($data as $obj) {
$this->items[] = [
'title' => sprintf("%s - %s", empty($obj['domain']) ? 'Unknown' : $obj['domain'], $obj['data_leaked']),
'timestamp' => $obj['breach_date'],
'uri' => "https://cybermonit.com/leaks",
'categories' => ['leaks'],
'content' => $obj['description'],
'uid' => sha1(sprintf("%s.%s.%s", $obj['domain'], $obj['breach_date'], self::SALT))
];
}
break;
case 'CVEs':
foreach ($data as $obj) {
$this->items[] = [
'title' => sprintf("%s (%s %s) - %s", $obj['cve_id'], $obj['score'], $obj['severity_en'], substr($obj['description'], 0, 150).'[...]'),
'timestamp' => $obj['publishedDate'],
'uri' => "https://cybermonit.com/cve",
"categories" => ['CVEs'],
'content' => $obj['description'],
'uid' => sha1(sprintf("%s.%s.%s", $obj['cve_id'], $obj['publishedDate'], self::SALT))
];
}
break;
case 'DDoS':
foreach ($data['targets'] as $obj) {
$this->items[] = [
'title' => sprintf("%s", $obj['host']),
'timestamp' => null,
'uri' => "https://cybermonit.com/ddos",
"categories" => ['DDoS'],
"content" => sprintf("IP: %s<br/>Type: %s<br>Method: %s<br>Port: %s<br>Use SSL: %s", $obj['ip'], $obj['type'], $obj['method'], $obj['port'], $obj['use_ssl'] == 1 ? "True" : "False"),
'uid' => sha1(sprintf("%s.%s.%s", $obj['host'], $obj['ip'], self::SALT))
];
}
break;
case 'Ransomware':
foreach ($data as $obj) {
$this->items[] = [
'title' => sprintf("%s attack on %s (%s)", $obj['group'], $obj['victim'], $obj['country']),
'timestamp' => $obj['attackdate'],
"categories" => ['Ransomware', $obj['group']],
'uri' => "https://cybermonit.com/ransomware",
"content" => "Ransomware attack ({$obj['group']}) on {$obj['victim']}, country: {$obj['country']}",
'uid' => sha1(sprintf("%s.%s.%s", $obj['victim'], $obj['attackdate'], self::SALT))
];
}
break;
case 'Releases':
foreach ($data as $obj) {
$this->items[] = [
'title' => sprintf("%s %s", $obj['nazwa_projektu'], $obj['wersja']),
'timestamp' => $obj['data_wydania'],
'categories' => ['Releases'],
'uri' => $obj['url_zdjecia'],
'content' => sprintf("%s updated to version %s. Project URL: %s, Release notes: %s", $obj['nazwa_projektu'], $obj['wersja'], $obj['url_projektu'], $obj['url_zdjecia']),
'uid' => sha1(sprintf("%s.%s.%s", $obj['nazwa_projektu'], $obj['data_wydania'], self::SALT))
];
}
break;
case 'EOL':
foreach ($data as $key => $v) {
$val = $v[0];
$this->items[] = [
'title' => sprintf("%s %s %s", $key, $val['cycle'], $val['lts'] == true ? 'LTS' : ''),
'timestamp'=> null,
'categories'=> ['EOL'],
'uri' => $this->getURI(),
'content' => sprintf("Product: %s<br>Version: %s<br>Release date: %s<br>End of life: %s", $key, $val['cycle'], $val['releaseDate'], $val['eol']),
'uid' => sha1(sprintf("%s.%s.%s", $key, $val['eol'], self::SALT))
];
}
break;
default:
break;
}
usort($this->items, function($a, $b) {
if ($a['timestamp'] == $b['timestamp']) {
return 0;
}
return ($a['timestamp'] > $b['timestamp']) ? -1 : 1;
});
}
}

View File

@ -48,6 +48,16 @@ https://www.dealabs.com/groupe/abonnements-internet?sortBy=lowest_price
Il faut alors saisir :
abonnements-internet',
],
'subgroups' => [
'name' => 'Catégorie',
'type' => 'text',
'exampleValue' => '1071',
'title' => 'Numéro du ou des catégories dans l\'URL : Il faut entrer le ou les numéros de catégories qui sont présent après "groups=" et avant tout éventuel "&"
Exemple : Si l\'URL du groupe affichées dans le navigateur est :
https://www.dealabs.com/groupe/telecommunications?groups=1071%2C1070&sortBy=new
Il faut alors saisir :
1071%2C1070',
],
'order' => [
'name' => 'Trier par',
'type' => 'list',
@ -88,6 +98,7 @@ abonnements-internet',
'uri-group' => 'groupe/',
'uri-deal' => 'bons-plans/',
'uri-merchant' => 'search/bons-plans?merchant-id=',
'image-host' => 'https://static-pepper.dealabs.com/',
'request-error' => 'Impossible de joindre Dealabs',
'thread-error' => 'Impossible de déterminer l\'ID de la discussion. Vérifiez l\'URL que vous avez entré',
'currency' => '€',

50
bridges/FeedProxyBridge.php Executable file
View File

@ -0,0 +1,50 @@
<?php
class FeedProxyBridge extends FeedExpander
{
const MAINTAINER = 'void';
const NAME = 'FeedProxy';
const URI = '';
const DESCRIPTION = <<<'TEXT'
This bridge simply parse an external RSS<br>
TEXT;
const PARAMETERS = [
[
'feed_name' => [
'name' => 'Feed name',
'type' => 'text',
'exampleValue' => 'FeedProxy',
],
'feed_1' => [
'name' => 'Feed url',
'type' => 'text',
'required' => true,
'exampleValue' => 'https://lorem-rss.herokuapp.com/feed?unit=day'
],
'limit' => self::LIMIT,
]
];
/**
* TODO: Consider a strategy which produces a shorter feed url
*/
public function collectData()
{
$limit = (int)($this->getInput('limit') ?: 99);
$feed = $this->getInput('feed_1');
$this->collectExpandableDatas($feed, $limit);
// Sort by timestamp, uri, title in descending order
usort($this->items, function ($a, $b) {
$t1 = $a['timestamp'] ?? $a['uri'] ?? $a['title'];
$t2 = $b['timestamp'] ?? $b['uri'] ?? $b['title'];
return $t2 <=> $t1;
});
}
public function getName()
{
return $this->getInput('feed_name') ?: 'FeedProxy';
}
}

View File

@ -3,7 +3,8 @@
class FreeTelechargerBridge extends BridgeAbstract
{
const NAME = 'Free-Telecharger';
const URI = 'https://www.free-telecharger.art/';
const URI = 'https://www.free-telecharger.fun/';
const ALTERNATEURI = 'https://www.free-telecharger.com/';
const DESCRIPTION = 'Suivi de série sur Free-Telecharger';
const MAINTAINER = 'sysadminstory';
const PARAMETERS = [
@ -12,19 +13,19 @@ class FreeTelechargerBridge extends BridgeAbstract
'name' => 'URL de la série',
'type' => 'text',
'required' => true,
'title' => 'URL d\'une série sans le https://www.free-telecharger.art/',
'title' => 'URL d\'une série sans le https://www.free-telecharger.fun/',
'pattern' => 'series.*\.html',
'exampleValue' => 'series-vf-hd/151432-wolf-saison-1-complete-web-dl-720p.html'
],
]
];
const CACHE_TIMEOUT = 3600;
private string $showTitle;
private string $showTechDetails;
private string $showTitle = '';
private string $showTechDetails = '';
public function collectData()
{
$html = getSimpleHTMLDOM(self::URI . $this->getInput('url'));
$html = getSimpleHTMLDOM(self::ALTERNATEURI . $this->getInput('url'));
// Find all block content of the page
$blocks = $html->find('div[class=block1]');

View File

@ -192,15 +192,18 @@ class GithubIssueBridge extends BridgeAbstract
public function collectData()
{
$html = getSimpleHTMLDOM($this->getURI());
$url = $this->getURI();
$html = getSimpleHTMLDOM($url);
switch ($this->queriedContext) {
case static::BRIDGE_OPTIONS[1]: // Issue comments
$this->items = $this->extractIssueComments($html);
break;
case static::BRIDGE_OPTIONS[0]: // Project Issues
foreach ($html->find('.js-active-navigation-container .js-navigation-item') as $issue) {
$info = $issue->find('.opened-by', 0);
$issues = $html->find('.js-active-navigation-container .js-navigation-item');
$issues = $html->find('.IssueRow-module__row--XmR1f');
foreach ($issues as $issue) {
$info = $issue->find('.issue-item-module__authorCreatedLink--wFZvk', 0);
preg_match('/\/([0-9]+)$/', $issue->find('a', 0)->href, $match);
$issueNbr = $match[1];
@ -222,24 +225,24 @@ class GithubIssueBridge extends BridgeAbstract
$item['content'] = 'Can not extract comments from ' . $uri;
}
$item['author'] = $info->find('a', 0)->plaintext;
$item['author'] = $issue->find('a', 1)->plaintext;
$item['timestamp'] = strtotime(
$info->find('relative-time', 0)->getAttribute('datetime')
$issue->find('relative-time', 0)->getAttribute('datetime')
);
$item['title'] = html_entity_decode(
$issue->find('.js-navigation-open', 0)->plaintext,
$issue->find('h3', 0)->plaintext,
ENT_QUOTES,
'UTF-8'
);
$comment_count = 0;
if ($span = $issue->find('a[aria-label*="comment"] span', 0)) {
$comment_count = $span->plaintext;
}
//$comment_count = 0;
//if ($span = $issue->find('a[aria-label*="comment"] span', 0)) {
// $comment_count = $span->plaintext;
//}
$item['content'] .= "\n" . 'Comments: ' . $comment_count;
//$item['content'] .= "\n" . 'Comments: ' . $comment_count;
$item['uri'] = self::URI
. trim($issue->find('.js-navigation-open', 0)->getAttribute('href'), '/');
. trim($issue->find('a', 0)->getAttribute('href'), '/');
$this->items[] = $item;
}
break;

View File

@ -1,6 +1,6 @@
<?php
class GovTrackBridge extends BridgeAbstract
class GovTrackBridge extends FeedExpander
{
const NAME = 'GovTrack';
const MAINTAINER = 'phantop';
@ -18,64 +18,50 @@ class GovTrackBridge extends BridgeAbstract
'Major Legislative Activity' => 'major-bill-activity',
'New Bills and Resolutions' => 'introduced-bills',
'New Laws' => 'enacted-bills',
'Posts from Us' => 'posts'
]
],
'limit' => self::LIMIT
'News from Us' => 'posts'
]
],
'limit' => self::LIMIT
]];
public function collectData()
{
$html = getSimpleHTMLDOMCached($this->getURI());
if ($this->getInput('feed') != 'posts') {
$this->collectEvent($html);
return;
}
$html = defaultLinkTo($html, parent::getURI());
$limit = $this->getInput('limit') ?? 10;
foreach ($html->find('section') as $element) {
if (--$limit == 0) {
break;
}
$info = explode(' ', $element->find('p', 0)->innertext);
$item = [
'categories' => [implode(' ', array_slice($info, 4))],
'timestamp' => strtotime(implode(' ', array_slice($info, 0, 3))),
'title' => $element->find('a', 0)->innertext,
'uri' => $element->find('a', 0)->href,
];
$html = getSimpleHTMLDOMCached($item['uri']);
$html = defaultLinkTo($html, parent::getURI());
$content = $html->find('#content .col-md', 1);
$info = explode(' by ', $content->find('p', 0)->plaintext);
$content->removeChild($content->firstChild());
$item['author'] = implode(' ', array_slice($info, 1));
$item['content'] = $content->innertext;
$this->items[] = $item;
$limit = $this->getInput('limit') ?? 15;
if ($this->getInput('feed') == 'posts') {
$this->collectExpandableDatas($this->getURI() . '.rss', $limit);
} else {
$this->collectEvent($this->getURI(), $limit);
}
}
private function collectEvent($html)
protected function parseItem(array $item)
{
$opt = [];
preg_match('/"csrfmiddlewaretoken" value="(.*)"/', $html, $opt);
$html = getSimpleHTMLDOMCached($item['uri']);
$html = defaultLinkTo($html, parent::getURI());
$item['categories'] = [$html->find('.breadcrumb-item', 1)->plaintext];
$content = $html->find('#content .col-md', 1);
$item['author'] = explode(' by ', $content->firstChild()->plaintext)[1];
$content->removeChild($content->firstChild());
$item['content'] = $content->innertext;
return $item;
}
private function collectEvent($uri, $limit)
{
$html = getSimpleHTMLDOMCached($uri);
preg_match('/"csrfmiddlewaretoken" value="(.*)"/', $html, $preg);
$header = [
"cookie: csrftoken=$opt[1]",
"x-csrftoken: $opt[1]",
"cookie: csrftoken=$preg[1]",
"x-csrftoken: $preg[1]",
'referer: ' . parent::getURI(),
];
preg_match('/var selected_feed = "(.*)";/', $html, $opt);
$post = [
'count' => $this->getInput('limit') ?? 20,
'feed' => $opt[1]
];
$opt = [ CURLOPT_POSTFIELDS => $post ];
preg_match('/var selected_feed = "(.*)";/', $html, $preg);
$opt = [ CURLOPT_POSTFIELDS => [
'count' => $limit,
'feed' => $preg[1]
]];
$html = getContents(parent::getURI() . 'events/_load_events', $header, $opt);
$html = defaultLinkTo(str_get_html($html), parent::getURI());
@ -83,10 +69,10 @@ class GovTrackBridge extends BridgeAbstract
foreach ($html->find('.tracked_event') as $event) {
$bill = $event->find('.event_title a, .event_body a', 0);
$date = explode(' ', $event->find('.event_date', 0)->plaintext);
preg_match('/Sponsor:(.*)\n/', $event->plaintext, $opt);
preg_match('/Sponsor:(.*)\n/', $event->plaintext, $preg);
$item = [
'author' => $opt[1] ?? '',
'author' => $preg[1] ?? '',
'content' => $event->find('td', 1)->innertext,
'enclosures' => [$event->find('img', 0)->src],
'timestamp' => strtotime(implode(' ', array_slice($date, 2))),
@ -115,10 +101,10 @@ class GovTrackBridge extends BridgeAbstract
public function getURI()
{
if ($this->getInput('feed') != 'posts') {
$url = parent::getURI() . 'events/' . $this->getInput('feed');
} else {
if ($this->getInput('feed') == 'posts') {
$url = parent::getURI() . $this->getInput('feed');
} else {
$url = parent::getURI() . 'events/' . $this->getInput('feed');
}
return $url;
}

View File

@ -47,6 +47,16 @@ Example: If the URL of the group displayed in the browser is :
https://www.hotukdeals.com/tag/broadband?sortBy=temp
Then enter :
broadband',
],
'subgroups' => [
'name' => 'category',
'type' => 'text',
'exampleValue' => '343563',
'title' => 'Category number in the URL : The category number that must be entered is present after "groups=" and before any "&".
Example: If the URL of the group displayed in the browser is :
https://www.hotukdeals.com/tag/broadband?groups=343563&sortBy=new
Then enter :
343563',
],
'order' => [
'name' => 'Order by',
@ -86,6 +96,7 @@ broadband',
'uri-group' => 'tag/',
'uri-deal' => 'deals/',
'uri-merchant' => 'search/deals?merchant-id=',
'image-host' => 'https://images.hotukdeals.com/',
'request-error' => 'Could not request HotUKDeals',
'thread-error' => 'Unable to determine the thread ID. Check the URL you entered',
'currency' => '£',

View File

@ -86,6 +86,11 @@ class InstagramBridge extends BridgeAbstract
$headers = [];
$sessionId = $this->getOption('session_id');
$dsUserId = $this->getOption('ds_user_id');
$headers[] = 'x-ig-app-id: 936619743392459';
$headers[] = 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36';
$headers[] = 'Accept-Language: en-US,en;q=0.9,ru;q=0.8';
$headers[] = 'Accept-Encoding: gzip, deflate, br';
$headers[] = 'Accept: */*';
if ($sessionId and $dsUserId) {
$headers[] = 'cookie: sessionid=' . $sessionId . '; ds_user_id=' . $dsUserId;
}
@ -125,8 +130,10 @@ class InstagramBridge extends BridgeAbstract
return;
}
if (!is_null($this->getInput('u'))) {
if (!is_null($this->getInput('u')) && !$this->fallbackMode) {
$userMedia = $data->data->user->edge_owner_to_timeline_media->edges;
} elseif (!is_null($this->getInput('u')) && $this->fallbackMode) {
$userMedia = $data->context->graphql_media;
} elseif (!is_null($this->getInput('h'))) {
$userMedia = $data->data->hashtag->edge_hashtag_to_media->edges;
} elseif (!is_null($this->getInput('l'))) {
@ -134,7 +141,12 @@ class InstagramBridge extends BridgeAbstract
}
foreach ($userMedia as $media) {
$media = $media->node;
// The media is not in the same element if in fallback mode than not
if (!$this->fallbackMode) {
$media = $media->node;
} else {
$media = $media->shortcode_media;
}
switch ($this->getInput('media_type')) {
case 'all':
@ -267,14 +279,39 @@ class InstagramBridge extends BridgeAbstract
protected function getInstagramJSON($uri)
{
// Sets fallbackMode to false
$this->fallbackMode = false;
if (!is_null($this->getInput('u'))) {
$userId = $this->getInstagramUserId($this->getInput('u'));
$data = $this->getContents(self::URI .
try {
$userId = $this->getInstagramUserId($this->getInput('u'));
$data = $this->getContents(self::URI .
'graphql/query/?query_hash=' .
self::USER_QUERY_HASH .
'&variables={"id"%3A"' .
$userId .
'"%2C"first"%3A10}');
} catch (HttpException $e) {
// If loading the data directly failed, we fall back to the "/embed" data loading
// We are in the fallback mode : set a booolean to handle this specific case while collecting the content
$this->fallbackMode = true;
// Get the HTML code of the profile embed page, and extract the JSON of it
$username = $this->getInput('u');
// Load the content using the integrated function to use helping headers
$htmlString = $this->getContents(self::URI . $username . '/embed/');
// Load the String as an SimpleHTMLDom Object
$html = new simple_html_dom();
$html->load($htmlString);
// Find the <script> tag containing the JSON content
$jsCode = $html->find('body', 0)->find('script', 3)->innertext;
// Extract the content needed by our bridge of the whole Javascript content
$regex = '#"contextJSON":"(.*)"}\]\],\["NavigationMetrics"#m';
preg_match($regex, $jsCode, $matches);
$jsVariable = $matches[1];
$data = stripcslashes($jsVariable);
// stripcslashes remove Javascript unicode escaping : add it back to the string so json_decode can handle it
$data = preg_replace('/(?<!\\\\)u[0-9A-Fa-f]{4}/', '\\\\$0', $data);
}
return json_decode($data);
} elseif (!is_null($this->getInput('h'))) {
$data = $this->getContents(self::URI .

View File

@ -0,0 +1,118 @@
<?php
class LeagueOfLegendsNewsBridge extends BridgeAbstract
{
const NAME = 'League of Legends News';
const URI = 'https://www.leagueoflegends.com';
const DESCRIPTION = 'Official League of Legends news.';
const MAINTAINER = 'KappaPrajd';
const PARAMETERS = [
[
'language' => [
'name' => 'Language',
'type' => 'list',
'defaultValue' => 'en-us',
'values' => [
'English (NA)' => 'en-us',
'English (EUW)' => 'en-gb',
'Deutsch' => 'de-de',
'Español (EUW)' => 'es-es',
'Français' => 'fr-fr',
'Italiano' => 'it-it',
'Polski' => 'pl-pl',
'Ελληνικά' => 'el-gr',
'Română' => 'ro-ro',
'Magyar' => 'hu-hu',
'Čeština' => 'cs-cz',
'Español (LATAM)' => 'es-mx',
'Português' => 'pt-br',
'日本語' => 'ja-jp',
'Русский' => 'ru-ru',
'Türkçe' => 'tr-tr',
'English (OCE)' => 'en-au',
'한국어' => 'ko-kr',
'English (SG)' => 'en-sg',
'English (PH)' => 'en-ph',
'Tiếng Việt' => 'vi-vn',
'ภาษาไทย' => 'th-th',
'繁體中文' => 'zh-tw',
'العربية' => 'ar-ae'
]
],
'category' => [
'name' => 'Category',
'type' => 'list',
'defaultValue' => 'all',
'values' => [
'All' => 'all',
'Game updates' => 'game-updates',
'Esports' => 'esports',
'Dev' => 'dev',
'Lore' => 'lore',
'Media' => 'media',
'Merch' => 'merch',
'Community' => 'community',
'Riot Games' => 'riot-games'
]
],
'onlyPatchNotes' => [
'name' => 'Only patch notes',
'type' => 'checkbox',
'defaultValue' => false,
],
],
];
public function collectData()
{
$siteUrl = $this->getSiteUrl();
$html = getSimpleHTMLDOM($siteUrl);
$articles = $html->find('a[data-testid=articlefeaturedcard-component]');
foreach ($articles as $article) {
$title = $article->find('div[data-testid=card-title]', 0)->plaintext;
$content = $article->find('div[data-testid=card-description] div div div', 0)->plaintext;
$timestamp = $article->find('div[data-testid=card-date] time', 0)->getAttribute('datetime');
$href = $article->getAttribute('href');
$item = [
'title' => $title,
'content' => $content,
'timestamp' => $timestamp,
'uri' => $this->getArticleUri($href),
];
$this->items[] = $item;
}
}
private function getSiteUrl()
{
$lang = $this->getInput('language');
$category = $this->getInput('category');
$onlyPatchNotes = $this->getInput('onlyPatchNotes');
$url = self::URI . '/' . $lang . '/news';
if ($onlyPatchNotes) {
return $url . '/tags/patch-notes';
} else if ($category === 'all') {
return $url;
}
return $url . '/' . $category;
}
private function getArticleUri($href)
{
$isInternalLink = str_starts_with($href, '/');
if ($isInternalLink) {
return self::URI . $href;
}
return $href;
}
}

View File

@ -78,8 +78,8 @@ class LfcPlBridge extends BridgeAbstract
foreach ($commentsDom as $comment) {
$header = $comment->find('.header', 0)->plaintext;
$content = $comment->find('.content', 0)->plaintext;
$comments .= $header . '<br />' . $content . '<br /><br />';
$commentContent = $comment->find('.content', 0)->plaintext;
$comments .= $header . '<br />' . $commentContent . '<br /><br />';
}
}
}

View File

@ -48,6 +48,16 @@ https://www.mydealz.de/gruppe/dsl?sortBy=temp
Dann geben Sie ein:
dsl',
],
'subgroups' => [
'name' => 'Kategorie',
'type' => 'text',
'exampleValue' => '293',
'title' => 'Nummer des Kategorie in der URL: Der einzugebende Kategorienummer steht nach "groups=" und vor einem "&".
Beispiel: Wenn die URL der Gruppe, die im Browser angezeigt wird, :
https://www.mydealz.de/gruppe/telefon-internet?groups=153%2C154&sortBy=new&time_frame=0
Dann geben Sie ein:
153%2C154',
],
'order' => [
'name' => 'sortieren nach',
'type' => 'list',
@ -84,6 +94,7 @@ dsl',
'uri-group' => 'gruppe/',
'uri-deal' => 'deals/',
'uri-merchant' => 'search/gutscheine?merchant-id=',
'image-host' => 'https://static.mydealz.de/',
'request-error' => 'Could not request mydeals',
'thread-error' => 'Die ID der Diskussion kann nicht ermittelt werden. Überprüfen Sie die eingegebene URL',
'currency' => '€',

View File

@ -6,7 +6,7 @@ class NurembergerNachrichtenBridge extends BridgeAbstract
const NAME = 'Nürnberger Nachrichten';
const CACHE_TIMEOUT = 3600;
const URI = 'https://www.nn.de';
const DESCRIPTION = 'Bridge for Bavarian regional news site nordbayern.de';
const DESCRIPTION = 'Bridge for NurembergerNachrichten news site nn.de';
const PARAMETERS = [ [
'region' => [
'name' => 'region',
@ -66,7 +66,7 @@ class NurembergerNachrichtenBridge extends BridgeAbstract
// exclude nn+ articles if desired
if (
$this->getInput('hideNNPlus') &&
str_contains($articleContent->find('article[id=article]', 0)->find('header', 0), 'icon-nnplus')
$articleContent->find('div[class=paywall]')
) {
continue;
}

54
bridges/OSVBridge.php Executable file
View File

@ -0,0 +1,54 @@
<?php
class OSVBridge extends BridgeAbstract
{
const NAME = 'OSV';
const DESCRIPTION = 'Parse osv.dev vulns';
const MAINTAINER = 'void';
const URI = "https://osv.dev/list";
const MAIN_DOMAIN = "https://osv.dev";
const PARAMETERS = [[
]];
protected function parseItems($html) {
foreach ($html->find('.vuln-table-row') as $element) {
$item = [];
$link = $element->find('.vuln-table-cell > a', 0);
if (empty($link)) continue;
$time = $element->find('span.vuln-table-cell',3)->find('relative-time',0);
$summary = $element->find('.vuln-summary',0);
$tags = $element->find('ul.tags > li',0);
$item['uri'] = self::MAIN_DOMAIN . $link->href;
$item['title'] = $link->innertext;
$item['title'] .= sprintf(" - %s", trim($summary->innertext));
$item['timestamp'] = $time->datetime;
$packages = "";
foreach ($element->find('ul.packages > li',0) as $pack) {
$packages .= !empty($pack->innertext) ? "<li>$pack->innertext</li>" : "";
}
$tagsout = "";
foreach ($element->find('ul.tags > li > span') as $tag) {
$tagsout .= !empty($tag->innertext) ? "<li>{$tag->innertext}</li>" : "";
}
$item['content'] = "{$link->innertext} - {$summary->innertext}<br/>Published: {$time->innertext}<br/>Packages: <ul>{$packages}</ul><br/>Tags: <ul>{$tagsout}</ul>";
$item['uid'] = $link->innertext;
$this->items[] = $item;
}
}
public function collectData()
{
$html = getSimpleHTMLDOM($this->getURI());
$this->parseItems($html);
usort($this->items, function($a, $b) {
if ($a['timestamp'] == $b['timestamp']) {
return 0;
}
return ($a['timestamp'] > $b['timestamp']) ? -1 : 1;
});
}
}

View File

@ -62,7 +62,7 @@ class PepperBridgeAbstract extends BridgeAbstract
foreach ($list as $deal) {
// Get the JSON Data stored as vue
$jsonDealData = $this->getDealJsonData($deal);
$dealMeta = Json::decode($deal->find('div[class=threadGrid-headerMeta]', 0)->find('div[class=js-vue2]', 1)->getAttribute('data-vue2'));
$dealMeta = Json::decode($deal->find('div[class=js-vue2]', 1)->getAttribute('data-vue2'));
$item = [];
$item['uri'] = $this->getDealURI($jsonDealData);
@ -80,7 +80,7 @@ class PepperBridgeAbstract extends BridgeAbstract
. $this->getShipsFrom($dealMeta)
. $this->getShippingCost($jsonDealData)
. $this->getSource($jsonDealData)
. $this->getDealLocation($dealMeta)
. $this->getDealLocation($jsonDealData)
. $deal->find('div[class*=' . $selectorDescription . ']', 0)->innertext
. '</td><td>'
. $this->getTemperature($jsonDealData)
@ -402,14 +402,9 @@ HEREDOC;
* Get the Deal location if it exists
* @return string String of the deal location
*/
private function getDealLocation($dealMeta)
private function getDealLocation($jsonDealData)
{
$ribbons = $dealMeta['props']['metaRibbons'];
$isLocal = false;
foreach ($ribbons as $ribbon) {
$isLocal |= ($ribbon['type'] == 'local');
}
if ($isLocal) {
if ($jsonDealData['props']['thread']['isLocal']) {
$content = '<div>' . $this->i8n('deal-type') . ' : ' . $this->i8n('localdeal') . '</div>';
} else {
$content = '';
@ -424,8 +419,11 @@ HEREDOC;
private function getImage($deal)
{
// Get thread Image JSON content
$content = Json::decode($deal->find('div[class*=threadGrid-image]', 0)->find('div[class=js-vue2]', 0)->getAttribute('data-vue2'));
return '<img src="' . $content['props']['threadImageUrl'] . '"/>';
$content = Json::decode($deal->find('div[class=js-vue2]', 0)->getAttribute('data-vue2'));
//return '<img src="' . $content['props']['threadImageUrl'] . '"/>';
return '<img src="' . $this->i8n('image-host') . $content['props']['thread']['mainImage']['path'] . '/'
. $content['props']['thread']['mainImage']['name'] . '/re/202x202/qt/70/'
. $content['props']['thread']['mainImage']['uid'] . '"/>';
}
/**
@ -434,7 +432,7 @@ HEREDOC;
*/
private function getShipsFrom($dealMeta)
{
$metas = $dealMeta['props']['metaRibbons'];
$metas = $dealMeta['props']['metaRibbons'] ?? [];
$shipsFrom = null;
foreach ($metas as $meta) {
if ($meta['type'] == 'dispatched-from') {
@ -524,6 +522,7 @@ HEREDOC;
{
$group = $this->getInput('group');
$order = $this->getInput('order');
$subgroups = $this->getInput('subgroups');
// This permit to keep the existing Feed to work
if ($order == $this->i8n('context-hot')) {
@ -533,7 +532,7 @@ HEREDOC;
}
$url = $this->i8n('bridge-uri')
. $this->i8n('uri-group') . $group . '?sortBy=' . $sortBy;
. $this->i8n('uri-group') . $group . '?sortBy=' . $sortBy . '&groups=' . $subgroups;
return $url;
}

View File

@ -40,7 +40,7 @@ class RadioMelodieBridge extends BridgeAbstract
$picture = [];
// Get the Main picture URL
$picture[] = self::URI . $article->find('figure[class*=photoviewer]', 0)->find('img', 0)->src;
$picture[] = $article->find('figure[class*=photoviewer]', 0)->find('img', 0)->src;
$audioHTML = $article->find('audio');
// Add the audio element to the enclosure
@ -123,7 +123,7 @@ class RadioMelodieBridge extends BridgeAbstract
preg_match('/wavesurfer[0-9]+.load\(\'(.*)\'\)/m', $js->innertext, $urls);
// Create the plain HTML <audio> content to play this audio file
$content = '<audio style="width: 100%" src="' . $urls[1] . '" controls ></audio>';
$content = '<audio style="width: 100%" src="' . self::URI . $urls[1] . '" controls ></audio>';
// Replace the <script> tag by the <audio> tag
$js->outertext = $content;

View File

@ -35,7 +35,7 @@ class ReutersBridge extends BridgeAbstract
'title' => 'Feeds from Reuters U.S/International edition',
'values' => [
'Top News' => 'home/topnews',
'Fact Check' => 'chan:abtpk0vm',
'Fact Check' => '/fact-check',
'Entertainment' => 'chan:8ym8q8dl',
'Politics' => 'politics',
'Wire' => 'wire',
@ -137,7 +137,6 @@ class ReutersBridge extends BridgeAbstract
const OLD_WIRE_SECTION = [
'home/topnews',
'chan:abtpk0vm',
'chan:8ym8q8dl',
'politics',
'wire'

100
bridges/ShadertoyBridge.php Normal file
View File

@ -0,0 +1,100 @@
<?php
class ShadertoyBridge extends BridgeAbstract
{
const NAME = 'Shadertoy';
const URI = 'https://www.shadertoy.com';
const DESCRIPTION = 'Latest submissions on Shadertoy';
const MAINTAINER = 'thefranke';
const CACHE_TIMEOUT = 3600; // 1h
const PARAMETERS = [
[
'category' => [
'name' => 'category',
'type' => 'list',
'exampleValue' => 'Popular',
'title' => 'Select a category',
'values' => [
'Shaders of the Week' => 'sotw',
'Popular' => 'popular',
'Newest' => 'newest',
'Hot' => 'hot',
]
]
]
];
public function postprocessDescription($content)
{
// replace [url] tags
$pattern = '/\[\/?url.*?\]/';
$replace = '';
$content = preg_replace($pattern, $replace, $content);
// find URLs and turn then into hyperlinks
$pattern = '/(http|https|ftp|ftps)\:\/\/[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}(\/\S*)?/';
$replace = '<a href="$0">$0</a>';
$content = preg_replace($pattern, $replace, $content);
return $content;
}
public function collectData()
{
$category = $this->getInput('category');
$json = null;
if ($category == 'sotw') {
$url = static::URI . '/playlist/week';
$contents = getContents($url);
$shaderids = extractFromDelimiters($contents, 'var gShaderIDs = ', ';');
$shaderids = str_replace('\'', '"', $shaderids);
$url = static::URI . '/shadertoy';
$data = 's=' . rawurlencode('{ "shaders": ' . $shaderids . ' }') . '&nt=0&nl=0&np=0';
$header = [
'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:135.0) Gecko/20100101 Firefox/135.0',
'Content-Type: application/x-www-form-urlencoded',
'Accept: */*',
'Origin: https://www.shadertoy.com',
'Referer: https://www.shadertoy.com/playlist/week',
];
$opts = [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $data,
CURLOPT_RETURNTRANSFER => true
];
$json = getContents($url, $header, $opts);
} else {
$url = static::URI . '/results?sort=' . $category;
$contents = getContents($url);
$json = extractFromDelimiters($contents, 'var gShaders=', 'var gUseScreenshots');
$json = substr(trim($json), 0, -1);
}
$json = Json::decode($json);
if (!$json) {
throw new Exception(sprintf('Unable to find css selector on `%s`', static::URI));
}
foreach ($json as $article) {
$id = $article['info']['id'];
$title = $article['info']['name'];
$author = $article['info']['username'];
$uri = static::URI . '/view/' . $id;
$content = '<p><img src="' . static::URI . '/media/shaders/' . $id . '.jpg"></p><p>' . $this->postprocessDescription($article['info']['description']) . '</p>';
$timestamp = $article['info']['date'];
$this->items[] = [
'title' => $title,
'author' => $author,
'uri' => $uri,
'content' => $content,
'timestamp' => $timestamp,
];
}
}
}

View File

@ -57,6 +57,9 @@ class TldrTechBridge extends BridgeAbstract
continue;
}
$itemUrl = Url::fromString(self::URI . ltrim($child->href, '/'));
if ($itemUrl == $locationUrl) {
continue;
}
$this->extractItem($itemUrl);
if (count($this->items) >= $limit) {
break;
@ -125,6 +128,11 @@ class TldrTechBridge extends BridgeAbstract
}
}
}
foreach ($content->find('section') as $section) {
if (count($section->children()) == 0) {
$content->removeChild($section);
}
}
$title = $content->find('h2', 0);
return [$content->innertext, $title->plaintext];
}

View File

@ -0,0 +1,28 @@
<?php
class TomsToucheBridge extends BridgeAbstract
{
const NAME = 'Toms Touché';
const URI = 'https://taz.de/#!tom=tomdestages';
const DESCRIPTION = 'Your daily dose of Toms Touche.';
const MAINTAINER = 'latz';
const CACHE_TIMEOUT = 3600; // 1h
public function collectData()
{
$url = 'https://taz.de/';
$html = getSimpleHTMLDOM($url); // Docs: https://simplehtmldom.sourceforge.io/docs/1.9/index.html
$date = $html->find('p[x-ref]');
$date = trim($date[0]->innertext);
[$day, $month, $year] = explode('.', $date);
$image = $html->find('img[alt="tom des tages"]');
$item = [];
$item['title'] = "Toms Touché - $date";
$item['uri'] = 'https://taz.de/#!tom=tomdestages';
$item['timestamp'] = mktime(0, 0, 0, $month, $day, $year);
$item['content'] = $image[0] . '</img>'; // This isn't good HTML style, but at least syntactically correct
$item['uid'] = $image[0]->getAttribute('src');
$this->items[] = $item;
}
}

View File

@ -7,8 +7,8 @@ class VkBridge extends BridgeAbstract
// const MAINTAINER = 'ahiles3005';
const NAME = 'VK.com';
const URI = 'https://vk.com/';
const CACHE_TIMEOUT = 300; // 5min
const DESCRIPTION = 'Working with open pages';
const CACHE_TIMEOUT = 3600; // 1h
const DESCRIPTION = 'Does not work anymore';
const PARAMETERS = [
[
'u' => [
@ -65,6 +65,7 @@ class VkBridge extends BridgeAbstract
public function collectData()
{
return;
$text_html = $this->getContents();
$text_html = iconv('windows-1251', 'utf-8//ignore', $text_html);
@ -391,10 +392,13 @@ class VkBridge extends BridgeAbstract
$item['categories'] = $hashtags;
// get post link
$post_link = $post->find('a.PostHeaderSubtitle__link', 0)->getAttribute('href');
preg_match('/wall-?\d+_(\d+)/', $post_link, $preg_match_result);
$item['post_id'] = intval($preg_match_result[1]);
$item['uri'] = $post_link;
$var = $post->find('a.PostHeaderSubtitle__link', 0);
if ($var) {
$post_link = $var->getAttribute('href');
preg_match('/wall-?\d+_(\d+)/', $post_link, $preg_match_result);
$item['post_id'] = intval($preg_match_result[1]);
$item['uri'] = $post_link;
}
$item['timestamp'] = $this->getTime($post);
$item['title'] = $this->getTitle($item['content']);
$item['author'] = $post_author;
@ -402,7 +406,7 @@ class VkBridge extends BridgeAbstract
// do not append it now
$pinned_post_item = $item;
} else {
$last_post_id = $item['post_id'];
$last_post_id = $item['post_id'] ?? null;
$this->items[] = $item;
}
}
@ -474,7 +478,10 @@ class VkBridge extends BridgeAbstract
if ($accurateDateElement) {
return $accurateDateElement->getAttribute('time');
} else {
$strdate = $post->find('time.PostHeaderSubtitle__item', 0)->plaintext;
$strdate = $post->find('time.PostHeaderSubtitle__item', 0)->plaintext ?? null;
if (!$strdate) {
return 0;
}
$strdate = preg_replace('/[\x00-\x1F\x7F-\xFF]/', ' ', $strdate);
$date = date_parse($strdate);

View File

@ -0,0 +1,86 @@
<?php
class YouTubeFeedExpanderBridge extends FeedExpander
{
const NAME = 'YouTube Feed Expander';
const MAINTAINER = 'phantop';
const URI = 'https://www.youtube.com/';
const DESCRIPTION = 'Returns the latest videos from a YouTube channel';
const PARAMETERS = [[
'channel' => [
'name' => 'Channel ID',
'required' => true,
// Example: vinesauce
'exampleValue' => 'UCzORJV8l3FWY4cFO8ot-F2w',
],
'embed' => [
'name' => 'Add embed to entry',
'type' => 'checkbox',
'required' => false,
'title' => 'Add embed to entry',
'defaultValue' => 'checked',
],
'embedurl' => [
'name' => 'Use embed page as entry url',
'type' => 'checkbox',
'required' => false,
'title' => 'Use embed page as entry url',
],
'nocookie' => [
'name' => 'Use nocookie embed page',
'type' => 'checkbox',
'required' => false,
'title' => 'Use nocookie embed page'
],
]];
public function getIcon()
{
if ($this->getInput('channel') != null) {
$html = getSimpleHTMLDOMCached($this->getURI());
$scriptRegex = '/var ytInitialData = (.*?);<\/script>/';
$result = preg_match($scriptRegex, $html, $matches);
if (isset($matches[1])) {
$json = json_decode($matches[1]);
return $json->metadata->channelMetadataRenderer->avatar->thumbnails[0]->url;
}
}
return parent::getIcon();
}
public function collectData()
{
$url = 'https://www.youtube.com/feeds/videos.xml?channel_id=' . $this->getInput('channel');
$this->collectExpandableDatas($url);
}
protected function parseItem(array $item)
{
$id = $item['yt']['videoId'];
$item['comments'] = $item['uri'] . '#comments';
$item['uid'] = $item['id'];
$thumbnail = sprintf('https://img.youtube.com/vi/%s/maxresdefault.jpg', $id);
$item['enclosures'] = [$thumbnail];
$item['content'] = $item['media']['group']['description'];
$item['content'] = str_replace("\n", '<br>', $item['content']);
unset($item['media']);
$embedURI = self::URI;
if ($this->getInput('nocookie')) {
$embedURI = 'https://www.youtube-nocookie.com/';
}
$embed = $embedURI . 'embed/' . $id;
if ($this->getInput('embed')) {
$iframe_fmt = '<iframe width="448" height="350" src="%s" title="%s" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>'; //phpcs:ignore
$iframe = sprintf($iframe_fmt, $embed, $item['title']) . '<br>';
$item['content'] = $iframe . $item['content'];
}
if ($this->getInput('embedurl')) {
$item['uri'] = $embed;
}
return $item;
}
}

View File

@ -75,7 +75,7 @@ custom_timeout = false
; "" = Disabled (default)
email = ""
; Advertise a contact Telegram url e.g. "https://t.me/elegantobjects"
; Advertise a contact URL (can be any URL!) e.g. "https://t.me/elegantobjects"
telegram = ""
; Show Donation information for bridges if available.

View File

@ -327,7 +327,7 @@ abstract class BridgeAbstract
return $this->cache->get($this->getShortName() . '_' . $key, $default);
}
protected function saveCacheValue(string $key, $value, int $ttl = null)
protected function saveCacheValue(string $key, $value, int $ttl = 86400)
{
$this->cache->set($this->getShortName() . '_' . $key, $value, $ttl);
}

View File

@ -177,11 +177,9 @@ function getSimpleHTMLDOM(
}
/**
* Gets contents from the Internet as simplhtmldom object. Contents are cached
* Fetch contents from the Internet as simplhtmldom object. Contents are cached
* and re-used for subsequent calls until the cache duration elapsed.
*
* _Notice_: Cached contents are forcefully removed after 24 hours (86400 seconds).
*
* @param string $url The URL.
* @param int $ttl Cache duration in seconds.
* @param array $header (optional) A list of cURL header.

View File

@ -52,7 +52,7 @@
<?php if ($admin_telegram): ?>
<div>
Telegram: <a href="<?= e($admin_telegram) ?>"><?= e($admin_telegram) ?></a>
Url: <a href="<?= e($admin_telegram) ?>"><?= e($admin_telegram) ?></a>
</div>
<?php endif; ?>