aboutsummaryrefslogtreecommitdiffhomepage
path: root/Digitigrade
diff options
context:
space:
mode:
authorwinter2025-03-15 18:35:59 +0000
committerwinter2025-03-15 18:52:54 +0000
commit61b122e88cc6783b4d0cf5142a22002f8e558c60 (patch)
tree847b864a93f716157456891d4526077e5f0b93f7 /Digitigrade
parentcb904ef3fb05ab106d5e5e12f6273b8117db4216 (diff)
implement pages
Diffstat (limited to 'Digitigrade')
-rw-r--r--Digitigrade/CharacterRange.php17
-rw-r--r--Digitigrade/Model/Actor.php4
-rw-r--r--Digitigrade/Model/BlockRelation.php2
-rw-r--r--Digitigrade/Model/FollowRelation.php2
-rw-r--r--Digitigrade/Model/Interaction.php4
-rw-r--r--Digitigrade/Model/Note.php52
-rw-r--r--Digitigrade/Notification/PendingFollowActionedNotif.php2
-rw-r--r--Digitigrade/Notification/UnblockNotif.php2
-rw-r--r--Digitigrade/Notification/UnfollowNotif.php2
-rw-r--r--Digitigrade/Page.php75
-rw-r--r--Digitigrade/PageLink.php15
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