forked from grumpydevelop/federator
		
	cleanup and removal of mastodon-integration
- In-&Outbox now use factories to convert data - initial creation of followers-data (wip, not working) - fixed some issue where connector would always fail (as we deliberately always pass username@domain and the regex there always fails) - deleted all mastodon-files (mastodon.ini and mastodon plugin) - minor fix where toObject of create would override id with an empty string if no url is set
This commit is contained in:
		
							parent
							
								
									4d36fc3c61
								
							
						
					
					
						commit
						21f73fb56f
					
				
					 16 changed files with 264 additions and 928 deletions
				
			
		| 
						 | 
				
			
			@ -52,7 +52,7 @@ To configure an apache server, add the following rewrite rules:
 | 
			
		|||
      RewriteCond expr "%{HTTP:content-type} -strcmatch '*application/activity+json*'"
 | 
			
		||||
      RewriteRule ^@(.*)$ /federator.php?_call=fedusers/$1 [L,END]
 | 
			
		||||
      RewriteRule ^users/(.*)$ /federator.php?_call=fedusers/$1 [L,END]
 | 
			
		||||
      RewriteRule ^inbox[/]?$ /federator.php?_call=fedusers/$1 [L,END]
 | 
			
		||||
      RewriteRule ^inbox[/]?$ /federator.php?_call=fedusers/inbox [L,END]
 | 
			
		||||
      RewriteRule ^api/federator/(.+)$ federator.php?_call=$1 [L,END]
 | 
			
		||||
      RewriteRule ^(\.well-known/.*)$ /federator.php?_call=$1 [L,END]
 | 
			
		||||
      RewriteRule ^(nodeinfo/2\.[01])$ /federator.php?_call=$1 [L,END]
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -46,7 +46,7 @@ class Api extends Main
 | 
			
		|||
     */
 | 
			
		||||
    public function __construct()
 | 
			
		||||
    {
 | 
			
		||||
        $this->contentType = "application/activity+json";
 | 
			
		||||
        $this->contentType = "application/json";
 | 
			
		||||
        Main::__construct();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -49,12 +49,23 @@ class FedUsers implements APIInterface
 | 
			
		|||
    {
 | 
			
		||||
        $method = $_SERVER["REQUEST_METHOD"];
 | 
			
		||||
        $handler = null;
 | 
			
		||||
        $username = '';
 | 
			
		||||
        $_username = $paths[1];
 | 
			
		||||
        if (preg_match("#^([^@]+)@([^/]+)#", $_username, $matches) != 1) {
 | 
			
		||||
            $hostUrl = $this->main->getHost();
 | 
			
		||||
            if ($hostUrl !== false) {
 | 
			
		||||
                $host = parse_url($hostUrl, PHP_URL_HOST);
 | 
			
		||||
                $port = parse_url($hostUrl, PHP_URL_PORT);
 | 
			
		||||
                if ($port !== null) {
 | 
			
		||||
                    $host .= ":$port";
 | 
			
		||||
                }
 | 
			
		||||
                $_username = "$_username@$host";
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        switch (sizeof($paths)) {
 | 
			
		||||
            case 2:
 | 
			
		||||
                if ($method === 'GET') {
 | 
			
		||||
                    // /fedusers/username or /@username
 | 
			
		||||
                    return $this->returnUserProfile($paths[1]);
 | 
			
		||||
                    return $this->returnUserProfile($_username);
 | 
			
		||||
                } else {
 | 
			
		||||
                    switch ($paths[1]) {
 | 
			
		||||
                        case 'inbox':
 | 
			
		||||
| 
						 | 
				
			
			@ -76,33 +87,9 @@ class FedUsers implements APIInterface
 | 
			
		|||
                        break;
 | 
			
		||||
                    case 'inbox':
 | 
			
		||||
                        $handler = new FedUsers\Inbox($this->main);
 | 
			
		||||
                        $username = $paths[1];
 | 
			
		||||
                        if (preg_match("#^([^@]+)@([^/]+)#", $username, $matches) != 1) {
 | 
			
		||||
                            $hostUrl = $this->main->getHost();
 | 
			
		||||
                            if ($hostUrl !== false) {
 | 
			
		||||
                                $host = parse_url($hostUrl, PHP_URL_HOST);
 | 
			
		||||
                                $port = parse_url($hostUrl, PHP_URL_PORT);
 | 
			
		||||
                                if ($port !== null) {
 | 
			
		||||
                                    $host .= `:$port`;
 | 
			
		||||
                                }
 | 
			
		||||
                                $username = `$username@$host`;
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                        break;
 | 
			
		||||
                    case 'outbox':
 | 
			
		||||
                        $handler = new FedUsers\Outbox($this->main);
 | 
			
		||||
                        $username = $paths[1];
 | 
			
		||||
                        if (preg_match("#^([^@]+)@([^/]+)#", $username, $matches) != 1) {
 | 
			
		||||
                            $hostUrl = $this->main->getHost();
 | 
			
		||||
                            if ($hostUrl !== false) {
 | 
			
		||||
                                $host = parse_url($hostUrl, PHP_URL_HOST);
 | 
			
		||||
                                $port = parse_url($hostUrl, PHP_URL_PORT);
 | 
			
		||||
                                if ($port !== null) {
 | 
			
		||||
                                    $host .= `:$port`;
 | 
			
		||||
                                }
 | 
			
		||||
                                $username = `$username@$host`;
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                        break;
 | 
			
		||||
                }
 | 
			
		||||
                break;
 | 
			
		||||
| 
						 | 
				
			
			@ -115,10 +102,10 @@ class FedUsers implements APIInterface
 | 
			
		|||
            $ret = false;
 | 
			
		||||
            switch ($method) {
 | 
			
		||||
                case 'GET':
 | 
			
		||||
                    $ret = $handler->get($username);
 | 
			
		||||
                    $ret = $handler->get($_username);
 | 
			
		||||
                    break;
 | 
			
		||||
                case 'POST':
 | 
			
		||||
                    $ret = $handler->post($username);
 | 
			
		||||
                    $ret = $handler->post($_username);
 | 
			
		||||
                    break;
 | 
			
		||||
            }
 | 
			
		||||
            if ($ret !== false) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -48,7 +48,6 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
 | 
			
		|||
     */
 | 
			
		||||
    public function post($_user)
 | 
			
		||||
    {
 | 
			
		||||
        $inboxActivity = null;
 | 
			
		||||
        $_rawInput = file_get_contents('php://input');
 | 
			
		||||
 | 
			
		||||
        $allHeaders = getallheaders();
 | 
			
		||||
| 
						 | 
				
			
			@ -57,231 +56,18 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
 | 
			
		|||
        } catch (\Federator\Exceptions\PermissionDenied $e) {
 | 
			
		||||
            error_log("Inbox::post Signature check failed: " . $e->getMessage());
 | 
			
		||||
            http_response_code(401);
 | 
			
		||||
            exit("Access denied");
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $activity = is_string($_rawInput) ? json_decode($_rawInput, true) : null;
 | 
			
		||||
        $host = $_SERVER['SERVER_NAME'];
 | 
			
		||||
 | 
			
		||||
        if (!is_array($activity)) {
 | 
			
		||||
            throw new \RuntimeException('Invalid activity format.');
 | 
			
		||||
            error_log("Inbox::post Input wasn't of type array");
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        switch ($activity['type']) {
 | 
			
		||||
            case 'Create':
 | 
			
		||||
                if (!isset($activity['object'])) {
 | 
			
		||||
                    break;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                $obj = $activity['object'];
 | 
			
		||||
                $published = strtotime($activity['published'] ?? $obj['published'] ?? 'now');
 | 
			
		||||
                $create = new \Federator\Data\ActivityPub\Common\Create();
 | 
			
		||||
                $create->setAActor($activity['actor'])
 | 
			
		||||
                    ->setID($activity['id'])
 | 
			
		||||
                    ->setURL($activity['id'])
 | 
			
		||||
                    ->setPublished($published !== false ? $published : time());
 | 
			
		||||
 | 
			
		||||
                if (array_key_exists('cc', $activity)) {
 | 
			
		||||
                    foreach ($activity['cc'] as $cc) {
 | 
			
		||||
                        $create->addCC($cc);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (array_key_exists('to', $activity)) {
 | 
			
		||||
                    foreach ($activity['to'] as $to) {
 | 
			
		||||
                        $create->addTo($to);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                switch ($obj['type']) {
 | 
			
		||||
                    case 'Note':
 | 
			
		||||
                        $published = strtotime($obj['published'] ?? 'now');
 | 
			
		||||
                        $apNote = new \Federator\Data\ActivityPub\Common\Note();
 | 
			
		||||
                        $apNote->setID($obj['id'])
 | 
			
		||||
                            ->setPublished($published !== false ? $published : time())
 | 
			
		||||
                            ->setContent($obj['content'] ?? '')
 | 
			
		||||
                            ->setSummary($obj['summary'])
 | 
			
		||||
                            ->setURL($obj['url'])
 | 
			
		||||
                            ->setAttributedTo($obj['attributedTo'] ?? $activity['actor'])
 | 
			
		||||
                            ->addTo("https://www.w3.org/ns/activitystreams#Public");
 | 
			
		||||
 | 
			
		||||
                        if (!empty($obj['sensitive'])) {
 | 
			
		||||
                            $apNote->setSensitive($obj['sensitive']);
 | 
			
		||||
                        }
 | 
			
		||||
                        if (!empty($obj['conversation'])) {
 | 
			
		||||
                            $apNote->setConversation($obj['conversation']);
 | 
			
		||||
                        }
 | 
			
		||||
                        if (!empty($obj['inReplyTo'])) {
 | 
			
		||||
                            $apNote->setInReplyTo($obj['inReplyTo']);
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        // Handle attachments
 | 
			
		||||
                        if (!empty($obj['attachment']) && is_array($obj['attachment'])) {
 | 
			
		||||
                            foreach ($obj['attachment'] as $media) {
 | 
			
		||||
                                if (!isset($media['type'], $media['url']))
 | 
			
		||||
                                    continue;
 | 
			
		||||
                                $mediaObj = new \Federator\Data\ActivityPub\Common\APObject($media['type']);
 | 
			
		||||
                                $mediaObj->setURL($media['url']);
 | 
			
		||||
                                $apNote->addAttachment($mediaObj);
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        if (array_key_exists('tag', $obj)) {
 | 
			
		||||
                            foreach ($obj['tag'] as $tag) {
 | 
			
		||||
                                $tagName = is_array($tag) && isset($tag['name']) ? $tag['name'] : (string) $tag;
 | 
			
		||||
                                $cleanName = preg_replace('/\s+/', '', ltrim($tagName, '#')); // Remove space and leading #
 | 
			
		||||
                                $tagObj = new \Federator\Data\ActivityPub\Common\Tag();
 | 
			
		||||
                                $tagObj->setName('#' . $cleanName)
 | 
			
		||||
                                    ->setHref("https://$host/tags/" . urlencode($cleanName))
 | 
			
		||||
                                    ->setType('Hashtag');
 | 
			
		||||
                                $apNote->addTag($tagObj);
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                        if (array_key_exists('cc', $obj)) {
 | 
			
		||||
                            foreach ($obj['cc'] as $cc) {
 | 
			
		||||
                                $apNote->addCC($cc);
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        $create->setObject($apNote);
 | 
			
		||||
                        break;
 | 
			
		||||
                    default:
 | 
			
		||||
                        error_log("Inbox::post we currently don't support the obj type " . $obj['type'] . "\n");
 | 
			
		||||
                        break;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                $inboxActivity = $create;
 | 
			
		||||
 | 
			
		||||
                break;
 | 
			
		||||
            case 'Announce':
 | 
			
		||||
                if (!isset($activity['object'])) {
 | 
			
		||||
                    break;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                $objectURL = is_array($activity['object']) ? $activity['object']['id'] : $activity['object'];
 | 
			
		||||
 | 
			
		||||
                // Fetch the original object (e.g. Note)
 | 
			
		||||
                [$response, $info] = \Federator\Main::getFromRemote($objectURL, ['Accept: application/activity+json']);
 | 
			
		||||
                if ($info['http_code'] != 200) {
 | 
			
		||||
                    print_r($info);
 | 
			
		||||
                    error_log("Inbox::post Failed to fetch original object for Announce: $objectURL\n");
 | 
			
		||||
                    break;
 | 
			
		||||
                }
 | 
			
		||||
                $objData = json_decode($response, true);
 | 
			
		||||
                if ($objData === false || $objData === null || !is_array($objData)) {
 | 
			
		||||
                    break;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                $published = strtotime((string) $activity['published']);
 | 
			
		||||
                $announce = new \Federator\Data\ActivityPub\Common\Announce();
 | 
			
		||||
                $announce->setAActor((string) $activity['actor'])
 | 
			
		||||
                    ->setPublished($published !== false ? $published : time())
 | 
			
		||||
                    ->setID((string) $activity['id'])
 | 
			
		||||
                    ->setURL((string) $activity['id'])
 | 
			
		||||
                    ->addTo("https://www.w3.org/ns/activitystreams#Public");
 | 
			
		||||
 | 
			
		||||
                if (array_key_exists('cc', $activity)) {
 | 
			
		||||
                    foreach ($activity['cc'] as $cc) {
 | 
			
		||||
                        $announce->addCC($cc);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                if (array_key_exists('to', $activity)) {
 | 
			
		||||
                    foreach ($activity['to'] as $to) {
 | 
			
		||||
                        $announce->addTo($to);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // Parse the shared object as a Note or something else
 | 
			
		||||
                switch ($objData['type']) {
 | 
			
		||||
                    case 'Note':
 | 
			
		||||
                        $published = strtotime($objData['published'] ?? 'now');
 | 
			
		||||
                        $note = new \Federator\Data\ActivityPub\Common\Note();
 | 
			
		||||
                        $note->setPublished($published !== false ? $published : time())
 | 
			
		||||
                            ->setID($objData['id'])
 | 
			
		||||
                            ->setSummary($objData['summary'])
 | 
			
		||||
                            ->setContent($objData['content'] ?? '')
 | 
			
		||||
                            ->setURL($objData['url'] ?? $objData['id'])
 | 
			
		||||
                            ->setAttributedTo($objData['attributedTo'] ?? null)
 | 
			
		||||
                            ->addTo("https://www.w3.org/ns/activitystreams#Public");
 | 
			
		||||
 | 
			
		||||
                        if (array_key_exists('cc', $objData)) {
 | 
			
		||||
                            foreach ($objData['cc'] as $cc) {
 | 
			
		||||
                                $note->addCC($cc);
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                        $announce->setObject($note);
 | 
			
		||||
                        break;
 | 
			
		||||
                    default:
 | 
			
		||||
                        // fallback object
 | 
			
		||||
                        $fallback = new \Federator\Data\ActivityPub\Common\APObject($objData['type']);
 | 
			
		||||
                        $fallback->setID($objData['id'] ?? $objectURL);
 | 
			
		||||
                        $announce->setObject($fallback);
 | 
			
		||||
                        break;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                $inboxActivity = $announce;
 | 
			
		||||
                break;
 | 
			
		||||
            case 'Undo':
 | 
			
		||||
                if (!isset($activity['object'])) {
 | 
			
		||||
                    break;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                $undo = new \Federator\Data\ActivityPub\Common\Undo();
 | 
			
		||||
                $undo->setActor($activity['actor'] ?? null)
 | 
			
		||||
                    ->setID($activity['id'] ?? "test")
 | 
			
		||||
                    ->setURL($activity['url'] ?? $activity['id']);
 | 
			
		||||
 | 
			
		||||
                if (array_key_exists('cc', $activity)) {
 | 
			
		||||
                    foreach ($activity['cc'] as $cc) {
 | 
			
		||||
                        $undo->addCC($cc);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                if (array_key_exists('to', $activity)) {
 | 
			
		||||
                    foreach ($activity['to'] as $to) {
 | 
			
		||||
                        $undo->addTo($to);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // what was undone
 | 
			
		||||
                $undone = $activity['object'];
 | 
			
		||||
                if (is_array($undone) && isset($undone['type'])) {
 | 
			
		||||
                    switch ($undone['type']) {
 | 
			
		||||
                        case 'Announce':
 | 
			
		||||
                            $published = strtotime($undone['published'] ?? 'now');
 | 
			
		||||
                            $announce = new \Federator\Data\ActivityPub\Common\Announce();
 | 
			
		||||
                            $announce->setAActor($undone['actor'] ?? null)
 | 
			
		||||
                                ->setPublished($published !== false ? $published : time())
 | 
			
		||||
                                ->setID($undone['id'] ?? null)
 | 
			
		||||
                                ->setURL($undone['url'] ?? $undone['id']);
 | 
			
		||||
 | 
			
		||||
                            if (array_key_exists('cc', $undone)) {
 | 
			
		||||
                                foreach ($undone['cc'] as $cc) {
 | 
			
		||||
                                    $announce->addCC($cc);
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                            $undo->setObject($announce);
 | 
			
		||||
                            break;
 | 
			
		||||
                        case 'Follow':
 | 
			
		||||
                            // Implement if needed
 | 
			
		||||
                            break;
 | 
			
		||||
                        default:
 | 
			
		||||
                            // Fallback for unknown types
 | 
			
		||||
                            $apObject = new \Federator\Data\ActivityPub\Common\APObject($undone['type']);
 | 
			
		||||
                            $apObject->setID($undone['id'] ?? null);
 | 
			
		||||
                            $undo->setObject($apObject);
 | 
			
		||||
                            break;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                $inboxActivity = $undo;
 | 
			
		||||
                break;
 | 
			
		||||
            default:
 | 
			
		||||
                error_log("Inbox::post we currently don't support the activity type " . $activity['type'] . "\n");
 | 
			
		||||
                $apObject = new \Federator\Data\ActivityPub\Common\Activity($activity['type']);
 | 
			
		||||
                $apObject->setID($activity['id'] ?? null);
 | 
			
		||||
                $inboxActivity = $apObject;
 | 
			
		||||
                break;
 | 
			
		||||
        }
 | 
			
		||||
        $inboxActivity = \Federator\Data\ActivityPub\Factory::newActivityFromJson($activity);
 | 
			
		||||
 | 
			
		||||
        // Shared inbox
 | 
			
		||||
        if (!isset($_user)) {
 | 
			
		||||
| 
						 | 
				
			
			@ -300,13 +86,13 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
 | 
			
		|||
            );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!isset($inboxActivity)) {
 | 
			
		||||
        if ($inboxActivity === false) {
 | 
			
		||||
            error_log("Inbox::post couldn't create inboxActivity, aborting");
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $sendTo = $inboxActivity->getCC();
 | 
			
		||||
        if ($inboxActivity->getType() === 'Undo') {
 | 
			
		||||
        if ($inboxActivity->getType() === 'Undo') { // for undo the object holds the proper cc
 | 
			
		||||
            $object = $inboxActivity->getObject();
 | 
			
		||||
            if ($object !== null) {
 | 
			
		||||
                $sendTo = $object->getCC();
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -67,7 +67,7 @@ class Outbox implements \Federator\Api\FedUsers\FedUsersInterface
 | 
			
		|||
            $items = [];
 | 
			
		||||
        }
 | 
			
		||||
        $host = $_SERVER['SERVER_NAME'];
 | 
			
		||||
        $id = 'https://' . $host . '/' . $_user . '/outbox';
 | 
			
		||||
        $id = 'https://' . $host . '/users/' . $_user . '/outbox';
 | 
			
		||||
        $outbox->setPartOf($id);
 | 
			
		||||
        $outbox->setID($id);
 | 
			
		||||
        if ($page !== '') {
 | 
			
		||||
| 
						 | 
				
			
			@ -84,7 +84,7 @@ class Outbox implements \Federator\Api\FedUsers\FedUsersInterface
 | 
			
		|||
            $outbox->setPrev($id . '&min=' . $oldestId);
 | 
			
		||||
        }
 | 
			
		||||
        $obj = $outbox->toObject();
 | 
			
		||||
        return json_encode($obj, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
 | 
			
		||||
        return json_encode($obj, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT); // todo remove pretty
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
| 
						 | 
				
			
			@ -95,7 +95,6 @@ class Outbox implements \Federator\Api\FedUsers\FedUsersInterface
 | 
			
		|||
     */
 | 
			
		||||
    public function post($_user)
 | 
			
		||||
    {
 | 
			
		||||
        $outboxActivity = null;
 | 
			
		||||
        $_rawInput = file_get_contents('php://input');
 | 
			
		||||
 | 
			
		||||
        $allHeaders = getallheaders();
 | 
			
		||||
| 
						 | 
				
			
			@ -104,233 +103,20 @@ class Outbox implements \Federator\Api\FedUsers\FedUsersInterface
 | 
			
		|||
        } catch (\Federator\Exceptions\PermissionDenied $e) {
 | 
			
		||||
            error_log("Outbox::post Signature check failed: " . $e->getMessage());
 | 
			
		||||
            http_response_code(401);
 | 
			
		||||
            exit("Access denied");
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $activity = is_string($_rawInput) ? json_decode($_rawInput, true) : null;
 | 
			
		||||
        $host = $_SERVER['SERVER_NAME'];
 | 
			
		||||
 | 
			
		||||
        if (!is_array($activity)) {
 | 
			
		||||
            throw new \RuntimeException('Invalid activity format.');
 | 
			
		||||
            error_log("Outbox::post Input wasn't of type array");
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
        $outboxActivity = \Federator\Data\ActivityPub\Factory::newActivityFromJson($activity);
 | 
			
		||||
 | 
			
		||||
        switch ($activity['type']) {
 | 
			
		||||
            case 'Create':
 | 
			
		||||
                if (!isset($activity['object'])) {
 | 
			
		||||
                    break;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                $obj = $activity['object'];
 | 
			
		||||
                $published = strtotime($activity['published'] ?? $obj['published'] ?? 'now');
 | 
			
		||||
                $create = new \Federator\Data\ActivityPub\Common\Create();
 | 
			
		||||
                $create->setAActor($activity['actor'])
 | 
			
		||||
                    ->setID($activity['id'])
 | 
			
		||||
                    ->setURL($activity['id'])
 | 
			
		||||
                    ->setPublished($published !== false ? $published : time());
 | 
			
		||||
 | 
			
		||||
                if (array_key_exists('cc', $activity)) {
 | 
			
		||||
                    foreach ($activity['cc'] as $cc) {
 | 
			
		||||
                        $create->addCC($cc);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (array_key_exists('to', $activity)) {
 | 
			
		||||
                    foreach ($activity['to'] as $to) {
 | 
			
		||||
                        $create->addTo($to);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                switch ($obj['type']) {
 | 
			
		||||
                    case 'Note':
 | 
			
		||||
                        $published = strtotime($obj['published'] ?? 'now');
 | 
			
		||||
                        $apNote = new \Federator\Data\ActivityPub\Common\Note();
 | 
			
		||||
                        $apNote->setID($obj['id'])
 | 
			
		||||
                            ->setPublished($published !== false ? $published : time())
 | 
			
		||||
                            ->setContent($obj['content'] ?? '')
 | 
			
		||||
                            ->setSummary($obj['summary'])
 | 
			
		||||
                            ->setURL($obj['url'])
 | 
			
		||||
                            ->setAttributedTo($obj['attributedTo'] ?? $activity['actor'])
 | 
			
		||||
                            ->addTo("https://www.w3.org/ns/activitystreams#Public");
 | 
			
		||||
 | 
			
		||||
                        if (!empty($obj['sensitive'])) {
 | 
			
		||||
                            $apNote->setSensitive($obj['sensitive']);
 | 
			
		||||
                        }
 | 
			
		||||
                        if (!empty($obj['conversation'])) {
 | 
			
		||||
                            $apNote->setConversation($obj['conversation']);
 | 
			
		||||
                        }
 | 
			
		||||
                        if (!empty($obj['inReplyTo'])) {
 | 
			
		||||
                            $apNote->setInReplyTo($obj['inReplyTo']);
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        // Handle attachments
 | 
			
		||||
                        if (!empty($obj['attachment']) && is_array($obj['attachment'])) {
 | 
			
		||||
                            foreach ($obj['attachment'] as $media) {
 | 
			
		||||
                                if (!isset($media['type'], $media['url']))
 | 
			
		||||
                                    continue;
 | 
			
		||||
                                $mediaObj = new \Federator\Data\ActivityPub\Common\APObject($media['type']);
 | 
			
		||||
                                $mediaObj->setURL($media['url']);
 | 
			
		||||
                                $apNote->addAttachment($mediaObj);
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        if (array_key_exists('tag', $obj)) {
 | 
			
		||||
                            foreach ($obj['tag'] as $tag) {
 | 
			
		||||
                                $tagName = is_array($tag) && isset($tag['name']) ? $tag['name'] : (string) $tag;
 | 
			
		||||
                                $cleanName = preg_replace('/\s+/', '', ltrim($tagName, '#')); // Remove space and leading #
 | 
			
		||||
                                $tagObj = new \Federator\Data\ActivityPub\Common\Tag();
 | 
			
		||||
                                $tagObj->setName('#' . $cleanName)
 | 
			
		||||
                                    ->setHref("https://$host/tags/" . urlencode($cleanName))
 | 
			
		||||
                                    ->setType('Hashtag');
 | 
			
		||||
                                $apNote->addTag($tagObj);
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                        if (array_key_exists('cc', $obj)) {
 | 
			
		||||
                            foreach ($obj['cc'] as $cc) {
 | 
			
		||||
                                $apNote->addCC($cc);
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        $create->setObject($apNote);
 | 
			
		||||
                        break;
 | 
			
		||||
                    default:
 | 
			
		||||
                        error_log("Outbox::post we currently don't support the obj type " . $obj['type'] . "\n");
 | 
			
		||||
                        break;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                $outboxActivity = $create;
 | 
			
		||||
 | 
			
		||||
                break;
 | 
			
		||||
            case 'Announce':
 | 
			
		||||
                if (!isset($activity['object'])) {
 | 
			
		||||
                    break;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                $objectURL = is_array($activity['object']) ? $activity['object']['id'] : $activity['object'];
 | 
			
		||||
 | 
			
		||||
                // Fetch the original object (e.g. Note)
 | 
			
		||||
                [$response, $info] = \Federator\Main::getFromRemote($objectURL, ['Accept: application/activity+json']);
 | 
			
		||||
                if ($info['http_code'] != 200) {
 | 
			
		||||
                    print_r($info);
 | 
			
		||||
                    error_log("Outbox::post Failed to fetch original object for Announce: $objectURL\n");
 | 
			
		||||
                    break;
 | 
			
		||||
                }
 | 
			
		||||
                $objData = json_decode($response, true);
 | 
			
		||||
                if ($objData === false || $objData === null || !is_array($objData)) {
 | 
			
		||||
                    break;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                $published = strtotime((string) $activity['published']);
 | 
			
		||||
                $announce = new \Federator\Data\ActivityPub\Common\Announce();
 | 
			
		||||
                $announce->setAActor((string) $activity['actor'])
 | 
			
		||||
                    ->setPublished($published !== false ? $published : time())
 | 
			
		||||
                    ->setID((string) $activity['id'])
 | 
			
		||||
                    ->setURL((string) $activity['id']);
 | 
			
		||||
 | 
			
		||||
                if (array_key_exists('cc', $activity)) {
 | 
			
		||||
                    foreach ($activity['cc'] as $cc) {
 | 
			
		||||
                        $announce->addCC($cc);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                if (array_key_exists('to', $activity)) {
 | 
			
		||||
                    foreach ($activity['to'] as $to) {
 | 
			
		||||
                        $announce->addTo($to);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // Parse the shared object as a Note or something else
 | 
			
		||||
                switch ($objData['type']) {
 | 
			
		||||
                    case 'Note':
 | 
			
		||||
                        $published = strtotime($objData['published'] ?? 'now');
 | 
			
		||||
                        $note = new \Federator\Data\ActivityPub\Common\Note();
 | 
			
		||||
                        $note->setPublished($published !== false ? $published : time())
 | 
			
		||||
                            ->setID($objData['id'])
 | 
			
		||||
                            ->setSummary($objData['summary'])
 | 
			
		||||
                            ->setContent($objData['content'] ?? '')
 | 
			
		||||
                            ->setURL($objData['url'] ?? $objData['id'])
 | 
			
		||||
                            ->setAttributedTo($objData['attributedTo'] ?? null)
 | 
			
		||||
                            ->addTo("https://www.w3.org/ns/activitystreams#Public");
 | 
			
		||||
 | 
			
		||||
                        if (array_key_exists('cc', $objData)) {
 | 
			
		||||
                            foreach ($objData['cc'] as $cc) {
 | 
			
		||||
                                $note->addCC($cc);
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                        $announce->setObject($note);
 | 
			
		||||
                        break;
 | 
			
		||||
                    default:
 | 
			
		||||
                        // fallback object
 | 
			
		||||
                        $fallback = new \Federator\Data\ActivityPub\Common\APObject($objData['type']);
 | 
			
		||||
                        $fallback->setID($objData['id'] ?? $objectURL);
 | 
			
		||||
                        $announce->setObject($fallback);
 | 
			
		||||
                        break;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                $outboxActivity = $announce;
 | 
			
		||||
                break;
 | 
			
		||||
            case 'Undo':
 | 
			
		||||
                if (!isset($activity['object'])) {
 | 
			
		||||
                    break;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                $undo = new \Federator\Data\ActivityPub\Common\Undo();
 | 
			
		||||
                $undo->setActor($activity['actor'] ?? null)
 | 
			
		||||
                    ->setID($activity['id'] ?? "test")
 | 
			
		||||
                    ->setURL($activity['url'] ?? $activity['id']);
 | 
			
		||||
 | 
			
		||||
                if (array_key_exists('cc', $activity)) {
 | 
			
		||||
                    foreach ($activity['cc'] as $cc) {
 | 
			
		||||
                        $undo->addCC($cc);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                if (array_key_exists('to', $activity)) {
 | 
			
		||||
                    foreach ($activity['to'] as $to) {
 | 
			
		||||
                        $undo->addTo($to);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // what was undone
 | 
			
		||||
                $undone = $activity['object'];
 | 
			
		||||
                if (is_array($undone) && isset($undone['type'])) {
 | 
			
		||||
                    switch ($undone['type']) {
 | 
			
		||||
                        case 'Announce':
 | 
			
		||||
                            $published = strtotime($undone['published'] ?? 'now');
 | 
			
		||||
                            $announce = new \Federator\Data\ActivityPub\Common\Announce();
 | 
			
		||||
                            $announce->setAActor($undone['actor'] ?? null)
 | 
			
		||||
                                ->setPublished($published !== false ? $published : time())
 | 
			
		||||
                                ->setID($undone['id'] ?? null)
 | 
			
		||||
                                ->setURL($undone['url'] ?? $undone['id']);
 | 
			
		||||
 | 
			
		||||
                            if (array_key_exists('cc', $undone)) {
 | 
			
		||||
                                foreach ($undone['cc'] as $cc) {
 | 
			
		||||
                                    $announce->addCC($cc);
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                            $undo->setObject($announce);
 | 
			
		||||
                            break;
 | 
			
		||||
                        case 'Follow':
 | 
			
		||||
                            // Implement if needed
 | 
			
		||||
                            break;
 | 
			
		||||
                        default:
 | 
			
		||||
                            // Fallback for unknown types
 | 
			
		||||
                            $apObject = new \Federator\Data\ActivityPub\Common\APObject($undone['type']);
 | 
			
		||||
                            $apObject->setID($undone['id'] ?? null);
 | 
			
		||||
                            $undo->setObject($apObject);
 | 
			
		||||
                            break;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                $outboxActivity = $undo;
 | 
			
		||||
                break;
 | 
			
		||||
            default:
 | 
			
		||||
                error_log("Outbox::post we currently don't support the activity type " . $activity['type'] . "\n");
 | 
			
		||||
                $apObject = new \Federator\Data\ActivityPub\Common\Activity($activity['type']);
 | 
			
		||||
                $apObject->setID($activity['id'] ?? null);
 | 
			
		||||
                $outboxActivity = $apObject;
 | 
			
		||||
                break;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!isset($outboxActivity)) {
 | 
			
		||||
            error_log("Outbox::post couldn't create outboxActivity, aborting");
 | 
			
		||||
        if ($outboxActivity === false) {
 | 
			
		||||
            error_log("Outbox::post couldn't create outboxActivity");
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										9
									
								
								php/federator/cache/cache.php
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										9
									
								
								php/federator/cache/cache.php
									
										
									
									
										vendored
									
									
								
							| 
						 | 
				
			
			@ -13,6 +13,15 @@ namespace Federator\Cache;
 | 
			
		|||
 */
 | 
			
		||||
interface Cache extends \Federator\Connector\Connector
 | 
			
		||||
{
 | 
			
		||||
    /**
 | 
			
		||||
     * save remote followers of user
 | 
			
		||||
     *
 | 
			
		||||
     * @param string $user user name
 | 
			
		||||
     * @param \Federator\Data\ActivityPub\Common\APObject[]|false $followers user followers
 | 
			
		||||
     * @return void
 | 
			
		||||
     */
 | 
			
		||||
    public function saveRemoteFollowersOfUser($user, $followers);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * save remote posts by user
 | 
			
		||||
     *
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -14,15 +14,24 @@ namespace Federator\Connector;
 | 
			
		|||
interface Connector
 | 
			
		||||
{
 | 
			
		||||
    /**
 | 
			
		||||
     * get posts by given user
 | 
			
		||||
     * get followers of given user
 | 
			
		||||
     *
 | 
			
		||||
     * @param string $id user id
 | 
			
		||||
     * @param string $minId min ID
 | 
			
		||||
     * @param string $maxId max ID
 | 
			
		||||
 | 
			
		||||
     * @return \Federator\Data\ActivityPub\Common\APObject[]|false
 | 
			
		||||
     */
 | 
			
		||||
    public function getRemotePostsByUser($id, $minId, $maxId);
 | 
			
		||||
    public function getRemoteFollowersOfUser($id);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * get posts by given user
 | 
			
		||||
     *
 | 
			
		||||
     * @param string $id user id
 | 
			
		||||
     * @param string $min min date
 | 
			
		||||
     * @param string $max max date
 | 
			
		||||
 | 
			
		||||
     * @return \Federator\Data\ActivityPub\Common\APObject[]|false
 | 
			
		||||
     */
 | 
			
		||||
    public function getRemotePostsByUser($id, $min, $max);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * get remote user by given name
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -26,7 +26,9 @@ class Create extends Activity
 | 
			
		|||
        $return = parent::toObject();
 | 
			
		||||
        $return['type'] = 'Create';
 | 
			
		||||
        // overwrite id from url
 | 
			
		||||
        $return['id'] = $this->getURL();
 | 
			
		||||
        if ($this->getURL() !== '') {
 | 
			
		||||
            $return['id'] = $this->getURL();
 | 
			
		||||
        }
 | 
			
		||||
        return $return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										50
									
								
								php/federator/data/activitypub/common/inbox.php
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								php/federator/data/activitypub/common/inbox.php
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,50 @@
 | 
			
		|||
<?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 Inbox extends OrderedCollectionPage
 | 
			
		||||
{
 | 
			
		||||
    public function __construct()
 | 
			
		||||
    {
 | 
			
		||||
        parent::__construct();
 | 
			
		||||
        parent::addContext('https://www.w3.org/ns/activitystreams');
 | 
			
		||||
        parent::addContexts([
 | 
			
		||||
            "ostatus" => "http://ostatus.org#",
 | 
			
		||||
            "atomUri" => "ostatus:atomUri",
 | 
			
		||||
            "inReplyToAtomUri" => "ostatus:inReplyToAtomUri",
 | 
			
		||||
            "conversation" => "ostatus:conversation",
 | 
			
		||||
            "sensitive" => "as:sensitive",
 | 
			
		||||
            "toot" => "http://joinmastodon.org/ns#",
 | 
			
		||||
            "votersCount" => "toot:votersCount",
 | 
			
		||||
            "Hashtag" => "as:Hashtag"
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 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);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -46,14 +46,20 @@ class Factory
 | 
			
		|||
            case 'Image':
 | 
			
		||||
                $return = new Common\Image();
 | 
			
		||||
                break;
 | 
			
		||||
            /*case 'Note':
 | 
			
		||||
            case 'Note':
 | 
			
		||||
                $return = new Common\Note();
 | 
			
		||||
                break;
 | 
			
		||||
            case 'Question':
 | 
			
		||||
                $return = new \Common\Question();
 | 
			
		||||
            case 'Outbox':
 | 
			
		||||
                $return = new Common\Outbox();
 | 
			
		||||
                break;
 | 
			
		||||
            case 'Inbox':
 | 
			
		||||
                $return = new Common\Inbox();
 | 
			
		||||
                break;
 | 
			
		||||
            /*case 'Question':
 | 
			
		||||
                $return = new Common\Question();
 | 
			
		||||
                break;
 | 
			
		||||
            case 'Video':
 | 
			
		||||
                $return = new \Common\Video();
 | 
			
		||||
                $return = new Common\Video();
 | 
			
		||||
                break;*/
 | 
			
		||||
            default:
 | 
			
		||||
                error_log("newFromJson: unknown type: '" . $json['type'] . "' " . $jsonstring);
 | 
			
		||||
| 
						 | 
				
			
			@ -77,32 +83,30 @@ class Factory
 | 
			
		|||
        }
 | 
			
		||||
        //$return = false;
 | 
			
		||||
        switch ($json['type']) {
 | 
			
		||||
            case 'MakePhanHappy':
 | 
			
		||||
                break;
 | 
			
		||||
/*            case 'Accept':
 | 
			
		||||
            /* case 'Accept':
 | 
			
		||||
                $return = new Common\Accept();
 | 
			
		||||
                break;
 | 
			
		||||
                break; */
 | 
			
		||||
            case 'Announce':
 | 
			
		||||
                $return = new Common\Announce();
 | 
			
		||||
                break;
 | 
			
		||||
            case 'Create':
 | 
			
		||||
                $return = new Common\Create();
 | 
			
		||||
                break;
 | 
			
		||||
            case 'Delete':
 | 
			
		||||
            /*case 'Delete':
 | 
			
		||||
                $return = new Common\Delete();
 | 
			
		||||
                break;
 | 
			
		||||
            case 'Follow':
 | 
			
		||||
                $return = new Common\Follow();
 | 
			
		||||
                break;
 | 
			
		||||
            case 'Undo':
 | 
			
		||||
                $return = new \Common\Undo();
 | 
			
		||||
                break;*/
 | 
			
		||||
            case 'Undo':
 | 
			
		||||
                $return = new Common\Undo();
 | 
			
		||||
                break;
 | 
			
		||||
            default:
 | 
			
		||||
                error_log("newActivityFromJson " . print_r($json, true));
 | 
			
		||||
        }
 | 
			
		||||
        /*if ($return !== false && $return->fromJson($json) !== null) {
 | 
			
		||||
        if (isset($return) && $return->fromJson($json) !== null) {
 | 
			
		||||
            return $return;
 | 
			
		||||
        }*/
 | 
			
		||||
        }
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										55
									
								
								php/federator/dio/followers.php
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								php/federator/dio/followers.php
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,55 @@
 | 
			
		|||
<?php
 | 
			
		||||
/**
 | 
			
		||||
 * SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop
 | 
			
		||||
 * SPDX-License-Identifier: GPL-3.0-or-later
 | 
			
		||||
 *
 | 
			
		||||
 * @author Sascha Nitsch (grumpydeveloper)
 | 
			
		||||
 **/
 | 
			
		||||
 | 
			
		||||
namespace Federator\DIO;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * IO functions related to followers
 | 
			
		||||
 */
 | 
			
		||||
class Followers
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * get followers of user
 | 
			
		||||
     *
 | 
			
		||||
     * @param \mysqli $dbh @unused-param
 | 
			
		||||
     *          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\ActivityPub\Common\APObject[]
 | 
			
		||||
     */
 | 
			
		||||
    public static function getFollowersByUser($dbh, $id, $connector, $cache)
 | 
			
		||||
    {
 | 
			
		||||
        // ask cache
 | 
			
		||||
        if ($cache !== null) {
 | 
			
		||||
            $followers = $cache->getRemoteFollowersOfUser($id);
 | 
			
		||||
            if ($followers !== false) {
 | 
			
		||||
                return $followers;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        $followers = [];
 | 
			
		||||
        // TODO: check our db
 | 
			
		||||
 | 
			
		||||
        if ($followers === []) {
 | 
			
		||||
            // ask connector for user-id
 | 
			
		||||
            $followers = $connector->getRemoteFollowersOfUser($id);
 | 
			
		||||
            if ($followers === false) {
 | 
			
		||||
                $followers = [];
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        // save posts to DB
 | 
			
		||||
        if ($cache !== null) {
 | 
			
		||||
            $cache->saveRemoteFollowersOfUser($id, $followers);
 | 
			
		||||
        }
 | 
			
		||||
        return $followers;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -25,17 +25,17 @@ class Posts
 | 
			
		|||
     *          connector to fetch use with
 | 
			
		||||
     * @param \Federator\Cache\Cache|null $cache
 | 
			
		||||
     *          optional caching service
 | 
			
		||||
     * @param string $minId
 | 
			
		||||
     *          minimum ID
 | 
			
		||||
     * @param string $maxId
 | 
			
		||||
     *          maximum ID
 | 
			
		||||
     * @param string $min
 | 
			
		||||
     *          minimum date
 | 
			
		||||
     * @param string $max
 | 
			
		||||
     *          maximum date
 | 
			
		||||
     * @return \Federator\Data\ActivityPub\Common\APObject[]
 | 
			
		||||
     */
 | 
			
		||||
    public static function getPostsByUser($dbh, $id, $connector, $cache, $minId, $maxId)
 | 
			
		||||
    public static function getPostsByUser($dbh, $id, $connector, $cache, $min, $max)
 | 
			
		||||
    {
 | 
			
		||||
        // ask cache
 | 
			
		||||
        if ($cache !== null) {
 | 
			
		||||
            $posts = $cache->getRemotePostsByUser($id, $minId, $maxId);
 | 
			
		||||
            $posts = $cache->getRemotePostsByUser($id, $min, $max);
 | 
			
		||||
            if ($posts !== false) {
 | 
			
		||||
                return $posts;
 | 
			
		||||
            }
 | 
			
		||||
| 
						 | 
				
			
			@ -45,7 +45,7 @@ class Posts
 | 
			
		|||
 | 
			
		||||
        if ($posts === []) {
 | 
			
		||||
            // ask connector for user-id
 | 
			
		||||
            $posts = $connector->getRemotePostsByUser($id, $minId, $maxId);
 | 
			
		||||
            $posts = $connector->getRemotePostsByUser($id, $min, $max);
 | 
			
		||||
            if ($posts === false) {
 | 
			
		||||
                $posts = [];
 | 
			
		||||
            }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -50,6 +50,32 @@ class ContentNation implements Connector
 | 
			
		|||
        $this->main->setHost($this->service);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * get followers of given user
 | 
			
		||||
     *
 | 
			
		||||
     * @param string $userId user id
 | 
			
		||||
     * @return \Federator\Data\ActivityPub\Common\APObject[]|false
 | 
			
		||||
     */
 | 
			
		||||
    public function getRemoteFollowersOfUser($userId)
 | 
			
		||||
    {
 | 
			
		||||
        if (preg_match("#^([^@]+)@([^/]+)#", $userId, $matches) == 1) {
 | 
			
		||||
            $userId = $matches[1];
 | 
			
		||||
        }
 | 
			
		||||
        $remoteURL = $this->service . '/api/profile/' . urlencode($userId) . '/followers'; // todo implement/change
 | 
			
		||||
 | 
			
		||||
        [$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
 | 
			
		||||
     *
 | 
			
		||||
| 
						 | 
				
			
			@ -231,14 +257,17 @@ class ContentNation implements Connector
 | 
			
		|||
     */
 | 
			
		||||
    public function getRemoteUserByName(string $_name)
 | 
			
		||||
    {
 | 
			
		||||
        if (preg_match("#^([^@]+)@([^/]+)#", $_name, $matches) == 1) {
 | 
			
		||||
            $_name = $matches[1];
 | 
			
		||||
        }
 | 
			
		||||
        // validate name
 | 
			
		||||
        if (preg_match("/^[a-zA-Z0-9_\-]+$/", $_name) != 1) {
 | 
			
		||||
        if (preg_match("/^[a-zA-Z@0-9\._\-]+$/", $_name) != 1) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
        $remoteURL = $this->service . '/api/users/info?user=' . urlencode($_name);
 | 
			
		||||
        // make sure we only get name part, without domain
 | 
			
		||||
        if (preg_match("#^([^@]+)@([^/]+)#", $_name, $matches) == 1) {
 | 
			
		||||
            $name = $matches[1];
 | 
			
		||||
        } else {
 | 
			
		||||
            $name = $_name;
 | 
			
		||||
        }
 | 
			
		||||
        $remoteURL = $this->service . '/api/users/info?user=' . urlencode($name);
 | 
			
		||||
        $headers = ['Accept: application/json'];
 | 
			
		||||
        [$response, $info] = \Federator\Main::getFromRemote($remoteURL, $headers);
 | 
			
		||||
        if ($info['http_code'] != 200) {
 | 
			
		||||
| 
						 | 
				
			
			@ -274,7 +303,7 @@ class ContentNation implements Connector
 | 
			
		|||
        if (preg_match("/^[a-z0-9]{16}$/", $_session) != 1) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
        if (preg_match("/^[a-zA-Z0-9_\-]+$/", $_user) != 1) {
 | 
			
		||||
        if (preg_match("/^[a-zA-Z@0-9\._\-]+$/", $_user) != 1) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
        $remoteURL = $this->service . '/api/users/permissions?profile=' . urlencode($_user);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -19,15 +19,26 @@ class DummyConnector implements Connector
 | 
			
		|||
    {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * get followers of given user
 | 
			
		||||
     *
 | 
			
		||||
     * @param string $userId user id @unused-param
 | 
			
		||||
     * @return \Federator\Data\ActivityPub\Common\APObject[]|false
 | 
			
		||||
     */
 | 
			
		||||
    public function getRemoteFollowersOfUser($userId)
 | 
			
		||||
    {
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * get posts by given user
 | 
			
		||||
     *
 | 
			
		||||
     * @param string $id user id @unused-param
 | 
			
		||||
     * @param string $minId min ID @unused-param
 | 
			
		||||
     * @param string $maxId max ID @unused-param
 | 
			
		||||
     * @param string $min min date @unused-param
 | 
			
		||||
     * @param string $max max date @unused-param
 | 
			
		||||
     * @return \Federator\Data\ActivityPub\Common\APObject[]|false
 | 
			
		||||
     */
 | 
			
		||||
    public function getRemotePostsByUser($id, $minId, $maxId)
 | 
			
		||||
    public function getRemotePostsByUser($id, $min, $max)
 | 
			
		||||
    {
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,417 +0,0 @@
 | 
			
		|||
<?php
 | 
			
		||||
/**
 | 
			
		||||
 * SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop
 | 
			
		||||
 * SPDX-License-Identifier: GPL-3.0-or-later
 | 
			
		||||
 *
 | 
			
		||||
 * @author Sascha Nitsch (grumpydeveloper)
 | 
			
		||||
 **/
 | 
			
		||||
 | 
			
		||||
namespace Federator\Connector;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Connector to Mastodon.social
 | 
			
		||||
 */
 | 
			
		||||
class Mastodon implements Connector
 | 
			
		||||
{
 | 
			
		||||
    /**
 | 
			
		||||
     * config parameter
 | 
			
		||||
     *
 | 
			
		||||
     * @var array<string, mixed> $config
 | 
			
		||||
     */
 | 
			
		||||
    private $config;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * main instance
 | 
			
		||||
     *
 | 
			
		||||
     * @var \Federator\Main $main
 | 
			
		||||
     */
 | 
			
		||||
    private $main;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * service-URL
 | 
			
		||||
     *
 | 
			
		||||
     * @var string $service
 | 
			
		||||
     */
 | 
			
		||||
    private $service;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * constructor
 | 
			
		||||
     *
 | 
			
		||||
     * @param \Federator\Main $main
 | 
			
		||||
     */
 | 
			
		||||
    public function __construct($main)
 | 
			
		||||
    {
 | 
			
		||||
        $config = parse_ini_file($_SERVER['DOCUMENT_ROOT'] . '../mastodon.ini', true);
 | 
			
		||||
        if ($config !== false) {
 | 
			
		||||
            $this->config = $config;
 | 
			
		||||
        }
 | 
			
		||||
        $this->service = $config['mastodon']['service-uri'];
 | 
			
		||||
        $this->main = $main;
 | 
			
		||||
        $this->main->setHost($this->service);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 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
 | 
			
		||||
     */
 | 
			
		||||
    public function getRemotePostsByUser($userId, $min = null, $max = null)
 | 
			
		||||
    {
 | 
			
		||||
        if (preg_match("#^([^@]+)@([^/]+)#", $userId, $matches) == 1) {
 | 
			
		||||
            $name = $matches[1];
 | 
			
		||||
        } else {
 | 
			
		||||
            $name = $userId;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $remoteURL = $this->service . '/users/' . $name . '/outbox';
 | 
			
		||||
        if ($min !== '') {
 | 
			
		||||
            $remoteURL .= '&minTS=' . urlencode($min);
 | 
			
		||||
        }
 | 
			
		||||
        if ($max !== '') {
 | 
			
		||||
            $remoteURL .= '&maxTS=' . urlencode($max);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $items = [];
 | 
			
		||||
 | 
			
		||||
        [$outboxResponse, $outboxInfo] = \Federator\Main::getFromRemote($remoteURL, ['Accept: application/activity+json']);
 | 
			
		||||
 | 
			
		||||
        if ($outboxInfo['http_code'] != 200) {
 | 
			
		||||
            echo "MastodonConnector::getRemotePostsByUser HTTP call failed for remoteURL $remoteURL\n";
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $outbox = json_decode($outboxResponse, true);
 | 
			
		||||
 | 
			
		||||
        // retrieve ALL outbox items - disabled for now
 | 
			
		||||
        /* do {
 | 
			
		||||
            // Fetch the current page of items (first or subsequent pages)
 | 
			
		||||
            [$outboxResponse, $outboxInfo] = \Federator\Main::getFromRemote($remoteURL, ['Accept: application/activity+json']);
 | 
			
		||||
 | 
			
		||||
            if ($outboxInfo['http_code'] !== 200) {
 | 
			
		||||
                echo "MastodonConnector::getRemotePostsByUser HTTP call failed for remoteURL $remoteURL\n";
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            $outbox = json_decode($outboxResponse, true);
 | 
			
		||||
 | 
			
		||||
            // Extract orderedItems from the current page
 | 
			
		||||
            if (isset($outbox['orderedItems'])) {
 | 
			
		||||
                $items = array_merge($items, $outbox['orderedItems']);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Use 'next' or 'last' URL to determine pagination
 | 
			
		||||
            if (isset($outbox['next'])) {
 | 
			
		||||
                $remoteURL = $outbox['next']; // Update target URL for the next page of items
 | 
			
		||||
            } else if (isset($outbox['last'])) {
 | 
			
		||||
                $remoteURL = $outbox['last']; // Update target URL for the next page of items
 | 
			
		||||
            } else {
 | 
			
		||||
                $remoteURL = "";
 | 
			
		||||
                break; // No more pages, exit pagination
 | 
			
		||||
            }
 | 
			
		||||
            if ($remoteURL !== "") {
 | 
			
		||||
                if ($min !== '') {
 | 
			
		||||
                    $remoteURL .= '&minTS=' . urlencode($min);
 | 
			
		||||
                }
 | 
			
		||||
                if ($max !== '') {
 | 
			
		||||
                    $remoteURL .= '&maxTS=' . urlencode($max);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        } while ($remoteURL !== ""); // Continue fetching until no 'last' URL */
 | 
			
		||||
 | 
			
		||||
        // Follow `first` page (or get orderedItems directly)
 | 
			
		||||
        if (isset($outbox['orderedItems'])) {
 | 
			
		||||
            $items = $outbox['orderedItems'];
 | 
			
		||||
        } elseif (isset($outbox['first'])) {
 | 
			
		||||
            $firstURL = is_array($outbox['first']) ? $outbox['first']['id'] : $outbox['first'];
 | 
			
		||||
            [$pageResponse, $pageInfo] = \Federator\Main::getFromRemote($firstURL, ['Accept: application/activity+json']);
 | 
			
		||||
            if ($pageInfo['http_code'] != 200) {
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
            $page = json_decode($pageResponse, true);
 | 
			
		||||
            $items = $page['orderedItems'] ?? [];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Convert to internal representation
 | 
			
		||||
        $posts = [];
 | 
			
		||||
        $host = $_SERVER['SERVER_NAME'];
 | 
			
		||||
        foreach ($items as $activity) {
 | 
			
		||||
            switch ($activity['type']) {
 | 
			
		||||
                case 'Create':
 | 
			
		||||
                    if (!isset($activity['object'])) {
 | 
			
		||||
                        break;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    $obj = $activity['object'];
 | 
			
		||||
                    $published = strtotime($activity['published'] ?? $obj['published'] ?? 'now');
 | 
			
		||||
                    $create = new \Federator\Data\ActivityPub\Common\Create();
 | 
			
		||||
                    $create->setAActor($activity['actor'])
 | 
			
		||||
                        ->setID($activity['id'])
 | 
			
		||||
                        ->setURL($activity['id'])
 | 
			
		||||
                        ->setPublished($published !== false ? $published : time())
 | 
			
		||||
                        ->addCC($activity['cc']);
 | 
			
		||||
 | 
			
		||||
                    if (array_key_exists('to', $activity)) {
 | 
			
		||||
                        foreach ($activity['to'] as $to) {
 | 
			
		||||
                            $create->addTo($to);
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    switch ($obj['type']) {
 | 
			
		||||
                        case 'Note':
 | 
			
		||||
                            $published = strtotime($obj['published'] ?? 'now');
 | 
			
		||||
                            $apNote = new \Federator\Data\ActivityPub\Common\Note();
 | 
			
		||||
                            $apNote->setID($obj['id'])
 | 
			
		||||
                                ->setPublished($published !== false ? $published : time())
 | 
			
		||||
                                ->setContent($obj['content'] ?? '')
 | 
			
		||||
                                ->setSummary($obj['summary'])
 | 
			
		||||
                                ->setURL($obj['url'])
 | 
			
		||||
                                ->setAttributedTo($obj['attributedTo'] ?? $activity['actor'])
 | 
			
		||||
                                ->addTo("https://www.w3.org/ns/activitystreams#Public");
 | 
			
		||||
 | 
			
		||||
                            if (!empty($obj['sensitive'])) {
 | 
			
		||||
                                $apNote->setSensitive($obj['sensitive']);
 | 
			
		||||
                            }
 | 
			
		||||
                            if (!empty($obj['conversation'])) {
 | 
			
		||||
                                $apNote->setConversation($obj['conversation']);
 | 
			
		||||
                            }
 | 
			
		||||
                            if (!empty($obj['inReplyTo'])) {
 | 
			
		||||
                                $apNote->setInReplyTo($obj['inReplyTo']);
 | 
			
		||||
                            }
 | 
			
		||||
 | 
			
		||||
                            // Handle attachments
 | 
			
		||||
                            if (!empty($obj['attachment']) && is_array($obj['attachment'])) {
 | 
			
		||||
                                foreach ($obj['attachment'] as $media) {
 | 
			
		||||
                                    if (!isset($media['type'], $media['url']))
 | 
			
		||||
                                        continue;
 | 
			
		||||
                                    $mediaObj = new \Federator\Data\ActivityPub\Common\APObject($media['type']);
 | 
			
		||||
                                    $mediaObj->setURL($media['url']);
 | 
			
		||||
                                    $apNote->addAttachment($mediaObj);
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
 | 
			
		||||
                            if (array_key_exists('tag', $obj)) {
 | 
			
		||||
                                foreach ($obj['tag'] as $tag) {
 | 
			
		||||
                                    $tagName = is_array($tag) && isset($tag['name']) ? $tag['name'] : (string) $tag;
 | 
			
		||||
                                    $cleanName = preg_replace('/\s+/', '', ltrim($tagName, '#')); // Remove space and leading #
 | 
			
		||||
                                    $tagObj = new \Federator\Data\ActivityPub\Common\Tag();
 | 
			
		||||
                                    $tagObj->setName('#' . $cleanName)
 | 
			
		||||
                                        ->setHref("https://$host/tags/" . urlencode($cleanName))
 | 
			
		||||
                                        ->setType('Hashtag');
 | 
			
		||||
                                    $apNote->addTag($tagObj);
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
 | 
			
		||||
                            $create->setObject($apNote);
 | 
			
		||||
                            break;
 | 
			
		||||
                        default:
 | 
			
		||||
                            echo "MastodonConnector::getRemotePostsByUser we currently don't support the obj type " . $obj['type'] . "\n";
 | 
			
		||||
                            break;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    $posts[] = $create;
 | 
			
		||||
 | 
			
		||||
                    break;
 | 
			
		||||
                case 'Announce':
 | 
			
		||||
                    if (!isset($activity['object'])) {
 | 
			
		||||
                        break;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    $objectURL = is_array($activity['object']) ? $activity['object']['id'] : $activity['object'];
 | 
			
		||||
 | 
			
		||||
                    if (!is_string($objectURL)) {
 | 
			
		||||
                        // Handle error or cast/normalize
 | 
			
		||||
                        throw new \InvalidArgumentException('objectURL must be a string, got ' . gettype($objectURL));
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    // Fetch the original object (e.g. Note)
 | 
			
		||||
                    [$response, $info] = \Federator\Main::getFromRemote($objectURL, ['Accept: application/activity+json']);
 | 
			
		||||
                    if ($info['http_code'] != 200) {
 | 
			
		||||
                        print_r($info);
 | 
			
		||||
                        echo "MastodonConnector::getRemotePostsByUser Failed to fetch original object for Announce: $objectURL\n";
 | 
			
		||||
                        break;
 | 
			
		||||
                    }
 | 
			
		||||
                    $objData = json_decode($response, true);
 | 
			
		||||
                    if ($objData === false || $objData === null || !is_array($objData)) {
 | 
			
		||||
                        break;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    assert(is_string($activity['id']));
 | 
			
		||||
                    assert(is_string($activity['actor']));
 | 
			
		||||
 | 
			
		||||
                    $published = strtotime((string) $activity['published']);
 | 
			
		||||
                    $announce = new \Federator\Data\ActivityPub\Common\Announce();
 | 
			
		||||
                    $announce->setAActor($activity['actor'])
 | 
			
		||||
                        ->setPublished($published !== false ? $published : time())
 | 
			
		||||
                        ->setID($activity['id'])
 | 
			
		||||
                        ->setURL($activity['id'])
 | 
			
		||||
                        ->addTo("https://www.w3.org/ns/activitystreams#Public");
 | 
			
		||||
 | 
			
		||||
                    if (array_key_exists('to', $activity)) {
 | 
			
		||||
                        foreach ($activity['to'] as $to) {
 | 
			
		||||
                            $announce->addTo($to);
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    // Optionally parse the shared object as a Note or something else
 | 
			
		||||
                    switch ($objData['type']) {
 | 
			
		||||
                        case 'Note':
 | 
			
		||||
                            $published = strtotime($objData['published'] ?? 'now');
 | 
			
		||||
                            $note = new \Federator\Data\ActivityPub\Common\Note();
 | 
			
		||||
                            $note->setID($objData['id'])
 | 
			
		||||
                                ->setContent($objData['content'] ?? '')
 | 
			
		||||
                                ->setPublished($published !== false ? $published : time())
 | 
			
		||||
                                ->setURL($objData['url'] ?? $objData['id'])
 | 
			
		||||
                                ->setAttributedTo($objData['attributedTo'] ?? null)
 | 
			
		||||
                                ->addTo("https://www.w3.org/ns/activitystreams#Public");
 | 
			
		||||
 | 
			
		||||
                            $announce->setObject($note);
 | 
			
		||||
                            break;
 | 
			
		||||
                        default:
 | 
			
		||||
                            // fallback object
 | 
			
		||||
                            $fallback = new \Federator\Data\ActivityPub\Common\APObject($objData['type']);
 | 
			
		||||
                            $fallback->setID($objData['id'] ?? $objectURL);
 | 
			
		||||
                            $announce->setObject($fallback);
 | 
			
		||||
                            break;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    $posts[] = $announce;
 | 
			
		||||
                    break;
 | 
			
		||||
                default:
 | 
			
		||||
                    echo "MastodonConnector::getRemotePostsByUser we currently don't support the activity type " . $activity['type'] . "\n";
 | 
			
		||||
                    break;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $posts;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * get statistics from remote system
 | 
			
		||||
     *
 | 
			
		||||
     * @return \Federator\Data\Stats|false
 | 
			
		||||
     */
 | 
			
		||||
    public function getRemoteStats()
 | 
			
		||||
    {
 | 
			
		||||
        $remoteURL = $this->service . '/api/stats';
 | 
			
		||||
        [$response, $info] = \Federator\Main::getFromRemote($remoteURL, []);
 | 
			
		||||
        if ($info['http_code'] != 200) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
        $r = json_decode($response, true);
 | 
			
		||||
        if ($r === false || $r === null || !is_array($r)) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
        $stats = new \Federator\Data\Stats();
 | 
			
		||||
        $stats->userCount = array_key_exists('userCount', $r) ? $r['userCount'] : 0;
 | 
			
		||||
        $stats->postCount = array_key_exists('pageCount', $r) ? $r['pageCount'] : 0;
 | 
			
		||||
        $stats->commentCount = array_key_exists('commentCount', $r) ? $r['commentCount'] : 0;
 | 
			
		||||
        return $stats;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * get remote user by given name
 | 
			
		||||
     *
 | 
			
		||||
     * @param string $_name user/profile name
 | 
			
		||||
     * @return \Federator\Data\User | false
 | 
			
		||||
     */
 | 
			
		||||
    public function getRemoteUserByName(string $_name)
 | 
			
		||||
    {
 | 
			
		||||
        // Validate username (Mastodon usernames can include @ and domain parts)
 | 
			
		||||
        if (preg_match("/^[a-zA-Z0-9_\-@.]+$/", $_name) != 1) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Mastodon lookup API endpoint
 | 
			
		||||
        $remoteURL = $this->service . '/api/v1/accounts/lookup?acct=' . urlencode($_name);
 | 
			
		||||
        // Set headers
 | 
			
		||||
        $headers = ['Accept: application/json'];
 | 
			
		||||
 | 
			
		||||
        // Fetch data from Mastodon instance
 | 
			
		||||
        [$response, $info] = \Federator\Main::getFromRemote($remoteURL, $headers);
 | 
			
		||||
 | 
			
		||||
        // Handle HTTP errors
 | 
			
		||||
        if ($info['http_code'] != 200) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Decode response
 | 
			
		||||
        $r = json_decode($response, true);
 | 
			
		||||
        if ($r === false || $r === null || !is_array($r)) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $createdAt = strtotime($r['created_at']);
 | 
			
		||||
        // Map response to User object
 | 
			
		||||
        $user = new \Federator\Data\User();
 | 
			
		||||
        $user->externalid = (string) $r['id']; // Mastodon uses numeric IDs
 | 
			
		||||
        $user->iconMediaType = 'image/png'; // Mastodon doesn't explicitly return this, assume PNG
 | 
			
		||||
        $user->iconURL = $r['avatar'] ?? null;
 | 
			
		||||
        $user->imageMediaType = 'image/png';
 | 
			
		||||
        $user->imageURL = $r['header'] ?? null;
 | 
			
		||||
        $user->name = $r['display_name'] ?: $r['username'];
 | 
			
		||||
        $user->summary = $r['note'];
 | 
			
		||||
        $user->type = 'Person'; // Mastodon profiles are ActivityPub "Person" objects
 | 
			
		||||
        $user->registered = $createdAt !== false ? $createdAt : time();
 | 
			
		||||
 | 
			
		||||
        return $user;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * get remote user by given session
 | 
			
		||||
     *
 | 
			
		||||
     * @param string $_session session id
 | 
			
		||||
     * @param string $_user user or profile name
 | 
			
		||||
     * @return \Federator\Data\User | false
 | 
			
		||||
     */
 | 
			
		||||
    public function getRemoteUserBySession(string $_session, string $_user)
 | 
			
		||||
    {
 | 
			
		||||
        // validate $_session and $user
 | 
			
		||||
        if (preg_match("/^[a-z0-9]{16}$/", $_session) != 1) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
        if (preg_match("/^[a-zA-Z0-9_\-]+$/", $_user) != 1) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
        $remoteURL = $this->service . '/api/users/permissions?profile=' . urlencode($_user);
 | 
			
		||||
        $headers = ['Cookie: session=' . $_session, 'Accept: application/json'];
 | 
			
		||||
        [$response, $info] = \Federator\Main::getFromRemote($remoteURL, $headers);
 | 
			
		||||
 | 
			
		||||
        if ($info['http_code'] != 200) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
        $r = json_decode($response, true);
 | 
			
		||||
        if ($r === false || !is_array($r) || !array_key_exists($_user, $r)) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
        $user = $this->getRemoteUserByName($_user);
 | 
			
		||||
        if ($user === false) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
        // extend with permissions
 | 
			
		||||
        $user->permissions = [];
 | 
			
		||||
        $user->session = $_session;
 | 
			
		||||
        foreach ($r[$_user] as $p) {
 | 
			
		||||
            $user->permissions[] = $p;
 | 
			
		||||
        }
 | 
			
		||||
        return $user;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
namespace Federator;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Function to initialize plugin
 | 
			
		||||
 *
 | 
			
		||||
 * @param  \Federator\Main $main main instance
 | 
			
		||||
 * @return void
 | 
			
		||||
 */
 | 
			
		||||
function mastodon_load($main)
 | 
			
		||||
{
 | 
			
		||||
    $mast = new Connector\Mastodon($main);
 | 
			
		||||
    # echo "mastodon::mastodon_load Loaded new connector, adding to main\n"; // TODO change to proper log
 | 
			
		||||
    $main->setConnector($mast);
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -78,15 +78,28 @@ class RedisCache implements Cache
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * get posts by given user
 | 
			
		||||
     * get followers of given user
 | 
			
		||||
     *
 | 
			
		||||
     * @param string $id user id @unused-param
 | 
			
		||||
     * @param string $minId min ID @unused-param
 | 
			
		||||
     * @param string $maxId max ID @unused-param
 | 
			
		||||
 | 
			
		||||
     * @return \Federator\Data\ActivityPub\Common\APObject[]|false
 | 
			
		||||
     */
 | 
			
		||||
    public function getRemotePostsByUser($id, $minId, $maxId)
 | 
			
		||||
    public function getRemoteFollowersOfUser($id)
 | 
			
		||||
    {
 | 
			
		||||
        error_log("rediscache::getRemoteFollowersOfUser not implemented");
 | 
			
		||||
        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
 | 
			
		||||
     */
 | 
			
		||||
    public function getRemotePostsByUser($id, $min, $max)
 | 
			
		||||
    {
 | 
			
		||||
        error_log("rediscache::getRemotePostsByUser not implemented");
 | 
			
		||||
        return false;
 | 
			
		||||
| 
						 | 
				
			
			@ -152,6 +165,18 @@ class RedisCache implements Cache
 | 
			
		|||
        return $user;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * save remote followers by user
 | 
			
		||||
     *
 | 
			
		||||
     * @param string $user user name @unused-param
 | 
			
		||||
     * @param \Federator\Data\ActivityPub\Common\APObject[]|false $followers user followers @unused-param
 | 
			
		||||
     * @return void
 | 
			
		||||
     */
 | 
			
		||||
    public function saveRemoteFollowersOfUser($user, $followers)
 | 
			
		||||
    {
 | 
			
		||||
        error_log("rediscache::saveRemoteFollowersOfUser not implemented");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * save remote posts by user
 | 
			
		||||
     *
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		
		Reference in a new issue