diff options
Diffstat (limited to 'Digitigrade')
| -rw-r--r-- | Digitigrade/CharacterRange.php | 17 | ||||
| -rw-r--r-- | Digitigrade/Model/Actor.php | 4 | ||||
| -rw-r--r-- | Digitigrade/Model/BlockRelation.php | 2 | ||||
| -rw-r--r-- | Digitigrade/Model/FollowRelation.php | 2 | ||||
| -rw-r--r-- | Digitigrade/Model/Interaction.php | 4 | ||||
| -rw-r--r-- | Digitigrade/Model/Note.php | 52 | ||||
| -rw-r--r-- | Digitigrade/Notification/PendingFollowActionedNotif.php | 2 | ||||
| -rw-r--r-- | Digitigrade/Notification/UnblockNotif.php | 2 | ||||
| -rw-r--r-- | Digitigrade/Notification/UnfollowNotif.php | 2 | ||||
| -rw-r--r-- | Digitigrade/Page.php | 75 | ||||
| -rw-r--r-- | Digitigrade/PageLink.php | 15 |
11 files changed, 168 insertions, 9 deletions
diff --git a/Digitigrade/CharacterRange.php b/Digitigrade/CharacterRange.php new file mode 100644 index 0000000..b2d2968 --- /dev/null +++ b/Digitigrade/CharacterRange.php @@ -0,0 +1,17 @@ +<?php +namespace Digitigrade; + +class CharacterRange implements \JsonSerializable { + public int $start; + public int $end; + + public function __construct(string $range) { + $parts = explode('-', $range, 2); + $this->start = +$parts[0]; + $this->end = +$parts[1]; + } + + public function jsonSerialize(): string { + return "$this->start-$this->end"; + } +}
\ No newline at end of file diff --git a/Digitigrade/Model/Actor.php b/Digitigrade/Model/Actor.php index 229582c..0541e1b 100644 --- a/Digitigrade/Model/Actor.php +++ b/Digitigrade/Model/Actor.php @@ -325,6 +325,10 @@ class Actor extends PushableModel implements RpcReceiver { return $this->handle . (($alwaysIncludeDomain || !$this->isLocal) ? '@' . hostname_from_uri($this->uri) : ''); } + public function getLocalUiHref(): string { + return '/@/' . $this->getFullHandle(); + } + public function jsonSerialize(): array { if ($this->deleted) { return [ diff --git a/Digitigrade/Model/BlockRelation.php b/Digitigrade/Model/BlockRelation.php index ca7166c..0b9054a 100644 --- a/Digitigrade/Model/BlockRelation.php +++ b/Digitigrade/Model/BlockRelation.php @@ -62,7 +62,7 @@ class BlockRelation extends Model implements Notifyable { return sprintf(__("notifications.block"), $this->subject->displayName); } public function getNotificationTitleLink(): ?string { - return '/@/' . $this->subject->getFullHandle(); + return $this->subject->getLocalUiHref(); } public function getNotificationImageUrl(): ?string { return $this->subject->avatar ?? '/static/default-avatar.png'; diff --git a/Digitigrade/Model/FollowRelation.php b/Digitigrade/Model/FollowRelation.php index ecb625a..066fc47 100644 --- a/Digitigrade/Model/FollowRelation.php +++ b/Digitigrade/Model/FollowRelation.php @@ -66,7 +66,7 @@ class FollowRelation extends Model implements Notifyable { return sprintf(__("notifications.follow.$status"), $this->subject->displayName); } public function getNotificationTitleLink(): ?string { - return '/@/' . $this->subject->getFullHandle(); + return $this->subject->getLocalUiHref(); } public function getNotificationImageUrl(): ?string { return $this->subject->avatar ?? '/static/default-avatar.png'; diff --git a/Digitigrade/Model/Interaction.php b/Digitigrade/Model/Interaction.php index 43d02a2..dd977fd 100644 --- a/Digitigrade/Model/Interaction.php +++ b/Digitigrade/Model/Interaction.php @@ -97,7 +97,7 @@ class Interaction extends PushableModel implements Notifyable { return sprintf(__('notifications.interaction.' . $this->kind->value . '.title'), $this->author->displayName); } public function getNotificationTitleLink(): ?string { - return '/@/' . $this->target->author->getFullHandle() . '/note/' . $this->target->id; + return $this->target->getLocalUiHref(); } public function getNotificationBody(): ?string { return isset($this->target->summary) ? ('[' . $this->target->summary . ']') : $this->target->plainContent; @@ -106,7 +106,7 @@ class Interaction extends PushableModel implements Notifyable { return $this->author->avatar ?? '/static/default-avatar.png'; } public function getNotificationImageLink(): ?string { - return '/@/' . $this->author->getFullHandle(); + return $this->author->getLocalUiHref(); } public function toJsonReference(): mixed { diff --git a/Digitigrade/Model/Note.php b/Digitigrade/Model/Note.php index 880e3e8..b32e625 100644 --- a/Digitigrade/Model/Note.php +++ b/Digitigrade/Model/Note.php @@ -7,11 +7,13 @@ use Digitigrade\FormattingProvider\SanitisedHtml; use Digitigrade\LinkPreview; use Digitigrade\Model\HomeTimelineItem; use Digitigrade\Notification\Notifyable; +use Digitigrade\Page; use Digitigrade\Timeline\TimelineIncludeable; use Digitigrade\Timeline\TimelineItem; class Note extends PushableModel implements TimelineIncludeable, Notifyable { public const EXTENSION_LINK_PREVIEWS = 'https://pawpub.entities.org.uk/extension/link-previews'; + public const EXTENSION_PAGES = 'https://pawpub.entities.org.uk/extension/pages'; public ?int $id; public string $uri; @@ -195,6 +197,29 @@ class Note extends PushableModel implements TimelineIncludeable, Notifyable { return $notes; } + public static function findAllWithExtension(string $extensionUri, ?int $limit = null, int $offset = 0): array { + $pdo = Db::getInstance()->getPdo(); + $query = 'SELECT note_id FROM note_extension LEFT JOIN note ON note.id = note_id ' + . 'WHERE note_extension.uri = ? ORDER BY note.created DESC'; + if ($limit != null) { + $query .= " LIMIT $limit OFFSET $offset"; + } + $stmt = $pdo->prepare($query); + $stmt->execute([$extensionUri]); + $notes = []; + while ($id = $stmt->fetchColumn(0)) { + $notes[] = self::find($id); + } + return $notes; + } + + public static function countWithExtension(string $extensionUri): int { + $pdo = Db::getInstance()->getPdo(); + $stmt = $pdo->prepare('SELECT COUNT(*) FROM note_extension WHERE uri = ?'); + $stmt->execute([$extensionUri]); + return $stmt->fetchColumn(0); + } + public function getInteractions(): array { return Interaction::findAllWithTarget($this); } @@ -268,6 +293,10 @@ class Note extends PushableModel implements TimelineIncludeable, Notifyable { return htmlspecialchars($this->plainContent); } + public function getLocalUiHref(bool $asPage = false): string { + return '/@/' . $this->author->getFullHandle() . ($asPage ? '/page/' : '/note/') . $this->id; + } + /** * Gets a list of LinkPreviews applicable to this note * @return LinkPreview[] @@ -285,6 +314,25 @@ class Note extends PushableModel implements TimelineIncludeable, Notifyable { } /** + * @return bool whether this note has an attached page + */ + public function hasPage(): bool { + return isset($this->extensions[self::EXTENSION_PAGES]); + } + + public function getPage(): ?Page { + if (!$this->hasPage()) + return null; + $mapper = new \JsonMapper(); + $mapper->bStrictObjectTypeChecking = false; // so that CharacterRanges can be decoded + return $mapper->map($this->extensions[self::EXTENSION_PAGES], Page::class); + } + + public function setPage(Page $page) { + $this->extensions[self::EXTENSION_PAGES] = json_decode(json_encode($page)); + } + + /** * Generates new LinkPreviews for this note (but does not save them automatically) * @return LinkPreview[] the new link previews */ @@ -324,7 +372,7 @@ class Note extends PushableModel implements TimelineIncludeable, Notifyable { return sprintf(__("notifications.$action.title"), $this->author->displayName); } public function getNotificationTitleLink(): ?string { - return '/@/' . $this->author->getFullHandle() . '/note/' . $this->id; + return $this->author->getLocalUiHref(); } public function getNotificationBody(): ?string { return isset($this->summary) ? "[$this->summary]" : $this->plainContent; @@ -333,7 +381,7 @@ class Note extends PushableModel implements TimelineIncludeable, Notifyable { return $this->author->avatar ?? '/static/default-avatar.png'; } public function getNotificationImageLink(): ?string { - return '/@/' . $this->author->getFullHandle(); + return $this->author->getLocalUiHref(); } public function processNotifications() { diff --git a/Digitigrade/Notification/PendingFollowActionedNotif.php b/Digitigrade/Notification/PendingFollowActionedNotif.php index 6ab36ee..080fd45 100644 --- a/Digitigrade/Notification/PendingFollowActionedNotif.php +++ b/Digitigrade/Notification/PendingFollowActionedNotif.php @@ -32,7 +32,7 @@ class PendingFollowActionedNotif implements Notifyable { return sprintf(__("notifications.follow.$this->action"), $this->subject->displayName); } public function getNotificationTitleLink(): ?string { - return '/@/' . $this->subject->getFullHandle(); + return $this->subject->getLocalUiHref(); } public function getNotificationImageUrl(): ?string { return $this->subject->avatar ?? '/static/default-avatar.png'; diff --git a/Digitigrade/Notification/UnblockNotif.php b/Digitigrade/Notification/UnblockNotif.php index 7f7ac64..708064d 100644 --- a/Digitigrade/Notification/UnblockNotif.php +++ b/Digitigrade/Notification/UnblockNotif.php @@ -29,7 +29,7 @@ class UnblockNotif implements Notifyable { return sprintf(__('notifications.unblock'), $this->subject->displayName); } public function getNotificationTitleLink(): ?string { - return '/@/' . $this->subject->getFullHandle(); + return $this->subject->getLocalUiHref(); } public function getNotificationImageUrl(): ?string { return $this->subject->avatar ?? '/static/default-avatar.png'; diff --git a/Digitigrade/Notification/UnfollowNotif.php b/Digitigrade/Notification/UnfollowNotif.php index e00c342..a131dab 100644 --- a/Digitigrade/Notification/UnfollowNotif.php +++ b/Digitigrade/Notification/UnfollowNotif.php @@ -29,7 +29,7 @@ class UnfollowNotif implements Notifyable { return sprintf(__('notifications.follow.removed'), $this->subject->displayName); } public function getNotificationTitleLink(): ?string { - return '/@/' . $this->subject->getFullHandle(); + return $this->subject->getLocalUiHref(); } public function getNotificationImageUrl(): ?string { return $this->subject->avatar ?? '/static/default-avatar.png'; diff --git a/Digitigrade/Page.php b/Digitigrade/Page.php new file mode 100644 index 0000000..963a27d --- /dev/null +++ b/Digitigrade/Page.php @@ -0,0 +1,75 @@ +<?php +namespace Digitigrade; + +use Digitigrade\FormattingProvider\CommonMark; +use Digitigrade\FormattingProvider\SanitisedHtml; +use Digitigrade\Model\Note; + +class Page implements \JsonSerializable { + public string $title; + public array $content; + /** @var PageLink[] */ + public array $links; + + public static function create(string $title, array $content) { + $page = new self(); + $page->title = $title; + $page->content = $content; + $page->links = []; + return $page; + } + + /** + * Gets the best available formatted content (or plain if there aren't any) + * and converts it to sanitised HTML + * @return string HTML that is hopefully safe to include directly + */ + public function getBestContentAsHtml(): string { + if (isset($this->content['text/html'])) { + return SanitisedHtml::getInstance()->renderToHtml($this->insertLinks($this->content['text/html'], 'text/html')); + } elseif (isset($this->content['text/markdown'])) { + return CommonMark::getInstance()->renderToHtml($this->insertLinks($this->content['text/markdown'], 'text/markdown')); + } + return htmlspecialchars($this->insertLinks($this->content['text/plain'] ?? __('placeholder'), 'text/plain')); + } + + private function insertLinks(string $originalContent, string $mediaType): string { + $result = $originalContent; + $offset = 0; + + foreach ($this->links as $link) { + $range = $link->range[$mediaType] ?? null; + $targetNote = Note::findByUri($link->target); + if ($range == null || $targetNote == null) + continue; + $target = path_to_uri(htmlspecialchars($targetNote->getLocalUiHref(true))); + $before = mb_substr($result, 0, $range->start, 'utf-8'); + $label = mb_substr($result, $range->start, $range->end - $range->start, 'utf-8'); + $after = mb_substr($result, $range->end, null, 'utf-8'); + $startTag = match ($mediaType) { + 'text/html' => '<a href="' . htmlspecialchars($target) . '">', + 'text/markdown' => '[', + 'text/plain' => '', + default => '' + }; + $endTag = match ($mediaType) { + 'text/html' => '</a>', + 'text/markdown' => '](' . htmlspecialchars($target) . ')', + 'text/plain' => " ($target)", + default => '' + }; + $result = $before . $startTag . $label . $endTag . $after; + $offset += strlen($startTag) + strlen($endTag); + } + + return $result; + } + + public function jsonSerialize(): array { + return [ + 'title' => $this->title, + 'content' => $this->content, + 'links' => $this->links + ]; + } +}
\ No newline at end of file diff --git a/Digitigrade/PageLink.php b/Digitigrade/PageLink.php new file mode 100644 index 0000000..3b8e656 --- /dev/null +++ b/Digitigrade/PageLink.php @@ -0,0 +1,15 @@ +<?php +namespace Digitigrade; + +class PageLink implements \JsonSerializable { + /** @var CharacterRange[] */ + public array $range; + public string $target; + + public function jsonSerialize(): array { + return [ + 'range' => $this->range, + 'target' => $this->target + ]; + } +}
\ No newline at end of file |
