diff --git a/php/federator/api/v1/newcontent.php b/php/federator/api/v1/newcontent.php index 2ff5202..43edcf1 100644 --- a/php/federator/api/v1/newcontent.php +++ b/php/federator/api/v1/newcontent.php @@ -149,11 +149,6 @@ class NewContent implements \Federator\Api\APIInterface } if (str_ends_with($receiver, '/followers')) { - $actor = $newActivity->getAActor(); - if ($actor === null || !is_string($actor)) { - error_log("NewContent::post no actor found"); - continue; - } if ($posterName === null) { error_log("NewContent::post no username found"); @@ -177,6 +172,10 @@ class NewContent implements \Federator\Api\APIInterface $receiverName = basename((string) (parse_url($receiver, PHP_URL_PATH) ?? '')); $domain = parse_url($receiver, PHP_URL_HOST); if ($receiverName === null || $domain === null) { + if ($receiver === $posterName) { + $users[] = $receiver; + continue; + } error_log("NewContent::post no receiverName or domain found for receiver: " . $receiver); continue; } @@ -327,15 +326,15 @@ class NewContent implements \Federator\Api\APIInterface error_log("NewContent::postForUser: Failed to remove follower for user $user->id"); } break; - case 'Vote': - // Undo Vote (remove vote) + case 'Like': + case 'Dislike': if (method_exists($object, 'getObject')) { $targetId = $object->getObject(); if (is_string($targetId)) { // \Federator\DIO\Votes::removeVote($dbh, $user->id, $targetId); \Federator\DIO\Posts::deletePost($dbh, $targetId); } else { - error_log("NewContent::postForUser: Error in Undo Vote for user $user->id, targetId is not a string"); + error_log("NewContent::postForUser: Error in Undo Like/Dislike for user $user->id, targetId is not a string"); } } break; @@ -346,6 +345,15 @@ class NewContent implements \Federator\Api\APIInterface \Federator\DIO\Posts::deletePost($dbh, $noteId); } break; + case 'Article': + // Undo Article (remove article) + if (method_exists($object, 'getID')) { + $articleId = $object->getID(); + \Federator\DIO\Posts::deletePost($dbh, $articleId); + // also remove latest saved article-update + \Federator\DIO\Posts::deletePost($dbh, $articleId . '#update'); + } + break; } } else if (is_string($object)) { \Federator\DIO\Posts::deletePost($dbh, $object); @@ -355,25 +363,14 @@ class NewContent implements \Federator\Api\APIInterface break; case 'Like': - // Add Like + case 'Dislike': + // Add Like/Dislike $targetId = $newActivity->getObject(); if (is_string($targetId)) { // \Federator\DIO\Votes::addVote($dbh, $user->id, $targetId, 'like'); \Federator\DIO\Posts::savePost($dbh, $user->id, $newActivity); } else { - error_log("NewContent::postForUser: Error in Add Like for user $user->id, targetId is not a string"); - return false; - } - break; - - case 'Dislike': - // Add Dislike - $targetId = $newActivity->getObject(); - if (is_string($targetId)) { - // \Federator\DIO\Votes::addVote($dbh, $user->id, $targetId, 'dislike'); - \Federator\DIO\Posts::savePost($dbh, $user->id, $newActivity); - } else { - error_log("NewContent::postForUser: Error in Add Dislike for user $user->id, targetId is not a string"); + error_log("NewContent::postForUser: Error in Add Like/Dislike for user $user->id, targetId is not a string"); return false; } break; diff --git a/php/federator/data/activitypub/factory.php b/php/federator/data/activitypub/factory.php index 20a041e..1c92f1a 100644 --- a/php/federator/data/activitypub/factory.php +++ b/php/federator/data/activitypub/factory.php @@ -58,6 +58,12 @@ class Factory case 'Vote': $return = new Common\Vote(); break; + case 'Like': + $return = new Common\Like(); + break; + case 'Dislike': + $return = new Common\Dislike(); + break; case 'Inbox': $return = new Common\Inbox(); break; diff --git a/plugins/federator/contentnation.php b/plugins/federator/contentnation.php index a7a2277..9886b33 100644 --- a/plugins/federator/contentnation.php +++ b/plugins/federator/contentnation.php @@ -368,7 +368,7 @@ class ContentNation implements Connector * @param array $jsonData the json data from our platfrom * @return \Federator\Data\ActivityPub\Common\Activity|false */ - public function jsonToActivity(array $jsonData) + public function jsonToActivity($jsonData) { $returnActivity = false; // Common fields for all activity types @@ -390,190 +390,273 @@ class ContentNation implements Connector $ap['actor'] = $ourUrl . '/' . $actorName; - // Handle specific fields based on the type - switch ($jsonData['type']) { - case 'article': - $articleName = $jsonData['object']['name'] ?? null; - $articleOwnerName = $jsonData['object']['ownerName'] ?? null; - // Set Create-level fields - $updatedOn = $jsonData['object']['modified'] ?? null; - $originalPublished = $jsonData['object']['published'] ?? null; - $update = $updatedOn !== $originalPublished; - $ap['published'] = $updatedOn ?? $originalPublished; - $ap['id'] = $ourUrl . "/" . $articleOwnerName . "/" . $articleName; - $ap['url'] = $ourUrl . "/" . $articleOwnerName . "/" . $articleName; - $ap['type'] = $update ? 'Update' : 'Create'; - $ap['actor'] = $ourUrl . '/' . $actorName; - // Set Article-level fields - $ap['object'] = [ - 'type' => 'Article', - 'id' => $ourUrl . "/" . $articleOwnerName . "/" . $articleName, - 'name' => $jsonData['object']['title'] ?? null, - 'published' => $originalPublished, - 'summary' => $jsonData['object']['summary'] ?? null, - 'content' => $jsonData['object']['content'] ?? null, - 'attributedTo' => $ap['actor'], - 'url' => $ap['url'], - 'cc' => ['https://www.w3.org/ns/activitystreams#Public'], - ]; - if ($update) { - $ap['id'] .= '#update'; - $ap['url'] .= '#update'; - $ap['object']['updated'] = $updatedOn; - } - $ap['cc'] = ['https://www.w3.org/ns/activitystreams#Public']; - if (isset($jsonData['object']['tags'])) { - if (is_array($jsonData['object']['tags'])) { - foreach ($jsonData['object']['tags'] as $tag) { - $ap['object']['tags'][] = $tag; - } - } elseif (is_string($jsonData['object']['tags']) && $jsonData['object']['tags'] !== '') { - // If it's a single tag as a string, add it as a one-element array - $ap['object']['tags'][] = $jsonData['object']['tags']; - } - } - - if (isset($jsonData['options'])) { - if (isset($jsonData['options']['informFollowers'])) { - if ($jsonData['options']['informFollowers'] === true) { - $ap['to'][] = $ourUrl . '/' . $actorName . '/followers'; - $ap['object']['to'][] = $ourUrl . '/' . $actorName . '/followers'; - } - } - } - $returnActivity = \Federator\Data\ActivityPub\Factory::newActivityFromJson($ap); - if ($returnActivity === false) { - error_log("ContentNation::jsonToActivity couldn't create article"); - $returnActivity = new \Federator\Data\ActivityPub\Common\Activity('Create'); - } else { - $returnActivity->setID($ap['id']); - $returnActivity->setURL($ap['url']); - } - break; - - 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['id'] = $ourUrl . "/" . $articleOwnerName . "/" . $articleName . "#" . $commentId; - $ap['url'] = $ourUrl . "/" . $articleOwnerName . "/" . $articleName . "#" . $commentId; - $ap['type'] = 'Create'; - $ap['object'] = [ - 'type' => 'Note', - '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); - if ($returnActivity === false) { - error_log("ContentNation::jsonToActivity couldn't create comment"); - $returnActivity = new \Federator\Data\ActivityPub\Common\Activity('Create'); - } else { - $returnActivity->setID($ap['id']); - $returnActivity->setURL($ap['url']); - } - break; - - case 'vote': - $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' - ) { + if (isset($jsonData['type'])) { + switch ($jsonData['type']) { + case 'undo': $ap['type'] = 'Undo'; - } elseif ($jsonData['vote']['value'] == 1) { - $ap['type'] = 'Like'; - } elseif ($jsonData['vote']['value'] == 0) { - $ap['type'] = 'Dislike'; - } else { - error_log("ContentNation::jsonToActivity unknown vote type: {$jsonData['vote']['type']} and value: {$jsonData['vote']['value']}"); - break; - } - - $objectId = $ourUrl . '/' . $articleOwnerName . '/' . $articleName; - if ($votedOn === "comment") { - $objectId .= '#' . $jsonData['object']['commentId']; - } - - $ap['object'] = $objectId; - - if ($ap['type'] === "Undo") { - $ap['object'] = $ap['id']; - } - - /* if ($ap['type'] === 'Undo') { - $ap['object'] = [ - 'id' => $objectId, - 'type' => 'Vote', - ]; - } else if ( - isset($jsonData['object']['type']) && - $jsonData['object']['type'] === 'Article' - ) { - $ap['object'] = [ - 'id' => $objectId, - 'type' => $jsonData['object']['type'], - 'name' => $jsonData['object']['name'] ?? null, - 'author' => $jsonData['object']['author'] ?? null, - ]; - } else if ($jsonData['object']['type'] === 'Comment') { - $ap['object'] = [ - 'id' => $objectId, - 'type' => 'Note', - 'author' => $jsonData['object']['author'] ?? null, - ]; - } */ - - $returnActivity = \Federator\Data\ActivityPub\Factory::newActivityFromJson($ap); - if ($returnActivity === false) { - error_log("ContentNation::jsonToActivity couldn't create vote"); - if ($ap['type'] === "Like") { - $returnActivity = new \Federator\Data\ActivityPub\Common\Like(); - } elseif ($ap['type'] === "Dislike") { - $returnActivity = new \Federator\Data\ActivityPub\Common\Dislike(); + $ap['actor'] = $ourUrl . '/' . $actorName; + $objectType = $jsonData['object']['type'] ?? null; + if ($objectType === "article") { + $articleName = $jsonData['object']['name'] ?? null; + $ownerName = $jsonData['object']['ownerName'] ?? null; + $ap['id'] = $ourUrl . '/' . $ownerName . '/' . $articleName . '/undo'; + $ap['object'] = self::generateObjectJson($ourUrl, $jsonData); + } elseif ($objectType === "comment") { + $articleName = $jsonData['object']['articleName'] ?? null; + $ownerName = $jsonData['object']['articleOwnerName'] ?? null; + $commentId = $jsonData['object']['id'] ?? null; + $ap['id'] = $ourUrl . '/' . $ownerName . '/' . $articleName . '#' . $commentId . '/undo'; + $ap['object'] = self::generateObjectJson($ourUrl, $jsonData); + } elseif ($objectType === "vote") { + $id = $jsonData['object']['id'] ?? null; + $articleName = $jsonData['object']['articleName'] ?? null; + $articleOwnerName = $jsonData['object']['articleOwnerName'] ?? null; + $ap['id'] = $ourUrl . '/' . $articleOwnerName . '/' . $articleName . '#' . $id . '/undo'; + $ap['published'] = $jsonData['object']['published'] ?? null; + $ap['actor'] = $ourUrl . '/' . $actorName; + $ap['object']['id'] = $ourUrl . '/' . $articleOwnerName . '/' . $articleName . '#' . $id; + $ap['object']['url'] = $ourUrl . '/' . $articleOwnerName . '/' . $articleName . '#' . $id; + $ap['object']['actor'] = $ourUrl . '/' . $actorName; + if ($jsonData['object']['vote']['value'] == 1) { + $ap['object']['type'] = 'Like'; + } elseif ($jsonData['object']['vote']['value'] == 0) { + $ap['object']['type'] = 'Dislike'; + } else { + error_log("ContentNation::jsonToActivity unknown vote value: {$jsonData['object']['vote']['value']}"); + break; + } + $ap['object']['object'] = self::generateObjectJson($ourUrl, $jsonData); } else { - $returnActivity = new \Federator\Data\ActivityPub\Common\Undo(); + error_log("ContentNation::jsonToActivity unknown undo type: {$objectType}"); + break; } - } else { - $returnActivity->setID($ap['id']); - $returnActivity->setURL($ap['url']); - } - break; + $returnActivity = \Federator\Data\ActivityPub\Factory::newActivityFromJson($ap); + if ($returnActivity === false) { + error_log("ContentNation::jsonToActivity couldn't create undo"); + $returnActivity = new \Federator\Data\ActivityPub\Common\Undo(); + } else { + $returnActivity->setID($ap['id']); + $returnActivity->setURL($ap['id']); + } + break; + default: + // Handle unsupported types or fallback to default behavior + throw new \InvalidArgumentException("ContentNation::jsonToActivity Unsupported type: {$jsonData['type']}"); + } + } else { + // Handle specific fields based on the type + switch ($jsonData['object']['type']) { + case 'article': + $articleName = $jsonData['object']['name'] ?? null; + $articleOwnerName = $jsonData['object']['ownerName'] ?? null; + // Set Create-level fields + $updatedOn = $jsonData['object']['modified'] ?? null; + $originalPublished = $jsonData['object']['published'] ?? null; + $update = $updatedOn !== $originalPublished; + $ap['published'] = $updatedOn ?? $originalPublished; + $ap['id'] = $ourUrl . "/" . $articleOwnerName . "/" . $articleName; + $ap['url'] = $ourUrl . "/" . $articleOwnerName . "/" . $articleName; + $ap['type'] = $update ? 'Update' : 'Create'; + $ap['actor'] = $ourUrl . '/' . $actorName; + if ($update) { + $ap['id'] .= '#update'; + $ap['url'] .= '#update'; + } + $ap['cc'] = ['https://www.w3.org/ns/activitystreams#Public']; - default: - // Handle unsupported types or fallback to default behavior - throw new \InvalidArgumentException("ContentNation::jsonToActivity Unsupported activity type: {$jsonData['type']}"); + if (isset($jsonData['options'])) { + if (isset($jsonData['options']['informFollowers'])) { + if ($jsonData['options']['informFollowers'] === true) { + $ap['to'][] = $ourUrl . '/' . $actorName . '/followers'; + } + } + } + $ap['object'] = self::generateObjectJson($ourUrl, $jsonData); + $returnActivity = \Federator\Data\ActivityPub\Factory::newActivityFromJson($ap); + if ($returnActivity === false) { + error_log("ContentNation::jsonToActivity couldn't create article"); + $returnActivity = new \Federator\Data\ActivityPub\Common\Activity('Create'); + } else { + $returnActivity->setID($ap['id']); + $returnActivity->setURL($ap['url']); + } + break; + + 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['actor'] = $ourUrl . '/' . $actorName; + $ap['id'] = $ourUrl . "/" . $articleOwnerName . "/" . $articleName . "#" . $commentId; + $ap['url'] = $ourUrl . "/" . $articleOwnerName . "/" . $articleName . "#" . $commentId; + $ap['type'] = 'Create'; + $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'; + } + } + } + $ap['object'] = self::generateObjectJson($ourUrl, $jsonData); + $returnActivity = \Federator\Data\ActivityPub\Factory::newActivityFromJson($ap); + if ($returnActivity === false) { + error_log("ContentNation::jsonToActivity couldn't create comment"); + $returnActivity = new \Federator\Data\ActivityPub\Common\Activity('Create'); + } else { + $returnActivity->setID($ap['id']); + $returnActivity->setURL($ap['url']); + } + break; + + case 'vote': + $articleName = $jsonData['object']['articleName'] ?? null; + $articleOwnerName = $jsonData['object']['articleOwnerName'] ?? null; + $voteId = $jsonData['object']['id'] ?? null; + $ap['published'] = $jsonData['object']['published'] ?? null; + $ap['actor'] = $ourUrl . '/' . $actorName; + $ap['id'] = $ourUrl . '/' . $articleOwnerName . '/' . $articleName . '#' . $voteId; + $ap['url'] = $ourUrl . '/' . $articleOwnerName . '/' . $articleName . '#' . $voteId; + if ($jsonData['object']['vote']['value'] == 1) { + $ap['type'] = 'Like'; + } elseif ($jsonData['object']['vote']['value'] == 0) { + $ap['type'] = 'Dislike'; + } else { + error_log("ContentNation::jsonToActivity unknown vote value: {$jsonData['object']['vote']['value']}"); + break; + } + + $ap['object'] = self::generateObjectJson($ourUrl, $jsonData); + $returnActivity = \Federator\Data\ActivityPub\Factory::newActivityFromJson($ap); + if ($returnActivity === false) { + error_log("ContentNation::jsonToActivity couldn't create vote"); + if ($ap['type'] === "Like") { + $returnActivity = new \Federator\Data\ActivityPub\Common\Like(); + } elseif ($ap['type'] === "Dislike") { + $returnActivity = new \Federator\Data\ActivityPub\Common\Dislike(); + } else { + $returnActivity = new \Federator\Data\ActivityPub\Common\Undo(); + } + } else { + $returnActivity->setID($ap['id']); + $returnActivity->setURL($ap['url']); + } + break; + + default: + // Handle unsupported types or fallback to default behavior + throw new \InvalidArgumentException("ContentNation::jsonToActivity Unsupported object type: {$jsonData['type']}"); + } } return $returnActivity; } + /** + * Convert jsonData to Activity format + * + * @param string $ourUrl the url of our instance + * @param array $jsonData the json data from our platfrom + * @return array|string|false the json object data or false + */ + private static function generateObjectJson($ourUrl, $jsonData) + { + $objectType = $jsonData['object']['type'] ?? null; + $actorData = $jsonData['actor'] ?? null; + $actorName = $actorData['name'] ?? null; + + $actorUrl = $ourUrl . '/' . $actorName; + + if ($objectType === "article") { + $articleName = $jsonData['object']['name'] ?? null; + $articleOwnerName = $jsonData['object']['ownerName'] ?? null; + $updatedOn = $jsonData['object']['modified'] ?? null; + $originalPublished = $jsonData['object']['published'] ?? null; + $update = $updatedOn !== $originalPublished; + $returnJson = [ + 'type' => 'Article', + 'id' => $ourUrl . "/" . $articleOwnerName . "/" . $articleName, + 'name' => $jsonData['object']['title'] ?? null, + 'published' => $originalPublished, + 'summary' => $jsonData['object']['summary'] ?? null, + 'content' => $jsonData['object']['content'] ?? null, + 'attributedTo' => $actorUrl, + 'url' => $ourUrl . "/" . $articleOwnerName . "/" . $articleName, + 'cc' => ['https://www.w3.org/ns/activitystreams#Public'], + ]; + if ($update) { + $returnJson['updated'] = $updatedOn; + } + if (isset($jsonData['object']['tags'])) { + if (is_array($jsonData['object']['tags'])) { + foreach ($jsonData['object']['tags'] as $tag) { + $returnJson['tags'][] = $tag; + } + } elseif (is_string($jsonData['object']['tags']) && $jsonData['object']['tags'] !== '') { + // If it's a single tag as a string, add it as a one-element array + $returnJson['tags'][] = $jsonData['object']['tags']; + } + } + + if (isset($jsonData['options'])) { + if (isset($jsonData['options']['informFollowers'])) { + if ($jsonData['options']['informFollowers'] === true) { + $returnJson['to'][] = $ourUrl . '/' . $actorName . '/followers'; + } + } + } + } elseif ($objectType === "comment") { + $commentId = $jsonData['object']['id'] ?? null; + $articleName = $jsonData['object']['articleName'] ?? null; + $articleOwnerName = $jsonData['object']['articleOwnerName'] ?? null; + $returnJson = [ + 'type' => 'Note', + 'id' => $ourUrl . "/" . $articleOwnerName . "/" . $articleName . "#" . $commentId, + 'url' => $ourUrl . "/" . $articleOwnerName . "/" . $articleName . "#" . $commentId, + 'attributedTo' => $actorUrl, + 'content' => $jsonData['object']['content'] ?? null, + 'summary' => $jsonData['object']['summary'] ?? null, + 'published' => $jsonData['object']['published'] ?? null, + 'cc' => ['https://www.w3.org/ns/activitystreams#Public'], + ]; + if (isset($jsonData['options'])) { + if (isset($jsonData['options']['informFollowers'])) { + if ($jsonData['options']['informFollowers'] === true) { + $returnJson['to'][] = $ourUrl . '/' . $actorName . '/followers'; + } + } + } + $replyType = $jsonData['object']['inReplyTo']['type'] ?? null; + if ($replyType === "article") { + $returnJson['object']['inReplyTo'] = $ourUrl . "/" . $articleOwnerName . "/" . $articleName; + } elseif ($replyType === "comment") { + $returnJson['object']['inReplyTo'] = $ourUrl . "/" . $articleOwnerName . "/" . $articleName . "#" . $jsonData['object']['inReplyTo']['id']; + } else { + error_log("ContentNation::generateObjectJson for comment - unknown inReplyTo type: {$replyType}"); + } + } elseif ($objectType === "vote") { + $votedOn = $jsonData['object']['type'] ?? null; + $articleName = $jsonData['object']['articleName'] ?? null; + $articleOwnerName = $jsonData['object']['articleOwnerName'] ?? null; + $objectId = $ourUrl . '/' . $articleOwnerName . '/' . $articleName; + if ($votedOn === "comment") { + $objectId .= '#' . $jsonData['object']['commentId']; + } + + $returnJson = $objectId; + } else { + error_log("ContentNation::generateObjectJson unknown object type: {$objectType}"); + return false; + } + + return $returnJson; + } + /** * check if the headers include a valid signature *