Merge branch 'develop' of github.com:jonnybarnes/jonnybarnes.uk into develop

This commit is contained in:
Jonny Barnes 2017-09-24 12:40:08 +01:00
commit 3be0a6c3a7
64 changed files with 1162 additions and 259 deletions

View file

@ -16,6 +16,7 @@ addons:
- nginx
- realpath
- postgresql-9.6-postgis-2.3
- imagemagick
artifacts:
s3_region: "eu-west-1"
paths:
@ -33,8 +34,10 @@ env:
php:
- 7.1
- 7.2
before_install:
- printf "\n" | pecl install imagick
- cp .env.travis .env
- echo 'error_log = "/tmp/php.error.log"' >> ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/travis.ini
- psql -U travis -c 'create database travis_ci_test'

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

@ -2,10 +2,14 @@
namespace App\Http\Controllers;
use Storage;
use Monolog\Logger;
use Ramsey\Uuid\Uuid;
use App\{Media, Note, Place};
use App\Jobs\ProcessImage;
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;
use Phaza\LaravelPostgis\Geometries\Point;
@ -70,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') {
@ -90,7 +102,9 @@ class MicropubController extends Controller
if (is_array($request->input('properties.location.0'))) {
if ($request->input('properties.location.0.type.0' === 'h-card')) {
try {
$place = $this->placeService->createPlaceFromCheckin($request->input('properties.location.0'));
$place = $this->placeService->createPlaceFromCheckin(
$request->input('properties.location.0')
);
$data['checkin'] = $place->longurl;
} catch (\Exception $e) {
//
@ -102,7 +116,9 @@ class MicropubController extends Controller
if (array_key_exists('checkin', $request->input('properties'))) {
$data['swarm-url'] = $request->input('properties.syndication.0');
try {
$place = $this->placeService->createPlaceFromCheckin($request->input('properties.checkin.0'));
$place = $this->placeService->createPlaceFromCheckin(
$request->input('properties.checkin.0')
);
$data['checkin'] = $place->longurl;
} catch (\Exception $e) {
$data['checkin'] = null;
@ -395,8 +411,11 @@ class MicropubController extends Controller
'error_description' => 'A problem occured handling your request',
], 500);
}
$size = $request->file('file')->getClientSize();
Storage::disk('local')->put($filename, $request->file('file')->openFile()->fread($size));
try {
$path = $request->file('file')->storeAs('media', $filename, 's3');
Storage::disk('s3')->put('media/' . $filename, $request->file('file')->openFile()->fread($size));
} catch (Exception $e) { // which exception?
return response()->json([
'response' => 'error',
@ -404,12 +423,25 @@ class MicropubController extends Controller
'error_description' => 'Unable to save media to S3',
], 503);
}
$manager = app()->make(ImageManager::class);
try {
$image = $manager->make($request->file('file'));
$width = $image->width();
} catch (\Intervention\Image\Exception\NotReadableException $exception) {
// not an image
$width = null;
}
$media = new Media();
$media->token = $request->bearerToken();
$media->path = $path;
$media->path = 'media/' . $filename;
$media->type = $this->getFileTypeFromMimeType($request->file('file')->getMimeType());
$media->image_widths = $width;
$media->save();
dispatch(new ProcessImage($filename));
return response()->json([
'response' => 'created',
'location' => $media->url,

View file

@ -5,20 +5,24 @@ namespace App\Http\Controllers;
use App\Note;
use Illuminate\Http\Request;
use Jonnybarnes\IndieWeb\Numbers;
use App\Services\ActivityStreamsService;
// Need to sort out Twitter and webmentions!
class NotesController extends Controller
{
/**
* Show all the notes.
* Show all the notes. This is also the homepage.
*
* @param Illuminate\Http\Request request;
* @return \Illuminte\View\Factory view
*/
public function index(Request $request)
public function index()
{
$notes = Note::orderBy('id', 'desc')
if (request()->wantsActivityStream()) {
return (new ActivityStreamsService)->siteOwnerResponse();
}
$notes = Note::latest()
->with('place', 'media', 'client')
->withCount(['webmentions As replies' => function ($query) {
$query->where('type', 'in-reply-to');
@ -37,6 +41,10 @@ class NotesController extends Controller
{
$note = Note::nb60($urlId)->with('webmentions')->firstOrFail();
if (request()->wantsActivityStream()) {
return (new ActivityStreamsService)->singleNoteResponse($note);
}
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;
}
}

68
app/Jobs/ProcessImage.php Normal file
View file

@ -0,0 +1,68 @@
<?php
namespace App\Jobs;
use Storage;
use Illuminate\Bus\Queueable;
use Intervention\Image\ImageManager;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Intervention\Image\Exception\NotReadableException;
class ProcessImage implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $filename;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct(string $filename)
{
$this->filename = $filename;
}
/**
* Execute the job.
*
* @return void
*/
public function handle(ImageManager $manager)
{
//open file
try {
$image = $manager->make(storage_path('app') . '/' . $this->filename);
} catch (NotReadableException $exception) {
// not an image; delete file and end job
unlink(storage_path('app') . '/' . $this->filename);
return;
}
//create smaller versions if necessary
if ($image->width() >= 1000) {
$filenameParts = explode('.', $this->filename);
$extension = array_pop($filenameParts);
// the following acheives this data flow
// foo.bar.png => ['foo', 'bar', 'png'] => ['foo', 'bar'] => foo.bar
$basename = ltrim(array_reduce($filenameParts, function ($carry, $item) {
return $carry . '.' . $item;
}, ''), '.');
$medium = $image->resize(1000, null, function ($constraint) {
$constraint->aspectRatio();
});
Storage::disk('s3')->put('media/'. $basename . '-medium.' . $extension, (string) $medium->encode());
$small = $image->resize(500, null, function ($constraint) {
$constraint->aspectRatio();
});
Storage::disk('s3')->put('media/' . $basename . '-small.' . $extension, (string) $small->encode());
}
// now we can delete the locally saved image
unlink(storage_path('app') . '/' . $this->filename);
}
}

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

@ -41,4 +41,40 @@ class Media extends Model
return config('filesystems.disks.s3.url') . '/' . $this->path;
}
/**
* Get the URL for the medium size of an S3 image file.
*
* @return string
*/
public function getMediumurlAttribute()
{
$filenameParts = explode('.', $this->path);
$extension = array_pop($filenameParts);
// the following acheives this data flow
// foo.bar.png => ['foo', 'bar', 'png'] => ['foo', 'bar'] => foo.bar
$basename = ltrim(array_reduce($filenameParts, function ($carry, $item) {
return $carry . '.' . $item;
}, ''), '.');
return config('filesystems.disks.s3.url') . '/' . $basename . '-medium.' . $extension;
}
/**
* Get the URL for the small size of an S3 image file.
*
* @return string
*/
public function getSmallurlAttribute()
{
$filenameParts = explode('.', $this->path);
$extension = array_pop($filenameParts);
// the following acheives this data flow
// foo.bar.png => ['foo', 'bar', 'png'] => ['foo', 'bar'] => foo.bar
$basename = ltrim(array_reduce($filenameParts, function ($carry, $item) {
return $carry . '.' . $item;
}, ''), '.');
return config('filesystems.disks.s3.url') . '/' . $basename . '-small.' . $extension;
}
}

View file

@ -280,8 +280,8 @@ class Note extends Model
try {
$oEmbed = Twitter::getOembed([
'id' => $tweetId,
'dnt' => true,
'align' => 'center',
'omit_script' => true,
'maxwidth' => 550,
]);
} catch (\Exception $e) {
@ -368,7 +368,8 @@ class Note extends Model
return $matches[0];
}
if ($contact->facebook) {
return '<a class="u-category h-card" href="https://facebook.com/' . $contact->facebook . '">' . $contact->name . '</a>';
return '<a class="u-category h-card" href="https://facebook.com/'
. $contact->facebook . '">' . $contact->name . '</a>';
}
return $contact->name;

View file

@ -67,7 +67,8 @@ class Place extends Model
public function scopeNear(Builder $query, Point $point, $distance = 1000)
{
$field = DB::raw(
sprintf("ST_Distance(%s.location, ST_GeogFromText('%s'))",
sprintf(
"ST_Distance(%s.location, ST_GeogFromText('%s'))",
$this->getTable(),
$point->toWKT()
)

View file

@ -5,6 +5,7 @@ namespace App\Providers;
use App\Tag;
use App\Note;
use Validator;
use Illuminate\Http\Request;
use Laravel\Dusk\DuskServiceProvider;
use Illuminate\Support\ServiceProvider;
@ -42,10 +43,20 @@ class AppServiceProvider extends ServiceProvider
$tag = Tag::firstOrCreate(['tag' => $tag]);
$tagsToAdd[] = $tag->id;
}
if (count($tagsToAdd > 0)) {
if (count($tagsToAdd) > 0) {
$note->tags()->attach($tagsToAdd);
}
});
// Request AS macro
Request::macro('wantsActivityStream', function () {
return str_contains(mb_strtolower($this->header('Accept')), 'application/activity+json');
});
// configure Intervention/Image
$this->app->bind('Intervention\Image\ImageManager', function () {
return new \Intervention\Image\ImageManager(['driver' => config('image.driver')]);
});
}
/**

View file

@ -0,0 +1,50 @@
<?php
namespace App\Services;
use App\Note;
class ActivityStreamsService
{
public function siteOwnerResponse()
{
$data = json_encode([
'@context' => 'https://www.w3.org/ns/activitystreams',
'id' => config('app.url'),
'type' => 'Person',
'name' => config('user.displayname'),
'preferredUsername' => config('user.username'),
]);
return response($data)->header('Content-Type', 'application/activity+json');
}
public function singleNoteResponse(Note $note)
{
$data = json_encode([
'@context' => 'https://www.w3.org/ns/activitystreams',
'summary' => strtok(config('user.displayname'), ' ') . ' added a note to their microblog',
'type' => 'Add',
'published' => $note->updated_at->toW3cString(),
'actor' => [
'type' => 'Person',
'id' => config('app.url'),
'name' => config('app.display_name'),
'url' => config('app.url'),
'image' => [
'type' => 'Link',
'href' => config('app.url') . '/assets/img/profile.jpg',
'mediaType' => '/image/jpeg',
],
],
'object' => [
'id' => $note->longurl,
'type' => 'Note',
'url' => $note->longurl,
'name' => strip_tags($note->note),
],
]);
return response($data)->header('Content-Type', 'application/activity+json');
}
}

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

@ -37,7 +37,7 @@ class NoteService
);
if (array_key_exists('published', $data) && empty($data['published']) === false) {
$carbon = new \Carbon\Carbon($data['published']);
$carbon = carbon($data['published']);
$note->created_at = $note->updated_at = $carbon->toDateTimeString();
}

View file

@ -5,7 +5,6 @@ namespace App;
use Cache;
use Twitter;
use HTMLPurifier;
use Carbon\Carbon;
use HTMLPurifier_Config;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Database\Eloquent\Model;
@ -63,10 +62,9 @@ class WebMention extends Model
public function getPublishedAttribute()
{
$microformats = json_decode($this->mf2, true);
$carbon = new Carbon();
if (isset($microformats['items'][0]['properties']['published'][0])) {
try {
$published = $carbon->parse(
$published = carbon()->parse(
$microformats['items'][0]['properties']['published'][0]
)->toDayDateTimeString();
} catch (\Exception $exception) {

View file

@ -1,5 +1,26 @@
# Changelog
## Version {next}
- 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)
## Version 0.7.3 (2017-09-13)
- Fix a test
## Version 0.7.2 (2017-09-13)
- Small AS2.0 improvements
## Version 0.7.1 (2017-09-13)
- Add content-negotiated AS data for homepage and single notes
## Version 0.7 (2017-09-08)
- Add Laravel Horizon

Some files were not shown because too many files have changed in this diff Show more