[PLUGIN][ActivityPub] Implement Actor Update

Diverse minor bug fixes
This commit is contained in:
Diogo Peralta Cordeiro 2021-12-05 03:11:08 +00:00
parent 9506909e7a
commit 9512890264
Signed by: diogo
GPG Key ID: 18D2D35001FBFAB0
6 changed files with 78 additions and 66 deletions

View File

@ -80,7 +80,7 @@ class Inbox extends Controller
$type = Model::jsonToType($body);
if ($type->has('actor') === false) {
$error('Actor not found in the request.');
return $error('Actor not found in the request.');
}
try {
@ -88,10 +88,11 @@ class Inbox extends Controller
$actor = Actor::getById($ap_actor->getActorId());
DB::flush();
} catch (Exception $e) {
$error('Invalid actor.');
return $error('Invalid actor.');
}
$actor_public_key = ActivitypubRsa::getByActor($actor)->getPublicKey();
$activitypub_rsa = ActivitypubRsa::getByActor($actor);
$actor_public_key = $activitypub_rsa->getPublicKey();
$headers = $this->request->headers->all();
// Flattify headers
@ -101,7 +102,7 @@ class Inbox extends Controller
if (!isset($headers['signature'])) {
Log::debug('ActivityPub Inbox: HTTP Signature: Missing Signature header.');
$error('Missing Signature header.', 400);
return $error('Missing Signature header.', 400);
// TODO: support other methods beyond HTTP Signatures
}
@ -110,22 +111,25 @@ class Inbox extends Controller
Log::debug('ActivityPub Inbox: HTTP Signature Data: ' . print_r($signatureData, true));
if (isset($signatureData['error'])) {
Log::debug('ActivityPub Inbox: HTTP Signature: ' . json_encode($signatureData, JSON_PRETTY_PRINT));
$error(json_encode($signatureData, JSON_PRETTY_PRINT), 400);
return $error(json_encode($signatureData, JSON_PRETTY_PRINT), 400);
}
list($verified, /*$headers*/) = HTTPSignature::verify($actor_public_key, $signatureData, $headers, $path, $body);
[$verified, /*$headers*/] = HTTPSignature::verify($actor_public_key, $signatureData, $headers, $path, $body);
// If the signature fails verification the first time, update profile as it might have changed public key
if ($verified !== 1) {
try {
$res = Explorer::get_remote_user_activity($ap_actor->getUri());
if (is_null($res)) {
return $error('Invalid remote actor.');
}
} catch (Exception) {
$error('Invalid remote actor.');
return $error('Invalid remote actor.');
}
try {
$actor = ActivitypubActor::update_profile($ap_actor, $res);
ActivitypubActor::update_profile($ap_actor, $actor, $activitypub_rsa, $res);
} catch (Exception) {
$error('Failed to updated remote actor information.');
return $error('Failed to updated remote actor information.');
}
[$verified, /*$headers*/] = HTTPSignature::verify($actor_public_key, $signatureData, $headers, $path, $body);
@ -134,7 +138,7 @@ class Inbox extends Controller
// If it still failed despite profile update
if ($verified !== 1) {
Log::debug('ActivityPub Inbox: HTTP Signature: Invalid signature.');
$error('Invalid signature.');
return $error('Invalid signature.');
}
// HTTP signature checked out, make sure the "actor" of the activity matches that of the signature

View File

@ -34,6 +34,7 @@ namespace Plugin\ActivityPub\Entity;
use App\Core\Cache;
use App\Core\Entity;
use App\Core\Log;
use App\Entity\Actor;
use Component\FreeNetwork\Util\Discovery;
use DateTimeInterface;
use Exception;
@ -237,6 +238,18 @@ class ActivitypubActor extends Entity
}
}
/**
* @param ActivitypubActor $ap_actor
* @param Actor $actor
* @param ActivitypubRsa $activitypub_rsa
* @param string $res
* @throws Exception
*/
public static function update_profile(self &$ap_actor, Actor &$actor, ActivitypubRsa &$activitypub_rsa, string $res): void
{
\Plugin\ActivityPub\Util\Model\Actor::fromJson($res, ['objects' => ['ActivitypubActor' => &$ap_actor, 'Actor' => &$actor, 'ActivitypubRsa' => &$activitypub_rsa]]);
}
public static function schemaDef(): array
{
return [

View File

@ -55,7 +55,7 @@ use const JSON_UNESCAPED_SLASHES;
*/
class Explorer
{
private array $discovered_actor_profiles = [];
private array $discovered_activitypub_actor_profiles = [];
/**
* Shortcut function to get a single profile from its URL.
@ -104,7 +104,7 @@ class Explorer
}
Log::debug('ActivityPub Explorer: Started now looking for ' . $url);
$this->discovered_actor_profiles = [];
$this->discovered_activitypub_actor_profiles = [];
return $this->_lookup($url, $grab_online);
}
@ -123,7 +123,7 @@ class Explorer
* @throws ServerExceptionInterface
* @throws TransportExceptionInterface
*
* @return array of Profile objects
* @return array of ActivityPub Actor objects
*/
private function _lookup(string $url, bool $grab_online = true): array
{
@ -135,7 +135,7 @@ class Explorer
throw new NoSuchActorException('Actor not found.');
}
return $this->discovered_actor_profiles;
return $this->discovered_activitypub_actor_profiles;
}
/**
@ -160,7 +160,7 @@ class Explorer
Log::debug('ActivityPub Explorer: Found a known Aprofile for ' . $uri);
// We found something!
$this->discovered_actor_profiles[] = $aprofile;
$this->discovered_activitypub_actor_profiles[] = $aprofile;
return true;
} else {
Log::debug('ActivityPub Explorer: Unable to find a known Aprofile for ' . $uri);
@ -204,7 +204,7 @@ class Explorer
return true;
} else {
try {
$this->discovered_actor_profiles[] = Model\Actor::fromJson(json_encode($res));
$this->discovered_activitypub_actor_profiles[] = Model\Actor::fromJson(json_encode($res));
return true;
} catch (Exception $e) {
Log::debug(
@ -219,19 +219,6 @@ class Explorer
return false;
}
/**
* Validates a remote response in order to determine whether this
* response is a valid profile or not
*
* @param array $res remote response
*
* @return bool success state
*/
public static function validate_remote_response(array $res): bool
{
return !(!isset($res['id'], $res['preferredUsername'], $res['inbox'], $res['publicKey']['publicKeyPem']));
}
/**
* Get a ActivityPub Profile from it's uri
*
@ -288,28 +275,20 @@ class Explorer
* @throws RedirectionExceptionInterface
* @throws ServerExceptionInterface
* @throws TransportExceptionInterface
* @throws Exception
*
* @return array|false If it is able to fetch, false if it's gone
* @return string|null If it is able to fetch, false if it's gone
* // Exceptions when network issues or unsupported Activity format
*/
public static function get_remote_user_activity(string $url): bool|array
public static function get_remote_user_activity(string $url): string|null
{
$response = HTTPClient::get($url, ['headers' => ACTIVITYPUB::HTTP_CLIENT_HEADERS]);
// If it was deleted
if ($response->getStatusCode() == 410) {
return false;
return null;
} elseif (!HTTPClient::statusCodeIsOkay($response)) { // If it is unavailable
throw new Exception('Non Ok Status Code for given Actor URL.');
}
$res = json_decode($response->getContent(), true);
if (is_null($res)) {
Log::debug('ActivityPub Explorer: Invalid JSON returned from given Actor URL: ' . $response->getContent());
throw new Exception('Given Actor URL didn\'t return a valid JSON.');
}
if (self::validate_remote_response($res)) {
Log::debug('ActivityPub Explorer: Found a valid remote actor for ' . $url);
return $res;
}
throw new Exception('ActivityPub Explorer: Failed to get activity.');
return $response->getContent();
}
}

View File

@ -66,6 +66,7 @@ class Activity extends Model
public static function fromJson(string|AbstractObject $json, array $options = []): ActivitypubActivity
{
$type_activity = is_string($json) ? self::jsonToType($json) : $json;
$source = $options['source'];
$activity_stream_two_verb_to_gs_verb = fn(string $verb): string => match ($verb) {
'Create' => 'create',
@ -82,15 +83,15 @@ class Activity extends Model
$actor = ActivityPub::getActorByUri($type_activity->get('actor'));
// Store Object
$obj = null;
if (!$type_activity->has('object') || !isset($type_activity->get('object')['type'])) {
if (!$type_activity->has('object') || !$type_activity->get('object')->has('type')) {
throw new InvalidArgumentException('Activity Object or Activity Object Type is missing.');
}
switch ($type_activity->get('object')['type']) {
switch ($type_activity->get('object')->get('type')) {
case 'Note':
$obj = Note::toJson($type_activity->get('object'), ['source' => $source, 'actor_uri' => $type_activity->get('actor'), 'actor_id' => $actor->getId()]);
$obj = Note::fromJson($type_activity->get('object'), ['source' => $source, 'actor_uri' => $type_activity->get('actor'), 'actor_id' => $actor->getId()]);
break;
default:
if (!Event::handle('ActivityPubObject', [$type_activity->get('object')['type'], $type_activity->get('object'), &$obj])) {
if (!Event::handle('ActivityPubObject', [$type_activity->get('object')->get('type'), $type_activity->get('object'), &$obj])) {
throw new ClientException('Unsupported Object type.');
}
break;
@ -100,20 +101,20 @@ class Activity extends Model
$act = GSActivity::create([
'actor_id' => $actor->getId(),
'verb' => $activity_stream_two_verb_to_gs_verb($type_activity->get('type')),
'object_type' => $activity_stream_two_object_type_to_gs_table($type_activity->get('object')['type']),
'object_type' => $activity_stream_two_object_type_to_gs_table($type_activity->get('object')->get('type')),
'object_id' => $obj->getId(),
'is_local' => false,
'created' => new DateTime($activity['published'] ?? 'now'),
'created' => new DateTime($type_activity->get('published') ?? 'now'),
'source' => $source,
]);
DB::persist($act);
// Store ActivityPub Activity
$ap_act = ActivitypubActivity::create([
'activity_id' => $act->getId(),
'activity_uri' => $activity['id'],
'object_uri' => $activity['object']['id'],
'activity_uri' => $type_activity->get('id'),
'object_uri' => $type_activity->get('object')->get('id'),
'is_local' => false,
'created' => new DateTime($activity['published'] ?? 'now'),
'created' => new DateTime($type_activity->get('published') ?? 'now'),
'modified' => new DateTime(),
]);
DB::persist($ap_act);

View File

@ -64,10 +64,10 @@ class Actor extends Model
*
* @param string|AbstractObject $json
* @param array $options
* @return GSActor
* @return ActivitypubActor
* @throws Exception
*/
public static function fromJson(string|AbstractObject $json, array $options = []): GSActor
public static function fromJson(string|AbstractObject $json, array $options = []): ActivitypubActor
{
$person = is_string($json) ? self::jsonToType($json) : $json;
@ -81,32 +81,39 @@ class Actor extends Model
'modified' => new DateTime(),
];
$actor = new GSActor();
$actor = $options['objects']['Actor'] ?? new GSActor();
foreach ($actor_map as $prop => $val) {
$set = Formatting::snakeCaseToCamelCase("set_{$prop}");
$actor->{$set}($val);
}
if (!isset($options['objects']['Actor'])) {
DB::persist($actor);
}
// ActivityPub Actor
$aprofile = ActivitypubActor::create([
$ap_actor = ActivitypubActor::create([
'inbox_uri' => $person->get('inbox'),
'inbox_shared_uri' => ($person->has('endpoints') && isset($person->get('endpoints')['sharedInbox'])) ? $person->get('endpoints')['sharedInbox'] : null,
'uri' => $person->get('id'),
'actor_id' => $actor->getId(),
'url' => $person->get('url') ?? null,
]);
], $options['objects']['ActivitypubActor'] ?? null);
DB::persist($aprofile);
if (!isset($options['objects']['ActivitypubActor'])) {
DB::persist($ap_actor);
}
// Public Key
$apRSA = ActivitypubRsa::create([
'actor_id' => $actor->getID(),
'public_key' => ($person->has('publicKey') && isset($person->get('publicKey')['publicKeyPem'])) ? $person->get('publicKey')['publicKeyPem'] : null,
]);
], $options['objects']['ActivitypubRsa'] ?? null);
if (!isset($options['objects']['ActivitypubRsa'])) {
DB::persist($apRSA);
}
// Avatar
//if (isset($res['icon']['url'])) {
@ -118,8 +125,8 @@ class Actor extends Model
// }
//}
Event::handle('ActivityPubNewActor', [&$aprofile, &$actor, &$apRSA]);
return $aprofile;
Event::handle('ActivityPubNewActor', [&$ap_actor, &$actor, &$apRSA]);
return $ap_actor;
}
/**

View File

@ -58,13 +58,20 @@ abstract class Entity
public static function create(array $args, $obj = null)
{
$class = static::class;
$obj = $obj ?: new $class();
$date = new DateTime();
if (!is_null($obj)) { // Update modified
if (property_exists($class, 'modified')) {
$args['modified'] = $date;
}
} else {
$obj = new $class();
foreach (['created', 'modified'] as $prop) {
if (property_exists($class, $prop)) {
$args[$prop] = $date;
}
}
}
foreach ($args as $prop => $val) {
if (property_exists($obj, $prop)) {
@ -75,6 +82,7 @@ abstract class Entity
throw new InvalidArgumentException($m);
}
}
return $obj;
}