' . PHP_EOL; echo '' . PHP_EOL; echo '
' . PHP_EOL;} ?> " . trim($m[1]) . ""; }, $text); } function formatDescriptionLinks(string $text): string { $escaped = htmlspecialchars($text); $html = preg_replace('~(https?://[^\s<]+)~i', '$1', $escaped); return nl2br($html); } function discoverInbox($actorUrl) { if (!str_ends_with($actorUrl, '.json')) $actorUrl = rtrim($actorUrl, '/') . '.json'; $ctx = stream_context_create(['http' => ['method' => 'GET','header' => "Accept: application/activity+json, application/ld+json\r\n"]]); $json = @file_get_contents($actorUrl, false, $ctx); if (!$json) { file_put_contents(__DIR__ . '/inbox.log', "[" . date('c') . "] Failed to fetch actor JSON from $actorUrl\n", FILE_APPEND); return null; } $actor = json_decode($json, true); return $actor['inbox'] ?? null; } function sendSignedRequest($inboxUrl, $body) { $keyId = "https://alceawis.com/alceawis#main-key"; $privateKeyPem = file_get_contents(__DIR__ . '/private.pem'); $date = gmdate('D, d M Y H:i:s \G\M\T'); $bodyJson = json_encode($body, JSON_UNESCAPED_SLASHES); $digest = 'SHA-256=' . base64_encode(hash('sha256', $bodyJson, true)); $p = parse_url($inboxUrl); $signatureStr = "(request-target): post {$p['path']}\nhost: {$p['host']}\ndate: $date\ndigest: $digest"; openssl_sign($signatureStr, $sig, $privateKeyPem, OPENSSL_ALGO_SHA256); $sigHeader = 'keyId="' . $keyId . '",algorithm="rsa-sha256",headers="(request-target) host date digest",signature="' . base64_encode($sig) . '"'; $hdrs = [ "Host: {$p['host']}", "Date: $date", "Digest: $digest", "Signature: $sigHeader", "Content-Type: application/activity+json" ]; // Debugging: Log the headers and request body file_put_contents(__DIR__ . '/debug.log', "[" . date('c') . "] Sending request to: $inboxUrl\n", FILE_APPEND); file_put_contents(__DIR__ . '/debug.log', "[" . date('c') . "] Headers:\n" . implode("\n", $hdrs) . "\n", FILE_APPEND); file_put_contents(__DIR__ . '/debug.log', "[" . date('c') . "] Body:\n$bodyJson\n\n", FILE_APPEND); $ch = curl_init($inboxUrl); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_POSTFIELDS => $bodyJson, CURLOPT_HTTPHEADER => $hdrs, CURLOPT_RETURNTRANSFER => true ]); $resp = curl_exec($ch); // Debugging: Log the response if (curl_errno($ch)) { file_put_contents(__DIR__ . '/inbox.log', "[" . date('c') . "] cURL error: " . curl_error($ch) . "\n", FILE_APPEND); } else { file_put_contents(__DIR__ . '/debug.log', "[" . date('c') . "] Response: $resp\n\n", FILE_APPEND); } curl_close($ch); return $resp; } function sendCreateActivity(array $note) { global $baseUrl, $username; $activity = [ '@context' => 'https://www.w3.org/ns/activitystreams', 'id' => $note['id'] . '/activity', 'type' => 'Create', 'actor' => "$baseUrl/$username", 'object' => $note, 'to' => ['https://www.w3.org/ns/activitystreams#Public'], ]; $followers = file_exists(__DIR__ . '/followers.json') ? json_decode(file_get_contents(__DIR__ . '/followers.json'), true) : []; foreach ($followers as $follower) { if ($inbox = discoverInbox($follower)) sendSignedRequest($inbox, $activity); } } /* ---------- build outbox ---------- */ $lastNoteId = null; foreach ($data as $entry) { foreach ($entry as $date => $content) { $hash = substr(md5($content['value']), 0, 8); $text = formatEmojis($content['value']); $inReplyTo = null; $mentionTag = null; $mentionAcct = null; if (preg_match('/💬(https?:\/\/[^\s💬]+)💬/', $text, $match)) { $inReplyTo = $match[1]; $lines = explode("\n", $text); array_shift($lines); $text = implode("\n", $lines); } // Extract hashtags $hashtags = array_filter(array_map('trim', explode(',', $content['hashtags'] ?? ''))); // Handle @mention if replying to someone if ($inReplyTo && preg_match('~https?://([^/]+)/@([a-zA-Z0-9_]+)~', $inReplyTo, $matches)) { $mentionDomain = $matches[1]; $mentionUser = $matches[2]; $mentionAcct = "$mentionUser@$mentionDomain"; $mentionUrl = "https://$mentionDomain/@$mentionUser"; $mentionText = "@$mentionAcct "; if (strpos($text, $mentionText) !== 0) { $text = $mentionText . $text; } // Create Mention tag object $mentionTag = [ 'type' => 'Mention', 'href' => $mentionUrl, 'name' => "@$mentionAcct" ]; } // Format quotes and links $quotedText = formatQuotes($text); $htmlText = preg_replace( '~(https?://[^\s<]+)~i', '$1', $quotedText ); // Convert hashtags to tag links $htmlText = preg_replace_callback('/#([\w-]+)/', function($matches) use ($domain) { $tag = $matches[1]; $url = "https://$domain/tags/" . urlencode($tag); return "#" . htmlspecialchars($tag) . ""; }, $htmlText); $htmlText = nl2br($htmlText); // Build hashtag tag objects $tags = array_map(function($tag) use ($domain) { return [ 'type' => 'Hashtag', 'name' => "#$tag", 'href' => "https://$domain/tags/$tag" ]; }, $hashtags); // Extract emoji shortcodes and create tag objects preg_match_all('/:([a-zA-Z0-9_]+):/', $content['value'], $emojiMatches); foreach ($emojiMatches[1] as $shortcode) { $tags[] = [ 'type' => 'Emoji', 'name' => ":$shortcode:", 'icon' => [ 'type' => 'Image', 'mediaType' => 'image/gif', 'url' => "https://$domain/z_files/emojis/$shortcode.gif" ] ]; } // Add mention tag if applicable if ($mentionTag) { $tags[] = $mentionTag; } // Compose Note ID $noteId = "$baseUrl/$username/status/{$date}-$hash"; // Build the Note object $note = [ 'id' => $noteId, 'type' => 'Note', 'published' => date(DATE_ATOM, strtotime($date)), 'attributedTo' => "$baseUrl/$username", 'to' => ['https://www.w3.org/ns/activitystreams#Public'], 'content' => $htmlText, 'contentMap' => [ 'und' => $text, 'html' => $htmlText, ], 'tag' => $tags, ]; // Add inReplyTo if exists if ($inReplyTo) { $note['inReplyTo'] = $inReplyTo; } $outboxItems[] = $note; // Send activity only if not already pushed if (!in_array($noteId, $pushed)) { sendCreateActivity($note); $pushed[] = $noteId; file_put_contents($pushedFile, json_encode($pushed, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); } } } if (!empty($pushed)) { $lastPushedUrl = end($pushed); foreach ($outboxItems as $item) { if ($item['id'] === $lastPushedUrl) { $noteText = $item['contentMap']['und'] ?? '[No text]'; $inReplyTo = $item['inReplyTo'] ?? 'None'; if (!file_exists('outbox.log')) { file_put_contents('outbox.log', ''); } $logEntry = sprintf( "[%s] New post created\nStatus URL: %s\nReply To: %s\nContent:\n%s\n\n", date('Y-m-d H:i:s'), $lastPushedUrl, $inReplyTo, $noteText ); //file_put_contents('outbox.log', $logEntry, FILE_APPEND | LOCK_EX); break; } } } /* ---------- router ---------- */ $uri = explode('?', $_SERVER['REQUEST_URI'] ?? '')[0]; if ($uri === "/$username" || $uri === "/$username/") { header('Content-Type: application/activity+json'); header('Vary: Accept'); $profileFile = __DIR__ . '/profileinfo.json'; $profileData = file_exists($profileFile) ? json_decode(file_get_contents($profileFile), true) : []; $imageUrl = $profileData['image_url'] ?? "$baseUrl/z_files/emojis/alceawis.gif"; $descHtml = formatDescriptionLinks($profileData['description'] ?? ''); $summaryRaw = $profileData['description'] ?? ''; $fields = []; foreach ($profileData['links'] ?? [] as $link) { if (!empty($link['name']) && !empty($link['url'])) { $fields[] = [ 'name' => $link['name'], 'value' => '' . htmlspecialchars($link['url']) . '' ]; } } echo json_encode([ '@context' => [ 'https://www.w3.org/ns/activitystreams', [ 'manuallyApprovesFollowers' => 'as:manuallyApprovesFollowers', 'toot' => 'http://joinmastodon.org/ns#', 'featured' => [ '@id' => 'toot:featured', '@type' => '@id' ] ] ], 'id' => "$baseUrl/$username", 'type' => 'Person', 'name' => 'Alcea Bot', 'preferredUsername' => $username, 'summary' => $descHtml, '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') ], 'manuallyApprovesFollowers' => false, 'bot' => false, 'discoverable' => true, 'indexable' => true, 'group' => false, 'attachment' => $fields ], JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT); exit; } if ($uri === "/$username/outbox" || $uri === "/$username/outbox/") { header('Content-Type: application/activity+json'); if (($_GET['page'] ?? '') === 'true') { echo json_encode([ '@context' => ['https://www.w3.org/ns/activitystreams',['manuallyApprovesFollowers'=>'as:manuallyApprovesFollowers','toot'=>'http://joinmastodon.org/ns#','featured'=>['@id'=>'toot:featured','@type'=>'@id']]], 'id' => "$baseUrl/$username/outbox?page=true", 'type' => 'OrderedCollectionPage', 'partOf' => "$baseUrl/$username/outbox", 'orderedItems' => $outboxItems, ], JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT); } else { echo json_encode([ '@context' => ['https://www.w3.org/ns/activitystreams',['manuallyApprovesFollowers'=>'as:manuallyApprovesFollowers','toot'=>'http://joinmastodon.org/ns#','featured'=>['@id'=>'toot:featured','@type'=>'@id']]], 'id' => "$baseUrl/$username/outbox", 'type' => 'OrderedCollection', 'totalItems' => count($outboxItems), 'first' => "$baseUrl/$username/outbox?page=true" ], JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT); } exit; } if (preg_match('/^\/' . $username . '\/status\/([a-z0-9\-]+)$/', $uri, $m)) { $postId = $m[1]; $post = null; $date = ''; $hash = ''; foreach ($data as $entry) foreach ($entry as $d => $c) if ("$d-" . substr(md5($c['value']), 0, 8) === $postId) { $post = $c; $date = $d; $hash = substr(md5($c['value']), 0, 8); break 2; } if (!$post) { http_response_code(404); echo "Not found"; exit; } // Construct noteId $noteId = "$baseUrl/$username/status/{$date}-$hash"; // Extract content $content = $post['value']; // Prepare tags (hashtags, mentions, emoji tags) $tags = []; $emojiTags = []; $hashtags = []; $mentions = []; // Extract hashtags: #hashtag preg_match_all('/#(\w+)/', $content, $hashtags); foreach ($hashtags[1] as $hashtag) { $tags[] = [ "type" => "Hashtag", "name" => "#$hashtag", "href" => "$baseUrl/tags/$hashtag" ]; } // Extract mentions: @username preg_match_all('/@(\w+)/', $content, $mentions); foreach ($mentions[1] as $mention) { // Not directly used in tags, but can be added if needed } // Extract emoji tags: :emoji: preg_match_all('/:([\w\+\-]+):/', $content, $emojiTags); foreach ($emojiTags[1] as $emoji) { // Assuming emoji images are stored on a path like "/z_files/emojis/{emoji}.gif" $tags[] = [ "type" => "Emoji", "name" => ":$emoji:", "icon" => [ "type" => "Image", "mediaType" => "image/gif", "url" => "$baseUrl/z_files/emojis/$emoji.gif" ] ]; } // Determine if client accepts JSON $accept = $_SERVER['HTTP_ACCEPT'] ?? ''; if (str_contains($accept, 'application/activity+json') || str_contains($accept, 'application/ld+json')) { header('Content-Type: application/activity+json'); echo json_encode([ '@context' => [ 'https://www.w3.org/ns/activitystreams', [ 'manuallyApprovesFollowers' => 'as:manuallyApprovesFollowers', 'toot' => 'http://joinmastodon.org/ns#', 'featured' => [ '@id' => 'toot:featured', '@type' => '@id' ] ] ], 'id' => $noteId, 'type' => 'Note', 'published' => date(DATE_ATOM, strtotime($date)), 'attributedTo' => "$baseUrl/$username", 'to' => ['https://www.w3.org/ns/activitystreams#Public'], 'content' => $content, 'contentMap' => [ 'und' => $content, 'html' => nl2br($content) // Convert newlines to
for HTML ], 'tag' => $tags, ], JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT); exit; } // Otherwise, return an HTML redirect after 1 second header('Content-Type: text/html'); $redirectUrl = "https://alceawis.com#" . $noteId; echo << Redirecting

