[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, array &$retry_args,
): bool { ): bool {
try { try {
$data = Model::toJson($activity); $data = Model::toType($activity);
if ($sender->isGroup()) { 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 an Announce activity // When the sender is a group, we have to wrap it in a transient Announce activity
$data = Type::create('Announce', ['object' => $data])->toJson(); $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 // accumulate errors for later use, if needed
$status_code = $res->getStatusCode(); $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 // 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 // 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))) { 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; continue;
} }
$to_addr[$ap_target->getInboxSharedUri() ?? $ap_target->getInboxUri()][] = $actor; $to_addr[$ap_target->getInboxSharedUri() ?? $ap_target->getInboxUri()][] = $actor;

View File

@ -98,7 +98,7 @@ class Inbox extends Controller
try { try {
$resource_parts = parse_url($type->get('actor')); $resource_parts = parse_url($type->get('actor'));
if ($resource_parts['host'] !== Common::config('site', 'server')) { 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()]); $ap_actor = DB::findOneBy(ActivitypubActor::class, ['actor_id' => $actor->getId()]);
} else { } else {
throw new Exception('Only remote actors can use this endpoint.'); 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; 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 * Get a JSON
* *
* @param ?int $options PHP JSON options * @param int $options PHP JSON options
* *
* @throws \App\Util\Exception\ServerException
* @throws ClientException * @throws ClientException
*/ */
public static function toJson(mixed $object, int $options = \JSON_UNESCAPED_SLASHES): string public static function toJson(mixed $object, int $options = \JSON_UNESCAPED_SLASHES): string
{ {
switch ($object::class) { return self::toType($object)->toJson($options);
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);
}
} }
} }

View File

