diff --git a/README.md b/README.md
index 37a2927..51df547 100644
--- a/README.md
+++ b/README.md
@@ -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]
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.
diff --git a/htdocs/index.html b/htdocs/index.html
index 7347200..61ab824 100644
--- a/htdocs/index.html
+++ b/htdocs/index.html
@@ -17,7 +17,7 @@
+ placeholder="Enter target link" value="users/grumpydevelop/outbox?page=0">
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;
}
diff --git a/php/federator/api/fedusers.php b/php/federator/api/fedusers.php
index 7e530dd..a6da348 100644
--- a/php/federator/api/fedusers.php
+++ b/php/federator/api/fedusers.php
@@ -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;
diff --git a/php/federator/api/fedusers/inbox.php b/php/federator/api/fedusers/inbox.php
index 627cf30..970ad5e 100644
--- a/php/federator/api/fedusers/inbox.php
+++ b/php/federator/api/fedusers/inbox.php
@@ -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;
- }
}
diff --git a/php/federator/api/fedusers/outbox.php b/php/federator/api/fedusers/outbox.php
index e943dbb..92b1170 100644
--- a/php/federator/api/fedusers/outbox.php
+++ b/php/federator/api/fedusers/outbox.php
@@ -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);
}
/**
diff --git a/php/federator/api/v1/dummy.php b/php/federator/api/v1/dummy.php
index 934de18..2769bd6 100644
--- a/php/federator/api/v1/dummy.php
+++ b/php/federator/api/v1/dummy.php
@@ -20,12 +20,8 @@ class Dummy implements \Federator\Api\APIInterface
*/
private $main;
- /**
- * internal message to output
- *
- * @var string $message
- */
- private $message = '';
+ /** @var array $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";
}
}
diff --git a/php/federator/api/v1/newcontent.php b/php/federator/api/v1/newcontent.php
index 450ef18..3a6b01b 100644
--- a/php/federator/api/v1/newcontent.php
+++ b/php/federator/api/v1/newcontent.php
@@ -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;
diff --git a/php/federator/api/wellknown.php b/php/federator/api/wellknown.php
index 6b1e76d..28e3746 100644
--- a/php/federator/api/wellknown.php
+++ b/php/federator/api/wellknown.php
@@ -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;
diff --git a/php/federator/api/wellknown/nodeinfo.php b/php/federator/api/wellknown/nodeinfo.php
index 0d5b251..250d0a3 100644
--- a/php/federator/api/wellknown/nodeinfo.php
+++ b/php/federator/api/wellknown/nodeinfo.php
@@ -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') {
diff --git a/php/federator/cache/cache.php b/php/federator/cache/cache.php
index 5f3878d..3742b8c 100644
--- a/php/federator/cache/cache.php
+++ b/php/federator/cache/cache.php
@@ -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
*
diff --git a/php/federator/connector/connector.php b/php/federator/connector/connector.php
index 3a49c5a..02ba83d 100644
--- a/php/federator/connector/connector.php
+++ b/php/federator/connector/connector.php
@@ -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);
diff --git a/php/federator/data/activitypub/common/Announce.php b/php/federator/data/activitypub/common/Announce.php
index d50a932..f3b5035 100644
--- a/php/federator/data/activitypub/common/Announce.php
+++ b/php/federator/data/activitypub/common/Announce.php
@@ -25,8 +25,6 @@ class Announce extends Activity
{
$return = parent::toObject();
$return['type'] = 'Announce';
- // overwrite id from url
- $return['id'] = $this->getURL();
return $return;
}
diff --git a/php/federator/data/activitypub/common/Undo.php b/php/federator/data/activitypub/common/Undo.php
index 28ec50b..5a42c49 100644
--- a/php/federator/data/activitypub/common/Undo.php
+++ b/php/federator/data/activitypub/common/Undo.php
@@ -25,8 +25,6 @@ class Undo extends Activity
{
$return = parent::toObject();
$return['type'] = 'Undo';
- // overwrite id from url
- $return['id'] = $this->getURL();
return $return;
}
diff --git a/php/federator/data/activitypub/common/accept.php b/php/federator/data/activitypub/common/accept.php
new file mode 100644
index 0000000..c0eef65
--- /dev/null
+++ b/php/federator/data/activitypub/common/accept.php
@@ -0,0 +1,18 @@
+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()
{
diff --git a/php/federator/data/activitypub/common/apobject.php b/php/federator/data/activitypub/common/apobject.php
index 62c2886..6d29cac 100644
--- a/php/federator/data/activitypub/common/apobject.php
+++ b/php/federator/data/activitypub/common/apobject.php
@@ -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;
diff --git a/php/federator/data/activitypub/common/delete.php b/php/federator/data/activitypub/common/delete.php
new file mode 100644
index 0000000..cdceb00
--- /dev/null
+++ b/php/federator/data/activitypub/common/delete.php
@@ -0,0 +1,54 @@
+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
+ */
+ public function toObject()
+ {
+ $return = parent::toObject();
+ if ($this->object !== "") {
+ $return['object'] = $this->object;
+ }
+ return $return;
+ }
+}
diff --git a/php/federator/data/activitypub/common/follow.php b/php/federator/data/activitypub/common/follow.php
new file mode 100644
index 0000000..cafcfcf
--- /dev/null
+++ b/php/federator/data/activitypub/common/follow.php
@@ -0,0 +1,50 @@
+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
+ */
+ public function toObject()
+ {
+ $return = parent::toObject();
+ if ($this->object !== "") {
+ $return['object'] = $this->object;
+ }
+ return $return;
+ }
+}
diff --git a/php/federator/data/activitypub/common/reject.php b/php/federator/data/activitypub/common/reject.php
new file mode 100644
index 0000000..66424f9
--- /dev/null
+++ b/php/federator/data/activitypub/common/reject.php
@@ -0,0 +1,18 @@
+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) ?: '';
+ }
+}
diff --git a/php/federator/dio/feduser.php b/php/federator/dio/feduser.php
new file mode 100644
index 0000000..65cd46b
--- /dev/null
+++ b/php/federator/dio/feduser.php
@@ -0,0 +1,242 @@
+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;
+ }
+}
diff --git a/php/federator/dio/followers.php b/php/federator/dio/followers.php
index 5972b1b..4d8a814 100644
--- a/php/federator/dio/followers.php
+++ b/php/federator/dio/followers.php
@@ -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;
+ }
}
diff --git a/php/federator/dio/user.php b/php/federator/dio/user.php
index ac936e7..845e8bd 100644
--- a/php/federator/dio/user.php
+++ b/php/federator/dio/user.php
@@ -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);
diff --git a/php/federator/main.php b/php/federator/main.php
index d588c30..f63f8ee 100644
--- a/php/federator/main.php
+++ b/php/federator/main.php
@@ -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
*
diff --git a/plugins/federator/contentnation.php b/plugins/federator/contentnation.php
index f0704af..fd9bf43 100644
--- a/plugins/federator/contentnation.php
+++ b/plugins/federator/contentnation.php
@@ -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'];
}
diff --git a/plugins/federator/dummyconnector.php b/plugins/federator/dummyconnector.php
index 2871f70..ba4b373 100644
--- a/plugins/federator/dummyconnector.php
+++ b/plugins/federator/dummyconnector.php
@@ -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)
{
diff --git a/plugins/federator/rediscache.php b/plugins/federator/rediscache.php
index 6768e2a..dc466a1 100644
--- a/plugins/federator/rediscache.php
+++ b/plugins/federator/rediscache.php
@@ -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
*
diff --git a/sql/2025-05-06.sql b/sql/2025-05-06.sql
new file mode 100644
index 0000000..6432661
--- /dev/null
+++ b/sql/2025-05-06.sql
@@ -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";
\ No newline at end of file
diff --git a/templates/federator/user.json b/templates/federator/user.json
index 752eb80..23d225e 100644
--- a/templates/federator/user.json
+++ b/templates/federator/user.json
@@ -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":[],