initial rough support for sending follow to CN

we now send follows to CN to the api/profile/profileName/fedfollow endpoint, either with a post, or a delete.
- commit also contains files where line-endings suddenly changed to crlf (now it's back to lf).
- targetRequestType (post/delete) now depends on the activity
- we now also set the id to username when fetching a user from CN
This commit is contained in:
Yannis Vogel 2025-06-11 18:15:13 +02:00
parent 097f871ed6
commit 2ae81a3748
No known key found for this signature in database
3 changed files with 118 additions and 14 deletions

View file

@ -194,6 +194,8 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
}
}
$users = array_unique($users); // remove duplicates
if (empty($users)) { // todo remove after proper implementation, debugging for now
$rootDir = PROJECT_ROOT . '/';
// Save the raw input and parsed JSON to a file for inspection
@ -229,15 +231,7 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
}
}
try {
$articleId = \Federator\DIO\Posts::getOriginalArticleId($dbh, $inboxActivity);
if ($articleId !== null) {
$connector->sendActivity($user, $inboxActivity);
}
} catch (\Throwable $e) {
error_log("Inbox::postForUser Error sending activity to connector. Exception: " . $e->getMessage());
return false;
}
$connector->sendActivity($user, $inboxActivity);
return "success";
}
@ -334,7 +328,7 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
$cache
);
if ($recipient === null || $recipient->id === null) {
error_log("Inbox::postForUser couldn't find user: $_recipientId");
error_log("Inbox::postForUser couldn't find recipient: $_recipientId");
return false;
}

View file