Redirecting to $redirectUrl

HTML; exit; } if ($uri === "/$username/followers" || $uri === "/$username/followers/") { header('Content-Type: application/activity+json'); header('Vary: Accept'); $followers = file_exists(__DIR__ . '/followers.json') ? json_decode(file_get_contents(__DIR__ . '/followers.json'), true) : []; echo json_encode(['@context'=>'https://www.w3.org/ns/activitystreams','id'=>"$baseUrl/$username/followers",'type'=>'OrderedCollection','totalItems'=>count($followers),'orderedItems'=>$followers], JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT); exit; } /* ----- inbox (merged) ----- */ if ($uri === "/$username/inbox" || $uri === "/$username/inbox/") { if ($_SERVER['REQUEST_METHOD'] !== 'POST') { http_response_code(405); header('Allow: POST'); echo "Method Not Allowed"; exit; } $payload = file_get_contents('php://input'); file_put_contents(__DIR__ . '/inbox.log', "[" . date('c') . "] Received payload:\n$payload\n\n", FILE_APPEND); $activity = json_decode($payload, true); if (!$activity) { file_put_contents(__DIR__ . '/inbox.log', "[" . date('c') . "] ERROR: Could not decode JSON payload.\n\n", FILE_APPEND); http_response_code(400); echo "Bad Request: Invalid JSON"; exit; } /* ---- Follow handling ---- */ if ($activity['type'] === 'Follow') { $follower = $activity['actor'] ?? null; if ($follower) { file_put_contents(__DIR__ . '/followed.log', $follower . "\n", FILE_APPEND); $followersFile = __DIR__ . '/followers.json'; $followers = file_exists($followersFile) ? json_decode(file_get_contents($followersFile), true) : []; if (!in_array($follower, $followers)) { $followers[] = $follower; file_put_contents($followersFile, json_encode($followers, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); } $accept = [ '@context' => 'https://www.w3.org/ns/activitystreams', 'id' => "$baseUrl/$username/accepts/" . uniqid(), 'type' => 'Accept', 'actor' => "$baseUrl/$username", 'object' => $activity, ]; if ($inbox = discoverInbox($follower)) sendSignedRequest($inbox, $accept); else file_put_contents(__DIR__ . '/inbox.log', "[" . date('c') . "] Could not discover inbox for follower: $follower\n", FILE_APPEND); } } /* ---- Like / Announce / Create logging ---- */ if (in_array($activity['type'], ['Like','Announce','Create'])) { $interactions = file_exists($interactionFile) ? json_decode(file_get_contents($interactionFile), true) : []; $interaction = [ 'id' => $activity['id'] ?? uniqid('interaction_'), 'type' => $activity['type'], 'actor' => $activity['actor'] ?? null, 'object' => $activity['object'] ?? null, 'published' => $activity['published'] ?? date(DATE_ATOM), 'received_at' => date(DATE_ATOM), ]; if ($activity['type'] === 'Create') { $mentionsYou = false; if (isset($activity['object']['tag']) && is_array($activity['object']['tag'])) { foreach ($activity['object']['tag'] as $tag) { if ( isset($tag['type'], $tag['href']) && $tag['type'] === 'Mention' && rtrim($tag['href'], '/') === "$baseUrl/$username" ) { $mentionsYou = true; break; } } } if (!empty($activity['object']['inReplyTo']) || $mentionsYou) { $interactions[] = $interaction; } } else { $interactions[] = $interaction; } file_put_contents($interactionFile, json_encode($interactions, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); } http_response_code(202); echo "Accepted"; exit; } http_response_code(404);