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