2019-11-01 15:54:03 +03:00
|
|
|
<?php
|
|
|
|
|
2023-12-19 09:53:25 +03:00
|
|
|
/**
|
|
|
|
* This bridge does NOT use reddit's official rss feeds.
|
|
|
|
*
|
|
|
|
* This bridge uses reddit's json api: https://old.reddit.com/search.json?q=
|
|
|
|
*/
|
2022-07-01 16:10:30 +03:00
|
|
|
class RedditBridge extends BridgeAbstract
|
|
|
|
{
|
|
|
|
const MAINTAINER = 'dawidsowa';
|
|
|
|
const NAME = 'Reddit Bridge';
|
2023-12-19 09:53:25 +03:00
|
|
|
const URI = 'https://old.reddit.com';
|
2022-07-01 16:10:30 +03:00
|
|
|
const DESCRIPTION = 'Return hot submissions from Reddit';
|
|
|
|
|
|
|
|
const PARAMETERS = [
|
|
|
|
'global' => [
|
|
|
|
'score' => [
|
|
|
|
'name' => 'Minimal score',
|
|
|
|
'required' => false,
|
|
|
|
'type' => 'number',
|
|
|
|
'exampleValue' => 100,
|
|
|
|
'title' => 'Filter out posts with lower score'
|
|
|
|
],
|
|
|
|
'd' => [
|
|
|
|
'name' => 'Sort By',
|
|
|
|
'type' => 'list',
|
|
|
|
'title' => 'Sort by new, hot, top or relevancy',
|
|
|
|
'values' => [
|
|
|
|
'Hot' => 'hot',
|
|
|
|
'Relevance' => 'relevance',
|
|
|
|
'New' => 'new',
|
|
|
|
'Top' => 'top'
|
|
|
|
],
|
|
|
|
'defaultValue' => 'Hot'
|
|
|
|
],
|
|
|
|
'search' => [
|
|
|
|
'name' => 'Keyword search',
|
|
|
|
'required' => false,
|
|
|
|
'exampleValue' => 'cats, dogs',
|
|
|
|
'title' => 'Keyword search, separated by commas'
|
|
|
|
]
|
|
|
|
],
|
|
|
|
'single' => [
|
|
|
|
'r' => [
|
|
|
|
'name' => 'SubReddit',
|
|
|
|
'required' => true,
|
|
|
|
'exampleValue' => 'selfhosted',
|
|
|
|
'title' => 'SubReddit name'
|
2022-10-02 08:34:20 +03:00
|
|
|
],
|
|
|
|
'f' => [
|
|
|
|
'name' => 'Flair',
|
|
|
|
'required' => false,
|
|
|
|
'exampleValue' => 'Proxy',
|
|
|
|
'title' => 'Flair filter'
|
2022-07-01 16:10:30 +03:00
|
|
|
]
|
|
|
|
],
|
|
|
|
'multi' => [
|
|
|
|
'rs' => [
|
|
|
|
'name' => 'SubReddits',
|
|
|
|
'required' => true,
|
|
|
|
'exampleValue' => 'selfhosted, php',
|
|
|
|
'title' => 'SubReddit names, separated by commas'
|
|
|
|
]
|
|
|
|
],
|
|
|
|
'user' => [
|
|
|
|
'u' => [
|
|
|
|
'name' => 'User',
|
|
|
|
'required' => true,
|
|
|
|
'exampleValue' => 'shwikibot',
|
|
|
|
'title' => 'User name'
|
|
|
|
],
|
|
|
|
'comments' => [
|
|
|
|
'type' => 'checkbox',
|
|
|
|
'name' => 'Comments',
|
|
|
|
'title' => 'Whether to return comments',
|
|
|
|
'defaultValue' => false
|
|
|
|
]
|
|
|
|
]
|
|
|
|
];
|
|
|
|
|
|
|
|
public function collectData()
|
2023-09-10 22:50:15 +03:00
|
|
|
{
|
2023-12-14 00:06:47 +03:00
|
|
|
$forbiddenKey = 'reddit_forbidden';
|
|
|
|
if ($this->cache->get($forbiddenKey)) {
|
|
|
|
throw new HttpException('403 Forbidden', 403);
|
|
|
|
}
|
|
|
|
|
|
|
|
$rateLimitKey = 'reddit_rate_limit';
|
|
|
|
if ($this->cache->get($rateLimitKey)) {
|
2023-09-10 22:50:15 +03:00
|
|
|
throw new HttpException('429 Too Many Requests', 429);
|
|
|
|
}
|
2023-12-14 00:06:47 +03:00
|
|
|
|
2023-09-10 22:50:15 +03:00
|
|
|
try {
|
|
|
|
$this->collectDataInternal();
|
|
|
|
} catch (HttpException $e) {
|
2023-12-13 23:56:14 +03:00
|
|
|
if ($e->getCode() === 403) {
|
|
|
|
// 403 Forbidden
|
|
|
|
// This can possibly mean that reddit has permanently blocked this server's ip address
|
2023-12-14 00:06:47 +03:00
|
|
|
$this->cache->set($forbiddenKey, true, 60 * 61);
|
|
|
|
}
|
|
|
|
if ($e->getCode() === 429) {
|
|
|
|
$this->cache->set($rateLimitKey, true, 60 * 16);
|
2023-12-13 23:56:14 +03:00
|
|
|
}
|
2023-09-14 04:26:01 +03:00
|
|
|
throw $e;
|
2023-09-10 22:50:15 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private function collectDataInternal(): void
|
2022-07-01 16:10:30 +03:00
|
|
|
{
|
|
|
|
$user = false;
|
|
|
|
$comments = false;
|
|
|
|
$section = $this->getInput('d');
|
|
|
|
|
|
|
|
switch ($this->queriedContext) {
|
|
|
|
case 'single':
|
|
|
|
$subreddits[] = $this->getInput('r');
|
|
|
|
break;
|
|
|
|
case 'multi':
|
|
|
|
$subreddits = explode(',', $this->getInput('rs'));
|
|
|
|
break;
|
|
|
|
case 'user':
|
|
|
|
$subreddits[] = $this->getInput('u');
|
|
|
|
$user = true;
|
|
|
|
$comments = $this->getInput('comments');
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!($this->getInput('search') === '')) {
|
|
|
|
$keywords = $this->getInput('search');
|
|
|
|
$keywords = str_replace([',', ' '], '%20', $keywords);
|
|
|
|
$keywords = $keywords . '%20';
|
|
|
|
} else {
|
|
|
|
$keywords = '';
|
|
|
|
}
|
|
|
|
|
2022-10-05 20:35:02 +03:00
|
|
|
if (!empty($this->getInput('f')) && $this->queriedContext == 'single') {
|
2022-10-02 08:34:20 +03:00
|
|
|
$flair = $this->getInput('f');
|
|
|
|
$flair = str_replace(' ', '%20', $flair);
|
|
|
|
$flair = 'flair%3A%22' . $flair . '%22%20';
|
|
|
|
} else {
|
|
|
|
$flair = '';
|
|
|
|
}
|
|
|
|
|
2022-07-01 16:10:30 +03:00
|
|
|
foreach ($subreddits as $subreddit) {
|
|
|
|
$name = trim($subreddit);
|
2023-07-19 07:39:17 +03:00
|
|
|
$url = self::URI
|
|
|
|
. '/search.json?q='
|
|
|
|
. $keywords
|
|
|
|
. $flair
|
|
|
|
. ($user ? 'author%3A' : 'subreddit%3A')
|
|
|
|
. $name
|
|
|
|
. '&sort='
|
|
|
|
. $this->getInput('d')
|
|
|
|
. '&include_over_18=on';
|
|
|
|
|
|
|
|
$version = 'v0.0.1';
|
|
|
|
$useragent = "rss-bridge $version (https://github.com/RSS-Bridge/rss-bridge)";
|
|
|
|
$json = getContents($url, ['User-Agent: ' . $useragent]);
|
|
|
|
$parsedJson = Json::decode($json, false);
|
|
|
|
|
|
|
|
foreach ($parsedJson->data->children as $post) {
|
2022-07-01 16:10:30 +03:00
|
|
|
if ($post->kind == 't1' && !$comments) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
$data = $post->data;
|
|
|
|
|
|
|
|
if ($data->score < $this->getInput('score')) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
$item = [];
|
|
|
|
$item['author'] = $data->author;
|
|
|
|
$item['uid'] = $data->id;
|
|
|
|
$item['timestamp'] = $data->created_utc;
|
2023-12-30 03:33:31 +03:00
|
|
|
$item['uri'] = $this->urlEncodePathParts($data->permalink);
|
2022-07-01 16:10:30 +03:00
|
|
|
|
|
|
|
$item['categories'] = [];
|
|
|
|
|
|
|
|
if ($post->kind == 't1') {
|
|
|
|
$item['title'] = 'Comment: ' . $data->link_title;
|
|
|
|
} else {
|
|
|
|
$item['title'] = $data->title;
|
|
|
|
|
|
|
|
$item['categories'][] = $data->link_flair_text;
|
|
|
|
$item['categories'][] = $data->pinned ? 'Pinned' : null;
|
|
|
|
$item['categories'][] = $data->spoiler ? 'Spoiler' : null;
|
|
|
|
}
|
|
|
|
|
|
|
|
$item['categories'][] = $data->over_18 ? 'NSFW' : null;
|
|
|
|
$item['categories'] = array_filter($item['categories']);
|
|
|
|
|
|
|
|
if ($post->kind == 't1') {
|
|
|
|
// Comment
|
|
|
|
|
2023-12-30 03:33:31 +03:00
|
|
|
$item['content'] = htmlspecialchars_decode($data->body_html);
|
2022-07-01 16:10:30 +03:00
|
|
|
} elseif ($data->is_self) {
|
|
|
|
// Text post
|
|
|
|
|
2023-12-30 03:33:31 +03:00
|
|
|
$item['content'] = htmlspecialchars_decode($data->selftext_html);
|
2022-10-26 01:47:45 +03:00
|
|
|
} elseif (isset($data->post_hint) && $data->post_hint == 'link') {
|
2022-07-01 16:10:30 +03:00
|
|
|
// Link with preview
|
|
|
|
|
|
|
|
if (isset($data->media)) {
|
2022-10-26 01:47:45 +03:00
|
|
|
// todo: maybe switch on the type
|
|
|
|
if (isset($data->media->oembed->html)) {
|
|
|
|
// Reddit embeds content for some sites (e.g. Twitter)
|
|
|
|
$embed = htmlspecialchars_decode($data->media->oembed->html);
|
|
|
|
} else {
|
|
|
|
$embed = '';
|
|
|
|
}
|
2022-07-01 16:10:30 +03:00
|
|
|
} else {
|
|
|
|
$embed = '';
|
|
|
|
}
|
|
|
|
|
2023-12-30 03:33:31 +03:00
|
|
|
$item['content'] = $this->createFigureLink($data->url, $data->thumbnail, $data->domain) . $embed;
|
|
|
|
} elseif (isset($data->post_hint) && $data->post_hint == 'image') {
|
2022-07-01 16:10:30 +03:00
|
|
|
// Single image
|
|
|
|
|
2023-12-30 03:33:31 +03:00
|
|
|
$item['content'] = $this->createLink($this->urlEncodePathParts($data->permalink), '<img src="' . $data->url . '" />');
|
Fix coding style missed by phpbcf (#2901)
$ composer require --dev friendsofphp/php-cs-fixer
$ echo >.php-cs-fixer.dist.php "<?php
$finder = PhpCsFixer\Finder::create()
->in(__DIR__);
$rules = [
'@PSR12' => true,
// '@PSR12:risky' => true,
'@PHP74Migration' => true,
// '@PHP74Migration:risky' => true,
// buggy, duplicates existing comment sometimes
'no_break_comment' => false,
'array_syntax' => true,
'lowercase_static_reference' => true,
'visibility_required' => false,
// Too much noise
'binary_operator_spaces' => false,
'heredoc_indentation' => false,
'trailing_comma_in_multiline' => false,
];
$config = new PhpCsFixer\Config();
return $config
->setRules($rules)
// ->setRiskyAllowed(true)
->setFinder($finder);
"
$ vendor/bin/php-cs-fixer --version
PHP CS Fixer 3.8.0 BerSzcz against war! by Fabien Potencier and Dariusz Ruminski.
PHP runtime: 8.1.7
$ vendor/bin/php-cs-fixer fix
$ rm .php-cs-fixer.cache
$ vendor/bin/php-cs-fixer fix
2022-07-08 14:00:52 +03:00
|
|
|
} elseif ($data->is_gallery ?? false) {
|
2022-07-01 16:10:30 +03:00
|
|
|
// Multiple images
|
|
|
|
|
|
|
|
$images = [];
|
|
|
|
foreach ($data->gallery_data->items as $media) {
|
|
|
|
$id = $media->media_id;
|
|
|
|
$type = $data->media_metadata->$id->m == 'image/gif' ? 'gif' : 'u';
|
|
|
|
$src = $data->media_metadata->$id->s->$type;
|
|
|
|
$images[] = '<figure><img src="' . $src . '"/></figure><br>';
|
|
|
|
}
|
|
|
|
|
|
|
|
$item['content'] = implode('', $images);
|
|
|
|
} elseif ($data->is_video) {
|
|
|
|
// Video
|
|
|
|
|
|
|
|
// Higher index -> Higher resolution
|
|
|
|
end($data->preview->images[0]->resolutions);
|
|
|
|
$index = key($data->preview->images[0]->resolutions);
|
|
|
|
|
2023-12-30 03:33:31 +03:00
|
|
|
$item['content'] = $this->createFigureLink($data->url, $data->preview->images[0]->resolutions[$index]->url, 'Video');
|
|
|
|
} elseif (isset($data->media) && $data->media->type == 'youtube.com') {
|
2022-07-01 16:10:30 +03:00
|
|
|
// Youtube link
|
2023-12-30 03:33:31 +03:00
|
|
|
$item['content'] = $this->createFigureLink($data->url, $data->media->oembed->thumbnail_url, 'YouTube');
|
|
|
|
//$item['content'] = htmlspecialchars_decode($data->media->oembed->html);
|
2022-07-01 16:10:30 +03:00
|
|
|
} elseif (explode('.', $data->domain)[0] == 'self') {
|
|
|
|
// Crossposted text post
|
|
|
|
// TODO (optionally?) Fetch content of the original post.
|
2023-12-30 03:33:31 +03:00
|
|
|
$item['content'] = $this->createLink($this->urlEncodePathParts($data->permalink), 'Crossposted from r/' . explode('.', $data->domain)[1]);
|
2022-07-01 16:10:30 +03:00
|
|
|
} else {
|
|
|
|
// Link WITHOUT preview
|
2023-12-30 03:33:31 +03:00
|
|
|
$item['content'] = $this->createLink($data->url, $data->domain);
|
2022-07-01 16:10:30 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
$this->items[] = $item;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Sort the order to put the latest posts first, even for mixed subreddits
|
|
|
|
usort($this->items, function ($a, $b) {
|
2023-12-30 03:33:31 +03:00
|
|
|
return $b['timestamp'] <=> $a['timestamp'];
|
2022-07-01 16:10:30 +03:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2023-07-19 07:39:17 +03:00
|
|
|
public function getIcon()
|
|
|
|
{
|
|
|
|
return 'https://www.redditstatic.com/desktop2x/img/favicon/favicon-96x96.png';
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getName()
|
|
|
|
{
|
|
|
|
if ($this->queriedContext == 'single') {
|
|
|
|
return 'Reddit r/' . $this->getInput('r');
|
|
|
|
} elseif ($this->queriedContext == 'user') {
|
|
|
|
return 'Reddit u/' . $this->getInput('u');
|
|
|
|
} else {
|
|
|
|
return self::NAME;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-12-30 03:33:31 +03:00
|
|
|
private function urlEncodePathParts($link)
|
2022-07-01 16:10:30 +03:00
|
|
|
{
|
2023-12-30 03:33:31 +03:00
|
|
|
return self::URI . implode('/', array_map('urlencode', explode('/', $link)));
|
2022-07-01 16:10:30 +03:00
|
|
|
}
|
|
|
|
|
2023-12-30 03:33:31 +03:00
|
|
|
private function createFigureLink($href, $src, $caption)
|
2022-07-01 16:10:30 +03:00
|
|
|
{
|
2023-12-30 03:33:31 +03:00
|
|
|
return sprintf('<a href="%s"><figure><figcaption>%s</figcaption><img src="%s"/></figure></a>', $href, $caption, $src);
|
2022-07-01 16:10:30 +03:00
|
|
|
}
|
|
|
|
|
2023-12-30 03:33:31 +03:00
|
|
|
private function createLink($href, $text)
|
2022-07-01 16:10:30 +03:00
|
|
|
{
|
2023-12-30 03:33:31 +03:00
|
|
|
return sprintf('<a href="%s">%s</a>', $href, $text);
|
2022-07-01 16:10:30 +03:00
|
|
|
}
|
2023-07-19 07:39:17 +03:00
|
|
|
|
|
|
|
public function detectParameters($url)
|
|
|
|
{
|
2023-09-24 19:34:09 +03:00
|
|
|
try {
|
|
|
|
$urlObject = Url::fromString($url);
|
|
|
|
} catch (UrlException $e) {
|
|
|
|
return null;
|
|
|
|
}
|
2023-07-19 07:39:17 +03:00
|
|
|
|
2023-09-24 19:34:09 +03:00
|
|
|
$host = $urlObject->getHost();
|
|
|
|
$path = $urlObject->getPath();
|
2023-07-19 07:39:17 +03:00
|
|
|
|
2023-09-24 19:34:09 +03:00
|
|
|
$pathSegments = explode('/', $path);
|
|
|
|
|
|
|
|
if ($host !== 'www.reddit.com' && $host !== 'old.reddit.com') {
|
2023-07-19 07:39:17 +03:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2023-09-24 19:34:09 +03:00
|
|
|
if ($pathSegments[1] == 'r') {
|
2023-07-19 07:39:17 +03:00
|
|
|
return [
|
2023-08-09 23:40:24 +03:00
|
|
|
'context' => 'single',
|
2023-09-24 19:34:09 +03:00
|
|
|
'r' => $pathSegments[2],
|
2023-07-19 07:39:17 +03:00
|
|
|
];
|
2023-09-24 19:34:09 +03:00
|
|
|
} elseif ($pathSegments[1] == 'user') {
|
2023-07-19 07:39:17 +03:00
|
|
|
return [
|
2023-08-09 23:40:24 +03:00
|
|
|
'context' => 'user',
|
2023-09-24 19:34:09 +03:00
|
|
|
'u' => $pathSegments[2],
|
2023-07-19 07:39:17 +03:00
|
|
|
];
|
|
|
|
} else {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
2019-11-01 15:54:03 +03:00
|
|
|
}
|