jonnybarnes.uk/app/Models/WebMention.php
Jonny Barnes df9b0dc490
Move webmention parsing and authorship into the app
The jonnybarnes/webmentions-parser package is now archived, and its
guzzlehttp/guzzle ^6|^7 constraint was the only thing holding Guzzle
back from 8. Port the parts we use into App\Services\WebMention and
drop the package.

Parser keeps getMentionType and the checkInReplyTo/LikeOf/RepostOf
checks unchanged. Authorship follows the same authorship algorithm,
with some fixes:

- fetch the author page with Laravel's Http client, so tests can fake
  it, and treat connection failures as AuthorshipParserException too
- keep state in locals so one instance can be reused safely
- make the url == uid == author-page check actually compare the
  h-card's uid, rather than an undefined variable

Add direct tests for both, and mark the library as archived on the
projects page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 16:23:49 +00:00

133 lines
3.9 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Models;
use App\Services\WebMention\Authorship;
use App\Traits\FilterHtml;
use Exception;
use Illuminate\Database\Eloquent\Attributes\Guarded;
use Illuminate\Database\Eloquent\Attributes\Table;
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;
#[Table('webmentions')]
#[Guarded(['id'])]
class WebMention extends Model
{
use FilterHtml;
use HasFactory;
public function commentable(): MorphTo
{
return $this->morphTo();
}
protected function author(): Attribute
{
return Attribute::get(
get: function ($value, $attributes) {
if (
! array_key_exists('mf2', $attributes) ||
$attributes['mf2'] === null
) {
return null;
}
$authorship = new Authorship;
$hCard = $authorship->findAuthor(json_decode($attributes['mf2'], true));
if ($hCard === false) {
return null;
}
if (
array_key_exists('properties', $hCard) &&
array_key_exists('photo', $hCard['properties'])
) {
$hCard['properties']['photo'][0] = $this->createPhotoLink($hCard['properties']['photo'][0]);
}
return $hCard;
}
);
}
protected function published(): Attribute
{
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 $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;
}
);
}
/**
* Create the photo link.
*/
public function createPhotoLink(string|array $url): string
{
if (is_array($url)) {
if (! array_key_exists('value', $url)) {
return '';
}
$url = $url['value'];
}
$url = normalize_url($url);
$host = parse_url($url, PHP_URL_HOST);
if ($host === 'pbs.twimg.com') {
// make sure we use HTTPS, we know twitter supports it
return str_replace('http://', 'https://', $url);
}
$filesystem = new Filesystem;
if ($filesystem->exists(public_path().'/assets/profile-images/'.$host.'/image')) {
return '/assets/profile-images/'.$host.'/image';
}
return $url;
}
}