Merge branch 'release/0.9'

This commit is contained in:
Jonny Barnes 2017-10-06 16:05:21 +01:00
commit b33c0cca08
27 changed files with 480 additions and 93 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 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') {

View file

@ -22,14 +22,13 @@ class NotesController extends Controller
return (new ActivityStreamsService)->siteOwnerResponse();
}
$notes = Note::orderBy('id', 'desc')
$notes = Note::latest()
->with('place', 'media', 'client')
->withCount(['webmentions As replies' => function ($query) {
$query->where('type', 'in-reply-to');
}])->paginate(10);
$aslink = config('app.url');
return view('notes.index', compact('notes', 'aslink'));
return view('notes.index', compact('notes'));
}
/**
@ -46,9 +45,7 @@ class NotesController extends Controller
return (new ActivityStreamsService)->singleNoteResponse($note);
}
$aslink = $note->longurl;
return view('notes.show', compact('note', 'aslink'));
return view('notes.show', compact('note'));
}
/**

View file

@ -38,6 +38,7 @@ class Kernel extends HttpKernel
\App\Http\Middleware\LinkHeadersMiddleware::class,
//\App\Http\Middleware\DevTokenMiddleware::class,
\App\Http\Middleware\LocalhostSessionMiddleware::class,
\App\Http\Middleware\ActivityStreamLinks::class,
],
'api' => [

View file

@ -0,0 +1,28 @@
<?php
namespace App\Http\Middleware;
use Closure;
class ActivityStreamLinks
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
$response = $next($request);
if ($request->path() === '/') {
$response->header('Link', '<' . config('app.url') . '>; rel="application/activity+json"', false);
}
if ($request->is('notes/*')) {
$response->header('Link', '<' . $request->url() . '>; rel="application/activity+json"', false);
}
return $response;
}
}

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

View file

@ -40,6 +40,7 @@ class SyndicateToFacebook implements ShouldQueue
'form_params' => [
'source' => $this->note->longurl,
'target' => 'https://brid.gy/publish/facebook',
'bridgy_omit_link' => 'maybe',
],
]
);

View file

@ -41,6 +41,7 @@ class SyndicateToTwitter implements ShouldQueue
'form_params' => [
'source' => $this->note->longurl,
'target' => 'https://brid.gy/publish/twitter',
'bridgy_omit_link' => 'maybe',
],
]
);

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,13 @@
# Changelog
## Version 0.9 (2017-10-06)
- Add support for `likes` (issue#69)
- Only included links on truncated syndicated notes https://brid.gy/about#omit-link
## Version 0.8.1 (2017-09-16)
- Order notes by latest (issue#70)
- AcitivtyStream support is now indicated with HTTP Link headers
## Version 0.8 (2017-09-16)
- Improve embedding of tweets (issue#66)
- Allow for “responsive” images (issue#62)

20
composer.lock generated
View file

@ -8,16 +8,16 @@
"packages": [
{
"name": "aws/aws-sdk-php",
"version": "3.36.7",
"version": "3.36.9",
"source": {
"type": "git",
"url": "https://github.com/aws/aws-sdk-php.git",
"reference": "421088947540b1c7956cd693b032124e2c74eb76"
"reference": "7b89fa65cccb966da1599b715dcea8c09eafc175"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/421088947540b1c7956cd693b032124e2c74eb76",
"reference": "421088947540b1c7956cd693b032124e2c74eb76",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/7b89fa65cccb966da1599b715dcea8c09eafc175",
"reference": "7b89fa65cccb966da1599b715dcea8c09eafc175",
"shasum": ""
},
"require": {
@ -84,7 +84,7 @@
"s3",
"sdk"
],
"time": "2017-09-13T18:56:17+00:00"
"time": "2017-09-15T19:12:04+00:00"
},
{
"name": "barnabywalters/mf-cleaner",
@ -2085,16 +2085,16 @@
},
{
"name": "laravel/scout",
"version": "v3.0.9",
"version": "v3.0.10",
"source": {
"type": "git",
"url": "https://github.com/laravel/scout.git",
"reference": "84762c8ed51cb57f09b5f465e09993e48baf9d55"
"reference": "681c15a26bbc973528af2e77e0bb61981dc07206"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/scout/zipball/84762c8ed51cb57f09b5f465e09993e48baf9d55",
"reference": "84762c8ed51cb57f09b5f465e09993e48baf9d55",
"url": "https://api.github.com/repos/laravel/scout/zipball/681c15a26bbc973528af2e77e0bb61981dc07206",
"reference": "681c15a26bbc973528af2e77e0bb61981dc07206",
"shasum": ""
},
"require": {
@ -2146,7 +2146,7 @@
"laravel",
"search"
],
"time": "2017-09-13T18:24:31+00:00"
"time": "2017-09-14T12:32:30+00:00"
},
{
"name": "laravel/tinker",

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(WebMentionsTableSeeder::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()
{
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 aarons 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',

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

@ -12,8 +12,6 @@
<link rel="alternate" type="application/rss+xml" title="Notes RSS Feed" href="/notes/feed.rss">
<link rel="alternate" type="application/atom+xml" title="Notes Atom Feed" href="/notes/feed.atom">
<link rel="alternate" type="application/json" title="Notes JSON Feed" href="/notes/feed.json">
@isset($aslink) <link rel="alternate" type="application/activity+json" href="{{ $aslink }}">
@endisset
<link rel="openid.server" href="https://indieauth.com/openid">
<link rel="openid.delegate" href="{{ config('app.url') }}">
<link rel="authorization_endpoint" href="https://indieauth.com/auth">

View file

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

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

View file

@ -38,6 +38,7 @@ class MicropubClientControllerTest extends TestCase
'mp-syndicate-to' => ['https://twitter.com/jonnybarnes', 'https://facebook.com/jonnybarnes'],
]
);
$expected = '{"type":["h-entry"],"properties":{"content":["Hello Fred"],"in-reply-to":["https:\/\/fredbloggs.com\/note\/abc"],"mp-syndicate-to":["https:\/\/twitter.com\/jonnybarnes","https:\/\/facebook.com\/jonnybarnes"]}}';
foreach ($container as $transaction) {

View file

@ -3,13 +3,14 @@
namespace Tests\Feature;
use Tests\TestCase;
use Tests\TestToken;
use Lcobucci\JWT\Builder;
use Lcobucci\JWT\Signer\Hmac\Sha256;
use Illuminate\Foundation\Testing\DatabaseTransactions;
class MicropubControllerTest extends TestCase
{
use DatabaseTransactions;
use DatabaseTransactions, TestToken;
/**
* Test a GET request for the micropub endpoint without a token gives a
@ -314,42 +315,4 @@ class MicropubControllerTest extends TestCase
'swarm_url' => 'https://www.swarmapp.com/checkin/123'
]);
}
/**
* Generate a valid token to be used in the tests.
*
* @return Lcobucci\JWT\Token\Plain $token
*/
private 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;
}
/**
* Generate an invalid token to be used in the tests.
*
* @return Lcobucci\JWT\Token\Plain $token
*/
private 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;
}
}

