[PLUGIN][ActivityPub] Improve flexibility of Type layer, accomodate more elaborate understanding of Group Announces after FEP-2100 development
This commit is contained in:
parent
7305a725cb
commit
be33c20614
@ -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;
|
||||||
|
@ -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.');
|
||||||
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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(),
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user