Files
Alcea 30de670dce Add files via upload
Set indexable to true too

    $profileObject = [
        '@context' => 'https://www.w3.org/ns/activitystreams',
        'id' => "$baseUrl/$username",
        'type' => 'Person',
        'name' => 'Alcea Bot',
        'discoverable'  => true,  // Update the discoverable field
        'locked'        => false,
        'bot'           => false,
        // Add any other fields you need to update
        'preferredUsername' => $username,
        'summary' => formatDescriptionLinks($description),
        'icon' => [
            'type' => 'Image',
            'mediaType' => 'image/gif',
            'url' => $imageUrl,
        ],
        'inbox' => "$baseUrl/$username/inbox",
        'outbox' => "$baseUrl/$username/outbox",
        'followers' => "$baseUrl/$username/followers",
        'publicKey' => [
            'id' => "$baseUrl/$username#main-key",
            'owner' => "$baseUrl/$username",
            'publicKeyPem' => @file_get_contents(__DIR__ . '/public.pem'),
'publicKeyPem' => @file_get_contents(__DIR__ . '/public.pem'),
        ],
        'fields' => $fields,
        'discoverability' => 'public', // <-- Here is the key for discoverability
        'indexable' => 'true',
    ];
2025-07-20 14:17:59 +02:00

312 lines
9.8 KiB
PHP

