diff --git a/components/Attachment/Attachment.php b/components/Attachment/Attachment.php index b2dce2c73b..675265bd42 100644 --- a/components/Attachment/Attachment.php +++ b/components/Attachment/Attachment.php @@ -77,7 +77,7 @@ class Attachment extends Component /** * Populate $note_expr with the criteria for looking for notes with attachments */ - public function onSearchCreateExpression(ExpressionBuilder $eb, string $term, ?string $language, ?Actor $actor, &$note_expr, &$actor_expr) + public function onSearchCreateExpression(ExpressionBuilder $eb, string $term, ?string $language, ?Actor $actor, &$note_expr, &$actor_expr): bool { $include_term = str_contains($term, ':') ? explode(':', $term)[1] : $term; if (Formatting::startsWith($term, ['note-types:', 'notes-incude:', 'note-filter:'])) { diff --git a/components/Feed/Feed.php b/components/Feed/Feed.php index e7d184aa1d..d5b3768841 100644 --- a/components/Feed/Feed.php +++ b/components/Feed/Feed.php @@ -28,10 +28,10 @@ use App\Core\Event; use App\Core\Modules\Component; use App\Core\Router\RouteLoader; use App\Entity\Actor; -use App\Entity\Subscription; use App\Util\Formatting; use Component\Feed\Controller as C; use Component\Search\Util\Parser; +use Component\Subscription\Entity\Subscription; use Doctrine\Common\Collections\ExpressionBuilder; use Doctrine\ORM\Query\Expr; use Doctrine\ORM\QueryBuilder; @@ -80,9 +80,9 @@ class Feed extends Component return ['notes' => $notes ?? null, 'actors' => $actors ?? null]; } - public function onSearchQueryAddJoins(QueryBuilder &$note_qb, QueryBuilder &$actor_qb) + public function onSearchQueryAddJoins(QueryBuilder &$note_qb, QueryBuilder &$actor_qb): bool { - $note_qb->leftJoin(Subscription::class, 'subscription', Expr\Join::WITH, 'note.actor_id = subscription.subscribed') + $note_qb->leftJoin(Subscription::class, 'subscription', Expr\Join::WITH, 'note.actor_id = subscription.subscribed_id') ->leftJoin(Actor::class, 'note_actor', Expr\Join::WITH, 'note.actor_id = note_actor.id'); return Event::next; } @@ -117,7 +117,7 @@ class Feed extends Component break; case 'note-from': case 'notes-from': - $subscribed_expr = $eb->eq('subscription.subscriber', $actor->getId()); + $subscribed_expr = $eb->eq('subscription.subscriber_id', $actor->getId()); $type_consts = []; if ($term[1] === 'subscribed') { $type_consts = null; diff --git a/components/Feed/templates/feed/feed.html.twig b/components/Feed/templates/feed/feed.html.twig index f4acd6020d..f8677e9e32 100644 --- a/components/Feed/templates/feed/feed.html.twig +++ b/components/Feed/templates/feed/feed.html.twig @@ -9,18 +9,16 @@ {% endblock stylesheets %} {% block body %} - {% if notes is defined and notes is not empty %} -
- {% if page_title is defined %} -

{{ page_title | trans }}

- {% endif %} - -
+
+ {% if page_title is defined %} +

{{ page_title | trans }}

{% endif %} + +
{# Backwards compatibility with hAtom 0.1 #}
diff --git a/components/Language/Language.php b/components/Language/Language.php index e91821bba6..41793a3c41 100644 --- a/components/Language/Language.php +++ b/components/Language/Language.php @@ -38,13 +38,13 @@ use Symfony\Component\HttpFoundation\Request; class Language extends Component { - public function onAddRoute(RouteLoader $r) + public function onAddRoute(RouteLoader $r): bool { $r->connect('settings_sort_languages', '/settings/sort_languages', [C\Language::class, 'sortLanguages']); return Event::next; } - public function onFilterNoteList(?Actor $actor, array &$notes, Request $request) + public function onFilterNoteList(?Actor $actor, array &$notes, Request $request): bool { if (\is_null($actor)) { return Event::next; @@ -60,7 +60,7 @@ class Language extends Component /** * Populate $note_expr or $actor_expr with an expression to match a language */ - public function onSearchCreateExpression(ExpressionBuilder $eb, string $term, ?string $language, ?Actor $actor, &$note_expr, &$actor_expr) + public function onSearchCreateExpression(ExpressionBuilder $eb, string $term, ?string $language, ?Actor $actor, &$note_expr, &$actor_expr): bool { $search_term = str_contains($term, ':') ? explode(':', $term)[1] : $term; diff --git a/src/Entity/Subscription.php b/components/Subscription/Entity/Subscription.php similarity index 51% rename from src/Entity/Subscription.php rename to components/Subscription/Entity/Subscription.php index 3a67f45a81..ea7403c868 100644 --- a/src/Entity/Subscription.php +++ b/components/Subscription/Entity/Subscription.php @@ -1,5 +1,7 @@ . // }}} -namespace App\Entity; +namespace Component\Subscription\Entity; use App\Core\Entity; -use DateTimeInterface; +use App\Entity\Actor; +use App\Entity\LocalUser; use Component\Group\Entity\LocalGroup; +use DateTimeInterface; /** * Entity for subscription @@ -41,51 +45,51 @@ class Subscription extends Entity { // {{{ Autocode // @codeCoverageIgnoreStart - private int $subscriber; - private int $subscribed; - private \DateTimeInterface $created; - private \DateTimeInterface $modified; + private int $subscriber_id; + private int $subscribed_id; + private DateTimeInterface $created; + private DateTimeInterface $modified; - public function setSubscriber(int $subscriber): self + public function setSubscriberId(int $subscriber_id): self { - $this->subscriber = $subscriber; + $this->subscriber_id = $subscriber_id; return $this; } - public function getSubscriber(): int + public function getSubscriberId(): int { - return $this->subscriber; + return $this->subscriber_id; } - public function setSubscribed(int $subscribed): self + public function setSubscribedId(int $subscribed_id): self { - $this->subscribed = $subscribed; + $this->subscribed_id = $subscribed_id; return $this; } - public function getSubscribed(): int + public function getSubscribedId(): int { - return $this->subscribed; + return $this->subscribed_id; } - public function setCreated(\DateTimeInterface $created): self + public function setCreated(DateTimeInterface $created): self { $this->created = $created; return $this; } - public function getCreated(): \DateTimeInterface + public function getCreated(): DateTimeInterface { return $this->created; } - public function setModified(\DateTimeInterface $modified): self + public function setModified(DateTimeInterface $modified): self { $this->modified = $modified; return $this; } - public function getModified(): \DateTimeInterface + public function getModified(): DateTimeInterface { return $this->modified; } @@ -93,6 +97,16 @@ class Subscription extends Entity // @codeCoverageIgnoreEnd // }}} Autocode + public function getSubscriber(): Actor + { + return Actor::getById($this->getSubscriberId()); + } + + public function getSubscribed(): Actor + { + return Actor::getById($this->getSubscribedId()); + } + public static function cacheKeys(LocalUser|LocalGroup|Actor $subject, LocalUser|LocalGroup|Actor $target): array { return [ @@ -105,15 +119,15 @@ class Subscription extends Entity return [ 'name' => 'subscription', 'fields' => [ - 'subscriber' => ['type' => 'int', 'foreign key' => true, 'target' => 'Actor.id', 'multiplicity' => 'one to one', 'name' => 'subscrib_subscriber_fkey', 'not null' => true, 'description' => 'actor listening'], - 'subscribed' => ['type' => 'int', 'foreign key' => true, 'target' => 'Actor.id', 'multiplicity' => 'one to one', 'name' => 'subscrib_subscribed_fkey', 'not null' => true, 'description' => 'actor being listened to'], - 'created' => ['type' => 'datetime', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was created'], - 'modified' => ['type' => 'timestamp', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was modified'], + 'subscriber_id' => ['type' => 'int', 'foreign key' => true, 'target' => 'Actor.id', 'multiplicity' => 'one to one', 'name' => 'subscription_subscriber_fkey', 'not null' => true, 'description' => 'actor listening'], + 'subscribed_id' => ['type' => 'int', 'foreign key' => true, 'target' => 'Actor.id', 'multiplicity' => 'one to one', 'name' => 'subscription_subscribed_fkey', 'not null' => true, 'description' => 'actor being listened to'], + 'created' => ['type' => 'datetime', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was created'], + 'modified' => ['type' => 'timestamp', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was modified'], ], - 'primary key' => ['subscriber', 'subscribed'], + 'primary key' => ['subscriber_id', 'subscribed_id'], 'indexes' => [ - 'subscrib_subscriber_idx' => ['subscriber', 'created'], - 'subscrib_subscribed_idx' => ['subscribed', 'created'], + 'subscription_subscriber_idx' => ['subscriber_id', 'created'], + 'subscription_subscribed_idx' => ['subscribed_id', 'created'], ], ]; } diff --git a/components/Subscription/Subscription.php b/components/Subscription/Subscription.php new file mode 100644 index 0000000000..ad97f8825f --- /dev/null +++ b/components/Subscription/Subscription.php @@ -0,0 +1,11 @@ +getIsLocal()) { - return $object->getUrl(); - } else { - // Try known remote objects - $known_object = ActivitypubObject::getByPK(['object_type' => 'note', 'object_id' => $object->getId()]); - if ($known_object instanceof ActivitypubObject) { - return $known_object->getObjectUri(); + switch ($object::class) { + case Note::class: + if ($object->getIsLocal()) { + return $object->getUrl(); + } else { + // Try known remote objects + $known_object = ActivitypubObject::getByPK(['object_type' => 'note', 'object_id' => $object->getId()]); + if ($known_object instanceof ActivitypubObject) { + return $known_object->getObjectUri(); + } else { + throw new BugFoundException('ActivityPub cannot generate an URI for a stored note.', [$object, $known_object]); + } } - } - } elseif ($object instanceof Activity) { - // Try known remote activities - $known_activity = ActivitypubActivity::getByPK(['activity_id' => $object->getId()]); - if ($known_activity instanceof ActivitypubActivity) { - return $known_activity->getActivityUri(); - } else { - return Router::url('activity_view', ['id' => $object->getId()], Router::ABSOLUTE_URL); - } + break; + case Actor::class: + return $object->getUri(); + break; + case Activity::class: + // Try known remote activities + $known_activity = ActivitypubActivity::getByPK(['activity_id' => $object->getId()]); + if ($known_activity instanceof ActivitypubActivity) { + return $known_activity->getActivityUri(); + } else { + return Router::url('activity_view', ['id' => $object->getId()], Router::ABSOLUTE_URL); + } + break; + default: + throw new InvalidArgumentException('ActivityPub::getUriByObject found a limitation with: ' . var_export($object, true)); } - throw new InvalidArgumentException('ActivityPub::getUriByObject found a limitation with: ' . var_export($object, true)); } /** @@ -407,31 +417,38 @@ class ActivityPub extends Plugin */ public static function getObjectByUri(string $resource, bool $try_online = true) { - // Try known objects + // Try known object $known_object = ActivitypubObject::getByPK(['object_uri' => $resource]); if ($known_object instanceof ActivitypubObject) { return $known_object->getObject(); } - // Try known activities + // Try known activity $known_activity = ActivitypubActivity::getByPK(['activity_uri' => $resource]); if ($known_activity instanceof ActivitypubActivity) { return $known_activity->getActivity(); } - // Try local Notes (pretty incomplete effort, I know) + // Try local Note if (Common::isValidHttpUrl($resource)) { // This means $resource is a valid url $resource_parts = parse_url($resource); // TODO: Use URLMatcher if ($resource_parts['host'] === $_ENV['SOCIAL_DOMAIN']) { // XXX: Common::config('site', 'server')) { - $local_note = DB::findOneBy('note', ['url' => $resource]); + $local_note = DB::findOneBy('note', ['url' => $resource], return_null: true); if ($local_note instanceof Note) { return $local_note; } } } + // Try Actor + try { + return self::getActorByUri($resource, try_online: false); + } catch (Exception) { + // Ignore, this is brute forcing, it's okay not to find + } + // Try remote if (!$try_online) { return; @@ -457,7 +474,7 @@ class ActivityPub extends Plugin * * @return Actor got from URI */ - public static function getActorByUri(string $resource): Actor + public static function getActorByUri(string $resource, bool $try_online = true): Actor { // Try local if (Common::isValidHttpUrl($resource)) { @@ -478,11 +495,12 @@ class ActivityPub extends Plugin } } // Try remote - $aprofile = ActivitypubActor::getByAddr($resource); - if ($aprofile instanceof ActivitypubActor) { - return Actor::getById($aprofile->getActorId()); - } else { - throw new NoSuchActorException("From URI: {$resource}"); + if ($try_online) { + $aprofile = ActivitypubActor::getByAddr($resource); + if ($aprofile instanceof ActivitypubActor) { + return Actor::getById($aprofile->getActorId()); + } } + throw new NoSuchActorException("From URI: {$resource}"); } } diff --git a/src/Entity/SubscriptionQueue.php b/plugins/ActivityPub/Entity/ActivitypubFollowRequestQueue.php similarity index 72% rename from src/Entity/SubscriptionQueue.php rename to plugins/ActivityPub/Entity/ActivitypubFollowRequestQueue.php index 2376eb18e1..909a642273 100644 --- a/src/Entity/SubscriptionQueue.php +++ b/plugins/ActivityPub/Entity/ActivitypubFollowRequestQueue.php @@ -1,5 +1,7 @@ . // }}} -namespace App\Entity; +namespace Plugin\ActivityPub\Entity; use App\Core\Entity; use DateTimeInterface; @@ -36,13 +38,13 @@ use DateTimeInterface; * @copyright 2020-2021 Free Software Foundation, Inc http://www.fsf.org * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later */ -class SubscriptionQueue extends Entity +class ActivitypubFollowRequestQueue extends Entity { // {{{ Autocode // @codeCoverageIgnoreStart private int $subscriber; private int $subscribed; - private \DateTimeInterface $created; + private DateTimeInterface $created; public function setSubscriber(int $subscriber): self { @@ -66,13 +68,13 @@ class SubscriptionQueue extends Entity return $this->subscribed; } - public function setCreated(\DateTimeInterface $created): self + public function setCreated(DateTimeInterface $created): self { $this->created = $created; return $this; } - public function getCreated(): \DateTimeInterface + public function getCreated(): DateTimeInterface { return $this->created; } @@ -83,17 +85,17 @@ class SubscriptionQueue extends Entity public static function schemaDef(): array { return [ - 'name' => 'subscription_queue', + 'name' => 'activitypub_follow_request_queue', 'description' => 'Holder for Subscription requests awaiting moderation.', 'fields' => [ - 'subscriber' => ['type' => 'int', 'foreign key' => true, 'target' => 'Actor.id', 'multiplicity' => 'many to one', 'name' => 'Subscription_queue_subscriber_fkey', 'not null' => true, 'description' => 'actor making the request'], - 'subscribed' => ['type' => 'int', 'foreign key' => true, 'target' => 'Actor.id', 'multiplicity' => 'many to one', 'name' => 'Subscription_queue_subscribed_fkey', 'not null' => true, 'description' => 'actor being subscribed'], - 'created' => ['type' => 'datetime', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was created'], + 'subscriber' => ['type' => 'int', 'foreign key' => true, 'target' => 'Actor.id', 'multiplicity' => 'many to one', 'name' => 'activitypub_follow_request_queue_subscriber_fkey', 'not null' => true, 'description' => 'actor making the request'], + 'subscribed' => ['type' => 'int', 'foreign key' => true, 'target' => 'Actor.id', 'multiplicity' => 'many to one', 'name' => 'activitypub_follow_request_queue_subscribed_fkey', 'not null' => true, 'description' => 'actor being subscribed'], + 'created' => ['type' => 'datetime', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was created'], ], 'primary key' => ['subscriber', 'subscribed'], 'indexes' => [ - 'subscription_queue_subscriber_created_idx' => ['subscriber', 'created'], - 'subscription_queue_subscribed_created_idx' => ['subscribed', 'created'], + 'activitypub_follow_request_queue_subscriber_created_idx' => ['subscriber', 'created'], + 'activitypub_follow_request_queue_subscribed_created_idx' => ['subscribed', 'created'], ], ]; } diff --git a/plugins/ActivityPub/Util/Model/Activity.php b/plugins/ActivityPub/Util/Model/Activity.php index 557d27ef3b..10bfcf4e86 100644 --- a/plugins/ActivityPub/Util/Model/Activity.php +++ b/plugins/ActivityPub/Util/Model/Activity.php @@ -34,16 +34,13 @@ namespace Plugin\ActivityPub\Util\Model; use ActivityPhp\Type; use ActivityPhp\Type\AbstractObject; -use App\Core\DB\DB; use App\Core\Event; use App\Core\Router\Router; use App\Entity\Activity as GSActivity; use App\Util\Exception\ClientException; use App\Util\Exception\NoSuchActorException; use App\Util\Exception\NotFoundException; -use DateTime; use DateTimeInterface; -use Exception; use InvalidArgumentException; use Plugin\ActivityPub\ActivityPub; use Plugin\ActivityPub\Entity\ActivitypubActivity; @@ -105,34 +102,12 @@ class Activity extends Model private static function handle_core_activity(\App\Entity\Actor $actor, AbstractObject $type_activity, mixed $type_object, ?ActivitypubActivity &$ap_act): ActivitypubActivity { - if ($type_activity->get('type') === 'Create' && $type_object->get('type') === 'Note') { - if ($type_object instanceof AbstractObject) { - $note = Note::fromJson($type_object, ['test_authority' => true, 'actor_uri' => $type_activity->get('actor'), 'actor' => $actor, 'actor_id' => $actor->getId()]); - } else { - if ($type_object instanceof \App\Entity\Note) { - $note = $type_object; - } else { - throw new Exception('dunno bro'); - } - } - // Store Activity - $act = GSActivity::create([ - 'actor_id' => $actor->getId(), - 'verb' => 'create', - 'object_type' => 'note', - 'object_id' => $note->getId(), - 'created' => new DateTime($type_activity->get('published') ?? 'now'), - 'source' => 'ActivityPub', - ]); - DB::persist($act); - // Store ActivityPub Activity - $ap_act = ActivitypubActivity::create([ - 'activity_id' => $act->getId(), - 'activity_uri' => $type_activity->get('id'), - 'created' => new DateTime($type_activity->get('published') ?? 'now'), - 'modified' => new DateTime(), - ]); - DB::persist($ap_act); + switch ($type_activity->get('type')) { + case 'Create': + ActivityCreate::handle_core_activity($actor, $type_activity, $type_object, $ap_act); + break; + case 'Follow': + ActivityFollow::handle_core_activity($actor, $type_activity, $type_object, $ap_act); } return $ap_act; } @@ -145,27 +120,28 @@ class Activity extends Model public static function toJson(mixed $object, ?int $options = null): string { if ($object::class !== GSActivity::class) { - throw new InvalidArgumentException('First argument type is Activity'); + throw new InvalidArgumentException('First argument type must be an Activity.'); } - $gs_verb_to_activity_stream_two_verb = null; - if (Event::handle('GSVerbToActivityStreamsTwoActivityType', [($verb = $object->getVerb()), &$gs_verb_to_activity_stream_two_verb]) === Event::next) { - $gs_verb_to_activity_stream_two_verb = match ($verb) { - 'create' => 'Create', - 'undo' => 'Undo', - default => throw new ClientException('Invalid verb'), + $gs_verb_to_activity_streams_two_verb = null; + if (Event::handle('GSVerbToActivityStreamsTwoActivityType', [($verb = $object->getVerb()), &$gs_verb_to_activity_streams_two_verb]) === Event::next) { + $gs_verb_to_activity_streams_two_verb = match ($verb) { + 'undo' => 'Undo', + 'create' => 'Create', + 'subscribe' => 'Follow', + default => throw new ClientException('Invalid verb'), }; } $attr = [ - 'type' => $gs_verb_to_activity_stream_two_verb, + 'type' => $gs_verb_to_activity_streams_two_verb, '@context' => 'https://www.w3.org/ns/activitystreams', 'id' => Router::url('activity_view', ['id' => $object->getId()], Router::ABSOLUTE_URL), 'published' => $object->getCreated()->format(DateTimeInterface::RFC3339), 'actor' => $object->getActor()->getUri(Router::ABSOLUTE_URL), - 'to' => ['https://www.w3.org/ns/activitystreams#Public'], // TODO: implement proper scope address - 'cc' => ['https://www.w3.org/ns/activitystreams#Public'], ]; + + // Get object or Tombstone try { $object = $object->getObject(); // Throws NotFoundException $attr['object'] = ($attr['type'] === 'Create') ? self::jsonToType(Model::toJson($object)) : ActivityPub::getUriByObject($object); @@ -181,9 +157,13 @@ class Activity extends Model ]); } - if (!\is_string($attr['object'])) { - $attr['to'] = array_unique(array_merge($attr['to'], $attr['object']->get('to') ?? [])); - $attr['cc'] = array_unique(array_merge($attr['cc'], $attr['object']->get('cc') ?? [])); + // If embedded non tombstone Object + if (!\is_string($attr['object']) && $attr['object']->get('type') !== 'Tombstone') { + // Little special case + if ($attr['type'] === 'Create' && $attr['object']->get('type') === 'Note') { + $attr['to'] = $attr['object']->get('to') ?? []; + $attr['cc'] = $attr['object']->get('cc') ?? []; + } } $type = self::jsonToType($attr); diff --git a/plugins/ActivityPub/Util/Model/ActivityCreate.php b/plugins/ActivityPub/Util/Model/ActivityCreate.php new file mode 100644 index 0000000000..5f9957bdd0 --- /dev/null +++ b/plugins/ActivityPub/Util/Model/ActivityCreate.php @@ -0,0 +1,84 @@ +. +// }}} + +/** + * ActivityPub implementation for GNU social + * + * @package GNUsocial + * @category ActivityPub + * + * @author Diogo Peralta Cordeiro <@diogo.site> + * @copyright 2021 Free Software Foundation, Inc http://www.fsf.org + * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later + */ + +namespace Plugin\ActivityPub\Util\Model; + +use _PHPStan_76800bfb5\Nette\NotImplementedException; +use ActivityPhp\Type\AbstractObject; +use App\Core\DB\DB; +use App\Entity\Activity as GSActivity; +use DateTime; +use Plugin\ActivityPub\ActivityPub; +use Plugin\ActivityPub\Entity\ActivitypubActivity; + +/** + * This class handles translation between JSON and ActivityPub Activities + * + * @copyright 2021 Free Software Foundation, Inc http://www.fsf.org + * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later + */ +class ActivityCreate 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) { + if ($type_object->get('type') === 'Note') { + $note = Note::fromJson($type_object, ['test_authority' => true, 'actor_uri' => $type_activity->get('actor'), 'actor' => $actor, 'actor_id' => $actor->getId()]); + } else { + throw new NotImplementedException('ActivityPub plugin can only handle Create with objects of type Note.'); + } + } elseif ($type_object instanceof \App\Entity\Note) { + $note = $type_object; + } else { + throw new \http\Exception\InvalidArgumentException('Create{:Object} should be either an AbstractObject or a Note.'); + } + // Store Activity + $act = GSActivity::create([ + 'actor_id' => $actor->getId(), + 'verb' => 'create', + 'object_type' => 'note', + 'object_id' => $note->getId(), + 'created' => new DateTime($type_activity->get('published') ?? 'now'), + 'source' => 'ActivityPub', + ]); + DB::persist($act); + // Store ActivityPub Activity + $ap_act = ActivitypubActivity::create([ + 'activity_id' => $act->getId(), + 'activity_uri' => $type_activity->get('id'), + 'created' => new DateTime($type_activity->get('published') ?? 'now'), + 'modified' => new DateTime(), + ]); + DB::persist($ap_act); + return $ap_act; + } +} diff --git a/plugins/ActivityPub/Util/Model/ActivityFollow.php b/plugins/ActivityPub/Util/Model/ActivityFollow.php new file mode 100644 index 0000000000..ff1d2601a3 --- /dev/null +++ b/plugins/ActivityPub/Util/Model/ActivityFollow.php @@ -0,0 +1,86 @@ +. +// }}} + +/** + * ActivityPub implementation for GNU social + * + * @package GNUsocial + * @category ActivityPub + * + * @author Diogo Peralta Cordeiro <@diogo.site> + * @copyright 2021 Free Software Foundation, Inc http://www.fsf.org + * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later + */ + +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 DateTime; +use InvalidArgumentException; +use Plugin\ActivityPub\Entity\ActivitypubActivity; + +/** + * This class handles translation between JSON and ActivityPub Activities + * + * @copyright 2021 Free Software Foundation, Inc http://www.fsf.org + * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later + */ +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); + } 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); + // Store ActivityPub Activity + $ap_act = ActivitypubActivity::create([ + 'activity_id' => $act->getId(), + 'activity_uri' => $type_activity->get('id'), + 'created' => new DateTime($type_activity->get('published') ?? 'now'), + 'modified' => new DateTime(), + ]); + DB::persist($ap_act); + return $ap_act; + } +} diff --git a/plugins/ActivityPub/Util/Model/Actor.php b/plugins/ActivityPub/Util/Model/Actor.php index c3ab3546a4..da46fd8ad3 100644 --- a/plugins/ActivityPub/Util/Model/Actor.php +++ b/plugins/ActivityPub/Util/Model/Actor.php @@ -177,7 +177,7 @@ class Actor extends Model public static function toJson(mixed $object, ?int $options = null): string { if ($object::class !== GSActor::class) { - throw new InvalidArgumentException('First argument type is Actor'); + throw new InvalidArgumentException('First argument type must be an Actor.'); } $rsa = ActivitypubRsa::getByActor($object); $public_key = $rsa->getPublicKey(); diff --git a/plugins/ActivityPub/Util/Model/Note.php b/plugins/ActivityPub/Util/Model/Note.php index fd8c1272cf..f2389d57c1 100644 --- a/plugins/ActivityPub/Util/Model/Note.php +++ b/plugins/ActivityPub/Util/Model/Note.php @@ -308,7 +308,7 @@ class Note extends Model public static function toJson(mixed $object, ?int $options = null): string { if ($object::class !== GSNote::class) { - throw new InvalidArgumentException('First argument type is Note'); + throw new InvalidArgumentException('First argument type must be a Note.'); } $attr = [ diff --git a/plugins/Directory/Controller/Directory.php b/plugins/Directory/Controller/Directory.php index 9cddd0b06a..068e75dc70 100644 --- a/plugins/Directory/Controller/Directory.php +++ b/plugins/Directory/Controller/Directory.php @@ -121,7 +121,7 @@ class Directory extends FeedController }, 'subscribers' => match ($actor_type) { // select by actors with most/least subscribers/members - Actor::PERSON => $count_query_fn(table: 'subscription', join_field: 'subscribed', aggregate_field: 'subscriber'), + Actor::PERSON => $count_query_fn(table: 'subscription', join_field: 'subscribed_id', aggregate_field: 'subscriber_id'), Actor::GROUP => $count_query_fn(table: 'group_member', join_field: 'group_id', aggregate_field: 'actor_id'), }, diff --git a/src/Controller/Security.php b/src/Controller/Security.php index f0e6cb77b9..49cb39098e 100644 --- a/src/Controller/Security.php +++ b/src/Controller/Security.php @@ -14,7 +14,6 @@ use App\Core\UserRoles; use App\Entity\Actor; use App\Entity\Feed; use App\Entity\LocalUser; -use App\Entity\Subscription; use App\Security\Authenticator; use App\Security\EmailVerifier; use App\Util\Common; @@ -30,6 +29,7 @@ use App\Util\Exception\NotFoundException; use App\Util\Exception\ServerException; use App\Util\Form\FormFields; use App\Util\Nickname; +use Component\Subscription\Entity\Subscription; use Doctrine\DBAL\Exception\UniqueConstraintViolationException; use LogicException; use Symfony\Component\Form\Extension\Core\Type\EmailType; @@ -164,7 +164,7 @@ class Security extends Controller $user, function (int $id) use ($user) { // Self subscription - DB::persist(Subscription::create(['subscriber' => $id, 'subscribed' => $id])); + DB::persist(Subscription::create(['subscriber_id' => $id, 'subscribed_id' => $id])); Feed::createDefaultFeeds($id, $user); }, ); diff --git a/src/Entity/Actor.php b/src/Entity/Actor.php index 256e587374..8aa6f0550f 100644 --- a/src/Entity/Actor.php +++ b/src/Entity/Actor.php @@ -384,12 +384,12 @@ class Actor extends Entity public function getSubscribersCount(): int { - return $this->getSubCount(which: 'subscriber', column: 'subscribed'); + return $this->getSubCount(which: 'subscriber', column: 'subscribed_id'); } public function getSubscribedCount() { - return $this->getSubCount(which: 'subscribed', column: 'subscriber'); + return $this->getSubCount(which: 'subscribed', column: 'subscriber_id'); } /** @@ -411,8 +411,8 @@ class Actor extends Entity fn () => DB::dql( <<<'EOF' select a from actor a where - a.id in (select fa.subscribed from subscription fa join actor aa with fa.subscribed = aa.id where fa.subscriber = :actor_id and aa.nickname = :nickname) or - a.id in (select fb.subscriber from subscription fb join actor ab with fb.subscriber = ab.id where fb.subscribed = :actor_id and ab.nickname = :nickname) or + a.id in (select fa.subscribed_id from subscription fa join actor aa with fa.subscribed = aa.id where fa.subscriber = :actor_id and aa.nickname = :nickname) or + a.id in (select fb.subscriber_id from subscription fb join actor ab with fb.subscriber = ab.id where fb.subscribed = :actor_id and ab.nickname = :nickname) or a.nickname = :nickname EOF, ['nickname' => $nickname, 'actor_id' => $this->getId()], diff --git a/src/Entity/Note.php b/src/Entity/Note.php index ad9e811ef7..3c8fd140f3 100644 --- a/src/Entity/Note.php +++ b/src/Entity/Note.php @@ -238,17 +238,17 @@ class Note extends Entity public function getActor(): Actor { - return Actor::getById($this->actor_id); + return Actor::getById($this->getActorId()); } public function getActorNickname(): string { - return Actor::getNicknameById($this->actor_id); + return Actor::getNicknameById($this->getActorId()); } public function getActorFullname(): ?string { - return Actor::getFullnameById($this->actor_id); + return Actor::getFullnameById($this->getActorId()); } public function getActorAvatarUrl(string $size = 'medium'): string