1
0
mirror of https://github.com/RSS-Bridge/rss-bridge.git synced 2025-08-17 14:00:43 +02:00

Compare commits

..

122 Commits

Author SHA1 Message Date
Dag
6c0e186d3f fix(jornaln): Array to string conversion at lib/BridgeAbstract.php li… (#3523)
* fix(jornaln): Array to string conversion at lib/BridgeAbstract.php line 320

* yup
2023-07-11 16:54:59 +02:00
Dag
c9a861e259 fix(cache): bug in prior refactor (#3520) 2023-07-09 15:24:29 +02:00
Dag
dfe78fb379 fix: various small fixes (#3519) 2023-07-09 10:08:30 +02:00
Dag
f0a504bb9a fix(FeedMerge): allow xml parse failure too (#3518) 2023-07-08 23:36:36 +02:00
Dag
7881c87bed fix: various small fixes (#3517)
* fix(asrocknews): Trying to get property src of non-object

Trying to get property 'src' of non-object at bridges/ASRockNewsBridge.php line 37

* refactor(http): tweak max redirs config

* fix(tiktok)

* fix(gizmodo)

* fix(craig)

* fix(nationalg)

* fix(roadandtrack)

* fix(etsy)
2023-07-08 23:21:55 +02:00
Dag
1a529fac46 fix(soundcloud): bug in prior cache refactor (#3516) 2023-07-08 22:53:23 +02:00
Dag
adc38e65d9 docs: add install method using composer (#3514) 2023-07-08 17:07:43 +02:00
Dag
0b95dc2d4f chore: synchronize composer.lock (#3513)
$ composer update
Loading composer repositories with package information
Updating dependencies
Info from https://repo.packagist.org: #StandWithUkraine
Lock file operations: 0 installs, 12 updates, 6 removals
  - Removing phpdocumentor/reflection-common (2.2.0)
  - Removing phpdocumentor/reflection-docblock (5.3.0)
  - Removing phpdocumentor/type-resolver (1.6.1)
  - Removing phpspec/prophecy (v1.15.0)
  - Removing symfony/polyfill-ctype (v1.25.0)
  - Removing webmozart/assert (1.10.0)
  - Upgrading doctrine/instantiator (1.4.1 => 1.5.0)
  - Upgrading myclabs/deep-copy (1.11.0 => 1.11.1)
  - Upgrading nikic/php-parser (v4.13.2 => v4.16.0)
  - Upgrading phpunit/php-code-coverage (9.2.15 => 9.2.26)
  - Upgrading phpunit/phpunit (9.5.20 => 9.6.9)
  - Upgrading sebastian/comparator (4.0.6 => 4.0.8)
  - Upgrading sebastian/diff (4.0.4 => 4.0.5)
  - Upgrading sebastian/environment (5.1.4 => 5.1.5)
  - Upgrading sebastian/exporter (4.0.4 => 4.0.5)
  - Upgrading sebastian/recursion-context (4.0.4 => 4.0.5)
  - Upgrading sebastian/type (3.0.0 => 3.2.1)
  - Upgrading squizlabs/php_codesniffer (3.6.2 => 3.7.2)
Writing lock file
Installing dependencies from lock file (including require-dev)
Package operations: 29 installs, 0 updates, 0 removals
  - Installing sebastian/version (3.0.2): Extracting archive
  - Installing sebastian/type (3.2.1): Extracting archive
  - Installing sebastian/resource-operations (3.0.3): Extracting archive
  - Installing sebastian/recursion-context (4.0.5): Extracting archive
  - Installing sebastian/object-reflector (2.0.4): Extracting archive
  - Installing sebastian/object-enumerator (4.0.4): Extracting archive
  - Installing sebastian/global-state (5.0.5): Extracting archive
  - Installing sebastian/exporter (4.0.5): Extracting archive
  - Installing sebastian/environment (5.1.5): Extracting archive
  - Installing sebastian/diff (4.0.5): Extracting archive
  - Installing sebastian/comparator (4.0.8): Extracting archive
  - Installing sebastian/code-unit (1.0.8): Extracting archive
  - Installing sebastian/cli-parser (1.0.1): Extracting archive
  - Installing phpunit/php-timer (5.0.3): Extracting archive
  - Installing phpunit/php-text-template (2.0.4): Extracting archive
  - Installing phpunit/php-invoker (3.1.1): Extracting archive
  - Installing phpunit/php-file-iterator (3.0.6): Extracting archive
  - Installing theseer/tokenizer (1.2.1): Extracting archive
  - Installing nikic/php-parser (v4.16.0): Extracting archive
  - Installing sebastian/lines-of-code (1.0.3): Extracting archive
  - Installing sebastian/complexity (2.0.2): Extracting archive
  - Installing sebastian/code-unit-reverse-lookup (2.0.3): Extracting archive
  - Installing phpunit/php-code-coverage (9.2.26): Extracting archive
  - Installing phar-io/version (3.2.1): Extracting archive
  - Installing phar-io/manifest (2.0.3): Extracting archive
  - Installing myclabs/deep-copy (1.11.1): Extracting archive
  - Installing doctrine/instantiator (1.5.0): Extracting archive
  - Installing phpunit/phpunit (9.6.9): Extracting archive
  - Installing squizlabs/php_codesniffer (3.7.2): Extracting archive
Generating autoload files
25 packages you are using are looking for funding.
Use the `composer fund` command to find out more!
2023-07-08 17:07:35 +02:00
Dag
61b307a9f9 fix(tiktok): feed item link (#3511)
* fix(tiktok): feed item link

* fix(tiktok): support entire url, for convenience
2023-07-08 17:07:20 +02:00
Dag
341649a8a4 fix: tweak the defaultly enabled bridges (#3510)
* fix: tweak the defaultly enabled bridges

enabled_bridges[] = FeedMerge
enabled_bridges[] = Filter
enabled_bridges[] = GettrBridge
enabled_bridges[] = MastodonBridge
enabled_bridges[] = Reddit
enabled_bridges[] = RumbleBridge
enabled_bridges[] = SoundcloudBridge
enabled_bridges[] = Telegram
enabled_bridges[] = ThePirateBay
enabled_bridges[] = Twitch
enabled_bridges[] = Twitter
enabled_bridges[] = Vk
enabled_bridges[] = XPathBridge
enabled_bridges[] = Youtube
enabled_bridges[] = YouTubeCommunityTabBridge

* add feed reducer too

* add tiktok too
2023-07-08 17:07:08 +02:00
Dag
91976f7d56 fix(file cache): acquire lock before writing (#3509) 2023-07-08 17:06:49 +02:00
Dag
8b996e3056 refactor: display action (#3508) 2023-07-08 17:06:33 +02:00
Dag
c1c8304fc0 refactor: dont create multiple instances of the cache (#3504) 2023-07-08 17:03:12 +02:00
Dag
b594ad2de3 fix: discard empty lines in whitelist.txt (#3507) 2023-07-07 11:25:36 +02:00
User123698745
ef0b86968c [PicnobBridge] fix missing images (#3506)
* [PicnobBridge] fix missing images

* [PicnobBridge] handle invalid relative date (e.g.: 'Just now')

* [PicnobBridge] fix code indent
2023-07-07 08:16:45 +02:00
somini
d49ea235f0 [JornalDeNoticiasBridge]: Remove bridge (#3505)
This is now broken, it was replaced by a React monstrosity. The API
doesn't even let me read the post data, it demands authentication.
Bummer.

Better remove it now, since it's worthless.
2023-07-07 08:14:31 +02:00
Dag
46f0e97c73 refactor: remove useless actual args to clearstatcache (#3503) 2023-07-06 19:39:40 +02:00
Dag
5e22459eb6 fix: remove unnecessary calls to purgeCache (#3502) 2023-07-06 18:52:19 +02:00
Dag
965d7d44c5 feat(sqlite cache): add config options (#3499)
* refactor: sqlite cache

* refactor

* feat: add config options to sqlite cache

* refactor
2023-07-06 15:59:38 +02:00
Dag
21c8d8775e fix: bug in #3442 (#3498)
* fix: bug in #3442

* docs: add inline doc on how to enable all bridges
2023-07-06 15:20:56 +02:00
Dag
caac7f572c refacor: improve cache interface (#3492)
* fix: proper typehint on setScope

* refactor: type hint setKey()

* typehint
2023-07-06 15:10:30 +02:00
Dag
f8801d8cb3 feat: add system config enable_maintenance_mode (#3497) 2023-07-06 15:09:44 +02:00
Dag
8f9147458d fix(gatesnotes): fix #3493 (#3494) 2023-07-06 03:36:01 +02:00
Dag
a9fd3b9e61 fix(CacheInterface): logic bug in getTime (#3491)
* fix(CacheInterface): logic bug in getTime

* test
2023-07-05 17:37:21 +02:00
Dag
18e1597361 fix(ph): consent cookie (#3490) 2023-07-05 17:06:23 +02:00
Patrick
e9af41d666 Add bridges for JohannesBlick Steinfeld, OM Online and UsesTech (#3489)
* Add bridges for JohannesBlick Steinfeld, OM Online and UsesTech

* Fixed linit alert
2023-07-05 16:43:59 +02:00
Dag
354317d010 fix(rumble): add timestamp to items (#3485) 2023-07-05 05:41:33 +02:00
Dag
84501cfc00 feat: add health check action (#3484) 2023-07-05 05:41:20 +02:00
Dag
82c22bd2b5 fix(feedmerge): allow a single feed to break, and dont break the whole bridge (#3476) 2023-07-05 05:41:01 +02:00
Dag
a21d496bc7 feat: add default arg to Configuration::getConfig (#3331) 2023-07-05 05:33:22 +02:00
Dag
cf920694d5 fix(futurasciences): fix broken bridge (#3488) 2023-07-04 23:34:24 +02:00
Thomas
bf0d771367 [YouTubeCommunityTabBridge] Fix getURI implementation
Previously the undefined property "feedUri" was accessed here, always
causing a fallback to the parent class
2023-07-04 17:03:30 +02:00
Dag
48385777b4 fix: php notices (#3482)
* fix(furaffinity): notice

* fox(releases3ds): remove references to non-existing vars
2023-07-03 10:48:33 +02:00
Dag
d8bc015efc fix: php notices (#3479)
* fix(jornaln): A non well formed numeric value encountered

fixes

A non well formed numeric value encountered at bridges/JornalNBridge.php line 89

* fix(reuters): fix notice
2023-07-03 00:39:01 +02:00
Eugene Molotov
0f14a0f6ee [YandexZenBridge] Add image to post (#3478) 2023-07-02 22:56:45 +02:00
Thomas
eb2b4747ae [YouTubeCommunityTabBridge] Add timestamps (#3477)
As YouTube doesn't provide precise dates for community posts, an
increasing multiple of 60 seconds is subtracted from each timestamp.
This ensures that the original order is always preserved, even if there
are multiple posts with the same date (e.g. "1 month ago").
2023-07-02 22:56:08 +02:00
Dag
bf73372d7f fix: dont be case-sensitive on env vars (#3475) 2023-07-02 06:47:21 +02:00
Dag
748fc9fd65 fix: various small notice fixes (#3474)
* fix(patreon): php notice

* fix(pepperbridge): php notice

* fix(ebay): php notice

* fix(tiktok): php notice

* fix(yandex): fix notice

* fix(justwatch): notice

* lint
2023-07-02 06:40:25 +02:00
Dag
372880b5ef fix: file cache tweaks (#3470)
* fix: improve file cache

* fix(filecache): log when unserialize fails
2023-06-30 22:31:19 +02:00
rmscoelho
cc91ee1e37 [JornalNBridge] getName() fix (#3456)
* [JornalNBridge] getName() fix

* [JornalNBridge] feed fixes
2023-06-30 15:54:53 +02:00
Arnav Jain
fece9ed344 [NotAlwaysBridge] Add new categories, remove duplicate header, and social meta (#3463)
* [NotAlwaysBridge] add new tags

* [NotAlwaysBridge] Remove duplicate header and social meta

* [NotAlwaysBridge] Add space to fix lint issues
2023-06-30 15:50:54 +02:00
Bocki
b6a263037a [core] Fix prtester for optgroups (#3467) 2023-06-30 15:41:00 +02:00
Mynacol
410ef85618 [GolemBridge] Strip <script> tags
Golem articles referencing their podcast contain a JavaScript reference
to Podigee. We don't want JS, so we strip it.

Example page: https://www.golem.de/news/podcast-besser-wissen-von-schlangenoel-und-sicherheit-2306-175185.html
2023-06-28 16:01:32 +02:00
Dag
8eabdbe5f8 fix(Twitter): properly find time line entries when ordering is inconsistent (#3461) 2023-06-27 16:11:41 +02:00
António Pereira
bd6f56383c [PresidenciaPTBridge]: Fix timestamp search (#3459) 2023-06-26 17:52:32 +02:00
ORelio
d4bc63ee98 [TheHackerNews] Update content extraction (#3458) 2023-06-25 19:01:57 +02:00
rmscoelho
1b02d4f49b [CorreioDaFeiraBridge] cache timeout + getName fixes (#3453)
* [CorreioDaFeiraBridge] cache timeout fix

* [CorreioDaFeiraBridge] cache timeout fix

* [CorreioDaFeiraBridge] getName() fix
2023-06-22 07:27:52 +02:00
rmscoelho
a4ed52ca30 [VideoCardzBridge] cache timeout fix + getName fixes (#3454)
* [VideoCardzBridge] cache timeout fix

* [VideoCardzBridge] getName() + title fix
2023-06-22 07:27:30 +02:00
rmscoelho
1769399da8 [ABolaBridge] cache timeout fix + getName fixes (#3455)
* [ABolaBridge] cache timeout fix

* [ABolaBridge] fix timestamp and image alt null

* [ABolaBridge] formatting fixes

* [ABolaBridge] getName() fix
2023-06-22 07:27:01 +02:00
rmscoelho
12ba6154f9 [JornalNBridge] cache timeout fix (#3452)
* [JornalNBridge] cache timeout fix

* [JornalNBridge] cache timeout fix
2023-06-21 11:15:36 +02:00
rmscoelho
8e35ebf482 [New Bridge] Jornal N (Portuguese local newspaper) (#3451)
* [New Bridge] Jornal N (Portuguese local newspaper)

* [JornalNBridge] formatting fixes
2023-06-21 05:17:11 +02:00
rmscoelho
61130e89b4 [ABolaBridge] timestamp (#3448)
* [ABolaBridge] timestamp

* [ABolaBridge] formatting fixes
2023-06-21 05:15:01 +02:00
rmscoelho
ebebb886c5 [ABolaBridge] feed fixes (#3446)
* [ABolaBridge] category name and url fixes

* [ABolaBridge] "Mercado" feed fix; feed name fix; img fix;

* [ABolaBridge] formatting fix
2023-06-20 17:26:55 +02:00
Matt Connell
6eaa31b999 [New Bridge] WYMT news bridge (#3444)
* feat: add WYMT bridge

* fix: phpcs error
2023-06-20 15:13:41 +02:00
rmscoelho
1d3888f22a [VideoCardzBridge] category name and url fixes (#3447)
* [VideoCardzBridge] category name and url fixes

* [VideoCardzBridge] error fixes

* [VideoCardzBridge] formatting fix

* [VideoCardzBridge] cache timeout removal
2023-06-20 15:01:41 +02:00
rmscoelho
60be4cdebd [CorreioDaFeiraBridge] adding timestamps; fixing categories; (#3445)
* [New Bridge] Correio da Feira (regional newspaper)

* [CorreioDaFeiraBridge] adding timestamp; fixing name

* [CorreioDaFeiraBridge] formatting fixes
2023-06-20 12:46:24 +02:00
rmscoelho
5a0bacbd8a [New Bridge] Videocardz.com bridge (#3442)
* [New Bridge] Videocardz.com Bridge

* [New Bridge] Videocardz.com Bridge

* [Videocardz.com] cache timeout increase

* [VideoCardzBridge] cache timeout change

* [VideoCardzBridge] formatting

* [VideoCardBridge] formatting fixes

* [VideoCardzBridge] formatting fixes
2023-06-20 12:45:50 +02:00
rmscoelho
0c808dc3a1 [New Bridge] Bridge for sports website A Bola (#3441)
* [New Bridge] Bridge for sports website A Bola

* [ABolaBridge] add thumbnail

* [ABolaBridge] formatting

* [ABolaBridge] formatting fixes

* [ABolaBridge] formatting fixes

* [ABolaBridge] formatting fixes
2023-06-20 12:45:34 +02:00
rmscoelho
98b72b2c5c [New Bridge] Correio da Feira (regional newspaper) (#3443) 2023-06-20 05:57:22 +02:00
Thomas
54d626d5cd [XPathAbstract] Use baseURI to fix relative links (if available) (#3439) 2023-06-17 17:53:00 +02:00
somini
1e470ef341 [PresidenciaPTBridge]: Fix title search (#3438)
This was changed on the site itself, in the last few days.
2023-06-17 06:13:09 +02:00
Dag
0a8fe57003 feat: enable bridges using env var (#3428)
* refactor: bridgefactory, add tests

* refactor: move defaultly enabled bridges to config

* refactor

* refactor

* feat: add support for enabling bridges with env var
2023-06-11 03:16:03 +02:00
Nick McCarthy
d9490c6518 GoogleScholarV2Bridge (#3415)
* Added google scholar v2 bridge with more functionality

* Corrected Sort By interpretation (this is weird on Googles part)

* Remove some debug statements

* Merged GoogleScholarBridge and GoogleScholarV2Bridge into GoogleScholarBridge with two contexts.

* Left V2 in Bridge Name

* Lint

* Update GoogleScholarBridge.php

* Update GoogleScholarBridge.php

* Lint.

* ;
2023-06-10 18:35:04 +02:00
Jisagi
eb799e59a6 [NyaaTorrentsBridge] Add custom fields (#3420)
* Update NyaaTorrentsBridge.php

* lint

* lint #2

* Sir Lint the Third

* Add torrent id to custom fields

* Proposed improvements
2023-06-10 18:28:00 +02:00
Eugene Molotov
ec1a3f4fe3 [YoutubeBridge] Unassign maintainer (#3431) 2023-06-10 18:27:49 +02:00
Eugene Molotov
80376830c5 [VkBridge] Handle some secondary attachments (#3430) 2023-06-10 18:27:32 +02:00
Shikiryu
e859497d6a [PicalaBridge] Fix article without image (#3429)
Co-authored-by: Clement Desmidt <clement@desmidt.fr>
2023-06-09 17:30:11 +02:00
Dag
ca351edbfe test: use correct path for bridges (#3427) 2023-06-08 23:44:26 +02:00
Dag
8f9eaae338 chore: fix ci (#3426) 2023-06-08 23:37:36 +02:00
Dag
fbaf26e8bf fix(html_format): add spacing below date if author is missing (#3425)
* small ui tweak

* remove unused <div>

* refactor: rename method

* refactor: inline const

* refactor
2023-06-08 23:04:16 +02:00
Simon Alberny
95071d0134 Add RemixAudioBridge (#3424) 2023-06-07 22:37:38 +02:00
Simon Alberny
3f8165207e Add RainLoopBridge (#3423) 2023-06-07 22:36:51 +02:00
Simon Alberny
08be0ad7a5 Add GoAccessBridge (#3422) 2023-06-07 22:36:21 +02:00
Simon Alberny
6fa1f349d9 Add AllocineFRSortiesBridge (#3421) 2023-06-07 22:35:54 +02:00
July
54957d2a03 [GameBananaBridge] Load all full quality screenshots (#3419)
Replaces the low quality preview images used previously
2023-06-06 20:00:55 +02:00
Tone
819e453064 remove newsletter ad from finanzflussBridge (#3417)
* remove newsletter ad

* whitespace
2023-06-02 20:28:29 +02:00
Dag
1636a84c25 fix(spotify): use non-predictable cache key (#3330)
* refactor

* fix(spotify): use non-predictable cache key
2023-06-02 20:22:28 +02:00
Dag
ee498eadf9 fix: move debug mode to config (#3324)
* fix: move debug mode to config

* fix: also move debug_whitelist to .ini config

* fix: move logic back to Debug class

* docs

* docs

* fix: disable debug mode by default

* fix: restore previous behavior for alerts

* fix: center-align alert text
2023-06-02 20:22:09 +02:00
Ryan Stafford
c5cd229445 [YoutubeBridge] Set icon (#3416) 2023-06-01 21:26:47 +02:00
July
845a8f7936 [MangaDexBridge] Add option to add chapter images to entries (#3412) 2023-05-28 18:23:01 +02:00
aysilu-kitsune
2f0784c287 added my instance (#3413) 2023-05-28 18:21:44 +02:00
piyushpaliwal
227c7b8968 Sleeper.com Alerts. Fixes #2234 (#3411)
* Sleeper.com Alerts. Fixes #2234

* fix: linter issue
2023-05-28 01:31:45 +02:00
July
01f731cfa4 [GameBananaBridge] Create new bridge (#3410) 2023-05-26 18:19:34 +02:00
mrnoname1000
87b9f2dd94 [core] Fix XPathAbstract while working around Simple HTML DOM bug (#3408) 2023-05-21 21:06:35 +02:00
Dag
f803ffa79a fix: ArgumentCountError: DOMDocument::getElementsByTagName() expects exactly 1 argument, 2 given, #3406 (#3407) 2023-05-21 19:59:39 +02:00
mrnoname1000
b5dbec4cc1 [AllSidesBridge] New bridge (#3405) 2023-05-20 04:15:56 +02:00
mrnoname1000
3e0d024888 [core] Fix defaultLinkTo for simple_html_dom objects (#3404) 2023-05-20 00:02:17 +02:00
Dag
cfe81ab2ac fix: Call to a member function setAttribute() on int, #3402 (#3403) 2023-05-19 16:05:52 +02:00
mrnoname1000
096c3bca73 [XPathAbstract] Fix relative links in fetched HTML (#3401)
* [core] Make defaultLinkTo compatible with DOMDocument

* [XPathAbstract] Fix relative links in fetched HTML
2023-05-18 13:50:50 +02:00
Tone
ecd717cf58 removing a-collapse (#3394)
it is only used for ads for their magazine
e.g.: https://www.heise.de/news/Eventtipps-fuer-Fotografen-und-Fotografiebegeisterte-9010049.html?seite=all
2023-05-12 23:41:08 +02:00
Tone
0c540b4637 added script to deleted elements in CaschyBridge (#3391)
* added script to deleted elements

Now it works much better with included content like twitter, e.g. in this article:
https://stadt-bremerhaven.de/1password-mit-android-14-wird-man-passkeys-in-chrome-und-apps-unterstuetzen/

* Update CaschyBridge.php

* Update CaschyBridge.php
2023-05-11 21:25:13 +02:00
mrnoname1000
d0f7f5e2d8 [New Bridge] FiderBridge (#3378)
* [core] Add config parameter to markdownToHtml

* [FiderBridge] New bridge
2023-05-11 21:24:12 +02:00
Alexandre Alapetite
e99e026fa8 Use standard Docker logs (#3333)
Instead of storing logs inside the container (where then cannot easily be seen not rotated), consider using the standard Docker approach of writing to standard output
https://docs.docker.com/config/containers/logging/
2023-05-11 01:44:11 +02:00
Dag
50865d5741 fix: add additional cloudflare title (Glassdoor specific) (#3342) 2023-05-11 01:33:38 +02:00
July
dc4134ed1d [ScribbleHubBridge] Add CloudFlare error handling (#3361)
* [ScribbleHubBridge] Set html defaultLinkTo

* [ScrubbleHubBridge] Add CloudFlare error handling
2023-05-11 01:33:21 +02:00
mrnoname1000
c6c4b3a24f [XPathAbstract] Fix encoding on feed title (#3365) 2023-05-11 01:32:01 +02:00
mrnoname1000
63dc500ae0 [XPathAbstract] Save HTML for entry content (#3366) 2023-05-11 01:31:34 +02:00
vincentvd1
723768c828 Add bridge for Magellantv articles (#3368)
* [MagellantvBrdige] added first version

* [MagellantvBridge]  cleanup, added tags and fixed bugs

* [MagellantvBridge] fix linting issues

* [MagellantvBridge] more linting fixes

* [MagellantvBridge] removed tabs
2023-05-11 01:30:25 +02:00
Tone
e7bda080b4 added iframe to $bad (#3380)
iframe can't be rendered in feed reader, so we can delete it
2023-05-10 22:14:34 +02:00
Joseph
8fd677f4ae [GithubTrendingBridge] Fix items (#3381) 2023-05-10 22:14:21 +02:00
Dag
88f646cf12 fix(TwitterBridge): trim screen name before passing it to twitter client (#3389) 2023-05-10 21:59:50 +02:00
Dag
49d105fd70 fix(TwitterBridge): remove ampersand from screen name, api dont like it (#3388) 2023-05-10 21:55:47 +02:00
Dag
ff49c9f731 fix(TwitterBridge): repair fetching of tweets by username (#3385)
* feat: alpha version of new twitter bridge

* fix: refetch guest_token if expired

* fix: purge cache

* fix: safeguards

* fix

* fix: two notices

* fix

* fix: use factory to create cache

* fix: fail properly instead of die()
2023-05-10 21:45:44 +02:00
Max
c628f99928 use lowercase 2023-05-10 18:40:33 +02:00
Tone
f26808d22c added article categories for CaschyBridge (#3379)
* added article categories

* whitespace
2023-05-08 16:21:39 +02:00
Tone
a1b6bca581 added article categories for GolemBridge (#3377)
* added article categories for GolemBridge

* tabs are bad, spaces good

* fixed duplicate categories on multi-page articles
2023-05-08 16:21:03 +02:00
Tone
ec091fb747 fixed authors and added categories for HeiseBridge (#3376) 2023-05-07 12:33:45 +02:00
mrnoname1000
887f4bbe15 [BugzillaBridge] Explicitly request JSON (#3364) 2023-04-27 19:24:29 +02:00
Paul Prechtel
212c56fde5 [HeiseBridge] Handle heise+ articles better (#3358)
- Stop parsing paywalled heise+ articles, as they had garbage content
  and anyways not the full article.
- Link to archive.today to access the full article without account.
  (Automatically getting the full article from archive.ph was not feasible
  b/c of captchas and problems extracting the actual content)
2023-04-20 23:02:08 +02:00
sysadminstory
00e716d84d [PepperBridgeAbstract] Fix "no results" check (#3357)
CSS class for "no results" text has changed, so the bridge has been
updated accordingly.
2023-04-20 11:22:53 +02:00
July
f0c96008bc [ScribbleHubBridge] Create new bridge (#3353)
* [ScribbleHubBridge] Create new bridge

* [ScribbleHubBridge] Improve 'Series' filtering

* [ScribbleHubBridge] Properly fetch feed name

* [ScribbleHubBridge] Fix feed name and set feed URI

* [ScribbleHubBridge] Fix linting violations with phpcbf

* [ScribbleHubBridge] Properly handle html encoding in titles
2023-04-19 20:35:04 +02:00
Eugene Molotov
343fd36671 [core] Remove hardcoded maximum duration of 24 hours in loadCacheValue (#3355) 2023-04-19 17:53:35 +02:00
Paul Prechtel
a4a7473abb [Docs] Fix link to SimpleHTMLDOM documentation (#3354) 2023-04-19 17:51:55 +02:00
Paul Prechtel
4068668de9 [ZeitBridge] Re-add paywall workaround (#3352)
Additionally to the Googlebot User-Agent, a Googlebot IP address has to
be used. For now, we can use `X-Forwarded-For` for this.
2023-04-18 18:41:40 +02:00
Eugene Molotov
7c4591c550 [VkBridge] Add detectParameters (#3351) 2023-04-18 18:41:11 +02:00
Paul Prechtel
0718fdc829 [ZeitBridge] Revert User-Agent (#3350)
The Googlebot User-Agent is no longer sufficient to circumvent the
paywall.
2023-04-17 15:33:14 +02:00
Dawid Wróbel
7eca527160 [eBayBridge] New bridge (#3349)
Fixes #3268
2023-04-15 18:40:49 +02:00
triatic
f1c54d5d55 [TwitterV2.md] Update to reflect price change to Twitter API (#3347) 2023-04-14 21:32:13 +02:00
Korytov Pavel
1ed7bdcddf [InternationalInstituteForStrategicStudiesBridge] Repair and improve bridge (#3338)
* [InternationalInstituteForStrategicStudiesBridge] Repair and improve bridge

* [InternationalInstituteForStrategicStudiesBridge] Fix lint
2023-04-08 22:09:07 +02:00
Paroleen
8486c0f8ca [SpotifyBridge] Add podcasts feed (#3329)
Co-authored-by: Matteo Parolin <matteoparolin99@gmail.com>
2023-03-24 20:34:51 +01:00
Eugene Molotov
249133204e [UI] Remove excessive top margin in all pages and logo in HTML feed preview (#3326) 2023-03-24 13:44:34 +01:00
Eugene Molotov
c8af9f9055 [VkBridge] Make timestamps more accurate (#3325) 2023-03-22 20:32:15 +01:00
125 changed files with 3358 additions and 1815 deletions

10
.github/prtester.py vendored
View File

@@ -52,10 +52,14 @@ def testBridges(bridges,status):
for listing in lists:
selectionvalue = ''
listname = listing.get('name')
if 'optgroup' in listing.contents[0].name:
listing = list(itertools.chain.from_iterable(listing))
cleanlist = []
for option in listing.contents:
if 'optgroup' in option.name:
cleanlist.extend(option)
else:
cleanlist.append(option)
firstselectionentry = 1
for selectionentry in listing:
for selectionentry in cleanlist:
if firstselectionentry:
selectionvalue = selectionentry.get('value')
firstselectionentry = 0

View File

@@ -40,20 +40,26 @@ Check out RSS-Bridge right now on https://rss-bridge.org/bridge01 or find anothe
RSS-Bridge requires php 7.4 (or higher).
### Install with git:
### Install with composer or git
```bash
```shell
cd /var/www
composer create-project --no-dev rss-bridge/rss-bridge
```
```shell
cd /var/www
git clone https://github.com/RSS-Bridge/rss-bridge.git
```
Config:
```shell
# Give the http user write permission to the cache folder
chown www-data:www-data /var/www/rss-bridge/cache
# Optionally copy over the default config file
cp config.default.ini.php config.ini.php
# Optionally copy over the default whitelist file
cp whitelist.default.txt whitelist.txt
```
Example config for nginx:
@@ -169,23 +175,18 @@ Learn more in [bridge api](https://rss-bridge.github.io/rss-bridge/Bridge_API/in
### How to enable all bridges
Write an asterisks to `whitelist.txt`:
enabled_bridges[] = *
echo '*' > whitelist.txt
### How to enable some bridges
Learn more in [enabling briges](https://rss-bridge.github.io/rss-bridge/For_Hosts/Whitelisting.html)
### How to enable a bridge
Add the bridge name to `whitelist.txt`:
echo 'FirefoxAddonsBridge' >> whitelist.txt
```
enabled_bridges[] = TwitchBridge
enabled_bridges[] = GettrBridge
```
### How to enable debug mode
Create a file named `DEBUG`:
touch DEBUG
enable_debug_mode = true
Learn more in [debug mode](https://rss-bridge.github.io/rss-bridge/For_Developers/Debug_mode.html).

View File

@@ -41,18 +41,14 @@ class ConnectivityAction implements ActionInterface
return render_template('connectivity.html.php');
}
$bridgeClassName = $this->bridgeFactory->sanitizeBridgeName($request['bridge']);
if ($bridgeClassName === null) {
throw new \InvalidArgumentException('Bridge name invalid!');
}
$bridgeClassName = $this->bridgeFactory->createBridgeClassName($request['bridge']);
return $this->reportBridgeConnectivity($bridgeClassName);
}
private function reportBridgeConnectivity($bridgeClassName)
{
if (!$this->bridgeFactory->isWhitelisted($bridgeClassName)) {
if (!$this->bridgeFactory->isEnabled($bridgeClassName)) {
throw new \Exception('Bridge is not whitelisted!');
}

View File

@@ -29,7 +29,7 @@ class DetectAction implements ActionInterface
$bridgeFactory = new BridgeFactory();
foreach ($bridgeFactory->getBridgeClassNames() as $bridgeClassName) {
if (!$bridgeFactory->isWhitelisted($bridgeClassName)) {
if (!$bridgeFactory->isEnabled($bridgeClassName)) {
continue;
}

View File

@@ -16,22 +16,19 @@ class DisplayAction implements ActionInterface
{
public function execute(array $request)
{
if (Configuration::getConfig('system', 'enable_maintenance_mode')) {
return new Response('503 Service Unavailable', 503);
}
$bridgeFactory = new BridgeFactory();
$bridgeClassName = null;
if (isset($request['bridge'])) {
$bridgeClassName = $bridgeFactory->sanitizeBridgeName($request['bridge']);
}
if ($bridgeClassName === null) {
throw new \InvalidArgumentException('Bridge name invalid!');
}
$bridgeClassName = $bridgeFactory->createBridgeClassName($request['bridge'] ?? '');
$format = $request['format'] ?? null;
if (!$format) {
throw new \Exception('You must specify a format!');
}
if (!$bridgeFactory->isWhitelisted($bridgeClassName)) {
if (!$bridgeFactory->isEnabled($bridgeClassName)) {
throw new \Exception('This bridge is not whitelisted');
}
@@ -41,23 +38,22 @@ class DisplayAction implements ActionInterface
$bridge = $bridgeFactory->create($bridgeClassName);
$bridge->loadConfiguration();
$noproxy = array_key_exists('_noproxy', $request)
&& filter_var($request['_noproxy'], FILTER_VALIDATE_BOOLEAN);
if (Configuration::getConfig('proxy', 'url') && Configuration::getConfig('proxy', 'by_bridge') && $noproxy) {
$noproxy = $request['_noproxy'] ?? null;
if (
Configuration::getConfig('proxy', 'url')
&& Configuration::getConfig('proxy', 'by_bridge')
&& $noproxy
) {
// This const is only used once in getContents()
define('NOPROXY', true);
}
if (array_key_exists('_cache_timeout', $request)) {
if (! Configuration::getConfig('cache', 'custom_timeout')) {
unset($request['_cache_timeout']);
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) . '?' . http_build_query($request);
return new Response('', 301, ['Location' => $uri]);
}
$cache_timeout = filter_var($request['_cache_timeout'], FILTER_VALIDATE_INT);
$cacheTimeout = $request['_cache_timeout'] ?? null;
if (Configuration::getConfig('cache', 'custom_timeout') && $cacheTimeout) {
$cacheTimeout = (int) $cacheTimeout;
} else {
$cache_timeout = $bridge->getCacheTimeout();
// At this point the query argument might still be in the url but it won't be used
$cacheTimeout = $bridge->getCacheTimeout();
}
// Remove parameters that don't concern bridges
@@ -91,23 +87,22 @@ class DisplayAction implements ActionInterface
)
);
$cacheFactory = new CacheFactory();
$cache = $cacheFactory->create();
$cache = RssBridge::getCache();
$cache->setScope('');
$cache->purgeCache(86400); // 24 hours
$cache->setKey($cache_params);
// This cache purge will basically delete all cache items older than 24h, regardless of scope and key
$cache->purgeCache(86400);
$items = [];
$infos = [];
$mtime = $cache->getTime();
if (
$mtime !== false
&& (time() - $cache_timeout < $mtime)
$mtime
&& (time() - $cacheTimeout < $mtime)
&& !Debug::isEnabled()
) {
// At this point we found the feed in the cache
// At this point we found the feed in the cache and debug mode is disabled
if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
// The client wants to know if the feed has changed since its last check
@@ -118,7 +113,7 @@ class DisplayAction implements ActionInterface
}
}
// Fetch the cached feed from the cache and prepare it
// Load the feed from cache and prepare it
$cached = $cache->loadData();
if (isset($cached['items']) && isset($cached['extraInfos'])) {
foreach ($cached['items'] as $item) {
@@ -127,7 +122,7 @@ class DisplayAction implements ActionInterface
$infos = $cached['extraInfos'];
}
} else {
// At this point we did NOT find the feed in the cache. So invoke the bridge!
// At this point we did NOT find the feed in the cache or debug mode is enabled.
try {
$bridge->setDatas($bridge_params);
$bridge->collectData();
@@ -169,6 +164,9 @@ class DisplayAction implements ActionInterface
}
}
// Unfortunately need to set scope and key again because they might be modified
$cache->setScope('');
$cache->setKey($cache_params);
$cache->saveData([
'items' => array_map(function (FeedItem $item) {
return $item->toArray();
@@ -200,7 +198,7 @@ class DisplayAction implements ActionInterface
$item->setURI(get_current_url());
$item->setTimestamp(time());
// Create a item identifier for feed readers e.g. "staysafetv twitch videos_19389"
// Create an item identifier for feed readers e.g. "staysafetv twitch videos_19389"
$item->setUid($bridge->getName() . '_' . $uniqueIdentifier);
$content = render_template(__DIR__ . '/../templates/bridge-error.html.php', [
@@ -215,11 +213,10 @@ class DisplayAction implements ActionInterface
private static function logBridgeError($bridgeName, $code)
{
$cacheFactory = new CacheFactory();
$cache = $cacheFactory->create();
$cache = RssBridge::getCache();
$cache->setScope('error_reporting');
$cache->setkey([$bridgeName . '_' . $code]);
$cache->purgeCache(86400); // 24 hours
if ($report = $cache->loadData()) {
$report = Json::decode($report);
$report['time'] = time();

View File

@@ -15,7 +15,7 @@ final class FrontpageAction implements ActionInterface
$body = '';
foreach ($bridgeClassNames as $bridgeClassName) {
if ($bridgeFactory->isWhitelisted($bridgeClassName)) {
if ($bridgeFactory->isEnabled($bridgeClassName)) {
$body .= BridgeCard::displayBridgeCard($bridgeClassName, $formats);
$activeBridges++;
} elseif ($showInactive) {
@@ -24,6 +24,7 @@ final class FrontpageAction implements ActionInterface
}
return render(__DIR__ . '/../templates/frontpage.html.php', [
'messages' => [],
'admin_email' => Configuration::getConfig('admin', 'email'),
'admin_telegram' => Configuration::getConfig('admin', 'telegram'),
'bridges' => $body,

15
actions/HealthAction.php Normal file
View File

@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
class HealthAction implements ActionInterface
{
public function execute(array $request)
{
$response = [
'code' => 200,
'message' => 'all is good',
];
return new Response(Json::encode($response), 200, ['content-type' => 'application/json']);
}
}

View File

@@ -26,7 +26,7 @@ class ListAction implements ActionInterface
$bridge = $bridgeFactory->create($bridgeClassName);
$list->bridges[$bridgeClassName] = [
'status' => $bridgeFactory->isWhitelisted($bridgeClassName) ? 'active' : 'inactive',
'status' => $bridgeFactory->isEnabled($bridgeClassName) ? 'active' : 'inactive',
'uri' => $bridge->getURI(),
'donationUri' => $bridge->getDonationURI(),
'name' => $bridge->getName(),

View File

@@ -21,19 +21,12 @@ class SetBridgeCacheAction implements ActionInterface
$key = $request['key'] or returnClientError('You must specify key!');
$bridgeFactory = new \BridgeFactory();
$bridgeFactory = new BridgeFactory();
$bridgeClassName = null;
if (isset($request['bridge'])) {
$bridgeClassName = $bridgeFactory->sanitizeBridgeName($request['bridge']);
}
if ($bridgeClassName === null) {
throw new \InvalidArgumentException('Bridge name invalid!');
}
$bridgeClassName = $bridgeFactory->createBridgeClassName($request['bridge'] ?? '');
// whitelist control
if (!$bridgeFactory->isWhitelisted($bridgeClassName)) {
if (!$bridgeFactory->isEnabled($bridgeClassName)) {
throw new \Exception('This bridge is not whitelisted', 401);
die;
}
@@ -42,10 +35,12 @@ class SetBridgeCacheAction implements ActionInterface
$bridge->loadConfiguration();
$value = $request['value'];
$cacheFactory = new CacheFactory();
$cache = $cacheFactory->create();
$cache = RssBridge::getCache();
$cache->setScope(get_class($bridge));
if (!is_array($key)) {
// not sure if $key is an array when it comes in from request
$key = [$key];
}
$cache->setKey($key);
$cache->saveData($value);

116
bridges/ABolaBridge.php Normal file
View File

@@ -0,0 +1,116 @@
<?php
class ABolaBridge extends BridgeAbstract
{
const NAME = 'A Bola';
const URI = 'https://abola.pt/';
const DESCRIPTION = 'Returns news from the Portuguese sports newspaper A BOLA.PT';
const MAINTAINER = 'rmscoelho';
const CACHE_TIMEOUT = 3600;
const PARAMETERS = [
[
'feed' => [
'name' => 'News Feed',
'type' => 'list',
'title' => 'Feeds from the Portuguese sports newspaper A BOLA.PT',
'values' => [
'Últimas' => 'Nnh/Noticias',
'Seleção Nacional' => 'Selecao/Noticias',
'Futebol Nacional' => [
'Notícias' => 'Nacional/Noticias',
'Primeira Liga' => 'Nacional/Liga/Noticias',
'Liga 2' => 'Nacional/Liga2/Noticias',
'Liga 3' => 'Nacional/Liga3/Noticias',
'Liga Revelação' => 'Nacional/Liga-Revelacao/Noticias',
'Campeonato de Portugal' => 'Nacional/Campeonato-Portugal/Noticias',
'Distritais' => 'Nacional/Distritais/Noticias',
'Taça de Portugal' => 'Nacional/TPortugal/Noticias',
'Futebol Feminino' => 'Nacional/FFeminino/Noticias',
'Futsal' => 'Nacional/Futsal/Noticias',
],
'Futebol Internacional' => [
'Notícias' => 'Internacional/Noticias/Noticias',
'Liga dos Campeões' => 'Internacional/Liga-dos-campeoes/Noticias',
'Liga Europa' => 'Internacional/Liga-europa/Noticias',
'Liga Conferência' => 'Internacional/Liga-conferencia/Noticias',
'Liga das Nações' => 'Internacional/Liga-das-nacoes/Noticias',
'UEFA Youth League' => 'Internacional/Uefa-Youth-League/Noticias',
],
'Mercado' => 'Mercado',
'Modalidades' => 'Modalidades/Noticias',
'Motores' => 'Motores/Noticias',
]
]
]
];
public function getIcon()
{
return 'https://abola.pt/img/icons/favicon-96x96.png';
}
public function getName()
{
return !is_null($this->getKey('feed')) ? self::NAME . ' | ' . $this->getKey('feed') : self::NAME;
}
public function getURI()
{
return self::URI . $this->getInput('feed');
}
public function collectData()
{
$url = sprintf('https://abola.pt/%s', $this->getInput('feed'));
$dom = getSimpleHTMLDOM($url);
if ($this->getInput('feed') !== 'Mercado') {
$dom = $dom->find('div#body_Todas1_upNoticiasTodas', 0);
} else {
$dom = $dom->find('div#body_NoticiasMercado_upNoticiasTodas', 0);
}
if (!$dom) {
throw new \Exception(sprintf('Unable to find css selector on `%s`', $url));
}
$dom = defaultLinkTo($dom, $this->getURI());
foreach ($dom->find('div.media') as $key => $article) {
//Get thumbnail
$image = $article->find('.media-img', 0)->style;
$image = preg_replace('/background-image: url\(/i', '', $image);
$image = substr_replace($image, '', -4);
$image = preg_replace('/https:\/\//i', '', $image);
$image = preg_replace('/www\./i', '', $image);
$image = preg_replace('/\/\//', '/', $image);
$image = preg_replace('/\/\/\//', '//', $image);
$image = substr($image, 7);
$image = 'https://' . $image;
$image = preg_replace('/ptimg/', 'pt/img', $image);
$image = preg_replace('/\/\/bola/', 'www.abola', $image);
//Timestamp
$date = date('Y/m/d');
if (!is_null($article->find("span#body_Todas1_rptNoticiasTodas_lblData_$key", 0))) {
$date = $article->find("span#body_Todas1_rptNoticiasTodas_lblData_$key", 0)->plaintext;
$date = preg_replace('/\./', '/', $date);
}
$time = $article->find("span#body_Todas1_rptNoticiasTodas_lblHora_$key", 0)->plaintext;
$date = explode('/', $date);
$time = explode(':', $time);
$year = $date[0];
$month = $date[1];
$day = $date[2];
$hour = $time[0];
$minute = $time[1];
$timestamp = mktime($hour, $minute, 0, $month, $day, $year);
//Content
$image = '<img src="' . $image . '" alt="' . $article->find('h4 span', 0)->plaintext . '" />';
$description = '<p>' . $article->find('.media-texto > span', 0)->plaintext . '</p>';
$content = $image . '</br>' . $description;
$a = $article->find('.media-body > a', 0);
$this->items[] = [
'title' => $a->find('h4 span', 0)->plaintext,
'uri' => $a->href,
'content' => $content,
'timestamp' => $timestamp,
];
}
}
}

View File

@@ -34,7 +34,12 @@ class ASRockNewsBridge extends BridgeAbstract
$item['content'] = $contents->innertext;
$item['timestamp'] = $this->extractDate($a->plaintext);
$item['enclosures'][] = $a->find('img', 0)->src;
$img = $a->find('img', 0);
if ($img) {
$item['enclosures'][] = $img->src;
}
$this->items[] = $item;
if (count($this->items) >= 10) {

View File

@@ -0,0 +1,85 @@
<?php
class AllSidesBridge extends BridgeAbstract
{
const NAME = 'AllSides';
const URI = 'https://www.allsides.com';
const DESCRIPTION = 'Balanced news and media bias ratings.';
const MAINTAINER = 'Oliver Nutter';
const PARAMETERS = [
'global' => [
'limit' => [
'name' => 'Number of posts to return',
'type' => 'number',
'defaultValue' => 10,
'required' => false,
'title' => 'Zero or negative values return all posts (ignored if not fetching full article)',
],
'fetch' => [
'name' => 'Fetch full article content',
'type' => 'checkbox',
'defaultValue' => 'checked',
],
],
'Headline Roundups' => [],
];
private const ROUNDUPS_URI = self::URI . '/headline-roundups';
public function collectData()
{
switch ($this->queriedContext) {
case 'Headline Roundups':
$index = getSimpleHTMLDOM(self::ROUNDUPS_URI);
defaultLinkTo($index, self::ROUNDUPS_URI);
$entries = $index->find('table.views-table > tbody > tr');
$limit = (int) $this->getInput('limit');
$fetch = (bool) $this->getInput('fetch');
if ($limit > 0 && $fetch) {
$entries = array_slice($entries, 0, $limit);
}
foreach ($entries as $entry) {
$item = [
'title' => $entry->find('.views-field-name', 0)->text(),
'uri' => $entry->find('a', 0)->href,
'timestamp' => $entry->find('.date-display-single', 0)->content,
'author' => 'AllSides Staff',
];
if ($fetch) {
$article = getSimpleHTMLDOMCached($item['uri']);
defaultLinkTo($article, $item['uri']);
$item['content'] = $article->find('.story-id-page-description', 0);
foreach ($article->find('.page-tags a') as $tag) {
$item['categories'][] = $tag->text();
}
}
$this->items[] = $item;
}
break;
}
}
public function getName()
{
if ($this->queriedContext) {
return self::NAME . " - {$this->queriedContext}";
}
return self::NAME;
}
public function getURI()
{
switch ($this->queriedContext) {
case 'Headline Roundups':
return self::ROUNDUPS_URI;
}
return self::URI;
}
}

View File

@@ -0,0 +1,41 @@
<?php
class AllocineFRSortiesBridge extends BridgeAbstract
{
const MAINTAINER = 'Simounet';
const NAME = 'AlloCiné Sorties Bridge';
const CACHE_TIMEOUT = 25200; // 7h
const BASE_URI = 'https://www.allocine.fr';
const URI = self::BASE_URI . '/film/sorties-semaine/';
const DESCRIPTION = 'Bridge for AlloCiné - Sorties cinéma cette semaine';
public function getName()
{
return self::NAME;
}
public function collectData()
{
$html = getSimpleHTMLDOM($this->getURI());
foreach ($html->find('section.section.section-wrap', 0)->find('li.mdl') as $element) {
$item = [];
$thumb = $element->find('figure.thumbnail', 0);
$meta = $element->find('div.meta-body', 0);
$synopsis = $element->find('div.synopsis', 0);
$title = $element->find('a[class*=meta-title-link]', 0);
$content = trim(defaultLinkTo($thumb->outertext . $meta->outertext . $synopsis->outertext, static::URI));
// Replace image 'src' with the one in 'data-src'
$content = preg_replace('@src="data:image/gif;base64,[A-Za-z0-9=+\/]*"@', '', $content);
$content = preg_replace('@data-src=@', 'src=', $content);
$item['content'] = $content;
$item['title'] = trim($title->innertext);
$item['uri'] = static::BASE_URI . '/' . substr($title->href, 1);
$this->items[] = $item;
}
}
}

View File

@@ -85,7 +85,7 @@ class BugzillaBridge extends BridgeAbstract
protected function getTitle($url)
{
// Only request the summary for a faster request
$json = json_decode(getContents($url . '?include_fields=summary'), true);
$json = self::getJSON($url . '?include_fields=summary');
$this->title = 'Bug ' . $this->bugid . ' - ' .
$json['bugs'][0]['summary'] . ' - ' .
// Remove https://
@@ -94,7 +94,7 @@ class BugzillaBridge extends BridgeAbstract
protected function collectComments($url)
{
$json = json_decode(getContents($url), true);
$json = self::getJSON($url);
// Array of comments is here
if (!isset($json['bugs'][$this->bugid]['comments'])) {
@@ -127,7 +127,7 @@ class BugzillaBridge extends BridgeAbstract
protected function collectUpdates($url)
{
$json = json_decode(getContents($url), true);
$json = self::getJSON($url);
// Array of changesets which contain an array of changes
if (!isset($json['bugs']['0']['history'])) {
@@ -159,7 +159,7 @@ class BugzillaBridge extends BridgeAbstract
protected function getUser($user)
{
// Check if the user endpoint is available
if ($this->loadCacheValue($this->instance . 'userEndpointClosed')) {
if ($this->loadCacheValue($this->instance . 'userEndpointClosed', 86400)) {
return $user;
}
@@ -170,7 +170,7 @@ class BugzillaBridge extends BridgeAbstract
$url = $this->instance . '/rest/user/' . $user . '?include_fields=real_name';
try {
$json = json_decode(getContents($url), true);
$json = self::getJSON($url);
if (isset($json['error']) and $json['error']) {
throw new Exception();
}
@@ -187,4 +187,12 @@ class BugzillaBridge extends BridgeAbstract
$this->saveCacheValue($this->instance . $user, $username);
return $username;
}
protected static function getJSON($url)
{
$headers = [
'Accept: application/json',
];
return json_decode(getContents($url, $headers), true);
}
}

View File

@@ -55,12 +55,20 @@ class CaschyBridge extends FeedExpander
private function addArticleToItem($item, $article)
{
// remove unwanted stuff
foreach ($article->find('div.video-container, div.aawp, p.aawp-disclaimer, iframe.wp-embedded-content, div.wp-embed, p.wp-caption-text') as $element) {
foreach (
$article->find('div.video-container, div.aawp, p.aawp-disclaimer, iframe.wp-embedded-content,
div.wp-embed, p.wp-caption-text, script') as $element
) {
$element->remove();
}
// reload html, as remove() is buggy
$article = str_get_html($article->outertext);
$categories = $article->find('div.post-category a');
foreach ($categories as $category) {
$item['categories'][] = $category->plaintext;
}
$content = $article->find('div.entry-inner', 0);
$item['content'] = $content;

View File

@@ -0,0 +1,75 @@
<?php
class CorreioDaFeiraBridge extends BridgeAbstract
{
const NAME = 'Correio da Feira';
const URI = 'https://www.correiodafeira.pt/';
const DESCRIPTION = 'Returns news from the Portuguese local newspaper Correio da Feira';
const MAINTAINER = 'rmscoelho';
const CACHE_TIMEOUT = 86400;
const PARAMETERS = [
[
'feed' => [
'name' => 'News Feed',
'type' => 'list',
'title' => 'Feeds from the Portuguese sports newspaper A BOLA.PT',
'values' => [
'Cultura' => 'cultura',
'Desporto' => 'desporto',
'Economia' => 'economia',
'Entrevista' => 'entrevista',
'Freguesias' => 'freguesias',
'Justiça' => 'justica',
'Opinião' => 'opiniao',
'Política' => 'politica',
'Reportagem' => 'reportagem',
'Sociedade' => 'sociedade',
'Tecnologia' => 'tecnologia',
]
]
]
];
public function getIcon()
{
return 'https://www.correiodafeira.pt/wp-content/uploads/base_reporter-200x200.jpg';
}
public function getName()
{
return !is_null($this->getKey('feed')) ? self::NAME . ' | ' . $this->getKey('feed') : self::NAME;
}
public function getURI()
{
return self::URI . $this->getInput('feed');
}
public function collectData()
{
$url = sprintf('https://www.correiodafeira.pt/categoria/%s', $this->getInput('feed'));
$dom = getSimpleHTMLDOM($url);
$dom = $dom->find('main', 0);
if (!$dom) {
throw new \Exception(sprintf('Unable to find css selector on `%s`', $url));
}
$dom = defaultLinkTo($dom, $this->getURI());
foreach ($dom->find('div.post') as $article) {
$a = $article->find('div.blog-box', 0);
//Get date and time of publishing
$time = $a->find('.post-date > :nth-child(2)', 0)->plaintext;
$datetime = explode('/', $time);
$year = $datetime[2];
$month = $datetime[1];
$day = $datetime[0];
$timestamp = mktime(0, 0, 0, $month, $day, $year);
$this->items[] = [
'title' => $a->find('h2.entry-title > a', 0)->plaintext,
'uri' => $a->find('h2.entry-title > a', 0)->href,
'author' => $a->find('li.post-author > a', 0)->plaintext,
'content' => $a->find('.entry-content > p', 0)->plaintext,
'timestamp' => $timestamp,
];
}
}
}

View File

@@ -63,7 +63,7 @@ class CraigslistBridge extends BridgeAbstract
$html = getSimpleHTMLDOM($uri);
// Check if no results page is shown (nearby results)
if ($html->find('.displaycountShow', 0)->plaintext == '0') {
if (($html->find('.displaycountShow', 0)->plaintext ?? '') == '0') {
return;
}

View File

@@ -23,7 +23,10 @@ class CuriousCatBridge extends BridgeAbstract
$apiJson = getContents($url);
$apiData = json_decode($apiJson, true);
$apiData = Json::decode($apiJson);
if (isset($apiData['error'])) {
throw new \Exception($apiData['error_code']);
}
foreach ($apiData['posts'] as $post) {
$item = [];

102
bridges/EBayBridge.php Normal file
View File

@@ -0,0 +1,102 @@
<?php
class EBayBridge extends BridgeAbstract
{
const NAME = 'eBay';
const DESCRIPTION = 'Returns the search results from the eBay auctioning platforms';
const URI = 'https://www.eBay.com';
const MAINTAINER = 'wrobelda';
const PARAMETERS = [[
'url' => [
'name' => 'Search URL',
'title' => 'Copy the URL from your browser\'s address bar after searching for your items and paste it here',
'pattern' => '^(https:\/\/)?(www.)?ebay\.(com|com\.au|at|be|ca|ch|cn|es|fr|de|com\.hk|ie|it|com\.my|nl|ph|pl|com\.sg|co\.uk).*$',
'exampleValue' => 'https://www.ebay.com/sch/i.html?_nkw=atom+rss',
'required' => true,
]
]];
public function getURI()
{
if ($this->getInput('url')) {
# make sure we order by the most recently listed offers
$uri = trim(preg_replace('/([?&])_sop=[^&]+(&|$)/', '$1', $this->getInput('url')), '?&/');
$uri .= (parse_url($uri, PHP_URL_QUERY) ? '&' : '?') . '_sop=10';
return $uri;
} else {
return parent::getURI();
}
}
public function getName()
{
$urlQueries = explode('&', parse_url($this->getInput('url'), PHP_URL_QUERY));
$searchQuery = array_reduce($urlQueries, function ($q, $p) {
if (preg_match('/^_nkw=(.+)$/i', $p, $matches)) {
$q[] = str_replace('+', ' ', urldecode($matches[1]));
}
return $q;
});
if ($searchQuery) {
return $searchQuery[0];
}
return parent::getName();
}
public function collectData()
{
$html = getSimpleHTMLDOM($this->getURI());
// Remove any unsolicited results, e.g. "Results matching fewer words"
foreach ($html->find('ul.srp-results > li.srp-river-answer--REWRITE_START ~ li') as $inexactMatches) {
$inexactMatches->remove();
}
$results = $html->find('ul.srp-results > li.s-item');
foreach ($results as $listing) {
$item = [];
// Remove "NEW LISTING" label, we sort by the newest, so this is redundant
foreach ($listing->find('.LIGHT_HIGHLIGHT') as $new_listing_label) {
$new_listing_label->remove();
}
$item['title'] = $listing->find('.s-item__title', 0)->plaintext;
$subtitle = implode('', $listing->find('.s-item__subtitle'));
$item['uri'] = $listing->find('.s-item__link', 0)->href;
preg_match('/.*\/itm\/(\d+).*/i', $item['uri'], $matches);
$item['uid'] = $matches[1];
$price = $listing->find('.s-item__details > .s-item__detail > .s-item__price', 0)->plaintext;
$shippingFree = $listing->find('.s-item__details > .s-item__detail > .s-item__freeXDays', 0)->plaintext ?? '';
$localDelivery = $listing->find('.s-item__details > .s-item__detail > .s-item__localDelivery', 0)->plaintext ?? '';
$logisticsCost = $listing->find('.s-item__details > .s-item__detail > .s-item__logisticsCost', 0)->plaintext ?? '';
$location = $listing->find('.s-item__details > .s-item__detail > .s-item__location', 0)->plaintext ?? '';
$sellerInfo = $listing->find('.s-item__seller-info-text', 0)->plaintext ?? '';
$image = $listing->find('.s-item__image-wrapper > img', 0);
if ($image) {
// Not quite sure why append fragment here
$imageUrl = $image->src . '#.image';
$item['enclosures'] = [$imageUrl];
}
$item['content'] = <<<CONTENT
<p>$sellerInfo $location</p>
<p><span style="font-weight:bold">$price</span> $shippingFree $localDelivery $logisticsCost<span></span></p>
<p>$subtitle</p>
CONTENT;
$this->items[] = $item;
}
}
}

View File

@@ -48,7 +48,7 @@ class EZTVBridge extends BridgeAbstract
public function collectData()
{
$eztv_uri = $this->getEztvUri();
Debug::log($eztv_uri);
Logger::debug($eztv_uri);
$ids = explode(',', trim($this->getInput('ids')));
foreach ($ids as $id) {
$data = json_decode(getContents(sprintf('%s/api/get-torrents?imdb_id=%s', $eztv_uri, $id)));

View File

@@ -113,9 +113,7 @@ class ElloBridge extends BridgeAbstract
private function getAPIKey()
{
$cacheFactory = new CacheFactory();
$cache = $cacheFactory->create();
$cache = RssBridge::getCache();
$cache->setScope('ElloBridge');
$cache->setKey(['key']);
$key = $cache->loadData();

View File

@@ -47,11 +47,11 @@ class EtsyBridge extends BridgeAbstract
$item['title'] = $result->find('a', 0)->title;
$item['uri'] = $result->find('a', 0)->href;
$item['author'] = $result->find('p.wt-text-gray > span', 2)->plaintext;
$item['author'] = $result->find('p.wt-text-gray > span', 2)->plaintext ?? '';
$item['content'] = '<p>'
. $result->find('span.currency-symbol', 0)->plaintext
. $result->find('span.currency-value', 0)->plaintext
. ($result->find('span.currency-symbol', 0)->plaintext ?? '')
. ($result->find('span.currency-value', 0)->plaintext ?? '')
. '</p><p>'
. $result->find('a', 0)->title
. '</p>';

View File

@@ -14,7 +14,7 @@ TEXT;
'feed_name' => [
'name' => 'Feed name',
'type' => 'text',
'exampleValue' => 'rss-bridge/FeedMerger',
'exampleValue' => 'FeedMerge',
],
'feed_1' => [
'name' => 'Feed url',
@@ -58,9 +58,29 @@ TEXT;
$feeds = array_filter($feeds);
foreach ($feeds as $feed) {
// Fetch all items from the feed
// todo: consider wrapping this in a try..catch to not let a single feed break the entire bridge?
$this->collectExpandableDatas($feed);
if (count($feeds) > 1) {
// Allow one or more feeds to fail
try {
$this->collectExpandableDatas($feed);
} catch (HttpException $e) {
Logger::warning(sprintf('Exception in FeedMergeBridge: %s', create_sane_exception_message($e)));
$this->items[] = [
'title' => 'RSS-Bridge: ' . $e->getMessage(),
// Give current time so it sorts to the top
'timestamp' => time(),
];
continue;
} catch (\Exception $e) {
if (str_starts_with($e->getMessage(), 'Unable to parse xml')) {
// Allow this particular exception from FeedExpander
Logger::warning(sprintf('Exception in FeedMergeBridge: %s', create_sane_exception_message($e)));
continue;
}
throw $e;
}
} else {
$this->collectExpandableDatas($feed);
}
}
// Sort by timestamp descending
@@ -91,6 +111,6 @@ TEXT;
public function getName()
{
return $this->getInput('feed_name') ?: 'rss-bridge/FeedMerger';
return $this->getInput('feed_name') ?: 'FeedMerge';
}
}

132
bridges/FiderBridge.php Normal file
View File

@@ -0,0 +1,132 @@
<?php
class FiderBridge extends BridgeAbstract
{
const NAME = 'Fider Bridge';
const URI = 'https://fider.io/';
const DESCRIPTION = 'Bridge for any Fider instance';
const MAINTAINER = 'Oliver Nutter';
const PARAMETERS = [
'global' => [
'instance' => [
'name' => 'Instance URL',
'required' => true,
'example' => 'https://feedback.fider.io',
],
],
'Post' => [
'num' => [
'name' => 'Post Number',
'type' => 'number',
'required' => true,
],
'limit' => [
'name' => 'Number of comments to return',
'type' => 'number',
'required' => false,
'title' => 'Specify number of comments to return',
],
],
];
private $instance;
private $posturi;
private $title;
public function getName()
{
return $this->title ?? parent::getName();
}
public function getURI()
{
return $this->posturi ?? parent::getURI();
}
protected function setTitle($title)
{
$html = getSimpleHTMLDOMCached($this->instance);
$name = $html->find('title', 0)->innertext;
$this->title = "$title - $name";
}
protected function getItem($post, $response = false, $first = false)
{
$item = [];
$item['uri'] = $this->getURI();
$item['timestamp'] = $response ? $post->respondedAt : $post->createdAt;
$item['author'] = $post->user->name;
$datetime = new DateTime($item['timestamp']);
if ($response) {
$item['uid'] = 'response';
$item['content'] = $post->text;
$item['title'] = "{$item['author']} marked as $post->status {$datetime->format('M d, Y')}";
} elseif ($first) {
$item['uid'] = 'post';
$item['content'] = $post->description;
$item['title'] = $post->title;
} else {
$item['uid'] = 'comment';
$item['content'] = $post->content;
$item['title'] = "{$item['author']} commented {$datetime->format('M d, Y')}";
}
$item['uid'] .= $item['author'] . $item['timestamp'];
// parse markdown with implicit line breaks
$item['content'] = markdownToHtml($item['content'], ['breaksEnabled' => true]);
if (property_exists($post, 'editedAt')) {
$item['title'] .= ' (edited)';
}
if ($first) {
$item['categories'] = $post->tags;
}
return $item;
}
public function collectData()
{
// collect first post
$this->instance = rtrim($this->getInput('instance'), '/');
$num = $this->getInput('num');
$this->posturi = "$this->instance/posts/$num";
$post_api_uri = "$this->instance/api/v1/posts/$num";
$post = json_decode(getContents($post_api_uri));
$this->setTitle($post->title);
$item = $this->getItem($post, false, true);
$this->items[] = $item;
// collect response to first post
if (property_exists($post, 'response')) {
$response = $post->response;
$response->status = $post->status;
$this->items[] = $this->getItem($response, true);
}
// collect comments
$comment_api_uri = "$post_api_uri/comments";
$comments = json_decode(getContents($comment_api_uri));
foreach ($comments as $post) {
$item = $this->getItem($post);
$this->items[] = $item;
}
usort($this->items, function ($a, $b) {
return $b['timestamp'] <=> $a['timestamp'];
});
if ($this->getInput('limit') ?? 0 > 0) {
$this->items = array_slice($this->items, 0, $this->getInput('limit'));
}
}
}

View File

@@ -36,6 +36,11 @@ class FinanzflussBridge extends BridgeAbstract
$img->srcset = $baseurl . $src;
}
//remove unwanted stuff
foreach ($content->find('div.newsletter-signup') as $element) {
$element->remove();
}
//get author
$author = $domarticle->find('div.author-name', 0);

View File

@@ -900,10 +900,16 @@ class FurAffinityBridge extends BridgeAbstract
$submissionHTML = $this->getFASimpleHTMLDOM($submissionURL, $cache);
$stats = $submissionHTML->find('.stats-container', 0);
$item['timestamp'] = strtotime($stats->find('.popup_date', 0)->title);
$item['enclosures'] = [
$submissionHTML->find('.actions a[href^=https://d.facdn]', 0)->href
];
$popupDate = $stats->find('.popup_date', 0);
if ($popupDate) {
$item['timestamp'] = strtotime($popupDate->title);
}
$var = $submissionHTML->find('.actions a[href^=https://d.facdn]', 0);
if ($var) {
$item['enclosures'] = [$var->href];
}
foreach ($stats->find('#keywords a') as $keyword) {
$item['categories'][] = $keyword->plaintext;
}

View File

@@ -90,7 +90,7 @@ class FuturaSciencesBridge extends FeedExpander
$item = parent::parseItem($newsItem);
$item['uri'] = str_replace('#xtor%3DRSS-8', '', $item['uri']);
$article = getSimpleHTMLDOMCached($item['uri']);
$item['content'] = $this->extractArticleContent($article);
//$item['content'] = $this->extractArticleContent($article);
$author = $this->extractAuthor($article);
if (!empty($author)) {
$item['author'] = $author;

View File

@@ -0,0 +1,96 @@
<?php
class GameBananaBridge extends BridgeAbstract
{
const NAME = 'GameBanana';
const MAINTAINER = 'phantop';
const URI = 'https://gamebanana.com/';
const DESCRIPTION = 'Returns mods from GameBanana.';
const PARAMETERS = [
'Game' => [
'gid' => [
'name' => 'Game ID',
'required' => true,
// Example: latest mods from Zelda: Tears of the Kingdom
'exampleValue' => '7617',
],
'updates' => [
'name' => 'Get updates',
'type' => 'checkbox',
'required' => false,
'title' => 'Enable game updates in feed'
],
]
];
public function getIcon()
{
return 'https://images.gamebanana.com/static/img/favicon/favicon.ico';
}
public function collectData()
{
$url = 'https://api.gamebanana.com/Core/List/New?itemtype=Mod&page=1&gameid=' . $this->getInput('gid');
if ($this->getInput('updates')) {
$url .= '&include_updated=1';
}
$api_response = getContents($url);
$json_list = json_decode($api_response, true); // Get first page mod list
$url = 'https://api.gamebanana.com/Core/Item/Data?itemtype[]=Game&fields[]=name&itemid[]=' . $this->getInput('gid');
$fields = 'name,Owner().name,text,screenshots,Files().aFiles(),date,Url().sProfileUrl(),udate';
foreach ($json_list as $element) { // Build api request to minimize API calls
$mid = $element[1];
$url .= '&itemtype[]=Mod&fields[]=' . $fields . '&itemid[]=' . $mid;
}
$api_response = getContents($url);
$json_list = json_decode($api_response, true);
$this->title = $json_list[0][0];
array_shift($json_list); // Take title from API request and remove from json
foreach ($json_list as $element) {
$item = [];
$item['uri'] = $element[6];
$item['comments'] = $item['uri'] . '#PostsListModule';
$item['title'] = $element[0];
$item['author'] = $element[1];
$item['timestamp'] = $element[5];
if ($this->getInput('updates')) {
$item['timestamp'] = $element[7];
}
$item['enclosures'] = [];
foreach ($element[4] as $file) { // Place mod downloads in enclosures
array_push($item['enclosures'], 'https://files.gamebanana.com/mods/' . $file['_sFile']);
}
// Get screenshots from element[3]
$img_list = json_decode($element[3], true);
$item['content'] = '';
foreach ($img_list as $img_element) {
$item['content'] .= '<img src="https://images.gamebanana.com/img/ss/mods/' . $img_element['_sFile'] . '"/>';
}
$item['content'] .= '<br>' . $element[2];
$item['uid'] = $item['uri'] . $item['title'] . $item['timestamp'];
$this->items[] = $item;
}
}
public function getName()
{
$name = parent::getName();
if (isset($this->title)) {
$name .= " - $this->title";
}
return $name;
}
public function getURI()
{
$uri = parent::getURI() . 'games/' . $this->getInput('gid');
return $uri;
}
}

View File

@@ -20,11 +20,13 @@ class GatesNotesBridge extends BridgeAbstract
$apiUrl = self::URI . $api_endpoint . http_build_query($params);
$rawContent = getContents($apiUrl);
$cleanedContent = str_replace('\r\n', '', substr($rawContent, 1, -1));
$cleanedContent = str_replace('\"', '"', $cleanedContent);
$cleanedContent = str_replace([
'<string xmlns="http://schemas.microsoft.com/2003/10/Serialization/">',
'</string>',
], '', $rawContent);
// The content is actually a json between quotes with \r\n inserted
$json = json_decode($cleanedContent);
$json = Json::decode($cleanedContent, false);
foreach ($json as $article) {
$item = [];
@@ -57,8 +59,10 @@ class GatesNotesBridge extends BridgeAbstract
$article_html = defaultLinkTo($article_html, $this->getURI());
$top_description = '<p>' . $article_html->find('div.article_top_description', 0)->innertext . '</p>';
$hero_image = '<img src=' . $article_html->find('img.article_top_DMT_Image', 0)->getAttribute('data-src') . '>';
$heroImage = $article_html->find('img.article_top_DMT_Image', 0);
if ($heroImage) {
$hero_image = '<img src=' . $heroImage->getAttribute('data-src') . '>';
}
$article_body = $article_html->find('div.TGN_Article_ReadTimeSection', 0);
// Remove the menu bar on some articles (PDF download etc.)

View File

@@ -603,10 +603,10 @@ class GithubTrendingBridge extends BridgeAbstract
$item = [];
// URI
$item['uri'] = self::URI_ITEM . $element->find('h1 a', 0)->href;
$item['uri'] = self::URI_ITEM . $element->find('h2 a', 0)->href;
// Title
$item['title'] = str_replace(' ', '', trim(strip_tags($element->find('h1 a', 0)->plaintext)));
$item['title'] = str_replace(' ', '', trim(strip_tags($element->find('h2 a', 0)->plaintext)));
// Description
$description = $element->find('p', 0);

View File

@@ -22,7 +22,7 @@ class GizmodoBridge extends FeedExpander
// Get header image
$image = $html->find('meta[property="og:image"]', 0)->content;
$item['content'] = $html->find('div.js_post-content', 0)->innertext;
$item['content'] = $html->find('div.js_post-content', 0)->innertext ?? '';
// Get categories
$categories = explode(',', $html->find('meta[name="keywords"]', 0)->content);

View File

@@ -0,0 +1,38 @@
<?php
class GoAccessBridge extends BridgeAbstract
{
const MAINTAINER = 'Simounet';
const NAME = 'GoAccess';
const URI_BASE = 'https://goaccess.io';
const URI = self::URI_BASE . '/release-notes';
const CACHE_TIMEOUT = 21600; //6h
const DESCRIPTION = 'GoAccess releases.';
public function collectData()
{
$html = getSimpleHTMLDOM(self::URI);
$container = $html->find('.container.content', 0);
foreach ($container->find('div') as $element) {
$titleEl = $element->find('h2', 0);
$dateEl = $titleEl->find('small', 0);
$date = trim($dateEl->plaintext);
$title = is_object($titleEl) ? str_replace($date, '', $titleEl->plaintext) : '';
$linkEl = $titleEl->find('a', 0);
$link = is_object($linkEl) ? $linkEl->href : '';
$postUrl = self::URI . $link;
$contentEl = $element->find('.dl-horizontal', 0);
$content = '<dl>' . $contentEl->xmltext() . '</dl>';
$item = [];
$item['uri'] = $postUrl;
$item['timestamp'] = strtotime($date);
$item['title'] = $title;
$item['content'] = $content;
$this->items[] = $item;
}
}
}

View File

@@ -88,6 +88,14 @@ class GolemBridge extends FeedExpander
$item['author'] = $author->plaintext;
}
$categories = $articlePage->find('ul.tags__list li');
foreach ($categories as $category) {
$trimmedcategories[] = trim(html_entity_decode($category->plaintext));
}
if (isset($trimmedcategories)) {
$item['categories'] = array_unique($trimmedcategories);
}
$item['content'] .= $this->extractContent($articlePage);
// next page
@@ -106,8 +114,8 @@ class GolemBridge extends FeedExpander
// delete known bad elements
foreach (
$article->find('div[id*="adtile"], #job-market, #seminars,
div.gbox_affiliate, div.toc, .embedcontent') as $bad
$article->find('div[id*="adtile"], #job-market, #seminars, iframe,
div.gbox_affiliate, div.toc, .embedcontent, script') as $bad
) {
$bad->remove();
}

View File

@@ -2,19 +2,101 @@
class GoogleScholarBridge extends BridgeAbstract
{
const NAME = 'Goolge Scholar';
const NAME = 'Google Scholar v2';
const URI = 'https://scholar.google.com/';
const DESCRIPTION = 'Follow authors of scientific publications.';
const MAINTAINER = 'thefranke';
const DESCRIPTION = 'Search for publications or follow authors on Google Scholar.';
const MAINTAINER = 'nicholasmccarthy';
const CACHE_TIMEOUT = 86400; // 24h
const PARAMETERS = [[
'userId' => [
'name' => 'User ID',
'exampleValue' => 'qc6CJjYAAAAJ',
'required' => true
]
]];
const PARAMETERS = [
'user' => [
'userId' => [
'name' => 'User ID',
'exampleValue' => 'qc6CJjYAAAAJ',
'required' => true
]
],
'query' => [
'q' => [
'name' => 'Search Query',
'title' => 'Search Query',
'required' => true,
'exampleValue' => 'machine learning'
],
'cites' => [
'name' => 'Cites',
'required' => false,
'default' => '',
'exampleValue' => '1275980731835430123',
'title' => 'Parameter defines unique ID for an article to trigger Cited By searches. Usage of cites
will bring up a list of citing documents in Google Scholar. Example value: cites=1275980731835430123.
Usage of cites and q parameters triggers search within citing articles.'
],
'language' => [
'name' => 'Language',
'required' => false,
'default' => '',
'exampleValue' => 'en',
'title' => 'Parameter defines the language to use for the Google Scholar search. '
],
'minCitations' => [
'name' => 'Minimum Citations',
'required' => false,
'type' => 'number',
'default' => '0',
'title' => 'Parameter defines the minimum number of citations in order for the results to be included.'
],
'sinceYear' => [
'name' => 'Since Year',
'required' => false,
'type' => 'number',
'default' => '0',
'title' => 'Parameter defines the year from which you want the results to be included.'
],
'untilYear' => [
'name' => 'Until Year',
'required' => false,
'type' => 'number',
'default' => '0',
'title' => 'Parameter defines the year until which you want the results to be included.'
],
'sortBy' => [
'name' => 'Sort By Date',
'type' => 'checkbox',
'default' => false,
'title' => 'Parameter defines articles added in the last year, sorted by date. Alternatively sorts
by relevance. This overrides Since-Until Year values.',
],
'includePatents' => [
'name' => 'Include Patents',
'type' => 'checkbox',
'default' => false,
'title' => 'Include Patents',
],
'includeCitations' => [
'name' => 'Include Citations',
'type' => 'checkbox',
'default' => true,
'title' => 'Parameter defines whether you would like to include citations or not.',
],
'reviewArticles' => [
'name' => 'Only Review Articles',
'type' => 'checkbox',
'default' => false,
'title' => 'Parameter defines whether you would like to show only review articles or not (these
articles consist of topic reviews, or discuss the works or authors you have searched for).',
],
'numResults' => [
'name' => 'Number of Results (max 20)',
'required' => false,
'type' => 'number',
'default' => 10,
'exampleValue' => 10,
'title' => 'Number of results to return'
]
],
];
public function getIcon()
{
@@ -23,58 +105,138 @@ class GoogleScholarBridge extends BridgeAbstract
public function collectData()
{
$uri = self::URI . '/citations?hl=en&view_op=list_works&sortby=pubdate&user=' . $this->getInput('userId');
switch ($this->queriedContext) {
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)
or returnServerError('Could not fetch Google Scholar data.');
$publications = $html->find('tr[class="gsc_a_tr"]');
$publications = $html->find('tr[class="gsc_a_tr"]');
foreach ($publications as $publication) {
$articleUrl = self::URI . htmlspecialchars_decode($publication->find('a[class="gsc_a_at"]', 0)->href);
$articleTitle = $publication->find('a[class="gsc_a_at"]', 0)->plaintext;
foreach ($publications as $publication) {
$articleUrl = self::URI . htmlspecialchars_decode($publication->find('a[class="gsc_a_at"]', 0)->href);
$articleTitle = $publication->find('a[class="gsc_a_at"]', 0)->plaintext;
# fetch the article itself to extract rest of content
$contentArticle = getSimpleHTMLDOMCached($articleUrl);
$articleEntries = $contentArticle->find('div[class="gs_scl"]');
# fetch the article itself to extract rest of content
$contentArticle = getSimpleHTMLDOMCached($articleUrl);
$articleEntries = $contentArticle->find('div[class="gs_scl"]');
$articleDate = '';
$articleAbstract = '';
$articleAuthor = '';
$content = '';
$articleDate = '';
$articleAbstract = '';
$articleAuthor = '';
$content = '';
foreach ($articleEntries as $entry) {
$field = $entry->find('div[class="gsc_oci_field"]', 0)->plaintext;
$value = $entry->find('div[class="gsc_oci_value"]', 0)->plaintext;
foreach ($articleEntries as $entry) {
$field = $entry->find('div[class="gsc_oci_field"]', 0)->plaintext;
$value = $entry->find('div[class="gsc_oci_value"]', 0)->plaintext;
if ($field == 'Publication date') {
$articleDate = $value;
} elseif ($field == 'Description') {
$articleAbstract = $value;
} elseif ($field == 'Authors') {
$articleAuthor = $value;
} elseif ($field == 'Scholar articles' || $field == 'Total citations') {
continue;
} else {
$content = $content . $field . ': ' . $value . '<br><br>';
}
}
if ($field == 'Publication date') {
$articleDate = $value;
} else if ($field == 'Description') {
$articleAbstract = $value;
} else if ($field == 'Authors') {
$articleAuthor = $value;
} else if ($field == 'Scholar articles' || $field == 'Total citations') {
continue;
} else {
$content = $content . $field . ': ' . $value . '<br><br>';
$content = $content . $articleAbstract;
$item = [];
$item['title'] = $articleTitle;
$item['uri'] = $articleUrl;
$item['timestamp'] = strtotime($articleDate);
$item['author'] = $articleAuthor;
$item['content'] = $content;
$this->items[] = $item;
if (count($this->items) >= 10) {
break;
}
}
break;
case 'query':
$query = urlencode($this->getInput('q'));
$cites = $this->getInput('cites');
$language = $this->getInput('language');
$sinceYear = $this->getInput('sinceYear');
$untilYear = $this->getInput('untilYear');
$minCitations = (int)$this->getInput('minCitations');
$includeCitations = $this->getInput('includeCitations');
$includePatents = $this->getInput('includePatents');
$reviewArticles = $this->getInput('reviewArticles');
$sortBy = $this->getInput('sortBy');
$numResults = $this->getInput('numResults');
# Build URI
$uri = self::URI . 'scholar?q=' . $query;
$uri .= $sinceYear != 0 ? '&as_ylo=' . $sinceYear : '';
$uri .= $untilYear != 0 ? '&as_yhi=' . $untilYear : '';
$uri .= $language != '' ? '&hl=' . $language : '';
$uri .= $includePatents ? '&as_vis=7' : '&as_vis=0';
$uri .= $includeCitations ? '&as_vis=0' : ($includePatents ? '&as_vis=1' : '');
$uri .= $reviewArticles ? '&as_rr=1' : '';
$uri .= $sortBy ? '&scisbd=1' : '';
$uri .= $numResults ? '&num=' . $numResults : '';
$html = getSimpleHTMLDOM($uri) or returnServerError('Could not fetch Google Scholar data.');
$publications = $html->find('div[class="gs_r gs_or gs_scl"]');
foreach ($publications as $publication) {
$articleTitleElement = $publication->find('h3[class="gs_rt"]', 0);
$articleUrl = $articleTitleElement->find('a', 0)->href;
$articleTitle = $articleTitleElement->plaintext;
$articleDateElement = $publication->find('div[class="gs_a"]', 0);
$articleDate = $articleDateElement ? $articleDateElement->plaintext : '';
$articleAbstractElement = $publication->find('div[class="gs_rs"]', 0);
$articleAbstract = $articleAbstractElement ? $articleAbstractElement->plaintext : '';
$articleAuthorElement = $publication->find('div[class="gs_a"]', 0);
$articleAuthor = $articleAuthorElement ? $articleAuthorElement->plaintext : '';
$bottomRowElement = $publication->find('div[class="gs_fl"]', 0);
$item = [
'title' => $articleTitle,
'uri' => $articleUrl,
'timestamp' => strtotime($articleDate),
'author' => $articleAuthor,
'content' => $articleAbstract
];
switch ($this->queriedContext) {
case 'user':
$this->items[] = $item;
break;
case 'query':
$citedBy = 0;
if ($bottomRowElement) {
$anchorTags = $bottomRowElement->find('a');
foreach ($anchorTags as $anchorTag) {
if (strpos($anchorTag->plaintext, 'Cited') !== false) {
$parts = explode('Cited by ', $anchorTag->plaintext);
if (isset($parts[1])) {
$citedBy = (int)$parts[1];
}
break;
}
}
}
if ($citedBy >= $minCitations) {
$this->items[] = $item;
}
break;
}
}
}
$content = $content . $articleAbstract;
$item = [];
$item['title'] = $articleTitle;
$item['uri'] = $articleUrl;
$item['timestamp'] = strtotime($articleDate);
$item['author'] = $articleAuthor;
$item['content'] = $content;
$this->items[] = $item;
if (count($this->items) >= 10) {
break;
}
}
}
}

View File

@@ -118,12 +118,22 @@ class HeiseBridge extends FeedExpander
protected function parseItem($feedItem)
{
$item = parent::parseItem($feedItem);
$item['uri'] = explode('?', $item['uri'])[0] . '?seite=all';
// strip rss parameter
$item['uri'] = explode('?', $item['uri'])[0];
// ignore TechStage articles
if (strpos($item['uri'], 'https://www.heise.de') !== 0) {
return $item;
}
// abort on heise+ articles and link to archive.ph for full-text content
if (str_starts_with($item['title'], 'heise+ |')) {
$item['uri'] = 'https://archive.ph/?run=1&url=' . urlencode($item['uri']);
return $item;
}
$item['uri'] .= '?seite=all';
$article = getSimpleHTMLDOMCached($item['uri']);
if ($article) {
@@ -140,7 +150,7 @@ class HeiseBridge extends FeedExpander
$article = defaultLinkTo($article, $item['uri']);
// remove unwanted stuff
foreach ($article->find('figure.branding, a-ad, div.ho-text, a-img, .opt-in__content-container, .a-toc__list') as $element) {
foreach ($article->find('figure.branding, a-ad, div.ho-text, a-img, .opt-in__content-container, .a-toc__list, a-collapse') as $element) {
$element->remove();
}
// reload html, as remove() is buggy
@@ -151,7 +161,7 @@ class HeiseBridge extends FeedExpander
$headerElements = $header->find('p, figure img, noscript img');
$item['content'] = implode('', $headerElements);
$authors = $header->find('.a-creator__names .a-creator__name');
$authors = $header->find('.creator__names .creator__name');
if ($authors) {
$item['author'] = implode(', ', array_map(function ($e) {
return $e->plaintext;
@@ -159,6 +169,11 @@ class HeiseBridge extends FeedExpander
}
}
$categories = $article->find('.article-footer__topics ul.topics li.topics__item');
foreach ($categories as $category) {
$item['categories'][] = trim($category->plaintext);
}
$content = $article->find('.article-content', 0);
if ($content) {
$contentElements = $content->find(

View File

@@ -98,9 +98,7 @@ class InstagramBridge extends BridgeAbstract
return $username;
}
$cacheFactory = new CacheFactory();
$cache = $cacheFactory->create();
$cache = RssBridge::getCache();
$cache->setScope('InstagramBridge');
$cache->setKey([$username]);
$key = $cache->loadData();

View File

@@ -9,23 +9,23 @@ class InternationalInstituteForStrategicStudiesBridge extends BridgeAbstract
const CACHE_TIMEOUT = 3600; // 1 hour
const DESCRIPTION = 'Returns the latest blog posts from the IISS';
const TEMPLATE_ID = '{6BCFD2C9-4F0B-4ACE-95D7-D14C8B60CD4D}';
const COMPONENT_ID = '{E9850380-3707-43C9-994F-75ECE8048E04}';
const TEMPLATE_ID = ['BlogArticlePage', 'BlogPage'];
const COMPONENT_ID = '9b0c6919-c78b-4910-9be9-d73e6ee40e50';
public function collectData()
{
$url = 'https://www.iiss.org/api/filter';
$url = 'https://www.iiss.org/api/filteredlist/filter';
$opts = [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_POSTFIELDS => json_encode([
'templateId' => [self::TEMPLATE_ID],
'templateId' => self::TEMPLATE_ID,
'componentId' => self::COMPONENT_ID,
'page' => '1',
'amount' => 10,
'amount' => 1,
'filter' => (object)[],
'tags' => null,
'sortType' => 'DateDesc',
'restrictionType' => 'None'
'sortType' => 'Newest',
'restrictionType' => 'Any'
])
];
$headers = [
@@ -36,6 +36,7 @@ class InternationalInstituteForStrategicStudiesBridge extends BridgeAbstract
$data = json_decode($json);
foreach ($data->model->Results as $record) {
[$content, $enclosures] = $this->getContents(self::URI . $record->Link);
$this->items[] = [
'uri' => self::URI . $record->Link,
'title' => $record->Heading,
@@ -44,7 +45,8 @@ class InternationalInstituteForStrategicStudiesBridge extends BridgeAbstract
return $author->Name;
}, $record->Authors)),
'timestamp' => DateTime::createFromFormat('jS F Y', $record->Date)->format('U'),
'content' => $this->getContents(self::URI . $record->Link)
'content' => $content,
'enclosures' => $enclosures
];
}
}
@@ -56,6 +58,8 @@ class InternationalInstituteForStrategicStudiesBridge extends BridgeAbstract
$scripts = $body->find('script');
$result = '';
$enclosures = [];
foreach ($scripts as $script) {
$script_text = $script->innertext;
if (str_contains($script_text, 'ReactDOM.render(React.createElement(Components.Reading')) {
@@ -63,14 +67,26 @@ class InternationalInstituteForStrategicStudiesBridge extends BridgeAbstract
$result .= $args->Html;
} elseif (str_contains($script_text, 'ReactDOM.render(React.createElement(Components.ImagePanel')) {
$args = $this->getRenderArguments($script_text);
$image_tag = str_replace('src="/-', 'src="' . self::URI . '/-', $args->Image);
$result .= '<figure>' . $image_tag . '</figure>';
$result .= '<figure><img src="' . self::URI . $args->Image . '"></img></figure>';
} elseif (str_contains($script_text, 'ReactDOM.render(React.createElement(Components.Intro')) {
$args = $this->getRenderArguments($script_text);
$result .= '<p>' . $args->Intro . '</p>';
} elseif (str_contains($script_text, 'ReactDOM.render(React.createElement(Components.Footnotes')) {
$args = $this->getRenderArguments($script_text);
$result .= '<p>' . $args->Content . '</p>';
} elseif (str_contains($script_text, 'ReactDOM.render(React.createElement(Components.List')) {
$args = $this->getRenderArguments($script_text);
foreach ($args->Items as $item) {
if ($item->Url != null) {
$match = preg_match('/\\"(.*)\\"/', $item->Url, $matches);
if ($match > 0) {
array_push($enclosures, self::URI . $matches[1]);
}
}
}
}
}
return $result;
return [$result, $enclosures];
}
private function getRenderArguments($script_text)

View File

@@ -0,0 +1,29 @@
<?php
class JohannesBlickBridge extends BridgeAbstract
{
const NAME = 'Johannes Blick';
const URI = 'https://www.st-johannes-baptist.de/index.php/unsere-medien/johannesblick-archiv';
const DESCRIPTION = 'RSS feed for Johannes Blick';
const MAINTAINER = 'jummo4@yahoo.de';
public function collectData()
{
$html = getSimpleHTMLDOM(self::URI)
or returnServerError('Could not request: ' . self::URI);
$html = defaultLinkTo($html, self::URI);
foreach ($html->find('td > a') as $index => $a) {
$item = []; // Create an empty item
$articlePath = $a->href;
$item['title'] = $a->innertext;
$item['uri'] = $articlePath;
$item['content'] = '';
$this->items[] = $item; // Add item to the list
if (count($this->items) >= 10) {
break;
}
}
}
}

View File

@@ -1,59 +0,0 @@
<?php
class JornalDeNoticiasBridge extends BridgeAbstract
{
const NAME = 'Jornal de Notícias (PT)';
const URI = 'https://jn.pt';
const DESCRIPTION = 'Jornal de Notícias (JN.PT)';
const MAINTAINER = 'somini';
const PARAMETERS = [
'URL' => [
'url' => [
'name' => 'URL (relative)',
'exampleValue' => 'opiniao/catia-domingues.html',
]
]
];
public function getIcon()
{
return 'https://static.globalnoticias.pt/jn/common/images/favicons/favicon-128.png';
}
public function getURI()
{
switch ($this->queriedContext) {
case 'URL':
$url = self::URI . '/' . $this->getInput('url');
break;
default:
$url = self::URI;
}
return $url;
}
public function collectData()
{
$archives = $this->getURI();
$html = getSimpleHTMLDOMCached($archives);
foreach ($html->find('article') as $element) {
$item = [];
$title = $element->find('h2 a', 0);
$link = $element->find('h2 a', 0);
$auth = $element->find('h3 a', 0);
$item['title'] = $title->plaintext;
$item['uri'] = self::URI . $link->href;
$item['author'] = $auth->plaintext;
$snippet = $element->find('h4 a', 0);
if ($snippet) {
$item['content'] = $snippet->plaintext;
}
$this->items[] = $item;
}
}
}

104
bridges/JornalNBridge.php Normal file
View File

@@ -0,0 +1,104 @@
<?php
class JornalNBridge extends BridgeAbstract
{
const NAME = 'Jornal N';
const URI = 'https://www.jornaln.pt/';
const DESCRIPTION = 'Returns news from the Portuguese local newspaper Jornal N';
const MAINTAINER = 'rmscoelho';
const CACHE_TIMEOUT = 3600;
const PARAMETERS = [
[
'feed' => [
'name' => 'News Feed',
'type' => 'list',
'title' => 'Feeds from the Portuguese sports newspaper A BOLA.PT',
'values' => [
'Concelhos' => [
'Espinho' => 'espinho',
'Ovar' => 'ovar',
'Santa Maria da Feira' => 'santa-maria-da-feira',
],
'Cultura' => 'ovar/cultura',
'Desporto' => 'desporto',
'Economia' => 'santa-maria-da-feira/economia',
'Política' => 'santa-maria-da-feira/politica',
'Opinião' => 'santa-maria-da-feira/opiniao',
'Sociedade' => 'santa-maria-da-feira/sociedade',
]
]
]
];
const PT_MONTH_NAMES = [
'janeiro' => '01',
'fevereiro' => '02',
'março' => '03',
'abril' => '04',
'maio' => '05',
'junho' => '06',
'julho' => '07',
'agosto' => '08',
'setembro' => '09',
'outubro' => '10',
'novembro' => '11',
'dezembro' => '12',
];
public function getIcon()
{
return 'https://www.jornaln.pt/wp-content/uploads/2023/01/cropped-NovoLogoJornal_Instagram-192x192.png';
}
public function getName()
{
if ($this->getKey('feed')) {
return self::NAME . ' | ' . $this->getKey('feed');
}
return self::NAME;
}
public function getURI()
{
return self::URI . $this->getInput('feed');
}
public function collectData()
{
$url = sprintf(self::URI . '/%s', $this->getInput('feed'));
$dom = getSimpleHTMLDOMCached($url);
$domSelector = '.elementor-widget-container > .elementor-posts-container';
$dom = $dom->find($domSelector, 0);
if (!$dom) {
throw new \Exception(sprintf('Unable to find css selector on `%s`', $url));
}
$dom = defaultLinkTo($dom, $this->getURI());
foreach ($dom->find('article') as $article) {
//Get thumbnail
$image = $article->find('.elementor-post__thumbnail img', 0)->src;
//Timestamp
$date = $article->find('.elementor-post-date', 0)->plaintext;
$date = trim($date, "\t ");
$date = preg_replace('/ de /i', '/', $date);
$date = preg_replace('/, /', '/', $date);
$date = explode('/', $date);
$year = (int) $date[2];
$month = (int) $date[1];
$day = (int) $date[0];
foreach (self::PT_MONTH_NAMES as $key => $item) {
if ($key === strtolower($month)) {
$month = (int) $item;
}
}
$timestamp = mktime(0, 0, 0, $month, $day, $year);
//Content
$content = '<img src="' . $image . '" alt="' . $article->find('.elementor-post__title > a', 0)->plaintext . '" />';
$this->items[] = [
'title' => $article->find('.elementor-post__title > a', 0)->plaintext,
'uri' => $article->find('a', 0)->href,
'content' => $content,
'timestamp' => $timestamp
];
}
}
}

View File

@@ -169,10 +169,17 @@ class JustWatchBridge extends BridgeAbstract
foreach ($titles as $title) {
$item = [];
$item['uri'] = $title->find('a', 0)->href;
$item['title'] = $provider->find('picture > img', 0)->alt . ' - ' . $title->find('.title-poster__image > img', 0)->alt;
$image = $title->find('.title-poster__image > img', 0)->attr['src'];
if (str_starts_with($image, 'data')) {
$image = $title->find('.title-poster__image > img', 0)->attr['data-src'];
$itemTitle = sprintf(
'%s - %s',
$provider->find('picture > img', 0)->alt ?? '',
$title->find('.title-poster__image > img', 0)->alt ?? ''
);
$item['title'] = $itemTitle;
$imageUrl = $title->find('.title-poster__image > img', 0)->attr['src'] ?? '';
if (str_starts_with($imageUrl, 'data')) {
$imageUrl = $title->find('.title-poster__image > img', 0)->attr['data-src'];
}
$content = '<b>Provider:</b> '
@@ -190,7 +197,7 @@ class JustWatchBridge extends BridgeAbstract
$content .= '<b>Poster:</b><br><a href="'
. $title->find('a', 0)->href
. '"><img src="'
. $image
. $imageUrl
. '"></a>';
$item['content'] = $content;

View File

@@ -0,0 +1,88 @@
<?php
class MagellantvBridge extends BridgeAbstract
{
const NAME = 'Magellantv articles';
const URI = 'https://www.magellantv.com/articles';
const DESCRIPTION = 'Articles of the documentery streaming service Magellantv';
const MAINTAINER = 'Vincentvd';
const CACHE_TIMEOUT = 60; // 15 minutes
const PARAMETERS = [
[
'topic' => [
'type' => 'list',
'name' => 'Article topic',
'values' => [
'All topics' => 'all',
'Ancient history' => 'ancient-history',
'Art & culture' => 'art-culture',
'Biography' => 'biography',
'Current history' => 'current-history',
'Early modern' => 'early-modern',
'Earth' => 'earth',
'Mind & body' => 'mind-body',
'Nature' => 'nature',
'Science & tech' => 'science-tech',
'Short takes' => 'short-takes',
'Space' => 'space',
'Travel & adventure' => 'travel-adventure',
'True crime' => 'true-crime',
'War & military' => 'war-military'
],
]
]
];
public function getIcon()
{
return 'https://www.magellantv.com/favicon-32x32.png';
}
private function retrieveTags($article)
{
// Retrieve all tags from an article and store in array
$article_tags_list = $article->find('div.articleCategory_article-category-tag__uEAXz > a');
$tags = [];
foreach ($article_tags_list as $tag) {
array_push($tags, $tag->plaintext);
}
return $tags;
}
public function collectData()
{
// Determine URL based on topic
$topic = $this->getInput('topic');
if ($topic == 'all') {
$url = 'https://www.magellantv.com/articles';
} else {
$url = sprintf('https://www.magellantv.com/articles/category/%s', $topic);
}
$dom = getSimpleHTMLDOM($url);
// Check whether items exists
$article_list = $dom->find('div.articlePreview_preview-card__mLMOm');
if (sizeof($article_list) == 0) {
throw new Exception(sprintf('Unable to find css selector on `%s`', $url));
}
// Loop over each article and store article information
foreach ($article_list as $article) {
$article = defaultLinkTo($article, $this->getURI());
$meta_information = $article->find('div.articlePreview_article-metas__kD1i7', 0);
$title = $article->find('div.articlePreview_article-title___Ci5V > h2 > a', 0);
$tags_list = $this->retrieveTags($article);
$item = [
'title' => $title->plaintext,
'uri' => $title->href,
'timestamp' => strtotime($meta_information->find('div.articlePreview_article-date__8Jyfn', 0)->plaintext),
'author' => $meta_information->find('div.articlePreview_article-author__Ie0_u > span', 1)->plaintext,
'categories' => $tags_list
];
$this->items[] = $item;
}
}
}

View File

@@ -21,6 +21,18 @@ class MangaDexBridge extends BridgeAbstract
'exampleValue' => 'en,jp',
'required' => false
],
'images' => [
'name' => 'Fetch chapter page images',
'type' => 'list',
'title' => 'Places chapter images in feed contents. Entries will consume more bandwidth.',
'defaultValue' => 'no',
'values' => [
'None' => 'no',
'Data Saver' => 'saver',
'Full Quality' => 'yes'
]
]
],
'Title Chapters' => [
'url' => [
@@ -239,6 +251,27 @@ class MangaDexBridge extends BridgeAbstract
$item['content'] .= '<br>Other Users: ' . implode(', ', $users);
}
// Fetch chapter page images if desired and add to content
if ($this->getInput('images') !== 'no') {
$api_uri = self::API_ROOT . 'at-home/server/' . $item['uid'];
$header = [ 'Content-Type: application/json' ];
$pages = json_decode(getContents($api_uri, $header), true);
if ($pages['result'] != 'ok') {
returnServerError('Could not retrieve API results');
}
if ($this->getInput('images') == 'saver') {
$page_base = $pages['baseUrl'] . '/data-saver/' . $pages['chapter']['hash'] . '/';
foreach ($pages['chapter']['dataSaver'] as $image) {
$item['content'] .= '<br><img src="' . $page_base . $image . '"/>';
}
} else {
$page_base = $pages['baseUrl'] . '/data/' . $pages['chapter']['hash'] . '/';
foreach ($pages['chapter']['data'] as $image) {
$item['content'] .= '<br><img src="' . $page_base . $image . '"/>';
}
}
}
$this->items[] = $item;
}
}

View File

@@ -319,7 +319,7 @@ EOD;
$content .= $module['note'];
break;
case 'listicle':
$content .= '<h2>' . $module['title'] . '</h2>';
$content .= '<h2>' . ($module['title'] ?? '(no title)') . '</h2>';
if (isset($module['image'])) {
$content .= $this->handleImages($module['image'], $module['image']['cmsType']);
}

View File

@@ -19,8 +19,10 @@ class NotAlwaysBridge extends BridgeAbstract
'Romantic' => 'romantic',
'Related' => 'related',
'Learning' => 'learning',
'Friendly' => 'friendly',
'Hopeless' => 'hopeless',
'Healthy' => 'healthy',
'Legal' => 'legal',
'Friendly' => 'friendly',
'Unfiltered' => 'unfiltered'
]
]
@@ -38,7 +40,9 @@ class NotAlwaysBridge extends BridgeAbstract
#print_r($post);
$item = [];
$item['uri'] = $post->find('h1', 0)->find('a', 0)->href;
$item['content'] = $post;
$postHeader = $post->find('.post_header', 0);
$storyContent = $post->find('.storycontent', 0);
$item['content'] = $postHeader . '<br/><br/>' . $storyContent;
$item['title'] = $post->find('h1', 0)->find('a', 0)->innertext;
$this->items[] = $item;
}

View File

@@ -2,10 +2,24 @@
class NyaaTorrentsBridge extends FeedExpander
{
const MAINTAINER = 'ORelio';
const MAINTAINER = 'ORelio & Jisagi';
const NAME = 'NyaaTorrents';
const URI = 'https://nyaa.si/';
const DESCRIPTION = 'Returns the newest torrents, with optional search criteria.';
const MAX_ITEMS = 20;
const CUSTOM_FIELD_PREFIX = 'nyaa:';
const CUSTOM_FIELDS = [
self::CUSTOM_FIELD_PREFIX . 'seeders' => 'seeders',
self::CUSTOM_FIELD_PREFIX . 'leechers' => 'leechers',
self::CUSTOM_FIELD_PREFIX . 'downloads' => 'downloads',
self::CUSTOM_FIELD_PREFIX . 'infoHash' => 'infoHash',
self::CUSTOM_FIELD_PREFIX . 'categoryId' => 'categoryId',
self::CUSTOM_FIELD_PREFIX . 'category' => 'category',
self::CUSTOM_FIELD_PREFIX . 'size' => 'size',
self::CUSTOM_FIELD_PREFIX . 'comments' => 'comments',
self::CUSTOM_FIELD_PREFIX . 'trusted' => 'trusted',
self::CUSTOM_FIELD_PREFIX . 'remake' => 'remake'
];
const PARAMETERS = [
[
'f' => [
@@ -65,23 +79,41 @@ class NyaaTorrentsBridge extends FeedExpander
return self::URI . 'static/favicon.png';
}
public function collectData()
public function getURI()
{
$this->collectExpandableDatas(
self::URI . '?page=rss&s=id&o=desc&'
return self::URI . '?page=rss&s=id&o=desc&'
. http_build_query([
'f' => $this->getInput('f'),
'c' => $this->getInput('c'),
'q' => $this->getInput('q'),
'u' => $this->getInput('u')
]),
20
);
]);
}
public function collectData()
{
$content = getContents($this->getURI());
$content = $this->fixCustomFields($content);
$rssContent = simplexml_load_string(trim($content));
$this->collectRss2($rssContent, self::MAX_ITEMS);
}
private function fixCustomFields($content)
{
$broken = array_keys(self::CUSTOM_FIELDS);
$fixed = array_values(self::CUSTOM_FIELDS);
return str_replace($broken, $fixed, $content);
}
protected function parseItem($newItem)
{
$item = parent::parseItem($newItem);
$item = parent::parseRss2Item($newItem);
// Add nyaa custom fields
$item['id'] = str_replace(['https://nyaa.si/download/', '.torrent'], '', $item['uri']);
foreach (array_values(self::CUSTOM_FIELDS) as $value) {
$item[$value] = (string) $newItem->$value;
}
//Convert URI from torrent file to web page
$item['uri'] = str_replace('/download/', '/view/', $item['uri']);

View File

@@ -0,0 +1,72 @@
<?php
class OMonlineBridge extends BridgeAbstract
{
const NAME = 'OM Online Bridge';
const URI = 'https://www.om-online.de';
const DESCRIPTION = 'RSS feed for OM Online';
const MAINTAINER = 'jummo4@yahoo.de';
const PARAMETERS = [
[
'ort' => [
'name' => 'Ortsname',
'title' => 'Für die Anzeige von Beitragen nur aus einem Ort oder mehreren Orten
geben einen Orstnamen ein. Mehrere Ortsnamen müssen mit / getrennt eingeben werden,
z.B. Vechta/Cloppenburg. Groß- und Kleinschreibung beachten!'
]
]
];
public function collectData()
{
if (!empty($this->getInput('ort'))) {
$url = sprintf('%s/ort/%s', self::URI, $this->getInput('ort'));
} else {
$url = sprintf('%s', self::URI);
}
$html = getSimpleHTMLDOM($url)
or returnServerError('Could not request: ' . $url);
$html = defaultLinkTo($html, $url);
foreach ($html->find('div.molecule-teaser > a ') as $index => $a) {
$item = [];
$articlePath = $a->href;
$articlePageHtml = getSimpleHTMLDOMCached($articlePath, self::CACHE_TIMEOUT)
or returnServerError('Could not request: ' . $articlePath);
$articlePageHtml = defaultLinkTo($articlePageHtml, self::URI);
$contents = $articlePageHtml->find('div.molecule-article', 0);
$item['uri'] = $articlePath;
$item['title'] = $contents->find('h1', 0)->innertext;
$contents->find('div.col-12 col-md-10 offset-0 offset-md-1', 0);
$item['content'] = $contents->innertext;
$item['timestamp'] = $this->extractDate2($a->plaintext);
$this->items[] = $item;
if (count($this->items) >= 10) {
break;
}
}
}
private function extractDate2($text)
{
$dateRegex = '/^([0-9]{4}\/[0-9]{1,2}\/[0-9]{1,2})/';
$text = trim($text);
if (preg_match($dateRegex, $text, $matches)) {
return $matches[1];
}
return '';
}
}

View File

@@ -100,12 +100,14 @@ class PatreonBridge extends BridgeAbstract
);
$item['author'] = $user->full_name;
if (isset($post->attributes->image)) {
$item['content'] .= '<p><a href="'
. $post->attributes->url
. '"><img src="'
. $post->attributes->image->thumb_url
. '" /></a></p>';
$image = $post->attributes->image ?? null;
if ($image) {
$logo = sprintf(
'<p><a href="%s"><img src="%s" /></a></p>',
$post->attributes->url,
$image->thumb_url ?? $image->url ?? $this->getURI()
);
$item['content'] .= $logo;
}
if (isset($post->attributes->content)) {

View File

@@ -94,7 +94,7 @@ class PepperBridgeAbstract extends BridgeAbstract
);
// If there is no results, we don't parse the content because it display some random deals
$noresult = $html->find('h3[class=size--all-l size--fromW2-xl size--fromW3-xxl]', 0);
$noresult = $html->find('h3[class=size--all-l]', 0);
if ($noresult != null && strpos($noresult->plaintext, $this->i8n('no-results')) !== false) {
$this->items = [];
} else {
@@ -129,7 +129,7 @@ class PepperBridgeAbstract extends BridgeAbstract
// Find the text corresponding to the clock
$spanDateDiv = $clock->parent()->find('span[class=hide--toW3]', 0);
$itemDate = $spanDateDiv->plaintext;
$itemDate = $spanDateDiv->plaintext ?? '';
// In case of a Local deal, there is no date, but we can use
// this case for other reason (like date not in the last field)
if ($this->contains($itemDate, $this->i8n('localdeal'))) {
@@ -481,12 +481,12 @@ HEREDOC;
]
);
if ($deal->find('span[class*=' . $selector . ']', 0) != null) {
return '<div>'
. $deal->find('span[class*=' . $selector . ']', 0)->children(2)->plaintext
. '</div>';
} else {
return '';
$children = $deal->find('span[class*=' . $selector . ']', 0)->children(2);
if ($children) {
return '<div>' . $children->plaintext . '</div>';
}
}
return '';
}
/**

View File

@@ -58,17 +58,26 @@ class PicalaBridge extends BridgeAbstract
{
$fullhtml = getSimpleHTMLDOM($this->getURI());
foreach ($fullhtml->find('.list-container-category a') as $article) {
$srcsets = explode(',', $article->find('img', 0)->getAttribute('srcset'));
$image = explode(' ', trim(array_shift($srcsets)))[0];
$firstImage = $article->find('img', 0);
$image = null;
if ($firstImage !== null) {
$srcsets = explode(',', $firstImage->getAttribute('srcset'));
$image = explode(' ', trim(array_shift($srcsets)))[0];
}
$item = [];
$item['uri'] = self::URI . $article->href;
$item['title'] = $article->find('h2', 0)->plaintext;
$item['content'] = sprintf(
'<img src="%s" /><br>%s',
$image,
$article->find('.teaser__text', 0)->plaintext
);
if ($image === null) {
$item['content'] = $article->find('.teaser__text', 0)->plaintext;
} else {
$item['content'] = sprintf(
'<img src="%s" /><br>%s',
$image,
$article->find('.teaser__text', 0)->plaintext
);
}
$this->items[] = $item;
}
}

View File

@@ -47,37 +47,36 @@ class PicnobBridge extends BridgeAbstract
$html = getSimpleHTMLDOM($this->getURI());
foreach ($html->find('.items') as $part) {
foreach ($part->find('.item') as $element) {
$url = urljoin(self::URI, $element->find('a', 0)->href);
$url = urljoin(self::URI, $element->find('a', 0)->href);
$date = date_create();
$relativeDate = date_interval_create_from_date_string(str_replace(' ago', '', $element->find('.time', 0)->plaintext));
if ($relativeDate) {
date_sub($date, $relativeDate);
}
$date = date_create();
$relativeDate = str_replace(' ago', '', $element->find('.time', 0)->plaintext);
date_sub($date, date_interval_create_from_date_string($relativeDate));
$description = defaultLinkTo(trim($element->find('.sum', 0)->innertext), self::URI);
$description = defaultLinkTo(trim($element->find('.sum', 0)->innertext), self::URI);
$isVideo = (bool) $element->find('.icon_video', 0);
$videoNote = $isVideo ? '<p><i>(video)</i></p>' : '';
$isVideo = (bool) $element->find('.icon_video', 0);
$videoNote = $isVideo ? '<p><i>(video)</i></p>' : '';
$isTV = (bool) $element->find('.icon_tv', 0);
$tvNote = $isTV ? '<p><i>(TV)</i></p>' : '';
$isTV = (bool) $element->find('.icon_tv', 0);
$tvNote = $isTV ? '<p><i>(TV)</i></p>' : '';
$isMoreContent = (bool) $element->find('.icon_multi', 0);
$moreContentNote = $isMoreContent ? '<p><i>(multiple images and/or videos)</i></p>' : '';
$isMoreContent = (bool) $element->find('.icon_multi', 0);
$moreContentNote = $isMoreContent ? '<p><i>(multiple images and/or videos)</i></p>' : '';
$imageUrl = $element->find('.img', 0)->getAttribute('data-src');
$imageUrl = $element->find('.img', 0)->getAttribute('data-src');
parse_str(parse_url($imageUrl, PHP_URL_QUERY), $imageVars);
$imageUrl = $imageVars['u'];
$uid = explode('/', parse_url($url, PHP_URL_PATH))[2];
$uid = explode('/', parse_url($url, PHP_URL_PATH))[2];
$this->items[] = [
'uri' => $url,
'timestamp' => date_format($date, 'r'),
'title' => strlen($description) > 60 ? mb_substr($description, 0, 57) . '...' : $description,
'thumbnail' => $imageUrl,
'enclosures' => [$imageUrl],
'content' => <<<HTML
$this->items[] = [
'uri' => $url,
'timestamp' => date_format($date, 'r'),
'title' => strlen($description) > 60 ? mb_substr($description, 0, 57) . '...' : $description,
'thumbnail' => $imageUrl,
'enclosures' => [$imageUrl],
'content' => <<<HTML
<a href="{$url}">
<img loading="lazy" src="{$imageUrl}" />
</a>
@@ -86,8 +85,8 @@ class PicnobBridge extends BridgeAbstract
{$moreContentNote}
<p>{$description}<p>
HTML,
'uid' => $uid
];
'uid' => $uid
];
}
}
}

View File

@@ -67,7 +67,9 @@ class PornhubBridge extends BridgeAbstract
$show_images = $this->getInput('show_images');
$html = getSimpleHTMLDOM($uri);
$html = getSimpleHTMLDOM($uri, [
'cookie: accessAgeDisclaimerPH=1'
]);
foreach ($html->find('div.videoUList ul.videos li.videoblock') as $element) {
$item = [];

View File

@@ -61,9 +61,9 @@ class PresidenciaPTBridge extends BridgeAbstract
$item = [];
$link = $element->find('a', 0);
$etitle = $element->find('.content-box h2', 0);
$edts = $element->find('p', 1);
$edt = html_entity_decode($edts->innertext, ENT_HTML5);
$etitle = $element->find('.article-title', 0);
$edts = $element->find('.date', 0);
$edt = $edts->innertext;
$item['title'] = strip_tags($etitle->innertext);
$item['uri'] = self::URI . $link->href;

View File

@@ -0,0 +1,40 @@
<?php
class RainLoopBridge extends BridgeAbstract
{
const MAINTAINER = 'Simounet';
const NAME = 'RainLoop';
const URI_BASE = 'https://www.rainloop.net';
const URI = self::URI_BASE . '/changelog/';
const CACHE_TIMEOUT = 21600; //6h
const DESCRIPTION = 'RainLoop\'s changelog';
public function collectData()
{
$html = getSimpleHTMLDOM(self::URI);
$mainContent = $html->find('.main-center', 0);
$elements = $mainContent->find('.row-fluid');
foreach ($elements as $i => $element) {
if ($i === 0) {
continue;
}
$titleEl = $element->find('.h3', 0);
$title = is_object($titleEl) ? $titleEl->plaintext : '';
$postUrl = self::URI . $title;
$contentEl = $element->find('.span9', 0);
$content = is_object($contentEl) ? $contentEl->xmltext() : '';
$item = [];
$item['uri'] = $postUrl;
$item['title'] = $title;
$item['content'] = $content;
$item['timestamp'] = strtotime('now');
$this->items[] = $item;
}
}
}

View File

@@ -82,10 +82,10 @@ class Releases3DSBridge extends BridgeAbstract
$item = [];
$item['title'] = $name;
$item['author'] = $publisher;
$item['timestamp'] = $ignDate;
$item['enclosures'] = [$ignCoverArt];
//$item['timestamp'] = $ignDate;
//$item['enclosures'] = [$ignCoverArt];
$item['uri'] = empty($ignLink) ? $searchLinkDuckDuckGo : $ignLink;
$item['content'] = $ignDescription . $releaseDescription . $releaseSearchLinks;
$item['content'] = $releaseDescription . $releaseSearchLinks;
$this->items[] = $item;
$limit++;
}

View File

@@ -0,0 +1,97 @@
<?php
class RemixAudioBridge extends BridgeAbstract
{
const MAINTAINER = 'Simounet';
const NAME = 'RemixAudio';
const URI = 'https://remix.audio';
const CACHE_TIMEOUT = 0; //6h
//const CACHE_TIMEOUT = 21600; //6h
const DESCRIPTION = 'RemixAudio profiles';
const PROFILE_QUERY_PARAM = 'profile';
const PARAMETERS = [
[
self::PROFILE_QUERY_PARAM => [
'name' => 'Profile',
'type' => 'text',
'exampleValue' => 'Amoraboy',
'required' => true
]
]
];
private $feedTitle = null;
private $feedIcon = null;
public function collectData()
{
$html = getSimpleHTMLDOM($this->getURI());
$user = $this->getUser($html);
$this->feedTitle = $user['name'];
$this->feedIcon = $user['avatar'];
$elements = $html->find('.song-container');
foreach ($elements as $element) {
$titleEl = $element->find('.song-title', 0);
$title = is_object($titleEl) ? $titleEl->plaintext : '';
$urlEl = $titleEl->find('a', 0);
$publishedEl = $element->find('.timeago', 0);
$songEl = $element->find('.song-play-btn', 0);
$song = is_object($songEl) ? '<audio controls><source src="' . $songEl->getAttribute('data-track-url') . '" type="audio/mpeg" /></audio>' : '';
$item = [];
$item['uri'] = $urlEl->href;
$item['title'] = $title;
$item['timestamp'] = strtotime($publishedEl->title);
$item['content'] = '<p>' . $user['name'] . ' - ' . $title . '</p>' . $song;
$this->items[] = $item;
}
}
public function getIcon()
{
if ($this->feedIcon) {
return $this->feedIcon;
}
return parent::getIcon();
}
public function getName()
{
if ($this->feedTitle) {
return $this->feedTitle . ' - ' . self::NAME;
}
return parent::getName();
}
public function getURI()
{
$profile = $this->getProfile();
if ($profile) {
return self::URI . '/profile/' . $profile;
}
return parent::getURI();
}
private function getUser($html)
{
return [
'avatar' => $html->find('.cover-avatar img', 0)->src,
'name' => $html->find('.cover-username a', 0)->plaintext
];
}
private function getProfile()
{
return $this->getInput(self::PROFILE_QUERY_PARAM);
}
}

View File

@@ -342,15 +342,12 @@ class ReutersBridge extends BridgeAbstract
{
$img_placeholder = '';
foreach ($images as $image) { // Add more image to article.
foreach ($images as $image) {
// Add more image to article.
$image_url = $image['url'];
$image_caption = $image['caption'];
$image_caption = $image['caption'] ?? $image['alt_text'] ?? $image['subtitle'] ?? '';
$image_alt_text = '';
if (isset($image['alt_text'])) {
$image_alt_text = $image['alt_text'];
} else {
$image_alt_text = $image_caption;
}
$image_alt_text = $image['alt_text'] ?? $image_caption;
$img = "<img src=\"$image_url\" alt=\"$image_alt_text\">";
$img_caption = "<figcaption style=\"text-align: center;\"><i>$image_caption</i></figcaption>";
$figure = "<figure>$img \t $img_caption</figure>";
@@ -557,7 +554,11 @@ EOD;
$image_placeholder = $this->handleImage([$story['thumbnail']]);
}
$content = $story['description'] . $image_placeholder;
$category = [$story['primary_section']['name']];
if (isset($story['primary_section']['name'])) {
$category = [$story['primary_section']['name']];
} else {
$category = [];
}
} else {
$content_detail = $this->getArticle($article_uri);
$description = $content_detail['content'];

View File

@@ -49,7 +49,7 @@ class RoadAndTrackBridge extends BridgeAbstract
$item['title'] = $title->innertext;
}
$item['author'] = $article->find('.byline-name', 0)->innertext;
$item['author'] = $article->find('.byline-name', 0)->innertext ?? '';
$item['timestamp'] = strtotime($article->find('.content-info-date', 0)->getAttribute('datetime'));
$content = $article->find('.content-container', 0);

View File

@@ -40,9 +40,12 @@ class RumbleBridge extends BridgeAbstract
$dom = getSimpleHTMLDOM($url);
foreach ($dom->find('li.video-listing-entry') as $video) {
$datetime = $video->find('time', 0)->getAttribute('datetime');
$this->items[] = [
'title' => $video->find('h3', 0)->plaintext,
'uri' => self::URI . $video->find('a', 0)->href,
'timestamp' => (new \DateTimeImmutable($datetime))->getTimestamp(),
'author' => $account . '@rumble.com',
'content' => defaultLinkTo($video, self::URI)->innertext,
];

View File

@@ -43,10 +43,6 @@ class SchweinfurtBuergerinformationenBridge extends BridgeAbstract
foreach ($articleIDs as $articleID) {
$this->items[] = $this->generateItemFromArticle($articleID);
if (Debug::isEnabled()) {
break;
}
}
}

View File

@@ -0,0 +1,138 @@
<?php
class ScribbleHubBridge extends FeedExpander
{
const MAINTAINER = 'phantop';
const NAME = 'Scribble Hub';
const URI = 'https://scribblehub.com/';
const DESCRIPTION = 'Returns chapters from Scribble Hub.';
const PARAMETERS = [
'All' => [],
'Author' => [
'uid' => [
'name' => 'uid',
'required' => true,
// Example: Alyson Greaves's stories
'exampleValue' => '76208',
],
],
'Series' => [
'sid' => [
'name' => 'sid',
'required' => true,
// Example: latest chapters from The Sisters of Dorley by Alyson Greaves
'exampleValue' => '421879',
],
]
];
public function getIcon()
{
return self::URI . 'favicon.ico';
}
public function collectData()
{
$url = 'https://rssscribblehub.com/rssfeed.php?type=';
if ($this->queriedContext === 'Author') {
$url = $url . 'author&uid=' . $this->getInput('uid');
} else { //All and Series use the same source feed
$url = $url . 'main';
}
$this->collectExpandableDatas($url);
}
protected function parseItem($newItem)
{
$item = parent::parseItem($newItem);
//For series, filter out other series from 'All' feed
if (
$this->queriedContext === 'Series'
&& preg_match('/read\/' . $this->getInput('sid') . '-/', $item['uri']) !== 1
) {
return [];
}
$item['comments'] = $item['uri'] . '#comments';
try {
$item_html = getSimpleHTMLDOMCached($item['uri']);
} catch (HttpException $e) {
// 403 Forbidden, This means we got anti-bot response
if ($e->getCode() === 403) {
return $item;
}
throw $e;
}
$item_html = defaultLinkTo($item_html, self::URI);
//Retrieve full description from page contents
$item['content'] = $item_html->find('#chp_raw', 0);
//Retrieve image for thumbnail
$item_image = $item_html->find('.s_novel_img > img', 0)->src;
$item['enclosures'] = [$item_image];
//Restore lost categories
$item_story = html_entity_decode($item_html->find('.chp_byauthor > a', 0)->innertext);
$item_sid = $item_html->find('#mysid', 0)->value;
$item['categories'] = [$item_story, $item_sid];
//Generate UID
$item_pid = $item_html->find('#mypostid', 0)->value;
$item['uid'] = $item_sid . "/$item_pid";
return $item;
}
public function getName()
{
$name = parent::getName() . " $this->queriedContext";
switch ($this->queriedContext) {
case 'Author':
try {
$page = getSimpleHTMLDOMCached(self::URI . 'profile/' . $this->getInput('uid'));
} catch (HttpException $e) {
// 403 Forbidden, This means we got anti-bot response
if ($e->getCode() === 403) {
return $name;
}
throw $e;
}
$title = html_entity_decode($page->find('.p_m_username.fp_authorname', 0)->plaintext);
break;
case 'Series':
try {
$page = getSimpleHTMLDOMCached(self::URI . 'series/' . $this->getInput('sid') . '/a');
} catch (HttpException $e) {
// 403 Forbidden, This means we got anti-bot response
if ($e->getCode() === 403) {
return $item;
}
throw $e;
}
$title = html_entity_decode($page->find('.fic_title', 0)->plaintext);
break;
}
if (isset($title)) {
$name .= " - $title";
}
return $name;
}
public function getURI()
{
$uri = parent::getURI();
switch ($this->queriedContext) {
case 'Author':
$uri = self::URI . 'profile/' . $this->getInput('uid');
break;
case 'Series':
$uri = self::URI . 'series/' . $this->getInput('sid') . '/a';
break;
}
return $uri;
}
}

View File

@@ -0,0 +1,44 @@
<?php
class SleeperFantasyFootballBridge extends BridgeAbstract
{
const NAME = 'Sleeper.com Alerts';
const URI = 'https://sleeper.com/topics/170000000000000000';
const DESCRIPTION = 'Fantasy Football Alerts from Sleeper.com';
const MAINTAINER = 'piyushpaliwal';
const PARAMETERS = [];
const CACHE_TIMEOUT = 3600; // 1 hour
public function collectData()
{
$html = getSimpleHTMLDOMCached(self::URI, self::CACHE_TIMEOUT);
foreach ($html->find('div.content > div.latest-topics > a') as $index => $a) {
$content = $a->find('div.title > p', 0)->innertext;
$meta = $this->processString($a->find('div.desc > div.username', 0)->innertext);
$item['title'] = $content;
$item['content'] = $content;
$item['categories'] = $a->find('div.title div.tag', 0)->innertext;
$item['timestamp'] = $meta['timestamp'];
$item['author'] = $meta['author'];
$item['enclosures'] = $a->find('div.player-photo amp-img', 0)->src;
$this->items[] = $item;
if (count($this->items) >= 10) {
break;
}
}
}
protected function processString($inputString)
{
$decodedString = str_replace(['&nbsp;', '&#8226;'], [' ', '|'], $inputString);
$splitArray = explode(' | ', $decodedString);
$author = trim($splitArray[0]);
$timeString = trim($splitArray[1]);
$timestamp = strtotime($timeString);
return [
'author' => $author,
'timestamp' => $timestamp
];
}
}

View File

@@ -36,13 +36,17 @@ class SoundCloudBridge extends BridgeAbstract
private $feedTitle = null;
private $feedIcon = null;
private $clientIDCache = null;
private $cache = null;
private $clientIdRegex = '/client_id.*?"(.+?)"/';
private $widgetRegex = '/widget-.+?\.js/';
public function collectData()
{
$this->cache = RssBridge::getCache();
$this->cache->setScope('SoundCloudBridge');
$this->cache->setKey(['client_id']);
$res = $this->getUser($this->getInput('u'));
$this->feedTitle = $res->username;
@@ -62,8 +66,7 @@ class SoundCloudBridge extends BridgeAbstract
$item['author'] = $apiItem->user->username;
$item['title'] = $apiItem->user->username . ' - ' . $apiItem->title;
$item['timestamp'] = strtotime($apiItem->created_at);
$description = nl2br($apiItem->description);
$description = nl2br($apiItem->description ?? '');
$item['content'] = <<<HTML
<p>{$description}</p>
@@ -116,24 +119,11 @@ HTML;
return parent::getName();
}
private function initClientIDCache()
{
if ($this->clientIDCache !== null) {
return;
}
$cacheFactory = new CacheFactory();
$this->clientIDCache = $cacheFactory->create();
$this->clientIDCache->setScope('SoundCloudBridge');
$this->clientIDCache->setKey(['client_id']);
}
private function getClientID()
{
$this->initClientIDCache();
$clientID = $this->clientIDCache->loadData();
$this->cache->setScope('SoundCloudBridge');
$this->cache->setKey(['client_id']);
$clientID = $this->cache->loadData();
if ($clientID == null) {
return $this->refreshClientID();
@@ -144,8 +134,6 @@ HTML;
private function refreshClientID()
{
$this->initClientIDCache();
$playerHTML = getContents($this->playerUrl);
// Extract widget JS filenames from player page
@@ -163,7 +151,9 @@ HTML;
if (preg_match($this->clientIdRegex, $widgetJS, $matches)) {
$clientID = $matches[1];
$this->clientIDCache->saveData($clientID);
$this->cache->setScope('SoundCloudBridge');
$this->cache->setKey(['client_id']);
$this->cache->saveData($clientID);
return $clientID;
}

View File

@@ -4,7 +4,7 @@ class SpotifyBridge extends BridgeAbstract
{
const NAME = 'Spotify';
const URI = 'https://spotify.com/';
const DESCRIPTION = 'Fetches the latest albums from one or more artists or the latest tracks from one or more playlists';
const DESCRIPTION = 'Fetches the latest items from one or more artists, playlists or podcasts';
const MAINTAINER = 'Paroleen';
const CACHE_TIMEOUT = 3600;
const PARAMETERS = [ [
@@ -19,7 +19,7 @@ class SpotifyBridge extends BridgeAbstract
'required' => true
],
'country' => [
'name' => 'Country',
'name' => 'Country/Market',
'type' => 'text',
'required' => false,
'exampleValue' => 'US',
@@ -36,7 +36,7 @@ class SpotifyBridge extends BridgeAbstract
'name' => 'Spotify URIs',
'type' => 'text',
'required' => true,
'exampleValue' => 'spotify:artist:4lianjyuR1tqf6oUX8kjrZ [,spotify:playlist:37i9dQZF1DXcBWIGoYBM5M]',
'exampleValue' => 'spotify:artist:4lianjyuR1tqf6oUX8kjrZ [,spotify:playlist:37i9dQZF1DXcBWIGoYBM5M,spotify:show:6ShFMYxeDNMo15COLObDvC]',
],
'albumtype' => [
'name' => 'Album type',
@@ -47,14 +47,199 @@ class SpotifyBridge extends BridgeAbstract
]
] ];
const TOKENURI = 'https://accounts.spotify.com/api/token';
const APIURI = 'https://api.spotify.com/v1/';
private $uri = '';
private $name = '';
private $token = '';
private $uris = [];
private $entries = [];
public function collectData()
{
$entries = $this->getAllEntries();
usort($entries, function ($entry1, $entry2) {
return $this->getDate($entry2) <=> $this->getDate($entry1);
});
foreach ($entries as $entry) {
if (! isset($entry['type'])) {
$item = $this->getTrackData($entry);
} elseif ($entry['type'] === 'album') {
$item = $this->getAlbumData($entry);
} elseif ($entry['type'] === 'episode') {
$item = $this->getEpisodeData($entry);
} else {
throw new \Exception('Spotify URI not supported');
}
$this->items[] = $item;
if ($this->getInput('limit') > 0 && count($this->items) >= $this->getInput('limit')) {
break;
}
}
}
private function getAllEntries()
{
$entries = [];
$uris = explode(',', $this->getInput('spotifyuri'));
foreach ($uris as $uri) {
$type = explode(':', $uri)[1];
$spotifyId = explode(':', $uri)[2];
$types = [
'artist' => 'album',
'playlist' => 'track',
'show' => 'episode',
];
if (!isset($types[$type])) {
throw new \Exception('Spotify URI not supported');
}
$entry_type = $types[$type];
$url = 'https://api.spotify.com/v1/' . $type . 's/' . $spotifyId . '/' . $entry_type . 's';
$query = [
'limit' => 50,
];
if ($type === 'artist') {
$query['country'] = $this->getInput('country');
$query['include_groups'] = $this->getInput('albumtype');
} else {
$query['market'] = $this->getInput('country');
}
$offset = 0;
while (true) {
$query['offset'] = $offset;
$partial = $this->fetchContent($url . '?' . http_build_query($query));
if (empty($partial['items'])) {
break;
}
$entries = array_merge($entries, $partial['items']);
$offset += 50;
}
}
return $entries;
}
private function getAlbumData($album)
{
$item = [];
$item['title'] = $album['name'];
$item['uri'] = $album['external_urls']['spotify'];
$item['timestamp'] = $this->getDate($album);
$item['author'] = $album['artists'][0]['name'];
$item['categories'] = [$album['album_type']];
$item['content'] = '<img style="width: 256px" src="' . $album['images'][0]['url'] . '">';
if ($album['total_tracks'] > 1) {
$item['content'] .= '<p>Total tracks: ' . $album['total_tracks'] . '</p>';
}
return $item;
}
private function getTrackData($track)
{
$item = [];
$item['title'] = $track['track']['name'];
$item['uri'] = $track['track']['external_urls']['spotify'];
$item['timestamp'] = $this->getDate($track);
$item['author'] = $track['track']['artists'][0]['name'];
$item['categories'] = ['track'];
$item['content'] = '<img style="width: 256px" src="' . $track['track']['album']['images'][0]['url'] . '">';
return $item;
}
private function getEpisodeData($episode)
{
$item = [];
$item['title'] = $episode['name'];
$item['uri'] = $episode['external_urls']['spotify'];
$item['timestamp'] = $this->getDate($episode);
$item['content'] = '<img style="width: 256px" src="' . $episode['images'][0]['url'] . '">';
if (isset($episode['description'])) {
$item['content'] = $item['content'] . '<p>' . $episode['description'] . '</p>';
}
if (isset($episode['audio_preview_url'])) {
$item['content'] = $item['content'] . '<audio controls src="' . $episode['audio_preview_url'] . '"></audio>';
}
return $item;
}
private function getDate($entry)
{
if (isset($entry['type'])) {
$type = 'release_date';
} else {
$type = 'added_at';
}
$date = $entry[$type];
if (strlen($date) == 4) {
$date .= '-01-01';
} elseif (strlen($date) == 7) {
$date .= '-01';
}
if (strlen($date) > 10) {
return DateTime::createFromFormat('Y-m-d\TH:i:s\Z', $date)->getTimestamp();
}
return DateTime::createFromFormat('Y-m-d', $date)->getTimestamp();
}
private function getToken()
{
$cache = RssBridge::getCache();
$cache->setScope('SpotifyBridge');
$cacheKey = sprintf('%s:%s', $this->getInput('clientid'), $this->getInput('clientsecret'));
$cache->setKey([$cacheKey]);
$time = null;
if ($cache->getTime()) {
$time = (new DateTime())->getTimestamp() - $cache->getTime();
}
if (!$cache->getTime() || $time >= 3600) {
$this->fetchToken();
$cache->saveData($this->token);
} else {
$this->token = $cache->loadData();
}
}
private function fetchToken()
{
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, 'https://accounts.spotify.com/api/token');
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($curl, CURLOPT_POST, 1);
curl_setopt($curl, CURLOPT_POSTFIELDS, 'grant_type=client_credentials');
$basic = sprintf('%s:%s', $this->getInput('clientid'), $this->getInput('clientsecret'));
curl_setopt($curl, CURLOPT_HTTPHEADER, ['Authorization: Basic ' . base64_encode($basic)]);
$json = curl_exec($curl);
$json = json_decode($json)->access_token;
curl_close($curl);
$this->token = $json;
}
private function fetchContent($url)
{
$this->getToken();
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, $url);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($curl, CURLOPT_HTTPHEADER, ['Authorization: Bearer ' . $this->token]);
$json = curl_exec($curl);
$json = json_decode($json, true);
curl_close($curl);
return $json;
}
public function getURI()
{
@@ -74,83 +259,22 @@ class SpotifyBridge extends BridgeAbstract
return $this->name;
}
public function getIcon()
{
return 'https://www.scdn.co/i/_global/favicon.png';
}
private function getUriType($uri)
{
return explode(':', $uri)[1];
}
private function getId($uri)
{
return explode(':', $uri)[2];
}
private function getDate($entry)
{
if ($entry['type'] === 'album') {
$date = $entry['release_date'];
} else {
$date = $entry['added_at'];
}
if (strlen($date) == 4) {
$date .= '-01-01';
} elseif (strlen($date) == 7) {
$date .= '-01';
}
if (strlen($date) > 10) {
return DateTime::createFromFormat('Y-m-d\TH:i:s\Z', $date)->getTimestamp();
}
return DateTime::createFromFormat('Y-m-d', $date)->getTimestamp();
}
private function getAlbumType()
{
return $this->getInput('albumtype');
}
private function getCountry()
{
return $this->getInput('country');
}
private function getToken()
{
$cacheFactory = new CacheFactory();
$cache = $cacheFactory->create();
$cache->setScope('SpotifyBridge');
$cache->setKey(['token']);
if ($cache->getTime()) {
$time = (new DateTime())->getTimestamp() - $cache->getTime();
Debug::log('Token time: ' . $time);
}
if ($cache->getTime() == false || $time >= 3600) {
Debug::log('Fetching token from Spotify');
$this->fetchToken();
$cache->saveData($this->token);
} else {
Debug::log('Loading token from cache');
$this->token = $cache->loadData();
}
Debug::log('Token: ' . $this->token);
}
private function getFirstEntry()
{
$uris = explode(',', $this->getInput('spotifyuri'));
if (!is_null($this->getInput('spotifyuri')) && strpos($this->getInput('spotifyuri'), ',') === false) {
$type = $this->getUriType($this->uris[0]) . 's';
$item = $this->fetchContent(self::APIURI . $type . '/'
. $this->getId($this->uris[0]));
$firstUri = $uris[0];
$type = explode(':', $firstUri)[1];
$spotifyId = explode(':', $firstUri)[2];
$uri = 'https://api.spotify.com/v1/' . $type . 's/' . $spotifyId;
$query = [];
if ($type === 'show') {
$query['market'] = $this->getInput('country');
}
$item = $this->fetchContent($uri . '?' . http_build_query($query));
$this->uri = $item['external_urls']['spotify'];
$this->name = $item['name'] . ' - Spotify';
} else {
@@ -159,165 +283,8 @@ class SpotifyBridge extends BridgeAbstract
}
}
private function getAllUris()
public function getIcon()
{
Debug::log('Parsing all uris');
$this->uris = explode(',', $this->getInput('spotifyuri'));
}
private function getAllEntries()
{
$this->entries = [];
$this->getAllUris();
Debug::log('Fetching all entries');
foreach ($this->uris as $uri) {
$type = $this->getUriType($uri) . 's';
$entry_type = $type === 'artists' ? 'albums' : 'tracks';
$fetch = true;
$offset = 0;
$api_url = self::APIURI . $type . '/'
. $this->getId($uri)
. '/' . $entry_type
. '?limit=50&country='
. $this->getCountry();
if ($type === 'artists') {
$api_url = $api_url . '&include_groups=' . $this->getAlbumType();
}
while ($fetch) {
$partial = $this->fetchContent($api_url
. '&offset='
. $offset);
if (!empty($partial['items'])) {
$this->entries = array_merge(
$this->entries,
$partial['items']
);
} else {
$fetch = false;
}
$offset += 50;
}
}
}
private function fetchToken()
{
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, self::TOKENURI);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($curl, CURLOPT_POST, 1);
curl_setopt($curl, CURLOPT_POSTFIELDS, 'grant_type=client_credentials');
curl_setopt($curl, CURLOPT_HTTPHEADER, ['Authorization: Basic '
. base64_encode($this->getInput('clientid')
. ':'
. $this->getInput('clientsecret'))]);
$json = curl_exec($curl);
$json = json_decode($json)->access_token;
curl_close($curl);
$this->token = $json;
}
private function fetchContent($url)
{
$this->getToken();
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, $url);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($curl, CURLOPT_HTTPHEADER, ['Authorization: Bearer '
. $this->token]);
Debug::log('Fetching content from ' . $url);
$json = curl_exec($curl);
$json = json_decode($json, true);
curl_close($curl);
return $json;
}
private function sortEntries()
{
Debug::log('Sorting entries');
usort($this->entries, function ($entry1, $entry2) {
if ($this->getDate($entry1) < $this->getDate($entry2)) {
return 1;
} else {
return -1;
}
});
}
private function getAlbumData($album)
{
$item = [];
$item['title'] = $album['name'];
$item['uri'] = $album['external_urls']['spotify'];
$item['timestamp'] = $this->getDate($album);
$item['author'] = $album['artists'][0]['name'];
$item['categories'] = [$album['album_type']];
$item['content'] = '<img style="width: 256px" src="'
. $album['images'][0]['url']
. '">';
if ($album['total_tracks'] > 1) {
$item['content'] .= '<p>Total tracks: '
. $album['total_tracks']
. '</p>';
}
return $item;
}
private function getTrackData($track)
{
$item = [];
$item['title'] = $track['track']['name'];
$item['uri'] = $track['track']['external_urls']['spotify'];
$item['timestamp'] = $this->getDate($track);
$item['author'] = $track['track']['artists'][0]['name'];
$item['categories'] = ['track'];
$item['content'] = '<img style="width: 256px" src="'
. $track['track']['album']['images'][0]['url']
. '">';
return $item;
}
public function collectData()
{
$offset = 0;
$this->getAllEntries();
$this->sortEntries();
Debug::log('Building RSS feed');
foreach ($this->entries as $entry) {
if ($entry['type'] === 'album') {
$item = $this->getAlbumData($entry);
} else {
$item = $this->getTrackData($entry);
}
$this->items[] = $item;
if ($this->getInput('limit') > 0 && count($this->items) >= $this->getInput('limit')) {
break;
}
}
return 'https://www.scdn.co/i/_global/favicon.png';
}
}

View File

@@ -10,6 +10,8 @@ class TheHackerNewsBridge extends BridgeAbstract
public function collectData()
{
$html = getSimpleHTMLDOM($this->getURI());
$html = convertLazyLoading($html);
$html = defaultLinkTo($html, $this->getURI());
$limit = 0;
foreach ($html->find('div.body-post') as $element) {
@@ -17,74 +19,68 @@ class TheHackerNewsBridge extends BridgeAbstract
break;
}
// Author (not present on home page)
$article_author = null;
$icon_user = $element->find('i.icon-user', 0);
if ($icon_user) {
$article_author = trim($icon_user->parent()->plaintext);
$article_author = str_replace('&#59396;', '', $article_author);
}
// Title
$article_title = $element->find('h2.home-title', 0)->plaintext;
// Date
$article_timestamp = time();
//Date without time
$calendar = $element->find('i.icon-calendar', 0);
if ($calendar) {
$article_timestamp = strtotime(
extractFromDelimiters(
$calendar->parent()->outertext,
'</i>',
'<span>'
'</span>'
)
);
}
//Article thumbnail in lazy-loading image
if (is_object($element->find('img[data-echo]', 0))) {
$article_thumbnail = [
extractFromDelimiters(
$element->find('img[data-echo]', 0)->outertext,
"data-echo='",
"'"
)
];
} else {
$article_thumbnail = [];
// Thumbnail
$article_thumbnail = [];
if (is_object($element->find('img', 0))) {
$article_thumbnail = [ $element->find('img', 0)->src ];
}
// Content (truncated)
$article_content = $element->find('div.home-desc', 0)->plaintext;
// Now try expanding article
$article_url = $element->find('a.story-link', 0)->href;
$article = getSimpleHTMLDOMCached($article_url);
if ($article) {
//Article body
$var = $article->find('div.articlebody', 0);
if ($var) {
$contents = $var->innertext;
$contents = stripRecursiveHtmlSection($contents, 'div', '<div class="ad_');
$contents = stripWithDelimiters($contents, 'id="google_ads', '</iframe>');
$contents = stripWithDelimiters($contents, '<script', '</script>');
$article_html = getSimpleHTMLDOMCached($article_url);
if ($article_html) {
// Content (expanded and cleaned)
$article_body = $article_html->find('div.articlebody', 0);
if ($article_body) {
$article_body = convertLazyLoading($article_body);
$article_body = defaultLinkTo($article_body, $article_url);
$header_img = $article_body->find('img', 0);
if ($header_img) {
$header_img->parent->style = '';
}
foreach ($article_body->find('center.cf') as $center_ad) {
$center_ad->outertext = '';
}
$article_content = $article_body->innertext;
}
//Date with time
if (is_object($article->find('meta[itemprop=dateModified]', 0))) {
$article_timestamp = strtotime(
extractFromDelimiters(
$article->find('meta[itemprop=dateModified]', 0)->outertext,
"content='",
"'"
)
);
// Author
$spans_author = $article_html->find('span.author');
if (count($spans_author) > 0) {
$article_author = $spans_author[array_key_last($spans_author)]->plaintext;
}
} else {
$contents = 'Could not request TheHackerNews: ' . $article_url;
}
$item = [];
$item['uri'] = $article_url;
$item['title'] = $article_title;
if ($article_author) {
if (!empty($article_author)) {
$item['author'] = $article_author;
}
$item['enclosures'] = $article_thumbnail;
$item['timestamp'] = $article_timestamp;
$item['content'] = trim($contents ?? '');
$item['content'] = trim($article_content);
$this->items[] = $item;
$limit++;
}

View File

@@ -26,18 +26,6 @@ class TikTokBridge extends BridgeAbstract
private $feedName = '';
public function detectParameters($url)
{
if (preg_match('/tiktok\.com\/(@[\w]+)/', $url, $matches) > 0) {
return [
'context' => 'By user',
'username' => $matches[1]
];
}
return null;
}
public function collectData()
{
$html = getSimpleHTMLDOM($this->getURI());
@@ -48,12 +36,24 @@ class TikTokBridge extends BridgeAbstract
foreach ($html->find('div.tiktok-x6y88p-DivItemContainerV2') as $div) {
$item = [];
// todo: find proper link to tiktok item
$link = $div->find('a', 0)->href;
$image = $div->find('img', 0)->src;
$image = $div->find('img', 0)->src ?? '';
$views = $div->find('strong.video-count', 0)->plaintext;
if ($link === 'https://www.tiktok.com/') {
$link = $this->getURI();
}
$item['uri'] = $link;
$item['title'] = $div->find('a', 1)->plaintext;
$a = $div->find('a', 1);
if ($a) {
$item['title'] = $a->plaintext;
} else {
$item['title'] = $this->getName();
}
$item['enclosures'][] = $image;
$item['content'] = <<<EOD
@@ -87,10 +87,25 @@ EOD;
private function processUsername()
{
if (substr($this->getInput('username'), 0, 1) !== '@') {
return '@' . $this->getInput('username');
$username = trim($this->getInput('username'));
if (preg_match('#^https?://www\.tiktok\.com/@(.*)$#', $username, $m)) {
return '@' . $m[1];
}
if (substr($username, 0, 1) !== '@') {
return '@' . $username;
}
return $username;
}
public function detectParameters($url)
{
if (preg_match('/tiktok\.com\/(@[\w]+)/', $url, $matches) > 0) {
return [
'context' => 'By user',
'username' => $matches[1]
];
}
return $this->getInput('username');
return null;
}
}

View File

@@ -223,10 +223,10 @@ EOD;
CURLOPT_POSTFIELDS => json_encode($request)
];
Debug::log("Sending GraphQL query:\n" . $query);
Debug::log("Sending GraphQL variables:\n" . json_encode($variables, JSON_PRETTY_PRINT));
Logger::debug("Sending GraphQL query:\n" . $query);
Logger::debug("Sending GraphQL variables:\n" . json_encode($variables, JSON_PRETTY_PRINT));
$response = json_decode(getContents('https://gql.twitch.tv/gql', $header, $opts));
Debug::log("Got GraphQL response:\n" . json_encode($response, JSON_PRETTY_PRINT));
Logger::debug("Got GraphQL response:\n" . json_encode($response, JSON_PRETTY_PRINT));
if (isset($response->errors)) {
$messages = array_column($response->errors, 'message');

View File

@@ -123,7 +123,7 @@ EOD
private $apiKey = null;
private $guestToken = null;
private $authHeader = [];
private $authHeaders = [];
public function detectParameters($url)
{
@@ -219,25 +219,27 @@ EOD
$tweets = [];
// Get authentication information
$this->getApiKey();
// Try to get all tweets
switch ($this->queriedContext) {
case 'By username':
$user = $this->makeApiCall('/1.1/users/show.json', ['screen_name' => $this->getInput('u')]);
if (!$user) {
returnServerError('Requested username can\'t be found.');
}
$cache = RssBridge::getCache();
$cache->setScope('twitter');
$cache->setKey(['cache']);
// todo: inspect mtime instead of purging with 3h
$cache->purgeCache(60 * 60 * 3);
$api = new TwitterClient($cache);
$params = [
'user_id' => $user->id_str,
'tweet_mode' => 'extended'
];
$screenName = $this->getInput('u');
$screenName = trim($screenName);
$screenName = ltrim($screenName, '@');
$data = $api->fetchUserTweets($screenName);
$data = $this->makeApiCall('/1.1/statuses/user_timeline.json', $params);
break;
case 'By keyword or hashtag':
// Does not work with the recent twitter changes
$params = [
'q' => urlencode($this->getInput('q')),
'tweet_mode' => 'extended',
@@ -248,6 +250,7 @@ EOD
break;
case 'By list':
// Does not work with the recent twitter changes
$params = [
'slug' => strtolower($this->getInput('list')),
'owner_screen_name' => strtolower($this->getInput('user')),
@@ -258,6 +261,7 @@ EOD
break;
case 'By list ID':
// Does not work with the recent twitter changes
$params = [
'list_id' => $this->getInput('listid'),
'tweet_mode' => 'extended',
@@ -284,7 +288,10 @@ EOD
}
// Filter out unwanted tweets
foreach ($data as $tweet) {
foreach ($data->tweets as $tweet) {
if (!$tweet) {
continue;
}
// Filter out retweets to remove possible duplicates of original tweet
switch ($this->queriedContext) {
case 'By keyword or hashtag':
@@ -333,9 +340,9 @@ EOD
$realtweet = $tweet->retweeted_status;
}
$item['username'] = $realtweet->user->screen_name;
$item['fullname'] = $realtweet->user->name;
$item['avatar'] = $realtweet->user->profile_image_url_https;
$item['username'] = $data->user_info->legacy->screen_name;
$item['fullname'] = $data->user_info->legacy->name;
$item['avatar'] = $data->user_info->legacy->profile_image_url_https;
$item['timestamp'] = $realtweet->created_at;
$item['id'] = $realtweet->id_str;
$item['uri'] = self::URI . $item['username'] . '/status/' . $item['id'];
@@ -503,9 +510,7 @@ EOD;
//This function takes 2 requests, and therefore is cached
private function getApiKey($forceNew = 0)
{
$cacheFactory = new CacheFactory();
$r_cache = $cacheFactory->create();
$r_cache = RssBridge::getCache();
$scope = 'TwitterBridge';
$r_cache->setScope($scope);
$r_cache->setKey(['refresh']);
@@ -521,7 +526,7 @@ EOD;
$cacheFactory = new CacheFactory();
$cache = $cacheFactory->create();
$cache = RssBridge::getCache();
$cache->setScope($scope);
$cache->setKey(['api_key']);
$data = $cache->loadData();
@@ -556,9 +561,7 @@ EOD;
$apiKey = $data;
}
$cacheFac2 = new CacheFactory();
$gt_cache = $cacheFactory->create();
$gt_cache = RssBridge::getCache();
$gt_cache->setScope($scope);
$gt_cache->setKey(['guest_token']);
$guestTokenUses = $gt_cache->loadData();

View File

@@ -0,0 +1,30 @@
<?php
class UsesTechbridge extends BridgeAbstract
{
const NAME = '/uses';
const URI = 'https://uses.tech/';
const DESCRIPTION = 'RSS feed for /uses';
const MAINTAINER = 'jummo4@yahoo.de';
const MAX_ITEM = 100; # Maximum items to loop through which works fast enough on my computer
public function collectData()
{
$html = getSimpleHTMLDOM(self::URI)
or returnServerError('Could not request: ' . self::URI);
foreach ($html->find('div[class=PersonInner]') as $index => $a) {
$item = []; // Create an empty item
$articlePath = $a->find('a[class=displayLink]', 0)->href;
$item['title'] = $a->find('img', 0)->getAttribute('alt');
$item['author'] = $a->find('img', 0)->getAttribute('alt');
$item['uri'] = $articlePath;
$item['content'] = $a->find('p', 0)->innertext;
$this->items[] = $item; // Add item to the list
if (count($this->items) >= self::MAX_ITEM) {
break;
}
}
}
}

View File

@@ -0,0 +1,76 @@
<?php
class VideoCardzBridge extends BridgeAbstract
{
const NAME = 'VideoCardz';
const URI = 'https://videocardz.com/';
const DESCRIPTION = 'Returns news from VideoCardz.com';
const MAINTAINER = 'rmscoelho';
const CACHE_TIMEOUT = 3600;
const PARAMETERS = [
[
'feed' => [
'name' => 'News Feed',
'type' => 'list',
'title' => 'Feeds from VideoCardz.com',
'values' => [
'News' => 'sections/news',
'Featured' => 'sections/featured',
'Leaks' => 'sections/leaks',
'Press Releases' => 'sections/press-releases',
'Preview Roundup' => 'sections/review-roundup',
'Rumour' => 'sections/rumor',
]
]
]
];
public function getIcon()
{
return 'https://videocardz.com/favicon-32x32.png?x66580';
}
public function getName()
{
return !is_null($this->getKey('feed')) ? self::NAME . ' | ' . $this->getKey('feed') : self::NAME;
}
public function getURI()
{
return self::URI . $this->getInput('feed');
}
public function collectData()
{
$url = sprintf('https://videocardz.com/%s', $this->getInput('feed'));
$dom = getSimpleHTMLDOM($url);
$dom = $dom->find('.subcategory-news', 0);
if (!$dom) {
throw new \Exception(sprintf('Unable to find css selector on `%s`', $url));
}
$dom = defaultLinkTo($dom, $this->getURI());
foreach ($dom->find('article') as $article) {
$title = preg_replace('/\(PR\) /i', '', $article->find('h2', 0)->plaintext);
//Get thumbnail
$image = $article->style;
$image = preg_replace('/background-image:url\(/i', '', $image);
$image = substr_replace($image, '', -3);
//Get date and time of publishing
$datetime = date_parse($article->find('.main-index-article-datetitle-date > a', 0)->plaintext);
$year = $datetime['year'];
$month = $datetime['month'];
$day = $datetime['day'];
$hour = $datetime['hour'];
$minute = $datetime['minute'];
$timestamp = mktime($hour, $minute, 0, $month, $day, $year);
$content = '<img src="' . $image . '" alt="' . $article->find('h2', 0)->plaintext . ' thumbnail" />';
$this->items[] = [
'title' => $title,
'uri' => $article->find('p.main-index-article-datetitle-date > a', 0)->href,
'content' => $content,
'timestamp' => $timestamp,
];
}
}
}

View File

@@ -22,8 +22,18 @@ class VkBridge extends BridgeAbstract
]
]
];
const TEST_DETECT_PARAMETERS = [
'https://vk.com/id1' => ['u' => 'id1'],
'https://vk.com/groupname' => ['u' => 'groupname'],
'https://m.vk.com/groupname' => ['u' => 'groupname'],
'https://vk.com/groupname/anythingelse' => ['u' => 'groupname'],
'https://vk.com/groupname?w=somethingelse' => ['u' => 'groupname'],
'https://vk.com/with_underscore' => ['u' => 'with_underscore'],
];
protected $pageName;
protected $tz = 0;
private $urlRegex = '/vk\.com\/([\w]+)/';
public function getURI()
{
@@ -43,6 +53,15 @@ class VkBridge extends BridgeAbstract
return parent::getName();
}
public function detectParameters($url)
{
if (preg_match($this->urlRegex, $url, $matches)) {
return ['u' => $matches[1]];
}
return null;
}
public function collectData()
{
$text_html = $this->getContents();
@@ -50,6 +69,13 @@ class VkBridge extends BridgeAbstract
$text_html = iconv('windows-1251', 'utf-8//ignore', $text_html);
$html = str_get_html($text_html);
foreach ($html->find('script') as $script) {
preg_match('/tz: ([0-9]+)/', $script->outertext, $matches);
if (count($matches) > 0) {
$this->tz = intval($matches[1]);
break;
}
}
$pageName = $html->find('.page_name', 0);
if (is_object($pageName)) {
$pageName = $pageName->plaintext;
@@ -285,6 +311,44 @@ class VkBridge extends BridgeAbstract
$copy_quote->outertext = "<br>Reposted ($copy_quote_author): <br>$copy_quote_content";
}
foreach ($post->find('.SecondaryAttachment') as $sa) {
$sa_href = $sa->getAttribute('href');
if (!$sa_href) {
$sa_href = '';
}
$sa_task_click = $sa->getAttribute('data-task-click');
if (str_starts_with($sa_href, 'https://vk.com/doc')) {
// document
$doc_title = $sa->find('.SecondaryAttachment__childrenText', 0)->innertext;
$doc_size = $sa->find('.SecondaryAttachmentSubhead', 0)->innertext;
$doc_link = $sa_href;
$content_suffix .= "<br>Doc: <a href='$doc_link'>$doc_title</a> ($doc_size)";
$sa->outertext = '';
} else if (str_starts_with($sa_href, 'https://vk.com/@')) {
// article
$article_title = $sa->find('.SecondaryAttachment__childrenText', 0)->innertext;
$article_author = explode('Article · from ', $sa->find('.SecondaryAttachmentSubhead', 0)->innertext)[1];
$article_link = $sa_href;
$content_suffix .= "<br>Article: <a href='$article_link'>$article_title ($article_author)</a>";
$sa->outertext = '';
} else if ($sa_task_click == 'SecondaryAttachment/playAudio') {
// audio
$audio_json = json_decode(html_entity_decode($sa->getAttribute('data-audio')));
$audio_link = $audio_json->url;
$audio_title = $sa->find('.SecondaryAttachment__childrenText', 0)->innertext;
$audio_author = $sa->find('.SecondaryAttachmentSubhead', 0)->innertext;
$content_suffix .= "<br>Audio: <a href='$audio_link'>$audio_title ($audio_author)</a>";
$sa->outertext = '';
} else if ($sa_task_click == 'SecondaryAttachment/playPlaylist') {
// playlist link
$playlist_title = $sa->find('.SecondaryAttachment__childrenText', 0)->innertext;
$playlist_link = $sa->find('.SecondaryAttachment__link', 0)->getAttribute('href');
$content_suffix .= "<br>Playlist: <a href='$playlist_link'>$playlist_title</a>";
$sa->outertext = '';
}
}
$item = [];
$content = strip_tags(backgroundToImg($post->find('div.wall_text', 0)->innertext), '<a><br><img>');
$content .= $content_suffix;
@@ -393,8 +457,9 @@ class VkBridge extends BridgeAbstract
private function getTime($post)
{
if ($time = $post->find('time.PostHeaderSubtitle__item', 0)->getAttribute('time')) {
return $time;
$accurateDateElement = $post->find('span.rel_date', 0);
if ($accurateDateElement) {
return $accurateDateElement->getAttribute('time');
} else {
$strdate = $post->find('time.PostHeaderSubtitle__item', 0)->plaintext;
$strdate = preg_replace('/[\x00-\x1F\x7F-\xFF]/', ' ', $strdate);
@@ -417,7 +482,7 @@ class VkBridge extends BridgeAbstract
$date['hour'] = $date['minute'] = '00';
}
return strtotime($date['day'] . '-' . $date['month'] . '-' . $date['year'] . ' ' .
$date['hour'] . ':' . $date['minute']);
$date['hour'] . ':' . $date['minute']) - $this->tz;
}
}

View File

@@ -0,0 +1,27 @@
<?php
class WYMTNewsBridge extends BridgeAbstract
{
const NAME = 'WYMT Mountain News';
const URI = 'https://www.wymt.com/news/';
const DESCRIPTION = 'Returns the recent articles published on WYMT Mountain News (Hazard KY)';
const MAINTAINER = 'mattconnell';
public function collectData()
{
$html = getSimpleHTMLDOM(self::URI);
$html = defaultLinkTo($html, self::URI);
$articles = $html->find('.card-body');
foreach ($articles as $article) {
$item = [];
$url = $article->find('.headline a', 0);
$item['uri'] = $url->href;
$item['title'] = trim($url->plaintext);
$item['author'] = $article->find('.author', 0)->plaintext;
$item['content'] = $article->find('.deck', 0)->plaintext;
$this->items[] = $item;
}
}
}

View File

@@ -117,7 +117,7 @@ The default URI shows the Madara demo page.';
protected function getMangaInfo($url)
{
$url_cache = 'TitleInfo_' . preg_replace('/[^\w]/', '.', rtrim($url, '/'));
$cache = $this->loadCacheValue($url_cache);
$cache = $this->loadCacheValue($url_cache, 86400);
if (isset($cache)) {
return $cache;
}

View File

@@ -39,8 +39,13 @@ class YandexZenBridge extends BridgeAbstract
$item['uri'] = $post->share_link;
$item['title'] = $post->title;
$item['timestamp'] = date(DateTimeInterface::ATOM, $post->publication_date);
$item['content'] = $post->text;
$publicationDateUnixTimestamp = $post->publication_date ?? null;
if ($publicationDateUnixTimestamp) {
$item['timestamp'] = date(DateTimeInterface::ATOM, $publicationDateUnixTimestamp);
}
$item['content'] = $post->text . "<br /><img src='$post->image' />";
$item['enclosures'] = [
$post->image,
];

View File

@@ -78,7 +78,7 @@ class YouTubeCommunityTabBridge extends BridgeAbstract
returnServerError('Channel does not have a community tab');
}
foreach ($this->getCommunityPosts($json) as $post) {
foreach ($this->getCommunityPosts($json) as $key => $post) {
$this->itemTitle = '';
if (!isset($post->backstagePostThreadRenderer)) {
@@ -102,14 +102,20 @@ class YouTubeCommunityTabBridge extends BridgeAbstract
$item['content'] .= $this->getAttachments($details);
$item['title'] = $this->itemTitle;
$date = strtotime(str_replace(' (edited)', '', $details->publishedTimeText->runs[0]->text));
if (is_int($date)) {
// subtract an increasing multiple of 60 seconds to always preserve the original order
$item['timestamp'] = $date - $key * 60;
}
$this->items[] = $item;
}
}
public function getURI()
{
if (!empty($this->feedUri)) {
return $this->feedUri;
if (!empty($this->feedUrl)) {
return $this->feedUrl;
}
return parent::getURI();

View File

@@ -13,7 +13,6 @@ class YoutubeBridge extends BridgeAbstract
const URI = 'https://www.youtube.com/';
const CACHE_TIMEOUT = 10800; // 3h
const DESCRIPTION = 'Returns the 10 newest videos by username/channel/playlist or search';
const MAINTAINER = 'em92';
const PARAMETERS = [
'By username' => [
@@ -234,7 +233,11 @@ class YoutubeBridge extends BridgeAbstract
private function getJSONData($html)
{
$scriptRegex = '/var ytInitialData = (.*?);<\/script>/';
preg_match($scriptRegex, $html, $matches) or returnServerError('Could not find ytInitialData');
$result = preg_match($scriptRegex, $html, $matches);
if (! $result) {
Logger::debug('Could not find ytInitialData');
return null;
}
return json_decode($matches[1]);
}
@@ -293,15 +296,17 @@ class YoutubeBridge extends BridgeAbstract
}
}
if (preg_match('/([\d]{1,2})\:([\d]{1,2})\:([\d]{2})/', $durationText)) {
$durationText = preg_replace('/([\d]{1,2})\:([\d]{1,2})\:([\d]{2})/', '$1:$2:$3', $durationText);
} else {
$durationText = preg_replace('/([\d]{1,2})\:([\d]{2})/', '00:$1:$2', $durationText);
}
sscanf($durationText, '%d:%d:%d', $hours, $minutes, $seconds);
$duration = $hours * 3600 + $minutes * 60 + $seconds;
if ($duration < $duration_min || $duration > $duration_max) {
continue;
if (is_string($durationText)) {
if (preg_match('/([\d]{1,2})\:([\d]{1,2})\:([\d]{2})/', $durationText)) {
$durationText = preg_replace('/([\d]{1,2})\:([\d]{1,2})\:([\d]{2})/', '$1:$2:$3', $durationText);
} else {
$durationText = preg_replace('/([\d]{1,2})\:([\d]{2})/', '00:$1:$2', $durationText);
}
sscanf($durationText, '%d:%d:%d', $hours, $minutes, $seconds);
$duration = $hours * 3600 + $minutes * 60 + $seconds;
if ($duration < $duration_min || $duration > $duration_max) {
continue;
}
}
// $vid_list .= $vid . ',';
@@ -336,6 +341,7 @@ class YoutubeBridge extends BridgeAbstract
$html = $this->ytGetSimpleHTMLDOM($url_listing);
$jsonData = $this->getJSONData($html);
$url_feed = $jsonData->metadata->channelMetadataRenderer->rssUrl;
$this->iconURL = $jsonData->metadata->channelMetadataRenderer->avatar->thumbnails[0]->url;
}
if (!$this->skipFeeds()) {
$html = $this->ytGetSimpleHTMLDOM($url_feed);
@@ -444,4 +450,13 @@ class YoutubeBridge extends BridgeAbstract
return parent::getName();
}
}
public function getIcon()
{
if (empty($this->iconURL)) {
return parent::getIcon();
} else {
return $this->iconURL;
}
}
}

View File

@@ -66,8 +66,10 @@ class ZeitBridge extends FeedExpander
$item['enclosures'] = [];
$headers = [
'User-Agent: Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)',
'X-Forwarded-For: 66.249.66.1',
'Cookie: zonconsent=' . date('Y-m-d\TH:i:s.v\Z'),
'User-Agent: Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)'];
];
// one-page article
$article = getSimpleHTMLDOM($item['uri'], $headers);

View File

@@ -3,42 +3,56 @@
class FileCache implements CacheInterface
{
private array $config;
protected $scope;
protected $key;
protected string $scope;
protected string $key;
public function __construct(array $config = [])
{
$this->config = $config;
$default = [
'path' => null,
'enable_purge' => true,
];
$this->config = array_merge($default, $config);
if (!$this->config['path']) {
throw new \Exception('The FileCache needs a path value');
}
// Normalize with a single trailing slash
$this->config['path'] = rtrim($this->config['path'], '/') . '/';
}
if (!is_dir($this->config['path'])) {
throw new \Exception('The cache path does not exists. You probably want: mkdir cache && chown www-data:www-data cache');
}
if (!is_writable($this->config['path'])) {
throw new \Exception('The cache path is not writeable. You probably want: chown www-data:www-data cache');
}
public function getConfig()
{
return $this->config;
}
public function loadData()
{
if (file_exists($this->getCacheFile())) {
return unserialize(file_get_contents($this->getCacheFile()));
if (!file_exists($this->getCacheFile())) {
return null;
}
return null;
$data = unserialize(file_get_contents($this->getCacheFile()));
if ($data === false) {
// Intentionally not throwing an exception
Logger::warning(sprintf('Failed to unserialize: %s', $this->getCacheFile()));
return null;
}
return $data;
}
public function saveData($data)
public function saveData($data): void
{
$writeStream = file_put_contents($this->getCacheFile(), serialize($data));
if ($writeStream === false) {
throw new \Exception('The cache path is not writeable. You probably want: chown www-data:www-data cache');
$bytes = file_put_contents($this->getCacheFile(), serialize($data), LOCK_EX);
if ($bytes === false) {
throw new \Exception(sprintf('Failed to write to: %s', $this->getCacheFile()));
}
return $this;
}
public function getTime()
public function getTime(): ?int
{
// https://www.php.net/manual/en/function.clearstatcache.php
clearstatcache();
$cacheFile = $this->getCacheFile();
clearstatcache(false, $cacheFile);
if (file_exists($cacheFile)) {
$time = filemtime($cacheFile);
if ($time !== false) {
@@ -50,7 +64,7 @@ class FileCache implements CacheInterface
return null;
}
public function purgeCache($seconds)
public function purgeCache(int $seconds): void
{
if (! $this->config['enable_purge']) {
return;
@@ -66,38 +80,32 @@ class FileCache implements CacheInterface
);
foreach ($cacheIterator as $cacheFile) {
if (in_array($cacheFile->getBasename(), ['.', '..', '.gitkeep'])) {
$basename = $cacheFile->getBasename();
$excluded = [
'.' => true,
'..' => true,
'.gitkeep' => true,
];
if (isset($excluded[$basename])) {
continue;
} elseif ($cacheFile->isFile()) {
if (filemtime($cacheFile->getPathname()) < time() - $seconds) {
$filepath = $cacheFile->getPathname();
if (filemtime($filepath) < time() - $seconds) {
// todo: sometimes this file doesn't exists
unlink($cacheFile->getPathname());
unlink($filepath);
}
}
}
}
public function setScope($scope)
public function setScope(string $scope): void
{
if (!is_string($scope)) {
throw new \Exception('The given scope is invalid!');
}
$this->scope = $this->config['path'] . trim($scope, " \t\n\r\0\x0B\\\/") . '/';
return $this;
}
public function setKey($key)
public function setKey(array $key): void
{
$key = json_encode($key);
if (!is_string($key)) {
throw new \Exception('The given key is invalid!');
}
$this->key = $key;
return $this;
$this->key = json_encode($key);
}
private function getScope()

View File

@@ -2,11 +2,11 @@
class MemcachedCache implements CacheInterface
{
private $scope;
private $key;
private string $scope;
private string $key;
private $conn;
private $expiration = 0;
private $time = false;
private $time = null;
private $data = null;
public function __construct()
@@ -58,11 +58,11 @@ class MemcachedCache implements CacheInterface
return $result['data'];
}
public function saveData($datas)
public function saveData($data): void
{
$time = time();
$object_to_save = [
'data' => $datas,
'data' => $data,
'time' => $time,
];
$result = $this->conn->set($this->getCacheKey(), $object_to_save, $this->expiration);
@@ -72,44 +72,31 @@ class MemcachedCache implements CacheInterface
}
$this->time = $time;
return $this;
}
public function getTime()
public function getTime(): ?int
{
if ($this->time === false) {
if ($this->time === null) {
$this->loadData();
}
return $this->time;
}
public function purgeCache($duration)
public function purgeCache(int $seconds): void
{
// Note: does not purges cache right now
// Just sets cache expiration and leave cache purging for memcached itself
$this->expiration = $duration;
$this->expiration = $seconds;
}
public function setScope($scope)
public function setScope(string $scope): void
{
$this->scope = $scope;
return $this;
}
public function setKey($key)
public function setKey(array $key): void
{
if (!empty($key) && is_array($key)) {
$key = array_map('strtolower', $key);
}
$key = json_encode($key);
if (!is_string($key)) {
throw new \Exception('The given key is invalid!');
}
$this->key = $key;
return $this;
$this->key = json_encode($key);
}
private function getCacheKey()

View File

@@ -4,11 +4,11 @@ declare(strict_types=1);
class NullCache implements CacheInterface
{
public function setScope($scope)
public function setScope(string $scope): void
{
}
public function setKey($key)
public function setKey(array $key): void
{
}
@@ -16,15 +16,16 @@ class NullCache implements CacheInterface
{
}
public function saveData($data)
public function saveData($data): void
{
}
public function getTime()
public function getTime(): ?int
{
return null;
}
public function purgeCache($seconds)
public function purgeCache(int $seconds): void
{
}
}

View File

@@ -5,51 +5,43 @@
*/
class SQLiteCache implements CacheInterface
{
protected $scope;
protected $key;
private \SQLite3 $db;
private string $scope;
private string $key;
private array $config;
private $db = null;
public function __construct()
public function __construct(array $config)
{
if (!extension_loaded('sqlite3')) {
throw new \Exception('"sqlite3" extension not loaded. Please check "php.ini"');
$default = [
'file' => null,
'timeout' => 5000,
'enable_purge' => true,
];
$config = array_merge($default, $config);
$this->config = $config;
if (!$config['file']) {
throw new \Exception('sqlite cache needs a file');
}
if (!is_writable(PATH_CACHE)) {
throw new \Exception('The cache folder is not writable');
}
$section = 'SQLiteCache';
$file = Configuration::getConfig($section, 'file');
if (!$file) {
throw new \Exception(sprintf('Configuration for %s missing.', $section));
}
if (dirname($file) == '.') {
$file = PATH_CACHE . $file;
} elseif (!is_dir(dirname($file))) {
throw new \Exception(sprintf('Invalid configuration for %s', $section));
}
if (!is_file($file)) {
// The instantiation creates the file
$this->db = new \SQLite3($file);
if (is_file($config['file'])) {
$this->db = new \SQLite3($config['file']);
$this->db->enableExceptions(true);
} else {
// Create the file and create sql schema
$this->db = new \SQLite3($config['file']);
$this->db->enableExceptions(true);
$this->db->exec("CREATE TABLE storage ('key' BLOB PRIMARY KEY, 'value' BLOB, 'updated' INTEGER)");
} else {
$this->db = new \SQLite3($file);
$this->db->enableExceptions(true);
}
$this->db->busyTimeout(5000);
$this->db->busyTimeout($config['timeout']);
}
public function loadData()
{
$Qselect = $this->db->prepare('SELECT value FROM storage WHERE key = :key');
$Qselect->bindValue(':key', $this->getCacheKey());
$result = $Qselect->execute();
if ($result instanceof \SQLite3Result) {
$stmt = $this->db->prepare('SELECT value FROM storage WHERE key = :key');
$stmt->bindValue(':key', $this->getCacheKey());
$result = $stmt->execute();
if ($result) {
$data = $result->fetchArray(\SQLITE3_ASSOC);
if (isset($data['value'])) {
return unserialize($data['value']);
@@ -59,24 +51,22 @@ class SQLiteCache implements CacheInterface
return null;
}
public function saveData($data)
public function saveData($data): void
{
$Qupdate = $this->db->prepare('INSERT OR REPLACE INTO storage (key, value, updated) VALUES (:key, :value, :updated)');
$Qupdate->bindValue(':key', $this->getCacheKey());
$Qupdate->bindValue(':value', serialize($data));
$Qupdate->bindValue(':updated', time());
$Qupdate->execute();
return $this;
$stmt = $this->db->prepare('INSERT OR REPLACE INTO storage (key, value, updated) VALUES (:key, :value, :updated)');
$stmt->bindValue(':key', $this->getCacheKey());
$stmt->bindValue(':value', serialize($data));
$stmt->bindValue(':updated', time());
$stmt->execute();
}
public function getTime()
public function getTime(): ?int
{
$Qselect = $this->db->prepare('SELECT updated FROM storage WHERE key = :key');
$Qselect->bindValue(':key', $this->getCacheKey());
$result = $Qselect->execute();
if ($result instanceof \SQLite3Result) {
$data = $result->fetchArray(SQLITE3_ASSOC);
$stmt = $this->db->prepare('SELECT updated FROM storage WHERE key = :key');
$stmt->bindValue(':key', $this->getCacheKey());
$result = $stmt->execute();
if ($result) {
$data = $result->fetchArray(\SQLITE3_ASSOC);
if (isset($data['updated'])) {
return $data['updated'];
}
@@ -85,44 +75,28 @@ class SQLiteCache implements CacheInterface
return null;
}
public function purgeCache($seconds)
public function purgeCache(int $seconds): void
{
$Qdelete = $this->db->prepare('DELETE FROM storage WHERE updated < :expired');
$Qdelete->bindValue(':expired', time() - $seconds);
$Qdelete->execute();
if (!$this->config['enable_purge']) {
return;
}
$stmt = $this->db->prepare('DELETE FROM storage WHERE updated < :expired');
$stmt->bindValue(':expired', time() - $seconds);
$stmt->execute();
}
public function setScope($scope)
public function setScope(string $scope): void
{
if (is_null($scope) || !is_string($scope)) {
throw new \Exception('The given scope is invalid!');
}
$this->scope = $scope;
return $this;
}
public function setKey($key)
public function setKey(array $key): void
{
if (!empty($key) && is_array($key)) {
$key = array_map('strtolower', $key);
}
$key = json_encode($key);
if (!is_string($key)) {
throw new \Exception('The given key is invalid!');
}
$this->key = $key;
return $this;
$this->key = json_encode($key);
}
private function getCacheKey()
{
if (is_null($this->key)) {
throw new \Exception('Call "setKey" first!');
}
return hash('sha1', $this->scope . $this->key, true);
}
}

557
composer.lock generated
View File

@@ -4,35 +4,35 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "0d00dfa7c120bdd750ac0ac4a45f5452",
"content-hash": "24083060ddb8be9a95e75f6596e3bb83",
"packages": [],
"packages-dev": [
{
"name": "doctrine/instantiator",
"version": "1.4.1",
"version": "1.5.0",
"source": {
"type": "git",
"url": "https://github.com/doctrine/instantiator.git",
"reference": "10dcfce151b967d20fde1b34ae6640712c3891bc"
"reference": "0a0fa9780f5d4e507415a065172d26a98d02047b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/instantiator/zipball/10dcfce151b967d20fde1b34ae6640712c3891bc",
"reference": "10dcfce151b967d20fde1b34ae6640712c3891bc",
"url": "https://api.github.com/repos/doctrine/instantiator/zipball/0a0fa9780f5d4e507415a065172d26a98d02047b",
"reference": "0a0fa9780f5d4e507415a065172d26a98d02047b",
"shasum": ""
},
"require": {
"php": "^7.1 || ^8.0"
},
"require-dev": {
"doctrine/coding-standard": "^9",
"doctrine/coding-standard": "^9 || ^11",
"ext-pdo": "*",
"ext-phar": "*",
"phpbench/phpbench": "^0.16 || ^1",
"phpstan/phpstan": "^1.4",
"phpstan/phpstan-phpunit": "^1",
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.5",
"vimeo/psalm": "^4.22"
"vimeo/psalm": "^4.30 || ^5.4"
},
"type": "library",
"autoload": {
@@ -59,7 +59,7 @@
],
"support": {
"issues": "https://github.com/doctrine/instantiator/issues",
"source": "https://github.com/doctrine/instantiator/tree/1.4.1"
"source": "https://github.com/doctrine/instantiator/tree/1.5.0"
},
"funding": [
{
@@ -75,20 +75,20 @@
"type": "tidelift"
}
],
"time": "2022-03-03T08:28:38+00:00"
"time": "2022-12-30T00:15:36+00:00"
},
{
"name": "myclabs/deep-copy",
"version": "1.11.0",
"version": "1.11.1",
"source": {
"type": "git",
"url": "https://github.com/myclabs/DeepCopy.git",
"reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614"
"reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/14daed4296fae74d9e3201d2c4925d1acb7aa614",
"reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614",
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/7284c22080590fb39f2ffa3e9057f10a4ddd0e0c",
"reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c",
"shasum": ""
},
"require": {
@@ -126,7 +126,7 @@
],
"support": {
"issues": "https://github.com/myclabs/DeepCopy/issues",
"source": "https://github.com/myclabs/DeepCopy/tree/1.11.0"
"source": "https://github.com/myclabs/DeepCopy/tree/1.11.1"
},
"funding": [
{
@@ -134,20 +134,20 @@
"type": "tidelift"
}
],
"time": "2022-03-03T13:19:32+00:00"
"time": "2023-03-08T13:26:56+00:00"
},
{
"name": "nikic/php-parser",
"version": "v4.13.2",
"version": "v4.16.0",
"source": {
"type": "git",
"url": "https://github.com/nikic/PHP-Parser.git",
"reference": "210577fe3cf7badcc5814d99455df46564f3c077"
"reference": "19526a33fb561ef417e822e85f08a00db4059c17"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/210577fe3cf7badcc5814d99455df46564f3c077",
"reference": "210577fe3cf7badcc5814d99455df46564f3c077",
"url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/19526a33fb561ef417e822e85f08a00db4059c17",
"reference": "19526a33fb561ef417e822e85f08a00db4059c17",
"shasum": ""
},
"require": {
@@ -188,9 +188,9 @@
],
"support": {
"issues": "https://github.com/nikic/PHP-Parser/issues",
"source": "https://github.com/nikic/PHP-Parser/tree/v4.13.2"
"source": "https://github.com/nikic/PHP-Parser/tree/v4.16.0"
},
"time": "2021-11-30T19:35:32+00:00"
"time": "2023-06-25T14:52:30+00:00"
},
{
"name": "phar-io/manifest",
@@ -303,252 +303,25 @@
},
"time": "2022-02-21T01:04:05+00:00"
},
{
"name": "phpdocumentor/reflection-common",
"version": "2.2.0",
"source": {
"type": "git",
"url": "https://github.com/phpDocumentor/ReflectionCommon.git",
"reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b",
"reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b",
"shasum": ""
},
"require": {
"php": "^7.2 || ^8.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-2.x": "2.x-dev"
}
},
"autoload": {
"psr-4": {
"phpDocumentor\\Reflection\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Jaap van Otterdijk",
"email": "opensource@ijaap.nl"
}
],
"description": "Common reflection classes used by phpdocumentor to reflect the code structure",
"homepage": "http://www.phpdoc.org",
"keywords": [
"FQSEN",
"phpDocumentor",
"phpdoc",
"reflection",
"static analysis"
],
"support": {
"issues": "https://github.com/phpDocumentor/ReflectionCommon/issues",
"source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x"
},
"time": "2020-06-27T09:03:43+00:00"
},
{
"name": "phpdocumentor/reflection-docblock",
"version": "5.3.0",
"source": {
"type": "git",
"url": "https://github.com/phpDocumentor/ReflectionDocBlock.git",
"reference": "622548b623e81ca6d78b721c5e029f4ce664f170"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/622548b623e81ca6d78b721c5e029f4ce664f170",
"reference": "622548b623e81ca6d78b721c5e029f4ce664f170",
"shasum": ""
},
"require": {
"ext-filter": "*",
"php": "^7.2 || ^8.0",
"phpdocumentor/reflection-common": "^2.2",
"phpdocumentor/type-resolver": "^1.3",
"webmozart/assert": "^1.9.1"
},
"require-dev": {
"mockery/mockery": "~1.3.2",
"psalm/phar": "^4.8"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "5.x-dev"
}
},
"autoload": {
"psr-4": {
"phpDocumentor\\Reflection\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Mike van Riel",
"email": "me@mikevanriel.com"
},
{
"name": "Jaap van Otterdijk",
"email": "account@ijaap.nl"
}
],
"description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.",
"support": {
"issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues",
"source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.3.0"
},
"time": "2021-10-19T17:43:47+00:00"
},
{
"name": "phpdocumentor/type-resolver",
"version": "1.6.1",
"source": {
"type": "git",
"url": "https://github.com/phpDocumentor/TypeResolver.git",
"reference": "77a32518733312af16a44300404e945338981de3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/77a32518733312af16a44300404e945338981de3",
"reference": "77a32518733312af16a44300404e945338981de3",
"shasum": ""
},
"require": {
"php": "^7.2 || ^8.0",
"phpdocumentor/reflection-common": "^2.0"
},
"require-dev": {
"ext-tokenizer": "*",
"psalm/phar": "^4.8"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-1.x": "1.x-dev"
}
},
"autoload": {
"psr-4": {
"phpDocumentor\\Reflection\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Mike van Riel",
"email": "me@mikevanriel.com"
}
],
"description": "A PSR-5 based resolver of Class names, Types and Structural Element Names",
"support": {
"issues": "https://github.com/phpDocumentor/TypeResolver/issues",
"source": "https://github.com/phpDocumentor/TypeResolver/tree/1.6.1"
},
"time": "2022-03-15T21:29:03+00:00"
},
{
"name": "phpspec/prophecy",
"version": "v1.15.0",
"source": {
"type": "git",
"url": "https://github.com/phpspec/prophecy.git",
"reference": "bbcd7380b0ebf3961ee21409db7b38bc31d69a13"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpspec/prophecy/zipball/bbcd7380b0ebf3961ee21409db7b38bc31d69a13",
"reference": "bbcd7380b0ebf3961ee21409db7b38bc31d69a13",
"shasum": ""
},
"require": {
"doctrine/instantiator": "^1.2",
"php": "^7.2 || ~8.0, <8.2",
"phpdocumentor/reflection-docblock": "^5.2",
"sebastian/comparator": "^3.0 || ^4.0",
"sebastian/recursion-context": "^3.0 || ^4.0"
},
"require-dev": {
"phpspec/phpspec": "^6.0 || ^7.0",
"phpunit/phpunit": "^8.0 || ^9.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.x-dev"
}
},
"autoload": {
"psr-4": {
"Prophecy\\": "src/Prophecy"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Konstantin Kudryashov",
"email": "ever.zet@gmail.com",
"homepage": "http://everzet.com"
},
{
"name": "Marcello Duarte",
"email": "marcello.duarte@gmail.com"
}
],
"description": "Highly opinionated mocking framework for PHP 5.3+",
"homepage": "https://github.com/phpspec/prophecy",
"keywords": [
"Double",
"Dummy",
"fake",
"mock",
"spy",
"stub"
],
"support": {
"issues": "https://github.com/phpspec/prophecy/issues",
"source": "https://github.com/phpspec/prophecy/tree/v1.15.0"
},
"time": "2021-12-08T12:19:24+00:00"
},
{
"name": "phpunit/php-code-coverage",
"version": "9.2.15",
"version": "9.2.26",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-code-coverage.git",
"reference": "2e9da11878c4202f97915c1cb4bb1ca318a63f5f"
"reference": "443bc6912c9bd5b409254a40f4b0f4ced7c80ea1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2e9da11878c4202f97915c1cb4bb1ca318a63f5f",
"reference": "2e9da11878c4202f97915c1cb4bb1ca318a63f5f",
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/443bc6912c9bd5b409254a40f4b0f4ced7c80ea1",
"reference": "443bc6912c9bd5b409254a40f4b0f4ced7c80ea1",
"shasum": ""
},
"require": {
"ext-dom": "*",
"ext-libxml": "*",
"ext-xmlwriter": "*",
"nikic/php-parser": "^4.13.0",
"nikic/php-parser": "^4.15",
"php": ">=7.3",
"phpunit/php-file-iterator": "^3.0.3",
"phpunit/php-text-template": "^2.0.2",
@@ -563,8 +336,8 @@
"phpunit/phpunit": "^9.3"
},
"suggest": {
"ext-pcov": "*",
"ext-xdebug": "*"
"ext-pcov": "PHP extension that provides line coverage",
"ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage"
},
"type": "library",
"extra": {
@@ -597,7 +370,7 @@
],
"support": {
"issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
"source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.15"
"source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.26"
},
"funding": [
{
@@ -605,7 +378,7 @@
"type": "github"
}
],
"time": "2022-03-07T09:28:20+00:00"
"time": "2023-03-06T12:58:08+00:00"
},
{
"name": "phpunit/php-file-iterator",
@@ -850,20 +623,20 @@
},
{
"name": "phpunit/phpunit",
"version": "9.5.20",
"version": "9.6.9",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
"reference": "12bc8879fb65aef2138b26fc633cb1e3620cffba"
"reference": "a9aceaf20a682aeacf28d582654a1670d8826778"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/12bc8879fb65aef2138b26fc633cb1e3620cffba",
"reference": "12bc8879fb65aef2138b26fc633cb1e3620cffba",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a9aceaf20a682aeacf28d582654a1670d8826778",
"reference": "a9aceaf20a682aeacf28d582654a1670d8826778",
"shasum": ""
},
"require": {
"doctrine/instantiator": "^1.3.1",
"doctrine/instantiator": "^1.3.1 || ^2",
"ext-dom": "*",
"ext-json": "*",
"ext-libxml": "*",
@@ -874,7 +647,6 @@
"phar-io/manifest": "^2.0.3",
"phar-io/version": "^3.0.2",
"php": ">=7.3",
"phpspec/prophecy": "^1.12.1",
"phpunit/php-code-coverage": "^9.2.13",
"phpunit/php-file-iterator": "^3.0.5",
"phpunit/php-invoker": "^3.1.1",
@@ -882,23 +654,19 @@
"phpunit/php-timer": "^5.0.2",
"sebastian/cli-parser": "^1.0.1",
"sebastian/code-unit": "^1.0.6",
"sebastian/comparator": "^4.0.5",
"sebastian/comparator": "^4.0.8",
"sebastian/diff": "^4.0.3",
"sebastian/environment": "^5.1.3",
"sebastian/exporter": "^4.0.3",
"sebastian/exporter": "^4.0.5",
"sebastian/global-state": "^5.0.1",
"sebastian/object-enumerator": "^4.0.3",
"sebastian/resource-operations": "^3.0.3",
"sebastian/type": "^3.0",
"sebastian/type": "^3.2",
"sebastian/version": "^3.0.2"
},
"require-dev": {
"ext-pdo": "*",
"phpspec/prophecy-phpunit": "^2.0.1"
},
"suggest": {
"ext-soap": "*",
"ext-xdebug": "*"
"ext-soap": "To be able to generate mocks based on WSDL files",
"ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage"
},
"bin": [
"phpunit"
@@ -906,7 +674,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "9.5-dev"
"dev-master": "9.6-dev"
}
},
"autoload": {
@@ -937,7 +705,8 @@
],
"support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
"source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.20"
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
"source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.9"
},
"funding": [
{
@@ -947,9 +716,13 @@
{
"url": "https://github.com/sebastianbergmann",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit",
"type": "tidelift"
}
],
"time": "2022-04-01T12:37:26+00:00"
"time": "2023-06-11T06:13:56+00:00"
},
{
"name": "sebastian/cli-parser",
@@ -1120,16 +893,16 @@
},
{
"name": "sebastian/comparator",
"version": "4.0.6",
"version": "4.0.8",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/comparator.git",
"reference": "55f4261989e546dc112258c7a75935a81a7ce382"
"reference": "fa0f136dd2334583309d32b62544682ee972b51a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/55f4261989e546dc112258c7a75935a81a7ce382",
"reference": "55f4261989e546dc112258c7a75935a81a7ce382",
"url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/fa0f136dd2334583309d32b62544682ee972b51a",
"reference": "fa0f136dd2334583309d32b62544682ee972b51a",
"shasum": ""
},
"require": {
@@ -1182,7 +955,7 @@
],
"support": {
"issues": "https://github.com/sebastianbergmann/comparator/issues",
"source": "https://github.com/sebastianbergmann/comparator/tree/4.0.6"
"source": "https://github.com/sebastianbergmann/comparator/tree/4.0.8"
},
"funding": [
{
@@ -1190,7 +963,7 @@
"type": "github"
}
],
"time": "2020-10-26T15:49:45+00:00"
"time": "2022-09-14T12:41:17+00:00"
},
{
"name": "sebastian/complexity",
@@ -1251,16 +1024,16 @@
},
{
"name": "sebastian/diff",
"version": "4.0.4",
"version": "4.0.5",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/diff.git",
"reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d"
"reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/3461e3fccc7cfdfc2720be910d3bd73c69be590d",
"reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d",
"url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/74be17022044ebaaecfdf0c5cd504fc9cd5a7131",
"reference": "74be17022044ebaaecfdf0c5cd504fc9cd5a7131",
"shasum": ""
},
"require": {
@@ -1305,7 +1078,7 @@
],
"support": {
"issues": "https://github.com/sebastianbergmann/diff/issues",
"source": "https://github.com/sebastianbergmann/diff/tree/4.0.4"
"source": "https://github.com/sebastianbergmann/diff/tree/4.0.5"
},
"funding": [
{
@@ -1313,20 +1086,20 @@
"type": "github"
}
],
"time": "2020-10-26T13:10:38+00:00"
"time": "2023-05-07T05:35:17+00:00"
},
{
"name": "sebastian/environment",
"version": "5.1.4",
"version": "5.1.5",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/environment.git",
"reference": "1b5dff7bb151a4db11d49d90e5408e4e938270f7"
"reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/1b5dff7bb151a4db11d49d90e5408e4e938270f7",
"reference": "1b5dff7bb151a4db11d49d90e5408e4e938270f7",
"url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed",
"reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed",
"shasum": ""
},
"require": {
@@ -1368,7 +1141,7 @@
],
"support": {
"issues": "https://github.com/sebastianbergmann/environment/issues",
"source": "https://github.com/sebastianbergmann/environment/tree/5.1.4"
"source": "https://github.com/sebastianbergmann/environment/tree/5.1.5"
},
"funding": [
{
@@ -1376,20 +1149,20 @@
"type": "github"
}
],
"time": "2022-04-03T09:37:03+00:00"
"time": "2023-02-03T06:03:51+00:00"
},
{
"name": "sebastian/exporter",
"version": "4.0.4",
"version": "4.0.5",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/exporter.git",
"reference": "65e8b7db476c5dd267e65eea9cab77584d3cfff9"
"reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/65e8b7db476c5dd267e65eea9cab77584d3cfff9",
"reference": "65e8b7db476c5dd267e65eea9cab77584d3cfff9",
"url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d",
"reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d",
"shasum": ""
},
"require": {
@@ -1445,7 +1218,7 @@
],
"support": {
"issues": "https://github.com/sebastianbergmann/exporter/issues",
"source": "https://github.com/sebastianbergmann/exporter/tree/4.0.4"
"source": "https://github.com/sebastianbergmann/exporter/tree/4.0.5"
},
"funding": [
{
@@ -1453,7 +1226,7 @@
"type": "github"
}
],
"time": "2021-11-11T14:18:36+00:00"
"time": "2022-09-14T06:03:37+00:00"
},
{
"name": "sebastian/global-state",
@@ -1690,16 +1463,16 @@
},
{
"name": "sebastian/recursion-context",
"version": "4.0.4",
"version": "4.0.5",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/recursion-context.git",
"reference": "cd9d8cf3c5804de4341c283ed787f099f5506172"
"reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/cd9d8cf3c5804de4341c283ed787f099f5506172",
"reference": "cd9d8cf3c5804de4341c283ed787f099f5506172",
"url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1",
"reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1",
"shasum": ""
},
"require": {
@@ -1738,10 +1511,10 @@
}
],
"description": "Provides functionality to recursively process PHP variables",
"homepage": "http://www.github.com/sebastianbergmann/recursion-context",
"homepage": "https://github.com/sebastianbergmann/recursion-context",
"support": {
"issues": "https://github.com/sebastianbergmann/recursion-context/issues",
"source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.4"
"source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.5"
},
"funding": [
{
@@ -1749,7 +1522,7 @@
"type": "github"
}
],
"time": "2020-10-26T13:17:30+00:00"
"time": "2023-02-03T06:07:39+00:00"
},
{
"name": "sebastian/resource-operations",
@@ -1808,16 +1581,16 @@
},
{
"name": "sebastian/type",
"version": "3.0.0",
"version": "3.2.1",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/type.git",
"reference": "b233b84bc4465aff7b57cf1c4bc75c86d00d6dad"
"reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/type/zipball/b233b84bc4465aff7b57cf1c4bc75c86d00d6dad",
"reference": "b233b84bc4465aff7b57cf1c4bc75c86d00d6dad",
"url": "https://api.github.com/repos/sebastianbergmann/type/zipball/75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7",
"reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7",
"shasum": ""
},
"require": {
@@ -1829,7 +1602,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.0-dev"
"dev-master": "3.2-dev"
}
},
"autoload": {
@@ -1852,7 +1625,7 @@
"homepage": "https://github.com/sebastianbergmann/type",
"support": {
"issues": "https://github.com/sebastianbergmann/type/issues",
"source": "https://github.com/sebastianbergmann/type/tree/3.0.0"
"source": "https://github.com/sebastianbergmann/type/tree/3.2.1"
},
"funding": [
{
@@ -1860,7 +1633,7 @@
"type": "github"
}
],
"time": "2022-03-15T09:54:48+00:00"
"time": "2023-02-03T06:13:03+00:00"
},
{
"name": "sebastian/version",
@@ -1917,16 +1690,16 @@
},
{
"name": "squizlabs/php_codesniffer",
"version": "3.6.2",
"version": "3.7.2",
"source": {
"type": "git",
"url": "https://github.com/squizlabs/PHP_CodeSniffer.git",
"reference": "5e4e71592f69da17871dba6e80dd51bce74a351a"
"reference": "ed8e00df0a83aa96acf703f8c2979ff33341f879"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/5e4e71592f69da17871dba6e80dd51bce74a351a",
"reference": "5e4e71592f69da17871dba6e80dd51bce74a351a",
"url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/ed8e00df0a83aa96acf703f8c2979ff33341f879",
"reference": "ed8e00df0a83aa96acf703f8c2979ff33341f879",
"shasum": ""
},
"require": {
@@ -1962,96 +1735,15 @@
"homepage": "https://github.com/squizlabs/PHP_CodeSniffer",
"keywords": [
"phpcs",
"standards"
"standards",
"static analysis"
],
"support": {
"issues": "https://github.com/squizlabs/PHP_CodeSniffer/issues",
"source": "https://github.com/squizlabs/PHP_CodeSniffer",
"wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki"
},
"time": "2021-12-12T21:44:58+00:00"
},
{
"name": "symfony/polyfill-ctype",
"version": "v1.25.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
"reference": "30885182c981ab175d4d034db0f6f469898070ab"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/30885182c981ab175d4d034db0f6f469898070ab",
"reference": "30885182c981ab175d4d034db0f6f469898070ab",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"provide": {
"ext-ctype": "*"
},
"suggest": {
"ext-ctype": "For best performance"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.23-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Ctype\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Gert de Pagter",
"email": "BackEndTea@gmail.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for ctype functions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"ctype",
"polyfill",
"portable"
],
"support": {
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.25.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2021-10-20T20:35:02+00:00"
"time": "2023-02-22T23:07:41+00:00"
},
{
"name": "theseer/tokenizer",
@@ -2102,64 +1794,6 @@
}
],
"time": "2021-07-28T10:34:58+00:00"
},
{
"name": "webmozart/assert",
"version": "1.10.0",
"source": {
"type": "git",
"url": "https://github.com/webmozarts/assert.git",
"reference": "6964c76c7804814a842473e0c8fd15bab0f18e25"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/webmozarts/assert/zipball/6964c76c7804814a842473e0c8fd15bab0f18e25",
"reference": "6964c76c7804814a842473e0c8fd15bab0f18e25",
"shasum": ""
},
"require": {
"php": "^7.2 || ^8.0",
"symfony/polyfill-ctype": "^1.8"
},
"conflict": {
"phpstan/phpstan": "<0.12.20",
"vimeo/psalm": "<4.6.1 || 4.6.2"
},
"require-dev": {
"phpunit/phpunit": "^8.5.13"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.10-dev"
}
},
"autoload": {
"psr-4": {
"Webmozart\\Assert\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Bernhard Schussek",
"email": "bschussek@gmail.com"
}
],
"description": "Assertions to validate method input/output with nice error messages.",
"keywords": [
"assert",
"check",
"validate"
],
"support": {
"issues": "https://github.com/webmozarts/assert/issues",
"source": "https://github.com/webmozarts/assert/tree/1.10.0"
},
"time": "2021-03-09T10:59:23+00:00"
}
],
"aliases": [],
@@ -2174,8 +1808,9 @@
"ext-openssl": "*",
"ext-libxml": "*",
"ext-simplexml": "*",
"ext-json": "*"
"ext-json": "*",
"ext-intl": "*"
},
"platform-dev": [],
"plugin-api-version": "2.3.0"
"plugin-api-version": "2.0.0"
}

View File

@@ -6,6 +6,26 @@
[system]
; Only these bridges are available for feed production
; How to enable all bridges: enabled_bridges[] = *
enabled_bridges[] = FeedMerge
enabled_bridges[] = FeedReducerBridge
enabled_bridges[] = Filter
enabled_bridges[] = GettrBridge
enabled_bridges[] = MastodonBridge
enabled_bridges[] = Reddit
enabled_bridges[] = RumbleBridge
enabled_bridges[] = SoundcloudBridge
enabled_bridges[] = Telegram
enabled_bridges[] = ThePirateBay
enabled_bridges[] = TikTokBridge
enabled_bridges[] = Twitch
enabled_bridges[] = Twitter
enabled_bridges[] = Vk
enabled_bridges[] = XPathBridge
enabled_bridges[] = Youtube
enabled_bridges[] = YouTubeCommunityTabBridge
; Defines the timezone used by RSS-Bridge
; Find a list of supported timezones at
; https://www.php.net/manual/en/timezones.php
@@ -15,6 +35,16 @@ timezone = "UTC"
; Display a system message to users.
message = ""
; Whether to enable debug mode.
enable_debug_mode = false
; Enable debug mode only for these permitted ip addresses
; debug_mode_whitelist[] = 127.0.0.1
; debug_mode_whitelist[] = 192.168.1.10
; Whether to enable maintenance mode. If enabled, feed requests receive 503 Service Unavailable
enable_maintenance_mode = false
[http]
timeout = 60
useragent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:102.0) Gecko/20100101 Firefox/102.0"
@@ -103,7 +133,12 @@ path = ""
enable_purge = true
[SQLiteCache]
; Filepath of the sqlite db file
file = "cache.sqlite"
; Whether to actually delete data when purging
enable_purge = true
; Busy wait in ms before timing out
timeout = 5000
[MemcachedCache]
host = "localhost"

View File

@@ -2,8 +2,8 @@ server {
listen 80 default_server;
listen [::]:80 default_server;
root /app;
access_log /var/log/nginx/rssbridge.access.log;
error_log /var/log/nginx/rssbridge.error.log;
access_log /dev/stdout;
error_log /dev/stderr;
index index.php;
location ~ /(\.|vendor|tests) {

View File

@@ -19,6 +19,7 @@
| ![](https://iplookup.flagfox.net/images/h16/DE.png) | https://rss-bridge.mediani.de | ![](https://img.shields.io/website/https/rss-bridge.mediani.de.svg) | [@sokai](https://github.com/sokai) | Hosted with Netcup, Germany |
| ![](https://iplookup.flagfox.net/images/h16/US.png) | http://rb.vern.cc/ | ![](https://img.shields.io/website/https/rb.vern.cc.svg) | [@vern.cc](https://vern.cc/en/admin) | Hosted with Hetzner, US |
| ![](https://iplookup.flagfox.net/images/h16/FR.png) | https://rssbridge.flossboxin.org.in/ | ![](https://img.shields.io/badge/website-up-brightgreen) | [@vdbhb59](https://github.com/vdbhb59) | Hosted with OVH SAS (Maintained in India)
| ![](https://iplookup.flagfox.net/images/h16/PL.png) | https://rss.foxhaven.cyou| ![](https://img.shields.io/badge/website-up-brightgreen) | [@Aysilu](https://foxhaven.cyou) | Hosted with Timeweb (Maintained in Poland)
## Inactive instances

View File

@@ -45,5 +45,5 @@ services:
If you want to add a bridge that is not part of [`/bridges`](https://github.com/RSS-Bridge/rss-bridge/tree/master/bridges), you can map a folder to the `/config` folder of the `rss-bridge` container.
1. Create a folder in the location of your docker-compose.yml or your general docker working area (in this example it will be `/home/docker/rssbridge/config` ).
2. Copy your [custom bridges](../05_Bridge_API/01_How_to_create_a_new_bridge.md) to the `/home/docker/rssbridge/config` folder. You can also add your custom [whitelist.txt](../03_For_Hosts/05_Whitelisting.md) file and your custom [config.ini.php](../03_For_Hosts/08_Custom_Configuration.md) to this folder.
2. Copy your [custom bridges](../05_Bridge_API/01_How_to_create_a_new_bridge.md) to the `/home/docker/rssbridge/config` folder. Applies also to [config.ini.php](../03_For_Hosts/08_Custom_Configuration.md).
3. Map the folder to `/config` inside the container. To do that, replace the `</local/custom/path>` from the previous examples with `/home/docker/rssbridge/config`

View File

@@ -14,7 +14,7 @@ You can simply press the button below to easily deploy RSS Bridge on Heroku and
![image](../images/fork_button.png)
2. To customise what bridges can be used if need, create a `whitelist.txt` file in your fork and follow the instructions given [here](../03_For_Hosts/05_Whitelisting.md). You dont need to do this if youre fine with the default bridges.
2. To customise what bridges can be used if need, see [here](../03_For_Hosts/05_Whitelisting.md). You dont need to do this if youre fine with the default bridges.
3. [Log in to Heroku](https://dashboard.heroku.com) and create a new app. The app name will be the URL of the RSS Bridge (appname.herokuapp.com)

View File

@@ -1,38 +1,26 @@
RSS-Bridge supports whitelists in order to limit the available bridges on your web server.
Modify `config.ini.php` to limit available bridges.
A default whitelist file (`whitelist.default.txt`) is shipped with RSS-Bridge. Please do not edit this file, as it gets replaced when upgrading RSS-Bridge!
## Enable all bridges
You should, however, use this file as template to create your own whitelist (or leave it as is, to keep the default bridges). In order to create your own whitelist perform following actions:
* Copy the file `whitelist.default.txt` in the RSS-Bridge root folder
* Rename the new file to `whitelist.txt`
* Change the lines to satisfy your requirements
RSS-Bridge will automatically detect the `whitelist.txt` and use it. If the file doesn't exist it will default to `whitelist.default.txt` automatically.
# Specific whitelisting
In order to specifically whitelist bridges, open `whitelist.txt` and add one line for each bridge you want to show. Make sure you use normal [line-feeds](https://en.wikipedia.org/wiki/Newline "Line-feed") at the end of a line (LF not [CRLF](https://en.wikipedia.org/wiki/Carriage_return "Carriage-return line-feed")). The bridge name must match the filename of the bridge in the bridges folder (see [folder structure](../04_For_Developers/03_Folder_structure.md)). The name may or may not include the 'Bridge' part.
**Examples**:
```TEXT
FacebookBridge
WikipediaBridge
TwitterBridge
```
enabled_bridges[] = *
```
or
## Enable some bridges
```TEXT
Facebook
Wikipedia
Twitter
```
enabled_bridges[] = TwitchBridge
enabled_bridges[] = GettrBridge
```
# Global whitelisting
## Enable all bridges (legacy shortcut)
In order to globally whitelist all bridges, open the `whitelist.txt` file, remove all contents and just write an asterisk `*` into the file (only this one character).
```
echo '*' > whitelist.txt
```
```TEXT
*
```
## Enable some bridges (legacy shortcut)
```
echo -e "TwitchBridge\nTwitterBridge" > whitelist.txt
```

View File

@@ -5,23 +5,29 @@ Enabling debug mode on a public server may result in malicious clients retrievin
***
Debug mode enables error reporting and prevents loading data from the cache (data is still written to the cache).
To enable debug mode, create a file named 'DEBUG' in the root directory of RSS-Bridge (next to `index.php`). For further security, insert your IP address in the file. You can add multiple addresses, one per line.
To enable debug mode, set in `config.ini.php`:
enable_debug_mode = true
Allow only explicit ip addresses:
debug_mode_whitelist[] = 127.0.0.1
debug_mode_whitelist[] = 192.168.1.10
_Notice_:
* An empty file enables debug mode for anyone!
* The bridge whitelist still applies! (debug mode does **not** enable all bridges)
RSS-Bridge will give you a visual feedback when debug mode is enabled:
![twitter bridge](../images/debug_mode.png)
RSS-Bridge will give you a visual feedback when debug mode is enabled.
While debug mode is active, RSS-Bridge will write additional data to your servers `error.log`.
Debug mode is controlled by the static class `Debug`. It provides three core functions:
`Debug::isEnabled()`: Returns `true` if debug mode is enabled.
`Debug::isSecure()`: Returns `true` if your client is on the debug whitelist.
`Debug::log($message)`: Adds a message to `error.log`. It takes one parameter, which can be anything. For example: `Debug::log('Hello World!');`
* `Debug::isEnabled()`: Returns `true` if debug mode is enabled.
* `Debug::log($message)`: Adds a message to `error.log`. It takes one parameter, which can be anything.
Example: `Debug::log('Hello World!');`
**Notice**: `Debug::log($message)` calls `Debug::isEnabled()` internally. You don't have to do that manually.

View File

@@ -489,11 +489,11 @@ public function collectData()
Within the context of the current bridge, loads a value by key from cache. Optionally specifies the cache duration for the key. Returns `null` if the key doesn't exist or the value is expired.
```php
protected function loadCacheValue($key, $duration = 86400)
protected function loadCacheValue($key, $duration = null)
```
- `$key` - the name under which the value is stored in the cache.
- `$duration` - the maximum time in seconds after which the value expires. The default duration is 86400 (24 hours).
- `$duration` - the maximum time in seconds after which the value expires.
Usage example:

View File

@@ -53,7 +53,7 @@ $html = getContents($url, $header, $opts);
```
# getSimpleHTMLDOM
The `getSimpleHTMLDOM` function is a wrapper for the [simple_html_dom](http://simplehtmldom.sourceforge.net/) [file_get_html](http://simplehtmldom.sourceforge.net/manual_api.htm#api) function in order to provide context by design.
The `getSimpleHTMLDOM` function is a wrapper for the [simple_html_dom](https://simplehtmldom.sourceforge.io/) [file_get_html](https://simplehtmldom.sourceforge.io/docs/1.9/api/file_get_html/) function in order to provide context by design.
```PHP
$html = getSimpleHTMLDOM('your URI');
@@ -194,8 +194,20 @@ $cleaned = stripRecursiveHTMLSection($string, $tag_name, $tag_start);
# markdownToHtml
Converts markdown input to HTML using [Parsedown](https://parsedown.org/).
| Parameter | Type | Optional | Description
| --------- | ------ | ---------- | ----------
| `string` | string | *required* | The URL of the contents to acquire
| `config` | array | *optional* | An array of Parsedown options in the format `['breaksEnabled' => true]`
Valid options:
| Option | Default | Description
| --------------- | ------- | -----------
| `breaksEnabled` | `false` | Enable automatic line breaks
| `markupEscaped` | `false` | Escape inline markup (HTML)
| `urlsLinked` | `true` | Automatically convert URLs to links
```php
function markdownToHtml(string $string) : string
function markdownToHtml(string $string, array $config = []) : string
```
**Example**

View File

@@ -1,24 +1,3 @@
Create a new file in the `caches/` folder (see [Folder structure](../04_For_Developers/03_Folder_structure.md)).
The file must be named according to following specification:
* It starts with the type
* The file name must end with 'Cache'
* The file type must be PHP, written in small letters (seriously!) ".php"
**Examples:**
Type | Filename
-----|---------
File | FileCache.php
MySQL | MySQLCache.php
The file must start with the PHP tags and end with an empty line. The closing tag `?>` is [omitted](http://php.net/basic-syntax.instruction-separation).
Example:
```PHP
<?PHP
// PHP code here
// This line is empty (just imagine it!)
```
See `NullCache` and `SQLiteCache` for examples.

View File

@@ -1,73 +1,18 @@
The `CacheInterface` interface defines functions that need to be implemented. To create a new cache that implements `CacheInterface` you must implement following functions:
See `CacheInterface`.
* [loadData](#the-loaddata-function)
* [saveData](#the-savedata-function)
* [getTime](#the-gettime-function)
* [purgeCache](#the-purgecache-function)
```php
interface CacheInterface
{
public function setScope(string $scope): void;
Find a [template](#template) at the end of this file.
public function setKey(array $key): void;
# Functions
public function loadData();
## The `loadData` function
public function saveData($data): void;
This function loads data from the cache and returns the data in the same format provided to the [saveData](#the-savedata-function) function.
public function getTime(): ?int;
```PHP
loadData(): mixed
```
## The `saveData` function
This function stores the given data into the cache and returns the object instance.
```PHP
saveData(mixed $data): self
```
## The `getTime` function
This function returns the last write time for the cache, or `false` if the cache does not yet exist. Please notice that 'cache' refers to one specific item in the cache repository and might require additional data to identify a specific item (introduce custom functions where necessary!).
```PHP
getTime(): int, false
```
## The `purgeCache` function
This function removes any data from the cache that is not within the given duration. The duration is specified in seconds and defines the period between now and the oldest item to keep.
```PHP
purgeCache(int $duration): null
```
# Template
This is the bare minimum template for a new cache:
```PHP
<?php
class MyTypeCache implements CacheInterface {
public function loadData(){
// Implement your algorithm here!
return null;
}
public function saveData($data){
// Implement your algorithm here!
return $this;
}
public function getTime(){
// Implement your algorithm here!
return false;
}
public function purgeCache($duration){
// Implement your algorithm here!
}
public function purgeCache(int $seconds): void;
}
// Imaginary empty line!
```

View File

@@ -1,13 +0,0 @@
The `FormatAbstract` class implements the [`FormatInterface`](../08_Format_API/02_FormatInterface.md) interface with basic functional behavior and adds common helper functions for new formats:
* [sanitizeHtml](#the-sanitizehtml-function)
# Functions
## The `sanitizeHtml` function
The `sanitizeHtml` function receives an HTML formatted string and returns the string with disabled `<script>`, `<iframe>` and `<link>` tags.
```PHP
sanitize_html(string $html): string
```

View File

@@ -1,3 +1,9 @@
A _Format_ is an class that allows **RSS-Bridge** to turn items from a bridge into an RSS-feed format. It is developed in a PHP file located in the `formats/` folder (see [Folder structure](../04_For_Developers/03_Folder_structure.md)) and either implements the [FormatInterface](../08_Format_API/02_FormatInterface.md) interface or extends the [FormatAbstract](../08_Format_API/03_FormatAbstract.md) class.
A Format is a class that allows RSS-Bridge to turn items from a bridge into an RSS-feed format.
It is developed in a PHP file located in the `formats/` folder
[Folder structure](../04_For_Developers/03_Folder_structure.md)
and either implements the
[FormatInterface](../08_Format_API/02_FormatInterface.md)
interface or extends the FormatAbstract class.
For more information about how to create a new _Format_, read [How to create a new Format?](./01_How_to_create_a_new_format.md)
For more information about how to create a new _Format_, read
[How to create a new Format?](./01_How_to_create_a_new_format.md)

View File

@@ -12,7 +12,7 @@ Configuration
- I will not detail exactly how to do this, as the specific process will likely change over time. You should easily be able to find guides using your search engine of choice.
- A basic free developer account grants Essential access to the Twitter API v2, which should be sufficient for this bridge.
- Note: as of April 2023, the "Free" access level no longer allows read access. The cheapest access level with read access is called "Basic".
2. Create a Twitter Project and App, get Bearer Token
@@ -34,4 +34,4 @@ Configuration
[TwitterV2Bridge]
twitterv2apitoken = %Bearer Token from step 2%
```
- If you don't have a **config.ini.php**, create one by making a copy of **config.default.ini.php**
- If you don't have a **config.ini.php**, create one by making a copy of **config.default.ini.php**

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -158,7 +158,7 @@ class AtomFormat extends FormatAbstract
$content = $document->createElement('content');
$content->setAttribute('type', 'html');
$content->appendChild($document->createTextNode(sanitize_html($entryContent)));
$content->appendChild($document->createTextNode(break_annoying_html_tags($entryContent)));
$entry->appendChild($content);
foreach ($item->getEnclosures() as $enclosure) {

View File

@@ -47,7 +47,7 @@ class JsonFormat extends FormatAbstract
$entryTitle = $item->getTitle();
$entryUri = $item->getURI();
$entryTimestamp = $item->getTimestamp();
$entryContent = $item->getContent() ? sanitize_html($item->getContent()) : '';
$entryContent = $item->getContent() ? break_annoying_html_tags($item->getContent()) : '';
$entryEnclosures = $item->getEnclosures();
$entryCategories = $item->getCategories();

View File

@@ -103,7 +103,7 @@ class MrssFormat extends FormatAbstract
$itemTimestamp = $item->getTimestamp();
$itemTitle = $item->getTitle();
$itemUri = $item->getURI();
$itemContent = $item->getContent() ? sanitize_html($item->getContent()) : '';
$itemContent = $item->getContent() ? break_annoying_html_tags($item->getContent()) : '';
$entryID = $item->getUid();
$isPermaLink = 'false';

Some files were not shown because too many files have changed in this diff Show More