<?php
// ActivityPub main script for alceawis.com
file_put_contents(__DIR__ . '/debug.log', "[" . date('c') . "] --- New Request: {$_SERVER['REQUEST_METHOD']} {$_SERVER['REQUEST_URI']} ---\n", FILE_APPEND);
require_once __DIR__ . '/vendor/autoload.php';
use ActivityPhp\Server;
$domain = 'alceawis.com';
$username = 'alceawis';
$baseUrl = "https://$domain";
$server = new Server();
$dataFile = __DIR__ . '/data_alcea.json';
$profileInfoFile = __DIR__ . '/profileinfo.json';
$followersFile = __DIR__ . '/followers.json';
$privateKeyFile = __DIR__ . '/private.pem';
$publicKeyFile = __DIR__ . '/public.pem';
$logFile = __DIR__ . '/debug.log';
$inboxLogFile = __DIR__ . '/inbox.log';
// ------------------- Helper Functions --------------------
function logDebug($msg) {
global $logFile;
file_put_contents($logFile, "[" . date('c') . "] $msg\n", FILE_APPEND);
}
function logInbox($msg) {
global $inboxLogFile;
file_put_contents($inboxLogFile, "[" . date('c') . "] $msg\n", FILE_APPEND);
}
function formatDescriptionLinks(string $text): string {
$escaped = htmlspecialchars($text, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
return nl2br(preg_replace('~(https?://[^\s<]+)~i', '<a href="$1" target="_blank" rel="nofollow noopener noreferrer">$1</a>', $escaped));
}
function discoverInbox(string $actorUrl): ?string {
$actorUrl = rtrim($actorUrl, '/') . '.json';
$ctx = stream_context_create([
'http' => [
'method' => 'GET',
'header' => "Accept: application/activity+json, application/ld+json\r\n",
'timeout' => 5,
]
]);
$json = @file_get_contents($actorUrl, false, $ctx);
if (!$json) {
logInbox("Failed to fetch actor JSON from $actorUrl");
return null;
}
$actor = json_decode($json, true);
if (!$actor) {
logInbox("Invalid JSON from actor URL $actorUrl");
return null;
}
return $actor['inbox'] ?? null;
}
function sendSignedRequest(string $inboxUrl, array $body): bool {
global $privateKeyFile, $username, $domain, $baseUrl;
$privateKeyPem = @file_get_contents($privateKeyFile);
if (!$privateKeyPem) {
logInbox("Private key file not found or unreadable.");
return false;
}
$bodyJson = json_encode($body, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
if ($bodyJson === false) {
logInbox("Failed to encode JSON body for sending.");
return false;
}
$digest = 'SHA-256=' . base64_encode(hash('sha256', $bodyJson, true));
$date = gmdate('D, d M Y H:i:s \G\M\T');
$urlParts = parse_url($inboxUrl);
$host = $urlParts['host'] ?? '';
$path = $urlParts['path'] ?? '/';
$signatureString = "(request-target): post $path\nhost: $host\ndate: $date\ndigest: $digest";
$privateKey = openssl_pkey_get_private($privateKeyPem);
if (!$privateKey) {
logInbox("Failed to load private key for signing.");
return false;
}
$signatureOk = openssl_sign($signatureString, $signature, $privateKey, OPENSSL_ALGO_SHA256);
openssl_free_key($privateKey);
if (!$signatureOk) {
logInbox("Failed to create signature.");
return false;
}
$signatureB64 = base64_encode($signature);
$keyId = "$baseUrl/$username#main-key";
$signatureHeader = sprintf(
'keyId="%s",algorithm="rsa-sha256",headers="(request-target) host date digest",signature="%s"',
$keyId,
$signatureB64
);
$headers = [
"Host: $host",
"Date: $date",
"Digest: $digest",
"Signature: $signatureHeader",
"Content-Type: application/activity+json"
];
$ch = curl_init($inboxUrl);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $bodyJson,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
]);
$response = curl_exec($ch);
$curlError = curl_error($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($curlError) {
logInbox("cURL error sending to $inboxUrl: $curlError");
return false;
}
logInbox("Sent activity to $inboxUrl, HTTP $httpCode, response: " . substr($response, 0, 300));
return $httpCode >= 200 && $httpCode < 300;
}
function sendCreateActivity(array $object) {
global $baseUrl, $username, $followersFile;
$activity = [
'@context' => 'https://www.w3.org/ns/activitystreams',
'id' => "$baseUrl/$username",
'discoverable' => true, // Update the discoverable field
'indexable' => true,
'locked' => false,
'bot' => false,
'id' => $object['id'] . '/activity',
'type' => 'Create',
'actor' => "$baseUrl/$username",
'object' => $object,
'to' => ['https://www.w3.org/ns/activitystreams#Public'],
];
$followers = [];
if (file_exists($followersFile)) {
$followers = json_decode(file_get_contents($followersFile), true);
if (!is_array($followers)) {
logDebug("Invalid JSON in followers.json");
$followers = [];
}
}
foreach ($followers as $followerUrl) {
$inbox = discoverInbox($followerUrl);
if ($inbox) {
sendSignedRequest($inbox, $activity);
} else {
logInbox("No inbox found for follower $followerUrl");
}
}
}
// ---------------- Update Profile on Fediverse (No checks, every run) -----------------
function updateProfile() {
global $profileInfoFile, $baseUrl, $username;
if (!file_exists($profileInfoFile)) {
logDebug("profileinfo.json file not found");
return false;
}
$profileData = json_decode(file_get_contents($profileInfoFile), true);
if (!$profileData) {
logDebug("Invalid JSON in profileinfo.json");
return false;
}
$description = $profileData['description'] ?? '';
$imageUrl = $profileData['image_url'] ?? '';
$links = $profileData['links'] ?? [];
// Build Mastodon profile fields from links, skipping empty ones
$fields = [];
foreach ($links as $link) {
$name = trim($link['name'] ?? '');
$url = trim($link['url'] ?? '');
if ($name !== '' && $url !== '') {
$safeUrl = htmlspecialchars($url, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
$fields[] = [
'name' => $name,
'value' => "<a href=\"$safeUrl\" target=\"_blank\" rel=\"nofollow noopener noreferrer\">$safeUrl</a>",
];
}
}
$profileObject = [
'@context' => 'https://www.w3.org/ns/activitystreams',
'id' => "$baseUrl/$username",
'type' => 'Person',
'name' => 'Alcea Bot',
'discoverable' => true, // Update the discoverable field
'locked' => false,
'bot' => false,
// Add any other fields you need to update
'preferredUsername' => $username,
'summary' => formatDescriptionLinks($description),
'icon' => [
'type' => 'Image',
'mediaType' => 'image/gif',
'url' => $imageUrl,
],
'inbox' => "$baseUrl/$username/inbox",
'outbox' => "$baseUrl/$username/outbox",
'followers' => "$baseUrl/$username/followers",
'publicKey' => [
'id' => "$baseUrl/$username#main-key",
'owner' => "$baseUrl/$username",
'publicKeyPem' => @file_get_contents(__DIR__ . '/public.pem'),
'publicKeyPem' => @file_get_contents(__DIR__ . '/public.pem'),
],
'fields' => $fields,
'discoverability' => 'public', // <-- Here is the key for discoverability
'indexable' => 'true',
];
$followers = [];
$followersFile = __DIR__ . '/followers.json';
if (file_exists($followersFile)) {
$followers = json_decode(file_get_contents($followersFile), true);
if (!is_array($followers)) {
logDebug("Invalid JSON in followers.json");
$followers = [];
}
}
$updateActivity = [
'@context' => 'https://www.w3.org/ns/activitystreams',
'id' => "$baseUrl/$username#update-" . time(),
'type' => 'Update',
'actor' => "$baseUrl/$username",
'object' => $profileObject,
'to' => ['https://www.w3.org/ns/activitystreams#Public'],
];
foreach ($followers as $followerUrl) {
$inbox = discoverInbox($followerUrl);
if ($inbox) {
logDebug("Pushing profile Update activity to $inbox");
sendSignedRequest($inbox, $updateActivity);
} else {
logDebug("No inbox discovered for follower $followerUrl");
}
}
return true;
}
// ------------- Federate Outbox items -------------------
function federateOutbox() {
global $dataFile;
if (!file_exists($dataFile)) {
logDebug("Outbox data file not found");
return false;
}
$data = json_decode(file_get_contents($dataFile), true);
if (!$data || !is_array($data)) {
logDebug("Invalid JSON in outbox data file");
return false;
}
foreach ($data as $note) {
if (!isset($note['id'])) {
logDebug("Skipping note without id");
continue;
}
sendCreateActivity($note);
}
return true;
}
// ---------------------- Main Execution --------------------
updateProfile(); // Push profile update every run
federateOutbox(); // Push all outbox activities every run
// -------------------- Serve ActivityPub requests --------------------
try {
$response = $server->run();
header('Content-Type: application/activity+json; charset=utf-8');
http_response_code($response->getStatusCode());
echo $response->getBody();
} catch (Throwable $e) {
// Ignore errors from run(), just log and continue
logDebug("Ignored error in server run(): " . $e->getMessage());
// No output here, continue silently
}