[PLUGIN][ActivityPub][Inbox] Accept Follow Activity

Improve how Core Activity is handled in general
This commit is contained in:
Diogo Peralta Cordeiro 2022-01-02 20:37:15 +00:00
parent afb7ae0f75
commit def5f36c25
No known key found for this signature in database
GPG Key ID: 18D2D35001FBFAB0
18 changed files with 336 additions and 140 deletions

View File

@ -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:'])) {

View File

@ -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;

View File

@ -9,18 +9,16 @@
{% endblock stylesheets %}
{% block body %}
{% if notes is defined and notes is not empty %}
<header class="feed-header">
{% if page_title is defined %}
<h1>{{ page_title | trans }}</h1>
{% endif %}
<nav class="feed-actions">
{% for block in handle_event('AddFeedActions', app.request) %}
{{ block | raw }}
{% endfor %}
</nav>
</header>
<header class="feed-header">
{% if page_title is defined %}
<h1>{{ page_title | trans }}</h1>
{% endif %}
<nav class="feed-actions">
{% for block in handle_event('AddFeedActions', app.request) %}
{{ block | raw }}
{% endfor %}
</nav>
</header>
{# Backwards compatibility with hAtom 0.1 #}
<main class="feed" tabindex="0" role="feed">

View File

@ -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;

View File

@ -1,5 +1,7 @@
<?php
declare(strict_types = 1);
// {{{ License
// This file is part of GNU social - https://www.gnu.org/software/social
//
@ -17,11 +19,13 @@
// along with GNU social. If not, see <http://www.gnu.org/licenses/>.
// }}}
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'],
],
];
}

View File

@ -0,0 +1,11 @@
<?php
declare(strict_types = 1);
namespace Component\Subscription;
use App\Core\Modules\Component;
class Subscription extends Component
{
}

View File

@ -165,8 +165,11 @@ class Tag extends Component
*
* $term /^(note|tag|people|actor)/ means we want to match only either a note or an actor
*/
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
{
if (!str_contains($term, ':')) {
return Event::next;
}
[$search_type, $search_term] = explode(':', $term);
if (str_starts_with($search_term, '#')) {
$search_term = self::ensureValid($search_term);

View File

@ -44,6 +44,7 @@ use App\Entity\Actor;
use App\Entity\LocalUser;
use App\Entity\Note;
use App\Util\Common;
use App\Util\Exception\BugFoundException;
use App\Util\Exception\NoSuchActorException;
use App\Util\Nickname;
use Component\FreeNetwork\Entity\FreeNetworkActorProtocol;
@ -372,26 +373,35 @@ class ActivityPub extends Plugin
*/
public static function getUriByObject(mixed $object): string
{
if ($object instanceof Note) {
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();
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}");
}
}

View File

@ -1,5 +1,7 @@
<?php
declare(strict_types = 1);
// {{{ License
// This file is part of GNU social - https://www.gnu.org/software/social
//
@ -17,7 +19,7 @@
// along with GNU social. If not, see <http://www.gnu.org/licenses/>.
// }}}
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'],
],
];
}

View File

@ -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);

View File

@ -0,0 +1,84 @@
<?php
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 <http://www.gnu.org/licenses/>.
// }}}
/**
* 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;
}
}

View File

@ -0,0 +1,86 @@
<?php
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 <http://www.gnu.org/licenses/>.
// }}}
/**
* 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;
}
}

View File

@ -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();

View File

@ -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 = [

View File

@ -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'),
},

View File

@ -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);
},
);

View File

@ -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()],

View File

@ -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