Compare commits

..

8 commits

Author SHA1 Message Date
Sascha Nitsch
5ef5c97d5e bugfixes and refactoring, cleanup 2025-07-22 18:38:27 +02:00
Sascha Nitsch
f8fb409327 use right domain name in case of split-domain 2025-07-20 17:35:05 +02:00
Sascha Nitsch
1b92265402 fixed hiding of total in case of 0 items 2025-07-20 17:34:46 +02:00
Sascha Nitsch
9464b28e11 also react to other domain name in case of split domain setups 2025-07-20 17:34:18 +02:00
Sascha Nitsch
6df40c1946 function renaming 2025-07-20 17:34:04 +02:00
Sascha Nitsch
f23a2b01e0 update user independent of cache 2025-07-19 19:05:36 +02:00
Sascha Nitsch
1cfbff6d4c better support for split domains 2025-07-19 19:04:10 +02:00
Sascha Nitsch
fa11d7d397 partial merge from fork yannis_federator 2025-07-11 20:56:23 +02:00
45 changed files with 887 additions and 905 deletions

16
.gitattributes vendored
View file

@ -1,16 +0,0 @@
# Set the default behavior, in case people don't have core.autocrlf set.
* text=auto
# Explicitly declare text files you want to always be normalized and converted
# to native line endings on checkout.
*.c text
*.h text
*.php text
# Declare files that will always have LF line endings on checkout.
*.sln text eol=lf
*.php text eol.lf
# Denote all files that are truly binary and should not be modified.
*.png binary
*.jpg binary

3
.gitignore vendored
View file

@ -6,3 +6,6 @@ php-docs
phpdoc phpdoc
html html
/cache /cache
contentnation.ini
*.pem*
composer.phar

View file

@ -1,5 +1,7 @@
[generic] [generic]
externaldomain = 'contentnation.net' protocol = 'https'
externaldomain = 'federator.your.fqdn'
sourcedomain = 'your.fqdn'
[database] [database]
host = '127.0.0.1' host = '127.0.0.1'
@ -8,18 +10,17 @@ password = '*change*me*'
database = 'federator' database = 'federator'
[templates] [templates]
path = 'templates/federator/' path = '../templates/federator/'
compiledir = 'cache' compiledir = '../cache'
[plugins] [plugins]
rediscache = 'rediscache.php' rediscache = 'rediscache.php'
# dummy = 'dummyconnector.php' dummy = 'dummyconnector.php'
contentnation = 'contentnation.php'
[maintenance] [maintenance]
username = 'federatoradmin' username = 'federatoradmin'
password = '*change*me*as*well' password = '*change*me*as*well'
[keys] [keys]
federatorPrivateKeyPath = 'federator.key' federatorPrivateKeyPath = 'federator.pem'
federatorPublicKeyPath = 'federator.pub' federatorPublicKeyPath = 'federator.pem.pub'

View file

@ -1,10 +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'
[keys]
headerSenderName = 'contentnation'
publicKeyPath = 'contentnation.pub'

View file

@ -14,7 +14,6 @@ date_default_timezone_set("Europe/Berlin");
spl_autoload_register(static function (string $className) { spl_autoload_register(static function (string $className) {
include '../php/' . str_replace("\\", "/", strtolower($className)) . '.php'; include '../php/' . str_replace("\\", "/", strtolower($className)) . '.php';
}); });
define('PROJECT_ROOT', dirname(__DIR__, 1));
/// main instance /// main instance
$contentnation = new \Federator\Api(); $contentnation = new \Federator\Api();

View file

