From 75b3778dccccddb5cc90b2b4e0a3f958826ad8ba Mon Sep 17 00:00:00 2001 From: winter Date: Wed, 15 Jan 2025 20:33:38 +0000 Subject: implement home timeline --- Digitigrade/Job/ProcessIncomingPush.php | 6 ++- Digitigrade/Model/Interaction.php | 20 +++++++++ Digitigrade/Model/Note.php | 37 ++++++++++++++--- Digitigrade/Model/UserAccount.php | 4 ++ Digitigrade/Timeline.php | 26 ------------ Digitigrade/Timeline/GlobalTimeline.php | 2 - Digitigrade/Timeline/HomeTimeline.php | 26 ++++++++++++ Digitigrade/Timeline/HomeTimelineItem.php | 52 ++++++++++++++++++++++++ Digitigrade/Timeline/LocalTimeline.php | 2 - Digitigrade/Timeline/ReasonReshared.php | 24 +++++++++++ Digitigrade/Timeline/Timeline.php | 24 +++++++++++ Digitigrade/Timeline/TimelineIncludeable.php | 8 ++++ Digitigrade/Timeline/TimelineInclusionReason.php | 8 ++++ Digitigrade/Timeline/TimelineItem.php | 20 +++++++++ Digitigrade/TimelineIncludeable.php | 6 --- Digitigrade/TimelineInclusionReason.php | 6 --- Digitigrade/TimelineInclusionReason/Reshared.php | 17 -------- Digitigrade/TimelineItem.php | 20 --------- 18 files changed, 221 insertions(+), 87 deletions(-) delete mode 100644 Digitigrade/Timeline.php create mode 100644 Digitigrade/Timeline/HomeTimeline.php create mode 100644 Digitigrade/Timeline/HomeTimelineItem.php create mode 100644 Digitigrade/Timeline/ReasonReshared.php create mode 100644 Digitigrade/Timeline/Timeline.php create mode 100644 Digitigrade/Timeline/TimelineIncludeable.php create mode 100644 Digitigrade/Timeline/TimelineInclusionReason.php create mode 100644 Digitigrade/Timeline/TimelineItem.php delete mode 100644 Digitigrade/TimelineIncludeable.php delete mode 100644 Digitigrade/TimelineInclusionReason.php delete mode 100644 Digitigrade/TimelineInclusionReason/Reshared.php delete mode 100644 Digitigrade/TimelineItem.php (limited to 'Digitigrade') diff --git a/Digitigrade/Job/ProcessIncomingPush.php b/Digitigrade/Job/ProcessIncomingPush.php index cd297ca..2d27f80 100644 --- a/Digitigrade/Job/ProcessIncomingPush.php +++ b/Digitigrade/Job/ProcessIncomingPush.php @@ -33,11 +33,13 @@ class ProcessIncomingPush extends Job { break; case 'note': $log->info('importing note with uri ' . $this->object->self); - Note::importFromReceivedObject($this->object); + $note = Note::importFromReceivedObject($this->object); + $note->processTimelineAdditions(); break; case 'interaction': $log->info('importing interaction with uri ' . $this->object->self); - Interaction::importFromReceivedObject($this->object); + $interaction = Interaction::importFromReceivedObject($this->object); + $interaction->processTimelineAdditions(); break; case 'extension': throw new \RuntimeException('object type ' . $this->object->type . ' not yet implemented :('); diff --git a/Digitigrade/Model/Interaction.php b/Digitigrade/Model/Interaction.php index 4ca1a0c..ed4ef45 100644 --- a/Digitigrade/Model/Interaction.php +++ b/Digitigrade/Model/Interaction.php @@ -1,6 +1,10 @@ kind != InteractionKind::RESHARE) + return; + $reason = new ReasonReshared($this->author); + $item = new TimelineItem($this->target, $reason); + + foreach ($this->target->getRelevantActors() as $actor) { + if (!$actor->isLocal) + return; + $user = UserAccount::findByLinkedActor($actor); + HomeTimelineItem::fromTimelineItem($item, $user)->save(); + } + } + public function jsonSerialize(): array { if ($this->deleted) { return [ diff --git a/Digitigrade/Model/Note.php b/Digitigrade/Model/Note.php index 71b25ae..7fb1221 100644 --- a/Digitigrade/Model/Note.php +++ b/Digitigrade/Model/Note.php @@ -2,8 +2,9 @@ namespace Digitigrade\Model; use Digitigrade\Db; -use Digitigrade\Job\PushObject; -use Digitigrade\TimelineIncludeable; +use Digitigrade\Timeline\HomeTimelineItem; +use Digitigrade\Timeline\TimelineIncludeable; +use Digitigrade\Timeline\TimelineItem; class Note extends PushableModel implements TimelineIncludeable { public ?int $id; @@ -139,13 +140,19 @@ class Note extends PushableModel implements TimelineIncludeable { return self::countWhere('in_reply_to = ? AND deleted = false', [$this->id]); } - protected function getRelevantServers(): array { - $recipientActors = match ($this->privacy->scope) { + /** + * @return Actor[] actors who this note can be seen by, are mentioned, etc + */ + public function getRelevantActors(): array { + return array_unique(array_merge(match ($this->privacy->scope) { NotePrivacyScope::NONE => [], NotePrivacyScope::MUTUALS => $this->author->findMutualFollows(), default => $this->author->findFollowers() - }; - $recipientActors = array_merge($recipientActors, $this->mentions, $this->privacy->alsoVisibleTo); + }, $this->mentions, $this->privacy->alsoVisibleTo, [$this->author])); + } + + protected function getRelevantServers(): array { + $recipientActors = $this->getRelevantActors(); // reply-to and thread apex author should be covered by mentions but in case they're not: if (isset($this->inReplyTo)) $recipientActors[] = $this->inReplyTo->author; @@ -170,6 +177,24 @@ class Note extends PushableModel implements TimelineIncludeable { render_template('note', ['note' => $this]); } + public function toJsonReference(): mixed { + return $this->id; + } + + public static function fromJsonReference(mixed $reference): self { + return self::find($reference); + } + + public function processTimelineAdditions() { + $item = new TimelineItem($this); + foreach ($this->getRelevantActors() as $actor) { + if (!$actor->isLocal) + continue; + $user = UserAccount::findByLinkedActor($actor); + HomeTimelineItem::fromTimelineItem($item, $user)->save(); + } + } + public function jsonSerialize(): array { if ($this->deleted) { return [ diff --git a/Digitigrade/Model/UserAccount.php b/Digitigrade/Model/UserAccount.php index 72a9ce9..d0fa94b 100644 --- a/Digitigrade/Model/UserAccount.php +++ b/Digitigrade/Model/UserAccount.php @@ -59,6 +59,10 @@ class UserAccount extends Model { return $user; } + public static function findByLinkedActor(Actor $actor): ?self { + return self::findWhere('actor = ?', [$actor->id]); + } + public function verifyPassword(#[\SensitiveParameter] $attemptedPassword): bool { return password_verify($attemptedPassword, $this->passwordHash); } diff --git a/Digitigrade/Timeline.php b/Digitigrade/Timeline.php deleted file mode 100644 index 64aabe1..0000000 --- a/Digitigrade/Timeline.php +++ /dev/null @@ -1,26 +0,0 @@ -getItems(self::RESULTS_PER_PAGE, self::RESULTS_PER_PAGE * $page); - } - - /** - * Gets a selection of items to be included in the timeline - * @param int $limit max number of items to return - * @param int $offset number of items to skip at first - * @return TimelineItem[] - */ - abstract protected function getItems(int $limit, int $offset): array; -} \ No newline at end of file diff --git a/Digitigrade/Timeline/GlobalTimeline.php b/Digitigrade/Timeline/GlobalTimeline.php index 278b557..81eccfb 100644 --- a/Digitigrade/Timeline/GlobalTimeline.php +++ b/Digitigrade/Timeline/GlobalTimeline.php @@ -3,8 +3,6 @@ namespace Digitigrade\Timeline; use Digitigrade\Db; use Digitigrade\Model\Note; -use Digitigrade\Timeline; -use Digitigrade\TimelineItem; class GlobalTimeline extends Timeline { protected function getItems(int $limit, int $offset): array { diff --git a/Digitigrade/Timeline/HomeTimeline.php b/Digitigrade/Timeline/HomeTimeline.php new file mode 100644 index 0000000..c2baf3d --- /dev/null +++ b/Digitigrade/Timeline/HomeTimeline.php @@ -0,0 +1,26 @@ +user = $user; + } + + protected function getItems(int $limit, int $offset): array { + return array_map( + fn(HomeTimelineItem $hti) => $hti->toTimelineItem(), + HomeTimelineItem::findAllForUser($this->user, $limit, $offset) + ); + } + + public function getItemsSince(\DateTimeInterface $when, ?int $limit = null, int $offset = 0): array { + return array_map( + fn(HomeTimelineItem $hti) => $hti->toTimelineItem(), + HomeTimelineItem::findAllForUser($this->user, $limit, $offset, $when) + ); + } +} \ No newline at end of file diff --git a/Digitigrade/Timeline/HomeTimelineItem.php b/Digitigrade/Timeline/HomeTimelineItem.php new file mode 100644 index 0000000..cb2b6e4 --- /dev/null +++ b/Digitigrade/Timeline/HomeTimelineItem.php @@ -0,0 +1,52 @@ +userAccount = $user; + $reason = $item->getReason(); + if ($reason != null) { + $hti->reasonKind = $reason::class; + $hti->reasonData = json_encode($reason->toJsonReference()); + } + $object = $item->getObject(); + $hti->itemKind = $object::class; + $hti->itemData = json_encode($object->toJsonReference()); + $hti->modified = new \DateTimeImmutable(); + return $hti; + } + + public function toTimelineItem(): TimelineItem { + $reason = null; + if (isset($this->reasonKind)) { + $reason = $this->reasonKind::fromJsonReference(json_decode($this->reasonData)); + } + $object = $this->itemKind::fromJsonReference(json_decode($this->itemData)); + return new TimelineItem($object, $reason); + } + + public static function findAllForUser(UserAccount $user, ?int $limit = null, int $offset = 0, ?\DateTimeInterface $since = null) { + $where = 'user_account = ?'; + $params = [$user->id]; + if (isset($since)) { + $where .= ' AND modified > ?'; + $params[] = $since->format('c'); + } + return self::findAllWhere($where, $params, $limit, $offset, 'id DESC'); + } +} \ No newline at end of file diff --git a/Digitigrade/Timeline/LocalTimeline.php b/Digitigrade/Timeline/LocalTimeline.php index c20ada6..6129576 100644 --- a/Digitigrade/Timeline/LocalTimeline.php +++ b/Digitigrade/Timeline/LocalTimeline.php @@ -3,8 +3,6 @@ namespace Digitigrade\Timeline; use Digitigrade\Db; use Digitigrade\Model\Note; -use Digitigrade\Timeline; -use Digitigrade\TimelineItem; class LocalTimeline extends Timeline { protected function getItems(int $limit, int $offset): array { diff --git a/Digitigrade/Timeline/ReasonReshared.php b/Digitigrade/Timeline/ReasonReshared.php new file mode 100644 index 0000000..2ffe1b2 --- /dev/null +++ b/Digitigrade/Timeline/ReasonReshared.php @@ -0,0 +1,24 @@ +resharer = $resharer; + } + + public function renderAsHtml() { + render_template('reason_reshared', ['actor' => $this->resharer]); + } + + public function toJsonReference(): mixed { + return $this->resharer->id; + } + + public static function fromJsonReference(mixed $reference): self { + return new self(Actor::find($reference)); + } +} \ No newline at end of file diff --git a/Digitigrade/Timeline/Timeline.php b/Digitigrade/Timeline/Timeline.php new file mode 100644 index 0000000..b3def28 --- /dev/null +++ b/Digitigrade/Timeline/Timeline.php @@ -0,0 +1,24 @@ +getItems(self::RESULTS_PER_PAGE, self::RESULTS_PER_PAGE * $page); + } + + /** + * Gets a selection of items to be included in the timeline + * @param int $limit max number of items to return + * @param int $offset number of items to skip at first + * @return TimelineItem[] + */ + abstract protected function getItems(int $limit, int $offset): array; +} \ No newline at end of file diff --git a/Digitigrade/Timeline/TimelineIncludeable.php b/Digitigrade/Timeline/TimelineIncludeable.php new file mode 100644 index 0000000..5406543 --- /dev/null +++ b/Digitigrade/Timeline/TimelineIncludeable.php @@ -0,0 +1,8 @@ +reason = $reason; + $this->object = $object; + } + + public function getReason(): ?TimelineInclusionReason { + return $this->reason; + } + + public function getObject(): TimelineIncludeable { + return $this->object; + } +} \ No newline at end of file diff --git a/Digitigrade/TimelineIncludeable.php b/Digitigrade/TimelineIncludeable.php deleted file mode 100644 index 44433b4..0000000 --- a/Digitigrade/TimelineIncludeable.php +++ /dev/null @@ -1,6 +0,0 @@ -resharer = $resharer; - } - - public function renderAsHtml() { - render_template('reason_reshared', ['actor' => $this->resharer]); - } -} \ No newline at end of file diff --git a/Digitigrade/TimelineItem.php b/Digitigrade/TimelineItem.php deleted file mode 100644 index e59fbec..0000000 --- a/Digitigrade/TimelineItem.php +++ /dev/null @@ -1,20 +0,0 @@ -reason = $reason; - $this->object = $object; - } - - public function getReason(): ?TimelineInclusionReason { - return $this->reason; - } - - public function getObject(): TimelineIncludeable { - return $this->object; - } -} \ No newline at end of file -- cgit v1.3