[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
Signed by: diogo
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 * 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; $include_term = str_contains($term, ':') ? explode(':', $term)[1] : $term;
if (Formatting::startsWith($term, ['note-types:', 'notes-incude:', 'note-filter:'])) { 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\Modules\Component;
use App\Core\Router\RouteLoader; use App\Core\Router\RouteLoader;
use App\Entity\Actor; use App\Entity\Actor;
use App\Entity\Subscription;
use App\Util\Formatting; use App\Util\Formatting;
use Component\Feed\Controller as C; use Component\Feed\Controller as C;
use Component\Search\Util\Parser; use Component\Search\Util\Parser;
use Component\Subscription\Entity\Subscription;
use Doctrine\Common\Collections\ExpressionBuilder; use Doctrine\Common\Collections\ExpressionBuilder;
use Doctrine\ORM\Query\Expr; use Doctrine\ORM\Query\Expr;
use Doctrine\ORM\QueryBuilder; use Doctrine\ORM\QueryBuilder;
@ -80,9 +80,9 @@ class Feed extends Component
return ['notes' => $notes ?? null, 'actors' => $actors ?? null]; 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'); ->leftJoin(Actor::class, 'note_actor', Expr\Join::WITH, 'note.actor_id = note_actor.id');
return Event::next; return Event::next;
} }
@ -117,7 +117,7 @@ class Feed extends Component
break; break;
case 'note-from': case 'note-from':
case 'notes-from': case 'notes-from':
$subscribed_expr = $eb->eq('subscription.subscriber', $actor->getId()); $subscribed_expr = $eb->eq('subscription.subscriber_id', $actor->getId());
$type_consts = []; $type_consts = [];
if ($term[1] === 'subscribed') { if ($term[1] === 'subscribed') {
$type_consts = null; $type_consts = null;

View File

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

View File

@ -38,13 +38,13 @@ use Symfony\Component\HttpFoundation\Request;
class Language extends Component 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']); $r->connect('settings_sort_languages', '/settings/sort_languages', [C\Language::class, 'sortLanguages']);
return Event::next; 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)) { if (\is_null($actor)) {
return Event::next; return Event::next;
@ -60,7 +60,7 @@ class Language extends Component
/** /**
* Populate $note_expr or $actor_expr with an expression to match a language * 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; $search_term = str_contains($term, ':') ? explode(':', $term)[1] : $term;

View File

@ -1,5 +1,7 @@
<?php <?php
declare(strict_types = 1);
// {{{ License // {{{ License
// This file is part of GNU social - https://www.gnu.org/software/social // 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/>. // along with GNU social. If not, see <http://www.gnu.org/licenses/>.
// }}} // }}}
namespace App\Entity; namespace Component\Subscription\Entity;
use App\Core\Entity; use App\Core\Entity;
use DateTimeInterface; use App\Entity\Actor;
use App\Entity\LocalUser;
use Component\Group\Entity\LocalGroup; use Component\Group\Entity\LocalGroup;
use DateTimeInterface;
/** /**
* Entity for subscription * Entity for subscription
@ -41,51 +45,51 @@ class Subscription extends Entity
{ {
// {{{ Autocode // {{{ Autocode
// @codeCoverageIgnoreStart // @codeCoverageIgnoreStart
private int $subscriber; private int $subscriber_id;
private int $subscribed; private int $subscribed_id;
private \DateTimeInterface $created; private DateTimeInterface $created;
private \DateTimeInterface $modified; 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; 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; 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; $this->created = $created;
return $this; return $this;
} }
public function getCreated(): \DateTimeInterface public function getCreated(): DateTimeInterface
{ {
return $this->created; return $this->created;
} }
public function setModified(\DateTimeInterface $modified): self public function setModified(DateTimeInterface $modified): self
{ {
$this->modified = $modified; $this->modified = $modified;
return $this; return $this;
} }
public function getModified(): \DateTimeInterface public function getModified(): DateTimeInterface
{ {
return $this->modified; return $this->modified;
} }
@ -93,6 +97,16 @@ class Subscription extends Entity
// @codeCoverageIgnoreEnd // @codeCoverageIgnoreEnd
// }}} Autocode // }}} 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 public static function cacheKeys(LocalUser|LocalGroup|Actor $subject, LocalUser|LocalGroup|Actor $target): array
{ {
return [ return [
@ -105,15 +119,15 @@ class Subscription extends Entity
return [ return [
'name' => 'subscription', 'name' => 'subscription',
'fields' => [ 'fields' => [
'subscriber' => ['type' => 'int', 'foreign key' => true, 'target' => 'Actor.id', 'multiplicity' => 'one to one', 'name' => 'subscrib_subscriber_fkey', 'not null' => true, 'description' => 'actor listening'], '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' => ['type' => 'int', 'foreign key' => true, 'target' => 'Actor.id', 'multiplicity' => 'one to one', 'name' => 'subscrib_subscribed_fkey', 'not null' => true, 'description' => 'actor being listened to'], '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'], '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'], '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' => [ 'indexes' => [
'subscrib_subscriber_idx' => ['subscriber', 'created'], 'subscription_subscriber_idx' => ['subscriber_id', 'created'],
'subscrib_subscribed_idx' => ['subscribed', '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 * $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); [$search_type, $search_term] = explode(':', $term);
if (str_starts_with($search_term, '#')) { if (str_starts_with($search_term, '#')) {
$search_term = self::ensureValid($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\LocalUser;
use App\Entity\Note; use App\Entity\Note;
use App\Util\Common; use App\Util\Common;
use App\Util\Exception\BugFoundException;
use App\Util\Exception\NoSuchActorException; use App\Util\Exception\NoSuchActorException;
use App\Util\Nickname; use App\Util\Nickname;
use Component\FreeNetwork\Entity\FreeNetworkActorProtocol; use Component\FreeNetwork\Entity\FreeNetworkActorProtocol;
@ -372,26 +373,35 @@ class ActivityPub extends Plugin
*/ */
public static function getUriByObject(mixed $object): string public static function getUriByObject(mixed $object): string
{ {
if ($object instanceof Note) { switch ($object::class) {
if ($object->getIsLocal()) { case Note::class:
return $object->getUrl(); if ($object->getIsLocal()) {
} else { return $object->getUrl();
// Try known remote objects } else {
$known_object = ActivitypubObject::getByPK(['object_type' => 'note', 'object_id' => $object->getId()]); // Try known remote objects
if ($known_object instanceof ActivitypubObject) { $known_object = ActivitypubObject::getByPK(['object_type' => 'note', 'object_id' => $object->getId()]);
return $known_object->getObjectUri(); 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]);
}
} }
} break;
} elseif ($object instanceof Activity) { case Actor::class:
// Try known remote activities return $object->getUri();
$known_activity = ActivitypubActivity::getByPK(['activity_id' => $object->getId()]); break;
if ($known_activity instanceof ActivitypubActivity) { case Activity::class:
return $known_activity->getActivityUri(); // Try known remote activities
} else { $known_activity = ActivitypubActivity::getByPK(['activity_id' => $object->getId()]);
return Router::url('activity_view', ['id' => $object->getId()], Router::ABSOLUTE_URL); 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) public static function getObjectByUri(string $resource, bool $try_online = true)
{ {
// Try known objects // Try known object
$known_object = ActivitypubObject::getByPK(['object_uri' => $resource]); $known_object = ActivitypubObject::getByPK(['object_uri' => $resource]);
if ($known_object instanceof ActivitypubObject) { if ($known_object instanceof ActivitypubObject) {
return $known_object->getObject(); return $known_object->getObject();
} }
// Try known activities // Try known activity
$known_activity = ActivitypubActivity::getByPK(['activity_uri' => $resource]); $known_activity = ActivitypubActivity::getByPK(['activity_uri' => $resource]);
if ($known_activity instanceof ActivitypubActivity) { if ($known_activity instanceof ActivitypubActivity) {
return $known_activity->getActivity(); return $known_activity->getActivity();
} }
// Try local Notes (pretty incomplete effort, I know) // Try local Note
if (Common::isValidHttpUrl($resource)) { if (Common::isValidHttpUrl($resource)) {
// This means $resource is a valid url // This means $resource is a valid url
$resource_parts = parse_url($resource); $resource_parts = parse_url($resource);
// TODO: Use URLMatcher // TODO: Use URLMatcher
if ($resource_parts['host'] === $_ENV['SOCIAL_DOMAIN']) { // XXX: Common::config('site', 'server')) { 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) { if ($local_note instanceof Note) {
return $local_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 // Try remote
if (!$try_online) { if (!$try_online) {
return; return;
@ -457,7 +474,7 @@ class ActivityPub extends Plugin
* *
* @return Actor got from URI * @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 // Try local
if (Common::isValidHttpUrl($resource)) { if (Common::isValidHttpUrl($resource)) {
@ -478,11 +495,12 @@ class ActivityPub extends Plugin
} }
} }
// Try remote // Try remote
$aprofile = ActivitypubActor::getByAddr($resource); if ($try_online) {
if ($aprofile instanceof ActivitypubActor) { $aprofile = ActivitypubActor::getByAddr($resource);
return Actor::getById($aprofile->getActorId()); if ($aprofile instanceof ActivitypubActor) {
} else { return Actor::getById($aprofile->getActorId());
throw new NoSuchActorException("From URI: {$resource}"); }
} }
throw new NoSuchActorException("From URI: {$resource}");
} }
} }

View File

@ -1,5 +1,7 @@
<?php <?php
declare(strict_types = 1);
// {{{ License // {{{ License
// This file is part of GNU social - https://www.gnu.org/software/social // 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/>. // along with GNU social. If not, see <http://www.gnu.org/licenses/>.
// }}} // }}}
namespace App\Entity; namespace Plugin\ActivityPub\Entity;
use App\Core\Entity; use App\Core\Entity;
use DateTimeInterface; use DateTimeInterface;
@ -36,13 +38,13 @@ use DateTimeInterface;
* @copyright 2020-2021 Free Software Foundation, Inc http://www.fsf.org * @copyright 2020-2021 Free Software Foundation, Inc http://www.fsf.org
* @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
*/ */
class SubscriptionQueue extends Entity class ActivitypubFollowRequestQueue extends Entity
{ {
// {{{ Autocode // {{{ Autocode
// @codeCoverageIgnoreStart // @codeCoverageIgnoreStart
private int $subscriber; private int $subscriber;
private int $subscribed; private int $subscribed;
private \DateTimeInterface $created; private DateTimeInterface $created;
public function setSubscriber(int $subscriber): self public function setSubscriber(int $subscriber): self
{ {
@ -66,13 +68,13 @@ class SubscriptionQueue extends Entity
return $this->subscribed; return $this->subscribed;
} }
public function setCreated(\DateTimeInterface $created): self public function setCreated(DateTimeInterface $created): self
{ {
$this->created = $created; $this->created = $created;
return $this; return $this;
} }
public function getCreated(): \DateTimeInterface public function getCreated(): DateTimeInterface
{ {
return $this->created; return $this->created;
} }
@ -83,17 +85,17 @@ class SubscriptionQueue extends Entity
public static function schemaDef(): array public static function schemaDef(): array
{ {
return [ return [
'name' => 'subscription_queue', 'name' => 'activitypub_follow_request_queue',
'description' => 'Holder for Subscription requests awaiting moderation.', 'description' => 'Holder for Subscription requests awaiting moderation.',
'fields' => [ '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'], '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' => 'Subscription_queue_subscribed_fkey', 'not null' => true, 'description' => 'actor being subscribed'], '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'], 'created' => ['type' => 'datetime', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was created'],
], ],
'primary key' => ['subscriber', 'subscribed'], 'primary key' => ['subscriber', 'subscribed'],
'indexes' => [ 'indexes' => [
'subscription_queue_subscriber_created_idx' => ['subscriber', 'created'], 'activitypub_follow_request_queue_subscriber_created_idx' => ['subscriber', 'created'],
'subscription_queue_subscribed_created_idx' => ['subscribed', '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;
use ActivityPhp\Type\AbstractObject; use ActivityPhp\Type\AbstractObject;
use App\Core\DB\DB;
use App\Core\Event; use App\Core\Event;
use App\Core\Router\Router; use App\Core\Router\Router;
use App\Entity\Activity as GSActivity; use App\Entity\Activity as GSActivity;
use App\Util\Exception\ClientException; use App\Util\Exception\ClientException;
use App\Util\Exception\NoSuchActorException; use App\Util\Exception\NoSuchActorException;
use App\Util\Exception\NotFoundException; use App\Util\Exception\NotFoundException;
use DateTime;
use DateTimeInterface; use DateTimeInterface;
use Exception;
use InvalidArgumentException; use InvalidArgumentException;
use Plugin\ActivityPub\ActivityPub; use Plugin\ActivityPub\ActivityPub;
use Plugin\ActivityPub\Entity\ActivitypubActivity; 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 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') { switch ($type_activity->get('type')) {
if ($type_object instanceof AbstractObject) { case 'Create':
$note = Note::fromJson($type_object, ['test_authority' => true, 'actor_uri' => $type_activity->get('actor'), 'actor' => $actor, 'actor_id' => $actor->getId()]); ActivityCreate::handle_core_activity($actor, $type_activity, $type_object, $ap_act);
} else { break;
if ($type_object instanceof \App\Entity\Note) { case 'Follow':
$note = $type_object; ActivityFollow::handle_core_activity($actor, $type_activity, $type_object, $ap_act);
} 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);
} }
return $ap_act; return $ap_act;
} }
@ -145,27 +120,28 @@ class Activity extends Model
public static function toJson(mixed $object, ?int $options = null): string public static function toJson(mixed $object, ?int $options = null): string
{ {
if ($object::class !== GSActivity::class) { 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; $gs_verb_to_activity_streams_two_verb = null;
if (Event::handle('GSVerbToActivityStreamsTwoActivityType', [($verb = $object->getVerb()), &$gs_verb_to_activity_stream_two_verb]) === Event::next) { if (Event::handle('GSVerbToActivityStreamsTwoActivityType', [($verb = $object->getVerb()), &$gs_verb_to_activity_streams_two_verb]) === Event::next) {
$gs_verb_to_activity_stream_two_verb = match ($verb) { $gs_verb_to_activity_streams_two_verb = match ($verb) {
'create' => 'Create', 'undo' => 'Undo',
'undo' => 'Undo', 'create' => 'Create',
default => throw new ClientException('Invalid verb'), 'subscribe' => 'Follow',
default => throw new ClientException('Invalid verb'),
}; };
} }
$attr = [ $attr = [
'type' => $gs_verb_to_activity_stream_two_verb, 'type' => $gs_verb_to_activity_streams_two_verb,
'@context' => 'https://www.w3.org/ns/activitystreams', '@context' => 'https://www.w3.org/ns/activitystreams',
'id' => Router::url('activity_view', ['id' => $object->getId()], Router::ABSOLUTE_URL), 'id' => Router::url('activity_view', ['id' => $object->getId()], Router::ABSOLUTE_URL),
'published' => $object->getCreated()->format(DateTimeInterface::RFC3339), 'published' => $object->getCreated()->format(DateTimeInterface::RFC3339),
'actor' => $object->getActor()->getUri(Router::ABSOLUTE_URL), '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 { try {
$object = $object->getObject(); // Throws NotFoundException $object = $object->getObject(); // Throws NotFoundException
$attr['object'] = ($attr['type'] === 'Create') ? self::jsonToType(Model::toJson($object)) : ActivityPub::getUriByObject($object); $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'])) { // If embedded non tombstone Object
$attr['to'] = array_unique(array_merge($attr['to'], $attr['object']->get('to') ?? [])); if (!\is_string($attr['object']) && $attr['object']->get('type') !== 'Tombstone') {
$attr['cc'] = array_unique(array_merge($attr['cc'], $attr['object']->get('cc') ?? [])); // 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); $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 public static function toJson(mixed $object, ?int $options = null): string
{ {
if ($object::class !== GSActor::class) { 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); $rsa = ActivitypubRsa::getByActor($object);
$public_key = $rsa->getPublicKey(); $public_key = $rsa->getPublicKey();

View File

@ -308,7 +308,7 @@ class Note extends Model
public static function toJson(mixed $object, ?int $options = null): string public static function toJson(mixed $object, ?int $options = null): string
{ {
if ($object::class !== GSNote::class) { if ($object::class !== GSNote::class) {
throw new InvalidArgumentException('First argument type is Note'); throw new InvalidArgumentException('First argument type must be a Note.');
} }
$attr = [ $attr = [

View File

@ -121,7 +121,7 @@ class Directory extends FeedController
}, },
'subscribers' => match ($actor_type) { // select by actors with most/least subscribers/members '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'), 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\Actor;
use App\Entity\Feed; use App\Entity\Feed;
use App\Entity\LocalUser; use App\Entity\LocalUser;
use App\Entity\Subscription;
use App\Security\Authenticator; use App\Security\Authenticator;
use App\Security\EmailVerifier; use App\Security\EmailVerifier;
use App\Util\Common; use App\Util\Common;
@ -30,6 +29,7 @@ use App\Util\Exception\NotFoundException;
use App\Util\Exception\ServerException; use App\Util\Exception\ServerException;
use App\Util\Form\FormFields; use App\Util\Form\FormFields;
use App\Util\Nickname; use App\Util\Nickname;
use Component\Subscription\Entity\Subscription;
use Doctrine\DBAL\Exception\UniqueConstraintViolationException; use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
use LogicException; use LogicException;
use Symfony\Component\Form\Extension\Core\Type\EmailType; use Symfony\Component\Form\Extension\Core\Type\EmailType;
@ -164,7 +164,7 @@ class Security extends Controller
$user, $user,
function (int $id) use ($user) { function (int $id) use ($user) {
// Self subscription // Self subscription
DB::persist(Subscription::create(['subscriber' => $id, 'subscribed' => $id])); DB::persist(Subscription::create(['subscriber_id' => $id, 'subscribed_id' => $id]));
Feed::createDefaultFeeds($id, $user); Feed::createDefaultFeeds($id, $user);
}, },
); );

View File

@ -384,12 +384,12 @@ class Actor extends Entity
public function getSubscribersCount(): int public function getSubscribersCount(): int
{ {
return $this->getSubCount(which: 'subscriber', column: 'subscribed'); return $this->getSubCount(which: 'subscriber', column: 'subscribed_id');
} }
public function getSubscribedCount() 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( fn () => DB::dql(
<<<'EOF' <<<'EOF'
select a from actor a where 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 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 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 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 a.nickname = :nickname
EOF, EOF,
['nickname' => $nickname, 'actor_id' => $this->getId()], ['nickname' => $nickname, 'actor_id' => $this->getId()],

View File

@ -238,17 +238,17 @@ class Note extends Entity
public function getActor(): Actor public function getActor(): Actor
{ {
return Actor::getById($this->actor_id); return Actor::getById($this->getActorId());
} }
public function getActorNickname(): string public function getActorNickname(): string
{ {
return Actor::getNicknameById($this->actor_id); return Actor::getNicknameById($this->getActorId());
} }
public function getActorFullname(): ?string public function getActorFullname(): ?string
{ {
return Actor::getFullnameById($this->actor_id); return Actor::getFullnameById($this->getActorId());
} }
public function getActorAvatarUrl(string $size = 'medium'): string public function getActorAvatarUrl(string $size = 'medium'): string