Compare commits
No commits in common. "develop" and "develop" have entirely different histories.
71 changed files with 311 additions and 5440 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -5,7 +5,3 @@ php-docs
|
||||||
.phpdoc
|
.phpdoc
|
||||||
phpdoc
|
phpdoc
|
||||||
html
|
html
|
||||||
/cache
|
|
||||||
contentnation.ini
|
|
||||||
*.pem*
|
|
||||||
composer.phar
|
|
||||||
|
|
|
@ -359,7 +359,6 @@ return [
|
||||||
'directory_list' => [
|
'directory_list' => [
|
||||||
'vendor/phan/phan/src/Phan',
|
'vendor/phan/phan/src/Phan',
|
||||||
'vendor/smarty/smarty/src',
|
'vendor/smarty/smarty/src',
|
||||||
'vendor/resque/php-resque/lib',
|
|
||||||
'php/',
|
'php/',
|
||||||
'plugins',
|
'plugins',
|
||||||
'htdocs',
|
'htdocs',
|
||||||
|
@ -368,7 +367,5 @@ return [
|
||||||
// A list of individual files to include in analysis
|
// A list of individual files to include in analysis
|
||||||
// with a path relative to the root directory of the
|
// with a path relative to the root directory of the
|
||||||
// project.
|
// project.
|
||||||
'file_list' => [
|
'file_list' => [],
|
||||||
'phan-stubs.php',
|
|
||||||
],
|
|
||||||
];
|
];
|
||||||
|
|
|
@ -35,7 +35,7 @@ If the include redis cache is enabled,
|
||||||
- install redis
|
- install redis
|
||||||
- create a users.acl for redis with the content:
|
- create a users.acl for redis with the content:
|
||||||
|
|
||||||
user federator on ~u_* +get +set ~s_* +get +setex ~m_* +get +setex ~publickey_* +get +setex >redis*change*password
|
user federator on ~u_* +get +set ~s_* +get +setex ~m_* +get +setex >redis*change*password
|
||||||
|
|
||||||
- change password in the rediscache.ini to match your given password.
|
- change password in the rediscache.ini to match your given password.
|
||||||
- install the redis plugin from pecl if not provided via your distro
|
- install the redis plugin from pecl if not provided via your distro
|
||||||
|
@ -52,11 +52,9 @@ To configure an apache server, add the following rewrite rules:
|
||||||
RewriteCond expr "%{HTTP:content-type} -strcmatch '*application/activity+json*'"
|
RewriteCond expr "%{HTTP:content-type} -strcmatch '*application/activity+json*'"
|
||||||
RewriteRule ^@(.*)$ /federator.php?_call=fedusers/$1 [L,END]
|
RewriteRule ^@(.*)$ /federator.php?_call=fedusers/$1 [L,END]
|
||||||
RewriteRule ^users/(.*)$ /federator.php?_call=fedusers/$1 [L,END]
|
RewriteRule ^users/(.*)$ /federator.php?_call=fedusers/$1 [L,END]
|
||||||
RewriteRule ^inbox[/]?$ /federator.php?_call=fedusers/inbox [L,END]
|
RewriteRule ^api/federator/(.+)$ federator.php?_call=$1 [L,END]
|
||||||
RewriteRule ^api/federator/(.+)$ /federator.php?_call=$1 [L,END]
|
|
||||||
RewriteRule ^(\.well-known/.*)$ /federator.php?_call=$1 [L,END]
|
RewriteRule ^(\.well-known/.*)$ /federator.php?_call=$1 [L,END]
|
||||||
RewriteRule ^(nodeinfo/2\.[01])$ /federator.php?_call=$1 [L,END]
|
RewriteRule ^(nodeinfo/2\.[01])$ /federator.php?_call=$1 [L,END]
|
||||||
RewriteRule ^([a-zA-Z0-9_-]+.*)$ /federator.php?_call=fedusers/$1 [L,END]
|
|
||||||
</Directory>
|
</Directory>
|
||||||
|
|
||||||
change your document root for the domain you want to use (or default one if using localhost) to the directory you installed it, with the /htdocs at the end. A user should only be able to open that file, not the other data.
|
change your document root for the domain you want to use (or default one if using localhost) to the directory you installed it, with the /htdocs at the end. A user should only be able to open that file, not the other data.
|
||||||
|
|
|
@ -3,8 +3,7 @@
|
||||||
"description": "A federation service",
|
"description": "A federation service",
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"require": {
|
"require": {
|
||||||
"smarty/smarty": "^5.3",
|
"smarty/smarty": "^5.3"
|
||||||
"resque/php-resque": "^1.3.6"
|
|
||||||
},
|
},
|
||||||
"license": "GPL-3.0-or-later",
|
"license": "GPL-3.0-or-later",
|
||||||
"authors": [
|
"authors": [
|
||||||
|
@ -15,10 +14,5 @@
|
||||||
],
|
],
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"phan/phan": "^5.4"
|
"phan/phan": "^5.4"
|
||||||
},
|
|
||||||
"autoload": {
|
|
||||||
"psr-4": {
|
|
||||||
"Federator\\": "php/federator/"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
[generic]
|
[generic]
|
||||||
protocol = 'https'
|
externaldomain = 'your.fqdn'
|
||||||
externaldomain = 'federator.your.fqdn'
|
|
||||||
sourcedomain = 'your.fqdn'
|
|
||||||
|
|
||||||
[database]
|
[database]
|
||||||
host = '127.0.0.1'
|
host = '127.0.0.1'
|
||||||
|
@ -20,7 +18,3 @@ dummy = 'dummyconnector.php'
|
||||||
[maintenance]
|
[maintenance]
|
||||||
username = 'federatoradmin'
|
username = 'federatoradmin'
|
||||||
password = '*change*me*as*well'
|
password = '*change*me*as*well'
|
||||||
|
|
||||||
[keys]
|
|
||||||
federatorPrivateKeyPath = 'federator.pem'
|
|
||||||
federatorPublicKeyPath = 'federator.pem.pub'
|
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
{
|
|
||||||
"activitypub": {
|
|
||||||
"article": [
|
|
||||||
"localhost",
|
|
||||||
"writefreely.org"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,10 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
if (!function_exists('getallheaders')) {
|
|
||||||
/**
|
|
||||||
* @return array<string, string>
|
|
||||||
*/
|
|
||||||
function getallheaders(): array {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -46,8 +46,8 @@ class Api extends Main
|
||||||
*/
|
*/
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$this->contentType = 'application/json';
|
$this->contentType = "application/json";
|
||||||
parent::__construct();
|
Main::__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,7 @@ class Api extends Main
|
||||||
$this->setPath((string)$_REQUEST['_call']);
|
$this->setPath((string)$_REQUEST['_call']);
|
||||||
$this->openDatabase();
|
$this->openDatabase();
|
||||||
$this->loadPlugins();
|
$this->loadPlugins();
|
||||||
$retval = '';
|
$retval = "";
|
||||||
$handler = null;
|
$handler = null;
|
||||||
if ($this->connector === null) {
|
if ($this->connector === null) {
|
||||||
http_response_code(500);
|
http_response_code(500);
|
||||||
|
@ -100,16 +100,12 @@ 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');
|
|
||||||
break;
|
break;
|
||||||
case 'v1':
|
case 'v1':
|
||||||
switch ($this->paths[1]) {
|
switch ($this->paths[1]) {
|
||||||
case 'dummy':
|
case 'dummy':
|
||||||
$handler = new Api\V1\Dummy($this);
|
$handler = new Api\V1\Dummy($this);
|
||||||
break;
|
break;
|
||||||
case 'newcontent':
|
|
||||||
$handler = new Api\V1\NewContent($this);
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -123,7 +119,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 +135,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 +168,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) {
|
||||||
|
@ -195,100 +191,6 @@ class Api extends Main
|
||||||
throw new $exception($message);
|
throw new $exception($message);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* check if the headers include a valid signature
|
|
||||||
*
|
|
||||||
* @param string[] $headers the headers
|
|
||||||
* @throws Exceptions\PermissionDenied
|
|
||||||
* @return string|Exceptions\PermissionDenied
|
|
||||||
*/
|
|
||||||
public function checkSignature($headers)
|
|
||||||
{
|
|
||||||
if (isset($headers['X-Sender'])) {
|
|
||||||
try {
|
|
||||||
return $this->connector->checkSignature($headers);
|
|
||||||
} catch (Exceptions\PermissionDenied $e) {
|
|
||||||
http_response_code(500);
|
|
||||||
throw $e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$signatureHeader = $headers['Signature'] ?? null;
|
|
||||||
|
|
||||||
if (!isset($signatureHeader)) {
|
|
||||||
throw new Exceptions\PermissionDenied('Missing Signature header');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse Signature header
|
|
||||||
preg_match_all('/(\w+)=["\']?([^"\',]+)["\']?/', $signatureHeader, $matches);
|
|
||||||
$signatureParts = array_combine($matches[1], $matches[2]);
|
|
||||||
|
|
||||||
$signature = base64_decode($signatureParts['signature']);
|
|
||||||
$signedHeaders = explode(' ', $signatureParts['headers']);
|
|
||||||
$keyId = $signatureParts['keyId'];
|
|
||||||
$publicKeyPem = false;
|
|
||||||
if ($this->cache !== null) {
|
|
||||||
$publicKeyPem = $this->cache->getPublicKey($keyId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($publicKeyPem === false) {
|
|
||||||
// Fetch public key from `keyId` (usually actor URL + #main-key)
|
|
||||||
[$publicKeyData, $info] = \Federator\Main::getFromRemote($keyId, ['Accept: application/activity+json']);
|
|
||||||
|
|
||||||
if ($info['http_code'] != 200) {
|
|
||||||
throw new Exceptions\PermissionDenied('Failed to fetch public key from keyId: ' . $keyId);
|
|
||||||
}
|
|
||||||
|
|
||||||
$actor = json_decode($publicKeyData, true);
|
|
||||||
|
|
||||||
if (!is_array($actor) || !isset($actor['id'])) {
|
|
||||||
throw new Exceptions\PermissionDenied('Invalid actor data');
|
|
||||||
}
|
|
||||||
|
|
||||||
$publicKeyPem = $actor['publicKey']['publicKeyPem'] ?? null;
|
|
||||||
|
|
||||||
if (!isset($publicKeyPem) || $publicKeyPem === false) {
|
|
||||||
http_response_code(500);
|
|
||||||
throw new Exceptions\PermissionDenied('Public key couldn\'t be determined');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cache the public key for 1 hour
|
|
||||||
if ($this->cache !== null) {
|
|
||||||
$this->cache->savePublicKey($keyId, $publicKeyPem);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reconstruct the signed string
|
|
||||||
$signedString = '';
|
|
||||||
foreach ($signedHeaders as $header) {
|
|
||||||
if ($header === '(request-target)') {
|
|
||||||
$method = strtolower($_SERVER['REQUEST_METHOD']);
|
|
||||||
$path = $_SERVER['REQUEST_URI'];
|
|
||||||
$headerValue = "$method $path";
|
|
||||||
} else {
|
|
||||||
$headerValue = $headers[ucwords($header, '-')] ?? '';
|
|
||||||
}
|
|
||||||
|
|
||||||
$signedString .= strtolower($header) . ': ' . $headerValue . "\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
$signedString = rtrim($signedString);
|
|
||||||
|
|
||||||
// Verify the signature
|
|
||||||
$pubkeyRes = openssl_pkey_get_public($publicKeyPem);
|
|
||||||
$verified = false;
|
|
||||||
if ($pubkeyRes instanceof \OpenSSLAsymmetricKey && is_string($signature)) {
|
|
||||||
$verified = openssl_verify($signedString, $signature, $pubkeyRes, OPENSSL_ALGO_SHA256);
|
|
||||||
}
|
|
||||||
if ($verified != 1) {
|
|
||||||
http_response_code(500);
|
|
||||||
throw new Exceptions\PermissionDenied('Signature verification failed');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Signature is valid!
|
|
||||||
return 'Signature verified.';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* remove unwanted elements from html input
|
* remove unwanted elements from html input
|
||||||
*
|
*
|
||||||
|
|
|
@ -16,7 +16,7 @@ class FedUsers implements APIInterface
|
||||||
/**
|
/**
|
||||||
* main instance
|
* main instance
|
||||||
*
|
*
|
||||||
* @var \Federator\Api $main
|
* @var \Federator\Main $main
|
||||||
*/
|
*/
|
||||||
private $main;
|
private $main;
|
||||||
|
|
||||||
|
@ -47,36 +47,26 @@ 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];
|
|
||||||
switch (sizeof($paths)) {
|
switch (sizeof($paths)) {
|
||||||
case 2:
|
case 2:
|
||||||
if ($method === 'GET') {
|
if ($method === 'GET') {
|
||||||
// /users/username or /@username or /username
|
// /users/username or /@username
|
||||||
return $this->returnUserProfile($_username);
|
return $this->returnUserProfile($paths[1]);
|
||||||
} else {
|
|
||||||
switch ($paths[1]) {
|
|
||||||
case 'inbox':
|
|
||||||
$_username = null;
|
|
||||||
$handler = new FedUsers\Inbox($this->main);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 3:
|
case 3:
|
||||||
// /users/username/(inbox|outbox|following|followers)
|
// /users/username/(inbox|outbox|following|followers)
|
||||||
switch ($paths[2]) {
|
switch ($paths[2]) {
|
||||||
case 'following':
|
case 'following':
|
||||||
$handler = new FedUsers\Following($this->main);
|
// $handler = new FedUsers\Following();
|
||||||
break;
|
break;
|
||||||
case 'followers':
|
case 'followers':
|
||||||
$handler = new FedUsers\Followers($this->main);
|
// $handler = new FedUsers\Followers();
|
||||||
break;
|
break;
|
||||||
case 'inbox':
|
case 'inbox':
|
||||||
$handler = new FedUsers\Inbox($this->main);
|
// $handler = new FedUsers\Inbox();
|
||||||
break;
|
break;
|
||||||
case 'outbox':
|
case 'outbox':
|
||||||
$handler = new FedUsers\Outbox($this->main);
|
$handler = new FedUsers\Outbox($this->main);
|
||||||
|
@ -92,10 +82,10 @@ class FedUsers implements APIInterface
|
||||||
$ret = false;
|
$ret = false;
|
||||||
switch ($method) {
|
switch ($method) {
|
||||||
case 'GET':
|
case 'GET':
|
||||||
$ret = $handler->get($_username);
|
$ret = $handler->get($paths[1]);
|
||||||
break;
|
break;
|
||||||
case 'POST':
|
case 'POST':
|
||||||
$ret = $handler->post($_username);
|
$ret = $handler->post($paths[1]);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if ($ret !== false) {
|
if ($ret !== false) {
|
||||||
|
@ -125,31 +115,22 @@ class FedUsers implements APIInterface
|
||||||
if ($user === false || $user->id === null) {
|
if ($user === false || $user->id === null) {
|
||||||
throw new \Federator\Exceptions\FileNotFound();
|
throw new \Federator\Exceptions\FileNotFound();
|
||||||
}
|
}
|
||||||
$config = $this->main->getConfig();
|
|
||||||
$domain = $config['generic']['externaldomain'];
|
|
||||||
$sourcedomain = $config['generic']['sourcedomain'];
|
|
||||||
$jsonKey = json_encode($user->publicKey);
|
|
||||||
if (!is_string($jsonKey)) {
|
|
||||||
throw new \Federator\Exceptions\FileNotFound();
|
|
||||||
}
|
|
||||||
$data = [
|
$data = [
|
||||||
'iconMediaType' => $user->iconMediaType,
|
'iconMediaType' => $user->iconMediaType,
|
||||||
'iconURL' => $user->iconURL,
|
'iconURL' => $user->iconURL,
|
||||||
'imageMediaType' => $user->imageMediaType,
|
'imageMediaType' => $user->imageMediaType,
|
||||||
'imageURL' => $user->imageURL,
|
'imageURL' => $user->imageURL,
|
||||||
'fqdn' => $domain,
|
'fqdn' => $_SERVER['SERVER_NAME'],
|
||||||
'sourcedomain' => $sourcedomain,
|
|
||||||
'name' => $user->name,
|
'name' => $user->name,
|
||||||
'username' => $user->id,
|
'username' => $user->id,
|
||||||
'publickey' => trim($jsonKey, '"'),
|
'publickey' => str_replace("\n", "\\n", $user->publicKey),
|
||||||
'registered' => gmdate('Y-m-d\TH:i:s\Z', $user->registered), // 2021-03-25T00:00:00Z
|
'registered' => gmdate('Y-m-d\TH:i:s\Z', $user->registered), // 2021-03-25T00:00:00Z
|
||||||
'summary' => $user->summary,
|
'summary' => $user->summary,
|
||||||
'type' => ucfirst($user->type) // capitalized user type
|
'type' => $user->type
|
||||||
];
|
];
|
||||||
$this->response = $this->main->renderTemplate('user.json', $data);
|
$this->response = $this->main->renderTemplate('user.json', $data);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* set response
|
* set response
|
||||||
*
|
*
|
||||||
|
|
|
@ -1,123 +0,0 @@
|
||||||
<?php
|
|
||||||
/**
|
|
||||||
* SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* @author Yannis Vogel (vogeldevelopment)
|
|
||||||
**/
|
|
||||||
|
|
||||||
namespace Federator\Api\FedUsers;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* handle activitypub followers requests
|
|
||||||
*/
|
|
||||||
class Followers implements \Federator\Api\FedUsers\FedUsersInterface
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* main instance
|
|
||||||
*
|
|
||||||
* @var \Federator\Api $main
|
|
||||||
*/
|
|
||||||
private $main;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* constructor
|
|
||||||
* @param \Federator\Api $main main instance
|
|
||||||
*/
|
|
||||||
public function __construct($main)
|
|
||||||
{
|
|
||||||
$this->main = $main;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* handle get call
|
|
||||||
*
|
|
||||||
* @param string|null $_user user to fetch followers for
|
|
||||||
* @return string|false response
|
|
||||||
*/
|
|
||||||
public function get($_user)
|
|
||||||
{
|
|
||||||
if (!isset($_user)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
$dbh = $this->main->getDatabase();
|
|
||||||
$cache = $this->main->getCache();
|
|
||||||
$connector = $this->main->getConnector();
|
|
||||||
|
|
||||||
// get user
|
|
||||||
$user = \Federator\DIO\User::getUserByName(
|
|
||||||
$dbh,
|
|
||||||
$_user,
|
|
||||||
$connector,
|
|
||||||
$cache
|
|
||||||
);
|
|
||||||
if ($user->id === null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$followers = new \Federator\Data\ActivityPub\Common\Followers();
|
|
||||||
$followerItems = \Federator\DIO\Followers::getFollowersByUser($dbh, $user->id, $connector, $cache);
|
|
||||||
|
|
||||||
$config = $this->main->getConfig();
|
|
||||||
$protocol = $config['generic']['protocol'];
|
|
||||||
$domain = $config['generic']['externaldomain'];
|
|
||||||
$baseUrl = $protocol . '://' . $domain . '/' . $_user . '/followers';
|
|
||||||
|
|
||||||
$pageSize = 10;
|
|
||||||
$page = $this->main->extractFromURI('page', '');
|
|
||||||
$id = $baseUrl;
|
|
||||||
$items = [];
|
|
||||||
$totalItems = count($followerItems);
|
|
||||||
|
|
||||||
if ($page !== '') {
|
|
||||||
$pageNum = max(0, (int) $page);
|
|
||||||
$offset = (int)($pageNum * $pageSize);
|
|
||||||
$pagedItems = array_slice($followerItems, $offset, $pageSize);
|
|
||||||
|
|
||||||
foreach ($pagedItems as $follower) {
|
|
||||||
$items[] = $follower->actorURL;
|
|
||||||
}
|
|
||||||
$followers->setItems($items);
|
|
||||||
$id .= '?page=' . urlencode($page);
|
|
||||||
}
|
|
||||||
|
|
||||||
$followers->setID($id);
|
|
||||||
$followers->setPartOf($baseUrl);
|
|
||||||
$followers->setTotalItems($totalItems);
|
|
||||||
|
|
||||||
// Pagination navigation
|
|
||||||
$lastPage = max(0, ceil($totalItems / $pageSize) - 1);
|
|
||||||
|
|
||||||
if ($page === '' || $followers->getCount() == 0) {
|
|
||||||
$followers->setFirst($baseUrl . '?page=0');
|
|
||||||
$followers->setLast($baseUrl . '?page=' . $lastPage);
|
|
||||||
}
|
|
||||||
if ($page !== '') {
|
|
||||||
$pageNum = max(0, (int) $page);
|
|
||||||
if ($pageNum < $lastPage) {
|
|
||||||
$followers->setNext($baseUrl . '?page=' . ($pageNum + 1));
|
|
||||||
}
|
|
||||||
if ($pageNum > 0) {
|
|
||||||
$followers->setPrev($baseUrl . '?page=' . ($pageNum - 1));
|
|
||||||
}
|
|
||||||
$followers->setFirst($baseUrl . '?page=0');
|
|
||||||
$followers->setLast($baseUrl . '?page=' . $lastPage);
|
|
||||||
} else {
|
|
||||||
$followers->setType('OrderedCollection');
|
|
||||||
}
|
|
||||||
$obj = $followers->toObject();
|
|
||||||
|
|
||||||
return json_encode($obj, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* handle post call
|
|
||||||
*
|
|
||||||
* @param string|null $_user user to add data to outbox @unused-param
|
|
||||||
* @return string|false response
|
|
||||||
*/
|
|
||||||
public function post($_user)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,122 +0,0 @@
|
||||||
<?php
|
|
||||||
/**
|
|
||||||
* SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* @author Yannis Vogel (vogeldevelopment)
|
|
||||||
**/
|
|
||||||
|
|
||||||
namespace Federator\Api\FedUsers;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* handle activitypub following requests
|
|
||||||
*/
|
|
||||||
class Following implements \Federator\Api\FedUsers\FedUsersInterface
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* main instance
|
|
||||||
*
|
|
||||||
* @var \Federator\Api $main
|
|
||||||
*/
|
|
||||||
private $main;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* constructor
|
|
||||||
* @param \Federator\Api $main main instance
|
|
||||||
*/
|
|
||||||
public function __construct($main)
|
|
||||||
{
|
|
||||||
$this->main = $main;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* handle get call
|
|
||||||
*
|
|
||||||
* @param string|null $_user user to fetch followers for
|
|
||||||
* @return string|false response
|
|
||||||
*/
|
|
||||||
public function get($_user)
|
|
||||||
{
|
|
||||||
if (!isset($_user)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
$dbh = $this->main->getDatabase();
|
|
||||||
$cache = $this->main->getCache();
|
|
||||||
$connector = $this->main->getConnector();
|
|
||||||
// get user
|
|
||||||
$user = \Federator\DIO\User::getUserByName(
|
|
||||||
$dbh,
|
|
||||||
$_user,
|
|
||||||
$connector,
|
|
||||||
$cache
|
|
||||||
);
|
|
||||||
if ($user->id === null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$following = new \Federator\Data\ActivityPub\Common\Following();
|
|
||||||
$followingItems = \Federator\DIO\Followers::getFollowingByUser($dbh, $user->id, $connector, $cache);
|
|
||||||
|
|
||||||
$config = $this->main->getConfig();
|
|
||||||
$protocol = $config['generic']['protocol'];
|
|
||||||
$domain = $config['generic']['externaldomain'];
|
|
||||||
$baseUrl = $protocol . '://' . $domain . '/users/' . $_user . '/following';
|
|
||||||
|
|
||||||
$pageSize = 10;
|
|
||||||
$page = $this->main->extractFromURI('page', '');
|
|
||||||
$id = $baseUrl;
|
|
||||||
$items = [];
|
|
||||||
$totalItems = count($followingItems);
|
|
||||||
|
|
||||||
if ($page !== '') {
|
|
||||||
$pageNum = max(0, (int) $page);
|
|
||||||
$offset = (int) ($pageNum * $pageSize);
|
|
||||||
$pagedItems = array_slice($followingItems, $offset, $pageSize);
|
|
||||||
|
|
||||||
foreach ($pagedItems as $followed) {
|
|
||||||
$items[] = $followed->actorURL;
|
|
||||||
}
|
|
||||||
$following->setItems($items);
|
|
||||||
$id .= '?page=' . urlencode($page);
|
|
||||||
}
|
|
||||||
|
|
||||||
$following->setID($id);
|
|
||||||
$following->setPartOf($baseUrl);
|
|
||||||
$following->setTotalItems($totalItems);
|
|
||||||
|
|
||||||
// Pagination navigation
|
|
||||||
$lastPage = max(0, ceil($totalItems / $pageSize) - 1);
|
|
||||||
|
|
||||||
if ($page === '' || $following->getCount() == 0) {
|
|
||||||
$following->setFirst($baseUrl . '?page=0');
|
|
||||||
$following->setLast($baseUrl . '?page=' . $lastPage);
|
|
||||||
}
|
|
||||||
if ($page !== '') {
|
|
||||||
$pageNum = max(0, (int) $page);
|
|
||||||
if ($pageNum < $lastPage) {
|
|
||||||
$following->setNext($baseUrl . '?page=' . ($pageNum + 1));
|
|
||||||
}
|
|
||||||
if ($pageNum > 0) {
|
|
||||||
$following->setPrev($baseUrl . '?page=' . ($pageNum - 1));
|
|
||||||
}
|
|
||||||
$following->setFirst($baseUrl . '?page=0');
|
|
||||||
$following->setLast($baseUrl . '?page=' . $lastPage);
|
|
||||||
} else {
|
|
||||||
$following->setType('OrderedCollection');
|
|
||||||
}
|
|
||||||
$obj = $following->toObject();
|
|
||||||
|
|
||||||
return json_encode($obj, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* handle post call
|
|
||||||
*
|
|
||||||
* @param string|null $_user user to add data to outbox @unused-param
|
|
||||||
* @return string|false response
|
|
||||||
*/
|
|
||||||
public function post($_user)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,262 +0,0 @@
|
||||||
<?php
|
|
||||||
/**
|
|
||||||
* SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* @author Yannis Vogel (vogeldevelopment)
|
|
||||||
**/
|
|
||||||
|
|
||||||
namespace Federator\Api\FedUsers;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* handle activitypub inbox requests
|
|
||||||
*/
|
|
||||||
class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* main instance
|
|
||||||
*
|
|
||||||
* @var \Federator\Api $main
|
|
||||||
*/
|
|
||||||
private $main;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* constructor
|
|
||||||
* @param \Federator\Api $main api main instance
|
|
||||||
*/
|
|
||||||
public function __construct($main)
|
|
||||||
{
|
|
||||||
$this->main = $main;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* handle get call
|
|
||||||
*
|
|
||||||
* @param string|null $_user user to fetch inbox for @unused-param
|
|
||||||
* @return string|false response
|
|
||||||
*/
|
|
||||||
public function get($_user)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* handle post call
|
|
||||||
*
|
|
||||||
* @param string|null $_user user to add data to inbox
|
|
||||||
* @return string|false response
|
|
||||||
*/
|
|
||||||
public function post($_user)
|
|
||||||
{
|
|
||||||
$_rawInput = file_get_contents('php://input');
|
|
||||||
|
|
||||||
$allHeaders = getallheaders();
|
|
||||||
try {
|
|
||||||
$this->main->checkSignature($allHeaders);
|
|
||||||
} catch (\Federator\Exceptions\PermissionDenied $e) {
|
|
||||||
error_log("signature check failed");
|
|
||||||
throw new \Federator\Exceptions\Unauthorized('Inbox::post Signature check failed: ' . $e->getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
$activity = is_string($_rawInput) ? json_decode($_rawInput, true) : null;
|
|
||||||
|
|
||||||
$dbh = $this->main->getDatabase();
|
|
||||||
$cache = $this->main->getCache();
|
|
||||||
$connector = $this->main->getConnector();
|
|
||||||
|
|
||||||
$config = $this->main->getConfig();
|
|
||||||
if (!is_array($activity)) {
|
|
||||||
throw new \Federator\Exceptions\ServerError('Inbox::post Input wasn\'t of type array');
|
|
||||||
}
|
|
||||||
|
|
||||||
$inboxActivity = \Federator\Data\ActivityPub\Factory::newActivityFromJson($activity);
|
|
||||||
|
|
||||||
if ($inboxActivity === false) {
|
|
||||||
throw new \Federator\Exceptions\ServerError('Inbox::post couldn\'t create inboxActivity');
|
|
||||||
}
|
|
||||||
$actor = $inboxActivity->getAActor(); // url of the sender https://contentnation.net/username
|
|
||||||
$username = basename((string) (parse_url($actor, PHP_URL_PATH) ?? ''));
|
|
||||||
$domain = parse_url($actor, PHP_URL_HOST);
|
|
||||||
$userId = $username . '@' . $domain;
|
|
||||||
$user = \Federator\DIO\FedUser::getUserByName(
|
|
||||||
$dbh,
|
|
||||||
$userId,
|
|
||||||
$cache
|
|
||||||
);
|
|
||||||
if ($user === null || $user->id === null) {
|
|
||||||
error_log('Inbox::post couldn\'t find user: ' . $userId);
|
|
||||||
throw new \Federator\Exceptions\ServerError('Inbox::post couldn\'t find user: ' . $userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
$users = [];
|
|
||||||
|
|
||||||
$receivers = array_merge($inboxActivity->getTo(), $inboxActivity->getCC());
|
|
||||||
|
|
||||||
// For Undo, the object may hold the proper to/cc
|
|
||||||
if ($inboxActivity->getType() === 'Undo') {
|
|
||||||
$object = $inboxActivity->getObject();
|
|
||||||
if ($object !== null && is_object($object)) {
|
|
||||||
$receivers = array_merge($object->getTo(), $object->getCC());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter out the public address and keep only actual URLs
|
|
||||||
$receivers = array_filter($receivers, static function (mixed $receiver): bool {
|
|
||||||
return is_string($receiver)
|
|
||||||
&& $receiver !== 'https://www.w3.org/ns/activitystreams#Public'
|
|
||||||
&& (filter_var($receiver, FILTER_VALIDATE_URL) !== false);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isset($_user)) {
|
|
||||||
$receivers[] = $dbh->real_escape_string($_user); // Add the target user to the receivers list
|
|
||||||
}
|
|
||||||
|
|
||||||
// Special handling for Follow and Undo follow activities
|
|
||||||
if (strtolower($inboxActivity->getType()) === 'follow') {
|
|
||||||
// For Follow, the object should hold the target
|
|
||||||
$object = $inboxActivity->getObject();
|
|
||||||
if ($object !== null && is_string($object)) {
|
|
||||||
$receivers[] = $object;
|
|
||||||
}
|
|
||||||
} elseif (strtolower($inboxActivity->getType()) === 'undo') {
|
|
||||||
$object = $inboxActivity->getObject();
|
|
||||||
if ($object !== null && is_object($object)) {
|
|
||||||
// For Undo, the objects object should hold the target
|
|
||||||
if (strtolower($object->getType()) === 'follow') {
|
|
||||||
$objObject = $object->getObject();
|
|
||||||
if ($objObject !== null && is_string($objObject)) {
|
|
||||||
$receivers[] = $objObject;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$ourDomain = $config['generic']['externaldomain'];
|
|
||||||
|
|
||||||
$finalReceivers = [];
|
|
||||||
foreach ($receivers as $receiver) {
|
|
||||||
if ($receiver === '' || !is_string($receiver)) {
|
|
||||||
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')) {
|
|
||||||
$actor = $inboxActivity->getAActor();
|
|
||||||
if ($actor === null || !is_string($actor)) {
|
|
||||||
error_log('Inbox::post no actor found');
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract username from the actor URL
|
|
||||||
$username = basename((string) (parse_url($actor, PHP_URL_PATH) ?? ''));
|
|
||||||
$domain = parse_url($actor, PHP_URL_HOST);
|
|
||||||
error_log("url $actor to username $username domain $domain");
|
|
||||||
if ($username === null || $domain === null) {
|
|
||||||
error_log('Inbox::post no username or domain found for recipient: ' . $receiver);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
$followers = \Federator\DIO\Followers::getFollowersByFedUser(
|
|
||||||
$dbh,
|
|
||||||
$connector,
|
|
||||||
$cache,
|
|
||||||
$username . '@' . $domain
|
|
||||||
);
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
error_log('Inbox::post get followers for user: ' . $username . '@' . $domain . '. Exception: '
|
|
||||||
. $e->getMessage());
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (is_array($followers)) {
|
|
||||||
$users = array_merge($users, array_column($followers, 'id'));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
$localUser = \Federator\DIO\User::getUserByName(
|
|
||||||
$dbh,
|
|
||||||
$receiver,
|
|
||||||
$connector,
|
|
||||||
$cache
|
|
||||||
);
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
error_log('Inbox::post get user by name: ' . $receiver . '. Exception: ' . $e->getMessage());
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if ($localUser === null || $localUser->id === null) {
|
|
||||||
error_log('Inbox::post 210 couldn\'t find user: ' . $receiver);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$users[] = $localUser->id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$users = array_unique($users); // remove duplicates
|
|
||||||
|
|
||||||
if (empty($users)) { // todo remove after proper implementation, debugging for now
|
|
||||||
$rootDir = '/tmp/';
|
|
||||||
// Save the raw input and parsed JSON to a file for inspection
|
|
||||||
file_put_contents(
|
|
||||||
$rootDir . 'logs/inbox.log',
|
|
||||||
date('Y-m-d H:i:s') . ": ==== POST Inbox Activity ====\n"
|
|
||||||
. json_encode($inboxActivity, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) . "\n\n",
|
|
||||||
FILE_APPEND
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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) {
|
|
||||||
if (!isset($receiver)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$token = \Resque::enqueue('inbox', 'Federator\\Jobs\\InboxJob', [
|
|
||||||
'user' => $user->id,
|
|
||||||
'recipientId' => $receiver,
|
|
||||||
'activity' => $inboxActivity->toObject(),
|
|
||||||
]);
|
|
||||||
error_log('Inbox::post enqueued job for user: ' . $user->id . ' with token: ' . $token);
|
|
||||||
}
|
|
||||||
if (empty($users)) {
|
|
||||||
$type = strtolower($inboxActivity->getType());
|
|
||||||
if ($type === 'undo' || $type === 'delete') {
|
|
||||||
$token = \Resque::enqueue('inbox', 'Federator\\Jobs\\InboxJob', [
|
|
||||||
'user' => $user->id,
|
|
||||||
'recipientId' => "",
|
|
||||||
'activity' => $inboxActivity->toObject(),
|
|
||||||
]);
|
|
||||||
error_log('Inbox::post enqueued job for user: ' . $user->id . ' with token: ' . $token);
|
|
||||||
} else {
|
|
||||||
error_log('Inbox::post no users found for activity, doing nothing: '
|
|
||||||
. json_encode($inboxActivity, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 'success';
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -16,13 +16,13 @@ class Outbox implements \Federator\Api\FedUsers\FedUsersInterface
|
||||||
/**
|
/**
|
||||||
* main instance
|
* main instance
|
||||||
*
|
*
|
||||||
* @var \Federator\Api $main
|
* @var \Federator\Main $main
|
||||||
*/
|
*/
|
||||||
private $main;
|
private $main;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* constructor
|
* constructor
|
||||||
* @param \Federator\Api $main main instance
|
* @param \Federator\Main $main main instance
|
||||||
*/
|
*/
|
||||||
public function __construct($main)
|
public function __construct($main)
|
||||||
{
|
{
|
||||||
|
@ -32,14 +32,11 @@ class Outbox implements \Federator\Api\FedUsers\FedUsersInterface
|
||||||
/**
|
/**
|
||||||
* handle get call
|
* handle get call
|
||||||
*
|
*
|
||||||
* @param string|null $_user user to fetch outbox for
|
* @param string $_user user to fetch outbox for
|
||||||
* @return string|false response
|
* @return string|false response
|
||||||
*/
|
*/
|
||||||
public function get($_user)
|
public function get($_user)
|
||||||
{
|
{
|
||||||
if (!isset($_user)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
$dbh = $this->main->getDatabase();
|
$dbh = $this->main->getDatabase();
|
||||||
$cache = $this->main->getCache();
|
$cache = $this->main->getCache();
|
||||||
$connector = $this->main->getConnector();
|
$connector = $this->main->getConnector();
|
||||||
|
@ -53,46 +50,42 @@ class Outbox implements \Federator\Api\FedUsers\FedUsersInterface
|
||||||
if ($user->id === null) {
|
if ($user->id === null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// get posts from user
|
// get posts from user
|
||||||
$outbox = new \Federator\Data\ActivityPub\Common\Outbox();
|
$outbox = new \Federator\Data\ActivityPub\Common\Outbox();
|
||||||
$min = intval($this->main->extractFromURI('min', '0'), 10);
|
$min = $this->main->extractFromURI("min", "");
|
||||||
$max = intval($this->main->extractFromURI('max', '0'), 10);
|
$max = $this->main->extractFromURI("max", "");
|
||||||
$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);
|
||||||
$outbox->setItems($items);
|
$outbox->setItems($items);
|
||||||
} else {
|
} else {
|
||||||
$tmpitems = \Federator\DIO\Posts::getPostsByUser($dbh, $user->id, $connector, $cache, $min, $max, 99999);
|
|
||||||
$outbox->setTotalItems(sizeof($tmpitems));
|
|
||||||
$items = [];
|
$items = [];
|
||||||
}
|
}
|
||||||
$config = $this->main->getConfig();
|
$host = $_SERVER['SERVER_NAME'];
|
||||||
$protocol = $config['generic']['protocol'];
|
$id = 'https://' . $host .'/' . $_user . '/outbox';
|
||||||
$domain = $config['generic']['externaldomain'];
|
|
||||||
$id = $protocol . '://' . $domain . '/' . $_user . '/outbox';
|
|
||||||
$outbox->setPartOf($id);
|
$outbox->setPartOf($id);
|
||||||
$outbox->setID($id);
|
$outbox->setID($id);
|
||||||
if ($page === '') {
|
if ($page !== '') {
|
||||||
$outbox->setType('OrderedCollection');
|
$id .= '?page=' . urlencode($page);
|
||||||
}
|
}
|
||||||
if ($page === '' || $outbox->getCount() == 0) {
|
if ($page === '' || $outbox->count() == 0) {
|
||||||
$outbox->setFirst($id . '?page=true');
|
$outbox->setFirst($id);
|
||||||
|
$outbox->setLast($id . '&min=0');
|
||||||
}
|
}
|
||||||
if (sizeof($items)>0) {
|
if (sizeof($items)>0) {
|
||||||
$oldestTS = $items[0]->getPublished();
|
$newestId = $items[0]->getPublished();
|
||||||
$newestTS = $items[sizeof($items) - 1]->getPublished();
|
$oldestId = $items[sizeof($items)-1]->getPublished();
|
||||||
$outbox->setNext($id . '?page=true&max=' . $newestTS);
|
$outbox->setNext($id . '&max=' . $newestId);
|
||||||
$outbox->setPrev($id . '?page=true&min=' . $oldestTS);
|
$outbox->setPrev($id . '&min=' . $oldestId);
|
||||||
}
|
}
|
||||||
$obj = $outbox->toObject();
|
$obj = $outbox->toObject();
|
||||||
return json_encode($obj, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
|
return json_encode($obj);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* handle post call
|
* handle post call
|
||||||
*
|
*
|
||||||
* @param string|null $_user user to add data to outbox @unused-param
|
* @param string $_user user to add data to outbox @unused-param
|
||||||
* @return string|false response
|
* @return string|false response
|
||||||
*/
|
*/
|
||||||
public function post($_user)
|
public function post($_user)
|
||||||
|
|
|
@ -41,7 +41,7 @@ class Dummy implements \Federator\Api\APIInterface
|
||||||
* run given url path
|
* run given url path
|
||||||
*
|
*
|
||||||
* @param array<string> $paths path array split by /
|
* @param array<string> $paths path array split by /
|
||||||
* @param \Federator\Data\User|false $user user who is calling us @unused-param
|
* @param \Federator\Data\User|false $user user who is calling us
|
||||||
* @return bool true on success
|
* @return bool true on success
|
||||||
*/
|
*/
|
||||||
public function exec($paths, $user) : bool
|
public function exec($paths, $user) : bool
|
||||||
|
@ -58,7 +58,6 @@ class Dummy implements \Federator\Api\APIInterface
|
||||||
if ($paths[2] === 'moo') {
|
if ($paths[2] === 'moo') {
|
||||||
return $this->getDummy();
|
return $this->getDummy();
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'POST':
|
case 'POST':
|
||||||
|
@ -69,7 +68,6 @@ class Dummy implements \Federator\Api\APIInterface
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
$this->main->setResponseCode(404);
|
$this->main->setResponseCode(404);
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -1,480 +0,0 @@
|
||||||
<?php
|
|
||||||
/**
|
|
||||||
* SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* @author Yannis Vogel (vogeldevelopment)
|
|
||||||
**/
|
|
||||||
|
|
||||||
namespace Federator\Api\V1;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called from our application to inform us about new content (f.e. new posts on contentnation.net)
|
|
||||||
*/
|
|
||||||
class NewContent implements \Federator\Api\APIInterface
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* main instance
|
|
||||||
*
|
|
||||||
* @var \Federator\Api $main
|
|
||||||
*/
|
|
||||||
private $main;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* response from sub-calls
|
|
||||||
*
|
|
||||||
* @var string $response
|
|
||||||
*/
|
|
||||||
private $response;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* constructor
|
|
||||||
*
|
|
||||||
* @param \Federator\Main $main main instance
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function __construct($main)
|
|
||||||
{
|
|
||||||
$this->main = $main;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* run given url path
|
|
||||||
*
|
|
||||||
* @param array<string> $paths path array split by /
|
|
||||||
* @param \Federator\Data\User|false $user user who is calling us @unused-param
|
|
||||||
* @return bool true on success
|
|
||||||
*/
|
|
||||||
public function exec($paths, $user)
|
|
||||||
{
|
|
||||||
$method = $_SERVER['REQUEST_METHOD'];
|
|
||||||
$_username = $paths[2];
|
|
||||||
if ($method === 'GET') { // unsupported
|
|
||||||
/// TODO: throw unsupported method exception
|
|
||||||
throw new \Federator\Exceptions\InvalidArgument('GET not supported');
|
|
||||||
}
|
|
||||||
switch (sizeof($paths)) {
|
|
||||||
case 3:
|
|
||||||
$ret = $this->post($_username);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isset($ret) && $ret !== false) {
|
|
||||||
$this->response = $ret;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->main->setResponseCode(404);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* handle post call
|
|
||||||
*
|
|
||||||
* @param string|null $_user optional user that triggered the post
|
|
||||||
* @return string|false response
|
|
||||||
*/
|
|
||||||
public function post($_user)
|
|
||||||
{
|
|
||||||
$_rawInput = file_get_contents('php://input');
|
|
||||||
$allHeaders = getallheaders();
|
|
||||||
try {
|
|
||||||
$this->main->checkSignature($allHeaders);
|
|
||||||
} catch (\Federator\Exceptions\PermissionDenied $e) {
|
|
||||||
error_log('NewContent::post Signature check failed: ' . $e->getMessage());
|
|
||||||
http_response_code(401);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$input = is_string($_rawInput) ? json_decode($_rawInput, true) : null;
|
|
||||||
|
|
||||||
|
|
||||||
$dbh = $this->main->getDatabase();
|
|
||||||
$cache = $this->main->getCache();
|
|
||||||
$connector = $this->main->getConnector();
|
|
||||||
|
|
||||||
$config = $this->main->getConfig();
|
|
||||||
$domain = $config['generic']['externaldomain'];
|
|
||||||
if (!is_array($input)) {
|
|
||||||
error_log('NewContent::post Input wasn\'t of type array');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$articleId = '';
|
|
||||||
if (isset($allHeaders['X-Sender'])) {
|
|
||||||
$newActivity = $connector->jsonToActivity($input, $articleId);
|
|
||||||
} else {
|
|
||||||
error_log('NewContent::post No X-Sender header found');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if ($newActivity === false) {
|
|
||||||
error_log('NewContent::post couldn\'t create newActivity');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!isset($_user)) {
|
|
||||||
$user = $newActivity->getAActor(); // url of the sender https://contentnation.net/username
|
|
||||||
$posterName = str_replace(
|
|
||||||
$domain,
|
|
||||||
'',
|
|
||||||
$user
|
|
||||||
); // retrieve only the last part of the url
|
|
||||||
} else {
|
|
||||||
$posterName = $dbh->real_escape_string($_user);
|
|
||||||
}
|
|
||||||
|
|
||||||
$users = [];
|
|
||||||
|
|
||||||
$receivers = array_merge($newActivity->getTo(), $newActivity->getCC());
|
|
||||||
|
|
||||||
// For Undo, the object may hold the proper to/cc
|
|
||||||
if ($newActivity->getType() === 'Undo') {
|
|
||||||
$object = $newActivity->getObject();
|
|
||||||
if ($object !== null && is_object($object)) {
|
|
||||||
$receivers = array_merge($object->getTo(), $object->getCC());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter out the public address and keep only actual URLs
|
|
||||||
$receivers = array_filter($receivers, static function (mixed $receiver): bool {
|
|
||||||
return is_string($receiver)
|
|
||||||
&& $receiver !== 'https://www.w3.org/ns/activitystreams#Public'
|
|
||||||
&& (filter_var($receiver, FILTER_VALIDATE_URL) !== false);
|
|
||||||
});
|
|
||||||
|
|
||||||
foreach ($receivers as $receiver) {
|
|
||||||
if ($receiver === '' || !is_string($receiver)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (str_ends_with($receiver, '/followers')) {
|
|
||||||
if ($posterName === null) {
|
|
||||||
error_log('NewContent::post no username found');
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
$followers = \Federator\DIO\Followers::getFollowersByUser($dbh, $posterName, $connector, $cache);
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
error_log('NewContent::post get followers for user: ' . $posterName . '. Exception: '
|
|
||||||
. $e->getMessage());
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (is_array($followers)) {
|
|
||||||
$users = array_merge($users, array_column($followers, 'id'));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// check if receiver is an actor url and not from our domain
|
|
||||||
if (str_contains($receiver, $domain)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$receiverName = basename((string) (parse_url($receiver, PHP_URL_PATH) ?? ''));
|
|
||||||
$domain = parse_url($receiver, PHP_URL_HOST);
|
|
||||||
if ($receiverName === null || $domain === null) {
|
|
||||||
if ($receiver === $posterName) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
error_log('NewContent::post no receiverName or domain found for receiver: ' . $receiver);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$receiver = $receiverName . '@' . $domain;
|
|
||||||
try {
|
|
||||||
$user = \Federator\DIO\FedUser::getUserByName(
|
|
||||||
$dbh,
|
|
||||||
$receiver,
|
|
||||||
$cache
|
|
||||||
);
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
error_log('NewContent::post get user by name: ' . $receiver . '. Exception: ' . $e->getMessage());
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if ($user === null || $user->id === null) {
|
|
||||||
error_log('NewContent::post couldn\'t find user: ' . $receiver);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$users[] = $user->id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$users = array_unique($users); // remove duplicates
|
|
||||||
|
|
||||||
if (empty($users)) { // todo remove after proper implementation, debugging for now
|
|
||||||
$rootDir = PROJECT_ROOT . '/';
|
|
||||||
// Save the raw input and parsed JSON to a file for inspection
|
|
||||||
file_put_contents(
|
|
||||||
$rootDir . 'logs/newContent.log',
|
|
||||||
date('Y-m-d H:i:s') . ": ==== POST NewContent Activity ====\n"
|
|
||||||
. json_encode($newActivity, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) . "\n\n",
|
|
||||||
FILE_APPEND
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($users as $receiver) {
|
|
||||||
if (!isset($receiver)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$token = \Resque::enqueue('inbox', 'Federator\\Jobs\\NewContentJob', [
|
|
||||||
'user' => $posterName,
|
|
||||||
'recipientId' => $receiver,
|
|
||||||
'activity' => $newActivity->toObject(),
|
|
||||||
'articleId' => $articleId,
|
|
||||||
]);
|
|
||||||
error_log('Inbox::post enqueued job for receiver: ' . $receiver . ' with token: ' . $token);
|
|
||||||
}
|
|
||||||
|
|
||||||
return json_encode($newActivity, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* handle post call for specific user
|
|
||||||
*
|
|
||||||
* @param \mysqli $dbh @unused-param
|
|
||||||
* database handle
|
|
||||||
* @param \Federator\Connector\Connector $connector
|
|
||||||
* connector to fetch use with
|
|
||||||
* @param \Federator\Cache\Cache|null $cache
|
|
||||||
* optional caching service
|
|
||||||
* @param string $host host url of our server (e.g. https://federator.com)
|
|
||||||
* @param string $_user user that triggered the post
|
|
||||||
* @param string $_recipientId recipient of the post
|
|
||||||
* @param \Federator\Data\ActivityPub\Common\Activity $newActivity the activity that we received
|
|
||||||
* @param string $articleId the original id of the article (if applicable)
|
|
||||||
* (used to identify the article in the remote system)
|
|
||||||
* @return boolean response
|
|
||||||
*/
|
|
||||||
public static function postForUser($dbh, $connector, $cache, $host, $_user, $_recipientId, $newActivity, $articleId)
|
|
||||||
{
|
|
||||||
if (!isset($_user)) {
|
|
||||||
error_log('NewContent::postForUser no user given');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// get sender
|
|
||||||
$user = \Federator\DIO\User::getUserByName(
|
|
||||||
$dbh,
|
|
||||||
$_user,
|
|
||||||
$connector,
|
|
||||||
$cache
|
|
||||||
);
|
|
||||||
if ($user === null || $user->id === null) {
|
|
||||||
error_log('NewContent::postForUser couldn\'t find user: ' . $_user);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// get recipient
|
|
||||||
$recipient = \Federator\DIO\FedUser::getUserByName(
|
|
||||||
$dbh,
|
|
||||||
$_recipientId,
|
|
||||||
$cache
|
|
||||||
);
|
|
||||||
if ($recipient === null || $recipient->id === null) {
|
|
||||||
error_log('NewContent::postForUser couldn\'t find user: ' . $_recipientId);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$rootDir = PROJECT_ROOT . '/';
|
|
||||||
// Save the raw input and parsed JSON to a file for inspection
|
|
||||||
file_put_contents(
|
|
||||||
$rootDir . 'logs/newcontent_' . $recipient->id . '.log',
|
|
||||||
date('Y-m-d H:i:s') . ": ==== POST " . $recipient->id . " NewContent Activity ====\n"
|
|
||||||
. json_encode($newActivity, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) . "\n\n",
|
|
||||||
FILE_APPEND
|
|
||||||
);
|
|
||||||
|
|
||||||
$type = strtolower($newActivity->getType());
|
|
||||||
|
|
||||||
switch ($type) {
|
|
||||||
case 'follow':
|
|
||||||
// $success = false;
|
|
||||||
$actor = $newActivity->getAActor();
|
|
||||||
if ($actor !== '') {
|
|
||||||
$newIdUrl = \Federator\DIO\Followers::generateNewFollowId($dbh, $host);
|
|
||||||
$newActivity->setID($newIdUrl);
|
|
||||||
/*if (is_string($followerDomain)) {
|
|
||||||
$followerId = "{$followerUsername}@{$followerDomain}";
|
|
||||||
$success = \Federator\DIO\Followers::sendFollowRequest($dbh, $connector, $cache, $user->id,
|
|
||||||
$followerId, $followerDomain);
|
|
||||||
}*/
|
|
||||||
}
|
|
||||||
/* if ($success === false) {
|
|
||||||
error_log("NewContent::postForUser Failed to add follower for user $user->id");
|
|
||||||
} */
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'delete':
|
|
||||||
// Delete Note/Post
|
|
||||||
$object = $newActivity->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 = $newActivity->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 !== '') {
|
|
||||||
$followerUsername = basename((string) (parse_url($actor, PHP_URL_PATH) ?? ''));
|
|
||||||
$followerDomain = parse_url($actor, PHP_URL_HOST);
|
|
||||||
if (is_string($followerDomain)) {
|
|
||||||
$followerId = $followerUsername . '@' . $followerDomain;
|
|
||||||
$removedId = \Federator\DIO\Followers::removeFollow(
|
|
||||||
$dbh,
|
|
||||||
$followerId,
|
|
||||||
$user->id
|
|
||||||
);
|
|
||||||
if ($removedId !== false) {
|
|
||||||
$object->setID($removedId);
|
|
||||||
$newActivity->setObject($object);
|
|
||||||
$success = true;
|
|
||||||
} else {
|
|
||||||
error_log('NewContent::postForUser Failed to remove follow for user '
|
|
||||||
. $user->id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ($success === false) {
|
|
||||||
error_log('NewContent::postForUser Failed to remove follower for user ' . $user->id);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'like':
|
|
||||||
case 'dislike':
|
|
||||||
if (method_exists($object, 'getObject')) {
|
|
||||||
$targetId = $object->getObject();
|
|
||||||
if (is_string($targetId)) {
|
|
||||||
\Federator\DIO\Votes::removeVote($dbh, $user->id, $targetId);
|
|
||||||
} else {
|
|
||||||
error_log('NewContent::postForUser Error in Undo Like/Dislike for user ' . $user->id
|
|
||||||
. ', targetId is not a string');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'note':
|
|
||||||
// Undo Note (remove note)
|
|
||||||
$noteId = $object->getID();
|
|
||||||
\Federator\DIO\Posts::deletePost($dbh, $noteId);
|
|
||||||
break;
|
|
||||||
case 'article':
|
|
||||||
$articleId = $object->getID();
|
|
||||||
\Federator\DIO\Posts::deletePost($dbh, $articleId);
|
|
||||||
// also remove latest saved article-update
|
|
||||||
\Federator\DIO\Posts::deletePost($dbh, $articleId . '#update');
|
|
||||||
|
|
||||||
// Undo Article (remove article)
|
|
||||||
$idPart = strrchr($recipient->id, '@');
|
|
||||||
if ($idPart === false) {
|
|
||||||
error_log('NewContent::postForUser Error in Undo Article. ' . $recipient->id
|
|
||||||
. ', recipient ID is not valid');
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
$targetUrl = ltrim($idPart, '@');
|
|
||||||
|
|
||||||
if ($object instanceof \Federator\Data\ActivityPub\Common\Article) {
|
|
||||||
$object = \Federator\DIO\Article::conditionalConvertToNote($object, $targetUrl);
|
|
||||||
$newActivity->setObject($object);
|
|
||||||
} else {
|
|
||||||
error_log('NewContent::postForUser Error in Undo Article for recipient '
|
|
||||||
. $recipient->id . ', object is not an Article');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} elseif (is_string($object)) {
|
|
||||||
\Federator\DIO\Posts::deletePost($dbh, $object);
|
|
||||||
} else {
|
|
||||||
error_log('NewContent::postForUser Error in Undo for recipient ' . $recipient->id
|
|
||||||
. ', object is not a string or object');
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'like':
|
|
||||||
case 'dislike':
|
|
||||||
// Add Like/Dislike
|
|
||||||
$targetId = $newActivity->getObject();
|
|
||||||
if (is_string($targetId)) {
|
|
||||||
\Federator\DIO\Votes::addVote($dbh, $user->id, $targetId, $type);
|
|
||||||
// \Federator\DIO\Posts::savePost($dbh, $user->id, $newActivity, $articleId);
|
|
||||||
} else {
|
|
||||||
error_log('NewContent::postForUser Error in Add Like/Dislike for recipient ' . $recipient->id
|
|
||||||
. ', targetId is not a string');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'create':
|
|
||||||
case 'update':
|
|
||||||
$object = $newActivity->getObject();
|
|
||||||
if (is_object($object)) {
|
|
||||||
switch (strtolower($object->getType())) {
|
|
||||||
case 'note':
|
|
||||||
\Federator\DIO\Posts::savePost($dbh, $user->id, $newActivity, $articleId);
|
|
||||||
|
|
||||||
break;
|
|
||||||
case 'article':
|
|
||||||
\Federator\DIO\Posts::savePost($dbh, $user->id, $newActivity, $articleId);
|
|
||||||
|
|
||||||
$idPart = strrchr($recipient->id, '@');
|
|
||||||
if ($idPart === false) {
|
|
||||||
error_log('NewContent::postForUser Error in Create/Update Article. ' . $recipient->id
|
|
||||||
. ', recipient ID is not valid');
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
$targetUrl = ltrim($idPart, '@');
|
|
||||||
|
|
||||||
if ($object instanceof \Federator\Data\ActivityPub\Common\Article) {
|
|
||||||
$object = \Federator\DIO\Article::conditionalConvertToNote($object, $targetUrl);
|
|
||||||
$newActivity->setObject($object);
|
|
||||||
} else {
|
|
||||||
error_log('NewContent::postForUser Error in Create/Update Article for recipient '
|
|
||||||
. $recipient->id . ', object is not an Article');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
\Federator\DIO\Posts::savePost($dbh, $user->id, $newActivity, $articleId);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Post Note
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
error_log('NewContent::postForUser Unhandled activity type $type for user ' . $user->id);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$response = \Federator\DIO\Server::sendActivity($dbh, $host, $user, $recipient, $newActivity);
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
error_log('NewContent::postForUser Failed to send activity: ' . $e->getMessage());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (empty($response)) {
|
|
||||||
error_log('NewContent::postForUser Sent activity to ' . $recipient->id);
|
|
||||||
} else {
|
|
||||||
error_log('NewContent::postForUser Sent activity to ' . $recipient->id . ' with response: '
|
|
||||||
. json_encode($response, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT));
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* get internal represenation as json string
|
|
||||||
* @return string json string or html
|
|
||||||
*/
|
|
||||||
public function toJson()
|
|
||||||
{
|
|
||||||
return $this->response;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -44,10 +44,8 @@ class WellKnown implements APIInterface
|
||||||
*/
|
*/
|
||||||
private function hostMeta()
|
private function hostMeta()
|
||||||
{
|
{
|
||||||
$config = $this->main->getConfig();
|
|
||||||
$domain = $config['generic']['externaldomain'];
|
|
||||||
$data = [
|
$data = [
|
||||||
'fqdn' => $domain
|
'fqdn' => $_SERVER['SERVER_NAME']
|
||||||
];
|
];
|
||||||
$this->response = $this->main->renderTemplate('host-meta.xml', $data);
|
$this->response = $this->main->renderTemplate('host-meta.xml', $data);
|
||||||
return true;
|
return true;
|
||||||
|
@ -61,7 +59,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)) {
|
||||||
|
|
|
@ -45,10 +45,8 @@ class NodeInfo
|
||||||
*/
|
*/
|
||||||
public function exec($paths)
|
public function exec($paths)
|
||||||
{
|
{
|
||||||
$config = $this->main->getConfig();
|
|
||||||
$domain = $config['generic']['externaldomain'];
|
|
||||||
$data = [
|
$data = [
|
||||||
'fqdn' => $domain
|
'fqdn' => $_SERVER['SERVER_NAME']
|
||||||
];
|
];
|
||||||
$template = null;
|
$template = null;
|
||||||
if (sizeof($paths) == 2 && $paths[0] === '.well-known' && $paths[1] === 'nodeinfo') {
|
if (sizeof($paths) == 2 && $paths[0] === '.well-known' && $paths[1] === 'nodeinfo') {
|
||||||
|
@ -67,6 +65,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,8 +48,7 @@ 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();
|
||||||
}
|
}
|
||||||
$user = \Federator\DIO\User::getUserByName(
|
$user = \Federator\DIO\User::getUserByName(
|
||||||
|
@ -64,7 +63,6 @@ class WebFinger
|
||||||
$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);
|
||||||
|
|
57
php/federator/cache/cache.php
vendored
57
php/federator/cache/cache.php
vendored
|
@ -13,35 +13,14 @@ namespace Federator\Cache;
|
||||||
*/
|
*/
|
||||||
interface Cache extends \Federator\Connector\Connector
|
interface Cache extends \Federator\Connector\Connector
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* save remote followers of user
|
|
||||||
*
|
|
||||||
* @param string $user user name
|
|
||||||
* @param \Federator\Data\FedUser[]|false $followers user followers
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function saveFollowersByUser($user, $followers);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* save remote following for user
|
|
||||||
*
|
|
||||||
* @param string $user user name
|
|
||||||
* @param \Federator\Data\FedUser[]|false $following user following
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function saveFollowingByUser($user, $following);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* save remote posts by user
|
* save remote posts by user
|
||||||
*
|
*
|
||||||
* @param string $user user name
|
* @param string $user user name
|
||||||
* @param int $min min timestamp
|
|
||||||
* @param int $max max timestamp
|
|
||||||
* @param int $limit limit results
|
|
||||||
* @param \Federator\Data\ActivityPub\Common\APObject[]|false $posts user posts
|
* @param \Federator\Data\ActivityPub\Common\APObject[]|false $posts user posts
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function saveRemotePostsByUser($user, $min, $max, $limit, $posts);
|
public function saveRemotePostsByUser($user, $posts);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* save remote stats
|
* save remote stats
|
||||||
|
@ -60,15 +39,6 @@ interface Cache extends \Federator\Connector\Connector
|
||||||
*/
|
*/
|
||||||
public function saveRemoteUserByName($_name, $user);
|
public function saveRemoteUserByName($_name, $user);
|
||||||
|
|
||||||
/**
|
|
||||||
* save remote federation user by given name
|
|
||||||
*
|
|
||||||
* @param string $_name user/profile name
|
|
||||||
* @param \Federator\Data\FedUser $user user data
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function saveRemoteFedUserByName(string $_name, \Federator\Data\FedUser $user);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* save remote user by given session
|
* save remote user by given session
|
||||||
*
|
*
|
||||||
|
@ -78,29 +48,4 @@ interface Cache extends \Federator\Connector\Connector
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function saveRemoteUserBySession($_session, $_user, $user);
|
public function saveRemoteUserBySession($_session, $_user, $user);
|
||||||
|
|
||||||
/**
|
|
||||||
* Save the public key for a given keyId
|
|
||||||
*
|
|
||||||
* @param string $keyId The keyId (e.g., actor URL + #main-key)
|
|
||||||
* @param string $publicKeyPem The public key PEM to cache
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function savePublicKey(string $keyId, string $publicKeyPem);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* get remote federation user by given name
|
|
||||||
*
|
|
||||||
* @param string $_name user/profile name
|
|
||||||
* @return \Federator\Data\FedUser | false
|
|
||||||
*/
|
|
||||||
public function getRemoteFedUserByName(string $_name);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve the public key for a given keyId
|
|
||||||
*
|
|
||||||
* @param string $keyId The keyId (e.g., actor URL + #main-key)
|
|
||||||
* @return string|false The cached public key PEM or false if not found
|
|
||||||
*/
|
|
||||||
public function getPublicKey(string $keyId);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,32 +13,16 @@ namespace Federator\Connector;
|
||||||
*/
|
*/
|
||||||
interface Connector
|
interface Connector
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* get followers of given user
|
|
||||||
*
|
|
||||||
* @param string $id user id
|
|
||||||
* @return \Federator\Data\FedUser[]|false
|
|
||||||
*/
|
|
||||||
public function getFollowersByUser($id);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* get following of given user
|
|
||||||
*
|
|
||||||
* @param string $id user id
|
|
||||||
* @return \Federator\Data\FedUser[]|false
|
|
||||||
*/
|
|
||||||
public function getFollowingByUser($id);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* get posts by given user
|
* get posts by given user
|
||||||
*
|
*
|
||||||
* @param string $id user id
|
* @param string $id user id
|
||||||
* @param int $min min value
|
* @param string $minId min ID
|
||||||
* @param int $max max value
|
* @param string $maxId max ID
|
||||||
* @param int $limit maximum number of results
|
|
||||||
* @return \Federator\Data\ActivityPub\Common\Activity[]|false
|
* @return \Federator\Data\ActivityPub\Common\APObject[]|false
|
||||||
*/
|
*/
|
||||||
public function getRemotePostsByUser($id, $min, $max, $limit);
|
public function getRemotePostsByUser($id, $minId, $maxId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* get remote user by given name
|
* get remote user by given name
|
||||||
|
@ -63,32 +47,4 @@ interface Connector
|
||||||
* @return \Federator\Data\Stats|false
|
* @return \Federator\Data\Stats|false
|
||||||
*/
|
*/
|
||||||
public function getRemoteStats();
|
public function getRemoteStats();
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert jsonData to Activity format
|
|
||||||
*
|
|
||||||
* @param array<string, mixed> $jsonData the json data from our platfrom
|
|
||||||
* @param string $articleId the original id of the article (if applicable)
|
|
||||||
* (used to identify the article in the remote system)
|
|
||||||
* @return \Federator\Data\ActivityPub\Common\Activity|false
|
|
||||||
*/
|
|
||||||
public function jsonToActivity(array $jsonData, &$articleId);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* send target-friendly json from ActivityPub activity
|
|
||||||
*
|
|
||||||
* @param \Federator\Data\FedUser $sender the user of the sender
|
|
||||||
* @param \Federator\Data\ActivityPub\Common\Activity $activity the activity
|
|
||||||
* @return boolean did we successfully send the activity?
|
|
||||||
*/
|
|
||||||
public function sendActivity($sender, $activity);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* check if the headers include a valid signature
|
|
||||||
*
|
|
||||||
* @param string[] $headers the headers
|
|
||||||
* @throws \Federator\Exceptions\PermissionDenied
|
|
||||||
* @return string|\Federator\Exceptions\PermissionDenied
|
|
||||||
*/
|
|
||||||
public function checkSignature($headers);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +0,0 @@
|
||||||
<?php
|
|
||||||
/**
|
|
||||||
* SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* @author Yannis Vogel (vogeldevelopment)
|
|
||||||
**/
|
|
||||||
|
|
||||||
namespace Federator\Data\ActivityPub\Common;
|
|
||||||
|
|
||||||
class Accept extends Activity
|
|
||||||
{
|
|
||||||
public function __construct()
|
|
||||||
{
|
|
||||||
parent::__construct('Accept');
|
|
||||||
parent::addContext('https://www.w3.org/ns/activitystreams');
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -74,7 +74,6 @@ class Activity extends APObject
|
||||||
{
|
{
|
||||||
if (array_key_exists('actor', $json)) {
|
if (array_key_exists('actor', $json)) {
|
||||||
$this->actor = $json['actor'];
|
$this->actor = $json['actor'];
|
||||||
$this->aactor = $json['actor'];
|
|
||||||
unset($json['actor']);
|
unset($json['actor']);
|
||||||
}
|
}
|
||||||
if (!parent::fromJson($json)) {
|
if (!parent::fromJson($json)) {
|
||||||
|
@ -103,7 +102,7 @@ class Activity extends APObject
|
||||||
/**
|
/**
|
||||||
* get Child Object
|
* get Child Object
|
||||||
*
|
*
|
||||||
* @return APObject|string|null
|
* @return APObject|null
|
||||||
*/
|
*/
|
||||||
public function getObject()
|
public function getObject()
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,41 +0,0 @@
|
||||||
<?php
|
|
||||||
/**
|
|
||||||
* SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* @author Yannis Vogel (vogeldevelopment)
|
|
||||||
**/
|
|
||||||
|
|
||||||
namespace Federator\Data\ActivityPub\Common;
|
|
||||||
|
|
||||||
class Announce extends Activity
|
|
||||||
{
|
|
||||||
public function __construct()
|
|
||||||
{
|
|
||||||
parent::__construct('Announce');
|
|
||||||
parent::addContext('https://www.w3.org/ns/activitystreams');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* convert internal state to php array
|
|
||||||
*
|
|
||||||
* @return array<string,mixed>
|
|
||||||
*/
|
|
||||||
public function toObject()
|
|
||||||
{
|
|
||||||
$return = parent::toObject();
|
|
||||||
$return['type'] = 'Announce';
|
|
||||||
return $return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* create object from json
|
|
||||||
*
|
|
||||||
* @param array<string,mixed> $json input json
|
|
||||||
* @return bool true on success
|
|
||||||
*/
|
|
||||||
public function fromJson($json)
|
|
||||||
{
|
|
||||||
return parent::fromJson($json);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -27,7 +27,7 @@ class APObject implements \JsonSerializable
|
||||||
/**
|
/**
|
||||||
* child object
|
* child object
|
||||||
*
|
*
|
||||||
* @var APObject|string|null $object
|
* @var APObject|null $object
|
||||||
*/
|
*/
|
||||||
private $object = null;
|
private $object = null;
|
||||||
|
|
||||||
|
@ -347,7 +347,7 @@ class APObject implements \JsonSerializable
|
||||||
/**
|
/**
|
||||||
* set child object
|
* set child object
|
||||||
*
|
*
|
||||||
* @param APObject|string $object
|
* @param APObject $object
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function setObject($object)
|
public function setObject($object)
|
||||||
|
@ -358,7 +358,7 @@ class APObject implements \JsonSerializable
|
||||||
/**
|
/**
|
||||||
* get child object
|
* get child object
|
||||||
*
|
*
|
||||||
* @return APObject|string|null child object
|
* @return APObject|null child object
|
||||||
*/
|
*/
|
||||||
public function getObject()
|
public function getObject()
|
||||||
{
|
{
|
||||||
|
@ -376,17 +376,6 @@ class APObject implements \JsonSerializable
|
||||||
$this->summary = $summary;
|
$this->summary = $summary;
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* get summary
|
|
||||||
*
|
|
||||||
* @return string summary
|
|
||||||
*/
|
|
||||||
public function getSummary()
|
|
||||||
{
|
|
||||||
return $this->summary;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* set type
|
* set type
|
||||||
*
|
*
|
||||||
|
@ -470,16 +459,6 @@ class APObject implements \JsonSerializable
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* get name
|
|
||||||
*
|
|
||||||
* @return string name
|
|
||||||
*/
|
|
||||||
public function getName() : string
|
|
||||||
{
|
|
||||||
return $this->name;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* add Image
|
* add Image
|
||||||
*
|
*
|
||||||
|
@ -668,7 +647,7 @@ class APObject implements \JsonSerializable
|
||||||
if (array_key_exists('duration', $json)) {
|
if (array_key_exists('duration', $json)) {
|
||||||
try {
|
try {
|
||||||
$this->duration = new \DateInterval($json['duration']);
|
$this->duration = new \DateInterval($json['duration']);
|
||||||
} catch (\Throwable $unused_e) {
|
} catch (\Exception $unused_e) {
|
||||||
error_log("error parsing duration ". $json['duration']);
|
error_log("error parsing duration ". $json['duration']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -771,8 +750,8 @@ class APObject implements \JsonSerializable
|
||||||
if (array_key_exists('mediaType', $json)) {
|
if (array_key_exists('mediaType', $json)) {
|
||||||
$this->mediaType = $json['mediaType'];
|
$this->mediaType = $json['mediaType'];
|
||||||
}
|
}
|
||||||
if (array_key_exists('object', $json)) { // some actPub servers send strings in the object field
|
if (array_key_exists('object', $json)) {
|
||||||
$this->object = is_array($json['object']) ? \Federator\Data\ActivityPub\Factory::newFromJson($json['object'], "") : $json['object'];
|
$this->object = \Federator\Data\ActivityPub\Factory::newFromJson($json['object'], "");
|
||||||
}
|
}
|
||||||
if (array_key_exists('sensitive', $json)) {
|
if (array_key_exists('sensitive', $json)) {
|
||||||
$this->sensitive = $json['sensitive'];
|
$this->sensitive = $json['sensitive'];
|
||||||
|
@ -794,7 +773,7 @@ class APObject implements \JsonSerializable
|
||||||
* {@inheritDoc}
|
* {@inheritDoc}
|
||||||
* @see JsonSerializable::jsonSerialize()
|
* @see JsonSerializable::jsonSerialize()
|
||||||
*/
|
*/
|
||||||
public function jsonSerialize(): mixed
|
public function jsonSerialize()
|
||||||
{
|
{
|
||||||
return $this->toObject();
|
return $this->toObject();
|
||||||
}
|
}
|
||||||
|
@ -896,7 +875,7 @@ class APObject implements \JsonSerializable
|
||||||
$return['tag'] = $tags;
|
$return['tag'] = $tags;
|
||||||
}
|
}
|
||||||
if ($this->updated > 0) {
|
if ($this->updated > 0) {
|
||||||
$return['updated'] = gmdate("Y-m-d\TH:i:s\Z", $this->updated);
|
$return['updated'] = gmdate("Y-m-d\TH:i:S\Z", $this->updated);
|
||||||
}
|
}
|
||||||
if ($this->url !== '') {
|
if ($this->url !== '') {
|
||||||
$return['url'] = $this->url;
|
$return['url'] = $this->url;
|
||||||
|
@ -911,7 +890,7 @@ class APObject implements \JsonSerializable
|
||||||
$return['mediaType'] = $this->mediaType;
|
$return['mediaType'] = $this->mediaType;
|
||||||
}
|
}
|
||||||
if ($this->object !== null) {
|
if ($this->object !== null) {
|
||||||
$return['object'] = is_string($this->object) ? $this->object : $this->object->toObject();
|
$return['object'] = $this->object->toObject();
|
||||||
}
|
}
|
||||||
if ($this->atomURI !== '') {
|
if ($this->atomURI !== '') {
|
||||||
$return['atomUri'] = $this->atomURI;
|
$return['atomUri'] = $this->atomURI;
|
||||||
|
|
|
@ -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 !== '') {
|
||||||
|
@ -48,42 +48,14 @@ class Collection extends APObject
|
||||||
*/
|
*/
|
||||||
public function fromJson($json)
|
public function fromJson($json)
|
||||||
{
|
{
|
||||||
$success = parent::fromJson($json);
|
return parent::fromJson($json);
|
||||||
if (!$success) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (array_key_exists('totalItems', $json)) {
|
|
||||||
$this->totalItems = $json['totalItems'];
|
|
||||||
}
|
|
||||||
if (array_key_exists('first', $json)) {
|
|
||||||
$this->first = $json['first'];
|
|
||||||
}
|
|
||||||
if (array_key_exists('last', $json)) {
|
|
||||||
$this->last = $json['last'];
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public function count() : int
|
||||||
* set total items
|
|
||||||
*
|
|
||||||
* @param int $totalItems total items
|
|
||||||
*/
|
|
||||||
public function setTotalItems(int $totalItems): void
|
|
||||||
{
|
|
||||||
$this->totalItems = $totalItems;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getTotalItems(): int
|
|
||||||
{
|
{
|
||||||
return $this->totalItems;
|
return $this->totalItems;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getFirst(): string
|
|
||||||
{
|
|
||||||
return $this->first;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setFirst(string $url) : void
|
public function setFirst(string $url) : void
|
||||||
{
|
{
|
||||||
$this->first = $url;
|
$this->first = $url;
|
||||||
|
|
|
@ -26,9 +26,7 @@ class Create extends Activity
|
||||||
$return = parent::toObject();
|
$return = parent::toObject();
|
||||||
$return['type'] = 'Create';
|
$return['type'] = 'Create';
|
||||||
// overwrite id from url
|
// overwrite id from url
|
||||||
if ($this->getURL() !== '') {
|
|
||||||
$return['id'] = $this->getURL();
|
$return['id'] = $this->getURL();
|
||||||
}
|
|
||||||
return $return;
|
return $return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,36 +0,0 @@
|
||||||
<?php
|
|
||||||
/**
|
|
||||||
* SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* @author Yannis Vogel (vogeldevelopment)
|
|
||||||
**/
|
|
||||||
|
|
||||||
namespace Federator\Data\ActivityPub\Common;
|
|
||||||
|
|
||||||
class Delete extends Activity
|
|
||||||
{
|
|
||||||
public function __construct()
|
|
||||||
{
|
|
||||||
parent::__construct('Delete');
|
|
||||||
parent::addContext('https://www.w3.org/ns/activitystreams');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* create from json/array
|
|
||||||
* @param mixed $json
|
|
||||||
*/
|
|
||||||
public function fromJson($json): bool
|
|
||||||
{
|
|
||||||
return parent::fromJson($json);
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* convert internal state to php array
|
|
||||||
* @return array<string,mixed>
|
|
||||||
*/
|
|
||||||
public function toObject()
|
|
||||||
{
|
|
||||||
$return = parent::toObject();
|
|
||||||
return $return;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,18 +0,0 @@
|
||||||
<?php
|
|
||||||
/**
|
|
||||||
* SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* @author Yannis Vogel (vogeldevelopment)
|
|
||||||
**/
|
|
||||||
|
|
||||||
namespace Federator\Data\ActivityPub\Common;
|
|
||||||
|
|
||||||
class Dislike extends Activity
|
|
||||||
{
|
|
||||||
public function __construct()
|
|
||||||
{
|
|
||||||
parent::__construct('Dislike');
|
|
||||||
parent::addContext('https://www.w3.org/ns/activitystreams');
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,55 +0,0 @@
|
||||||
<?php
|
|
||||||
/**
|
|
||||||
* SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* @author Yannis Vogel (vogeldevelopment)
|
|
||||||
**/
|
|
||||||
|
|
||||||
namespace Federator\Data\ActivityPub\Common;
|
|
||||||
|
|
||||||
class Follow extends Activity
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* object overwrite
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
private $object = "";
|
|
||||||
|
|
||||||
public function setFObject(string $object): void
|
|
||||||
{
|
|
||||||
$this->object = $object;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getObject(): string
|
|
||||||
{
|
|
||||||
return $this->object;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function __construct()
|
|
||||||
{
|
|
||||||
parent::__construct("Follow");
|
|
||||||
parent::addContext('https://www.w3.org/ns/activitystreams');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function fromJson($json): bool
|
|
||||||
{
|
|
||||||
if (array_key_exists('object', $json)) {
|
|
||||||
$this->object = $json['object'];
|
|
||||||
unset($json['object']);
|
|
||||||
}
|
|
||||||
return parent::fromJson($json);
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* convert internal state to php array
|
|
||||||
* @return array<string,mixed>
|
|
||||||
*/
|
|
||||||
public function toObject()
|
|
||||||
{
|
|
||||||
$return = parent::toObject();
|
|
||||||
if ($this->object !== "") {
|
|
||||||
$return['object'] = $this->object;
|
|
||||||
}
|
|
||||||
return $return;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,53 +0,0 @@
|
||||||
<?php
|
|
||||||
/**
|
|
||||||
* SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* @author Yannis Vogel (vogeldevelopment)
|
|
||||||
**/
|
|
||||||
|
|
||||||
namespace Federator\Data\ActivityPub\Common;
|
|
||||||
|
|
||||||
class Followers extends OrderedCollectionPage
|
|
||||||
{
|
|
||||||
public function __construct()
|
|
||||||
{
|
|
||||||
parent::__construct();
|
|
||||||
parent::addContext('https://www.w3.org/ns/activitystreams');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* set items
|
|
||||||
*
|
|
||||||
* @param string[] $items the items in the collection
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function setItems(&$items)
|
|
||||||
{
|
|
||||||
// Optionally: type check that all $items are Activity objects
|
|
||||||
$this->orderedItems = $items;
|
|
||||||
$this->totalItems = sizeof($items);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* convert internal state to php array
|
|
||||||
*
|
|
||||||
* @return array<string,mixed>
|
|
||||||
*/
|
|
||||||
public function toObject()
|
|
||||||
{
|
|
||||||
$return = parent::toObject();
|
|
||||||
return $return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* create object from json
|
|
||||||
*
|
|
||||||
* @param array<string,mixed> $json input json
|
|
||||||
* @return bool true on success
|
|
||||||
*/
|
|
||||||
public function fromJson($json)
|
|
||||||
{
|
|
||||||
return parent::fromJson($json);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,53 +0,0 @@
|
||||||
<?php
|
|
||||||
/**
|
|
||||||
* SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* @author Yannis Vogel (vogeldevelopment)
|
|
||||||
**/
|
|
||||||
|
|
||||||
namespace Federator\Data\ActivityPub\Common;
|
|
||||||
|
|
||||||
class Following extends OrderedCollectionPage
|
|
||||||
{
|
|
||||||
public function __construct()
|
|
||||||
{
|
|
||||||
parent::__construct();
|
|
||||||
parent::addContext('https://www.w3.org/ns/activitystreams');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* set items
|
|
||||||
*
|
|
||||||
* @param string[] $items the items in the collection
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function setItems(&$items)
|
|
||||||
{
|
|
||||||
// Optionally: type check that all $items are Activity objects
|
|
||||||
$this->orderedItems = $items;
|
|
||||||
$this->totalItems = sizeof($items);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* convert internal state to php array
|
|
||||||
*
|
|
||||||
* @return array<string,mixed>
|
|
||||||
*/
|
|
||||||
public function toObject()
|
|
||||||
{
|
|
||||||
$return = parent::toObject();
|
|
||||||
return $return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* create object from json
|
|
||||||
*
|
|
||||||
* @param array<string,mixed> $json input json
|
|
||||||
* @return bool true on success
|
|
||||||
*/
|
|
||||||
public function fromJson($json)
|
|
||||||
{
|
|
||||||
return parent::fromJson($json);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,50 +0,0 @@
|
||||||
<?php
|
|
||||||
/**
|
|
||||||
* SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* @author Yannis Vogel (vogeldevelopment)
|
|
||||||
**/
|
|
||||||
|
|
||||||
namespace Federator\Data\ActivityPub\Common;
|
|
||||||
|
|
||||||
class Inbox extends OrderedCollectionPage
|
|
||||||
{
|
|
||||||
public function __construct()
|
|
||||||
{
|
|
||||||
parent::__construct();
|
|
||||||
parent::addContext('https://www.w3.org/ns/activitystreams');
|
|
||||||
parent::addContexts([
|
|
||||||
"ostatus" => "http://ostatus.org#",
|
|
||||||
"atomUri" => "ostatus:atomUri",
|
|
||||||
"inReplyToAtomUri" => "ostatus:inReplyToAtomUri",
|
|
||||||
"conversation" => "ostatus:conversation",
|
|
||||||
"sensitive" => "as:sensitive",
|
|
||||||
"toot" => "http://joinmastodon.org/ns#",
|
|
||||||
"votersCount" => "toot:votersCount",
|
|
||||||
"Hashtag" => "as:Hashtag"
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* convert internal state to php array
|
|
||||||
*
|
|
||||||
* @return array<string,mixed>
|
|
||||||
*/
|
|
||||||
public function toObject()
|
|
||||||
{
|
|
||||||
$return = parent::toObject();
|
|
||||||
return $return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* create object from json
|
|
||||||
*
|
|
||||||
* @param array<string,mixed> $json input json
|
|
||||||
* @return bool true on success
|
|
||||||
*/
|
|
||||||
public function fromJson($json)
|
|
||||||
{
|
|
||||||
return parent::fromJson($json);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,18 +0,0 @@
|
||||||
<?php
|
|
||||||
/**
|
|
||||||
* SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* @author Yannis Vogel (vogeldevelopment)
|
|
||||||
**/
|
|
||||||
|
|
||||||
namespace Federator\Data\ActivityPub\Common;
|
|
||||||
|
|
||||||
class Like extends Activity
|
|
||||||
{
|
|
||||||
public function __construct()
|
|
||||||
{
|
|
||||||
parent::__construct('Like');
|
|
||||||
parent::addContext('https://www.w3.org/ns/activitystreams');
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -13,9 +13,9 @@ class OrderedCollection extends Collection
|
||||||
/**
|
/**
|
||||||
* nested items
|
* nested items
|
||||||
*
|
*
|
||||||
* @var APObject[]|string[]
|
* @var APObject[]
|
||||||
*/
|
*/
|
||||||
protected $orderedItems = [];
|
protected $items = [];
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
|
@ -32,13 +32,9 @@ class OrderedCollection extends Collection
|
||||||
{
|
{
|
||||||
$return = parent::toObject();
|
$return = parent::toObject();
|
||||||
$return['type'] = 'OrderedCollection';
|
$return['type'] = 'OrderedCollection';
|
||||||
if (sizeof($this->orderedItems) > 0) {
|
if ($this->totalItems > 0) {
|
||||||
foreach ($this->orderedItems as $item) {
|
foreach ($this->items as $item) {
|
||||||
if (is_string($item)) {
|
$return['OrderedItems'][] = $item->toObject();
|
||||||
$return['orderedItems'][] = $item;
|
|
||||||
} elseif (is_object($item)) {
|
|
||||||
$return['orderedItems'][] = $item->toObject();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return $return;
|
return $return;
|
||||||
|
@ -52,63 +48,44 @@ class OrderedCollection extends Collection
|
||||||
*/
|
*/
|
||||||
public function fromJson($json)
|
public function fromJson($json)
|
||||||
{
|
{
|
||||||
$success = parent::fromJson($json);
|
return parent::fromJson($json);
|
||||||
if (!$success) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (array_key_exists('orderedItems', $json)) {
|
|
||||||
foreach ($json['orderedItems'] as $item) {
|
|
||||||
$obj = \Federator\Data\ActivityPub\Factory::newActivityFromJson($item);
|
|
||||||
if ($obj !== false) {
|
|
||||||
$this->orderedItems[] = $obj;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
public function append(APObject &$item) : void
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* add item to collection
|
|
||||||
* @param APObject|string $item
|
|
||||||
*/
|
|
||||||
public function append(&$item): void
|
|
||||||
{
|
{
|
||||||
$this->orderedItems[] = $item;
|
$this->items[] = $item;
|
||||||
|
$this->totalItems = sizeof($this->items);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* get item with given index
|
* get item with given index
|
||||||
*
|
*
|
||||||
* @return APObject|string|false
|
* @return APObject|false
|
||||||
*/
|
*/
|
||||||
public function get(int $index)
|
public function get(int $index)
|
||||||
{
|
{
|
||||||
if ($index >= 0) {
|
if ($index >= 0) {
|
||||||
if ($index >= sizeof($this->orderedItems)) {
|
if ($index >= $this->totalItems) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return $this->orderedItems[$index];
|
return $this->items[$index];
|
||||||
} else {
|
} else {
|
||||||
if (sizeof($this->orderedItems) + $index < 0) {
|
if ($this->totalItems+ $index < 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return $this->orderedItems[sizeof($this->orderedItems) + $index];
|
return $this->items[$this->totalItems + $index];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getCount(): int
|
|
||||||
{
|
|
||||||
return sizeof($this->orderedItems);
|
|
||||||
}
|
|
||||||
/**
|
/**
|
||||||
* set items
|
* set items
|
||||||
*
|
*
|
||||||
* @param APObject[]|string[] $items
|
* @param APObject[] $items
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function setItems(&$items)
|
public function setItems(&$items)
|
||||||
{
|
{
|
||||||
$this->orderedItems = $items;
|
$this->items = $items;
|
||||||
|
$this->totalItems = sizeof($items);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,6 @@ class OrderedCollectionPage extends OrderedCollection
|
||||||
{
|
{
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
parent::addContext('https://www.w3.org/ns/activitystreams');
|
parent::addContext('https://www.w3.org/ns/activitystreams');
|
||||||
$this->setType('OrderedCollectionPage');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -39,7 +38,7 @@ class OrderedCollectionPage extends OrderedCollection
|
||||||
if ($this->partOf !== '') {
|
if ($this->partOf !== '') {
|
||||||
$return['partOf'] = $this->partOf;
|
$return['partOf'] = $this->partOf;
|
||||||
}
|
}
|
||||||
$return['type'] = $this->getType();
|
$return['type'] = 'OrderedCollectionPage';
|
||||||
return $return;
|
return $return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,29 +50,7 @@ class OrderedCollectionPage extends OrderedCollection
|
||||||
*/
|
*/
|
||||||
public function fromJson($json)
|
public function fromJson($json)
|
||||||
{
|
{
|
||||||
$success = parent::fromJson($json);
|
return parent::fromJson($json);
|
||||||
if (!$success) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (array_key_exists('next', $json)) {
|
|
||||||
$this->next = $json['next'];
|
|
||||||
}
|
|
||||||
if (array_key_exists('prev', $json)) {
|
|
||||||
$this->prev = $json['prev'];
|
|
||||||
}
|
|
||||||
if (array_key_exists('partOf', $json)) {
|
|
||||||
$this->partOf = $json['partOf'];
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* get next url
|
|
||||||
* @return string next URL
|
|
||||||
*/
|
|
||||||
public function getNext()
|
|
||||||
{
|
|
||||||
return $this->next;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -4,7 +4,6 @@
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
*
|
||||||
* @author Sascha Nitsch (grumpydeveloper)
|
* @author Sascha Nitsch (grumpydeveloper)
|
||||||
* @author Yannis Vogel (vogeldevelopment)
|
|
||||||
**/
|
**/
|
||||||
|
|
||||||
namespace Federator\Data\ActivityPub\Common;
|
namespace Federator\Data\ActivityPub\Common;
|
||||||
|
@ -27,17 +26,6 @@ class Outbox extends OrderedCollectionPage
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* set items
|
|
||||||
*
|
|
||||||
* @param \Federator\Data\ActivityPub\Common\APObject[] $items the items in the collection
|
|
||||||
*/
|
|
||||||
public function setItems(&$items)
|
|
||||||
{
|
|
||||||
// Optionally: type check that all $items are Activity objects
|
|
||||||
$this->orderedItems = $items;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* convert internal state to php array
|
* convert internal state to php array
|
||||||
*
|
*
|
||||||
|
|
|
@ -1,18 +0,0 @@
|
||||||
<?php
|
|
||||||
/**
|
|
||||||
* SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* @author Yannis Vogel (vogeldevelopment)
|
|
||||||
**/
|
|
||||||
|
|
||||||
namespace Federator\Data\ActivityPub\Common;
|
|
||||||
|
|
||||||
class Reject extends Activity
|
|
||||||
{
|
|
||||||
public function __construct()
|
|
||||||
{
|
|
||||||
parent::__construct('Reject');
|
|
||||||
parent::addContext('https://www.w3.org/ns/activitystreams');
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,41 +0,0 @@
|
||||||
<?php
|
|
||||||
/**
|
|
||||||
* SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* @author Yannis Vogel (vogeldevelopment)
|
|
||||||
**/
|
|
||||||
|
|
||||||
namespace Federator\Data\ActivityPub\Common;
|
|
||||||
|
|
||||||
class Undo extends Activity
|
|
||||||
{
|
|
||||||
public function __construct()
|
|
||||||
{
|
|
||||||
parent::__construct('Undo');
|
|
||||||
parent::addContext('https://www.w3.org/ns/activitystreams');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* convert internal state to php array
|
|
||||||
*
|
|
||||||
* @return array<string,mixed>
|
|
||||||
*/
|
|
||||||
public function toObject()
|
|
||||||
{
|
|
||||||
$return = parent::toObject();
|
|
||||||
$return['type'] = 'Undo';
|
|
||||||
return $return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* create object from json
|
|
||||||
*
|
|
||||||
* @param array<string,mixed> $json input json
|
|
||||||
* @return bool true on success
|
|
||||||
*/
|
|
||||||
public function fromJson($json)
|
|
||||||
{
|
|
||||||
return parent::fromJson($json);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,16 +0,0 @@
|
||||||
Array
|
|
||||||
(
|
|
||||||
[type] => Document
|
|
||||||
[mediaType] => image/jpeg
|
|
||||||
[url] => https://noc.social/system/media_attachments/files/112/350/286/131/419/396/original/26ab9c8a4ab13f16.jpg
|
|
||||||
[name] => Screenshot Zeitleiste aus kdenlive.
|
|
||||||
[blurhash] => UC8|^tSwI-bu-taeRiaeu5e.aJjGsBWnR*jH
|
|
||||||
[focalPoint] => Array
|
|
||||||
(
|
|
||||||
[0] => -0.01
|
|
||||||
[1] => -0.79
|
|
||||||
)
|
|
||||||
|
|
||||||
[width] => 1333
|
|
||||||
[height] => 651
|
|
||||||
)
|
|
|
@ -1,45 +0,0 @@
|
||||||
<?php
|
|
||||||
/**
|
|
||||||
* SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* @author Yannis Vogel (vogeldevelopment)
|
|
||||||
**/
|
|
||||||
|
|
||||||
namespace Federator\Data\ActivityPub\Common;
|
|
||||||
|
|
||||||
class Update extends Activity
|
|
||||||
{
|
|
||||||
public function __construct()
|
|
||||||
{
|
|
||||||
parent::__construct('Update');
|
|
||||||
parent::addContext('https://www.w3.org/ns/activitystreams');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* convert internal state to php array
|
|
||||||
*
|
|
||||||
* @return array<string,mixed>
|
|
||||||
*/
|
|
||||||
public function toObject()
|
|
||||||
{
|
|
||||||
$return = parent::toObject();
|
|
||||||
$return['type'] = 'Update';
|
|
||||||
// overwrite id from url
|
|
||||||
if ($this->getURL() !== '') {
|
|
||||||
$return['id'] = $this->getURL();
|
|
||||||
}
|
|
||||||
return $return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* create object from json
|
|
||||||
*
|
|
||||||
* @param array<string,mixed> $json input json
|
|
||||||
* @return bool true on success
|
|
||||||
*/
|
|
||||||
public function fromJson($json)
|
|
||||||
{
|
|
||||||
return parent::fromJson($json);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,27 +0,0 @@
|
||||||
<?php
|
|
||||||
/**
|
|
||||||
* SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* @author Yannis Vogel (vogeldevelopment)
|
|
||||||
**/
|
|
||||||
|
|
||||||
namespace Federator\Data\ActivityPub\Common;
|
|
||||||
|
|
||||||
class Vote extends APObject
|
|
||||||
{
|
|
||||||
public function __construct()
|
|
||||||
{
|
|
||||||
parent::__construct('Vote');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* create object from json
|
|
||||||
* @param mixed $json input
|
|
||||||
* @return bool true on success
|
|
||||||
*/
|
|
||||||
public function fromJson($json)
|
|
||||||
{
|
|
||||||
return parent::fromJson($json);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -16,7 +16,7 @@ class Factory
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* create object tree from json
|
* create object tree from json
|
||||||
* @param array<string, mixed>|mixed $json input json
|
* @param array<string, mixed> $json input json
|
||||||
* @return Common\APObject|null object or false on error
|
* @return Common\APObject|null object or false on error
|
||||||
*/
|
*/
|
||||||
public static function newFromJson($json, string $jsonstring)
|
public static function newFromJson($json, string $jsonstring)
|
||||||
|
@ -31,9 +31,6 @@ class Factory
|
||||||
}
|
}
|
||||||
$return = null;
|
$return = null;
|
||||||
switch ($json['type']) {
|
switch ($json['type']) {
|
||||||
case 'Announce':
|
|
||||||
$return = new Common\Announce();
|
|
||||||
break;
|
|
||||||
case 'Article':
|
case 'Article':
|
||||||
$return = new Common\Article();
|
$return = new Common\Article();
|
||||||
break;
|
break;
|
||||||
|
@ -42,45 +39,21 @@ class Factory
|
||||||
break;
|
break;
|
||||||
case 'Event':
|
case 'Event':
|
||||||
$return = new Common\Event();
|
$return = new Common\Event();
|
||||||
break;*/
|
break;
|
||||||
case 'Follow':
|
case 'Follow':
|
||||||
$return = new Common\Follow();
|
$return = new Common\Follow();
|
||||||
break;
|
break;*/
|
||||||
case 'Image':
|
case 'Image':
|
||||||
$return = new Common\Image();
|
$return = new Common\Image();
|
||||||
break;
|
break;
|
||||||
case 'Note':
|
/*case 'Note':
|
||||||
$return = new Common\Note();
|
$return = new Common\Note();
|
||||||
break;
|
break;
|
||||||
case 'Outbox':
|
case 'Question':
|
||||||
$return = new Common\Outbox();
|
$return = new \Common\Question();
|
||||||
break;
|
|
||||||
case 'Vote':
|
|
||||||
$return = new Common\Vote();
|
|
||||||
break;
|
|
||||||
case 'Like':
|
|
||||||
$return = new Common\Like();
|
|
||||||
break;
|
|
||||||
case 'Dislike':
|
|
||||||
$return = new Common\Dislike();
|
|
||||||
break;
|
|
||||||
case 'Inbox':
|
|
||||||
$return = new Common\Inbox();
|
|
||||||
break;
|
|
||||||
case 'OrderedCollection':
|
|
||||||
$return = new Common\OrderedCollection();
|
|
||||||
break;
|
|
||||||
case 'OrderedCollectionPage':
|
|
||||||
$return = new Common\OrderedCollectionPage();
|
|
||||||
break;
|
|
||||||
case 'Tombstone':
|
|
||||||
$return = new Common\APObject("Tombstone");
|
|
||||||
break;
|
|
||||||
/*case 'Question':
|
|
||||||
$return = new Common\Question();
|
|
||||||
break;
|
break;
|
||||||
case 'Video':
|
case 'Video':
|
||||||
$return = new Common\Video();
|
$return = new \Common\Video();
|
||||||
break;*/
|
break;*/
|
||||||
default:
|
default:
|
||||||
error_log("newFromJson: unknown type: '" . $json['type'] . "' " . $jsonstring);
|
error_log("newFromJson: unknown type: '" . $json['type'] . "' " . $jsonstring);
|
||||||
|
@ -104,7 +77,9 @@ class Factory
|
||||||
}
|
}
|
||||||
//$return = false;
|
//$return = false;
|
||||||
switch ($json['type']) {
|
switch ($json['type']) {
|
||||||
case 'Accept':
|
case 'MakePhanHappy':
|
||||||
|
break;
|
||||||
|
/* case 'Accept':
|
||||||
$return = new Common\Accept();
|
$return = new Common\Accept();
|
||||||
break;
|
break;
|
||||||
case 'Announce':
|
case 'Announce':
|
||||||
|
@ -116,30 +91,18 @@ class Factory
|
||||||
case 'Delete':
|
case 'Delete':
|
||||||
$return = new Common\Delete();
|
$return = new Common\Delete();
|
||||||
break;
|
break;
|
||||||
case 'Like':
|
|
||||||
$return = new Common\Like();
|
|
||||||
break;
|
|
||||||
case 'Dislike':
|
|
||||||
$return = new Common\Dislike();
|
|
||||||
break;
|
|
||||||
case 'Follow':
|
case 'Follow':
|
||||||
$return = new Common\Follow();
|
$return = new Common\Follow();
|
||||||
break;
|
break;
|
||||||
case 'Reject':
|
|
||||||
$return = new Common\Reject();
|
|
||||||
break;
|
|
||||||
case 'Undo':
|
case 'Undo':
|
||||||
$return = new Common\Undo();
|
$return = new \Common\Undo();
|
||||||
break;
|
break;*/
|
||||||
case 'Update':
|
|
||||||
$return = new Common\Update();
|
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
error_log("newActivityFromJson unsupported type: " . print_r($json, true));
|
error_log("newActivityFromJson " . print_r($json, true));
|
||||||
}
|
}
|
||||||
if (isset($return) && $return->fromJson($json) !== null) {
|
/*if ($return !== false && $return->fromJson($json) !== null) {
|
||||||
return $return;
|
return $return;
|
||||||
}
|
}*/
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,151 +0,0 @@
|
||||||
<?php
|
|
||||||
/**
|
|
||||||
* SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* @author Yannis Vogel (vogeldevelopment)
|
|
||||||
**/
|
|
||||||
|
|
||||||
namespace Federator\Data;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* storage class for user attributes
|
|
||||||
*/
|
|
||||||
class FedUser
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* user id
|
|
||||||
*
|
|
||||||
* @var string $id
|
|
||||||
*/
|
|
||||||
public $id;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* user url
|
|
||||||
*
|
|
||||||
* @var string $actorURL
|
|
||||||
*/
|
|
||||||
public $actorURL;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* user name
|
|
||||||
*
|
|
||||||
* @var string $name
|
|
||||||
*/
|
|
||||||
public $name = '';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* user public key
|
|
||||||
*
|
|
||||||
* @var string $publicKey
|
|
||||||
*/
|
|
||||||
public $publicKey;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* summary for user/profile
|
|
||||||
*
|
|
||||||
* @var string $summary
|
|
||||||
*/
|
|
||||||
public $summary = '';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* type of user (person/group)
|
|
||||||
*
|
|
||||||
* @var string $type
|
|
||||||
*/
|
|
||||||
public $type = 'Person';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* inbox URL
|
|
||||||
*
|
|
||||||
* @var string $inboxURL
|
|
||||||
*/
|
|
||||||
public $inboxURL;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* shared inbox URL
|
|
||||||
*
|
|
||||||
* @var string $sharedInboxURL
|
|
||||||
*/
|
|
||||||
public $sharedInboxURL;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* followers URL
|
|
||||||
*
|
|
||||||
* @var string $followersURL
|
|
||||||
*/
|
|
||||||
public $followersURL;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* following URL
|
|
||||||
*
|
|
||||||
* @var string $followingURL
|
|
||||||
*/
|
|
||||||
public $followingURL;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* public key ID
|
|
||||||
*
|
|
||||||
* @var string $publicKeyId
|
|
||||||
*/
|
|
||||||
public $publicKeyId;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* outbox URL
|
|
||||||
*
|
|
||||||
* @var string $outboxURL
|
|
||||||
*/
|
|
||||||
public $outboxURL;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* create new user object from json string
|
|
||||||
*
|
|
||||||
* @param string $input input string
|
|
||||||
* @return FedUser|false
|
|
||||||
*/
|
|
||||||
public static function createFromJson($input)
|
|
||||||
{
|
|
||||||
$data = json_decode($input, true);
|
|
||||||
if ($data === null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
$user = new FedUser();
|
|
||||||
$user->id = $data['id'] ?? '';
|
|
||||||
$user->actorURL = $data['actorURL'] ?? '';
|
|
||||||
$user->name = $data['name'] ?? '';
|
|
||||||
$user->publicKey = $data['publicKey'] ?? '';
|
|
||||||
$user->summary = $data['summary'] ?? '';
|
|
||||||
$user->type = $data['type'] ?? 'Person';
|
|
||||||
$user->inboxURL = $data['inbox'] ?? '';
|
|
||||||
$user->sharedInboxURL = $data['sharedInbox'] ?? '';
|
|
||||||
$user->followersURL = $data['followers'] ?? '';
|
|
||||||
$user->followingURL = $data['following'] ?? '';
|
|
||||||
$user->publicKeyId = $data['publicKeyId'] ?? '';
|
|
||||||
$user->outboxURL = $data['outbox'] ?? '';
|
|
||||||
return $user;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* convert internal data to json string
|
|
||||||
*
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public function toJson()
|
|
||||||
{
|
|
||||||
$data = [
|
|
||||||
'id' => $this->id,
|
|
||||||
'actorURL' => $this->actorURL,
|
|
||||||
'name' => $this->name,
|
|
||||||
'publicKey' => $this->publicKey,
|
|
||||||
'summary' => $this->summary,
|
|
||||||
'type' => $this->type,
|
|
||||||
'inbox' => $this->inboxURL,
|
|
||||||
'sharedInbox' => $this->sharedInboxURL,
|
|
||||||
'followers' => $this->followersURL,
|
|
||||||
'following' => $this->followingURL,
|
|
||||||
'publicKeyId' => $this->publicKeyId,
|
|
||||||
'outbox' => $this->outboxURL,
|
|
||||||
];
|
|
||||||
return json_encode($data) ?: '';
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,67 +0,0 @@
|
||||||
<?php
|
|
||||||
/**
|
|
||||||
* SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* @author Yannis Vogel (vogeldevelopment)
|
|
||||||
**/
|
|
||||||
|
|
||||||
namespace Federator\DIO;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* IO functions related to articles
|
|
||||||
*/
|
|
||||||
class Article
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Convert an Article to a Note
|
|
||||||
*
|
|
||||||
* @param \Federator\Data\ActivityPub\Common\Article $article
|
|
||||||
* @return \Federator\Data\ActivityPub\Common\Note
|
|
||||||
* The generated note
|
|
||||||
*/
|
|
||||||
public static function convertToNote($article)
|
|
||||||
{
|
|
||||||
$note = new \Federator\Data\ActivityPub\Common\Note();
|
|
||||||
$note->setId($article->getId())
|
|
||||||
->setURL($article->getURL());
|
|
||||||
$note->setContent($article->getContent());
|
|
||||||
$note->setSummary($article->getSummary());
|
|
||||||
$note->setPublished($article->getPublished());
|
|
||||||
$note->setName($article->getName());
|
|
||||||
$note->setAttributedTo($article->getAttributedTo());
|
|
||||||
foreach ($article->getTo() as $to) {
|
|
||||||
$note->addTo($to);
|
|
||||||
}
|
|
||||||
foreach ($article->getCc() as $cc) {
|
|
||||||
$note->addCc($cc);
|
|
||||||
}
|
|
||||||
return $note;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Conditionally convert article to a note
|
|
||||||
*
|
|
||||||
* @param \Federator\Data\ActivityPub\Common\Article $article
|
|
||||||
* @param string $targetUrl
|
|
||||||
* The target URL for the activity (e.g. mastodon.social)
|
|
||||||
* @return \Federator\Data\ActivityPub\Common\Note|\Federator\Data\ActivityPub\Common\Article
|
|
||||||
* The generated note on success, false on failure
|
|
||||||
*/
|
|
||||||
public static function conditionalConvertToNote($article, $targetUrl)
|
|
||||||
{
|
|
||||||
$supportFile = file_get_contents($_SERVER['DOCUMENT_ROOT'] . '../formatsupport.json');
|
|
||||||
if ($supportFile === false) {
|
|
||||||
error_log("Article::conditionalConvertToNote Failed to read support file for article conversion.");
|
|
||||||
return $article; // Fallback to original article if file read fails
|
|
||||||
}
|
|
||||||
$supportlist = json_decode($supportFile, true);
|
|
||||||
|
|
||||||
if (!isset($supportlist['activitypub']['article']) ||
|
|
||||||
!is_array($supportlist['activitypub']['article']) ||
|
|
||||||
!in_array($targetUrl, $supportlist['activitypub']['article'], true)
|
|
||||||
) {
|
|
||||||
return self::convertToNote($article); // Articles are not supported for this target
|
|
||||||
}
|
|
||||||
return $article; // Articles are supported, return as is
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,469 +0,0 @@
|
||||||
<?php
|
|
||||||
/**
|
|
||||||
* SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* @author Yannis Vogel (vogeldevelopment)
|
|
||||||
**/
|
|
||||||
|
|
||||||
namespace Federator\DIO;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* IO functions related to fedUsers
|
|
||||||
*/
|
|
||||||
class FedUser
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* add local user based on given user object received from remote service
|
|
||||||
* @param \mysqli $dbh database handle
|
|
||||||
* @param \Federator\Data\FedUser $user user object to use
|
|
||||||
* @param string $_user user/profile name
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
protected static function addUserToDB($dbh, $user, $_user)
|
|
||||||
{
|
|
||||||
// check if it is timed out user
|
|
||||||
$sql = 'select unix_timestamp(`validuntil`) from fedusers where id=?';
|
|
||||||
$stmt = $dbh->prepare($sql);
|
|
||||||
if ($stmt === false) {
|
|
||||||
throw new \Federator\Exceptions\ServerError('FedUser::addLocalUser Failed to prepare statement');
|
|
||||||
}
|
|
||||||
$stmt->bind_param('s', $_user);
|
|
||||||
$validuntil = 0;
|
|
||||||
$ret = $stmt->bind_result($validuntil);
|
|
||||||
$stmt->execute();
|
|
||||||
if ($ret) {
|
|
||||||
$stmt->fetch();
|
|
||||||
}
|
|
||||||
$stmt->close();
|
|
||||||
if ($validuntil == 0) {
|
|
||||||
$sql = 'insert into fedusers (id, url, name, publickey, summary, type, inboxurl, sharedinboxurl,';
|
|
||||||
$sql .= ' followersurl, followingurl, publickeyid, outboxurl, validuntil)';
|
|
||||||
$sql .= ' values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, now() + interval 1 day)';
|
|
||||||
$stmt = $dbh->prepare($sql);
|
|
||||||
if ($stmt === false) {
|
|
||||||
throw new \Federator\Exceptions\ServerError('FedUser::addLocalUser Failed to prepare create statement');
|
|
||||||
}
|
|
||||||
$stmt->bind_param(
|
|
||||||
'ssssssssssss',
|
|
||||||
$_user,
|
|
||||||
$user->actorURL,
|
|
||||||
$user->name,
|
|
||||||
$user->publicKey,
|
|
||||||
$user->summary,
|
|
||||||
$user->type,
|
|
||||||
$user->inboxURL,
|
|
||||||
$user->sharedInboxURL,
|
|
||||||
$user->followersURL,
|
|
||||||
$user->followingURL,
|
|
||||||
$user->publicKeyId,
|
|
||||||
$user->outboxURL
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// update to existing user
|
|
||||||
$sql = 'update fedusers set validuntil=now() + interval 1 day, url=?, name=?, publickey=?, summary=?,';
|
|
||||||
$sql .= ' type=?, inboxurl=?, sharedinboxurl=?, followersurl=?, followingurl=?, publickeyid=?, outboxurl=?';
|
|
||||||
$sql .= ' where id=?';
|
|
||||||
$stmt = $dbh->prepare($sql);
|
|
||||||
if ($stmt === false) {
|
|
||||||
throw new \Federator\Exceptions\ServerError('FedUser::extendUser Failed to prepare update statement');
|
|
||||||
}
|
|
||||||
$stmt->bind_param(
|
|
||||||
'ssssssssssss',
|
|
||||||
$user->actorURL,
|
|
||||||
$user->name,
|
|
||||||
$user->publicKey,
|
|
||||||
$user->summary,
|
|
||||||
$user->type,
|
|
||||||
$user->inboxURL,
|
|
||||||
$user->sharedInboxURL,
|
|
||||||
$user->followersURL,
|
|
||||||
$user->followingURL,
|
|
||||||
$user->publicKeyId,
|
|
||||||
$user->outboxURL,
|
|
||||||
$_user
|
|
||||||
);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
$stmt->execute();
|
|
||||||
$stmt->close();
|
|
||||||
$user->id = $_user;
|
|
||||||
} catch (\mysqli_sql_exception $e) {
|
|
||||||
error_log($sql);
|
|
||||||
error_log(print_r($user, true));
|
|
||||||
error_log($e->getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* extend the given user with internal data
|
|
||||||
* @param \mysqli $dbh database handle
|
|
||||||
* @param \Federator\Data\FedUser $user user to extend
|
|
||||||
* @param string $_user user/profile name
|
|
||||||
*/
|
|
||||||
protected static function extendUser(\mysqli $dbh, \Federator\Data\FedUser $user, $_user): void
|
|
||||||
{
|
|
||||||
$sql = 'select id,unix_timestamp(`validuntil`) from fedusers where id=?';
|
|
||||||
$stmt = $dbh->prepare($sql);
|
|
||||||
if ($stmt === false) {
|
|
||||||
throw new \Federator\Exceptions\ServerError('FedUser::extendUser Failed to prepare statement');
|
|
||||||
}
|
|
||||||
$stmt->bind_param('s', $_user);
|
|
||||||
$validuntil = 0;
|
|
||||||
$ret = $stmt->bind_result($user->id, $validuntil);
|
|
||||||
$stmt->execute();
|
|
||||||
if ($ret) {
|
|
||||||
$stmt->fetch();
|
|
||||||
}
|
|
||||||
$stmt->close();
|
|
||||||
// if a new user, create own database entry with additionally needed info
|
|
||||||
if ($user->id === null || $validuntil < time()) {
|
|
||||||
self::addUserToDB($dbh, $user, $_user);
|
|
||||||
}
|
|
||||||
|
|
||||||
// no further processing for now
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* get user by name
|
|
||||||
*
|
|
||||||
* @param \mysqli $dbh
|
|
||||||
* database handle
|
|
||||||
* @param string $_name
|
|
||||||
* user name
|
|
||||||
* @param \Federator\Cache\Cache|null $cache
|
|
||||||
* optional caching service
|
|
||||||
* @return \Federator\Data\FedUser
|
|
||||||
*/
|
|
||||||
public static function getUserByName($dbh, $_name, $cache)
|
|
||||||
{
|
|
||||||
$user = false;
|
|
||||||
|
|
||||||
// ask cache
|
|
||||||
if ($cache !== null) {
|
|
||||||
$user = $cache->getRemoteFedUserByName($_name);
|
|
||||||
}
|
|
||||||
if ($user !== false) {
|
|
||||||
return $user;
|
|
||||||
}
|
|
||||||
// check our db
|
|
||||||
$sql = 'select `id`, `url`, `name`, `publickey`, `summary`, `type`, `inboxurl`, `sharedinboxurl`, ';
|
|
||||||
$sql .= '`followersurl`, `followingurl`, `publickeyid`, `outboxurl`';
|
|
||||||
$sql .= ' from fedusers where `id`=? and `validuntil`>=now()';
|
|
||||||
$stmt = $dbh->prepare($sql);
|
|
||||||
if ($stmt === false) {
|
|
||||||
throw new \Federator\Exceptions\ServerError('FedUser::getUserByName Failed to prepare statement');
|
|
||||||
}
|
|
||||||
$stmt->bind_param('s', $_name);
|
|
||||||
$user = new \Federator\Data\FedUser();
|
|
||||||
$ret = $stmt->bind_result(
|
|
||||||
$user->id,
|
|
||||||
$user->actorURL,
|
|
||||||
$user->name,
|
|
||||||
$user->publicKey,
|
|
||||||
$user->summary,
|
|
||||||
$user->type,
|
|
||||||
$user->inboxURL,
|
|
||||||
$user->sharedInboxURL,
|
|
||||||
$user->followersURL,
|
|
||||||
$user->followingURL,
|
|
||||||
$user->publicKeyId,
|
|
||||||
$user->outboxURL
|
|
||||||
);
|
|
||||||
$stmt->execute();
|
|
||||||
if ($ret) {
|
|
||||||
$stmt->fetch();
|
|
||||||
}
|
|
||||||
$stmt->close();
|
|
||||||
|
|
||||||
if ($user->id === null) {
|
|
||||||
// check if its a federated user with username@domain.ending
|
|
||||||
if (preg_match("/^([^@]+)@(.*)$/", $_name, $matches) == 1) {
|
|
||||||
// make webfinger request
|
|
||||||
$remoteURL = 'https://' . $matches[2] . '/.well-known/webfinger?resource=acct:' . urlencode($_name);
|
|
||||||
$headers = ['Accept: application/activity+json'];
|
|
||||||
[$response, $info] = \Federator\Main::getFromRemote($remoteURL, $headers);
|
|
||||||
if ($info['http_code'] != 200) {
|
|
||||||
throw new \Federator\Exceptions\ServerError('FedUser::getUserByName Failed to fetch webfinger 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 webfinger for '
|
|
||||||
. $_name);
|
|
||||||
}
|
|
||||||
// get the webwinger user url and fetch the user
|
|
||||||
if (isset($r['links'])) {
|
|
||||||
foreach ($r['links'] as $link) {
|
|
||||||
if (isset($link['rel']) && $link['rel'] === 'self') {
|
|
||||||
$remoteURL = $link['href'];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!isset($remoteURL)) {
|
|
||||||
throw new \Federator\Exceptions\ServerError('FedUser::getUserByName Failed to find self link '
|
|
||||||
. 'in webfinger for ' . $_name);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$remoteURL = $_name;
|
|
||||||
}
|
|
||||||
// fetch the user
|
|
||||||
$headers = ['Accept: application/activity+json'];
|
|
||||||
[$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['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 ($user->id !== null && $user->actorURL !== null) {
|
|
||||||
self::addUserToDB($dbh, $user, $_name);
|
|
||||||
}
|
|
||||||
$cache->saveRemoteFedUserByName($_name, $user);
|
|
||||||
}
|
|
||||||
if ($user === false) {
|
|
||||||
throw new \Federator\Exceptions\ServerError('FedUser::getUserByName User not found');
|
|
||||||
}
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,530 +0,0 @@
|
||||||
<?php
|
|
||||||
/**
|
|
||||||
* SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* @author Yannis Vogel (vogeldevelopment)
|
|
||||||
**/
|
|
||||||
|
|
||||||
namespace Federator\DIO;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* IO functions related to followers
|
|
||||||
*/
|
|
||||||
class Followers
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* get followers of user
|
|
||||||
*
|
|
||||||
* @param \mysqli $dbh
|
|
||||||
* database handle
|
|
||||||
* @param string $id
|
|
||||||
* user id
|
|
||||||
* @param \Federator\Connector\Connector $connector
|
|
||||||
* connector to fetch use with
|
|
||||||
* @param \Federator\Cache\Cache|null $cache
|
|
||||||
* optional caching service
|
|
||||||
* @return \Federator\Data\FedUser[]
|
|
||||||
*/
|
|
||||||
public static function getFollowersByUser($dbh, $id, $connector, $cache)
|
|
||||||
{
|
|
||||||
// ask cache
|
|
||||||
if ($cache !== null) {
|
|
||||||
$followers = $cache->getFollowersByUser($id);
|
|
||||||
if ($followers !== false) {
|
|
||||||
return $followers;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$followers = [];
|
|
||||||
$sql = 'select source_user from follows where target_user = ?';
|
|
||||||
$stmt = $dbh->prepare($sql);
|
|
||||||
if ($stmt === false) {
|
|
||||||
throw new \Federator\Exceptions\ServerError('Followers::getFollowersByUser Failed to prepare statement');
|
|
||||||
}
|
|
||||||
$stmt->bind_param('s', $id);
|
|
||||||
$stmt->execute();
|
|
||||||
$followerIds = [];
|
|
||||||
$stmt->bind_result($sourceUser);
|
|
||||||
while ($stmt->fetch()) {
|
|
||||||
$followerIds[] = $sourceUser;
|
|
||||||
}
|
|
||||||
$stmt->close();
|
|
||||||
foreach ($followerIds as $followerId) {
|
|
||||||
try {
|
|
||||||
$user = \Federator\DIO\FedUser::getUserByName(
|
|
||||||
$dbh,
|
|
||||||
$followerId,
|
|
||||||
$cache,
|
|
||||||
);
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
error_log('Followers::getFollowersByUser Exception: ' . $e->getMessage());
|
|
||||||
continue; // Skip this user if an exception occurs
|
|
||||||
}
|
|
||||||
if ($user !== false && $user->id !== null) {
|
|
||||||
$followers[] = $user;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($followers === []) {
|
|
||||||
// ask connector for user-id
|
|
||||||
$followers = $connector->getFollowersByUser($id);
|
|
||||||
if ($followers === false) {
|
|
||||||
$followers = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// save followers to cache
|
|
||||||
if ($cache !== null) {
|
|
||||||
$cache->saveFollowersByUser($id, $followers);
|
|
||||||
}
|
|
||||||
return $followers;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* get following for user - who does the user follow
|
|
||||||
*
|
|
||||||
* @param \mysqli $dbh
|
|
||||||
* database handle
|
|
||||||
* @param string $id
|
|
||||||
* user id
|
|
||||||
* @param \Federator\Connector\Connector $connector
|
|
||||||
* connector to fetch use with
|
|
||||||
* @param \Federator\Cache\Cache|null $cache
|
|
||||||
* optional caching service
|
|
||||||
* @return \Federator\Data\FedUser[]
|
|
||||||
*/
|
|
||||||
public static function getFollowingByUser($dbh, $id, $connector, $cache)
|
|
||||||
{
|
|
||||||
// ask cache
|
|
||||||
if ($cache !== null) {
|
|
||||||
$following = $cache->getFollowingByUser($id);
|
|
||||||
if ($following !== false) {
|
|
||||||
return $following;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$following = [];
|
|
||||||
$sql = 'select target_user from follows where source_user = ?';
|
|
||||||
$stmt = $dbh->prepare($sql);
|
|
||||||
if ($stmt === false) {
|
|
||||||
throw new \Federator\Exceptions\ServerError('Followers::getFollowingForUser Failed to prepare statement');
|
|
||||||
}
|
|
||||||
$stmt->bind_param('s', $id);
|
|
||||||
$stmt->execute();
|
|
||||||
$followingIds = [];
|
|
||||||
$stmt->bind_result($sourceUser);
|
|
||||||
while ($stmt->fetch()) {
|
|
||||||
$followingIds[] = $sourceUser;
|
|
||||||
}
|
|
||||||
$stmt->close();
|
|
||||||
foreach ($followingIds as $followingId) {
|
|
||||||
try {
|
|
||||||
$user = \Federator\DIO\FedUser::getUserByName(
|
|
||||||
$dbh,
|
|
||||||
$followingId,
|
|
||||||
$cache,
|
|
||||||
);
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
error_log('Followers::getFollowingByUser Exception: ' . $e->getMessage());
|
|
||||||
continue; // Skip this user if an exception occurs
|
|
||||||
}
|
|
||||||
if ($user !== false && $user->id !== null) {
|
|
||||||
$following[] = $user;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($following === []) {
|
|
||||||
// ask connector for user-id
|
|
||||||
$following = $connector->getFollowingByUser($id);
|
|
||||||
if ($following === false) {
|
|
||||||
$following = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// save posts to DB
|
|
||||||
if ($cache !== null) {
|
|
||||||
$cache->saveFollowingByUser($id, $following);
|
|
||||||
}
|
|
||||||
return $following;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* get followers of federated external user (e.g. mastodon)
|
|
||||||
*
|
|
||||||
* @param \mysqli $dbh
|
|
||||||
* database handle
|
|
||||||
* @param \Federator\Connector\Connector $connector
|
|
||||||
* connector to fetch use with
|
|
||||||
* @param \Federator\Cache\Cache|null $cache
|
|
||||||
* optional caching service
|
|
||||||
* @param string $id
|
|
||||||
* user id
|
|
||||||
* @return \Federator\Data\User[]
|
|
||||||
*/
|
|
||||||
|
|
||||||
public static function getFollowersByFedUser($dbh, $connector, $cache, $id)
|
|
||||||
{
|
|
||||||
$followers = [];
|
|
||||||
|
|
||||||
$sql = 'select source_user from follows where target_user = ?';
|
|
||||||
$stmt = $dbh->prepare($sql);
|
|
||||||
if ($stmt === false) {
|
|
||||||
throw new \Federator\Exceptions\ServerError('Followers::getFollowersByFedUser Failed to prepare statement');
|
|
||||||
}
|
|
||||||
$stmt->bind_param("s", $id);
|
|
||||||
$stmt->execute();
|
|
||||||
$followerIds = [];
|
|
||||||
$stmt->bind_result($sourceUser);
|
|
||||||
while ($stmt->fetch()) {
|
|
||||||
$followerIds[] = $sourceUser;
|
|
||||||
}
|
|
||||||
foreach ($followerIds as $followerId) {
|
|
||||||
try {
|
|
||||||
$user = \Federator\DIO\User::getUserByName(
|
|
||||||
$dbh,
|
|
||||||
$followerId,
|
|
||||||
$connector,
|
|
||||||
$cache
|
|
||||||
);
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
error_log('Followers::getFollowersByFedUser Exception: ' . $e->getMessage());
|
|
||||||
continue; // Skip this user if an exception occurs
|
|
||||||
}
|
|
||||||
if ($user !== false && $user->id !== null) {
|
|
||||||
$followers[] = $user;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $followers;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* send follow request
|
|
||||||
*
|
|
||||||
* @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 source user
|
|
||||||
* @param string $_targetUser target user id
|
|
||||||
* @param string $host the host for generating the follow ID
|
|
||||||
* @return string|false the generated follow ID on success, false on failure
|
|
||||||
*/
|
|
||||||
public static function sendFollowRequest($dbh, $connector, $cache, $_user, $_targetUser, $host)
|
|
||||||
{
|
|
||||||
if ($dbh === false) {
|
|
||||||
throw new \Federator\Exceptions\ServerError('Followers::sendFollowRequest Failed to get database handle');
|
|
||||||
}
|
|
||||||
$user = \Federator\DIO\User::getUserByName(
|
|
||||||
$dbh,
|
|
||||||
$_user,
|
|
||||||
$connector,
|
|
||||||
$cache
|
|
||||||
);
|
|
||||||
if ($user === false || $user->id === null) {
|
|
||||||
throw new \Federator\Exceptions\FileNotFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
$fedUser = \Federator\DIO\FedUser::getUserByName(
|
|
||||||
$dbh,
|
|
||||||
$_targetUser,
|
|
||||||
$cache
|
|
||||||
);
|
|
||||||
if ($fedUser === false || $fedUser->actorURL === null) {
|
|
||||||
throw new \Federator\Exceptions\FileNotFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
$sourceUser = $user->id;
|
|
||||||
$idUrl = self::addFollow($dbh, $sourceUser, $fedUser->id, $host);
|
|
||||||
if ($idUrl === false) {
|
|
||||||
return false; // Failed to add follow
|
|
||||||
}
|
|
||||||
$followObj = new \Federator\Data\ActivityPub\Common\Follow();
|
|
||||||
$sourceUserUrl = 'https://' . $host . '/' . $sourceUser;
|
|
||||||
$followObj->setFObject($fedUser->actorURL);
|
|
||||||
$followObj->setAActor($sourceUserUrl);
|
|
||||||
$followObj->setID($idUrl);
|
|
||||||
|
|
||||||
// Send the Follow activity
|
|
||||||
$inboxUrl = $fedUser->inboxURL;
|
|
||||||
|
|
||||||
$json = json_encode($followObj, JSON_UNESCAPED_SLASHES);
|
|
||||||
|
|
||||||
if ($json === false) {
|
|
||||||
self::removeFollow($dbh, $sourceUser, $fedUser->id);
|
|
||||||
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) {
|
|
||||||
self::removeFollow($dbh, $sourceUser, $fedUser->id);
|
|
||||||
throw new \Exception('Failed to parse URL: ' . $inboxUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isset($parsed['host']) || !isset($parsed['path'])) {
|
|
||||||
self::removeFollow($dbh, $sourceUser, $fedUser->id);
|
|
||||||
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, $user->id); // OR from DB
|
|
||||||
if ($privateKey === false) {
|
|
||||||
self::removeFollow($dbh, $sourceUser, $fedUser->id);
|
|
||||||
throw new \Exception('Failed to get private key');
|
|
||||||
}
|
|
||||||
$pkeyId = openssl_pkey_get_private($privateKey);
|
|
||||||
|
|
||||||
if ($pkeyId === false) {
|
|
||||||
self::removeFollow($dbh, $sourceUser, $fedUser->id);
|
|
||||||
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 = 'https://' . $host . '/' . $user->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) {
|
|
||||||
self::removeFollow($dbh, $sourceUser, $fedUser->id);
|
|
||||||
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);
|
|
||||||
|
|
||||||
// Log the response for debugging if needed
|
|
||||||
if ($response === false) {
|
|
||||||
self::removeFollow($dbh, $sourceUser, $fedUser->id);
|
|
||||||
throw new \Exception('Failed to send Follow activity: ' . curl_error($ch));
|
|
||||||
} else {
|
|
||||||
$httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
||||||
if ($httpcode != 200 && $httpcode != 202) {
|
|
||||||
self::removeFollow($dbh, $sourceUser, $fedUser->id);
|
|
||||||
throw new \Exception('Unexpected HTTP code ' . $httpcode . ':' . $response);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $idUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* add follow
|
|
||||||
*
|
|
||||||
* @param \mysqli $dbh database handle
|
|
||||||
* @param string $sourceUser source user id
|
|
||||||
* @param string $targetUserId target user id
|
|
||||||
* @param string $host the host for generating the follow ID
|
|
||||||
* @return string|false the generated follow ID on success, false on failure
|
|
||||||
*/
|
|
||||||
public static function addFollow($dbh, $sourceUser, $targetUserId, $host)
|
|
||||||
{
|
|
||||||
// Check if we already follow this user
|
|
||||||
$sql = 'select id from follows where source_user = ? and target_user = ?';
|
|
||||||
$stmt = $dbh->prepare($sql);
|
|
||||||
if ($stmt === false) {
|
|
||||||
throw new \Federator\Exceptions\ServerError('Followers::addFollow Failed to prepare statement');
|
|
||||||
}
|
|
||||||
$stmt->bind_param('ss', $sourceUser, $targetUserId);
|
|
||||||
$foundId = 0;
|
|
||||||
$ret = $stmt->bind_result($foundId);
|
|
||||||
$stmt->execute();
|
|
||||||
if ($ret) {
|
|
||||||
$stmt->fetch();
|
|
||||||
}
|
|
||||||
$stmt->close();
|
|
||||||
if ($foundId != 0) {
|
|
||||||
return false; // Already following this user
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate a unique ID for the follow relationship
|
|
||||||
do {
|
|
||||||
$id = bin2hex(openssl_random_pseudo_bytes(16));
|
|
||||||
$idurl = 'https://' . $host . '/' . $sourceUser . '/' . $id;
|
|
||||||
|
|
||||||
// Check if the generated ID is unique
|
|
||||||
$sql = 'select id from follows where id = ?';
|
|
||||||
$stmt = $dbh->prepare($sql);
|
|
||||||
if ($stmt === false) {
|
|
||||||
throw new \Federator\Exceptions\ServerError('Followers::addFollow Failed to prepare id-check'
|
|
||||||
. 'statement');
|
|
||||||
}
|
|
||||||
$stmt->bind_param('s', $idurl);
|
|
||||||
$foundId = 0;
|
|
||||||
$ret = $stmt->bind_result($foundId);
|
|
||||||
$stmt->execute();
|
|
||||||
if ($ret) {
|
|
||||||
$stmt->fetch();
|
|
||||||
}
|
|
||||||
$stmt->close();
|
|
||||||
} while ($foundId > 0);
|
|
||||||
|
|
||||||
// Add follow with created_at timestamp
|
|
||||||
$sql = 'insert into follows (id, source_user, target_user, created_at) values (?, ?, ?, NOW())';
|
|
||||||
$stmt = $dbh->prepare($sql);
|
|
||||||
if ($stmt === false) {
|
|
||||||
throw new \Federator\Exceptions\ServerError('Followers::addFollow Failed to prepare insert statement');
|
|
||||||
}
|
|
||||||
$stmt->bind_param('sss', $idurl, $sourceUser, $targetUserId);
|
|
||||||
$stmt->execute();
|
|
||||||
$stmt->close();
|
|
||||||
return $idurl; // Return the generated follow ID
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* add follow
|
|
||||||
*
|
|
||||||
* @param \mysqli $dbh database handle
|
|
||||||
* @param string $followId the follow ID to use (should be an external url)
|
|
||||||
* @param string $sourceUserId source user id
|
|
||||||
* @param string $targetUserId target user id
|
|
||||||
* @return boolean true on success, false on failure
|
|
||||||
*/
|
|
||||||
public static function addExternalFollow($dbh, $followId, $sourceUserId, $targetUserId)
|
|
||||||
{
|
|
||||||
// Check if we already follow this user
|
|
||||||
$sql = 'select id from follows where source_user = ? and target_user = ?';
|
|
||||||
$stmt = $dbh->prepare($sql);
|
|
||||||
if ($stmt === false) {
|
|
||||||
throw new \Federator\Exceptions\ServerError('Followers::addExternalFollow Failed to prepare statement');
|
|
||||||
}
|
|
||||||
$stmt->bind_param('ss', $sourceUserId, $targetUserId);
|
|
||||||
$foundId = 0;
|
|
||||||
$ret = $stmt->bind_result($foundId);
|
|
||||||
$stmt->execute();
|
|
||||||
if ($ret) {
|
|
||||||
$stmt->fetch();
|
|
||||||
}
|
|
||||||
$stmt->close();
|
|
||||||
if ($foundId != 0) {
|
|
||||||
return false; // Already following this user
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add follow with created_at timestamp
|
|
||||||
$sql = 'insert into follows (id, source_user, target_user, created_at) values (?, ?, ?, NOW())';
|
|
||||||
$stmt = $dbh->prepare($sql);
|
|
||||||
if ($stmt === false) {
|
|
||||||
throw new \Federator\Exceptions\ServerError('Followers::addExternalFollow Failed to prepare insert '
|
|
||||||
. 'statement');
|
|
||||||
}
|
|
||||||
$stmt->bind_param('sss', $followId, $sourceUserId, $targetUserId);
|
|
||||||
$stmt->execute();
|
|
||||||
$stmt->close();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* generate new follow id
|
|
||||||
*
|
|
||||||
* @param \mysqli $dbh database handle
|
|
||||||
* @param string $hostUrl the host URL (e.g. federator URL)
|
|
||||||
* @return string the new follow id
|
|
||||||
*/
|
|
||||||
public static function generateNewFollowId($dbh, $hostUrl)
|
|
||||||
{
|
|
||||||
// Generate a new unique follow ID
|
|
||||||
do {
|
|
||||||
$newId = bin2hex(openssl_random_pseudo_bytes(16));
|
|
||||||
$newIdUrl = $hostUrl . '/' . $newId;
|
|
||||||
|
|
||||||
// Check if the generated ID is unique
|
|
||||||
$sql = 'select id from follows where id = ?';
|
|
||||||
$stmt = $dbh->prepare($sql);
|
|
||||||
if ($stmt === false) {
|
|
||||||
throw new \Federator\Exceptions\ServerError('Followers::generateNewFollowId Failed to prepare id-check'
|
|
||||||
. ' statement');
|
|
||||||
}
|
|
||||||
$stmt->bind_param('s', $newIdUrl);
|
|
||||||
$foundId = 0;
|
|
||||||
$ret = $stmt->bind_result($foundId);
|
|
||||||
$stmt->execute();
|
|
||||||
if ($ret) {
|
|
||||||
$stmt->fetch();
|
|
||||||
}
|
|
||||||
$stmt->close();
|
|
||||||
} while ($foundId > 0);
|
|
||||||
|
|
||||||
return $newIdUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* remove follow
|
|
||||||
*
|
|
||||||
* @param \mysqli $dbh database handle
|
|
||||||
* @param string $sourceUser source user id
|
|
||||||
* @param string $targetUserId target user id
|
|
||||||
* @return string|false removed followId on success, false on failure
|
|
||||||
*/
|
|
||||||
public static function removeFollow($dbh, $sourceUser, $targetUserId)
|
|
||||||
{
|
|
||||||
// Combine retrieval and removal in one query using MySQL's RETURNING (if supported)
|
|
||||||
$sql = 'delete from follows where source_user = ? and target_user = ? RETURNING id';
|
|
||||||
$stmt = $dbh->prepare($sql);
|
|
||||||
if ($stmt !== false) {
|
|
||||||
$stmt->bind_param('ss', $sourceUser, $targetUserId);
|
|
||||||
if ($stmt->execute()) {
|
|
||||||
$stmt->bind_result($followId);
|
|
||||||
if ($stmt->fetch() === true) {
|
|
||||||
$stmt->close();
|
|
||||||
if (!empty($followId)) {
|
|
||||||
return $followId;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$stmt->close();
|
|
||||||
} else {
|
|
||||||
// Fallback for MySQL versions that do not support RETURNING
|
|
||||||
// First, fetch the id of the follow to be removed
|
|
||||||
$sql = 'select id from follows where source_user = ? and target_user = ?';
|
|
||||||
$stmt = $dbh->prepare($sql);
|
|
||||||
if ($stmt === false) {
|
|
||||||
throw new \Federator\Exceptions\ServerError('Followers::removeFollow Failed to prepare select '
|
|
||||||
. 'statement');
|
|
||||||
}
|
|
||||||
$stmt->bind_param('ss', $sourceUser, $targetUserId);
|
|
||||||
$stmt->execute();
|
|
||||||
$stmt->bind_result($followId);
|
|
||||||
$found = $stmt->fetch();
|
|
||||||
$stmt->close();
|
|
||||||
|
|
||||||
if ($found === false || empty($followId)) {
|
|
||||||
return false; // No such follow found
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now, delete the row
|
|
||||||
$sql = 'delete from follows where source_user = ? and target_user = ?';
|
|
||||||
$stmt = $dbh->prepare($sql);
|
|
||||||
if ($stmt === false) {
|
|
||||||
throw new \Federator\Exceptions\ServerError('Followers::removeFollow Failed to prepare delete '
|
|
||||||
. 'statement');
|
|
||||||
}
|
|
||||||
$stmt->bind_param('ss', $sourceUser, $targetUserId);
|
|
||||||
$stmt->execute();
|
|
||||||
$affectedRows = $stmt->affected_rows;
|
|
||||||
$stmt->close();
|
|
||||||
|
|
||||||
return $affectedRows > 0 ? $followId : false;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -13,331 +13,47 @@ namespace Federator\DIO;
|
||||||
*/
|
*/
|
||||||
class Posts
|
class Posts
|
||||||
{
|
{
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* get posts by user
|
* get posts by user
|
||||||
*
|
*
|
||||||
* @param \mysqli $dbh @unused-param
|
* @param \mysqli $dbh @unused-param
|
||||||
* database handle
|
* database handle
|
||||||
* @param string $userid
|
* @param string $id
|
||||||
* user id
|
* user id
|
||||||
* @param \Federator\Connector\Connector $connector
|
* @param \Federator\Connector\Connector $connector
|
||||||
* connector to fetch use with
|
* connector to fetch use with
|
||||||
* @param \Federator\Cache\Cache|null $cache
|
* @param \Federator\Cache\Cache|null $cache
|
||||||
* optional caching service
|
* optional caching service
|
||||||
* @param int $min
|
* @param string $minId
|
||||||
* minimum timestamp
|
* minimum ID
|
||||||
* @param int $max
|
* @param string $maxId
|
||||||
* maximum timestamp
|
* maximum ID
|
||||||
* @param int $limit
|
* @return \Federator\Data\ActivityPub\Common\APObject[]
|
||||||
* maximum number of results
|
|
||||||
* @return \Federator\Data\ActivityPub\Common\Activity[]
|
|
||||||
*/
|
*/
|
||||||
public static function getPostsByUser($dbh, $userid, $connector, $cache, $min, $max, $limit)
|
public static function getPostsByUser($dbh, $id, $connector, $cache, $minId, $maxId)
|
||||||
{
|
{
|
||||||
// ask cache
|
// ask cache
|
||||||
if ($cache !== null) {
|
if ($cache !== null) {
|
||||||
$posts = $cache->getRemotePostsByUser($userid, $min, $max, $limit);
|
$posts = $cache->getRemotePostsByUser($id, $minId, $maxId);
|
||||||
if ($posts !== false) {
|
if ($posts !== false) {
|
||||||
return $posts;
|
return $posts;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$posts = self::getPostsFromDb($dbh, $userid, $min, $max, $limit);
|
$posts = [];
|
||||||
|
// TODO: check our db
|
||||||
|
|
||||||
|
if ($posts === []) {
|
||||||
|
// ask connector for user-id
|
||||||
|
$posts = $connector->getRemotePostsByUser($id, $minId, $maxId);
|
||||||
if ($posts === false) {
|
if ($posts === false) {
|
||||||
$posts = [];
|
$posts = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only override $min if we found posts in our DB
|
|
||||||
$remoteMin = $min;
|
|
||||||
if (!empty($posts)) {
|
|
||||||
// Find the latest published date in the DB posts
|
|
||||||
$latestPublished = null;
|
|
||||||
foreach ($posts as $post) {
|
|
||||||
$published = $post->getPublished();
|
|
||||||
if ($published != null) {
|
|
||||||
if ($latestPublished === null || $published > $latestPublished) {
|
|
||||||
$latestPublished = $published;
|
|
||||||
}
|
}
|
||||||
}
|
// save posts to DB
|
||||||
}
|
|
||||||
if ($latestPublished !== null) {
|
|
||||||
$remoteMin = $latestPublished;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch newer posts from connector (if any) if max is not set and limit not reached
|
|
||||||
if ($max == 0 && sizeof($posts) < $limit) {
|
|
||||||
$newPosts = $connector->getRemotePostsByUser($userid, $remoteMin, $max, $limit);
|
|
||||||
if ($newPosts !== false && is_array($newPosts)) {
|
|
||||||
// Merge new posts with DB posts, avoiding duplicates by ID
|
|
||||||
$existingIds = [];
|
|
||||||
foreach ($posts as $post) {
|
|
||||||
$existingIds[$post->getID()] = true;
|
|
||||||
}
|
|
||||||
foreach ($newPosts as $newPost) {
|
|
||||||
if (!isset($existingIds[$newPost->getID()])) {
|
|
||||||
if ($newPost->getID() !== '') {
|
|
||||||
self::savePost($dbh, $userid, $newPost);
|
|
||||||
}
|
|
||||||
if (sizeof($posts) < $limit) {
|
|
||||||
$posts[] = $newPost;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$originUrl = 'localhost';
|
|
||||||
if (isset($_SERVER['HTTP_HOST'])) {
|
|
||||||
$originUrl = $_SERVER['HTTP_HOST']; // origin of our request - e.g. mastodon
|
|
||||||
} elseif (isset($_SERVER['HTTP_ORIGIN'])) {
|
|
||||||
$origin = $_SERVER['HTTP_ORIGIN'];
|
|
||||||
$parsed = parse_url($origin);
|
|
||||||
if (isset($parsed) && isset($parsed['host'])) {
|
|
||||||
$parsedHost = $parsed['host'];
|
|
||||||
if (is_string($parsedHost) && $parsedHost !== '') {
|
|
||||||
$originUrl = $parsedHost;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!isset($originUrl) || $originUrl === '') {
|
|
||||||
$originUrl = 'localhost'; // Fallback to localhost if no origin is set
|
|
||||||
}
|
|
||||||
|
|
||||||
// optionally convert from article to note
|
|
||||||
foreach ($posts as $post) {
|
|
||||||
switch (strtolower($post->getType())) {
|
|
||||||
case 'undo':
|
|
||||||
$object = $post->getObject();
|
|
||||||
if (is_object($object)) {
|
|
||||||
if (strtolower($object->getType()) === 'article') {
|
|
||||||
if ($object instanceof \Federator\Data\ActivityPub\Common\Article) {
|
|
||||||
$object = \Federator\DIO\Article::conditionalConvertToNote($object, $originUrl);
|
|
||||||
$post->setObject($object);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'create':
|
|
||||||
case 'update':
|
|
||||||
$object = $post->getObject();
|
|
||||||
if (is_object($object)) {
|
|
||||||
if (strtolower($object->getType()) === 'article') {
|
|
||||||
if ($object instanceof \Federator\Data\ActivityPub\Common\Article) {
|
|
||||||
$object = \Federator\DIO\Article::conditionalConvertToNote($object, $originUrl);
|
|
||||||
$post->setObject($object);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($cache !== null) {
|
if ($cache !== null) {
|
||||||
$cache->saveRemotePostsByUser($userid, $min, $max, $limit, $posts);
|
$cache->saveRemotePostsByUser($id, $posts);
|
||||||
}
|
}
|
||||||
return $posts;
|
return $posts;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get posts for a user from the DB (optionally by date)
|
|
||||||
*
|
|
||||||
* @param \mysqli $dbh
|
|
||||||
* @param string $userId
|
|
||||||
* @param int $min min timestamp
|
|
||||||
* @param int $max max timestamp
|
|
||||||
* @param int $limit
|
|
||||||
* @return \Federator\Data\ActivityPub\Common\Activity[]|false
|
|
||||||
*/
|
|
||||||
public static function getPostsFromDb($dbh, $userId, $min, $max, $limit = 20)
|
|
||||||
{
|
|
||||||
$sql = 'SELECT `id`, `url`, `user_id`, `actor`, `type`, `object`, `to`, `cc`, unix_timestamp(`published`) as '
|
|
||||||
. 'published FROM posts WHERE user_id = ?';
|
|
||||||
$params = [$userId];
|
|
||||||
$types = 's';
|
|
||||||
if ($min > 0) {
|
|
||||||
$sql .= ' AND published >= from_unixtime(?)';
|
|
||||||
$params[] = $min;
|
|
||||||
$types .= 's';
|
|
||||||
}
|
|
||||||
if ($max > 0) {
|
|
||||||
$sql .= ' AND published <= from_unixtime(?)';
|
|
||||||
$params[] = $max;
|
|
||||||
$types .= 's';
|
|
||||||
}
|
|
||||||
$sql .= ' ORDER BY published DESC LIMIT ' . $limit;
|
|
||||||
|
|
||||||
$stmt = $dbh->prepare($sql);
|
|
||||||
if ($stmt === false) {
|
|
||||||
throw new \Federator\Exceptions\ServerError();
|
|
||||||
}
|
|
||||||
$stmt->bind_param($types, ...$params);
|
|
||||||
$stmt->execute();
|
|
||||||
$result = $stmt->get_result();
|
|
||||||
if (!($result instanceof \mysqli_result)) {
|
|
||||||
$stmt->close();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
$posts = [];
|
|
||||||
while ($row = $result->fetch_assoc()) {
|
|
||||||
if (isset($row['to']) && $row['to'] !== null) {
|
|
||||||
$row['to'] = json_decode($row['to'], true);
|
|
||||||
}
|
|
||||||
if (isset($row['cc']) && $row['cc'] !== null) {
|
|
||||||
$row['cc'] = json_decode($row['cc'], true);
|
|
||||||
}
|
|
||||||
if (isset($row['object']) && $row['object'] !== null) {
|
|
||||||
$decoded = json_decode($row['object'], true);
|
|
||||||
// Only use decoded value if it's an array/object
|
|
||||||
if (is_array($decoded)) {
|
|
||||||
$row['object'] = $decoded;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (isset($row['published']) && $row['published'] !== null) {
|
|
||||||
// If it's numeric, keep as int. If it's a string, try to parse as ISO 8601.
|
|
||||||
if (!is_numeric($row['published'])) {
|
|
||||||
// Try to parse as datetime string
|
|
||||||
$timestamp = strtotime($row['published']);
|
|
||||||
$row['published'] = $timestamp !== false ? $timestamp : null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$activity = \Federator\Data\ActivityPub\Factory::newActivityFromJson($row);
|
|
||||||
if ($activity !== false) {
|
|
||||||
$posts[] = $activity;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$stmt->close();
|
|
||||||
return $posts;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Save a post (insert or update)
|
|
||||||
*
|
|
||||||
* @param \mysqli $dbh
|
|
||||||
* @param string $userId
|
|
||||||
* @param \Federator\Data\ActivityPub\Common\Activity $post
|
|
||||||
* @param string|null $articleId the original id of the article
|
|
||||||
* (used to identify the source article in the remote system)
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public static function savePost($dbh, $userId, $post, $articleId = null)
|
|
||||||
{
|
|
||||||
$sql = 'INSERT INTO posts (
|
|
||||||
`id`, `url`, `user_id`, `actor`, `type`, `object`, `to`, `cc`, `published`, `article_id`
|
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
||||||
ON DUPLICATE KEY UPDATE
|
|
||||||
`url` = VALUES(`url`),
|
|
||||||
`user_id` = VALUES(`user_id`),
|
|
||||||
`actor` = VALUES(`actor`),
|
|
||||||
`type` = VALUES(`type`),
|
|
||||||
`object` = VALUES(`object`),
|
|
||||||
`to` = VALUES(`to`),
|
|
||||||
`cc` = VALUES(`cc`),
|
|
||||||
`published` = VALUES(`published`),
|
|
||||||
`article_id` = VALUES(`article_id`)';
|
|
||||||
$stmt = $dbh->prepare($sql);
|
|
||||||
if ($stmt === false) {
|
|
||||||
throw new \Federator\Exceptions\ServerError();
|
|
||||||
}
|
|
||||||
|
|
||||||
$id = $post->getID();
|
|
||||||
$url = $post->getUrl();
|
|
||||||
$actor = $post->getAActor();
|
|
||||||
$type = $post->getType();
|
|
||||||
$object = $post->getObject();
|
|
||||||
$objectJson = ($object instanceof \Federator\Data\ActivityPub\Common\APObject)
|
|
||||||
? json_encode($object)
|
|
||||||
: $object;
|
|
||||||
if ($objectJson === false) {
|
|
||||||
$objectJson = null;
|
|
||||||
}
|
|
||||||
if (is_object($object)) {
|
|
||||||
$id = $object->getID();
|
|
||||||
}
|
|
||||||
$to = $post->getTo();
|
|
||||||
$cc = $post->getCC();
|
|
||||||
$toJson = is_array($to) ? json_encode($to) : (is_string($to) ? json_encode([$to]) : null);
|
|
||||||
$ccJson = is_array($cc) ? json_encode($cc) : (is_string($cc) ? json_encode([$cc]) : null);
|
|
||||||
$published = $post->getPublished();
|
|
||||||
$publishedStr = $published ? gmdate('Y-m-d H:i:s', $published) : gmdate('Y-m-d H:i:s');
|
|
||||||
|
|
||||||
$stmt->bind_param(
|
|
||||||
'ssssssssss',
|
|
||||||
$id,
|
|
||||||
$url,
|
|
||||||
$userId,
|
|
||||||
$actor,
|
|
||||||
$type,
|
|
||||||
$objectJson,
|
|
||||||
$toJson,
|
|
||||||
$ccJson,
|
|
||||||
$publishedStr,
|
|
||||||
$articleId,
|
|
||||||
);
|
|
||||||
$result = $stmt->execute();
|
|
||||||
$stmt->close();
|
|
||||||
return $result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete a post
|
|
||||||
*
|
|
||||||
* @param \mysqli $dbh
|
|
||||||
* @param string $id The post ID
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public static function deletePost($dbh, $id)
|
|
||||||
{
|
|
||||||
$sql = 'delete from posts where id = ?';
|
|
||||||
$stmt = $dbh->prepare($sql);
|
|
||||||
if ($stmt === false) {
|
|
||||||
throw new \Federator\Exceptions\ServerError();
|
|
||||||
}
|
|
||||||
$stmt->bind_param('s', $id);
|
|
||||||
$stmt->execute();
|
|
||||||
$affectedRows = $stmt->affected_rows;
|
|
||||||
$stmt->close();
|
|
||||||
return $affectedRows > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** retrieve original article id of post
|
|
||||||
*
|
|
||||||
* @param \mysqli $dbh
|
|
||||||
* @param \Federator\Data\ActivityPub\Common\Activity $post
|
|
||||||
* @return string|null
|
|
||||||
*/
|
|
||||||
public static function getOriginalArticleId($dbh, $post)
|
|
||||||
{
|
|
||||||
$sql = 'SELECT `article_id` FROM posts WHERE id = ?';
|
|
||||||
$stmt = $dbh->prepare($sql);
|
|
||||||
if ($stmt === false) {
|
|
||||||
throw new \Federator\Exceptions\ServerError();
|
|
||||||
}
|
|
||||||
$id = $post->getID();
|
|
||||||
$object = $post->getObject();
|
|
||||||
if (is_object($object)) {
|
|
||||||
$inReplyTo = $object->getInReplyTo();
|
|
||||||
if ($inReplyTo !== '') {
|
|
||||||
$id = $inReplyTo; // Use inReplyTo as ID if it's a string
|
|
||||||
} else {
|
|
||||||
$id = $object->getObject();
|
|
||||||
}
|
|
||||||
} elseif (is_string($object)) {
|
|
||||||
$id = $object; // If object is a string, use it directly
|
|
||||||
}
|
|
||||||
$stmt->bind_param('s', $id);
|
|
||||||
$articleId = null;
|
|
||||||
$ret = $stmt->bind_result($articleId);
|
|
||||||
$stmt->execute();
|
|
||||||
if ($ret) {
|
|
||||||
$stmt->fetch();
|
|
||||||
}
|
|
||||||
$stmt->close();
|
|
||||||
return $articleId;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
$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,7 +42,6 @@ class User
|
||||||
throw new \Federator\Exceptions\ServerError();
|
throw new \Federator\Exceptions\ServerError();
|
||||||
}
|
}
|
||||||
$public = openssl_pkey_get_details($private_key)['key'];
|
$public = openssl_pkey_get_details($private_key)['key'];
|
||||||
$user->publicKey = $public;
|
|
||||||
$private = '';
|
$private = '';
|
||||||
openssl_pkey_export($private_key, $private);
|
openssl_pkey_export($private_key, $private);
|
||||||
$sql = 'insert into users (id, externalid, rsapublic, rsaprivate, validuntil,';
|
$sql = 'insert into users (id, externalid, rsapublic, rsaprivate, validuntil,';
|
||||||
|
@ -50,11 +49,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();
|
||||||
}
|
}
|
||||||
$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 +73,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();
|
||||||
}
|
}
|
||||||
$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,
|
||||||
|
@ -101,30 +100,6 @@ class User
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* get private rsa key
|
|
||||||
* @return string|false key or false
|
|
||||||
*/
|
|
||||||
public static function getrsaprivate(\mysqli $dbh, string $_user)
|
|
||||||
{
|
|
||||||
$sql = 'select rsaprivate from users where id=?';
|
|
||||||
$stmt = $dbh->prepare($sql);
|
|
||||||
if ($stmt === false) {
|
|
||||||
throw new \Federator\Exceptions\ServerError('User::getrsaprivate Failed to prepare statement');
|
|
||||||
}
|
|
||||||
$stmt->bind_param('s', $_user);
|
|
||||||
$ret = $stmt->bind_result($rsaPrivateKey);
|
|
||||||
$stmt->execute();
|
|
||||||
if ($ret) {
|
|
||||||
$stmt->fetch();
|
|
||||||
}
|
|
||||||
$stmt->close();
|
|
||||||
if ($rsaPrivateKey !== null) {
|
|
||||||
return $rsaPrivateKey;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* extend the given user with internal data
|
* extend the given user with internal data
|
||||||
* @param \mysqli $dbh database handle
|
* @param \mysqli $dbh database handle
|
||||||
|
@ -136,7 +111,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();
|
||||||
}
|
}
|
||||||
$stmt->bind_param("s", $_user);
|
$stmt->bind_param("s", $_user);
|
||||||
$validuntil = 0;
|
$validuntil = 0;
|
||||||
|
@ -182,9 +157,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();
|
||||||
}
|
}
|
||||||
$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,
|
||||||
|
@ -209,10 +184,12 @@ class User
|
||||||
$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;
|
||||||
|
|
|
@ -1,97 +0,0 @@
|
||||||
<?php
|
|
||||||
/**
|
|
||||||
* SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* @author Yannis Vogel (vogeldevelopment)
|
|
||||||
**/
|
|
||||||
|
|
||||||
namespace Federator\DIO;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* IO functions related to votes
|
|
||||||
*/
|
|
||||||
class Votes
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Add a vote (like/dislike)
|
|
||||||
*
|
|
||||||
* @param \mysqli $dbh
|
|
||||||
* @param string $userId The user who votes
|
|
||||||
* @param string $targetId The object being voted on (e.g., post id)
|
|
||||||
* @param string $type 'like' or 'dislike'
|
|
||||||
* @return string|false The generated vote ID on success, false on failure
|
|
||||||
*/
|
|
||||||
public static function addVote($dbh, $userId, $targetId, $type)
|
|
||||||
{
|
|
||||||
// Check if already voted
|
|
||||||
$sql = 'SELECT id FROM votes WHERE user_id = ? AND target_id = ? AND type = ?';
|
|
||||||
$stmt = $dbh->prepare($sql);
|
|
||||||
if ($stmt === false) {
|
|
||||||
throw new \Federator\Exceptions\ServerError();
|
|
||||||
}
|
|
||||||
$stmt->bind_param('sss', $userId, $targetId, $type);
|
|
||||||
$foundId = 0;
|
|
||||||
$ret = $stmt->bind_result($foundId);
|
|
||||||
$stmt->execute();
|
|
||||||
if ($ret) {
|
|
||||||
$stmt->fetch();
|
|
||||||
}
|
|
||||||
$stmt->close();
|
|
||||||
if ($foundId != 0) {
|
|
||||||
return false; // Already voted
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate a unique ID for the vote
|
|
||||||
do {
|
|
||||||
$id = bin2hex(openssl_random_pseudo_bytes(16));
|
|
||||||
// Check if the generated ID is unique
|
|
||||||
$sql = 'SELECT id FROM votes WHERE id = ?';
|
|
||||||
$stmt = $dbh->prepare($sql);
|
|
||||||
if ($stmt === false) {
|
|
||||||
throw new \Federator\Exceptions\ServerError();
|
|
||||||
}
|
|
||||||
$stmt->bind_param('s', $id);
|
|
||||||
$foundId = 0;
|
|
||||||
$ret = $stmt->bind_result($foundId);
|
|
||||||
$stmt->execute();
|
|
||||||
if ($ret) {
|
|
||||||
$stmt->fetch();
|
|
||||||
}
|
|
||||||
$stmt->close();
|
|
||||||
} while ($foundId > 0);
|
|
||||||
|
|
||||||
// Add vote with created_at timestamp
|
|
||||||
$sql = 'INSERT INTO votes (id, user_id, target_id, type, created_at) VALUES (?, ?, ?, ?, NOW())';
|
|
||||||
$stmt = $dbh->prepare($sql);
|
|
||||||
if ($stmt === false) {
|
|
||||||
throw new \Federator\Exceptions\ServerError();
|
|
||||||
}
|
|
||||||
$stmt->bind_param('ssss', $id, $userId, $targetId, $type);
|
|
||||||
$stmt->execute();
|
|
||||||
$stmt->close();
|
|
||||||
return $id; // Return the generated vote ID
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove a vote (like/dislike)
|
|
||||||
*
|
|
||||||
* @param \mysqli $dbh
|
|
||||||
* @param string $userId
|
|
||||||
* @param string $targetId
|
|
||||||
* @return bool true on success
|
|
||||||
*/
|
|
||||||
public static function removeVote($dbh, $userId, $targetId)
|
|
||||||
{
|
|
||||||
$sql = 'DELETE FROM votes WHERE user_id = ? AND target_id = ?';
|
|
||||||
$stmt = $dbh->prepare($sql);
|
|
||||||
if ($stmt === false) {
|
|
||||||
throw new \Federator\Exceptions\ServerError();
|
|
||||||
}
|
|
||||||
$stmt->bind_param('ss', $userId, $targetId);
|
|
||||||
$stmt->execute();
|
|
||||||
$affectedRows = $stmt->affected_rows;
|
|
||||||
$stmt->close();
|
|
||||||
return $affectedRows > 0;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,78 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace Federator\Jobs;
|
|
||||||
|
|
||||||
class InboxJob extends \Federator\Api
|
|
||||||
{
|
|
||||||
/** @var array<string, mixed> $args Arguments for the job */
|
|
||||||
public $args = [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* cache instance
|
|
||||||
*
|
|
||||||
* @var \Federator\Cache\Cache $cache
|
|
||||||
*/
|
|
||||||
protected $cache;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* remote connector
|
|
||||||
*
|
|
||||||
* @var \Federator\Connector\Connector $connector
|
|
||||||
*/
|
|
||||||
protected $connector = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* database instance
|
|
||||||
*
|
|
||||||
* @var \Mysqli $dbh
|
|
||||||
*/
|
|
||||||
protected $dbh;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* constructor
|
|
||||||
*/
|
|
||||||
public function __construct()
|
|
||||||
{
|
|
||||||
parent::__construct();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set up environment for this job
|
|
||||||
*/
|
|
||||||
public function setUp(): void
|
|
||||||
{
|
|
||||||
$this->openDatabase();
|
|
||||||
$this->loadPlugins();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Perform the inbox job.
|
|
||||||
*
|
|
||||||
* @return bool true on success, false on failure
|
|
||||||
*/
|
|
||||||
public function perform(): bool
|
|
||||||
{
|
|
||||||
error_log('InboxJob: Starting job');
|
|
||||||
$user = $this->args['user'];
|
|
||||||
$recipientId = $this->args['recipientId'];
|
|
||||||
$activity = $this->args['activity'];
|
|
||||||
|
|
||||||
$inboxActivity = \Federator\Data\ActivityPub\Factory::newActivityFromJson($activity);
|
|
||||||
|
|
||||||
if ($inboxActivity === false) {
|
|
||||||
error_log('InboxJob: Failed to create inboxActivity from JSON');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
\Federator\DIO\FedUser::inboxForUser(
|
|
||||||
$this,
|
|
||||||
$this->dbh,
|
|
||||||
$this->connector,
|
|
||||||
$this->cache,
|
|
||||||
$user,
|
|
||||||
$recipientId,
|
|
||||||
$inboxActivity
|
|
||||||
);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,83 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace Federator\Jobs;
|
|
||||||
|
|
||||||
class NewContentJob extends \Federator\Api
|
|
||||||
{
|
|
||||||
/** @var array<string, mixed> $args Arguments for the job */
|
|
||||||
public $args = [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* cache instance
|
|
||||||
*
|
|
||||||
* @var \Federator\Cache\Cache $cache
|
|
||||||
*/
|
|
||||||
protected $cache;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* remote connector
|
|
||||||
*
|
|
||||||
* @var \Federator\Connector\Connector $connector
|
|
||||||
*/
|
|
||||||
protected $connector = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* database instance
|
|
||||||
*
|
|
||||||
* @var \Mysqli $dbh
|
|
||||||
*/
|
|
||||||
protected $dbh;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* constructor
|
|
||||||
*/
|
|
||||||
public function __construct()
|
|
||||||
{
|
|
||||||
parent::__construct();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set up environment for this job
|
|
||||||
*/
|
|
||||||
public function setUp(): void
|
|
||||||
{
|
|
||||||
$this->openDatabase();
|
|
||||||
$this->loadPlugins();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Perform the inbox job.
|
|
||||||
*
|
|
||||||
* @return bool true on success, false on failure
|
|
||||||
*/
|
|
||||||
public function perform(): bool
|
|
||||||
{
|
|
||||||
error_log('NewContentJob: Starting job');
|
|
||||||
$user = $this->args['user'];
|
|
||||||
$recipientId = $this->args['recipientId'];
|
|
||||||
$activity = $this->args['activity'];
|
|
||||||
$articleId = $this->args['articleId'] ?? null;
|
|
||||||
|
|
||||||
$activity = \Federator\Data\ActivityPub\Factory::newActivityFromJson($activity);
|
|
||||||
|
|
||||||
if ($activity === false) {
|
|
||||||
error_log('NewContentJob: Failed to create activity from JSON');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
$domain = $this->config['generic']['externaldomain'];
|
|
||||||
|
|
||||||
$ourUrl = 'https://' . $domain;
|
|
||||||
|
|
||||||
\Federator\Api\V1\NewContent::postForUser(
|
|
||||||
$this->dbh,
|
|
||||||
$this->connector,
|
|
||||||
$this->cache,
|
|
||||||
$ourUrl,
|
|
||||||
$user,
|
|
||||||
$recipientId,
|
|
||||||
$activity,
|
|
||||||
$articleId
|
|
||||||
);
|
|
||||||
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
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -96,8 +96,8 @@ class Language
|
||||||
if ($root === '') {
|
if ($root === '') {
|
||||||
$root = '.';
|
$root = '.';
|
||||||
}
|
}
|
||||||
if (@file_exists($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');
|
require($root . '../lang/federator/' . $this->uselang . "/$group.inc");
|
||||||
$this->lang[$group] = $l;
|
$this->lang[$group] = $l;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -107,15 +107,15 @@ class Language
|
||||||
if (isset($values[$i])) {
|
if (isset($values[$i])) {
|
||||||
$string = str_replace("\$$i", $values[$i], $string);
|
$string = str_replace("\$$i", $values[$i], $string);
|
||||||
} else {
|
} else {
|
||||||
$string = str_replace("\$$i", '', $string);
|
$string = str_replace("\$$i", "", $string);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return $string;
|
return $string;
|
||||||
}
|
}
|
||||||
$basedir = $_SERVER['DOCUMENT_ROOT'] . '/../';
|
$basedir = $_SERVER['DOCUMENT_ROOT'] . '/../';
|
||||||
$fh = @fopen($basedir . '/logs/missingtrans.txt', 'a');
|
$fh = @fopen("$basedir/logs/missingtrans.txt", 'a');
|
||||||
if ($fh !== false) {
|
if ($fh !== false) {
|
||||||
fwrite($fh, $this->uselang . ':' . $group . ':' . "$key\n");
|
fwrite($fh, $this->uselang.":$group:$key\n");
|
||||||
fclose($fh);
|
fclose($fh);
|
||||||
}
|
}
|
||||||
return ">>$group:$key<<";
|
return ">>$group:$key<<";
|
||||||
|
@ -132,7 +132,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($_SERVER['DOCUMENT_ROOT'] . '/../lang/' . $this->uselang . "/$group.inc");
|
||||||
$this->lang[$group] = $l;
|
$this->lang[$group] = $l;
|
||||||
}
|
}
|
||||||
// @phan-suppress-next-line PhanPartialTypeMismatchReturn
|
// @phan-suppress-next-line PhanPartialTypeMismatchReturn
|
||||||
|
@ -288,7 +288,7 @@ function smarty_function_printlang($params, $template) : string
|
||||||
*/
|
*/
|
||||||
function smarty_function_printjslang($params, $template) : string
|
function smarty_function_printjslang($params, $template) : string
|
||||||
{
|
{
|
||||||
$lang = $template->getTemplateVars('language');
|
$lang = $template->getTemplateVars("language");
|
||||||
$prefix = 'window.translations.' . $params['group'] . '.' . $params['key'] . ' = \'';
|
$prefix = 'window.translations.' . $params['group'] . '.' . $params['key'] . ' = \'';
|
||||||
$postfix = '\';';
|
$postfix = '\';';
|
||||||
if (isset($params['var'])) {
|
if (isset($params['var'])) {
|
||||||
|
|
|
@ -20,43 +20,31 @@ 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)
|
|
||||||
*
|
|
||||||
* @var string $host
|
|
||||||
*/
|
|
||||||
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
|
||||||
*
|
*
|
||||||
|
@ -162,19 +150,9 @@ class Main
|
||||||
return $this->connector;
|
return $this->connector;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* get host (f.e. https://contentnation.net)
|
|
||||||
*
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public function getHost()
|
|
||||||
{
|
|
||||||
return $this->host;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* get config
|
* get config
|
||||||
* @return array<string, mixed>
|
* @return Array<String, Mixed>
|
||||||
*/
|
*/
|
||||||
public function getConfig()
|
public function getConfig()
|
||||||
{
|
{
|
||||||
|
@ -218,7 +196,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'],
|
||||||
|
@ -254,8 +232,6 @@ class Main
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* set cache
|
* set cache
|
||||||
*
|
|
||||||
* @param \Federator\Cache\Cache $cache the new cache
|
|
||||||
*/
|
*/
|
||||||
public function setCache(Cache\Cache $cache) : void
|
public function setCache(Cache\Cache $cache) : void
|
||||||
{
|
{
|
||||||
|
@ -264,34 +240,12 @@ class Main
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* set connector
|
* set connector
|
||||||
*
|
|
||||||
* @param \Federator\Connector\Connector $connector the new connector
|
|
||||||
*/
|
*/
|
||||||
public function setConnector(Connector\Connector $connector) : void
|
public function setConnector(Connector\Connector $connector) : void
|
||||||
{
|
{
|
||||||
$this->connector = $connector;
|
$this->connector = $connector;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* set host
|
|
||||||
*
|
|
||||||
* @param string $host the new host url
|
|
||||||
*/
|
|
||||||
public function setHost(string $host) : void
|
|
||||||
{
|
|
||||||
$this->host = $host;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* set content type
|
|
||||||
*
|
|
||||||
* @param string $_type content type
|
|
||||||
*/
|
|
||||||
public function setContentType($_type): void
|
|
||||||
{
|
|
||||||
$this->contentType = $_type;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* set response code
|
* set response code
|
||||||
*
|
*
|
||||||
|
|
|
@ -22,7 +22,7 @@ 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'];
|
$root = $_SERVER['DOCUMENT_ROOT'];
|
||||||
include $root . '../php/' . str_replace("\\", "/", strtolower($className)) . '.php';
|
include $root . '../php/' . str_replace("\\", "/", strtolower($className)) . '.php';
|
||||||
|
|
|
@ -1,200 +0,0 @@
|
||||||
<?php
|
|
||||||
/**
|
|
||||||
* SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* @author Sascha Nitsch (grumpydeveloper)
|
|
||||||
**/
|
|
||||||
|
|
||||||
namespace Federator;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* test functions
|
|
||||||
*/
|
|
||||||
class Test
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* fetch outbox
|
|
||||||
* @param string $_name user handle to fetch
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public static function fetchOutbox($_name)
|
|
||||||
{
|
|
||||||
// make webfinger request
|
|
||||||
$r = self::webfinger($_name);
|
|
||||||
$outbox = $r['outbox'];
|
|
||||||
$headers = ['Accept: application/activity+json'];
|
|
||||||
[$response, $info] = \Federator\Main::getFromRemote($outbox, $headers);
|
|
||||||
if ($info['http_code'] != 200) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
$json = json_decode($response, true);
|
|
||||||
if ($json === false) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
$ap = \Federator\Data\ActivityPub\Factory::newFromJson($json, $response);
|
|
||||||
if (!($ap instanceof \Federator\Data\ActivityPub\Common\OrderedCollection)) {
|
|
||||||
echo "unsupport reply from $outbox\n";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// first and total items should be set
|
|
||||||
$total = $ap->getTotalItems();
|
|
||||||
echo "total items: " . $total;
|
|
||||||
echo " first: " . $ap->getFirst() . "\n";
|
|
||||||
|
|
||||||
$page = $ap->getFirst();
|
|
||||||
$count = 0;
|
|
||||||
|
|
||||||
while ($count < $total) {
|
|
||||||
echo "query $page\n";
|
|
||||||
// query pages
|
|
||||||
[$response, $info] = \Federator\Main::getFromRemote($page, $headers);
|
|
||||||
if ($info['http_code'] != 200) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// echo "'$response'\n";
|
|
||||||
$json = json_decode($response, true);
|
|
||||||
$ap = \Federator\Data\ActivityPub\Factory::newFromJson($json, $response);
|
|
||||||
if (!($ap instanceof \Federator\Data\ActivityPub\Common\OrderedCollectionPage)) {
|
|
||||||
echo "unsupport reply from $page\n";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
$thisCount = $ap->getCount();
|
|
||||||
echo "count: " . $thisCount . "\n";
|
|
||||||
for ($i = 0; $i < $thisCount; ++$i) {
|
|
||||||
$entry = $ap->get($i);
|
|
||||||
if ($entry instanceof \Federator\Data\ActivityPub\Common\APObject) {
|
|
||||||
echo $entry->getID() . " " . $entry->getPublished() . "\n";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$count += $thisCount;
|
|
||||||
$page = $ap->getNext();
|
|
||||||
}
|
|
||||||
//print_r($ap);
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* run test
|
|
||||||
*
|
|
||||||
* @param int $argc number of arguments
|
|
||||||
* @param string[] $argv arguments
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public static function run($argc, $argv)
|
|
||||||
{
|
|
||||||
date_default_timezone_set('Europe/Berlin');
|
|
||||||
spl_autoload_register(static function (string $className) {
|
|
||||||
include PROJECT_ROOT . '/php/' . str_replace("\\", "/", strtolower($className)) . '.php';
|
|
||||||
});
|
|
||||||
if ($argc < 2) {
|
|
||||||
self::printUsage();
|
|
||||||
}
|
|
||||||
// pretend that we are running from web directory
|
|
||||||
define('PROJECT_ROOT', dirname(__DIR__, 2));
|
|
||||||
$api = new \Federator\Api();
|
|
||||||
$api->loadPlugins();
|
|
||||||
$api->openDatabase();
|
|
||||||
|
|
||||||
for ($i = 1; $i < $argc; ++$i) {
|
|
||||||
switch ($argv[$i]) {
|
|
||||||
case 'fetchoutbox':
|
|
||||||
self::fetchOutbox($argv[$i + 1]);
|
|
||||||
++$i;
|
|
||||||
break;
|
|
||||||
case 'upvote':
|
|
||||||
self::upvote($api, $argv[$i + 1]);
|
|
||||||
++$i;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
self::printUsage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* print usage of test tool
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public static function printUsage()
|
|
||||||
{
|
|
||||||
echo "usage php test.php <command> <parameter> [<command> <parameter>...]\n";
|
|
||||||
echo "command can be one of:\n";
|
|
||||||
echo " fetchoutbox - fetch users outbox. parameter: username\n";
|
|
||||||
echo " upvote - upvote. parameter: URL to upvote\n";
|
|
||||||
echo " downvote - downvote. parameter: URL to downvote\n";
|
|
||||||
echo " comment - comment. parameter: URL to comment, text to comment\n";
|
|
||||||
echo " Run this after you updated the program files\n";
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* upvote given URL
|
|
||||||
*
|
|
||||||
* @param \Federator\Api $api api instance
|
|
||||||
* @param string $_url URL to upvote
|
|
||||||
* @note uses hardcoded source
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public static function upvote($api, $_url)
|
|
||||||
{
|
|
||||||
$dbh = $api->getDatabase();
|
|
||||||
$inboxActivity = new \Federator\Data\ActivityPub\Common\Like();
|
|
||||||
$inboxActivity->setAActor('https://mastodon.local/users/admin');
|
|
||||||
$inboxActivity->setObject($_url);
|
|
||||||
$inboxActivity->setID("https://mastodon.local/users/admin#like/" . md5($_url));
|
|
||||||
\Federator\DIO\FedUser::inboxForUser(
|
|
||||||
$api,
|
|
||||||
$dbh,
|
|
||||||
$api->getConnector(),
|
|
||||||
null,
|
|
||||||
'admin@mastodon.local',
|
|
||||||
"grumpydevelop@192.168.178.143",
|
|
||||||
$inboxActivity
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* do a webfinger request
|
|
||||||
* @param string $_name name to query
|
|
||||||
*/
|
|
||||||
private static function webfinger($_name): mixed
|
|
||||||
{
|
|
||||||
// make webfinger request
|
|
||||||
if (preg_match("/^([^@]+)@(.*)$/", $_name, $matches) != 1) {
|
|
||||||
echo "username is malformed";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
$remoteURL = 'https://' . $matches[2] . '/.well-known/webfinger?resource=acct:' . urlencode($_name);
|
|
||||||
$headers = ['Accept: application/activity+json'];
|
|
||||||
[$response, $info] = \Federator\Main::getFromRemote($remoteURL, $headers);
|
|
||||||
if ($info['http_code'] != 200) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
$r = json_decode($response, true);
|
|
||||||
if (isset($r['links'])) {
|
|
||||||
foreach ($r['links'] as $link) {
|
|
||||||
if (isset($link['rel']) && $link['rel'] === 'self') {
|
|
||||||
$remoteURL = $link['href'];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!isset($remoteURL)) {
|
|
||||||
echo "FedUser::getUserByName Failed to find self link in webfinger for " . $_name . "\n";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// fetch the user
|
|
||||||
$headers = ['Accept: application/activity+json'];
|
|
||||||
[$response, $info] = \Federator\Main::getFromRemote($remoteURL, $headers);
|
|
||||||
if ($info['http_code'] != 200) {
|
|
||||||
echo "FedUser::getUserByName Failed to fetch user from remoteUrl for " . $_name . "\n";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
$r = json_decode($response, true);
|
|
||||||
return $r;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Test::run($argc, $argv);
|
|
|
@ -1,27 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
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) {
|
|
||||||
include PROJECT_ROOT . '/php/' . str_replace("\\", "/", strtolower($className)) . '.php';
|
|
||||||
});
|
|
||||||
|
|
||||||
$config = parse_ini_file(PROJECT_ROOT . '/rediscache.ini');
|
|
||||||
|
|
||||||
// Set the Redis backend for Resque
|
|
||||||
$redisUrl = sprintf(
|
|
||||||
'redis://%s:%s@%s:%d?password-encoding=u',
|
|
||||||
urlencode($config['username']),
|
|
||||||
urlencode($config['password']),
|
|
||||||
$config['host'],
|
|
||||||
intval($config['port'], 10)
|
|
||||||
);
|
|
||||||
\Resque::setBackend($redisUrl);
|
|
||||||
|
|
||||||
// Start the worker
|
|
||||||
$worker = new \Resque_Worker(['inbox']);
|
|
||||||
|
|
||||||
fwrite(STDOUT, "*** Starting worker for inbox queue\n");
|
|
||||||
$worker->work(10); // 10 seconds interval
|
|
File diff suppressed because it is too large
Load diff
|
@ -19,38 +19,15 @@ class DummyConnector implements Connector
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* get followers of given user
|
|
||||||
*
|
|
||||||
* @param string $userId user id @unused-param
|
|
||||||
* @return \Federator\Data\FedUser[]|false
|
|
||||||
*/
|
|
||||||
public function getFollowersByUser($userId)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* get following of given user
|
|
||||||
*
|
|
||||||
* @param string $id user id @unused-param
|
|
||||||
* @return \Federator\Data\FedUser[]|false
|
|
||||||
*/
|
|
||||||
public function getFollowingByUser($id)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* get posts by given user
|
* get posts by given user
|
||||||
*
|
*
|
||||||
* @param string $id user id @unused-param
|
* @param string $id user id @unused-param
|
||||||
* @param int $min min timestamp @unused-param
|
* @param string $minId min ID @unused-param
|
||||||
* @param int $max max timestamp @unused-param
|
* @param string $maxId max ID @unused-param
|
||||||
* @param int $limit limit number of results @unused-param
|
* @return \Federator\Data\ActivityPub\Common\APObject[]|false
|
||||||
* @return \Federator\Data\ActivityPub\Common\Activity[]|false
|
|
||||||
*/
|
*/
|
||||||
public function getRemotePostsByUser($id, $min, $max, $limit)
|
public function getRemotePostsByUser($id, $minId, $maxId)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -69,19 +46,6 @@ class DummyConnector implements Connector
|
||||||
return $stats;
|
return $stats;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert jsonData to Activity format
|
|
||||||
*
|
|
||||||
* @param array<string, mixed> $jsonData the json data from our platfrom @unused-param
|
|
||||||
* @param string $articleId the original id of the article (if applicable)
|
|
||||||
* (used to identify the article in the remote system) @unused-param
|
|
||||||
* @return \Federator\Data\ActivityPub\Common\Activity|false
|
|
||||||
*/
|
|
||||||
public function jsonToActivity(array $jsonData, &$articleId)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* get remote user by name
|
* get remote user by name
|
||||||
* @param string $_name user or profile name
|
* @param string $_name user or profile name
|
||||||
|
@ -106,34 +70,10 @@ class DummyConnector implements Connector
|
||||||
// validate $_session and $user
|
// validate $_session and $user
|
||||||
$user = new \Federator\Data\User();
|
$user = new \Federator\Data\User();
|
||||||
$user->externalid = $_user;
|
$user->externalid = $_user;
|
||||||
$user->permissions = ['publish'];
|
$user->permissions = ['PUBLISH'];
|
||||||
$user->session = $_session;
|
$user->session = $_session;
|
||||||
return $user;
|
return $user;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* send target-friendly json from ActivityPub activity
|
|
||||||
*
|
|
||||||
* @param \Federator\Data\FedUser $sender the user of the sender @unused-param
|
|
||||||
* @param \Federator\Data\ActivityPub\Common\Activity $activity the activity @unused-param
|
|
||||||
* @return boolean did we successfully send the activity?
|
|
||||||
*/
|
|
||||||
public function sendActivity($sender, $activity)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* check if the headers include a valid signature
|
|
||||||
*
|
|
||||||
* @param string[] $headers the headers @unused-param
|
|
||||||
* @throws \Federator\Exceptions\PermissionDenied
|
|
||||||
* @return string|\Federator\Exceptions\PermissionDenied
|
|
||||||
*/
|
|
||||||
public function checkSignature($headers)
|
|
||||||
{
|
|
||||||
return new \Federator\Exceptions\PermissionDenied("Dummy connector: no signature check");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace Federator;
|
namespace Federator;
|
||||||
|
|
|
@ -41,13 +41,6 @@ class RedisCache implements Cache
|
||||||
*/
|
*/
|
||||||
private $userTTL;
|
private $userTTL;
|
||||||
|
|
||||||
/**
|
|
||||||
* public key cache time to live in secods
|
|
||||||
*
|
|
||||||
* @var int $publicKeyPemTTL
|
|
||||||
*/
|
|
||||||
private $publicKeyPemTTL;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* constructor
|
* constructor
|
||||||
*/
|
*/
|
||||||
|
@ -57,8 +50,6 @@ class RedisCache implements Cache
|
||||||
if ($config !== false) {
|
if ($config !== false) {
|
||||||
$this->config = $config;
|
$this->config = $config;
|
||||||
$this->userTTL = array_key_exists('userttl', $config) ? intval($config['userttl'], 10) : 60;
|
$this->userTTL = array_key_exists('userttl', $config) ? intval($config['userttl'], 10) : 60;
|
||||||
$this->publicKeyPemTTL = array_key_exists('publickeypemttl', $config)
|
|
||||||
? intval($config['publickeypemttl'], 10) : 3600;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,16 +63,6 @@ class RedisCache implements Cache
|
||||||
$this->redis->pconnect($this->config['host'], intval($this->config['port'], 10));
|
$this->redis->pconnect($this->config['host'], intval($this->config['port'], 10));
|
||||||
// @phan-suppress-next-line PhanTypeMismatchArgumentInternalProbablyReal
|
// @phan-suppress-next-line PhanTypeMismatchArgumentInternalProbablyReal
|
||||||
$this->redis->auth([$this->config['username'], $this->config['password']]);
|
$this->redis->auth([$this->config['username'], $this->config['password']]);
|
||||||
|
|
||||||
// Set the Redis backend for Resque
|
|
||||||
$redisUrl = sprintf(
|
|
||||||
'redis://%s:%s@%s:%d',
|
|
||||||
urlencode($this->config['username']),
|
|
||||||
urlencode($this->config['password']),
|
|
||||||
$this->config['host'],
|
|
||||||
intval($this->config['port'], 10)
|
|
||||||
);
|
|
||||||
\Resque::setBackend($redisUrl);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -96,57 +77,16 @@ class RedisCache implements Cache
|
||||||
return $prefix . '_' . md5($input);
|
return $prefix . '_' . md5($input);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* get followers of given user
|
|
||||||
*
|
|
||||||
* @param string $id user id @unused-param
|
|
||||||
|
|
||||||
* @return \Federator\Data\FedUser[]|false
|
|
||||||
*/
|
|
||||||
public function getFollowersByUser($id)
|
|
||||||
{
|
|
||||||
error_log("rediscache::getFollowersByUser not implemented");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* get following of given user
|
|
||||||
*
|
|
||||||
* @param string $id user id @unused-param
|
|
||||||
|
|
||||||
* @return \Federator\Data\FedUser[]|false
|
|
||||||
*/
|
|
||||||
public function getFollowingByUser($id)
|
|
||||||
{
|
|
||||||
error_log("rediscache::getFollowingByUser not implemented");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert jsonData to Activity format
|
|
||||||
*
|
|
||||||
* @param array<string, mixed> $jsonData the json data from our platfrom @unused-param
|
|
||||||
* @param string $articleId the original id of the article (if applicable)
|
|
||||||
* (used to identify the article in the remote system) @unused-param
|
|
||||||
* @return \Federator\Data\ActivityPub\Common\Activity|false
|
|
||||||
*/
|
|
||||||
public function jsonToActivity(array $jsonData, &$articleId)
|
|
||||||
{
|
|
||||||
error_log("rediscache::jsonToActivity not implemented");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* get posts by given user
|
* get posts by given user
|
||||||
*
|
*
|
||||||
* @param string $id user id @unused-param
|
* @param string $id user id @unused-param
|
||||||
* @param int $min min timestamp @unused-param
|
* @param string $minId min ID @unused-param
|
||||||
* @param int $max max timestamp @unused-param
|
* @param string $maxId max ID @unused-param
|
||||||
* @param int $limit limit results @unused-param
|
|
||||||
|
|
||||||
* @return \Federator\Data\ActivityPub\Common\Activity[]|false
|
* @return \Federator\Data\ActivityPub\Common\APObject[]|false
|
||||||
*/
|
*/
|
||||||
public function getRemotePostsByUser($id, $min, $max, $limit)
|
public function getRemotePostsByUser($id, $minId, $maxId)
|
||||||
{
|
{
|
||||||
error_log("rediscache::getRemotePostsByUser not implemented");
|
error_log("rediscache::getRemotePostsByUser not implemented");
|
||||||
return false;
|
return false;
|
||||||
|
@ -191,26 +131,6 @@ class RedisCache implements Cache
|
||||||
return $user;
|
return $user;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* get remote federation user by given name
|
|
||||||
*
|
|
||||||
* @param string $_name user/profile name
|
|
||||||
* @return \Federator\Data\FedUser | false
|
|
||||||
*/
|
|
||||||
public function getRemoteFedUserByName(string $_name)
|
|
||||||
{
|
|
||||||
if (!$this->connected) {
|
|
||||||
$this->connect();
|
|
||||||
}
|
|
||||||
$key = self::createKey('u', $_name);
|
|
||||||
$data = $this->redis->get($key);
|
|
||||||
if ($data === false) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
$user = \Federator\Data\FedUser::createFromJson($data);
|
|
||||||
return $user;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* get remote user by given session
|
* get remote user by given session
|
||||||
*
|
*
|
||||||
|
@ -232,56 +152,14 @@ class RedisCache implements Cache
|
||||||
return $user;
|
return $user;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve the public key for a given keyId
|
|
||||||
*
|
|
||||||
* @param string $keyId The keyId (e.g., actor URL + #main-key)
|
|
||||||
* @return string|false The cached public key PEM or false if not found
|
|
||||||
*/
|
|
||||||
public function getPublicKey(string $keyId)
|
|
||||||
{
|
|
||||||
if (!$this->connected) {
|
|
||||||
$this->connect();
|
|
||||||
}
|
|
||||||
$key = self::createKey('publickey', $keyId);
|
|
||||||
return $this->redis->get($key);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* save remote followers by user
|
|
||||||
*
|
|
||||||
* @param string $user user name @unused-param
|
|
||||||
* @param \Federator\Data\FedUser[]|false $followers user followers @unused-param
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function saveFollowersByUser($user, $followers)
|
|
||||||
{
|
|
||||||
error_log("rediscache::saveFollowersByUser not implemented");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* save remote following for user
|
|
||||||
*
|
|
||||||
* @param string $user user name @unused-param
|
|
||||||
* @param \Federator\Data\FedUser[]|false $following user following @unused-param
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function saveFollowingByUser($user, $following)
|
|
||||||
{
|
|
||||||
error_log("rediscache::saveFollowingByUser not implemented");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* save remote posts by user
|
* save remote posts by user
|
||||||
*
|
*
|
||||||
* @param string $user user name @unused-param
|
* @param string $user user name @unused-param
|
||||||
* @param int $min min timestamp @unused-param
|
|
||||||
* @param int $max max timestamp @unused-param
|
|
||||||
* @param int $limit limit results @unused-param
|
|
||||||
* @param \Federator\Data\ActivityPub\Common\APObject[]|false $posts user posts @unused-param
|
* @param \Federator\Data\ActivityPub\Common\APObject[]|false $posts user posts @unused-param
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function saveRemotePostsByUser($user, $min, $max, $limit, $posts)
|
public function saveRemotePostsByUser($user, $posts)
|
||||||
{
|
{
|
||||||
error_log("rediscache::saveRemotePostsByUser not implemented");
|
error_log("rediscache::saveRemotePostsByUser not implemented");
|
||||||
}
|
}
|
||||||
|
@ -301,7 +179,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
|
||||||
*
|
*
|
||||||
|
@ -310,23 +187,6 @@ class RedisCache implements Cache
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function saveRemoteUserByName($_name, $user)
|
public function saveRemoteUserByName($_name, $user)
|
||||||
{
|
|
||||||
if (!$this->connected) {
|
|
||||||
$this->connect();
|
|
||||||
}
|
|
||||||
$key = self::createKey('u', $_name);
|
|
||||||
$serialized = $user->toJson();
|
|
||||||
$this->redis->setEx($key, $this->userTTL, $serialized);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* save remote federation user by given name
|
|
||||||
*
|
|
||||||
* @param string $_name user/profile name
|
|
||||||
* @param \Federator\Data\FedUser $user user data
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function saveRemoteFedUserByName(string $_name, \Federator\Data\FedUser $user)
|
|
||||||
{
|
{
|
||||||
$key = self::createKey('u', $_name);
|
$key = self::createKey('u', $_name);
|
||||||
$serialized = $user->toJson();
|
$serialized = $user->toJson();
|
||||||
|
@ -345,47 +205,7 @@ class RedisCache implements Cache
|
||||||
{
|
{
|
||||||
$key = self::createKey('s', $_session . $_user);
|
$key = self::createKey('s', $_session . $_user);
|
||||||
$serialized = $user->toJson();
|
$serialized = $user->toJson();
|
||||||
$this->redis->setEx($key, $this->userTTL, $serialized);
|
$this->redis->setEx($key, $this->userTTL, $serialized,);
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Save the public key for a given keyId
|
|
||||||
*
|
|
||||||
* @param string $keyId The keyId (e.g., actor URL + #main-key)
|
|
||||||
* @param string $publicKeyPem The public key PEM to cache
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function savePublicKey(string $keyId, string $publicKeyPem)
|
|
||||||
{
|
|
||||||
if (!$this->connected) {
|
|
||||||
$this->connect();
|
|
||||||
}
|
|
||||||
$key = self::createKey('publickey', $keyId);
|
|
||||||
$this->redis->setEx($key, $this->publicKeyPemTTL, $publicKeyPem);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* send target-friendly json from ActivityPub activity
|
|
||||||
*
|
|
||||||
* @param \Federator\Data\FedUser $sender the user of the sender @unused-param
|
|
||||||
* @param \Federator\Data\ActivityPub\Common\Activity $activity the activity @unused-param
|
|
||||||
* @return boolean did we successfully send the activity?
|
|
||||||
*/
|
|
||||||
public function sendActivity($sender, $activity)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* check if the headers include a valid signature
|
|
||||||
*
|
|
||||||
* @param string[] $headers the headers @unused-param
|
|
||||||
* @throws \Federator\Exceptions\PermissionDenied
|
|
||||||
* @return string|\Federator\Exceptions\PermissionDenied
|
|
||||||
*/
|
|
||||||
public function checkSignature($headers)
|
|
||||||
{
|
|
||||||
return new \Federator\Exceptions\PermissionDenied("RedisCache: no signature check");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -4,5 +4,4 @@ port = 6379
|
||||||
username = federator
|
username = federator
|
||||||
password = redis*change*password
|
password = redis*change*password
|
||||||
userttl = 10
|
userttl = 10
|
||||||
publickeypemttl = 3600
|
|
||||||
statsttl = 60
|
statsttl = 60
|
||||||
|
|
|
@ -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");
|
|
|
@ -1,3 +0,0 @@
|
||||||
create table follows(`id` varchar(255) unique primary key,`source_user` varchar(255) not null,`target_user` varchar(255) not null,`created_at` timestamp default current_timestamp,unique key `unique_follow` (`source_user`, `target_user`));
|
|
||||||
create table fedusers(`id` varchar(255) unique primary key,`url` varchar(255) not null,`name` varchar(255) default '',`publickey` text default '',`summary` text default '',`validuntil` timestamp null default null,`type` enum('person', 'group') default 'person',`inboxurl` varchar(255) default null,`sharedinboxurl` varchar(255) default null,`followersurl` varchar(255) default null,`followingurl` varchar(255) default null,`publickeyid` varchar(255) default null,`outboxurl` varchar(255) default null,unique key `unique_feduser` (`url`),unique key `unique_feduser_id` (`url`));
|
|
||||||
update settings set `value`="2025-05-06" where `key`="database_version";
|
|
|
@ -1,2 +0,0 @@
|
||||||
create table posts(`id` varchar(255) primary key,`url` varchar(255) not null,`user_id` varchar(255) not null,`actor` varchar(255) not null,`type` varchar(255) not null default 'note',`object` text default null,`to` text default null,`cc` text default null,`published` timestamp not null default current_timestamp);
|
|
||||||
update settings set `value`="2025-05-19" where `key`="database_version";
|
|
|
@ -1,2 +0,0 @@
|
||||||
alter table posts add `article_id` varchar(255) null default null comment 'The optional original article id (of non-federated system, e.g. CN)';
|
|
||||||
update settings set `value`="2025-05-27" where `key`="database_version";
|
|
|
@ -1,4 +1,4 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
version="1.0" encoding="UTF-8"?>
|
||||||
<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">
|
<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">
|
||||||
<Link rel="lrdd" template="https://{$fqdn}/.well-known/webfinger?resource={ldelim}uri{rdelim}"/>
|
<Link rel="lrdd" template="https://{$fqdn}/.well-known/webfinger?resource={ldelim}uri{rdelim}"/>
|
||||||
</XRD>
|
</XRD>
|
||||||
|
|
|
@ -58,24 +58,24 @@
|
||||||
{rdelim}
|
{rdelim}
|
||||||
{rdelim}
|
{rdelim}
|
||||||
],
|
],
|
||||||
"id":"https://{$fqdn}/{$username}",
|
"id":"https://{$fqdn}/users/{$username}",
|
||||||
"type":"{$type}",
|
"type":"{$type}",
|
||||||
"following":"https://{$fqdn}/{$username}/following",
|
"following":"https://{$fqdn}/users/{$username}/following",
|
||||||
"followers":"https://{$fqdn}/{$username}/followers",
|
"followers":"https://{$fqdn}/users/{$username}/followers",
|
||||||
"inbox":"https://{$fqdn}/{$username}/inbox",
|
"inbox":"https://{$fqdn}/users/{$username}/inbox",
|
||||||
"outbox":"https://{$fqdn}/{$username}/outbox",
|
"outbox":"https://{$fqdn}/users/{$username}/outbox",
|
||||||
{*"featured":"https://{$fqdn}/{$username}/collections/featured",
|
"featured":"https://{$fqdn}/users/{$username}/collections/featured",
|
||||||
"featuredTags":"https://{$fqdn}/{$username}/collections/tags",*}
|
"featuredTags":"https://{$fqdn}/users/{$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}/users/{$username}#main-key",
|
||||||
"owner":"https://{$sourcedomain}/{$username}",
|
"owner":"https://{$fqdn}/users/{$username}",
|
||||||
"publicKeyPem":"{$publickey}"
|
"publicKeyPem":"{$publickey}"
|
||||||
{rdelim},
|
{rdelim},
|
||||||
"tag":[],
|
"tag":[],
|
||||||
|
@ -83,7 +83,7 @@
|
||||||
{if $type==='group'}{ldelim}
|
{if $type==='group'}{ldelim}
|
||||||
"type":"PropertyValue",
|
"type":"PropertyValue",
|
||||||
"name":"website",
|
"name":"website",
|
||||||
"value":"\u003ca href=\"https://{$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,8 @@
|
||||||
{ldelim}
|
{ldelim}
|
||||||
"subject": "acct:{$username}@{$sourcedomain}",
|
"subject": "acct:{$username}@{$domain}",
|
||||||
"aliases": [
|
"aliases": [
|
||||||
"https://{$sourcedomain}/@{$username}"
|
"https://{$domain}/@{$username}",
|
||||||
|
"https://{$domain}/users/{$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