[PLUGIN][ActivityPub][Inbox] Accept Follow Activity
Improve how Core Activity is handled in general
This commit is contained in:
parent
afb7ae0f75
commit
def5f36c25
|
@ -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:'])) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -9,8 +9,7 @@
|
|||
{% endblock stylesheets %}
|
||||
|
||||
{% block body %}
|
||||
{% if notes is defined and notes is not empty %}
|
||||
<header class="feed-header">
|
||||
<header class="feed-header">
|
||||
{% if page_title is defined %}
|
||||
<h1>{{ page_title | trans }}</h1>
|
||||
{% endif %}
|
||||
|
@ -19,8 +18,7 @@
|
|||
{{ block | raw }}
|
||||
{% endfor %}
|
||||
</nav>
|
||||
</header>
|
||||
{% endif %}
|
||||
</header>
|
||||
|
||||
{# Backwards compatibility with hAtom 0.1 #}
|
||||
<main class="feed" tabindex="0" role="feed">
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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'],
|
||||
'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'],
|
||||
],
|
||||
];
|
||||
}
|
11
components/Subscription/Subscription.php
Normal file
11
components/Subscription/Subscription.php
Normal file
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace Component\Subscription;
|
||||
|
||||
use App\Core\Modules\Component;
|
||||
|
||||
class Subscription extends Component
|
||||
{
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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,7 +373,8 @@ class ActivityPub extends Plugin
|
|||
*/
|
||||
public static function getUriByObject(mixed $object): string
|
||||
{
|
||||
if ($object instanceof Note) {
|
||||
switch ($object::class) {
|
||||
case Note::class:
|
||||
if ($object->getIsLocal()) {
|
||||
return $object->getUrl();
|
||||
} else {
|
||||
|
@ -380,9 +382,15 @@ class ActivityPub extends Plugin
|
|||
$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) {
|
||||
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) {
|
||||
|
@ -390,9 +398,11 @@ class ActivityPub extends Plugin
|
|||
} 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));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a Note from ActivityPub URI, if it doesn't exist, attempt to fetch it
|
||||
|
@ -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
|
||||
if ($try_online) {
|
||||
$aprofile = ActivitypubActor::getByAddr($resource);
|
||||
if ($aprofile instanceof ActivitypubActor) {
|
||||
return Actor::getById($aprofile->getActorId());
|
||||
} else {
|
||||
}
|
||||
}
|
||||
throw new NoSuchActorException("From URI: {$resource}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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'],
|
||||
'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'],
|
||||
],
|
||||
];
|
||||
}
|
|
@ -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',
|
||||
$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);
|
||||
|
|
84
plugins/ActivityPub/Util/Model/ActivityCreate.php
Normal file
84
plugins/ActivityPub/Util/Model/ActivityCreate.php
Normal 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;
|
||||
}
|
||||
}
|
86
plugins/ActivityPub/Util/Model/ActivityFollow.php
Normal file
86
plugins/ActivityPub/Util/Model/ActivityFollow.php
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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 = [
|
||||
|
|
|
@ -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'),
|
||||
},
|
||||
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
);
|
||||
|
|
|
@ -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()],
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue
Block a user