diff --git a/php/federator/api.php b/php/federator/api.php index 94333f8..91117ec 100644 --- a/php/federator/api.php +++ b/php/federator/api.php @@ -78,7 +78,7 @@ class Api extends Main $host = 'dummy'; // fallback // Check if path matches something like fedusers/username@domain.tld - if (preg_match("#^fedusers/([^@]+)@([^/]+)$#", $this->path, $matches) === 1) { + if (preg_match("#^fedusers/([^@]+)@([^/]+)/.*$#", $this->path, $matches) === 1) { $host = strtolower($matches[2]); // extract domain } else { $host = 'dummy'; diff --git a/php/federator/api/fedusers/outbox.php b/php/federator/api/fedusers/outbox.php index 16ea175..ffdcf60 100644 --- a/php/federator/api/fedusers/outbox.php +++ b/php/federator/api/fedusers/outbox.php @@ -40,6 +40,7 @@ class Outbox implements \Federator\Api\FedUsers\FedUsersInterface { $dbh = $this->main->getDatabase(); $cache = $this->main->getCache(); + // get user $user = \Federator\DIO\User::getUserByName( $dbh, @@ -69,10 +70,6 @@ class Outbox implements \Federator\Api\FedUsers\FedUsersInterface if ($page !== '') { $id .= '?page=' . urlencode($page); } - if ($page === '' || $outbox->count() == 0) { - $outbox->setFirst($id); - $outbox->setLast($id . '&min=0'); - } if (sizeof($items)>0) { $newestId = $items[0]->getPublished(); $oldestId = $items[sizeof($items)-1]->getPublished(); diff --git a/plugins/federator/contentnation.php b/plugins/federator/contentnation.php index a9051b6..b7d4ac0 100644 --- a/plugins/federator/contentnation.php +++ b/plugins/federator/contentnation.php @@ -244,7 +244,7 @@ class ContentNation implements Connector */ public function getRemoteUserByName(string $_name) { - if (preg_match("#^([^@]+)@([^/]+)$#", $_name, $matches) === 1) { + if (preg_match("#^([^@]+)@([^/]+)#", $_name, $matches) === 1) { $_name = $matches[1]; } // validate name diff --git a/plugins/federator/mastodon.php b/plugins/federator/mastodon.php index 96bfdd1..3a9c3f1 100644 --- a/plugins/federator/mastodon.php +++ b/plugins/federator/mastodon.php @@ -48,13 +48,14 @@ class Mastodon implements Connector $this->service = $config['mastodon']['service-uri']; $this->main = $main; } - + /** * get the host this connector is dedicated to * * @return string */ - public function getHost() { + public function getHost() + { return "mastodon.social"; } @@ -68,22 +69,35 @@ class Mastodon implements Connector */ public function getRemotePostsByUser($userId, $min = null, $max = null) { - $remoteURL = $this->service . '/users/' . $userId . '/outbox'; + if (preg_match("#^([^@]+)@([^/]+)#", $userId, $matches) === 1) { + $name = $matches[1]; + } + $remoteURL = $this->service . '/users/' . $name . '/outbox'; + if ($min !== '') { + $remoteURL .= '&minTS=' . urlencode($min); + } + if ($max !== '') { + $remoteURL .= '&maxTS=' . urlencode($max); + } $items = []; - do { - if ($min !== '') { - $remoteURL .= '&minTS=' . urlencode($min); - } - if ($max !== '') { - $remoteURL .= '&maxTS=' . urlencode($max); - } + [$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 "aborting"; + echo "MastodonConnector::getRemotePostsByUser HTTP call failed for remoteURL $remoteURL\n"; return false; } @@ -94,18 +108,25 @@ class Mastodon implements Connector $items = array_merge($items, $outbox['orderedItems']); } - // Step 4: Use 'last' URL to determine pagination - if (isset($outbox['last'])) { - // The 'last' URL will usually have a query string that includes min_id for the next set of results - $remoteURL = $outbox['last']; // Update to the last URL for the next page of items + // 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 (!empty($outbox['last'])); // Continue fetching until no 'last' URL - - - $items = []; + } while ($remoteURL !== ""); // Continue fetching until no 'last' URL */ // Follow `first` page (or get orderedItems directly) if (isset($outbox['orderedItems'])) { @@ -122,42 +143,86 @@ class Mastodon implements Connector // Convert to internal representation $posts = []; + $host = $_SERVER['SERVER_NAME']; foreach ($items as $activity) { - if (!isset($activity['type']) || $activity['type'] !== 'Create' || !isset($activity['object'])) { - continue; // Skip non-Create activities - } - - $obj = $activity['object']; - $create = new \Federator\Data\ActivityPub\Common\Create(); - $create->setID($activity['id']) - ->setPublished(strtotime($activity['published'] ?? $obj['published'] ?? 'now')) - ->setAActor($activity['actor']) - ->addTo("https://www.w3.org/ns/activitystreams#Public"); - - // Handle main Note content - if ($obj['type'] === 'Note') { - $apNote = new \Federator\Data\ActivityPub\Common\Note(); - $apNote->setID($obj['id']) - ->setPublished(strtotime($obj['published'] ?? 'now')) - ->setContent($obj['content'] ?? '') - ->setAttributedTo($obj['attributedTo'] ?? $activity['actor']) - ->addTo("https://www.w3.org/ns/activitystreams#Public"); - - // 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); + switch ($activity['type']) { + case 'Create': + if (!isset($activity['object'])) { + break; } - } - $create->setObject($apNote); + $obj = $activity['object']; + $create = new \Federator\Data\ActivityPub\Common\Create(); + $create->setID($activity['id']) + ->setURL($activity['id']) + ->setPublished(strtotime($activity['published'] ?? $obj['published'] ?? 'now')) + ->setAActor($activity['actor']) + ->addCC($activity['cc']); + + if (array_key_exists('to', $activity)) { + foreach ($activity['to'] as $to) { + $create->addTo($to); + } + } + + switch ($obj['type']) { + case 'Note': + $apNote = new \Federator\Data\ActivityPub\Common\Note(); + $apNote->setID($obj['id']) + ->setPublished(strtotime($obj['published'] ?? 'now')) + ->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; + default: + echo "MastodonConnector::getRemotePostsByUser we currently don't support the activity type " . $activity['type'] . "\n"; + break; } - - $posts[] = $create; } return $posts;