bugfixes and refactoring, cleanup
This commit is contained in:
		
							parent
							
								
									f8fb409327
								
							
						
					
					
						commit
						5ef5c97d5e
					
				
					 14 changed files with 426 additions and 364 deletions
				
			
		| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										108
									
								
								php/federator/dio/server.php
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								php/federator/dio/server.php
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,108 @@
 | 
			
		|||
<?php
 | 
			
		||||
/**
 | 
			
		||||
 * SPDX-FileCopyrightText: 2025 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop
 | 
			
		||||
 * SPDX-License-Identifier: GPL-3.0-or-later
 | 
			
		||||
 *
 | 
			
		||||
 * @author Sascha Nitsch (grumpyveveloper)
 | 
			
		||||
 **/
 | 
			
		||||
 | 
			
		||||
namespace Federator\DIO;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Do the Server2Server communication
 | 
			
		||||
 */
 | 
			
		||||
class Server
 | 
			
		||||
{
 | 
			
		||||
    /**
 | 
			
		||||
     * send activity to federated server
 | 
			
		||||
     *
 | 
			
		||||
     * @param \mysqli $dbh database handle
 | 
			
		||||
     * @param string $ourhost host url of our server (e.g. federator)
 | 
			
		||||
     * @param \Federator\Data\User $sender source user
 | 
			
		||||
     * @param \Federator\Data\FedUser $receiver federated user to receive the activity
 | 
			
		||||
     * @param \Federator\Data\ActivityPub\Common\Activity $activity activity to send
 | 
			
		||||
     * @return boolean true on success
 | 
			
		||||
     */
 | 
			
		||||
    public static function sendActivity($dbh, $ourhost, $sender, $receiver, $activity)
 | 
			
		||||
    {
 | 
			
		||||
        $receiverInboxUrl = $receiver->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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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,
 | 
			
		||||
| 
						 | 
				
			
			@ -20,7 +20,7 @@ class Main
 | 
			
		|||
     *
 | 
			
		||||
     * @var Cache\Cache $cache
 | 
			
		||||
     */
 | 
			
		||||
    protected $cache;
 | 
			
		||||
    protected $cache = null;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * current config
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,14 +1,18 @@
 | 
			
		|||
<?php
 | 
			
		||||
 | 
			
		||||
define('PROJECT_ROOT', dirname(__DIR__, 3));
 | 
			
		||||
 | 
			
		||||
require_once PROJECT_ROOT . '/vendor/autoload.php';
 | 
			
		||||
$_SERVER['DOCUMENT_ROOT'] = PROJECT_ROOT . '/htdocs/';
 | 
			
		||||
 | 
			
		||||
spl_autoload_register(static function (string $className) {
 | 
			
		||||
    include PROJECT_ROOT . '/php/' . str_replace("\\", "/", strtolower($className)) . '.php';
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
$config = parse_ini_file(PROJECT_ROOT . '/rediscache.ini');
 | 
			
		||||
 | 
			
		||||
// Set the Redis backend for Resque
 | 
			
		||||
$redisUrl = sprintf(
 | 
			
		||||
    'redis://%s:%s@%s:%d',
 | 
			
		||||
    'redis://%s:%s@%s:%d?password-encoding=u',
 | 
			
		||||
    urlencode($config['username']),
 | 
			
		||||
    urlencode($config['password']),
 | 
			
		||||
    $config['host'],
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -19,10 +19,10 @@ primary goal is to connect ContentNation via ActivityPub again.
 | 
			
		|||
- [X] full cache for users
 | 
			
		||||
- [X] webfinger
 | 
			
		||||
- [X] discovery endpoints
 | 
			
		||||
- [ ] ap outbox
 | 
			
		||||
- [ ] ap inbox
 | 
			
		||||
- [X] ap outbox
 | 
			
		||||
- [X] ap inbox
 | 
			
		||||
- [ ] support for AP profile in service
 | 
			
		||||
- [ ] support for article
 | 
			
		||||
- [ ] support for comment
 | 
			
		||||
- [ ] posting comments from ap to service
 | 
			
		||||
- [ ] callback from service to add new input
 | 
			
		||||
- [X] callback from service to add new input
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -58,7 +58,7 @@
 | 
			
		|||
            {rdelim}
 | 
			
		||||
        {rdelim}
 | 
			
		||||
    ],
 | 
			
		||||
    "id":"https://{$fqdn}/@{$username}",
 | 
			
		||||
    "id":"https://{$fqdn}/{$username}",
 | 
			
		||||
    "type":"{$type}",
 | 
			
		||||
    "following":"https://{$fqdn}/{$username}/following",
 | 
			
		||||
    "followers":"https://{$fqdn}/{$username}/followers",
 | 
			
		||||
| 
						 | 
				
			
			@ -74,9 +74,9 @@
 | 
			
		|||
    "discoverable":true,
 | 
			
		||||
    "published":"{$registered}",
 | 
			
		||||
    "publicKey":{ldelim}
 | 
			
		||||
    "id":"https://{$fqdn}/{$username}#main-key",
 | 
			
		||||
    "owner":"https://{$sourcedomain}/@{$username}",
 | 
			
		||||
    "publicKeyPem":"{$publickey}"
 | 
			
		||||
        "id":"https://{$fqdn}/{$username}#main-key",
 | 
			
		||||
        "owner":"https://{$sourcedomain}/{$username}",
 | 
			
		||||
        "publicKeyPem":"{$publickey}"
 | 
			
		||||
    {rdelim},
 | 
			
		||||
    "tag":[],
 | 
			
		||||
    "attachment":[
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,7 +4,7 @@
 | 
			
		|||
    "https://{$sourcedomain}/@{$username}"
 | 
			
		||||
  ],
 | 
			
		||||
  "links": [
 | 
			
		||||
    {ldelim}"rel": "self", "type": "application/activity+json", "href": "https://{$domain}/@{$username}"{rdelim},
 | 
			
		||||
    {ldelim}"rel": "self", "type": "application/activity+json", "href": "https://{$domain}/{$username}"{rdelim},
 | 
			
		||||
{if $type=='Group'}
 | 
			
		||||
    {ldelim}"rel": "http://webfinger.net/rel/profile-page", "type": "text/html", "href": "https://{$domain}/@{$username}/"{rdelim},
 | 
			
		||||
{/if}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		
		Reference in a new issue