forked from grumpydevelop/federator
		
	support external services to comment&like on CN
- integrate support to send new posts to CN - save original article-id in DB (needs db-migration) - votes and comments on CN-articles and comments are sent to CN, with proper signing and format - fixed minor issue where delete-activity was not properly working with objects - fixed minor issue where tombstone wasn't supported (which prevented being able to delete mastodon-posts from the db)
This commit is contained in:
		
							parent
							
								
									8891234617
								
							
						
					
					
						commit
						96bb1efe16
					
				
					 11 changed files with 503 additions and 118 deletions
				
			
		| 
						 | 
				
			
			@ -43,7 +43,7 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
 | 
			
		|||
    /**
 | 
			
		||||
     * handle post call
 | 
			
		||||
     *
 | 
			
		||||
     * @param string|null $_user user to add data to inbox @unused-param
 | 
			
		||||
     * @param string|null $_user user to add data to inbox
 | 
			
		||||
     * @return string|false response
 | 
			
		||||
     */
 | 
			
		||||
    public function post($_user)
 | 
			
		||||
| 
						 | 
				
			
			@ -107,6 +107,10 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
 | 
			
		|||
                && (filter_var($receiver, FILTER_VALIDATE_URL) !== false);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        if (isset($_user)) {
 | 
			
		||||
            $receivers[] = $dbh->real_escape_string($_user); // Add the target user to the receivers list
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Special handling for Follow and Undo follow activities
 | 
			
		||||
        if (strtolower($inboxActivity->getType()) === 'follow') {
 | 
			
		||||
            // For Follow, the object should hold the target
 | 
			
		||||
| 
						 | 
				
			
			@ -126,6 +130,7 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
 | 
			
		|||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        $ourDomain = $config['generic']['externaldomain'];
 | 
			
		||||
 | 
			
		||||
        foreach ($receivers as $receiver) {
 | 
			
		||||
            if ($receiver === '' || !is_string($receiver)) {
 | 
			
		||||
| 
						 | 
				
			
			@ -157,18 +162,19 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
 | 
			
		|||
                    $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)) {
 | 
			
		||||
                if (!str_contains($receiver, $ourDomain) && $receiver !== $_user) {
 | 
			
		||||
                    continue;
 | 
			
		||||
                }
 | 
			
		||||
                $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 ($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;
 | 
			
		||||
                }
 | 
			
		||||
                $receiver = $receiverName . '@' . $ourDomain;
 | 
			
		||||
                try {
 | 
			
		||||
                    $localUser = \Federator\DIO\User::getUserByName(
 | 
			
		||||
                        $dbh,
 | 
			
		||||
| 
						 | 
				
			
			@ -209,6 +215,30 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
 | 
			
		|||
            ]);
 | 
			
		||||
            error_log("Inbox::post enqueued job for user: $user->id with token: $token");
 | 
			
		||||
        }
 | 
			
		||||
        if (empty($users)) {
 | 
			
		||||
            $type = strtolower($inboxActivity->getType());
 | 
			
		||||
            if ($type === 'undo' || $type === 'delete') {
 | 
			
		||||
                $token = \Resque::enqueue('inbox', 'Federator\\Jobs\\InboxJob', [
 | 
			
		||||
                    'user' => $user->id,
 | 
			
		||||
                    'recipientId' => "",
 | 
			
		||||
                    'activity' => $inboxActivity->toObject(),
 | 
			
		||||
                ]);
 | 
			
		||||
                error_log("Inbox::post enqueued job for user: $user->id with token: $token");
 | 
			
		||||
            } else {
 | 
			
		||||
                error_log("Inbox::post no users found for activity, doing nothing: " . json_encode($inboxActivity, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            $articleId = \Federator\DIO\Posts::getOriginalArticleId($dbh, $inboxActivity);
 | 
			
		||||
            if ($articleId !== null) {
 | 
			
		||||
                $connector->sendActivity($user, $inboxActivity);
 | 
			
		||||
            }
 | 
			
		||||
        } catch (\Throwable $e) {
 | 
			
		||||
            error_log("Inbox::postForUser Error sending activity to connector. Exception: " . $e->getMessage());
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return "success";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -241,6 +271,56 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
 | 
			
		|||
            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);
 | 
			
		||||
| 
						 | 
				
			
			@ -266,18 +346,16 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
 | 
			
		|||
            FILE_APPEND
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        $type = $inboxActivity->getType();
 | 
			
		||||
 | 
			
		||||
        switch ($type) {
 | 
			
		||||
            case 'Follow':
 | 
			
		||||
            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");
 | 
			
		||||
                    error_log("Inbox::postForUser Failed to add follower for user $user->id");
 | 
			
		||||
                }
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            case 'Delete':
 | 
			
		||||
            case 'delete':
 | 
			
		||||
                // Delete Note/Post
 | 
			
		||||
                $object = $inboxActivity->getObject();
 | 
			
		||||
                if (is_string($object)) {
 | 
			
		||||
| 
						 | 
				
			
			@ -288,11 +366,11 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
 | 
			
		|||
                }
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            case 'Undo':
 | 
			
		||||
            case 'undo':
 | 
			
		||||
                $object = $inboxActivity->getObject();
 | 
			
		||||
                if (is_object($object)) {
 | 
			
		||||
                    switch ($object->getType()) {
 | 
			
		||||
                        case 'Follow':
 | 
			
		||||
                    switch (strtolower($object->getType())) {
 | 
			
		||||
                        case 'follow':
 | 
			
		||||
                            $success = false;
 | 
			
		||||
                            if ($object instanceof \Federator\Data\ActivityPub\Common\Activity) {
 | 
			
		||||
                                $actor = $object->getAActor();
 | 
			
		||||
| 
						 | 
				
			
			@ -301,75 +379,60 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
 | 
			
		|||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                            if ($success === false) {
 | 
			
		||||
                                error_log("Inbox::postForUser: Failed to remove follower for user $user->id");
 | 
			
		||||
                                error_log("Inbox::postForUser Failed to remove follower for user $user->id");
 | 
			
		||||
                            }
 | 
			
		||||
                            break;
 | 
			
		||||
                        case 'Like':
 | 
			
		||||
                            // Undo Like (remove like)
 | 
			
		||||
                            if (method_exists($object, 'getObject')) {
 | 
			
		||||
                                $targetId = $object->getObject();
 | 
			
		||||
                                if (is_string($targetId)) {
 | 
			
		||||
                                    // \Federator\DIO\Votes::removeVote($dbh, $user->id, $targetId, 'like');
 | 
			
		||||
                                    \Federator\DIO\Posts::deletePost($dbh, $targetId);
 | 
			
		||||
                                } else {
 | 
			
		||||
                                    error_log("Inbox::postForUser: Error in Undo Like for user $user->id, targetId is not a string");
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                        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 'Dislike':
 | 
			
		||||
                            // Undo Dislike (remove dislike)
 | 
			
		||||
                            if (method_exists($object, 'getObject')) {
 | 
			
		||||
                                $targetId = $object->getObject();
 | 
			
		||||
                                if (is_string($targetId)) {
 | 
			
		||||
                                    // \Federator\DIO\Votes::removeVote($dbh, $user->id, $targetId, 'dislike');
 | 
			
		||||
                                    \Federator\DIO\Posts::deletePost($dbh, $targetId);
 | 
			
		||||
                                } else {
 | 
			
		||||
                                    error_log("Inbox::postForUser: Error in Undo Dislike for user $user->id, targetId is not a string");
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                            break;
 | 
			
		||||
                        case 'Note':
 | 
			
		||||
                        case 'note':
 | 
			
		||||
                            // Undo Note (remove note)
 | 
			
		||||
                            if (method_exists($object, 'getID')) {
 | 
			
		||||
                                $noteId = $object->getID();
 | 
			
		||||
                                \Federator\DIO\Posts::deletePost($dbh, $noteId);
 | 
			
		||||
                            }
 | 
			
		||||
                            $noteId = $object->getID();
 | 
			
		||||
                            \Federator\DIO\Posts::deletePost($dbh, $noteId);
 | 
			
		||||
                            break;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            case 'Like':
 | 
			
		||||
                // Add Like
 | 
			
		||||
                $targetId = $inboxActivity->getObject();
 | 
			
		||||
                if (is_string($targetId)) {
 | 
			
		||||
                    // \Federator\DIO\Votes::addVote($dbh, $user->id, $targetId, 'like');
 | 
			
		||||
                    \Federator\DIO\Posts::savePost($dbh, $user->id, $inboxActivity);
 | 
			
		||||
                } else {
 | 
			
		||||
                    error_log("Inbox::postForUser: Error in Add Like for user $user->id, targetId is not a string");
 | 
			
		||||
                    return false;
 | 
			
		||||
                }
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            case 'Dislike':
 | 
			
		||||
                // Add Dislike
 | 
			
		||||
            case 'like':
 | 
			
		||||
            case 'dislike':
 | 
			
		||||
                // Add Like/Dislike
 | 
			
		||||
                $targetId = $inboxActivity->getObject();
 | 
			
		||||
                if (is_string($targetId)) {
 | 
			
		||||
                    // \Federator\DIO\Votes::addVote($dbh, $user->id, $targetId, 'dislike');
 | 
			
		||||
                    \Federator\DIO\Posts::savePost($dbh, $user->id, $inboxActivity);
 | 
			
		||||
                } else {
 | 
			
		||||
                    error_log("Inbox::postForUser: Error in Add Dislike for user $user->id, targetId is not a string");
 | 
			
		||||
                    error_log("Inbox::postForUser Error in Add Like/Dislike for user $user->id, targetId is not a string");
 | 
			
		||||
                    return false;
 | 
			
		||||
                }
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            case 'Note':
 | 
			
		||||
                // Post Note
 | 
			
		||||
                \Federator\DIO\Posts::savePost($dbh, $user->id, $inboxActivity);
 | 
			
		||||
            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");
 | 
			
		||||
                error_log("Inbox::postForUser Unhandled activity type $type for user $user->id");
 | 
			
		||||
                break;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -100,8 +100,9 @@ class NewContent implements \Federator\Api\APIInterface
 | 
			
		|||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $articleId = "";
 | 
			
		||||
        if (isset($allHeaders['X-Sender'])) {
 | 
			
		||||
            $newActivity = $connector->jsonToActivity($input);
 | 
			
		||||
            $newActivity = $connector->jsonToActivity($input, $articleId);
 | 
			
		||||
        } else {
 | 
			
		||||
            error_log("NewContent::post No X-Sender header found");
 | 
			
		||||
            return false;
 | 
			
		||||
| 
						 | 
				
			
			@ -212,6 +213,7 @@ class NewContent implements \Federator\Api\APIInterface
 | 
			
		|||
                'user' => $posterName,
 | 
			
		||||
                'recipientId' => $receiver,
 | 
			
		||||
                'activity' => $newActivity->toObject(),
 | 
			
		||||
                'articleId' => $articleId,
 | 
			
		||||
            ]);
 | 
			
		||||
            error_log("Inbox::post enqueued job for receiver: $receiver with token: $token");
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			@ -232,9 +234,11 @@ class NewContent implements \Federator\Api\APIInterface
 | 
			
		|||
     * @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
 | 
			
		||||
     * @param string $articleId the original id of the article (if applicable)
 | 
			
		||||
     *                           (used to identify the article in the remote system)
 | 
			
		||||
     * @return boolean response
 | 
			
		||||
     */
 | 
			
		||||
    public static function postForUser($dbh, $connector, $cache, $host, $_user, $_recipientId, $newActivity)
 | 
			
		||||
    public static function postForUser($dbh, $connector, $cache, $host, $_user, $_recipientId, $newActivity, $articleId)
 | 
			
		||||
    {
 | 
			
		||||
        if (!isset($_user)) {
 | 
			
		||||
            error_log("NewContent::postForUser no user given");
 | 
			
		||||
| 
						 | 
				
			
			@ -272,10 +276,10 @@ class NewContent implements \Federator\Api\APIInterface
 | 
			
		|||
            FILE_APPEND
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        $type = $newActivity->getType();
 | 
			
		||||
        $type = strtolower($newActivity->getType());
 | 
			
		||||
 | 
			
		||||
        switch ($type) {
 | 
			
		||||
            case 'Follow':
 | 
			
		||||
            case 'follow':
 | 
			
		||||
                // $success = false;
 | 
			
		||||
                $actor = $newActivity->getAActor();
 | 
			
		||||
                if ($actor !== '') {
 | 
			
		||||
| 
						 | 
				
			
			@ -293,7 +297,7 @@ class NewContent implements \Federator\Api\APIInterface
 | 
			
		|||
                } */
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            case 'Delete':
 | 
			
		||||
            case 'delete':
 | 
			
		||||
                // Delete Note/Post
 | 
			
		||||
                $object = $newActivity->getObject();
 | 
			
		||||
                if (is_string($object)) {
 | 
			
		||||
| 
						 | 
				
			
			@ -304,11 +308,11 @@ class NewContent implements \Federator\Api\APIInterface
 | 
			
		|||
                }
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            case 'Undo':
 | 
			
		||||
            case 'undo':
 | 
			
		||||
                $object = $newActivity->getObject();
 | 
			
		||||
                if (is_object($object)) {
 | 
			
		||||
                    switch ($object->getType()) {
 | 
			
		||||
                        case 'Follow':
 | 
			
		||||
                    switch (strtolower($object->getType())) {
 | 
			
		||||
                        case 'follow':
 | 
			
		||||
                            $success = false;
 | 
			
		||||
                            if ($object instanceof \Federator\Data\ActivityPub\Common\Activity) {
 | 
			
		||||
                                $actor = $object->getAActor();
 | 
			
		||||
| 
						 | 
				
			
			@ -332,8 +336,8 @@ class NewContent implements \Federator\Api\APIInterface
 | 
			
		|||
                                error_log("NewContent::postForUser Failed to remove follower for user $user->id");
 | 
			
		||||
                            }
 | 
			
		||||
                            break;
 | 
			
		||||
                        case 'Like':
 | 
			
		||||
                        case 'Dislike':
 | 
			
		||||
                        case 'like':
 | 
			
		||||
                        case 'dislike':
 | 
			
		||||
                            if (method_exists($object, 'getObject')) {
 | 
			
		||||
                                $targetId = $object->getObject();
 | 
			
		||||
                                if (is_string($targetId)) {
 | 
			
		||||
| 
						 | 
				
			
			@ -344,13 +348,13 @@ class NewContent implements \Federator\Api\APIInterface
 | 
			
		|||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                            break;
 | 
			
		||||
                        case 'Note':
 | 
			
		||||
                        case 'note':
 | 
			
		||||
                            // Undo Note (remove note)
 | 
			
		||||
                            $noteId = $object->getID();
 | 
			
		||||
                            \Federator\DIO\Posts::deletePost($dbh, $noteId);
 | 
			
		||||
 | 
			
		||||
                            break;
 | 
			
		||||
                        case 'Article':
 | 
			
		||||
                        case 'article':
 | 
			
		||||
                            $articleId = $object->getID();
 | 
			
		||||
                            \Federator\DIO\Posts::deletePost($dbh, $articleId);
 | 
			
		||||
                            // also remove latest saved article-update
 | 
			
		||||
| 
						 | 
				
			
			@ -381,30 +385,30 @@ class NewContent implements \Federator\Api\APIInterface
 | 
			
		|||
                }
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            case 'Like':
 | 
			
		||||
            case 'Dislike':
 | 
			
		||||
            case 'like':
 | 
			
		||||
            case 'dislike':
 | 
			
		||||
                // Add Like/Dislike
 | 
			
		||||
                $targetId = $newActivity->getObject();
 | 
			
		||||
                if (is_string($targetId)) {
 | 
			
		||||
                    // \Federator\DIO\Votes::addVote($dbh, $user->id, $targetId, 'like');
 | 
			
		||||
                    \Federator\DIO\Posts::savePost($dbh, $user->id, $newActivity);
 | 
			
		||||
                    \Federator\DIO\Posts::savePost($dbh, $user->id, $newActivity, $articleId);
 | 
			
		||||
                } else {
 | 
			
		||||
                    error_log("NewContent::postForUser Error in Add Like/Dislike for recipient $recipient->id, targetId is not a string");
 | 
			
		||||
                    return false;
 | 
			
		||||
                }
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            case 'Create':
 | 
			
		||||
            case 'Update':
 | 
			
		||||
            case 'create':
 | 
			
		||||
            case 'update':
 | 
			
		||||
                $object = $newActivity->getObject();
 | 
			
		||||
                if (is_object($object)) {
 | 
			
		||||
                    switch ($object->getType()) {
 | 
			
		||||
                        case 'Note':
 | 
			
		||||
                            \Federator\DIO\Posts::savePost($dbh, $user->id, $newActivity);
 | 
			
		||||
                    switch (strtolower($object->getType())) {
 | 
			
		||||
                        case 'note':
 | 
			
		||||
                            \Federator\DIO\Posts::savePost($dbh, $user->id, $newActivity, $articleId);
 | 
			
		||||
 | 
			
		||||
                            break;
 | 
			
		||||
                        case 'Article':
 | 
			
		||||
                            \Federator\DIO\Posts::savePost($dbh, $user->id, $newActivity);
 | 
			
		||||
                        case 'article':
 | 
			
		||||
                            \Federator\DIO\Posts::savePost($dbh, $user->id, $newActivity, $articleId);
 | 
			
		||||
 | 
			
		||||
                            $idPart = strrchr($recipient->id, '@');
 | 
			
		||||
                            if ($idPart === false) {
 | 
			
		||||
| 
						 | 
				
			
			@ -423,7 +427,7 @@ class NewContent implements \Federator\Api\APIInterface
 | 
			
		|||
 | 
			
		||||
                            break;
 | 
			
		||||
                        default:
 | 
			
		||||
                            \Federator\DIO\Posts::savePost($dbh, $user->id, $newActivity);
 | 
			
		||||
                            \Federator\DIO\Posts::savePost($dbh, $user->id, $newActivity, $articleId);
 | 
			
		||||
                            break;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
| 
						 | 
				
			
			@ -531,7 +535,6 @@ class NewContent implements \Federator\Api\APIInterface
 | 
			
		|||
        $response = curl_exec($ch);
 | 
			
		||||
        curl_close($ch);
 | 
			
		||||
 | 
			
		||||
        // Log the response for debugging if needed
 | 
			
		||||
        if ($response === false) {
 | 
			
		||||
            throw new \Exception("Failed to send activity: " . curl_error($ch));
 | 
			
		||||
        } else {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -70,9 +70,20 @@ interface Connector
 | 
			
		|||
     * Convert jsonData to Activity format
 | 
			
		||||
     *
 | 
			
		||||
     * @param array<string, mixed> $jsonData the json data from our platfrom
 | 
			
		||||
     * @param string $articleId the original id of the article (if applicable)
 | 
			
		||||
     *                           (used to identify the article in the remote system)
 | 
			
		||||
     * @return \Federator\Data\ActivityPub\Common\Activity|false
 | 
			
		||||
     */
 | 
			
		||||
    public function jsonToActivity(array $jsonData);
 | 
			
		||||
    public function jsonToActivity(array $jsonData, &$articleId);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * send target-friendly json from ActivityPub activity
 | 
			
		||||
     *
 | 
			
		||||
     * @param \Federator\Data\FedUser $sender the user of the sender
 | 
			
		||||
     * @param \Federator\Data\ActivityPub\Common\Activity $activity the activity
 | 
			
		||||
     * @return boolean did we successfully send the activity?
 | 
			
		||||
     */
 | 
			
		||||
    public function sendActivity($sender, $activity);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * check if the headers include a valid signature
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10,17 +10,6 @@ namespace Federator\Data\ActivityPub\Common;
 | 
			
		|||
 | 
			
		||||
class Delete extends Activity
 | 
			
		||||
{
 | 
			
		||||
    /**
 | 
			
		||||
     * object overwrite
 | 
			
		||||
     * @var string
 | 
			
		||||
     */
 | 
			
		||||
    private $object = "";
 | 
			
		||||
 | 
			
		||||
    public function setFObject(string $object): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->object = $object;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function __construct()
 | 
			
		||||
    {
 | 
			
		||||
        parent::__construct('Delete');
 | 
			
		||||
| 
						 | 
				
			
			@ -33,10 +22,6 @@ class Delete extends Activity
 | 
			
		|||
     */
 | 
			
		||||
    public function fromJson($json): bool
 | 
			
		||||
    {
 | 
			
		||||
        if (array_key_exists('object', $json)) {
 | 
			
		||||
            $this->object = $json['object'];
 | 
			
		||||
            unset($json['object']);
 | 
			
		||||
        }
 | 
			
		||||
        return parent::fromJson($json);
 | 
			
		||||
    }
 | 
			
		||||
    /**
 | 
			
		||||
| 
						 | 
				
			
			@ -46,9 +31,6 @@ class Delete extends Activity
 | 
			
		|||
    public function toObject()
 | 
			
		||||
    {
 | 
			
		||||
        $return = parent::toObject();
 | 
			
		||||
        if ($this->object !== "") {
 | 
			
		||||
            $return['object'] = $this->object;
 | 
			
		||||
        }
 | 
			
		||||
        return $return;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -67,6 +67,9 @@ class Factory
 | 
			
		|||
            case 'Inbox':
 | 
			
		||||
                $return = new Common\Inbox();
 | 
			
		||||
                break;
 | 
			
		||||
            case 'Tombstone':
 | 
			
		||||
                $return = new Common\APObject("Tombstone");
 | 
			
		||||
                break;
 | 
			
		||||
            /*case 'Question':
 | 
			
		||||
                $return = new Common\Question();
 | 
			
		||||
                break;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -216,13 +216,15 @@ class Posts
 | 
			
		|||
     * @param \mysqli $dbh
 | 
			
		||||
     * @param string $userId
 | 
			
		||||
     * @param \Federator\Data\ActivityPub\Common\Activity $post
 | 
			
		||||
     * @param string|null $articleId the original id of the article
 | 
			
		||||
     *                           (used to identify the source article in the remote system)
 | 
			
		||||
     * @return bool
 | 
			
		||||
     */
 | 
			
		||||
    public static function savePost($dbh, $userId, $post)
 | 
			
		||||
    public static function savePost($dbh, $userId, $post, $articleId = null)
 | 
			
		||||
    {
 | 
			
		||||
        $sql = 'INSERT INTO posts (
 | 
			
		||||
                `id`, `url`, `user_id`, `actor`, `type`, `object`, `to`, `cc`, `published`
 | 
			
		||||
            ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
 | 
			
		||||
                `id`, `url`, `user_id`, `actor`, `type`, `object`, `to`, `cc`, `published`, `article_id`
 | 
			
		||||
            ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
 | 
			
		||||
            ON DUPLICATE KEY UPDATE
 | 
			
		||||
                `url` = VALUES(`url`),
 | 
			
		||||
                `user_id` = VALUES(`user_id`),
 | 
			
		||||
| 
						 | 
				
			
			@ -231,7 +233,8 @@ class Posts
 | 
			
		|||
                `object` = VALUES(`object`),
 | 
			
		||||
                `to` = VALUES(`to`),
 | 
			
		||||
                `cc` = VALUES(`cc`),
 | 
			
		||||
                `published` = VALUES(`published`)';
 | 
			
		||||
                `published` = VALUES(`published`),
 | 
			
		||||
                `article_id` = VALUES(`article_id`)';
 | 
			
		||||
        $stmt = $dbh->prepare($sql);
 | 
			
		||||
        if ($stmt === false) {
 | 
			
		||||
            throw new \Federator\Exceptions\ServerError();
 | 
			
		||||
| 
						 | 
				
			
			@ -248,6 +251,9 @@ class Posts
 | 
			
		|||
        if ($objectJson === false) {
 | 
			
		||||
            $objectJson = null;
 | 
			
		||||
        }
 | 
			
		||||
        if (is_object($object)) {
 | 
			
		||||
            $id = $object->getID();
 | 
			
		||||
        }
 | 
			
		||||
        $to = $post->getTo();
 | 
			
		||||
        $cc = $post->getCC();
 | 
			
		||||
        $toJson = is_array($to) ? json_encode($to) : (is_string($to) ? json_encode([$to]) : null);
 | 
			
		||||
| 
						 | 
				
			
			@ -256,7 +262,7 @@ class Posts
 | 
			
		|||
        $publishedStr = $published ? gmdate('Y-m-d H:i:s', $published) : gmdate('Y-m-d H:i:s');
 | 
			
		||||
 | 
			
		||||
        $stmt->bind_param(
 | 
			
		||||
            "sssssssss",
 | 
			
		||||
            "ssssssssss",
 | 
			
		||||
            $id,
 | 
			
		||||
            $url,
 | 
			
		||||
            $userId,
 | 
			
		||||
| 
						 | 
				
			
			@ -265,7 +271,8 @@ class Posts
 | 
			
		|||
            $objectJson,
 | 
			
		||||
            $toJson,
 | 
			
		||||
            $ccJson,
 | 
			
		||||
            $publishedStr
 | 
			
		||||
            $publishedStr,
 | 
			
		||||
            $articleId,
 | 
			
		||||
        );
 | 
			
		||||
        $result = $stmt->execute();
 | 
			
		||||
        $stmt->close();
 | 
			
		||||
| 
						 | 
				
			
			@ -292,4 +299,40 @@ class Posts
 | 
			
		|||
        $stmt->close();
 | 
			
		||||
        return $affectedRows > 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** retrieve original article id of post
 | 
			
		||||
     *
 | 
			
		||||
     * @param \mysqli $dbh
 | 
			
		||||
     * @param \Federator\Data\ActivityPub\Common\Activity $post
 | 
			
		||||
     * @return string|null
 | 
			
		||||
     */
 | 
			
		||||
    public static function getOriginalArticleId($dbh, $post)
 | 
			
		||||
    {
 | 
			
		||||
        $sql = 'SELECT `article_id` FROM posts WHERE id = ?';
 | 
			
		||||
        $stmt = $dbh->prepare($sql);
 | 
			
		||||
        if ($stmt === false) {
 | 
			
		||||
            throw new \Federator\Exceptions\ServerError();
 | 
			
		||||
        }
 | 
			
		||||
        $id = $post->getID();
 | 
			
		||||
        $object = $post->getObject();
 | 
			
		||||
        if (is_object($object)) {
 | 
			
		||||
            $inReplyTo = $object->getInReplyTo();
 | 
			
		||||
            if ($inReplyTo !== "") {
 | 
			
		||||
                $id = $inReplyTo; // Use inReplyTo as ID if it's a string
 | 
			
		||||
            } else {
 | 
			
		||||
                $id = $object->getObject();
 | 
			
		||||
            }
 | 
			
		||||
        } elseif (is_string($object)) {
 | 
			
		||||
            $id = $object; // If object is a string, use it directly
 | 
			
		||||
        }
 | 
			
		||||
        $stmt->bind_param("s", $id);
 | 
			
		||||
        $articleId = null;
 | 
			
		||||
        $ret = $stmt->bind_result($articleId);
 | 
			
		||||
        $stmt->execute();
 | 
			
		||||
        if ($ret) {
 | 
			
		||||
            $stmt->fetch();
 | 
			
		||||
        }
 | 
			
		||||
        $stmt->close();
 | 
			
		||||
        return $articleId;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -56,6 +56,7 @@ class NewContentJob extends \Federator\Api
 | 
			
		|||
        $user = $this->args['user'];
 | 
			
		||||
        $recipientId = $this->args['recipientId'];
 | 
			
		||||
        $activity = $this->args['activity'];
 | 
			
		||||
        $articleId = $this->args['articleId'] ?? null;
 | 
			
		||||
 | 
			
		||||
        $activity = \Federator\Data\ActivityPub\Factory::newActivityFromJson($activity);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -67,7 +68,7 @@ class NewContentJob extends \Federator\Api
 | 
			
		|||
 | 
			
		||||
        $ourUrl = 'https://' . $domain;
 | 
			
		||||
 | 
			
		||||
        \Federator\Api\V1\NewContent::postForUser($this->dbh, $this->connector, $this->cache, $ourUrl, $user, $recipientId, $activity);
 | 
			
		||||
        \Federator\Api\V1\NewContent::postForUser($this->dbh, $this->connector, $this->cache, $ourUrl, $user, $recipientId, $activity, $articleId);
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -368,9 +368,11 @@ class ContentNation implements Connector
 | 
			
		|||
     * Convert jsonData to Activity format
 | 
			
		||||
     *
 | 
			
		||||
     * @param array<string, mixed> $jsonData the json data from our platfrom
 | 
			
		||||
     * @param string $articleId the original id of the article (if applicable)
 | 
			
		||||
     *                           (used to identify the article in the remote system)
 | 
			
		||||
     * @return \Federator\Data\ActivityPub\Common\Activity|false
 | 
			
		||||
     */
 | 
			
		||||
    public function jsonToActivity($jsonData)
 | 
			
		||||
    public function jsonToActivity($jsonData, &$articleId)
 | 
			
		||||
    {
 | 
			
		||||
        $returnActivity = false;
 | 
			
		||||
        // Common fields for all activity types
 | 
			
		||||
| 
						 | 
				
			
			@ -482,6 +484,7 @@ class ContentNation implements Connector
 | 
			
		|||
                        $returnActivity->setID($ap['id']);
 | 
			
		||||
                        $returnActivity->setURL($ap['url']);
 | 
			
		||||
                    }
 | 
			
		||||
                    $articleId = $jsonData['object']['id']; // Set the article ID for the activity
 | 
			
		||||
                    break;
 | 
			
		||||
 | 
			
		||||
                case 'comment':
 | 
			
		||||
| 
						 | 
				
			
			@ -514,6 +517,7 @@ class ContentNation implements Connector
 | 
			
		|||
                        $returnActivity->setID($ap['id']);
 | 
			
		||||
                        $returnActivity->setURL($ap['url']);
 | 
			
		||||
                    }
 | 
			
		||||
                    $articleId = $jsonData['object']['articleId']; // Set the article ID for the activity
 | 
			
		||||
                    break;
 | 
			
		||||
 | 
			
		||||
                case 'vote':
 | 
			
		||||
| 
						 | 
				
			
			@ -548,6 +552,7 @@ class ContentNation implements Connector
 | 
			
		|||
                        $returnActivity->setID($ap['id']);
 | 
			
		||||
                        $returnActivity->setURL($ap['url']);
 | 
			
		||||
                    }
 | 
			
		||||
                    $articleId = $jsonData['object']['articleId']; // Set the article ID for the activity
 | 
			
		||||
                    break;
 | 
			
		||||
 | 
			
		||||
                default:
 | 
			
		||||
| 
						 | 
				
			
			@ -659,6 +664,250 @@ class ContentNation implements Connector
 | 
			
		|||
        return $returnJson;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * send CN-friendly json from ActivityPub activity
 | 
			
		||||
     *
 | 
			
		||||
     * @param \Federator\Data\FedUser $sender the user of the sender
 | 
			
		||||
     * @param \Federator\Data\ActivityPub\Common\Activity $activity the activity
 | 
			
		||||
     * @return boolean did we successfully send the activity?
 | 
			
		||||
     */
 | 
			
		||||
    public function sendActivity($sender, $activity)
 | 
			
		||||
    {
 | 
			
		||||
        $targetUrl = $this->service;
 | 
			
		||||
        // Convert ActivityPub activity to ContentNation JSON format and retrieve target url
 | 
			
		||||
        $jsonData = self::activityToJson($this->main->getDatabase(), $this->service, $activity, $targetUrl);
 | 
			
		||||
 | 
			
		||||
        if ($jsonData === false) {
 | 
			
		||||
            error_log("ContentNation::sendActivity failed to convert activity to JSON");
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $json = json_encode($jsonData, 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($targetUrl);
 | 
			
		||||
        if ($parsed === false) {
 | 
			
		||||
            throw new \Exception('Failed to parse URL: ' . $targetUrl);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!isset($parsed['host']) || !isset($parsed['path'])) {
 | 
			
		||||
            throw new \Exception('Invalid target 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}";
 | 
			
		||||
 | 
			
		||||
        $pKeyPath = PROJECT_ROOT . '/' . $this->main->getConfig()['keys']['federatorPrivateKeyPath'];
 | 
			
		||||
        $privateKeyPem = file_get_contents($pKeyPath);
 | 
			
		||||
        if ($privateKeyPem === false) {
 | 
			
		||||
            http_response_code(500);
 | 
			
		||||
            throw new \Federator\Exceptions\PermissionDenied("Private key couldn't be determined");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $pkeyId = openssl_pkey_get_private($privateKeyPem);
 | 
			
		||||
 | 
			
		||||
        if ($pkeyId === false) {
 | 
			
		||||
            throw new \Exception('Invalid private key');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        openssl_sign($signatureString, $signature, $pkeyId, OPENSSL_ALGO_SHA256);
 | 
			
		||||
        $signature_b64 = base64_encode($signature);
 | 
			
		||||
 | 
			
		||||
        $signatureHeader = 'algorithm="rsa-sha256",headers="(request-target) host date digest",signature="' . $signature_b64 . '"';
 | 
			
		||||
 | 
			
		||||
        $ch = curl_init($targetUrl);
 | 
			
		||||
        if ($ch === false) {
 | 
			
		||||
            throw new \Exception('Failed to initialize cURL');
 | 
			
		||||
        }
 | 
			
		||||
        $headers = [
 | 
			
		||||
            'Host: ' . $extHost,
 | 
			
		||||
            'Date: ' . $date,
 | 
			
		||||
            'Digest: ' . $digest,
 | 
			
		||||
            'Content-Type: application/json',
 | 
			
		||||
            'Signature: ' . $signatureHeader,
 | 
			
		||||
            'Accept: application/json',
 | 
			
		||||
            'Username: ' . $sender->id,
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        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 true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Convert ActivityPub activity to ContentNation JSON format
 | 
			
		||||
     *
 | 
			
		||||
     * @param \mysqli $dbh database handle
 | 
			
		||||
     * @param string $serviceUrl the service URL
 | 
			
		||||
     * @param \Federator\Data\ActivityPub\Common\Activity $activity the activity
 | 
			
		||||
     * @param string $targetUrl the target URL for the activity
 | 
			
		||||
     * @return array<string, mixed>|false the json data or false on failure
 | 
			
		||||
     */
 | 
			
		||||
    private static function activityToJson($dbh, $serviceUrl, \Federator\Data\ActivityPub\Common\Activity $activity, string &$targetUrl)
 | 
			
		||||
    {
 | 
			
		||||
        $type = strtolower($activity->getType());
 | 
			
		||||
        switch ($type) {
 | 
			
		||||
            case 'create':
 | 
			
		||||
            case 'update':
 | 
			
		||||
                $object = $activity->getObject();
 | 
			
		||||
                if (is_object($object)) {
 | 
			
		||||
                    $objType = strtolower($object->getType());
 | 
			
		||||
                    $articleId = \Federator\DIO\Posts::getOriginalArticleId($dbh, $activity);
 | 
			
		||||
                    if ($articleId === null) {
 | 
			
		||||
                        error_log("ContentNation::activityToJson Failed to get original article ID for create/update activity");
 | 
			
		||||
                    }
 | 
			
		||||
                    switch ($objType) {
 | 
			
		||||
                        case 'article':
 | 
			
		||||
                            // We don't support article create/update at this point in time
 | 
			
		||||
                            error_log("ContentNation::activityToJson Unsupported create/update object type: {$objType}");
 | 
			
		||||
                            break;
 | 
			
		||||
                        case 'note':
 | 
			
		||||
                            $targetUrl = $serviceUrl . '/api/article/' . $articleId . '/comment';
 | 
			
		||||
                            $type = 'comment';
 | 
			
		||||
                            $inReplyTo = $object->getInReplyTo();
 | 
			
		||||
                            if ($inReplyTo !== '') {
 | 
			
		||||
                                $target = $inReplyTo;
 | 
			
		||||
                            } else {
 | 
			
		||||
                                $target = $object->getObject();
 | 
			
		||||
                            }
 | 
			
		||||
                            $comment = null;
 | 
			
		||||
                            if (is_string($target)) {
 | 
			
		||||
                                if (strpos($target, '#') !== false) {
 | 
			
		||||
                                    $parts = explode('#', $target);
 | 
			
		||||
                                    if (count($parts) > 0) {
 | 
			
		||||
                                        $comment = $parts[count($parts) - 1];
 | 
			
		||||
                                    }
 | 
			
		||||
                                }
 | 
			
		||||
                            } else {
 | 
			
		||||
                                error_log("ContentNation::activityToJson Unsupported target type for comment with id: " . $activity->getID() . " Type: " . gettype($target));
 | 
			
		||||
                                return false;
 | 
			
		||||
                            }
 | 
			
		||||
                            return [
 | 
			
		||||
                                'type' => $type,
 | 
			
		||||
                                'id' => $activity->getID(),
 | 
			
		||||
                                'parent' => $comment,
 | 
			
		||||
                                'subject' => $object->getSummary(),
 | 
			
		||||
                                'comment' => $object->getContent(),
 | 
			
		||||
                            ];
 | 
			
		||||
                        default:
 | 
			
		||||
                            error_log("ContentNation::activityToJson Unsupported create/update object type: {$objType}");
 | 
			
		||||
                            return false;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            case 'like':
 | 
			
		||||
            case 'dislike':
 | 
			
		||||
                $articleId = \Federator\DIO\Posts::getOriginalArticleId($dbh, $activity);
 | 
			
		||||
                if ($articleId === null) {
 | 
			
		||||
                    error_log("ContentNation::activityToJson Failed to get original article ID for vote activity");
 | 
			
		||||
                }
 | 
			
		||||
                $voteValue = $type === 'like' ? true : false;
 | 
			
		||||
                $activityType = 'vote';
 | 
			
		||||
                $inReplyTo = $activity->getInReplyTo();
 | 
			
		||||
                if ($inReplyTo !== '') {
 | 
			
		||||
                    $target = $inReplyTo;
 | 
			
		||||
                } else {
 | 
			
		||||
                    $target = $activity->getObject();
 | 
			
		||||
                }
 | 
			
		||||
                $comment = null;
 | 
			
		||||
                if (is_string($target)) {
 | 
			
		||||
                    if (strpos($target, '#') !== false) {
 | 
			
		||||
                        $parts = explode('#', $target);
 | 
			
		||||
                        if (count($parts) > 0) {
 | 
			
		||||
                            $comment = $parts[count($parts) - 1];
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                } else {
 | 
			
		||||
                    error_log("ContentNation::activityToJson Unsupported target type for vote with id: " . $activity->getID() . " Type: " . gettype($target));
 | 
			
		||||
                    return false;
 | 
			
		||||
                }
 | 
			
		||||
                $targetUrl = $serviceUrl . '/api/article/' . $articleId . '/vote';
 | 
			
		||||
                return [
 | 
			
		||||
                    'vote' => $voteValue,
 | 
			
		||||
                    'type' => $activityType,
 | 
			
		||||
                    'id' => $activity->getID(),
 | 
			
		||||
                    'comment' => $comment,
 | 
			
		||||
                ];
 | 
			
		||||
 | 
			
		||||
            case 'undo':
 | 
			
		||||
                $object = $activity->getObject();
 | 
			
		||||
                if (is_object($object)) {
 | 
			
		||||
                    $objType = strtolower($object->getType());
 | 
			
		||||
                    switch ($objType) {
 | 
			
		||||
                        case 'like':
 | 
			
		||||
                        case 'dislike':
 | 
			
		||||
                            $articleId = \Federator\DIO\Posts::getOriginalArticleId($dbh, $activity);
 | 
			
		||||
                            if ($articleId === null) {
 | 
			
		||||
                                error_log("ContentNation::activityToJson Failed to get original article ID for undo vote activity");
 | 
			
		||||
                            }
 | 
			
		||||
                            $activityType = 'vote';
 | 
			
		||||
                            $inReplyTo = $object->getInReplyTo();
 | 
			
		||||
                            if ($inReplyTo !== '') {
 | 
			
		||||
                                $target = $inReplyTo;
 | 
			
		||||
                            } else {
 | 
			
		||||
                                $target = $object->getObject();
 | 
			
		||||
                            }
 | 
			
		||||
                            $comment = null;
 | 
			
		||||
                            if (is_string($target)) {
 | 
			
		||||
                                if (strpos($target, '#') !== false) {
 | 
			
		||||
                                    $parts = explode('#', $target);
 | 
			
		||||
                                    if (count($parts) > 0) {
 | 
			
		||||
                                        $comment = $parts[count($parts) - 1];
 | 
			
		||||
                                    }
 | 
			
		||||
                                }
 | 
			
		||||
                            } else {
 | 
			
		||||
                                error_log("ContentNation::activityToJson Unsupported target type for undo vote with id: " . $activity->getID() . " Type: " . gettype($target));
 | 
			
		||||
                                return false;
 | 
			
		||||
                            }
 | 
			
		||||
                            $targetUrl = $serviceUrl . '/api/article/' . $articleId . '/vote';
 | 
			
		||||
                            return [
 | 
			
		||||
                                'vote' => null,
 | 
			
		||||
                                'type' => $activityType,
 | 
			
		||||
                                'id' => $object->getID(),
 | 
			
		||||
                                'comment' => $comment,
 | 
			
		||||
                            ];
 | 
			
		||||
                        case 'note':
 | 
			
		||||
                            // We don't support comment deletions at this point in time
 | 
			
		||||
                            error_log("ContentNation::activityToJson Unsupported undo object type: {$objType}");
 | 
			
		||||
                            break;
 | 
			
		||||
                        default:
 | 
			
		||||
                            error_log("ContentNation::activityToJson Unsupported create/update object type: {$objType}");
 | 
			
		||||
                            return false;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                break;
 | 
			
		||||
            default:
 | 
			
		||||
                error_log("ContentNation::activityToJson Unsupported activity type: {$type}");
 | 
			
		||||
                return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * check if the headers include a valid signature
 | 
			
		||||
     *
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -73,9 +73,11 @@ class DummyConnector implements Connector
 | 
			
		|||
     * Convert jsonData to Activity format
 | 
			
		||||
     *
 | 
			
		||||
     * @param array<string, mixed> $jsonData the json data from our platfrom @unused-param
 | 
			
		||||
     * @param string $articleId the original id of the article (if applicable)
 | 
			
		||||
     *                           (used to identify the article in the remote system) @unused-param
 | 
			
		||||
     * @return \Federator\Data\ActivityPub\Common\Activity|false
 | 
			
		||||
     */
 | 
			
		||||
    public function jsonToActivity(array $jsonData) {
 | 
			
		||||
    public function jsonToActivity(array $jsonData, &$articleId) {
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -108,6 +110,18 @@ class DummyConnector implements Connector
 | 
			
		|||
        return $user;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * send target-friendly json from ActivityPub activity
 | 
			
		||||
     *
 | 
			
		||||
     * @param \Federator\Data\FedUser $sender the user of the sender @unused-param
 | 
			
		||||
     * @param \Federator\Data\ActivityPub\Common\Activity $activity the activity @unused-param
 | 
			
		||||
     * @return boolean did we successfully send the activity?
 | 
			
		||||
     */
 | 
			
		||||
    public function sendActivity($sender, $activity)
 | 
			
		||||
    {
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * check if the headers include a valid signature
 | 
			
		||||
     *
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -125,9 +125,11 @@ class RedisCache implements Cache
 | 
			
		|||
     * Convert jsonData to Activity format
 | 
			
		||||
     *
 | 
			
		||||
     * @param array<string, mixed> $jsonData the json data from our platfrom @unused-param
 | 
			
		||||
     * @param string $articleId the original id of the article (if applicable)
 | 
			
		||||
     *                           (used to identify the article in the remote system) @unused-param
 | 
			
		||||
     * @return \Federator\Data\ActivityPub\Common\Activity|false
 | 
			
		||||
     */
 | 
			
		||||
    public function jsonToActivity(array $jsonData)
 | 
			
		||||
    public function jsonToActivity(array $jsonData, &$articleId)
 | 
			
		||||
    {
 | 
			
		||||
        error_log("rediscache::jsonToActivity not implemented");
 | 
			
		||||
        return false;
 | 
			
		||||
| 
						 | 
				
			
			@ -353,6 +355,18 @@ class RedisCache implements Cache
 | 
			
		|||
        $this->redis->setEx($key, $this->publicKeyPemTTL, $publicKeyPem);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * send target-friendly json from ActivityPub activity
 | 
			
		||||
     *
 | 
			
		||||
     * @param \Federator\Data\FedUser $sender the user of the sender @unused-param
 | 
			
		||||
     * @param \Federator\Data\ActivityPub\Common\Activity $activity the activity @unused-param
 | 
			
		||||
     * @return boolean did we successfully send the activity?
 | 
			
		||||
     */
 | 
			
		||||
    public function sendActivity($sender, $activity)
 | 
			
		||||
    {
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * check if the headers include a valid signature
 | 
			
		||||
     *
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										2
									
								
								sql/2025-05-27.sql
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								sql/2025-05-27.sql
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,2 @@
 | 
			
		|||
alter table posts add `article_id` varchar(255) null default null comment 'The optional original article id (of non-federated system, e.g. CN)';
 | 
			
		||||
update settings set `value`="2025-05-27" where `key`="database_version";
 | 
			
		||||
		Loading…
	
	Add table
		
		Reference in a new issue