@ -194,6 +194,9 @@ class NewContent implements \Federator\Api\APIInterface
$users[] = $user->id;
}
}
$users = array_unique($users); // remove duplicates
if (empty($users)) { // todo remove after proper implementation, debugging for now
$rootDir = PROJECT_ROOT . '/';
// Save the raw input and parsed JSON to a file for inspection

View file

@ -314,6 +314,7 @@ class ContentNation implements Connector
}
$user = new \Federator\Data\User();
$user->externalid = $_name;
$user->id = $_name;
$user->iconMediaType = $r['iconMediaType'];
$user->iconURL = $r['iconURL'];
$user->imageMediaType = $r['imageMediaType'];
@ -675,8 +676,9 @@ class ContentNation implements Connector
public function sendActivity($sender, $activity)
{
$targetUrl = $this->service;
$targetRequestType = 'post'; // Default request type
// Convert ActivityPub activity to ContentNation JSON format and retrieve target url
$jsonData = self::activityToJson($this->main->getDatabase(), $this->service, $activity, $targetUrl);
$jsonData = self::activityToJson($this->main->getDatabase(), $this->service, $activity, $targetUrl, $targetRequestType);
if ($jsonData === false) {
error_log("ContentNation::sendActivity failed to convert activity to JSON");
@ -702,7 +704,7 @@ class ContentNation implements Connector
$path = $parsed['path'];
// Build the signature string
$signatureString = "(request-target): post {$path}\n" .
$signatureString = "(request-target): $targetRequestType {$path}\n" .
"host: {$extHost}\n" .
"date: {$date}\n" .
"digest: {$digest}";
@ -740,7 +742,16 @@ class ContentNation implements Connector
];
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
switch ($targetRequestType) {
case 'post':
curl_setopt($ch, CURLOPT_POST, true);
break;
case 'delete':
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
break;
default:
throw new \Exception("ContentNation::sendActivity Unsupported target request type: $targetRequestType");
}
curl_setopt($ch, CURLOPT_POSTFIELDS, $json);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
$response = curl_exec($ch);
@ -765,11 +776,13 @@ class ContentNation implements Connector
* @param string $serviceUrl the service URL
* @param \Federator\Data\ActivityPub\Common\Activity $activity the activity
* @param string $targetUrl the target URL for the activity
* @param string $targetRequestType the target request type (e.g., 'post', 'delete', etc.)
* @return array<string, mixed>|false the json data or false on failure
*/
private static function activityToJson($dbh, $serviceUrl, \Federator\Data\ActivityPub\Common\Activity $activity, string &$targetUrl)
private function activityToJson($dbh, $serviceUrl, \Federator\Data\ActivityPub\Common\Activity $activity, string &$targetUrl, string &$targetRequestType)
{
$type = strtolower($activity->getType());
$targetRequestType = 'post'; // Default request type
switch ($type) {
case 'create':
case 'update':
@ -820,6 +833,50 @@ class ContentNation implements Connector
}
break;
case 'follow':
$profileUrl = $activity->getObject();
if (!is_string($profileUrl)) {
error_log("ContentNation::activityToJson Invalid profile URL: " . json_encode($profileUrl));
return false;
}
$receiverName = basename((string) (parse_url($profileUrl, PHP_URL_PATH) ?? ''));
$ourDomain = parse_url($profileUrl, PHP_URL_HOST);
if ($receiverName === "" || $ourDomain === "") {
error_log("ContentNation::activityToJson no profileName or domain found for object url: " . $profileUrl);
return false;
}
$receiver = $receiverName;
try {
$localUser = \Federator\DIO\User::getUserByName(
$dbh,
$receiver,
$this,
null
);
} catch (\Throwable $e) {
error_log("ContentNation::activityToJson get user by name: " . $receiver . ". Exception: " . $e->getMessage());
return false;
}
if ($localUser === null || $localUser->id === null) {
error_log("ContentNation::activityToJson couldn't find user: $receiver");
return false;
}
$targetUrl = $serviceUrl . '/api/profile/' . $localUser->id . '/fedfollow';
$type = 'follow';
$actor = $activity->getAActor();
$fedUser = \Federator\DIO\FedUser::getUserByName(
$dbh,
$actor,
null
);
$from = $fedUser->id;
return [
'type' => $type,
'id' => $activity->getID(),
'from' => $from,
'to' => $localUser->id,
];
case 'like':
case 'dislike':
$articleId = \Federator\DIO\Posts::getOriginalArticleId($dbh, $activity);
@ -859,6 +916,56 @@ class ContentNation implements Connector
if (is_object($object)) {
$objType = strtolower($object->getType());
switch ($objType) {
case 'follow':
$profileUrl = $object->getObject();
if (!is_string($profileUrl)) {
error_log("ContentNation::activityToJson Invalid profile URL: " . json_encode($profileUrl));
return false;
}
$receiverName = basename((string) (parse_url($profileUrl, PHP_URL_PATH) ?? ''));
$ourDomain = parse_url($profileUrl, PHP_URL_HOST);
if ($receiverName === "" || $ourDomain === "") {
error_log("ContentNation::activityToJson no profileName or domain found for object url: " . $profileUrl);
return false;
}
$receiver = $receiverName;
try {
$localUser = \Federator\DIO\User::getUserByName(
$dbh,
$receiver,
$this,
null
);
} catch (\Throwable $e) {
error_log("ContentNation::activityToJson get user by name: " . $receiver . ". Exception: " . $e->getMessage());
return false;
}
if ($localUser === null || $localUser->id === null) {
error_log("ContentNation::activityToJson couldn't find user: $receiver");
return false;
}
$targetUrl = $serviceUrl . '/api/profile/' . $localUser->id . '/fedfollow';
$type = 'follow';
if ($object instanceof \Federator\Data\ActivityPub\Common\Activity) {
$actor = $object->getAActor();
if ($actor !== '') {
$fedUser = \Federator\DIO\FedUser::getUserByName(
$dbh,
$actor,
null
);
$from = $fedUser->id;
$targetRequestType = 'delete';
return [
'type' => $type,
'id' => $object->getID(),
'from' => $from,
'to' => $localUser->id,
];
}
}
return false;
case 'like':
case 'dislike':
$articleId = \Federator\DIO\Posts::getOriginalArticleId($dbh, $activity);