diff --git a/php/federator/api.php b/php/federator/api.php index 1c85e5e..0433fa4 100644 --- a/php/federator/api.php +++ b/php/federator/api.php @@ -226,10 +226,12 @@ class Api extends Main $signature = base64_decode($signatureParts['signature']); $signedHeaders = explode(' ', $signatureParts['headers']); $keyId = $signatureParts['keyId']; + $publicKeyPem = false; + if ($this->cache !== null) { + $publicKeyPem = $this->cache->getPublicKey($keyId); + } - $publicKeyPem = $this->cache->getPublicKey($keyId); - - if (!isset($publicKeyPem) || $publicKeyPem === false) { + if ($publicKeyPem === false) { // Fetch public key from `keyId` (usually actor URL + #main-key) [$publicKeyData, $info] = \Federator\Main::getFromRemote($keyId, ['Accept: application/activity+json']); @@ -251,7 +253,9 @@ class Api extends Main } // Cache the public key for 1 hour - $this->cache->savePublicKey($keyId, $publicKeyPem); + if ($this->cache !== null) { + $this->cache->savePublicKey($keyId, $publicKeyPem); + } } // Reconstruct the signed string diff --git a/php/federator/api/fedusers/inbox.php b/php/federator/api/fedusers/inbox.php index e63782c..9b8992d 100644 --- a/php/federator/api/fedusers/inbox.php +++ b/php/federator/api/fedusers/inbox.php @@ -54,6 +54,7 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface try { $this->main->checkSignature($allHeaders); } catch (\Federator\Exceptions\PermissionDenied $e) { + error_log("signature check failed"); throw new \Federator\Exceptions\Unauthorized('Inbox::post Signature check failed: ' . $e->getMessage()); } @@ -64,7 +65,6 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface $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'); } @@ -74,9 +74,9 @@ 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); + $actor = $inboxActivity->getAActor(); // url of the sender https://contentnation.net/username + $username = basename((string) (parse_url($actor, PHP_URL_PATH) ?? '')); + $domain = parse_url($actor, PHP_URL_HOST); $userId = $username . '@' . $domain; $user = \Federator\DIO\FedUser::getUserByName( $dbh, @@ -132,11 +132,31 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface } $ourDomain = $config['generic']['externaldomain']; + $finalReceivers = []; foreach ($receivers as $receiver) { if ($receiver === '' || !is_string($receiver)) { continue; } - + if (!str_contains($receiver, $ourDomain) && $receiver !== $_user) { + continue; + } + // check if receiver is an actor url from our domain + if ($receiver !== $_user) { + $receiverName = basename((string) (parse_url($receiver, PHP_URL_PATH) ?? '')); + $ourDomain = parse_url($receiver, PHP_URL_HOST); + if ($receiverName === null || $ourDomain === null) { + error_log('Inbox::post no receiverName or domain found for receiver: ' . $receiver); + continue; + } + if ($receiverName[0] === '@') { + $receiverName = substr($receiverName, 1); + } + $receiver = $receiverName; + } + $finalReceivers[] = $receiver; + } + $finalReceivers = array_unique($finalReceivers); // remove duplicates + foreach ($finalReceivers as $receiver) { if (str_ends_with($receiver, '/followers')) { $actor = $inboxActivity->getAActor(); if ($actor === null || !is_string($actor)) { @@ -147,6 +167,7 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface // Extract username from the actor URL $username = basename((string) (parse_url($actor, PHP_URL_PATH) ?? '')); $domain = parse_url($actor, PHP_URL_HOST); + error_log("url $actor to username $username domain $domain"); if ($username === null || $domain === null) { error_log('Inbox::post no username or domain found for recipient: ' . $receiver); continue; @@ -168,19 +189,6 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface $users = array_merge($users, array_column($followers, 'id')); } } else { - // check if receiver is an actor url from our domain - if (!str_contains($receiver, $ourDomain) && $receiver !== $_user) { - continue; - } - if ($receiver !== $_user) { - $receiverName = basename((string) (parse_url($receiver, PHP_URL_PATH) ?? '')); - $ourDomain = parse_url($receiver, PHP_URL_HOST); - if ($receiverName === null || $ourDomain === null) { - error_log('Inbox::post no receiverName or domain found for receiver: ' . $receiver); - continue; - } - $receiver = $receiverName; - } try { $localUser = \Federator\DIO\User::getUserByName( $dbh, @@ -193,7 +201,7 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface continue; } if ($localUser === null || $localUser->id === null) { - error_log('Inbox::post couldn\'t find user: ' . $receiver); + error_log('Inbox::post 210 couldn\'t find user: ' . $receiver); continue; } $users[] = $localUser->id; @@ -203,7 +211,7 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface $users = array_unique($users); // remove duplicates if (empty($users)) { // todo remove after proper implementation, debugging for now - $rootDir = PROJECT_ROOT . '/'; + $rootDir = '/tmp/'; // Save the raw input and parsed JSON to a file for inspection file_put_contents( $rootDir . 'logs/inbox.log', @@ -213,6 +221,17 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface ); } + // Set the Redis backend for Resque + $rconfig = parse_ini_file($_SERVER['DOCUMENT_ROOT'] . '/../rediscache.ini'); + $redisUrl = sprintf( + 'redis://%s:%s@%s:%d?password-encoding=u', + urlencode($rconfig['username']), + urlencode($rconfig['password']), + $rconfig['host'], + intval($rconfig['port'], 10) + ); + \Resque::setBackend($redisUrl); + foreach ($users as $receiver) { if (!isset($receiver)) { continue; @@ -238,209 +257,6 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface . json_encode($inboxActivity, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT)); } } - - $connector->sendActivity($user, $inboxActivity); - return 'success'; } - - /** - * handle post call for specific user - * - * @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 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, $_recipientId, $inboxActivity) - { - if (!isset($_user)) { - error_log('Inbox::postForUser no user given'); - return false; - } - - // get sender - $user = \Federator\DIO\FedUser::getUserByName( - $dbh, - $_user, - $cache - ); - if ($user === null || $user->id === null) { - error_log('Inbox::postForUser couldn\'t find user: ' . $_user); - return false; - } - - $type = strtolower($inboxActivity->getType()); - - if ($_recipientId === '') { - if ($type === 'undo' || $type === 'delete') { - switch ($type) { - case 'delete': - // Delete Note/Post - $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); - } else { - error_log('Inbox::postForUser Error in Delete Post for user ' . $user->id - . ', object is not a string or object'); - error_log(' object of type ' . gettype($object)); - return false; - } - break; - - case 'undo': - $object = $inboxActivity->getObject(); - if (is_object($object)) { - switch (strtolower($object->getType())) { - case 'like': - case 'dislike': - // Undo Like/Dislike (remove like/dislike) - $targetId = $object->getID(); - // \Federator\DIO\Votes::removeVote($dbh, $user->id, $targetId, 'dislike'); - \Federator\DIO\Posts::deletePost($dbh, $targetId); - break; - case 'note': - case 'article': - // Undo Note (remove note) - $noteId = $object->getID(); - \Federator\DIO\Posts::deletePost($dbh, $noteId); - break; - } - } - break; - - default: - error_log('Inbox::postForUser Unhandled activity type ' . $type . ' for user ' . $user->id); - break; - } - - return true; - } - } - - $atPos = strpos($_recipientId, '@'); - if ($atPos !== false) { - $_recipientId = substr($_recipientId, 0, $atPos); - } - - // get recipient - $recipient = \Federator\DIO\User::getUserByName( - $dbh, - $_recipientId, - $connector, - $cache - ); - if ($recipient === null || $recipient->id === null) { - error_log('Inbox::postForUser couldn\'t find recipient: ' . $_recipientId); - return false; - } - - $rootDir = PROJECT_ROOT . '/'; - // Save the raw input and parsed JSON to a file for inspection - file_put_contents( - $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 - ); - - switch ($type) { - case 'follow': - $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); - } - break; - - case 'delete': - // Delete Note/Post - $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)) { - switch (strtolower($object->getType())) { - case 'follow': - $success = false; - if ($object instanceof \Federator\Data\ActivityPub\Common\Activity) { - $actor = $object->getAActor(); - if ($actor !== '') { - $success = \Federator\DIO\Followers::removeFollow($dbh, $user->id, $recipient->id); - } - } - if ($success === false) { - error_log('Inbox::postForUser Failed to remove follower for user ' . $user->id); - } - break; - case 'like': - case 'dislike': - // Undo Like/Dislike (remove like/dislike) - $targetId = $object->getID(); - \Federator\DIO\Votes::removeVote($dbh, $user->id, $targetId); - // \Federator\DIO\Posts::deletePost($dbh, $targetId); - break; - case 'note': - // Undo Note (remove note) - $noteId = $object->getID(); - \Federator\DIO\Posts::deletePost($dbh, $noteId); - break; - } - } - break; - - case 'like': - case 'dislike': - // Add Like/Dislike - $targetId = $inboxActivity->getObject(); - if (is_string($targetId)) { - \Federator\DIO\Votes::addVote($dbh, $user->id, $targetId, $type); - } else { - error_log('Inbox::postForUser Error in Add Like/Dislike for user ' . $user->id - . ', targetId is not a string'); - return false; - } - break; - - case 'create': - case 'update': - $object = $inboxActivity->getObject(); - if (is_object($object)) { - switch (strtolower($object->getType())) { - case 'note': - \Federator\DIO\Posts::savePost($dbh, $user->id, $inboxActivity); - break; - case 'article': - \Federator\DIO\Posts::savePost($dbh, $user->id, $inboxActivity); - break; - default: - \Federator\DIO\Posts::savePost($dbh, $user->id, $inboxActivity); - break; - } - } - break; - default: - error_log('Inbox::postForUser Unhandled activity type $type for user ' . $user->id); - break; - } - return true; - } } diff --git a/php/federator/api/v1/newcontent.php b/php/federator/api/v1/newcontent.php index 9a82f25..73e0301 100644 --- a/php/federator/api/v1/newcontent.php +++ b/php/federator/api/v1/newcontent.php @@ -454,7 +454,7 @@ class NewContent implements \Federator\Api\APIInterface } try { - $response = self::sendActivity($dbh, $host, $user, $recipient, $newActivity); + $response = \Federator\DIO\Server::sendActivity($dbh, $host, $user, $recipient, $newActivity); } catch (\Exception $e) { error_log('NewContent::postForUser Failed to send activity: ' . $e->getMessage()); return false; @@ -469,99 +469,6 @@ class NewContent implements \Federator\Api\APIInterface return true; } - /** - * send activity to federated server - * - * @param \mysqli $dbh database handle - * @param string $host host url of our server (e.g. federator) - * @param \Federator\Data\User $sender source user - * @param \Federator\Data\FedUser $target federated target user - * @param \Federator\Data\ActivityPub\Common\Activity $activity activity to send - * @return string|true the generated follow ID on success, false on failure - */ - public static function sendActivity($dbh, $host, $sender, $target, $activity) - { - if ($dbh === false) { - throw new \Federator\Exceptions\ServerError('NewContent::sendActivity Failed to get database handle'); - } - - $inboxUrl = $target->inboxURL; - - $json = json_encode($activity, JSON_UNESCAPED_SLASHES); - - if ($json === false) { - throw new \Exception('Failed to encode JSON: ' . json_last_error_msg()); - } - $digest = 'SHA-256=' . base64_encode(hash('sha256', $json, true)); - $date = gmdate('D, d M Y H:i:s') . ' GMT'; - $parsed = parse_url($inboxUrl); - if ($parsed === false) { - throw new \Exception('Failed to parse URL: ' . $inboxUrl); - } - - if (!isset($parsed['host']) || !isset($parsed['path'])) { - throw new \Exception('Invalid inbox URL: missing host or path'); - } - $extHost = $parsed['host']; - $path = $parsed['path']; - - // Build the signature string - $signatureString = "(request-target): post {$path}\n" . - "host: {$extHost}\n" . - "date: {$date}\n" . - "digest: {$digest}"; - - // Get rsa private key - $privateKey = \Federator\DIO\User::getrsaprivate($dbh, $sender->id); // OR from DB - if ($privateKey === false) { - throw new \Exception('Failed to get private key'); - } - $pkeyId = openssl_pkey_get_private($privateKey); - - if ($pkeyId === false) { - throw new \Exception('Invalid private key'); - } - - openssl_sign($signatureString, $signature, $pkeyId, OPENSSL_ALGO_SHA256); - $signature_b64 = base64_encode($signature); - - // Build keyId (public key ID from your actor object) - $keyId = $host . '/' . $sender->id . '#main-key'; - - $signatureHeader = 'keyId="' . $keyId - . '",algorithm="rsa-sha256",headers="(request-target) host date digest",signature="' . $signature_b64 . '"'; - - $ch = curl_init($inboxUrl); - if ($ch === false) { - throw new \Exception('Failed to initialize cURL'); - } - $headers = [ - 'Host: ' . $extHost, - 'Date: ' . $date, - 'Digest: ' . $digest, - 'Content-Type: application/activity+json', - 'Signature: ' . $signatureHeader, - 'Accept: application/activity+json', - ]; - - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_POST, true); - curl_setopt($ch, CURLOPT_POSTFIELDS, $json); - curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); - $response = curl_exec($ch); - curl_close($ch); - - if ($response === false) { - throw new \Exception('Failed to send activity: ' . curl_error($ch)); - } else { - $httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE); - if ($httpcode != 200 && $httpcode != 202) { - throw new \Exception('Unexpected HTTP code ' . $httpcode . ':' . $response); - } - } - return $response; - } - /** * get internal represenation as json string * @return string json string or html diff --git a/php/federator/data/activitypub/common/Undo.php b/php/federator/data/activitypub/common/undo.php similarity index 100% rename from php/federator/data/activitypub/common/Undo.php rename to php/federator/data/activitypub/common/undo.php diff --git a/php/federator/dio/feduser.php b/php/federator/dio/feduser.php index 3058cbc..4603a44 100644 --- a/php/federator/dio/feduser.php +++ b/php/federator/dio/feduser.php @@ -20,7 +20,7 @@ class FedUser * @param string $_user user/profile name * @return void */ - protected static function addLocalUser($dbh, $user, $_user) + protected static function addUserToDB($dbh, $user, $_user) { // check if it is timed out user $sql = 'select unix_timestamp(`validuntil`) from fedusers where id=?'; @@ -28,7 +28,7 @@ class FedUser if ($stmt === false) { throw new \Federator\Exceptions\ServerError('FedUser::addLocalUser Failed to prepare statement'); } - $stmt->bind_param("s", $_user); + $stmt->bind_param('s', $_user); $validuntil = 0; $ret = $stmt->bind_result($validuntil); $stmt->execute(); @@ -118,7 +118,7 @@ class FedUser $stmt->close(); // if a new user, create own database entry with additionally needed info if ($user->id === null || $validuntil < time()) { - self::addLocalUser($dbh, $user, $_user); + self::addUserToDB($dbh, $user, $_user); } // no further processing for now @@ -205,38 +205,40 @@ class FedUser 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('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('FedUser::getUserByName Failed to decode user for ' - . $_name); - } - $r['publicKeyId'] = $r['publicKey']['id']; - $r['publicKey'] = $r['publicKey']['publicKeyPem']; - if (isset($r['endpoints'])) { - if (isset($r['endpoints']['sharedInbox'])) { - $r['sharedInbox'] = $r['endpoints']['sharedInbox']; - } - } - $r['actorURL'] = $remoteURL; - $data = json_encode($r); - if ($data === false) { - throw new \Federator\Exceptions\ServerError('FedUser::getUserByName Failed to encode userdata ' - . $_name); - } - $user = \Federator\Data\FedUser::createFromJson($data); + } else { + $remoteURL = $_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('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('FedUser::getUserByName Failed to decode user for ' + . $_name); + } + $r['publicKeyId'] = $r['publicKey']['id']; + $r['publicKey'] = $r['publicKey']['publicKeyPem']; + if (isset($r['endpoints'])) { + if (isset($r['endpoints']['sharedInbox'])) { + $r['sharedInbox'] = $r['endpoints']['sharedInbox']; + } + } + $r['actorURL'] = $remoteURL; + $data = json_encode($r); + if ($data === false) { + 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) { - self::addLocalUser($dbh, $user, $_name); + self::addUserToDB($dbh, $user, $_name); } $cache->saveRemoteFedUserByName($_name, $user); } @@ -245,4 +247,223 @@ class FedUser } return $user; } + + /** + * handle post call for specific user + * + * @param \Federator\Main $main main instance + * @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 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 inboxForUser($main, $dbh, $connector, $cache, $_user, $_recipientId, $inboxActivity) + { + if (!isset($_user)) { + error_log('Inbox::postForUser no user given'); + return false; + } + + // get sender + $user = \Federator\DIO\FedUser::getUserByName( + $dbh, + $_user, + $cache + ); + if ($user === null || $user->id === null) { + error_log('Inbox::postForUser couldn\'t find user: ' . $_user); + return false; + } + + $type = strtolower($inboxActivity->getType()); + + if ($_recipientId === '') { + if ($type === 'undo' || $type === 'delete') { + switch ($type) { + case 'delete': + // Delete Note/Post + $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); + } else { + error_log('Inbox::postForUser Error in Delete Post for user ' . $user->id + . ', object is not a string or object'); + error_log(' object of type ' . gettype($object)); + return false; + } + break; + + case 'undo': + $object = $inboxActivity->getObject(); + if (is_object($object)) { + switch (strtolower($object->getType())) { + case 'like': + case 'dislike': + // Undo Like/Dislike (remove like/dislike) + $targetId = $object->getID(); + // \Federator\DIO\Votes::removeVote($dbh, $user->id, $targetId, 'dislike'); + \Federator\DIO\Posts::deletePost($dbh, $targetId); + break; + case 'note': + case 'article': + // Undo Note (remove note) + $noteId = $object->getID(); + \Federator\DIO\Posts::deletePost($dbh, $noteId); + break; + } + } + break; + + default: + error_log('Inbox::postForUser Unhandled activity type ' . $type . ' for user ' . $user->id); + break; + } + + return true; + } + } + + $atPos = strpos($_recipientId, '@'); + if ($atPos !== false) { + $_recipientId = substr($_recipientId, 0, $atPos); + } + + // get recipient + $recipient = \Federator\DIO\User::getUserByName( + $dbh, + $_recipientId, + $connector, + $cache + ); + if ($recipient === null || $recipient->id === null) { + error_log('Inbox::postForUser couldn\'t find recipient: ' . $_recipientId); + return false; + } + + $rootDir = $_SERVER['DOCUMENT_ROOT'] . '../'; + // Save the raw input and parsed JSON to a file for inspection + file_put_contents( + $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 + ); + + switch ($type) { + case 'follow': + $success = \Federator\DIO\Followers::addExternalFollow( + $dbh, + $inboxActivity->getID(), + $user->id, + $recipient->id + ); + + if ($success === true) { + // send accept back + $accept = new \Federator\Data\ActivityPub\Common\Accept(); + $local = $inboxActivity->getObject(); + if (is_string($local)) { + $accept->setAActor($local); + $id = bin2hex(openssl_random_pseudo_bytes(4)); + $accept->setID($local . '#accepts/follows/' . $id); + $obj = new \Federator\Data\ActivityPub\Common\Activity($inboxActivity->getType()); + $config = $main->getConfig(); + $ourhost = $config['generic']['protocol'] . '://' . $config['generic']['externaldomain']; + $obj->setID($ourhost . '/' . $id); + $obj->setAActor($inboxActivity->getAActor()); + $obj->setObject($local); + $accept->setObject($obj); + // send + \Federator\DIO\Server::sendActivity($dbh, $ourhost, $recipient, $user, $accept); + } + } else { + error_log('Inbox::postForUser Failed to add follower for user ' . $user->id); + } + break; + + case 'delete': + // Delete Note/Post + $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)) { + switch (strtolower($object->getType())) { + case 'follow': + $success = false; + if ($object instanceof \Federator\Data\ActivityPub\Common\Activity) { + $actor = $object->getAActor(); + if ($actor !== '') { + $success = \Federator\DIO\Followers::removeFollow($dbh, $user->id, $recipient->id); + } + } + if ($success === false) { + error_log('Inbox::postForUser Failed to remove follower for user ' . $user->id); + } + break; + case 'like': + case 'dislike': + // Undo Like/Dislike (remove like/dislike) + $targetId = $object->getID(); + \Federator\DIO\Votes::removeVote($dbh, $user->id, $targetId); + // \Federator\DIO\Posts::deletePost($dbh, $targetId); + break; + case 'note': + // Undo Note (remove note) + $noteId = $object->getID(); + \Federator\DIO\Posts::deletePost($dbh, $noteId); + break; + } + } + break; + + case 'like': + case 'dislike': + // Add Like/Dislike + $targetId = $inboxActivity->getObject(); + if (is_string($targetId)) { + \Federator\DIO\Votes::addVote($dbh, $user->id, $targetId, $type); + } else { + error_log('Inbox::postForUser Error in Add Like/Dislike for user ' . $user->id + . ', targetId is not a string'); + return false; + } + break; + + case 'create': + case 'update': + $object = $inboxActivity->getObject(); + if (is_object($object)) { + switch (strtolower($object->getType())) { + case 'note': + \Federator\DIO\Posts::savePost($dbh, $user->id, $inboxActivity); + break; + case 'article': + \Federator\DIO\Posts::savePost($dbh, $user->id, $inboxActivity); + break; + default: + \Federator\DIO\Posts::savePost($dbh, $user->id, $inboxActivity); + break; + } + } + break; + default: + error_log('Inbox::postForUser Unhandled activity type $type for user ' . $user->id); + break; + } + return true; + } } diff --git a/php/federator/dio/server.php b/php/federator/dio/server.php new file mode 100644 index 0000000..f9f5580 --- /dev/null +++ b/php/federator/dio/server.php @@ -0,0 +1,108 @@ +inboxURL; + + $json = json_encode($activity, JSON_UNESCAPED_SLASHES); + + if ($json === false) { + throw new \Exception('Failed to encode JSON: ' . json_last_error_msg()); + } + $digest = 'SHA-256=' . base64_encode(hash('sha256', $json, true)); + $date = gmdate('D, d M Y H:i:s') . ' GMT'; + echo "inboxurl $receiverInboxUrl\n"; + $parsedReceiverInboxUrl = parse_url($receiverInboxUrl); + if ($parsedReceiverInboxUrl === false) { + throw new \Exception('Failed to parse URL: ' . $receiverInboxUrl); + } + + if (!isset($parsedReceiverInboxUrl['host']) || !isset($parsedReceiverInboxUrl['path'])) { + throw new \Exception('Invalid inbox URL: missing host or path'); + } + $extHost = $parsedReceiverInboxUrl['host']; + $path = $parsedReceiverInboxUrl['path']; + + // Build the signature string + $signatureString = "(request-target): post {$path}\n" . + "host: {$extHost}\n" . + "date: {$date}\n" . + "digest: {$digest}"; + + // Get rsa private key + $privateKey = \Federator\DIO\User::getrsaprivate($dbh, $sender->id); // OR from DB + if ($privateKey === false) { + throw new \Exception('Failed to get private key'); + } + $pkeyId = openssl_pkey_get_private($privateKey); + + if ($pkeyId === false) { + throw new \Exception('Invalid private key'); + } +echo "signaturestring $signatureString\n"; + openssl_sign($signatureString, $signature, $pkeyId, OPENSSL_ALGO_SHA256); + $signature_b64 = base64_encode($signature); + + // Build keyId (public key ID from your actor object) + $keyId = $ourhost . '/' . $sender->id . '#main-key'; + + $signatureHeader = 'keyId="' . $keyId + . '",algorithm="rsa-sha256",headers="(request-target) host date digest",signature="' . $signature_b64 . '"'; + + $ch = curl_init($receiverInboxUrl); + if ($ch === false) { + throw new \Exception('Failed to initialize cURL'); + } + $headers = [ + 'Host: ' . $extHost, + 'Date: ' . $date, + 'Digest: ' . $digest, + 'Content-Type: application/activity+json', + 'Signature: ' . $signatureHeader, + 'Accept: application/activity+json', + ]; +print_r($headers); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, $json); + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + $response = curl_exec($ch); + curl_close($ch); + + if ($response === false) { + throw new \Exception('Failed to send activity: ' . curl_error($ch)); + } else { + $httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + if ($httpcode != 200 && $httpcode != 202) { + throw new \Exception('Unexpected HTTP code ' . $httpcode . ':' . $response); + } + } + if ($response !== true) { + error_log($response); + } + return true; + } +} diff --git a/php/federator/jobs/inboxJob.php b/php/federator/jobs/inboxjob.php similarity index 95% rename from php/federator/jobs/inboxJob.php rename to php/federator/jobs/inboxjob.php index b4a4301..7368c6e 100644 --- a/php/federator/jobs/inboxJob.php +++ b/php/federator/jobs/inboxjob.php @@ -64,7 +64,8 @@ class InboxJob extends \Federator\Api return false; } - \Federator\Api\FedUsers\Inbox::postForUser( + \Federator\DIO\FedUser::inboxForUser( + $this, $this->dbh, $this->connector, $this->cache, diff --git a/php/federator/jobs/newContentJob.php b/php/federator/jobs/newcontentjob.php similarity index 100% rename from php/federator/jobs/newContentJob.php rename to php/federator/jobs/newcontentjob.php diff --git a/php/federator/main.php b/php/federator/main.php index 0683151..8a8d1c8 100644 --- a/php/federator/main.php +++ b/php/federator/main.php @@ -20,7 +20,7 @@ class Main * * @var Cache\Cache $cache */ - protected $cache; + protected $cache = null; /** * current config diff --git a/php/federator/test.php b/php/federator/test.php index 0b0b131..38b6b9b 100644 --- a/php/federator/test.php +++ b/php/federator/test.php @@ -143,7 +143,8 @@ class Test $inboxActivity->setAActor('https://mastodon.local/users/admin'); $inboxActivity->setObject($_url); $inboxActivity->setID("https://mastodon.local/users/admin#like/" . md5($_url)); - \Federator\Api\FedUsers\Inbox::postForUser( + \Federator\DIO\FedUser::inboxForUser( + $api, $dbh, $api->getConnector(), null, diff --git a/php/federator/workers/worker_inbox.php b/php/federator/workers/worker_inbox.php index 64be128..f57b626 100644 --- a/php/federator/workers/worker_inbox.php +++ b/php/federator/workers/worker_inbox.php @@ -1,14 +1,18 @@