federator/php/federator/api/fedusers/inbox.php
Yannis Vogel 767f51cc5b
queue-redis connection + fqdn->CN follow-support
- the resque queue integration is now connected to our redis instance.
- IMPORTANT NOTICE: In order for this to work, we needed to change the plugins file under vendor/resque/php-resque/lib/Resque/Redis.php #137 ($this->driver->auth($password, $user);    (this change is neccessary for our implementation, as they normally don't support the $user attribute as it would be upstream breaking change). I might create a fork for this, to which we bind with composer
- The inboxJob now inherits from api instead of creating its own api instance
- inbox now actually works with follows and Undo-follows from mastodon->CN (adding the follower to our followers-db and removing it on undo).
- fixed bug where paths for templates was incorrectly adjusted
- better/proper support for comment-activity in getExternalPosts
2025-05-18 08:51:18 +02:00

215 lines
7.7 KiB
PHP

<?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 inbox requests
*/
class Inbox implements \Federator\Api\FedUsers\FedUsersInterface
{
/**
* main instance
*
* @var \Federator\Api $main
*/
private $main;
/**
* constructor
* @param \Federator\Api $main api main instance
*/
public function __construct($main)
{
$this->main = $main;
}
/**
* handle get call
*
* @param string|null $_user user to fetch inbox for @unused-param
* @return string|false response
*/
public function get($_user)
{
return false;
}
/**
* handle post call
*
* @param string|null $_user user to add data to inbox
* @return string|false response
*/
public function post($_user)
{
$_rawInput = file_get_contents('php://input');
$allHeaders = getallheaders();
try {
$this->main->checkSignature($allHeaders);
} catch (\Federator\Exceptions\PermissionDenied $e) {
throw new \Federator\Exceptions\Unauthorized("Inbox::post Signature check failed: " . $e->getMessage());
}
$activity = is_string($_rawInput) ? json_decode($_rawInput, true) : null;
if (!is_array($activity)) {
throw new \Federator\Exceptions\ServerError("Inbox::post Input wasn't of type array");
}
$inboxActivity = \Federator\Data\ActivityPub\Factory::newActivityFromJson($activity);
if ($inboxActivity === false) {
throw new \Federator\Exceptions\ServerError("Inbox::post couldn't create inboxActivity");
}
$rootDir = PROJECT_ROOT . '/';
// Shared inbox
if (!isset($_user)) {
// Save the raw input and parsed JSON to a file for inspection
file_put_contents(
$rootDir . 'logs/inbox.log',
date('Y-m-d H:i:s') . ": ==== POST Inbox Activity ====\n" . json_encode($inboxActivity, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) . "\n\n",
FILE_APPEND
);
}
$sendTo = $inboxActivity->getCC();
if ($inboxActivity->getType() === 'Undo') { // for undo the object holds the proper cc
$object = $inboxActivity->getObject();
if ($object !== null && is_object($object)) {
$sendTo = $object->getCC();
}
}
$users = [];
$dbh = $this->main->getDatabase();
$cache = $this->main->getCache();
$connector = $this->main->getConnector();
foreach ($sendTo as $receiver) {
if ($receiver === '' || !is_string($receiver)) {
continue;
}
if (str_ends_with($receiver, '/followers')) {
$actor = $inboxActivity->getAActor();
if ($actor === null || !is_string($actor)) {
error_log("Inbox::post no actor found");
continue;
}
// Extract username from the actor URL
$username = basename((string) (parse_url($actor, PHP_URL_PATH) ?? ''));
$domain = parse_url($actor, PHP_URL_HOST);
if ($username === null || $domain === null) {
error_log("Inbox::post no username or domain found");
continue;
}
$followers = \Federator\DIO\Followers::getFollowersByFedUser($dbh, $connector, $cache, $username . '@' . $domain);
if (is_array($followers)) {
$users = array_merge($users, array_column($followers, 'id'));
}
}
}
if ($_user !== false && !in_array($_user, $users, true)) {
$users[] = $_user;
}
foreach ($users as $user) {
if (!isset($user)) {
continue;
}
$token = \Resque::enqueue('inbox', 'Federator\\Jobs\\InboxJob', [
'user' => $user,
'activity' => $inboxActivity->toObject(),
]);
error_log("Inbox::post enqueued job for user: $user with token: $token");
}
return "success";
}
/**
* handle post call for specific user
*
* @param \mysqli $dbh database handle
* @param \Federator\Connector\Connector $connector connector to use
* @param \Federator\Cache\Cache|null $cache optional caching service
* @param string $_user user to add data to inbox
* @param \Federator\Data\ActivityPub\Common\Activity $inboxActivity the activity that we received
* @return boolean response
*/
public static function postForUser($dbh, $connector, $cache, $_user, $inboxActivity)
{
if (isset($_user)) {
// get user
$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;
}
$rootDir = PROJECT_ROOT . '/';
// Save the raw input and parsed JSON to a file for inspection
file_put_contents(
$rootDir . 'logs/inbox_' . $_user . '.log',
date('Y-m-d H:i:s') . ": ==== POST " . $_user . " Inbox Activity ====\n" . json_encode($inboxActivity, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) . "\n\n",
FILE_APPEND
);
$type = $inboxActivity->getType();
if ($type === 'Follow') {
// Someone wants to follow our user
$actor = $inboxActivity->getAActor(); // The follower's actor URI
if ($actor !== '') {
// Extract follower username (you may need to adjust this logic)
$followerUsername = basename((string) (parse_url($actor, PHP_URL_PATH) ?? ''));
$followerDomain = parse_url($actor, PHP_URL_HOST);
if (is_string($followerDomain)) {
$followerId = "{$followerUsername}@{$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");
}
}
}
}
}
return true;
}
}