From 62cfd6ef0d1837c80d07fac27cf8f318bef024f2 Mon Sep 17 00:00:00 2001 From: Yannis Vogel Date: Thu, 22 May 2025 20:44:37 +0200 Subject: [PATCH] initial support for articles from CN - fixed how To and CC field (recipients) are handled in general - fixed posts in database - improved some error exceptions and prevented early breaks through try-catch blocks - we now support CN-articles on our newcontent endpoint, with create and update calls --- php/federator/api/fedusers/inbox.php | 132 +++++++++++---- php/federator/api/v1/newcontent.php | 151 +++++++++++------- .../data/activitypub/common/Update.php | 45 ++++++ .../data/activitypub/common/apobject.php | 4 +- php/federator/data/activitypub/factory.php | 3 + php/federator/dio/feduser.php | 32 ++-- php/federator/dio/followers.php | 65 +++++--- php/federator/dio/posts.php | 1 - php/federator/dio/user.php | 12 +- php/federator/jobs/inboxJob.php | 3 +- php/federator/jobs/newContentJob.php | 3 +- plugins/federator/contentnation.php | 97 +++++++++-- 12 files changed, 398 insertions(+), 150 deletions(-) create mode 100644 php/federator/data/activitypub/common/Update.php diff --git a/php/federator/api/fedusers/inbox.php b/php/federator/api/fedusers/inbox.php index 869dcf3..e951add 100644 --- a/php/federator/api/fedusers/inbox.php +++ b/php/federator/api/fedusers/inbox.php @@ -43,7 +43,7 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface /** * handle post call * - * @param string|null $_user user to add data to inbox + * @param string|null $_user user to add data to inbox @unused-param * @return string|false response */ public function post($_user) @@ -59,6 +59,12 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface $activity = is_string($_rawInput) ? json_decode($_rawInput, true) : null; + $dbh = $this->main->getDatabase(); + $cache = $this->main->getCache(); + $connector = $this->main->getConnector(); + + $config = $this->main->getConfig(); + if (!is_array($activity)) { throw new \Federator\Exceptions\ServerError("Inbox::post Input wasn't of type array"); } @@ -68,33 +74,33 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface if ($inboxActivity === false) { throw new \Federator\Exceptions\ServerError("Inbox::post couldn't create inboxActivity"); } + $user = $inboxActivity->getAActor(); // url of the sender https://contentnation.net/username + $username = basename((string) (parse_url($user, PHP_URL_PATH) ?? '')); + $domain = parse_url($user, PHP_URL_HOST); - $rootDir = PROJECT_ROOT . '/'; + $users = []; - // Shared inbox - if (!isset($_user)) { - // Save the raw input and parsed JSON to a file for inspection - file_put_contents( - $rootDir . 'logs/inbox.log', - date('Y-m-d H:i:s') . ": ==== POST Inbox Activity ====\n" . json_encode($inboxActivity, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) . "\n\n", - FILE_APPEND - ); - } + $receivers = array_merge($inboxActivity->getTo(), $inboxActivity->getCC()); - $sendTo = $inboxActivity->getCC(); - if ($inboxActivity->getType() === 'Undo') { // for undo the object holds the proper cc + // For Undo, the object may hold the proper to/cc + if ($inboxActivity->getType() === 'Undo') { $object = $inboxActivity->getObject(); if ($object !== null && is_object($object)) { - $sendTo = $object->getCC(); + $receivers = array_merge($object->getTo(), $object->getCC()); } } - $users = []; - $dbh = $this->main->getDatabase(); - $cache = $this->main->getCache(); - $connector = $this->main->getConnector(); + // Filter out the public address and keep only actual URLs + $receivers = array_filter($receivers, static function (mixed $receiver): bool { + return is_string($receiver) + && $receiver !== 'https://www.w3.org/ns/activitystreams#Public' + && (filter_var($receiver, FILTER_VALIDATE_URL) !== false); + }); - foreach ($sendTo as $receiver) { + if (!in_array($username, $receivers, true)) { + $receivers[] = $username; + } + foreach ($receivers as $receiver) { if ($receiver === '' || !is_string($receiver)) { continue; } @@ -110,25 +116,68 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface $username = basename((string) (parse_url($actor, PHP_URL_PATH) ?? '')); $domain = parse_url($actor, PHP_URL_HOST); if ($username === null || $domain === null) { - error_log("Inbox::post no username or domain found"); + error_log("Inbox::post no username or domain found for recipient: $receiver"); + continue; + } + try { + $followers = \Federator\DIO\Followers::getFollowersByFedUser($dbh, $connector, $cache, $username . '@' . $domain); + } catch (\Throwable $e) { + error_log("Inbox::post get followers for user: " . $username . '@' . $domain . ". Exception: " . $e->getMessage()); continue; } - $followers = \Federator\DIO\Followers::getFollowersByFedUser($dbh, $connector, $cache, $username . '@' . $domain); if (is_array($followers)) { $users = array_merge($users, array_column($followers, 'id')); } + } else { + $ourDomain = $config['generic']['externaldomain']; + // check if receiver is an actor url from our domain + if (!str_contains($receiver, $ourDomain)) { + continue; + } + $receiverName = basename((string) (parse_url($receiver, PHP_URL_PATH) ?? '')); + $domain = parse_url($receiver, PHP_URL_HOST); + if ($receiverName === null || $domain === null) { + error_log("Inbox::post no receiverName or domain found for receiver: " . $receiver); + continue; + } + $receiver = $receiverName . '@' . $domain; + try { + $user = \Federator\DIO\User::getUserByName( + $dbh, + $receiver, + $connector, + $cache + ); + } catch (\Throwable $e) { + error_log("Inbox::post get user by name: " . $receiver . ". Exception: " . $e->getMessage()); + continue; + } + if ($user === null || $user->id === null) { + error_log("Inbox::post couldn't find user: $receiver"); + continue; + } + $users[] = $user->id; } } - if ($_user !== false && !in_array($_user, $users, true)) { - $users[] = $_user; + + if (empty($users)) { // todo remove after proper implementation, debugging for now + $rootDir = PROJECT_ROOT . '/'; + // Save the raw input and parsed JSON to a file for inspection + file_put_contents( + $rootDir . 'logs/inbox.log', + date('Y-m-d H:i:s') . ": ==== POST Inbox Activity ====\n" . json_encode($inboxActivity, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) . "\n\n", + FILE_APPEND + ); } - foreach ($users as $user) { + + foreach ($users as $receiver) { if (!isset($user)) { continue; } $token = \Resque::enqueue('inbox', 'Federator\\Jobs\\InboxJob', [ - 'user' => $user, + 'user' => $username . '@' . $domain, + 'recipientId' => $receiver, 'activity' => $inboxActivity->toObject(), ]); error_log("Inbox::post enqueued job for user: $user with token: $token"); @@ -142,32 +191,46 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface * @param \mysqli $dbh database handle * @param \Federator\Connector\Connector $connector connector to use * @param \Federator\Cache\Cache|null $cache optional caching service - * @param string $_user user to add data to inbox + * @param string $_user user that triggered the post + * @param string $_recipientId recipient of the post * @param \Federator\Data\ActivityPub\Common\Activity $inboxActivity the activity that we received * @return boolean response */ - public static function postForUser($dbh, $connector, $cache, $_user, $inboxActivity) + public static function postForUser($dbh, $connector, $cache, $_user, $_recipientId, $inboxActivity) { if (!isset($_user)) { error_log("Inbox::postForUser no user given"); return false; } - $user = \Federator\DIO\User::getUserByName( + // get sender + $user = \Federator\DIO\FedUser::getUserByName( $dbh, $_user, - $connector, $cache ); if ($user === null || $user->id === null) { - throw new \Federator\Exceptions\ServerError("Inbox::postForUser couldn't find user: $_user"); + error_log("Inbox::postForUser couldn't find user: $_user"); + return false; + } + + // get recipient + $recipient = \Federator\DIO\User::getUserByName( + $dbh, + $_recipientId, + $connector, + $cache + ); + if ($recipient === null || $recipient->id === null) { + error_log("Inbox::postForUser couldn't find user: $_recipientId"); + return false; } $rootDir = PROJECT_ROOT . '/'; // Save the raw input and parsed JSON to a file for inspection file_put_contents( - $rootDir . 'logs/inbox_' . $_user . '.log', - date('Y-m-d H:i:s') . ": ==== POST " . $_user . " Inbox Activity ====\n" . json_encode($inboxActivity, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) . "\n\n", + $rootDir . 'logs/inbox_' . $recipient->id . '.log', + date('Y-m-d H:i:s') . ": ==== POST " . $recipient->id . " Inbox Activity ====\n" . json_encode($inboxActivity, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) . "\n\n", FILE_APPEND ); @@ -195,12 +258,15 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface $object = $inboxActivity->getObject(); if (is_string($object)) { \Federator\DIO\Posts::deletePost($dbh, $object); + } elseif (is_object($object)) { + $objectId = $object->getID(); + \Federator\DIO\Posts::deletePost($dbh, $objectId); } break; case 'Undo': $object = $inboxActivity->getObject(); - if (is_object($object) && method_exists($object, 'getType')) { + if (is_object($object)) { switch ($object->getType()) { case 'Follow': $success = false; diff --git a/php/federator/api/v1/newcontent.php b/php/federator/api/v1/newcontent.php index 8449f84..2ff5202 100644 --- a/php/federator/api/v1/newcontent.php +++ b/php/federator/api/v1/newcontent.php @@ -77,7 +77,6 @@ class NewContent implements \Federator\Api\APIInterface public function post($_user) { $_rawInput = file_get_contents('php://input'); - $allHeaders = getallheaders(); try { $this->main->checkSignature($allHeaders); @@ -111,27 +110,94 @@ class NewContent implements \Federator\Api\APIInterface error_log("NewContent::post couldn't create newActivity"); return false; } - if (!isset($_user)) { $user = $newActivity->getAActor(); // url of the sender https://contentnation.net/username - $user = str_replace( + $posterName = str_replace( $domain, '', $user ); // retrieve only the last part of the url } else { - $user = $dbh->real_escape_string($_user); + $posterName = $dbh->real_escape_string($_user); } $users = []; - if ($newActivity->getType() === 'Create') { - $followers = $this->fetchAllFollowers($dbh, $connector, $cache, $user); - } - if (!empty($followers)) { - $users = array_merge($users, $followers); + $receivers = array_merge($newActivity->getTo(), $newActivity->getCC()); + + // For Undo, the object may hold the proper to/cc + if ($newActivity->getType() === 'Undo') { + $object = $newActivity->getObject(); + if ($object !== null && is_object($object)) { + $receivers = array_merge($object->getTo(), $object->getCC()); + } } + // Filter out the public address and keep only actual URLs + $receivers = array_filter($receivers, static function (mixed $receiver): bool { + return is_string($receiver) + && $receiver !== 'https://www.w3.org/ns/activitystreams#Public' + && (filter_var($receiver, FILTER_VALIDATE_URL) !== false); + }); + + if (!in_array($posterName, $receivers, true)) { + $receivers[] = $posterName; + } + foreach ($receivers as $receiver) { + if ($receiver === '' || !is_string($receiver)) { + continue; + } + + 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"); + continue; + } + try { + $followers = \Federator\DIO\Followers::getFollowersByUser($dbh, $posterName, $connector, $cache); + } catch (\Throwable $e) { + error_log("NewContent::post get followers for user: " . $posterName . ". Exception: " . $e->getMessage()); + continue; + } + + if (is_array($followers)) { + $users = array_merge($users, array_column($followers, 'id')); + } + } else { + // check if receiver is an actor url and not from our domain + if (str_contains($receiver, $domain)) { + continue; + } + $receiverName = basename((string) (parse_url($receiver, PHP_URL_PATH) ?? '')); + $domain = parse_url($receiver, PHP_URL_HOST); + if ($receiverName === null || $domain === null) { + error_log("NewContent::post no receiverName or domain found for receiver: " . $receiver); + continue; + } + $receiver = $receiverName . '@' . $domain; + try { + $user = \Federator\DIO\FedUser::getUserByName( + $dbh, + $receiver, + $cache + ); + } catch (\Throwable $e) { + error_log("NewContent::post get user by name: " . $receiver . ". Exception: " . $e->getMessage()); + continue; + } + if ($user === null || $user->id === null) { + error_log("NewContent::post couldn't find user: $receiver"); + continue; + } + $users[] = $user->id; + } + } if (empty($users)) { // todo remove after proper implementation, debugging for now $rootDir = PROJECT_ROOT . '/'; // Save the raw input and parsed JSON to a file for inspection @@ -141,20 +207,18 @@ class NewContent implements \Federator\Api\APIInterface FILE_APPEND ); } - if ($_user !== false && !in_array($_user, $users, true)) { - $users[] = $_user; - } - foreach ($users as $user) { - if (!isset($user)) { + foreach ($users as $receiver) { + if (!isset($receiver)) { continue; } $token = \Resque::enqueue('inbox', 'Federator\\Jobs\\NewContentJob', [ - 'user' => $user, + 'user' => $posterName, + 'recipientId' => $receiver, 'activity' => $newActivity->toObject(), ]); - error_log("Inbox::post enqueued job for user: $user with token: $token"); + error_log("Inbox::post enqueued job for receiver: $receiver with token: $token"); } return json_encode($newActivity, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT); @@ -170,17 +234,18 @@ class NewContent implements \Federator\Api\APIInterface * @param \Federator\Cache\Cache|null $cache * optional caching service * @param string $_user user that triggered the post + * @param string $_recipientId recipient of the post * @param \Federator\Data\ActivityPub\Common\Activity $newActivity the activity that we received * @return boolean response */ - public static function postForUser($dbh, $connector, $cache, $_user, $newActivity) + public static function postForUser($dbh, $connector, $cache, $_user, $_recipientId, $newActivity) { if (!isset($_user)) { error_log("NewContent::postForUser no user given"); return false; } - // get user + // get sender $user = \Federator\DIO\User::getUserByName( $dbh, $_user, @@ -192,11 +257,22 @@ class NewContent implements \Federator\Api\APIInterface return false; } + // get recipient + $recipient = \Federator\DIO\FedUser::getUserByName( + $dbh, + $_recipientId, + $cache + ); + if ($recipient === null || $recipient->id === null) { + error_log("NewContent::postForUser couldn't find user: $_recipientId"); + return false; + } + $rootDir = PROJECT_ROOT . '/'; // Save the raw input and parsed JSON to a file for inspection file_put_contents( - $rootDir . 'logs/newcontent_' . $_user . '.log', - date('Y-m-d H:i:s') . ": ==== POST " . $_user . " NewContent Activity ====\n" . json_encode($newActivity, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) . "\n\n", + $rootDir . 'logs/newcontent_' . $recipient->id . '.log', + date('Y-m-d H:i:s') . ": ==== POST " . $recipient->id . " NewContent Activity ====\n" . json_encode($newActivity, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) . "\n\n", FILE_APPEND ); @@ -303,6 +379,7 @@ class NewContent implements \Federator\Api\APIInterface break; case 'Create': + case 'Update': $object = $newActivity->getObject(); if (is_object($object)) { switch ($object->getType()) { @@ -324,40 +401,6 @@ class NewContent implements \Federator\Api\APIInterface return true; } - /** - * fetch all followers from url and return the ones that belong to our server - * - * @param \mysqli $dbh - * database handle - * @param \Federator\Connector\Connector $connector - * connector to fetch use with - * @param \Federator\Cache\Cache|null $cache - * optional caching service - * @param string $userId The id of the user - * @return string[] the names of the followers that are hosted on our server - */ - private static function fetchAllFollowers($dbh, $connector, $cache, string $userId): array - { - if (empty($userId)) { - return []; - } - - $users = []; - - $apFollowers = \Federator\DIO\Followers::getFollowersByUser( - $dbh, - $userId, - $connector, - cache: $cache, - ); - - foreach ($apFollowers as $follower) { - $users[] = $follower->id; - } - - return $users; - } - /** * get internal represenation as json string * @return string json string or html diff --git a/php/federator/data/activitypub/common/Update.php b/php/federator/data/activitypub/common/Update.php new file mode 100644 index 0000000..5585357 --- /dev/null +++ b/php/federator/data/activitypub/common/Update.php @@ -0,0 +1,45 @@ + + */ + public function toObject() + { + $return = parent::toObject(); + $return['type'] = 'Update'; + // overwrite id from url + if ($this->getURL() !== '') { + $return['id'] = $this->getURL(); + } + return $return; + } + + /** + * create object from json + * + * @param array $json input json + * @return bool true on success + */ + public function fromJson($json) + { + return parent::fromJson($json); + } +} diff --git a/php/federator/data/activitypub/common/apobject.php b/php/federator/data/activitypub/common/apobject.php index 2ed32b3..ee6f311 100644 --- a/php/federator/data/activitypub/common/apobject.php +++ b/php/federator/data/activitypub/common/apobject.php @@ -647,7 +647,7 @@ class APObject implements \JsonSerializable if (array_key_exists('duration', $json)) { try { $this->duration = new \DateInterval($json['duration']); - } catch (\Exception $unused_e) { + } catch (\Throwable $unused_e) { error_log("error parsing duration ". $json['duration']); } } @@ -875,7 +875,7 @@ class APObject implements \JsonSerializable $return['tag'] = $tags; } if ($this->updated > 0) { - $return['updated'] = gmdate("Y-m-d\TH:i:S\Z", $this->updated); + $return['updated'] = gmdate("Y-m-d\TH:i:s\Z", $this->updated); } if ($this->url !== '') { $return['url'] = $this->url; diff --git a/php/federator/data/activitypub/factory.php b/php/federator/data/activitypub/factory.php index 2902596..20a041e 100644 --- a/php/federator/data/activitypub/factory.php +++ b/php/federator/data/activitypub/factory.php @@ -116,6 +116,9 @@ class Factory case 'Undo': $return = new Common\Undo(); break; + case 'Update': + $return = new Common\Update(); + break; default: error_log("newActivityFromJson unsupported type: " . print_r($json, true)); } diff --git a/php/federator/dio/feduser.php b/php/federator/dio/feduser.php index 65cd46b..a3f8557 100644 --- a/php/federator/dio/feduser.php +++ b/php/federator/dio/feduser.php @@ -26,7 +26,7 @@ class FedUser $sql = 'select unix_timestamp(`validuntil`) from fedusers where id=?'; $stmt = $dbh->prepare($sql); if ($stmt === false) { - throw new \Federator\Exceptions\ServerError(); + throw new \Federator\Exceptions\ServerError("FedUser::addLocalUser Failed to prepare statement"); } $stmt->bind_param("s", $_user); $validuntil = 0; @@ -42,7 +42,7 @@ class FedUser $sql .= ' values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, now() + interval 1 day)'; $stmt = $dbh->prepare($sql); if ($stmt === false) { - throw new \Federator\Exceptions\ServerError(); + throw new \Federator\Exceptions\ServerError("FedUser::addLocalUser Failed to prepare create statement"); } $stmt->bind_param( "ssssssssssss", @@ -66,7 +66,7 @@ class FedUser $sql .= ' where id=?'; $stmt = $dbh->prepare($sql); if ($stmt === false) { - throw new \Federator\Exceptions\ServerError(); + throw new \Federator\Exceptions\ServerError("FedUser::extendUser Failed to prepare update statement"); } $stmt->bind_param( "ssssssssssss", @@ -106,7 +106,7 @@ class FedUser $sql = 'select id,unix_timestamp(`validuntil`) from fedusers where id=?'; $stmt = $dbh->prepare($sql); if ($stmt === false) { - throw new \Federator\Exceptions\ServerError(); + throw new \Federator\Exceptions\ServerError("FedUser::extendUser Failed to prepare statement"); } $stmt->bind_param("s", $_user); $validuntil = 0; @@ -147,12 +147,12 @@ class FedUser return $user; } // check our db - $sql = 'select id, url, name, publickey, summary, type, inboxurl, sharedinboxurl, followersurl,'; - $sql .= ' followingurl,publickeyid,outboxurl'; - $sql .= ' from fedusers where id=? and validuntil>=now()'; + $sql = 'select `id`, `url`, `name`, `publickey`, `summary`, `type`, `inboxurl`, `sharedinboxurl`, `followersurl`,'; + $sql .= ' `followingurl`, `publickeyid`, `outboxurl`'; + $sql .= ' from fedusers where `id`=? and `validuntil`>=now()'; $stmt = $dbh->prepare($sql); if ($stmt === false) { - throw new \Federator\Exceptions\ServerError(); + throw new \Federator\Exceptions\ServerError("FedUser::getUserByName Failed to prepare statement"); } $stmt->bind_param("s", $_name); $user = new \Federator\Data\FedUser(); @@ -184,11 +184,11 @@ class FedUser $headers = ['Accept: application/activity+json']; [$response, $info] = \Federator\Main::getFromRemote($remoteURL, $headers); if ($info['http_code'] != 200) { - throw new \Federator\Exceptions\ServerError(); + throw new \Federator\Exceptions\ServerError("FedUser::getUserByName Failed to fetch webfinger for " . $_name); } $r = json_decode($response, true); if ($r === false || $r === null || !is_array($r)) { - throw new \Federator\Exceptions\ServerError(); + throw new \Federator\Exceptions\ServerError("FedUser::getUserByName Failed to decode webfinger for " . $_name); } // get the webwinger user url and fetch the user if (isset($r['links'])) { @@ -200,17 +200,17 @@ class FedUser } } if (!isset($remoteURL)) { - throw new \Federator\Exceptions\ServerError(); + throw new \Federator\Exceptions\ServerError("FedUser::getUserByName Failed to find self link in webfinger for " . $_name); } // fetch the user $headers = ['Accept: application/activity+json']; [$response, $info] = \Federator\Main::getFromRemote($remoteURL, $headers); if ($info['http_code'] != 200) { - throw new \Federator\Exceptions\ServerError(); + throw new \Federator\Exceptions\ServerError("FedUser::getUserByName Failed to fetch user from remoteUrl for " . $_name); } $r = json_decode($response, true); if ($r === false || $r === null || !is_array($r)) { - throw new \Federator\Exceptions\ServerError(); + throw new \Federator\Exceptions\ServerError("FedUser::getUserByName Failed to decode user for " . $_name); } $r['publicKeyId'] = $r['publicKey']['id']; $r['publicKey'] = $r['publicKey']['publicKeyPem']; @@ -222,20 +222,20 @@ class FedUser $r['actorURL'] = $remoteURL; $data = json_encode($r); if ($data === false) { - throw new \Federator\Exceptions\ServerError(); + throw new \Federator\Exceptions\ServerError("FedUser::getUserByName Failed to encode userdata " . $_name); } $user = \Federator\Data\FedUser::createFromJson($data); } } if ($cache !== null && $user !== false) { - if ($user->id === null && $user->actorURL !== null) { + if ($user->id !== null && $user->actorURL !== null) { self::addLocalUser($dbh, $user, $_name); } $cache->saveRemoteFedUserByName($_name, $user); } if ($user === false) { - throw new \Federator\Exceptions\ServerError(); + throw new \Federator\Exceptions\ServerError("FedUser::getUserByName User not found"); } return $user; } diff --git a/php/federator/dio/followers.php b/php/federator/dio/followers.php index 3038894..0379e24 100644 --- a/php/federator/dio/followers.php +++ b/php/federator/dio/followers.php @@ -39,7 +39,7 @@ class Followers $sql = 'select source_user from follows where target_user = ?'; $stmt = $dbh->prepare($sql); if ($stmt === false) { - throw new \Federator\Exceptions\ServerError(); + throw new \Federator\Exceptions\ServerError("Followers::getFollowersByUser Failed to prepare statement"); } $stmt->bind_param("s", $id); $stmt->execute(); @@ -50,11 +50,16 @@ class Followers } $stmt->close(); foreach ($followerIds as $followerId) { - $user = \Federator\DIO\FedUser::getUserByName( - $dbh, - $followerId, - $cache, - ); + try { + $user = \Federator\DIO\FedUser::getUserByName( + $dbh, + $followerId, + $cache, + ); + } catch (\Throwable $e) { + error_log("Followers::getFollowersByUser Exception: " . $e->getMessage()); + continue; // Skip this user if an exception occurs + } if ($user !== false && $user->id !== null) { $followers[] = $user; } @@ -67,7 +72,7 @@ class Followers $followers = []; } } - // save posts to DB + // save followers to cache if ($cache !== null) { $cache->saveRemoteFollowersOfUser($id, $followers); } @@ -100,7 +105,7 @@ class Followers $sql = 'select target_user from follows where source_user = ?'; $stmt = $dbh->prepare($sql); if ($stmt === false) { - throw new \Federator\Exceptions\ServerError(); + throw new \Federator\Exceptions\ServerError("Followers::getFollowingForUser Failed to prepare statement"); } $stmt->bind_param("s", $id); $stmt->execute(); @@ -111,11 +116,16 @@ class Followers } $stmt->close(); foreach ($followingIds as $followingId) { - $user = \Federator\DIO\FedUser::getUserByName( - $dbh, - $followingId, - $cache, - ); + try { + $user = \Federator\DIO\FedUser::getUserByName( + $dbh, + $followingId, + $cache, + ); + } catch (\Throwable $e) { + error_log("Followers::getFollowingForUser Exception: " . $e->getMessage()); + continue; // Skip this user if an exception occurs + } if ($user !== false && $user->id !== null) { $following[] = $user; } @@ -156,7 +166,7 @@ class Followers $sql = 'select source_user from follows where target_user = ?'; $stmt = $dbh->prepare($sql); if ($stmt === false) { - throw new \Federator\Exceptions\ServerError(); + throw new \Federator\Exceptions\ServerError("Followers::getFollowersByFedUser Failed to prepare statement"); } $stmt->bind_param("s", $id); $stmt->execute(); @@ -166,12 +176,17 @@ class Followers $followerIds[] = $sourceUser; } foreach ($followerIds as $followerId) { - $user = \Federator\DIO\User::getUserByName( - $dbh, - $followerId, - $connector, - $cache - ); + try { + $user = \Federator\DIO\User::getUserByName( + $dbh, + $followerId, + $connector, + $cache + ); + } catch (\Throwable $e) { + error_log("Followers::getFollowersByFedUser Exception: " . $e->getMessage()); + continue; // Skip this user if an exception occurs + } if ($user !== false && $user->id !== null) { $followers[] = $user; } @@ -193,7 +208,7 @@ class Followers public static function sendFollowRequest($dbh, $connector, $cache, $_user, $_targetUser, $host) { if ($dbh === false) { - throw new \Federator\Exceptions\ServerError(); + throw new \Federator\Exceptions\ServerError("Followers::sendFollowRequest Failed to get database handle"); } $user = \Federator\DIO\User::getUserByName( $dbh, @@ -326,7 +341,7 @@ class Followers $sql = 'select id from follows where source_user = ? and target_user = ?'; $stmt = $dbh->prepare($sql); if ($stmt === false) { - throw new \Federator\Exceptions\ServerError(); + throw new \Federator\Exceptions\ServerError("Followers::addFollow Failed to prepare statement"); } $stmt->bind_param("ss", $sourceUser, $targetUserId); $foundId = 0; @@ -349,7 +364,7 @@ class Followers $sql = 'select id from follows where id = ?'; $stmt = $dbh->prepare($sql); if ($stmt === false) { - throw new \Federator\Exceptions\ServerError(); + throw new \Federator\Exceptions\ServerError("Followers::addFollow Failed to prepare id-check statement"); } $stmt->bind_param("s", $idurl); $foundId = 0; @@ -365,7 +380,7 @@ class Followers $sql = 'insert into follows (id, source_user, target_user, created_at) values (?, ?, ?, NOW())'; $stmt = $dbh->prepare($sql); if ($stmt === false) { - throw new \Federator\Exceptions\ServerError(); + throw new \Federator\Exceptions\ServerError("Followers::addFollow Failed to prepare insert statement"); } $stmt->bind_param("sss", $idurl, $sourceUser, $targetUserId); $stmt->execute(); @@ -386,7 +401,7 @@ class Followers $sql = 'delete from follows where source_user = ? and target_user = ?'; $stmt = $dbh->prepare($sql); if ($stmt === false) { - throw new \Federator\Exceptions\ServerError(); + throw new \Federator\Exceptions\ServerError("Followers::removeFollow Failed to prepare statement"); } $stmt->bind_param("ss", $sourceUser, $targetUserId); $stmt->execute(); diff --git a/php/federator/dio/posts.php b/php/federator/dio/posts.php index 7c5331c..fb3adb9 100644 --- a/php/federator/dio/posts.php +++ b/php/federator/dio/posts.php @@ -43,7 +43,6 @@ 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; diff --git a/php/federator/dio/user.php b/php/federator/dio/user.php index 845e8bd..f8bbf4b 100644 --- a/php/federator/dio/user.php +++ b/php/federator/dio/user.php @@ -26,7 +26,7 @@ class User $sql = 'select unix_timestamp(`validuntil`) from users where id=?'; $stmt = $dbh->prepare($sql); if ($stmt === false) { - throw new \Federator\Exceptions\ServerError(); + throw new \Federator\Exceptions\ServerError("User::addLocalUser Failed to prepare statement"); } $stmt->bind_param("s", $_user); $validuntil = 0; @@ -50,7 +50,7 @@ class User $sql .= ' values (?, ?, ?, ?, now() + interval 1 day, ?, ?, ?, ?, ?, ?, ?, ?)'; $stmt = $dbh->prepare($sql); if ($stmt === false) { - throw new \Federator\Exceptions\ServerError(); + throw new \Federator\Exceptions\ServerError("User::addLocalUser Failed to prepare create statement"); } $registered = gmdate('Y-m-d H:i:s', $user->registered); $stmt->bind_param( @@ -74,7 +74,7 @@ class User $sql .= ' iconmediatype=?, iconurl=?, imagemediatype=?, imageurl=? where id=?'; $stmt = $dbh->prepare($sql); if ($stmt === false) { - throw new \Federator\Exceptions\ServerError(); + throw new \Federator\Exceptions\ServerError("User::addLocalUser Failed to prepare update statement"); } $registered = gmdate('Y-m-d H:i:s', $user->registered); $stmt->bind_param( @@ -110,7 +110,7 @@ class User $sql = "select rsaprivate from users where id=?"; $stmt = $dbh->prepare($sql); if ($stmt === false) { - throw new \Federator\Exceptions\ServerError(); + throw new \Federator\Exceptions\ServerError("User::getrsaprivate Failed to prepare statement"); } $stmt->bind_param("s", $_user); $ret = $stmt->bind_result($rsaPrivateKey); @@ -136,7 +136,7 @@ class User $sql = 'select id,unix_timestamp(`validuntil`) from users where id=?'; $stmt = $dbh->prepare($sql); if ($stmt === false) { - throw new \Federator\Exceptions\ServerError(); + throw new \Federator\Exceptions\ServerError("User::extendUser Failed to prepare statement"); } $stmt->bind_param("s", $_user); $validuntil = 0; @@ -183,7 +183,7 @@ class User $sql .= 'iconmediatype,iconurl,imagemediatype,imageurl from users where id=? and validuntil>=now()'; $stmt = $dbh->prepare($sql); if ($stmt === false) { - throw new \Federator\Exceptions\ServerError(); + throw new \Federator\Exceptions\ServerError("User::getUserByName Failed to prepare statement"); } $stmt->bind_param("s", $_name); $user = new \Federator\Data\User(); diff --git a/php/federator/jobs/inboxJob.php b/php/federator/jobs/inboxJob.php index 6452d3b..134a820 100644 --- a/php/federator/jobs/inboxJob.php +++ b/php/federator/jobs/inboxJob.php @@ -54,6 +54,7 @@ class InboxJob extends \Federator\Api { error_log("InboxJob: Starting inbox job"); $user = $this->args['user']; + $recipientId = $this->args['recipientId']; $activity = $this->args['activity']; $inboxActivity = \Federator\Data\ActivityPub\Factory::newActivityFromJson($activity); @@ -63,7 +64,7 @@ class InboxJob extends \Federator\Api return false; } - \Federator\Api\FedUsers\Inbox::postForUser($this->dbh, $this->connector, $this->cache, $user, $inboxActivity); + \Federator\Api\FedUsers\Inbox::postForUser($this->dbh, $this->connector, $this->cache, $user, $recipientId, $inboxActivity); return true; } } \ No newline at end of file diff --git a/php/federator/jobs/newContentJob.php b/php/federator/jobs/newContentJob.php index 4f7b16a..3c26a35 100644 --- a/php/federator/jobs/newContentJob.php +++ b/php/federator/jobs/newContentJob.php @@ -54,6 +54,7 @@ class NewContentJob extends \Federator\Api { error_log("NewContentJob: Starting inbox job"); $user = $this->args['user']; + $recipientId = $this->args['recipientId']; $activity = $this->args['activity']; $activity = \Federator\Data\ActivityPub\Factory::newActivityFromJson($activity); @@ -63,7 +64,7 @@ class NewContentJob extends \Federator\Api return false; } - \Federator\Api\V1\NewContent::postForUser($this->dbh, $this->connector, $this->cache, $user, $activity); + \Federator\Api\V1\NewContent::postForUser($this->dbh, $this->connector, $this->cache, $user, $recipientId, $activity); return true; } } \ No newline at end of file diff --git a/plugins/federator/contentnation.php b/plugins/federator/contentnation.php index 3189910..de85112 100644 --- a/plugins/federator/contentnation.php +++ b/plugins/federator/contentnation.php @@ -148,8 +148,8 @@ class ContentNation implements Connector $create->setAActor('https://' . $domain . '/' . $userId); $create->setID($activity['id']) ->setPublished($activity['published'] ?? $activity['timestamp']) - ->addTo("https://www.w3.org/ns/activitystreams#Public") - ->addCC('https://' . $domain . '/' . $userId . '/followers'); + ->addTo('https://' . $domain . '/' . $userId . '/followers') + ->addCC("https://www.w3.org/ns/activitystreams#Public"); $create->setURL('https://' . $domain . '/' . $activity['profilename'] . '/' . $activity['name']); $create->setID('https://' . $domain . '/' . $activity['profilename'] . '/' . $activity['id']); $apArticle = new \Federator\Data\ActivityPub\Common\Article(); @@ -203,8 +203,8 @@ class ContentNation implements Connector $create->setAActor('https://' . $domain . '/' . $userId); $create->setID($activity['id']) ->setPublished($activity['published'] ?? $activity['timestamp']) - ->addTo("https://www.w3.org/ns/activitystreams#Public") - ->addCC('https://' . $domain . '/' . $userId . '/followers'); + ->addTo('https://' . $domain . '/' . $userId . '/followers') + ->addCC("https://www.w3.org/ns/activitystreams#Public"); $commentJson = $activity; $commentJson['type'] = 'Note'; $commentJson['summary'] = $activity['subject']; @@ -219,7 +219,7 @@ class ContentNation implements Connector $note->setID($commentJson['id']); if (!isset($commentJson['parent']) || $commentJson['parent'] === null) { $note->setInReplyTo('https://' . $domain . '/' . $activity['articleOwnerName'] . '/' . $activity['articleName']); - } elseif ($replyType === "comment") { + } else { $note->setInReplyTo('https://' . $domain . '/' . $activity['articleOwnerName'] . '/' . $activity['articleName'] . "#" . $commentJson['parent']); } $url = 'https://' . $domain . '/' . $activity['articleOwnerName'] . '/' . $activity['articleName'] . '#' . $activity['id']; @@ -237,7 +237,7 @@ class ContentNation implements Connector $like->setID($activity['id']) ->setPublished($activity['published'] ?? $activity['timestamp']); // $like->addTo("https://www.w3.org/ns/activitystreams#Public") - // ->addCC('https://' . $domain . '/' . $userId . '/followers'); + // ->addCC('https://' . $domain . '/' . $userId . '/followers'); $like->setSummary( $this->main->translate( $activity['articlelang'], @@ -392,6 +392,65 @@ class ContentNation implements Connector // 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']['name'] ?? 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; @@ -411,7 +470,7 @@ class ContentNation implements Connector if (isset($jsonData['options'])) { if (isset($jsonData['options']['informFollowers'])) { if ($jsonData['options']['informFollowers'] === true) { - if ($actorName != $articleOwnerName) { + if ($actorName !== $articleOwnerName) { $ap['to'][] = $ourUrl . '/' . $articleOwnerName; } $ap['to'][] = $ourUrl . '/' . $actorName . '/followers'; @@ -427,8 +486,13 @@ class ContentNation implements Connector error_log("ContentNation::jsonToActivity unknown inReplyTo type: {$replyType}"); } $returnActivity = \Federator\Data\ActivityPub\Factory::newActivityFromJson($ap); - $returnActivity->setID($ap['id']); - $returnActivity->setURL($ap['url']); + 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': @@ -487,8 +551,19 @@ class ContentNation implements Connector } */ $returnActivity = \Federator\Data\ActivityPub\Factory::newActivityFromJson($ap); - $returnActivity->setID($ap['id']); - $returnActivity->setURL($ap['url']); + 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: