Squashed commit of the following:
commit ebbaf83a331395d86754f231ebf3852c31ee13e7 Author: Jonny Barnes <jonny@jonnybarnes.uk> Date: Tue Sep 19 16:07:16 2017 +0100 Show just a name if no known author url commit 7c3fc38a5101635efbb1659d7dc0e4e87f28977a Author: Jonny Barnes <jonny@jonnybarnes.uk> Date: Tue Sep 19 15:55:07 2017 +0100 Update changelog commit e05876d604b2655fdd1b03fe5390c3333cd5e064 Author: Jonny Barnes <jonny@jonnybarnes.uk> Date: Tue Sep 19 15:54:10 2017 +0100 Add a trait for testing tokens commit 1288769757e6c69fccf849a73ef53e6497953d74 Author: Jonny Barnes <jonny@jonnybarnes.uk> Date: Tue Sep 19 15:53:54 2017 +0100 Add a test for the process like job commit d85a7109d51c979846b2b15d92e2b4c3978c6dc7 Author: Jonny Barnes <jonny@jonnybarnes.uk> Date: Tue Sep 19 15:53:25 2017 +0100 fix typo, and allow for array of author info, or just a name commit 1fc63c6fb6c5648e31759502a011b2be0525af54 Author: Jonny Barnes <jonny@jonnybarnes.uk> Date: Mon Sep 18 15:38:16 2017 +0100 Add another test for creating likes commit 487723ac41fa00a8182f5bf3665ab7b5f8fece52 Author: Jonny Barnes <jonny@jonnybarnes.uk> Date: Mon Sep 18 15:38:03 2017 +0100 fix unexpected end of file error commit a24eef82ae7a2a3e1d3943a6cfed85757c713434 Author: Jonny Barnes <jonny@jonnybarnes.uk> Date: Mon Sep 18 15:37:31 2017 +0100 Better response when creating likes commit fa49df98613b136167dc093a97745eeb90a4a7a6 Author: Jonny Barnes <jonny@jonnybarnes.uk> Date: Mon Sep 18 14:43:39 2017 +0100 Make the author fields nullable commit 5a2f9273c18cf31a54eb54f40732024159c3dc2d Author: Jonny Barnes <jonny@jonnybarnes.uk> Date: Mon Sep 18 14:43:20 2017 +0100 Delegate to the LikeService for creating likes commit 801d6567ec3456cbcdfa6260339dd9ed2fdfa5b0 Author: Jonny Barnes <jonny@jonnybarnes.uk> Date: Mon Sep 18 14:42:54 2017 +0100 Create the Job that gets the content of the like and the author info commit df563473606b43a330c4e977b230d4b7b2a85268 Author: Jonny Barnes <jonny@jonnybarnes.uk> Date: Mon Sep 18 14:42:28 2017 +0100 Create the service the mpub controller delegates to commit ab6ebee71ffdeb584bbef0454874d3fc1c6499f4 Author: Jonny Barnes <jonny@jonnybarnes.uk> Date: Mon Sep 18 14:42:08 2017 +0100 Allow Like::create to work for just the url commit 6d70c43f11056597a493f863c3a1ac681ed06b71 Author: Jonny Barnes <jonny@jonnybarnes.uk> Date: Mon Sep 18 14:10:20 2017 +0100 Add some initial tests commit 4049342b061594656dbf7183d7428f95ba6b3598 Author: Jonny Barnes <jonny@jonnybarnes.uk> Date: Mon Sep 18 14:10:06 2017 +0100 Add database migration/seed/factory commit 5b3aa20fa14202e84af310477b97044723201ea7 Author: Jonny Barnes <jonny@jonnybarnes.uk> Date: Mon Sep 18 14:09:21 2017 +0100 Add domain logic for likes commit 7ef5392a1833df6cee77ecb1166af4fc0abc0eb5 Author: Jonny Barnes <jonny@jonnybarnes.uk> Date: Mon Sep 18 14:08:47 2017 +0100 Add routes for likes
This commit is contained in:
parent
85ae2e7c4f
commit
1c7bff508f
16 changed files with 424 additions and 1 deletions
20
app/Http/Controllers/LikesController.php
Normal file
20
app/Http/Controllers/LikesController.php
Normal file
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Like;
|
||||
|
||||
class LikesController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$likes = Like::latest()->paginate(20);
|
||||
|
||||
return view('likes.index', compact('likes'));
|
||||
}
|
||||
|
||||
public function show(Like $like)
|
||||
{
|
||||
return view('likes.show', compact('like'));
|
||||
}
|
||||
}
|
|
@ -6,8 +6,9 @@ use Storage;
|
|||
use Monolog\Logger;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use App\Jobs\ProcessImage;
|
||||
use App\{Media, Note, Place};
|
||||
use App\Services\LikeService;
|
||||
use Monolog\Handler\StreamHandler;
|
||||
use App\{Like, Media, Note, Place};
|
||||
use Intervention\Image\ImageManager;
|
||||
use Illuminate\Http\{Request, Response};
|
||||
use App\Exceptions\InvalidTokenException;
|
||||
|
@ -73,6 +74,14 @@ class MicropubController extends Controller
|
|||
if (stristr($tokenData->getClaim('scope'), 'create') === false) {
|
||||
return $this->returnInsufficientScopeResponse();
|
||||
}
|
||||
if ($request->has('properties.like-of') || $request->has('like-of')) {
|
||||
$like = (new LikeService())->createLike($request);
|
||||
|
||||
return response()->json([
|
||||
'response' => 'created',
|
||||
'location' => config('app.url') . "/likes/$like->id",
|
||||
], 201)->header('Location', config('app.url') . "/likes/$like->id");
|
||||
}
|
||||
$data = [];
|
||||
$data['client-id'] = $tokenData->getClaim('client_id');
|
||||
if ($request->header('Content-Type') == 'application/json') {
|
||||
|
|
59
app/Jobs/ProcessLike.php
Normal file
59
app/Jobs/ProcessLike.php
Normal file
|
@ -0,0 +1,59 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Like;
|
||||
use GuzzleHttp\Client;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Jonnybarnes\WebmentionsParser\Authorship;
|
||||
use Jonnybarnes\WebmentionsParser\Exceptions\AuthorshipParserException;
|
||||
|
||||
class ProcessLike implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $like;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Like $like)
|
||||
{
|
||||
$this->like = $like;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle(Client $client, Authorship $authorship)
|
||||
{
|
||||
$response = $client->request('GET', $this->like->url);
|
||||
$mf2 = \Mf2\parse((string) $response->getBody(), $this->like->url);
|
||||
if (array_has($mf2, 'items.0.properties.content')) {
|
||||
$this->like->content = $mf2['items'][0]['properties']['content'][0]['html'];
|
||||
}
|
||||
|
||||
try {
|
||||
$author = $authorship->findAuthor($mf2);
|
||||
if (is_array($author)) {
|
||||
$this->like->author_name = $author['name'];
|
||||
$this->like->author_url = $author['url'];
|
||||
}
|
||||
if (is_string($author) && $author !== '') {
|
||||
$this->like->author_name = $author;
|
||||
}
|
||||
} catch (AuthorshipParserException $exception) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->like->save();
|
||||
}
|
||||
}
|
44
app/Like.php
Normal file
44
app/Like.php
Normal file
|
@ -0,0 +1,44 @@
|
|||
<?php
|
||||
|
||||
namespace App;
|
||||
|
||||
use Mf2;
|
||||
use HTMLPurifier;
|
||||
use HTMLPurifier_Config;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Like extends Model
|
||||
{
|
||||
protected $fillable = ['url'];
|
||||
|
||||
public function setUrlAttribute($value)
|
||||
{
|
||||
$this->attributes['url'] = normalize_url($value);
|
||||
}
|
||||
|
||||
public function setAuthorUrlAttribute($value)
|
||||
{
|
||||
$this->attributes['author_url'] = normalize_url($value);
|
||||
}
|
||||
|
||||
public function getContentAttribute($value)
|
||||
{
|
||||
if ($value === null) {
|
||||
return $this->url;
|
||||
}
|
||||
|
||||
$mf2 = Mf2\parse($value, $this->url);
|
||||
|
||||
return $this->filterHTML($mf2['items'][0]['properties']['content'][0]['html']);
|
||||
}
|
||||
|
||||
public function filterHTML($html)
|
||||
{
|
||||
$config = HTMLPurifier_Config::createDefault();
|
||||
$config->set('Cache.SerializerPath', storage_path() . '/HTMLPurifier');
|
||||
$config->set('HTML.TargetBlank', true);
|
||||
$purifier = new HTMLPurifier($config);
|
||||
|
||||
return $purifier->purify($html);
|
||||
}
|
||||
}
|
37
app/Services/LikeService.php
Normal file
37
app/Services/LikeService.php
Normal file
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Like;
|
||||
use App\Jobs\ProcessLike;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class LikeService
|
||||
{
|
||||
/**
|
||||
* Create a new Like.
|
||||
*
|
||||
* @param Request $request
|
||||
*/
|
||||
public function createLike(Request $request): Like
|
||||
{
|
||||
if ($request->header('Content-Type') == 'application/json') {
|
||||
//micropub request
|
||||
$url = normalize_url($request->input('properties.like-of.0'));
|
||||
}
|
||||
if (
|
||||
($request->header('Content-Type') == 'x-www-url-formencoded')
|
||||
||
|
||||
($request->header('Content-Type') == 'multipart/form-data')
|
||||
) {
|
||||
$url = normalize_url($request->input('like-of'));
|
||||
}
|
||||
|
||||
$like = Like::create(['url' => $url]);
|
||||
ProcessLike::dispatch($like);
|
||||
|
||||
return $like;
|
||||
}
|
||||
}
|
|
@ -1,5 +1,8 @@
|
|||
# Changelog
|
||||
|
||||
## Version {next}
|
||||
- Add support for `likes` (issue#69)
|
||||
|
||||
## Version 0.8.1 (2017-09-16)
|
||||
- Order notes by latest (issue#70)
|
||||
- AcitivtyStream support is now indicated with HTTP Link headers
|
||||
|
|
12
database/factories/LikeFactory.php
Normal file
12
database/factories/LikeFactory.php
Normal file
|
@ -0,0 +1,12 @@
|
|||
<?php
|
||||
|
||||
use Faker\Generator as Faker;
|
||||
|
||||
$factory->define(App\Like::class, function (Faker $faker) {
|
||||
return [
|
||||
'url' => $faker->url,
|
||||
'author_name' => $faker->name,
|
||||
'author_url' => $faker->url,
|
||||
'content' => '<html><body><div class="h-entry"><div class="e-content">' . $faker->realtext() . '</div></div></body></html>',
|
||||
];
|
||||
});
|
35
database/migrations/2017_09_16_191741_create_likes_table.php
Normal file
35
database/migrations/2017_09_16_191741_create_likes_table.php
Normal file
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class CreateLikesTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('likes', function (Blueprint $table) {
|
||||
$table->increments('id');
|
||||
$table->string('url');
|
||||
$table->string('author_name')->nullable();
|
||||
$table->string('author_url')->nullable();
|
||||
$table->text('content')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('likes');
|
||||
}
|
||||
}
|
|
@ -18,5 +18,6 @@ class DatabaseSeeder extends Seeder
|
|||
$this->call(NotesTableSeeder::class);
|
||||
$this->call(WebMentionsTableSeeder::class);
|
||||
$this->call(IndieWebUserTableSeeder::class);
|
||||
$this->call(LikesTableSeeder::class);
|
||||
}
|
||||
}
|
||||
|
|
16
database/seeds/LikesTableSeeder.php
Normal file
16
database/seeds/LikesTableSeeder.php
Normal file
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class LikesTableSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function run()
|
||||
{
|
||||
factory(App\Like::class, 10)->create();
|
||||
}
|
||||
}
|
|
@ -12,6 +12,7 @@ class NotesTableSeeder extends Seeder
|
|||
public function run()
|
||||
{
|
||||
factory(App\Note::class, 10)->create();
|
||||
sleep(1);
|
||||
$noteWithPlace = App\Note::create([
|
||||
'note' => 'Having a #beer at the local. 🍺',
|
||||
'tweet_id' => '123456789',
|
||||
|
@ -19,17 +20,21 @@ class NotesTableSeeder extends Seeder
|
|||
$place = App\Place::find(1);
|
||||
$noteWithPlace->place()->associate($place);
|
||||
$noteWithPlace->save();
|
||||
sleep(1);
|
||||
$noteWithContact = App\Note::create([
|
||||
'note' => 'Hi @tantek'
|
||||
]);
|
||||
sleep(1);
|
||||
$noteWithContactPlusPic = App\Note::create([
|
||||
'note' => 'Hi @aaron',
|
||||
'client_id' => 'https://jbl5.dev/notes/new'
|
||||
]);
|
||||
sleep(1);
|
||||
$noteWithoutContact = App\Note::create([
|
||||
'note' => 'Hi @bob',
|
||||
'client_id' => 'https://quill.p3k.io'
|
||||
]);
|
||||
sleep(1);
|
||||
//copy aaron’s profile pic in place
|
||||
$spl = new SplFileInfo(public_path() . '/assets/profile-images/aaronparecki.com');
|
||||
if ($spl->isDir() === false) {
|
||||
|
@ -40,6 +45,7 @@ class NotesTableSeeder extends Seeder
|
|||
'note' => 'Note from somehwere',
|
||||
'location' => '53.499,-2.379'
|
||||
]);
|
||||
sleep(1);
|
||||
$noteSyndicated = App\Note::create([
|
||||
'note' => 'This note has all the syndication targets',
|
||||
'tweet_id' => '123456',
|
||||
|
|
27
resources/views/likes/index.blade.php
Normal file
27
resources/views/likes/index.blade.php
Normal file
|
@ -0,0 +1,27 @@
|
|||
@extends('master')
|
||||
|
||||
@section('title')
|
||||
Likes «
|
||||
@stop
|
||||
|
||||
@section('content')
|
||||
<div class="h-feed">
|
||||
@foreach($likes as $like)
|
||||
<div class="h-entry">
|
||||
<div class="h-cite u-like-of">
|
||||
Liked <a class="u-url" href="{{ $like->url }}">a post</a> by
|
||||
<span class="p-author h-card">
|
||||
@isset($like->author_url)
|
||||
<a class="u-url p-name" href="{{ $like->author_url }}">{{ $like->author_name }}</a>
|
||||
@else
|
||||
<span class="p-name">{{ $like->author_name }}</span>
|
||||
@endisset
|
||||
</span>:
|
||||
<blockquote class="e-content">
|
||||
{!! $like->content !!}
|
||||
</blockquote>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@stop
|
23
resources/views/likes/show.blade.php
Normal file
23
resources/views/likes/show.blade.php
Normal file
|
@ -0,0 +1,23 @@
|
|||
@extends('master')
|
||||
|
||||
@section('title')
|
||||
Like «
|
||||
@stop
|
||||
|
||||
@section('content')
|
||||
<div class="h-entry">
|
||||
<div class="h-cite u-like-of">
|
||||
Liked <a class="u-url" href="{{ $like->url }}">a post</a> by
|
||||
<span class="p-author h-card">
|
||||
@isset($like->author_url)
|
||||
<a class="u-url p-name" href="{{ $like->author_url }}">{{ $like->author_name }}</a>
|
||||
@else
|
||||
<span class="p-name">{{ $like->author_name }}</span>
|
||||
@endisset
|
||||
</span>:
|
||||
<blockquote class="e-content">
|
||||
{!! $like->content !!}
|
||||
</blockquote>
|
||||
</div>
|
||||
</div>
|
||||
@stop
|
|
@ -108,6 +108,12 @@ Route::group(['domain' => config('url.longurl')], function () {
|
|||
});
|
||||
Route::get('note/{id}', 'NotesController@redirect'); // for legacy note URLs
|
||||
|
||||
// Likes
|
||||
Route::group(['prefix' => 'likes'], function () {
|
||||
Route::get('/', 'LikesController@index');
|
||||
Route::get('/{like}', 'LikesController@show');
|
||||
});
|
||||
|
||||
// Micropub Client
|
||||
Route::group(['prefix' => 'micropub'], function () {
|
||||
Route::get('/create', 'MicropubClientController@create')->name('micropub-client');
|
||||
|
|
88
tests/Feature/LikesTest.php
Normal file
88
tests/Feature/LikesTest.php
Normal file
|
@ -0,0 +1,88 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use Queue;
|
||||
use App\Like;
|
||||
use Tests\TestCase;
|
||||
use Tests\TestToken;
|
||||
use GuzzleHttp\Client;
|
||||
use App\Jobs\ProcessLike;
|
||||
use Lcobucci\JWT\Builder;
|
||||
use GuzzleHttp\HandlerStack;
|
||||
use GuzzleHttp\Psr7\Response;
|
||||
use GuzzleHttp\Handler\MockHandler;
|
||||
use Lcobucci\JWT\Signer\Hmac\Sha256;
|
||||
use Jonnybarnes\WebmentionsParser\Authorship;
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
|
||||
class LikesTest extends TestCase
|
||||
{
|
||||
use DatabaseTransactions, TestToken;
|
||||
|
||||
public function test_likes_page()
|
||||
{
|
||||
$response = $this->get('/likes');
|
||||
$response->assertViewIs('likes.index');
|
||||
}
|
||||
|
||||
public function test_single_like_page()
|
||||
{
|
||||
$response = $this->get('/likes');
|
||||
$response->assertViewIs('likes.index');
|
||||
}
|
||||
|
||||
public function test_like_micropub_request()
|
||||
{
|
||||
Queue::fake();
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'Authorization' => 'Bearer ' . $this->getToken(),
|
||||
])->json('POST', '/api/post', [
|
||||
'type' => ['h-entry'],
|
||||
'properties' => [
|
||||
'like-of' => ['https://example.org/blog-post'],
|
||||
],
|
||||
]);
|
||||
|
||||
$response->assertJson(['response' => 'created']);
|
||||
|
||||
Queue::assertPushed(ProcessLike::class);
|
||||
$this->assertDatabaseHas('likes', ['url' => 'https://example.org/blog-post']);
|
||||
}
|
||||
|
||||
public function test_process_like_job()
|
||||
{
|
||||
$like = new Like();
|
||||
$like->url = 'http://example.org/note/id';
|
||||
$like->save();
|
||||
$id = $like->id;
|
||||
|
||||
$job = new ProcessLike($like);
|
||||
|
||||
$content = <<<END
|
||||
<html>
|
||||
<body>
|
||||
<div class="h-entry">
|
||||
<div class="e-content">
|
||||
A post that I like.
|
||||
</div>
|
||||
by <span class="p-author">Fred Bloggs</span>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
END;
|
||||
$mock = new MockHandler([
|
||||
new Response(200, [], $content),
|
||||
new Response(200, [], $content),
|
||||
]);
|
||||
$handler = HandlerStack::create($mock);
|
||||
$client = new Client(['handler' => $handler]);
|
||||
$this->app->bind(Client::class, $client);
|
||||
$authorship = new Authorship();
|
||||
|
||||
$job->handle($client, $authorship);
|
||||
|
||||
$this->assertEquals('Fred Bloggs', Like::find($id)->author_name);
|
||||
}
|
||||
}
|
37
tests/TestToken.php
Normal file
37
tests/TestToken.php
Normal file
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
namespace Tests;
|
||||
|
||||
use Lcobucci\JWT\Builder;
|
||||
use Lcobucci\JWT\Signer\Hmac\Sha256;
|
||||
|
||||
trait TestToken
|
||||
{
|
||||
public function getToken()
|
||||
{
|
||||
$signer = new Sha256();
|
||||
$token = (new Builder())
|
||||
->set('client_id', 'https://quill.p3k.io')
|
||||
->set('me', 'https://jonnybarnes.localhost')
|
||||
->set('scope', 'create update')
|
||||
->set('issued_at', time())
|
||||
->sign($signer, env('APP_KEY'))
|
||||
->getToken();
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
public function getInvalidToken()
|
||||
{
|
||||
$signer = new Sha256();
|
||||
$token = (new Builder())
|
||||
->set('client_id', 'https://quill.p3k.io')
|
||||
->set('me', 'https://jonnybarnes.localhost')
|
||||
->set('scope', 'view') //error here
|
||||
->set('issued_at', time())
|
||||
->sign($signer, env('APP_KEY'))
|
||||
->getToken();
|
||||
|
||||
return $token;
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue