diff --git a/components/Subscription/Subscription.php b/components/Subscription/Subscription.php index ad97f8825f..98a78f0375 100644 --- a/components/Subscription/Subscription.php +++ b/components/Subscription/Subscription.php @@ -2,10 +2,107 @@ declare(strict_types = 1); +// {{{ License + +// This file is part of GNU social - https://www.gnu.org/software/social +// +// GNU social is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// GNU social is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with GNU social. If not, see . + +// }}} + namespace Component\Subscription; +use App\Core\DB\DB; +use App\Core\Event; +use function App\Core\I18n\_m; use App\Core\Modules\Component; +use App\Entity\Activity; +use App\Entity\Actor; +use App\Entity\LocalUser; +use App\Util\Exception\ServerException; class Subscription extends Component { + /** + * Persists a new Subscription Entity from Subscriber to Subject (Actor being subscribed) and Activity + * + * A new notification is then handled, informing all interested Actors of this action + * + * @throws ServerException + */ + public static function subscribe(int|Actor|LocalUser $subscriber, int|Actor|LocalUser $subject, string $source = 'web'): ?Activity + { + $subscriber_id = \is_int($subscriber) ? $subscriber : $subscriber->getId(); + $subscribed_id = \is_int($subject) ? $subject : $subject->getId(); + $opts = [ + 'subscriber_id' => $subscriber_id, + 'subscribed_id' => $subscribed_id, + ]; + $subscription = DB::findOneBy(table: \Component\Subscription\Entity\Subscription::class, criteria: $opts, return_null: true); + $activity = null; + if (\is_null($subscription)) { + DB::persist(\Component\Subscription\Entity\Subscription::create($opts)); + $activity = Activity::create([ + 'actor_id' => $subscriber_id, + 'verb' => 'subscribe', + 'object_type' => 'actor', + 'object_id' => $subscribed_id, + 'source' => $source, + ]); + DB::persist($activity); + + Event::handle('NewNotification', [ + $actor = ($subscriber instanceof Actor ? $subscriber : Actor::getById($subscribed_id)), + $activity, + ['object' => [$subscribed_id]], + _m('{nickname} subscribed to {subject}.', ['{actor}' => $actor->getId(), '{subject}' => $activity->getObjectId()]), + ]); + } + return $activity; + } + + /** + * Removes the Subscription Entity created beforehand, by the same Actor, and on the same subject + * + * Informs all interested Actors of this action, handling out the NewNotification event + * + * @throws ServerException + */ + public static function unsubscribe(int|Actor|LocalUser $subscriber, int|Actor|LocalUser $subject, string $source = 'web'): ?Activity + { + $subscriber_id = \is_int($subscriber) ? $subscriber : $subscriber->getId(); + $subscribed_id = \is_int($subject) ? $subject : $subject->getId(); + $opts = [ + 'subscriber_id' => $subscriber_id, + 'subscribed_id' => $subscribed_id, + ]; + $subscription = DB::findOneBy(table: \Component\Subscription\Entity\Subscription::class, criteria: $opts, return_null: true); + $activity = null; + if (!\is_null($subscription)) { + // Remove Subscription + DB::remove($subscription); + $previous_follow_activity = DB::findBy('activity', ['verb' => 'subscribe', 'object_type' => 'actor', 'object_id' => $subscribed_id], order_by: ['created' => 'DESC'])[0]; + // Store Activity + $activity = Activity::create([ + 'actor_id' => $subscriber_id, + 'verb' => 'undo', + 'object_type' => 'activity', + 'object_id' => $previous_follow_activity->getId(), + 'source' => $source, + ]); + DB::persist($activity); + } + return $activity; + } } diff --git a/plugins/ActivityPub/ActivityPub.php b/plugins/ActivityPub/ActivityPub.php index 1fb15e686e..8dce380db6 100644 --- a/plugins/ActivityPub/ActivityPub.php +++ b/plugins/ActivityPub/ActivityPub.php @@ -494,6 +494,13 @@ class ActivityPub extends Plugin } } } + + // Try known remote + $aprofile = DB::findOneBy(ActivitypubActor::class, ['uri' => $resource], return_null: true); + if (!\is_null($aprofile)) { + return Actor::getById($aprofile->getActorId()); + } + // Try remote if ($try_online) { $aprofile = ActivitypubActor::getByAddr($resource); diff --git a/plugins/ActivityPub/Util/Model/Activity.php b/plugins/ActivityPub/Util/Model/Activity.php index bae6636e00..ad99d34935 100644 --- a/plugins/ActivityPub/Util/Model/Activity.php +++ b/plugins/ActivityPub/Util/Model/Activity.php @@ -32,7 +32,6 @@ declare(strict_types = 1); namespace Plugin\ActivityPub\Util\Model; -use _PHPStan_4a258568e\Nette\NotImplementedException; use ActivityPhp\Type; use ActivityPhp\Type\AbstractObject; use App\Core\Event; @@ -41,6 +40,7 @@ use App\Entity\Activity as GSActivity; use App\Util\Exception\ClientException; use App\Util\Exception\NoSuchActorException; use App\Util\Exception\NotFoundException; +use App\Util\Exception\NotImplementedException; use DateTimeInterface; use InvalidArgumentException; use Plugin\ActivityPub\ActivityPub; @@ -168,7 +168,7 @@ class Activity extends Model $uri = match ($object->getObjectType()) { 'note' => Router::url('note_view', ['id' => $object->getObjectId()], type: Router::ABSOLUTE_URL), 'actor' => Router::url('actor_view_id', ['id' => $object->getObjectId()], type: Router::ABSOLUTE_URL), - default => throw new \App\Util\Exception\NotImplementedException(), + default => throw new NotImplementedException(), }; $attr['object'] = Type::create('Tombstone', [ 'id' => $uri, diff --git a/plugins/ActivityPub/Util/Model/ActivityFollow.php b/plugins/ActivityPub/Util/Model/ActivityFollow.php index 4e0a88b0fb..94368bd056 100644 --- a/plugins/ActivityPub/Util/Model/ActivityFollow.php +++ b/plugins/ActivityPub/Util/Model/ActivityFollow.php @@ -35,7 +35,8 @@ namespace Plugin\ActivityPub\Util\Model; use ActivityPhp\Type\AbstractObject; use App\Core\DB\DB; use App\Entity\Activity as GSActivity; -use Component\Subscription\Entity\Subscription; +use App\Util\Exception\ClientException; +use Component\Subscription\Subscription; use DateTime; use InvalidArgumentException; use Plugin\ActivityPub\Entity\ActivitypubActivity; @@ -51,28 +52,17 @@ class ActivityFollow extends Activity protected static function handle_core_activity(\App\Entity\Actor $actor, AbstractObject $type_activity, mixed $type_object, ?ActivitypubActivity &$ap_act): ActivitypubActivity { if ($type_object instanceof AbstractObject) { - $subscribed = Actor::fromJson($type_object); + $subscribed = Actor::fromJson($type_object)->getActorId(); } elseif ($type_object instanceof \App\Entity\Actor) { $subscribed = $type_object; } else { throw new InvalidArgumentException('Follow{:Object} should be either an AbstractObject or an Actor.'); } - // Store Subscription - DB::persist(Subscription::create([ - 'subscriber_id' => $actor->getId(), - 'subscribed_id' => $subscribed->getActorId(), - 'created' => new DateTime($type_activity->get('published') ?? 'now'), - ])); - // Store Activity - $act = GSActivity::create([ - 'actor_id' => $actor->getId(), - 'verb' => 'subscribe', - 'object_type' => 'actor', - 'object_id' => $subscribed->getActorId(), - 'created' => new DateTime($type_activity->get('published') ?? 'now'), - 'source' => 'ActivityPub', - ]); - DB::persist($act); + // Execute Subscribe + $act = Subscription::subscribe($actor, $subscribed, 'ActivityPub'); + if (\is_null($act)) { + throw new ClientException('You are already subscribed to this actor.'); + } // Store ActivityPub Activity $ap_act = ActivitypubActivity::create([ 'activity_id' => $act->getId(), @@ -86,21 +76,11 @@ class ActivityFollow extends Activity public static function handle_undo(\App\Entity\Actor $actor, AbstractObject $type_activity, GSActivity $type_object, ?ActivitypubActivity &$ap_act): ActivitypubActivity { - // Remove Subscription - DB::removeBy(Subscription::class, [ - 'subscriber_id' => $type_object->getActorId(), - 'subscribed_id' => $type_object->getObjectId(), - ]); - // Store Activity - $act = GSActivity::create([ - 'actor_id' => $actor->getId(), - 'verb' => 'undo', - 'object_type' => 'activity', - 'object_id' => $type_object->getId(), - 'created' => new DateTime($type_activity->get('published') ?? 'now'), - 'source' => 'ActivityPub', - ]); - DB::persist($act); + // Execute Unsubscribe + $act = Subscription::unsubscribe($actor, $type_object->getObjectId(), 'ActivityPub'); + if (\is_null($act)) { + throw new ClientException('You are already unsubscribed of this actor.'); + } // Store ActivityPub Activity $ap_act = ActivitypubActivity::create([ 'activity_id' => $act->getId(), diff --git a/plugins/ActivityPub/Util/Model/Actor.php b/plugins/ActivityPub/Util/Model/Actor.php index da46fd8ad3..e203d55bb6 100644 --- a/plugins/ActivityPub/Util/Model/Actor.php +++ b/plugins/ActivityPub/Util/Model/Actor.php @@ -91,7 +91,7 @@ class Actor extends Model } if (!isset($options['objects']['Actor'])) { - DB::persist($actor); + DB::wrapInTransaction(fn () => DB::persist($actor)); } // ActivityPub Actor @@ -104,7 +104,7 @@ class Actor extends Model ], $options['objects']['ActivitypubActor'] ?? null); if (!isset($options['objects']['ActivitypubActor'])) { - DB::persist($ap_actor); + DB::wrapInTransaction(fn () => DB::persist($ap_actor)); } // Public Key @@ -114,7 +114,7 @@ class Actor extends Model ], $options['objects']['ActivitypubRsa'] ?? null); if (!isset($options['objects']['ActivitypubRsa'])) { - DB::persist($apRSA); + DB::wrapInTransaction(fn () => DB::persist($apRSA)); } // Avatar diff --git a/plugins/Favourite/Favourite.php b/plugins/Favourite/Favourite.php index 5146f3784c..d3df29c6c9 100644 --- a/plugins/Favourite/Favourite.php +++ b/plugins/Favourite/Favourite.php @@ -25,6 +25,7 @@ namespace Plugin\Favourite; use App\Core\DB\DB; use App\Core\Event; +use function App\Core\I18n\_m; use App\Core\Modules\NoteHandlerPlugin; use App\Core\Router\RouteLoader; use App\Core\Router\Router; @@ -38,7 +39,6 @@ use App\Util\Nickname; use DateTime; use Plugin\Favourite\Entity\NoteFavourite as FavouriteEntity; use Symfony\Component\HttpFoundation\Request; -use function App\Core\I18n\_m; class Favourite extends NoteHandlerPlugin { @@ -48,33 +48,28 @@ class Favourite extends NoteHandlerPlugin * * A new notification is then handled, informing all interested Actors of this action * - * @param int $note_id - * @param int $actor_id - * @param string $source - * - * @return \App\Entity\Activity|null * @throws \App\Util\Exception\ServerException - * @throws \Doctrine\ORM\ORMException * @throws \Doctrine\ORM\OptimisticLockException + * @throws \Doctrine\ORM\ORMException * @throws \Doctrine\ORM\TransactionRequiredException */ public static function favourNote(int $note_id, int $actor_id, string $source = 'web'): ?Activity { - $opts = ['note_id' => $note_id, 'actor_id' => $actor_id]; - $note_already_favoured = DB::find('note_favourite', $opts); - $activity = null; + $opts = ['note_id' => $note_id, 'actor_id' => $actor_id]; + $note_already_favoured = DB::findOneBy('note_favourite', $opts, return_null: true); + $activity = null; if (\is_null($note_already_favoured)) { DB::persist(FavouriteEntity::create($opts)); $activity = Activity::create([ - 'actor_id' => $actor_id, - 'verb' => 'favourite', + 'actor_id' => $actor_id, + 'verb' => 'favourite', 'object_type' => 'note', - 'object_id' => $note_id, - 'source' => $source, + 'object_id' => $note_id, + 'source' => $source, ]); DB::persist($activity); - Event::handle('NewNotification', [$actor = Actor::getById($actor_id), $activity, [], _m('{nickname} favoured note {note_id}.', ['nickname' => $actor->getNickname(), 'note_id' => $activity->getObjectId()])]); + Event::handle('NewNotification', [$actor = Actor::getById($actor_id), $activity, [], _m('{nickname} favoured note {note_id}.', ['{nickname}' => $actor->getNickname(), '{note_id}' => $activity->getObjectId()])]); } return $activity; } @@ -84,33 +79,28 @@ class Favourite extends NoteHandlerPlugin * * Informs all interested Actors of this action, handling out the NewNotification event * - * @param int $note_id - * @param int $actor_id - * @param string $source - * - * @return \App\Entity\Activity|null * @throws \App\Util\Exception\ServerException - * @throws \Doctrine\ORM\ORMException * @throws \Doctrine\ORM\OptimisticLockException + * @throws \Doctrine\ORM\ORMException * @throws \Doctrine\ORM\TransactionRequiredException */ public static function unfavourNote(int $note_id, int $actor_id, string $source = 'web'): ?Activity { - $note_already_favoured = DB::find('note_favourite', ['note_id' => $note_id, 'actor_id' => $actor_id]); - $activity = null; + $note_already_favoured = DB::findOneBy('note_favourite', ['note_id' => $note_id, 'actor_id' => $actor_id], return_null: true); + $activity = null; if (!\is_null($note_already_favoured)) { DB::remove($note_already_favoured); - $favourite_activity = DB::findBy('activity', ['verb' => 'favourite', 'object_type' => 'note', 'object_id' => $note_id], order_by: ['created' => 'DESC'])[ 0 ]; - $activity = Activity::create([ - 'actor_id' => $actor_id, - 'verb' => 'undo', // 'undo_favourite', + $favourite_activity = DB::findBy('activity', ['verb' => 'favourite', 'object_type' => 'note', 'object_id' => $note_id], order_by: ['created' => 'DESC'])[0]; + $activity = Activity::create([ + 'actor_id' => $actor_id, + 'verb' => 'undo', // 'undo_favourite', 'object_type' => 'activity', // 'note', - 'object_id' => $favourite_activity->getId(), // $note_id, - 'source' => $source, + 'object_id' => $favourite_activity->getId(), // $note_id, + 'source' => $source, ]); DB::persist($activity); - Event::handle('NewNotification', [$actor = Actor::getById($actor_id), $activity, [], _m('{nickname} unfavoured note {note_id}.', ['nickname' => $actor->getNickname(), 'note_id' => $activity->getObjectId()])]); + Event::handle('NewNotification', [$actor = Actor::getById($actor_id), $activity, [], _m('{nickname} unfavoured note {note_id}.', ['{nickname}' => $actor->getNickname(), '{note_id}' => $activity->getObjectId()])]); } return $activity; } @@ -119,14 +109,11 @@ class Favourite extends NoteHandlerPlugin * HTML rendering event that adds the favourite form as a note * action, if a user is logged in * - * @param \Symfony\Component\HttpFoundation\Request $request - * @param \App\Entity\Note $note - * @param array $actions + * @throws \Doctrine\ORM\OptimisticLockException + * @throws \Doctrine\ORM\ORMException + * @throws \Doctrine\ORM\TransactionRequiredException * * @return bool Event hook - * @throws \Doctrine\ORM\ORMException - * @throws \Doctrine\ORM\OptimisticLockException - * @throws \Doctrine\ORM\TransactionRequiredException */ public function onAddNoteActions(Request $request, Note $note, array &$actions): bool { @@ -135,12 +122,12 @@ class Favourite extends NoteHandlerPlugin } // If note is favourite, "is_favourite" is 1 - $opts = ['note_id' => $note->getId(), 'actor_id' => $user->getId()]; - $is_favourite = DB::find('note_favourite', $opts) !== null; + $opts = ['note_id' => $note->getId(), 'actor_id' => $user->getId()]; + $is_favourite = !\is_null(DB::findOneBy('note_favourite', $opts, return_null: true)); // Generating URL for favourite action route - $args = ['id' => $note->getId()]; - $type = Router::ABSOLUTE_PATH; + $args = ['id' => $note->getId()]; + $type = Router::ABSOLUTE_PATH; $favourite_action_url = $is_favourite ? Router::url('favourite_remove', $args, $type) : Router::url('favourite_add', $args, $type); @@ -149,12 +136,12 @@ class Favourite extends NoteHandlerPlugin // Concatenating get parameter to redirect the user to where he came from $favourite_action_url .= '?from=' . urlencode($request->getRequestUri()); - $extra_classes = $is_favourite ? 'note-actions-set' : 'note-actions-unset'; + $extra_classes = $is_favourite ? 'note-actions-set' : 'note-actions-unset'; $favourite_action = [ - 'url' => $favourite_action_url, - 'title' => $is_favourite ? 'Remove this note from favourites' : 'Favourite this note!', + 'url' => $favourite_action_url, + 'title' => $is_favourite ? 'Remove this note from favourites' : 'Favourite this note!', 'classes' => "button-container favourite-button-container {$extra_classes}", - 'id' => 'favourite-button-container-' . $note->getId(), + 'id' => 'favourite-button-container-' . $note->getId(), ]; $actions[] = $favourite_action; @@ -168,7 +155,7 @@ class Favourite extends NoteHandlerPlugin $check_user = !\is_null(Common::user()); // The current Note being rendered - $note = $vars[ 'note' ]; + $note = $vars['note']; // Will have actors array, and action string // Actors are the subjects, action is the verb (in the final phrase) @@ -179,18 +166,13 @@ class Favourite extends NoteHandlerPlugin } // Filter out multiple replies from the same actor - $favourite_actors = array_unique($favourite_actors, SORT_REGULAR); - $result[] = ['actors' => $favourite_actors, 'action' => 'favourited']; + $favourite_actors = array_unique($favourite_actors, \SORT_REGULAR); + $result[] = ['actors' => $favourite_actors, 'action' => 'favourited']; return Event::next; } /** * Deletes every favourite entity in table related to a deleted Note - * - * @param \App\Entity\Note $note - * @param \App\Entity\Actor $actor - * - * @return bool */ public function onNoteDeleteRelated(Note &$note, Actor $actor): bool {