forked from grumpydevelop/federator
very rudimental follower-support
- fixed rewrites to properly support @username/outbox, username/outbox, users/username/outbox, ... - initial support for sending follow to e.g. mastodon (code commented out in api.php) - database migration for follows table and fedusers table - save retrieved fedusers in cache and db - depend more on configs externaldomain, less on server_name - properly implemented following-logic in dio - made postForuUser static in newContent and inbox - provide rsaprivate for signing reuests -> actPub-Servers - change contenttype depending on use-case - changed user json-template to have loop-back between user-template and webfinger (mastodon f.e. needs this)
This commit is contained in:
parent
ce7aa5c72d
commit
da18d37a79
31 changed files with 1103 additions and 243 deletions
|
@ -53,9 +53,10 @@ To configure an apache server, add the following rewrite rules:
|
|||
RewriteRule ^@(.*)$ /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 ^(nodeinfo/2\.[01])$ /federator.php?_call=$1 [L,END]
|
||||
RewriteRule ^([a-zA-Z0-9_-]+.*)$ /federator.php?_call=fedusers/$1 [L,END]
|
||||
</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.
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
<div class="request-box border p-4 rounded-lg mb-4 bg-gray-50 overflow-y-auto">
|
||||
<label class="block font-medium">API target link</label>
|
||||
<input type="text" class="target-link-input w-full p-2 border rounded-md mb-2"
|
||||
placeholder="Enter target link" value="federator/fedusers/grumpydevelop/outbox?page=0">
|
||||
placeholder="Enter target link" value="users/grumpydevelop/outbox?page=0">
|
||||
|
||||
<label class="block font-medium">Request type</label>
|
||||
<input type="text" class="request-type-input w-full p-2 border rounded-md mb-2"
|
||||
|
@ -65,7 +65,7 @@
|
|||
...(profile ? { "X-Profile": profile } : {})
|
||||
};
|
||||
|
||||
fetch("http://localhost/api/" + targetLink, {
|
||||
fetch("http://localhost/" + targetLink, {
|
||||
method: requestType,
|
||||
headers
|
||||
})
|
||||
|
@ -94,7 +94,7 @@
|
|||
const container = document.getElementById("request-container");
|
||||
const requestBox = container.firstElementChild.cloneNode(true);
|
||||
|
||||
requestBox.querySelector(".target-link-input").value = "federator/fedusers/grumpydevelop@contentnation.net/outbox?page=0";
|
||||
requestBox.querySelector(".target-link-input").value = "users/grumpydevelop@contentnation.net/outbox?page=0";
|
||||
requestBox.querySelector(".request-type-input").value = "GET";
|
||||
requestBox.querySelector(".session-input").value = "";
|
||||
requestBox.querySelector(".profile-input").value = "";
|
||||
|
|
|
@ -101,6 +101,7 @@ class Api extends Main
|
|||
break;
|
||||
case 'fedusers':
|
||||
$handler = new Api\FedUsers($this);
|
||||
$this->setContentType("application/activity+json");
|
||||
break;
|
||||
case 'v1':
|
||||
switch ($this->paths[1]) {
|
||||
|
@ -110,6 +111,39 @@ class Api extends Main
|
|||
case 'newcontent':
|
||||
$handler = new Api\V1\NewContent($this);
|
||||
break;
|
||||
/* case 'sendFollow': { // hacky implementation for testing purposes
|
||||
$username = $this->paths[2];
|
||||
$domain = $this->config['generic']['externaldomain'];
|
||||
$response = \Federator\DIO\Followers::sendFollowRequest(
|
||||
$this->dbh,
|
||||
$this->connector,
|
||||
$this->cache,
|
||||
$username,
|
||||
"admin@mastodon.local",
|
||||
$domain
|
||||
);
|
||||
header("Content-type: " . $this->contentType);
|
||||
header("Access-Control-Allow-Origin: *");
|
||||
header("Cache-Control: no-cache, no-store, must-revalidate");
|
||||
header("Pragma: no-cache");
|
||||
header("Expires: 0");
|
||||
if (is_string($response)) {
|
||||
$this->setResponseCode(200);
|
||||
$retval = json_encode(array(
|
||||
"status" => "ok",
|
||||
"message" => $response
|
||||
));
|
||||
} else {
|
||||
$this->setResponseCode(500);
|
||||
$retval = json_encode(array(
|
||||
"status" => "error",
|
||||
"message" => "Failed to send follow request"
|
||||
));
|
||||
}
|
||||
http_response_code($this->responseCode);
|
||||
echo $retval;
|
||||
return;
|
||||
} */
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -50,21 +50,10 @@ class FedUsers implements APIInterface
|
|||
$method = $_SERVER["REQUEST_METHOD"];
|
||||
$handler = null;
|
||||
$_username = $paths[1];
|
||||
if (preg_match("#^([^@]+)@([^/]+)#", $_username, $matches) != 1) {
|
||||
$hostUrl = $this->main->getHost();
|
||||
if ($hostUrl !== false) {
|
||||
$host = parse_url($hostUrl, PHP_URL_HOST);
|
||||
$port = parse_url($hostUrl, PHP_URL_PORT);
|
||||
if ($port !== null) {
|
||||
$host .= ":$port";
|
||||
}
|
||||
$_username = "$_username@$host";
|
||||
}
|
||||
}
|
||||
switch (sizeof($paths)) {
|
||||
case 2:
|
||||
if ($method === 'GET') {
|
||||
// /fedusers/username or /@username
|
||||
// /users/username or /@username or /username
|
||||
return $this->returnUserProfile($_username);
|
||||
} else {
|
||||
switch ($paths[1]) {
|
||||
|
@ -78,7 +67,7 @@ class FedUsers implements APIInterface
|
|||
}
|
||||
break;
|
||||
case 3:
|
||||
// /fedusers/username/(inbox|outbox|following|followers)
|
||||
// /users/username/(inbox|outbox|following|followers)
|
||||
switch ($paths[2]) {
|
||||
case 'following':
|
||||
// $handler = new FedUsers\Following();
|
||||
|
@ -95,7 +84,7 @@ class FedUsers implements APIInterface
|
|||
}
|
||||
break;
|
||||
case 4:
|
||||
// /fedusers/username/collections/(features|tags)
|
||||
// /users/username/collections/(features|tags)
|
||||
// not yet implemented
|
||||
break;
|
||||
}
|
||||
|
@ -153,7 +142,7 @@ class FedUsers implements APIInterface
|
|||
'publickey' => trim($jsonKey, '"'),
|
||||
'registered' => gmdate('Y-m-d\TH:i:s\Z', $user->registered), // 2021-03-25T00:00:00Z
|
||||
'summary' => $user->summary,
|
||||
'type' => $user->type
|
||||
'type' => ucfirst($user->type) // capitalized user type
|
||||
];
|
||||
$this->response = $this->main->renderTemplate('user.json', $data);
|
||||
return true;
|
||||
|
|
|
@ -60,7 +60,6 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
|
|||
}
|
||||
|
||||
$activity = is_string($_rawInput) ? json_decode($_rawInput, true) : null;
|
||||
$host = $_SERVER['SERVER_NAME'];
|
||||
|
||||
if (!is_array($activity)) {
|
||||
error_log("Inbox::post Input wasn't of type array");
|
||||
|
@ -69,10 +68,10 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
|
|||
|
||||
$inboxActivity = \Federator\Data\ActivityPub\Factory::newActivityFromJson($activity);
|
||||
|
||||
$rootDir = $_SERVER['DOCUMENT_ROOT'] . '../';
|
||||
|
||||
// Shared inbox
|
||||
if (!isset($_user)) {
|
||||
$rootDir = $_SERVER['DOCUMENT_ROOT'] . '../';
|
||||
|
||||
// Save the raw input and parsed JSON to a file for inspection
|
||||
file_put_contents(
|
||||
$rootDir . 'logs/inbox.log',
|
||||
|
@ -89,12 +88,15 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
|
|||
$sendTo = $inboxActivity->getCC();
|
||||
if ($inboxActivity->getType() === 'Undo') { // for undo the object holds the proper cc
|
||||
$object = $inboxActivity->getObject();
|
||||
if ($object !== null) {
|
||||
if ($object !== null && is_object($object)) {
|
||||
$sendTo = $object->getCC();
|
||||
}
|
||||
}
|
||||
|
||||
$users = [];
|
||||
$dbh = $this->main->getDatabase();
|
||||
$cache = $this->main->getCache();
|
||||
$connector = $this->main->getConnector();
|
||||
|
||||
foreach ($sendTo as $receiver) {
|
||||
if ($receiver === '' || !is_string($receiver)) {
|
||||
|
@ -102,9 +104,23 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
|
|||
}
|
||||
|
||||
if (str_ends_with($receiver, '/followers')) {
|
||||
$followers = $this->fetchAllFollowers($receiver, $host);
|
||||
$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);
|
||||
if ($username === null || $domain === null) {
|
||||
error_log("Inbox::post no username or domain found");
|
||||
continue;
|
||||
}
|
||||
$followers = \Federator\DIO\Followers::getFollowersByFedUser($dbh, $connector, $cache, $username . '@' . $domain);
|
||||
|
||||
if (is_array($followers)) {
|
||||
$users = array_merge($users, $followers);
|
||||
$users = array_merge($users, array_column($followers, 'id'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -116,7 +132,7 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
|
|||
continue;
|
||||
}
|
||||
|
||||
$this->postForUser($user, $inboxActivity);
|
||||
$this->postForUser($dbh, $connector, $cache, $user, $inboxActivity);
|
||||
}
|
||||
return json_encode($inboxActivity, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
|
||||
}
|
||||
|
@ -124,17 +140,16 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
|
|||
/**
|
||||
* handle post call for specific user
|
||||
*
|
||||
* @param \mysqli $dbh database handle
|
||||
* @param \Federator\Connector\Connector $connector connector to use
|
||||
* @param \Federator\Cache\Cache|null $cache optional caching service
|
||||
* @param string $_user user to add data to inbox
|
||||
* @param \Federator\Data\ActivityPub\Common\Activity $inboxActivity the activity that we received
|
||||
* @return boolean response
|
||||
*/
|
||||
private function postForUser($_user, $inboxActivity)
|
||||
private static function postForUser($dbh, $connector, $cache, $_user, $inboxActivity)
|
||||
{
|
||||
if (isset($_user)) {
|
||||
$dbh = $this->main->getDatabase();
|
||||
$cache = $this->main->getCache();
|
||||
$connector = $this->main->getConnector();
|
||||
|
||||
// get user
|
||||
$user = \Federator\DIO\User::getUserByName(
|
||||
$dbh,
|
||||
|
@ -142,7 +157,7 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
|
|||
$connector,
|
||||
$cache
|
||||
);
|
||||
if ($user->id === null) {
|
||||
if ($user === null || $user->id === null) {
|
||||
error_log("Inbox::postForUser couldn't find user: $_user");
|
||||
return false;
|
||||
}
|
||||
|
@ -158,64 +173,4 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
|
|||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* fetch all followers from url and return the ones that belong to our server
|
||||
*
|
||||
* @param string $collectionUrl The url of f.e. the posters followers
|
||||
* @param string $host our current host-url
|
||||
* @return string[] the names of the followers that are hosted on our server
|
||||
*/
|
||||
private static function fetchAllFollowers(string $collectionUrl, string $host): array
|
||||
{
|
||||
$users = [];
|
||||
|
||||
[$collectionResponse, $collectionInfo] = \Federator\Main::getFromRemote($collectionUrl, ['Accept: application/activity+json']);
|
||||
if ($collectionInfo['http_code'] != 200) {
|
||||
error_log("Inbox::fetchAllFollowers Failed to fetch follower collection metadata from $collectionUrl");
|
||||
return [];
|
||||
}
|
||||
|
||||
$collectionData = json_decode($collectionResponse, true);
|
||||
$nextPage = $collectionData['first'] ?? $collectionData['current'] ?? null;
|
||||
|
||||
if (!isset($nextPage)) {
|
||||
error_log("Inbox::fetchAllFollowers No 'first' or 'current' page in collection at $collectionUrl");
|
||||
return [];
|
||||
}
|
||||
|
||||
// Loop through all pages
|
||||
while ($nextPage) {
|
||||
[$pageResponse, $pageInfo] = \Federator\Main::getFromRemote($nextPage, ['Accept: application/activity+json']);
|
||||
if ($pageInfo['http_code'] != 200) {
|
||||
error_log("Inbox::fetchAllFollowers Failed to fetch follower page at $nextPage");
|
||||
break;
|
||||
}
|
||||
|
||||
$pageData = json_decode($pageResponse, true);
|
||||
$items = $pageData['orderedItems'] ?? $pageData['items'] ?? [];
|
||||
|
||||
foreach ($items as $followerUrl) {
|
||||
$parts = parse_url($followerUrl);
|
||||
if (!isset($parts['host']) || !str_ends_with($parts['host'], $host)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
[$actorResponse, $actorInfo] = \Federator\Main::getFromRemote($followerUrl, ['Accept: application/activity+json']);
|
||||
if ($actorInfo['http_code'] != 200) {
|
||||
error_log("Inbox::fetchAllFollowers Failed to fetch actor data for follower: $followerUrl");
|
||||
continue;
|
||||
}
|
||||
|
||||
$actorData = json_decode($actorResponse, true);
|
||||
if (isset($actorData['preferredUsername'])) {
|
||||
$users[] = $actorData['preferredUsername'];
|
||||
}
|
||||
}
|
||||
|
||||
$nextPage = $pageData['next'] ?? null;
|
||||
}
|
||||
|
||||
return $users;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -66,15 +66,16 @@ class Outbox implements \Federator\Api\FedUsers\FedUsersInterface
|
|||
} else {
|
||||
$items = [];
|
||||
}
|
||||
$host = $_SERVER['SERVER_NAME'];
|
||||
$id = 'https://' . $host . '/users/' . $_user . '/outbox';
|
||||
$config = $this->main->getConfig();
|
||||
$domain = $config['generic']['externaldomain'];
|
||||
$id = 'https://' . $domain . '/users/' . $_user . '/outbox';
|
||||
$outbox->setPartOf($id);
|
||||
$outbox->setID($id);
|
||||
if ($page !== '') {
|
||||
$id .= '?page=' . urlencode($page);
|
||||
}
|
||||
if ($page === '' || $outbox->count() == 0) {
|
||||
$outbox->setFirst($id);
|
||||
$outbox->setFirst($id . '?page=0');
|
||||
$outbox->setLast($id . '&min=0');
|
||||
}
|
||||
if (sizeof($items) > 0) {
|
||||
|
@ -84,7 +85,7 @@ class Outbox implements \Federator\Api\FedUsers\FedUsersInterface
|
|||
$outbox->setPrev($id . '&min=' . $oldestId);
|
||||
}
|
||||
$obj = $outbox->toObject();
|
||||
return json_encode($obj);
|
||||
return json_encode($obj, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -20,12 +20,8 @@ class Dummy implements \Federator\Api\APIInterface
|
|||
*/
|
||||
private $main;
|
||||
|
||||
/**
|
||||
* internal message to output
|
||||
*
|
||||
* @var string $message
|
||||
*/
|
||||
private $message = '';
|
||||
/** @var array<string, string> $message internal message to output */
|
||||
private $message = [];
|
||||
|
||||
/**
|
||||
* constructor
|
||||
|
@ -47,9 +43,9 @@ class Dummy implements \Federator\Api\APIInterface
|
|||
public function exec($paths, $user): bool
|
||||
{
|
||||
// only for user with the 'publish' permission
|
||||
// if ($user === false || $user->hasPermission('publish') === false) {
|
||||
// throw new \Federator\Exceptions\PermissionDenied();
|
||||
// }
|
||||
if ($user === false || $user->hasPermission('publish') === false) {
|
||||
throw new \Federator\Exceptions\PermissionDenied();
|
||||
}
|
||||
$method = $_SERVER["REQUEST_METHOD"];
|
||||
switch ($method) {
|
||||
case 'GET':
|
||||
|
@ -87,16 +83,13 @@ class Dummy implements \Federator\Api\APIInterface
|
|||
*/
|
||||
public function getDummy()
|
||||
{
|
||||
$dummyResponse = json_encode([
|
||||
$this->message = [
|
||||
'r1' => ' (__) ',
|
||||
'r2' => ' `------(oo) ',
|
||||
'r3' => ' || __ (__) ',
|
||||
'r4' => ' ||w || ',
|
||||
'r5' => ' '
|
||||
], JSON_PRETTY_PRINT);
|
||||
if ($dummyResponse !== false) {
|
||||
$this->message = $dummyResponse;
|
||||
}
|
||||
];
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -117,6 +110,6 @@ class Dummy implements \Federator\Api\APIInterface
|
|||
*/
|
||||
public function toJson()
|
||||
{
|
||||
return $this->message;
|
||||
return json_encode($this->message, JSON_PRETTY_PRINT) . "\n";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -71,12 +71,11 @@ class NewContent implements \Federator\Api\APIInterface
|
|||
/**
|
||||
* handle post call
|
||||
*
|
||||
* @param string|null $_user user that triggered the post
|
||||
* @param string|null $_user optional user that triggered the post
|
||||
* @return string|false response
|
||||
*/
|
||||
public function post($_user)
|
||||
{
|
||||
error_log("NewContent::post called with user: $_user");
|
||||
$_rawInput = file_get_contents('php://input');
|
||||
|
||||
$allHeaders = getallheaders();
|
||||
|
@ -89,7 +88,9 @@ class NewContent implements \Federator\Api\APIInterface
|
|||
}
|
||||
|
||||
$input = is_string($_rawInput) ? json_decode($_rawInput, true) : null;
|
||||
$host = $_SERVER['SERVER_NAME'];
|
||||
|
||||
$config = $this->main->getConfig();
|
||||
$domain = $config['generic']['externaldomain'];
|
||||
if (!is_array($input)) {
|
||||
error_log("NewContent::post Input wasn't of type array");
|
||||
return false;
|
||||
|
@ -98,7 +99,8 @@ class NewContent implements \Federator\Api\APIInterface
|
|||
if (isset($allHeaders['X-Sender'])) {
|
||||
$newActivity = $this->main->getConnector()->jsonToActivity($input);
|
||||
} else {
|
||||
$newActivity = \Federator\Data\ActivityPub\Factory::newActivityFromJson($input);
|
||||
error_log("NewContent::post No X-Sender header found");
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($newActivity === false) {
|
||||
|
@ -106,27 +108,26 @@ class NewContent implements \Federator\Api\APIInterface
|
|||
return false;
|
||||
}
|
||||
|
||||
$sendTo = $newActivity->getCC();
|
||||
if ($newActivity->getType() === 'Undo') {
|
||||
$object = $newActivity->getObject();
|
||||
if ($object !== null) {
|
||||
$sendTo = $object->getCC();
|
||||
}
|
||||
$dbh = $this->main->getDatabase();
|
||||
$cache = $this->main->getCache();
|
||||
$connector = $this->main->getConnector();
|
||||
|
||||
if (!isset($_user)) {
|
||||
$user = $newActivity->getAActor(); // url of the sender https://contentnation.net/username
|
||||
$user = str_replace(
|
||||
$domain,
|
||||
'',
|
||||
$user
|
||||
); // retrieve only the last part of the url
|
||||
} else {
|
||||
$user = $dbh->real_escape_string($_user);
|
||||
}
|
||||
|
||||
$users = [];
|
||||
|
||||
foreach ($sendTo as $receiver) {
|
||||
if ($receiver === '' || !is_string($receiver)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (str_ends_with($receiver, '/followers')) {
|
||||
$followers = $this->fetchAllFollowers($receiver, $host);
|
||||
if (is_array($followers)) {
|
||||
$users = array_merge($users, $followers);
|
||||
}
|
||||
}
|
||||
$followers = $this->fetchAllFollowers($dbh, $connector, $cache, $user);
|
||||
if (!empty($followers)) {
|
||||
$users = array_merge($users, $followers);
|
||||
}
|
||||
|
||||
if (empty($users)) { // todo remove after proper implementation, debugging for now
|
||||
|
@ -147,7 +148,7 @@ class NewContent implements \Federator\Api\APIInterface
|
|||
continue;
|
||||
}
|
||||
|
||||
$this->postForUser($user, $newActivity);
|
||||
$this->postForUser($dbh, $connector, $cache, $user, $newActivity);
|
||||
}
|
||||
|
||||
return json_encode($newActivity, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
|
||||
|
@ -156,28 +157,33 @@ class NewContent implements \Federator\Api\APIInterface
|
|||
/**
|
||||
* 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 $_user user that triggered the post
|
||||
* @param \Federator\Data\ActivityPub\Common\Activity $newActivity the activity that we received
|
||||
* @return boolean response
|
||||
*/
|
||||
private function postForUser($_user, $newActivity)
|
||||
private static function postForUser($dbh, $connector, $cache, $_user, $newActivity)
|
||||
{
|
||||
if (isset($_user)) {
|
||||
$dbh = $this->main->getDatabase();
|
||||
$cache = $this->main->getCache();
|
||||
$connector = $this->main->getConnector();
|
||||
if (!isset($_user)) {
|
||||
error_log("NewContent::postForUser no user given");
|
||||
return false;
|
||||
}
|
||||
|
||||
// get user
|
||||
$user = \Federator\DIO\User::getUserByName(
|
||||
$dbh,
|
||||
$_user,
|
||||
$connector,
|
||||
$cache
|
||||
);
|
||||
if ($user->id === null) {
|
||||
error_log("NewContent::postForUser couldn't find user: $_user");
|
||||
return false;
|
||||
}
|
||||
// get user
|
||||
$user = \Federator\DIO\User::getUserByName(
|
||||
$dbh,
|
||||
$_user,
|
||||
$connector,
|
||||
$cache
|
||||
);
|
||||
if ($user->id === null) {
|
||||
error_log("NewContent::postForUser couldn't find user: $_user");
|
||||
return false;
|
||||
}
|
||||
|
||||
$rootDir = $_SERVER['DOCUMENT_ROOT'] . '../';
|
||||
|
@ -194,58 +200,32 @@ class NewContent implements \Federator\Api\APIInterface
|
|||
/**
|
||||
* fetch all followers from url and return the ones that belong to our server
|
||||
*
|
||||
* @param string $collectionUrl The url of f.e. the posters followers
|
||||
* @param string $host our current host-url
|
||||
* @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 $userId The id of the user
|
||||
* @return string[] the names of the followers that are hosted on our server
|
||||
*/
|
||||
private static function fetchAllFollowers(string $collectionUrl, string $host): array
|
||||
private static function fetchAllFollowers($dbh, $connector, $cache, string $userId): array
|
||||
{
|
||||
if (empty($userId)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$users = [];
|
||||
|
||||
[$collectionResponse, $collectionInfo] = \Federator\Main::getFromRemote($collectionUrl, ['Accept: application/activity+json']);
|
||||
if ($collectionInfo['http_code'] != 200) {
|
||||
error_log("NewContent::fetchAllFollowers Failed to fetch follower collection metadata from $collectionUrl");
|
||||
return [];
|
||||
}
|
||||
$apFollowers = \Federator\DIO\Followers::getFollowersByUser(
|
||||
$dbh,
|
||||
$userId,
|
||||
$connector,
|
||||
cache: $cache,
|
||||
);
|
||||
|
||||
$collectionData = json_decode($collectionResponse, true);
|
||||
$nextPage = $collectionData['first'] ?? $collectionData['current'] ?? null;
|
||||
|
||||
if (!isset($nextPage)) {
|
||||
error_log("NewContent::fetchAllFollowers No 'first' or 'current' page in collection at $collectionUrl");
|
||||
return [];
|
||||
}
|
||||
|
||||
// Loop through all pages
|
||||
while ($nextPage) {
|
||||
[$pageResponse, $pageInfo] = \Federator\Main::getFromRemote($nextPage, ['Accept: application/activity+json']);
|
||||
if ($pageInfo['http_code'] != 200) {
|
||||
error_log("NewContent::fetchAllFollowers Failed to fetch follower page at $nextPage");
|
||||
break;
|
||||
}
|
||||
|
||||
$pageData = json_decode($pageResponse, true);
|
||||
$items = $pageData['orderedItems'] ?? $pageData['items'] ?? [];
|
||||
|
||||
foreach ($items as $followerUrl) {
|
||||
$parts = parse_url($followerUrl);
|
||||
if (!isset($parts['host']) || !str_ends_with($parts['host'], $host)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
[$actorResponse, $actorInfo] = \Federator\Main::getFromRemote($followerUrl, ['Accept: application/activity+json']);
|
||||
if ($actorInfo['http_code'] != 200) {
|
||||
error_log("NewContent::fetchAllFollowers Failed to fetch actor data for follower: $followerUrl");
|
||||
continue;
|
||||
}
|
||||
|
||||
$actorData = json_decode($actorResponse, true);
|
||||
if (isset($actorData['preferredUsername'])) {
|
||||
$users[] = $actorData['preferredUsername'];
|
||||
}
|
||||
}
|
||||
|
||||
$nextPage = $pageData['next'] ?? null;
|
||||
foreach ($apFollowers as $follower) {
|
||||
$users[] = $follower->id;
|
||||
}
|
||||
|
||||
return $users;
|
||||
|
|
|
@ -44,8 +44,10 @@ class WellKnown implements APIInterface
|
|||
*/
|
||||
private function hostMeta()
|
||||
{
|
||||
$config = $this->main->getConfig();
|
||||
$domain = $config['generic']['externaldomain'];
|
||||
$data = [
|
||||
'fqdn' => $_SERVER['SERVER_NAME']
|
||||
'fqdn' => $domain
|
||||
];
|
||||
$this->response = $this->main->renderTemplate('host-meta.xml', $data);
|
||||
return true;
|
||||
|
|
|
@ -45,8 +45,10 @@ class NodeInfo
|
|||
*/
|
||||
public function exec($paths)
|
||||
{
|
||||
$config = $this->main->getConfig();
|
||||
$domain = $config['generic']['externaldomain'];
|
||||
$data = [
|
||||
'fqdn' => $_SERVER['SERVER_NAME']
|
||||
'fqdn' => $domain
|
||||
];
|
||||
$template = null;
|
||||
if (sizeof($paths) == 2 && $paths[0] === '.well-known' && $paths[1] === 'nodeinfo') {
|
||||
|
|
19
php/federator/cache/cache.php
vendored
19
php/federator/cache/cache.php
vendored
|
@ -17,7 +17,7 @@ interface Cache extends \Federator\Connector\Connector
|
|||
* save remote followers of user
|
||||
*
|
||||
* @param string $user user name
|
||||
* @param \Federator\Data\ActivityPub\Common\APObject[]|false $followers user followers
|
||||
* @param \Federator\Data\FedUser[]|false $followers user followers
|
||||
* @return void
|
||||
*/
|
||||
public function saveRemoteFollowersOfUser($user, $followers);
|
||||
|
@ -48,6 +48,15 @@ interface Cache extends \Federator\Connector\Connector
|
|||
*/
|
||||
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
|
||||
*
|
||||
|
@ -67,6 +76,14 @@ interface Cache extends \Federator\Connector\Connector
|
|||
*/
|
||||
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
|
||||
*
|
||||
|
|
|
@ -18,7 +18,7 @@ interface Connector
|
|||
*
|
||||
* @param string $id user id
|
||||
|
||||
* @return \Federator\Data\ActivityPub\Common\APObject[]|false
|
||||
* @return \Federator\Data\FedUser[]|false
|
||||
*/
|
||||
public function getRemoteFollowersOfUser($id);
|
||||
|
||||
|
|
|
@ -25,8 +25,6 @@ class Announce extends Activity
|
|||
{
|
||||
$return = parent::toObject();
|
||||
$return['type'] = 'Announce';
|
||||
// overwrite id from url
|
||||
$return['id'] = $this->getURL();
|
||||
return $return;
|
||||
}
|
||||
|
||||
|
|
|
@ -25,8 +25,6 @@ class Undo extends Activity
|
|||
{
|
||||
$return = parent::toObject();
|
||||
$return['type'] = 'Undo';
|
||||
// overwrite id from url
|
||||
$return['id'] = $this->getURL();
|
||||
return $return;
|
||||
}
|
||||
|
||||
|
|
18
php/federator/data/activitypub/common/accept.php
Normal file
18
php/federator/data/activitypub/common/accept.php
Normal file
|
@ -0,0 +1,18 @@
|
|||
<?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,6 +74,7 @@ class Activity extends APObject
|
|||
{
|
||||
if (array_key_exists('actor', $json)) {
|
||||
$this->actor = $json['actor'];
|
||||
$this->aactor = $json['actor'];
|
||||
unset($json['actor']);
|
||||
}
|
||||
if (!parent::fromJson($json)) {
|
||||
|
@ -102,7 +103,7 @@ class Activity extends APObject
|
|||
/**
|
||||
* get Child Object
|
||||
*
|
||||
* @return APObject|null
|
||||
* @return APObject|string|null
|
||||
*/
|
||||
public function getObject()
|
||||
{
|
||||
|
|
|
@ -27,7 +27,7 @@ class APObject implements \JsonSerializable
|
|||
/**
|
||||
* child object
|
||||
*
|
||||
* @var APObject|null $object
|
||||
* @var APObject|string|null $object
|
||||
*/
|
||||
private $object = null;
|
||||
|
||||
|
@ -358,7 +358,7 @@ class APObject implements \JsonSerializable
|
|||
/**
|
||||
* get child object
|
||||
*
|
||||
* @return APObject|null child object
|
||||
* @return APObject|string|null child object
|
||||
*/
|
||||
public function getObject()
|
||||
{
|
||||
|
@ -750,8 +750,8 @@ class APObject implements \JsonSerializable
|
|||
if (array_key_exists('mediaType', $json)) {
|
||||
$this->mediaType = $json['mediaType'];
|
||||
}
|
||||
if (array_key_exists('object', $json)) {
|
||||
$this->object = \Federator\Data\ActivityPub\Factory::newFromJson($json['object'], "");
|
||||
if (array_key_exists('object', $json)) { // some actPub servers send strings in the object field
|
||||
$this->object = is_array($json['object']) ? \Federator\Data\ActivityPub\Factory::newFromJson($json['object'], "") : $json['object'];
|
||||
}
|
||||
if (array_key_exists('sensitive', $json)) {
|
||||
$this->sensitive = $json['sensitive'];
|
||||
|
@ -890,7 +890,7 @@ class APObject implements \JsonSerializable
|
|||
$return['mediaType'] = $this->mediaType;
|
||||
}
|
||||
if ($this->object !== null) {
|
||||
$return['object'] = $this->object->toObject();
|
||||
$return['object'] = is_string($this->object) ? $this->object : $this->object->toObject();
|
||||
}
|
||||
if ($this->atomURI !== '') {
|
||||
$return['atomUri'] = $this->atomURI;
|
||||
|
|
54
php/federator/data/activitypub/common/delete.php
Normal file
54
php/federator/data/activitypub/common/delete.php
Normal file
|
@ -0,0 +1,54 @@
|
|||
<?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
|
||||
{
|
||||
/**
|
||||
* object overwrite
|
||||
* @var string
|
||||
*/
|
||||
private $object = "";
|
||||
|
||||
public function setFObject(string $object): void
|
||||
{
|
||||
$this->object = $object;
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
50
php/federator/data/activitypub/common/follow.php
Normal file
50
php/federator/data/activitypub/common/follow.php
Normal file
|
@ -0,0 +1,50 @@
|
|||
<?php
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*
|
||||
* @author Yannis Vogel (vogeldevelopment)
|
||||
**/
|
||||
|
||||
namespace Federator\Data\ActivityPub\Common;
|
||||
|
||||
class Follow extends Activity
|
||||
{
|
||||
/**
|
||||
* object overwrite
|
||||
* @var string
|
||||
*/
|
||||
private $object = "";
|
||||
|
||||
public function setFObject(string $object): void
|
||||
{
|
||||
$this->object = $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;
|
||||
}
|
||||
}
|
18
php/federator/data/activitypub/common/reject.php
Normal file
18
php/federator/data/activitypub/common/reject.php
Normal file
|
@ -0,0 +1,18 @@
|
|||
<?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');
|
||||
}
|
||||
}
|
|
@ -31,6 +31,9 @@ class Factory
|
|||
}
|
||||
$return = null;
|
||||
switch ($json['type']) {
|
||||
case 'Announce':
|
||||
$return = new Common\Announce();
|
||||
break;
|
||||
case 'Article':
|
||||
$return = new Common\Article();
|
||||
break;
|
||||
|
@ -39,10 +42,10 @@ class Factory
|
|||
break;
|
||||
case 'Event':
|
||||
$return = new Common\Event();
|
||||
break;
|
||||
break;*/
|
||||
case 'Follow':
|
||||
$return = new Common\Follow();
|
||||
break;*/
|
||||
break;
|
||||
case 'Image':
|
||||
$return = new Common\Image();
|
||||
break;
|
||||
|
@ -83,21 +86,24 @@ class Factory
|
|||
}
|
||||
//$return = false;
|
||||
switch ($json['type']) {
|
||||
/* case 'Accept':
|
||||
case 'Accept':
|
||||
$return = new Common\Accept();
|
||||
break; */
|
||||
break;
|
||||
case 'Announce':
|
||||
$return = new Common\Announce();
|
||||
break;
|
||||
case 'Create':
|
||||
$return = new Common\Create();
|
||||
break;
|
||||
/*case 'Delete':
|
||||
case 'Delete':
|
||||
$return = new Common\Delete();
|
||||
break;
|
||||
case 'Follow':
|
||||
$return = new Common\Follow();
|
||||
break;*/
|
||||
break;
|
||||
case 'Reject':
|
||||
$return = new Common\Reject();
|
||||
break;
|
||||
case 'Undo':
|
||||
$return = new Common\Undo();
|
||||
break;
|
||||
|
|
151
php/federator/data/feduser.php
Normal file
151
php/federator/data/feduser.php
Normal file
|
@ -0,0 +1,151 @@
|
|||
<?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) ?: '';
|
||||
}
|
||||
}
|
242
php/federator/dio/feduser.php
Normal file
242
php/federator/dio/feduser.php
Normal file
|
@ -0,0 +1,242 @@
|
|||
<?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 addLocalUser($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();
|
||||
}
|
||||
$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();
|
||||
}
|
||||
$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();
|
||||
}
|
||||
$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();
|
||||
}
|
||||
$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::addLocalUser($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, followersurl,';
|
||||
$sql .= ' followingurl,publickeyid,outboxurl';
|
||||
$sql .= ' from fedusers where id=? and validuntil>=now()';
|
||||
$stmt = $dbh->prepare($sql);
|
||||
if ($stmt === false) {
|
||||
throw new \Federator\Exceptions\ServerError();
|
||||
}
|
||||
$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();
|
||||
}
|
||||
$r = json_decode($response, true);
|
||||
if ($r === false || $r === null || !is_array($r)) {
|
||||
throw new \Federator\Exceptions\ServerError();
|
||||
}
|
||||
// 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();
|
||||
}
|
||||
// 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();
|
||||
}
|
||||
$r = json_decode($response, true);
|
||||
if ($r === false || $r === null || !is_array($r)) {
|
||||
throw new \Federator\Exceptions\ServerError();
|
||||
}
|
||||
$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();
|
||||
}
|
||||
$user = \Federator\Data\FedUser::createFromJson($data);
|
||||
}
|
||||
}
|
||||
|
||||
if ($cache !== null && $user !== false) {
|
||||
if ($user->id === null && $user->actorURL !== null) {
|
||||
self::addLocalUser($dbh, $user, $_name);
|
||||
}
|
||||
$cache->saveRemoteFedUserByName($_name, $user);
|
||||
}
|
||||
if ($user === false) {
|
||||
throw new \Federator\Exceptions\ServerError();
|
||||
}
|
||||
return $user;
|
||||
}
|
||||
}
|
|
@ -13,11 +13,10 @@ namespace Federator\DIO;
|
|||
*/
|
||||
class Followers
|
||||
{
|
||||
|
||||
/**
|
||||
* get followers of user
|
||||
*
|
||||
* @param \mysqli $dbh @unused-param
|
||||
* @param \mysqli $dbh
|
||||
* database handle
|
||||
* @param string $id
|
||||
* user id
|
||||
|
@ -25,7 +24,7 @@ class Followers
|
|||
* connector to fetch use with
|
||||
* @param \Federator\Cache\Cache|null $cache
|
||||
* optional caching service
|
||||
* @return \Federator\Data\ActivityPub\Common\APObject[]
|
||||
* @return \Federator\Data\FedUser[]
|
||||
*/
|
||||
public static function getFollowersByUser($dbh, $id, $connector, $cache)
|
||||
{
|
||||
|
@ -37,7 +36,29 @@ class Followers
|
|||
}
|
||||
}
|
||||
$followers = [];
|
||||
// TODO: check our db
|
||||
$sql = 'select source_user from follows where target_user = ?';
|
||||
$stmt = $dbh->prepare($sql);
|
||||
if ($stmt === false) {
|
||||
throw new \Federator\Exceptions\ServerError();
|
||||
}
|
||||
$stmt->bind_param("s", $id);
|
||||
$stmt->execute();
|
||||
$followerIds = [];
|
||||
$stmt->bind_result($sourceUser);
|
||||
while ($stmt->fetch()) {
|
||||
$followerIds[] = $sourceUser;
|
||||
}
|
||||
$stmt->close();
|
||||
foreach ($followerIds as $followerId) {
|
||||
$user = \Federator\DIO\FedUser::getUserByName(
|
||||
$dbh,
|
||||
$followerId,
|
||||
$cache,
|
||||
);
|
||||
if ($user !== false && $user->id !== null) {
|
||||
$followers[] = $user;
|
||||
}
|
||||
}
|
||||
|
||||
if ($followers === []) {
|
||||
// ask connector for user-id
|
||||
|
@ -52,4 +73,263 @@ class Followers
|
|||
}
|
||||
return $followers;
|
||||
}
|
||||
/**
|
||||
* get followers of user
|
||||
*
|
||||
* @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();
|
||||
}
|
||||
$stmt->bind_param("s", $id);
|
||||
$stmt->execute();
|
||||
$followerIds = [];
|
||||
$stmt->bind_result($sourceUser);
|
||||
while ($stmt->fetch()) {
|
||||
$followerIds[] = $sourceUser;
|
||||
}
|
||||
foreach ($followerIds as $followerId) {
|
||||
$user = \Federator\DIO\User::getUserByName(
|
||||
$dbh,
|
||||
$followerId,
|
||||
$connector,
|
||||
$cache
|
||||
);
|
||||
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();
|
||||
}
|
||||
$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();
|
||||
}
|
||||
$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();
|
||||
}
|
||||
$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();
|
||||
}
|
||||
$stmt->bind_param("sss", $idurl, $sourceUser, $targetUserId);
|
||||
$stmt->execute();
|
||||
$stmt->close();
|
||||
return $idurl; // Return the generated follow ID
|
||||
}
|
||||
|
||||
/**
|
||||
* remove follow
|
||||
*
|
||||
* @param \mysqli $dbh database handle
|
||||
* @param string $sourceUser source user id
|
||||
* @param string $targetUserId target user id
|
||||
* @return bool true on success
|
||||
*/
|
||||
public static function removeFollow($dbh, $sourceUser, $targetUserId)
|
||||
{
|
||||
$sql = 'delete from follows where source_user = ? and target_user = ?';
|
||||
$stmt = $dbh->prepare($sql);
|
||||
if ($stmt === false) {
|
||||
throw new \Federator\Exceptions\ServerError();
|
||||
}
|
||||
$stmt->bind_param("ss", $sourceUser, $targetUserId);
|
||||
$stmt->execute();
|
||||
$affectedRows = $stmt->affected_rows;
|
||||
$stmt->close();
|
||||
return $affectedRows > 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -42,6 +42,7 @@ class User
|
|||
throw new \Federator\Exceptions\ServerError();
|
||||
}
|
||||
$public = openssl_pkey_get_details($private_key)['key'];
|
||||
$user->publicKey = $public;
|
||||
$private = '';
|
||||
openssl_pkey_export($private_key, $private);
|
||||
$sql = 'insert into users (id, externalid, rsapublic, rsaprivate, validuntil,';
|
||||
|
@ -100,13 +101,37 @@ 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();
|
||||
}
|
||||
$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
|
||||
* @param \mysqli $dbh database handle
|
||||
* @param \Federator\Data\User $user user to extend
|
||||
* @param string $_user user/profile name
|
||||
*/
|
||||
protected static function extendUser(\mysqli $dbh, $user, $_user) : void
|
||||
protected static function extendUser(\mysqli $dbh, $user, $_user): void
|
||||
{
|
||||
$sql = 'select id,unix_timestamp(`validuntil`) from users where id=?';
|
||||
$stmt = $dbh->prepare($sql);
|
||||
|
|
|
@ -280,6 +280,16 @@ class Main
|
|||
$this->host = $host;
|
||||
}
|
||||
|
||||
/**
|
||||
* set content type
|
||||
*
|
||||
* @param string $_type content type
|
||||
*/
|
||||
public function setContentType($_type): void
|
||||
{
|
||||
$this->contentType = $_type;
|
||||
}
|
||||
|
||||
/**
|
||||
* set response code
|
||||
*
|
||||
|
|
|
@ -54,11 +54,11 @@ class ContentNation implements Connector
|
|||
* get followers of given user
|
||||
*
|
||||
* @param string $userId user id
|
||||
* @return \Federator\Data\ActivityPub\Common\APObject[]|false
|
||||
* @return \Federator\Data\FedUser[]|false
|
||||
*/
|
||||
public function getRemoteFollowersOfUser($userId)
|
||||
{
|
||||
// todo implement queue for this, move to DIO
|
||||
// todo implement queue for this
|
||||
if (preg_match("#^([^@]+)@([^/]+)#", $userId, $matches) == 1) {
|
||||
$userId = $matches[1];
|
||||
}
|
||||
|
@ -109,25 +109,25 @@ class ContentNation implements Connector
|
|||
$posts = [];
|
||||
if (array_key_exists('activities', $r)) {
|
||||
$activities = $r['activities'];
|
||||
$host = $_SERVER['SERVER_NAME'];
|
||||
$config = $this->main->getConfig();
|
||||
$domain = $config['generic']['externaldomain'];
|
||||
$imgpath = $this->config['userdata']['path'];
|
||||
$userdata = $this->config['userdata']['url'];
|
||||
foreach ($activities as $activity) {
|
||||
$create = new \Federator\Data\ActivityPub\Common\Create();
|
||||
$create->setAActor('https://' . $host . '/' . $userId);
|
||||
$create->setAActor('https://' . $domain . '/' . $userId);
|
||||
$create->setID($activity['id'])
|
||||
->setPublished($activity['timestamp'])
|
||||
->setPublished($activity['published'] ?? $activity['timestamp'])
|
||||
->addTo("https://www.w3.org/ns/activitystreams#Public")
|
||||
->addCC('https://' . $host . '/' . $userId . '/followers.json');
|
||||
->addCC('https://' . $domain . '/' . $userId . '/followers');
|
||||
|
||||
switch ($activity['type']) {
|
||||
case 'Article':
|
||||
$create->setURL('https://' . $host . '/' . $activity['language'] . '/' . $userId . '/'
|
||||
. $activity['name']);
|
||||
$create->setURL('https://' . $domain . '/' . $activity['name']);
|
||||
$apArticle = new \Federator\Data\ActivityPub\Common\Article();
|
||||
if (array_key_exists('tags', $activity)) {
|
||||
foreach ($activity['tags'] as $tag) {
|
||||
$href = 'https://' . $host . '/' . $activity['language']
|
||||
. '/search.htm?tagsearch=' . urlencode($tag);
|
||||
$href = 'https://' . $domain . '/search.htm?tagsearch=' . urlencode($tag);
|
||||
$tagObj = new \Federator\Data\ActivityPub\Common\Tag();
|
||||
$tagObj->setHref($href)
|
||||
->setName('#' . urlencode(str_replace(' ', '', $tag)))
|
||||
|
@ -137,7 +137,7 @@ class ContentNation implements Connector
|
|||
}
|
||||
$apArticle->setPublished($activity['published'])
|
||||
->setName($activity['title'])
|
||||
->setAttributedTo('https://' . $host . '/' . $activity['profilename'])
|
||||
->setAttributedTo('https://' . $domain . '/' . $activity['profilename'])
|
||||
->setContent(
|
||||
$activity['teaser'] ??
|
||||
$this->main->translate(
|
||||
|
@ -147,11 +147,10 @@ class ContentNation implements Connector
|
|||
)
|
||||
)
|
||||
->addTo("https://www.w3.org/ns/activitystreams#Public")
|
||||
->addCC('https://' . $host . '/' . $userId . '/followers.json');
|
||||
->addCC('https://' . $domain . '/' . $userId . '/followers.json');
|
||||
$articleimage = $activity['imagealt'] ??
|
||||
$this->main->translate($activity['language'], 'article', 'image');
|
||||
$idurl = 'https://' . $host . '/' . $activity['language']
|
||||
. '/' . $userId . '/' . $activity['name'];
|
||||
$idurl = 'https://' . $domain . '/' . $userId . '/' . $activity['name'];
|
||||
$apArticle->setID($idurl)
|
||||
->setURL($idurl);
|
||||
$image = $activity['image'] ?? $activity['profileimg'];
|
||||
|
@ -176,7 +175,7 @@ class ContentNation implements Connector
|
|||
$posts[] = $create;
|
||||
break; // Comment
|
||||
case 'Vote':
|
||||
$url = 'https://' . $host . '/' . $activity['articlelang'] . $userId . '/'
|
||||
$url = 'https://' . $domain . '/' . $userId . '/'
|
||||
. $activity['articlename'];
|
||||
$url .= '/vote/' . $activity['id'];
|
||||
$create->setURL($url);
|
||||
|
@ -204,8 +203,7 @@ class ContentNation implements Connector
|
|||
$actor = new \Federator\Data\ActivityPub\Common\APObject('Person');
|
||||
$actor->setName($activity['username']);
|
||||
$like->setActor($actor);
|
||||
$url = 'https://' . $host . '/' . $activity['articlelang']
|
||||
. '/' . $userId . '/' . $activity['articlename'];
|
||||
$url = 'https://' . $domain . '/' . $userId . '/' . $activity['articlename'];
|
||||
if ($activity['comment'] !== '') {
|
||||
$url .= '/comment/' . $activity['comment'];
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ class DummyConnector implements Connector
|
|||
* get followers of given user
|
||||
*
|
||||
* @param string $userId user id @unused-param
|
||||
* @return \Federator\Data\ActivityPub\Common\APObject[]|false
|
||||
* @return \Federator\Data\FedUser[]|false
|
||||
*/
|
||||
public function getRemoteFollowersOfUser($userId)
|
||||
{
|
||||
|
|
|
@ -90,7 +90,7 @@ class RedisCache implements Cache
|
|||
*
|
||||
* @param string $id user id @unused-param
|
||||
|
||||
* @return \Federator\Data\ActivityPub\Common\APObject[]|false
|
||||
* @return \Federator\Data\FedUser[]|false
|
||||
*/
|
||||
public function getRemoteFollowersOfUser($id)
|
||||
{
|
||||
|
@ -163,6 +163,26 @@ class RedisCache implements Cache
|
|||
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
|
||||
*
|
||||
|
@ -203,7 +223,7 @@ class RedisCache implements Cache
|
|||
* save remote followers by user
|
||||
*
|
||||
* @param string $user user name @unused-param
|
||||
* @param \Federator\Data\ActivityPub\Common\APObject[]|false $followers user followers @unused-param
|
||||
* @param \Federator\Data\FedUser[]|false $followers user followers @unused-param
|
||||
* @return void
|
||||
*/
|
||||
public function saveRemoteFollowersOfUser($user, $followers)
|
||||
|
@ -252,6 +272,20 @@ class RedisCache implements Cache
|
|||
$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);
|
||||
$serialized = $user->toJson();
|
||||
$this->redis->setEx($key, $this->userTTL, $serialized);
|
||||
}
|
||||
|
||||
/**
|
||||
* save remote user by given session
|
||||
*
|
||||
|
|
3
sql/2025-05-06.sql
Normal file
3
sql/2025-05-06.sql
Normal file
|
@ -0,0 +1,3 @@
|
|||
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";
|
|
@ -58,7 +58,7 @@
|
|||
{rdelim}
|
||||
{rdelim}
|
||||
],
|
||||
"id":"https://{$fqdn}/users/{$username}",
|
||||
"id":"https://{$fqdn}/{$username}",
|
||||
"type":"{$type}",
|
||||
"following":"https://{$fqdn}/users/{$username}/following",
|
||||
"followers":"https://{$fqdn}/users/{$username}/followers",
|
||||
|
@ -74,8 +74,8 @@
|
|||
"discoverable":true,
|
||||
"published":"{$registered}",
|
||||
"publicKey":{ldelim}
|
||||
"id":"https://{$fqdn}/users/{$username}#main-key",
|
||||
"owner":"https://{$fqdn}/users/{$username}",
|
||||
"id":"https://{$fqdn}/{$username}#main-key",
|
||||
"owner":"https://{$fqdn}/{$username}",
|
||||
"publicKeyPem":"{$publickey}"
|
||||
{rdelim},
|
||||
"tag":[],
|
||||
|
|
Loading…
Add table
Reference in a new issue