[PLUGIN][ActivityPub] Improve flexibility of Type layer, accomodate more elaborate understanding of Group Announces after FEP-2100 development

This commit is contained in:
Diogo Peralta Cordeiro 2022-03-28 20:58:48 +01:00
parent 7305a725cb
commit be33c20614
No known key found for this signature in database
GPG Key ID: 18D2D35001FBFAB0
8 changed files with 64 additions and 51 deletions

View File

@ -324,12 +324,16 @@ class ActivityPub extends Plugin
array &$retry_args,
): bool {
try {
$data = Model::toJson($activity);
if ($sender->isGroup()) {
// When the sender is a group, we have to wrap it in an Announce activity
$data = Type::create('Announce', ['object' => $data])->toJson();
$data = Model::toType($activity);
if ($sender->isGroup() && ($activity->getVerb() !== 'subscribe' || !($activity->getVerb() === 'undo' && $data->get('object')->get('type') === 'Follow'))) {
// When the sender is a group, we have to wrap it in a transient Announce activity
$data = Type::create('Announce', [
'@context' => 'https:\/\/www.w3.org\/ns\/activitystreams',
'actor' => $sender->getUri(type: Router::ABSOLUTE_URL),
'object' => $data,
]);
}
$res = self::postman($sender, $data, $inbox);
$res = self::postman($sender, $data->toJson(), $inbox);
// accumulate errors for later use, if needed
$status_code = $res->getStatusCode();
@ -377,6 +381,7 @@ class ActivityPub extends Plugin
// the actor, that could for example mean that OStatus handled this actor while we were deactivated
// On next interaction this should be resolved, for now continue
if (\is_null($ap_target = DB::findOneBy(ActivitypubActor::class, ['actor_id' => $actor->getId()], return_null: true))) {
Log::info('FreeNetwork wrongly told ActivityPub that it can handle actor id: ' . $actor->getId() . ' you might want to keep an eye on it.');
continue;
}
$to_addr[$ap_target->getInboxSharedUri() ?? $ap_target->getInboxUri()][] = $actor;

View File

@ -98,7 +98,7 @@ class Inbox extends Controller
try {
$resource_parts = parse_url($type->get('actor'));
if ($resource_parts['host'] !== Common::config('site', 'server')) {
$actor = DB::wrapInTransaction(fn () => Explorer::getOneFromUri($type->get('actor')));
$actor = DB::wrapInTransaction(fn () => Explorer::getOneFromUri($type->get('actor')));
$ap_actor = DB::findOneBy(ActivitypubActor::class, ['actor_id' => $actor->getId()]);
} else {
throw new Exception('Only remote actors can use this endpoint.');

View File

@ -114,24 +114,36 @@ abstract class Model
*/
abstract public static function fromJson(string|Type\AbstractObject $json, array $options = []): Entity;
/**
* Get a Type
*
* @throws \App\Util\Exception\ServerException
* @throws ClientException
*/
public static function toType(mixed $object): Type\AbstractObject
{
switch ($object::class) {
case \App\Entity\Activity::class:
return Activity::toType($object);
case \App\Entity\Note::class:
return Note::toType($object);
default:
$type = self::jsonToType($object);
Event::handle('ActivityPubAddActivityStreamsTwoData', [$type->get('type'), &$type]);
return $type;
}
}
/**
* Get a JSON
*
* @param ?int $options PHP JSON options
* @param int $options PHP JSON options
*
* @throws \App\Util\Exception\ServerException
* @throws ClientException
*/
public static function toJson(mixed $object, int $options = \JSON_UNESCAPED_SLASHES): string
{
switch ($object::class) {
case \App\Entity\Activity::class:
return Activity::toJson($object, $options);
case \App\Entity\Note::class:
return Note::toJson($object, $options);
default:
$type = self::jsonToType($object);
Event::handle('ActivityPubAddActivityStreamsTwoData', [$type->get('type'), &$type]);
return $type->toJson($options);
}
return self::toType($object)->toJson($options);
}
}

View File

@ -39,6 +39,7 @@ use App\Core\Event;
use App\Core\Log;
use App\Core\Router\Router;
use App\Entity\Activity as GSActivity;
use App\Util\Common;
use App\Util\Exception\ClientException;
use App\Util\Exception\NoSuchActorException;
use App\Util\Exception\NotFoundException;
@ -46,7 +47,6 @@ use App\Util\Exception\NotImplementedException;
use DateTimeInterface;
use Exception;
use InvalidArgumentException;
use const JSON_UNESCAPED_SLASHES;
use Plugin\ActivityPub\ActivityPub;
use Plugin\ActivityPub\Entity\ActivitypubActivity;
use Plugin\ActivityPub\Util\Explorer;
@ -90,9 +90,14 @@ class Activity extends Model
// Find Actor and Object
$actor = Explorer::getOneFromUri($type_activity->get('actor'));
$type_object = $type_activity->get('object');
if (\is_string($type_object)) { // Retrieve it
$type_object = ActivityPub::getObjectByUri($type_object, try_online: true);
} else { // Encapsulated, if we have it locally, prefer it
if (\is_string($type_object)) {
if (Common::isValidHttpUrl($type_object)) { // Retrieve it
$type_object = ActivityPub::getObjectByUri($type_object, try_online: true);
} else {
$type_object = Type::fromJson($type_object);
}
}
if ($type_object instanceof AbstractObject) { // Encapsulated, if we have it locally, prefer it
// TODO: Test authority of activity over object
try {
$type_object = ActivityPub::getObjectByUri($type_object->get('id'), try_online: false);
@ -153,7 +158,7 @@ class Activity extends Model
*
* @throws ClientException
*/
public static function toJson(mixed $object, int $options = JSON_UNESCAPED_SLASHES): string
public static function toType(mixed $object): AbstractObject
{
if ($object::class !== GSActivity::class) {
throw new InvalidArgumentException('First argument type must be an Activity.');
@ -186,7 +191,8 @@ class Activity extends Model
// Get object or Tombstone
try {
$child = $object->getObject(); // Throws NotFoundException
$attr['object'] = ($attr['type'] === 'Create') ? self::jsonToType(Model::toJson($child)) : ActivityPub::getUriByObject($child);
$prefer_embed = ['Create', 'Undo'];
$attr['object'] = \in_array($attr['type'], $prefer_embed) ? self::jsonToType(Model::toJson($child)) : ActivityPub::getUriByObject($child);
} catch (NotFoundException) {
// It seems this object was deleted, refer to it as a Tombstone
$uri = match ($object->getObjectType()) {
@ -203,6 +209,6 @@ class Activity extends Model
}
$type = self::jsonToType($attr);
Event::handle('ActivityPubAddActivityStreamsTwoData', [$type->get('type'), &$type]);
return $type->toJson($options);
return $type;
}
}

View File

@ -33,6 +33,8 @@ declare(strict_types = 1);
namespace Plugin\ActivityPub\Util\Model;
use ActivityPhp\Type\AbstractObject;
use Exception;
use InvalidArgumentException;
use Plugin\ActivityPub\Entity\ActivitypubActivity;
/**
@ -45,27 +47,15 @@ class ActivityAnnounce extends Activity
{
protected static function handle_core_activity(\App\Entity\Actor $actor, AbstractObject $type_activity, mixed $type_object, ?ActivitypubActivity &$ap_act): ActivitypubActivity
{
// The only core Announce we recognise is for (transitive) activities coming from Group actors
// The only core Announce we recognise is for (transient) activities coming from Group actors
if ($actor->isGroup()) {
if ($type_object instanceof AbstractObject) {
$actual_to = array_flip(\is_string($type_object->get('to')) ? [$type_object->get('to')] : $type_object->get('to'));
$actual_cc = array_flip(\is_string($type_object->get('cc')) ? [$type_object->get('cc')] : $type_object->get('cc'));
$actual_cc[$type_activity->get('actor')] = true; // Add group to targets
foreach (\is_string($type_activity->get('to')) ? [$type_activity->get('to')] : $type_activity->get('to') as $to) {
if ($to !== 'https://www.w3.org/ns/activitystreams#Public') {
$actual_to[$to] = true;
}
}
foreach (\is_string($type_activity->get('cc')) ? [$type_activity->get('cc')] : $type_activity->get('cc') as $cc) {
if ($cc !== 'https://www.w3.org/ns/activitystreams#Public') {
$actual_cc[$cc] = true;
}
}
$type_object->set('to', array_keys($actual_to));
$type_object->set('cc', array_keys($actual_cc));
$ap_act = self::fromJson($type_object);
return $ap_act = Activity::fromJson($type_object);
} else {
throw new Exception('Already handled.');
}
} else {
throw new InvalidArgumentException('Unsupported Announce Activity.');
}
return $ap_act ?? ($ap_act = $type_object);
}
}

View File

@ -37,6 +37,7 @@ use App\Core\DB\DB;
use App\Entity\Activity as GSActivity;
use App\Util\Exception\ClientException;
use Component\Subscription\Subscription;
use Component\Subscription\Subscription as SubscriptionComponent;
use DateTime;
use InvalidArgumentException;
use Plugin\ActivityPub\Entity\ActivitypubActivity;
@ -63,6 +64,8 @@ class ActivityFollow extends Activity
if (\is_null($act)) {
throw new ClientException('You are already subscribed to this actor.');
}
SubscriptionComponent::refreshSubscriptionCount($actor, $subscribed);
// Store ActivityPub Activity
$ap_act = ActivitypubActivity::create([
'activity_id' => $act->getId(),

View File

@ -166,6 +166,7 @@ class Note extends Model
'reply_to' => $reply_to = $handleInReplyTo($type_note),
'modified' => new DateTime(),
'type' => match ($type_note->get('type')) {
'Article' => 'article',
'Page' => 'page',
default => 'note'
},
@ -361,7 +362,7 @@ class Note extends Model
* @throws InvalidArgumentException
* @throws ServerException
*/
public static function toJson(mixed $object, int $options = \JSON_UNESCAPED_SLASHES): string
public static function toType(mixed $object): AbstractObject
{
if ($object::class !== GSNote::class) {
throw new InvalidArgumentException('First argument type must be a Note.');
@ -469,6 +470,6 @@ class Note extends Model
$type = self::jsonToType($attr);
Event::handle('ActivityPubAddActivityStreamsTwoData', [$type->get('type'), &$type]);
return $type->toJson($options);
return $type;
}
}

View File

@ -263,9 +263,9 @@ abstract class Common
public static function getPreferredPhpUploadLimit(): int
{
return min(
self::sizeStrToInt(ini_get('post_max_size')),
self::sizeStrToInt(ini_get('upload_max_filesize')),
self::sizeStrToInt(ini_get('memory_limit')),
self::sizeStrToInt(\ini_get('post_max_size')),
self::sizeStrToInt(\ini_get('upload_max_filesize')),
self::sizeStrToInt(\ini_get('memory_limit')),
);
}
@ -295,10 +295,6 @@ abstract class Common
*/
public static function isValidHttpUrl(string $url, bool $ensure_secure = false)
{
if (empty($url)) {
return false;
}
// (if false, we use '?' in 'https?' to say the 's' is optional)
$regex = $ensure_secure ? '/^https$/' : '/^https?$/';
return filter_var($url, \FILTER_VALIDATE_URL) !== false