Merge pull request #8904 from MrPetovan/task/ap-conversion-admin-module

Add new admin debug module for ActivityPub
This commit is contained in:
Michael Vogel 2020-07-21 21:32:06 +02:00 committed by GitHub
commit 41141965fc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 248 additions and 74 deletions

View file

@ -121,6 +121,7 @@ abstract class BaseAdmin extends BaseModule
'webfinger' => ['webfinger' , DI::l10n()->t('check webfinger') , 'webfinger'],
'itemsource' => ['admin/item/source' , DI::l10n()->t('Item Source') , 'itemsource'],
'babel' => ['babel' , DI::l10n()->t('Babel') , 'babel'],
'debug/ap' => ['debug/ap' , DI::l10n()->t('ActivityPub Conversion') , 'debug/ap'],
]],
];

View file

@ -0,0 +1,144 @@
<?php
/**
* @copyright Copyright (C) 2020, Friendica
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
namespace Friendica\Module\Debug;
use Friendica\BaseModule;
use Friendica\Content\Text;
use Friendica\Core\Logger;
use Friendica\Core\Renderer;
use Friendica\DI;
use Friendica\Model\Item;
use Friendica\Model\Tag;
use Friendica\Protocol\ActivityPub;
use Friendica\Util\JsonLD;
use Friendica\Util\XML;
class ActivityPubConversion extends BaseModule
{
public static function content(array $parameters = [])
{
function visible_whitespace($s)
{
return '<pre>' . htmlspecialchars($s) . '</pre>';
}
$results = [];
if (!empty($_REQUEST['source'])) {
try {
$source = json_decode($_REQUEST['source'], true);
$trust_source = true;
$uid = local_user();
$push = false;
if (!$source) {
throw new \Exception('Failed to decode source JSON');
}
$formatted = json_encode($source, JSON_PRETTY_PRINT);
$results[] = [
'title' => DI::l10n()->t('Formatted'),
'content' => visible_whitespace(trim(var_export($formatted, true), "'")),
];
$results[] = [
'title' => DI::l10n()->t('Source'),
'content' => visible_whitespace(var_export($source, true))
];
$activity = JsonLD::compact($source);
if (!$activity) {
throw new \Exception('Failed to compact JSON');
}
$results[] = [
'title' => DI::l10n()->t('Activity'),
'content' => visible_whitespace(var_export($activity, true))
];
$type = JsonLD::fetchElement($activity, '@type');
if (!$type) {
throw new \Exception('Empty type');
}
if (!JsonLD::fetchElement($activity, 'as:object', '@id')) {
throw new \Exception('Empty object');
}
if (!JsonLD::fetchElement($activity, 'as:actor', '@id')) {
throw new \Exception('Empty actor');
}
// Don't trust the source if "actor" differs from "attributedTo". The content could be forged.
if ($trust_source && ($type == 'as:Create') && is_array($activity['as:object'])) {
$actor = JsonLD::fetchElement($activity, 'as:actor', '@id');
$attributed_to = JsonLD::fetchElement($activity['as:object'], 'as:attributedTo', '@id');
$trust_source = ($actor == $attributed_to);
if (!$trust_source) {
throw new \Exception('Not trusting actor: ' . $actor . '. It differs from attributedTo: ' . $attributed_to);
}
}
// $trust_source is called by reference and is set to true if the content was retrieved successfully
$object_data = ActivityPub\Receiver::prepareObjectData($activity, $uid, $push, $trust_source);
if (empty($object_data)) {
throw new \Exception('No object data found');
}
if (!$trust_source) {
throw new \Exception('No trust for activity type "' . $type . '", so we quit now.');
}
if (!empty($body) && empty($object_data['raw'])) {
$object_data['raw'] = $body;
}
// Internal flag for thread completion. See Processor.php
if (!empty($activity['thread-completion'])) {
$object_data['thread-completion'] = $activity['thread-completion'];
}
$results[] = [
'title' => DI::l10n()->t('Object data'),
'content' => visible_whitespace(var_export($object_data, true))
];
$item = ActivityPub\Processor::createItem($object_data);
$results[] = [
'title' => DI::l10n()->t('Result Item'),
'content' => visible_whitespace(var_export($item, true))
];
} catch (\Throwable $e) {
$results[] = [
'title' => DI::l10n()->t('Error'),
'content' => $e->getMessage(),
];
}
}
$tpl = Renderer::getMarkupTemplate('debug/activitypubconversion.tpl');
$o = Renderer::replaceMacros($tpl, [
'$source' => ['source', DI::l10n()->t('Source activity'), $_REQUEST['source'] ?? '', ''],
'$results' => $results
]);
return $o;
}
}

View file

@ -170,7 +170,8 @@ class Processor
$item = Item::selectFirst(['uri', 'uri-id', 'thr-parent', 'gravity'], ['uri' => $activity['id']]);
if (!DBA::isResult($item)) {
Logger::warning('No existing item, item will be created', ['uri' => $activity['id']]);
self::createItem($activity);
$item = self::createItem($activity);
self::postItem($activity, $item);
return;
}
@ -189,6 +190,7 @@ class Processor
* Prepares data for a message
*
* @param array $activity Activity array
* @return array Internal item
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
@ -216,7 +218,71 @@ class Processor
$item['diaspora_signed_text'] = $activity['diaspora:comment'] ?? '';
self::postItem($activity, $item);
/// @todo What to do with $activity['context']?
if (empty($activity['directmessage']) && ($item['gravity'] != GRAVITY_PARENT) && !Item::exists(['uri' => $item['thr-parent']])) {
Logger::info('Parent not found, message will be discarded.', ['thr-parent' => $item['thr-parent']]);
return [];
}
$item['network'] = Protocol::ACTIVITYPUB;
$item['author-link'] = $activity['author'];
$item['author-id'] = Contact::getIdForURL($activity['author'], 0, false);
$item['owner-link'] = $activity['actor'];
$item['owner-id'] = Contact::getIdForURL($activity['actor'], 0, false);
if (in_array(0, $activity['receiver']) && !empty($activity['unlisted'])) {
$item['private'] = Item::UNLISTED;
} elseif (in_array(0, $activity['receiver'])) {
$item['private'] = Item::PUBLIC;
} else {
$item['private'] = Item::PRIVATE;
}
if (!empty($activity['raw'])) {
$item['source'] = $activity['raw'];
$item['protocol'] = Conversation::PARCEL_ACTIVITYPUB;
$item['conversation-href'] = $activity['context'] ?? '';
$item['conversation-uri'] = $activity['conversation'] ?? '';
if (isset($activity['push'])) {
$item['direction'] = $activity['push'] ? Conversation::PUSH : Conversation::PULL;
}
}
$item['isForum'] = false;
if (!empty($activity['thread-completion'])) {
// Store the original actor in the "causer" fields to enable the check for ignored or blocked contacts
$item['causer-link'] = $item['owner-link'];
$item['causer-id'] = $item['owner-id'];
Logger::info('Ignoring actor because of thread completion.', ['actor' => $item['owner-link']]);
$item['owner-link'] = $item['author-link'];
$item['owner-id'] = $item['author-id'];
} else {
$actor = APContact::getByURL($item['owner-link'], false);
$item['isForum'] = ($actor['type'] == 'Group');
}
$item['uri'] = $activity['id'];
$item['created'] = DateTimeFormat::utc($activity['published']);
$item['edited'] = DateTimeFormat::utc($activity['updated']);
$guid = $activity['sc:identifier'] ?: self::getGUIDByURL($item['uri']);
$item['guid'] = $activity['diaspora:guid'] ?: $guid;
$item['uri-id'] = ItemURI::insert(['uri' => $item['uri'], 'guid' => $item['guid']]);
$item = self::processContent($activity, $item);
if (empty($item)) {
return [];
}
$item['plink'] = $activity['alternate-url'] ?? $item['uri'];
$item = self::constructAttachList($activity, $item);
return $item;
}
/**
@ -303,7 +369,7 @@ class Processor
*/
public static function createActivity($activity, $verb)
{
$item = [];
$item = self::createItem($activity);
$item['verb'] = $verb;
$item['thr-parent'] = $activity['object_id'];
$item['gravity'] = GRAVITY_ACTIVITY;
@ -446,72 +512,8 @@ class Processor
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
private static function postItem($activity, $item)
public static function postItem(array $activity, array $item)
{
/// @todo What to do with $activity['context']?
if (empty($activity['directmessage']) && ($item['gravity'] != GRAVITY_PARENT) && !Item::exists(['uri' => $item['thr-parent']])) {
Logger::info('Parent not found, message will be discarded.', ['thr-parent' => $item['thr-parent']]);
return;
}
$item['network'] = Protocol::ACTIVITYPUB;
$item['author-link'] = $activity['author'];
$item['author-id'] = Contact::getIdForURL($activity['author'], 0, false);
$item['owner-link'] = $activity['actor'];
$item['owner-id'] = Contact::getIdForURL($activity['actor'], 0, false);
if (in_array(0, $activity['receiver']) && !empty($activity['unlisted'])) {
$item['private'] = Item::UNLISTED;
} elseif (in_array(0, $activity['receiver'])) {
$item['private'] = Item::PUBLIC;
} else {
$item['private'] = Item::PRIVATE;
}
if (!empty($activity['raw'])) {
$item['source'] = $activity['raw'];
$item['protocol'] = Conversation::PARCEL_ACTIVITYPUB;
$item['conversation-href'] = $activity['context'] ?? '';
$item['conversation-uri'] = $activity['conversation'] ?? '';
if (isset($activity['push'])) {
$item['direction'] = $activity['push'] ? Conversation::PUSH : Conversation::PULL;
}
}
$isForum = false;
if (!empty($activity['thread-completion'])) {
// Store the original actor in the "causer" fields to enable the check for ignored or blocked contacts
$item['causer-link'] = $item['owner-link'];
$item['causer-id'] = $item['owner-id'];
Logger::info('Ignoring actor because of thread completion.', ['actor' => $item['owner-link']]);
$item['owner-link'] = $item['author-link'];
$item['owner-id'] = $item['author-id'];
} else {
$actor = APContact::getByURL($item['owner-link'], false);
$isForum = ($actor['type'] == 'Group');
}
$item['uri'] = $activity['id'];
$item['created'] = DateTimeFormat::utc($activity['published']);
$item['edited'] = DateTimeFormat::utc($activity['updated']);
$guid = $activity['sc:identifier'] ?: self::getGUIDByURL($item['uri']);
$item['guid'] = $activity['diaspora:guid'] ?: $guid;
$item['uri-id'] = ItemURI::insert(['uri' => $item['uri'], 'guid' => $item['guid']]);
$item = self::processContent($activity, $item);
if (empty($item)) {
return;
}
$item['plink'] = $activity['alternate-url'] ?? $item['uri'];
$item = self::constructAttachList($activity, $item);
$stored = false;
foreach ($activity['receiver'] as $receiver) {
@ -521,7 +523,7 @@ class Processor
$item['uid'] = $receiver;
if ($isForum) {
if ($item['isForum'] ?? false) {
$item['contact-id'] = Contact::getIdForURL($activity['actor'], $receiver, false);
} else {
$item['contact-id'] = Contact::getIdForURL($activity['author'], $receiver, false);
@ -539,7 +541,7 @@ class Processor
if (DI::pConfig()->get($receiver, 'system', 'accept_only_sharer', false) && ($receiver != 0) && ($item['gravity'] == GRAVITY_PARENT)) {
$skip = !Contact::isSharingByURL($activity['author'], $receiver);
if ($skip && (($activity['type'] == 'as:Announce') || $isForum)) {
if ($skip && (($activity['type'] == 'as:Announce') || ($item['isForum'] ?? false))) {
$skip = !Contact::isSharingByURL($activity['actor'], $receiver);
}

View file

@ -184,7 +184,7 @@ class Receiver
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
private static function prepareObjectData($activity, $uid, $push, &$trust_source)
public static function prepareObjectData($activity, $uid, $push, &$trust_source)
{
$actor = JsonLD::fetchElement($activity, 'as:actor', '@id');
if (empty($actor)) {
@ -227,6 +227,7 @@ class Receiver
if ($type == 'as:Announce') {
$trust_source = false;
}
$object_data = self::fetchObject($object_id, $activity['as:object'], $trust_source, $uid);
if (empty($object_data)) {
Logger::log("Object data couldn't be processed", Logger::DEBUG);
@ -337,7 +338,6 @@ class Receiver
if (!JsonLD::fetchElement($activity, 'as:actor', '@id')) {
Logger::log('Empty actor', Logger::DEBUG);
return;
}
// Don't trust the source if "actor" differs from "attributedTo". The content could be forged.
@ -374,7 +374,8 @@ class Receiver
switch ($type) {
case 'as:Create':
if (in_array($object_data['object_type'], self::CONTENT_TYPES)) {
ActivityPub\Processor::createItem($object_data);
$item = ActivityPub\Processor::createItem($object_data);
ActivityPub\Processor::postItem($object_data, $item);
}
break;
@ -391,7 +392,8 @@ class Receiver
// If this isn't set, then a single reshare appears on top. This is used for groups.
$object_data['thread-completion'] = ($profile['type'] != 'Group');
ActivityPub\Processor::createItem($object_data);
$item = ActivityPub\Processor::createItem($object_data);
ActivityPub\Processor::postItem($object_data, $item);
// Add the bottom reshare information only for persons
if ($profile['type'] != 'Group') {

View file

@ -107,6 +107,7 @@ return [
'/apps' => [Module\Apps::class, [R::GET]],
'/attach/{item:\d+}' => [Module\Attach::class, [R::GET]],
'/babel' => [Module\Debug\Babel::class, [R::GET, R::POST]],
'/debug/ap' => [Module\Debug\ActivityPubConversion::class, [R::GET, R::POST]],
'/bookmarklet' => [Module\Bookmarklet::class, [R::GET]],
'/community[/{content}[/{accounttype}]]' => [Module\Conversation\Community::class, [R::GET]],

View file

@ -0,0 +1,24 @@
<h2>ActivityPub Conversion</h2>
<form action="debug/ap" method="post" class="panel panel-default">
<div class="panel-body">
<div class="form-group">
{{include file="field_textarea.tpl" field=$source}}
</div>
<p><button type="submit" class="btn btn-primary">Submit</button></p>
</div>
</form>
{{if $results}}
<div class="babel-results">
{{foreach $results as $result}}
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">{{$result.title}}</h3>
</div>
<div class="panel-body">
{{$result.content nofilter}}
</div>
</div>
{{/foreach}}
</div>
{{/if}}