diff --git a/app/Http/Controllers/LikesController.php b/app/Http/Controllers/LikesController.php
new file mode 100644
index 00000000..02922e86
--- /dev/null
+++ b/app/Http/Controllers/LikesController.php
@@ -0,0 +1,20 @@
+paginate(20);
+
+ return view('likes.index', compact('likes'));
+ }
+
+ public function show(Like $like)
+ {
+ return view('likes.show', compact('like'));
+ }
+}
diff --git a/app/Http/Controllers/MicropubController.php b/app/Http/Controllers/MicropubController.php
index c6498a69..36cba4ab 100644
--- a/app/Http/Controllers/MicropubController.php
+++ b/app/Http/Controllers/MicropubController.php
@@ -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') {
diff --git a/app/Jobs/ProcessLike.php b/app/Jobs/ProcessLike.php
new file mode 100644
index 00000000..84fffa7d
--- /dev/null
+++ b/app/Jobs/ProcessLike.php
@@ -0,0 +1,59 @@
+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();
+ }
+}
diff --git a/app/Like.php b/app/Like.php
new file mode 100644
index 00000000..aae728e6
--- /dev/null
+++ b/app/Like.php
@@ -0,0 +1,44 @@
+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);
+ }
+}
diff --git a/app/Services/LikeService.php b/app/Services/LikeService.php
new file mode 100644
index 00000000..d4ff6f76
--- /dev/null
+++ b/app/Services/LikeService.php
@@ -0,0 +1,37 @@
+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;
+ }
+}
diff --git a/changelog.md b/changelog.md
index f5b38ea3..854deb3c 100644
--- a/changelog.md
+++ b/changelog.md
@@ -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
diff --git a/database/factories/LikeFactory.php b/database/factories/LikeFactory.php
new file mode 100644
index 00000000..ad3e9551
--- /dev/null
+++ b/database/factories/LikeFactory.php
@@ -0,0 +1,12 @@
+define(App\Like::class, function (Faker $faker) {
+ return [
+ 'url' => $faker->url,
+ 'author_name' => $faker->name,
+ 'author_url' => $faker->url,
+ 'content' => '
' . $faker->realtext() . '
',
+ ];
+});
diff --git a/database/migrations/2017_09_16_191741_create_likes_table.php b/database/migrations/2017_09_16_191741_create_likes_table.php
new file mode 100644
index 00000000..5f413c82
--- /dev/null
+++ b/database/migrations/2017_09_16_191741_create_likes_table.php
@@ -0,0 +1,35 @@
+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');
+ }
+}
diff --git a/database/seeds/DatabaseSeeder.php b/database/seeds/DatabaseSeeder.php
index 8e906692..bb2d4f85 100644
--- a/database/seeds/DatabaseSeeder.php
+++ b/database/seeds/DatabaseSeeder.php
@@ -18,5 +18,6 @@ class DatabaseSeeder extends Seeder
$this->call(NotesTableSeeder::class);
$this->call(WebMentionsTableSeeder::class);
$this->call(IndieWebUserTableSeeder::class);
+ $this->call(LikesTableSeeder::class);
}
}
diff --git a/database/seeds/LikesTableSeeder.php b/database/seeds/LikesTableSeeder.php
new file mode 100644
index 00000000..59c5af07
--- /dev/null
+++ b/database/seeds/LikesTableSeeder.php
@@ -0,0 +1,16 @@
+create();
+ }
+}
diff --git a/database/seeds/NotesTableSeeder.php b/database/seeds/NotesTableSeeder.php
index 334ba2df..3509ae8a 100644
--- a/database/seeds/NotesTableSeeder.php
+++ b/database/seeds/NotesTableSeeder.php
@@ -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',
diff --git a/resources/views/likes/index.blade.php b/resources/views/likes/index.blade.php
new file mode 100644
index 00000000..8c114d98
--- /dev/null
+++ b/resources/views/likes/index.blade.php
@@ -0,0 +1,27 @@
+@extends('master')
+
+@section('title')
+Likes «
+@stop
+
+@section('content')
+
+@foreach($likes as $like)
+
+
+ Liked
a post by
+
+ @isset($like->author_url)
+ {{ $like->author_name }}
+ @else
+ {{ $like->author_name }}
+ @endisset
+ :
+
+ {!! $like->content !!}
+
+
+
+@endforeach
+
+@stop
diff --git a/resources/views/likes/show.blade.php b/resources/views/likes/show.blade.php
new file mode 100644
index 00000000..ca06c018
--- /dev/null
+++ b/resources/views/likes/show.blade.php
@@ -0,0 +1,23 @@
+@extends('master')
+
+@section('title')
+Like «
+@stop
+
+@section('content')
+
+
+ Liked
a post by
+
+ @isset($like->author_url)
+ {{ $like->author_name }}
+ @else
+ {{ $like->author_name }}
+ @endisset
+ :
+
+ {!! $like->content !!}
+
+
+
+@stop
diff --git a/routes/web.php b/routes/web.php
index ebb209a0..ae1713bb 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -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');
diff --git a/tests/Feature/LikesTest.php b/tests/Feature/LikesTest.php
new file mode 100644
index 00000000..e33f3d12
--- /dev/null
+++ b/tests/Feature/LikesTest.php
@@ -0,0 +1,88 @@
+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 = <<
+
+
+
+ A post that I like.
+
+ by
Fred Bloggs
+
+
+