Skip to content

Commit

Permalink
Merge pull request #35 from spatie/patreon-api
Browse files Browse the repository at this point in the history
Patreon api
  • Loading branch information
rubenvanassche committed Dec 10, 2018
2 parents 83e540e + 1e41be2 commit 45ff082
Show file tree
Hide file tree
Showing 23 changed files with 709 additions and 5 deletions.
5 changes: 4 additions & 1 deletion .env.example
Expand Up @@ -41,4 +41,7 @@ BASIC_AUTH_PASSWORD=secret

RESPONSE_CACHE_ENABLED=false

INSTAGRAM_TOKEN=
INSTAGRAM_TOKEN=

PATREON_CLIENT_ID=
PATREON_SECRET=
73 changes: 73 additions & 0 deletions app/Console/Commands/ImportPatreonPledgers.php
@@ -0,0 +1,73 @@
<?php

namespace App\Console\Commands;

use App\Models\PatreonPledger;
use App\Services\Patreon\Patreon;
use App\Services\Patreon\Resources\Campaign;
use App\Services\Patreon\Resources\Pledge;
use App\Services\Patreon\Resources\Reward;
use Exception;
use Illuminate\Console\Command;
use Illuminate\Support\Collection;

class ImportPatreonPledgers extends Command
{
protected $signature = 'import:patreon-pledgers';

protected $description = 'Import pledgers from Patreon';

/** @var \App\Services\Patreon\Patreon */
protected $patreon;

public function __construct(Patreon $patreon)
{
$this->patreon = $patreon;

parent::__construct();
}

public function handle()
{
$this->info('Importing pledgers from Patreon...');

$this->removePreviousPledgers();

$campaigns = $this->patreon->campaigns();

if ($campaigns->count() === 0) {
throw new Exception("No Patreon campaigns found.");
}

$this->getPledges($campaigns->first())->each(function (Pledge $pledge) {
PatreonPledger::import($pledge->user);
});

$this->info('All done!');
}

protected function getPledges(Campaign $campaign): Collection
{
$rewards = $this->getSuitableRewards($campaign);

return $this->patreon->pledges($campaign->id)->filter(function (Pledge $pledge) use ($rewards) {
return $rewards->contains(function (Reward $reward) use ($pledge) {
return $reward->id === $pledge->rewardId;
});
});
}

protected function getSuitableRewards(Campaign $campaign): Collection
{
$minimalAmount = 5000;

return $campaign->rewards->filter(function (Reward $reward) use ($minimalAmount) {
return $reward->amount >= $minimalAmount;
})->values();
}

protected function removePreviousPledgers()
{
PatreonPledger::query()->truncate();
}
}
1 change: 1 addition & 0 deletions app/Console/Kernel.php
Expand Up @@ -14,6 +14,7 @@ protected function schedule(Schedule $schedule)
$schedule->command('import:random-contributor')->hourly();
$schedule->command('import:packagist-downloads')->hourly();
$schedule->command('import:github-repositories')->daily();
$schedule->command('import:patreon-pledgers')->daily();
}

protected function commands()
Expand Down
5 changes: 4 additions & 1 deletion app/Http/Controllers/OpenSourceController.php
Expand Up @@ -5,6 +5,7 @@
use App\Http\Resources\RepositoryResource;
use App\Models\Contributor;
use App\Models\Issue;
use App\Models\PatreonPledger;
use App\Models\Repository;

class OpenSourceController extends Controller
Expand All @@ -19,7 +20,9 @@ public function index()

$contributor = Contributor::first();

return view('pages.open-source.index', compact('repositories', 'issues', 'contributor'));
$patreonPledger = PatreonPledger::get()->random();

return view('pages.open-source.index', compact('repositories', 'issues', 'contributor', 'patreonPledger'));
}

public function packages()
Expand Down
19 changes: 19 additions & 0 deletions app/Http/Resources/PatreonResource.php
@@ -0,0 +1,19 @@
<?php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\JsonResource;

class PatreonResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function toArray($request)
{
return parent::toArray($request);
}
}
36 changes: 36 additions & 0 deletions app/Models/PatreonPledger.php
@@ -0,0 +1,36 @@
<?php

namespace App\Models;

use App\Services\Patreon\Resources\User;
use Illuminate\Database\Eloquent\Model;

class PatreonPledger extends Model
{
public $dates = ['taken_at'];

public static function import(User $user)
{
if (static::where('patreon_id', $user->id)->count() > 0) {
return;
}

$model = new static();

$model->patreon_id = $user->id;
$model->name = $user->name;
$model->avatar_url = $user->avatarUrl;

$model->save();
}

public function getRespectPhraseAttribute()
{
return collect([
"Thank your for your pledge",
"You sir/madam are awesome",
"We eat our monthly pasta thanks to you",
"Your actions are heart-warming",
])->random();
}
}
105 changes: 105 additions & 0 deletions app/Services/Patreon/Patreon.php
@@ -0,0 +1,105 @@
<?php

namespace App\Services\Patreon;

