[WIP] Begin implementing actor creation
This commit is contained in:
parent
dacd01a47b
commit
48f7c6205c
14
src/Actors/ActivityPubActor.php
Normal file
14
src/Actors/ActivityPubActor.php
Normal file
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
namespace ActivityPub\Actors;
|
||||
|
||||
/**
|
||||
* Represents an ActivityPub actor object
|
||||
*
|
||||
* This class is the main entrypoint for the ActivityPub API, via the
|
||||
* post_to_inbox() and post_to_outbox() methods.
|
||||
*/
|
||||
class ActivityPubActor
|
||||
{
|
||||
// TODO
|
||||
}
|
||||
?>
|
80
src/Actors/ActorService.php
Normal file
80
src/Actors/ActorService.php
Normal file
@ -0,0 +1,80 @@
|
||||
<?php
|
||||
namespace ActivityPub\Actors;
|
||||
|
||||
use ActivityPub\Crypto\RsaKeypair;
|
||||
use ActivityPub\Objects\ObjectService;
|
||||
use ActivityPub\Utils\Util;
|
||||
use Doctrine\ORM\EntityManager;
|
||||
|
||||
class ActorService
|
||||
{
|
||||
/**
|
||||
* @var ObjectService
|
||||
*/
|
||||
private $objectService;
|
||||
|
||||
/**
|
||||
* @var EntityManager
|
||||
*/
|
||||
private $entityManager;
|
||||
|
||||
public function __construct( ObjectService $objectService, EntityManager entityManager )
|
||||
{
|
||||
$this->objectService = $objectService;
|
||||
$this->entityManager = $entityManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new Actor object
|
||||
*
|
||||
* Also creates the actor's collections - inbox, outbox, following,
|
||||
* followers, and liked - and the actor's public/private keypair.
|
||||
* The collections will have ids generated by appending /<collection name>
|
||||
* to the end of the actor's id, e.g. /inbox, /outbox, etc. The public key
|
||||
* will have its id generated by appending #main-key to the end of the actor's
|
||||
* id. The private key will be persisted and associated with the actor object.
|
||||
*
|
||||
* @param array $fields The actor's fields. The id and type fields are required
|
||||
* @return ActivityPubActor The created Actor object
|
||||
*/
|
||||
public function createActor( $fields )
|
||||
{
|
||||
$requiredFields = array( 'id', 'type' );
|
||||
if ( ! Util::arrayKeysExist( $fields, $requiredFields ) ) {
|
||||
throw new InvalidArgumentException( 'Actors require id and type fields' );
|
||||
}
|
||||
$actorId = rtrim( $fields['id'], '/' );
|
||||
$keypair = RsaKeypair::generate();
|
||||
$publicKeyField = array(
|
||||
'id' => "${actorId}#main-key",
|
||||
'owner' => $actorId,
|
||||
'publicKeyPem' => $keypair->getPublicKey(),
|
||||
);
|
||||
$fields['publicKey'] = $publicKeyField;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the actor identified by $id
|
||||
*
|
||||
* @param string $id The actor's id
|
||||
* @return ActivityPubActor The actor
|
||||
*/
|
||||
public function getActor( $id )
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the actor identified by $id by replacing it with a Tombstone object
|
||||
*
|
||||
* Also deletes the actor's collections - inbox, outbox, following,
|
||||
* followers, and liked - and the actor's public/private keypair.
|
||||
* @param string $id The actor's id
|
||||
* @return ActivityPubObject The Tombstone that the actor was replaced with
|
||||
*/
|
||||
public function deleteActor( $id )
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
?>
|
56
src/Collections/CollectionsService.php
Normal file
56
src/Collections/CollectionsService.php
Normal file
@ -0,0 +1,56 @@
|
||||
<?php
|
||||
namespace ActivityPub\Collections;
|
||||
|
||||
use ActivityPub\Entities\ActivityPubObject;
|
||||
use ActivityPub\Objects\ObjectsService;
|
||||
|
||||
class CollectionsService
|
||||
{
|
||||
/**
|
||||
* Creates a new collection - ordered/unordered and paged/unpaged
|
||||
*
|
||||
* @return ActivityPubObject The created Collection object
|
||||
*/
|
||||
public function createCollection()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the object to the collection
|
||||
*
|
||||
* This method handles ordered/unordered and paged/unpaged collections.
|
||||
* @param string $collectionId The id of collection
|
||||
* @param ActivityPubObject $object The object to add to the collection
|
||||
*/
|
||||
public function addToCollection( string $collectionId, ActivityPubObject $object )
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the object from the collection
|
||||
*
|
||||
* This method handles ordered/unordered and paged/unpaged collections.
|
||||
* @param string $collectionId The id of the collection
|
||||
* @param string $objectId The id of the object to remove
|
||||
*/
|
||||
public function removeFromCollection( string $collectionId, string $objectId )
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the collection by replacing it with a Tombstone object
|
||||
*
|
||||
* None of the items in the collection will be deleted; however, if it is
|
||||
* a PagedCollection then all the collection page objects will be deleted.
|
||||
* @param string $collectionId The id of the collection
|
||||
* @return ActivityPubObject The Tombstone that the collection was replaced with
|
||||
*/
|
||||
public function deleteCollection( string $collectionId )
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
?>
|
@ -38,6 +38,16 @@ class RsaKeypair
|
||||
return $this->publicKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the private key as a string
|
||||
*
|
||||
* @return string The private key
|
||||
*/
|
||||
public function getPrivateKey()
|
||||
{
|
||||
return $this->privateKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a signature for $data
|
||||
*
|
||||
@ -63,7 +73,7 @@ class RsaKeypair
|
||||
*
|
||||
* @param string $data The data
|
||||
* @param string $signature The signature
|
||||
* @return boolean
|
||||
* @return bool
|
||||
*/
|
||||
public function verify( $data, $signature )
|
||||
{
|
||||
|
@ -49,6 +49,13 @@ class ActivityPubObject implements ArrayAccess
|
||||
*/
|
||||
protected $lastUpdated;
|
||||
|
||||
/**
|
||||
* The private key associated with this object, if any
|
||||
* @OneToOne(targetEntity="PrivateKey", mappedBy="object", cascade={"all"})
|
||||
* @var PrivateKey
|
||||
*/
|
||||
protected $privateKey;
|
||||
|
||||
public function __construct( DateTime $time = null ) {
|
||||
if ( ! $time ) {
|
||||
$time = new DateTime( "now" );
|
||||
@ -254,6 +261,30 @@ class ActivityPubObject implements ArrayAccess
|
||||
$this->lastUpdated = $lastUpdated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this object has an associated private key, false if otherwise
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasPrivateKey()
|
||||
{
|
||||
return $this->privateKey !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the object's private key
|
||||
*
|
||||
* @param string $key The new private key value
|
||||
*/
|
||||
public function setPrivateKey( string $key )
|
||||
{
|
||||
if ( $this->hasPrivateKey() ) {
|
||||
$this->privateKey->setKey( $key );
|
||||
} else {
|
||||
$this->privateKey = new PrivateKey( $key, $this );
|
||||
}
|
||||
}
|
||||
|
||||
public function offsetExists( $offset )
|
||||
{
|
||||
return $this->hasField( $offset );
|
||||
|
61
src/Entities/PrivateKey.php
Normal file
61
src/Entities/PrivateKey.php
Normal file
@ -0,0 +1,61 @@
|
||||
<?php
|
||||
namespace ActivityPub\Entities;
|
||||
|
||||
use ActivityPub\Entities\ActivityPubObject;
|
||||
|
||||
/**
|
||||
* The keys table holds the private keys associated with ActivityPub actors
|
||||
*
|
||||
* @Entity @Table(name="keys")
|
||||
*/
|
||||
class PrivateKey
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
* @Id
|
||||
* @Column(type="integer")
|
||||
* @GeneratedValue
|
||||
*/
|
||||
protected $id;
|
||||
|
||||
/**
|
||||
* The private key
|
||||
*
|
||||
* @var string
|
||||
* @Column(type="string")
|
||||
*/
|
||||
protected $key;
|
||||
|
||||
/**
|
||||
* The object associated with this private key
|
||||
*
|
||||
* @var ActivityPubObject
|
||||
* @OneToOne(targetEntity="ActivityPubObject", inversedBy="privateKey")
|
||||
*/
|
||||
protected $object;
|
||||
|
||||
/**
|
||||
* Creates a new PrivateKey
|
||||
*
|
||||
* Don't call this directly - instead, use ActivityPubObject->setPrivateKey()
|
||||
* @param string $key The private key as a string
|
||||
* @param ActivityPubObject $object The object associated with this key
|
||||
*/
|
||||
public function __construct( string $key, ActivityPubObject $object )
|
||||
{
|
||||
$this->key = $key;
|
||||
$this->object = $object;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the private key string
|
||||
*
|
||||
* Don't call this directly - instead, use ActivityPubObject->setPrivateKey()
|
||||
* @param string $key The private key as a string
|
||||
*/
|
||||
public function setKey( string $key )
|
||||
{
|
||||
$this->key = $key;
|
||||
}
|
||||
}
|
||||
?>
|
@ -3,10 +3,33 @@ namespace ActivityPub\Utils;
|
||||
|
||||
class Util
|
||||
{
|
||||
public static function isAssoc(array $arr)
|
||||
/**
|
||||
* Returns true if the input array is associative
|
||||
*
|
||||
* @param array $arr The array to test
|
||||
* @return bool True if the array is associative, false otherwise
|
||||
*/
|
||||
public static function isAssoc( array $arr )
|
||||
{
|
||||
if (array() === $arr) return false;
|
||||
return array_keys($arr) !== range(0, count($arr) - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if all of the specified keys exist in the array
|
||||
*
|
||||
* @param array $arr The array to check
|
||||
* @param array $keys The keys to check the existence of
|
||||
* @return bool True if all of the keys are in the array, false otherwise
|
||||
*/
|
||||
public static function arrayKeysExist( array $arr, array $keys )
|
||||
{
|
||||
foreach ( $keys as $key ) {
|
||||
if ( ! array_key_exists( $key, $arr ) ) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
112
test/EntityTest.php
Normal file
112
test/EntityTest.php
Normal file
@ -0,0 +1,112 @@
|
||||
<?php
|
||||
namespace ActivityPub\Test;
|
||||
|
||||
use DateTime;
|
||||
use ActivityPub\Crypto\RsaKeypair;
|
||||
use ActivityPub\Entities\ActivityPubObject;
|
||||
use ActivityPub\Database\PrefixNamingStrategy;
|
||||
use ActivityPub\Test\Config\ArrayDataSet;
|
||||
use ActivityPub\Test\Config\SQLiteTestCase;
|
||||
use ActivityPub\Test\TestUtils\TestDateTimeProvider;
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Doctrine\ORM\Tools\Setup;
|
||||
|
||||
class EntityTest extends SQLiteTestCase
|
||||
{
|
||||
protected $entityManager;
|
||||
protected $dateTimeProvider;
|
||||
|
||||
protected function getDataSet()
|
||||
{
|
||||
return new ArrayDataSet( array(
|
||||
'objects' => array(),
|
||||
'fields' => array(),
|
||||
'keys' => array(),
|
||||
) );
|
||||
}
|
||||
|
||||
protected function setUp()
|
||||
{
|
||||
parent::setUp();
|
||||
$dbConfig = Setup::createAnnotationMetadataConfiguration(
|
||||
array( __DIR__ . '/../src/Entities' ), true
|
||||
);
|
||||
$namingStrategy = new PrefixNamingStrategy( '' );
|
||||
$dbConfig->setNamingStrategy( $namingStrategy );
|
||||
$dbParams = array(
|
||||
'driver' => 'pdo_sqlite',
|
||||
'path' => __DIR__ . '/db.sqlite',
|
||||
);
|
||||
$this->entityManager = EntityManager::create( $dbParams, $dbConfig );
|
||||
$this->dateTimeProvider = new TestDateTimeProvider(
|
||||
new DateTime( "12:00" ), new DateTime( "12:01" )
|
||||
);
|
||||
}
|
||||
|
||||
private function getTime( $context ) {
|
||||
return $this->dateTimeProvider
|
||||
->getTime( $context )
|
||||
->format( "Y-m-d H:i:s" );
|
||||
}
|
||||
|
||||
public function testItCreatesAnObjectWithAPrivateKey()
|
||||
{
|
||||
$object = new ActivityPubObject( $this->dateTimeProvider->getTime( 'create' ) );
|
||||
$privateKey = 'a private key';
|
||||
$object->setPrivateKey( $privateKey );
|
||||
$this->entityManager->persist( $object );
|
||||
$this->entityManager->flush();
|
||||
$now = $this->getTime( 'create' );
|
||||
$expected = new ArrayDataSet( array(
|
||||
'objects' => array(
|
||||
array( 'id' => 1, 'created' => $now, 'lastUpdated' => $now ),
|
||||
),
|
||||
'keys' => array(
|
||||
array( 'id' => 1, 'object_id' => 1, 'key' => $privateKey )
|
||||
),
|
||||
) );
|
||||
$expectedObjectsTable = $expected->getTable( 'objects' );
|
||||
$expectedKeysTable = $expected->getTable( 'keys' );
|
||||
$objectsQueryTable = $this->getConnection()->createQueryTable(
|
||||
'objects', 'SELECT * FROM objects'
|
||||
);
|
||||
$keysQueryTable = $this->getConnection()->createQueryTable(
|
||||
'keys', 'SELECT * FROM keys'
|
||||
);
|
||||
$this->assertTablesEqual( $expectedObjectsTable, $objectsQueryTable );
|
||||
$this->assertTablesEqual( $expectedKeysTable, $keysQueryTable );
|
||||
}
|
||||
|
||||
public function itUpdatesAPrivateKey()
|
||||
{
|
||||
$object = new ActivityPubObject( $this->dateTimeProvider->getTime( 'create' ) );
|
||||
$privateKey = 'a private key';
|
||||
$object->setPrivateKey( $privateKey );
|
||||
$this->entityManager->persist( $object );
|
||||
$this->entityManager->flush();
|
||||
$newPrivateKey = 'a new private key';
|
||||
$object->setPrivateKey( $newPrivateKey );
|
||||
$this->entityManager->persiste( $object );
|
||||
$this->entityManager->flush();
|
||||
$now = $this->getTime( 'create' );
|
||||
$expected = new ArrayDataSet( array(
|
||||
'objects' => array(
|
||||
array( 'id' => 1, 'created' => $now, 'lastUpdated' => $now ),
|
||||
),
|
||||
'keys' => array(
|
||||
array( 'id' => 1, 'object_id' => 1, 'key' => $newPrivateKey )
|
||||
),
|
||||
) );
|
||||
$expectedObjectsTable = $expected->getTable( 'objects' );
|
||||
$expectedKeysTable = $expected->getTable( 'keys' );
|
||||
$objectsQueryTable = $this->getConnection()->createQueryTable(
|
||||
'objects', 'SELECT * FROM objects'
|
||||
);
|
||||
$keysQueryTable = $this->getConnection()->createQueryTable(
|
||||
'keys', 'SELECT * FROM keys'
|
||||
);
|
||||
$this->assertTablesEqual( $expectedObjectsTable, $objectsQueryTable );
|
||||
$this->assertTablesEqual( $expectedKeysTable, $keysQueryTable );
|
||||
}
|
||||
}
|
||||
?>
|
@ -38,8 +38,12 @@ class ObjectsServiceTest extends SQLiteTestCase
|
||||
'path' => __DIR__ . '/db.sqlite',
|
||||
);
|
||||
$this->entityManager = EntityManager::create( $dbParams, $dbConfig );
|
||||
$this->dateTimeProvider = new TestDateTimeProvider( new DateTime( "12:00" ), new DateTime( "12:01" ) );
|
||||
$this->objectsService = new ObjectsService( $this->entityManager, $this->dateTimeProvider );
|
||||
$this->dateTimeProvider = new TestDateTimeProvider(
|
||||
new DateTime( "12:00" ), new DateTime( "12:01" )
|
||||
);
|
||||
$this->objectsService = new ObjectsService(
|
||||
$this->entityManager, $this->dateTimeProvider
|
||||
);
|
||||
}
|
||||
|
||||
private function getTime( $context ) {
|
||||
|
@ -13,6 +13,12 @@ class RsaKeypairTest extends TestCase
|
||||
$keypair = RsaKeypair::generate();
|
||||
$this->assertStringStartsWith( '-----BEGIN PUBLIC KEY-----', $keypair->getPublicKey() );
|
||||
$this->assertStringEndsWith( '-----END PUBLIC KEY-----', $keypair->getPublicKey() );
|
||||
$this->assertStringStartsWith(
|
||||
'-----BEGIN RSA PRIVATE KEY-----', $keypair->getPrivateKey()
|
||||
);
|
||||
$this->assertStringEndsWith(
|
||||
'-----END RSA PRIVATE KEY-----', $keypair->getPrivateKey()
|
||||
);
|
||||
}
|
||||
|
||||
public function testItSignsAndValidatesSignatures()
|
||||
|
85
test/UtilTest.php
Normal file
85
test/UtilTest.php
Normal file
@ -0,0 +1,85 @@
|
||||
<?php
|
||||
namespace ActivityPub\Test;
|
||||
|
||||
use ActivityPub\Utils\Util;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class UtilTest extends TestCase
|
||||
{
|
||||
public function testItFindsAssocArray()
|
||||
{
|
||||
$arr = array( 'foo' => 'bar' );
|
||||
$isAssoc = Util::isAssoc( $arr );
|
||||
$this->assertTrue( $isAssoc );
|
||||
}
|
||||
|
||||
public function testItReturnsFalseForNonAssoc()
|
||||
{
|
||||
$arr = array( 'foo', 'bar' );
|
||||
$isAssoc = Util::isAssoc( $arr );
|
||||
$this->assertFalse( $isAssoc );
|
||||
}
|
||||
|
||||
public function testItHandlesMixedArray()
|
||||
{
|
||||
$arr = array( 'foo' => 'bar', 'baz' );
|
||||
$isAssoc = Util::isAssoc( $arr );
|
||||
$this->assertTrue( $isAssoc );
|
||||
}
|
||||
|
||||
public function testItChecksEmptyArrayIsAssoc()
|
||||
{
|
||||
$arr = array();
|
||||
$isAssoc = Util::isAssoc( $arr );
|
||||
$this->assertFalse( $isAssoc );
|
||||
}
|
||||
|
||||
public function testArrayKeysExist()
|
||||
{
|
||||
$arr = array( 'foo' => 'bar', 'baz' => 'qux' );
|
||||
$keys = array( 'foo', 'baz' );
|
||||
$keysExist = Util::arrayKeysExist( $arr, $keys );
|
||||
$this->assertTrue( $keysExist );
|
||||
}
|
||||
|
||||
public function testItChecksForAllKeys()
|
||||
{
|
||||
$arr = array( 'foo' => 'bar' );
|
||||
$keys = array( 'foo', 'baz' );
|
||||
$keysExist = Util::arrayKeysExist( $arr, $keys );
|
||||
$this->assertFalse( $keysExist );
|
||||
}
|
||||
|
||||
public function testItAllowsExtraKeys()
|
||||
{
|
||||
$arr = array( 'foo' => 'bar', 'baz' => 'qux' );
|
||||
$keys = array( 'foo' );
|
||||
$keysExist = Util::arrayKeysExist( $arr, $keys );
|
||||
$this->assertTrue( $keysExist );
|
||||
}
|
||||
|
||||
public function testItHandlesEmptyArray()
|
||||
{
|
||||
$arr = array();
|
||||
$keys = array( 'foo' );
|
||||
$keysExist = Util::arrayKeysExist( $arr, $keys );
|
||||
$this->assertFalse( $keysExist );
|
||||
}
|
||||
|
||||
public function testItHandlesEmptyKeys()
|
||||
{
|
||||
$arr = array( 'foo' => 'bar', 'baz' => 'qux' );
|
||||
$keys = array();
|
||||
$keysExist = Util::arrayKeysExist( $arr, $keys );
|
||||
$this->assertTrue( $keysExist );
|
||||
}
|
||||
|
||||
public function testItHandlesBothEmpty()
|
||||
{
|
||||
$arr = array();
|
||||
$keys = array();
|
||||
$keysExist = Util::arrayKeysExist( $arr, $keys );
|
||||
$this->assertTrue( $keysExist );
|
||||
}
|
||||
}
|
||||
?>
|
Loading…
Reference in New Issue
Block a user