initial support for articles from CN

- fixed how To and CC field (recipients) are handled in general
- fixed posts in database
- improved some error exceptions and prevented early breaks through try-catch blocks
- we now support CN-articles on our newcontent endpoint, with create and update calls
This commit is contained in:
Yannis Vogel 2025-05-22 20:44:37 +02:00
parent ba88adcebd
commit 62cfd6ef0d
No known key found for this signature in database
12 changed files with 398 additions and 150 deletions

View file

@ -43,7 +43,7 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
/**
* handle post call
*
* @param string|null $_user user to add data to inbox
* @param string|null $_user user to add data to inbox @unused-param
* @return string|false response
*/
public function post($_user)
@ -59,6 +59,12 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
$activity = is_string($_rawInput) ? json_decode($_rawInput, true) : null;
$dbh = $this->main->getDatabase();
$cache = $this->main->getCache();
$connector = $this->main->getConnector();
$config = $this->main->getConfig();
if (!is_array($activity)) {
throw new \Federator\Exceptions\ServerError("Inbox::post Input wasn't of type array");
}
@ -68,33 +74,33 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
if ($inboxActivity === false) {
throw new \Federator\Exceptions\ServerError("Inbox::post couldn't create inboxActivity");
}
$user = $inboxActivity->getAActor(); // url of the sender https://contentnation.net/username
$username = basename((string) (parse_url($user, PHP_URL_PATH) ?? ''));
$domain = parse_url($user, PHP_URL_HOST);
$rootDir = PROJECT_ROOT . '/';
$users = [];
// Shared inbox
if (!isset($_user)) {
// Save the raw input and parsed JSON to a file for inspection
file_put_contents(
$rootDir . 'logs/inbox.log',
date('Y-m-d H:i:s') . ": ==== POST Inbox Activity ====\n" . json_encode($inboxActivity, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) . "\n\n",
FILE_APPEND
);
}
$receivers = array_merge($inboxActivity->getTo(), $inboxActivity->getCC());
$sendTo = $inboxActivity->getCC();
if ($inboxActivity->getType() === 'Undo') { // for undo the object holds the proper cc
// For Undo, the object may hold the proper to/cc
if ($inboxActivity->getType() === 'Undo') {
$object = $inboxActivity->getObject();
if ($object !== null && is_object($object)) {
$sendTo = $object->getCC();
$receivers = array_merge($object->getTo(), $object->getCC());
}
}
$users = [];
$dbh = $this->main->getDatabase();
$cache = $this->main->getCache();
$connector = $this->main->getConnector();
// Filter out the public address and keep only actual URLs
$receivers = array_filter($receivers, static function (mixed $receiver): bool {
return is_string($receiver)
&& $receiver !== 'https://www.w3.org/ns/activitystreams#Public'
&& (filter_var($receiver, FILTER_VALIDATE_URL) !== false);
});
foreach ($sendTo as $receiver) {
if (!in_array($username, $receivers, true)) {
$receivers[] = $username;
}
foreach ($receivers as $receiver) {
if ($receiver === '' || !is_string($receiver)) {
continue;
}
@ -110,25 +116,68 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
$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");
error_log("Inbox::post no username or domain found for recipient: $receiver");
continue;
}
try {
$followers = \Federator\DIO\Followers::getFollowersByFedUser($dbh, $connector, $cache, $username . '@' . $domain);
} catch (\Throwable $e) {
error_log("Inbox::post get followers for user: " . $username . '@' . $domain . ". Exception: " . $e->getMessage());
continue;
}
$followers = \Federator\DIO\Followers::getFollowersByFedUser($dbh, $connector, $cache, $username . '@' . $domain);
if (is_array($followers)) {
$users = array_merge($users, array_column($followers, 'id'));
}
} else {
$ourDomain = $config['generic']['externaldomain'];
// check if receiver is an actor url from our domain
if (!str_contains($receiver, $ourDomain)) {
continue;
}
$receiverName = basename((string) (parse_url($receiver, PHP_URL_PATH) ?? ''));
$domain = parse_url($receiver, PHP_URL_HOST);
if ($receiverName === null || $domain === null) {
error_log("Inbox::post no receiverName or domain found for receiver: " . $receiver);
continue;
}
$receiver = $receiverName . '@' . $domain;
try {
$user = \Federator\DIO\User::getUserByName(
$dbh,
$receiver,
$connector,
$cache
);
} catch (\Throwable $e) {
error_log("Inbox::post get user by name: " . $receiver . ". Exception: " . $e->getMessage());
continue;
}
if ($user === null || $user->id === null) {
error_log("Inbox::post couldn't find user: $receiver");
continue;
}
$users[] = $user->id;
}
}
if ($_user !== false && !in_array($_user, $users, true)) {
$users[] = $_user;
if (empty($users)) { // todo remove after proper implementation, debugging for now
$rootDir = PROJECT_ROOT . '/';
// Save the raw input and parsed JSON to a file for inspection
file_put_contents(
$rootDir . 'logs/inbox.log',
date('Y-m-d H:i:s') . ": ==== POST Inbox Activity ====\n" . json_encode($inboxActivity, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) . "\n\n",
FILE_APPEND
);
}
foreach ($users as $user) {
foreach ($users as $receiver) {
if (!isset($user)) {
continue;
}
$token = \Resque::enqueue('inbox', 'Federator\\Jobs\\InboxJob', [
'user' => $user,
'user' => $username . '@' . $domain,
'recipientId' => $receiver,
'activity' => $inboxActivity->toObject(),
]);
error_log("Inbox::post enqueued job for user: $user with token: $token");
@ -142,32 +191,46 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
* @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 string $_user user that triggered the post
* @param string $_recipientId recipient of the post
* @param \Federator\Data\ActivityPub\Common\Activity $inboxActivity the activity that we received
* @return boolean response
*/
public static function postForUser($dbh, $connector, $cache, $_user, $inboxActivity)
public static function postForUser($dbh, $connector, $cache, $_user, $_recipientId, $inboxActivity)
{
if (!isset($_user)) {
error_log("Inbox::postForUser no user given");
return false;
}
$user = \Federator\DIO\User::getUserByName(
// get sender
$user = \Federator\DIO\FedUser::getUserByName(
$dbh,
$_user,
$connector,
$cache
);
if ($user === null || $user->id === null) {
throw new \Federator\Exceptions\ServerError("Inbox::postForUser couldn't find user: $_user");
error_log("Inbox::postForUser couldn't find user: $_user");
return false;
}
// get recipient
$recipient = \Federator\DIO\User::getUserByName(
$dbh,
$_recipientId,
$connector,
$cache
);
if ($recipient === null || $recipient->id === null) {
error_log("Inbox::postForUser couldn't find user: $_recipientId");
return false;
}
$rootDir = PROJECT_ROOT . '/';
// Save the raw input and parsed JSON to a file for inspection
file_put_contents(
$rootDir . 'logs/inbox_' . $_user . '.log',
date('Y-m-d H:i:s') . ": ==== POST " . $_user . " Inbox Activity ====\n" . json_encode($inboxActivity, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) . "\n\n",
$rootDir . 'logs/inbox_' . $recipient->id . '.log',
date('Y-m-d H:i:s') . ": ==== POST " . $recipient->id . " Inbox Activity ====\n" . json_encode($inboxActivity, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) . "\n\n",
FILE_APPEND
);
@ -195,12 +258,15 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
$object = $inboxActivity->getObject();
if (is_string($object)) {
\Federator\DIO\Posts::deletePost($dbh, $object);
} elseif (is_object($object)) {
$objectId = $object->getID();
\Federator\DIO\Posts::deletePost($dbh, $objectId);
}
break;
case 'Undo':
$object = $inboxActivity->getObject();
if (is_object($object) && method_exists($object, 'getType')) {
if (is_object($object)) {
switch ($object->getType()) {
case 'Follow':
$success = false;

View file

@ -77,7 +77,6 @@ class NewContent implements \Federator\Api\APIInterface
public function post($_user)
{
$_rawInput = file_get_contents('php://input');
$allHeaders = getallheaders();
try {
$this->main->checkSignature($allHeaders);
@ -111,27 +110,94 @@ class NewContent implements \Federator\Api\APIInterface
error_log("NewContent::post couldn't create newActivity");
return false;
}
if (!isset($_user)) {
$user = $newActivity->getAActor(); // url of the sender https://contentnation.net/username
$user = str_replace(
$posterName = str_replace(
$domain,
'',
$user
); // retrieve only the last part of the url
} else {
$user = $dbh->real_escape_string($_user);
$posterName = $dbh->real_escape_string($_user);
}
$users = [];
if ($newActivity->getType() === 'Create') {
$followers = $this->fetchAllFollowers($dbh, $connector, $cache, $user);
}
if (!empty($followers)) {
$users = array_merge($users, $followers);
$receivers = array_merge($newActivity->getTo(), $newActivity->getCC());
// For Undo, the object may hold the proper to/cc
if ($newActivity->getType() === 'Undo') {
$object = $newActivity->getObject();
if ($object !== null && is_object($object)) {
$receivers = array_merge($object->getTo(), $object->getCC());
}
}
// Filter out the public address and keep only actual URLs
$receivers = array_filter($receivers, static function (mixed $receiver): bool {
return is_string($receiver)
&& $receiver !== 'https://www.w3.org/ns/activitystreams#Public'
&& (filter_var($receiver, FILTER_VALIDATE_URL) !== false);
});
if (!in_array($posterName, $receivers, true)) {
$receivers[] = $posterName;
}
foreach ($receivers as $receiver) {
if ($receiver === '' || !is_string($receiver)) {
continue;
}
if (str_ends_with($receiver, '/followers')) {
$actor = $newActivity->getAActor();
if ($actor === null || !is_string($actor)) {
error_log("NewContent::post no actor found");
continue;
}
if ($posterName === null) {
error_log("NewContent::post no username found");
continue;
}
try {
$followers = \Federator\DIO\Followers::getFollowersByUser($dbh, $posterName, $connector, $cache);
} catch (\Throwable $e) {
error_log("NewContent::post get followers for user: " . $posterName . ". Exception: " . $e->getMessage());
continue;
}
if (is_array($followers)) {
$users = array_merge($users, array_column($followers, 'id'));
}
} else {
// check if receiver is an actor url and not from our domain
if (str_contains($receiver, $domain)) {
continue;
}
$receiverName = basename((string) (parse_url($receiver, PHP_URL_PATH) ?? ''));
$domain = parse_url($receiver, PHP_URL_HOST);
if ($receiverName === null || $domain === null) {
error_log("NewContent::post no receiverName or domain found for receiver: " . $receiver);
continue;
}
$receiver = $receiverName . '@' . $domain;
try {
$user = \Federator\DIO\FedUser::getUserByName(
$dbh,
$receiver,
$cache
);
} catch (\Throwable $e) {
error_log("NewContent::post get user by name: " . $receiver . ". Exception: " . $e->getMessage());
continue;
}
if ($user === null || $user->id === null) {
error_log("NewContent::post couldn't find user: $receiver");
continue;
}
$users[] = $user->id;
}
}
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
@ -141,20 +207,18 @@ class NewContent implements \Federator\Api\APIInterface
FILE_APPEND
);
}
if ($_user !== false && !in_array($_user, $users, true)) {
$users[] = $_user;
}
foreach ($users as $user) {
if (!isset($user)) {
foreach ($users as $receiver) {
if (!isset($receiver)) {
continue;
}
$token = \Resque::enqueue('inbox', 'Federator\\Jobs\\NewContentJob', [
'user' => $user,
'user' => $posterName,
'recipientId' => $receiver,
'activity' => $newActivity->toObject(),
]);
error_log("Inbox::post enqueued job for user: $user with token: $token");
error_log("Inbox::post enqueued job for receiver: $receiver with token: $token");
}
return json_encode($newActivity, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
@ -170,17 +234,18 @@ class NewContent implements \Federator\Api\APIInterface
* @param \Federator\Cache\Cache|null $cache
* optional caching service
* @param string $_user user that triggered the post
* @param string $_recipientId recipient of the post
* @param \Federator\Data\ActivityPub\Common\Activity $newActivity the activity that we received
* @return boolean response
*/
public static function postForUser($dbh, $connector, $cache, $_user, $newActivity)
public static function postForUser($dbh, $connector, $cache, $_user, $_recipientId, $newActivity)
{
if (!isset($_user)) {
error_log("NewContent::postForUser no user given");
return false;
}
// get user
// get sender
$user = \Federator\DIO\User::getUserByName(
$dbh,
$_user,
@ -192,11 +257,22 @@ class NewContent implements \Federator\Api\APIInterface
return false;
}
// get recipient
$recipient = \Federator\DIO\FedUser::getUserByName(
$dbh,
$_recipientId,
$cache
);
if ($recipient === null || $recipient->id === null) {
error_log("NewContent::postForUser couldn't find user: $_recipientId");
return false;
}
$rootDir = PROJECT_ROOT . '/';
// Save the raw input and parsed JSON to a file for inspection
file_put_contents(
$rootDir . 'logs/newcontent_' . $_user . '.log',
date('Y-m-d H:i:s') . ": ==== POST " . $_user . " NewContent Activity ====\n" . json_encode($newActivity, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) . "\n\n",
$rootDir . 'logs/newcontent_' . $recipient->id . '.log',
date('Y-m-d H:i:s') . ": ==== POST " . $recipient->id . " NewContent Activity ====\n" . json_encode($newActivity, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) . "\n\n",
FILE_APPEND
);
@ -303,6 +379,7 @@ class NewContent implements \Federator\Api\APIInterface
break;
case 'Create':
case 'Update':
$object = $newActivity->getObject();
if (is_object($object)) {
switch ($object->getType()) {
@ -324,40 +401,6 @@ class NewContent implements \Federator\Api\APIInterface
return true;
}
/**
* fetch all followers from url and return the ones that belong to our server
*
* @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($dbh, $connector, $cache, string $userId): array
{
if (empty($userId)) {
return [];
}
$users = [];
$apFollowers = \Federator\DIO\Followers::getFollowersByUser(
$dbh,
$userId,
$connector,
cache: $cache,
);
foreach ($apFollowers as $follower) {
$users[] = $follower->id;
}
return $users;
}
/**
* get internal represenation as json string
* @return string json string or html

View file

@ -0,0 +1,45 @@
<?php
/**
* SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop
* SPDX-License-Identifier: GPL-3.0-or-later
*
* @author Yannis Vogel (vogeldevelopment)
**/
namespace Federator\Data\ActivityPub\Common;
class Update extends Activity
{
public function __construct()
{
parent::__construct('Update');
parent::addContext('https://www.w3.org/ns/activitystreams');
}
/**
* convert internal state to php array
*
* @return array<string,mixed>
*/
public function toObject()
{
$return = parent::toObject();
$return['type'] = 'Update';
// overwrite id from url
if ($this->getURL() !== '') {
$return['id'] = $this->getURL();
}
return $return;
}
/**
* create object from json
*
* @param array<string,mixed> $json input json
* @return bool true on success
*/
public function fromJson($json)
{
return parent::fromJson($json);
}
}

View file

@ -647,7 +647,7 @@ class APObject implements \JsonSerializable
if (array_key_exists('duration', $json)) {
try {
$this->duration = new \DateInterval($json['duration']);
} catch (\Exception $unused_e) {
} catch (\Throwable $unused_e) {
error_log("error parsing duration ". $json['duration']);
}
}
@ -875,7 +875,7 @@ class APObject implements \JsonSerializable
$return['tag'] = $tags;
}
if ($this->updated > 0) {
$return['updated'] = gmdate("Y-m-d\TH:i:S\Z", $this->updated);
$return['updated'] = gmdate("Y-m-d\TH:i:s\Z", $this->updated);
}
if ($this->url !== '') {
$return['url'] = $this->url;

View file

@ -116,6 +116,9 @@ class Factory
case 'Undo':
$return = new Common\Undo();
break;
case 'Update':
$return = new Common\Update();
break;
default:
error_log("newActivityFromJson unsupported type: " . print_r($json, true));
}

View file

@ -26,7 +26,7 @@ class FedUser
$sql = 'select unix_timestamp(`validuntil`) from fedusers where id=?';
$stmt = $dbh->prepare($sql);
if ($stmt === false) {
throw new \Federator\Exceptions\ServerError();
throw new \Federator\Exceptions\ServerError("FedUser::addLocalUser Failed to prepare statement");
}
$stmt->bind_param("s", $_user);
$validuntil = 0;
@ -42,7 +42,7 @@ class FedUser
$sql .= ' values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, now() + interval 1 day)';
$stmt = $dbh->prepare($sql);
if ($stmt === false) {
throw new \Federator\Exceptions\ServerError();
throw new \Federator\Exceptions\ServerError("FedUser::addLocalUser Failed to prepare create statement");
}
$stmt->bind_param(
"ssssssssssss",
@ -66,7 +66,7 @@ class FedUser
$sql .= ' where id=?';
$stmt = $dbh->prepare($sql);
if ($stmt === false) {
throw new \Federator\Exceptions\ServerError();
throw new \Federator\Exceptions\ServerError("FedUser::extendUser Failed to prepare update statement");
}
$stmt->bind_param(
"ssssssssssss",
@ -106,7 +106,7 @@ class FedUser
$sql = 'select id,unix_timestamp(`validuntil`) from fedusers where id=?';
$stmt = $dbh->prepare($sql);
if ($stmt === false) {
throw new \Federator\Exceptions\ServerError();
throw new \Federator\Exceptions\ServerError("FedUser::extendUser Failed to prepare statement");
}
$stmt->bind_param("s", $_user);
$validuntil = 0;
@ -147,12 +147,12 @@ class FedUser
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()';
$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();
throw new \Federator\Exceptions\ServerError("FedUser::getUserByName Failed to prepare statement");
}
$stmt->bind_param("s", $_name);
$user = new \Federator\Data\FedUser();
@ -184,11 +184,11 @@ class FedUser
$headers = ['Accept: application/activity+json'];
[$response, $info] = \Federator\Main::getFromRemote($remoteURL, $headers);
if ($info['http_code'] != 200) {
throw new \Federator\Exceptions\ServerError();
throw new \Federator\Exceptions\ServerError("FedUser::getUserByName Failed to fetch webfinger for " . $_name);
}
$r = json_decode($response, true);
if ($r === false || $r === null || !is_array($r)) {
throw new \Federator\Exceptions\ServerError();
throw new \Federator\Exceptions\ServerError("FedUser::getUserByName Failed to decode webfinger for " . $_name);
}
// get the webwinger user url and fetch the user
if (isset($r['links'])) {
@ -200,17 +200,17 @@ class FedUser
}
}
if (!isset($remoteURL)) {
throw new \Federator\Exceptions\ServerError();
throw new \Federator\Exceptions\ServerError("FedUser::getUserByName Failed to find self link in webfinger for " . $_name);
}
// fetch the user
$headers = ['Accept: application/activity+json'];
[$response, $info] = \Federator\Main::getFromRemote($remoteURL, $headers);
if ($info['http_code'] != 200) {
throw new \Federator\Exceptions\ServerError();
throw new \Federator\Exceptions\ServerError("FedUser::getUserByName Failed to fetch user from remoteUrl for " . $_name);
}
$r = json_decode($response, true);
if ($r === false || $r === null || !is_array($r)) {
throw new \Federator\Exceptions\ServerError();
throw new \Federator\Exceptions\ServerError("FedUser::getUserByName Failed to decode user for " . $_name);
}
$r['publicKeyId'] = $r['publicKey']['id'];
$r['publicKey'] = $r['publicKey']['publicKeyPem'];
@ -222,20 +222,20 @@ class FedUser
$r['actorURL'] = $remoteURL;
$data = json_encode($r);
if ($data === false) {
throw new \Federator\Exceptions\ServerError();
throw new \Federator\Exceptions\ServerError("FedUser::getUserByName Failed to encode userdata " . $_name);
}
$user = \Federator\Data\FedUser::createFromJson($data);
}
}
if ($cache !== null && $user !== false) {
if ($user->id === null && $user->actorURL !== null) {
if ($user->id !== null && $user->actorURL !== null) {
self::addLocalUser($dbh, $user, $_name);
}
$cache->saveRemoteFedUserByName($_name, $user);
}
if ($user === false) {
throw new \Federator\Exceptions\ServerError();
throw new \Federator\Exceptions\ServerError("FedUser::getUserByName User not found");
}
return $user;
}

View file

@ -39,7 +39,7 @@ class Followers
$sql = 'select source_user from follows where target_user = ?';
$stmt = $dbh->prepare($sql);
if ($stmt === false) {
throw new \Federator\Exceptions\ServerError();
throw new \Federator\Exceptions\ServerError("Followers::getFollowersByUser Failed to prepare statement");
}
$stmt->bind_param("s", $id);
$stmt->execute();
@ -50,11 +50,16 @@ class Followers
}
$stmt->close();
foreach ($followerIds as $followerId) {
$user = \Federator\DIO\FedUser::getUserByName(
$dbh,
$followerId,
$cache,
);
try {
$user = \Federator\DIO\FedUser::getUserByName(
$dbh,
$followerId,
$cache,
);
} catch (\Throwable $e) {
error_log("Followers::getFollowersByUser Exception: " . $e->getMessage());
continue; // Skip this user if an exception occurs
}
if ($user !== false && $user->id !== null) {
$followers[] = $user;
}
@ -67,7 +72,7 @@ class Followers
$followers = [];
}
}
// save posts to DB
// save followers to cache
if ($cache !== null) {
$cache->saveRemoteFollowersOfUser($id, $followers);
}
@ -100,7 +105,7 @@ class Followers
$sql = 'select target_user from follows where source_user = ?';
$stmt = $dbh->prepare($sql);
if ($stmt === false) {
throw new \Federator\Exceptions\ServerError();
throw new \Federator\Exceptions\ServerError("Followers::getFollowingForUser Failed to prepare statement");
}
$stmt->bind_param("s", $id);
$stmt->execute();
@ -111,11 +116,16 @@ class Followers
}
$stmt->close();
foreach ($followingIds as $followingId) {
$user = \Federator\DIO\FedUser::getUserByName(
$dbh,
$followingId,
$cache,
);
try {
$user = \Federator\DIO\FedUser::getUserByName(
$dbh,
$followingId,
$cache,
);
} catch (\Throwable $e) {
error_log("Followers::getFollowingForUser Exception: " . $e->getMessage());
continue; // Skip this user if an exception occurs
}
if ($user !== false && $user->id !== null) {
$following[] = $user;
}
@ -156,7 +166,7 @@ class Followers
$sql = 'select source_user from follows where target_user = ?';
$stmt = $dbh->prepare($sql);
if ($stmt === false) {
throw new \Federator\Exceptions\ServerError();
throw new \Federator\Exceptions\ServerError("Followers::getFollowersByFedUser Failed to prepare statement");
}
$stmt->bind_param("s", $id);
$stmt->execute();
@ -166,12 +176,17 @@ class Followers
$followerIds[] = $sourceUser;
}
foreach ($followerIds as $followerId) {
$user = \Federator\DIO\User::getUserByName(
$dbh,
$followerId,
$connector,
$cache
);
try {
$user = \Federator\DIO\User::getUserByName(
$dbh,
$followerId,
$connector,
$cache
);
} catch (\Throwable $e) {
error_log("Followers::getFollowersByFedUser Exception: " . $e->getMessage());
continue; // Skip this user if an exception occurs
}
if ($user !== false && $user->id !== null) {
$followers[] = $user;
}
@ -193,7 +208,7 @@ class Followers
public static function sendFollowRequest($dbh, $connector, $cache, $_user, $_targetUser, $host)
{
if ($dbh === false) {
throw new \Federator\Exceptions\ServerError();
throw new \Federator\Exceptions\ServerError("Followers::sendFollowRequest Failed to get database handle");
}
$user = \Federator\DIO\User::getUserByName(
$dbh,
@ -326,7 +341,7 @@ class Followers
$sql = 'select id from follows where source_user = ? and target_user = ?';
$stmt = $dbh->prepare($sql);
if ($stmt === false) {
throw new \Federator\Exceptions\ServerError();
throw new \Federator\Exceptions\ServerError("Followers::addFollow Failed to prepare statement");
}
$stmt->bind_param("ss", $sourceUser, $targetUserId);
$foundId = 0;
@ -349,7 +364,7 @@ class Followers
$sql = 'select id from follows where id = ?';
$stmt = $dbh->prepare($sql);
if ($stmt === false) {
throw new \Federator\Exceptions\ServerError();
throw new \Federator\Exceptions\ServerError("Followers::addFollow Failed to prepare id-check statement");
}
$stmt->bind_param("s", $idurl);
$foundId = 0;
@ -365,7 +380,7 @@ class Followers
$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();
throw new \Federator\Exceptions\ServerError("Followers::addFollow Failed to prepare insert statement");
}
$stmt->bind_param("sss", $idurl, $sourceUser, $targetUserId);
$stmt->execute();
@ -386,7 +401,7 @@ class Followers
$sql = 'delete from follows where source_user = ? and target_user = ?';
$stmt = $dbh->prepare($sql);
if ($stmt === false) {
throw new \Federator\Exceptions\ServerError();
throw new \Federator\Exceptions\ServerError("Followers::removeFollow Failed to prepare statement");
}
$stmt->bind_param("ss", $sourceUser, $targetUserId);
$stmt->execute();

View file

@ -43,7 +43,6 @@ class Posts
if ($posts === false) {
$posts = [];
}
echo "Found " . count($posts) . " posts in DB\n";
// Only override $min if we found posts in our DB
$remoteMin = $min;

View file

@ -26,7 +26,7 @@ class User
$sql = 'select unix_timestamp(`validuntil`) from users where id=?';
$stmt = $dbh->prepare($sql);
if ($stmt === false) {
throw new \Federator\Exceptions\ServerError();
throw new \Federator\Exceptions\ServerError("User::addLocalUser Failed to prepare statement");
}
$stmt->bind_param("s", $_user);
$validuntil = 0;
@ -50,7 +50,7 @@ class User
$sql .= ' values (?, ?, ?, ?, now() + interval 1 day, ?, ?, ?, ?, ?, ?, ?, ?)';
$stmt = $dbh->prepare($sql);
if ($stmt === false) {
throw new \Federator\Exceptions\ServerError();
throw new \Federator\Exceptions\ServerError("User::addLocalUser Failed to prepare create statement");
}
$registered = gmdate('Y-m-d H:i:s', $user->registered);
$stmt->bind_param(
@ -74,7 +74,7 @@ class User
$sql .= ' iconmediatype=?, iconurl=?, imagemediatype=?, imageurl=? where id=?';
$stmt = $dbh->prepare($sql);
if ($stmt === false) {
throw new \Federator\Exceptions\ServerError();
throw new \Federator\Exceptions\ServerError("User::addLocalUser Failed to prepare update statement");
}
$registered = gmdate('Y-m-d H:i:s', $user->registered);
$stmt->bind_param(
@ -110,7 +110,7 @@ class User
$sql = "select rsaprivate from users where id=?";
$stmt = $dbh->prepare($sql);
if ($stmt === false) {
throw new \Federator\Exceptions\ServerError();
throw new \Federator\Exceptions\ServerError("User::getrsaprivate Failed to prepare statement");
}
$stmt->bind_param("s", $_user);
$ret = $stmt->bind_result($rsaPrivateKey);
@ -136,7 +136,7 @@ class User
$sql = 'select id,unix_timestamp(`validuntil`) from users where id=?';
$stmt = $dbh->prepare($sql);
if ($stmt === false) {
throw new \Federator\Exceptions\ServerError();
throw new \Federator\Exceptions\ServerError("User::extendUser Failed to prepare statement");
}
$stmt->bind_param("s", $_user);
$validuntil = 0;
@ -183,7 +183,7 @@ class User
$sql .= 'iconmediatype,iconurl,imagemediatype,imageurl from users where id=? and validuntil>=now()';
$stmt = $dbh->prepare($sql);
if ($stmt === false) {
throw new \Federator\Exceptions\ServerError();
throw new \Federator\Exceptions\ServerError("User::getUserByName Failed to prepare statement");
}
$stmt->bind_param("s", $_name);
$user = new \Federator\Data\User();

View file

@ -54,6 +54,7 @@ class InboxJob extends \Federator\Api
{
error_log("InboxJob: Starting inbox job");
$user = $this->args['user'];
$recipientId = $this->args['recipientId'];
$activity = $this->args['activity'];
$inboxActivity = \Federator\Data\ActivityPub\Factory::newActivityFromJson($activity);
@ -63,7 +64,7 @@ class InboxJob extends \Federator\Api
return false;
}
\Federator\Api\FedUsers\Inbox::postForUser($this->dbh, $this->connector, $this->cache, $user, $inboxActivity);
\Federator\Api\FedUsers\Inbox::postForUser($this->dbh, $this->connector, $this->cache, $user, $recipientId, $inboxActivity);
return true;
}
}

View file

@ -54,6 +54,7 @@ class NewContentJob extends \Federator\Api
{
error_log("NewContentJob: Starting inbox job");
$user = $this->args['user'];
$recipientId = $this->args['recipientId'];
$activity = $this->args['activity'];
$activity = \Federator\Data\ActivityPub\Factory::newActivityFromJson($activity);
@ -63,7 +64,7 @@ class NewContentJob extends \Federator\Api
return false;
}
\Federator\Api\V1\NewContent::postForUser($this->dbh, $this->connector, $this->cache, $user, $activity);
\Federator\Api\V1\NewContent::postForUser($this->dbh, $this->connector, $this->cache, $user, $recipientId, $activity);
return true;
}
}

View file

@ -148,8 +148,8 @@ class ContentNation implements Connector
$create->setAActor('https://' . $domain . '/' . $userId);
$create->setID($activity['id'])
->setPublished($activity['published'] ?? $activity['timestamp'])
->addTo("https://www.w3.org/ns/activitystreams#Public")
->addCC('https://' . $domain . '/' . $userId . '/followers');
->addTo('https://' . $domain . '/' . $userId . '/followers')
->addCC("https://www.w3.org/ns/activitystreams#Public");
$create->setURL('https://' . $domain . '/' . $activity['profilename'] . '/' . $activity['name']);
$create->setID('https://' . $domain . '/' . $activity['profilename'] . '/' . $activity['id']);
$apArticle = new \Federator\Data\ActivityPub\Common\Article();
@ -203,8 +203,8 @@ class ContentNation implements Connector
$create->setAActor('https://' . $domain . '/' . $userId);
$create->setID($activity['id'])
->setPublished($activity['published'] ?? $activity['timestamp'])
->addTo("https://www.w3.org/ns/activitystreams#Public")
->addCC('https://' . $domain . '/' . $userId . '/followers');
->addTo('https://' . $domain . '/' . $userId . '/followers')
->addCC("https://www.w3.org/ns/activitystreams#Public");
$commentJson = $activity;
$commentJson['type'] = 'Note';
$commentJson['summary'] = $activity['subject'];
@ -219,7 +219,7 @@ class ContentNation implements Connector
$note->setID($commentJson['id']);
if (!isset($commentJson['parent']) || $commentJson['parent'] === null) {
$note->setInReplyTo('https://' . $domain . '/' . $activity['articleOwnerName'] . '/' . $activity['articleName']);
} elseif ($replyType === "comment") {
} else {
$note->setInReplyTo('https://' . $domain . '/' . $activity['articleOwnerName'] . '/' . $activity['articleName'] . "#" . $commentJson['parent']);
}
$url = 'https://' . $domain . '/' . $activity['articleOwnerName'] . '/' . $activity['articleName'] . '#' . $activity['id'];
@ -237,7 +237,7 @@ class ContentNation implements Connector
$like->setID($activity['id'])
->setPublished($activity['published'] ?? $activity['timestamp']);
// $like->addTo("https://www.w3.org/ns/activitystreams#Public")
// ->addCC('https://' . $domain . '/' . $userId . '/followers');
// ->addCC('https://' . $domain . '/' . $userId . '/followers');
$like->setSummary(
$this->main->translate(
$activity['articlelang'],
@ -392,6 +392,65 @@ class ContentNation implements Connector
// Handle specific fields based on the type
switch ($jsonData['type']) {
case 'article':
$articleName = $jsonData['object']['name'] ?? null;
$articleOwnerName = $jsonData['object']['ownerName'] ?? null;
// Set Create-level fields
$updatedOn = $jsonData['object']['modified'] ?? null;
$originalPublished = $jsonData['object']['published'] ?? null;
$update = $updatedOn !== $originalPublished;
$ap['published'] = $updatedOn ?? $originalPublished;
$ap['id'] = $ourUrl . "/" . $articleOwnerName . "/" . $articleName;
$ap['url'] = $ourUrl . "/" . $articleOwnerName . "/" . $articleName;
$ap['type'] = $update ? 'Update' : 'Create';
$ap['actor'] = $ourUrl . '/' . $actorName;
// Set Article-level fields
$ap['object'] = [
'type' => 'Article',
'id' => $ourUrl . "/" . $articleOwnerName . "/" . $articleName,
'name' => $jsonData['object']['name'] ?? null,
'published' => $originalPublished,
'summary' => $jsonData['object']['summary'] ?? null,
'content' => $jsonData['object']['content'] ?? null,
'attributedTo' => $ap['actor'],
'url' => $ap['url'],
'cc' => ['https://www.w3.org/ns/activitystreams#Public'],
];
if ($update) {
$ap['id'] .= '#update';
$ap['url'] .= '#update';
$ap['object']['updated'] = $updatedOn;
}
$ap['cc'] = ['https://www.w3.org/ns/activitystreams#Public'];
if (isset($jsonData['object']['tags'])) {
if (is_array($jsonData['object']['tags'])) {
foreach ($jsonData['object']['tags'] as $tag) {
$ap['object']['tags'][] = $tag;
}
} elseif (is_string($jsonData['object']['tags']) && $jsonData['object']['tags'] !== '') {
// If it's a single tag as a string, add it as a one-element array
$ap['object']['tags'][] = $jsonData['object']['tags'];
}
}
if (isset($jsonData['options'])) {
if (isset($jsonData['options']['informFollowers'])) {
if ($jsonData['options']['informFollowers'] === true) {
$ap['to'][] = $ourUrl . '/' . $actorName . '/followers';
$ap['object']['to'][] = $ourUrl . '/' . $actorName . '/followers';
}
}
}
$returnActivity = \Federator\Data\ActivityPub\Factory::newActivityFromJson($ap);
if ($returnActivity === false) {
error_log("ContentNation::jsonToActivity couldn't create article");
$returnActivity = new \Federator\Data\ActivityPub\Common\Activity('Create');
} else {
$returnActivity->setID($ap['id']);
$returnActivity->setURL($ap['url']);
}
break;
case 'comment':
$commentId = $jsonData['object']['id'] ?? null;
$articleName = $jsonData['object']['articleName'] ?? null;
@ -411,7 +470,7 @@ class ContentNation implements Connector
if (isset($jsonData['options'])) {
if (isset($jsonData['options']['informFollowers'])) {
if ($jsonData['options']['informFollowers'] === true) {
if ($actorName != $articleOwnerName) {
if ($actorName !== $articleOwnerName) {
$ap['to'][] = $ourUrl . '/' . $articleOwnerName;
}
$ap['to'][] = $ourUrl . '/' . $actorName . '/followers';
@ -427,8 +486,13 @@ class ContentNation implements Connector
error_log("ContentNation::jsonToActivity unknown inReplyTo type: {$replyType}");
}
$returnActivity = \Federator\Data\ActivityPub\Factory::newActivityFromJson($ap);
$returnActivity->setID($ap['id']);
$returnActivity->setURL($ap['url']);
if ($returnActivity === false) {
error_log("ContentNation::jsonToActivity couldn't create comment");
$returnActivity = new \Federator\Data\ActivityPub\Common\Activity('Create');
} else {
$returnActivity->setID($ap['id']);
$returnActivity->setURL($ap['url']);
}
break;
case 'vote':
@ -487,8 +551,19 @@ class ContentNation implements Connector
} */
$returnActivity = \Federator\Data\ActivityPub\Factory::newActivityFromJson($ap);
$returnActivity->setID($ap['id']);
$returnActivity->setURL($ap['url']);
if ($returnActivity === false) {
error_log("ContentNation::jsonToActivity couldn't create vote");
if ($ap['type'] === "Like") {
$returnActivity = new \Federator\Data\ActivityPub\Common\Like();
} elseif ($ap['type'] === "Dislike") {
$returnActivity = new \Federator\Data\ActivityPub\Common\Dislike();
} else {
$returnActivity = new \Federator\Data\ActivityPub\Common\Undo();
}
} else {
$returnActivity->setID($ap['id']);
$returnActivity->setURL($ap['url']);
}
break;
default: