aboutsummaryrefslogtreecommitdiffhomepage
path: root/Digitigrade/Model
diff options
context:
space:
mode:
authorwinter2024-12-09 21:31:54 +0000
committerwinter2024-12-09 21:31:54 +0000
commit4c7bde1400820f36caf8b2a5374007384c3018f3 (patch)
tree83ed26ff368117b8e392f7bc28736482077fc2f4 /Digitigrade/Model
parent50edbd3907c26a2d4d616421de9bb51a4d083c1b (diff)
rename to Digitigrade / PawPub
:3
Diffstat (limited to 'Digitigrade/Model')
-rw-r--r--Digitigrade/Model/Actor.php90
-rw-r--r--Digitigrade/Model/ActorEndpoints.php27
-rw-r--r--Digitigrade/Model/FetchableModel.php78
-rw-r--r--Digitigrade/Model/Note.php95
-rw-r--r--Digitigrade/Model/NoteAttachment.php23
-rw-r--r--Digitigrade/Model/NotePrivacy.php39
-rw-r--r--Digitigrade/Model/NotePrivacyInteractors.php9
-rw-r--r--Digitigrade/Model/NotePrivacyScope.php9
8 files changed, 370 insertions, 0 deletions
diff --git a/Digitigrade/Model/Actor.php b/Digitigrade/Model/Actor.php
new file mode 100644
index 0000000..946265e
--- /dev/null
+++ b/Digitigrade/Model/Actor.php
@@ -0,0 +1,90 @@
+<?php
+namespace Digitigrade\Model;
+
+use Digitigrade\RemoteFetchable;
+
+class Actor extends FetchableModel implements \JsonSerializable {
+ public const REL_URI = "https://pawpub.entities.org.uk/rel/actor.html";
+
+ public ?int $id;
+ public string $uri;
+ public bool $isLocal = false;
+ public \DateTimeImmutable $created;
+ public ?\DateTimeImmutable $modified;
+ public ?string $homepage;
+ public string $handle;
+ public string $displayName;
+ public ?string $avatar;
+ public ?string $bio;
+ public ?string $pronouns;
+ public bool $automated = false;
+ public bool $requestToFollow = false;
+ public ActorEndpoints $endpoints;
+ public array $extensions = [];
+
+ protected function getUpdateWhereClause(\PDO $db): ?string {
+ if (self::findWhere('uri = ?', [$this->uri]) != null)
+ return 'uri = ' . $db->quote($this->uri);
+ if (self::findWhere('id = ?', [$this->id]) != null)
+ return "id = $this->id";
+ return null;
+ }
+
+ public static function findLocalByHandle(string $handle): ?self {
+ return self::findWhere('is_local = true AND handle = ?', [$handle]);
+ }
+
+ public static function findByWebfinger(string $acct, bool $autoSave = true, bool $forceRefetch = false): ?self {
+ // normalise uri
+ if (!str_starts_with($acct, 'acct:')) {
+ if (str_starts_with($acct, '@')) {
+ $acct = substr($acct, 1);
+ }
+ $acct = "acct:$acct";
+ }
+ // fetch it
+ $domain = explode('@', $acct, 2)[1];
+ $data = @file_get_contents("https://$domain/.well-known/webfinger?resource=$acct",
+ context: stream_context_create([
+ 'http' => ['header' => 'Accept: application/jrd+json, application/json']
+ ])
+ );
+ if ($data === false)
+ return null;
+ $data = json_decode($data);
+ if ($data === null)
+ return null;
+ // find the right link
+ if (!isset($data->links))
+ return null;
+ $matching = array_filter($data->links, function (\stdClass $linkObj) {
+ return $linkObj->rel == self::REL_URI;
+ });
+ if (count($matching) == 0)
+ return null;
+ return self::findByUri($matching[0]->href, $autoSave, $forceRefetch);
+ }
+
+ public function jsonSerialize(): array {
+ return [
+ 'type' => 'actor',
+ 'self' => path_to_uri("/user/$this->handle"),
+ 'created' => $this->created?->format('c'),
+ 'modified' => $this->modified?->format('c'),
+ 'homepage' => path_to_uri("/$this->handle"),
+ 'handle' => $this->handle,
+ 'displayName' => $this->displayName,
+ 'bio' => $this->bio,
+ 'pronouns' => $this->pronouns,
+ 'automated' => $this->automated,
+ 'endpoints' => [
+ 'basicFeed' => path_to_uri("/user/$this->handle/basicFeed"),
+ 'fullFeed' => path_to_uri("/user/$this->handle/fullFeed")
+ ]
+ ];
+ }
+
+ public function hydrate() {
+ $this->endpoints = ActorEndpoints::findWhere('actor_id = ?', [$this->id]);
+ }
+} \ No newline at end of file
diff --git a/Digitigrade/Model/ActorEndpoints.php b/Digitigrade/Model/ActorEndpoints.php
new file mode 100644
index 0000000..8821d1c
--- /dev/null
+++ b/Digitigrade/Model/ActorEndpoints.php
@@ -0,0 +1,27 @@
+<?php
+namespace Digitigrade\Model;
+
+use Digitigrade\Model;
+
+class ActorEndpoints extends Model {
+ public ?int $actorId;
+ public string $basicFeed;
+ public ?string $fullFeed;
+ public ?string $follow;
+ public ?string $unfollow;
+ public ?string $block;
+ public ?string $unblock;
+ public ?string $subscribe;
+ public ?string $unsubscribe;
+
+ protected function setOwnerId(int $id) {
+ $this->actorId = $id;
+ }
+
+ protected function getUpdateWhereClause($db): ?string {
+ if (self::findWhere('actor_id = ?', [$this->actorId]) != null) {
+ return "actor_id = $this->actorId";
+ }
+ return null;
+ }
+} \ No newline at end of file
diff --git a/Digitigrade/Model/FetchableModel.php b/Digitigrade/Model/FetchableModel.php
new file mode 100644
index 0000000..30b68e7
--- /dev/null
+++ b/Digitigrade/Model/FetchableModel.php
@@ -0,0 +1,78 @@
+<?php
+namespace Digitigrade\Model;
+
+use Digitigrade\Model;
+use Digitigrade\RemoteFetchable;
+
+abstract class FetchableModel extends Model implements RemoteFetchable {
+ private static \JsonMapper $mapper;
+
+ public static function __initStatic() {
+ self::$mapper = new \JsonMapper();
+ // this is unfortunately necessary to parse DateTime(Immutable)s properly
+ self::$mapper->bStrictObjectTypeChecking = false;
+ }
+
+ private static function fetchFromRemote(string $uri, bool $autoSave): ?static {
+ // fetch the object
+ $data = @file_get_contents($uri, context: stream_context_create(['http' => [
+ 'header' => 'Accept: application/json'
+ ]]));
+ if ($data === false)
+ return null;
+ $data = json_decode($data);
+ if ($data === null)
+ return null;
+ // default everything nullable to null
+ $obj = new static();
+ $reflProps = (new \ReflectionClass(static::class))->getProperties();
+ foreach ($reflProps as $reflProp) {
+ assert($reflProp instanceof \ReflectionProperty); // for intellisense lmao
+ if ($reflProp->getType()->allowsNull()) {
+ $obj->{$reflProp->getName()} = null;
+ }
+ }
+ // rename 'self' to 'uri' because of reasons
+ if (property_exists($data, 'self')) {
+ $data->uri = $data->self;
+ unset($data->self);
+ }
+ // map all present fields
+ self::$mapper->map($data, $obj);
+ // (hopefully) recursively fetch any other fetchable models contained in this one
+ foreach ($reflProps as $reflProp) {
+ assert($reflProp instanceof \ReflectionProperty);
+ $reflType = $reflProp->getType();
+ $propName = $reflProp->getName();
+ if (!($reflType instanceof \ReflectionNamedType))
+ continue;
+ $type = $reflType->getName();
+ if (is_subclass_of($type, FetchableModel::class) && is_string($data->{$propName})) {
+ // try to avoid recursion
+ // TODO: make a better effort at avoiding recursion (e.g. when it's more than one step nested)
+ if ($data->{$propName} == $uri) {
+ $obj->{$propName} = $obj;
+ } else {
+ $obj->{$propName} = $type::findByUri($data->{$propName}, $autoSave);
+ }
+ }
+ }
+
+ if ($autoSave) {
+ $obj->save();
+ }
+
+ return $obj;
+ }
+
+ public static function findByUri(string $uri, bool $autoSave = true, bool $forceRefetch = false): ?static {
+ if (!$forceRefetch) {
+ $obj = static::findWhere('uri = ?', [$uri]);
+ if ($obj != null)
+ return $obj;
+ }
+ return static::fetchFromRemote($uri, $autoSave);
+ }
+}
+
+FetchableModel::__initStatic(); \ No newline at end of file
diff --git a/Digitigrade/Model/Note.php b/Digitigrade/Model/Note.php
new file mode 100644
index 0000000..3b7c362
--- /dev/null
+++ b/Digitigrade/Model/Note.php
@@ -0,0 +1,95 @@
+<?php
+namespace Digitigrade\Model;
+
+use Digitigrade\Db;
+
+class Note extends FetchableModel implements \JsonSerializable {
+ public ?int $id;
+ public string $uri;
+ public \DateTimeImmutable $created;
+ public ?\DateTimeImmutable $modified;
+ public Actor $author;
+ public ?string $summary;
+ public string $plainContent;
+ /**
+ * @var array<string, string>
+ */
+ public array $formattedContent = [];
+ public string $language;
+ /**
+ * @var Actor[]
+ */
+ public array $mentions = [];
+ public ?Note $inReplyTo;
+ public ?Note $threadApex;
+ public NotePrivacy $privacy;
+ /**
+ * @var NoteAttachment[]
+ */
+ public array $attachments = [];
+ public array $extensions = [];
+
+ protected function getUpdateWhereClause(\PDO $db): ?string {
+ if (self::findWhere('uri = ?', [$this->uri]) != null)
+ return 'uri = ' . $db->quote($this->uri);
+ if (self::findWhere('id = ?', [$this->id]) != null)
+ return "id = $this->id";
+ return null;
+ }
+
+ protected function hydrate() {
+ $this->privacy = NotePrivacy::findWhere('note_id = ?', [$this->id]);
+ $this->formattedContent = $this->findFormattedContents();
+ $this->mentions = $this->findMentions();
+ $this->attachments = NoteAttachment::findAllWhere('note_id = ?', [$this->id]);
+ }
+
+ private function findFormattedContents(): array {
+ $pdo = Db::getInstance()->getPdo();
+ $stmt = $pdo->prepare('SELECT mimetype, body FROM note_formatted_content WHERE note_id = ?');
+ $stmt->execute([$this->id]);
+ return $stmt->fetchAll(\PDO::FETCH_ASSOC);
+ }
+
+ private function findMentions(): array {
+ $pdo = Db::getInstance()->getPdo();
+ $stmt = $pdo->prepare('SELECT actor_id FROM note_mention WHERE note_id = ?');
+ $stmt->execute([$this->id]);
+ $actors = array_map(function ($actorId) {
+ return Actor::find($actorId);
+ }, $stmt->fetchAll(\PDO::FETCH_COLUMN, 0));
+ return $actors;
+ }
+
+ public static function findAllWithAuthor(Actor $author): array {
+ return self::findAllWhere('author = ?', [$author->id]);
+ }
+
+ public function jsonSerialize(): array {
+ return [
+ 'type' => 'note',
+ 'self' => path_to_uri("/post/$this->id"),
+ 'created' => $this->created->format('c'),
+ 'modified' => $this->modified?->format('c'),
+ 'author' => $this->author->uri,
+ 'summary' => $this->summary,
+ 'plainContent' => $this->plainContent,
+ 'formattedContent' => $this->formattedContent,
+ 'language' => $this->language,
+ 'mentions' => array_map(function (Actor $actor) {
+ return $actor->uri;
+ }, $this->mentions),
+ 'inReplyTo' => $this->inReplyTo?->uri,
+ 'threadApex' => $this->threadApex?->uri,
+ 'privacy' => $this->privacy,
+ 'attachments' => array_map(function (NoteAttachment $attachment) {
+ return [
+ 'type' => $attachment->type,
+ 'href' => $attachment->href,
+ 'description' => $attachment->description
+ ];
+ }, $this->attachments),
+ //'extensions' => []
+ ];
+ }
+} \ No newline at end of file
diff --git a/Digitigrade/Model/NoteAttachment.php b/Digitigrade/Model/NoteAttachment.php
new file mode 100644
index 0000000..c47ec64
--- /dev/null
+++ b/Digitigrade/Model/NoteAttachment.php
@@ -0,0 +1,23 @@
+<?php
+namespace Digitigrade\Model;
+
+use Digitigrade\Model;
+
+class NoteAttachment extends Model {
+ public ?int $noteId;
+ public int $index;
+ public string $type;
+ public string $href;
+ public ?string $description;
+
+ protected function setOwnerId(int $id) {
+ $this->noteId = $id;
+ }
+
+ protected function getUpdateWhereClause($db): ?string {
+ if (self::findWhere('note_id = ?', [$this->noteId]) != null) {
+ return "note_id = $this->noteId";
+ }
+ return null;
+ }
+} \ No newline at end of file
diff --git a/Digitigrade/Model/NotePrivacy.php b/Digitigrade/Model/NotePrivacy.php
new file mode 100644
index 0000000..faaacf6
--- /dev/null
+++ b/Digitigrade/Model/NotePrivacy.php
@@ -0,0 +1,39 @@
+<?php
+namespace Digitigrade\Model;
+
+use Digitigrade\Model;
+
+class NotePrivacy extends Model implements \JsonSerializable {
+ public ?int $noteId;
+ public NotePrivacyScope $scope;
+ /**
+ * @var Actor[]
+ */
+ public array $alsoVisibleTo = [];
+ public bool $indexable;
+ public NotePrivacyInteractors $canReshare = NotePrivacyInteractors::ALL;
+ public NotePrivacyInteractors $canReply = NotePrivacyInteractors::ALL;
+ public NotePrivacyInteractors $canOtherInteract = NotePrivacyInteractors::ALL;
+
+ protected function setOwnerId(int $id) {
+ $this->noteId = $id;
+ }
+
+ protected function getUpdateWhereClause($db): ?string {
+ if (self::findWhere('note_id = ?', [$this->noteId]) != null) {
+ return "note_id = $this->noteId";
+ }
+ return null;
+ }
+
+ public function jsonSerialize(): array {
+ return [
+ 'scope' => $this->scope->value,
+ 'alsoVisibleTo' => $this->alsoVisibleTo,
+ 'indexable' => $this->indexable,
+ 'canReshare' => $this->canReshare->value,
+ 'canReply' => $this->canReply->value,
+ 'canOtherInteract' => $this->canOtherInteract->value
+ ];
+ }
+} \ No newline at end of file
diff --git a/Digitigrade/Model/NotePrivacyInteractors.php b/Digitigrade/Model/NotePrivacyInteractors.php
new file mode 100644
index 0000000..8313e90
--- /dev/null
+++ b/Digitigrade/Model/NotePrivacyInteractors.php
@@ -0,0 +1,9 @@
+<?php
+namespace Digitigrade\Model;
+
+enum NotePrivacyInteractors: string {
+ case ALL = 'all';
+ case FOLLOWERS = 'followers';
+ case MUTUALS = 'mutuals';
+ case NONE = 'none';
+} \ No newline at end of file
diff --git a/Digitigrade/Model/NotePrivacyScope.php b/Digitigrade/Model/NotePrivacyScope.php
new file mode 100644
index 0000000..84d84f2
--- /dev/null
+++ b/Digitigrade/Model/NotePrivacyScope.php
@@ -0,0 +1,9 @@
+<?php
+namespace Digitigrade\Model;
+
+enum NotePrivacyScope: string {
+ case PUBLIC = 'public';
+ case FOLLOWERS = 'followers';
+ case MUTUALS = 'mutuals';
+ case NONE = 'none';
+} \ No newline at end of file