forked from grumpydevelop/federator
		
	integrate queue for NewContent
- also integrated better support for newContent types - Integrated saving posts to database (posts gotten via outbox request as well as posts received in the NewContent endpoint) - proper support for handling comments - support for likes/dislikes - support for requesting followers / following endpoints - better inbox support database needs changes, don't forget to run migration
This commit is contained in:
		
							parent
							
								
									767f51cc5b
								
							
						
					
					
						commit
						d355b5a7cd
					
				
					 25 changed files with 1363 additions and 160 deletions
				
			
		| 
						 | 
				
			
			@ -70,10 +70,10 @@ class FedUsers implements APIInterface
 | 
			
		|||
                // /users/username/(inbox|outbox|following|followers)
 | 
			
		||||
                switch ($paths[2]) {
 | 
			
		||||
                    case 'following':
 | 
			
		||||
                        // $handler = new FedUsers\Following();
 | 
			
		||||
                        $handler = new FedUsers\Following($this->main);
 | 
			
		||||
                        break;
 | 
			
		||||
                    case 'followers':
 | 
			
		||||
                        // $handler = new FedUsers\Followers();
 | 
			
		||||
                        $handler = new FedUsers\Followers($this->main);
 | 
			
		||||
                        break;
 | 
			
		||||
                    case 'inbox':
 | 
			
		||||
                        $handler = new FedUsers\Inbox($this->main);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										120
									
								
								php/federator/api/fedusers/followers.php
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								php/federator/api/fedusers/followers.php
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,120 @@
 | 
			
		|||
<?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 followers requests
 | 
			
		||||
 */
 | 
			
		||||
class Followers implements \Federator\Api\FedUsers\FedUsersInterface
 | 
			
		||||
{
 | 
			
		||||
    /**
 | 
			
		||||
     * main instance
 | 
			
		||||
     *
 | 
			
		||||
     * @var \Federator\Api $main
 | 
			
		||||
     */
 | 
			
		||||
    private $main;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * constructor
 | 
			
		||||
     * @param \Federator\Api $main main instance
 | 
			
		||||
     */
 | 
			
		||||
    public function __construct($main)
 | 
			
		||||
    {
 | 
			
		||||
        $this->main = $main;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * handle get call
 | 
			
		||||
     *
 | 
			
		||||
     * @param string|null $_user user to fetch followers for
 | 
			
		||||
     * @return string|false response
 | 
			
		||||
     */
 | 
			
		||||
    public function get($_user)
 | 
			
		||||
    {
 | 
			
		||||
        if (!isset($_user)) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
        $dbh = $this->main->getDatabase();
 | 
			
		||||
        $cache = $this->main->getCache();
 | 
			
		||||
        $connector = $this->main->getConnector();
 | 
			
		||||
 | 
			
		||||
        // get user
 | 
			
		||||
        $user = \Federator\DIO\User::getUserByName(
 | 
			
		||||
            $dbh,
 | 
			
		||||
            $_user,
 | 
			
		||||
            $connector,
 | 
			
		||||
            $cache
 | 
			
		||||
        );
 | 
			
		||||
        if ($user->id === null) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $followers = new \Federator\Data\ActivityPub\Common\Followers();
 | 
			
		||||
        $followerItems = \Federator\DIO\Followers::getFollowersByUser($dbh, $user->id, $connector, $cache);
 | 
			
		||||
 | 
			
		||||
        $config = $this->main->getConfig();
 | 
			
		||||
        $domain = $config['generic']['externaldomain'];
 | 
			
		||||
        $baseUrl = 'https://' . $domain . '/' . $_user . '/followers';
 | 
			
		||||
 | 
			
		||||
        $pageSize = 10;
 | 
			
		||||
        $page = $this->main->extractFromURI("page", "");
 | 
			
		||||
        $id = $baseUrl;
 | 
			
		||||
        $items = [];
 | 
			
		||||
        $totalItems = count($followerItems);
 | 
			
		||||
 | 
			
		||||
        if ($page !== "") {
 | 
			
		||||
            $pageNum = max(0, (int) $page);
 | 
			
		||||
            $offset = (int)($pageNum * $pageSize);
 | 
			
		||||
            $pagedItems = array_slice($followerItems, $offset, $pageSize);
 | 
			
		||||
 | 
			
		||||
            foreach ($pagedItems as $follower) {
 | 
			
		||||
                $items[] = $follower->actorURL;
 | 
			
		||||
            }
 | 
			
		||||
            $followers->setItems($items);
 | 
			
		||||
            $id .= '?page=' . urlencode($page);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $followers->setID($id);
 | 
			
		||||
        $followers->setPartOf($baseUrl);
 | 
			
		||||
        $followers->setTotalItems($totalItems);
 | 
			
		||||
 | 
			
		||||
        // Pagination navigation
 | 
			
		||||
        $lastPage = max(0, ceil($totalItems / $pageSize) - 1);
 | 
			
		||||
 | 
			
		||||
        if ($page === "" || $followers->count() == 0) {
 | 
			
		||||
            $followers->setFirst($baseUrl . '?page=0');
 | 
			
		||||
            $followers->setLast($baseUrl . '?page=' . $lastPage);
 | 
			
		||||
        }
 | 
			
		||||
        if ($page !== "") {
 | 
			
		||||
            $pageNum = max(0, (int) $page);
 | 
			
		||||
            if ($pageNum < $lastPage) {
 | 
			
		||||
                $followers->setNext($baseUrl . '?page=' . ($pageNum + 1));
 | 
			
		||||
            }
 | 
			
		||||
            if ($pageNum > 0) {
 | 
			
		||||
                $followers->setPrev($baseUrl . '?page=' . ($pageNum - 1));
 | 
			
		||||
            }
 | 
			
		||||
            $followers->setFirst($baseUrl . '?page=0');
 | 
			
		||||
            $followers->setLast($baseUrl . '?page=' . $lastPage);
 | 
			
		||||
        }
 | 
			
		||||
        $obj = $followers->toObject();
 | 
			
		||||
 | 
			
		||||
        return json_encode($obj, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * handle post call
 | 
			
		||||
     *
 | 
			
		||||
     * @param string|null $_user user to add data to outbox @unused-param
 | 
			
		||||
     * @return string|false response
 | 
			
		||||
     */
 | 
			
		||||
    public function post($_user)
 | 
			
		||||
    {
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										120
									
								
								php/federator/api/fedusers/following.php
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								php/federator/api/fedusers/following.php
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,120 @@
 | 
			
		|||
<?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 following requests
 | 
			
		||||
 */
 | 
			
		||||
class Following implements \Federator\Api\FedUsers\FedUsersInterface
 | 
			
		||||
{
 | 
			
		||||
    /**
 | 
			
		||||
     * main instance
 | 
			
		||||
     *
 | 
			
		||||
     * @var \Federator\Api $main
 | 
			
		||||
     */
 | 
			
		||||
    private $main;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * constructor
 | 
			
		||||
     * @param \Federator\Api $main main instance
 | 
			
		||||
     */
 | 
			
		||||
    public function __construct($main)
 | 
			
		||||
    {
 | 
			
		||||
        $this->main = $main;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * handle get call
 | 
			
		||||
     *
 | 
			
		||||
     * @param string|null $_user user to fetch followers for
 | 
			
		||||
     * @return string|false response
 | 
			
		||||
     */
 | 
			
		||||
    public function get($_user)
 | 
			
		||||
    {
 | 
			
		||||
        if (!isset($_user)) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
        $dbh = $this->main->getDatabase();
 | 
			
		||||
        $cache = $this->main->getCache();
 | 
			
		||||
        $connector = $this->main->getConnector();
 | 
			
		||||
 | 
			
		||||
        // get user
 | 
			
		||||
        $user = \Federator\DIO\User::getUserByName(
 | 
			
		||||
            $dbh,
 | 
			
		||||
            $_user,
 | 
			
		||||
            $connector,
 | 
			
		||||
            $cache
 | 
			
		||||
        );
 | 
			
		||||
        if ($user->id === null) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $following = new \Federator\Data\ActivityPub\Common\Following();
 | 
			
		||||
        $followingItems = \Federator\DIO\Followers::getFollowingForUser($dbh, $user->id, $connector, $cache);
 | 
			
		||||
 | 
			
		||||
        $config = $this->main->getConfig();
 | 
			
		||||
        $domain = $config['generic']['externaldomain'];
 | 
			
		||||
        $baseUrl = 'https://' . $domain . '/users/' . $_user . '/following';
 | 
			
		||||
 | 
			
		||||
        $pageSize = 10;
 | 
			
		||||
        $page = $this->main->extractFromURI("page", "");
 | 
			
		||||
        $id = $baseUrl;
 | 
			
		||||
        $items = [];
 | 
			
		||||
        $totalItems = count($followingItems);
 | 
			
		||||
 | 
			
		||||
        if ($page !== "") {
 | 
			
		||||
            $pageNum = max(0, (int) $page);
 | 
			
		||||
            $offset = (int) ($pageNum * $pageSize);
 | 
			
		||||
            $pagedItems = array_slice($followingItems, $offset, $pageSize);
 | 
			
		||||
 | 
			
		||||
            foreach ($pagedItems as $followed) {
 | 
			
		||||
                $items[] = $followed->actorURL;
 | 
			
		||||
            }
 | 
			
		||||
            $following->setItems($items);
 | 
			
		||||
            $id .= '?page=' . urlencode($page);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $following->setID($id);
 | 
			
		||||
        $following->setPartOf($baseUrl);
 | 
			
		||||
        $following->setTotalItems($totalItems);
 | 
			
		||||
 | 
			
		||||
        // Pagination navigation
 | 
			
		||||
        $lastPage = max(0, ceil($totalItems / $pageSize) - 1);
 | 
			
		||||
 | 
			
		||||
        if ($page === "" || $following->count() == 0) {
 | 
			
		||||
            $following->setFirst($baseUrl . '?page=0');
 | 
			
		||||
            $following->setLast($baseUrl . '?page=' . $lastPage);
 | 
			
		||||
        }
 | 
			
		||||
        if ($page !== "") {
 | 
			
		||||
            $pageNum = max(0, (int) $page);
 | 
			
		||||
            if ($pageNum < $lastPage) {
 | 
			
		||||
                $following->setNext($baseUrl . '?page=' . ($pageNum + 1));
 | 
			
		||||
            }
 | 
			
		||||
            if ($pageNum > 0) {
 | 
			
		||||
                $following->setPrev($baseUrl . '?page=' . ($pageNum - 1));
 | 
			
		||||
            }
 | 
			
		||||
            $following->setFirst($baseUrl . '?page=0');
 | 
			
		||||
            $following->setLast($baseUrl . '?page=' . $lastPage);
 | 
			
		||||
        }
 | 
			
		||||
        $obj = $following->toObject();
 | 
			
		||||
 | 
			
		||||
        return json_encode($obj, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * handle post call
 | 
			
		||||
     *
 | 
			
		||||
     * @param string|null $_user user to add data to outbox @unused-param
 | 
			
		||||
     * @return string|false response
 | 
			
		||||
     */
 | 
			
		||||
    public function post($_user)
 | 
			
		||||
    {
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -148,22 +148,21 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
 | 
			
		|||
     */
 | 
			
		||||
    public static function postForUser($dbh, $connector, $cache, $_user, $inboxActivity)
 | 
			
		||||
    {
 | 
			
		||||
        if (isset($_user)) {
 | 
			
		||||
            // get user
 | 
			
		||||
            $user = \Federator\DIO\User::getUserByName(
 | 
			
		||||
                $dbh,
 | 
			
		||||
                $_user,
 | 
			
		||||
                $connector,
 | 
			
		||||
                $cache
 | 
			
		||||
            );
 | 
			
		||||
            if ($user === null || $user->id === null) {
 | 
			
		||||
                throw new \Federator\Exceptions\ServerError("Inbox::postForUser couldn't find user: $_user");
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            // Not a local user, nothing to do
 | 
			
		||||
        if (!isset($_user)) {
 | 
			
		||||
            error_log("Inbox::postForUser no user given");
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $user = \Federator\DIO\User::getUserByName(
 | 
			
		||||
            $dbh,
 | 
			
		||||
            $_user,
 | 
			
		||||
            $connector,
 | 
			
		||||
            $cache
 | 
			
		||||
        );
 | 
			
		||||
        if ($user === null || $user->id === null) {
 | 
			
		||||
            throw new \Federator\Exceptions\ServerError("Inbox::postForUser couldn't find user: $_user");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $rootDir = PROJECT_ROOT . '/';
 | 
			
		||||
        // Save the raw input and parsed JSON to a file for inspection
 | 
			
		||||
        file_put_contents(
 | 
			
		||||
| 
						 | 
				
			
			@ -174,40 +173,119 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
 | 
			
		|||
 | 
			
		||||
        $type = $inboxActivity->getType();
 | 
			
		||||
 | 
			
		||||
        if ($type === 'Follow') {
 | 
			
		||||
            // Someone wants to follow our user
 | 
			
		||||
            $actor = $inboxActivity->getAActor(); // The follower's actor URI
 | 
			
		||||
            if ($actor !== '') {
 | 
			
		||||
                // Extract follower username (you may need to adjust this logic)
 | 
			
		||||
                $followerUsername = basename((string) (parse_url($actor, PHP_URL_PATH) ?? ''));
 | 
			
		||||
                $followerDomain = parse_url($actor, PHP_URL_HOST);
 | 
			
		||||
                if (is_string($followerDomain)) {
 | 
			
		||||
                    $followerId = "{$followerUsername}@{$followerDomain}";
 | 
			
		||||
 | 
			
		||||
                    // Save the follow relationship
 | 
			
		||||
                    \Federator\DIO\Followers::addFollow($dbh, $followerId, $user->id, $followerDomain);
 | 
			
		||||
                    error_log("Inbox::postForUser: Added follower $followerId for user $user->id");
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        } elseif ($type === 'Undo') {
 | 
			
		||||
            // Check if this is an Undo of a Follow (i.e., Unfollow)
 | 
			
		||||
            $object = $inboxActivity->getObject();
 | 
			
		||||
            if (is_object($object) && method_exists($object, 'getType') && $object->getType() === 'Follow') {
 | 
			
		||||
                if ($object instanceof \Federator\Data\ActivityPub\Common\Activity) {
 | 
			
		||||
                    $actor = $object->getAActor();
 | 
			
		||||
                    if ($actor !== '') {
 | 
			
		||||
                        $followerUsername = basename((string) (parse_url($actor, PHP_URL_PATH) ?? ''));
 | 
			
		||||
                        $followerDomain = parse_url($actor, PHP_URL_HOST);
 | 
			
		||||
                        if (is_string($followerDomain)) {
 | 
			
		||||
                            $followerId = "{$followerUsername}@{$followerDomain}";
 | 
			
		||||
 | 
			
		||||
                            // Remove the follow relationship
 | 
			
		||||
                            \Federator\DIO\Followers::removeFollow($dbh, $followerId, $user->id);
 | 
			
		||||
                            error_log("Inbox::postForUser: Removed follower $followerId for user $user->id");
 | 
			
		||||
                        }
 | 
			
		||||
        switch ($type) {
 | 
			
		||||
            case 'Follow':
 | 
			
		||||
                $success = false;
 | 
			
		||||
                $actor = $inboxActivity->getAActor();
 | 
			
		||||
                if ($actor !== '') {
 | 
			
		||||
                    $followerUsername = basename((string) (parse_url($actor, PHP_URL_PATH) ?? ''));
 | 
			
		||||
                    $followerDomain = parse_url($actor, PHP_URL_HOST);
 | 
			
		||||
                    if (is_string($followerDomain)) {
 | 
			
		||||
                        $followerId = "{$followerUsername}@{$followerDomain}";
 | 
			
		||||
                        $success = \Federator\DIO\Followers::addFollow($dbh, $followerId, $user->id, $followerDomain);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
                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);
 | 
			
		||||
                }
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            case 'Undo':
 | 
			
		||||
                $object = $inboxActivity->getObject();
 | 
			
		||||
                if (is_object($object) && method_exists($object, 'getType')) {
 | 
			
		||||
                    switch ($object->getType()) {
 | 
			
		||||
                        case 'Follow':
 | 
			
		||||
                            $success = false;
 | 
			
		||||
                            if ($object instanceof \Federator\Data\ActivityPub\Common\Activity) {
 | 
			
		||||
                                $actor = $object->getAActor();
 | 
			
		||||
                                if ($actor !== '') {
 | 
			
		||||
                                    $followerUsername = basename((string) (parse_url($actor, PHP_URL_PATH) ?? ''));
 | 
			
		||||
                                    $followerDomain = parse_url($actor, PHP_URL_HOST);
 | 
			
		||||
                                    if (is_string($followerDomain)) {
 | 
			
		||||
                                        $followerId = "{$followerUsername}@{$followerDomain}";
 | 
			
		||||
                                        $success = \Federator\DIO\Followers::removeFollow($dbh, $followerId, $user->id);
 | 
			
		||||
                                    }
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                            if ($success === false) {
 | 
			
		||||
                                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");
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                            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':
 | 
			
		||||
                            // Undo Note (remove note)
 | 
			
		||||
                            if (method_exists($object, 'getID')) {
 | 
			
		||||
                                $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
 | 
			
		||||
                $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");
 | 
			
		||||
                    return false;
 | 
			
		||||
                }
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            case 'Note':
 | 
			
		||||
                // Post Note
 | 
			
		||||
                \Federator\DIO\Posts::savePost($dbh, $user->id, $inboxActivity);
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            default:
 | 
			
		||||
                error_log("Inbox::postForUser: Unhandled activity type $type for user $user->id");
 | 
			
		||||
                break;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return true;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -125,7 +125,9 @@ class NewContent implements \Federator\Api\APIInterface
 | 
			
		|||
 | 
			
		||||
        $users = [];
 | 
			
		||||
 | 
			
		||||
        $followers = $this->fetchAllFollowers($dbh, $connector, $cache, $user);
 | 
			
		||||
        if ($newActivity->getType() === 'Create') {
 | 
			
		||||
            $followers = $this->fetchAllFollowers($dbh, $connector, $cache, $user);
 | 
			
		||||
        }
 | 
			
		||||
        if (!empty($followers)) {
 | 
			
		||||
            $users = array_merge($users, $followers);
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			@ -148,7 +150,11 @@ class NewContent implements \Federator\Api\APIInterface
 | 
			
		|||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            $this->postForUser($dbh, $connector, $cache, $user, $newActivity);
 | 
			
		||||
            $token = \Resque::enqueue('inbox', 'Federator\\Jobs\\NewContentJob', [
 | 
			
		||||
                'user' => $user,
 | 
			
		||||
                'activity' => $newActivity->toObject(),
 | 
			
		||||
            ]);
 | 
			
		||||
            error_log("Inbox::post enqueued job for user: $user with token: $token");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return json_encode($newActivity, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
 | 
			
		||||
| 
						 | 
				
			
			@ -167,7 +173,7 @@ class NewContent implements \Federator\Api\APIInterface
 | 
			
		|||
     * @param \Federator\Data\ActivityPub\Common\Activity $newActivity the activity that we received
 | 
			
		||||
     * @return boolean response
 | 
			
		||||
     */
 | 
			
		||||
    private static function postForUser($dbh, $connector, $cache, $_user, $newActivity)
 | 
			
		||||
    public static function postForUser($dbh, $connector, $cache, $_user, $newActivity)
 | 
			
		||||
    {
 | 
			
		||||
        if (!isset($_user)) {
 | 
			
		||||
            error_log("NewContent::postForUser no user given");
 | 
			
		||||
| 
						 | 
				
			
			@ -181,7 +187,7 @@ class NewContent implements \Federator\Api\APIInterface
 | 
			
		|||
            $connector,
 | 
			
		||||
            $cache
 | 
			
		||||
        );
 | 
			
		||||
        if ($user->id === null) {
 | 
			
		||||
        if ($user === null || $user->id === null) {
 | 
			
		||||
            error_log("NewContent::postForUser couldn't find user: $_user");
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			@ -194,6 +200,127 @@ class NewContent implements \Federator\Api\APIInterface
 | 
			
		|||
            FILE_APPEND
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        $type = $newActivity->getType();
 | 
			
		||||
 | 
			
		||||
        switch ($type) {
 | 
			
		||||
            case 'Follow':
 | 
			
		||||
                $success = false;
 | 
			
		||||
                $actor = $newActivity->getAActor();
 | 
			
		||||
                if ($actor !== '') {
 | 
			
		||||
                    $followerUsername = basename((string) (parse_url($actor, PHP_URL_PATH) ?? ''));
 | 
			
		||||
                    $followerDomain = parse_url($actor, PHP_URL_HOST);
 | 
			
		||||
                    if (is_string($followerDomain)) {
 | 
			
		||||
                        $followerId = "{$followerUsername}@{$followerDomain}";
 | 
			
		||||
                        $success = \Federator\DIO\Followers::addFollow($dbh, $followerId, $user->id, $followerDomain);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                if ($success === false) {
 | 
			
		||||
                    error_log("NewContent::postForUser: Failed to add follower for user $user->id");
 | 
			
		||||
                }
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            case 'Delete':
 | 
			
		||||
                // Delete Note/Post
 | 
			
		||||
                $object = $newActivity->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 = $newActivity->getObject();
 | 
			
		||||
                if (is_object($object)) {
 | 
			
		||||
                    switch ($object->getType()) {
 | 
			
		||||
                        case 'Follow':
 | 
			
		||||
                            $success = false;
 | 
			
		||||
                            if ($object instanceof \Federator\Data\ActivityPub\Common\Activity) {
 | 
			
		||||
                                $actor = $object->getAActor();
 | 
			
		||||
                                if ($actor !== '') {
 | 
			
		||||
                                    $followerUsername = basename((string) (parse_url($actor, PHP_URL_PATH) ?? ''));
 | 
			
		||||
                                    $followerDomain = parse_url($actor, PHP_URL_HOST);
 | 
			
		||||
                                    if (is_string($followerDomain)) {
 | 
			
		||||
                                        $followerId = "{$followerUsername}@{$followerDomain}";
 | 
			
		||||
                                        $success = \Federator\DIO\Followers::removeFollow($dbh, $followerId, $user->id);
 | 
			
		||||
                                    }
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                            if ($success === false) {
 | 
			
		||||
                                error_log("NewContent::postForUser: Failed to remove follower for user $user->id");
 | 
			
		||||
                            }
 | 
			
		||||
                            break;
 | 
			
		||||
                        case 'Vote':
 | 
			
		||||
                            // Undo Vote (remove vote)
 | 
			
		||||
                            if (method_exists($object, 'getObject')) {
 | 
			
		||||
                                $targetId = $object->getObject();
 | 
			
		||||
                                if (is_string($targetId)) {
 | 
			
		||||
                                    // \Federator\DIO\Votes::removeVote($dbh, $user->id, $targetId);
 | 
			
		||||
                                    \Federator\DIO\Posts::deletePost($dbh, $targetId);
 | 
			
		||||
                                } else {
 | 
			
		||||
                                    error_log("NewContent::postForUser: Error in Undo Vote for user $user->id, targetId is not a string");
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                            break;
 | 
			
		||||
                        case 'Note':
 | 
			
		||||
                            // Undo Note (remove note)
 | 
			
		||||
                            if (method_exists($object, 'getID')) {
 | 
			
		||||
                                $noteId = $object->getID();
 | 
			
		||||
                                \Federator\DIO\Posts::deletePost($dbh, $noteId);
 | 
			
		||||
                            }
 | 
			
		||||
                            break;
 | 
			
		||||
                    }
 | 
			
		||||
                } else if (is_string($object)) {
 | 
			
		||||
                    \Federator\DIO\Posts::deletePost($dbh, $object);
 | 
			
		||||
                } else {
 | 
			
		||||
                    error_log("NewContent::postForUser: Error in Undo for user $user->id, object is not a string or object");
 | 
			
		||||
                }
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            case 'Like':
 | 
			
		||||
                // Add Like
 | 
			
		||||
                $targetId = $newActivity->getObject();
 | 
			
		||||
                if (is_string($targetId)) {
 | 
			
		||||
                    // \Federator\DIO\Votes::addVote($dbh, $user->id, $targetId, 'like');
 | 
			
		||||
                    \Federator\DIO\Posts::savePost($dbh, $user->id, $newActivity);
 | 
			
		||||
                } else {
 | 
			
		||||
                    error_log("NewContent::postForUser: Error in Add Like for user $user->id, targetId is not a string");
 | 
			
		||||
                    return false;
 | 
			
		||||
                }
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            case 'Dislike':
 | 
			
		||||
                // Add Dislike
 | 
			
		||||
                $targetId = $newActivity->getObject();
 | 
			
		||||
                if (is_string($targetId)) {
 | 
			
		||||
                    // \Federator\DIO\Votes::addVote($dbh, $user->id, $targetId, 'dislike');
 | 
			
		||||
                    \Federator\DIO\Posts::savePost($dbh, $user->id, $newActivity);
 | 
			
		||||
                } else {
 | 
			
		||||
                    error_log("NewContent::postForUser: Error in Add Dislike for user $user->id, targetId is not a string");
 | 
			
		||||
                    return false;
 | 
			
		||||
                }
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            case 'Create':
 | 
			
		||||
                $object = $newActivity->getObject();
 | 
			
		||||
                if (is_object($object)) {
 | 
			
		||||
                    switch ($object->getType()) {
 | 
			
		||||
                        case 'Note':
 | 
			
		||||
                        case 'Article':
 | 
			
		||||
                        default:
 | 
			
		||||
                            \Federator\DIO\Posts::savePost($dbh, $user->id, $newActivity);
 | 
			
		||||
                            break;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                // Post Note
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            default:
 | 
			
		||||
                error_log("NewContent::postForUser: Unhandled activity type $type for user $user->id");
 | 
			
		||||
                break;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										9
									
								
								php/federator/cache/cache.php
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										9
									
								
								php/federator/cache/cache.php
									
										
									
									
										vendored
									
									
								
							| 
						 | 
				
			
			@ -22,6 +22,15 @@ interface Cache extends \Federator\Connector\Connector
 | 
			
		|||
     */
 | 
			
		||||
    public function saveRemoteFollowersOfUser($user, $followers);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * save remote following for user
 | 
			
		||||
     *
 | 
			
		||||
     * @param string $user user name
 | 
			
		||||
     * @param \Federator\Data\FedUser[]|false $following user following
 | 
			
		||||
     * @return void
 | 
			
		||||
     */
 | 
			
		||||
    public function saveRemoteFollowingForUser($user, $following);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * save remote posts by user
 | 
			
		||||
     *
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -22,6 +22,15 @@ interface Connector
 | 
			
		|||
     */
 | 
			
		||||
    public function getRemoteFollowersOfUser($id);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * get following of given user
 | 
			
		||||
     *
 | 
			
		||||
     * @param string $id user id
 | 
			
		||||
 | 
			
		||||
     * @return \Federator\Data\FedUser[]|false
 | 
			
		||||
     */
 | 
			
		||||
    public function getRemoteFollowingForUser($id);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * get posts by given user
 | 
			
		||||
     *
 | 
			
		||||
| 
						 | 
				
			
			@ -29,7 +38,7 @@ interface Connector
 | 
			
		|||
     * @param string $min min date
 | 
			
		||||
     * @param string $max max date
 | 
			
		||||
 | 
			
		||||
     * @return \Federator\Data\ActivityPub\Common\APObject[]|false
 | 
			
		||||
     * @return \Federator\Data\ActivityPub\Common\Activity[]|false
 | 
			
		||||
     */
 | 
			
		||||
    public function getRemotePostsByUser($id, $min, $max);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -347,7 +347,7 @@ class APObject implements \JsonSerializable
 | 
			
		|||
    /**
 | 
			
		||||
     * set child object
 | 
			
		||||
     *
 | 
			
		||||
     * @param APObject $object
 | 
			
		||||
     * @param APObject|string $object
 | 
			
		||||
     * @return void
 | 
			
		||||
     */
 | 
			
		||||
    public function setObject($object)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -51,17 +51,27 @@ class Collection extends APObject
 | 
			
		|||
        return parent::fromJson($json);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function count() : int
 | 
			
		||||
    /**
 | 
			
		||||
     * set total items
 | 
			
		||||
     *
 | 
			
		||||
     * @param int $totalItems total items
 | 
			
		||||
     */
 | 
			
		||||
    public function setTotalItems(int $totalItems): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->totalItems = $totalItems;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function count(): int
 | 
			
		||||
    {
 | 
			
		||||
        return $this->totalItems;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function setFirst(string $url) : void
 | 
			
		||||
    public function setFirst(string $url): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->first = $url;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function setLast(string $url) : void
 | 
			
		||||
    public function setLast(string $url): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->last = $url;
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										18
									
								
								php/federator/data/activitypub/common/dislike.php
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								php/federator/data/activitypub/common/dislike.php
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,18 @@
 | 
			
		|||
<?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\Data\ActivityPub\Common;
 | 
			
		||||
 | 
			
		||||
class Dislike extends Activity
 | 
			
		||||
{
 | 
			
		||||
    public function __construct()
 | 
			
		||||
    {
 | 
			
		||||
        parent::__construct('Dislike');
 | 
			
		||||
        parent::addContext('https://www.w3.org/ns/activitystreams');
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										53
									
								
								php/federator/data/activitypub/common/followers.php
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								php/federator/data/activitypub/common/followers.php
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,53 @@
 | 
			
		|||
<?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\Data\ActivityPub\Common;
 | 
			
		||||
 | 
			
		||||
class Followers extends OrderedCollectionPage
 | 
			
		||||
{
 | 
			
		||||
    public function __construct()
 | 
			
		||||
    {
 | 
			
		||||
        parent::__construct();
 | 
			
		||||
        parent::addContext('https://www.w3.org/ns/activitystreams');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * set items
 | 
			
		||||
     *
 | 
			
		||||
     * @param string[] $items the items in the collection
 | 
			
		||||
     * @return void
 | 
			
		||||
     */
 | 
			
		||||
    public function setItems(&$items)
 | 
			
		||||
    {
 | 
			
		||||
        // Optionally: type check that all $items are Activity objects
 | 
			
		||||
        $this->items = $items;
 | 
			
		||||
        $this->totalItems = sizeof($items);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * convert internal state to php array
 | 
			
		||||
     *
 | 
			
		||||
     * @return array<string,mixed>
 | 
			
		||||
     */
 | 
			
		||||
    public function toObject()
 | 
			
		||||
    {
 | 
			
		||||
        $return = parent::toObject();
 | 
			
		||||
        return $return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * create object from json
 | 
			
		||||
     *
 | 
			
		||||
     * @param array<string,mixed> $json input json
 | 
			
		||||
     * @return bool true on success
 | 
			
		||||
     */
 | 
			
		||||
    public function fromJson($json)
 | 
			
		||||
    {
 | 
			
		||||
        return parent::fromJson($json);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										53
									
								
								php/federator/data/activitypub/common/following.php
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								php/federator/data/activitypub/common/following.php
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,53 @@
 | 
			
		|||
<?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\Data\ActivityPub\Common;
 | 
			
		||||
 | 
			
		||||
class Following extends OrderedCollectionPage
 | 
			
		||||
{
 | 
			
		||||
    public function __construct()
 | 
			
		||||
    {
 | 
			
		||||
        parent::__construct();
 | 
			
		||||
        parent::addContext('https://www.w3.org/ns/activitystreams');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * set items
 | 
			
		||||
     *
 | 
			
		||||
     * @param string[] $items the items in the collection
 | 
			
		||||
     * @return void
 | 
			
		||||
     */
 | 
			
		||||
    public function setItems(&$items)
 | 
			
		||||
    {
 | 
			
		||||
        // Optionally: type check that all $items are Activity objects
 | 
			
		||||
        $this->items = $items;
 | 
			
		||||
        $this->totalItems = sizeof($items);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * convert internal state to php array
 | 
			
		||||
     *
 | 
			
		||||
     * @return array<string,mixed>
 | 
			
		||||
     */
 | 
			
		||||
    public function toObject()
 | 
			
		||||
    {
 | 
			
		||||
        $return = parent::toObject();
 | 
			
		||||
        return $return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * create object from json
 | 
			
		||||
     *
 | 
			
		||||
     * @param array<string,mixed> $json input json
 | 
			
		||||
     * @return bool true on success
 | 
			
		||||
     */
 | 
			
		||||
    public function fromJson($json)
 | 
			
		||||
    {
 | 
			
		||||
        return parent::fromJson($json);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										18
									
								
								php/federator/data/activitypub/common/like.php
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								php/federator/data/activitypub/common/like.php
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,18 @@
 | 
			
		|||
<?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\Data\ActivityPub\Common;
 | 
			
		||||
 | 
			
		||||
class Like extends Activity
 | 
			
		||||
{
 | 
			
		||||
    public function __construct()
 | 
			
		||||
    {
 | 
			
		||||
        parent::__construct('Like');
 | 
			
		||||
        parent::addContext('https://www.w3.org/ns/activitystreams');
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -13,7 +13,7 @@ class OrderedCollection extends Collection
 | 
			
		|||
    /**
 | 
			
		||||
     * nested items
 | 
			
		||||
     *
 | 
			
		||||
     * @var APObject[]
 | 
			
		||||
     * @var APObject[]|string[]
 | 
			
		||||
     */
 | 
			
		||||
    protected $items = [];
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -34,7 +34,11 @@ class OrderedCollection extends Collection
 | 
			
		|||
        $return['type'] = 'OrderedCollection';
 | 
			
		||||
        if ($this->totalItems > 0) {
 | 
			
		||||
            foreach ($this->items as $item) {
 | 
			
		||||
                $return['OrderedItems'][] = $item->toObject();
 | 
			
		||||
                if (is_string($item)) {
 | 
			
		||||
                    $return['orderedItems'][] = $item;
 | 
			
		||||
                } elseif (is_object($item)) {
 | 
			
		||||
                    $return['orderedItems'][] = $item->toObject();
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return $return;
 | 
			
		||||
| 
						 | 
				
			
			@ -51,7 +55,11 @@ class OrderedCollection extends Collection
 | 
			
		|||
        return parent::fromJson($json);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function append(APObject &$item) : void
 | 
			
		||||
    /**
 | 
			
		||||
     * add item to collection
 | 
			
		||||
     * @param APObject|string $item
 | 
			
		||||
     */
 | 
			
		||||
    public function append(&$item): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->items[] = $item;
 | 
			
		||||
        $this->totalItems = sizeof($this->items);
 | 
			
		||||
| 
						 | 
				
			
			@ -60,7 +68,7 @@ class OrderedCollection extends Collection
 | 
			
		|||
    /**
 | 
			
		||||
     * get item with given index
 | 
			
		||||
     *
 | 
			
		||||
     * @return APObject|false
 | 
			
		||||
     * @return APObject|string|false
 | 
			
		||||
     */
 | 
			
		||||
    public function get(int $index)
 | 
			
		||||
    {
 | 
			
		||||
| 
						 | 
				
			
			@ -70,7 +78,7 @@ class OrderedCollection extends Collection
 | 
			
		|||
            }
 | 
			
		||||
            return $this->items[$index];
 | 
			
		||||
        } else {
 | 
			
		||||
            if ($this->totalItems+ $index < 0) {
 | 
			
		||||
            if ($this->totalItems + $index < 0) {
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
            return $this->items[$this->totalItems + $index];
 | 
			
		||||
| 
						 | 
				
			
			@ -80,7 +88,7 @@ class OrderedCollection extends Collection
 | 
			
		|||
    /**
 | 
			
		||||
     * set items
 | 
			
		||||
     *
 | 
			
		||||
     * @param APObject[] $items
 | 
			
		||||
     * @param APObject[]|string[] $items
 | 
			
		||||
     * @return void
 | 
			
		||||
     */
 | 
			
		||||
    public function setItems(&$items)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -27,6 +27,18 @@ class Outbox extends OrderedCollectionPage
 | 
			
		|||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * set items
 | 
			
		||||
     *
 | 
			
		||||
     * @param \Federator\Data\ActivityPub\Common\APObject[] $items the items in the collection
 | 
			
		||||
     */
 | 
			
		||||
    public function setItems(&$items)
 | 
			
		||||
    {
 | 
			
		||||
        // Optionally: type check that all $items are Activity objects
 | 
			
		||||
        $this->items = $items;
 | 
			
		||||
        $this->totalItems = sizeof($items);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * convert internal state to php array
 | 
			
		||||
     *
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										27
									
								
								php/federator/data/activitypub/common/vote.php
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								php/federator/data/activitypub/common/vote.php
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,27 @@
 | 
			
		|||
<?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\Data\ActivityPub\Common;
 | 
			
		||||
 | 
			
		||||
class Vote extends APObject
 | 
			
		||||
{
 | 
			
		||||
    public function __construct()
 | 
			
		||||
    {
 | 
			
		||||
        parent::__construct('Vote');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * create object from json
 | 
			
		||||
     * @param mixed $json input
 | 
			
		||||
     * @return bool true on success
 | 
			
		||||
     */
 | 
			
		||||
    public function fromJson($json)
 | 
			
		||||
    {
 | 
			
		||||
        return parent::fromJson($json);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -55,6 +55,9 @@ class Factory
 | 
			
		|||
            case 'Outbox':
 | 
			
		||||
                $return = new Common\Outbox();
 | 
			
		||||
                break;
 | 
			
		||||
            case 'Vote':
 | 
			
		||||
                $return = new Common\Vote();
 | 
			
		||||
                break;
 | 
			
		||||
            case 'Inbox':
 | 
			
		||||
                $return = new Common\Inbox();
 | 
			
		||||
                break;
 | 
			
		||||
| 
						 | 
				
			
			@ -98,6 +101,12 @@ class Factory
 | 
			
		|||
            case 'Delete':
 | 
			
		||||
                $return = new Common\Delete();
 | 
			
		||||
                break;
 | 
			
		||||
            case 'Like':
 | 
			
		||||
                $return = new Common\Like();
 | 
			
		||||
                break;
 | 
			
		||||
            case 'Dislike':
 | 
			
		||||
                $return = new Common\Dislike();
 | 
			
		||||
                break;
 | 
			
		||||
            case 'Follow':
 | 
			
		||||
                $return = new Common\Follow();
 | 
			
		||||
                break;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -74,7 +74,69 @@ class Followers
 | 
			
		|||
        return $followers;
 | 
			
		||||
    }
 | 
			
		||||
    /**
 | 
			
		||||
     * get followers of user
 | 
			
		||||
     * get following for user - who does the user follow
 | 
			
		||||
     *
 | 
			
		||||
     * @param \mysqli $dbh
 | 
			
		||||
     *          database handle
 | 
			
		||||
     * @param string $id
 | 
			
		||||
     *          user id
 | 
			
		||||
     * @param \Federator\Connector\Connector $connector
 | 
			
		||||
     *          connector to fetch use with
 | 
			
		||||
     * @param \Federator\Cache\Cache|null $cache
 | 
			
		||||
     *          optional caching service
 | 
			
		||||
     * @return \Federator\Data\FedUser[]
 | 
			
		||||
     */
 | 
			
		||||
 | 
			
		||||
    public static function getFollowingForUser($dbh, $id, $connector, $cache)
 | 
			
		||||
    {
 | 
			
		||||
        // ask cache
 | 
			
		||||
        if ($cache !== null) {
 | 
			
		||||
            $following = $cache->getRemoteFollowingForUser($id);
 | 
			
		||||
            if ($following !== false) {
 | 
			
		||||
                return $following;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        $following = [];
 | 
			
		||||
        $sql = 'select target_user from follows where source_user = ?';
 | 
			
		||||
        $stmt = $dbh->prepare($sql);
 | 
			
		||||
        if ($stmt === false) {
 | 
			
		||||
            throw new \Federator\Exceptions\ServerError();
 | 
			
		||||
        }
 | 
			
		||||
        $stmt->bind_param("s", $id);
 | 
			
		||||
        $stmt->execute();
 | 
			
		||||
        $followingIds = [];
 | 
			
		||||
        $stmt->bind_result($sourceUser);
 | 
			
		||||
        while ($stmt->fetch()) {
 | 
			
		||||
            $followingIds[] = $sourceUser;
 | 
			
		||||
        }
 | 
			
		||||
        $stmt->close();
 | 
			
		||||
        foreach ($followingIds as $followingId) {
 | 
			
		||||
            $user = \Federator\DIO\FedUser::getUserByName(
 | 
			
		||||
                $dbh,
 | 
			
		||||
                $followingId,
 | 
			
		||||
                $cache,
 | 
			
		||||
            );
 | 
			
		||||
            if ($user !== false && $user->id !== null) {
 | 
			
		||||
                $following[] = $user;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if ($following === []) {
 | 
			
		||||
            // ask connector for user-id
 | 
			
		||||
            $following = $connector->getRemoteFollowingForUser($id);
 | 
			
		||||
            if ($following === false) {
 | 
			
		||||
                $following = [];
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        // save posts to DB
 | 
			
		||||
        if ($cache !== null) {
 | 
			
		||||
            $cache->saveRemoteFollowingForUser($id, $following);
 | 
			
		||||
        }
 | 
			
		||||
        return $following;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * get followers of federated external user (e.g. mastodon)
 | 
			
		||||
     *
 | 
			
		||||
     * @param \mysqli $dbh
 | 
			
		||||
     *          database handle
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -13,13 +13,12 @@ namespace Federator\DIO;
 | 
			
		|||
 */
 | 
			
		||||
class Posts
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * get posts by user
 | 
			
		||||
     *
 | 
			
		||||
     * @param \mysqli $dbh @unused-param
 | 
			
		||||
     *          database handle
 | 
			
		||||
     * @param string $id
 | 
			
		||||
     * @param string $userid
 | 
			
		||||
     *          user id
 | 
			
		||||
     * @param \Federator\Connector\Connector $connector
 | 
			
		||||
     *          connector to fetch use with
 | 
			
		||||
| 
						 | 
				
			
			@ -29,31 +28,203 @@ class Posts
 | 
			
		|||
     *          minimum date
 | 
			
		||||
     * @param string $max
 | 
			
		||||
     *          maximum date
 | 
			
		||||
     * @return \Federator\Data\ActivityPub\Common\APObject[]
 | 
			
		||||
     * @return \Federator\Data\ActivityPub\Common\Activity[]
 | 
			
		||||
     */
 | 
			
		||||
    public static function getPostsByUser($dbh, $id, $connector, $cache, $min, $max)
 | 
			
		||||
    public static function getPostsByUser($dbh, $userid, $connector, $cache, $min, $max)
 | 
			
		||||
    {
 | 
			
		||||
        // ask cache
 | 
			
		||||
        if ($cache !== null) {
 | 
			
		||||
            $posts = $cache->getRemotePostsByUser($id, $min, $max);
 | 
			
		||||
            $posts = $cache->getRemotePostsByUser($userid, $min, $max);
 | 
			
		||||
            if ($posts !== false) {
 | 
			
		||||
                return $posts;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        $posts = [];
 | 
			
		||||
        // TODO: check our db
 | 
			
		||||
        $posts = self::getPostsFromDb($dbh, $userid, $min, $max);
 | 
			
		||||
        if ($posts === false) {
 | 
			
		||||
            $posts = [];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if ($posts === []) {
 | 
			
		||||
            // ask connector for user-id
 | 
			
		||||
            $posts = $connector->getRemotePostsByUser($id, $min, $max);
 | 
			
		||||
            if ($posts === false) {
 | 
			
		||||
                $posts = [];
 | 
			
		||||
        // Only override $min if we found posts in our DB
 | 
			
		||||
        $remoteMin = $min;
 | 
			
		||||
        if (!empty($posts)) {
 | 
			
		||||
            // Find the latest published date in the DB posts
 | 
			
		||||
            $latestPublished = null;
 | 
			
		||||
            foreach ($posts as $post) {
 | 
			
		||||
                $published = $post->getPublished();
 | 
			
		||||
                if ($published != null) {
 | 
			
		||||
                    $publishedStr = gmdate('Y-m-d H:i:s', $published);
 | 
			
		||||
                    if ($latestPublished === null || $publishedStr > $latestPublished) {
 | 
			
		||||
                        $latestPublished = $publishedStr;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            if ($latestPublished !== null) {
 | 
			
		||||
                $remoteMin = $latestPublished;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Always fetch newer posts from connector (if any)
 | 
			
		||||
        $newPosts = $connector->getRemotePostsByUser($userid, $remoteMin, $max);
 | 
			
		||||
        if ($newPosts !== false && is_array($newPosts)) {
 | 
			
		||||
            // Merge new posts with DB posts, avoiding duplicates by ID
 | 
			
		||||
            $existingIds = [];
 | 
			
		||||
            foreach ($posts as $post) {
 | 
			
		||||
                $existingIds[$post->getID()] = true;
 | 
			
		||||
            }
 | 
			
		||||
            foreach ($newPosts as $newPost) {
 | 
			
		||||
                if (!isset($existingIds[$newPost->getID()])) {
 | 
			
		||||
                    $posts[] = $newPost;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // save posts to DB
 | 
			
		||||
        foreach ($posts as $post) {
 | 
			
		||||
            if ($post->getID() !== "") {
 | 
			
		||||
                self::savePost($dbh, $userid, $post);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if ($cache !== null) {
 | 
			
		||||
            $cache->saveRemotePostsByUser($id, $posts);
 | 
			
		||||
            $cache->saveRemotePostsByUser($userid, $posts);
 | 
			
		||||
        }
 | 
			
		||||
        return $posts;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get posts for a user from the DB (optionally by date)
 | 
			
		||||
     *
 | 
			
		||||
     * @param \mysqli $dbh
 | 
			
		||||
     * @param string $userId
 | 
			
		||||
     * @param string|null $min
 | 
			
		||||
     * @param string|null $max
 | 
			
		||||
     * @return \Federator\Data\ActivityPub\Common\Activity[]|false
 | 
			
		||||
     */
 | 
			
		||||
    public static function getPostsFromDb($dbh, $userId, $min = null, $max = null)
 | 
			
		||||
    {
 | 
			
		||||
        $sql = 'SELECT id, user_id, type, object, published FROM posts WHERE user_id = ?';
 | 
			
		||||
        $params = [$userId];
 | 
			
		||||
        $types = 's';
 | 
			
		||||
        if ($min !== null) {
 | 
			
		||||
            $sql .= ' AND published >= ?';
 | 
			
		||||
            $params[] = $min;
 | 
			
		||||
            $types .= 's';
 | 
			
		||||
        }
 | 
			
		||||
        if ($max !== null) {
 | 
			
		||||
            $sql .= ' AND published <= ?';
 | 
			
		||||
            $params[] = $max;
 | 
			
		||||
            $types .= 's';
 | 
			
		||||
        }
 | 
			
		||||
        $sql .= ' ORDER BY published DESC';
 | 
			
		||||
 | 
			
		||||
        $stmt = $dbh->prepare($sql);
 | 
			
		||||
        if ($stmt === false) {
 | 
			
		||||
            throw new \Federator\Exceptions\ServerError();
 | 
			
		||||
        }
 | 
			
		||||
        $stmt->bind_param($types, ...$params);
 | 
			
		||||
        $stmt->execute();
 | 
			
		||||
        $result = $stmt->get_result();
 | 
			
		||||
        if (!($result instanceof \mysqli_result)) {
 | 
			
		||||
            $stmt->close();
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
        $posts = [];
 | 
			
		||||
        while ($row = $result->fetch_assoc()) {
 | 
			
		||||
            if (!empty($row['object'])) {
 | 
			
		||||
                $objectData = json_decode($row['object'], true);
 | 
			
		||||
                if (is_array($objectData)) {
 | 
			
		||||
                    // Use the ActivityPub factory to create the APObject
 | 
			
		||||
                    $object = \Federator\Data\ActivityPub\Factory::newActivityFromJson($objectData);
 | 
			
		||||
                    if ($object !== false) {
 | 
			
		||||
                        $posts[] = $object;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        $stmt->close();
 | 
			
		||||
        return $posts;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Save a post (insert or update)
 | 
			
		||||
     *
 | 
			
		||||
     * @param \mysqli $dbh
 | 
			
		||||
     * @param string $userId
 | 
			
		||||
     * @param \Federator\Data\ActivityPub\Common\Activity $post
 | 
			
		||||
     * @return bool
 | 
			
		||||
     */
 | 
			
		||||
    public static function savePost($dbh, $userId, $post)
 | 
			
		||||
    {
 | 
			
		||||
        $sql = 'INSERT INTO posts (
 | 
			
		||||
                `id`, `url`, `user_id`, `actor`, `type`, `object`, `to`, `cc`, `published`
 | 
			
		||||
            ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
 | 
			
		||||
            ON DUPLICATE KEY UPDATE
 | 
			
		||||
                `url` = VALUES(`url`),
 | 
			
		||||
                `user_id` = VALUES(`user_id`),
 | 
			
		||||
                `actor` = VALUES(`actor`),
 | 
			
		||||
                `type` = VALUES(`type`),
 | 
			
		||||
                `object` = VALUES(`object`),
 | 
			
		||||
                `to` = VALUES(`to`),
 | 
			
		||||
                `cc` = VALUES(`cc`),
 | 
			
		||||
                `published` = VALUES(`published`)';
 | 
			
		||||
        $stmt = $dbh->prepare($sql);
 | 
			
		||||
        if ($stmt === false) {
 | 
			
		||||
            throw new \Federator\Exceptions\ServerError();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $id = $post->getID();
 | 
			
		||||
        $url = $post->getUrl();
 | 
			
		||||
        $actor = $post->getAActor();
 | 
			
		||||
        $type = $post->getType();
 | 
			
		||||
        $object = $post->getObject();
 | 
			
		||||
        $objectJson = ($object instanceof \Federator\Data\ActivityPub\Common\APObject)
 | 
			
		||||
            ? json_encode($object)
 | 
			
		||||
            : $object;
 | 
			
		||||
        if ($objectJson === false) {
 | 
			
		||||
            $objectJson = null;
 | 
			
		||||
        }
 | 
			
		||||
        $to = $post->getTo();
 | 
			
		||||
        $cc = $post->getCC();
 | 
			
		||||
        $toJson = is_array($to) ? json_encode($to) : (is_string($to) ? json_encode([$to]) : null);
 | 
			
		||||
        $ccJson = is_array($cc) ? json_encode($cc) : (is_string($cc) ? json_encode([$cc]) : null);
 | 
			
		||||
        $published = $post->getPublished();
 | 
			
		||||
        $publishedStr = $published ? gmdate('Y-m-d H:i:s', $published) : gmdate('Y-m-d H:i:s');
 | 
			
		||||
 | 
			
		||||
        $stmt->bind_param(
 | 
			
		||||
            "sssssssss",
 | 
			
		||||
            $id,
 | 
			
		||||
            $url,
 | 
			
		||||
            $userId,
 | 
			
		||||
            $actor,
 | 
			
		||||
            $type,
 | 
			
		||||
            $objectJson,
 | 
			
		||||
            $toJson,
 | 
			
		||||
            $ccJson,
 | 
			
		||||
            $publishedStr
 | 
			
		||||
        );
 | 
			
		||||
        $result = $stmt->execute();
 | 
			
		||||
        $stmt->close();
 | 
			
		||||
        return $result;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Delete a post
 | 
			
		||||
     *
 | 
			
		||||
     * @param \mysqli $dbh
 | 
			
		||||
     * @param string $id   The post ID
 | 
			
		||||
     * @return bool
 | 
			
		||||
     */
 | 
			
		||||
    public static function deletePost($dbh, $id)
 | 
			
		||||
    {
 | 
			
		||||
        $sql = 'delete from posts where id = ?';
 | 
			
		||||
        $stmt = $dbh->prepare($sql);
 | 
			
		||||
        if ($stmt === false) {
 | 
			
		||||
            throw new \Federator\Exceptions\ServerError();
 | 
			
		||||
        }
 | 
			
		||||
        $stmt->bind_param("s", $id);
 | 
			
		||||
        $stmt->execute();
 | 
			
		||||
        $affectedRows = $stmt->affected_rows;
 | 
			
		||||
        $stmt->close();
 | 
			
		||||
        return $affectedRows > 0;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										97
									
								
								php/federator/dio/votes.php
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								php/federator/dio/votes.php
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,97 @@
 | 
			
		|||
<?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\DIO;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * IO functions related to votes
 | 
			
		||||
 */
 | 
			
		||||
class Votes
 | 
			
		||||
{
 | 
			
		||||
    /**
 | 
			
		||||
     * Add a vote (like/dislike)
 | 
			
		||||
     *
 | 
			
		||||
     * @param \mysqli $dbh
 | 
			
		||||
     * @param string $userId      The user who votes
 | 
			
		||||
     * @param string $targetId    The object being voted on (e.g., post id)
 | 
			
		||||
     * @param string $type        'like' or 'dislike'
 | 
			
		||||
     * @return string|false       The generated vote ID on success, false on failure
 | 
			
		||||
     */
 | 
			
		||||
    public static function addVote($dbh, $userId, $targetId, $type)
 | 
			
		||||
    {
 | 
			
		||||
        // Check if already voted
 | 
			
		||||
        $sql = 'SELECT id FROM votes WHERE user_id = ? AND target_id = ? AND type = ?';
 | 
			
		||||
        $stmt = $dbh->prepare($sql);
 | 
			
		||||
        if ($stmt === false) {
 | 
			
		||||
            throw new \Federator\Exceptions\ServerError();
 | 
			
		||||
        }
 | 
			
		||||
        $stmt->bind_param("sss", $userId, $targetId, $type);
 | 
			
		||||
        $foundId = 0;
 | 
			
		||||
        $ret = $stmt->bind_result($foundId);
 | 
			
		||||
        $stmt->execute();
 | 
			
		||||
        if ($ret) {
 | 
			
		||||
            $stmt->fetch();
 | 
			
		||||
        }
 | 
			
		||||
        $stmt->close();
 | 
			
		||||
        if ($foundId != 0) {
 | 
			
		||||
            return false; // Already voted
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Generate a unique ID for the vote
 | 
			
		||||
        do {
 | 
			
		||||
            $id = bin2hex(openssl_random_pseudo_bytes(16));
 | 
			
		||||
            // Check if the generated ID is unique
 | 
			
		||||
            $sql = 'SELECT id FROM votes WHERE id = ?';
 | 
			
		||||
            $stmt = $dbh->prepare($sql);
 | 
			
		||||
            if ($stmt === false) {
 | 
			
		||||
                throw new \Federator\Exceptions\ServerError();
 | 
			
		||||
            }
 | 
			
		||||
            $stmt->bind_param("s", $id);
 | 
			
		||||
            $foundId = 0;
 | 
			
		||||
            $ret = $stmt->bind_result($foundId);
 | 
			
		||||
            $stmt->execute();
 | 
			
		||||
            if ($ret) {
 | 
			
		||||
                $stmt->fetch();
 | 
			
		||||
            }
 | 
			
		||||
            $stmt->close();
 | 
			
		||||
        } while ($foundId > 0);
 | 
			
		||||
 | 
			
		||||
        // Add vote with created_at timestamp
 | 
			
		||||
        $sql = 'INSERT INTO votes (id, user_id, target_id, type, created_at) VALUES (?, ?, ?, ?, NOW())';
 | 
			
		||||
        $stmt = $dbh->prepare($sql);
 | 
			
		||||
        if ($stmt === false) {
 | 
			
		||||
            throw new \Federator\Exceptions\ServerError();
 | 
			
		||||
        }
 | 
			
		||||
        $stmt->bind_param("ssss", $id, $userId, $targetId, $type);
 | 
			
		||||
        $stmt->execute();
 | 
			
		||||
        $stmt->close();
 | 
			
		||||
        return $id; // Return the generated vote ID
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Remove a vote (like/dislike)
 | 
			
		||||
     *
 | 
			
		||||
     * @param \mysqli $dbh
 | 
			
		||||
     * @param string $userId
 | 
			
		||||
     * @param string $targetId
 | 
			
		||||
     * @return bool             true on success
 | 
			
		||||
     */
 | 
			
		||||
    public static function removeVote($dbh, $userId, $targetId)
 | 
			
		||||
    {
 | 
			
		||||
        $sql = 'DELETE FROM votes WHERE user_id = ? AND target_id = ?';
 | 
			
		||||
        $stmt = $dbh->prepare($sql);
 | 
			
		||||
        if ($stmt === false) {
 | 
			
		||||
            throw new \Federator\Exceptions\ServerError();
 | 
			
		||||
        }
 | 
			
		||||
        $stmt->bind_param("ss", $userId, $targetId);
 | 
			
		||||
        $stmt->execute();
 | 
			
		||||
        $affectedRows = $stmt->affected_rows;
 | 
			
		||||
        $stmt->close();
 | 
			
		||||
        return $affectedRows > 0;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										69
									
								
								php/federator/jobs/newContentJob.php
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								php/federator/jobs/newContentJob.php
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,69 @@
 | 
			
		|||
<?php
 | 
			
		||||
 | 
			
		||||
namespace Federator\Jobs;
 | 
			
		||||
 | 
			
		||||
class NewContentJob extends \Federator\Api
 | 
			
		||||
{
 | 
			
		||||
    /** @var array<string, mixed> $args Arguments for the job */
 | 
			
		||||
    public $args = [];
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * cache instance
 | 
			
		||||
     *
 | 
			
		||||
     * @var \Federator\Cache\Cache $cache
 | 
			
		||||
     */
 | 
			
		||||
    protected $cache;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * remote connector
 | 
			
		||||
     *
 | 
			
		||||
     * @var \Federator\Connector\Connector $connector
 | 
			
		||||
     */
 | 
			
		||||
    protected $connector = null;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * database instance
 | 
			
		||||
     *
 | 
			
		||||
     * @var \Mysqli $dbh
 | 
			
		||||
     */
 | 
			
		||||
    protected $dbh;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * constructor
 | 
			
		||||
     */
 | 
			
		||||
    public function __construct()
 | 
			
		||||
    {
 | 
			
		||||
        parent::__construct();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Set up environment for this job
 | 
			
		||||
     */
 | 
			
		||||
    public function setUp(): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->openDatabase();
 | 
			
		||||
        $this->loadPlugins();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Perform the inbox job.
 | 
			
		||||
     *
 | 
			
		||||
     * @return bool true on success, false on failure
 | 
			
		||||
     */
 | 
			
		||||
    public function perform(): bool
 | 
			
		||||
    {
 | 
			
		||||
        error_log("NewContentJob: Starting inbox job");
 | 
			
		||||
        $user = $this->args['user'];
 | 
			
		||||
        $activity = $this->args['activity'];
 | 
			
		||||
 | 
			
		||||
        $activity = \Federator\Data\ActivityPub\Factory::newActivityFromJson($activity);
 | 
			
		||||
 | 
			
		||||
        if ($activity === false) {
 | 
			
		||||
            error_log("NewContentJob: Failed to create activity from JSON");
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        \Federator\Api\V1\NewContent::postForUser($this->dbh, $this->connector, $this->cache, $user, $activity);
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -77,13 +77,41 @@ class ContentNation implements Connector
 | 
			
		|||
        return $followers;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * get following of given user
 | 
			
		||||
     *
 | 
			
		||||
     * @param string $userId user id
 | 
			
		||||
 | 
			
		||||
     * @return \Federator\Data\FedUser[]|false
 | 
			
		||||
     */
 | 
			
		||||
    public function getRemoteFollowingForUser($userId)
 | 
			
		||||
    {
 | 
			
		||||
        // todo implement queue for this
 | 
			
		||||
        if (preg_match("#^([^@]+)@([^/]+)#", $userId, $matches) == 1) {
 | 
			
		||||
            $userId = $matches[1];
 | 
			
		||||
        }
 | 
			
		||||
        $remoteURL = $this->service . '/api/profile/' . urlencode($userId) . '/following';
 | 
			
		||||
 | 
			
		||||
        [$response, $info] = \Federator\Main::getFromRemote($remoteURL, []);
 | 
			
		||||
        if ($info['http_code'] != 200) {
 | 
			
		||||
            print_r($info);
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
        $r = json_decode($response, true);
 | 
			
		||||
        if ($r === false || $r === null || !is_array($r)) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
        $followers = [];
 | 
			
		||||
        return $followers;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * get posts by given user
 | 
			
		||||
     *
 | 
			
		||||
     * @param string $userId user id
 | 
			
		||||
     * @param string $min min date
 | 
			
		||||
     * @param string $max max date
 | 
			
		||||
     * @return \Federator\Data\ActivityPub\Common\APObject[]|false
 | 
			
		||||
     * @return \Federator\Data\ActivityPub\Common\Activity[]|false
 | 
			
		||||
     */
 | 
			
		||||
    public function getRemotePostsByUser($userId, $min, $max)
 | 
			
		||||
    {
 | 
			
		||||
| 
						 | 
				
			
			@ -114,16 +142,15 @@ class ContentNation implements Connector
 | 
			
		|||
            $imgpath = $this->config['userdata']['path'];
 | 
			
		||||
            $userdata = $this->config['userdata']['url'];
 | 
			
		||||
            foreach ($activities as $activity) {
 | 
			
		||||
                $create = new \Federator\Data\ActivityPub\Common\Create();
 | 
			
		||||
                $create->setAActor('https://' . $domain . '/' . $userId);
 | 
			
		||||
                $create->setID($activity['id'])
 | 
			
		||||
                    ->setPublished($activity['published'] ?? $activity['timestamp'])
 | 
			
		||||
                    ->addTo("https://www.w3.org/ns/activitystreams#Public")
 | 
			
		||||
                    ->addCC('https://' . $domain . '/' . $userId . '/followers');
 | 
			
		||||
 | 
			
		||||
                switch ($activity['type']) {
 | 
			
		||||
                    case 'Article':
 | 
			
		||||
                        $create->setURL('https://' . $domain . '/' . $activity['name']);
 | 
			
		||||
                        $create = new \Federator\Data\ActivityPub\Common\Create();
 | 
			
		||||
                        $create->setAActor('https://' . $domain . '/' . $userId);
 | 
			
		||||
                        $create->setID($activity['id'])
 | 
			
		||||
                            ->setPublished($activity['published'] ?? $activity['timestamp'])
 | 
			
		||||
                            ->addTo("https://www.w3.org/ns/activitystreams#Public")
 | 
			
		||||
                            ->addCC('https://' . $domain . '/' . $userId . '/followers');
 | 
			
		||||
                        $create->setURL('https://' . $domain . '/' . $activity['profilename'] . '/' . $activity['name']);
 | 
			
		||||
                        $apArticle = new \Federator\Data\ActivityPub\Common\Article();
 | 
			
		||||
                        if (array_key_exists('tags', $activity)) {
 | 
			
		||||
                            foreach ($activity['tags'] as $tag) {
 | 
			
		||||
| 
						 | 
				
			
			@ -169,10 +196,18 @@ class ContentNation implements Connector
 | 
			
		|||
                        $create->setObject($apArticle);
 | 
			
		||||
                        $posts[] = $create;
 | 
			
		||||
                        break; // Article
 | 
			
		||||
 | 
			
		||||
                    case 'Comment':
 | 
			
		||||
                        $create = new \Federator\Data\ActivityPub\Common\Create();
 | 
			
		||||
                        $create->setAActor('https://' . $domain . '/' . $userId);
 | 
			
		||||
                        $create->setID($activity['id'])
 | 
			
		||||
                            ->setPublished($activity['published'] ?? $activity['timestamp'])
 | 
			
		||||
                            ->addTo("https://www.w3.org/ns/activitystreams#Public")
 | 
			
		||||
                            ->addCC('https://' . $domain . '/' . $userId . '/followers');
 | 
			
		||||
                        $commentJson = $activity;
 | 
			
		||||
                        $commentJson['type'] = 'Note';
 | 
			
		||||
                        $commentJson['summary'] = $activity['subject'];
 | 
			
		||||
                        $commentJson['id'] = $activity['id'];
 | 
			
		||||
                        $note = \Federator\Data\ActivityPub\Factory::newFromJson($commentJson, "");
 | 
			
		||||
                        if ($note === null) {
 | 
			
		||||
                            error_log("ContentNation::getRemotePostsByUser couldn't create comment");
 | 
			
		||||
| 
						 | 
				
			
			@ -180,53 +215,36 @@ class ContentNation implements Connector
 | 
			
		|||
                            $create->setObject($comment);
 | 
			
		||||
                            break;
 | 
			
		||||
                        }
 | 
			
		||||
                        $url = 'https://' . $domain . '/' . $activity['articleOwnerName'] . '/' . $activity['articleName'] . '#' . $note->getID();
 | 
			
		||||
                        $create->setURL($url);
 | 
			
		||||
                        $create->setObject($note);
 | 
			
		||||
                        $posts[] = $create;
 | 
			
		||||
                        break; // Comment
 | 
			
		||||
 | 
			
		||||
                    case 'Vote':
 | 
			
		||||
                        $url = 'https://' . $domain . '/' . $userId . '/'
 | 
			
		||||
                            . $activity['articlename'];
 | 
			
		||||
                        $url .= '/vote/' . $activity['id'];
 | 
			
		||||
                        $create->setURL($url);
 | 
			
		||||
                        if ($activity['upvote'] === true) {
 | 
			
		||||
                            $like = new \Federator\Data\ActivityPub\Common\Activity('Like');
 | 
			
		||||
                            $like->setSummary(
 | 
			
		||||
                                $this->main->translate(
 | 
			
		||||
                                    $activity['articlelang'],
 | 
			
		||||
                                    'vote',
 | 
			
		||||
                                    'like',
 | 
			
		||||
                                    [$activity['username']]
 | 
			
		||||
                                )
 | 
			
		||||
                            );
 | 
			
		||||
                        } else {
 | 
			
		||||
                            $like = new \Federator\Data\ActivityPub\Common\Activity('Dislike');
 | 
			
		||||
                            $like->setSummary(
 | 
			
		||||
                                $this->main->translate(
 | 
			
		||||
                                    $activity['articlelang'],
 | 
			
		||||
                                    'vote',
 | 
			
		||||
                                    'dislike',
 | 
			
		||||
                                    [$activity['username']]
 | 
			
		||||
                                )
 | 
			
		||||
                            );
 | 
			
		||||
                        }
 | 
			
		||||
                        $actor = new \Federator\Data\ActivityPub\Common\APObject('Person');
 | 
			
		||||
                        $actor->setName($activity['username']);
 | 
			
		||||
                        $like->setActor($actor);
 | 
			
		||||
                        $url = 'https://' . $domain . '/' . $userId . '/' . $activity['articlename'];
 | 
			
		||||
                        // Build Like/Dislike as top-level activity
 | 
			
		||||
                        $likeType = $activity['upvote'] === true ? 'Like' : 'Dislike';
 | 
			
		||||
                        $like = new \Federator\Data\ActivityPub\Common\Activity($likeType);
 | 
			
		||||
                        $like->setAActor('https://' . $domain . '/' . $userId);
 | 
			
		||||
                        $like->setID($activity['id'])
 | 
			
		||||
                            ->setPublished($activity['published'] ?? $activity['timestamp'])
 | 
			
		||||
                            ->addTo("https://www.w3.org/ns/activitystreams#Public")
 | 
			
		||||
                            ->addCC('https://' . $domain . '/' . $userId . '/followers');
 | 
			
		||||
                        $like->setSummary(
 | 
			
		||||
                            $this->main->translate(
 | 
			
		||||
                                $activity['articlelang'],
 | 
			
		||||
                                'vote',
 | 
			
		||||
                                $likeType === 'Like' ? 'like' : 'dislike',
 | 
			
		||||
                                [$activity['username']]
 | 
			
		||||
                            )
 | 
			
		||||
                        );
 | 
			
		||||
                        $objectUrl = 'https://' . $domain . '/' . $userId . '/' . $activity['articlename'];
 | 
			
		||||
                        if ($activity['comment'] !== '') {
 | 
			
		||||
                            $url .= '/comment/' . $activity['comment'];
 | 
			
		||||
                            $objectUrl .= "#" . $activity['comment'];
 | 
			
		||||
                        }
 | 
			
		||||
                        $type = 'Article';
 | 
			
		||||
                        switch ($activity['votetype']) {
 | 
			
		||||
                            case 'comment':
 | 
			
		||||
                                $type = 'Comment';
 | 
			
		||||
                                break;
 | 
			
		||||
                        }
 | 
			
		||||
                        $object = new \Federator\Data\ActivityPub\Common\APObject($type);
 | 
			
		||||
                        $object->setHref($url);
 | 
			
		||||
                        $like->setObject($object);
 | 
			
		||||
                        $create->setObject($like);
 | 
			
		||||
                        $posts[] = $create;
 | 
			
		||||
                        $like->setURL($objectUrl . '#' . $activity['id']);
 | 
			
		||||
                        $like->setObject($objectUrl);
 | 
			
		||||
                        $posts[] = $like;
 | 
			
		||||
                        break; // Vote
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
| 
						 | 
				
			
			@ -352,50 +370,98 @@ class ContentNation implements Connector
 | 
			
		|||
            'type' => 'Create', // Default to 'Create'
 | 
			
		||||
            'id' => $jsonData['id'] ?? null,
 | 
			
		||||
            'actor' => $jsonData['actor'] ?? null,
 | 
			
		||||
            'published' => $jsonData['object']['published'] ?? null,
 | 
			
		||||
            'to' => ['https://www.w3.org/ns/activitystreams#Public'],
 | 
			
		||||
            'cc' => [$jsonData['related']['cc']['followers'] ?? null],
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        // Extract actorName as the last segment of the actor URL (after the last '/')
 | 
			
		||||
        $actorUrl = $jsonData['actor'] ?? null;
 | 
			
		||||
        $actorName = null;
 | 
			
		||||
        $replaceUrl = null;
 | 
			
		||||
        if (is_array($actorUrl)) {
 | 
			
		||||
            $actorUrl = $actorUrl['id'];
 | 
			
		||||
            $replaceUrl = $actorUrl->url ?? null;
 | 
			
		||||
        }
 | 
			
		||||
        if ($actorUrl !== null) {
 | 
			
		||||
            $actorUrlParts = parse_url($actorUrl);
 | 
			
		||||
            if (isset($actorUrlParts['path'])) {
 | 
			
		||||
                $pathSegments = array_values(array_filter(explode('/', $actorUrlParts['path'])));
 | 
			
		||||
                $actorName = end($pathSegments);
 | 
			
		||||
                // Build replaceUrl as scheme://host if both are set
 | 
			
		||||
                if (isset($actorUrlParts['scheme'], $actorUrlParts['host'])) {
 | 
			
		||||
                    $replaceUrl = $actorUrlParts['scheme'] . '://' . $actorUrlParts['host'];
 | 
			
		||||
                } else {
 | 
			
		||||
                    $replaceUrl = $actorUrl;
 | 
			
		||||
                }
 | 
			
		||||
            } else {
 | 
			
		||||
                $actorName = $actorUrl;
 | 
			
		||||
                $replaceUrl = $actorUrl;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        $ap['actor'] = $actorUrl;
 | 
			
		||||
 | 
			
		||||
        // Handle specific fields based on the type
 | 
			
		||||
        switch ($jsonData['type']) {
 | 
			
		||||
            case 'comment':
 | 
			
		||||
                // Set Create-level fields
 | 
			
		||||
                $ap['published'] = $jsonData['object']['published'] ?? null;
 | 
			
		||||
                $ap['url'] = $jsonData['object']['url'] ?? null;
 | 
			
		||||
                $ap['to'] = ['https://www.w3.org/ns/activitystreams#Public'];
 | 
			
		||||
                $ap['cc'] = [$jsonData['related']['cc']['followers'] ?? null];
 | 
			
		||||
 | 
			
		||||
                // Set object as Note with only required fields
 | 
			
		||||
                $ap['object'] = [
 | 
			
		||||
                    'type' => 'Note',
 | 
			
		||||
                    'id' => $jsonData['object']['id'] ?? null,
 | 
			
		||||
                    'summary' => $jsonData['object']['summary'] ?? '',
 | 
			
		||||
                    'type' => 'Note',
 | 
			
		||||
                    'content' => $jsonData['object']['content'] ?? '',
 | 
			
		||||
                    'published' => $jsonData['object']['published'] ?? null,
 | 
			
		||||
                    'attributedTo' => $jsonData['actor']['id'] ?? null,
 | 
			
		||||
                    'to' => ['https://www.w3.org/ns/activitystreams#Public'],
 | 
			
		||||
                    'cc' => [$jsonData['related']['cc']['followers'] ?? null],
 | 
			
		||||
                    'url' => $jsonData['object']['url'] ?? null,
 | 
			
		||||
                    'inReplyTo' => $jsonData['related']['article']['id'] ?? null,
 | 
			
		||||
                    'summary' => $jsonData['object']['summary'] ?? '',
 | 
			
		||||
                ];
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            // todo fix this to properly handle votes, data is mocked for now
 | 
			
		||||
            case 'vote':
 | 
			
		||||
                // Handle voting on a comment or an article
 | 
			
		||||
                if ($jsonData['object']['type'] === 'Comment') {
 | 
			
		||||
                    $jsonData['object']['type'] = 'Note';
 | 
			
		||||
                }
 | 
			
		||||
                $ap['object'] = [
 | 
			
		||||
                    'id' => $jsonData['object']['id'] ?? null,
 | 
			
		||||
                    'type' => $jsonData['object']['type'] ?? 'Article',
 | 
			
		||||
                ];
 | 
			
		||||
 | 
			
		||||
                // Include additional fields if voting on an article
 | 
			
		||||
                if ($ap['object']['type'] === 'Article') {
 | 
			
		||||
                    $ap['object']['name'] = $jsonData['object']['name'] ?? null;
 | 
			
		||||
                    $ap['object']['author'] = $jsonData['object']['author'] ?? null;
 | 
			
		||||
                $ap['id'] .= "_$actorName";
 | 
			
		||||
                if (
 | 
			
		||||
                    isset($jsonData['vote']['type']) &&
 | 
			
		||||
                    strtolower($jsonData['vote']['type']) === 'undo'
 | 
			
		||||
                ) {
 | 
			
		||||
                    $ap['type'] = 'Undo';
 | 
			
		||||
                } elseif ($jsonData['vote']['value'] == 1) {
 | 
			
		||||
                    $ap['type'] = 'Like';
 | 
			
		||||
                } elseif ($jsonData['vote']['value'] == 0) {
 | 
			
		||||
                    $ap['type'] = 'Dislike';
 | 
			
		||||
                } else {
 | 
			
		||||
                    error_log("ContentNation::jsonToActivity unknown vote type: {$jsonData['vote']['type']} and value: {$jsonData['vote']['value']}");
 | 
			
		||||
                    break;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // Add vote-specific fields
 | 
			
		||||
                $ap['vote'] = [
 | 
			
		||||
                    'value' => $jsonData['vote']['value'] ?? null,
 | 
			
		||||
                    'type' => $jsonData['vote']['type'] ?? null,
 | 
			
		||||
                ];
 | 
			
		||||
                $objectId = $jsonData['object']['id'] ?? null;
 | 
			
		||||
 | 
			
		||||
                $ap['object'] = $objectId;
 | 
			
		||||
 | 
			
		||||
                if ($ap['type'] === "Undo") {
 | 
			
		||||
                    $ap['object'] = $ap['id'];
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                /* if ($ap['type'] === 'Undo') {
 | 
			
		||||
                    $ap['object'] = [
 | 
			
		||||
                        'id' => $objectId,
 | 
			
		||||
                        'type' => 'Vote',
 | 
			
		||||
                    ];
 | 
			
		||||
                } else if (
 | 
			
		||||
                    isset($jsonData['object']['type']) &&
 | 
			
		||||
                    $jsonData['object']['type'] === 'Article'
 | 
			
		||||
                ) {
 | 
			
		||||
                    $ap['object'] = [
 | 
			
		||||
                        'id' => $objectId,
 | 
			
		||||
                        'type' => $jsonData['object']['type'],
 | 
			
		||||
                        'name' => $jsonData['object']['name'] ?? null,
 | 
			
		||||
                        'author' => $jsonData['object']['author'] ?? null,
 | 
			
		||||
                    ];
 | 
			
		||||
                } else if ($jsonData['object']['type'] === 'Comment') {
 | 
			
		||||
                    $ap['object'] = [
 | 
			
		||||
                        'id' => $objectId,
 | 
			
		||||
                        'type' => 'Note',
 | 
			
		||||
                        'author' => $jsonData['object']['author'] ?? null,
 | 
			
		||||
                    ];
 | 
			
		||||
                } */
 | 
			
		||||
                break;
 | 
			
		||||
 | 
			
		||||
            default:
 | 
			
		||||
| 
						 | 
				
			
			@ -403,6 +469,35 @@ class ContentNation implements Connector
 | 
			
		|||
                throw new \InvalidArgumentException("Unsupported activity type: {$jsonData['type']}");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $config = $this->main->getConfig();
 | 
			
		||||
        $domain = $config['generic']['externaldomain'];
 | 
			
		||||
        /**
 | 
			
		||||
         * Recursively replace strings in an array.
 | 
			
		||||
         *
 | 
			
		||||
         * @param string $search
 | 
			
		||||
         * @param string $replace
 | 
			
		||||
         * @param array<mixed, mixed> $array
 | 
			
		||||
         * @return array<mixed, mixed>
 | 
			
		||||
         */
 | 
			
		||||
        function array_str_replace_recursive(string $search, string $replace, array $array): array
 | 
			
		||||
        {
 | 
			
		||||
            foreach ($array as $key => $value) {
 | 
			
		||||
                if (is_array($value)) {
 | 
			
		||||
                    $array[$key] = array_str_replace_recursive($search, $replace, $value);
 | 
			
		||||
                } elseif (is_string($value)) {
 | 
			
		||||
                    $array[$key] = str_replace($search, $replace, $value);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            return $array;
 | 
			
		||||
        }
 | 
			
		||||
        if (is_string($replaceUrl)) {
 | 
			
		||||
            $ap = array_str_replace_recursive(
 | 
			
		||||
                $replaceUrl,
 | 
			
		||||
                'https://' . $domain,
 | 
			
		||||
                $ap
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return \Federator\Data\ActivityPub\Factory::newActivityFromJson($ap);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -421,8 +516,7 @@ class ContentNation implements Connector
 | 
			
		|||
            throw new \Federator\Exceptions\PermissionDenied("Missing Signature header");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!isset($headers['X-Sender']) || $headers['X-Sender'] !== $this->config['keys']['headerSenderName'])
 | 
			
		||||
        {
 | 
			
		||||
        if (!isset($headers['X-Sender']) || $headers['X-Sender'] !== $this->config['keys']['headerSenderName']) {
 | 
			
		||||
            throw new \Federator\Exceptions\PermissionDenied("Invalid sender name");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -30,13 +30,25 @@ class DummyConnector implements Connector
 | 
			
		|||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * get following of given user
 | 
			
		||||
     *
 | 
			
		||||
     * @param string $id user id @unused-param
 | 
			
		||||
 | 
			
		||||
     * @return \Federator\Data\FedUser[]|false
 | 
			
		||||
     */
 | 
			
		||||
    public function getRemoteFollowingForUser($id)
 | 
			
		||||
    {
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * get posts by given user
 | 
			
		||||
     *
 | 
			
		||||
     * @param string $id user id @unused-param
 | 
			
		||||
     * @param string $min min date @unused-param
 | 
			
		||||
     * @param string $max max date @unused-param
 | 
			
		||||
     * @return \Federator\Data\ActivityPub\Common\APObject[]|false
 | 
			
		||||
     * @return \Federator\Data\ActivityPub\Common\Activity[]|false
 | 
			
		||||
     */
 | 
			
		||||
    public function getRemotePostsByUser($id, $min, $max)
 | 
			
		||||
    {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -108,6 +108,19 @@ class RedisCache implements Cache
 | 
			
		|||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * get following of given user
 | 
			
		||||
     *
 | 
			
		||||
     * @param string $id user id @unused-param
 | 
			
		||||
 | 
			
		||||
     * @return \Federator\Data\FedUser[]|false
 | 
			
		||||
     */
 | 
			
		||||
    public function getRemoteFollowingForUser($id)
 | 
			
		||||
    {
 | 
			
		||||
        error_log("rediscache::getRemoteFollowingForUser not implemented");
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Convert jsonData to Activity format
 | 
			
		||||
     *
 | 
			
		||||
| 
						 | 
				
			
			@ -127,7 +140,7 @@ class RedisCache implements Cache
 | 
			
		|||
     * @param string $min min date @unused-param
 | 
			
		||||
     * @param string $max max date @unused-param
 | 
			
		||||
 | 
			
		||||
     * @return \Federator\Data\ActivityPub\Common\APObject[]|false
 | 
			
		||||
     * @return \Federator\Data\ActivityPub\Common\Activity[]|false
 | 
			
		||||
     */
 | 
			
		||||
    public function getRemotePostsByUser($id, $min, $max)
 | 
			
		||||
    {
 | 
			
		||||
| 
						 | 
				
			
			@ -242,6 +255,18 @@ class RedisCache implements Cache
 | 
			
		|||
        error_log("rediscache::saveRemoteFollowersOfUser not implemented");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * save remote following for user
 | 
			
		||||
     *
 | 
			
		||||
     * @param string $user user name @unused-param
 | 
			
		||||
     * @param \Federator\Data\FedUser[]|false $following user following @unused-param
 | 
			
		||||
     * @return void
 | 
			
		||||
     */
 | 
			
		||||
    public function saveRemoteFollowingForUser($user, $following)
 | 
			
		||||
    {
 | 
			
		||||
        error_log("rediscache::saveRemoteFollowingForUser not implemented");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * save remote posts by user
 | 
			
		||||
     *
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										2
									
								
								sql/2025-05-19.sql
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								sql/2025-05-19.sql
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,2 @@
 | 
			
		|||
create table posts(`id` varchar(255) primary key,`url` varchar(255) not null,`user_id` varchar(255) not null,`actor` varchar(255) not null,`type` varchar(255) not null default 'note',`object` text default null,`to` text default null,`cc` text default null,`published` timestamp not null default current_timestamp);
 | 
			
		||||
update settings set `value`="2025-05-19" where `key`="database_version";
 | 
			
		||||
		Loading…
	
	Add table
		
		Reference in a new issue