From 4c7bde1400820f36caf8b2a5374007384c3018f3 Mon Sep 17 00:00:00 2001 From: winter Date: Mon, 9 Dec 2024 21:31:54 +0000 Subject: rename to Digitigrade / PawPub :3 --- Digitigrade/Model/Actor.php | 90 ++++++++++++++++++++++++++ Digitigrade/Model/ActorEndpoints.php | 27 ++++++++ Digitigrade/Model/FetchableModel.php | 78 +++++++++++++++++++++++ Digitigrade/Model/Note.php | 95 ++++++++++++++++++++++++++++ Digitigrade/Model/NoteAttachment.php | 23 +++++++ Digitigrade/Model/NotePrivacy.php | 39 ++++++++++++ Digitigrade/Model/NotePrivacyInteractors.php | 9 +++ Digitigrade/Model/NotePrivacyScope.php | 9 +++ 8 files changed, 370 insertions(+) create mode 100644 Digitigrade/Model/Actor.php create mode 100644 Digitigrade/Model/ActorEndpoints.php create mode 100644 Digitigrade/Model/FetchableModel.php create mode 100644 Digitigrade/Model/Note.php create mode 100644 Digitigrade/Model/NoteAttachment.php create mode 100644 Digitigrade/Model/NotePrivacy.php create mode 100644 Digitigrade/Model/NotePrivacyInteractors.php create mode 100644 Digitigrade/Model/NotePrivacyScope.php (limited to 'Digitigrade/Model') 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 @@ +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 @@ +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 @@ +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 @@ + + */ + 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 @@ +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 @@ +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 @@ +