forked from grumpydevelop/federator
support external services to comment&like on CN
- integrate support to send new posts to CN - save original article-id in DB (needs db-migration) - votes and comments on CN-articles and comments are sent to CN, with proper signing and format - fixed minor issue where delete-activity was not properly working with objects - fixed minor issue where tombstone wasn't supported (which prevented being able to delete mastodon-posts from the db)
This commit is contained in:
parent
8891234617
commit
96bb1efe16
11 changed files with 503 additions and 118 deletions
|
@ -43,7 +43,7 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
|
||||||
/**
|
/**
|
||||||
* handle post call
|
* handle post call
|
||||||
*
|
*
|
||||||
* @param string|null $_user user to add data to inbox @unused-param
|
* @param string|null $_user user to add data to inbox
|
||||||
* @return string|false response
|
* @return string|false response
|
||||||
*/
|
*/
|
||||||
public function post($_user)
|
public function post($_user)
|
||||||
|
@ -107,6 +107,10 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
|
||||||
&& (filter_var($receiver, FILTER_VALIDATE_URL) !== false);
|
&& (filter_var($receiver, FILTER_VALIDATE_URL) !== false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (isset($_user)) {
|
||||||
|
$receivers[] = $dbh->real_escape_string($_user); // Add the target user to the receivers list
|
||||||
|
}
|
||||||
|
|
||||||
// Special handling for Follow and Undo follow activities
|
// Special handling for Follow and Undo follow activities
|
||||||
if (strtolower($inboxActivity->getType()) === 'follow') {
|
if (strtolower($inboxActivity->getType()) === 'follow') {
|
||||||
// For Follow, the object should hold the target
|
// For Follow, the object should hold the target
|
||||||
|
@ -126,6 +130,7 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
$ourDomain = $config['generic']['externaldomain'];
|
||||||
|
|
||||||
foreach ($receivers as $receiver) {
|
foreach ($receivers as $receiver) {
|
||||||
if ($receiver === '' || !is_string($receiver)) {
|
if ($receiver === '' || !is_string($receiver)) {
|
||||||
|
@ -157,18 +162,19 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
|
||||||
$users = array_merge($users, array_column($followers, 'id'));
|
$users = array_merge($users, array_column($followers, 'id'));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$ourDomain = $config['generic']['externaldomain'];
|
|
||||||
// check if receiver is an actor url from our domain
|
// check if receiver is an actor url from our domain
|
||||||
if (!str_contains($receiver, $ourDomain)) {
|
if (!str_contains($receiver, $ourDomain) && $receiver !== $_user) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$receiverName = basename((string) (parse_url($receiver, PHP_URL_PATH) ?? ''));
|
if ($receiver !== $_user) {
|
||||||
$ourDomain = parse_url($receiver, PHP_URL_HOST);
|
$receiverName = basename((string) (parse_url($receiver, PHP_URL_PATH) ?? ''));
|
||||||
if ($receiverName === null || $ourDomain === null) {
|
$ourDomain = parse_url($receiver, PHP_URL_HOST);
|
||||||
error_log("Inbox::post no receiverName or domain found for receiver: " . $receiver);
|
if ($receiverName === null || $ourDomain === null) {
|
||||||
continue;
|
error_log("Inbox::post no receiverName or domain found for receiver: " . $receiver);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$receiver = $receiverName;
|
||||||
}
|
}
|
||||||
$receiver = $receiverName . '@' . $ourDomain;
|
|
||||||
try {
|
try {
|
||||||
$localUser = \Federator\DIO\User::getUserByName(
|
$localUser = \Federator\DIO\User::getUserByName(
|
||||||
$dbh,
|
$dbh,
|
||||||
|
@ -209,6 +215,30 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
|
||||||
]);
|
]);
|
||||||
error_log("Inbox::post enqueued job for user: $user->id with token: $token");
|
error_log("Inbox::post enqueued job for user: $user->id with token: $token");
|
||||||
}
|
}
|
||||||
|
if (empty($users)) {
|
||||||
|
$type = strtolower($inboxActivity->getType());
|
||||||
|
if ($type === 'undo' || $type === 'delete') {
|
||||||
|
$token = \Resque::enqueue('inbox', 'Federator\\Jobs\\InboxJob', [
|
||||||
|
'user' => $user->id,
|
||||||
|
'recipientId' => "",
|
||||||
|
'activity' => $inboxActivity->toObject(),
|
||||||
|
]);
|
||||||
|
error_log("Inbox::post enqueued job for user: $user->id with token: $token");
|
||||||
|
} else {
|
||||||
|
error_log("Inbox::post no users found for activity, doing nothing: " . json_encode($inboxActivity, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
return "success";
|
return "success";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -241,6 +271,56 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$type = strtolower($inboxActivity->getType());
|
||||||
|
|
||||||
|
if ($_recipientId === '') {
|
||||||
|
if ($type === 'undo' || $type === 'delete') {
|
||||||
|
switch ($type) {
|
||||||
|
case 'delete':
|
||||||
|
// Delete Note/Post
|
||||||
|
$object = $inboxActivity->getObject();
|
||||||
|
if (is_string($object)) {
|
||||||
|
\Federator\DIO\Posts::deletePost($dbh, $object);
|
||||||
|
} elseif (is_object($object)) {
|
||||||
|
$objectId = $object->getID();
|
||||||
|
\Federator\DIO\Posts::deletePost($dbh, $objectId);
|
||||||
|
} else {
|
||||||
|
error_log("Inbox::postForUser Error in Delete Post for user $user->id, object is not a string or object");
|
||||||
|
error_log(" object of type " . gettype($object));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'undo':
|
||||||
|
$object = $inboxActivity->getObject();
|
||||||
|
if (is_object($object)) {
|
||||||
|
switch (strtolower($object->getType())) {
|
||||||
|
case 'like':
|
||||||
|
case 'dislike':
|
||||||
|
// Undo Like/Dislike (remove like/dislike)
|
||||||
|
$targetId = $object->getID();
|
||||||
|
// \Federator\DIO\Votes::removeVote($dbh, $user->id, $targetId, 'dislike');
|
||||||
|
\Federator\DIO\Posts::deletePost($dbh, $targetId);
|
||||||
|
break;
|
||||||
|
case 'note':
|
||||||
|
case 'article':
|
||||||
|
// Undo Note (remove note)
|
||||||
|
$noteId = $object->getID();
|
||||||
|
\Federator\DIO\Posts::deletePost($dbh, $noteId);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
error_log("Inbox::postForUser Unhandled activity type $type for user $user->id");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$atPos = strpos($_recipientId, '@');
|
$atPos = strpos($_recipientId, '@');
|
||||||
if ($atPos !== false) {
|
if ($atPos !== false) {
|
||||||
$_recipientId = substr($_recipientId, 0, $atPos);
|
$_recipientId = substr($_recipientId, 0, $atPos);
|
||||||
|
@ -266,18 +346,16 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
|
||||||
FILE_APPEND
|
FILE_APPEND
|
||||||
);
|
);
|
||||||
|
|
||||||
$type = $inboxActivity->getType();
|
|
||||||
|
|
||||||
switch ($type) {
|
switch ($type) {
|
||||||
case 'Follow':
|
case 'follow':
|
||||||
$success = \Federator\DIO\Followers::addExternalFollow($dbh, $inboxActivity->getID(), $user->id, $recipient->id);
|
$success = \Federator\DIO\Followers::addExternalFollow($dbh, $inboxActivity->getID(), $user->id, $recipient->id);
|
||||||
|
|
||||||
if ($success === false) {
|
if ($success === false) {
|
||||||
error_log("Inbox::postForUser: Failed to add follower for user $user->id");
|
error_log("Inbox::postForUser Failed to add follower for user $user->id");
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'Delete':
|
case 'delete':
|
||||||
// Delete Note/Post
|
// Delete Note/Post
|
||||||
$object = $inboxActivity->getObject();
|
$object = $inboxActivity->getObject();
|
||||||
if (is_string($object)) {
|
if (is_string($object)) {
|
||||||
|
@ -288,11 +366,11 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'Undo':
|
case 'undo':
|
||||||
$object = $inboxActivity->getObject();
|
$object = $inboxActivity->getObject();
|
||||||
if (is_object($object)) {
|
if (is_object($object)) {
|
||||||
switch ($object->getType()) {
|
switch (strtolower($object->getType())) {
|
||||||
case 'Follow':
|
case 'follow':
|
||||||
$success = false;
|
$success = false;
|
||||||
if ($object instanceof \Federator\Data\ActivityPub\Common\Activity) {
|
if ($object instanceof \Federator\Data\ActivityPub\Common\Activity) {
|
||||||
$actor = $object->getAActor();
|
$actor = $object->getAActor();
|
||||||
|
@ -301,75 +379,60 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ($success === false) {
|
if ($success === false) {
|
||||||
error_log("Inbox::postForUser: Failed to remove follower for user $user->id");
|
error_log("Inbox::postForUser Failed to remove follower for user $user->id");
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'Like':
|
case 'like':
|
||||||
// Undo Like (remove like)
|
case 'dislike':
|
||||||
if (method_exists($object, 'getObject')) {
|
// Undo Like/Dislike (remove like/dislike)
|
||||||
$targetId = $object->getObject();
|
$targetId = $object->getID();
|
||||||
if (is_string($targetId)) {
|
// \Federator\DIO\Votes::removeVote($dbh, $user->id, $targetId, 'dislike');
|
||||||
// \Federator\DIO\Votes::removeVote($dbh, $user->id, $targetId, 'like');
|
\Federator\DIO\Posts::deletePost($dbh, $targetId);
|
||||||
\Federator\DIO\Posts::deletePost($dbh, $targetId);
|
|
||||||
} else {
|
|
||||||
error_log("Inbox::postForUser: Error in Undo Like for user $user->id, targetId is not a string");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case 'Dislike':
|
case 'note':
|
||||||
// Undo Dislike (remove dislike)
|
|
||||||
if (method_exists($object, 'getObject')) {
|
|
||||||
$targetId = $object->getObject();
|
|
||||||
if (is_string($targetId)) {
|
|
||||||
// \Federator\DIO\Votes::removeVote($dbh, $user->id, $targetId, 'dislike');
|
|
||||||
\Federator\DIO\Posts::deletePost($dbh, $targetId);
|
|
||||||
} else {
|
|
||||||
error_log("Inbox::postForUser: Error in Undo Dislike for user $user->id, targetId is not a string");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'Note':
|
|
||||||
// Undo Note (remove note)
|
// Undo Note (remove note)
|
||||||
if (method_exists($object, 'getID')) {
|
$noteId = $object->getID();
|
||||||
$noteId = $object->getID();
|
\Federator\DIO\Posts::deletePost($dbh, $noteId);
|
||||||
\Federator\DIO\Posts::deletePost($dbh, $noteId);
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'Like':
|
case 'like':
|
||||||
// Add Like
|
case 'dislike':
|
||||||
$targetId = $inboxActivity->getObject();
|
// Add Like/Dislike
|
||||||
if (is_string($targetId)) {
|
|
||||||
// \Federator\DIO\Votes::addVote($dbh, $user->id, $targetId, 'like');
|
|
||||||
\Federator\DIO\Posts::savePost($dbh, $user->id, $inboxActivity);
|
|
||||||
} else {
|
|
||||||
error_log("Inbox::postForUser: Error in Add Like for user $user->id, targetId is not a string");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'Dislike':
|
|
||||||
// Add Dislike
|
|
||||||
$targetId = $inboxActivity->getObject();
|
$targetId = $inboxActivity->getObject();
|
||||||
if (is_string($targetId)) {
|
if (is_string($targetId)) {
|
||||||
// \Federator\DIO\Votes::addVote($dbh, $user->id, $targetId, 'dislike');
|
// \Federator\DIO\Votes::addVote($dbh, $user->id, $targetId, 'dislike');
|
||||||
\Federator\DIO\Posts::savePost($dbh, $user->id, $inboxActivity);
|
\Federator\DIO\Posts::savePost($dbh, $user->id, $inboxActivity);
|
||||||
} else {
|
} else {
|
||||||
error_log("Inbox::postForUser: Error in Add Dislike for user $user->id, targetId is not a string");
|
error_log("Inbox::postForUser Error in Add Like/Dislike for user $user->id, targetId is not a string");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'Note':
|
case 'create':
|
||||||
// Post Note
|
case 'update':
|
||||||
\Federator\DIO\Posts::savePost($dbh, $user->id, $inboxActivity);
|
$object = $inboxActivity->getObject();
|
||||||
|
if (is_object($object)) {
|
||||||
|
switch (strtolower($object->getType())) {
|
||||||
|
case 'note':
|
||||||
|
\Federator\DIO\Posts::savePost($dbh, $user->id, $inboxActivity);
|
||||||
|
|
||||||
|
break;
|
||||||
|
case 'article':
|
||||||
|
\Federator\DIO\Posts::savePost($dbh, $user->id, $inboxActivity);
|
||||||
|
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
\Federator\DIO\Posts::savePost($dbh, $user->id, $inboxActivity);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
error_log("Inbox::postForUser: Unhandled activity type $type for user $user->id");
|
error_log("Inbox::postForUser Unhandled activity type $type for user $user->id");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -100,8 +100,9 @@ class NewContent implements \Federator\Api\APIInterface
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$articleId = "";
|
||||||
if (isset($allHeaders['X-Sender'])) {
|
if (isset($allHeaders['X-Sender'])) {
|
||||||
$newActivity = $connector->jsonToActivity($input);
|
$newActivity = $connector->jsonToActivity($input, $articleId);
|
||||||
} else {
|
} else {
|
||||||
error_log("NewContent::post No X-Sender header found");
|
error_log("NewContent::post No X-Sender header found");
|
||||||
return false;
|
return false;
|
||||||
|
@ -212,6 +213,7 @@ class NewContent implements \Federator\Api\APIInterface
|
||||||
'user' => $posterName,
|
'user' => $posterName,
|
||||||
'recipientId' => $receiver,
|
'recipientId' => $receiver,
|
||||||
'activity' => $newActivity->toObject(),
|
'activity' => $newActivity->toObject(),
|
||||||
|
'articleId' => $articleId,
|
||||||
]);
|
]);
|
||||||
error_log("Inbox::post enqueued job for receiver: $receiver with token: $token");
|
error_log("Inbox::post enqueued job for receiver: $receiver with token: $token");
|
||||||
}
|
}
|
||||||
|
@ -232,9 +234,11 @@ class NewContent implements \Federator\Api\APIInterface
|
||||||
* @param string $_user user that triggered the post
|
* @param string $_user user that triggered the post
|
||||||
* @param string $_recipientId recipient of the post
|
* @param string $_recipientId recipient of the post
|
||||||
* @param \Federator\Data\ActivityPub\Common\Activity $newActivity the activity that we received
|
* @param \Federator\Data\ActivityPub\Common\Activity $newActivity the activity that we received
|
||||||
|
* @param string $articleId the original id of the article (if applicable)
|
||||||
|
* (used to identify the article in the remote system)
|
||||||
* @return boolean response
|
* @return boolean response
|
||||||
*/
|
*/
|
||||||
public static function postForUser($dbh, $connector, $cache, $host, $_user, $_recipientId, $newActivity)
|
public static function postForUser($dbh, $connector, $cache, $host, $_user, $_recipientId, $newActivity, $articleId)
|
||||||
{
|
{
|
||||||
if (!isset($_user)) {
|
if (!isset($_user)) {
|
||||||
error_log("NewContent::postForUser no user given");
|
error_log("NewContent::postForUser no user given");
|
||||||
|
@ -272,10 +276,10 @@ class NewContent implements \Federator\Api\APIInterface
|
||||||
FILE_APPEND
|
FILE_APPEND
|
||||||
);
|
);
|
||||||
|
|
||||||
$type = $newActivity->getType();
|
$type = strtolower($newActivity->getType());
|
||||||
|
|
||||||
switch ($type) {
|
switch ($type) {
|
||||||
case 'Follow':
|
case 'follow':
|
||||||
// $success = false;
|
// $success = false;
|
||||||
$actor = $newActivity->getAActor();
|
$actor = $newActivity->getAActor();
|
||||||
if ($actor !== '') {
|
if ($actor !== '') {
|
||||||
|
@ -293,7 +297,7 @@ class NewContent implements \Federator\Api\APIInterface
|
||||||
} */
|
} */
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'Delete':
|
case 'delete':
|
||||||
// Delete Note/Post
|
// Delete Note/Post
|
||||||
$object = $newActivity->getObject();
|
$object = $newActivity->getObject();
|
||||||
if (is_string($object)) {
|
if (is_string($object)) {
|
||||||
|
@ -304,11 +308,11 @@ class NewContent implements \Federator\Api\APIInterface
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'Undo':
|
case 'undo':
|
||||||
$object = $newActivity->getObject();
|
$object = $newActivity->getObject();
|
||||||
if (is_object($object)) {
|
if (is_object($object)) {
|
||||||
switch ($object->getType()) {
|
switch (strtolower($object->getType())) {
|
||||||
case 'Follow':
|
case 'follow':
|
||||||
$success = false;
|
$success = false;
|
||||||
if ($object instanceof \Federator\Data\ActivityPub\Common\Activity) {
|
if ($object instanceof \Federator\Data\ActivityPub\Common\Activity) {
|
||||||
$actor = $object->getAActor();
|
$actor = $object->getAActor();
|
||||||
|
@ -332,8 +336,8 @@ class NewContent implements \Federator\Api\APIInterface
|
||||||
error_log("NewContent::postForUser Failed to remove follower for user $user->id");
|
error_log("NewContent::postForUser Failed to remove follower for user $user->id");
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'Like':
|
case 'like':
|
||||||
case 'Dislike':
|
case 'dislike':
|
||||||
if (method_exists($object, 'getObject')) {
|
if (method_exists($object, 'getObject')) {
|
||||||
$targetId = $object->getObject();
|
$targetId = $object->getObject();
|
||||||
if (is_string($targetId)) {
|
if (is_string($targetId)) {
|
||||||
|
@ -344,13 +348,13 @@ class NewContent implements \Federator\Api\APIInterface
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'Note':
|
case 'note':
|
||||||
// Undo Note (remove note)
|
// Undo Note (remove note)
|
||||||
$noteId = $object->getID();
|
$noteId = $object->getID();
|
||||||
\Federator\DIO\Posts::deletePost($dbh, $noteId);
|
\Federator\DIO\Posts::deletePost($dbh, $noteId);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case 'Article':
|
case 'article':
|
||||||
$articleId = $object->getID();
|
$articleId = $object->getID();
|
||||||
\Federator\DIO\Posts::deletePost($dbh, $articleId);
|
\Federator\DIO\Posts::deletePost($dbh, $articleId);
|
||||||
// also remove latest saved article-update
|
// also remove latest saved article-update
|
||||||
|
@ -381,30 +385,30 @@ class NewContent implements \Federator\Api\APIInterface
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'Like':
|
case 'like':
|
||||||
case 'Dislike':
|
case 'dislike':
|
||||||
// Add Like/Dislike
|
// Add Like/Dislike
|
||||||
$targetId = $newActivity->getObject();
|
$targetId = $newActivity->getObject();
|
||||||
if (is_string($targetId)) {
|
if (is_string($targetId)) {
|
||||||
// \Federator\DIO\Votes::addVote($dbh, $user->id, $targetId, 'like');
|
// \Federator\DIO\Votes::addVote($dbh, $user->id, $targetId, 'like');
|
||||||
\Federator\DIO\Posts::savePost($dbh, $user->id, $newActivity);
|
\Federator\DIO\Posts::savePost($dbh, $user->id, $newActivity, $articleId);
|
||||||
} else {
|
} else {
|
||||||
error_log("NewContent::postForUser Error in Add Like/Dislike for recipient $recipient->id, targetId is not a string");
|
error_log("NewContent::postForUser Error in Add Like/Dislike for recipient $recipient->id, targetId is not a string");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'Create':
|
case 'create':
|
||||||
case 'Update':
|
case 'update':
|
||||||
$object = $newActivity->getObject();
|
$object = $newActivity->getObject();
|
||||||
if (is_object($object)) {
|
if (is_object($object)) {
|
||||||
switch ($object->getType()) {
|
switch (strtolower($object->getType())) {
|
||||||
case 'Note':
|
case 'note':
|
||||||
\Federator\DIO\Posts::savePost($dbh, $user->id, $newActivity);
|
\Federator\DIO\Posts::savePost($dbh, $user->id, $newActivity, $articleId);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case 'Article':
|
case 'article':
|
||||||
\Federator\DIO\Posts::savePost($dbh, $user->id, $newActivity);
|
\Federator\DIO\Posts::savePost($dbh, $user->id, $newActivity, $articleId);
|
||||||
|
|
||||||
$idPart = strrchr($recipient->id, '@');
|
$idPart = strrchr($recipient->id, '@');
|
||||||
if ($idPart === false) {
|
if ($idPart === false) {
|
||||||
|
@ -423,7 +427,7 @@ class NewContent implements \Federator\Api\APIInterface
|
||||||
|
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
\Federator\DIO\Posts::savePost($dbh, $user->id, $newActivity);
|
\Federator\DIO\Posts::savePost($dbh, $user->id, $newActivity, $articleId);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -531,7 +535,6 @@ class NewContent implements \Federator\Api\APIInterface
|
||||||
$response = curl_exec($ch);
|
$response = curl_exec($ch);
|
||||||
curl_close($ch);
|
curl_close($ch);
|
||||||
|
|
||||||
// Log the response for debugging if needed
|
|
||||||
if ($response === false) {
|
if ($response === false) {
|
||||||
throw new \Exception("Failed to send activity: " . curl_error($ch));
|
throw new \Exception("Failed to send activity: " . curl_error($ch));
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -70,9 +70,20 @@ interface Connector
|
||||||
* Convert jsonData to Activity format
|
* Convert jsonData to Activity format
|
||||||
*
|
*
|
||||||
* @param array<string, mixed> $jsonData the json data from our platfrom
|
* @param array<string, mixed> $jsonData the json data from our platfrom
|
||||||
|
* @param string $articleId the original id of the article (if applicable)
|
||||||
|
* (used to identify the article in the remote system)
|
||||||
* @return \Federator\Data\ActivityPub\Common\Activity|false
|
* @return \Federator\Data\ActivityPub\Common\Activity|false
|
||||||
*/
|
*/
|
||||||
public function jsonToActivity(array $jsonData);
|
public function jsonToActivity(array $jsonData, &$articleId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* send target-friendly json from ActivityPub activity
|
||||||
|
*
|
||||||
|
* @param \Federator\Data\FedUser $sender the user of the sender
|
||||||
|
* @param \Federator\Data\ActivityPub\Common\Activity $activity the activity
|
||||||
|
* @return boolean did we successfully send the activity?
|
||||||
|
*/
|
||||||
|
public function sendActivity($sender, $activity);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* check if the headers include a valid signature
|
* check if the headers include a valid signature
|
||||||
|
|
|
@ -10,17 +10,6 @@ namespace Federator\Data\ActivityPub\Common;
|
||||||
|
|
||||||
class Delete extends Activity
|
class Delete extends Activity
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* object overwrite
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
private $object = "";
|
|
||||||
|
|
||||||
public function setFObject(string $object): void
|
|
||||||
{
|
|
||||||
$this->object = $object;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
parent::__construct('Delete');
|
parent::__construct('Delete');
|
||||||
|
@ -33,10 +22,6 @@ class Delete extends Activity
|
||||||
*/
|
*/
|
||||||
public function fromJson($json): bool
|
public function fromJson($json): bool
|
||||||
{
|
{
|
||||||
if (array_key_exists('object', $json)) {
|
|
||||||
$this->object = $json['object'];
|
|
||||||
unset($json['object']);
|
|
||||||
}
|
|
||||||
return parent::fromJson($json);
|
return parent::fromJson($json);
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
|
@ -46,9 +31,6 @@ class Delete extends Activity
|
||||||
public function toObject()
|
public function toObject()
|
||||||
{
|
{
|
||||||
$return = parent::toObject();
|
$return = parent::toObject();
|
||||||
if ($this->object !== "") {
|
|
||||||
$return['object'] = $this->object;
|
|
||||||
}
|
|
||||||
return $return;
|
return $return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -67,6 +67,9 @@ class Factory
|
||||||
case 'Inbox':
|
case 'Inbox':
|
||||||
$return = new Common\Inbox();
|
$return = new Common\Inbox();
|
||||||
break;
|
break;
|
||||||
|
case 'Tombstone':
|
||||||
|
$return = new Common\APObject("Tombstone");
|
||||||
|
break;
|
||||||
/*case 'Question':
|
/*case 'Question':
|
||||||
$return = new Common\Question();
|
$return = new Common\Question();
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -216,13 +216,15 @@ class Posts
|
||||||
* @param \mysqli $dbh
|
* @param \mysqli $dbh
|
||||||
* @param string $userId
|
* @param string $userId
|
||||||
* @param \Federator\Data\ActivityPub\Common\Activity $post
|
* @param \Federator\Data\ActivityPub\Common\Activity $post
|
||||||
|
* @param string|null $articleId the original id of the article
|
||||||
|
* (used to identify the source article in the remote system)
|
||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
public static function savePost($dbh, $userId, $post)
|
public static function savePost($dbh, $userId, $post, $articleId = null)
|
||||||
{
|
{
|
||||||
$sql = 'INSERT INTO posts (
|
$sql = 'INSERT INTO posts (
|
||||||
`id`, `url`, `user_id`, `actor`, `type`, `object`, `to`, `cc`, `published`
|
`id`, `url`, `user_id`, `actor`, `type`, `object`, `to`, `cc`, `published`, `article_id`
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
ON DUPLICATE KEY UPDATE
|
ON DUPLICATE KEY UPDATE
|
||||||
`url` = VALUES(`url`),
|
`url` = VALUES(`url`),
|
||||||
`user_id` = VALUES(`user_id`),
|
`user_id` = VALUES(`user_id`),
|
||||||
|
@ -231,7 +233,8 @@ class Posts
|
||||||
`object` = VALUES(`object`),
|
`object` = VALUES(`object`),
|
||||||
`to` = VALUES(`to`),
|
`to` = VALUES(`to`),
|
||||||
`cc` = VALUES(`cc`),
|
`cc` = VALUES(`cc`),
|
||||||
`published` = VALUES(`published`)';
|
`published` = VALUES(`published`),
|
||||||
|
`article_id` = VALUES(`article_id`)';
|
||||||
$stmt = $dbh->prepare($sql);
|
$stmt = $dbh->prepare($sql);
|
||||||
if ($stmt === false) {
|
if ($stmt === false) {
|
||||||
throw new \Federator\Exceptions\ServerError();
|
throw new \Federator\Exceptions\ServerError();
|
||||||
|
@ -248,6 +251,9 @@ class Posts
|
||||||
if ($objectJson === false) {
|
if ($objectJson === false) {
|
||||||
$objectJson = null;
|
$objectJson = null;
|
||||||
}
|
}
|
||||||
|
if (is_object($object)) {
|
||||||
|
$id = $object->getID();
|
||||||
|
}
|
||||||
$to = $post->getTo();
|
$to = $post->getTo();
|
||||||
$cc = $post->getCC();
|
$cc = $post->getCC();
|
||||||
$toJson = is_array($to) ? json_encode($to) : (is_string($to) ? json_encode([$to]) : null);
|
$toJson = is_array($to) ? json_encode($to) : (is_string($to) ? json_encode([$to]) : null);
|
||||||
|
@ -256,7 +262,7 @@ class Posts
|
||||||
$publishedStr = $published ? gmdate('Y-m-d H:i:s', $published) : gmdate('Y-m-d H:i:s');
|
$publishedStr = $published ? gmdate('Y-m-d H:i:s', $published) : gmdate('Y-m-d H:i:s');
|
||||||
|
|
||||||
$stmt->bind_param(
|
$stmt->bind_param(
|
||||||
"sssssssss",
|
"ssssssssss",
|
||||||
$id,
|
$id,
|
||||||
$url,
|
$url,
|
||||||
$userId,
|
$userId,
|
||||||
|
@ -265,7 +271,8 @@ class Posts
|
||||||
$objectJson,
|
$objectJson,
|
||||||
$toJson,
|
$toJson,
|
||||||
$ccJson,
|
$ccJson,
|
||||||
$publishedStr
|
$publishedStr,
|
||||||
|
$articleId,
|
||||||
);
|
);
|
||||||
$result = $stmt->execute();
|
$result = $stmt->execute();
|
||||||
$stmt->close();
|
$stmt->close();
|
||||||
|
@ -292,4 +299,40 @@ class Posts
|
||||||
$stmt->close();
|
$stmt->close();
|
||||||
return $affectedRows > 0;
|
return $affectedRows > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** retrieve original article id of post
|
||||||
|
*
|
||||||
|
* @param \mysqli $dbh
|
||||||
|
* @param \Federator\Data\ActivityPub\Common\Activity $post
|
||||||
|
* @return string|null
|
||||||
|
*/
|
||||||
|
public static function getOriginalArticleId($dbh, $post)
|
||||||
|
{
|
||||||
|
$sql = 'SELECT `article_id` FROM posts WHERE id = ?';
|
||||||
|
$stmt = $dbh->prepare($sql);
|
||||||
|
if ($stmt === false) {
|
||||||
|
throw new \Federator\Exceptions\ServerError();
|
||||||
|
}
|
||||||
|
$id = $post->getID();
|
||||||
|
$object = $post->getObject();
|
||||||
|
if (is_object($object)) {
|
||||||
|
$inReplyTo = $object->getInReplyTo();
|
||||||
|
if ($inReplyTo !== "") {
|
||||||
|
$id = $inReplyTo; // Use inReplyTo as ID if it's a string
|
||||||
|
} else {
|
||||||
|
$id = $object->getObject();
|
||||||
|
}
|
||||||
|
} elseif (is_string($object)) {
|
||||||
|
$id = $object; // If object is a string, use it directly
|
||||||
|
}
|
||||||
|
$stmt->bind_param("s", $id);
|
||||||
|
$articleId = null;
|
||||||
|
$ret = $stmt->bind_result($articleId);
|
||||||
|
$stmt->execute();
|
||||||
|
if ($ret) {
|
||||||
|
$stmt->fetch();
|
||||||
|
}
|
||||||
|
$stmt->close();
|
||||||
|
return $articleId;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -56,6 +56,7 @@ class NewContentJob extends \Federator\Api
|
||||||
$user = $this->args['user'];
|
$user = $this->args['user'];
|
||||||
$recipientId = $this->args['recipientId'];
|
$recipientId = $this->args['recipientId'];
|
||||||
$activity = $this->args['activity'];
|
$activity = $this->args['activity'];
|
||||||
|
$articleId = $this->args['articleId'] ?? null;
|
||||||
|
|
||||||
$activity = \Federator\Data\ActivityPub\Factory::newActivityFromJson($activity);
|
$activity = \Federator\Data\ActivityPub\Factory::newActivityFromJson($activity);
|
||||||
|
|
||||||
|
@ -67,7 +68,7 @@ class NewContentJob extends \Federator\Api
|
||||||
|
|
||||||
$ourUrl = 'https://' . $domain;
|
$ourUrl = 'https://' . $domain;
|
||||||
|
|
||||||
\Federator\Api\V1\NewContent::postForUser($this->dbh, $this->connector, $this->cache, $ourUrl, $user, $recipientId, $activity);
|
\Federator\Api\V1\NewContent::postForUser($this->dbh, $this->connector, $this->cache, $ourUrl, $user, $recipientId, $activity, $articleId);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -368,9 +368,11 @@ class ContentNation implements Connector
|
||||||
* Convert jsonData to Activity format
|
* Convert jsonData to Activity format
|
||||||
*
|
*
|
||||||
* @param array<string, mixed> $jsonData the json data from our platfrom
|
* @param array<string, mixed> $jsonData the json data from our platfrom
|
||||||
|
* @param string $articleId the original id of the article (if applicable)
|
||||||
|
* (used to identify the article in the remote system)
|
||||||
* @return \Federator\Data\ActivityPub\Common\Activity|false
|
* @return \Federator\Data\ActivityPub\Common\Activity|false
|
||||||
*/
|
*/
|
||||||
public function jsonToActivity($jsonData)
|
public function jsonToActivity($jsonData, &$articleId)
|
||||||
{
|
{
|
||||||
$returnActivity = false;
|
$returnActivity = false;
|
||||||
// Common fields for all activity types
|
// Common fields for all activity types
|
||||||
|
@ -482,6 +484,7 @@ class ContentNation implements Connector
|
||||||
$returnActivity->setID($ap['id']);
|
$returnActivity->setID($ap['id']);
|
||||||
$returnActivity->setURL($ap['url']);
|
$returnActivity->setURL($ap['url']);
|
||||||
}
|
}
|
||||||
|
$articleId = $jsonData['object']['id']; // Set the article ID for the activity
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'comment':
|
case 'comment':
|
||||||
|
@ -514,6 +517,7 @@ class ContentNation implements Connector
|
||||||
$returnActivity->setID($ap['id']);
|
$returnActivity->setID($ap['id']);
|
||||||
$returnActivity->setURL($ap['url']);
|
$returnActivity->setURL($ap['url']);
|
||||||
}
|
}
|
||||||
|
$articleId = $jsonData['object']['articleId']; // Set the article ID for the activity
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'vote':
|
case 'vote':
|
||||||
|
@ -548,6 +552,7 @@ class ContentNation implements Connector
|
||||||
$returnActivity->setID($ap['id']);
|
$returnActivity->setID($ap['id']);
|
||||||
$returnActivity->setURL($ap['url']);
|
$returnActivity->setURL($ap['url']);
|
||||||
}
|
}
|
||||||
|
$articleId = $jsonData['object']['articleId']; // Set the article ID for the activity
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
@ -659,6 +664,250 @@ class ContentNation implements Connector
|
||||||
return $returnJson;
|
return $returnJson;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* send CN-friendly json from ActivityPub activity
|
||||||
|
*
|
||||||
|
* @param \Federator\Data\FedUser $sender the user of the sender
|
||||||
|
* @param \Federator\Data\ActivityPub\Common\Activity $activity the activity
|
||||||
|
* @return boolean did we successfully send the activity?
|
||||||
|
*/
|
||||||
|
public function sendActivity($sender, $activity)
|
||||||
|
{
|
||||||
|
$targetUrl = $this->service;
|
||||||
|
// Convert ActivityPub activity to ContentNation JSON format and retrieve target url
|
||||||
|
$jsonData = self::activityToJson($this->main->getDatabase(), $this->service, $activity, $targetUrl);
|
||||||
|
|
||||||
|
if ($jsonData === false) {
|
||||||
|
error_log("ContentNation::sendActivity failed to convert activity to JSON");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$json = json_encode($jsonData, JSON_UNESCAPED_SLASHES);
|
||||||
|
|
||||||
|
if ($json === false) {
|
||||||
|
throw new \Exception('Failed to encode JSON: ' . json_last_error_msg());
|
||||||
|
}
|
||||||
|
$digest = 'SHA-256=' . base64_encode(hash('sha256', $json, true));
|
||||||
|
$date = gmdate('D, d M Y H:i:s') . ' GMT';
|
||||||
|
$parsed = parse_url($targetUrl);
|
||||||
|
if ($parsed === false) {
|
||||||
|
throw new \Exception('Failed to parse URL: ' . $targetUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($parsed['host']) || !isset($parsed['path'])) {
|
||||||
|
throw new \Exception('Invalid target 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}";
|
||||||
|
|
||||||
|
$pKeyPath = PROJECT_ROOT . '/' . $this->main->getConfig()['keys']['federatorPrivateKeyPath'];
|
||||||
|
$privateKeyPem = file_get_contents($pKeyPath);
|
||||||
|
if ($privateKeyPem === false) {
|
||||||
|
http_response_code(500);
|
||||||
|
throw new \Federator\Exceptions\PermissionDenied("Private key couldn't be determined");
|
||||||
|
}
|
||||||
|
|
||||||
|
$pkeyId = openssl_pkey_get_private($privateKeyPem);
|
||||||
|
|
||||||
|
if ($pkeyId === false) {
|
||||||
|
throw new \Exception('Invalid private key');
|
||||||
|
}
|
||||||
|
|
||||||
|
openssl_sign($signatureString, $signature, $pkeyId, OPENSSL_ALGO_SHA256);
|
||||||
|
$signature_b64 = base64_encode($signature);
|
||||||
|
|
||||||
|
$signatureHeader = 'algorithm="rsa-sha256",headers="(request-target) host date digest",signature="' . $signature_b64 . '"';
|
||||||
|
|
||||||
|
$ch = curl_init($targetUrl);
|
||||||
|
if ($ch === false) {
|
||||||
|
throw new \Exception('Failed to initialize cURL');
|
||||||
|
}
|
||||||
|
$headers = [
|
||||||
|
'Host: ' . $extHost,
|
||||||
|
'Date: ' . $date,
|
||||||
|
'Digest: ' . $digest,
|
||||||
|
'Content-Type: application/json',
|
||||||
|
'Signature: ' . $signatureHeader,
|
||||||
|
'Accept: application/json',
|
||||||
|
'Username: ' . $sender->id,
|
||||||
|
];
|
||||||
|
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
curl_setopt($ch, CURLOPT_POST, true);
|
||||||
|
curl_setopt($ch, CURLOPT_POSTFIELDS, $json);
|
||||||
|
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
||||||
|
$response = curl_exec($ch);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
if ($response === false) {
|
||||||
|
throw new \Exception("Failed to send activity: " . curl_error($ch));
|
||||||
|
} else {
|
||||||
|
$httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
if ($httpcode != 200 && $httpcode != 202) {
|
||||||
|
throw new \Exception("Unexpected HTTP code $httpcode: $response");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert ActivityPub activity to ContentNation JSON format
|
||||||
|
*
|
||||||
|
* @param \mysqli $dbh database handle
|
||||||
|
* @param string $serviceUrl the service URL
|
||||||
|
* @param \Federator\Data\ActivityPub\Common\Activity $activity the activity
|
||||||
|
* @param string $targetUrl the target URL for the activity
|
||||||
|
* @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)
|
||||||
|
{
|
||||||
|
$type = strtolower($activity->getType());
|
||||||
|
switch ($type) {
|
||||||
|
case 'create':
|
||||||
|
case 'update':
|
||||||
|
$object = $activity->getObject();
|
||||||
|
if (is_object($object)) {
|
||||||
|
$objType = strtolower($object->getType());
|
||||||
|
$articleId = \Federator\DIO\Posts::getOriginalArticleId($dbh, $activity);
|
||||||
|
if ($articleId === null) {
|
||||||
|
error_log("ContentNation::activityToJson Failed to get original article ID for create/update activity");
|
||||||
|
}
|
||||||
|
switch ($objType) {
|
||||||
|
case 'article':
|
||||||
|
// We don't support article create/update at this point in time
|
||||||
|
error_log("ContentNation::activityToJson Unsupported create/update object type: {$objType}");
|
||||||
|
break;
|
||||||
|
case 'note':
|
||||||
|
$targetUrl = $serviceUrl . '/api/article/' . $articleId . '/comment';
|
||||||
|
$type = 'comment';
|
||||||
|
$inReplyTo = $object->getInReplyTo();
|
||||||
|
if ($inReplyTo !== '') {
|
||||||
|
$target = $inReplyTo;
|
||||||
|
} else {
|
||||||
|
$target = $object->getObject();
|
||||||
|
}
|
||||||
|
$comment = null;
|
||||||
|
if (is_string($target)) {
|
||||||
|
if (strpos($target, '#') !== false) {
|
||||||
|
$parts = explode('#', $target);
|
||||||
|
if (count($parts) > 0) {
|
||||||
|
$comment = $parts[count($parts) - 1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
error_log("ContentNation::activityToJson Unsupported target type for comment with id: " . $activity->getID() . " Type: " . gettype($target));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
'type' => $type,
|
||||||
|
'id' => $activity->getID(),
|
||||||
|
'parent' => $comment,
|
||||||
|
'subject' => $object->getSummary(),
|
||||||
|
'comment' => $object->getContent(),
|
||||||
|
];
|
||||||
|
default:
|
||||||
|
error_log("ContentNation::activityToJson Unsupported create/update object type: {$objType}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'like':
|
||||||
|
case 'dislike':
|
||||||
|
$articleId = \Federator\DIO\Posts::getOriginalArticleId($dbh, $activity);
|
||||||
|
if ($articleId === null) {
|
||||||
|
error_log("ContentNation::activityToJson Failed to get original article ID for vote activity");
|
||||||
|
}
|
||||||
|
$voteValue = $type === 'like' ? true : false;
|
||||||
|
$activityType = 'vote';
|
||||||
|
$inReplyTo = $activity->getInReplyTo();
|
||||||
|
if ($inReplyTo !== '') {
|
||||||
|
$target = $inReplyTo;
|
||||||
|
} else {
|
||||||
|
$target = $activity->getObject();
|
||||||
|
}
|
||||||
|
$comment = null;
|
||||||
|
if (is_string($target)) {
|
||||||
|
if (strpos($target, '#') !== false) {
|
||||||
|
$parts = explode('#', $target);
|
||||||
|
if (count($parts) > 0) {
|
||||||
|
$comment = $parts[count($parts) - 1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
error_log("ContentNation::activityToJson Unsupported target type for vote with id: " . $activity->getID() . " Type: " . gettype($target));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$targetUrl = $serviceUrl . '/api/article/' . $articleId . '/vote';
|
||||||
|
return [
|
||||||
|
'vote' => $voteValue,
|
||||||
|
'type' => $activityType,
|
||||||
|
'id' => $activity->getID(),
|
||||||
|
'comment' => $comment,
|
||||||
|
];
|
||||||
|
|
||||||
|
case 'undo':
|
||||||
|
$object = $activity->getObject();
|
||||||
|
if (is_object($object)) {
|
||||||
|
$objType = strtolower($object->getType());
|
||||||
|
switch ($objType) {
|
||||||
|
case 'like':
|
||||||
|
case 'dislike':
|
||||||
|
$articleId = \Federator\DIO\Posts::getOriginalArticleId($dbh, $activity);
|
||||||
|
if ($articleId === null) {
|
||||||
|
error_log("ContentNation::activityToJson Failed to get original article ID for undo vote activity");
|
||||||
|
}
|
||||||
|
$activityType = 'vote';
|
||||||
|
$inReplyTo = $object->getInReplyTo();
|
||||||
|
if ($inReplyTo !== '') {
|
||||||
|
$target = $inReplyTo;
|
||||||
|
} else {
|
||||||
|
$target = $object->getObject();
|
||||||
|
}
|
||||||
|
$comment = null;
|
||||||
|
if (is_string($target)) {
|
||||||
|
if (strpos($target, '#') !== false) {
|
||||||
|
$parts = explode('#', $target);
|
||||||
|
if (count($parts) > 0) {
|
||||||
|
$comment = $parts[count($parts) - 1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
error_log("ContentNation::activityToJson Unsupported target type for undo vote with id: " . $activity->getID() . " Type: " . gettype($target));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$targetUrl = $serviceUrl . '/api/article/' . $articleId . '/vote';
|
||||||
|
return [
|
||||||
|
'vote' => null,
|
||||||
|
'type' => $activityType,
|
||||||
|
'id' => $object->getID(),
|
||||||
|
'comment' => $comment,
|
||||||
|
];
|
||||||
|
case 'note':
|
||||||
|
// We don't support comment deletions at this point in time
|
||||||
|
error_log("ContentNation::activityToJson Unsupported undo object type: {$objType}");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
error_log("ContentNation::activityToJson Unsupported create/update object type: {$objType}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
error_log("ContentNation::activityToJson Unsupported activity type: {$type}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* check if the headers include a valid signature
|
* check if the headers include a valid signature
|
||||||
*
|
*
|
||||||
|
|
|
@ -73,9 +73,11 @@ class DummyConnector implements Connector
|
||||||
* Convert jsonData to Activity format
|
* Convert jsonData to Activity format
|
||||||
*
|
*
|
||||||
* @param array<string, mixed> $jsonData the json data from our platfrom @unused-param
|
* @param array<string, mixed> $jsonData the json data from our platfrom @unused-param
|
||||||
|
* @param string $articleId the original id of the article (if applicable)
|
||||||
|
* (used to identify the article in the remote system) @unused-param
|
||||||
* @return \Federator\Data\ActivityPub\Common\Activity|false
|
* @return \Federator\Data\ActivityPub\Common\Activity|false
|
||||||
*/
|
*/
|
||||||
public function jsonToActivity(array $jsonData) {
|
public function jsonToActivity(array $jsonData, &$articleId) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -108,6 +110,18 @@ class DummyConnector implements Connector
|
||||||
return $user;
|
return $user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* send target-friendly json from ActivityPub activity
|
||||||
|
*
|
||||||
|
* @param \Federator\Data\FedUser $sender the user of the sender @unused-param
|
||||||
|
* @param \Federator\Data\ActivityPub\Common\Activity $activity the activity @unused-param
|
||||||
|
* @return boolean did we successfully send the activity?
|
||||||
|
*/
|
||||||
|
public function sendActivity($sender, $activity)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* check if the headers include a valid signature
|
* check if the headers include a valid signature
|
||||||
*
|
*
|
||||||
|
|
|
@ -125,9 +125,11 @@ class RedisCache implements Cache
|
||||||
* Convert jsonData to Activity format
|
* Convert jsonData to Activity format
|
||||||
*
|
*
|
||||||
* @param array<string, mixed> $jsonData the json data from our platfrom @unused-param
|
* @param array<string, mixed> $jsonData the json data from our platfrom @unused-param
|
||||||
|
* @param string $articleId the original id of the article (if applicable)
|
||||||
|
* (used to identify the article in the remote system) @unused-param
|
||||||
* @return \Federator\Data\ActivityPub\Common\Activity|false
|
* @return \Federator\Data\ActivityPub\Common\Activity|false
|
||||||
*/
|
*/
|
||||||
public function jsonToActivity(array $jsonData)
|
public function jsonToActivity(array $jsonData, &$articleId)
|
||||||
{
|
{
|
||||||
error_log("rediscache::jsonToActivity not implemented");
|
error_log("rediscache::jsonToActivity not implemented");
|
||||||
return false;
|
return false;
|
||||||
|
@ -353,6 +355,18 @@ class RedisCache implements Cache
|
||||||
$this->redis->setEx($key, $this->publicKeyPemTTL, $publicKeyPem);
|
$this->redis->setEx($key, $this->publicKeyPemTTL, $publicKeyPem);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* send target-friendly json from ActivityPub activity
|
||||||
|
*
|
||||||
|
* @param \Federator\Data\FedUser $sender the user of the sender @unused-param
|
||||||
|
* @param \Federator\Data\ActivityPub\Common\Activity $activity the activity @unused-param
|
||||||
|
* @return boolean did we successfully send the activity?
|
||||||
|
*/
|
||||||
|
public function sendActivity($sender, $activity)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* check if the headers include a valid signature
|
* check if the headers include a valid signature
|
||||||
*
|
*
|
||||||
|
|
2
sql/2025-05-27.sql
Normal file
2
sql/2025-05-27.sql
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
alter table posts add `article_id` varchar(255) null default null comment 'The optional original article id (of non-federated system, e.g. CN)';
|
||||||
|
update settings set `value`="2025-05-27" where `key`="database_version";
|
Loading…
Add table
Reference in a new issue