forked from grumpydevelop/federator
integrate queue for NewContent
- also integrated better support for newContent types - Integrated saving posts to database (posts gotten via outbox request as well as posts received in the NewContent endpoint) - proper support for handling comments - support for likes/dislikes - support for requesting followers / following endpoints - better inbox support database needs changes, don't forget to run migration
This commit is contained in:
parent
767f51cc5b
commit
d355b5a7cd
25 changed files with 1363 additions and 160 deletions
|
@ -70,10 +70,10 @@ class FedUsers implements APIInterface
|
||||||
// /users/username/(inbox|outbox|following|followers)
|
// /users/username/(inbox|outbox|following|followers)
|
||||||
switch ($paths[2]) {
|
switch ($paths[2]) {
|
||||||
case 'following':
|
case 'following':
|
||||||
// $handler = new FedUsers\Following();
|
$handler = new FedUsers\Following($this->main);
|
||||||
break;
|
break;
|
||||||
case 'followers':
|
case 'followers':
|
||||||
// $handler = new FedUsers\Followers();
|
$handler = new FedUsers\Followers($this->main);
|
||||||
break;
|
break;
|
||||||
case 'inbox':
|
case 'inbox':
|
||||||
$handler = new FedUsers\Inbox($this->main);
|
$handler = new FedUsers\Inbox($this->main);
|
||||||
|
|
120
php/federator/api/fedusers/followers.php
Normal file
120
php/federator/api/fedusers/followers.php
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
<?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\Api\FedUsers;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* handle activitypub followers requests
|
||||||
|
*/
|
||||||
|
class Followers implements \Federator\Api\FedUsers\FedUsersInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* main instance
|
||||||
|
*
|
||||||
|
* @var \Federator\Api $main
|
||||||
|
*/
|
||||||
|
private $main;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* constructor
|
||||||
|
* @param \Federator\Api $main main instance
|
||||||
|
*/
|
||||||
|
public function __construct($main)
|
||||||
|
{
|
||||||
|
$this->main = $main;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* handle get call
|
||||||
|
*
|
||||||
|
* @param string|null $_user user to fetch followers for
|
||||||
|
* @return string|false response
|
||||||
|
*/
|
||||||
|
public function get($_user)
|
||||||
|
{
|
||||||
|
if (!isset($_user)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$dbh = $this->main->getDatabase();
|
||||||
|
$cache = $this->main->getCache();
|
||||||
|
$connector = $this->main->getConnector();
|
||||||
|
|
||||||
|
// get user
|
||||||
|
$user = \Federator\DIO\User::getUserByName(
|
||||||
|
$dbh,
|
||||||
|
$_user,
|
||||||
|
$connector,
|
||||||
|
$cache
|
||||||
|
);
|
||||||
|
if ($user->id === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$followers = new \Federator\Data\ActivityPub\Common\Followers();
|
||||||
|
$followerItems = \Federator\DIO\Followers::getFollowersByUser($dbh, $user->id, $connector, $cache);
|
||||||
|
|
||||||
|
$config = $this->main->getConfig();
|
||||||
|
$domain = $config['generic']['externaldomain'];
|
||||||
|
$baseUrl = 'https://' . $domain . '/' . $_user . '/followers';
|
||||||
|
|
||||||
|
$pageSize = 10;
|
||||||
|
$page = $this->main->extractFromURI("page", "");
|
||||||
|
$id = $baseUrl;
|
||||||
|
$items = [];
|
||||||
|
$totalItems = count($followerItems);
|
||||||
|
|
||||||
|
if ($page !== "") {
|
||||||
|
$pageNum = max(0, (int) $page);
|
||||||
|
$offset = (int)($pageNum * $pageSize);
|
||||||
|
$pagedItems = array_slice($followerItems, $offset, $pageSize);
|
||||||
|
|
||||||
|
foreach ($pagedItems as $follower) {
|
||||||
|
$items[] = $follower->actorURL;
|
||||||
|
}
|
||||||
|
$followers->setItems($items);
|
||||||
|
$id .= '?page=' . urlencode($page);
|
||||||
|
}
|
||||||
|
|
||||||
|
$followers->setID($id);
|
||||||
|
$followers->setPartOf($baseUrl);
|
||||||
|
$followers->setTotalItems($totalItems);
|
||||||
|
|
||||||
|
// Pagination navigation
|
||||||
|
$lastPage = max(0, ceil($totalItems / $pageSize) - 1);
|
||||||
|
|
||||||
|
if ($page === "" || $followers->count() == 0) {
|
||||||
|
$followers->setFirst($baseUrl . '?page=0');
|
||||||
|
$followers->setLast($baseUrl . '?page=' . $lastPage);
|
||||||
|
}
|
||||||
|
if ($page !== "") {
|
||||||
|
$pageNum = max(0, (int) $page);
|
||||||
|
if ($pageNum < $lastPage) {
|
||||||
|
$followers->setNext($baseUrl . '?page=' . ($pageNum + 1));
|
||||||
|
}
|
||||||
|
if ($pageNum > 0) {
|
||||||
|
$followers->setPrev($baseUrl . '?page=' . ($pageNum - 1));
|
||||||
|
}
|
||||||
|
$followers->setFirst($baseUrl . '?page=0');
|
||||||
|
$followers->setLast($baseUrl . '?page=' . $lastPage);
|
||||||
|
}
|
||||||
|
$obj = $followers->toObject();
|
||||||
|
|
||||||
|
return json_encode($obj, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* handle post call
|
||||||
|
*
|
||||||
|
* @param string|null $_user user to add data to outbox @unused-param
|
||||||
|
* @return string|false response
|
||||||
|
*/
|
||||||
|
public function post($_user)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
120
php/federator/api/fedusers/following.php
Normal file
120
php/federator/api/fedusers/following.php
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
<?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\Api\FedUsers;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* handle activitypub following requests
|
||||||
|
*/
|
||||||
|
class Following implements \Federator\Api\FedUsers\FedUsersInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* main instance
|
||||||
|
*
|
||||||
|
* @var \Federator\Api $main
|
||||||
|
*/
|
||||||
|
private $main;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* constructor
|
||||||
|
* @param \Federator\Api $main main instance
|
||||||
|
*/
|
||||||
|
public function __construct($main)
|
||||||
|
{
|
||||||
|
$this->main = $main;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* handle get call
|
||||||
|
*
|
||||||
|
* @param string|null $_user user to fetch followers for
|
||||||
|
* @return string|false response
|
||||||
|
*/
|
||||||
|
public function get($_user)
|
||||||
|
{
|
||||||
|
if (!isset($_user)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$dbh = $this->main->getDatabase();
|
||||||
|
$cache = $this->main->getCache();
|
||||||
|
$connector = $this->main->getConnector();
|
||||||
|
|
||||||
|
// get user
|
||||||
|
$user = \Federator\DIO\User::getUserByName(
|
||||||
|
$dbh,
|
||||||
|
$_user,
|
||||||
|
$connector,
|
||||||
|
$cache
|
||||||
|
);
|
||||||
|
if ($user->id === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$following = new \Federator\Data\ActivityPub\Common\Following();
|
||||||
|
$followingItems = \Federator\DIO\Followers::getFollowingForUser($dbh, $user->id, $connector, $cache);
|
||||||
|
|
||||||
|
$config = $this->main->getConfig();
|
||||||
|
$domain = $config['generic']['externaldomain'];
|
||||||
|
$baseUrl = 'https://' . $domain . '/users/' . $_user . '/following';
|
||||||
|
|
||||||
|
$pageSize = 10;
|
||||||
|
$page = $this->main->extractFromURI("page", "");
|
||||||
|
$id = $baseUrl;
|
||||||
|
$items = [];
|
||||||
|
$totalItems = count($followingItems);
|
||||||
|
|
||||||
|
if ($page !== "") {
|
||||||
|
$pageNum = max(0, (int) $page);
|
||||||
|
$offset = (int) ($pageNum * $pageSize);
|
||||||
|
$pagedItems = array_slice($followingItems, $offset, $pageSize);
|
||||||
|
|
||||||
|
foreach ($pagedItems as $followed) {
|
||||||
|
$items[] = $followed->actorURL;
|
||||||
|
}
|
||||||
|
$following->setItems($items);
|
||||||
|
$id .= '?page=' . urlencode($page);
|
||||||
|
}
|
||||||
|
|
||||||
|
$following->setID($id);
|
||||||
|
$following->setPartOf($baseUrl);
|
||||||
|
$following->setTotalItems($totalItems);
|
||||||
|
|
||||||
|
// Pagination navigation
|
||||||
|
$lastPage = max(0, ceil($totalItems / $pageSize) - 1);
|
||||||
|
|
||||||
|
if ($page === "" || $following->count() == 0) {
|
||||||
|
$following->setFirst($baseUrl . '?page=0');
|
||||||
|
$following->setLast($baseUrl . '?page=' . $lastPage);
|
||||||
|
}
|
||||||
|
if ($page !== "") {
|
||||||
|
$pageNum = max(0, (int) $page);
|
||||||
|
if ($pageNum < $lastPage) {
|
||||||
|
$following->setNext($baseUrl . '?page=' . ($pageNum + 1));
|
||||||
|
}
|
||||||
|
if ($pageNum > 0) {
|
||||||
|
$following->setPrev($baseUrl . '?page=' . ($pageNum - 1));
|
||||||
|
}
|
||||||
|
$following->setFirst($baseUrl . '?page=0');
|
||||||
|
$following->setLast($baseUrl . '?page=' . $lastPage);
|
||||||
|
}
|
||||||
|
$obj = $following->toObject();
|
||||||
|
|
||||||
|
return json_encode($obj, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* handle post call
|
||||||
|
*
|
||||||
|
* @param string|null $_user user to add data to outbox @unused-param
|
||||||
|
* @return string|false response
|
||||||
|
*/
|
||||||
|
public function post($_user)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
|
@ -148,22 +148,21 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
|
||||||
*/
|
*/
|
||||||
public static function postForUser($dbh, $connector, $cache, $_user, $inboxActivity)
|
public static function postForUser($dbh, $connector, $cache, $_user, $inboxActivity)
|
||||||
{
|
{
|
||||||
if (isset($_user)) {
|
if (!isset($_user)) {
|
||||||
// get user
|
error_log("Inbox::postForUser no user given");
|
||||||
$user = \Federator\DIO\User::getUserByName(
|
|
||||||
$dbh,
|
|
||||||
$_user,
|
|
||||||
$connector,
|
|
||||||
$cache
|
|
||||||
);
|
|
||||||
if ($user === null || $user->id === null) {
|
|
||||||
throw new \Federator\Exceptions\ServerError("Inbox::postForUser couldn't find user: $_user");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Not a local user, nothing to do
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$user = \Federator\DIO\User::getUserByName(
|
||||||
|
$dbh,
|
||||||
|
$_user,
|
||||||
|
$connector,
|
||||||
|
$cache
|
||||||
|
);
|
||||||
|
if ($user === null || $user->id === null) {
|
||||||
|
throw new \Federator\Exceptions\ServerError("Inbox::postForUser couldn't find user: $_user");
|
||||||
|
}
|
||||||
|
|
||||||
$rootDir = PROJECT_ROOT . '/';
|
$rootDir = PROJECT_ROOT . '/';
|
||||||
// Save the raw input and parsed JSON to a file for inspection
|
// Save the raw input and parsed JSON to a file for inspection
|
||||||
file_put_contents(
|
file_put_contents(
|
||||||
|
@ -174,40 +173,119 @@ class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
|
||||||
|
|
||||||
$type = $inboxActivity->getType();
|
$type = $inboxActivity->getType();
|
||||||
|
|
||||||
if ($type === 'Follow') {
|
switch ($type) {
|
||||||
// Someone wants to follow our user
|
case 'Follow':
|
||||||
$actor = $inboxActivity->getAActor(); // The follower's actor URI
|
$success = false;
|
||||||
if ($actor !== '') {
|
$actor = $inboxActivity->getAActor();
|
||||||
// Extract follower username (you may need to adjust this logic)
|
if ($actor !== '') {
|
||||||
$followerUsername = basename((string) (parse_url($actor, PHP_URL_PATH) ?? ''));
|
$followerUsername = basename((string) (parse_url($actor, PHP_URL_PATH) ?? ''));
|
||||||
$followerDomain = parse_url($actor, PHP_URL_HOST);
|
$followerDomain = parse_url($actor, PHP_URL_HOST);
|
||||||
if (is_string($followerDomain)) {
|
if (is_string($followerDomain)) {
|
||||||
$followerId = "{$followerUsername}@{$followerDomain}";
|
$followerId = "{$followerUsername}@{$followerDomain}";
|
||||||
|
$success = \Federator\DIO\Followers::addFollow($dbh, $followerId, $user->id, $followerDomain);
|
||||||
// Save the follow relationship
|
|
||||||
\Federator\DIO\Followers::addFollow($dbh, $followerId, $user->id, $followerDomain);
|
|
||||||
error_log("Inbox::postForUser: Added follower $followerId for user $user->id");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} elseif ($type === 'Undo') {
|
|
||||||
// Check if this is an Undo of a Follow (i.e., Unfollow)
|
|
||||||
$object = $inboxActivity->getObject();
|
|
||||||
if (is_object($object) && method_exists($object, 'getType') && $object->getType() === 'Follow') {
|
|
||||||
if ($object instanceof \Federator\Data\ActivityPub\Common\Activity) {
|
|
||||||
$actor = $object->getAActor();
|
|
||||||
if ($actor !== '') {
|
|
||||||
$followerUsername = basename((string) (parse_url($actor, PHP_URL_PATH) ?? ''));
|
|
||||||
$followerDomain = parse_url($actor, PHP_URL_HOST);
|
|
||||||
if (is_string($followerDomain)) {
|
|
||||||
$followerId = "{$followerUsername}@{$followerDomain}";
|
|
||||||
|
|
||||||
// Remove the follow relationship
|
|
||||||
\Federator\DIO\Followers::removeFollow($dbh, $followerId, $user->id);
|
|
||||||
error_log("Inbox::postForUser: Removed follower $followerId for user $user->id");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
if ($success === false) {
|
||||||
|
error_log("Inbox::postForUser: Failed to add follower for user $user->id");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'Delete':
|
||||||
|
// Delete Note/Post
|
||||||
|
$object = $inboxActivity->getObject();
|
||||||
|
if (is_string($object)) {
|
||||||
|
\Federator\DIO\Posts::deletePost($dbh, $object);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'Undo':
|
||||||
|
$object = $inboxActivity->getObject();
|
||||||
|
if (is_object($object) && method_exists($object, 'getType')) {
|
||||||
|
switch ($object->getType()) {
|
||||||
|
case 'Follow':
|
||||||
|
$success = false;
|
||||||
|
if ($object instanceof \Federator\Data\ActivityPub\Common\Activity) {
|
||||||
|
$actor = $object->getAActor();
|
||||||
|
if ($actor !== '') {
|
||||||
|
$followerUsername = basename((string) (parse_url($actor, PHP_URL_PATH) ?? ''));
|
||||||
|
$followerDomain = parse_url($actor, PHP_URL_HOST);
|
||||||
|
if (is_string($followerDomain)) {
|
||||||
|
$followerId = "{$followerUsername}@{$followerDomain}";
|
||||||
|
$success = \Federator\DIO\Followers::removeFollow($dbh, $followerId, $user->id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($success === false) {
|
||||||
|
error_log("Inbox::postForUser: Failed to remove follower for user $user->id");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'Like':
|
||||||
|
// Undo Like (remove like)
|
||||||
|
if (method_exists($object, 'getObject')) {
|
||||||
|
$targetId = $object->getObject();
|
||||||
|
if (is_string($targetId)) {
|
||||||
|
// \Federator\DIO\Votes::removeVote($dbh, $user->id, $targetId, 'like');
|
||||||
|
\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;
|
||||||
|
case 'Dislike':
|
||||||
|
// 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)
|
||||||
|
if (method_exists($object, 'getID')) {
|
||||||
|
$noteId = $object->getID();
|
||||||
|
\Federator\DIO\Posts::deletePost($dbh, $noteId);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'Like':
|
||||||
|
// Add Like
|
||||||
|
$targetId = $inboxActivity->getObject();
|
||||||
|
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();
|
||||||
|
if (is_string($targetId)) {
|
||||||
|
// \Federator\DIO\Votes::addVote($dbh, $user->id, $targetId, 'dislike');
|
||||||
|
\Federator\DIO\Posts::savePost($dbh, $user->id, $inboxActivity);
|
||||||
|
} else {
|
||||||
|
error_log("Inbox::postForUser: Error in Add Dislike for user $user->id, targetId is not a string");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'Note':
|
||||||
|
// Post Note
|
||||||
|
\Federator\DIO\Posts::savePost($dbh, $user->id, $inboxActivity);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
error_log("Inbox::postForUser: Unhandled activity type $type for user $user->id");
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|
|
@ -125,7 +125,9 @@ class NewContent implements \Federator\Api\APIInterface
|
||||||
|
|
||||||
$users = [];
|
$users = [];
|
||||||
|
|
||||||
$followers = $this->fetchAllFollowers($dbh, $connector, $cache, $user);
|
if ($newActivity->getType() === 'Create') {
|
||||||
|
$followers = $this->fetchAllFollowers($dbh, $connector, $cache, $user);
|
||||||
|
}
|
||||||
if (!empty($followers)) {
|
if (!empty($followers)) {
|
||||||
$users = array_merge($users, $followers);
|
$users = array_merge($users, $followers);
|
||||||
}
|
}
|
||||||
|
@ -148,7 +150,11 @@ class NewContent implements \Federator\Api\APIInterface
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->postForUser($dbh, $connector, $cache, $user, $newActivity);
|
$token = \Resque::enqueue('inbox', 'Federator\\Jobs\\NewContentJob', [
|
||||||
|
'user' => $user,
|
||||||
|
'activity' => $newActivity->toObject(),
|
||||||
|
]);
|
||||||
|
error_log("Inbox::post enqueued job for user: $user with token: $token");
|
||||||
}
|
}
|
||||||
|
|
||||||
return json_encode($newActivity, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
|
return json_encode($newActivity, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
|
||||||
|
@ -167,7 +173,7 @@ class NewContent implements \Federator\Api\APIInterface
|
||||||
* @param \Federator\Data\ActivityPub\Common\Activity $newActivity the activity that we received
|
* @param \Federator\Data\ActivityPub\Common\Activity $newActivity the activity that we received
|
||||||
* @return boolean response
|
* @return boolean response
|
||||||
*/
|
*/
|
||||||
private static function postForUser($dbh, $connector, $cache, $_user, $newActivity)
|
public static function postForUser($dbh, $connector, $cache, $_user, $newActivity)
|
||||||
{
|
{
|
||||||
if (!isset($_user)) {
|
if (!isset($_user)) {
|
||||||
error_log("NewContent::postForUser no user given");
|
error_log("NewContent::postForUser no user given");
|
||||||
|
@ -181,7 +187,7 @@ class NewContent implements \Federator\Api\APIInterface
|
||||||
$connector,
|
$connector,
|
||||||
$cache
|
$cache
|
||||||
);
|
);
|
||||||
if ($user->id === null) {
|
if ($user === null || $user->id === null) {
|
||||||
error_log("NewContent::postForUser couldn't find user: $_user");
|
error_log("NewContent::postForUser couldn't find user: $_user");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -194,6 +200,127 @@ class NewContent implements \Federator\Api\APIInterface
|
||||||
FILE_APPEND
|
FILE_APPEND
|
||||||
);
|
);
|
||||||
|
|
||||||
|
$type = $newActivity->getType();
|
||||||
|
|
||||||
|
switch ($type) {
|
||||||
|
case 'Follow':
|
||||||
|
$success = false;
|
||||||
|
$actor = $newActivity->getAActor();
|
||||||
|
if ($actor !== '') {
|
||||||
|
$followerUsername = basename((string) (parse_url($actor, PHP_URL_PATH) ?? ''));
|
||||||
|
$followerDomain = parse_url($actor, PHP_URL_HOST);
|
||||||
|
if (is_string($followerDomain)) {
|
||||||
|
$followerId = "{$followerUsername}@{$followerDomain}";
|
||||||
|
$success = \Federator\DIO\Followers::addFollow($dbh, $followerId, $user->id, $followerDomain);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($success === false) {
|
||||||
|
error_log("NewContent::postForUser: Failed to add follower for user $user->id");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'Delete':
|
||||||
|
// Delete Note/Post
|
||||||
|
$object = $newActivity->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 = $newActivity->getObject();
|
||||||
|
if (is_object($object)) {
|
||||||
|
switch ($object->getType()) {
|
||||||
|
case 'Follow':
|
||||||
|
$success = false;
|
||||||
|
if ($object instanceof \Federator\Data\ActivityPub\Common\Activity) {
|
||||||
|
$actor = $object->getAActor();
|
||||||
|
if ($actor !== '') {
|
||||||
|
$followerUsername = basename((string) (parse_url($actor, PHP_URL_PATH) ?? ''));
|
||||||
|
$followerDomain = parse_url($actor, PHP_URL_HOST);
|
||||||
|
if (is_string($followerDomain)) {
|
||||||
|
$followerId = "{$followerUsername}@{$followerDomain}";
|
||||||
|
$success = \Federator\DIO\Followers::removeFollow($dbh, $followerId, $user->id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($success === false) {
|
||||||
|
error_log("NewContent::postForUser: Failed to remove follower for user $user->id");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'Vote':
|
||||||
|
// Undo Vote (remove vote)
|
||||||
|
if (method_exists($object, 'getObject')) {
|
||||||
|
$targetId = $object->getObject();
|
||||||
|
if (is_string($targetId)) {
|
||||||
|
// \Federator\DIO\Votes::removeVote($dbh, $user->id, $targetId);
|
||||||
|
\Federator\DIO\Posts::deletePost($dbh, $targetId);
|
||||||
|
} else {
|
||||||
|
error_log("NewContent::postForUser: Error in Undo Vote for user $user->id, targetId is not a string");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'Note':
|
||||||
|
// Undo Note (remove note)
|
||||||
|
if (method_exists($object, 'getID')) {
|
||||||
|
$noteId = $object->getID();
|
||||||
|
\Federator\DIO\Posts::deletePost($dbh, $noteId);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else if (is_string($object)) {
|
||||||
|
\Federator\DIO\Posts::deletePost($dbh, $object);
|
||||||
|
} else {
|
||||||
|
error_log("NewContent::postForUser: Error in Undo for user $user->id, object is not a string or object");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'Like':
|
||||||
|
// Add Like
|
||||||
|
$targetId = $newActivity->getObject();
|
||||||
|
if (is_string($targetId)) {
|
||||||
|
// \Federator\DIO\Votes::addVote($dbh, $user->id, $targetId, 'like');
|
||||||
|
\Federator\DIO\Posts::savePost($dbh, $user->id, $newActivity);
|
||||||
|
} else {
|
||||||
|
error_log("NewContent::postForUser: Error in Add Like for user $user->id, targetId is not a string");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'Dislike':
|
||||||
|
// Add Dislike
|
||||||
|
$targetId = $newActivity->getObject();
|
||||||
|
if (is_string($targetId)) {
|
||||||
|
// \Federator\DIO\Votes::addVote($dbh, $user->id, $targetId, 'dislike');
|
||||||
|
\Federator\DIO\Posts::savePost($dbh, $user->id, $newActivity);
|
||||||
|
} else {
|
||||||
|
error_log("NewContent::postForUser: Error in Add Dislike for user $user->id, targetId is not a string");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'Create':
|
||||||
|
$object = $newActivity->getObject();
|
||||||
|
if (is_object($object)) {
|
||||||
|
switch ($object->getType()) {
|
||||||
|
case 'Note':
|
||||||
|
case 'Article':
|
||||||
|
default:
|
||||||
|
\Federator\DIO\Posts::savePost($dbh, $user->id, $newActivity);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Post Note
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
error_log("NewContent::postForUser: Unhandled activity type $type for user $user->id");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
9
php/federator/cache/cache.php
vendored
9
php/federator/cache/cache.php
vendored
|
@ -22,6 +22,15 @@ interface Cache extends \Federator\Connector\Connector
|
||||||
*/
|
*/
|
||||||
public function saveRemoteFollowersOfUser($user, $followers);
|
public function saveRemoteFollowersOfUser($user, $followers);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* save remote following for user
|
||||||
|
*
|
||||||
|
* @param string $user user name
|
||||||
|
* @param \Federator\Data\FedUser[]|false $following user following
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function saveRemoteFollowingForUser($user, $following);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* save remote posts by user
|
* save remote posts by user
|
||||||
*
|
*
|
||||||
|
|
|
@ -22,6 +22,15 @@ interface Connector
|
||||||
*/
|
*/
|
||||||
public function getRemoteFollowersOfUser($id);
|
public function getRemoteFollowersOfUser($id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get following of given user
|
||||||
|
*
|
||||||
|
* @param string $id user id
|
||||||
|
|
||||||
|
* @return \Federator\Data\FedUser[]|false
|
||||||
|
*/
|
||||||
|
public function getRemoteFollowingForUser($id);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* get posts by given user
|
* get posts by given user
|
||||||
*
|
*
|
||||||
|
@ -29,7 +38,7 @@ interface Connector
|
||||||
* @param string $min min date
|
* @param string $min min date
|
||||||
* @param string $max max date
|
* @param string $max max date
|
||||||
|
|
||||||
* @return \Federator\Data\ActivityPub\Common\APObject[]|false
|
* @return \Federator\Data\ActivityPub\Common\Activity[]|false
|
||||||
*/
|
*/
|
||||||
public function getRemotePostsByUser($id, $min, $max);
|
public function getRemotePostsByUser($id, $min, $max);
|
||||||
|
|
||||||
|
|
|
@ -347,7 +347,7 @@ class APObject implements \JsonSerializable
|
||||||
/**
|
/**
|
||||||
* set child object
|
* set child object
|
||||||
*
|
*
|
||||||
* @param APObject $object
|
* @param APObject|string $object
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function setObject($object)
|
public function setObject($object)
|
||||||
|
|
|
@ -51,17 +51,27 @@ class Collection extends APObject
|
||||||
return parent::fromJson($json);
|
return parent::fromJson($json);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function count() : int
|
/**
|
||||||
|
* set total items
|
||||||
|
*
|
||||||
|
* @param int $totalItems total items
|
||||||
|
*/
|
||||||
|
public function setTotalItems(int $totalItems): void
|
||||||
|
{
|
||||||
|
$this->totalItems = $totalItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function count(): int
|
||||||
{
|
{
|
||||||
return $this->totalItems;
|
return $this->totalItems;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setFirst(string $url) : void
|
public function setFirst(string $url): void
|
||||||
{
|
{
|
||||||
$this->first = $url;
|
$this->first = $url;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setLast(string $url) : void
|
public function setLast(string $url): void
|
||||||
{
|
{
|
||||||
$this->last = $url;
|
$this->last = $url;
|
||||||
}
|
}
|
||||||
|
|
18
php/federator/data/activitypub/common/dislike.php
Normal file
18
php/federator/data/activitypub/common/dislike.php
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*
|
||||||
|
* @author Yannis Vogel (vogeldevelopment)
|
||||||
|
**/
|
||||||
|
|
||||||
|
namespace Federator\Data\ActivityPub\Common;
|
||||||
|
|
||||||
|
class Dislike extends Activity
|
||||||
|
{
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct('Dislike');
|
||||||
|
parent::addContext('https://www.w3.org/ns/activitystreams');
|
||||||
|
}
|
||||||
|
}
|
53
php/federator/data/activitypub/common/followers.php
Normal file
53
php/federator/data/activitypub/common/followers.php
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
<?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 Followers extends OrderedCollectionPage
|
||||||
|
{
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
parent::addContext('https://www.w3.org/ns/activitystreams');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* set items
|
||||||
|
*
|
||||||
|
* @param string[] $items the items in the collection
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function setItems(&$items)
|
||||||
|
{
|
||||||
|
// Optionally: type check that all $items are Activity objects
|
||||||
|
$this->items = $items;
|
||||||
|
$this->totalItems = sizeof($items);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* convert internal state to php array
|
||||||
|
*
|
||||||
|
* @return array<string,mixed>
|
||||||
|
*/
|
||||||
|
public function toObject()
|
||||||
|
{
|
||||||
|
$return = parent::toObject();
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
53
php/federator/data/activitypub/common/following.php
Normal file
53
php/federator/data/activitypub/common/following.php
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
<?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 Following extends OrderedCollectionPage
|
||||||
|
{
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
parent::addContext('https://www.w3.org/ns/activitystreams');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* set items
|
||||||
|
*
|
||||||
|
* @param string[] $items the items in the collection
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function setItems(&$items)
|
||||||
|
{
|
||||||
|
// Optionally: type check that all $items are Activity objects
|
||||||
|
$this->items = $items;
|
||||||
|
$this->totalItems = sizeof($items);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* convert internal state to php array
|
||||||
|
*
|
||||||
|
* @return array<string,mixed>
|
||||||
|
*/
|
||||||
|
public function toObject()
|
||||||
|
{
|
||||||
|
$return = parent::toObject();
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
18
php/federator/data/activitypub/common/like.php
Normal file
18
php/federator/data/activitypub/common/like.php
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*
|
||||||
|
* @author Yannis Vogel (vogeldevelopment)
|
||||||
|
**/
|
||||||
|
|
||||||
|
namespace Federator\Data\ActivityPub\Common;
|
||||||
|
|
||||||
|
class Like extends Activity
|
||||||
|
{
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct('Like');
|
||||||
|
parent::addContext('https://www.w3.org/ns/activitystreams');
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,7 +13,7 @@ class OrderedCollection extends Collection
|
||||||
/**
|
/**
|
||||||
* nested items
|
* nested items
|
||||||
*
|
*
|
||||||
* @var APObject[]
|
* @var APObject[]|string[]
|
||||||
*/
|
*/
|
||||||
protected $items = [];
|
protected $items = [];
|
||||||
|
|
||||||
|
@ -34,7 +34,11 @@ class OrderedCollection extends Collection
|
||||||
$return['type'] = 'OrderedCollection';
|
$return['type'] = 'OrderedCollection';
|
||||||
if ($this->totalItems > 0) {
|
if ($this->totalItems > 0) {
|
||||||
foreach ($this->items as $item) {
|
foreach ($this->items as $item) {
|
||||||
$return['OrderedItems'][] = $item->toObject();
|
if (is_string($item)) {
|
||||||
|
$return['orderedItems'][] = $item;
|
||||||
|
} elseif (is_object($item)) {
|
||||||
|
$return['orderedItems'][] = $item->toObject();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return $return;
|
return $return;
|
||||||
|
@ -51,7 +55,11 @@ class OrderedCollection extends Collection
|
||||||
return parent::fromJson($json);
|
return parent::fromJson($json);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function append(APObject &$item) : void
|
/**
|
||||||
|
* add item to collection
|
||||||
|
* @param APObject|string $item
|
||||||
|
*/
|
||||||
|
public function append(&$item): void
|
||||||
{
|
{
|
||||||
$this->items[] = $item;
|
$this->items[] = $item;
|
||||||
$this->totalItems = sizeof($this->items);
|
$this->totalItems = sizeof($this->items);
|
||||||
|
@ -60,7 +68,7 @@ class OrderedCollection extends Collection
|
||||||
/**
|
/**
|
||||||
* get item with given index
|
* get item with given index
|
||||||
*
|
*
|
||||||
* @return APObject|false
|
* @return APObject|string|false
|
||||||
*/
|
*/
|
||||||
public function get(int $index)
|
public function get(int $index)
|
||||||
{
|
{
|
||||||
|
@ -70,7 +78,7 @@ class OrderedCollection extends Collection
|
||||||
}
|
}
|
||||||
return $this->items[$index];
|
return $this->items[$index];
|
||||||
} else {
|
} else {
|
||||||
if ($this->totalItems+ $index < 0) {
|
if ($this->totalItems + $index < 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return $this->items[$this->totalItems + $index];
|
return $this->items[$this->totalItems + $index];
|
||||||
|
@ -80,7 +88,7 @@ class OrderedCollection extends Collection
|
||||||
/**
|
/**
|
||||||
* set items
|
* set items
|
||||||
*
|
*
|
||||||
* @param APObject[] $items
|
* @param APObject[]|string[] $items
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function setItems(&$items)
|
public function setItems(&$items)
|
||||||
|
|
|
@ -27,6 +27,18 @@ class Outbox extends OrderedCollectionPage
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* set items
|
||||||
|
*
|
||||||
|
* @param \Federator\Data\ActivityPub\Common\APObject[] $items the items in the collection
|
||||||
|
*/
|
||||||
|
public function setItems(&$items)
|
||||||
|
{
|
||||||
|
// Optionally: type check that all $items are Activity objects
|
||||||
|
$this->items = $items;
|
||||||
|
$this->totalItems = sizeof($items);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* convert internal state to php array
|
* convert internal state to php array
|
||||||
*
|
*
|
||||||
|
|
27
php/federator/data/activitypub/common/vote.php
Normal file
27
php/federator/data/activitypub/common/vote.php
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
<?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 Vote extends APObject
|
||||||
|
{
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct('Vote');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* create object from json
|
||||||
|
* @param mixed $json input
|
||||||
|
* @return bool true on success
|
||||||
|
*/
|
||||||
|
public function fromJson($json)
|
||||||
|
{
|
||||||
|
return parent::fromJson($json);
|
||||||
|
}
|
||||||
|
}
|
|
@ -55,6 +55,9 @@ class Factory
|
||||||
case 'Outbox':
|
case 'Outbox':
|
||||||
$return = new Common\Outbox();
|
$return = new Common\Outbox();
|
||||||
break;
|
break;
|
||||||
|
case 'Vote':
|
||||||
|
$return = new Common\Vote();
|
||||||
|
break;
|
||||||
case 'Inbox':
|
case 'Inbox':
|
||||||
$return = new Common\Inbox();
|
$return = new Common\Inbox();
|
||||||
break;
|
break;
|
||||||
|
@ -98,6 +101,12 @@ class Factory
|
||||||
case 'Delete':
|
case 'Delete':
|
||||||
$return = new Common\Delete();
|
$return = new Common\Delete();
|
||||||
break;
|
break;
|
||||||
|
case 'Like':
|
||||||
|
$return = new Common\Like();
|
||||||
|
break;
|
||||||
|
case 'Dislike':
|
||||||
|
$return = new Common\Dislike();
|
||||||
|
break;
|
||||||
case 'Follow':
|
case 'Follow':
|
||||||
$return = new Common\Follow();
|
$return = new Common\Follow();
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -74,7 +74,69 @@ class Followers
|
||||||
return $followers;
|
return $followers;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* get followers of user
|
* get following for user - who does the user follow
|
||||||
|
*
|
||||||
|
* @param \mysqli $dbh
|
||||||
|
* database handle
|
||||||
|
* @param string $id
|
||||||
|
* user id
|
||||||
|
* @param \Federator\Connector\Connector $connector
|
||||||
|
* connector to fetch use with
|
||||||
|
* @param \Federator\Cache\Cache|null $cache
|
||||||
|
* optional caching service
|
||||||
|
* @return \Federator\Data\FedUser[]
|
||||||
|
*/
|
||||||
|
|
||||||
|
public static function getFollowingForUser($dbh, $id, $connector, $cache)
|
||||||
|
{
|
||||||
|
// ask cache
|
||||||
|
if ($cache !== null) {
|
||||||
|
$following = $cache->getRemoteFollowingForUser($id);
|
||||||
|
if ($following !== false) {
|
||||||
|
return $following;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$following = [];
|
||||||
|
$sql = 'select target_user from follows where source_user = ?';
|
||||||
|
$stmt = $dbh->prepare($sql);
|
||||||
|
if ($stmt === false) {
|
||||||
|
throw new \Federator\Exceptions\ServerError();
|
||||||
|
}
|
||||||
|
$stmt->bind_param("s", $id);
|
||||||
|
$stmt->execute();
|
||||||
|
$followingIds = [];
|
||||||
|
$stmt->bind_result($sourceUser);
|
||||||
|
while ($stmt->fetch()) {
|
||||||
|
$followingIds[] = $sourceUser;
|
||||||
|
}
|
||||||
|
$stmt->close();
|
||||||
|
foreach ($followingIds as $followingId) {
|
||||||
|
$user = \Federator\DIO\FedUser::getUserByName(
|
||||||
|
$dbh,
|
||||||
|
$followingId,
|
||||||
|
$cache,
|
||||||
|
);
|
||||||
|
if ($user !== false && $user->id !== null) {
|
||||||
|
$following[] = $user;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($following === []) {
|
||||||
|
// ask connector for user-id
|
||||||
|
$following = $connector->getRemoteFollowingForUser($id);
|
||||||
|
if ($following === false) {
|
||||||
|
$following = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// save posts to DB
|
||||||
|
if ($cache !== null) {
|
||||||
|
$cache->saveRemoteFollowingForUser($id, $following);
|
||||||
|
}
|
||||||
|
return $following;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get followers of federated external user (e.g. mastodon)
|
||||||
*
|
*
|
||||||
* @param \mysqli $dbh
|
* @param \mysqli $dbh
|
||||||
* database handle
|
* database handle
|
||||||
|
|
|
@ -13,13 +13,12 @@ namespace Federator\DIO;
|
||||||
*/
|
*/
|
||||||
class Posts
|
class Posts
|
||||||
{
|
{
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* get posts by user
|
* get posts by user
|
||||||
*
|
*
|
||||||
* @param \mysqli $dbh @unused-param
|
* @param \mysqli $dbh @unused-param
|
||||||
* database handle
|
* database handle
|
||||||
* @param string $id
|
* @param string $userid
|
||||||
* user id
|
* user id
|
||||||
* @param \Federator\Connector\Connector $connector
|
* @param \Federator\Connector\Connector $connector
|
||||||
* connector to fetch use with
|
* connector to fetch use with
|
||||||
|
@ -29,31 +28,203 @@ class Posts
|
||||||
* minimum date
|
* minimum date
|
||||||
* @param string $max
|
* @param string $max
|
||||||
* maximum date
|
* maximum date
|
||||||
* @return \Federator\Data\ActivityPub\Common\APObject[]
|
* @return \Federator\Data\ActivityPub\Common\Activity[]
|
||||||
*/
|
*/
|
||||||
public static function getPostsByUser($dbh, $id, $connector, $cache, $min, $max)
|
public static function getPostsByUser($dbh, $userid, $connector, $cache, $min, $max)
|
||||||
{
|
{
|
||||||
// ask cache
|
// ask cache
|
||||||
if ($cache !== null) {
|
if ($cache !== null) {
|
||||||
$posts = $cache->getRemotePostsByUser($id, $min, $max);
|
$posts = $cache->getRemotePostsByUser($userid, $min, $max);
|
||||||
if ($posts !== false) {
|
if ($posts !== false) {
|
||||||
return $posts;
|
return $posts;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$posts = [];
|
$posts = self::getPostsFromDb($dbh, $userid, $min, $max);
|
||||||
// TODO: check our db
|
if ($posts === false) {
|
||||||
|
$posts = [];
|
||||||
|
}
|
||||||
|
|
||||||
if ($posts === []) {
|
// Only override $min if we found posts in our DB
|
||||||
// ask connector for user-id
|
$remoteMin = $min;
|
||||||
$posts = $connector->getRemotePostsByUser($id, $min, $max);
|
if (!empty($posts)) {
|
||||||
if ($posts === false) {
|
// Find the latest published date in the DB posts
|
||||||
$posts = [];
|
$latestPublished = null;
|
||||||
|
foreach ($posts as $post) {
|
||||||
|
$published = $post->getPublished();
|
||||||
|
if ($published != null) {
|
||||||
|
$publishedStr = gmdate('Y-m-d H:i:s', $published);
|
||||||
|
if ($latestPublished === null || $publishedStr > $latestPublished) {
|
||||||
|
$latestPublished = $publishedStr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($latestPublished !== null) {
|
||||||
|
$remoteMin = $latestPublished;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Always fetch newer posts from connector (if any)
|
||||||
|
$newPosts = $connector->getRemotePostsByUser($userid, $remoteMin, $max);
|
||||||
|
if ($newPosts !== false && is_array($newPosts)) {
|
||||||
|
// Merge new posts with DB posts, avoiding duplicates by ID
|
||||||
|
$existingIds = [];
|
||||||
|
foreach ($posts as $post) {
|
||||||
|
$existingIds[$post->getID()] = true;
|
||||||
|
}
|
||||||
|
foreach ($newPosts as $newPost) {
|
||||||
|
if (!isset($existingIds[$newPost->getID()])) {
|
||||||
|
$posts[] = $newPost;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// save posts to DB
|
// save posts to DB
|
||||||
|
foreach ($posts as $post) {
|
||||||
|
if ($post->getID() !== "") {
|
||||||
|
self::savePost($dbh, $userid, $post);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ($cache !== null) {
|
if ($cache !== null) {
|
||||||
$cache->saveRemotePostsByUser($id, $posts);
|
$cache->saveRemotePostsByUser($userid, $posts);
|
||||||
}
|
}
|
||||||
return $posts;
|
return $posts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get posts for a user from the DB (optionally by date)
|
||||||
|
*
|
||||||
|
* @param \mysqli $dbh
|
||||||
|
* @param string $userId
|
||||||
|
* @param string|null $min
|
||||||
|
* @param string|null $max
|
||||||
|
* @return \Federator\Data\ActivityPub\Common\Activity[]|false
|
||||||
|
*/
|
||||||
|
public static function getPostsFromDb($dbh, $userId, $min = null, $max = null)
|
||||||
|
{
|
||||||
|
$sql = 'SELECT id, user_id, type, object, published FROM posts WHERE user_id = ?';
|
||||||
|
$params = [$userId];
|
||||||
|
$types = 's';
|
||||||
|
if ($min !== null) {
|
||||||
|
$sql .= ' AND published >= ?';
|
||||||
|
$params[] = $min;
|
||||||
|
$types .= 's';
|
||||||
|
}
|
||||||
|
if ($max !== null) {
|
||||||
|
$sql .= ' AND published <= ?';
|
||||||
|
$params[] = $max;
|
||||||
|
$types .= 's';
|
||||||
|
}
|
||||||
|
$sql .= ' ORDER BY published DESC';
|
||||||
|
|
||||||
|
$stmt = $dbh->prepare($sql);
|
||||||
|
if ($stmt === false) {
|
||||||
|
throw new \Federator\Exceptions\ServerError();
|
||||||
|
}
|
||||||
|
$stmt->bind_param($types, ...$params);
|
||||||
|
$stmt->execute();
|
||||||
|
$result = $stmt->get_result();
|
||||||
|
if (!($result instanceof \mysqli_result)) {
|
||||||
|
$stmt->close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$posts = [];
|
||||||
|
while ($row = $result->fetch_assoc()) {
|
||||||
|
if (!empty($row['object'])) {
|
||||||
|
$objectData = json_decode($row['object'], true);
|
||||||
|
if (is_array($objectData)) {
|
||||||
|
// Use the ActivityPub factory to create the APObject
|
||||||
|
$object = \Federator\Data\ActivityPub\Factory::newActivityFromJson($objectData);
|
||||||
|
if ($object !== false) {
|
||||||
|
$posts[] = $object;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$stmt->close();
|
||||||
|
return $posts;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save a post (insert or update)
|
||||||
|
*
|
||||||
|
* @param \mysqli $dbh
|
||||||
|
* @param string $userId
|
||||||
|
* @param \Federator\Data\ActivityPub\Common\Activity $post
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function savePost($dbh, $userId, $post)
|
||||||
|
{
|
||||||
|
$sql = 'INSERT INTO posts (
|
||||||
|
`id`, `url`, `user_id`, `actor`, `type`, `object`, `to`, `cc`, `published`
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
`url` = VALUES(`url`),
|
||||||
|
`user_id` = VALUES(`user_id`),
|
||||||
|
`actor` = VALUES(`actor`),
|
||||||
|
`type` = VALUES(`type`),
|
||||||
|
`object` = VALUES(`object`),
|
||||||
|
`to` = VALUES(`to`),
|
||||||
|
`cc` = VALUES(`cc`),
|
||||||
|
`published` = VALUES(`published`)';
|
||||||
|
$stmt = $dbh->prepare($sql);
|
||||||
|
if ($stmt === false) {
|
||||||
|
throw new \Federator\Exceptions\ServerError();
|
||||||
|
}
|
||||||
|
|
||||||
|
$id = $post->getID();
|
||||||
|
$url = $post->getUrl();
|
||||||
|
$actor = $post->getAActor();
|
||||||
|
$type = $post->getType();
|
||||||
|
$object = $post->getObject();
|
||||||
|
$objectJson = ($object instanceof \Federator\Data\ActivityPub\Common\APObject)
|
||||||
|
? json_encode($object)
|
||||||
|
: $object;
|
||||||
|
if ($objectJson === false) {
|
||||||
|
$objectJson = null;
|
||||||
|
}
|
||||||
|
$to = $post->getTo();
|
||||||
|
$cc = $post->getCC();
|
||||||
|
$toJson = is_array($to) ? json_encode($to) : (is_string($to) ? json_encode([$to]) : null);
|
||||||
|
$ccJson = is_array($cc) ? json_encode($cc) : (is_string($cc) ? json_encode([$cc]) : null);
|
||||||
|
$published = $post->getPublished();
|
||||||
|
$publishedStr = $published ? gmdate('Y-m-d H:i:s', $published) : gmdate('Y-m-d H:i:s');
|
||||||
|
|
||||||
|
$stmt->bind_param(
|
||||||
|
"sssssssss",
|
||||||
|
$id,
|
||||||
|
$url,
|
||||||
|
$userId,
|
||||||
|
$actor,
|
||||||
|
$type,
|
||||||
|
$objectJson,
|
||||||
|
$toJson,
|
||||||
|
$ccJson,
|
||||||
|
$publishedStr
|
||||||
|
);
|
||||||
|
$result = $stmt->execute();
|
||||||
|
$stmt->close();
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a post
|
||||||
|
*
|
||||||
|
* @param \mysqli $dbh
|
||||||
|
* @param string $id The post ID
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function deletePost($dbh, $id)
|
||||||
|
{
|
||||||
|
$sql = 'delete from posts where id = ?';
|
||||||
|
$stmt = $dbh->prepare($sql);
|
||||||
|
if ($stmt === false) {
|
||||||
|
throw new \Federator\Exceptions\ServerError();
|
||||||
|
}
|
||||||
|
$stmt->bind_param("s", $id);
|
||||||
|
$stmt->execute();
|
||||||
|
$affectedRows = $stmt->affected_rows;
|
||||||
|
$stmt->close();
|
||||||
|
return $affectedRows > 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
97
php/federator/dio/votes.php
Normal file
97
php/federator/dio/votes.php
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*
|
||||||
|
* @author Yannis Vogel (vogeldevelopment)
|
||||||
|
**/
|
||||||
|
|
||||||
|
namespace Federator\DIO;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IO functions related to votes
|
||||||
|
*/
|
||||||
|
class Votes
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Add a vote (like/dislike)
|
||||||
|
*
|
||||||
|
* @param \mysqli $dbh
|
||||||
|
* @param string $userId The user who votes
|
||||||
|
* @param string $targetId The object being voted on (e.g., post id)
|
||||||
|
* @param string $type 'like' or 'dislike'
|
||||||
|
* @return string|false The generated vote ID on success, false on failure
|
||||||
|
*/
|
||||||
|
public static function addVote($dbh, $userId, $targetId, $type)
|
||||||
|
{
|
||||||
|
// Check if already voted
|
||||||
|
$sql = 'SELECT id FROM votes WHERE user_id = ? AND target_id = ? AND type = ?';
|
||||||
|
$stmt = $dbh->prepare($sql);
|
||||||
|
if ($stmt === false) {
|
||||||
|
throw new \Federator\Exceptions\ServerError();
|
||||||
|
}
|
||||||
|
$stmt->bind_param("sss", $userId, $targetId, $type);
|
||||||
|
$foundId = 0;
|
||||||
|
$ret = $stmt->bind_result($foundId);
|
||||||
|
$stmt->execute();
|
||||||
|
if ($ret) {
|
||||||
|
$stmt->fetch();
|
||||||
|
}
|
||||||
|
$stmt->close();
|
||||||
|
if ($foundId != 0) {
|
||||||
|
return false; // Already voted
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a unique ID for the vote
|
||||||
|
do {
|
||||||
|
$id = bin2hex(openssl_random_pseudo_bytes(16));
|
||||||
|
// Check if the generated ID is unique
|
||||||
|
$sql = 'SELECT id FROM votes WHERE id = ?';
|
||||||
|
$stmt = $dbh->prepare($sql);
|
||||||
|
if ($stmt === false) {
|
||||||
|
throw new \Federator\Exceptions\ServerError();
|
||||||
|
}
|
||||||
|
$stmt->bind_param("s", $id);
|
||||||
|
$foundId = 0;
|
||||||
|
$ret = $stmt->bind_result($foundId);
|
||||||
|
$stmt->execute();
|
||||||
|
if ($ret) {
|
||||||
|
$stmt->fetch();
|
||||||
|
}
|
||||||
|
$stmt->close();
|
||||||
|
} while ($foundId > 0);
|
||||||
|
|
||||||
|
// Add vote with created_at timestamp
|
||||||
|
$sql = 'INSERT INTO votes (id, user_id, target_id, type, created_at) VALUES (?, ?, ?, ?, NOW())';
|
||||||
|
$stmt = $dbh->prepare($sql);
|
||||||
|
if ($stmt === false) {
|
||||||
|
throw new \Federator\Exceptions\ServerError();
|
||||||
|
}
|
||||||
|
$stmt->bind_param("ssss", $id, $userId, $targetId, $type);
|
||||||
|
$stmt->execute();
|
||||||
|
$stmt->close();
|
||||||
|
return $id; // Return the generated vote ID
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a vote (like/dislike)
|
||||||
|
*
|
||||||
|
* @param \mysqli $dbh
|
||||||
|
* @param string $userId
|
||||||
|
* @param string $targetId
|
||||||
|
* @return bool true on success
|
||||||
|
*/
|
||||||
|
public static function removeVote($dbh, $userId, $targetId)
|
||||||
|
{
|
||||||
|
$sql = 'DELETE FROM votes WHERE user_id = ? AND target_id = ?';
|
||||||
|
$stmt = $dbh->prepare($sql);
|
||||||
|
if ($stmt === false) {
|
||||||
|
throw new \Federator\Exceptions\ServerError();
|
||||||
|
}
|
||||||
|
$stmt->bind_param("ss", $userId, $targetId);
|
||||||
|
$stmt->execute();
|
||||||
|
$affectedRows = $stmt->affected_rows;
|
||||||
|
$stmt->close();
|
||||||
|
return $affectedRows > 0;
|
||||||
|
}
|
||||||
|
}
|
69
php/federator/jobs/newContentJob.php
Normal file
69
php/federator/jobs/newContentJob.php
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Federator\Jobs;
|
||||||
|
|
||||||
|
class NewContentJob extends \Federator\Api
|
||||||
|
{
|
||||||
|
/** @var array<string, mixed> $args Arguments for the job */
|
||||||
|
public $args = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* cache instance
|
||||||
|
*
|
||||||
|
* @var \Federator\Cache\Cache $cache
|
||||||
|
*/
|
||||||
|
protected $cache;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* remote connector
|
||||||
|
*
|
||||||
|
* @var \Federator\Connector\Connector $connector
|
||||||
|
*/
|
||||||
|
protected $connector = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* database instance
|
||||||
|
*
|
||||||
|
* @var \Mysqli $dbh
|
||||||
|
*/
|
||||||
|
protected $dbh;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* constructor
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set up environment for this job
|
||||||
|
*/
|
||||||
|
public function setUp(): void
|
||||||
|
{
|
||||||
|
$this->openDatabase();
|
||||||
|
$this->loadPlugins();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform the inbox job.
|
||||||
|
*
|
||||||
|
* @return bool true on success, false on failure
|
||||||
|
*/
|
||||||
|
public function perform(): bool
|
||||||
|
{
|
||||||
|
error_log("NewContentJob: Starting inbox job");
|
||||||
|
$user = $this->args['user'];
|
||||||
|
$activity = $this->args['activity'];
|
||||||
|
|
||||||
|
$activity = \Federator\Data\ActivityPub\Factory::newActivityFromJson($activity);
|
||||||
|
|
||||||
|
if ($activity === false) {
|
||||||
|
error_log("NewContentJob: Failed to create activity from JSON");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
\Federator\Api\V1\NewContent::postForUser($this->dbh, $this->connector, $this->cache, $user, $activity);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
|
@ -77,13 +77,41 @@ class ContentNation implements Connector
|
||||||
return $followers;
|
return $followers;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get following of given user
|
||||||
|
*
|
||||||
|
* @param string $userId user id
|
||||||
|
|
||||||
|
* @return \Federator\Data\FedUser[]|false
|
||||||
|
*/
|
||||||
|
public function getRemoteFollowingForUser($userId)
|
||||||
|
{
|
||||||
|
// todo implement queue for this
|
||||||
|
if (preg_match("#^([^@]+)@([^/]+)#", $userId, $matches) == 1) {
|
||||||
|
$userId = $matches[1];
|
||||||
|
}
|
||||||
|
$remoteURL = $this->service . '/api/profile/' . urlencode($userId) . '/following';
|
||||||
|
|
||||||
|
[$response, $info] = \Federator\Main::getFromRemote($remoteURL, []);
|
||||||
|
if ($info['http_code'] != 200) {
|
||||||
|
print_r($info);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$r = json_decode($response, true);
|
||||||
|
if ($r === false || $r === null || !is_array($r)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$followers = [];
|
||||||
|
return $followers;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* get posts by given user
|
* get posts by given user
|
||||||
*
|
*
|
||||||
* @param string $userId user id
|
* @param string $userId user id
|
||||||
* @param string $min min date
|
* @param string $min min date
|
||||||
* @param string $max max date
|
* @param string $max max date
|
||||||
* @return \Federator\Data\ActivityPub\Common\APObject[]|false
|
* @return \Federator\Data\ActivityPub\Common\Activity[]|false
|
||||||
*/
|
*/
|
||||||
public function getRemotePostsByUser($userId, $min, $max)
|
public function getRemotePostsByUser($userId, $min, $max)
|
||||||
{
|
{
|
||||||
|
@ -114,16 +142,15 @@ class ContentNation implements Connector
|
||||||
$imgpath = $this->config['userdata']['path'];
|
$imgpath = $this->config['userdata']['path'];
|
||||||
$userdata = $this->config['userdata']['url'];
|
$userdata = $this->config['userdata']['url'];
|
||||||
foreach ($activities as $activity) {
|
foreach ($activities as $activity) {
|
||||||
$create = new \Federator\Data\ActivityPub\Common\Create();
|
|
||||||
$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');
|
|
||||||
|
|
||||||
switch ($activity['type']) {
|
switch ($activity['type']) {
|
||||||
case 'Article':
|
case 'Article':
|
||||||
$create->setURL('https://' . $domain . '/' . $activity['name']);
|
$create = new \Federator\Data\ActivityPub\Common\Create();
|
||||||
|
$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');
|
||||||
|
$create->setURL('https://' . $domain . '/' . $activity['profilename'] . '/' . $activity['name']);
|
||||||
$apArticle = new \Federator\Data\ActivityPub\Common\Article();
|
$apArticle = new \Federator\Data\ActivityPub\Common\Article();
|
||||||
if (array_key_exists('tags', $activity)) {
|
if (array_key_exists('tags', $activity)) {
|
||||||
foreach ($activity['tags'] as $tag) {
|
foreach ($activity['tags'] as $tag) {
|
||||||
|
@ -169,10 +196,18 @@ class ContentNation implements Connector
|
||||||
$create->setObject($apArticle);
|
$create->setObject($apArticle);
|
||||||
$posts[] = $create;
|
$posts[] = $create;
|
||||||
break; // Article
|
break; // Article
|
||||||
|
|
||||||
case 'Comment':
|
case 'Comment':
|
||||||
|
$create = new \Federator\Data\ActivityPub\Common\Create();
|
||||||
|
$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');
|
||||||
$commentJson = $activity;
|
$commentJson = $activity;
|
||||||
$commentJson['type'] = 'Note';
|
$commentJson['type'] = 'Note';
|
||||||
$commentJson['summary'] = $activity['subject'];
|
$commentJson['summary'] = $activity['subject'];
|
||||||
|
$commentJson['id'] = $activity['id'];
|
||||||
$note = \Federator\Data\ActivityPub\Factory::newFromJson($commentJson, "");
|
$note = \Federator\Data\ActivityPub\Factory::newFromJson($commentJson, "");
|
||||||
if ($note === null) {
|
if ($note === null) {
|
||||||
error_log("ContentNation::getRemotePostsByUser couldn't create comment");
|
error_log("ContentNation::getRemotePostsByUser couldn't create comment");
|
||||||
|
@ -180,53 +215,36 @@ class ContentNation implements Connector
|
||||||
$create->setObject($comment);
|
$create->setObject($comment);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
$url = 'https://' . $domain . '/' . $activity['articleOwnerName'] . '/' . $activity['articleName'] . '#' . $note->getID();
|
||||||
|
$create->setURL($url);
|
||||||
$create->setObject($note);
|
$create->setObject($note);
|
||||||
$posts[] = $create;
|
$posts[] = $create;
|
||||||
break; // Comment
|
break; // Comment
|
||||||
|
|
||||||
case 'Vote':
|
case 'Vote':
|
||||||
$url = 'https://' . $domain . '/' . $userId . '/'
|
// Build Like/Dislike as top-level activity
|
||||||
. $activity['articlename'];
|
$likeType = $activity['upvote'] === true ? 'Like' : 'Dislike';
|
||||||
$url .= '/vote/' . $activity['id'];
|
$like = new \Federator\Data\ActivityPub\Common\Activity($likeType);
|
||||||
$create->setURL($url);
|
$like->setAActor('https://' . $domain . '/' . $userId);
|
||||||
if ($activity['upvote'] === true) {
|
$like->setID($activity['id'])
|
||||||
$like = new \Federator\Data\ActivityPub\Common\Activity('Like');
|
->setPublished($activity['published'] ?? $activity['timestamp'])
|
||||||
$like->setSummary(
|
->addTo("https://www.w3.org/ns/activitystreams#Public")
|
||||||
$this->main->translate(
|
->addCC('https://' . $domain . '/' . $userId . '/followers');
|
||||||
$activity['articlelang'],
|
$like->setSummary(
|
||||||
'vote',
|
$this->main->translate(
|
||||||
'like',
|
$activity['articlelang'],
|
||||||
[$activity['username']]
|
'vote',
|
||||||
)
|
$likeType === 'Like' ? 'like' : 'dislike',
|
||||||
);
|
[$activity['username']]
|
||||||
} else {
|
)
|
||||||
$like = new \Federator\Data\ActivityPub\Common\Activity('Dislike');
|
);
|
||||||
$like->setSummary(
|
$objectUrl = 'https://' . $domain . '/' . $userId . '/' . $activity['articlename'];
|
||||||
$this->main->translate(
|
|
||||||
$activity['articlelang'],
|
|
||||||
'vote',
|
|
||||||
'dislike',
|
|
||||||
[$activity['username']]
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
$actor = new \Federator\Data\ActivityPub\Common\APObject('Person');
|
|
||||||
$actor->setName($activity['username']);
|
|
||||||
$like->setActor($actor);
|
|
||||||
$url = 'https://' . $domain . '/' . $userId . '/' . $activity['articlename'];
|
|
||||||
if ($activity['comment'] !== '') {
|
if ($activity['comment'] !== '') {
|
||||||
$url .= '/comment/' . $activity['comment'];
|
$objectUrl .= "#" . $activity['comment'];
|
||||||
}
|
}
|
||||||
$type = 'Article';
|
$like->setURL($objectUrl . '#' . $activity['id']);
|
||||||
switch ($activity['votetype']) {
|
$like->setObject($objectUrl);
|
||||||
case 'comment':
|
$posts[] = $like;
|
||||||
$type = 'Comment';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
$object = new \Federator\Data\ActivityPub\Common\APObject($type);
|
|
||||||
$object->setHref($url);
|
|
||||||
$like->setObject($object);
|
|
||||||
$create->setObject($like);
|
|
||||||
$posts[] = $create;
|
|
||||||
break; // Vote
|
break; // Vote
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -352,50 +370,98 @@ class ContentNation implements Connector
|
||||||
'type' => 'Create', // Default to 'Create'
|
'type' => 'Create', // Default to 'Create'
|
||||||
'id' => $jsonData['id'] ?? null,
|
'id' => $jsonData['id'] ?? null,
|
||||||
'actor' => $jsonData['actor'] ?? null,
|
'actor' => $jsonData['actor'] ?? null,
|
||||||
'published' => $jsonData['object']['published'] ?? null,
|
|
||||||
'to' => ['https://www.w3.org/ns/activitystreams#Public'],
|
|
||||||
'cc' => [$jsonData['related']['cc']['followers'] ?? null],
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Extract actorName as the last segment of the actor URL (after the last '/')
|
||||||
|
$actorUrl = $jsonData['actor'] ?? null;
|
||||||
|
$actorName = null;
|
||||||
|
$replaceUrl = null;
|
||||||
|
if (is_array($actorUrl)) {
|
||||||
|
$actorUrl = $actorUrl['id'];
|
||||||
|
$replaceUrl = $actorUrl->url ?? null;
|
||||||
|
}
|
||||||
|
if ($actorUrl !== null) {
|
||||||
|
$actorUrlParts = parse_url($actorUrl);
|
||||||
|
if (isset($actorUrlParts['path'])) {
|
||||||
|
$pathSegments = array_values(array_filter(explode('/', $actorUrlParts['path'])));
|
||||||
|
$actorName = end($pathSegments);
|
||||||
|
// Build replaceUrl as scheme://host if both are set
|
||||||
|
if (isset($actorUrlParts['scheme'], $actorUrlParts['host'])) {
|
||||||
|
$replaceUrl = $actorUrlParts['scheme'] . '://' . $actorUrlParts['host'];
|
||||||
|
} else {
|
||||||
|
$replaceUrl = $actorUrl;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$actorName = $actorUrl;
|
||||||
|
$replaceUrl = $actorUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$ap['actor'] = $actorUrl;
|
||||||
|
|
||||||
// Handle specific fields based on the type
|
// Handle specific fields based on the type
|
||||||
switch ($jsonData['type']) {
|
switch ($jsonData['type']) {
|
||||||
case 'comment':
|
case 'comment':
|
||||||
|
// Set Create-level fields
|
||||||
|
$ap['published'] = $jsonData['object']['published'] ?? null;
|
||||||
|
$ap['url'] = $jsonData['object']['url'] ?? null;
|
||||||
|
$ap['to'] = ['https://www.w3.org/ns/activitystreams#Public'];
|
||||||
|
$ap['cc'] = [$jsonData['related']['cc']['followers'] ?? null];
|
||||||
|
|
||||||
|
// Set object as Note with only required fields
|
||||||
$ap['object'] = [
|
$ap['object'] = [
|
||||||
'type' => 'Note',
|
|
||||||
'id' => $jsonData['object']['id'] ?? null,
|
'id' => $jsonData['object']['id'] ?? null,
|
||||||
'summary' => $jsonData['object']['summary'] ?? '',
|
'type' => 'Note',
|
||||||
'content' => $jsonData['object']['content'] ?? '',
|
'content' => $jsonData['object']['content'] ?? '',
|
||||||
'published' => $jsonData['object']['published'] ?? null,
|
'summary' => $jsonData['object']['summary'] ?? '',
|
||||||
'attributedTo' => $jsonData['actor']['id'] ?? null,
|
|
||||||
'to' => ['https://www.w3.org/ns/activitystreams#Public'],
|
|
||||||
'cc' => [$jsonData['related']['cc']['followers'] ?? null],
|
|
||||||
'url' => $jsonData['object']['url'] ?? null,
|
|
||||||
'inReplyTo' => $jsonData['related']['article']['id'] ?? null,
|
|
||||||
];
|
];
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// todo fix this to properly handle votes, data is mocked for now
|
|
||||||
case 'vote':
|
case 'vote':
|
||||||
// Handle voting on a comment or an article
|
$ap['id'] .= "_$actorName";
|
||||||
if ($jsonData['object']['type'] === 'Comment') {
|
if (
|
||||||
$jsonData['object']['type'] = 'Note';
|
isset($jsonData['vote']['type']) &&
|
||||||
}
|
strtolower($jsonData['vote']['type']) === 'undo'
|
||||||
$ap['object'] = [
|
) {
|
||||||
'id' => $jsonData['object']['id'] ?? null,
|
$ap['type'] = 'Undo';
|
||||||
'type' => $jsonData['object']['type'] ?? 'Article',
|
} elseif ($jsonData['vote']['value'] == 1) {
|
||||||
];
|
$ap['type'] = 'Like';
|
||||||
|
} elseif ($jsonData['vote']['value'] == 0) {
|
||||||
// Include additional fields if voting on an article
|
$ap['type'] = 'Dislike';
|
||||||
if ($ap['object']['type'] === 'Article') {
|
} else {
|
||||||
$ap['object']['name'] = $jsonData['object']['name'] ?? null;
|
error_log("ContentNation::jsonToActivity unknown vote type: {$jsonData['vote']['type']} and value: {$jsonData['vote']['value']}");
|
||||||
$ap['object']['author'] = $jsonData['object']['author'] ?? null;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add vote-specific fields
|
$objectId = $jsonData['object']['id'] ?? null;
|
||||||
$ap['vote'] = [
|
|
||||||
'value' => $jsonData['vote']['value'] ?? null,
|
$ap['object'] = $objectId;
|
||||||
'type' => $jsonData['vote']['type'] ?? null,
|
|
||||||
];
|
if ($ap['type'] === "Undo") {
|
||||||
|
$ap['object'] = $ap['id'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/* if ($ap['type'] === 'Undo') {
|
||||||
|
$ap['object'] = [
|
||||||
|
'id' => $objectId,
|
||||||
|
'type' => 'Vote',
|
||||||
|
];
|
||||||
|
} else if (
|
||||||
|
isset($jsonData['object']['type']) &&
|
||||||
|
$jsonData['object']['type'] === 'Article'
|
||||||
|
) {
|
||||||
|
$ap['object'] = [
|
||||||
|
'id' => $objectId,
|
||||||
|
'type' => $jsonData['object']['type'],
|
||||||
|
'name' => $jsonData['object']['name'] ?? null,
|
||||||
|
'author' => $jsonData['object']['author'] ?? null,
|
||||||
|
];
|
||||||
|
} else if ($jsonData['object']['type'] === 'Comment') {
|
||||||
|
$ap['object'] = [
|
||||||
|
'id' => $objectId,
|
||||||
|
'type' => 'Note',
|
||||||
|
'author' => $jsonData['object']['author'] ?? null,
|
||||||
|
];
|
||||||
|
} */
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
@ -403,6 +469,35 @@ class ContentNation implements Connector
|
||||||
throw new \InvalidArgumentException("Unsupported activity type: {$jsonData['type']}");
|
throw new \InvalidArgumentException("Unsupported activity type: {$jsonData['type']}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$config = $this->main->getConfig();
|
||||||
|
$domain = $config['generic']['externaldomain'];
|
||||||
|
/**
|
||||||
|
* Recursively replace strings in an array.
|
||||||
|
*
|
||||||
|
* @param string $search
|
||||||
|
* @param string $replace
|
||||||
|
* @param array<mixed, mixed> $array
|
||||||
|
* @return array<mixed, mixed>
|
||||||
|
*/
|
||||||
|
function array_str_replace_recursive(string $search, string $replace, array $array): array
|
||||||
|
{
|
||||||
|
foreach ($array as $key => $value) {
|
||||||
|
if (is_array($value)) {
|
||||||
|
$array[$key] = array_str_replace_recursive($search, $replace, $value);
|
||||||
|
} elseif (is_string($value)) {
|
||||||
|
$array[$key] = str_replace($search, $replace, $value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $array;
|
||||||
|
}
|
||||||
|
if (is_string($replaceUrl)) {
|
||||||
|
$ap = array_str_replace_recursive(
|
||||||
|
$replaceUrl,
|
||||||
|
'https://' . $domain,
|
||||||
|
$ap
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return \Federator\Data\ActivityPub\Factory::newActivityFromJson($ap);
|
return \Federator\Data\ActivityPub\Factory::newActivityFromJson($ap);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -421,8 +516,7 @@ class ContentNation implements Connector
|
||||||
throw new \Federator\Exceptions\PermissionDenied("Missing Signature header");
|
throw new \Federator\Exceptions\PermissionDenied("Missing Signature header");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isset($headers['X-Sender']) || $headers['X-Sender'] !== $this->config['keys']['headerSenderName'])
|
if (!isset($headers['X-Sender']) || $headers['X-Sender'] !== $this->config['keys']['headerSenderName']) {
|
||||||
{
|
|
||||||
throw new \Federator\Exceptions\PermissionDenied("Invalid sender name");
|
throw new \Federator\Exceptions\PermissionDenied("Invalid sender name");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -30,13 +30,25 @@ class DummyConnector implements Connector
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get following of given user
|
||||||
|
*
|
||||||
|
* @param string $id user id @unused-param
|
||||||
|
|
||||||
|
* @return \Federator\Data\FedUser[]|false
|
||||||
|
*/
|
||||||
|
public function getRemoteFollowingForUser($id)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* get posts by given user
|
* get posts by given user
|
||||||
*
|
*
|
||||||
* @param string $id user id @unused-param
|
* @param string $id user id @unused-param
|
||||||
* @param string $min min date @unused-param
|
* @param string $min min date @unused-param
|
||||||
* @param string $max max date @unused-param
|
* @param string $max max date @unused-param
|
||||||
* @return \Federator\Data\ActivityPub\Common\APObject[]|false
|
* @return \Federator\Data\ActivityPub\Common\Activity[]|false
|
||||||
*/
|
*/
|
||||||
public function getRemotePostsByUser($id, $min, $max)
|
public function getRemotePostsByUser($id, $min, $max)
|
||||||
{
|
{
|
||||||
|
|
|
@ -108,6 +108,19 @@ class RedisCache implements Cache
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get following of given user
|
||||||
|
*
|
||||||
|
* @param string $id user id @unused-param
|
||||||
|
|
||||||
|
* @return \Federator\Data\FedUser[]|false
|
||||||
|
*/
|
||||||
|
public function getRemoteFollowingForUser($id)
|
||||||
|
{
|
||||||
|
error_log("rediscache::getRemoteFollowingForUser not implemented");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert jsonData to Activity format
|
* Convert jsonData to Activity format
|
||||||
*
|
*
|
||||||
|
@ -127,7 +140,7 @@ class RedisCache implements Cache
|
||||||
* @param string $min min date @unused-param
|
* @param string $min min date @unused-param
|
||||||
* @param string $max max date @unused-param
|
* @param string $max max date @unused-param
|
||||||
|
|
||||||
* @return \Federator\Data\ActivityPub\Common\APObject[]|false
|
* @return \Federator\Data\ActivityPub\Common\Activity[]|false
|
||||||
*/
|
*/
|
||||||
public function getRemotePostsByUser($id, $min, $max)
|
public function getRemotePostsByUser($id, $min, $max)
|
||||||
{
|
{
|
||||||
|
@ -242,6 +255,18 @@ class RedisCache implements Cache
|
||||||
error_log("rediscache::saveRemoteFollowersOfUser not implemented");
|
error_log("rediscache::saveRemoteFollowersOfUser not implemented");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* save remote following for user
|
||||||
|
*
|
||||||
|
* @param string $user user name @unused-param
|
||||||
|
* @param \Federator\Data\FedUser[]|false $following user following @unused-param
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function saveRemoteFollowingForUser($user, $following)
|
||||||
|
{
|
||||||
|
error_log("rediscache::saveRemoteFollowingForUser not implemented");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* save remote posts by user
|
* save remote posts by user
|
||||||
*
|
*
|
||||||
|
|
2
sql/2025-05-19.sql
Normal file
2
sql/2025-05-19.sql
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
create table posts(`id` varchar(255) primary key,`url` varchar(255) not null,`user_id` varchar(255) not null,`actor` varchar(255) not null,`type` varchar(255) not null default 'note',`object` text default null,`to` text default null,`cc` text default null,`published` timestamp not null default current_timestamp);
|
||||||
|
update settings set `value`="2025-05-19" where `key`="database_version";
|
Loading…
Add table
Reference in a new issue