Skip to content
Draft
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
80 changes: 70 additions & 10 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 via OAuth2: https://oauth.reddit.com/search.json?q=
*
* Requires a Reddit "script" app and its "ID" and "secret".
* `app_id` and `app_secret` parameters must be set in the configuration for this bridge.
*/
class RedditBridge extends BridgeAbstract
{
Expand All @@ -12,6 +15,18 @@ class RedditBridge extends BridgeAbstract
const URI = 'https://old.reddit.com';
const CACHE_TIMEOUT = 60 * 60 * 2; // 2h
const DESCRIPTION = 'Return hot submissions from Reddit';
const VERSION = 'v0.0.3';
const USER_AGENT = 'rss-bridge ' . self::VERSION . ' (https://github.com/RSS-Bridge/rss-bridge)';
const REDDIT_OAUTH_TOKEN_KEY = 'reddit_oauth_token';

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

const PARAMETERS = [
'global' => [
Expand Down Expand Up @@ -167,17 +182,33 @@ private function collectDataInternal(): void

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

$headers = [
'User-Agent: ' . self::USER_AGENT,
'Authorization: Bearer ' . $this->getAccessToken(),
];

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);

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

$json = $response->getBody();
try {
$response = getContents($url, $headers, [], true);
} catch (HttpException $e) {
if ($e->getCode() === 401 || $e->getCode() === 403) {
// Token might've been rejected, or expired earlier than expected.
// Clear cached token and try one more time with a fresh one.
$this->cache->set(self::REDDIT_OAUTH_TOKEN_KEY, null, 0);
$accessToken = $this->getAccessToken();
$headers[1] = 'Authorization: Bearer ' . $accessToken;
$response = getContents($url, $headers, [], true);
} else {
throw $e;
}
}

$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 +217,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,6 +328,37 @@ private function collectDataInternal(): void
});
}

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

$headers = [
'User-Agent: ' . self::USER_AGENT,
'Authorization: Basic ' . base64_encode($this->getOption('app_id') . ':' . $this->getOption('app_secret')),
];
$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::REDDIT_OAUTH_TOKEN_KEY, $token, $expiresIn - 60);
return $token;
}

public static function createUrl($search, $flareInput, $subreddit, bool $user, $section, $time, $queriedContext): string
{
$keywords = '';
Expand All @@ -322,7 +382,7 @@ public static function createUrl($search, $flareInput, $subreddit, bool $user, $
'include_over_18' => 'on',
't' => $time
];
return 'https://old.reddit.com/search.json?' . http_build_query($query);
return 'https://oauth.reddit.com/search.json?' . http_build_query($query);
}

public function getIcon()
Expand Down
31 changes: 31 additions & 0 deletions docs/10_Bridge_Specific/Reddit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
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

So currently, to get the bridge to work 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.
16 changes: 8 additions & 8 deletions tests/RedditBridgeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,23 @@ public function test()
{
$sut = new RedditBridge(new NullCache(), new NullLogger());

// https://old.reddit.com/search.json?q=cats dogs hen subreddit:php&sort=hot&include_over_18=on
$expected = 'https://old.reddit.com/search.json?q=cats+dogs+hen+subreddit%3Aphp&sort=hot&include_over_18=on&t=all';
// https://oauth.reddit.com/search.json?q=cats dogs hen subreddit:php&sort=hot&include_over_18=on
$expected = 'https://oauth.reddit.com/search.json?q=cats+dogs+hen+subreddit%3Aphp&sort=hot&include_over_18=on&t=all';
$actual = RedditBridge::createUrl('cats,dogs hen', '', 'php', false, 'hot', 'all', 'single');
$this->assertSame($expected, $actual);

// https://old.reddit.com/search.json?q=author:RavenousRandy&sort=hot&include_over_18=on
$expected = 'https://old.reddit.com/search.json?q=author%3ARavenousRandy&sort=hot&include_over_18=on&t=week';
// https://oauth.reddit.com/search.json?q=author:RavenousRandy&sort=hot&include_over_18=on
$expected = 'https://oauth.reddit.com/search.json?q=author%3ARavenousRandy&sort=hot&include_over_18=on&t=week';
$actual = RedditBridge::createUrl('', '', 'RavenousRandy', true, 'hot', 'week', 'user');
$this->assertSame($expected, $actual);

// https://old.reddit.com/search.json?q=cats dogs hen flair:"Proxy" subreddit:php&sort=hot&include_over_18=on
$expected = 'https://old.reddit.com/search.json?q=cats+dogs+hen+flair%3A%22Proxy%22+subreddit%3Aphp&sort=hot&include_over_18=on&t=month';
// https://oauth.reddit.com/search.json?q=cats dogs hen flair:"Proxy" subreddit:php&sort=hot&include_over_18=on
$expected = 'https://oauth.reddit.com/search.json?q=cats+dogs+hen+flair%3A%22Proxy%22+subreddit%3Aphp&sort=hot&include_over_18=on&t=month';
$actual = RedditBridge::createUrl('cats,dogs hen', 'Proxy', 'php', false, 'hot', 'month', 'single');
$this->assertSame($expected, $actual);

// https://old.reddit.com/search.json?q=cats dogs hen flair:"Proxy Linux Server" subreddit:php&sort=hot&include_over_18=on
$expected = 'https://old.reddit.com/search.json?q=cats+dogs+hen+flair%3A%22Proxy+Linux+Server%22+subreddit%3Aphp&sort=hot&include_over_18=on&t=day';
// https://oauth.reddit.com/search.json?q=cats dogs hen flair:"Proxy Linux Server" subreddit:php&sort=hot&include_over_18=on
$expected = 'https://oauth.reddit.com/search.json?q=cats+dogs+hen+flair%3A%22Proxy+Linux+Server%22+subreddit%3Aphp&sort=hot&include_over_18=on&t=day';
$actual = RedditBridge::createUrl('cats,dogs hen', 'Proxy,Linux Server', 'php', false, 'hot', 'day', 'single');
$this->assertSame($expected, $actual);
}
Expand Down
Loading