@ -39,6 +39,7 @@ use App\Core\Event;
use App\Core\Log; use App\Core\Log;
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\Common;
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;
@ -46,7 +47,6 @@ use App\Util\Exception\NotImplementedException;
use DateTimeInterface; use DateTimeInterface;
use Exception; use Exception;
use InvalidArgumentException; use InvalidArgumentException;
use const JSON_UNESCAPED_SLASHES;
use Plugin\ActivityPub\ActivityPub; use Plugin\ActivityPub\ActivityPub;
use Plugin\ActivityPub\Entity\ActivitypubActivity; use Plugin\ActivityPub\Entity\ActivitypubActivity;
use Plugin\ActivityPub\Util\Explorer; use Plugin\ActivityPub\Util\Explorer;
@ -90,9 +90,14 @@ class Activity extends Model
// Find Actor and Object // Find Actor and Object
$actor = Explorer::getOneFromUri($type_activity->get('actor')); $actor = Explorer::getOneFromUri($type_activity->get('actor'));
$type_object = $type_activity->get('object'); $type_object = $type_activity->get('object');
if (\is_string($type_object)) { // Retrieve it if (\is_string($type_object)) {
$type_object = ActivityPub::getObjectByUri($type_object, try_online: true); if (Common::isValidHttpUrl($type_object)) { // Retrieve it
} else { // Encapsulated, if we have it locally, prefer 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 // TODO: Test authority of activity over object
try { try {
$type_object = ActivityPub::getObjectByUri($type_object->get('id'), try_online: false); $type_object = ActivityPub::getObjectByUri($type_object->get('id'), try_online: false);
@ -153,7 +158,7 @@ class Activity extends Model
* *
* @throws ClientException * @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) { if ($object::class !== GSActivity::class) {
throw new InvalidArgumentException('First argument type must be an Activity.'); throw new InvalidArgumentException('First argument type must be an Activity.');
@ -186,7 +191,8 @@ class Activity extends Model
// Get object or Tombstone // Get object or Tombstone
try { try {
$child = $object->getObject(); // Throws NotFoundException $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) { } catch (NotFoundException) {
// It seems this object was deleted, refer to it as a Tombstone // It seems this object was deleted, refer to it as a Tombstone
$uri = match ($object->getObjectType()) { $uri = match ($object->getObjectType()) {
@ -203,6 +209,6 @@ class Activity extends Model
} }
$type = self::jsonToType($attr); $type = self::jsonToType($attr);
Event::handle('ActivityPubAddActivityStreamsTwoData', [$type->get('type'), &$type]); 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; namespace Plugin\ActivityPub\Util\Model;
use ActivityPhp\Type\AbstractObject; use ActivityPhp\Type\AbstractObject;
use Exception;
use InvalidArgumentException;
use Plugin\ActivityPub\Entity\ActivitypubActivity; 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 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 ($actor->isGroup()) {
if ($type_object instanceof AbstractObject) { if ($type_object instanceof AbstractObject) {
$actual_to = array_flip(\is_string($type_object->get('to')) ? [$type_object->get('to')] : $type_object->get('to')); return $ap_act = Activity::fromJson($type_object);
$actual_cc = array_flip(\is_string($type_object->get('cc')) ? [$type_object->get('cc')] : $type_object->get('cc')); } else {
$actual_cc[$type_activity->get('actor')] = true; // Add group to targets throw new Exception('Already handled.');
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);
} }
} 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\Entity\Activity as GSActivity;
use App\Util\Exception\ClientException; use App\Util\Exception\ClientException;
use Component\Subscription\Subscription; use Component\Subscription\Subscription;
use Component\Subscription\Subscription as SubscriptionComponent;
use DateTime; use DateTime;
use InvalidArgumentException; use InvalidArgumentException;
use Plugin\ActivityPub\Entity\ActivitypubActivity; use Plugin\ActivityPub\Entity\ActivitypubActivity;
@ -63,6 +64,8 @@ class ActivityFollow extends Activity
if (\is_null($act)) { if (\is_null($act)) {
throw new ClientException('You are already subscribed to this actor.'); throw new ClientException('You are already subscribed to this actor.');
} }
SubscriptionComponent::refreshSubscriptionCount($actor, $subscribed);
// Store ActivityPub Activity // Store ActivityPub Activity
$ap_act = ActivitypubActivity::create([ $ap_act = ActivitypubActivity::create([
'activity_id' => $act->getId(), 'activity_id' => $act->getId(),

View File

@ -166,6 +166,7 @@ class Note extends Model
'reply_to' => $reply_to = $handleInReplyTo($type_note), 'reply_to' => $reply_to = $handleInReplyTo($type_note),
'modified' => new DateTime(), 'modified' => new DateTime(),
'type' => match ($type_note->get('type')) { 'type' => match ($type_note->get('type')) {
'Article' => 'article',
'Page' => 'page', 'Page' => 'page',
default => 'note' default => 'note'
}, },
@ -361,7 +362,7 @@ class Note extends Model
* @throws InvalidArgumentException * @throws InvalidArgumentException
* @throws ServerException * @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) { if ($object::class !== GSNote::class) {
throw new InvalidArgumentException('First argument type must be a Note.'); throw new InvalidArgumentException('First argument type must be a Note.');
@ -469,6 +470,6 @@ class Note extends Model
$type = self::jsonToType($attr); $type = self::jsonToType($attr);
Event::handle('ActivityPubAddActivityStreamsTwoData', [$type->get('type'), &$type]); 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 public static function getPreferredPhpUploadLimit(): int
{ {
return min( return min(
self::sizeStrToInt(ini_get('post_max_size')), self::sizeStrToInt(\ini_get('post_max_size')),
self::sizeStrToInt(ini_get('upload_max_filesize')), self::sizeStrToInt(\ini_get('upload_max_filesize')),
self::sizeStrToInt(ini_get('memory_limit')), self::sizeStrToInt(\ini_get('memory_limit')),
); );
} }
@ -295,10 +295,6 @@ abstract class Common
*/ */
public static function isValidHttpUrl(string $url, bool $ensure_secure = false) 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) // (if false, we use '?' in 'https?' to say the 's' is optional)
$regex = $ensure_secure ? '/^https$/' : '/^https?$/'; $regex = $ensure_secure ? '/^https$/' : '/^https?$/';
return filter_var($url, \FILTER_VALIDATE_URL) !== false return filter_var($url, \FILTER_VALIDATE_URL) !== false