mirror of
https://github.com/RSS-Bridge/rss-bridge.git
synced 2025-04-04 16:49:35 +00:00
Compare commits
79 Commits
2025-01-02
...
master
Author | SHA1 | Date | |
---|---|---|---|
|
d6a9da1cc8 | ||
|
85962e18d3 | ||
|
a19b63e840 | ||
|
5365b57638 | ||
|
462c005f2c | ||
|
db42f2786c | ||
|
26a4c255d3 | ||
|
3055e69c23 | ||
|
7c1e01b45a | ||
|
4d8a46d46e | ||
|
9d6aa5ee38 | ||
|
1c45eff505 | ||
|
68ff39e164 | ||
|
abb1602524 | ||
|
87112497de | ||
|
38bb5115c9 | ||
|
23cb9349fc | ||
|
05a9ac0f06 | ||
|
91fe6c1fae | ||
|
7260f28e10 | ||
|
87ab1e4513 | ||
|
dee734d360 | ||
|
744f996224 | ||
|
f270cd35e7 | ||
|
83c36a87e2 | ||
|
810e17b556 | ||
|
97f07cf216 | ||
|
62fafdc24b | ||
|
cd4cdcfd65 | ||
|
00a24e2f69 | ||
|
92b5e7093f | ||
|
b52f01505d | ||
|
e4c32bb046 | ||
|
dd4dcfa59c | ||
|
4e678c955f | ||
|
549bed64d2 | ||
|
94924d8e16 | ||
|
920b21b1fd | ||
|
935075072b | ||
|
3ae7a10223 | ||
|
bf431a6eae | ||
|
824ac5e373 | ||
|
ae8394d976 | ||
|
4da61b7922 | ||
|
8b1ba003a8 | ||
|
230edf602e | ||
|
bd7d1734c3 | ||
|
dd8bc077ed | ||
|
952a2d99a3 | ||
|
58b3cfb158 | ||
|
028acd0af1 | ||
|
2a58f82bd8 | ||
|
5214581386 | ||
|
eadea242a7 | ||
|
1a2c1f5bba | ||
|
776a1f47f3 | ||
|
39ecd63f72 | ||
|
0e2655fc8a | ||
|
e355276378 | ||
|
cb65125dbd | ||
|
1d02214e12 | ||
|
48cb7d71ed | ||
|
f9e9c8101e | ||
|
97f7df0d06 | ||
|
db3899f2e6 | ||
|
d36cd0a332 | ||
|
662e0bfa95 | ||
|
3fc38c15a3 | ||
|
be51ba17df | ||
|
c44a76ff17 | ||
|
7c6d4a932c | ||
|
45ee018a6e | ||
|
e825272987 | ||
|
97eebfb562 | ||
|
2a44a006b2 | ||
|
974f00cd6a | ||
|
4b4d622333 | ||
|
b4a63e7040 | ||
|
7d544f1fab |
4
.github/ISSUE_TEMPLATE/bridge-request.md
vendored
4
.github/ISSUE_TEMPLATE/bridge-request.md
vendored
@ -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!-->
|
||||
|
||||
|
@ -15,7 +15,7 @@
|
||||
* [Astalaseven](https://github.com/Astalaseven)
|
||||
* [Astyan-42](https://github.com/Astyan-42)
|
||||
* [austinhuang0131](https://github.com/austinhuang0131)
|
||||
* [AxorPL](https://github.com/AxorPL)
|
||||
* [axor-mst](https://github.com/axor-mst)
|
||||
* [ayacoo](https://github.com/ayacoo)
|
||||
* [az5he6ch](https://github.com/az5he6ch)
|
||||
* [b1nj](https://github.com/b1nj)
|
||||
@ -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)
|
||||
|
37
README.md
37
README.md
@ -29,7 +29,7 @@ Requires minimum PHP 7.4.
|
||||
|||
|
||||
|||
|
||||
|
||||
## A subset of bridges (16/447)
|
||||
## A subset of bridges (15/447)
|
||||
|
||||
* `CssSelectorBridge`: [Scrape out a feed using CSS selectors](https://rss-bridge.org/bridge01/#bridge-CssSelectorBridge)
|
||||
* `FeedMergeBridge`: [Combine multiple feeds into one](https://rss-bridge.org/bridge01/#bridge-FeedMergeBridge)
|
||||
@ -44,7 +44,6 @@ Requires minimum PHP 7.4.
|
||||
* `ThePirateBayBridge:` [Fetches torrents by search/user/category](https://rss-bridge.org/bridge01/#bridge-ThePirateBayBridge)
|
||||
* `TikTokBridge`: [Fetches posts by username](https://rss-bridge.org/bridge01/#bridge-TikTokBridge)
|
||||
* `TwitchBridge`: [Fetches videos from channel](https://rss-bridge.org/bridge01/#bridge-TwitchBridge)
|
||||
* `VkBridge`: [Fetches posts from user/group](https://rss-bridge.org/bridge01/#bridge-VkBridge)
|
||||
* `XPathBridge`: [Scrape out a feed using XPath expressions](https://rss-bridge.org/bridge01/#bridge-XPathBridge)
|
||||
* `YoutubeBridge`: [Fetches videos by username/channel/playlist/search](https://rss-bridge.org/bridge01/#bridge-YoutubeBridge)
|
||||
* `YouTubeCommunityTabBridge`: [Fetches posts from a channel's community tab](https://rss-bridge.org/bridge01/#bridge-YouTubeCommunityTabBridge)
|
||||
@ -72,27 +71,27 @@ useradd --shell /bin/bash --create-home rss-bridge
|
||||
|
||||
cd /var/www
|
||||
|
||||
# Create folder and change ownership
|
||||
# Create folder and change its ownership to rss-bridge
|
||||
mkdir rss-bridge && chown rss-bridge:rss-bridge rss-bridge/
|
||||
|
||||
# Become user
|
||||
# Become rss-bridge
|
||||
su rss-bridge
|
||||
|
||||
# Fetch latest master
|
||||
# Clone master branch into existing folder
|
||||
git clone https://github.com/RSS-Bridge/rss-bridge.git rss-bridge/
|
||||
cd rss-bridge
|
||||
|
||||
# Copy over the default config
|
||||
# Copy over the default config (OPTIONAL)
|
||||
cp -v config.default.ini.php config.ini.php
|
||||
|
||||
# Give full permissions only to owner (rss-bridge)
|
||||
chmod 700 -R ./
|
||||
# Recursively give full permissions to user/owner
|
||||
chmod 700 --recursive ./
|
||||
|
||||
# Give read and execute to others (nginx and php-fpm)
|
||||
# Give read and execute to others on folder ./static
|
||||
chmod o+rx ./ ./static
|
||||
|
||||
# Give read to others (nginx)
|
||||
chmod o+r -R ./static
|
||||
# Recursively give give read to others on folder ./static
|
||||
chmod o+r --recursive ./static
|
||||
```
|
||||
|
||||
Nginx config:
|
||||
@ -110,17 +109,14 @@ server {
|
||||
error_log /var/log/nginx/rss-bridge.error.log;
|
||||
log_not_found off;
|
||||
|
||||
# Intentionally not setting a root folder here
|
||||
|
||||
# autoindex is off by default but feels good to explicitly turn off
|
||||
autoindex off;
|
||||
# Intentionally not setting a root folder
|
||||
|
||||
# Static content only served here
|
||||
location /static/ {
|
||||
alias /var/www/rss-bridge/static/;
|
||||
}
|
||||
|
||||
# Pass off to php-fpm when location is exactly /
|
||||
# Pass off to php-fpm only when location is EXACTLY == /
|
||||
location = / {
|
||||
root /var/www/rss-bridge/;
|
||||
include snippets/fastcgi-php.conf;
|
||||
@ -128,12 +124,12 @@ server {
|
||||
fastcgi_pass unix:/run/php/rss-bridge.sock;
|
||||
}
|
||||
|
||||
# Reduce spam
|
||||
# Reduce log noise
|
||||
location = /favicon.ico {
|
||||
access_log off;
|
||||
}
|
||||
|
||||
# Reduce spam
|
||||
# Reduce log noise
|
||||
location = /robots.txt {
|
||||
access_log off;
|
||||
}
|
||||
@ -154,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
|
||||
```
|
||||
|
||||
@ -464,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!
|
||||
|
@ -23,7 +23,7 @@ class DisplayAction implements ActionInterface
|
||||
$noproxy = $request->get('_noproxy');
|
||||
|
||||
if (!$bridgeName) {
|
||||
return new Response(render(__DIR__ . '/../templates/error.html.php', ['message' => 'Missing bridge parameter']), 400);
|
||||
return new Response(render(__DIR__ . '/../templates/error.html.php', ['message' => 'Missing bridge name parameter']), 400);
|
||||
}
|
||||
$bridgeClassName = $this->bridgeFactory->createBridgeClassName($bridgeName);
|
||||
if (!$bridgeClassName) {
|
||||
|
@ -12,7 +12,7 @@ final class FrontpageAction implements ActionInterface
|
||||
|
||||
public function __invoke(Request $request): Response
|
||||
{
|
||||
$token = $request->attribute('token');
|
||||
$token = $request->getAttribute('token');
|
||||
|
||||
$messages = [];
|
||||
$activeBridges = 0;
|
||||
|
@ -27,6 +27,13 @@ class AO3Bridge extends BridgeAbstract
|
||||
'Entire work' => 'all',
|
||||
],
|
||||
],
|
||||
'unique' => [
|
||||
'name' => 'Make separate entries for new fic chapters',
|
||||
'type' => 'checkbox',
|
||||
'required' => false,
|
||||
'title' => 'Make separate entries for new fic chapters',
|
||||
'defaultValue' => 'checked',
|
||||
],
|
||||
'limit' => self::LIMIT,
|
||||
],
|
||||
'Bookmarks' => [
|
||||
@ -118,7 +125,12 @@ class AO3Bridge extends BridgeAbstract
|
||||
$chapters = $element->find('dl dd.chapters', 0);
|
||||
// bookmarked series and external works do not have a chapters count
|
||||
$chapters = (isset($chapters) ? $chapters->plaintext : 0);
|
||||
$item['uid'] = $item['uri'] . "/$strdate/$chapters";
|
||||
if ($this->getInput('unique')) {
|
||||
$item['uid'] = $item['uri'] . "/$strdate/$chapters";
|
||||
} else {
|
||||
$item['uid'] = $item['uri'];
|
||||
}
|
||||
|
||||
|
||||
// Fetch workskin of desired chapter(s) in list
|
||||
if ($this->getInput('range') && ($limit == 0 || $count++ < $limit)) {
|
||||
|
@ -32,8 +32,7 @@ class AirBreizhBridge extends BridgeAbstract
|
||||
public function collectData()
|
||||
{
|
||||
$html = '';
|
||||
$html = getSimpleHTMLDOM(static::URI . 'publications/?fwp_publications_thematiques=' . $this->getInput('theme'))
|
||||
or returnClientError('No results for this query.');
|
||||
$html = getSimpleHTMLDOM(static::URI . 'publications/?fwp_publications_thematiques=' . $this->getInput('theme'));
|
||||
|
||||
foreach ($html->find('article') as $article) {
|
||||
$item = [];
|
||||
|
@ -146,7 +146,7 @@ EOT;
|
||||
{
|
||||
$uri = $this->getURI();
|
||||
|
||||
return getSimpleHTMLDOM($uri) ?: returnServerError('Could not request Amazon.');
|
||||
return getSimpleHTMLDOM($uri);
|
||||
}
|
||||
|
||||
private function scrapePriceFromMetrics($html)
|
||||
|
@ -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"
|
||||
|
@ -105,8 +105,7 @@ class AssociatedPressNewsBridge extends BridgeAbstract
|
||||
|
||||
private function collectCardData()
|
||||
{
|
||||
$json = getContents($this->getTagURI())
|
||||
or returnServerError('Could not request: ' . $this->getTagURI());
|
||||
$json = getContents($this->getTagURI());
|
||||
|
||||
$tagContents = json_decode($json, true);
|
||||
|
||||
|
344
bridges/AuctionetBridge.php
Normal file
344
bridges/AuctionetBridge.php
Normal 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;
|
||||
}
|
||||
}
|
@ -29,7 +29,7 @@ class BAEBridge extends BridgeAbstract
|
||||
public function collectData()
|
||||
{
|
||||
$url = $this->getURI();
|
||||
$html = getSimpleHTMLDOM($url) or returnClientError('No results for this query.');
|
||||
$html = getSimpleHTMLDOM($url);
|
||||
|
||||
$annonces = $html->find('main article');
|
||||
foreach ($annonces as $annonce) {
|
||||
|
@ -93,8 +93,7 @@ class BandcampDailyBridge extends BridgeAbstract
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
$html = getSimpleHTMLDOM($this->getURI())
|
||||
or returnServerError('Could not request: ' . $this->getURI());
|
||||
$html = getSimpleHTMLDOM($this->getURI());
|
||||
|
||||
$html = defaultLinkTo($html, self::URI);
|
||||
|
||||
@ -105,8 +104,7 @@ class BandcampDailyBridge extends BridgeAbstract
|
||||
|
||||
$articlePath = $article->find('a.title', 0)->href;
|
||||
|
||||
$articlePageHtml = getSimpleHTMLDOMCached($articlePath, 3600)
|
||||
or returnServerError('Could not request: ' . $articlePath);
|
||||
$articlePageHtml = getSimpleHTMLDOMCached($articlePath, 3600);
|
||||
|
||||
$item['uri'] = $articlePath;
|
||||
$item['title'] = $articlePageHtml->find('article-title', 0)->innertext;
|
||||
|
139
bridges/BazarakiBridge.php
Normal file
139
bridges/BazarakiBridge.php
Normal file
@ -0,0 +1,139 @@
|
||||
<?php
|
||||
|
||||
class BazarakiBridge extends BridgeAbstract
|
||||
{
|
||||
const NAME = 'Bazaraki Bridge';
|
||||
const URI = 'https://bazaraki.com';
|
||||
const DESCRIPTION = 'Fetch adverts from Bazaraki, a Cyprus-based classifieds website.';
|
||||
const MAINTAINER = 'danwain';
|
||||
const PARAMETERS = [
|
||||
[
|
||||
'url' => [
|
||||
'name' => 'URL',
|
||||
'type' => 'text',
|
||||
'required' => true,
|
||||
'title' => 'Enter the URL of the Bazaraki page to fetch adverts from.',
|
||||
'exampleValue' => 'https://www.bazaraki.com/real-estate-for-sale/houses/?lat=0&lng=0&radius=100000',
|
||||
],
|
||||
'limit' => [
|
||||
'name' => 'Limit',
|
||||
'type' => 'number',
|
||||
'required' => false,
|
||||
'title' => 'Enter the number of adverts to fetch. (max 50)',
|
||||
'exampleValue' => '10',
|
||||
'defaultValue' => 10,
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
$url = $this->getInput('url');
|
||||
if (! str_starts_with($url, 'https://www.bazaraki.com/')) {
|
||||
throw new \Exception('Nope');
|
||||
}
|
||||
|
||||
$html = getSimpleHTMLDOM($url);
|
||||
|
||||
$i = 0;
|
||||
foreach ($html->find('div.advert') as $element) {
|
||||
$i++;
|
||||
if ($i > $this->getInput('limit') || $i > 50) {
|
||||
break;
|
||||
}
|
||||
|
||||
$item = [];
|
||||
|
||||
$item['uri'] = 'https://www.bazaraki.com' . $element->find('a.advert__content-title', 0)->href;
|
||||
|
||||
# Get the content
|
||||
$advert = getSimpleHTMLDOM($item['uri']);
|
||||
|
||||
$price = trim($advert->find('div.announcement-price__cost', 0)->plaintext);
|
||||
$name = trim($element->find('a.advert__content-title', 0)->plaintext);
|
||||
|
||||
$item['title'] = $name . ' - ' . $price;
|
||||
|
||||
$time = trim($advert->find('span.date-meta', 0)->plaintext);
|
||||
$time = str_replace('Posted: ', '', $time);
|
||||
|
||||
|
||||
$item['content'] = $this->processAdvertContent($advert);
|
||||
$item['timestamp'] = $this->convertRelativeTime($time);
|
||||
$item['author'] = trim($advert->find('div.author-name', 0)->plaintext);
|
||||
$item['uid'] = $advert->find('span.number-announcement', 0)->plaintext;
|
||||
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the advert content to clean up HTML
|
||||
*
|
||||
* @param simple_html_dom $advert The SimpleHTMLDOM object for the advert page
|
||||
* @return string Processed HTML content
|
||||
*/
|
||||
private function processAdvertContent($advert)
|
||||
{
|
||||
// Get the content sections
|
||||
$header = $advert->find('div.announcement-content-header', 0);
|
||||
$characteristics = $advert->find('div.announcement-characteristics', 0);
|
||||
$description = $advert->find('div.js-description', 0);
|
||||
$images = $advert->find('div.announcement__images', 0);
|
||||
|
||||
// Remove all favorites divs
|
||||
foreach ($advert->find('div.announcement-meta__favorites') as $favorites) {
|
||||
$favorites->outertext = '';
|
||||
}
|
||||
|
||||
// Replace all <a> tags with their text content
|
||||
foreach ($advert->find('a') as $a) {
|
||||
$a->outertext = $a->innertext;
|
||||
}
|
||||
|
||||
// Format the content with section headers and dividers
|
||||
$formattedContent = '';
|
||||
|
||||
// Add header section
|
||||
$formattedContent .= $header->innertext;
|
||||
$formattedContent .= '<hr/>';
|
||||
|
||||
// Add characteristics section with header
|
||||
$formattedContent .= '<h3>Details</h3>';
|
||||
$formattedContent .= $characteristics->innertext;
|
||||
$formattedContent .= '<hr/>';
|
||||
|
||||
// Add description section with header
|
||||
$formattedContent .= '<h3>Description</h3>';
|
||||
$formattedContent .= $description->innertext;
|
||||
$formattedContent .= '<hr/>';
|
||||
|
||||
// Add images section with header
|
||||
$formattedContent .= '<h3>Images</h3>';
|
||||
$formattedContent .= $images->innertext;
|
||||
|
||||
return $formattedContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert relative time strings like "Yesterday 12:32" to proper timestamps
|
||||
*
|
||||
* @param string $timeString The relative time string from the website
|
||||
* @return string Timestamp in a format compatible with strtotime()
|
||||
*/
|
||||
private function convertRelativeTime($timeString)
|
||||
{
|
||||
if (strpos($timeString, 'Yesterday') !== false) {
|
||||
// Replace "Yesterday" with actual date
|
||||
$time = str_replace('Yesterday', date('Y-m-d', strtotime('-1 day')), $timeString);
|
||||
return date('Y-m-d H:i:s', strtotime($time));
|
||||
} elseif (strpos($timeString, 'Today') !== false) {
|
||||
// Replace "Today" with actual date
|
||||
$time = str_replace('Today', date('Y-m-d'), $timeString);
|
||||
return date('Y-m-d H:i:s', strtotime($time));
|
||||
} else {
|
||||
// For other formats, return as is and let strtotime handle it
|
||||
return $timeString;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
class BlizzardNewsBridge extends XPathAbstract
|
||||
class BlizzardNewsBridge extends BridgeAbstract
|
||||
{
|
||||
const NAME = 'Blizzard News';
|
||||
const URI = 'https://news.blizzard.com';
|
||||
@ -35,33 +35,73 @@ class BlizzardNewsBridge extends XPathAbstract
|
||||
];
|
||||
const CACHE_TIMEOUT = 3600;
|
||||
|
||||
const XPATH_EXPRESSION_ITEM = '/html/body/div/div[4]/div[2]/div[2]/div/div/section/ol/li/article';
|
||||
const XPATH_EXPRESSION_ITEM_TITLE = './/div/div[2]/h2';
|
||||
const XPATH_EXPRESSION_ITEM_CONTENT = './/div[@class="ArticleListItem-description"]/div[@class="h6"]/text()';
|
||||
const XPATH_EXPRESSION_ITEM_URI = './/a[@class="ArticleLink ArticleLink"]/@href';
|
||||
const XPATH_EXPRESSION_ITEM_AUTHOR = '';
|
||||
const XPATH_EXPRESSION_ITEM_TIMESTAMP = './/time[@class="ArticleListItem-footerTimestamp"]/@timestamp';
|
||||
const XPATH_EXPRESSION_ITEM_ENCLOSURES = './/div[@class="ArticleListItem-image"]/@style';
|
||||
const XPATH_EXPRESSION_ITEM_CATEGORIES = './/div[@class="ArticleListItem-label"]';
|
||||
const SETTING_FIX_ENCODING = true;
|
||||
private const PRODUCT_IDS = [
|
||||
'blt525c436e4a1b0a97',
|
||||
'blt54fbd3787a705054',
|
||||
'blt2031aef34200656d',
|
||||
'blt795c314400d7ded9',
|
||||
'blt5cfc6affa3ca0638',
|
||||
'blt2e50e1521bb84dc6',
|
||||
'blt376fb94931906b6f',
|
||||
'blt81d46fcb05ab8811',
|
||||
'bltede2389c0a8885aa',
|
||||
'blt24859ba8086fb294',
|
||||
'blte27d02816a8ff3e1',
|
||||
'blt2caca37e42f19839',
|
||||
'blt90855744d00cd378',
|
||||
'bltec70ad0ea4fd6d1d',
|
||||
'blt500c1f8b5470bfdb'
|
||||
];
|
||||
|
||||
private const API_PATH = '/api/news/blizzard?';
|
||||
|
||||
/**
|
||||
* Source Web page URL (should provide either HTML or XML content)
|
||||
* @return string
|
||||
*/
|
||||
protected function getSourceUrl()
|
||||
private function getSourceUrl(): string
|
||||
{
|
||||
$locale = $this->getInput('locale');
|
||||
if ('zh-cn' === $locale) {
|
||||
return 'https://cn.news.blizzard.com';
|
||||
$baseUrl = 'https://cn.news.blizzard.com' . self::API_PATH;
|
||||
} else {
|
||||
$baseUrl = 'https://news.blizzard.com/' . $locale . self::API_PATH;
|
||||
}
|
||||
return 'https://news.blizzard.com/' . $locale;
|
||||
return $baseUrl .= http_build_query([
|
||||
'feedCxpProductIds' => self::PRODUCT_IDS
|
||||
]);
|
||||
}
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
$feedContent = json_decode(getContents($this->getSourceUrl()), true);
|
||||
|
||||
foreach ($feedContent['feed']['contentItems'] as $entry) {
|
||||
$properties = $entry['properties'];
|
||||
|
||||
$item = [];
|
||||
|
||||
$item['title'] = $this->filterChars($properties['title']);
|
||||
$item['content'] = $this->filterChars($properties['summary']);
|
||||
$item['uri'] = $properties['newsUrl'];
|
||||
$item['author'] = $this->filterChars($properties['author']);
|
||||
$item['timestamp'] = strtotime($properties['lastUpdated']);
|
||||
$item['enclosures'] = [$properties['staticAsset']['imageUrl']];
|
||||
$item['categories'] = [$this->filterChars($properties['cxpProduct']['title'])];
|
||||
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
private function filterChars($content)
|
||||
{
|
||||
return htmlspecialchars($content, ENT_XML1);
|
||||
}
|
||||
|
||||
public function getIcon()
|
||||
{
|
||||
return <<<icon
|
||||
https://blznews.akamaized.net/images/favicon-cb34a003c6f2f637ee8f4f7b406f3b9b120b918c04cabec7f03a760e708977ea9689a1c638f4396def8dce7b202cd007eae91946cc3c4a578aa8b5694226cfc6.ico
|
||||
https://dfbmfbnnydoln.cloudfront.net/production/images/favicons/favicon.ba01bb119359d74970b02902472fd82e96b5aba7.ico
|
||||
icon;
|
||||
}
|
||||
}
|
||||
|
@ -2,10 +2,12 @@
|
||||
|
||||
class BlueskyBridge extends BridgeAbstract
|
||||
{
|
||||
const NAME = 'Bluesky';
|
||||
//Initial PR by [RSSBridge contributors](https://github.com/RSS-Bridge/rss-bridge/issues/4058).
|
||||
//Modified from [©DIYgod and contributors at RSSHub](https://github.com/DIYgod/RSSHub/tree/master/lib/routes/bsky), MIT License';
|
||||
const NAME = 'Bluesky Bridge';
|
||||
const URI = 'https://bsky.app';
|
||||
const DESCRIPTION = 'Fetches posts from Bluesky';
|
||||
const MAINTAINER = 'Code modified from rsshub (TonyRL https://github.com/TonyRL) and expanded';
|
||||
const MAINTAINER = 'mruac';
|
||||
const PARAMETERS = [
|
||||
[
|
||||
'data_source' => [
|
||||
@ -17,24 +19,39 @@ class BlueskyBridge extends BridgeAbstract
|
||||
],
|
||||
'title' => 'Select the type of data source to fetch from Bluesky.'
|
||||
],
|
||||
'handle' => [
|
||||
'name' => 'User Handle',
|
||||
'user_id' => [
|
||||
'name' => 'User Handle or DID',
|
||||
'type' => 'text',
|
||||
'required' => true,
|
||||
'exampleValue' => 'jackdodo.bsky.social',
|
||||
'title' => 'Handle found in URL'
|
||||
'exampleValue' => 'did:plc:z72i7hdynmk6r22z27h6tvur',
|
||||
'title' => 'ATProto / Bsky.app handle or DID'
|
||||
],
|
||||
'filter' => [
|
||||
'name' => 'Filter',
|
||||
'feed_filter' => [
|
||||
'name' => 'Feed type',
|
||||
'type' => 'list',
|
||||
'defaultValue' => 'posts_and_author_threads',
|
||||
'values' => [
|
||||
'posts_and_author_threads' => 'posts_and_author_threads',
|
||||
'posts_with_replies' => 'posts_with_replies',
|
||||
'posts_no_replies' => 'posts_no_replies',
|
||||
'posts_with_media' => 'posts_with_media',
|
||||
],
|
||||
'title' => 'Combinations of post/repost types to include in response.'
|
||||
'Posts feed' => 'posts_and_author_threads',
|
||||
'All posts and replies' => 'posts_with_replies',
|
||||
'Root posts only' => 'posts_no_replies',
|
||||
'Media only' => 'posts_with_media',
|
||||
]
|
||||
],
|
||||
|
||||
'include_reposts' => [
|
||||
'name' => 'Include Reposts?',
|
||||
'type' => 'checkbox',
|
||||
'defaultValue' => 'checked'
|
||||
],
|
||||
|
||||
'include_reply_context' => [
|
||||
'name' => 'Include Reply context?',
|
||||
'type' => 'checkbox'
|
||||
],
|
||||
|
||||
'verbose_title' => [
|
||||
'name' => 'Use verbose feed item titles?',
|
||||
'type' => 'checkbox'
|
||||
]
|
||||
]
|
||||
];
|
||||
@ -44,7 +61,11 @@ class BlueskyBridge extends BridgeAbstract
|
||||
public function getName()
|
||||
{
|
||||
if (isset($this->profile)) {
|
||||
return sprintf('%s (@%s) - Bluesky', $this->profile['displayName'], $this->profile['handle']);
|
||||
if ($this->profile['handle'] === 'handle.invalid') {
|
||||
return sprintf('Bluesky - %s', $this->profile['displayName']);
|
||||
} else {
|
||||
return sprintf('Bluesky - %s (@%s)', $this->profile['displayName'], $this->profile['handle']);
|
||||
}
|
||||
}
|
||||
return parent::getName();
|
||||
}
|
||||
@ -52,7 +73,11 @@ class BlueskyBridge extends BridgeAbstract
|
||||
public function getURI()
|
||||
{
|
||||
if (isset($this->profile)) {
|
||||
return self::URI . '/profile/' . $this->profile['handle'];
|
||||
if ($this->profile['handle'] === 'handle.invalid') {
|
||||
return self::URI . '/profile/' . $this->profile['did'];
|
||||
} else {
|
||||
return self::URI . '/profile/' . $this->profile['handle'];
|
||||
}
|
||||
}
|
||||
return parent::getURI();
|
||||
}
|
||||
@ -77,118 +102,385 @@ class BlueskyBridge extends BridgeAbstract
|
||||
{
|
||||
$description = '';
|
||||
$externalUri = $external['uri'];
|
||||
$externalTitle = htmlspecialchars($external['title'], ENT_QUOTES, 'UTF-8');
|
||||
$externalDescription = htmlspecialchars($external['description'], ENT_QUOTES, 'UTF-8');
|
||||
$externalTitle = e($external['title']);
|
||||
$externalDescription = e($external['description']);
|
||||
$thumb = $external['thumb'] ?? null;
|
||||
|
||||
if (preg_match('/youtube\.com\/watch\?v=([^\&\?\/]+)/', $externalUri, $id) || preg_match('/youtu\.be\/([^\&\?\/]+)/', $externalUri, $id)) {
|
||||
$videoId = $id[1];
|
||||
$description .= "<p>External Link: <a href=\"$externalUri\">$externalTitle</a></p>";
|
||||
$description .= "<iframe width=\"560\" height=\"315\" src=\"https://www.youtube.com/embed/$videoId\" frameborder=\"0\" allowfullscreen></iframe>";
|
||||
if (preg_match('/http(|s):\/\/media\.tenor\.com/', $externalUri)) {
|
||||
//tenor gif embed
|
||||
$tenorInterstitial = str_replace('media.tenor.com', 'media1.tenor.com/m', $externalUri);
|
||||
$description .= "<figure><a href=\"$tenorInterstitial\"><img src=\"$externalUri\"/></a><figcaption>$externalTitle</figcaption></figure>";
|
||||
} else {
|
||||
$description .= "<p>External Link: <a href=\"$externalUri\">$externalTitle</a></p>";
|
||||
$description .= "<p>$externalDescription</p>";
|
||||
|
||||
if ($thumb) {
|
||||
$thumbUrl = 'https://cdn.bsky.app/img/feed_thumbnail/plain/' . $did . '/' . $thumb['ref']['$link'] . '@jpeg';
|
||||
$description .= "<p><a href=\"$externalUri\"><img src=\"$thumbUrl\" alt=\"External Thumbnail\" /></a></p>";
|
||||
}
|
||||
//link embed preview
|
||||
$host = parse_url($externalUri)['host'];
|
||||
$thumbDesc = $thumb ? ('<img src="https://cdn.bsky.app/img/feed_thumbnail/plain/' . $did . '/' . $thumb['ref']['$link'] . '@jpeg"/>') : '';
|
||||
$externalDescription = strlen($externalDescription) > 0 ? "<figcaption>($host) $externalDescription</figcaption>" : '';
|
||||
$description .= '<br><blockquote><b><a href="' . $externalUri . '">' . $externalTitle . '</a></b>';
|
||||
$description .= '<figure>' . $thumbDesc . $externalDescription . '</figure></blockquote>';
|
||||
}
|
||||
return $description;
|
||||
}
|
||||
|
||||
private function textToDescription($text)
|
||||
private function textToDescription($record)
|
||||
{
|
||||
$text = nl2br(htmlspecialchars($text, ENT_QUOTES, 'UTF-8'));
|
||||
$text = preg_replace('/(https?:\/\/[^\s]+)/i', '<a href="$1">$1</a>', $text);
|
||||
|
||||
if (isset($record['value'])) {
|
||||
$record = $record['value'];
|
||||
}
|
||||
$text = $record['text'];
|
||||
$text_copy = $text;
|
||||
$text = nl2br(e($text));
|
||||
if (isset($record['facets'])) {
|
||||
$facets = $record['facets'];
|
||||
foreach ($facets as $facet) {
|
||||
if ($facet['features'][0]['$type'] === 'app.bsky.richtext.facet#link') {
|
||||
$substring = substr($text_copy, $facet['index']['byteStart'], $facet['index']['byteEnd'] - $facet['index']['byteStart']);
|
||||
$text = str_replace($substring, '<a href="' . $facet['features'][0]['uri'] . '">' . $substring . '</a>', $text);
|
||||
}
|
||||
}
|
||||
}
|
||||
return $text;
|
||||
}
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
$handle = $this->getInput('handle');
|
||||
$filter = $this->getInput('filter') ?: 'posts_and_author_threads';
|
||||
$user_id = $this->getInput('user_id');
|
||||
$handle_match = preg_match('/(?:[a-zA-Z]*\.)+([a-zA-Z](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)/', $user_id, $handle_res); //gets the TLD in $handle_match[1]
|
||||
$did_match = preg_match('/did:plc:[a-z2-7]{24}/', $user_id); //https://github.com/did-method-plc/did-method-plc#identifier-syntax
|
||||
$exclude = ['alt', 'arpa', 'example', 'internal', 'invalid', 'local', 'localhost', 'onion']; //https://en.wikipedia.org/wiki/Top-level_domain#Reserved_domains
|
||||
if ($handle_match == true && array_search($handle_res[1], $exclude) == false) {
|
||||
//valid bsky handle
|
||||
$did = $this->resolveHandle($user_id);
|
||||
} elseif ($did_match == true) {
|
||||
//valid DID
|
||||
$did = $user_id;
|
||||
} else {
|
||||
returnClientError('Invalid ATproto handle or DID provided.');
|
||||
}
|
||||
|
||||
$filter = $this->getInput('feed_filter') ?: 'posts_and_author_threads';
|
||||
$replyContext = $this->getInput('include_reply_context');
|
||||
|
||||
$did = $this->resolveHandle($handle);
|
||||
$this->profile = $this->getProfile($did);
|
||||
$authorFeed = $this->getAuthorFeed($did, $filter);
|
||||
|
||||
foreach ($authorFeed['feed'] as $post) {
|
||||
$postRecord = $post['post']['record'];
|
||||
|
||||
$item = [];
|
||||
$item['uri'] = self::URI . '/profile/' . $post['post']['author']['handle'] . '/post/' . explode('app.bsky.feed.post/', $post['post']['uri'])[1];
|
||||
$item['title'] = strtok($post['post']['record']['text'], "\n");
|
||||
$item['timestamp'] = strtotime($post['post']['record']['createdAt']);
|
||||
$item['author'] = $this->profile['displayName'];
|
||||
$item['uri'] = self::URI . '/profile/' . $this->fallbackAuthor($post['post']['author'], 'url') . '/post/' . explode('app.bsky.feed.post/', $post['post']['uri'])[1];
|
||||
$item['title'] = $this->getInput('verbose_title') ? $this->generateVerboseTitle($post) : strtok($postRecord['text'], "\n");
|
||||
$item['timestamp'] = strtotime($postRecord['createdAt']);
|
||||
$item['author'] = $this->fallbackAuthor($post['post']['author'], 'display');
|
||||
|
||||
$description = $this->textToDescription($post['post']['record']['text']);
|
||||
$postAuthorDID = $post['post']['author']['did'];
|
||||
$postAuthorHandle = $post['post']['author']['handle'] !== 'handle.invalid' ? '<i>@' . $post['post']['author']['handle'] . '</i> ' : '';
|
||||
$postDisplayName = $post['post']['author']['displayName'] ?? '';
|
||||
$postDisplayName = e($postDisplayName);
|
||||
$postUri = $item['uri'];
|
||||
|
||||
// Retrieve DID for constructing image URLs
|
||||
$authorDid = $post['post']['author']['did'];
|
||||
|
||||
if (isset($post['post']['record']['embed']['$type']) && $post['post']['record']['embed']['$type'] === 'app.bsky.embed.external') {
|
||||
$description .= $this->parseExternal($post['post']['record']['embed']['external'], $authorDid);
|
||||
if (Debug::isEnabled()) {
|
||||
$url = explode('/', $post['post']['uri']);
|
||||
$this->logger->debug('https://bsky.app/profile/' . $url[2] . '/post/' . $url[4]);
|
||||
}
|
||||
|
||||
if (isset($post['post']['record']['embed']['$type']) && $post['post']['record']['embed']['$type'] === 'app.bsky.embed.video') {
|
||||
$thumbnail = $post['post']['embed']['thumbnail'] ?? null;
|
||||
if ($thumbnail) {
|
||||
$itemUri = self::URI . '/profile/' . $post['post']['author']['handle'] . '/post/' . explode('app.bsky.feed.post/', $post['post']['uri'])[1];
|
||||
$description .= "<p><a href=\"$itemUri\"><img src=\"$thumbnail\" alt=\"Video Thumbnail\" /></a></p>";
|
||||
$description = '';
|
||||
$description .= '<p>';
|
||||
//post
|
||||
$description .= $this->getPostDescription(
|
||||
$postDisplayName,
|
||||
$postAuthorHandle,
|
||||
$postUri,
|
||||
$postRecord,
|
||||
'post'
|
||||
);
|
||||
|
||||
if (isset($postRecord['embed']['$type'])) {
|
||||
//post link embed
|
||||
if ($postRecord['embed']['$type'] === 'app.bsky.embed.external') {
|
||||
$description .= $this->parseExternal($postRecord['embed']['external'], $postAuthorDID);
|
||||
} elseif (
|
||||
$postRecord['embed']['$type'] === 'app.bsky.embed.recordWithMedia' &&
|
||||
$postRecord['embed']['media']['$type'] === 'app.bsky.embed.external'
|
||||
) {
|
||||
$description .= $this->parseExternal($postRecord['embed']['media']['external'], $postAuthorDID);
|
||||
}
|
||||
|
||||
//post images
|
||||
if (
|
||||
$postRecord['embed']['$type'] === 'app.bsky.embed.images' ||
|
||||
(
|
||||
$postRecord['embed']['$type'] === 'app.bsky.embed.recordWithMedia' &&
|
||||
$postRecord['embed']['media']['$type'] === 'app.bsky.embed.images'
|
||||
)
|
||||
) {
|
||||
$images = $post['post']['embed']['images'] ?? $post['post']['embed']['media']['images'];
|
||||
foreach ($images as $image) {
|
||||
$description .= $this->getPostImageDescription($image);
|
||||
}
|
||||
}
|
||||
|
||||
//post video
|
||||
if (
|
||||
$postRecord['embed']['$type'] === 'app.bsky.embed.video' ||
|
||||
(
|
||||
$postRecord['embed']['$type'] === 'app.bsky.embed.recordWithMedia' &&
|
||||
$postRecord['embed']['media']['$type'] === 'app.bsky.embed.video'
|
||||
)
|
||||
) {
|
||||
$description .= $this->getPostVideoDescription(
|
||||
$postRecord['embed']['video'] ?? $postRecord['embed']['media']['video'],
|
||||
$postAuthorDID
|
||||
);
|
||||
}
|
||||
}
|
||||
$description .= '</p>';
|
||||
|
||||
if (isset($post['post']['record']['embed']['$type']) && $post['post']['record']['embed']['$type'] === 'app.bsky.embed.recordWithMedia#view') {
|
||||
$thumbnail = $post['post']['embed']['media']['thumbnail'] ?? null;
|
||||
$playlist = $post['post']['embed']['media']['playlist'] ?? null;
|
||||
if ($thumbnail) {
|
||||
$description .= "<p><video controls poster=\"$thumbnail\">";
|
||||
$description .= "<source src=\"$playlist\" type=\"application/x-mpegURL\">";
|
||||
$description .= 'Video source not supported</video></p>';
|
||||
}
|
||||
}
|
||||
//quote post
|
||||
if (
|
||||
isset($postRecord['embed']) &&
|
||||
(
|
||||
$postRecord['embed']['$type'] === 'app.bsky.embed.record' ||
|
||||
$postRecord['embed']['$type'] === 'app.bsky.embed.recordWithMedia'
|
||||
) &&
|
||||
isset($post['post']['embed']['record'])
|
||||
) {
|
||||
$description .= '<p>';
|
||||
$quotedRecord = $post['post']['embed']['record']['record'] ?? $post['post']['embed']['record'];
|
||||
|
||||
if (!empty($post['post']['record']['embed']['images'])) {
|
||||
foreach ($post['post']['record']['embed']['images'] as $image) {
|
||||
$linkRef = $image['image']['ref']['$link'];
|
||||
$thumbnailUrl = $this->resolveThumbnailUrl($authorDid, $linkRef);
|
||||
$fullsizeUrl = $this->resolveFullsizeUrl($authorDid, $linkRef);
|
||||
$description .= "<br /><br /><a href=\"$fullsizeUrl\"><img src=\"$thumbnailUrl\" alt=\"Image\"></a>";
|
||||
}
|
||||
}
|
||||
if (isset($quotedRecord['notFound']) && $quotedRecord['notFound']) { //deleted post
|
||||
$description .= 'Quoted post deleted.';
|
||||
} elseif (isset($quotedRecord['detached']) && $quotedRecord['detached']) { //detached quote
|
||||
$uri_explode = explode('/', $quotedRecord['uri']);
|
||||
$uri_reconstructed = self::URI . '/profile/' . $uri_explode[2] . '/post/' . $uri_explode[4];
|
||||
$description .= '<a href="' . $uri_reconstructed . '">Quoted post detached.</a>';
|
||||
} elseif (isset($quotedRecord['blocked']) && $quotedRecord['blocked']) { //blocked by quote author
|
||||
$description .= 'Author of quoted post has blocked OP.';
|
||||
} elseif (
|
||||
($quotedRecord['$type'] ?? '') === 'app.bsky.feed.defs#generatorView' ||
|
||||
($quotedRecord['$type'] ?? '') === 'app.bsky.graph.defs#listView'
|
||||
) {
|
||||
$description .= $this->getListFeedDescription($quotedRecord);
|
||||
} elseif (
|
||||
($quotedRecord['$type'] ?? '') === 'app.bsky.graph.starterpack' ||
|
||||
($quotedRecord['$type'] ?? '') === 'app.bsky.graph.defs#starterPackViewBasic'
|
||||
) {
|
||||
$description .= $this->getStarterPackDescription($post['post']['embed']['record']);
|
||||
} else {
|
||||
$quotedAuthorDid = $quotedRecord['author']['did'];
|
||||
$quotedDisplayName = $quotedRecord['author']['displayName'] ?? '';
|
||||
$quotedDisplayName = e($quotedDisplayName);
|
||||
$quotedAuthorHandle = $quotedRecord['author']['handle'] !== 'handle.invalid' ? '<i>@' . $quotedRecord['author']['handle'] . '</i>' : '';
|
||||
|
||||
// Enhanced handling for quote posts with images
|
||||
if (isset($post['post']['record']['embed']) && $post['post']['record']['embed']['$type'] === 'app.bsky.embed.record') {
|
||||
$quotedRecord = $post['post']['record']['embed']['record'];
|
||||
$quotedAuthor = $post['post']['embed']['record']['author']['handle'] ?? null;
|
||||
$quotedDisplayName = $post['post']['embed']['record']['author']['displayName'] ?? null;
|
||||
$quotedText = $post['post']['embed']['record']['value']['text'] ?? null;
|
||||
|
||||
if ($quotedAuthor && isset($quotedRecord['uri'])) {
|
||||
$parts = explode('/', $quotedRecord['uri']);
|
||||
$quotedPostId = end($parts);
|
||||
$quotedPostUri = self::URI . '/profile/' . $quotedAuthor . '/post/' . $quotedPostId;
|
||||
}
|
||||
$quotedPostUri = self::URI . '/profile/' . $this->fallbackAuthor($quotedRecord['author'], 'url') . '/post/' . $quotedPostId;
|
||||
|
||||
if ($quotedText) {
|
||||
$description .= '<hr /><strong>Quote from ' . htmlspecialchars($quotedDisplayName) . ' (@ ' . htmlspecialchars($quotedAuthor) . '):</strong><br />';
|
||||
$description .= $this->textToDescription($quotedText);
|
||||
if (isset($quotedPostUri)) {
|
||||
$description .= "<p><a href=\"$quotedPostUri\">View original quote post</a></p>";
|
||||
//quoted post - post
|
||||
$description .= $this->getPostDescription(
|
||||
$quotedDisplayName,
|
||||
$quotedAuthorHandle,
|
||||
$quotedPostUri,
|
||||
$quotedRecord,
|
||||
'quote'
|
||||
);
|
||||
|
||||
if (isset($quotedRecord['value']['embed']['$type'])) {
|
||||
//quoted post - post link embed
|
||||
if ($quotedRecord['value']['embed']['$type'] === 'app.bsky.embed.external') {
|
||||
$description .= $this->parseExternal($quotedRecord['value']['embed']['external'], $quotedAuthorDid);
|
||||
}
|
||||
|
||||
//quoted post - post video
|
||||
if (
|
||||
$quotedRecord['value']['embed']['$type'] === 'app.bsky.embed.video' ||
|
||||
(
|
||||
$quotedRecord['value']['embed']['$type'] === 'app.bsky.embed.recordWithMedia' &&
|
||||
$quotedRecord['value']['embed']['media']['$type'] === 'app.bsky.embed.video'
|
||||
)
|
||||
) {
|
||||
$description .= $this->getPostVideoDescription(
|
||||
$quotedRecord['value']['embed']['video'] ?? $quotedRecord['value']['embed']['media']['video'],
|
||||
$quotedAuthorDid
|
||||
);
|
||||
}
|
||||
|
||||
//quoted post - post images
|
||||
if (
|
||||
$quotedRecord['value']['embed']['$type'] === 'app.bsky.embed.images' ||
|
||||
(
|
||||
$quotedRecord['value']['embed']['$type'] === 'app.bsky.embed.recordWithMedia' &&
|
||||
$quotedRecord['value']['embed']['media']['$type'] === 'app.bsky.embed.images'
|
||||
)
|
||||
) {
|
||||
foreach ($quotedRecord['embeds'] as $embed) {
|
||||
if (
|
||||
$embed['$type'] === 'app.bsky.embed.images#view' ||
|
||||
($embed['$type'] === 'app.bsky.embed.recordWithMedia#view' && $embed['media']['$type'] === 'app.bsky.embed.images#view')
|
||||
) {
|
||||
$images = $embed['images'] ?? $embed['media']['images'];
|
||||
foreach ($images as $image) {
|
||||
$description .= $this->getPostImageDescription($image);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$description .= '</p>';
|
||||
}
|
||||
|
||||
if (isset($post['post']['embed']['record']['value']['embed']['images'])) {
|
||||
$quotedImages = $post['post']['embed']['record']['value']['embed']['images'];
|
||||
foreach ($quotedImages as $image) {
|
||||
$linkRef = $image['image']['ref']['$link'] ?? null;
|
||||
if ($linkRef) {
|
||||
$quotedAuthorDid = $post['post']['embed']['record']['author']['did'] ?? null;
|
||||
$thumbnailUrl = $this->resolveThumbnailUrl($quotedAuthorDid, $linkRef);
|
||||
$fullsizeUrl = $this->resolveFullsizeUrl($quotedAuthorDid, $linkRef);
|
||||
$description .= "<br /><br /><a href=\"$fullsizeUrl\"><img src=\"$thumbnailUrl\" alt=\"Quoted Image\"></a>";
|
||||
//reply
|
||||
if ($replyContext && isset($post['reply']) && !isset($post['reply']['parent']['notFound'])) {
|
||||
$replyPost = $post['reply']['parent'];
|
||||
$replyPostRecord = $replyPost['record'];
|
||||
$description .= '<hr/>';
|
||||
$description .= '<p>';
|
||||
|
||||
$replyPostAuthorDID = $replyPost['author']['did'];
|
||||
$replyPostAuthorHandle = $replyPost['author']['handle'] !== 'handle.invalid' ? '<i>@' . $replyPost['author']['handle'] . '</i> ' : '';
|
||||
$replyPostDisplayName = $replyPost['author']['displayName'] ?? '';
|
||||
$replyPostDisplayName = e($replyPostDisplayName);
|
||||
$replyPostUri = self::URI . '/profile/' . $this->fallbackAuthor($replyPost['author'], 'url') . '/post/' . explode('app.bsky.feed.post/', $replyPost['uri'])[1];
|
||||
|
||||
// reply post
|
||||
$description .= $this->getPostDescription(
|
||||
$replyPostDisplayName,
|
||||
$replyPostAuthorHandle,
|
||||
$replyPostUri,
|
||||
$replyPostRecord,
|
||||
'reply'
|
||||
);
|
||||
|
||||
if (isset($replyPostRecord['embed']['$type'])) {
|
||||
//post link embed
|
||||
if ($replyPostRecord['embed']['$type'] === 'app.bsky.embed.external') {
|
||||
$description .= $this->parseExternal($replyPostRecord['embed']['external'], $replyPostAuthorDID);
|
||||
} elseif (
|
||||
$replyPostRecord['embed']['$type'] === 'app.bsky.embed.recordWithMedia' &&
|
||||
$replyPostRecord['embed']['media']['$type'] === 'app.bsky.embed.external'
|
||||
) {
|
||||
$description .= $this->parseExternal($replyPostRecord['embed']['media']['external'], $replyPostAuthorDID);
|
||||
}
|
||||
|
||||
//post images
|
||||
if (
|
||||
$replyPostRecord['embed']['$type'] === 'app.bsky.embed.images' ||
|
||||
(
|
||||
$replyPostRecord['embed']['$type'] === 'app.bsky.embed.recordWithMedia' &&
|
||||
$replyPostRecord['embed']['media']['$type'] === 'app.bsky.embed.images'
|
||||
)
|
||||
) {
|
||||
$images = $replyPost['embed']['images'] ?? $replyPost['embed']['media']['images'];
|
||||
foreach ($images as $image) {
|
||||
$description .= $this->getPostImageDescription($image);
|
||||
}
|
||||
}
|
||||
|
||||
//post video
|
||||
if (
|
||||
$replyPostRecord['embed']['$type'] === 'app.bsky.embed.video' ||
|
||||
(
|
||||
$replyPostRecord['embed']['$type'] === 'app.bsky.embed.recordWithMedia' &&
|
||||
$replyPostRecord['embed']['media']['$type'] === 'app.bsky.embed.video'
|
||||
)
|
||||
) {
|
||||
$description .= $this->getPostVideoDescription(
|
||||
$replyPostRecord['embed']['video'] ?? $replyPostRecord['embed']['media']['video'],
|
||||
$replyPostAuthorDID
|
||||
);
|
||||
}
|
||||
}
|
||||
$description .= '</p>';
|
||||
|
||||
//quote post
|
||||
if (
|
||||
isset($replyPostRecord['embed']) &&
|
||||
($replyPostRecord['embed']['$type'] === 'app.bsky.embed.record' || $replyPostRecord['embed']['$type'] === 'app.bsky.embed.recordWithMedia') &&
|
||||
isset($replyPost['embed']['record'])
|
||||
) {
|
||||
$description .= '<p>';
|
||||
$replyQuotedRecord = $replyPost['embed']['record']['record'] ?? $replyPost['embed']['record'];
|
||||
|
||||
if (isset($replyQuotedRecord['notFound']) && $replyQuotedRecord['notFound']) { //deleted post
|
||||
$description .= 'Quoted post deleted.';
|
||||
} elseif (isset($replyQuotedRecord['detached']) && $replyQuotedRecord['detached']) { //detached quote
|
||||
$uri_explode = explode('/', $replyQuotedRecord['uri']);
|
||||
$uri_reconstructed = self::URI . '/profile/' . $uri_explode[2] . '/post/' . $uri_explode[4];
|
||||
$description .= '<a href="' . $uri_reconstructed . '">Quoted post detached.</a>';
|
||||
} elseif (isset($replyQuotedRecord['blocked']) && $replyQuotedRecord['blocked']) { //blocked by quote author
|
||||
$description .= 'Author of quoted post has blocked OP.';
|
||||
} elseif (
|
||||
($replyQuotedRecord['$type'] ?? '') === 'app.bsky.feed.defs#generatorView' ||
|
||||
($replyQuotedRecord['$type'] ?? '') === 'app.bsky.graph.defs#listView'
|
||||
) {
|
||||
$description .= $this->getListFeedDescription($replyQuotedRecord);
|
||||
} elseif (
|
||||
($replyQuotedRecord['$type'] ?? '') === 'app.bsky.graph.starterpack' ||
|
||||
($replyQuotedRecord['$type'] ?? '') === 'app.bsky.graph.defs#starterPackViewBasic'
|
||||
) {
|
||||
$description .= $this->getStarterPackDescription($replyPost['embed']['record']);
|
||||
} else {
|
||||
$quotedAuthorDid = $replyQuotedRecord['author']['did'];
|
||||
$quotedDisplayName = $replyQuotedRecord['author']['displayName'] ?? '';
|
||||
$quotedDisplayName = e($quotedDisplayName);
|
||||
$quotedAuthorHandle = $replyQuotedRecord['author']['handle'] !== 'handle.invalid' ? '<i>@' . $replyQuotedRecord['author']['handle'] . '</i>' : '';
|
||||
|
||||
$parts = explode('/', $replyQuotedRecord['uri']);
|
||||
$quotedPostId = end($parts);
|
||||
$quotedPostUri = self::URI . '/profile/' . $this->fallbackAuthor($replyQuotedRecord['author'], 'url') . '/post/' . $quotedPostId;
|
||||
|
||||
//quoted post - post
|
||||
$description .= $this->getPostDescription(
|
||||
$quotedDisplayName,
|
||||
$quotedAuthorHandle,
|
||||
$quotedPostUri,
|
||||
$replyQuotedRecord,
|
||||
'quote'
|
||||
);
|
||||
|
||||
if (isset($replyQuotedRecord['value']['embed']['$type'])) {
|
||||
//quoted post - post link embed
|
||||
if ($replyQuotedRecord['value']['embed']['$type'] === 'app.bsky.embed.external') {
|
||||
$description .= $this->parseExternal($replyQuotedRecord['value']['embed']['external'], $quotedAuthorDid);
|
||||
}
|
||||
|
||||
//quoted post - post video
|
||||
if (
|
||||
$replyQuotedRecord['value']['embed']['$type'] === 'app.bsky.embed.video' ||
|
||||
(
|
||||
$replyQuotedRecord['value']['embed']['$type'] === 'app.bsky.embed.recordWithMedia' &&
|
||||
$replyQuotedRecord['value']['embed']['media']['$type'] === 'app.bsky.embed.video'
|
||||
)
|
||||
) {
|
||||
$description .= $this->getPostVideoDescription(
|
||||
$replyQuotedRecord['value']['embed']['video'] ?? $replyQuotedRecord['value']['embed']['media']['video'],
|
||||
$quotedAuthorDid
|
||||
);
|
||||
}
|
||||
|
||||
//quoted post - post images
|
||||
if (
|
||||
$replyQuotedRecord['value']['embed']['$type'] === 'app.bsky.embed.images' ||
|
||||
(
|
||||
$replyQuotedRecord['value']['embed']['$type'] === 'app.bsky.embed.recordWithMedia' &&
|
||||
$replyQuotedRecord['value']['embed']['media']['$type'] === 'app.bsky.embed.images'
|
||||
)
|
||||
) {
|
||||
foreach ($replyQuotedRecord['embeds'] as $embed) {
|
||||
if (
|
||||
$embed['$type'] === 'app.bsky.embed.images#view' ||
|
||||
($embed['$type'] === 'app.bsky.embed.recordWithMedia#view' && $embed['media']['$type'] === 'app.bsky.embed.images#view')
|
||||
) {
|
||||
$images = $embed['images'] ?? $embed['media']['images'];
|
||||
foreach ($images as $image) {
|
||||
$description .= $this->getPostImageDescription($image);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$description .= '</p>';
|
||||
}
|
||||
}
|
||||
|
||||
@ -197,6 +489,106 @@ class BlueskyBridge extends BridgeAbstract
|
||||
}
|
||||
}
|
||||
|
||||
private function getPostVideoDescription(array $video, $authorDID)
|
||||
{
|
||||
//https://video.bsky.app/watch/$did/$cid/thumbnail.jpg
|
||||
$videoCID = $video['ref']['$link'];
|
||||
$videoMime = $video['mimeType'];
|
||||
$thumbnail = "poster=\"https://video.bsky.app/watch/$authorDID/$videoCID/thumbnail.jpg\"" ?? '';
|
||||
$videoURL = "https://bsky.social/xrpc/com.atproto.sync.getBlob?did=$authorDID&cid=$videoCID";
|
||||
return "<figure><video loop $thumbnail controls src=\"$videoURL\" type=\"$videoMime\"/></figure>";
|
||||
}
|
||||
|
||||
private function getPostImageDescription(array $image)
|
||||
{
|
||||
$thumbnailUrl = $image['thumb'];
|
||||
$fullsizeUrl = $image['fullsize'];
|
||||
$alt = strlen($image['alt']) > 0 ? '<figcaption>' . e($image['alt']) . '</figcaption>' : '';
|
||||
return "<figure><a href=\"$fullsizeUrl\"><img src=\"$thumbnailUrl\"></a>$alt</figure>";
|
||||
}
|
||||
|
||||
private function getPostDescription(
|
||||
string $postDisplayName,
|
||||
string $postAuthorHandle,
|
||||
string $postUri,
|
||||
array $postRecord,
|
||||
string $type
|
||||
) {
|
||||
$description = '';
|
||||
if ($type === 'quote') {
|
||||
// Quoted post/reply from bbb @bbb.com:
|
||||
$postType = isset($postRecord['reply']) ? 'reply' : 'post';
|
||||
$description .= "<a href=\"$postUri\">Quoted $postType</a> from <b>$postDisplayName</b> $postAuthorHandle:<br>";
|
||||
} elseif ($type === 'reply') {
|
||||
// Replying to aaa @aaa.com's post/reply:
|
||||
$postType = isset($postRecord['reply']) ? 'reply' : 'post';
|
||||
$description .= "Replying to <b>$postDisplayName</b> $postAuthorHandle's <a href=\"$postUri\">$postType</a>:<br>";
|
||||
} else {
|
||||
// aaa @aaa.com posted:
|
||||
$description .= "<b>$postDisplayName</b> $postAuthorHandle <a href=\"$postUri\">posted</a>:<br>";
|
||||
}
|
||||
$description .= $this->textToDescription($postRecord);
|
||||
return $description;
|
||||
}
|
||||
|
||||
//used if handle verification fails, fallsback to displayName or DID depending on context.
|
||||
private function fallbackAuthor($author, $reason)
|
||||
{
|
||||
if ($author['handle'] === 'handle.invalid') {
|
||||
switch ($reason) {
|
||||
case 'url':
|
||||
return $author['did'];
|
||||
case 'display':
|
||||
$displayName = $author['displayName'] ?? '';
|
||||
return e($displayName);
|
||||
}
|
||||
}
|
||||
return $author['handle'];
|
||||
}
|
||||
|
||||
private function generateVerboseTitle($post)
|
||||
{
|
||||
//use "Post by A, replying to B, quoting C" instead of post contents
|
||||
$title = '';
|
||||
if (isset($post['reason']) && str_contains($post['reason']['$type'], 'reasonRepost')) {
|
||||
$title .= 'Repost by ' . $this->fallbackAuthor($post['reason']['by'], 'display') . ', post by ' . $this->fallbackAuthor($post['post']['author'], 'display');
|
||||
} else {
|
||||
$title .= 'Post by ' . $this->fallbackAuthor($post['post']['author'], 'display');
|
||||
}
|
||||
|
||||
if (isset($post['reply'])) {
|
||||
if (isset($post['reply']['parent']['blocked'])) {
|
||||
$replyAuthor = 'blocked user';
|
||||
} elseif (isset($post['reply']['parent']['notFound'])) {
|
||||
$replyAuthor = 'deleted post';
|
||||
} else {
|
||||
$replyAuthor = $this->fallbackAuthor($post['reply']['parent']['author'], 'display');
|
||||
}
|
||||
$title .= ', replying to ' . $replyAuthor;
|
||||
}
|
||||
|
||||
if (
|
||||
isset($post['post']['embed']) &&
|
||||
isset($post['post']['embed']['record']) &&
|
||||
//if not starter pack, feed or list
|
||||
($post['post']['embed']['record']['$type'] ?? '') !== 'app.bsky.feed.defs#generatorView' &&
|
||||
($post['post']['embed']['record']['$type'] ?? '') !== 'app.bsky.graph.defs#listView' &&
|
||||
($post['post']['embed']['record']['$type'] ?? '') !== 'app.bsky.graph.defs#starterPackViewBasic'
|
||||
) {
|
||||
if (isset($post['post']['embed']['record']['blocked'])) {
|
||||
$quotedAuthor = 'blocked user';
|
||||
} elseif (isset($post['post']['embed']['record']['notFound'])) {
|
||||
$quotedAuthor = 'deleted psost';
|
||||
} elseif (isset($post['post']['embed']['record']['detached'])) {
|
||||
$quotedAuthor = 'detached post';
|
||||
} else {
|
||||
$quotedAuthor = $this->fallbackAuthor($post['post']['embed']['record']['record']['author'] ?? $post['post']['embed']['record']['author'], 'display');
|
||||
}
|
||||
$title .= ', quoting ' . $quotedAuthor;
|
||||
}
|
||||
return $title;
|
||||
}
|
||||
|
||||
private function resolveHandle($handle)
|
||||
{
|
||||
$uri = 'https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle?handle=' . urlencode($handle);
|
||||
@ -214,17 +606,65 @@ class BlueskyBridge extends BridgeAbstract
|
||||
private function getAuthorFeed($did, $filter)
|
||||
{
|
||||
$uri = 'https://public.api.bsky.app/xrpc/app.bsky.feed.getAuthorFeed?actor=' . urlencode($did) . '&filter=' . urlencode($filter) . '&limit=30';
|
||||
if (Debug::isEnabled()) {
|
||||
$this->logger->debug($uri);
|
||||
}
|
||||
$response = json_decode(getContents($uri), true);
|
||||
return $response;
|
||||
}
|
||||
|
||||
private function resolveThumbnailUrl($authorDid, $linkRef)
|
||||
//Embed for generated feeds and lists
|
||||
private function getListFeedDescription(array $record): string
|
||||
{
|
||||
return 'https://cdn.bsky.app/img/feed_thumbnail/plain/' . $authorDid . '/' . $linkRef . '@jpeg';
|
||||
$feedViewAvatar = isset($record['avatar']) ? '<img src="' . preg_replace('/\/img\/avatar\//', '/img/avatar_thumbnail/', $record['avatar']) . '">' : '';
|
||||
$feedViewName = e($record['displayName'] ?? $record['name']);
|
||||
$feedViewDescription = e($record['description'] ?? '');
|
||||
$authorDisplayName = e($record['creator']['displayName']);
|
||||
$authorHandle = e($record['creator']['handle']);
|
||||
$likeCount = isset($record['likeCount']) ? '<br>Liked by ' . e($record['likeCount']) . ' users' : '';
|
||||
preg_match('/\/([^\/]+)$/', $record['uri'], $matches);
|
||||
if (($record['purpose'] ?? '') === 'app.bsky.graph.defs#modlist') {
|
||||
$typeURL = '/lists/';
|
||||
$typeDesc = 'moderation list';
|
||||
} elseif (($record['purpose'] ?? '') === 'app.bsky.graph.defs#curatelist') {
|
||||
$typeURL = '/lists/';
|
||||
$typeDesc = 'list';
|
||||
} else {
|
||||
$typeURL = '/feed/';
|
||||
$typeDesc = 'feed';
|
||||
}
|
||||
$uri = e('https://bsky.app/profile/' . $record['creator']['did'] . $typeURL . $matches[1]);
|
||||
|
||||
return <<<END
|
||||
<blockquote>
|
||||
<b><a href="{$uri}">{$feedViewName}</a></b><br/>
|
||||
Bluesky {$typeDesc} by <b>{$authorDisplayName}</b> <i>@{$authorHandle}</i>
|
||||
<figure>
|
||||
{$feedViewAvatar}
|
||||
<figcaption>{$feedViewDescription}{$likeCount}</figcaption>
|
||||
</figure>
|
||||
</blockquote>
|
||||
END;
|
||||
}
|
||||
|
||||
private function resolveFullsizeUrl($authorDid, $linkRef)
|
||||
private function getStarterPackDescription(array $record): string
|
||||
{
|
||||
return 'https://cdn.bsky.app/img/feed_fullsize/plain/' . $authorDid . '/' . $linkRef . '@jpeg';
|
||||
if (!isset($record['record'])) {
|
||||
return 'Failed to get starter pack information.';
|
||||
}
|
||||
$starterpackRecord = $record['record'];
|
||||
$starterpackName = e($starterpackRecord['name']);
|
||||
$starterpackDescription = e($starterpackRecord['description']);
|
||||
$creatorDisplayName = e($record['creator']['displayName']);
|
||||
$creatorHandle = e($record['creator']['handle']);
|
||||
preg_match('/\/([^\/]+)$/', $starterpackRecord['list'], $matches);
|
||||
$uri = e('https://bsky.app/starter-pack/' . $record['creator']['did'] . '/' . $matches[1]);
|
||||
return <<<END
|
||||
<blockquote>
|
||||
<b><a href="{$uri}">{$starterpackName}</a></b><br/>
|
||||
Bluesky starter pack by <b>{$creatorDisplayName}</b> <i>@{$creatorHandle}</i><br/>
|
||||
{$starterpackDescription}
|
||||
</blockquote>
|
||||
END;
|
||||
}
|
||||
}
|
||||
|
63
bridges/BruegelBridge.php
Normal file
63
bridges/BruegelBridge.php
Normal file
@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
class BruegelBridge extends BridgeAbstract
|
||||
{
|
||||
const NAME = 'Bruegel';
|
||||
const URI = 'https://www.bruegel.org';
|
||||
const DESCRIPTION = 'European think-tank commentary and publications.';
|
||||
const MAINTAINER = 'KappaPrajd';
|
||||
const PARAMETERS = [
|
||||
[
|
||||
'category' => [
|
||||
'name' => 'Category',
|
||||
'type' => 'list',
|
||||
'defaultValue' => '/publications',
|
||||
'values' => [
|
||||
'Publications' => '/publications',
|
||||
'Commentary' => '/commentary'
|
||||
]
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
public function getIcon()
|
||||
{
|
||||
return self::URI . '/themes/custom/bruegel/assets/favicon/android-icon-72x72.png';
|
||||
}
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
$url = self::URI . $this->getInput('category');
|
||||
$html = getSimpleHTMLDOM($url);
|
||||
|
||||
$articles = $html->find('.c-listing__content article');
|
||||
|
||||
foreach ($articles as $article) {
|
||||
$title = $article->find('.c-list-item__title a span', 0)->plaintext;
|
||||
$content = trim($article->find('.c-list-item__description', 0)->plaintext);
|
||||
$publishDate = $article->find('.c-list-item__date', 0)->plaintext;
|
||||
$href = $article->find('.c-list-item__title a', 0)->getAttribute('href');
|
||||
|
||||
$item = [
|
||||
'title' => $title,
|
||||
'content' => $content,
|
||||
'timestamp' => strtotime($publishDate),
|
||||
'uri' => self::URI . $href,
|
||||
'author' => $this->getAuthor($article),
|
||||
];
|
||||
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
private function getAuthor($article)
|
||||
{
|
||||
$authorsElements = $article->find('.c-list-item__authors a');
|
||||
|
||||
$authors = array_map(function ($author) {
|
||||
return $author->plaintext;
|
||||
}, $authorsElements);
|
||||
|
||||
return join(', ', $authors);
|
||||
}
|
||||
}
|
@ -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),
|
||||
];
|
||||
|
@ -26,18 +26,16 @@ TMPL;
|
||||
https://www.bundestag.de/ajax/filterlist/de/parlament/praesidium/parteienfinanzierung/fundstellen50000/462002-462002
|
||||
URI;
|
||||
// Get the main page
|
||||
$html = getSimpleHTMLDOMCached($ajaxUri, self::CACHE_TIMEOUT)
|
||||
or returnServerError('Could not request AJAX list.');
|
||||
$html = getSimpleHTMLDOMCached($ajaxUri, self::CACHE_TIMEOUT);
|
||||
|
||||
// Build the URL from the first anchor element. The list is sorted by year, descending, so the first element is the current year.
|
||||
$firstAnchor = $html->find('a', 0)
|
||||
or returnServerError('Could not find the proper HTML element.');
|
||||
|
||||
$url = 'https://www.bundestag.de' . $firstAnchor->href;
|
||||
$url = $firstAnchor->href;
|
||||
|
||||
// Get the actual page with the soft money donations
|
||||
$html = getSimpleHTMLDOMCached($url, self::CACHE_TIMEOUT)
|
||||
or returnServerError('Could not request ' . $url);
|
||||
$html = getSimpleHTMLDOMCached($url, self::CACHE_TIMEOUT);
|
||||
|
||||
$rows = $html->find('table.table > tbody > tr')
|
||||
or returnServerError('Could not find the proper HTML elements.');
|
||||
|
@ -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) {
|
||||
|
@ -48,6 +48,11 @@ class CentreFranceBridge extends BridgeAbstract
|
||||
]
|
||||
];
|
||||
|
||||
private static array $monthNumberByFrenchName = [
|
||||
'janvier' => 1, 'février' => 2, 'mars' => 3, 'avril' => 4, 'mai' => 5, 'juin' => 6, 'juillet' => 7,
|
||||
'août' => 8, 'septembre' => 9, 'octobre' => 10, 'novembre' => 11, 'décembre' => 12
|
||||
];
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
$value = $this->getInput('limit');
|
||||
@ -130,14 +135,22 @@ class CentreFranceBridge extends BridgeAbstract
|
||||
'enclosures' => [],
|
||||
];
|
||||
|
||||
$articleInformations = $html->find('.c-article-informations p');
|
||||
$articleInformations = $html->find('#content hgroup > div.typo-p3 > *');
|
||||
if (is_array($articleInformations) && $articleInformations !== []) {
|
||||
$authorPosition = 1;
|
||||
$publicationDateIndex = 0;
|
||||
|
||||
// Article author
|
||||
$probableAuthorName = strip_tags($articleInformations[0]->innertext);
|
||||
if (str_starts_with($probableAuthorName, 'Par ')) {
|
||||
$publicationDateIndex = 1;
|
||||
$item['author'] = substr($probableAuthorName, 4);
|
||||
}
|
||||
|
||||
// Article publication date
|
||||
if (preg_match('/(\d{2})\/(\d{2})\/(\d{4})( à (\d{2})h(\d{2}))?/', $articleInformations[0]->innertext, $articleDateParts) > 0) {
|
||||
preg_match('/Publié le (\d{2}) (.+) (\d{4})( à (\d{2})h(\d{2}))?/', strip_tags($articleInformations[$publicationDateIndex]->innertext), $articleDateParts);
|
||||
if ($articleDateParts !== [] && array_key_exists($articleDateParts[2], self::$monthNumberByFrenchName)) {
|
||||
$articleDate = new \DateTime('midnight');
|
||||
$articleDate->setDate($articleDateParts[3], $articleDateParts[2], $articleDateParts[1]);
|
||||
$articleDate->setDate($articleDateParts[3], self::$monthNumberByFrenchName[$articleDateParts[2]], $articleDateParts[1]);
|
||||
|
||||
if (count($articleDateParts) === 7) {
|
||||
$articleDate->setTime($articleDateParts[5], $articleDateParts[6]);
|
||||
@ -145,57 +158,31 @@ class CentreFranceBridge extends BridgeAbstract
|
||||
|
||||
$item['timestamp'] = $articleDate->getTimestamp();
|
||||
}
|
||||
|
||||
// Article update date
|
||||
if (count($articleInformations) >= 2 && preg_match('/(\d{2})\/(\d{2})\/(\d{4})( à (\d{2})h(\d{2}))?/', $articleInformations[1]->innertext, $articleDateParts) > 0) {
|
||||
$authorPosition = 2;
|
||||
|
||||
$articleDate = new \DateTime('midnight');
|
||||
$articleDate->setDate($articleDateParts[3], $articleDateParts[2], $articleDateParts[1]);
|
||||
|
||||
if (count($articleDateParts) === 7) {
|
||||
$articleDate->setTime($articleDateParts[5], $articleDateParts[6]);
|
||||
}
|
||||
|
||||
$item['timestamp'] = $articleDate->getTimestamp();
|
||||
}
|
||||
|
||||
if (count($articleInformations) === ($authorPosition + 1)) {
|
||||
$item['author'] = $articleInformations[$authorPosition]->innertext;
|
||||
}
|
||||
}
|
||||
|
||||
$articleContent = $html->find('.b-article .contenu > *');
|
||||
if (is_array($articleContent)) {
|
||||
$item['content'] = '';
|
||||
|
||||
foreach ($articleContent as $contentPart) {
|
||||
if (in_array($contentPart->getAttribute('id'), ['cf-audio-player', 'poool-widget'], true)) {
|
||||
continue;
|
||||
$articleContent = $html->find('#content>div.flex+div.grid section>.z-10')[0] ?? null;
|
||||
if ($articleContent instanceof \simple_html_dom_node) {
|
||||
$articleHiddenParts = $articleContent->find('.ad-slot, #cf-digiteka-player');
|
||||
if (is_array($articleHiddenParts)) {
|
||||
foreach ($articleHiddenParts as $articleHiddenPart) {
|
||||
$articleContent->removeChild($articleHiddenPart);
|
||||
}
|
||||
|
||||
$articleHiddenParts = $contentPart->find('.bloc, .p402_hide');
|
||||
if (is_array($articleHiddenParts)) {
|
||||
foreach ($articleHiddenParts as $articleHiddenPart) {
|
||||
$contentPart->removeChild($articleHiddenPart);
|
||||
}
|
||||
}
|
||||
|
||||
$item['content'] .= $contentPart->innertext;
|
||||
}
|
||||
|
||||
$item['content'] = $articleContent->innertext;
|
||||
}
|
||||
|
||||
$articleIllustration = $html->find('.photo-wrapper .photo-box img');
|
||||
$articleIllustration = $html->find('#content>div.flex+div.grid section>figure>img');
|
||||
if (is_array($articleIllustration) && count($articleIllustration) === 1) {
|
||||
$item['enclosures'][] = $articleIllustration[0]->getAttribute('src');
|
||||
}
|
||||
|
||||
$articleAudio = $html->find('#cf-audio-player-container audio');
|
||||
$articleAudio = $html->find('audio[src^="https://api.octopus.saooti.com/"]');
|
||||
if (is_array($articleAudio) && count($articleAudio) === 1) {
|
||||
$item['enclosures'][] = $articleAudio[0]->getAttribute('src');
|
||||
}
|
||||
|
||||
$articleTags = $html->find('.b-article > ul.c-tags > li > a.t-simple');
|
||||
$articleTags = $html->find('#content>div.flex+div.grid section>.bg-gray-light>a.border-gray-dark');
|
||||
if (is_array($articleTags)) {
|
||||
$item['categories'] = array_map(static fn ($articleTag) => $articleTag->innertext, $articleTags);
|
||||
}
|
||||
|
@ -18,25 +18,6 @@ class CeskaTelevizeBridge extends BridgeAbstract
|
||||
]
|
||||
];
|
||||
|
||||
private function fixChars($text)
|
||||
{
|
||||
return html_entity_decode($text, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
private function getUploadTimeFromString($string)
|
||||
{
|
||||
if (strpos($string, 'dnes') !== false) {
|
||||
return strtotime('today');
|
||||
} elseif (strpos($string, 'včera') !== false) {
|
||||
return strtotime('yesterday');
|
||||
} elseif (!preg_match('/(\d+).\s(\d+).(\s(\d+))?/', $string, $match)) {
|
||||
returnServerError('Could not get date from Česká televize string');
|
||||
}
|
||||
|
||||
$date = sprintf('%04d-%02d-%02d', $match[3] ?? date('Y'), $match[2], $match[1]);
|
||||
return strtotime($date);
|
||||
}
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
$url = $this->getInput('url');
|
||||
@ -58,24 +39,42 @@ class CeskaTelevizeBridge extends BridgeAbstract
|
||||
}
|
||||
|
||||
foreach ($html->find('#episodeListSection a[data-testid=card]') as $element) {
|
||||
$itemTitle = $element->find('h3', 0);
|
||||
$itemContent = $element->find('p[class^=content-]', 0);
|
||||
$itemDate = $element->find('div[class^=playTime-] span, [data-testid=episode-item-broadcast] span', 0);
|
||||
$itemThumbnail = $element->find('img', 0);
|
||||
$itemUri = self::URI . $element->getAttribute('href');
|
||||
|
||||
// Remove special characters and whitespace
|
||||
$cleanDate = preg_replace('/[^0-9.]/', '', $itemDate->plaintext);
|
||||
|
||||
$item = [
|
||||
'title' => $this->fixChars($itemTitle->plaintext),
|
||||
'uri' => $itemUri,
|
||||
'content' => '<img src="' . $itemThumbnail->getAttribute('src') . '" /><br />'
|
||||
. $this->fixChars($itemContent->plaintext),
|
||||
'timestamp' => $this->getUploadTimeFromString($itemDate->plaintext)
|
||||
'title' => $this->fixChars($element->find('h3', 0)->plaintext),
|
||||
'uri' => self::URI . $element->getAttribute('href'),
|
||||
'content' => '<img src="' . $element->find('img', 0)->getAttribute('srcset') . '" /><br />' . $this->fixChars($itemContent->plaintext),
|
||||
'timestamp' => $this->getUploadTimeFromString($cleanDate),
|
||||
];
|
||||
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
private function getUploadTimeFromString($string)
|
||||
{
|
||||
if (strpos($string, 'dnes') !== false) {
|
||||
return strtotime('today');
|
||||
} elseif (strpos($string, 'včera') !== false) {
|
||||
return strtotime('yesterday');
|
||||
} elseif (!preg_match('/(\d+).(\d+).((\d+))?/', $string, $match)) {
|
||||
returnServerError('Could not get date from Česká televize string');
|
||||
}
|
||||
|
||||
$date = sprintf('%04d-%02d-%02d', $match[3] ?? date('Y'), $match[2], $match[1]);
|
||||
return strtotime($date);
|
||||
}
|
||||
|
||||
private function fixChars($text)
|
||||
{
|
||||
return html_entity_decode($text, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
public function getURI()
|
||||
{
|
||||
return $this->feedUri ?? parent::getURI();
|
||||
|
@ -109,7 +109,7 @@ class CrewbayBridge extends BridgeAbstract
|
||||
public function collectData()
|
||||
{
|
||||
$url = $this->getURI();
|
||||
$html = getSimpleHTMLDOM($url) or returnClientError('No results for this query.');
|
||||
$html = getSimpleHTMLDOM($url);
|
||||
|
||||
$annonces = $html->find('#SearchResults div.result');
|
||||
$limit = 0;
|
||||
|
@ -53,8 +53,7 @@ class DacksnackBridge extends BridgeAbstract
|
||||
public function collectData()
|
||||
{
|
||||
$NEWSURL = self::URI;
|
||||
$html = getSimpleHTMLDOMCached($NEWSURL, 18000) or
|
||||
returnServerError('Could not request: ' . $NEWSURL);
|
||||
$html = getSimpleHTMLDOMCached($NEWSURL, 18000);
|
||||
|
||||
foreach ($html->find('a.main-news-item') as $element) {
|
||||
// Debug::log($element);
|
||||
@ -64,8 +63,7 @@ class DacksnackBridge extends BridgeAbstract
|
||||
$url = self::URI . $element->getAttribute('href');
|
||||
$published = $this->parseSwedishDates(trim($element->find('.published', 0)->plaintext));
|
||||
|
||||
$article_html = getSimpleHTMLDOMCached($url, 18000) or
|
||||
returnServerError('Could not request: ' . $url);
|
||||
$article_html = getSimpleHTMLDOMCached($url, 18000);
|
||||
$article_content = $article_html->find('#ctl00_ContentPlaceHolder1_NewsArticleVeiw_pnlArticle', 0);
|
||||
|
||||
$figure = self::URI . $article_content->find('img.news-image', 0)->getAttribute('src');
|
||||
|
@ -18,8 +18,7 @@ class DagensNyheterDirektBridge extends BridgeAbstract
|
||||
{
|
||||
$NEWSURL = self::BASEURL . '/ajax/direkt/';
|
||||
|
||||
$html = getSimpleHTMLDOM($NEWSURL) or
|
||||
returnServerError('Could not request: ' . $NEWSURL);
|
||||
$html = getSimpleHTMLDOM($NEWSURL);
|
||||
|
||||
foreach ($html->find('article') as $element) {
|
||||
$link = $element->find('button', 0)->getAttribute('data-link');
|
||||
|
@ -10,9 +10,11 @@ class DansTonChatBridge extends BridgeAbstract
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
$html = getSimpleHTMLDOM(self::URI . 'latest.html');
|
||||
$url = self::URI . 'latest.html';
|
||||
$dom = getSimpleHTMLDOM($url);
|
||||
|
||||
foreach ($html->find('div.item') as $element) {
|
||||
$items = $dom->find('div.item');
|
||||
foreach ($items as $element) {
|
||||
$item = [];
|
||||
$item['uri'] = $element->find('a', 0)->href;
|
||||
$titleContent = $element->find('h3 a', 0);
|
||||
|
@ -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' => '€',
|
||||
|
@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Retourne les dons d'une recherche filtrée sur le site Donnons.org
|
||||
* Example: https://donnons.org/Sport/Ile-de-France
|
||||
@ -44,58 +46,60 @@ class DonnonsBridge extends BridgeAbstract
|
||||
{
|
||||
$uri = $this->getPageURI($page);
|
||||
|
||||
$html = getSimpleHTMLDOM($uri);
|
||||
$dom = getSimpleHTMLDOM($uri);
|
||||
|
||||
$searchDiv = $html->find('div[id=search]', 0);
|
||||
$searchDiv = $dom->find('div[id=search]', 0);
|
||||
|
||||
if (!is_null($searchDiv)) {
|
||||
$elements = $searchDiv->find('a.lst-annonce');
|
||||
foreach ($elements as $element) {
|
||||
$item = [];
|
||||
if (! $searchDiv) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Lien vers le don
|
||||
$item['uri'] = self::URI . $element->href;
|
||||
// Id de l'objet
|
||||
$item['uid'] = $element->getAttribute('data-id');
|
||||
$elements = $searchDiv->find('a.lst-annonce');
|
||||
foreach ($elements as $element) {
|
||||
$item = [];
|
||||
|
||||
// Grab info from json
|
||||
$jsonString = $element->find('script', 0)->innertext;
|
||||
$json = json_decode($jsonString, true);
|
||||
// Lien vers le don
|
||||
$item['uri'] = self::URI . $element->href;
|
||||
// Id de l'objet
|
||||
$item['uid'] = $element->getAttribute('data-id');
|
||||
|
||||
$name = $json['name'];
|
||||
$category = $json['category'];
|
||||
$date = $json['availabilityStarts'];
|
||||
$description = $json['description'];
|
||||
$city = $json['availableAtOrFrom']['address']['addressLocality'];
|
||||
$region = $json['availableAtOrFrom']['address']['addressRegion'];
|
||||
// Grab info from json
|
||||
$jsonString = $element->find('script', 0)->innertext;
|
||||
$json = json_decode($jsonString, true);
|
||||
|
||||
// Grab info from HTML
|
||||
$imageSrc = $element->find('img.ima-center', 0)->getAttribute('src');
|
||||
// Use large image instead of small one
|
||||
$imageSrc = str_replace('/xs/', '/lg/', $imageSrc);
|
||||
$image = self::URI . $imageSrc;
|
||||
$author = $element->find('div.avatar-holder', 0)->plaintext;
|
||||
$name = $json['name'];
|
||||
$category = $json['category'];
|
||||
$date = $json['availabilityStarts'];
|
||||
$description = $json['description'];
|
||||
$city = $json['availableAtOrFrom']['address']['addressLocality'];
|
||||
$region = $json['availableAtOrFrom']['address']['addressRegion'];
|
||||
|
||||
$content = '
|
||||
<img style="margin-right:1em;" src="' . $image . '">
|
||||
<div>
|
||||
<h1>' . $name . '</h1>
|
||||
<p>' . $description . '</p>
|
||||
<p>Lieu : <b>' . $city . '</b> - ' . $region . '</p>
|
||||
<p>Par : ' . $author . '</p>
|
||||
<p>Date : ' . $date . '</p>
|
||||
</div>
|
||||
';
|
||||
// Grab info from HTML
|
||||
$imageSrc = $element->find('img.ima-center', 0)->getAttribute('src');
|
||||
// Use large image instead of small one
|
||||
$imageSrc = str_replace('/xs/', '/lg/', $imageSrc);
|
||||
$image = self::URI . $imageSrc;
|
||||
$author = $element->find('div.avatar-holder', 0)->plaintext;
|
||||
|
||||
// Titre du don
|
||||
$item['title'] = '[' . $category . '] ' . $name;
|
||||
$item['timestamp'] = $date;
|
||||
$item['author'] = $author;
|
||||
$item['content'] = $content;
|
||||
$item['enclosures'] = [$image];
|
||||
$content = '
|
||||
<img style="margin-right:1em;" src="' . $image . '">
|
||||
<div>
|
||||
<h1>' . $name . '</h1>
|
||||
<p>' . $description . '</p>
|
||||
<p>Lieu : <b>' . $city . '</b> - ' . $region . '</p>
|
||||
<p>Par : ' . $author . '</p>
|
||||
<p>Date : ' . $date . '</p>
|
||||
</div>
|
||||
';
|
||||
|
||||
$this->items[] = $item;
|
||||
}
|
||||
// Titre du don
|
||||
$item['title'] = '[' . $category . '] ' . $name;
|
||||
$item['timestamp'] = $date;
|
||||
$item['author'] = $author;
|
||||
$item['content'] = $content;
|
||||
$item['enclosures'] = [$image];
|
||||
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -41,6 +41,12 @@ class EconomistWorldInBriefBridge extends BridgeAbstract
|
||||
'quote' => [
|
||||
'name' => 'Include the quote of the day',
|
||||
'type' => 'checkbox'
|
||||
],
|
||||
'mergeEverything' => [
|
||||
'name' => 'Merge everything into one entry',
|
||||
'type' => 'checkbox',
|
||||
'defaultValue' => false,
|
||||
'title' => 'Whether to merge all the stories into one entry'
|
||||
]
|
||||
]
|
||||
];
|
||||
@ -61,7 +67,7 @@ class EconomistWorldInBriefBridge extends BridgeAbstract
|
||||
}
|
||||
$html = getSimpleHTMLDOM(self::URI, $headers);
|
||||
$gobbets = $html->find('p[data-component="the-world-in-brief-paragraph"]');
|
||||
if ($this->getInput('splitGobbets') == 1) {
|
||||
if ($this->getInput('splitGobbets') == 1 && !$this->getInput('mergeEverything')) {
|
||||
$this->splitGobbets($gobbets);
|
||||
} else {
|
||||
$this->mergeGobbets($gobbets);
|
||||
@ -77,6 +83,9 @@ class EconomistWorldInBriefBridge extends BridgeAbstract
|
||||
$quote = $html->find('blockquote[data-test-id="inspirational-quote"]', 0);
|
||||
$this->addQuote($quote);
|
||||
}
|
||||
if ($this->getInput('mergeEverything') == 1) {
|
||||
$this->mergeEverything();
|
||||
}
|
||||
}
|
||||
|
||||
private function splitGobbets($gobbets)
|
||||
@ -131,6 +140,9 @@ class EconomistWorldInBriefBridge extends BridgeAbstract
|
||||
if ($element->tag != 'div') {
|
||||
continue;
|
||||
}
|
||||
if ($element->find('._newsletterContentPromo', 0) != null) {
|
||||
continue;
|
||||
}
|
||||
$image = $element->find('figure', 0);
|
||||
$title = $element->find('h3', 0)->plaintext;
|
||||
$content = $element->find('h3', 0)->parent();
|
||||
@ -165,4 +177,35 @@ class EconomistWorldInBriefBridge extends BridgeAbstract
|
||||
'uid' => 'quote-' . $today->format('U')
|
||||
];
|
||||
}
|
||||
|
||||
private function mergeEverything()
|
||||
{
|
||||
$today = new Datetime();
|
||||
$today->setTime(0, 0, 0, 0);
|
||||
$contents = '';
|
||||
|
||||
foreach ($this->items as $item) {
|
||||
$header = null;
|
||||
if (str_contains($item['uid'], 'story-')) {
|
||||
$header = $item['title'];
|
||||
} elseif (str_contains($item['uid'], 'quote-')) {
|
||||
$header = 'Quote of the day';
|
||||
} elseif (str_contains($item['uid'], 'world-in-brief-')) {
|
||||
$header = 'World in brief';
|
||||
}
|
||||
if ($header != null) {
|
||||
$contents .= "<h2>{$header}</h2>";
|
||||
}
|
||||
$contents .= $item['content'];
|
||||
}
|
||||
|
||||
$item = [
|
||||
'uri' => self::URI,
|
||||
'title' => 'The Economist World in Brief ' . $today->format('d.m.Y'),
|
||||
'content' => $contents,
|
||||
'timestamp' => $today->format('U'),
|
||||
'uid' => 'world-in-brief-merged' . $today->format('U')
|
||||
];
|
||||
$this->items = [$item];
|
||||
}
|
||||
}
|
||||
|
@ -12,8 +12,28 @@ class EdfPricesBridge extends BridgeAbstract
|
||||
'contract' => [
|
||||
'name' => 'Choisir un contrat',
|
||||
'type' => 'list',
|
||||
// we can add later HCHP, EJP, base
|
||||
'values' => ['Tempo' => '/energie/edf/tarifs/tempo'],
|
||||
// we can add later more option prices
|
||||
'values' => [
|
||||
'Base' => '/energie/edf/tarifs/tarif-bleu#base',
|
||||
'HPHC' => '/energie/edf/tarifs/tarif-bleu#hphc',
|
||||
'EJP' => '/energie/edf/tarifs/tarif-bleu#ejp',
|
||||
'Tempo' => '/energie/edf/tarifs/tempo'
|
||||
],
|
||||
],
|
||||
'power' => [
|
||||
'name' => 'Choisir une puissance',
|
||||
'type' => 'list',
|
||||
'values' => [
|
||||
'3 kVA' => 3,
|
||||
'6 kVA' => 6,
|
||||
'9 kVA' => 9,
|
||||
'12 kVA' => 12,
|
||||
'15 kVA' => 15,
|
||||
'18 kVA' => 18,
|
||||
'24 kVA' => 24,
|
||||
'30 kVA' => 30,
|
||||
'36 kVA' => 36
|
||||
]
|
||||
]
|
||||
]
|
||||
];
|
||||
@ -24,36 +44,20 @@ class EdfPricesBridge extends BridgeAbstract
|
||||
* @param string $contractUri
|
||||
* @return void
|
||||
*/
|
||||
private function tempo(simple_html_dom $html, string $contractUri): void
|
||||
private function tempo(simple_html_dom $html, string $contractUri, int $power): void
|
||||
{
|
||||
// current color and next
|
||||
$daysDom = $html->find('#calendrier', 0)->nextSibling()->find('.card--ejp');
|
||||
if ($daysDom && count($daysDom) === 2) {
|
||||
foreach ($daysDom as $dayDom) {
|
||||
$day = trim($dayDom->find('.card__title', 0)->innertext) . '/' . (new \DateTime('now'))->format(('Y'));
|
||||
$dayColor = $dayDom->find('.card-ejp__icon span', 0)->innertext;
|
||||
|
||||
$text = $day . ' - ' . $dayColor;
|
||||
$item['uri'] = self::URI . $contractUri;
|
||||
$item['title'] = $text;
|
||||
$item['author'] = self::MAINTAINER;
|
||||
$item['content'] = $text;
|
||||
$item['uid'] = hash('sha256', $item['title']);
|
||||
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
// colors
|
||||
$ulDom = $html->find('#tarif-de-l-offre-tempo-edf-template-date-now-y', 0)->nextSibling()->nextSibling()->nextSibling();
|
||||
$elementsDom = $ulDom->find('li');
|
||||
if ($elementsDom && count($elementsDom) === 3) {
|
||||
// price per kWh is same for all powers
|
||||
foreach ($elementsDom as $elementDom) {
|
||||
$item = [];
|
||||
|
||||
$matches = [];
|
||||
preg_match_all('/Jour (.*) : Heures (.*) : (.*) € \/ Heures (.*) : (.*) €/um', $elementDom->innertext, $matches, PREG_SET_ORDER, 0);
|
||||
|
||||
// for tempo contract we have 2x3 colors
|
||||
if ($matches && count($matches[0]) === 6) {
|
||||
for ($i = 0; $i < 2; $i++) {
|
||||
$text = 'Jour ' . $matches[0][1] . ' - Heures ' . $matches[0][2 + 2 * $i] . ' : ' . $matches[0][3 + 2 * $i] . '€';
|
||||
@ -69,26 +73,166 @@ class EdfPricesBridge extends BridgeAbstract
|
||||
}
|
||||
}
|
||||
|
||||
// powers
|
||||
$ulPowerContract = $ulDom->nextSibling()->nextSibling();
|
||||
$elementsPowerContractDom = $ulPowerContract->find('li');
|
||||
if ($elementsPowerContractDom && count($elementsPowerContractDom) === 4) {
|
||||
foreach ($elementsPowerContractDom as $elementPowerContractDom) {
|
||||
// add subscription power info
|
||||
$tablePrices = $ulDom->nextSibling()->nextSibling()->nextSibling()->find('.table--responsive', 0);
|
||||
$this->addSubscriptionPowerInfo($tablePrices, $contractUri, $power, 7);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param simple_html_dom $html
|
||||
* @param string $contractUri
|
||||
* @return void
|
||||
*/
|
||||
private function base(simple_html_dom $html, string $contractUri, int $power): void
|
||||
{
|
||||
$tablePrices = $html
|
||||
->find('#grille-tarifaire-et-prix-du-kwh-du-tarif-reglemente-edf-en-option-base', 0)
|
||||
->nextSibling()
|
||||
->nextSibling()
|
||||
->nextSibling();
|
||||
|
||||
$prices = $tablePrices->find('.table--stripped tbody tr');
|
||||
// last element is useless because part of another table
|
||||
array_pop($prices);
|
||||
|
||||
// price per kWh is same for all powers
|
||||
if ($prices && count($prices) === 9) {
|
||||
$item = [];
|
||||
|
||||
$text = 'Base : ' . $prices[0]->children(2);
|
||||
$item['uri'] = self::URI . $contractUri;
|
||||
$item['title'] = $text;
|
||||
$item['author'] = self::MAINTAINER;
|
||||
$item['content'] = $text;
|
||||
$item['uid'] = hash('sha256', $item['title']);
|
||||
|
||||
$this->items[] = $item;
|
||||
}
|
||||
|
||||
$this->addSubscriptionPowerInfo($tablePrices, $contractUri, $power, 9);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param simple_html_dom $html
|
||||
* @param string $contractUri
|
||||
* @return void
|
||||
*/
|
||||
private function hphc(simple_html_dom $html, string $contractUri, int $power): void
|
||||
{
|
||||
$tablePrices = $html
|
||||
->find('#grille-tarifaire-et-prix-du-kwh-du-tarif-reglemente-edf-en-option-heures-pleines-heures-creuses', 0)
|
||||
->nextSibling()
|
||||
->nextSibling()
|
||||
->nextSibling();
|
||||
|
||||
$prices = $tablePrices->find('.table--stripped tbody tr');
|
||||
// last element is useless because part of another table
|
||||
array_pop($prices);
|
||||
|
||||
// price per kWh is same for all powers
|
||||
if ($prices && count($prices) === 8) {
|
||||
$values = ['HC', 'HP'];
|
||||
foreach ($values as $key => $value) {
|
||||
$i++;
|
||||
$item = [];
|
||||
|
||||
$matches = [];
|
||||
preg_match_all('/(.*) kVA : (.*) €/um', $elementPowerContractDom->innertext, $matches, PREG_SET_ORDER, 0);
|
||||
$text = $values[$key] . ' : ' . $prices[0]->children($key + 2);
|
||||
$item['uri'] = self::URI . $contractUri;
|
||||
$item['title'] = $text;
|
||||
$item['author'] = self::MAINTAINER;
|
||||
$item['content'] = $text;
|
||||
$item['uid'] = hash('sha256', $item['title']);
|
||||
|
||||
if ($matches && count($matches[0]) === 3) {
|
||||
$text = $matches[0][1] . ' kVA : ' . $matches[0][2] . '€';
|
||||
$item['uri'] = self::URI . $contractUri;
|
||||
$item['title'] = $text;
|
||||
$item['author'] = self::MAINTAINER;
|
||||
$item['content'] = $text;
|
||||
$item['uid'] = hash('sha256', $item['title']);
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
$this->items[] = $item;
|
||||
$this->addSubscriptionPowerInfo($tablePrices, $contractUri, $power, 8);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param simple_html_dom $html
|
||||
* @param string $contractUri
|
||||
* @return void
|
||||
*/
|
||||
private function ejp(simple_html_dom $html, string $contractUri, int $power): void
|
||||
{
|
||||
$tablePrices = $html
|
||||
->find('#grille-tarifaire-et-prix-du-kwh-du-tarif-reglemente-edf-en-option-ejp', 0)
|
||||
->nextSibling()
|
||||
->nextSibling()
|
||||
->nextSibling();
|
||||
|
||||
$prices = $tablePrices->find('.table--stripped tbody tr');
|
||||
// last element is useless because part of another table
|
||||
array_pop($prices);
|
||||
|
||||
// price per kWh is same for all powers
|
||||
if ($prices && count($prices) === 5) {
|
||||
$values = ['Non EJP', 'EJP'];
|
||||
foreach ($values as $key => $value) {
|
||||
$i++;
|
||||
$item = [];
|
||||
|
||||
$text = $values[$key] . ' : ' . $prices[0]->children($key + 2);
|
||||
$item['uri'] = self::URI . $contractUri;
|
||||
$item['title'] = $text;
|
||||
$item['author'] = self::MAINTAINER;
|
||||
$item['content'] = $text;
|
||||
$item['uid'] = hash('sha256', $item['title']);
|
||||
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
$this->addSubscriptionPowerInfo($tablePrices, $contractUri, $power, 5);
|
||||
}
|
||||
|
||||
private function addSubscriptionPowerInfo(simple_html_dom_node $tablePrices, string $contractUri, int $power, int $numberOfPrices): void
|
||||
{
|
||||
$prices = $tablePrices->find('.table--stripped tbody tr');
|
||||
// last element is useless because part of another table
|
||||
array_pop($prices);
|
||||
|
||||
// 7 contracts for tempo: 6, 9, 12, 15, 18, 30 and 36 kVA
|
||||
// 9 contracts for base: 3, 6, 9, 12, 15, 18, 24, 30 and 36 kVA
|
||||
// 7 contracts for HPHC: 6, 9, 12, 15, 18, 24, 30 and 36 kVA
|
||||
// 5 contracts for EJP: 9, 12, 15, 18 and 36 kVA
|
||||
if ($prices && count($prices) === $numberOfPrices) {
|
||||
$powerFound = false;
|
||||
foreach ($prices as $price) {
|
||||
$powerText = $price->firstChild()->firstChild()->innertext;
|
||||
$powerValue = (int)substr($powerText, 0, strpos($powerText, ' kVA'));
|
||||
|
||||
if ($powerValue !== $power) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$item = [];
|
||||
|
||||
$text = $powerText . ' : ' . $price->children(1) . '/an';
|
||||
$item['uri'] = self::URI . $contractUri;
|
||||
$item['title'] = $text;
|
||||
$item['author'] = self::MAINTAINER;
|
||||
$item['content'] = $text;
|
||||
$item['uid'] = hash('sha256', $item['title']);
|
||||
|
||||
$this->items[] = $item;
|
||||
$powerFound = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!$powerFound) {
|
||||
$item = [];
|
||||
|
||||
$text = 'Pas de tarif abonnement pour cette puissance et ce contrat';
|
||||
$item['uri'] = self::URI . $contractUri;
|
||||
$item['title'] = $text;
|
||||
$item['author'] = self::MAINTAINER;
|
||||
$item['content'] = $text;
|
||||
$item['uid'] = hash('sha256', $item['title']);
|
||||
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -97,10 +241,23 @@ class EdfPricesBridge extends BridgeAbstract
|
||||
{
|
||||
$contract = $this->getKey('contract');
|
||||
$contractUri = $this->getInput('contract');
|
||||
$power = $this->getInput('power');
|
||||
$html = getSimpleHTMLDOM(self::URI . $contractUri);
|
||||
|
||||
if ($contract === 'Tempo') {
|
||||
$this->tempo($html, $contractUri);
|
||||
$this->tempo($html, $contractUri, $power);
|
||||
}
|
||||
|
||||
if ($contract === 'Base') {
|
||||
$this->base($html, $contractUri, $power);
|
||||
}
|
||||
|
||||
if ($contract === 'HPHC') {
|
||||
$this->hphc($html, $contractUri, $power);
|
||||
}
|
||||
|
||||
if ($contract === 'EJP') {
|
||||
$this->ejp($html, $contractUri, $power);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,8 +6,10 @@ class FeedMergeBridge extends FeedExpander
|
||||
const NAME = 'FeedMerge';
|
||||
const URI = 'https://github.com/RSS-Bridge/rss-bridge';
|
||||
const DESCRIPTION = <<<'TEXT'
|
||||
This bridge merges two or more feeds into a single feed. Max 10 items are fetched from each feed.
|
||||
TEXT;
|
||||
This bridge merges two or more feeds into a single feed. <br>
|
||||
Max 10 latest items are fetched from each individual feed. <br>
|
||||
Items with identical url or title are considered duplicates (and are removed). <br>
|
||||
TEXT;
|
||||
|
||||
const PARAMETERS = [
|
||||
[
|
||||
@ -36,11 +38,11 @@ TEXT;
|
||||
];
|
||||
|
||||
/**
|
||||
* todo: Consider a strategy which produces a shorter feed url
|
||||
* TODO: Consider a strategy which produces a shorter feed url
|
||||
*/
|
||||
public function collectData()
|
||||
{
|
||||
$limit = (int)($this->getInput('limit') ?: 10);
|
||||
$limit = (int)($this->getInput('limit') ?: 99);
|
||||
$feeds = [
|
||||
$this->getInput('feed_1'),
|
||||
$this->getInput('feed_2'),
|
||||
@ -61,7 +63,7 @@ TEXT;
|
||||
if (count($feeds) > 1) {
|
||||
// Allow one or more feeds to fail
|
||||
try {
|
||||
$this->collectExpandableDatas($feed);
|
||||
$this->collectExpandableDatas($feed, 10);
|
||||
} catch (HttpException $e) {
|
||||
$this->logger->warning(sprintf('Exception in FeedMergeBridge: %s', create_sane_exception_message($e)));
|
||||
// This feed item might be spammy. Considering dropping it.
|
||||
@ -80,31 +82,48 @@ TEXT;
|
||||
throw $e;
|
||||
}
|
||||
} else {
|
||||
$this->collectExpandableDatas($feed);
|
||||
$this->collectExpandableDatas($feed, 10);
|
||||
}
|
||||
}
|
||||
|
||||
// If $this->items is empty we should consider throw exception here
|
||||
|
||||
// Sort by timestamp descending
|
||||
// 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;
|
||||
});
|
||||
|
||||
// Remove duplicates by using url as unique key
|
||||
// Remove duplicates by url
|
||||
$items = [];
|
||||
foreach ($this->items as $item) {
|
||||
$index = $item['uri'] ?? null;
|
||||
if ($index) {
|
||||
// Overwrite duplicates
|
||||
$items[$index] = $item;
|
||||
$uri = $item['uri'] ?? null;
|
||||
if ($uri) {
|
||||
// Insert or override the existing duplicate
|
||||
$items[$uri] = $item;
|
||||
} else {
|
||||
// The item doesn't have a uri!
|
||||
$items[] = $item;
|
||||
}
|
||||
}
|
||||
$this->items = array_slice(array_values($items), 0, $limit);
|
||||
$this->items = array_values($items);
|
||||
|
||||
// Remove duplicates by title
|
||||
$items = [];
|
||||
foreach ($this->items as $item) {
|
||||
$title = $item['title'] ?? null;
|
||||
if ($title) {
|
||||
// Insert or override the existing duplicate
|
||||
$items[$title] = $item;
|
||||
} else {
|
||||
// The item doesn't have a title!
|
||||
$items[] = $item;
|
||||
}
|
||||
}
|
||||
$this->items = array_values($items);
|
||||
|
||||
$this->items = array_slice($this->items, 0, $limit);
|
||||
}
|
||||
|
||||
public function getIcon()
|
||||
|
@ -60,7 +60,7 @@ class FindACrewBridge extends BridgeAbstract
|
||||
CURLOPT_POSTFIELDS => http_build_query($data) . "\n"
|
||||
];
|
||||
|
||||
$html = getSimpleHTMLDOM($url, $header, $opts) or returnClientError('No results for this query.');
|
||||
$html = getSimpleHTMLDOM($url, $header, $opts);
|
||||
|
||||
$annonces = $html->find('.css_SrhRst');
|
||||
$limit = $this->getInput('limit') ?? 10;
|
||||
|
@ -5,13 +5,13 @@ class Formula1Bridge extends BridgeAbstract
|
||||
const NAME = 'Formula1 Bridge';
|
||||
const URI = 'https://formula1.com/';
|
||||
const DESCRIPTION = 'Returns latest official Formula 1 news';
|
||||
const MAINTAINER = 'AxorPL';
|
||||
const MAINTAINER = 'axor-mst';
|
||||
|
||||
const API_KEY = 'qPgPPRJyGCIPxFT3el4MF7thXHyJCzAP';
|
||||
const API_KEY = 'xZ7AOODSjiQadLsIYWefQrpCSQVDbHGC';
|
||||
const API_URL = 'https://api.formula1.com/v1/editorial/articles?limit=%u';
|
||||
|
||||
const ARTICLE_AUTHOR = 'Formula 1';
|
||||
const ARTICLE_URL = 'https://formula1.com/en/latest/article.%s.%s.html';
|
||||
const ARTICLE_URL = 'https://formula1.com/en/latest/article/%s.%s';
|
||||
|
||||
const LIMIT_MIN = 1;
|
||||
const LIMIT_DEFAULT = 10;
|
||||
@ -36,7 +36,11 @@ class Formula1Bridge extends BridgeAbstract
|
||||
$limit = min(self::LIMIT_MAX, max(self::LIMIT_MIN, $limit));
|
||||
$url = sprintf(self::API_URL, $limit);
|
||||
|
||||
$json = json_decode(getContents($url, ['apikey: ' . self::API_KEY]));
|
||||
$json = json_decode(getContents($url, [
|
||||
'Accept: application/json',
|
||||
'apikey: ' . self::API_KEY,
|
||||
'locale: en'
|
||||
]));
|
||||
if (property_exists($json, 'error')) {
|
||||
returnServerError($json->message);
|
||||
}
|
||||
|
@ -1,78 +0,0 @@
|
||||
<?php
|
||||
|
||||
class FragDenStaatBridge extends BridgeAbstract
|
||||
{
|
||||
const MAINTAINER = 'swofl';
|
||||
const NAME = 'FragDenStaat';
|
||||
const URI = 'https://fragdenstaat.de';
|
||||
const CACHE_TIMEOUT = 2 * 60 * 60; // 2h
|
||||
const DESCRIPTION = 'Get latest blog posts from FragDenStaat Exklusiv';
|
||||
const PARAMETERS = [ [
|
||||
'qLimit' => [
|
||||
'name' => 'Query Limit',
|
||||
'title' => 'Amount of articles to query',
|
||||
'type' => 'number',
|
||||
'defaultValue' => 5,
|
||||
],
|
||||
] ];
|
||||
|
||||
protected function parseTeaser($teaser)
|
||||
{
|
||||
$result = [];
|
||||
|
||||
$header = $teaser->find('h3 > a', 0);
|
||||
$result['title'] = $header->plaintext;
|
||||
$result['uri'] = static::URI . $header->href;
|
||||
$result['enclosures'] = [];
|
||||
$result['enclosures'][] = $teaser->find('img', 0)->src;
|
||||
$result['uid'] = hash('sha256', $result['title']);
|
||||
$result['timestamp'] = strtotime($teaser->find('time', 0)->getAttribute('datetime'));
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
$html = getSimpleHTMLDOM(self::URI . '/artikel/exklusiv/');
|
||||
|
||||
$queryLimit = (int) $this->getInput('qLimit');
|
||||
if ($queryLimit > 12) {
|
||||
$queryLimit = 12;
|
||||
}
|
||||
|
||||
$teasers = [];
|
||||
|
||||
$teaserElements = $html->find('article');
|
||||
|
||||
for ($i = 0; $i < $queryLimit; $i++) {
|
||||
array_push($teasers, $this->parseTeaser($teaserElements[$i]));
|
||||
}
|
||||
|
||||
foreach ($teasers as $article) {
|
||||
$articleHtml = getSimpleHTMLDOMCached($article['uri'], static::CACHE_TIMEOUT * 6);
|
||||
$articleCore = $articleHtml->find('article.blog-article', 0);
|
||||
|
||||
$content = '';
|
||||
|
||||
$lead = $articleCore->find('div.lead > p', 0)->innertext;
|
||||
|
||||
$content .= '<h2>' . $lead . '</h2>';
|
||||
|
||||
foreach ($articleCore->find('div.blog-content > p, div.blog-content > h3') as $paragraph) {
|
||||
$content .= $paragraph->outertext;
|
||||
}
|
||||
|
||||
$article['content'] = '<img src="' . $article['enclosures'][0] . '"/>' . $content;
|
||||
|
||||
$article['author'] = '';
|
||||
|
||||
foreach ($articleCore->find('a[rel="author"]') as $author) {
|
||||
$article['author'] .= $author->innertext . ', ';
|
||||
}
|
||||
|
||||
$article['author'] = rtrim($article['author'], ', ');
|
||||
|
||||
$this->items[] = $article;
|
||||
}
|
||||
}
|
||||
}
|
@ -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]');
|
||||
|
@ -34,8 +34,7 @@ class FurAffinityUserBridge extends BridgeAbstract
|
||||
|
||||
$url = self::URI . '/gallery/' . $this->getInput('searchUsername');
|
||||
|
||||
$html = getSimpleHTMLDOM($url, [], $opt)
|
||||
or returnServerError('Could not load the user\'s gallery page.');
|
||||
$html = getSimpleHTMLDOM($url, [], $opt);
|
||||
|
||||
$submissions = $html->find('section[id=gallery-gallery]', 0)->find('figure');
|
||||
foreach ($submissions as $submission) {
|
||||
|
@ -155,8 +155,7 @@ class GiteaBridge extends BridgeAbstract
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
$html = getSimpleHTMLDOM($this->getURI())
|
||||
or returnServerError('Could not request ' . $this->getURI());
|
||||
$html = getSimpleHTMLDOM($this->getURI());
|
||||
$html = defaultLinkTo($html, $this->getURI());
|
||||
|
||||
$this->title = $html->find('[property="og:title"]', 0)->content;
|
||||
@ -246,8 +245,7 @@ class GiteaBridge extends BridgeAbstract
|
||||
];
|
||||
|
||||
if ($this->getInput('include_description')) {
|
||||
$issue_html = getSimpleHTMLDOMCached($uri, 3600)
|
||||
or returnServerError('Unable to load issue description');
|
||||
$issue_html = getSimpleHTMLDOMCached($uri, 3600);
|
||||
|
||||
$issue_html = defaultLinkTo($issue_html, $uri);
|
||||
|
||||
@ -308,8 +306,7 @@ class GiteaBridge extends BridgeAbstract
|
||||
];
|
||||
|
||||
if ($this->getInput('include_description')) {
|
||||
$issue_html = getSimpleHTMLDOMCached($uri, 3600)
|
||||
or returnServerError('Unable to load issue description');
|
||||
$issue_html = getSimpleHTMLDOMCached($uri, 3600);
|
||||
|
||||
$issue_html = defaultLinkTo($issue_html, $uri);
|
||||
|
||||
|
@ -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;
|
||||
|
@ -28,7 +28,7 @@ class GlowficBridge extends BridgeAbstract
|
||||
public function collectData()
|
||||
{
|
||||
$url = $this->getAPIURI();
|
||||
$metadata = get_headers($url . '/replies', true) or returnClientError('Post did not return reply headers.');
|
||||
$metadata = get_headers($url . '/replies', true);
|
||||
$metadata['Last-Page'] = ceil($metadata['Total'] / $metadata['Per-Page']);
|
||||
if (
|
||||
!is_null($this->getInput('start_page')) &&
|
||||
|
@ -2,7 +2,8 @@
|
||||
|
||||
class GoComicsBridge extends BridgeAbstract
|
||||
{
|
||||
const MAINTAINER = 'sky';
|
||||
const MAINTAINER = 'TReKiE';
|
||||
//const MAINTAINER = 'sky';
|
||||
const NAME = 'GoComics Unofficial RSS';
|
||||
const URI = 'https://www.gocomics.com/';
|
||||
const CACHE_TIMEOUT = 21600; // 6h
|
||||
@ -13,32 +14,53 @@ class GoComicsBridge extends BridgeAbstract
|
||||
'type' => 'text',
|
||||
'exampleValue' => 'heartofthecity',
|
||||
'required' => true
|
||||
],
|
||||
'date-in-title' => [
|
||||
'name' => 'Add date and full name to each day\'s title',
|
||||
'type' => 'checkbox',
|
||||
'title' => 'Adds the date and the full name into the title of each day\'s comic',
|
||||
],
|
||||
'limit' => [
|
||||
'name' => 'Limit',
|
||||
'type' => 'number',
|
||||
'title' => 'The number of recent comics to get',
|
||||
'defaultValue' => 5
|
||||
]
|
||||
]];
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
$html = getSimpleHTMLDOM($this->getURI());
|
||||
$link = $this->getURI();
|
||||
|
||||
//Get info from first page
|
||||
$author = preg_replace('/By /', '', $html->find('.media-subheading', 0)->plaintext);
|
||||
for ($i = 0; $i < $this->getInput('limit'); $i++) {
|
||||
$html = getSimpleHTMLDOM($link);
|
||||
// get json data from the first page
|
||||
$json = $html->find('div.ComicViewer_comicViewer__comic__oftX6 script[type="application/ld+json"]', 0)->innertext;
|
||||
$data = json_decode($json, false);
|
||||
|
||||
$link = self::URI . $html->find('.gc-deck--cta-0', 0)->find('a', 0)->href;
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
$item = [];
|
||||
|
||||
$page = getSimpleHTMLDOM($link);
|
||||
$imagelink = $page->find('.comic.container', 0)->getAttribute('data-image');
|
||||
$date = explode('/', $link);
|
||||
$author = $data->author->name;
|
||||
$imagelink = $data->contentUrl;
|
||||
$date = $data->datePublished;
|
||||
$title = $data->name . ' - GoComics';
|
||||
|
||||
// get a permlink for this day's comic if there isn't one specified
|
||||
if ($link === $this->getURI()) {
|
||||
$link = $this->getURI() . '/' . DateTime::createFromFormat('F j, Y', $date)->format('Y/m/d');
|
||||
}
|
||||
|
||||
$item['id'] = $imagelink;
|
||||
$item['uri'] = $link;
|
||||
$item['author'] = $author;
|
||||
$item['title'] = 'GoComics ' . $this->getInput('comicname');
|
||||
$item['timestamp'] = DateTime::createFromFormat('Ymd', $date[5] . $date[6] . $date[7])->getTimestamp();
|
||||
if ($this->getInput('date-in-title') === true) {
|
||||
$item['title'] = $title;
|
||||
}
|
||||
$item['timestamp'] = DateTime::createFromFormat('F j, Y', $date)->setTime(0, 0, 0)->getTimestamp();
|
||||
$item['content'] = '<img src="' . $imagelink . '" />';
|
||||
|
||||
$link = self::URI . $page->find('.js-previous-comic', 0)->href;
|
||||
$link = rtrim(self::URI, '/') . $html->find('.Controls_controls__button_previous__P4LhX', 0)->href;
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
|
@ -171,8 +171,7 @@ class GogsBridge extends BridgeAbstract
|
||||
];
|
||||
|
||||
if ($this->getInput('include_description')) {
|
||||
$issue_html = getSimpleHTMLDOMCached($uri, 3600)
|
||||
or returnServerError('Unable to load issue description');
|
||||
$issue_html = getSimpleHTMLDOMCached($uri, 3600);
|
||||
|
||||
$issue_html = defaultLinkTo($issue_html, $uri);
|
||||
|
||||
|
@ -53,7 +53,7 @@ class GolemBridge extends FeedExpander
|
||||
]
|
||||
]];
|
||||
const LIMIT = 5;
|
||||
const HEADERS = ['Cookie: golem_consent20=simple|220101;'];
|
||||
const HEADERS = ['Cookie: golem_consent20=simple|250101;'];
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
|
@ -109,7 +109,7 @@ class GoogleScholarBridge extends BridgeAbstract
|
||||
case 'user':
|
||||
$userId = $this->getInput('userId');
|
||||
$uri = self::URI . '/citations?hl=en&view_op=list_works&sortby=pubdate&user=' . $userId;
|
||||
$html = getSimpleHTMLDOM($uri) or returnServerError('Could not fetch Google Scholar data.');
|
||||
$html = getSimpleHTMLDOM($uri);
|
||||
|
||||
$publications = $html->find('tr[class="gsc_a_tr"]');
|
||||
|
||||
@ -184,7 +184,7 @@ class GoogleScholarBridge extends BridgeAbstract
|
||||
$uri .= $sortBy ? '&scisbd=1' : '';
|
||||
$uri .= $numResults ? '&num=' . $numResults : '';
|
||||
|
||||
$html = getSimpleHTMLDOM($uri) or returnServerError('Could not fetch Google Scholar data.');
|
||||
$html = getSimpleHTMLDOM($uri);
|
||||
|
||||
$publications = $html->find('div[class="gs_r gs_or gs_scl"]');
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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' => '£',
|
||||
|
@ -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 .
|
||||
|
@ -438,8 +438,7 @@ class ItakuBridge extends BridgeAbstract
|
||||
private function getOwnerID($username)
|
||||
{
|
||||
$url = self::URI . "/api/user_profiles/{$username}/?format=json";
|
||||
$data = $this->getData($url, true, true)
|
||||
or returnServerError("Could not load $url");
|
||||
$data = $this->getData($url, true, true);
|
||||
|
||||
return $data['owner'];
|
||||
}
|
||||
@ -451,8 +450,7 @@ class ItakuBridge extends BridgeAbstract
|
||||
}
|
||||
$uri = self::URI . '/posts/' . $id;
|
||||
$url = self::URI . '/api/posts/' . $id . '/?format=json';
|
||||
$data = $metadata ?? $this->getData($url, true, true)
|
||||
or returnServerError("Could not load $url");
|
||||
$data = $metadata ?? $this->getData($url, true, true);
|
||||
|
||||
$content_str = nl2br($data['content']);
|
||||
$content = "<p>{$content_str}</p><br/>"; //TODO: Add link and itaku user mention detection and convert into links.
|
||||
@ -497,8 +495,7 @@ class ItakuBridge extends BridgeAbstract
|
||||
$content .= "<a href=\"{$url}\"><b>{$title}</b></a><br/>";
|
||||
if ($media['is_thumbnail_for_video']) {
|
||||
$url = self::URI . '/api/galleries/images/' . $media['id'] . '/?format=json';
|
||||
$media_data = $this->getData($url, true, true)
|
||||
or returnServerError("Could not load $url");
|
||||
$media_data = $this->getData($url, true, true);
|
||||
$content .= "<video controls src=\"{$media_data['video']['video']}\" poster=\"{$media['image_xl']}\"/>";
|
||||
} else {
|
||||
$content .= "<a href=\"{$url}\"><img src=\"{$src}\"></a>";
|
||||
@ -523,11 +520,11 @@ class ItakuBridge extends BridgeAbstract
|
||||
$url = self::URI . '/api/commissions/' . $id . '/?format=json';
|
||||
$uri = self::URI . '/commissions/' . $id;
|
||||
|
||||
$data = $metadata ?? $this->getData($url, true, true)
|
||||
or returnServerError("Could not load $url");
|
||||
$data = $metadata ?? $this->getData($url, true, true);
|
||||
|
||||
$content_str = nl2br($data['description']);
|
||||
$content = "<p>{$content_str}</p><br>"; //TODO: Add link and itaku user mention detection and convert into links.
|
||||
$content = "<p>{$content_str}</p><br>";
|
||||
//TODO: Add link and itaku user mention detection and convert into links.
|
||||
|
||||
if (array_key_exists('tags', $data) && count($data['tags']) > 0) {
|
||||
// $content .= "🏷 Tag(s): ";
|
||||
@ -570,8 +567,7 @@ class ItakuBridge extends BridgeAbstract
|
||||
$content .= "<a href=\"{$uri}\"><b>{$data['thumbnail_detail']['title']}</b></a><br/>";
|
||||
if ($data['thumbnail_detail']['is_thumbnail_for_video']) {
|
||||
$url = self::URI . '/api/galleries/images/' . $data['thumbnail_detail']['id'] . '/?format=json';
|
||||
$media_data = $this->getData($url, true, true)
|
||||
or returnServerError("Could not load $url");
|
||||
$media_data = $this->getData($url, true, true);
|
||||
$content .= "<video controls src=\"{$media_data['video']['video']}\" poster=\"{$data['thumbnail_detail']['image_lg']}\"/>";
|
||||
} else {
|
||||
$content .= "<a href=\"{$uri}\"><img src=\"{$data['thumbnail_detail']['image_lg']}\"></a>";
|
||||
@ -595,8 +591,7 @@ class ItakuBridge extends BridgeAbstract
|
||||
{
|
||||
$uri = self::URI . '/images/' . $id;
|
||||
$url = self::URI . '/api/galleries/images/' . $id . '/?format=json';
|
||||
$data = /* $metadata ?? */ $this->getData($url, true, true)
|
||||
or returnServerError("Could not load $url");
|
||||
$data = /* $metadata ?? */ $this->getData($url, true, true);
|
||||
|
||||
$content_str = nl2br($data['description']);
|
||||
$content = "<p>{$content_str}</p><br/>"; //TODO: Add link and itaku user mention detection and convert into links.
|
||||
@ -640,8 +635,7 @@ class ItakuBridge extends BridgeAbstract
|
||||
|
||||
if (array_key_exists('is_thumbnail_for_video', $data)) {
|
||||
$url = self::URI . '/api/galleries/images/' . $data['id'] . '/?format=json';
|
||||
$media_data = $this->getData($url, true, true)
|
||||
or returnServerError("Could not load $url");
|
||||
$media_data = $this->getData($url, true, true);
|
||||
$content .= "<video controls src=\"{$media_data['video']['video']}\" poster=\"{$data['image_xl']}\"/>";
|
||||
} else {
|
||||
if (array_key_exists('video', $data) && is_null($data['video'])) {
|
||||
|
@ -9,8 +9,7 @@ class JohannesBlickBridge extends BridgeAbstract
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
$html = getSimpleHTMLDOM(self::URI)
|
||||
or returnServerError('Could not request: ' . self::URI);
|
||||
$html = getSimpleHTMLDOM(self::URI);
|
||||
|
||||
$html = defaultLinkTo($html, self::URI);
|
||||
foreach ($html->find('ul[class=easyfolderlisting] > li > a') as $index => $a) {
|
||||
|
@ -181,8 +181,7 @@ class JustETFBridge extends BridgeAbstract
|
||||
if ($this->getInput('full')) {
|
||||
$uri = $this->extractNewsUri($article);
|
||||
|
||||
$html = getSimpleHTMLDOMCached($uri)
|
||||
or returnServerError('Failed loading full article from ' . $uri);
|
||||
$html = getSimpleHTMLDOMCached($uri);
|
||||
|
||||
$fullArticle = $html->find('div.article', 0)
|
||||
or returnServerError('No content found! Layout might have changed!');
|
||||
|
@ -64,10 +64,6 @@ Returns feeds for bug comments';
|
||||
DEFAULT_SPAN_TEXT
|
||||
);
|
||||
|
||||
if ($html === false) {
|
||||
returnServerError('Failed to load page!');
|
||||
}
|
||||
|
||||
$html = defaultLinkTo($html, self::URI);
|
||||
|
||||
// Store header information into private members
|
||||
|
@ -11,7 +11,7 @@ class LaTeX3ProjectNewslettersBridge extends BridgeAbstract
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
$html = getSimpleHTMLDOM(static::URI . '/news/latex3-news/') or returnServerError('No contents received!');
|
||||
$html = getSimpleHTMLDOM(static::URI . '/news/latex3-news/');
|
||||
$newsContainer = $html->find('article tbody', 0);
|
||||
|
||||
foreach ($newsContainer->find('tr') as $row) {
|
||||
|
118
bridges/LeagueOfLegendsNewsBridge.php
Normal file
118
bridges/LeagueOfLegendsNewsBridge.php
Normal 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;
|
||||
}
|
||||
}
|
@ -14,6 +14,37 @@ class LegifranceJOBridge extends BridgeAbstract
|
||||
private $timestamp;
|
||||
private $uri;
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
$html = getSimpleHTMLDOM(self::URI);
|
||||
|
||||
$title = $html->find('h2.titleJO', 0);
|
||||
|
||||
//$this->author = trim($title->plaintext);
|
||||
$uri1 = $html->find('h2.titleELI', 0);
|
||||
//$uri = $uri1->plaintext;
|
||||
//$this->uri = trim(substr($uri, strpos($uri, 'https')));
|
||||
$this->timestamp = strtotime(substr($this->uri, strpos($this->uri, 'eli/jo/') + strlen('eli/jo/'), -5));
|
||||
|
||||
foreach ($html->find('h3') as $section) {
|
||||
$subsections = $section->nextSibling()->find('h4');
|
||||
foreach ($subsections as $subsection) {
|
||||
$origins = $subsection->nextSibling()->find('h5');
|
||||
foreach ($origins as $origin) {
|
||||
$this->items[] = $this->extractItem($section, $subsection, $origin);
|
||||
}
|
||||
if (!empty($origins)) {
|
||||
continue;
|
||||
}
|
||||
$this->items[] = $this->extractItem($section, $subsection);
|
||||
}
|
||||
if (!empty($subsections)) {
|
||||
continue;
|
||||
}
|
||||
$this->items[] = $this->extractItem($section);
|
||||
}
|
||||
}
|
||||
|
||||
private function extractItem($section, $subsection = null, $origin = null)
|
||||
{
|
||||
$item = [];
|
||||
@ -35,7 +66,9 @@ class LegifranceJOBridge extends BridgeAbstract
|
||||
$item['content'] = '';
|
||||
foreach ($data->nextSibling()->find('a') as $content) {
|
||||
$text = $content->plaintext;
|
||||
$href = $content->nextSibling()->getAttribute('resource');
|
||||
$href = '';
|
||||
//$href = $content->nextSibling()->getAttribute('resource');
|
||||
|
||||
$item['content'] .= '<p><a href="' . $href . '">' . $text . '</a></p>';
|
||||
}
|
||||
return $item;
|
||||
@ -45,33 +78,4 @@ class LegifranceJOBridge extends BridgeAbstract
|
||||
{
|
||||
return 'https://www.legifrance.gouv.fr/img/favicon.ico';
|
||||
}
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
$html = getSimpleHTMLDOM(self::URI)
|
||||
or $this->returnServer('Unable to download ' . self::URI);
|
||||
|
||||
$this->author = trim($html->find('h2.titleJO', 0)->plaintext);
|
||||
$uri = $html->find('h2.titleELI', 0)->plaintext;
|
||||
$this->uri = trim(substr($uri, strpos($uri, 'https')));
|
||||
$this->timestamp = strtotime(substr($this->uri, strpos($this->uri, 'eli/jo/') + strlen('eli/jo/'), -5));
|
||||
|
||||
foreach ($html->find('h3') as $section) {
|
||||
$subsections = $section->nextSibling()->find('h4');
|
||||
foreach ($subsections as $subsection) {
|
||||
$origins = $subsection->nextSibling()->find('h5');
|
||||
foreach ($origins as $origin) {
|
||||
$this->items[] = $this->extractItem($section, $subsection, $origin);
|
||||
}
|
||||
if (!empty($origins)) {
|
||||
continue;
|
||||
}
|
||||
$this->items[] = $this->extractItem($section, $subsection);
|
||||
}
|
||||
if (!empty($subsections)) {
|
||||
continue;
|
||||
}
|
||||
$this->items[] = $this->extractItem($section);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
110
bridges/LfcPlBridge.php
Normal file
110
bridges/LfcPlBridge.php
Normal file
@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
class LfcPlBridge extends BridgeAbstract
|
||||
{
|
||||
const NAME = 'LFC (lfc.pl)';
|
||||
const DESCRIPTION = 'LFC.pl - największa polska strona o Liverpool FC';
|
||||
const URI = 'https://lfc.pl';
|
||||
const MAINTAINER = 'brtsos';
|
||||
const PARAMETERS = [
|
||||
[
|
||||
'comments' => [
|
||||
'type' => 'list',
|
||||
'name' => 'Include comments',
|
||||
'title' => 'Include comments in the article content',
|
||||
'values' => [
|
||||
'No' => 'no',
|
||||
'Yes' => 'yes',
|
||||
],
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
$dom = getSimpleHTMLDOM(self::URI . '/Archiwum/' . date('Y') . date('m'));
|
||||
|
||||
$list = $dom->find('#page .list-vertical li');
|
||||
$list = array_reverse($list);
|
||||
$list = array_slice($list, 0, 10);
|
||||
|
||||
foreach ($list as $li) {
|
||||
$link = $li->find('a', 0);
|
||||
$url = self::URI . $link->href;
|
||||
|
||||
$articleDom = getSimpleHTMLDOM($url);
|
||||
|
||||
$description = $this->getContent($articleDom);
|
||||
if (mb_strpos($description, 'Artykuł sponsorowany') !== false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$image = '<img src="' . $this->getImage($articleDom) . '" alt="' . $link->plaintext . '" />';
|
||||
|
||||
$content = $image . '</br>' . $description;
|
||||
|
||||
$tagsToRemove = ['script', 'iframe', 'input', 'form'];
|
||||
$content = sanitize($content, $tagsToRemove);
|
||||
|
||||
$footerArticle = $articleDom->find('.footer', 0)->find('.item', 0)->find('div', 1);
|
||||
$author = $footerArticle->find('a', 0)->plaintext;
|
||||
|
||||
$dateTime = $footerArticle->find('div', 0)->plaintext;
|
||||
$date = DateTime::createFromFormat('d.m.Y H:i', $dateTime);
|
||||
$timestamp = $date->getTimestamp();
|
||||
$this->items[] = [
|
||||
'title' => $link->plaintext,
|
||||
'uri' => $url,
|
||||
'timestamp' => $timestamp,
|
||||
'content' => $content,
|
||||
'author' => $author,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
private function getContent($article)
|
||||
{
|
||||
$content = $article->find('.news-body', 0)->innertext;
|
||||
$commentsHtml = $article->find('#comments', 0);
|
||||
|
||||
$comments = '';
|
||||
if ($this->withComment()) {
|
||||
if ($commentsHtml) {
|
||||
$commentsDom = $commentsHtml->find('.comment');
|
||||
|
||||
if (count($commentsDom) > 0) {
|
||||
$comments = '<h3>Komentarze:</h3>';
|
||||
}
|
||||
|
||||
foreach ($commentsDom as $comment) {
|
||||
$header = $comment->find('.header', 0)->plaintext;
|
||||
$commentContent = $comment->find('.content', 0)->plaintext;
|
||||
$comments .= $header . '<br />' . $commentContent . '<br /><br />';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $content . '<br /> <br />' . $comments;
|
||||
}
|
||||
|
||||
private function getImage($article): ?string
|
||||
{
|
||||
$imgElement = $article->find('#news .img', 0);
|
||||
if ($imgElement) {
|
||||
$style = $imgElement->style;
|
||||
|
||||
if (preg_match('/background-image:\s*url\(([^)]+)\)/i', $style, $matches)) {
|
||||
return self::URI . trim($matches[1], "'\"");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function withComment(): bool
|
||||
{
|
||||
return $this->getInput('comments') === 'yes';
|
||||
}
|
||||
}
|
@ -2,11 +2,11 @@
|
||||
|
||||
class ManyVidsBridge extends BridgeAbstract
|
||||
{
|
||||
const NAME = 'MANYVIDS';
|
||||
const NAME = 'ManyVids';
|
||||
const URI = 'https://www.manyvids.com';
|
||||
const DESCRIPTION = 'Fetches the latest posts from a profile';
|
||||
const MAINTAINER = 'dvikan';
|
||||
const CACHE_TIMEOUT = 60 * 60;
|
||||
const MAINTAINER = 'dvikan, subtle4553';
|
||||
const CACHE_TIMEOUT = 3600;
|
||||
const PARAMETERS = [
|
||||
[
|
||||
'profile' => [
|
||||
@ -19,31 +19,103 @@ class ManyVidsBridge extends BridgeAbstract
|
||||
]
|
||||
];
|
||||
|
||||
private ?simple_html_dom $htmlDom = null;
|
||||
private ?string $parsedProfileInput = null;
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
$profile = $this->getInput('profile');
|
||||
if (preg_match('#^(\d+/.*)$#', $profile, $m)) {
|
||||
$profile = $m[1];
|
||||
} elseif (preg_match('#https://www.manyvids.com/Profile/(\d+/\w+)#', $profile, $m)) {
|
||||
$profile = $m[1];
|
||||
} else {
|
||||
throw new \Exception('nope');
|
||||
if (!$profile) {
|
||||
throw new \Exception('No value for \'profile\' was provided.');
|
||||
}
|
||||
$url = sprintf('https://www.manyvids.com/Profile/%s/Store/Videos/', $profile);
|
||||
$dom = getSimpleHTMLDOM($url);
|
||||
$videos = $dom->find('div[class^="ProfileTabGrid_card"]');
|
||||
foreach ($videos as $item) {
|
||||
$a = $item->find('a', 1);
|
||||
$uri = 'https://www.manyvids.com' . $a->href;
|
||||
if (preg_match('#Video/(\d+)/#', $uri, $m)) {
|
||||
$uid = 'manyvids/' . $m[1];
|
||||
|
||||
if (preg_match('#^(\d+/.*)$#', $profile, $m)) {
|
||||
$this->parsedProfileInput = $m[1];
|
||||
} elseif (preg_match('#https://(www.)?manyvids.com/Profile/(\d+/.*?)/#', $profile, $m)) {
|
||||
$this->parsedProfileInput = $m[2];
|
||||
} else {
|
||||
throw new \Exception(sprintf('Profile could not be parsed: %s', $profile));
|
||||
}
|
||||
|
||||
$profileUrl = $this->getUri();
|
||||
$url = sprintf('%s?sort=newest', $profileUrl);
|
||||
$opt = [CURLOPT_COOKIE => 'sfwtoggle=false'];
|
||||
$this->htmlDom = getSimpleHTMLDOM($url, [], $opt);
|
||||
|
||||
$elements = $this->htmlDom->find('div[class^="ProfileTabGrid_card__"]');
|
||||
|
||||
foreach ($elements as $element) {
|
||||
$content = '';
|
||||
|
||||
$title = $element->find('span[class^="VideoCardUI_videoTitle__"] > a', 0);
|
||||
if (!$title) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$linkElement = $element->find('a[href^="/Video/"]', 0);
|
||||
if ($linkElement) {
|
||||
$itemUri = self::URI . $linkElement->getAttribute('href');
|
||||
}
|
||||
|
||||
$image = $element->find('img', 0);
|
||||
if ($image) {
|
||||
if (isset($itemUri)) {
|
||||
$content .= sprintf('<p><a href="%s"><img src="%s"></a></p>', $itemUri, $image->getAttribute('src'));
|
||||
} else {
|
||||
$content .= sprintf('<p><img src="%s"></p>', $image->getAttribute('src'));
|
||||
}
|
||||
}
|
||||
|
||||
$contentSegments = [];
|
||||
|
||||
$videoLength = $element->find('[class^="CardMedia_videoDuration__"] > span', 0);
|
||||
if ($videoLength) {
|
||||
$contentSegments[] = sprintf('%s', $videoLength->innertext);
|
||||
}
|
||||
|
||||
$price = $element->find('[class^="PriceUI_regularPrice__"], [class^="PriceUI_card_price__"] > p, [class^="PriceUI_card_free_text__"]', 0);
|
||||
$discountedPrice = $element->find('[class^="PriceUI_discountedPrice__"]', 0);
|
||||
|
||||
if ($price && $discountedPrice) {
|
||||
$contentSegments[] = sprintf('<s>%s</s> <strong>%s</strong>', $price->innertext, $discountedPrice->innertext);
|
||||
} elseif ($price && !$discountedPrice) {
|
||||
$contentSegments[] = sprintf('<strong>%s</strong>', $price->innertext);
|
||||
}
|
||||
|
||||
$content .= implode(' • ', $contentSegments);
|
||||
|
||||
$this->items[] = [
|
||||
'title' => $a->plaintext,
|
||||
'uri' => $uri,
|
||||
'uid' => $uid ?? $uri,
|
||||
'content' => $item->innertext,
|
||||
'title' => $title->innertext,
|
||||
'uri' => isset($itemUri) ? $itemUri : null,
|
||||
'content' => $content,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
public function getName()
|
||||
{
|
||||
if (!is_null($this->htmlDom)) {
|
||||
$profileNameElement = $this->htmlDom->find('[class^="ProfileAboutMeUI_stageName__"]', 0);
|
||||
if (!$profileNameElement) {
|
||||
return parent::getName();
|
||||
}
|
||||
|
||||
$profileNameElementContent = $profileNameElement->innertext;
|
||||
$index = strpos($profileNameElementContent, '<');
|
||||
$profileName = substr($profileNameElementContent, 0, $index);
|
||||
|
||||
return 'ManyVids: ' . $profileName;
|
||||
}
|
||||
|
||||
return parent::getName();
|
||||
}
|
||||
|
||||
public function getUri()
|
||||
{
|
||||
if (!is_null($this->parsedProfileInput)) {
|
||||
return sprintf('%s/Profile/%s/Store/Videos', self::URI, $this->parsedProfileInput);
|
||||
}
|
||||
|
||||
return parent::getUri();
|
||||
}
|
||||
}
|
||||
|
36
bridges/MinecraftBridge.php
Normal file
36
bridges/MinecraftBridge.php
Normal file
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
class MinecraftBridge extends BridgeAbstract
|
||||
{
|
||||
const NAME = 'Minecraft';
|
||||
const URI = 'https://www.minecraft.net';
|
||||
const DESCRIPTION = 'Catch up on the latest Minecraft articles';
|
||||
const MAINTAINER = 'tillcash';
|
||||
|
||||
public function getIcon()
|
||||
{
|
||||
return 'https://www.minecraft.net/etc.clientlibs/minecraftnet/clientlibs/clientlib-site/resources/favicon.ico';
|
||||
}
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
$json = getContents(
|
||||
'https://www.minecraft.net/content/minecraftnet/language-masters/en-us/_jcr_content.articles.page-1.json'
|
||||
);
|
||||
|
||||
$articles = json_decode($json);
|
||||
|
||||
if ($articles === null) {
|
||||
returnServerError('Failed to decode JSON content.');
|
||||
}
|
||||
|
||||
foreach ($articles->article_grid as $article) {
|
||||
$this->items[] = [
|
||||
'title' => $article->default_tile->title,
|
||||
'uid' => $article->article_url,
|
||||
'uri' => self::URI . $article->article_url,
|
||||
'content' => $article->default_tile->sub_header,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
@ -1,49 +1,80 @@
|
||||
<?php
|
||||
|
||||
class MixologyBridge extends FeedExpander
|
||||
class MixologyBridge extends BridgeAbstract
|
||||
{
|
||||
const MAINTAINER = 'swofl';
|
||||
const NAME = 'Mixology';
|
||||
const URI = 'https://mixology.eu';
|
||||
const CACHE_TIMEOUT = 6 * 60 * 60; // 6h
|
||||
const DESCRIPTION = 'Get latest blog posts from Mixology';
|
||||
const PARAMETERS = [ [
|
||||
'limit' => self::LIMIT,
|
||||
] ];
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
$feed_url = self::URI . '/feed';
|
||||
$limit = $this->getInput('limit') ?? 10;
|
||||
$this->collectExpandableDatas($feed_url, $limit);
|
||||
$html = getSimpleHTMLDOM(self::URI);
|
||||
|
||||
$teasers = [];
|
||||
$teaserElements = [];
|
||||
|
||||
$teaserElements[] = $html->find('.aufmacher .views-view-responsive-grid__item-inner', 0);
|
||||
foreach ($html->find('.block-views-blockmixology-frontpage-block-2 .views-col') as $teaser) {
|
||||
$teaserElements[] = $teaser;
|
||||
}
|
||||
|
||||
foreach ($teaserElements as $teaser) {
|
||||
$teasers[] = $this->parseTeaser($teaser);
|
||||
}
|
||||
|
||||
foreach ($teasers as $article) {
|
||||
$this->items[] = $this->parseItem($article);
|
||||
}
|
||||
}
|
||||
|
||||
protected function parseTeaser($teaser)
|
||||
{
|
||||
$result = [];
|
||||
|
||||
$title = $teaser->find('.views-field-title a', 0);
|
||||
$result['title'] = $title->plaintext;
|
||||
$result['uri'] = self::URI . $title->href;
|
||||
$result['enclosures'] = [];
|
||||
$result['enclosures'][] = self::URI . $teaser->find('img', 0)->src;
|
||||
$result['uid'] = hash('sha256', $result['title']);
|
||||
|
||||
$categories = $teaser->find('.views-field-field-kategorie', 0);
|
||||
if ($categories) {
|
||||
$result['categories'] = [];
|
||||
foreach ($categories->find('a') as $category) {
|
||||
$result['categories'][] = $category->innertext;
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
protected function parseItem(array $item)
|
||||
{
|
||||
$article = getSimpleHTMLDOMCached($item['uri']);
|
||||
|
||||
$content = '';
|
||||
|
||||
$headerImage = $article->find('div.edgtf-full-width img.wp-post-image', 0);
|
||||
|
||||
if (is_object($headerImage)) {
|
||||
$item['enclosures'] = [];
|
||||
$item['enclosures'][] = $headerImage->src;
|
||||
$content .= '<img src="' . $headerImage->src . '"/>';
|
||||
$authorLink = $article->find('.beitrag-author a', 0);
|
||||
if (!empty($authorLink)) {
|
||||
$item['author'] = $authorLink->plaintext;
|
||||
}
|
||||
|
||||
foreach ($article->find('article .wpb_content_element > .wpb_wrapper') as $element) {
|
||||
$timeElement = $article->find('.beitrag-date time', 0);
|
||||
if (!empty($timeElement)) {
|
||||
$item['timestamp'] = strtotime($timeElement->datetime);
|
||||
}
|
||||
|
||||
$content = '';
|
||||
|
||||
$content .= '<img src="' . $item['enclosures'][0] . '"/>';
|
||||
|
||||
foreach ($article->find('article .wpb_content_element>.wpb_wrapper, article .field--type-text-with-summary>.wp-block-columns>.wp-block-column') as $element) {
|
||||
$content .= $element->innertext;
|
||||
}
|
||||
|
||||
$item['content'] = $content;
|
||||
|
||||
$item['categories'] = [];
|
||||
|
||||
foreach ($article->find('.edgtf-tags > a') as $tag) {
|
||||
$item['categories'][] = $tag->plaintext;
|
||||
}
|
||||
|
||||
return $item;
|
||||
}
|
||||
}
|
||||
|
@ -19,14 +19,28 @@ class MondeDiploBridge extends BridgeAbstract
|
||||
|
||||
foreach ($html->find('div.unarticle') as $article) {
|
||||
$element = $article->parent();
|
||||
$title = $element->find('h3', 0)->plaintext;
|
||||
$datesAuteurs = $element->find('div.dates_auteurs', 0)->plaintext;
|
||||
$titleElement = $element->find('h3', 0);
|
||||
if (!$titleElement) {
|
||||
continue;
|
||||
}
|
||||
$title = $titleElement->plaintext;
|
||||
$datesAuteursElement = $element->find('div.dates_auteurs', 0);
|
||||
$datesAuteurs = is_null($datesAuteursElement) ? '' : $element->find('div.dates_auteurs', 0)->plaintext;
|
||||
$item = [];
|
||||
$item['uri'] = urljoin(self::URI, $element->href);
|
||||
$item['title'] = $this->cleanText($title) . ' - ' . $this->cleanText($datesAuteurs);
|
||||
$item['title'] = $this->getItemTitle($title, $datesAuteurs);
|
||||
$item['content'] = $this->cleanText(str_replace([$title, $datesAuteurs], '', $element->plaintext));
|
||||
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
private function getItemTitle($title, $datesAuteurs)
|
||||
{
|
||||
$itemTitle = $this->cleanText($title);
|
||||
if (strlen($datesAuteurs) > 0) {
|
||||
$itemTitle .= ' - ' . $this->cleanText($datesAuteurs);
|
||||
}
|
||||
return $itemTitle;
|
||||
}
|
||||
}
|
||||
|
@ -64,10 +64,6 @@ Returns feeds for bug comments';
|
||||
DEFAULT_SPAN_TEXT
|
||||
);
|
||||
|
||||
if ($html === false) {
|
||||
returnServerError('Failed to load page!');
|
||||
}
|
||||
|
||||
// Fix relative URLs
|
||||
defaultLinkTo($html, self::URI);
|
||||
|
||||
|
@ -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' => '€',
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -25,8 +25,7 @@ class OMonlineBridge extends BridgeAbstract
|
||||
$url = sprintf('%s', self::URI);
|
||||
}
|
||||
|
||||
$html = getSimpleHTMLDOM($url)
|
||||
or returnServerError('Could not request: ' . $url);
|
||||
$html = getSimpleHTMLDOM($url);
|
||||
|
||||
$html = defaultLinkTo($html, $url);
|
||||
|
||||
@ -35,8 +34,7 @@ class OMonlineBridge extends BridgeAbstract
|
||||
|
||||
$articlePath = $a->href;
|
||||
|
||||
$articlePageHtml = getSimpleHTMLDOMCached($articlePath, self::CACHE_TIMEOUT)
|
||||
or returnServerError('Could not request: ' . $articlePath);
|
||||
$articlePageHtml = getSimpleHTMLDOMCached($articlePath, self::CACHE_TIMEOUT);
|
||||
|
||||
$articlePageHtml = defaultLinkTo($articlePageHtml, self::URI);
|
||||
|
||||
|
61
bridges/OllamaBridge.php
Normal file
61
bridges/OllamaBridge.php
Normal file
@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
class OllamaBridge extends BridgeAbstract
|
||||
{
|
||||
const MAINTAINER = 'sqrtminusone';
|
||||
const NAME = 'Ollama Blog Bridge';
|
||||
const URI = 'https://ollama.com';
|
||||
|
||||
const CACHE_TIMEOUT = 3600; // 1 hour
|
||||
const DESCRIPTION = 'Returns latest blog posts from Ollama';
|
||||
|
||||
const PARAMETERS = [
|
||||
'' => [
|
||||
'limit' => [
|
||||
'name' => 'Limit',
|
||||
'type' => 'number',
|
||||
'required' => true,
|
||||
'defaultValue' => 10
|
||||
],
|
||||
]
|
||||
];
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
$html = getSimpleHTMLDOM(self::URI . '/blog/');
|
||||
$limit = $this->getInput('limit');
|
||||
|
||||
$posts = $html->find('main > section > a.group');
|
||||
for ($i = 0; $i < min(count($posts), $limit); $i++) {
|
||||
$post = $posts[$i];
|
||||
$title = $post->find('h2', 0)->plaintext;
|
||||
$date_text = $post->find('h3[datetime]', 0)->getAttribute('datetime');
|
||||
$timestamp = (new DateTime(mb_substr($date_text, 0, 19)))->format('U');
|
||||
$uri = self::URI . $post->getAttribute('href');
|
||||
$this->items[] = [
|
||||
'uri' => $uri,
|
||||
'title' => $title,
|
||||
'timestamp' => $timestamp,
|
||||
'content' => $this->parsePage($uri),
|
||||
'uid' => $uri
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
private function parsePage($uri)
|
||||
{
|
||||
$html = getSimpleHTMLDOMCached(
|
||||
$uri,
|
||||
86400,
|
||||
[],
|
||||
[],
|
||||
true,
|
||||
true,
|
||||
DEFAULT_TARGET_CHARSET,
|
||||
false // Do not strip \n from <code> blocks
|
||||
);
|
||||
$contents = $html->find('main > article > section.prose', 0);
|
||||
$contents = defaultLinkTo($contents, self::URI);
|
||||
return $contents->innertext;
|
||||
}
|
||||
}
|
@ -14,8 +14,8 @@ class OpenCVEBridge extends BridgeAbstract
|
||||
'instance' => [
|
||||
'name' => 'OpenCVE Instance',
|
||||
'required' => true,
|
||||
'defaultValue' => 'https://www.opencve.io',
|
||||
'exampleValue' => 'https://www.opencve.io'
|
||||
'defaultValue' => 'https://app.opencve.io',
|
||||
'exampleValue' => 'https://app.opencve.io'
|
||||
],
|
||||
'login' => [
|
||||
'name' => 'Login',
|
||||
@ -155,14 +155,14 @@ class OpenCVEBridge extends BridgeAbstract
|
||||
$titlePrefix = '[' . $queryName . '] ';
|
||||
}
|
||||
|
||||
foreach (json_decode($response) as $cveItem) {
|
||||
if (array_key_exists($cveItem->id, $fetchedIds)) {
|
||||
foreach (json_decode($response)->results as $cveItem) {
|
||||
if (array_key_exists($cveItem->cve_id, $fetchedIds)) {
|
||||
continue;
|
||||
}
|
||||
$fetchedIds[$cveItem->id] = true;
|
||||
$fetchedIds[$cveItem->cve_id] = true;
|
||||
$item = [
|
||||
'uri' => $instance . '/cve/' . $cveItem->id,
|
||||
'uid' => $cveItem->id,
|
||||
'uri' => $instance . '/cve/' . $cveItem->cve_id,
|
||||
'uid' => $cveItem->cve_id,
|
||||
];
|
||||
if ($this->getInput('upd_timestamp') == 1) {
|
||||
$item['timestamp'] = strtotime($cveItem->updated_at);
|
||||
@ -179,7 +179,7 @@ class OpenCVEBridge extends BridgeAbstract
|
||||
$item['content'] = $content;
|
||||
$item['title'] = $title;
|
||||
} else {
|
||||
$item['content'] = $cveItem->summary . $this->getLinks($cveItem->id);
|
||||
$item['content'] = $cveItem->description . $this->getLinks($cveItem->cve_id);
|
||||
$item['title'] = $this->getTitle($titlePrefix, $cveItem);
|
||||
}
|
||||
$this->items[] = $item;
|
||||
@ -193,17 +193,17 @@ class OpenCVEBridge extends BridgeAbstract
|
||||
|
||||
private function getTitle($titlePrefix, $cveItem)
|
||||
{
|
||||
$summary = $cveItem->summary;
|
||||
$summary = $cveItem->description;
|
||||
$limit = $this->getInput('limit');
|
||||
if ($limit && mb_strlen($summary) > 100) {
|
||||
$summary = mb_substr($summary, 0, $limit) + '...';
|
||||
}
|
||||
return $titlePrefix . $cveItem->id . '. ' . $summary;
|
||||
return $titlePrefix . $cveItem->cve_id . '. ' . $summary;
|
||||
}
|
||||
|
||||
private function fetchContents($cveItem, $titlePrefix, $instance, $authHeader)
|
||||
{
|
||||
$url = $instance . '/api/cve/' . $cveItem->id;
|
||||
$url = $instance . '/api/cve/' . $cveItem->cve_id;
|
||||
|
||||
$response = getContents($url, [$authHeader]);
|
||||
$datum = json_decode($response);
|
||||
@ -211,26 +211,36 @@ class OpenCVEBridge extends BridgeAbstract
|
||||
$title = $this->getTitleFromDatum($datum, $titlePrefix);
|
||||
|
||||
$result = self::CSS;
|
||||
$result .= '<h1>' . $cveItem->id . '</h1>';
|
||||
$result .= '<h1>' . $cveItem->cve_id . '</h1>';
|
||||
$result .= $this->getCVSSLabels($datum);
|
||||
$result .= '<p>' . $datum->summary . '</p>';
|
||||
$result .= '<p>' . $datum->description . '</p>';
|
||||
$result .= <<<EOD
|
||||
<h3>Information:</h3>
|
||||
<p>
|
||||
<ul>
|
||||
<li><b>Publication date</b>: {$datum->raw_nvd_data->published}
|
||||
<li><b>Last modified</b>: {$datum->raw_nvd_data->lastModified}
|
||||
<li><b>Last modified</b>: {$datum->raw_nvd_data->lastModified}
|
||||
<li><b>Created At</b>: {$datum->created_at}
|
||||
<li><b>Updated At</b>: {$datum->updated_at}
|
||||
</ul>
|
||||
</p>
|
||||
EOD;
|
||||
|
||||
$result .= $this->getV3Table($datum);
|
||||
$result .= $this->getV2Table($datum);
|
||||
if (isset($datum->metrics->cvssV4_0->data->vector)) {
|
||||
$result .= $this->cvssV4VectorToTable($datum->metrics->cvssV4_0->data->vector);
|
||||
}
|
||||
|
||||
$result .= $this->getLinks($datum->id);
|
||||
$result .= $this->getReferences($datum);
|
||||
if (isset($datum->metrics->cvssV3_1->data->vector)) {
|
||||
$result .= $this->cvssV3VectorToTable($datum->metrics->cvssV3_1->data->vector);
|
||||
}
|
||||
|
||||
if (isset($datum->metrics->cvssV3_0->data->vector)) {
|
||||
$result .= $this->cvssV3VectorToTable($datum->metrics->cvssV3_0->data->vector);
|
||||
}
|
||||
|
||||
if (isset($datum->metrics->cvssV2_0->data->vector)) {
|
||||
$result .= $this->cvssV2VectorToTable($datum->metrics->cvssV2_0->data->vector);
|
||||
}
|
||||
|
||||
$result .= $this->getLinks($datum->cve_id);
|
||||
$result .= $this->getVendors($datum);
|
||||
|
||||
return [$result, $title];
|
||||
@ -239,14 +249,20 @@ class OpenCVEBridge extends BridgeAbstract
|
||||
private function getTitleFromDatum($datum, $titlePrefix)
|
||||
{
|
||||
$title = $titlePrefix;
|
||||
if ($datum->cvss->v3) {
|
||||
$title .= "[v3: {$datum->cvss->v3}] ";
|
||||
if (isset($datum->metrics->cvssV4_0->data->score)) {
|
||||
$title .= "[v4: {$datum->metrics->cvssV4_0->data->score}] ";
|
||||
}
|
||||
if ($datum->cvss->v2) {
|
||||
$title .= "[v2: {$datum->cvss->v2}] ";
|
||||
if (isset($datum->metrics->cvssV3_1->data->score)) {
|
||||
$title .= "[v3.1: {$datum->metrics->cvssV3_1->data->score}] ";
|
||||
}
|
||||
$title .= $datum->id . '. ';
|
||||
$titlePostfix = $datum->summary;
|
||||
if (isset($datum->metrics->cvssV3_0->data->score)) {
|
||||
$title .= "[v3: {$datum->metrics->cvssV3_0->data->score}] ";
|
||||
}
|
||||
if (isset($datum->metrics->cvssV2_0->data->score)) {
|
||||
$title .= "[v2: {$datum->metrics->cvssV2_0->data->score}] ";
|
||||
}
|
||||
$title .= $datum->cve_id . '. ';
|
||||
$titlePostfix = $datum->description;
|
||||
$limit = $this->getInput('limit');
|
||||
if ($limit && mb_strlen($titlePostfix) > 100) {
|
||||
$titlePostfix = mb_substr($titlePostfix, 0, $limit) + '...';
|
||||
@ -257,64 +273,49 @@ class OpenCVEBridge extends BridgeAbstract
|
||||
|
||||
private function getCVSSLabels($datum)
|
||||
{
|
||||
$CVSSv2Text = 'n/a';
|
||||
$CVSSv2Class = 'cvss-na-color';
|
||||
if ($datum->cvss->v2) {
|
||||
$importance = '';
|
||||
if ($datum->cvss->v2 >= 7) {
|
||||
$importance = 'HIGH';
|
||||
$CVSSv2Class = 'cvss-high-color';
|
||||
} else if ($datum->cvss->v2 >= 4) {
|
||||
$importance = 'MEDIUM';
|
||||
$CVSSv2Class = 'cvss-medium-color';
|
||||
} else {
|
||||
$importance = 'LOW';
|
||||
$CVSSv2Class = 'cvss-low-color';
|
||||
}
|
||||
$CVSSv2Text = sprintf('[%s] %.1f', $importance, $datum->cvss->v2);
|
||||
$cvss4 = '';
|
||||
$cvss31 = '';
|
||||
$cvss3 = '';
|
||||
$cvss2 = '';
|
||||
if (isset($datum->metrics->cvssV4_0->data->score)) {
|
||||
$cvss4 = $this->formatCVSSLabel($datum->metrics->cvssV4_0->data->score, '4.0', 9, 7, 4);
|
||||
}
|
||||
if (isset($datum->metrics->cvssV3_1->data->score)) {
|
||||
$cvss31 = $this->formatCVSSLabel($datum->metrics->cvssV3_1->data->score, '3.1', 9, 7, 4);
|
||||
}
|
||||
if (isset($datum->metrics->cvssV3_0->data->score)) {
|
||||
$cvss3 = $this->formatCVSSLabel($datum->metrics->cvssV3_0->data->score, '3.0', 9, 7, 4);
|
||||
}
|
||||
if (isset($datum->metrics->cvssV2_0->data->score)) {
|
||||
$cvss2 = $this->formatCVSSLabel($datum->metrics->cvssV2_0->data->score, '2.0', 99, 7, 4);
|
||||
}
|
||||
$CVSSv2Item = "<div>CVSS v2: </div><div class=\"label {$CVSSv2Class}\">{$CVSSv2Text}</div>";
|
||||
|
||||
$CVSSv3Text = 'n/a';
|
||||
$CVSSv3Class = 'cvss-na-color';
|
||||
if ($datum->cvss->v3) {
|
||||
$importance = '';
|
||||
if ($datum->cvss->v3 >= 9) {
|
||||
$importance = 'CRITICAL';
|
||||
$CVSSv3Class = 'cvss-crit-color';
|
||||
} else if ($datum->cvss->v3 >= 7) {
|
||||
$importance = 'HIGH';
|
||||
$CVSSv3Class = 'cvss-high-color';
|
||||
} else if ($datum->cvss->v3 >= 4) {
|
||||
$importance = 'MEDIUM';
|
||||
$CVSSv3Class = 'cvss-medium-color';
|
||||
} else {
|
||||
$importance = 'LOW';
|
||||
$CVSSv3Class = 'cvss-low-color';
|
||||
}
|
||||
$CVSSv3Text = sprintf('[%s] %.1f', $importance, $datum->cvss->v3);
|
||||
}
|
||||
$CVSSv3Item = "<div>CVSS v3: </div><div class=\"label {$CVSSv3Class}\">{$CVSSv3Text}</div>";
|
||||
return '<div class="labels-row">' . $CVSSv3Item . $CVSSv2Item . '</div>';
|
||||
return '<div class="labels-row">' . $cvss4 . $cvss31 . $cvss3 . $cvss2 . '</div>';
|
||||
}
|
||||
|
||||
private function getReferences($datum)
|
||||
private function formatCVSSLabel($score, $version, $critical_thr, $high_thr, $medium_thr)
|
||||
{
|
||||
if (count($datum->raw_nvd_data->references) == 0) {
|
||||
return '';
|
||||
}
|
||||
$res = '<h3>References:</h3> <p><ul>';
|
||||
foreach ($datum->raw_nvd_data->references as $ref) {
|
||||
$item = '<li>';
|
||||
if (isset($ref->tags) && count($ref->tags) > 0) {
|
||||
$item .= '[' . implode(', ', $ref->tags) . '] ';
|
||||
$text = 'n/a';
|
||||
$class = 'cvss-na-color';
|
||||
if ($score) {
|
||||
$importance = '';
|
||||
if ($score >= $critical_thr) {
|
||||
$importance = 'CRITICAL';
|
||||
$class = 'cvss-crit-color';
|
||||
} else if ($score >= $high_thr) {
|
||||
$importance = 'HIGH';
|
||||
$class = 'cvss-high-color';
|
||||
} else if ($score >= $medium_thr) {
|
||||
$importance = 'MEDIUM';
|
||||
$class = 'cvss-medium-color';
|
||||
} else {
|
||||
$importance = 'LOW';
|
||||
$class = 'cvss-low-color';
|
||||
}
|
||||
$item .= "<a href=\"{$ref->url}\">{$ref->url}</a>";
|
||||
$item .= '<li>';
|
||||
$res .= $item;
|
||||
$text = sprintf('[%s] %.1f', $importance, $score);
|
||||
}
|
||||
$res .= '</p></ul>';
|
||||
return $res;
|
||||
$item = "<div>CVSS {$version}: </div><div class=\"label {$class}\">{$text}</div>";
|
||||
return $item;
|
||||
}
|
||||
|
||||
private function getLinks($id)
|
||||
@ -331,84 +332,253 @@ class OpenCVEBridge extends BridgeAbstract
|
||||
EOD;
|
||||
}
|
||||
|
||||
private function getV3Table($datum)
|
||||
private function cvssV3VectorToTable($cvssVector)
|
||||
{
|
||||
$metrics = $datum->raw_nvd_data->metrics;
|
||||
if (!isset($metrics->cvssMetricV31) || count($metrics->cvssMetricV31) == 0) {
|
||||
return '';
|
||||
$vectorComponents = [];
|
||||
$parts = explode('/', $cvssVector);
|
||||
|
||||
if (!preg_match('/^CVSS:3\.[01]/', $parts[0])) {
|
||||
return 'Error: Not a valid CVSS v3.0 or v3.1 vector';
|
||||
}
|
||||
$v3 = $metrics->cvssMetricV31[0];
|
||||
$data = $v3->cvssData;
|
||||
return <<<EOD
|
||||
<div class="cvss-table">
|
||||
|
||||
for ($i = 1; $i < count($parts); $i++) {
|
||||
$component = explode(':', $parts[$i]);
|
||||
if (count($component) == 2) {
|
||||
$vectorComponents[$component[0]] = $component[1];
|
||||
}
|
||||
}
|
||||
|
||||
$readableNames = [
|
||||
'AV' => ['N' => 'Network', 'A' => 'Adjacent', 'L' => 'Local', 'P' => 'Physical'],
|
||||
'AC' => ['L' => 'Low', 'H' => 'High'],
|
||||
'PR' => ['N' => 'None', 'L' => 'Low', 'H' => 'High'],
|
||||
'UI' => ['N' => 'None', 'R' => 'Required'],
|
||||
'S' => ['U' => 'Unchanged', 'C' => 'Changed'],
|
||||
'C' => ['N' => 'None', 'L' => 'Low', 'H' => 'High'],
|
||||
'I' => ['N' => 'None', 'L' => 'Low', 'H' => 'High'],
|
||||
'A' => ['N' => 'None', 'L' => 'Low', 'H' => 'High']
|
||||
];
|
||||
|
||||
$data = new stdClass();
|
||||
$data->attackVector = isset($readableNames['AV'][$vectorComponents['AV']]) ? $readableNames['AV'][$vectorComponents['AV']] : 'Unknown';
|
||||
$data->attackComplexity = isset($readableNames['AC'][$vectorComponents['AC']]) ? $readableNames['AC'][$vectorComponents['AC']] : 'Unknown';
|
||||
$data->privilegesRequired = isset($readableNames['PR'][$vectorComponents['PR']]) ? $readableNames['PR'][$vectorComponents['PR']] : 'Unknown';
|
||||
$data->userInteraction = isset($readableNames['UI'][$vectorComponents['UI']]) ? $readableNames['UI'][$vectorComponents['UI']] : 'Unknown';
|
||||
$data->scope = isset($readableNames['S'][$vectorComponents['S']]) ? $readableNames['S'][$vectorComponents['S']] : 'Unknown';
|
||||
$data->confidentialityImpact = isset($readableNames['C'][$vectorComponents['C']]) ? $readableNames['C'][$vectorComponents['C']] : 'Unknown';
|
||||
$data->integrityImpact = isset($readableNames['I'][$vectorComponents['I']]) ? $readableNames['I'][$vectorComponents['I']] : 'Unknown';
|
||||
$data->availabilityImpact = isset($readableNames['A'][$vectorComponents['A']]) ? $readableNames['A'][$vectorComponents['A']] : 'Unknown';
|
||||
|
||||
$html = '<div class="cvss-table">
|
||||
<h3>CVSS v3 details</h3>
|
||||
<table>
|
||||
<tr>
|
||||
<td>Impact score</td><td>{$v3->impactScore}</td>
|
||||
<td>Exploitability score</td><td>{$v3->exploitabilityScore}</td>
|
||||
<td>Attack vector</td><td>' . $data->attackVector . '</td>
|
||||
<td>Confidentiality Impact</td><td>' . $data->confidentialityImpact . '</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Attack vector</td><td>{$data->attackVector}</td>
|
||||
<td>Confidentiality Impact</td><td>{$data->confidentialityImpact}</td>
|
||||
<td>Attack complexity</td><td>' . $data->attackComplexity . '</td>
|
||||
<td>Integrity Impact</td><td>' . $data->integrityImpact . '</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Attack complexity</td><td>{$data->attackComplexity}</td>
|
||||
<td>Integrity Impact</td><td>{$data->integrityImpact}</td>
|
||||
<td>Privileges Required</td><td>' . $data->privilegesRequired . '</td>
|
||||
<td>Availability Impact</td><td>' . $data->availabilityImpact . '</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Privileges Required</td><td>{$data->privilegesRequired}</td>
|
||||
<td>Availability Impact</td><td>{$data->availabilityImpact}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>User Interaction</td><td>{$data->userInteraction}</td>
|
||||
<td>Scope</td><td>{$data->scope}</td>
|
||||
<td>User Interaction</td><td>' . $data->userInteraction . '</td>
|
||||
<td>Scope</td><td>' . $data->scope . '</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
EOD;
|
||||
</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function getV2Table($datum)
|
||||
private function cvssV2VectorToTable($cvssVector)
|
||||
{
|
||||
$metrics = $datum->raw_nvd_data->metrics;
|
||||
if (!isset($metrics->cvssMetricV2) || count($metrics->cvssMetricV2) == 0) {
|
||||
return '';
|
||||
$vectorComponents = [];
|
||||
$parts = explode('/', $cvssVector);
|
||||
|
||||
foreach ($parts as $part) {
|
||||
$component = explode(':', $part);
|
||||
if (count($component) == 2) {
|
||||
$vectorComponents[$component[0]] = $component[1];
|
||||
}
|
||||
}
|
||||
$v2 = $metrics->cvssMetricV2[0];
|
||||
$data = $v2->cvssData;
|
||||
return <<<EOD
|
||||
<div class="cvss-table">
|
||||
|
||||
$readableNames = [
|
||||
'AV' => ['L' => 'Local', 'A' => 'Adjacent Network', 'N' => 'Network'],
|
||||
'AC' => ['H' => 'High', 'M' => 'Medium', 'L' => 'Low'],
|
||||
'Au' => ['M' => 'Multiple', 'S' => 'Single', 'N' => 'None'],
|
||||
'C' => ['N' => 'None', 'P' => 'Partial', 'C' => 'Complete'],
|
||||
'I' => ['N' => 'None', 'P' => 'Partial', 'C' => 'Complete'],
|
||||
'A' => ['N' => 'None', 'P' => 'Partial', 'C' => 'Complete']
|
||||
];
|
||||
|
||||
$metricValues = [
|
||||
'AV' => ['L' => 0.395, 'A' => 0.646, 'N' => 1.0],
|
||||
'AC' => ['H' => 0.35, 'M' => 0.61, 'L' => 0.71],
|
||||
'Au' => ['M' => 0.45, 'S' => 0.56, 'N' => 0.704],
|
||||
'C' => ['N' => 0, 'P' => 0.275, 'C' => 0.660],
|
||||
'I' => ['N' => 0, 'P' => 0.275, 'C' => 0.660],
|
||||
'A' => ['N' => 0, 'P' => 0.275, 'C' => 0.660]
|
||||
];
|
||||
|
||||
$confImpact = isset($metricValues['C'][$vectorComponents['C']]) ? $metricValues['C'][$vectorComponents['C']] : 0;
|
||||
$integImpact = isset($metricValues['I'][$vectorComponents['I']]) ? $metricValues['I'][$vectorComponents['I']] : 0;
|
||||
$availImpact = isset($metricValues['A'][$vectorComponents['A']]) ? $metricValues['A'][$vectorComponents['A']] : 0;
|
||||
|
||||
$impact = 10.41 * (1 - (1 - $confImpact) * (1 - $integImpact) * (1 - $availImpact));
|
||||
|
||||
$av = isset($metricValues['AV'][$vectorComponents['AV']]) ? $metricValues['AV'][$vectorComponents['AV']] : 0;
|
||||
$ac = isset($metricValues['AC'][$vectorComponents['AC']]) ? $metricValues['AC'][$vectorComponents['AC']] : 0;
|
||||
$au = isset($metricValues['Au'][$vectorComponents['Au']]) ? $metricValues['Au'][$vectorComponents['Au']] : 0;
|
||||
|
||||
$exploitability = 20 * $av * $ac * $au;
|
||||
|
||||
$impact = round($impact, 1);
|
||||
$exploitability = round($exploitability, 1);
|
||||
|
||||
$data = new stdClass();
|
||||
$data->accessVector = isset($readableNames['AV'][$vectorComponents['AV']]) ? $readableNames['AV'][$vectorComponents['AV']] : 'Unknown';
|
||||
$data->accessComplexity = isset($readableNames['AC'][$vectorComponents['AC']]) ? $readableNames['AC'][$vectorComponents['AC']] : 'Unknown';
|
||||
$data->authentication = isset($readableNames['Au'][$vectorComponents['Au']]) ? $readableNames['Au'][$vectorComponents['Au']] : 'Unknown';
|
||||
$data->confidentialityImpact = isset($readableNames['C'][$vectorComponents['C']]) ? $readableNames['C'][$vectorComponents['C']] : 'Unknown';
|
||||
$data->integrityImpact = isset($readableNames['I'][$vectorComponents['I']]) ? $readableNames['I'][$vectorComponents['I']] : 'Unknown';
|
||||
$data->availabilityImpact = isset($readableNames['A'][$vectorComponents['A']]) ? $readableNames['A'][$vectorComponents['A']] : 'Unknown';
|
||||
|
||||
$v2 = new stdClass();
|
||||
$v2->impactScore = $impact;
|
||||
$v2->exploitabilityScore = $exploitability;
|
||||
|
||||
$html = '<div class="cvss-table">
|
||||
<h3>CVSS v2 details</h3>
|
||||
<table>
|
||||
<tr>
|
||||
<td>Impact score</td><td>{$v2->impactScore}</td>
|
||||
<td>Exploitability score</td><td>{$v2->exploitabilityScore}</td>
|
||||
<td>Impact score</td><td>' . $v2->impactScore . '</td>
|
||||
<td>Exploitability score</td><td>' . $v2->exploitabilityScore . '</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Access Vector</td><td>{$data->accessVector}</td>
|
||||
<td>Confidentiality Impact</td><td>{$data->confidentialityImpact}</td>
|
||||
<td>Access Vector</td><td>' . $data->accessVector . '</td>
|
||||
<td>Confidentiality Impact</td><td>' . $data->confidentialityImpact . '</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Access Complexity</td><td>{$data->accessComplexity}</td>
|
||||
<td>Integrity Impact</td><td>{$data->integrityImpact}</td>
|
||||
<td>Access Complexity</td><td>' . $data->accessComplexity . '</td>
|
||||
<td>Integrity Impact</td><td>' . $data->integrityImpact . '</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Authentication</td><td>{$data->authentication}</td>
|
||||
<td>Availability Impact</td><td>{$data->availabilityImpact}</td>
|
||||
<td>Authentication</td><td>' . $data->authentication . '</td>
|
||||
<td>Availability Impact</td><td>' . $data->availabilityImpact . '</td>
|
||||
</tr>
|
||||
<tr>
|
||||
</table>
|
||||
</div>
|
||||
EOD;
|
||||
</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function cvssV4VectorToTable($cvssVector)
|
||||
{
|
||||
$vectorComponents = [];
|
||||
$parts = explode('/', $cvssVector);
|
||||
|
||||
if (!preg_match('/^CVSS:4\.0/', $parts[0])) {
|
||||
return 'Error: Not a valid CVSS v4.0 vector';
|
||||
}
|
||||
|
||||
for ($i = 1; $i < count($parts); $i++) {
|
||||
$component = explode(':', $parts[$i]);
|
||||
if (count($component) == 2) {
|
||||
$vectorComponents[$component[0]] = $component[1];
|
||||
}
|
||||
}
|
||||
|
||||
$readableNames = [
|
||||
'AV' => ['N' => 'Network', 'A' => 'Adjacent', 'L' => 'Local', 'P' => 'Physical'],
|
||||
'AC' => ['L' => 'Low', 'H' => 'High'],
|
||||
'AT' => ['N' => 'None', 'P' => 'Present'],
|
||||
'PR' => ['N' => 'None', 'L' => 'Low', 'H' => 'High'],
|
||||
'UI' => ['N' => 'None', 'P' => 'Passive', 'A' => 'Active'],
|
||||
'VC' => ['N' => 'None', 'L' => 'Low', 'H' => 'High'],
|
||||
'VI' => ['N' => 'None', 'L' => 'Low', 'H' => 'High'],
|
||||
'VA' => ['N' => 'None', 'L' => 'Low', 'H' => 'High'],
|
||||
'SC' => ['N' => 'None', 'L' => 'Low', 'H' => 'High'],
|
||||
'SI' => ['N' => 'None', 'L' => 'Low', 'H' => 'High'],
|
||||
'SA' => ['N' => 'None', 'L' => 'Low', 'H' => 'High']
|
||||
];
|
||||
|
||||
$data = new stdClass();
|
||||
$data->attackVector = isset($readableNames['AV'][$vectorComponents['AV']]) ? $readableNames['AV'][$vectorComponents['AV']] : 'Unknown';
|
||||
$data->attackComplexity = isset($readableNames['AC'][$vectorComponents['AC']]) ? $readableNames['AC'][$vectorComponents['AC']] : 'Unknown';
|
||||
$data->privilegesRequired = isset($readableNames['PR'][$vectorComponents['PR']]) ? $readableNames['PR'][$vectorComponents['PR']] : 'Unknown';
|
||||
$data->attackRequirements = isset($readableNames['AT'][$vectorComponents['AT']]) ? $readableNames['AT'][$vectorComponents['AT']] : 'Unknown';
|
||||
$data->userInteraction = isset($readableNames['UI'][$vectorComponents['UI']]) ? $readableNames['UI'][$vectorComponents['UI']] : 'Unknown';
|
||||
$data->confidentialityImpact = isset($readableNames['VC'][$vectorComponents['VC']]) ? $readableNames['VC'][$vectorComponents['VC']] : 'Unknown';
|
||||
$data->integrityImpact = isset($readableNames['VI'][$vectorComponents['VI']]) ? $readableNames['VI'][$vectorComponents['VI']] : 'Unknown';
|
||||
$data->availabilityImpact = isset($readableNames['VA'][$vectorComponents['VA']]) ? $readableNames['VA'][$vectorComponents['VA']] : 'Unknown';
|
||||
$data->confidentialityImpactS = isset($readableNames['SC'][$vectorComponents['SC']]) ? $readableNames['SC'][$vectorComponents['SC']] : 'Unknown';
|
||||
$data->integrityImpactS = isset($readableNames['SI'][$vectorComponents['SI']]) ? $readableNames['SI'][$vectorComponents['SI']] : 'Unknown';
|
||||
$data->availabilityImpactS = isset($readableNames['SA'][$vectorComponents['SA']]) ? $readableNames['SA'][$vectorComponents['SA']] : 'Unknown';
|
||||
|
||||
$html = '<div class="cvss-table">
|
||||
<h3>CVSS v4.0 details</h3>
|
||||
<table>
|
||||
<tr>
|
||||
<td>Attack vector</td><td>' . $data->attackVector . '</td>
|
||||
<td>Vulnerable System Confidentiality Impact</td><td>' . $data->confidentialityImpact . '</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Attack complexity</td><td>' . $data->attackComplexity . '</td>
|
||||
<td>Vulnerable System Integrity Impact</td><td>' . $data->integrityImpact . '</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Privileges Required</td><td>' . $data->privilegesRequired . '</td>
|
||||
<td>Vulnerable System Availability Impact</td><td>' . $data->availabilityImpact . '</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Attack Requirements</td><td>' . $data->attackRequirements . '</td>
|
||||
<td>Subsequent System Confidentiality Impact</td><td>' . $data->confidentialityImpactS . '</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>User Interaction</td><td>' . $data->userInteraction . '</td>
|
||||
<td>Subsequent System Integrity Impact</td><td>' . $data->integrityImpactS . '</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td><td></td>
|
||||
<td>Subsequent System Avaliablity Impact</td><td>' . $data->availabilityImpactS . '</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
|
||||
private function getVendors($datum)
|
||||
{
|
||||
if (count((array)$datum->vendors) == 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$vendor_data = [];
|
||||
foreach ($datum->vendors as $vendor_str) {
|
||||
$pieces = explode('$PRODUCT$', $vendor_str);
|
||||
if (count($pieces) == 1) {
|
||||
$vendor = $pieces[0];
|
||||
if (!array_key_exists($vendor, $vendor_data)) {
|
||||
$vendor_data[$vendor] = [];
|
||||
}
|
||||
} else {
|
||||
$vendor = $pieces[0];
|
||||
$product = $pieces[1];
|
||||
if (!array_key_exists($vendor, $vendor_data)) {
|
||||
$vendor_data[$vendor] = [];
|
||||
}
|
||||
array_push($vendor_data[$vendor], $product);
|
||||
}
|
||||
}
|
||||
|
||||
$res = '<h3>Affected products</h3><p><ul>';
|
||||
foreach ($datum->vendors as $vendor => $products) {
|
||||
foreach ($vendor_data as $vendor => $products) {
|
||||
$res .= "<li>{$vendor}";
|
||||
if (count($products) > 0) {
|
||||
$res .= '<ul>';
|
||||
@ -420,5 +590,6 @@ class OpenCVEBridge extends BridgeAbstract
|
||||
$res .= '</li>';
|
||||
}
|
||||
$res .= '</ul></p>';
|
||||
return $res;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -20,7 +20,15 @@ class RedditBridge extends BridgeAbstract
|
||||
'required' => false,
|
||||
'type' => 'number',
|
||||
'exampleValue' => 100,
|
||||
'title' => 'Filter out posts with lower score'
|
||||
'title' => 'Filter out posts with lower score. Set to -1 to disable. If both score and comments are set, an OR is applied.',
|
||||
],
|
||||
'min_comments' => [
|
||||
'name' => 'Minimal number of comments',
|
||||
'required' => false,
|
||||
'type' => 'number',
|
||||
'exampleValue' => 100,
|
||||
'title' => 'Filter out posts with lower number of comments. Set to -1 to disable. If both score and comments are set, an OR is applied.',
|
||||
'defaultValue' => -1
|
||||
],
|
||||
'd' => [
|
||||
'name' => 'Sort By',
|
||||
@ -30,10 +38,25 @@ class RedditBridge extends BridgeAbstract
|
||||
'Hot' => 'hot',
|
||||
'Relevance' => 'relevance',
|
||||
'New' => 'new',
|
||||
'Top' => 'top'
|
||||
'Top' => 'top',
|
||||
'Comments' => 'comments',
|
||||
],
|
||||
'defaultValue' => 'Hot'
|
||||
],
|
||||
't' => [
|
||||
'name' => 'Time',
|
||||
'type' => 'list',
|
||||
'title' => 'Sort by new, hot, top or relevancy',
|
||||
'values' => [
|
||||
'All' => 'all',
|
||||
'Year' => 'year',
|
||||
'Month' => 'month',
|
||||
'Week' => 'week',
|
||||
'Day' => 'day',
|
||||
'Hour' => 'hour',
|
||||
],
|
||||
'defaultValue' => 'week'
|
||||
],
|
||||
'search' => [
|
||||
'name' => 'Keyword search',
|
||||
'required' => false,
|
||||
@ -126,6 +149,7 @@ class RedditBridge extends BridgeAbstract
|
||||
$frontend = 'https://old.reddit.com';
|
||||
}
|
||||
$section = $this->getInput('d');
|
||||
$time = $this->getInput('t');
|
||||
|
||||
switch ($this->queriedContext) {
|
||||
case 'single':
|
||||
@ -147,7 +171,7 @@ class RedditBridge extends BridgeAbstract
|
||||
foreach ($subreddits as $subreddit) {
|
||||
$version = 'v0.0.2';
|
||||
$useragent = "rss-bridge $version (https://github.com/RSS-Bridge/rss-bridge)";
|
||||
$url = self::createUrl($search, $flareInput, $subreddit, $user, $section, $this->queriedContext);
|
||||
$url = self::createUrl($search, $flareInput, $subreddit, $user, $section, $time, $this->queriedContext);
|
||||
|
||||
$response = getContents($url, ['User-Agent: ' . $useragent], [], true);
|
||||
|
||||
@ -162,8 +186,20 @@ class RedditBridge extends BridgeAbstract
|
||||
|
||||
$data = $post->data;
|
||||
|
||||
if ($data->score < $this->getInput('score')) {
|
||||
continue;
|
||||
$min_score = $this->getInput('score');
|
||||
$min_comments = $this->getInput('min_comments');
|
||||
if ($min_score >= 0 && $min_comments >= 0) {
|
||||
if ($data->num_comments < $min_comments || $data->score < $min_score) {
|
||||
continue;
|
||||
}
|
||||
} elseif ($min_score >= 0) {
|
||||
if ($data->score < $min_score) {
|
||||
continue;
|
||||
}
|
||||
} elseif ($min_comments >= 0) {
|
||||
if ($data->num_comments < $min_comments) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$item = [];
|
||||
@ -234,11 +270,14 @@ class RedditBridge extends BridgeAbstract
|
||||
} elseif ($data->is_video) {
|
||||
// Video
|
||||
|
||||
// Higher index -> Higher resolution
|
||||
end($data->preview->images[0]->resolutions);
|
||||
$index = key($data->preview->images[0]->resolutions);
|
||||
|
||||
$item['content'] = $this->createFigureLink($data->url, $data->preview->images[0]->resolutions[$index]->url, 'Video');
|
||||
if ($data->media->reddit_video) {
|
||||
$item['content'] = $this->createVideoContent($data->media->reddit_video);
|
||||
} else {
|
||||
// Higher index -> Higher resolution
|
||||
end($data->preview->images[0]->resolutions);
|
||||
$index = key($data->preview->images[0]->resolutions);
|
||||
$item['content'] = $this->createFigureLink($data->url, $data->preview->images[0]->resolutions[$index]->url, 'Video');
|
||||
}
|
||||
} elseif (isset($data->media) && $data->media->type == 'youtube.com') {
|
||||
// Youtube link
|
||||
$item['content'] = $this->createFigureLink($data->url, $data->media->oembed->thumbnail_url, 'YouTube');
|
||||
@ -261,7 +300,7 @@ class RedditBridge extends BridgeAbstract
|
||||
});
|
||||
}
|
||||
|
||||
public static function createUrl($search, $flareInput, $subreddit, bool $user, $section, $queriedContext): string
|
||||
public static function createUrl($search, $flareInput, $subreddit, bool $user, $section, $time, $queriedContext): string
|
||||
{
|
||||
if ($search === '') {
|
||||
$keywords = '';
|
||||
@ -283,6 +322,7 @@ class RedditBridge extends BridgeAbstract
|
||||
'q' => $keywords . $flair . ($user ? 'author:' : 'subreddit:') . $name,
|
||||
'sort' => $section,
|
||||
'include_over_18' => 'on',
|
||||
't' => $time
|
||||
];
|
||||
return 'https://old.reddit.com/search.json?' . http_build_query($query);
|
||||
}
|
||||
@ -318,6 +358,16 @@ class RedditBridge extends BridgeAbstract
|
||||
return sprintf('<a href="%s">%s</a>', $href, $text);
|
||||
}
|
||||
|
||||
private function createVideoContent(\stdClass $video): string
|
||||
{
|
||||
return <<<HTML
|
||||
<video width="$video->width" height="$video->height" controls>
|
||||
<source src="$video->fallback_url" type="video/mp4">
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
HTML;
|
||||
}
|
||||
|
||||
public function detectParameters($url)
|
||||
{
|
||||
try {
|
||||
|
@ -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'
|
||||
|
@ -60,15 +60,10 @@ class RumbleBridge extends BridgeAbstract
|
||||
|
||||
$dom = getSimpleHTMLDOM($url);
|
||||
foreach ($dom->find('ol.thumbnail__grid div.thumbnail__grid--item') as $video) {
|
||||
$itemUrlString = self::URI . $video->find('a', 0)->href;
|
||||
$itemUrl = Url::fromString($itemUrlString);
|
||||
$href = $video->find('a', 0)->href;
|
||||
|
||||
$item = [
|
||||
'title' => $video->find('h3', 0)->plaintext,
|
||||
|
||||
// Remove tracking parameter in query string
|
||||
'uri' => $itemUrl->withQueryString(null)->__toString(),
|
||||
|
||||
'author' => $account . '@rumble.com',
|
||||
'content' => defaultLinkTo($video, self::URI)->innertext,
|
||||
];
|
||||
@ -78,6 +73,12 @@ class RumbleBridge extends BridgeAbstract
|
||||
$publishedAt = new \DateTimeImmutable($time->getAttribute('datetime'));
|
||||
$item['timestamp'] = $publishedAt->getTimestamp();
|
||||
}
|
||||
|
||||
$href = ltrim($href, '/');
|
||||
$itemUrl = Url::fromString(self::URI . $href);
|
||||
// Remove tracking parameter in query string
|
||||
$item['uri'] = $itemUrl->withQueryString(null)->__toString();
|
||||
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
|
@ -132,7 +132,7 @@ class RutubeBridge extends BridgeAbstract
|
||||
$video->description . ' '
|
||||
)
|
||||
);
|
||||
$item['timestamp'] = $video->created_ts;
|
||||
$item['timestamp'] = $video->publication_ts;
|
||||
$item['author'] = $video->author->name;
|
||||
$item['content'] = $content;
|
||||
|
||||
|
@ -49,8 +49,7 @@ class SchweinfurtBuergerinformationenBridge extends BridgeAbstract
|
||||
private function getArticleIDsFromPage($page)
|
||||
{
|
||||
$url = sprintf(self::URI . '?art_pager=%d', $page);
|
||||
$html = getSimpleHTMLDOMCached($url, self::INDEX_CACHE_TIMEOUT)
|
||||
or returnServerError('Could not retrieve ' . $url);
|
||||
$html = getSimpleHTMLDOMCached($url, self::INDEX_CACHE_TIMEOUT);
|
||||
|
||||
$articles = $html->find('div.artikel-uebersicht');
|
||||
$articleIDs = [];
|
||||
@ -70,8 +69,7 @@ class SchweinfurtBuergerinformationenBridge extends BridgeAbstract
|
||||
private function generateItemFromArticle($id)
|
||||
{
|
||||
$url = sprintf(self::ARTICLE_URI, $id);
|
||||
$html = getSimpleHTMLDOMCached($url, self::ARTICLE_CACHE_TIMEOUT)
|
||||
or returnServerError('Could not retrieve ' . $url);
|
||||
$html = getSimpleHTMLDOMCached($url, self::ARTICLE_CACHE_TIMEOUT);
|
||||
|
||||
$div = $html->find('div#artikel-detail', 0);
|
||||
$divContent = $div->find('.c-content', 0);
|
||||
|
100
bridges/ShadertoyBridge.php
Normal file
100
bridges/ShadertoyBridge.php
Normal 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,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
@ -633,8 +633,7 @@ class SkimfeedBridge extends BridgeAbstract
|
||||
$author = '<a href="' . $anchor->href . '">' . trim($anchor->plaintext) . '</a>';
|
||||
$uri = $anchor->href;
|
||||
|
||||
$box_html = getSimpleHTMLDOM($uri)
|
||||
or returnServerError('Could not load custom feed!');
|
||||
$box_html = getSimpleHTMLDOM($uri);
|
||||
|
||||
$this->extractFeed($box_html, $author);
|
||||
}
|
||||
@ -665,8 +664,7 @@ class SkimfeedBridge extends BridgeAbstract
|
||||
*/
|
||||
private function exportBoxChannels()
|
||||
{
|
||||
$html = getSimpleHTMLDOMCached(static::URI)
|
||||
or returnServerError('No contents received from Skimfeed!');
|
||||
$html = getSimpleHTMLDOMCached(static::URI);
|
||||
|
||||
if (!$this->isCompatible($html)) {
|
||||
returnServerError('Skimfeed version is not compatible!');
|
||||
@ -722,8 +720,7 @@ EOD;
|
||||
*/
|
||||
private function exportTechChannels()
|
||||
{
|
||||
$html = getSimpleHTMLDOMCached(static::URI)
|
||||
or returnServerError('No contents received from Skimfeed!');
|
||||
$html = getSimpleHTMLDOMCached(static::URI);
|
||||
|
||||
if (!$this->isCompatible($html)) {
|
||||
returnServerError('Skimfeed version is not compatible!');
|
||||
@ -759,8 +756,7 @@ EOD;
|
||||
|
||||
$message .= "\t\t'{$title}' => array(\n";
|
||||
|
||||
$channel_html = getSimpleHTMLDOMCached(static::URI . $uri)
|
||||
or returnServerError('Could not load tech channel ' . $channel->plaintext . '!');
|
||||
$channel_html = getSimpleHTMLDOMCached(static::URI . $uri);
|
||||
|
||||
$boxes = $channel_html->find('#boxx .boxes')
|
||||
or returnServerError('Could not find boxes!');
|
||||
|
@ -30,8 +30,7 @@ class StanfordSIRbookreviewBridge extends BridgeAbstract
|
||||
break;
|
||||
}
|
||||
|
||||
$html = getSimpleHTMLDOM($url)
|
||||
or returnServerError('Failed loading content!');
|
||||
$html = getSimpleHTMLDOM($url);
|
||||
foreach ($html->find('article') as $element) {
|
||||
$item = [];
|
||||
$item['title'] = $element->find('div > h4 > a', 0)->plaintext;
|
||||
|
@ -65,7 +65,7 @@ class StockFilingsBridge extends FeedExpander
|
||||
{
|
||||
$uri = $this->getSearchUrl();
|
||||
|
||||
return getSimpleHTMLDOM($uri) ?: returnServerError('Could not request SEC.');
|
||||
return getSimpleHTMLDOM($uri);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -25,9 +25,6 @@ class StorytelBridge extends BridgeAbstract
|
||||
}
|
||||
|
||||
$html = getSimpleHTMLDOM($url);
|
||||
if (!$html) {
|
||||
returnServerError('Unable to fetch Storytel list');
|
||||
}
|
||||
|
||||
foreach ($html->find('li.sc-4615116a-1') as $element) {
|
||||
$item = [];
|
||||
|
209
bridges/SubstackProfileBridge.php
Normal file
209
bridges/SubstackProfileBridge.php
Normal file
@ -0,0 +1,209 @@
|
||||
<?php
|
||||
|
||||
class SubstackProfileBridge extends BridgeAbstract
|
||||
{
|
||||
const NAME = 'Substack Profile';
|
||||
const MAINTAINER = 'phantop';
|
||||
const URI = 'https://substack.com/';
|
||||
const DESCRIPTION = 'Returns posts from profiles on Substack';
|
||||
const PARAMETERS = [[
|
||||
'profile' => [
|
||||
'name' => 'Profile name to use',
|
||||
'exampleValue' => 'taliabhatt',
|
||||
],
|
||||
]];
|
||||
|
||||
private $name;
|
||||
private $icon;
|
||||
public function collectData()
|
||||
{
|
||||
$html = getSimpleHTMLDOMCached($this->getURI());
|
||||
preg_match('/<script>window\._preloads\s*= JSON\.parse\("(.+?)"\)\s*<\/script>/', $html, $preg);
|
||||
$json = stripcslashes($preg[1]);
|
||||
$profile = json_decode($json, true)['profile'];
|
||||
|
||||
$this->name = $profile['name'];
|
||||
$this->icon = $profile['photo_url'];
|
||||
|
||||
$id = $profile['id'];
|
||||
$json = getContents(parent::getURI() . "api/v1/reader/feed/profile/$id");
|
||||
foreach (json_decode($json, true)['items'] as $element) {
|
||||
$this->items[] = $this->processAttachment($element);
|
||||
}
|
||||
}
|
||||
|
||||
private function processAttachment(array $element)
|
||||
{
|
||||
$item = [];
|
||||
switch ($element['type']) {
|
||||
case 'comment':
|
||||
$element = $element['comment'];
|
||||
$item['author'] = $element['name'] ?? $element['user']['name'];
|
||||
$item['content'] = '';
|
||||
if (isset($element['body_json'])) {
|
||||
$item['content'] = $this->processBodyJson($element['body_json']);
|
||||
}
|
||||
$item['timestamp'] = $element['date'];
|
||||
$item['title'] = 'Comment by ' . $item['author'];
|
||||
$item['uri'] = $this->getURI() . '/note/c-' . $element['id'];
|
||||
break;
|
||||
case 'post':
|
||||
$item['content'] = $element['postSelection']['text'] ?? '';
|
||||
$element = $element['post'];
|
||||
$item['author'] = $element['publishedBylines'][0]['name'];
|
||||
$item['content'] .= $this->fetchPost($element['id']);
|
||||
$item['timestamp'] = $element['post_date'];
|
||||
$item['title'] = $element['title'];
|
||||
$item['uri'] = parent::getURI() . 'home/post/p-' . $element['id'];
|
||||
break;
|
||||
case 'link':
|
||||
$element = $element['linkMetadata'];
|
||||
$item['author'] = $element['host'];
|
||||
$item['content'] = $element['description'];
|
||||
$item['title'] = $element['title'];
|
||||
$item['uri'] = $element['url'];
|
||||
break;
|
||||
case 'image':
|
||||
$item['uri'] = $element['imageUrl'];
|
||||
break;
|
||||
default:
|
||||
throw new Exception('Invalid Substack entry type: ' . $element['type']);
|
||||
}
|
||||
|
||||
$item['enclosures'] = [
|
||||
$element['audio_items'][0]['audio_url'] ?? null,
|
||||
$element['audio_items'][1]['audio_url'] ?? null,
|
||||
$element['cover_image'] ?? null,
|
||||
$element['image'] ?? null,
|
||||
$element['imageUrl'] ?? null,
|
||||
];
|
||||
|
||||
$item['categories'] = array_map(fn($tag) => $tag['name'], $element['postTags'] ?? []);
|
||||
$item['comments'] = $item['uri'] . '/restacks/notes';
|
||||
|
||||
if (isset($element['attachments'])) {
|
||||
foreach ($element['attachments'] as $attachment) {
|
||||
$attachment = $this->processAttachment($attachment);
|
||||
$item['categories'] = array_merge($item['categories'], $attachment['categories']);
|
||||
$item['enclosures'] = array_merge($item['enclosures'], $attachment['enclosures']);
|
||||
if (isset($attachment['title'])) { // Nothing to quote for images
|
||||
$item['content'] .= $this->quoteAttachment($attachment);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $item;
|
||||
}
|
||||
|
||||
private function fetchPost(string $id)
|
||||
{
|
||||
$json = getContents(parent::getURI() . "api/v1/posts/by-id/$id");
|
||||
$json = json_decode($json, true)['post'];
|
||||
$html = str_get_html($json['body_html']);
|
||||
$body = $html->root;
|
||||
$block = $html->createElement('div');
|
||||
$block->appendChild($html->createElement('hr'));
|
||||
$block->appendChild($html->createElement('h4', 'Full text:'));
|
||||
$block->appendChild($body);
|
||||
return $block->innertext();
|
||||
}
|
||||
|
||||
private function quoteAttachment(array $attachment)
|
||||
{
|
||||
$html = new simple_html_dom();
|
||||
$body = $html->createElement('div');
|
||||
$body->appendChild($html->createElement('hr'));
|
||||
$link = $html->createElement('a');
|
||||
$link->href = $attachment['uri'];
|
||||
$link->appendChild($html->createElement('h3', $attachment['title']));
|
||||
$body->appendChild($link);
|
||||
if ($attachment['content'] != '') {
|
||||
$body->appendChild($html->createElement('h4', 'Qouting ' . $attachment['author'] . ':'));
|
||||
$body->appendChild($html->createElement('blockquote', $attachment['content']));
|
||||
}
|
||||
return $body->innertext();
|
||||
}
|
||||
|
||||
private function processBodyJson(array $json)
|
||||
{
|
||||
$html = new simple_html_dom();
|
||||
$body = $html->createElement('div');
|
||||
foreach ($json['content'] as $block) {
|
||||
if (isset($block['content'])) {
|
||||
$content = $this->processBodyJson($block);
|
||||
}
|
||||
switch ($block['type']) {
|
||||
case 'blockquote':
|
||||
$content->tag = 'blockquote';
|
||||
$body->appendChild($content);
|
||||
break;
|
||||
case 'paragraph':
|
||||
$content->tag = 'p';
|
||||
$body->appendChild($content);
|
||||
break;
|
||||
case 'text':
|
||||
$text = $html->createTextNode($block['text']);
|
||||
if (isset($block['marks'])) {
|
||||
foreach ($block['marks'] as $mark) {
|
||||
switch ($mark['type']) {
|
||||
case 'bold':
|
||||
$marked = $html->createElement('strong');
|
||||
$marked->appendChild($text);
|
||||
$text = $marked;
|
||||
break;
|
||||
case 'italic':
|
||||
$marked = $html->createElement('em');
|
||||
$marked->appendChild($text);
|
||||
$text = $marked;
|
||||
break;
|
||||
case 'link':
|
||||
$marked = $html->createElement('a');
|
||||
$marked->href = $mark['attrs']['href'];
|
||||
$marked->appendChild($text);
|
||||
$text = $marked;
|
||||
break;
|
||||
default:
|
||||
throw new Exception('Invalid text mark type: ' . $mark['type']);
|
||||
}
|
||||
}
|
||||
}
|
||||
$body->appendChild($text);
|
||||
break;
|
||||
case 'substack_mention':
|
||||
$link = $html->createElement('a');
|
||||
$link->href = parent::getURI() . 'profile/' . $block['attrs']['id'];
|
||||
$link->appendChild($html->createTextNode($block['attrs']['label']));
|
||||
$body->appendChild($link);
|
||||
break;
|
||||
default:
|
||||
throw new Exception('Invalid body type: ' . $block['type']);
|
||||
}
|
||||
}
|
||||
return $body;
|
||||
}
|
||||
|
||||
public function getName()
|
||||
{
|
||||
$name = parent::getName();
|
||||
if (isset($this->name)) {
|
||||
$name .= " - $this->name";
|
||||
}
|
||||
return $name;
|
||||
}
|
||||
|
||||
public function getIcon()
|
||||
{
|
||||
if (isset($this->icon)) {
|
||||
return $this->icon;
|
||||
}
|
||||
return parent::getIcon();
|
||||
}
|
||||
|
||||
public function getURI()
|
||||
{
|
||||
if ($this->getInput('profile') != null) {
|
||||
return parent::getURI() . '@' . $this->getInput('profile');
|
||||
}
|
||||
return parent::getURI();
|
||||
}
|
||||
}
|
@ -36,7 +36,7 @@ class TapasBridge extends FeedExpander
|
||||
$this->id = $this->getInput('title');
|
||||
}
|
||||
if ($this->getInput('force_title') || !$this->id) {
|
||||
$html = getSimpleHTMLDOM($this->getURI()) or returnServerError('Could not request ' . $this->getURI());
|
||||
$html = getSimpleHTMLDOM($this->getURI());
|
||||
$this->id = $html->find('meta[property$=":url"]', 0)->content;
|
||||
$this->id = str_ireplace(['tapastic://series/', '/info'], '', $this->id);
|
||||
}
|
||||
|
@ -15,6 +15,14 @@ class TelegramBridge extends BridgeAbstract
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
const CONFIGURATION = [
|
||||
'max_pages' => [
|
||||
'required' => false,
|
||||
'defaultValue' => 1,
|
||||
],
|
||||
];
|
||||
|
||||
const TEST_DETECT_PARAMETERS = [
|
||||
'https://t.me/s/rssbridge' => ['username' => 'rssbridge'],
|
||||
'https://t.me/rssbridge' => ['username' => 'rssbridge'],
|
||||
@ -26,7 +34,7 @@ class TelegramBridge extends BridgeAbstract
|
||||
'https://rssbridge.t.me/' => ['username' => 'rssbridge'],
|
||||
];
|
||||
|
||||
const CACHE_TIMEOUT = 60 * 15; // 15 mins
|
||||
const CACHE_TIMEOUT = 60 * 60; // 1h
|
||||
private $feedName = '';
|
||||
|
||||
private $enclosures = [];
|
||||
@ -36,33 +44,56 @@ class TelegramBridge extends BridgeAbstract
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
$html = getSimpleHTMLDOM($this->getURI());
|
||||
$pages = 0;
|
||||
$url = 'https://t.me/s/' . $this->normalizeUsername();
|
||||
|
||||
$channelTitle = $html->find('div.tgme_channel_info_header_title span', 0)->plaintext ?? '';
|
||||
$channelTitle = htmlspecialchars_decode($channelTitle, ENT_QUOTES);
|
||||
$this->feedName = $channelTitle . ' (@' . $this->normalizeUsername() . ')';
|
||||
$posts = $html->find('div.tgme_widget_message_wrap.js-widget_message_wrap');
|
||||
if (!$channelTitle && !$posts) {
|
||||
throw new \Exception('Unable to find channel. The channel is non-existing or non-public.');
|
||||
}
|
||||
foreach ($posts as $messageDiv) {
|
||||
$this->itemTitle = '';
|
||||
$this->enclosures = [];
|
||||
$item = [];
|
||||
$max_pages = $this->getOption('max_pages');
|
||||
|
||||
$item['uri'] = $messageDiv->find('a.tgme_widget_message_date', 0)->href;
|
||||
$item['content'] = $this->processContent($messageDiv);
|
||||
$item['title'] = $this->itemTitle;
|
||||
$item['timestamp'] = $messageDiv->find('span.tgme_widget_message_meta', 0)->find('time', 0)->datetime;
|
||||
$item['enclosures'] = $this->enclosures;
|
||||
// Hard-coded upper bound of 100 loops
|
||||
while ($pages < $max_pages && $pages < 100) {
|
||||
$pages++;
|
||||
|
||||
$messageOwner = $messageDiv->find('a.tgme_widget_message_owner_name', 0);
|
||||
if ($messageOwner) {
|
||||
$item['author'] = html_entity_decode(trim($messageOwner->plaintext), ENT_QUOTES);
|
||||
$dom = getSimpleHTMLDOM($url);
|
||||
|
||||
$channelTitle = $dom->find('div.tgme_channel_info_header_title span', 0)->plaintext ?? '';
|
||||
$channelTitle = htmlspecialchars_decode($channelTitle, ENT_QUOTES);
|
||||
$this->feedName = $channelTitle . ' (@' . $this->normalizeUsername() . ')';
|
||||
|
||||
$messages = $dom->find('div.tgme_widget_message_wrap.js-widget_message_wrap');
|
||||
if (!$channelTitle && !$messages) {
|
||||
throw new \Exception('Unable to find channel. The channel is non-existing or non-public.');
|
||||
}
|
||||
|
||||
$this->items[] = $item;
|
||||
foreach (array_reverse($messages) as $message) {
|
||||
$this->itemTitle = '';
|
||||
$this->enclosures = [];
|
||||
|
||||
$item = [];
|
||||
|
||||
$item['uri'] = $message->find('a.tgme_widget_message_date', 0)->href;
|
||||
$item['content'] = $this->processContent($message);
|
||||
$item['title'] = $this->itemTitle;
|
||||
$item['timestamp'] = $message->find('span.tgme_widget_message_meta', 0)->find('time', 0)->datetime;
|
||||
$item['enclosures'] = $this->enclosures;
|
||||
|
||||
$messageOwner = $message->find('a.tgme_widget_message_owner_name', 0);
|
||||
if ($messageOwner) {
|
||||
$item['author'] = html_entity_decode(trim($messageOwner->plaintext), ENT_QUOTES);
|
||||
}
|
||||
|
||||
array_unshift($this->items, $item);
|
||||
}
|
||||
|
||||
$more = $dom->find('> div.tgme_widget_message_centered.js-messages_more_wrap a', 0);
|
||||
if ($more && str_contains($more->href, 'before')) {
|
||||
$url = 'https://t.me/' . $more->href;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$this->logger->info(sprintf('Fetched %s messages from %s pages (%s)', count($this->items), $pages, $url));
|
||||
|
||||
$this->items = array_reverse($this->items);
|
||||
}
|
||||
|
||||
@ -369,12 +400,7 @@ EOD;
|
||||
|
||||
private function normalizeUsername()
|
||||
{
|
||||
// todo: can be replaced with ltrim($username, '@');
|
||||
$username = $this->getInput('username');
|
||||
if (substr($username, 0, 1) === '@') {
|
||||
return substr($username, 1);
|
||||
}
|
||||
return $username;
|
||||
return ltrim($this->getInput('username'), '@');
|
||||
}
|
||||
|
||||
public function detectParameters($url)
|
||||
|
@ -56,8 +56,7 @@ class TestFaktaBridge extends BridgeAbstract
|
||||
public function collectData()
|
||||
{
|
||||
$NEWSURL = self::URI . '/sv';
|
||||
$html = getSimpleHTMLDOMCached($NEWSURL, 18000) or
|
||||
returnServerError('Could not request: ' . $NEWSURL);
|
||||
$html = getSimpleHTMLDOMCached($NEWSURL, 18000);
|
||||
|
||||
foreach ($html->find('.row-container') as $element) {
|
||||
// Debug::log($element);
|
||||
@ -68,8 +67,7 @@ class TestFaktaBridge extends BridgeAbstract
|
||||
$figure = $element->find('img', 0);
|
||||
$preamble = trim($element->find('.text', 0)->plaintext);
|
||||
|
||||
$article_html = getSimpleHTMLDOMCached($url, 18000) or
|
||||
returnServerError('Could not request: ' . $url);
|
||||
$article_html = getSimpleHTMLDOMCached($url, 18000);
|
||||
$article_content = $article_html->find('div.content', 0);
|
||||
$article_text = $article_html->find('article', 0);
|
||||
|
||||
|
@ -26,21 +26,16 @@ class TheFarSideBridge extends BridgeAbstract
|
||||
$image = $card->find('img', 0);
|
||||
$imageUrl = $image->attr['data-src'];
|
||||
|
||||
// Images are downloaded to bypass the hotlink protection.
|
||||
$image = getContents($imageUrl, ['Referer: ' . self::URI]);
|
||||
|
||||
// Encode image as base64
|
||||
$imageBase64 = base64_encode($image);
|
||||
|
||||
$caption = '';
|
||||
|
||||
if ($card->find('figcaption', 0)) {
|
||||
$caption = $card->find('figcaption', 0)->innertext;
|
||||
}
|
||||
|
||||
$item['enclosures'][] = $imageUrl;
|
||||
$item['content'] .= <<<EOD
|
||||
<figure>
|
||||
<img title="{$caption}" src="data:image/jpeg;base64,{$imageBase64}"/>
|
||||
<img title="{$caption}" src="{$imageUrl}"/>
|
||||
<figcaption>{$caption}</figcaption>
|
||||
</figure>
|
||||
<br/>
|
||||
|
@ -56,7 +56,11 @@ class TldrTechBridge extends BridgeAbstract
|
||||
if ($child->tag != 'a') {
|
||||
continue;
|
||||
}
|
||||
$this->extractItem(Url::fromString(self::URI . $child->href));
|
||||
$itemUrl = Url::fromString(self::URI . ltrim($child->href, '/'));
|
||||
if ($itemUrl == $locationUrl) {
|
||||
continue;
|
||||
}
|
||||
$this->extractItem($itemUrl);
|
||||
if (count($this->items) >= $limit) {
|
||||
break;
|
||||
}
|
||||
@ -124,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];
|
||||
}
|
||||
|
28
bridges/TomsToucheBridge.php
Normal file
28
bridges/TomsToucheBridge.php
Normal 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;
|
||||
}
|
||||
}
|
@ -10,8 +10,7 @@ class UsesTechbridge extends BridgeAbstract
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
$html = getSimpleHTMLDOM(self::URI)
|
||||
or returnServerError('Could not request: ' . self::URI);
|
||||
$html = getSimpleHTMLDOM(self::URI);
|
||||
|
||||
foreach ($html->find('div[class=PersonInner]') as $index => $a) {
|
||||
$item = []; // Create an empty item
|
||||
|
@ -2,13 +2,13 @@
|
||||
|
||||
class VkBridge extends BridgeAbstract
|
||||
{
|
||||
const MAINTAINER = 'em92';
|
||||
// const MAINTAINER = 'em92';
|
||||
// const MAINTAINER = 'pmaziere';
|
||||
// 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);
|
||||
|
@ -16,8 +16,7 @@ class VproTegenlichtBridge extends BridgeAbstract
|
||||
public function collectData()
|
||||
{
|
||||
$url = sprintf('https://www.vpro.nl/programmas/tegenlicht/lees/artikelen.html');
|
||||
$dom = getSimpleHTMLDOM($url)
|
||||
or returnServerError('No contents received!');
|
||||
$dom = getSimpleHTMLDOM($url);
|
||||
$dom = $dom->find('ul#browsable-news-overview', 0);
|
||||
$dom = defaultLinkTo($dom, $this->getURI());
|
||||
foreach ($dom->find('li') as $article) {
|
||||
|
@ -105,10 +105,6 @@ class WikipediaBridge extends BridgeAbstract
|
||||
// This will automatically send us to the correct main page in any language (try it!)
|
||||
$html = getSimpleHTMLDOM($this->getURI() . '/wiki');
|
||||
|
||||
if (!$html) {
|
||||
returnServerError('Could not load site: ' . $this->getURI() . '!');
|
||||
}
|
||||
|
||||
/*
|
||||
* Now read content depending on the language (make sure to create one function per language!)
|
||||
* We build the function name automatically, just make sure you create a private function ending
|
||||
|
@ -1,149 +0,0 @@
|
||||
<?php
|
||||
|
||||
class WorldCosplayBridge extends BridgeAbstract
|
||||
{
|
||||
const NAME = 'WorldCosplay Bridge';
|
||||
const URI = 'https://worldcosplay.net/';
|
||||
const DESCRIPTION = 'Returns WorldCosplay photos';
|
||||
const MAINTAINER = 'AxorPL';
|
||||
|
||||
const API_CHARACTER = 'api/photo/list.json?character_id=%u&limit=%u';
|
||||
const API_COSPLAYER = 'api/member/photos.json?member_id=%u&limit=%u';
|
||||
const API_SERIES = 'api/photo/list.json?title_id=%u&limit=%u';
|
||||
const API_TAG = 'api/tag/photo_list.json?id=%u&limit=%u';
|
||||
|
||||
const CONTENT_HTML
|
||||
= '<a href="%s" target="_blank"><img src="%s" alt="%s" title="%s"></a>';
|
||||
|
||||
const ERR_CONTEXT = 'No context provided';
|
||||
const ERR_QUERY = 'Unable to query: %s';
|
||||
|
||||
const LIMIT_MIN = 1;
|
||||
const LIMIT_MAX = 24;
|
||||
|
||||
const PARAMETERS = [
|
||||
'Character' => [
|
||||
'cid' => [
|
||||
'name' => 'Character ID',
|
||||
'type' => 'number',
|
||||
'required' => true,
|
||||
'title' => 'WorldCosplay character ID',
|
||||
'exampleValue' => 18204
|
||||
]
|
||||
],
|
||||
'Cosplayer' => [
|
||||
'uid' => [
|
||||
'name' => 'Cosplayer ID',
|
||||
'type' => 'number',
|
||||
'required' => true,
|
||||
'title' => 'Cosplayer\'s WorldCosplay profile ID',
|
||||
'exampleValue' => 406782
|
||||
]
|
||||
],
|
||||
'Series' => [
|
||||
'sid' => [
|
||||
'name' => 'Series ID',
|
||||
'type' => 'number',
|
||||
'required' => true,
|
||||
'title' => 'WorldCosplay series ID',
|
||||
'exampleValue' => 3139
|
||||
]
|
||||
],
|
||||
'Tag' => [
|
||||
'tid' => [
|
||||
'name' => 'Tag ID',
|
||||
'type' => 'number',
|
||||
'required' => true,
|
||||
'title' => 'WorldCosplay tag ID',
|
||||
'exampleValue' => 33643
|
||||
]
|
||||
],
|
||||
'global' => [
|
||||
'limit' => [
|
||||
'name' => 'Limit',
|
||||
'type' => 'number',
|
||||
'required' => false,
|
||||
'title' => 'Maximum number of photos to return',
|
||||
'exampleValue' => 5,
|
||||
'defaultValue' => 5
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
public function collectData()
|
||||
{
|
||||
$limit = $this->getInput('limit');
|
||||
$limit = min(self::LIMIT_MAX, max(self::LIMIT_MIN, $limit));
|
||||
switch ($this->queriedContext) {
|
||||
case 'Character':
|
||||
$id = $this->getInput('cid');
|
||||
$url = self::API_CHARACTER;
|
||||
break;
|
||||
case 'Cosplayer':
|
||||
$id = $this->getInput('uid');
|
||||
$url = self::API_COSPLAYER;
|
||||
break;
|
||||
case 'Series':
|
||||
$id = $this->getInput('sid');
|
||||
$url = self::API_SERIES;
|
||||
break;
|
||||
case 'Tag':
|
||||
$id = $this->getInput('tid');
|
||||
$url = self::API_TAG;
|
||||
break;
|
||||
default:
|
||||
returnClientError(self::ERR_CONTEXT);
|
||||
}
|
||||
$url = self::URI . sprintf($url, $id, $limit);
|
||||
|
||||
$json = json_decode(getContents($url));
|
||||
if ($json->has_error) {
|
||||
returnServerError($json->message);
|
||||
}
|
||||
$list = $json->list;
|
||||
|
||||
foreach ($list as $img) {
|
||||
$image = $img->photo ?? $img;
|
||||
$item = [
|
||||
'uri' => self::URI . substr($image->url, 1),
|
||||
'title' => $image->subject,
|
||||
'author' => $img->member->global_name,
|
||||
'enclosures' => [$image->large_url],
|
||||
'uid' => $image->id,
|
||||
];
|
||||
// Context cosplayer don't have created_at
|
||||
if (isset($image->created_at)) {
|
||||
$item['timestamp'] = $image->created_at;
|
||||
}
|
||||
$item['content'] = sprintf(
|
||||
self::CONTENT_HTML,
|
||||
$item['uri'],
|
||||
$item['enclosures'][0],
|
||||
$item['title'],
|
||||
$item['title']
|
||||
);
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
public function getName()
|
||||
{
|
||||
switch ($this->queriedContext) {
|
||||
case 'Character':
|
||||
$id = $this->getInput('cid');
|
||||
break;
|
||||
case 'Cosplayer':
|
||||
$id = $this->getInput('uid');
|
||||
break;
|
||||
case 'Series':
|
||||
$id = $this->getInput('sid');
|
||||
break;
|
||||
case 'Tag':
|
||||
$id = $this->getInput('tid');
|
||||
break;
|
||||
default:
|
||||
return parent::getName();
|
||||
}
|
||||
return sprintf('%s %u - ', $this->queriedContext, $id) . self::NAME;
|
||||
}
|
||||
}
|
@ -304,11 +304,9 @@ class XenForoBridge extends BridgeAbstract
|
||||
|
||||
// We can optimize performance by caching all but the last page
|
||||
if ($page != $lastpage) {
|
||||
$html = getSimpleHTMLDOMCached($pageurl)
|
||||
or returnServerError('Error loading contents from ' . $pageurl . '!');
|
||||
$html = getSimpleHTMLDOMCached($pageurl);
|
||||
} else {
|
||||
$html = getSimpleHTMLDOM($pageurl)
|
||||
or returnServerError('Error loading contents from ' . $pageurl . '!');
|
||||
$html = getSimpleHTMLDOM($pageurl);
|
||||
}
|
||||
|
||||
$html = defaultLinkTo($html, $hosturl);
|
||||
@ -347,11 +345,9 @@ class XenForoBridge extends BridgeAbstract
|
||||
|
||||
// We can optimize performance by caching all but the last page
|
||||
if ($page != $lastpage) {
|
||||
$html = getSimpleHTMLDOMCached($pageurl)
|
||||
or returnServerError('Error loading contents from ' . $pageurl . '!');
|
||||
$html = getSimpleHTMLDOMCached($pageurl);
|
||||
} else {
|
||||
$html = getSimpleHTMLDOM($pageurl)
|
||||
or returnServerError('Error loading contents from ' . $pageurl . '!');
|
||||
$html = getSimpleHTMLDOM($pageurl);
|
||||
}
|
||||
|
||||
$html = defaultLinkTo($html, $hosturl);
|
||||
|
86
bridges/YouTubeFeedExpanderBridge.php
Normal file
86
bridges/YouTubeFeedExpanderBridge.php
Normal 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;
|
||||
}
|
||||
}
|
@ -1,12 +1,5 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* RssBridgeYoutube
|
||||
* Returns the newest videos
|
||||
* WARNING: to parse big playlists (over ~90 videos), you need to edit simple_html_dom.php:
|
||||
* change: define('MAX_FILE_SIZE', 600000);
|
||||
* into: define('MAX_FILE_SIZE', 900000); (or more)
|
||||
*/
|
||||
class YoutubeBridge extends BridgeAbstract
|
||||
{
|
||||
const NAME = 'YouTube Bridge';
|
||||
|
@ -21,20 +21,15 @@
|
||||
;enabled_bridges[] = ThePirateBay
|
||||
;enabled_bridges[] = TikTokBridge
|
||||
;enabled_bridges[] = Twitch
|
||||
;enabled_bridges[] = Vk
|
||||
;enabled_bridges[] = XPathBridge
|
||||
;enabled_bridges[] = Youtube
|
||||
;enabled_bridges[] = YouTubeCommunityTabBridge
|
||||
enabled_bridges[] = *
|
||||
|
||||
; Defines the timezone used by RSS-Bridge
|
||||
; Find a list of supported timezones at
|
||||
; https://www.php.net/manual/en/timezones.php
|
||||
; timezone = "UTC" (default)
|
||||
timezone = "UTC"
|
||||
|
||||
; Display a system message to users.
|
||||
message = ""
|
||||
;message = "Hello world"
|
||||
|
||||
; Whether to enable debug mode.
|
||||
enable_debug_mode = false
|
||||
@ -46,14 +41,18 @@ enable_debug_mode = false
|
||||
; Whether to enable maintenance mode. If enabled, feed requests receive 503 Service Unavailable
|
||||
enable_maintenance_mode = false
|
||||
|
||||
; Max file size for simple_html_dom in bytes (10000000 => 10 MB)
|
||||
max_file_size = 10000000
|
||||
|
||||
[http]
|
||||
|
||||
; Operation timeout in seconds
|
||||
timeout = 15
|
||||
timeout = 5
|
||||
|
||||
; Operation retry count in case of curl error
|
||||
retries = 2
|
||||
retries = 1
|
||||
|
||||
; User agent
|
||||
; Curl user agent
|
||||
useragent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:102.0) Gecko/20100101 Firefox/102.0"
|
||||
|
||||
; Max http response size in MB
|
||||
@ -70,12 +69,13 @@ type = "file"
|
||||
custom_timeout = false
|
||||
|
||||
[admin]
|
||||
|
||||
; Advertise an email address where people can reach the administrator.
|
||||
; This address is displayed on the main page, visible to everyone!
|
||||
; "" = 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.
|
||||
@ -86,6 +86,7 @@ telegram = ""
|
||||
donations = true
|
||||
|
||||
[proxy]
|
||||
|
||||
; The HTTP proxy to tunnel requests through
|
||||
; https://curl.se/libcurl/c/CURLOPT_PROXY.html
|
||||
; "" = Proxy disabled (default)
|
||||
@ -135,6 +136,7 @@ report_limit = 1
|
||||
; --- Cache specific configuration ---------------------------------------------
|
||||
|
||||
[FileCache]
|
||||
|
||||
; The root folder to store files in.
|
||||
; "" = Use the cache folder in the repository (default)
|
||||
path = ""
|
||||
@ -142,6 +144,7 @@ path = ""
|
||||
enable_purge = true
|
||||
|
||||
[SQLiteCache]
|
||||
|
||||
; Filepath of the sqlite db file
|
||||
file = "cache.sqlite"
|
||||
; Whether to actually delete data when purging
|
||||
@ -150,11 +153,17 @@ enable_purge = true
|
||||
timeout = 5000
|
||||
|
||||
[MemcachedCache]
|
||||
|
||||
host = "localhost"
|
||||
port = 11211
|
||||
|
||||
; --- Bridge specific configuration ------
|
||||
|
||||
[TelegramBridge]
|
||||
|
||||
; Max pages to fetch (1 page => 20 messages), min=1 max=100
|
||||
max_pages = 1
|
||||
|
||||
[DiscogsBridge]
|
||||
|
||||
; Sets the personal access token for interactions with Discogs. When
|
||||
|
@ -3,7 +3,7 @@
|
||||
| Country | Address | Status | Contact | Comment |
|
||||
|:-------:|---------|--------|----------|---------|
|
||||
|  | https://rss-bridge.org/bridge01 |  | [@dvikan](https://github.com/dvikan) | London, Digital Ocean|
|
||||
|  | https://rssbridge.flossboxin.org.in |  | [@vdbhb59](https://github.com/vdbhb59) | Hosted with OVH SAS (Maintained in India) |
|
||||
|  | https://rssbridge.flossboxin.org.in |  | [@vdbhb59](https://github.com/vdbhb59) | Hosted with Netcup Germany (Maintained in India) |
|
||||
|  | https://rss-bridge.cheredeprince.net |  | [@La_Bécasse](https://cheredeprince.net/contact) | Self-Hosted at home in France |
|
||||
|  | https://rss-bridge.sans-nuage.fr |  | [@Alsace Réseau Neutre](https://arn-fai.net/contact) | Hosted in Alsace, France |
|
||||
|  | https://rss-bridge.lewd.tech |  | [@Erisa](https://github.com/Erisa) | Hosted in London, protected by Cloudflare Rate Limiting |
|
||||
|
@ -99,7 +99,7 @@ This method should return the HTML source as a base for the XPath expressions. U
|
||||
This method should provide the feed title. Usually the XPath expression defined in `XPATH_EXPRESSION_FEED_TITLE` is used for extracting the title directly from the page source.
|
||||
|
||||
### Method `provideFeedIcon()`
|
||||
This method should provide the feed title. Usually the XPath expression defined in `XPATH_EXPRESSION_FEED_ICON` is used for extracting the title directly from the page source.
|
||||
This method should provide the URL of the feed's favicon. Usually the XPath expression defined in `XPATH_EXPRESSION_FEED_ICON` is used for extracting the title directly from the page source.
|
||||
|
||||
### Method `provideFeedItems()`
|
||||
This method should provide the feed items. Usually the XPath expression defined in `XPATH_EXPRESSION_ITEM` is used for extracting the items from the page source. All other XPath expressions are applied on a per-item basis, item by item, and only on the item's contents.
|
||||
@ -122,8 +122,8 @@ Accepts the items author as parameter, processes and returns it. Should return a
|
||||
### Method `formatItemTimestamp()`
|
||||
Accepts the items creation timestamp as parameter, processes and returns it. Should return a unix timestamp as integer.
|
||||
|
||||
### Method `cleanImageUrl()`
|
||||
Method invoked for cleaning feed icon and item image URL's. Extracts the image URL from the passed parameter, stripping any additional content. Furthermore makes sure that relative image URL's get transformed to absolute ones.
|
||||
### Method `cleanMediaUrl()`
|
||||
Method invoked for cleaning feed icon, item image and media attachment (like .mp3, .webp) URL's. Extracts the media URL from the passed parameter, stripping any additional content. Furthermore, makes sure that relative media URL's get transformed to absolute ones.
|
||||
|
||||
### Method `fixEncoding()`
|
||||
Only invoked when class constant `SETTING_FIX_ENCODING` is set to true. It then passes all extracted string values through PHP's `utf8_decode` function.
|
||||
|
12
docs/10_Bridge_Specific/Telegram.md
Normal file
12
docs/10_Bridge_Specific/Telegram.md
Normal file
@ -0,0 +1,12 @@
|
||||
# TelegramBridge
|
||||
|
||||
By default, it fetches a single page with up to 20 messages.
|
||||
|
||||
To increase this limit, tweak the `max_pages` config:
|
||||
|
||||
```ini
|
||||
[TelegramBridge]
|
||||
|
||||
; Fetch a maximum of 3 pages (requires 3 http requests)
|
||||
max_pages = 3
|
||||
```
|
@ -80,14 +80,8 @@ class MrssFormat extends FormatAbstract
|
||||
$feedUrl = get_current_url();
|
||||
$linkSelf->setAttribute('href', $feedUrl);
|
||||
} elseif ($feedKey === 'icon') {
|
||||
$allowedIconExtensions = [
|
||||
'.gif',
|
||||
'.jpg',
|
||||
'.png',
|
||||
'.ico',
|
||||
];
|
||||
$icon = $feedValue;
|
||||
if ($icon && in_array(substr($icon, -4), $allowedIconExtensions)) {
|
||||
if ($icon) {
|
||||
$feedImage = $document->createElement('image');
|
||||
$channel->appendChild($feedImage);
|
||||
$iconUrl = $document->createElement('url');
|
||||
|
@ -5,6 +5,11 @@ if (version_compare(\PHP_VERSION, '7.4.0') === -1) {
|
||||
exit("RSS-Bridge requires minimum PHP version 7.4\n");
|
||||
}
|
||||
|
||||
if (!extension_loaded('curl')) {
|
||||
http_response_code(500);
|
||||
exit("RSS-Bridge requires curl (apt install php-curl)\n");
|
||||
}
|
||||
|
||||
require __DIR__ . '/lib/bootstrap.php';
|
||||
require __DIR__ . '/lib/config.php';
|
||||
|
||||
|
@ -240,7 +240,7 @@ abstract class BridgeAbstract
|
||||
if (isset($input[$name])) {
|
||||
$value = $input[$name];
|
||||
} else {
|
||||
if ($properties['type'] ?? null === 'checkbox') {
|
||||
if (($properties['type'] ?? null) === 'checkbox') {
|
||||
$value = false;
|
||||
} elseif (isset($properties['defaultValue'])) {
|
||||
$value = $properties['defaultValue'];
|
||||
@ -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);
|
||||
}
|
||||
|
@ -44,6 +44,10 @@ final class BridgeCard
|
||||
data-short-name="$shortName"
|
||||
>
|
||||
|
||||
<a style="position: absolute; top: 10px; left: 10px" href="#bridge-{$bridgeClassName}">
|
||||
<h1>#</h1>
|
||||
<a>
|
||||
|
||||
<h2><a href="{$uri}">{$name}</a></h2>
|
||||
<p class="description">{$description}</p>
|
||||
|
||||
|
@ -7,7 +7,7 @@
|
||||
*/
|
||||
final class Configuration
|
||||
{
|
||||
private const VERSION = '2024-02-02';
|
||||
private const VERSION = '2025-01-26';
|
||||
|
||||
private static $config = [];
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user