<?php
|
namespace JVBase\integrations;
|
|
use Exception;
|
use WP_Error;
|
|
if (!defined('ABSPATH')) {
|
exit;
|
}
|
|
class BlueSky extends Integrations
|
{
|
protected string $api_base = 'https://bsky.social/xrpc/';
|
protected int $max_description_length = 300;
|
protected ?string $access_token = null;
|
protected ?string $did = null;
|
|
public function __construct(?int $user_id = null) {
|
$this->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' => '<strong>Security:</strong> 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 [];
|
}
|
}
|