use App\Services\Patreon\Resources\Campaign;
use App\Services\Patreon\Resources\Pledge;
use App\Services\Patreon\Resources\Reward;
use App\Services\Patreon\Resources\User;
use GuzzleHttp\Client;
use Illuminate\Support\Collection;

class Patreon
{
/** @var \GuzzleHttp\Client */
protected $client;

public function __construct(Client $client)
{
$this->client = $client;
}

public function campaigns(): Collection
{
$data = $this->request("current_user/campaigns?include=pledges,rewards");

return $this->importCampaigns($data);
}

public function pledges(int $campaignId): Collection
{
return $this->fetchPledges("campaigns/{$campaignId}/pledges?include=patron.null,reward");
}

protected function request(string $endpoint): array
{
$response = $this->client->get($endpoint);

return json_decode($response->getBody(), true);
}

protected function fetchPledges(string $endpoint): Collection
{
$data = $this->request($endpoint);

$pledges = $this->importPledges($data);

if (array_key_exists('next', $data['links'])) {
$pledges = $pledges->merge($this->fetchPledges($data['links']['next']));
}

return $pledges;
}

protected function importCampaigns(array $data): Collection
{
$rewards = $this->importRewards($data);

return collect($data['data'])->map(function (array $item) use ($rewards) {
$campagin = Campaign::import($item);

$campagin->rewards = collect($item['relationships']['rewards']['data'])
->map(function ($item) use ($rewards) {
return $rewards->first(function (Reward $reward) use ($rewards, $item) {
return $reward->id === (int ) $item['id'];
});
});

return $campagin;
});
}

protected function importPledges(array $data)
{
$users = $this->importUsers($data);
$rewards = $this->importRewards($data);

return collect($data['data'])->map(function (array $pledge) use ($rewards, $users) {
$pledge = Pledge::import($pledge);

$pledge->user = $users->first(function (User $user) use ($pledge) {
return $user->id === $pledge->userId;
});

return $pledge;
});
}

protected function importUsers(array $data): Collection
{
return collect($data['included'])->filter(function (array $item) {
return $item['type'] === 'user';
})->map(function ($item) {
return User::import($item);
})->values();
}

protected function importRewards(array $data): Collection
{
return collect($data['included'])->filter(function (array $item) {
return $item['type'] === 'reward';
})->map(function ($item) {
return Reward::import($item);
})->values();
}
}
82 changes: 82 additions & 0 deletions app/Services/Patreon/PatreonAuthenticator.php
@@ -0,0 +1,82 @@
<?php

namespace App\Services\Patreon;

use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
use Spatie\Valuestore\Valuestore;

class PatreonAuthenticator
{
/** @var \GuzzleHttp\Client */
protected $client;

/** @var \Spatie\Valuestore\Valuestore */
protected $valueStore;

/** @var string */
protected $clientId;

/** @var string */
protected $clientSecret;

public function __construct(string $clientId, string $clientSecret)
{
$this->client = new Client([
'base_uri' => 'https://api.patreon.com/oauth2/token',
]);

$this->valueStore = Valuestore::make(storage_path('/app/patreon-access.json'));

$this->clientId = $clientId;

$this->clientSecret = $clientSecret;
}

public function autoRefresh(string $refreshToken = null) : array
{
$tokens = $this->getTokens();

return $this->refresh($refreshToken ?? $tokens['refresh_token']);
}

protected function refresh($refreshToken) : array
{
$data = [
"grant_type" => "refresh_token",
"refresh_token" => $refreshToken,
"client_id" => $this->clientId,
"client_secret" => $this->clientSecret,
];

try {
$response = $this->client->request('post', 'token', [
'query' => $data,
]);
} catch (RequestException $exception) {
return $this->getTokens();
}

$tokens = json_decode($response->getBody(), true);

$this->saveTokens($tokens);

return $tokens;
}

protected function saveTokens($tokens)
{
$this->valueStore->put('access_token', $tokens['access_token']);
$this->valueStore->put('refresh_token', $tokens['refresh_token']);
}

protected function getTokens(): array
{
$tokens = [];

$tokens['access_token'] = $this->valueStore->get('access_token');
$tokens['refresh_token'] = $this->valueStore->get('refresh_token');

return $tokens;
}
}
32 changes: 32 additions & 0 deletions app/Services/Patreon/PatreonServiceProvider.php
@@ -0,0 +1,32 @@
<?php

namespace App\Services\Patreon;

use Carbon\Laravel\ServiceProvider;
use GuzzleHttp\Client;

class PatreonServiceProvider extends ServiceProvider
{
public function register()
{
$this->app->singleton(Patreon::class, function () {
$authenticator = new PatreonAuthenticator(config('services.patreon.id'), config('services.patreon.secret'));

$tokens = $authenticator->autoRefresh();

$client = $this->buildClient($tokens['access_token']);

return new Patreon($client);
});
}

protected function buildClient($accessToken): Client
{
return new Client([
'base_uri' => 'https://www.patreon.com/api/oauth2/api/',
'headers' => [
'authorization' => "Bearer {$accessToken}",
],
]);
}
}

0 comments on commit 45ff082

Please sign in to comment.