ensureCliAccess(); $this->mastodonAccessToken = getenv('MASTODON_ACCESS_TOKEN') ?: $_ENV['MASTODON_ACCESS_TOKEN'] ?? ''; $this->httpClient = $httpClient ?: new Client(); $this->validateAuthorization(); } private function validateAuthorization(): void { $authHeader = $_SERVER['HTTP_AUTHORIZATION'] ?? ''; $expectedToken = 'Bearer '.getenv('MASTODON_SYNDICATION_TOKEN'); if ($authHeader !== $expectedToken) { http_response_code(401); echo json_encode(['error' => 'Unauthorized']); exit(); } } public function handlePost(): void { if (! $this->isDatabaseAvailable()) { echo "Database is unavailable. Exiting.\n"; return; } $latestItems = $this->fetchRSSFeed($this->rssFeedUrl); foreach (array_reverse($latestItems) as $item) { $existing = $this->fetchFromApi('mastodon_posts', 'link=eq.'.urlencode($item['link'])); if (! empty($existing)) { continue; } $content = $this->truncateContent( $item['title'], strip_tags($item['description']), $item['link'], 500 ); $timestamp = date('Y-m-d H:i:s'); if (! $this->storeInDatabase($item['link'], $timestamp)) { echo "Skipping post: database write failed for {$item['link']}\n"; continue; } $postedUrl = $this->postToMastodon($content, $item['image'] ?? null); if ($postedUrl) { echo "Posted: {$postedUrl}\n"; } else { echo "Failed to post to Mastodon for: {$item['link']}\n"; } } echo "RSS processed successfully.\n"; } private function fetchRSSFeed(string $rssFeedUrl): array { $rssText = file_get_contents($rssFeedUrl); if (! $rssText) { throw new \Exception('Failed to fetch RSS feed.'); } $rss = new \SimpleXMLElement($rssText); $items = []; foreach ($rss->channel->item as $item) { $items[] = [ 'title' => $this->cleanText((string) $item->title), 'link' => (string) $item->link, 'description' => $this->cleanText((string) $item->description), ]; } return $items; } private function cleanText(string $text): string { $decoded = html_entity_decode($text, ENT_QUOTES | ENT_XML1, 'UTF-8'); return mb_convert_encoding($decoded, 'UTF-8', 'UTF-8'); } private function postToMastodon(string $content): ?string { $headers = [ 'Authorization' => "Bearer {$this->mastodonAccessToken}", 'Content-Type' => 'application/json', ]; $postData = ['status' => $content]; $response = $this->httpClient->request('POST', self::MASTODON_API_STATUS, [ 'headers' => $headers, 'json' => $postData, ]); if ($response->getStatusCode() >= 400) { throw new \Exception("Mastodon post failed: {$response->getBody()}"); } $body = json_decode($response->getBody()->getContents(), true); return $body['url'] ?? null; } private function storeInDatabase(string $link, string $timestamp): bool { try { $this->makeRequest('POST', 'mastodon_posts', [ 'json' => [ 'link' => $link, 'created_at' => $timestamp, ], ]); return true; } catch (\Exception $e) { echo 'Error storing post in DB: '.$e->getMessage()."\n"; return false; } } private function isDatabaseAvailable(): bool { try { $response = $this->fetchFromApi('mastodon_posts', 'limit=1'); return is_array($response); } catch (\Exception $e) { echo 'Database check failed: '.$e->getMessage()."\n"; return false; } } private function truncateContent(string $title, string $description, string $link, int $maxLength): string { $baseLength = strlen("$title\n\n$link"); $available = $maxLength - $baseLength - 4; if (strlen($description) > $available) { $description = substr($description, 0, $available); $description = preg_replace('/\s+\S*$/', '', $description).'...'; } return "$title\n\n$description\n\n$link"; } } try { $handler = new MastodonPostHandler(); $handler->handlePost(); } catch (\Exception $e) { http_response_code(500); echo json_encode(['error' => $e->getMessage()]); }