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:
Jonny Barnes 2017-09-19 16:07:32 +01:00
parent 85ae2e7c4f
commit 1c7bff508f
16 changed files with 424 additions and 1 deletions

View 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'));
}
}

View file

@ -6,8 +6,9 @@ use Storage;
use Monolog\Logger; use Monolog\Logger;
use Ramsey\Uuid\Uuid; use Ramsey\Uuid\Uuid;
use App\Jobs\ProcessImage; use App\Jobs\ProcessImage;
use App\{Media, Note, Place}; use App\Services\LikeService;
use Monolog\Handler\StreamHandler; use Monolog\Handler\StreamHandler;
use App\{Like, Media, Note, Place};
use Intervention\Image\ImageManager; use Intervention\Image\ImageManager;
use Illuminate\Http\{Request, Response}; use Illuminate\Http\{Request, Response};
use App\Exceptions\InvalidTokenException; use App\Exceptions\InvalidTokenException;
@ -73,6 +74,14 @@ class MicropubController extends Controller
if (stristr($tokenData->getClaim('scope'), 'create') === false) { if (stristr($tokenData->getClaim('scope'), 'create') === false) {
return $this->returnInsufficientScopeResponse(); 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 = [];
$data['client-id'] = $tokenData->getClaim('client_id'); $data['client-id'] = $tokenData->getClaim('client_id');
if ($request->header('Content-Type') == 'application/json') { if ($request->header('Content-Type') == 'application/json') {

59
app/Jobs/ProcessLike.php Normal file
View 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
View 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);
}
}

View 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;
}
}

View file

@ -1,5 +1,8 @@
# Changelog # Changelog
## Version {next}
- Add support for `likes` (issue#69)
## Version 0.8.1 (2017-09-16) ## Version 0.8.1 (2017-09-16)
- Order notes by latest (issue#70) - Order notes by latest (issue#70)
- AcitivtyStream support is now indicated with HTTP Link headers - AcitivtyStream support is now indicated with HTTP Link headers

View 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>',
];
});

View 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');
}
}

View file

@ -18,5 +18,6 @@ class DatabaseSeeder extends Seeder
$this->call(NotesTableSeeder::class); $this->call(NotesTableSeeder::class);
$this->call(WebMentionsTableSeeder::class); $this->call(WebMentionsTableSeeder::class);
$this->call(IndieWebUserTableSeeder::class); $this->call(IndieWebUserTableSeeder::class);
$this->call(LikesTableSeeder::class);
} }
} }

View 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();
}
}

View file

@ -12,6 +12,7 @@ class NotesTableSeeder extends Seeder
public function run() public function run()
{ {
factory(App\Note::class, 10)->create(); factory(App\Note::class, 10)->create();
sleep(1);
$noteWithPlace = App\Note::create([ $noteWithPlace = App\Note::create([
'note' => 'Having a #beer at the local. 🍺', 'note' => 'Having a #beer at the local. 🍺',
'tweet_id' => '123456789', 'tweet_id' => '123456789',
@ -19,17 +20,21 @@ class NotesTableSeeder extends Seeder
$place = App\Place::find(1); $place = App\Place::find(1);
$noteWithPlace->place()->associate($place); $noteWithPlace->place()->associate($place);
$noteWithPlace->save(); $noteWithPlace->save();
sleep(1);
$noteWithContact = App\Note::create([ $noteWithContact = App\Note::create([
'note' => 'Hi @tantek' 'note' => 'Hi @tantek'
]); ]);
sleep(1);
$noteWithContactPlusPic = App\Note::create([ $noteWithContactPlusPic = App\Note::create([
'note' => 'Hi @aaron', 'note' => 'Hi @aaron',
'client_id' => 'https://jbl5.dev/notes/new' 'client_id' => 'https://jbl5.dev/notes/new'
]); ]);
sleep(1);
$noteWithoutContact = App\Note::create([ $noteWithoutContact = App\Note::create([
'note' => 'Hi @bob', 'note' => 'Hi @bob',
'client_id' => 'https://quill.p3k.io' 'client_id' => 'https://quill.p3k.io'
]); ]);
sleep(1);
//copy aarons profile pic in place //copy aarons profile pic in place
$spl = new SplFileInfo(public_path() . '/assets/profile-images/aaronparecki.com'); $spl = new SplFileInfo(public_path() . '/assets/profile-images/aaronparecki.com');
if ($spl->isDir() === false) { if ($spl->isDir() === false) {
@ -40,6 +45,7 @@ class NotesTableSeeder extends Seeder
'note' => 'Note from somehwere', 'note' => 'Note from somehwere',
'location' => '53.499,-2.379' 'location' => '53.499,-2.379'
]); ]);
sleep(1);
$noteSyndicated = App\Note::create([ $noteSyndicated = App\Note::create([
'note' => 'This note has all the syndication targets', 'note' => 'This note has all the syndication targets',
'tweet_id' => '123456', 'tweet_id' => '123456',

View 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

View 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

View file

@ -108,6 +108,12 @@ Route::group(['domain' => config('url.longurl')], function () {
}); });
Route::get('note/{id}', 'NotesController@redirect'); // for legacy note URLs 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 // Micropub Client
Route::group(['prefix' => 'micropub'], function () { Route::group(['prefix' => 'micropub'], function () {
Route::get('/create', 'MicropubClientController@create')->name('micropub-client'); Route::get('/create', 'MicropubClientController@create')->name('micropub-client');

View 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
View 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;
}
}