refactored ContentNation data-format

- also fixed issue where we didn't retrieve posts from the DB
- also fixed issue where posts from db were malformatted
- changed contentnation data-converter for comments & votes to support the new data-structure (more consistent, easier to read)
This commit is contained in:
Yannis Vogel 2025-05-21 20:06:43 +02:00
parent d355b5a7cd
commit ba88adcebd
No known key found for this signature in database
4 changed files with 109 additions and 93 deletions

View file

@ -89,6 +89,11 @@ class NewContent implements \Federator\Api\APIInterface
$input = is_string($_rawInput) ? json_decode($_rawInput, true) : null;
$dbh = $this->main->getDatabase();
$cache = $this->main->getCache();
$connector = $this->main->getConnector();
$config = $this->main->getConfig();
$domain = $config['generic']['externaldomain'];
if (!is_array($input)) {
@ -97,21 +102,16 @@ class NewContent implements \Federator\Api\APIInterface
}
if (isset($allHeaders['X-Sender'])) {
$newActivity = $this->main->getConnector()->jsonToActivity($input);
$newActivity = $connector->jsonToActivity($input);
} else {
error_log("NewContent::post No X-Sender header found");
return false;
}
if ($newActivity === false) {
error_log("NewContent::post couldn't create newActivity");
return false;
}
$dbh = $this->main->getDatabase();
$cache = $this->main->getCache();
$connector = $this->main->getConnector();
if (!isset($_user)) {
$user = $newActivity->getAActor(); // url of the sender https://contentnation.net/username
$user = str_replace(

View file

@ -117,7 +117,7 @@ class Factory
$return = new Common\Undo();
break;
default:
error_log("newActivityFromJson " . print_r($json, true));
error_log("newActivityFromJson unsupported type: " . print_r($json, true));
}
if (isset($return) && $return->fromJson($json) !== null) {
return $return;

View file

@ -43,6 +43,7 @@ class Posts
if ($posts === false) {
$posts = [];
}
echo "Found " . count($posts) . " posts in DB\n";
// Only override $min if we found posts in our DB
$remoteMin = $min;
@ -102,15 +103,15 @@ class Posts
*/
public static function getPostsFromDb($dbh, $userId, $min = null, $max = null)
{
$sql = 'SELECT id, user_id, type, object, published FROM posts WHERE user_id = ?';
$sql = 'SELECT `id`, `url`, `user_id`, `actor`, `type`, `object`, `to`, `cc`, `published` FROM posts WHERE user_id = ?';
$params = [$userId];
$types = 's';
if ($min !== null) {
if ($min !== null && $min !== "") {
$sql .= ' AND published >= ?';
$params[] = $min;
$types .= 's';
}
if ($max !== null) {
if ($max !== null && $max !== "") {
$sql .= ' AND published <= ?';
$params[] = $max;
$types .= 's';
@ -130,15 +131,32 @@ class Posts
}
$posts = [];
while ($row = $result->fetch_assoc()) {
if (!empty($row['object'])) {
$objectData = json_decode($row['object'], true);
if (is_array($objectData)) {
// Use the ActivityPub factory to create the APObject
$object = \Federator\Data\ActivityPub\Factory::newActivityFromJson($objectData);
if ($object !== false) {
$posts[] = $object;
if (isset($row['to']) && $row['to'] !== null) {
$row['to'] = json_decode($row['to'], true);
}
if (isset($row['cc']) && $row['cc'] !== null) {
$row['cc'] = json_decode($row['cc'], true);
}
if (isset($row['object']) && $row['object'] !== null) {
$decoded = json_decode($row['object'], true);
// Only use decoded value if it's an array/object
if (is_array($decoded)) {
$row['object'] = $decoded;
}
}
if (isset($row['published']) && $row['published'] !== null) {
// If it's numeric, keep as int. If it's a string, try to parse as ISO 8601.
if (is_numeric($row['published'])) {
$row['published'] = intval($row['published'], 10);
} else {
// Try to parse as datetime string
$timestamp = strtotime($row['published']);
$row['published'] = $timestamp !== false ? $timestamp : null;
}
}
$activity = \Federator\Data\ActivityPub\Factory::newActivityFromJson($row);
if ($activity !== false) {
$posts[] = $activity;
}
}
$stmt->close();

View file

@ -151,6 +151,7 @@ class ContentNation implements Connector
->addTo("https://www.w3.org/ns/activitystreams#Public")
->addCC('https://' . $domain . '/' . $userId . '/followers');
$create->setURL('https://' . $domain . '/' . $activity['profilename'] . '/' . $activity['name']);
$create->setID('https://' . $domain . '/' . $activity['profilename'] . '/' . $activity['id']);
$apArticle = new \Federator\Data\ActivityPub\Common\Article();
if (array_key_exists('tags', $activity)) {
foreach ($activity['tags'] as $tag) {
@ -207,16 +208,23 @@ class ContentNation implements Connector
$commentJson = $activity;
$commentJson['type'] = 'Note';
$commentJson['summary'] = $activity['subject'];
$commentJson['id'] = $activity['id'];
$commentJson['id'] = 'https://' . $domain . '/' . $activity['articleOwnerName'] . '/' . $activity['articleName'] . '#' . $activity['id'];
$note = \Federator\Data\ActivityPub\Factory::newFromJson($commentJson, "");
if ($note === null) {
error_log("ContentNation::getRemotePostsByUser couldn't create comment");
$comment = new \Federator\Data\ActivityPub\Common\Activity('Comment');
$create->setObject($comment);
$note = new \Federator\Data\ActivityPub\Common\Activity('Comment');
$create->setObject($note);
break;
}
$url = 'https://' . $domain . '/' . $activity['articleOwnerName'] . '/' . $activity['articleName'] . '#' . $note->getID();
$note->setID($commentJson['id']);
if (!isset($commentJson['parent']) || $commentJson['parent'] === null) {
$note->setInReplyTo('https://' . $domain . '/' . $activity['articleOwnerName'] . '/' . $activity['articleName']);
} elseif ($replyType === "comment") {
$note->setInReplyTo('https://' . $domain . '/' . $activity['articleOwnerName'] . '/' . $activity['articleName'] . "#" . $commentJson['parent']);
}
$url = 'https://' . $domain . '/' . $activity['articleOwnerName'] . '/' . $activity['articleName'] . '#' . $activity['id'];
$create->setURL($url);
$create->setID($url);
$create->setObject($note);
$posts[] = $create;
break; // Comment
@ -227,9 +235,9 @@ class ContentNation implements Connector
$like = new \Federator\Data\ActivityPub\Common\Activity($likeType);
$like->setAActor('https://' . $domain . '/' . $userId);
$like->setID($activity['id'])
->setPublished($activity['published'] ?? $activity['timestamp'])
->addTo("https://www.w3.org/ns/activitystreams#Public")
->addCC('https://' . $domain . '/' . $userId . '/followers');
->setPublished($activity['published'] ?? $activity['timestamp']);
// $like->addTo("https://www.w3.org/ns/activitystreams#Public")
// ->addCC('https://' . $domain . '/' . $userId . '/followers');
$like->setSummary(
$this->main->translate(
$activity['articlelang'],
@ -239,10 +247,8 @@ class ContentNation implements Connector
)
);
$objectUrl = 'https://' . $domain . '/' . $userId . '/' . $activity['articlename'];
if ($activity['comment'] !== '') {
$objectUrl .= "#" . $activity['comment'];
}
$like->setURL($objectUrl . '#' . $activity['id']);
$like->setID($objectUrl . '#' . $activity['id']);
$like->setObject($objectUrl);
$posts[] = $like;
break; // Vote
@ -364,6 +370,7 @@ class ContentNation implements Connector
*/
public function jsonToActivity(array $jsonData)
{
$returnActivity = false;
// Common fields for all activity types
$ap = [
'@context' => 'https://www.w3.org/ns/activitystreams',
@ -372,52 +379,65 @@ class ContentNation implements Connector
'actor' => $jsonData['actor'] ?? null,
];
$config = $this->main->getConfig();
$domain = $config['generic']['externaldomain'];
$ourUrl = 'https://' . $domain;
// Extract actorName as the last segment of the actor URL (after the last '/')
$actorUrl = $jsonData['actor'] ?? null;
$actorName = null;
$replaceUrl = null;
if (is_array($actorUrl)) {
$actorUrl = $actorUrl['id'];
$replaceUrl = $actorUrl->url ?? null;
}
if ($actorUrl !== null) {
$actorUrlParts = parse_url($actorUrl);
if (isset($actorUrlParts['path'])) {
$pathSegments = array_values(array_filter(explode('/', $actorUrlParts['path'])));
$actorName = end($pathSegments);
// Build replaceUrl as scheme://host if both are set
if (isset($actorUrlParts['scheme'], $actorUrlParts['host'])) {
$replaceUrl = $actorUrlParts['scheme'] . '://' . $actorUrlParts['host'];
} else {
$replaceUrl = $actorUrl;
}
} else {
$actorName = $actorUrl;
$replaceUrl = $actorUrl;
}
}
$ap['actor'] = $actorUrl;
$actorData = $jsonData['actor'] ?? null;
$actorName = $actorData['name'] ?? null;
$ap['actor'] = $ourUrl . '/' . $actorName;
// Handle specific fields based on the type
switch ($jsonData['type']) {
case 'comment':
$commentId = $jsonData['object']['id'] ?? null;
$articleName = $jsonData['object']['articleName'] ?? null;
$articleOwnerName = $jsonData['object']['articleOwnerName'] ?? null;
// Set Create-level fields
$ap['published'] = $jsonData['object']['published'] ?? null;
$ap['url'] = $jsonData['object']['url'] ?? null;
$ap['to'] = ['https://www.w3.org/ns/activitystreams#Public'];
$ap['cc'] = [$jsonData['related']['cc']['followers'] ?? null];
// Set object as Note with only required fields
$ap['id'] = $ourUrl . "/" . $articleOwnerName . "/" . $articleName . "#" . $commentId;
$ap['url'] = $ourUrl . "/" . $articleOwnerName . "/" . $articleName . "#" . $commentId;
$ap['type'] = 'Create';
$ap['object'] = [
'id' => $jsonData['object']['id'] ?? null,
'type' => 'Note',
'content' => $jsonData['object']['content'] ?? '',
'summary' => $jsonData['object']['summary'] ?? '',
'id' => $ourUrl . "/" . $articleOwnerName . "/" . $articleName . "#" . $commentId,
'content' => $jsonData['object']['content'] ?? null,
'summary' => $jsonData['object']['summary'] ?? null,
];
$ap['cc'] = ['https://www.w3.org/ns/activitystreams#Public'];
if (isset($jsonData['options'])) {
if (isset($jsonData['options']['informFollowers'])) {
if ($jsonData['options']['informFollowers'] === true) {
if ($actorName != $articleOwnerName) {
$ap['to'][] = $ourUrl . '/' . $articleOwnerName;
}
$ap['to'][] = $ourUrl . '/' . $actorName . '/followers';
}
}
}
$replyType = $jsonData['object']['inReplyTo']['type'] ?? null;
if ($replyType === "article") {
$ap['object']['inReplyTo'] = $ourUrl . "/" . $articleOwnerName . "/" . $articleName;
} elseif ($replyType === "comment") {
$ap['object']['inReplyTo'] = $ourUrl . "/" . $articleOwnerName . "/" . $articleName . "#" . $jsonData['object']['inReplyTo']['id'];
} else {
error_log("ContentNation::jsonToActivity unknown inReplyTo type: {$replyType}");
}
$returnActivity = \Federator\Data\ActivityPub\Factory::newActivityFromJson($ap);
$returnActivity->setID($ap['id']);
$returnActivity->setURL($ap['url']);
break;
case 'vote':
$ap['id'] .= "_$actorName";
$votedOn = $jsonData['object']['type'] ?? null;
$articleName = $jsonData['object']['articleName'] ?? null;
$articleOwnerName = $jsonData['object']['articleOwnerName'] ?? null;
$voteId = $jsonData['object']['id'] ?? null;
$ap['id'] = $ourUrl . '/' . $articleOwnerName . '/' . $articleName . '#' . $voteId;
$ap['url'] = $ourUrl . '/' . $articleOwnerName . '/' . $articleName . '#' . $voteId;
if (
isset($jsonData['vote']['type']) &&
strtolower($jsonData['vote']['type']) === 'undo'
@ -432,7 +452,10 @@ class ContentNation implements Connector
break;
}
$objectId = $jsonData['object']['id'] ?? null;
$objectId = $ourUrl . '/' . $articleOwnerName . '/' . $articleName;
if ($votedOn === "comment") {
$objectId .= '#' . $jsonData['object']['commentId'];
}
$ap['object'] = $objectId;
@ -462,43 +485,18 @@ class ContentNation implements Connector
'author' => $jsonData['object']['author'] ?? null,
];
} */
$returnActivity = \Federator\Data\ActivityPub\Factory::newActivityFromJson($ap);
$returnActivity->setID($ap['id']);
$returnActivity->setURL($ap['url']);
break;
default:
// Handle unsupported types or fallback to default behavior
throw new \InvalidArgumentException("Unsupported activity type: {$jsonData['type']}");
throw new \InvalidArgumentException("ContentNation::jsonToActivity Unsupported activity type: {$jsonData['type']}");
}
$config = $this->main->getConfig();
$domain = $config['generic']['externaldomain'];
/**
* Recursively replace strings in an array.
*
* @param string $search
* @param string $replace
* @param array<mixed, mixed> $array
* @return array<mixed, mixed>
*/
function array_str_replace_recursive(string $search, string $replace, array $array): array
{
foreach ($array as $key => $value) {
if (is_array($value)) {
$array[$key] = array_str_replace_recursive($search, $replace, $value);
} elseif (is_string($value)) {
$array[$key] = str_replace($search, $replace, $value);
}
}
return $array;
}
if (is_string($replaceUrl)) {
$ap = array_str_replace_recursive(
$replaceUrl,
'https://' . $domain,
$ap
);
}
return \Federator\Data\ActivityPub\Factory::newActivityFromJson($ap);
return $returnActivity;
}
/**