Merge pull request #8904 from MrPetovan/task/ap-conversion-admin-module
Add new admin debug module for ActivityPub
This commit is contained in:
commit
41141965fc
|
@ -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'],
|
||||
]],
|
||||
];
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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') {
|
||||
|
|
|
@ -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]],
|
||||
|
|
|
@ -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}}
|
Loading…
Reference in New Issue
Block a user