Merge pull request #639 from jonnybarnes/develop

MTM: Recent work
This commit is contained in:
Jonny Barnes 2023-02-04 12:22:23 +00:00 • committed by GitHub
commit 054bba1da9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
48 changed files with 1909 additions and 2459 deletions

1
.gitignore vendored
View file

@ -11,6 +11,7 @@ yarn-error.log
/lsp
.phpstorm.meta.php
_ide_helper.php
ray.php
# Custom paths in /public
/public/coverage
/public/hot

View file

@ -22,6 +22,7 @@ class Kernel extends ConsoleKernel
*
* @param \Illuminate\Console\Scheduling\Schedule $schedule
* @return void
*
* @codeCoverageIgnore
*/
protected function schedule(Schedule $schedule)

View file

@ -53,7 +53,7 @@ class Handler extends ExceptionHandler
{
parent::report($throwable);
if ($this->shouldReport($throwable)) {
if (config('logging.slack') && $this->shouldReport($throwable)) {
$guzzle = new Client([
'headers' => [
'Content-Type' => 'application/json',

View file

@ -41,13 +41,14 @@ class NotesController extends Controller
/**
* Process a request to make a new note.
*
* @return \Illuminate\Http\RedirectResponse
* @param Request $request
* @return RedirectResponse
*/
public function store(): RedirectResponse
public function store(Request $request): RedirectResponse
{
Note::create([
'in-reply-to' => request()->input('in-reply-to'),
'note' => request()->input('content'),
'in_reply_to' => $request->input('in-reply-to'),
'note' => $request->input('content'),
]);
return redirect('/admin/notes');

View file

@ -111,7 +111,7 @@ class FeedsController extends Controller
*/
public function notesJson()
{
$notes = Note::latest()->take(20)->get();
$notes = Note::latest()->with('media')->take(20)->get();
$data = [
'version' => 'https://jsonfeed.org/version/1',
'title' => 'The JSON Feed for ' . config('app.display_name') . '’s notes',

View file

@ -23,7 +23,7 @@ class FrontPageController extends Controller
return (new ActivityStreamsService())->siteOwnerResponse();
}
$notes = Note::latest()->get();
$notes = Note::latest()->with(['media', 'client', 'place'])->get();
$articles = Article::latest()->get();
$bookmarks = Bookmark::latest()->get();
$likes = Like::latest()->get();

View file

@ -52,7 +52,7 @@ class MicropubController extends Controller
{
try {
$tokenData = $this->tokenService->validateToken($request->input('access_token'));
} catch (RequiredConstraintsViolated | InvalidTokenStructure | CannotDecodeContent) {
} catch (RequiredConstraintsViolated|InvalidTokenStructure|CannotDecodeContent) {
$micropubResponses = new MicropubResponses();
return $micropubResponses->invalidTokenResponse();
@ -124,7 +124,7 @@ class MicropubController extends Controller
{
try {
$tokenData = $this->tokenService->validateToken(request()->input('access_token'));
} catch (RequiredConstraintsViolated | InvalidTokenStructure) {
} catch (RequiredConstraintsViolated|InvalidTokenStructure) {
return (new MicropubResponses())->invalidTokenResponse();
}

View file

@ -37,7 +37,7 @@ class MicropubMediaController extends Controller
{
try {
$tokenData = $this->tokenService->validateToken($request->input('access_token'));
} catch (RequiredConstraintsViolated | InvalidTokenStructure) {
} catch (RequiredConstraintsViolated|InvalidTokenStructure) {
$micropubResponses = new MicropubResponses();
return $micropubResponses->invalidTokenResponse();
@ -75,7 +75,7 @@ class MicropubMediaController extends Controller
return [
'url' => $mediaItem->url,
'published' => $mediaItem->created_at->toW3cString(),
'mime_type' => $mediaItem->getMimeType(),
'mime_type' => $mediaItem->mimetype,
];
});
@ -107,7 +107,7 @@ class MicropubMediaController extends Controller
{
try {
$tokenData = $this->tokenService->validateToken(request()->input('access_token'));
} catch (RequiredConstraintsViolated | InvalidTokenStructure $exception) {
} catch (RequiredConstraintsViolated|InvalidTokenStructure $exception) {
$micropubResponses = new MicropubResponses();
return $micropubResponses->invalidTokenResponse();

View file

@ -20,8 +20,7 @@ class ProcessBookmark implements ShouldQueue
use Queueable;
use SerializesModels;
/** @var Bookmark */
protected $bookmark;
protected Bookmark $bookmark;
/**
* Create a new job instance.
@ -38,14 +37,13 @@ class ProcessBookmark implements ShouldQueue
*
* @return void
*/
public function handle()
public function handle(): void
{
$uuid = (resolve(BookmarkService::class))->saveScreenshot($this->bookmark->url);
$this->bookmark->screenshot = $uuid;
SaveScreenshot::dispatch($this->bookmark);
try {
$archiveLink = (resolve(BookmarkService::class))->getArchiveLink($this->bookmark->url);
} catch (InternetArchiveException $e) {
} catch (InternetArchiveException) {
$archiveLink = null;
}
$this->bookmark->archive = $archiveLink;

109
app/Jobs/SaveScreenshot.php Executable file
View file

@ -0,0 +1,109 @@
<?php
declare(strict_types=1);
namespace App\Jobs;
use App\Models\Bookmark;
use GuzzleHttp\Client;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Storage;
use JsonException;
class SaveScreenshot implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
private Bookmark $bookmark;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct(Bookmark $bookmark)
{
$this->bookmark = $bookmark;
}
/**
* Execute the job.
*
* @return void
*
* @throws JsonException
*/
public function handle(): void
{
// A normal Guzzle client
$client = resolve(Client::class);
// A Guzzle client with a custom Middleware to retry the CloudConvert API requests
$retryClient = resolve('RetryGuzzle');
// First request that CloudConvert takes a screenshot of the URL
$takeScreenshotJobResponse = $client->request('POST', 'https://api.cloudconvert.com/v2/capture-website', [
'headers' => [
'Authorization' => 'Bearer ' . config('services.cloudconvert.token'),
],
'json' => [
'url' => $this->bookmark->url,
'output_format' => 'png',
'screen_width' => 1440,
'screen_height' => 900,
'wait_until' => 'networkidle0',
'wait_time' => 100,
],
]);
$taskId = json_decode($takeScreenshotJobResponse->getBody()->getContents(), false, 512, JSON_THROW_ON_ERROR)->data->id;
// Now wait till the status job is finished
$screenshotJobStatusResponse = $retryClient->request('GET', 'https://api.cloudconvert.com/v2/tasks/' . $taskId, [
'headers' => [
'Authorization' => 'Bearer ' . config('services.cloudconvert.token'),
],
'query' => [
'include' => 'payload',
],
]);
$finishedCaptureId = json_decode($screenshotJobStatusResponse->getBody()->getContents(), false, 512, JSON_THROW_ON_ERROR)->data->id;
// Now we can create a new job to request thst the screenshot is exported to a temporary URL we can download the screenshot from
$exportImageJob = $client->request('POST', 'https://api.cloudconvert.com/v2/export/url', [
'headers' => [
'Authorization' => 'Bearer ' . config('services.cloudconvert.token'),
],
'json' => [
'input' => $finishedCaptureId,
'archive_multiple_files' => false,
],
]);
$exportImageJobId = json_decode($exportImageJob->getBody()->getContents(), false, 512, JSON_THROW_ON_ERROR)->data->id;
// Again, wait till the status of this export job is finished
$finalImageUrlResponse = $retryClient->request('GET', 'https://api.cloudconvert.com/v2/tasks/' . $exportImageJobId, [
'headers' => [
'Authorization' => 'Bearer ' . config('services.cloudconvert.token'),
],
'query' => [
'include' => 'payload',
],
]);
// Now we can download the screenshot and save it to the storage
$finalImageUrl = json_decode($finalImageUrlResponse->getBody()->getContents(), false, 512, JSON_THROW_ON_ERROR)->data->result->files[0]->url;
$finalImageUrlContent = $client->request('GET', $finalImageUrl);
Storage::disk('public')->put('/assets/img/bookmarks/' . $taskId . '.png', $finalImageUrlContent->getBody()->getContents());
$this->bookmark->screenshot = $taskId;
$this->bookmark->save();
}
}

View file

@ -6,8 +6,8 @@ namespace App\Jobs;
use App\Models\Note;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\Psr7\Header;
use GuzzleHttp\Psr7\Uri;
use GuzzleHttp\Psr7\UriResolver;
use GuzzleHttp\Psr7\Utils;
use Illuminate\Bus\Queueable;
@ -22,8 +22,7 @@ class SendWebMentions implements ShouldQueue
use Queueable;
use SerializesModels;
/** @var Note */
protected $note;
protected Note $note;
/**
* Create the job instance, inject dependencies.
@ -39,15 +38,14 @@ class SendWebMentions implements ShouldQueue
* Execute the job.
*
* @return void
*
* @throws GuzzleException
*/
public function handle()
public function handle(): void
{
//grab the URLs
$inReplyTo = $this->note->in_reply_to ?? '';
// above so explode doesn’t complain about null being passed in
$urlsInReplyTo = explode(' ', $inReplyTo);
$urlsInReplyTo = explode(' ', $this->note->in_reply_to ?? '');
$urlsNote = $this->getLinks($this->note->note);
$urls = array_filter(array_merge($urlsInReplyTo, $urlsNote)); //filter out none URLs
$urls = array_filter(array_merge($urlsInReplyTo, $urlsNote));
foreach ($urls as $url) {
$endpoint = $this->discoverWebmentionEndpoint($url);
if ($endpoint !== null) {
@ -67,10 +65,12 @@ class SendWebMentions implements ShouldQueue
*
* @param string $url
* @return string|null
*
* @throws GuzzleException
*/
public function discoverWebmentionEndpoint(string $url): ?string
{
//let’s not send webmentions to myself
// let’s not send webmentions to myself
if (parse_url($url, PHP_URL_HOST) === config('app.longurl')) {
return null;
}
@ -80,6 +80,7 @@ class SendWebMentions implements ShouldQueue
$endpoint = null;
/** @var Client $guzzle */
$guzzle = resolve(Client::class);
$response = $guzzle->get($url);
//check HTTP Headers for webmention endpoint
@ -134,8 +135,6 @@ class SendWebMentions implements ShouldQueue
/**
* Resolve a URI if necessary.
*
* @todo Update deprecated resolve method
*
* @param string $url
* @param string $base The base of the URL
* @return string

View file

@ -6,6 +6,7 @@ namespace App\Models;
use Cviebrock\EloquentSluggable\Sluggable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
@ -52,76 +53,65 @@ class Article extends Model
}
/**
* We shall set a blacklist of non-modifiable model attributes.
* The attributes that are mass assignable.
*
* @var array
* @var array<int, string>
*/
protected $guarded = ['id'];
protected $fillable = [
'url',
'title',
'main',
'published',
];
/**
* Process the article for display.
*
* @return string
*/
public function getHtmlAttribute(): string
protected function html(): Attribute
{
$environment = new Environment();
$environment->addExtension(new CommonMarkCoreExtension());
$environment->addRenderer(FencedCode::class, new FencedCodeRenderer());
$environment->addRenderer(IndentedCode::class, new IndentedCodeRenderer());
$markdownConverter = new MarkdownConverter($environment);
return Attribute::get(
get: function () {
$environment = new Environment();
$environment->addExtension(new CommonMarkCoreExtension());
$environment->addRenderer(FencedCode::class, new FencedCodeRenderer());
$environment->addRenderer(IndentedCode::class, new IndentedCodeRenderer());
$markdownConverter = new MarkdownConverter($environment);
return $markdownConverter->convert($this->main)->getContent();
return $markdownConverter->convert($this->main)->getContent();
},
);
}
/**
* Convert updated_at to W3C time format.
*
* @return string
*/
public function getW3cTimeAttribute(): string
protected function w3cTime(): Attribute
{
return $this->updated_at->toW3CString();
return Attribute::get(
get: fn () => $this->updated_at->toW3CString(),
);
}
/**
* Convert updated_at to a tooltip appropriate format.
*
* @return string
*/
public function getTooltipTimeAttribute(): string
protected function tooltipTime(): Attribute
{
return $this->updated_at->toRFC850String();
return Attribute::get(
get: fn () => $this->updated_at->toRFC850String(),
);
}
/**
* Convert updated_at to a human readable format.
*
* @return string
*/
public function getHumanTimeAttribute(): string
protected function humanTime(): Attribute
{
return $this->updated_at->diffForHumans();
return Attribute::get(
get: fn () => $this->updated_at->diffForHumans(),
);
}
/**
* Get the pubdate value for RSS feeds.
*
* @return string
*/
public function getPubdateAttribute(): string
protected function pubdate(): Attribute
{
return $this->updated_at->toRSSString();
return Attribute::get(
get: fn () => $this->updated_at->toRSSString(),
);
}
/**
* A link to the article, i.e. `/blog/1999/12/25/merry-christmas`.
*
* @return string
*/
public function getLinkAttribute(): string
protected function link(): Attribute
{
return '/blog/' . $this->updated_at->year . '/' . $this->updated_at->format('m') . '/' . $this->titleurl;
return Attribute::get(
get: fn () => '/blog/' . $this->updated_at->year . '/' . $this->updated_at->format('m') . '/' . $this->titleurl,
);
}
/**
@ -134,7 +124,7 @@ class Article extends Model
*/
public function scopeDate(Builder $query, int $year = null, int $month = null): Builder
{
if ($year == null) {
if ($year === null) {
return $query;
}
$start = $year . '-01-01 00:00:00';

View file

@ -4,43 +4,11 @@ declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Support\Carbon;
/**
* App\Models\Bookmark.
*
* @property int $id
* @property string $url
* @property string|null $name
* @property string|null $content
* @property string|null $screenshot
* @property string|null $archive
* @property array|null $syndicates
* @property Carbon|null $created_at
* @property Carbon|null $updated_at
* @property-read string $longurl
* @property-read Collection|Tag[] $tags
* @property-read int|null $tags_count
*
* @method static Builder|Bookmark newModelQuery()
* @method static Builder|Bookmark newQuery()
* @method static Builder|Bookmark query()
* @method static Builder|Bookmark whereArchive($value)
* @method static Builder|Bookmark whereContent($value)
* @method static Builder|Bookmark whereCreatedAt($value)
* @method static Builder|Bookmark whereId($value)
* @method static Builder|Bookmark whereName($value)
* @method static Builder|Bookmark whereScreenshot($value)
* @method static Builder|Bookmark whereSyndicates($value)
* @method static Builder|Bookmark whereUpdatedAt($value)
* @method static Builder|Bookmark whereUrl($value)
* @mixin Eloquent
*/
class Bookmark extends Model
{
use HasFactory;
@ -71,13 +39,10 @@ class Bookmark extends Model
return $this->belongsToMany('App\Models\Tag');
}
/**
* The full url of a bookmark.
*
* @return string
*/
public function getLongurlAttribute(): string
protected function longurl(): Attribute
{
return config('app.url') . '/bookmarks/' . $this->id;
return Attribute::get(
get: fn () => config('app.url') . '/bookmarks/' . $this->id,
);
}
}

View file

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace App\Models;
use App\Traits\FilterHtml;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Arr;
@ -17,46 +18,38 @@ class Like extends Model
protected $fillable = ['url'];
/**
* Normalize the URL of a Like.
*
* @param string $value The provided URL
*/
public function setUrlAttribute(string $value)
protected function url(): Attribute
{
$this->attributes['url'] = normalize_url($value);
return Attribute::set(
set: fn ($value) => normalize_url($value),
);
}
/**
* Normalize the URL of the author of the like.
*
* @param string|null $value The author’s url
*/
public function setAuthorUrlAttribute(?string $value)
protected function authorUrl(): Attribute
{
$this->attributes['author_url'] = normalize_url($value);
return Attribute::set(
set: fn ($value) => normalize_url($value),
);
}
/**
* If the content contains HTML, filter it.
*
* @param string|null $value The content of the like
* @return string|null
*/
public function getContentAttribute(?string $value): ?string
protected function content(): Attribute
{
if ($value === null) {
return null;
}
return Attribute::get(
get: function ($value, $attributes) {
if ($value === null) {
return null;
}
$mf2 = Mf2\parse($value, $this->url);
$mf2 = Mf2\parse($value, $attributes['url']);
if (Arr::get($mf2, 'items.0.properties.content.0.html')) {
return $this->filterHtml(
$mf2['items'][0]['properties']['content'][0]['html']
);
}
if (Arr::get($mf2, 'items.0.properties.content.0.html')) {
return $this->filterHtml(
$mf2['items'][0]['properties']['content'][0]['html']
);
}
return $value;
return $value;
}
);
}
}

View file

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@ -37,53 +38,63 @@ class Media extends Model
return $this->belongsTo(Note::class);
}
/**
* Get the URL for an S3 media file.
*
* @return string
*/
public function getUrlAttribute(): string
protected function url(): Attribute
{
if (Str::startsWith($this->path, 'https://')) {
return $this->path;
}
return Attribute::get(
get: function ($value, $attributes) {
if (Str::startsWith($attributes['path'], 'https://')) {
return $attributes['path'];
}
return config('filesystems.disks.s3.url') . '/' . $this->path;
return config('filesystems.disks.s3.url') . '/' . $attributes['path'];
}
);
}
/**
* Get the URL for the medium size of an S3 image file.
*
* @return string
*/
public function getMediumurlAttribute(): string
protected function mediumurl(): Attribute
{
$basename = $this->getBasename($this->path);
$extension = $this->getExtension($this->path);
return config('filesystems.disks.s3.url') . '/' . $basename . '-medium.' . $extension;
return Attribute::get(
get: fn ($value, $attributes) => $this->getSizeUrl($attributes['path'], 'medium'),
);
}
/**
* Get the URL for the small size of an S3 image file.
*
* @return string
*/
public function getSmallurlAttribute(): string
protected function smallurl(): Attribute
{
$basename = $this->getBasename($this->path);
$extension = $this->getExtension($this->path);
return config('filesystems.disks.s3.url') . '/' . $basename . '-small.' . $extension;
return Attribute::get(
get: fn ($value, $attributes) => $this->getSizeUrl($attributes['path'], 'small'),
);
}
/**
* Give the real part of a filename, i.e. strip the file extension.
*
* @param string $path
* @return string
*/
public function getBasename(string $path): string
protected function mimetype(): Attribute
{
return Attribute::get(
get: function ($value, $attributes) {
$extension = $this->getExtension($attributes['path']);
return match ($extension) {
'gif' => 'image/gif',
'jpeg', 'jpg' => 'image/jpeg',
'png' => 'image/png',
'svg' => 'image/svg+xml',
'tiff' => 'image/tiff',
'webp' => 'image/webp',
'mp4' => 'video/mp4',
'mkv' => 'video/mkv',
default => 'application/octet-stream',
};
},
);
}
private function getSizeUrl(string $path, string $size): string
{
$basename = $this->getBasename($path);
$extension = $this->getExtension($path);
return config('filesystems.disks.s3.url') . '/' . $basename . '-' . $size . '.' . $extension;
}
private function getBasename(string $path): string
{
// the following achieves this data flow
// foo.bar.png => ['foo', 'bar', 'png'] => ['foo', 'bar'] => foo.bar
@ -95,40 +106,10 @@ class Media extends Model
}, ''), '.');
}
/**
* Get the extension from a given filename.
*
* @param string $path
* @return string
*/
public function getExtension(string $path): string
private function getExtension(string $path): string
{
$parts = explode('.', $path);
return array_pop($parts);
}
/**
* Get the mime type of the media file.
*
* For now we will just use the extension, but this could be improved.
*
* @return string
*/
public function getMimeType(): string
{
$extension = $this->getExtension($this->path);
return match ($extension) {
'gif' => 'image/gif',
'jpeg', 'jpg' => 'image/jpeg',
'png' => 'image/png',
'svg' => 'image/svg+xml',
'tiff' => 'image/tiff',
'webp' => 'image/webp',
'mp4' => 'video/mp4',
'mkv' => 'video/mkv',
default => 'application/octet-stream',
};
}
}

View file

@ -19,7 +19,6 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\Cache;
use JetBrains\PhpStorm\ArrayShape;
use Jonnybarnes\IndieWeb\Numbers;
use League\CommonMark\Environment\Environment;
use League\CommonMark\Extension\Autolink\AutolinkExtension;
@ -141,7 +140,6 @@ class Note extends Model
*
* @return array
*/
#[ArrayShape(['note' => 'null|string'])]
public function toSearchableArray(): array
{
return [
@ -190,7 +188,7 @@ class Note extends Model
/**
* Provide the content_html for JSON feed.
*
* In particular we want to include media links such as images.
* In particular, we want to include media links such as images.
*
* @return string
*/

View file

@ -6,6 +6,7 @@ namespace App\Models;
use Cviebrock\EloquentSluggable\Sluggable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
@ -105,53 +106,46 @@ class Place extends Model
]));
}
/**
* The Long URL for a place.
*
* @return string
*/
public function getLongurlAttribute(): string
protected function longurl(): Attribute
{
return config('app.url') . '/places/' . $this->slug;
return Attribute::get(
get: fn ($value, $attributes) => config('app.url') . '/places/' . $attributes['slug'],
);
}
/**
* The Short URL for a place.
*
* @return string
*/
public function getShorturlAttribute(): string
protected function shorturl(): Attribute
{
return config('app.shorturl') . '/places/' . $this->slug;
return Attribute::get(
get: fn ($value, $attributes) => config('app.shorturl') . '/places/' . $attributes['slug'],
);
}
/**
* This method is an alternative for `longurl`.
*
* @return string
*/
public function getUriAttribute(): string
protected function uri(): Attribute
{
return $this->longurl;
return Attribute::get(
get: fn () => $this->longurl,
);
}
/**
* Dealing with a jsonb column, so we check input first.
*
* @param string|null $url
*/
public function setExternalUrlsAttribute(?string $url)
protected function externalUrls(): Attribute
{
if ($url === null) {
return;
}
$type = $this->getType($url);
$already = [];
if (array_key_exists('external_urls', $this->attributes)) {
$already = json_decode($this->attributes['external_urls'], true);
}
$already[$type] = $url;
$this->attributes['external_urls'] = json_encode($already);
return Attribute::set(
set: function ($value, $attributes) {
if ($value === null) {
return $attributes['external_urls'] ?? null;
}
$type = $this->getType($value);
$already = [];
if (array_key_exists('external_urls', $attributes)) {
$already = json_decode($attributes['external_urls'], true);
}
$already[$type] = $value;
return json_encode($already);
}
);
}
/**

View file

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
@ -40,14 +41,11 @@ class Tag extends Model
return $this->belongsToMany('App\Models\Bookmark');
}
/**
* When creating a Tag model instance, invoke the nomralize method on the tag.
*
* @param string $value
*/
public function setTagAttribute(string $value)
protected function tag(): Attribute
{
$this->attributes['tag'] = $this->normalize($value);
return Attribute::set(
set: fn ($value) => self::normalize($value),
);
}
/**

View file

@ -6,13 +6,14 @@ namespace App\Models;
use App\Traits\FilterHtml;
use Codebird\Codebird;
use Exception;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Support\Facades\Cache;
use Jonnybarnes\WebmentionsParser\Authorship;
use Jonnybarnes\WebmentionsParser\Exceptions\AuthorshipParserException;
class WebMention extends Model
{
@ -43,72 +44,79 @@ class WebMention extends Model
return $this->morphTo();
}
/**
* Get the author of the webmention.
*
* @return array
*
* @throws AuthorshipParserException
*/
public function getAuthorAttribute(): array
protected function author(): Attribute
{
$authorship = new Authorship();
$hCard = $authorship->findAuthor(json_decode($this->mf2, true));
return Attribute::get(
get: function ($value, $attributes) {
if (
! array_key_exists('mf2', $attributes) ||
$attributes['mf2'] === null
) {
return null;
}
if ($hCard === false) {
return [];
}
$authorship = new Authorship();
$hCard = $authorship->findAuthor(json_decode($attributes['mf2'], true));
if (
array_key_exists('properties', $hCard) &&
array_key_exists('photo', $hCard['properties'])
) {
$hCard['properties']['photo'][0] = $this->createPhotoLink($hCard['properties']['photo'][0]);
}
if ($hCard === false) {
return null;
}
return $hCard;
}
if (
array_key_exists('properties', $hCard) &&
array_key_exists('photo', $hCard['properties'])
) {
$hCard['properties']['photo'][0] = $this->createPhotoLink($hCard['properties']['photo'][0]);
}
/**
* Get the published value for the webmention.
*
* @return string|null
*/
public function getPublishedAttribute(): ?string
{
$mf2 = $this->mf2 ?? '';
$microformats = json_decode($mf2, true);
if (isset($microformats['items'][0]['properties']['published'][0])) {
try {
$published = carbon()->parse(
$microformats['items'][0]['properties']['published'][0]
)->toDayDateTimeString();
} catch (\Exception $exception) {
$published = $this->updated_at->toDayDateTimeString();
return $hCard;
}
} else {
$published = $this->updated_at->toDayDateTimeString();
}
return $published;
);
}
/**
* Get the filtered HTML of a reply.
*
* @return string|null
*/
public function getReplyAttribute(): ?string
protected function published(): Attribute
{
if ($this->mf2 === null) {
return null;
}
$microformats = json_decode($this->mf2, true);
if (isset($microformats['items'][0]['properties']['content'][0]['html'])) {
return $this->filterHtml($microformats['items'][0]['properties']['content'][0]['html']);
}
return Attribute::get(
get: function ($value, $attributes) {
$mf2 = $attributes['mf2'] ?? '';
$microformats = json_decode($mf2, true);
if (isset($microformats['items'][0]['properties']['published'][0])) {
try {
$published = carbon()->parse(
$microformats['items'][0]['properties']['published'][0]
)->toDayDateTimeString();
} catch (Exception) {
$published = $this->updated_at->toDayDateTimeString();
}
} else {
$published = $this->updated_at->toDayDateTimeString();
}
return null;
return $published;
}
);
}
protected function reply(): Attribute
{
return Attribute::get(
get: function ($value, $attributes) {
if (
! array_key_exists('mf2', $attributes) ||
$attributes['mf2'] === null
) {
return null;
}
$microformats = json_decode($attributes['mf2'], true);
if (isset($microformats['items'][0]['properties']['content'][0]['html'])) {
return $this->filterHtml($microformats['items'][0]['properties']['content'][0]['html']);
}
return null;
}
);
}
/**
@ -121,11 +129,13 @@ class WebMention extends Model
{
$url = normalize_url($url);
$host = parse_url($url, PHP_URL_HOST);
if ($host == 'pbs.twimg.com') {
if ($host === 'pbs.twimg.com') {
//make sure we use HTTPS, we know twitter supports it
return str_replace('http://', 'https://', $url);
}
if ($host == 'twitter.com') {
if ($host === 'twitter.com') {
if (Cache::has($url)) {
return Cache::get($url);
}
@ -137,6 +147,7 @@ class WebMention extends Model
return $profile_image;
}
$filesystem = new Filesystem();
if ($filesystem->exists(public_path() . '/assets/profile-images/' . $host . '/image')) {
return '/assets/profile-images/' . $host . '/image';

View file

@ -5,6 +5,9 @@ namespace App\Providers;
use App\Models\Note;
use App\Observers\NoteObserver;
use Codebird\Codebird;
use GuzzleHttp\Client;
use GuzzleHttp\Middleware;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\Request;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Collection;
@ -102,6 +105,41 @@ class AppServiceProvider extends ServiceProvider
->forceAttribute('a', 'rel', 'noopener nofollow')
);
});
// Configure Guzzle
$this->app->bind('RetryGuzzle', function () {
$handlerStack = \GuzzleHttp\HandlerStack::create();
$handlerStack->push(Middleware::retry(
function ($retries, $request, $response, $exception) {
// Limit the number of retries to 5
if ($retries >= 5) {
return false;
}
// Retry connection exceptions
if ($exception instanceof \GuzzleHttp\Exception\ConnectException) {
return true;
}
// Retry on server errors
if ($response && $response->getStatusCode() >= 500) {
return true;
}
// Finally for CloudConvert, retry if status is not final
return json_decode($response, false, 512, JSON_THROW_ON_ERROR)->data->status !== 'finished';
},
function () {
// Retry after 1 second
return 1000;
}
));
return new Client(['handler' => $handlerStack]);
});
// Turn on Eloquent strict mode when developing
Model::shouldBeStrict(! $this->app->isProduction());
}
/**

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