@ -1,113 +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="users/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 } : {}),
"HTTP_HOST": "localhost",
};
fetch("http://localhost/" + 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 = "users/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>

View file

@ -46,7 +46,7 @@ class Api extends Main
*/ */
public function __construct() public function __construct()
{ {
$this->contentType = "application/json"; $this->contentType = 'application/json';
parent::__construct(); parent::__construct();
} }
@ -63,7 +63,7 @@ class Api extends Main
while ($this->path[0] === '/') { while ($this->path[0] === '/') {
$this->path = substr($this->path, 1); $this->path = substr($this->path, 1);
} }
$this->paths = explode("/", $this->path); $this->paths = explode('/', $this->path);
} }
/** /**
@ -74,8 +74,7 @@ class Api extends Main
$this->setPath((string) $_REQUEST['_call']); $this->setPath((string) $_REQUEST['_call']);
$this->openDatabase(); $this->openDatabase();
$this->loadPlugins(); $this->loadPlugins();
$retval = '';
$retval = "";
$handler = null; $handler = null;
if ($this->connector === null) { if ($this->connector === null) {
http_response_code(500); http_response_code(500);
@ -101,7 +100,7 @@ class Api extends Main
break; break;
case 'fedusers': case 'fedusers':
$handler = new Api\FedUsers($this); $handler = new Api\FedUsers($this);
$this->setContentType("application/activity+json"); $this->setContentType('application/activity+json');
break; break;
case 'v1': case 'v1':
switch ($this->paths[1]) { switch ($this->paths[1]) {
@ -111,39 +110,6 @@ class Api extends Main
case 'newcontent': case 'newcontent':
$handler = new Api\V1\NewContent($this); $handler = new Api\V1\NewContent($this);
break; break;
/* case 'sendFollow': { // hacky implementation for testing purposes
$username = $this->paths[2];
$domain = $this->config['generic']['externaldomain'];
$response = \Federator\DIO\Followers::sendFollowRequest(
$this->dbh,
$this->connector,
$this->cache,
$username,
"admin@mastodon.local",
$domain
);
header("Content-type: " . $this->contentType);
header("Access-Control-Allow-Origin: *");
header("Cache-Control: no-cache, no-store, must-revalidate");
header("Pragma: no-cache");
header("Expires: 0");
if (is_string($response)) {
$this->setResponseCode(200);
$retval = json_encode(array(
"status" => "ok",
"message" => $response
));
} else {
$this->setResponseCode(500);
$retval = json_encode(array(
"status" => "error",
"message" => "Failed to send follow request"
));
}
http_response_code($this->responseCode);
echo $retval;
return;
} */
} }
break; break;
} }
@ -157,7 +123,7 @@ class Api extends Main
} catch (Exceptions\Exception $e) { } catch (Exceptions\Exception $e) {
$this->setResponseCode($e->getRetCode()); $this->setResponseCode($e->getRetCode());
$retval = json_encode(array( $retval = json_encode(array(
"error" => $e->getMessage() 'error' => $e->getMessage()
)); ));
} }
} else { } else {
@ -173,26 +139,26 @@ class Api extends Main
} }
if ($printresponse) { if ($printresponse) {
if ($this->redirect !== null) { if ($this->redirect !== null) {
header("Location: $this->redirect"); header('Location: ' . $this->redirect);
} }
if ($this->responseCode != 404) { if ($this->responseCode != 404) {
header("Content-type: " . $this->contentType); header('Content-type: ' . $this->contentType);
header("Access-Control-Allow-Origin: *"); header('Access-Control-Allow-Origin: *');
} }
if ($this->cacheTime == 0) { if ($this->cacheTime == 0) {
header("Cache-Control: no-cache, no-store, must-revalidate"); header('Cache-Control: no-cache, no-store, must-revalidate');
header("Pragma: no-cache"); header('Pragma: no-cache');
header("Expires: 0"); header('Expires: 0');
} else { } else {
$ts = gmdate("D, d M Y H:i:s", time() + $this->cacheTime) . " GMT"; $ts = gmdate('D, d M Y H:i:s', time() + $this->cacheTime) . ' GMT';
header("Expires: $ts"); header('Expires: ' . $ts);
header("Pragma: cache"); header('Pragma: cache');
header("Cache-Control: max-age=" . $this->cacheTime); header('Cache-Control: max-age=' . $this->cacheTime);
} }
echo $retval; echo $retval;
} else { } else {
if (!headers_sent()) { if (!headers_sent()) {
header("Content-type: " . $this->contentType); header('Content-type: ' . $this->contentType);
} }
} }
} }
@ -206,7 +172,7 @@ class Api extends Main
* @param string $message optional message * @param string $message optional message
* @throws Exceptions\PermissionDenied * @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 // generic check first
if ($this->user === false) { if ($this->user === false) {
@ -250,7 +216,7 @@ class Api extends Main
$signatureHeader = $headers['Signature'] ?? null; $signatureHeader = $headers['Signature'] ?? null;
if (!isset($signatureHeader)) { if (!isset($signatureHeader)) {
throw new Exceptions\PermissionDenied("Missing Signature header"); throw new Exceptions\PermissionDenied('Missing Signature header');
} }
// Parse Signature header // Parse Signature header
@ -260,33 +226,37 @@ class Api extends Main
$signature = base64_decode($signatureParts['signature']); $signature = base64_decode($signatureParts['signature']);
$signedHeaders = explode(' ', $signatureParts['headers']); $signedHeaders = explode(' ', $signatureParts['headers']);
$keyId = $signatureParts['keyId']; $keyId = $signatureParts['keyId'];
$publicKeyPem = false;
if ($this->cache !== null) {
$publicKeyPem = $this->cache->getPublicKey($keyId); $publicKeyPem = $this->cache->getPublicKey($keyId);
}
if (!isset($publicKeyPem) || $publicKeyPem === false) { if ($publicKeyPem === false) {
// Fetch public key from `keyId` (usually actor URL + #main-key) // Fetch public key from `keyId` (usually actor URL + #main-key)
[$publicKeyData, $info] = \Federator\Main::getFromRemote($keyId, ['Accept: application/activity+json']); [$publicKeyData, $info] = \Federator\Main::getFromRemote($keyId, ['Accept: application/activity+json']);
if ($info['http_code'] != 200) { if ($info['http_code'] != 200) {
throw new Exceptions\PermissionDenied("Failed to fetch public key from keyId: $keyId"); throw new Exceptions\PermissionDenied('Failed to fetch public key from keyId: ' . $keyId);
} }
$actor = json_decode($publicKeyData, true); $actor = json_decode($publicKeyData, true);
if (!is_array($actor) || !isset($actor['id'])) { if (!is_array($actor) || !isset($actor['id'])) {
throw new Exceptions\PermissionDenied("Invalid actor data"); throw new Exceptions\PermissionDenied('Invalid actor data');
} }
$publicKeyPem = $actor['publicKey']['publicKeyPem'] ?? null; $publicKeyPem = $actor['publicKey']['publicKeyPem'] ?? null;
if (!isset($publicKeyPem) || $publicKeyPem === false) { if (!isset($publicKeyPem) || $publicKeyPem === false) {
http_response_code(500); http_response_code(500);
throw new Exceptions\PermissionDenied("Public key couldn't be determined"); throw new Exceptions\PermissionDenied('Public key couldn\'t be determined');
} }
// Cache the public key for 1 hour // Cache the public key for 1 hour
if ($this->cache !== null) {
$this->cache->savePublicKey($keyId, $publicKeyPem); $this->cache->savePublicKey($keyId, $publicKeyPem);
} }
}
// Reconstruct the signed string // Reconstruct the signed string
$signedString = ''; $signedString = '';
@ -299,7 +269,7 @@ class Api extends Main
$headerValue = $headers[ucwords($header, '-')] ?? ''; $headerValue = $headers[ucwords($header, '-')] ?? '';
} }
$signedString .= strtolower($header) . ": " . $headerValue . "\n"; $signedString .= strtolower($header) . ': ' . $headerValue . "\n";
} }
$signedString = rtrim($signedString); $signedString = rtrim($signedString);
@ -312,11 +282,11 @@ class Api extends Main
} }
if ($verified != 1) { if ($verified != 1) {
http_response_code(500); http_response_code(500);
throw new Exceptions\PermissionDenied("Signature verification failed"); throw new Exceptions\PermissionDenied('Signature verification failed');
} }
// Signature is valid! // Signature is valid!
return "Signature verified."; return 'Signature verified.';
} }
/** /**

View file

@ -47,7 +47,7 @@ class FedUsers implements APIInterface
*/ */
public function exec($paths, $user) public function exec($paths, $user)
{ {
$method = $_SERVER["REQUEST_METHOD"]; $method = $_SERVER['REQUEST_METHOD'];
$handler = null; $handler = null;
$_username = $paths[1]; $_username = $paths[1];
switch (sizeof($paths)) { switch (sizeof($paths)) {
@ -58,7 +58,7 @@ class FedUsers implements APIInterface
} else { } else {
switch ($paths[1]) { switch ($paths[1]) {
case 'inbox': case 'inbox':
$_username = NULL; $_username = null;
$handler = new FedUsers\Inbox($this->main); $handler = new FedUsers\Inbox($this->main);
break; break;
default: default:
@ -127,6 +127,7 @@ class FedUsers implements APIInterface
} }
$config = $this->main->getConfig(); $config = $this->main->getConfig();
$domain = $config['generic']['externaldomain']; $domain = $config['generic']['externaldomain'];
$sourcedomain = $config['generic']['sourcedomain'];
$jsonKey = json_encode($user->publicKey); $jsonKey = json_encode($user->publicKey);
if (!is_string($jsonKey)) { if (!is_string($jsonKey)) {
throw new \Federator\Exceptions\FileNotFound(); throw new \Federator\Exceptions\FileNotFound();
@ -137,6 +138,7 @@ class FedUsers implements APIInterface
'imageMediaType' => $user->imageMediaType, 'imageMediaType' => $user->imageMediaType,
'imageURL' => $user->imageURL, 'imageURL' => $user->imageURL,
'fqdn' => $domain, 'fqdn' => $domain,
'sourcedomain' => $sourcedomain,
'name' => $user->name, 'name' => $user->name,
'username' => $user->id, 'username' => $user->id,
'publickey' => trim($jsonKey, '"'), 'publickey' => trim($jsonKey, '"'),
@ -147,6 +149,7 @@ class FedUsers implements APIInterface
$this->response = $this->main->renderTemplate('user.json', $data); $this->response = $this->main->renderTemplate('user.json', $data);
return true; return true;
} }
/** /**
* set response * set response
* *

View file

@ -59,16 +59,17 @@ class Followers implements \Federator\Api\FedUsers\FedUsersInterface
$followerItems = \Federator\DIO\Followers::getFollowersByUser($dbh, $user->id, $connector, $cache); $followerItems = \Federator\DIO\Followers::getFollowersByUser($dbh, $user->id, $connector, $cache);
$config = $this->main->getConfig(); $config = $this->main->getConfig();
$protocol = $config['generic']['protocol'];
$domain = $config['generic']['externaldomain']; $domain = $config['generic']['externaldomain'];
$baseUrl = 'https://' . $domain . '/' . $_user . '/followers'; $baseUrl = $protocol . '://' . $domain . '/' . $_user . '/followers';
$pageSize = 10; $pageSize = 10;
$page = $this->main->extractFromURI("page", ""); $page = $this->main->extractFromURI('page', '');
$id = $baseUrl; $id = $baseUrl;
$items = []; $items = [];
$totalItems = count($followerItems); $totalItems = count($followerItems);
if ($page !== "") { if ($page !== '') {
$pageNum = max(0, (int) $page); $pageNum = max(0, (int) $page);
$offset = (int)($pageNum * $pageSize); $offset = (int)($pageNum * $pageSize);
$pagedItems = array_slice($followerItems, $offset, $pageSize); $pagedItems = array_slice($followerItems, $offset, $pageSize);
@ -87,11 +88,11 @@ class Followers implements \Federator\Api\FedUsers\FedUsersInterface
// Pagination navigation // Pagination navigation
$lastPage = max(0, ceil($totalItems / $pageSize) - 1); $lastPage = max(0, ceil($totalItems / $pageSize) - 1);
if ($page === "" || $followers->getCount() == 0) { if ($page === '' || $followers->getCount() == 0) {
$followers->setFirst($baseUrl . '?page=0'); $followers->setFirst($baseUrl . '?page=0');
$followers->setLast($baseUrl . '?page=' . $lastPage); $followers->setLast($baseUrl . '?page=' . $lastPage);
} }
if ($page !== "") { if ($page !== '') {
$pageNum = max(0, (int) $page); $pageNum = max(0, (int) $page);
if ($pageNum < $lastPage) { if ($pageNum < $lastPage) {
$followers->setNext($baseUrl . '?page=' . ($pageNum + 1)); $followers->setNext($baseUrl . '?page=' . ($pageNum + 1));

View file

@ -43,7 +43,6 @@ class Following implements \Federator\Api\FedUsers\FedUsersInterface
$dbh = $this->main->getDatabase(); $dbh = $this->main->getDatabase();
$cache = $this->main->getCache(); $cache = $this->main->getCache();
$connector = $this->main->getConnector(); $connector = $this->main->getConnector();
// get user // get user
$user = \Federator\DIO\User::getUserByName( $user = \Federator\DIO\User::getUserByName(
$dbh, $dbh,
@ -56,19 +55,20 @@ class Following implements \Federator\Api\FedUsers\FedUsersInterface
} }
$following = new \Federator\Data\ActivityPub\Common\Following(); $following = new \Federator\Data\ActivityPub\Common\Following();
$followingItems = \Federator\DIO\Followers::getFollowingForUser($dbh, $user->id, $connector, $cache); $followingItems = \Federator\DIO\Followers::getFollowingByUser($dbh, $user->id, $connector, $cache);
$config = $this->main->getConfig(); $config = $this->main->getConfig();
$protocol = $config['generic']['protocol'];
$domain = $config['generic']['externaldomain']; $domain = $config['generic']['externaldomain'];
$baseUrl = 'https://' . $domain . '/users/' . $_user . '/following'; $baseUrl = $protocol . '://' . $domain . '/users/' . $_user . '/following';
$pageSize = 10; $pageSize = 10;
$page = $this->main->extractFromURI("page", ""); $page = $this->main->extractFromURI('page', '');
$id = $baseUrl; $id = $baseUrl;
$items = []; $items = [];
$totalItems = count($followingItems); $totalItems = count($followingItems);
if ($page !== "") { if ($page !== '') {
$pageNum = max(0, (int) $page); $pageNum = max(0, (int) $page);
$offset = (int) ($pageNum * $pageSize); $offset = (int) ($pageNum * $pageSize);
$pagedItems = array_slice($followingItems, $offset, $pageSize); $pagedItems = array_slice($followingItems, $offset, $pageSize);
@ -87,11 +87,11 @@ class Following implements \Federator\Api\FedUsers\FedUsersInterface
// Pagination navigation // Pagination navigation
$lastPage = max(0, ceil($totalItems / $pageSize) - 1); $lastPage = max(0, ceil($totalItems / $pageSize) - 1);
if ($page === "" || $following->getCount() == 0) { if ($page === '' || $following->getCount() == 0) {
$following->setFirst($baseUrl . '?page=0'); $following->setFirst($baseUrl . '?page=0');
$following->setLast($baseUrl . '?page=' . $lastPage); $following->setLast($baseUrl . '?page=' . $lastPage);
} }
if ($page !== "") { if ($page !== '') {
$pageNum = max(0, (int) $page); $pageNum = max(0, (int) $page);
if ($pageNum < $lastPage) { if ($pageNum < $lastPage) {
$following->setNext($baseUrl . '?page=' . ($pageNum + 1)); $following->setNext($baseUrl . '?page=' . ($pageNum + 1));

View file

@ -54,7 +54,8 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
try { try {
$this->main->checkSignature($allHeaders); $this->main->checkSignature($allHeaders);
} catch (\Federator\Exceptions\PermissionDenied $e) { } catch (\Federator\Exceptions\PermissionDenied $e) {
throw new \Federator\Exceptions\Unauthorized("Inbox::post Signature check failed: " . $e->getMessage()); error_log("signature check failed");
throw new \Federator\Exceptions\Unauthorized('Inbox::post Signature check failed: ' . $e->getMessage());
} }
$activity = is_string($_rawInput) ? json_decode($_rawInput, true) : null; $activity = is_string($_rawInput) ? json_decode($_rawInput, true) : null;
@ -64,19 +65,18 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
$connector = $this->main->getConnector(); $connector = $this->main->getConnector();
$config = $this->main->getConfig(); $config = $this->main->getConfig();
if (!is_array($activity)) { if (!is_array($activity)) {
throw new \Federator\Exceptions\ServerError("Inbox::post Input wasn't of type array"); throw new \Federator\Exceptions\ServerError('Inbox::post Input wasn\'t of type array');
} }
$inboxActivity = \Federator\Data\ActivityPub\Factory::newActivityFromJson($activity); $inboxActivity = \Federator\Data\ActivityPub\Factory::newActivityFromJson($activity);
if ($inboxActivity === false) { if ($inboxActivity === false) {
throw new \Federator\Exceptions\ServerError("Inbox::post couldn't create inboxActivity"); throw new \Federator\Exceptions\ServerError('Inbox::post couldn\'t create inboxActivity');
} }
$user = $inboxActivity->getAActor(); // url of the sender https://contentnation.net/username $actor = $inboxActivity->getAActor(); // url of the sender https://contentnation.net/username
$username = basename((string) (parse_url($user, PHP_URL_PATH) ?? '')); $username = basename((string) (parse_url($actor, PHP_URL_PATH) ?? ''));
$domain = parse_url($user, PHP_URL_HOST); $domain = parse_url($actor, PHP_URL_HOST);
$userId = $username . '@' . $domain; $userId = $username . '@' . $domain;
$user = \Federator\DIO\FedUser::getUserByName( $user = \Federator\DIO\FedUser::getUserByName(
$dbh, $dbh,
@ -84,8 +84,8 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
$cache $cache
); );
if ($user === null || $user->id === null) { if ($user === null || $user->id === null) {
error_log("Inbox::post couldn't find user: $userId"); error_log('Inbox::post couldn\'t find user: ' . $userId);
throw new \Federator\Exceptions\ServerError("Inbox::post couldn't find user: $userId"); throw new \Federator\Exceptions\ServerError('Inbox::post couldn\'t find user: ' . $userId);
} }
$users = []; $users = [];
@ -132,29 +132,56 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
} }
$ourDomain = $config['generic']['externaldomain']; $ourDomain = $config['generic']['externaldomain'];
$finalReceivers = [];
foreach ($receivers as $receiver) { foreach ($receivers as $receiver) {
if ($receiver === '' || !is_string($receiver)) { if ($receiver === '' || !is_string($receiver)) {
continue; continue;
} }
if (!str_contains($receiver, $ourDomain) && $receiver !== $_user) {
continue;
}
// check if receiver is an actor url from our domain
if ($receiver !== $_user) {
$receiverName = basename((string) (parse_url($receiver, PHP_URL_PATH) ?? ''));
$ourDomain = parse_url($receiver, PHP_URL_HOST);
if ($receiverName === null || $ourDomain === null) {
error_log('Inbox::post no receiverName or domain found for receiver: ' . $receiver);
continue;
}
if ($receiverName[0] === '@') {
$receiverName = substr($receiverName, 1);
}
$receiver = $receiverName;
}
$finalReceivers[] = $receiver;
}
$finalReceivers = array_unique($finalReceivers); // remove duplicates
foreach ($finalReceivers as $receiver) {
if (str_ends_with($receiver, '/followers')) { if (str_ends_with($receiver, '/followers')) {
$actor = $inboxActivity->getAActor(); $actor = $inboxActivity->getAActor();
if ($actor === null || !is_string($actor)) { if ($actor === null || !is_string($actor)) {
error_log("Inbox::post no actor found"); error_log('Inbox::post no actor found');
continue; continue;
} }
// Extract username from the actor URL // Extract username from the actor URL
$username = basename((string) (parse_url($actor, PHP_URL_PATH) ?? '')); $username = basename((string) (parse_url($actor, PHP_URL_PATH) ?? ''));
$domain = parse_url($actor, PHP_URL_HOST); $domain = parse_url($actor, PHP_URL_HOST);
error_log("url $actor to username $username domain $domain");
if ($username === null || $domain === null) { if ($username === null || $domain === null) {
error_log("Inbox::post no username or domain found for recipient: $receiver"); error_log('Inbox::post no username or domain found for recipient: ' . $receiver);
continue; continue;
} }
try { try {
$followers = \Federator\DIO\Followers::getFollowersByFedUser($dbh, $connector, $cache, $username . '@' . $domain); $followers = \Federator\DIO\Followers::getFollowersByFedUser(
$dbh,
$connector,
$cache,
$username . '@' . $domain
);
} catch (\Throwable $e) { } catch (\Throwable $e) {
error_log("Inbox::post get followers for user: " . $username . '@' . $domain . ". Exception: " . $e->getMessage()); error_log('Inbox::post get followers for user: ' . $username . '@' . $domain . '. Exception: '
. $e->getMessage());
continue; continue;
} }
@ -162,19 +189,6 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
$users = array_merge($users, array_column($followers, 'id')); $users = array_merge($users, array_column($followers, 'id'));
} }
} else { } else {
// check if receiver is an actor url from our domain
if (!str_contains($receiver, $ourDomain) && $receiver !== $_user) {
continue;
}
if ($receiver !== $_user) {
$receiverName = basename((string) (parse_url($receiver, PHP_URL_PATH) ?? ''));
$ourDomain = parse_url($receiver, PHP_URL_HOST);
if ($receiverName === null || $ourDomain === null) {
error_log("Inbox::post no receiverName or domain found for receiver: " . $receiver);
continue;
}
$receiver = $receiverName;
}
try { try {
$localUser = \Federator\DIO\User::getUserByName( $localUser = \Federator\DIO\User::getUserByName(
$dbh, $dbh,
@ -183,11 +197,11 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
$cache $cache
); );
} catch (\Throwable $e) { } catch (\Throwable $e) {
error_log("Inbox::post get user by name: " . $receiver . ". Exception: " . $e->getMessage()); error_log('Inbox::post get user by name: ' . $receiver . '. Exception: ' . $e->getMessage());
continue; continue;
} }
if ($localUser === null || $localUser->id === null) { if ($localUser === null || $localUser->id === null) {
error_log("Inbox::post couldn't find user: $receiver"); error_log('Inbox::post 210 couldn\'t find user: ' . $receiver);
continue; continue;
} }
$users[] = $localUser->id; $users[] = $localUser->id;
@ -197,15 +211,27 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
$users = array_unique($users); // remove duplicates $users = array_unique($users); // remove duplicates
if (empty($users)) { // todo remove after proper implementation, debugging for now if (empty($users)) { // todo remove after proper implementation, debugging for now
$rootDir = PROJECT_ROOT . '/'; $rootDir = '/tmp/';
// Save the raw input and parsed JSON to a file for inspection // Save the raw input and parsed JSON to a file for inspection
file_put_contents( file_put_contents(
$rootDir . 'logs/inbox.log', $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", date('Y-m-d H:i:s') . ": ==== POST Inbox Activity ====\n"
. json_encode($inboxActivity, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) . "\n\n",
FILE_APPEND FILE_APPEND
); );
} }
// Set the Redis backend for Resque
$rconfig = parse_ini_file($_SERVER['DOCUMENT_ROOT'] . '/../rediscache.ini');
$redisUrl = sprintf(
'redis://%s:%s@%s:%d?password-encoding=u',
urlencode($rconfig['username']),
urlencode($rconfig['password']),
$rconfig['host'],
intval($rconfig['port'], 10)
);
\Resque::setBackend($redisUrl);
foreach ($users as $receiver) { foreach ($users as $receiver) {
if (!isset($receiver)) { if (!isset($receiver)) {
continue; continue;
@ -215,7 +241,7 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
'recipientId' => $receiver, 'recipientId' => $receiver,
'activity' => $inboxActivity->toObject(), 'activity' => $inboxActivity->toObject(),
]); ]);
error_log("Inbox::post enqueued job for user: $user->id with token: $token"); error_log('Inbox::post enqueued job for user: ' . $user->id . ' with token: ' . $token);
} }
if (empty($users)) { if (empty($users)) {
$type = strtolower($inboxActivity->getType()); $type = strtolower($inboxActivity->getType());
@ -225,211 +251,12 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
'recipientId' => "", 'recipientId' => "",
'activity' => $inboxActivity->toObject(), 'activity' => $inboxActivity->toObject(),
]); ]);
error_log("Inbox::post enqueued job for user: $user->id with token: $token"); error_log('Inbox::post enqueued job for user: ' . $user->id . ' with token: ' . $token);
} else { } else {
error_log("Inbox::post no users found for activity, doing nothing: " . json_encode($inboxActivity, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT)); error_log('Inbox::post no users found for activity, doing nothing: '
. json_encode($inboxActivity, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT));
} }
} }
return 'success';
$connector->sendActivity($user, $inboxActivity);
return "success";
}
/**
* handle post call for specific user
*
* @param \mysqli $dbh database handle
* @param \Federator\Connector\Connector $connector connector to use
* @param \Federator\Cache\Cache|null $cache optional caching service
* @param string $_user user that triggered the post
* @param string $_recipientId recipient of the post
* @param \Federator\Data\ActivityPub\Common\Activity $inboxActivity the activity that we received
* @return boolean response
*/
public static function postForUser($dbh, $connector, $cache, $_user, $_recipientId, $inboxActivity)
{
if (!isset($_user)) {
error_log("Inbox::postForUser no user given");
return false;
}
// get sender
$user = \Federator\DIO\FedUser::getUserByName(
$dbh,
$_user,
$cache
);
if ($user === null || $user->id === null) {
error_log("Inbox::postForUser couldn't find user: $_user");
return false;
}
$type = strtolower($inboxActivity->getType());
if ($_recipientId === '') {
if ($type === 'undo' || $type === 'delete') {
switch ($type) {
case 'delete':
// Delete Note/Post
$object = $inboxActivity->getObject();
if (is_string($object)) {
\Federator\DIO\Posts::deletePost($dbh, $object);
} elseif (is_object($object)) {
$objectId = $object->getID();
\Federator\DIO\Posts::deletePost($dbh, $objectId);
} else {
error_log("Inbox::postForUser Error in Delete Post for user $user->id, object is not a string or object");
error_log(" object of type " . gettype($object));
return false;
}
break;
case 'undo':
$object = $inboxActivity->getObject();
if (is_object($object)) {
switch (strtolower($object->getType())) {
case 'like':
case 'dislike':
// Undo Like/Dislike (remove like/dislike)
$targetId = $object->getID();
// \Federator\DIO\Votes::removeVote($dbh, $user->id, $targetId, 'dislike');
\Federator\DIO\Posts::deletePost($dbh, $targetId);
break;
case 'note':
case 'article':
// Undo Note (remove note)
$noteId = $object->getID();
\Federator\DIO\Posts::deletePost($dbh, $noteId);
break;
}
}
break;
default:
error_log("Inbox::postForUser Unhandled activity type $type for user $user->id");
break;
}
return true;
}
}
$atPos = strpos($_recipientId, '@');
if ($atPos !== false) {
$_recipientId = substr($_recipientId, 0, $atPos);
}
// get recipient
$recipient = \Federator\DIO\User::getUserByName(
$dbh,
$_recipientId,
$connector,
$cache
);
if ($recipient === null || $recipient->id === null) {
error_log("Inbox::postForUser couldn't find recipient: $_recipientId");
return false;
}
$rootDir = PROJECT_ROOT . '/';
// Save the raw input and parsed JSON to a file for inspection
file_put_contents(
$rootDir . 'logs/inbox_' . $recipient->id . '.log',
date('Y-m-d H:i:s') . ": ==== POST " . $recipient->id . " Inbox Activity ====\n" . json_encode($inboxActivity, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) . "\n\n",
FILE_APPEND
);
switch ($type) {
case 'follow':
$success = \Federator\DIO\Followers::addExternalFollow($dbh, $inboxActivity->getID(), $user->id, $recipient->id);
if ($success === false) {
error_log("Inbox::postForUser Failed to add follower for user $user->id");
}
break;
case 'delete':
// Delete Note/Post
$object = $inboxActivity->getObject();
if (is_string($object)) {
\Federator\DIO\Posts::deletePost($dbh, $object);
} elseif (is_object($object)) {
$objectId = $object->getID();
\Federator\DIO\Posts::deletePost($dbh, $objectId);
}
break;
case 'undo':
$object = $inboxActivity->getObject();
if (is_object($object)) {
switch (strtolower($object->getType())) {
case 'follow':
$success = false;
if ($object instanceof \Federator\Data\ActivityPub\Common\Activity) {
$actor = $object->getAActor();
if ($actor !== '') {
$success = \Federator\DIO\Followers::removeFollow($dbh, $user->id, $recipient->id);
}
}
if ($success === false) {
error_log("Inbox::postForUser Failed to remove follower for user $user->id");
}
break;
case 'like':
case 'dislike':
// Undo Like/Dislike (remove like/dislike)
$targetId = $object->getID();
// \Federator\DIO\Votes::removeVote($dbh, $user->id, $targetId, 'dislike');
\Federator\DIO\Posts::deletePost($dbh, $targetId);
break;
case 'note':
// Undo Note (remove note)
$noteId = $object->getID();
\Federator\DIO\Posts::deletePost($dbh, $noteId);
break;
}
}
break;
case 'like':
case 'dislike':
// Add Like/Dislike
$targetId = $inboxActivity->getObject();
if (is_string($targetId)) {
// \Federator\DIO\Votes::addVote($dbh, $user->id, $targetId, 'dislike');
\Federator\DIO\Posts::savePost($dbh, $user->id, $inboxActivity);
} else {
error_log("Inbox::postForUser Error in Add Like/Dislike for user $user->id, targetId is not a string");
return false;
}
break;
case 'create':
case 'update':
$object = $inboxActivity->getObject();
if (is_object($object)) {
switch (strtolower($object->getType())) {
case 'note':
\Federator\DIO\Posts::savePost($dbh, $user->id, $inboxActivity);
break;
case 'article':
\Federator\DIO\Posts::savePost($dbh, $user->id, $inboxActivity);
break;
default:
\Federator\DIO\Posts::savePost($dbh, $user->id, $inboxActivity);
break;
}
}
break;
default:
error_log("Inbox::postForUser Unhandled activity type $type for user $user->id");
break;
}
return true;
} }
} }

View file

@ -58,7 +58,7 @@ class Outbox implements \Federator\Api\FedUsers\FedUsersInterface
$outbox = new \Federator\Data\ActivityPub\Common\Outbox(); $outbox = new \Federator\Data\ActivityPub\Common\Outbox();
$min = intval($this->main->extractFromURI('min', '0'), 10); $min = intval($this->main->extractFromURI('min', '0'), 10);
$max = intval($this->main->extractFromURI('max', '0'), 10); $max = intval($this->main->extractFromURI('max', '0'), 10);
$page = $this->main->extractFromURI("page", ""); $page = $this->main->extractFromURI('page', '');
if ($page !== "") { if ($page !== "") {
$items = \Federator\DIO\Posts::getPostsByUser($dbh, $user->id, $connector, $cache, $min, $max, 20); $items = \Federator\DIO\Posts::getPostsByUser($dbh, $user->id, $connector, $cache, $min, $max, 20);
$outbox->setItems($items); $outbox->setItems($items);
@ -68,8 +68,9 @@ class Outbox implements \Federator\Api\FedUsers\FedUsersInterface
$items = []; $items = [];
} }
$config = $this->main->getConfig(); $config = $this->main->getConfig();
$protocol = $config['generic']['protocol'];
$domain = $config['generic']['externaldomain']; $domain = $config['generic']['externaldomain'];
$id = 'https://' . $domain . '/' . $_user . '/outbox'; $id = $protocol . '://' . $domain . '/' . $_user . '/outbox';
$outbox->setPartOf($id); $outbox->setPartOf($id);
$outbox->setID($id); $outbox->setID($id);
if ($page === '') { if ($page === '') {

View file

@ -20,7 +20,11 @@ class Dummy implements \Federator\Api\APIInterface
*/ */
private $main; private $main;
/** @var array<string, string> $message internal message to output */ /**
* internal message to output
*
* @var Array<string, mixed> $message
*/
private $message = []; private $message = [];
/** /**
@ -51,11 +55,8 @@ class Dummy implements \Federator\Api\APIInterface
case 'GET': case 'GET':
switch (sizeof($paths)) { switch (sizeof($paths)) {
case 3: case 3:
switch ($paths[2]) { if ($paths[2] === 'moo') {
case 'moo':
return $this->getDummy(); return $this->getDummy();
default:
break;
} }
break; break;
} }
@ -63,14 +64,12 @@ class Dummy implements \Federator\Api\APIInterface
case 'POST': case 'POST':
switch (sizeof($paths)) { switch (sizeof($paths)) {
case 3: case 3:
switch ($paths[2]) { if ($paths[2] === 'moo') {
case 'moo':
return $this->postDummy(); return $this->postDummy();
default:
break;
} }
break; break;
} }
break;
} }
$this->main->setResponseCode(404); $this->main->setResponseCode(404);
return false; return false;

View file

@ -47,10 +47,11 @@ class NewContent implements \Federator\Api\APIInterface
*/ */
public function exec($paths, $user) public function exec($paths, $user)
{ {
$method = $_SERVER["REQUEST_METHOD"]; $method = $_SERVER['REQUEST_METHOD'];
$_username = $paths[2]; $_username = $paths[2];
if ($method === 'GET') { // unsupported if ($method === 'GET') { // unsupported
throw new \Federator\Exceptions\InvalidArgument("GET not supported"); /// TODO: throw unsupported method exception
throw new \Federator\Exceptions\InvalidArgument('GET not supported');
} }
switch (sizeof($paths)) { switch (sizeof($paths)) {
case 3: case 3:
@ -67,7 +68,6 @@ class NewContent implements \Federator\Api\APIInterface
return false; return false;
} }
/** /**
* handle post call * handle post call
* *
@ -81,7 +81,7 @@ class NewContent implements \Federator\Api\APIInterface
try { try {
$this->main->checkSignature($allHeaders); $this->main->checkSignature($allHeaders);
} catch (\Federator\Exceptions\PermissionDenied $e) { } catch (\Federator\Exceptions\PermissionDenied $e) {
error_log("NewContent::post Signature check failed: " . $e->getMessage()); error_log('NewContent::post Signature check failed: ' . $e->getMessage());
http_response_code(401); http_response_code(401);
return false; return false;
} }
@ -96,19 +96,19 @@ class NewContent implements \Federator\Api\APIInterface
$config = $this->main->getConfig(); $config = $this->main->getConfig();
$domain = $config['generic']['externaldomain']; $domain = $config['generic']['externaldomain'];
if (!is_array($input)) { if (!is_array($input)) {
error_log("NewContent::post Input wasn't of type array"); error_log('NewContent::post Input wasn\'t of type array');
return false; return false;
} }
$articleId = ""; $articleId = '';
if (isset($allHeaders['X-Sender'])) { if (isset($allHeaders['X-Sender'])) {
$newActivity = $connector->jsonToActivity($input, $articleId); $newActivity = $connector->jsonToActivity($input, $articleId);
} else { } else {
error_log("NewContent::post No X-Sender header found"); error_log('NewContent::post No X-Sender header found');
return false; return false;
} }
if ($newActivity === false) { if ($newActivity === false) {
error_log("NewContent::post couldn't create newActivity"); error_log('NewContent::post couldn\'t create newActivity');
return false; return false;
} }
if (!isset($_user)) { if (!isset($_user)) {
@ -147,15 +147,15 @@ class NewContent implements \Federator\Api\APIInterface
} }
if (str_ends_with($receiver, '/followers')) { if (str_ends_with($receiver, '/followers')) {
if ($posterName === null) { if ($posterName === null) {
error_log("NewContent::post no username found"); error_log('NewContent::post no username found');
continue; continue;
} }
try { try {
$followers = \Federator\DIO\Followers::getFollowersByUser($dbh, $posterName, $connector, $cache); $followers = \Federator\DIO\Followers::getFollowersByUser($dbh, $posterName, $connector, $cache);
} catch (\Throwable $e) { } catch (\Throwable $e) {
error_log("NewContent::post get followers for user: " . $posterName . ". Exception: " . $e->getMessage()); error_log('NewContent::post get followers for user: ' . $posterName . '. Exception: '
. $e->getMessage());
continue; continue;
} }
@ -173,7 +173,7 @@ class NewContent implements \Federator\Api\APIInterface
if ($receiver === $posterName) { if ($receiver === $posterName) {
continue; continue;
} }
error_log("NewContent::post no receiverName or domain found for receiver: " . $receiver); error_log('NewContent::post no receiverName or domain found for receiver: ' . $receiver);
continue; continue;
} }
$receiver = $receiverName . '@' . $domain; $receiver = $receiverName . '@' . $domain;
@ -184,11 +184,11 @@ class NewContent implements \Federator\Api\APIInterface
$cache $cache
); );
} catch (\Throwable $e) { } catch (\Throwable $e) {
error_log("NewContent::post get user by name: " . $receiver . ". Exception: " . $e->getMessage()); error_log('NewContent::post get user by name: ' . $receiver . '. Exception: ' . $e->getMessage());
continue; continue;
} }
if ($user === null || $user->id === null) { if ($user === null || $user->id === null) {
error_log("NewContent::post couldn't find user: $receiver"); error_log('NewContent::post couldn\'t find user: ' . $receiver);
continue; continue;
} }
$users[] = $user->id; $users[] = $user->id;
@ -202,7 +202,8 @@ class NewContent implements \Federator\Api\APIInterface
// Save the raw input and parsed JSON to a file for inspection // Save the raw input and parsed JSON to a file for inspection
file_put_contents( file_put_contents(
$rootDir . 'logs/newContent.log', $rootDir . 'logs/newContent.log',
date('Y-m-d H:i:s') . ": ==== POST NewContent Activity ====\n" . json_encode($newActivity, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) . "\n\n", date('Y-m-d H:i:s') . ": ==== POST NewContent Activity ====\n"
. json_encode($newActivity, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) . "\n\n",
FILE_APPEND FILE_APPEND
); );
} }
@ -218,7 +219,7 @@ class NewContent implements \Federator\Api\APIInterface
'activity' => $newActivity->toObject(), 'activity' => $newActivity->toObject(),
'articleId' => $articleId, 'articleId' => $articleId,
]); ]);
error_log("Inbox::post enqueued job for receiver: $receiver with token: $token"); error_log('Inbox::post enqueued job for receiver: ' . $receiver . ' with token: ' . $token);
} }
return json_encode($newActivity, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT); return json_encode($newActivity, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
@ -244,7 +245,7 @@ class NewContent implements \Federator\Api\APIInterface
public static function postForUser($dbh, $connector, $cache, $host, $_user, $_recipientId, $newActivity, $articleId) public static function postForUser($dbh, $connector, $cache, $host, $_user, $_recipientId, $newActivity, $articleId)
{ {
if (!isset($_user)) { if (!isset($_user)) {
error_log("NewContent::postForUser no user given"); error_log('NewContent::postForUser no user given');
return false; return false;
} }
@ -256,7 +257,7 @@ class NewContent implements \Federator\Api\APIInterface
$cache $cache
); );
if ($user === null || $user->id === null) { if ($user === null || $user->id === null) {
error_log("NewContent::postForUser couldn't find user: $_user"); error_log('NewContent::postForUser couldn\'t find user: ' . $_user);
return false; return false;
} }
@ -267,7 +268,7 @@ class NewContent implements \Federator\Api\APIInterface
$cache $cache
); );
if ($recipient === null || $recipient->id === null) { if ($recipient === null || $recipient->id === null) {
error_log("NewContent::postForUser couldn't find user: $_recipientId"); error_log('NewContent::postForUser couldn\'t find user: ' . $_recipientId);
return false; return false;
} }
@ -275,7 +276,8 @@ class NewContent implements \Federator\Api\APIInterface
// Save the raw input and parsed JSON to a file for inspection // Save the raw input and parsed JSON to a file for inspection
file_put_contents( file_put_contents(
$rootDir . 'logs/newcontent_' . $recipient->id . '.log', $rootDir . 'logs/newcontent_' . $recipient->id . '.log',
date('Y-m-d H:i:s') . ": ==== POST " . $recipient->id . " NewContent Activity ====\n" . json_encode($newActivity, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) . "\n\n", date('Y-m-d H:i:s') . ": ==== POST " . $recipient->id . " NewContent Activity ====\n"
. json_encode($newActivity, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) . "\n\n",
FILE_APPEND FILE_APPEND
); );
@ -286,13 +288,12 @@ class NewContent implements \Federator\Api\APIInterface
// $success = false; // $success = false;
$actor = $newActivity->getAActor(); $actor = $newActivity->getAActor();
if ($actor !== '') { if ($actor !== '') {
// $followerUsername = basename((string) (parse_url($actor, PHP_URL_PATH) ?? ''));
// $followerDomain = parse_url($actor, PHP_URL_HOST);
$newIdUrl = \Federator\DIO\Followers::generateNewFollowId($dbh, $host); $newIdUrl = \Federator\DIO\Followers::generateNewFollowId($dbh, $host);
$newActivity->setID($newIdUrl); $newActivity->setID($newIdUrl);
/*if (is_string($followerDomain)) { /*if (is_string($followerDomain)) {
$followerId = "{$followerUsername}@{$followerDomain}"; $followerId = "{$followerUsername}@{$followerDomain}";
$success = \Federator\DIO\Followers::sendFollowRequest($dbh, $connector, $cache, $user->id, $followerId, $followerDomain); $success = \Federator\DIO\Followers::sendFollowRequest($dbh, $connector, $cache, $user->id,
$followerId, $followerDomain);
}*/ }*/
} }
/* if ($success === false) { /* if ($success === false) {
@ -323,20 +324,25 @@ class NewContent implements \Federator\Api\APIInterface
$followerUsername = basename((string) (parse_url($actor, PHP_URL_PATH) ?? '')); $followerUsername = basename((string) (parse_url($actor, PHP_URL_PATH) ?? ''));
$followerDomain = parse_url($actor, PHP_URL_HOST); $followerDomain = parse_url($actor, PHP_URL_HOST);
if (is_string($followerDomain)) { if (is_string($followerDomain)) {
$followerId = "{$followerUsername}@{$followerDomain}"; $followerId = $followerUsername . '@' . $followerDomain;
$removedId = \Federator\DIO\Followers::removeFollow($dbh, $followerId, $user->id); $removedId = \Federator\DIO\Followers::removeFollow(
$dbh,
$followerId,
$user->id
);
if ($removedId !== false) { if ($removedId !== false) {
$object->setID($removedId); $object->setID($removedId);
$newActivity->setObject($object); $newActivity->setObject($object);
$success = true; $success = true;
} else { } else {
error_log("NewContent::postForUser Failed to remove follow for user $user->id"); error_log('NewContent::postForUser Failed to remove follow for user '
. $user->id);
} }
} }
} }
} }
if ($success === false) { if ($success === false) {
error_log("NewContent::postForUser Failed to remove follower for user $user->id"); error_log('NewContent::postForUser Failed to remove follower for user ' . $user->id);
} }
break; break;
case 'like': case 'like':
@ -344,10 +350,10 @@ class NewContent implements \Federator\Api\APIInterface
if (method_exists($object, 'getObject')) { if (method_exists($object, 'getObject')) {
$targetId = $object->getObject(); $targetId = $object->getObject();
if (is_string($targetId)) { if (is_string($targetId)) {
// \Federator\DIO\Votes::removeVote($dbh, $user->id, $targetId); \Federator\DIO\Votes::removeVote($dbh, $user->id, $targetId);
\Federator\DIO\Posts::deletePost($dbh, $targetId);
} else { } else {
error_log("NewContent::postForUser Error in Undo Like/Dislike for user $user->id, targetId is not a string"); error_log('NewContent::postForUser Error in Undo Like/Dislike for user ' . $user->id
. ', targetId is not a string');
} }
} }
break; break;
@ -355,7 +361,6 @@ class NewContent implements \Federator\Api\APIInterface
// Undo Note (remove note) // Undo Note (remove note)
$noteId = $object->getID(); $noteId = $object->getID();
\Federator\DIO\Posts::deletePost($dbh, $noteId); \Federator\DIO\Posts::deletePost($dbh, $noteId);
break; break;
case 'article': case 'article':
$articleId = $object->getID(); $articleId = $object->getID();
@ -366,7 +371,8 @@ class NewContent implements \Federator\Api\APIInterface
// Undo Article (remove article) // Undo Article (remove article)
$idPart = strrchr($recipient->id, '@'); $idPart = strrchr($recipient->id, '@');
if ($idPart === false) { if ($idPart === false) {
error_log("NewContent::postForUser Error in Undo Article. $recipient->id, recipient ID is not valid"); error_log('NewContent::postForUser Error in Undo Article. ' . $recipient->id
. ', recipient ID is not valid');
return false; return false;
} else { } else {
$targetUrl = ltrim($idPart, '@'); $targetUrl = ltrim($idPart, '@');
@ -375,7 +381,8 @@ class NewContent implements \Federator\Api\APIInterface
$object = \Federator\DIO\Article::conditionalConvertToNote($object, $targetUrl); $object = \Federator\DIO\Article::conditionalConvertToNote($object, $targetUrl);
$newActivity->setObject($object); $newActivity->setObject($object);
} else { } else {
error_log("NewContent::postForUser Error in Undo Article for recipient $recipient->id, object is not an Article"); error_log('NewContent::postForUser Error in Undo Article for recipient '
. $recipient->id . ', object is not an Article');
} }
} }
@ -384,7 +391,8 @@ class NewContent implements \Federator\Api\APIInterface
} elseif (is_string($object)) { } elseif (is_string($object)) {
\Federator\DIO\Posts::deletePost($dbh, $object); \Federator\DIO\Posts::deletePost($dbh, $object);
} else { } else {
error_log("NewContent::postForUser Error in Undo for recipient $recipient->id, object is not a string or object"); error_log('NewContent::postForUser Error in Undo for recipient ' . $recipient->id
. ', object is not a string or object');
} }
break; break;
@ -393,10 +401,11 @@ class NewContent implements \Federator\Api\APIInterface
// Add Like/Dislike // Add Like/Dislike
$targetId = $newActivity->getObject(); $targetId = $newActivity->getObject();
if (is_string($targetId)) { if (is_string($targetId)) {
// \Federator\DIO\Votes::addVote($dbh, $user->id, $targetId, 'like'); \Federator\DIO\Votes::addVote($dbh, $user->id, $targetId, $type);
\Federator\DIO\Posts::savePost($dbh, $user->id, $newActivity, $articleId); // \Federator\DIO\Posts::savePost($dbh, $user->id, $newActivity, $articleId);
} else { } else {
error_log("NewContent::postForUser Error in Add Like/Dislike for recipient $recipient->id, targetId is not a string"); error_log('NewContent::postForUser Error in Add Like/Dislike for recipient ' . $recipient->id
. ', targetId is not a string');
return false; return false;
} }
break; break;
@ -415,7 +424,8 @@ class NewContent implements \Federator\Api\APIInterface
$idPart = strrchr($recipient->id, '@'); $idPart = strrchr($recipient->id, '@');
if ($idPart === false) { if ($idPart === false) {
error_log("NewContent::postForUser Error in Create/Update Article. $recipient->id, recipient ID is not valid"); error_log('NewContent::postForUser Error in Create/Update Article. ' . $recipient->id
. ', recipient ID is not valid');
return false; return false;
} else { } else {
$targetUrl = ltrim($idPart, '@'); $targetUrl = ltrim($idPart, '@');
@ -424,7 +434,8 @@ class NewContent implements \Federator\Api\APIInterface
$object = \Federator\DIO\Article::conditionalConvertToNote($object, $targetUrl); $object = \Federator\DIO\Article::conditionalConvertToNote($object, $targetUrl);
$newActivity->setObject($object); $newActivity->setObject($object);
} else { } else {
error_log("NewContent::postForUser Error in Create/Update Article for recipient $recipient->id, object is not an Article"); error_log('NewContent::postForUser Error in Create/Update Article for recipient '
. $recipient->id . ', object is not an Article');
} }
} }
@ -438,117 +449,26 @@ class NewContent implements \Federator\Api\APIInterface
break; break;
default: default:
error_log("NewContent::postForUser Unhandled activity type $type for user $user->id"); error_log('NewContent::postForUser Unhandled activity type $type for user ' . $user->id);
break; break;
} }
try { try {
$response = self::sendActivity($dbh, $host, $user, $recipient, $newActivity); $response = \Federator\DIO\Server::sendActivity($dbh, $host, $user, $recipient, $newActivity);
} catch (\Exception $e) { } catch (\Exception $e) {
error_log("NewContent::postForUser Failed to send activity: " . $e->getMessage()); error_log('NewContent::postForUser Failed to send activity: ' . $e->getMessage());
return false; return false;
} }
if (empty($response)) { if (empty($response)) {
error_log("NewContent::postForUser Sent activity to $recipient->id"); error_log('NewContent::postForUser Sent activity to ' . $recipient->id);
} else { } else {
error_log("NewContent::postForUser Sent activity to $recipient->id with response: " . json_encode($response, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT)); error_log('NewContent::postForUser Sent activity to ' . $recipient->id . ' with response: '
. json_encode($response, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT));
} }
return true; return true;
} }
/**
* send activity to federated server
*
* @param \mysqli $dbh database handle
* @param string $host host url of our server (e.g. federator)
* @param \Federator\Data\User $sender source user
* @param \Federator\Data\FedUser $target federated target user
* @param \Federator\Data\ActivityPub\Common\Activity $activity activity to send
* @return string|true the generated follow ID on success, false on failure
*/
public static function sendActivity($dbh, $host, $sender, $target, $activity)
{
if ($dbh === false) {
throw new \Federator\Exceptions\ServerError("NewContent::sendActivity Failed to get database handle");
}
$inboxUrl = $target->inboxURL;
$json = json_encode($activity, JSON_UNESCAPED_SLASHES);
if ($json === false) {
throw new \Exception('Failed to encode JSON: ' . json_last_error_msg());
}
$digest = 'SHA-256=' . base64_encode(hash('sha256', $json, true));
$date = gmdate('D, d M Y H:i:s') . ' GMT';
$parsed = parse_url($inboxUrl);
if ($parsed === false) {
throw new \Exception('Failed to parse URL: ' . $inboxUrl);
}
if (!isset($parsed['host']) || !isset($parsed['path'])) {
throw new \Exception('Invalid inbox URL: missing host or path');
}
$extHost = $parsed['host'];
$path = $parsed['path'];
// Build the signature string
$signatureString = "(request-target): post {$path}\n" .
"host: {$extHost}\n" .
"date: {$date}\n" .
"digest: {$digest}";
// Get rsa private key
$privateKey = \Federator\DIO\User::getrsaprivate($dbh, $sender->id); // OR from DB
if ($privateKey === false) {
throw new \Exception('Failed to get private key');
}
$pkeyId = openssl_pkey_get_private($privateKey);
if ($pkeyId === false) {
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 = $host . '/' . $sender->id . '#main-key';
$signatureHeader = 'keyId="' . $keyId . '",algorithm="rsa-sha256",headers="(request-target) host date digest",signature="' . $signature_b64 . '"';
$ch = curl_init($inboxUrl);
if ($ch === false) {
throw new \Exception('Failed to initialize cURL');
}
$headers = [
'Host: ' . $extHost,
'Date: ' . $date,
'Digest: ' . $digest,
'Content-Type: application/activity+json',
'Signature: ' . $signatureHeader,
'Accept: application/activity+json',
];
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);
curl_close($ch);
if ($response === false) {
throw new \Exception("Failed to send activity: " . curl_error($ch));
} else {
$httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($httpcode != 200 && $httpcode != 202) {
throw new \Exception("Unexpected HTTP code $httpcode: $response");
}
}
return $response;
}
/** /**
* get internal represenation as json string * get internal represenation as json string
* @return string json string or html * @return string json string or html

View file

@ -61,7 +61,7 @@ class WellKnown implements APIInterface
*/ */
public function exec($paths, $user) public function exec($paths, $user)
{ {
$method = $_SERVER["REQUEST_METHOD"]; $method = $_SERVER['REQUEST_METHOD'];
switch ($method) { switch ($method) {
case 'GET': case 'GET':
switch (sizeof($paths)) { switch (sizeof($paths)) {

View file

@ -67,7 +67,6 @@ class NodeInfo
$template = 'nodeinfo2.0.json'; $template = 'nodeinfo2.0.json';
} }
$stats = \Federator\DIO\Stats::getStats($this->main); $stats = \Federator\DIO\Stats::getStats($this->main);
echo "fetch usercount via connector\n";
$data['usercount'] = $stats->userCount; $data['usercount'] = $stats->userCount;
$data['postcount'] = $stats->postCount; $data['postcount'] = $stats->postCount;
$data['commentcount'] = $stats->commentCount; $data['commentcount'] = $stats->commentCount;

View file

@ -48,10 +48,10 @@ class WebFinger
$matches = []; $matches = [];
$config = $this->main->getConfig(); $config = $this->main->getConfig();
$domain = $config['generic']['externaldomain']; $domain = $config['generic']['externaldomain'];
if (preg_match("/^acct:([^@]+)@(.*)$/", $_resource, $matches) != 1 || $matches[2] !== $domain) { $sourcedomain = $config['generic']['sourcedomain'];
if (preg_match("/^acct:([^@]+)@(.*)$/", $_resource, $matches) != 1 || ($matches[2] !== $sourcedomain && $matches[2] !== $domain)) {
throw new \Federator\Exceptions\InvalidArgument(); throw new \Federator\Exceptions\InvalidArgument();
} }
$domain = $matches[2];
$user = \Federator\DIO\User::getUserByName( $user = \Federator\DIO\User::getUserByName(
$this->main->getDatabase(), $this->main->getDatabase(),
$matches[1], $matches[1],
@ -59,12 +59,12 @@ class WebFinger
$this->main->getCache() $this->main->getCache()
); );
if ($user->id == 0) { if ($user->id == 0) {
echo "not found";
throw new \Federator\Exceptions\FileNotFound(); throw new \Federator\Exceptions\FileNotFound();
} }
$data = [ $data = [
'username' => $user->id, 'username' => $user->id,
'domain' => $domain, 'domain' => $domain,
'sourcedomain' => $sourcedomain,
]; ];
$response = $this->main->renderTemplate('webfinger_acct.json', $data); $response = $this->main->renderTemplate('webfinger_acct.json', $data);
$this->wellKnown->setResponse($response); $this->wellKnown->setResponse($response);

View file

@ -20,7 +20,7 @@ interface Cache extends \Federator\Connector\Connector
* @param \Federator\Data\FedUser[]|false $followers user followers * @param \Federator\Data\FedUser[]|false $followers user followers
* @return void * @return void
*/ */
public function saveRemoteFollowersOfUser($user, $followers); public function saveFollowersByUser($user, $followers);
/** /**
* save remote following for user * save remote following for user
@ -29,7 +29,7 @@ interface Cache extends \Federator\Connector\Connector
* @param \Federator\Data\FedUser[]|false $following user following * @param \Federator\Data\FedUser[]|false $following user following
* @return void * @return void
*/ */
public function saveRemoteFollowingForUser($user, $following); public function saveFollowingByUser($user, $following);
/** /**
* save remote posts by user * save remote posts by user

View file

@ -17,19 +17,17 @@ interface Connector
* get followers of given user * get followers of given user
* *
* @param string $id user id * @param string $id user id
* @return \Federator\Data\FedUser[]|false * @return \Federator\Data\FedUser[]|false
*/ */
public function getRemoteFollowersOfUser($id); public function getFollowersByUser($id);
/** /**
* get following of given user * get following of given user
* *
* @param string $id user id * @param string $id user id
* @return \Federator\Data\FedUser[]|false * @return \Federator\Data\FedUser[]|false
*/ */
public function getRemoteFollowingForUser($id); public function getFollowingByUser($id);
/** /**
* get posts by given user * get posts by given user
@ -38,7 +36,6 @@ interface Connector
* @param int $min min value * @param int $min min value
* @param int $max max value * @param int $max max value
* @param int $limit maximum number of results * @param int $limit maximum number of results
* @return \Federator\Data\ActivityPub\Common\Activity[]|false * @return \Federator\Data\ActivityPub\Common\Activity[]|false
*/ */
public function getRemotePostsByUser($id, $min, $max, $limit); public function getRemotePostsByUser($id, $min, $max, $limit);

View file

@ -10,7 +10,7 @@ namespace Federator\Data\ActivityPub\Common;
class Collection extends APObject class Collection extends APObject
{ {
protected int $totalItems = 0; protected int $totalItems = -1;
private string $first = ''; private string $first = '';
private string $last = ''; private string $last = '';
@ -28,7 +28,7 @@ class Collection extends APObject
{ {
$return = parent::toObject(); $return = parent::toObject();
$return['type'] = 'Collection'; $return['type'] = 'Collection';
if ($this->totalItems > 0) { if ($this->totalItems >= 0) {
$return['totalItems'] = $this->totalItems; $return['totalItems'] = $this->totalItems;
} }
if ($this->first !== '') { if ($this->first !== '') {

View file

@ -49,15 +49,14 @@ class Article
*/ */
public static function conditionalConvertToNote($article, $targetUrl) public static function conditionalConvertToNote($article, $targetUrl)
{ {
$supportFile = file_get_contents(PROJECT_ROOT . '/formatsupport.json'); $supportFile = file_get_contents($_SERVER['DOCUMENT_ROOT'] . '../formatsupport.json');
if ($supportFile === false) { if ($supportFile === false) {
error_log("Article::conditionalConvertToNote Failed to read support file for article conversion."); error_log("Article::conditionalConvertToNote Failed to read support file for article conversion.");
return $article; // Fallback to original article if file read fails return $article; // Fallback to original article if file read fails
} }
$supportlist = json_decode($supportFile, true); $supportlist = json_decode($supportFile, true);
if ( if (!isset($supportlist['activitypub']['article']) ||
!isset($supportlist['activitypub']['article']) ||
!is_array($supportlist['activitypub']['article']) || !is_array($supportlist['activitypub']['article']) ||
!in_array($targetUrl, $supportlist['activitypub']['article'], true) !in_array($targetUrl, $supportlist['activitypub']['article'], true)
) { ) {

View file

@ -20,15 +20,15 @@ class FedUser
* @param string $_user user/profile name * @param string $_user user/profile name
* @return void * @return void
*/ */
protected static function addLocalUser($dbh, $user, $_user) protected static function addUserToDB($dbh, $user, $_user)
{ {
// check if it is timed out user // check if it is timed out user
$sql = 'select unix_timestamp(`validuntil`) from fedusers where id=?'; $sql = 'select unix_timestamp(`validuntil`) from fedusers where id=?';
$stmt = $dbh->prepare($sql); $stmt = $dbh->prepare($sql);
if ($stmt === false) { if ($stmt === false) {
throw new \Federator\Exceptions\ServerError("FedUser::addLocalUser Failed to prepare statement"); throw new \Federator\Exceptions\ServerError('FedUser::addLocalUser Failed to prepare statement');
} }
$stmt->bind_param("s", $_user); $stmt->bind_param('s', $_user);
$validuntil = 0; $validuntil = 0;
$ret = $stmt->bind_result($validuntil); $ret = $stmt->bind_result($validuntil);
$stmt->execute(); $stmt->execute();
@ -42,10 +42,10 @@ class FedUser
$sql .= ' values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, now() + interval 1 day)'; $sql .= ' values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, now() + interval 1 day)';
$stmt = $dbh->prepare($sql); $stmt = $dbh->prepare($sql);
if ($stmt === false) { if ($stmt === false) {
throw new \Federator\Exceptions\ServerError("FedUser::addLocalUser Failed to prepare create statement"); throw new \Federator\Exceptions\ServerError('FedUser::addLocalUser Failed to prepare create statement');
} }
$stmt->bind_param( $stmt->bind_param(
"ssssssssssss", 'ssssssssssss',
$_user, $_user,
$user->actorURL, $user->actorURL,
$user->name, $user->name,
@ -66,10 +66,10 @@ class FedUser
$sql .= ' where id=?'; $sql .= ' where id=?';
$stmt = $dbh->prepare($sql); $stmt = $dbh->prepare($sql);
if ($stmt === false) { if ($stmt === false) {
throw new \Federator\Exceptions\ServerError("FedUser::extendUser Failed to prepare update statement"); throw new \Federator\Exceptions\ServerError('FedUser::extendUser Failed to prepare update statement');
} }
$stmt->bind_param( $stmt->bind_param(
"ssssssssssss", 'ssssssssssss',
$user->actorURL, $user->actorURL,
$user->name, $user->name,
$user->publicKey, $user->publicKey,
@ -106,9 +106,9 @@ class FedUser
$sql = 'select id,unix_timestamp(`validuntil`) from fedusers where id=?'; $sql = 'select id,unix_timestamp(`validuntil`) from fedusers where id=?';
$stmt = $dbh->prepare($sql); $stmt = $dbh->prepare($sql);
if ($stmt === false) { if ($stmt === false) {
throw new \Federator\Exceptions\ServerError("FedUser::extendUser Failed to prepare statement"); throw new \Federator\Exceptions\ServerError('FedUser::extendUser Failed to prepare statement');
} }
$stmt->bind_param("s", $_user); $stmt->bind_param('s', $_user);
$validuntil = 0; $validuntil = 0;
$ret = $stmt->bind_result($user->id, $validuntil); $ret = $stmt->bind_result($user->id, $validuntil);
$stmt->execute(); $stmt->execute();
@ -118,7 +118,7 @@ class FedUser
$stmt->close(); $stmt->close();
// if a new user, create own database entry with additionally needed info // if a new user, create own database entry with additionally needed info
if ($user->id === null || $validuntil < time()) { if ($user->id === null || $validuntil < time()) {
self::addLocalUser($dbh, $user, $_user); self::addUserToDB($dbh, $user, $_user);
} }
// no further processing for now // no further processing for now
@ -147,14 +147,14 @@ class FedUser
return $user; return $user;
} }
// check our db // check our db
$sql = 'select `id`, `url`, `name`, `publickey`, `summary`, `type`, `inboxurl`, `sharedinboxurl`, `followersurl`,'; $sql = 'select `id`, `url`, `name`, `publickey`, `summary`, `type`, `inboxurl`, `sharedinboxurl`, ';
$sql .= ' `followingurl`, `publickeyid`, `outboxurl`'; $sql .= '`followersurl`, `followingurl`, `publickeyid`, `outboxurl`';
$sql .= ' from fedusers where `id`=? and `validuntil`>=now()'; $sql .= ' from fedusers where `id`=? and `validuntil`>=now()';
$stmt = $dbh->prepare($sql); $stmt = $dbh->prepare($sql);
if ($stmt === false) { if ($stmt === false) {
throw new \Federator\Exceptions\ServerError("FedUser::getUserByName Failed to prepare statement"); throw new \Federator\Exceptions\ServerError('FedUser::getUserByName Failed to prepare statement');
} }
$stmt->bind_param("s", $_name); $stmt->bind_param('s', $_name);
$user = new \Federator\Data\FedUser(); $user = new \Federator\Data\FedUser();
$ret = $stmt->bind_result( $ret = $stmt->bind_result(
$user->id, $user->id,
@ -184,11 +184,13 @@ class FedUser
$headers = ['Accept: application/activity+json']; $headers = ['Accept: application/activity+json'];
[$response, $info] = \Federator\Main::getFromRemote($remoteURL, $headers); [$response, $info] = \Federator\Main::getFromRemote($remoteURL, $headers);
if ($info['http_code'] != 200) { if ($info['http_code'] != 200) {
throw new \Federator\Exceptions\ServerError("FedUser::getUserByName Failed to fetch webfinger for " . $_name); throw new \Federator\Exceptions\ServerError('FedUser::getUserByName Failed to fetch webfinger for '
. $_name);
} }
$r = json_decode($response, true); $r = json_decode($response, true);
if ($r === false || $r === null || !is_array($r)) { if ($r === false || $r === null || !is_array($r)) {
throw new \Federator\Exceptions\ServerError("FedUser::getUserByName Failed to decode webfinger for " . $_name); throw new \Federator\Exceptions\ServerError('FedUser::getUserByName Failed to decode webfinger for '
. $_name);
} }
// get the webwinger user url and fetch the user // get the webwinger user url and fetch the user
if (isset($r['links'])) { if (isset($r['links'])) {
@ -200,17 +202,23 @@ class FedUser
} }
} }
if (!isset($remoteURL)) { if (!isset($remoteURL)) {
throw new \Federator\Exceptions\ServerError("FedUser::getUserByName Failed to find self link in webfinger for " . $_name); throw new \Federator\Exceptions\ServerError('FedUser::getUserByName Failed to find self link '
. 'in webfinger for ' . $_name);
}
} else {
$remoteURL = $_name;
} }
// fetch the user // fetch the user
$headers = ['Accept: application/activity+json']; $headers = ['Accept: application/activity+json'];
[$response, $info] = \Federator\Main::getFromRemote($remoteURL, $headers); [$response, $info] = \Federator\Main::getFromRemote($remoteURL, $headers);
if ($info['http_code'] != 200) { if ($info['http_code'] != 200) {
throw new \Federator\Exceptions\ServerError("FedUser::getUserByName Failed to fetch user from remoteUrl for " . $_name); throw new \Federator\Exceptions\ServerError('FedUser::getUserByName Failed to fetch user from '
. 'remoteUrl for ' . $_name);
} }
$r = json_decode($response, true); $r = json_decode($response, true);
if ($r === false || $r === null || !is_array($r)) { if ($r === false || $r === null || !is_array($r)) {
throw new \Federator\Exceptions\ServerError("FedUser::getUserByName Failed to decode user for " . $_name); throw new \Federator\Exceptions\ServerError('FedUser::getUserByName Failed to decode user for '
. $_name);
} }
$r['publicKeyId'] = $r['publicKey']['id']; $r['publicKeyId'] = $r['publicKey']['id'];
$r['publicKey'] = $r['publicKey']['publicKeyPem']; $r['publicKey'] = $r['publicKey']['publicKeyPem'];
@ -222,21 +230,240 @@ class FedUser
$r['actorURL'] = $remoteURL; $r['actorURL'] = $remoteURL;
$data = json_encode($r); $data = json_encode($r);
if ($data === false) { if ($data === false) {
throw new \Federator\Exceptions\ServerError("FedUser::getUserByName Failed to encode userdata " . $_name); throw new \Federator\Exceptions\ServerError('FedUser::getUserByName Failed to encode userdata '
. $_name);
} }
$user = \Federator\Data\FedUser::createFromJson($data); $user = \Federator\Data\FedUser::createFromJson($data);
} }
}
if ($cache !== null && $user !== false) { if ($cache !== null && $user !== false) {
if ($user->id !== null && $user->actorURL !== null) { if ($user->id !== null && $user->actorURL !== null) {
self::addLocalUser($dbh, $user, $_name); self::addUserToDB($dbh, $user, $_name);
} }
$cache->saveRemoteFedUserByName($_name, $user); $cache->saveRemoteFedUserByName($_name, $user);
} }
if ($user === false) { if ($user === false) {
throw new \Federator\Exceptions\ServerError("FedUser::getUserByName User not found"); throw new \Federator\Exceptions\ServerError('FedUser::getUserByName User not found');
} }
return $user; return $user;
} }
/**
* handle post call for specific user
*
* @param \Federator\Main $main main instance
* @param \mysqli $dbh database handle
* @param \Federator\Connector\Connector $connector connector to use
* @param \Federator\Cache\Cache|null $cache optional caching service
* @param string $_user user that triggered the post
* @param string $_recipientId recipient of the post
* @param \Federator\Data\ActivityPub\Common\Activity $inboxActivity the activity that we received
* @return boolean response
*/
public static function inboxForUser($main, $dbh, $connector, $cache, $_user, $_recipientId, $inboxActivity)
{
if (!isset($_user)) {
error_log('Inbox::postForUser no user given');
return false;
}
// get sender
$user = \Federator\DIO\FedUser::getUserByName(
$dbh,
$_user,
$cache
);
if ($user === null || $user->id === null) {
error_log('Inbox::postForUser couldn\'t find user: ' . $_user);
return false;
}
$type = strtolower($inboxActivity->getType());
if ($_recipientId === '') {
if ($type === 'undo' || $type === 'delete') {
switch ($type) {
case 'delete':
// Delete Note/Post
$object = $inboxActivity->getObject();
if (is_string($object)) {
\Federator\DIO\Posts::deletePost($dbh, $object);
} elseif (is_object($object)) {
$objectId = $object->getID();
\Federator\DIO\Posts::deletePost($dbh, $objectId);
} else {
error_log('Inbox::postForUser Error in Delete Post for user ' . $user->id
. ', object is not a string or object');
error_log(' object of type ' . gettype($object));
return false;
}
break;
case 'undo':
$object = $inboxActivity->getObject();
if (is_object($object)) {
switch (strtolower($object->getType())) {
case 'like':
case 'dislike':
// Undo Like/Dislike (remove like/dislike)
$targetId = $object->getID();
// \Federator\DIO\Votes::removeVote($dbh, $user->id, $targetId, 'dislike');
\Federator\DIO\Posts::deletePost($dbh, $targetId);
break;
case 'note':
case 'article':
// Undo Note (remove note)
$noteId = $object->getID();
\Federator\DIO\Posts::deletePost($dbh, $noteId);
break;
}
}
break;
default:
error_log('Inbox::postForUser Unhandled activity type ' . $type . ' for user ' . $user->id);
break;
}
return true;
}
}
$atPos = strpos($_recipientId, '@');
if ($atPos !== false) {
$_recipientId = substr($_recipientId, 0, $atPos);
}
// get recipient
$recipient = \Federator\DIO\User::getUserByName(
$dbh,
$_recipientId,
$connector,
$cache
);
if ($recipient === null || $recipient->id === null) {
error_log('Inbox::postForUser couldn\'t find recipient: ' . $_recipientId);
return false;
}
$rootDir = $_SERVER['DOCUMENT_ROOT'] . '../';
// Save the raw input and parsed JSON to a file for inspection
file_put_contents(
$rootDir . 'logs/inbox_' . $recipient->id . '.log',
date('Y-m-d H:i:s') . ": ==== POST " . $recipient->id . " Inbox Activity ====\n"
. json_encode($inboxActivity, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) . "\n\n",
FILE_APPEND
);
switch ($type) {
case 'follow':
$success = \Federator\DIO\Followers::addExternalFollow(
$dbh,
$inboxActivity->getID(),
$user->id,
$recipient->id
);
if ($success === true) {
// send accept back
$accept = new \Federator\Data\ActivityPub\Common\Accept();
$local = $inboxActivity->getObject();
if (is_string($local)) {
$accept->setAActor($local);
$id = bin2hex(openssl_random_pseudo_bytes(4));
$accept->setID($local . '#accepts/follows/' . $id);
$obj = new \Federator\Data\ActivityPub\Common\Activity($inboxActivity->getType());
$config = $main->getConfig();
$ourhost = $config['generic']['protocol'] . '://' . $config['generic']['externaldomain'];
$obj->setID($ourhost . '/' . $id);
$obj->setAActor($inboxActivity->getAActor());
$obj->setObject($local);
$accept->setObject($obj);
// send
\Federator\DIO\Server::sendActivity($dbh, $ourhost, $recipient, $user, $accept);
}
} else {
error_log('Inbox::postForUser Failed to add follower for user ' . $user->id);
}
break;
case 'delete':
// Delete Note/Post
$object = $inboxActivity->getObject();
if (is_string($object)) {
\Federator\DIO\Posts::deletePost($dbh, $object);
} elseif (is_object($object)) {
$objectId = $object->getID();
\Federator\DIO\Posts::deletePost($dbh, $objectId);
}
break;
case 'undo':
$object = $inboxActivity->getObject();
if (is_object($object)) {
switch (strtolower($object->getType())) {
case 'follow':
$success = false;
if ($object instanceof \Federator\Data\ActivityPub\Common\Activity) {
$actor = $object->getAActor();
if ($actor !== '') {
$success = \Federator\DIO\Followers::removeFollow($dbh, $user->id, $recipient->id);
}
}
if ($success === false) {
error_log('Inbox::postForUser Failed to remove follower for user ' . $user->id);
}
break;
case 'like':
case 'dislike':
// Undo Like/Dislike (remove like/dislike)
$targetId = $object->getID();
\Federator\DIO\Votes::removeVote($dbh, $user->id, $targetId);
// \Federator\DIO\Posts::deletePost($dbh, $targetId);
break;
case 'note':
// Undo Note (remove note)
$noteId = $object->getID();
\Federator\DIO\Posts::deletePost($dbh, $noteId);
break;
}
}
break;
case 'like':
case 'dislike':
// Add Like/Dislike
$targetId = $inboxActivity->getObject();
if (is_string($targetId)) {
\Federator\DIO\Votes::addVote($dbh, $user->id, $targetId, $type);
} else {
error_log('Inbox::postForUser Error in Add Like/Dislike for user ' . $user->id
. ', targetId is not a string');
return false;
}
break;
case 'create':
case 'update':
$object = $inboxActivity->getObject();
if (is_object($object)) {
switch (strtolower($object->getType())) {
case 'note':
\Federator\DIO\Posts::savePost($dbh, $user->id, $inboxActivity);
break;
case 'article':
\Federator\DIO\Posts::savePost($dbh, $user->id, $inboxActivity);
break;
default:
\Federator\DIO\Posts::savePost($dbh, $user->id, $inboxActivity);
break;
}
}
break;
default:
error_log('Inbox::postForUser Unhandled activity type $type for user ' . $user->id);
break;
}
return true;
}
} }

View file

@ -30,7 +30,7 @@ class Followers
{ {
// ask cache // ask cache
if ($cache !== null) { if ($cache !== null) {
$followers = $cache->getRemoteFollowersOfUser($id); $followers = $cache->getFollowersByUser($id);
if ($followers !== false) { if ($followers !== false) {
return $followers; return $followers;
} }
@ -39,9 +39,9 @@ class Followers
$sql = 'select source_user from follows where target_user = ?'; $sql = 'select source_user from follows where target_user = ?';
$stmt = $dbh->prepare($sql); $stmt = $dbh->prepare($sql);
if ($stmt === false) { if ($stmt === false) {
throw new \Federator\Exceptions\ServerError("Followers::getFollowersByUser Failed to prepare statement"); throw new \Federator\Exceptions\ServerError('Followers::getFollowersByUser Failed to prepare statement');
} }
$stmt->bind_param("s", $id); $stmt->bind_param('s', $id);
$stmt->execute(); $stmt->execute();
$followerIds = []; $followerIds = [];
$stmt->bind_result($sourceUser); $stmt->bind_result($sourceUser);
@ -57,7 +57,7 @@ class Followers
$cache, $cache,
); );
} catch (\Throwable $e) { } catch (\Throwable $e) {
error_log("Followers::getFollowersByUser Exception: " . $e->getMessage()); error_log('Followers::getFollowersByUser Exception: ' . $e->getMessage());
continue; // Skip this user if an exception occurs continue; // Skip this user if an exception occurs
} }
if ($user !== false && $user->id !== null) { if ($user !== false && $user->id !== null) {
@ -67,14 +67,14 @@ class Followers
if ($followers === []) { if ($followers === []) {
// ask connector for user-id // ask connector for user-id
$followers = $connector->getRemoteFollowersOfUser($id); $followers = $connector->getFollowersByUser($id);
if ($followers === false) { if ($followers === false) {
$followers = []; $followers = [];
} }
} }
// save followers to cache // save followers to cache
if ($cache !== null) { if ($cache !== null) {
$cache->saveRemoteFollowersOfUser($id, $followers); $cache->saveFollowersByUser($id, $followers);
} }
return $followers; return $followers;
} }
@ -91,12 +91,11 @@ class Followers
* optional caching service * optional caching service
* @return \Federator\Data\FedUser[] * @return \Federator\Data\FedUser[]
*/ */
public static function getFollowingByUser($dbh, $id, $connector, $cache)
public static function getFollowingForUser($dbh, $id, $connector, $cache)
{ {
// ask cache // ask cache
if ($cache !== null) { if ($cache !== null) {
$following = $cache->getRemoteFollowingForUser($id); $following = $cache->getFollowingByUser($id);
if ($following !== false) { if ($following !== false) {
return $following; return $following;
} }
@ -105,9 +104,9 @@ class Followers
$sql = 'select target_user from follows where source_user = ?'; $sql = 'select target_user from follows where source_user = ?';
$stmt = $dbh->prepare($sql); $stmt = $dbh->prepare($sql);
if ($stmt === false) { if ($stmt === false) {
throw new \Federator\Exceptions\ServerError("Followers::getFollowingForUser Failed to prepare statement"); throw new \Federator\Exceptions\ServerError('Followers::getFollowingForUser Failed to prepare statement');
} }
$stmt->bind_param("s", $id); $stmt->bind_param('s', $id);
$stmt->execute(); $stmt->execute();
$followingIds = []; $followingIds = [];
$stmt->bind_result($sourceUser); $stmt->bind_result($sourceUser);
@ -123,7 +122,7 @@ class Followers
$cache, $cache,
); );
} catch (\Throwable $e) { } catch (\Throwable $e) {
error_log("Followers::getFollowingForUser Exception: " . $e->getMessage()); error_log('Followers::getFollowingByUser Exception: ' . $e->getMessage());
continue; // Skip this user if an exception occurs continue; // Skip this user if an exception occurs
} }
if ($user !== false && $user->id !== null) { if ($user !== false && $user->id !== null) {
@ -133,14 +132,14 @@ class Followers
if ($following === []) { if ($following === []) {
// ask connector for user-id // ask connector for user-id
$following = $connector->getRemoteFollowingForUser($id); $following = $connector->getFollowingByUser($id);
if ($following === false) { if ($following === false) {
$following = []; $following = [];
} }
} }
// save posts to DB // save posts to DB
if ($cache !== null) { if ($cache !== null) {
$cache->saveRemoteFollowingForUser($id, $following); $cache->saveFollowingByUser($id, $following);
} }
return $following; return $following;
} }
@ -166,7 +165,7 @@ class Followers
$sql = 'select source_user from follows where target_user = ?'; $sql = 'select source_user from follows where target_user = ?';
$stmt = $dbh->prepare($sql); $stmt = $dbh->prepare($sql);
if ($stmt === false) { if ($stmt === false) {
throw new \Federator\Exceptions\ServerError("Followers::getFollowersByFedUser Failed to prepare statement"); throw new \Federator\Exceptions\ServerError('Followers::getFollowersByFedUser Failed to prepare statement');
} }
$stmt->bind_param("s", $id); $stmt->bind_param("s", $id);
$stmt->execute(); $stmt->execute();
@ -184,7 +183,7 @@ class Followers
$cache $cache
); );
} catch (\Throwable $e) { } catch (\Throwable $e) {
error_log("Followers::getFollowersByFedUser Exception: " . $e->getMessage()); error_log('Followers::getFollowersByFedUser Exception: ' . $e->getMessage());
continue; // Skip this user if an exception occurs continue; // Skip this user if an exception occurs
} }
if ($user !== false && $user->id !== null) { if ($user !== false && $user->id !== null) {
@ -208,7 +207,7 @@ class Followers
public static function sendFollowRequest($dbh, $connector, $cache, $_user, $_targetUser, $host) public static function sendFollowRequest($dbh, $connector, $cache, $_user, $_targetUser, $host)
{ {
if ($dbh === false) { if ($dbh === false) {
throw new \Federator\Exceptions\ServerError("Followers::sendFollowRequest Failed to get database handle"); throw new \Federator\Exceptions\ServerError('Followers::sendFollowRequest Failed to get database handle');
} }
$user = \Federator\DIO\User::getUserByName( $user = \Federator\DIO\User::getUserByName(
$dbh, $dbh,
@ -289,7 +288,8 @@ class Followers
// Build keyId (public key ID from your actor object) // Build keyId (public key ID from your actor object)
$keyId = 'https://' . $host . '/' . $user->id . '#main-key'; $keyId = 'https://' . $host . '/' . $user->id . '#main-key';
$signatureHeader = 'keyId="' . $keyId . '",algorithm="rsa-sha256",headers="(request-target) host date digest",signature="' . $signature_b64 . '"'; $signatureHeader = 'keyId="' . $keyId . '",algorithm="rsa-sha256",headers="(request-target) host date digest"'
. ',signature="' . $signature_b64 . '"';
$ch = curl_init($inboxUrl); $ch = curl_init($inboxUrl);
if ($ch === false) { if ($ch === false) {
@ -315,12 +315,12 @@ class Followers
// Log the response for debugging if needed // Log the response for debugging if needed
if ($response === false) { if ($response === false) {
self::removeFollow($dbh, $sourceUser, $fedUser->id); self::removeFollow($dbh, $sourceUser, $fedUser->id);
throw new \Exception("Failed to send Follow activity: " . curl_error($ch)); throw new \Exception('Failed to send Follow activity: ' . curl_error($ch));
} else { } else {
$httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($httpcode != 200 && $httpcode != 202) { if ($httpcode != 200 && $httpcode != 202) {
self::removeFollow($dbh, $sourceUser, $fedUser->id); self::removeFollow($dbh, $sourceUser, $fedUser->id);
throw new \Exception("Unexpected HTTP code $httpcode: $response"); throw new \Exception('Unexpected HTTP code ' . $httpcode . ':' . $response);
} }
} }
return $idUrl; return $idUrl;
@ -341,9 +341,9 @@ class Followers
$sql = 'select id from follows where source_user = ? and target_user = ?'; $sql = 'select id from follows where source_user = ? and target_user = ?';
$stmt = $dbh->prepare($sql); $stmt = $dbh->prepare($sql);
if ($stmt === false) { if ($stmt === false) {
throw new \Federator\Exceptions\ServerError("Followers::addFollow Failed to prepare statement"); throw new \Federator\Exceptions\ServerError('Followers::addFollow Failed to prepare statement');
} }
$stmt->bind_param("ss", $sourceUser, $targetUserId); $stmt->bind_param('ss', $sourceUser, $targetUserId);
$foundId = 0; $foundId = 0;
$ret = $stmt->bind_result($foundId); $ret = $stmt->bind_result($foundId);
$stmt->execute(); $stmt->execute();
@ -364,9 +364,10 @@ class Followers
$sql = 'select id from follows where id = ?'; $sql = 'select id from follows where id = ?';
$stmt = $dbh->prepare($sql); $stmt = $dbh->prepare($sql);
if ($stmt === false) { if ($stmt === false) {
throw new \Federator\Exceptions\ServerError("Followers::addFollow Failed to prepare id-check statement"); throw new \Federator\Exceptions\ServerError('Followers::addFollow Failed to prepare id-check'
. 'statement');
} }
$stmt->bind_param("s", $idurl); $stmt->bind_param('s', $idurl);
$foundId = 0; $foundId = 0;
$ret = $stmt->bind_result($foundId); $ret = $stmt->bind_result($foundId);
$stmt->execute(); $stmt->execute();
@ -380,9 +381,9 @@ class Followers
$sql = 'insert into follows (id, source_user, target_user, created_at) values (?, ?, ?, NOW())'; $sql = 'insert into follows (id, source_user, target_user, created_at) values (?, ?, ?, NOW())';
$stmt = $dbh->prepare($sql); $stmt = $dbh->prepare($sql);
if ($stmt === false) { if ($stmt === false) {
throw new \Federator\Exceptions\ServerError("Followers::addFollow Failed to prepare insert statement"); throw new \Federator\Exceptions\ServerError('Followers::addFollow Failed to prepare insert statement');
} }
$stmt->bind_param("sss", $idurl, $sourceUser, $targetUserId); $stmt->bind_param('sss', $idurl, $sourceUser, $targetUserId);
$stmt->execute(); $stmt->execute();
$stmt->close(); $stmt->close();
return $idurl; // Return the generated follow ID return $idurl; // Return the generated follow ID
@ -403,9 +404,9 @@ class Followers
$sql = 'select id from follows where source_user = ? and target_user = ?'; $sql = 'select id from follows where source_user = ? and target_user = ?';
$stmt = $dbh->prepare($sql); $stmt = $dbh->prepare($sql);
if ($stmt === false) { if ($stmt === false) {
throw new \Federator\Exceptions\ServerError("Followers::addExternalFollow Failed to prepare statement"); throw new \Federator\Exceptions\ServerError('Followers::addExternalFollow Failed to prepare statement');
} }
$stmt->bind_param("ss", $sourceUserId, $targetUserId); $stmt->bind_param('ss', $sourceUserId, $targetUserId);
$foundId = 0; $foundId = 0;
$ret = $stmt->bind_result($foundId); $ret = $stmt->bind_result($foundId);
$stmt->execute(); $stmt->execute();
@ -421,9 +422,10 @@ class Followers
$sql = 'insert into follows (id, source_user, target_user, created_at) values (?, ?, ?, NOW())'; $sql = 'insert into follows (id, source_user, target_user, created_at) values (?, ?, ?, NOW())';
$stmt = $dbh->prepare($sql); $stmt = $dbh->prepare($sql);
if ($stmt === false) { if ($stmt === false) {
throw new \Federator\Exceptions\ServerError("Followers::addExternalFollow Failed to prepare insert statement"); throw new \Federator\Exceptions\ServerError('Followers::addExternalFollow Failed to prepare insert '
. 'statement');
} }
$stmt->bind_param("sss", $followId, $sourceUserId, $targetUserId); $stmt->bind_param('sss', $followId, $sourceUserId, $targetUserId);
$stmt->execute(); $stmt->execute();
$stmt->close(); $stmt->close();
return true; return true;
@ -447,9 +449,10 @@ class Followers
$sql = 'select id from follows where id = ?'; $sql = 'select id from follows where id = ?';
$stmt = $dbh->prepare($sql); $stmt = $dbh->prepare($sql);
if ($stmt === false) { if ($stmt === false) {
throw new \Federator\Exceptions\ServerError("Followers::generateNewFollowId Failed to prepare id-check statement"); throw new \Federator\Exceptions\ServerError('Followers::generateNewFollowId Failed to prepare id-check'
. ' statement');
} }
$stmt->bind_param("s", $newIdUrl); $stmt->bind_param('s', $newIdUrl);
$foundId = 0; $foundId = 0;
$ret = $stmt->bind_result($foundId); $ret = $stmt->bind_result($foundId);
$stmt->execute(); $stmt->execute();
@ -476,7 +479,7 @@ class Followers
$sql = 'delete from follows where source_user = ? and target_user = ? RETURNING id'; $sql = 'delete from follows where source_user = ? and target_user = ? RETURNING id';
$stmt = $dbh->prepare($sql); $stmt = $dbh->prepare($sql);
if ($stmt !== false) { if ($stmt !== false) {
$stmt->bind_param("ss", $sourceUser, $targetUserId); $stmt->bind_param('ss', $sourceUser, $targetUserId);
if ($stmt->execute()) { if ($stmt->execute()) {
$stmt->bind_result($followId); $stmt->bind_result($followId);
if ($stmt->fetch() === true) { if ($stmt->fetch() === true) {
@ -495,9 +498,10 @@ class Followers
$sql = 'select id from follows where source_user = ? and target_user = ?'; $sql = 'select id from follows where source_user = ? and target_user = ?';
$stmt = $dbh->prepare($sql); $stmt = $dbh->prepare($sql);
if ($stmt === false) { if ($stmt === false) {
throw new \Federator\Exceptions\ServerError("Followers::removeFollow Failed to prepare select statement"); throw new \Federator\Exceptions\ServerError('Followers::removeFollow Failed to prepare select '
. 'statement');
} }
$stmt->bind_param("ss", $sourceUser, $targetUserId); $stmt->bind_param('ss', $sourceUser, $targetUserId);
$stmt->execute(); $stmt->execute();
$stmt->bind_result($followId); $stmt->bind_result($followId);
$found = $stmt->fetch(); $found = $stmt->fetch();
@ -511,9 +515,10 @@ class Followers
$sql = 'delete from follows where source_user = ? and target_user = ?'; $sql = 'delete from follows where source_user = ? and target_user = ?';
$stmt = $dbh->prepare($sql); $stmt = $dbh->prepare($sql);
if ($stmt === false) { if ($stmt === false) {
throw new \Federator\Exceptions\ServerError("Followers::removeFollow Failed to prepare delete statement"); throw new \Federator\Exceptions\ServerError('Followers::removeFollow Failed to prepare delete '
. 'statement');
} }
$stmt->bind_param("ss", $sourceUser, $targetUserId); $stmt->bind_param('ss', $sourceUser, $targetUserId);
$stmt->execute(); $stmt->execute();
$affectedRows = $stmt->affected_rows; $affectedRows = $stmt->affected_rows;
$stmt->close(); $stmt->close();

View file

@ -75,7 +75,7 @@ class Posts
} }
foreach ($newPosts as $newPost) { foreach ($newPosts as $newPost) {
if (!isset($existingIds[$newPost->getID()])) { if (!isset($existingIds[$newPost->getID()])) {
if ($newPost->getID() !== "") { if ($newPost->getID() !== '') {
self::savePost($dbh, $userid, $newPost); self::savePost($dbh, $userid, $newPost);
} }
if (sizeof($posts) < $limit) { if (sizeof($posts) < $limit) {
@ -94,12 +94,12 @@ class Posts
$parsed = parse_url($origin); $parsed = parse_url($origin);
if (isset($parsed) && isset($parsed['host'])) { if (isset($parsed) && isset($parsed['host'])) {
$parsedHost = $parsed['host']; $parsedHost = $parsed['host'];
if (is_string($parsedHost) && $parsedHost !== "") { if (is_string($parsedHost) && $parsedHost !== '') {
$originUrl = $parsedHost; $originUrl = $parsedHost;
} }
} }
} }
if (!isset($originUrl) || $originUrl === "") { if (!isset($originUrl) || $originUrl === '') {
$originUrl = 'localhost'; // Fallback to localhost if no origin is set $originUrl = 'localhost'; // Fallback to localhost if no origin is set
} }
@ -156,7 +156,8 @@ class Posts
*/ */
public static function getPostsFromDb($dbh, $userId, $min, $max, $limit = 20) public static function getPostsFromDb($dbh, $userId, $min, $max, $limit = 20)
{ {
$sql = 'SELECT `id`, `url`, `user_id`, `actor`, `type`, `object`, `to`, `cc`, unix_timestamp(`published`) as published FROM posts WHERE user_id = ?'; $sql = 'SELECT `id`, `url`, `user_id`, `actor`, `type`, `object`, `to`, `cc`, unix_timestamp(`published`) as '
. 'published FROM posts WHERE user_id = ?';
$params = [$userId]; $params = [$userId];
$types = 's'; $types = 's';
if ($min > 0) { if ($min > 0) {
@ -266,7 +267,7 @@ class Posts
$publishedStr = $published ? gmdate('Y-m-d H:i:s', $published) : gmdate('Y-m-d H:i:s'); $publishedStr = $published ? gmdate('Y-m-d H:i:s', $published) : gmdate('Y-m-d H:i:s');
$stmt->bind_param( $stmt->bind_param(
"ssssssssss", 'ssssssssss',
$id, $id,
$url, $url,
$userId, $userId,
@ -297,7 +298,7 @@ class Posts
if ($stmt === false) { if ($stmt === false) {
throw new \Federator\Exceptions\ServerError(); throw new \Federator\Exceptions\ServerError();
} }
$stmt->bind_param("s", $id); $stmt->bind_param('s', $id);
$stmt->execute(); $stmt->execute();
$affectedRows = $stmt->affected_rows; $affectedRows = $stmt->affected_rows;
$stmt->close(); $stmt->close();
@ -321,7 +322,7 @@ class Posts
$object = $post->getObject(); $object = $post->getObject();
if (is_object($object)) { if (is_object($object)) {
$inReplyTo = $object->getInReplyTo(); $inReplyTo = $object->getInReplyTo();
if ($inReplyTo !== "") { if ($inReplyTo !== '') {
$id = $inReplyTo; // Use inReplyTo as ID if it's a string $id = $inReplyTo; // Use inReplyTo as ID if it's a string
} else { } else {
$id = $object->getObject(); $id = $object->getObject();
@ -329,7 +330,7 @@ class Posts
} elseif (is_string($object)) { } elseif (is_string($object)) {
$id = $object; // If object is a string, use it directly $id = $object; // If object is a string, use it directly
} }
$stmt->bind_param("s", $id); $stmt->bind_param('s', $id);
$articleId = null; $articleId = null;
$ret = $stmt->bind_result($articleId); $ret = $stmt->bind_result($articleId);
$stmt->execute(); $stmt->execute();

View file

@ -0,0 +1,108 @@
<?php
/**
* SPDX-FileCopyrightText: 2025 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop
* SPDX-License-Identifier: GPL-3.0-or-later
*
* @author Sascha Nitsch (grumpyveveloper)
**/
namespace Federator\DIO;
/**
* Do the Server2Server communication
*/
class Server
{
/**
* send activity to federated server
*
* @param \mysqli $dbh database handle
* @param string $ourhost host url of our server (e.g. federator)
* @param \Federator\Data\User $sender source user
* @param \Federator\Data\FedUser $receiver federated user to receive the activity
* @param \Federator\Data\ActivityPub\Common\Activity $activity activity to send
* @return boolean true on success
*/
public static function sendActivity($dbh, $ourhost, $sender, $receiver, $activity)
{
$receiverInboxUrl = $receiver->inboxURL;
$json = json_encode($activity, JSON_UNESCAPED_SLASHES);
if ($json === false) {
throw new \Exception('Failed to encode JSON: ' . json_last_error_msg());
}
$digest = 'SHA-256=' . base64_encode(hash('sha256', $json, true));
$date = gmdate('D, d M Y H:i:s') . ' GMT';
echo "inboxurl $receiverInboxUrl\n";
$parsedReceiverInboxUrl = parse_url($receiverInboxUrl);
if ($parsedReceiverInboxUrl === false) {
throw new \Exception('Failed to parse URL: ' . $receiverInboxUrl);
}
if (!isset($parsedReceiverInboxUrl['host']) || !isset($parsedReceiverInboxUrl['path'])) {
throw new \Exception('Invalid inbox URL: missing host or path');
}
$extHost = $parsedReceiverInboxUrl['host'];
$path = $parsedReceiverInboxUrl['path'];
// Build the signature string
$signatureString = "(request-target): post {$path}\n" .
"host: {$extHost}\n" .
"date: {$date}\n" .
"digest: {$digest}";
// Get rsa private key
$privateKey = \Federator\DIO\User::getrsaprivate($dbh, $sender->id); // OR from DB
if ($privateKey === false) {
throw new \Exception('Failed to get private key');
}
$pkeyId = openssl_pkey_get_private($privateKey);
if ($pkeyId === false) {
throw new \Exception('Invalid private key');
}
echo "signaturestring $signatureString\n";
openssl_sign($signatureString, $signature, $pkeyId, OPENSSL_ALGO_SHA256);
$signature_b64 = base64_encode($signature);
// Build keyId (public key ID from your actor object)
$keyId = $ourhost . '/' . $sender->id . '#main-key';
$signatureHeader = 'keyId="' . $keyId
. '",algorithm="rsa-sha256",headers="(request-target) host date digest",signature="' . $signature_b64 . '"';
$ch = curl_init($receiverInboxUrl);
if ($ch === false) {
throw new \Exception('Failed to initialize cURL');
}
$headers = [
'Host: ' . $extHost,
'Date: ' . $date,
'Digest: ' . $digest,
'Content-Type: application/activity+json',
'Signature: ' . $signatureHeader,
'Accept: application/activity+json',
];
print_r($headers);
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);
curl_close($ch);
if ($response === false) {
throw new \Exception('Failed to send activity: ' . curl_error($ch));
} else {
$httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($httpcode != 200 && $httpcode != 202) {
throw new \Exception('Unexpected HTTP code ' . $httpcode . ':' . $response);
}
}
if ($response !== true) {
error_log($response);
}
return true;
}
}

View file

@ -17,7 +17,8 @@ class Stats
/** /**
* get remote stats * get remote stats
* *
* @param \Federator\Main $main main instance * @param \Federator\Main $main
* main instance
* @return \Federator\Data\Stats * @return \Federator\Data\Stats
*/ */
public static function getStats($main) public static function getStats($main)

View file

@ -26,9 +26,9 @@ class User
$sql = 'select unix_timestamp(`validuntil`) from users where id=?'; $sql = 'select unix_timestamp(`validuntil`) from users where id=?';
$stmt = $dbh->prepare($sql); $stmt = $dbh->prepare($sql);
if ($stmt === false) { if ($stmt === false) {
throw new \Federator\Exceptions\ServerError("User::addLocalUser Failed to prepare statement"); throw new \Federator\Exceptions\ServerError('User::addLocalUser Failed to prepare statement');
} }
$stmt->bind_param("s", $_user); $stmt->bind_param('s', $_user);
$validuntil = 0; $validuntil = 0;
$ret = $stmt->bind_result($validuntil); $ret = $stmt->bind_result($validuntil);
$stmt->execute(); $stmt->execute();
@ -50,11 +50,11 @@ class User
$sql .= ' values (?, ?, ?, ?, now() + interval 1 day, ?, ?, ?, ?, ?, ?, ?, ?)'; $sql .= ' values (?, ?, ?, ?, now() + interval 1 day, ?, ?, ?, ?, ?, ?, ?, ?)';
$stmt = $dbh->prepare($sql); $stmt = $dbh->prepare($sql);
if ($stmt === false) { if ($stmt === false) {
throw new \Federator\Exceptions\ServerError("User::addLocalUser Failed to prepare create statement"); throw new \Federator\Exceptions\ServerError('User::addLocalUser Failed to prepare create statement');
} }
$registered = gmdate('Y-m-d H:i:s', $user->registered); $registered = gmdate('Y-m-d H:i:s', $user->registered);
$stmt->bind_param( $stmt->bind_param(
"ssssssssssss", 'ssssssssssss',
$_user, $_user,
$user->externalid, $user->externalid,
$public, $public,
@ -74,11 +74,11 @@ class User
$sql .= ' iconmediatype=?, iconurl=?, imagemediatype=?, imageurl=? where id=?'; $sql .= ' iconmediatype=?, iconurl=?, imagemediatype=?, imageurl=? where id=?';
$stmt = $dbh->prepare($sql); $stmt = $dbh->prepare($sql);
if ($stmt === false) { if ($stmt === false) {
throw new \Federator\Exceptions\ServerError("User::addLocalUser Failed to prepare update statement"); throw new \Federator\Exceptions\ServerError('User::addLocalUser Failed to prepare update statement');
} }
$registered = gmdate('Y-m-d H:i:s', $user->registered); $registered = gmdate('Y-m-d H:i:s', $user->registered);
$stmt->bind_param( $stmt->bind_param(
"sssssssss", 'sssssssss',
$user->type, $user->type,
$user->name, $user->name,
$user->summary, $user->summary,
@ -107,12 +107,12 @@ class User
*/ */
public static function getrsaprivate(\mysqli $dbh, string $_user) public static function getrsaprivate(\mysqli $dbh, string $_user)
{ {
$sql = "select rsaprivate from users where id=?"; $sql = 'select rsaprivate from users where id=?';
$stmt = $dbh->prepare($sql); $stmt = $dbh->prepare($sql);
if ($stmt === false) { if ($stmt === false) {
throw new \Federator\Exceptions\ServerError("User::getrsaprivate Failed to prepare statement"); throw new \Federator\Exceptions\ServerError('User::getrsaprivate Failed to prepare statement');
} }
$stmt->bind_param("s", $_user); $stmt->bind_param('s', $_user);
$ret = $stmt->bind_result($rsaPrivateKey); $ret = $stmt->bind_result($rsaPrivateKey);
$stmt->execute(); $stmt->execute();
if ($ret) { if ($ret) {
@ -136,7 +136,7 @@ class User
$sql = 'select id,unix_timestamp(`validuntil`) from users where id=?'; $sql = 'select id,unix_timestamp(`validuntil`) from users where id=?';
$stmt = $dbh->prepare($sql); $stmt = $dbh->prepare($sql);
if ($stmt === false) { if ($stmt === false) {
throw new \Federator\Exceptions\ServerError("User::extendUser Failed to prepare statement"); throw new \Federator\Exceptions\ServerError('User::extendUser Failed to prepare statement');
} }
$stmt->bind_param("s", $_user); $stmt->bind_param("s", $_user);
$validuntil = 0; $validuntil = 0;
@ -170,7 +170,6 @@ class User
public static function getUserByName($dbh, $_name, $connector, $cache) public static function getUserByName($dbh, $_name, $connector, $cache)
{ {
$user = false; $user = false;
// ask cache // ask cache
if ($cache !== null) { if ($cache !== null) {
$user = $cache->getRemoteUserByName($_name); $user = $cache->getRemoteUserByName($_name);
@ -183,9 +182,9 @@ class User
$sql .= 'iconmediatype,iconurl,imagemediatype,imageurl from users where id=? and validuntil>=now()'; $sql .= 'iconmediatype,iconurl,imagemediatype,imageurl from users where id=? and validuntil>=now()';
$stmt = $dbh->prepare($sql); $stmt = $dbh->prepare($sql);
if ($stmt === false) { if ($stmt === false) {
throw new \Federator\Exceptions\ServerError("User::getUserByName Failed to prepare statement"); throw new \Federator\Exceptions\ServerError('User::getUserByName Failed to prepare statement');
} }
$stmt->bind_param("s", $_name); $stmt->bind_param('s', $_name);
$user = new \Federator\Data\User(); $user = new \Federator\Data\User();
$ret = $stmt->bind_result( $ret = $stmt->bind_result(
$user->id, $user->id,
@ -205,18 +204,15 @@ class User
$stmt->fetch(); $stmt->fetch();
} }
$stmt->close(); $stmt->close();
if ($user->id === null) { if ($user->id === null) {
// ask connector for user-id // ask connector for user-id
$ruser = $connector->getRemoteUserByName($_name); $ruser = $connector->getRemoteUserByName($_name);
if ($ruser !== false) { if ($ruser !== false) {
$user = $ruser; $user = $ruser;
self::addLocalUser($dbh, $user, $_name);
} }
} }
if ($cache !== null) { if ($cache !== null) {
if ($user->id === null && $user->externalid !== null) {
self::addLocalUser($dbh, $user, $_name);
}
$cache->saveRemoteUserByName($_name, $user); $cache->saveRemoteUserByName($_name, $user);
} }
return $user; return $user;

View file

@ -30,7 +30,7 @@ class Votes
if ($stmt === false) { if ($stmt === false) {
throw new \Federator\Exceptions\ServerError(); throw new \Federator\Exceptions\ServerError();
} }
$stmt->bind_param("sss", $userId, $targetId, $type); $stmt->bind_param('sss', $userId, $targetId, $type);
$foundId = 0; $foundId = 0;
$ret = $stmt->bind_result($foundId); $ret = $stmt->bind_result($foundId);
$stmt->execute(); $stmt->execute();
@ -51,7 +51,7 @@ class Votes
if ($stmt === false) { if ($stmt === false) {
throw new \Federator\Exceptions\ServerError(); throw new \Federator\Exceptions\ServerError();
} }
$stmt->bind_param("s", $id); $stmt->bind_param('s', $id);
$foundId = 0; $foundId = 0;
$ret = $stmt->bind_result($foundId); $ret = $stmt->bind_result($foundId);
$stmt->execute(); $stmt->execute();
@ -67,7 +67,7 @@ class Votes
if ($stmt === false) { if ($stmt === false) {
throw new \Federator\Exceptions\ServerError(); throw new \Federator\Exceptions\ServerError();
} }
$stmt->bind_param("ssss", $id, $userId, $targetId, $type); $stmt->bind_param('ssss', $id, $userId, $targetId, $type);
$stmt->execute(); $stmt->execute();
$stmt->close(); $stmt->close();
return $id; // Return the generated vote ID return $id; // Return the generated vote ID
@ -88,7 +88,7 @@ class Votes
if ($stmt === false) { if ($stmt === false) {
throw new \Federator\Exceptions\ServerError(); throw new \Federator\Exceptions\ServerError();
} }
$stmt->bind_param("ss", $userId, $targetId); $stmt->bind_param('ss', $userId, $targetId);
$stmt->execute(); $stmt->execute();
$affectedRows = $stmt->affected_rows; $affectedRows = $stmt->affected_rows;
$stmt->close(); $stmt->close();

View file

@ -52,7 +52,7 @@ class InboxJob extends \Federator\Api
*/ */
public function perform(): bool public function perform(): bool
{ {
error_log("InboxJob: Starting job"); error_log('InboxJob: Starting job');
$user = $this->args['user']; $user = $this->args['user'];
$recipientId = $this->args['recipientId']; $recipientId = $this->args['recipientId'];
$activity = $this->args['activity']; $activity = $this->args['activity'];
@ -60,11 +60,19 @@ class InboxJob extends \Federator\Api
$inboxActivity = \Federator\Data\ActivityPub\Factory::newActivityFromJson($activity); $inboxActivity = \Federator\Data\ActivityPub\Factory::newActivityFromJson($activity);
if ($inboxActivity === false) { if ($inboxActivity === false) {
error_log("InboxJob: Failed to create inboxActivity from JSON"); error_log('InboxJob: Failed to create inboxActivity from JSON');
return false; return false;
} }
\Federator\Api\FedUsers\Inbox::postForUser($this->dbh, $this->connector, $this->cache, $user, $recipientId, $inboxActivity); \Federator\DIO\FedUser::inboxForUser(
$this,
$this->dbh,
$this->connector,
$this->cache,
$user,
$recipientId,
$inboxActivity
);
return true; return true;
} }
} }

View file

@ -52,7 +52,7 @@ class NewContentJob extends \Federator\Api
*/ */
public function perform(): bool public function perform(): bool
{ {
error_log("NewContentJob: Starting job"); error_log('NewContentJob: Starting job');
$user = $this->args['user']; $user = $this->args['user'];
$recipientId = $this->args['recipientId']; $recipientId = $this->args['recipientId'];
$activity = $this->args['activity']; $activity = $this->args['activity'];
@ -61,14 +61,23 @@ class NewContentJob extends \Federator\Api
$activity = \Federator\Data\ActivityPub\Factory::newActivityFromJson($activity); $activity = \Federator\Data\ActivityPub\Factory::newActivityFromJson($activity);
if ($activity === false) { if ($activity === false) {
error_log("NewContentJob: Failed to create activity from JSON"); error_log('NewContentJob: Failed to create activity from JSON');
return false; return false;
} }
$domain = $this->config['generic']['externaldomain']; $domain = $this->config['generic']['externaldomain'];
$ourUrl = 'https://' . $domain; $ourUrl = 'https://' . $domain;
\Federator\Api\V1\NewContent::postForUser($this->dbh, $this->connector, $this->cache, $ourUrl, $user, $recipientId, $activity, $articleId); \Federator\Api\V1\NewContent::postForUser(
$this->dbh,
$this->connector,
$this->cache,
$ourUrl,
$user,
$recipientId,
$activity,
$articleId
);
return true; return true;
} }
} }

View file

@ -20,9 +20,9 @@ class Language
* @var array $validLanguages * @var array $validLanguages
*/ */
private $validLanguages = array( private $validLanguages = array(
"de" => true, 'de' => true,
"en" => true, 'en' => true,
"xy" => true 'xy' => true
); );
/** /**
@ -92,9 +92,12 @@ class Language
} }
if (! isset($this->lang[$group])) { if (! isset($this->lang[$group])) {
$l = []; $l = [];
$root = PROJECT_ROOT; $root = $_SERVER['DOCUMENT_ROOT'];
if (@file_exists($root . '/lang/federator/' . $this->uselang . "/$group.inc")) { if ($root === '') {
require($root . '/lang/federator/' . $this->uselang . "/$group.inc"); $root = '.';
}
if (@file_exists($root . '../lang/federator/' . $this->uselang . '/' . $group . '.inc')) {
require($root . '../lang/federator/' . $this->uselang . '/' . $group . '.inc');
$this->lang[$group] = $l; $this->lang[$group] = $l;
} }
} }
@ -104,15 +107,15 @@ class Language
if (isset($values[$i])) { if (isset($values[$i])) {
$string = str_replace("\$$i", $values[$i], $string); $string = str_replace("\$$i", $values[$i], $string);
} else { } else {
$string = str_replace("\$$i", "", $string); $string = str_replace("\$$i", '', $string);
} }
} }
return $string; return $string;
} }
$basedir = PROJECT_ROOT; $basedir = $_SERVER['DOCUMENT_ROOT'] . '/../';
$fh = @fopen("$basedir/logs/missingtrans.txt", 'a'); $fh = @fopen($basedir . '/logs/missingtrans.txt', 'a');
if ($fh !== false) { if ($fh !== false) {
fwrite($fh, $this->uselang.":$group:$key\n"); fwrite($fh, $this->uselang . ':' . $group . ':' . "$key\n");
fclose($fh); fclose($fh);
} }
return "&gt;&gt;$group:$key&lt;&lt;"; return "&gt;&gt;$group:$key&lt;&lt;";
@ -129,7 +132,7 @@ class Language
{ {
if (! isset($this->lang[$group])) { if (! isset($this->lang[$group])) {
$l = []; $l = [];
require_once(PROJECT_ROOT . '/lang/' . $this->uselang . "/$group.inc"); require_once($_SERVER['DOCUMENT_ROOT'] . '/../lang/' . $this->uselang . '/' . $group . '.inc');
$this->lang[$group] = $l; $this->lang[$group] = $l;
} }
// @phan-suppress-next-line PhanPartialTypeMismatchReturn // @phan-suppress-next-line PhanPartialTypeMismatchReturn
@ -285,7 +288,7 @@ function smarty_function_printlang($params, $template) : string
*/ */
function smarty_function_printjslang($params, $template) : string function smarty_function_printjslang($params, $template) : string
{ {
$lang = $template->getTemplateVars("language"); $lang = $template->getTemplateVars('language');
$prefix = 'window.translations.' . $params['group'] . '.' . $params['key'] . ' = \''; $prefix = 'window.translations.' . $params['group'] . '.' . $params['key'] . ' = \'';
$postfix = '\';'; $postfix = '\';';
if (isset($params['var'])) { if (isset($params['var'])) {

View file

@ -20,37 +20,43 @@ class Main
* *
* @var Cache\Cache $cache * @var Cache\Cache $cache
*/ */
protected $cache; protected $cache = null;
/** /**
* current config * current config
* *
* @var array<string,mixed> $config * @var array<string,mixed> $config
*/ */
protected $config; protected $config;
/** /**
* remote connector * remote connector
* *
* @var Connector\Connector $connector * @var Connector\Connector $connector
*/ */
protected $connector = null; protected $connector = null;
/** /**
* remote host (f.e. https://contentnation.net) * remote host (f.e. https://contentnation.net)
* *
* @var string $host * @var string $host
*/ */
protected $host = null; protected $host = null;
/** /**
* response content type * response content type
* *
* @var string $contentType * @var string $contentType
*/ */
protected $contentType = "text/html"; protected $contentType = 'text/html';
/** /**
* database instance * database instance
* *
* @var \Mysqli $dbh * @var \Mysqli $dbh
*/ */
protected $dbh; protected $dbh;
/** /**
* extra headers * extra headers
* *
@ -78,9 +84,9 @@ class Main
*/ */
public function __construct() public function __construct()
{ {
require_once(PROJECT_ROOT . '/vendor/autoload.php'); require_once($_SERVER['DOCUMENT_ROOT'] . '../vendor/autoload.php');
$this->responseCode = 200; $this->responseCode = 200;
$rootDir = PROJECT_ROOT . '/'; $rootDir = $_SERVER['DOCUMENT_ROOT'] . '../';
$config = parse_ini_file($rootDir . 'config.ini', true); $config = parse_ini_file($rootDir . 'config.ini', true);
if ($config !== false) { if ($config !== false) {
$this->config = $config; $this->config = $config;
@ -145,6 +151,7 @@ class Main
{ {
return $this->cache; return $this->cache;
} }
/** /**
* get connector * get connector
* *
@ -154,6 +161,7 @@ class Main
{ {
return $this->connector; return $this->connector;
} }
/** /**
* get host (f.e. https://contentnation.net) * get host (f.e. https://contentnation.net)
* *
@ -189,7 +197,7 @@ class Main
public function loadPlugins(): void public function loadPlugins(): void
{ {
if (array_key_exists('plugins', $this->config)) { if (array_key_exists('plugins', $this->config)) {
$basepath = PROJECT_ROOT . '/plugins/federator/'; $basepath = $_SERVER['DOCUMENT_ROOT'] . '../plugins/federator/';
$plugins = $this->config['plugins']; $plugins = $this->config['plugins'];
foreach ($plugins as $name => $file) { foreach ($plugins as $name => $file) {
require_once($basepath . $file); require_once($basepath . $file);
@ -210,7 +218,7 @@ class Main
*/ */
public function openDatabase($usernameOverride = null, $passwordOverride = null) public function openDatabase($usernameOverride = null, $passwordOverride = null)
{ {
$dbconf = $this->config["database"]; $dbconf = $this->config['database'];
$this->dbh = new \mysqli( $this->dbh = new \mysqli(
$dbconf['host'], $dbconf['host'],
$usernameOverride ?? (string) $dbconf['username'], $usernameOverride ?? (string) $dbconf['username'],
@ -232,10 +240,10 @@ class Main
*/ */
public function renderTemplate($template, $data) public function renderTemplate($template, $data)
{ {
$rootDir = PROJECT_ROOT . '/';
$smarty = new \Smarty\Smarty(); $smarty = new \Smarty\Smarty();
$smarty->setCompileDir($rootDir . $this->config['templates']['compiledir']); $root = $_SERVER['DOCUMENT_ROOT'];
$smarty->setTemplateDir((string) realpath($rootDir . $this->config['templates']['path'])); $smarty->setCompileDir($root . $this->config['templates']['compiledir']);
$smarty->setTemplateDir((string)realpath($root . $this->config['templates']['path']));
$smarty->assign('database', $this->dbh); $smarty->assign('database', $this->dbh);
$smarty->assign('maininstance', $this); $smarty->assign('maininstance', $this);
foreach ($data as $key => $value) { foreach ($data as $key => $value) {
@ -261,9 +269,6 @@ class Main
*/ */
public function setConnector(Connector\Connector $connector) : void public function setConnector(Connector\Connector $connector) : void
{ {
if (isset($this->connector)) {
# echo "main::setConnector Setting new connector will override old one.\n"; // TODO CHANGE TO LOG WARNING
}
$this->connector = $connector; $this->connector = $connector;
} }
@ -274,9 +279,6 @@ class Main
*/ */
public function setHost(string $host) : void public function setHost(string $host) : void
{ {
if (isset($this->host)) {
# echo "main::setHost Setting new host will override old one.\n"; // TODO CHANGE TO LOG WARNING
}
$this->host = $host; $this->host = $host;
} }

View file

@ -22,15 +22,16 @@ class Maintenance
*/ */
public static function run($argc, $argv) public static function run($argc, $argv)
{ {
date_default_timezone_set("Europe/Berlin"); date_default_timezone_set('Europe/Berlin');
spl_autoload_register(static function (string $className) { spl_autoload_register(static function (string $className) {
include PROJECT_ROOT . '/php/' . str_replace("\\", "/", strtolower($className)) . '.php'; $root = $_SERVER['DOCUMENT_ROOT'];
include $root . '../php/' . str_replace("\\", "/", strtolower($className)) . '.php';
}); });
if ($argc < 2) { if ($argc < 2) {
self::printUsage(); self::printUsage();
} }
// pretend that we are running from web directory // pretend that we are running from web directory
define('PROJECT_ROOT', dirname(__DIR__, 2)); $_SERVER['DOCUMENT_ROOT'] = realpath('../../htdocs') . '/';
$main = new \Federator\Main(); $main = new \Federator\Main();
switch ($argv[1]) { switch ($argv[1]) {
case 'dbupgrade': case 'dbupgrade':
@ -70,7 +71,7 @@ class Maintenance
} }
} }
echo "current version: $version\n"; echo "current version: $version\n";
$root = PROJECT_ROOT . '/'; $root = $_SERVER['DOCUMENT_ROOT'] . '../';
$updateFolder = opendir($root . 'sql'); $updateFolder = opendir($root . 'sql');
if ($updateFolder === false) { if ($updateFolder === false) {
die(); die();

View file

@ -81,7 +81,7 @@ class Test
*/ */
public static function run($argc, $argv) public static function run($argc, $argv)
{ {
date_default_timezone_set("Europe/Berlin"); date_default_timezone_set('Europe/Berlin');
spl_autoload_register(static function (string $className) { spl_autoload_register(static function (string $className) {
include PROJECT_ROOT . '/php/' . str_replace("\\", "/", strtolower($className)) . '.php'; include PROJECT_ROOT . '/php/' . str_replace("\\", "/", strtolower($className)) . '.php';
}); });
@ -143,7 +143,8 @@ class Test
$inboxActivity->setAActor('https://mastodon.local/users/admin'); $inboxActivity->setAActor('https://mastodon.local/users/admin');
$inboxActivity->setObject($_url); $inboxActivity->setObject($_url);
$inboxActivity->setID("https://mastodon.local/users/admin#like/" . md5($_url)); $inboxActivity->setID("https://mastodon.local/users/admin#like/" . md5($_url));
\Federator\Api\FedUsers\Inbox::postForUser( \Federator\DIO\FedUser::inboxForUser(
$api,
$dbh, $dbh,
$api->getConnector(), $api->getConnector(),
null, null,

View file

@ -1,14 +1,18 @@
<?php <?php
define('PROJECT_ROOT', dirname(__DIR__, 3)); define('PROJECT_ROOT', dirname(__DIR__, 3));
require_once PROJECT_ROOT . '/vendor/autoload.php'; require_once PROJECT_ROOT . '/vendor/autoload.php';
$_SERVER['DOCUMENT_ROOT'] = PROJECT_ROOT . '/htdocs/';
spl_autoload_register(static function (string $className) {
include PROJECT_ROOT . '/php/' . str_replace("\\", "/", strtolower($className)) . '.php';
});
$config = parse_ini_file(PROJECT_ROOT . '/rediscache.ini'); $config = parse_ini_file(PROJECT_ROOT . '/rediscache.ini');
// Set the Redis backend for Resque // Set the Redis backend for Resque
$redisUrl = sprintf( $redisUrl = sprintf(
'redis://%s:%s@%s:%d', 'redis://%s:%s@%s:%d?password-encoding=u',
urlencode($config['username']), urlencode($config['username']),
urlencode($config['password']), urlencode($config['password']),
$config['host'], $config['host'],

View file

@ -41,7 +41,7 @@ class ContentNation implements Connector
*/ */
public function __construct($main) public function __construct($main)
{ {
$config = parse_ini_file(PROJECT_ROOT . '/contentnation.ini', true); $config = parse_ini_file($_SERVER['DOCUMENT_ROOT'] . '../contentnation.ini', true);
if ($config !== false) { if ($config !== false) {
$this->config = $config; $this->config = $config;
} }
@ -53,26 +53,28 @@ class ContentNation implements Connector
/** /**
* get followers of given user * get followers of given user
* *
* @param string $userId user id * @param string $userId user id @unused-param
* @return \Federator\Data\FedUser[]|false * @return \Federator\Data\FedUser[]|false
*/ */
public function getRemoteFollowersOfUser($userId) public function getFollowersByUser($userId)
{ {
// todo implement queue for this // ContentNation does not export followers
/*
if (preg_match("#^([^@]+)@([^/]+)#", $userId, $matches) == 1) { if (preg_match("#^([^@]+)@([^/]+)#", $userId, $matches) == 1) {
$userId = $matches[1]; $userId = $matches[1];
} }
$remoteURL = $this->service . '/api/profile/' . urlencode($userId) . '/followers'; $remoteURL = $this->service . '/api/profile/' . urlencode($userId);# . '/followers';
[$response, $info] = \Federator\Main::getFromRemote($remoteURL, []); [$response, $info] = \Federator\Main::getFromRemote($remoteURL, []);
if ($info['http_code'] != 200) { if ($info['http_code'] != 200) {
error_log("ContentNation::getRemoteFollowersOfUser error retrieving followers for userId: $userId . Error: " . json_encode($info)); error_log("ContentNation::getFollowersByUser error retrieving followers for userId: $userId . Error: "
. json_encode($info));
return false; return false;
} }
$r = json_decode($response, true); $r = json_decode($response, true);
if ($r === false || $r === null || !is_array($r)) { if ($r === false || $r === null || !is_array($r)) {
return false; return false;
} }*/
$followers = []; $followers = [];
return $followers; return $followers;
} }
@ -80,13 +82,13 @@ class ContentNation implements Connector
/** /**
* get following of given user * get following of given user
* *
* @param string $userId user id * @param string $userId user id @unused-param
* @return \Federator\Data\FedUser[]|false * @return \Federator\Data\FedUser[]|false
*/ */
public function getRemoteFollowingForUser($userId) public function getFollowingByUser($userId)
{ {
// todo implement queue for this // ContentNation does not export Following for user
/*
if (preg_match("#^([^@]+)@([^/]+)#", $userId, $matches) == 1) { if (preg_match("#^([^@]+)@([^/]+)#", $userId, $matches) == 1) {
$userId = $matches[1]; $userId = $matches[1];
} }
@ -94,15 +96,16 @@ class ContentNation implements Connector
[$response, $info] = \Federator\Main::getFromRemote($remoteURL, []); [$response, $info] = \Federator\Main::getFromRemote($remoteURL, []);
if ($info['http_code'] != 200) { if ($info['http_code'] != 200) {
error_log("ContentNation::getRemoteFollowingForUser error retrieving following for userId: $userId . Error: " . json_encode($info)); error_log('ContentNation::getRemoteFollowingForUser error retrieving following for userId: ' . $userId
. '. Error: ' . json_encode($info));
return false; return false;
} }
$r = json_decode($response, true); $r = json_decode($response, true);
if ($r === false || $r === null || !is_array($r)) { if ($r === false || $r === null || !is_array($r)) {
return false; return false;
} }*/
$followers = []; $following = [];
return $followers; return $following;
} }
/** /**
@ -129,7 +132,8 @@ class ContentNation implements Connector
} }
[$response, $info] = \Federator\Main::getFromRemote($remoteURL, []); [$response, $info] = \Federator\Main::getFromRemote($remoteURL, []);
if ($info['http_code'] != 200) { if ($info['http_code'] != 200) {
error_log("ContentNation::getRemotePostsByUser error retrieving activities for userId: $userId . Error: " . json_encode($info)); error_log('ContentNation::getRemotePostsByUser error retrieving activities for userId: ' . $userId
. '. Error: ' . json_encode($info));
return false; return false;
} }
$r = json_decode($response, true); $r = json_decode($response, true);
@ -211,7 +215,8 @@ class ContentNation implements Connector
$commentJson = $activity; $commentJson = $activity;
$commentJson['type'] = 'Note'; $commentJson['type'] = 'Note';
$commentJson['summary'] = $activity['subject']; $commentJson['summary'] = $activity['subject'];
$commentJson['id'] = $ourUrl . '/' . $activity['articleOwnerName'] . '/' . $activity['articleName'] . '#' . $activity['id']; $commentJson['id'] = $ourUrl . '/' . $activity['articleOwnerName'] . '/'
. $activity['articleName'] . '#' . $activity['id'];
$note = \Federator\Data\ActivityPub\Factory::newFromJson($commentJson, ""); $note = \Federator\Data\ActivityPub\Factory::newFromJson($commentJson, "");
if ($note === null) { if ($note === null) {
error_log("ContentNation::getRemotePostsByUser couldn't create comment"); error_log("ContentNation::getRemotePostsByUser couldn't create comment");
@ -221,11 +226,14 @@ class ContentNation implements Connector
} }
$note->setID($commentJson['id']); $note->setID($commentJson['id']);
if (!isset($commentJson['parent']) || $commentJson['parent'] === null) { if (!isset($commentJson['parent']) || $commentJson['parent'] === null) {
$note->setInReplyTo($ourUrl . '/' . $activity['articleOwnerName'] . '/' . $activity['articleName']); $note->setInReplyTo($ourUrl . '/' . $activity['articleOwnerName'] . '/'
. $activity['articleName']);
} else { } else {
$note->setInReplyTo($ourUrl . '/' . $activity['articleOwnerName'] . '/' . $activity['articleName'] . "#" . $commentJson['parent']); $note->setInReplyTo($ourUrl . '/' . $activity['articleOwnerName'] . '/'
. $activity['articleName'] . "#" . $commentJson['parent']);
} }
$url = $ourUrl . '/' . $activity['articleOwnerName'] . '/' . $activity['articleName'] . '#' . $activity['id']; $url = $ourUrl . '/' . $activity['articleOwnerName'] . '/' . $activity['articleName']
. '#' . $activity['id'];
$create->setURL($url); $create->setURL($url);
$create->setID($url); $create->setID($url);
$create->setObject($note); $create->setObject($note);
@ -293,16 +301,10 @@ class ContentNation implements Connector
public function getRemoteUserByName(string $_name) public function getRemoteUserByName(string $_name)
{ {
// validate name // validate name
if (preg_match("/^[a-zA-Z@0-9\._\-]+$/", $_name) != 1) { if (preg_match("/^[a-zA-Z0-9\._\-]+$/", $_name) != 1) {
return false; return false;
} }
// make sure we only get name part, without domain $remoteURL = $this->service . '/api/users/info?user=' . urlencode($_name);
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']; $headers = ['Accept: application/json'];
[$response, $info] = \Federator\Main::getFromRemote($remoteURL, $headers); [$response, $info] = \Federator\Main::getFromRemote($remoteURL, $headers);
if ($info['http_code'] != 200) { if ($info['http_code'] != 200) {
@ -339,7 +341,7 @@ class ContentNation implements Connector
if (preg_match("/^[a-z0-9]{16}$/", $_session) != 1) { if (preg_match("/^[a-z0-9]{16}$/", $_session) != 1) {
return false; return false;
} }
if (preg_match("/^[a-zA-Z@0-9\._\-]+$/", $_user) != 1) { if (preg_match("/^[a-zA-Z0-9_\-]+$/", $_user) != 1) {
return false; return false;
} }
$remoteURL = $this->service . '/api/users/permissions?profile=' . urlencode($_user); $remoteURL = $this->service . '/api/users/permissions?profile=' . urlencode($_user);
@ -428,7 +430,8 @@ class ContentNation implements Connector
} elseif ($jsonData['object']['vote']['value'] == 0) { } elseif ($jsonData['object']['vote']['value'] == 0) {
$ap['object']['type'] = 'Dislike'; $ap['object']['type'] = 'Dislike';
} else { } else {
error_log("ContentNation::jsonToActivity unknown vote value: {$jsonData['object']['vote']['value']}"); error_log('ContentNation::jsonToActivity unknown vote value: '
. $jsonData['object']['vote']['value']);
break; break;
} }
$ap['object']['object'] = self::generateObjectJson($ourUrl, $jsonData); $ap['object']['object'] = self::generateObjectJson($ourUrl, $jsonData);
@ -438,7 +441,7 @@ class ContentNation implements Connector
} }
$returnActivity = \Federator\Data\ActivityPub\Factory::newActivityFromJson($ap); $returnActivity = \Federator\Data\ActivityPub\Factory::newActivityFromJson($ap);
if ($returnActivity === false) { if ($returnActivity === false) {
error_log("ContentNation::jsonToActivity couldn't create undo"); error_log('ContentNation::jsonToActivity couldn\'t create undo');
$returnActivity = new \Federator\Data\ActivityPub\Common\Undo(); $returnActivity = new \Federator\Data\ActivityPub\Common\Undo();
} else { } else {
$returnActivity->setID($ap['id']); $returnActivity->setID($ap['id']);
@ -447,7 +450,8 @@ class ContentNation implements Connector
break; break;
default: default:
// Handle unsupported types or fallback to default behavior // Handle unsupported types or fallback to default behavior
throw new \InvalidArgumentException("ContentNation::jsonToActivity Unsupported type: {$jsonData['type']}"); throw new \InvalidArgumentException('ContentNation::jsonToActivity Unsupported type: '
. $jsonData['type']);
} }
} else { } else {
// Handle specific fields based on the type // Handle specific fields based on the type
@ -480,7 +484,7 @@ class ContentNation implements Connector
$ap['object'] = self::generateObjectJson($ourUrl, $jsonData); $ap['object'] = self::generateObjectJson($ourUrl, $jsonData);
$returnActivity = \Federator\Data\ActivityPub\Factory::newActivityFromJson($ap); $returnActivity = \Federator\Data\ActivityPub\Factory::newActivityFromJson($ap);
if ($returnActivity === false) { if ($returnActivity === false) {
error_log("ContentNation::jsonToActivity couldn't create article"); error_log('ContentNation::jsonToActivity couldn\'t create article');
$returnActivity = new \Federator\Data\ActivityPub\Common\Activity('Create'); $returnActivity = new \Federator\Data\ActivityPub\Common\Activity('Create');
} else { } else {
$returnActivity->setID($ap['id']); $returnActivity->setID($ap['id']);
@ -513,7 +517,7 @@ class ContentNation implements Connector
$ap['object'] = self::generateObjectJson($ourUrl, $jsonData); $ap['object'] = self::generateObjectJson($ourUrl, $jsonData);
$returnActivity = \Federator\Data\ActivityPub\Factory::newActivityFromJson($ap); $returnActivity = \Federator\Data\ActivityPub\Factory::newActivityFromJson($ap);
if ($returnActivity === false) { if ($returnActivity === false) {
error_log("ContentNation::jsonToActivity couldn't create comment"); error_log('ContentNation::jsonToActivity couldn\'t create comment');
$returnActivity = new \Federator\Data\ActivityPub\Common\Activity('Create'); $returnActivity = new \Federator\Data\ActivityPub\Common\Activity('Create');
} else { } else {
$returnActivity->setID($ap['id']); $returnActivity->setID($ap['id']);
@ -535,17 +539,18 @@ class ContentNation implements Connector
} elseif ($jsonData['object']['vote']['value'] == 0) { } elseif ($jsonData['object']['vote']['value'] == 0) {
$ap['type'] = 'Dislike'; $ap['type'] = 'Dislike';
} else { } else {
error_log("ContentNation::jsonToActivity unknown vote value: {$jsonData['object']['vote']['value']}"); error_log('ContentNation::jsonToActivity unknown vote value: '
. $jsonData['object']['vote']['value']);
break; break;
} }
$ap['object'] = self::generateObjectJson($ourUrl, $jsonData); $ap['object'] = self::generateObjectJson($ourUrl, $jsonData);
$returnActivity = \Federator\Data\ActivityPub\Factory::newActivityFromJson($ap); $returnActivity = \Federator\Data\ActivityPub\Factory::newActivityFromJson($ap);
if ($returnActivity === false) { if ($returnActivity === false) {
error_log("ContentNation::jsonToActivity couldn't create vote"); error_log('ContentNation::jsonToActivity couldn\'t create vote');
if ($ap['type'] === "Like") { if ($ap['type'] === 'Like') {
$returnActivity = new \Federator\Data\ActivityPub\Common\Like(); $returnActivity = new \Federator\Data\ActivityPub\Common\Like();
} elseif ($ap['type'] === "Dislike") { } elseif ($ap['type'] === 'Dislike') {
$returnActivity = new \Federator\Data\ActivityPub\Common\Dislike(); $returnActivity = new \Federator\Data\ActivityPub\Common\Dislike();
} else { } else {
$returnActivity = new \Federator\Data\ActivityPub\Common\Undo(); $returnActivity = new \Federator\Data\ActivityPub\Common\Undo();
@ -559,7 +564,8 @@ class ContentNation implements Connector
default: default:
// Handle unsupported types or fallback to default behavior // Handle unsupported types or fallback to default behavior
throw new \InvalidArgumentException("ContentNation::jsonToActivity Unsupported object type: {$jsonData['type']}"); throw new \InvalidArgumentException('ContentNation::jsonToActivity Unsupported object type: '
. $jsonData['type']);
} }
} }
@ -581,7 +587,7 @@ class ContentNation implements Connector
$actorUrl = $ourUrl . '/' . $actorName; $actorUrl = $ourUrl . '/' . $actorName;
if ($objectType === "article") { if ($objectType === 'article') {
$articleName = $jsonData['object']['name'] ?? null; $articleName = $jsonData['object']['name'] ?? null;
$articleOwnerName = $jsonData['object']['ownerName'] ?? null; $articleOwnerName = $jsonData['object']['ownerName'] ?? null;
$updatedOn = $jsonData['object']['modified'] ?? null; $updatedOn = $jsonData['object']['modified'] ?? null;
@ -589,13 +595,13 @@ class ContentNation implements Connector
$update = $updatedOn !== $originalPublished; $update = $updatedOn !== $originalPublished;
$returnJson = [ $returnJson = [
'type' => 'Article', 'type' => 'Article',
'id' => $ourUrl . "/" . $articleOwnerName . "/" . $articleName, 'id' => $ourUrl . '/' . $articleOwnerName . '/' . $articleName,
'name' => $jsonData['object']['title'] ?? null, 'name' => $jsonData['object']['title'] ?? null,
'published' => $originalPublished, 'published' => $originalPublished,
'summary' => $jsonData['object']['summary'] ?? null, 'summary' => $jsonData['object']['summary'] ?? null,
'content' => $jsonData['object']['content'] ?? null, 'content' => $jsonData['object']['content'] ?? null,
'attributedTo' => $actorUrl, 'attributedTo' => $actorUrl,
'url' => $ourUrl . "/" . $articleOwnerName . "/" . $articleName, 'url' => $ourUrl . '/' . $articleOwnerName . '/' . $articleName,
'cc' => ['https://www.w3.org/ns/activitystreams#Public'], 'cc' => ['https://www.w3.org/ns/activitystreams#Public'],
]; ];
if ($update) { if ($update) {
@ -619,14 +625,14 @@ class ContentNation implements Connector
} }
} }
} }
} elseif ($objectType === "comment") { } elseif ($objectType === 'comment') {
$commentId = $jsonData['object']['id'] ?? null; $commentId = $jsonData['object']['id'] ?? null;
$articleName = $jsonData['object']['articleName'] ?? null; $articleName = $jsonData['object']['articleName'] ?? null;
$articleOwnerName = $jsonData['object']['articleOwnerName'] ?? null; $articleOwnerName = $jsonData['object']['articleOwnerName'] ?? null;
$returnJson = [ $returnJson = [
'type' => 'Note', 'type' => 'Note',
'id' => $ourUrl . "/" . $articleOwnerName . "/" . $articleName . "#" . $commentId, 'id' => $ourUrl . '/' . $articleOwnerName . '/' . $articleName . '#' . $commentId,
'url' => $ourUrl . "/" . $articleOwnerName . "/" . $articleName . "#" . $commentId, 'url' => $ourUrl . '/' . $articleOwnerName . '/' . $articleName . '#' . $commentId,
'attributedTo' => $actorUrl, 'attributedTo' => $actorUrl,
'content' => $jsonData['object']['content'] ?? null, 'content' => $jsonData['object']['content'] ?? null,
'summary' => $jsonData['object']['summary'] ?? null, 'summary' => $jsonData['object']['summary'] ?? null,
@ -642,24 +648,26 @@ class ContentNation implements Connector
} }
$replyType = $jsonData['object']['inReplyTo']['type'] ?? null; $replyType = $jsonData['object']['inReplyTo']['type'] ?? null;
if ($replyType === "article") { if ($replyType === "article") {
$returnJson['inReplyTo'] = $ourUrl . "/" . $articleOwnerName . "/" . $articleName; $returnJson['inReplyTo'] = $ourUrl . '/' . $articleOwnerName . '/' . $articleName;
} elseif ($replyType === "comment") { } elseif ($replyType === "comment") {
$returnJson['inReplyTo'] = $ourUrl . "/" . $articleOwnerName . "/" . $articleName . "#" . $jsonData['object']['inReplyTo']['id']; $returnJson['inReplyTo'] = $ourUrl . '/' . $articleOwnerName . '/' . $articleName
. '#' . $jsonData['object']['inReplyTo']['id'];
} else { } else {
error_log("ContentNation::generateObjectJson for comment - unknown inReplyTo type: {$replyType}"); error_log('ContentNation::generateObjectJson for comment - unknown inReplyTo type: '
. $replyType);
} }
} elseif ($objectType === "vote") { } elseif ($objectType === 'vote') {
$votedOn = $jsonData['object']['type'] ?? null; $votedOn = $jsonData['object']['type'] ?? null;
$articleName = $jsonData['object']['articleName'] ?? null; $articleName = $jsonData['object']['articleName'] ?? null;
$articleOwnerName = $jsonData['object']['articleOwnerName'] ?? null; $articleOwnerName = $jsonData['object']['articleOwnerName'] ?? null;
$objectId = $ourUrl . '/' . $articleOwnerName . '/' . $articleName; $objectId = $ourUrl . '/' . $articleOwnerName . '/' . $articleName;
if ($votedOn === "comment") { if ($votedOn === 'comment') {
$objectId .= '#' . $jsonData['object']['commentId']; $objectId .= '#' . $jsonData['object']['commentId'];
} }
$returnJson = $objectId; $returnJson = $objectId;
} else { } else {
error_log("ContentNation::generateObjectJson unknown object type: {$objectType}"); error_log('ContentNation::generateObjectJson unknown object type: ' . $objectType);
return false; return false;
} }
@ -678,10 +686,16 @@ class ContentNation implements Connector
$targetUrl = $this->service; $targetUrl = $this->service;
$targetRequestType = 'post'; // Default request type $targetRequestType = 'post'; // Default request type
// Convert ActivityPub activity to ContentNation JSON format and retrieve target url // Convert ActivityPub activity to ContentNation JSON format and retrieve target url
$jsonData = self::activityToJson($this->main->getDatabase(), $this->service, $activity, $targetUrl, $targetRequestType); $jsonData = self::activityToJson(
$this->main->getDatabase(),
$this->service,
$activity,
$targetUrl,
$targetRequestType
);
if ($jsonData === false) { if ($jsonData === false) {
error_log("ContentNation::sendActivity failed to convert activity to JSON"); error_log('ContentNation::sendActivity failed to convert activity to JSON');
return false; return false;
} }
@ -709,7 +723,7 @@ class ContentNation implements Connector
"date: {$date}\n" . "date: {$date}\n" .
"digest: {$digest}"; "digest: {$digest}";
$pKeyPath = PROJECT_ROOT . '/' . $this->main->getConfig()['keys']['federatorPrivateKeyPath']; $pKeyPath = $_SERVER['DOCUMENT_ROOT'] . '../' . $this->main->getConfig()['keys']['federatorPrivateKeyPath'];
$privateKeyPem = file_get_contents($pKeyPath); $privateKeyPem = file_get_contents($pKeyPath);
if ($privateKeyPem === false) { if ($privateKeyPem === false) {
http_response_code(500); http_response_code(500);
@ -725,7 +739,8 @@ class ContentNation implements Connector
openssl_sign($signatureString, $signature, $pkeyId, OPENSSL_ALGO_SHA256); openssl_sign($signatureString, $signature, $pkeyId, OPENSSL_ALGO_SHA256);
$signature_b64 = base64_encode($signature); $signature_b64 = base64_encode($signature);
$signatureHeader = 'algorithm="rsa-sha256",headers="(request-target) host date digest",signature="' . $signature_b64 . '"'; $signatureHeader = 'algorithm="rsa-sha256",headers="(request-target) host date digest",signature="'
. $signature_b64 . '"';
$ch = curl_init($targetUrl); $ch = curl_init($targetUrl);
if ($ch === false) { if ($ch === false) {
@ -750,7 +765,8 @@ class ContentNation implements Connector
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE'); curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
break; break;
default: default:
throw new \Exception("ContentNation::sendActivity Unsupported target request type: $targetRequestType"); throw new \Exception('ContentNation::sendActivity Unsupported target request type: '
. $targetRequestType);
} }
curl_setopt($ch, CURLOPT_POSTFIELDS, $json); curl_setopt($ch, CURLOPT_POSTFIELDS, $json);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
@ -758,11 +774,11 @@ class ContentNation implements Connector
curl_close($ch); curl_close($ch);
if ($response === false) { if ($response === false) {
throw new \Exception("Failed to send activity: " . curl_error($ch)); throw new \Exception('Failed to send activity: ' . curl_error($ch));
} else { } else {
$httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($httpcode != 200 && $httpcode != 202) { if ($httpcode != 200 && $httpcode != 202) {
throw new \Exception("Unexpected HTTP code $httpcode: $response"); throw new \Exception('Unexpected HTTP code ' . $httpcode .':' . $response);
} }
} }
@ -779,7 +795,7 @@ class ContentNation implements Connector
* @param string $targetRequestType the target request type (e.g., 'post', 'delete', etc.) * @param string $targetRequestType the target request type (e.g., 'post', 'delete', etc.)
* @return array<string, mixed>|false the json data or false on failure * @return array<string, mixed>|false the json data or false on failure
*/ */
private function activityToJson($dbh, $serviceUrl, \Federator\Data\ActivityPub\Common\Activity $activity, string &$targetUrl, string &$targetRequestType) private function activityToJson($dbh, $serviceUrl, $activity, &$targetUrl, &$targetRequestType)
{ {
$type = strtolower($activity->getType()); $type = strtolower($activity->getType());
$targetRequestType = 'post'; // Default request type $targetRequestType = 'post'; // Default request type
@ -791,12 +807,14 @@ class ContentNation implements Connector
$objType = strtolower($object->getType()); $objType = strtolower($object->getType());
$articleId = \Federator\DIO\Posts::getOriginalArticleId($dbh, $activity); $articleId = \Federator\DIO\Posts::getOriginalArticleId($dbh, $activity);
if ($articleId === null) { if ($articleId === null) {
error_log("ContentNation::activityToJson Failed to get original article ID for create/update activity"); error_log('ContentNation::activityToJson Failed to get original article ID'
.' for create/update activity');
} }
switch ($objType) { switch ($objType) {
case 'article': case 'article':
// We don't support article create/update at this point in time // We don't support article create/update at this point in time
error_log("ContentNation::activityToJson Unsupported create/update object type: {$objType}"); error_log('ContentNation::activityToJson Unsupported create/update object type: '
.$objType);
break; break;
case 'note': case 'note':
$targetUrl = $serviceUrl . '/api/article/' . $articleId . '/comment'; $targetUrl = $serviceUrl . '/api/article/' . $articleId . '/comment';
@ -816,7 +834,8 @@ class ContentNation implements Connector
} }
} }
} else { } else {
error_log("ContentNation::activityToJson Unsupported target type for comment with id: " . $activity->getID() . " Type: " . gettype($target)); error_log('ContentNation::activityToJson Unsupported target type for comment with id: '
. $activity->getID() . ' Type: ' . gettype($target));
return false; return false;
} }
return [ return [
@ -827,7 +846,8 @@ class ContentNation implements Connector
'comment' => $object->getContent(), 'comment' => $object->getContent(),
]; ];
default: default:
error_log("ContentNation::activityToJson Unsupported create/update object type: {$objType}"); error_log('ContentNation::activityToJson Unsupported create/update object type: '
. $objType);
return false; return false;
} }
} }
@ -836,13 +856,14 @@ class ContentNation implements Connector
case 'follow': case 'follow':
$profileUrl = $activity->getObject(); $profileUrl = $activity->getObject();
if (!is_string($profileUrl)) { if (!is_string($profileUrl)) {
error_log("ContentNation::activityToJson Invalid profile URL: " . json_encode($profileUrl)); error_log('ContentNation::activityToJson Invalid profile URL: ' . json_encode($profileUrl));
return false; return false;
} }
$receiverName = basename((string) (parse_url($profileUrl, PHP_URL_PATH) ?? '')); $receiverName = basename((string) (parse_url($profileUrl, PHP_URL_PATH) ?? ''));
$ourDomain = parse_url($profileUrl, PHP_URL_HOST); $ourDomain = parse_url($profileUrl, PHP_URL_HOST);
if ($receiverName === "" || $ourDomain === "") { if ($receiverName === '' || $ourDomain === '') {
error_log("ContentNation::activityToJson no profileName or domain found for object url: " . $profileUrl); error_log('ContentNation::activityToJson no profileName or domain found for object url: '
. $profileUrl);
return false; return false;
} }
$receiver = $receiverName; $receiver = $receiverName;
@ -854,11 +875,12 @@ class ContentNation implements Connector
null null
); );
} catch (\Throwable $e) { } catch (\Throwable $e) {
error_log("ContentNation::activityToJson get user by name: " . $receiver . ". Exception: " . $e->getMessage()); error_log('ContentNation::activityToJson get user by name: ' . $receiver . '. Exception: '
. $e->getMessage());
return false; return false;
} }
if ($localUser === null || $localUser->id === null) { if ($localUser === null || $localUser->id === null) {
error_log("ContentNation::activityToJson couldn't find user: $receiver"); error_log('ContentNation::activityToJson couldn\'t find user: ' . $receiver);
return false; return false;
} }
$targetUrl = $serviceUrl . '/api/profile/' . $localUser->id . '/fedfollow'; $targetUrl = $serviceUrl . '/api/profile/' . $localUser->id . '/fedfollow';
@ -881,7 +903,7 @@ class ContentNation implements Connector
case 'dislike': case 'dislike':
$articleId = \Federator\DIO\Posts::getOriginalArticleId($dbh, $activity); $articleId = \Federator\DIO\Posts::getOriginalArticleId($dbh, $activity);
if ($articleId === null) { if ($articleId === null) {
error_log("ContentNation::activityToJson Failed to get original article ID for vote activity"); error_log('ContentNation::activityToJson Failed to get original article ID for vote activity');
} }
$voteValue = $type === 'like' ? true : false; $voteValue = $type === 'like' ? true : false;
$activityType = 'vote'; $activityType = 'vote';
@ -900,7 +922,8 @@ class ContentNation implements Connector
} }
} }
} else { } else {
error_log("ContentNation::activityToJson Unsupported target type for vote with id: " . $activity->getID() . " Type: " . gettype($target)); error_log('ContentNation::activityToJson Unsupported target type for vote with id: '
. $activity->getID() . ' Type: ' . gettype($target));
return false; return false;
} }
$targetUrl = $serviceUrl . '/api/article/' . $articleId . '/vote'; $targetUrl = $serviceUrl . '/api/article/' . $articleId . '/vote';
@ -919,13 +942,15 @@ class ContentNation implements Connector
case 'follow': case 'follow':
$profileUrl = $object->getObject(); $profileUrl = $object->getObject();
if (!is_string($profileUrl)) { if (!is_string($profileUrl)) {
error_log("ContentNation::activityToJson Invalid profile URL: " . json_encode($profileUrl)); error_log('ContentNation::activityToJson Invalid profile URL: '
. json_encode($profileUrl));
return false; return false;
} }
$receiverName = basename((string) (parse_url($profileUrl, PHP_URL_PATH) ?? '')); $receiverName = basename((string) (parse_url($profileUrl, PHP_URL_PATH) ?? ''));
$ourDomain = parse_url($profileUrl, PHP_URL_HOST); $ourDomain = parse_url($profileUrl, PHP_URL_HOST);
if ($receiverName === "" || $ourDomain === "") { if ($receiverName === '' || $ourDomain === '') {
error_log("ContentNation::activityToJson no profileName or domain found for object url: " . $profileUrl); error_log('ContentNation::activityToJson no profileName or domain found for object'
. ' url: ' . $profileUrl);
return false; return false;
} }
$receiver = $receiverName; $receiver = $receiverName;
@ -937,11 +962,12 @@ class ContentNation implements Connector
null null
); );
} catch (\Throwable $e) { } catch (\Throwable $e) {
error_log("ContentNation::activityToJson get user by name: " . $receiver . ". Exception: " . $e->getMessage()); error_log('ContentNation::activityToJson get user by name: ' . $receiver
. '. Exception: ' . $e->getMessage());
return false; return false;
} }
if ($localUser === null || $localUser->id === null) { if ($localUser === null || $localUser->id === null) {
error_log("ContentNation::activityToJson couldn't find user: $receiver"); error_log('ContentNation::activityToJson couldn\'t find user: ' . $receiver);
return false; return false;
} }
$targetUrl = $serviceUrl . '/api/profile/' . $localUser->id . '/fedfollow'; $targetUrl = $serviceUrl . '/api/profile/' . $localUser->id . '/fedfollow';
@ -970,7 +996,8 @@ class ContentNation implements Connector
case 'dislike': case 'dislike':
$articleId = \Federator\DIO\Posts::getOriginalArticleId($dbh, $activity); $articleId = \Federator\DIO\Posts::getOriginalArticleId($dbh, $activity);
if ($articleId === null) { if ($articleId === null) {
error_log("ContentNation::activityToJson Failed to get original article ID for undo vote activity"); error_log('ContentNation::activityToJson Failed to get original article ID '
. 'for undo vote activity');
} }
$activityType = 'vote'; $activityType = 'vote';
$inReplyTo = $object->getInReplyTo(); $inReplyTo = $object->getInReplyTo();
@ -988,7 +1015,8 @@ class ContentNation implements Connector
} }
} }
} else { } else {
error_log("ContentNation::activityToJson Unsupported target type for undo vote with id: " . $activity->getID() . " Type: " . gettype($target)); error_log('ContentNation::activityToJson Unsupported target type for undo '
. 'vote with id: ' . $activity->getID() . " Type: " . gettype($target));
return false; return false;
} }
$targetUrl = $serviceUrl . '/api/article/' . $articleId . '/vote'; $targetUrl = $serviceUrl . '/api/article/' . $articleId . '/vote';
@ -1000,16 +1028,17 @@ class ContentNation implements Connector
]; ];
case 'note': case 'note':
// We don't support comment deletions at this point in time // We don't support comment deletions at this point in time
error_log("ContentNation::activityToJson Unsupported undo object type: {$objType}"); error_log('ContentNation::activityToJson Unsupported undo object type: ' . $objType);
break; break;
default: default:
error_log("ContentNation::activityToJson Unsupported create/update object type: {$objType}"); error_log('ContentNation::activityToJson Unsupported create/update object type: '
. $objType);
return false; return false;
} }
} }
break; break;
default: default:
error_log("ContentNation::activityToJson Unsupported activity type: {$type}"); error_log('ContentNation::activityToJson Unsupported activity type: ' . $type);
return false; return false;
} }
@ -1028,11 +1057,11 @@ class ContentNation implements Connector
$signatureHeader = $headers['Signature'] ?? null; $signatureHeader = $headers['Signature'] ?? null;
if (!isset($signatureHeader)) { if (!isset($signatureHeader)) {
throw new \Federator\Exceptions\PermissionDenied("Missing Signature header"); throw new \Federator\Exceptions\PermissionDenied('Missing Signature header');
} }
if (!isset($headers['X-Sender']) || $headers['X-Sender'] !== $this->config['keys']['headerSenderName']) { if (!isset($headers['X-Sender']) || $headers['X-Sender'] !== $this->config['keys']['headerSenderName']) {
throw new \Federator\Exceptions\PermissionDenied("Invalid sender name"); throw new \Federator\Exceptions\PermissionDenied('Invalid sender name');
} }
// Parse Signature header // Parse Signature header
@ -1042,11 +1071,11 @@ class ContentNation implements Connector
$signature = base64_decode($signatureParts['signature']); $signature = base64_decode($signatureParts['signature']);
$signedHeaders = explode(' ', $signatureParts['headers']); $signedHeaders = explode(' ', $signatureParts['headers']);
$pKeyPath = PROJECT_ROOT . '/' . $this->config['keys']['publicKeyPath']; $pKeyPath = $_SERVER['DOCUMENT_ROOT'] . '../' . $this->config['keys']['publicKeyPath'];
$publicKeyPem = file_get_contents($pKeyPath); $publicKeyPem = file_get_contents($pKeyPath);
if ($publicKeyPem === false) { if ($publicKeyPem === false) {
http_response_code(500); http_response_code(500);
throw new \Federator\Exceptions\PermissionDenied("Public key couldn't be determined"); throw new \Federator\Exceptions\PermissionDenied('Public key couldn\'t be determined');
} }
// Reconstruct the signed string // Reconstruct the signed string
@ -1060,7 +1089,7 @@ class ContentNation implements Connector
$headerValue = $headers[ucwords($header, '-')] ?? ''; $headerValue = $headers[ucwords($header, '-')] ?? '';
} }
$signedString .= strtolower($header) . ": " . $headerValue . "\n"; $signedString .= strtolower($header) . ': ' . $headerValue . "\n";
} }
$signedString = rtrim($signedString); $signedString = rtrim($signedString);
@ -1073,9 +1102,9 @@ class ContentNation implements Connector
} }
if ($verified != 1) { if ($verified != 1) {
http_response_code(500); http_response_code(500);
throw new \Federator\Exceptions\PermissionDenied("Signature verification failed"); throw new \Federator\Exceptions\PermissionDenied('Signature verification failed');
} }
return "Signature verified."; return 'Signature verified.';
} }
} }
@ -1090,6 +1119,5 @@ namespace Federator;
function contentnation_load($main) function contentnation_load($main)
{ {
$cn = new Connector\ContentNation($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); $main->setConnector($cn);
} }

View file

@ -25,7 +25,7 @@ class DummyConnector implements Connector
* @param string $userId user id @unused-param * @param string $userId user id @unused-param
* @return \Federator\Data\FedUser[]|false * @return \Federator\Data\FedUser[]|false
*/ */
public function getRemoteFollowersOfUser($userId) public function getFollowersByUser($userId)
{ {
return false; return false;
} }
@ -34,10 +34,9 @@ class DummyConnector implements Connector
* get following of given user * get following of given user
* *
* @param string $id user id @unused-param * @param string $id user id @unused-param
* @return \Federator\Data\FedUser[]|false * @return \Federator\Data\FedUser[]|false
*/ */
public function getRemoteFollowingForUser($id) public function getFollowingByUser($id)
{ {
return false; return false;
} }
@ -78,7 +77,8 @@ class DummyConnector implements Connector
* (used to identify the article in the remote system) @unused-param * (used to identify the article in the remote system) @unused-param
* @return \Federator\Data\ActivityPub\Common\Activity|false * @return \Federator\Data\ActivityPub\Common\Activity|false
*/ */
public function jsonToActivity(array $jsonData, &$articleId) { public function jsonToActivity(array $jsonData, &$articleId)
{
return false; return false;
} }
@ -147,6 +147,5 @@ namespace Federator;
function dummy_load($main) function dummy_load($main)
{ {
$dummy = new Connector\DummyConnector(); $dummy = new Connector\DummyConnector();
# echo "dummyconnector::dummy_load Loaded new connector, adding to main\n"; // TODO change to proper log
$main->setConnector($dummy); $main->setConnector($dummy);
} }

View file

@ -53,11 +53,12 @@ class RedisCache implements Cache
*/ */
public function __construct() public function __construct()
{ {
$config = parse_ini_file(PROJECT_ROOT . '/rediscache.ini'); $config = parse_ini_file('../rediscache.ini');
if ($config !== false) { if ($config !== false) {
$this->config = $config; $this->config = $config;
$this->userTTL = array_key_exists('userttl', $config) ? intval($config['userttl'], 10) : 60; $this->userTTL = array_key_exists('userttl', $config) ? intval($config['userttl'], 10) : 60;
$this->publicKeyPemTTL = array_key_exists('publickeypemttl', $config) ? intval($config['publickeypemttl'], 10) : 3600; $this->publicKeyPemTTL = array_key_exists('publickeypemttl', $config)
? intval($config['publickeypemttl'], 10) : 3600;
} }
} }
@ -102,9 +103,9 @@ class RedisCache implements Cache
* @return \Federator\Data\FedUser[]|false * @return \Federator\Data\FedUser[]|false
*/ */
public function getRemoteFollowersOfUser($id) public function getFollowersByUser($id)
{ {
error_log("rediscache::getRemoteFollowersOfUser not implemented"); error_log("rediscache::getFollowersByUser not implemented");
return false; return false;
} }
@ -115,9 +116,9 @@ class RedisCache implements Cache
* @return \Federator\Data\FedUser[]|false * @return \Federator\Data\FedUser[]|false
*/ */
public function getRemoteFollowingForUser($id) public function getFollowingByUser($id)
{ {
error_log("rediscache::getRemoteFollowingForUser not implemented"); error_log("rediscache::getFollowingByUser not implemented");
return false; return false;
} }
@ -253,9 +254,9 @@ class RedisCache implements Cache
* @param \Federator\Data\FedUser[]|false $followers user followers @unused-param * @param \Federator\Data\FedUser[]|false $followers user followers @unused-param
* @return void * @return void
*/ */
public function saveRemoteFollowersOfUser($user, $followers) public function saveFollowersByUser($user, $followers)
{ {
error_log("rediscache::saveRemoteFollowersOfUser not implemented"); error_log("rediscache::saveFollowersByUser not implemented");
} }
/** /**
@ -265,9 +266,9 @@ class RedisCache implements Cache
* @param \Federator\Data\FedUser[]|false $following user following @unused-param * @param \Federator\Data\FedUser[]|false $following user following @unused-param
* @return void * @return void
*/ */
public function saveRemoteFollowingForUser($user, $following) public function saveFollowingByUser($user, $following)
{ {
error_log("rediscache::saveRemoteFollowingForUser not implemented"); error_log("rediscache::saveFollowingByUser not implemented");
} }
/** /**
@ -300,6 +301,7 @@ class RedisCache implements Cache
$serialized = $stats->toJson(); $serialized = $stats->toJson();
$this->redis->setEx($key, $this->config['statsttl'], $serialized); $this->redis->setEx($key, $this->config['statsttl'], $serialized);
} }
/** /**
* save remote user by name * save remote user by name
* *

View file

@ -19,10 +19,10 @@ primary goal is to connect ContentNation via ActivityPub again.
- [X] full cache for users - [X] full cache for users
- [X] webfinger - [X] webfinger
- [X] discovery endpoints - [X] discovery endpoints
- [ ] ap outbox - [X] ap outbox
- [ ] ap inbox - [X] ap inbox
- [ ] support for AP profile in service - [ ] support for AP profile in service
- [ ] support for article - [ ] support for article
- [ ] support for comment - [ ] support for comment
- [ ] posting comments from ap to service - [ ] posting comments from ap to service
- [ ] callback from service to add new input - [X] callback from service to add new input

2
sql/2024-07-24.sql Normal file
View file

@ -0,0 +1,2 @@
create table outbox (`id` char(32) unique primary key, `user` varchar(255), index(user), `timestamp` timestamp, `type` enum ("article", "note"), index(type), `externalid` varchar(255), index(externalid), `apjson` text);
update settings set `value`="2024-07-24" where `key`="database_version";

2
sql/2024-07-30.sql Normal file
View file

@ -0,0 +1,2 @@
create table webfinger (`subject` varchar(255) unique primary key, `timestamp` timestamp, index(`timestamp`), `aliases` text, `links` text);
update settings set `value`="2024-07-22" where `key`="database_version";

3
sql/2024-08-19.sql Normal file
View file

@ -0,0 +1,3 @@
create table settings(`key` varchar(255) unique primary key, `value` text);
create table users(`id` varchar(255) unique primary key, `externalid` varchar(255), index(`externalid`), `rsapublic` text, `rsaprivate` text);
insert into settings (`key`, `value`) value ("database_version", "2024-07-19");

View file

@ -64,18 +64,18 @@
"followers":"https://{$fqdn}/{$username}/followers", "followers":"https://{$fqdn}/{$username}/followers",
"inbox":"https://{$fqdn}/{$username}/inbox", "inbox":"https://{$fqdn}/{$username}/inbox",
"outbox":"https://{$fqdn}/{$username}/outbox", "outbox":"https://{$fqdn}/{$username}/outbox",
"featured":"https://{$fqdn}/{$username}/collections/featured", {*"featured":"https://{$fqdn}/{$username}/collections/featured",
"featuredTags":"https://{$fqdn}/{$username}/collections/tags", "featuredTags":"https://{$fqdn}/{$username}/collections/tags",*}
"preferredUsername":"{$username}", "preferredUsername":"{$username}",
"name":"{$name}", "name":"{$name}",
"summary":"{$summary}", "summary":"{$summary}",
"url":"https://{$fqdn}/@{$username}", "url":"https://{$sourcedomain}/@{$username}",
"manuallyApprovesFollowers":false, "manuallyApprovesFollowers":false,
"discoverable":true, "discoverable":true,
"published":"{$registered}", "published":"{$registered}",
"publicKey":{ldelim} "publicKey":{ldelim}
"id":"https://{$fqdn}/{$username}#main-key", "id":"https://{$fqdn}/{$username}#main-key",
"owner":"https://{$fqdn}/{$username}", "owner":"https://{$sourcedomain}/{$username}",
"publicKeyPem":"{$publickey}" "publicKeyPem":"{$publickey}"
{rdelim}, {rdelim},
"tag":[], "tag":[],
@ -83,7 +83,7 @@
{if $type==='group'}{ldelim} {if $type==='group'}{ldelim}
"type":"PropertyValue", "type":"PropertyValue",
"name":"website", "name":"website",
"value":"\u003ca href=\"https://{$fqdn}/@{$username}\" target=\"_blank\" rel=\"nofollow noopener noreferrer me\"\u003e\u003cspan class=\"invisible\"\u003ehttps://\u003c/span\u003e\u003cspan class=\"\"\u003e{$fqdn}/@{$username}\u003c/span\u003e\u003cspan class=\"invisible\"\u003e\u003c/span\u003e\u003c/a\u003e" "value":"\u003ca href=\"https://{$sourcedomain}/@{$username}\" target=\"_blank\" rel=\"nofollow noopener noreferrer me\"\u003e\u003cspan class=\"invisible\"\u003ehttps://\u003c/span\u003e\u003cspan class=\"\"\u003e{$fqdn}/@{$username}\u003c/span\u003e\u003cspan class=\"invisible\"\u003e\u003c/span\u003e\u003c/a\u003e"
{rdelim}{/if} {rdelim}{/if}
], ],
"endpoints":{ldelim} "endpoints":{ldelim}

View file

@ -1,7 +1,7 @@
{ldelim} {ldelim}
"subject": "acct:{$username}@{$domain}", "subject": "acct:{$username}@{$sourcedomain}",
"aliases": [ "aliases": [
"https://{$domain}/@{$username}" "https://{$sourcedomain}/@{$username}"
], ],
"links": [ "links": [
{ldelim}"rel": "self", "type": "application/activity+json", "href": "https://{$domain}/{$username}"{rdelim}, {ldelim}"rel": "self", "type": "application/activity+json", "href": "https://{$domain}/{$username}"{rdelim},