service_name = 'bluesky'; $this->title = 'BlueSky'; $this->icon = 'fediverse-logo'; $this->canSync = [ 'initial' => true, 'schedule' => true, ]; $this->fields = [ 'handle' => [ 'type' => 'text', 'placeholder'=> 'your-handle.bsky.social', 'required' => true, 'label' => 'Handle', ], 'password' => [ 'type' => 'text', 'subtype'=>'password', 'label' => 'Application Password', 'required' => true, 'hint' => 'Security: Generate an App Password in your BlueSky settings for better security.' ] ]; parent::__construct($user_id); } protected function initialize(): void { } /** * Setup BlueSky client with credentials */ protected function setupClient(?int $user_id = null): bool { $creds = $this->credentials; if (empty($creds['identifier']) || empty($creds['password'])) { return false; } try { $session = $this->createSession($creds['identifier'], $creds['password']); if ($session) { $this->access_token = $session['accessJwt']; $this->did = $session['did']; return true; } } catch (Exception $e) { $this->handleError($e); } return false; } /** * Create BlueSky session */ private function createSession(string $identifier, string $password): ?array { $response = wp_remote_post($this->api_base . 'com.atproto.server.createSession', [ 'headers' => [ 'Content-Type' => 'application/json', ], 'body' => json_encode([ 'identifier' => $identifier, 'password' => $password ]), 'timeout' => 30 ]); if (is_wp_error($response)) { throw new Exception('Failed to connect to BlueSky: ' . $response->get_error_message()); } $body = json_decode(wp_remote_retrieve_body($response), true); $status_code = wp_remote_retrieve_response_code($response); if ($status_code !== 200) { throw new Exception('BlueSky authentication failed: ' . ($body['message'] ?? 'Unknown error')); } return $body; } /** * Publish a post to BlueSky */ public function publishPost(array $content, ?int $user_id = null): array { try { // Validate content $errors = $this->validateContent($content); if (!empty($errors)) { return [ 'success' => false, 'error' => implode(', ', $errors) ]; } // Setup client for this request if (!$this->setupClient($user_id)) { return [ 'success' => false, 'error' => 'Failed to authenticate with BlueSky' ]; } // Check rate limiting if (!$this->checkRateLimit()) { return [ 'success' => false, 'error' => 'Rate limit exceeded' ]; } // Prepare post data $post_data = [ 'repo' => $this->did, 'collection' => 'app.bsky.feed.post', 'record' => [ 'text' => $this->prepareDescription($content['description'] ?? ''), 'createdAt' => date('c'), '$type' => 'app.bsky.feed.post' ] ]; // Add link if post URL provided if (!empty($content['post_url'])) { $post_data['record']['text'] .= "\n\n" . $content['post_url']; } // Upload and attach image if provided if (!empty($content['image_path']) && file_exists($content['image_path'])) { $image_result = $this->uploadImage($content['image_path']); if ($image_result['success']) { $post_data['record']['embed'] = [ '$type' => 'app.bsky.embed.images', 'images' => [ [ 'alt' => $content['title'] ?? 'Image', 'image' => $image_result['blob'] ] ] ]; } } // Post to BlueSky $response = $this->makeApiRequest('com.atproto.repo.createRecord', $post_data); if ($response['success']) { $this->log("Successfully posted to BlueSky", 'info'); return [ 'success' => true, 'platform_id' => $response['data']['uri'] ?? null, 'post_url' => $this->getPostUrl($response['data']['uri'] ?? ''), 'data' => $response['data'] ]; } return [ 'success' => false, 'error' => $response['error'] ?? 'Unknown error' ]; } catch (Exception $e) { $this->handleError($e); return [ 'success' => false, 'error' => $e->getMessage() ]; } } /** * Validate content before posting */ protected function validateContent(array $content): array { $errors = []; // Check description length if (isset($content['description']) && strlen($content['description']) > $this->max_description_length) { $errors[] = "Description exceeds {$this->max_description_length} characters"; } // Check image if provided if (isset($content['image_path']) && $content['image_path']) { if (!file_exists($content['image_path'])) { $errors[] = "Image file does not exist"; } else { $mime_type = mime_content_type($content['image_path']); $supported_types = ['image/jpeg', 'image/png', 'image/gif']; if (!in_array($mime_type, $supported_types)) { $errors[] = "Unsupported image type: {$mime_type}"; } $file_size = filesize($content['image_path']); $max_size = 5242880; // 5MB if ($file_size > $max_size) { $errors[] = "Image too large: " . round($file_size / 1048576, 2) . "MB (max: 5MB)"; } } } return $errors; } /** * Prepare description with length limits */ protected function prepareDescription(string $description): string { if (strlen($description) <= $this->max_description_length) { return $description; } // Truncate intelligently at word boundaries $truncated = substr($description, 0, $this->max_description_length - 3); $last_space = strrpos($truncated, ' '); if ($last_space !== false) { $truncated = substr($truncated, 0, $last_space); } return $truncated . '...'; } /** * Upload image to BlueSky */ protected function uploadImage(string $image_path): array { try { $mime_type = mime_content_type($image_path); $image_data = file_get_contents($image_path); $response = wp_remote_post($this->api_base . 'com.atproto.repo.uploadBlob', [ 'headers' => [ 'Authorization' => 'Bearer ' . $this->access_token, 'Content-Type' => $mime_type, ], 'body' => $image_data, 'timeout' => 60 ]); if (is_wp_error($response)) { throw new Exception('Failed to upload image: ' . $response->get_error_message()); } $body = json_decode(wp_remote_retrieve_body($response), true); $status_code = wp_remote_retrieve_response_code($response); if ($status_code === 200 && isset($body['blob'])) { return [ 'success' => true, 'blob' => $body['blob'] ]; } return [ 'success' => false, 'error' => $body['message'] ?? 'Image upload failed' ]; } catch (Exception $e) { return [ 'success' => false, 'error' => $e->getMessage() ]; } } /** * Make API request to BlueSky */ private function makeApiRequest(string $endpoint, array $data): array { $response = wp_remote_post($this->api_base . $endpoint, [ 'headers' => [ 'Authorization' => 'Bearer ' . $this->access_token, 'Content-Type' => 'application/json', ], 'body' => json_encode($data), 'timeout' => 30 ]); if (is_wp_error($response)) { return [ 'success' => false, 'error' => $response->get_error_message() ]; } $body = json_decode(wp_remote_retrieve_body($response), true); $status_code = wp_remote_retrieve_response_code($response); if ($status_code === 200) { return [ 'success' => true, 'data' => $body ]; } return [ 'success' => false, 'error' => $body['message'] ?? "HTTP {$status_code} error" ]; } /** * Get post URL from BlueSky URI */ private function getPostUrl(string $uri): string { if (preg_match('/at:\/\/([^\/]+)\/app\.bsky\.feed\.post\/(.+)/', $uri, $matches)) { $did = $matches[1]; $rkey = $matches[2]; return "https://bsky.app/profile/{$did}/post/{$rkey}"; } return ''; } /** * Disconnect user from this platform */ public function disconnectUser(int $user_id): bool { $key = "user_{$user_id}_{$this->service_name}"; $credentials_manager = CredentialsManager::getInstance(); return $credentials_manager->deleteCredentials($key); } /** * Get display name for this platform */ public function getDisplayName(): string { return 'BlueSky'; } /** * Get icon name for this platform */ public function getIconName(): string { return 'fediverse'; // Using existing fediverse icon } /** * Get supported post types */ public function getSupportedPostTypes(): array { return ['text', 'image', 'link']; } public function getServiceDescription(): string { return "Cross-post content to the BlueSky social network."; } protected function getRequestHeaders(): array { // TODO: Implement getRequestHeaders() method. return []; } protected function getApiUrl(string $endpoint, ?string $baseUrl = null): string { return 'https://bsky.social/xrpc/'; } protected function processIntegrationAction(string $action, array $data):\WP_Error|array { return []; } }