forked from grumpydevelop/federator
Compare commits
No commits in common. "mastodon-support" and "develop" have entirely different histories.
mastodon-s
...
develop
21 changed files with 87 additions and 2095 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -5,4 +5,3 @@ php-docs
|
|||
.phpdoc
|
||||
phpdoc
|
||||
html
|
||||
/cache
|
||||
|
|
|
@ -14,8 +14,6 @@ compiledir = '../cache'
|
|||
[plugins]
|
||||
rediscache = 'rediscache.php'
|
||||
dummy = 'dummyconnector.php'
|
||||
contentnation = 'contentnation.php'
|
||||
mastodon = 'mastodon.php'
|
||||
|
||||
[maintenance]
|
||||
username = 'federatoradmin'
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
[contentnation]
|
||||
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
|
||||
url = 'https://userdata.contentnation.net'
|
|
@ -1,112 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>API Request UI</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
</head>
|
||||
|
||||
<body class="flex justify-center items-center min-h-screen bg-gray-100">
|
||||
<div class="w-full max-w-3xl bg-white shadow-lg rounded-lg p-6 m-6">
|
||||
<h2 class="text-2xl font-semibold mb-4 text-center">API Request UI</h2>
|
||||
|
||||
<div id="request-container">
|
||||
<!-- Request Form Template -->
|
||||
<div class="request-box border p-4 rounded-lg mb-4 bg-gray-50 overflow-y-auto">
|
||||
<label class="block font-medium">API target link</label>
|
||||
<input type="text" class="target-link-input w-full p-2 border rounded-md mb-2"
|
||||
placeholder="Enter target link" value="federator/fedusers/grumpydevelop/outbox?page=0">
|
||||
|
||||
<label class="block font-medium">Request type</label>
|
||||
<input type="text" class="request-type-input w-full p-2 border rounded-md mb-2"
|
||||
placeholder="POST or GET" value="GET">
|
||||
|
||||
<label class="block font-medium">X-Session:</label>
|
||||
<input type="text" class="session-input w-full p-2 border rounded-md mb-2"
|
||||
placeholder="Enter X-Session token" value="">
|
||||
|
||||
<label class="block font-medium">X-Profile:</label>
|
||||
<input type="text" class="profile-input w-full p-2 border rounded-md mb-2" placeholder="Enter X-Profile"
|
||||
value="">
|
||||
|
||||
<div class="buttonContainer">
|
||||
<button class="send-btn bg-blue-500 text-white px-4 py-2 rounded-md w-full hover:bg-blue-600">
|
||||
Send Request
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p class="mt-2 text-sm text-gray-700">Response:</p>
|
||||
<p class="response mt-2 text-sm text-gray-700 font-mono whitespace-pre-wrap">Waiting for response</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button id="add-request" class="mt-4 bg-green-500 text-white px-4 py-2 rounded-md w-full hover:bg-green-600">
|
||||
Add Another Request
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function sendRequest(button) {
|
||||
const container = button.parentElement.parentElement;
|
||||
const targetLink = container.querySelector(".target-link-input").value;
|
||||
const requestType = container.querySelector(".request-type-input").value;
|
||||
const session = container.querySelector(".session-input").value;
|
||||
const profile = container.querySelector(".profile-input").value;
|
||||
const responseField = container.querySelector(".response");
|
||||
button.parentElement.style.cursor = "not-allowed";
|
||||
button.style.pointerEvents = "none";
|
||||
button.textContent = "Sending...";
|
||||
responseField.textContent = "Waiting for response";
|
||||
|
||||
const headers = {
|
||||
...(session ? { "X-Session": session } : {}),
|
||||
...(profile ? { "X-Profile": profile } : {})
|
||||
};
|
||||
|
||||
fetch("http://localhost/api/" + targetLink, {
|
||||
method: requestType,
|
||||
headers
|
||||
})
|
||||
.then(response => response.text())
|
||||
.then(data => {
|
||||
responseField.textContent = data;
|
||||
button.parentElement.style.cursor = "";
|
||||
button.style.pointerEvents = "";
|
||||
button.textContent = "Send Request";
|
||||
})
|
||||
.catch(error => {
|
||||
responseField.textContent = "Error: " + error;
|
||||
button.parentElement.style.cursor = "";
|
||||
button.style.pointerEvents = "";
|
||||
button.textContent = "Send Request";
|
||||
});
|
||||
}
|
||||
|
||||
document.querySelectorAll(".send-btn").forEach(btn => {
|
||||
btn.addEventListener("click", function () {
|
||||
sendRequest(this);
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById("add-request").addEventListener("click", function () {
|
||||
const container = document.getElementById("request-container");
|
||||
const requestBox = container.firstElementChild.cloneNode(true);
|
||||
|
||||
requestBox.querySelector(".target-link-input").value = "federator/fedusers/grumpydevelop@contentnation.net/outbox?page=0";
|
||||
requestBox.querySelector(".request-type-input").value = "GET";
|
||||
requestBox.querySelector(".session-input").value = "";
|
||||
requestBox.querySelector(".profile-input").value = "";
|
||||
requestBox.querySelector(".response").textContent = "Waiting for response";
|
||||
|
||||
requestBox.querySelector(".send-btn").addEventListener("click", function () {
|
||||
sendRequest(this);
|
||||
});
|
||||
|
||||
container.appendChild(requestBox);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -1,3 +0,0 @@
|
|||
<?php
|
||||
|
||||
echo phpinfo();
|
|
@ -1,6 +0,0 @@
|
|||
[mastodon]
|
||||
service-uri = http://mastodon.local
|
||||
|
||||
[userdata]
|
||||
path = '/home/net/contentnation/userdata/htdocs/' // need to download local copy of image and put img-path here
|
||||
url = 'https://files.mastodon.net'
|
|
@ -6,7 +6,7 @@
|
|||
* @author Sascha Nitsch (grumpydeveloper)
|
||||
**/
|
||||
|
||||
namespace Federator;
|
||||
namespace Federator;
|
||||
|
||||
/**
|
||||
* main API class
|
||||
|
@ -46,7 +46,7 @@ class Api extends Main
|
|||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->contentType = "application/activity+json";
|
||||
$this->contentType = "application/json";
|
||||
Main::__construct();
|
||||
}
|
||||
|
||||
|
@ -69,12 +69,11 @@ class Api extends Main
|
|||
/**
|
||||
* main API function
|
||||
*/
|
||||
public function run(): void
|
||||
public function run() : void
|
||||
{
|
||||
$this->setPath((string) $_REQUEST['_call']);
|
||||
$this->setPath((string)$_REQUEST['_call']);
|
||||
$this->openDatabase();
|
||||
$this->loadPlugins();
|
||||
|
||||
$retval = "";
|
||||
$handler = null;
|
||||
if ($this->connector === null) {
|
||||
|
@ -169,7 +168,7 @@ class Api extends Main
|
|||
* @param string $message optional message
|
||||
* @throws Exceptions\PermissionDenied
|
||||
*/
|
||||
public function checkPermission($permission, $exception = "\Exceptions\PermissionDenied", $message = null): void
|
||||
public function checkPermission($permission, $exception = "\Exceptions\PermissionDenied", $message = null) : void
|
||||
{
|
||||
// generic check first
|
||||
if ($this->user === false) {
|
||||
|
@ -192,78 +191,6 @@ class Api extends Main
|
|||
throw new $exception($message);
|
||||
}
|
||||
|
||||
/**
|
||||
* check if the headers include a valid signature
|
||||
*
|
||||
* @param string[] $headers
|
||||
* permission(s) to check for
|
||||
* @throws Exceptions\PermissionDenied
|
||||
*/
|
||||
public function checkSignature($headers)
|
||||
{
|
||||
$signatureHeader = $headers['Signature'] ?? null;
|
||||
|
||||
if (!$signatureHeader) {
|
||||
http_response_code(400);
|
||||
throw new Exceptions\PermissionDenied("Missing Signature header");
|
||||
}
|
||||
|
||||
// Parse Signature header
|
||||
preg_match_all('/(\w+)=["\']?([^"\',]+)["\']?/', $signatureHeader, $matches);
|
||||
$signatureParts = array_combine($matches[1], $matches[2]);
|
||||
|
||||
$signature = base64_decode($signatureParts['signature']);
|
||||
$keyId = $signatureParts['keyId'];
|
||||
$signedHeaders = explode(' ', $signatureParts['headers']);
|
||||
|
||||
// Fetch public key from `keyId` (usually actor URL + #main-key)
|
||||
[$publicKeyData, $info] = \Federator\Main::getFromRemote($keyId, ['Accept: application/activity+json']);
|
||||
|
||||
if ($info['http_code'] !== 200) {
|
||||
http_response_code(500);
|
||||
throw new Exceptions\PermissionDenied("Failed to fetch public key from keyId: $keyId");
|
||||
}
|
||||
|
||||
$actor = json_decode($publicKeyData, true);
|
||||
$publicKeyPem = $actor['publicKey']['publicKeyPem'] ?? null;
|
||||
|
||||
if (!$publicKeyPem) {
|
||||
http_response_code(500);
|
||||
throw new Exceptions\PermissionDenied("Invalid public key format from actor with keyId: $keyId");
|
||||
}
|
||||
|
||||
// Reconstruct the signed string
|
||||
$signedString = '';
|
||||
foreach ($signedHeaders as $header) {
|
||||
$headerValue = '';
|
||||
|
||||
if ($header === '(request-target)') {
|
||||
$method = strtolower($_SERVER['REQUEST_METHOD']);
|
||||
$path = $_SERVER['REQUEST_URI'];
|
||||
$headerValue = "$method $path";
|
||||
} else {
|
||||
$headerValue = $headers[ucwords($header, '-')] ?? '';
|
||||
}
|
||||
|
||||
$signedString .= strtolower($header) . ": " . $headerValue . "\n";
|
||||
}
|
||||
$signedString = rtrim($signedString);
|
||||
|
||||
// Verify the signature
|
||||
$pubkeyRes = openssl_pkey_get_public($publicKeyPem);
|
||||
$verified = false;
|
||||
if ($pubkeyRes) {
|
||||
$verified = openssl_verify($signedString, $signature, $pubkeyRes, OPENSSL_ALGO_SHA256);
|
||||
}
|
||||
if ($verified !== 1) {
|
||||
http_response_code(500);
|
||||
throw new Exceptions\PermissionDenied("Signature verification failed for publicKey with keyId: $keyId");
|
||||
}
|
||||
|
||||
// Signature is valid!
|
||||
return "Signature verified from actor: " . $actor['id'];
|
||||
}
|
||||
|
||||
/**
|
||||
* remove unwanted elements from html input
|
||||
*
|
||||
|
@ -271,7 +198,7 @@ class Api extends Main
|
|||
* input to strip
|
||||
* @return string stripped input
|
||||
*/
|
||||
public static function stripHTML(string $_input): string
|
||||
public static function stripHTML(string $_input) : string
|
||||
{
|
||||
$out = preg_replace('/<(script[^>]*)>/i', '<${1}>', $_input);
|
||||
$out = preg_replace('/<\/(script)>/i', '</${1};>', $out);
|
||||
|
@ -285,7 +212,7 @@ class Api extends Main
|
|||
* parameter to check
|
||||
* @return bool true if in
|
||||
*/
|
||||
public static function hasPost(string $_key): bool
|
||||
public static function hasPost(string $_key) : bool
|
||||
{
|
||||
return array_key_exists($_key, $_POST);
|
||||
}
|
||||
|
@ -301,13 +228,13 @@ class Api extends Main
|
|||
*/
|
||||
public function escapePost(string $key, $int = false)
|
||||
{
|
||||
if (!array_key_exists($key, $_POST)) {
|
||||
if (! array_key_exists($key, $_POST)) {
|
||||
return $int ? 0 : "";
|
||||
}
|
||||
if ($int === true) {
|
||||
return intval($_POST[$key]);
|
||||
}
|
||||
$ret = $this->dbh->escape_string($this->stripHTML((string) $_POST[$key]));
|
||||
$ret = $this->dbh->escape_string($this->stripHTML((string)$_POST[$key]));
|
||||
return $ret;
|
||||
}
|
||||
|
||||
|
|
|
@ -52,20 +52,12 @@ class FedUsers implements APIInterface
|
|||
switch (sizeof($paths)) {
|
||||
case 2:
|
||||
if ($method === 'GET') {
|
||||
// /fedusers/username or /@username
|
||||
// /users/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:
|
||||
// /fedusers/username/(inbox|outbox|following|followers)
|
||||
// /users/username/(inbox|outbox|following|followers)
|
||||
switch ($paths[2]) {
|
||||
case 'following':
|
||||
// $handler = new FedUsers\Following();
|
||||
|
@ -74,39 +66,15 @@ class FedUsers implements APIInterface
|
|||
// $handler = new FedUsers\Followers();
|
||||
break;
|
||||
case '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`;
|
||||
}
|
||||
}
|
||||
// $handler = new FedUsers\Inbox();
|
||||
break;
|
||||
case 'outbox':
|
||||
$handler = new FedUsers\Outbox($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;
|
||||
}
|
||||
break;
|
||||
case 4:
|
||||
// /fedusers/username/collections/(features|tags)
|
||||
// /users/username/collections/(features|tags)
|
||||
// not yet implemented
|
||||
break;
|
||||
}
|
||||
|
@ -114,10 +82,10 @@ class FedUsers implements APIInterface
|
|||
$ret = false;
|
||||
switch ($method) {
|
||||
case 'GET':
|
||||
$ret = $handler->get($user);
|
||||
$ret = $handler->get($paths[1]);
|
||||
break;
|
||||
case 'POST':
|
||||
$ret = $handler->post($user);
|
||||
$ret = $handler->post($paths[1]);
|
||||
break;
|
||||
}
|
||||
if ($ret !== false) {
|
||||
|
@ -155,7 +123,7 @@ class FedUsers implements APIInterface
|
|||
'fqdn' => $_SERVER['SERVER_NAME'],
|
||||
'name' => $user->name,
|
||||
'username' => $user->id,
|
||||
'publickey' => $user->publicKey,
|
||||
'publickey' => str_replace("\n", "\\n", $user->publicKey),
|
||||
'registered' => gmdate('Y-m-d\TH:i:s\Z', $user->registered), // 2021-03-25T00:00:00Z
|
||||
'summary' => $user->summary,
|
||||
'type' => $user->type
|
||||
|
|
|
@ -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\Api\FedUsers;
|
||||
|
||||
/**
|
||||
* handle activitypub outbox requests
|
||||
*/
|
||||
class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
|
||||
{
|
||||
/**
|
||||
* main instance
|
||||
*
|
||||
* @var \Federator\Main $main
|
||||
*/
|
||||
private $main;
|
||||
|
||||
/**
|
||||
* constructor
|
||||
* @param \Federator\Api $main api main instance
|
||||
*/
|
||||
public function __construct($main)
|
||||
{
|
||||
$this->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');
|
||||
|
||||
$allHeaders = getallheaders();
|
||||
try {
|
||||
$this->main->checkSignature($allHeaders);
|
||||
} catch (\Federator\Exceptions\PermissionDenied $e) {
|
||||
error_log("Inbox::post Signature check failed: " . $e->getMessage());
|
||||
http_response_code(401);
|
||||
exit("Access denied");
|
||||
}
|
||||
|
||||
$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);
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
$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'])
|
||||
->setSummary($objData['summary'])
|
||||
->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) {
|
||||
error_log("Inbox::postForUser couldn't find user: $_user");
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -40,7 +40,6 @@ class Outbox implements \Federator\Api\FedUsers\FedUsersInterface
|
|||
$dbh = $this->main->getDatabase();
|
||||
$cache = $this->main->getCache();
|
||||
$connector = $this->main->getConnector();
|
||||
|
||||
// get user
|
||||
$user = \Federator\DIO\User::getUserByName(
|
||||
$dbh,
|
||||
|
@ -51,7 +50,6 @@ class Outbox implements \Federator\Api\FedUsers\FedUsersInterface
|
|||
if ($user->id === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// get posts from user
|
||||
$outbox = new \Federator\Data\ActivityPub\Common\Outbox();
|
||||
$min = $this->main->extractFromURI("min", "");
|
||||
|
@ -64,7 +62,7 @@ class Outbox implements \Federator\Api\FedUsers\FedUsersInterface
|
|||
$items = [];
|
||||
}
|
||||
$host = $_SERVER['SERVER_NAME'];
|
||||
$id = 'https://' . $host . '/' . $_user . '/outbox';
|
||||
$id = 'https://' . $host .'/' . $_user . '/outbox';
|
||||
$outbox->setPartOf($id);
|
||||
$outbox->setID($id);
|
||||
if ($page !== '') {
|
||||
|
@ -74,371 +72,24 @@ class Outbox implements \Federator\Api\FedUsers\FedUsersInterface
|
|||
$outbox->setFirst($id);
|
||||
$outbox->setLast($id . '&min=0');
|
||||
}
|
||||
if (sizeof($items) > 0) {
|
||||
if (sizeof($items)>0) {
|
||||
$newestId = $items[0]->getPublished();
|
||||
$oldestId = $items[sizeof($items) - 1]->getPublished();
|
||||
$oldestId = $items[sizeof($items)-1]->getPublished();
|
||||
$outbox->setNext($id . '&max=' . $newestId);
|
||||
$outbox->setPrev($id . '&min=' . $oldestId);
|
||||
}
|
||||
$obj = $outbox->toObject();
|
||||
return json_encode($obj, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
|
||||
return json_encode($obj);
|
||||
}
|
||||
|
||||
/**
|
||||
* handle post call
|
||||
*
|
||||
* @param string $_user user to add data to outbox
|
||||
* @param string $_user user to add data to outbox @unused-param
|
||||
* @return string|false response
|
||||
*/
|
||||
public function post($_user)
|
||||
{
|
||||
$outboxActivity = null;
|
||||
$_rawInput = file_get_contents('php://input');
|
||||
|
||||
$allHeaders = getallheaders();
|
||||
try {
|
||||
$this->main->checkSignature($allHeaders);
|
||||
} catch (\Federator\Exceptions\PermissionDenied $e) {
|
||||
error_log("Outbox::post Signature check failed: " . $e->getMessage());
|
||||
http_response_code(401);
|
||||
exit("Access denied");
|
||||
}
|
||||
|
||||
$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(published: 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);
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
$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'])
|
||||
->setSummary($objData['summary'])
|
||||
->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;
|
||||
}
|
||||
|
||||
$outboxActivity = $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;
|
||||
}
|
||||
}
|
||||
|
||||
$outboxActivity = $undo;
|
||||
break;
|
||||
default:
|
||||
error_log("Outbox::post we currently don't support the activity type " . $activity['type'] . "\n");
|
||||
break;
|
||||
}
|
||||
|
||||
$sendTo = $outboxActivity->getCC();
|
||||
if ($outboxActivity->getType() === 'Undo') {
|
||||
$sendTo = $outboxActivity->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, $outboxActivity);
|
||||
}
|
||||
return json_encode($outboxActivity, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
|
||||
}
|
||||
|
||||
/**
|
||||
* handle post call for specific user
|
||||
*
|
||||
* @param string $_user user to add data to outbox
|
||||
* @param \Federator\Data\ActivityPub\Common\Activity $outboxActivity the activity that we received
|
||||
* @return string|false response
|
||||
*/
|
||||
private function postForUser($_user, $outboxActivity)
|
||||
{
|
||||
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) {
|
||||
error_log("Outbox::postForUser couldn't find user: $_user");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$rootDir = $_SERVER['DOCUMENT_ROOT'] . '../';
|
||||
// Save the raw input and parsed JSON to a file for inspection
|
||||
file_put_contents(
|
||||
$rootDir . 'logs/outbox_' . $_user . '.log',
|
||||
date('Y-m-d H:i:s') . ": ==== POST " . $_user . " Outbox Activity ====\n" . json_encode($outboxActivity, 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("Outbox::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("Outbox::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("Outbox::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("Outbox::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;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ class Dummy implements \Federator\Api\APIInterface
|
|||
/**
|
||||
* internal message to output
|
||||
*
|
||||
* @var string $response
|
||||
* @var Array<string, mixed> $message
|
||||
*/
|
||||
private $message = [];
|
||||
|
||||
|
@ -44,100 +44,29 @@ 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:
|
||||
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;
|
||||
if ($paths[2] === 'moo') {
|
||||
return $this->getDummy();
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'POST':
|
||||
switch (sizeof($paths)) {
|
||||
case 3:
|
||||
switch ($paths[2]) {
|
||||
case 'moo':
|
||||
return $this->postDummy();
|
||||
case 'sharedInbox':
|
||||
return $this->postSharedInbox();
|
||||
default:
|
||||
break;
|
||||
if ($paths[2] === 'moo') {
|
||||
return $this->postDummy();
|
||||
}
|
||||
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);
|
||||
|
@ -151,190 +80,16 @@ class Dummy implements \Federator\Api\APIInterface
|
|||
*/
|
||||
public function getDummy()
|
||||
{
|
||||
$this->message = json_encode([
|
||||
$this->message = [
|
||||
'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 = <<<PEM
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1MDmIPDcTey9lNYicfho
|
||||
u3EeVKeQkm1FkFl4Yoj1FW0SFyGkgtPr8hgAL1JIqyrFokgbPRtihmhTUHaQNoV8
|
||||
Uj5UIKG6zM1y1dHWizqwQw13pSMWri3IcSf08GSiolYBb19A98EMzIyGZHzjlfw8
|
||||
VAhW6qL6ML5YAR2YvckRRpS4pPVseQLHfDzkWlyXePJQInMai0kdrH39XiXw8B0C
|
||||
ver+7I1Z3rzJu+iOLmlblekFJtWDiipjuMedzluL3mNwV9Lk1ka1m7vHrtyqjtv1
|
||||
X5FLRXVzpFziJsIpWZ6ojU9KRX8l4yvv9FL4dZIn7edbcosvnNDpnvEl+NsGnf4R
|
||||
1wIDAQAB
|
||||
-----END PUBLIC KEY-----
|
||||
PEM;
|
||||
$publicKeyPemJsonSafe = json_encode($publicKeyPem); // gives string with \n inside
|
||||
$data = [
|
||||
'iconMediaType' => $user->iconMediaType,
|
||||
'iconURL' => $user->iconURL,
|
||||
'imageMediaType' => $user->imageMediaType,
|
||||
'imageURL' => $user->imageURL,
|
||||
'fqdn' => '192.168.178.143',
|
||||
'name' => $user->name,
|
||||
'username' => $user->id,
|
||||
'publickey' => "<placeholderPublicKey>",
|
||||
'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(
|
||||
'"<placeholderPublicKey>"',
|
||||
$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"
|
||||
*
|
||||
|
@ -345,216 +100,6 @@ $publicKeyPemJsonSafe = json_encode($publicKeyPem); // gives string with \n insi
|
|||
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
|
||||
*
|
||||
|
@ -562,6 +107,6 @@ $publicKeyPemJsonSafe = json_encode($publicKeyPem); // gives string with \n insi
|
|||
*/
|
||||
public function toJson()
|
||||
{
|
||||
return $this->message;
|
||||
return json_encode($this->message, JSON_PRETTY_PRINT) . "\n";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -51,7 +51,6 @@ class WebFinger
|
|||
if (preg_match("/^acct:([^@]+)@(.*)$/", $_resource, $matches) != 1 || $matches[2] !== $domain) {
|
||||
throw new \Federator\Exceptions\InvalidArgument();
|
||||
}
|
||||
$domain = $matches[2];
|
||||
$user = \Federator\DIO\User::getUserByName(
|
||||
$this->main->getDatabase(),
|
||||
$matches[1],
|
||||
|
@ -59,7 +58,6 @@ class WebFinger
|
|||
$this->main->getCache()
|
||||
);
|
||||
if ($user->id == 0) {
|
||||
echo "not found";
|
||||
throw new \Federator\Exceptions\FileNotFound();
|
||||
}
|
||||
$data = [
|
||||
|
|
|
@ -1,43 +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\Data\ActivityPub\Common;
|
||||
|
||||
class Announce extends Activity
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('Announce');
|
||||
parent::addContext('https://www.w3.org/ns/activitystreams');
|
||||
}
|
||||
|
||||
/**
|
||||
* convert internal state to php array
|
||||
*
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
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<string,mixed> $json input json
|
||||
* @return bool true on success
|
||||
*/
|
||||
public function fromJson($json)
|
||||
{
|
||||
return parent::fromJson($json);
|
||||
}
|
||||
}
|
|
@ -1,43 +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\Data\ActivityPub\Common;
|
||||
|
||||
class Undo extends Activity
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('Undo');
|
||||
parent::addContext('https://www.w3.org/ns/activitystreams');
|
||||
}
|
||||
|
||||
/**
|
||||
* convert internal state to php array
|
||||
*
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
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<string,mixed> $json input json
|
||||
* @return bool true on success
|
||||
*/
|
||||
public function fromJson($json)
|
||||
{
|
||||
return parent::fromJson($json);
|
||||
}
|
||||
}
|
|
@ -773,7 +773,7 @@ class APObject implements \JsonSerializable
|
|||
* {@inheritDoc}
|
||||
* @see JsonSerializable::jsonSerialize()
|
||||
*/
|
||||
public function jsonSerialize(): mixed
|
||||
public function jsonSerialize()
|
||||
{
|
||||
return $this->toObject();
|
||||
}
|
||||
|
|
|
@ -17,7 +17,8 @@ class Stats
|
|||
/**
|
||||
* get remote stats
|
||||
*
|
||||
* @param \Federator\Main $main main instance
|
||||
* @param \Federator\Main $main
|
||||
* main instance
|
||||
* @return \Federator\Data\Stats
|
||||
*/
|
||||
public static function getStats($main)
|
||||
|
|
|
@ -145,7 +145,6 @@ class User
|
|||
public static function getUserByName($dbh, $_name, $connector, $cache)
|
||||
{
|
||||
$user = false;
|
||||
|
||||
// ask cache
|
||||
if ($cache !== null) {
|
||||
$user = $cache->getRemoteUserByName($_name);
|
||||
|
@ -180,7 +179,6 @@ class User
|
|||
$stmt->fetch();
|
||||
}
|
||||
$stmt->close();
|
||||
|
||||
if ($user->id === null) {
|
||||
// ask connector for user-id
|
||||
$ruser = $connector->getRemoteUserByName($_name);
|
||||
|
|
|
@ -33,12 +33,6 @@ class Main
|
|||
* @var Connector\Connector $connector
|
||||
*/
|
||||
protected $connector = null;
|
||||
/**
|
||||
* remote host (f.e. https://contentnation.net)
|
||||
*
|
||||
* @var string $host
|
||||
*/
|
||||
protected $host = null;
|
||||
/**
|
||||
* response content type
|
||||
*
|
||||
|
@ -99,7 +93,7 @@ class Main
|
|||
public static function extractFromURI($param, $fallback = '')
|
||||
{
|
||||
$uri = $_SERVER['REQUEST_URI'];
|
||||
$params = substr($uri, (int) (strpos($uri, '?') + 1));
|
||||
$params = substr($uri, (int)(strpos($uri, '?') + 1));
|
||||
$params = explode('&', $params);
|
||||
foreach ($params as $p) {
|
||||
$tokens = explode('=', $p);
|
||||
|
@ -145,28 +139,20 @@ class Main
|
|||
{
|
||||
return $this->cache;
|
||||
}
|
||||
|
||||
/**
|
||||
* get connector
|
||||
*
|
||||
* @return \Federator\Connector\Connector
|
||||
*/
|
||||
* get connector
|
||||
*
|
||||
* @return \Federator\Connector\Connector
|
||||
*/
|
||||
public function getConnector()
|
||||
{
|
||||
return $this->connector;
|
||||
}
|
||||
/**
|
||||
* get host (f.e. https://contentnation.net)
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getHost()
|
||||
{
|
||||
return $this->host;
|
||||
}
|
||||
|
||||
/**
|
||||
* get config
|
||||
* @return array<string, mixed>
|
||||
* @return Array<String, Mixed>
|
||||
*/
|
||||
public function getConfig()
|
||||
{
|
||||
|
@ -186,7 +172,7 @@ class Main
|
|||
/**
|
||||
* load plugins
|
||||
*/
|
||||
public function loadPlugins(): void
|
||||
public function loadPlugins() : void
|
||||
{
|
||||
if (array_key_exists('plugins', $this->config)) {
|
||||
$basepath = $_SERVER['DOCUMENT_ROOT'] . '../plugins/federator/';
|
||||
|
@ -213,8 +199,8 @@ class Main
|
|||
$dbconf = $this->config["database"];
|
||||
$this->dbh = new \mysqli(
|
||||
$dbconf['host'],
|
||||
$usernameOverride ?? (string) $dbconf['username'],
|
||||
$passwordOverride ?? (string) $dbconf['password'],
|
||||
$usernameOverride ?? (string)$dbconf['username'],
|
||||
$passwordOverride ?? (string)$dbconf['password'],
|
||||
$dbconf['database']
|
||||
);
|
||||
if ($this->dbh->connect_error !== null) {
|
||||
|
@ -235,7 +221,7 @@ class Main
|
|||
$smarty = new \Smarty\Smarty();
|
||||
$root = $_SERVER['DOCUMENT_ROOT'];
|
||||
$smarty->setCompileDir($root . $this->config['templates']['compiledir']);
|
||||
$smarty->setTemplateDir((string) realpath($root . $this->config['templates']['path']));
|
||||
$smarty->setTemplateDir((string)realpath($root . $this->config['templates']['path']));
|
||||
$smarty->assign('database', $this->dbh);
|
||||
$smarty->assign('maininstance', $this);
|
||||
foreach ($data as $key => $value) {
|
||||
|
@ -246,47 +232,27 @@ class Main
|
|||
|
||||
/**
|
||||
* set cache
|
||||
*
|
||||
* @param \Federator\Cache\Cache $cache the new cache
|
||||
*/
|
||||
public function setCache(Cache\Cache $cache): void
|
||||
public function setCache(Cache\Cache $cache) : void
|
||||
{
|
||||
$this->cache = $cache;
|
||||
}
|
||||
|
||||
/**
|
||||
* set connector
|
||||
*
|
||||
* @param \Federator\Connector\Connector $connector the new connector
|
||||
*/
|
||||
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
|
||||
}
|
||||
$this->connector = $connector;
|
||||
}
|
||||
|
||||
/**
|
||||
* set host
|
||||
*
|
||||
* @param string $host the new host url
|
||||
*/
|
||||
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
|
||||
}
|
||||
$this->host = $host;
|
||||
}
|
||||
|
||||
/**
|
||||
* set response code
|
||||
*
|
||||
* @param int $code
|
||||
* new response code
|
||||
*/
|
||||
public function setResponseCode(int $code): void
|
||||
public function setResponseCode(int $code) : void
|
||||
{
|
||||
$this->responseCode = $code;
|
||||
}
|
||||
|
@ -304,7 +270,7 @@ class Main
|
|||
* optional parameters
|
||||
* @return string translation
|
||||
*/
|
||||
public static function translate(?string $lang, string $group, string $key, array $parameters = array()): string
|
||||
public static function translate(?string $lang, string $group, string $key, array $parameters = array()) : string
|
||||
{
|
||||
$l = new Language($lang);
|
||||
return $l->printlang($group, $key, $parameters);
|
||||
|
@ -315,7 +281,7 @@ class Main
|
|||
*
|
||||
* @param ?string $lang
|
||||
*/
|
||||
public static function validLanguage(?string $lang): bool
|
||||
public static function validLanguage(?string $lang) : bool
|
||||
{
|
||||
$language = new Language($lang);
|
||||
if ($language->getLang() === $lang) {
|
||||
|
|
|
@ -6,11 +6,11 @@
|
|||
* @author Sascha Nitsch (grumpydeveloper)
|
||||
**/
|
||||
|
||||
namespace Federator\Connector;
|
||||
namespace Federator\Connector;
|
||||
|
||||
/**
|
||||
* Connector to ContentNation.net
|
||||
*/
|
||||
/**
|
||||
* Connector to ContentNation.net
|
||||
*/
|
||||
class ContentNation implements Connector
|
||||
{
|
||||
/**
|
||||
|
@ -47,7 +47,6 @@ class ContentNation implements Connector
|
|||
}
|
||||
$this->service = $config['contentnation']['service-uri'];
|
||||
$this->main = $main;
|
||||
$this->main->setHost($this->service);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -60,10 +59,7 @@ class ContentNation implements Connector
|
|||
*/
|
||||
public function getRemotePostsByUser($userId, $min, $max)
|
||||
{
|
||||
if (preg_match("#^([^@]+)@([^/]+)#", $userId, $matches) === 1) {
|
||||
$userId = $matches[1];
|
||||
}
|
||||
$remoteURL = $this->service . '/api/profile/' . urlencode($userId) . '/activities';
|
||||
$remoteURL = $this->service . '/api/profile/' . $userId . '/activities';
|
||||
if ($min !== '') {
|
||||
$remoteURL .= '&minTS=' . urlencode($min);
|
||||
}
|
||||
|
@ -87,67 +83,63 @@ class ContentNation implements Connector
|
|||
$userdata = $this->config['userdata']['url'];
|
||||
foreach ($activities as $activity) {
|
||||
$create = new \Federator\Data\ActivityPub\Common\Create();
|
||||
$create->setAActor('https://' . $host . '/' . $userId);
|
||||
$create->setAActor('https://' . $host .'/' . $userId);
|
||||
$create->setID($activity['id'])
|
||||
->setPublished($activity['timestamp'])
|
||||
->addTo("https://www.w3.org/ns/activitystreams#Public")
|
||||
->addCC('https://' . $host . '/' . $userId . '/followers.json');
|
||||
switch ($activity['type']) {
|
||||
case 'Article':
|
||||
$create->setURL('https://' . $host . '/' . $activity['language'] . '/' . $userId . '/'
|
||||
. $activity['name']);
|
||||
$create->setURL('https://'.$host . '/' . $activity['language'] . '/' . $userId . '/'
|
||||
. $activity['name']);
|
||||
$apArticle = new \Federator\Data\ActivityPub\Common\Article();
|
||||
if (array_key_exists('tags', $activity)) {
|
||||
foreach ($activity['tags'] as $tag) {
|
||||
$href = 'https://' . $host . '/' . $activity['language']
|
||||
. '/search.htm?tagsearch=' . urlencode($tag);
|
||||
. '/search.htm?tagsearch=' . urlencode($tag);
|
||||
$tagObj = new \Federator\Data\ActivityPub\Common\Tag();
|
||||
$tagObj->setHref($href)
|
||||
->setName('#' . urlencode(str_replace(' ', '', $tag)))
|
||||
->setType('Hashtag');
|
||||
->setName('#' . urlencode(str_replace(' ', '', $tag)))
|
||||
->setType('Hashtag');
|
||||
$apArticle->addTag($tagObj);
|
||||
}
|
||||
}
|
||||
$apArticle->setPublished($activity['published'])
|
||||
->setName($activity['title'])
|
||||
->setAttributedTo('https://' . $host . '/' . $activity['profilename'])
|
||||
->setContent(
|
||||
$activity['teaser'] ??
|
||||
$this->main->translate(
|
||||
$activity['language'],
|
||||
'article',
|
||||
'newarticle'
|
||||
)
|
||||
)
|
||||
->addTo("https://www.w3.org/ns/activitystreams#Public")
|
||||
->addCC('https://' . $host . '/' . $userId . '/followers.json');
|
||||
->setName($activity['title'])
|
||||
->setAttributedTo('https://' . $host .'/' . $activity['profilename'])
|
||||
->setContent(
|
||||
$activity['teaser'] ??
|
||||
$this->main->translate(
|
||||
$activity['language'],
|
||||
'article',
|
||||
'newarticle'
|
||||
)
|
||||
)
|
||||
->addTo("https://www.w3.org/ns/activitystreams#Public")
|
||||
->addCC('https://' . $host . '/' . $userId . '/followers.json');
|
||||
$articleimage = $activity['imagealt'] ??
|
||||
$this->main->translate($activity['language'], 'article', 'image');
|
||||
$idurl = 'https://' . $host . '/' . $activity['language']
|
||||
. '/' . $userId . '/' . $activity['name'];
|
||||
. '/' . $userId . '/'. $activity['name'];
|
||||
$apArticle->setID($idurl)
|
||||
->setURL($idurl);
|
||||
->setURL($idurl);
|
||||
$image = $activity['image'] ?? $activity['profileimg'];
|
||||
$path = $imgpath . $activity['profile'] . '/' . $image;
|
||||
$mediaType = (file_exists($path) && ($type = mime_content_type($path)) && !str_starts_with($type, 'text/'))
|
||||
? $type
|
||||
: 'image/jpeg';
|
||||
$mediaType = @mime_content_type($imgpath . $activity['profile'] . '/' . $image) | 'text/plain';
|
||||
$img = new \Federator\Data\ActivityPub\Common\Image();
|
||||
$img->setMediaType($mediaType)
|
||||
->setName($articleimage)
|
||||
->setURL($userdata . '/' . $activity['profile'] . $image);
|
||||
->setName($articleimage)
|
||||
->setURL($userdata . '/' . $activity['profile'] . $image);
|
||||
$apArticle->addImage($img);
|
||||
$create->setObject($apArticle);
|
||||
$posts[] = $create;
|
||||
break; // Article
|
||||
case 'Comment':
|
||||
$comment = new \Federator\Data\ActivityPub\Common\Activity('Comment');
|
||||
$create->setObject($comment);
|
||||
$posts[] = $create;
|
||||
// echo "comment\n";
|
||||
// print_r($activity);
|
||||
break; // Comment
|
||||
case 'Vote':
|
||||
$url = 'https://' . $host . '/' . $activity['articlelang'] . $userId . '/'
|
||||
. $activity['articlename'];
|
||||
$url = 'https://'.$host . '/' . $activity['articlelang'] . $userId . '/'
|
||||
. $activity['articlename'];
|
||||
$url .= '/vote/' . $activity['id'];
|
||||
$create->setURL($url);
|
||||
if ($activity['upvote'] === true) {
|
||||
|
@ -175,7 +167,7 @@ class ContentNation implements Connector
|
|||
$actor->setName($activity['username']);
|
||||
$like->setActor($actor);
|
||||
$url = 'https://' . $host . '/' . $activity['articlelang']
|
||||
. '/' . $userId . '/' . $activity['articlename'];
|
||||
. '/' . $userId . '/'. $activity['articlename'];
|
||||
if ($activity['comment'] !== '') {
|
||||
$url .= '/comment/' . $activity['comment'];
|
||||
}
|
||||
|
@ -220,7 +212,7 @@ class ContentNation implements Connector
|
|||
return $stats;
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* get remote user by given name
|
||||
*
|
||||
* @param string $_name user/profile name
|
||||
|
@ -228,9 +220,6 @@ 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) {
|
||||
return false;
|
||||
|
@ -282,7 +271,7 @@ class ContentNation implements Connector
|
|||
return false;
|
||||
}
|
||||
$r = json_decode($response, true);
|
||||
if ($r === false || !is_array($r) || !array_key_exists($_user, $r)) {
|
||||
if ($r === false || !is_array($r) || !array_key_exists($_user, $r)) {
|
||||
return false;
|
||||
}
|
||||
$user = $this->getRemoteUserByName($_user);
|
||||
|
@ -310,6 +299,5 @@ 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
|
||||
$main->setConnector($cn);
|
||||
}
|
||||
|
|
|
@ -70,7 +70,7 @@ class DummyConnector implements Connector
|
|||
// validate $_session and $user
|
||||
$user = new \Federator\Data\User();
|
||||
$user->externalid = $_user;
|
||||
$user->permissions = ['publish'];
|
||||
$user->permissions = ['PUBLISH'];
|
||||
$user->session = $_session;
|
||||
return $user;
|
||||
}
|
||||
|
@ -87,6 +87,5 @@ 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
|
||||
$main->setConnector($dummy);
|
||||
}
|
||||
|
|
|
@ -1,416 +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'];
|
||||
$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;
|
||||
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";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $posts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the MIME type of a remote file by its URL.
|
||||
*
|
||||
* @param string $_url The URL of the remote file.
|
||||
* @return string|false The MIME type if found, or false on failure.
|
||||
*/
|
||||
public function getRemoteMimeType($url)
|
||||
{
|
||||
$headers = get_headers($url, 1);
|
||||
return $headers['Content-Type'] ?? 'unknown';
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
// 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 = strtotime($r['created_at']);
|
||||
|
||||
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);
|
||||
}
|
Loading…
Add table
Reference in a new issue