diff --git a/contentnation.ini b/contentnation.ini index 17de2d5..6f447fd 100644 --- a/contentnation.ini +++ b/contentnation.ini @@ -1,5 +1,5 @@ [contentnation] -service-uri = https://contentnation.net +service-uri = http://local.contentnation.net [userdata] path = '/home/net/contentnation/userdata/htdocs/' // need to download local copy of image and put img-path here diff --git a/mastodon.ini b/mastodon.ini index 27f66ee..7e54f58 100644 --- a/mastodon.ini +++ b/mastodon.ini @@ -1,5 +1,5 @@ [mastodon] -service-uri = https://mastodon.social +service-uri = http://mastodon.local [userdata] path = '/home/net/contentnation/userdata/htdocs/' // need to download local copy of image and put img-path here diff --git a/php/federator/api.php b/php/federator/api.php index 5b6e99a..8463319 100644 --- a/php/federator/api.php +++ b/php/federator/api.php @@ -46,7 +46,7 @@ class Api extends Main */ public function __construct() { - $this->contentType = "application/json"; + $this->contentType = "application/activity+json"; Main::__construct(); } diff --git a/php/federator/api/fedusers.php b/php/federator/api/fedusers.php index 6a71bee..727c304 100644 --- a/php/federator/api/fedusers.php +++ b/php/federator/api/fedusers.php @@ -52,12 +52,20 @@ class FedUsers implements APIInterface switch (sizeof($paths)) { case 2: if ($method === 'GET') { - // /users/username or /@username + // /fedusers/username or /@username return $this->returnUserProfile($paths[1]); + } else { + switch ($paths[1]) { + case 'inbox': + $handler = new FedUsers\Inbox($this->main); + break; + default: + break; + } } break; case 3: - // /users/username/(inbox|outbox|following|followers) + // /fedusers/username/(inbox|outbox|following|followers) switch ($paths[2]) { case 'following': // $handler = new FedUsers\Following(); @@ -66,7 +74,19 @@ class FedUsers implements APIInterface // $handler = new FedUsers\Followers(); break; case 'inbox': - // $handler = new FedUsers\Inbox(); + $handler = new FedUsers\Inbox($this->main); + $user = $paths[1]; + if (!preg_match("#^([^@]+)@([^/]+)#", $user, $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`; + } + $user = `$user@$host`; + } + } break; case 'outbox': $handler = new FedUsers\Outbox($this->main); @@ -86,7 +106,7 @@ class FedUsers implements APIInterface } break; case 4: - // /users/username/collections/(features|tags) + // /fedusers/username/collections/(features|tags) // not yet implemented break; } diff --git a/php/federator/api/fedusers/inbox.php b/php/federator/api/fedusers/inbox.php new file mode 100644 index 0000000..de40196 --- /dev/null +++ b/php/federator/api/fedusers/inbox.php @@ -0,0 +1,401 @@ +main = $main; + } + + /** + * handle get call + * + * @param string $_user user to fetch inbox for @unused-param + * @return string|false response + */ + public function get($_user) + { + return false; + } + + /** + * handle post call + * + * @param string $_user user to add data to inbox + * @return string|false response + */ + public function post($_user) + { + $inboxActivity = null; + $_rawInput = file_get_contents('php://input'); + + $activity = json_decode($_rawInput, true); + $host = $_SERVER['SERVER_NAME']; + + $sendTo = []; + + switch ($activity['type']) { + case 'Create': + if (!isset($activity['object'])) { + break; + } + + $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']); + + 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': + $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: + 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; + } + + $announce = new \Federator\Data\ActivityPub\Common\Announce(); + $announce->setID($activity['id']) + ->setURL($activity['id']) + ->setPublished(strtotime($activity['published'] ?? 'now')) + ->setAActor($activity['actor']); + + 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': + $note = new \Federator\Data\ActivityPub\Common\Note(); + $note->setID($objData['id']) + ->setContent($objData['content'] ?? '') + ->setPublished(strtotime($objData['published'] ?? 'now')) + ->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->setID($activity['id'] ?? "test") + ->setURL($activity['url'] ?? $activity['id']) + ->setActor($activity['actor'] ?? null); + + 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': + $announce = new \Federator\Data\ActivityPub\Common\Announce(); + $announce->setID($undone['id'] ?? null) + ->setAActor($undone['actor'] ?? null) + ->setURL($undone['url'] ?? $undone['id']) + ->setPublished(strtotime($undone['published'] ?? 'now')); + + 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"); + break; + } + + // Shared inbox + if (!$_user) { + $rootDir = $_SERVER['DOCUMENT_ROOT'] . '../'; + file_put_contents( + $rootDir . 'logs/inbox.log', + date('Y-m-d H:i:s') . ": ==== WILL TRY WORK WITH ACTIVITY ====\n" . json_encode($activity, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) . "\n\n", + FILE_APPEND + ); + + // Save the raw input and parsed JSON to a file for inspection + file_put_contents( + $rootDir . 'logs/inbox.log', + date('Y-m-d H:i:s') . ": ==== POST Inbox Activity ====\n" . json_encode($inboxActivity, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) . "\n\n", + FILE_APPEND + ); + } + + $sendTo = $inboxActivity->getCC(); + if ($inboxActivity->getType() === 'Undo') { + $sendTo = $inboxActivity->getObject()->getCC(); + } + + $users = []; + + foreach ($sendTo as $receiver) { + if (!$receiver || !is_string($receiver)) { + continue; + } + + if (str_ends_with($receiver, '/followers')) { + $users = array_merge($users, $this->fetchAllFollowers($receiver, $host)); + } + } + if ($_user !== false && in_array($_user, $users)) { + $users[] = $_user; + } + foreach ($users as $user) { + if (!$user) + continue; + + $this->postForUser($user, $inboxActivity); + } + return json_encode($inboxActivity, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT); + } + + /** + * handle post call for specific user + * + * @param string $_user user to add data to inbox + * @param \Federator\Data\ActivityPub\Common\Activity $inboxActivity the activity that we received + * @return string|false response + */ + private function postForUser($_user, $inboxActivity) + { + if ($_user) { + $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; + } + } + + $rootDir = $_SERVER['DOCUMENT_ROOT'] . '../'; + // Save the raw input and parsed JSON to a file for inspection + file_put_contents( + $rootDir . 'logs/inbox_' . $_user . '.log', + date('Y-m-d H:i:s') . ": ==== POST " . $_user . " Inbox Activity ====\n" . json_encode($inboxActivity, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) . "\n\n", + FILE_APPEND + ); + + return true; + } + + /** + * fetch all followers from url and return the ones that belong to our server + * + * @param string $collectionUrl The url of f.e. the posters followers + * @param string $host our current host-url + * @return array|false the names of the followers that are hosted on our server + */ + private function fetchAllFollowers(string $collectionUrl, string $host): array + { + $users = []; + + [$collectionResponse, $collectionInfo] = \Federator\Main::getFromRemote($collectionUrl, ['Accept: application/activity+json']); + if ($collectionInfo['http_code'] !== 200) { + error_log("Inbox::fetchAllFollowers Failed to fetch follower collection metadata from $collectionUrl"); + return []; + } + + $collectionData = json_decode($collectionResponse, true); + $nextPage = $collectionData['first'] ?? $collectionData['current'] ?? null; + + if (!$nextPage) { + error_log("Inbox::fetchAllFollowers No 'first' or 'current' page in collection at $collectionUrl"); + return []; + } + + // Loop through all pages + while ($nextPage) { + [$pageResponse, $pageInfo] = \Federator\Main::getFromRemote($nextPage, ['Accept: application/activity+json']); + if ($pageInfo['http_code'] !== 200) { + error_log("Inbox::fetchAllFollowers Failed to fetch follower page at $nextPage"); + break; + } + + $pageData = json_decode($pageResponse, true); + $items = $pageData['orderedItems'] ?? $pageData['items'] ?? []; + + foreach ($items as $followerUrl) { + $parts = parse_url($followerUrl); + if (!isset($parts['host']) || !str_ends_with($parts['host'], $host)) { + continue; + } + + [$actorResponse, $actorInfo] = \Federator\Main::getFromRemote($followerUrl, ['Accept: application/activity+json']); + if ($actorInfo['http_code'] !== 200) { + error_log("Inbox::fetchAllFollowers Failed to fetch actor data for follower: $followerUrl"); + continue; + } + + $actorData = json_decode($actorResponse, true); + if (isset($actorData['preferredUsername'])) { + $users[] = $actorData['preferredUsername']; + } + } + + $nextPage = $pageData['next'] ?? null; + } + + return $users; + } +} diff --git a/php/federator/api/v1/dummy.php b/php/federator/api/v1/dummy.php index 6914691..3a0d4b7 100644 --- a/php/federator/api/v1/dummy.php +++ b/php/federator/api/v1/dummy.php @@ -23,7 +23,7 @@ class Dummy implements \Federator\Api\APIInterface /** * internal message to output * - * @var array $message + * @var string $response */ private $message = []; @@ -44,29 +44,100 @@ class Dummy implements \Federator\Api\APIInterface * @param \Federator\Data\User|false $user user who is calling us * @return bool true on success */ - public function exec($paths, $user) : bool + public function exec($paths, $user): bool { // only for user with the 'publish' permission - if ($user === false || $user->hasPermission('publish') === false) { - throw new \Federator\Exceptions\PermissionDenied(); - } + // if ($user === false || $user->hasPermission('publish') === false) { + // throw new \Federator\Exceptions\PermissionDenied(); + // } $method = $_SERVER["REQUEST_METHOD"]; switch ($method) { case 'GET': switch (sizeof($paths)) { case 3: - if ($paths[2] === 'moo') { - return $this->getDummy(); + switch ($paths[2]) { + case 'moo': + return $this->getDummy(); + case 'sharedInbox': + return $this->getSharedInbox(); + default: + break; + } + break; + case 4: + case 5: + switch ($paths[2]) { + case 'inbox': + return $this->getInbox($paths[3]); + case 'follow': + return $this->followAdmin($paths[3]); + case 'users': + switch (sizeof($paths)) { + case 4: + return $this->getUser($paths[3]); + case 5: + switch ($paths[4]) { + case 'inbox': + return $this->getInbox($paths[3]); + case 'outbox': + return $this->getOutbox($paths[3]); + case 'following': + return $this->getFollowing($paths[3]); + case 'followers': + return $this->getFollowing($paths[3]); + default: + break; + } + break; + default: + break; + } + default: + break; } } break; case 'POST': switch (sizeof($paths)) { case 3: - if ($paths[2] === 'moo') { - return $this->postDummy(); + switch ($paths[2]) { + case 'moo': + return $this->postDummy(); + case 'sharedInbox': + return $this->postSharedInbox(); + default: + break; } break; + case 4: + case 5: + switch ($paths[2]) { + case 'inbox': + return $this->postInbox($paths[3]); + case 'follow': + return $this->followAdmin($paths[3]); + case 'users': + switch (sizeof($paths)) { + case 5: + switch ($paths[4]) { + case 'inbox': + return $this->postInbox($paths[3]); + case 'outbox': + return $this->postOutbox($paths[3]); + case 'following': + return $this->postFollowing($paths[3]); + case 'followers': + return $this->postFollowing($paths[3]); + default: + break; + } + break; + default: + break; + } + default: + break; + } } } $this->main->setResponseCode(404); @@ -80,16 +151,190 @@ class Dummy implements \Federator\Api\APIInterface */ public function getDummy() { - $this->message = [ + $this->message = json_encode([ 'r1' => ' (__) ', 'r2' => ' `------(oo) ', 'r3' => ' || __ (__) ', 'r4' => ' ||w || ', 'r5' => ' ' - ]; + ], JSON_PRETTY_PRINT); return true; } + public function getUser($_name) + { + error_log("Someone tried to get user: " . $_name); + $user = \Federator\DIO\User::getUserByName( + $this->main->getDatabase(), + $_name, + $this->main->getConnector(), + $this->main->getCache() + ); + if ($user === false || $user->id === null) { + throw new \Federator\Exceptions\FileNotFound(); + } + $publicKeyPem = << $user->iconMediaType, + 'iconURL' => $user->iconURL, + 'imageMediaType' => $user->imageMediaType, + 'imageURL' => $user->imageURL, + 'fqdn' => '192.168.178.143', + 'name' => $user->name, + 'username' => $user->id, + 'publickey' => "", + 'registered' => gmdate('Y-m-d\TH:i:s\Z', $user->registered), // 2021-03-25T00:00:00Z + 'summary' => $user->summary, + 'type' => "Person" + ]; + $this->message = $this->main->renderTemplate('user.json', $data); + $fixedJson = str_replace( + 'https://192.168.178.143/users/yannis_test', + 'https://192.168.178.143/api/federator/v1/dummy/users/yannis_test', + $this->message + ); + $fixedJson = preg_replace( + '/"id"\s*:\s*"[^"]+"/', + '"id": "http://192.168.178.143/api/federator/v1/dummy/users/yannis_test"', + $fixedJson + ); + $fixedJson = preg_replace( + '/"inbox"\s*:\s*"[^"]+"/', + '"inbox": "http://192.168.178.143/users/yannis_test/inbox"', + $fixedJson + ); + $fixedJson = str_replace( + 'https://192.168.178.143', + 'http://192.168.178.143', + $fixedJson + ); + $fixedJson = str_replace( + '""', + $publicKeyPemJsonSafe, + $fixedJson + ); + $fixedJson = str_replace( + 'http://192.168.178.143/inbox', + 'http://192.168.178.143/api/federator/fedusers/inbox', + $fixedJson + ); + // $fixedJson = str_replace( + // 'http://192.168.178.143/api/federator/v1/dummy/users/yannis_test@192.168.178.143#main-key', + // 'http://192.168.178.143/api/federator/v1/dummy/users/yannis_test@192.168.178.143/key#main-key', + // $fixedJson + // ); + + $this->message = $fixedJson; + return true; + } + + public function followAdmin($_name) + { + $user = \Federator\DIO\User::getUserByName( + $this->main->getDatabase(), + $_name, + $this->main->getConnector(), + $this->main->getCache() + ); + if ($user === false || $user->id === null) { + throw new \Federator\Exceptions\FileNotFound(); + } + + + // Step 2: Prepare the Follow activity + $activityData = [ + '@context' => 'https://www.w3.org/ns/activitystreams', + 'type' => 'Follow', + 'actor' => 'http://192.168.178.143/api/federator/v1/dummy/users/' . $_name, // Your user URL + 'object' => 'http://mastodon.local/users/admin' // Mastodon user to follow (e.g., http://mastodon.local/users/admin) + ]; + + // Step 3: Send the Follow activity to Mastodon + $inboxUrl = 'http://mastodon.local/users/admin/inbox'; // The inbox URL for the Mastodon user + $this->sendFollowActivityToMastodon($inboxUrl, $activityData); + + $this->message = "\n"; + return true; + } + + private function sendFollowActivityToMastodon($url, $data) + { + $json = json_encode($data, JSON_UNESCAPED_SLASHES); + $digest = 'SHA-256=' . base64_encode(hash('sha256', $json, true)); + $date = gmdate('D, d M Y H:i:s') . ' GMT'; + $parsed = parse_url($url); + $host = $parsed['host']; + $path = $parsed['path']; + + // Build the signature string + $signatureString = "(request-target): post {$path}\n" . + "host: {$host}\n" . + "date: {$date}\n" . + "digest: {$digest}"; + + // Load your private key here (replace with how you store keys) + $privateKey = "REDACTED"; // OR from DB + $pkeyId = openssl_pkey_get_private($privateKey); + + if (!$pkeyId) { + throw new \Exception('Invalid private key'); + } + + openssl_sign($signatureString, $signature, $pkeyId, OPENSSL_ALGO_SHA256); + $signature_b64 = base64_encode($signature); + + // Build keyId (public key ID from your actor object) + $keyId = 'http://192.168.178.143/api/federator/v1/dummy/users/yannis_test#main-key'; + + $signatureHeader = 'keyId="' . $keyId . '",algorithm="rsa-sha256",headers="(request-target) host date digest",signature="' . $signature_b64 . '"'; + + $headers = [ + 'Host: ' . $host, + 'Date: ' . $date, + 'Digest: ' . $digest, + 'Content-Type: application/activity+json', + 'Signature: ' . $signatureHeader, + 'Accept: application/activity+json', + ]; + + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, $json); + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + + $response = curl_exec($ch); + $err = curl_error($ch); + curl_close($ch); + + $response = curl_exec($ch); + curl_close($ch); + + // Log the response for debugging if needed + if ($response === false) { + error_log("Failed to send Follow activity to Mastodon: " . curl_error($ch)); + echo "Failed to send Follow activity to Mastodon: " . curl_error($ch); + } else { + $httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + if ($httpcode !== 200 && $httpcode !== 202) { + throw new \Exception("Unexpected HTTP code $httpcode: $response"); + } + error_log("Follow activity response from Mastodon: " . $response); + echo "Follow activity response from Mastodon: " . $response; + } + } + /** * post function for /v1/dummy/moo" * @@ -100,6 +345,216 @@ class Dummy implements \Federator\Api\APIInterface return $this->getDummy(); } + public function getInbox($_name) + { + $_rawInput = file_get_contents('php://input'); + + // Decode if it's JSON (as Mastodon usually sends JSON) + $jsonData = json_decode($_rawInput, true); + error_log("=== Masto GET Inbox Raw ===\n" . $_rawInput); + error_log("=== Masto GET Inbox JSON ===\n" . print_r($jsonData, true)); + // Save the raw input and parsed JSON to a file for inspection + file_put_contents( + __DIR__ . '/inbox_log.txt', + time() . ": ==== Masto GET Inbox RAW ====\n" . $_rawInput . "\n\n==== Masto GET Inbox JSON ====\n" . print_r($jsonData, true) . "\n\n", + FILE_APPEND + ); + + $this->message = json_encode([ + 'status' => 'received', + ]); + return true; + } + + public function getOutbox($_name) + { + $_rawInput = file_get_contents('php://input'); + + // Decode if it's JSON (as Mastodon usually sends JSON) + $jsonData = json_decode($_rawInput, true); + error_log("=== Masto GET Outbox Raw ===\n" . $_rawInput); + error_log("=== Masto GET Outbox JSON ===\n" . print_r($jsonData, true)); + // Save the raw input and parsed JSON to a file for inspection + file_put_contents( + __DIR__ . '/outbox_log.txt', + time() . ": ==== Masto GET Outbox RAW ====\n" . $_rawInput . "\n\n==== Masto GET Outbox JSON ====\n" . print_r($jsonData, true) . "\n\n", + FILE_APPEND + ); + + $this->message = json_encode([ + 'status' => 'received', + ]); + return true; + } + + public function getFollowing($_name) + { + $_rawInput = file_get_contents('php://input'); + + // Decode if it's JSON (as Mastodon usually sends JSON) + $jsonData = json_decode($_rawInput, true); + error_log("=== Masto GET Following Raw ===\n" . $_rawInput); + error_log("=== Masto GET Following JSON ===\n" . print_r($jsonData, true)); + // Save the raw input and parsed JSON to a file for inspection + file_put_contents( + __DIR__ . '/following_log.txt', + time() . ": ==== Masto GET Following RAW ====\n" . $_rawInput . "\n\n==== Masto GET Following JSON ====\n" . print_r($jsonData, true) . "\n\n", + FILE_APPEND + ); + + $this->message = json_encode([ + 'status' => 'received', + ]); + return true; + } + + public function getFollowers($_name) + { + $_rawInput = file_get_contents('php://input'); + + // Decode if it's JSON (as Mastodon usually sends JSON) + $jsonData = json_decode($_rawInput, true); + error_log("=== Masto GET Followers Raw ===\n" . $_rawInput); + error_log("=== Masto GET Followers JSON ===\n" . print_r($jsonData, true)); + // Save the raw input and parsed JSON to a file for inspection + file_put_contents( + __DIR__ . '/followers_log.txt', + time() . ": ==== Masto GET Followers RAW ====\n" . $_rawInput . "\n\n==== Masto GET Followers JSON ====\n" . print_r($jsonData, true) . "\n\n", + FILE_APPEND + ); + + $this->message = json_encode([ + 'status' => 'received', + ]); + return true; + } + + public function getSharedInbox() + { + $_rawInput = file_get_contents('php://input'); + + // Decode if it's JSON (as Mastodon usually sends JSON) + $jsonData = json_decode($_rawInput, true); + error_log("=== Masto GET SharedInbox Raw ===\n" . $_rawInput); + error_log("=== Masto GET SharedInbox JSON ===\n" . print_r($jsonData, true)); + // Save the raw input and parsed JSON to a file for inspection + file_put_contents( + __DIR__ . '/sharedInbox_log.txt', + time() . ": ==== Masto GET SharedInbox RAW ====\n" . $_rawInput . "\n\n==== Masto GET SharedInbox JSON ====\n" . print_r($jsonData, true) . "\n\n", + FILE_APPEND + ); + + $this->message = json_encode([ + 'status' => 'received', + ]); + return true; + } + + public function postInbox($_name) + { + $_rawInput = file_get_contents('php://input'); + + // Decode if it's JSON (as Mastodon usually sends JSON) + $jsonData = json_decode($_rawInput, true); + error_log("=== Masto POST Inbox Raw ===\n" . $_rawInput); + error_log("=== Masto POST Inbox JSON ===\n" . print_r($jsonData, true)); + // Save the raw input and parsed JSON to a file for inspection + file_put_contents( + __DIR__ . '/inbox_log.txt', + time() . ": ==== Masto POST Inbox RAW ====\n" . $_rawInput . "\n\n==== Masto POST Inbox JSON ====\n" . print_r($jsonData, true) . "\n\n", + FILE_APPEND + ); + + $this->message = json_encode([ + 'status' => 'received', + ]); + return true; + } + + public function postOutbox($_name) + { + $_rawInput = file_get_contents('php://input'); + + // Decode if it's JSON (as Mastodon usually sends JSON) + $jsonData = json_decode($_rawInput, true); + error_log("=== Masto POST Outbox Raw ===\n" . $_rawInput); + error_log("=== Masto POST Outbox JSON ===\n" . print_r($jsonData, true)); + // Save the raw input and parsed JSON to a file for inspection + file_put_contents( + __DIR__ . '/outbox_log.txt', + time() . ": ==== Masto POST Outbox RAW ====\n" . $_rawInput . "\n\n==== Masto POST Outbox JSON ====\n" . print_r($jsonData, true) . "\n\n", + FILE_APPEND + ); + + $this->message = json_encode([ + 'status' => 'received', + ]); + return true; + } + + public function postFollowing($_name) + { + $_rawInput = file_get_contents('php://input'); + + // Decode if it's JSON (as Mastodon usually sends JSON) + $jsonData = json_decode($_rawInput, true); + error_log("=== Masto POST Following Raw ===\n" . $_rawInput); + error_log("=== Masto POST Following JSON ===\n" . print_r($jsonData, true)); + // Save the raw input and parsed JSON to a file for inspection + file_put_contents( + __DIR__ . '/following_log.txt', + time() . ": ==== Masto POST Following RAW ====\n" . $_rawInput . "\n\n==== Masto POST Following JSON ====\n" . print_r($jsonData, true) . "\n\n", + FILE_APPEND + ); + + $this->message = json_encode([ + 'status' => 'received', + ]); + return true; + } + + public function postFollowers($_name) + { + $_rawInput = file_get_contents('php://input'); + + // Decode if it's JSON (as Mastodon usually sends JSON) + $jsonData = json_decode($_rawInput, true); + error_log("=== Masto POST Followers Raw ===\n" . $_rawInput); + error_log("=== Masto POST Followers JSON ===\n" . print_r($jsonData, true)); + // Save the raw input and parsed JSON to a file for inspection + file_put_contents( + __DIR__ . '/followers_log.txt', + time() . ": ==== Masto POST Followers RAW ====\n" . $_rawInput . "\n\n==== Masto POST Followers JSON ====\n" . print_r($jsonData, true) . "\n\n", + FILE_APPEND + ); + + $this->message = json_encode([ + 'status' => 'received', + ]); + return true; + } + + public function postSharedInbox() + { + $_rawInput = file_get_contents('php://input'); + + // Decode if it's JSON (as Mastodon usually sends JSON) + $jsonData = json_decode($_rawInput, true); + error_log("=== Masto POST SharedInbox Raw ===\n" . $_rawInput); + error_log("=== Masto POST SharedInbox JSON ===\n" . print_r($jsonData, true)); + // Save the raw input and parsed JSON to a file for inspection + file_put_contents( + __DIR__ . '/sharedInbox_log.txt', + time() . ": ==== Masto POST SharedInbox RAW ====\n" . $_rawInput . "\n\n==== Masto POST SharedInbox JSON ====\n" . print_r($jsonData, true) . "\n\n", + FILE_APPEND + ); + + $this->message = json_encode([ + 'status' => 'received', + ]); + return true; + } + /** * get internal represenation as json string * @@ -107,6 +562,6 @@ class Dummy implements \Federator\Api\APIInterface */ public function toJson() { - return json_encode($this->message, JSON_PRETTY_PRINT) . "\n"; + return $this->message; } } diff --git a/php/federator/data/activitypub/common/Announce.php b/php/federator/data/activitypub/common/Announce.php new file mode 100644 index 0000000..4f18004 --- /dev/null +++ b/php/federator/data/activitypub/common/Announce.php @@ -0,0 +1,43 @@ + + */ + public function toObject() + { + $return = parent::toObject(); + $return['type'] = 'Announce'; + // overwrite id from url + $return['id'] = $this->getURL(); + return $return; + } + + /** + * create object from json + * + * @param array $json input json + * @return bool true on success + */ + public function fromJson($json) + { + return parent::fromJson($json); + } +} diff --git a/php/federator/data/activitypub/common/Undo.php b/php/federator/data/activitypub/common/Undo.php new file mode 100644 index 0000000..b4732d1 --- /dev/null +++ b/php/federator/data/activitypub/common/Undo.php @@ -0,0 +1,43 @@ + + */ + public function toObject() + { + $return = parent::toObject(); + $return['type'] = 'Undo'; + // overwrite id from url + $return['id'] = $this->getURL(); + return $return; + } + + /** + * create object from json + * + * @param array $json input json + * @return bool true on success + */ + public function fromJson($json) + { + return parent::fromJson($json); + } +} diff --git a/php/federator/main.php b/php/federator/main.php index 8721e9b..c7da143 100644 --- a/php/federator/main.php +++ b/php/federator/main.php @@ -262,7 +262,7 @@ class Main public function setConnector(Connector\Connector $connector) : void { if ($this->connector) { - echo "main::setConnector Setting new connector will override old one.\n"; // TODO CHANGE TO LOG WARNING + # echo "main::setConnector Setting new connector will override old one.\n"; // TODO CHANGE TO LOG WARNING } $this->connector = $connector; } @@ -275,7 +275,7 @@ class Main public function setHost(string $host) : void { if ($this->host) { - echo "main::setHost Setting new host will override old one.\n"; // TODO CHANGE TO LOG WARNING + # echo "main::setHost Setting new host will override old one.\n"; // TODO CHANGE TO LOG WARNING } $this->host = $host; } diff --git a/plugins/federator/contentnation.php b/plugins/federator/contentnation.php index 3f116f8..dc7ef5f 100644 --- a/plugins/federator/contentnation.php +++ b/plugins/federator/contentnation.php @@ -310,6 +310,6 @@ namespace Federator; function contentnation_load($main) { $cn = new Connector\ContentNation($main); - echo "contentnation::contentnation_load Loaded new connector, adding to main\n"; // TODO change to proper log + # echo "contentnation::contentnation_load Loaded new connector, adding to main\n"; // TODO change to proper log $main->setConnector($cn); } diff --git a/plugins/federator/dummyconnector.php b/plugins/federator/dummyconnector.php index 0459db0..6a011cc 100644 --- a/plugins/federator/dummyconnector.php +++ b/plugins/federator/dummyconnector.php @@ -87,6 +87,6 @@ namespace Federator; function dummy_load($main) { $dummy = new Connector\DummyConnector(); - echo "dummyconnector::dummy_load Loaded new connector, adding to main\n"; // TODO change to proper log + # echo "dummyconnector::dummy_load Loaded new connector, adding to main\n"; // TODO change to proper log $main->setConnector($dummy); } diff --git a/plugins/federator/mastodon.php b/plugins/federator/mastodon.php index c176d5b..689abbf 100644 --- a/plugins/federator/mastodon.php +++ b/plugins/federator/mastodon.php @@ -62,7 +62,10 @@ class Mastodon implements Connector { if (preg_match("#^([^@]+)@([^/]+)#", $userId, $matches) === 1) { $name = $matches[1]; + } else { + $name = $userId; } + $remoteURL = $this->service . '/users/' . $name . '/outbox'; if ($min !== '') { $remoteURL .= '&minTS=' . urlencode($min); @@ -209,6 +212,61 @@ class Mastodon implements Connector $posts[] = $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); + 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; + } + + $announce = new \Federator\Data\ActivityPub\Common\Announce(); + $announce->setID($activity['id']) + ->setURL($activity['id']) + ->setPublished(strtotime($activity['published'] ?? 'now')) + ->setAActor($activity['actor']) + ->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': + $note = new \Federator\Data\ActivityPub\Common\Note(); + $note->setID($objData['id']) + ->setContent($objData['content'] ?? '') + ->setPublished(strtotime($objData['published'] ?? 'now')) + ->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"; @@ -269,7 +327,6 @@ class Mastodon implements Connector // Mastodon lookup API endpoint $remoteURL = $this->service . '/api/v1/accounts/lookup?acct=' . urlencode($_name); - // Set headers $headers = ['Accept: application/json']; @@ -354,6 +411,6 @@ namespace Federator; 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 + # echo "mastodon::mastodon_load Loaded new connector, adding to main\n"; // TODO change to proper log $main->setConnector($mast); }