forked from grumpydevelop/federator
		
	- 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)
		
			
				
	
	
		
			441 lines
		
	
	
	
		
			17 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			441 lines
		
	
	
	
		
			17 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
<?php
 | 
						|
/**
 | 
						|
 * SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop
 | 
						|
 * SPDX-License-Identifier: GPL-3.0-or-later
 | 
						|
 *
 | 
						|
 * @author Yannis Vogel (vogeldevelopment)
 | 
						|
 **/
 | 
						|
 | 
						|
namespace Federator\Api\FedUsers;
 | 
						|
 | 
						|
/**
 | 
						|
 * handle activitypub inbox requests
 | 
						|
 */
 | 
						|
class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
 | 
						|
{
 | 
						|
    /**
 | 
						|
     * main instance
 | 
						|
     *
 | 
						|
     * @var \Federator\Api $main
 | 
						|
     */
 | 
						|
    private $main;
 | 
						|
 | 
						|
    /**
 | 
						|
     * constructor
 | 
						|
     * @param \Federator\Api $main api main instance
 | 
						|
     */
 | 
						|
    public function __construct($main)
 | 
						|
    {
 | 
						|
        $this->main = $main;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * handle get call
 | 
						|
     *
 | 
						|
     * @param string|null $_user user to fetch inbox for @unused-param
 | 
						|
     * @return string|false response
 | 
						|
     */
 | 
						|
    public function get($_user)
 | 
						|
    {
 | 
						|
        return false;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * handle post call
 | 
						|
     *
 | 
						|
     * @param string|null $_user user to add data to inbox
 | 
						|
     * @return string|false response
 | 
						|
     */
 | 
						|
    public function post($_user)
 | 
						|
    {
 | 
						|
        $_rawInput = file_get_contents('php://input');
 | 
						|
 | 
						|
        $allHeaders = getallheaders();
 | 
						|
        try {
 | 
						|
            $this->main->checkSignature($allHeaders);
 | 
						|
        } catch (\Federator\Exceptions\PermissionDenied $e) {
 | 
						|
            throw new \Federator\Exceptions\Unauthorized("Inbox::post Signature check failed: " . $e->getMessage());
 | 
						|
        }
 | 
						|
 | 
						|
        $activity = is_string($_rawInput) ? json_decode($_rawInput, true) : null;
 | 
						|
 | 
						|
        $dbh = $this->main->getDatabase();
 | 
						|
        $cache = $this->main->getCache();
 | 
						|
        $connector = $this->main->getConnector();
 | 
						|
 | 
						|
        $config = $this->main->getConfig();
 | 
						|
 | 
						|
        if (!is_array($activity)) {
 | 
						|
            throw new \Federator\Exceptions\ServerError("Inbox::post Input wasn't of type array");
 | 
						|
        }
 | 
						|
 | 
						|
        $inboxActivity = \Federator\Data\ActivityPub\Factory::newActivityFromJson($activity);
 | 
						|
 | 
						|
        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);
 | 
						|
        $userId = $username . '@' . $domain;
 | 
						|
        $user = \Federator\DIO\FedUser::getUserByName(
 | 
						|
            $dbh,
 | 
						|
            $userId,
 | 
						|
            $cache
 | 
						|
        );
 | 
						|
        if ($user === null || $user->id === null) {
 | 
						|
            error_log("Inbox::post couldn't find user: $userId");
 | 
						|
            throw new \Federator\Exceptions\ServerError("Inbox::post couldn't find user: $userId");
 | 
						|
        }
 | 
						|
 | 
						|
        $users = [];
 | 
						|
 | 
						|
        $receivers = array_merge($inboxActivity->getTo(), $inboxActivity->getCC());
 | 
						|
 | 
						|
        // For Undo, the object may hold the proper to/cc
 | 
						|
        if ($inboxActivity->getType() === 'Undo') {
 | 
						|
            $object = $inboxActivity->getObject();
 | 
						|
            if ($object !== null && is_object($object)) {
 | 
						|
                $receivers = array_merge($object->getTo(), $object->getCC());
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        // Filter out the public address and keep only actual URLs
 | 
						|
        $receivers = array_filter($receivers, static function (mixed $receiver): bool {
 | 
						|
            return is_string($receiver)
 | 
						|
                && $receiver !== 'https://www.w3.org/ns/activitystreams#Public'
 | 
						|
                && (filter_var($receiver, FILTER_VALIDATE_URL) !== false);
 | 
						|
        });
 | 
						|
 | 
						|
        if (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
 | 
						|
            $object = $inboxActivity->getObject();
 | 
						|
            if ($object !== null && is_string($object)) {
 | 
						|
                $receivers[] = $object;
 | 
						|
            }
 | 
						|
        } elseif (strtolower($inboxActivity->getType()) === 'undo') {
 | 
						|
            $object = $inboxActivity->getObject();
 | 
						|
            if ($object !== null && is_object($object)) {
 | 
						|
                // For Undo, the objects object should hold the target
 | 
						|
                if (strtolower($object->getType()) === 'follow') {
 | 
						|
                    $objObject = $object->getObject();
 | 
						|
                    if ($objObject !== null && is_string($objObject)) {
 | 
						|
                        $receivers[] = $objObject;
 | 
						|
                    }
 | 
						|
                }
 | 
						|
            }
 | 
						|
        }
 | 
						|
        $ourDomain = $config['generic']['externaldomain'];
 | 
						|
 | 
						|
        foreach ($receivers as $receiver) {
 | 
						|
            if ($receiver === '' || !is_string($receiver)) {
 | 
						|
                continue;
 | 
						|
            }
 | 
						|
 | 
						|
            if (str_ends_with($receiver, '/followers')) {
 | 
						|
                $actor = $inboxActivity->getAActor();
 | 
						|
                if ($actor === null || !is_string($actor)) {
 | 
						|
                    error_log("Inbox::post no actor found");
 | 
						|
                    continue;
 | 
						|
                }
 | 
						|
 | 
						|
                // Extract username from the actor URL
 | 
						|
                $username = basename((string) (parse_url($actor, PHP_URL_PATH) ?? ''));
 | 
						|
                $domain = parse_url($actor, PHP_URL_HOST);
 | 
						|
                if ($username === null || $domain === null) {
 | 
						|
                    error_log("Inbox::post no username or domain found for recipient: $receiver");
 | 
						|
                    continue;
 | 
						|
                }
 | 
						|
                try {
 | 
						|
                    $followers = \Federator\DIO\Followers::getFollowersByFedUser($dbh, $connector, $cache, $username . '@' . $domain);
 | 
						|
                } catch (\Throwable $e) {
 | 
						|
                    error_log("Inbox::post get followers for user: " . $username . '@' . $domain . ". Exception: " . $e->getMessage());
 | 
						|
                    continue;
 | 
						|
                }
 | 
						|
 | 
						|
                if (is_array($followers)) {
 | 
						|
                    $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,
 | 
						|
                        $receiver,
 | 
						|
                        $connector,
 | 
						|
                        $cache
 | 
						|
                    );
 | 
						|
                } catch (\Throwable $e) {
 | 
						|
                    error_log("Inbox::post get user by name: " . $receiver . ". Exception: " . $e->getMessage());
 | 
						|
                    continue;
 | 
						|
                }
 | 
						|
                if ($localUser === null || $localUser->id === null) {
 | 
						|
                    error_log("Inbox::post couldn't find user: $receiver");
 | 
						|
                    continue;
 | 
						|
                }
 | 
						|
                $users[] = $localUser->id;
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        if (empty($users)) { // todo remove after proper implementation, debugging for now
 | 
						|
            $rootDir = PROJECT_ROOT . '/';
 | 
						|
            // Save the raw input and parsed JSON to a file for inspection
 | 
						|
            file_put_contents(
 | 
						|
                $rootDir . 'logs/inbox.log',
 | 
						|
                date('Y-m-d H:i:s') . ": ==== POST Inbox Activity ====\n" . json_encode($inboxActivity, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) . "\n\n",
 | 
						|
                FILE_APPEND
 | 
						|
            );
 | 
						|
        }
 | 
						|
 | 
						|
        foreach ($users as $receiver) {
 | 
						|
            if (!isset($receiver)) {
 | 
						|
                continue;
 | 
						|
            }
 | 
						|
            $token = \Resque::enqueue('inbox', 'Federator\\Jobs\\InboxJob', [
 | 
						|
                'user' => $user->id,
 | 
						|
                'recipientId' => $receiver,
 | 
						|
                'activity' => $inboxActivity->toObject(),
 | 
						|
            ]);
 | 
						|
            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";
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * 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 user: $_recipientId");
 | 
						|
            return false;
 | 
						|
        }
 | 
						|
 | 
						|
        $rootDir = PROJECT_ROOT . '/';
 | 
						|
        // Save the raw input and parsed JSON to a file for inspection
 | 
						|
        file_put_contents(
 | 
						|
            $rootDir . 'logs/inbox_' . $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, 'dislike');
 | 
						|
                            \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, 'dislike');
 | 
						|
                    \Federator\DIO\Posts::savePost($dbh, $user->id, $inboxActivity);
 | 
						|
                } 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;
 | 
						|
    }
 | 
						|
}
 |