Compare commits
42 commits
develop
...
mastodon-s
Author | SHA1 | Date | |
---|---|---|---|
![]() |
61f6fe3e7b | ||
![]() |
2ae81a3748 | ||
![]() |
097f871ed6 | ||
![]() |
c1cf2a6be8 | ||
![]() |
d99479c188 | ||
![]() |
4cc9cfdc8c | ||
![]() |
474631dff2 | ||
![]() |
207d876254 | ||
![]() |
9b3ae63c7e | ||
![]() |
96bb1efe16 | ||
![]() |
8891234617 | ||
![]() |
90fe8fab5c | ||
![]() |
f8539e479e | ||
![]() |
10dec5ebd3 | ||
![]() |
30c577c82f | ||
![]() |
5c90b4cfc9 | ||
![]() |
7a5870de95 | ||
![]() |
572bb376c1 | ||
![]() |
62cfd6ef0d | ||
![]() |
ba88adcebd | ||
![]() |
d355b5a7cd | ||
![]() |
767f51cc5b | ||
![]() |
6cf9a030a4 | ||
![]() |
49a4bee76a | ||
![]() |
da18d37a79 | ||
![]() |
ce7aa5c72d | ||
![]() |
8ea9bdcf9a | ||
![]() |
10a3b1e0f9 | ||
![]() |
352377887c | ||
![]() |
0edc4c0db6 | ||
![]() |
21f73fb56f | ||
![]() |
4d36fc3c61 | ||
![]() |
d9b02bd95b | ||
![]() |
957b4f5266 | ||
![]() |
64ee4c518f | ||
![]() |
f6d6c74bc6 | ||
![]() |
20c2db4b35 | ||
![]() |
305ded4986 | ||
![]() |
823283183e | ||
![]() |
721e37882d | ||
![]() |
1a7b8264a1 | ||
![]() |
530caa7ea6 |
45 changed files with 904 additions and 886 deletions
16
.gitattributes
vendored
Normal file
16
.gitattributes
vendored
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
# 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
3
.gitignore
vendored
|
@ -6,6 +6,3 @@ php-docs
|
||||||
phpdoc
|
phpdoc
|
||||||
html
|
html
|
||||||
/cache
|
/cache
|
||||||
contentnation.ini
|
|
||||||
*.pem*
|
|
||||||
composer.phar
|
|
||||||
|
|
15
config.ini
15
config.ini
|
@ -1,7 +1,5 @@
|
||||||
[generic]
|
[generic]
|
||||||
protocol = 'https'
|
externaldomain = 'contentnation.net'
|
||||||
externaldomain = 'federator.your.fqdn'
|
|
||||||
sourcedomain = 'your.fqdn'
|
|
||||||
|
|
||||||
[database]
|
[database]
|
||||||
host = '127.0.0.1'
|
host = '127.0.0.1'
|
||||||
|
@ -10,17 +8,18 @@ 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.pem'
|
federatorPrivateKeyPath = 'federator.key'
|
||||||
federatorPublicKeyPath = 'federator.pem.pub'
|
federatorPublicKeyPath = 'federator.pub'
|
10
contentnation.ini
Normal file
10
contentnation.ini
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
[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'
|
|
@ -14,6 +14,7 @@ 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();
|
||||||
|
|
113
htdocs/index.html
Normal file
113
htdocs/index.html
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
<!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>
|
|
@ -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,7 +74,8 @@ 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);
|
||||||
|
@ -100,7 +101,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]) {
|
||||||
|
@ -110,6 +111,39 @@ 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;
|
||||||
}
|
}
|
||||||
|
@ -123,7 +157,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 {
|
||||||
|
@ -139,26 +173,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -172,7 +206,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) {
|
||||||
|
@ -216,7 +250,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
|
||||||
|
@ -226,36 +260,32 @@ 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($publicKeyPem === false) {
|
$publicKeyPem = $this->cache->getPublicKey($keyId);
|
||||||
|
|
||||||
|
if (!isset($publicKeyPem) || $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
|
||||||
|
@ -269,7 +299,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);
|
||||||
|
@ -282,11 +312,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.";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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,7 +127,6 @@ 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();
|
||||||
|
@ -138,7 +137,6 @@ 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, '"'),
|
||||||
|
@ -149,7 +147,6 @@ 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
|
||||||
*
|
*
|
||||||
|
|
|
@ -59,17 +59,16 @@ 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 = $protocol . '://' . $domain . '/' . $_user . '/followers';
|
$baseUrl = 'https://' . $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);
|
||||||
|
@ -88,11 +87,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));
|
||||||
|
|
|
@ -43,6 +43,7 @@ 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,
|
||||||
|
@ -55,20 +56,19 @@ 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::getFollowingByUser($dbh, $user->id, $connector, $cache);
|
$followingItems = \Federator\DIO\Followers::getFollowingForUser($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 = $protocol . '://' . $domain . '/users/' . $_user . '/following';
|
$baseUrl = 'https://' . $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));
|
||||||
|
|
|
@ -54,8 +54,7 @@ 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) {
|
||||||
error_log("signature check failed");
|
throw new \Federator\Exceptions\Unauthorized("Inbox::post Signature check failed: " . $e->getMessage());
|
||||||
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;
|
||||||
|
@ -65,18 +64,19 @@ 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");
|
||||||
}
|
}
|
||||||
$actor = $inboxActivity->getAActor(); // url of the sender https://contentnation.net/username
|
$user = $inboxActivity->getAActor(); // url of the sender https://contentnation.net/username
|
||||||
$username = basename((string) (parse_url($actor, PHP_URL_PATH) ?? ''));
|
$username = basename((string) (parse_url($user, PHP_URL_PATH) ?? ''));
|
||||||
$domain = parse_url($actor, PHP_URL_HOST);
|
$domain = parse_url($user, 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,56 +132,29 @@ 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(
|
$followers = \Federator\DIO\Followers::getFollowersByFedUser($dbh, $connector, $cache, $username . '@' . $domain);
|
||||||
$dbh,
|
|
||||||
$connector,
|
|
||||||
$cache,
|
|
||||||
$username . '@' . $domain
|
|
||||||
);
|
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
error_log('Inbox::post get followers for user: ' . $username . '@' . $domain . '. Exception: '
|
error_log("Inbox::post get followers for user: " . $username . '@' . $domain . ". Exception: " . $e->getMessage());
|
||||||
. $e->getMessage());
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -189,6 +162,19 @@ 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,
|
||||||
|
@ -197,11 +183,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 210 couldn\'t find user: ' . $receiver);
|
error_log("Inbox::post couldn't find user: $receiver");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$users[] = $localUser->id;
|
$users[] = $localUser->id;
|
||||||
|
@ -211,27 +197,15 @@ 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 = '/tmp/';
|
$rootDir = PROJECT_ROOT . '/';
|
||||||
// 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"
|
date('Y-m-d H:i:s') . ": ==== POST Inbox Activity ====\n" . json_encode($inboxActivity, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) . "\n\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;
|
||||||
|
@ -241,7 +215,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());
|
||||||
|
@ -251,12 +225,211 @@ 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: '
|
error_log("Inbox::post no users found for activity, doing nothing: " . json_encode($inboxActivity, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT));
|
||||||
. 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,9 +68,8 @@ 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 = $protocol . '://' . $domain . '/' . $_user . '/outbox';
|
$id = 'https://' . $domain . '/' . $_user . '/outbox';
|
||||||
$outbox->setPartOf($id);
|
$outbox->setPartOf($id);
|
||||||
$outbox->setID($id);
|
$outbox->setID($id);
|
||||||
if ($page === '') {
|
if ($page === '') {
|
||||||
|
|
|
@ -20,11 +20,7 @@ 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 = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -55,8 +51,11 @@ class Dummy implements \Federator\Api\APIInterface
|
||||||
case 'GET':
|
case 'GET':
|
||||||
switch (sizeof($paths)) {
|
switch (sizeof($paths)) {
|
||||||
case 3:
|
case 3:
|
||||||
if ($paths[2] === 'moo') {
|
switch ($paths[2]) {
|
||||||
return $this->getDummy();
|
case 'moo':
|
||||||
|
return $this->getDummy();
|
||||||
|
default:
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -64,12 +63,14 @@ class Dummy implements \Federator\Api\APIInterface
|
||||||
case 'POST':
|
case 'POST':
|
||||||
switch (sizeof($paths)) {
|
switch (sizeof($paths)) {
|
||||||
case 3:
|
case 3:
|
||||||
if ($paths[2] === 'moo') {
|
switch ($paths[2]) {
|
||||||
return $this->postDummy();
|
case 'moo':
|
||||||
|
return $this->postDummy();
|
||||||
|
default:
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
$this->main->setResponseCode(404);
|
$this->main->setResponseCode(404);
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -47,11 +47,10 @@ 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
|
||||||
/// TODO: throw unsupported method exception
|
throw new \Federator\Exceptions\InvalidArgument("GET not supported");
|
||||||
throw new \Federator\Exceptions\InvalidArgument('GET not supported');
|
|
||||||
}
|
}
|
||||||
switch (sizeof($paths)) {
|
switch (sizeof($paths)) {
|
||||||
case 3:
|
case 3:
|
||||||
|
@ -68,6 +67,7 @@ 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: '
|
error_log("NewContent::post get followers for user: " . $posterName . ". Exception: " . $e->getMessage());
|
||||||
. $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,8 +202,7 @@ 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"
|
date('Y-m-d H:i:s') . ": ==== POST NewContent Activity ====\n" . json_encode($newActivity, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) . "\n\n",
|
||||||
. json_encode($newActivity, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) . "\n\n",
|
|
||||||
FILE_APPEND
|
FILE_APPEND
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -219,7 +218,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);
|
||||||
|
@ -245,7 +244,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -257,7 +256,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -268,7 +267,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -276,8 +275,7 @@ 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"
|
date('Y-m-d H:i:s') . ": ==== POST " . $recipient->id . " NewContent Activity ====\n" . json_encode($newActivity, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) . "\n\n",
|
||||||
. json_encode($newActivity, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) . "\n\n",
|
|
||||||
FILE_APPEND
|
FILE_APPEND
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -288,13 +286,14 @@ 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,
|
$success = \Federator\DIO\Followers::sendFollowRequest($dbh, $connector, $cache, $user->id, $followerId, $followerDomain);
|
||||||
$followerId, $followerDomain);
|
} */
|
||||||
}*/
|
|
||||||
}
|
}
|
||||||
/* if ($success === false) {
|
/* if ($success === false) {
|
||||||
error_log("NewContent::postForUser Failed to add follower for user $user->id");
|
error_log("NewContent::postForUser Failed to add follower for user $user->id");
|
||||||
|
@ -324,25 +323,20 @@ 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(
|
$removedId = \Federator\DIO\Followers::removeFollow($dbh, $followerId, $user->id);
|
||||||
$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 '
|
error_log("NewContent::postForUser Failed to remove follow for user $user->id");
|
||||||
. $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':
|
||||||
|
@ -350,10 +344,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
|
error_log("NewContent::postForUser Error in Undo Like/Dislike for user $user->id, targetId is not a string");
|
||||||
. ', targetId is not a string');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
@ -361,6 +355,7 @@ 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();
|
||||||
|
@ -371,8 +366,7 @@ 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
|
error_log("NewContent::postForUser Error in Undo Article. $recipient->id, recipient ID is not valid");
|
||||||
. ', recipient ID is not valid');
|
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
$targetUrl = ltrim($idPart, '@');
|
$targetUrl = ltrim($idPart, '@');
|
||||||
|
@ -381,18 +375,16 @@ 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 '
|
error_log("NewContent::postForUser Error in Undo Article for recipient $recipient->id, object is not an Article");
|
||||||
. $recipient->id . ', object is not an Article');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} elseif (is_string($object)) {
|
} else if (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
|
error_log("NewContent::postForUser Error in Undo for recipient $recipient->id, object is not a string or object");
|
||||||
. ', object is not a string or object');
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
@ -401,11 +393,10 @@ 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, $type);
|
// \Federator\DIO\Votes::addVote($dbh, $user->id, $targetId, 'like');
|
||||||
// \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
|
error_log("NewContent::postForUser Error in Add Like/Dislike for recipient $recipient->id, targetId is not a string");
|
||||||
. ', targetId is not a string');
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
@ -424,8 +415,7 @@ 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
|
error_log("NewContent::postForUser Error in Create/Update Article. $recipient->id, recipient ID is not valid");
|
||||||
. ', recipient ID is not valid');
|
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
$targetUrl = ltrim($idPart, '@');
|
$targetUrl = ltrim($idPart, '@');
|
||||||
|
@ -434,8 +424,7 @@ 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 '
|
error_log("NewContent::postForUser Error in Create/Update Article for recipient $recipient->id, object is not an Article");
|
||||||
. $recipient->id . ', object is not an Article');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -449,26 +438,117 @@ 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 = \Federator\DIO\Server::sendActivity($dbh, $host, $user, $recipient, $newActivity);
|
$response = self::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: '
|
error_log("NewContent::postForUser Sent activity to $recipient->id with response: " . json_encode($response, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT));
|
||||||
. 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
|
||||||
|
@ -477,4 +557,4 @@ class NewContent implements \Federator\Api\APIInterface
|
||||||
{
|
{
|
||||||
return $this->response;
|
return $this->response;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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)) {
|
||||||
|
|
|
@ -67,6 +67,7 @@ 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;
|
||||||
|
|
|
@ -48,10 +48,10 @@ class WebFinger
|
||||||
$matches = [];
|
$matches = [];
|
||||||
$config = $this->main->getConfig();
|
$config = $this->main->getConfig();
|
||||||
$domain = $config['generic']['externaldomain'];
|
$domain = $config['generic']['externaldomain'];
|
||||||
$sourcedomain = $config['generic']['sourcedomain'];
|
if (preg_match("/^acct:([^@]+)@(.*)$/", $_resource, $matches) != 1 || $matches[2] !== $domain) {
|
||||||
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);
|
||||||
|
|
4
php/federator/cache/cache.php
vendored
4
php/federator/cache/cache.php
vendored
|
@ -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 saveFollowersByUser($user, $followers);
|
public function saveRemoteFollowersOfUser($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 saveFollowingByUser($user, $following);
|
public function saveRemoteFollowingForUser($user, $following);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* save remote posts by user
|
* save remote posts by user
|
||||||
|
|
|
@ -17,17 +17,19 @@ 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 getFollowersByUser($id);
|
public function getRemoteFollowersOfUser($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 getFollowingByUser($id);
|
public function getRemoteFollowingForUser($id);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* get posts by given user
|
* get posts by given user
|
||||||
|
@ -36,6 +38,7 @@ 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);
|
||||||
|
|
|
@ -10,7 +10,7 @@ namespace Federator\Data\ActivityPub\Common;
|
||||||
|
|
||||||
class Collection extends APObject
|
class Collection extends APObject
|
||||||
{
|
{
|
||||||
protected int $totalItems = -1;
|
protected int $totalItems = 0;
|
||||||
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 !== '') {
|
||||||
|
|
|
@ -49,14 +49,15 @@ class Article
|
||||||
*/
|
*/
|
||||||
public static function conditionalConvertToNote($article, $targetUrl)
|
public static function conditionalConvertToNote($article, $targetUrl)
|
||||||
{
|
{
|
||||||
$supportFile = file_get_contents($_SERVER['DOCUMENT_ROOT'] . '../formatsupport.json');
|
$supportFile = file_get_contents(PROJECT_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 (!isset($supportlist['activitypub']['article']) ||
|
if (
|
||||||
|
!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)
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -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 addUserToDB($dbh, $user, $_user)
|
protected static function addLocalUser($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::addUserToDB($dbh, $user, $_user);
|
self::addLocalUser($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`, ';
|
$sql = 'select `id`, `url`, `name`, `publickey`, `summary`, `type`, `inboxurl`, `sharedinboxurl`, `followersurl`,';
|
||||||
$sql .= '`followersurl`, `followingurl`, `publickeyid`, `outboxurl`';
|
$sql .= ' `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,13 +184,11 @@ 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 '
|
throw new \Federator\Exceptions\ServerError("FedUser::getUserByName Failed to fetch webfinger for " . $_name);
|
||||||
. $_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 '
|
throw new \Federator\Exceptions\ServerError("FedUser::getUserByName Failed to decode webfinger for " . $_name);
|
||||||
. $_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'])) {
|
||||||
|
@ -202,268 +200,43 @@ class FedUser
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!isset($remoteURL)) {
|
if (!isset($remoteURL)) {
|
||||||
throw new \Federator\Exceptions\ServerError('FedUser::getUserByName Failed to find self link '
|
throw new \Federator\Exceptions\ServerError("FedUser::getUserByName Failed to find self link in webfinger for " . $_name);
|
||||||
. 'in webfinger for ' . $_name);
|
|
||||||
}
|
}
|
||||||
} else {
|
// fetch the user
|
||||||
$remoteURL = $_name;
|
$headers = ['Accept: application/activity+json'];
|
||||||
}
|
[$response, $info] = \Federator\Main::getFromRemote($remoteURL, $headers);
|
||||||
// fetch the user
|
if ($info['http_code'] != 200) {
|
||||||
$headers = ['Accept: application/activity+json'];
|
throw new \Federator\Exceptions\ServerError("FedUser::getUserByName Failed to fetch user from remoteUrl for " . $_name);
|
||||||
[$response, $info] = \Federator\Main::getFromRemote($remoteURL, $headers);
|
|
||||||
if ($info['http_code'] != 200) {
|
|
||||||
throw new \Federator\Exceptions\ServerError('FedUser::getUserByName Failed to fetch user from '
|
|
||||||
. 'remoteUrl for ' . $_name);
|
|
||||||
}
|
|
||||||
$r = json_decode($response, true);
|
|
||||||
if ($r === false || $r === null || !is_array($r)) {
|
|
||||||
throw new \Federator\Exceptions\ServerError('FedUser::getUserByName Failed to decode user for '
|
|
||||||
. $_name);
|
|
||||||
}
|
|
||||||
$r['publicKeyId'] = $r['publicKey']['id'];
|
|
||||||
$r['publicKey'] = $r['publicKey']['publicKeyPem'];
|
|
||||||
if (isset($r['endpoints'])) {
|
|
||||||
if (isset($r['endpoints']['sharedInbox'])) {
|
|
||||||
$r['sharedInbox'] = $r['endpoints']['sharedInbox'];
|
|
||||||
}
|
}
|
||||||
|
$r = json_decode($response, true);
|
||||||
|
if ($r === false || $r === null || !is_array($r)) {
|
||||||
|
throw new \Federator\Exceptions\ServerError("FedUser::getUserByName Failed to decode user for " . $_name);
|
||||||
|
}
|
||||||
|
$r['publicKeyId'] = $r['publicKey']['id'];
|
||||||
|
$r['publicKey'] = $r['publicKey']['publicKeyPem'];
|
||||||
|
if (isset($r['endpoints'])) {
|
||||||
|
if (isset($r['endpoints']['sharedInbox'])) {
|
||||||
|
$r['sharedInbox'] = $r['endpoints']['sharedInbox'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$r['actorURL'] = $remoteURL;
|
||||||
|
$data = json_encode($r);
|
||||||
|
if ($data === false) {
|
||||||
|
throw new \Federator\Exceptions\ServerError("FedUser::getUserByName Failed to encode userdata " . $_name);
|
||||||
|
}
|
||||||
|
$user = \Federator\Data\FedUser::createFromJson($data);
|
||||||
}
|
}
|
||||||
$r['actorURL'] = $remoteURL;
|
|
||||||
$data = json_encode($r);
|
|
||||||
if ($data === false) {
|
|
||||||
throw new \Federator\Exceptions\ServerError('FedUser::getUserByName Failed to encode userdata '
|
|
||||||
. $_name);
|
|
||||||
}
|
|
||||||
$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::addUserToDB($dbh, $user, $_name);
|
self::addLocalUser($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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,7 +30,7 @@ class Followers
|
||||||
{
|
{
|
||||||
// ask cache
|
// ask cache
|
||||||
if ($cache !== null) {
|
if ($cache !== null) {
|
||||||
$followers = $cache->getFollowersByUser($id);
|
$followers = $cache->getRemoteFollowersOfUser($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->getFollowersByUser($id);
|
$followers = $connector->getRemoteFollowersOfUser($id);
|
||||||
if ($followers === false) {
|
if ($followers === false) {
|
||||||
$followers = [];
|
$followers = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// save followers to cache
|
// save followers to cache
|
||||||
if ($cache !== null) {
|
if ($cache !== null) {
|
||||||
$cache->saveFollowersByUser($id, $followers);
|
$cache->saveRemoteFollowersOfUser($id, $followers);
|
||||||
}
|
}
|
||||||
return $followers;
|
return $followers;
|
||||||
}
|
}
|
||||||
|
@ -91,11 +91,12 @@ 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->getFollowingByUser($id);
|
$following = $cache->getRemoteFollowingForUser($id);
|
||||||
if ($following !== false) {
|
if ($following !== false) {
|
||||||
return $following;
|
return $following;
|
||||||
}
|
}
|
||||||
|
@ -104,9 +105,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);
|
||||||
|
@ -122,7 +123,7 @@ class Followers
|
||||||
$cache,
|
$cache,
|
||||||
);
|
);
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
error_log('Followers::getFollowingByUser Exception: ' . $e->getMessage());
|
error_log("Followers::getFollowingForUser 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) {
|
||||||
|
@ -132,14 +133,14 @@ class Followers
|
||||||
|
|
||||||
if ($following === []) {
|
if ($following === []) {
|
||||||
// ask connector for user-id
|
// ask connector for user-id
|
||||||
$following = $connector->getFollowingByUser($id);
|
$following = $connector->getRemoteFollowingForUser($id);
|
||||||
if ($following === false) {
|
if ($following === false) {
|
||||||
$following = [];
|
$following = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// save posts to DB
|
// save posts to DB
|
||||||
if ($cache !== null) {
|
if ($cache !== null) {
|
||||||
$cache->saveFollowingByUser($id, $following);
|
$cache->saveRemoteFollowingForUser($id, $following);
|
||||||
}
|
}
|
||||||
return $following;
|
return $following;
|
||||||
}
|
}
|
||||||
|
@ -165,7 +166,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();
|
||||||
|
@ -183,7 +184,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) {
|
||||||
|
@ -207,7 +208,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,
|
||||||
|
@ -288,8 +289,7 @@ 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"'
|
$signatureHeader = 'keyId="' . $keyId . '",algorithm="rsa-sha256",headers="(request-target) host date digest",signature="' . $signature_b64 . '"';
|
||||||
. ',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,10 +364,9 @@ 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'
|
throw new \Federator\Exceptions\ServerError("Followers::addFollow Failed to prepare id-check statement");
|
||||||
. '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();
|
||||||
|
@ -381,9 +380,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
|
||||||
|
@ -404,9 +403,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();
|
||||||
|
@ -422,10 +421,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::addExternalFollow Failed to prepare insert '
|
throw new \Federator\Exceptions\ServerError("Followers::addExternalFollow Failed to prepare insert statement");
|
||||||
. '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;
|
||||||
|
@ -449,10 +447,9 @@ 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'
|
throw new \Federator\Exceptions\ServerError("Followers::generateNewFollowId Failed to prepare id-check statement");
|
||||||
. ' 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();
|
||||||
|
@ -479,7 +476,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) {
|
||||||
|
@ -498,10 +495,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::removeFollow Failed to prepare select '
|
throw new \Federator\Exceptions\ServerError("Followers::removeFollow Failed to prepare select statement");
|
||||||
. '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();
|
||||||
|
@ -515,10 +511,9 @@ 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 '
|
throw new \Federator\Exceptions\ServerError("Followers::removeFollow Failed to prepare delete statement");
|
||||||
. '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();
|
||||||
|
|
|
@ -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,8 +156,7 @@ 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 '
|
$sql = 'SELECT `id`, `url`, `user_id`, `actor`, `type`, `object`, `to`, `cc`, unix_timestamp(`published`) as published FROM posts WHERE user_id = ?';
|
||||||
. 'published FROM posts WHERE user_id = ?';
|
|
||||||
$params = [$userId];
|
$params = [$userId];
|
||||||
$types = 's';
|
$types = 's';
|
||||||
if ($min > 0) {
|
if ($min > 0) {
|
||||||
|
@ -267,7 +266,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,
|
||||||
|
@ -298,7 +297,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();
|
||||||
|
@ -322,7 +321,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();
|
||||||
|
@ -330,7 +329,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();
|
||||||
|
|
|
@ -1,108 +0,0 @@
|
||||||
<?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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -17,8 +17,7 @@ class Stats
|
||||||
/**
|
/**
|
||||||
* get remote stats
|
* get remote stats
|
||||||
*
|
*
|
||||||
* @param \Federator\Main $main
|
* @param \Federator\Main $main main instance
|
||||||
* main instance
|
|
||||||
* @return \Federator\Data\Stats
|
* @return \Federator\Data\Stats
|
||||||
*/
|
*/
|
||||||
public static function getStats($main)
|
public static function getStats($main)
|
||||||
|
|
|
@ -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,6 +170,7 @@ 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);
|
||||||
|
@ -182,9 +183,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,
|
||||||
|
@ -204,15 +205,18 @@ 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;
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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,19 +60,11 @@ 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\DIO\FedUser::inboxForUser(
|
\Federator\Api\FedUsers\Inbox::postForUser($this->dbh, $this->connector, $this->cache, $user, $recipientId, $inboxActivity);
|
||||||
$this,
|
|
||||||
$this->dbh,
|
|
||||||
$this->connector,
|
|
||||||
$this->cache,
|
|
||||||
$user,
|
|
||||||
$recipientId,
|
|
||||||
$inboxActivity
|
|
||||||
);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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,23 +61,14 @@ 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(
|
\Federator\Api\V1\NewContent::postForUser($this->dbh, $this->connector, $this->cache, $ourUrl, $user, $recipientId, $activity, $articleId);
|
||||||
$this->dbh,
|
|
||||||
$this->connector,
|
|
||||||
$this->cache,
|
|
||||||
$ourUrl,
|
|
||||||
$user,
|
|
||||||
$recipientId,
|
|
||||||
$activity,
|
|
||||||
$articleId
|
|
||||||
);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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,12 +92,9 @@ class Language
|
||||||
}
|
}
|
||||||
if (! isset($this->lang[$group])) {
|
if (! isset($this->lang[$group])) {
|
||||||
$l = [];
|
$l = [];
|
||||||
$root = $_SERVER['DOCUMENT_ROOT'];
|
$root = PROJECT_ROOT;
|
||||||
if ($root === '') {
|
if (@file_exists($root . '/lang/federator/' . $this->uselang . "/$group.inc")) {
|
||||||
$root = '.';
|
require($root . '/lang/federator/' . $this->uselang . "/$group.inc");
|
||||||
}
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -107,15 +104,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 = $_SERVER['DOCUMENT_ROOT'] . '/../';
|
$basedir = PROJECT_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 ">>$group:$key<<";
|
return ">>$group:$key<<";
|
||||||
|
@ -132,7 +129,7 @@ class Language
|
||||||
{
|
{
|
||||||
if (! isset($this->lang[$group])) {
|
if (! isset($this->lang[$group])) {
|
||||||
$l = [];
|
$l = [];
|
||||||
require_once($_SERVER['DOCUMENT_ROOT'] . '/../lang/' . $this->uselang . '/' . $group . '.inc');
|
require_once(PROJECT_ROOT . '/lang/' . $this->uselang . "/$group.inc");
|
||||||
$this->lang[$group] = $l;
|
$this->lang[$group] = $l;
|
||||||
}
|
}
|
||||||
// @phan-suppress-next-line PhanPartialTypeMismatchReturn
|
// @phan-suppress-next-line PhanPartialTypeMismatchReturn
|
||||||
|
@ -288,7 +285,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'])) {
|
||||||
|
|
|
@ -20,43 +20,37 @@ class Main
|
||||||
*
|
*
|
||||||
* @var Cache\Cache $cache
|
* @var Cache\Cache $cache
|
||||||
*/
|
*/
|
||||||
protected $cache = null;
|
protected $cache;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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
|
||||||
*
|
*
|
||||||
|
@ -84,9 +78,9 @@ class Main
|
||||||
*/
|
*/
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
require_once($_SERVER['DOCUMENT_ROOT'] . '../vendor/autoload.php');
|
require_once(PROJECT_ROOT . '/vendor/autoload.php');
|
||||||
$this->responseCode = 200;
|
$this->responseCode = 200;
|
||||||
$rootDir = $_SERVER['DOCUMENT_ROOT'] . '../';
|
$rootDir = PROJECT_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;
|
||||||
|
@ -105,7 +99,7 @@ class Main
|
||||||
public static function extractFromURI($param, $fallback = '')
|
public static function extractFromURI($param, $fallback = '')
|
||||||
{
|
{
|
||||||
$uri = $_SERVER['REQUEST_URI'];
|
$uri = $_SERVER['REQUEST_URI'];
|
||||||
$params = substr($uri, (int)(strpos($uri, '?') + 1));
|
$params = substr($uri, (int) (strpos($uri, '?') + 1));
|
||||||
$params = explode('&', $params);
|
$params = explode('&', $params);
|
||||||
foreach ($params as $p) {
|
foreach ($params as $p) {
|
||||||
$tokens = explode('=', $p);
|
$tokens = explode('=', $p);
|
||||||
|
@ -151,22 +145,20 @@ class Main
|
||||||
{
|
{
|
||||||
return $this->cache;
|
return $this->cache;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* get connector
|
* get connector
|
||||||
*
|
*
|
||||||
* @return \Federator\Connector\Connector
|
* @return \Federator\Connector\Connector
|
||||||
*/
|
*/
|
||||||
public function getConnector()
|
public function getConnector()
|
||||||
{
|
{
|
||||||
return $this->connector;
|
return $this->connector;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* get host (f.e. https://contentnation.net)
|
* get host (f.e. https://contentnation.net)
|
||||||
*
|
*
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
public function getHost()
|
public function getHost()
|
||||||
{
|
{
|
||||||
return $this->host;
|
return $this->host;
|
||||||
|
@ -197,7 +189,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 = $_SERVER['DOCUMENT_ROOT'] . '../plugins/federator/';
|
$basepath = PROJECT_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);
|
||||||
|
@ -218,7 +210,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'],
|
||||||
|
@ -240,10 +232,10 @@ class Main
|
||||||
*/
|
*/
|
||||||
public function renderTemplate($template, $data)
|
public function renderTemplate($template, $data)
|
||||||
{
|
{
|
||||||
|
$rootDir = PROJECT_ROOT . '/';
|
||||||
$smarty = new \Smarty\Smarty();
|
$smarty = new \Smarty\Smarty();
|
||||||
$root = $_SERVER['DOCUMENT_ROOT'];
|
$smarty->setCompileDir($rootDir . $this->config['templates']['compiledir']);
|
||||||
$smarty->setCompileDir($root . $this->config['templates']['compiledir']);
|
$smarty->setTemplateDir((string) realpath($rootDir . $this->config['templates']['path']));
|
||||||
$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) {
|
||||||
|
@ -269,6 +261,9 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -279,6 +274,9 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -22,16 +22,15 @@ 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) {
|
||||||
$root = $_SERVER['DOCUMENT_ROOT'];
|
include PROJECT_ROOT . '/php/' . str_replace("\\", "/", strtolower($className)) . '.php';
|
||||||
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
|
||||||
$_SERVER['DOCUMENT_ROOT'] = realpath('../../htdocs') . '/';
|
define('PROJECT_ROOT', dirname(__DIR__, 2));
|
||||||
$main = new \Federator\Main();
|
$main = new \Federator\Main();
|
||||||
switch ($argv[1]) {
|
switch ($argv[1]) {
|
||||||
case 'dbupgrade':
|
case 'dbupgrade':
|
||||||
|
@ -71,7 +70,7 @@ class Maintenance
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
echo "current version: $version\n";
|
echo "current version: $version\n";
|
||||||
$root = $_SERVER['DOCUMENT_ROOT'] . '../';
|
$root = PROJECT_ROOT . '/';
|
||||||
$updateFolder = opendir($root . 'sql');
|
$updateFolder = opendir($root . 'sql');
|
||||||
if ($updateFolder === false) {
|
if ($updateFolder === false) {
|
||||||
die();
|
die();
|
||||||
|
|
|
@ -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,8 +143,7 @@ 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\DIO\FedUser::inboxForUser(
|
\Federator\Api\FedUsers\Inbox::postForUser(
|
||||||
$api,
|
|
||||||
$dbh,
|
$dbh,
|
||||||
$api->getConnector(),
|
$api->getConnector(),
|
||||||
null,
|
null,
|
||||||
|
|
|
@ -1,18 +1,14 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
define('PROJECT_ROOT', dirname(__DIR__, 3));
|
define('PROJECT_ROOT', dirname(__DIR__, 3));
|
||||||
require_once PROJECT_ROOT . '/vendor/autoload.php';
|
|
||||||
$_SERVER['DOCUMENT_ROOT'] = PROJECT_ROOT . '/htdocs/';
|
|
||||||
|
|
||||||
spl_autoload_register(static function (string $className) {
|
require_once PROJECT_ROOT . '/vendor/autoload.php';
|
||||||
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?password-encoding=u',
|
'redis://%s:%s@%s:%d',
|
||||||
urlencode($config['username']),
|
urlencode($config['username']),
|
||||||
urlencode($config['password']),
|
urlencode($config['password']),
|
||||||
$config['host'],
|
$config['host'],
|
||||||
|
@ -24,4 +20,4 @@ $redisUrl = sprintf(
|
||||||
$worker = new \Resque_Worker(['inbox']);
|
$worker = new \Resque_Worker(['inbox']);
|
||||||
|
|
||||||
fwrite(STDOUT, "*** Starting worker for inbox queue\n");
|
fwrite(STDOUT, "*** Starting worker for inbox queue\n");
|
||||||
$worker->work(10); // 10 seconds interval
|
$worker->work(10); // 10 seconds interval
|
|
@ -41,7 +41,7 @@ class ContentNation implements Connector
|
||||||
*/
|
*/
|
||||||
public function __construct($main)
|
public function __construct($main)
|
||||||
{
|
{
|
||||||
$config = parse_ini_file($_SERVER['DOCUMENT_ROOT'] . '../contentnation.ini', true);
|
$config = parse_ini_file(PROJECT_ROOT . '/contentnation.ini', true);
|
||||||
if ($config !== false) {
|
if ($config !== false) {
|
||||||
$this->config = $config;
|
$this->config = $config;
|
||||||
}
|
}
|
||||||
|
@ -53,28 +53,26 @@ class ContentNation implements Connector
|
||||||
/**
|
/**
|
||||||
* get followers of given user
|
* get followers of given user
|
||||||
*
|
*
|
||||||
* @param string $userId user id @unused-param
|
* @param string $userId user id
|
||||||
* @return \Federator\Data\FedUser[]|false
|
* @return \Federator\Data\FedUser[]|false
|
||||||
*/
|
*/
|
||||||
public function getFollowersByUser($userId)
|
public function getRemoteFollowersOfUser($userId)
|
||||||
{
|
{
|
||||||
// ContentNation does not export followers
|
// todo implement queue for this
|
||||||
/*
|
|
||||||
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::getFollowersByUser error retrieving followers for userId: $userId . Error: "
|
error_log("ContentNation::getRemoteFollowersOfUser error retrieving followers for userId: $userId . Error: " . json_encode($info));
|
||||||
. 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;
|
||||||
}
|
}
|
||||||
|
@ -82,13 +80,13 @@ class ContentNation implements Connector
|
||||||
/**
|
/**
|
||||||
* get following of given user
|
* get following of given user
|
||||||
*
|
*
|
||||||
* @param string $userId user id @unused-param
|
* @param string $userId user id
|
||||||
|
|
||||||
* @return \Federator\Data\FedUser[]|false
|
* @return \Federator\Data\FedUser[]|false
|
||||||
*/
|
*/
|
||||||
public function getFollowingByUser($userId)
|
public function getRemoteFollowingForUser($userId)
|
||||||
{
|
{
|
||||||
// ContentNation does not export Following for user
|
// todo implement queue for this
|
||||||
/*
|
|
||||||
if (preg_match("#^([^@]+)@([^/]+)#", $userId, $matches) == 1) {
|
if (preg_match("#^([^@]+)@([^/]+)#", $userId, $matches) == 1) {
|
||||||
$userId = $matches[1];
|
$userId = $matches[1];
|
||||||
}
|
}
|
||||||
|
@ -96,16 +94,15 @@ 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_log("ContentNation::getRemoteFollowingForUser error retrieving following for userId: $userId . Error: " . json_encode($info));
|
||||||
. '. 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;
|
||||||
}*/
|
}
|
||||||
$following = [];
|
$followers = [];
|
||||||
return $following;
|
return $followers;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -132,8 +129,7 @@ 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_log("ContentNation::getRemotePostsByUser error retrieving activities for userId: $userId . Error: " . json_encode($info));
|
||||||
. '. Error: ' . json_encode($info));
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
$r = json_decode($response, true);
|
$r = json_decode($response, true);
|
||||||
|
@ -215,8 +211,7 @@ 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'] . '/'
|
$commentJson['id'] = $ourUrl . '/' . $activity['articleOwnerName'] . '/' . $activity['articleName'] . '#' . $activity['id'];
|
||||||
. $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");
|
||||||
|
@ -226,14 +221,11 @@ 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'] . '/'
|
$note->setInReplyTo($ourUrl . '/' . $activity['articleOwnerName'] . '/' . $activity['articleName']);
|
||||||
. $activity['articleName']);
|
|
||||||
} else {
|
} else {
|
||||||
$note->setInReplyTo($ourUrl . '/' . $activity['articleOwnerName'] . '/'
|
$note->setInReplyTo($ourUrl . '/' . $activity['articleOwnerName'] . '/' . $activity['articleName'] . "#" . $commentJson['parent']);
|
||||||
. $activity['articleName'] . "#" . $commentJson['parent']);
|
|
||||||
}
|
}
|
||||||
$url = $ourUrl . '/' . $activity['articleOwnerName'] . '/' . $activity['articleName']
|
$url = $ourUrl . '/' . $activity['articleOwnerName'] . '/' . $activity['articleName'] . '#' . $activity['id'];
|
||||||
. '#' . $activity['id'];
|
|
||||||
$create->setURL($url);
|
$create->setURL($url);
|
||||||
$create->setID($url);
|
$create->setID($url);
|
||||||
$create->setObject($note);
|
$create->setObject($note);
|
||||||
|
@ -301,10 +293,16 @@ class ContentNation implements Connector
|
||||||
public function getRemoteUserByName(string $_name)
|
public function getRemoteUserByName(string $_name)
|
||||||
{
|
{
|
||||||
// validate name
|
// validate name
|
||||||
if (preg_match("/^[a-zA-Z0-9\._\-]+$/", $_name) != 1) {
|
if (preg_match("/^[a-zA-Z@0-9\._\-]+$/", $_name) != 1) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
$remoteURL = $this->service . '/api/users/info?user=' . urlencode($_name);
|
// make sure we only get name part, without domain
|
||||||
|
if (preg_match("#^([^@]+)@([^/]+)#", $_name, $matches) == 1) {
|
||||||
|
$name = $matches[1];
|
||||||
|
} else {
|
||||||
|
$name = $_name;
|
||||||
|
}
|
||||||
|
$remoteURL = $this->service . '/api/users/info?user=' . urlencode($name);
|
||||||
$headers = ['Accept: application/json'];
|
$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) {
|
||||||
|
@ -341,7 +339,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-Z0-9_\-]+$/", $_user) != 1) {
|
if (preg_match("/^[a-zA-Z@0-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);
|
||||||
|
@ -430,8 +428,7 @@ 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: '
|
error_log("ContentNation::jsonToActivity unknown vote value: {$jsonData['object']['vote']['value']}");
|
||||||
. $jsonData['object']['vote']['value']);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
$ap['object']['object'] = self::generateObjectJson($ourUrl, $jsonData);
|
$ap['object']['object'] = self::generateObjectJson($ourUrl, $jsonData);
|
||||||
|
@ -441,7 +438,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']);
|
||||||
|
@ -450,8 +447,7 @@ 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: '
|
throw new \InvalidArgumentException("ContentNation::jsonToActivity Unsupported type: {$jsonData['type']}");
|
||||||
. $jsonData['type']);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Handle specific fields based on the type
|
// Handle specific fields based on the type
|
||||||
|
@ -484,7 +480,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']);
|
||||||
|
@ -517,7 +513,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']);
|
||||||
|
@ -539,18 +535,17 @@ 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: '
|
error_log("ContentNation::jsonToActivity unknown vote value: {$jsonData['object']['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();
|
||||||
|
@ -564,8 +559,7 @@ 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: '
|
throw new \InvalidArgumentException("ContentNation::jsonToActivity Unsupported object type: {$jsonData['type']}");
|
||||||
. $jsonData['type']);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -587,7 +581,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;
|
||||||
|
@ -595,13 +589,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) {
|
||||||
|
@ -625,14 +619,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,
|
||||||
|
@ -648,26 +642,24 @@ 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
|
$returnJson['inReplyTo'] = $ourUrl . "/" . $articleOwnerName . "/" . $articleName . "#" . $jsonData['object']['inReplyTo']['id'];
|
||||||
. '#' . $jsonData['object']['inReplyTo']['id'];
|
|
||||||
} else {
|
} else {
|
||||||
error_log('ContentNation::generateObjectJson for comment - unknown inReplyTo type: '
|
error_log("ContentNation::generateObjectJson for comment - unknown inReplyTo type: {$replyType}");
|
||||||
. $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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -686,16 +678,10 @@ 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(
|
$jsonData = self::activityToJson($this->main->getDatabase(), $this->service, $activity, $targetUrl, $targetRequestType);
|
||||||
$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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -723,7 +709,7 @@ class ContentNation implements Connector
|
||||||
"date: {$date}\n" .
|
"date: {$date}\n" .
|
||||||
"digest: {$digest}";
|
"digest: {$digest}";
|
||||||
|
|
||||||
$pKeyPath = $_SERVER['DOCUMENT_ROOT'] . '../' . $this->main->getConfig()['keys']['federatorPrivateKeyPath'];
|
$pKeyPath = PROJECT_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);
|
||||||
|
@ -739,8 +725,7 @@ 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="'
|
$signatureHeader = 'algorithm="rsa-sha256",headers="(request-target) host date digest",signature="' . $signature_b64 . '"';
|
||||||
. $signature_b64 . '"';
|
|
||||||
|
|
||||||
$ch = curl_init($targetUrl);
|
$ch = curl_init($targetUrl);
|
||||||
if ($ch === false) {
|
if ($ch === false) {
|
||||||
|
@ -765,8 +750,7 @@ 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: '
|
throw new \Exception("ContentNation::sendActivity Unsupported target request type: $targetRequestType");
|
||||||
. $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);
|
||||||
|
@ -774,11 +758,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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -795,7 +779,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, $activity, &$targetUrl, &$targetRequestType)
|
private function activityToJson($dbh, $serviceUrl, \Federator\Data\ActivityPub\Common\Activity $activity, string &$targetUrl, string &$targetRequestType)
|
||||||
{
|
{
|
||||||
$type = strtolower($activity->getType());
|
$type = strtolower($activity->getType());
|
||||||
$targetRequestType = 'post'; // Default request type
|
$targetRequestType = 'post'; // Default request type
|
||||||
|
@ -807,14 +791,12 @@ 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'
|
error_log("ContentNation::activityToJson Failed to get original article ID for create/update activity");
|
||||||
.' 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: '
|
error_log("ContentNation::activityToJson Unsupported create/update object type: {$objType}");
|
||||||
.$objType);
|
|
||||||
break;
|
break;
|
||||||
case 'note':
|
case 'note':
|
||||||
$targetUrl = $serviceUrl . '/api/article/' . $articleId . '/comment';
|
$targetUrl = $serviceUrl . '/api/article/' . $articleId . '/comment';
|
||||||
|
@ -834,8 +816,7 @@ class ContentNation implements Connector
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
error_log('ContentNation::activityToJson Unsupported target type for comment with id: '
|
error_log("ContentNation::activityToJson Unsupported target type for comment with id: " . $activity->getID() . " Type: " . gettype($target));
|
||||||
. $activity->getID() . ' Type: ' . gettype($target));
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return [
|
return [
|
||||||
|
@ -846,8 +827,7 @@ class ContentNation implements Connector
|
||||||
'comment' => $object->getContent(),
|
'comment' => $object->getContent(),
|
||||||
];
|
];
|
||||||
default:
|
default:
|
||||||
error_log('ContentNation::activityToJson Unsupported create/update object type: '
|
error_log("ContentNation::activityToJson Unsupported create/update object type: {$objType}");
|
||||||
. $objType);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -856,14 +836,13 @@ 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: '
|
error_log("ContentNation::activityToJson no profileName or domain found for object url: " . $profileUrl);
|
||||||
. $profileUrl);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
$receiver = $receiverName;
|
$receiver = $receiverName;
|
||||||
|
@ -875,12 +854,11 @@ class ContentNation implements Connector
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
error_log('ContentNation::activityToJson get user by name: ' . $receiver . '. Exception: '
|
error_log("ContentNation::activityToJson get user by name: " . $receiver . ". Exception: " . $e->getMessage());
|
||||||
. $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';
|
||||||
|
@ -903,7 +881,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';
|
||||||
|
@ -922,8 +900,7 @@ class ContentNation implements Connector
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
error_log('ContentNation::activityToJson Unsupported target type for vote with id: '
|
error_log("ContentNation::activityToJson Unsupported target type for vote with id: " . $activity->getID() . " Type: " . gettype($target));
|
||||||
. $activity->getID() . ' Type: ' . gettype($target));
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
$targetUrl = $serviceUrl . '/api/article/' . $articleId . '/vote';
|
$targetUrl = $serviceUrl . '/api/article/' . $articleId . '/vote';
|
||||||
|
@ -942,15 +919,13 @@ 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: '
|
error_log("ContentNation::activityToJson Invalid profile URL: " . json_encode($profileUrl));
|
||||||
. 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'
|
error_log("ContentNation::activityToJson no profileName or domain found for object url: " . $profileUrl);
|
||||||
. ' url: ' . $profileUrl);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
$receiver = $receiverName;
|
$receiver = $receiverName;
|
||||||
|
@ -962,12 +937,11 @@ class ContentNation implements Connector
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
error_log('ContentNation::activityToJson get user by name: ' . $receiver
|
error_log("ContentNation::activityToJson get user by name: " . $receiver . ". Exception: " . $e->getMessage());
|
||||||
. '. 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';
|
||||||
|
@ -996,8 +970,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 '
|
error_log("ContentNation::activityToJson Failed to get original article ID for undo vote activity");
|
||||||
. 'for undo vote activity');
|
|
||||||
}
|
}
|
||||||
$activityType = 'vote';
|
$activityType = 'vote';
|
||||||
$inReplyTo = $object->getInReplyTo();
|
$inReplyTo = $object->getInReplyTo();
|
||||||
|
@ -1015,8 +988,7 @@ class ContentNation implements Connector
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
error_log('ContentNation::activityToJson Unsupported target type for undo '
|
error_log("ContentNation::activityToJson Unsupported target type for undo vote with id: " . $activity->getID() . " Type: " . gettype($target));
|
||||||
. 'vote with id: ' . $activity->getID() . " Type: " . gettype($target));
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
$targetUrl = $serviceUrl . '/api/article/' . $articleId . '/vote';
|
$targetUrl = $serviceUrl . '/api/article/' . $articleId . '/vote';
|
||||||
|
@ -1028,17 +1000,16 @@ 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: '
|
error_log("ContentNation::activityToJson Unsupported create/update object type: {$objType}");
|
||||||
. $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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1057,11 +1028,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
|
||||||
|
@ -1071,11 +1042,11 @@ class ContentNation implements Connector
|
||||||
$signature = base64_decode($signatureParts['signature']);
|
$signature = base64_decode($signatureParts['signature']);
|
||||||
$signedHeaders = explode(' ', $signatureParts['headers']);
|
$signedHeaders = explode(' ', $signatureParts['headers']);
|
||||||
|
|
||||||
$pKeyPath = $_SERVER['DOCUMENT_ROOT'] . '../' . $this->config['keys']['publicKeyPath'];
|
$pKeyPath = PROJECT_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
|
||||||
|
@ -1089,7 +1060,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);
|
||||||
|
@ -1102,9 +1073,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.";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1119,5 +1090,6 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 getFollowersByUser($userId)
|
public function getRemoteFollowersOfUser($userId)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -34,9 +34,10 @@ 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 getFollowingByUser($id)
|
public function getRemoteFollowingForUser($id)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -77,8 +78,7 @@ 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,5 +147,6 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,12 +53,11 @@ class RedisCache implements Cache
|
||||||
*/
|
*/
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$config = parse_ini_file('../rediscache.ini');
|
$config = parse_ini_file(PROJECT_ROOT . '/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)
|
$this->publicKeyPemTTL = array_key_exists('publickeypemttl', $config) ? intval($config['publickeypemttl'], 10) : 3600;
|
||||||
? intval($config['publickeypemttl'], 10) : 3600;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -103,9 +102,9 @@ class RedisCache implements Cache
|
||||||
|
|
||||||
* @return \Federator\Data\FedUser[]|false
|
* @return \Federator\Data\FedUser[]|false
|
||||||
*/
|
*/
|
||||||
public function getFollowersByUser($id)
|
public function getRemoteFollowersOfUser($id)
|
||||||
{
|
{
|
||||||
error_log("rediscache::getFollowersByUser not implemented");
|
error_log("rediscache::getRemoteFollowersOfUser not implemented");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -116,9 +115,9 @@ class RedisCache implements Cache
|
||||||
|
|
||||||
* @return \Federator\Data\FedUser[]|false
|
* @return \Federator\Data\FedUser[]|false
|
||||||
*/
|
*/
|
||||||
public function getFollowingByUser($id)
|
public function getRemoteFollowingForUser($id)
|
||||||
{
|
{
|
||||||
error_log("rediscache::getFollowingByUser not implemented");
|
error_log("rediscache::getRemoteFollowingForUser not implemented");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -254,9 +253,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 saveFollowersByUser($user, $followers)
|
public function saveRemoteFollowersOfUser($user, $followers)
|
||||||
{
|
{
|
||||||
error_log("rediscache::saveFollowersByUser not implemented");
|
error_log("rediscache::saveRemoteFollowersOfUser not implemented");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -266,9 +265,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 saveFollowingByUser($user, $following)
|
public function saveRemoteFollowingForUser($user, $following)
|
||||||
{
|
{
|
||||||
error_log("rediscache::saveFollowingByUser not implemented");
|
error_log("rediscache::saveRemoteFollowingForUser not implemented");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -301,7 +300,6 @@ 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
|
||||||
*
|
*
|
||||||
|
|
|
@ -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
|
||||||
- [X] ap outbox
|
- [ ] ap outbox
|
||||||
- [X] ap inbox
|
- [ ] 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
|
||||||
- [X] callback from service to add new input
|
- [ ] callback from service to add new input
|
||||||
|
|
|
@ -1,2 +0,0 @@
|
||||||
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";
|
|
|
@ -1,2 +0,0 @@
|
||||||
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";
|
|
|
@ -1,3 +0,0 @@
|
||||||
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");
|
|
|
@ -64,26 +64,26 @@
|
||||||
"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://{$sourcedomain}/@{$username}",
|
"url":"https://{$fqdn}/@{$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://{$sourcedomain}/{$username}",
|
"owner":"https://{$fqdn}/{$username}",
|
||||||
"publicKeyPem":"{$publickey}"
|
"publicKeyPem":"{$publickey}"
|
||||||
{rdelim},
|
{rdelim},
|
||||||
"tag":[],
|
"tag":[],
|
||||||
"attachment":[
|
"attachment":[
|
||||||
{if $type==='group'}{ldelim}
|
{if $type==='group'}{ldelim}
|
||||||
"type":"PropertyValue",
|
"type":"PropertyValue",
|
||||||
"name":"website",
|
"name":"website",
|
||||||
"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"
|
"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"
|
||||||
{rdelim}{/if}
|
{rdelim}{/if}
|
||||||
],
|
],
|
||||||
"endpoints":{ldelim}
|
"endpoints":{ldelim}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{ldelim}
|
{ldelim}
|
||||||
"subject": "acct:{$username}@{$sourcedomain}",
|
"subject": "acct:{$username}@{$domain}",
|
||||||
"aliases": [
|
"aliases": [
|
||||||
"https://{$sourcedomain}/@{$username}"
|
"https://{$domain}/@{$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},
|
||||||
|
|
Loading…
Add table
Reference in a new issue