Commit 2bc80451 authored by swentel's avatar swentel

Issue #3174747: send to followers

parent d4720a89
......@@ -61,16 +61,17 @@ entity which will process this node in the 'ActivityPub outbox' fieldset.
**Important**
1. The 'Send to' fieldset must currently have a value and point to a remote
user. Sending a post to multiple followers is in development and will arrive
soon.
2. Make sure a Reply URL, Like object points to the id of the post You can get
this id from the application/activity+json representation of a post. Examples:
1. The 'Send to' fieldset must have the canonical URL of a user in case the
owner does not follow you and you are replying to a post. Any post will be
sent automatically in 'cc' to all your followers too.
2. Make sure a Reply, Like or Announce URL point ot the id of the post You can
get this id from the application/activity+json representation of a post.
Examples:
- Mastodon reply/like
- Will work: https://mastodon.social/@swentel/104960564425633436
- Will not work: https://mastodon.social/web/statuses/104960564425633436
- Pleroma like 'object' needs to be the id of an activity, not the browser
URL, but works fine for the inReplyTo property.
URL, but works fine for a reply.
Footnotes:
......
......@@ -4,6 +4,10 @@ use Drupal\Core\File\FileSystemInterface;
/**
* Implements hook_requirements().
*
* @param $phase
*
* @return array
*/
function activitypub_requirements($phase) {
$requirements = [];
......
......@@ -17,7 +17,7 @@
"drupal/core": "^8.8 || ^9",
"drupal/webfinger": "~1.0",
"drupal/nodeinfo": "~1.0",
"landrok/activitypub": "^0.4.2",
"landrok/activitypub": "^0.5.0",
"ext-json": "*"
},
"extra": {
......
......@@ -2,8 +2,6 @@
namespace Drupal\activitypub\Commands;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Url;
use Drush\Commands\DrushCommands;
/**
......
......@@ -106,7 +106,7 @@ class FollowController extends BaseController {
$total = $storage->getActivityCount($conditions);
}
return [
'@context' => 'https://www.w3.org/ns/activitystreams',
'@context' => ActivityPubActivityInterface::CONTEXT_URL,
'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(),
......@@ -129,7 +129,7 @@ class FollowController extends BaseController {
protected function getCollectionItems(UserInterface $user, ActivitypubActorInterface $activitypub_actor, int $page, $type) {
$data = [
'@context' => 'https://www.w3.org/ns/activitystreams',
'@context' => ActivityPubActivityInterface::CONTEXT_URL,
'type' => 'OrderedCollectionPage',
'partOf' => Url::fromRoute('activitypub.' . $type, ['user' => $user->id(), 'activitypub_actor' => $activitypub_actor->getName()], ['absolute' => TRUE])->toString(),
'id' => Url::fromRoute('activitypub.' . $type, ['user' => $user->id(), 'activitypub_actor' => $activitypub_actor->getName()], ['absolute' => TRUE, 'query' => ['page' => $page]])->toString(),
......
......@@ -2,7 +2,7 @@
namespace Drupal\activitypub\Controller;
use Drupal\activitypub\Services\ActivityPubProcessClientInterface;
use Drupal\activitypub\Entity\ActivityPubActivityInterface;
use Drupal\node\NodeInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
......@@ -40,7 +40,7 @@ class NodeController extends BaseController {
else {
$output = $build;
}
$output['@context'] = ActivityPubProcessClientInterface::STREAMS_CONTEXT;
$output['@context'] = ActivityPubActivityInterface::CONTEXT_URL;
$response = new JsonResponse($output, 200);
$response->headers->set('Content-Type', 'application/activity+json');
return $response;
......
......@@ -2,6 +2,7 @@
namespace Drupal\activitypub\Controller;
use Drupal\activitypub\Entity\ActivityPubActivityInterface;
use Drupal\activitypub\Entity\ActivitypubActorInterface;
use Drupal\Core\Cache\CacheableJsonResponse;
use Drupal\Core\Cache\CacheableMetadata;
......@@ -74,7 +75,7 @@ class OutboxController extends BaseController {
$first = Url::fromRoute('activitypub.outbox', ['user' => $user->id(), 'activitypub_actor' => $activitypub_actor->getName()], ['absolute' => TRUE, 'query' => ['page' => 0]])->toString(TRUE);
$metadata->addCacheableDependency($first);
return [
'@context' => 'https://www.w3.org/ns/activitystreams',
'@context' => ActivityPubActivityInterface::CONTEXT_URL,
'type' => 'OrderedCollection',
'id' => $id->getGeneratedUrl(),
'first' => $first->getGeneratedUrl(),
......@@ -103,7 +104,7 @@ class OutboxController extends BaseController {
$metadata->addCacheableDependency($page);
$data = [
'@context' => 'https://www.w3.org/ns/activitystreams',
'@context' => ActivityPubActivityInterface::CONTEXT_URL,
'type' => 'OrderedCollectionPage',
'partOf' => $id->getGeneratedUrl(),
'id' => $page->getGeneratedUrl(),
......
......@@ -2,6 +2,7 @@
namespace Drupal\activitypub\Controller;
use Drupal\activitypub\Entity\ActivityPubActivityInterface;
use Drupal\activitypub\Entity\ActivityPubActorInterface;
use Drupal\activitypub\Services\ActivityPubKeysInterface;
use Drupal\activitypub\Services\ActivityPubUtilityInterface;
......@@ -76,7 +77,7 @@ class UserController extends BaseController {
$data = [
'type' => 'Person',
'@context' => ['https://www.w3.org/ns/activitystreams', 'https://w3id.org/security/v1'],
'@context' => [ActivityPubActivityInterface::CONTEXT_URL, ActivityPubActivityInterface::SECURITY_URL],
'name' => $activitypub_actor->getName(),
'preferredUsername' => $activitypub_actor->getName(),
'id' => $this->activityPubUtility->getActivityPubID($activitypub_actor),
......
......@@ -14,6 +14,9 @@ interface ActivityPubActivityInterface extends ContentEntityInterface, EntityOwn
const FOLLOWING = 'following';
const INBOX = 'inbox';
const OUTBOX = 'outbox';
const CONTEXT_URL = 'https://www.w3.org/ns/activitystreams';
const SECURITY_URL = 'https://w3id.org/security/v1';
const PUBLIC_URL = 'https://www.w3.org/ns/activitystreams#Public';
/**
* Gets the activity creation timestamp.
......
......@@ -3,10 +3,10 @@
namespace Drupal\activitypub\Plugin\activitypub\type;
use Drupal\activitypub\Entity\ActivityPubActivityInterface;
use Drupal\activitypub\Services\ActivityPubProcessClientInterface;
use Drupal\activitypub\Services\Type\TypePluginBase;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
/**
* The ActivityPub core types.
......@@ -106,7 +106,7 @@ class DynamicTypes extends TypePluginBase {
public function build(ActivityPubActivityInterface $activity, EntityInterface $entity = NULL) {
$object = [
'type' => $this->getConfiguration()['object'],
'id' => $this->renderUrl($entity),
'id' => $this->renderEntityUrl($entity),
'attributedTo' => $activity->getActor(),
];
......@@ -120,33 +120,38 @@ class DynamicTypes extends TypePluginBase {
}
}
$to = [ActivityPubProcessClientInterface::PUBLIC];
$to = [ActivityPubActivityInterface::PUBLIC_URL];
/** @var \Drupal\activitypub\Entity\ActivityPubActorInterface $actor */
$actor = $this->entityTypeManager->getStorage('activitypub_actor')->loadActorByEntityIdAndType($activity->getOwnerId(), 'person');
$cc = [$this->renderUrl(Url::fromRoute('activitypub.followers', ['user' => $actor->getOwnerId(), 'activitypub_actor' => $actor->getName()], ['absolute' => TRUE]))];
if (!empty($activity->getObject())) {
$to[] = $activity->getObject();
}
$object['to'] = $to;
$object['cc'] = $cc;
// Create.
if ($this->getConfiguration()['activity'] == 'Create') {
$activity = [
'type' => $this->getConfiguration()['activity'],
'id' => $this->renderUrl($activity),
'id' => $this->renderEntityUrl($activity),
'actor' => $activity->getActor(),
'to' => $to,
'cc' => $cc,
'object' => $object
];
}
else {
$activity = [
'type' => $this->getConfiguration()['activity'],
'id' => $this->renderUrl($activity),
'id' => $this->renderEntityUrl($activity),
'actor' => $activity->getActor(),
'to' => $to,
'cc' => $cc,
'object' => $object['object']
];
}
return $activity;
}
......
......@@ -4,7 +4,6 @@ namespace Drupal\activitypub\Plugin\activitypub\type;
use ActivityPhp\Type;
use Drupal\activitypub\Entity\ActivityPubActivityInterface;
use Drupal\activitypub\Services\ActivityPubProcessClientInterface;
use Drupal\activitypub\Services\Type\TypePluginBase;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Site\Settings;
......@@ -60,8 +59,8 @@ class StaticTypes extends TypePluginBase {
// Undo request.
if ($activity->getType() == 'Undo' && !$update) {
$payload = json_decode($activity->getPayLoad());
if (isset($payload->object) && isset($payload->object->type) && $payload->object->type == 'Follow') {
$payload = @json_decode($activity->getPayLoad(), TRUE);
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);
......@@ -89,7 +88,7 @@ class StaticTypes extends TypePluginBase {
if ($this->getConfiguration()['activity'] == 'Accept') {
$attributes = [
'id' => $this->renderUrl($activity),
'id' => $this->renderEntityUrl($activity),
'type' => $this->getConfiguration()['activity'],
'actor' => $activity->getActor(),
'object' => [
......@@ -104,23 +103,30 @@ class StaticTypes extends TypePluginBase {
if ($this->getConfiguration()['activity'] == 'Delete') {
$to = [ActivityPubProcessClientInterface::PUBLIC];
$to = [ActivityPubActivityInterface::PUBLIC_URL];
if (!empty($activity->getObject())) {
$to[] = $activity->getObject();
}
/** @var \Drupal\activitypub\Entity\ActivityPubActorInterface $actor */
$actor = $this->entityTypeManager->getStorage('activitypub_actor')->loadActorByEntityIdAndType($activity->getOwnerId(), 'person');
$cc = [$this->renderUrl(Url::fromRoute('activitypub.followers', ['user' => $actor->getOwnerId(), 'activitypub_actor' => $actor->getName()], ['absolute' => TRUE]))];
$attributes['to'] = $to;
$attributes['cc'] = $cc;
if (isset($entity)) {
$id = $this->renderUrl($entity);
$id = $this->renderEntityUrl($entity);
}
else {
$id = $this->renderUrl($activity);
$id = $this->renderEntityUrl($activity);
}
$attributes = [
'type' => $this->getConfiguration()['activity'],
'actor' => $activity->getActor(),
'to' => $to,
'cc' => $cc,
'id' => $id,
'object' => ['id' => $id],
];
......
......@@ -4,6 +4,7 @@ namespace Drupal\activitypub\Services;
use Drupal\activitypub\Entity\ActivityPubActivityInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Url;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Exception\RequestException;
use Psr\Log\LoggerInterface;
......@@ -95,41 +96,62 @@ class ActivityPubProcessClient implements ActivityPubProcessClientInterface {
}
$remove_queue_item = TRUE;
if (!$remove_item_from_queue) {
$remove_queue_item = FALSE;
}
try {
/** @var \Drupal\activitypub\Entity\ActivityPubActivityInterface $activity */
$activity = $this->entityTypeManager->getStorage('activitypub_activity')->load($data['activity']);
$actor = $actorStorage->loadActorByEntityIdAndType($activity->getOwnerId(), 'person');
// Build activity.
$build = $activity->buildActivity();
// Add context.
$build['@context'] = ActivityPubProcessClientInterface::STREAMS_CONTEXT;
$this->debug($build);
$build['@context'] = ActivityPubActivityInterface::CONTEXT_URL;
// Send to.
$inboxes = [];
$targets = [];
if (!empty($build['to'])) {
foreach ($build['to'] as $t) {
if ($t != self::PUBLIC) {
if ($t != ActivityPubActivityInterface::PUBLIC_URL) {
$targets[] = $t;
}
}
}
elseif (!empty($activity->getObject())) {
$targets[] = $activity->getObject();
// cc, these are always followers at the moment.
if (!empty($build['cc'][0])) {
if (isset($build['object']['cc'])) {
$build['object']['cc'] = $build['cc'];
}
// Get followers.
$conditions = ['type' => 'Follow', 'object' => str_replace('/followers', '', $build['cc'][0])];
/** @var \Drupal\activitypub\Entity\ActivityPubActivityInterface $followers */
$followers = $this->entityTypeManager->getStorage('activitypub_activity')->loadByProperties($conditions);
foreach ($followers as $follower) {
$targets[] = $follower->getActor();
}
}
$this->debug($build);
// Get inboxes.
foreach ($targets as $target) {
$inboxes[] = $server->actor($target)->get('inbox');
$inbox = (string) $server->actor($target)->get('inbox');
$inboxes[$inbox] = $inbox;
}
if (!empty($inboxes)) {
$this->debug($inboxes);
$keyId = $activity->getActor();
foreach ($inboxes as $inbox) {
$actor = $actorStorage->loadActorByEntityIdAndType($activity->getOwnerId(), 'person');
$keyId = $activity->getActor();
$parsed = parse_url($inbox);
$host = $parsed['host'];
......@@ -150,31 +172,28 @@ class ActivityPubProcessClient implements ActivityPubProcessClientInterface {
if ($send) {
try {
$response = $this->httpClient->post($inbox, ['json' => $build, 'headers' => $headers]);
$this->logger->notice('Outbox response for @id: code: @code - @response', ['@id' => $activity->id(), '@code' => $response->getStatusCode(), '@response' => (string) $response->getBody()]);
$activity->set('processed', TRUE);
$activity->set('queued', FALSE);
$activity->save();
$this->logger->notice('Outbox response to @inbox for @id: code: @code - @response', ['@inbox' => $inbox, '@id' => $activity->id(), '@code' => $response->getStatusCode(), '@response' => (string) $response->getBody()]);
}
catch (RequestException $e) {
$remove_queue_item = FALSE;
$this->logger->notice('Outbox request exception for @id to @inbox: @message', ['@inbox' => $inbox, '@message' => $e->getMessage(), '@id' => $activity->id()]);
$this->logger->notice('Outbox exception to @inbox for @id to @inbox: @message', ['@inbox' => $inbox, '@message' => $e->getMessage(), '@id' => $activity->id()]);
}
}
}
}
if (!$remove_item_from_queue) {
$remove_queue_item = FALSE;
}
}
catch (\Exception $e) {
$this->debug($e->getMessage());
$remove_queue_item = FALSE;
$this->logger->notice('Outbox general exception for @id: @message', ['@message' => $e->getMessage(), '@id' => $activity->id()]);
$this->logger->notice('Outbox general exception to @inbox for @id: @message', ['@message' => $e->getMessage(), '@id' => $activity->id()]);
}
// Remove or release the queue item.
if ($remove_queue_item) {
try {
$activity->set('processed', TRUE);
$activity->set('queued', FALSE);
$activity->save();
}
catch (\Exception $ignored) {}
$this->deleteItemFromQueue($item, ACTIVITYPUB_OUTBOX_QUEUE);
}
else {
......
......@@ -6,9 +6,6 @@ use Drupal\activitypub\Entity\ActivityPubActivityInterface;
interface ActivityPubProcessClientInterface {
const STREAMS_CONTEXT = 'https://www.w3.org/ns/activitystreams';
const PUBLIC = 'https://www.w3.org/ns/activitystreams#Public';
/**
* Handles the Outbox queue.
*
......
......@@ -3,7 +3,6 @@
namespace Drupal\activitypub\Services;
use ActivityPhp\Server;
use ActivityPhp\Type\TypeConfiguration;
use Drupal\activitypub\Entity\ActivityPubActorInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
......@@ -111,7 +110,7 @@ class ActivityPubUtility implements ActivityPubUtilityInterface {
$config += [
'cache' => ['enabled' => FALSE],
];
TypeConfiguration::set('undefined_properties', 'ignore');
$config['instance']['types'] = 'ignore';
return new Server($config);
}
......
......@@ -15,6 +15,7 @@ use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Plugin\PluginBase;
use Drupal\Core\Render\RenderContext;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
......@@ -173,18 +174,31 @@ abstract class TypePluginBase extends PluginBase implements TypePluginInterface,
}
/**
* Render a URL.
* Render a URL from an entity.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
*
* @return string
*/
protected function renderUrl(EntityInterface $entity) {
protected function renderEntityUrl(EntityInterface $entity) {
return $this->renderer->executeInRenderContext(new RenderContext(), function () use ($entity) {
return $entity->toUrl('canonical', ['absolute' => TRUE])->toString();
});
}
/**
* Render a Url.
*
* @param \Drupal\Core\Url $url
*
* @return string
*/
protected function renderUrl(Url $url) {
return $this->renderer->executeInRenderContext(new RenderContext(), function () use ($url) {
return $url->toString();
});
}
/**
* {@inheritdoc}
*/
......
......@@ -86,7 +86,7 @@ class CommentTest extends ActivityPubTestBase {
'body' => ['value' => $node_content],
];
$node = $this->drupalCreateNode($node_values);
$this->drupalLogout();;
$this->drupalLogout();
// Actor href.
$actor_href = Url::fromRoute('activitypub.user.self', ['user' => $this->authenticatedUserOne->id(), 'activitypub_actor' => $this->accountNameOne], ['absolute' => TRUE])->toString();
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment