From 10dec5ebd3ca2525864592d38d166271876b0ed2 Mon Sep 17 00:00:00 2001 From: Yannis Vogel Date: Mon, 26 May 2025 16:17:23 +0200 Subject: [PATCH] conditionally convert article to note - fix bug in which inReplyTo isn't correctly set from contentnation-comments - added dio/article which has functions to convert article to note based on new file - added formatsupport.json to manage special cases (f.e. includes which servers can handle articles) --- formatsupport.json | 8 +++ htdocs/index.html | 3 +- php/federator/api/fedusers/inbox.php | 12 ++-- php/federator/api/v1/newcontent.php | 54 ++++++++++++--- .../data/activitypub/common/apobject.php | 21 ++++++ php/federator/dio/article.php | 68 +++++++++++++++++++ php/federator/dio/followers.php | 8 +-- php/federator/dio/posts.php | 48 +++++++++++++ plugins/federator/contentnation.php | 4 +- 9 files changed, 203 insertions(+), 23 deletions(-) create mode 100644 formatsupport.json create mode 100644 php/federator/dio/article.php diff --git a/formatsupport.json b/formatsupport.json new file mode 100644 index 0000000..3535021 --- /dev/null +++ b/formatsupport.json @@ -0,0 +1,8 @@ +{ + "activitypub": { + "article": [ + "localhost", + "writefreely.org" + ] + } +} \ No newline at end of file diff --git a/htdocs/index.html b/htdocs/index.html index 61ab824..4c145eb 100644 --- a/htdocs/index.html +++ b/htdocs/index.html @@ -62,7 +62,8 @@ const headers = { ...(session ? { "X-Session": session } : {}), - ...(profile ? { "X-Profile": profile } : {}) + ...(profile ? { "X-Profile": profile } : {}), + "HTTP_HOST": "localhost", }; fetch("http://localhost/" + targetLink, { diff --git a/php/federator/api/fedusers/inbox.php b/php/federator/api/fedusers/inbox.php index 9e5dd71..9cb03e1 100644 --- a/php/federator/api/fedusers/inbox.php +++ b/php/federator/api/fedusers/inbox.php @@ -241,6 +241,11 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface return false; } + $atPos = strpos($_recipientId, '@'); + if ($atPos !== false) { + $_recipientId = substr($_recipientId, 0, $atPos); + } + // get recipient $recipient = \Federator\DIO\User::getUserByName( $dbh, @@ -265,11 +270,8 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface switch ($type) { case 'Follow': - $success = false; - $actor = $inboxActivity->getAActor(); - if ($actor !== '') { - $success = \Federator\DIO\Followers::addExternalFollow($dbh, $inboxActivity->getID(), $user->id, $recipient->id); - } + $success = \Federator\DIO\Followers::addExternalFollow($dbh, $inboxActivity->getID(), $user->id, $recipient->id); + if ($success === false) { error_log("Inbox::postForUser: Failed to add follower for user $user->id"); } diff --git a/php/federator/api/v1/newcontent.php b/php/federator/api/v1/newcontent.php index e5f63f7..124bb87 100644 --- a/php/federator/api/v1/newcontent.php +++ b/php/federator/api/v1/newcontent.php @@ -346,25 +346,37 @@ class NewContent implements \Federator\Api\APIInterface break; case 'Note': // Undo Note (remove note) - if (method_exists($object, 'getID')) { - $noteId = $object->getID(); - \Federator\DIO\Posts::deletePost($dbh, $noteId); - } + $noteId = $object->getID(); + \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'); + $idPart = strrchr($recipient->id, '@'); + if ($idPart === false) { + error_log("NewContent::postForUser Error in Undo Article. $recipient->id, recipient ID is not valid"); + return false; + } else { + $targetUrl = ltrim($idPart, '@'); + + if ($object instanceof \Federator\Data\ActivityPub\Common\Article) { + $object = \Federator\DIO\Article::conditionalConvertToNote($object, $targetUrl); + $newActivity->setObject($object); + } else { + error_log("NewContent::postForUser Error in Undo Article for recipient $recipient->id, object is not an Article"); + } } + $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); } else { - error_log("NewContent::postForUser Error in Undo for user $user->id, object is not a string or object"); + error_log("NewContent::postForUser Error in Undo for recipient $recipient->id, object is not a string or object"); } break; @@ -376,7 +388,7 @@ class NewContent implements \Federator\Api\APIInterface // \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/Dislike for user $user->id, targetId is not a string"); + error_log("NewContent::postForUser Error in Add Like/Dislike for recipient $recipient->id, targetId is not a string"); return false; } break; @@ -387,7 +399,27 @@ class NewContent implements \Federator\Api\APIInterface if (is_object($object)) { switch ($object->getType()) { case 'Note': + \Federator\DIO\Posts::savePost($dbh, $user->id, $newActivity); + + break; case 'Article': + $idPart = strrchr($recipient->id, '@'); + if ($idPart === false) { + error_log("NewContent::postForUser Error in Create/Update Article. $recipient->id, recipient ID is not valid"); + return false; + } else { + $targetUrl = ltrim($idPart, '@'); + + if ($object instanceof \Federator\Data\ActivityPub\Common\Article) { + $object = \Federator\DIO\Article::conditionalConvertToNote($object, $targetUrl); + $newActivity->setObject($object); + } else { + error_log("NewContent::postForUser Error in Create/Update Article for recipient $recipient->id, object is not an Article"); + } + } + \Federator\DIO\Posts::savePost($dbh, $user->id, $newActivity); + + break; default: \Federator\DIO\Posts::savePost($dbh, $user->id, $newActivity); break; diff --git a/php/federator/data/activitypub/common/apobject.php b/php/federator/data/activitypub/common/apobject.php index ee6f311..d04f3e7 100644 --- a/php/federator/data/activitypub/common/apobject.php +++ b/php/federator/data/activitypub/common/apobject.php @@ -376,6 +376,17 @@ class APObject implements \JsonSerializable $this->summary = $summary; return $this; } + + /** + * get summary + * + * @return string summary + */ + public function getSummary() + { + return $this->summary; + } + /** * set type * @@ -459,6 +470,16 @@ class APObject implements \JsonSerializable return $this; } + /** + * get name + * + * @return string name + */ + public function getName() : string + { + return $this->name; + } + /** * add Image * diff --git a/php/federator/dio/article.php b/php/federator/dio/article.php new file mode 100644 index 0000000..a82c344 --- /dev/null +++ b/php/federator/dio/article.php @@ -0,0 +1,68 @@ +setId($article->getId()) + ->setURL($article->getURL()); + $note->setContent($article->getContent()); + $note->setSummary($article->getSummary()); + $note->setPublished($article->getPublished()); + $note->setName($article->getName()); + $note->setAttributedTo($article->getAttributedTo()); + foreach ($article->getTo() as $to) { + $note->addTo($to); + } + foreach ($article->getCc() as $cc) { + $note->addCc($cc); + } + return $note; + } + + /** Conditionally convert article to a note + * + * @param \Federator\Data\ActivityPub\Common\Article $article + * @param string $targetUrl + * The target URL for the activity (e.g. mastodon.social) + * @return \Federator\Data\ActivityPub\Common\Note|\Federator\Data\ActivityPub\Common\Article + * The generated note on success, false on failure + */ + public static function conditionalConvertToNote($article, $targetUrl) + { + $supportFile = file_get_contents(PROJECT_ROOT . '/formatsupport.json'); + if ($supportFile === false) { + error_log("Article::conditionalConvertToNote Failed to read support file for article conversion."); + return $article; // Fallback to original article if file read fails + } + $supportlist = json_decode($supportFile, true); + + if ( + !isset($supportlist['activitypub']['article']) || + !is_array($supportlist['activitypub']['article']) || + !in_array($targetUrl, $supportlist['activitypub']['article'], true) + ) { + return self::convertToNote($article); // Articles are not supported for this target + } + return $article; // Articles are supported, return as is + } +} diff --git a/php/federator/dio/followers.php b/php/federator/dio/followers.php index ce0161e..188a0c8 100644 --- a/php/federator/dio/followers.php +++ b/php/federator/dio/followers.php @@ -393,11 +393,11 @@ class Followers * * @param \mysqli $dbh database handle * @param string $followId the follow ID to use (should be an external url) - * @param string $sourceUser source user id + * @param string $sourceUserId source user id * @param string $targetUserId target user id * @return boolean true on success, false on failure */ - public static function addExternalFollow($dbh, $followId, $sourceUser, $targetUserId) + public static function addExternalFollow($dbh, $followId, $sourceUserId, $targetUserId) { // Check if we already follow this user $sql = 'select id from follows where source_user = ? and target_user = ?'; @@ -405,7 +405,7 @@ class Followers if ($stmt === false) { throw new \Federator\Exceptions\ServerError("Followers::addExternalFollow Failed to prepare statement"); } - $stmt->bind_param("ss", $sourceUser, $targetUserId); + $stmt->bind_param("ss", $sourceUserId, $targetUserId); $foundId = 0; $ret = $stmt->bind_result($foundId); $stmt->execute(); @@ -423,7 +423,7 @@ class Followers if ($stmt === false) { throw new \Federator\Exceptions\ServerError("Followers::addExternalFollow Failed to prepare insert statement"); } - $stmt->bind_param("sss", $followId, $sourceUser, $targetUserId); + $stmt->bind_param("sss", $followId, $sourceUserId, $targetUserId); $stmt->execute(); $stmt->close(); return true; diff --git a/php/federator/dio/posts.php b/php/federator/dio/posts.php index fb3adb9..fbd33d7 100644 --- a/php/federator/dio/posts.php +++ b/php/federator/dio/posts.php @@ -78,11 +78,59 @@ class Posts } } + $originUrl = 'localhost'; + if (isset($_SERVER['HTTP_HOST'])) { + $originUrl = $_SERVER['HTTP_HOST']; // origin of our request - e.g. mastodon + } elseif (isset($_SERVER['HTTP_ORIGIN'])) { + $origin = $_SERVER['HTTP_ORIGIN']; + $parsed = parse_url($origin); + if (isset($parsed) && isset($parsed['host'])) { + $parsedHost = $parsed['host']; + if (is_string($parsedHost) && $parsedHost !== "") { + $originUrl = $parsedHost; + } + } + } + if (!isset($originUrl) || $originUrl === "") { + $originUrl = 'localhost'; // Fallback to localhost if no origin is set + } + // save posts to DB foreach ($posts as $post) { if ($post->getID() !== "") { self::savePost($dbh, $userid, $post); } + switch (strtolower($post->getType())) { + case 'undo': + $object = $post->getObject(); + if (is_object($object)) { + if (strtolower($object->getType()) === 'article') { + if ($object instanceof \Federator\Data\ActivityPub\Common\Article) { + $object = \Federator\DIO\Article::conditionalConvertToNote($object, $originUrl); + $post->setObject($object); + } + } + } + + break; + + case 'create': + case 'update': + $object = $post->getObject(); + if (is_object($object)) { + if (strtolower($object->getType()) === 'article') { + if ($object instanceof \Federator\Data\ActivityPub\Common\Article) { + $object = \Federator\DIO\Article::conditionalConvertToNote($object, $originUrl); + $post->setObject($object); + } + } + } + + break; + + default: + break; + } } if ($cache !== null) { diff --git a/plugins/federator/contentnation.php b/plugins/federator/contentnation.php index 199b834..1db8e6f 100644 --- a/plugins/federator/contentnation.php +++ b/plugins/federator/contentnation.php @@ -635,9 +635,9 @@ class ContentNation implements Connector } $replyType = $jsonData['object']['inReplyTo']['type'] ?? null; if ($replyType === "article") { - $returnJson['object']['inReplyTo'] = $ourUrl . "/" . $articleOwnerName . "/" . $articleName; + $returnJson['inReplyTo'] = $ourUrl . "/" . $articleOwnerName . "/" . $articleName; } elseif ($replyType === "comment") { - $returnJson['object']['inReplyTo'] = $ourUrl . "/" . $articleOwnerName . "/" . $articleName . "#" . $jsonData['object']['inReplyTo']['id']; + $returnJson['inReplyTo'] = $ourUrl . "/" . $articleOwnerName . "/" . $articleName . "#" . $jsonData['object']['inReplyTo']['id']; } else { error_log("ContentNation::generateObjectJson for comment - unknown inReplyTo type: {$replyType}"); }