diff options
| author | winter | 2025-02-16 19:36:51 +0000 |
|---|---|---|
| committer | winter | 2025-02-16 19:36:51 +0000 |
| commit | 5a44137ddea7b3a6d7f68cbe71594f7633d6e9a6 (patch) | |
| tree | 1583032971f5fbde6e333a4c377c2b6672dc296c /Digitigrade | |
| parent | 65dfe7aff06dfd3b044a5deb47b03d643c651ec1 (diff) | |
link previews
Diffstat (limited to 'Digitigrade')
| -rw-r--r-- | Digitigrade/LinkPreview.php | 91 | ||||
| -rw-r--r-- | Digitigrade/Model/Note.php | 45 |
2 files changed, 135 insertions, 1 deletions
diff --git a/Digitigrade/LinkPreview.php b/Digitigrade/LinkPreview.php new file mode 100644 index 0000000..4716cc8 --- /dev/null +++ b/Digitigrade/LinkPreview.php @@ -0,0 +1,91 @@ +<?php +namespace Digitigrade; + +class LinkPreview implements \JsonSerializable { + public string $url; + public string $siteName; + public string $title; + public string $description; + public string $style; + public ?string $imageUrl = null; + + /** + * Generates a link preview for the given URL. + * @param string $url + * @return LinkPreview|null the link preview, or null if none is available + */ + public static function generate(string $url): ?self { + $p = new self(); + $p->url = $url; + return $p->update() ? $p : null; + } + + /** + * Updates the information for this link preview. + * @return bool true if successful + */ + public function update(): bool { + if (!(str_starts_with($this->url, "https://") || str_starts_with($this->url, "http://"))) { + return false; + } + + $d = new \DOMDocument(); + $c = @file_get_contents($this->url); + if (!$c) + return false; + @$d->loadHTML($c); + + $url = $this->url; + $siteName = hostname_from_uri($this->url); + $style = 'text'; + $title = $d->getElementsByTagName('title')->item(0)?->textContent; + $metas = $d->getElementsByTagName('meta'); + foreach ($metas as $tag) { + if ($tag->attributes->getNamedItem('name')?->textContent == 'description') { + // <meta name="description" content="..."> + $description = $tag->attributes->getNamedItem('content')?->textContent; + } elseif ($prop = $tag->attributes->getNamedItem('property')) { + // <meta property="og:..." content="..."> + $content = $tag->attributes->getNamedItem('content')?->textContent; + match ($prop->textContent) { + 'og:title' => $title = $content, + 'og:description' => $description = $content, + 'og:url' => $url = $content, + 'og:site_name' => $siteName = $content, + 'og:image' => $imageUrl = $content, + default => null + }; + + } elseif ($tag->attributes->getNamedItem('name')?->textContent == 'twitter:card' + && $tag->attributes->getNamedItem("content")?->textContent == 'summary_large_image') { + // <meta name="twitter:card" content="summary_large_image"> + $style = 'largeImage'; + } + } + // turn text into smallImage if an image is present + $style = ($style == 'text' && isset($imageUrl)) ? 'smallImage' : $style; + + if (!isset($title, $description)) + return false; + + $this->url = $url; + $this->siteName = $siteName; + $this->title = $title; + $this->description = $description; + $this->style = $style; + $this->imageUrl = $imageUrl ?? null; + + return true; + } + + public function jsonSerialize(): array { + return [ + 'url' => $this->url, + 'siteName' => $this->siteName, + 'title' => $this->title, + 'description' => $this->description, + 'style' => $this->style, + 'imageUrl' => $this->imageUrl + ]; + } +}
\ No newline at end of file diff --git a/Digitigrade/Model/Note.php b/Digitigrade/Model/Note.php index d5d4e69..ac9cc3c 100644 --- a/Digitigrade/Model/Note.php +++ b/Digitigrade/Model/Note.php @@ -4,12 +4,16 @@ namespace Digitigrade\Model; use Digitigrade\Db; use Digitigrade\FormattingProvider\CommonMark; use Digitigrade\FormattingProvider\SanitisedHtml; +use Digitigrade\LinkPreview; use Digitigrade\Model\HomeTimelineItem; use Digitigrade\Notification\Notifyable; use Digitigrade\Timeline\TimelineIncludeable; use Digitigrade\Timeline\TimelineItem; +use JsonMapper; class Note extends PushableModel implements TimelineIncludeable, Notifyable { + public const EXTENSION_LINK_PREVIEWS = 'https://pawpub.entities.org.uk/extension/link-previews'; + public ?int $id; public string $uri; public \DateTimeImmutable $created; @@ -265,6 +269,45 @@ class Note extends PushableModel implements TimelineIncludeable, Notifyable { return htmlspecialchars($this->plainContent); } + /** + * Gets a list of LinkPreviews applicable to this note + * @return LinkPreview[] + */ + public function getLinkPreviews(): array { + if (($this->extensions[self::EXTENSION_LINK_PREVIEWS] ?? []) == []) { + return []; + } + $mapper = new JsonMapper(); + $results = []; + foreach ($this->extensions[self::EXTENSION_LINK_PREVIEWS] as $preview) { + $results[] = $mapper->map($preview, LinkPreview::class); + } + return $results; + } + + /** + * Generates new LinkPreviews for this note (but does not save them automatically) + * @return LinkPreview[] the new link previews + */ + public function generateLinkPreviews(): array { + $matches = []; + preg_match_all( + '`[a-z+]+://[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(([-a-zA-Z0-9@:(%_\+.~#?&/=]|\)[^\s])*)`', + $this->getFormattedContent('text/markdown') ?? $this->plainContent, + $matches + ); + $urls = $matches[0] ?? []; + $previews = array_filter( + array_map(fn($url) => LinkPreview::generate($url), $urls), + fn($p) => $p != null + ); + $this->extensions[self::EXTENSION_LINK_PREVIEWS] = array_map( + fn($p) => json_decode(json_encode($p)), + $previews + ); + return $previews; + } + public function renderAsHtml() { render_template('note', ['note' => $this]); } @@ -345,7 +388,7 @@ class Note extends PushableModel implements TimelineIncludeable, Notifyable { 'description' => $attachment->description ]; }, $this->attachments), - //'extensions' => [] + 'extensions' => $this->extensions ]; } }
\ No newline at end of file |
