Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 34 additions & 14 deletions bridges/RedditBridge.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
/**
* This bridge does NOT use reddit's official rss feeds.
*
* This bridge uses reddit's json api: https://old.reddit.com/search.json?q=
* This bridge uses Reddit's JSON API.
*
* It uses the legacy unauthenticated API by default.
* Configure both `app_id` and `app_secret` to use OAuth2 instead.
*/
class RedditBridge extends BridgeAbstract
{
Expand All @@ -13,6 +16,15 @@ class RedditBridge extends BridgeAbstract
const CACHE_TIMEOUT = 60 * 60 * 2; // 2h
const DESCRIPTION = 'Return hot submissions from Reddit';

const CONFIGURATION = [
'app_id' => [
'required' => false,
],
'app_secret' => [
'required' => false,
],
];

const PARAMETERS = [
'global' => [
'score' => [
Expand Down Expand Up @@ -112,6 +124,8 @@ class RedditBridge extends BridgeAbstract
]
];

private ?RedditClient $redditClient = null;

public function collectData()
{
$forbiddenKey = 'reddit_forbidden';
Expand Down Expand Up @@ -167,17 +181,14 @@ private function collectDataInternal(): void

$search = $this->getInput('search');
$flareInput = $this->getInput('f');
$min_score = $this->getInput('score');
$min_comments = $this->getInput('min_comments');

foreach ($subreddits as $subreddit) {
$version = 'v0.0.2';
$useragent = "rss-bridge $version (https://github.com/RSS-Bridge/rss-bridge)";
$url = self::createUrl($search, $flareInput, $subreddit, $user, $section, $time, $this->queriedContext);
$parameters = self::createSearchParameters($search, $flareInput, $subreddit, $user, $section, $time, $this->queriedContext);
$response = $this->getRedditClient()->search($parameters);

$response = getContents($url, ['User-Agent: ' . $useragent], [], true);

$json = $response->getBody();

$parsedJson = Json::decode($json, false);
$parsedJson = Json::decode($response->getBody(), false);

foreach ($parsedJson->data->children as $post) {
if ($post->kind == 't1' && !$comments) {
Expand All @@ -186,8 +197,6 @@ private function collectDataInternal(): void

$data = $post->data;

$min_score = $this->getInput('score');
$min_comments = $this->getInput('min_comments');
if ($min_score >= 0 && $min_comments >= 0) {
if ($data->num_comments < $min_comments || $data->score < $min_score) {
continue;
Expand Down Expand Up @@ -299,7 +308,7 @@ private function collectDataInternal(): void
});
}

public static function createUrl($search, $flareInput, $subreddit, bool $user, $section, $time, $queriedContext): string
private static function createSearchParameters($search, $flareInput, $subreddit, bool $user, $section, $time, $queriedContext): array
{
$keywords = '';

Expand All @@ -316,13 +325,12 @@ public static function createUrl($search, $flareInput, $subreddit, bool $user, $
$flair = '';
}
$name = trim($subreddit);
$query = [
return [
'q' => $keywords . $flair . ($user ? 'author:' : 'subreddit:') . $name,
'sort' => $section,
'include_over_18' => 'on',
't' => $time
];
return 'https://old.reddit.com/search.json?' . http_build_query($query);
}

public function getIcon()
Expand Down Expand Up @@ -397,4 +405,16 @@ public function detectParameters($url)
return null;
}
}

private function getRedditClient(): RedditClient
{
if ($this->redditClient === null) {
$this->redditClient = new RedditClient(
$this->cache,
$this->getOption('app_id'),
$this->getOption('app_secret')
);
}
return $this->redditClient;
}
}
35 changes: 35 additions & 0 deletions docs/10_Bridge_Specific/Reddit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
RedditBridge
============

Since Reddit shut down unauthorized JSON API access:
https://www.reddit.com/r/modnews/comments/1tq9vxo/protecting_communities_from_scrapers_and_platform/

Unfortunately, Reddit also blocked "easy" setup for developer apps:
https://old.reddit.com/wiki/api#wiki_read_the_full_api_terms_and_sign_up_for_usage

The bridge uses Reddit's legacy unauthenticated endpoint by default.
To use Reddit's OAuth2 API instead, configure both credentials below.

If you want to use the OAuth2 API, you need to either:

- Submit a request for an app using Reddit's Data API **and somehow get it approved by Reddit**.

- Already have an existing app - they still seem to work.

In both cases you need to have a private "script" app and its "id" and "secret":
https://old.reddit.com/prefs/apps

"Id" should be visible right in the base view; secret is available when selecting "edit".

You'll also need a private RSS-Bridge instance to set up your own configuration.

In `config.ini.php` add the following configuration:

```ini
[RedditBridge]
app_id = "<ID>"
app_secret = "<secret>"
```

The bridge will handle OAuth, refresh bearer, etc., if both `app_id` and `app_secret` are configured.
If either, or both, are missing, then the bridge falls back to legacy unauthenticated access.
94 changes: 94 additions & 0 deletions lib/RedditClient.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
<?php

class RedditClient
{
private const LEGACY_API_URI = 'https://old.reddit.com';
private const OAUTH_API_URI = 'https://oauth.reddit.com';
private const OAUTH_TOKEN_KEY = 'reddit_oauth_token';
private const VERSION = 'v0.0.3';
private const USER_AGENT = 'rss-bridge ' . self::VERSION . ' (https://github.com/RSS-Bridge/rss-bridge)';

private CacheInterface $cache;
private ?string $appId;
private ?string $appSecret;

public function __construct(CacheInterface $cache, ?string $appId = null, ?string $appSecret = null)
{
$this->cache = $cache;
$this->appId = $appId === '' ? null : $appId;
$this->appSecret = $appSecret === '' ? null : $appSecret;
}

public function search(array $parameters): Response
{
if ($this->appId === null || $this->appSecret === null) {
return $this->searchUnauthenticated($parameters);
}
return $this->searchAuthenticated($parameters);
}

private function searchUnauthenticated(array $parameters): Response
{
$url = self::createSearchUrl(self::LEGACY_API_URI, $parameters);
return getContents($url, ['User-Agent: ' . self::USER_AGENT], [], true);
}

private function searchAuthenticated(array $parameters): Response
{
$url = self::createSearchUrl(self::OAUTH_API_URI, $parameters);
try {
return $this->requestAuthenticated($url);
} catch (HttpException $e) {
if ($e->getCode() !== 401 && $e->getCode() !== 403) {
throw $e;
}
$this->cache->delete(self::OAUTH_TOKEN_KEY);
return $this->requestAuthenticated($url);
}
}

private function requestAuthenticated(string $url): Response
{
$headers = [
'User-Agent: ' . self::USER_AGENT,
'Authorization: Bearer ' . $this->getAccessToken(),
];
return getContents($url, $headers, [], true);
}

private function getAccessToken(): string
{
$cachedToken = $this->cache->get(self::OAUTH_TOKEN_KEY);
if ($cachedToken) {
return $cachedToken;
}

$headers = [
'User-Agent: ' . self::USER_AGENT,
'Authorization: Basic ' . base64_encode((string) $this->appId . ':' . (string) $this->appSecret),
];
$data = [
'grant_type' => 'client_credentials',
'scope' => 'read',
];
$curlopts = [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query($data),
];
$response = getContents('https://www.reddit.com/api/v1/access_token', $headers, $curlopts);

$data = Json::decode($response, false);
$token = $data->access_token;
if (!isset($token)) {
throw new \Exception('Failed to obtain Reddit OAuth access token: ' . $response);
}
$expiresIn = $data->expires_in ?? 3600;
$this->cache->set(self::OAUTH_TOKEN_KEY, $token, $expiresIn - 60);
return $token;
}

private static function createSearchUrl(string $apiUri, array $parameters): string
{
return $apiUri . '/search.json?' . http_build_query($parameters);
}
}
33 changes: 0 additions & 33 deletions tests/RedditBridgeTest.php

This file was deleted.

Loading