View file

@ -3,13 +3,14 @@
namespace Tests\Feature;
use Tests\TestCase;
use Tests\TestToken;
use Lcobucci\JWT\Builder;
use Lcobucci\JWT\Signer\Hmac\Sha256;
use Illuminate\Foundation\Testing\DatabaseTransactions;
class OwnYourGramTest extends TestCase
{
use DatabaseTransactions;
use DatabaseTransactions, TestToken;
public function test_ownyourgram_post()
{
@ -40,18 +41,4 @@ class OwnYourGramTest extends TestCase
'instagram_url' => 'https://www.instagram.com/p/BVC_nVTBFfi/'
]);
}
private function getToken()
{
$signer = new Sha256();
$token = (new Builder())
->set('client_id', 'https://ownyourgram.com')
->set('me', config('app.url'))
->set('scope', 'create')
->set('issued_at', time())
->sign($signer, env('APP_KEY'))
->getToken();
return $token;
}
}

View file

@ -3,13 +3,14 @@
namespace Tests\Feature;
use Tests\TestCase;
use Tests\TestToken;
use Lcobucci\JWT\Builder;
use Lcobucci\JWT\Signer\Hmac\Sha256;
use Illuminate\Foundation\Testing\DatabaseTransactions;
class SwarmTest extends TestCase
{
use DatabaseTransactions;
use DatabaseTransactions, TestToken;
public function test_faked_ownyourswarm_request()
{
@ -82,23 +83,4 @@ class SwarmTest extends TestCase
'swarm_url' => 'https://www.swarmapp.com/checkin/def'
]);
}
/**
* Generate a valid token to be used in the tests.
*
* @return Lcobucci\JWT\Token\Plain $token
*/
private function getToken()
{
$signer = new Sha256();
$token = (new Builder())
->set('client_id', 'https://ownyourswarm.p3k.io')
->set('me', 'https://jonnybarnes.localhost')
->set('scope', 'create update')
->set('issued_at', time())
->sign($signer, env('APP_KEY'))
->getToken();
return $token;
}
}

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