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 %}
-
+
{# 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