Commit 4604b317 authored by swentel's avatar swentel

Issue #3166193: accept request works!

parent 827c7147
......@@ -9,6 +9,12 @@ The default path can be overridden in settings.php via settings:
$settings['activitypub_keys_path'] = '/your/path/';
```
## Inbox
Every user has an inbox which currently only accepts a few activities:
'Accept', 'Delete', 'Undo'. All other activity types are ignored.
## Sending posts to followers
Activities in the outbox are stored in a queue and send either by
......
......@@ -9,7 +9,15 @@ activitypub.type.collection:
base_route: activitypub.settings
weight: 1
activitypub.user:
route_name: activitypub.user
base_route: entity.user.canonical
title: 'ActivityPub'
route_name: activitypub.user.activities
base_route: entity.user.canonical
weight: 2
activitypub.user.activities_tab:
title: 'Activities'
route_name: activitypub.user.activities
parent_id: activitypub.user
activitypub.user.settings_tab:
title: 'Settings'
route_name: activitypub.user.settings
parent_id: activitypub.user
......@@ -38,14 +38,27 @@ activitypub.settings:
requirements:
_permission: 'administer activitypub settings'
activitypub.user:
activitypub.user.activities:
path: '/user/{user}/activitypub'
defaults:
_form: '\Drupal\activitypub\Form\ActivityPubUserForm'
_controller: '\Drupal\activitypub\Controller\UserController::activities'
_title: 'ActivityPub'
requirements:
_entity_access: 'user.update'
#_custom_access: '\Drupal\activitypub\Form\ActivityPubUserForm::currentUserCheck'
options:
_admin_route: TRUE
activitypub.user.settings:
path: '/user/{user}/activitypub/settings'
defaults:
_form: '\Drupal\activitypub\Form\ActivityPubUserForm'
_title: 'Settings'
requirements:
_entity_access: 'user.update'
_custom_access: '\Drupal\activitypub\Form\ActivityPubUserForm::currentUserCheck'
options:
_admin_route: TRUE
activitypub.user.self:
path: '/user/{user}/activitypub/{activitypub_actor}'
......
......@@ -27,4 +27,4 @@ services:
arguments: ['activitypub']
activitypub.outbox.client:
class: Drupal\activitypub\Services\ActivityPubOutboxClient
arguments: ['@entity_type.manager', '@activitypub.utility', '@http_client']
arguments: ['@entity_type.manager', '@activitypub.utility', '@http_client', '@logger.channel.activitypub']
......@@ -2,6 +2,7 @@
namespace Drupal\activitypub\Commands;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Url;
use Drush\Commands\DrushCommands;
......@@ -116,12 +117,39 @@ class ActivityPubCommands extends DrushCommands {
$utility = \Drupal::service('activitypub.utility');
$server = $utility->getServer(['instance' => ['debug' => (bool) $debug]]);
// Get a WebFinger instance
// Get actor.
$actor = $server->actor($handle);
print_r($actor->get('inbox'));
// Dumps all properties
//print_r($webfinger->toArray());
// Get a WebFinger instance
$webfinger = $actor->webfinger();
print_r($webfinger->toArray());
}
/**
* Deletes a field. This is a command used during development.
*
* @command activitypub:delete-field
*
* @param $field
* @param $entity_type_id
*/
public function deleteField($field, $entity_type_id) {
$definition_manager = \Drupal::entityDefinitionUpdateManager();
$field_storage_definition = $definition_manager->getFieldStorageDefinition($field, $entity_type_id);
$definition_manager->uninstallFieldStorageDefinition($field_storage_definition);
}
/**
* Adds a field. This is a command used during development.
*
* @command activitypub:add-field
*/
public function addField() {
$external_id_field = BaseFieldDefinition::create('string')
->setLabel(t('External ID'))
->setSetting('max_length', 128);
$definition_manager = \Drupal::entityDefinitionUpdateManager();
$definition_manager->installFieldStorageDefinition('external_id', 'activitypub_activity', 'activitypub', $external_id_field);
}
}
......@@ -28,5 +28,4 @@ class BaseController extends ControllerBase {
return AccessResult::forbidden();
}
}
......@@ -98,16 +98,19 @@ class FollowController extends BaseController {
protected function getCollectionInfo(UserInterface $user, ActivitypubActorInterface $activitypub_actor, $type) {
/** @var \Drupal\activitypub\Entity\Storage\ActivityPubActivityStorageInterface $storage */
$storage = $this->entityTypeManager()->getStorage('activitypub_activity');
$conditions = ['type' => 'Follow'];
$url = Url::fromRoute('activitypub.user.self', ['user' => $activitypub_actor->getOwnerId(), 'activitypub_actor' => $activitypub_actor->getName()], ['absolute' => TRUE])->toString();
$type == ActivityPubActivityInterface::FOLLOWERS ? $key = 'target' : $key = 'actor';
$conditions[$key] = $url;
$total = 0;
if ($type == ActivityPubActivityInterface::FOLLOWERS) {
$conditions = ['type' => 'Follow'];
$conditions['object'] = $url;
$total = $storage->getActivityCount($conditions);
}
return [
'@context' => 'https://www.w3.org/ns/activitystreams',
'type' => 'OrderedCollection',
'id' => Url::fromRoute('activitypub.' . $type, ['user' => $user->id(), 'activitypub_actor' => $activitypub_actor->getName()], ['absolute' => TRUE])->toString(),
'first' => Url::fromRoute('activitypub.' . $type, ['user' => $user->id(), 'activitypub_actor' => $activitypub_actor->getName()], ['absolute' => TRUE, 'query' => ['page' => 0]])->toString(),
'totalItems' => $storage->getActivityCount($conditions),
'totalItems' => $total,
];
}
......@@ -133,17 +136,17 @@ class FollowController extends BaseController {
];
$items = [];
/** @var \Drupal\activitypub\Entity\ActivityPubActivityInterface[] $activities */
$conditions = ['type' => 'Follow'];
$url = Url::fromRoute('activitypub.user.self', ['user' => $activitypub_actor->getOwnerId(), 'activitypub_actor' => $activitypub_actor->getName()], ['absolute' => TRUE])->toString();
$type == ActivityPubActivityInterface::FOLLOWERS ? $key = 'target' : $key = 'actor';
$conditions[$key] = $url;
$activities = $this->entityTypeManager()->getStorage('activitypub_activity')->loadByProperties($conditions);
foreach ($activities as $activity) {
$items[] = $type == ActivityPubActivityInterface::FOLLOWERS ? $activity->getActor() : $activity->getTarget();
if ($type == ActivityPubActivityInterface::FOLLOWERS) {
/** @var \Drupal\activitypub\Entity\ActivityPubActivityInterface[] $activities */
$url = Url::fromRoute('activitypub.user.self', ['user' => $activitypub_actor->getOwnerId(), 'activitypub_actor' => $activitypub_actor->getName()], ['absolute' => TRUE])->toString();
$conditions = ['type' => 'Follow', 'object' => $url];
$activities = $this->entityTypeManager()->getStorage('activitypub_activity')->loadByProperties($conditions);
foreach ($activities as $activity) {
$items[] = $activity->getActor();
}
}
$data['orderedItems'] = $items;
$data['orderedItems'] = $items;
return $data;
}
......
......@@ -2,8 +2,8 @@
namespace Drupal\activitypub\Controller;
use ActivityPhp\Server\Http\HttpSignature;
use ActivityPhp\Type;
use ActivityPhp\Type\TypeConfiguration;
use ActivityPhp\Type\Util;
use Drupal\activitypub\Entity\ActivitypubActorInterface;
use Drupal\activitypub\Services\ActivityPubOutboxClientInterface;
......@@ -76,31 +76,37 @@ class InboxController extends BaseController {
try {
$payload = Util::decodeJson((string)$request->getContent());
// In tests, we can't use the verification as the server has no clue
// about the test. Need to figure out how to get around that.
if ($this->moduleHandler()->moduleExists('activitypub_test')) {
$verified = TRUE;
}
else {
$httpSignature = new HttpSignature($this->activityPubUtility->getServer());
$verified = $httpSignature->verify($request);
}
// Ignore undefined properties.
TypeConfiguration::set('undefined_properties', 'ignore');
// Cast as an ActivityStreams type.
$activityObject = Type::create($payload);
// We currently don't verify the signature if it's available since we only
// accept types which are useful for the user at the moment. The plugins
// handle them when necessary.
// Save when verified and the actor is not blocked.
if ($verified && !$this->domainIsBlocked($activitypub_actor->getBlockedDomains(), $activityObject->get('actor'))) {
if (!$this->domainIsBlocked($activitypub_actor->getBlockedDomains(), $activityObject->get('actor'))) {
$object = $activityObject->get('object');
if (is_array($object) && isset($object['object'])) {
$object = $object['object'];
}
elseif (is_object($object) && isset($object->object)) {
$object = $object->object;
}
/** @var \Drupal\activitypub\Entity\Storage\ActivityPubActivityStorage $storage */
$storage = $this->entityTypeManager()->getStorage('activitypub_activity');
$values = [
'uid' => $user->id(),
'collection' => ' inbox',
'external_id' => $activityObject->get('id'),
'type' => $activityObject->get('type'),
'actor' => $activityObject->get('actor'),
'target' => $activityObject->get('object'),
'payload' => $payload,
'object' => $object,
'payload' => (string) $request->getContent(),
];
/** @var \Drupal\activitypub\Entity\ActivityPubActivityInterface $activity */
......@@ -115,7 +121,7 @@ class InboxController extends BaseController {
}
catch (\Exception $e) {
$this->getLogger('activitypub')->notice('Inbox error: @message', ['@message' => $e->getMessage()]);
$this->getLogger('activitypub')->notice('Inbox error: @message - @content', ['@message' => $e->getMessage(), '@content' => (string) $request->getContent()]);
}
return new JsonResponse(NULL, $status);
......
......@@ -64,20 +64,26 @@ class UserController extends BaseController {
$canonical = Url::fromRoute('entity.user.canonical', ['user' => $user->id()], ['absolute' => TRUE])->toString(TRUE);
$inbox = Url::fromRoute('activitypub.inbox', ['user' => $user->id(), 'activitypub_actor' => $activitypub_actor->getName()], ['absolute' => TRUE])->toString(TRUE);
$outbox = Url::fromRoute('activitypub.outbox', ['user' => $user->id(), 'activitypub_actor' => $activitypub_actor->getName()], ['absolute' => TRUE])->toString(TRUE);
$following = Url::fromRoute('activitypub.following', ['user' => $user->id(), 'activitypub_actor' => $activitypub_actor->getName()], ['absolute' => TRUE])->toString(TRUE);
$followers = Url::fromRoute('activitypub.followers', ['user' => $user->id(), 'activitypub_actor' => $activitypub_actor->getName()], ['absolute' => TRUE])->toString(TRUE);
$metadata->addCacheableDependency($canonical);
$metadata->addCacheableDependency($inbox);
$metadata->addCacheableDependency($outbox);
$metadata->addCacheableDependency($following);
$metadata->addCacheableDependency($followers);
$metadata->addCacheTags(['user:' . $user->id()]);
$data = [
'type' => 'Person',
'@context' => 'https://www.w3.org/ns/activitystreams',
'@context' => ['https://www.w3.org/ns/activitystreams', 'https://w3id.org/security/v1'],
'name' => $activitypub_actor->getName(),
'preferredUsername' => $activitypub_actor->getName(),
'id' => $this->activityPubUtility->getActivityPubID($activitypub_actor),
'url' => $canonical->getGeneratedUrl(),
'inbox' => $inbox->getGeneratedUrl(),
'outbox' => $outbox->getGeneratedUrl(),
'following' => $following->getGeneratedUrl(),
'followers' => $followers->getGeneratedUrl(),
'publicKey' => [
'owner' => $this->activityPubUtility->getActivityPubID($activitypub_actor),
'id' => $this->activityPubUtility->getActivityPubID($activitypub_actor) . '#main-key',
......@@ -102,4 +108,28 @@ class UserController extends BaseController {
return $response;
}
/**
* Activities routing callback.
*
* @param \Drupal\user\UserInterface $user
*
* @return array
*/
public function activities(UserInterface $user) {
/** @var \Drupal\activitypub\Entity\Storage\ActivityPubActorStorageInterface $storage */
$storage = $this->entityTypeManager()->getStorage('activitypub_actor');
$actor = $storage->loadActorByEntityIdAndType($user->id(), 'person');
if (!$actor) {
$settings_link = Url::fromRoute('activitypub.user.settings', ['user' => $user->id()])->toString();
return [
'info' => ['#markup' => '<p>' . $this->t('ActivityPub is not enabled for your account.') . '</p>'],
'link' => ['#markup' => '<p>' . $this->t('<a href="@url">Enable ActivityPub</a>', ['@url' => $settings_link]) . '</p>'],
];
}
else {
return ['#markup' => 'activities'];
}
}
}
......@@ -53,6 +53,18 @@ class ActivityPubActivity extends ContentEntityBase implements ActivityPubActivi
*/
protected $typePluginManager;
/**
* Returns the object plugin manager.
*
* @return \Drupal\activitypub\Services\Type\TypePluginManager
*/
protected function getTypePluginManager() {
if (!isset($this->typePluginManager)) {
$this->typePluginManager = \Drupal::service('plugin.manager.activitypub.type');
}
return $this->typePluginManager;
}
/**
* {@inheritdoc}
*
......@@ -78,6 +90,20 @@ class ActivityPubActivity extends ContentEntityBase implements ActivityPubActivi
return (bool) $this->get('processed')->value;
}
/**
* {@inheritdoc}
*/
public function getCollection() {
return $this->get('collection')->value;
}
/**
* {@inheritdoc}
*/
public function getExternalId() {
return $this->get('external_id')->value;
}
/**
* {@inheritdoc}
*/
......@@ -88,22 +114,22 @@ class ActivityPubActivity extends ContentEntityBase implements ActivityPubActivi
/**
* {@inheritdoc}
*/
public function getConfigID() {
return $this->get('config_id')->value;
public function getObject() {
return $this->get('object')->value;
}
/**
* {@inheritdoc}
*/
public function getActor() {
return $this->get('actor')->value;
public function getConfigID() {
return $this->get('config_id')->value;
}
/**
* {@inheritdoc}
*/
public function getTarget() {
return $this->get('target')->value;
public function getActor() {
return $this->get('actor')->value;
}
/**
......@@ -121,15 +147,10 @@ class ActivityPubActivity extends ContentEntityBase implements ActivityPubActivi
}
/**
* Returns the object plugin manager.
*
* @return \Drupal\activitypub\Services\Type\TypePluginManager
* {@inheritdoc}
*/
protected function getTypePluginManager() {
if (!isset($this->typePluginManager)) {
$this->typePluginManager = \Drupal::service('plugin.manager.activitypub.type');
}
return $this->typePluginManager;
public function getPayLoad() {
return $this->get('payload')->value;
}
/**
......@@ -154,16 +175,14 @@ class ActivityPubActivity extends ContentEntityBase implements ActivityPubActivi
/**
* {@inheritdoc}
*/
public function save() {
$return = parent::save();
public function postSave(EntityStorageInterface $storage, $update = TRUE) {
foreach (array_keys($this->getTypePluginManager()->getDefinitions()) as $plugin_id) {
/** @var \Drupal\activitypub\Services\Type\TypePluginInterface $object */
$object = $this->getTypePluginManager()->createInstance($plugin_id);
$object->onActivitySave($this);
$object->onActivitySave($this, $update);
}
return $return;
}
/**
......@@ -178,6 +197,10 @@ class ActivityPubActivity extends ContentEntityBase implements ActivityPubActivi
->setRequired(TRUE)
->setSetting('max_length', 40);
$fields['external_id'] = BaseFieldDefinition::create('string')
->setLabel(t('External ID'))
->setSetting('max_length', 128);
$fields['type'] = BaseFieldDefinition::create('string')
->setLabel(t('Type'))
->setSetting('max_length', 128);
......@@ -186,10 +209,6 @@ class ActivityPubActivity extends ContentEntityBase implements ActivityPubActivi
->setLabel(t('Actor'))
->setSetting('max_length', 128);
$fields['target'] = BaseFieldDefinition::create('string')
->setLabel(t('Target'))
->setSetting('max_length', 128);
$fields['object'] = BaseFieldDefinition::create('string')
->setLabel(t('Object'))
->setSetting('max_length', 128);
......
......@@ -27,6 +27,20 @@ interface ActivityPubActivityInterface extends ContentEntityInterface, EntityOwn
*/
public function isProcessed();
/**
* Returns the collection.
*
* @return string
*/
public function getCollection();
/**
* Returns the external ID.
*
* @return string
*/
public function getExternalId();
/**
* Returns the config id.
*
......@@ -42,18 +56,18 @@ interface ActivityPubActivityInterface extends ContentEntityInterface, EntityOwn
public function getType();
/**
* Returns the Activity actor.
* Returns the object.
*
* @return string
*/
public function getActor();
public function getObject();
/**
* Returns the Activity target.
* Returns the Activity actor.
*
* @return string
*/
public function getTarget();
public function getActor();
/**
* Returns the target Entity Type.
......@@ -69,6 +83,13 @@ interface ActivityPubActivityInterface extends ContentEntityInterface, EntityOwn
*/
public function getTargetEntityId();
/**
* Get the payload.
*
* @return string
*/
public function getPayLoad();
/**
* Build the activity.
*
......
......@@ -89,15 +89,32 @@ class WebfingerProfileSubscriber implements EventSubscriberInterface {
if (isset($params[WebfingerParameters::ACCOUNT_KEY_NAME]) && ($actor = $this->getActor($params[WebfingerParameters::ACCOUNT_KEY_NAME])) && ($user = $this->getUserByUid($actor->getOwnerId()))) {
$response_cacheability->addCacheContexts(['user']);
$url = Url::fromRoute('entity.user.canonical', ['user' => $user->id()], ['absolute' => TRUE])->toString(TRUE);
$response_cacheability->addCacheableDependency($url);
$account_href = $url->getGeneratedUrl();
$json_rd->addAlias($account_href);
$link = new JsonRdLink();
$link->setRel('http://webfinger.net/rel/profile-page')
->setType('text/html')
->setHref($account_href);
$json_rd->addLink($link);
// Add alias if it's still empty.
if (empty($json_rd->getAliases())) {
$json_rd->addAlias($account_href);
}
$add_profile_link = TRUE;
$links = $json_rd->getLinks();
/** @var JsonRdLink $link */
foreach ($links as $link) {
if ($link->getRel() == 'http://webfinger.net/rel/profile-page') {
$add_profile_link = FALSE;
}
}
if ($add_profile_link) {
$link = new JsonRdLink();
$link->setRel('http://webfinger.net/rel/profile-page')
->setType('text/html')
->setHref($account_href);
$json_rd->addLink($link);
}
$url = Url::fromRoute('activitypub.user.self', ['user' => $user->id(), 'activitypub_actor' => $actor->getName()], ['absolute' => TRUE])->toString(TRUE);
$response_cacheability->addCacheableDependency($url);
......
......@@ -23,7 +23,7 @@ class ConfirmActivityPubActorDeleteForm extends ConfirmFormBase {
* {@inheritdoc}
*/
public function getCancelUrl() {
return Url::fromRoute('activitypub.user', ['user' => $this->currentUser()->id()]);
return Url::fromRoute('activitypub.user.settings', ['user' => $this->currentUser()->id()]);
}
/**
......@@ -74,7 +74,7 @@ class ConfirmActivityPubActorDeleteForm extends ConfirmFormBase {
$activitypub_actor->delete();
$this->messenger()->addMessage($this->t('ActivityPub profile has been deleted.'));
Cache::invalidateTags(['user:' . $activitypub_actor->getOwnerId()]);
$form_state->setRedirectUrl(Url::fromRoute('activitypub.user', ['user' => $activitypub_actor->getOwnerId()]));
$form_state->setRedirectUrl(Url::fromRoute('activitypub.user.settings', ['user' => $activitypub_actor->getOwnerId()]));
}
}
......@@ -30,7 +30,7 @@ class DynamicTypes extends TypePluginBase {
*/
public function getActivities() {
return [
'Create', 'Like'
'Create', 'Like', 'Announce',
];
}
......
......@@ -29,23 +29,25 @@ class StaticTypes extends TypePluginBase {
*/
public function getActivities() {
return [
'Accept', 'Delete',
'Accept', 'Delete', 'Undo'
];
}
/**
* {@inheritdoc}
*/
public function onActivitySave(ActivityPubActivityInterface $activity) {
if ($activity->getType() == 'Follow') {
public function onActivitySave(ActivityPubActivityInterface $activity, $update = TRUE) {
// Follow request.
if ($activity->getType() == 'Follow' && !$update) {
$values = [
'uid' => $activity->getOwnerId(),
'collection' => 'outbox',
'config_id' => 'accept',
'type' => 'Accept',
'actor' => $activity->get('actor'),
'target' => $activity->get('object'),
'actor' => $activity->get('object'),
'object' => $activity->get('actor'),
'status' => 0,
];
......@@ -54,6 +56,19 @@ class StaticTypes extends TypePluginBase {
$outboxActivity->save();
$this->activityPubOutboxClient->createQueueItem($outboxActivity);
}
// Undo request.
if ($activity->getType() == 'Undo' && !$update) {
$payload = json_decode($activity->getPayLoad());
if (isset($payload->object) && isset($payload->object->type) && $payload->object->type == 'Follow') {
$activities = $this->entityTypeManager->getStorage('activitypub_activity')->loadByProperties(['type' => 'Follow', 'actor' => $activity->getActor(), 'object' => $activity->getObject()]);
if ($activities) {
$deleteFollow = array_shift($activities);
$deleteFollow->delete();
}
}
}
}
/**
......@@ -69,7 +84,7 @@ class StaticTypes extends TypePluginBase {
'actor' => $activity->getActor(),
'object' => [
'type' => 'Follow',
'actor' => $activity->getTarget(),
'actor' => $activity->getObject(),
'object' => $activity->getActor(),
]
];
......
......@@ -60,9 +60,9 @@ class ActivityPubKeys implements ActivityPubKeysInterface {
$rsa = new RSA();
$rsa->loadKey($this->getPrivateKey($private_key_path));
$plaintext = "(request-target) post $path\nhost: $host\ndate: $date";
$plaintext = "(request-target): post $path\nhost: $host\ndate: $date";
$rsa->setHash("sha256");
$rsa->setSignatureMode(RSA::SIGNATURE_PSS);
$rsa->setSignatureMode(RSA::SIGNATURE_PKCS1);
return $rsa->sign($plaintext);