diff --git a/php/federator/api/v1/newcontent.php b/php/federator/api/v1/newcontent.php index c3164fc..8449f84 100644 --- a/php/federator/api/v1/newcontent.php +++ b/php/federator/api/v1/newcontent.php @@ -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( diff --git a/php/federator/data/activitypub/factory.php b/php/federator/data/activitypub/factory.php index 1dcd067..2902596 100644 --- a/php/federator/data/activitypub/factory.php +++ b/php/federator/data/activitypub/factory.php @@ -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; diff --git a/php/federator/dio/posts.php b/php/federator/dio/posts.php index 6b261aa..7c5331c 100644 --- a/php/federator/dio/posts.php +++ b/php/federator/dio/posts.php @@ -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,16 +131,33 @@ 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(); return $posts; diff --git a/plugins/federator/contentnation.php b/plugins/federator/contentnation.php index ecf7d9f..3189910 100644 --- a/plugins/federator/contentnation.php +++ b/plugins/federator/contentnation.php @@ -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 $array - * @return array - */ - 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; } /**