forked from grumpydevelop/federator

- includes hacky following-mechanic in order to simulate a follow on mastodon (not properly working, need to also inject the user this creates into the followers db for the target mastodon-user) - created endpoint for inbox. SharedInbox is used when no user is provided (/api/federator/fedusers/inbox'), the regular inbox link now works (/users/username/inbox). - Retrieve all followers of sender and, if they're part of our system, send the activity into their personal inbox - Support Announce and Undo Activity-Types - Inbox currently converts to proper ActPub-objects and saves data to log-files
567 lines
21 KiB
PHP
567 lines
21 KiB
PHP
<?php
|
|
/**
|
|
* SPDX-FileCopyrightText: 2024 Sascha Nitsch (grumpydeveloper) https://contentnation.net/@grumpydevelop
|
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
*
|
|
* @author Sascha Nitsch (grumpydeveloper)
|
|
**/
|
|
|
|
namespace Federator\Api\V1;
|
|
|
|
/**
|
|
* dummy api class for functional poc
|
|
*/
|
|
class Dummy implements \Federator\Api\APIInterface
|
|
{
|
|
/**
|
|
* \Federator\Main instance
|
|
*
|
|
* @var \Federator\Main $main
|
|
*/
|
|
private $main;
|
|
|
|
/**
|
|
* internal message to output
|
|
*
|
|
* @var string $response
|
|
*/
|
|
private $message = [];
|
|
|
|
/**
|
|
* constructor
|
|
*
|
|
* @param \Federator\Main $main main instance
|
|
*/
|
|
public function __construct(\Federator\Main $main)
|
|
{
|
|
$this->main = $main;
|
|
}
|
|
|
|
/**
|
|
* run given url path
|
|
*
|
|
* @param array<string> $paths path array split by /
|
|
* @param \Federator\Data\User|false $user user who is calling us
|
|
* @return bool true on success
|
|
*/
|
|
public function exec($paths, $user): bool
|
|
{
|
|
// only for user with the 'publish' permission
|
|
// if ($user === false || $user->hasPermission('publish') === false) {
|
|
// throw new \Federator\Exceptions\PermissionDenied();
|
|
// }
|
|
$method = $_SERVER["REQUEST_METHOD"];
|
|
switch ($method) {
|
|
case 'GET':
|
|
switch (sizeof($paths)) {
|
|
case 3:
|
|
switch ($paths[2]) {
|
|
case 'moo':
|
|
return $this->getDummy();
|
|
case 'sharedInbox':
|
|
return $this->getSharedInbox();
|
|
default:
|
|
break;
|
|
}
|
|
break;
|
|
case 4:
|
|
case 5:
|
|
switch ($paths[2]) {
|
|
case 'inbox':
|
|
return $this->getInbox($paths[3]);
|
|
case 'follow':
|
|
return $this->followAdmin($paths[3]);
|
|
case 'users':
|
|
switch (sizeof($paths)) {
|
|
case 4:
|
|
return $this->getUser($paths[3]);
|
|
case 5:
|
|
switch ($paths[4]) {
|
|
case 'inbox':
|
|
return $this->getInbox($paths[3]);
|
|
case 'outbox':
|
|
return $this->getOutbox($paths[3]);
|
|
case 'following':
|
|
return $this->getFollowing($paths[3]);
|
|
case 'followers':
|
|
return $this->getFollowing($paths[3]);
|
|
default:
|
|
break;
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
case 'POST':
|
|
switch (sizeof($paths)) {
|
|
case 3:
|
|
switch ($paths[2]) {
|
|
case 'moo':
|
|
return $this->postDummy();
|
|
case 'sharedInbox':
|
|
return $this->postSharedInbox();
|
|
default:
|
|
break;
|
|
}
|
|
break;
|
|
case 4:
|
|
case 5:
|
|
switch ($paths[2]) {
|
|
case 'inbox':
|
|
return $this->postInbox($paths[3]);
|
|
case 'follow':
|
|
return $this->followAdmin($paths[3]);
|
|
case 'users':
|
|
switch (sizeof($paths)) {
|
|
case 5:
|
|
switch ($paths[4]) {
|
|
case 'inbox':
|
|
return $this->postInbox($paths[3]);
|
|
case 'outbox':
|
|
return $this->postOutbox($paths[3]);
|
|
case 'following':
|
|
return $this->postFollowing($paths[3]);
|
|
case 'followers':
|
|
return $this->postFollowing($paths[3]);
|
|
default:
|
|
break;
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
$this->main->setResponseCode(404);
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* get function for "/v1/dummy/moo"
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function getDummy()
|
|
{
|
|
$this->message = json_encode([
|
|
'r1' => ' (__) ',
|
|
'r2' => ' `------(oo) ',
|
|
'r3' => ' || __ (__) ',
|
|
'r4' => ' ||w || ',
|
|
'r5' => ' '
|
|
], JSON_PRETTY_PRINT);
|
|
return true;
|
|
}
|
|
|
|
public function getUser($_name)
|
|
{
|
|
error_log("Someone tried to get user: " . $_name);
|
|
$user = \Federator\DIO\User::getUserByName(
|
|
$this->main->getDatabase(),
|
|
$_name,
|
|
$this->main->getConnector(),
|
|
$this->main->getCache()
|
|
);
|
|
if ($user === false || $user->id === null) {
|
|
throw new \Federator\Exceptions\FileNotFound();
|
|
}
|
|
$publicKeyPem = <<<PEM
|
|
-----BEGIN PUBLIC KEY-----
|
|
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1MDmIPDcTey9lNYicfho
|
|
u3EeVKeQkm1FkFl4Yoj1FW0SFyGkgtPr8hgAL1JIqyrFokgbPRtihmhTUHaQNoV8
|
|
Uj5UIKG6zM1y1dHWizqwQw13pSMWri3IcSf08GSiolYBb19A98EMzIyGZHzjlfw8
|
|
VAhW6qL6ML5YAR2YvckRRpS4pPVseQLHfDzkWlyXePJQInMai0kdrH39XiXw8B0C
|
|
ver+7I1Z3rzJu+iOLmlblekFJtWDiipjuMedzluL3mNwV9Lk1ka1m7vHrtyqjtv1
|
|
X5FLRXVzpFziJsIpWZ6ojU9KRX8l4yvv9FL4dZIn7edbcosvnNDpnvEl+NsGnf4R
|
|
1wIDAQAB
|
|
-----END PUBLIC KEY-----
|
|
PEM;
|
|
$publicKeyPemJsonSafe = json_encode($publicKeyPem); // gives string with \n inside
|
|
$data = [
|
|
'iconMediaType' => $user->iconMediaType,
|
|
'iconURL' => $user->iconURL,
|
|
'imageMediaType' => $user->imageMediaType,
|
|
'imageURL' => $user->imageURL,
|
|
'fqdn' => '192.168.178.143',
|
|
'name' => $user->name,
|
|
'username' => $user->id,
|
|
'publickey' => "<placeholderPublicKey>",
|
|
'registered' => gmdate('Y-m-d\TH:i:s\Z', $user->registered), // 2021-03-25T00:00:00Z
|
|
'summary' => $user->summary,
|
|
'type' => "Person"
|
|
];
|
|
$this->message = $this->main->renderTemplate('user.json', $data);
|
|
$fixedJson = str_replace(
|
|
'https://192.168.178.143/users/yannis_test',
|
|
'https://192.168.178.143/api/federator/v1/dummy/users/yannis_test',
|
|
$this->message
|
|
);
|
|
$fixedJson = preg_replace(
|
|
'/"id"\s*:\s*"[^"]+"/',
|
|
'"id": "http://192.168.178.143/api/federator/v1/dummy/users/yannis_test"',
|
|
$fixedJson
|
|
);
|
|
$fixedJson = preg_replace(
|
|
'/"inbox"\s*:\s*"[^"]+"/',
|
|
'"inbox": "http://192.168.178.143/users/yannis_test/inbox"',
|
|
$fixedJson
|
|
);
|
|
$fixedJson = str_replace(
|
|
'https://192.168.178.143',
|
|
'http://192.168.178.143',
|
|
$fixedJson
|
|
);
|
|
$fixedJson = str_replace(
|
|
'"<placeholderPublicKey>"',
|
|
$publicKeyPemJsonSafe,
|
|
$fixedJson
|
|
);
|
|
$fixedJson = str_replace(
|
|
'http://192.168.178.143/inbox',
|
|
'http://192.168.178.143/api/federator/fedusers/inbox',
|
|
$fixedJson
|
|
);
|
|
// $fixedJson = str_replace(
|
|
// 'http://192.168.178.143/api/federator/v1/dummy/users/yannis_test@192.168.178.143#main-key',
|
|
// 'http://192.168.178.143/api/federator/v1/dummy/users/yannis_test@192.168.178.143/key#main-key',
|
|
// $fixedJson
|
|
// );
|
|
|
|
$this->message = $fixedJson;
|
|
return true;
|
|
}
|
|
|
|
public function followAdmin($_name)
|
|
{
|
|
$user = \Federator\DIO\User::getUserByName(
|
|
$this->main->getDatabase(),
|
|
$_name,
|
|
$this->main->getConnector(),
|
|
$this->main->getCache()
|
|
);
|
|
if ($user === false || $user->id === null) {
|
|
throw new \Federator\Exceptions\FileNotFound();
|
|
}
|
|
|
|
|
|
// Step 2: Prepare the Follow activity
|
|
$activityData = [
|
|
'@context' => 'https://www.w3.org/ns/activitystreams',
|
|
'type' => 'Follow',
|
|
'actor' => 'http://192.168.178.143/api/federator/v1/dummy/users/' . $_name, // Your user URL
|
|
'object' => 'http://mastodon.local/users/admin' // Mastodon user to follow (e.g., http://mastodon.local/users/admin)
|
|
];
|
|
|
|
// Step 3: Send the Follow activity to Mastodon
|
|
$inboxUrl = 'http://mastodon.local/users/admin/inbox'; // The inbox URL for the Mastodon user
|
|
$this->sendFollowActivityToMastodon($inboxUrl, $activityData);
|
|
|
|
$this->message = "\n";
|
|
return true;
|
|
}
|
|
|
|
private function sendFollowActivityToMastodon($url, $data)
|
|
{
|
|
$json = json_encode($data, JSON_UNESCAPED_SLASHES);
|
|
$digest = 'SHA-256=' . base64_encode(hash('sha256', $json, true));
|
|
$date = gmdate('D, d M Y H:i:s') . ' GMT';
|
|
$parsed = parse_url($url);
|
|
$host = $parsed['host'];
|
|
$path = $parsed['path'];
|
|
|
|
// Build the signature string
|
|
$signatureString = "(request-target): post {$path}\n" .
|
|
"host: {$host}\n" .
|
|
"date: {$date}\n" .
|
|
"digest: {$digest}";
|
|
|
|
// Load your private key here (replace with how you store keys)
|
|
$privateKey = "REDACTED"; // OR from DB
|
|
$pkeyId = openssl_pkey_get_private($privateKey);
|
|
|
|
if (!$pkeyId) {
|
|
throw new \Exception('Invalid private key');
|
|
}
|
|
|
|
openssl_sign($signatureString, $signature, $pkeyId, OPENSSL_ALGO_SHA256);
|
|
$signature_b64 = base64_encode($signature);
|
|
|
|
// Build keyId (public key ID from your actor object)
|
|
$keyId = 'http://192.168.178.143/api/federator/v1/dummy/users/yannis_test#main-key';
|
|
|
|
$signatureHeader = 'keyId="' . $keyId . '",algorithm="rsa-sha256",headers="(request-target) host date digest",signature="' . $signature_b64 . '"';
|
|
|
|
$headers = [
|
|
'Host: ' . $host,
|
|
'Date: ' . $date,
|
|
'Digest: ' . $digest,
|
|
'Content-Type: application/activity+json',
|
|
'Signature: ' . $signatureHeader,
|
|
'Accept: application/activity+json',
|
|
];
|
|
|
|
$ch = curl_init($url);
|
|
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);
|
|
$err = curl_error($ch);
|
|
curl_close($ch);
|
|
|
|
$response = curl_exec($ch);
|
|
curl_close($ch);
|
|
|
|
// Log the response for debugging if needed
|
|
if ($response === false) {
|
|
error_log("Failed to send Follow activity to Mastodon: " . curl_error($ch));
|
|
echo "Failed to send Follow activity to Mastodon: " . curl_error($ch);
|
|
} else {
|
|
$httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
if ($httpcode !== 200 && $httpcode !== 202) {
|
|
throw new \Exception("Unexpected HTTP code $httpcode: $response");
|
|
}
|
|
error_log("Follow activity response from Mastodon: " . $response);
|
|
echo "Follow activity response from Mastodon: " . $response;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* post function for /v1/dummy/moo"
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function postDummy()
|
|
{
|
|
return $this->getDummy();
|
|
}
|
|
|
|
public function getInbox($_name)
|
|
{
|
|
$_rawInput = file_get_contents('php://input');
|
|
|
|
// Decode if it's JSON (as Mastodon usually sends JSON)
|
|
$jsonData = json_decode($_rawInput, true);
|
|
error_log("=== Masto GET Inbox Raw ===\n" . $_rawInput);
|
|
error_log("=== Masto GET Inbox JSON ===\n" . print_r($jsonData, true));
|
|
// Save the raw input and parsed JSON to a file for inspection
|
|
file_put_contents(
|
|
__DIR__ . '/inbox_log.txt',
|
|
time() . ": ==== Masto GET Inbox RAW ====\n" . $_rawInput . "\n\n==== Masto GET Inbox JSON ====\n" . print_r($jsonData, true) . "\n\n",
|
|
FILE_APPEND
|
|
);
|
|
|
|
$this->message = json_encode([
|
|
'status' => 'received',
|
|
]);
|
|
return true;
|
|
}
|
|
|
|
public function getOutbox($_name)
|
|
{
|
|
$_rawInput = file_get_contents('php://input');
|
|
|
|
// Decode if it's JSON (as Mastodon usually sends JSON)
|
|
$jsonData = json_decode($_rawInput, true);
|
|
error_log("=== Masto GET Outbox Raw ===\n" . $_rawInput);
|
|
error_log("=== Masto GET Outbox JSON ===\n" . print_r($jsonData, true));
|
|
// Save the raw input and parsed JSON to a file for inspection
|
|
file_put_contents(
|
|
__DIR__ . '/outbox_log.txt',
|
|
time() . ": ==== Masto GET Outbox RAW ====\n" . $_rawInput . "\n\n==== Masto GET Outbox JSON ====\n" . print_r($jsonData, true) . "\n\n",
|
|
FILE_APPEND
|
|
);
|
|
|
|
$this->message = json_encode([
|
|
'status' => 'received',
|
|
]);
|
|
return true;
|
|
}
|
|
|
|
public function getFollowing($_name)
|
|
{
|
|
$_rawInput = file_get_contents('php://input');
|
|
|
|
// Decode if it's JSON (as Mastodon usually sends JSON)
|
|
$jsonData = json_decode($_rawInput, true);
|
|
error_log("=== Masto GET Following Raw ===\n" . $_rawInput);
|
|
error_log("=== Masto GET Following JSON ===\n" . print_r($jsonData, true));
|
|
// Save the raw input and parsed JSON to a file for inspection
|
|
file_put_contents(
|
|
__DIR__ . '/following_log.txt',
|
|
time() . ": ==== Masto GET Following RAW ====\n" . $_rawInput . "\n\n==== Masto GET Following JSON ====\n" . print_r($jsonData, true) . "\n\n",
|
|
FILE_APPEND
|
|
);
|
|
|
|
$this->message = json_encode([
|
|
'status' => 'received',
|
|
]);
|
|
return true;
|
|
}
|
|
|
|
public function getFollowers($_name)
|
|
{
|
|
$_rawInput = file_get_contents('php://input');
|
|
|
|
// Decode if it's JSON (as Mastodon usually sends JSON)
|
|
$jsonData = json_decode($_rawInput, true);
|
|
error_log("=== Masto GET Followers Raw ===\n" . $_rawInput);
|
|
error_log("=== Masto GET Followers JSON ===\n" . print_r($jsonData, true));
|
|
// Save the raw input and parsed JSON to a file for inspection
|
|
file_put_contents(
|
|
__DIR__ . '/followers_log.txt',
|
|
time() . ": ==== Masto GET Followers RAW ====\n" . $_rawInput . "\n\n==== Masto GET Followers JSON ====\n" . print_r($jsonData, true) . "\n\n",
|
|
FILE_APPEND
|
|
);
|
|
|
|
$this->message = json_encode([
|
|
'status' => 'received',
|
|
]);
|
|
return true;
|
|
}
|
|
|
|
public function getSharedInbox()
|
|
{
|
|
$_rawInput = file_get_contents('php://input');
|
|
|
|
// Decode if it's JSON (as Mastodon usually sends JSON)
|
|
$jsonData = json_decode($_rawInput, true);
|
|
error_log("=== Masto GET SharedInbox Raw ===\n" . $_rawInput);
|
|
error_log("=== Masto GET SharedInbox JSON ===\n" . print_r($jsonData, true));
|
|
// Save the raw input and parsed JSON to a file for inspection
|
|
file_put_contents(
|
|
__DIR__ . '/sharedInbox_log.txt',
|
|
time() . ": ==== Masto GET SharedInbox RAW ====\n" . $_rawInput . "\n\n==== Masto GET SharedInbox JSON ====\n" . print_r($jsonData, true) . "\n\n",
|
|
FILE_APPEND
|
|
);
|
|
|
|
$this->message = json_encode([
|
|
'status' => 'received',
|
|
]);
|
|
return true;
|
|
}
|
|
|
|
public function postInbox($_name)
|
|
{
|
|
$_rawInput = file_get_contents('php://input');
|
|
|
|
// Decode if it's JSON (as Mastodon usually sends JSON)
|
|
$jsonData = json_decode($_rawInput, true);
|
|
error_log("=== Masto POST Inbox Raw ===\n" . $_rawInput);
|
|
error_log("=== Masto POST Inbox JSON ===\n" . print_r($jsonData, true));
|
|
// Save the raw input and parsed JSON to a file for inspection
|
|
file_put_contents(
|
|
__DIR__ . '/inbox_log.txt',
|
|
time() . ": ==== Masto POST Inbox RAW ====\n" . $_rawInput . "\n\n==== Masto POST Inbox JSON ====\n" . print_r($jsonData, true) . "\n\n",
|
|
FILE_APPEND
|
|
);
|
|
|
|
$this->message = json_encode([
|
|
'status' => 'received',
|
|
]);
|
|
return true;
|
|
}
|
|
|
|
public function postOutbox($_name)
|
|
{
|
|
$_rawInput = file_get_contents('php://input');
|
|
|
|
// Decode if it's JSON (as Mastodon usually sends JSON)
|
|
$jsonData = json_decode($_rawInput, true);
|
|
error_log("=== Masto POST Outbox Raw ===\n" . $_rawInput);
|
|
error_log("=== Masto POST Outbox JSON ===\n" . print_r($jsonData, true));
|
|
// Save the raw input and parsed JSON to a file for inspection
|
|
file_put_contents(
|
|
__DIR__ . '/outbox_log.txt',
|
|
time() . ": ==== Masto POST Outbox RAW ====\n" . $_rawInput . "\n\n==== Masto POST Outbox JSON ====\n" . print_r($jsonData, true) . "\n\n",
|
|
FILE_APPEND
|
|
);
|
|
|
|
$this->message = json_encode([
|
|
'status' => 'received',
|
|
]);
|
|
return true;
|
|
}
|
|
|
|
public function postFollowing($_name)
|
|
{
|
|
$_rawInput = file_get_contents('php://input');
|
|
|
|
// Decode if it's JSON (as Mastodon usually sends JSON)
|
|
$jsonData = json_decode($_rawInput, true);
|
|
error_log("=== Masto POST Following Raw ===\n" . $_rawInput);
|
|
error_log("=== Masto POST Following JSON ===\n" . print_r($jsonData, true));
|
|
// Save the raw input and parsed JSON to a file for inspection
|
|
file_put_contents(
|
|
__DIR__ . '/following_log.txt',
|
|
time() . ": ==== Masto POST Following RAW ====\n" . $_rawInput . "\n\n==== Masto POST Following JSON ====\n" . print_r($jsonData, true) . "\n\n",
|
|
FILE_APPEND
|
|
);
|
|
|
|
$this->message = json_encode([
|
|
'status' => 'received',
|
|
]);
|
|
return true;
|
|
}
|
|
|
|
public function postFollowers($_name)
|
|
{
|
|
$_rawInput = file_get_contents('php://input');
|
|
|
|
// Decode if it's JSON (as Mastodon usually sends JSON)
|
|
$jsonData = json_decode($_rawInput, true);
|
|
error_log("=== Masto POST Followers Raw ===\n" . $_rawInput);
|
|
error_log("=== Masto POST Followers JSON ===\n" . print_r($jsonData, true));
|
|
// Save the raw input and parsed JSON to a file for inspection
|
|
file_put_contents(
|
|
__DIR__ . '/followers_log.txt',
|
|
time() . ": ==== Masto POST Followers RAW ====\n" . $_rawInput . "\n\n==== Masto POST Followers JSON ====\n" . print_r($jsonData, true) . "\n\n",
|
|
FILE_APPEND
|
|
);
|
|
|
|
$this->message = json_encode([
|
|
'status' => 'received',
|
|
]);
|
|
return true;
|
|
}
|
|
|
|
public function postSharedInbox()
|
|
{
|
|
$_rawInput = file_get_contents('php://input');
|
|
|
|
// Decode if it's JSON (as Mastodon usually sends JSON)
|
|
$jsonData = json_decode($_rawInput, true);
|
|
error_log("=== Masto POST SharedInbox Raw ===\n" . $_rawInput);
|
|
error_log("=== Masto POST SharedInbox JSON ===\n" . print_r($jsonData, true));
|
|
// Save the raw input and parsed JSON to a file for inspection
|
|
file_put_contents(
|
|
__DIR__ . '/sharedInbox_log.txt',
|
|
time() . ": ==== Masto POST SharedInbox RAW ====\n" . $_rawInput . "\n\n==== Masto POST SharedInbox JSON ====\n" . print_r($jsonData, true) . "\n\n",
|
|
FILE_APPEND
|
|
);
|
|
|
|
$this->message = json_encode([
|
|
'status' => 'received',
|
|
]);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* get internal represenation as json string
|
|
*
|
|
* @return string json string
|
|
*/
|
|
public function toJson()
|
|
{
|
|
return $this->message;
|
|
}
|
|
}
|